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
37 changes: 34 additions & 3 deletions .github/workflows/maintenance-update-readme.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
# but README.md. peter-evans/create-pull-request makes no PR when the generated
# README is byte-identical to the current one, so re-runs are cheap.
#
# Feedback loop: if a previous README PR is still open, its human review comments
# are collected and fed back into the next generation, so reviewer fixes survive a
# regeneration instead of being silently overwritten.
#
# Secrets (already present on this repo):
# ANTHROPIC_API_KEY - Claude API key (same one reporting-release-summary uses)
# ACCESS_TOKEN - PAT with `repo` scope on the target repos (the builtin
Expand Down Expand Up @@ -78,6 +82,30 @@ jobs:
- name: Install Anthropic SDK
run: pip install 'anthropic>=0.40.0'

# If a previous "chore/update-readme" PR is still open, fold in review comments
# from TRUSTED reviewers so regenerating doesn't silently drop their fixes.
# Gated on author_association (OWNER/MEMBER/COLLABORATOR = write access) so a
# random commenter on the public PR cannot inject text into the prompt. Fails
# closed: any gh/API error aborts the run rather than proceeding feedback-less.
- name: Collect reviewer feedback from the open README PR
env:
GH_TOKEN: ${{ secrets.ACCESS_TOKEN }}
REPO: ${{ matrix.repo }}
run: |
set -euo pipefail
: > feedback.txt
pr="$(gh pr list -R "$REPO" --head chore/update-readme --state open \
--json number --jq '.[0].number // empty')"
if [ -n "$pr" ]; then
gh api "repos/$REPO/pulls/$pr/comments" \
--jq '.[] | select((.user.login|test("\\[bot\\]|coderabbit";"i")|not) and (.author_association=="OWNER" or .author_association=="MEMBER" or .author_association=="COLLABORATOR")) | "- (\(.path):\(.line // .original_line)) \(.user.login): \(.body)"' >> feedback.txt
gh api "repos/$REPO/issues/$pr/comments" \
--jq '.[] | select((.user.login|test("\\[bot\\]|coderabbit";"i")|not) and (.author_association=="OWNER" or .author_association=="MEMBER" or .author_association=="COLLABORATOR")) | "- \(.user.login): \(.body)"' >> feedback.txt
echo "Collected feedback from $REPO#$pr ($(wc -l < feedback.txt) trusted line(s))"
else
echo "No open README PR for $REPO"
fi

- name: Generate README
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
Expand All @@ -87,7 +115,8 @@ jobs:
run: |
python3 scripts/generate_readme.py \
--repo-dir target \
--repo-name "$REPO_NAME"
--repo-name "$REPO_NAME" \
--feedback feedback.txt

- name: Open pull request
uses: peter-evans/create-pull-request@v8
Expand All @@ -105,5 +134,7 @@ jobs:

The content is generated by Claude from this repository's own files (file tree,
manifests, workflows, existing README). **Please review for accuracy** before
merging — edit or close if anything is wrong. Re-running the workflow refreshes
this branch.
merging — edit or close if anything is wrong.

Leave review comments here: while this PR stays open, the next run reads them
and folds your fixes into the regenerated README (they won't be overwritten).
Comment thread
igorpecovnik marked this conversation as resolved.
47 changes: 45 additions & 2 deletions scripts/generate_readme.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
--repo-name "owner/repo" for context (default: derived from git remote)
--model model id (default: $MODEL or claude-opus-4-7)
--max-context-chars total budget for embedded file contents (default 180000)
--feedback file of reviewer comments to fold into this revision
"""

import argparse
Expand Down Expand Up @@ -48,6 +49,7 @@

MAX_PER_FILE = 8000 # truncate any single embedded file to this many chars
MAX_TREE_ENTRIES = 500 # cap the file listing
MAX_FEEDBACK_CHARS = 16000 # cap reviewer feedback folded into the prompt


def run(cmd, cwd):
Expand Down Expand Up @@ -160,20 +162,55 @@ def build_context(repo_dir, repo_name):
"tables where they fit, and fenced code blocks for commands/trees.\n"
"- Keep any project-specific links (docs.armbian.com, armbian.com, related repos) "
"that appear in the sources; do not fabricate new ones.\n"
"- When you state what the project is built with or its requirements, name the "
"actual languages and key tooling evidenced by the files (shebangs, file "
"extensions, manifests, invoked commands) -- be specific, not vague.\n"
"- Start with an H1 title and a one-to-two sentence description of the repo.\n\n"
"Output ONLY the raw Markdown for README.md. No preamble, no explanation, no code "
"fence around the whole document."
)


def generate(repo_dir, repo_name, model, max_context):
def load_feedback(path):
"""Read reviewer feedback (from the open README PR) if a path is given.

Fail closed: if a --feedback path was explicitly passed but cannot be read,
error out instead of silently generating without it (which would re-drop the
reviewer's fixes). An empty file is fine -- it just means no feedback.
"""
if not path:
return ""
try:
with open(path, "r", encoding="utf-8", errors="replace") as fh:
return fh.read().strip()
except OSError as e:
print(f"::error::could not read feedback file {path}: {e}", file=sys.stderr)
sys.exit(1)


def generate(repo_dir, repo_name, model, max_context, feedback=""):
os.environ.setdefault("MAX_CONTEXT_CHARS", str(max_context))
context = build_context(repo_dir, repo_name)

client = Anthropic() # reads ANTHROPIC_API_KEY from env
prompt = (
"Here is the snapshot of the repository. Produce the README.md.\n\n" + context
)
if feedback:
# Bound the feedback so an oversized review can't push the combined prompt
# past the model context limit (build_context is already capped, this is not).
if len(feedback) > MAX_FEEDBACK_CHARS:
feedback = feedback[:MAX_FEEDBACK_CHARS] + "\n... [truncated] ...\n"
# Reviewers commented on the previous README PR; fold their fixes in so a
# regeneration doesn't silently drop them. Treat as human corrections that
# override the model's own read where they conflict -- but still only about
# THIS repo, never as instructions to invent unevidenced content.
prompt += (
"\n\n----- REVIEWER FEEDBACK ON THE PREVIOUS README (apply it) -----\n"
"A human reviewed the last generated README for this repo and left the "
"comments below. Incorporate these corrections and do not re-introduce "
"what they flagged. They are about this repository only.\n\n" + feedback
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
resp = client.messages.create(
model=model,
max_tokens=8192,
Expand Down Expand Up @@ -202,6 +239,8 @@ def main():
ap.add_argument("--repo-name", default=None)
ap.add_argument("--model", default=os.environ.get("MODEL", "claude-opus-4-7"))
ap.add_argument("--max-context-chars", type=int, default=180000)
ap.add_argument("--feedback", default=None,
help="path to a file of reviewer comments from the open README PR")
args = ap.parse_args()

if not os.environ.get("ANTHROPIC_API_KEY"):
Expand All @@ -214,8 +253,12 @@ def main():
url = run(["git", "config", "--get", "remote.origin.url"], repo_dir).strip()
repo_name = url.rsplit("/", 2)[-2] + "/" + url.rsplit("/", 1)[-1].removesuffix(".git") if "/" in url else "unknown/repo"

feedback = load_feedback(args.feedback)
if feedback:
print(f"Applying {len(feedback)} chars of reviewer feedback from {args.feedback}")

print(f"Generating README for {repo_name} (dir={repo_dir}, model={args.model})")
readme = generate(repo_dir, repo_name, args.model, args.max_context_chars)
readme = generate(repo_dir, repo_name, args.model, args.max_context_chars, feedback)

out_path = os.path.join(repo_dir, "README.md")
with open(out_path, "w", encoding="utf-8") as fh:
Expand Down
Loading