Skip to content

Commit 042ddf6

Browse files
Merge branch 'main' into max/notify-fix
2 parents 83f4c24 + 519a0a9 commit 042ddf6

File tree

74 files changed

+3118
-139
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

74 files changed

+3118
-139
lines changed

.github/workflows/_build-app.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ jobs:
8787
8888
build-test-app:
8989
name: Build Test App
90-
runs-on: ubuntu-latest
90+
runs-on: ubuntu-large
9191
steps:
9292
- name: Checkout
9393
uses: actions/checkout@v4

.github/workflows/_run-tests.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,11 @@ jobs:
104104
- codecov_url_secret: CODECOV_PUBLIC_QA_URL
105105
codecov_token_secret: CODECOV_PUBLIC_QA_TOKEN
106106
name: public qa
107+
- codecov_url_secret: CODECOV_EUROPE_WEST3_URL
108+
codecov_token_secret: CODECOV_EUROPE_WEST3_TOKEN
109+
name: europe-west3
107110

111+
108112
steps:
109113
- name: Checkout
110114
uses: actions/checkout@v4

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
25.10.6
1+
25.11.3

apps/codecov-api/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ check-for-migration-conflicts:
2626
python manage.py check_for_migration_conflicts
2727

2828
test:
29-
COVERAGE_CORE=sysmon pytest --cov=./ --junitxml=junit.xml -o junit_family=legacy -c pytest.ini --rootdir=${PYTEST_ROOTDIR}
29+
pytest --cov=./ --junitxml=junit.xml -o junit_family=legacy -c pytest.ini --rootdir=${PYTEST_ROOTDIR}
3030

3131
shell:
3232
docker compose exec api bash

apps/codecov-api/api.sh

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,44 @@ set -euo pipefail
44
# Default entrypoint for api
55
echo "Starting api"
66

7+
# Signal handling for graceful shutdown
8+
shutdown_in_progress=false
9+
shutdown() {
10+
# Prevent duplicate shutdown attempts (in case both shell and process receive signal)
11+
if [[ "$shutdown_in_progress" == "true" ]]; then
12+
return 0
13+
fi
14+
shutdown_in_progress=true
15+
16+
echo "Received SIGTERM, shutting down gracefully..."
17+
if [[ -n "${api_pid:-}" ]]; then
18+
# Send SIGTERM to the API process (gunicorn)
19+
kill -TERM "$api_pid" 2>/dev/null || true
20+
21+
# Wait for graceful shutdown with timeout (default 25s, less than K8s terminationGracePeriodSeconds)
22+
local timeout=${SHUTDOWN_TIMEOUT:-25}
23+
local count=0
24+
25+
while kill -0 "$api_pid" 2>/dev/null && [[ $count -lt $timeout ]]; do
26+
sleep 1
27+
((count++))
28+
done
29+
30+
# If process is still running, force kill
31+
if kill -0 "$api_pid" 2>/dev/null; then
32+
echo "API did not shutdown gracefully after ${timeout}s, forcing shutdown..."
33+
kill -KILL "$api_pid" 2>/dev/null || true
34+
wait "$api_pid" 2>/dev/null || true
35+
else
36+
echo "API shutdown complete"
37+
fi
38+
fi
39+
exit 0
40+
}
41+
42+
# Trap SIGTERM and SIGINT
43+
trap shutdown SIGTERM SIGINT
44+
745
# Script section to keep in sync with worker.sh
846
#### Start ####
947

@@ -67,6 +105,8 @@ if [[ -z "${1:-}" ]]; then
67105
echo "Starting gunicorn in default mode"
68106
;;
69107
esac
108+
109+
# Start gunicorn in the background and capture its PID
70110
$pre $berglas gunicorn codecov.wsgi:application \
71111
$added_args \
72112
$statsd \
@@ -76,7 +116,12 @@ if [[ -z "${1:-}" ]]; then
76116
--bind "${CODECOV_API_BIND:-0.0.0.0}":"${CODECOV_API_PORT:-8000}" \
77117
--access-logfile '-' \
78118
--timeout "${GUNICORN_TIMEOUT:-600}" \
79-
$post
119+
$post &
120+
api_pid=$!
121+
echo "Gunicorn started with PID $api_pid"
122+
123+
# Wait for the background process
124+
wait "$api_pid"
80125
else
81126
echo "Executing custom command"
82127
exec "$@"

apps/codecov-api/api/internal/owner/serializers.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ def validate_value(self, value: str) -> str:
140140

141141
def validate(self, plan: dict[str, Any]) -> dict[str, Any]:
142142
current_org = self.context["view"].owner
143-
if current_org.account:
143+
if current_org.has_billing_account:
144144
raise serializers.ValidationError(
145145
detail="You cannot update your plan manually, for help or changes to plan, connect with [email protected]"
146146
)
@@ -312,22 +312,22 @@ def get_checkout_session_id(self, _: Any) -> str:
312312
return self.context.get("checkout_session_id")
313313

314314
def get_activated_student_count(self, owner: Owner) -> int:
315-
if owner.account:
315+
if owner.has_billing_account:
316316
return owner.account.activated_student_count
317317
return owner.activated_student_count
318318

319319
def get_activated_user_count(self, owner: Owner) -> int:
320-
if owner.account:
320+
if owner.has_billing_account:
321321
return owner.account.activated_user_count
322322
return owner.activated_user_count
323323

324324
def get_delinquent(self, owner: Owner) -> bool:
325-
if owner.account:
325+
if owner.has_billing_account:
326326
return owner.account.is_delinquent
327327
return owner.delinquent
328328

329329
def get_uses_invoice(self, owner: Owner) -> bool:
330-
if owner.account:
330+
if owner.has_billing_account:
331331
return (
332332
hasattr(owner.account, "invoice_billing")
333333
and owner.account.invoice_billing.is_active

apps/codecov-api/api/internal/owner/views.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,7 @@
1717
from shared.django_apps.codecov_auth.models import Owner
1818
from shared.plan.constants import DEFAULT_FREE_PLAN
1919

20-
from .serializers import (
21-
AccountDetailsSerializer,
22-
OwnerSerializer,
23-
UserSerializer,
24-
)
20+
from .serializers import AccountDetailsSerializer, OwnerSerializer, UserSerializer
2521

2622
log = logging.getLogger(__name__)
2723

@@ -62,7 +58,7 @@ def destroy(self, request, *args, **kwargs):
6258
return Response(status=status.HTTP_204_NO_CONTENT)
6359

6460
def get_object(self):
65-
if self.owner.account:
61+
if self.owner.has_billing_account:
6662
# gets the related account and invoice_billing objects from db in 1 query
6763
# otherwise, each reference to owner.account would be an additional query
6864
self.owner = (
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
interactions:
2+
- request:
3+
body: null
4+
headers:
5+
Accept:
6+
- '*/*'
7+
Accept-Encoding:
8+
- gzip, deflate
9+
Connection:
10+
- keep-alive
11+
Stripe-Version:
12+
- 2024-12-18.acacia
13+
User-Agent:
14+
- Stripe/v1 PythonBindings/12.1.0
15+
X-Stripe-Client-Telemetry:
16+
- '{"last_request_metrics": {"request_id": "req_AaY8IvHbbSDcvz", "request_duration_ms":
17+
1}}'
18+
X-Stripe-Client-User-Agent:
19+
- '{"bindings_version": "12.1.0", "lang": "python", "publisher": "stripe", "httplib":
20+
"requests", "lang_version": "3.13.8", "platform": "Linux-6.8.0-50-generic-aarch64-with-glibc2.36",
21+
"uname": "Linux affeb8b59f6a 6.8.0-50-generic #51-Ubuntu SMP PREEMPT_DYNAMIC
22+
Sat Nov 9 18:03:35 UTC 2024 aarch64 "}'
23+
method: GET
24+
uri: https://api.stripe.com/v1/subscriptions/sub_test123?expand%5B0%5D=latest_invoice&expand%5B1%5D=customer&expand%5B2%5D=customer.invoice_settings.default_payment_method&expand%5B3%5D=customer.tax_ids
25+
response:
26+
body:
27+
string: "{\n \"error\": {\n \"message\": \"Invalid API Key provided: default\",\n
28+
\ \"type\": \"invalid_request_error\"\n }\n}\n"
29+
headers:
30+
Access-Control-Allow-Credentials:
31+
- 'true'
32+
Access-Control-Allow-Methods:
33+
- GET, HEAD, PUT, PATCH, POST, DELETE
34+
Access-Control-Allow-Origin:
35+
- '*'
36+
Access-Control-Expose-Headers:
37+
- Request-Id, Stripe-Manage-Version, Stripe-Should-Retry, X-Stripe-External-Auth-Required,
38+
X-Stripe-Privileged-Session-Required
39+
Access-Control-Max-Age:
40+
- '300'
41+
Cache-Control:
42+
- no-cache, no-store
43+
Connection:
44+
- keep-alive
45+
Content-Length:
46+
- '109'
47+
Content-Security-Policy:
48+
- base-uri 'none'; default-src 'none'; form-action 'none'; frame-ancestors 'none';
49+
img-src 'self'; script-src 'self' 'report-sample'; style-src 'self'; worker-src
50+
'none'; upgrade-insecure-requests; report-uri https://q.stripe.com/csp-violation?q=ExuJuLbTEDCmAz4Yn3NBLx-GG6q1al6t0FbBrJYe4vkB3kq1BMch4lmx4XLhDMGPvdcJoWL6ddxZhq9D
51+
Content-Type:
52+
- application/json
53+
Date:
54+
- Wed, 08 Oct 2025 21:44:22 GMT
55+
Server:
56+
- nginx
57+
Strict-Transport-Security:
58+
- max-age=63072000; includeSubDomains; preload
59+
Vary:
60+
- Origin
61+
Www-Authenticate:
62+
- Bearer realm="Stripe"
63+
X-Robots-Tag:
64+
- none
65+
X-Wc:
66+
- ABHIJ
67+
status:
68+
code: 401
69+
message: Unauthorized
70+
version: 1

apps/codecov-api/api/internal/tests/views/test_account_viewset.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1859,6 +1859,77 @@ def test_retrieve_org_with_account(self):
18591859
self.assertDictEqual(response.data["plan"], expected_response["plan"])
18601860
self.assertDictEqual(response.data, expected_response)
18611861

1862+
def test_retrieve_with_sentry_merge_account_uses_owner_fields(self):
1863+
org = OwnerFactory(
1864+
plan=PlanName.CODECOV_PRO_YEARLY.value,
1865+
plan_user_count=10,
1866+
stripe_customer_id="cus_test123",
1867+
delinquent=True,
1868+
)
1869+
org.plan_activated_users = []
1870+
org.save()
1871+
1872+
activated_owner = OwnerFactory(
1873+
service=Service.GITHUB.value,
1874+
user=UserFactory(),
1875+
organizations=[org.ownerid],
1876+
)
1877+
org.plan_activated_users = [activated_owner.ownerid]
1878+
org.admins = [activated_owner.ownerid]
1879+
org.save()
1880+
1881+
account = AccountFactory(
1882+
plan=PlanName.SENTRY_MERGE_PLAN.value,
1883+
plan_seat_count=5,
1884+
is_delinquent=False,
1885+
)
1886+
InvoiceBillingFactory(account=account, is_active=True)
1887+
org.account = account
1888+
org.save()
1889+
1890+
self.client.force_login_owner(activated_owner)
1891+
response = self._retrieve(
1892+
kwargs={"service": Service.GITHUB.value, "owner_username": org.username}
1893+
)
1894+
assert response.status_code == status.HTTP_200_OK
1895+
1896+
expected_response = {
1897+
"activated_user_count": 1,
1898+
"activated_student_count": 0,
1899+
"delinquent": True,
1900+
"uses_invoice": False,
1901+
"plan": {
1902+
"marketing_name": "Pro",
1903+
"value": PlanName.CODECOV_PRO_YEARLY.value,
1904+
"billing_rate": "annually",
1905+
"base_unit_price": 10,
1906+
"benefits": [
1907+
"Configurable # of users",
1908+
"Unlimited public repositories",
1909+
"Unlimited private repositories",
1910+
"Priority Support",
1911+
],
1912+
"quantity": 10,
1913+
},
1914+
"root_organization": None,
1915+
"integration_id": org.integration_id,
1916+
"plan_auto_activate": org.plan_auto_activate,
1917+
"inactive_user_count": 0,
1918+
"subscription_detail": None,
1919+
"checkout_session_id": None,
1920+
"name": org.name,
1921+
"email": org.email,
1922+
"nb_active_private_repos": 0,
1923+
"repo_total_credits": 99999999,
1924+
"plan_provider": org.plan_provider,
1925+
"student_count": 0,
1926+
"schedule_detail": None,
1927+
}
1928+
self.assertDictEqual(response.data["plan"], expected_response["plan"])
1929+
self.assertEqual(response.data["activated_user_count"], 1)
1930+
self.assertEqual(response.data["delinquent"], True)
1931+
self.assertEqual(response.data["uses_invoice"], False)
1932+
18621933

18631934
@override_settings(IS_ENTERPRISE=True)
18641935
class EnterpriseAccountViewSetTests(APITestCase):

apps/codecov-api/api/public/v2/tests/test_api_evals_viewset.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -192,11 +192,17 @@ def test_summary_aggregation_and_filtering_no_filter(self):
192192
assert data["totalItems"] == 3
193193
assert data["passedItems"] == 2
194194
assert data["failedItems"] == 1
195-
assert data["avgDurationSeconds"] == (10.0 + 20.0 + 30.0) / 3
196-
assert data["avgCost"] == (5.0 + 7.0 + 3.0) / 3
195+
assert data["avgDurationSeconds"] == pytest.approx((10.0 + 20.0 + 30.0) / 3)
196+
assert data["avgCost"] == pytest.approx((5.0 + 7.0 + 3.0) / 3)
197197
assert data["scores"] == {
198-
"accuracy": {"sum": (0.9 + 0.7 + 0.1), "avg": (0.9 + 0.7 + 0.1) / 3},
199-
"f1": {"sum": (0.8 + 0.6 + 0.2), "avg": (0.8 + 0.6 + 0.2) / 3},
198+
"accuracy": {
199+
"sum": pytest.approx(0.9 + 0.7 + 0.1),
200+
"avg": pytest.approx((0.9 + 0.7 + 0.1) / 3),
201+
},
202+
"f1": {
203+
"sum": pytest.approx(0.8 + 0.6 + 0.2),
204+
"avg": pytest.approx((0.8 + 0.6 + 0.2) / 3),
205+
},
200206
}
201207

202208
def test_summary_aggregation_and_filtering_filter_classA(self):

0 commit comments

Comments
 (0)