Skip to content
This repository was archived by the owner on Jun 13, 2025. It is now read-only.

Commit a4a5466

Browse files
authored
Merge branch 'main' into tony/coverage-single-upload
2 parents 3f74fa4 + 136300e commit a4a5466

File tree

13 files changed

+197
-19
lines changed

13 files changed

+197
-19
lines changed

.github/workflows/ci.yml

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,25 @@ concurrency:
1515
permissions:
1616
contents: "read"
1717
id-token: "write"
18+
issues: "write"
19+
pull-requests: "write"
1820

1921
jobs:
2022
lint:
2123
name: Run Lint
22-
uses: codecov/gha-workflows/.github/workflows/[email protected].24
24+
uses: codecov/gha-workflows/.github/workflows/[email protected].26
2325

2426
build:
2527
name: Build API
26-
uses: codecov/gha-workflows/.github/workflows/[email protected].24
28+
uses: codecov/gha-workflows/.github/workflows/[email protected].26
2729
secrets: inherit
2830
with:
2931
repo: ${{ vars.CODECOV_IMAGE_V2 || 'codecov/self-hosted-api' }}
3032

3133
codecovstartup:
3234
name: Codecov Startup
3335
needs: build
34-
uses: codecov/gha-workflows/.github/workflows/[email protected].24
36+
uses: codecov/gha-workflows/.github/workflows/[email protected].26
3537
secrets: inherit
3638

3739
# ats:
@@ -47,15 +49,15 @@ jobs:
4749
test:
4850
name: Test
4951
needs: [build]
50-
uses: codecov/gha-workflows/.github/workflows/[email protected].24
52+
uses: codecov/gha-workflows/.github/workflows/[email protected].26
5153
secrets: inherit
5254
with:
5355
repo: ${{ vars.CODECOV_IMAGE_V2 || 'codecov/self-hosted-api' }}
5456

5557
build-self-hosted:
5658
name: Build Self Hosted API
5759
needs: [build, test]
58-
uses: codecov/gha-workflows/.github/workflows/[email protected].24
60+
uses: codecov/gha-workflows/.github/workflows/[email protected].26
5961
secrets: inherit
6062
with:
6163
repo: ${{ vars.CODECOV_IMAGE_V2 || 'codecov/self-hosted-api' }}
@@ -64,7 +66,7 @@ jobs:
6466
name: Push Staging Image
6567
needs: [build, test]
6668
if: ${{ github.event_name == 'push' && github.event.ref == 'refs/heads/staging' && github.repository_owner == 'codecov' }}
67-
uses: codecov/gha-workflows/.github/workflows/[email protected].24
69+
uses: codecov/gha-workflows/.github/workflows/[email protected].26
6870
secrets: inherit
6971
with:
7072
environment: staging
@@ -74,7 +76,7 @@ jobs:
7476
name: Push Production Image
7577
needs: [build, test]
7678
if: ${{ github.event_name == 'push' && github.event.ref == 'refs/heads/main' && github.repository_owner == 'codecov' }}
77-
uses: codecov/gha-workflows/.github/workflows/[email protected].24
79+
uses: codecov/gha-workflows/.github/workflows/[email protected].26
7880
secrets: inherit
7981
with:
8082
environment: production
@@ -85,7 +87,7 @@ jobs:
8587
needs: [build-self-hosted, test]
8688
secrets: inherit
8789
if: ${{ github.event_name == 'push' && github.event.ref == 'refs/heads/main' && github.repository_owner == 'codecov' }}
88-
uses: codecov/gha-workflows/.github/workflows/[email protected].24
90+
uses: codecov/gha-workflows/.github/workflows/[email protected].26
8991
with:
9092
push_rolling: true
9193
repo: ${{ vars.CODECOV_IMAGE_V2 || 'codecov/self-hosted-api' }}

codecov/settings_base.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,10 @@
106106

107107
GRAPHQL_INTROSPECTION_ENABLED = False
108108

109+
GRAPHQL_MAX_DEPTH = get_config("setup", "graphql", "max_depth", default=20)
110+
111+
GRAPHQL_MAX_ALIASES = get_config("setup", "graphql", "max_aliases", default=10)
112+
109113
# Database
110114
# https://docs.djangoproject.com/en/2.1/ref/settings/#databases
111115

@@ -166,6 +170,8 @@
166170
"'sha256-eKdXhLyOdPl2/gp1Ob116rCU2Ox54rseyz1MwCmzb6w='",
167171
"'sha256-a1pELtDJXf8fPX1YL2JiBM91RQBeIAswunzgwMEsvwA='",
168172
"'sha256-cNIcuS0BVLuBVP5rpfeFE42xHz7r5hMyf9YdfknWuCg='",
173+
"'sha256-bmwAzHxhO1mBINfkKkKPopyKEv4ppCHx/z84wQJ9nOY='",
174+
"'sha256-jQoC6QpIonlMBPFbUGlJFRJFFWbbijMl7Z8XqWrb46o='",
169175
"https://cdn.jsdelivr.net/npm/graphql-playground-react/build/static/js/middleware.js",
170176
"https://cdn.jsdelivr.net/npm/graphql-playground-react/build/favicon.png",
171177
"https://cdn.jsdelivr.net/npm/graphql-playground-react/build/static/css/index.css",
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
from graphql import (
2+
GraphQLField,
3+
GraphQLObjectType,
4+
GraphQLSchema,
5+
GraphQLString,
6+
parse,
7+
validate,
8+
)
9+
10+
from ..validation import (
11+
create_max_aliases_rule,
12+
create_max_depth_rule,
13+
)
14+
15+
16+
def resolve_field(*args):
17+
return "test"
18+
19+
20+
QueryType = GraphQLObjectType(
21+
"Query", {"field": GraphQLField(GraphQLString, resolve=resolve_field)}
22+
)
23+
schema = GraphQLSchema(query=QueryType)
24+
25+
26+
def validate_query(query, *rules):
27+
ast = parse(query)
28+
return validate(schema, ast, rules=rules)
29+
30+
31+
def test_max_depth_rule_allows_within_depth():
32+
query = """
33+
query {
34+
field
35+
}
36+
"""
37+
errors = validate_query(query, create_max_depth_rule(2))
38+
assert not errors, "Expected no errors for depth within the limit"
39+
40+
41+
def test_max_depth_rule_rejects_exceeding_depth():
42+
query = """
43+
query {
44+
field {
45+
field {
46+
field
47+
}
48+
}
49+
}
50+
"""
51+
errors = validate_query(query, create_max_depth_rule(2))
52+
assert errors, "Expected errors for exceeding depth limit"
53+
assert any(
54+
"Query depth exceeds the maximum allowed depth" in str(e) for e in errors
55+
)
56+
57+
58+
def test_max_depth_rule_exact_depth():
59+
query = """
60+
query {
61+
field
62+
}
63+
"""
64+
errors = validate_query(query, create_max_depth_rule(2))
65+
assert not errors, "Expected no errors when query depth matches the limit"
66+
67+
68+
def test_max_aliases_rule_allows_within_alias_limit():
69+
query = """
70+
query {
71+
alias1: field
72+
alias2: field
73+
}
74+
"""
75+
errors = validate_query(query, create_max_aliases_rule(2))
76+
assert not errors, "Expected no errors for alias count within the limit"
77+
78+
79+
def test_max_aliases_rule_rejects_exceeding_alias_limit():
80+
query = """
81+
query {
82+
alias1: field
83+
alias2: field
84+
alias3: field
85+
}
86+
"""
87+
errors = validate_query(query, create_max_aliases_rule(2))
88+
assert errors, "Expected errors for exceeding alias limit"
89+
assert any("Query uses too many aliases" in str(e) for e in errors)
90+
91+
92+
def test_max_aliases_rule_exact_alias_limit():
93+
query = """
94+
query {
95+
alias1: field
96+
alias2: field
97+
}
98+
"""
99+
errors = validate_query(query, create_max_aliases_rule(2))
100+
assert not errors, "Expected no errors when alias count matches the limit"

graphql_api/validation.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
from typing import Any, Type
2+
3+
from graphql import GraphQLError, ValidationRule
4+
from graphql.language.ast import DocumentNode, FieldNode, OperationDefinitionNode
5+
from graphql.validation import ValidationContext
6+
7+
8+
def create_max_depth_rule(max_depth: int) -> Type[ValidationRule]:
9+
class MaxDepthRule(ValidationRule):
10+
def __init__(self, context: ValidationContext) -> None:
11+
super().__init__(context)
12+
self.operation_depth: int = 1
13+
self.max_depth_reached: bool = False
14+
self.max_depth: int = max_depth
15+
16+
def enter_operation_definition(
17+
self, node: OperationDefinitionNode, *_args: Any
18+
) -> None:
19+
self.operation_depth = 1
20+
self.max_depth_reached = False
21+
22+
def enter_field(self, node: FieldNode, *_args: Any) -> None:
23+
self.operation_depth += 1
24+
25+
if self.operation_depth > self.max_depth and not self.max_depth_reached:
26+
self.max_depth_reached = True
27+
self.report_error(
28+
GraphQLError(
29+
"Query depth exceeds the maximum allowed depth",
30+
node,
31+
)
32+
)
33+
34+
def leave_field(self, node: FieldNode, *_args: Any) -> None:
35+
self.operation_depth -= 1
36+
37+
return MaxDepthRule
38+
39+
40+
def create_max_aliases_rule(max_aliases: int) -> Type[ValidationRule]:
41+
class MaxAliasesRule(ValidationRule):
42+
def __init__(self, context: ValidationContext) -> None:
43+
super().__init__(context)
44+
self.alias_count: int = 0
45+
self.has_reported_error: bool = False
46+
self.max_aliases: int = max_aliases
47+
48+
def enter_document(self, node: DocumentNode, *_args: Any) -> None:
49+
self.alias_count = 0
50+
self.has_reported_error = False
51+
52+
def enter_field(self, node: FieldNode, *_args: Any) -> None:
53+
if node.alias:
54+
self.alias_count += 1
55+
56+
if self.alias_count > self.max_aliases and not self.has_reported_error:
57+
self.has_reported_error = True
58+
self.report_error(
59+
GraphQLError(
60+
"Query uses too many aliases",
61+
node,
62+
)
63+
)
64+
65+
return MaxAliasesRule

graphql_api/views.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from services.redis_configuration import get_redis_connection
2929

3030
from .schema import schema
31+
from .validation import create_max_aliases_rule, create_max_depth_rule
3132

3233
log = logging.getLogger(__name__)
3334

@@ -188,7 +189,7 @@ def __exit__(self, exc_type, exc_value, exc_traceback):
188189
class AsyncGraphqlView(GraphQLAsyncView):
189190
schema = schema
190191
extensions = [QueryMetricsExtension]
191-
introspection = getattr(settings, "GRAPHQL_INTROSPECTION_ENABLED", False)
192+
introspection = settings.GRAPHQL_INTROSPECTION_ENABLED
192193

193194
def get_validation_rules(
194195
self,
@@ -197,11 +198,13 @@ def get_validation_rules(
197198
data: dict,
198199
) -> Optional[Collection]:
199200
return [
201+
create_max_aliases_rule(max_aliases=settings.GRAPHQL_MAX_ALIASES),
202+
create_max_depth_rule(max_depth=settings.GRAPHQL_MAX_DEPTH),
200203
cost_validator(
201204
maximum_cost=settings.GRAPHQL_QUERY_COST_THRESHOLD,
202205
default_cost=1,
203206
variables=data.get("variables"),
204-
)
207+
),
205208
]
206209

207210
validation_rules = get_validation_rules # type: ignore

upload/helpers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -754,9 +754,9 @@ def dispatch_upload_task(
754754

755755
def validate_activated_repo(repository):
756756
if repository.active and not repository.activated:
757-
settings_url = f"{settings.CODECOV_DASHBOARD_URL}/{repository.author.service}/{repository.author.username}/{repository.name}/settings"
757+
config_url = f"{settings.CODECOV_DASHBOARD_URL}/{repository.author.service}/{repository.author.username}/{repository.name}/config/general"
758758
raise ValidationError(
759-
f"This repository is deactivated. To resume uploading to it, please activate the repository in the codecov UI: {settings_url}"
759+
f"This repository is deactivated. To resume uploading to it, please activate the repository in the codecov UI: {config_url}"
760760
)
761761

762762

upload/tests/test_helpers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -259,12 +259,12 @@ def test_validate_upload_too_many_uploads_for_commit(
259259

260260
def test_deactivated_repo(db, mocker):
261261
repository = RepositoryFactory.create(active=True, activated=False)
262-
settings_url = f"{settings.CODECOV_DASHBOARD_URL}/{repository.author.service}/{repository.author.username}/{repository.name}/settings"
262+
config_url = f"{settings.CODECOV_DASHBOARD_URL}/{repository.author.service}/{repository.author.username}/{repository.name}/config/general"
263263

264264
with pytest.raises(ValidationError) as exp:
265265
validate_activated_repo(repository)
266266
assert exp.match(
267-
f"This repository is deactivated. To resume uploading to it, please activate the repository in the codecov UI: {settings_url}"
267+
f"This repository is deactivated. To resume uploading to it, please activate the repository in the codecov UI: {config_url}"
268268
)
269269

270270

upload/tests/views/test_commits.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def test_deactivated_repo(db):
6868
response_json = response.json()
6969
assert response.status_code == 400
7070
assert response_json == [
71-
f"This repository is deactivated. To resume uploading to it, please activate the repository in the codecov UI: {settings.CODECOV_DASHBOARD_URL}/github/codecov/the_repo/settings"
71+
f"This repository is deactivated. To resume uploading to it, please activate the repository in the codecov UI: {settings.CODECOV_DASHBOARD_URL}/github/codecov/the_repo/config/general"
7272
]
7373

7474

upload/tests/views/test_reports.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def test_deactivated_repo(db):
6565
response_json = response.json()
6666
assert response.status_code == 400
6767
assert response_json == [
68-
f"This repository is deactivated. To resume uploading to it, please activate the repository in the codecov UI: {settings.CODECOV_DASHBOARD_URL}/github/codecov/the_repo/settings"
68+
f"This repository is deactivated. To resume uploading to it, please activate the repository in the codecov UI: {settings.CODECOV_DASHBOARD_URL}/github/codecov/the_repo/config/general"
6969
]
7070

7171

upload/tests/views/test_uploads.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -793,7 +793,7 @@ def test_deactivated_repo(db, mocker, mock_redis):
793793
response_json = response.json()
794794
assert response.status_code == 400
795795
assert response_json == [
796-
f"This repository is deactivated. To resume uploading to it, please activate the repository in the codecov UI: {settings.CODECOV_DASHBOARD_URL}/github/codecov/the_repo/settings"
796+
f"This repository is deactivated. To resume uploading to it, please activate the repository in the codecov UI: {settings.CODECOV_DASHBOARD_URL}/github/codecov/the_repo/config/general"
797797
]
798798

799799

0 commit comments

Comments
 (0)