jleechanorg-pr-automation 0.2.48__tar.gz → 0.2.82__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {jleechanorg_pr_automation-0.2.48 → jleechanorg_pr_automation-0.2.82}/PKG-INFO +4 -3
- {jleechanorg_pr_automation-0.2.48 → jleechanorg_pr_automation-0.2.82}/README.md +1 -1
- {jleechanorg_pr_automation-0.2.48 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/__init__.py +15 -4
- {jleechanorg_pr_automation-0.2.48 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/automation_safety_manager.py +104 -27
- {jleechanorg_pr_automation-0.2.48 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/codex_config.py +2 -0
- {jleechanorg_pr_automation-0.2.48 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/jleechanorg_pr_monitor.py +1258 -342
- {jleechanorg_pr_automation-0.2.48 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/logging_utils.py +5 -3
- jleechanorg_pr_automation-0.2.82/jleechanorg_pr_automation/orchestrated_pr_runner.py +835 -0
- jleechanorg_pr_automation-0.2.82/jleechanorg_pr_automation/tests/test_atomic_race_condition.py +261 -0
- {jleechanorg_pr_automation-0.2.48 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/tests/test_attempt_limit_logic.py +25 -16
- jleechanorg_pr_automation-0.2.82/jleechanorg_pr_automation/tests/test_auto_detect_user.py +90 -0
- {jleechanorg_pr_automation-0.2.48 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/tests/test_automation_over_running_reproduction.py +2 -1
- {jleechanorg_pr_automation-0.2.48 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/tests/test_automation_safety_limits.py +7 -3
- {jleechanorg_pr_automation-0.2.48 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/tests/test_automation_safety_manager_comprehensive.py +2 -2
- {jleechanorg_pr_automation-0.2.48 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/tests/test_cleanup_pending_reviews.py +242 -12
- jleechanorg_pr_automation-0.2.82/jleechanorg_pr_automation/tests/test_cli_agent_argument.py +125 -0
- jleechanorg_pr_automation-0.2.82/jleechanorg_pr_automation/tests/test_comment_validation.py +227 -0
- jleechanorg_pr_automation-0.2.82/jleechanorg_pr_automation/tests/test_cursor_bot_round2_bugs.py +263 -0
- jleechanorg_pr_automation-0.2.82/jleechanorg_pr_automation/tests/test_cursor_bug_fixes.py +274 -0
- jleechanorg_pr_automation-0.2.82/jleechanorg_pr_automation/tests/test_dirty_repo_handling.py +195 -0
- {jleechanorg_pr_automation-0.2.48 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/tests/test_fixpr_prompt.py +9 -5
- jleechanorg_pr_automation-0.2.82/jleechanorg_pr_automation/tests/test_fixpr_return_value.py +506 -0
- jleechanorg_pr_automation-0.2.82/jleechanorg_pr_automation/tests/test_inflight_cache_reload.py +191 -0
- jleechanorg_pr_automation-0.2.82/jleechanorg_pr_automation/tests/test_jleechanorg_pr_monitor_requests.py +326 -0
- {jleechanorg_pr_automation-0.2.48 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/tests/test_model_parameter.py +19 -6
- jleechanorg_pr_automation-0.2.82/jleechanorg_pr_automation/tests/test_orchestrated_pr_runner.py +1203 -0
- {jleechanorg_pr_automation-0.2.48 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/tests/test_pr_filtering_matrix.py +2 -1
- jleechanorg_pr_automation-0.2.82/jleechanorg_pr_automation/tests/test_pr_monitor_eligibility.py +1013 -0
- jleechanorg_pr_automation-0.2.82/jleechanorg_pr_automation/tests/test_rolling_window.py +377 -0
- {jleechanorg_pr_automation-0.2.48 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/tests/test_workflow_specific_limits.py +25 -2
- {jleechanorg_pr_automation-0.2.48 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/utils.py +59 -0
- {jleechanorg_pr_automation-0.2.48 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation.egg-info/PKG-INFO +4 -3
- {jleechanorg_pr_automation-0.2.48 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation.egg-info/SOURCES.txt +10 -0
- {jleechanorg_pr_automation-0.2.48 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation.egg-info/requires.txt +1 -0
- {jleechanorg_pr_automation-0.2.48 → jleechanorg_pr_automation-0.2.82}/pyproject.toml +5 -4
- jleechanorg_pr_automation-0.2.48/jleechanorg_pr_automation/orchestrated_pr_runner.py +0 -526
- jleechanorg_pr_automation-0.2.48/jleechanorg_pr_automation/tests/test_fixpr_return_value.py +0 -140
- jleechanorg_pr_automation-0.2.48/jleechanorg_pr_automation/tests/test_orchestrated_pr_runner.py +0 -697
- jleechanorg_pr_automation-0.2.48/jleechanorg_pr_automation/tests/test_pr_monitor_eligibility.py +0 -354
- {jleechanorg_pr_automation-0.2.48 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/STORAGE_STATE_TESTING_PROTOCOL.md +0 -0
- {jleechanorg_pr_automation-0.2.48 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/automation_safety_wrapper.py +0 -0
- {jleechanorg_pr_automation-0.2.48 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/automation_utils.py +0 -0
- {jleechanorg_pr_automation-0.2.48 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/check_codex_comment.py +0 -0
- {jleechanorg_pr_automation-0.2.48 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/codex_branch_updater.py +0 -0
- {jleechanorg_pr_automation-0.2.48 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/openai_automation/__init__.py +0 -0
- {jleechanorg_pr_automation-0.2.48 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/openai_automation/codex_github_mentions.py +0 -0
- {jleechanorg_pr_automation-0.2.48 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/openai_automation/debug_page_content.py +0 -0
- {jleechanorg_pr_automation-0.2.48 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/openai_automation/oracle_cli.py +0 -0
- {jleechanorg_pr_automation-0.2.48 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/openai_automation/test_auth_restoration.py +0 -0
- {jleechanorg_pr_automation-0.2.48 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/openai_automation/test_codex_comprehensive.py +0 -0
- {jleechanorg_pr_automation-0.2.48 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/openai_automation/test_codex_integration.py +0 -0
- {jleechanorg_pr_automation-0.2.48 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/tests/__init__.py +0 -0
- {jleechanorg_pr_automation-0.2.48 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/tests/conftest.py +0 -0
- {jleechanorg_pr_automation-0.2.48 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/tests/test_actionable_counting_matrix.py +0 -0
- {jleechanorg_pr_automation-0.2.48 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/tests/test_automation_marker_functions.py +0 -0
- {jleechanorg_pr_automation-0.2.48 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/tests/test_codex_actor_matching.py +0 -0
- {jleechanorg_pr_automation-0.2.48 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/tests/test_graphql_error_handling.py +0 -0
- {jleechanorg_pr_automation-0.2.48 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/tests/test_packaging_integration.py +0 -0
- {jleechanorg_pr_automation-0.2.48 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/tests/test_pr_targeting.py +0 -0
- {jleechanorg_pr_automation-0.2.48 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/tests/test_version_consistency.py +0 -0
- {jleechanorg_pr_automation-0.2.48 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/tests/test_workspace_dispatch_missing_dir.py +0 -0
- {jleechanorg_pr_automation-0.2.48 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation.egg-info/dependency_links.txt +0 -0
- {jleechanorg_pr_automation-0.2.48 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation.egg-info/entry_points.txt +0 -0
- {jleechanorg_pr_automation-0.2.48 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation.egg-info/top_level.txt +0 -0
- {jleechanorg_pr_automation-0.2.48 → jleechanorg_pr_automation-0.2.82}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: jleechanorg-pr-automation
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.82
|
|
4
4
|
Summary: GitHub PR automation system with safety limits and actionable counting
|
|
5
5
|
Author-email: jleechan <jlee@jleechan.org>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -17,13 +17,14 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.12
|
|
18
18
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
19
|
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
20
|
-
Requires-Python: >=3.
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
22
|
Requires-Dist: requests>=2.25.0
|
|
23
23
|
Requires-Dist: jleechanorg-orchestration>=0.1.18
|
|
24
24
|
Requires-Dist: playwright>=1.40.0
|
|
25
25
|
Requires-Dist: playwright-stealth>=1.0.0
|
|
26
26
|
Requires-Dist: aiohttp>=3.8.0
|
|
27
|
+
Requires-Dist: PyYAML>=6.0
|
|
27
28
|
Provides-Extra: email
|
|
28
29
|
Requires-Dist: keyring>=23.0.0; extra == "email"
|
|
29
30
|
Provides-Extra: dev
|
|
@@ -290,7 +291,7 @@ jleechanorg-pr-monitor --fixpr --dry-run
|
|
|
290
291
|
|
|
291
292
|
```bash
|
|
292
293
|
# Monitor and fix in one command
|
|
293
|
-
jleechanorg-pr-monitor --fixpr --max-prs 5 --
|
|
294
|
+
jleechanorg-pr-monitor --fixpr --max-prs 5 --cli-agent claude
|
|
294
295
|
```
|
|
295
296
|
|
|
296
297
|
### Agent CLI Options
|
|
@@ -255,7 +255,7 @@ jleechanorg-pr-monitor --fixpr --dry-run
|
|
|
255
255
|
|
|
256
256
|
```bash
|
|
257
257
|
# Monitor and fix in one command
|
|
258
|
-
jleechanorg-pr-monitor --fixpr --max-prs 5 --
|
|
258
|
+
jleechanorg-pr-monitor --fixpr --max-prs 5 --cli-agent claude
|
|
259
259
|
```
|
|
260
260
|
|
|
261
261
|
### Agent CLI Options
|
|
@@ -6,12 +6,23 @@ safety features, intelligent filtering, and cross-process synchronization.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import re
|
|
9
|
-
from importlib.metadata import PackageNotFoundError
|
|
9
|
+
from importlib.metadata import PackageNotFoundError
|
|
10
|
+
from importlib.metadata import version as dist_version
|
|
10
11
|
from pathlib import Path
|
|
11
|
-
from typing import
|
|
12
|
+
from typing import Any
|
|
12
13
|
|
|
13
14
|
from .automation_safety_manager import AutomationSafetyManager
|
|
14
|
-
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Lazy import to avoid RuntimeWarning when running as script
|
|
18
|
+
# Use __getattr__ for Python 3.7+ lazy module imports
|
|
19
|
+
def __getattr__(name: str) -> Any:
|
|
20
|
+
"""Lazy import of JleechanorgPRMonitor to avoid frozen module warning."""
|
|
21
|
+
if name == "JleechanorgPRMonitor":
|
|
22
|
+
from .jleechanorg_pr_monitor import JleechanorgPRMonitor
|
|
23
|
+
return JleechanorgPRMonitor
|
|
24
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
25
|
+
|
|
15
26
|
from .utils import (
|
|
16
27
|
SafeJSONManager,
|
|
17
28
|
get_automation_limits,
|
|
@@ -26,7 +37,7 @@ _SECTION_RE = re.compile(r"^\s*\[[^\]]+\]\s*$")
|
|
|
26
37
|
_VERSION_RE = re.compile(r'^\s*version\s*=\s*"([^"]+)"\s*$')
|
|
27
38
|
|
|
28
39
|
|
|
29
|
-
def _version_from_pyproject(pyproject_path: Path) ->
|
|
40
|
+
def _version_from_pyproject(pyproject_path: Path) -> str | None:
|
|
30
41
|
if not pyproject_path.exists():
|
|
31
42
|
return None
|
|
32
43
|
|
|
@@ -17,7 +17,7 @@ import os
|
|
|
17
17
|
import smtplib
|
|
18
18
|
import sys
|
|
19
19
|
import threading
|
|
20
|
-
from datetime import datetime, timedelta
|
|
20
|
+
from datetime import datetime, timedelta, timezone
|
|
21
21
|
from email.mime.multipart import MIMEMultipart
|
|
22
22
|
from email.mime.text import MIMEText
|
|
23
23
|
from typing import Dict, Optional, Union
|
|
@@ -194,6 +194,29 @@ class AutomationSafetyManager:
|
|
|
194
194
|
# Sync inflight cache to prevent concurrent processing
|
|
195
195
|
self._write_json_file(self.inflight_file, self._pr_inflight_cache)
|
|
196
196
|
|
|
197
|
+
def _parse_timestamp(self, timestamp_str: str) -> datetime:
|
|
198
|
+
"""Parse ISO timestamp string to datetime object.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
datetime object in UTC, or epoch (1970-01-01) if parsing fails
|
|
202
|
+
"""
|
|
203
|
+
if not timestamp_str:
|
|
204
|
+
return datetime(1970, 1, 1, tzinfo=timezone.utc)
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
# Parse ISO format timestamp (e.g., "2026-01-18T02:33:26.798956+00:00")
|
|
208
|
+
dt = datetime.fromisoformat(timestamp_str)
|
|
209
|
+
# Ensure timezone-aware (convert to UTC if needed)
|
|
210
|
+
if dt.tzinfo is None:
|
|
211
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
212
|
+
return dt
|
|
213
|
+
except (ValueError, AttributeError, TypeError):
|
|
214
|
+
# Return epoch if parse fails (very old attempt, will be filtered out)
|
|
215
|
+
# TypeError: timestamp is not a string (e.g., int from corrupted/legacy data)
|
|
216
|
+
# ValueError: timestamp string is malformed
|
|
217
|
+
# AttributeError: timestamp_str is None or has no expected attributes
|
|
218
|
+
return datetime(1970, 1, 1, tzinfo=timezone.utc)
|
|
219
|
+
|
|
197
220
|
def _make_pr_key(
|
|
198
221
|
self,
|
|
199
222
|
pr_number: Union[int, str],
|
|
@@ -354,11 +377,12 @@ class AutomationSafetyManager:
|
|
|
354
377
|
def can_process_pr(self, pr_number: Union[int, str], repo: str = None, branch: str = None) -> bool:
|
|
355
378
|
"""Check if PR can be processed (under attempt limit).
|
|
356
379
|
|
|
357
|
-
NEW BEHAVIOR:
|
|
380
|
+
NEW BEHAVIOR: Uses rolling window (default 24 hours) instead of daily reset.
|
|
381
|
+
Attempts expire gradually as they age out of the window, not all at midnight.
|
|
358
382
|
Supports per-PR limit overrides (0 = unlimited).
|
|
359
383
|
|
|
360
384
|
Blocks if:
|
|
361
|
-
1.
|
|
385
|
+
1. Attempts in last N hours >= effective_pr_limit (respects overrides)
|
|
362
386
|
"""
|
|
363
387
|
with self.lock:
|
|
364
388
|
raw_data = self._read_json_file(self.pr_attempts_file)
|
|
@@ -370,54 +394,106 @@ class AutomationSafetyManager:
|
|
|
370
394
|
# Get effective limit (checks for per-PR override)
|
|
371
395
|
effective_limit = self._get_effective_pr_limit(pr_number, repo, branch)
|
|
372
396
|
|
|
373
|
-
#
|
|
374
|
-
|
|
397
|
+
# Filter attempts to rolling window (last N hours)
|
|
398
|
+
# Get window hours from env var or config (default 24)
|
|
399
|
+
# Use coerce_positive_int to gracefully handle invalid env var values
|
|
400
|
+
window_hours = coerce_positive_int(
|
|
401
|
+
os.environ.get("AUTOMATION_ATTEMPT_WINDOW_HOURS", "24"),
|
|
402
|
+
default=24
|
|
403
|
+
)
|
|
404
|
+
cutoff_time = datetime.now(timezone.utc) - timedelta(hours=window_hours)
|
|
405
|
+
|
|
406
|
+
window_attempts = [
|
|
407
|
+
attempt for attempt in attempts
|
|
408
|
+
if isinstance(attempt, dict) and self._parse_timestamp(attempt.get("timestamp")) >= cutoff_time
|
|
409
|
+
]
|
|
410
|
+
|
|
411
|
+
# Count attempts within rolling window
|
|
412
|
+
total_attempts = len(window_attempts)
|
|
375
413
|
|
|
376
414
|
# Check against effective limit
|
|
377
415
|
return total_attempts < effective_limit
|
|
378
416
|
|
|
379
417
|
def try_process_pr(self, pr_number: Union[int, str], repo: str = None, branch: str = None) -> bool:
|
|
380
|
-
"""Atomically reserve a processing slot for PR."""
|
|
418
|
+
"""Atomically reserve a processing slot for PR with proper cross-process locking."""
|
|
381
419
|
with self.lock:
|
|
382
420
|
# Check consecutive failure limit first
|
|
383
421
|
if not self.can_process_pr(pr_number, repo, branch):
|
|
384
422
|
return False
|
|
385
423
|
|
|
386
424
|
pr_key = self._make_pr_key(pr_number, repo, branch)
|
|
387
|
-
inflight = self._pr_inflight_cache.get(pr_key, 0)
|
|
388
425
|
|
|
389
|
-
#
|
|
390
|
-
|
|
391
|
-
|
|
426
|
+
# Use atomic_update to prevent race conditions between processes
|
|
427
|
+
# This holds the file lock across the entire read-modify-write operation
|
|
428
|
+
success = False
|
|
392
429
|
|
|
393
|
-
|
|
394
|
-
|
|
430
|
+
def reserve_slot(inflight_data: dict) -> dict:
|
|
431
|
+
nonlocal success
|
|
432
|
+
# Update in-memory cache from disk data
|
|
433
|
+
self._pr_inflight_cache = {k: int(v) for k, v in inflight_data.items()}
|
|
395
434
|
|
|
396
|
-
|
|
397
|
-
self._write_json_file(self.inflight_file, self._pr_inflight_cache)
|
|
435
|
+
inflight = self._pr_inflight_cache.get(pr_key, 0)
|
|
398
436
|
|
|
399
|
-
|
|
437
|
+
# Check if we're at the concurrent processing limit for this PR
|
|
438
|
+
if inflight >= self.pr_limit:
|
|
439
|
+
success = False
|
|
440
|
+
return inflight_data # Return unchanged
|
|
441
|
+
|
|
442
|
+
# Reserve a processing slot
|
|
443
|
+
self._pr_inflight_cache[pr_key] = inflight + 1
|
|
444
|
+
success = True
|
|
445
|
+
return self._pr_inflight_cache
|
|
446
|
+
|
|
447
|
+
json_manager.atomic_update(self.inflight_file, reserve_slot, {})
|
|
448
|
+
return success
|
|
400
449
|
|
|
401
450
|
def release_pr_slot(self, pr_number: Union[int, str], repo: str = None, branch: str = None):
|
|
402
|
-
"""Release a processing slot for PR (call in finally block)"""
|
|
451
|
+
"""Release a processing slot for PR (call in finally block) with atomic cross-process locking."""
|
|
403
452
|
with self.lock:
|
|
404
453
|
pr_key = self._make_pr_key(pr_number, repo, branch)
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
#
|
|
409
|
-
self.
|
|
454
|
+
|
|
455
|
+
# Use atomic_update to prevent race conditions between processes
|
|
456
|
+
def release_slot(inflight_data: dict) -> dict:
|
|
457
|
+
# Update in-memory cache from disk data
|
|
458
|
+
self._pr_inflight_cache = {k: int(v) for k, v in inflight_data.items()}
|
|
459
|
+
|
|
460
|
+
inflight = self._pr_inflight_cache.get(pr_key, 0)
|
|
461
|
+
if inflight > 0:
|
|
462
|
+
self._pr_inflight_cache[pr_key] = inflight - 1
|
|
463
|
+
if self._pr_inflight_cache[pr_key] == 0:
|
|
464
|
+
self._pr_inflight_cache.pop(pr_key, None)
|
|
465
|
+
|
|
466
|
+
return self._pr_inflight_cache
|
|
467
|
+
|
|
468
|
+
json_manager.atomic_update(self.inflight_file, release_slot, {})
|
|
410
469
|
|
|
411
470
|
def get_pr_attempts(self, pr_number: Union[int, str], repo: str = None, branch: str = None):
|
|
412
|
-
"""Get count of
|
|
471
|
+
"""Get count of attempts for a specific PR.
|
|
413
472
|
|
|
414
|
-
NEW BEHAVIOR:
|
|
473
|
+
NEW BEHAVIOR: Uses rolling window (consistent with can_process_pr()).
|
|
474
|
+
This ensures CLI output matches actual attempt counting logic.
|
|
415
475
|
"""
|
|
416
476
|
with self.lock:
|
|
477
|
+
raw_data = self._read_json_file(self.pr_attempts_file)
|
|
478
|
+
self._pr_attempts_cache = self._normalize_pr_attempt_keys(raw_data)
|
|
417
479
|
pr_key = self._make_pr_key(pr_number, repo, branch)
|
|
418
480
|
attempts = list(self._pr_attempts_cache.get(pr_key, []))
|
|
419
|
-
|
|
420
|
-
|
|
481
|
+
|
|
482
|
+
# Filter attempts to rolling window (last N hours)
|
|
483
|
+
# MUST match the logic in can_process_pr() to avoid misleading CLI output
|
|
484
|
+
window_hours = coerce_positive_int(
|
|
485
|
+
os.environ.get("AUTOMATION_ATTEMPT_WINDOW_HOURS", "24"),
|
|
486
|
+
default=24
|
|
487
|
+
)
|
|
488
|
+
cutoff_time = datetime.now(timezone.utc) - timedelta(hours=window_hours)
|
|
489
|
+
|
|
490
|
+
window_attempts = [
|
|
491
|
+
attempt for attempt in attempts
|
|
492
|
+
if isinstance(attempt, dict) and self._parse_timestamp(attempt.get("timestamp")) >= cutoff_time
|
|
493
|
+
]
|
|
494
|
+
|
|
495
|
+
# Return count of attempts within rolling window
|
|
496
|
+
return len(window_attempts)
|
|
421
497
|
|
|
422
498
|
def get_pr_attempt_list(self, pr_number: Union[int, str], repo: str = None, branch: str = None):
|
|
423
499
|
"""Get list of attempts for a specific PR (for detailed analysis)"""
|
|
@@ -436,7 +512,7 @@ class AutomationSafetyManager:
|
|
|
436
512
|
# Create attempt record
|
|
437
513
|
attempt_record = {
|
|
438
514
|
"result": result,
|
|
439
|
-
"timestamp": datetime.now().isoformat(),
|
|
515
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
440
516
|
"pr_number": pr_number,
|
|
441
517
|
"repo": repo,
|
|
442
518
|
"branch": branch
|
|
@@ -547,7 +623,8 @@ class AutomationSafetyManager:
|
|
|
547
623
|
return False
|
|
548
624
|
|
|
549
625
|
try:
|
|
550
|
-
|
|
626
|
+
# Use replace(tzinfo=None) for compatibility with datetime.now()
|
|
627
|
+
approval_date = REAL_DATETIME.fromisoformat(approval_date_str).replace(tzinfo=None)
|
|
551
628
|
except (TypeError, ValueError):
|
|
552
629
|
return False
|
|
553
630
|
expiry = approval_date + timedelta(hours=self.approval_hours)
|
|
@@ -39,6 +39,8 @@ FIX_COMMENT_RUN_MARKER_SUFFIX = "-->"
|
|
|
39
39
|
# Updated to match new format from build_automation_marker()
|
|
40
40
|
FIXPR_MARKER_PREFIX = "<!-- fixpr-run-automation-commit:"
|
|
41
41
|
FIXPR_MARKER_SUFFIX = "-->"
|
|
42
|
+
COMMENT_VALIDATION_MARKER_PREFIX = "<!-- comment-validation-commit:"
|
|
43
|
+
COMMENT_VALIDATION_MARKER_SUFFIX = "-->"
|
|
42
44
|
|
|
43
45
|
|
|
44
46
|
def build_automation_marker(workflow: str, agent: str, commit_sha: str) -> str:
|