Skip to content

Commit

Permalink
feat: GithubAppInstallation model
Browse files Browse the repository at this point in the history
Model definitions for GithubAppInstallation.
This will unlock multiple apps for an org.
It also will improve how we keep track of what repos
an installation can access.

context: codecov/engineering-team#969
  • Loading branch information
giovanni-guidini committed Jan 18, 2024
1 parent 86431d3 commit a58ea09
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 1 deletion.
61 changes: 61 additions & 0 deletions codecov_auth/migrations/0048_githubappinstallation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Generated by Django 4.2.7 on 2024-01-17 13:37

import uuid

import django.contrib.postgres.fields
import django.db.models.deletion
import django_prometheus.models
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("codecov_auth", "0047_auto_20231009_1257"),
]

# BEGIN;
# --
# -- Create model GithubAppInstallation
# --
# CREATE TABLE "codecov_auth_githubappinstallation" ("id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, "external_id" uuid NOT NULL, "created_at" timestamp with time zone NOT NULL, "updated_at" timestamp with time zone NOT NULL, "installation_id" integer NOT NULL, "name" text NOT NULL, "repository_service_ids" text[] NULL, "owner_id" integer NOT NULL);
# ALTER TABLE "codecov_auth_githubappinstallation" ADD CONSTRAINT "codecov_auth_githuba_owner_id_82ba29b1_fk_owners_ow" FOREIGN KEY ("owner_id") REFERENCES "owners" ("ownerid") DEFERRABLE INITIALLY DEFERRED;
# CREATE INDEX "codecov_auth_githubappinstallation_owner_id_82ba29b1" ON "codecov_auth_githubappinstallation" ("owner_id");
# COMMIT;

operations = [
migrations.CreateModel(
name="GithubAppInstallation",
fields=[
("id", models.BigAutoField(primary_key=True, serialize=False)),
("external_id", models.UUIDField(default=uuid.uuid4, editable=False)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
("installation_id", models.IntegerField()),
("name", models.TextField(default="codecov_app_installation")),
(
"repository_service_ids",
django.contrib.postgres.fields.ArrayField(
base_field=models.TextField(), null=True, size=None
),
),
(
"owner",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="github_app_installations",
to="codecov_auth.owner",
),
),
],
options={
"abstract": False,
},
bases=(
django_prometheus.models.ExportModelOperationsMixin(
"codecov_auth.github_app_installation"
),
models.Model,
),
),
]
46 changes: 46 additions & 0 deletions codecov_auth/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from django.contrib.postgres.fields import ArrayField, CITextField
from django.db import models
from django.db.models.manager import BaseManager
from django.forms import ValidationError
from django.utils import timezone
from django_prometheus.models import ExportModelOperationsMixin
Expand Down Expand Up @@ -183,7 +184,10 @@ class Meta:
updatestamp = DateTimeWithoutTZField(default=datetime.now)
organizations = ArrayField(models.IntegerField(null=True), null=True, blank=True)
admins = ArrayField(models.IntegerField(null=True), null=True, blank=True)

# DEPRECATED - replaced by GithubAppInstallation model
integration_id = models.IntegerField(null=True, blank=True)

permission = ArrayField(models.IntegerField(null=True), null=True)
bot = models.ForeignKey(
"Owner", db_column="bot", null=True, on_delete=models.SET_NULL, blank=True
Expand Down Expand Up @@ -471,6 +475,48 @@ def remove_admin(self, user):
self.save()


GITHUB_APP_INSTALLATION_DEFAULT_NAME = "codecov_app_installation"


class GithubAppInstallation(
ExportModelOperationsMixin("codecov_auth.github_app_installation"), BaseCodecovModel
):

# replacement for owner.integration_id
# installation id GitHub sends us in the installation-related webhook events
installation_id = models.IntegerField(null=False, blank=False)
name = models.TextField(default=GITHUB_APP_INSTALLATION_DEFAULT_NAME)
# if null, all repos are covered by this installation
# otherwise, it's a list of repo.id values
repository_service_ids = ArrayField(models.TextField(null=False), null=True)

owner = models.ForeignKey(
Owner,
null=False,
on_delete=models.CASCADE,
blank=False,
related_name="github_app_installations",
)

def repository_queryset(self) -> BaseManager[Repository]:
"""Returns a QuerySet of repositories covered by this installation"""
if self.repository_service_ids is None:
# All repos covered
return Repository.objects.filter(author=self.owner)
# Some repos covered
return Repository.objects.filter(
service_id__in=self.repository_service_ids, author=self.owner
)

def covers_all_repos(self) -> bool:
return self.repository_service_ids is None

def is_repo_covered_by_integration(self, repo: Repository) -> bool:
if self.covers_all_repos():
return repo.author.ownerid == self.owner.ownerid
return repo.service_id in self.repository_service_ids


class SentryUser(
ExportModelOperationsMixin("codecov_auth.sentry_user"), BaseCodecovModel
):
Expand Down
49 changes: 48 additions & 1 deletion codecov_auth/tests/unit/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
SERVICE_CODECOV_ENTERPRISE,
SERVICE_GITHUB,
SERVICE_GITHUB_ENTERPRISE,
GithubAppInstallation,
OrganizationLevelToken,
Service,
TokenTypeChoices,
)
from codecov_auth.tests.factories import OrganizationLevelTokenFactory, OwnerFactory
from core.tests.factories import RepositoryFactory
Expand Down Expand Up @@ -463,3 +463,50 @@ def test_token_is_deleted_when_changing_user_plan(
owner.plan = "users-basic"
owner.save()
assert OrganizationLevelToken.objects.filter(owner=owner).count() == 0


class TestGithubAppInstallationModel(TransactionTestCase):
def test_covers_all_repos(self):
owner = OwnerFactory()
repo1 = RepositoryFactory(author=owner)
repo2 = RepositoryFactory(author=owner)
repo3 = RepositoryFactory(author=owner)
other_repo_different_owner = RepositoryFactory()
installation_obj = GithubAppInstallation(
owner=owner,
repository_service_ids=None,
installation_id=100,
)
installation_obj.save()
assert installation_obj.name == "codecov_app_installation"
assert installation_obj.covers_all_repos() == True
assert installation_obj.is_repo_covered_by_integration(repo1) == True
assert (
installation_obj.is_repo_covered_by_integration(other_repo_different_owner)
== False
)
assert list(owner.github_app_installations.all()) == [installation_obj]
assert installation_obj.repository_queryset().exists()
assert set(installation_obj.repository_queryset().all()) == set(
[repo1, repo2, repo3]
)

def test_covers_some_repos(self):
owner = OwnerFactory()
repo = RepositoryFactory(author=owner)
other_repo_different_owner = RepositoryFactory()
installation_obj = GithubAppInstallation(
owner=owner,
repository_service_ids=[repo.service_id],
installation_id=100,
)
installation_obj.save()
assert installation_obj.covers_all_repos() == False
assert installation_obj.is_repo_covered_by_integration(repo) == True
assert (
installation_obj.is_repo_covered_by_integration(other_repo_different_owner)
== False
)
assert list(owner.github_app_installations.all()) == [installation_obj]
assert installation_obj.repository_queryset().exists()
assert list(installation_obj.repository_queryset().all()) == [repo]
3 changes: 3 additions & 0 deletions core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,10 @@ class Languages(models.TextChoices):
upload_token = models.UUIDField(unique=True, default=uuid.uuid4)
yaml = models.JSONField(null=True)
image_token = models.TextField(null=True, default=_gen_image_token)

# DEPRECATED - replaced by GithubAppInstallation model
using_integration = models.BooleanField(null=True)

hookid = models.TextField(null=True)
webhook_secret = models.TextField(null=True)
bot = models.ForeignKey(
Expand Down

0 comments on commit a58ea09

Please sign in to comment.