#!/usr/bin/env bash # Run opt-in hardware/lab gates that are unsafe for shared CI desktops. set -euo pipefail ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd) REPORT_DIR="${ROOT_DIR}/target/baremetal-lab-gate" SUMMARY_JSON="${REPORT_DIR}/summary.json" SUMMARY_TXT="${REPORT_DIR}/summary.txt" METRICS_FILE="${REPORT_DIR}/metrics.prom" RUN_LOG="${REPORT_DIR}/baremetal-lab-gate.log" PUSHGATEWAY_URL=${QUALITY_GATE_PUSHGATEWAY_URL:-} PUSHGATEWAY_JOB=${LESAVKA_LAB_GATE_PUSHGATEWAY_JOB:-lesavka-baremetal-lab-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:-} log() { printf '%s\n' "$*" | tee -a "${RUN_LOG}" } : >"${RUN_LOG}" status=0 outcome=ok detail="ran configured bare-metal lab gates" steps_jsonl="${REPORT_DIR}/steps.jsonl" : >"${steps_jsonl}" start_seconds=$(date +%s) record_step() { local name="$1" local result="$2" local note="$3" python3 - "${steps_jsonl}" "${name}" "${result}" "${note}" <<'PY' import json import pathlib import sys path = pathlib.Path(sys.argv[1]) entry = {'name': sys.argv[2], 'result': sys.argv[3], 'note': sys.argv[4]} with path.open('a', encoding='utf-8') as fh: fh.write(json.dumps(entry, sort_keys=True) + '\n') PY } run_shell_step() { local name="$1" local command="$2" log "==> ${name}" set +e bash -lc "${command}" 2>&1 | tee -a "${RUN_LOG}" local step_status=${PIPESTATUS[0]} set -e if [[ "${step_status}" -eq 0 ]]; then record_step "${name}" ok "completed" else record_step "${name}" failed "exit ${step_status}" status="${step_status}" fi } run_step() { local name="$1" shift log "==> ${name}" set +e "$@" 2>&1 | tee -a "${RUN_LOG}" local step_status=${PIPESTATUS[0]} set -e if [[ "${step_status}" -eq 0 ]]; then record_step "${name}" ok "completed" else record_step "${name}" failed "exit ${step_status}" status="${step_status}" fi } if [[ "${LESAVKA_ALLOW_LAB_HARDWARE_TESTS:-0}" != "1" ]]; then outcome=skipped detail="bare-metal lab gates require LESAVKA_ALLOW_LAB_HARDWARE_TESTS=1" log "Skipping bare-metal lab gates." log "These gates may use Theia/Tethys, UVC/UAC devices, or virtual HID input." log "Run only on an isolated worker/session with LESAVKA_ALLOW_LAB_HARDWARE_TESTS=1." else if [[ "${LESAVKA_RUN_VIDEO_DOWNSTREAM_GATE:-1}" == "1" ]]; then run_step "video_downstream_gate" scripts/ci/video_downstream_gate.sh else record_step "video_downstream_gate" skipped "LESAVKA_RUN_VIDEO_DOWNSTREAM_GATE!=1" fi if [[ "${LESAVKA_ALLOW_DISRUPTIVE_INPUT_TESTS:-0}" == "1" ]]; then run_step "input_transport_gate" scripts/ci/input_transport_gate.sh else record_step "input_transport_gate" skipped "LESAVKA_ALLOW_DISRUPTIVE_INPUT_TESTS!=1" log "Skipping disruptive input transport gate; set LESAVKA_ALLOW_DISRUPTIVE_INPUT_TESTS=1 on an isolated worker." fi if [[ "${LESAVKA_RUN_SERVER_RCT_MATRIX:-0}" == "1" ]]; then server_rct_matrix_cmd=${LESAVKA_SERVER_RCT_MATRIX_CMD:-"${ROOT_DIR}/scripts/manual/run_server_to_rc_mode_matrix.sh"} run_shell_step "server_to_rct_matrix" "${server_rct_matrix_cmd}" else record_step "server_to_rct_matrix" skipped "LESAVKA_RUN_SERVER_RCT_MATRIX!=1" fi if [[ "${LESAVKA_RUN_CLIENT_RCT_PROBE:-0}" == "1" ]]; then client_rct_probe_cmd=${LESAVKA_CLIENT_RCT_PROBE_CMD:-"${ROOT_DIR}/scripts/manual/run_client_to_rct_transport_probe.sh"} run_shell_step "client_to_rct_transport_probe" "${client_rct_probe_cmd}" else record_step "client_to_rct_transport_probe" skipped "LESAVKA_RUN_CLIENT_RCT_PROBE!=1" fi if [[ "${status}" -ne 0 ]]; then outcome=failed detail="one or more bare-metal lab gates failed" fi fi duration_seconds=$(($(date +%s) - start_seconds)) python3 - \ "${SUMMARY_JSON}" \ "${SUMMARY_TXT}" \ "${METRICS_FILE}" \ "${steps_jsonl}" \ "${branch}" \ "${commit}" \ "${build_url}" \ "${outcome}" \ "${status}" \ "${duration_seconds}" \ "${detail}" <<'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]) steps_path = pathlib.Path(sys.argv[4]) branch = sys.argv[5] commit = sys.argv[6] build_url = sys.argv[7] outcome = sys.argv[8] status = int(sys.argv[9]) duration_seconds = int(sys.argv[10]) detail = sys.argv[11] def esc(value: str) -> str: return value.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"') steps = [] if steps_path.exists(): for line in steps_path.read_text(encoding='utf-8').splitlines(): if line.strip(): steps.append(json.loads(line)) summary = { 'suite': 'lesavka', 'profile': 'lab', 'branch': branch, 'commit': commit, 'build_url': build_url, 'outcome': outcome, 'exit_code': status, 'duration_seconds': duration_seconds, 'detail': detail, 'steps': steps, '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( '\n'.join([ f'lesavka bare-metal lab gate: {outcome}', f'branch: {branch}', f'commit: {commit}', f'duration: {duration_seconds}s', f'detail: {detail}', 'steps:', *[f"- {step['name']}: {step['result']} ({step['note']})" for step in steps], ]) + '\n', encoding='utf-8', ) labels = f'suite="lesavka",profile="lab",branch="{esc(branch)}",commit="{esc(commit)}"' ok = 1 if outcome == 'ok' else 0 failed = 1 if outcome == 'failed' else 0 skipped = 1 if outcome == 'skipped' else 0 lines = [ '# HELP lesavka_ci_profile_last_run_success Whether the latest Lesavka CI profile run succeeded.', '# TYPE lesavka_ci_profile_last_run_success gauge', f'lesavka_ci_profile_last_run_success{{{labels}}} {ok}', '# HELP lesavka_ci_profile_duration_seconds Duration of the latest Lesavka CI profile run.', '# TYPE lesavka_ci_profile_duration_seconds gauge', f'lesavka_ci_profile_duration_seconds{{{labels}}} {duration_seconds}', '# HELP lesavka_ci_profile_runs Current profile run outcome.', '# TYPE lesavka_ci_profile_runs gauge', f'lesavka_ci_profile_runs{{{labels},status="ok"}} {ok}', f'lesavka_ci_profile_runs{{{labels},status="failed"}} {failed}', f'lesavka_ci_profile_runs{{{labels},status="skipped"}} {skipped}', '# HELP lesavka_lab_gate_step_result Bare-metal lab gate step result.', '# TYPE lesavka_lab_gate_step_result gauge', ] for step in steps: name = esc(step['name']) for result in ['ok', 'failed', 'skipped']: value = 1 if step['result'] == result else 0 lines.append(f'lesavka_lab_gate_step_result{{{labels},step="{name}",result="{result}"}} {value}') metrics_path.write_text('\n'.join(lines) + '\n', 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}"