mirror of
git://f0xx.org/ac/ac-ms-build
synced 2026-07-29 03:38:09 +03:00
feat(build): error sanitizer and failed-build notifier
Signed-off-by: Anton Afanasyeu <a.afanasieff@gmail.com>
This commit is contained in:
@@ -94,6 +94,9 @@ YAML;
|
||||
|
||||
public static function enqueue(array $params, ?int $userId): array {
|
||||
BuildRepository::ensureSchema();
|
||||
if (!isset($params['trigger_source'])) {
|
||||
$params['trigger_source'] = $userId !== null ? 'manual' : 'agent';
|
||||
}
|
||||
$params['pipeline_yaml'] = $params['pipeline_yaml'] ?? self::resolvePipelineYaml($params);
|
||||
$buildCode = self::generateBuildCode();
|
||||
$id = BuildRepository::create([
|
||||
@@ -138,6 +141,8 @@ YAML;
|
||||
. ' -C ' . escapeshellarg($repo)
|
||||
. ' remote get-url origin 2>/dev/null'
|
||||
));
|
||||
$mobileSub = trim((string) cfg('build.mobile_subdir', 'ac-mobile-android'));
|
||||
$scriptsDir = trim((string) cfg('build.scripts_dir', dirname((string) cfg('build.runner_script', ''))));
|
||||
$env = [
|
||||
'BUILD_ID=' . $id,
|
||||
'BUILD_LOG_FILE=' . escapeshellarg($logPath),
|
||||
@@ -146,6 +151,9 @@ YAML;
|
||||
'BUILD_GIT_DEPTH=' . max(1, (int) cfg('build.git_clone_depth', 1)),
|
||||
'BUILD_WORK_DIR=' . escapeshellarg($repo),
|
||||
'BUILD_OUT_DIR=' . escapeshellarg($dir),
|
||||
'BUILD_MOBILE_SUBDIR=' . escapeshellarg($mobileSub),
|
||||
'BUILD_SCRIPTS_DIR=' . escapeshellarg($scriptsDir !== '' ? $scriptsDir : dirname($runner)),
|
||||
'BUILD_DOCKERFILE=' . escapeshellarg((string) cfg('build.dockerfile', '')),
|
||||
'ANDROIDCAST_CI_VERSION=' . escapeshellarg((string) cfg('build.ci_version', '00.01.00.1000')),
|
||||
'GIT_REF=' . escapeshellarg((string) ($params['git_ref'] ?? '')),
|
||||
'GIT_SHA=' . escapeshellarg((string) ($params['git_sha'] ?? '')),
|
||||
@@ -185,15 +193,20 @@ YAML;
|
||||
}
|
||||
|
||||
public static function failBuild(int $id, string $message, ?string $logPath = null): void {
|
||||
$safe = BuildErrorSanitizer::sanitize($message);
|
||||
if ($logPath) {
|
||||
file_put_contents($logPath, "\n[builder] ERROR: {$message}\n", FILE_APPEND);
|
||||
file_put_contents($logPath, "\n[builder] ERROR: {$safe}\n", FILE_APPEND);
|
||||
}
|
||||
BuildRepository::update($id, [
|
||||
'status' => 'failed',
|
||||
'phase' => 'error',
|
||||
'error_message' => substr($message, 0, 512),
|
||||
'error_message' => substr($safe, 0, 512),
|
||||
'finished_at' => gmdate('Y-m-d H:i:s'),
|
||||
]);
|
||||
$build = BuildRepository::getById($id);
|
||||
if ($build) {
|
||||
BuildNotifier::onFailure($id, $build, $safe);
|
||||
}
|
||||
}
|
||||
|
||||
public static function stopBuild(int $id): bool {
|
||||
@@ -276,6 +289,14 @@ YAML;
|
||||
|
||||
/** CircleCI-style: resume from the step that failed (git → docker-build → docker-run). */
|
||||
public static function detectFailedStep(int $buildId): string {
|
||||
$build = BuildRepository::getById($buildId);
|
||||
if ($build) {
|
||||
$err = (string) ($build['error_message'] ?? '');
|
||||
$started = (string) ($build['started_at'] ?? '');
|
||||
if ($started === '' && str_contains($err, 'Docker daemon not available')) {
|
||||
return 'docker-setup';
|
||||
}
|
||||
}
|
||||
$dir = self::artifactDir($buildId);
|
||||
$logTail = '';
|
||||
$logPath = $dir . '/build.log';
|
||||
@@ -456,7 +477,7 @@ YAML;
|
||||
if (strlen($summary) > 480) {
|
||||
$summary = '…' . substr($summary, -477);
|
||||
}
|
||||
return 'Build runner exited unexpectedly: ' . $summary;
|
||||
return BuildErrorSanitizer::sanitize('Build runner exited unexpectedly: ' . $summary);
|
||||
}
|
||||
|
||||
public static function readLogTail(?string $logPath, int $offset = 0, int $maxBytes = 65536): array {
|
||||
@@ -508,13 +529,27 @@ YAML;
|
||||
$versionApp = $info['gitSha'] ?? null;
|
||||
}
|
||||
}
|
||||
BuildRepository::update($buildId, [
|
||||
$update = [
|
||||
'status' => $status,
|
||||
'phase' => 'done',
|
||||
'artifacts_json' => json_encode($artifacts, JSON_UNESCAPED_SLASHES),
|
||||
'version_app' => $versionApp,
|
||||
'finished_at' => gmdate('Y-m-d H:i:s'),
|
||||
]);
|
||||
];
|
||||
if ($status === 'failed') {
|
||||
$update['error_message'] = substr(
|
||||
BuildErrorSanitizer::sanitize('Build finished without APK artifact'),
|
||||
0,
|
||||
512
|
||||
);
|
||||
}
|
||||
BuildRepository::update($buildId, $update);
|
||||
if ($status === 'failed') {
|
||||
$build = BuildRepository::getById($buildId);
|
||||
if ($build) {
|
||||
BuildNotifier::onFailure($buildId, $build, (string) ($update['error_message'] ?? 'Build failed'));
|
||||
}
|
||||
}
|
||||
if (!empty($artifacts['ota']) && cfg('build.ota_mount')) {
|
||||
$build = BuildRepository::getById($buildId);
|
||||
if (!empty($build['auto_deploy'])) {
|
||||
@@ -533,19 +568,86 @@ YAML;
|
||||
return;
|
||||
}
|
||||
$build = BuildRepository::getById($buildId);
|
||||
$channel = $build['ota_channel'] ?? 'staging';
|
||||
$channel = (string) ($build['ota_channel'] ?? 'staging');
|
||||
$dest = $mount . '/v0';
|
||||
shell_exec('mkdir -p ' . escapeshellarg($dest) . ' && cp -a ' . escapeshellarg($src . '/.') . ' ' . escapeshellarg($dest . '/'));
|
||||
if ($channel !== '' && $channel !== 'staging') {
|
||||
$channelJson = $src . '/ota/channel/' . $channel . '.json';
|
||||
if (is_file($channelJson)) {
|
||||
shell_exec(
|
||||
'cp -a '
|
||||
. escapeshellarg($channelJson)
|
||||
. ' '
|
||||
. escapeshellarg($dest . '/ota/channel/' . $channel . '.json')
|
||||
);
|
||||
$channelJson = $src . '/ota/channel/' . $channel . '.json';
|
||||
if (is_file($channelJson)) {
|
||||
shell_exec(
|
||||
'mkdir -p '
|
||||
. escapeshellarg($dest . '/ota/channel')
|
||||
. ' && cp -a '
|
||||
. escapeshellarg($channelJson)
|
||||
. ' '
|
||||
. escapeshellarg($dest . '/ota/channel/' . $channel . '.json')
|
||||
);
|
||||
}
|
||||
self::publishBrowserApk($src, $channel);
|
||||
}
|
||||
|
||||
/** Copy installable .apk to hub /v0/downloads/ for browser sideload. */
|
||||
private static function publishBrowserApk(string $otaV0Dir, string $channel): void {
|
||||
$downloads = rtrim((string) cfg('build.downloads_mount', ''), '/');
|
||||
if ($downloads === '') {
|
||||
$parent = dirname(rtrim((string) cfg('build.ota_mount', ''), '/'));
|
||||
$downloads = $parent !== '' && $parent !== '.'
|
||||
? $parent . '/downloads'
|
||||
: '/var/www/localhost/htdocs/apps/app/androidcast_project/downloads';
|
||||
}
|
||||
if (!is_dir($downloads) && !@mkdir($downloads, 0775, true) && !is_dir($downloads)) {
|
||||
return;
|
||||
}
|
||||
$apkPath = self::resolveBrowserApkPath($otaV0Dir, $channel);
|
||||
if ($apkPath === null || !is_file($apkPath)) {
|
||||
return;
|
||||
}
|
||||
$latest = $downloads . '/android_cast-latest.apk';
|
||||
$versioned = $downloads . '/' . basename($apkPath);
|
||||
@copy($apkPath, $latest);
|
||||
@copy($apkPath, $versioned);
|
||||
@chown($latest, 'nginx');
|
||||
@chgrp($latest, 'nginx');
|
||||
@chown($versioned, 'nginx');
|
||||
@chgrp($versioned, 'nginx');
|
||||
}
|
||||
|
||||
private static function resolveBrowserApkPath(string $otaV0Dir, string $channel): ?string {
|
||||
$channelFile = $otaV0Dir . '/ota/channel/' . $channel . '.json';
|
||||
if (is_file($channelFile)) {
|
||||
$channelData = json_decode((string) file_get_contents($channelFile), true);
|
||||
$manifestUrl = is_array($channelData) ? (string) ($channelData['manifestUrl'] ?? '') : '';
|
||||
if ($manifestUrl !== '') {
|
||||
$manifestPath = self::otaUrlToLocalPath($manifestUrl, $otaV0Dir);
|
||||
if ($manifestPath !== null && is_file($manifestPath)) {
|
||||
$manifest = json_decode((string) file_get_contents($manifestPath), true);
|
||||
if (is_array($manifest)) {
|
||||
$browser = (string) ($manifest['browserApkUrl'] ?? '');
|
||||
if ($browser !== '') {
|
||||
$local = self::otaUrlToLocalPath($browser, $otaV0Dir);
|
||||
if ($local !== null && is_file($local)) {
|
||||
return $local;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$glob = glob($otaV0Dir . '/ota/*/*/*/*.apk') ?: [];
|
||||
if ($glob !== []) {
|
||||
usort($glob, static fn(string $a, string $b): int => filemtime($b) <=> filemtime($a));
|
||||
return $glob[0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static function otaUrlToLocalPath(string $url, string $otaV0Dir): ?string {
|
||||
$path = (string) parse_url($url, PHP_URL_PATH);
|
||||
$prefix = '/v0/ota/';
|
||||
if (!str_starts_with($path, $prefix)) {
|
||||
return null;
|
||||
}
|
||||
$rel = substr($path, strlen($prefix));
|
||||
$local = $otaV0Dir . '/ota/' . $rel;
|
||||
return is_file($local) ? $local : null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user