fix(ci): retain Telegram candidate diagnostics (#105472)

* fix(ci): retain Telegram candidate diagnostics

* fix(ci): stream redacted Telegram diagnostics
This commit is contained in:
Peter Steinberger
2026-07-12 16:10:41 +01:00
committed by GitHub
parent 761839343a
commit eaedcdcd4e
2 changed files with 192 additions and 0 deletions

View File

@@ -2164,6 +2164,159 @@ jobs:
echo "Isolated Telegram SUT UID still owns processes after terminal cleanup." >&2
exit 1
- name: Capture isolated Telegram runtime diagnostics
id: capture_diagnostics
if: always() && steps.terminate_sut.outputs.quiescent == 'true' && steps.run_lane.outputs.output_dir != ''
env:
OUTPUT_DIR: ${{ steps.run_lane.outputs.output_dir }}
RUNTIME_ROOT: ${{ steps.create_sut.outputs.runtime_root }}
shell: bash
run: |
set -euo pipefail
umask 077
[[ "$RUNTIME_ROOT" == "/var/lib/openclaw-telegram-sut-runtime-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" ]]
diagnostics_dir="${OUTPUT_DIR}/trusted-runtime-diagnostics"
install -d -m 0700 "$diagnostics_dir"
trusted_temp_root="$(mktemp -d "${RUNNER_TEMP}/openclaw-telegram-diagnostics.XXXXXX")"
trap 'rm -rf -- "$trusted_temp_root"' EXIT
redactor_script="${trusted_temp_root}/redact-gateway-tail.mts"
cat >"$redactor_script" <<'NODE'
import fs from "node:fs";
import path from "node:path";
import { pathToFileURL } from "node:url";
const [outputPath] = process.argv.slice(2);
const redactorPath = path.resolve(
"extensions/qa-lab/src/gateway-log-redaction.ts",
);
const { redactQaGatewayDebugText } = await import(pathToFileURL(redactorPath).href);
const limitBytes = 131_072;
const maxInputRecordBytes = 1_048_576;
const oversizedMarker = "[truncated oversized gateway log record] ";
const omittedMarker = "[omitted oversized gateway log record]\n";
const retained = [];
let retainedBytes = 0;
function boundRedactedLine(line) {
const redactedLine = `${redactQaGatewayDebugText(line)}\n`;
const lineBuffer = Buffer.from(redactedLine);
if (lineBuffer.length <= limitBytes) {
return redactedLine;
}
const markerBytes = Buffer.byteLength(oversizedMarker);
const suffixBuffer = lineBuffer.subarray(
lineBuffer.length - (limitBytes - markerBytes),
);
let suffix = suffixBuffer.toString("utf8");
while (Buffer.byteLength(`${oversizedMarker}${suffix}`) > limitBytes) {
suffix = suffix.slice(1);
}
return `${oversizedMarker}${suffix}`;
}
function retainLine(boundedLine) {
const lineBytes = Buffer.byteLength(boundedLine);
while (retained.length > 0 && retainedBytes + lineBytes > limitBytes) {
const removed = retained.shift();
retainedBytes -= Buffer.byteLength(removed);
}
retained.push(boundedLine);
retainedBytes += lineBytes;
}
let recordParts = [];
let recordBytes = 0;
let recordOmitted = false;
function appendRecordPart(part) {
if (recordOmitted) {
return;
}
if (recordBytes + part.length > maxInputRecordBytes) {
recordParts = [];
recordBytes = 0;
recordOmitted = true;
return;
}
recordParts.push(Buffer.from(part));
recordBytes += part.length;
}
function finishRecord() {
if (recordOmitted) {
retainLine(omittedMarker);
} else {
const line = Buffer.concat(recordParts, recordBytes).toString("utf8").replace(/\r$/u, "");
retainLine(boundRedactedLine(line));
}
recordParts = [];
recordBytes = 0;
recordOmitted = false;
}
for await (const value of process.stdin) {
const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value);
let offset = 0;
while (offset < chunk.length) {
const newline = chunk.indexOf(0x0a, offset);
const end = newline === -1 ? chunk.length : newline;
appendRecordPart(chunk.subarray(offset, end));
if (newline === -1) {
break;
}
finishRecord();
offset = newline + 1;
}
}
if (recordOmitted || recordBytes > 0) {
finishRecord();
}
fs.writeFileSync(outputPath, retained.join(""), { encoding: "utf8", mode: 0o600 });
NODE
chmod 0600 "$redactor_script"
mapfile -t gateway_logs < <(
sudo find "$RUNTIME_ROOT/tmp" -xdev -type f -name 'openclaw-*.log' -print | sort
)
((${#gateway_logs[@]} > 0 && ${#gateway_logs[@]} <= 8))
for index in "${!gateway_logs[@]}"; do
log_path="$(sudo realpath -e "${gateway_logs[$index]}")"
case "$log_path" in
"$RUNTIME_ROOT"/tmp/*) ;;
*) echo "Telegram gateway log escaped the isolated runtime root." >&2; exit 1 ;;
esac
[[ "$(sudo stat -c '%F' "$log_path")" == "regular file" ]]
output_path="${diagnostics_dir}/gateway-internal-tail-$((index + 1)).log"
sudo cat "$log_path" | node --import tsx "$redactor_script" "$output_path"
done
mapfile -t gateway_configs < <(
sudo find "$RUNTIME_ROOT/tmp" -xdev -type f -name openclaw.json -print | sort
)
((${#gateway_configs[@]} > 0 && ${#gateway_configs[@]} <= 8))
for index in "${!gateway_configs[@]}"; do
config_path="$(sudo realpath -e "${gateway_configs[$index]}")"
case "$config_path" in
"$RUNTIME_ROOT"/tmp/*) ;;
*) echo "Telegram gateway config escaped the isolated runtime root." >&2; exit 1 ;;
esac
proof_path="${diagnostics_dir}/gateway-model-config-$((index + 1)).json"
sudo jq -e '
{
agentDefaultModel: .agents.defaults.model,
agentModelRefs: ((.agents.defaults.models // {}) | keys),
providers: (
(.models.providers // {}) |
to_entries |
map({
id: .key,
api: .value.api,
modelIds: [.value.models[]?.id]
})
)
}
' "$config_path" >"$proof_path"
chmod 0600 "$proof_path"
done
- name: Finalize trusted Telegram process-boundary evidence
id: finalize_boundary
if: always() && steps.terminate_sut.outputs.quiescent == 'true' && steps.run_lane.outputs.output_dir != ''
@@ -2360,6 +2513,7 @@ jobs:
${{ steps.validate_credentials.outcome }}
${{ steps.run_lane.outcome }}
${{ steps.terminate_sut.outcome }}
${{ steps.capture_diagnostics.outcome }}
${{ steps.finalize_boundary.outcome }}
${{ steps.validate_evidence.outcome }}
${{ steps.remove_sut.outcome }}

View File

@@ -557,6 +557,44 @@ describe("release Telegram QA workflow", () => {
expect(finalizeStep?.env?.RUN_LANE_OUTCOME).toBe("${{ steps.run_lane.outcome }}");
expect(finalizeStep?.run).toContain('--arg runLaneOutcome "$RUN_LANE_OUTCOME"');
expect(finalizeStep?.run).toContain('if $runLaneOutcome == "success" then 2 else 1 end');
const captureStep = job?.steps?.find(
(step) => step.name === "Capture isolated Telegram runtime diagnostics",
);
expect(captureStep?.if).toContain("steps.terminate_sut.outputs.quiescent == 'true'");
expect(captureStep?.env?.OUTPUT_DIR).toBe("${{ steps.run_lane.outputs.output_dir }}");
expect(captureStep?.env?.RUNTIME_ROOT).toBe("${{ steps.create_sut.outputs.runtime_root }}");
expect(captureStep?.run).toContain("-name 'openclaw-*.log'");
expect(captureStep?.run).toContain(
'trusted_temp_root="$(mktemp -d "${RUNNER_TEMP}/openclaw-telegram-diagnostics.XXXXXX")"',
);
expect(captureStep?.run).not.toContain(".raw");
expect(captureStep?.run).toContain(
'sudo cat "$log_path" | node --import tsx "$redactor_script" "$output_path"',
);
expect(captureStep?.run).toContain("const redactedLine =");
expect(captureStep?.run).toContain("const limitBytes = 131_072");
expect(captureStep?.run).toContain("const maxInputRecordBytes = 1_048_576");
expect(captureStep?.run).toContain("[truncated oversized gateway log record]");
expect(captureStep?.run).toContain("[omitted oversized gateway log record]");
expect(captureStep?.run).toContain("chunk.indexOf(0x0a, offset)");
expect(captureStep?.run).not.toContain("readline.createInterface");
expect(captureStep?.run).toContain("while (retained.length > 0");
expect(captureStep?.run).toContain("redactQaGatewayDebugText");
expect(captureStep?.run).toContain("agentDefaultModel: .agents.defaults.model");
expect(captureStep?.run).toContain("modelIds: [.value.models[]?.id]");
expect(
job?.steps?.findIndex(
(step) => step.name === "Capture isolated Telegram runtime diagnostics",
),
).toBeLessThan(
job?.steps?.findIndex(
(step) => step.name === "Finalize trusted Telegram process-boundary evidence",
) ?? -1,
);
const recordStep = job?.steps?.find((step) => step.name === "Record Telegram execution status");
expect(recordStep?.env?.OUTCOMES).toContain("${{ steps.capture_diagnostics.outcome }}");
});
it("serializes stderr behind the workflow-command pause", () => {