From cf868b94506af5ecb395add1d155bdd3dfc34eec Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Wed, 15 Apr 2026 09:06:11 +0900 Subject: [PATCH 01/47] feat: add new agentic workflows and scripts --- .github/workflows/pr-quality-check.yml | 27 +++ .github/workflows/security-review.yml | 43 +++++ .github/workflows/triage.yml | 25 +++ scripts/pr_checker_agent.py | 149 ++++++++++++++++ scripts/security_review_agent.py | 200 +++++++++++++++++++++ scripts/triage_agent.py | 233 +++++++++++++++++++++++++ 6 files changed, 677 insertions(+) create mode 100644 .github/workflows/pr-quality-check.yml create mode 100644 .github/workflows/security-review.yml create mode 100644 .github/workflows/triage.yml create mode 100644 scripts/pr_checker_agent.py create mode 100644 scripts/security_review_agent.py create mode 100644 scripts/triage_agent.py diff --git a/.github/workflows/pr-quality-check.yml b/.github/workflows/pr-quality-check.yml new file mode 100644 index 000000000..e4ab3c5a5 --- /dev/null +++ b/.github/workflows/pr-quality-check.yml @@ -0,0 +1,27 @@ +name: PR Quality Check +on: + pull_request: + types: [opened, reopened] + +jobs: + pr_quality_check: + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - run: pip install anthropic PyGithub + - name: Run PR quality check agent + env: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + AUTHOR_USERNAME: ${{ github.event.pull_request.user.login }} + AUTHOR_ASSOCIATION: ${{ github.event.pull_request.author_association }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + REPO_NAME: ${{ github.repository }} + PR_TITLE: ${{ github.event.pull_request.title }} + PR_BODY: ${{ github.event.pull_request.body }} + run: python scripts/pr_checker_agent.py \ No newline at end of file diff --git a/.github/workflows/security-review.yml b/.github/workflows/security-review.yml new file mode 100644 index 000000000..12db3640f --- /dev/null +++ b/.github/workflows/security-review.yml @@ -0,0 +1,43 @@ +name: Security Review + +on: + pull_request: + types: [opened, reopened] + issue_comment: + types: [created] + +jobs: + security-review: + runs-on: ubuntu-latest + + # Always runs on PR creation + # Also runs if comment on PR contains "/security-review" + if: > + github.event_name == 'pull_request' || + ( + github.event_name == 'issue_comment' && + github.event.issue.pull_request != null && + contains(github.event.comment.body, '/security-review') + ) + + permissions: + issues: write + pull-requests: write + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - run: pip install anthropic "PyGithub>=2.0" + + - name: Run security review agent + env: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO_NAME: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} + TRIGGER: ${{ github.event_name }} + run: python scripts/security_review_agent.py \ No newline at end of file diff --git a/.github/workflows/triage.yml b/.github/workflows/triage.yml new file mode 100644 index 000000000..f20748a96 --- /dev/null +++ b/.github/workflows/triage.yml @@ -0,0 +1,25 @@ +name: Issue Triage +on: + issues: + types: [opened, reopened] + +jobs: + triage: + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - run: pip install anthropic PyGithub + - name: Run triage agent + env: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + REPO_NAME: ${{ github.repository }} + ISSUE_TITLE: ${{ github.event.issue.title }} + ISSUE_BODY: ${{ github.event.issue.body }} + run: python scripts/triage_agent.py \ No newline at end of file diff --git a/scripts/pr_checker_agent.py b/scripts/pr_checker_agent.py new file mode 100644 index 000000000..2420efe6f --- /dev/null +++ b/scripts/pr_checker_agent.py @@ -0,0 +1,149 @@ +import os +import anthropic +from github import Github, Auth + +# Setup + +gh = Github(auth=Auth.Token(os.environ["GITHUB_TOKEN"])) +repo = gh.get_repo(os.environ["REPO_NAME"]) +pr = repo.get_pull(int(os.environ["PR_NUMBER"])) +author = os.environ["AUTHOR_USERNAME"] +client = anthropic.Anthropic() + +# Tools + +TOOLS = [ + { + "name": "post_comment", + "description": ( + "Post a comment on the PR. Use this to welcome a first-time contributor, " + "ask for a clearer description, request an issue link, or flag non-compliance " + "with CONTRIBUTING.md. Combine multiple concerns into a single comment where " + "possible rather than posting several separate ones." + ), + "input_schema": { + "type": "object", + "properties": { + "body": {"type": "string", "description": "The comment text (markdown supported)."} + }, + "required": ["body"], + }, + }, +] + +# System prompt + +SYSTEM_PROMPT = """You are a PR review assistant for an open-source GitHub repository. +Given a newly opened PR, its author's contribution history, and the repository's CONTRIBUTING.md, +you must check the following — in this order: + +1. FIRST CONTRIBUTION: If this is the author's first contribution to the repo, welcome them warmly. + Acknowledge their effort and point them to any relevant getting-started resources in CONTRIBUTING.md. + +2. DESCRIPTION CLARITY: If the PR description is missing, too vague, or doesn't explain what + the change does and why, ask for a clearer description. + +3. LINKED ISSUE: Check whether the description contains a linked issue using keywords like + "Fixes #N", "Closes #N", "Resolves #N", or "Related to #N". If no issue is linked, + ask the author to either link an existing issue or create a new one. + +4. CONTRIBUTING.md COMPLIANCE: Check whether the PR description follows the structure or + requirements defined in CONTRIBUTING.md. If it doesn't comply, quote the relevant section + and point out specifically what needs to change. + +Important rules: +- If multiple concerns apply, combine them into a single comment — never post more than one. +- If everything looks good, stay silent. Do not post a comment just to say things look fine. +- Be warm and constructive, never demanding. Remember this may be someone's first open-source contribution. +- When referencing CONTRIBUTING.md requirements, be specific — quote or paraphrase the rule, + don't just say "please read the contributing guide". +- Most importantly, be as succint as possible.""" + +# GitHub helpers + +def get_contributing_md() -> str: + """Fetches CONTRIBUTING.md from the repo root, or returns a notice if absent.""" + try: + contents = repo.get_contents("CONTRIBUTING.md") + return contents.decoded_content.decode("utf-8") + except Exception: + return "(No CONTRIBUTING.md found in this repository.)" + + +def is_first_contribution() -> bool: + """Returns True if the author has no previously merged PRs in this repo.""" + first_contribution_list = ['FIRST_TIMER', 'FIRST_TIME_CONTRIBUTOR', 'NONE'] + if os.environ["AUTHOR_ASSOCIATION"] in first_contribution_list: + return True + return False + + +def post_comment(body: str) -> str: + pr.create_issue_comment(body) + return "Comment posted." + +# Tool dispatch + +def handle_tool_call(name: str, inputs: dict) -> str: + if name == "post_comment": + result = post_comment(inputs["body"]) + else: + result = f"Unknown tool: {name}" + + print(f"[tool] {name}: {result}") + return result + +# Agentic loop + +def build_initial_message() -> str: + first_contribution = is_first_contribution() + contributing_md = get_contributing_md() + + return ( + f"Please review this newly opened PR:\n\n" + f"Title: {os.environ['PR_TITLE']}\n" + f"Author: {author} ({'first-time contributor' if first_contribution else 'returning contributor'})\n" + f"Description:\n{os.environ.get('PR_BODY') or '(no description provided)'}\n\n" + f"---\n" + f"CONTRIBUTING.md contents:\n\n" + f"{contributing_md}" + ) + + +def run_pr_review_agent(): + messages = [{"role": "user", "content": build_initial_message()}] + + while True: + response = client.messages.create( + model="claude-sonnet-4-20250514", + max_tokens=1024, + system=SYSTEM_PROMPT, + tools=TOOLS, + messages=messages, + ) + + for block in response.content: + if block.type == "text" and block.text: + print(f"[agent] {block.text}") + + messages.append({"role": "assistant", "content": response.content}) + + if response.stop_reason == "end_turn": + break + + tool_results = [] + for block in response.content: + if block.type != "tool_use": + continue + result = handle_tool_call(block.name, block.input) + tool_results.append({ + "type": "tool_result", + "tool_use_id": block.id, + "content": result, + }) + + messages.append({"role": "user", "content": tool_results}) + + +if __name__ == "__main__": + run_pr_review_agent() \ No newline at end of file diff --git a/scripts/security_review_agent.py b/scripts/security_review_agent.py new file mode 100644 index 000000000..c83ec8937 --- /dev/null +++ b/scripts/security_review_agent.py @@ -0,0 +1,200 @@ +import os +import anthropic +from github import Github, Auth + +# Setup + +gh = Github(auth=Auth.Token(os.environ["GITHUB_TOKEN"])) +repo = gh.get_repo(os.environ["REPO_NAME"]) +pr = repo.get_pull(int(os.environ["PR_NUMBER"])) +client = anthropic.Anthropic() + +# Exclude files that are not useful for security analysis +IGNORED_FILENAMES = { + "package-lock.json", + "yarn.lock", + "poetry.lock", + "Gemfile.lock", + "Cargo.lock", + "composer.lock", + "pnpm-lock.yaml", + "pip.lock", +} + +IGNORED_EXTENSIONS = {".lock", ".sum"} + +# Truncate very large diffs like generated files to prevent bloating the prompt +MAX_PATCH_CHARS_PER_FILE = 3000 + +# System prompt + +SYSTEM_PROMPT = """You are a security analysis assistant for a GitHub repository. +You are given the diff of a pull request and must identify potential security issues. + +Focus only on security-relevant concerns such as: +- Hardcoded secrets, tokens, passwords or API keys +- Injection vulnerabilities (SQL, shell, template, etc.) +- Insecure use of cryptography or hashing +- Unsafe deserialization +- Path traversal or directory traversal risks +- Insecure direct object references +- Missing input validation or sanitisation on user-controlled data +- Use of known-vulnerable dependency versions (if visible in the diff) +- Overly permissive file or network access + +Do NOT comment on code style, performance, test coverage, or general best practices +unless they have a direct security implication. + +If you find no issues, say so clearly and briefly — do not invent concerns. +Format your response as a markdown comment suitable for posting directly on a GitHub PR. +Start with a short summary line, then list findings with file references where applicable. +If there are no findings, keep the response to 2-3 sentences maximum.""" + +# GitHub helpers + +def get_pr_diff() -> str: + """ + Fetches changed files and their patches, filtering out lockfiles and + other noise. Returns a formatted string ready to be included in the prompt. + """ + sections = [] + for f in pr.get_files(): + filename = os.path.basename(f.filename) + _, ext = os.path.splitext(filename) + + if filename in IGNORED_FILENAMES or ext in IGNORED_EXTENSIONS: + print(f"[diff] Skipping {f.filename} (ignored file type)") + continue + + if not f.patch: + print(f"[diff] Skipping {f.filename} (no patch — binary or too large)") + continue + + patch = f.patch[:MAX_PATCH_CHARS_PER_FILE] + truncated = len(f.patch) > MAX_PATCH_CHARS_PER_FILE + sections.append( + f"### {f.filename}\n```diff\n{patch}" + + ("\n... (truncated)" if truncated else "") + + "\n```" + ) + + return "\n\n".join(sections) if sections else "(no reviewable changes found)" + + +def find_previous_security_comment() -> object | None: + """ + Looks for an existing security review comment posted by github-actions[bot] + so we can replace it rather than stacking multiple comments on updated reviews. + """ + for comment in pr.get_issue_comments(): + if ( + comment.user.login == "github-actions[bot]" + and "🔒 Automated Security Review" in comment.body + ): + return comment + return None + + +def post_or_update_comment(body: str): + """ + If a previous security review comment exists, edit it in place. + Otherwise post a new one to keep the PR timeline clean. + """ + existing = find_previous_security_comment() + if existing: + existing.edit(body) + print("[comment] Updated existing security review comment.") + else: + pr.create_issue_comment(body) + print("[comment] Posted new security review comment.") + +# Tools + +TOOLS = [ + { + "name": "post_security_review", + "description": ( + "Post the security review findings as a comment on the PR. " + "Call this once when your analysis is complete. " + "If there are no findings, still call this to confirm the review ran." + ), + "input_schema": { + "type": "object", + "properties": { + "body": { + "type": "string", + "description": "The full markdown comment body to post on the PR.", + } + }, + "required": ["body"], + }, + } +] + +# Tool dispatch + +def handle_tool_call(name: str, inputs: dict) -> str: + if name == "post_security_review": + # Prepend a header to identify review comments across runs + body = f"## 🔒 Automated Security Review\n\n{inputs['body']}" + post_or_update_comment(body) + return "Security review comment posted." + return f"Unknown tool: {name}" + +# Agentic loop + +def build_initial_message() -> str: + trigger = os.environ.get("TRIGGER", "pull_request") + trigger_note = ( + "This review was requested manually via `/security-review`." + if trigger == "issue_comment" + else "This review was triggered automatically on PR creation." + ) + + return ( + f"Please perform a security review of this pull request.\n\n" + f"**PR #{pr.number}:** {pr.title}\n" + f"_{trigger_note}_\n\n" + f"---\n\n" + f"{get_pr_diff()}" + ) + + +def run_security_review_agent(): + messages = [{"role": "user", "content": build_initial_message()}] + + while True: + response = client.messages.create( + model="claude-sonnet-4-20250514", + max_tokens=1024, + system=SYSTEM_PROMPT, + tools=TOOLS, + messages=messages, + ) + + for block in response.content: + if block.type == "text" and block.text: + print(f"[agent] {block.text}") + + messages.append({"role": "assistant", "content": response.content}) + + if response.stop_reason == "end_turn": + break + + tool_results = [] + for block in response.content: + if block.type != "tool_use": + continue + result = handle_tool_call(block.name, block.input) + print(f"[tool] {block.name}: {result}") + tool_results.append({ + "type": "tool_result", + "tool_use_id": block.id, + "content": result, + }) + + messages.append({"role": "user", "content": tool_results}) + + +if __name__ == "__main__": + run_security_review_agent() \ No newline at end of file diff --git a/scripts/triage_agent.py b/scripts/triage_agent.py new file mode 100644 index 000000000..b11fc82f4 --- /dev/null +++ b/scripts/triage_agent.py @@ -0,0 +1,233 @@ +import os +import anthropic +from github import Github, Auth + +# Setup + +gh = Github(auth=Auth.Token(os.environ["GITHUB_TOKEN"])) +repo = gh.get_repo(os.environ["REPO_NAME"]) +issue = repo.get_issue(int(os.environ["ISSUE_NUMBER"])) +client = anthropic.Anthropic() + +LATEST_ISSUES_LIMIT = 100 + +# Tools + +TOOLS = [ + { + "name": "apply_label", + "description": ( + "Apply one or more labels to the issue. " + "Use labels like: automation, bug, dependencies, " + "documentation, enhancement, good-first-issue, " + "meeting, needs-info, plugins, protocol, question, " + "security, tech-debt, testing." + ), + "input_schema": { + "type": "object", + "properties": { + "labels": { + "type": "array", + "items": {"type": "string"}, + "description": "List of labels to apply.", + } + }, + "required": ["labels"], + }, + }, + { + "name": "post_comment", + "description": "Post a comment on the issue, e.g. to ask for clarification or acknowledge receipt.", + "input_schema": { + "type": "object", + "properties": { + "body": {"type": "string", "description": "The comment text (markdown supported)."} + }, + "required": ["body"], + }, + }, + { + "name": "mark_duplicate", + "description": ( + "Mark this issue as a duplicate of an existing one. " + "Use this when the issue is clearly asking about the same thing as an open issue. " + "This will post a comment pointing to the original, however the issue will remain open for maintainers to address." + ), + "input_schema": { + "type": "object", + "properties": { + "original_issue_number": { + "type": "integer", + "description": "The issue number this is a duplicate of.", + }, + "reason": { + "type": "string", + "description": "Brief explanation of why these issues are duplicates.", + }, + }, + "required": ["original_issue_number", "reason"], + }, + }, + { + "name": "suggest_possible_duplicate", + "description": ( + "Use when an existing issue is related but not clearly the same thing. " + "Posts a comment pointing to the similar issue without closing anything. " + "Triage should still continue normally after calling this." + ), + "input_schema": { + "type": "object", + "properties": { + "related_issue_number": { + "type": "integer", + "description": "The issue number that might be related.", + }, + "reason": { + "type": "string", + "description": "Brief explanation of why these issues seem related.", + }, + }, + "required": ["related_issue_number", "reason"], + }, + }, +] + +# System prompt + +SYSTEM_PROMPT = """You are an issue triage assistant for a GitHub repository. +Given a new issue and a list of existing open issues, you must: + +1. Check whether the new issue is a duplicate of an existing one. + - If it clearly is the same issue, call mark_duplicate and stop — do not label or acknowledge further. + - If it seems related but could be distinct, call suggest_possible_duplicate. That comment + will serve as the acknowledgment too, so do NOT post a separate acknowledgment afterward. +2. Otherwise, classify it by applying appropriate labels + (bug, feature-request, question, documentation, needs-info, good-first-issue). +3. If the issue is missing key info (steps to reproduce for bugs, use case for features, etc.), + post a friendly comment asking for it. +4. If no possible duplicate was flagged, post a short acknowledgment comment so the + author knows their issue was received. Do NOT post comments on administrative issues + such as meeting minutes, roadmaps, etc. + +Keep comments concise and friendly.""" + +# GitHub helpers + +def get_existing_issues(limit: int = LATEST_ISSUES_LIMIT) -> str: + """ + Fetches the most recent open issues (excluding the current one) + and formats them into a string for the prompt. + """ + open_issues = repo.get_issues(state="open") + lines = [] + count = 0 + for existing in open_issues: + if existing.number == issue.number: + continue + lines.append( + f"- #{existing.number}: {existing.title}\n" + f" {(existing.body or '').strip()[:200]}" # truncate long bodies + ) + count += 1 + if count >= limit: + break + return "\n".join(lines) if lines else "(no other open issues)" + + +def apply_label(labels: list[str]) -> str: + existing_label_names = [l.name for l in repo.get_labels()] + for label in labels: + if label not in existing_label_names: + repo.create_label(label, "ededed") + issue.add_to_labels(*labels) + return f"Applied labels: {labels}" + + +def post_comment(body: str) -> str: + issue.create_comment(body) + return "Comment posted." + + +def mark_duplicate(original_issue_number: int, reason: str) -> str: + original = repo.get_issue(original_issue_number) + issue.create_comment( + f"Thanks for the report! This looks like a duplicate of #{original_issue_number} " + f"({original.html_url}).\n\n> {reason}\n\n" + f"Please edit this issue to add any distinguishing details if you believe it's not a duplicate." + ) + issue.add_to_labels("duplicate") + return f"Marked as duplicate of #{original_issue_number}." + + +def suggest_possible_duplicate(related_issue_number: int, reason: str) -> str: + related = repo.get_issue(related_issue_number) + issue.create_comment( + f"Hey! This might be related to #{related_issue_number} " + f"({related.html_url}) — {reason}\n\n" + f"Feel free to check if that one already covers what you're reporting!" + ) + return f"Flagged as possibly related to #{related_issue_number}." + + +# Tool dispatch + +def handle_tool_call(name: str, inputs: dict) -> str: + if name == "apply_label": + result = apply_label(inputs["labels"]) + elif name == "post_comment": + result = post_comment(inputs["body"]) + elif name == "mark_duplicate": + result = mark_duplicate(inputs["original_issue_number"], inputs["reason"]) + elif name == "suggest_possible_duplicate": + result = suggest_possible_duplicate(inputs["related_issue_number"], inputs["reason"]) + else: + result = f"Unknown tool: {name}" + print(f"Tool {name}: {result}") + return result + +# Agentic loop + +def build_initial_message() -> str: + return ( + f"Please triage this new GitHub issue:\n\n" + f"Title: {os.environ['ISSUE_TITLE']}\n" + f"Body:\n{os.environ.get('ISSUE_BODY') or '(no description provided)'}\n\n" + f"---\n" + f"Here are the currently open issues for duplicate detection:\n\n" + f"{get_existing_issues()}" + ) + + +def run_triage_agent(): + messages = [{"role": "user", "content": build_initial_message()}] + + while True: + response = client.messages.create( + model="claude-sonnet-4-20250514", + max_tokens=1024, + system=SYSTEM_PROMPT, + tools=TOOLS, + messages=messages, + ) + + messages.append({"role": "assistant", "content": response.content}) + + if response.stop_reason == "end_turn": + break + + tool_results = [] + for block in response.content: + if block.type != "tool_use": + continue + result = handle_tool_call(block.name, block.input) + tool_results.append({ + "type": "tool_result", + "tool_use_id": block.id, + "content": result, + }) + + messages.append({"role": "user", "content": tool_results}) + + +if __name__ == "__main__": + run_triage_agent() From a4d1482b09f347d98e9d3330a1b70110aa67ba02 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Wed, 15 Apr 2026 14:31:13 +0900 Subject: [PATCH 02/47] feat: add agentic issue tagging, PR description quality and security review --- .github/workflows/pr-quality-check.yml | 33 ++++ .github/workflows/security-review.yml | 42 ++++ .github/workflows/triage.yml | 31 +++ scripts/pr_checker_agent.py | 161 ++++++++++++++++ scripts/security_review_agent.py | 214 +++++++++++++++++++++ scripts/triage_agent.py | 256 +++++++++++++++++++++++++ 6 files changed, 737 insertions(+) create mode 100644 .github/workflows/pr-quality-check.yml create mode 100644 .github/workflows/security-review.yml create mode 100644 .github/workflows/triage.yml create mode 100644 scripts/pr_checker_agent.py create mode 100644 scripts/security_review_agent.py create mode 100644 scripts/triage_agent.py diff --git a/.github/workflows/pr-quality-check.yml b/.github/workflows/pr-quality-check.yml new file mode 100644 index 000000000..9790283c4 --- /dev/null +++ b/.github/workflows/pr-quality-check.yml @@ -0,0 +1,33 @@ +name: PR Quality Check +on: + pull_request: + types: [opened, reopened] + +jobs: + pr_quality_check: + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - run: pip install litellm PyGithub + - name: Run PR quality check agent + env: + # e.g: "claude-sonnet-4-6", "gpt-4o", etc. + MODEL: ${{ secrets.MODEL }} + # Only API key for the chosen model is required + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Obtained automatically by GH Actions + AUTHOR_USERNAME: ${{ github.event.pull_request.user.login }} + AUTHOR_ASSOCIATION: ${{ github.event.pull_request.author_association }} + PR_NUMBER: ${{ github.event.pull_request.number }} + REPO_NAME: ${{ github.repository }} + PR_TITLE: ${{ github.event.pull_request.title }} + PR_BODY: ${{ github.event.pull_request.body }} + run: python scripts/pr_checker_agent.py diff --git a/.github/workflows/security-review.yml b/.github/workflows/security-review.yml new file mode 100644 index 000000000..08751138b --- /dev/null +++ b/.github/workflows/security-review.yml @@ -0,0 +1,42 @@ +name: Security Review +on: + pull_request: + types: [opened, reopened] + issue_comment: + types: [created] + +jobs: + security-review: + runs-on: ubuntu-latest + # Always runs on PR creation + # Also runs if comment on PR contains "/security-review" + if: > + github.event_name == 'pull_request' || + ( + github.event_name == 'issue_comment' && + github.event.issue.pull_request != null && + contains(github.event.comment.body, '/security-review') + ) + permissions: + issues: write + pull-requests: write + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - run: pip install litellm PyGithub + - name: Run security review agent + env: + # e.g: "claude-sonnet-4-6", "gpt-4o", etc. + MODEL: ${{ secrets.MODEL }} + # Only API key for the chosen model is required + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Obtained automatically by GH Actions + REPO_NAME: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} + TRIGGER: ${{ github.event_name }} + run: python scripts/security_review_agent.py diff --git a/.github/workflows/triage.yml b/.github/workflows/triage.yml new file mode 100644 index 000000000..b52bb72a0 --- /dev/null +++ b/.github/workflows/triage.yml @@ -0,0 +1,31 @@ +name: Issue Triage +on: + issues: + types: [opened, reopened] + +jobs: + triage: + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - run: pip install litellm PyGithub + - name: Run triage agent + env: + # e.g: "claude-sonnet-4-6", "gpt-4o", etc. + MODEL: ${{ secrets.MODEL }} + # Only API key for the chosen model is required + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Obtained automatically by GH Actions + ISSUE_NUMBER: ${{ github.event.issue.number }} + REPO_NAME: ${{ github.repository }} + ISSUE_TITLE: ${{ github.event.issue.title }} + ISSUE_BODY: ${{ github.event.issue.body }} + run: python scripts/triage_agent.py diff --git a/scripts/pr_checker_agent.py b/scripts/pr_checker_agent.py new file mode 100644 index 000000000..1b115a5fe --- /dev/null +++ b/scripts/pr_checker_agent.py @@ -0,0 +1,161 @@ +import os +import json +import litellm +from github import Github, Auth + +# Setup + +gh = Github(auth=Auth.Token(os.environ["GITHUB_TOKEN"])) +repo = gh.get_repo(os.environ["REPO_NAME"]) +pr = repo.get_pull(int(os.environ["PR_NUMBER"])) +author = os.environ["AUTHOR_USERNAME"] + +MODEL = os.environ["MODEL"] +for env_var in ["GITHUB_TOKEN", "REPO_NAME", "PR_NUMBER", "AUTHOR_USERNAME", "MODEL"]: + if not os.environ[env_var]: + raise ValueError(f"{env_var} is not set") + +valid_api_keys = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY"] +if not any(os.environ.get(api_key) for api_key in valid_api_keys): + raise ValueError("No API key is set") + + +# Tools + +TOOLS = [ + { + "type": "function", + "function": { + "name": "post_comment", + "description": ( + "Post a comment on the PR. Use this to welcome a first-time contributor, " + "ask for a clearer description, request an issue link, or flag non-compliance " + "with CONTRIBUTING.md. Combine multiple concerns into a single comment where " + "possible rather than posting several separate ones." + ), + "parameters": { + "type": "object", + "properties": { + "body": {"type": "string", "description": "The comment text (markdown supported)."} + }, + "required": ["body"], + }, + }, + }, +] + +# System prompt + +SYSTEM_PROMPT = """You are a PR review assistant for an open-source GitHub repository. +Given a newly opened PR, its author's contribution history, and the repository's CONTRIBUTING.md, +you must check the following - in this order: + +1. FIRST CONTRIBUTION: If this is the author's first contribution to the repo, welcome them warmly. + Acknowledge their effort and point them to any relevant getting-started resources in CONTRIBUTING.md. + +2. DESCRIPTION CLARITY: If the PR description is missing, too vague, or doesn't explain what + the change does and why, ask for a clearer description. + +3. LINKED ISSUE: Check whether the description contains a linked issue using keywords like + "Fixes #N", "Closes #N", "Resolves #N", or "Related to #N". If no issue is linked, + ask the author to either link an existing issue or create a new one. + +4. CONTRIBUTING.md COMPLIANCE: Check whether the PR description follows the structure or + requirements defined in CONTRIBUTING.md. If it doesn't comply, quote the relevant section + and point out specifically what needs to change. + +Important rules: +- If multiple concerns apply, combine them into a single comment, never post more than one. +- If everything looks good, stay silent. Do not post a comment just to say things look fine. +- Be warm and constructive, never demanding. Remember this may be someone's first open-source contribution. +- When referencing CONTRIBUTING.md requirements, be specific: quote or paraphrase the rule, + don't just say "please read the contributing guide". +- Most importantly, be as succinct as possible.""" + +# GitHub helpers + +def get_contributing_md() -> str: + """Fetches CONTRIBUTING.md from the repo root, or returns a notice if absent.""" + try: + contents = repo.get_contents("CONTRIBUTING.md") + return contents.decoded_content.decode("utf-8") + except Exception: + return "(No CONTRIBUTING.md found in this repository.)" + + +def is_first_contribution() -> bool: + """Returns True if the author has no previously merged PRs in this repo.""" + first_contribution_list = ['FIRST_TIMER', 'FIRST_TIME_CONTRIBUTOR', 'NONE'] + return os.environ["AUTHOR_ASSOCIATION"] in first_contribution_list + + +def post_comment(body: str) -> str: + pr.create_issue_comment(body) + return "Comment posted." + +# Tool dispatch + +def handle_tool_call(name: str, inputs: dict) -> str: + if name == "post_comment": + result = post_comment(inputs["body"]) + else: + result = f"Unknown tool: {name}" + + print(f"[tool] {name}: {result}") + return result + +# Agentic loop + +def build_initial_message() -> str: + first_contribution = is_first_contribution() + contributing_md = get_contributing_md() + + return ( + f"Please review this newly opened PR:\n\n" + f"Title: {os.environ['PR_TITLE']}\n" + f"Author: {author} ({'first-time contributor' if first_contribution else 'returning contributor'})\n" + f"Description:\n{os.environ.get('PR_BODY') or '(no description provided)'}\n\n" + f"---\n" + f"CONTRIBUTING.md contents:\n\n" + f"{contributing_md}" + ) + + +def run_pr_review_agent(): + messages = [ + {"role": "system", "content": SYSTEM_PROMPT}, + {"role": "user", "content": build_initial_message()}, + ] + + while True: + response = litellm.completion( + model=MODEL, + messages=messages, + tools=TOOLS, + ) + + message = response.choices[0].message + + if message.content: + print(f"[agent] {message.content}") + + messages.append(message.model_dump(exclude_none=True)) + + if response.choices[0].finish_reason == "stop" or not message.tool_calls: + break + + tool_results = [] + for tool_call in message.tool_calls: + inputs = json.loads(tool_call.function.arguments) + result = handle_tool_call(tool_call.function.name, inputs) + tool_results.append({ + "role": "tool", + "tool_call_id": tool_call.id, + "content": result, + }) + + messages.extend(tool_results) + + +if __name__ == "__main__": + run_pr_review_agent() diff --git a/scripts/security_review_agent.py b/scripts/security_review_agent.py new file mode 100644 index 000000000..6e1184d6c --- /dev/null +++ b/scripts/security_review_agent.py @@ -0,0 +1,214 @@ +import os +import json +import litellm +from github import Github, Auth + +# Setup + +gh = Github(auth=Auth.Token(os.environ["GITHUB_TOKEN"])) +repo = gh.get_repo(os.environ["REPO_NAME"]) +pr = repo.get_pull(int(os.environ["PR_NUMBER"])) + +MODEL = os.environ["MODEL"] +for env_var in ["GITHUB_TOKEN", "REPO_NAME", "PR_NUMBER", "MODEL"]: + if not os.environ[env_var]: + raise ValueError(f"{env_var} is not set") + +valid_api_keys = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY"] +if not any(os.environ.get(api_key) for api_key in valid_api_keys): + raise ValueError("No API key is set") + + +# Exclude files that are not useful for security analysis +IGNORED_FILENAMES = { + "package-lock.json", + "yarn.lock", + "poetry.lock", + "Gemfile.lock", + "Cargo.lock", + "composer.lock", + "pnpm-lock.yaml", + "pip.lock", +} + +IGNORED_EXTENSIONS = {".lock", ".sum"} + +# Truncate very large diffs like generated files to prevent bloating the prompt +MAX_PATCH_CHARS_PER_FILE = 3000 + +# System prompt + +SYSTEM_PROMPT = """You are a security analysis assistant for a GitHub repository. +You are given the diff of a pull request and must identify potential security issues. + +Focus only on security-relevant concerns such as: +- Hardcoded secrets, tokens, passwords or API keys +- Injection vulnerabilities (SQL, shell, template, etc.) +- Insecure use of cryptography or hashing +- Unsafe deserialization +- Path traversal or directory traversal risks +- Insecure direct object references +- Missing input validation or sanitisation on user-controlled data +- Use of known-vulnerable dependency versions (if visible in the diff) +- Overly permissive file or network access + +Do NOT comment on code style, performance, test coverage, or general best practices +unless they have a direct security implication. + +If you find no issues, say so clearly and briefly — do not invent concerns. +Format your response as a markdown comment suitable for posting directly on a GitHub PR. +Start with a short summary line, then list findings with file references where applicable. +If there are no findings, keep the response to 2-3 sentences maximum.""" + +# GitHub helpers + +def get_pr_diff() -> str: + """ + Fetches changed files and their patches, filtering out lockfiles and + other noise. Returns a formatted string ready to be included in the prompt. + """ + sections = [] + for f in pr.get_files(): + filename = os.path.basename(f.filename) + _, ext = os.path.splitext(filename) + + if filename in IGNORED_FILENAMES or ext in IGNORED_EXTENSIONS: + print(f"[diff] Skipping {f.filename} (ignored file type)") + continue + + if not f.patch: + print(f"[diff] Skipping {f.filename} (no patch — binary or too large)") + continue + + patch = f.patch[:MAX_PATCH_CHARS_PER_FILE] + truncated = len(f.patch) > MAX_PATCH_CHARS_PER_FILE + sections.append( + f"### {f.filename}\n```diff\n{patch}" + + ("\n... (truncated)" if truncated else "") + + "\n```" + ) + + return "\n\n".join(sections) if sections else "(no reviewable changes found)" + + +def find_previous_security_comment() -> object | None: + """ + Looks for an existing security review comment posted by github-actions[bot] + so we can replace it rather than stacking multiple comments on updated reviews. + """ + for comment in pr.get_issue_comments(): + if ( + comment.user.login == "github-actions[bot]" + and "🔒 Automated Security Review" in comment.body + ): + return comment + return None + + +def post_or_update_comment(body: str): + """ + If a previous security review comment exists, edit it in place. + Otherwise post a new one to keep the PR timeline clean. + """ + existing = find_previous_security_comment() + if existing: + existing.edit(body) + print("[comment] Updated existing security review comment.") + else: + pr.create_issue_comment(body) + print("[comment] Posted new security review comment.") + +# Tools + +TOOLS = [ + { + "type": "function", + "function": { + "name": "post_security_review", + "description": ( + "Post the security review findings as a comment on the PR. " + "Call this once when your analysis is complete. " + "If there are no findings, still call this to confirm the review ran." + ), + "parameters": { + "type": "object", + "properties": { + "body": { + "type": "string", + "description": "The full markdown comment body to post on the PR.", + } + }, + "required": ["body"], + }, + }, + } +] + +# Tool dispatch + +def handle_tool_call(name: str, inputs: dict) -> str: + if name == "post_security_review": + # Prepend a header to identify review comments across runs + body = f"## 🔒 Automated Security Review\n\n{inputs['body']}" + post_or_update_comment(body) + return "Security review comment posted." + return f"Unknown tool: {name}" + +# Agentic loop + +def build_initial_message() -> str: + trigger = os.environ.get("TRIGGER", "pull_request") + trigger_note = ( + "This review was requested manually via `/security-review`." + if trigger == "issue_comment" + else "This review was triggered automatically on PR creation." + ) + + return ( + f"Please perform a security review of this pull request.\n\n" + f"**PR #{pr.number}:** {pr.title}\n" + f"_{trigger_note}_\n\n" + f"---\n\n" + f"{get_pr_diff()}" + ) + + +def run_security_review_agent(): + messages = [ + {"role": "system", "content": SYSTEM_PROMPT}, + {"role": "user", "content": build_initial_message()}, + ] + + while True: + response = litellm.completion( + model=MODEL, + messages=messages, + tools=TOOLS, + ) + + message = response.choices[0].message + + if message.content: + print(f"[agent] {message.content}") + + messages.append(message.model_dump(exclude_none=True)) + + if response.choices[0].finish_reason == "stop" or not message.tool_calls: + break + + tool_results = [] + for tool_call in message.tool_calls: + inputs = json.loads(tool_call.function.arguments) + result = handle_tool_call(tool_call.function.name, inputs) + print(f"[tool] {tool_call.function.name}: {result}") + tool_results.append({ + "role": "tool", + "tool_call_id": tool_call.id, + "content": result, + }) + + messages.extend(tool_results) + + +if __name__ == "__main__": + run_security_review_agent() diff --git a/scripts/triage_agent.py b/scripts/triage_agent.py new file mode 100644 index 000000000..f5fdbb18d --- /dev/null +++ b/scripts/triage_agent.py @@ -0,0 +1,256 @@ +import os +import json +import litellm +from github import Github, Auth + +# Setup + +gh = Github(auth=Auth.Token(os.environ["GITHUB_TOKEN"])) +repo = gh.get_repo(os.environ["REPO_NAME"]) +issue = repo.get_issue(int(os.environ["ISSUE_NUMBER"])) + +LATEST_ISSUES_LIMIT = 100 +MODEL = os.environ["MODEL"] + +for env_var in ["GITHUB_TOKEN", "REPO_NAME", "ISSUE_NUMBER", "ISSUE_TITLE", "ISSUE_BODY", "MODEL"]: + if not os.environ[env_var]: + raise ValueError(f"{env_var} is not set") + +valid_api_keys = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY"] +if not any(os.environ.get(api_key) for api_key in valid_api_keys): + raise ValueError("No API key is set") + +# Tools + +TOOLS = [ + { + "type": "function", + "function": { + "name": "apply_label", + "description": ( + "Apply one or more labels to the issue. " + "Use labels like: automation, bug, dependencies, " + "documentation, enhancement, good-first-issue, " + "meeting, needs-info, plugins, protocol, question, " + "security, tech-debt, testing." + ), + "parameters": { + "type": "object", + "properties": { + "labels": { + "type": "array", + "items": {"type": "string"}, + "description": "List of labels to apply.", + } + }, + "required": ["labels"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "post_comment", + "description": "Post a comment on the issue, e.g. to ask for clarification or acknowledge receipt.", + "parameters": { + "type": "object", + "properties": { + "body": {"type": "string", "description": "The comment text (markdown supported)."} + }, + "required": ["body"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "mark_duplicate", + "description": ( + "Mark this issue as a duplicate of an existing one. " + "Use this when the issue is clearly asking about the same thing as an open issue. " + "Post a comment pointing to the original issue without closing anything." + ), + "parameters": { + "type": "object", + "properties": { + "original_issue_number": { + "type": "integer", + "description": "The issue number this is a duplicate of.", + }, + "reason": { + "type": "string", + "description": "Brief explanation of why these issues are duplicates.", + }, + }, + "required": ["original_issue_number", "reason"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "suggest_possible_duplicate", + "description": ( + "Use when an existing issue is related but not clearly the same thing. " + "Posts a comment pointing to the similar issue without closing anything." + "Continue triage normally after posting the comment." + ), + "parameters": { + "type": "object", + "properties": { + "related_issue_number": { + "type": "integer", + "description": "The issue number that might be related.", + }, + "reason": { + "type": "string", + "description": "Brief explanation of why these issues seem related.", + }, + }, + "required": ["related_issue_number", "reason"], + }, + }, + }, +] + +# System prompt + +SYSTEM_PROMPT = """You are an issue triage assistant for a GitHub repository. +Given a new issue and a list of existing open issues, you must: + +1. Check whether the new issue is a duplicate of an existing one. + - If it clearly is the same issue, call mark_duplicate and stop — do not label or acknowledge further. + - If it seems related but could be distinct, call suggest_possible_duplicate. That comment + will serve as the acknowledgment too, so do NOT post a separate acknowledgment afterward. +2. Otherwise, classify it by applying appropriate labels + (bug, feature-request, question, documentation, needs-info, good-first-issue). +3. If the issue is missing key info (steps to reproduce for bugs, use case for features, etc.), + post a friendly comment asking for it. +4. If no possible duplicate was flagged, post a short acknowledgment comment so the + author knows their issue was received. Do NOT post comments on administrative issues + such as meeting minutes, roadmaps, etc. + +Keep comments concise and friendly.""" + +# GitHub helpers + +def get_existing_issues(limit: int = LATEST_ISSUES_LIMIT) -> str: + """ + Fetches the most recent open issues (excluding the current one) + and formats them into a string for the prompt. + """ + open_issues = repo.get_issues(state="open") + lines = [] + count = 0 + for existing in open_issues: + if existing.number == issue.number: + continue + lines.append( + f"- #{existing.number}: {existing.title}\n" + f" {(existing.body or '').strip()[:200]}" # truncate long bodies + ) + count += 1 + if count >= limit: + break + return "\n".join(lines) if lines else "(no other open issues)" + + +def apply_label(labels: list[str]) -> str: + existing_label_names = [l.name for l in repo.get_labels()] + for label in labels: + if label not in existing_label_names: + repo.create_label(label, "ededed") + issue.add_to_labels(*labels) + return f"Applied labels: {labels}" + + +def post_comment(body: str) -> str: + issue.create_comment(body) + return "Comment posted." + + +def mark_duplicate(original_issue_number: int, reason: str) -> str: + original = repo.get_issue(original_issue_number) + issue.create_comment( + f"Thanks for the report! This looks like a duplicate of #{original_issue_number} " + f"({original.html_url}).\n\n> {reason}\n\n" + f"Please edit this issue to add any distinguishing details if you believe it's not a duplicate." + ) + issue.add_to_labels("duplicate") + return f"Marked as duplicate of #{original_issue_number}." + + +def suggest_possible_duplicate(related_issue_number: int, reason: str) -> str: + related = repo.get_issue(related_issue_number) + issue.create_comment( + f"Hey! This might be related to #{related_issue_number} " + f"({related.html_url}) — {reason}\n\n" + f"Feel free to check if that one already covers what you're reporting!" + ) + return f"Flagged as possibly related to #{related_issue_number}." + + +# Tool dispatch + +def handle_tool_call(name: str, inputs: dict) -> str: + if name == "apply_label": + result = apply_label(inputs["labels"]) + elif name == "post_comment": + result = post_comment(inputs["body"]) + elif name == "mark_duplicate": + result = mark_duplicate(inputs["original_issue_number"], inputs["reason"]) + elif name == "suggest_possible_duplicate": + result = suggest_possible_duplicate(inputs["related_issue_number"], inputs["reason"]) + else: + result = f"Unknown tool: {name}" + print(f"Tool {name}: {result}") + return result + +# Agentic loop + +def build_initial_message() -> str: + return ( + f"Please triage this new GitHub issue:\n\n" + f"Title: {os.environ['ISSUE_TITLE']}\n" + f"Body:\n{os.environ.get('ISSUE_BODY') or '(no description provided)'}\n\n" + f"---\n" + f"Here are the currently open issues for duplicate detection:\n\n" + f"{get_existing_issues()}" + ) + + +def run_triage_agent(): + messages = [ + {"role": "system", "content": SYSTEM_PROMPT}, + {"role": "user", "content": build_initial_message()}, + ] + + while True: + response = litellm.completion( + model=MODEL, + messages=messages, + tools=TOOLS, + ) + + message = response.choices[0].message + messages.append(message.model_dump(exclude_none=True)) + + finish_reason = response.choices[0].finish_reason + if finish_reason == "stop" or not message.tool_calls: + break + + tool_results = [] + for tool_call in message.tool_calls: + inputs = json.loads(tool_call.function.arguments) + result = handle_tool_call(tool_call.function.name, inputs) + tool_results.append({ + "role": "tool", + "tool_call_id": tool_call.id, + "content": result, + }) + + messages.extend(tool_results) + + +if __name__ == "__main__": + run_triage_agent() From cdd01cf314ec0694b418f6063ea7374bc3014c9b Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Wed, 15 Apr 2026 15:57:23 +0900 Subject: [PATCH 03/47] chore: update PR workflows to run on pull_request_target --- .github/workflows/pr-quality-check.yml | 2 +- .github/workflows/security-review.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-quality-check.yml b/.github/workflows/pr-quality-check.yml index 9790283c4..8ccaaa6ea 100644 --- a/.github/workflows/pr-quality-check.yml +++ b/.github/workflows/pr-quality-check.yml @@ -1,6 +1,6 @@ name: PR Quality Check on: - pull_request: + pull_request_target: types: [opened, reopened] jobs: diff --git a/.github/workflows/security-review.yml b/.github/workflows/security-review.yml index 08751138b..38c5194c4 100644 --- a/.github/workflows/security-review.yml +++ b/.github/workflows/security-review.yml @@ -1,6 +1,6 @@ name: Security Review on: - pull_request: + pull_request_target: types: [opened, reopened] issue_comment: types: [created] From ea6509abeb4c205a11b9ec0aa0875d9add342d10 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Fri, 17 Apr 2026 13:05:00 +0900 Subject: [PATCH 04/47] refactor: optimize system prompts and standardize outputs --- scripts/pr_checker_agent.py | 39 ++++++++++++------------- scripts/security_review_agent.py | 49 ++++++++++++++++++-------------- scripts/triage_agent.py | 43 +++++++++++++++------------- 3 files changed, 70 insertions(+), 61 deletions(-) diff --git a/scripts/pr_checker_agent.py b/scripts/pr_checker_agent.py index 1b115a5fe..74c0cc12d 100644 --- a/scripts/pr_checker_agent.py +++ b/scripts/pr_checker_agent.py @@ -47,30 +47,30 @@ # System prompt SYSTEM_PROMPT = """You are a PR review assistant for an open-source GitHub repository. -Given a newly opened PR, its author's contribution history, and the repository's CONTRIBUTING.md, -you must check the following - in this order: +Check the following in order, then post at most one comment combining all concerns. If nothing needs flagging, stay silent. -1. FIRST CONTRIBUTION: If this is the author's first contribution to the repo, welcome them warmly. - Acknowledge their effort and point them to any relevant getting-started resources in CONTRIBUTING.md. +Checks: +1. FIRST CONTRIBUTION: Welcome first-time contributors and link any getting-started resources from CONTRIBUTING.md. +2. DESCRIPTION: If missing or too vague to explain what changed and why, ask for clarification. +3. LINKED ISSUE: If no "Fixes/Closes/Resolves/Related to #N" link exists, ask the author to add one. +4. CONTRIBUTING.md: If the PR doesn't follow the required structure, quote the specific rule that is violated. -2. DESCRIPTION CLARITY: If the PR description is missing, too vague, or doesn't explain what - the change does and why, ask for a clearer description. +Rules: +- One comment maximum. Combine all concerns. +- Silence if everything is fine. +- Be constructive, not demanding. +- No emojis. -3. LINKED ISSUE: Check whether the description contains a linked issue using keywords like - "Fixes #N", "Closes #N", "Resolves #N", or "Related to #N". If no issue is linked, - ask the author to either link an existing issue or create a new one. +When posting a comment, always use this exact structure (omit sections that don't apply): -4. CONTRIBUTING.md COMPLIANCE: Check whether the PR description follows the structure or - requirements defined in CONTRIBUTING.md. If it doesn't comply, quote the relevant section - and point out specifically what needs to change. + (first-time contributors only) -Important rules: -- If multiple concerns apply, combine them into a single comment, never post more than one. -- If everything looks good, stay silent. Do not post a comment just to say things look fine. -- Be warm and constructive, never demanding. Remember this may be someone's first open-source contribution. -- When referencing CONTRIBUTING.md requirements, be specific: quote or paraphrase the rule, - don't just say "please read the contributing guide". -- Most importantly, be as succinct as possible.""" + + + + + +... (repeat for each rule that is violated)""" # GitHub helpers @@ -132,6 +132,7 @@ def run_pr_review_agent(): model=MODEL, messages=messages, tools=TOOLS, + temperature=0, ) message = response.choices[0].message diff --git a/scripts/security_review_agent.py b/scripts/security_review_agent.py index 6e1184d6c..51ff803bf 100644 --- a/scripts/security_review_agent.py +++ b/scripts/security_review_agent.py @@ -39,26 +39,30 @@ # System prompt SYSTEM_PROMPT = """You are a security analysis assistant for a GitHub repository. -You are given the diff of a pull request and must identify potential security issues. - -Focus only on security-relevant concerns such as: -- Hardcoded secrets, tokens, passwords or API keys -- Injection vulnerabilities (SQL, shell, template, etc.) -- Insecure use of cryptography or hashing -- Unsafe deserialization -- Path traversal or directory traversal risks -- Insecure direct object references -- Missing input validation or sanitisation on user-controlled data -- Use of known-vulnerable dependency versions (if visible in the diff) -- Overly permissive file or network access - -Do NOT comment on code style, performance, test coverage, or general best practices -unless they have a direct security implication. - -If you find no issues, say so clearly and briefly — do not invent concerns. -Format your response as a markdown comment suitable for posting directly on a GitHub PR. -Start with a short summary line, then list findings with file references where applicable. -If there are no findings, keep the response to 2-3 sentences maximum.""" +You are given a pull request diff and must identify potential security issues. + +Flag only: hardcoded secrets or credentials, injection vulnerabilities (SQL, shell, template), insecure cryptography or hashing, unsafe deserialization, path traversal, missing input validation on user-controlled data, known-vulnerable dependency versions, overly permissive file or network access. + +Do not comment on style, performance, test coverage, or best practices unless directly tied to a security risk. + +Always call post_security_review once when done, even if there are no findings. +No emojis. + +Use this exact format: + +### Summary + + +### Findings (omit section if none) + +**** + + + + + +... (repeat for each finding) +""" # GitHub helpers @@ -99,7 +103,7 @@ def find_previous_security_comment() -> object | None: for comment in pr.get_issue_comments(): if ( comment.user.login == "github-actions[bot]" - and "🔒 Automated Security Review" in comment.body + and "Automated Security Review" in comment.body ): return comment return None @@ -149,7 +153,7 @@ def post_or_update_comment(body: str): def handle_tool_call(name: str, inputs: dict) -> str: if name == "post_security_review": # Prepend a header to identify review comments across runs - body = f"## 🔒 Automated Security Review\n\n{inputs['body']}" + body = f"## Automated Security Review\n\n{inputs['body']}" post_or_update_comment(body) return "Security review comment posted." return f"Unknown tool: {name}" @@ -184,6 +188,7 @@ def run_security_review_agent(): model=MODEL, messages=messages, tools=TOOLS, + temperature=0, ) message = response.choices[0].message diff --git a/scripts/triage_agent.py b/scripts/triage_agent.py index f5fdbb18d..1f68182a9 100644 --- a/scripts/triage_agent.py +++ b/scripts/triage_agent.py @@ -116,21 +116,23 @@ # System prompt SYSTEM_PROMPT = """You are an issue triage assistant for a GitHub repository. -Given a new issue and a list of existing open issues, you must: - -1. Check whether the new issue is a duplicate of an existing one. - - If it clearly is the same issue, call mark_duplicate and stop — do not label or acknowledge further. - - If it seems related but could be distinct, call suggest_possible_duplicate. That comment - will serve as the acknowledgment too, so do NOT post a separate acknowledgment afterward. -2. Otherwise, classify it by applying appropriate labels - (bug, feature-request, question, documentation, needs-info, good-first-issue). -3. If the issue is missing key info (steps to reproduce for bugs, use case for features, etc.), - post a friendly comment asking for it. -4. If no possible duplicate was flagged, post a short acknowledgment comment so the - author knows their issue was received. Do NOT post comments on administrative issues - such as meeting minutes, roadmaps, etc. - -Keep comments concise and friendly.""" +Given a new issue and a list of existing open issues, follow these steps in order. +No emojis. + +1. DUPLICATE CHECK: If the issue clearly duplicates an existing one, call mark_duplicate and stop. + If it seems related but distinct, call suggest_possible_duplicate and continue triage. +2. LABEL: Apply appropriate labels (bug, enhancement, question, documentation, needs-info, good-first-issue, etc.). +3. NEEDS INFO: If the issue lacks key details (reproduction steps for bugs, use case for features), post a comment asking for them using this format: + +Thanks for opening this issue. To help us investigate, please provide: +- +... (repeat for each missing detail) + +4. ACKNOWLEDGE: If no duplicate was flagged and no needs-info comment was posted, acknowledge receipt with this format: + +Thanks for the report. We will take a look. + +Do not post acknowledgments on administrative issues such as meeting minutes or roadmaps.""" # GitHub helpers @@ -172,9 +174,9 @@ def post_comment(body: str) -> str: def mark_duplicate(original_issue_number: int, reason: str) -> str: original = repo.get_issue(original_issue_number) issue.create_comment( - f"Thanks for the report! This looks like a duplicate of #{original_issue_number} " + f"This looks like a duplicate of #{original_issue_number} " f"({original.html_url}).\n\n> {reason}\n\n" - f"Please edit this issue to add any distinguishing details if you believe it's not a duplicate." + f"If you believe it is distinct, please edit this issue with any additional details." ) issue.add_to_labels("duplicate") return f"Marked as duplicate of #{original_issue_number}." @@ -183,9 +185,9 @@ def mark_duplicate(original_issue_number: int, reason: str) -> str: def suggest_possible_duplicate(related_issue_number: int, reason: str) -> str: related = repo.get_issue(related_issue_number) issue.create_comment( - f"Hey! This might be related to #{related_issue_number} " - f"({related.html_url}) — {reason}\n\n" - f"Feel free to check if that one already covers what you're reporting!" + f"This may be related to #{related_issue_number} " + f"({related.html_url}): {reason}\n\n" + f"Please check if that issue already covers what you are reporting." ) return f"Flagged as possibly related to #{related_issue_number}." @@ -230,6 +232,7 @@ def run_triage_agent(): model=MODEL, messages=messages, tools=TOOLS, + temperature=0, ) message = response.choices[0].message From 54c73c599b9d711071a215378ce0b888048cf762 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Fri, 17 Apr 2026 13:43:35 +0900 Subject: [PATCH 05/47] refactor: extract configurable params to workflow file --- .github/workflows/pr-quality-check.yml | 10 +++++----- .github/workflows/security-review.yml | 9 ++++++--- .github/workflows/triage.yml | 10 ++++++---- scripts/security_review_agent.py | 24 ++++++++++-------------- scripts/triage_agent.py | 8 +++----- 5 files changed, 30 insertions(+), 31 deletions(-) diff --git a/.github/workflows/pr-quality-check.yml b/.github/workflows/pr-quality-check.yml index 8ccaaa6ea..94961cee5 100644 --- a/.github/workflows/pr-quality-check.yml +++ b/.github/workflows/pr-quality-check.yml @@ -20,14 +20,14 @@ jobs: MODEL: ${{ secrets.MODEL }} # Only API key for the chosen model is required ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} # Obtained automatically by GH Actions - AUTHOR_USERNAME: ${{ github.event.pull_request.user.login }} AUTHOR_ASSOCIATION: ${{ github.event.pull_request.author_association }} + AUTHOR_USERNAME: ${{ github.event.pull_request.user.login }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_BODY: ${{ github.event.pull_request.body }} PR_NUMBER: ${{ github.event.pull_request.number }} - REPO_NAME: ${{ github.repository }} PR_TITLE: ${{ github.event.pull_request.title }} - PR_BODY: ${{ github.event.pull_request.body }} + REPO_NAME: ${{ github.repository }} run: python scripts/pr_checker_agent.py diff --git a/.github/workflows/security-review.yml b/.github/workflows/security-review.yml index 38c5194c4..4b0a55d5e 100644 --- a/.github/workflows/security-review.yml +++ b/.github/workflows/security-review.yml @@ -28,15 +28,18 @@ jobs: - run: pip install litellm PyGithub - name: Run security review agent env: + IGNORED_EXTENSIONS: .lock,.sum + IGNORED_FILENAMES: package-lock.json,yarn.lock,poetry.lock,Gemfile.lock,Cargo.lock,composer.lock,pnpm-lock.yaml,pip.lock + MAX_PATCH_CHARS_PER_FILE: 3000 # e.g: "claude-sonnet-4-6", "gpt-4o", etc. MODEL: ${{ secrets.MODEL }} # Only API key for the chosen model is required ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} # Obtained automatically by GH Actions - REPO_NAME: ${{ github.repository }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} + REPO_NAME: ${{ github.repository }} TRIGGER: ${{ github.event_name }} run: python scripts/security_review_agent.py diff --git a/.github/workflows/triage.yml b/.github/workflows/triage.yml index b52bb72a0..7359d2fc5 100644 --- a/.github/workflows/triage.yml +++ b/.github/workflows/triage.yml @@ -16,16 +16,18 @@ jobs: - run: pip install litellm PyGithub - name: Run triage agent env: + AVAILABLE_LABELS: automation,bug,dependencies,documentation,enhancement,good-first-issue,meeting,needs-info,plugins,protocol,question,security,tech-debt,testing + LATEST_ISSUES_LIMIT: 100 # e.g: "claude-sonnet-4-6", "gpt-4o", etc. MODEL: ${{ secrets.MODEL }} # Only API key for the chosen model is required ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} # Obtained automatically by GH Actions + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ISSUE_BODY: ${{ github.event.issue.body }} ISSUE_NUMBER: ${{ github.event.issue.number }} - REPO_NAME: ${{ github.repository }} ISSUE_TITLE: ${{ github.event.issue.title }} - ISSUE_BODY: ${{ github.event.issue.body }} + REPO_NAME: ${{ github.repository }} run: python scripts/triage_agent.py diff --git a/scripts/security_review_agent.py b/scripts/security_review_agent.py index 51ff803bf..d421d6ee4 100644 --- a/scripts/security_review_agent.py +++ b/scripts/security_review_agent.py @@ -18,23 +18,19 @@ if not any(os.environ.get(api_key) for api_key in valid_api_keys): raise ValueError("No API key is set") +IGNORED_FILENAMES = set(os.environ.get( + "IGNORED_FILENAMES", + "package-lock.json,yarn.lock,poetry.lock,Gemfile.lock,Cargo.lock,composer.lock,pnpm-lock.yaml,pip.lock" +).split(",")) -# Exclude files that are not useful for security analysis -IGNORED_FILENAMES = { - "package-lock.json", - "yarn.lock", - "poetry.lock", - "Gemfile.lock", - "Cargo.lock", - "composer.lock", - "pnpm-lock.yaml", - "pip.lock", -} - -IGNORED_EXTENSIONS = {".lock", ".sum"} +# Extensions must include a leading dot +IGNORED_EXTENSIONS = set(os.environ.get( + "IGNORED_EXTENSIONS", + ".lock,.sum" +).split(",")) # Truncate very large diffs like generated files to prevent bloating the prompt -MAX_PATCH_CHARS_PER_FILE = 3000 +MAX_PATCH_CHARS_PER_FILE = int(os.environ.get("MAX_PATCH_CHARS_PER_FILE", 3000)) # System prompt diff --git a/scripts/triage_agent.py b/scripts/triage_agent.py index 1f68182a9..88fd911c2 100644 --- a/scripts/triage_agent.py +++ b/scripts/triage_agent.py @@ -9,7 +9,8 @@ repo = gh.get_repo(os.environ["REPO_NAME"]) issue = repo.get_issue(int(os.environ["ISSUE_NUMBER"])) -LATEST_ISSUES_LIMIT = 100 +LATEST_ISSUES_LIMIT = int(os.environ["LATEST_ISSUES_LIMIT"], 100) +AVAILABLE_LABELS = os.environ.get("AVAILABLE_LABELS", "bug,enhancement,question,documentation,needs-info") MODEL = os.environ["MODEL"] for env_var in ["GITHUB_TOKEN", "REPO_NAME", "ISSUE_NUMBER", "ISSUE_TITLE", "ISSUE_BODY", "MODEL"]: @@ -29,10 +30,7 @@ "name": "apply_label", "description": ( "Apply one or more labels to the issue. " - "Use labels like: automation, bug, dependencies, " - "documentation, enhancement, good-first-issue, " - "meeting, needs-info, plugins, protocol, question, " - "security, tech-debt, testing." + "Use labels like: " + AVAILABLE_LABELS ), "parameters": { "type": "object", From c652d04fb41da7b626b2ea07d0d5c4c8ef13673e Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Fri, 17 Apr 2026 14:04:28 +0900 Subject: [PATCH 06/47] refactor: extract helper functions, move scripts to scripts/agents --- .github/workflows/pr-quality-check.yml | 2 +- .github/workflows/security-review.yml | 2 +- .github/workflows/triage.yml | 2 +- scripts/agents/helpers.py | 33 +++++++++++++++ scripts/{ => agents}/pr_checker_agent.py | 42 ++----------------- scripts/{ => agents}/security_review_agent.py | 42 ++----------------- scripts/{ => agents}/triage_agent.py | 38 ++--------------- 7 files changed, 48 insertions(+), 113 deletions(-) create mode 100644 scripts/agents/helpers.py rename scripts/{ => agents}/pr_checker_agent.py (76%) rename scripts/{ => agents}/security_review_agent.py (81%) rename scripts/{ => agents}/triage_agent.py (86%) diff --git a/.github/workflows/pr-quality-check.yml b/.github/workflows/pr-quality-check.yml index 94961cee5..2deda4c0b 100644 --- a/.github/workflows/pr-quality-check.yml +++ b/.github/workflows/pr-quality-check.yml @@ -30,4 +30,4 @@ jobs: PR_NUMBER: ${{ github.event.pull_request.number }} PR_TITLE: ${{ github.event.pull_request.title }} REPO_NAME: ${{ github.repository }} - run: python scripts/pr_checker_agent.py + run: python scripts/agents/pr_checker_agent.py diff --git a/.github/workflows/security-review.yml b/.github/workflows/security-review.yml index 4b0a55d5e..47dfb548c 100644 --- a/.github/workflows/security-review.yml +++ b/.github/workflows/security-review.yml @@ -42,4 +42,4 @@ jobs: PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} REPO_NAME: ${{ github.repository }} TRIGGER: ${{ github.event_name }} - run: python scripts/security_review_agent.py + run: python scripts/agents/security_review_agent.py diff --git a/.github/workflows/triage.yml b/.github/workflows/triage.yml index 7359d2fc5..6188ba3ca 100644 --- a/.github/workflows/triage.yml +++ b/.github/workflows/triage.yml @@ -30,4 +30,4 @@ jobs: ISSUE_NUMBER: ${{ github.event.issue.number }} ISSUE_TITLE: ${{ github.event.issue.title }} REPO_NAME: ${{ github.repository }} - run: python scripts/triage_agent.py + run: python scripts/agents/triage_agent.py diff --git a/scripts/agents/helpers.py b/scripts/agents/helpers.py new file mode 100644 index 000000000..fdafb5198 --- /dev/null +++ b/scripts/agents/helpers.py @@ -0,0 +1,33 @@ +def validate_api_keys(): + valid_api_keys = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY"] + if not any(os.environ.get(k) for k in valid_api_keys): + raise ValueError("No API key is set") + + +def validate_env_vars(env_vars: list[str]): + for env_var in env_vars: + if not os.environ.get(env_var): + raise ValueError(f"{env_var} is not set") + + +def run_agent(messages: list, tools: list, handle_tool_call, model: str): + while True: + response = litellm.completion( + model=model, messages=messages, tools=tools, temperature=0 + ) + message = response.choices[0].message + if message.content: + print(f"[agent] {message.content}") + messages.append(message.model_dump(exclude_none=True)) + if response.choices[0].finish_reason == "stop" or not message.tool_calls: + break + tool_results = [] + for tool_call in message.tool_calls: + inputs = json.loads(tool_call.function.arguments) + result = handle_tool_call(tool_call.function.name, inputs) + tool_results.append({ + "role": "tool", + "tool_call_id": tool_call.id, + "content": result, + }) + messages.extend(tool_results) diff --git a/scripts/pr_checker_agent.py b/scripts/agents/pr_checker_agent.py similarity index 76% rename from scripts/pr_checker_agent.py rename to scripts/agents/pr_checker_agent.py index 74c0cc12d..a4a4257f0 100644 --- a/scripts/pr_checker_agent.py +++ b/scripts/agents/pr_checker_agent.py @@ -2,6 +2,7 @@ import json import litellm from github import Github, Auth +from helpers import validate_env_vars, validate_api_keys, run_agent # Setup @@ -11,14 +12,8 @@ author = os.environ["AUTHOR_USERNAME"] MODEL = os.environ["MODEL"] -for env_var in ["GITHUB_TOKEN", "REPO_NAME", "PR_NUMBER", "AUTHOR_USERNAME", "MODEL"]: - if not os.environ[env_var]: - raise ValueError(f"{env_var} is not set") - -valid_api_keys = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY"] -if not any(os.environ.get(api_key) for api_key in valid_api_keys): - raise ValueError("No API key is set") - +validate_env_vars(["GITHUB_TOKEN", "REPO_NAME", "PR_NUMBER", "AUTHOR_USERNAME", "MODEL"]) +validate_api_keys() # Tools @@ -126,36 +121,7 @@ def run_pr_review_agent(): {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": build_initial_message()}, ] - - while True: - response = litellm.completion( - model=MODEL, - messages=messages, - tools=TOOLS, - temperature=0, - ) - - message = response.choices[0].message - - if message.content: - print(f"[agent] {message.content}") - - messages.append(message.model_dump(exclude_none=True)) - - if response.choices[0].finish_reason == "stop" or not message.tool_calls: - break - - tool_results = [] - for tool_call in message.tool_calls: - inputs = json.loads(tool_call.function.arguments) - result = handle_tool_call(tool_call.function.name, inputs) - tool_results.append({ - "role": "tool", - "tool_call_id": tool_call.id, - "content": result, - }) - - messages.extend(tool_results) + run_agent(messages, TOOLS, handle_tool_call, MODEL) if __name__ == "__main__": diff --git a/scripts/security_review_agent.py b/scripts/agents/security_review_agent.py similarity index 81% rename from scripts/security_review_agent.py rename to scripts/agents/security_review_agent.py index d421d6ee4..43251bb7b 100644 --- a/scripts/security_review_agent.py +++ b/scripts/agents/security_review_agent.py @@ -2,6 +2,7 @@ import json import litellm from github import Github, Auth +from helpers import validate_env_vars, validate_api_keys, run_agent # Setup @@ -10,13 +11,8 @@ pr = repo.get_pull(int(os.environ["PR_NUMBER"])) MODEL = os.environ["MODEL"] -for env_var in ["GITHUB_TOKEN", "REPO_NAME", "PR_NUMBER", "MODEL"]: - if not os.environ[env_var]: - raise ValueError(f"{env_var} is not set") - -valid_api_keys = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY"] -if not any(os.environ.get(api_key) for api_key in valid_api_keys): - raise ValueError("No API key is set") +validate_env_vars(["GITHUB_TOKEN", "REPO_NAME", "PR_NUMBER", "MODEL"]) +validate_api_keys() IGNORED_FILENAMES = set(os.environ.get( "IGNORED_FILENAMES", @@ -178,37 +174,7 @@ def run_security_review_agent(): {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": build_initial_message()}, ] - - while True: - response = litellm.completion( - model=MODEL, - messages=messages, - tools=TOOLS, - temperature=0, - ) - - message = response.choices[0].message - - if message.content: - print(f"[agent] {message.content}") - - messages.append(message.model_dump(exclude_none=True)) - - if response.choices[0].finish_reason == "stop" or not message.tool_calls: - break - - tool_results = [] - for tool_call in message.tool_calls: - inputs = json.loads(tool_call.function.arguments) - result = handle_tool_call(tool_call.function.name, inputs) - print(f"[tool] {tool_call.function.name}: {result}") - tool_results.append({ - "role": "tool", - "tool_call_id": tool_call.id, - "content": result, - }) - - messages.extend(tool_results) + run_agent(messages, TOOLS, handle_tool_call, MODEL) if __name__ == "__main__": diff --git a/scripts/triage_agent.py b/scripts/agents/triage_agent.py similarity index 86% rename from scripts/triage_agent.py rename to scripts/agents/triage_agent.py index 88fd911c2..aa2002e13 100644 --- a/scripts/triage_agent.py +++ b/scripts/agents/triage_agent.py @@ -2,6 +2,7 @@ import json import litellm from github import Github, Auth +from helpers import validate_env_vars, validate_api_keys, run_agent # Setup @@ -13,13 +14,8 @@ AVAILABLE_LABELS = os.environ.get("AVAILABLE_LABELS", "bug,enhancement,question,documentation,needs-info") MODEL = os.environ["MODEL"] -for env_var in ["GITHUB_TOKEN", "REPO_NAME", "ISSUE_NUMBER", "ISSUE_TITLE", "ISSUE_BODY", "MODEL"]: - if not os.environ[env_var]: - raise ValueError(f"{env_var} is not set") - -valid_api_keys = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY"] -if not any(os.environ.get(api_key) for api_key in valid_api_keys): - raise ValueError("No API key is set") +validate_env_vars(["GITHUB_TOKEN", "REPO_NAME", "ISSUE_NUMBER", "ISSUE_TITLE", "ISSUE_BODY", "MODEL"]) +validate_api_keys() # Tools @@ -224,33 +220,7 @@ def run_triage_agent(): {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": build_initial_message()}, ] - - while True: - response = litellm.completion( - model=MODEL, - messages=messages, - tools=TOOLS, - temperature=0, - ) - - message = response.choices[0].message - messages.append(message.model_dump(exclude_none=True)) - - finish_reason = response.choices[0].finish_reason - if finish_reason == "stop" or not message.tool_calls: - break - - tool_results = [] - for tool_call in message.tool_calls: - inputs = json.loads(tool_call.function.arguments) - result = handle_tool_call(tool_call.function.name, inputs) - tool_results.append({ - "role": "tool", - "tool_call_id": tool_call.id, - "content": result, - }) - - messages.extend(tool_results) + run_agent(messages, TOOLS, handle_tool_call, MODEL) if __name__ == "__main__": From b7da4016577f5d31eaa7b46600a53f0b0224c53d Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Fri, 17 Apr 2026 14:21:35 +0900 Subject: [PATCH 07/47] fix: python imports and remove unused --- scripts/agents/helpers.py | 4 ++++ scripts/agents/pr_checker_agent.py | 2 -- scripts/agents/security_review_agent.py | 2 -- scripts/agents/triage_agent.py | 2 -- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/scripts/agents/helpers.py b/scripts/agents/helpers.py index fdafb5198..0cda26e17 100644 --- a/scripts/agents/helpers.py +++ b/scripts/agents/helpers.py @@ -1,3 +1,7 @@ +import os +import json +import litellm + def validate_api_keys(): valid_api_keys = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY"] if not any(os.environ.get(k) for k in valid_api_keys): diff --git a/scripts/agents/pr_checker_agent.py b/scripts/agents/pr_checker_agent.py index a4a4257f0..52bbced9d 100644 --- a/scripts/agents/pr_checker_agent.py +++ b/scripts/agents/pr_checker_agent.py @@ -1,6 +1,4 @@ import os -import json -import litellm from github import Github, Auth from helpers import validate_env_vars, validate_api_keys, run_agent diff --git a/scripts/agents/security_review_agent.py b/scripts/agents/security_review_agent.py index 43251bb7b..d60660715 100644 --- a/scripts/agents/security_review_agent.py +++ b/scripts/agents/security_review_agent.py @@ -1,6 +1,4 @@ import os -import json -import litellm from github import Github, Auth from helpers import validate_env_vars, validate_api_keys, run_agent diff --git a/scripts/agents/triage_agent.py b/scripts/agents/triage_agent.py index aa2002e13..366a4dbb6 100644 --- a/scripts/agents/triage_agent.py +++ b/scripts/agents/triage_agent.py @@ -1,6 +1,4 @@ import os -import json -import litellm from github import Github, Auth from helpers import validate_env_vars, validate_api_keys, run_agent From b544668e396847fe31f4ebdfbe18178f7217be52 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Fri, 17 Apr 2026 14:34:48 +0900 Subject: [PATCH 08/47] chore: add greeting and disclaimer to output --- scripts/agents/pr_checker_agent.py | 2 +- scripts/agents/security_review_agent.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/agents/pr_checker_agent.py b/scripts/agents/pr_checker_agent.py index 52bbced9d..ac2742e75 100644 --- a/scripts/agents/pr_checker_agent.py +++ b/scripts/agents/pr_checker_agent.py @@ -56,7 +56,7 @@ When posting a comment, always use this exact structure (omit sections that don't apply): - (first-time contributors only) +Thanks for the contribution! diff --git a/scripts/agents/security_review_agent.py b/scripts/agents/security_review_agent.py index d60660715..9a6c73723 100644 --- a/scripts/agents/security_review_agent.py +++ b/scripts/agents/security_review_agent.py @@ -52,6 +52,8 @@ ... (repeat for each finding) + +Disclaimer: This review is AI-generated. Please validate the findings before fixing. """ # GitHub helpers From 1d4e706744808001177688f0375e33333f09d5fe Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Sun, 3 May 2026 10:48:06 +0900 Subject: [PATCH 09/47] ci: handle NPM releases for multiple branches --- .github/workflows/npm.yml | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/.github/workflows/npm.yml b/.github/workflows/npm.yml index e71bc334b..7ffed0fd4 100644 --- a/.github/workflows/npm.yml +++ b/.github/workflows/npm.yml @@ -16,7 +16,6 @@ jobs: egress-policy: audit - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - # Setup .npmrc file to publish to npm - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 with: node-version: '24' @@ -24,13 +23,37 @@ jobs: - run: npm ci - run: npm run build - - name: Check if pre-release and publish to NPM + - name: Determine dist-tag and publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} run: | + set -euo pipefail VERSION=$(node -p "require('./package.json').version") + PKG_NAME=$(node -p "require('./package.json').name") + echo "Publishing $PKG_NAME@$VERSION" + + # Pre-releases (ex: 2.1.0-rc.1) get tagged as 'rc' if [[ "$VERSION" == *"-"* ]]; then - echo "Publishing pre-release: $VERSION" - npm publish --access=public --tag rc + echo "Pre-release detected, publishing under 'rc' tag" + npm publish --access=public --provenance --tag rc + exit 0 + fi + + # Look up current 'latest' on NPM. If never published before, + # defaults to 0.0.0 so any release (0.1.0, 1.0.0, etc.) becomes latest + CURRENT_LATEST=$(npm view "$PKG_NAME" version 2>/dev/null || echo "0.0.0") + echo "Current 'latest' on npm: $CURRENT_LATEST" + + # If this version is strictly greater than the current 'latest', + # it becomes latest + if npx --yes semver "$VERSION" -r ">$CURRENT_LATEST" >/dev/null 2>&1; then + echo "Publishing as 'latest'" + npm publish --access=public --provenance else - echo "Publishing stable release: $VERSION" - npm publish --access=public + # Otherwise, this is a maintenance release on an older line + # Tag as v. so users can pin to it + MAJOR_MINOR=$(echo "$VERSION" | cut -d. -f1,2) + DIST_TAG="v${MAJOR_MINOR}" + echo "Maintenance release detected, publishing under '$DIST_TAG' tag" + npm publish --access=public --provenance --tag "$DIST_TAG" fi From 94f5f5dc46bcb336296a91c0f0cb530989bea886 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Sun, 3 May 2026 11:03:05 +0900 Subject: [PATCH 10/47] ci: fix original release-drafter --- .github/release-drafter.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index a4c29ffc0..316b60361 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -1,4 +1,3 @@ ---- name-template: 'Version $RESOLVED_VERSION' tag-template: 'v$RESOLVED_VERSION' change-template: '- $TITLE @$AUTHOR (#$NUMBER)' @@ -9,7 +8,7 @@ template: | --- - *Full Changelog**: https://github.com/finos/git-proxy/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION + **Full Changelog**: https://github.com/finos/git-proxy/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION categories: - title: '🚀 Features' @@ -44,10 +43,7 @@ version-resolver: autolabeler: - label: 'automation' title: - - '/^(ci|perf|refactor|test).*/i' - - label: 'enhancement' - title: - - '/^(style).*/i' + - '/^(ci|perf|refactor|test|style).*/i' - label: 'documentation' title: - '/^(docs).*/i' From f4c80c55627dff447a1e3f658c9017a010564bd0 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Sun, 3 May 2026 11:03:51 +0900 Subject: [PATCH 11/47] ci: add release-branch-drafter.yml --- .github/release-branch-drafter.yml | 33 ++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 .github/release-branch-drafter.yml diff --git a/.github/release-branch-drafter.yml b/.github/release-branch-drafter.yml new file mode 100644 index 000000000..6755bcc3e --- /dev/null +++ b/.github/release-branch-drafter.yml @@ -0,0 +1,33 @@ +name: Release Drafter (Release Branches) + +on: + push: + branches: + - 'release/**' + pull_request: + # needed so autolabeler runs on PRs from forks before merge, + # for when PR is finally merged into a release branch. + types: [opened, reopened, synchronize] + +permissions: + contents: read + +jobs: + update_release_draft: + permissions: + contents: write # needed to create/update the draft release + pull-requests: write # needed for the autolabeler + runs-on: ubuntu-latest + steps: + - name: Harden Runner + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + with: + egress-policy: audit + + - uses: release-drafter/release-drafter@v6 + with: + # Target the branch that triggered this run + # Ex: "release/2.1" becomes the commitish for the draft + commitish: ${{ github.ref_name }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 1120e1d12cf1c9751a6844b463bbe87b28fa3f88 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Sun, 3 May 2026 11:34:29 +0900 Subject: [PATCH 12/47] ci: modify workflows for testing, edit package.json for publish --- .github/workflows/npm.yml | 25 ++++++++++++++++++++++--- package.json | 6 +++--- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/.github/workflows/npm.yml b/.github/workflows/npm.yml index 7ffed0fd4..76e913b9c 100644 --- a/.github/workflows/npm.yml +++ b/.github/workflows/npm.yml @@ -2,6 +2,17 @@ name: Publish to NPM on: release: types: [published] + workflow_dispatch: + inputs: + dry_run: + description: 'Dry run only (no actual publish)' + required: false + default: 'true' + type: choice + options: + - 'true' + - 'false' + permissions: contents: read id-token: write @@ -26,16 +37,24 @@ jobs: - name: Determine dist-tag and publish env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run || 'false' }} run: | set -euo pipefail VERSION=$(node -p "require('./package.json').version") PKG_NAME=$(node -p "require('./package.json').name") echo "Publishing $PKG_NAME@$VERSION" + # Build the publish command flags. + PUBLISH_FLAGS=(--access=public) + if [[ "$DRY_RUN" == "true" ]]; then + PUBLISH_FLAGS+=(--dry-run) + echo "DRY RUN — nothing will actually be published." + fi + # Pre-releases (ex: 2.1.0-rc.1) get tagged as 'rc' if [[ "$VERSION" == *"-"* ]]; then echo "Pre-release detected, publishing under 'rc' tag" - npm publish --access=public --provenance --tag rc + npm publish "${PUBLISH_FLAGS[@]}" --tag rc exit 0 fi @@ -48,12 +67,12 @@ jobs: # it becomes latest if npx --yes semver "$VERSION" -r ">$CURRENT_LATEST" >/dev/null 2>&1; then echo "Publishing as 'latest'" - npm publish --access=public --provenance + npm publish "${PUBLISH_FLAGS[@]}" else # Otherwise, this is a maintenance release on an older line # Tag as v. so users can pin to it MAJOR_MINOR=$(echo "$VERSION" | cut -d. -f1,2) DIST_TAG="v${MAJOR_MINOR}" echo "Maintenance release detected, publishing under '$DIST_TAG' tag" - npm publish --access=public --provenance --tag "$DIST_TAG" + npm publish "${PUBLISH_FLAGS[@]}" --tag "$DIST_TAG" fi diff --git a/package.json b/package.json index 4b5e6232f..1a7ec6137 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "@finos/git-proxy", - "version": "2.0.0-rc.6", - "description": "Deploy custom push protections and policies on top of Git.", + "name": "@jescalada/git-proxy-deployment-testing", + "version": "1.0.0", + "description": "Testing deployment workflows for @finos/git-proxy.", "main": "dist/index.js", "types": "dist/index.d.ts", "exports": { From 600dc91f828c1fdd8cb3d63978afcb67e72f825d Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Sun, 3 May 2026 11:39:15 +0900 Subject: [PATCH 13/47] fix: update package.json versions --- experimental/li-cli/package.json | 2 +- experimental/license-inventory/package.json | 2 +- package-lock.json | 109 +++++++++++++++--- packages/git-proxy-cli/package.json | 4 +- plugins/git-proxy-plugin-samples/package.json | 2 +- 5 files changed, 100 insertions(+), 19 deletions(-) diff --git a/experimental/li-cli/package.json b/experimental/li-cli/package.json index 82f12c34f..eac643fc5 100644 --- a/experimental/li-cli/package.json +++ b/experimental/li-cli/package.json @@ -1,5 +1,5 @@ { - "name": "@finos/git-proxy-li-cli", + "name": "@jescalada/git-proxy-li-cli", "version": "0.0.1", "author": "git-proxy contributors", "license": "Apache-2.0", diff --git a/experimental/license-inventory/package.json b/experimental/license-inventory/package.json index 6d8a04f97..d580f4e1a 100644 --- a/experimental/license-inventory/package.json +++ b/experimental/license-inventory/package.json @@ -1,5 +1,5 @@ { - "name": "@finos/git-proxy-license-inventory", + "name": "@jescalada/git-proxy-license-inventory", "version": "0.0.2", "author": "git-proxy contributors", "license": "Apache-2.0", diff --git a/package-lock.json b/package-lock.json index e35608362..52074a03e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { - "name": "@finos/git-proxy", - "version": "2.0.0-rc.6", + "name": "@jescalada/git-proxy-deployment-testing", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@finos/git-proxy", - "version": "2.0.0-rc.6", + "name": "@jescalada/git-proxy-deployment-testing", + "version": "1.0.0", "license": "Apache-2.0", "workspaces": [ "./packages/git-proxy-cli" @@ -1036,6 +1036,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -2522,14 +2523,6 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@finos/git-proxy": { - "resolved": "", - "link": true - }, - "node_modules/@finos/git-proxy-cli": { - "resolved": "packages/git-proxy-cli", - "link": true - }, "node_modules/@fontsource/roboto": { "version": "5.2.9", "resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.2.9.tgz", @@ -2733,6 +2726,10 @@ "node": ">=8" } }, + "node_modules/@jescalada/git-proxy-cli": { + "resolved": "packages/git-proxy-cli", + "link": true + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.12", "dev": true, @@ -4402,6 +4399,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.8.tgz", "integrity": "sha512-ebO/Yl+EAvVe8DnMfi+iaAyIqYdK0q/q0y0rw82INWEKJOBe6b/P3YWE8NW7oOlF/nXFNrHwhARrN/hdgDkraA==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -4456,6 +4454,7 @@ "node_modules/@types/react": { "version": "17.0.74", "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -4641,6 +4640,7 @@ "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/types": "8.56.0", @@ -5218,6 +5218,7 @@ "version": "8.15.0", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5842,6 +5843,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", @@ -7166,6 +7168,7 @@ "version": "2.4.1", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" @@ -7453,6 +7456,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -7878,6 +7882,7 @@ "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.19.0.tgz", "integrity": "sha512-0csaMkGq+vaiZTmSMMGkfdCOabYv192VbytFypcvI0MANrp+4i/7yEkJ0sbAEhycQjntaKGzYfjfXQyVb7BHMA==", "license": "MIT", + "peer": true, "dependencies": { "cookie": "~0.7.2", "cookie-signature": "~1.0.7", @@ -10981,6 +10986,7 @@ "node_modules/mongodb": { "version": "5.9.2", "license": "Apache-2.0", + "peer": true, "dependencies": { "bson": "^5.5.0", "mongodb-connection-string-url": "^2.6.0", @@ -12300,6 +12306,7 @@ "node_modules/react": { "version": "16.14.0", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -12312,6 +12319,7 @@ "node_modules/react-dom": { "version": "16.14.0", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -13696,6 +13704,7 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -13917,6 +13926,7 @@ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" @@ -14078,6 +14088,7 @@ "version": "5.9.3", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -14359,6 +14370,7 @@ "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -14493,6 +14505,7 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -14506,6 +14519,7 @@ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -15051,8 +15065,8 @@ } }, "packages/git-proxy-cli": { - "name": "@finos/git-proxy-cli", - "version": "2.0.0-rc.6", + "name": "@jescalada/git-proxy-cli", + "version": "1.0.0", "license": "Apache-2.0", "dependencies": { "@finos/git-proxy": "2.0.0-rc.6", @@ -15062,6 +15076,73 @@ "bin": { "git-proxy-cli": "dist/index.js" } + }, + "packages/git-proxy-cli/node_modules/@finos/git-proxy": { + "version": "2.0.0-rc.6", + "resolved": "https://registry.npmjs.org/@finos/git-proxy/-/git-proxy-2.0.0-rc.6.tgz", + "integrity": "sha512-Crb+uQHb/U6wmJzuuhvEWO2DJmVW/1VKooAFmWox69XooqpxD1WqerxL0edU1k3tGws9h07IXmrd0KwoJw/CxA==", + "license": "Apache-2.0", + "workspaces": [ + "./packages/git-proxy-cli" + ], + "dependencies": { + "@aws-sdk/credential-providers": "^3.980.0", + "@fontsource/roboto": "^5.2.9", + "@material-ui/core": "^4.12.4", + "@material-ui/icons": "4.11.3", + "@primer/octicons-react": "^19.21.2", + "@seald-io/nedb": "^4.1.2", + "axios": "^1.13.4", + "bcryptjs": "^3.0.3", + "clsx": "^2.1.1", + "concurrently": "^9.2.1", + "connect-mongo": "^5.1.0", + "cors": "^2.8.6", + "diff2html": "^3.4.56", + "env-paths": "^3.0.0", + "escape-string-regexp": "^5.0.0", + "express": "^5.2.1", + "express-http-proxy": "^2.1.2", + "express-rate-limit": "^8.2.1", + "express-session": "^1.19.0", + "font-awesome": "^4.7.0", + "history": "5.3.0", + "isomorphic-git": "^1.36.3", + "jsonwebtoken": "^9.0.3", + "load-plugin": "^6.0.3", + "lodash": "^4.17.23", + "lusca": "^1.7.0", + "material-design-icons": "^3.0.1", + "moment": "^2.30.1", + "mongodb": "^5.9.2", + "openid-client": "^6.8.1", + "parse-diff": "^0.11.1", + "passport": "^0.7.0", + "passport-activedirectory": "^1.4.0", + "passport-local": "^1.0.0", + "perfect-scrollbar": "^1.5.6", + "react": "^16.14.0", + "react-dom": "^16.14.0", + "react-html-parser": "^2.0.2", + "react-router-dom": "6.30.3", + "simple-git": "^3.30.0", + "uuid": "^13.0.0", + "validator": "^13.15.26", + "yargs": "^17.7.2" + }, + "bin": { + "git-proxy": "dist/index.js", + "git-proxy-all": "concurrently 'npm run server' 'npm run client'" + }, + "engines": { + "node": ">=22.13.1 || >=24.0.0" + }, + "optionalDependencies": { + "@esbuild/darwin-arm64": "^0.27.2", + "@esbuild/darwin-x64": "^0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } } } } diff --git a/packages/git-proxy-cli/package.json b/packages/git-proxy-cli/package.json index 75221b134..47f7dc92e 100644 --- a/packages/git-proxy-cli/package.json +++ b/packages/git-proxy-cli/package.json @@ -1,6 +1,6 @@ { - "name": "@finos/git-proxy-cli", - "version": "2.0.0-rc.6", + "name": "@jescalada/git-proxy-cli", + "version": "1.0.0", "description": "Command line interface tool for FINOS GitProxy.", "bin": { "git-proxy-cli": "./dist/index.js" diff --git a/plugins/git-proxy-plugin-samples/package.json b/plugins/git-proxy-plugin-samples/package.json index e571da7d9..fd1ff13e1 100644 --- a/plugins/git-proxy-plugin-samples/package.json +++ b/plugins/git-proxy-plugin-samples/package.json @@ -1,5 +1,5 @@ { - "name": "@finos/git-proxy-plugin-samples", + "name": "@jescalada/git-proxy-plugin-samples", "version": "0.1.2", "description": "A set of sample (dummy) plugins for GitProxy to demonstrate how plugins are authored.", "scripts": { From ba674eea40b10b544d6097fd4d75fefa9e68405c Mon Sep 17 00:00:00 2001 From: Juan Escalada <97265671+jescalada@users.noreply.github.com> Date: Sun, 3 May 2026 11:59:02 +0900 Subject: [PATCH 14/47] feat: test GitProxy v1.1.0 auto-draft and NPM publish --- README.md | 124 +----------------------------------------------------- 1 file changed, 2 insertions(+), 122 deletions(-) diff --git a/README.md b/README.md index dfd10b6c2..fe84b343b 100644 --- a/README.md +++ b/README.md @@ -1,123 +1,3 @@ -
-
- - Logo - - -
-
- -

- Deploy custom push protections and policies
on top of Git -
-
-
- Docs - · - Demo - · - Report a bug - · - Suggest a new feature -

+# GitProxy v1.1.0 -
- -[![FINOS - Graduated](https://cdn.jsdelivr.net/gh/finos/contrib-toolbox@master/images/badge-graduated.svg)](https://community.finos.org/docs/governance/lifecycle-stages/graduated) -[![NPM](https://img.shields.io/npm/v/@finos/git-proxy?colorA=00C586&colorB=000000)](https://www.npmjs.com/package/@finos/git-proxy) -[![Build](https://img.shields.io/github/actions/workflow/status/finos/git-proxy/ci.yml?branch=main&label=CI&logo=github&colorA=00C586&colorB=000000)](https://github.com/finos/git-proxy/actions/workflows/ci.yml) -[![codecov](https://codecov.io/gh/finos/git-proxy/branch/main/graph/badge.svg)](https://codecov.io/gh/finos/git-proxy) -[![Documentation](https://img.shields.io/badge/_-documentation-000000?colorA=00C586&logo=docusaurus&logoColor=FFFFFF&)](https://git-proxy.finos.org) -
-[![License](https://img.shields.io/github/license/finos/git-proxy?colorA=00C586&colorB=000000)](https://github.com/finos/git-proxy/blob/main/LICENSE) -[![Contributors](https://img.shields.io/github/contributors/finos/git-proxy?colorA=00C586&colorB=000000)](https://github.com/finos/git-proxy/graphs/contributors) -[![Slack](https://img.shields.io/badge/_-Chat_on_Slack-000000.svg?logo=slack&colorA=00C586)](https://app.slack.com/client/T01E7QRQH97/C06LXNW0W76) -[![git-proxy](https://api.securityscorecards.dev/projects/github.com/finos/git-proxy/badge)](https://api.securityscorecards.dev/projects/github.com/finos/git-proxy) -[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/10520/badge)](https://www.bestpractices.dev/projects/10520) - -
-
- -## What is GitProxy - -GitProxy is an application that stands between developers and a Git remote endpoint (e.g., `github.com`). It applies rules and workflows (configurable as `plugins`) to all outgoing `git push` operations to ensure they are compliant. - -The main goal of GitProxy is to marry the defacto standard Open Source developer experience (git-based workflow of branching out, submitting changes and merging back) with security and legal requirements that firms have to comply with, when operating in highly regulated industries like financial services. - -That said, GitProxy can also be used on a local environment to enforce a single developer's best practices, which tends to be the easiest setup to start with and the most comfortable one to build new GitProxy plugins. - -```mermaid -sequenceDiagram - actor Developer - Developer->>+Git Server: git clone - Developer->>Workstation: git remote add proxy - Developer->>+GitProxy: git push proxy - GitProxy-->>-Developer: Failed license check - Developer->>Workstation: git commit -m 'fix license issue' - Developer->>+GitProxy: git push - GitProxy-->>-Git Server: Approved -``` - -## Getting Started 🚀 - -Install & run git-proxy (requires [Nodejs](https://nodejs.org/en/download/)): - -```bash -$ npx -- @finos/git-proxy -``` - -Clone a repository, set the remote to the GitProxy URL and push your changes: - -```bash -# Only HTTPS cloning is supported at the moment, see https://github.com/finos/git-proxy/issues/27. -$ git clone https://github.com/octocat/Hello-World.git && cd Hello-World -# The below command is using the GitHub official CLI to fork the repo that is cloned. -# You can also fork on the GitHub UI. For usage details on the CLI, see https://github.com/cli/cli -$ gh repo fork -✓ Created fork yourGithubUser/Hello-World -... -$ git remote add proxy http://localhost:8000/yourGithubUser/Hello-World.git -# This fetches the repository's default branch and pushes it (https://stackoverflow.com/a/44750379). -$ git push proxy $(git symbolic-ref refs/remotes/origin/HEAD | sed 's@^refs/remotes/origin/@@') -``` - -Using the default configuration, GitProxy intercepts the push and _blocks_ it. To enable code pushing to your fork via GitProxy, add your repository URL into the GitProxy config file (`proxy.config.json`). For more information, refer to [our documentation](https://git-proxy.finos.org). - -## Documentation - -For detailed step-by-step instructions for how to install, deploy & configure GitProxy and -customize for your environment, see the [project's documentation](https://git-proxy.finos.org/docs/): - -- [Quickstart](https://git-proxy.finos.org/docs/category/quickstart/) -- [Installation](https://git-proxy.finos.org/docs/quickstart/installation) -- [Configuration](https://git-proxy.finos.org/docs/category/configuration) -- [Contributing](https://git-proxy.finos.org/docs/development/contributing) -- [Testing](https://git-proxy.finos.org/docs/development/testing) - -## Contributing - -Your contributions are at the core of making this a truly open source project. Any contributions you make are **greatly appreciated**. See [`CONTRIBUTING.md`](CONTRIBUTING.md) for more information. - -## Security - -If you identify a security vulnerability in the codebase, please follow the steps in [`SECURITY.md`](https://github.com/finos/git-proxy/security/policy). This includes logic-based vulnerabilities and sensitive information or secrets found in code. - -## Code of Conduct - -We are committed to making open source an enjoyable and respectful experience for our community. See [`CODE_OF_CONDUCT`](CODE_OF_CONDUCT.md) for more information. - -## License - -This project is distributed under the Apache-2.0 license. See [`LICENSE`](LICENSE) for more information. - -## Contact - -Drop a note, ask a question or just say hello in our community Slack channel, which is accessible via the [FINOS Slack Workspace](https://finos-lf.slack.com) 👋 - -If you can't access Slack, you can also [subscribe to our mailing list](mailto:git-proxy+subscribe@lists.finos.org) 📨 - -Otherwise, if you have a deeper query or require more support, please [raise an issue](https://github.com/finos/git-proxy/issues) 🧵 - -🤝 Join our [fortnightly Zoom meeting](https://zoom-lfx.platform.linuxfoundation.org/meeting/95849833904?password=99413314-d03a-4b1c-b682-1ede2c399595) on Monday, 4PM BST (odd week numbers). -🌍 [Convert to your local time](https://www.timeanddate.com/worldclock) -📅 [Click here](https://calendar.google.com/calendar/event?action=TEMPLATE&tmeid=MTRvbzM0NG01dWNvNGc4OGJjNWphM2ZtaTZfMjAyNTA2MDJUMTUwMDAwWiBzYW0uaG9sbWVzQGNvbnRyb2wtcGxhbmUuaW8&tmsrc=sam.holmes%40control-plane.io&scp=ALL) for the recurring Google Calendar meeting invite. Alternatively, send an e-mail to [help@finos.org](https://zoom-lfx.platform.linuxfoundation.org/meeting/95849833904?password=99413314-d03a-4b1c-b682-1ede2c399595#:~:text=Need-,an,-invite%3F) to get a calendar invitation. +Testing GitLabFlow branching strategy. This should automatically create a draft setting the project to `v1.1.0`. From 423826b2c97e034d75d2b8c0afe52556d781e6f3 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Sun, 3 May 2026 12:08:31 +0900 Subject: [PATCH 15/47] chore: bump @jescalada/git-proxy-development-testing to 1.1.0 --- package.json | 2 +- packages/git-proxy-cli/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 1a7ec6137..fda5d44d6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@jescalada/git-proxy-deployment-testing", - "version": "1.0.0", + "version": "1.1.0", "description": "Testing deployment workflows for @finos/git-proxy.", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/git-proxy-cli/package.json b/packages/git-proxy-cli/package.json index 47f7dc92e..8cb4d36f9 100644 --- a/packages/git-proxy-cli/package.json +++ b/packages/git-proxy-cli/package.json @@ -1,6 +1,6 @@ { "name": "@jescalada/git-proxy-cli", - "version": "1.0.0", + "version": "1.1.0", "description": "Command line interface tool for FINOS GitProxy.", "bin": { "git-proxy-cli": "./dist/index.js" From 129b618b09601549a55028dee222ed43f6130945 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Sun, 3 May 2026 12:13:01 +0900 Subject: [PATCH 16/47] chore: bump @jescalada/git-proxy-deployment-testing to 1.1.0 --- package-lock.json | 6 +++--- package.json | 2 +- packages/git-proxy-cli/package.json | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 52074a03e..ccaf37b04 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@jescalada/git-proxy-deployment-testing", - "version": "1.0.0", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@jescalada/git-proxy-deployment-testing", - "version": "1.0.0", + "version": "1.1.0", "license": "Apache-2.0", "workspaces": [ "./packages/git-proxy-cli" @@ -15066,7 +15066,7 @@ }, "packages/git-proxy-cli": { "name": "@jescalada/git-proxy-cli", - "version": "1.0.0", + "version": "1.1.0", "license": "Apache-2.0", "dependencies": { "@finos/git-proxy": "2.0.0-rc.6", diff --git a/package.json b/package.json index 1a7ec6137..fda5d44d6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@jescalada/git-proxy-deployment-testing", - "version": "1.0.0", + "version": "1.1.0", "description": "Testing deployment workflows for @finos/git-proxy.", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/git-proxy-cli/package.json b/packages/git-proxy-cli/package.json index 47f7dc92e..8cb4d36f9 100644 --- a/packages/git-proxy-cli/package.json +++ b/packages/git-proxy-cli/package.json @@ -1,6 +1,6 @@ { "name": "@jescalada/git-proxy-cli", - "version": "1.0.0", + "version": "1.1.0", "description": "Command line interface tool for FINOS GitProxy.", "bin": { "git-proxy-cli": "./dist/index.js" From 34efe8c9649ec24487d63b71fd4a67b5cdb05b41 Mon Sep 17 00:00:00 2001 From: Juan Escalada <97265671+jescalada@users.noreply.github.com> Date: Sun, 3 May 2026 12:40:31 +0900 Subject: [PATCH 17/47] chore: add YAML front matter to release drafter config --- .github/release-drafter.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 316b60361..646cab2b7 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -1,3 +1,4 @@ +--- name-template: 'Version $RESOLVED_VERSION' tag-template: 'v$RESOLVED_VERSION' change-template: '- $TITLE @$AUTHOR (#$NUMBER)' From 861270d97675cf1aafdd9f98a678580497e88590 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Sun, 3 May 2026 13:08:38 +0900 Subject: [PATCH 18/47] ci: set pr-lint.yml to only set labels --- .github/workflows/pr-lint.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-lint.yml b/.github/workflows/pr-lint.yml index 6301eb168..444e11d01 100644 --- a/.github/workflows/pr-lint.yml +++ b/.github/workflows/pr-lint.yml @@ -16,7 +16,7 @@ permissions: jobs: pr_title: permissions: - contents: write + contents: read pull-requests: write statuses: write name: Validate & Label PR @@ -45,8 +45,11 @@ jobs: revert test break + + # Run release-drafter in label-only mode, drafts are made in release-drafter.yml - uses: release-drafter/release-drafter@139054aeaa9adc52ab36ddf67437541f039b88e2 # v7 with: - commitish: main + config-name: release-drafter.yml + disable-releaser: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 7986cfa1ad42e2c71fc8e23958d61a6900a79cb5 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Sun, 3 May 2026 13:20:16 +0900 Subject: [PATCH 19/47] ci: move release branch drafter --- .github/release-drafter.yml | 2 +- .github/{ => workflows}/release-branch-drafter.yml | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename .github/{ => workflows}/release-branch-drafter.yml (100%) diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 316b60361..8fabb5be9 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -1,4 +1,4 @@ -name-template: 'Version $RESOLVED_VERSION' +name-template: 'v$RESOLVED_VERSION' tag-template: 'v$RESOLVED_VERSION' change-template: '- $TITLE @$AUTHOR (#$NUMBER)' template: | diff --git a/.github/release-branch-drafter.yml b/.github/workflows/release-branch-drafter.yml similarity index 100% rename from .github/release-branch-drafter.yml rename to .github/workflows/release-branch-drafter.yml From 90799b350e20c758ab70787d1c3b87c52ed1c198 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Sun, 3 May 2026 13:54:23 +0900 Subject: [PATCH 20/47] ci: add auto-label action and remove from pr-lint --- .github/workflows/auto-label.yml | 19 +++++++++++++++++++ .github/workflows/pr-lint.yml | 14 +------------- 2 files changed, 20 insertions(+), 13 deletions(-) create mode 100644 .github/workflows/auto-label.yml diff --git a/.github/workflows/auto-label.yml b/.github/workflows/auto-label.yml new file mode 100644 index 000000000..f03990f39 --- /dev/null +++ b/.github/workflows/auto-label.yml @@ -0,0 +1,19 @@ +--- +name: Auto Label + +on: + pull_request: + types: [opened, reopened, synchronize] + pull_request_target: + types: [opened, reopened, synchronize] + +permissions: + contents: read + +jobs: + auto_label: + permissions: + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: release-drafter/release-drafter/autolabeler@v7 diff --git a/.github/workflows/pr-lint.yml b/.github/workflows/pr-lint.yml index 444e11d01..5fb3768ca 100644 --- a/.github/workflows/pr-lint.yml +++ b/.github/workflows/pr-lint.yml @@ -4,11 +4,7 @@ name: 'PR' on: pull_request_target: - types: - - opened - - reopened - - edited - - synchronize + types: [opened, reopened, edited, synchronize] permissions: contents: read @@ -45,11 +41,3 @@ jobs: revert test break - - # Run release-drafter in label-only mode, drafts are made in release-drafter.yml - - uses: release-drafter/release-drafter@139054aeaa9adc52ab36ddf67437541f039b88e2 # v7 - with: - config-name: release-drafter.yml - disable-releaser: true - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 443492892d4ffbdcc6473eeda23906eec80e3f41 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Sun, 3 May 2026 14:00:58 +0900 Subject: [PATCH 21/47] chore: pin autolabeler action --- .github/workflows/auto-label.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/auto-label.yml b/.github/workflows/auto-label.yml index f03990f39..1cd94d477 100644 --- a/.github/workflows/auto-label.yml +++ b/.github/workflows/auto-label.yml @@ -16,4 +16,4 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: release-drafter/release-drafter/autolabeler@v7 + - uses: release-drafter/release-drafter/autolabeler@563bf132657a13ded0b01fcb723c5a58cdd824e2 From 402319a471e72c53b5e637983ae2e7d505ff848a Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Sun, 3 May 2026 14:04:28 +0900 Subject: [PATCH 22/47] chore: remove unsafe pull_request trigger --- .github/workflows/auto-label.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/auto-label.yml b/.github/workflows/auto-label.yml index 1cd94d477..f1b406b19 100644 --- a/.github/workflows/auto-label.yml +++ b/.github/workflows/auto-label.yml @@ -2,8 +2,6 @@ name: Auto Label on: - pull_request: - types: [opened, reopened, synchronize] pull_request_target: types: [opened, reopened, synchronize] From 669bd1eed1c543dfed7807d40f346c3b9e48898a Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Mon, 4 May 2026 11:37:11 +0900 Subject: [PATCH 23/47] ci: rename release drafter config --- .github/{release-drafter.yml => release-drafter-config.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/{release-drafter.yml => release-drafter-config.yml} (100%) diff --git a/.github/release-drafter.yml b/.github/release-drafter-config.yml similarity index 100% rename from .github/release-drafter.yml rename to .github/release-drafter-config.yml From 8eddf8c214aa8d6d04a47a0064aca963a2f2cc9e Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Mon, 4 May 2026 11:38:18 +0900 Subject: [PATCH 24/47] ci: fix release-drafter to run on pushes to release branches only --- ...se-branch-drafter.yml => release-drafter.yml} | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) rename .github/workflows/{release-branch-drafter.yml => release-drafter.yml} (59%) diff --git a/.github/workflows/release-branch-drafter.yml b/.github/workflows/release-drafter.yml similarity index 59% rename from .github/workflows/release-branch-drafter.yml rename to .github/workflows/release-drafter.yml index 6755bcc3e..2afb7474d 100644 --- a/.github/workflows/release-branch-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -1,13 +1,9 @@ -name: Release Drafter (Release Branches) +name: Release Drafter on: push: branches: - 'release/**' - pull_request: - # needed so autolabeler runs on PRs from forks before merge, - # for when PR is finally merged into a release branch. - types: [opened, reopened, synchronize] permissions: contents: read @@ -15,8 +11,10 @@ permissions: jobs: update_release_draft: permissions: - contents: write # needed to create/update the draft release - pull-requests: write # needed for the autolabeler + # Required to create/update the draft release + # Autolabeling runs in a separate workflow so this job only + # needs to read merged PRs + contents: write runs-on: ubuntu-latest steps: - name: Harden Runner @@ -24,10 +22,12 @@ jobs: with: egress-policy: audit - - uses: release-drafter/release-drafter@v6 + - uses: release-drafter/release-drafter@563bf132657a13ded0b01fcb723c5a58cdd824e2 with: # Target the branch that triggered this run # Ex: "release/2.1" becomes the commitish for the draft commitish: ${{ github.ref_name }} + repository: ${{ github.repository }} + config-name: release-drafter-config.yml env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 4fdb3371935c3cc779e2c2da252996e55c7dc00c Mon Sep 17 00:00:00 2001 From: Juan Escalada <97265671+jescalada@users.noreply.github.com> Date: Mon, 4 May 2026 12:09:42 +0900 Subject: [PATCH 25/47] feat: simulate v1.2.0 feature --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index fe84b343b..7f07ee61e 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ # GitProxy v1.1.0 Testing GitLabFlow branching strategy. This should automatically create a draft setting the project to `v1.1.0`. + +Testing new feature on v1.1.0, from `main`. This should automatically create a draft setting the project to `v1.2.0`. Note that `v1.1.0` release has already been made, and a `v1.1.1` bugfix draft is present. From 367a41e2be9b697f52fc0d2b9fcdb3a8fb83c0f5 Mon Sep 17 00:00:00 2001 From: Juan Escalada <97265671+jescalada@users.noreply.github.com> Date: Mon, 4 May 2026 12:18:29 +0900 Subject: [PATCH 26/47] ci: fix missing config-name in auto-label.yml --- .github/workflows/auto-label.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/auto-label.yml b/.github/workflows/auto-label.yml index f1b406b19..c1fcd22e2 100644 --- a/.github/workflows/auto-label.yml +++ b/.github/workflows/auto-label.yml @@ -1,5 +1,5 @@ --- -name: Auto Label +name: Add PR Labels on: pull_request_target: @@ -15,3 +15,5 @@ jobs: runs-on: ubuntu-latest steps: - uses: release-drafter/release-drafter/autolabeler@563bf132657a13ded0b01fcb723c5a58cdd824e2 + with: + config-name: release-drafter-config.yml From 391c202f0e248c958115f5eacbffcc2e80295427 Mon Sep 17 00:00:00 2001 From: Juan Escalada <97265671+jescalada@users.noreply.github.com> Date: Mon, 4 May 2026 12:30:13 +0900 Subject: [PATCH 27/47] Fix formatting in README.md for version notes --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7f07ee61e..80fb21f42 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,4 @@ Testing GitLabFlow branching strategy. This should automatically create a draft setting the project to `v1.1.0`. -Testing new feature on v1.1.0, from `main`. This should automatically create a draft setting the project to `v1.2.0`. Note that `v1.1.0` release has already been made, and a `v1.1.1` bugfix draft is present. +Testing new feature on `v1.1.0`, from `main`. This should automatically create a draft setting the project to `v1.2.0`. Note that `v1.1.0` release has already been made, and a `v1.1.1` bugfix draft is present. From 3ed7c9191667e399adc76bcc7963daa86d04a834 Mon Sep 17 00:00:00 2001 From: Juan Escalada <97265671+jescalada@users.noreply.github.com> Date: Mon, 4 May 2026 13:17:01 +0900 Subject: [PATCH 28/47] fix: add filter-by-commitish option to release drafter config --- .github/release-drafter-config.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/release-drafter-config.yml b/.github/release-drafter-config.yml index dc4953957..0bea1b1ff 100644 --- a/.github/release-drafter-config.yml +++ b/.github/release-drafter-config.yml @@ -2,6 +2,7 @@ name-template: 'v$RESOLVED_VERSION' tag-template: 'v$RESOLVED_VERSION' change-template: '- $TITLE @$AUTHOR (#$NUMBER)' +filter-by-commitish: true template: | ### What's Changed From 4dee3c8e18234eee873a6254bbe5e515d0785329 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Mon, 4 May 2026 13:57:31 +0900 Subject: [PATCH 29/47] feat: bump to 1.3, simulate feature being added --- README.md | 2 ++ package-lock.json | 6 +++--- package.json | 2 +- packages/git-proxy-cli/package.json | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 80fb21f42..0050586e3 100644 --- a/README.md +++ b/README.md @@ -3,3 +3,5 @@ Testing GitLabFlow branching strategy. This should automatically create a draft setting the project to `v1.1.0`. Testing new feature on `v1.1.0`, from `main`. This should automatically create a draft setting the project to `v1.2.0`. Note that `v1.1.0` release has already been made, and a `v1.1.1` bugfix draft is present. + +Testing new feature on `v1.2.0` from `main`. This should create a draft to `1.3.0` once the `release/1.3` branch is created. diff --git a/package-lock.json b/package-lock.json index ccaf37b04..35bacbc72 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@jescalada/git-proxy-deployment-testing", - "version": "1.1.0", + "version": "1.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@jescalada/git-proxy-deployment-testing", - "version": "1.1.0", + "version": "1.3.0", "license": "Apache-2.0", "workspaces": [ "./packages/git-proxy-cli" @@ -15066,7 +15066,7 @@ }, "packages/git-proxy-cli": { "name": "@jescalada/git-proxy-cli", - "version": "1.1.0", + "version": "1.3.0", "license": "Apache-2.0", "dependencies": { "@finos/git-proxy": "2.0.0-rc.6", diff --git a/package.json b/package.json index fda5d44d6..b6ece5701 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@jescalada/git-proxy-deployment-testing", - "version": "1.1.0", + "version": "1.3.0", "description": "Testing deployment workflows for @finos/git-proxy.", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/git-proxy-cli/package.json b/packages/git-proxy-cli/package.json index 8cb4d36f9..88712acd5 100644 --- a/packages/git-proxy-cli/package.json +++ b/packages/git-proxy-cli/package.json @@ -1,6 +1,6 @@ { "name": "@jescalada/git-proxy-cli", - "version": "1.1.0", + "version": "1.3.0", "description": "Command line interface tool for FINOS GitProxy.", "bin": { "git-proxy-cli": "./dist/index.js" From 49bc02224579f183500662a9cd39e60200060b0c Mon Sep 17 00:00:00 2001 From: Juan Escalada <97265671+jescalada@users.noreply.github.com> Date: Mon, 4 May 2026 14:06:09 +0900 Subject: [PATCH 30/47] fix: remove filter-by-commitish option release drafter config --- .github/release-drafter-config.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/release-drafter-config.yml b/.github/release-drafter-config.yml index 0bea1b1ff..dc4953957 100644 --- a/.github/release-drafter-config.yml +++ b/.github/release-drafter-config.yml @@ -2,7 +2,6 @@ name-template: 'v$RESOLVED_VERSION' tag-template: 'v$RESOLVED_VERSION' change-template: '- $TITLE @$AUTHOR (#$NUMBER)' -filter-by-commitish: true template: | ### What's Changed From 4586c346e921f7839543ba4f2f7feeb606247d63 Mon Sep 17 00:00:00 2001 From: Juan Escalada <97265671+jescalada@users.noreply.github.com> Date: Mon, 4 May 2026 14:33:45 +0900 Subject: [PATCH 31/47] feat: retest feature on 1.2.0 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0050586e3..95d8569a7 100644 --- a/README.md +++ b/README.md @@ -4,4 +4,4 @@ Testing GitLabFlow branching strategy. This should automatically create a draft Testing new feature on `v1.1.0`, from `main`. This should automatically create a draft setting the project to `v1.2.0`. Note that `v1.1.0` release has already been made, and a `v1.1.1` bugfix draft is present. -Testing new feature on `v1.2.0` from `main`. This should create a draft to `1.3.0` once the `release/1.3` branch is created. +Testing new feature on `v1.2.0` from `main`. This should create a draft to `1.3.0` once the `release/1.3` branch is created. Retesting. From 58daeca58349ec5f4cf4de0d821abc07e075fa34 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Wed, 1 Jul 2026 13:09:02 +0900 Subject: [PATCH 32/47] ci: add version-bump.yml --- .github/workflows/version-bump.yml | 110 +++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 .github/workflows/version-bump.yml diff --git a/.github/workflows/version-bump.yml b/.github/workflows/version-bump.yml new file mode 100644 index 000000000..91d30540d --- /dev/null +++ b/.github/workflows/version-bump.yml @@ -0,0 +1,110 @@ +name: Bump package versions + +# Can be triggered by closing a milestone or manually +on: + milestone: + types: [closed] + workflow_dispatch: + inputs: + version: + description: "Version to bump to, e.g. 2.2.0" + required: true + +permissions: + contents: write + pull-requests: write + +concurrency: + group: version-bump-${{ github.event.milestone.number || github.run_id }} + cancel-in-progress: false + +jobs: + bump-version: + runs-on: ubuntu-latest + steps: + - name: Harden Runner + uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + with: + egress-policy: audit + + - name: Checkout main + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: main + + - name: Setup Node + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 + with: + node-version: "24" + + - name: Resolve version + id: version + env: + MILESTONE_TITLE: ${{ github.event.milestone.title }} + MANUAL_VERSION: ${{ inputs.version }} + run: | + RAW="${MANUAL_VERSION:-$MILESTONE_TITLE}" + RAW="$(printf '%s' "$RAW" | tr -d '[:space:]')" + + if [[ ! "$RAW" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$ ]]; then + echo "::error::'$RAW' isn't a plain semver version (for example '2.2.0'). Not bumping." + exit 1 + fi + + VERSION="${RAW#v}" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "Resolved version: $VERSION" + + - name: Sanity check against current version + env: + VERSION: ${{ steps.version.outputs.version }} + run: | + CURRENT=$(node -p "require('./package.json').version") + NEWEST=$(printf '%s\n%s\n' "$CURRENT" "$VERSION" | sort -V | tail -n1) + if [ "$VERSION" = "$CURRENT" ] || [ "$NEWEST" != "$VERSION" ]; then + echo "::error::$VERSION is not newer than the current package.json version ($CURRENT)." + exit 1 + fi + + - name: Bump root package + env: + VERSION: ${{ steps.version.outputs.version }} + run: npm version "$VERSION" --no-git-tag-version --allow-same-version + + - name: Bump git-proxy-cli package + working-directory: packages/git-proxy-cli + env: + VERSION: ${{ steps.version.outputs.version }} + run: npm version "$VERSION" --no-git-tag-version --allow-same-version + + - name: Open version bump PR + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION: ${{ steps.version.outputs.version }} + MILESTONE_TITLE: ${{ github.event.milestone.title }} + MILESTONE_URL: ${{ github.event.milestone.html_url }} + run: | + BRANCH="chore/bump-version-$VERSION" + RELEASE_BRANCH="release/${VERSION%.*}" + + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + git checkout -b "$BRANCH" + git add package.json package-lock.json packages/git-proxy-cli/package.json packages/git-proxy-cli/package-lock.json + git commit -m "chore: bump version to $VERSION" + git push -u origin "$BRANCH" --force + + if [ -n "$MILESTONE_URL" ]; then + SOURCE_LINE="Triggered by closing milestone **$MILESTONE_TITLE** ($MILESTONE_URL)." + else + SOURCE_LINE="Triggered manually for version $VERSION." + fi + + BODY=$(printf '%s\n\nBumps:\n- `package.json` / `package-lock.json`\n- `packages/git-proxy-cli/package.json` / `packages/git-proxy-cli/package-lock.json`\n\nOnce merged, cut `%s` from `main` per the release process.\n' "$SOURCE_LINE" "$RELEASE_BRANCH") + + if gh pr view "$BRANCH" >/dev/null 2>&1; then + echo "PR for $BRANCH already exists. Branch updated, no new PR opened." + else + gh pr create --base main --head "$BRANCH" --title "chore: bump version to $VERSION" --body "$BODY" + fi From cc24c0ebdb670ade55a486795080760ad104c0fb Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Wed, 1 Jul 2026 13:21:33 +0900 Subject: [PATCH 33/47] chore: fix package-lock.json --- package-lock.json | 319 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 224 insertions(+), 95 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1d2a483ba..ca7306b0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1040,7 +1040,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -2601,6 +2600,230 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@finos/git-proxy": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@finos/git-proxy/-/git-proxy-2.0.0.tgz", + "integrity": "sha512-55UPvUZeZ6Z4TEz5P2ADLQKD1+4ZCzPeaLSXzukpfn2Do8+ughHHSRtHDXuWR5u9GlZFzZVxhW/P0LTr3G3aBw==", + "license": "Apache-2.0", + "workspaces": [ + "./packages/git-proxy-cli" + ], + "dependencies": { + "@aws-sdk/credential-providers": "^3.980.0", + "@fontsource/roboto": "^5.2.9", + "@material-ui/core": "^4.12.4", + "@material-ui/icons": "4.11.3", + "@primer/octicons-react": "^19.21.2", + "@seald-io/nedb": "^4.1.2", + "axios": "^1.13.4", + "bcryptjs": "^3.0.3", + "clsx": "^2.1.1", + "concurrently": "^9.2.1", + "connect-mongo": "^5.1.0", + "cors": "^2.8.6", + "diff2html": "^3.4.56", + "env-paths": "^3.0.0", + "escape-string-regexp": "^5.0.0", + "express": "^5.2.1", + "express-http-proxy": "^2.1.2", + "express-rate-limit": "^8.2.1", + "express-session": "^1.19.0", + "font-awesome": "^4.7.0", + "history": "5.3.0", + "isomorphic-git": "^1.36.3", + "jsonwebtoken": "^9.0.3", + "load-plugin": "^6.0.3", + "lodash": "^4.17.23", + "lusca": "^1.7.0", + "material-design-icons": "^3.0.1", + "moment": "^2.30.1", + "mongodb": "^5.9.2", + "openid-client": "^6.8.1", + "parse-diff": "^0.11.1", + "passport": "^0.7.0", + "passport-activedirectory": "^1.4.0", + "passport-local": "^1.0.0", + "perfect-scrollbar": "^1.5.6", + "react": "^16.14.0", + "react-dom": "^16.14.0", + "react-html-parser": "^2.0.2", + "react-router-dom": "6.30.3", + "simple-git": "^3.30.0", + "uuid": "^13.0.0", + "validator": "^13.15.26", + "yargs": "^17.7.2" + }, + "bin": { + "git-proxy": "dist/index.js", + "git-proxy-all": "concurrently 'npm run server' 'npm run client'" + }, + "engines": { + "node": ">=22.13.1 || >=24.0.0" + }, + "optionalDependencies": { + "@esbuild/darwin-arm64": "^0.27.2", + "@esbuild/darwin-x64": "^0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/@finos/git-proxy-cli": { + "resolved": "packages/git-proxy-cli", + "link": true + }, + "node_modules/@finos/git-proxy/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@finos/git-proxy/node_modules/concurrently": { + "version": "9.2.3", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.3.tgz", + "integrity": "sha512-ihjs0E2SxvDgq/MK418hX6YycQgKhsqxpbZuZbHo0yKfqDWdymWMjWYIpCIzqDDLLKClHlXev8whW/8WXmJ0BA==", + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.4", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/@finos/git-proxy/node_modules/concurrently/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@finos/git-proxy/node_modules/connect-mongo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/connect-mongo/-/connect-mongo-5.1.0.tgz", + "integrity": "sha512-xT0vxQLqyqoUTxPLzlP9a/u+vir0zNkhiy9uAdHjSCcUUf7TS5b55Icw8lVyYFxfemP3Mf9gdwUOgeF3cxCAhw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.1", + "kruptein": "^3.0.0" + }, + "engines": { + "node": ">=12.9.0" + }, + "peerDependencies": { + "express-session": "^1.17.1", + "mongodb": ">= 5.1.0 < 7" + } + }, + "node_modules/@finos/git-proxy/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/@finos/git-proxy/node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@finos/git-proxy/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@finos/git-proxy/node_modules/uuid": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.2.tgz", + "integrity": "sha512-vzi9uRZ926x4XV73S/4qQaTwPXM2JBj6/6lI/byHH1jOpCzb0zDbfytgA9LcN/hzb2l7WQSQnxITOVx5un/wGw==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/@finos/git-proxy/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@finos/git-proxy/node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@fontsource/roboto": { "version": "5.2.9", "resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.2.9.tgz", @@ -2804,10 +3027,6 @@ "node": ">=8" } }, - "node_modules/@jescalada/git-proxy-cli": { - "resolved": "packages/git-proxy-cli", - "link": true - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.12", "dev": true, @@ -4578,7 +4797,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.8.tgz", "integrity": "sha512-ebO/Yl+EAvVe8DnMfi+iaAyIqYdK0q/q0y0rw82INWEKJOBe6b/P3YWE8NW7oOlF/nXFNrHwhARrN/hdgDkraA==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -4633,7 +4851,6 @@ "node_modules/@types/react": { "version": "17.0.74", "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -4846,7 +5063,6 @@ "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/types": "8.56.0", @@ -5424,7 +5640,6 @@ "version": "8.15.0", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6074,7 +6289,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", @@ -6470,7 +6684,6 @@ }, "node_modules/chalk": { "version": "4.1.2", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -6499,7 +6712,6 @@ }, "node_modules/chalk/node_modules/supports-color": { "version": "7.2.0", - "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -7576,7 +7788,6 @@ "version": "2.4.1", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" @@ -7869,7 +8080,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -8308,7 +8518,6 @@ "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.19.0.tgz", "integrity": "sha512-0csaMkGq+vaiZTmSMMGkfdCOabYv192VbytFypcvI0MANrp+4i/7yEkJ0sbAEhycQjntaKGzYfjfXQyVb7BHMA==", "license": "MIT", - "peer": true, "dependencies": { "cookie": "~0.7.2", "cookie-signature": "~1.0.7", @@ -9127,7 +9336,6 @@ }, "node_modules/has-flag": { "version": "4.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -11439,7 +11647,6 @@ "node_modules/mongodb": { "version": "5.9.2", "license": "Apache-2.0", - "peer": true, "dependencies": { "bson": "^5.5.0", "mongodb-connection-string-url": "^2.6.0", @@ -12744,7 +12951,6 @@ "node_modules/react": { "version": "16.14.0", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -12757,7 +12963,6 @@ "node_modules/react-dom": { "version": "16.14.0", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -12933,7 +13138,6 @@ }, "node_modules/require-directory": { "version": "2.1.1", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -13977,7 +14181,6 @@ }, "node_modules/supports-color": { "version": "8.1.1", - "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -14170,7 +14373,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -14381,7 +14583,6 @@ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" @@ -14542,7 +14743,6 @@ "version": "5.9.3", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -14824,7 +15024,6 @@ "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -14959,7 +15158,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -14973,7 +15171,6 @@ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -15472,7 +15669,6 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, "license": "ISC", "engines": { "node": ">=12" @@ -15577,73 +15773,6 @@ "bin": { "git-proxy-cli": "dist/index.js" } - }, - "packages/git-proxy-cli/node_modules/@finos/git-proxy": { - "version": "2.0.0-rc.6", - "resolved": "https://registry.npmjs.org/@finos/git-proxy/-/git-proxy-2.0.0-rc.6.tgz", - "integrity": "sha512-Crb+uQHb/U6wmJzuuhvEWO2DJmVW/1VKooAFmWox69XooqpxD1WqerxL0edU1k3tGws9h07IXmrd0KwoJw/CxA==", - "license": "Apache-2.0", - "workspaces": [ - "./packages/git-proxy-cli" - ], - "dependencies": { - "@aws-sdk/credential-providers": "^3.980.0", - "@fontsource/roboto": "^5.2.9", - "@material-ui/core": "^4.12.4", - "@material-ui/icons": "4.11.3", - "@primer/octicons-react": "^19.21.2", - "@seald-io/nedb": "^4.1.2", - "axios": "^1.13.4", - "bcryptjs": "^3.0.3", - "clsx": "^2.1.1", - "concurrently": "^9.2.1", - "connect-mongo": "^5.1.0", - "cors": "^2.8.6", - "diff2html": "^3.4.56", - "env-paths": "^3.0.0", - "escape-string-regexp": "^5.0.0", - "express": "^5.2.1", - "express-http-proxy": "^2.1.2", - "express-rate-limit": "^8.2.1", - "express-session": "^1.19.0", - "font-awesome": "^4.7.0", - "history": "5.3.0", - "isomorphic-git": "^1.36.3", - "jsonwebtoken": "^9.0.3", - "load-plugin": "^6.0.3", - "lodash": "^4.17.23", - "lusca": "^1.7.0", - "material-design-icons": "^3.0.1", - "moment": "^2.30.1", - "mongodb": "^5.9.2", - "openid-client": "^6.8.1", - "parse-diff": "^0.11.1", - "passport": "^0.7.0", - "passport-activedirectory": "^1.4.0", - "passport-local": "^1.0.0", - "perfect-scrollbar": "^1.5.6", - "react": "^16.14.0", - "react-dom": "^16.14.0", - "react-html-parser": "^2.0.2", - "react-router-dom": "6.30.3", - "simple-git": "^3.30.0", - "uuid": "^13.0.0", - "validator": "^13.15.26", - "yargs": "^17.7.2" - }, - "bin": { - "git-proxy": "dist/index.js", - "git-proxy-all": "concurrently 'npm run server' 'npm run client'" - }, - "engines": { - "node": ">=22.13.1 || >=24.0.0" - }, - "optionalDependencies": { - "@esbuild/darwin-arm64": "^0.27.2", - "@esbuild/darwin-x64": "^0.27.2", - "@esbuild/linux-x64": "0.27.2", - "@esbuild/win32-x64": "0.27.2" - } } } } From 8d584701ed40dbf21247c8771866c4152e41b9a0 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Wed, 1 Jul 2026 13:41:25 +0900 Subject: [PATCH 34/47] fix: cli package-lock.json not needed --- .github/workflows/version-bump.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/version-bump.yml b/.github/workflows/version-bump.yml index 91d30540d..3298566ad 100644 --- a/.github/workflows/version-bump.yml +++ b/.github/workflows/version-bump.yml @@ -7,7 +7,7 @@ on: workflow_dispatch: inputs: version: - description: "Version to bump to, e.g. 2.2.0" + description: 'Version to bump to, e.g. 2.2.0' required: true permissions: @@ -35,7 +35,7 @@ jobs: - name: Setup Node uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: - node-version: "24" + node-version: '24' - name: Resolve version id: version @@ -91,7 +91,7 @@ jobs: git config user.email "github-actions[bot]@users.noreply.github.com" git checkout -b "$BRANCH" - git add package.json package-lock.json packages/git-proxy-cli/package.json packages/git-proxy-cli/package-lock.json + git add package.json package-lock.json packages/git-proxy-cli/package.json git commit -m "chore: bump version to $VERSION" git push -u origin "$BRANCH" --force @@ -101,7 +101,7 @@ jobs: SOURCE_LINE="Triggered manually for version $VERSION." fi - BODY=$(printf '%s\n\nBumps:\n- `package.json` / `package-lock.json`\n- `packages/git-proxy-cli/package.json` / `packages/git-proxy-cli/package-lock.json`\n\nOnce merged, cut `%s` from `main` per the release process.\n' "$SOURCE_LINE" "$RELEASE_BRANCH") + BODY=$(printf '%s\n\nBumps:\n- `package.json` / `package-lock.json`\n- `packages / `packages/git-proxy-cli/package.json`\n\nOnce merged, cut `%s` from `main` per the release process.\n' "$SOURCE_LINE" "$RELEASE_BRANCH") if gh pr view "$BRANCH" >/dev/null 2>&1; then echo "PR for $BRANCH already exists. Branch updated, no new PR opened." From b24fa39311dd01dfe8837922a114bba20c3fef90 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Mon, 10 Aug 2026 14:00:59 +0900 Subject: [PATCH 35/47] ci: add debug mode for ai triage workflows --- .github/workflows/pr-quality-check.yml | 2 ++ .github/workflows/security-review.yml | 2 ++ .github/workflows/triage.yml | 2 ++ scripts/agents/helpers.py | 38 ++++++++++++++++++++++++++ 4 files changed, 44 insertions(+) diff --git a/.github/workflows/pr-quality-check.yml b/.github/workflows/pr-quality-check.yml index 2deda4c0b..9a7a5c98f 100644 --- a/.github/workflows/pr-quality-check.yml +++ b/.github/workflows/pr-quality-check.yml @@ -22,6 +22,8 @@ jobs: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + # Set repo variable DEBUG_MODE=true to enable token/cost logging in job logs + DEBUG_MODE: ${{ secrets.DEBUG_AI_WORKFLOWS }} # Obtained automatically by GH Actions AUTHOR_ASSOCIATION: ${{ github.event.pull_request.author_association }} AUTHOR_USERNAME: ${{ github.event.pull_request.user.login }} diff --git a/.github/workflows/security-review.yml b/.github/workflows/security-review.yml index 47dfb548c..3f359f120 100644 --- a/.github/workflows/security-review.yml +++ b/.github/workflows/security-review.yml @@ -37,6 +37,8 @@ jobs: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + # Set repo variable DEBUG_MODE=true to enable token/cost logging in job logs + DEBUG_MODE: ${{ secrets.DEBUG_AI_WORKFLOWS }} # Obtained automatically by GH Actions GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} diff --git a/.github/workflows/triage.yml b/.github/workflows/triage.yml index 6188ba3ca..60b9d6f9c 100644 --- a/.github/workflows/triage.yml +++ b/.github/workflows/triage.yml @@ -24,6 +24,8 @@ jobs: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + # Set repo variable DEBUG_MODE=true to enable token/cost logging in job logs + DEBUG_MODE: ${{ secrets.DEBUG_AI_WORKFLOWS }} # Obtained automatically by GH Actions GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ISSUE_BODY: ${{ github.event.issue.body }} diff --git a/scripts/agents/helpers.py b/scripts/agents/helpers.py index 0cda26e17..bd55b5283 100644 --- a/scripts/agents/helpers.py +++ b/scripts/agents/helpers.py @@ -14,11 +14,42 @@ def validate_env_vars(env_vars: list[str]): raise ValueError(f"{env_var} is not set") +def _debug_mode_enabled(): + return os.environ.get("DEBUG_AI_WORKFLOWS", "").strip().lower() in ("true", "1", "yes") + + def run_agent(messages: list, tools: list, handle_tool_call, model: str): + debug = _debug_mode_enabled() + total_prompt_tokens = 0 + total_completion_tokens = 0 + total_tokens = 0 + total_cost = 0.0 + while True: response = litellm.completion( model=model, messages=messages, tools=tools, temperature=0 ) + if debug: + usage = getattr(response, "usage", None) + prompt_tokens = getattr(usage, "prompt_tokens", None) if usage else None + completion_tokens = getattr(usage, "completion_tokens", None) if usage else None + tokens = getattr(usage, "total_tokens", None) if usage else None + if prompt_tokens is not None: + total_prompt_tokens += prompt_tokens + if completion_tokens is not None: + total_completion_tokens += completion_tokens + if tokens is not None: + total_tokens += tokens + print( + f"[debug] tokens prompt={prompt_tokens} " + f"completion={completion_tokens} total={tokens}" + ) + try: + cost = litellm.completion_cost(completion_response=response) + total_cost += cost + print(f"[debug] estimated cost=${cost:.6f}") + except Exception: + print("[debug] estimated cost=unavailable") message = response.choices[0].message if message.content: print(f"[agent] {message.content}") @@ -35,3 +66,10 @@ def run_agent(messages: list, tools: list, handle_tool_call, model: str): "content": result, }) messages.extend(tool_results) + + if debug: + print( + f"[debug] summary prompt={total_prompt_tokens} " + f"completion={total_completion_tokens} total={total_tokens} " + f"estimated_cost=${total_cost:.6f}" + ) From e9129075b0ae387551cf8e612d2b020d6ffdae63 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Mon, 10 Aug 2026 14:37:06 +0900 Subject: [PATCH 36/47] fix: debug mode env name --- .github/workflows/pr-quality-check.yml | 4 ++-- .github/workflows/security-review.yml | 4 ++-- .github/workflows/triage.yml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/pr-quality-check.yml b/.github/workflows/pr-quality-check.yml index bebb931fd..4bff9acb5 100644 --- a/.github/workflows/pr-quality-check.yml +++ b/.github/workflows/pr-quality-check.yml @@ -22,8 +22,8 @@ jobs: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - # Set repo variable DEBUG_MODE=true to enable token/cost logging in job logs - DEBUG_MODE: ${{ secrets.DEBUG_AI_WORKFLOWS }} + # Enable token/cost logging in job logs + DEBUG_AI_WORKFLOWS: ${{ secrets.DEBUG_AI_WORKFLOWS }} # Obtained automatically by GH Actions AUTHOR_ASSOCIATION: ${{ github.event.pull_request.author_association }} AUTHOR_USERNAME: ${{ github.event.pull_request.user.login }} diff --git a/.github/workflows/security-review.yml b/.github/workflows/security-review.yml index 471fb858e..40e5efacf 100644 --- a/.github/workflows/security-review.yml +++ b/.github/workflows/security-review.yml @@ -38,8 +38,8 @@ jobs: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - # Set repo variable DEBUG_MODE=true to enable token/cost logging in job logs - DEBUG_MODE: ${{ secrets.DEBUG_AI_WORKFLOWS }} + # Enable token/cost logging in job logs + DEBUG_AI_WORKFLOWS: ${{ secrets.DEBUG_AI_WORKFLOWS }} # Obtained automatically by GH Actions GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} diff --git a/.github/workflows/triage.yml b/.github/workflows/triage.yml index 60b9d6f9c..eef1c0f7c 100644 --- a/.github/workflows/triage.yml +++ b/.github/workflows/triage.yml @@ -24,8 +24,8 @@ jobs: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - # Set repo variable DEBUG_MODE=true to enable token/cost logging in job logs - DEBUG_MODE: ${{ secrets.DEBUG_AI_WORKFLOWS }} + # Enable token/cost logging in job logs + DEBUG_AI_WORKFLOWS: ${{ secrets.DEBUG_AI_WORKFLOWS }} # Obtained automatically by GH Actions GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ISSUE_BODY: ${{ github.event.issue.body }} From e17317157d3935c065b23db74150dd652f79b5dc Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Mon, 10 Aug 2026 16:30:50 +0900 Subject: [PATCH 37/47] chore: remove unused scripts --- scripts/pr_checker_agent.py | 149 -------------------- scripts/security_review_agent.py | 200 -------------------------- scripts/triage_agent.py | 233 ------------------------------- 3 files changed, 582 deletions(-) delete mode 100644 scripts/pr_checker_agent.py delete mode 100644 scripts/security_review_agent.py delete mode 100644 scripts/triage_agent.py diff --git a/scripts/pr_checker_agent.py b/scripts/pr_checker_agent.py deleted file mode 100644 index 2420efe6f..000000000 --- a/scripts/pr_checker_agent.py +++ /dev/null @@ -1,149 +0,0 @@ -import os -import anthropic -from github import Github, Auth - -# Setup - -gh = Github(auth=Auth.Token(os.environ["GITHUB_TOKEN"])) -repo = gh.get_repo(os.environ["REPO_NAME"]) -pr = repo.get_pull(int(os.environ["PR_NUMBER"])) -author = os.environ["AUTHOR_USERNAME"] -client = anthropic.Anthropic() - -# Tools - -TOOLS = [ - { - "name": "post_comment", - "description": ( - "Post a comment on the PR. Use this to welcome a first-time contributor, " - "ask for a clearer description, request an issue link, or flag non-compliance " - "with CONTRIBUTING.md. Combine multiple concerns into a single comment where " - "possible rather than posting several separate ones." - ), - "input_schema": { - "type": "object", - "properties": { - "body": {"type": "string", "description": "The comment text (markdown supported)."} - }, - "required": ["body"], - }, - }, -] - -# System prompt - -SYSTEM_PROMPT = """You are a PR review assistant for an open-source GitHub repository. -Given a newly opened PR, its author's contribution history, and the repository's CONTRIBUTING.md, -you must check the following — in this order: - -1. FIRST CONTRIBUTION: If this is the author's first contribution to the repo, welcome them warmly. - Acknowledge their effort and point them to any relevant getting-started resources in CONTRIBUTING.md. - -2. DESCRIPTION CLARITY: If the PR description is missing, too vague, or doesn't explain what - the change does and why, ask for a clearer description. - -3. LINKED ISSUE: Check whether the description contains a linked issue using keywords like - "Fixes #N", "Closes #N", "Resolves #N", or "Related to #N". If no issue is linked, - ask the author to either link an existing issue or create a new one. - -4. CONTRIBUTING.md COMPLIANCE: Check whether the PR description follows the structure or - requirements defined in CONTRIBUTING.md. If it doesn't comply, quote the relevant section - and point out specifically what needs to change. - -Important rules: -- If multiple concerns apply, combine them into a single comment — never post more than one. -- If everything looks good, stay silent. Do not post a comment just to say things look fine. -- Be warm and constructive, never demanding. Remember this may be someone's first open-source contribution. -- When referencing CONTRIBUTING.md requirements, be specific — quote or paraphrase the rule, - don't just say "please read the contributing guide". -- Most importantly, be as succint as possible.""" - -# GitHub helpers - -def get_contributing_md() -> str: - """Fetches CONTRIBUTING.md from the repo root, or returns a notice if absent.""" - try: - contents = repo.get_contents("CONTRIBUTING.md") - return contents.decoded_content.decode("utf-8") - except Exception: - return "(No CONTRIBUTING.md found in this repository.)" - - -def is_first_contribution() -> bool: - """Returns True if the author has no previously merged PRs in this repo.""" - first_contribution_list = ['FIRST_TIMER', 'FIRST_TIME_CONTRIBUTOR', 'NONE'] - if os.environ["AUTHOR_ASSOCIATION"] in first_contribution_list: - return True - return False - - -def post_comment(body: str) -> str: - pr.create_issue_comment(body) - return "Comment posted." - -# Tool dispatch - -def handle_tool_call(name: str, inputs: dict) -> str: - if name == "post_comment": - result = post_comment(inputs["body"]) - else: - result = f"Unknown tool: {name}" - - print(f"[tool] {name}: {result}") - return result - -# Agentic loop - -def build_initial_message() -> str: - first_contribution = is_first_contribution() - contributing_md = get_contributing_md() - - return ( - f"Please review this newly opened PR:\n\n" - f"Title: {os.environ['PR_TITLE']}\n" - f"Author: {author} ({'first-time contributor' if first_contribution else 'returning contributor'})\n" - f"Description:\n{os.environ.get('PR_BODY') or '(no description provided)'}\n\n" - f"---\n" - f"CONTRIBUTING.md contents:\n\n" - f"{contributing_md}" - ) - - -def run_pr_review_agent(): - messages = [{"role": "user", "content": build_initial_message()}] - - while True: - response = client.messages.create( - model="claude-sonnet-4-20250514", - max_tokens=1024, - system=SYSTEM_PROMPT, - tools=TOOLS, - messages=messages, - ) - - for block in response.content: - if block.type == "text" and block.text: - print(f"[agent] {block.text}") - - messages.append({"role": "assistant", "content": response.content}) - - if response.stop_reason == "end_turn": - break - - tool_results = [] - for block in response.content: - if block.type != "tool_use": - continue - result = handle_tool_call(block.name, block.input) - tool_results.append({ - "type": "tool_result", - "tool_use_id": block.id, - "content": result, - }) - - messages.append({"role": "user", "content": tool_results}) - - -if __name__ == "__main__": - run_pr_review_agent() \ No newline at end of file diff --git a/scripts/security_review_agent.py b/scripts/security_review_agent.py deleted file mode 100644 index c83ec8937..000000000 --- a/scripts/security_review_agent.py +++ /dev/null @@ -1,200 +0,0 @@ -import os -import anthropic -from github import Github, Auth - -# Setup - -gh = Github(auth=Auth.Token(os.environ["GITHUB_TOKEN"])) -repo = gh.get_repo(os.environ["REPO_NAME"]) -pr = repo.get_pull(int(os.environ["PR_NUMBER"])) -client = anthropic.Anthropic() - -# Exclude files that are not useful for security analysis -IGNORED_FILENAMES = { - "package-lock.json", - "yarn.lock", - "poetry.lock", - "Gemfile.lock", - "Cargo.lock", - "composer.lock", - "pnpm-lock.yaml", - "pip.lock", -} - -IGNORED_EXTENSIONS = {".lock", ".sum"} - -# Truncate very large diffs like generated files to prevent bloating the prompt -MAX_PATCH_CHARS_PER_FILE = 3000 - -# System prompt - -SYSTEM_PROMPT = """You are a security analysis assistant for a GitHub repository. -You are given the diff of a pull request and must identify potential security issues. - -Focus only on security-relevant concerns such as: -- Hardcoded secrets, tokens, passwords or API keys -- Injection vulnerabilities (SQL, shell, template, etc.) -- Insecure use of cryptography or hashing -- Unsafe deserialization -- Path traversal or directory traversal risks -- Insecure direct object references -- Missing input validation or sanitisation on user-controlled data -- Use of known-vulnerable dependency versions (if visible in the diff) -- Overly permissive file or network access - -Do NOT comment on code style, performance, test coverage, or general best practices -unless they have a direct security implication. - -If you find no issues, say so clearly and briefly — do not invent concerns. -Format your response as a markdown comment suitable for posting directly on a GitHub PR. -Start with a short summary line, then list findings with file references where applicable. -If there are no findings, keep the response to 2-3 sentences maximum.""" - -# GitHub helpers - -def get_pr_diff() -> str: - """ - Fetches changed files and their patches, filtering out lockfiles and - other noise. Returns a formatted string ready to be included in the prompt. - """ - sections = [] - for f in pr.get_files(): - filename = os.path.basename(f.filename) - _, ext = os.path.splitext(filename) - - if filename in IGNORED_FILENAMES or ext in IGNORED_EXTENSIONS: - print(f"[diff] Skipping {f.filename} (ignored file type)") - continue - - if not f.patch: - print(f"[diff] Skipping {f.filename} (no patch — binary or too large)") - continue - - patch = f.patch[:MAX_PATCH_CHARS_PER_FILE] - truncated = len(f.patch) > MAX_PATCH_CHARS_PER_FILE - sections.append( - f"### {f.filename}\n```diff\n{patch}" - + ("\n... (truncated)" if truncated else "") - + "\n```" - ) - - return "\n\n".join(sections) if sections else "(no reviewable changes found)" - - -def find_previous_security_comment() -> object | None: - """ - Looks for an existing security review comment posted by github-actions[bot] - so we can replace it rather than stacking multiple comments on updated reviews. - """ - for comment in pr.get_issue_comments(): - if ( - comment.user.login == "github-actions[bot]" - and "🔒 Automated Security Review" in comment.body - ): - return comment - return None - - -def post_or_update_comment(body: str): - """ - If a previous security review comment exists, edit it in place. - Otherwise post a new one to keep the PR timeline clean. - """ - existing = find_previous_security_comment() - if existing: - existing.edit(body) - print("[comment] Updated existing security review comment.") - else: - pr.create_issue_comment(body) - print("[comment] Posted new security review comment.") - -# Tools - -TOOLS = [ - { - "name": "post_security_review", - "description": ( - "Post the security review findings as a comment on the PR. " - "Call this once when your analysis is complete. " - "If there are no findings, still call this to confirm the review ran." - ), - "input_schema": { - "type": "object", - "properties": { - "body": { - "type": "string", - "description": "The full markdown comment body to post on the PR.", - } - }, - "required": ["body"], - }, - } -] - -# Tool dispatch - -def handle_tool_call(name: str, inputs: dict) -> str: - if name == "post_security_review": - # Prepend a header to identify review comments across runs - body = f"## 🔒 Automated Security Review\n\n{inputs['body']}" - post_or_update_comment(body) - return "Security review comment posted." - return f"Unknown tool: {name}" - -# Agentic loop - -def build_initial_message() -> str: - trigger = os.environ.get("TRIGGER", "pull_request") - trigger_note = ( - "This review was requested manually via `/security-review`." - if trigger == "issue_comment" - else "This review was triggered automatically on PR creation." - ) - - return ( - f"Please perform a security review of this pull request.\n\n" - f"**PR #{pr.number}:** {pr.title}\n" - f"_{trigger_note}_\n\n" - f"---\n\n" - f"{get_pr_diff()}" - ) - - -def run_security_review_agent(): - messages = [{"role": "user", "content": build_initial_message()}] - - while True: - response = client.messages.create( - model="claude-sonnet-4-20250514", - max_tokens=1024, - system=SYSTEM_PROMPT, - tools=TOOLS, - messages=messages, - ) - - for block in response.content: - if block.type == "text" and block.text: - print(f"[agent] {block.text}") - - messages.append({"role": "assistant", "content": response.content}) - - if response.stop_reason == "end_turn": - break - - tool_results = [] - for block in response.content: - if block.type != "tool_use": - continue - result = handle_tool_call(block.name, block.input) - print(f"[tool] {block.name}: {result}") - tool_results.append({ - "type": "tool_result", - "tool_use_id": block.id, - "content": result, - }) - - messages.append({"role": "user", "content": tool_results}) - - -if __name__ == "__main__": - run_security_review_agent() \ No newline at end of file diff --git a/scripts/triage_agent.py b/scripts/triage_agent.py deleted file mode 100644 index b11fc82f4..000000000 --- a/scripts/triage_agent.py +++ /dev/null @@ -1,233 +0,0 @@ -import os -import anthropic -from github import Github, Auth - -# Setup - -gh = Github(auth=Auth.Token(os.environ["GITHUB_TOKEN"])) -repo = gh.get_repo(os.environ["REPO_NAME"]) -issue = repo.get_issue(int(os.environ["ISSUE_NUMBER"])) -client = anthropic.Anthropic() - -LATEST_ISSUES_LIMIT = 100 - -# Tools - -TOOLS = [ - { - "name": "apply_label", - "description": ( - "Apply one or more labels to the issue. " - "Use labels like: automation, bug, dependencies, " - "documentation, enhancement, good-first-issue, " - "meeting, needs-info, plugins, protocol, question, " - "security, tech-debt, testing." - ), - "input_schema": { - "type": "object", - "properties": { - "labels": { - "type": "array", - "items": {"type": "string"}, - "description": "List of labels to apply.", - } - }, - "required": ["labels"], - }, - }, - { - "name": "post_comment", - "description": "Post a comment on the issue, e.g. to ask for clarification or acknowledge receipt.", - "input_schema": { - "type": "object", - "properties": { - "body": {"type": "string", "description": "The comment text (markdown supported)."} - }, - "required": ["body"], - }, - }, - { - "name": "mark_duplicate", - "description": ( - "Mark this issue as a duplicate of an existing one. " - "Use this when the issue is clearly asking about the same thing as an open issue. " - "This will post a comment pointing to the original, however the issue will remain open for maintainers to address." - ), - "input_schema": { - "type": "object", - "properties": { - "original_issue_number": { - "type": "integer", - "description": "The issue number this is a duplicate of.", - }, - "reason": { - "type": "string", - "description": "Brief explanation of why these issues are duplicates.", - }, - }, - "required": ["original_issue_number", "reason"], - }, - }, - { - "name": "suggest_possible_duplicate", - "description": ( - "Use when an existing issue is related but not clearly the same thing. " - "Posts a comment pointing to the similar issue without closing anything. " - "Triage should still continue normally after calling this." - ), - "input_schema": { - "type": "object", - "properties": { - "related_issue_number": { - "type": "integer", - "description": "The issue number that might be related.", - }, - "reason": { - "type": "string", - "description": "Brief explanation of why these issues seem related.", - }, - }, - "required": ["related_issue_number", "reason"], - }, - }, -] - -# System prompt - -SYSTEM_PROMPT = """You are an issue triage assistant for a GitHub repository. -Given a new issue and a list of existing open issues, you must: - -1. Check whether the new issue is a duplicate of an existing one. - - If it clearly is the same issue, call mark_duplicate and stop — do not label or acknowledge further. - - If it seems related but could be distinct, call suggest_possible_duplicate. That comment - will serve as the acknowledgment too, so do NOT post a separate acknowledgment afterward. -2. Otherwise, classify it by applying appropriate labels - (bug, feature-request, question, documentation, needs-info, good-first-issue). -3. If the issue is missing key info (steps to reproduce for bugs, use case for features, etc.), - post a friendly comment asking for it. -4. If no possible duplicate was flagged, post a short acknowledgment comment so the - author knows their issue was received. Do NOT post comments on administrative issues - such as meeting minutes, roadmaps, etc. - -Keep comments concise and friendly.""" - -# GitHub helpers - -def get_existing_issues(limit: int = LATEST_ISSUES_LIMIT) -> str: - """ - Fetches the most recent open issues (excluding the current one) - and formats them into a string for the prompt. - """ - open_issues = repo.get_issues(state="open") - lines = [] - count = 0 - for existing in open_issues: - if existing.number == issue.number: - continue - lines.append( - f"- #{existing.number}: {existing.title}\n" - f" {(existing.body or '').strip()[:200]}" # truncate long bodies - ) - count += 1 - if count >= limit: - break - return "\n".join(lines) if lines else "(no other open issues)" - - -def apply_label(labels: list[str]) -> str: - existing_label_names = [l.name for l in repo.get_labels()] - for label in labels: - if label not in existing_label_names: - repo.create_label(label, "ededed") - issue.add_to_labels(*labels) - return f"Applied labels: {labels}" - - -def post_comment(body: str) -> str: - issue.create_comment(body) - return "Comment posted." - - -def mark_duplicate(original_issue_number: int, reason: str) -> str: - original = repo.get_issue(original_issue_number) - issue.create_comment( - f"Thanks for the report! This looks like a duplicate of #{original_issue_number} " - f"({original.html_url}).\n\n> {reason}\n\n" - f"Please edit this issue to add any distinguishing details if you believe it's not a duplicate." - ) - issue.add_to_labels("duplicate") - return f"Marked as duplicate of #{original_issue_number}." - - -def suggest_possible_duplicate(related_issue_number: int, reason: str) -> str: - related = repo.get_issue(related_issue_number) - issue.create_comment( - f"Hey! This might be related to #{related_issue_number} " - f"({related.html_url}) — {reason}\n\n" - f"Feel free to check if that one already covers what you're reporting!" - ) - return f"Flagged as possibly related to #{related_issue_number}." - - -# Tool dispatch - -def handle_tool_call(name: str, inputs: dict) -> str: - if name == "apply_label": - result = apply_label(inputs["labels"]) - elif name == "post_comment": - result = post_comment(inputs["body"]) - elif name == "mark_duplicate": - result = mark_duplicate(inputs["original_issue_number"], inputs["reason"]) - elif name == "suggest_possible_duplicate": - result = suggest_possible_duplicate(inputs["related_issue_number"], inputs["reason"]) - else: - result = f"Unknown tool: {name}" - print(f"Tool {name}: {result}") - return result - -# Agentic loop - -def build_initial_message() -> str: - return ( - f"Please triage this new GitHub issue:\n\n" - f"Title: {os.environ['ISSUE_TITLE']}\n" - f"Body:\n{os.environ.get('ISSUE_BODY') or '(no description provided)'}\n\n" - f"---\n" - f"Here are the currently open issues for duplicate detection:\n\n" - f"{get_existing_issues()}" - ) - - -def run_triage_agent(): - messages = [{"role": "user", "content": build_initial_message()}] - - while True: - response = client.messages.create( - model="claude-sonnet-4-20250514", - max_tokens=1024, - system=SYSTEM_PROMPT, - tools=TOOLS, - messages=messages, - ) - - messages.append({"role": "assistant", "content": response.content}) - - if response.stop_reason == "end_turn": - break - - tool_results = [] - for block in response.content: - if block.type != "tool_use": - continue - result = handle_tool_call(block.name, block.input) - tool_results.append({ - "type": "tool_result", - "tool_use_id": block.id, - "content": result, - }) - - messages.append({"role": "user", "content": tool_results}) - - -if __name__ == "__main__": - run_triage_agent() From de2c1ac8a4dc83de86cfa2b0938236a339f77cb3 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Mon, 10 Aug 2026 16:31:41 +0900 Subject: [PATCH 38/47] feat: add token limits to agentic loop, reduce token usage and improve debug output --- scripts/agents/helpers.py | 117 ++++++++++++++++++++++++++++++-------- 1 file changed, 92 insertions(+), 25 deletions(-) diff --git a/scripts/agents/helpers.py b/scripts/agents/helpers.py index bd55b5283..0a8b25e0c 100644 --- a/scripts/agents/helpers.py +++ b/scripts/agents/helpers.py @@ -18,58 +18,125 @@ def _debug_mode_enabled(): return os.environ.get("DEBUG_AI_WORKFLOWS", "").strip().lower() in ("true", "1", "yes") -def run_agent(messages: list, tools: list, handle_tool_call, model: str): +def run_agent( + messages: list, + tools: list, + handle_tool_call, + model: str, + terminal_tools: set | frozenset = frozenset(), + max_turns: int = 10, + max_output_tokens: int = 5000, + token_budget: int = 1000000, +): + """ + Runs the agent loop until the model stops, calls no tools, or calls a tool + listed in `terminal_tools`. Terminal tools end the run immediately, to + prevent further model calls (and wasted tokens). + """ debug = _debug_mode_enabled() total_prompt_tokens = 0 total_completion_tokens = 0 total_tokens = 0 total_cost = 0.0 + truncated = False - while True: + for turn in range(1, max_turns + 1): response = litellm.completion( - model=model, messages=messages, tools=tools, temperature=0 + model=model, + messages=messages, + tools=tools, + temperature=0, + max_tokens=max_tokens, ) + + # Accounting always runs; only the printing is gated on debug. + usage = getattr(response, "usage", None) + prompt_tokens = (getattr(usage, "prompt_tokens", 0) if usage else 0) or 0 + completion_tokens = (getattr(usage, "completion_tokens", 0) if usage else 0) or 0 + tokens = (getattr(usage, "total_tokens", 0) if usage else 0) or 0 + total_prompt_tokens += prompt_tokens + total_completion_tokens += completion_tokens + total_tokens += tokens or (prompt_tokens + completion_tokens) + + try: + total_cost += litellm.completion_cost(completion_response=response) + except Exception: + pass + if debug: - usage = getattr(response, "usage", None) - prompt_tokens = getattr(usage, "prompt_tokens", None) if usage else None - completion_tokens = getattr(usage, "completion_tokens", None) if usage else None - tokens = getattr(usage, "total_tokens", None) if usage else None - if prompt_tokens is not None: - total_prompt_tokens += prompt_tokens - if completion_tokens is not None: - total_completion_tokens += completion_tokens - if tokens is not None: - total_tokens += tokens print( - f"[debug] tokens prompt={prompt_tokens} " - f"completion={completion_tokens} total={tokens}" + f"[debug] turn={turn} tokens prompt={prompt_tokens} " + f"completion={completion_tokens} total={tokens} " + f"running_total={total_tokens}" ) - try: - cost = litellm.completion_cost(completion_response=response) - total_cost += cost - print(f"[debug] estimated cost=${cost:.6f}") - except Exception: - print("[debug] estimated cost=unavailable") - message = response.choices[0].message + + choice = response.choices[0] + message = choice.message + + if choice.finish_reason == "length": + truncated = True + print( + f"[agent] WARNING: output hit max_tokens={max_tokens} on turn {turn}. " + "Any tool call from this turn is likely malformed." + ) + if message.content: print(f"[agent] {message.content}") messages.append(message.model_dump(exclude_none=True)) - if response.choices[0].finish_reason == "stop" or not message.tool_calls: + + if choice.finish_reason == "stop" or not message.tool_calls: break + + finished = False tool_results = [] for tool_call in message.tool_calls: - inputs = json.loads(tool_call.function.arguments) - result = handle_tool_call(tool_call.function.name, inputs) + name = tool_call.function.name + try: + inputs = json.loads(tool_call.function.arguments) + except json.JSONDecodeError as e: + print(f"[agent] Malformed arguments for {name}: {e}") + tool_results.append({ + "role": "tool", + "tool_call_id": tool_call.id, + "content": f"Error: arguments were not valid JSON ({e}). Please retry.", + }) + continue + + result = handle_tool_call(name, inputs) tool_results.append({ "role": "tool", "tool_call_id": tool_call.id, "content": result, }) + if name in terminal_tools: + finished = True + messages.extend(tool_results) + if finished: + print("[agent] Terminal tool called, ending run.") + break + + if token_budget is not None and total_tokens >= token_budget: + print( + f"[agent] Token budget exhausted " + f"({total_tokens} >= {token_budget}), stopping before next call." + ) + break + else: + print(f"[agent] Hit max_turns={max_turns} without finishing.") + if debug: print( f"[debug] summary prompt={total_prompt_tokens} " f"completion={total_completion_tokens} total={total_tokens} " f"estimated_cost=${total_cost:.6f}" ) + + return { + "prompt_tokens": total_prompt_tokens, + "completion_tokens": total_completion_tokens, + "total_tokens": total_tokens, + "estimated_cost": total_cost, + "truncated": truncated, + } From 130c2599d7499a8d8c63a10b8d464a66af8c025e Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Mon, 10 Aug 2026 16:32:09 +0900 Subject: [PATCH 39/47] feat: add token limits to PR and issue processors --- scripts/agents/pr_checker_agent.py | 8 +++++++- scripts/agents/security_review_agent.py | 9 ++++++++- scripts/agents/triage_agent.py | 8 +++++++- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/scripts/agents/pr_checker_agent.py b/scripts/agents/pr_checker_agent.py index ac2742e75..a2e1c265d 100644 --- a/scripts/agents/pr_checker_agent.py +++ b/scripts/agents/pr_checker_agent.py @@ -119,7 +119,13 @@ def run_pr_review_agent(): {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": build_initial_message()}, ] - run_agent(messages, TOOLS, handle_tool_call, MODEL) + stats = run_agent(messages, TOOLS, handle_tool_call, MODEL, + terminal_tools={"post_comment"}, + max_output_tokens=int(os.environ.get("MAX_OUTPUT_TOKENS", 5000)), + token_budget=int(os.environ.get("TOKEN_BUDGET", 1000000)), + ) + if stats["truncated"]: + raise SystemExit("PR review output was truncated: results may be incomplete.") if __name__ == "__main__": diff --git a/scripts/agents/security_review_agent.py b/scripts/agents/security_review_agent.py index 9a6c73723..f5948c1d9 100644 --- a/scripts/agents/security_review_agent.py +++ b/scripts/agents/security_review_agent.py @@ -174,7 +174,14 @@ def run_security_review_agent(): {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": build_initial_message()}, ] - run_agent(messages, TOOLS, handle_tool_call, MODEL) + stats = run_agent(messages, TOOLS, handle_tool_call, MODEL, + terminal_tools={"post_security_review"}, + max_output_tokens=int(os.environ.get("MAX_OUTPUT_TOKENS", 5000)), + token_budget=int(os.environ.get("TOKEN_BUDGET", 1000000)), + ) + + if stats["truncated"]: + raise SystemExit("Security review output was truncated: results may be incomplete.") if __name__ == "__main__": diff --git a/scripts/agents/triage_agent.py b/scripts/agents/triage_agent.py index 366a4dbb6..f7d074dbc 100644 --- a/scripts/agents/triage_agent.py +++ b/scripts/agents/triage_agent.py @@ -218,7 +218,13 @@ def run_triage_agent(): {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": build_initial_message()}, ] - run_agent(messages, TOOLS, handle_tool_call, MODEL) + stats = run_agent(messages, TOOLS, handle_tool_call, MODEL, + terminal_tools={"post_comment"}, + max_output_tokens=int(os.environ.get("MAX_OUTPUT_TOKENS", 5000)), + token_budget=int(os.environ.get("TOKEN_BUDGET", 1000000)), + ) + if stats["truncated"]: + raise SystemExit("Triage output was truncated: results may be incomplete.") if __name__ == "__main__": From 740ef21d7fd8dd14bc8bc162d43880779558ac5a Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Mon, 10 Aug 2026 16:36:48 +0900 Subject: [PATCH 40/47] fix: max_outputs not defined --- scripts/agents/helpers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/agents/helpers.py b/scripts/agents/helpers.py index 0a8b25e0c..78d4987bb 100644 --- a/scripts/agents/helpers.py +++ b/scripts/agents/helpers.py @@ -46,7 +46,7 @@ def run_agent( messages=messages, tools=tools, temperature=0, - max_tokens=max_tokens, + max_tokens=max_output_tokens, ) # Accounting always runs; only the printing is gated on debug. @@ -76,7 +76,7 @@ def run_agent( if choice.finish_reason == "length": truncated = True print( - f"[agent] WARNING: output hit max_tokens={max_tokens} on turn {turn}. " + f"[agent] WARNING: output hit max_tokens={max_output_tokens} on turn {turn}. " "Any tool call from this turn is likely malformed." ) From f85f80bc17672e28f8edfc75e2de9aeb254ec5b6 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Mon, 10 Aug 2026 17:10:48 +0900 Subject: [PATCH 41/47] fix: remove temperature=0 setting to enable thinking model scans --- scripts/agents/helpers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/agents/helpers.py b/scripts/agents/helpers.py index 78d4987bb..0cddb7fff 100644 --- a/scripts/agents/helpers.py +++ b/scripts/agents/helpers.py @@ -45,7 +45,6 @@ def run_agent( model=model, messages=messages, tools=tools, - temperature=0, max_tokens=max_output_tokens, ) From 8567a5026de85da2d3f867ef1b3d35ac5afcfa8a Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Mon, 10 Aug 2026 17:15:04 +0900 Subject: [PATCH 42/47] chore: temporarily increase max output tokens for testing --- scripts/agents/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/agents/helpers.py b/scripts/agents/helpers.py index 0cddb7fff..402770c76 100644 --- a/scripts/agents/helpers.py +++ b/scripts/agents/helpers.py @@ -25,7 +25,7 @@ def run_agent( model: str, terminal_tools: set | frozenset = frozenset(), max_turns: int = 10, - max_output_tokens: int = 5000, + max_output_tokens: int = 500000, token_budget: int = 1000000, ): """ From 8b8d90e9fb59e1c68c6fdee8ef49372cbfee83a6 Mon Sep 17 00:00:00 2001 From: Juan Escalada <97265671+jescalada@users.noreply.github.com> Date: Mon, 10 Aug 2026 17:25:48 +0900 Subject: [PATCH 43/47] chore: add model information to debug output --- scripts/agents/helpers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/agents/helpers.py b/scripts/agents/helpers.py index 402770c76..fe562fac7 100644 --- a/scripts/agents/helpers.py +++ b/scripts/agents/helpers.py @@ -130,6 +130,7 @@ def run_agent( f"[debug] summary prompt={total_prompt_tokens} " f"completion={total_completion_tokens} total={total_tokens} " f"estimated_cost=${total_cost:.6f}" + f"model=${model}" ) return { From 219483ffe151f9228c62d2856a86082b26645771 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Mon, 10 Aug 2026 17:39:12 +0900 Subject: [PATCH 44/47] chore: convert model and debug flag into variables for proper debugging --- .github/workflows/pr-quality-check.yml | 6 +++--- .github/workflows/security-review.yml | 6 +++--- .github/workflows/triage.yml | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/pr-quality-check.yml b/.github/workflows/pr-quality-check.yml index 4bff9acb5..e72a6d568 100644 --- a/.github/workflows/pr-quality-check.yml +++ b/.github/workflows/pr-quality-check.yml @@ -17,13 +17,13 @@ jobs: - name: Run PR quality check agent env: # e.g: "claude-sonnet-4-6", "gpt-4o", etc. - MODEL: ${{ secrets.MODEL }} + MODEL: ${{ vars.MODEL }} + # Enable token/cost logging in job logs + DEBUG_AI_WORKFLOWS: ${{ vars.DEBUG_AI_WORKFLOWS }} # Only API key for the chosen model is required ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - # Enable token/cost logging in job logs - DEBUG_AI_WORKFLOWS: ${{ secrets.DEBUG_AI_WORKFLOWS }} # Obtained automatically by GH Actions AUTHOR_ASSOCIATION: ${{ github.event.pull_request.author_association }} AUTHOR_USERNAME: ${{ github.event.pull_request.user.login }} diff --git a/.github/workflows/security-review.yml b/.github/workflows/security-review.yml index 40e5efacf..5d8b01853 100644 --- a/.github/workflows/security-review.yml +++ b/.github/workflows/security-review.yml @@ -33,13 +33,13 @@ jobs: IGNORED_FILENAMES: package-lock.json,yarn.lock,poetry.lock,Gemfile.lock,Cargo.lock,composer.lock,pnpm-lock.yaml,pip.lock MAX_PATCH_CHARS_PER_FILE: 3000 # e.g: "claude-sonnet-4-6", "gpt-4o", etc. - MODEL: ${{ secrets.MODEL }} + MODEL: ${{ vars.MODEL }} + # Enable token/cost logging in job logs + DEBUG_AI_WORKFLOWS: ${{ vars.DEBUG_AI_WORKFLOWS }} # Only API key for the chosen model is required ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - # Enable token/cost logging in job logs - DEBUG_AI_WORKFLOWS: ${{ secrets.DEBUG_AI_WORKFLOWS }} # Obtained automatically by GH Actions GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} diff --git a/.github/workflows/triage.yml b/.github/workflows/triage.yml index eef1c0f7c..d80ed6699 100644 --- a/.github/workflows/triage.yml +++ b/.github/workflows/triage.yml @@ -19,13 +19,13 @@ jobs: AVAILABLE_LABELS: automation,bug,dependencies,documentation,enhancement,good-first-issue,meeting,needs-info,plugins,protocol,question,security,tech-debt,testing LATEST_ISSUES_LIMIT: 100 # e.g: "claude-sonnet-4-6", "gpt-4o", etc. - MODEL: ${{ secrets.MODEL }} + MODEL: ${{ vars.MODEL }} + # Enable token/cost logging in job logs + DEBUG_AI_WORKFLOWS: ${{ vars.DEBUG_AI_WORKFLOWS }} # Only API key for the chosen model is required ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - # Enable token/cost logging in job logs - DEBUG_AI_WORKFLOWS: ${{ secrets.DEBUG_AI_WORKFLOWS }} # Obtained automatically by GH Actions GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ISSUE_BODY: ${{ github.event.issue.body }} From 3199edd7a42261887c54db8268ebdd80dec72c86 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Mon, 10 Aug 2026 17:45:08 +0900 Subject: [PATCH 45/47] chore: improve debug output --- scripts/agents/helpers.py | 3 +++ scripts/agents/security_review_agent.py | 7 ++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/scripts/agents/helpers.py b/scripts/agents/helpers.py index 402770c76..353b5797e 100644 --- a/scripts/agents/helpers.py +++ b/scripts/agents/helpers.py @@ -130,6 +130,9 @@ def run_agent( f"[debug] summary prompt={total_prompt_tokens} " f"completion={total_completion_tokens} total={total_tokens} " f"estimated_cost=${total_cost:.6f}" + f"model={model}" + f"max_output_tokens={max_output_tokens}" + f"token_budget={token_budget}" ) return { diff --git a/scripts/agents/security_review_agent.py b/scripts/agents/security_review_agent.py index f5948c1d9..c62feb794 100644 --- a/scripts/agents/security_review_agent.py +++ b/scripts/agents/security_review_agent.py @@ -53,9 +53,14 @@ ... (repeat for each finding) -Disclaimer: This review is AI-generated. Please validate the findings before fixing. +**Disclaimer:** This review is AI-generated. Please validate the findings before fixing. + +Re-run by commenting `/security-review` on the PR. """ +if os.environ["DEBUG_AI_WORKFLOWS"]: + SYSTEM_PROMPT += f"\n\n**Model:** {MODEL}" + # GitHub helpers def get_pr_diff() -> str: From be6fa523133c3b70e466fba6aa14daf18d81363d Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Mon, 10 Aug 2026 17:59:15 +0900 Subject: [PATCH 46/47] chore: add token limit env inputs --- .github/workflows/pr-quality-check.yml | 2 ++ .github/workflows/security-review.yml | 2 ++ .github/workflows/triage.yml | 2 ++ scripts/agents/security_review_agent.py | 4 +++- 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr-quality-check.yml b/.github/workflows/pr-quality-check.yml index e72a6d568..c7b4d2526 100644 --- a/.github/workflows/pr-quality-check.yml +++ b/.github/workflows/pr-quality-check.yml @@ -20,6 +20,8 @@ jobs: MODEL: ${{ vars.MODEL }} # Enable token/cost logging in job logs DEBUG_AI_WORKFLOWS: ${{ vars.DEBUG_AI_WORKFLOWS }} + MAX_OUTPUT_TOKENS: ${{ vars.MAX_OUTPUT_TOKENS }} + TOKEN_BUDGET: ${{ vars.TOKEN_BUDGET }} # Only API key for the chosen model is required ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} diff --git a/.github/workflows/security-review.yml b/.github/workflows/security-review.yml index 5d8b01853..dc0619f91 100644 --- a/.github/workflows/security-review.yml +++ b/.github/workflows/security-review.yml @@ -36,6 +36,8 @@ jobs: MODEL: ${{ vars.MODEL }} # Enable token/cost logging in job logs DEBUG_AI_WORKFLOWS: ${{ vars.DEBUG_AI_WORKFLOWS }} + MAX_OUTPUT_TOKENS: ${{ vars.MAX_OUTPUT_TOKENS }} + TOKEN_BUDGET: ${{ vars.TOKEN_BUDGET }} # Only API key for the chosen model is required ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} diff --git a/.github/workflows/triage.yml b/.github/workflows/triage.yml index d80ed6699..a52ea703c 100644 --- a/.github/workflows/triage.yml +++ b/.github/workflows/triage.yml @@ -22,6 +22,8 @@ jobs: MODEL: ${{ vars.MODEL }} # Enable token/cost logging in job logs DEBUG_AI_WORKFLOWS: ${{ vars.DEBUG_AI_WORKFLOWS }} + MAX_OUTPUT_TOKENS: ${{ vars.MAX_OUTPUT_TOKENS }} + TOKEN_BUDGET: ${{ vars.TOKEN_BUDGET }} # Only API key for the chosen model is required ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} diff --git a/scripts/agents/security_review_agent.py b/scripts/agents/security_review_agent.py index c62feb794..31315ec0c 100644 --- a/scripts/agents/security_review_agent.py +++ b/scripts/agents/security_review_agent.py @@ -55,11 +55,13 @@ **Disclaimer:** This review is AI-generated. Please validate the findings before fixing. -Re-run by commenting `/security-review` on the PR. +Re-run by commenting `/security-review` on the PR. """ if os.environ["DEBUG_AI_WORKFLOWS"]: SYSTEM_PROMPT += f"\n\n**Model:** {MODEL}" + SYSTEM_PROMPT += f"\n\n**Max output tokens:** {os.environ.get('MAX_OUTPUT_TOKENS', 5000)}" + SYSTEM_PROMPT += f"\n\n**Token budget:** {os.environ.get('TOKEN_BUDGET', 1000000)}" # GitHub helpers From d567b12dbe82392b43052d61c5314bbad53a85ab Mon Sep 17 00:00:00 2001 From: Juan Escalada <97265671+jescalada@users.noreply.github.com> Date: Mon, 10 Aug 2026 18:09:57 +0900 Subject: [PATCH 47/47] chore: edit SYSTEM_PROMPT for dynamic model reference --- scripts/agents/security_review_agent.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/agents/security_review_agent.py b/scripts/agents/security_review_agent.py index c62feb794..d502e7af8 100644 --- a/scripts/agents/security_review_agent.py +++ b/scripts/agents/security_review_agent.py @@ -28,7 +28,7 @@ # System prompt -SYSTEM_PROMPT = """You are a security analysis assistant for a GitHub repository. +SYSTEM_PROMPT = f"""You are a security analysis assistant for a GitHub repository. You are given a pull request diff and must identify potential security issues. Flag only: hardcoded secrets or credentials, injection vulnerabilities (SQL, shell, template), insecure cryptography or hashing, unsafe deserialization, path traversal, missing input validation on user-controlled data, known-vulnerable dependency versions, overly permissive file or network access. @@ -55,7 +55,7 @@ **Disclaimer:** This review is AI-generated. Please validate the findings before fixing. -Re-run by commenting `/security-review` on the PR. +Reviewed by {MODEL}. Re-run by commenting `/security-review` on this PR. """ if os.environ["DEBUG_AI_WORKFLOWS"]: