Skip to content

Commit 69ca5fa

Browse files
fix(build): validate Discord package after backend dependency install (#128)
* chore(version): sync desktop version to v4.23.0-beta.1 * fix: add backend startup heartbeat liveness probe (#114) * fix: add backend startup heartbeat liveness probe * fix: tighten startup heartbeat validation * refactor: centralize startup heartbeat metadata * fix: surface heartbeat invalidation sooner * fix: harden startup heartbeat parsing * fix: warn on stop-time heartbeat failures * refactor: simplify startup heartbeat control flow * refactor: flatten readiness heartbeat helpers * refactor: clarify heartbeat helper responsibilities * docs: clarify startup heartbeat path coupling * fix: harden startup heartbeat coordination * fix: make startup heartbeat checks monotonic * fix: clean up heartbeat test and exit handling * chore(version): sync desktop version to v4.23.1 * fix(ci): prepare macOS resources before optional signing (#119) * fix(ci): prepare macOS resources before optional signing * test(ci): parse macOS workflow steps structurally * test(ci): parse workflow YAML structurally * test(ci): relax workflow assertions and install test deps * test(ci): share workflow test helpers * test(ci): harden script workflow dependency setup * fix(ci): setup pnpm before enabling pnpm cache * test(ci): relax workflow step assertions * [codex] default desktop chat transport to websocket (#121) * chore(version): sync desktop version to v4.23.0-beta.1 * fix: add backend startup heartbeat liveness probe (#114) * fix: add backend startup heartbeat liveness probe * fix: tighten startup heartbeat validation * refactor: centralize startup heartbeat metadata * fix: surface heartbeat invalidation sooner * fix: harden startup heartbeat parsing * fix: warn on stop-time heartbeat failures * refactor: simplify startup heartbeat control flow * refactor: flatten readiness heartbeat helpers * refactor: clarify heartbeat helper responsibilities * docs: clarify startup heartbeat path coupling * fix: harden startup heartbeat coordination * fix: make startup heartbeat checks monotonic * fix: clean up heartbeat test and exit handling * fix: default desktop chat transport to websocket * fix: respect existing desktop transport preference * fix: harden desktop transport bootstrap * fix: centralize desktop transport contract * fix: harden desktop bridge transport injection --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * chore(version): sync desktop version to v4.23.2 * chore(version): sync desktop version to v4.23.3 * chore(version): sync desktop version to v4.23.5 * chore(version): sync desktop version to v4.23.6 * chore: ignore local task plans * fix(build): validate Discord package after backend dependency install The PyPI 'discord' stub package (v0.0.2) shadows the real 'py-cord' package because both occupy the 'discord' namespace. When the stub is installed by mistake (e.g. cache pollution or dependency resolution glitch), the Discord adapter fails at runtime with: module 'discord' has no attribute 'Bot' This change adds a post-install validation step that imports 'discord' and asserts 'discord.Bot' exists. If the stub is present, the build fails early with a clear error message instead of producing a broken runtime bundle. Closes #8016 --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 9f29fc5 commit 69ca5fa

2 files changed

Lines changed: 108 additions & 0 deletions

File tree

scripts/backend/build-backend.mjs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const runtimeCoreLockPath = path.join(appDir, 'runtime-core-lock.json');
3434
const launcherPath = path.join(outputDir, 'launch_backend.py');
3535
const launcherTemplatePath = path.join(__dirname, 'templates', 'launch_backend.py');
3636
const importScannerScriptPath = path.join(__dirname, 'tools', 'scan_imports.py');
37+
const validateDiscordScriptPath = path.join(__dirname, 'tools', 'validate_discord.py');
3738

3839
const runtimeSource =
3940
process.env.ASTRBOT_DESKTOP_BACKEND_RUNTIME ||
@@ -584,6 +585,87 @@ const installRuntimeDependencies = (runtimePython) => {
584585
}
585586
};
586587

588+
// Configurable via env for slower CI environments.
589+
const DISCORD_VALIDATION_TIMEOUT_MS =
590+
Number(process.env.ASTRBOT_DISCORD_VALIDATION_TIMEOUT) || 30_000;
591+
// Protocol prefix shared with scripts/backend/tools/validate_discord.py.
592+
const DISCORD_JSON_PREFIX = 'ASTRBOT_VALIDATE_DISCORD_JSON:';
593+
594+
const extractPrefixedJson = (rawStdout) => {
595+
if (!rawStdout) return null;
596+
const lines = String(rawStdout).split(/\r?\n/);
597+
const jsonLine = lines.find((line) => line.startsWith(DISCORD_JSON_PREFIX));
598+
if (!jsonLine) return null;
599+
try {
600+
return JSON.parse(jsonLine.slice(DISCORD_JSON_PREFIX.length));
601+
} catch {
602+
return null;
603+
}
604+
};
605+
606+
const validateDiscordPackage = (runtimePython) => {
607+
const result = spawnSync(
608+
runtimePython.absolute,
609+
[validateDiscordScriptPath, DISCORD_JSON_PREFIX],
610+
{
611+
encoding: 'utf8',
612+
cwd: outputDir,
613+
windowsHide: true,
614+
timeout: DISCORD_VALIDATION_TIMEOUT_MS,
615+
},
616+
);
617+
618+
if (result.error) {
619+
const isTimeout = result.error.code === 'ETIMEDOUT';
620+
const details = [
621+
`exit status: ${result.status ?? 'unknown'}`,
622+
`error code: ${result.error.code ?? 'unknown'}`,
623+
result.signal ? `signal: ${result.signal}` : null,
624+
result.stdout ? `stdout: ${String(result.stdout).trim()}` : null,
625+
result.stderr ? `stderr: ${String(result.stderr).trim()}` : null,
626+
]
627+
.filter(Boolean)
628+
.join('; ');
629+
630+
throw new Error(
631+
`Discord package validation could not run (interpreter: ${runtimePython.absolute}). ` +
632+
(isTimeout
633+
? `Timed out after ${DISCORD_VALIDATION_TIMEOUT_MS}ms. `
634+
: 'This may indicate a Python runtime issue. ') +
635+
`Details: ${details}. ` +
636+
`Underlying error: ${result.error.message}`,
637+
);
638+
}
639+
640+
if (result.status === 0) {
641+
console.log('[build-backend] Discord package validation passed.');
642+
return;
643+
}
644+
645+
let techDetails = 'unknown error';
646+
const parsed = extractPrefixedJson(result.stdout);
647+
if (parsed && !parsed.ok) {
648+
const parts = [
649+
parsed.missing?.length && `missing: ${parsed.missing.join(', ')}`,
650+
parsed.file && `package file: ${parsed.file}`,
651+
parsed.version && `version: ${parsed.version}`,
652+
parsed.error && `error: ${parsed.error}`,
653+
].filter(Boolean);
654+
if (parts.length) techDetails = parts.join('; ');
655+
} else {
656+
const stderr = result.stderr?.trim();
657+
const stdout = result.stdout?.trim();
658+
techDetails = stderr || stdout || techDetails;
659+
}
660+
661+
throw new Error(
662+
`Discord package validation failed (exit code ${result.status}): ${techDetails}. ` +
663+
'The most common cause is the PyPI "discord" stub package being installed ' +
664+
'instead of "py-cord", but other mis-installs can also trigger this. ' +
665+
'Please clean the build environment (pip cache, virtualenv) and retry.',
666+
);
667+
};
668+
587669
const main = () => {
588670
const resolvedSourceDir = requireSourceDir();
589671

@@ -615,6 +697,7 @@ const main = () => {
615697
copyAppSources(resolvedSourceDir);
616698
const runtimePython = prepareRuntimeExecutable(runtimeSourceReal);
617699
installRuntimeDependencies(runtimePython);
700+
validateDiscordPackage(runtimePython);
618701
generateRuntimeCoreLock({
619702
runtimePython,
620703
outputPath: runtimeCoreLockPath,
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import json
2+
import sys
3+
4+
5+
result = {"ok": False, "missing": ["discord"], "file": None, "version": None}
6+
try:
7+
import discord
8+
result["file"] = getattr(discord, "__file__", None)
9+
result["version"] = getattr(discord, "__version__", None)
10+
11+
if not hasattr(discord, "Bot"):
12+
result["missing"] = ["discord.Bot"]
13+
else:
14+
result["ok"] = True
15+
result["missing"] = []
16+
except ImportError as e:
17+
result["error"] = f"{type(e).__name__}: {e}"
18+
except Exception as e:
19+
result["error"] = f"{type(e).__name__}: {e}"
20+
21+
# Prefix is passed as argv[1] by the caller so it is defined in one place.
22+
# Default allows running the script standalone for quick checks.
23+
prefix = sys.argv[1] if len(sys.argv) > 1 else "ASTRBOT_VALIDATE_DISCORD_JSON:"
24+
print(f"{prefix}{json.dumps(result)}")
25+
sys.exit(0 if result["ok"] else 1)

0 commit comments

Comments
 (0)