mirror of
git://f0xx.org/android_cast
synced 2026-07-29 06:58:51 +03:00
sync BE
This commit is contained in:
@@ -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());
|
||||
|
||||
@@ -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); });
|
||||
}
|
||||
|
||||
@@ -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<string, mixed> */
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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)',
|
||||
];
|
||||
?>
|
||||
<h1>Build <?= h($build['build_code'] ?? '') ?></h1>
|
||||
<p class="muted">Status: <strong><?= h($status) ?></strong> · Phase: <?= h($build['phase'] ?? '') ?>
|
||||
<p class="muted">Status: <strong><?= h($status) ?></strong> · Phase: <?= h($phase) ?>
|
||||
<?php if (!empty($build['error_message'])): ?>
|
||||
· <span class="error-text"><?= h($build['error_message']) ?></span>
|
||||
<?php endif; ?>
|
||||
@@ -21,17 +32,34 @@ if ($status === 'failed' && $dockerFail && $healthDocker):
|
||||
?>
|
||||
<p class="muted graphs-footnote">Ecosystem health reports <strong>Docker: ok</strong> now. The log below is from this failed run — use <strong>Re-run</strong> (do not only refresh).</p>
|
||||
<?php endif; ?>
|
||||
<?php if ($canRerunFromFailed && $failedStep !== ''): ?>
|
||||
<p class="muted graphs-footnote">Failed at step: <strong><?= h($stepLabels[$failedStep] ?? $failedStep) ?></strong> — <em>Re-run from failed</em> skips earlier steps when artifacts allow (like CircleCI).</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="build-actions card card--lift" style="margin-bottom:12px">
|
||||
<h2>Actions</h2>
|
||||
<div class="toolbar-actions" style="flex-wrap:wrap;gap:8px">
|
||||
<a class="btn" href="<?= h(Auth::basePath()) ?>/">← Back</a>
|
||||
<button type="button" class="btn" id="build-rerun-btn" data-build-id="<?= (int) $build['id'] ?>">Re-run</button>
|
||||
<?php if ($canRerunFromFailed): ?>
|
||||
<button type="button" class="btn" id="build-rerun-failed-btn" data-build-id="<?= (int) $build['id'] ?>" title="Resume from <?= h($failedStep) ?>">Re-run from failed</button>
|
||||
<button type="button" class="btn" id="build-rerun-ssh-btn" data-build-id="<?= (int) $build['id'] ?>" title="Resume from failed step, then open an interactive container (CircleCI-style SSH debug)">Re-run with SSH</button>
|
||||
<?php endif; ?>
|
||||
<button type="button" class="btn" id="build-rerun-as-toggle">Re-run as…</button>
|
||||
<?php if ($isRunning): ?>
|
||||
<button type="button" class="btn btn-danger" id="build-stop-btn" data-build-id="<?= (int) $build['id'] ?>">Stop</button>
|
||||
<button type="button" class="btn btn-danger" id="build-stop-btn" data-build-id="<?= (int) $build['id'] ?>"><?= $isSshDebug ? 'End SSH session' : 'Stop' ?></button>
|
||||
<?php endif; ?>
|
||||
<span id="build-action-status" class="muted"></span>
|
||||
</div>
|
||||
<?php if ($isSshDebug): ?>
|
||||
<div class="build-ssh-panel" id="build-ssh-panel">
|
||||
<strong>SSH debug session</strong>
|
||||
<p class="muted graphs-footnote" style="margin:6px 0 0">Shell into the CI container on the builder host (same idea as CircleCI “Rerun with SSH”). Stop ends the session.</p>
|
||||
<pre id="build-ssh-command">ssh <user>@<?= h($builderHost) ?>
|
||||
|
||||
docker exec -it <?= h($sshContainer) ?> bash</pre>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<form id="build-rerun-as-form" class="build-form" hidden style="margin-top:12px">
|
||||
<div class="build-form-grid">
|
||||
<label>Git ref<input name="git_ref" value="<?= h($params['git_ref'] ?? $build['git_ref'] ?? 'next') ?>"></label>
|
||||
|
||||
Reference in New Issue
Block a user