Repository: codecov/codecov-api
Pull request #1295: feat(admin): add action to trigger sync repos
Author: login: giovanni-guidini
Created: 2025-04-23T18:04:53Z | Merged: 2025-04-24T10:52:50Z

This new action triggers the "sync_repos" for the selected owner from the admin dashboard. Would have been helpful for me today.

(only tested through the unit tests)



Diff:
diff --git a/codecov_auth/admin.py b/codecov_auth/admin.py
index 935f2fbd7a..f22a580bb1 100644
--- a/codecov_auth/admin.py
+++ b/codecov_auth/admin.py
@@ -12,6 +12,8 @@
 from django.http import HttpRequest
 from django.shortcuts import redirect, render
 from django.utils import timezone
+from services.task import TaskService
+
 from django.utils.html import format_html
 from shared.django_apps.codecov_auth.models import (
     Account,
@@ -111,6 +113,40 @@ def impersonate_owner(self, request, queryset):
 impersonate_owner.short_description = "Impersonate the selected owner"
 
 
+def refresh_owner(self, request, queryset):
+    if queryset.count() != 1:
+        self.message_user(
+            request, "You must refresh exactly one Owner.", level=messages.ERROR
+        )
+        return
+
+    owner = queryset.first()
+
+    task_service = TaskService()
+    task_service.refresh(
+        ownerid=owner.ownerid,
+        username=owner.username,
+        sync_teams=True,
+        sync_repos=True,
+        manual_trigger=True,
+    )
+
+    self.message_user(
+        request,
+        f"Refresh task triggered for {owner.username} (ownerid: {owner.ownerid})",
+        level=messages.SUCCESS,
+    )
+    History.log(
+        Owner.objects.get(ownerid=owner.ownerid),
+        "Refresh task triggered",
+        request.user,
+    )
+    return
+
+
+refresh_owner.short_description = "Sync repos and teams for the selected owner"
+
+
 class AccountsUsersInline(admin.TabularInline):
     model = AccountsUsers
     max_num = 10
@@ -559,7 +595,7 @@ class OwnerAdmin(AdminMixin, admin.ModelAdmin):
     list_display = ("name", "username", "email", "service")
     readonly_fields = []
     search_fields = ("name__iregex", "username__iregex", "email__iregex", "ownerid")
-    actions = [impersonate_owner, extend_trial]
+    actions = [impersonate_owner, extend_trial, refresh_owner]
     autocomplete_fields = ("bot", "account")
     inlines = [OrgUploadTokenInline]
 
diff --git a/codecov_auth/tests/test_admin.py b/codecov_auth/tests/test_admin.py
index 824b7929d4..d5a3a8439f 100644
--- a/codecov_auth/tests/test_admin.py
+++ b/codecov_auth/tests/test_admin.py
@@ -107,6 +107,45 @@ def test_owner_admin_impersonate_owner(self):
                 str(owner_to_impersonate.pk),
             )
 
+    @patch("codecov_auth.admin.TaskService.refresh")
+    def test_owner_admin_refresh_owner(self, mock_refresh):
+        owner_to_refresh = OwnerFactory(service="github", plan=DEFAULT_FREE_PLAN)
+        other_owner = OwnerFactory(plan=DEFAULT_FREE_PLAN)
+
+        with self.subTest("more than one user selected"):
+            response = self.client.post(
+                reverse("admin:codecov_auth_owner_changelist"),
+                {
+                    "action": "refresh_owner",
+                    ACTION_CHECKBOX_NAME: [
+                        owner_to_refresh.pk,
+                        other_owner.pk,
+                    ],
+                },
+                follow=True,
+            )
+            assert "You must refresh exactly one Owner." in str(response.content)
+
+        with self.subTest("one user selected"):
+            response = self.client.post(
+                reverse("admin:codecov_auth_owner_changelist"),
+                {
+                    "action": "refresh_owner",
+                    ACTION_CHECKBOX_NAME: [owner_to_refresh.pk],
+                },
+                follow=True,
+            )
+            assert f"Refresh task triggered for {owner_to_refresh.username}" in str(
+                response.content
+            )
+            mock_refresh.assert_called_once_with(
+                ownerid=owner_to_refresh.ownerid,
+                username=owner_to_refresh.username,
+                sync_teams=True,
+                sync_repos=True,
+                manual_trigger=True,
+            )
+
     @patch("codecov_auth.admin.TaskService.delete_owner")
     def test_delete_queryset(self, delete_mock):
         user_to_delete = OwnerFactory(plan=DEFAULT_FREE_PLAN)
@@ -608,6 +647,7 @@ def test_link_users_to_account(self):
         another_owner_with_user = OwnerFactory(user=UserFactory())
         self.org_1.plan_activated_users.append(another_owner_with_user.ownerid)
         self.org_1.save()
+
         # rerun action to re-sync
         res = self.client.post(
             reverse("admin:codecov_auth_account_changelist"),


Reviews:
user: login: Swatinem
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T09:32:00Z
user: login: giovanni-guidini
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T10:16:54Z

Review comments:
diff_hunk: @@ -111,6 +113,40 @@ def impersonate_owner(self, request, queryset):
 impersonate_owner.short_description = "Impersonate the selected owner"
 
 
+def refresh_owner(self, request, queryset):
+    if queryset.count() != 1:
+        self.message_user(
+            request, "You must refresh exactly one Owner.", level=messages.ERROR
+        )
+        return
+
+    owner = queryset.first()
+
+    task_service = TaskService()
+    task_service.refresh(
+        ownerid=owner.ownerid,
+        username=owner.username,
+        sync_teams=True,
+        sync_repos=True,
+        manual_trigger=True,
+    )
+
+    self.message_user(
+        request,
+        f"Refresh task triggered for {owner.username} (ownerid: {owner.ownerid})",
+        level=messages.SUCCESS,
+    )
+    History.log(
+        Owner.objects.get(ownerid=owner.ownerid),
path: codecov_auth/admin.py
user: login: Swatinem
body: this would just be `owner`, right?
created_at: 2025-04-24T09:31:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -111,6 +113,40 @@ def impersonate_owner(self, request, queryset):
 impersonate_owner.short_description = "Impersonate the selected owner"
 
 
+def refresh_owner(self, request, queryset):
+    if queryset.count() != 1:
+        self.message_user(
+            request, "You must refresh exactly one Owner.", level=messages.ERROR
path: codecov_auth/admin.py
user: login: Swatinem
body: I think it should be trivially possible to also support multiple-select.
created_at: 2025-04-24T09:31:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -111,6 +113,40 @@ def impersonate_owner(self, request, queryset):
 impersonate_owner.short_description = "Impersonate the selected owner"
 
 
+def refresh_owner(self, request, queryset):
+    if queryset.count() != 1:
+        self.message_user(
+            request, "You must refresh exactly one Owner.", level=messages.ERROR
+        )
+        return
+
+    owner = queryset.first()
+
+    task_service = TaskService()
+    task_service.refresh(
+        ownerid=owner.ownerid,
+        username=owner.username,
+        sync_teams=True,
+        sync_repos=True,
+        manual_trigger=True,
+    )
+
+    self.message_user(
+        request,
+        f"Refresh task triggered for {owner.username} (ownerid: {owner.ownerid})",
+        level=messages.SUCCESS,
+    )
+    History.log(
+        Owner.objects.get(ownerid=owner.ownerid),
path: codecov_auth/admin.py
user: login: giovanni-guidini
body: I wasn't sure, cause there is an `OwnerAdmin` class (line 593) so I wasn't sure if the queryset is of `OwnerAdmin`s or `Owner`s :E
created_at: 2025-04-24T10:16:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -111,6 +113,40 @@ def impersonate_owner(self, request, queryset):
 impersonate_owner.short_description = "Impersonate the selected owner"
 
 
+def refresh_owner(self, request, queryset):
+    if queryset.count() != 1:
+        self.message_user(
+            request, "You must refresh exactly one Owner.", level=messages.ERROR
+        )
+        return
+
+    owner = queryset.first()
+
+    task_service = TaskService()
+    task_service.refresh(
+        ownerid=owner.ownerid,
+        username=owner.username,
+        sync_teams=True,
+        sync_repos=True,
+        manual_trigger=True,
+    )
+
+    self.message_user(
+        request,
+        f"Refresh task triggered for {owner.username} (ownerid: {owner.ownerid})",
+        level=messages.SUCCESS,
+    )
+    History.log(
+        Owner.objects.get(ownerid=owner.ownerid),
path: codecov_auth/admin.py
user: login: giovanni-guidini
body: SO I copied what `impersonate_owner` does :P
created_at: 2025-04-24T10:16:49Z
author_association: CONTRIBUTOR

PR comments:
user: login: seer-by-sentry[bot]
created_at: 2025-04-23T18:06:17Z
body: ### :rotating_light: Sentry detected 2 potential issues in your recent changes :rotating_light:


<details>
<summary>Lower risk findings</summary>

- [<u>Line 123, codecov_auth/admin.py</u>](https://github.com/codecov/codecov-api/blob/c8be6e8e2c64060d1543147427939def1751b0d5/codecov_auth/admin.py#L123)

  - Handle potential exceptions when retrieving the owner.
- [<u>Line 126, codecov_auth/admin.py</u>](https://github.com/codecov/codecov-api/blob/c8be6e8e2c64060d1543147427939def1751b0d5/codecov_auth/admin.py#L126)

  - Add error handling for `TaskService.refresh` failures.
</details>

<sub>Did you find this useful? React with a :+1: or :-1:</sub>

author_association: CONTRIBUTOR
user: login: codecov-notifications[bot]
created_at: 2025-04-24T08:57:44Z
body: ## [Codecov](https://stage-app.codecov.dev/gh/codecov/codecov-api/pull/1295?dropdown=coverage&src=pr&el=h1) Report
All modified and coverable lines are covered by tests :white_check_mark:

:white_check_mark: All tests successful. No failed tests found.


:loudspeaker: Thoughts on this report? [Let us know!](https://about.codecov.io/pull-request-comment-report/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=codecov)

author_association: NONE
user: login: codecov[bot]
created_at: 2025-04-24T08:58:25Z
body: ## [Codecov](https://app.codecov.io/gh/codecov/codecov-api/pull/1295?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=codecov) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 96.30%. Comparing base [(`18eb985`)](https://app.codecov.io/gh/codecov/codecov-api/commit/18eb985ef8ed3d409421df684e8fb9f6442cec17?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=codecov) to head [(`06d3fe1`)](https://app.codecov.io/gh/codecov/codecov-api/commit/06d3fe1391a8945ab249e0662555c8f829ac0a50?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=codecov).
> Report is 1 commits behind head on main.

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##             main    #1295   +/-   ##
=======================================
  Coverage   96.29%   96.30%           
=======================================
  Files         488      488           
  Lines       16995    17007   +12     
=======================================
+ Hits        16366    16378   +12     
  Misses        629      629           
```

| [Flag](https://app.codecov.io/gh/codecov/codecov-api/pull/1295/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=codecov) | Coverage Δ | |
|---|---|---|
| [unit](https://app.codecov.io/gh/codecov/codecov-api/pull/1295/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=codecov) | `96.30% <100.00%> (+<0.01%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=codecov#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/codecov/codecov-api/pull/1295?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=codecov).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=codecov).

author_association: NONE