1
0
mirror of git://f0xx.org/android_cast synced 2026-07-29 04:57:40 +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); $build = BuildRunner::rerunBuild($buildId, $override, $userId);
json_out(['ok' => true, 'build' => $build]); 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); json_out(['ok' => false, 'error' => 'unknown_action'], 400);
} catch (Throwable $e) { } catch (Throwable $e) {
error_log('build_actions: ' . $e->getMessage()); error_log('build_actions: ' . $e->getMessage());

View File

@@ -79,29 +79,43 @@
} }
var rerunBtn = document.getElementById('build-rerun-btn'); 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 stopBtn = document.getElementById('build-stop-btn');
var rerunAsToggle = document.getElementById('build-rerun-as-toggle'); var rerunAsToggle = document.getElementById('build-rerun-as-toggle');
var rerunAsForm = document.getElementById('build-rerun-as-form'); var rerunAsForm = document.getElementById('build-rerun-as-form');
var actionStatus = document.getElementById('build-action-status'); 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) { if (rerunAsToggle && rerunAsForm) {
rerunAsToggle.addEventListener('click', function () { rerunAsToggle.addEventListener('click', function () {
rerunAsForm.hidden = !rerunAsForm.hidden; rerunAsForm.hidden = !rerunAsForm.hidden;
}); });
} }
if (rerunBtn) { bindRerunAction(rerunBtn, 'rerun', null);
rerunBtn.addEventListener('click', function () { bindRerunAction(rerunFailedBtn, 'rerun_from_failed', null);
var id = Number(rerunBtn.getAttribute('data-build-id')); bindRerunAction(
if (!id) return; rerunSshBtn,
if (actionStatus) actionStatus.textContent = 'Re-running…'; 'rerun_with_ssh',
postBuildAction({ action: 'rerun', build_id: id }).then(function (j) { 'Start an SSH debug session on the builder? You will need shell access to the builder host.'
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) { if (stopBtn) {
stopBtn.addEventListener('click', function () { stopBtn.addEventListener('click', function () {
var id = Number(stopBtn.getAttribute('data-build-id')); var id = Number(stopBtn.getAttribute('data-build-id'));
@@ -170,6 +184,8 @@
var eof = j.log && j.log.eof; var eof = j.log && j.log.eof;
if (running || !eof) { if (running || !eof) {
setTimeout(poll, running ? 1000 : 250); 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); }); }).catch(function () { setTimeout(poll, 2000); });
} }

View File

@@ -130,9 +130,10 @@ YAML;
return BuildRepository::getById($id) ?? ['id' => $id, 'build_code' => $buildCode, 'status' => 'failed']; return BuildRepository::getById($id) ?? ['id' => $id, 'build_code' => $buildCode, 'status' => 'failed'];
} }
$phase = !empty($params['ssh_debug']) ? 'ssh_debug' : 'docker';
BuildRepository::update($id, [ BuildRepository::update($id, [
'status' => 'running', 'status' => 'running',
'phase' => 'docker', 'phase' => $phase,
'started_at' => gmdate('Y-m-d H:i:s'), 'started_at' => gmdate('Y-m-d H:i:s'),
]); ]);
@@ -163,6 +164,16 @@ YAML;
'OTA_CHANNEL=' . escapeshellarg((string) ($params['ota_channel'] ?? 'staging')), 'OTA_CHANNEL=' . escapeshellarg((string) ($params['ota_channel'] ?? 'staging')),
'AUTO_OTA=' . (!empty($params['auto_ota']) ? '1' : '0'), '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 $!'; $cmd = implode(' ', $env) . ' nohup ' . escapeshellarg($runner) . ' >> ' . escapeshellarg($logPath) . ' 2>&1 & echo $!';
$pid = trim((string) shell_exec($cmd)); $pid = trim((string) shell_exec($cmd));
file_put_contents($dir . '/runner.pid', $pid); file_put_contents($dir . '/runner.pid', $pid);
@@ -193,6 +204,7 @@ YAML;
if ($pid !== '' && ctype_digit($pid)) { if ($pid !== '' && ctype_digit($pid)) {
shell_exec('kill ' . escapeshellarg($pid) . ' 2>/dev/null'); shell_exec('kill ' . escapeshellarg($pid) . ' 2>/dev/null');
} }
self::stopDebugContainer($id);
$logPath = $build['log_path'] ?? ($dir . '/build.log'); $logPath = $build['log_path'] ?? ($dir . '/build.log');
if ($logPath) { if ($logPath) {
file_put_contents($logPath, "\n[builder] Stopped by operator\n", FILE_APPEND); file_put_contents($logPath, "\n[builder] Stopped by operator\n", FILE_APPEND);
@@ -222,10 +234,78 @@ YAML;
if (empty($params['git_ref'])) { if (empty($params['git_ref'])) {
$params['git_ref'] = $build['git_ref'] ?? $build['branch'] ?? 'next'; $params['git_ref'] = $build['git_ref'] ?? $build['branch'] ?? 'next';
} }
if (!isset($params['rerun_of'])) {
$params['rerun_of'] = $id; $params['rerun_of'] = $id;
}
return self::enqueue($params, $userId); 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 { public static function runnerAlive(int $buildId): bool {
$pidFile = self::artifactDir($buildId) . '/runner.pid'; $pidFile = self::artifactDir($buildId) . '/runner.pid';
$pid = is_file($pidFile) ? trim((string) file_get_contents($pidFile)) : ''; $pid = is_file($pidFile) ? trim((string) file_get_contents($pidFile)) : '';
@@ -258,6 +338,15 @@ YAML;
$size = filesize($logPath) ?: 0; $size = filesize($logPath) ?: 0;
$tail = (string) file_get_contents($logPath, false, null, max(0, $size - 8192)); $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)) { if (self::runnerAlive($id)) {
continue; continue;
} }

View File

@@ -2,14 +2,25 @@
$artifacts = json_decode($build['artifacts_json'] ?? '{}', true) ?: []; $artifacts = json_decode($build['artifacts_json'] ?? '{}', true) ?: [];
$params = json_decode($build['params_json'] ?? '{}', true) ?: []; $params = json_decode($build['params_json'] ?? '{}', true) ?: [];
$status = (string) ($build['status'] ?? ''); $status = (string) ($build['status'] ?? '');
$phase = (string) ($build['phase'] ?? '');
$isRunning = $status === 'running'; $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'] ?? '')); $pipelineYaml = trim((string) ($build['pipeline_yaml'] ?? ''));
if ($pipelineYaml === '') { if ($pipelineYaml === '') {
$pipelineYaml = '(none — will be generated on next trigger from build.config.yml or params)'; $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> <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'])): ?> <?php if (!empty($build['error_message'])): ?>
· <span class="error-text"><?= h($build['error_message']) ?></span> · <span class="error-text"><?= h($build['error_message']) ?></span>
<?php endif; ?> <?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> <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 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"> <div class="build-actions card card--lift" style="margin-bottom:12px">
<h2>Actions</h2> <h2>Actions</h2>
<div class="toolbar-actions" style="flex-wrap:wrap;gap:8px"> <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> <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> <button type="button" class="btn" id="build-rerun-as-toggle">Re-run as…</button>
<?php if ($isRunning): ?> <?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; ?> <?php endif; ?>
<span id="build-action-status" class="muted"></span> <span id="build-action-status" class="muted"></span>
</div> </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"> <form id="build-rerun-as-form" class="build-form" hidden style="margin-top:12px">
<div class="build-form-grid"> <div class="build-form-grid">
<label>Git ref<input name="git_ref" value="<?= h($params['git_ref'] ?? $build['git_ref'] ?? 'next') ?>"></label> <label>Git ref<input name="git_ref" value="<?= h($params['git_ref'] ?? $build['git_ref'] ?? 'next') ?>"></label>

View File

@@ -499,12 +499,26 @@ a:hover { text-decoration: underline; }
} }
.login-card button { .login-card button {
padding: 12px; padding: 12px;
border: none; border: 1px solid var(--accent);
border-radius: 8px; border-radius: 8px;
background: var(--accent); background: var(--accent);
color: #fff; color: #fff;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
box-shadow:
0 0 0 1px rgba(61, 139, 253, .25),
0 4px 14px rgba(0, 0, 0, .3),
0 0 20px rgba(61, 139, 253, .35);
transition: box-shadow .18s ease, filter .18s ease, transform .12s ease;
}
.login-card button:hover,
.login-card button:focus-visible {
filter: brightness(1.08);
box-shadow:
0 0 0 1px rgba(61, 139, 253, .55),
0 6px 18px rgba(0, 0, 0, .34),
0 0 28px rgba(61, 139, 253, .48);
outline: none;
} }
.toolbar { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px; } .toolbar { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px; }
.btn { .btn {
@@ -516,8 +530,53 @@ a:hover { text-decoration: underline; }
cursor: pointer; cursor: pointer;
background: var(--surface); background: var(--surface);
color: var(--text); color: var(--text);
text-decoration: none;
font: inherit;
line-height: 1.25;
box-shadow:
0 0 0 1px rgba(61, 139, 253, .12),
0 2px 10px rgba(0, 0, 0, .28),
0 0 18px rgba(61, 139, 253, .08);
transition:
border-color .18s ease,
box-shadow .18s ease,
transform .12s ease,
filter .18s ease,
background .18s ease;
}
[data-theme="light"] .btn {
box-shadow:
0 0 0 1px rgba(37, 99, 235, .14),
0 2px 8px rgba(15, 23, 42, .12),
0 0 16px rgba(37, 99, 235, .1);
}
.btn:hover,
.btn:focus-visible {
border-color: var(--accent);
text-decoration: none;
box-shadow:
0 0 0 1px rgba(61, 139, 253, .45),
0 4px 16px rgba(0, 0, 0, .32),
0 0 22px rgba(61, 139, 253, .38);
outline: none;
}
[data-theme="light"] .btn:hover,
[data-theme="light"] .btn:focus-visible {
box-shadow:
0 0 0 1px rgba(37, 99, 235, .5),
0 4px 14px rgba(15, 23, 42, .16),
0 0 20px rgba(37, 99, 235, .28);
}
.btn:active {
transform: translateY(1px);
} }
.btn.active { background: var(--accent); border-color: var(--accent); color: #fff; } .btn.active { background: var(--accent); border-color: var(--accent); color: #fff; }
.btn:disabled,
.btn[aria-disabled="true"] {
opacity: .55;
cursor: not-allowed;
box-shadow: 0 1px 4px rgba(0, 0, 0, .15);
}
.data-table { width: 100%; border-collapse: collapse; margin-top: 16px; font-size: 14px; } .data-table { width: 100%; border-collapse: collapse; margin-top: 16px; font-size: 14px; }
.data-table th, .data-table td { border-bottom: 1px solid var(--border); padding: 10px 8px; text-align: left; } .data-table th, .data-table td { border-bottom: 1px solid var(--border); padding: 10px 8px; text-align: left; }
.reports-tree .report-tree-head { width: 2.5rem; padding: 0; } .reports-tree .report-tree-head { width: 2.5rem; padding: 0; }
@@ -1087,8 +1146,65 @@ button.report-tag--filter:hover {
background: var(--accent); background: var(--accent);
border-color: var(--accent); border-color: var(--accent);
color: #fff; color: #fff;
box-shadow:
0 0 0 1px rgba(61, 139, 253, .35),
0 3px 12px rgba(0, 0, 0, .3),
0 0 22px rgba(61, 139, 253, .32);
}
.btn-primary:hover,
.btn-primary:focus-visible {
filter: brightness(1.08);
box-shadow:
0 0 0 1px rgba(61, 139, 253, .6),
0 5px 18px rgba(0, 0, 0, .34),
0 0 30px rgba(61, 139, 253, .5);
}
.btn-danger {
background: rgba(248, 113, 113, .14);
border-color: var(--danger);
color: var(--danger);
box-shadow:
0 0 0 1px rgba(248, 113, 113, .28),
0 3px 12px rgba(0, 0, 0, .28),
0 0 18px rgba(248, 113, 113, .22);
}
[data-theme="light"] .btn-danger {
background: rgba(220, 38, 38, .08);
box-shadow:
0 0 0 1px rgba(220, 38, 38, .3),
0 3px 10px rgba(15, 23, 42, .12),
0 0 16px rgba(220, 38, 38, .18);
}
.btn-danger:hover,
.btn-danger:focus-visible {
background: rgba(248, 113, 113, .22);
box-shadow:
0 0 0 1px rgba(248, 113, 113, .55),
0 5px 16px rgba(0, 0, 0, .32),
0 0 26px rgba(248, 113, 113, .42);
}
.build-actions .toolbar-actions .btn,
.build-actions .toolbar-actions a.btn,
.build-log-actions .btn,
.build-log-actions a.btn {
margin-left: 0;
}
.build-ssh-panel {
margin-top: 12px;
padding: 12px 14px;
border-radius: 8px;
border: 1px solid var(--accent);
background: rgba(61, 139, 253, .08);
box-shadow: 0 0 18px rgba(61, 139, 253, .15);
}
.build-ssh-panel pre {
margin: 8px 0 0;
padding: 10px 12px;
background: var(--surface2);
border-radius: 6px;
overflow-x: auto;
font-size: 13px;
} }
.btn-primary:hover { filter: brightness(1.08); }
.tag-edit-btn { .tag-edit-btn {
flex-shrink: 0; flex-shrink: 0;

View File

@@ -25,6 +25,10 @@ CLONE_DEPTH="${BUILD_GIT_DEPTH:-1}"
SOURCE_DIR="${OUT_DIR}/src" SOURCE_DIR="${OUT_DIR}/src"
DOCKER_WORKSPACE="${WORK_DIR}" DOCKER_WORKSPACE="${WORK_DIR}"
CONTAINER_NAME="androidcast-bld-${BUILD_ID}" CONTAINER_NAME="androidcast-bld-${BUILD_ID}"
RESUME_FROM="${BUILD_RESUME_FROM:-}"
REUSE_SRC_DIR="${BUILD_REUSE_SRC_DIR:-}"
SSH_DEBUG="${BUILD_SSH_DEBUG:-0}"
SSH_DEBUG_SECONDS="${BUILD_SSH_DEBUG_SECONDS:-7200}"
mkdir -p "$(dirname "$LOG_FILE")" "$OUT_DIR" mkdir -p "$(dirname "$LOG_FILE")" "$OUT_DIR"
@@ -136,22 +140,89 @@ sync_shared_checkout() {
run_logged git_safe -C "${WORK_DIR}" rev-parse HEAD run_logged git_safe -C "${WORK_DIR}" rev-parse HEAD
} }
log "[runner] image=${IMAGE} ci_version=${CI_VERSION} build_id=${BUILD_ID} container=${CONTAINER_NAME}" should_skip_git() {
log "[runner] workdir=${WORK_DIR} out=${OUT_DIR} isolate=${ISOLATE_SOURCE}" [[ "$RESUME_FROM" == "docker-build" || "$RESUME_FROM" == "docker-run" ]]
}
if [[ "$ISOLATE_SOURCE" == "1" ]]; then should_skip_docker_build() {
[[ "$RESUME_FROM" == "docker-run" ]] && docker image inspect "$IMAGE" >/dev/null 2>&1
}
reuse_source_tree() {
if [[ -n "$REUSE_SRC_DIR" && -d "${REUSE_SRC_DIR}/.git" ]]; then
log "[runner] reuse: source tree from ${REUSE_SRC_DIR}"
rm -rf "${SOURCE_DIR}"
mkdir -p "${SOURCE_DIR}"
cp -a "${REUSE_SRC_DIR}/." "${SOURCE_DIR}/"
DOCKER_WORKSPACE="${SOURCE_DIR}"
return 0
fi
return 1
}
run_git_step() {
if [[ "$ISOLATE_SOURCE" == "1" ]]; then
prepare_isolated_source prepare_isolated_source
else else
sync_shared_checkout sync_shared_checkout
DOCKER_WORKSPACE="${WORK_DIR}"
fi
}
start_ssh_debug_container() {
log "[runner] SSH debug: starting container ${CONTAINER_NAME} (${SSH_DEBUG_SECONDS}s)"
docker rm -f "${CONTAINER_NAME}" 2>/dev/null || true
run_step docker-run-ssh docker run -d --name "${CONTAINER_NAME}" \
-v "${DOCKER_WORKSPACE}:/workspace" \
-v "${ROOT}/out:/workspace/out" \
-w /workspace \
-e BUILD_ID="${BUILD_ID}" \
-e OUT_DIR="/workspace/out/builds/${BUILD_ID}" \
"$IMAGE" \
sleep "${SSH_DEBUG_SECONDS}"
printf '%s\n' 'ssh_debug' > "${OUT_DIR}/runner.mode"
local host
host="$(hostname -f 2>/dev/null || hostname 2>/dev/null || echo builder)"
log "[runner] SSH debug: session open on ${host}"
log "[runner] SSH debug: ssh <you>@${host}"
log "[runner] SSH debug: docker exec -it ${CONTAINER_NAME} bash"
log "[runner] SSH debug: workspace is /workspace inside the container"
log "[runner] finished build_id=${BUILD_ID} (ssh debug — stop build or wait ${SSH_DEBUG_SECONDS}s)"
}
log "[runner] image=${IMAGE} ci_version=${CI_VERSION} build_id=${BUILD_ID} container=${CONTAINER_NAME}"
log "[runner] workdir=${WORK_DIR} out=${OUT_DIR} isolate=${ISOLATE_SOURCE} resume=${RESUME_FROM:-full}"
if should_skip_git; then
if reuse_source_tree; then
log "[runner] resume: skipped git (reused source)"
elif [[ "$ISOLATE_SOURCE" != "1" && -d "${WORK_DIR}/.git" ]]; then
DOCKER_WORKSPACE="${WORK_DIR}"
log "[runner] resume: skipped git (shared workdir ${WORK_DIR})"
else
log "[runner] resume: no reusable source; running git step"
run_git_step
fi
else
run_git_step
fi fi
log "[runner] docker workspace=${DOCKER_WORKSPACE}" log "[runner] docker workspace=${DOCKER_WORKSPACE}"
run_step docker-build docker build \ if should_skip_docker_build; then
log "[runner] resume: skipped docker-build (image ${IMAGE} exists)"
else
run_step docker-build docker build \
--build-arg ANDROIDCAST_CI_VERSION="${CI_VERSION}" \ --build-arg ANDROIDCAST_CI_VERSION="${CI_VERSION}" \
-t "$IMAGE" \ -t "$IMAGE" \
-f "${ROOT}/Dockerfile" \ -f "${ROOT}/Dockerfile" \
"${ROOT}" "${ROOT}"
fi
if [[ "$SSH_DEBUG" == "1" ]]; then
start_ssh_debug_container
exit 0
fi
run_step docker-run docker run --rm --name "${CONTAINER_NAME}" \ run_step docker-run docker run --rm --name "${CONTAINER_NAME}" \
-v "${DOCKER_WORKSPACE}:/workspace" \ -v "${DOCKER_WORKSPACE}:/workspace" \