Skip to content

Commit

Permalink
Use Repo type for repo field and fully populate
Browse files Browse the repository at this point in the history
For consistency with other dataclasses declared in github.py,
use the Repo type for a Codespace's repo attribute.
At the time of writing, the organisation codespaces API endpoint
does not provide all the data required for this type, and since
we are retreiving info on codespaces non-Tech team repos/orgs,
extra API calls are required to get the required information
(teams, repos, repo metadata)
  • Loading branch information
Jongmassey committed May 21, 2024
1 parent 56c710f commit 1a89fbf
Show file tree
Hide file tree
Showing 6 changed files with 71 additions and 18 deletions.
4 changes: 2 additions & 2 deletions INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ Code scanning alerts, Dependabot alerts, Metadata, Pull requests and Repository

The `GITHUB_OS_TOKEN` is a fine-grained GitHub personal access token that is used for authenticating with the GitHub REST API.
It is assigned to a single organisation and should have the following *read-only* permissions:
* organisation permissions: codespaces
* organisation permissions: Organisation codespaces and Members
* *all repositories* owned by the organisation with the following permissions:
Codespaces and Metadata
Codespaces, Metadata, and Repository security advisories

## Disable checks
Dokku performs health checks on apps during deploy by sending requests to port 80.
Expand Down
2 changes: 1 addition & 1 deletion metrics/github/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def rest_query(self, path, **variables):
# unlike the PRs/Issues/Commits GitHub API endpoints
# the codespaces endpoint returns a dict containing a
# count of codespaces, and a list thereof (what we want)
if "codespaces" in path and isinstance(data, dict):
elif "codespaces" in path and isinstance(data, dict):
yield from data["codespaces"]
else:
raise RuntimeError("Unexpected response format:", data)
Expand Down
30 changes: 20 additions & 10 deletions metrics/github/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,26 +110,36 @@ def from_dict(cls, data, repo):

@dataclass(frozen=True)
class Codespace:
org: str
# The Repo type requires fields neither returned by the codespaces
# endpoint, nor required for codespaces metrics so str for repo name
repo: str
repo: Repo
user: str
created_at: datetime.datetime
last_used_at: datetime.datetime

@classmethod
def from_dict(cls, **kwargs):
return cls(**kwargs)
def from_dict(cls, repo, **kwargs):
if "repo_name" in kwargs:
del kwargs["repo_name"]
return cls(repo, **kwargs)


def codespaces(org):
org_teams = teams(org)
ownership = _repo_owners(org, org_teams)
repos = {
r["name"]: Repo.from_dict(r, org=org, team=ownership.get(r["name"], None))
for r in query.repos(org)
}

return [
Codespace.from_dict(**({"org": org} | codespace))
for codespace in query.codespaces(org)
Codespace.from_dict(repo=repos[c["repo_name"]], **c)
for c in query.codespaces(org)
]


def teams(org):
return [t["name"] for t in query.teams(org)]


def tech_prs():
tech_team_members = _tech_team_members()
return [
Expand Down Expand Up @@ -165,8 +175,8 @@ def _get_repos():
return repos


def _repo_owners(org):
return {repo: team for team in _TECH_TEAMS for repo in query.team_repos(org, team)}
def _repo_owners(org, teams=_TECH_TEAMS):
return {repo: team for team in teams for repo in query.team_repos(org, team)}


def _tech_team_members():
Expand Down
4 changes: 2 additions & 2 deletions metrics/github/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ def get_codespaces_metrics(codespaces):
return [
{
"created_at": c.created_at,
"organisation": c.org,
"repo": c.repo,
"organisation": c.repo.org,
"repo": c.repo.name,
"user": c.user,
"last_used_at": c.last_used_at,
}
Expand Down
23 changes: 22 additions & 1 deletion metrics/github/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,27 @@ def team_members(org, team):
yield member["login"]


def teams(org):
query = """
query teams($cursor:String, $org: String!) {
organization(login: $org) {
teams(first:100, after: $cursor) {
nodes {
name
}
pageInfo{
endCursor
hasNextPage
}
}
}
}
"""
return maybe_truncate(
_client().graphql_query(query, path=["organization", "teams"], org=org)
)


def vulnerabilities(org, repo):
query = """
query vulnerabilities($cursor: String, $org: String!, $repo: String!) {
Expand Down Expand Up @@ -146,7 +167,7 @@ def codespaces(org):
for codespace in codespaces:
yield {
"user": codespace["owner"]["login"],
"repo": codespace["repository"]["name"],
"repo_name": codespace["repository"]["name"],
"created_at": codespace["created_at"],
"last_used_at": codespace["last_used_at"],
}
Expand Down
26 changes: 24 additions & 2 deletions tests/metrics/github/test_github.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,36 @@ def test_codespaces(patch):
"opensafely": [
{
"user": "testuser",
"repo": "testrepo",
"repo_name": "testrepo",
"created_at": datetime.datetime.now().isoformat(),
"last_used_at": datetime.datetime.now().isoformat(),
},
{
"user": "testuser",
"repo_name": "testrepo2",
"created_at": datetime.datetime.now().isoformat(),
"last_used_at": datetime.datetime.now().isoformat(),
},
]
},
)
assert len(github.codespaces("opensafely")) == 1
patch("teams", {"opensafely": [{"name": "testteam"}]})
# Expect that at least one repo will not be assigned to a team
patch("team_repos", {"opensafely": {"testteam": ["testrepo"]}})
# Expect that there will be repositories without codespaces
patch(
"repos",
{
"opensafely": [
repo_data("testrepo"),
repo_data("testrepo2"),
repo_data("testrepo3"),
]
},
)

# All codespaces for repos in this org, regardless of team should be counted
assert len(github.codespaces("opensafely")) == 2


def test_includes_tech_owned_repos(patch):
Expand Down

0 comments on commit 1a89fbf

Please sign in to comment.