From 8d6ae67c5fad8cb261acb2a9bd2f028705cb7fd2 Mon Sep 17 00:00:00 2001 From: Zahed-Riyaz Date: Wed, 9 Jul 2025 14:34:58 +0530 Subject: [PATCH 01/18] Store Github branch with challenge data --- apps/challenges/admin.py | 2 + apps/challenges/challenge_config_utils.py | 3 ++ .../0113_add_github_branch_field.py | 18 +++++++++ apps/challenges/models.py | 4 ++ apps/challenges/serializers.py | 5 +++ apps/challenges/views.py | 3 ++ docker-compose.yml | 2 +- tests/unit/challenges/test_views.py | 38 +++++++++++++++++++ 8 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 apps/challenges/migrations/0113_add_github_branch_field.py diff --git a/apps/challenges/admin.py b/apps/challenges/admin.py index cada930b76..3225d17688 100644 --- a/apps/challenges/admin.py +++ b/apps/challenges/admin.py @@ -58,6 +58,7 @@ class ChallengeAdmin(ImportExportTimeStampedAdmin): "workers", "task_def_arn", "github_repository", + "github_branch", ) list_filter = ( ChallengeFilter, @@ -75,6 +76,7 @@ class ChallengeAdmin(ImportExportTimeStampedAdmin): "creator__team_name", "slug", "github_repository", + "github_branch", ) actions = [ "start_selected_workers", diff --git a/apps/challenges/challenge_config_utils.py b/apps/challenges/challenge_config_utils.py index 5e0c2a3714..a4c683fbdd 100644 --- a/apps/challenges/challenge_config_utils.py +++ b/apps/challenges/challenge_config_utils.py @@ -586,6 +586,9 @@ def validate_serializer(self): "github_repository": self.request.data[ "GITHUB_REPOSITORY" ], + "github_branch": self.request.data.get( + "GITHUB_REF_NAME", "" + ), }, ) if not serializer.is_valid(): diff --git a/apps/challenges/migrations/0113_add_github_branch_field.py b/apps/challenges/migrations/0113_add_github_branch_field.py new file mode 100644 index 0000000000..87a4571e89 --- /dev/null +++ b/apps/challenges/migrations/0113_add_github_branch_field.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.20 on 2025-07-07 14:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('challenges', '0112_challenge_sqs_retention_period'), + ] + + operations = [ + migrations.AddField( + model_name='challenge', + name='github_branch', + field=models.CharField(blank=True, default='', max_length=200, null=True), + ), + ] diff --git a/apps/challenges/models.py b/apps/challenges/models.py index 919c95209f..823309d9d1 100644 --- a/apps/challenges/models.py +++ b/apps/challenges/models.py @@ -186,6 +186,10 @@ def __init__(self, *args, **kwargs): github_repository = models.CharField( max_length=1000, null=True, blank=True, default="" ) + # The github branch name used to create/update the challenge + github_branch = models.CharField( + max_length=200, null=True, blank=True, default="" + ) # The number of vCPU for a Fargate worker for the challenge. Default value # is 0.25 vCPU. worker_cpu_cores = models.IntegerField(null=True, blank=True, default=512) diff --git a/apps/challenges/serializers.py b/apps/challenges/serializers.py index 8be657de57..4e933be0a6 100644 --- a/apps/challenges/serializers.py +++ b/apps/challenges/serializers.py @@ -95,6 +95,7 @@ class Meta: "worker_instance_type", "sqs_retention_period", "github_repository", + "github_branch", ) @@ -256,6 +257,9 @@ def __init__(self, *args, **kwargs): github_repository = context.get("github_repository") if github_repository: kwargs["data"]["github_repository"] = github_repository + github_branch = context.get("github_branch") + if github_branch: + kwargs["data"]["github_branch"] = github_branch class Meta: model = Challenge @@ -294,6 +298,7 @@ class Meta: "max_docker_image_size", "cli_version", "github_repository", + "github_branch", "vpc_cidr", "subnet_1_cidr", "subnet_2_cidr", diff --git a/apps/challenges/views.py b/apps/challenges/views.py index 893505db6d..61885b7957 100644 --- a/apps/challenges/views.py +++ b/apps/challenges/views.py @@ -3979,6 +3979,9 @@ def create_or_update_github_challenge(request, challenge_host_team_pk): "github_repository": request.data[ "GITHUB_REPOSITORY" ], + "github_branch": request.data.get( + "GITHUB_REF_NAME", "" + ), "worker_image_url": worker_image_url, }, ) diff --git a/docker-compose.yml b/docker-compose.yml index a8a1b9a9cb..a89a8ab5dd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: db: - image: postgres:16.8 + image: postgres:10.4 ports: - "5432:5432" env_file: diff --git a/tests/unit/challenges/test_views.py b/tests/unit/challenges/test_views.py index 91c7a04ee3..130bfa10ef 100644 --- a/tests/unit/challenges/test_views.py +++ b/tests/unit/challenges/test_views.py @@ -5911,6 +5911,7 @@ def test_create_challenge_using_github_success(self): self.url, { "GITHUB_REPOSITORY": "https://github.com/yourusername/repository", + "GITHUB_REF_NAME": "refs/heads/challenge", "zip_configuration": self.input_zip_file, }, format="multipart", @@ -5925,6 +5926,11 @@ def test_create_challenge_using_github_success(self): self.assertEqual(DatasetSplit.objects.count(), 1) self.assertEqual(Leaderboard.objects.count(), 1) self.assertEqual(ChallengePhaseSplit.objects.count(), 1) + + # Verify github_branch is properly stored + challenge = Challenge.objects.first() + self.assertEqual(challenge.github_repository, "https://github.com/yourusername/repository") + self.assertEqual(challenge.github_branch, "refs/heads/challenge") def test_create_challenge_using_github_when_challenge_host_team_does_not_exist( self, @@ -5964,6 +5970,37 @@ def test_create_challenge_using_github_when_user_is_not_authenticated( self.assertEqual(list(response.data.values())[0], expected["error"]) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + def test_create_challenge_using_github_without_branch_name(self): + self.url = reverse_lazy( + "challenges:create_or_update_github_challenge", + kwargs={"challenge_host_team_pk": self.challenge_host_team.pk}, + ) + + with mock.patch("challenges.views.requests.get") as m: + resp = mock.Mock() + resp.content = self.test_zip_file.read() + resp.status_code = 200 + m.return_value = resp + response = self.client.post( + self.url, + { + "GITHUB_REPOSITORY": "https://github.com/yourusername/repository", + "zip_configuration": self.input_zip_file, + }, + format="multipart", + ) + expected = { + "Success": "Challenge Challenge Title has been created successfully and sent for review to EvalAI Admin." + } + + self.assertEqual(response.status_code, 201) + self.assertEqual(response.json(), expected) + + # Verify github_branch defaults to empty string when not provided + challenge = Challenge.objects.first() + self.assertEqual(challenge.github_repository, "https://github.com/yourusername/repository") + self.assertEqual(challenge.github_branch, "") + class ValidateChallengeTest(APITestCase): def setUp(self): @@ -6030,6 +6067,7 @@ def test_validate_challenge_using_success(self): self.url, { "GITHUB_REPOSITORY": "https://github.com/yourusername/repository", + "GITHUB_REF_NAME": "refs/heads/challenge", "zip_configuration": self.input_zip_file, }, format="multipart", From 9164bf09aa7a86f93205cc8edeedc387daad00bd Mon Sep 17 00:00:00 2001 From: Zahed-Riyaz Date: Wed, 9 Jul 2025 20:25:08 +0530 Subject: [PATCH 02/18] Modify backend to store github branch --- ..._add_github_repo_branch_unique_constraint.py | 17 +++++++++++++++++ apps/challenges/models.py | 1 + 2 files changed, 18 insertions(+) create mode 100644 apps/challenges/migrations/0114_add_github_repo_branch_unique_constraint.py diff --git a/apps/challenges/migrations/0114_add_github_repo_branch_unique_constraint.py b/apps/challenges/migrations/0114_add_github_repo_branch_unique_constraint.py new file mode 100644 index 0000000000..bf91139fbe --- /dev/null +++ b/apps/challenges/migrations/0114_add_github_repo_branch_unique_constraint.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.20 on 2025-07-09 09:52 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('challenges', '0113_add_github_branch_field'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='challenge', + unique_together={('github_repository', 'github_branch')}, + ), + ] diff --git a/apps/challenges/models.py b/apps/challenges/models.py index 823309d9d1..2100952564 100644 --- a/apps/challenges/models.py +++ b/apps/challenges/models.py @@ -241,6 +241,7 @@ class Meta: app_label = "challenges" db_table = "challenge" ordering = ("title",) + unique_together = (("github_repository", "github_branch"),) def __str__(self): """Returns the title of Challenge""" From 2f6e1a329c1be12abc26211446d88e08869ec276 Mon Sep 17 00:00:00 2001 From: Zahed-Riyaz Date: Wed, 9 Jul 2025 20:50:00 +0530 Subject: [PATCH 03/18] Handle empty branches --- apps/challenges/challenge_config_utils.py | 4 +- ...dd_github_repo_branch_unique_constraint.py | 44 +++++++++++++++++++ apps/challenges/views.py | 12 +++-- tests/unit/challenges/test_views.py | 4 +- 4 files changed, 56 insertions(+), 8 deletions(-) diff --git a/apps/challenges/challenge_config_utils.py b/apps/challenges/challenge_config_utils.py index a4c683fbdd..ace6967203 100644 --- a/apps/challenges/challenge_config_utils.py +++ b/apps/challenges/challenge_config_utils.py @@ -587,8 +587,8 @@ def validate_serializer(self): "GITHUB_REPOSITORY" ], "github_branch": self.request.data.get( - "GITHUB_REF_NAME", "" - ), + "GITHUB_REF_NAME", "challenge" + ) or "challenge", }, ) if not serializer.is_valid(): diff --git a/apps/challenges/migrations/0114_add_github_repo_branch_unique_constraint.py b/apps/challenges/migrations/0114_add_github_repo_branch_unique_constraint.py index bf91139fbe..5ad6287a54 100644 --- a/apps/challenges/migrations/0114_add_github_repo_branch_unique_constraint.py +++ b/apps/challenges/migrations/0114_add_github_repo_branch_unique_constraint.py @@ -1,6 +1,46 @@ # Generated by Django 2.2.20 on 2025-07-09 09:52 from django.db import migrations +import uuid + + +def fix_duplicate_github_fields(apps, schema_editor): + """ + Fix duplicate empty github_repository and github_branch combinations + by assigning unique values to challenges that don't have GitHub info. + """ + Challenge = apps.get_model('challenges', 'Challenge') + + # Find challenges with empty github_repository and github_branch + challenges_with_empty_github = Challenge.objects.filter( + github_repository__in=['', None], + github_branch__in=['', None] + ) + + # Update each challenge to have unique github fields + for challenge in challenges_with_empty_github: + # For non-GitHub challenges, create a unique identifier + unique_id = str(uuid.uuid4())[:8] + challenge.github_repository = f"local-challenge-{challenge.id}-{unique_id}" + challenge.github_branch = "main" + challenge.save() + + +def reverse_fix_duplicate_github_fields(apps, schema_editor): + """ + Reverse the fix by setting github fields back to empty for local challenges. + """ + Challenge = apps.get_model('challenges', 'Challenge') + + # Find challenges that were created as local challenges + local_challenges = Challenge.objects.filter( + github_repository__startswith='local-challenge-' + ) + + for challenge in local_challenges: + challenge.github_repository = '' + challenge.github_branch = '' + challenge.save() class Migration(migrations.Migration): @@ -10,6 +50,10 @@ class Migration(migrations.Migration): ] operations = [ + migrations.RunPython( + fix_duplicate_github_fields, + reverse_fix_duplicate_github_fields, + ), migrations.AlterUniqueTogether( name='challenge', unique_together={('github_repository', 'github_branch')}, diff --git a/apps/challenges/views.py b/apps/challenges/views.py index 61885b7957..70a5aa5380 100644 --- a/apps/challenges/views.py +++ b/apps/challenges/views.py @@ -3897,8 +3897,14 @@ def create_or_update_github_challenge(request, challenge_host_team_pk): response_data = {"error": "ChallengeHostTeam does not exist"} return Response(response_data, status=status.HTTP_406_NOT_ACCEPTABLE) + # Get branch name with default fallback + github_branch = request.data.get("GITHUB_REF_NAME", "challenge") + if not github_branch: + github_branch = "challenge" + challenge_queryset = Challenge.objects.filter( - github_repository=request.data["GITHUB_REPOSITORY"] + github_repository=request.data["GITHUB_REPOSITORY"], + github_branch=github_branch ) if challenge_queryset: @@ -3979,9 +3985,7 @@ def create_or_update_github_challenge(request, challenge_host_team_pk): "github_repository": request.data[ "GITHUB_REPOSITORY" ], - "github_branch": request.data.get( - "GITHUB_REF_NAME", "" - ), + "github_branch": github_branch, "worker_image_url": worker_image_url, }, ) diff --git a/tests/unit/challenges/test_views.py b/tests/unit/challenges/test_views.py index 130bfa10ef..3e391eb9f4 100644 --- a/tests/unit/challenges/test_views.py +++ b/tests/unit/challenges/test_views.py @@ -5996,10 +5996,10 @@ def test_create_challenge_using_github_without_branch_name(self): self.assertEqual(response.status_code, 201) self.assertEqual(response.json(), expected) - # Verify github_branch defaults to empty string when not provided + # Verify github_branch defaults to "challenge" when not provided challenge = Challenge.objects.first() self.assertEqual(challenge.github_repository, "https://github.com/yourusername/repository") - self.assertEqual(challenge.github_branch, "") + self.assertEqual(challenge.github_branch, "challenge") class ValidateChallengeTest(APITestCase): From 69b95359eac45c07e622faa5ff563dbe8e23c733 Mon Sep 17 00:00:00 2001 From: Zahed-Riyaz Date: Wed, 9 Jul 2025 22:42:44 +0530 Subject: [PATCH 04/18] Merge migrations --- .../migrations/0113_add_github_branch_field.py | 18 ------------------ ...thub_branch_field_and_unique_constraint.py} | 11 ++++++++--- 2 files changed, 8 insertions(+), 21 deletions(-) delete mode 100644 apps/challenges/migrations/0113_add_github_branch_field.py rename apps/challenges/migrations/{0114_add_github_repo_branch_unique_constraint.py => 0113_add_github_branch_field_and_unique_constraint.py} (83%) diff --git a/apps/challenges/migrations/0113_add_github_branch_field.py b/apps/challenges/migrations/0113_add_github_branch_field.py deleted file mode 100644 index 87a4571e89..0000000000 --- a/apps/challenges/migrations/0113_add_github_branch_field.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.2.20 on 2025-07-07 14:00 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('challenges', '0112_challenge_sqs_retention_period'), - ] - - operations = [ - migrations.AddField( - model_name='challenge', - name='github_branch', - field=models.CharField(blank=True, default='', max_length=200, null=True), - ), - ] diff --git a/apps/challenges/migrations/0114_add_github_repo_branch_unique_constraint.py b/apps/challenges/migrations/0113_add_github_branch_field_and_unique_constraint.py similarity index 83% rename from apps/challenges/migrations/0114_add_github_repo_branch_unique_constraint.py rename to apps/challenges/migrations/0113_add_github_branch_field_and_unique_constraint.py index 5ad6287a54..2c832186dc 100644 --- a/apps/challenges/migrations/0114_add_github_repo_branch_unique_constraint.py +++ b/apps/challenges/migrations/0113_add_github_branch_field_and_unique_constraint.py @@ -1,6 +1,6 @@ -# Generated by Django 2.2.20 on 2025-07-09 09:52 +# Generated by Django 2.2.20 on 2025-07-09 15:00 -from django.db import migrations +from django.db import migrations, models import uuid @@ -46,10 +46,15 @@ def reverse_fix_duplicate_github_fields(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('challenges', '0113_add_github_branch_field'), + ('challenges', '0112_challenge_sqs_retention_period'), ] operations = [ + migrations.AddField( + model_name='challenge', + name='github_branch', + field=models.CharField(blank=True, default='', max_length=200, null=True), + ), migrations.RunPython( fix_duplicate_github_fields, reverse_fix_duplicate_github_fields, From 4bff095f5f965fa626405bbb8ac7df3d8700380a Mon Sep 17 00:00:00 2001 From: Zahed-Riyaz Date: Wed, 9 Jul 2025 22:44:16 +0530 Subject: [PATCH 05/18] Update seed.py --- scripts/seed.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/seed.py b/scripts/seed.py index ed7b378c9a..353b2a9889 100644 --- a/scripts/seed.py +++ b/scripts/seed.py @@ -255,6 +255,8 @@ def create_challenge( queue=queue, featured=is_featured, image=image_file, + github_repository=f"evalai-examples/{slug}", + github_branch="main", ) challenge.save() From 75a78689713874101ad164d7b3b6aa7cc37130ef Mon Sep 17 00:00:00 2001 From: Zahed-Riyaz Date: Wed, 9 Jul 2025 23:04:41 +0530 Subject: [PATCH 06/18] 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. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 499fe02a0f..c2c4959b18 100755 --- a/.travis.yml +++ b/.travis.yml @@ -81,4 +81,4 @@ notifications: email: on_success: change on_failure: always - slack: cloudcv:gy3CGQGNXLwXOqVyzXGZfdea + slack: cloudcv:gy3CGQGNXLwXOqVyzXGZfdea \ No newline at end of file From e2d7f5333c67fcbd77564de743910483df773fa3 Mon Sep 17 00:00:00 2001 From: Zahed-Riyaz Date: Wed, 9 Jul 2025 23:34:59 +0530 Subject: [PATCH 07/18] Revert unnecessary changes --- apps/challenges/challenge_config_utils.py | 4 +- ...thub_branch_field_and_unique_constraint.py | 40 ++++--------------- apps/challenges/models.py | 1 - apps/challenges/views.py | 4 +- tests/unit/challenges/test_views.py | 4 +- 5 files changed, 13 insertions(+), 40 deletions(-) diff --git a/apps/challenges/challenge_config_utils.py b/apps/challenges/challenge_config_utils.py index ace6967203..a4c683fbdd 100644 --- a/apps/challenges/challenge_config_utils.py +++ b/apps/challenges/challenge_config_utils.py @@ -587,8 +587,8 @@ def validate_serializer(self): "GITHUB_REPOSITORY" ], "github_branch": self.request.data.get( - "GITHUB_REF_NAME", "challenge" - ) or "challenge", + "GITHUB_REF_NAME", "" + ), }, ) if not serializer.is_valid(): diff --git a/apps/challenges/migrations/0113_add_github_branch_field_and_unique_constraint.py b/apps/challenges/migrations/0113_add_github_branch_field_and_unique_constraint.py index 2c832186dc..1efc22cc5c 100644 --- a/apps/challenges/migrations/0113_add_github_branch_field_and_unique_constraint.py +++ b/apps/challenges/migrations/0113_add_github_branch_field_and_unique_constraint.py @@ -6,41 +6,16 @@ def fix_duplicate_github_fields(apps, schema_editor): """ - Fix duplicate empty github_repository and github_branch combinations - by assigning unique values to challenges that don't have GitHub info. + No data migration needed since we're using a partial unique constraint. """ - Challenge = apps.get_model('challenges', 'Challenge') - - # Find challenges with empty github_repository and github_branch - challenges_with_empty_github = Challenge.objects.filter( - github_repository__in=['', None], - github_branch__in=['', None] - ) - - # Update each challenge to have unique github fields - for challenge in challenges_with_empty_github: - # For non-GitHub challenges, create a unique identifier - unique_id = str(uuid.uuid4())[:8] - challenge.github_repository = f"local-challenge-{challenge.id}-{unique_id}" - challenge.github_branch = "main" - challenge.save() + pass def reverse_fix_duplicate_github_fields(apps, schema_editor): """ - Reverse the fix by setting github fields back to empty for local challenges. + No reverse migration needed. """ - Challenge = apps.get_model('challenges', 'Challenge') - - # Find challenges that were created as local challenges - local_challenges = Challenge.objects.filter( - github_repository__startswith='local-challenge-' - ) - - for challenge in local_challenges: - challenge.github_repository = '' - challenge.github_branch = '' - challenge.save() + pass class Migration(migrations.Migration): @@ -59,8 +34,9 @@ class Migration(migrations.Migration): fix_duplicate_github_fields, reverse_fix_duplicate_github_fields, ), - migrations.AlterUniqueTogether( - name='challenge', - unique_together={('github_repository', 'github_branch')}, + # Add a partial unique constraint that only applies when both fields are not empty + migrations.RunSQL( + "CREATE UNIQUE INDEX challenge_github_repo_branch_partial_idx ON challenge (github_repository, github_branch) WHERE github_repository != '' AND github_branch != '';", + reverse_sql="DROP INDEX IF EXISTS challenge_github_repo_branch_partial_idx;" ), ] diff --git a/apps/challenges/models.py b/apps/challenges/models.py index 2100952564..823309d9d1 100644 --- a/apps/challenges/models.py +++ b/apps/challenges/models.py @@ -241,7 +241,6 @@ class Meta: app_label = "challenges" db_table = "challenge" ordering = ("title",) - unique_together = (("github_repository", "github_branch"),) def __str__(self): """Returns the title of Challenge""" diff --git a/apps/challenges/views.py b/apps/challenges/views.py index 70a5aa5380..bd7c7b4d94 100644 --- a/apps/challenges/views.py +++ b/apps/challenges/views.py @@ -3898,9 +3898,7 @@ def create_or_update_github_challenge(request, challenge_host_team_pk): return Response(response_data, status=status.HTTP_406_NOT_ACCEPTABLE) # Get branch name with default fallback - github_branch = request.data.get("GITHUB_REF_NAME", "challenge") - if not github_branch: - github_branch = "challenge" + github_branch = request.data.get("GITHUB_REF_NAME", "") challenge_queryset = Challenge.objects.filter( github_repository=request.data["GITHUB_REPOSITORY"], diff --git a/tests/unit/challenges/test_views.py b/tests/unit/challenges/test_views.py index 3e391eb9f4..130bfa10ef 100644 --- a/tests/unit/challenges/test_views.py +++ b/tests/unit/challenges/test_views.py @@ -5996,10 +5996,10 @@ def test_create_challenge_using_github_without_branch_name(self): self.assertEqual(response.status_code, 201) self.assertEqual(response.json(), expected) - # Verify github_branch defaults to "challenge" when not provided + # Verify github_branch defaults to empty string when not provided challenge = Challenge.objects.first() self.assertEqual(challenge.github_repository, "https://github.com/yourusername/repository") - self.assertEqual(challenge.github_branch, "challenge") + self.assertEqual(challenge.github_branch, "") class ValidateChallengeTest(APITestCase): From f6a06d7d503d1e80c8e686b91990627267e93e4f Mon Sep 17 00:00:00 2001 From: Zahed-Riyaz Date: Thu, 10 Jul 2025 01:35:42 +0530 Subject: [PATCH 08/18] Fix tests --- ...thub_branch_field_and_unique_constraint.py | 12 +++++---- apps/challenges/views.py | 4 +-- docker-compose.yml | 2 +- tests/unit/challenges/test_views.py | 25 +++++++++++++++---- 4 files changed, 30 insertions(+), 13 deletions(-) diff --git a/apps/challenges/migrations/0113_add_github_branch_field_and_unique_constraint.py b/apps/challenges/migrations/0113_add_github_branch_field_and_unique_constraint.py index 1efc22cc5c..2b1a516f84 100644 --- a/apps/challenges/migrations/0113_add_github_branch_field_and_unique_constraint.py +++ b/apps/challenges/migrations/0113_add_github_branch_field_and_unique_constraint.py @@ -21,14 +21,16 @@ def reverse_fix_duplicate_github_fields(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('challenges', '0112_challenge_sqs_retention_period'), + ("challenges", "0112_challenge_sqs_retention_period"), ] operations = [ migrations.AddField( - model_name='challenge', - name='github_branch', - field=models.CharField(blank=True, default='', max_length=200, null=True), + model_name="challenge", + name="github_branch", + field=models.CharField( + blank=True, default="", max_length=200, null=True + ), ), migrations.RunPython( fix_duplicate_github_fields, @@ -37,6 +39,6 @@ class Migration(migrations.Migration): # Add a partial unique constraint that only applies when both fields are not empty migrations.RunSQL( "CREATE UNIQUE INDEX challenge_github_repo_branch_partial_idx ON challenge (github_repository, github_branch) WHERE github_repository != '' AND github_branch != '';", - reverse_sql="DROP INDEX IF EXISTS challenge_github_repo_branch_partial_idx;" + reverse_sql="DROP INDEX IF EXISTS challenge_github_repo_branch_partial_idx;", ), ] diff --git a/apps/challenges/views.py b/apps/challenges/views.py index bd7c7b4d94..f41fb80077 100644 --- a/apps/challenges/views.py +++ b/apps/challenges/views.py @@ -3899,10 +3899,10 @@ def create_or_update_github_challenge(request, challenge_host_team_pk): # Get branch name with default fallback github_branch = request.data.get("GITHUB_REF_NAME", "") - + challenge_queryset = Challenge.objects.filter( github_repository=request.data["GITHUB_REPOSITORY"], - github_branch=github_branch + github_branch=github_branch, ) if challenge_queryset: diff --git a/docker-compose.yml b/docker-compose.yml index a89a8ab5dd..a8a1b9a9cb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: db: - image: postgres:10.4 + image: postgres:16.8 ports: - "5432:5432" env_file: diff --git a/tests/unit/challenges/test_views.py b/tests/unit/challenges/test_views.py index 130bfa10ef..71234444d4 100644 --- a/tests/unit/challenges/test_views.py +++ b/tests/unit/challenges/test_views.py @@ -1,3 +1,4 @@ +import collections import csv import io import json @@ -205,11 +206,14 @@ def test_get_challenge(self): "worker_instance_type": self.challenge.worker_instance_type, "sqs_retention_period": self.challenge.sqs_retention_period, "github_repository": self.challenge.github_repository, + "github_branch": self.challenge.github_branch, } ] response = self.client.get(self.url, {}) - self.assertEqual(response.data["results"], expected) + self.assertEqual( + response.data["results"], json.loads(json.dumps(expected)) + ) self.assertEqual(response.status_code, status.HTTP_200_OK) def test_particular_challenge_host_team_for_challenge_does_not_exist(self): @@ -577,6 +581,7 @@ def test_get_particular_challenge(self): "worker_instance_type": self.challenge.worker_instance_type, "sqs_retention_period": self.challenge.sqs_retention_period, "github_repository": self.challenge.github_repository, + "github_branch": self.challenge.github_branch, } response = self.client.get(self.url, {}) self.assertEqual(response.data, expected) @@ -680,6 +685,7 @@ def test_update_challenge_when_user_is_its_creator(self): "worker_instance_type": self.challenge.worker_instance_type, "sqs_retention_period": self.challenge.sqs_retention_period, "github_repository": self.challenge.github_repository, + "github_branch": self.challenge.github_branch, } response = self.client.put( self.url, {"title": new_title, "description": new_description} @@ -809,6 +815,7 @@ def test_particular_challenge_partial_update(self): "worker_instance_type": self.challenge.worker_instance_type, "sqs_retention_period": self.challenge.sqs_retention_period, "github_repository": self.challenge.github_repository, + "github_branch": self.challenge.github_branch, } response = self.client.patch(self.url, self.partial_update_data) self.assertEqual(response.data, expected) @@ -887,6 +894,7 @@ def test_particular_challenge_update(self): "worker_instance_type": self.challenge.worker_instance_type, "sqs_retention_period": self.challenge.sqs_retention_period, "github_repository": self.challenge.github_repository, + "github_branch": self.challenge.github_branch, } response = self.client.put(self.url, self.data) self.assertEqual(response.data, expected) @@ -2661,6 +2669,7 @@ def test_get_challenge_when_mode_is_host(self): "worker_instance_type": self.challenge.worker_instance_type, "sqs_retention_period": self.challenge.sqs_retention_period, "github_repository": self.challenge.github_repository, + "github_branch": self.challenge.github_branch, }, { "id": self.challenge2.pk, @@ -5926,10 +5935,13 @@ def test_create_challenge_using_github_success(self): self.assertEqual(DatasetSplit.objects.count(), 1) self.assertEqual(Leaderboard.objects.count(), 1) self.assertEqual(ChallengePhaseSplit.objects.count(), 1) - + # Verify github_branch is properly stored challenge = Challenge.objects.first() - self.assertEqual(challenge.github_repository, "https://github.com/yourusername/repository") + self.assertEqual( + challenge.github_repository, + "https://github.com/yourusername/repository", + ) self.assertEqual(challenge.github_branch, "refs/heads/challenge") def test_create_challenge_using_github_when_challenge_host_team_does_not_exist( @@ -5995,10 +6007,13 @@ def test_create_challenge_using_github_without_branch_name(self): self.assertEqual(response.status_code, 201) self.assertEqual(response.json(), expected) - + # Verify github_branch defaults to empty string when not provided challenge = Challenge.objects.first() - self.assertEqual(challenge.github_repository, "https://github.com/yourusername/repository") + self.assertEqual( + challenge.github_repository, + "https://github.com/yourusername/repository", + ) self.assertEqual(challenge.github_branch, "") From 3287cc9f6a6d6caeddde565da5bbe8c9ea3e5d46 Mon Sep 17 00:00:00 2001 From: Zahed-Riyaz Date: Thu, 10 Jul 2025 23:23:29 +0530 Subject: [PATCH 09/18] Update Github branch var --- apps/challenges/challenge_config_utils.py | 2 +- apps/challenges/views.py | 2 +- tests/unit/challenges/test_views.py | 17 +++++++++++++++-- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/apps/challenges/challenge_config_utils.py b/apps/challenges/challenge_config_utils.py index a4c683fbdd..197cbdb358 100644 --- a/apps/challenges/challenge_config_utils.py +++ b/apps/challenges/challenge_config_utils.py @@ -587,7 +587,7 @@ def validate_serializer(self): "GITHUB_REPOSITORY" ], "github_branch": self.request.data.get( - "GITHUB_REF_NAME", "" + "GITHUB_BRANCH_NAME", "" ), }, ) diff --git a/apps/challenges/views.py b/apps/challenges/views.py index f41fb80077..de07ee47c4 100644 --- a/apps/challenges/views.py +++ b/apps/challenges/views.py @@ -3898,7 +3898,7 @@ def create_or_update_github_challenge(request, challenge_host_team_pk): return Response(response_data, status=status.HTTP_406_NOT_ACCEPTABLE) # Get branch name with default fallback - github_branch = request.data.get("GITHUB_REF_NAME", "") + github_branch = request.data.get("GITHUB_BRANCH_NAME", "") challenge_queryset = Challenge.objects.filter( github_repository=request.data["GITHUB_REPOSITORY"], diff --git a/tests/unit/challenges/test_views.py b/tests/unit/challenges/test_views.py index 71234444d4..54f2c55bf8 100644 --- a/tests/unit/challenges/test_views.py +++ b/tests/unit/challenges/test_views.py @@ -1492,6 +1492,7 @@ def test_get_past_challenges(self): "worker_instance_type": self.challenge3.worker_instance_type, "sqs_retention_period": self.challenge3.sqs_retention_period, "github_repository": self.challenge3.github_repository, + "github_branch": self.challenge3.github_branch, } ] response = self.client.get(self.url, {}, format="json") @@ -1576,6 +1577,7 @@ def test_get_present_challenges(self): "worker_instance_type": self.challenge2.worker_instance_type, "sqs_retention_period": self.challenge2.sqs_retention_period, "github_repository": self.challenge2.github_repository, + "github_branch": self.challenge2.github_branch, } ] response = self.client.get(self.url, {}, format="json") @@ -1660,6 +1662,7 @@ def test_get_future_challenges(self): "worker_instance_type": self.challenge4.worker_instance_type, "sqs_retention_period": self.challenge4.sqs_retention_period, "github_repository": self.challenge4.github_repository, + "github_branch": self.challenge4.github_branch, } ] response = self.client.get(self.url, {}, format="json") @@ -1744,6 +1747,7 @@ def test_get_all_challenges(self): "worker_instance_type": self.challenge4.worker_instance_type, "sqs_retention_period": self.challenge4.sqs_retention_period, "github_repository": self.challenge4.github_repository, + "github_branch": self.challenge4.github_branch, }, { "id": self.challenge3.pk, @@ -1812,6 +1816,7 @@ def test_get_all_challenges(self): "worker_instance_type": self.challenge3.worker_instance_type, "sqs_retention_period": self.challenge3.sqs_retention_period, "github_repository": self.challenge3.github_repository, + "github_branch": self.challenge3.github_branch, }, { "id": self.challenge2.pk, @@ -1880,6 +1885,7 @@ def test_get_all_challenges(self): "worker_instance_type": self.challenge2.worker_instance_type, "sqs_retention_period": self.challenge2.sqs_retention_period, "github_repository": self.challenge2.github_repository, + "github_branch": self.challenge2.github_branch, }, ] response = self.client.get(self.url, {}, format="json") @@ -2020,6 +2026,7 @@ def test_get_featured_challenges(self): "worker_instance_type": self.challenge3.worker_instance_type, "sqs_retention_period": self.challenge3.sqs_retention_period, "github_repository": self.challenge3.github_repository, + "github_branch": self.challenge3.github_branch, } ] response = self.client.get(self.url, {}, format="json") @@ -2185,6 +2192,7 @@ def test_get_challenge_by_pk_when_user_is_challenge_host(self): "worker_instance_type": self.challenge3.worker_instance_type, "sqs_retention_period": self.challenge3.sqs_retention_period, "github_repository": self.challenge3.github_repository, + "github_branch": self.challenge3.github_branch, } response = self.client.get(self.url, {}) @@ -2277,6 +2285,7 @@ def test_get_challenge_by_pk_when_user_is_participant(self): "worker_instance_type": self.challenge4.worker_instance_type, "sqs_retention_period": self.challenge4.sqs_retention_period, "github_repository": self.challenge4.github_repository, + "github_branch": self.challenge4.github_branch, } self.client.force_authenticate(user=self.user1) @@ -2431,6 +2440,7 @@ def test_get_challenge_when_host_team_is_given(self): "worker_instance_type": self.challenge2.worker_instance_type, "sqs_retention_period": self.challenge2.sqs_retention_period, "github_repository": self.challenge2.github_repository, + "github_branch": self.challenge2.github_branch, } ] @@ -2511,6 +2521,7 @@ def test_get_challenge_when_participant_team_is_given(self): "worker_instance_type": self.challenge2.worker_instance_type, "sqs_retention_period": self.challenge2.sqs_retention_period, "github_repository": self.challenge2.github_repository, + "github_branch": self.challenge2.github_branch, } ] @@ -2591,6 +2602,7 @@ def test_get_challenge_when_mode_is_participant(self): "worker_instance_type": self.challenge2.worker_instance_type, "sqs_retention_period": self.challenge2.sqs_retention_period, "github_repository": self.challenge2.github_repository, + "github_branch": self.challenge2.github_branch, } ] @@ -2738,6 +2750,7 @@ def test_get_challenge_when_mode_is_host(self): "worker_instance_type": self.challenge2.worker_instance_type, "sqs_retention_period": self.challenge2.sqs_retention_period, "github_repository": self.challenge2.github_repository, + "github_branch": self.challenge2.github_branch, }, ] @@ -5920,7 +5933,7 @@ def test_create_challenge_using_github_success(self): self.url, { "GITHUB_REPOSITORY": "https://github.com/yourusername/repository", - "GITHUB_REF_NAME": "refs/heads/challenge", + "GITHUB_BRANCH_NAME": "refs/heads/challenge", "zip_configuration": self.input_zip_file, }, format="multipart", @@ -6082,7 +6095,7 @@ def test_validate_challenge_using_success(self): self.url, { "GITHUB_REPOSITORY": "https://github.com/yourusername/repository", - "GITHUB_REF_NAME": "refs/heads/challenge", + "GITHUB_BRANCH_NAME": "refs/heads/challenge", "zip_configuration": self.input_zip_file, }, format="multipart", From a1bd1ea40a576dbcaa11d7445439dc6a1217cf85 Mon Sep 17 00:00:00 2001 From: Zahed-Riyaz Date: Fri, 11 Jul 2025 00:26:40 +0530 Subject: [PATCH 10/18] Fix failing tests --- tests/unit/participants/test_views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unit/participants/test_views.py b/tests/unit/participants/test_views.py index 89539ede1f..eaa3327fd9 100644 --- a/tests/unit/participants/test_views.py +++ b/tests/unit/participants/test_views.py @@ -884,6 +884,7 @@ def test_get_teams_and_corresponding_challenges_for_a_participant(self): "worker_instance_type": self.challenge1.worker_instance_type, "sqs_retention_period": self.challenge1.sqs_retention_period, "github_repository": self.challenge1.github_repository, + "github_branch": self.challenge1.github_branch, }, "participant_team": { "id": self.participant_team.id, @@ -981,6 +982,7 @@ def test_get_participant_team_challenge_list(self): "worker_instance_type": self.challenge1.worker_instance_type, "sqs_retention_period": self.challenge1.sqs_retention_period, "github_repository": self.challenge1.github_repository, + "github_branch": self.challenge1.github_branch, } ] From 6bbcea7610f6bb6b6f4b9ca46bb129c293f40701 Mon Sep 17 00:00:00 2001 From: Zahed-Riyaz Date: Fri, 11 Jul 2025 00:34:11 +0530 Subject: [PATCH 11/18] Pass flake8 and pylint tests --- .../0113_add_github_branch_field_and_unique_constraint.py | 1 - tests/unit/challenges/test_views.py | 1 - 2 files changed, 2 deletions(-) diff --git a/apps/challenges/migrations/0113_add_github_branch_field_and_unique_constraint.py b/apps/challenges/migrations/0113_add_github_branch_field_and_unique_constraint.py index 2b1a516f84..4ff6d35e3b 100644 --- a/apps/challenges/migrations/0113_add_github_branch_field_and_unique_constraint.py +++ b/apps/challenges/migrations/0113_add_github_branch_field_and_unique_constraint.py @@ -1,7 +1,6 @@ # Generated by Django 2.2.20 on 2025-07-09 15:00 from django.db import migrations, models -import uuid def fix_duplicate_github_fields(apps, schema_editor): diff --git a/tests/unit/challenges/test_views.py b/tests/unit/challenges/test_views.py index 54f2c55bf8..d62311d10f 100644 --- a/tests/unit/challenges/test_views.py +++ b/tests/unit/challenges/test_views.py @@ -1,4 +1,3 @@ -import collections import csv import io import json From 4188b9c53df38422dfd64c4a42441b93cb6a8af5 Mon Sep 17 00:00:00 2001 From: Zahed-Riyaz Date: Thu, 17 Jul 2025 04:14:07 +0530 Subject: [PATCH 12/18] Add github_branch field to backend --- apps/challenges/challenge_config_utils.py | 2 +- apps/challenges/models.py | 2 +- apps/challenges/views.py | 4 +++- scripts/seed.py | 2 +- tests/unit/challenges/test_views.py | 4 ++-- 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/apps/challenges/challenge_config_utils.py b/apps/challenges/challenge_config_utils.py index 197cbdb358..8e60c93efe 100644 --- a/apps/challenges/challenge_config_utils.py +++ b/apps/challenges/challenge_config_utils.py @@ -587,7 +587,7 @@ def validate_serializer(self): "GITHUB_REPOSITORY" ], "github_branch": self.request.data.get( - "GITHUB_BRANCH_NAME", "" + "GITHUB_BRANCH_NAME", "challenge" ), }, ) diff --git a/apps/challenges/models.py b/apps/challenges/models.py index 823309d9d1..acdcd5925a 100644 --- a/apps/challenges/models.py +++ b/apps/challenges/models.py @@ -188,7 +188,7 @@ def __init__(self, *args, **kwargs): ) # The github branch name used to create/update the challenge github_branch = models.CharField( - max_length=200, null=True, blank=True, default="" + max_length=200, null=True, blank=True, default="challenge" ) # The number of vCPU for a Fargate worker for the challenge. Default value # is 0.25 vCPU. diff --git a/apps/challenges/views.py b/apps/challenges/views.py index de07ee47c4..4f051f8194 100644 --- a/apps/challenges/views.py +++ b/apps/challenges/views.py @@ -3898,7 +3898,7 @@ def create_or_update_github_challenge(request, challenge_host_team_pk): return Response(response_data, status=status.HTTP_406_NOT_ACCEPTABLE) # Get branch name with default fallback - github_branch = request.data.get("GITHUB_BRANCH_NAME", "") + github_branch = request.data.get("GITHUB_BRANCH_NAME") or request.data.get("BRANCH_NAME", "challenge") challenge_queryset = Challenge.objects.filter( github_repository=request.data["GITHUB_REPOSITORY"], @@ -4288,6 +4288,8 @@ def create_or_update_github_challenge(request, challenge_host_team_pk): "challenge_evaluation_script_file" ], "worker_image_url": worker_image_url, + "github_repository": request.data["GITHUB_REPOSITORY"], + "github_branch": github_branch, }, ) if serializer.is_valid(): diff --git a/scripts/seed.py b/scripts/seed.py index 353b2a9889..4cfe1eddda 100644 --- a/scripts/seed.py +++ b/scripts/seed.py @@ -256,7 +256,7 @@ def create_challenge( featured=is_featured, image=image_file, github_repository=f"evalai-examples/{slug}", - github_branch="main", + github_branch="challenge", ) challenge.save() diff --git a/tests/unit/challenges/test_views.py b/tests/unit/challenges/test_views.py index d62311d10f..59c8e63ceb 100644 --- a/tests/unit/challenges/test_views.py +++ b/tests/unit/challenges/test_views.py @@ -6020,13 +6020,13 @@ def test_create_challenge_using_github_without_branch_name(self): self.assertEqual(response.status_code, 201) self.assertEqual(response.json(), expected) - # Verify github_branch defaults to empty string when not provided + # Verify github_branch defaults to "challenge" when not provided challenge = Challenge.objects.first() self.assertEqual( challenge.github_repository, "https://github.com/yourusername/repository", ) - self.assertEqual(challenge.github_branch, "") + self.assertEqual(challenge.github_branch, "challenge") class ValidateChallengeTest(APITestCase): From 831fec2a1415069dfac80a9a44f74f0147163e8f Mon Sep 17 00:00:00 2001 From: Zahed-Riyaz Date: Thu, 17 Jul 2025 04:59:54 +0530 Subject: [PATCH 13/18] Add scripts for populating field --- apps/challenges/challenge_config_utils.py | 21 +++++ ...thub_branch_field_and_unique_constraint.py | 34 +++++++- apps/challenges/views.py | 4 +- scripts/migration/populate_github_branch.py | 79 +++++++++++++++++++ 4 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 scripts/migration/populate_github_branch.py diff --git a/apps/challenges/challenge_config_utils.py b/apps/challenges/challenge_config_utils.py index 8e60c93efe..bbbdb499d5 100644 --- a/apps/challenges/challenge_config_utils.py +++ b/apps/challenges/challenge_config_utils.py @@ -312,6 +312,7 @@ def get_value_from_field(data, base_location, field_name): "challenge_metadata_schema_errors": "ERROR: Unable to serialize the challenge because of the following errors: {}.", "evaluation_script_not_zip": "ERROR: Please pass in a zip file as evaluation script. If using the `evaluation_script` directory (recommended), it should be `evaluation_script.zip`.", "docker_based_challenge": "ERROR: New Docker based challenges are not supported starting March 15, 2025.", + "invalid_github_branch_format": "ERROR: GitHub branch name '{branch}' is invalid. It must match the pattern 'challenge--' (e.g., challenge-2024-1).", } @@ -364,6 +365,23 @@ def __init__( self.phase_ids = [] self.leaderboard_ids = [] + def validate_github_branch_format(self): + """ + Ensure the github branch name matches challenge-- + """ + branch = self.request.data.get( + "GITHUB_BRANCH_NAME" + ) or self.request.data.get("BRANCH_NAME") + if not branch: + branch = "challenge" + pattern = r"^challenge-\d{4}-\d+$" + if not re.match(pattern, branch): + self.error_messages.append( + self.error_messages_dict[ + "invalid_github_branch_format" + ].format(branch=branch) + ) + def read_and_validate_yaml(self): if not self.yaml_file_count: message = self.error_messages_dict.get("no_yaml_file") @@ -1134,6 +1152,9 @@ def validate_challenge_config_util( val_config_util.validate_serializer() + # Add branch format validation + val_config_util.validate_github_branch_format() + # Get existing config IDs for leaderboards and dataset splits if current_challenge: current_challenge_phases = ChallengePhase.objects.filter( diff --git a/apps/challenges/migrations/0113_add_github_branch_field_and_unique_constraint.py b/apps/challenges/migrations/0113_add_github_branch_field_and_unique_constraint.py index 4ff6d35e3b..28aa45e5d9 100644 --- a/apps/challenges/migrations/0113_add_github_branch_field_and_unique_constraint.py +++ b/apps/challenges/migrations/0113_add_github_branch_field_and_unique_constraint.py @@ -3,6 +3,33 @@ from django.db import migrations, models +def populate_github_branch_default(apps, schema_editor): + """ + Populate existing challenges with empty github_branch fields to use "challenge" as default. + """ + Challenge = apps.get_model("challenges", "Challenge") + # Update all challenges that have github_repository but empty github_branch + Challenge.objects.filter(github_repository__isnull=False).exclude( + github_repository="" + ).filter(github_branch__isnull=True).update(github_branch="challenge") + + # Also update challenges with empty string github_branch + Challenge.objects.filter(github_repository__isnull=False).exclude( + github_repository="" + ).filter(github_branch="").update(github_branch="challenge") + + +def reverse_populate_github_branch_default(apps, schema_editor): + """ + Reverse migration - set github_branch back to empty string for challenges that were set to "challenge". + """ + Challenge = apps.get_model("challenges", "Challenge") + # Only reverse if the field was set to "challenge" by this migration + Challenge.objects.filter(github_repository__isnull=False).exclude( + github_repository="" + ).filter(github_branch="challenge").update(github_branch="") + + def fix_duplicate_github_fields(apps, schema_editor): """ No data migration needed since we're using a partial unique constraint. @@ -28,9 +55,14 @@ class Migration(migrations.Migration): model_name="challenge", name="github_branch", field=models.CharField( - blank=True, default="", max_length=200, null=True + blank=True, default="challenge", max_length=200, null=True ), ), + # Data migration to populate existing records + migrations.RunPython( + populate_github_branch_default, + reverse_populate_github_branch_default, + ), migrations.RunPython( fix_duplicate_github_fields, reverse_fix_duplicate_github_fields, diff --git a/apps/challenges/views.py b/apps/challenges/views.py index 4f051f8194..ec84ec52d4 100644 --- a/apps/challenges/views.py +++ b/apps/challenges/views.py @@ -3898,7 +3898,9 @@ def create_or_update_github_challenge(request, challenge_host_team_pk): return Response(response_data, status=status.HTTP_406_NOT_ACCEPTABLE) # Get branch name with default fallback - github_branch = request.data.get("GITHUB_BRANCH_NAME") or request.data.get("BRANCH_NAME", "challenge") + github_branch = request.data.get("GITHUB_BRANCH_NAME") or request.data.get( + "BRANCH_NAME", "challenge" + ) challenge_queryset = Challenge.objects.filter( github_repository=request.data["GITHUB_REPOSITORY"], diff --git a/scripts/migration/populate_github_branch.py b/scripts/migration/populate_github_branch.py new file mode 100644 index 0000000000..99aaaa5858 --- /dev/null +++ b/scripts/migration/populate_github_branch.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python +# Command to run: python manage.py shell < scripts/migration/populate_github_branch.py +""" +Populate existing challenges with github_branch="challenge" for backward compatibility. + +This script should be run after the migration to ensure all existing challenges +have the github_branch field populated with the default value. +""" + +import traceback + +from challenges.models import Challenge +from django.db import models + + +def populate_github_branch_fields(): + """ + Populate existing challenges with empty github_branch fields to use "challenge" as default. + """ + print("Starting github_branch field population...") + + challenges_to_update = ( + Challenge.objects.filter(github_repository__isnull=False) + .exclude(github_repository="") + .filter( + models.Q(github_branch__isnull=True) | models.Q(github_branch="") + ) + ) + + count = challenges_to_update.count() + + if count == 0: + print("No challenges found that need github_branch population.") + return + + print(f"Found {count} challenges that need github_branch population.") + + updated_count = challenges_to_update.update(github_branch="challenge") + + print( + f"Successfully updated {updated_count} challenges with github_branch='challenge'" + ) + + remaining_empty = ( + Challenge.objects.filter(github_repository__isnull=False) + .exclude(github_repository="") + .filter( + models.Q(github_branch__isnull=True) | models.Q(github_branch="") + ) + .count() + ) + + if remaining_empty == 0: + print("✅ All challenges now have github_branch populated!") + else: + print( + f"⚠️ Warning: {remaining_empty} challenges still have empty github_branch fields" + ) + + sample_challenges = ( + Challenge.objects.filter(github_repository__isnull=False) + .exclude(github_repository="") + .values("id", "title", "github_repository", "github_branch")[:5] + ) + + print("\nSample updated challenges:") + for challenge in sample_challenges: + print( + f" ID: {challenge['id']}, Title: {challenge['title']}, " + f"Repo: {challenge['github_repository']}, Branch: {challenge['github_branch']}" + ) + + +try: + populate_github_branch_fields() + print("\n✅ Script completed successfully!") +except Exception as e: + print(f"\n❌ Error occurred: {e}") + print(traceback.print_exc()) From 8af18d5e391494ff551bc68020c43ad215ba33b8 Mon Sep 17 00:00:00 2001 From: Zahed-Riyaz Date: Thu, 17 Jul 2025 05:41:26 +0530 Subject: [PATCH 14/18] Allow alphanumeric values for branch name --- apps/challenges/challenge_config_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/challenges/challenge_config_utils.py b/apps/challenges/challenge_config_utils.py index c7c10566b7..b94b8df12f 100644 --- a/apps/challenges/challenge_config_utils.py +++ b/apps/challenges/challenge_config_utils.py @@ -312,7 +312,7 @@ def get_value_from_field(data, base_location, field_name): "challenge_metadata_schema_errors": "ERROR: Unable to serialize the challenge because of the following errors: {}.", "evaluation_script_not_zip": "ERROR: Please pass in a zip file as evaluation script. If using the `evaluation_script` directory (recommended), it should be `evaluation_script.zip`.", "docker_based_challenge": "ERROR: New Docker based challenges are not supported starting March 15, 2025.", - "invalid_github_branch_format": "ERROR: GitHub branch name '{branch}' is invalid. It must match the pattern 'challenge--' (e.g., challenge-2024-1).", + "invalid_github_branch_format": "ERROR: GitHub branch name '{branch}' is invalid. It must match the pattern 'challenge--' (e.g., challenge-2024-1, challenge-2060-v2).", } @@ -374,7 +374,7 @@ def validate_github_branch_format(self): ) or self.request.data.get("BRANCH_NAME") if not branch: branch = "challenge" - pattern = r"^challenge-\d{4}-\d+$" + pattern = r"^challenge-\d{4}-[a-zA-Z0-9]+$" if not re.match(pattern, branch): self.error_messages.append( self.error_messages_dict[ From 350936f1afe97b3084c79ba40ad976c1b5c2c9ac Mon Sep 17 00:00:00 2001 From: Zahed-Riyaz Date: Thu, 17 Jul 2025 05:55:15 +0530 Subject: [PATCH 15/18] Remove duplicate params --- apps/challenges/models.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/challenges/models.py b/apps/challenges/models.py index 6848d7c3d7..d24be100f0 100644 --- a/apps/challenges/models.py +++ b/apps/challenges/models.py @@ -188,9 +188,7 @@ def __init__(self, *args, **kwargs): ) # The github branch name used to create/update the challenge github_branch = models.CharField( - max_length=200, null=True, blank=True, default="challenge" - max_length=200, null=True, blank=True, default="" - + max_length=200, null=True, blank=True, default=challenge ) # The number of vCPU for a Fargate worker for the challenge. Default value # is 0.25 vCPU. From fb379aab5f2dd3ef82448e98b20f737eb8c21e83 Mon Sep 17 00:00:00 2001 From: Zahed-Riyaz Date: Thu, 17 Jul 2025 14:52:00 +0530 Subject: [PATCH 16/18] Update migrations and fallback logic --- ...thub_branch_field_and_unique_constraint.py | 49 ------------------- apps/challenges/models.py | 2 +- apps/challenges/views.py | 12 +++-- 3 files changed, 9 insertions(+), 54 deletions(-) diff --git a/apps/challenges/migrations/0113_add_github_branch_field_and_unique_constraint.py b/apps/challenges/migrations/0113_add_github_branch_field_and_unique_constraint.py index c12aa40ffd..6a315f6379 100644 --- a/apps/challenges/migrations/0113_add_github_branch_field_and_unique_constraint.py +++ b/apps/challenges/migrations/0113_add_github_branch_field_and_unique_constraint.py @@ -3,46 +3,6 @@ from django.db import migrations, models -def populate_github_branch_default(apps, schema_editor): - """ - Populate existing challenges with empty github_branch fields to use "challenge" as default. - """ - Challenge = apps.get_model("challenges", "Challenge") - # Update all challenges that have github_repository but empty github_branch - Challenge.objects.filter(github_repository__isnull=False).exclude( - github_repository="" - ).filter(github_branch__isnull=True).update(github_branch="challenge") - - # Also update challenges with empty string github_branch - Challenge.objects.filter(github_repository__isnull=False).exclude( - github_repository="" - ).filter(github_branch="").update(github_branch="challenge") - - -def reverse_populate_github_branch_default(apps, schema_editor): - """ - Reverse migration - set github_branch back to empty string for challenges that were set to "challenge". - """ - Challenge = apps.get_model("challenges", "Challenge") - # Only reverse if the field was set to "challenge" by this migration - Challenge.objects.filter(github_repository__isnull=False).exclude( - github_repository="" - ).filter(github_branch="challenge").update(github_branch="") - -def fix_duplicate_github_fields(apps, schema_editor): - """ - No data migration needed since we're using a partial unique constraint. - """ - pass - - -def reverse_fix_duplicate_github_fields(apps, schema_editor): - """ - No reverse migration needed. - """ - pass - - class Migration(migrations.Migration): dependencies = [ @@ -57,15 +17,6 @@ class Migration(migrations.Migration): blank=True, default="challenge", max_length=200, null=True ), ), - # Data migration to populate existing records - migrations.RunPython( - populate_github_branch_default, - reverse_populate_github_branch_default, - ), - migrations.RunPython( - fix_duplicate_github_fields, - reverse_fix_duplicate_github_fields, - ), # Add a partial unique constraint that only applies when both fields are not empty migrations.RunSQL( "CREATE UNIQUE INDEX challenge_github_repo_branch_partial_idx ON challenge (github_repository, github_branch) WHERE github_repository != '' AND github_branch != '';", diff --git a/apps/challenges/models.py b/apps/challenges/models.py index d24be100f0..acdcd5925a 100644 --- a/apps/challenges/models.py +++ b/apps/challenges/models.py @@ -188,7 +188,7 @@ def __init__(self, *args, **kwargs): ) # The github branch name used to create/update the challenge github_branch = models.CharField( - max_length=200, null=True, blank=True, default=challenge + max_length=200, null=True, blank=True, default="challenge" ) # The number of vCPU for a Fargate worker for the challenge. Default value # is 0.25 vCPU. diff --git a/apps/challenges/views.py b/apps/challenges/views.py index 7f0a052524..88f89070ae 100644 --- a/apps/challenges/views.py +++ b/apps/challenges/views.py @@ -3897,10 +3897,14 @@ def create_or_update_github_challenge(request, challenge_host_team_pk): response_data = {"error": "ChallengeHostTeam does not exist"} return Response(response_data, status=status.HTTP_406_NOT_ACCEPTABLE) - # Get branch name with default fallback - github_branch = request.data.get("GITHUB_BRANCH_NAME") or request.data.get( - "BRANCH_NAME", "challenge" - ) + # Get branch name - required for GitHub-based challenges + github_branch = request.data.get("GITHUB_BRANCH_NAME") or request.data.get("BRANCH_NAME") + if not github_branch: + response_data = { + "error": "GITHUB_BRANCH_NAME or BRANCH_NAME is required for GitHub-based challenges. See EvalAI docs for details." + } + return Response(response_data, status=status.HTTP_400_BAD_REQUEST) + challenge_queryset = Challenge.objects.filter( github_repository=request.data["GITHUB_REPOSITORY"], github_branch=github_branch, From 8a8e2d8aeef267857b00c96b9bcabd181e1b7fb0 Mon Sep 17 00:00:00 2001 From: Zahed-Riyaz Date: Thu, 17 Jul 2025 22:06:10 +0530 Subject: [PATCH 17/18] Update branch validation logic --- apps/challenges/challenge_config_utils.py | 31 ++- apps/challenges/views.py | 9 +- tests/unit/challenges/test_views.py | 310 ++++++++++++++++++---- 3 files changed, 272 insertions(+), 78 deletions(-) diff --git a/apps/challenges/challenge_config_utils.py b/apps/challenges/challenge_config_utils.py index b94b8df12f..10f064936c 100644 --- a/apps/challenges/challenge_config_utils.py +++ b/apps/challenges/challenge_config_utils.py @@ -368,19 +368,27 @@ def __init__( def validate_github_branch_format(self): """ Ensure the github branch name matches challenge-- + For new challenges, enforce strict format. For existing challenges, allow "challenge" fallback. """ - branch = self.request.data.get( - "GITHUB_BRANCH_NAME" - ) or self.request.data.get("BRANCH_NAME") - if not branch: - branch = "challenge" + branch = self.request.data.get("GITHUB_BRANCH_NAME", "challenge") pattern = r"^challenge-\d{4}-[a-zA-Z0-9]+$" - if not re.match(pattern, branch): - self.error_messages.append( - self.error_messages_dict[ - "invalid_github_branch_format" - ].format(branch=branch) - ) + + # For new challenge creation (when current_challenge is None), enforce strict format + if not self.current_challenge: + if not re.match(pattern, branch): + self.error_messages.append( + self.error_messages_dict[ + "invalid_github_branch_format" + ].format(branch=branch) + ) + else: + # For existing challenges, allow "challenge" fallback but still validate other formats + if branch != "challenge" and not re.match(pattern, branch): + self.error_messages.append( + self.error_messages_dict[ + "invalid_github_branch_format" + ].format(branch=branch) + ) def read_and_validate_yaml(self): if not self.yaml_file_count: @@ -606,7 +614,6 @@ def validate_serializer(self): ], "github_branch": self.request.data.get( "GITHUB_BRANCH_NAME", "challenge" - ), }, ) diff --git a/apps/challenges/views.py b/apps/challenges/views.py index 88f89070ae..ab56ca26f0 100644 --- a/apps/challenges/views.py +++ b/apps/challenges/views.py @@ -3898,13 +3898,8 @@ def create_or_update_github_challenge(request, challenge_host_team_pk): return Response(response_data, status=status.HTTP_406_NOT_ACCEPTABLE) # Get branch name - required for GitHub-based challenges - github_branch = request.data.get("GITHUB_BRANCH_NAME") or request.data.get("BRANCH_NAME") - if not github_branch: - response_data = { - "error": "GITHUB_BRANCH_NAME or BRANCH_NAME is required for GitHub-based challenges. See EvalAI docs for details." - } - return Response(response_data, status=status.HTTP_400_BAD_REQUEST) - + # Uses GITHUB_BRANCH_NAME with fallback to "challenge" for backward compatibility + github_branch = request.data.get("GITHUB_BRANCH_NAME", "challenge") challenge_queryset = Challenge.objects.filter( github_repository=request.data["GITHUB_REPOSITORY"], github_branch=github_branch, diff --git a/tests/unit/challenges/test_views.py b/tests/unit/challenges/test_views.py index cff58da861..2425605453 100644 --- a/tests/unit/challenges/test_views.py +++ b/tests/unit/challenges/test_views.py @@ -2317,7 +2317,7 @@ def setUp(self): permissions=ChallengeHost.ADMIN, ) - self.challenge = Challenge.objects.create( + self.challenge1 = Challenge.objects.create( title="Test Challenge", short_description="Short description for test challenge", description="Description for test challenge", @@ -2334,7 +2334,7 @@ def setUp(self): start_date=timezone.now() - timedelta(days=2), end_date=timezone.now() + timedelta(days=1), approved_by_admin=True, - github_repository="challenge/github_repo", + github_repository="challenge1/github_repo", ) self.challenge2 = Challenge.objects.create( @@ -2614,73 +2614,73 @@ def test_get_challenge_when_mode_is_host(self): expected = [ { - "id": self.challenge.pk, - "title": self.challenge.title, - "short_description": self.challenge.short_description, - "description": self.challenge.description, - "terms_and_conditions": self.challenge.terms_and_conditions, - "submission_guidelines": self.challenge.submission_guidelines, - "evaluation_details": self.challenge.evaluation_details, + "id": self.challenge1.pk, + "title": self.challenge1.title, + "short_description": self.challenge1.short_description, + "description": self.challenge1.description, + "terms_and_conditions": self.challenge1.terms_and_conditions, + "submission_guidelines": self.challenge1.submission_guidelines, + "evaluation_details": self.challenge1.evaluation_details, "image": None, "start_date": "{0}{1}".format( - self.challenge.start_date.isoformat(), "Z" + self.challenge1.start_date.isoformat(), "Z" ).replace("+00:00", ""), "end_date": "{0}{1}".format( - self.challenge.end_date.isoformat(), "Z" + self.challenge1.end_date.isoformat(), "Z" ).replace("+00:00", ""), "creator": { - "id": self.challenge.creator.pk, - "team_name": self.challenge.creator.team_name, - "created_by": self.challenge.creator.created_by.username, - "team_url": self.challenge.creator.team_url, + "id": self.challenge1.creator.pk, + "team_name": self.challenge1.creator.team_name, + "created_by": self.challenge1.creator.created_by.username, + "team_url": self.challenge1.creator.team_url, }, - "domain": self.challenge.domain, + "domain": self.challenge1.domain, "domain_name": "Computer Vision", - "list_tags": self.challenge.list_tags, - "has_prize": self.challenge.has_prize, - "has_sponsors": self.challenge.has_sponsors, - "published": self.challenge.published, - "submission_time_limit": self.challenge.submission_time_limit, - "is_registration_open": self.challenge.is_registration_open, - "enable_forum": self.challenge.enable_forum, - "leaderboard_description": self.challenge.leaderboard_description, - "anonymous_leaderboard": self.challenge.anonymous_leaderboard, - "manual_participant_approval": self.challenge.manual_participant_approval, + "list_tags": self.challenge1.list_tags, + "has_prize": self.challenge1.has_prize, + "has_sponsors": self.challenge1.has_sponsors, + "published": self.challenge1.published, + "submission_time_limit": self.challenge1.submission_time_limit, + "is_registration_open": self.challenge1.is_registration_open, + "enable_forum": self.challenge1.enable_forum, + "leaderboard_description": self.challenge1.leaderboard_description, + "anonymous_leaderboard": self.challenge1.anonymous_leaderboard, + "manual_participant_approval": self.challenge1.manual_participant_approval, "is_active": True, "allowed_email_domains": [], "blocked_email_domains": [], "banned_email_ids": [], "approved_by_admin": True, - "forum_url": self.challenge.forum_url, - "is_docker_based": self.challenge.is_docker_based, - "is_static_dataset_code_upload": self.challenge.is_static_dataset_code_upload, - "slug": self.challenge.slug, - "max_docker_image_size": self.challenge.max_docker_image_size, - "cli_version": self.challenge.cli_version, - "remote_evaluation": self.challenge.remote_evaluation, - "allow_resuming_submissions": self.challenge.allow_resuming_submissions, - "allow_host_cancel_submissions": self.challenge.allow_host_cancel_submissions, - "allow_cancel_running_submissions": self.challenge.allow_cancel_running_submissions, - "allow_participants_resubmissions": self.challenge.allow_participants_resubmissions, - "workers": self.challenge.workers, + "forum_url": self.challenge1.forum_url, + "is_docker_based": self.challenge1.is_docker_based, + "is_static_dataset_code_upload": self.challenge1.is_static_dataset_code_upload, + "slug": self.challenge1.slug, + "max_docker_image_size": self.challenge1.max_docker_image_size, + "cli_version": self.challenge1.cli_version, + "remote_evaluation": self.challenge1.remote_evaluation, + "allow_resuming_submissions": self.challenge1.allow_resuming_submissions, + "allow_host_cancel_submissions": self.challenge1.allow_host_cancel_submissions, + "allow_cancel_running_submissions": self.challenge1.allow_cancel_running_submissions, + "allow_participants_resubmissions": self.challenge1.allow_participants_resubmissions, + "workers": self.challenge1.workers, "created_at": "{0}{1}".format( - self.challenge.created_at.isoformat(), "Z" + self.challenge1.created_at.isoformat(), "Z" ).replace("+00:00", ""), - "queue": self.challenge.queue, + "queue": self.challenge1.queue, "worker_cpu_cores": 512, "worker_memory": 1024, - "cpu_only_jobs": self.challenge.cpu_only_jobs, - "job_cpu_cores": self.challenge.job_cpu_cores, - "job_memory": self.challenge.job_memory, - "uses_ec2_worker": self.challenge.uses_ec2_worker, - "evaluation_module_error": self.challenge.evaluation_module_error, - "ec2_storage": self.challenge.ec2_storage, - "ephemeral_storage": self.challenge.ephemeral_storage, - "worker_image_url": self.challenge.worker_image_url, - "worker_instance_type": self.challenge.worker_instance_type, - "sqs_retention_period": self.challenge.sqs_retention_period, - "github_repository": self.challenge.github_repository, - "github_branch": self.challenge.github_branch, + "cpu_only_jobs": self.challenge1.cpu_only_jobs, + "job_cpu_cores": self.challenge1.job_cpu_cores, + "job_memory": self.challenge1.job_memory, + "uses_ec2_worker": self.challenge1.uses_ec2_worker, + "evaluation_module_error": self.challenge1.evaluation_module_error, + "ec2_storage": self.challenge1.ec2_storage, + "ephemeral_storage": self.challenge1.ephemeral_storage, + "worker_image_url": self.challenge1.worker_image_url, + "worker_instance_type": self.challenge1.worker_instance_type, + "sqs_retention_period": self.challenge1.sqs_retention_period, + "github_repository": self.challenge1.github_repository, + "github_branch": self.challenge1.github_branch, }, { "id": self.challenge2.pk, @@ -5932,7 +5932,7 @@ def test_create_challenge_using_github_success(self): self.url, { "GITHUB_REPOSITORY": "https://github.com/yourusername/repository", - "GITHUB_BRANCH_NAME": "refs/heads/challenge", + "GITHUB_BRANCH_NAME": "challenge-2025-v1", "zip_configuration": self.input_zip_file, }, format="multipart", @@ -5954,7 +5954,7 @@ def test_create_challenge_using_github_success(self): challenge.github_repository, "https://github.com/yourusername/repository", ) - self.assertEqual(challenge.github_branch, "refs/heads/challenge") + self.assertEqual(challenge.github_branch, "challenge-2025-v1") def test_create_challenge_using_github_when_challenge_host_team_does_not_exist( self, @@ -5995,6 +5995,34 @@ def test_create_challenge_using_github_when_user_is_not_authenticated( self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) def test_create_challenge_using_github_without_branch_name(self): + """Test that missing GITHUB_BRANCH_NAME fails for new challenge creation""" + self.url = reverse_lazy( + "challenges:create_or_update_github_challenge", + kwargs={"challenge_host_team_pk": self.challenge_host_team.pk}, + ) + + with mock.patch("challenges.views.requests.get") as m: + resp = mock.Mock() + resp.content = self.test_zip_file.read() + resp.status_code = 200 + m.return_value = resp + response = self.client.post( + self.url, + { + "GITHUB_REPOSITORY": "https://github.com/yourusername/repository", + "zip_configuration": self.input_zip_file, + }, + format="multipart", + ) + + # Should fail because "challenge" doesn't match the required format for new challenges + self.assertEqual(response.status_code, 400) + self.assertIn("error", response.json()) + # The error comes from challenge_config_utils validation + self.assertIn("invalid", str(response.json()["error"])) + + def test_create_challenge_using_github_with_branch_name_challenge(self): + """Test when branch name is 'challenge' - should fail validation for new challenges""" self.url = reverse_lazy( "challenges:create_or_update_github_challenge", kwargs={"challenge_host_team_pk": self.challenge_host_team.pk}, @@ -6009,6 +6037,62 @@ def test_create_challenge_using_github_without_branch_name(self): self.url, { "GITHUB_REPOSITORY": "https://github.com/yourusername/repository", + "GITHUB_BRANCH_NAME": "challenge", + "zip_configuration": self.input_zip_file, + }, + format="multipart", + ) + + # Should fail because "challenge" doesn't match the required format for new challenges + self.assertEqual(response.status_code, 400) + self.assertIn("error", response.json()) + # The error comes from challenge_config_utils validation + self.assertIn("invalid", str(response.json()["error"])) + + def test_create_challenge_using_github_with_invalid_branch_name(self): + """Test when branch name is an invalid format (e.g., 'xyzabc')""" + self.url = reverse_lazy( + "challenges:create_or_update_github_challenge", + kwargs={"challenge_host_team_pk": self.challenge_host_team.pk}, + ) + + with mock.patch("challenges.views.requests.get") as m: + resp = mock.Mock() + resp.content = self.test_zip_file.read() + resp.status_code = 200 + m.return_value = resp + response = self.client.post( + self.url, + { + "GITHUB_REPOSITORY": "https://github.com/yourusername/repository", + "GITHUB_BRANCH_NAME": "xyzabc", + "zip_configuration": self.input_zip_file, + }, + format="multipart", + ) + + # Should fail validation due to invalid branch format + self.assertEqual(response.status_code, 400) + self.assertIn("error", response.json()) + self.assertIn("invalid", response.json()["error"]) + + def test_create_challenge_using_github_with_valid_branch_format(self): + """Test when branch name follows the correct format (e.g., 'challenge-2025-v2')""" + self.url = reverse_lazy( + "challenges:create_or_update_github_challenge", + kwargs={"challenge_host_team_pk": self.challenge_host_team.pk}, + ) + + with mock.patch("challenges.views.requests.get") as m: + resp = mock.Mock() + resp.content = self.test_zip_file.read() + resp.status_code = 200 + m.return_value = resp + response = self.client.post( + self.url, + { + "GITHUB_REPOSITORY": "https://github.com/yourusername/repository", + "GITHUB_BRANCH_NAME": "challenge-2025-v2", "zip_configuration": self.input_zip_file, }, format="multipart", @@ -6020,13 +6104,120 @@ def test_create_challenge_using_github_without_branch_name(self): self.assertEqual(response.status_code, 201) self.assertEqual(response.json(), expected) - # Verify github_branch defaults to "challenge" when not provided + # Verify github_branch is properly stored with the correct format challenge = Challenge.objects.first() self.assertEqual( challenge.github_repository, "https://github.com/yourusername/repository", ) - self.assertEqual(challenge.github_branch, "challenge") + self.assertEqual(challenge.github_branch, "challenge-2025-v2") + + def test_create_challenge_using_github_with_other_valid_branch_formats( + self, + ): + """Test various valid branch name formats""" + self.url = reverse_lazy( + "challenges:create_or_update_github_challenge", + kwargs={"challenge_host_team_pk": self.challenge_host_team.pk}, + ) + + valid_branches = [ + "challenge-2024-1", + "challenge-2025-v1", + "challenge-2060-v2", + "challenge-2024-final", + ] + + for branch_name in valid_branches: + with mock.patch("challenges.views.requests.get") as m: + self.test_zip_file.seek(0) + resp = mock.Mock() + resp.content = self.test_zip_file.read() + resp.status_code = 200 + m.return_value = resp + self.test_zip_file.seek(0) + response = self.client.post( + self.url, + { + "GITHUB_REPOSITORY": "https://github.com/yourusername/repository", + "GITHUB_BRANCH_NAME": branch_name, + "zip_configuration": self.test_zip_file, + }, + format="multipart", + ) + expected = { + "Success": "Challenge Challenge Title has been created successfully and sent for review to EvalAI Admin." + } + + self.assertEqual( + response.status_code, + 201, + f"Failed for branch: {branch_name}", + ) + self.assertEqual(response.json(), expected) + + # Verify github_branch is properly stored + challenge = Challenge.objects.first() + self.assertEqual( + challenge.github_repository, + "https://github.com/yourusername/repository", + ) + self.assertEqual(challenge.github_branch, branch_name) + + # Clean up for next iteration + Challenge.objects.all().delete() + + def test_create_challenge_using_github_with_invalid_branch_formats(self): + """Test various invalid branch name formats""" + self.url = reverse_lazy( + "challenges:create_or_update_github_challenge", + kwargs={"challenge_host_team_pk": self.challenge_host_team.pk}, + ) + + invalid_branches = [ + "main", + "master", + "develop", + "feature-branch", + "challenge", + "challenge-2025", + "challenge-v2", + "2025-challenge-v2", + "challenge-2025-v2-extra", + ] + + for branch_name in invalid_branches: + with mock.patch("challenges.views.requests.get") as m: + self.test_zip_file.seek( + 0 + ) # Reset file pointer to the beginning + resp = mock.Mock() + resp.content = self.test_zip_file.read() + resp.status_code = 200 + m.return_value = resp + self.test_zip_file.seek( + 0 + ) # Reset file pointer to the beginning + response = self.client.post( + self.url, + { + "GITHUB_REPOSITORY": "https://github.com/yourusername/repository", + "GITHUB_BRANCH_NAME": branch_name, + "zip_configuration": self.test_zip_file, + }, + format="multipart", + ) + + # Should fail validation due to invalid branch format + self.assertEqual( + response.status_code, + 400, + f"Should fail for branch: {branch_name}", + ) + self.assertIn("error", response.json()) + # The error comes from challenge_config_utils validation + self.assertIn("invalid", str(response.json()["error"])) + class ValidateChallengeTest(APITestCase): def setUp(self): @@ -6093,7 +6284,7 @@ def test_validate_challenge_using_success(self): self.url, { "GITHUB_REPOSITORY": "https://github.com/yourusername/repository", - "GITHUB_BRANCH_NAME": "refs/heads/challenge", + "GITHUB_BRANCH_NAME": "challenge-2025-v1", "zip_configuration": self.input_zip_file, }, format="multipart", @@ -6116,13 +6307,14 @@ def test_validate_challenge_using_failure(self): resp = mock.Mock() resp.content = self.test_zip_incorrect_file.read() resp.status_code = 200 - m.return_value = resp + self.test_zip_incorrect_file.seek(0) response = self.client.post( self.url, { "GITHUB_REPOSITORY": "https://github.com/yourusername/repository", - "zip_configuration": self.input_zip_file, + "GITHUB_BRANCH_NAME": "challenge-2025-v1", + "zip_configuration": self.test_zip_incorrect_file, }, format="multipart", ) From c8bd7c96329ed026385906b55fd36d0b95114077 Mon Sep 17 00:00:00 2001 From: Zahed-Riyaz Date: Thu, 17 Jul 2025 23:28:11 +0530 Subject: [PATCH 18/18] Reformat for quality checks --- tests/unit/challenges/test_views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/challenges/test_views.py b/tests/unit/challenges/test_views.py index e21db5c7ff..68b2ba5a83 100644 --- a/tests/unit/challenges/test_views.py +++ b/tests/unit/challenges/test_views.py @@ -5941,8 +5941,8 @@ def test_create_challenge_using_github_success(self): "Success": "Challenge Challenge Title has been created successfully and sent for review to EvalAI Admin." } - self.assertEqual(response.status_code, 201) - self.assertEqual(response.json(), expected) + self.assertEqual(response.status_code, 201) + self.assertEqual(response.json(), expected) self.assertEqual(Challenge.objects.count(), 1) self.assertEqual(DatasetSplit.objects.count(), 1) self.assertEqual(Leaderboard.objects.count(), 1) @@ -5955,7 +5955,7 @@ def test_create_challenge_using_github_success(self): "https://github.com/yourusername/repository", ) self.assertEqual(challenge.github_branch, "challenge-2025-v1") - + def test_create_challenge_using_github_when_challenge_host_team_does_not_exist( self, ):