Skip to content

Commit feae1fa

Browse files
authored
fix(release): expose Telegram launcher failures safely (#104099)
* fix(release): expose Telegram launcher failures safely * test(release): prove namespace launcher diagnostics
1 parent 4a00b96 commit feae1fa

4 files changed

Lines changed: 74 additions & 3 deletions

File tree

.github/workflows/openclaw-release-telegram-qa.yml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1137,7 +1137,9 @@ jobs:
11371137
11381138
sudo tee "$launcher" >/dev/null <<'LAUNCHER'
11391139
#!/bin/bash
1140-
set -euo pipefail
1140+
set -Eeuo pipefail
1141+
launcher_stage=entry
1142+
trap 'exit_status=$?; trap - ERR; printf "Telegram SUT launcher failed: stage=%s line=%s status=%s\n" "$launcher_stage" "$LINENO" "$exit_status" >&2; exit "$exit_status"' ERR
11411143
11421144
transport_keys=(
11431145
HOME
@@ -1514,6 +1516,7 @@ jobs:
15141516
exec sudo "${sudo_args[@]}" -- "$0" --root-run "$@"
15151517
fi
15161518
shift
1519+
launcher_stage=root-run-setup
15171520
[[ "$EUID" == "0" ]]
15181521
source /etc/openclaw-telegram-sut.conf
15191522
@@ -1635,11 +1638,14 @@ jobs:
16351638
export boundary_mode generation command_file identity_file sandbox_file
16361639
export command_sha256 expected_env_keys_b64 sandbox_payload_b64
16371640
1638-
exec /usr/bin/unshare \
1641+
launcher_stage=enter-mount-namespace
1642+
/usr/bin/unshare \
16391643
--mount \
16401644
--fork \
16411645
--propagation private \
16421646
/bin/bash -ceu '
1647+
launcher_stage=mask-host-paths
1648+
trap "exit_status=\$?; trap - ERR; printf \"Telegram SUT launcher failed: stage=%s line=%s status=%s\\n\" \"\$launcher_stage\" \"\$LINENO\" \"\$exit_status\" >&2; exit \"\$exit_status\"" ERR
16431649
for masked_path in "$RUNNER_HOME" /tmp /var/tmp /dev/shm; do
16441650
[[ -d "$masked_path" ]]
16451651
mount \
@@ -1648,8 +1654,10 @@ jobs:
16481654
openclaw-telegram-mask \
16491655
"$masked_path"
16501656
done
1657+
launcher_stage=mount-proc
16511658
mount -t proc proc /proc -o nosuid,nodev,noexec,hidepid=2
16521659
if [[ "$boundary_mode" == "true" ]]; then
1660+
launcher_stage=write-identity
16531661
pid="$$"
16541662
proc_stat="$(cat "/proc/${pid}/stat")"
16551663
proc_tail="${proc_stat##*) }"

extensions/qa-lab/src/gateway-child.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,24 @@ describe("monitorQaGatewayChildFailure", () => {
190190
});
191191
});
192192

193+
describe("formatQaGatewayProcessBoundaryStartupFailure", () => {
194+
it("includes only a bounded, redacted launcher log tail", () => {
195+
const prefix = "x".repeat(9_000);
196+
const longSecret = "s".repeat(9_000);
197+
const message = testing.formatQaGatewayProcessBoundaryStartupFailure(
198+
new Error("launcher exited before identity"),
199+
`${prefix}\nAuthorization: Bearer ${longSecret}\nlauncher stage=mount-proc`,
200+
);
201+
202+
expect(message).toContain("launcher exited before identity");
203+
expect(message).toContain("Gateway logs:");
204+
expect(message).toContain("Authorization: Bearer <redacted>");
205+
expect(message).toContain("launcher stage=mount-proc");
206+
expect(message).not.toContain("s".repeat(100));
207+
expect(message).not.toContain(prefix);
208+
});
209+
});
210+
193211
describe("Gateway child fixture helpers", () => {
194212
it("creates an empty transport config seam", () => {
195213
expect(testing.createQaGatewayEmptyTransport()).toEqual({

extensions/qa-lab/src/gateway-child.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,13 @@ function monitorQaGatewayChildFailure(child: ChildProcess, output: { push(chunk:
459459
return () => childFailure;
460460
}
461461

462+
const QA_GATEWAY_PROCESS_BOUNDARY_LOG_TAIL_CHARS = 8_192;
463+
464+
function formatQaGatewayProcessBoundaryStartupFailure(error: unknown, logs: string) {
465+
const logTail = redactQaGatewayDebugText(logs).slice(-QA_GATEWAY_PROCESS_BOUNDARY_LOG_TAIL_CHARS);
466+
return `${formatErrorMessage(error)}${formatQaGatewayLogsForError(logTail)}`;
467+
}
468+
462469
async function fetchLocalGatewayHealth(params: {
463470
baseUrl: string;
464471
healthPath: "/readyz" | "/healthz";
@@ -549,6 +556,7 @@ export const testing = {
549556
createQaGatewayChildLogCollector,
550557
monitorQaGatewayChildFailure,
551558
throwQaGatewayChildFailure,
559+
formatQaGatewayProcessBoundaryStartupFailure,
552560
createQaBundledPluginsDir,
553561
signalQaGatewayChildProcessTree,
554562
stopQaGatewayChildProcessTree,
@@ -1098,14 +1106,22 @@ export async function startQaGatewayChild(params: {
10981106
cleanupErrors.push(cleanupError);
10991107
}
11001108
}
1109+
const boundaryFailure = preparedBoundary
1110+
? formatQaGatewayProcessBoundaryStartupFailure(error, logs())
1111+
: null;
11011112
if (cleanupErrors.length > 0) {
11021113
const cleanupFailure = new AggregateError(
11031114
[error, ...cleanupErrors],
1104-
"qa gateway failed before verified process cleanup completed",
1115+
boundaryFailure
1116+
? `qa gateway failed before verified process cleanup completed: ${boundaryFailure}`
1117+
: "qa gateway failed before verified process cleanup completed",
11051118
{ cause: error },
11061119
);
11071120
throw cleanupFailure;
11081121
}
1122+
if (boundaryFailure) {
1123+
throw new Error(boundaryFailure, { cause: error });
1124+
}
11091125
throw error;
11101126
}
11111127
};

test/scripts/openclaw-release-telegram-qa-workflow.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,15 @@ describe("release Telegram QA workflow", () => {
503503

504504
it("derives SUT-writable paths from the verified runtime root after sudo", () => {
505505
const source = readFileSync(WORKFLOW_PATH, "utf8");
506+
expect(source).toContain("Telegram SUT launcher failed: stage=%s line=%s status=%s");
507+
expect(source).toContain("launcher_stage=root-run-setup");
508+
expect(source).toContain("launcher_stage=enter-mount-namespace");
509+
expect(source).toContain("launcher_stage=mask-host-paths");
510+
expect(source).toContain("launcher_stage=mount-proc");
511+
expect(source).toContain("launcher_stage=write-identity");
512+
expect(source).toMatch(/launcher_stage=enter-mount-namespace\n\s+\/usr\/bin\/unshare/u);
513+
expect(source).not.toContain("exec /usr/bin/unshare");
514+
expect(source).not.toContain("set -x");
506515
expect(source).toContain('temp_root="$(realpath -e "${OPENCLAW_QA_TEMP_ROOT:?}")"');
507516
expect(source).toContain('proc_stat="$(cat "/proc/${pid}/stat")"');
508517
expect(source).not.toContain('proc_stat="$(cat /proc/self/stat)"');
@@ -514,4 +523,24 @@ describe("release Telegram QA workflow", () => {
514523
expect(source).toContain('export XDG_CONFIG_HOME="${temp_root}/xdg-config"');
515524
expect(source).toContain('if [[ "${1:-}" == "--root-terminate-uid" ]]');
516525
});
526+
527+
it("reports the namespace-entry stage when its supervised child fails", () => {
528+
const source = readFileSync(WORKFLOW_PATH, "utf8");
529+
const trapLine = source.match(/^\s+(trap 'exit_status=.*' ERR)$/mu)?.[1];
530+
expect(trapLine).toBeTruthy();
531+
532+
const result = spawnSync(
533+
"bash",
534+
[
535+
"-c",
536+
`set -Eeuo pipefail\nlauncher_stage=enter-mount-namespace\n${trapLine}\nbash -c 'exit 23'`,
537+
],
538+
{ encoding: "utf8" },
539+
);
540+
541+
expect(result.status).toBe(23);
542+
expect(result.stderr).toMatch(
543+
/Telegram SUT launcher failed: stage=enter-mount-namespace line=[0-9]+ status=23/u,
544+
);
545+
});
517546
});

0 commit comments

Comments
 (0)