diff --git a/ai-failure-notifier/src/charm_tech_code/ai_failure_notifier/_apply.py b/ai-failure-notifier/src/charm_tech_code/ai_failure_notifier/_apply.py index d30fbf5..a4b60c2 100644 --- a/ai-failure-notifier/src/charm_tech_code/ai_failure_notifier/_apply.py +++ b/ai-failure-notifier/src/charm_tech_code/ai_failure_notifier/_apply.py @@ -23,7 +23,7 @@ def plain_fallback_body(workflow_name: str, run_url: str) -> str: - """The plain, generic body text used whenever enrichment is unavailable.""" + """Render the generic body used whenever enrichment is unavailable.""" return f"Scheduled workflow '{workflow_name}' failed: {run_url}" diff --git a/ai-failure-notifier/src/charm_tech_code/ai_failure_notifier/_github.py b/ai-failure-notifier/src/charm_tech_code/ai_failure_notifier/_github.py index 16d0922..2efba8e 100644 --- a/ai-failure-notifier/src/charm_tech_code/ai_failure_notifier/_github.py +++ b/ai-failure-notifier/src/charm_tech_code/ai_failure_notifier/_github.py @@ -18,7 +18,7 @@ from __future__ import annotations import json -import subprocess +import subprocess # ruff: ignore[suspicious-subprocess-import] -- see `gh` below from typing import Any from . import _summary @@ -28,8 +28,14 @@ def gh(*args: str, check: bool = True) -> subprocess.CompletedProcess: """Run a `gh` subcommand, returning the completed process.""" - # S607: `gh` is deliberately called by name, resolved from the runner's PATH. - return subprocess.run(['gh', *args], text=True, capture_output=True, check=check) # noqa: S607 + # The argv is a list this module builds, and `gh` is deliberately called by + # name so the runner's PATH resolves it. + return subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true] + ['gh', *args], # ruff: ignore[start-process-with-partial-path] + text=True, + capture_output=True, + check=check, + ) def gh_json(*args: str) -> Any: @@ -111,8 +117,7 @@ def fetch_issue_texts(repo: str, number: int) -> list[str]: """Fetch an issue's body plus all comment bodies, for marker scanning.""" data = gh_json('issue', 'view', str(number), '--repo', repo, '--json', 'body,comments') or {} texts = [data.get('body') or ''] - for c in data.get('comments') or []: - texts.append(c.get('body') or '') + texts.extend(comment.get('body') or '' for comment in data.get('comments') or []) return texts @@ -138,7 +143,7 @@ def resolve_origin( issue keeps that lookup to reading the one issue we were handed. """ texts = [(notify_issue, text) for text in fetch_issue_texts(repo, notify_issue)] - enriched_issue, origin_kind, origin_issue = find_run_markers(texts, run_id) + enriched_issue, origin_kind, _ = find_run_markers(texts, run_id) # The passed-in values win: a marker we failed to find on the issue does # not make the issue the wrong one. return enriched_issue, notify_origin or origin_kind, notify_issue diff --git a/ai-failure-notifier/src/charm_tech_code/ai_failure_notifier/_models.py b/ai-failure-notifier/src/charm_tech_code/ai_failure_notifier/_models.py index 56f68c2..0897332 100644 --- a/ai-failure-notifier/src/charm_tech_code/ai_failure_notifier/_models.py +++ b/ai-failure-notifier/src/charm_tech_code/ai_failure_notifier/_models.py @@ -93,6 +93,6 @@ def from_gh(cls, data: dict[str, Any]) -> CandidateIssue: ) def excerpt(self) -> str: - """The first line of the body, bounded, for the candidate block.""" + """Return the first line of the body, bounded, for the candidate block.""" lines = (self.body or '').strip().splitlines() return lines[0][:300] if lines else '(no body)' diff --git a/ai-failure-notifier/src/charm_tech_code/ai_failure_notifier/_openrouter.py b/ai-failure-notifier/src/charm_tech_code/ai_failure_notifier/_openrouter.py index 9d57e5e..f56e181 100644 --- a/ai-failure-notifier/src/charm_tech_code/ai_failure_notifier/_openrouter.py +++ b/ai-failure-notifier/src/charm_tech_code/ai_failure_notifier/_openrouter.py @@ -55,8 +55,8 @@ def call_openrouter( headers={'Authorization': f'Bearer {api_key}', 'Content-Type': 'application/json'}, method='POST', ) - # S310: the URL is a literal https endpoint, not caller-controlled. - with urllib.request.urlopen(request, timeout=60) as response: # noqa: S310 + # The URL is a literal https endpoint, not caller-controlled. + with urllib.request.urlopen(request, timeout=60) as response: # ruff: ignore[suspicious-url-open-usage] body = json.loads(response.read().decode()) content = body['choices'][0]['message']['content'] return json.loads(content) diff --git a/ai-failure-notifier/tests/conftest.py b/ai-failure-notifier/tests/conftest.py index 8bc6488..a0ff215 100644 --- a/ai-failure-notifier/tests/conftest.py +++ b/ai-failure-notifier/tests/conftest.py @@ -24,7 +24,7 @@ from __future__ import annotations -import subprocess +import subprocess # ruff: ignore[suspicious-subprocess-import] -- see `_no_subprocess` import urllib.request import pytest diff --git a/ai-failure-notifier/tests/test_ai_failure_notifier.py b/ai-failure-notifier/tests/test_ai_failure_notifier.py index 885be20..6364801 100644 --- a/ai-failure-notifier/tests/test_ai_failure_notifier.py +++ b/ai-failure-notifier/tests/test_ai_failure_notifier.py @@ -1144,9 +1144,11 @@ def test_http_error_propagates_so_main_can_fall_back(self): io.BytesIO(b''), ) self.addCleanup(error.close) - with mock.patch.object(_openrouter.urllib.request, 'urlopen', side_effect=error): - with self.assertRaises(urllib.error.HTTPError): - _openrouter.call_openrouter('sys', 'user', 'm', 'k') + with ( + mock.patch.object(_openrouter.urllib.request, 'urlopen', side_effect=error), + self.assertRaises(urllib.error.HTTPError), + ): + _openrouter.call_openrouter('sys', 'user', 'm', 'k') class ResolveOriginTests(unittest.TestCase):