From 12280b9b41cc4f1c7099aa97a85bd7e97829908c Mon Sep 17 00:00:00 2001 From: Philipp Walter Date: Fri, 17 Jul 2026 13:10:45 +0200 Subject: [PATCH] probe: add history artifacts --- ci_run_android.sh | 14 ++++++ docs/mainnet-probe.md | 4 ++ test/helpers/probe.ts | 101 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 119 insertions(+) diff --git a/ci_run_android.sh b/ci_run_android.sh index a44d7fc..ebd961f 100755 --- a/ci_run_android.sh +++ b/ci_run_android.sh @@ -24,7 +24,19 @@ LOGFILE="$ARTIFACTS_DIR/logcat.txt" adb logcat -v threadtime -T 1 -b all > "$LOGFILE" & LOGCAT_PID=$! +clear_app_logs() { + set +e + adb shell "run-as $APP_ID sh -c 'rm -rf files/logs && mkdir -p files/logs'" + local status=$? + set -e + + if [[ "$status" -ne 0 ]]; then + echo "Could not clear app logs for $APP_ID" >&2 + fi +} + collect_app_logs() { + rm -rf "$APP_LOGS_DIR" mkdir -p "$APP_LOGS_DIR" set +e adb exec-out run-as "$APP_ID" tar -cf - -C "/data/data/$APP_ID/files" logs \ @@ -46,6 +58,8 @@ cleanup() { } trap cleanup EXIT INT TERM +clear_app_logs + # local/regtest helper ports if [[ "${BACKEND:-local}" != "mainnet" ]]; then # regtest electrum port diff --git a/docs/mainnet-probe.md b/docs/mainnet-probe.md index cc0bdfe..5589202 100644 --- a/docs/mainnet-probe.md +++ b/docs/mainnet-probe.md @@ -69,6 +69,10 @@ Written to `artifacts/` (or `artifacts/attempt-N/` when `ATTEMPT` is set): - `probe-report.md` — markdown summary table (also appended to `GITHUB_STEP_SUMMARY` on CI) - `probe-readiness.json` — node readiness snapshot at probe start - `probe-targets-replay.json` — configured targets expanded into the exact target+amount order used by this run. Use it with `PROBE_ORDER=config` to replay a random run. +- `app-logs/` — Android application logs collected by the runner when available. These are short-lived CI artifacts and are not copied into `probe-history/`. +- `probe-history//` — canonical compact per-run history snapshot used for trend reports and CI persistence. It contains `probe-run.json`, `probe-results.json`, `probe-report.md`, `probe-targets-replay.json`, and `probe-readiness.json` when readiness was captured. + +`probe-run.json` includes local/CI metadata when available, including attempt, wallet, GitHub run details, amount profile, probe order, retry count, reset-scores setting, and source refs. CI expects this `probe-history//` format; top-level result files are not treated as persistent history. ## Running locally diff --git a/test/helpers/probe.ts b/test/helpers/probe.ts index 1068feb..2447951 100644 --- a/test/helpers/probe.ts +++ b/test/helpers/probe.ts @@ -66,6 +66,7 @@ const DEFAULT_MIN_GRAPH_CHANNELS = 10_000; const DEFAULT_RESET_SCORES_TIMEOUT_SECONDS = 180; const DEFAULT_SCORES_SYNC_MAX_AGE_S = 900; const DEFAULT_PROBE_AMOUNT_PROFILE = 'full'; +const PROBE_RUN_ID = new Date().toISOString().replace(/[:.]/g, '-'); const PROBE_AMOUNT_PROFILES = { small: [1_000_000], large: [80_000_000], @@ -590,6 +591,7 @@ export function writeProbeArtifacts( `${formatProbeTargetsReplayJson(replayQueue, 2)}\n` ); } + writeProbeHistoryArtifacts(PROBE_RUN_ID, results, report, readiness, replayQueue); if (readiness) { const readinessPath = path.join(artifactsDir, 'probe-readiness.json'); @@ -604,6 +606,48 @@ export function writeProbeArtifacts( } } +function writeProbeHistoryArtifacts( + runId: string, + results: ProbeResult[], + report: string, + readiness?: ProbeReadiness | null, + replayProbes?: ProbeQueueEntry[] +): void { + const historyDir = path.join(resolveArtifactsDir(), 'probe-history', runId); + fs.mkdirSync(historyDir, { recursive: true }); + + const metadata = { + runId, + updatedAt: new Date().toISOString(), + attempt: resolveAttempt(), + wallet: probeWalletForReport(), + ci: ciMetadataForReport(), + config: probeConfigForReport(), + resultCount: results.length, + }; + + fs.writeFileSync( + path.join(historyDir, 'probe-run.json'), + `${JSON.stringify(metadata, null, 2)}\n` + ); + fs.writeFileSync( + path.join(historyDir, 'probe-results.json'), + `${JSON.stringify(results, null, 2)}\n` + ); + fs.writeFileSync(path.join(historyDir, 'probe-report.md'), report); + if (replayProbes && replayProbes.length > 0) { + fs.writeFileSync( + path.join(historyDir, 'probe-targets-replay.json'), + `${formatProbeTargetsReplayJson(replayProbes, 2)}\n` + ); + } + + if (readiness) { + const readinessPath = path.join(historyDir, 'probe-readiness.json'); + fs.writeFileSync(readinessPath, `${JSON.stringify(readiness, null, 2)}\n`); + } +} + export function renderProbeReport( results: ProbeResult[], readiness?: ProbeReadiness | null @@ -659,6 +703,63 @@ function scoresResetForReport(): string { } } +function probeWalletForReport(): Record | undefined { + const id = nonEmptyEnv('PROBE_WALLET_ID'); + const label = nonEmptyEnv('PROBE_WALLET_LABEL'); + if (!id && !label) return undefined; + return removeUndefinedValues({ id, label }); +} + +function ciMetadataForReport(): Record | undefined { + const runId = nonEmptyEnv('GITHUB_RUN_ID'); + if (!runId) return undefined; + + return removeUndefinedValues({ + repository: nonEmptyEnv('GITHUB_REPOSITORY'), + runId, + runNumber: nonEmptyEnv('GITHUB_RUN_NUMBER'), + runAttempt: nonEmptyEnv('GITHUB_RUN_ATTEMPT'), + runUrl: githubRunUrlForReport(runId), + eventName: nonEmptyEnv('GITHUB_EVENT_NAME'), + workflow: nonEmptyEnv('GITHUB_WORKFLOW'), + ref: nonEmptyEnv('GITHUB_REF_NAME'), + sha: nonEmptyEnv('GITHUB_SHA'), + }); +} + +function probeConfigForReport(): Record { + return removeUndefinedValues({ + amountProfile: process.env.PROBE_AMOUNT_PROFILE ?? DEFAULT_PROBE_AMOUNT_PROFILE, + order: probeOrderForReport(), + retries: process.env.PROBE_RETRIES, + delayMs: process.env.PROBE_DELAY_MS, + resetScores: scoresResetForReport(), + e2eTestsRef: process.env.E2E_TESTS_REF, + probeAndroidRef: process.env.PROBE_ANDROID_REF, + probeApkUrl: process.env.PROBE_APK_URL, + }); +} + +function githubRunUrlForReport(runId: string): string | undefined { + const serverUrl = nonEmptyEnv('GITHUB_SERVER_URL'); + const repository = nonEmptyEnv('GITHUB_REPOSITORY'); + if (!serverUrl || !repository) return undefined; + return `${serverUrl}/${repository}/actions/runs/${runId}`; +} + +function nonEmptyEnv(name: string): string | undefined { + const value = process.env[name]; + return value && value.length > 0 ? value : undefined; +} + +function removeUndefinedValues>( + value: T +): Record { + return Object.fromEntries( + Object.entries(value).filter((entry): entry is [string, string] => entry[1] !== undefined) + ); +} + function parseProbeTarget(value: unknown): ProbeTarget { if (typeof value !== 'object' || value === null) { throw new Error('Each probe target must be an object');