Report build statuses to GitHub

This commit is contained in:
Stéphane Bidoul 2021-11-05 18:28:52 +01:00
parent 968f94c1e7
commit 903a05fc0e
No known key found for this signature in database
GPG key ID: BCAB2555446B5B92
3 changed files with 65 additions and 6 deletions

View file

@ -122,7 +122,6 @@ resources:
Prototype (min required to open the project): Prototype (min required to open the project):
- report build status to github
- plug it on a bunch of OCA and shopinvader repos to test load - plug it on a bunch of OCA and shopinvader repos to test load
- basic tests - basic tests
@ -142,6 +141,8 @@ More:
- shiny UI - shiny UI
- websocket stream of build changes, for a dynamic UI - websocket stream of build changes, for a dynamic UI
- better target_url in GitHub status: instead of providing the link to the ingress,
provide a link to the build, which redirects to the ingress if the build is started
- handle PR close (delete all builds for PR) - handle PR close (delete all builds for PR)
- handle branch delete (delete all builds for branch) - handle branch delete (delete all builds for branch)
- create builds for all supported repos on startup (goes with sticky branches) - create builds for all supported repos on startup (goes with sticky branches)

View file

@ -1,4 +1,6 @@
import logging
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum
from typing import Any from typing import Any
import httpx import httpx
@ -6,8 +8,10 @@ import httpx
from .exceptions import NotFoundOnGitHub from .exceptions import NotFoundOnGitHub
from .settings import settings from .settings import settings
_logger = logging.getLogger(__name__)
async def _github_get(url: str) -> Any:
async def _github_request(method: str, url: str, json: Any = None) -> Any:
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
full_url = f"https://api.github.com{url}" full_url = f"https://api.github.com{url}"
headers = { headers = {
@ -15,7 +19,8 @@ async def _github_get(url: str) -> Any:
} }
if settings.github_token: if settings.github_token:
headers["Authorization"] = f"token {settings.github_token}" headers["Authorization"] = f"token {settings.github_token}"
response = await client.get(full_url, headers=headers) _logger.debug("%s %s", method, full_url)
response = await client.request(method, full_url, headers=headers, json=json)
if response.status_code == 404: if response.status_code == 404:
raise NotFoundOnGitHub(f"GitHub URL not found: {full_url}.") raise NotFoundOnGitHub(f"GitHub URL not found: {full_url}.")
response.raise_for_status() response.raise_for_status()
@ -30,7 +35,7 @@ class BranchInfo:
async def get_branch_info(repo: str, branch: str) -> BranchInfo: async def get_branch_info(repo: str, branch: str) -> BranchInfo:
branch_data = await _github_get(f"/repos/{repo}/git/ref/heads/{branch}") branch_data = await _github_request("GET", f"/repos/{repo}/git/ref/heads/{branch}")
return BranchInfo( return BranchInfo(
repo=repo, repo=repo,
name=branch, name=branch,
@ -47,10 +52,32 @@ class PullInfo:
async def get_pull_info(repo: str, pr: int) -> PullInfo: async def get_pull_info(repo: str, pr: int) -> PullInfo:
pr_data = await _github_get(f"/repos/{repo}/pulls/{pr}") pr_data = await _github_request("GET", f"/repos/{repo}/pulls/{pr}")
return PullInfo( return PullInfo(
repo=repo, repo=repo,
number=pr, number=pr,
head_sha=pr_data["head"]["sha"], head_sha=pr_data["head"]["sha"],
target_branch=pr_data["base"]["ref"], target_branch=pr_data["base"]["ref"],
) )
class GitHubStatusState(str, Enum):
error = "error"
failure = "failure"
pending = "pending"
success = "success"
async def notify_status(
repo: str, sha: str, state: GitHubStatusState, target_url: str | None
) -> None:
# https://docs.github.com/en/rest/reference/repos#create-a-commit-status
await _github_request(
"POST",
f"/repos/{repo}/statuses/{sha}",
json={
"state": state,
"target_url": target_url,
"context": "ci/runboat",
},
)

View file

@ -7,8 +7,9 @@ from typing import Optional
from kubernetes.client.models.v1_deployment import V1Deployment from kubernetes.client.models.v1_deployment import V1Deployment
from pydantic import BaseModel from pydantic import BaseModel
from . import k8s from . import github, k8s
from .build_images import get_build_image from .build_images import get_build_image
from .github import GitHubStatusState
from .settings import settings from .settings import settings
from .utils import slugify from .utils import slugify
@ -151,6 +152,12 @@ class Build(BaseModel):
image, image,
) )
await k8s.deploy(deployment_vars) await k8s.deploy(deployment_vars)
await github.notify_status(
repo,
git_commit,
GitHubStatusState.pending,
target_url=None,
)
async def start(self) -> None: async def start(self) -> None:
"""Start build if init succeeded, or reinitialize if failed.""" """Start build if init succeeded, or reinitialize if failed."""
@ -167,6 +174,12 @@ class Build(BaseModel):
_logger.info(f"Marking failed {self} for reinitialization.") _logger.info(f"Marking failed {self} for reinitialization.")
await k8s.delete_job(self.name, job_kind=k8s.DeploymentMode.initialize) await k8s.delete_job(self.name, job_kind=k8s.DeploymentMode.initialize)
await self._patch(init_status=BuildInitStatus.todo, desired_replicas=0) await self._patch(init_status=BuildInitStatus.todo, desired_replicas=0)
await github.notify_status(
self.repo,
self.git_commit,
GitHubStatusState.pending,
target_url=None,
)
elif self.status in (BuildStatus.stopped, BuildStatus.stopping): elif self.status in (BuildStatus.stopped, BuildStatus.stopping):
_logger.info(f"Starting {self} that was last scaled on {self.last_scaled}.") _logger.info(f"Starting {self} that was last scaled on {self.last_scaled}.")
await self._patch(desired_replicas=1) await self._patch(desired_replicas=1)
@ -229,6 +242,12 @@ class Build(BaseModel):
return return
_logger.info(f"Initialization job started for {self}.") _logger.info(f"Initialization job started for {self}.")
await self._patch(init_status=BuildInitStatus.started, desired_replicas=0) await self._patch(init_status=BuildInitStatus.started, desired_replicas=0)
await github.notify_status(
self.repo,
self.git_commit,
GitHubStatusState.pending,
target_url=None,
)
async def on_initialize_succeeded(self) -> None: async def on_initialize_succeeded(self) -> None:
if self.init_status == BuildInitStatus.succeeded: if self.init_status == BuildInitStatus.succeeded:
@ -237,6 +256,12 @@ class Build(BaseModel):
return return
_logger.info(f"Initialization job succeded for {self}, starting.") _logger.info(f"Initialization job succeded for {self}, starting.")
await self._patch(init_status=BuildInitStatus.succeeded, desired_replicas=1) await self._patch(init_status=BuildInitStatus.succeeded, desired_replicas=1)
await github.notify_status(
self.repo,
self.git_commit,
GitHubStatusState.success,
target_url=self.link,
)
async def on_initialize_failed(self) -> None: async def on_initialize_failed(self) -> None:
if self.init_status == BuildInitStatus.failed: if self.init_status == BuildInitStatus.failed:
@ -245,6 +270,12 @@ class Build(BaseModel):
return return
_logger.info(f"Initialization job failed for {self}.") _logger.info(f"Initialization job failed for {self}.")
await self._patch(init_status=BuildInitStatus.failed, desired_replicas=0) await self._patch(init_status=BuildInitStatus.failed, desired_replicas=0)
await github.notify_status(
self.repo,
self.git_commit,
GitHubStatusState.failure,
target_url=None,
)
async def on_cleanup_started(self) -> None: async def on_cleanup_started(self) -> None:
_logger.info(f"Cleanup job started for {self}.") _logger.info(f"Cleanup job started for {self}.")