diff --git a/.claude/agents/_autonomous_pickup.md b/.claude/agents/_autonomous_pickup.md index 554d9edc..d36af51f 100644 --- a/.claude/agents/_autonomous_pickup.md +++ b/.claude/agents/_autonomous_pickup.md @@ -36,8 +36,11 @@ env vars доступны во **всех** новых shell-сессиях ав Проверь что выставлены: ```powershell -[System.Environment]::GetEnvironmentVariable("FORGEJO_TOKEN_BACKEND", "User") | Out-Null -if (-not $?) { Write-Error "❌ Запусти setup-bot-env.ps1 сначала" } +# GetEnvironmentVariable возвращает $null если var отсутствует — НЕ throws. +# Поэтому проверяем результат напрямую (не через $? — он у Get* всегда $true). +if (-not [System.Environment]::GetEnvironmentVariable("FORGEJO_TOKEN_BACKEND", "User")) { + Write-Error "❌ FORGEJO_TOKEN_BACKEND не выставлен. Запусти scripts/setup-bot-env.ps1 сначала" +} ``` ### Шаг 2 — Env vars + git identity (per окно) diff --git a/.forgejo/workflows/stale-claims.yml b/.forgejo/workflows/stale-claims.yml index c760c751..171c9ebe 100644 --- a/.forgejo/workflows/stale-claims.yml +++ b/.forgejo/workflows/stale-claims.yml @@ -15,6 +15,10 @@ on: - cron: '*/30 * * * *' workflow_dispatch: {} # manual run возможен через UI +concurrency: + group: stale-claims + cancel-in-progress: false + jobs: cleanup: runs-on: ubuntu-latest diff --git a/scripts/cleanup-stale-claims.sh b/scripts/cleanup-stale-claims.sh index 23a4ccee..3eebf60c 100644 --- a/scripts/cleanup-stale-claims.sh +++ b/scripts/cleanup-stale-claims.sh @@ -3,7 +3,7 @@ # # Освобождает issues застрявшие в `status/wip` дольше 4 часов: # - убирает assignee -# - меняет label status/wip → status/ready +# - меняет label status/wip → status/ready (delete wip → add ready) # # Why: если worker crashes или window закрывается во время работы, issue # остаётся wip+assigned forever. Без cleanup queue забивается. @@ -16,6 +16,11 @@ # FORGEJO_REPO — lekss361/gendesign # STALE_HOURS — порог в часах (default 4) # +# Assumption: issue.updated_at = последний touch (label/comment/assignee +# change). Worker делающий task >4h без касаний issue будет считаться stale. +# Для текущих small tasks 4h enough; в Phase 4 можно добавить heartbeat +# comment'ы от worker'а. +# # Exit codes: # 0 — success (even if 0 stale found) # 1 — env vars missing / API error @@ -26,7 +31,7 @@ set -euo pipefail : "${FORGEJO_URL:?FORGEJO_URL required}" : "${FORGEJO_REPO:?FORGEJO_REPO required}" -STALE_HOURS="${STALE_HOURS:-4}" +export STALE_HOURS="${STALE_HOURS:-4}" STALE_SECS=$((STALE_HOURS * 3600)) curl_forgejo() { @@ -36,34 +41,56 @@ curl_forgejo() { } now_utc_epoch() { - python3 -c 'import datetime; print(int(datetime.datetime.now(datetime.UTC).timestamp()))' -} - -iso_to_epoch() { - python3 -c "import datetime; print(int(datetime.datetime.fromisoformat('$1'.replace('Z','+00:00')).timestamp()))" + python3 -c 'import datetime; print(int(datetime.datetime.now(datetime.timezone.utc).timestamp()))' } echo "[$(date -Is)] cleanup-stale-claims: scanning status/wip issues (stale threshold: ${STALE_HOURS}h)" NOW=$(now_utc_epoch) -CUTOFF=$((NOW - STALE_SECS)) +export CUTOFF=$((NOW - STALE_SECS)) -# GET all open issues с label status/wip -RESPONSE=$(curl_forgejo "repos/$FORGEJO_REPO/issues?labels=status/wip&state=open&limit=50&page=1") +# GET all open issues с label status/wip — pagination в Python (более надёжно чем +# склеивать JSON arrays в bash). Defensive cap 10 pages × 50 = 500. + +ISSUES_TMP=$(mktemp) +trap 'rm -f "$ISSUES_TMP"' EXIT + +for PAGE in 1 2 3 4 5 6 7 8 9 10; do + PAGE_RESP=$(curl_forgejo "repos/$FORGEJO_REPO/issues?labels=status/wip&state=open&limit=50&page=$PAGE") + echo "$PAGE_RESP" >> "$ISSUES_TMP" + echo "---PAGE_END---" >> "$ISSUES_TMP" + PAGE_LEN=$(echo "$PAGE_RESP" | python3 -c 'import json,sys; print(len(json.load(sys.stdin)))') + if [ "$PAGE_LEN" -lt 50 ]; then + break + fi +done # Парсим JSON -echo "$RESPONSE" | python3 <= cutoff: @@ -85,7 +110,6 @@ for issue in data: print(f" assignees: {assignees}") print(f" → releasing claim") - # Build commands repo = os.environ["FORGEJO_REPO"] base = os.environ["FORGEJO_URL"] + "/api/v1" token = os.environ["FORGEJO_TOKEN"] @@ -99,20 +123,21 @@ for issue in data: check=True, capture_output=True, ) - # 2. Add status/ready label - subprocess.run( - ["curl", "-sS", "-X", "POST", *auth, "-d", '{"labels": ["status/ready"]}', - f"{base}/repos/{repo}/issues/{issue_num}/labels"], - check=True, capture_output=True, - ) - - # 3. Remove status/wip label + # 2. Remove status/wip label FIRST (если падает — issue в neutral state без status/*, + # что менее опасно чем оба status/ready+status/wip одновременно) subprocess.run( ["curl", "-sS", "-X", "DELETE", *auth, f"{base}/repos/{repo}/issues/{issue_num}/labels/status%2Fwip"], check=True, capture_output=True, ) + # 3. Add status/ready label + subprocess.run( + ["curl", "-sS", "-X", "POST", *auth, "-d", '{"labels": ["status/ready"]}', + f"{base}/repos/{repo}/issues/{issue_num}/labels"], + check=True, capture_output=True, + ) + # 4. Post comment for trace comment_body = json.dumps({ "body": f"⚠️ Stale claim released: issue stuck in status/wip for {age_hours}h " @@ -130,7 +155,7 @@ for issue in data: print(f" ✗ FAILED: {e.stderr.decode() if e.stderr else 'unknown'}") errors += 1 -print(f"") +print() print(f"[OK] Released {released} stale claim(s), {errors} errors") sys.exit(1 if errors else 0) PYEOF