fix(pilot): strip opencode $schema + defensive harvest dict-strip
- opencode_review.install_config: opencode 1.3.10 rejects factory opencode.json$'\''s top-level "$schema" key with 'Unrecognized key' at parse time, causing every fresh review to fail as the misleading 'opencode empty text (rc=0)'. Add a small drop-list of known-bad top-level keys + a sanitizer applied to both the fast (no-env) and slow (env-substituted) write paths. - feedback_harvest: Gitea versions occasionally serialize reaction content / inline-comment resolver as a dict instead of a string, crashing harvester with 'dict object has no attribute strip'. Coerce both via str() with a brief comment documenting the WHY. Affected reviews: techspark/suaspark-dashboard #11 (fixed), PRAgent reviews on PR #9 era (recovered). Verified: opencode 1.3.10 emits real '### Summary of Changes' body.
This commit is contained in:
@@ -244,7 +244,9 @@ def harvest_for_pr(
|
|||||||
if react_status == 200 and isinstance(react_payload, list):
|
if react_status == 200 and isinstance(react_payload, list):
|
||||||
for r in react_payload:
|
for r in react_payload:
|
||||||
ruser = (r.get("user") or {}).get("login", "") or "?"
|
ruser = (r.get("user") or {}).get("login", "") or "?"
|
||||||
rcontent = (r.get("content") or "").strip()
|
# Gitea has occasionally returned `content` as a
|
||||||
|
# dict on older versions; coerce to str defensively.
|
||||||
|
rcontent = str(r.get("content") or "").strip()
|
||||||
if not rcontent:
|
if not rcontent:
|
||||||
continue
|
continue
|
||||||
if record_reaction(
|
if record_reaction(
|
||||||
@@ -255,9 +257,15 @@ def harvest_for_pr(
|
|||||||
stats["reactions_recorded"] += 1
|
stats["reactions_recorded"] += 1
|
||||||
|
|
||||||
# 4. Thread state (Gitea's `resolver` field on the inline
|
# 4. Thread state (Gitea's `resolver` field on the inline
|
||||||
# comment). Non-empty string = resolved.
|
# comment). Some Gitea versions serialize this as a user
|
||||||
resolver = (ic.get("resolver") or "").strip()
|
# object ({login, ...}) instead of a username string —
|
||||||
if ic.get("resolver") is not None: # field present, even if ""
|
# coerce defensively before calling .strip().
|
||||||
|
resolver_raw = ic.get("resolver")
|
||||||
|
if isinstance(resolver_raw, dict):
|
||||||
|
resolver = (resolver_raw.get("login") or "").strip()
|
||||||
|
else:
|
||||||
|
resolver = str(resolver_raw or "").strip()
|
||||||
|
if resolver_raw is not None: # field present, even if ""
|
||||||
record_thread_state(
|
record_thread_state(
|
||||||
conn, finding_id=finding_id,
|
conn, finding_id=finding_id,
|
||||||
resolved=bool(resolver),
|
resolved=bool(resolver),
|
||||||
|
|||||||
@@ -421,8 +421,32 @@ def install_config(src: str, dst: str) -> bool:
|
|||||||
return False
|
return False
|
||||||
default_url = os.environ.get("PRAGENT_MODEL_BASE_URL", "").strip()
|
default_url = os.environ.get("PRAGENT_MODEL_BASE_URL", "").strip()
|
||||||
default_key = os.environ.get("PRAGENT_MODEL_API_KEY", "").strip()
|
default_key = os.environ.get("PRAGENT_MODEL_API_KEY", "").strip()
|
||||||
|
|
||||||
|
# Strip keys opencode's runtime rejects on every version bump we touch. The
|
||||||
|
# factory `opencode.json` is committed for documentation (so `$schema`
|
||||||
|
# stays in the file for editor IntelliSense), but opencode 1.3.10 errors
|
||||||
|
# with "Unrecognized key: schema" at config-parse time and refuses to
|
||||||
|
# register ANY provider/model — surfacing to the user as the misleading
|
||||||
|
# "opencode empty text (rc=0)" failure post. Keep the drop list small and
|
||||||
|
# documented; smoke-test before adding more.
|
||||||
|
_OPENCODE_INCOMPATIBLE_TOP_KEYS = ("$schema",)
|
||||||
|
|
||||||
|
def _sanitize_and_write(cfg: dict) -> None:
|
||||||
|
for k in _OPENCODE_INCOMPATIBLE_TOP_KEYS:
|
||||||
|
cfg.pop(k, None)
|
||||||
|
with open(dst, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(cfg, f, indent=2)
|
||||||
|
|
||||||
if not default_url and not default_key:
|
if not default_url and not default_key:
|
||||||
# Fast path: no env at all → commit copy is fine, no rewrite needed.
|
# Fast path: no env at all → still sanitize (the schema key would
|
||||||
|
# poison every fresh-pod warm-up if we skipped).
|
||||||
|
try:
|
||||||
|
with open(src, encoding="utf-8") as f:
|
||||||
|
cfg = json.load(f)
|
||||||
|
_sanitize_and_write(cfg)
|
||||||
|
except (OSError, ValueError):
|
||||||
|
# If we can't parse, fall back to verbatim copy — opencode will
|
||||||
|
# report the parse error itself, no need to hide it.
|
||||||
shutil.copy2(src, dst)
|
shutil.copy2(src, dst)
|
||||||
return True
|
return True
|
||||||
try:
|
try:
|
||||||
@@ -439,8 +463,7 @@ def install_config(src: str, dst: str) -> bool:
|
|||||||
prov["options"]["baseURL"] = url
|
prov["options"]["baseURL"] = url
|
||||||
if key:
|
if key:
|
||||||
prov["options"]["apiKey"] = key
|
prov["options"]["apiKey"] = key
|
||||||
with open(dst, "w", encoding="utf-8") as f:
|
_sanitize_and_write(cfg)
|
||||||
json.dump(cfg, f, indent=2)
|
|
||||||
except (OSError, ValueError, AttributeError):
|
except (OSError, ValueError, AttributeError):
|
||||||
# A malformed config is opencode's problem to report, not ours to hide.
|
# A malformed config is opencode's problem to report, not ours to hide.
|
||||||
shutil.copy2(src, dst)
|
shutil.copy2(src, dst)
|
||||||
|
|||||||
Reference in New Issue
Block a user