mirror of
git://f0xx.org/ac/ac-be-builder
synced 2026-07-29 02:58:34 +03:00
initial
This commit is contained in:
55
public/api/build_actions.php
Normal file
55
public/api/build_actions.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/../../src/bootstrap.php';
|
||||
Auth::check();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
json_out(['ok' => 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 === 'force_rerun') {
|
||||
$build = BuildRunner::forceRerun($buildId, $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]);
|
||||
}
|
||||
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());
|
||||
json_out(['ok' => false, 'error' => $e->getMessage()], 500);
|
||||
}
|
||||
39
public/api/build_log.php
Normal file
39
public/api/build_log.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/../../src/bootstrap.php';
|
||||
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);
|
||||
}
|
||||
$build = BuildRepository::getById($id);
|
||||
if (!$build) {
|
||||
json_out(['ok' => false, 'error' => 'not found'], 404);
|
||||
}
|
||||
$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);
|
||||
}
|
||||
json_out([
|
||||
'ok' => true,
|
||||
'build' => $build,
|
||||
'log' => $tail,
|
||||
]);
|
||||
27
public/api/build_trigger.php
Normal file
27
public/api/build_trigger.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/../../src/bootstrap.php';
|
||||
Auth::check();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
json_out(['ok' => false, 'error' => 'POST required'], 405);
|
||||
}
|
||||
|
||||
$raw = file_get_contents('php://input') ?: '{}';
|
||||
$params = json_decode($raw, true);
|
||||
if (!is_array($params)) {
|
||||
$params = $_POST;
|
||||
}
|
||||
|
||||
$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;
|
||||
$params['auto_ota'] = !empty($params['auto_ota']);
|
||||
$params['auto_deploy'] = !empty($params['auto_deploy']);
|
||||
$params['ota_channel'] = $params['ota_channel'] ?? 'staging';
|
||||
$params['gradle_task'] = $params['gradle_task'] ?? 'assembleDebug';
|
||||
|
||||
$user = Auth::user();
|
||||
$build = BuildRunner::enqueue($params, isset($user['id']) ? (int) $user['id'] : null);
|
||||
json_out(['ok' => true, 'build' => $build]);
|
||||
7
public/api/builds.php
Normal file
7
public/api/builds.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/../../src/bootstrap.php';
|
||||
Auth::check();
|
||||
BuildRunner::reconcileRunningBuilds();
|
||||
BuildRunner::reconcileQueuedBuilds();
|
||||
json_out(['ok' => true, 'builds' => BuildRepository::listRecent(10)]);
|
||||
4
public/api/health.php
Normal file
4
public/api/health.php
Normal file
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/../../src/bootstrap.php';
|
||||
json_out(['ok' => true, 'health' => BuildRepository::healthSummary()]);
|
||||
51
public/api/heartbeat.php
Normal file
51
public/api/heartbeat.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/../../src/bootstrap.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['error' => 'method_not_allowed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$raw = file_get_contents('php://input') ?: '';
|
||||
$req = json_decode($raw, true);
|
||||
if (!is_array($req)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'invalid_json']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$heartbeat = $req['heartbeat'] ?? [];
|
||||
if (!is_array($heartbeat)) {
|
||||
$heartbeat = [];
|
||||
}
|
||||
|
||||
$type = (string)($heartbeat['type'] ?? 'generic');
|
||||
$srvEpoch = time();
|
||||
$srvTz = date('O');
|
||||
|
||||
$resp = [
|
||||
'heartbeat' => [
|
||||
'type' => $type,
|
||||
'server_date' => $srvEpoch,
|
||||
'server_tz' => $srvTz,
|
||||
],
|
||||
];
|
||||
|
||||
if ($type === 'ntp') {
|
||||
$clientDate = (int)($heartbeat['date'] ?? 0);
|
||||
$clientTz = (string)($heartbeat['tz'] ?? '');
|
||||
$resp['heartbeat']['client_date'] = $clientDate;
|
||||
$resp['heartbeat']['client_tz'] = $clientTz;
|
||||
$resp['heartbeat']['correction_seconds'] = $clientDate > 0 ? ($srvEpoch - $clientDate) : null;
|
||||
$resp['heartbeat']['enabled'] = true;
|
||||
}
|
||||
|
||||
if ($type === 'ip') {
|
||||
$resp['heartbeat']['external_ip'] = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? ($_SERVER['REMOTE_ADDR'] ?? '');
|
||||
}
|
||||
|
||||
echo json_encode($resp, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
355
public/assets/js/builder.js
Normal file
355
public/assets/js/builder.js
Normal file
@@ -0,0 +1,355 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
var base = document.body.getAttribute('data-base-path') || '';
|
||||
|
||||
function buildStatusIconHtml(status) {
|
||||
var s = String(status || '').toLowerCase();
|
||||
if (s === 'running' || s === 'queued') {
|
||||
return '<span class="build-status-icon build-status-icon--running" role="img" aria-label="In progress">'
|
||||
+ '<span class="build-status-spinner"></span></span>';
|
||||
}
|
||||
if (s === 'passed') {
|
||||
return '<span class="build-status-icon build-status-icon--passed" role="img" aria-label="Succeeded"></span>';
|
||||
}
|
||||
return '<span class="build-status-icon build-status-icon--failed" role="img" aria-label="Failed or cancelled">'
|
||||
+ '<span class="build-status-fail-mark" aria-hidden="true">!</span></span>';
|
||||
}
|
||||
|
||||
function updateBuildStatusCell(cell, status, phase) {
|
||||
if (!cell) return;
|
||||
var icon = cell.querySelector('.build-status-icon');
|
||||
var label = cell.querySelector('.build-status-label');
|
||||
var html = buildStatusIconHtml(status);
|
||||
if (icon) {
|
||||
icon.outerHTML = html;
|
||||
} else {
|
||||
cell.insertAdjacentHTML('afterbegin', html);
|
||||
}
|
||||
if (label) {
|
||||
label.textContent = status || '';
|
||||
}
|
||||
if (phase !== undefined) {
|
||||
var row = cell.closest('tr');
|
||||
if (row) {
|
||||
var phaseCell = row.querySelector('.build-phase-cell');
|
||||
if (phaseCell) {
|
||||
phaseCell.textContent = phase || '';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateBuildRow(row, build) {
|
||||
if (!row || !build) return;
|
||||
row.setAttribute('data-build-status', build.status || '');
|
||||
var cell = row.querySelector('[data-build-status-cell="' + build.id + '"]');
|
||||
updateBuildStatusCell(cell, build.status, build.phase);
|
||||
var play = row.querySelector('.build-transport-play');
|
||||
var stop = row.querySelector('.build-transport-stop');
|
||||
var active = build.status === 'running' || build.status === 'queued';
|
||||
if (play) {
|
||||
play.setAttribute('data-build-status', build.status || '');
|
||||
}
|
||||
if (stop) {
|
||||
stop.setAttribute('data-build-status', build.status || '');
|
||||
stop.disabled = !active;
|
||||
}
|
||||
}
|
||||
|
||||
function refreshRecentBuilds() {
|
||||
var tbody = document.getElementById('builds-recent-tbody');
|
||||
if (!tbody) return;
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', base + '/api/builds.php', true);
|
||||
xhr.onload = function () {
|
||||
var j = null;
|
||||
try { j = JSON.parse(xhr.responseText); } catch (e) { j = null; }
|
||||
if (!j || !j.ok || !j.builds) return;
|
||||
j.builds.forEach(function (b) {
|
||||
var row = tbody.querySelector('tr[data-build-id="' + b.id + '"]');
|
||||
updateBuildRow(row, b);
|
||||
});
|
||||
};
|
||||
xhr.send();
|
||||
setTimeout(refreshRecentBuilds, 1000);
|
||||
}
|
||||
|
||||
function bindBuildListTransport() {
|
||||
var tbody = document.getElementById('builds-recent-tbody');
|
||||
if (!tbody) return;
|
||||
tbody.addEventListener('click', function (ev) {
|
||||
var play = ev.target.closest('.build-transport-play');
|
||||
var stop = ev.target.closest('.build-transport-stop');
|
||||
if (play) {
|
||||
var pid = Number(play.getAttribute('data-build-id'));
|
||||
var pst = play.getAttribute('data-build-status') || '';
|
||||
if (!pid) return;
|
||||
var msg = pst === 'running' || pst === 'queued'
|
||||
? 'Force re-run? This stops the current run and starts a new build with the same parameters.'
|
||||
: null;
|
||||
if (msg && !confirm(msg)) return;
|
||||
postBuildAction({ action: 'force_rerun', build_id: pid }).then(function (j) {
|
||||
if (j.build && j.build.id) {
|
||||
window.location.href = base + '/?view=build&id=' + j.build.id;
|
||||
}
|
||||
}).catch(function (err) {
|
||||
alert(err.message || 'Re-run failed');
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (stop) {
|
||||
var sid = Number(stop.getAttribute('data-build-id'));
|
||||
if (!sid || stop.disabled) return;
|
||||
if (!confirm('Stop this build now?')) return;
|
||||
postBuildAction({ action: 'stop', build_id: sid }).then(function () {
|
||||
refreshRecentBuilds();
|
||||
}).catch(function (err) {
|
||||
alert(err.message || 'Stop failed');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (document.body.getAttribute('data-view') === 'home' && document.getElementById('builds-recent-tbody')) {
|
||||
bindBuildListTransport();
|
||||
refreshRecentBuilds();
|
||||
}
|
||||
|
||||
var themeSel = document.getElementById('theme-select');
|
||||
if (themeSel) {
|
||||
var cur = localStorage.getItem('crash_console_theme') || 'dark';
|
||||
themeSel.value = cur;
|
||||
themeSel.addEventListener('change', function () {
|
||||
document.documentElement.setAttribute('data-theme', themeSel.value);
|
||||
localStorage.setItem('crash_console_theme', themeSel.value);
|
||||
});
|
||||
}
|
||||
|
||||
var form = document.getElementById('build-trigger-form');
|
||||
if (form) {
|
||||
form.addEventListener('submit', function (ev) {
|
||||
ev.preventDefault();
|
||||
var status = document.getElementById('build-trigger-status');
|
||||
var fd = new FormData(form);
|
||||
var payload = {
|
||||
git_ref: fd.get('git_ref') || 'next',
|
||||
gradle_task: fd.get('gradle_task') || 'assembleDebug',
|
||||
ota_channel: fd.get('ota_channel') || 'staging',
|
||||
run_tests: !!form.querySelector('[name=run_tests]').checked,
|
||||
run_native: !!form.querySelector('[name=run_native]').checked,
|
||||
run_apk: !!form.querySelector('[name=run_apk]').checked,
|
||||
auto_ota: !!form.querySelector('[name=auto_ota]').checked,
|
||||
auto_deploy: !!form.querySelector('[name=auto_deploy]').checked
|
||||
};
|
||||
status.textContent = 'Starting…';
|
||||
fetch(base + '/api/build_trigger.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) {
|
||||
throw new Error((j && j.error) || ('HTTP ' + r.status));
|
||||
}
|
||||
return j;
|
||||
});
|
||||
}).then(function (j) {
|
||||
if (j.ok && j.build && j.build.id) {
|
||||
window.location.href = base + '/?view=build&id=' + j.build.id;
|
||||
} else {
|
||||
status.textContent = j.error || 'Failed';
|
||||
}
|
||||
}).catch(function (err) {
|
||||
status.textContent = err && err.message ? err.message : 'Network error';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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 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;
|
||||
});
|
||||
}
|
||||
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'));
|
||||
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 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() {
|
||||
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;
|
||||
renderLog();
|
||||
}
|
||||
if (j.build) {
|
||||
var iconEl = document.getElementById('build-status-icon');
|
||||
var textEl = document.getElementById('build-status-text');
|
||||
if (iconEl) {
|
||||
iconEl.innerHTML = buildStatusIconHtml(j.build.status);
|
||||
}
|
||||
if (textEl) {
|
||||
var errEl = document.getElementById('build-error-message');
|
||||
var err = j.build.error_message ? (' · <span class="error-text" id="build-error-message">'
|
||||
+ String(j.build.error_message).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
+ '</span>') : '';
|
||||
textEl.innerHTML = 'Status: <strong>' + (j.build.status || '') + '</strong> · Phase: '
|
||||
+ (j.build.phase || '') + err;
|
||||
}
|
||||
document.body.setAttribute('data-build-status', j.build.status || '');
|
||||
}
|
||||
var running = j.build && j.build.status === 'running';
|
||||
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); });
|
||||
}
|
||||
|
||||
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 () {
|
||||
showFull = true;
|
||||
if (full === '') {
|
||||
loadFullLog().then(function () { renderLog(); });
|
||||
} else {
|
||||
renderLog();
|
||||
}
|
||||
expand.textContent = 'Showing full log';
|
||||
expand.disabled = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
})();
|
||||
88
public/index.php
Normal file
88
public/index.php
Normal file
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/../src/bootstrap.php';
|
||||
|
||||
$uri = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/';
|
||||
$base = Auth::basePath();
|
||||
if ($base !== '' && str_starts_with($uri, $base)) {
|
||||
$uri = substr($uri, strlen($base)) ?: '/';
|
||||
}
|
||||
$route = rtrim($uri, '/') ?: '/';
|
||||
|
||||
if ($route === '/api/health.php' || str_ends_with($route, '/api/health.php')) {
|
||||
require __DIR__ . '/api/health.php';
|
||||
exit;
|
||||
}
|
||||
if ($route === '/api/builds.php' || str_ends_with($route, '/api/builds.php')) {
|
||||
require __DIR__ . '/api/builds.php';
|
||||
exit;
|
||||
}
|
||||
if ($route === '/api/build_trigger.php' || str_ends_with($route, '/api/build_trigger.php')) {
|
||||
require __DIR__ . '/api/build_trigger.php';
|
||||
exit;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
if ($route === '/logout') {
|
||||
Auth::logout();
|
||||
header('Location: ' . $base . '/login');
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($route === '/login' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$user = trim($_POST['username'] ?? '');
|
||||
$pass = $_POST['password'] ?? '';
|
||||
if (Auth::login($user, $pass)) {
|
||||
header('Location: ' . $base . '/');
|
||||
exit;
|
||||
}
|
||||
$loginError = 'Invalid credentials';
|
||||
require __DIR__ . '/../views/login.php';
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($route === '/login') {
|
||||
require __DIR__ . '/../views/login.php';
|
||||
exit;
|
||||
}
|
||||
|
||||
Auth::check();
|
||||
|
||||
BuildRunner::reconcileRunningBuilds();
|
||||
|
||||
$view = $_GET['view'] ?? 'home';
|
||||
|
||||
if ($view === 'build' && isset($_GET['id'])) {
|
||||
$build = BuildRepository::getById((int) $_GET['id']);
|
||||
if (!$build) {
|
||||
http_response_code(404);
|
||||
echo 'Not found';
|
||||
exit;
|
||||
}
|
||||
if ($build['status'] === 'running' && is_file(BuildRunner::artifactDir((int) $build['id']) . '/android_cast-latest.apk')) {
|
||||
BuildRunner::finalizeFromArtifacts((int) $build['id']);
|
||||
$build = BuildRepository::getById((int) $_GET['id']);
|
||||
}
|
||||
$health = BuildRepository::healthSummary();
|
||||
$view = 'build';
|
||||
$pageTitle = 'Build ' . ($build['build_code'] ?? '');
|
||||
require __DIR__ . '/../views/layout.php';
|
||||
exit;
|
||||
}
|
||||
|
||||
$view = 'home';
|
||||
$health = BuildRepository::healthSummary();
|
||||
$builds = BuildRepository::listRecent(10);
|
||||
$pageTitle = 'Builder';
|
||||
require __DIR__ . '/../views/layout.php';
|
||||
Reference in New Issue
Block a user