mirror of
git://f0xx.org/android_cast
synced 2026-07-29 05:17:39 +03:00
orchestration beginning
This commit is contained in:
27
Dockerfile
27
Dockerfile
@@ -1,18 +1,25 @@
|
|||||||
# Minimal Android Cast CI image (glibc-based; NDK prebuilts require linux-x86_64 glibc).
|
# Minimal Android Cast CI image (glibc-based; NDK prebuilts require linux-x86_64 glibc).
|
||||||
#
|
#
|
||||||
# Build:
|
# Build:
|
||||||
# docker build -t androidcast-ci:latest -f Dockerfile .
|
# docker build --build-arg ANDROIDCAST_CI_VERSION=00.01.00.1000 -t androidcast-ci:00.01.00.1000 -f Dockerfile .
|
||||||
#
|
#
|
||||||
# Run CI build (mount repo):
|
# Run CI build (mount repo):
|
||||||
# docker run --rm -v "$(pwd)":/workspace -w /workspace androidcast-ci:latest
|
# docker run --rm -v "$(pwd)":/workspace -w /workspace androidcast-ci:latest
|
||||||
#
|
#
|
||||||
# Interactive:
|
# CI image version (AA.BB.CC.DDDD — AA always 00)
|
||||||
# docker run --rm -it -v "$(pwd)":/workspace -w /workspace androidcast-ci:latest bash
|
ARG ANDROIDCAST_CI_MAJOR=00
|
||||||
#
|
ARG ANDROIDCAST_CI_MINOR=01
|
||||||
# Note: Alpine/musl breaks NDK clang prebuilts; use Debian slim instead of rust:* for size.
|
ARG ANDROIDCAST_CI_PATCH=00
|
||||||
|
ARG ANDROIDCAST_CI_BUILD=1000
|
||||||
|
ARG ANDROIDCAST_CI_VERSION=00.01.00.1000
|
||||||
|
|
||||||
FROM debian:bookworm-slim
|
FROM debian:bookworm-slim
|
||||||
|
|
||||||
|
ARG ANDROIDCAST_CI_VERSION
|
||||||
|
LABEL org.androidcast.ci.version="${ANDROIDCAST_CI_VERSION}"
|
||||||
|
LABEL org.opencontainers.image.title="androidcast-ci"
|
||||||
|
LABEL org.opencontainers.image.version="${ANDROIDCAST_CI_VERSION}"
|
||||||
|
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
ARG ANDROID_SDK_ROOT=/opt/android-sdk
|
ARG ANDROID_SDK_ROOT=/opt/android-sdk
|
||||||
ARG ANDROID_NDK_VERSION=27.0.12077973
|
ARG ANDROID_NDK_VERSION=27.0.12077973
|
||||||
@@ -26,6 +33,7 @@ ENV ANDROID_SDK_ROOT="${ANDROID_SDK_ROOT}" \
|
|||||||
PATH="${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin:${PATH}" \
|
PATH="${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin:${PATH}" \
|
||||||
ANDROIDCAST_CCACHE=1 \
|
ANDROIDCAST_CCACHE=1 \
|
||||||
ANDROIDCAST_USE_DISTCC=0 \
|
ANDROIDCAST_USE_DISTCC=0 \
|
||||||
|
ANDROIDCAST_CI_VERSION="${ANDROIDCAST_CI_VERSION}" \
|
||||||
GRADLE_USER_HOME=/root/.gradle \
|
GRADLE_USER_HOME=/root/.gradle \
|
||||||
JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64
|
JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64
|
||||||
|
|
||||||
@@ -34,7 +42,6 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
ninja-build cmake ccache \
|
ninja-build cmake ccache \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Optional distributed compile (off unless ANDROIDCAST_USE_DISTCC=1 + DISTCC_HOSTS at run time).
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends distcc \
|
RUN apt-get update && apt-get install -y --no-install-recommends distcc \
|
||||||
&& rm -rf /var/lib/apt/lists/* || true
|
&& rm -rf /var/lib/apt/lists/* || true
|
||||||
|
|
||||||
@@ -55,13 +62,13 @@ RUN yes | sdkmanager --licenses >/dev/null \
|
|||||||
|
|
||||||
WORKDIR /workspace
|
WORKDIR /workspace
|
||||||
|
|
||||||
# Pre-create ccache dir (volume can override at runtime).
|
|
||||||
RUN mkdir -p /workspace/build/.ccache || true
|
RUN mkdir -p /workspace/build/.ccache || true
|
||||||
|
|
||||||
COPY scripts/ci-build.sh scripts/android-ndk.sh scripts/native-build-cache.sh scripts/build-native-codecs.sh \
|
COPY scripts/ci-build.sh scripts/ci-build-and-publish-ota.sh scripts/generate-ota-v0.sh \
|
||||||
|
scripts/android-ndk.sh scripts/native-build-cache.sh scripts/build-native-codecs.sh \
|
||||||
/workspace/scripts/
|
/workspace/scripts/
|
||||||
|
|
||||||
RUN chmod +x /workspace/scripts/ci-build.sh /workspace/scripts/build-native-codecs.sh
|
RUN chmod +x /workspace/scripts/ci-build.sh /workspace/scripts/ci-build-and-publish-ota.sh \
|
||||||
|
/workspace/scripts/generate-ota-v0.sh /workspace/scripts/build-native-codecs.sh
|
||||||
|
|
||||||
# Full tree is bind-mounted in CI; default command runs the pipeline.
|
|
||||||
CMD ["./scripts/ci-build.sh"]
|
CMD ["./scripts/ci-build.sh"]
|
||||||
|
|||||||
62
docs/BUILD_DEPLOY.md
Normal file
62
docs/BUILD_DEPLOY.md
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# Android Cast build ecosystem
|
||||||
|
|
||||||
|
## Shared authentication
|
||||||
|
|
||||||
|
All web consoles (Crashes, Tickets, Builder) share:
|
||||||
|
|
||||||
|
| Setting | Value |
|
||||||
|
|---------|--------|
|
||||||
|
| Session name | `ac_crash_sess` |
|
||||||
|
| Cookie path | `/app/androidcast_project` |
|
||||||
|
| User store | MariaDB `users` table (same DB as crashes) |
|
||||||
|
| PHP auth | `examples/crash_reporter/backend/src/Auth.php` |
|
||||||
|
|
||||||
|
Sign in once on any console; the session cookie is valid for `/crashes/`, `/build/`, and future apps under the same path prefix.
|
||||||
|
|
||||||
|
Orchestration generates matching configs:
|
||||||
|
|
||||||
|
- `orchestration/runtime/crash-config.php`
|
||||||
|
- `orchestration/runtime/build-config.php`
|
||||||
|
|
||||||
|
Both use `${MARIADB_USER}` / `${MARIADB_PASSWORD}` from `orchestration/.env`.
|
||||||
|
|
||||||
|
## Local stack
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd orchestration
|
||||||
|
./deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
| URL | Purpose |
|
||||||
|
|-----|---------|
|
||||||
|
| `http://localhost:8080/app/androidcast_project/` | Hub landing |
|
||||||
|
| `…/crashes/` | Crash / ticket console |
|
||||||
|
| `…/build/` | APK builder console |
|
||||||
|
| `…/v0/ota/` | Published OTA artifacts (when auto-deploy enabled) |
|
||||||
|
|
||||||
|
Direct debug ports: crashes `:8082`, builder `:8083`.
|
||||||
|
|
||||||
|
## Trigger a build
|
||||||
|
|
||||||
|
1. Open Builder → sign in (default dev user from crash schema seed).
|
||||||
|
2. Set git ref, pipeline options, OTA channel.
|
||||||
|
3. **Start build** → detail page polls log every 1s.
|
||||||
|
4. Artifacts land in `out/builds/<id>/`; optional OTA copy to `orchestration/runtime/ota-artifacts/`.
|
||||||
|
|
||||||
|
## Docker CI
|
||||||
|
|
||||||
|
- Image version label: `00.BB.CC.DDDD` (see root `Dockerfile`).
|
||||||
|
- Host wrapper: `scripts/docker-build-runner.sh`
|
||||||
|
- In-container pipeline: `scripts/ci-build-and-publish-ota.sh`
|
||||||
|
- OTA generator: `scripts/generate-ota-v0.sh [channel]`
|
||||||
|
|
||||||
|
## CircleCI
|
||||||
|
|
||||||
|
- Pipeline description: `build.config.yml`
|
||||||
|
- Local CLI stub: `.circleci/config.yml` (`circleci config validate`)
|
||||||
|
|
||||||
|
## Production notes
|
||||||
|
|
||||||
|
- Mount builder PHP with Docker socket (build host only).
|
||||||
|
- FE nginx: read-only OTA under `/v0/ota/`; BE RWX on artifact mount.
|
||||||
|
- Keep `session_cookie_path` identical across all PHP apps behind the hub.
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<link rel="stylesheet" href="/app/androidcast_project/crashes/assets/css/app.css">
|
<link rel="stylesheet" href="/app/androidcast_project/crashes/assets/css/app.css">
|
||||||
<link rel="stylesheet" href="hub.css?v=20260530h">
|
<link rel="stylesheet" href="hub.css?v=20260530i">
|
||||||
<link rel="stylesheet" href="hub-logo-layers.css">
|
<link rel="stylesheet" href="hub-logo-layers.css">
|
||||||
<script src="assets/analytics.config.js"></script>
|
<script src="assets/analytics.config.js"></script>
|
||||||
<script src="/app/androidcast_project/crashes/assets/js/analytics.js" defer></script>
|
<script src="/app/androidcast_project/crashes/assets/js/analytics.js" defer></script>
|
||||||
@@ -122,10 +122,9 @@
|
|||||||
var dock = document.getElementById('hub-nav-dock');
|
var dock = document.getElementById('hub-nav-dock');
|
||||||
if (!dock) return;
|
if (!dock) return;
|
||||||
var globe = globeCoverRect();
|
var globe = globeCoverRect();
|
||||||
var insetX = Math.max(16, globe.width * 0.06);
|
|
||||||
var dockW = Math.min(300, Math.max(210, globe.width * 0.38));
|
var dockW = Math.min(300, Math.max(210, globe.width * 0.38));
|
||||||
dock.style.width = dockW + 'px';
|
dock.style.width = dockW + 'px';
|
||||||
dock.style.left = (globe.left + insetX) + 'px';
|
dock.style.left = globe.left + 'px';
|
||||||
dock.style.top = globe.top + 'px';
|
dock.style.top = globe.top + 'px';
|
||||||
dock.style.height = globe.height + 'px';
|
dock.style.height = globe.height + 'px';
|
||||||
dock.style.display = 'flex';
|
dock.style.display = 'flex';
|
||||||
@@ -159,7 +158,7 @@
|
|||||||
var supportsLayout = !!(window.CSS && CSS.supports && CSS.supports('object-fit', 'cover'));
|
var supportsLayout = !!(window.CSS && CSS.supports && CSS.supports('object-fit', 'cover'));
|
||||||
if (!supportsSvg || !supportsLayout) return;
|
if (!supportsSvg || !supportsLayout) return;
|
||||||
|
|
||||||
var logoBuild = '20260530h';
|
var logoBuild = '20260530i';
|
||||||
var fragments = {
|
var fragments = {
|
||||||
space: 'assets/hub-logo-fragment-space.svg',
|
space: 'assets/hub-logo-fragment-space.svg',
|
||||||
globe: 'assets/hub-logo-fragment-globe.svg',
|
globe: 'assets/hub-logo-fragment-globe.svg',
|
||||||
@@ -230,10 +229,9 @@
|
|||||||
var dock = document.getElementById('hub-nav-dock');
|
var dock = document.getElementById('hub-nav-dock');
|
||||||
if (!dock) return;
|
if (!dock) return;
|
||||||
var globe = globeCoverRect();
|
var globe = globeCoverRect();
|
||||||
var insetX = Math.max(16, globe.width * 0.06);
|
|
||||||
var dockW = Math.min(300, Math.max(210, globe.width * 0.38));
|
var dockW = Math.min(300, Math.max(210, globe.width * 0.38));
|
||||||
dock.style.width = dockW + 'px';
|
dock.style.width = dockW + 'px';
|
||||||
dock.style.left = (globe.left + insetX) + 'px';
|
dock.style.left = globe.left + 'px';
|
||||||
dock.style.top = globe.top + 'px';
|
dock.style.top = globe.top + 'px';
|
||||||
dock.style.height = globe.height + 'px';
|
dock.style.height = globe.height + 'px';
|
||||||
dock.style.right = 'auto';
|
dock.style.right = 'auto';
|
||||||
|
|||||||
42
examples/build_console/backend/config/config.example.php
Normal file
42
examples/build_console/backend/config/config.example.php
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
return [
|
||||||
|
'app_name' => 'Android Cast Builder',
|
||||||
|
'base_path' => '/app/androidcast_project/build',
|
||||||
|
'session_name' => 'ac_crash_sess',
|
||||||
|
'session_cookie_path' => '/app/androidcast_project',
|
||||||
|
'db' => [
|
||||||
|
'driver' => 'mysql',
|
||||||
|
'sqlite_path' => __DIR__ . '/../../crash_reporter/backend/data/crashes.sqlite',
|
||||||
|
'mysql' => [
|
||||||
|
'host' => 'mariadb',
|
||||||
|
'port' => 3306,
|
||||||
|
'socket' => '',
|
||||||
|
'database' => 'androidcast_crashes',
|
||||||
|
'username' => 'androidcast',
|
||||||
|
'password' => 'androidcast',
|
||||||
|
'charset' => 'utf8mb4',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'debug' => true,
|
||||||
|
'rbac' => [
|
||||||
|
'default_company_slug' => 'default',
|
||||||
|
'default_company_id' => 1,
|
||||||
|
],
|
||||||
|
'build' => [
|
||||||
|
'docker_image' => 'androidcast-ci:latest',
|
||||||
|
'ci_version' => '00.01.00.1000',
|
||||||
|
'repo_root' => '/workspace',
|
||||||
|
'artifacts_root' => '/workspace/out/builds',
|
||||||
|
'ota_mount' => '/workspace/orchestration/runtime/ota-artifacts',
|
||||||
|
'ota_base_url' => 'http://localhost:8080',
|
||||||
|
'runner_script' => '/workspace/scripts/docker-build-runner.sh',
|
||||||
|
'pipeline_config' => '/workspace/build.config.yml',
|
||||||
|
'default_channels' => ['stable', 'staging', 'dev', 'nightly'],
|
||||||
|
],
|
||||||
|
'links' => [
|
||||||
|
'hub' => '/app/androidcast_project/',
|
||||||
|
'crashes' => '/app/androidcast_project/crashes/',
|
||||||
|
'git' => '/app/androidcast_project/git/',
|
||||||
|
'tickets' => '/app/androidcast_project/crashes/?view=tickets',
|
||||||
|
],
|
||||||
|
];
|
||||||
24
examples/build_console/backend/public/api/build_log.php
Normal file
24
examples/build_console/backend/public/api/build_log.php
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
require_once __DIR__ . '/../../src/bootstrap.php';
|
||||||
|
Auth::check();
|
||||||
|
|
||||||
|
$id = (int) ($_GET['id'] ?? 0);
|
||||||
|
$offset = (int) ($_GET['offset'] ?? 0);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
$tail = BuildRunner::readLogTail($build['log_path'] ?? null, $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,
|
||||||
|
]);
|
||||||
28
examples/build_console/backend/public/api/build_trigger.php
Normal file
28
examples/build_console/backend/public/api/build_trigger.php
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?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;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pipelinePath = (string) cfg('build.pipeline_config', '/workspace/build.config.yml');
|
||||||
|
$params['pipeline_yaml'] = is_file($pipelinePath) ? (string) file_get_contents($pipelinePath) : null;
|
||||||
|
$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]);
|
||||||
5
examples/build_console/backend/public/api/builds.php
Normal file
5
examples/build_console/backend/public/api/builds.php
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
require_once __DIR__ . '/../../src/bootstrap.php';
|
||||||
|
Auth::check();
|
||||||
|
json_out(['ok' => true, 'builds' => BuildRepository::listRecent(10)]);
|
||||||
4
examples/build_console/backend/public/api/health.php
Normal file
4
examples/build_console/backend/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()]);
|
||||||
74
examples/build_console/backend/public/assets/js/builder.js
Normal file
74
examples/build_console/backend/public/assets/js/builder.js
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
var base = document.body.getAttribute('data-base-path') || '';
|
||||||
|
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' },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
}).then(function (r) { return r.json(); }).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 () { status.textContent = 'Network error'; });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var buildId = document.body.getAttribute('data-build-id');
|
||||||
|
var logEl = document.getElementById('build-log-tail');
|
||||||
|
if (buildId && logEl) {
|
||||||
|
var offset = 0;
|
||||||
|
var full = '';
|
||||||
|
function poll() {
|
||||||
|
fetch(base + '/api/build_log.php?id=' + encodeURIComponent(buildId) + '&offset=' + offset)
|
||||||
|
.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;
|
||||||
|
logEl.textContent = full.slice(-12000);
|
||||||
|
logEl.scrollTop = logEl.scrollHeight;
|
||||||
|
}
|
||||||
|
if (j.build && j.build.status === 'running') {
|
||||||
|
setTimeout(poll, 1000);
|
||||||
|
}
|
||||||
|
}).catch(function () { setTimeout(poll, 2000); });
|
||||||
|
}
|
||||||
|
poll();
|
||||||
|
var expand = document.getElementById('build-log-expand');
|
||||||
|
if (expand) {
|
||||||
|
expand.addEventListener('click', function () {
|
||||||
|
logEl.textContent = full;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
77
examples/build_console/backend/public/index.php
Normal file
77
examples/build_console/backend/public/index.php
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
<?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 === '/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();
|
||||||
|
|
||||||
|
$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']);
|
||||||
|
}
|
||||||
|
$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';
|
||||||
127
examples/build_console/backend/src/BuildRepository.php
Normal file
127
examples/build_console/backend/src/BuildRepository.php
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
final class BuildRepository {
|
||||||
|
public static function ensureSchema(): void {
|
||||||
|
$pdo = Database::pdo();
|
||||||
|
$pdo->exec(<<<'SQL'
|
||||||
|
CREATE TABLE IF NOT EXISTS builds (
|
||||||
|
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
build_code VARCHAR(64) NOT NULL,
|
||||||
|
status ENUM('queued','running','passed','failed','cancelled') NOT NULL DEFAULT 'queued',
|
||||||
|
phase VARCHAR(64) NOT NULL DEFAULT 'init',
|
||||||
|
git_ref VARCHAR(256) NULL,
|
||||||
|
git_sha VARCHAR(64) NULL,
|
||||||
|
branch VARCHAR(128) NULL,
|
||||||
|
pipeline_yaml LONGTEXT NULL,
|
||||||
|
params_json LONGTEXT NOT NULL,
|
||||||
|
dockerfile_version VARCHAR(32) NOT NULL DEFAULT '00.01.00.1000',
|
||||||
|
builder_id VARCHAR(128) NOT NULL DEFAULT 'local-docker',
|
||||||
|
version_app VARCHAR(32) NULL,
|
||||||
|
version_code INT NULL,
|
||||||
|
target_os VARCHAR(32) NOT NULL DEFAULT 'android',
|
||||||
|
min_api_level INT NOT NULL DEFAULT 24,
|
||||||
|
ota_channel VARCHAR(64) NULL,
|
||||||
|
auto_ota TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
auto_deploy TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
log_path VARCHAR(512) NULL,
|
||||||
|
artifacts_json LONGTEXT NULL,
|
||||||
|
error_message VARCHAR(512) NULL,
|
||||||
|
created_by_user_id INT UNSIGNED NULL,
|
||||||
|
started_at TIMESTAMP NULL,
|
||||||
|
finished_at TIMESTAMP NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE KEY uq_builds_code (build_code),
|
||||||
|
KEY idx_builds_status (status),
|
||||||
|
KEY idx_builds_created (created_at)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||||
|
SQL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function create(array $row): int {
|
||||||
|
self::ensureSchema();
|
||||||
|
$pdo = Database::pdo();
|
||||||
|
$stmt = $pdo->prepare(<<<'SQL'
|
||||||
|
INSERT INTO builds (
|
||||||
|
build_code, status, phase, git_ref, git_sha, branch, pipeline_yaml, params_json,
|
||||||
|
dockerfile_version, builder_id, target_os, min_api_level, ota_channel, auto_ota, auto_deploy,
|
||||||
|
log_path, created_by_user_id
|
||||||
|
) VALUES (
|
||||||
|
:build_code, :status, :phase, :git_ref, :git_sha, :branch, :pipeline_yaml, :params_json,
|
||||||
|
:dockerfile_version, :builder_id, :target_os, :min_api_level, :ota_channel, :auto_ota, :auto_deploy,
|
||||||
|
:log_path, :created_by_user_id
|
||||||
|
)
|
||||||
|
SQL);
|
||||||
|
$stmt->execute([
|
||||||
|
':build_code' => $row['build_code'],
|
||||||
|
':status' => $row['status'] ?? 'queued',
|
||||||
|
':phase' => $row['phase'] ?? 'queued',
|
||||||
|
':git_ref' => $row['git_ref'] ?? null,
|
||||||
|
':git_sha' => $row['git_sha'] ?? null,
|
||||||
|
':branch' => $row['branch'] ?? null,
|
||||||
|
':pipeline_yaml' => $row['pipeline_yaml'] ?? null,
|
||||||
|
':params_json' => $row['params_json'] ?? '{}',
|
||||||
|
':dockerfile_version' => $row['dockerfile_version'] ?? '00.01.00.1000',
|
||||||
|
':builder_id' => $row['builder_id'] ?? 'local-docker',
|
||||||
|
':target_os' => $row['target_os'] ?? 'android',
|
||||||
|
':min_api_level' => (int) ($row['min_api_level'] ?? 24),
|
||||||
|
':ota_channel' => $row['ota_channel'] ?? null,
|
||||||
|
':auto_ota' => !empty($row['auto_ota']) ? 1 : 0,
|
||||||
|
':auto_deploy' => !empty($row['auto_deploy']) ? 1 : 0,
|
||||||
|
':log_path' => $row['log_path'] ?? null,
|
||||||
|
':created_by_user_id' => $row['created_by_user_id'] ?? null,
|
||||||
|
]);
|
||||||
|
return (int) $pdo->lastInsertId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function update(int $id, array $fields): void {
|
||||||
|
self::ensureSchema();
|
||||||
|
$allowed = ['status', 'phase', 'version_app', 'version_code', 'artifacts_json', 'error_message', 'started_at', 'finished_at', 'git_sha', 'log_path'];
|
||||||
|
$sets = [];
|
||||||
|
$params = [':id' => $id];
|
||||||
|
foreach ($fields as $k => $v) {
|
||||||
|
if (!in_array($k, $allowed, true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$sets[] = "$k = :$k";
|
||||||
|
$params[":$k"] = $v;
|
||||||
|
}
|
||||||
|
if (!$sets) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$sql = 'UPDATE builds SET ' . implode(', ', $sets) . ' WHERE id = :id';
|
||||||
|
Database::pdo()->prepare($sql)->execute($params);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getById(int $id): ?array {
|
||||||
|
self::ensureSchema();
|
||||||
|
$stmt = Database::pdo()->prepare('SELECT * FROM builds WHERE id = ? LIMIT 1');
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$row = $stmt->fetch();
|
||||||
|
return $row ?: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function listRecent(int $limit = 10): array {
|
||||||
|
self::ensureSchema();
|
||||||
|
$stmt = Database::pdo()->prepare('SELECT * FROM builds ORDER BY id DESC LIMIT ?');
|
||||||
|
$stmt->bindValue(1, $limit, PDO::PARAM_INT);
|
||||||
|
$stmt->execute();
|
||||||
|
return $stmt->fetchAll() ?: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function healthSummary(): array {
|
||||||
|
self::ensureSchema();
|
||||||
|
$pdo = Database::pdo();
|
||||||
|
$running = (int) $pdo->query("SELECT COUNT(*) FROM builds WHERE status = 'running'")->fetchColumn();
|
||||||
|
$failed = (int) $pdo->query("SELECT COUNT(*) FROM builds WHERE status = 'failed' AND created_at > (NOW() - INTERVAL 7 DAY)")->fetchColumn();
|
||||||
|
$passed = (int) $pdo->query("SELECT COUNT(*) FROM builds WHERE status = 'passed' AND created_at > (NOW() - INTERVAL 7 DAY)")->fetchColumn();
|
||||||
|
$dockerOk = trim((string) shell_exec('docker info >/dev/null 2>&1 && echo ok')) === 'ok';
|
||||||
|
return [
|
||||||
|
'docker' => $dockerOk ? 'ok' : 'down',
|
||||||
|
'running' => $running,
|
||||||
|
'passed_7d' => $passed,
|
||||||
|
'failed_7d' => $failed,
|
||||||
|
'ci_version' => cfg('build.ci_version', '00.01.00.1000'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
137
examples/build_console/backend/src/BuildRunner.php
Normal file
137
examples/build_console/backend/src/BuildRunner.php
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
final class BuildRunner {
|
||||||
|
public static function generateBuildCode(): string {
|
||||||
|
return 'bld-' . gmdate('Ymd-His') . '-' . substr(bin2hex(random_bytes(3)), 0, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function artifactDir(int $buildId): string {
|
||||||
|
$root = rtrim((string) cfg('build.artifacts_root', '/workspace/out/builds'), '/');
|
||||||
|
return $root . '/' . $buildId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function enqueue(array $params, ?int $userId): array {
|
||||||
|
BuildRepository::ensureSchema();
|
||||||
|
$buildCode = self::generateBuildCode();
|
||||||
|
$id = BuildRepository::create([
|
||||||
|
'build_code' => $buildCode,
|
||||||
|
'status' => 'queued',
|
||||||
|
'phase' => 'queued',
|
||||||
|
'git_ref' => $params['git_ref'] ?? null,
|
||||||
|
'git_sha' => $params['git_sha'] ?? null,
|
||||||
|
'branch' => $params['branch'] ?? null,
|
||||||
|
'pipeline_yaml' => $params['pipeline_yaml'] ?? null,
|
||||||
|
'params_json' => json_encode($params, JSON_UNESCAPED_SLASHES),
|
||||||
|
'dockerfile_version' => cfg('build.ci_version', '00.01.00.1000'),
|
||||||
|
'builder_id' => gethostname() ?: 'builder',
|
||||||
|
'ota_channel' => $params['ota_channel'] ?? 'staging',
|
||||||
|
'auto_ota' => !empty($params['auto_ota']),
|
||||||
|
'auto_deploy' => !empty($params['auto_deploy']),
|
||||||
|
'created_by_user_id' => $userId,
|
||||||
|
]);
|
||||||
|
$dir = self::artifactDir($id);
|
||||||
|
if (!is_dir($dir) && !mkdir($dir, 0775, true) && !is_dir($dir)) {
|
||||||
|
throw new RuntimeException('Cannot create artifact dir: ' . $dir);
|
||||||
|
}
|
||||||
|
$logPath = $dir . '/build.log';
|
||||||
|
BuildRepository::update($id, [
|
||||||
|
'log_path' => $logPath,
|
||||||
|
'status' => 'running',
|
||||||
|
'phase' => 'docker',
|
||||||
|
'started_at' => gmdate('Y-m-d H:i:s'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$runner = (string) cfg('build.runner_script', '/workspace/scripts/docker-build-runner.sh');
|
||||||
|
$repo = (string) cfg('build.repo_root', '/workspace');
|
||||||
|
$env = [
|
||||||
|
'BUILD_ID=' . $id,
|
||||||
|
'BUILD_LOG_FILE=' . escapeshellarg($logPath),
|
||||||
|
'BUILD_WORK_DIR=' . escapeshellarg($repo),
|
||||||
|
'BUILD_OUT_DIR=' . escapeshellarg($dir),
|
||||||
|
'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'] ?? '')),
|
||||||
|
'GRADLE_TASK=' . escapeshellarg((string) ($params['gradle_task'] ?? 'assembleDebug')),
|
||||||
|
'RUN_UNIT_TESTS=' . (!empty($params['run_tests']) ? '1' : '0'),
|
||||||
|
'RUN_NATIVE=' . (!empty($params['run_native']) ? '1' : '0'),
|
||||||
|
'RUN_APK=' . (!empty($params['run_apk']) ? '1' : '0'),
|
||||||
|
'OTA_BASE_URL=' . escapeshellarg((string) ($params['ota_base_url'] ?? cfg('build.ota_base_url', ''))),
|
||||||
|
'OTA_CHANNEL=' . escapeshellarg((string) ($params['ota_channel'] ?? 'staging')),
|
||||||
|
'AUTO_OTA=' . (!empty($params['auto_ota']) ? '1' : '0'),
|
||||||
|
];
|
||||||
|
$cmd = implode(' ', $env) . ' nohup ' . escapeshellarg($runner) . ' >> ' . escapeshellarg($logPath) . ' 2>&1 & echo $!';
|
||||||
|
$pid = trim((string) shell_exec($cmd));
|
||||||
|
file_put_contents($dir . '/runner.pid', $pid);
|
||||||
|
|
||||||
|
return BuildRepository::getById($id) ?? ['id' => $id, 'build_code' => $buildCode];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function readLogTail(?string $logPath, int $offset = 0, int $maxBytes = 65536): array {
|
||||||
|
if (!$logPath || !is_file($logPath)) {
|
||||||
|
return ['offset' => 0, 'eof' => true, 'text' => ''];
|
||||||
|
}
|
||||||
|
$size = filesize($logPath) ?: 0;
|
||||||
|
$offset = max(0, min($offset, $size));
|
||||||
|
$fh = fopen($logPath, 'rb');
|
||||||
|
if (!$fh) {
|
||||||
|
return ['offset' => $offset, 'eof' => true, 'text' => ''];
|
||||||
|
}
|
||||||
|
fseek($fh, $offset);
|
||||||
|
$chunk = fread($fh, $maxBytes) ?: '';
|
||||||
|
$newOffset = $offset + strlen($chunk);
|
||||||
|
fclose($fh);
|
||||||
|
return ['offset' => $newOffset, 'eof' => $newOffset >= $size, 'text' => $chunk];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function finalizeFromArtifacts(int $buildId): void {
|
||||||
|
$dir = self::artifactDir($buildId);
|
||||||
|
$infoPath = $dir . '/BUILD_INFO.json';
|
||||||
|
$status = is_file($dir . '/android_cast-latest.apk') ? 'passed' : 'failed';
|
||||||
|
$artifacts = [];
|
||||||
|
if (is_file($dir . '/android_cast-latest.apk')) {
|
||||||
|
$artifacts['apk'] = 'android_cast-latest.apk';
|
||||||
|
}
|
||||||
|
if (is_dir($dir . '/ota/v0')) {
|
||||||
|
$artifacts['ota'] = 'ota/v0';
|
||||||
|
}
|
||||||
|
$versionApp = null;
|
||||||
|
if (is_file($infoPath)) {
|
||||||
|
$info = json_decode((string) file_get_contents($infoPath), true);
|
||||||
|
if (is_array($info)) {
|
||||||
|
$versionApp = $info['gitSha'] ?? null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BuildRepository::update($buildId, [
|
||||||
|
'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 (!empty($artifacts['ota']) && cfg('build.ota_mount')) {
|
||||||
|
$build = BuildRepository::getById($buildId);
|
||||||
|
if (!empty($build['auto_deploy'])) {
|
||||||
|
self::publishOta($buildId, $dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function publishOta(int $buildId, string $dir): void {
|
||||||
|
$mount = rtrim((string) cfg('build.ota_mount', ''), '/');
|
||||||
|
if ($mount === '' || !is_dir($mount)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$src = $dir . '/ota/v0';
|
||||||
|
if (!is_dir($src)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$build = BuildRepository::getById($buildId);
|
||||||
|
$channel = $build['ota_channel'] ?? 'staging';
|
||||||
|
$dest = $mount . '/v0';
|
||||||
|
shell_exec('mkdir -p ' . escapeshellarg($dest) . ' && cp -a ' . escapeshellarg($src . '/.') . ' ' . escapeshellarg($dest . '/'));
|
||||||
|
if ($channel !== '' && $channel !== 'staging') {
|
||||||
|
shell_exec('cp -a ' . escapeshellarg($src . '/ota/channel/' . $channel . '.json') . ' ' . escapeshellarg($dest . '/ota/channel/' . $channel . '.json 2>/dev/null'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
54
examples/build_console/backend/src/bootstrap.php
Normal file
54
examples/build_console/backend/src/bootstrap.php
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once dirname(__DIR__, 2) . '/platform/shared_session.php';
|
||||||
|
|
||||||
|
$crashSrc = dirname(__DIR__, 2) . '/crash_reporter/backend/src';
|
||||||
|
$configPath = __DIR__ . '/../config/config.php';
|
||||||
|
if (!is_file($configPath)) {
|
||||||
|
$configPath = __DIR__ . '/../config/config.example.php';
|
||||||
|
}
|
||||||
|
$config = require $configPath;
|
||||||
|
|
||||||
|
platform_start_session(
|
||||||
|
$config['session_name'] ?? 'ac_crash_sess',
|
||||||
|
$config['session_cookie_path'] ?? '/app/androidcast_project'
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach (['Database.php', 'Rbac.php', 'Auth.php', 'UserRepository.php', 'AnalyticsHead.php'] as $f) {
|
||||||
|
require_once $crashSrc . '/' . $f;
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once __DIR__ . '/BuildRepository.php';
|
||||||
|
require_once __DIR__ . '/BuildRunner.php';
|
||||||
|
|
||||||
|
function cfg(string $key, $default = null) {
|
||||||
|
global $config;
|
||||||
|
$parts = explode('.', $key);
|
||||||
|
$v = $config;
|
||||||
|
foreach ($parts as $p) {
|
||||||
|
if (!is_array($v) || !array_key_exists($p, $v)) {
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
$v = $v[$p];
|
||||||
|
}
|
||||||
|
return $v;
|
||||||
|
}
|
||||||
|
|
||||||
|
function h(mixed $s): string {
|
||||||
|
if ($s === null || $s === false) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return htmlspecialchars((string) $s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
function json_out(array $data, int $code = 200): void {
|
||||||
|
http_response_code($code);
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
echo json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
function assets_base(): string {
|
||||||
|
return rtrim((string) cfg('links.crashes', '/app/androidcast_project/crashes/'), '/');
|
||||||
|
}
|
||||||
40
examples/build_console/backend/views/build_detail.php
Normal file
40
examples/build_console/backend/views/build_detail.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
$artifacts = json_decode($build['artifacts_json'] ?? '{}', true) ?: [];
|
||||||
|
$params = json_decode($build['params_json'] ?? '{}', true) ?: [];
|
||||||
|
?>
|
||||||
|
<h1>Build <?= h($build['build_code'] ?? '') ?></h1>
|
||||||
|
<p class="muted">Status: <strong><?= h($build['status'] ?? '') ?></strong> · Phase: <?= h($build['phase'] ?? '') ?></p>
|
||||||
|
|
||||||
|
<div class="detail-grid card card--lift">
|
||||||
|
<dl>
|
||||||
|
<dt>Builder</dt><dd><?= h($build['builder_id'] ?? '') ?></dd>
|
||||||
|
<dt>CI image</dt><dd><code><?= h($build['dockerfile_version'] ?? '') ?></code></dd>
|
||||||
|
<dt>Target OS</dt><dd><?= h($build['target_os'] ?? 'android') ?> (min API <?= (int) ($build['min_api_level'] ?? 24) ?>)</dd>
|
||||||
|
<dt>Git ref</dt><dd><code><?= h($build['git_ref'] ?? '—') ?></code></dd>
|
||||||
|
<dt>Git SHA</dt><dd><code><?= h($build['git_sha'] ?? '—') ?></code></dd>
|
||||||
|
<dt>OTA channel</dt><dd><?= h($build['ota_channel'] ?? '—') ?></dd>
|
||||||
|
<dt>Gradle task</dt><dd><code><?= h($params['gradle_task'] ?? '—') ?></code></dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($artifacts): ?>
|
||||||
|
<section class="card card--lift">
|
||||||
|
<h2>Artifacts</h2>
|
||||||
|
<ul>
|
||||||
|
<?php foreach ($artifacts as $k => $v): ?>
|
||||||
|
<li><strong><?= h($k) ?>:</strong> <code><?= h((string) $v) ?></code></li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<section class="card card--lift">
|
||||||
|
<h2>Pipeline (YAML)</h2>
|
||||||
|
<pre class="log-preview"><?= h($build['pipeline_yaml'] ?? '(none)') ?></pre>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card card--lift">
|
||||||
|
<h2>Build log</h2>
|
||||||
|
<pre id="build-log-tail" class="log-preview log-preview--live" aria-live="polite"></pre>
|
||||||
|
<button type="button" class="btn" id="build-log-expand">Show full log</button>
|
||||||
|
</section>
|
||||||
68
examples/build_console/backend/views/home.php
Normal file
68
examples/build_console/backend/views/home.php
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<?php declare(strict_types=1); ?>
|
||||||
|
<h1>Android Cast Builder</h1>
|
||||||
|
<p class="muted">Docker CI for APK baking, OTA staging, and pipeline history.</p>
|
||||||
|
|
||||||
|
<section class="card card--lift" style="margin-bottom:16px">
|
||||||
|
<h2>Ecosystem health</h2>
|
||||||
|
<ul>
|
||||||
|
<li>Docker: <strong><?= h($health['docker'] ?? 'unknown') ?></strong></li>
|
||||||
|
<li>CI image version: <code><?= h($health['ci_version'] ?? '') ?></code></li>
|
||||||
|
<li>Running builds: <?= (int) ($health['running'] ?? 0) ?></li>
|
||||||
|
<li>Passed (7d): <?= (int) ($health['passed_7d'] ?? 0) ?> · Failed (7d): <?= (int) ($health['failed_7d'] ?? 0) ?></li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card card--lift" id="build-trigger-panel">
|
||||||
|
<h2>Run pipeline</h2>
|
||||||
|
<form id="build-trigger-form" class="build-form">
|
||||||
|
<div class="build-form-grid">
|
||||||
|
<label>Git ref (branch / tag / commit)<input name="git_ref" placeholder="next" value="next"></label>
|
||||||
|
<label>Gradle task<input name="gradle_task" value="assembleDebug"></label>
|
||||||
|
<label>OTA channel<select name="ota_channel">
|
||||||
|
<option value="staging" selected>staging</option>
|
||||||
|
<option value="dev">dev</option>
|
||||||
|
<option value="nightly">nightly</option>
|
||||||
|
<option value="stable">stable / prod</option>
|
||||||
|
</select></label>
|
||||||
|
</div>
|
||||||
|
<div class="build-form-checks">
|
||||||
|
<label><input type="checkbox" name="run_tests" checked> Unit tests</label>
|
||||||
|
<label><input type="checkbox" name="run_native" checked> Native codecs</label>
|
||||||
|
<label><input type="checkbox" name="run_apk" checked> APK output</label>
|
||||||
|
<label><input type="checkbox" name="auto_ota"> Create OTA artifacts</label>
|
||||||
|
<label><input type="checkbox" name="auto_deploy"> Publish OTA to mount</label>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Start build</button>
|
||||||
|
<span id="build-trigger-status" class="muted"></span>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card card--lift">
|
||||||
|
<h2>Recent builds (last 10)</h2>
|
||||||
|
<div class="reports-table-wrap">
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Code</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Phase</th>
|
||||||
|
<th>Branch/ref</th>
|
||||||
|
<th>Channel</th>
|
||||||
|
<th>Created</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($builds as $b): ?>
|
||||||
|
<tr>
|
||||||
|
<td><a href="<?= h(Auth::basePath()) ?>/?view=build&id=<?= (int) $b['id'] ?>"><?= h($b['build_code']) ?></a></td>
|
||||||
|
<td><?= h($b['status']) ?></td>
|
||||||
|
<td><?= h($b['phase']) ?></td>
|
||||||
|
<td><?= h($b['git_ref'] ?? $b['branch'] ?? '—') ?></td>
|
||||||
|
<td><?= h($b['ota_channel'] ?? '—') ?></td>
|
||||||
|
<td><?= h($b['created_at'] ?? '') ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
58
examples/build_console/backend/views/layout.php
Normal file
58
examples/build_console/backend/views/layout.php
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
$ab = assets_base();
|
||||||
|
$links = cfg('links', []);
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" data-theme="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title><?= h($pageTitle ?? 'Builder') ?> — <?= h(cfg('app_name')) ?></title>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var t = localStorage.getItem('crash_console_theme');
|
||||||
|
if (t === 'light' || t === 'dark') document.documentElement.setAttribute('data-theme', t);
|
||||||
|
var l = localStorage.getItem('crash_console_lang');
|
||||||
|
if (l === 'en' || l === 'ru') document.documentElement.setAttribute('lang', l);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<link rel="stylesheet" href="<?= h($ab) ?>/assets/css/app.css">
|
||||||
|
<script src="<?= h($ab) ?>/assets/js/i18n.js" defer></script>
|
||||||
|
<script src="<?= h(Auth::basePath()) ?>/assets/js/builder.js" defer></script>
|
||||||
|
</head>
|
||||||
|
<body data-base-path="<?= h(Auth::basePath()) ?>"
|
||||||
|
data-view="<?= h($view ?? 'home') ?>"
|
||||||
|
<?= (($view ?? '') === 'build' && !empty($build['id'])) ? ' data-build-id="' . (int) $build['id'] . '"' : '' ?>>
|
||||||
|
<div class="shell">
|
||||||
|
<nav class="nav-pane" id="nav-pane" aria-label="Builder navigation">
|
||||||
|
<button type="button" class="nav-handle" id="nav-handle" aria-label="Toggle navigation">
|
||||||
|
<span class="nav-icon nav-icon--menu" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
<ul class="nav-list">
|
||||||
|
<li><a href="<?= h(Auth::basePath()) ?>/" class="nav-link <?= ($view ?? '') === 'home' ? 'active' : '' ?>"><span class="nav-icon nav-icon--home"></span><span class="nav-label">Builder</span></a></li>
|
||||||
|
<li><a href="<?= h($links['crashes'] ?? '') ?>?view=reports" class="nav-link"><span class="nav-icon nav-icon--reports"></span><span class="nav-label">Crashes</span></a></li>
|
||||||
|
<li><a href="<?= h($links['tickets'] ?? '') ?>" class="nav-link"><span class="nav-icon nav-icon--tickets"></span><span class="nav-label">Tickets</span></a></li>
|
||||||
|
<li><a href="<?= h($links['git'] ?? '') ?>" class="nav-link"><span class="nav-icon nav-icon--git"></span><span class="nav-label">Git</span></a></li>
|
||||||
|
<li><a href="<?= h($links['hub'] ?? '') ?>" class="nav-link"><span class="nav-icon nav-icon--home"></span><span class="nav-label">Hub</span></a></li>
|
||||||
|
</ul>
|
||||||
|
<div class="nav-locale">
|
||||||
|
<label class="toolbar-select"><span>Theme</span>
|
||||||
|
<select id="theme-select" aria-label="UI theme"><option value="dark">Dark</option><option value="light">Light</option></select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="nav-user">
|
||||||
|
<span class="nav-user-name"><?= h(Auth::user()['username'] ?? '') ?></span>
|
||||||
|
<a href="<?= h(Auth::basePath()) ?>/logout" class="nav-link nav-link--logout">Logout</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<main class="main-pane">
|
||||||
|
<?php if (($view ?? '') === 'build' && !empty($build)): ?>
|
||||||
|
<?php require __DIR__ . '/build_detail.php'; ?>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php require __DIR__ . '/home.php'; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
<footer class="bottom-bar">(c) Anton Afanaasyeu, <?= (int) date('Y') ?></footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
25
examples/build_console/backend/views/login.php
Normal file
25
examples/build_console/backend/views/login.php
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
$bp = Auth::basePath();
|
||||||
|
$ab = assets_base();
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Login — <?= h(cfg('app_name')) ?></title>
|
||||||
|
<link rel="stylesheet" href="<?= h($ab) ?>/assets/css/app.css">
|
||||||
|
</head>
|
||||||
|
<body class="login-page">
|
||||||
|
<form class="login-card" method="post" action="<?= h($bp) ?>/login">
|
||||||
|
<h1><?= h(cfg('app_name')) ?></h1>
|
||||||
|
<p class="muted">Sign in (same credentials as Crashes / Tickets)</p>
|
||||||
|
<?php if (!empty($loginError)): ?>
|
||||||
|
<div class="alert"><?= h($loginError) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<label><span>Username</span><input name="username" autocomplete="username" required></label>
|
||||||
|
<label><span>Password</span><input name="password" type="password" autocomplete="current-password" required></label>
|
||||||
|
<button type="submit">Sign in</button>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -30,6 +30,8 @@ return [
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
'session_name' => 'ac_crash_sess',
|
'session_name' => 'ac_crash_sess',
|
||||||
|
// Shared login cookie across crashes, build, and other consoles under this path
|
||||||
|
'session_cookie_path' => '/app/androidcast_project',
|
||||||
// When true, upload API may include a short error hint (do not enable on public prod long-term)
|
// When true, upload API may include a short error hint (do not enable on public prod long-term)
|
||||||
'debug' => false,
|
'debug' => false,
|
||||||
// Phase 1 RBAC: anonymous uploads and legacy rows attach to this company
|
// Phase 1 RBAC: anonymous uploads and legacy rows attach to this company
|
||||||
|
|||||||
@@ -12,6 +12,8 @@
|
|||||||
*/
|
*/
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once dirname(__DIR__, 2) . '/platform/shared_session.php';
|
||||||
|
|
||||||
if (!function_exists('session_start')) {
|
if (!function_exists('session_start')) {
|
||||||
http_response_code(500);
|
http_response_code(500);
|
||||||
header('Content-Type: text/plain; charset=utf-8');
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
@@ -27,10 +29,7 @@ if (!is_file($configPath)) {
|
|||||||
}
|
}
|
||||||
$config = require $configPath;
|
$config = require $configPath;
|
||||||
|
|
||||||
session_name($config['session_name'] ?? 'ac_crash_sess');
|
platform_start_session($config['session_name'] ?? 'ac_crash_sess', $config['session_cookie_path'] ?? '/app/androidcast_project');
|
||||||
if (session_status() !== PHP_SESSION_ACTIVE) {
|
|
||||||
session_start();
|
|
||||||
}
|
|
||||||
|
|
||||||
require_once __DIR__ . '/Database.php';
|
require_once __DIR__ . '/Database.php';
|
||||||
require_once __DIR__ . '/Rbac.php';
|
require_once __DIR__ . '/Rbac.php';
|
||||||
|
|||||||
@@ -75,6 +75,15 @@
|
|||||||
<span class="nav-label" data-i18n="nav.tickets">Tickets</span>
|
<span class="nav-label" data-i18n="nav.tickets">Tickets</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/app/androidcast_project/build/"
|
||||||
|
class="nav-link"
|
||||||
|
aria-label="Builder"
|
||||||
|
title="Builder">
|
||||||
|
<span class="nav-icon nav-icon--git" aria-hidden="true"></span>
|
||||||
|
<span class="nav-label">Builder</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="nav-locale">
|
<div class="nav-locale">
|
||||||
<div class="locale-picker" data-i18n-title="lang.label" title="Language">
|
<div class="locale-picker" data-i18n-title="lang.label" title="Language">
|
||||||
|
|||||||
22
examples/platform/shared_session.php
Normal file
22
examples/platform/shared_session.php
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared session for all Android Cast web consoles (crashes, build, …).
|
||||||
|
* Same cookie path + session name => one login across sub-apps.
|
||||||
|
*/
|
||||||
|
function platform_start_session(string $sessionName = 'ac_crash_sess', string $cookiePath = '/app/androidcast_project'): void
|
||||||
|
{
|
||||||
|
if (session_status() === PHP_SESSION_ACTIVE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
session_name($sessionName);
|
||||||
|
session_set_cookie_params([
|
||||||
|
'lifetime' => 0,
|
||||||
|
'path' => $cookiePath,
|
||||||
|
'httponly' => true,
|
||||||
|
'samesite' => 'Lax',
|
||||||
|
'secure' => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'),
|
||||||
|
]);
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ LANDING_HTTP_PORT=8080
|
|||||||
# Optional direct debug ports
|
# Optional direct debug ports
|
||||||
GITEA_FE_PORT=8081
|
GITEA_FE_PORT=8081
|
||||||
CRASHES_FE_PORT=8082
|
CRASHES_FE_PORT=8082
|
||||||
|
BUILD_FE_PORT=8083
|
||||||
|
|
||||||
# Internal app creds (local dev defaults)
|
# Internal app creds (local dev defaults)
|
||||||
MARIADB_ROOT_PASSWORD=androidcast_root
|
MARIADB_ROOT_PASSWORD=androidcast_root
|
||||||
|
|||||||
@@ -7,10 +7,13 @@ ENV_EXAMPLE="$ROOT_DIR/.env.example"
|
|||||||
RUNTIME_DIR="$ROOT_DIR/runtime"
|
RUNTIME_DIR="$ROOT_DIR/runtime"
|
||||||
DB_INIT_DIR="$RUNTIME_DIR/db-init"
|
DB_INIT_DIR="$RUNTIME_DIR/db-init"
|
||||||
CRASH_CONFIG="$RUNTIME_DIR/crash-config.php"
|
CRASH_CONFIG="$RUNTIME_DIR/crash-config.php"
|
||||||
|
BUILD_CONFIG="$RUNTIME_DIR/build-config.php"
|
||||||
|
OTA_DIR="$RUNTIME_DIR/ota-artifacts"
|
||||||
|
|
||||||
SRC_SCHEMA="$ROOT_DIR/../examples/crash_reporter/backend/sql/schema.mariadb.sql"
|
SRC_SCHEMA="$ROOT_DIR/../examples/crash_reporter/backend/sql/schema.mariadb.sql"
|
||||||
SRC_MIG_004="$ROOT_DIR/../examples/crash_reporter/backend/sql/migrations/004_ticket_workflow.sql"
|
SRC_MIG_004="$ROOT_DIR/../examples/crash_reporter/backend/sql/migrations/004_ticket_workflow.sql"
|
||||||
SRC_MIG_005="$ROOT_DIR/../examples/crash_reporter/backend/sql/migrations/005_ticket_attachments.sql"
|
SRC_MIG_005="$ROOT_DIR/../examples/crash_reporter/backend/sql/migrations/005_ticket_attachments.sql"
|
||||||
|
SRC_MIG_060="$ROOT_DIR/runtime/db-init/060_build_ecosystem.sql"
|
||||||
|
|
||||||
if ! command -v docker >/dev/null 2>&1; then
|
if ! command -v docker >/dev/null 2>&1; then
|
||||||
echo "docker not found in PATH"
|
echo "docker not found in PATH"
|
||||||
@@ -32,13 +35,15 @@ set -a
|
|||||||
source "$ENV_FILE"
|
source "$ENV_FILE"
|
||||||
set +a
|
set +a
|
||||||
|
|
||||||
mkdir -p "$DB_INIT_DIR"
|
mkdir -p "$DB_INIT_DIR" "$OTA_DIR"
|
||||||
|
|
||||||
cat >"$CRASH_CONFIG" <<EOF
|
cat >"$CRASH_CONFIG" <<EOF
|
||||||
<?php
|
<?php
|
||||||
return [
|
return [
|
||||||
'app_name' => 'Android Cast Crashes',
|
'app_name' => 'Android Cast Crashes',
|
||||||
'base_path' => '/app/androidcast_project/crashes',
|
'base_path' => '/app/androidcast_project/crashes',
|
||||||
|
'session_name' => 'ac_crash_sess',
|
||||||
|
'session_cookie_path' => '/app/androidcast_project',
|
||||||
'db' => [
|
'db' => [
|
||||||
'driver' => 'mysql',
|
'driver' => 'mysql',
|
||||||
'sqlite_path' => __DIR__ . '/../data/crashes.sqlite',
|
'sqlite_path' => __DIR__ . '/../data/crashes.sqlite',
|
||||||
@@ -52,7 +57,6 @@ return [
|
|||||||
'charset' => 'utf8mb4',
|
'charset' => 'utf8mb4',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
'session_name' => 'ac_crash_sess',
|
|
||||||
'debug' => true,
|
'debug' => true,
|
||||||
'rbac' => [
|
'rbac' => [
|
||||||
'default_company_slug' => 'default',
|
'default_company_slug' => 'default',
|
||||||
@@ -61,6 +65,51 @@ return [
|
|||||||
];
|
];
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
cat >"$BUILD_CONFIG" <<EOF
|
||||||
|
<?php
|
||||||
|
return [
|
||||||
|
'app_name' => 'Android Cast Builder',
|
||||||
|
'base_path' => '/app/androidcast_project/build',
|
||||||
|
'session_name' => 'ac_crash_sess',
|
||||||
|
'session_cookie_path' => '/app/androidcast_project',
|
||||||
|
'db' => [
|
||||||
|
'driver' => 'mysql',
|
||||||
|
'sqlite_path' => __DIR__ . '/../../crash_reporter/backend/data/crashes.sqlite',
|
||||||
|
'mysql' => [
|
||||||
|
'host' => 'mariadb',
|
||||||
|
'port' => 3306,
|
||||||
|
'socket' => '',
|
||||||
|
'database' => '${MARIADB_DATABASE}',
|
||||||
|
'username' => '${MARIADB_USER}',
|
||||||
|
'password' => '${MARIADB_PASSWORD}',
|
||||||
|
'charset' => 'utf8mb4',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'debug' => true,
|
||||||
|
'rbac' => [
|
||||||
|
'default_company_slug' => 'default',
|
||||||
|
'default_company_id' => 1,
|
||||||
|
],
|
||||||
|
'build' => [
|
||||||
|
'docker_image' => 'androidcast-ci:latest',
|
||||||
|
'ci_version' => '00.01.00.1000',
|
||||||
|
'repo_root' => '/workspace',
|
||||||
|
'artifacts_root' => '/workspace/out/builds',
|
||||||
|
'ota_mount' => '/workspace/orchestration/runtime/ota-artifacts',
|
||||||
|
'ota_base_url' => '${PUBLIC_BASE_URL}',
|
||||||
|
'runner_script' => '/workspace/scripts/docker-build-runner.sh',
|
||||||
|
'pipeline_config' => '/workspace/build.config.yml',
|
||||||
|
'default_channels' => ['stable', 'staging', 'dev', 'nightly'],
|
||||||
|
],
|
||||||
|
'links' => [
|
||||||
|
'hub' => '/app/androidcast_project/',
|
||||||
|
'crashes' => '/app/androidcast_project/crashes/',
|
||||||
|
'git' => '/app/androidcast_project/git/',
|
||||||
|
'tickets' => '/app/androidcast_project/crashes/?view=tickets',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
EOF
|
||||||
|
|
||||||
cp "$SRC_SCHEMA" "$DB_INIT_DIR/010_schema.sql"
|
cp "$SRC_SCHEMA" "$DB_INIT_DIR/010_schema.sql"
|
||||||
{
|
{
|
||||||
echo "USE \`${MARIADB_DATABASE}\`;"
|
echo "USE \`${MARIADB_DATABASE}\`;"
|
||||||
@@ -70,9 +119,13 @@ cp "$SRC_SCHEMA" "$DB_INIT_DIR/010_schema.sql"
|
|||||||
echo "USE \`${MARIADB_DATABASE}\`;"
|
echo "USE \`${MARIADB_DATABASE}\`;"
|
||||||
cat "$SRC_MIG_005"
|
cat "$SRC_MIG_005"
|
||||||
} >"$DB_INIT_DIR/050_ticket_attachments.sql"
|
} >"$DB_INIT_DIR/050_ticket_attachments.sql"
|
||||||
|
{
|
||||||
|
echo "USE \`${MARIADB_DATABASE}\`;"
|
||||||
|
cat "$SRC_MIG_060"
|
||||||
|
} >"$DB_INIT_DIR/060_build_ecosystem.sql"
|
||||||
|
|
||||||
echo "Pulling images..."
|
echo "Pulling images..."
|
||||||
docker compose -f "$ROOT_DIR/docker-compose.yml" pull landing-fe gitea-fe crashes-fe mariadb gitea || true
|
docker compose -f "$ROOT_DIR/docker-compose.yml" pull landing-fe gitea-fe crashes-fe build-fe mariadb gitea || true
|
||||||
|
|
||||||
echo "Starting/updating stack..."
|
echo "Starting/updating stack..."
|
||||||
docker compose -f "$ROOT_DIR/docker-compose.yml" up -d --build --remove-orphans --force-recreate
|
docker compose -f "$ROOT_DIR/docker-compose.yml" up -d --build --remove-orphans --force-recreate
|
||||||
@@ -82,5 +135,7 @@ echo "Stack is up."
|
|||||||
echo "Landing: ${PUBLIC_BASE_URL}/app/androidcast_project/"
|
echo "Landing: ${PUBLIC_BASE_URL}/app/androidcast_project/"
|
||||||
echo "Gitea (direct FE): http://localhost:${GITEA_FE_PORT}/app/androidcast_project/git/"
|
echo "Gitea (direct FE): http://localhost:${GITEA_FE_PORT}/app/androidcast_project/git/"
|
||||||
echo "Crashes (direct FE): http://localhost:${CRASHES_FE_PORT}/app/androidcast_project/crashes/"
|
echo "Crashes (direct FE): http://localhost:${CRASHES_FE_PORT}/app/androidcast_project/crashes/"
|
||||||
|
echo "Builder (direct FE): http://localhost:${BUILD_FE_PORT:-8083}/app/androidcast_project/build/"
|
||||||
|
echo "Shared login: same user/password as Crashes (session cookie path /app/androidcast_project)"
|
||||||
echo
|
echo
|
||||||
docker compose -f "$ROOT_DIR/docker-compose.yml" ps
|
docker compose -f "$ROOT_DIR/docker-compose.yml" ps
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- gitea-fe
|
- gitea-fe
|
||||||
- crashes-fe
|
- crashes-fe
|
||||||
|
- build-fe
|
||||||
ports:
|
ports:
|
||||||
- "${LANDING_HTTP_PORT}:8080"
|
- "${LANDING_HTTP_PORT}:8080"
|
||||||
volumes:
|
volumes:
|
||||||
@@ -38,6 +39,34 @@ services:
|
|||||||
- appnet
|
- appnet
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
build-fe:
|
||||||
|
image: nginx:1.27-alpine
|
||||||
|
depends_on:
|
||||||
|
- build-php
|
||||||
|
ports:
|
||||||
|
- "${BUILD_FE_PORT}:8080"
|
||||||
|
volumes:
|
||||||
|
- ../:/workspace
|
||||||
|
- ./nginx/build-fe.conf:/etc/nginx/conf.d/default.conf:ro
|
||||||
|
networks:
|
||||||
|
- appnet
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
build-php:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: images/build-php-fpm/Dockerfile
|
||||||
|
depends_on:
|
||||||
|
- mariadb
|
||||||
|
volumes:
|
||||||
|
- ../:/workspace
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
- ./runtime/build-config.php:/workspace/examples/build_console/backend/config/config.php
|
||||||
|
- ./runtime/ota-artifacts:/workspace/orchestration/runtime/ota-artifacts
|
||||||
|
networks:
|
||||||
|
- appnet
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
crash-php:
|
crash-php:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
|
|||||||
7
orchestration/images/build-php-fpm/Dockerfile
Normal file
7
orchestration/images/build-php-fpm/Dockerfile
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
FROM php:8.2-fpm-alpine
|
||||||
|
|
||||||
|
RUN apk add --no-cache bash docker-cli icu-dev libzip-dev oniguruma-dev mariadb-client sqlite-dev \
|
||||||
|
&& docker-php-ext-install pdo pdo_mysql pdo_sqlite \
|
||||||
|
&& rm -rf /var/cache/apk/*
|
||||||
|
|
||||||
|
WORKDIR /workspace/examples/build_console/backend
|
||||||
39
orchestration/nginx/build-fe.conf
Normal file
39
orchestration/nginx/build-fe.conf
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
server {
|
||||||
|
listen 8080;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
client_max_body_size 64m;
|
||||||
|
index index.php;
|
||||||
|
|
||||||
|
location = / {
|
||||||
|
return 302 /app/androidcast_project/build/;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /app/androidcast_project/build/ {
|
||||||
|
alias /workspace/examples/build_console/backend/public/;
|
||||||
|
index index.php;
|
||||||
|
try_files $uri $uri/ @build_front;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /app/androidcast_project/crashes/assets/ {
|
||||||
|
alias /workspace/examples/crash_reporter/backend/public/assets/;
|
||||||
|
}
|
||||||
|
|
||||||
|
location @build_front {
|
||||||
|
rewrite ^ /app/androidcast_project/build/index.php?$query_string last;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ ^/app/androidcast_project/build/(.+\.php)$ {
|
||||||
|
alias /workspace/examples/build_console/backend/public/$1;
|
||||||
|
include fastcgi_params;
|
||||||
|
fastcgi_param SCRIPT_FILENAME /workspace/examples/build_console/backend/public/$1;
|
||||||
|
fastcgi_param SCRIPT_NAME /app/androidcast_project/build/$1;
|
||||||
|
fastcgi_param DOCUMENT_ROOT /workspace/examples/build_console/backend/public;
|
||||||
|
fastcgi_pass build-php:9000;
|
||||||
|
fastcgi_read_timeout 300s;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ /\. {
|
||||||
|
deny all;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,20 @@ server {
|
|||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location /app/androidcast_project/build/ {
|
||||||
|
proxy_pass http://build-fe:8080;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /app/androidcast_project/v0/ota/ {
|
||||||
|
alias /workspace/orchestration/runtime/ota-artifacts/v0/;
|
||||||
|
add_header Cache-Control "public, max-age=60";
|
||||||
|
}
|
||||||
|
|
||||||
location /app/androidcast_project/ {
|
location /app/androidcast_project/ {
|
||||||
alias /workspace/examples/app_hub/;
|
alias /workspace/examples/app_hub/;
|
||||||
index index.html;
|
index index.html;
|
||||||
|
|||||||
30
orchestration/runtime/db-init/060_build_ecosystem.sql
Normal file
30
orchestration/runtime/db-init/060_build_ecosystem.sql
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS builds (
|
||||||
|
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
build_code VARCHAR(64) NOT NULL,
|
||||||
|
status ENUM('queued','running','passed','failed','cancelled') NOT NULL DEFAULT 'queued',
|
||||||
|
phase VARCHAR(64) NOT NULL DEFAULT 'init',
|
||||||
|
git_ref VARCHAR(256) NULL,
|
||||||
|
git_sha VARCHAR(64) NULL,
|
||||||
|
branch VARCHAR(128) NULL,
|
||||||
|
pipeline_yaml LONGTEXT NULL,
|
||||||
|
params_json LONGTEXT NOT NULL,
|
||||||
|
dockerfile_version VARCHAR(32) NOT NULL DEFAULT '00.01.00.1000',
|
||||||
|
builder_id VARCHAR(128) NOT NULL DEFAULT 'local-docker',
|
||||||
|
version_app VARCHAR(32) NULL,
|
||||||
|
version_code INT NULL,
|
||||||
|
target_os VARCHAR(32) NOT NULL DEFAULT 'android',
|
||||||
|
min_api_level INT NOT NULL DEFAULT 24,
|
||||||
|
ota_channel VARCHAR(64) NULL,
|
||||||
|
auto_ota TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
auto_deploy TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
log_path VARCHAR(512) NULL,
|
||||||
|
artifacts_json LONGTEXT NULL,
|
||||||
|
error_message VARCHAR(512) NULL,
|
||||||
|
created_by_user_id INT UNSIGNED NULL,
|
||||||
|
started_at TIMESTAMP NULL,
|
||||||
|
finished_at TIMESTAMP NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE KEY uq_builds_code (build_code),
|
||||||
|
KEY idx_builds_status (status),
|
||||||
|
KEY idx_builds_created (created_at)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
124
scripts/ci-build-and-publish-ota.sh
Executable file
124
scripts/ci-build-and-publish-ota.sh
Executable file
@@ -0,0 +1,124 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# CI pipeline entry: optional git checkout, native, gradle, OTA publish.
|
||||||
|
#
|
||||||
|
# Env:
|
||||||
|
# GRADLE_TASK assembleDebug | assembleRelease | testDebugUnitTest | …
|
||||||
|
# RUN_UNIT_TESTS 1 (default) | 0
|
||||||
|
# RUN_NATIVE 1 (default) | 0
|
||||||
|
# RUN_APK 1 (default) | 0 — set 0 for tests-only
|
||||||
|
# OTA_BASE_URL public base for v0/ota (empty = skip OTA)
|
||||||
|
# OTA_CHANNEL stable | staging | dev | nightly | custom name
|
||||||
|
# AUTO_OTA 1 to emit OTA tree under OUT_DIR
|
||||||
|
# OUT_DIR artifact root (default ./out/builds/$BUILD_ID)
|
||||||
|
# BUILD_ID build record id
|
||||||
|
# GIT_REF branch | tag | commit to checkout before build
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
BUILD_ID="${BUILD_ID:-local}"
|
||||||
|
OUT_DIR="${OUT_DIR:-${ROOT}/out/builds/${BUILD_ID}}"
|
||||||
|
GRADLE_TASK="${GRADLE_TASK:-assembleDebug}"
|
||||||
|
RUN_UNIT_TESTS="${RUN_UNIT_TESTS:-1}"
|
||||||
|
RUN_NATIVE="${RUN_NATIVE:-1}"
|
||||||
|
RUN_APK="${RUN_APK:-1}"
|
||||||
|
OTA_CHANNEL="${OTA_CHANNEL:-staging}"
|
||||||
|
AUTO_OTA="${AUTO_OTA:-0}"
|
||||||
|
|
||||||
|
mkdir -p "$OUT_DIR"
|
||||||
|
exec > >(tee -a "${OUT_DIR}/build.log") 2>&1
|
||||||
|
|
||||||
|
echo "==> Phase: init build_id=${BUILD_ID} channel=${OTA_CHANNEL}"
|
||||||
|
|
||||||
|
if [[ -n "${GIT_REF:-}" ]]; then
|
||||||
|
echo "==> Phase: git checkout ${GIT_REF}"
|
||||||
|
git fetch --all --tags 2>/dev/null || true
|
||||||
|
git checkout --force "$GIT_REF"
|
||||||
|
if [[ -n "${GIT_SHA:-}" ]]; then
|
||||||
|
git reset --hard "$GIT_SHA"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
export ANDROIDCAST_ROOT="$ROOT"
|
||||||
|
export ANDROIDCAST_CCACHE="${ANDROIDCAST_CCACHE:-1}"
|
||||||
|
# shellcheck source=android-ndk.sh
|
||||||
|
source "$ROOT/scripts/android-ndk.sh"
|
||||||
|
export ANDROID_NDK_HOME="$(androidcast_find_ndk_root "$ROOT")"
|
||||||
|
export ANDROID_NDK_ROOT="$ANDROID_NDK_HOME"
|
||||||
|
|
||||||
|
if [[ "$RUN_NATIVE" == "1" && "$RUN_APK" == "1" ]]; then
|
||||||
|
echo "==> Phase: native codecs"
|
||||||
|
ABIS="${ANDROIDCAST_CI_ABIS:-armeabi-v7a arm64-v8a x86_64}"
|
||||||
|
rm -rf "$ROOT/build/native"
|
||||||
|
mkdir -p "$ROOT/build/native"
|
||||||
|
for abi in $ABIS; do
|
||||||
|
./scripts/build-native-codecs.sh "$abi"
|
||||||
|
done
|
||||||
|
else
|
||||||
|
echo "==> Phase: native codecs (skipped)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$RUN_APK" == "1" ]]; then
|
||||||
|
echo "==> Phase: gradle ${GRADLE_TASK}"
|
||||||
|
if [[ "$RUN_UNIT_TESTS" == "1" ]]; then
|
||||||
|
./gradlew --no-daemon clean "${GRADLE_TASK}" testDebugUnitTest
|
||||||
|
else
|
||||||
|
./gradlew --no-daemon clean "${GRADLE_TASK}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "==> Phase: gradle tests-only"
|
||||||
|
./gradlew --no-daemon testDebugUnitTest
|
||||||
|
fi
|
||||||
|
|
||||||
|
APK=""
|
||||||
|
if [[ "$RUN_APK" == "1" ]]; then
|
||||||
|
for candidate in \
|
||||||
|
"$ROOT/app/build/outputs/apk/debug/app-debug.apk" \
|
||||||
|
"$ROOT/app/build/outputs/apk/release/app-release-unsigned.apk" \
|
||||||
|
"$ROOT/app/build/outputs/apk/release/app-release.apk"; do
|
||||||
|
if [[ -f "$candidate" ]]; then
|
||||||
|
APK="$candidate"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [[ -z "$APK" ]]; then
|
||||||
|
echo "ERROR: no APK produced" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
cp -f "$APK" "${OUT_DIR}/android_cast-latest.apk"
|
||||||
|
echo "==> APK: ${OUT_DIR}/android_cast-latest.apk"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$AUTO_OTA" == "1" && -n "${OTA_BASE_URL:-}" && -n "$APK" ]]; then
|
||||||
|
echo "==> Phase: OTA publish channel=${OTA_CHANNEL}"
|
||||||
|
OTA_PUBLISH="${OUT_DIR}/ota-publish"
|
||||||
|
rm -rf "$OTA_PUBLISH"
|
||||||
|
./scripts/generate-ota-v0.sh "$APK" "$OTA_BASE_URL" "$OTA_PUBLISH" "$OTA_CHANNEL"
|
||||||
|
mkdir -p "${OUT_DIR}/ota"
|
||||||
|
cp -rf "${OTA_PUBLISH}/v0" "${OUT_DIR}/ota/"
|
||||||
|
for ch in staging dev nightly stable; do
|
||||||
|
if [[ "$OTA_CHANNEL" == "$ch" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if [[ "$OTA_CHANNEL" == "stable" || "$OTA_CHANNEL" == "prod" || "$OTA_CHANNEL" == "production" || "$OTA_CHANNEL" == "release" ]]; then
|
||||||
|
./scripts/generate-ota-v0.sh "$APK" "$OTA_BASE_URL" "${OUT_DIR}/ota-publish-${ch}" "$ch" || true
|
||||||
|
mkdir -p "${OUT_DIR}/ota-${ch}"
|
||||||
|
cp -rf "${OUT_DIR}/ota-publish-${ch}/v0" "${OUT_DIR}/ota-${ch}/" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat >"${OUT_DIR}/BUILD_INFO.json" <<EOF
|
||||||
|
{
|
||||||
|
"buildId": "${BUILD_ID}",
|
||||||
|
"gradleTask": "${GRADLE_TASK}",
|
||||||
|
"otaChannel": "${OTA_CHANNEL}",
|
||||||
|
"apk": "android_cast-latest.apk",
|
||||||
|
"gitSha": "$(git -C "$ROOT" rev-parse HEAD 2>/dev/null || echo unknown)",
|
||||||
|
"gitRef": "${GIT_REF:-}",
|
||||||
|
"ciVersion": "${ANDROIDCAST_CI_VERSION:-00.01.00.1000}"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "==> Phase: done"
|
||||||
47
scripts/docker-build-runner.sh
Executable file
47
scripts/docker-build-runner.sh
Executable file
@@ -0,0 +1,47 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Host wrapper: build CI image + run pipeline with log capture.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./scripts/docker-build-runner.sh --build-id 42 --workdir /path/to/repo
|
||||||
|
#
|
||||||
|
# Env: see ci-build-and-publish-ota.sh
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
IMAGE="${ANDROIDCAST_CI_IMAGE:-androidcast-ci:latest}"
|
||||||
|
CI_VERSION="${ANDROIDCAST_CI_VERSION:-00.01.00.1000}"
|
||||||
|
BUILD_ID="${BUILD_ID:-local}"
|
||||||
|
LOG_FILE="${BUILD_LOG_FILE:-${ROOT}/out/builds/${BUILD_ID}/build.log}"
|
||||||
|
WORK_DIR="${BUILD_WORK_DIR:-${ROOT}}"
|
||||||
|
OUT_DIR="${BUILD_OUT_DIR:-${WORK_DIR}/out/builds/${BUILD_ID}}"
|
||||||
|
|
||||||
|
mkdir -p "$(dirname "$LOG_FILE")" "$OUT_DIR"
|
||||||
|
|
||||||
|
echo "[runner] image=${IMAGE} ci_version=${CI_VERSION} build_id=${BUILD_ID}" | tee -a "$LOG_FILE"
|
||||||
|
echo "[runner] workdir=${WORK_DIR} out=${OUT_DIR}" | tee -a "$LOG_FILE"
|
||||||
|
|
||||||
|
docker build \
|
||||||
|
--build-arg ANDROIDCAST_CI_VERSION="${CI_VERSION}" \
|
||||||
|
-t "$IMAGE" \
|
||||||
|
-f "${ROOT}/Dockerfile" \
|
||||||
|
"${ROOT}" 2>&1 | tee -a "$LOG_FILE"
|
||||||
|
|
||||||
|
docker run --rm \
|
||||||
|
-v "${WORK_DIR}:/workspace" \
|
||||||
|
-v "${ROOT}/out:/workspace/out" \
|
||||||
|
-w /workspace \
|
||||||
|
-e BUILD_ID="${BUILD_ID}" \
|
||||||
|
-e OUT_DIR="/workspace/out/builds/${BUILD_ID}" \
|
||||||
|
-e OTA_BASE_URL="${OTA_BASE_URL:-}" \
|
||||||
|
-e OTA_CHANNEL="${OTA_CHANNEL:-staging}" \
|
||||||
|
-e GRADLE_TASK="${GRADLE_TASK:-assembleDebug}" \
|
||||||
|
-e RUN_UNIT_TESTS="${RUN_UNIT_TESTS:-1}" \
|
||||||
|
-e RUN_NATIVE="${RUN_NATIVE:-1}" \
|
||||||
|
-e AUTO_OTA="${AUTO_OTA:-0}" \
|
||||||
|
-e GIT_REF="${GIT_REF:-}" \
|
||||||
|
-e GIT_SHA="${GIT_SHA:-}" \
|
||||||
|
-e RELEASE_NOTES="${RELEASE_NOTES:-}" \
|
||||||
|
"$IMAGE" \
|
||||||
|
./scripts/ci-build-and-publish-ota.sh 2>&1 | tee -a "$LOG_FILE"
|
||||||
|
|
||||||
|
echo "[runner] finished build_id=${BUILD_ID}" | tee -a "$LOG_FILE"
|
||||||
@@ -2,8 +2,9 @@
|
|||||||
# Generate v0 OTA artifacts for one APK (stdout = channel stable.json).
|
# Generate v0 OTA artifacts for one APK (stdout = channel stable.json).
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# ./scripts/generate-ota-v0.sh path/to/app-release.apk https://host[:port] [out-dir]
|
# ./scripts/generate-ota-v0.sh path/to/app-release.apk https://host[:port] [out-dir] [channel]
|
||||||
#
|
#
|
||||||
|
# channel — JSON under v0/ota/channel/ (stable, staging, dev, nightly, or custom)
|
||||||
# Writes under out-dir (default: ./ota-publish):
|
# Writes under out-dir (default: ./ota-publish):
|
||||||
# v0/ota/channel/stable.json
|
# v0/ota/channel/stable.json
|
||||||
# v0/ota/00/00.MM/00.MM.mm/android_cast_00.MM.mm.BB_manifest.json
|
# v0/ota/00/00.MM/00.MM.mm/android_cast_00.MM.mm.BB_manifest.json
|
||||||
@@ -15,6 +16,7 @@ set -euo pipefail
|
|||||||
apk="${1:?APK path required}"
|
apk="${1:?APK path required}"
|
||||||
host_base="${2:?Base URL required, e.g. https://192.168.1.1:8080}"
|
host_base="${2:?Base URL required, e.g. https://192.168.1.1:8080}"
|
||||||
out_dir="${3:-ota-publish}"
|
out_dir="${3:-ota-publish}"
|
||||||
|
channel="${4:-stable}"
|
||||||
|
|
||||||
if [[ ! -f "$apk" ]]; then
|
if [[ ! -f "$apk" ]]; then
|
||||||
echo "APK not found: $apk" >&2
|
echo "APK not found: $apk" >&2
|
||||||
@@ -115,7 +117,8 @@ cat >"${rel_dir}/${manifest_name}" <<EOF
|
|||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
cat >"${rel_root}/channel/stable.json" <<EOF
|
channel_file="${rel_root}/channel/${channel}.json"
|
||||||
|
cat >"${channel_file}" <<EOF
|
||||||
{
|
{
|
||||||
"schema": "v0",
|
"schema": "v0",
|
||||||
"manifestUrl": "${manifest_url}"
|
"manifestUrl": "${manifest_url}"
|
||||||
@@ -123,6 +126,6 @@ cat >"${rel_root}/channel/stable.json" <<EOF
|
|||||||
EOF
|
EOF
|
||||||
|
|
||||||
echo "Published v0 OTA under ${out_dir}/v0/ota" >&2
|
echo "Published v0 OTA under ${out_dir}/v0/ota" >&2
|
||||||
echo "Channel: ${host_base%/}/v0/ota/channel/stable.json" >&2
|
echo "Channel: ${host_base%/}/v0/ota/channel/${channel}.json" >&2
|
||||||
echo "Bundle: ${bundle_url} (${bundle_size} bytes)" >&2
|
echo "Bundle: ${bundle_url} (${bundle_size} bytes)" >&2
|
||||||
cat "${rel_root}/channel/stable.json"
|
cat "${channel_file}"
|
||||||
|
|||||||
Reference in New Issue
Block a user