Skip to content

Commit

Permalink
Merge pull request #42 from lsst-sqre/tickets/DM-35816
Browse files Browse the repository at this point in the history
DM-35816: Support for viewing GitHub PR previews in Squareone
  • Loading branch information
jonathansick authored Aug 18, 2022
2 parents 306ec22 + 122598a commit 58d0b0f
Show file tree
Hide file tree
Showing 14 changed files with 1,251 additions and 1,357 deletions.
8 changes: 4 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.1.0
rev: v4.3.0
hooks:
- id: check-yaml
- id: check-toml
Expand All @@ -13,11 +13,11 @@ repos:
- toml

- repo: https://github.com/psf/black
rev: 22.3.0
rev: 22.6.0
hooks:
- id: black

- repo: https://gitlab.com/pycqa/flake8
rev: 4.0.1
- repo: https://github.com/pycqa/flake8
rev: 5.0.3
hooks:
- id: flake8
10 changes: 10 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
Change log
==========

0.6.0 (2022-08-18)
------------------

Times Square now exposes information about pages created during GitHub PR check runs:

- ``GET /times-square/api/v1/github-pr/:org/:repo/:sha`` provides metadata for a repository's check run in general, such as the contents in the check run and the GitHub pull request or check run.
- ``GET /times-square/api/v1/github-pr/:org/:repo/:sha/:path`` provides metadata about a specific notebook.

Times Square check runs also link to the pull request preview pages published through Times Square's interface in Squareone.

0.5.0 (2022-07-04)
------------------

Expand Down
481 changes: 52 additions & 429 deletions requirements/dev.txt

Large diffs are not rendered by default.

884 changes: 85 additions & 799 deletions requirements/main.txt

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions src/timessquare/dependencies/requestcontext.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from timessquare.config import Config, config
from timessquare.dependencies.redis import redis_dependency
from timessquare.services.github.repo import GitHubRepoService
from timessquare.services.page import PageService
from timessquare.storage.nbhtmlcache import NbHtmlCacheStore
from timessquare.storage.noteburstjobstore import NoteburstJobStore
Expand Down Expand Up @@ -55,6 +56,7 @@ class RequestContext:

@property
def page_service(self) -> PageService:
"""An instance of the page service."""
return PageService(
page_store=PageStore(self.session),
html_cache=NbHtmlCacheStore(self.redis),
Expand All @@ -63,6 +65,20 @@ def page_service(self) -> PageService:
logger=self.logger,
)

async def create_github_repo_service(
self, owner: str, repo: str
) -> GitHubRepoService:
"""An instance of the GitHub repository service for manging
GitHub-backed pages and accessing GitHub's API.
"""
return await GitHubRepoService.create_for_repo(
owner=owner,
repo=repo,
http_client=self.http_client,
page_service=self.page_service,
logger=self.logger,
)

def get_request_username(self) -> Optional[str]:
"""Get the username who made the request
Expand Down
69 changes: 67 additions & 2 deletions src/timessquare/domain/githubapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from base64 import b64decode
from enum import Enum
from typing import Optional
from typing import List, Optional

from pydantic import BaseModel, Field, HttpUrl

Expand All @@ -20,6 +20,23 @@ class GitHubRepoOwnerModel(BaseModel):
)


class GitHubUserModel(BaseModel):
"""A Pydantic model for the "user" field found in GitHub API resources.
This contains brief (public) info about a user.
"""

login: str = Field(title="Login name", description="GitHub username")

html_url: HttpUrl = Field(description="Homepage for the user on GitHub")

url: HttpUrl = Field(
description="URL for the user's resource in the GitHub API"
)

avatar_url: HttpUrl = Field(description="URL to the user's avatar")


class GitHubRepositoryModel(BaseModel):
"""A Pydantic model for the "repository" field, often found in webhook
payloads.
Expand Down Expand Up @@ -77,6 +94,16 @@ class GitHubRepositoryModel(BaseModel):
)


class GitHubPullState(str, Enum):
"""The state of a GitHub PR.
https://docs.github.com/en/rest/pulls/pulls#get-a-pull-request
"""

open = "open"
closed = "closed"


class GitHubPullRequestModel(BaseModel):
"""A Pydantic model for a GitHub Pull Request.
Expand All @@ -92,7 +119,15 @@ class GitHubPullRequestModel(BaseModel):

title: str = Field(title="Title")

# TODO a lot more data is available. Expand this model as needed.
state: GitHubPullState = Field(
description="Whether the PR is opened or closed"
)

draft: bool = Field(description="True if the PR is a draft")

merged: bool = Field(description="True if the PR is merged")

user: GitHubUserModel = Field(description="The user that opened the PR")


class GitHubBranchCommitModel(BaseModel):
Expand Down Expand Up @@ -226,6 +261,28 @@ class GitHubCheckSuiteId(BaseModel):
id: str = Field(description="Check suite ID")


class GitHubCheckRunOutput(BaseModel):
"""Check run output report."""

title: Optional[str] = Field(None, description="Title of the report")

summary: Optional[str] = Field(
None, description="Summary information (markdown formatted"
)

text: Optional[str] = Field(None, description="Extended report (markdown)")


class GitHubCheckRunPrInfoModel(BaseModel):
"""A Pydantic model of the "pull_requsts[]" items in a check run
GitHub API model.
https://docs.github.com/en/rest/checks/runs#get-a-check-run
"""

url: HttpUrl = Field(description="GitHub API URL for this pull request")


class GitHubCheckRunModel(BaseModel):
"""A Pydantic model for the "check_run" field in a check_run webhook
payload (`GitHubCheckRunPayloadModel`).
Expand Down Expand Up @@ -257,3 +314,11 @@ class GitHubCheckRunModel(BaseModel):
html_url: HttpUrl = Field(description="URL of the check run webpage.")

check_suite: GitHubCheckSuiteId

output: Optional[GitHubCheckRunOutput] = Field(
None, title="Output", description="Check run output, if available."
)

pull_requests: List[GitHubCheckRunPrInfoModel] = Field(
default_factory=list
)
48 changes: 40 additions & 8 deletions src/timessquare/domain/githubcheckrun.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@

from __future__ import annotations

import os.path
from abc import ABCMeta, abstractproperty
from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Sequence, Union

from gidgethub.httpx import GitHubAPI
from pydantic import ValidationError

from timessquare.config import config

from .githubapi import (
GitHubBlobModel,
GitHubCheckRunAnnotationLevel,
Expand Down Expand Up @@ -99,8 +102,11 @@ class GitHubCheck(metaclass=ABCMeta):
share the same external ID.
"""

def __init__(self, check_run: GitHubCheckRunModel) -> None:
def __init__(
self, *, check_run: GitHubCheckRunModel, repo: GitHubRepositoryModel
) -> None:
self.check_run = check_run
self.repo = repo
self.annotations: List[Annotation] = []

@property
Expand All @@ -125,6 +131,25 @@ def text(self) -> str:
"""The text body of the check's message."""
raise NotImplementedError

@property
def squareone_pr_url_root(self) -> str:
"""Root URL for this check run in Squareone.
Formatted as ``{host}/times-square/github-pr/{owner}/{repo}/{commit}``
"""
if config.environment_url.endswith("/"):
squareone_url = str(config.environment_url)
else:
squareone_url = f"{str(config.environment_url)}/"
return (
f"{squareone_url}/times-square/github-pr/{self.repo.owner.login}"
f"/{self.repo.name}/{self.check_run.head_sha}"
)

def get_preview_url(self, notebook_path: str) -> str:
display_path = os.path.splitext(notebook_path)[0]
return f"{self.squareone_pr_url_root}/{display_path}"

def export_truncated_annotations(self) -> List[Dict[str, Any]]:
"""Export the first 50 annotations to objects serializable to
GitHub.
Expand Down Expand Up @@ -155,6 +180,7 @@ async def submit_conclusion(
data={
"status": GitHubCheckRunStatus.completed,
"conclusion": self.conclusion,
"details_url": self.squareone_pr_url_root,
"output": {
"title": self.title,
"summary": self.summary,
Expand All @@ -175,14 +201,16 @@ class GitHubConfigsCheck(GitHubCheck):
share the same external ID.
"""

def __init__(self, check_run: GitHubCheckRunModel) -> None:
def __init__(
self, check_run: GitHubCheckRunModel, repo: GitHubRepositoryModel
) -> None:
self.sidecar_files_checked: List[str] = []

# Optional caching for data reuse
self.checkout: Optional[GitHubRepositoryCheckout] = None
self.tree: Optional[RecursiveGitTreeModel] = None

super().__init__(check_run=check_run)
super().__init__(check_run=check_run, repo=repo)

@classmethod
async def create_check_run_and_validate(
Expand Down Expand Up @@ -225,7 +253,7 @@ async def validate_repo(
repository containing Times Square notebooks given a check run already
registered with GitHub.
"""
check = cls(check_run)
check = cls(check_run, repo)
await check.submit_in_progress(github_client)

try:
Expand Down Expand Up @@ -365,9 +393,11 @@ class NotebookExecutionsCheck(GitHubCheck):
share the same external ID.
"""

def __init__(self, check_run: GitHubCheckRunModel) -> None:
def __init__(
self, check_run: GitHubCheckRunModel, repo: GitHubRepositoryModel
) -> None:
self.notebook_paths_checked: List[str] = []
super().__init__(check_run=check_run)
super().__init__(check_run=check_run, repo=repo)

def report_noteburst_failure(
self, page_execution: PageExecutionInfo
Expand Down Expand Up @@ -432,10 +462,12 @@ def text(self) -> str:
notebook_paths = list(set(self.notebook_paths_checked))
notebook_paths.sort()
for notebook_path in notebook_paths:
preview_url = self.get_preview_url(notebook_path)
linked_notebook = f"[{notebook_path}]({preview_url})"
if self._is_file_ok(notebook_path):
text = f"{text}| {notebook_path} | ✅ |\n"
text = f"{text}| {linked_notebook} | ✅ |\n"
else:
text = f"{text}| {notebook_path} | ❌ |\n"
text = f"{text}| {linked_notebook} | ❌ |\n"

return text

Expand Down
Loading

0 comments on commit 58d0b0f

Please sign in to comment.