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
105 changes: 105 additions & 0 deletions .github/scripts/extract_exports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
"""
.github/scripts/extract_exports.py

Walks R/, finds every function tagged with @export, and extracts its full
roxygen2 documentation block. Outputs a JSON blob to GITHUB_OUTPUT so that
upgrade_readme.py can consume it without re-parsing the R files.

Also deterministically picks a section focus for this week based on the
ISO week number, so the README improves a different area each week in rotation.
"""

import os
import re
import sys
import json
from datetime import date

SEARCH_DIR = "R"

SECTION_ROTATION = [
"Getting Started / Installation",
"Usage Examples",
"Function Reference Table",
"Motivation & Use Cases",
"FAQ / Common Pitfalls",
"Contributing & Development Setup",
]


def extract_function_docs(filepath: str) -> list[dict]:
"""
Returns a list of dicts, one per @export-tagged function:
{ "name": str, "file": str, "roxygen": str }
"""
with open(filepath, "r", errors="ignore") as f:
lines = f.readlines()

results = []
for i, line in enumerate(lines):
m = re.match(r"^(\w+)\s*(?:<-|=)\s*function\s*\(", line)
if not m:
continue
fn_name = m.group(1)

# Collect the contiguous roxygen2 block above this line
roxygen_lines = []
j = i - 1
while j >= 0 and re.match(r"^#'", lines[j]):
roxygen_lines.insert(0, lines[j].rstrip())
j -= 1

if any("@export" in l for l in roxygen_lines):
results.append({
"name": fn_name,
"file": filepath,
"roxygen": "\n".join(roxygen_lines),
})

return results


def main():
all_exports = []

for root, _dirs, files in os.walk(SEARCH_DIR):
for fname in sorted(files):
if not fname.endswith(".R"):
continue
fpath = os.path.join(root, fname)
try:
all_exports.extend(extract_function_docs(fpath))
except Exception as e:
print(f"⚠️ Skipping {fpath}: {e}", file=sys.stderr)

if not all_exports:
print(
"❌ No @export-tagged functions found in R/.\n"
" Add #' @export above your exported function definitions.",
file=sys.stderr,
)
sys.exit(1)

print(f"✅ Found {len(all_exports)} exported functions:")
for exp in all_exports:
print(f" • {exp['name']} ({exp['file']})")

# Rotate section focus by ISO week number so it's deterministic
week_number = date.today().isocalendar().week
section_focus = SECTION_ROTATION[week_number % len(SECTION_ROTATION)]
print(f"\n📌 This week's README focus: {section_focus}")

exports_json = json.dumps(all_exports)

github_output = os.environ.get("GITHUB_OUTPUT")
if github_output:
with open(github_output, "a") as fh:
fh.write(f"exports_json={exports_json}\n")
fh.write(f"section_focus={section_focus}\n")
else:
print(f"\nEXPORTS_JSON={exports_json}")
print(f"SECTION_FOCUS={section_focus}")


if __name__ == "__main__":
main()
106 changes: 106 additions & 0 deletions .github/scripts/upgrade_readme.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""
.github/scripts/upgrade_readme.py

Reads the current README.md and the full list of exported functions (from
EXPORTS_JSON), then asks the AI to make one focused, cohesive improvement
to the README based on the week's section focus.

Required environment variables:
OPENAI_API_KEY - your OpenAI secret key
EXPORTS_JSON - JSON string produced by extract_exports.py
"""

import os
import sys
import json
from openai import OpenAI

README_PATH = "README.md"

PROMPT_TEMPLATE = """\
You are a technical writer helping improve the README for an R package.

## Current README

```markdown
{readme_content}
```

## Exported Functions

Below are all the exported functions in this package, along with their \
current roxygen2 documentation:

{exports_block}

## Your Task

Make a single, cohesive improvement to the README focused on: **{section_focus}**

Guidelines:
- Improve or add the "{section_focus}" section using the exported functions above as your source of truth.
- Write clean, idiomatic R code examples that actually work.
- Keep the tone friendly and practical — help someone get up and running quickly.
- Do not remove or substantially alter any existing sections outside your focus area.
- Do not add placeholder text like "coming soon" or "TODO".
- Return ONLY the full updated README.md contents, with no explanation or markdown fences around it.
"""


def format_exports_block(exports: list[dict]) -> str:
blocks = []
for exp in exports:
blocks.append(f"### `{exp['name']}` ({exp['file']})\n\n{exp['roxygen']}")
return "\n\n---\n\n".join(blocks)


def main():
api_key = os.environ.get("OPENAI_API_KEY")
exports_json = os.environ.get("EXPORTS_JSON")

missing = [k for k, v in {
"OPENAI_API_KEY": api_key,
"EXPORTS_JSON": exports_json,
}.items() if not v]

if missing:
print(f"❌ Missing required environment variables: {', '.join(missing)}", file=sys.stderr)
sys.exit(1)

exports = json.loads(exports_json)
section_focus = os.environ.get("SECTION_FOCUS", "Usage Examples")

if not os.path.exists(README_PATH):
print(f"❌ {README_PATH} not found in repo root.", file=sys.stderr)
sys.exit(1)

with open(README_PATH, "r") as f:
readme_content = f.read()

exports_block = format_exports_block(exports)

prompt = PROMPT_TEMPLATE.format(
readme_content=readme_content,
exports_block=exports_block,
section_focus=section_focus,
)

client = OpenAI(api_key=api_key)
print(f"⏳ Calling OpenAI to improve README (focus: {section_focus}) ...")

response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}],
temperature=0.4,
)

updated_readme = response.choices[0].message.content.strip()

with open(README_PATH, "w") as f:
f.write(updated_readme)

print(f"✅ README updated successfully.")


if __name__ == "__main__":
main()
82 changes: 82 additions & 0 deletions .github/workflows/weekly-readme-upgrade.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# .github/workflows/weekly-readme-upgrade.yml
#
# Runs every week, reads all exported R functions + their roxygen2 docs,
# and asks the AI to make one cohesive improvement to the README.
# Each week it rotates focus: examples, getting started, function reference, etc.
#
# SETUP: Same prerequisites as weekly-doc-upgrade.yml
# - MSSTATS_OPENAI_KEY secret
# - Actions read/write permissions

name: Weekly README Upgrade

on:
schedule:
- cron: '0 9 * * 2' # Every Tuesday at 9am UTC (day after the roxygen2 job)
workflow_dispatch:

jobs:
readme-upgrade:
runs-on: ubuntu-latest

steps:
- name: Checkout repo
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Install dependencies
run: pip install openai

- name: Extract exported functions
id: extract
run: python3 .github/scripts/extract_exports.py

- name: Upgrade README with AI
env:
OPENAI_API_KEY: ${{ secrets.MSSTATS_OPENAI_KEY }}
EXPORTS_JSON: ${{ steps.extract.outputs.exports_json }}
run: python3 .github/scripts/upgrade_readme.py

- name: Create branch and commit
run: |
BRANCH="ai-docs/readme-$(date +%Y-%m-%d)"
echo "BRANCH=$BRANCH" >> $GITHUB_ENV

git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

git checkout -b "$BRANCH"
git add README.md
git commit -m "docs: Weekly AI README upgrade ($(date +%Y-%m-%d))"
git push origin "$BRANCH"

- name: Open Pull Request
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SECTION_FOCUS: ${{ steps.extract.outputs.section_focus }}
run: |
gh pr create \
--title "docs: Weekly AI README upgrade — ${SECTION_FOCUS}" \
--body "## 🤖 Automated README Upgrade

This PR was auto-generated by the weekly README upgrade workflow.

**This week's focus:** ${SECTION_FOCUS}

The AI was given the full list of exported functions and their roxygen2
documentation, and made a single cohesive improvement to \`README.md\`.

**What to check:**
- [ ] The added content is accurate
- [ ] Code examples actually run
- [ ] The tone and style match the rest of the README
- [ ] No existing content was accidentally removed

> Merge if it looks good. Each week the bot will focus on a different section." \
--base devel \
--head "$BRANCH" \
--label "documentation"