Report build statuses to GitHub
This commit is contained in:
parent
968f94c1e7
commit
903a05fc0e
3 changed files with 65 additions and 6 deletions
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -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}.")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue