1
0
mirror of git://f0xx.org/android_cast synced 2026-07-29 06:58:51 +03:00
This commit is contained in:
Anton Afanasyeu
2026-06-05 16:50:08 +02:00
parent 7dc992fb19
commit a27958b430
6 changed files with 358 additions and 30 deletions

View File

@@ -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());

View File

@@ -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); });
}

View File

@@ -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;
}

View File

@@ -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 &lt;user&gt;@<?= 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>