From 13e5cdb3d2742abae29b9e6b31b4892e91b49cb8 Mon Sep 17 00:00:00 2001 From: Changyong Gong Date: Thu, 2 Jul 2026 10:57:09 +0800 Subject: [PATCH] Enforce IssueLens labeling instructions --- .github/agents/issuelens.agent.md | 42 +++----- .github/skills/label-issue/SKILL.md | 37 ++++--- .../skills/label-issue/scripts/label_issue.py | 97 ++++++++++++------- .github/workflows/issueLens-run.yml | 6 +- 4 files changed, 94 insertions(+), 88 deletions(-) diff --git a/.github/agents/issuelens.agent.md b/.github/agents/issuelens.agent.md index af31c7b1..541e28f1 100644 --- a/.github/agents/issuelens.agent.md +++ b/.github/agents/issuelens.agent.md @@ -1,13 +1,15 @@ --- name: issuelens description: "An agent specialized in Java Tooling (IDE, extensions, build tools, language servers) area, responsible for commenting and labeling on GitHub issues. Use when: triaging issues, labeling issues, analyzing Java tooling bugs." -tools: ['github/*', 'execute', 'read', 'search', 'web', 'javatooling-search/*'] +tools: ['execute', 'read', 'search', 'web', 'javatooling-search/*'] --- # IssueLens — Java Tooling Issue Triage Agent You are an experienced developer specialized in Java tooling (IDEs, extensions, build tools, language servers), responsible for commenting and labeling on GitHub issues. +For GitHub issue, comment, label, and repository-content operations, use the `gh` CLI with `GH_TOKEN`/`GITHUB_TOKEN`. Do not use GitHub MCP tools. If `gh` fails, stop and report the error instead of falling back to GitHub MCP tools or ad hoc API scripts. + > **Before starting any step**, fetch the full issue content using available tools. You **must** retrieve all of the following before proceeding: > - **Title** > - **Body** @@ -56,11 +58,12 @@ The comment must follow this exact structure: **Outro** (always last): > The team will respond to your issue shortly. I hope these suggestions are helpful in the meantime. If this comment helped you, please give it a 👍. If the suggestion was not helpful or incorrect, please give it a 👎. Your feedback helps us improve! -Post the comment on the issue using `github/add_issue_comment`. If encounter errors with MCP tools, create Python code to post the comment using GitHub API as a fallback, you can get the necessary token from the environment variables `GITHUB_TOKEN`, `GITHUB_ACCESS_TOKEN`, or `GITHUB_PAT`. +Post the comment on the issue using `gh issue comment` with the token from `GH_TOKEN`/`GITHUB_TOKEN`. If `gh` fails, stop and report the error. ## Step 2: Label the Issue Use the `label-issue` skill to classify and apply labels to the issue. +Before applying any non-lifecycle label, load and follow `.github/llms.md` from the target repository. Do not apply labels directly with `gh issue edit --add-label`; use the `label-issue` skill's `scripts/label_issue.py` helper so labels are validated against `.github/llms.md`. ## Step 3: Detect Duplicate Issues @@ -70,38 +73,19 @@ Using the search results already obtained in Step 1.2, determine whether the cur - Exclude the current issue itself from the results. - If a duplicate is found: 1. Apply the `duplicate` label to the issue. - 2. Close the issue as a duplicate by executing the following Python script inline, substituting the actual values: - -```python -import os, sys, requests - -def close_as_duplicate(owner, repo, issue_number): - token = ( - os.environ.get("GITHUB_TOKEN") - or os.environ.get("GITHUB_ACCESS_TOKEN") - or os.environ.get("GITHUB_PAT") - ) - if not token: - print("❌ GitHub token not found. Set GITHUB_TOKEN, GITHUB_ACCESS_TOKEN, or GITHUB_PAT.", file=sys.stderr) - sys.exit(1) - headers = { - "Authorization": f"Bearer {token}", - "Accept": "application/vnd.github+json", - "X-GitHub-Api-Version": "2022-11-28", - } - patch_url = f"https://api.github.com/repos/{owner}/{repo}/issues/{issue_number}" - response = requests.patch(patch_url, headers=headers, json={"state": "closed", "state_reason": "duplicate"}) - response.raise_for_status() - print(f"✅ Issue #{issue_number} closed as duplicate") - -# Fill in values before running: -close_as_duplicate("", "", ) + 2. Close the issue with `gh issue close`: + +```bash +gh issue close --repo / --reason "not planned" ``` +GitHub CLI does not expose a `duplicate` close reason for issues; the `duplicate` label is the authoritative duplicate marker. + ## Step 4: Apply the `ai-triaged` Label After completing all triage steps, always apply the `ai-triaged` label to the issue to indicate it has been processed by the AI agent. +This lifecycle label is allowed even when it is not listed in `.github/llms.md`. ## Notes -- Use `gh` CLI as a fallback if you encounter issues with MCP tools. +- Use `gh` CLI for all GitHub operations. - Always use available tools to complete each step before moving to the next. diff --git a/.github/skills/label-issue/SKILL.md b/.github/skills/label-issue/SKILL.md index 200cd33c..dec4484f 100644 --- a/.github/skills/label-issue/SKILL.md +++ b/.github/skills/label-issue/SKILL.md @@ -14,7 +14,7 @@ This skill analyzes GitHub issue content (title, body, comments) and applies app ## Workflow 1. **Input**: Receive issue URL or issue number with repository (owner/repo) -2. **Fetch labeling instructions**: Read `.github/llms.md` from the repository +2. **Fetch labeling instructions**: Read `.github/llms.md` from the checked-out repository before deciding labels 3. **Fetch issue**: Get issue details (title, body, existing labels) 4. **Analyze issue**: Match issue content against labeling rules 5. **Determine labels**: Select appropriate labels based on: @@ -27,18 +27,18 @@ This skill analyzes GitHub issue content (title, body, comments) and applies app ## Reading Labeling Instructions -Fetch `.github/llms.md` from the target repository using GitHub MCP tools. The file should define: +Fetch `.github/llms.md` from the checked-out target repository. Do not use GitHub MCP tools. If repository content must be fetched remotely, use `gh api` with `GH_TOKEN`/`GITHUB_TOKEN`. The file should define: - **Available labels**: List of valid labels with descriptions - **Labeling rules**: Criteria for when to apply each label - **Keywords mapping**: Keywords that trigger specific labels **Only apply labels explicitly defined in this document. Do not apply any other labels.** +The only exceptions are IssueLens lifecycle labels that are applied by the owning agent, such as `ai-triaged` and `duplicate`. If `.github/llms.md` is not found: -1. Fetch the list of labels defined in the target repository using `github/list_labels` -2. Create a brief summary of available labels based on their names and descriptions -3. Use the summary to determine which labels best match the issue content +1. Stop and report that labeling cannot continue because repository labeling instructions are missing. +2. Do not fall back to applying labels from the repository label list alone. ## Issue Analysis @@ -50,23 +50,21 @@ Analyze issue content to determine appropriate labels by: ## Applying Labels -Run the bundled Python script to add labels: +Run the bundled Python script from the repository root to validate labels against `.github/llms.md` and add them via `gh` CLI: ```bash -# Install dependency -pip install requests - # Add labels to an issue -python scripts/label_issue.py +python .github/skills/label-issue/scripts/label_issue.py -# Example: add bug and priority:high labels -python scripts/label_issue.py microsoft vscode 123 "bug,priority:high" +# Example: add bug and ai-triaged labels +python .github/skills/label-issue/scripts/label_issue.py microsoft vscode 123 "bug,ai-triaged" -# Example: add multiple area labels -python scripts/label_issue.py microsoft vscode 123 "bug,area:ui,area:api" +# Example: add needs more info and ai-triaged labels +python .github/skills/label-issue/scripts/label_issue.py microsoft vscode 123 "needs more info,ai-triaged" ``` -The script [scripts/label_issue.py](scripts/label_issue.py) handles the GitHub API call. +The script [scripts/label_issue.py](scripts/label_issue.py) handles the `gh issue edit` call. +Do not use `gh issue edit --add-label` directly for IssueLens labeling because that bypasses `.github/llms.md` validation. ## Example Commands @@ -103,12 +101,11 @@ Report the labeling decision with: The skill requires: -1. **GITHUB_ACCESS_TOKEN** or **GITHUB_TOKEN** environment variable with `repo` scope -2. **.github/llms.md** in target repository (optional but recommended) +1. **GH_TOKEN** or **GITHUB_TOKEN** environment variable with `issues: write` permission +2. **.github/llms.md** in target repository ## Fallback Behavior If labeling instructions are not found: -1. Use default type detection rules -2. Skip priority and area labels -3. Report that default rules were used +1. Do not apply labels. +2. Report that `.github/llms.md` is required for labeling. diff --git a/.github/skills/label-issue/scripts/label_issue.py b/.github/skills/label-issue/scripts/label_issue.py index b87756c0..57a5fca2 100644 --- a/.github/skills/label-issue/scripts/label_issue.py +++ b/.github/skills/label-issue/scripts/label_issue.py @@ -1,43 +1,61 @@ #!/usr/bin/env python3 """ -Add labels to GitHub issues. +Add labels to GitHub issues with gh CLI after validating them against .github/llms.md. Usage: - python label_issue.py + python label_issue.py [--dry-run] Environment Variables: - GITHUB_ACCESS_TOKEN or GITHUB_PAT: GitHub personal access token with repo scope + GH_TOKEN or GITHUB_TOKEN: GitHub token with issues: write permission """ -import os import sys import argparse -import requests +import re +import subprocess +from pathlib import Path -def get_github_token() -> str: - """Get GitHub token from environment variables.""" - token = os.environ.get("GITHUB_ACCESS_TOKEN") or os.environ.get("GITHUB_TOKEN") or os.environ.get("GITHUB_PAT") - if not token: - raise ValueError( - "GitHub token not found. Set GITHUB_ACCESS_TOKEN, GITHUB_TOKEN, or GITHUB_PAT environment variable." - ) - return token +LLMS_PATH = Path(".github/llms.md") +LIFECYCLE_LABELS = {"ai-triaged", "duplicate"} -def add_labels( - owner: str, repo: str, issue_number: int, labels: list[str], token: str -) -> dict: - """Add labels to a GitHub issue.""" - url = f"https://api.github.com/repos/{owner}/{repo}/issues/{issue_number}/labels" - headers = { - "Authorization": f"Bearer {token}", - "Accept": "application/vnd.github+json", - "X-GitHub-Api-Version": "2022-11-28", - } - response = requests.post(url, headers=headers, json={"labels": labels}) - response.raise_for_status() - return response.json() +def add_labels(owner: str, repo: str, issue_number: int, labels: list[str]) -> None: + """Add labels to a GitHub issue using gh CLI.""" + subprocess.run( + [ + "gh", + "issue", + "edit", + str(issue_number), + "--repo", + f"{owner}/{repo}", + "--add-label", + ",".join(labels), + ], + check=True, + ) + + +def load_allowed_labels(llms_path: Path = LLMS_PATH) -> set[str]: + """Load labels explicitly defined in .github/llms.md.""" + if not llms_path.exists(): + raise FileNotFoundError(f"Required labeling instructions not found: {llms_path}") + + content = llms_path.read_text(encoding="utf-8") + labels = set(re.findall(r"^-\s+`([^`]+)`\s*:", content, flags=re.MULTILINE)) + labels.update(LIFECYCLE_LABELS) + return labels + + +def validate_labels(labels: list[str], allowed_labels: set[str]) -> None: + invalid_labels = [label for label in labels if label not in allowed_labels] + if invalid_labels: + allowed = ", ".join(sorted(allowed_labels)) + invalid = ", ".join(invalid_labels) + raise ValueError( + f"Labels not allowed by .github/llms.md: {invalid}. Allowed labels: {allowed}" + ) def main(): @@ -48,7 +66,10 @@ def main(): parser.add_argument("repo", help="Repository name (e.g., 'vscode')") parser.add_argument("issue_number", type=int, help="Issue number to label") parser.add_argument( - "labels", help="Comma-separated list of labels (e.g., 'bug,priority:high')" + "labels", help="Comma-separated list of labels (e.g., 'bug,ai-triaged')" + ) + parser.add_argument( + "--dry-run", action="store_true", help="Validate labels without calling the GitHub API" ) args = parser.parse_args() @@ -59,18 +80,22 @@ def main(): sys.exit(1) try: - token = get_github_token() - result = add_labels(args.owner, args.repo, args.issue_number, labels, token) - applied_labels = [label["name"] for label in result] - print(f"✅ Labels added to issue #{args.issue_number}: {', '.join(applied_labels)}") + allowed_labels = load_allowed_labels() + validate_labels(labels, allowed_labels) + if args.dry_run: + print(f"✅ Labels valid for issue #{args.issue_number}: {', '.join(labels)}") + sys.exit(0) + add_labels(args.owner, args.repo, args.issue_number, labels) + print(f"✅ Labels added to issue #{args.issue_number}: {', '.join(labels)}") sys.exit(0) + except FileNotFoundError as e: + print(f"❌ Labeling instructions error: {e}", file=sys.stderr) + sys.exit(1) except ValueError as e: - print(f"❌ Configuration error: {e}", file=sys.stderr) + print(f"❌ Label validation error: {e}", file=sys.stderr) sys.exit(1) - except requests.HTTPError as e: - print(f"❌ GitHub API error: {e}", file=sys.stderr) - if e.response is not None: - print(f" Response: {e.response.text}", file=sys.stderr) + except subprocess.CalledProcessError as e: + print(f"❌ gh CLI error: {e}", file=sys.stderr) sys.exit(1) diff --git a/.github/workflows/issueLens-run.yml b/.github/workflows/issueLens-run.yml index 2beedbd7..4575feb6 100644 --- a/.github/workflows/issueLens-run.yml +++ b/.github/workflows/issueLens-run.yml @@ -45,7 +45,7 @@ jobs: echo '{"mcpServers": {"javatooling-search": {"type": "http", "url": "${{ secrets.JAVATOOLING_INDEX_URL }}"}}}' > mcp-config.json # Run copilot CLI with issuelens custom agent - output=$(copilot --agent=issuelens --allow-all --enable-all-github-mcp-tools --no-custom-instructions --no-ask-user --additional-mcp-config @mcp-config.json --prompt "triage issue #${{ steps.issue.outputs.number }} for repo \"${{ github.repository }}\"" 2>&1 || true) + output=$(copilot --agent=issuelens --allow-all --no-ask-user --additional-mcp-config @mcp-config.json --prompt "triage issue #${{ steps.issue.outputs.number }} for repo \"${{ github.repository }}\"" 2>&1 || true) echo "Raw output:" echo "$output" @@ -53,9 +53,9 @@ jobs: # Save to file for artifact upload echo "$output" > triage-output.txt env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ github.token }} COPILOT_GITHUB_TOKEN: ${{ secrets.PAT }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ github.token }} JAVATOOLING_INDEX_URL: ${{ secrets.JAVATOOLING_INDEX_URL }} MAILING_URL: ${{ secrets.MAILING_URL }} REPORT_RECIPIENTS: ${{ secrets.REPORT_RECIPIENTS }}