Skip to content

Commit

Permalink
Add GitHub API query, dataclass, and test
Browse files Browse the repository at this point in the history
Define a Codespace dataclass containing required fields (see discussion
in opensafely-core/codespaces-initiative#42).
Rather than use an instance of the existing Repo dataclass to store
repo data, we only need the name and we only receive a
minimal amount of repo data from the API so just store the name as a
string. This is hopefully less confusing than modifying the Repo class
or populating the extra fields this class requires with dummy data.

An additional PAT is required to query codespaces for the opensafely
GitHub organisation. Any future querying of codespaces for other
organisations will require similarly permissioned PATs.

The organisation codespaces endpoint is queried and returned data is
passed unmodified to the Codespace dataclass's from_dict() method, which
does the required data conversion. This follows the pattern established
for the other domain dataclasses.
  • Loading branch information
Jongmassey committed May 30, 2024
1 parent b1548f1 commit 261259c
Show file tree
Hide file tree
Showing 6 changed files with 63 additions and 0 deletions.
7 changes: 7 additions & 0 deletions INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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.
Expand Down
1 change: 1 addition & 0 deletions dotenv-sample
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
5 changes: 5 additions & 0 deletions metrics/github/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
elif "codespaces" in path and isinstance(data, dict):
yield from data["codespaces"]
else:
raise RuntimeError("Unexpected response format:", data)

Expand Down
28 changes: 28 additions & 0 deletions metrics/github/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,34 @@ 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_name: str
user: str
created_at: datetime.datetime
last_used_at: datetime.datetime

@classmethod
def from_dict(cls, data, org):
return cls(
org=org,
repo_name=data["repository"]["name"],
user=data["owner"]["login"],
created_at=data["created_at"],
last_used_at=data["last_used_at"],
)


def codespaces(org):
return [
Codespace.from_dict(data=codespace, org=org)
for codespace in query.codespaces(org)
]


def tech_prs():
tech_team_members = _tech_team_members()
return [
Expand Down
5 changes: 5 additions & 0 deletions metrics/github/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,11 +141,16 @@ def issues(org, repo):
)


def codespaces(org):
yield from _client().rest_query("/orgs/{org}/codespaces", org=org)


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"],
}
)

Expand Down
17 changes: 17 additions & 0 deletions tests/metrics/github/test_github.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,23 @@ def fake(*keys):
return patch


def test_codespaces(patch):
patch(
"codespaces",
{
"opensafely": [
{
"owner": {"login": "testuser"},
"repository": {"name": "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",
Expand Down

0 comments on commit 261259c

Please sign in to comment.