From 0ec7eabf600ab5d6b41001ad1ce70489a9e6d171 Mon Sep 17 00:00:00 2001 From: Jon Massey Date: Fri, 10 May 2024 17:03:37 +0100 Subject: [PATCH] GitHub query, dataclass, and test Spec of fields to extract from API response taken from discussion in https://github.com/opensafely-core/codespaces-initiative/issues/42 --- INSTALL.md | 7 +++++++ dotenv-sample | 1 + metrics/github/client.py | 5 +++++ metrics/github/github.py | 22 ++++++++++++++++++++++ metrics/github/query.py | 12 ++++++++++++ tests/metrics/github/test_github.py | 17 +++++++++++++++++ 6 files changed, 64 insertions(+) diff --git a/INSTALL.md b/INSTALL.md index a80e61dd..761a8798 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -9,6 +9,7 @@ dokku$ dokku git:set metrics deploy-branch main ```bash dokku config:set metrics GITHUB_EBMDATALAB_TOKEN='xxx' dokku config:set metrics GITHUB_OS_CORE_TOKEN='xxx' +dokku config:set metrics GITHUB_OS_TOKEN='xxx' dokku config:set metrics SLACK_SIGNING_SECRET='xxx' dokku config:set metrics SLACK_TECH_SUPPORT_CHANNEL_ID='xxx' dokku config:set metrics SLACK_TOKEN='xxx' @@ -22,6 +23,12 @@ Each token is assigned to a single organisation and should have the following *r * *all repositories* owned by the organisation with the following permissions: Code scanning alerts, Dependabot alerts, Metadata, Pull requests and Repository security advisories +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 +* *all repositories* owned by the organisation with the following permissions: +Codespaces and Metadata + ## Disable checks Dokku performs health checks on apps during deploy by sending requests to port 80. This tool isn't a web app so it can't accept requests on a port. diff --git a/dotenv-sample b/dotenv-sample index bba73f88..7032b7ba 100644 --- a/dotenv-sample +++ b/dotenv-sample @@ -4,6 +4,7 @@ TIMESCALEDB_URL=postgresql://user:pass@localhost:5433/metrics # API tokens for pulling data from Github GITHUB_EBMDATALAB_TOKEN= GITHUB_OS_CORE_TOKEN= +GITHUB_OS_TOKEN= # Slack API access credentials. # The slack app used for this will need the following OAuth scopes: diff --git a/metrics/github/client.py b/metrics/github/client.py index ce3512c8..96dea7a6 100644 --- a/metrics/github/client.py +++ b/metrics/github/client.py @@ -52,6 +52,11 @@ def rest_query(self, path, **variables): data = response.json() if isinstance(data, list): yield from data + # 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): + yield from data["codespaces"] else: raise RuntimeError("Unexpected response format:", data) diff --git a/metrics/github/github.py b/metrics/github/github.py index 50d4f5e6..08270a12 100644 --- a/metrics/github/github.py +++ b/metrics/github/github.py @@ -108,6 +108,28 @@ 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 + user: str + created_at: datetime.datetime + last_used_at: datetime.datetime + + @classmethod + def from_dict(cls, **kwargs): + return cls(**kwargs) + + +def codespaces(org): + return [ + Codespace.from_dict(**({"org": org} | codespace)) + for codespace in query.codespaces(org) + ] + + def tech_prs(): tech_team_members = _tech_team_members() return [ diff --git a/metrics/github/query.py b/metrics/github/query.py index e3a69f51..8cb7061a 100644 --- a/metrics/github/query.py +++ b/metrics/github/query.py @@ -141,11 +141,23 @@ def issues(org, repo): ) +def codespaces(org): + codespaces = _client().rest_query("/orgs/{org}/codespaces", org=org) + for codespace in codespaces: + yield { + "user": codespace["owner"]["login"], + "repo": codespace["repository"]["name"], + "created_at": codespace["created_at"], + "last_used_at": codespace["last_used_at"], + } + + def _client(): return GitHubClient( tokens={ "ebmdatalab": os.environ["GITHUB_EBMDATALAB_TOKEN"], "opensafely-core": os.environ["GITHUB_OS_CORE_TOKEN"], + "opensafely": os.environ["GITHUB_OS_TOKEN"], } ) diff --git a/tests/metrics/github/test_github.py b/tests/metrics/github/test_github.py index 6fe76fe7..18d69240 100644 --- a/tests/metrics/github/test_github.py +++ b/tests/metrics/github/test_github.py @@ -35,6 +35,23 @@ def fake(*keys): return patch +def test_codespaces(patch): + patch( + "codespaces", + { + "opensafely": [ + { + "user": "testuser", + "repo": "testrepo", + "created_at": datetime.datetime.now().isoformat(), + "last_used_at": datetime.datetime.now().isoformat(), + }, + ] + }, + ) + assert len(github.codespaces("opensafely")) == 1 + + def test_includes_tech_owned_repos(patch): patch( "team_repos",