Skip to content
Merged
4 changes: 3 additions & 1 deletion .github/workflows/update-readme.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ jobs:
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git add README.md README_DATA.json
if ! git diff --cached --quiet; then
git commit -m "docs: update contributions artifacts"
# Only generated files change here, and a bot-pushed commit would
# otherwise start a Tests run that waits for manual approval.
git commit -m "docs: update contributions artifacts [skip ci]"
git push
else
echo "No changes to commit"
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# OSS Contributions

| Project | 🐛 Bug fixes | ✨ Features | ♻️ Refactors | 🔧 Chores | Total Merged |
| :--- | :---: | :---: | :---: | :---: | :---: |
| <a href="https://github.com/Kilo-Org/kilocode"><img src="https://github.com/Kilo-Org.png" width="24" height="24" style="vertical-align:middle;" title="Kilo Code" alt="Kilo Code"/></a> | 14 | 2 | 1 | 2 | **19** |

# 2026

## September
Expand Down
38 changes: 38 additions & 0 deletions README_DATA.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,44 @@
{
"title": "OSS Contributions",
"featured_projects": [],
"stats": {
"columns": [
{
"category": "fix",
"label": "Bug fixes",
"emoji": "🐛"
},
{
"category": "feat",
"label": "Features",
"emoji": "✨"
},
{
"category": "refactor",
"label": "Refactors",
"emoji": "♻️"
},
{
"category": "chore",
"label": "Chores",
"emoji": "🔧"
}
],
"projects": [
{
"name": "Kilo Code",
"repo_url": "https://github.com/Kilo-Org/kilocode",
"logo_url": "https://github.com/Kilo-Org.png",
"total_merged": 19,
"categories": {
"fix": 14,
"feat": 2,
"refactor": 1,
"chore": 2
}
}
]
},
"years": [
{
"year": 2026,
Expand Down
46 changes: 45 additions & 1 deletion __test__/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from datetime import datetime

from src import config, model
from helpers import SAMPLE_DATA, grouped_mock
from helpers import SAMPLE_DATA, grouped_mock, mock_pr


class TestBuildModel(unittest.TestCase):
Expand Down Expand Up @@ -106,5 +106,49 @@ def test_featured_projects_sorted_by_order(self):
self.assertEqual(names, ['o/a', 'o/b'])


class TestStats(unittest.TestCase):
def setUp(self):
self.data, self.featured = grouped_mock([
mock_pr("fix: a", "u1", 1, "MERGED", "Org/proj", "2026-02-01T00:00:00Z"),
mock_pr("fix(ui): b", "u2", 2, "OPEN", "Org/proj", "2026-01-01T00:00:00Z"),
mock_pr("feat: c", "u3", 3, "MERGED", "Org/proj", "2025-12-01T00:00:00Z"),
mock_pr("Tidy things", "u4", 4, "MERGED", "Org/proj", "2025-12-02T00:00:00Z"),
mock_pr("fix: d", "u5", 5, "MERGED", "other/repo", "2026-01-01T00:00:00Z"),
])

def test_no_stats_unless_opted_in(self):
m = model.build_readme_model(self.data, self.featured)
self.assertEqual(m['stats'], {'columns': [], 'projects': []})

def test_counts_merged_prs_by_category_across_months(self):
m = model.build_readme_model(self.data, self.featured, ['org/proj'])
[project] = m['stats']['projects']
self.assertEqual(project['name'], 'Org/proj')
# The OPEN fix is left out.
self.assertEqual(project['categories'], {'fix': 1, 'feat': 1, 'other': 1})
self.assertEqual(project['total_merged'], 3)
self.assertEqual([c['category'] for c in m['stats']['columns']], ['fix', 'feat', 'other'])

def test_repo_with_only_open_prs_is_skipped(self):
data, featured = grouped_mock([
mock_pr("fix: a", "u1", 1, "OPEN", "o/r", "2026-01-01T00:00:00Z"),
])
m = model.build_readme_model(data, featured, ['o/r'])
self.assertEqual(m['stats']['projects'], [])

def test_repo_without_prs_is_skipped(self):
m = model.build_readme_model(self.data, self.featured, ['nobody/here'])
self.assertEqual(m['stats']['projects'], [])

def test_group_sums_its_repos_into_one_row(self):
group = {'name': 'Org', 'repos': ['org/proj', 'Other/Repo', 'nobody/here']}
m = model.build_readme_model(self.data, self.featured, [group])
[project] = m['stats']['projects']
self.assertEqual(project['name'], 'Org')
self.assertEqual(project['repo_url'], 'https://github.com/Org/proj')
self.assertEqual(project['categories'], {'fix': 2, 'feat': 1, 'other': 1})
self.assertEqual(project['total_merged'], 4)


if __name__ == "__main__":
unittest.main(verbosity=2)
26 changes: 25 additions & 1 deletion oss-contributions.json
Original file line number Diff line number Diff line change
@@ -1 +1,25 @@
{"username": "hdcodedev", "repos": ["Kilo-Org/kilocode", "ImranR98/Obtainium", "home-assistant/android", "metabrainz/listenbrainz-android", "openai/codex", "kilo-org/kilocode-legacy"], "statuses": ["MERGED", "OPEN"], "featured_projects": []}
{
"username": "hdcodedev",
"repos": [
"Kilo-Org/kilocode",
"ImranR98/Obtainium",
"home-assistant/android",
"metabrainz/listenbrainz-android",
"openai/codex",
"kilo-org/kilocode-legacy"
],
"statuses": [
"MERGED",
"OPEN"
],
"featured_projects": [],
"stats_projects": [
{
"name": "Kilo Code",
"repos": [
"Kilo-Org/kilocode",
"kilo-org/kilocode-legacy"
]
}
]
}
2 changes: 1 addition & 1 deletion src/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def main():
print(f"Featured projects: {len(config['featured_projects'])}")

data, featured_repos = fetch_from_config(config)
model = build_readme_model(data, featured_repos)
model = build_readme_model(data, featured_repos, config['stats_projects'])

if _count_contributions(model) == 0:
print("No contributions found; refusing to overwrite existing artifacts.")
Expand Down
21 changes: 21 additions & 0 deletions src/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,24 @@ def custom_logo(repo_name):
'revert': '⏪',
}

# Conventional-commit prefix -> label used in the stats table.
CATEGORY_LABELS = {
'fix': 'Bug fixes',
'feat': 'Features',
'refactor': 'Refactors',
'perf': 'Performance',
'docs': 'Docs',
'test': 'Tests',
'chore': 'Chores',
'build': 'Build',
'ci': 'CI',
'style': 'Style',
'revert': 'Reverts',
}

# Bucket for titles without a known conventional-commit prefix.
OTHER_CATEGORY = 'other'

# Keyword fallback -> emoji (checked against the start of the title).
KEYWORD_EMOJI = {
'add': '✨',
Expand All @@ -97,6 +115,9 @@ def load_config(config_file=None):
"repos": data.get("repos", []),
"statuses": [status.upper() for status in data.get("statuses", ["MERGED", "OPEN"])],
"featured_projects": data.get("featured_projects", []),
# Repos that get a per-category merged-PR counter at the top of the README; an
# entry may also be {"name": ..., "repos": [...]} to sum repos into one row.
"stats_projects": data.get("stats_projects", []),
# None means "whoever the gh token belongs to".
"username": data.get("username"),
}
71 changes: 65 additions & 6 deletions src/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,40 @@
from itertools import groupby

from .config import (
CATEGORY_LABELS,
CONVENTIONAL_EMOJI,
DEFAULT_PR_EMOJI,
DEFAULT_STATUS_ICON,
KEYWORD_EMOJI,
OTHER_CATEGORY,
STATUS_ICONS,
STATUS_LEGEND,
custom_logo,
)
from .github import fetch_authored_prs, get_repo_details


def _conventional_prefix(title):
match = re.match(r'^(\w+)(?:\([^)]+\))?:', title.lower().strip())
return match.group(1) if match else None


def get_pr_emoji(title):
prefix = _conventional_prefix(title)
if prefix in CONVENTIONAL_EMOJI:
return CONVENTIONAL_EMOJI[prefix]
title_lower = title.lower().strip()
match = re.match(r'^(\w+)(?:\([^)]+\))?:', title_lower)
if match:
prefix = match.group(1)
if prefix in CONVENTIONAL_EMOJI:
return CONVENTIONAL_EMOJI[prefix]
for keyword, emoji in KEYWORD_EMOJI.items():
if title_lower.startswith(keyword):
return emoji
return DEFAULT_PR_EMOJI


def get_pr_category(title):
prefix = _conventional_prefix(title)
return prefix if prefix in CATEGORY_LABELS else OTHER_CATEGORY


def query_states(allowed_statuses):
"""Translate config statuses into GraphQL ``PullRequestState`` values.

Expand Down Expand Up @@ -106,7 +116,54 @@ def fetch_from_config(config):
return contributions_by_date, featured_repos


def build_readme_model(contributions_by_date, featured_repos):
def build_stats(years, stats_projects):
"""Count merged PRs per category for each opted-in project, in config order.

An entry is either a repo name or ``{"name": ..., "repos": [...]}``,
whose repos are summed into one row. Built from the rendered rows, so
private repos never reach the counts. Projects with no merged PRs are
left out.
"""
counts = defaultdict(lambda: defaultdict(int))
rows_by_repo = {}
for year in years:
for month in year['months']:
for row in month['rows']:
if row['status'] != 'MERGED':
continue
key = row['repo_name'].lower()
rows_by_repo[key] = row
for item in row['contributions']:
counts[key][item['category']] += 1

category_order = [*CATEGORY_LABELS, OTHER_CATEGORY]
projects = []
for entry in stats_projects:
if isinstance(entry, str):
entry = {'name': None, 'repos': [entry]}
keys = [repo.lower() for repo in entry['repos'] if repo.lower() in counts]
if not keys:
continue
# Link and logo come from the first repo in the entry that has PRs.
row = rows_by_repo[keys[0]]
merged = {c: sum(counts[key][c] for key in keys) for c in category_order}
projects.append({
'name': entry['name'] or row['repo_name'],
'repo_url': row['repo_url'],
'logo_url': row['logo_url'],
'total_merged': sum(merged.values()),
'categories': {c: n for c, n in merged.items() if n},
})

used = {c for project in projects for c in project['categories']}
columns = [
{'category': c, 'label': CATEGORY_LABELS.get(c, 'Other'), 'emoji': CONVENTIONAL_EMOJI.get(c, DEFAULT_PR_EMOJI)}
for c in category_order if c in used
]
return {'columns': columns, 'projects': projects}


def build_readme_model(contributions_by_date, featured_repos, stats_projects=()):
featured_projects = []
if featured_repos:
sorted_featured = sorted(featured_repos.items(), key=lambda item: (item[1], item[0].lower()))
Expand Down Expand Up @@ -158,6 +215,7 @@ def build_readme_model(contributions_by_date, featured_repos):
cell_title = pr['title'].replace('|', '\\|')
contributions.append({
'emoji': emoji,
'category': get_pr_category(pr['title']),
'number': pr['number'],
'title': pr['title'],
'url': pr['url'],
Expand Down Expand Up @@ -193,6 +251,7 @@ def build_readme_model(contributions_by_date, featured_repos):
return {
'title': 'OSS Contributions',
'featured_projects': featured_projects,
'stats': build_stats(years, stats_projects),
'years': years,
'status_legend': STATUS_LEGEND,
}
17 changes: 17 additions & 0 deletions src/render.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,22 @@ def generate_markdown(contributions_by_date, featured_repos, output_file="README
with open(output_file, "w") as f:
f.write("# OSS Contributions\n\n")

stats = model['stats']
if stats['projects']:
header = ["Project", *(f"{c['emoji']} {c['label']}" for c in stats['columns']), "Total Merged"]
f.write(f"| {' | '.join(header)} |\n")
f.write(f"| :--- |{' :---: |' * (len(header) - 1)}\n")
for project in stats['projects']:
repo_display = (
f"<a href=\"{project['repo_url']}\">"
f"<img src=\"{project['logo_url']}\" width=\"24\" height=\"24\" style=\"vertical-align:middle;\" "
f"title=\"{project['name']}\" alt=\"{project['name']}\"/>"
"</a>"
)
cells = [str(project['categories'].get(c['category'], 0)) for c in stats['columns']]
f.write(f"| {repo_display} | {' | '.join(cells)} | **{project['total_merged']}** |\n")
f.write("\n")

if model['featured_projects']:
f.write("## Featured Projects\n\n")
f.write("<p float=\"left\">\n")
Expand Down Expand Up @@ -58,6 +74,7 @@ def generate_json_snapshot(contributions_by_date, featured_repos, output_file="R
json_model = {
'title': model['title'],
'featured_projects': model['featured_projects'],
'stats': model['stats'],
'years': [],
}

Expand Down
Loading