Skip to content
Open
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
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,19 @@ Automated daily notification system that fetches open PRs from [FilOzone GitHub
***Periodic Runs:***
This notifier is scheduled to run periodically per [./github/workflows/fog-wg-pr-notifier.yml](fog-wg-pr-notifier.yml).

GraphQL fetch logic for Project 14 lives in [`foc_project14_client.py`](foc_project14_client.py) (shared with the FOC PR report).

### 📋 FOC PR report (Markdown)
**Directory:** [foc-pr-report/](foc-pr-report/)

Generates a Markdown table of PR workload on [Project 14 / View 2](https://github.com/orgs/FilOzone/projects/14/views/2): counts per GitHub user and board status (excludes Done and Todo), with deep links for each cell. Run with **`uv`**:

```bash
cd foc-pr-report && uv sync && GITHUB_TOKEN=your_token uv run foc-pr-report -o report.md
```

See [foc-pr-report/README.md](foc-pr-report/README.md).

### 🎯 GitHub Milestone Manager
**Directory:** [github-milestone-creator/](github-milestone-creator/)

Expand Down
2 changes: 2 additions & 0 deletions foc-pr-report/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.venv/
__pycache__/
76 changes: 76 additions & 0 deletions foc-pr-report/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# FOC PR report (Project 14)

Generates a Markdown table of pull requests on [FilOzone Project 14](https://github.com/orgs/FilOzone/projects/14), grouped by GitHub user and board **Status**. PRs in **Done** or **Todo** are excluded. All PR states (open/closed/draft) are included if they remain on the board with a qualifying status.

Requested **team** reviewers are not attributed to user rows (same as other tpm-utils tooling).

## Token

You need credentials that can read FilOzone org data and **GitHub Projects (classic + v2)**. The GraphQL API requires the **`read:project`** scope. (`repo` and `read:org` alone are not enough.)

### GitHub CLI (recommended)

1. Ensure Project scope is on your login (default host `github.com` must be an account that can see FilOzone Project 14):

```bash
gh auth refresh -s read:project
```

2. Pass the token to this tool:

```bash
GITHUB_TOKEN=$(gh auth token) uv run foc-pr-report -o foc-report.md
```

Or export it for the shell session:

```bash
export GITHUB_TOKEN=$(gh auth token)
uv run foc-pr-report
```

If you see an **INSUFFICIENT_SCOPES** / **read:project** error, run `gh auth refresh -s read:project` (or `gh auth login` and include project read when asked). For org-owned projects, accept **organization** access for FilOzone if prompted.

### Personal access token

Alternatively set `GITHUB_TOKEN` to a classic PAT from [GitHub → Settings → Developer settings → Tokens](https://github.com/settings/tokens) and enable **`read:project`** (plus **`read:org`** and repo read as needed).

## Run with uv

```bash
cd foc-pr-report
uv sync
GITHUB_TOKEN=$(gh auth token) uv run foc-pr-report -o foc-report.md
```

Or print to stdout:

```bash
GITHUB_TOKEN=$(gh auth token) uv run foc-pr-report
```

## How fetching works

The tool uses the **REST** [List project items](https://docs.github.com/en/rest/projects/items#list-items-for-an-organization-owned-project) endpoint with the same **`q`** filter string as the board (for example `is:pr` plus `-status:"…"`). GitHub applies that filter **server-side**, so only matching cards are returned and paginated—unlike listing the entire project via GraphQL when the board is very large.

### Reviewer column vs GitHub APIs

The **project table’s “Reviewers” column** in the GitHub UI is a single place to show review-related people, but the data comes from more than one backend concept:

| Concept | What it is | Typical API |
|--------|------------|-------------|
| **Requested reviewers** | Users (or teams) currently asked to review **this PR**; this is what powers “awaiting review” for those names. | On each card, the REST payload exposes this as `requested_reviewers` (often the same as [`requested_reviewers` on the pull request](https://docs.github.com/en/rest/pulls/pulls#get-a-pull-request)). |
| **Submitted pull request reviews** | A formal review action: **Comment**, **Approve**, or **Request changes** (and related states), as listed under [List reviews for a pull request](https://docs.github.com/en/rest/pulls/reviews#list-reviews-for-a-pull-request). A **COMMENTED** submission is still a review, not the same as a random **issue comment** on the conversation tab. | `GET /repos/{owner}/{repo}/pulls/{pull_number}/reviews` |

Those two can diverge. For example, you might **submit a review** (so the UI can list you under **Reviewers**), while newer requests go to other people—so you are **no longer** in `requested_reviewers`, but you still appear on the board and match filters like a bare `username`.

**What this tool does:** For each PR in the report, the **reviewer** counts use the **union** of (1) `requested_reviewers` on the card and (2) human users who have submitted at least one non-`PENDING` pull request review (same review list as above), **excluding the PR author**. That is meant to stay close to how filtering by person on the board behaves and what you see in the **Reviewers** column—not “everyone who left an issue comment.”

## Output tables

1. **PR count by individual** — rows are GitHub users × board status; assignee and reviewer counts match the semantics above. Rows labeled **empty** (after all named users) count PRs with **no assignee** in the **assignee** column and PRs with **no reviewer** in the **reviewer** column (same reviewer union as above—only user logins; team-only review requests still show as empty here). The **assignee** cell links add `no:assignee`; the **reviewer** cell links add GitHub’s `review:none` (no submitted pull request reviews), which can differ slightly from this row’s count when a user is requested but has not submitted a review yet. The **empty** label itself links to the base filter only.
2. **PR count by repository and status** — rows are repositories (`owner/repo`), columns are statuses present in the filtered set, plus a **Total** column (row sums) and **Total** row (column sums). Repository names link to the base filter plus `repo:owner/name`. Status column headers link to the base filter plus `status:"…"` only. Each non-zero cell links to that repo and status combined. The **Total** column header and the bottom-right grand total link to the base filter only; row totals link like the repo row; the total row’s status cells link like the status column headers.

## Links

[View 2](https://github.com/orgs/FilOzone/projects/14/views/2) `filterQuery` values use the same base filter as the tool (`is:pr` excluding Done/Todo), plus qualifiers per cell as described above.
1 change: 1 addition & 0 deletions foc-pr-report/foc_pr_report/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""FOC project 14 PR markdown report."""
85 changes: 85 additions & 0 deletions foc-pr-report/foc_pr_report/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#!/usr/bin/env python3
"""CLI: fetch FilOzone Project 14 and print Markdown workload table."""

from __future__ import annotations

import argparse
import os
import sys
from pathlib import Path

import requests

# Repo layout: tpm-utils/foc-pr-report/foc_pr_report/cli.py → parents[2] == tpm-utils
_TPM_UTILS_ROOT = Path(__file__).resolve().parents[2]
if str(_TPM_UTILS_ROOT) not in sys.path:
sys.path.insert(0, str(_TPM_UTILS_ROOT))

from foc_project14_client import ( # noqa: E402
enrich_pull_items_with_submitted_reviewers,
fetch_project_board_items_rest_filtered,
)

from foc_pr_report.report import ( # noqa: E402
BASE_FILTER,
aggregate_rows,
render_full_markdown,
)


def main() -> None:
parser = argparse.ArgumentParser(
description="Generate Markdown table of FOC (Project 14) PR counts by person and status",
)
parser.add_argument(
"--token",
help="GitHub token (or set GITHUB_TOKEN)",
)
parser.add_argument(
"-o",
"--output",
help="Write Markdown to this file (default: stdout)",
)
parser.add_argument(
"-q",
"--quiet",
action="store_true",
help="Less progress output from the GitHub client",
)
args = parser.parse_args()

token = args.token or os.environ.get("GITHUB_TOKEN")
if not token:
print("Error: set GITHUB_TOKEN or pass --token", file=sys.stderr)
sys.exit(1)

session = requests.Session()
session.headers.update(
{
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
}
)

items = fetch_project_board_items_rest_filtered(
session,
filter_query=BASE_FILTER,
verbose=not args.quiet,
)
enrich_pull_items_with_submitted_reviewers(
session,
items,
verbose=not args.quiet,
)
md = render_full_markdown(items, aggregate_rows(items))

if args.output:
Path(args.output).write_text(md, encoding="utf-8")
if not args.quiet:
print(f"Wrote {args.output}")
else:
sys.stdout.write(md)


if __name__ == "__main__":
main()
Loading
Loading