mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-26 07:24:18 +02:00
293 lines
18 KiB
YAML
293 lines
18 KiB
YAML
name: Marvin Label Triage
|
|
# Automatically triage GitHub issues and PRs using Marvin
|
|
|
|
on:
|
|
issues:
|
|
types: [opened]
|
|
pull_request_target:
|
|
types: [opened]
|
|
workflow_dispatch:
|
|
inputs:
|
|
issue_number:
|
|
description: "Issue or PR number to triage"
|
|
required: true
|
|
type: string
|
|
|
|
concurrency:
|
|
group: triage-${{ github.event.issue.number || github.event.pull_request.number || inputs.issue_number }}
|
|
cancel-in-progress: false
|
|
|
|
jobs:
|
|
label-issue-or-pr:
|
|
if: github.actor != 'dependabot[bot]'
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 10
|
|
permissions:
|
|
contents: read
|
|
issues: write
|
|
pull-requests: write
|
|
|
|
# TEMPORARY PIN — remove once upstream ships a fix.
|
|
#
|
|
# Claude Code 2.1.216 regressed the sandbox that claude-code-action wraps
|
|
# every Bash call in when `allowed_non_write_users` is set: the mountpoint
|
|
# walk fails closed, so every command — down to `true` — dies with
|
|
# `bwrap: Can't create file at /home/.mcp.json: Permission denied`.
|
|
# Marvin still reads the issue and picks correct labels, then cannot run
|
|
# the helper that applies them, so triage silently applied zero labels
|
|
# from 2026-07-20 onward while every run reported success.
|
|
#
|
|
# 2.1.215 is the last release without the regression.
|
|
# https://github.com/anthropics/claude-code/issues/79997
|
|
# https://github.com/anthropics/claude-code-action/issues/1547
|
|
env:
|
|
PINNED_CLAUDE_CODE_VERSION: "2.1.215"
|
|
|
|
steps:
|
|
- name: Checkout base repository
|
|
uses: actions/checkout@v7
|
|
with:
|
|
repository: ${{ github.repository }}
|
|
ref: ${{ github.event.repository.default_branch }}
|
|
|
|
- name: Generate Marvin App token
|
|
id: marvin-token
|
|
uses: actions/create-github-app-token@v3
|
|
with:
|
|
app-id: ${{ secrets.MARVIN_APP_ID }}
|
|
private-key: ${{ secrets.MARVIN_APP_PRIVATE_KEY }}
|
|
owner: PrefectHQ
|
|
|
|
- name: Set triage prompt
|
|
id: triage-prompt
|
|
run: |
|
|
cat >> $GITHUB_OUTPUT << 'EOF'
|
|
PROMPT<<PROMPT_END
|
|
You're an issue triage assistant for FastMCP, a Python framework for building Model Context Protocol servers and clients. Your task is to analyze issues/PRs and apply appropriate labels.
|
|
|
|
IMPORTANT: Your primary action should be to apply labels using the locked-down helper `.github/scripts/triage-label.sh`. DO NOT post comments EXCEPT when applying the too-long label (see below).
|
|
|
|
CRITICAL — LABEL MECHANICS:
|
|
- Apply labels ONLY through the helper, which adds or removes repository labels on THIS issue/PR. It already knows the target repo and number (from the workflow environment) — you never pass them:
|
|
add: `bash .github/scripts/triage-label.sh add "label1" "label2"`
|
|
remove: `bash .github/scripts/triage-label.sh remove "label1"`
|
|
- The helper uses the additive REST labels endpoint, so it works for both issues and PRs and never clobbers labels applied by other workflows — notably the Require Issue Link workflow's `missing-issue-link` control label, which must survive or an auto-closed PR won't reopen when its author is assigned.
|
|
- The helper is your ONLY GitHub write access. Do NOT use raw `gh api`, `gh issue edit`, `gh pr edit`, or any other mutation — they are not available to you.
|
|
- Only apply labels that exist in the repository (from `gh label list` in step 1). Never invent labels.
|
|
- Use `remove` only to correct a label you believe is wrong, and never remove the control labels `missing-issue-link`, `bypass-issue-check`, or `trusted-contributor`.
|
|
|
|
Issue/PR Information:
|
|
- REPO: ${{ github.repository }}
|
|
- NUMBER: ${{ github.event.issue.number || github.event.pull_request.number || inputs.issue_number }}
|
|
- TYPE: ${{ github.event.issue && 'issue' || (github.event.pull_request && 'pull_request') || 'unknown' }}
|
|
|
|
TRIAGE PROCESS:
|
|
|
|
1. Get available labels:
|
|
Run: `gh label list`
|
|
|
|
2. Retrieve issue/PR details using GitHub tools:
|
|
- mcp__github__get_issue: Get the issue/PR details
|
|
- mcp__github__get_issue_comments: Read any discussion
|
|
- If the issue/PR mentions other issues (e.g., "fixes #123", "related to #456"), use mcp__github__get_issue to read those linked issues for additional context
|
|
|
|
3. Analyze and apply labels based on these guidelines:
|
|
|
|
CORE CATEGORIES (apply EXACTLY ONE - these are mutually exclusive; skip if applying too-long):
|
|
- bug: Reports of broken functionality OR PRs that fix bugs
|
|
- enhancement: New functions/endpoints, improvements to existing features, internal tooling, workflow improvements, minor new capabilities
|
|
- feature: ONLY for major headline functionality worthy of a blog post announcement (2-4 per release, never for issues)
|
|
- documentation: Primary change is to user-facing docs, examples, or guides
|
|
|
|
SPECIAL DOCUMENTATION RULES:
|
|
- DO NOT apply "documentation" label if PR only updates auto-generated SDK docs (docs/python-sdk/**)
|
|
- DO apply "documentation" label for significant user-facing documentation changes (guides, examples, API docs)
|
|
- Auto-generated docs updates should get appropriate category label (enhancement, bug, etc.) based on the underlying code changes
|
|
|
|
FEATURE vs ENHANCEMENT guidance:
|
|
- feature: Major systems like new auth systems, MCP composition, proxying MCP servers, major CLI commands that transform workflows
|
|
- enhancement: New functions/endpoints, internal workflows, CI improvements, developer tooling, refactoring, utilities, typical new CLI commands
|
|
- If unsure between feature/enhancement, choose enhancement
|
|
|
|
Note: If a PR fixes a bug, label it "bug" not "enhancement"
|
|
|
|
SPECIAL CATEGORY (can be combined with above):
|
|
- breaking change: Changes that break backward compatibility (in addition to core category)
|
|
|
|
PRIORITY (apply if clearly evident):
|
|
- high-priority: Critical bugs affecting many users, security issues, or blocking core functionality
|
|
- low-priority: Edge cases, nice-to-have improvements, or cosmetic issues
|
|
- Default to no priority label if unclear
|
|
|
|
STATUS (apply if applicable):
|
|
- needs more info: Issue lacks reproduction steps, error messages, or clear description
|
|
- invalid: Spam, completely off-topic, or nonsensical (often LLM-generated)
|
|
- too-long: Apply when an issue or PR doesn't conform to CONTRIBUTING.md. Issues should be a short problem description, an MRE, and expected vs. actual behavior — not a design document. PRs should have a focused description of the change — not a report. We don't need proposed solutions or design alternatives (the issue should describe the problem and let maintainers architect the fix), summaries of what tests cover, explanations of code we can read ourselves, or speculative root-cause analysis. Common LLM failure modes to watch for: verbose "diagnostic" writeups, large proposed patches in issue bodies, multi-section reports restating what's visible in the diff, numbered lists of possible approaches or solutions, "suggested" schemas/shapes/APIs, generic analysis that doesn't reference specific code, and "Notes" sections. But these are heuristics, not rules — a complex PR may legitimately need more context, and a brief submission can still be low-quality. Judge by whether the content helps a reviewer or just adds noise. When applying too-long, still apply the core category and area labels — too-long is a format signal, not a replacement for categorization. Issues still need to be findable by category.
|
|
|
|
WHEN APPLYING too-long: After labeling, post a brief comment using mcp__github__add_issue_comment:
|
|
"Thanks for the report. This issue goes beyond what our contributor guidelines ask for — we just need a short problem description and an MRE. Please see our [contributing guidelines](https://github.com/PrefectHQ/fastmcp/blob/main/CONTRIBUTING.md) and condense this issue. We'll triage it once it's trimmed down."
|
|
Use this exact text (or very close to it). Do not editorialize or add details.
|
|
|
|
AREA LABELS (apply ONLY when thematically central to the issue):
|
|
- cli: Issues primarily about FastMCP CLI commands (run, dev, install)
|
|
- client: Issues primarily about the Client SDK or client-side functionality
|
|
- server: Issues primarily about FastMCP server implementation
|
|
- auth: Authentication is the main concern (Bearer, JWT, OAuth, WorkOS)
|
|
- openapi: OpenAPI integration/parsing is the primary topic
|
|
- http: HTTP transport or networking is the main issue
|
|
- contrib: Specifically about community contributions in fastmcp_slim/fastmcp/contrib/
|
|
- tests: Issues primarily about testing infrastructure, CI/CD workflows, or test coverage
|
|
- security: Apply ONLY when the issue/PR addresses an exploitable vulnerability or hardens against one. Examples: SSRF, LFI, path traversal, injection, auth bypass allowing unauthorized access, scope escalation, open redirects. Do NOT apply for ordinary auth bugs (wrong scopes returned, token refresh logic, OAuth flow correctness) unless an attacker could exploit the bug to bypass access controls or escalate privileges. The key question: "Could a malicious actor exploit this?" If the answer is just "it breaks for legitimate users," that's a bug, not a security issue.
|
|
|
|
LABELING PRINCIPLES:
|
|
- Precision over recall: a missing label is a minor inconvenience; a wrong label sends the wrong people to the wrong issue. When in doubt, don't apply.
|
|
- Don't apply area labels just because a file in that area is mentioned — the issue must be PRIMARILY about that area.
|
|
- Apply 2-5 labels total typically (category + maybe priority + maybe 1-2 areas).
|
|
- For ambiguous cases (bug vs enhancement, which area label), prefer the more conservative choice or omit the uncertain label entirely.
|
|
|
|
META LABELS (rarely needed for issues):
|
|
- dependencies: Only for dependabot PRs or issues specifically about package updates
|
|
- DON'T MERGE: Only if PR author explicitly states it's not ready
|
|
|
|
4. Apply selected labels:
|
|
Add them with `bash .github/scripts/triage-label.sh add "label1" "label2"`.
|
|
DO NOT post any comments unless applying too-long (see above)
|
|
PROMPT_END
|
|
EOF
|
|
|
|
- name: Clean up stale Claude locks
|
|
run: rm -rf ~/.claude/.locks ~/.local/state/claude/locks || true
|
|
|
|
# Mirrors how the action installs Claude Code itself, minus the version
|
|
# it hardcodes. Passing path_to_claude_code_executable makes the action
|
|
# skip its own install and use this build.
|
|
- name: Install pinned Claude Code
|
|
id: pin-claude
|
|
run: |
|
|
curl -fsSL https://claude.ai/install.sh | bash -s -- "$PINNED_CLAUDE_CODE_VERSION"
|
|
echo "path=$HOME/.local/bin/claude" >> "$GITHUB_OUTPUT"
|
|
"$HOME/.local/bin/claude" --version
|
|
|
|
- name: Run Marvin for Issue Triage
|
|
id: marvin
|
|
uses: anthropics/claude-code-action@v1
|
|
with:
|
|
path_to_claude_code_executable: ${{ steps.pin-claude.outputs.path }}
|
|
github_token: ${{ steps.marvin-token.outputs.token }}
|
|
bot_name: "Marvin Context Protocol"
|
|
prompt: ${{ steps.triage-prompt.outputs.PROMPT }}
|
|
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY_FOR_CI }}
|
|
allowed_non_write_users: "*"
|
|
allowed_bots: "marvin-context-protocol"
|
|
claude_args: |
|
|
--allowedTools "Bash(gh label list:*)","Bash(bash .github/scripts/triage-label.sh:*)",mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__add_issue_comment,mcp__github__get_pull_request,mcp__github__get_pull_request_files
|
|
settings: |
|
|
{
|
|
"model": "claude-sonnet-5",
|
|
"env": {
|
|
"GH_TOKEN": "${{ steps.marvin-token.outputs.token }}",
|
|
"TRIAGE_REPO": "${{ github.repository }}",
|
|
"TRIAGE_NUMBER": "${{ github.event.issue.number || github.event.pull_request.number || inputs.issue_number }}"
|
|
}
|
|
}
|
|
|
|
# Triage is fire-and-forget: nobody watches a green run, so a broken
|
|
# allowlist has to fail the job or it goes unnoticed indefinitely — a
|
|
# mangled pattern silently produced zero labels across a dozen PRs
|
|
# because the run still reported success.
|
|
#
|
|
# Only denials of commands we MEANT to grant indicate that breakage. An
|
|
# agent reaching for something never on the allowlist (falling back to
|
|
# `gh issue view` when the API is down, say) is behaving normally, and
|
|
# failing on that would cry wolf during every GitHub incident.
|
|
- name: Fail if Marvin could not run its tools
|
|
if: always() && steps.marvin.conclusion != 'skipped'
|
|
env:
|
|
EXECUTION_FILE: ${{ steps.marvin.outputs.execution_file }}
|
|
run: |
|
|
file="${EXECUTION_FILE:-}"
|
|
if [[ -z "$file" || ! -s "$file" ]]; then
|
|
file="${RUNNER_TEMP}/claude-execution-output.json"
|
|
fi
|
|
# A missing or empty log means we cannot tell a clean run from a
|
|
# blocked one, which is the exact failure this step exists to catch.
|
|
if [[ ! -s "$file" ]]; then
|
|
echo "::error::No Marvin execution log found; cannot verify tool permissions."
|
|
exit 1
|
|
fi
|
|
|
|
# The persisted log carries a `permission_denials` array on each
|
|
# `type: result` entry; the `permission_denials_count` scalar only
|
|
# appears in the action's condensed stdout summary, never on disk.
|
|
# Anchor to result entries rather than recursing with `..`, which
|
|
# descends into each denial's `tool_input` and double-counts any
|
|
# denied command that happens to mention the field name.
|
|
if ! summary=$(jq -sr '
|
|
[ .[] | if type == "array" then .[] else . end ]
|
|
| map(select(type == "object" and .type == "result"))
|
|
| map(.permission_denials // []) | flatten
|
|
| map(.tool_input.command // "")
|
|
| { total: length,
|
|
granted: map(select(
|
|
startswith("gh label list")
|
|
or startswith("bash .github/scripts/triage-label.sh")
|
|
))
|
|
}
|
|
| "\(.total)\t\(.granted | length)\t\(.granted | join(" | "))"
|
|
' "$file"); then
|
|
echo "::error::Could not parse Marvin execution log ($file)."
|
|
exit 1
|
|
fi
|
|
IFS=$'\t' read -r total granted commands <<<"$summary"
|
|
echo "Denied tool calls: $total (of which allowlisted: $granted)"
|
|
|
|
if [[ "$granted" -gt 0 ]]; then
|
|
echo "::error::Marvin was denied $granted call(s) to tools this workflow grants, so it could not apply labels: ${commands}. The --allowedTools value is not reaching the permission matcher intact — claude_args is lexed with shell-quote, so any Bash(...) pattern containing a space must be quoted or it is split into fragments."
|
|
exit 1
|
|
fi
|
|
if [[ "$total" -gt 0 ]]; then
|
|
echo "::notice::Marvin was denied $total call(s), none of them to tools this workflow grants. That is expected when it probes for a tool we deliberately withhold; the allowlist is intact."
|
|
fi
|
|
|
|
# A granted tool can also fail *after* the permission check, which the
|
|
# denial count above cannot see. Claude Code 2.1.216 did exactly that:
|
|
# the sandbox refused to build and every Bash call — including the
|
|
# labeling helper — exited 1 with `bwrap: ...`, while the run stayed
|
|
# green. Correlate results back to their Bash tool_use rather than
|
|
# grepping the whole log, so an issue body quoting a sandbox error
|
|
# cannot fail an otherwise healthy run.
|
|
if ! sandbox=$(jq -sr '
|
|
[ .[] | if type == "array" then .[] else . end ]
|
|
| map(select(type == "object" and (.type == "assistant" or .type == "user")))
|
|
| map(.message.content // []) | flatten
|
|
| map(select(type == "object"))
|
|
| . as $blocks
|
|
| ( $blocks
|
|
| map(select(.type == "tool_use" and .name == "Bash"))
|
|
| map(.id) ) as $bash
|
|
| $blocks
|
|
| map(select(.type == "tool_result" and (.tool_use_id as $i | $bash | index($i))))
|
|
| map(.content | tostring)
|
|
| map(select(test("bwrap:|Failed to (start|create) sandbox")))
|
|
| "\(length)\t\(.[0] // "" | gsub("[\t\n]"; " ") | .[0:200])"
|
|
' "$file"); then
|
|
echo "::error::Could not scan Marvin execution log for sandbox failures ($file)."
|
|
exit 1
|
|
fi
|
|
IFS=$'\t' read -r sandbox_failures sandbox_sample <<<"$sandbox"
|
|
|
|
if [[ "$sandbox_failures" -gt 0 ]]; then
|
|
echo "::error::Marvin's Bash tool failed $sandbox_failures time(s) inside the action's subprocess sandbox, so it could not apply labels: ${sandbox_sample}. This is an environment failure, not a prompt or allowlist problem — check whether the pinned Claude Code version (${PINNED_CLAUDE_CODE_VERSION}) still avoids the upstream sandbox regression."
|
|
exit 1
|
|
fi
|
|
|
|
- name: Upload Marvin execution log
|
|
if: always() && steps.marvin.conclusion != 'skipped'
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: marvin-triage-execution-log
|
|
path: |
|
|
${{ steps.marvin.outputs.execution_file }}
|
|
${{ runner.temp }}/claude-execution-output.json
|
|
if-no-files-found: ignore
|
|
retention-days: 14
|