Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Agent instructions for github_pm workspaces

## Required checks before finishing any task

- **`tox` must complete successfully** for every change that touches the Python backend (and should be run once you believe backend work is done). Run it from the **`backend`** directory:

```bash
cd backend && tox
```

This runs the environments defined in `backend/pyproject.toml` (format, import order, lint, tests, coverage). Do **not** consider backend work complete while **`tox`** reports failures.

- **Fix all lint failures and unit test failures** reported by those checks (and any other checks you ran) **before** stopping. A green **`tox`** run is the acceptance bar for backend changes.

- For **frontend** (`frontend/`) changes, run **`npm test`** (and **`npm run format:check`** if you edited formatted sources) from `frontend/` and fix failures there as well when the task involves the UI or client code.

## Notes

- Use **`uv`** in the backend as described in the project `README.md` (e.g. `uv sync`, `uv run`).
- Prefer small, focused diffs; match existing style and patterns in both backend and frontend.
4 changes: 4 additions & 0 deletions backend/.env_sample
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
github_token=<personal access token>
github_repo=<organization>/<repository>
app_name=<GitHub Planner for project>
sdlc_feature_labels=enhancement
sdlc_bug_labels=bug
sdlc_docs_labels=documentation
sdlc_escape_label=escape
8 changes: 5 additions & 3 deletions backend/src/github_pm/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,17 @@


class Connector:
def __init__(self, github_token: str):
def __init__(self, github_token: str, *, github_repo: str | None = None):
"""Initialize a GitHub connection.

Args:
github_token: The GitHub Personal Access Token to use
github_repo: ``owner/name``; defaults to ``context.github_repo`` when omitted.
"""
self.github_token = github_token
self.base_url = "https://api.github.com"
self.owner, self.repo = context.github_repo.split("/", maxsplit=1)
repo = github_repo if github_repo is not None else context.github_repo
self.owner, self.repo = repo.split("/", maxsplit=1)
self.github = requests.session()
self.github.headers.update(
{
Expand All @@ -40,7 +42,7 @@ def __init__(self, github_token: str):
logger.info(
"Initializing GitHub Connector service to %s/%s",
self.base_url,
context.github_repo,
repo,
)

def get(self, path: str, headers: dict[str, str] | None = None) -> dict:
Expand Down
2 changes: 2 additions & 0 deletions backend/src/github_pm/app.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from fastapi import APIRouter, FastAPI

from github_pm.api import api_router
from github_pm.sdlc_api import sdlc_router

router = APIRouter()

Expand All @@ -11,6 +12,7 @@ async def health():


router.include_router(api_router, prefix="/api/v1")
router.include_router(sdlc_router, prefix="/api/v1")

app = FastAPI(
title="GitHub Project Management API",
Expand Down
17 changes: 17 additions & 0 deletions backend/src/github_pm/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@
from pydantic_settings import BaseSettings, SettingsConfigDict


def _parse_sdlc_label_csv(value: object) -> frozenset[str]:
"""Parse comma-separated label names into a lowercase set."""
if isinstance(value, frozenset):
return value
if not isinstance(value, str):
return frozenset()
return frozenset(part.strip().lower() for part in value.split(",") if part.strip())


class Settings(BaseSettings):
model_config = SettingsConfigDict(
extra="ignore",
Expand All @@ -14,6 +23,14 @@ class Settings(BaseSettings):
app_name: Annotated[str, Field(default="GitHub Project Manager")]
github_repo: Annotated[str, Field(default="vllm-project/guidellm")]
github_token: Annotated[str, Field(default="")]
# SDLC KPIs: classify PRs (comma-separated; matched case-insensitively on label name).
# Stored as str so empty .env values do not break settings parsing. Use
# sdlc_metrics._parse_sdlc_label_csv for set semantics.
# Precedence when multiple match: bug fix > docs > feature (see sdlc_metrics.classify_pr_type).
sdlc_feature_labels: Annotated[str, Field(default="enhancement,feature")]
sdlc_bug_labels: Annotated[str, Field(default="bug")]
sdlc_docs_labels: Annotated[str, Field(default="documentation")]
sdlc_escape_label: Annotated[str, Field(default="escape")]


context = Settings()
65 changes: 65 additions & 0 deletions backend/src/github_pm/sdlc_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""SDLC KPI REST endpoints (GitHub-backed)."""

from __future__ import annotations

from typing import Annotated

from fastapi import APIRouter, Depends, Query

from github_pm import sdlc_service
from github_pm.api import connection, Connector
from github_pm.context import context
from github_pm.sdlc_models import (
BugBacklogSeriesResponse,
DeliverySeriesResponse,
EscapedDefectSeriesResponse,
)

sdlc_router = APIRouter(prefix="/sdlc", tags=["sdlc"])


@sdlc_router.get("/delivery", response_model=DeliverySeriesResponse)
async def get_sdlc_delivery(
gitctx: Annotated[Connector, Depends(connection)],
weeks: Annotated[int, Query(ge=1, le=52)] = 4,
week_days: Annotated[int, Query(ge=1, le=90)] = 7,
):
"""
Delivery metrics: merged PR throughput, median cycle time, median time to first human review,
repeated for each of the last ``weeks`` windows of ``week_days`` days (oldest slice first).

Each slice window is ``(slice_end - week_days, slice_end]`` in UTC. PRs authored by bots
(Dependabot, Mergify, etc.) are excluded from all delivery stats. Reviews exclude GitHub bots.
"""
return sdlc_service.compute_sdlc_delivery_series(
gitctx, context, weeks=weeks, week_days=week_days
)


@sdlc_router.get("/escaped-defect-rate", response_model=EscapedDefectSeriesResponse)
async def get_escaped_defect_rate(
gitctx: Annotated[Connector, Depends(connection)],
weeks: Annotated[int, Query(ge=1, le=52)] = 4,
week_days: Annotated[int, Query(ge=1, le=90)] = 7,
):
"""
Escaped defect metrics per week (oldest slice first). Milestone rows match the cumulative
endpoint (next open line plus two previous minors), but counts are **incremental** within
each ``week_days`` window: PRs merged into the milestone and escape issues **created** in
that window (same prior-milestone attribution). Bot-authored PRs are excluded from denominators.
"""
return sdlc_service.compute_escaped_defect_rate_series(
gitctx, context, weeks=weeks, week_days=week_days
)


@sdlc_router.get("/bug-backlog-delta", response_model=BugBacklogSeriesResponse)
async def get_bug_backlog_delta(
gitctx: Annotated[Connector, Depends(connection)],
weeks: Annotated[int, Query(ge=1, le=52)] = 4,
week_days: Annotated[int, Query(ge=1, le=90)] = 7,
):
"""Bug issues opened, closed, and net per week (``weeks`` slices of ``week_days``, oldest first)."""
return sdlc_service.compute_bug_backlog_delta_series(
gitctx, context, weeks=weeks, week_days=week_days
)
Loading
Loading