From 6a75b589ad89c631559fee911e949b3a23dbe63b Mon Sep 17 00:00:00 2001 From: Richard Gibert Date: Tue, 9 Jun 2026 11:51:16 -0400 Subject: [PATCH 1/8] feat(linear): Add parent status comment config fields Add two Jinja2 template config fields to LinearConfig for posting comments when a parent Linear issue's workflow state changes: - parent_status_comment_completed: used when all action items are done - parent_status_comment_started: used when an incident is in progress Expose both via the LINEAR settings dict following the same pattern as the existing action item nag comment fields. Co-Authored-By: Claude Agent transcript: https://claudescope.sentry.dev/share/cr0BpGpZtcU2cpuA25FIpW2OdcXVBuVd_TD8I4UNi-8 --- src/firetower/config.py | 11 +++++++++++ src/firetower/settings.py | 2 ++ 2 files changed, 13 insertions(+) diff --git a/src/firetower/config.py b/src/firetower/config.py index cfcbc543..3a6cdbb9 100644 --- a/src/firetower/config.py +++ b/src/firetower/config.py @@ -93,6 +93,17 @@ class LinearConfig: "days from incident creation. Please prioritize this work or close " "out the issue if it is no longer relevant." ) + parent_status_comment_completed: str = ( + "Firetower set this issue to **Completed**. " + "Incident {{ incident.incident_number }} is {{ incident.status }} " + "and {% if total_action_items == 0 %}there are no action items." + "{% else %}all {{ total_action_items }} action item(s) are complete.{% endif %}" + ) + parent_status_comment_started: str = ( + "Firetower set this issue to **Started**. " + "Incident {{ incident.incident_number }} is {{ incident.status }}. " + "{{ completed_action_items }} of {{ total_action_items }} action item(s) complete." + ) @deserialize diff --git a/src/firetower/settings.py b/src/firetower/settings.py index 11fadea0..9cc6da37 100644 --- a/src/firetower/settings.py +++ b/src/firetower/settings.py @@ -327,6 +327,8 @@ class StatuspageSettings(TypedDict): "ACTION_ITEM_SLO_DAYS_MEDIUM_PRIORITY": config.linear.action_item_slo_days_medium_priority, "ACTION_ITEM_NAG_COMMENT_HIGH_PRIORITY": config.linear.action_item_nag_comment_high_priority, "ACTION_ITEM_NAG_COMMENT_MEDIUM_PRIORITY": config.linear.action_item_nag_comment_medium_priority, + "PARENT_STATUS_COMMENT_COMPLETED": config.linear.parent_status_comment_completed, + "PARENT_STATUS_COMMENT_STARTED": config.linear.parent_status_comment_started, } if config.linear else None From 84c87bacb7d6a2ca2792a268c3faeb5eea07d7ff Mon Sep 17 00:00:00 2001 From: Richard Gibert Date: Tue, 9 Jun 2026 11:54:52 -0400 Subject: [PATCH 2/8] test(linear): Add tests for parent status comment posting Cover the new _comment_parent_issue_status_change() helper with nine tests: completed/started templates, canceled-counts-as-done, empty and whitespace-only templates skip commenting, zero action items, API failure resilience, and template render errors. Co-Authored-By: Claude Agent transcript: https://claudescope.sentry.dev/share/dLHchExcBC82al4op1_w0nFAoNixM6ORNUJj5-n2ExM --- .../incidents/tests/test_services.py | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) diff --git a/src/firetower/incidents/tests/test_services.py b/src/firetower/incidents/tests/test_services.py index f9c64d7b..c7038d8c 100644 --- a/src/firetower/incidents/tests/test_services.py +++ b/src/firetower/incidents/tests/test_services.py @@ -16,6 +16,7 @@ IncidentStatus, ) from firetower.incidents.services import ( + _comment_parent_issue_status_change, _update_parent_issue_status, sync_incident_participants_from_slack, ) @@ -531,3 +532,126 @@ def test_mitigated_incident_all_items_done_sets_started(self): _update_parent_issue_status(incident, svc) svc.update_issue.assert_called_once_with("lin-123", state_id="state-started") + + +@pytest.mark.django_db +class TestCommentParentIssueStatusChange: + def _make_incident(self, status=IncidentStatus.ACTIVE): + return Incident.objects.create( + title="Test Incident", + status=status, + severity=IncidentSeverity.P1, + linear_parent_issue_id="lin-123", + ) + + @pytest.fixture(autouse=True) + def _linear_settings(self, settings): + settings.LINEAR = { + "TEAM_ID": "team-1", + "API_KEY": "key", + "PARENT_STATUS_COMMENT_COMPLETED": ( + "Set to Completed. " + "Incident {{ incident.incident_number }} is {{ incident.status }}. " + "{{ completed_action_items }}/{{ total_action_items }} done." + ), + "PARENT_STATUS_COMMENT_STARTED": ( + "Set to Started. " + "Incident {{ incident.incident_number }} is {{ incident.status }}. " + "{{ completed_action_items }}/{{ total_action_items }} done." + ), + } + + def test_posts_completed_comment(self): + incident = self._make_incident(status=IncidentStatus.DONE) + svc = MagicMock() + + _comment_parent_issue_status_change( + incident, svc, "completed", ["Done", "Done"] + ) + + svc.create_comment.assert_called_once_with( + "lin-123", + f"Set to Completed. Incident {incident.incident_number} is Done. 2/2 done.", + ) + + def test_posts_started_comment_with_mixed_statuses(self): + incident = self._make_incident(status=IncidentStatus.ACTIVE) + svc = MagicMock() + + _comment_parent_issue_status_change( + incident, svc, "started", ["Done", "In Progress", "Todo"] + ) + + svc.create_comment.assert_called_once_with( + "lin-123", + f"Set to Started. Incident {incident.incident_number} is Active. 1/3 done.", + ) + + def test_counts_canceled_as_completed(self): + incident = self._make_incident(status=IncidentStatus.DONE) + svc = MagicMock() + + _comment_parent_issue_status_change( + incident, svc, "completed", ["Done", "Canceled"] + ) + + svc.create_comment.assert_called_once_with( + "lin-123", + f"Set to Completed. Incident {incident.incident_number} is Done. 2/2 done.", + ) + + def test_empty_template_skips_comment(self, settings): + settings.LINEAR["PARENT_STATUS_COMMENT_COMPLETED"] = "" + incident = self._make_incident(status=IncidentStatus.DONE) + svc = MagicMock() + + _comment_parent_issue_status_change(incident, svc, "completed", ["Done"]) + + svc.create_comment.assert_not_called() + + def test_whitespace_only_template_skips_comment(self, settings): + settings.LINEAR["PARENT_STATUS_COMMENT_STARTED"] = " " + incident = self._make_incident(status=IncidentStatus.ACTIVE) + svc = MagicMock() + + _comment_parent_issue_status_change(incident, svc, "started", ["Todo"]) + + svc.create_comment.assert_not_called() + + def test_no_action_items(self): + incident = self._make_incident(status=IncidentStatus.DONE) + svc = MagicMock() + + _comment_parent_issue_status_change(incident, svc, "completed", []) + + svc.create_comment.assert_called_once_with( + "lin-123", + f"Set to Completed. Incident {incident.incident_number} is Done. 0/0 done.", + ) + + def test_create_comment_failure_logs_and_continues(self): + incident = self._make_incident(status=IncidentStatus.DONE) + svc = MagicMock() + svc.create_comment.return_value = False + + _comment_parent_issue_status_change(incident, svc, "completed", ["Done"]) + + svc.create_comment.assert_called_once() + + def test_create_comment_exception_logs_and_continues(self): + incident = self._make_incident(status=IncidentStatus.DONE) + svc = MagicMock() + svc.create_comment.side_effect = Exception("API error") + + _comment_parent_issue_status_change(incident, svc, "completed", ["Done"]) + + svc.create_comment.assert_called_once() + + def test_template_render_error_logs_and_continues(self, settings): + settings.LINEAR["PARENT_STATUS_COMMENT_COMPLETED"] = "{{ unterminated" + incident = self._make_incident(status=IncidentStatus.DONE) + svc = MagicMock() + + _comment_parent_issue_status_change(incident, svc, "completed", ["Done"]) + + svc.create_comment.assert_not_called() From 59bf766ce2d6d6d2ba9d51c1cb29a955be0fe0f1 Mon Sep 17 00:00:00 2001 From: Richard Gibert Date: Tue, 9 Jun 2026 11:54:58 -0400 Subject: [PATCH 3/8] feat(linear): Post comment when parent issue state changes Add _comment_parent_issue_status_change() which renders a Jinja2 template from LINEAR settings and posts a comment on the parent issue after its workflow state is updated. Wire it into _update_parent_issue_status() so the comment is posted only when the state update succeeds. Co-Authored-By: Claude Agent transcript: https://claudescope.sentry.dev/share/tCBtmLfNirISE8EeLk3zwRkb-gNFdxVpq6Lx2KnsjsY --- src/firetower/incidents/services.py | 53 +++++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/src/firetower/incidents/services.py b/src/firetower/incidents/services.py index 09090e66..ab7599a7 100644 --- a/src/firetower/incidents/services.py +++ b/src/firetower/incidents/services.py @@ -6,6 +6,7 @@ from django.contrib.auth.models import User from django.db import transaction from django.utils import timezone +from jinja2 import Environment, TemplateError from firetower.auth.models import ExternalProfile, ExternalProfileType from firetower.auth.services import ( @@ -185,6 +186,50 @@ def _resolve_assignees( COMPLETED_STATUSES = {ActionItemStatus.DONE, ActionItemStatus.CANCELED} +_PARENT_STATUS_TEMPLATE_ENV = Environment(autoescape=False) + + +def _comment_parent_issue_status_change( + incident: Incident, + linear_service: LinearService, + target_state: str, + statuses: list[str], +) -> None: + if not settings.LINEAR or not incident.linear_parent_issue_id: + return + + template_key = ( + "PARENT_STATUS_COMMENT_COMPLETED" + if target_state == "completed" + else "PARENT_STATUS_COMMENT_STARTED" + ) + template_source = settings.LINEAR.get(template_key, "") + if not template_source or not template_source.strip(): + return + + completed_action_items = sum( + 1 for s in statuses if s in {ActionItemStatus.DONE, ActionItemStatus.CANCELED} + ) + try: + comment = _PARENT_STATUS_TEMPLATE_ENV.from_string(template_source).render( + incident=incident, + total_action_items=len(statuses), + completed_action_items=completed_action_items, + target_state=target_state, + ) + except TemplateError: + logger.exception( + f"Failed to render parent status comment template for incident {incident.id}" + ) + return + + try: + linear_service.create_comment(incident.linear_parent_issue_id, comment) + except Exception: + logger.exception( + f"Failed to post parent status comment for incident {incident.id}" + ) + def _update_parent_issue_status( incident: Incident, linear_service: LinearService @@ -207,8 +252,12 @@ def _update_parent_issue_status( target_state = "completed" if all_complete else "started" state_id = states.get(target_state) - if state_id: - linear_service.update_issue(incident.linear_parent_issue_id, state_id=state_id) + if state_id and linear_service.update_issue( + incident.linear_parent_issue_id, state_id=state_id + ): + _comment_parent_issue_status_change( + incident, linear_service, target_state, statuses + ) def _sync_parent_assignee(incident: Incident, linear_service: LinearService) -> None: From 186a5a5b880081b1e8c68f27c1085fbfb02c3b72 Mon Sep 17 00:00:00 2001 From: Richard Gibert Date: Tue, 9 Jun 2026 12:01:29 -0400 Subject: [PATCH 4/8] test(linear): update parent status tests for comment posting Co-Authored-By: Claude Opus 4.6 Agent transcript: https://claudescope.sentry.dev/share/10uEt-yMHSBMpV2hgTO9Ii9B2zOOgLY1AawJE56YeLQ --- .../incidents/tests/test_services.py | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/firetower/incidents/tests/test_services.py b/src/firetower/incidents/tests/test_services.py index c7038d8c..b5df99b3 100644 --- a/src/firetower/incidents/tests/test_services.py +++ b/src/firetower/incidents/tests/test_services.py @@ -431,11 +431,17 @@ def _make_linear_service(self): "started": "state-started", "completed": "state-completed", } + svc.update_issue.return_value = True return svc @pytest.fixture(autouse=True) def _linear_settings(self, settings): - settings.LINEAR = {"TEAM_ID": "team-1", "API_KEY": "key"} + settings.LINEAR = { + "TEAM_ID": "team-1", + "API_KEY": "key", + "PARENT_STATUS_COMMENT_COMPLETED": "completed comment", + "PARENT_STATUS_COMMENT_STARTED": "started comment", + } def test_active_incident_no_action_items_sets_started(self): incident = self._make_incident(status=IncidentStatus.ACTIVE) @@ -444,6 +450,7 @@ def test_active_incident_no_action_items_sets_started(self): _update_parent_issue_status(incident, svc) svc.update_issue.assert_called_once_with("lin-123", state_id="state-started") + svc.create_comment.assert_called_once() def test_active_incident_all_items_done_sets_started(self): incident = self._make_incident(status=IncidentStatus.ACTIVE) @@ -460,6 +467,7 @@ def test_active_incident_all_items_done_sets_started(self): _update_parent_issue_status(incident, svc) svc.update_issue.assert_called_once_with("lin-123", state_id="state-started") + svc.create_comment.assert_called_once() def test_done_incident_no_action_items_sets_completed(self): incident = self._make_incident(status=IncidentStatus.DONE) @@ -468,6 +476,7 @@ def test_done_incident_no_action_items_sets_completed(self): _update_parent_issue_status(incident, svc) svc.update_issue.assert_called_once_with("lin-123", state_id="state-completed") + svc.create_comment.assert_called_once() def test_done_incident_all_items_done_sets_completed(self): incident = self._make_incident(status=IncidentStatus.DONE) @@ -492,6 +501,7 @@ def test_done_incident_all_items_done_sets_completed(self): _update_parent_issue_status(incident, svc) svc.update_issue.assert_called_once_with("lin-123", state_id="state-completed") + svc.create_comment.assert_called_once() def test_done_incident_incomplete_items_sets_started(self): incident = self._make_incident(status=IncidentStatus.DONE) @@ -516,6 +526,7 @@ def test_done_incident_incomplete_items_sets_started(self): _update_parent_issue_status(incident, svc) svc.update_issue.assert_called_once_with("lin-123", state_id="state-started") + svc.create_comment.assert_called_once() def test_mitigated_incident_all_items_done_sets_started(self): incident = self._make_incident(status=IncidentStatus.MITIGATED) @@ -532,6 +543,17 @@ def test_mitigated_incident_all_items_done_sets_started(self): _update_parent_issue_status(incident, svc) svc.update_issue.assert_called_once_with("lin-123", state_id="state-started") + svc.create_comment.assert_called_once() + + def test_update_issue_failure_skips_comment(self): + incident = self._make_incident(status=IncidentStatus.DONE) + svc = self._make_linear_service() + svc.update_issue.return_value = False + + _update_parent_issue_status(incident, svc) + + svc.update_issue.assert_called_once_with("lin-123", state_id="state-completed") + svc.create_comment.assert_not_called() @pytest.mark.django_db From cf6986d2777bab715dd26a88ee93f0029332149c Mon Sep 17 00:00:00 2001 From: Richard Gibert Date: Tue, 9 Jun 2026 13:40:03 -0400 Subject: [PATCH 5/8] feat(linear): Include state_type in get_issue() return dict The GraphQL query already fetches state { type } via ISSUE_FIELDS, but the return dict was discarding it. Expose it so callers can check the current state type before making redundant updates. Co-Authored-By: Claude Agent transcript: https://claudescope.sentry.dev/share/zE0rzhPkht-CdM7066fpTcSjse8eht6ysIVitFlRsx0 --- src/firetower/integrations/services/linear.py | 1 + .../integrations/tests/test_linear_service.py | 59 +++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/src/firetower/integrations/services/linear.py b/src/firetower/integrations/services/linear.py index e6d82d5c..eaa55d44 100644 --- a/src/firetower/integrations/services/linear.py +++ b/src/firetower/integrations/services/linear.py @@ -201,6 +201,7 @@ def get_issue(self, issue_id: str) -> dict[str, Any] | None: "identifier": issue["identifier"], "title": issue["title"], "url": issue["url"], + "state_type": (issue.get("state") or {}).get("type", ""), } def get_user_by_email(self, email: str) -> dict[str, str] | None: diff --git a/src/firetower/integrations/tests/test_linear_service.py b/src/firetower/integrations/tests/test_linear_service.py index abec990e..c881e1e9 100644 --- a/src/firetower/integrations/tests/test_linear_service.py +++ b/src/firetower/integrations/tests/test_linear_service.py @@ -436,3 +436,62 @@ def test_returns_none_on_api_failure(self, linear_service): result = linear_service.get_user_by_email("alice@example.com") assert result is None + + +class TestGetIssue: + def test_returns_issue_with_state_type(self, linear_service): + mock_response = { + "issue": { + "id": "issue-123", + "identifier": "LIN-42", + "title": "Fix the thing", + "url": "https://linear.app/team/issue/LIN-42", + "priority": 1, + "state": {"type": "started"}, + "assignee": {"id": "user-1", "email": "alice@example.com"}, + } + } + + with patch.object(linear_service, "_graphql", return_value=mock_response): + result = linear_service.get_issue("issue-123") + + assert result == { + "id": "issue-123", + "identifier": "LIN-42", + "title": "Fix the thing", + "url": "https://linear.app/team/issue/LIN-42", + "state_type": "started", + } + + def test_returns_empty_state_type_when_state_is_null(self, linear_service): + mock_response = { + "issue": { + "id": "issue-123", + "identifier": "LIN-42", + "title": "Fix the thing", + "url": "https://linear.app/team/issue/LIN-42", + "priority": 1, + "state": None, + "assignee": None, + } + } + + with patch.object(linear_service, "_graphql", return_value=mock_response): + result = linear_service.get_issue("issue-123") + + assert result is not None + assert result["state_type"] == "" + + def test_returns_none_on_api_failure(self, linear_service): + with patch.object(linear_service, "_graphql", return_value=None): + result = linear_service.get_issue("issue-123") + + assert result is None + + def test_returns_none_when_issue_not_found(self, linear_service): + mock_response = {"issue": None} + + with patch.object(linear_service, "_graphql", return_value=mock_response): + result = linear_service.get_issue("nonexistent") + + assert result is None From d90c348797148fbd2895225236878b37e335c61d Mon Sep 17 00:00:00 2001 From: Richard Gibert Date: Tue, 9 Jun 2026 13:44:15 -0400 Subject: [PATCH 6/8] fix(linear): Skip redundant parent issue state updates and comments Fetch the parent issue's current state before updating. If it already matches the target state, skip the update_issue call and comment post. This prevents spamming the parent issue with duplicate "Firetower set this issue to..." comments on every action-item sync. If get_issue fails (returns None), fall through to the existing update path to preserve reliability. Co-Authored-By: Claude Agent transcript: https://claudescope.sentry.dev/share/HpgRbZkX1TIMFHKpVSuXTOf1DJ0QUIcIKlakW4ixWpM --- src/firetower/incidents/services.py | 5 +++ .../incidents/tests/test_services.py | 31 ++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/firetower/incidents/services.py b/src/firetower/incidents/services.py index ab7599a7..f2a43885 100644 --- a/src/firetower/incidents/services.py +++ b/src/firetower/incidents/services.py @@ -251,6 +251,11 @@ def _update_parent_issue_status( return target_state = "completed" if all_complete else "started" + + parent_issue = linear_service.get_issue(incident.linear_parent_issue_id) + if parent_issue and parent_issue.get("state_type") == target_state: + return + state_id = states.get(target_state) if state_id and linear_service.update_issue( incident.linear_parent_issue_id, state_id=state_id diff --git a/src/firetower/incidents/tests/test_services.py b/src/firetower/incidents/tests/test_services.py index b5df99b3..a2b6c77d 100644 --- a/src/firetower/incidents/tests/test_services.py +++ b/src/firetower/incidents/tests/test_services.py @@ -425,13 +425,14 @@ def _make_incident(self, status=IncidentStatus.ACTIVE): linear_parent_issue_id="lin-123", ) - def _make_linear_service(self): + def _make_linear_service(self, current_state_type="unstarted"): svc = MagicMock() svc.get_workflow_states.return_value = { "started": "state-started", "completed": "state-completed", } svc.update_issue.return_value = True + svc.get_issue.return_value = {"state_type": current_state_type} return svc @pytest.fixture(autouse=True) @@ -555,6 +556,34 @@ def test_update_issue_failure_skips_comment(self): svc.update_issue.assert_called_once_with("lin-123", state_id="state-completed") svc.create_comment.assert_not_called() + def test_skips_update_when_already_in_target_state(self): + incident = self._make_incident(status=IncidentStatus.ACTIVE) + svc = self._make_linear_service(current_state_type="started") + + _update_parent_issue_status(incident, svc) + + svc.update_issue.assert_not_called() + svc.create_comment.assert_not_called() + + def test_updates_when_in_different_state(self): + incident = self._make_incident(status=IncidentStatus.DONE) + svc = self._make_linear_service(current_state_type="started") + + _update_parent_issue_status(incident, svc) + + svc.update_issue.assert_called_once_with("lin-123", state_id="state-completed") + svc.create_comment.assert_called_once() + + def test_updates_when_get_issue_fails(self): + incident = self._make_incident(status=IncidentStatus.ACTIVE) + svc = self._make_linear_service() + svc.get_issue.return_value = None + + _update_parent_issue_status(incident, svc) + + svc.update_issue.assert_called_once_with("lin-123", state_id="state-started") + svc.create_comment.assert_called_once() + @pytest.mark.django_db class TestCommentParentIssueStatusChange: From a47afe1ed2b27900ea169dc9573885636b843f42 Mon Sep 17 00:00:00 2001 From: Richard Gibert Date: Thu, 11 Jun 2026 14:59:41 -0400 Subject: [PATCH 7/8] fix(linear): Skip parent status update when get_issue fails When get_issue returns None, the code cannot determine the current state of the parent issue. Previously it would proceed with the update and post a comment describing a transition that may not have happened. Now it returns early, consistent with the existing guard for matching state. Co-Authored-By: Claude Agent transcript: https://claudescope.sentry.dev/share/XGpB-bGfRojw3T90v9RFRnEDBL1cZwaTCmkwqsjrPA0 --- src/firetower/incidents/services.py | 2 +- src/firetower/incidents/tests/test_services.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/firetower/incidents/services.py b/src/firetower/incidents/services.py index 04b75601..16cc0103 100644 --- a/src/firetower/incidents/services.py +++ b/src/firetower/incidents/services.py @@ -253,7 +253,7 @@ def _update_parent_issue_status( target_state = "completed" if all_complete else "started" parent_issue = linear_service.get_issue(incident.linear_parent_issue_id) - if parent_issue and parent_issue.get("state_type") == target_state: + if not parent_issue or parent_issue.get("state_type") == target_state: return state_id = states.get(target_state) diff --git a/src/firetower/incidents/tests/test_services.py b/src/firetower/incidents/tests/test_services.py index a2b6c77d..97c70042 100644 --- a/src/firetower/incidents/tests/test_services.py +++ b/src/firetower/incidents/tests/test_services.py @@ -574,15 +574,15 @@ def test_updates_when_in_different_state(self): svc.update_issue.assert_called_once_with("lin-123", state_id="state-completed") svc.create_comment.assert_called_once() - def test_updates_when_get_issue_fails(self): + def test_skips_update_when_get_issue_fails(self): incident = self._make_incident(status=IncidentStatus.ACTIVE) svc = self._make_linear_service() svc.get_issue.return_value = None _update_parent_issue_status(incident, svc) - svc.update_issue.assert_called_once_with("lin-123", state_id="state-started") - svc.create_comment.assert_called_once() + svc.update_issue.assert_not_called() + svc.create_comment.assert_not_called() @pytest.mark.django_db From 0af5af7db2f8a6c62f23fce2b10a9ef0095a0267 Mon Sep 17 00:00:00 2001 From: Richard Gibert Date: Fri, 12 Jun 2026 12:11:00 -0400 Subject: [PATCH 8/8] fix(linear): Use proper pluralization in comment templates Replace lazy "(s)" suffixes with Jinja conditionals that correctly pluralize "day" and "action item" based on actual count. Co-Authored-By: Claude Agent transcript: https://claudescope.sentry.dev/share/6LrCPJ7RAsrvngoaj47g2KedAPHpDx4Q_Tp_5v02Uq0 --- src/firetower/config.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/firetower/config.py b/src/firetower/config.py index 3a6cdbb9..59280e55 100644 --- a/src/firetower/config.py +++ b/src/firetower/config.py @@ -80,15 +80,15 @@ class LinearConfig: action_item_slo_days_high_priority: int = 14 action_item_slo_days_medium_priority: int = 30 action_item_nag_comment_high_priority: str = ( - "{% if days_past_due > 0 %}This action item is **{{ days_past_due }} day(s) " - "past due**. {% endif %}" + "{% if days_past_due > 0 %}This action item is **{{ days_past_due }} " + "day{% if days_past_due != 1 %}s{% endif %} past due**. {% endif %}" "The SLO for completing P0/P1 incident action items is {{ slo_days }} " "days from incident creation. Please prioritize this work or close " "out the issue if it is no longer relevant." ) action_item_nag_comment_medium_priority: str = ( - "{% if days_past_due > 0 %}This action item is **{{ days_past_due }} day(s) " - "past due**. {% endif %}" + "{% if days_past_due > 0 %}This action item is **{{ days_past_due }} " + "day{% if days_past_due != 1 %}s{% endif %} past due**. {% endif %}" "The SLO for completing P2 incident action items is {{ slo_days }} " "days from incident creation. Please prioritize this work or close " "out the issue if it is no longer relevant." @@ -97,12 +97,14 @@ class LinearConfig: "Firetower set this issue to **Completed**. " "Incident {{ incident.incident_number }} is {{ incident.status }} " "and {% if total_action_items == 0 %}there are no action items." - "{% else %}all {{ total_action_items }} action item(s) are complete.{% endif %}" + "{% else %}all {{ total_action_items }} action " + "item{% if total_action_items != 1 %}s{% endif %} are complete.{% endif %}" ) parent_status_comment_started: str = ( "Firetower set this issue to **Started**. " "Incident {{ incident.incident_number }} is {{ incident.status }}. " - "{{ completed_action_items }} of {{ total_action_items }} action item(s) complete." + "{{ completed_action_items }} of {{ total_action_items }} action " + "item{% if total_action_items != 1 %}s{% endif %} complete." )