ci: add lesavka hygiene gate and launcher crash guard

This commit is contained in:
Brad Stein 2026-04-23 01:13:29 -03:00
parent 64ded7839d
commit 62f99b07f6
39 changed files with 5494 additions and 87 deletions

10
.gitignore vendored
View File

@ -1,8 +1,16 @@
target/
Cargo.lock
dist/
coverage/
logs/
captures/
override.toml
.cache/sccache/
/unit-graph.json
*.log
*.h264
*.aac
*.wav
*.rgba
/**/*.rs.bk
**/*.rs.orig
**/*.rs.rej

4278
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,8 @@
[workspace]
members = [
"common",
"server",
"client",
"server",
"testing",
]
resolver = "3"

66
Jenkinsfile vendored
View File

@ -59,6 +59,7 @@ spec:
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
python3 \
curl \
file \
clang \
llvm \
pkg-config \
@ -82,15 +83,7 @@ spec:
}
}
stage('Format') {
steps {
container('rust-ci') {
sh 'cargo fmt --all -- --check'
}
}
}
stage('Hygiene') {
stage('Style Docs LOC Naming') {
steps {
container('rust-ci') {
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 {
container('rust-ci') {
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 {
container('rust-ci') {
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
'''
sh 'QUALITY_GATE_PUSHGATEWAY_URL="${QUALITY_GATE_PUSHGATEWAY_URL}" scripts/ci/media_reliability_gate.sh'
}
}
}
stage('Publish test metrics') {
stage('Gate Glue') {
steps {
container('rust-ci') {
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
'''
sh 'QUALITY_GATE_PUSHGATEWAY_URL="${QUALITY_GATE_PUSHGATEWAY_URL}" scripts/ci/gate_glue_gate.sh'
}
}
}
stage('Enforce quality gate') {
stage('SonarQube') {
steps {
container('rust-ci') {
sh '''
set -eu
exit "$(cat target/quality-gate/quality-gate.rc 2>/dev/null || echo 1)"
'''
sh 'QUALITY_GATE_PUSHGATEWAY_URL="${QUALITY_GATE_PUSHGATEWAY_URL}" scripts/ci/sonarqube_gate.sh'
}
}
}
@ -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') {
when {
expression { return params.PUSH_IMAGES }
@ -192,7 +178,7 @@ spec:
always {
script {
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) {
echo "archive step unavailable: ${err.class.simpleName}"
}

View File

@ -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 `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
1. Install or update the client and server through the install scripts.
2. Launch `Lesavka` from the KDE application launcher or run `lesavka`.

View File

@ -4,7 +4,7 @@ path = "src/main.rs"
[package]
name = "lesavka_client"
version = "0.12.1"
version = "0.12.2"
edition = "2024"
[dependencies]

View File

@ -800,6 +800,7 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
let diagnostics_popout = Rc::clone(&view.diagnostics_popout);
let log_popout = Rc::clone(&view.log_popout);
let shutdown_cleaned = Rc::new(Cell::new(false));
let camera_quality_syncing = Rc::new(Cell::new(false));
{
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 child_proc = Rc::clone(&child_proc);
let tests = Rc::clone(&tests);
let camera_quality_syncing = Rc::clone(&camera_quality_syncing);
let camera_combo = camera_combo.clone();
let camera_quality_combo = camera_quality_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 mut state = state.borrow_mut();
state.select_camera(selected.clone());
camera_quality_syncing.set(true);
sync_camera_quality_selection(&camera_quality_combo, &mut state, &catalog);
camera_quality_syncing.set(false);
}
let quality = state.borrow().camera_quality;
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 child_proc = Rc::clone(&child_proc);
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_read = camera_quality_combo.clone();
camera_quality_combo.connect_changed(move |_| {
if camera_quality_syncing.get() {
return;
}
let selected = selected_camera_quality(&camera_quality_combo_read);
let preview_was_running =
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) {
widgets
.status_label
@ -1343,6 +1355,7 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
let speaker_combo = speaker_combo.clone();
let keyboard_combo = keyboard_combo.clone();
let mouse_combo = mouse_combo.clone();
let camera_quality_syncing = Rc::clone(&camera_quality_syncing);
widgets.device_refresh_button.connect_clicked(move |_| {
let fresh_catalog = DeviceCatalog::discover();
let (
@ -1379,11 +1392,13 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
{
let mut state = state.borrow_mut();
state.select_camera(selected_camera);
camera_quality_syncing.set(true);
sync_camera_quality_selection(
&camera_quality_combo,
&mut state,
&fresh_catalog,
);
camera_quality_syncing.set(false);
state.select_microphone(selected_microphone);
state.select_speaker(selected_speaker);
state.select_keyboard(selected_keyboard);

View File

@ -715,14 +715,18 @@ pub fn build_launcher_view(
console_level_combo.set_size_request(78, 36);
console_level_combo.set_tooltip_text(Some("Show relay logs at this level or higher."));
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."));
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."));
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_copy_button);
console_toolbar.append(&console_popout_button);
console_toolbar.append(&console_buttons);
let status_label = gtk::Label::new(Some("Session log ready."));
status_label.add_css_class("status-line");
status_label.set_halign(gtk::Align::Start);

View File

@ -1,6 +1,6 @@
[package]
name = "lesavka_common"
version = "0.12.1"
version = "0.12.2"
edition = "2024"
build = "build.rs"

235
docs/operational-env.md Normal file
View 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
View 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.

View File

@ -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")
TARGET=$(rustc -vV | awk '/^host:/{print $2}')
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"
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
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}"

110
scripts/ci/gate_glue_gate.sh Executable file
View 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}"

View File

@ -6,15 +6,32 @@ REPORT_DIR="${ROOT_DIR}/target/hygiene-gate"
CLIPPY_JSON="${REPORT_DIR}/clippy.json"
SUMMARY_TXT="${REPORT_DIR}/summary.txt"
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}"
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}"
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 os
import pathlib
import re
import stat
import subprocess
import sys
from collections import defaultdict
@ -22,8 +39,20 @@ clippy_path = pathlib.Path(sys.argv[1])
baseline_path = pathlib.Path(sys.argv[2])
summary_path = pathlib.Path(sys.argv[3])
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+')
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):
for raw in path.read_text(encoding='utf-8').splitlines():
@ -41,6 +70,142 @@ def repo_relative(path: str) -> str | None:
except Exception:
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]:
counts: dict[str, int] = defaultdict(int)
for item in load_json_lines(path):
@ -204,6 +369,11 @@ for path, current_entry in current.items():
layout_violations = integration_layout_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 = {
'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'legacy integration-test layout violations: {len(layout_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('path | loc | clippy warnings | doc debt | baseline status')
lines.append('-' * 78)
@ -258,15 +432,45 @@ if testing_violations:
lines.append('-' * 78)
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')
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:
print(line, file=sys.stderr)
for line in layout_violations:
print(line, file=sys.stderr)
for line in testing_violations:
print(line, file=sys.stderr)
for line in policy_violations:
print(line, file=sys.stderr)
raise SystemExit(1)
PY

View File

@ -98,12 +98,12 @@
"client/src/launcher/ui.rs": {
"clippy_warnings": 64,
"doc_debt": 23,
"loc": 2635
"loc": 2650
},
"client/src/launcher/ui_components.rs": {
"clippy_warnings": 12,
"doc_debt": 18,
"loc": 1595
"loc": 1599
},
"client/src/launcher/ui_runtime.rs": {
"clippy_warnings": 74,

View 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}"

View 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

View File

@ -12,11 +12,20 @@ PUSHGATEWAY_URL=${QUALITY_GATE_PUSHGATEWAY_URL:-}
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.
# TYPE platform_quality_gate_runs_total counter
platform_quality_gate_runs_total{suite="lesavka",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="ok"} 0
platform_quality_gate_runs_total{suite="lesavka",branch="${branch}",commit="${commit}",status="failed"} 1
METRICS
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 '# 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\",status=\"failed\"} ${failed_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\",branch=\"${branch}\",commit=\"${commit}\",status=\"failed\"} ${failed_count}"
awk '
/^# HELP 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
# 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 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 pathlib
import sys
@ -98,6 +107,8 @@ metrics_path = pathlib.Path(sys.argv[3])
summary_path = pathlib.Path(sys.argv[4])
root = pathlib.Path(sys.argv[5])
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:
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
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.append('# HELP platform_quality_gate_runs_total Number of quality gate runs by result.')
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'
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('# 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('# 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('# 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('# 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('# 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('# 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('# 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('# TYPE platform_quality_gate_contract_files_below_target_total gauge')
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('# 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('# 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:
label = esc(item['path'])
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(
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')

View File

@ -62,7 +62,7 @@
},
"client/src/launcher/ui.rs": {
"line_percent": 100.0,
"loc": 2635
"loc": 2650
},
"client/src/layout.rs": {
"line_percent": 97.73,

98
scripts/ci/sonarqube_gate.sh Executable file
View 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
View 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
View File

0
scripts/daemon/lesavka-uvc.sh Normal file → Executable file
View File

0
scripts/kernel/build-linux-rpi.sh Normal file → Executable file
View File

View File

@ -1,14 +1,18 @@
#!/usr/bin/env bash
# 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 lesavkaserver
PI_HOST="nikto@192.168.42.253" # adjust
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; }
LATEST=$(ls -1t ear-*.aac | head -n1)
LATEST=$(ls -1t "$TMPDIR"/ear-*.aac | head -n1)
echo "🎧 playing ${LATEST} ..."
gst-play-1.0 --quiet "${LATEST}"

View File

@ -1,9 +1,15 @@
#!/usr/bin/env bash
# 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
REMOTE_DIR="/tmp"
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT
scp -q "${PI_HOST}:${REMOTE_DIR}/voice-*.aac" "$TMPDIR/" 2>/dev/null \
|| { echo "❌ no mic clip files found yet"; exit 1; }

View File

@ -1,4 +1,6 @@
#!/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
DEVICE="${1:-/dev/lesavka_r_eye}"

View File

@ -1,5 +1,6 @@
#!/usr/bin/env bash
# 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
# - 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

View File

@ -1,6 +1,8 @@
#!/usr/bin/env bash
# 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.
# Intended for remote use after SSH-ing into tethys.
#

View File

@ -1,4 +1,6 @@
#!/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
DEVICE="${1:-/dev/lesavka_r_eye}"

View File

@ -1,5 +1,6 @@
#!/usr/bin/env bash
# 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
SERVER_HOST=${LESAVKA_SERVER_HOST:-theia}

View File

@ -1,5 +1,6 @@
#!/usr/bin/env bash
# scripts/manual/usb-reset.sh - trigger USB reset RPC on the server
# Manual: operator recovery action; not part of CI.
set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"

View File

@ -1,11 +1,14 @@
#!/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
REMOTE_DIR="/tmp" # where eye*-idr.h264 are written
FIRST_FEW=10
set -eu
WORKDIR="$(mktemp -d)"
echo "⏬ pulling h264 samples from $PI_HOST ..."
scp "${PI_HOST}:${REMOTE_DIR}/eye*.h264" "$WORKDIR/"

View File

@ -1,11 +1,15 @@
#!/usr/bin/env bash
# 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 \
-d '{"id":0,"max_bitrate":6000}' \
-import-path ./../../common/proto -proto lesavka.proto \
192.168.42.253:50051 \
lesavka.relay.Relay/CaptureVideo \
| jq -r '.data'
| base64 -d \
| gst-launch-1.0 fdsrc ! h264parse ! avdec_h264 ! autovideosink
lesavka.relay.Relay/CaptureVideo |
jq -r '.data' |
base64 -d |
gst-launch-1.0 fdsrc ! h264parse ! avdec_h264 ! autovideosink

View File

@ -1,7 +1,11 @@
#!/usr/bin/env bash
# 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)
cd /home/brad/cyberghost
sudo openvpn --config openvpn.ovpn
cd $here
cd "$here"

View File

@ -1,4 +1,10 @@
#!/usr/bin/env bash
# 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"

View File

@ -10,7 +10,7 @@ bench = false
[package]
name = "lesavka_server"
version = "0.12.1"
version = "0.12.2"
edition = "2024"
autobins = false

View File

@ -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]
fn relay_controls_keep_connect_inline_with_server_entry() {
assert!(UI_SRC.contains("build_panel(\"Relay Controls\")"));

View File

@ -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() {
assert!(UI_SRC.contains("selected_camera_quality(&camera_quality_combo"));
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!(DEVICE_TEST_SRC.contains("pub fn set_camera_quality"));
assert!(DEVICE_TEST_SRC.contains("build_camera_preview_pipeline(&device, mode)"));