Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,12 @@ sentry-cli issues list --org gitauto-ai --project agent --query "search terms"
python3 scripts/sentry/get_issue.py AGENT-20N
```

Don't pass `--max-rows`. The CLI defaults already return the full list; capping it means you miss issues that should be resolved but sit below the cap.

Then **just `Read` `/tmp/sentry_agent-20n.json`** — it's small. Don't pipe through `python -m json.tool`, `jq`, or inline `python3 -c`; those fail on quoting and waste retries.

Don't pipe `resolve_issue.py` output through `tail` — each issue prints one line and any failure gets hidden if the truncation window is wrong.

### AWS CLI

Configured for us-west-1. **Always `--start-from-head`** with `get-log-events`.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "GitAuto"
version = "1.52.0"
version = "1.55.0"
requires-python = ">=3.14"
dependencies = [
"annotated-doc==0.0.4",
Expand Down
32 changes: 32 additions & 0 deletions services/aws/disable_scheduler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# pyright: reportAssignmentType=false
# Third party imports
from botocore.exceptions import ClientError
from mypy_boto3_scheduler.type_defs import UpdateScheduleInputTypeDef

# Local imports
from services.aws.clients import scheduler_client
from utils.error.handle_exceptions import handle_exceptions
from utils.logging.logging_config import logger

# Derived from the SDK, not hardcoded — stays in sync with update_schedule's signature.
ALLOWED_UPDATE_FIELDS = frozenset(UpdateScheduleInputTypeDef.__annotations__)


@handle_exceptions(default_return_value=False, raise_on_error=False)
def disable_scheduler(schedule_name: str):
try:
current = scheduler_client.get_schedule(Name=schedule_name)
except ClientError as err:
if err.response.get("Error", {}).get("Code") == "ResourceNotFoundException":
logger.info("EventBridge Scheduler not found: %s", schedule_name)
return True
logger.error("EventBridge Scheduler get_schedule failed: %s", err)
raise

update_input: UpdateScheduleInputTypeDef = {
k: v for k, v in current.items() if k in ALLOWED_UPDATE_FIELDS
}
update_input["State"] = "DISABLED"
scheduler_client.update_schedule(**update_input)
logger.info("Disabled EventBridge Scheduler: %s", schedule_name)
return True
121 changes: 121 additions & 0 deletions services/aws/test_disable_scheduler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# pylint: disable=unused-argument
# pyright: reportUnusedVariable=false
from unittest.mock import patch

import pytest
from botocore.exceptions import ClientError

from services.aws.disable_scheduler import disable_scheduler


# Shape mirrors the real get_schedule response, including read-only metadata.
# Fields it should echo into update_schedule mirror ../website/app/actions/aws/create-or-update-schedule.ts.
GET_SCHEDULE_RESPONSE = {
"Name": "gitauto-repo-123-456",
"GroupName": "default",
"ScheduleExpression": "cron(0 15 ? * MON-FRI *)",
"Target": {
"Arn": "arn:aws:lambda:us-west-1:123:function:pr-agent-prod",
"RoleArn": "arn:aws:iam::123:role/scheduler",
"Input": '{"ownerId":123,"repoId":456,"triggerType":"schedule"}',
},
"FlexibleTimeWindow": {"Mode": "OFF"},
"State": "ENABLED",
"Description": "GitAuto scheduled trigger for repository foo/bar",
"ActionAfterCompletion": "NONE",
# Read-only metadata that get_schedule returns but update_schedule rejects:
"Arn": "arn:aws:scheduler:us-west-1:123:schedule/default/gitauto-repo-123-456",
"CreationDate": "2026-01-01T00:00:00Z",
"LastModificationDate": "2026-04-01T00:00:00Z",
"ResponseMetadata": {"RequestId": "abc", "HTTPStatusCode": 200},
}


@pytest.fixture
def mock_scheduler_client():
with patch("services.aws.disable_scheduler.scheduler_client") as mock:
yield mock


@pytest.fixture
def mock_logger():
with patch("services.aws.disable_scheduler.logger") as mock:
yield mock


def test_disable_scheduler_success(mock_scheduler_client, mock_logger):
mock_scheduler_client.get_schedule.return_value = dict(GET_SCHEDULE_RESPONSE)
mock_scheduler_client.update_schedule.return_value = {"ScheduleArn": "arn:..."}

result = disable_scheduler("gitauto-repo-123-456")

assert result is True
mock_scheduler_client.get_schedule.assert_called_once_with(
Name="gitauto-repo-123-456"
)
mock_scheduler_client.update_schedule.assert_called_once_with(
Name="gitauto-repo-123-456",
GroupName="default",
ScheduleExpression="cron(0 15 ? * MON-FRI *)",
Target=GET_SCHEDULE_RESPONSE["Target"],
FlexibleTimeWindow={"Mode": "OFF"},
State="DISABLED",
Description="GitAuto scheduled trigger for repository foo/bar",
ActionAfterCompletion="NONE",
)
mock_logger.info.assert_any_call(
"Disabled EventBridge Scheduler: %s", "gitauto-repo-123-456"
)


def test_disable_scheduler_forwards_timezone_not_in_hardcoded_set(
mock_scheduler_client, mock_logger
):
"""Whitelist comes from UpdateScheduleInputTypeDef, so optional fields the website
never sets (e.g. ScheduleExpressionTimezone) are still forwarded if present on
the schedule. Read-only metadata stays out."""
response = dict(GET_SCHEDULE_RESPONSE)
response["ScheduleExpressionTimezone"] = "America/New_York"
mock_scheduler_client.get_schedule.return_value = response

disable_scheduler("gitauto-repo-123-456")

kwargs = mock_scheduler_client.update_schedule.call_args.kwargs
assert kwargs == {
"Name": "gitauto-repo-123-456",
"GroupName": "default",
"ScheduleExpression": "cron(0 15 ? * MON-FRI *)",
"Target": GET_SCHEDULE_RESPONSE["Target"],
"FlexibleTimeWindow": {"Mode": "OFF"},
"State": "DISABLED",
"Description": "GitAuto scheduled trigger for repository foo/bar",
"ActionAfterCompletion": "NONE",
"ScheduleExpressionTimezone": "America/New_York",
}


def test_disable_scheduler_not_found(mock_scheduler_client, mock_logger):
mock_scheduler_client.get_schedule.side_effect = ClientError(
{"Error": {"Code": "ResourceNotFoundException", "Message": "not found"}},
"GetSchedule",
)

result = disable_scheduler("missing-schedule")

assert result is True
mock_scheduler_client.update_schedule.assert_not_called()
mock_logger.info.assert_called_once_with(
"EventBridge Scheduler not found: %s", "missing-schedule"
)


def test_disable_scheduler_access_denied(mock_scheduler_client, mock_logger):
mock_scheduler_client.get_schedule.side_effect = ClientError(
{"Error": {"Code": "AccessDeniedException", "Message": "denied"}},
"GetSchedule",
)

result = disable_scheduler("restricted-schedule")

assert result is False
mock_scheduler_client.update_schedule.assert_not_called()
Loading