fix(scripts): forgejo timeout coverage for python oracles and create-readme #42

Closed
opened 2026-08-11 16:08:39 +03:00 by slaid098 · 0 comments
Owner

Контекст

PR#41 (issue #40) пофиксил hang в _shared.ts:callForgejo (fetch timeout 10с × 3 retry + AbortSignal.any). Но аудит выявил непокрытые call sites к Forgejo API — тот же класс 5-мин hang'а:

  1. pipeline-status.py:110 — _forgejo_request вызывает urllib.request.urlopen(req) БЕЗ timeout=. 10 call sites (:128,157,164,194,238,253,281,302,332,350) наследуют безтаймаутный urlopen. Это оракул оркестратора — вызывается перед каждым pipeline-action. CI poll loop wait_timeout=300 ограничивает ЦИКЛ, но не отдельный urlopen — один hung-вызов побеждает бюджет.
  2. project-status.py:202 — _forgejo_get urlopen без timeout (branch protection check, 1 call site).
  3. spec-status.py:127 — _forgejo_get urlopen без timeout (issue view, 1 call site).
  4. create-readme.ts:316,332 — прямой fetch() к Forgejo /contents/README.md (GET + PUT), без timeout/signal/retry. Полностью bypass'ит _shared.ts:callForgejo.
  5. pipeline-status.ts:12, project-status.ts:20, spec-status.ts:13 — spawnSync("python3", [script]) без timeout. Обёртки над Python-оракулами.

Задача

  1. pipeline-status.py:110 _forgejo_request: добавить urlopen(req, timeout=OPENCODE_FORGEJO_TIMEOUT) (10с дефолт) + retry-цикл (3×, 1с sleep, skip на первой попытке — как _retry_no_checks:589-606). Retry на timeout/5xx, НЕ на 4xx. При исчерпании — return (0, "", "[forgejo] ... failed after N retries").
  2. project-status.py:202 _forgejo_get: тот же паттерн (urlopen timeout + retry).
  3. spec-status.py:127 _forgejo_get: тот же паттерн.
  4. pipeline-status.ts:12, project-status.ts:20, spec-status.ts:13: spawnSync("python3", ...) добавить timeout: 60000 (60с — с запасом над Python-внутренними 10с×3 retry = 32с).
  5. create-readme.ts:316,332: мигрировать прямой fetch() на callForgejo из _shared.ts (уже с timeout+retry+signal). Если миграция невозможна (другая семантика response) — добавить AbortSignal.timeout(10000) + retry-цикл локально, в стиле _shared.ts.
  6. Env vars: Python читает OPENCODE_FORGEJO_TIMEOUT/RETRY/RETRY_INTERVAL через os.environ.get (тот же helper-стиль что _env_int в pipeline-status.py:392-400).
  7. Логи: Python — print(..., file=sys.stderr) с [forgejo] prefix (только retry/error, не успешные вызовы). TS — process.stderr.write с [forgejo] prefix.
  8. Тесты: расширить tests/test_pipeline_status.py (если есть) на timeout/retry/4xx-no-retry. Для create-readme — Bun test если возможно.

Контракты

  • _forgejo_request(method, path, body) — сигнатура НЕ меняется. Внутренняя реализация добавляет timeout+retry.
  • urlopen(req, timeout=N) — Python 3 stdlib поддерживает timeout параметр.
  • Retry: на urllib.error.URLError (timeout/connection) и HTTP 5xx; НЕ на 4xx.
  • create-readme.ts — если мигрирует на callForgejo, то import из _shared.ts. Если локальный retry — helper _fetchWithRetry в create-readme.ts.
  • spawnSync timeout: 60000мс для Python-оракулов (60с с запасом).

Инварианты

  • CI poll loop wait_timeout=300 (5 мин) — НЕ трогаем. Это by design (ожидание CI билда). Меняем только individual urlopen timeout.
  • 4xx ошибки падают мгновенно (без ретраев).
  • Существующие вызовы _forgejo_request/_forgejo_get продолжают работать (сигнатуры не меняются).
  • OPENCODE_FORGEJO_RETRY=0 — мгновенный фейл без retry.
  • OPENCODE_FORGEJO_TIMEOUT=0 — невалидный, fallback на дефолт 10с (логировать warning в stderr).

Граничные случаи

  • urlopen timeout на медленном CI logs endpoint (/actions/jobs/{id}/logs) — может быть большой response. Timeout 10с может быть tight. Рассмотреть больший timeout для logs (20с?) или оставить 10с + retry.
  • create-readme.ts PUT /contents/README.md — large body (README может быть 50KB+). Timeout 10с может быть tight для upload. Рассмотреть 30с для PUT-операций или оставить 10с + retry.
  • pipeline-status.py CI poll loop: после фикса каждая итерация опроса ограничена 10с + retry. Если CI полностью завис, poll loop теперь будет делать 3 retry за итерацию → 32с × (300/10) итераций = много. Но это лучше бесконечного hang на одном urlopen.
  • spawnSync("python3", timeout: 60000) — если Python-скрипт превысит 60с (много retry), spawnSync убьёт процесс. TS-тул получит ошибку — нужно читаемое сообщение.

Влияние на связанные компоненты

  • pipeline-status TS tool → pipeline-status.py — транзитивно получает timeout+retry.
  • project-status TS tool → project-status.py — то же.
  • spec-status TS tool → spec-status.py — то же.
  • create-readme tool — отдельный фикс (миграция на callForgejo или локальный retry).
  • После фикса все оракулы оркестратора защищены от 5-мин hang'а.

Вне scope

  • curl в skills/agents — отдельный issue (Волна 2).
  • git spawnSync в merge-pr/create-pr/commit — отдельный issue (Волна 3).
  • Server-side Forgejo (SQLite, webhook, indexer) — issue #19 в forgejo-infra.
  • CI poll loop wait_timeout=300 — by design, не трогаем.

Критерии приемки

  • pipeline-status.py:110 urlopen имеет timeout=OPENCODE_FORGEJO_TIMEOUT (10с default).
  • pipeline-status.py _forgejo_request ретраит на timeout/5xx, НЕ на 4xx.
  • project-status.py:202 urlopen имеет timeout + retry.
  • spec-status.py:127 urlopen имеет timeout + retry.
  • pipeline-status.ts:12, project-status.ts:20, spec-status.ts:13 — spawnSync timeout: 60000.
  • create-readme.ts:316,332 — fetch с timeout (через callForgejo или локальный AbortSignal.timeout).
  • Env vars OPENCODE_FORGEJO_TIMEOUT/RETRY/RETRY_INTERVAL читаются в Python через os.environ.get.
  • Логи retry/error в stderr с [forgejo] prefix (Python: print(file=sys.stderr), TS: process.stderr.write).
  • Тесты на timeout/retry/4xx-no-retry (Python + Bun если применимо).
  • CI poll loop wait_timeout=300 НЕ изменён.
  • Худший кейс на любой Forgejo-вызов в оракулах: ≤32с (вместо бесконечности).
## Контекст PR#41 (issue #40) пофиксил hang в `_shared.ts:callForgejo` (fetch timeout 10с × 3 retry + `AbortSignal.any`). Но аудит выявил непокрытые call sites к Forgejo API — тот же класс 5-мин hang'а: 1. **`pipeline-status.py:110`** — `_forgejo_request` вызывает `urllib.request.urlopen(req)` БЕЗ `timeout=`. 10 call sites (`:128,157,164,194,238,253,281,302,332,350`) наследуют безтаймаутный urlopen. Это оракул оркестратора — вызывается перед каждым pipeline-action. CI poll loop `wait_timeout=300` ограничивает ЦИКЛ, но не отдельный `urlopen` — один hung-вызов побеждает бюджет. 2. **`project-status.py:202`** — `_forgejo_get` `urlopen` без timeout (branch protection check, 1 call site). 3. **`spec-status.py:127`** — `_forgejo_get` `urlopen` без timeout (issue view, 1 call site). 4. **`create-readme.ts:316,332`** — прямой `fetch()` к Forgejo `/contents/README.md` (GET + PUT), без timeout/signal/retry. Полностью bypass'ит `_shared.ts:callForgejo`. 5. **`pipeline-status.ts:12`, `project-status.ts:20`, `spec-status.ts:13`** — `spawnSync("python3", [script])` без timeout. Обёртки над Python-оракулами. ## Задача 1. `pipeline-status.py:110` `_forgejo_request`: добавить `urlopen(req, timeout=OPENCODE_FORGEJO_TIMEOUT)` (10с дефолт) + retry-цикл (3×, 1с sleep, skip на первой попытке — как `_retry_no_checks:589-606`). Retry на timeout/5xx, НЕ на 4xx. При исчерпании — return `(0, "", "[forgejo] ... failed after N retries")`. 2. `project-status.py:202` `_forgejo_get`: тот же паттерн (urlopen timeout + retry). 3. `spec-status.py:127` `_forgejo_get`: тот же паттерн. 4. `pipeline-status.ts:12`, `project-status.ts:20`, `spec-status.ts:13`: `spawnSync("python3", ...)` добавить `timeout: 60000` (60с — с запасом над Python-внутренними 10с×3 retry = 32с). 5. `create-readme.ts:316,332`: мигрировать прямой `fetch()` на `callForgejo` из `_shared.ts` (уже с timeout+retry+signal). Если миграция невозможна (другая семантика response) — добавить `AbortSignal.timeout(10000)` + retry-цикл локально, в стиле `_shared.ts`. 6. Env vars: Python читает `OPENCODE_FORGEJO_TIMEOUT/RETRY/RETRY_INTERVAL` через `os.environ.get` (тот же helper-стиль что `_env_int` в `pipeline-status.py:392-400`). 7. Логи: Python — `print(..., file=sys.stderr)` с `[forgejo]` prefix (только retry/error, не успешные вызовы). TS — `process.stderr.write` с `[forgejo]` prefix. 8. Тесты: расширить `tests/test_pipeline_status.py` (если есть) на timeout/retry/4xx-no-retry. Для `create-readme` — Bun test если возможно. ## Контракты - `_forgejo_request(method, path, body)` — сигнатура НЕ меняется. Внутренняя реализация добавляет timeout+retry. - `urlopen(req, timeout=N)` — Python 3 stdlib поддерживает `timeout` параметр. - Retry: на `urllib.error.URLError` (timeout/connection) и HTTP 5xx; НЕ на 4xx. - `create-readme.ts` — если мигрирует на `callForgejo`, то import из `_shared.ts`. Если локальный retry — helper `_fetchWithRetry` в `create-readme.ts`. - `spawnSync` timeout: 60000мс для Python-оракулов (60с с запасом). ## Инварианты - CI poll loop `wait_timeout=300` (5 мин) — НЕ трогаем. Это by design (ожидание CI билда). Меняем только individual `urlopen` timeout. - 4xx ошибки падают мгновенно (без ретраев). - Существующие вызовы `_forgejo_request`/`_forgejo_get` продолжают работать (сигнатуры не меняются). - `OPENCODE_FORGEJO_RETRY=0` — мгновенный фейл без retry. - `OPENCODE_FORGEJO_TIMEOUT=0` — невалидный, fallback на дефолт 10с (логировать warning в stderr). ## Граничные случаи - `urlopen` timeout на медленном CI logs endpoint (`/actions/jobs/{id}/logs`) — может быть большой response. Timeout 10с может быть tight. Рассмотреть больший timeout для logs (20с?) или оставить 10с + retry. - `create-readme.ts` PUT `/contents/README.md` — large body (README может быть 50KB+). Timeout 10с может быть tight для upload. Рассмотреть 30с для PUT-операций или оставить 10с + retry. - `pipeline-status.py` CI poll loop: после фикса каждая итерация опроса ограничена 10с + retry. Если CI полностью завис, poll loop теперь будет делать 3 retry за итерацию → 32с × (300/10) итераций = много. Но это лучше бесконечного hang на одном urlopen. - `spawnSync("python3", timeout: 60000)` — если Python-скрипт превысит 60с (много retry), spawnSync убьёт процесс. TS-тул получит ошибку — нужно читаемое сообщение. ## Влияние на связанные компоненты - `pipeline-status` TS tool → `pipeline-status.py` — транзитивно получает timeout+retry. - `project-status` TS tool → `project-status.py` — то же. - `spec-status` TS tool → `spec-status.py` — то же. - `create-readme` tool — отдельный фикс (миграция на `callForgejo` или локальный retry). - После фикса все оракулы оркестратора защищены от 5-мин hang'а. ## Вне scope - curl в skills/agents — отдельный issue (Волна 2). - git spawnSync в merge-pr/create-pr/commit — отдельный issue (Волна 3). - Server-side Forgejo (SQLite, webhook, indexer) — issue #19 в forgejo-infra. - CI poll loop `wait_timeout=300` — by design, не трогаем. ## Критерии приемки - [ ] `pipeline-status.py:110` `urlopen` имеет `timeout=OPENCODE_FORGEJO_TIMEOUT` (10с default). - [ ] `pipeline-status.py` `_forgejo_request` ретраит на timeout/5xx, НЕ на 4xx. - [ ] `project-status.py:202` `urlopen` имеет timeout + retry. - [ ] `spec-status.py:127` `urlopen` имеет timeout + retry. - [ ] `pipeline-status.ts:12`, `project-status.ts:20`, `spec-status.ts:13` — `spawnSync timeout: 60000`. - [ ] `create-readme.ts:316,332` — fetch с timeout (через `callForgejo` или локальный `AbortSignal.timeout`). - [ ] Env vars `OPENCODE_FORGEJO_TIMEOUT/RETRY/RETRY_INTERVAL` читаются в Python через `os.environ.get`. - [ ] Логи retry/error в stderr с `[forgejo]` prefix (Python: `print(file=sys.stderr)`, TS: `process.stderr.write`). - [ ] Тесты на timeout/retry/4xx-no-retry (Python + Bun если применимо). - [ ] CI poll loop `wait_timeout=300` НЕ изменён. - [ ] Худший кейс на любой Forgejo-вызов в оракулах: ≤32с (вместо бесконечности).
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
slaid098/opencode-config#42
No description provided.