Skip to content

Commit 6549fcb

Browse files
authored
Backend: Add field to store github branch for challenge creation(Cloud-CV#4737)
* Store Github branch with challenge data * Modify backend to store github branch * Handle empty branches * Merge migrations * Update seed.py * Add GitHub branch versioning support for multi-version challenges - Update create_or_update_github_challenge to use both github_repository and github_branch for challenge lookup - Add default branch logic: when GITHUB_REF_NAME is empty, defaults to 'challenge' - Create combined migration 0113 that adds github_branch field and unique constraint - Fix database seeder to use unique github values for each challenge - Update tests to expect 'challenge' as default branch name - Enable multiple challenge versions from same repository using different branches Fixes issue where different branches would update existing challenges instead of creating new ones. * Revert unnecessary changes * Fix tests * Update Github branch var * Fix failing tests * Pass flake8 and pylint tests
1 parent 791be3b commit 6549fcb

10 files changed

Lines changed: 134 additions & 3 deletions

File tree

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,4 @@ notifications:
8181
email:
8282
on_success: change
8383
on_failure: always
84-
slack: cloudcv:gy3CGQGNXLwXOqVyzXGZfdea
84+
slack: cloudcv:gy3CGQGNXLwXOqVyzXGZfdea

apps/challenges/admin.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ class ChallengeAdmin(ImportExportTimeStampedAdmin):
5858
"workers",
5959
"task_def_arn",
6060
"github_repository",
61+
"github_branch",
6162
)
6263
list_filter = (
6364
ChallengeFilter,
@@ -75,6 +76,7 @@ class ChallengeAdmin(ImportExportTimeStampedAdmin):
7576
"creator__team_name",
7677
"slug",
7778
"github_repository",
79+
"github_branch",
7880
)
7981
actions = [
8082
"start_selected_workers",

apps/challenges/challenge_config_utils.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,9 @@ def validate_serializer(self):
586586
"github_repository": self.request.data[
587587
"GITHUB_REPOSITORY"
588588
],
589+
"github_branch": self.request.data.get(
590+
"GITHUB_BRANCH_NAME", ""
591+
),
589592
},
590593
)
591594
if not serializer.is_valid():
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Generated by Django 2.2.20 on 2025-07-09 15:00
2+
3+
from django.db import migrations, models
4+
5+
6+
def fix_duplicate_github_fields(apps, schema_editor):
7+
"""
8+
No data migration needed since we're using a partial unique constraint.
9+
"""
10+
pass
11+
12+
13+
def reverse_fix_duplicate_github_fields(apps, schema_editor):
14+
"""
15+
No reverse migration needed.
16+
"""
17+
pass
18+
19+
20+
class Migration(migrations.Migration):
21+
22+
dependencies = [
23+
("challenges", "0112_challenge_sqs_retention_period"),
24+
]
25+
26+
operations = [
27+
migrations.AddField(
28+
model_name="challenge",
29+
name="github_branch",
30+
field=models.CharField(
31+
blank=True, default="", max_length=200, null=True
32+
),
33+
),
34+
migrations.RunPython(
35+
fix_duplicate_github_fields,
36+
reverse_fix_duplicate_github_fields,
37+
),
38+
# Add a partial unique constraint that only applies when both fields are not empty
39+
migrations.RunSQL(
40+
"CREATE UNIQUE INDEX challenge_github_repo_branch_partial_idx ON challenge (github_repository, github_branch) WHERE github_repository != '' AND github_branch != '';",
41+
reverse_sql="DROP INDEX IF EXISTS challenge_github_repo_branch_partial_idx;",
42+
),
43+
]

apps/challenges/models.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,10 @@ def __init__(self, *args, **kwargs):
186186
github_repository = models.CharField(
187187
max_length=1000, null=True, blank=True, default=""
188188
)
189+
# The github branch name used to create/update the challenge
190+
github_branch = models.CharField(
191+
max_length=200, null=True, blank=True, default=""
192+
)
189193
# The number of vCPU for a Fargate worker for the challenge. Default value
190194
# is 0.25 vCPU.
191195
worker_cpu_cores = models.IntegerField(null=True, blank=True, default=512)

apps/challenges/serializers.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ class Meta:
9595
"worker_instance_type",
9696
"sqs_retention_period",
9797
"github_repository",
98+
"github_branch",
9899
)
99100

100101

@@ -256,6 +257,9 @@ def __init__(self, *args, **kwargs):
256257
github_repository = context.get("github_repository")
257258
if github_repository:
258259
kwargs["data"]["github_repository"] = github_repository
260+
github_branch = context.get("github_branch")
261+
if github_branch:
262+
kwargs["data"]["github_branch"] = github_branch
259263

260264
class Meta:
261265
model = Challenge
@@ -294,6 +298,7 @@ class Meta:
294298
"max_docker_image_size",
295299
"cli_version",
296300
"github_repository",
301+
"github_branch",
297302
"vpc_cidr",
298303
"subnet_1_cidr",
299304
"subnet_2_cidr",

apps/challenges/views.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3897,8 +3897,12 @@ def create_or_update_github_challenge(request, challenge_host_team_pk):
38973897
response_data = {"error": "ChallengeHostTeam does not exist"}
38983898
return Response(response_data, status=status.HTTP_406_NOT_ACCEPTABLE)
38993899

3900+
# Get branch name with default fallback
3901+
github_branch = request.data.get("GITHUB_BRANCH_NAME", "")
3902+
39003903
challenge_queryset = Challenge.objects.filter(
3901-
github_repository=request.data["GITHUB_REPOSITORY"]
3904+
github_repository=request.data["GITHUB_REPOSITORY"],
3905+
github_branch=github_branch,
39023906
)
39033907

39043908
if challenge_queryset:
@@ -3979,6 +3983,7 @@ def create_or_update_github_challenge(request, challenge_host_team_pk):
39793983
"github_repository": request.data[
39803984
"GITHUB_REPOSITORY"
39813985
],
3986+
"github_branch": github_branch,
39823987
"worker_image_url": worker_image_url,
39833988
},
39843989
)

scripts/seed.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,8 @@ def create_challenge(
255255
queue=queue,
256256
featured=is_featured,
257257
image=image_file,
258+
github_repository=f"evalai-examples/{slug}",
259+
github_branch="main",
258260
)
259261
challenge.save()
260262

tests/unit/challenges/test_views.py

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,11 +205,14 @@ def test_get_challenge(self):
205205
"worker_instance_type": self.challenge.worker_instance_type,
206206
"sqs_retention_period": self.challenge.sqs_retention_period,
207207
"github_repository": self.challenge.github_repository,
208+
"github_branch": self.challenge.github_branch,
208209
}
209210
]
210211

211212
response = self.client.get(self.url, {})
212-
self.assertEqual(response.data["results"], expected)
213+
self.assertEqual(
214+
response.data["results"], json.loads(json.dumps(expected))
215+
)
213216
self.assertEqual(response.status_code, status.HTTP_200_OK)
214217

215218
def test_particular_challenge_host_team_for_challenge_does_not_exist(self):
@@ -577,6 +580,7 @@ def test_get_particular_challenge(self):
577580
"worker_instance_type": self.challenge.worker_instance_type,
578581
"sqs_retention_period": self.challenge.sqs_retention_period,
579582
"github_repository": self.challenge.github_repository,
583+
"github_branch": self.challenge.github_branch,
580584
}
581585
response = self.client.get(self.url, {})
582586
self.assertEqual(response.data, expected)
@@ -680,6 +684,7 @@ def test_update_challenge_when_user_is_its_creator(self):
680684
"worker_instance_type": self.challenge.worker_instance_type,
681685
"sqs_retention_period": self.challenge.sqs_retention_period,
682686
"github_repository": self.challenge.github_repository,
687+
"github_branch": self.challenge.github_branch,
683688
}
684689
response = self.client.put(
685690
self.url, {"title": new_title, "description": new_description}
@@ -809,6 +814,7 @@ def test_particular_challenge_partial_update(self):
809814
"worker_instance_type": self.challenge.worker_instance_type,
810815
"sqs_retention_period": self.challenge.sqs_retention_period,
811816
"github_repository": self.challenge.github_repository,
817+
"github_branch": self.challenge.github_branch,
812818
}
813819
response = self.client.patch(self.url, self.partial_update_data)
814820
self.assertEqual(response.data, expected)
@@ -887,6 +893,7 @@ def test_particular_challenge_update(self):
887893
"worker_instance_type": self.challenge.worker_instance_type,
888894
"sqs_retention_period": self.challenge.sqs_retention_period,
889895
"github_repository": self.challenge.github_repository,
896+
"github_branch": self.challenge.github_branch,
890897
}
891898
response = self.client.put(self.url, self.data)
892899
self.assertEqual(response.data, expected)
@@ -1484,6 +1491,7 @@ def test_get_past_challenges(self):
14841491
"worker_instance_type": self.challenge3.worker_instance_type,
14851492
"sqs_retention_period": self.challenge3.sqs_retention_period,
14861493
"github_repository": self.challenge3.github_repository,
1494+
"github_branch": self.challenge3.github_branch,
14871495
}
14881496
]
14891497
response = self.client.get(self.url, {}, format="json")
@@ -1568,6 +1576,7 @@ def test_get_present_challenges(self):
15681576
"worker_instance_type": self.challenge2.worker_instance_type,
15691577
"sqs_retention_period": self.challenge2.sqs_retention_period,
15701578
"github_repository": self.challenge2.github_repository,
1579+
"github_branch": self.challenge2.github_branch,
15711580
}
15721581
]
15731582
response = self.client.get(self.url, {}, format="json")
@@ -1652,6 +1661,7 @@ def test_get_future_challenges(self):
16521661
"worker_instance_type": self.challenge4.worker_instance_type,
16531662
"sqs_retention_period": self.challenge4.sqs_retention_period,
16541663
"github_repository": self.challenge4.github_repository,
1664+
"github_branch": self.challenge4.github_branch,
16551665
}
16561666
]
16571667
response = self.client.get(self.url, {}, format="json")
@@ -1736,6 +1746,7 @@ def test_get_all_challenges(self):
17361746
"worker_instance_type": self.challenge4.worker_instance_type,
17371747
"sqs_retention_period": self.challenge4.sqs_retention_period,
17381748
"github_repository": self.challenge4.github_repository,
1749+
"github_branch": self.challenge4.github_branch,
17391750
},
17401751
{
17411752
"id": self.challenge3.pk,
@@ -1804,6 +1815,7 @@ def test_get_all_challenges(self):
18041815
"worker_instance_type": self.challenge3.worker_instance_type,
18051816
"sqs_retention_period": self.challenge3.sqs_retention_period,
18061817
"github_repository": self.challenge3.github_repository,
1818+
"github_branch": self.challenge3.github_branch,
18071819
},
18081820
{
18091821
"id": self.challenge2.pk,
@@ -1872,6 +1884,7 @@ def test_get_all_challenges(self):
18721884
"worker_instance_type": self.challenge2.worker_instance_type,
18731885
"sqs_retention_period": self.challenge2.sqs_retention_period,
18741886
"github_repository": self.challenge2.github_repository,
1887+
"github_branch": self.challenge2.github_branch,
18751888
},
18761889
]
18771890
response = self.client.get(self.url, {}, format="json")
@@ -2012,6 +2025,7 @@ def test_get_featured_challenges(self):
20122025
"worker_instance_type": self.challenge3.worker_instance_type,
20132026
"sqs_retention_period": self.challenge3.sqs_retention_period,
20142027
"github_repository": self.challenge3.github_repository,
2028+
"github_branch": self.challenge3.github_branch,
20152029
}
20162030
]
20172031
response = self.client.get(self.url, {}, format="json")
@@ -2177,6 +2191,7 @@ def test_get_challenge_by_pk_when_user_is_challenge_host(self):
21772191
"worker_instance_type": self.challenge3.worker_instance_type,
21782192
"sqs_retention_period": self.challenge3.sqs_retention_period,
21792193
"github_repository": self.challenge3.github_repository,
2194+
"github_branch": self.challenge3.github_branch,
21802195
}
21812196

21822197
response = self.client.get(self.url, {})
@@ -2269,6 +2284,7 @@ def test_get_challenge_by_pk_when_user_is_participant(self):
22692284
"worker_instance_type": self.challenge4.worker_instance_type,
22702285
"sqs_retention_period": self.challenge4.sqs_retention_period,
22712286
"github_repository": self.challenge4.github_repository,
2287+
"github_branch": self.challenge4.github_branch,
22722288
}
22732289

22742290
self.client.force_authenticate(user=self.user1)
@@ -2423,6 +2439,7 @@ def test_get_challenge_when_host_team_is_given(self):
24232439
"worker_instance_type": self.challenge2.worker_instance_type,
24242440
"sqs_retention_period": self.challenge2.sqs_retention_period,
24252441
"github_repository": self.challenge2.github_repository,
2442+
"github_branch": self.challenge2.github_branch,
24262443
}
24272444
]
24282445

@@ -2503,6 +2520,7 @@ def test_get_challenge_when_participant_team_is_given(self):
25032520
"worker_instance_type": self.challenge2.worker_instance_type,
25042521
"sqs_retention_period": self.challenge2.sqs_retention_period,
25052522
"github_repository": self.challenge2.github_repository,
2523+
"github_branch": self.challenge2.github_branch,
25062524
}
25072525
]
25082526

@@ -2583,6 +2601,7 @@ def test_get_challenge_when_mode_is_participant(self):
25832601
"worker_instance_type": self.challenge2.worker_instance_type,
25842602
"sqs_retention_period": self.challenge2.sqs_retention_period,
25852603
"github_repository": self.challenge2.github_repository,
2604+
"github_branch": self.challenge2.github_branch,
25862605
}
25872606
]
25882607

@@ -2661,6 +2680,7 @@ def test_get_challenge_when_mode_is_host(self):
26612680
"worker_instance_type": self.challenge.worker_instance_type,
26622681
"sqs_retention_period": self.challenge.sqs_retention_period,
26632682
"github_repository": self.challenge.github_repository,
2683+
"github_branch": self.challenge.github_branch,
26642684
},
26652685
{
26662686
"id": self.challenge2.pk,
@@ -2729,6 +2749,7 @@ def test_get_challenge_when_mode_is_host(self):
27292749
"worker_instance_type": self.challenge2.worker_instance_type,
27302750
"sqs_retention_period": self.challenge2.sqs_retention_period,
27312751
"github_repository": self.challenge2.github_repository,
2752+
"github_branch": self.challenge2.github_branch,
27322753
},
27332754
]
27342755

@@ -5911,6 +5932,7 @@ def test_create_challenge_using_github_success(self):
59115932
self.url,
59125933
{
59135934
"GITHUB_REPOSITORY": "https://github.com/yourusername/repository",
5935+
"GITHUB_BRANCH_NAME": "refs/heads/challenge",
59145936
"zip_configuration": self.input_zip_file,
59155937
},
59165938
format="multipart",
@@ -5926,6 +5948,14 @@ def test_create_challenge_using_github_success(self):
59265948
self.assertEqual(Leaderboard.objects.count(), 1)
59275949
self.assertEqual(ChallengePhaseSplit.objects.count(), 1)
59285950

5951+
# Verify github_branch is properly stored
5952+
challenge = Challenge.objects.first()
5953+
self.assertEqual(
5954+
challenge.github_repository,
5955+
"https://github.com/yourusername/repository",
5956+
)
5957+
self.assertEqual(challenge.github_branch, "refs/heads/challenge")
5958+
59295959
def test_create_challenge_using_github_when_challenge_host_team_does_not_exist(
59305960
self,
59315961
):
@@ -5964,6 +5994,40 @@ def test_create_challenge_using_github_when_user_is_not_authenticated(
59645994
self.assertEqual(list(response.data.values())[0], expected["error"])
59655995
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
59665996

5997+
def test_create_challenge_using_github_without_branch_name(self):
5998+
self.url = reverse_lazy(
5999+
"challenges:create_or_update_github_challenge",
6000+
kwargs={"challenge_host_team_pk": self.challenge_host_team.pk},
6001+
)
6002+
6003+
with mock.patch("challenges.views.requests.get") as m:
6004+
resp = mock.Mock()
6005+
resp.content = self.test_zip_file.read()
6006+
resp.status_code = 200
6007+
m.return_value = resp
6008+
response = self.client.post(
6009+
self.url,
6010+
{
6011+
"GITHUB_REPOSITORY": "https://github.com/yourusername/repository",
6012+
"zip_configuration": self.input_zip_file,
6013+
},
6014+
format="multipart",
6015+
)
6016+
expected = {
6017+
"Success": "Challenge Challenge Title has been created successfully and sent for review to EvalAI Admin."
6018+
}
6019+
6020+
self.assertEqual(response.status_code, 201)
6021+
self.assertEqual(response.json(), expected)
6022+
6023+
# Verify github_branch defaults to empty string when not provided
6024+
challenge = Challenge.objects.first()
6025+
self.assertEqual(
6026+
challenge.github_repository,
6027+
"https://github.com/yourusername/repository",
6028+
)
6029+
self.assertEqual(challenge.github_branch, "")
6030+
59676031

59686032
class ValidateChallengeTest(APITestCase):
59696033
def setUp(self):
@@ -6030,6 +6094,7 @@ def test_validate_challenge_using_success(self):
60306094
self.url,
60316095
{
60326096
"GITHUB_REPOSITORY": "https://github.com/yourusername/repository",
6097+
"GITHUB_BRANCH_NAME": "refs/heads/challenge",
60336098
"zip_configuration": self.input_zip_file,
60346099
},
60356100
format="multipart",

tests/unit/participants/test_views.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -884,6 +884,7 @@ def test_get_teams_and_corresponding_challenges_for_a_participant(self):
884884
"worker_instance_type": self.challenge1.worker_instance_type,
885885
"sqs_retention_period": self.challenge1.sqs_retention_period,
886886
"github_repository": self.challenge1.github_repository,
887+
"github_branch": self.challenge1.github_branch,
887888
},
888889
"participant_team": {
889890
"id": self.participant_team.id,
@@ -981,6 +982,7 @@ def test_get_participant_team_challenge_list(self):
981982
"worker_instance_type": self.challenge1.worker_instance_type,
982983
"sqs_retention_period": self.challenge1.sqs_retention_period,
983984
"github_repository": self.challenge1.github_repository,
985+
"github_branch": self.challenge1.github_branch,
984986
}
985987
]
986988

0 commit comments

Comments
 (0)