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
23 changes: 23 additions & 0 deletions .github/workflows/update-readme.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,19 @@ jobs:
with:
python-version: '3.x'

# The profile repo's README shows the same stats tables, so the token
# needs write access to it as well.
- name: Checkout profile repository
uses: actions/checkout@v7
with:
repository: hdcodedev/hdcodedev
token: ${{ secrets.OSS_CONTRIBUTIONS_TOKEN }}
path: profile

- name: Run generation script
env:
OSS_CONTRIBUTIONS_TOKEN: ${{ secrets.OSS_CONTRIBUTIONS_TOKEN }}
PROFILE_README: profile/README.md
run: |
set -e
python3 generate_readme.py
Expand Down Expand Up @@ -58,3 +68,16 @@ jobs:
else
echo "No changes to commit"
fi

- name: Commit and push profile README
working-directory: profile
run: |
git config --local user.name "github-actions[bot]"
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git add README.md
if ! git diff --cached --quiet; then
git commit -m "Update OSS contribution stats"
git push
else
echo "No profile changes to commit"
fi
55 changes: 55 additions & 0 deletions __test__/test_render.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""Tests for the profile README stats block (src.render)."""

import os
import tempfile
import unittest

from src import model, render
from helpers import grouped_mock, mock_pr


def write_readme(text):
handle = tempfile.NamedTemporaryFile('w', suffix='.md', delete=False)
handle.write(text)
handle.close()
return handle.name


class TestUpdateProfileReadme(unittest.TestCase):
def setUp(self):
data, featured = grouped_mock([
mock_pr("fix: a", "u1", 1, "MERGED", "Org/proj", "2026-01-01T00:00:00Z"),
])
self.model = model.build_readme_model(data, featured, ['org/proj'])

def update(self, text):
path = write_readme(text)
try:
render.update_profile_readme(path, self.model)
with open(path) as f:
return f.read()
finally:
os.unlink(path)

def test_replaces_only_the_marked_block(self):
text = self.update(
"# Hi\n\n<!-- oss-stats:start -->\nold\n<!-- oss-stats:end -->\n\nMore below.\n"
)
self.assertEqual(
text,
"# Hi\n\n<!-- oss-stats:start -->\n"
f"{render.render_stats(self.model)}"
"<!-- oss-stats:end -->\n\nMore below.\n",
)

def test_running_twice_gives_the_same_file(self):
once = self.update("<!-- oss-stats:start --><!-- oss-stats:end -->\n")
self.assertEqual(self.update(once), once)

def test_missing_markers_raise(self):
with self.assertRaises(ValueError):
self.update("# Hi\n\n<!-- oss-stats:start -->\n")


if __name__ == "__main__":
unittest.main(verbosity=2)
9 changes: 8 additions & 1 deletion src/cli.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
"""Command-line entry point."""

import os
import sys

from .config import load_config
from .model import fetch_from_config, build_readme_model
from .render import generate_json_snapshot, generate_markdown
from .render import generate_json_snapshot, generate_markdown, update_profile_readme


def _count_contributions(model):
Expand Down Expand Up @@ -37,6 +38,12 @@ def main():
generate_markdown(data, featured_repos, model=model)
generate_json_snapshot(data, featured_repos, model=model)

# Set by the workflow to a checkout of the GitHub profile repo.
profile_readme = os.environ.get("PROFILE_README")
if profile_readme:
update_profile_readme(profile_readme, model)
print("Profile README stats updated.")

print("Done! README.md and README_DATA.json updated.")
return 0

Expand Down
71 changes: 50 additions & 21 deletions src/render.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,34 +15,63 @@ def _project_cell(project):
)


def render_stats(model):
"""The merged-PR and reported-issue tables, shared by README and profile."""
lines = []

stats = model['stats']
if stats['projects']:
header = ["Project", *(f"{c['emoji']} {c['label']}" for c in stats['columns']), "Total Merged"]
lines.append(f"| {' | '.join(header)} |")
lines.append(f"| :--- |{' :---: |' * (len(header) - 1)}")
for project in stats['projects']:
cells = [str(project['categories'].get(c['category'], 0)) for c in stats['columns']]
lines.append(f"| {_project_cell(project)} | {' | '.join(cells)} | **{project['total_merged']}** |")
lines.append("")

issue_stats = model['issue_stats']
if issue_stats['projects']:
lines.append(f"### {ISSUE_EMOJI} {issue_stats['title']}")
lines.append("")
header = ["Project", *(f"{c['emoji']} {c['label']}" for c in issue_stats['columns']), "Total Reported"]
lines.append(f"| {' | '.join(header)} |")
lines.append(f"| :--- |{' :---: |' * (len(header) - 1)}")
for project in issue_stats['projects']:
cells = [str(project['statuses'].get(c['status'], 0)) for c in issue_stats['columns']]
lines.append(f"| {_project_cell(project)} | {' | '.join(cells)} | **{project['total_reported']}** |")
lines.append("")

return "".join(f"{line}\n" for line in lines)


PROFILE_START = "<!-- oss-stats:start -->"
PROFILE_END = "<!-- oss-stats:end -->"


def update_profile_readme(path, model):
"""Replace the block between the oss-stats markers in ``path``.

Missing markers raise, so a profile README edited by hand fails the run
instead of silently losing its stats.
"""
with open(path) as f:
text = f.read()
before, start, rest = text.partition(PROFILE_START)
_, end, after = rest.partition(PROFILE_END)
if not start or not end:
raise ValueError(f"{path} needs {PROFILE_START} and {PROFILE_END} markers")
with open(path, "w") as f:
f.write(f"{before}{PROFILE_START}\n{render_stats(model)}{PROFILE_END}{after}")


def generate_markdown(contributions_by_date, featured_repos, output_file="README.md", model=None):
"""Render the grouped contributions into a Markdown README."""
if model is None:
model = build_readme_model(contributions_by_date, featured_repos)

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']:
cells = [str(project['categories'].get(c['category'], 0)) for c in stats['columns']]
f.write(f"| {_project_cell(project)} | {' | '.join(cells)} | **{project['total_merged']}** |\n")
f.write("\n")

issue_stats = model['issue_stats']
if issue_stats['projects']:
f.write(f"### {ISSUE_EMOJI} {issue_stats['title']}\n\n")
header = ["Project", *(f"{c['emoji']} {c['label']}" for c in issue_stats['columns']), "Total Reported"]
f.write(f"| {' | '.join(header)} |\n")
f.write(f"| :--- |{' :---: |' * (len(header) - 1)}\n")
for project in issue_stats['projects']:
cells = [str(project['statuses'].get(c['status'], 0)) for c in issue_stats['columns']]
f.write(f"| {_project_cell(project)} | {' | '.join(cells)} | **{project['total_reported']}** |\n")
f.write("\n")
f.write(render_stats(model))

if model['featured_projects']:
f.write("## Featured Projects\n\n")
Expand Down
Loading