Compare commits
2 Commits
feature/OR
...
fix/ORCH-3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f93cba297 | ||
| 053ea3b1c5 |
@@ -12,11 +12,17 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
python3 -m pip install --user --upgrade pip
|
python3 -m pip install --user --upgrade pip
|
||||||
python3 -m pip install --user -r requirements.txt
|
python3 -m pip install --user -r requirements.txt
|
||||||
- name: Test
|
- name: Test
|
||||||
env:
|
env:
|
||||||
PYTHONPATH: ${{ github.workspace }}
|
PYTHONPATH: ${{ github.workspace }}
|
||||||
run: |
|
run: |
|
||||||
|
# ORCH-39: fail the job on ANY failure. Run the WHOLE suite from the
|
||||||
|
# repo root. --strict-markers + pytest-asyncio (asyncio_mode=auto, see
|
||||||
|
# pytest.ini) make async tests actually run instead of silently
|
||||||
|
# skipping (the hole that hid red tests behind a green CI).
|
||||||
|
set -euo pipefail
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
python3 -m pytest tests/ -q
|
python3 -m pytest tests/ -q -p no:cacheprovider --strict-markers
|
||||||
|
|||||||
97
docs/work-items/ORCH-016/15-staging-log.md
Normal file
97
docs/work-items/ORCH-016/15-staging-log.md
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
---
|
||||||
|
staging_status: SUCCESS
|
||||||
|
timestamp: 2026-06-05T12:47:48Z
|
||||||
|
base_url: http://localhost:8501
|
||||||
|
work_item: ORCH-016
|
||||||
|
branch: feature/ORCH-016-plane
|
||||||
|
mode: stub
|
||||||
|
---
|
||||||
|
|
||||||
|
# Staging Gate Log — ORCH-016
|
||||||
|
|
||||||
|
## Verdict
|
||||||
|
|
||||||
|
**`staging_status: SUCCESS`** — staging test suite completed, all 10/10 checks PASS.
|
||||||
|
|
||||||
|
## Окружение
|
||||||
|
|
||||||
|
- **Base URL:** `http://localhost:8501` (orchestrator-staging)
|
||||||
|
- **Mode:** `stub` (без LLM-spend; проверяет ранние артефакты pipeline — branch + queued analyst job)
|
||||||
|
- **Suite:** `scripts/staging_check.py` (ORCH-33)
|
||||||
|
- **Sandbox project:** `8c5a3025-4f9d-4190-b79f-fa06276bb27e` (ORCH Sandbox)
|
||||||
|
- **Repo под тест:** `orchestrator-sandbox`
|
||||||
|
|
||||||
|
## Результаты (10/10 PASS)
|
||||||
|
|
||||||
|
### Block A — SMOKE
|
||||||
|
| ID | Проверка | Результат |
|
||||||
|
|----|----------|-----------|
|
||||||
|
| A1 | `GET /health` → 200, `status=ok` | ✓ PASS |
|
||||||
|
| A2 | `GET /queue` → 200, ключи `counts/max_concurrency/resilience` | ✓ PASS |
|
||||||
|
| A3 | `ORCH_STAGING=true` (защита от прод-окружения) | ✓ PASS |
|
||||||
|
|
||||||
|
### Block B — ACCESS
|
||||||
|
| ID | Проверка | Результат |
|
||||||
|
|----|----------|-----------|
|
||||||
|
| B4 | Plane: sandbox project accessible (5 projects, sandbox=YES) | ✓ PASS |
|
||||||
|
| B5 | Gitea: `orchestrator-sandbox` доступен, `push=true` | ✓ PASS |
|
||||||
|
| B6 | Registry: sandbox в known IDs, prod ET/ORCH отсутствуют | ✓ PASS |
|
||||||
|
|
||||||
|
### Block C — E2E (mode=stub)
|
||||||
|
| ID | Проверка | Результат |
|
||||||
|
|----|----------|-----------|
|
||||||
|
| C7 | Create issue in Plane SANDBOX → HTTP 201, `issue_id=37d91fba-5ac1-460b-ab06-a13f963911bc` | ✓ PASS |
|
||||||
|
| C8 | Trigger pipeline via `POST /webhook/plane` (с HMAC) → HTTP 200, `status=accepted` | ✓ PASS |
|
||||||
|
| C9a | Branch появилась в `orchestrator-sandbox` → `feature/SANDBOX-009-staging-check-e2e-20260605t124` | ✓ PASS |
|
||||||
|
| C9b | Analyst job в очереди staging (`/queue` → recent) → `job_id=5, status=queued, agent=analyst` | ✓ PASS |
|
||||||
|
|
||||||
|
### Cleanup
|
||||||
|
- Удалена тестовая ветка в Gitea (HTTP 204).
|
||||||
|
- Удалён тестовый Plane issue (HTTP 204).
|
||||||
|
- DB-cleanup: task row отсутствовал (нормально для stub-mode), dedup-таблица отсутствует (некритично).
|
||||||
|
|
||||||
|
## Что значит "SUCCESS" для ORCH-016
|
||||||
|
|
||||||
|
ORCH-016 — это унификация финальных коммент-логов агентов (`usage.build_status_comment` + длительность). Изменения затрагивают:
|
||||||
|
- `src/usage.py` — расширен билдер коммента (длительность, defensive формат).
|
||||||
|
- `src/agents/launcher.py` — пробрасывает `duration_s` из `_monitor_agent` в `_post_usage_comments`.
|
||||||
|
- `src/stage_engine.py` — для analyst-стадии использует DB-fallback `usage.get_agent_duration(task_id, agent)`.
|
||||||
|
- `src/frontmatter.py` — defensive `read_frontmatter_value(...)`.
|
||||||
|
|
||||||
|
Staging-стенд (orchestrator-staging) поднят на актуальном образе и:
|
||||||
|
1. Принимает Plane-webhook (HMAC OK).
|
||||||
|
2. Корректно фильтрует проекты через registry (B6 — sandbox разрешён, прод ET/ORCH отрезаны).
|
||||||
|
3. Дотягивает pipeline до постановки analyst job в персистентную очередь (ORCH-1) и создания ветки в Gitea.
|
||||||
|
|
||||||
|
Поведение коммент-логов в реальном e2e (mode=full-real) НЕ проверялось — это требует LLM-spend и реального запуска агентов. В рамках staging-gate для ORCH-016 это считается достаточным: финальный коммент строится из артефактов (`12-review.md`, `13-test-report.md`, ...) и uses-данных из `agent_runs`, которые уже покрыты unit-тестами в `tests/`.
|
||||||
|
|
||||||
|
## Откат не требуется
|
||||||
|
|
||||||
|
Все 10 проверок зелёные → переход на стадию `deploy` разрешён. Прод-контейнер `orchestrator` (8500) в рамках этой стадии НЕ перезапускался (правило self-hosting, `CLAUDE.md`).
|
||||||
|
|
||||||
|
## Команда запуска (для воспроизведения)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Загрузить .env.staging БЕЗ shell-source (JSON-значения ломают bash):
|
||||||
|
python3 -c "
|
||||||
|
import os, subprocess
|
||||||
|
env = dict(os.environ)
|
||||||
|
with open('/repos/orchestrator/.env.staging') as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
if not line or line.startswith('#') or '=' not in line:
|
||||||
|
continue
|
||||||
|
k, _, v = line.partition('=')
|
||||||
|
env[k.strip()] = v.strip()
|
||||||
|
r = subprocess.run(
|
||||||
|
['python3', 'scripts/staging_check.py',
|
||||||
|
'--base-url', 'http://localhost:8501', '--mode', 'stub'],
|
||||||
|
env=env,
|
||||||
|
)
|
||||||
|
exit(r.returncode)
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Stage: `deploy-staging` → `deploy`. Quality Gate `check_staging_status` ожидает `staging_status: SUCCESS` в frontmatter этого файла.*
|
||||||
13
pytest.ini
Normal file
13
pytest.ini
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[pytest]
|
||||||
|
# ORCH-39: make the async webhook/state tests (test_orch10_states.py) actually
|
||||||
|
# run in every environment. Without pytest-asyncio + asyncio_mode=auto these
|
||||||
|
# @pytest.mark.asyncio tests were silently SKIPPED, so a broken async path
|
||||||
|
# could pass CI. asyncio_mode=auto runs `async def test_*` natively.
|
||||||
|
asyncio_mode = auto
|
||||||
|
|
||||||
|
# Fail loudly on unknown markers so a typo'd @pytest.mark.* can't silently
|
||||||
|
# disable a test.
|
||||||
|
markers =
|
||||||
|
asyncio: mark a coroutine test to be run by pytest-asyncio.
|
||||||
|
|
||||||
|
testpaths = tests
|
||||||
@@ -3,3 +3,4 @@ uvicorn[standard]==0.30.0
|
|||||||
pydantic-settings==2.5.0
|
pydantic-settings==2.5.0
|
||||||
httpx==0.27.0
|
httpx==0.27.0
|
||||||
pytest==8.3.3
|
pytest==8.3.3
|
||||||
|
pytest-asyncio==0.23.8
|
||||||
|
|||||||
@@ -34,6 +34,27 @@ import src.plane_sync as plane_sync # noqa: E402
|
|||||||
ORCH_PLANE_ID = "8da6aa25-a60e-44d6-a1e2-d8ae59aa7d6a"
|
ORCH_PLANE_ID = "8da6aa25-a60e-44d6-a1e2-d8ae59aa7d6a"
|
||||||
ENDURO_PLANE_ID = "7a79f0a9-5278-49cd-9007-9a338f238f9c"
|
ENDURO_PLANE_ID = "7a79f0a9-5278-49cd-9007-9a338f238f9c"
|
||||||
|
|
||||||
|
# ORCH-39: after ORCH-10 the webhook resolves Plane state UUIDs per-project via
|
||||||
|
# get_project_states(project_id). Mock it deterministically (no network) and
|
||||||
|
# send each request with the UUID that matches its own project.
|
||||||
|
_PROJECT_STATES = {
|
||||||
|
ENDURO_PLANE_ID: {
|
||||||
|
"in_progress": "b873d9eb-993c-48cd-97ac-99a9b1623967",
|
||||||
|
"approved": "a519a341-dada-4a91-8910-7604f82b79c5",
|
||||||
|
"rejected": "ba958f3c-5db5-461d-8f82-89425e413b97",
|
||||||
|
},
|
||||||
|
ORCH_PLANE_ID: {
|
||||||
|
"in_progress": "e331bfb3-e17e-4699-ba48-4abb89c21b7b",
|
||||||
|
"approved": "63f2c8fe-dcda-4ace-952f-dd88bd0118ff",
|
||||||
|
"rejected": "4c769e90-bf80-4a52-b97a-e1c84904bfc3",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _fake_get_project_states(project_id):
|
||||||
|
return _PROJECT_STATES.get(project_id, _PROJECT_STATES[ENDURO_PLANE_ID])
|
||||||
|
|
||||||
|
|
||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
|
|
||||||
|
|
||||||
@@ -48,6 +69,10 @@ def setup(monkeypatch):
|
|||||||
|
|
||||||
monkeypatch.setattr("src.webhooks.plane.verify_plane_signature", lambda body, sig: True)
|
monkeypatch.setattr("src.webhooks.plane.verify_plane_signature", lambda body, sig: True)
|
||||||
|
|
||||||
|
# ORCH-39: deterministic per-project Plane states, clean cache per test.
|
||||||
|
plane_sync.reload_project_states()
|
||||||
|
monkeypatch.setattr(plane_sync, "get_project_states", _fake_get_project_states)
|
||||||
|
|
||||||
registry_json = (
|
registry_json = (
|
||||||
f'[{{"plane_project_id": "{ENDURO_PLANE_ID}", "repo": "enduro-trails",'
|
f'[{{"plane_project_id": "{ENDURO_PLANE_ID}", "repo": "enduro-trails",'
|
||||||
f' "work_item_prefix": "ET", "name": "enduro-trails"}},'
|
f' "work_item_prefix": "ET", "name": "enduro-trails"}},'
|
||||||
@@ -60,6 +85,7 @@ def setup(monkeypatch):
|
|||||||
yield
|
yield
|
||||||
|
|
||||||
reload_projects()
|
reload_projects()
|
||||||
|
plane_sync.reload_project_states()
|
||||||
if os.path.exists(_test_db):
|
if os.path.exists(_test_db):
|
||||||
os.unlink(_test_db)
|
os.unlink(_test_db)
|
||||||
|
|
||||||
@@ -103,10 +129,9 @@ def test_fetch_sequence_id_missing_field_returns_none():
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
# Feature 1: pipeline starts on a status change to In Progress, not on creation.
|
# Feature 1: pipeline starts on a status change to In Progress, not on creation.
|
||||||
_IN_PROGRESS = "b873d9eb-993c-48cd-97ac-99a9b1623967"
|
# ORCH-39: in_progress UUID is project-specific; derive it from the project.
|
||||||
|
|
||||||
|
|
||||||
def _post(plane_id, plane_project_id=ORCH_PLANE_ID, name="A valid work item title"):
|
def _post(plane_id, plane_project_id=ORCH_PLANE_ID, name="A valid work item title"):
|
||||||
|
in_progress = _fake_get_project_states(plane_project_id)["in_progress"]
|
||||||
return client.post(
|
return client.post(
|
||||||
"/webhook/plane",
|
"/webhook/plane",
|
||||||
json={
|
json={
|
||||||
@@ -117,7 +142,7 @@ def _post(plane_id, plane_project_id=ORCH_PLANE_ID, name="A valid work item titl
|
|||||||
"name": name,
|
"name": name,
|
||||||
"description_stripped": "This is a sufficiently long description.",
|
"description_stripped": "This is a sufficiently long description.",
|
||||||
"project": plane_project_id,
|
"project": plane_project_id,
|
||||||
"state": {"id": _IN_PROGRESS, "name": "In Progress", "group": "started"},
|
"state": {"id": in_progress, "name": "In Progress", "group": "started"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -33,11 +33,36 @@ from src.main import app # noqa: E402
|
|||||||
from src.db import init_db, get_db # noqa: E402
|
from src.db import init_db, get_db # noqa: E402
|
||||||
from src import projects as P # noqa: E402
|
from src import projects as P # noqa: E402
|
||||||
from src.projects import reload_projects # noqa: E402
|
from src.projects import reload_projects # noqa: E402
|
||||||
|
import src.plane_sync as plane_sync # noqa: E402
|
||||||
|
|
||||||
ORCH_PLANE_ID = "8da6aa25-a60e-44d6-a1e2-d8ae59aa7d6a"
|
ORCH_PLANE_ID = "8da6aa25-a60e-44d6-a1e2-d8ae59aa7d6a"
|
||||||
ENDURO_PLANE_ID = "7a79f0a9-5278-49cd-9007-9a338f238f9c"
|
ENDURO_PLANE_ID = "7a79f0a9-5278-49cd-9007-9a338f238f9c"
|
||||||
UNKNOWN_PLANE_ID = "deadbeef-0000-0000-0000-000000000000"
|
UNKNOWN_PLANE_ID = "deadbeef-0000-0000-0000-000000000000"
|
||||||
|
|
||||||
|
# ORCH-39: after ORCH-10 the webhook resolves Plane state UUIDs per-project via
|
||||||
|
# get_project_states(project_id). Hardcoding the enduro in_progress UUID for an
|
||||||
|
# ORCH-project payload no longer matches, so the pipeline never starts. We mock
|
||||||
|
# get_project_states with a deterministic per-project map (no network) and send
|
||||||
|
# each request with the UUID that matches its own project.
|
||||||
|
_PROJECT_STATES = {
|
||||||
|
ENDURO_PLANE_ID: {
|
||||||
|
"in_progress": "b873d9eb-993c-48cd-97ac-99a9b1623967",
|
||||||
|
"approved": "a519a341-dada-4a91-8910-7604f82b79c5",
|
||||||
|
"rejected": "ba958f3c-5db5-461d-8f82-89425e413b97",
|
||||||
|
},
|
||||||
|
ORCH_PLANE_ID: {
|
||||||
|
"in_progress": "e331bfb3-e17e-4699-ba48-4abb89c21b7b",
|
||||||
|
"approved": "63f2c8fe-dcda-4ace-952f-dd88bd0118ff",
|
||||||
|
"rejected": "4c769e90-bf80-4a52-b97a-e1c84904bfc3",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _fake_get_project_states(project_id):
|
||||||
|
"""Deterministic per-project state map; mirrors get_project_states' fallback
|
||||||
|
for unknown projects so the webhook still behaves sensibly."""
|
||||||
|
return _PROJECT_STATES.get(project_id, _PROJECT_STATES[ENDURO_PLANE_ID])
|
||||||
|
|
||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
|
|
||||||
|
|
||||||
@@ -57,6 +82,13 @@ def setup(monkeypatch):
|
|||||||
# focuses on the project filter, so bypass signature verification.
|
# focuses on the project filter, so bypass signature verification.
|
||||||
monkeypatch.setattr("src.webhooks.plane.verify_plane_signature", lambda body, sig: True)
|
monkeypatch.setattr("src.webhooks.plane.verify_plane_signature", lambda body, sig: True)
|
||||||
|
|
||||||
|
# ORCH-39: resolve Plane states deterministically per-project (no network)
|
||||||
|
# and start from a clean per-project cache so suites don't leak into each
|
||||||
|
# other. plane.py imports get_project_states locally from ..plane_sync, so
|
||||||
|
# patch it at the src.plane_sync source.
|
||||||
|
plane_sync.reload_project_states()
|
||||||
|
monkeypatch.setattr(plane_sync, "get_project_states", _fake_get_project_states)
|
||||||
|
|
||||||
registry_json = (
|
registry_json = (
|
||||||
f'[{{"plane_project_id": "{ENDURO_PLANE_ID}", "repo": "enduro-trails",'
|
f'[{{"plane_project_id": "{ENDURO_PLANE_ID}", "repo": "enduro-trails",'
|
||||||
f' "work_item_prefix": "ET", "name": "enduro-trails"}},'
|
f' "work_item_prefix": "ET", "name": "enduro-trails"}},'
|
||||||
@@ -69,6 +101,7 @@ def setup(monkeypatch):
|
|||||||
yield
|
yield
|
||||||
|
|
||||||
reload_projects() # restore from env
|
reload_projects() # restore from env
|
||||||
|
plane_sync.reload_project_states()
|
||||||
if os.path.exists(_test_db):
|
if os.path.exists(_test_db):
|
||||||
os.unlink(_test_db)
|
os.unlink(_test_db)
|
||||||
|
|
||||||
@@ -76,10 +109,10 @@ def setup(monkeypatch):
|
|||||||
# Feature 1: the pipeline now starts on a status change to In Progress (not on
|
# Feature 1: the pipeline now starts on a status change to In Progress (not on
|
||||||
# creation). _post_created drives that status-change event so these ORCH-6
|
# creation). _post_created drives that status-change event so these ORCH-6
|
||||||
# routing tests still exercise task creation through the new trigger.
|
# routing tests still exercise task creation through the new trigger.
|
||||||
_IN_PROGRESS = "b873d9eb-993c-48cd-97ac-99a9b1623967"
|
# ORCH-39: the in_progress UUID is now project-specific, so derive it from the
|
||||||
|
# project being posted to (matches get_project_states resolution above).
|
||||||
|
|
||||||
def _post_created(plane_project_id, plane_id="wi-1", name="A valid work item title"):
|
def _post_created(plane_project_id, plane_id="wi-1", name="A valid work item title"):
|
||||||
|
in_progress = _fake_get_project_states(plane_project_id)["in_progress"]
|
||||||
return client.post(
|
return client.post(
|
||||||
"/webhook/plane",
|
"/webhook/plane",
|
||||||
json={
|
json={
|
||||||
@@ -90,7 +123,7 @@ def _post_created(plane_project_id, plane_id="wi-1", name="A valid work item tit
|
|||||||
"name": name,
|
"name": name,
|
||||||
"description_stripped": "This is a sufficiently long description.",
|
"description_stripped": "This is a sufficiently long description.",
|
||||||
"project": plane_project_id,
|
"project": plane_project_id,
|
||||||
"state": {"id": _IN_PROGRESS, "name": "In Progress", "group": "started"},
|
"state": {"id": in_progress, "name": "In Progress", "group": "started"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user