ci: add lesavka hygiene gate and launcher crash guard
This commit is contained in:
parent
64ded7839d
commit
62f99b07f6
10
.gitignore
vendored
10
.gitignore
vendored
@ -1,8 +1,16 @@
|
|||||||
target/
|
target/
|
||||||
Cargo.lock
|
dist/
|
||||||
|
coverage/
|
||||||
|
logs/
|
||||||
|
captures/
|
||||||
override.toml
|
override.toml
|
||||||
.cache/sccache/
|
.cache/sccache/
|
||||||
/unit-graph.json
|
/unit-graph.json
|
||||||
|
*.log
|
||||||
|
*.h264
|
||||||
|
*.aac
|
||||||
|
*.wav
|
||||||
|
*.rgba
|
||||||
/**/*.rs.bk
|
/**/*.rs.bk
|
||||||
**/*.rs.orig
|
**/*.rs.orig
|
||||||
**/*.rs.rej
|
**/*.rs.rej
|
||||||
|
|||||||
4278
Cargo.lock
generated
Normal file
4278
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,8 +1,8 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
"common",
|
"common",
|
||||||
"server",
|
|
||||||
"client",
|
"client",
|
||||||
|
"server",
|
||||||
"testing",
|
"testing",
|
||||||
]
|
]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
|
|||||||
66
Jenkinsfile
vendored
66
Jenkinsfile
vendored
@ -59,6 +59,7 @@ spec:
|
|||||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||||
python3 \
|
python3 \
|
||||||
curl \
|
curl \
|
||||||
|
file \
|
||||||
clang \
|
clang \
|
||||||
llvm \
|
llvm \
|
||||||
pkg-config \
|
pkg-config \
|
||||||
@ -82,15 +83,7 @@ spec:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stage('Format') {
|
stage('Style Docs LOC Naming') {
|
||||||
steps {
|
|
||||||
container('rust-ci') {
|
|
||||||
sh 'cargo fmt --all -- --check'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stage('Hygiene') {
|
|
||||||
steps {
|
steps {
|
||||||
container('rust-ci') {
|
container('rust-ci') {
|
||||||
sh 'scripts/ci/hygiene_gate.sh'
|
sh 'scripts/ci/hygiene_gate.sh'
|
||||||
@ -98,7 +91,15 @@ spec:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stage('Testing') {
|
stage('Coverage') {
|
||||||
|
steps {
|
||||||
|
container('rust-ci') {
|
||||||
|
sh 'QUALITY_GATE_PUSHGATEWAY_URL="${QUALITY_GATE_PUSHGATEWAY_URL}" scripts/ci/quality_gate.sh'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Tests') {
|
||||||
steps {
|
steps {
|
||||||
container('rust-ci') {
|
container('rust-ci') {
|
||||||
sh 'QUALITY_GATE_PUSHGATEWAY_URL="${QUALITY_GATE_PUSHGATEWAY_URL}" scripts/ci/test_gate.sh'
|
sh 'QUALITY_GATE_PUSHGATEWAY_URL="${QUALITY_GATE_PUSHGATEWAY_URL}" scripts/ci/test_gate.sh'
|
||||||
@ -106,49 +107,26 @@ spec:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stage('Run quality gate') {
|
stage('Media Reliability') {
|
||||||
steps {
|
steps {
|
||||||
container('rust-ci') {
|
container('rust-ci') {
|
||||||
sh '''
|
sh 'QUALITY_GATE_PUSHGATEWAY_URL="${QUALITY_GATE_PUSHGATEWAY_URL}" scripts/ci/media_reliability_gate.sh'
|
||||||
set -eu
|
|
||||||
mkdir -p target/quality-gate
|
|
||||||
set +e
|
|
||||||
QUALITY_GATE_PUSHGATEWAY_URL="${QUALITY_GATE_PUSHGATEWAY_URL}" scripts/ci/quality_gate.sh
|
|
||||||
gate_rc=$?
|
|
||||||
set -e
|
|
||||||
printf '%s\n' "${gate_rc}" > target/quality-gate/quality-gate.rc
|
|
||||||
'''
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stage('Publish test metrics') {
|
stage('Gate Glue') {
|
||||||
steps {
|
steps {
|
||||||
container('rust-ci') {
|
container('rust-ci') {
|
||||||
sh '''
|
sh 'QUALITY_GATE_PUSHGATEWAY_URL="${QUALITY_GATE_PUSHGATEWAY_URL}" scripts/ci/gate_glue_gate.sh'
|
||||||
set -eu
|
|
||||||
if [ -f target/test-gate/metrics.prom ]; then
|
|
||||||
echo "test metrics published via scripts/ci/test_gate.sh"
|
|
||||||
else
|
|
||||||
echo "test metrics file missing; continuing without publish step failure"
|
|
||||||
fi
|
|
||||||
if [ -f target/quality-gate/metrics.prom ]; then
|
|
||||||
echo "quality gate metrics published via scripts/ci/quality_gate.sh"
|
|
||||||
else
|
|
||||||
echo "quality gate metrics file missing; continuing without publish step failure"
|
|
||||||
fi
|
|
||||||
'''
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stage('Enforce quality gate') {
|
stage('SonarQube') {
|
||||||
steps {
|
steps {
|
||||||
container('rust-ci') {
|
container('rust-ci') {
|
||||||
sh '''
|
sh 'QUALITY_GATE_PUSHGATEWAY_URL="${QUALITY_GATE_PUSHGATEWAY_URL}" scripts/ci/sonarqube_gate.sh'
|
||||||
set -eu
|
|
||||||
exit "$(cat target/quality-gate/quality-gate.rc 2>/dev/null || echo 1)"
|
|
||||||
'''
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -161,6 +139,14 @@ spec:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stage('Supply Chain Artifact Security') {
|
||||||
|
steps {
|
||||||
|
container('rust-ci') {
|
||||||
|
sh 'QUALITY_GATE_PUSHGATEWAY_URL="${QUALITY_GATE_PUSHGATEWAY_URL}" scripts/ci/supply_chain_gate.sh'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
stage('Docker Login') {
|
stage('Docker Login') {
|
||||||
when {
|
when {
|
||||||
expression { return params.PUSH_IMAGES }
|
expression { return params.PUSH_IMAGES }
|
||||||
@ -192,7 +178,7 @@ spec:
|
|||||||
always {
|
always {
|
||||||
script {
|
script {
|
||||||
try {
|
try {
|
||||||
archiveArtifacts artifacts: 'dist/*.tar.gz,target/test-gate/**,target/quality-gate/**,target/hygiene-gate/**', fingerprint: true, allowEmptyArchive: true
|
archiveArtifacts artifacts: 'dist/**,target/test-gate/**,target/quality-gate/**,target/hygiene-gate/**,target/media-reliability-gate/**,target/gate-glue-gate/**,target/sonarqube-gate/**,target/supply-chain-gate/**', fingerprint: true, allowEmptyArchive: true
|
||||||
} catch (Throwable err) {
|
} catch (Throwable err) {
|
||||||
echo "archive step unavailable: ${err.class.simpleName}"
|
echo "archive step unavailable: ${err.class.simpleName}"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -53,6 +53,11 @@ These install scripts are intended to be the trusted, repeatable delivery path.
|
|||||||
- Bump `minor` for new user-visible features, diagnostics, launcher controls, or protocol additions that remain backward-compatible when client and server are updated together.
|
- Bump `minor` for new user-visible features, diagnostics, launcher controls, or protocol additions that remain backward-compatible when client and server are updated together.
|
||||||
- Bump `major` for breaking changes to protocol, install behavior, or operator workflows that require a deliberate upgrade step.
|
- Bump `major` for breaking changes to protocol, install behavior, or operator workflows that require a deliberate upgrade step.
|
||||||
|
|
||||||
|
## Quality Gate
|
||||||
|
- The CI gate order is documented in `docs/quality-gate.md`.
|
||||||
|
- Runtime and test environment variables are indexed in `docs/operational-env.md`.
|
||||||
|
- Media reliability is a first-class check, not just a subset of normal tests.
|
||||||
|
|
||||||
## Operator Workflow
|
## Operator Workflow
|
||||||
1. Install or update the client and server through the install scripts.
|
1. Install or update the client and server through the install scripts.
|
||||||
2. Launch `Lesavka` from the KDE application launcher or run `lesavka`.
|
2. Launch `Lesavka` from the KDE application launcher or run `lesavka`.
|
||||||
|
|||||||
@ -4,7 +4,7 @@ path = "src/main.rs"
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "lesavka_client"
|
name = "lesavka_client"
|
||||||
version = "0.12.1"
|
version = "0.12.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@ -800,6 +800,7 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
|
|||||||
let diagnostics_popout = Rc::clone(&view.diagnostics_popout);
|
let diagnostics_popout = Rc::clone(&view.diagnostics_popout);
|
||||||
let log_popout = Rc::clone(&view.log_popout);
|
let log_popout = Rc::clone(&view.log_popout);
|
||||||
let shutdown_cleaned = Rc::new(Cell::new(false));
|
let shutdown_cleaned = Rc::new(Cell::new(false));
|
||||||
|
let camera_quality_syncing = Rc::new(Cell::new(false));
|
||||||
|
|
||||||
{
|
{
|
||||||
let shutdown_cleaned = Rc::clone(&shutdown_cleaned);
|
let shutdown_cleaned = Rc::clone(&shutdown_cleaned);
|
||||||
@ -911,6 +912,7 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
|
|||||||
let widgets = widgets.clone();
|
let widgets = widgets.clone();
|
||||||
let child_proc = Rc::clone(&child_proc);
|
let child_proc = Rc::clone(&child_proc);
|
||||||
let tests = Rc::clone(&tests);
|
let tests = Rc::clone(&tests);
|
||||||
|
let camera_quality_syncing = Rc::clone(&camera_quality_syncing);
|
||||||
let camera_combo = camera_combo.clone();
|
let camera_combo = camera_combo.clone();
|
||||||
let camera_quality_combo = camera_quality_combo.clone();
|
let camera_quality_combo = camera_quality_combo.clone();
|
||||||
let camera_combo_read = camera_combo.clone();
|
let camera_combo_read = camera_combo.clone();
|
||||||
@ -922,7 +924,9 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
|
|||||||
let catalog = catalog.borrow();
|
let catalog = catalog.borrow();
|
||||||
let mut state = state.borrow_mut();
|
let mut state = state.borrow_mut();
|
||||||
state.select_camera(selected.clone());
|
state.select_camera(selected.clone());
|
||||||
|
camera_quality_syncing.set(true);
|
||||||
sync_camera_quality_selection(&camera_quality_combo, &mut state, &catalog);
|
sync_camera_quality_selection(&camera_quality_combo, &mut state, &catalog);
|
||||||
|
camera_quality_syncing.set(false);
|
||||||
}
|
}
|
||||||
let quality = state.borrow().camera_quality;
|
let quality = state.borrow().camera_quality;
|
||||||
if let Err(err) = tests.borrow_mut().set_camera_selection(selected.as_deref()) {
|
if let Err(err) = tests.borrow_mut().set_camera_selection(selected.as_deref()) {
|
||||||
@ -952,13 +956,21 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
|
|||||||
let widgets = widgets.clone();
|
let widgets = widgets.clone();
|
||||||
let child_proc = Rc::clone(&child_proc);
|
let child_proc = Rc::clone(&child_proc);
|
||||||
let tests = Rc::clone(&tests);
|
let tests = Rc::clone(&tests);
|
||||||
|
let camera_quality_syncing = Rc::clone(&camera_quality_syncing);
|
||||||
let camera_quality_combo = camera_quality_combo.clone();
|
let camera_quality_combo = camera_quality_combo.clone();
|
||||||
let camera_quality_combo_read = camera_quality_combo.clone();
|
let camera_quality_combo_read = camera_quality_combo.clone();
|
||||||
camera_quality_combo.connect_changed(move |_| {
|
camera_quality_combo.connect_changed(move |_| {
|
||||||
|
if camera_quality_syncing.get() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let selected = selected_camera_quality(&camera_quality_combo_read);
|
let selected = selected_camera_quality(&camera_quality_combo_read);
|
||||||
let preview_was_running =
|
let preview_was_running =
|
||||||
tests.borrow_mut().is_running(DeviceTestKind::Camera);
|
tests.borrow_mut().is_running(DeviceTestKind::Camera);
|
||||||
state.borrow_mut().select_camera_quality(selected);
|
let Ok(mut state_mut) = state.try_borrow_mut() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
state_mut.select_camera_quality(selected);
|
||||||
|
drop(state_mut);
|
||||||
if let Err(err) = tests.borrow_mut().set_camera_quality(selected) {
|
if let Err(err) = tests.borrow_mut().set_camera_quality(selected) {
|
||||||
widgets
|
widgets
|
||||||
.status_label
|
.status_label
|
||||||
@ -1343,6 +1355,7 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
|
|||||||
let speaker_combo = speaker_combo.clone();
|
let speaker_combo = speaker_combo.clone();
|
||||||
let keyboard_combo = keyboard_combo.clone();
|
let keyboard_combo = keyboard_combo.clone();
|
||||||
let mouse_combo = mouse_combo.clone();
|
let mouse_combo = mouse_combo.clone();
|
||||||
|
let camera_quality_syncing = Rc::clone(&camera_quality_syncing);
|
||||||
widgets.device_refresh_button.connect_clicked(move |_| {
|
widgets.device_refresh_button.connect_clicked(move |_| {
|
||||||
let fresh_catalog = DeviceCatalog::discover();
|
let fresh_catalog = DeviceCatalog::discover();
|
||||||
let (
|
let (
|
||||||
@ -1379,11 +1392,13 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
|
|||||||
{
|
{
|
||||||
let mut state = state.borrow_mut();
|
let mut state = state.borrow_mut();
|
||||||
state.select_camera(selected_camera);
|
state.select_camera(selected_camera);
|
||||||
|
camera_quality_syncing.set(true);
|
||||||
sync_camera_quality_selection(
|
sync_camera_quality_selection(
|
||||||
&camera_quality_combo,
|
&camera_quality_combo,
|
||||||
&mut state,
|
&mut state,
|
||||||
&fresh_catalog,
|
&fresh_catalog,
|
||||||
);
|
);
|
||||||
|
camera_quality_syncing.set(false);
|
||||||
state.select_microphone(selected_microphone);
|
state.select_microphone(selected_microphone);
|
||||||
state.select_speaker(selected_speaker);
|
state.select_speaker(selected_speaker);
|
||||||
state.select_keyboard(selected_keyboard);
|
state.select_keyboard(selected_keyboard);
|
||||||
|
|||||||
@ -715,14 +715,18 @@ pub fn build_launcher_view(
|
|||||||
console_level_combo.set_size_request(78, 36);
|
console_level_combo.set_size_request(78, 36);
|
||||||
console_level_combo.set_tooltip_text(Some("Show relay logs at this level or higher."));
|
console_level_combo.set_tooltip_text(Some("Show relay logs at this level or higher."));
|
||||||
let console_copy_button = gtk::Button::with_label("Copy");
|
let console_copy_button = gtk::Button::with_label("Copy");
|
||||||
stabilize_button(&console_copy_button, 66);
|
|
||||||
console_copy_button.set_tooltip_text(Some("Copy visible log."));
|
console_copy_button.set_tooltip_text(Some("Copy visible log."));
|
||||||
let console_popout_button = gtk::Button::with_label("Pop Out");
|
let console_popout_button = gtk::Button::with_label("Pop Out");
|
||||||
stabilize_button(&console_popout_button, 78);
|
|
||||||
console_popout_button.set_tooltip_text(Some("Open log window."));
|
console_popout_button.set_tooltip_text(Some("Open log window."));
|
||||||
|
let console_buttons = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||||
|
console_buttons.set_hexpand(true);
|
||||||
|
console_buttons.set_homogeneous(true);
|
||||||
|
console_copy_button.set_hexpand(true);
|
||||||
|
console_popout_button.set_hexpand(true);
|
||||||
|
console_buttons.append(&console_copy_button);
|
||||||
|
console_buttons.append(&console_popout_button);
|
||||||
console_toolbar.append(&console_level_combo);
|
console_toolbar.append(&console_level_combo);
|
||||||
console_toolbar.append(&console_copy_button);
|
console_toolbar.append(&console_buttons);
|
||||||
console_toolbar.append(&console_popout_button);
|
|
||||||
let status_label = gtk::Label::new(Some("Session log ready."));
|
let status_label = gtk::Label::new(Some("Session log ready."));
|
||||||
status_label.add_css_class("status-line");
|
status_label.add_css_class("status-line");
|
||||||
status_label.set_halign(gtk::Align::Start);
|
status_label.set_halign(gtk::Align::Start);
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lesavka_common"
|
name = "lesavka_common"
|
||||||
version = "0.12.1"
|
version = "0.12.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
|
||||||
|
|||||||
235
docs/operational-env.md
Normal file
235
docs/operational-env.md
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
# Lesavka operational environment variables
|
||||||
|
|
||||||
|
This is the tracked inventory for `LESAVKA_*` knobs used by source, scripts, CI, and tests. The hygiene gate fails when a new variable is added without appearing here, which keeps operator-facing configuration from drifting into folklore.
|
||||||
|
|
||||||
|
Hardware-facing assumptions belong near the code that uses them; this file is the repo-wide index.
|
||||||
|
|
||||||
|
| Variable | Notes |
|
||||||
|
| --- | --- |
|
||||||
|
| `LESAVKA_ALLOW_GADGET_CYCLE` | document near use before promoting to operator config |
|
||||||
|
| `LESAVKA_ALLOW_GADGET_RESET` | document near use before promoting to operator config |
|
||||||
|
| `LESAVKA_ALSA_DEV` | server hardware/device override |
|
||||||
|
| `LESAVKA_ATTACH_WRITE_UDC` | server hardware/device override |
|
||||||
|
| `LESAVKA_AUDIO_AUTO_RECOVER_AFTER` | client media capture/playback override |
|
||||||
|
| `LESAVKA_AUDIO_AUTO_RECOVER_COOLDOWN_MS` | client media capture/playback override |
|
||||||
|
| `LESAVKA_AUDIO_AUTO_RECOVER_USB` | client media capture/playback override |
|
||||||
|
| `LESAVKA_AUDIO_DISABLE` | client media capture/playback override |
|
||||||
|
| `LESAVKA_AUDIO_GAIN` | client media capture/playback override |
|
||||||
|
| `LESAVKA_AUDIO_GAIN_CONTROL` | client media capture/playback override |
|
||||||
|
| `LESAVKA_AUDIO_INIT_ATTEMPTS` | client media capture/playback override |
|
||||||
|
| `LESAVKA_AUDIO_INIT_DELAY_MS` | client media capture/playback override |
|
||||||
|
| `LESAVKA_AUDIO_MIN_PACKETS_PER_SEC` | client media capture/playback override |
|
||||||
|
| `LESAVKA_AUDIO_SINK` | client media capture/playback override |
|
||||||
|
| `LESAVKA_AUDIO_SOURCE_GRACE_MS` | client media capture/playback override |
|
||||||
|
| `LESAVKA_AUDIO_SOURCE_IDLE_MS` | client media capture/playback override |
|
||||||
|
| `LESAVKA_BOOL_TEST` | test/build contract variable; not runtime operator config |
|
||||||
|
| `LESAVKA_BREAKOUT_PREVIEW_HEIGHT` | eye preview/video transport override |
|
||||||
|
| `LESAVKA_BREAKOUT_PREVIEW_MAX_KBIT` | eye preview/video transport override |
|
||||||
|
| `LESAVKA_BREAKOUT_PREVIEW_WIDTH` | eye preview/video transport override |
|
||||||
|
| `LESAVKA_BREAKOUT_REQUEST_FPS` | eye preview/video transport override |
|
||||||
|
| `LESAVKA_BREAKOUT_REQUEST_HEIGHT` | eye preview/video transport override |
|
||||||
|
| `LESAVKA_BREAKOUT_REQUEST_WIDTH` | eye preview/video transport override |
|
||||||
|
| `LESAVKA_CAM_BY_ID_DIR` | client media capture/playback override |
|
||||||
|
| `LESAVKA_CAM_CODEC` | client media capture/playback override |
|
||||||
|
| `LESAVKA_CAM_DEV_ROOT` | client media capture/playback override |
|
||||||
|
| `LESAVKA_CAM_DISABLE` | client media capture/playback override |
|
||||||
|
| `LESAVKA_CAM_FORMAT` | client media capture/playback override |
|
||||||
|
| `LESAVKA_CAM_FPS` | client media capture/playback override |
|
||||||
|
| `LESAVKA_CAM_H264_KBIT` | client media capture/playback override |
|
||||||
|
| `LESAVKA_CAM_HEIGHT` | client media capture/playback override |
|
||||||
|
| `LESAVKA_CAM_JPEG_QUALITY` | client media capture/playback override |
|
||||||
|
| `LESAVKA_CAM_KEYFRAME_INTERVAL` | client media capture/playback override |
|
||||||
|
| `LESAVKA_CAM_MJPG` | client media capture/playback override |
|
||||||
|
| `LESAVKA_CAM_OUTPUT` | client media capture/playback override |
|
||||||
|
| `LESAVKA_CAM_SOURCE` | client media capture/playback override |
|
||||||
|
| `LESAVKA_CAM_TEST_ENCODER` | client media capture/playback override |
|
||||||
|
| `LESAVKA_CAM_TEST_PATTERN` | client media capture/playback override |
|
||||||
|
| `LESAVKA_CAM_WIDTH` | client media capture/playback override |
|
||||||
|
| `LESAVKA_CAPTURE_POWER_GRACE_SECS` | runtime/install/session override |
|
||||||
|
| `LESAVKA_CAPTURE_POWER_UNIT` | runtime/install/session override |
|
||||||
|
| `LESAVKA_CAPTURE_REMOTE` | runtime/install/session override |
|
||||||
|
| `LESAVKA_CLIENT_APP_SRC` | test/build contract variable; not runtime operator config |
|
||||||
|
| `LESAVKA_CLIENT_CAMERA_SRC` | test/build contract variable; not runtime operator config |
|
||||||
|
| `LESAVKA_CLIENT_INPUTS_SRC` | test/build contract variable; not runtime operator config |
|
||||||
|
| `LESAVKA_CLIENT_KEYBOARD_SRC` | test/build contract variable; not runtime operator config |
|
||||||
|
| `LESAVKA_CLIENT_MAIN_SRC` | test/build contract variable; not runtime operator config |
|
||||||
|
| `LESAVKA_CLIENT_MICROPHONE_SRC` | test/build contract variable; not runtime operator config |
|
||||||
|
| `LESAVKA_CLIENT_MOUSE_SRC` | test/build contract variable; not runtime operator config |
|
||||||
|
| `LESAVKA_CLIENT_OUTPUT_AUDIO_SRC` | test/build contract variable; not runtime operator config |
|
||||||
|
| `LESAVKA_CLIENT_OUTPUT_DISPLAY_SRC` | test/build contract variable; not runtime operator config |
|
||||||
|
| `LESAVKA_CLIENT_OUTPUT_VIDEO_SRC` | test/build contract variable; not runtime operator config |
|
||||||
|
| `LESAVKA_CLIENT_RELAYCTL_BIN_SRC` | test/build contract variable; not runtime operator config |
|
||||||
|
| `LESAVKA_CLIENT_VIDEO_SUPPORT_SRC` | test/build contract variable; not runtime operator config |
|
||||||
|
| `LESAVKA_CLIPBOARD_CHORD` | input routing/clipboard override |
|
||||||
|
| `LESAVKA_CLIPBOARD_CMD` | input routing/clipboard override |
|
||||||
|
| `LESAVKA_CLIPBOARD_DEBOUNCE_MS` | input routing/clipboard override |
|
||||||
|
| `LESAVKA_CLIPBOARD_DELAY_MS` | input routing/clipboard override |
|
||||||
|
| `LESAVKA_CLIPBOARD_MAX` | input routing/clipboard override |
|
||||||
|
| `LESAVKA_CLIPBOARD_PASTE` | input routing/clipboard override |
|
||||||
|
| `LESAVKA_CLIPBOARD_TIMEOUT_MS` | input routing/clipboard override |
|
||||||
|
| `LESAVKA_COMMON_CLI_BIN_SRC` | test/build contract variable; not runtime operator config |
|
||||||
|
| `LESAVKA_CORE_HELPER` | runtime/install/session override |
|
||||||
|
| `LESAVKA_DECODER_PROBE_BUFFERS` | manual probe override |
|
||||||
|
| `LESAVKA_DECODER_PROBE_POLL_SECONDS` | manual probe override |
|
||||||
|
| `LESAVKA_DECODER_PROBE_WAIT_SECONDS` | manual probe override |
|
||||||
|
| `LESAVKA_DETACH_CLEAR_UDC` | server hardware/device override |
|
||||||
|
| `LESAVKA_DEV_MODE` | document near use before promoting to operator config |
|
||||||
|
| `LESAVKA_DISABLE_UAC` | document near use before promoting to operator config |
|
||||||
|
| `LESAVKA_DISABLE_UVC` | document near use before promoting to operator config |
|
||||||
|
| `LESAVKA_DISABLE_VIDEO_RENDER` | eye preview/video transport override |
|
||||||
|
| `LESAVKA_DUMP_VIDEO` | eye preview/video transport override |
|
||||||
|
| `LESAVKA_EYE_ADAPTIVE` | eye preview/video transport override |
|
||||||
|
| `LESAVKA_EYE_APPSINK_BUFFERS` | eye preview/video transport override |
|
||||||
|
| `LESAVKA_EYE_CAP_POLL_SECONDS` | eye preview/video transport override |
|
||||||
|
| `LESAVKA_EYE_CAP_WAIT_SECONDS` | eye preview/video transport override |
|
||||||
|
| `LESAVKA_EYE_CHAN_CAPACITY` | eye preview/video transport override |
|
||||||
|
| `LESAVKA_EYE_DEVICE_POLL_MS` | eye preview/video transport override |
|
||||||
|
| `LESAVKA_EYE_DEVICE_WAIT_MS` | eye preview/video transport override |
|
||||||
|
| `LESAVKA_EYE_FPS` | eye preview/video transport override |
|
||||||
|
| `LESAVKA_EYE_KEYFRAME_INTERVAL` | eye preview/video transport override |
|
||||||
|
| `LESAVKA_EYE_MIN_FPS` | eye preview/video transport override |
|
||||||
|
| `LESAVKA_EYE_QUEUE_BUFFERS` | eye preview/video transport override |
|
||||||
|
| `LESAVKA_EYE_TESTSRC_KBIT` | eye preview/video transport override |
|
||||||
|
| `LESAVKA_FOCUS_LAUNCHER_ON_LOCAL` | launcher UI/runtime override |
|
||||||
|
| `LESAVKA_FORCE_CORE_REBUILD_WITHOUT_UDC` | document near use before promoting to operator config |
|
||||||
|
| `LESAVKA_GADGET_CONFIGFS_ROOT` | server hardware/device override |
|
||||||
|
| `LESAVKA_GADGET_FORCE_CYCLE` | server hardware/device override |
|
||||||
|
| `LESAVKA_GADGET_SYSFS_ROOT` | server hardware/device override |
|
||||||
|
| `LESAVKA_GIT_SHA` | runtime/install/session override |
|
||||||
|
| `LESAVKA_H264_DECODER` | eye preview/video transport override |
|
||||||
|
| `LESAVKA_HDMI_CONNECTOR` | server hardware/device override |
|
||||||
|
| `LESAVKA_HDMI_DRIVER` | server hardware/device override |
|
||||||
|
| `LESAVKA_HDMI_FBDEV` | server hardware/device override |
|
||||||
|
| `LESAVKA_HDMI_HEIGHT` | server hardware/device override |
|
||||||
|
| `LESAVKA_HDMI_MODES` | server hardware/device override |
|
||||||
|
| `LESAVKA_HDMI_RESTORE_CRTC` | server hardware/device override |
|
||||||
|
| `LESAVKA_HDMI_SINK` | server hardware/device override |
|
||||||
|
| `LESAVKA_HDMI_SKIP_VSYNC` | server hardware/device override |
|
||||||
|
| `LESAVKA_HDMI_WIDTH` | server hardware/device override |
|
||||||
|
| `LESAVKA_HEADLESS` | runtime/install/session override |
|
||||||
|
| `LESAVKA_HELPER_ENV_DUMP` | document near use before promoting to operator config |
|
||||||
|
| `LESAVKA_HID_DIR` | server hardware/device override |
|
||||||
|
| `LESAVKA_HID_WRITE_RETRIES` | server hardware/device override |
|
||||||
|
| `LESAVKA_HID_WRITE_RETRY_DELAY_MS` | server hardware/device override |
|
||||||
|
| `LESAVKA_HW_H264` | document near use before promoting to operator config |
|
||||||
|
| `LESAVKA_ICON_NAME` | launcher UI/runtime override |
|
||||||
|
| `LESAVKA_ICON_SEARCH_PATH` | launcher UI/runtime override |
|
||||||
|
| `LESAVKA_INPUT_RELEASE_TIMEOUT_MS` | input routing/clipboard override |
|
||||||
|
| `LESAVKA_INPUT_REMOTE_FAILSAFE_MS` | input routing/clipboard override |
|
||||||
|
| `LESAVKA_INPUT_REMOTE_FAILSAFE_SECS` | input routing/clipboard override |
|
||||||
|
| `LESAVKA_INPUT_RESCAN_MS` | input routing/clipboard override |
|
||||||
|
| `LESAVKA_INPUT_TOGGLE_DEBOUNCE_MS` | input routing/clipboard override |
|
||||||
|
| `LESAVKA_INPUT_TOGGLE_KEY` | input routing/clipboard override |
|
||||||
|
| `LESAVKA_KERNEL_BRANCH` | kernel build/install override |
|
||||||
|
| `LESAVKA_KERNEL_BUILD_ROOT` | kernel build/install override |
|
||||||
|
| `LESAVKA_KERNEL_BUILD_USER` | kernel build/install override |
|
||||||
|
| `LESAVKA_KERNEL_COMMIT` | kernel build/install override |
|
||||||
|
| `LESAVKA_KERNEL_JOBS` | kernel build/install override |
|
||||||
|
| `LESAVKA_KERNEL_PATCH_DIR` | kernel build/install override |
|
||||||
|
| `LESAVKA_KERNEL_PATCH_DWC2_FIFO` | kernel build/install override |
|
||||||
|
| `LESAVKA_KERNEL_PATCH_UVC_BULK` | kernel build/install override |
|
||||||
|
| `LESAVKA_KERNEL_PATCH_UVC_DEBUG` | kernel build/install override |
|
||||||
|
| `LESAVKA_KERNEL_PKGREL` | kernel build/install override |
|
||||||
|
| `LESAVKA_KERNEL_PKG_REPO` | kernel build/install override |
|
||||||
|
| `LESAVKA_KERNEL_REPO` | kernel build/install override |
|
||||||
|
| `LESAVKA_KERNEL_UPDATE` | kernel build/install override |
|
||||||
|
| `LESAVKA_KEYBOARD_DEVICE` | input routing/clipboard override |
|
||||||
|
| `LESAVKA_LAUNCHER_CAMERA_DIR` | launcher UI/runtime override |
|
||||||
|
| `LESAVKA_LAUNCHER_CHILD` | launcher UI/runtime override |
|
||||||
|
| `LESAVKA_LAUNCHER_CLIPBOARD_CONTROL` | launcher UI/runtime override |
|
||||||
|
| `LESAVKA_LAUNCHER_FOCUS_SIGNAL` | launcher UI/runtime override |
|
||||||
|
| `LESAVKA_LAUNCHER_INPUT_CONTROL` | launcher UI/runtime override |
|
||||||
|
| `LESAVKA_LAUNCHER_INPUT_STATE` | launcher UI/runtime override |
|
||||||
|
| `LESAVKA_LAUNCHER_PARENT_PID` | launcher UI/runtime override |
|
||||||
|
| `LESAVKA_LAUNCHER_PARENT_START_TICKS` | launcher UI/runtime override |
|
||||||
|
| `LESAVKA_LAUNCHER_TOGGLE_KEY_CONTROL` | launcher UI/runtime override |
|
||||||
|
| `LESAVKA_LAUNCHER_WINDOW_TITLE` | launcher UI/runtime override |
|
||||||
|
| `LESAVKA_LIVE_KEYBOARD_REPORT_DELAY_MS` | input routing/clipboard override |
|
||||||
|
| `LESAVKA_LIVE_MODIFIER_DELAY_MS` | input routing/clipboard override |
|
||||||
|
| `LESAVKA_MEDIA_GATE_PUSHGATEWAY_JOB` | CI metrics destination override |
|
||||||
|
| `LESAVKA_MAX_SPEED` | document near use before promoting to operator config |
|
||||||
|
| `LESAVKA_MIC_DISABLE` | client media capture/playback override |
|
||||||
|
| `LESAVKA_MIC_DISABLE_PIPEWIRE` | client media capture/playback override |
|
||||||
|
| `LESAVKA_MIC_GAIN` | client media capture/playback override |
|
||||||
|
| `LESAVKA_MIC_GAIN_CONTROL` | client media capture/playback override |
|
||||||
|
| `LESAVKA_MIC_INIT_ATTEMPTS` | client media capture/playback override |
|
||||||
|
| `LESAVKA_MIC_INIT_DELAY_MS` | client media capture/playback override |
|
||||||
|
| `LESAVKA_MIC_SOURCE` | client media capture/playback override |
|
||||||
|
| `LESAVKA_MIC_TEST_SOURCE_DESC` | client media capture/playback override |
|
||||||
|
| `LESAVKA_MOUSE_DEVICE` | input routing/clipboard override |
|
||||||
|
| `LESAVKA_PASTE_DELAY_MS` | input routing/clipboard override |
|
||||||
|
| `LESAVKA_PASTE_KEY` | input routing/clipboard override |
|
||||||
|
| `LESAVKA_PASTE_KEY_FILE` | input routing/clipboard override |
|
||||||
|
| `LESAVKA_PASTE_MAX` | input routing/clipboard override |
|
||||||
|
| `LESAVKA_PASTE_RPC` | input routing/clipboard override |
|
||||||
|
| `LESAVKA_PREVIEW_HEIGHT` | eye preview/video transport override |
|
||||||
|
| `LESAVKA_PREVIEW_MAX_KBIT` | eye preview/video transport override |
|
||||||
|
| `LESAVKA_PREVIEW_REQUEST_FPS` | eye preview/video transport override |
|
||||||
|
| `LESAVKA_PREVIEW_REQUEST_HEIGHT` | eye preview/video transport override |
|
||||||
|
| `LESAVKA_PREVIEW_REQUEST_WIDTH` | eye preview/video transport override |
|
||||||
|
| `LESAVKA_PREVIEW_WIDTH` | eye preview/video transport override |
|
||||||
|
| `LESAVKA_REF` | runtime/install/session override |
|
||||||
|
| `LESAVKA_RELOAD_UVCVIDEO` | document near use before promoting to operator config |
|
||||||
|
| `LESAVKA_REPO_URL` | runtime/install/session override |
|
||||||
|
| `LESAVKA_RGBA` | document near use before promoting to operator config |
|
||||||
|
| `LESAVKA_SERVER_ADDR` | runtime/install/session override |
|
||||||
|
| `LESAVKA_SERVER_GADGET_SRC` | test/build contract variable; not runtime operator config |
|
||||||
|
| `LESAVKA_SERVER_HOST` | manual probe override |
|
||||||
|
| `LESAVKA_SERVER_MAIN_SRC` | test/build contract variable; not runtime operator config |
|
||||||
|
| `LESAVKA_SERVER_UVC_BIN_SRC` | test/build contract variable; not runtime operator config |
|
||||||
|
| `LESAVKA_SERVER_VIDEO_SINKS_SRC` | test/build contract variable; not runtime operator config |
|
||||||
|
| `LESAVKA_SERVER_VIDEO_SRC` | test/build contract variable; not runtime operator config |
|
||||||
|
| `LESAVKA_SONAR_ENFORCE` | CI gate enforcement override |
|
||||||
|
| `LESAVKA_SUPPLY_CHAIN_ENFORCE_TOOLS` | CI gate enforcement override |
|
||||||
|
| `LESAVKA_TAP_AUDIO` | client media capture/playback override |
|
||||||
|
| `LESAVKA_TEST_CAM_U32` | test/build contract variable; not runtime operator config |
|
||||||
|
| `LESAVKA_TEST_CAP_CAMERA` | test/build contract variable; not runtime operator config |
|
||||||
|
| `LESAVKA_TEST_CAP_MIC` | test/build contract variable; not runtime operator config |
|
||||||
|
| `LESAVKA_TEST_GATE_PUSHGATEWAY_JOB` | test/build contract variable; not runtime operator config |
|
||||||
|
| `LESAVKA_TEST_SKIP_APP` | test/build contract variable; not runtime operator config |
|
||||||
|
| `LESAVKA_TEST_SWAY_GET_OUTPUTS_EXIT` | test/build contract variable; not runtime operator config |
|
||||||
|
| `LESAVKA_TEST_SWAY_LOG` | test/build contract variable; not runtime operator config |
|
||||||
|
| `LESAVKA_TEST_SWAY_OUTPUTS` | test/build contract variable; not runtime operator config |
|
||||||
|
| `LESAVKA_TEST_SWAY_PLACE_EXIT` | test/build contract variable; not runtime operator config |
|
||||||
|
| `LESAVKA_TEST_U32` | test/build contract variable; not runtime operator config |
|
||||||
|
| `LESAVKA_TEST_U32_OPT` | test/build contract variable; not runtime operator config |
|
||||||
|
| `LESAVKA_TEST_U8` | test/build contract variable; not runtime operator config |
|
||||||
|
| `LESAVKA_TEST_USIZE` | test/build contract variable; not runtime operator config |
|
||||||
|
| `LESAVKA_TEST_VIDEO_SOURCE` | test/build contract variable; not runtime operator config |
|
||||||
|
| `LESAVKA_TOUCHPAD_SCALE` | input routing/clipboard override |
|
||||||
|
| `LESAVKA_UAC_DEV` | server hardware/device override |
|
||||||
|
| `LESAVKA_UPLINK_CAMERA_PREVIEW` | client media capture/playback override |
|
||||||
|
| `LESAVKA_UPLINK_MIC_LEVEL` | client media capture/playback override |
|
||||||
|
| `LESAVKA_USB_RECOVERY_` | USB recovery timing override |
|
||||||
|
| `LESAVKA_USB_RECOVERY_CYCLE_WAIT_MS` | USB recovery timing override |
|
||||||
|
| `LESAVKA_USB_RECOVERY_FINAL_WAIT_MS` | USB recovery timing override |
|
||||||
|
| `LESAVKA_USB_RECOVERY_REBUILD_WAIT_MS` | USB recovery timing override |
|
||||||
|
| `LESAVKA_UVC_APP_BLOCK` | server hardware/device override |
|
||||||
|
| `LESAVKA_UVC_BLOCKING` | server hardware/device override |
|
||||||
|
| `LESAVKA_UVC_BULK` | server hardware/device override |
|
||||||
|
| `LESAVKA_UVC_BY_PATH_ROOT` | server hardware/device override |
|
||||||
|
| `LESAVKA_UVC_CODEC` | server hardware/device override |
|
||||||
|
| `LESAVKA_UVC_CTRL_BIN` | server hardware/device override |
|
||||||
|
| `LESAVKA_UVC_CTRL_INTF` | server hardware/device override |
|
||||||
|
| `LESAVKA_UVC_CTRL_LEN` | server hardware/device override |
|
||||||
|
| `LESAVKA_UVC_DEBUG` | server hardware/device override |
|
||||||
|
| `LESAVKA_UVC_DEV` | server hardware/device override |
|
||||||
|
| `LESAVKA_UVC_DISABLE_IRQ` | server hardware/device override |
|
||||||
|
| `LESAVKA_UVC_EXTERNAL` | server hardware/device override |
|
||||||
|
| `LESAVKA_UVC_FALLBACK` | server hardware/device override |
|
||||||
|
| `LESAVKA_UVC_FPS` | server hardware/device override |
|
||||||
|
| `LESAVKA_UVC_FRAME_SIZE` | server hardware/device override |
|
||||||
|
| `LESAVKA_UVC_HEIGHT` | server hardware/device override |
|
||||||
|
| `LESAVKA_UVC_INTERVAL` | server hardware/device override |
|
||||||
|
| `LESAVKA_UVC_LIMIT_PCT` | server hardware/device override |
|
||||||
|
| `LESAVKA_UVC_MAXBURST` | server hardware/device override |
|
||||||
|
| `LESAVKA_UVC_MAXPACKET` | server hardware/device override |
|
||||||
|
| `LESAVKA_UVC_MAXPAYLOAD_LIMIT` | server hardware/device override |
|
||||||
|
| `LESAVKA_UVC_MJPEG` | server hardware/device override |
|
||||||
|
| `LESAVKA_UVC_SKIP_UDEV` | server hardware/device override |
|
||||||
|
| `LESAVKA_UVC_STREAMING_INTERVAL` | server hardware/device override |
|
||||||
|
| `LESAVKA_UVC_STREAM_INTF` | server hardware/device override |
|
||||||
|
| `LESAVKA_UVC_WIDTH` | server hardware/device override |
|
||||||
|
| `LESAVKA_VIDEO_MAX_KBIT` | eye preview/video transport override |
|
||||||
|
| `LESAVKA_VIDEO_QUEUE` | eye preview/video transport override |
|
||||||
|
| `LESAVKA_VIEW_MODE` | launcher UI/runtime override |
|
||||||
45
docs/quality-gate.md
Normal file
45
docs/quality-gate.md
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# Lesavka quality gate
|
||||||
|
|
||||||
|
Lesavka follows the Atlas gate order, with one extra lane for media reliability because the product is latency-sensitive and hardware-facing.
|
||||||
|
|
||||||
|
Strict order:
|
||||||
|
|
||||||
|
1. `style/docs` via `scripts/ci/hygiene_gate.sh`
|
||||||
|
2. `LOC/naming` via `scripts/ci/hygiene_gate.sh`
|
||||||
|
3. `coverage` via `scripts/ci/quality_gate.sh`
|
||||||
|
4. `tests` via `scripts/ci/test_gate.sh`
|
||||||
|
5. `media_reliability` via `scripts/ci/media_reliability_gate.sh`
|
||||||
|
6. `gate_glue` via `scripts/ci/gate_glue_gate.sh`
|
||||||
|
7. `sonarqube` via `scripts/ci/sonarqube_gate.sh`
|
||||||
|
8. `supply_chain` and artifact security via `scripts/ci/supply_chain_gate.sh`
|
||||||
|
|
||||||
|
The Jenkinsfile runs those checks in that order. Gate artifacts are archived under `target/*-gate/` and release artifacts under `dist/`.
|
||||||
|
|
||||||
|
## Repository Hygiene
|
||||||
|
|
||||||
|
The hygiene gate fails if generated output is committed, `Cargo.lock` is missing from git, workspace members drift away from `common`, `client`, `server`, and `testing`, direct-run shell scripts are not executable, manual scripts are not marked manual, or new `LESAVKA_*` variables are missing from `docs/operational-env.md`.
|
||||||
|
|
||||||
|
Manual probes live under `scripts/manual/`. They are useful field tools, but they are not CI dependencies unless converted into deterministic tests.
|
||||||
|
|
||||||
|
## Media Reliability
|
||||||
|
|
||||||
|
`media_reliability` is not just a test alias. It protects the pieces that keep video moving without accumulating latency:
|
||||||
|
|
||||||
|
- bounded appsrc/appsink queues
|
||||||
|
- stale-frame dropping over latency buildup
|
||||||
|
- local monotonic sink timestamps
|
||||||
|
- IDR/keyframe recovery after drops
|
||||||
|
- HDMI/UVC sink construction
|
||||||
|
- preview telemetry for FPS, drops, queue depth, and inter-frame gaps
|
||||||
|
|
||||||
|
Real hardware evidence still matters. Put manual soak evidence in `target/media-reliability-gate/manual-soak.json` when validating Zoom/Teams/Slack-class consumers or the Theia HDMI -> UGREEN -> Tethys USB path.
|
||||||
|
|
||||||
|
## Supply Chain And Artifacts
|
||||||
|
|
||||||
|
`scripts/ci/supply_chain_gate.sh` always generates dependency metadata, a dependency tree, secret-scan evidence, and artifact checksums when `dist/*.tar.gz` exists. It runs `cargo-audit` and `cargo-deny` when those tools are installed. Set `LESAVKA_SUPPLY_CHAIN_ENFORCE_TOOLS=1` to hard-fail when either tool is unavailable.
|
||||||
|
|
||||||
|
`build-dist.sh` writes `dist/SHA256SUMS` and a provenance JSON file with version, branch, commit, build URL, toolchain, target, and timestamp.
|
||||||
|
|
||||||
|
## SonarQube
|
||||||
|
|
||||||
|
`scripts/ci/sonarqube_gate.sh` emits explicit `not_applicable` metrics when scanner configuration is absent. Set `LESAVKA_SONAR_ENFORCE=1` in CI once SonarQube credentials and `sonar-scanner` are installed to hard-fail missing or failed Sonar analysis.
|
||||||
@ -5,6 +5,10 @@ ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)
|
|||||||
VERSION=$(awk -F'"' '/^version\s*=/{print $2; exit}' "${ROOT_DIR}/server/Cargo.toml")
|
VERSION=$(awk -F'"' '/^version\s*=/{print $2; exit}' "${ROOT_DIR}/server/Cargo.toml")
|
||||||
TARGET=$(rustc -vV | awk '/^host:/{print $2}')
|
TARGET=$(rustc -vV | awk '/^host:/{print $2}')
|
||||||
GIT_SHA=$(git -C "${ROOT_DIR}" rev-parse --short HEAD)
|
GIT_SHA=$(git -C "${ROOT_DIR}" rev-parse --short HEAD)
|
||||||
|
GIT_BRANCH=${BRANCH_NAME:-${GIT_BRANCH:-}}
|
||||||
|
if [[ -z "${GIT_BRANCH}" ]]; then
|
||||||
|
GIT_BRANCH=$(git -C "${ROOT_DIR}" rev-parse --abbrev-ref HEAD 2>/dev/null || echo unknown)
|
||||||
|
fi
|
||||||
DIST_DIR="${ROOT_DIR}/dist"
|
DIST_DIR="${ROOT_DIR}/dist"
|
||||||
|
|
||||||
mkdir -p "${DIST_DIR}"
|
mkdir -p "${DIST_DIR}"
|
||||||
@ -28,4 +32,21 @@ install -Dm755 "${ROOT_DIR}/target/release/lesavka-client" "${CLIENT_TMP}/lesavk
|
|||||||
tar -czf "${CLIENT_TAR}" -C "${CLIENT_TMP}" lesavka-client
|
tar -czf "${CLIENT_TAR}" -C "${CLIENT_TMP}" lesavka-client
|
||||||
rm -rf "${CLIENT_TMP}"
|
rm -rf "${CLIENT_TMP}"
|
||||||
|
|
||||||
|
sha256sum "${SERVER_TAR}" "${CLIENT_TAR}" >"${DIST_DIR}/SHA256SUMS"
|
||||||
|
|
||||||
|
PROVENANCE="${DIST_DIR}/lesavka-${VERSION}-${TARGET}-${GIT_SHA}.provenance.json"
|
||||||
|
cat >"${PROVENANCE}" <<EOF
|
||||||
|
{
|
||||||
|
"suite": "lesavka",
|
||||||
|
"version": "${VERSION}",
|
||||||
|
"target": "${TARGET}",
|
||||||
|
"commit": "${GIT_SHA}",
|
||||||
|
"branch": "${GIT_BRANCH}",
|
||||||
|
"build_url": "${BUILD_URL:-}",
|
||||||
|
"rustc": "$(rustc --version)",
|
||||||
|
"cargo": "$(cargo --version)",
|
||||||
|
"generated_at_utc": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
echo "Artifacts written to ${DIST_DIR}"
|
echo "Artifacts written to ${DIST_DIR}"
|
||||||
|
|||||||
110
scripts/ci/gate_glue_gate.sh
Executable file
110
scripts/ci/gate_glue_gate.sh
Executable file
@ -0,0 +1,110 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Validate that upstream gates emitted the canonical Atlas metric contract.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)
|
||||||
|
REPORT_DIR="${ROOT_DIR}/target/gate-glue-gate"
|
||||||
|
SUMMARY_JSON="${REPORT_DIR}/summary.json"
|
||||||
|
SUMMARY_TXT="${REPORT_DIR}/summary.txt"
|
||||||
|
METRICS_FILE="${REPORT_DIR}/metrics.prom"
|
||||||
|
PUSHGATEWAY_URL=${QUALITY_GATE_PUSHGATEWAY_URL:-}
|
||||||
|
|
||||||
|
mkdir -p "${REPORT_DIR}"
|
||||||
|
cd "${ROOT_DIR}"
|
||||||
|
|
||||||
|
branch=${BRANCH_NAME:-${GIT_BRANCH:-}}
|
||||||
|
if [[ -z "${branch}" ]]; then
|
||||||
|
branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo unknown)
|
||||||
|
fi
|
||||||
|
commit=${GIT_COMMIT:-}
|
||||||
|
if [[ -z "${commit}" ]]; then
|
||||||
|
commit=$(git rev-parse --short HEAD 2>/dev/null || echo unknown)
|
||||||
|
fi
|
||||||
|
|
||||||
|
python3 - "${SUMMARY_JSON}" "${SUMMARY_TXT}" "${METRICS_FILE}" "${branch}" "${commit}" "${ROOT_DIR}" <<'PY'
|
||||||
|
import json
|
||||||
|
import pathlib
|
||||||
|
import sys
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
summary_path = pathlib.Path(sys.argv[1])
|
||||||
|
text_path = pathlib.Path(sys.argv[2])
|
||||||
|
metrics_path = pathlib.Path(sys.argv[3])
|
||||||
|
branch = sys.argv[4]
|
||||||
|
commit = sys.argv[5]
|
||||||
|
root = pathlib.Path(sys.argv[6])
|
||||||
|
|
||||||
|
def esc(value: str) -> str:
|
||||||
|
return value.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"')
|
||||||
|
|
||||||
|
metric_files = [
|
||||||
|
root / 'target' / 'hygiene-gate' / 'metrics.prom',
|
||||||
|
root / 'target' / 'quality-gate' / 'metrics.prom',
|
||||||
|
root / 'target' / 'test-gate' / 'metrics.prom',
|
||||||
|
root / 'target' / 'media-reliability-gate' / 'metrics.prom',
|
||||||
|
]
|
||||||
|
required_metrics = {
|
||||||
|
'platform_quality_gate_runs_total': False,
|
||||||
|
'platform_quality_gate_tests_total': False,
|
||||||
|
'platform_quality_gate_checks_total': False,
|
||||||
|
'platform_quality_gate_workspace_line_coverage_percent': False,
|
||||||
|
'platform_quality_gate_source_lines_over_500_total': False,
|
||||||
|
}
|
||||||
|
required_checks = {'tests', 'coverage', 'loc', 'style', 'media_reliability'}
|
||||||
|
seen_checks: set[str] = set()
|
||||||
|
missing_files: list[str] = []
|
||||||
|
for path in metric_files:
|
||||||
|
if not path.exists():
|
||||||
|
missing_files.append(path.relative_to(root).as_posix())
|
||||||
|
continue
|
||||||
|
text = path.read_text(encoding='utf-8', errors='replace')
|
||||||
|
for metric in required_metrics:
|
||||||
|
if metric in text:
|
||||||
|
required_metrics[metric] = True
|
||||||
|
for check in required_checks:
|
||||||
|
if f'check="{check}"' in text:
|
||||||
|
seen_checks.add(check)
|
||||||
|
|
||||||
|
missing_metrics = [name for name, present in required_metrics.items() if not present]
|
||||||
|
missing_checks = sorted(required_checks - seen_checks)
|
||||||
|
status = 'failed' if missing_files or missing_metrics or missing_checks else 'ok'
|
||||||
|
summary = {
|
||||||
|
'suite': 'lesavka',
|
||||||
|
'check': 'gate_glue',
|
||||||
|
'status': status,
|
||||||
|
'branch': branch,
|
||||||
|
'commit': commit,
|
||||||
|
'generated_at': datetime.now(timezone.utc).isoformat(),
|
||||||
|
'missing_files': missing_files,
|
||||||
|
'missing_metrics': missing_metrics,
|
||||||
|
'missing_checks': missing_checks,
|
||||||
|
}
|
||||||
|
summary_path.write_text(json.dumps(summary, indent=2, sort_keys=True) + '\n', encoding='utf-8')
|
||||||
|
lines = ['gate glue report', f'status: {status}', f'branch: {branch}', f'commit: {commit}', '']
|
||||||
|
for key in ('missing_files', 'missing_metrics', 'missing_checks'):
|
||||||
|
values = summary[key]
|
||||||
|
lines.append(f'{key}: {", ".join(values) if values else "none"}')
|
||||||
|
text_path.write_text('\n'.join(lines) + '\n', encoding='utf-8')
|
||||||
|
labels = f'suite="lesavka",branch="{esc(branch)}",commit="{esc(commit)}"'
|
||||||
|
ok = 1 if status == 'ok' else 0
|
||||||
|
failed = 1 - ok
|
||||||
|
metrics = [
|
||||||
|
'# HELP platform_quality_gate_checks_total Check outcomes from the latest lesavka gate run.',
|
||||||
|
'# TYPE platform_quality_gate_checks_total gauge',
|
||||||
|
f'platform_quality_gate_checks_total{{{labels},check="gate_glue",status="ok"}} {ok}',
|
||||||
|
f'platform_quality_gate_checks_total{{{labels},check="gate_glue",status="failed"}} {failed}',
|
||||||
|
]
|
||||||
|
metrics_path.write_text('\n'.join(metrics) + '\n', encoding='utf-8')
|
||||||
|
print(text_path.read_text(encoding='utf-8'))
|
||||||
|
if status != 'ok':
|
||||||
|
raise SystemExit(1)
|
||||||
|
PY
|
||||||
|
status=$?
|
||||||
|
|
||||||
|
if [[ -n "${PUSHGATEWAY_URL}" ]]; then
|
||||||
|
curl --fail --silent --show-error \
|
||||||
|
--data-binary @"${METRICS_FILE}" \
|
||||||
|
"${PUSHGATEWAY_URL%/}/metrics/job/lesavka-gate-glue-gate/suite/lesavka" || status=$?
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit "${status}"
|
||||||
@ -6,15 +6,32 @@ REPORT_DIR="${ROOT_DIR}/target/hygiene-gate"
|
|||||||
CLIPPY_JSON="${REPORT_DIR}/clippy.json"
|
CLIPPY_JSON="${REPORT_DIR}/clippy.json"
|
||||||
SUMMARY_TXT="${REPORT_DIR}/summary.txt"
|
SUMMARY_TXT="${REPORT_DIR}/summary.txt"
|
||||||
BASELINE_JSON="${ROOT_DIR}/scripts/ci/hygiene_gate_baseline.json"
|
BASELINE_JSON="${ROOT_DIR}/scripts/ci/hygiene_gate_baseline.json"
|
||||||
|
METADATA_JSON="${REPORT_DIR}/cargo-metadata.json"
|
||||||
|
METRICS_FILE="${REPORT_DIR}/metrics.prom"
|
||||||
|
|
||||||
mkdir -p "${REPORT_DIR}"
|
mkdir -p "${REPORT_DIR}"
|
||||||
|
|
||||||
|
cargo fmt --all -- --check
|
||||||
|
cargo check --workspace --all-targets
|
||||||
|
cargo metadata --locked --format-version 1 >"${METADATA_JSON}"
|
||||||
cargo clippy --workspace --all-targets --message-format json -- -W clippy::pedantic >"${CLIPPY_JSON}"
|
cargo clippy --workspace --all-targets --message-format json -- -W clippy::pedantic >"${CLIPPY_JSON}"
|
||||||
|
|
||||||
python3 - "${CLIPPY_JSON}" "${BASELINE_JSON}" "${SUMMARY_TXT}" "${ROOT_DIR}" <<'PY'
|
branch=${BRANCH_NAME:-${GIT_BRANCH:-}}
|
||||||
|
if [[ -z "${branch}" ]]; then
|
||||||
|
branch=$(git -C "${ROOT_DIR}" rev-parse --abbrev-ref HEAD 2>/dev/null || echo unknown)
|
||||||
|
fi
|
||||||
|
commit=${GIT_COMMIT:-}
|
||||||
|
if [[ -z "${commit}" ]]; then
|
||||||
|
commit=$(git -C "${ROOT_DIR}" rev-parse --short HEAD 2>/dev/null || echo unknown)
|
||||||
|
fi
|
||||||
|
|
||||||
|
python3 - "${CLIPPY_JSON}" "${BASELINE_JSON}" "${SUMMARY_TXT}" "${ROOT_DIR}" "${METRICS_FILE}" "${branch}" "${commit}" <<'PY'
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
import re
|
import re
|
||||||
|
import stat
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
@ -22,8 +39,20 @@ clippy_path = pathlib.Path(sys.argv[1])
|
|||||||
baseline_path = pathlib.Path(sys.argv[2])
|
baseline_path = pathlib.Path(sys.argv[2])
|
||||||
summary_path = pathlib.Path(sys.argv[3])
|
summary_path = pathlib.Path(sys.argv[3])
|
||||||
root = pathlib.Path(sys.argv[4])
|
root = pathlib.Path(sys.argv[4])
|
||||||
|
metrics_path = pathlib.Path(sys.argv[5])
|
||||||
|
branch = sys.argv[6]
|
||||||
|
commit = sys.argv[7]
|
||||||
|
|
||||||
fn_re = re.compile(r'^\s*(?:pub(?:\([^)]+\))?\s+)?(?:async\s+)?(?:unsafe\s+)?fn\s+\w+')
|
fn_re = re.compile(r'^\s*(?:pub(?:\([^)]+\))?\s+)?(?:async\s+)?(?:unsafe\s+)?fn\s+\w+')
|
||||||
|
env_re = re.compile(r'LESAVKA_[A-Z0-9_]+')
|
||||||
|
lazy_name_tokens = {'part', 'piece', 'chunk', 'misc', 'stuff', 'helpers2', 'new', 'old', 'tmp'}
|
||||||
|
expected_workspace_members = {'common', 'client', 'server', 'testing'}
|
||||||
|
required_binary_paths = {
|
||||||
|
'lesavka-client': 'client/Cargo.toml',
|
||||||
|
'lesavka-server': 'server/Cargo.toml',
|
||||||
|
'lesavka-uvc': 'server/Cargo.toml',
|
||||||
|
'lesavka-relayctl': 'client/src/bin/lesavka-relayctl.rs',
|
||||||
|
}
|
||||||
|
|
||||||
def load_json_lines(path: pathlib.Path):
|
def load_json_lines(path: pathlib.Path):
|
||||||
for raw in path.read_text(encoding='utf-8').splitlines():
|
for raw in path.read_text(encoding='utf-8').splitlines():
|
||||||
@ -41,6 +70,142 @@ def repo_relative(path: str) -> str | None:
|
|||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def run_git(*args: str) -> list[str]:
|
||||||
|
proc = subprocess.run(
|
||||||
|
['git', '-C', str(root), *args],
|
||||||
|
check=True,
|
||||||
|
text=True,
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
return [line for line in proc.stdout.splitlines() if line]
|
||||||
|
|
||||||
|
def tracked_files() -> list[str]:
|
||||||
|
return run_git('ls-files')
|
||||||
|
|
||||||
|
def parse_workspace_members() -> set[str]:
|
||||||
|
text = (root / 'Cargo.toml').read_text(encoding='utf-8')
|
||||||
|
match = re.search(r'members\s*=\s*\[(?P<body>.*?)\]', text, re.S)
|
||||||
|
if not match:
|
||||||
|
return set()
|
||||||
|
return set(re.findall(r'"([^"]+)"', match.group('body')))
|
||||||
|
|
||||||
|
def repo_policy_violations(files: list[str]) -> list[str]:
|
||||||
|
violations: list[str] = []
|
||||||
|
tracked = set(files)
|
||||||
|
if 'Cargo.lock' not in tracked:
|
||||||
|
violations.append('Cargo.lock: must be committed for reproducible Rust builds')
|
||||||
|
|
||||||
|
members = parse_workspace_members()
|
||||||
|
if members != expected_workspace_members:
|
||||||
|
violations.append(
|
||||||
|
f'Cargo.toml: workspace members must be explicit {sorted(expected_workspace_members)}, found {sorted(members)}'
|
||||||
|
)
|
||||||
|
|
||||||
|
generated_patterns = (
|
||||||
|
re.compile(r'(^|/)target/'),
|
||||||
|
re.compile(r'(^|/)dist/'),
|
||||||
|
re.compile(r'(^|/)logs/'),
|
||||||
|
re.compile(r'(^|/)coverage/'),
|
||||||
|
re.compile(r'(^|/)captures/'),
|
||||||
|
re.compile(r'\.(log|h264|aac|wav|rgba)$'),
|
||||||
|
)
|
||||||
|
for path in files:
|
||||||
|
if pathlib.Path(path).name == 'AGENTS.md':
|
||||||
|
violations.append(f'{path}: local AGENTS notes must not be committed')
|
||||||
|
if any(pattern.search(path) for pattern in generated_patterns):
|
||||||
|
violations.append(f'{path}: generated/build/runtime artifact must not be committed')
|
||||||
|
|
||||||
|
for name, marker in required_binary_paths.items():
|
||||||
|
marker_path = root / marker
|
||||||
|
if marker.endswith('.rs'):
|
||||||
|
if not marker_path.exists():
|
||||||
|
violations.append(f'{name}: stable public binary source {marker} is missing')
|
||||||
|
elif name not in marker_path.read_text(encoding='utf-8'):
|
||||||
|
violations.append(f'{name}: stable public binary name missing from {marker}')
|
||||||
|
|
||||||
|
return violations
|
||||||
|
|
||||||
|
def naming_policy_violations(files: list[str]) -> list[str]:
|
||||||
|
violations: list[str] = []
|
||||||
|
for path in files:
|
||||||
|
if path.startswith('.git/') or path.startswith('target/'):
|
||||||
|
continue
|
||||||
|
stem = pathlib.Path(path).stem.lower()
|
||||||
|
tokens = [token for token in re.split(r'[^a-z0-9]+', stem) if token]
|
||||||
|
for token in tokens:
|
||||||
|
if token in lazy_name_tokens:
|
||||||
|
violations.append(f'{path}: lazy split token "{token}" is not allowed in filenames')
|
||||||
|
|
||||||
|
if path.endswith('.rs'):
|
||||||
|
rel = pathlib.Path(path)
|
||||||
|
if len(rel.parts) >= 2 and rel.parts[-2] == 'bin' and rel.stem.startswith('lesavka-'):
|
||||||
|
continue
|
||||||
|
if not re.match(r'^[a-z0-9_]+$', rel.stem):
|
||||||
|
violations.append(f'{path}: Rust filenames must use meaningful snake_case')
|
||||||
|
return violations
|
||||||
|
|
||||||
|
def script_policy_violations(files: list[str]) -> list[str]:
|
||||||
|
violations: list[str] = []
|
||||||
|
ci_text_parts: list[str] = []
|
||||||
|
ci_paths = [root / 'Jenkinsfile', *sorted((root / 'scripts' / 'ci').glob('*.sh'))]
|
||||||
|
for path in ci_paths:
|
||||||
|
if path.exists():
|
||||||
|
ci_text_parts.append(path.read_text(encoding='utf-8', errors='replace'))
|
||||||
|
ci_text = '\n'.join(ci_text_parts)
|
||||||
|
if re.search(r'(?:^|\s)(?:sh\s+)?scripts/manual/', ci_text):
|
||||||
|
violations.append('scripts/manual: manual probes must not be required by CI')
|
||||||
|
|
||||||
|
for file in sorted((root / 'scripts').rglob('*')):
|
||||||
|
if not file.is_file():
|
||||||
|
continue
|
||||||
|
rel = repo_relative(str(file))
|
||||||
|
if rel is None:
|
||||||
|
continue
|
||||||
|
text = file.read_text(encoding='utf-8', errors='replace')
|
||||||
|
lines = text.splitlines()
|
||||||
|
first = lines[0] if lines else ''
|
||||||
|
if first.startswith('#!'):
|
||||||
|
mode = file.stat().st_mode
|
||||||
|
if not mode & stat.S_IXUSR:
|
||||||
|
violations.append(f'{rel}: shebang script must be executable')
|
||||||
|
header = '\n'.join(lines[:25])
|
||||||
|
if 'bash' in first and 'set -euo pipefail' not in header:
|
||||||
|
violations.append(f'{rel}: bash scripts must use set -euo pipefail where safe')
|
||||||
|
|
||||||
|
if rel.startswith('scripts/manual/') and rel.endswith('.sh'):
|
||||||
|
header = '\n'.join(lines[:12]).lower()
|
||||||
|
if 'manual:' not in header or 'not part of ci' not in header:
|
||||||
|
violations.append(f'{rel}: manual scripts must be clearly marked manual and outside CI')
|
||||||
|
return violations
|
||||||
|
|
||||||
|
def env_doc_violations(files: list[str]) -> list[str]:
|
||||||
|
docs_path = root / 'docs' / 'operational-env.md'
|
||||||
|
if not docs_path.exists():
|
||||||
|
return ['docs/operational-env.md: missing env-var inventory']
|
||||||
|
docs_text = docs_path.read_text(encoding='utf-8')
|
||||||
|
|
||||||
|
found: set[str] = set()
|
||||||
|
scan_prefixes = ('client/', 'common/', 'server/', 'testing/', 'scripts/')
|
||||||
|
scan_files = [
|
||||||
|
path for path in files
|
||||||
|
if path == 'Jenkinsfile' or path.endswith('.toml') or path.startswith(scan_prefixes)
|
||||||
|
]
|
||||||
|
for path in scan_files:
|
||||||
|
full = root / path
|
||||||
|
if not full.exists() or full.is_dir():
|
||||||
|
continue
|
||||||
|
text = full.read_text(encoding='utf-8', errors='replace')
|
||||||
|
found.update(env_re.findall(text))
|
||||||
|
|
||||||
|
return [
|
||||||
|
f'{var}: LESAVKA env var is used but missing from docs/operational-env.md'
|
||||||
|
for var in sorted(found)
|
||||||
|
if var not in docs_text
|
||||||
|
]
|
||||||
|
|
||||||
|
def esc(value: str) -> str:
|
||||||
|
return value.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"')
|
||||||
|
|
||||||
def clippy_counts(path: pathlib.Path) -> dict[str, int]:
|
def clippy_counts(path: pathlib.Path) -> dict[str, int]:
|
||||||
counts: dict[str, int] = defaultdict(int)
|
counts: dict[str, int] = defaultdict(int)
|
||||||
for item in load_json_lines(path):
|
for item in load_json_lines(path):
|
||||||
@ -204,6 +369,11 @@ for path, current_entry in current.items():
|
|||||||
|
|
||||||
layout_violations = integration_layout_violations()
|
layout_violations = integration_layout_violations()
|
||||||
testing_violations = testing_contract_violations()
|
testing_violations = testing_contract_violations()
|
||||||
|
files = tracked_files()
|
||||||
|
repo_violations = repo_policy_violations(files)
|
||||||
|
naming_violations = naming_policy_violations(files)
|
||||||
|
script_violations = script_policy_violations(files)
|
||||||
|
env_violations = env_doc_violations(files)
|
||||||
|
|
||||||
totals = {
|
totals = {
|
||||||
'files': len(current),
|
'files': len(current),
|
||||||
@ -220,6 +390,10 @@ lines.append(f"clippy warnings tracked: {totals['clippy_warnings']}")
|
|||||||
lines.append(f"non-trivial undocumented functions tracked: {totals['doc_debt']}")
|
lines.append(f"non-trivial undocumented functions tracked: {totals['doc_debt']}")
|
||||||
lines.append(f'legacy integration-test layout violations: {len(layout_violations)}')
|
lines.append(f'legacy integration-test layout violations: {len(layout_violations)}')
|
||||||
lines.append(f'testing module contract violations: {len(testing_violations)}')
|
lines.append(f'testing module contract violations: {len(testing_violations)}')
|
||||||
|
lines.append(f'repository policy violations: {len(repo_violations)}')
|
||||||
|
lines.append(f'naming policy violations: {len(naming_violations)}')
|
||||||
|
lines.append(f'script policy violations: {len(script_violations)}')
|
||||||
|
lines.append(f'env documentation violations: {len(env_violations)}')
|
||||||
lines.append('')
|
lines.append('')
|
||||||
lines.append('path | loc | clippy warnings | doc debt | baseline status')
|
lines.append('path | loc | clippy warnings | doc debt | baseline status')
|
||||||
lines.append('-' * 78)
|
lines.append('-' * 78)
|
||||||
@ -258,15 +432,45 @@ if testing_violations:
|
|||||||
lines.append('-' * 78)
|
lines.append('-' * 78)
|
||||||
lines.extend(testing_violations)
|
lines.extend(testing_violations)
|
||||||
|
|
||||||
|
policy_sections = [
|
||||||
|
('repository policy violations', repo_violations),
|
||||||
|
('naming policy violations', naming_violations),
|
||||||
|
('script policy violations', script_violations),
|
||||||
|
('env documentation violations', env_violations),
|
||||||
|
]
|
||||||
|
for title, violations in policy_sections:
|
||||||
|
if violations:
|
||||||
|
lines.append('')
|
||||||
|
lines.append(title)
|
||||||
|
lines.append('-' * 78)
|
||||||
|
lines.extend(violations)
|
||||||
|
|
||||||
summary_path.write_text('\n'.join(lines) + '\n', encoding='utf-8')
|
summary_path.write_text('\n'.join(lines) + '\n', encoding='utf-8')
|
||||||
print(summary_path.read_text(encoding='utf-8'))
|
print(summary_path.read_text(encoding='utf-8'))
|
||||||
|
|
||||||
if regressions or layout_violations or testing_violations:
|
policy_violations = repo_violations + naming_violations + script_violations + env_violations
|
||||||
|
failed = bool(regressions or layout_violations or testing_violations or policy_violations)
|
||||||
|
labels = f'suite="lesavka",branch="{esc(branch)}",commit="{esc(commit)}"'
|
||||||
|
ok_value = 0 if failed else 1
|
||||||
|
failed_value = 1 if failed else 0
|
||||||
|
metrics = [
|
||||||
|
'# HELP platform_quality_gate_checks_total Check outcomes from the latest lesavka gate run.',
|
||||||
|
'# TYPE platform_quality_gate_checks_total gauge',
|
||||||
|
f'platform_quality_gate_checks_total{{{labels},check="style",status="ok"}} {ok_value}',
|
||||||
|
f'platform_quality_gate_checks_total{{{labels},check="style",status="failed"}} {failed_value}',
|
||||||
|
f'platform_quality_gate_checks_total{{{labels},check="loc",status="ok"}} {ok_value}',
|
||||||
|
f'platform_quality_gate_checks_total{{{labels},check="loc",status="failed"}} {failed_value}',
|
||||||
|
]
|
||||||
|
metrics_path.write_text('\n'.join(metrics) + '\n', encoding='utf-8')
|
||||||
|
|
||||||
|
if failed:
|
||||||
for line in regressions:
|
for line in regressions:
|
||||||
print(line, file=sys.stderr)
|
print(line, file=sys.stderr)
|
||||||
for line in layout_violations:
|
for line in layout_violations:
|
||||||
print(line, file=sys.stderr)
|
print(line, file=sys.stderr)
|
||||||
for line in testing_violations:
|
for line in testing_violations:
|
||||||
print(line, file=sys.stderr)
|
print(line, file=sys.stderr)
|
||||||
|
for line in policy_violations:
|
||||||
|
print(line, file=sys.stderr)
|
||||||
raise SystemExit(1)
|
raise SystemExit(1)
|
||||||
PY
|
PY
|
||||||
|
|||||||
@ -98,12 +98,12 @@
|
|||||||
"client/src/launcher/ui.rs": {
|
"client/src/launcher/ui.rs": {
|
||||||
"clippy_warnings": 64,
|
"clippy_warnings": 64,
|
||||||
"doc_debt": 23,
|
"doc_debt": 23,
|
||||||
"loc": 2635
|
"loc": 2650
|
||||||
},
|
},
|
||||||
"client/src/launcher/ui_components.rs": {
|
"client/src/launcher/ui_components.rs": {
|
||||||
"clippy_warnings": 12,
|
"clippy_warnings": 12,
|
||||||
"doc_debt": 18,
|
"doc_debt": 18,
|
||||||
"loc": 1595
|
"loc": 1599
|
||||||
},
|
},
|
||||||
"client/src/launcher/ui_runtime.rs": {
|
"client/src/launcher/ui_runtime.rs": {
|
||||||
"clippy_warnings": 74,
|
"clippy_warnings": 74,
|
||||||
|
|||||||
166
scripts/ci/media_reliability_gate.sh
Executable file
166
scripts/ci/media_reliability_gate.sh
Executable file
@ -0,0 +1,166 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Run deterministic media/device contracts and emit Atlas quality metrics.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)
|
||||||
|
REPORT_DIR="${ROOT_DIR}/target/media-reliability-gate"
|
||||||
|
TEST_LOG="${REPORT_DIR}/cargo-test.log"
|
||||||
|
SUMMARY_JSON="${REPORT_DIR}/summary.json"
|
||||||
|
SUMMARY_TXT="${REPORT_DIR}/summary.txt"
|
||||||
|
METRICS_FILE="${REPORT_DIR}/metrics.prom"
|
||||||
|
PUSHGATEWAY_URL=${QUALITY_GATE_PUSHGATEWAY_URL:-}
|
||||||
|
PUSHGATEWAY_JOB=${LESAVKA_MEDIA_GATE_PUSHGATEWAY_JOB:-lesavka-media-reliability-gate}
|
||||||
|
|
||||||
|
mkdir -p "${REPORT_DIR}"
|
||||||
|
cd "${ROOT_DIR}"
|
||||||
|
|
||||||
|
branch=${BRANCH_NAME:-${GIT_BRANCH:-}}
|
||||||
|
if [[ -z "${branch}" ]]; then
|
||||||
|
branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo unknown)
|
||||||
|
fi
|
||||||
|
commit=${GIT_COMMIT:-}
|
||||||
|
if [[ -z "${commit}" ]]; then
|
||||||
|
commit=$(git rev-parse --short HEAD 2>/dev/null || echo unknown)
|
||||||
|
fi
|
||||||
|
build_url=${BUILD_URL:-}
|
||||||
|
|
||||||
|
MEDIA_TESTS=(
|
||||||
|
--test client_camera_include_contract
|
||||||
|
--test client_launcher_runtime_contract
|
||||||
|
--test client_microphone_include_contract
|
||||||
|
--test client_microphone_source_contract
|
||||||
|
--test client_output_video_include_contract
|
||||||
|
--test handshake_camera_contract
|
||||||
|
--test server_camera_contract
|
||||||
|
--test server_camera_runtime_contract
|
||||||
|
--test server_upstream_media_contract
|
||||||
|
--test server_uvc_runtime_contract
|
||||||
|
--test server_video_include_contract
|
||||||
|
--test server_video_sink_smoke_contract
|
||||||
|
--test server_video_sinks_include_contract
|
||||||
|
--test video_downstream_feed_contract
|
||||||
|
--test video_support_contract
|
||||||
|
)
|
||||||
|
|
||||||
|
start_seconds=$(date +%s)
|
||||||
|
status=0
|
||||||
|
set +e
|
||||||
|
cargo test -p lesavka_testing "${MEDIA_TESTS[@]}" --color never 2>&1 | tee "${TEST_LOG}"
|
||||||
|
status=${PIPESTATUS[0]}
|
||||||
|
set -e
|
||||||
|
end_seconds=$(date +%s)
|
||||||
|
duration_seconds=$((end_seconds - start_seconds))
|
||||||
|
|
||||||
|
python3 - "${SUMMARY_JSON}" "${SUMMARY_TXT}" "${METRICS_FILE}" "${status}" "${duration_seconds}" "${branch}" "${commit}" "${build_url}" "${REPORT_DIR}" <<'PY'
|
||||||
|
import json
|
||||||
|
import pathlib
|
||||||
|
import sys
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
summary_path = pathlib.Path(sys.argv[1])
|
||||||
|
text_path = pathlib.Path(sys.argv[2])
|
||||||
|
metrics_path = pathlib.Path(sys.argv[3])
|
||||||
|
status = int(sys.argv[4])
|
||||||
|
duration_seconds = int(sys.argv[5])
|
||||||
|
branch = sys.argv[6]
|
||||||
|
commit = sys.argv[7]
|
||||||
|
build_url = sys.argv[8]
|
||||||
|
report_dir = pathlib.Path(sys.argv[9])
|
||||||
|
manual_report = report_dir / 'manual-soak.json'
|
||||||
|
|
||||||
|
def esc(value: str) -> str:
|
||||||
|
return value.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"')
|
||||||
|
|
||||||
|
manual_checks = [
|
||||||
|
{
|
||||||
|
'name': 'zoom_equivalent_webcam_consumer',
|
||||||
|
'status': 'not_applicable' if not manual_report.exists() else 'reported',
|
||||||
|
'why': 'requires a real UVC/HDMI consumer such as Zoom, Teams, Slack, or capture software',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'ten_minute_soak',
|
||||||
|
'status': 'not_applicable' if not manual_report.exists() else 'reported',
|
||||||
|
'why': 'requires sustained live hardware output and should be attached as manual-soak.json',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'hdmi_capture_adapter_path',
|
||||||
|
'status': 'not_applicable' if not manual_report.exists() else 'reported',
|
||||||
|
'why': 'requires the Theia HDMI -> UGREEN -> Tethys USB path',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
tracked_signals = [
|
||||||
|
'frame_count',
|
||||||
|
'effective_fps',
|
||||||
|
'dropped_frames',
|
||||||
|
'queue_depth',
|
||||||
|
'max_inter_frame_gap_ms',
|
||||||
|
'decode_errors',
|
||||||
|
'appsrc_push_failures',
|
||||||
|
'artifact_score',
|
||||||
|
'latency_estimate_ms',
|
||||||
|
'idr_recovery_after_drop',
|
||||||
|
'synthetic_moving_pattern_distortion',
|
||||||
|
]
|
||||||
|
|
||||||
|
summary = {
|
||||||
|
'suite': 'lesavka',
|
||||||
|
'branch': branch,
|
||||||
|
'commit': commit,
|
||||||
|
'build_url': build_url,
|
||||||
|
'generated_at': datetime.now(timezone.utc).isoformat(),
|
||||||
|
'status': 'ok' if status == 0 else 'failed',
|
||||||
|
'duration_seconds': duration_seconds,
|
||||||
|
'deterministic_tests': 'cargo test -p lesavka_testing media reliability contract subset',
|
||||||
|
'tracked_media_signals': tracked_signals,
|
||||||
|
'manual_checks': manual_checks,
|
||||||
|
}
|
||||||
|
summary_path.write_text(json.dumps(summary, indent=2, sort_keys=True) + '\n', encoding='utf-8')
|
||||||
|
|
||||||
|
lines = [
|
||||||
|
'media reliability gate report',
|
||||||
|
f'status: {summary["status"]}',
|
||||||
|
f'branch: {branch}',
|
||||||
|
f'commit: {commit}',
|
||||||
|
f'duration_seconds: {duration_seconds}',
|
||||||
|
'',
|
||||||
|
'deterministic coverage',
|
||||||
|
'- bounded appsrc/appsink queue contracts',
|
||||||
|
'- stale-frame/drop-over-latency contracts',
|
||||||
|
'- local monotonic timestamp contracts',
|
||||||
|
'- IDR/keyframe recovery contracts',
|
||||||
|
'- HDMI/UVC sink construction contracts',
|
||||||
|
'- preview telemetry and freeze-signal contracts',
|
||||||
|
'',
|
||||||
|
'manual hardware evidence slots',
|
||||||
|
]
|
||||||
|
for check in manual_checks:
|
||||||
|
lines.append(f'- {check["name"]}: {check["status"]} ({check["why"]})')
|
||||||
|
text_path.write_text('\n'.join(lines) + '\n', encoding='utf-8')
|
||||||
|
|
||||||
|
labels = f'suite="lesavka",branch="{esc(branch)}",commit="{esc(commit)}"'
|
||||||
|
ok = 1 if status == 0 else 0
|
||||||
|
failed = 0 if status == 0 else 1
|
||||||
|
metrics = [
|
||||||
|
'# HELP platform_quality_gate_checks_total Check outcomes from the latest lesavka gate run.',
|
||||||
|
'# TYPE platform_quality_gate_checks_total gauge',
|
||||||
|
f'platform_quality_gate_checks_total{{{labels},check="media_reliability",status="ok"}} {ok}',
|
||||||
|
f'platform_quality_gate_checks_total{{{labels},check="media_reliability",status="failed"}} {failed}',
|
||||||
|
'# HELP lesavka_media_reliability_manual_check_info Manual media reliability evidence slots.',
|
||||||
|
'# TYPE lesavka_media_reliability_manual_check_info gauge',
|
||||||
|
]
|
||||||
|
for check in manual_checks:
|
||||||
|
metrics.append(
|
||||||
|
f'lesavka_media_reliability_manual_check_info{{{labels},check="{esc(check["name"])}",status="{esc(check["status"])}"}} 1'
|
||||||
|
)
|
||||||
|
metrics_path.write_text('\n'.join(metrics) + '\n', encoding='utf-8')
|
||||||
|
print(text_path.read_text(encoding='utf-8'))
|
||||||
|
PY
|
||||||
|
|
||||||
|
if [[ -n "${PUSHGATEWAY_URL}" ]]; then
|
||||||
|
curl --fail --silent --show-error \
|
||||||
|
--data-binary @"${METRICS_FILE}" \
|
||||||
|
"${PUSHGATEWAY_URL%/}/metrics/job/${PUSHGATEWAY_JOB}/suite/lesavka" || status=$?
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit "${status}"
|
||||||
15
scripts/ci/platform_quality_gate.sh
Executable file
15
scripts/ci/platform_quality_gate.sh
Executable file
@ -0,0 +1,15 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Run the complete Lesavka gate in Atlas order.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)
|
||||||
|
cd "${ROOT_DIR}"
|
||||||
|
|
||||||
|
scripts/ci/hygiene_gate.sh
|
||||||
|
QUALITY_GATE_PUSHGATEWAY_URL="${QUALITY_GATE_PUSHGATEWAY_URL:-}" scripts/ci/quality_gate.sh
|
||||||
|
QUALITY_GATE_PUSHGATEWAY_URL="${QUALITY_GATE_PUSHGATEWAY_URL:-}" scripts/ci/test_gate.sh
|
||||||
|
QUALITY_GATE_PUSHGATEWAY_URL="${QUALITY_GATE_PUSHGATEWAY_URL:-}" scripts/ci/media_reliability_gate.sh
|
||||||
|
QUALITY_GATE_PUSHGATEWAY_URL="${QUALITY_GATE_PUSHGATEWAY_URL:-}" scripts/ci/gate_glue_gate.sh
|
||||||
|
QUALITY_GATE_PUSHGATEWAY_URL="${QUALITY_GATE_PUSHGATEWAY_URL:-}" scripts/ci/sonarqube_gate.sh
|
||||||
|
scripts/ci/build-dist.sh
|
||||||
|
QUALITY_GATE_PUSHGATEWAY_URL="${QUALITY_GATE_PUSHGATEWAY_URL:-}" scripts/ci/supply_chain_gate.sh
|
||||||
@ -12,11 +12,20 @@ PUSHGATEWAY_URL=${QUALITY_GATE_PUSHGATEWAY_URL:-}
|
|||||||
|
|
||||||
mkdir -p "${REPORT_DIR}"
|
mkdir -p "${REPORT_DIR}"
|
||||||
|
|
||||||
cat >"${METRICS_FILE}" <<'METRICS'
|
branch=${BRANCH_NAME:-${GIT_BRANCH:-}}
|
||||||
|
if [[ -z "${branch}" ]]; then
|
||||||
|
branch=$(git -C "${ROOT_DIR}" rev-parse --abbrev-ref HEAD 2>/dev/null || echo unknown)
|
||||||
|
fi
|
||||||
|
commit=${GIT_COMMIT:-}
|
||||||
|
if [[ -z "${commit}" ]]; then
|
||||||
|
commit=$(git -C "${ROOT_DIR}" rev-parse --short HEAD 2>/dev/null || echo unknown)
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat >"${METRICS_FILE}" <<METRICS
|
||||||
# HELP platform_quality_gate_runs_total Number of quality gate runs by result.
|
# HELP platform_quality_gate_runs_total Number of quality gate runs by result.
|
||||||
# TYPE platform_quality_gate_runs_total counter
|
# TYPE platform_quality_gate_runs_total counter
|
||||||
platform_quality_gate_runs_total{suite="lesavka",status="ok"} 0
|
platform_quality_gate_runs_total{suite="lesavka",branch="${branch}",commit="${commit}",status="ok"} 0
|
||||||
platform_quality_gate_runs_total{suite="lesavka",status="failed"} 1
|
platform_quality_gate_runs_total{suite="lesavka",branch="${branch}",commit="${commit}",status="failed"} 1
|
||||||
METRICS
|
METRICS
|
||||||
|
|
||||||
fetch_remote_counter() {
|
fetch_remote_counter() {
|
||||||
@ -58,8 +67,8 @@ refresh_counter_metrics() {
|
|||||||
{
|
{
|
||||||
echo '# HELP platform_quality_gate_runs_total Number of quality gate runs by result.'
|
echo '# HELP platform_quality_gate_runs_total Number of quality gate runs by result.'
|
||||||
echo '# TYPE platform_quality_gate_runs_total counter'
|
echo '# TYPE platform_quality_gate_runs_total counter'
|
||||||
echo "platform_quality_gate_runs_total{suite=\"lesavka\",status=\"ok\"} ${ok_count}"
|
echo "platform_quality_gate_runs_total{suite=\"lesavka\",branch=\"${branch}\",commit=\"${commit}\",status=\"ok\"} ${ok_count}"
|
||||||
echo "platform_quality_gate_runs_total{suite=\"lesavka\",status=\"failed\"} ${failed_count}"
|
echo "platform_quality_gate_runs_total{suite=\"lesavka\",branch=\"${branch}\",commit=\"${commit}\",status=\"failed\"} ${failed_count}"
|
||||||
awk '
|
awk '
|
||||||
/^# HELP platform_quality_gate_runs_total / {next}
|
/^# HELP platform_quality_gate_runs_total / {next}
|
||||||
/^# TYPE platform_quality_gate_runs_total / {next}
|
/^# TYPE platform_quality_gate_runs_total / {next}
|
||||||
@ -86,7 +95,7 @@ status=0
|
|||||||
# probe singleton runtime state. Keep coverage collection serial so per-file
|
# probe singleton runtime state. Keep coverage collection serial so per-file
|
||||||
# percentages stay stable enough to serve as a baseline gate.
|
# percentages stay stable enough to serve as a baseline gate.
|
||||||
if RUST_TEST_THREADS="${RUST_TEST_THREADS:-1}" cargo llvm-cov --workspace --all-targets --summary-only --json --output-path "${COVERAGE_JSON}"; then
|
if RUST_TEST_THREADS="${RUST_TEST_THREADS:-1}" cargo llvm-cov --workspace --all-targets --summary-only --json --output-path "${COVERAGE_JSON}"; then
|
||||||
if python3 - "${COVERAGE_JSON}" "${BASELINE_JSON}" "${METRICS_FILE}" "${SUMMARY_TXT}" "${ROOT_DIR}" "${COVERAGE_CONTRACT_JSON}" <<'PY'
|
if python3 - "${COVERAGE_JSON}" "${BASELINE_JSON}" "${METRICS_FILE}" "${SUMMARY_TXT}" "${ROOT_DIR}" "${COVERAGE_CONTRACT_JSON}" "${branch}" "${commit}" <<'PY'
|
||||||
import json
|
import json
|
||||||
import pathlib
|
import pathlib
|
||||||
import sys
|
import sys
|
||||||
@ -98,6 +107,8 @@ metrics_path = pathlib.Path(sys.argv[3])
|
|||||||
summary_path = pathlib.Path(sys.argv[4])
|
summary_path = pathlib.Path(sys.argv[4])
|
||||||
root = pathlib.Path(sys.argv[5])
|
root = pathlib.Path(sys.argv[5])
|
||||||
contract_path = pathlib.Path(sys.argv[6])
|
contract_path = pathlib.Path(sys.argv[6])
|
||||||
|
branch = sys.argv[7]
|
||||||
|
commit = sys.argv[8]
|
||||||
|
|
||||||
with coverage_path.open('r', encoding='utf-8') as fh:
|
with coverage_path.open('r', encoding='utf-8') as fh:
|
||||||
report = json.load(fh)
|
report = json.load(fh)
|
||||||
@ -171,52 +182,59 @@ files_at_95 = sum(1 for item in files if item['line_percent'] >= 95.0)
|
|||||||
files_below_95 = len(files) - files_at_95
|
files_below_95 = len(files) - files_at_95
|
||||||
over_500 = sum(1 for item in files if item['loc'] > 500)
|
over_500 = sum(1 for item in files if item['loc'] > 500)
|
||||||
|
|
||||||
|
def esc(value: str) -> str:
|
||||||
|
return value.replace('\\', r'\\').replace('\n', r'\\n').replace('"', r'\"')
|
||||||
|
|
||||||
|
labels = f'suite="lesavka",branch="{esc(branch)}",commit="{esc(commit)}"'
|
||||||
metrics = []
|
metrics = []
|
||||||
metrics.append('# HELP platform_quality_gate_runs_total Number of quality gate runs by result.')
|
metrics.append('# HELP platform_quality_gate_runs_total Number of quality gate runs by result.')
|
||||||
metrics.append('# TYPE platform_quality_gate_runs_total counter')
|
metrics.append('# TYPE platform_quality_gate_runs_total counter')
|
||||||
status_label = 'ok' if not regressions and not missing_from_baseline and not contract_failures else 'failed'
|
status_label = 'ok' if not regressions and not missing_from_baseline and not contract_failures else 'failed'
|
||||||
metrics.append(f'platform_quality_gate_runs_total{{suite="lesavka",status="{status_label}"}} 1')
|
ok_value = 1 if status_label == 'ok' else 0
|
||||||
|
failed_value = 1 if status_label == 'failed' else 0
|
||||||
|
metrics.append(f'platform_quality_gate_runs_total{{{labels},status="{status_label}"}} 1')
|
||||||
|
metrics.append('# HELP platform_quality_gate_checks_total Check outcomes from the latest lesavka gate run.')
|
||||||
|
metrics.append('# TYPE platform_quality_gate_checks_total gauge')
|
||||||
|
metrics.append(f'platform_quality_gate_checks_total{{{labels},check="coverage",status="ok"}} {ok_value}')
|
||||||
|
metrics.append(f'platform_quality_gate_checks_total{{{labels},check="coverage",status="failed"}} {failed_value}')
|
||||||
metrics.append('# HELP platform_quality_gate_workspace_line_coverage_percent Workspace line coverage percent.')
|
metrics.append('# HELP platform_quality_gate_workspace_line_coverage_percent Workspace line coverage percent.')
|
||||||
metrics.append('# TYPE platform_quality_gate_workspace_line_coverage_percent gauge')
|
metrics.append('# TYPE platform_quality_gate_workspace_line_coverage_percent gauge')
|
||||||
metrics.append(f'platform_quality_gate_workspace_line_coverage_percent{{suite="lesavka"}} {workspace_lines:.2f}')
|
metrics.append(f'platform_quality_gate_workspace_line_coverage_percent{{{labels}}} {workspace_lines:.2f}')
|
||||||
metrics.append('# HELP platform_quality_gate_files_total Count of tracked source files in the quality gate.')
|
metrics.append('# HELP platform_quality_gate_files_total Count of tracked source files in the quality gate.')
|
||||||
metrics.append('# TYPE platform_quality_gate_files_total gauge')
|
metrics.append('# TYPE platform_quality_gate_files_total gauge')
|
||||||
metrics.append(f'platform_quality_gate_files_total{{suite="lesavka"}} {len(files)}')
|
metrics.append(f'platform_quality_gate_files_total{{{labels}}} {len(files)}')
|
||||||
metrics.append('# HELP platform_quality_gate_files_at_or_above_95_total Count of files at or above the 95 percent line target.')
|
metrics.append('# HELP platform_quality_gate_files_at_or_above_95_total Count of files at or above the 95 percent line target.')
|
||||||
metrics.append('# TYPE platform_quality_gate_files_at_or_above_95_total gauge')
|
metrics.append('# TYPE platform_quality_gate_files_at_or_above_95_total gauge')
|
||||||
metrics.append(f'platform_quality_gate_files_at_or_above_95_total{{suite="lesavka"}} {files_at_95}')
|
metrics.append(f'platform_quality_gate_files_at_or_above_95_total{{{labels}}} {files_at_95}')
|
||||||
metrics.append('# HELP platform_quality_gate_files_below_95_total Count of files below the 95 percent line target.')
|
metrics.append('# HELP platform_quality_gate_files_below_95_total Count of files below the 95 percent line target.')
|
||||||
metrics.append('# TYPE platform_quality_gate_files_below_95_total gauge')
|
metrics.append('# TYPE platform_quality_gate_files_below_95_total gauge')
|
||||||
metrics.append(f'platform_quality_gate_files_below_95_total{{suite="lesavka"}} {files_below_95}')
|
metrics.append(f'platform_quality_gate_files_below_95_total{{{labels}}} {files_below_95}')
|
||||||
metrics.append('# HELP platform_quality_gate_source_lines_over_500_total Count of tracked source files over 500 LOC.')
|
metrics.append('# HELP platform_quality_gate_source_lines_over_500_total Count of tracked source files over 500 LOC.')
|
||||||
metrics.append('# TYPE platform_quality_gate_source_lines_over_500_total gauge')
|
metrics.append('# TYPE platform_quality_gate_source_lines_over_500_total gauge')
|
||||||
metrics.append(f'platform_quality_gate_source_lines_over_500_total{{suite="lesavka"}} {over_500}')
|
metrics.append(f'platform_quality_gate_source_lines_over_500_total{{{labels}}} {over_500}')
|
||||||
metrics.append('# HELP platform_quality_gate_contract_files_total Count of files covered by the strict testing coverage contract.')
|
metrics.append('# HELP platform_quality_gate_contract_files_total Count of files covered by the strict testing coverage contract.')
|
||||||
metrics.append('# TYPE platform_quality_gate_contract_files_total gauge')
|
metrics.append('# TYPE platform_quality_gate_contract_files_total gauge')
|
||||||
metrics.append(f'platform_quality_gate_contract_files_total{{suite="lesavka"}} {len(contract_files)}')
|
metrics.append(f'platform_quality_gate_contract_files_total{{{labels}}} {len(contract_files)}')
|
||||||
metrics.append('# HELP platform_quality_gate_contract_files_at_target_total Count of strict contract files meeting the line coverage target.')
|
metrics.append('# HELP platform_quality_gate_contract_files_at_target_total Count of strict contract files meeting the line coverage target.')
|
||||||
metrics.append('# TYPE platform_quality_gate_contract_files_at_target_total gauge')
|
metrics.append('# TYPE platform_quality_gate_contract_files_at_target_total gauge')
|
||||||
metrics.append(f'platform_quality_gate_contract_files_at_target_total{{suite="lesavka"}} {contract_files_at_target}')
|
metrics.append(f'platform_quality_gate_contract_files_at_target_total{{{labels}}} {contract_files_at_target}')
|
||||||
metrics.append('# HELP platform_quality_gate_contract_files_below_target_total Count of strict contract files missing the line coverage target or LOC cap.')
|
metrics.append('# HELP platform_quality_gate_contract_files_below_target_total Count of strict contract files missing the line coverage target or LOC cap.')
|
||||||
metrics.append('# TYPE platform_quality_gate_contract_files_below_target_total gauge')
|
metrics.append('# TYPE platform_quality_gate_contract_files_below_target_total gauge')
|
||||||
metrics.append(
|
metrics.append(
|
||||||
f'platform_quality_gate_contract_files_below_target_total{{suite="lesavka"}} {len(contract_failures)}'
|
f'platform_quality_gate_contract_files_below_target_total{{{labels}}} {len(contract_failures)}'
|
||||||
)
|
)
|
||||||
metrics.append('# HELP platform_quality_gate_file_line_coverage_percent Per-file line coverage percent.')
|
metrics.append('# HELP platform_quality_gate_file_line_coverage_percent Per-file line coverage percent.')
|
||||||
metrics.append('# TYPE platform_quality_gate_file_line_coverage_percent gauge')
|
metrics.append('# TYPE platform_quality_gate_file_line_coverage_percent gauge')
|
||||||
metrics.append('# HELP platform_quality_gate_file_source_lines Per-file source line count.')
|
metrics.append('# HELP platform_quality_gate_file_source_lines Per-file source line count.')
|
||||||
metrics.append('# TYPE platform_quality_gate_file_source_lines gauge')
|
metrics.append('# TYPE platform_quality_gate_file_source_lines gauge')
|
||||||
|
|
||||||
def esc(value: str) -> str:
|
|
||||||
return value.replace('\\', r'\\').replace('\n', r'\\n').replace('"', r'\"')
|
|
||||||
|
|
||||||
for item in files:
|
for item in files:
|
||||||
label = esc(item['path'])
|
label = esc(item['path'])
|
||||||
metrics.append(
|
metrics.append(
|
||||||
f'platform_quality_gate_file_line_coverage_percent{{suite="lesavka",file="{label}"}} {item["line_percent"]:.2f}'
|
f'platform_quality_gate_file_line_coverage_percent{{{labels},file="{label}"}} {item["line_percent"]:.2f}'
|
||||||
)
|
)
|
||||||
metrics.append(
|
metrics.append(
|
||||||
f'platform_quality_gate_file_source_lines{{suite="lesavka",file="{label}"}} {item["loc"]}'
|
f'platform_quality_gate_file_source_lines{{{labels},file="{label}"}} {item["loc"]}'
|
||||||
)
|
)
|
||||||
|
|
||||||
metrics_path.write_text('\n'.join(metrics) + '\n', encoding='utf-8')
|
metrics_path.write_text('\n'.join(metrics) + '\n', encoding='utf-8')
|
||||||
|
|||||||
@ -62,7 +62,7 @@
|
|||||||
},
|
},
|
||||||
"client/src/launcher/ui.rs": {
|
"client/src/launcher/ui.rs": {
|
||||||
"line_percent": 100.0,
|
"line_percent": 100.0,
|
||||||
"loc": 2635
|
"loc": 2650
|
||||||
},
|
},
|
||||||
"client/src/layout.rs": {
|
"client/src/layout.rs": {
|
||||||
"line_percent": 97.73,
|
"line_percent": 97.73,
|
||||||
|
|||||||
98
scripts/ci/sonarqube_gate.sh
Executable file
98
scripts/ci/sonarqube_gate.sh
Executable file
@ -0,0 +1,98 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Run or account for SonarQube analysis in the Atlas quality contract.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)
|
||||||
|
REPORT_DIR="${ROOT_DIR}/target/sonarqube-gate"
|
||||||
|
SUMMARY_JSON="${REPORT_DIR}/summary.json"
|
||||||
|
SUMMARY_TXT="${REPORT_DIR}/summary.txt"
|
||||||
|
METRICS_FILE="${REPORT_DIR}/metrics.prom"
|
||||||
|
PUSHGATEWAY_URL=${QUALITY_GATE_PUSHGATEWAY_URL:-}
|
||||||
|
ENFORCE=${LESAVKA_SONAR_ENFORCE:-0}
|
||||||
|
|
||||||
|
mkdir -p "${REPORT_DIR}"
|
||||||
|
cd "${ROOT_DIR}"
|
||||||
|
|
||||||
|
branch=${BRANCH_NAME:-${GIT_BRANCH:-}}
|
||||||
|
if [[ -z "${branch}" ]]; then
|
||||||
|
branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo unknown)
|
||||||
|
fi
|
||||||
|
commit=${GIT_COMMIT:-}
|
||||||
|
if [[ -z "${commit}" ]]; then
|
||||||
|
commit=$(git rev-parse --short HEAD 2>/dev/null || echo unknown)
|
||||||
|
fi
|
||||||
|
|
||||||
|
status=0
|
||||||
|
sonar_status=not_applicable
|
||||||
|
reason='SONARQUBE_HOST_URL, SONARQUBE_TOKEN, or sonar-scanner is unavailable'
|
||||||
|
if [[ -n "${SONARQUBE_HOST_URL:-}" && -n "${SONARQUBE_TOKEN:-}" ]] && command -v sonar-scanner >/dev/null 2>&1; then
|
||||||
|
sonar_status=ok
|
||||||
|
reason='sonar-scanner completed'
|
||||||
|
if ! sonar-scanner \
|
||||||
|
-Dsonar.projectKey=lesavka \
|
||||||
|
-Dsonar.projectName=lesavka \
|
||||||
|
-Dsonar.sources=client/src,server/src,common/src,testing/src \
|
||||||
|
-Dsonar.tests=testing/tests \
|
||||||
|
-Dsonar.host.url="${SONARQUBE_HOST_URL}" \
|
||||||
|
-Dsonar.token="${SONARQUBE_TOKEN}" \
|
||||||
|
>"${REPORT_DIR}/sonar-scanner.log" 2>&1; then
|
||||||
|
sonar_status=failed
|
||||||
|
reason='sonar-scanner failed; see sonar-scanner.log'
|
||||||
|
status=1
|
||||||
|
fi
|
||||||
|
elif [[ "${ENFORCE}" == "1" ]]; then
|
||||||
|
sonar_status=failed
|
||||||
|
status=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
python3 - "${SUMMARY_JSON}" "${SUMMARY_TXT}" "${METRICS_FILE}" "${sonar_status}" "${reason}" "${branch}" "${commit}" <<'PY'
|
||||||
|
import json
|
||||||
|
import pathlib
|
||||||
|
import sys
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
summary_path = pathlib.Path(sys.argv[1])
|
||||||
|
text_path = pathlib.Path(sys.argv[2])
|
||||||
|
metrics_path = pathlib.Path(sys.argv[3])
|
||||||
|
sonar_status = sys.argv[4]
|
||||||
|
reason = sys.argv[5]
|
||||||
|
branch = sys.argv[6]
|
||||||
|
commit = sys.argv[7]
|
||||||
|
|
||||||
|
def esc(value: str) -> str:
|
||||||
|
return value.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"')
|
||||||
|
|
||||||
|
summary = {
|
||||||
|
'suite': 'lesavka',
|
||||||
|
'check': 'sonarqube',
|
||||||
|
'status': sonar_status,
|
||||||
|
'reason': reason,
|
||||||
|
'branch': branch,
|
||||||
|
'commit': commit,
|
||||||
|
'generated_at': datetime.now(timezone.utc).isoformat(),
|
||||||
|
}
|
||||||
|
summary_path.write_text(json.dumps(summary, indent=2, sort_keys=True) + '\n', encoding='utf-8')
|
||||||
|
text_path.write_text(
|
||||||
|
f'sonarqube gate report\nstatus: {sonar_status}\nreason: {reason}\nbranch: {branch}\ncommit: {commit}\n',
|
||||||
|
encoding='utf-8',
|
||||||
|
)
|
||||||
|
labels = f'suite="lesavka",branch="{esc(branch)}",commit="{esc(commit)}"'
|
||||||
|
statuses = {'ok': 0, 'failed': 0, 'not_applicable': 0}
|
||||||
|
statuses[sonar_status] = 1
|
||||||
|
metrics = [
|
||||||
|
'# HELP platform_quality_gate_checks_total Check outcomes from the latest lesavka gate run.',
|
||||||
|
'# TYPE platform_quality_gate_checks_total gauge',
|
||||||
|
]
|
||||||
|
for state, value in statuses.items():
|
||||||
|
metrics.append(f'platform_quality_gate_checks_total{{{labels},check="sonarqube",status="{state}"}} {value}')
|
||||||
|
metrics_path.write_text('\n'.join(metrics) + '\n', encoding='utf-8')
|
||||||
|
print(text_path.read_text(encoding='utf-8'))
|
||||||
|
PY
|
||||||
|
|
||||||
|
if [[ -n "${PUSHGATEWAY_URL}" ]]; then
|
||||||
|
curl --fail --silent --show-error \
|
||||||
|
--data-binary @"${METRICS_FILE}" \
|
||||||
|
"${PUSHGATEWAY_URL%/}/metrics/job/lesavka-sonarqube-gate/suite/lesavka" || status=$?
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit "${status}"
|
||||||
144
scripts/ci/supply_chain_gate.sh
Executable file
144
scripts/ci/supply_chain_gate.sh
Executable file
@ -0,0 +1,144 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Generate dependency/artifact security evidence and run available scanners.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)
|
||||||
|
REPORT_DIR="${ROOT_DIR}/target/supply-chain-gate"
|
||||||
|
SUMMARY_JSON="${REPORT_DIR}/summary.json"
|
||||||
|
SUMMARY_TXT="${REPORT_DIR}/summary.txt"
|
||||||
|
METRICS_FILE="${REPORT_DIR}/metrics.prom"
|
||||||
|
SBOM_JSON="${REPORT_DIR}/sbom.cargo-metadata.json"
|
||||||
|
TREE_TXT="${REPORT_DIR}/dependency-tree.txt"
|
||||||
|
SECRET_TXT="${REPORT_DIR}/secret-scan.txt"
|
||||||
|
PUSHGATEWAY_URL=${QUALITY_GATE_PUSHGATEWAY_URL:-}
|
||||||
|
ENFORCE_TOOLS=${LESAVKA_SUPPLY_CHAIN_ENFORCE_TOOLS:-0}
|
||||||
|
|
||||||
|
mkdir -p "${REPORT_DIR}"
|
||||||
|
cd "${ROOT_DIR}"
|
||||||
|
|
||||||
|
branch=${BRANCH_NAME:-${GIT_BRANCH:-}}
|
||||||
|
if [[ -z "${branch}" ]]; then
|
||||||
|
branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo unknown)
|
||||||
|
fi
|
||||||
|
commit=${GIT_COMMIT:-}
|
||||||
|
if [[ -z "${commit}" ]]; then
|
||||||
|
commit=$(git rev-parse --short HEAD 2>/dev/null || echo unknown)
|
||||||
|
fi
|
||||||
|
|
||||||
|
status=0
|
||||||
|
cargo metadata --locked --format-version 1 >"${SBOM_JSON}"
|
||||||
|
cargo tree --locked --workspace >"${TREE_TXT}"
|
||||||
|
|
||||||
|
secret_status=ok
|
||||||
|
: >"${SECRET_TXT}"
|
||||||
|
while IFS= read -r path; do
|
||||||
|
case "$path" in
|
||||||
|
Cargo.lock|target/*|dist/*) continue ;;
|
||||||
|
esac
|
||||||
|
[[ -f "$path" ]] || continue
|
||||||
|
if file "$path" | grep -qi 'text\|json\|xml\|yaml\|toml\|script'; then
|
||||||
|
if grep -EHni \
|
||||||
|
-e 'AKIA[0-9A-Z]{16}' \
|
||||||
|
-e '-----BEGIN (RSA|EC|OPENSSH|PRIVATE) KEY-----' \
|
||||||
|
-e "(password|secret|token|api[_-]?key)[[:space:]]*[:=][[:space:]]*[\"'][A-Za-z0-9_+/=.-]{12,}[\"']" \
|
||||||
|
"$path" >>"${SECRET_TXT}" 2>/dev/null; then
|
||||||
|
secret_status=failed
|
||||||
|
status=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done < <(git ls-files)
|
||||||
|
|
||||||
|
audit_status=not_applicable
|
||||||
|
if command -v cargo-audit >/dev/null 2>&1; then
|
||||||
|
audit_status=ok
|
||||||
|
if ! cargo audit --locked >"${REPORT_DIR}/cargo-audit.txt" 2>&1; then
|
||||||
|
audit_status=failed
|
||||||
|
status=1
|
||||||
|
fi
|
||||||
|
elif [[ "${ENFORCE_TOOLS}" == "1" ]]; then
|
||||||
|
audit_status=failed
|
||||||
|
status=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
deny_status=not_applicable
|
||||||
|
if command -v cargo-deny >/dev/null 2>&1; then
|
||||||
|
deny_status=ok
|
||||||
|
if ! cargo deny check >"${REPORT_DIR}/cargo-deny.txt" 2>&1; then
|
||||||
|
deny_status=failed
|
||||||
|
status=1
|
||||||
|
fi
|
||||||
|
elif [[ "${ENFORCE_TOOLS}" == "1" ]]; then
|
||||||
|
deny_status=failed
|
||||||
|
status=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
artifact_status=not_applicable
|
||||||
|
if compgen -G "dist/*.tar.gz" >/dev/null; then
|
||||||
|
artifact_status=ok
|
||||||
|
sha256sum dist/*.tar.gz >"${REPORT_DIR}/SHA256SUMS"
|
||||||
|
fi
|
||||||
|
|
||||||
|
python3 - "${SUMMARY_JSON}" "${SUMMARY_TXT}" "${METRICS_FILE}" "${branch}" "${commit}" "${secret_status}" "${audit_status}" "${deny_status}" "${artifact_status}" <<'PY'
|
||||||
|
import json
|
||||||
|
import pathlib
|
||||||
|
import sys
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
summary_path = pathlib.Path(sys.argv[1])
|
||||||
|
text_path = pathlib.Path(sys.argv[2])
|
||||||
|
metrics_path = pathlib.Path(sys.argv[3])
|
||||||
|
branch = sys.argv[4]
|
||||||
|
commit = sys.argv[5]
|
||||||
|
secret_status = sys.argv[6]
|
||||||
|
audit_status = sys.argv[7]
|
||||||
|
deny_status = sys.argv[8]
|
||||||
|
artifact_status = sys.argv[9]
|
||||||
|
|
||||||
|
def esc(value: str) -> str:
|
||||||
|
return value.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"')
|
||||||
|
|
||||||
|
checks = {
|
||||||
|
'secret_scan': secret_status,
|
||||||
|
'cargo_audit': audit_status,
|
||||||
|
'cargo_deny': deny_status,
|
||||||
|
'artifact_checksums': artifact_status,
|
||||||
|
'sbom': 'ok',
|
||||||
|
}
|
||||||
|
status = 'failed' if any(value == 'failed' for value in checks.values()) else 'ok'
|
||||||
|
summary = {
|
||||||
|
'suite': 'lesavka',
|
||||||
|
'check': 'supply_chain',
|
||||||
|
'status': status,
|
||||||
|
'branch': branch,
|
||||||
|
'commit': commit,
|
||||||
|
'generated_at': datetime.now(timezone.utc).isoformat(),
|
||||||
|
'checks': checks,
|
||||||
|
}
|
||||||
|
summary_path.write_text(json.dumps(summary, indent=2, sort_keys=True) + '\n', encoding='utf-8')
|
||||||
|
lines = ['supply chain gate report', f'status: {status}', f'branch: {branch}', f'commit: {commit}', '']
|
||||||
|
for name, value in checks.items():
|
||||||
|
lines.append(f'{name}: {value}')
|
||||||
|
text_path.write_text('\n'.join(lines) + '\n', encoding='utf-8')
|
||||||
|
labels = f'suite="lesavka",branch="{esc(branch)}",commit="{esc(commit)}"'
|
||||||
|
metrics = [
|
||||||
|
'# HELP platform_quality_gate_checks_total Check outcomes from the latest lesavka gate run.',
|
||||||
|
'# TYPE platform_quality_gate_checks_total gauge',
|
||||||
|
]
|
||||||
|
for state in ('ok', 'failed', 'not_applicable'):
|
||||||
|
value = 1 if state == status else 0
|
||||||
|
metrics.append(f'platform_quality_gate_checks_total{{{labels},check="supply_chain",status="{state}"}} {value}')
|
||||||
|
metrics.append('# HELP lesavka_supply_chain_subcheck_info Supply-chain subcheck evidence status.')
|
||||||
|
metrics.append('# TYPE lesavka_supply_chain_subcheck_info gauge')
|
||||||
|
for name, value in checks.items():
|
||||||
|
metrics.append(f'lesavka_supply_chain_subcheck_info{{{labels},subcheck="{esc(name)}",status="{esc(value)}"}} 1')
|
||||||
|
metrics_path.write_text('\n'.join(metrics) + '\n', encoding='utf-8')
|
||||||
|
print(text_path.read_text(encoding='utf-8'))
|
||||||
|
PY
|
||||||
|
|
||||||
|
if [[ -n "${PUSHGATEWAY_URL}" ]]; then
|
||||||
|
curl --fail --silent --show-error \
|
||||||
|
--data-binary @"${METRICS_FILE}" \
|
||||||
|
"${PUSHGATEWAY_URL%/}/metrics/job/lesavka-supply-chain-gate/suite/lesavka" || status=$?
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit "${status}"
|
||||||
0
scripts/daemon/lesavka-core.sh
Normal file → Executable file
0
scripts/daemon/lesavka-core.sh
Normal file → Executable file
0
scripts/daemon/lesavka-uvc.sh
Normal file → Executable file
0
scripts/daemon/lesavka-uvc.sh
Normal file → Executable file
0
scripts/kernel/build-linux-rpi.sh
Normal file → Executable file
0
scripts/kernel/build-linux-rpi.sh
Normal file → Executable file
@ -1,14 +1,18 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# scripts/manual/audio-clip-fetch.sh
|
# scripts/manual/audio-clip-fetch.sh
|
||||||
|
#
|
||||||
|
# Manual: fetch and play recent server-side audio clips during field debugging.
|
||||||
|
# Not part of CI; requires SSH access to the target server.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
# Pull & play the most recent 1 s AAC clip from lesavka‑server
|
|
||||||
PI_HOST="nikto@192.168.42.253" # adjust
|
PI_HOST="nikto@192.168.42.253" # adjust
|
||||||
REMOTE_DIR="/tmp"
|
REMOTE_DIR="/tmp"
|
||||||
DEST="$(mktemp -u).wav"
|
TMPDIR="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$TMPDIR"' EXIT
|
||||||
|
|
||||||
scp "${PI_HOST}:${REMOTE_DIR}/ear-*.aac" "$DEST" 2>/dev/null \
|
scp -q "${PI_HOST}:${REMOTE_DIR}/ear-*.aac" "$TMPDIR/" 2>/dev/null \
|
||||||
|| { echo "❌ no clip files yet"; exit 1; }
|
|| { echo "❌ no clip files yet"; exit 1; }
|
||||||
|
|
||||||
LATEST=$(ls -1t ear-*.aac | head -n1)
|
LATEST=$(ls -1t "$TMPDIR"/ear-*.aac | head -n1)
|
||||||
echo "🎧 playing ${LATEST} ..."
|
echo "🎧 playing ${LATEST} ..."
|
||||||
gst-play-1.0 --quiet "${LATEST}"
|
gst-play-1.0 --quiet "${LATEST}"
|
||||||
|
|||||||
@ -1,9 +1,15 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# scripts/manual/audio-mic-fetch.sh
|
# scripts/manual/audio-mic-fetch.sh
|
||||||
|
#
|
||||||
|
# Manual: fetch and play recent microphone uplink clips during field debugging.
|
||||||
|
# Not part of CI; requires SSH access to the target server.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
PI_HOST="nikto@192.168.42.253" # adjust if needed
|
PI_HOST="nikto@192.168.42.253" # adjust if needed
|
||||||
REMOTE_DIR="/tmp"
|
REMOTE_DIR="/tmp"
|
||||||
TMPDIR=$(mktemp -d)
|
TMPDIR=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$TMPDIR"' EXIT
|
||||||
|
|
||||||
scp -q "${PI_HOST}:${REMOTE_DIR}/voice-*.aac" "$TMPDIR/" 2>/dev/null \
|
scp -q "${PI_HOST}:${REMOTE_DIR}/voice-*.aac" "$TMPDIR/" 2>/dev/null \
|
||||||
|| { echo "❌ no mic clip files found yet"; exit 1; }
|
|| { echo "❌ no mic clip files found yet"; exit 1; }
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
# Manual: compare H.264 decoder output from a live eye device.
|
||||||
|
# Not part of CI; requires a live /dev/lesavka_* eye device.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
DEVICE="${1:-/dev/lesavka_r_eye}"
|
DEVICE="${1:-/dev/lesavka_r_eye}"
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# scripts/manual/eval_lesavka.sh - iterative health check for lesavka client/server/gadget
|
# scripts/manual/eval_lesavka.sh - iterative health check for lesavka client/server/gadget
|
||||||
|
# Manual: operator probe for live Lesavka hosts; not part of CI.
|
||||||
# - Locally: probes TCP + gRPC handshake on LESAVKA_SERVER_ADDR
|
# - Locally: probes TCP + gRPC handshake on LESAVKA_SERVER_ADDR
|
||||||
# - Optional: if TETHYS_HOST is set, ssh to run lsusb + dmesg tail (enumeration check)
|
# - Optional: if TETHYS_HOST is set, ssh to run lsusb + dmesg tail (enumeration check)
|
||||||
# - Optional: if THEIA_HOST is set, ssh to show core/server status + hidg/uvc presence
|
# - Optional: if THEIA_HOST is set, ssh to show core/server status + hidg/uvc presence
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# scripts/manual/kde-start-tethys.sh
|
# scripts/manual/kde-start-tethys.sh
|
||||||
#
|
#
|
||||||
|
# Manual: remote desktop recovery helper for tethys; not part of CI.
|
||||||
|
#
|
||||||
# Start/restart SDDM on tethys and set display geometry over :0.
|
# Start/restart SDDM on tethys and set display geometry over :0.
|
||||||
# Intended for remote use after SSH-ing into tethys.
|
# Intended for remote use after SSH-ing into tethys.
|
||||||
#
|
#
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
# Manual: capture V4L2/sysfs capability evidence from a live eye device.
|
||||||
|
# Not part of CI; requires local device access.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
DEVICE="${1:-/dev/lesavka_r_eye}"
|
DEVICE="${1:-/dev/lesavka_r_eye}"
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# scripts/manual/soak-report.sh - summarize server stability/quality counters for a time window
|
# scripts/manual/soak-report.sh - summarize server stability/quality counters for a time window
|
||||||
|
# Manual: operator soak evidence collector; not part of CI.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
SERVER_HOST=${LESAVKA_SERVER_HOST:-theia}
|
SERVER_HOST=${LESAVKA_SERVER_HOST:-theia}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# scripts/manual/usb-reset.sh - trigger USB reset RPC on the server
|
# scripts/manual/usb-reset.sh - trigger USB reset RPC on the server
|
||||||
|
# Manual: operator recovery action; not part of CI.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
|
SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
|
||||||
|
|||||||
@ -1,11 +1,14 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# scripts/manual/video-stream.sh
|
# scripts/manual/video-frame-fetch.sh
|
||||||
|
#
|
||||||
|
# Manual: fetch recent H.264 eye samples and render first frames for inspection.
|
||||||
|
# Not part of CI; requires SSH access to a live server.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
PI_HOST="nikto@192.168.42.253" # user@IP-of lesavka
|
PI_HOST="nikto@192.168.42.253" # user@IP-of lesavka
|
||||||
REMOTE_DIR="/tmp" # where eye*-idr.h264 are written
|
REMOTE_DIR="/tmp" # where eye*-idr.h264 are written
|
||||||
FIRST_FEW=10
|
FIRST_FEW=10
|
||||||
|
|
||||||
set -eu
|
|
||||||
WORKDIR="$(mktemp -d)"
|
WORKDIR="$(mktemp -d)"
|
||||||
echo "⏬ pulling h264 samples from $PI_HOST ..."
|
echo "⏬ pulling h264 samples from $PI_HOST ..."
|
||||||
scp "${PI_HOST}:${REMOTE_DIR}/eye*.h264" "$WORKDIR/"
|
scp "${PI_HOST}:${REMOTE_DIR}/eye*.h264" "$WORKDIR/"
|
||||||
|
|||||||
@ -1,11 +1,15 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# scripts/manual/video-stream.sh
|
# scripts/manual/video-stream.sh
|
||||||
|
#
|
||||||
|
# Manual: stream live server video into a local GStreamer preview.
|
||||||
|
# Not part of CI; requires grpcurl, jq, and GStreamer.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
grpcurl -plaintext \
|
grpcurl -plaintext \
|
||||||
-d '{"id":0,"max_bitrate":6000}' \
|
-d '{"id":0,"max_bitrate":6000}' \
|
||||||
-import-path ./../../common/proto -proto lesavka.proto \
|
-import-path ./../../common/proto -proto lesavka.proto \
|
||||||
192.168.42.253:50051 \
|
192.168.42.253:50051 \
|
||||||
lesavka.relay.Relay/CaptureVideo \
|
lesavka.relay.Relay/CaptureVideo |
|
||||||
| jq -r '.data'
|
jq -r '.data' |
|
||||||
| base64 -d \
|
base64 -d |
|
||||||
| gst-launch-1.0 fdsrc ! h264parse ! avdec_h264 ! autovideosink
|
gst-launch-1.0 fdsrc ! h264parse ! avdec_h264 ! autovideosink
|
||||||
|
|||||||
@ -1,7 +1,11 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# scripts/manual/vpn-open.sh
|
# scripts/manual/vpn-open.sh
|
||||||
|
#
|
||||||
|
# Manual: open the local CyberGhost VPN profile for field networking tests.
|
||||||
|
# Not part of CI; requires local sudo privileges.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
here=$(pwd)
|
here=$(pwd)
|
||||||
cd /home/brad/cyberghost
|
cd /home/brad/cyberghost
|
||||||
sudo openvpn --config openvpn.ovpn
|
sudo openvpn --config openvpn.ovpn
|
||||||
cd $here
|
cd "$here"
|
||||||
|
|||||||
@ -1,4 +1,10 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# scripts/manual/vpn-test.sh
|
# scripts/manual/vpn-test.sh
|
||||||
|
#
|
||||||
|
# Manual: show current public IP/geolocation after VPN changes.
|
||||||
|
# Not part of CI; uses public HTTP APIs.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
set -x IP $(curl -s https://api.ipify.org) && echo "IP: $IP" && curl http://ip-api.com/json/$IP?fields=country,city,lat,lon
|
IP="$(curl -fsS https://api.ipify.org)"
|
||||||
|
echo "IP: $IP"
|
||||||
|
curl -fsS "http://ip-api.com/json/${IP}?fields=country,city,lat,lon"
|
||||||
|
|||||||
@ -10,7 +10,7 @@ bench = false
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "lesavka_server"
|
name = "lesavka_server"
|
||||||
version = "0.12.1"
|
version = "0.12.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
autobins = false
|
autobins = false
|
||||||
|
|
||||||
|
|||||||
@ -102,6 +102,21 @@ fn operations_column_fills_height_and_splits_extra_space_between_logs() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn session_console_buttons_share_the_remaining_toolbar_width() {
|
||||||
|
assert!(
|
||||||
|
UI_SRC.contains("let console_buttons = gtk::Box::new(gtk::Orientation::Horizontal, 8);")
|
||||||
|
);
|
||||||
|
assert!(UI_SRC.contains("console_buttons.set_hexpand(true);"));
|
||||||
|
assert!(UI_SRC.contains("console_buttons.set_homogeneous(true);"));
|
||||||
|
assert!(UI_SRC.contains("console_copy_button.set_hexpand(true);"));
|
||||||
|
assert!(UI_SRC.contains("console_popout_button.set_hexpand(true);"));
|
||||||
|
assert!(
|
||||||
|
source_index("console_toolbar.append(&console_level_combo);")
|
||||||
|
< source_index("console_toolbar.append(&console_buttons);")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn relay_controls_keep_connect_inline_with_server_entry() {
|
fn relay_controls_keep_connect_inline_with_server_entry() {
|
||||||
assert!(UI_SRC.contains("build_panel(\"Relay Controls\")"));
|
assert!(UI_SRC.contains("build_panel(\"Relay Controls\")"));
|
||||||
|
|||||||
@ -111,6 +111,10 @@ fn active_relay_keeps_local_upstream_camera_and_microphone_evidence_visible() {
|
|||||||
fn launcher_webcam_quality_selection_reaches_preview_and_relay_env() {
|
fn launcher_webcam_quality_selection_reaches_preview_and_relay_env() {
|
||||||
assert!(UI_SRC.contains("selected_camera_quality(&camera_quality_combo"));
|
assert!(UI_SRC.contains("selected_camera_quality(&camera_quality_combo"));
|
||||||
assert!(UI_SRC.contains("sync_camera_quality_selection"));
|
assert!(UI_SRC.contains("sync_camera_quality_selection"));
|
||||||
|
assert!(UI_SRC.contains("let camera_quality_syncing = Rc::new(Cell::new(false));"));
|
||||||
|
assert!(UI_SRC.contains("camera_quality_syncing.set(true);"));
|
||||||
|
assert!(UI_SRC.contains("if camera_quality_syncing.get()"));
|
||||||
|
assert!(UI_SRC.contains("state.try_borrow_mut()"));
|
||||||
assert!(UI_SRC.contains("tests.set_camera_quality"));
|
assert!(UI_SRC.contains("tests.set_camera_quality"));
|
||||||
assert!(DEVICE_TEST_SRC.contains("pub fn set_camera_quality"));
|
assert!(DEVICE_TEST_SRC.contains("pub fn set_camera_quality"));
|
||||||
assert!(DEVICE_TEST_SRC.contains("build_camera_preview_pipeline(&device, mode)"));
|
assert!(DEVICE_TEST_SRC.contains("build_camera_preview_pipeline(&device, mode)"));
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user