Skip to content

Commit bf67b1d

Browse files
authored
fix: Create GitHub comment as table (#3948)
1 parent 06eb8a4 commit bf67b1d

File tree

7 files changed

+117
-43
lines changed

7 files changed

+117
-43
lines changed

api/conftest.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,29 @@ def segment_featurestate(feature_segment, feature, environment):
553553
)
554554

555555

556+
@pytest.fixture()
557+
def feature_with_value_segment(
558+
feature_with_value: Feature, segment: Segment, environment: Environment
559+
) -> FeatureSegment:
560+
return FeatureSegment.objects.create(
561+
feature=feature_with_value, segment=segment, environment=environment
562+
)
563+
564+
565+
@pytest.fixture()
566+
def segment_featurestate_and_feature_with_value(
567+
feature_with_value_segment: FeatureSegment,
568+
feature_with_value: Feature,
569+
environment: Environment,
570+
) -> FeatureState:
571+
return FeatureState.objects.create(
572+
feature_segment=feature_with_value_segment,
573+
feature=feature_with_value,
574+
environment=environment,
575+
updated_at="2024-01-01 00:00:00",
576+
)
577+
578+
556579
@pytest.fixture()
557580
def environment_api_key(environment):
558581
return EnvironmentAPIKey.objects.create(

api/features/feature_external_resources/models.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,12 @@ def execute_after_save_actions(self):
6161
feature_id=self.feature_id, identity_id__isnull=True
6262
)
6363
feature_data: GithubData = generate_data(
64-
github_configuration,
65-
self.feature_id,
66-
self.feature.name,
67-
WebhookEventType.FEATURE_EXTERNAL_RESOURCE_ADDED.value,
68-
feature_states,
64+
github_configuration=github_configuration,
65+
feature_id=self.feature_id,
66+
feature_name=self.feature.name,
67+
type=WebhookEventType.FEATURE_EXTERNAL_RESOURCE_ADDED.value,
68+
feature_states=feature_states,
69+
project_id=self.feature.project_id,
6970
)
7071

7172
call_github_app_webhook_for_feature_state.delay(

api/features/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1014,7 +1014,6 @@ def create_github_comment(self) -> None:
10141014

10151015
if (
10161016
not self.identity_id
1017-
and not self.feature_segment
10181017
and self.feature.external_resources.exists()
10191018
and self.environment.project.github_project.exists()
10201019
and self.environment.project.organisation.github_config.exists()
@@ -1032,6 +1031,7 @@ def create_github_comment(self) -> None:
10321031
feature_name=self.feature.name,
10331032
type=WebhookEventType.FLAG_UPDATED.value,
10341033
feature_states=feature_states,
1034+
project_id=self.environment.project_id,
10351035
)
10361036

10371037
if self.deleted_at is not None:

api/integrations/github/constants.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
GITHUB_API_URL = "https://api.github.com/"
22
GITHUB_API_VERSION = "2022-11-28"
33

4-
LINK_FEATURE_TEXT = "### This pull request is linked to a Flagsmith Feature (`%s`):\n"
4+
LINK_FEATURE_TITLE = """**Flagsmith feature linked:** `%s`
5+
Default Values:\n"""
6+
FEATURE_TABLE_HEADER = """| Environment | Enabled | Value | Last Updated (UTC) |
7+
| :--- | :----- | :------ | :------ |\n"""
8+
FEATURE_TABLE_ROW = "| [%s](%s) | %s | %s | %s |\n"
9+
LINK_SEGMENT_TITLE = "Segment `%s` values:\n"
510
UNLINKED_FEATURE_TEXT = "### The feature flag `%s` was unlinked from the issue/PR"
6-
UPDATED_FEATURE_TEXT = "### The Flagsmith Feature `%s` was updated in the environment "
7-
LAST_UPDATED_FEATURE_TEXT = "Last Updated %s"
11+
UPDATED_FEATURE_TEXT = "Flagsmith Feature `%s` has been updated:\n"
812
DELETED_FEATURE_TEXT = "### The Feature Flag `%s` was deleted"
13+
FEATURE_ENVIRONMENT_URL = "%s/project/%s/environment/%s/features?feature=%s&tab=%s"

api/integrations/github/github.py

Lines changed: 46 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
1-
import datetime
21
import logging
32
import typing
43
from dataclasses import dataclass
54

65
import requests
6+
from core.helpers import get_current_site_url
77
from django.conf import settings
88

99
from features.models import FeatureState, FeatureStateValue
1010
from integrations.github.client import generate_token
1111
from integrations.github.constants import (
1212
DELETED_FEATURE_TEXT,
13+
FEATURE_ENVIRONMENT_URL,
14+
FEATURE_TABLE_HEADER,
15+
FEATURE_TABLE_ROW,
1316
GITHUB_API_URL,
14-
LAST_UPDATED_FEATURE_TEXT,
15-
LINK_FEATURE_TEXT,
17+
LINK_FEATURE_TITLE,
18+
LINK_SEGMENT_TITLE,
1619
UNLINKED_FEATURE_TEXT,
1720
UPDATED_FEATURE_TEXT,
1821
)
@@ -28,8 +31,9 @@ class GithubData:
2831
feature_id: int
2932
feature_name: str
3033
type: str
31-
feature_states: typing.List[dict[str, typing.Any]] = None
32-
url: str = None
34+
feature_states: typing.List[dict[str, typing.Any]] | None = None
35+
url: str | None = None
36+
project_id: int | None = None
3337

3438
@classmethod
3539
def from_dict(cls, data_dict: dict) -> "GithubData":
@@ -65,6 +69,8 @@ def post_comment_to_github(
6569
def generate_body_comment(
6670
name: str,
6771
event_type: str,
72+
project_id: int,
73+
feature_id: int,
6874
feature_states: typing.List[typing.Dict[str, typing.Any]],
6975
) -> str:
7076

@@ -78,25 +84,34 @@ def generate_body_comment(
7884
if is_removed:
7985
return delete_text
8086

81-
last_updated_string = LAST_UPDATED_FEATURE_TEXT % (
82-
datetime.datetime.now().strftime("%dth %b %Y %I:%M%p")
83-
)
84-
updated_text = UPDATED_FEATURE_TEXT % (name)
85-
86-
result = updated_text if is_update else LINK_FEATURE_TEXT % (name)
87+
result = UPDATED_FEATURE_TEXT % (name) if is_update else LINK_FEATURE_TITLE % (name)
88+
last_segment_name = ""
89+
if len(feature_states) > 0 and not feature_states[0].get("segment_name"):
90+
result += FEATURE_TABLE_HEADER
8791

88-
# if feature_states is None:
8992
for v in feature_states:
9093
feature_value = v.get("feature_state_value")
91-
feature_value_type = v.get("feature_state_value_type")
92-
93-
feature_value_string = f"\n{feature_value_type}\n```{feature_value if feature_value else 'No value.'}```\n\n"
94-
95-
result += f"**{v['environment_name']}{' - ' + v['segment_name'] if v.get('segment_name') else ''}**\n"
96-
result += f"- [{'x' if v['feature_value'] else ' '}] {'Enabled' if v['feature_value'] else 'Disabled'}"
97-
result += feature_value_string
94+
tab = "segment-overrides" if v.get("segment_name") is not None else "value"
95+
environment_link_url = FEATURE_ENVIRONMENT_URL % (
96+
get_current_site_url(),
97+
project_id,
98+
v.get("environment_api_key"),
99+
feature_id,
100+
tab,
101+
)
102+
if v.get("segment_name") is not None and v["segment_name"] != last_segment_name:
103+
result += "\n" + LINK_SEGMENT_TITLE % (v["segment_name"])
104+
last_segment_name = v["segment_name"]
105+
result += FEATURE_TABLE_HEADER
106+
table_row = FEATURE_TABLE_ROW % (
107+
v["environment_name"],
108+
environment_link_url,
109+
"✅ Enabled" if v["enabled"] else "❌ Disabled",
110+
f"`{feature_value}`" if feature_value else "",
111+
v["last_updated"],
112+
)
113+
result += table_row
98114

99-
result += last_updated_string
100115
return result
101116

102117

@@ -109,8 +124,11 @@ def generate_data(
109124
feature_id: int,
110125
feature_name: str,
111126
type: str,
112-
feature_states: typing.Union[list[FeatureState], list[FeatureStateValue]] = None,
113-
url: str = None,
127+
feature_states: (
128+
typing.Union[list[FeatureState], list[FeatureStateValue]] | None
129+
) = None,
130+
url: str | None = None,
131+
project_id: int | None = None,
114132
) -> GithubData:
115133

116134
if feature_states:
@@ -126,7 +144,11 @@ def generate_data(
126144
feature_env_data["feature_state_value_type"] = feature_state_value_type
127145
if type is not WebhookEventType.FEATURE_EXTERNAL_RESOURCE_REMOVED.value:
128146
feature_env_data["environment_name"] = feature_state.environment.name
129-
feature_env_data["feature_value"] = feature_state.enabled
147+
feature_env_data["enabled"] = feature_state.enabled
148+
feature_env_data["last_updated"] = feature_state.updated_at
149+
feature_env_data["environment_api_key"] = (
150+
feature_state.environment.api_key
151+
)
130152
if (
131153
hasattr(feature_state, "feature_segment")
132154
and feature_state.feature_segment is not None
@@ -147,4 +169,5 @@ def generate_data(
147169
else None
148170
),
149171
feature_states=feature_states_list if feature_states else None,
172+
project_id=project_id,
150173
)

api/integrations/github/tasks.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,16 @@ class CallGithubData:
2424

2525
def send_post_request(data: CallGithubData) -> None:
2626
feature_name = data.github_data.feature_name
27+
feature_id = data.github_data.feature_id
28+
project_id = data.github_data.project_id
2729
event_type = data.event_type
2830
feature_states = feature_states = (
2931
data.github_data.feature_states if data.github_data.feature_states else None
3032
)
3133
installation_id = data.github_data.installation_id
32-
body = generate_body_comment(feature_name, event_type, feature_states)
34+
body = generate_body_comment(
35+
feature_name, event_type, project_id, feature_id, feature_states
36+
)
3337

3438
if (
3539
event_type == WebhookEventType.FLAG_UPDATED.value

api/tests/unit/features/test_unit_feature_external_resources_views.py

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
import datetime
2-
31
import pytest
42
import simplejson as json
53
from django.core.serializers.json import DjangoJSONEncoder
64
from django.urls import reverse
7-
from freezegun import freeze_time
5+
from pytest_mock import MockerFixture
86
from rest_framework import status
97
from rest_framework.test import APIClient
108

@@ -37,24 +35,44 @@ def json(self):
3735
return MockResponse(json_data={"data": "data"}, status_code=200)
3836

3937

40-
@freeze_time("2024-01-01")
4138
def test_create_feature_external_resource(
4239
admin_client_new: APIClient,
4340
feature_with_value: Feature,
41+
segment_featurestate_and_feature_with_value: FeatureState,
42+
environment: Environment,
4443
project: Project,
4544
github_configuration: GithubConfiguration,
4645
github_repository: GithubRepository,
47-
mocker,
46+
mocker: MockerFixture,
4847
) -> None:
4948
# Given
5049
mock_generate_token = mocker.patch(
5150
"integrations.github.github.generate_token",
5251
)
52+
5353
mock_generate_token.return_value = "mocked_token"
5454
github_request_mock = mocker.patch(
5555
"requests.post", side_effect=mocked_requests_post
5656
)
57-
datetime_now = datetime.datetime.now()
57+
58+
feature_state = FeatureState.objects.filter(feature=feature_with_value).first()
59+
60+
expected_comment_body = (
61+
"**Flagsmith feature linked:** `feature_with_value`\n"
62+
"Default Values:\n"
63+
"| Environment | Enabled | Value | Last Updated (UTC) |\n"
64+
"| :--- | :----- | :------ | :------ |\n"
65+
f"| [Test Environment](https://example.com/project/{project.id}/"
66+
f"environment/{environment.api_key}/features?feature={feature_with_value.id}&tab=value) "
67+
f"| ❌ Disabled | `value` | {feature_state.updated_at} |\n"
68+
"\n"
69+
"Segment `segment` values:\n"
70+
"| Environment | Enabled | Value | Last Updated (UTC) |\n"
71+
"| :--- | :----- | :------ | :------ |\n"
72+
f"| [Test Environment](https://example.com/project/{project.id}/"
73+
f"environment/{environment.api_key}/features?feature={feature_with_value.id}&tab=segment-overrides) "
74+
f"| ❌ Disabled | `value` | {segment_featurestate_and_feature_with_value.updated_at} |\n"
75+
)
5876

5977
feature_external_resource_data = {
6078
"type": "GITHUB_ISSUE",
@@ -76,9 +94,7 @@ def test_create_feature_external_resource(
7694
# Then
7795
github_request_mock.assert_called_with(
7896
"https://api.github.com/repos/repoowner/repo-name/issues/35/comments",
79-
json={
80-
"body": f"### This pull request is linked to a Flagsmith Feature (`feature_with_value`):\n**Test Environment**\n- [ ] Disabled\nunicode\n```value```\n\nLast Updated {datetime_now.strftime('%dth %b %Y %I:%M%p')}" # noqa E501
81-
},
97+
json={"body": f"{expected_comment_body}"},
8298
headers={
8399
"Accept": "application/vnd.github.v3+json",
84100
"Authorization": "Bearer mocked_token",
@@ -320,9 +336,10 @@ def test_create_github_comment_on_feature_state_updated( # noqa: C901
320336
with_environment_permissions: WithEnvironmentPermissionsCallable,
321337
feature_external_resource: FeatureExternalResource,
322338
feature: Feature,
339+
project: Project,
323340
github_configuration: GithubConfiguration,
324341
github_repository: GithubRepository,
325-
mocker,
342+
mocker: MockerFixture,
326343
environment: Environment,
327344
event_type: str,
328345
) -> None:
@@ -412,4 +429,5 @@ def test_create_github_comment_on_feature_state_updated( # noqa: C901
412429
feature_name=feature.name,
413430
type=WebhookEventType.FLAG_UPDATED.value,
414431
feature_states=[feature_state],
432+
project_id=project.id,
415433
)

0 commit comments

Comments
 (0)