From dd90f35cabb1632a47be711f92978c69e4453529 Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Tue, 5 May 2026 12:22:10 -0700 Subject: [PATCH 01/18] initial vnext changes, pyright skill --- .github/skills/fix-pyright/SKILL.md | 197 +++++++++ doc/eng_sys_checks.md | 9 + .../gh_tools/vnext_issue_creator.py | 247 +++++++++++- .../tests/test_vnext_auto_fix.py | 374 ++++++++++++++++++ 4 files changed, 823 insertions(+), 4 deletions(-) create mode 100644 .github/skills/fix-pyright/SKILL.md create mode 100644 eng/tools/azure-sdk-tools/tests/test_vnext_auto_fix.py diff --git a/.github/skills/fix-pyright/SKILL.md b/.github/skills/fix-pyright/SKILL.md new file mode 100644 index 000000000000..70fffdec86f7 --- /dev/null +++ b/.github/skills/fix-pyright/SKILL.md @@ -0,0 +1,197 @@ +--- +name: fix-pyright +description: Automatically fix pyright type checking issues in any Azure SDK for Python package following Azure SDK Python patterns. +--- + +# Fix Pyright Issues Skill + +This skill automatically fixes pyright type checking errors in any Azure SDK for Python package by analyzing existing code patterns and applying fixes with 100% confidence. + +## Overview + +Intelligently fixes pyright issues by: +1. Getting the package path or GitHub issue URL from the user +2. Reading and analyzing the issue details (if issue URL provided) +3. Setting up or using existing virtual environment +4. Installing required dependencies +5. Running pyright on the package +6. Analyzing the pyright output to identify type errors +7. Searching codebase for existing type annotation patterns +8. Applying fixes only with 100% confidence +9. Re-running pyright to verify fixes +10. Creating a pull request +11. Providing a summary of what was fixed + +## Running Pyright + +**Command:** +```powershell +cd +azpysdk --isolate next-pyright . +``` + +> **Note:** `azpysdk next-pyright` runs pyright at the package level only. To focus on specific files, run the full check and filter the output by file path. + +## Reference Documentation + +- [Official Pyright Documentation](https://microsoft.github.io/pyright/) +- [Pyright Configuration](https://microsoft.github.io/pyright/#/configuration) +- [Pyright Error Codes](https://microsoft.github.io/pyright/#/configuration?id=type-check-diagnostics-settings) +- [Azure SDK Python Type Checking Guide](https://github.com/Azure/azure-sdk-for-python/blob/main/doc/dev/static_type_checking_cheat_sheet.md) + +## Fixing Strategy + +### Step 0: Get Package and Issue Details + +**Check if user provided in their request:** +- GitHub issue URL (look for `https://github.com/Azure/azure-sdk-for-python/issues/...` in user's message) +- Package path or name (e.g. `sdk/storage/azure-storage-blob` or `azure-storage-blob`) +- Virtual environment path (look for phrases like "using venv", "use env", "virtual environment at", or just the venv name) + +**If both GitHub issue URL and package path are missing:** +Ask: "Please provide either the GitHub issue URL or the package path (e.g. sdk/storage/azure-storage-blob) for the pyright type checking problems you want to fix." + +**If a GitHub issue URL is provided:** +Read the issue to understand which package and files/modules are affected, and the specific error codes to fix. + +**If only a package path is provided:** +Run pyright checks directly on the package. + +**If virtual environment is missing:** +Ask: "Do you have an existing virtual environment path, or should I create 'env'?" + +### Step 1: CRITICAL - Activate Virtual Environment FIRST + +**IMMEDIATELY activate the virtual environment before ANY other command:** + +```powershell +# Activate the provided virtual environment (e.g., env, venv) +.\\Scripts\Activate.ps1 + +# If creating new virtual environment +python -m venv env +.\env\Scripts\Activate.ps1 +``` + +**⚠️ IMPORTANT: ALL subsequent commands MUST run within the activated virtual environment. Never run commands outside the venv.** + +### Step 2: Install Dependencies (within activated venv) + +```powershell +# Navigate to the package directory (within activated venv) +cd + +# Install dev dependencies from dev_requirements.txt (within activated venv) +pip install -r dev_requirements.txt + +# Install the package in editable mode (within activated venv) +pip install -e . +``` + +### Step 3: Identify Target Files (within activated venv) + +Based on the GitHub issue details, determine which files to check: + +**Option A - Run pyright on the package and filter output:** +```powershell +# Ensure you're in the package directory (within activated venv) +cd + +# Run pyright on the full package, then filter output for files from the issue +azpysdk --isolate next-pyright . +# Review output for errors in the specific files/modules mentioned in the issue +``` + +**Option B - Check modified files (if no specific target):** +```powershell +git diff --name-only HEAD | Select-String "" +git diff --cached --name-only | Select-String "" +``` + +### Step 4: Run Pyright (within activated venv) + +**⚠️ Ensure virtual environment is still activated before running:** + +```powershell +# Navigate to the package directory +cd + +# Run pyright on the package (within activated venv) +azpysdk --isolate next-pyright . +# Filter output for the specific files/modules from the issue +``` + +### Step 5: Analyze Type Errors + +Parse the pyright output to identify: +- Error type and rule (e.g., reportGeneralClassIssues, reportMissingTypeArgument, reportAttributeAccessIssue) +- File path and line number +- Specific error description +- Expected vs actual types +- **Cross-reference with the GitHub issue** (if provided) to ensure you're fixing the right problems + +### Step 6: Search for Existing Type Annotation Patterns + +Before fixing, search the codebase for how similar types are annotated: +```powershell +# Example: Search for similar function signatures +grep -r "def similar_function" / -A 5 + +# Search for type imports +grep -r "from typing import" / +``` + +Use the existing type annotation patterns to ensure consistency. + +### Step 7: Apply Fixes (ONLY if 100% confident) + +**ALLOWED ACTIONS:** +✅ Fix type errors with 100% confidence +✅ Use existing type annotation patterns as reference +✅ Follow Azure SDK Python type checking guidelines +✅ Add missing type hints +✅ Fix incorrect type annotations +✅ Add proper type narrowing (isinstance checks, assertions) +✅ Make minimal, targeted changes + +**FORBIDDEN ACTIONS:** +❌ Fix errors without complete confidence +❌ Create new files for solutions +❌ Import non-existent types or modules +❌ Add new dependencies or imports outside typing module +❌ Use `# type: ignore` or `# pyright: ignore` without clear justification +❌ Change code logic to avoid type errors +❌ Delete code without clear justification + +### Step 8: Verify Fixes + +Re-run pyright to ensure: +- The type error is resolved +- No new errors were introduced +- The code still functions correctly + +### Step 9: Summary + +Provide a summary: +- GitHub issue being addressed +- Number of type errors fixed +- Number of errors remaining +- Types of fixes applied (e.g., added type hints, fixed return types, added type narrowing) +- Any errors that need manual review + +### Step 10: Create Pull Request + +> **⚠️ REQUIRED when a GitHub issue URL was provided:** You MUST create a pull request after validating fixes. This is not optional. + +Create a pull request with a descriptive title and body referencing the issue. Include what was fixed and confirm all pyright checks pass. The PR title should follow the format: "fix(): Resolve pyright type errors (#)". + +## Notes + +- Always read the existing code to understand type annotation patterns before making changes +- Prefer following existing patterns over adding new complex types +- Use Python 3.10+ compatible type hints (use `Optional[X]` instead of `X | None`) +- If unsure about a fix, mark it for manual review +- Some errors may require architectural changes - don't force fixes +- Test the code after fixing to ensure functionality is preserved +- Avoid using `# pyright: ignore` unless absolutely necessary and document why +- Pyright is stricter than mypy in many cases - ensure fixes satisfy pyright's type narrowing requirements diff --git a/doc/eng_sys_checks.md b/doc/eng_sys_checks.md index 10e8e998ad7f..643fa5be1d42 100644 --- a/doc/eng_sys_checks.md +++ b/doc/eng_sys_checks.md @@ -610,6 +610,15 @@ The weekly pipeline also runs "next" variants of mypy, pylint, pyright, and sphi Results are posted as GitHub issues in the repository. These checks run with `continueOnError: true` and do not block PRs. +#### Copilot auto-fix + +For `pylint`, `mypy`, `sphinx`, and `pyright` failures, the pipeline automatically assigns the Copilot coding agent to open a fix PR. + +- **You don't need to do anything** if Copilot opens a PR — just review and merge it like any other PR. +- **To opt out**, add the `copilot-auto-fix-disabled` label to the issue. +- **If Copilot fails**, the issue gets a `copilot-auto-fix-failed` label and a comment explaining what happened. Remove the label to allow a retry on the next weekly run. +- **Weekly retry**: if no matching PR exists when the pipeline runs again, Copilot is reassigned automatically. + To test a "next" check locally, use `--next`: ```bash diff --git a/eng/tools/azure-sdk-tools/gh_tools/vnext_issue_creator.py b/eng/tools/azure-sdk-tools/gh_tools/vnext_issue_creator.py index 66f0f1e08779..b328ebfcae9d 100644 --- a/eng/tools/azure-sdk-tools/gh_tools/vnext_issue_creator.py +++ b/eng/tools/azure-sdk-tools/gh_tools/vnext_issue_creator.py @@ -25,6 +25,235 @@ CHECK_TYPE = Literal["mypy", "pylint", "pyright", "sphinx"] +# --------------------------------------------------------------------------- +# Auto-fix automation constants +# --------------------------------------------------------------------------- + +#: Label constants for auto-fix state management. +LABEL_AUTO_FIX = "copilot-auto-fix" +LABEL_AUTO_FIX_FAILED = "copilot-auto-fix-failed" +LABEL_AUTO_FIX_DISABLED = "copilot-auto-fix-disabled" + +#: Copilot coding-agent bot login and node ID +DEFAULT_COPILOT_LOGIN = "copilot-swe-agent" +DEFAULT_COPILOT_NODE_ID = "BOT_kgDOC9w8XQ" + +_AUTO_FIX_START = "" +_AUTO_FIX_END = "" + + +# --------------------------------------------------------------------------- +# Auto-fix helpers +# --------------------------------------------------------------------------- + + +def _copilot_login() -> str: + """Return the Copilot assignable login, configurable via env var.""" + return os.getenv("COPILOT_LOGIN", DEFAULT_COPILOT_LOGIN) + + +def _copilot_node_id() -> str: + """Return the Copilot bot node ID, configurable via env var.""" + return os.getenv("COPILOT_NODE_ID", DEFAULT_COPILOT_NODE_ID) + + +def is_auto_fix_eligible( + issue_labels: list[str], +) -> bool: + """Return True when the package/check combination qualifies for auto-fix. + + Eligibility requires all of: + 1. The issue does not carry the opt-out label. + """ + if LABEL_AUTO_FIX_DISABLED in issue_labels: + return False + return True + + +def find_existing_fix_prs( + repo, + issue_number: int, + package_name: str, + check_type: str, +) -> list: + """Search for open PRs that likely address the same vnext failure. + + Returns a (possibly empty) list of matching PR objects. + """ + matches = [] + try: + open_prs = repo.get_pulls(state="open", sort="created", direction="desc") + for pr in open_prs: + body = pr.body or "" + title = pr.title or "" + search_text = f"{title} {body}".lower() + + # 1. PR explicitly references the issue number + issue_ref = f"#{issue_number}" + has_issue_ref = issue_ref in title or issue_ref in body + + # 2. PR mentions package + check type + has_pkg_and_check = ( + package_name.lower() in search_text and check_type.lower() in search_text + ) + + if has_issue_ref or has_pkg_and_check: + matches.append(pr) + except GithubException as e: + logging.warning(f"Failed to search PRs for duplicate detection: {e}") + + return matches + + +def build_copilot_instructions(package_path: str, check_type: str) -> str: + """Build the Copilot auto-fix instruction block for the issue body.""" + skill_name = f"fix-{check_type}" + validation_cmd = f"azpysdk {check_type} ." + + return ( + f"\n\n{_AUTO_FIX_START}\n" + f"## Copilot auto-fix request\n\n" + f"Use the `{skill_name}` skill to resolve `{check_type}` failures " + f"in `{package_path}`.\n" + f"Do not make unrelated formatting, changelog, version, or " + f"generated-code changes.\n\n" + f"Run `{validation_cmd}` from `{package_path}` and attempt to " + f"resolve all outputted errors. Include the final result in the " + f"PR body.\n" + f"Open a PR that links this issue and includes:\n\n" + f"> Automated Fix: This PR was automatically generated by Copilot " + f"in response to a vnext compatibility issue.\n\n" + f"If a safe fix is not possible, leave an issue comment with " + f"attempted commands, the failure category, and the recommended " + f"manual next step.\n" + f"{_AUTO_FIX_END}" + ) + + +def _strip_auto_fix_block(body: str) -> str: + """Remove any existing auto-fix instruction block from the issue body.""" + start = body.find(_AUTO_FIX_START) + end = body.find(_AUTO_FIX_END) + if start != -1 and end != -1: + return body[:start].rstrip() + body[end + len(_AUTO_FIX_END):] + return body + + +def reconcile_auto_fix_labels(issue, check_type: str, eligible: bool) -> None: + """Add or verify automation labels on the issue. + + Preserves all existing labels; only adds auto-fix labels when eligible. + """ + current_labels = [lbl.name if hasattr(lbl, "name") else str(lbl) for lbl in issue.labels] + + if eligible: + labels_to_add = [] + if LABEL_AUTO_FIX not in current_labels: + labels_to_add.append(LABEL_AUTO_FIX) + # Remove failure label if present (allows retry) + if LABEL_AUTO_FIX_FAILED in current_labels: + try: + issue.remove_from_labels(LABEL_AUTO_FIX_FAILED) + logging.info(f"Removed {LABEL_AUTO_FIX_FAILED} from issue #{issue.number} for retry") + except GithubException as e: + logging.warning(f"Failed to remove {LABEL_AUTO_FIX_FAILED}: {e}") + for label in labels_to_add: + try: + issue.add_to_labels(label) + logging.info(f"Added label '{label}' to issue #{issue.number}") + except GithubException as e: + logging.warning(f"Failed to add label '{label}' to issue #{issue.number}: {e}") + + +def _is_copilot_already_assigned(issue) -> bool: + """Check whether the Copilot login is already among the issue assignees.""" + login = _copilot_login() + for assignee in issue.assignees: + name = assignee.login if hasattr(assignee, "login") else str(assignee) + if name.lower() == login.lower(): + return True + return False + + +def assign_copilot(issue, github_instance, package_name: str, check_type: str) -> bool: + """Attempt to assign the Copilot coding agent to the issue. + + Uses the GraphQL ``addAssigneesToAssignable`` mutation because the + Copilot bot (``copilot-swe-agent``) is not assignable via the REST + assignees endpoint. + + Returns True on success, False on failure (labels/comments the issue). + """ + if _is_copilot_already_assigned(issue): + logging.info(f"Copilot already assigned to issue #{issue.number}, skipping") + return True + + login = _copilot_login() + node_id = _copilot_node_id() + issue_node_id = issue.raw_data["node_id"] + try: + github_instance._Github__requester.graphql_named_mutation( + "addAssigneesToAssignable", + { + "assignableId": issue_node_id, + "assigneeIds": [node_id], + }, + output="assignable { ... on Issue { id } }", + ) + logging.info(f"Assigned {login} to issue #{issue.number} for {package_name}/{check_type}") + return True + except Exception as e: + logging.warning(f"Failed to assign {login} to issue #{issue.number}: {e}") + try: + issue.add_to_labels(LABEL_AUTO_FIX_FAILED) + issue.create_comment( + f"⚠️ **Copilot auto-fix assignment failed**\n\n" + f"Could not assign `{login}` to this issue.\n" + f"Error: `{e}`\n\n" + f"Human triage is required. The `{LABEL_AUTO_FIX_FAILED}` label " + f"has been added. Remove it and re-run the vnext pipeline to retry." + ) + except GithubException as comment_err: + logging.warning(f"Failed to add failure label/comment: {comment_err}") + return False + + +def _try_auto_fix( + repo, + issue, + github_instance, + package_name: str, + package_path: str, + check_type: str, + issue_labels: list[str], +) -> None: + """Run the auto-fix eligibility → duplicate check → assign flow.""" + eligible = is_auto_fix_eligible(issue_labels) + + if not eligible: + return + + reconcile_auto_fix_labels(issue, check_type, eligible=True) + + # Duplicate PR detection + matching_prs = find_existing_fix_prs(repo, issue.number, package_name, check_type) + if matching_prs: + pr_urls = ", ".join(pr.html_url for pr in matching_prs) + logging.info( + f"Skipping Copilot assignment for issue #{issue.number}: " + f"matching PR(s) found: {pr_urls}" + ) + return + + # Append / replace Copilot instructions in the issue body + body = issue.body or "" + body = _strip_auto_fix_block(body) + instructions = build_copilot_instructions(package_path, check_type) + issue.edit(body=body + instructions) + + # Assign Copilot + assign_copilot(issue, github_instance, package_name, check_type) + def get_version_running(check_type: CHECK_TYPE) -> str: commands = [sys.executable, "-m", check_type, "--version"] @@ -145,9 +374,9 @@ def create_vnext_issue(package_dir: str, check_type: CHECK_TYPE, check_version: """This is called when a client library fails a vnext check. An issue is created with the details or an existing issue is updated with the latest information.""" - package_path = pathlib.Path(package_dir) - package_name = package_path.name - service_directory = package_path.parent.name + package_dir_path = pathlib.Path(package_dir) + package_name = package_dir_path.name + service_directory = package_dir_path.parent.name auth = Auth.Token(os.environ["GH_TOKEN"]) g = Github(auth=auth) @@ -186,6 +415,8 @@ def create_vnext_issue(package_dir: str, check_type: CHECK_TYPE, check_version: f"See the {guide_link} for more information." ) + package_path = f"sdk/{service_directory}/{package_name}" + # create an issue for the library failing the vnext check if not vnext_issue: try: @@ -194,7 +425,7 @@ def create_vnext_issue(package_dir: str, check_type: CHECK_TYPE, check_version: logging.warning(f"Failed to get labels and assignees from CODEOWNERS for {package_name}: {e}") labels = [] assignees = [] - if "mgmt" in package_name: + if package_name.startswith("azure-mgmt-"): labels.append("Mgmt") labels.extend([check_type]) @@ -208,6 +439,10 @@ def create_vnext_issue(package_dir: str, check_type: CHECK_TYPE, check_version: logging.info(f"Assigned {assignee} to issue for {package_name}") except GithubException as e: logging.warning(f"Failed to assign {assignee} to issue for {package_name}: {e}") + + # Auto-fix: check eligibility and assign Copilot + issue_label_names = [lbl if isinstance(lbl, str) else lbl.name for lbl in issue.labels] + _try_auto_fix(repo, issue, g, package_name, package_path, check_type, issue_label_names) return # an issue exists, let's update it so it reflects the latest typing/linting errors @@ -232,6 +467,10 @@ def create_vnext_issue(package_dir: str, check_type: CHECK_TYPE, check_version: except GithubException as e: logging.warning(f"Failed to assign {assignee} to issue for {package_name}: {e}") + # Auto-fix: reconcile labels and retry assignment if no matching PR + issue_label_names = [lbl.name if hasattr(lbl, "name") else str(lbl) for lbl in vnext_issue[0].labels] + _try_auto_fix(repo, vnext_issue[0], g, package_name, package_path, check_type, issue_label_names) + def close_vnext_issue(package_name: str, check_type: CHECK_TYPE) -> None: """This is called when a client library passes a vnext check. If an issue exists for the library, it is closed.""" diff --git a/eng/tools/azure-sdk-tools/tests/test_vnext_auto_fix.py b/eng/tools/azure-sdk-tools/tests/test_vnext_auto_fix.py new file mode 100644 index 000000000000..c9f435a52565 --- /dev/null +++ b/eng/tools/azure-sdk-tools/tests/test_vnext_auto_fix.py @@ -0,0 +1,374 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +"""Tests for vnext issue auto-fix automation helpers.""" + +from __future__ import annotations + +import os +from types import SimpleNamespace +from unittest.mock import MagicMock, patch + +import pytest +from github import GithubException + +from gh_tools.vnext_issue_creator import ( + DEFAULT_COPILOT_NODE_ID, + LABEL_AUTO_FIX, + LABEL_AUTO_FIX_DISABLED, + LABEL_AUTO_FIX_FAILED, + _AUTO_FIX_END, + _AUTO_FIX_START, + _copilot_login, + _copilot_node_id, + _is_copilot_already_assigned, + _strip_auto_fix_block, + _try_auto_fix, + assign_copilot, + build_copilot_instructions, + find_existing_fix_prs, + is_auto_fix_eligible, + reconcile_auto_fix_labels, +) + + +# --------------------------------------------------------------------------- +# Helpers to build lightweight fakes +# --------------------------------------------------------------------------- + +def _make_label(name: str) -> SimpleNamespace: + return SimpleNamespace(name=name) + + +def _make_assignee(login: str) -> SimpleNamespace: + return SimpleNamespace(login=login) + + +def _make_issue( + number: int = 1, + body: str = "", + labels: list | None = None, + assignees: list | None = None, + node_id: str = "I_abc123", +) -> MagicMock: + issue = MagicMock() + issue.number = number + issue.body = body + issue.labels = [_make_label(l) for l in (labels or [])] + issue.assignees = [_make_assignee(a) for a in (assignees or [])] + issue.html_url = f"https://github.com/test/repo/issues/{number}" + issue.raw_data = {"node_id": node_id} + return issue + + +def _make_github_instance() -> MagicMock: + """Create a mock Github instance with a requester that supports graphql_named_mutation.""" + g = MagicMock() + g._Github__requester = MagicMock() + return g + + +def _make_pr( + title: str = "", + body: str = "", + html_url: str = "https://github.com/test/repo/pull/99", +) -> SimpleNamespace: + return SimpleNamespace(title=title, body=body, html_url=html_url) + + +# --------------------------------------------------------------------------- +# Eligibility tests +# --------------------------------------------------------------------------- + +class TestIsAutoFixEligible: + """Tests for is_auto_fix_eligible.""" + + def test_any_check_type_eligible(self): + assert is_auto_fix_eligible("azure-ai-test", "pylint", []) is True + assert is_auto_fix_eligible("azure-ai-test", "mypy", []) is True + assert is_auto_fix_eligible("azure-ai-test", "sphinx", []) is True + assert is_auto_fix_eligible("azure-ai-test", "pyright", []) is True + assert is_auto_fix_eligible("azure-ai-test", "bandit", []) is True + + def test_mgmt_package_eligible(self): + assert is_auto_fix_eligible("azure-mgmt-compute", "pylint", []) is True + + def test_opt_out_label(self): + assert is_auto_fix_eligible( + "azure-ai-test", "pylint", [LABEL_AUTO_FIX_DISABLED] + ) is False + + +# --------------------------------------------------------------------------- +# Duplicate PR detection tests +# --------------------------------------------------------------------------- + +class TestFindExistingFixPrs: + + def test_match_by_issue_ref_in_title(self): + repo = MagicMock() + repo.get_pulls.return_value = [ + _make_pr(title="Fix pylint for azure-ai-test #42"), + ] + result = find_existing_fix_prs(repo, 42, "azure-ai-test", "pylint") + assert len(result) == 1 + + def test_match_by_issue_ref_in_body(self): + repo = MagicMock() + repo.get_pulls.return_value = [ + _make_pr(body="Fixes #42"), + ] + result = find_existing_fix_prs(repo, 42, "azure-ai-test", "pylint") + assert len(result) == 1 + + def test_match_by_package_and_check(self): + repo = MagicMock() + repo.get_pulls.return_value = [ + _make_pr(title="Fix azure-ai-test pylint errors"), + ] + result = find_existing_fix_prs(repo, 99, "azure-ai-test", "pylint") + assert len(result) == 1 + + def test_no_match(self): + repo = MagicMock() + repo.get_pulls.return_value = [ + _make_pr(title="Unrelated PR", body="Nothing here"), + ] + result = find_existing_fix_prs(repo, 42, "azure-ai-test", "pylint") + assert len(result) == 0 + + def test_github_exception_returns_empty(self): + repo = MagicMock() + repo.get_pulls.side_effect = GithubException(500, "error", None) + result = find_existing_fix_prs(repo, 42, "azure-ai-test", "pylint") + assert result == [] + + +# --------------------------------------------------------------------------- +# Copilot instruction builder tests +# --------------------------------------------------------------------------- + +class TestBuildCopilotInstructions: + + @pytest.mark.parametrize("check_type", ["pylint", "mypy", "sphinx", "pyright"]) + def test_contains_required_elements(self, check_type): + result = build_copilot_instructions("sdk/ai/azure-ai-test", check_type) + + assert _AUTO_FIX_START in result + assert _AUTO_FIX_END in result + assert f"fix-{check_type}" in result + assert f"azpysdk {check_type} ." in result + assert "sdk/ai/azure-ai-test" in result + assert "Automated Fix" in result + assert "Do not make unrelated" in result + + +# --------------------------------------------------------------------------- +# Strip auto-fix block tests +# --------------------------------------------------------------------------- + +class TestStripAutoFixBlock: + + def test_removes_block(self): + body = f"Hello\n{_AUTO_FIX_START}\ncopilot stuff\n{_AUTO_FIX_END}\ntrailer" + result = _strip_auto_fix_block(body) + assert _AUTO_FIX_START not in result + assert "copilot stuff" not in result + assert "trailer" in result + + def test_no_block_unchanged(self): + body = "No auto-fix block here" + assert _strip_auto_fix_block(body) == body + + +# --------------------------------------------------------------------------- +# Label reconciliation tests +# --------------------------------------------------------------------------- + +class TestReconcileAutoFixLabels: + + def test_adds_auto_fix_label(self): + issue = _make_issue(labels=["pylint"]) + reconcile_auto_fix_labels(issue, "pylint", eligible=True) + issue.add_to_labels.assert_called_once_with(LABEL_AUTO_FIX) + + def test_skips_if_already_labeled(self): + issue = _make_issue(labels=["pylint", LABEL_AUTO_FIX]) + reconcile_auto_fix_labels(issue, "pylint", eligible=True) + issue.add_to_labels.assert_not_called() + + def test_removes_failed_label_on_retry(self): + issue = _make_issue(labels=["pylint", LABEL_AUTO_FIX_FAILED]) + reconcile_auto_fix_labels(issue, "pylint", eligible=True) + issue.remove_from_labels.assert_called_once_with(LABEL_AUTO_FIX_FAILED) + issue.add_to_labels.assert_called_once_with(LABEL_AUTO_FIX) + + def test_not_eligible_no_op(self): + issue = _make_issue(labels=["pylint"]) + reconcile_auto_fix_labels(issue, "pylint", eligible=False) + issue.add_to_labels.assert_not_called() + issue.remove_from_labels.assert_not_called() + + +# --------------------------------------------------------------------------- +# Copilot assignment tests +# --------------------------------------------------------------------------- + +class TestAssignCopilot: + + def test_success(self): + issue = _make_issue() + g = _make_github_instance() + assert assign_copilot(issue, g, "azure-ai-test", "pylint") is True + g._Github__requester.graphql_named_mutation.assert_called_once() + call_args = g._Github__requester.graphql_named_mutation.call_args + assert call_args[0][0] == "addAssigneesToAssignable" + assert call_args[0][1]["assigneeIds"] == [DEFAULT_COPILOT_NODE_ID] + + def test_already_assigned_skips(self): + issue = _make_issue(assignees=["copilot-swe-agent"]) + g = _make_github_instance() + assert assign_copilot(issue, g, "azure-ai-test", "pylint") is True + g._Github__requester.graphql_named_mutation.assert_not_called() + + def test_failure_adds_label_and_comment(self): + issue = _make_issue() + g = _make_github_instance() + g._Github__requester.graphql_named_mutation.side_effect = Exception("mutation failed") + assert assign_copilot(issue, g, "azure-ai-test", "pylint") is False + issue.add_to_labels.assert_called_once_with(LABEL_AUTO_FIX_FAILED) + issue.create_comment.assert_called_once() + comment_text = issue.create_comment.call_args[0][0] + assert "auto-fix assignment failed" in comment_text + + @patch.dict(os.environ, {"COPILOT_LOGIN": "custom-bot", "COPILOT_NODE_ID": "BOT_custom"}) + def test_configurable_login_and_node_id(self): + issue = _make_issue() + g = _make_github_instance() + assert assign_copilot(issue, g, "azure-ai-test", "pylint") is True + call_args = g._Github__requester.graphql_named_mutation.call_args + assert call_args[0][1]["assigneeIds"] == ["BOT_custom"] + + +# --------------------------------------------------------------------------- +# Copilot login helper tests +# --------------------------------------------------------------------------- + +class TestCopilotLogin: + + def test_default(self): + with patch.dict(os.environ, {}, clear=True): + os.environ.pop("COPILOT_LOGIN", None) + assert _copilot_login() == "copilot-swe-agent" + + @patch.dict(os.environ, {"COPILOT_LOGIN": "my-bot"}) + def test_env_override(self): + assert _copilot_login() == "my-bot" + + +class TestCopilotNodeId: + + def test_default(self): + with patch.dict(os.environ, {}, clear=True): + os.environ.pop("COPILOT_NODE_ID", None) + assert _copilot_node_id() == "BOT_kgDOC9w8XQ" + + @patch.dict(os.environ, {"COPILOT_NODE_ID": "BOT_custom"}) + def test_env_override(self): + assert _copilot_node_id() == "BOT_custom" + + +# --------------------------------------------------------------------------- +# _is_copilot_already_assigned tests +# --------------------------------------------------------------------------- + +class TestIsCopilotAlreadyAssigned: + + def test_assigned(self): + issue = _make_issue(assignees=["copilot-swe-agent"]) + assert _is_copilot_already_assigned(issue) is True + + def test_not_assigned(self): + issue = _make_issue(assignees=["human-user"]) + assert _is_copilot_already_assigned(issue) is False + + def test_case_insensitive(self): + issue = _make_issue(assignees=["Copilot-SWE-Agent"]) + assert _is_copilot_already_assigned(issue) is True + + +# --------------------------------------------------------------------------- +# Integration: _try_auto_fix tests +# --------------------------------------------------------------------------- + +class TestTryAutoFix: + + def test_eligible_no_duplicate_assigns(self): + repo = MagicMock() + repo.get_pulls.return_value = [] + issue = _make_issue(labels=["pylint"]) + g = _make_github_instance() + + _try_auto_fix(repo, issue, g, "azure-ai-test", "sdk/ai/azure-ai-test", "pylint", ["pylint"]) + + # Labels reconciled + issue.add_to_labels.assert_any_call(LABEL_AUTO_FIX) + # Instructions appended + issue.edit.assert_called_once() + body_arg = issue.edit.call_args[1]["body"] + assert _AUTO_FIX_START in body_arg + # Copilot assigned via GraphQL + g._Github__requester.graphql_named_mutation.assert_called_once() + + def test_eligible_with_duplicate_pr_skips(self): + repo = MagicMock() + repo.get_pulls.return_value = [ + _make_pr(title="Fix pylint #1"), + ] + issue = _make_issue(number=1, labels=["pylint"]) + g = _make_github_instance() + + _try_auto_fix(repo, issue, g, "azure-ai-test", "sdk/ai/azure-ai-test", "pylint", ["pylint"]) + + # Should NOT assign Copilot + g._Github__requester.graphql_named_mutation.assert_not_called() + + def test_opt_out_label_prevents_assignment(self): + repo = MagicMock() + issue = _make_issue(labels=["pylint", LABEL_AUTO_FIX_DISABLED]) + g = _make_github_instance() + + _try_auto_fix( + repo, issue, g, "azure-ai-test", "sdk/ai/azure-ai-test", "pylint", + ["pylint", LABEL_AUTO_FIX_DISABLED], + ) + + g._Github__requester.graphql_named_mutation.assert_not_called() + + def test_weekly_retry_reassigns_when_no_pr(self): + """Simulates a weekly re-run: issue already has copilot-auto-fix label + but no matching PR exists, so Copilot should be reassigned.""" + repo = MagicMock() + repo.get_pulls.return_value = [] + issue = _make_issue(labels=["pylint", LABEL_AUTO_FIX]) + g = _make_github_instance() + + _try_auto_fix(repo, issue, g, "azure-ai-test", "sdk/ai/azure-ai-test", "pylint", + ["pylint", LABEL_AUTO_FIX]) + + g._Github__requester.graphql_named_mutation.assert_called_once() + + def test_assignment_failure_adds_failed_label(self): + repo = MagicMock() + repo.get_pulls.return_value = [] + issue = _make_issue(labels=["pylint"]) + g = _make_github_instance() + g._Github__requester.graphql_named_mutation.side_effect = Exception("mutation failed") + + _try_auto_fix(repo, issue, g, "azure-ai-test", "sdk/ai/azure-ai-test", "pylint", ["pylint"]) + + # Should have tried to add the failed label + issue.add_to_labels.assert_any_call(LABEL_AUTO_FIX_FAILED) + issue.create_comment.assert_called_once() From ba0ea8131b647be3db0e34b3124c27eae690920f Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Tue, 5 May 2026 12:38:41 -0700 Subject: [PATCH 02/18] improvements --- .../gh_tools/vnext_issue_creator.py | 44 +++++-------------- .../tests/test_vnext_auto_fix.py | 42 +++--------------- 2 files changed, 17 insertions(+), 69 deletions(-) diff --git a/eng/tools/azure-sdk-tools/gh_tools/vnext_issue_creator.py b/eng/tools/azure-sdk-tools/gh_tools/vnext_issue_creator.py index b328ebfcae9d..7c02f179b749 100644 --- a/eng/tools/azure-sdk-tools/gh_tools/vnext_issue_creator.py +++ b/eng/tools/azure-sdk-tools/gh_tools/vnext_issue_creator.py @@ -34,13 +34,12 @@ LABEL_AUTO_FIX_FAILED = "copilot-auto-fix-failed" LABEL_AUTO_FIX_DISABLED = "copilot-auto-fix-disabled" -#: Copilot coding-agent bot login and node ID +#: Copilot coding-agent bot login and node ID. +#: The node ID was retrieved via the suggestedActors GraphQL query on +#: Azure/azure-sdk-for-python. Override with env vars if needed. DEFAULT_COPILOT_LOGIN = "copilot-swe-agent" DEFAULT_COPILOT_NODE_ID = "BOT_kgDOC9w8XQ" -_AUTO_FIX_START = "" -_AUTO_FIX_END = "" - # --------------------------------------------------------------------------- # Auto-fix helpers @@ -60,11 +59,7 @@ def _copilot_node_id() -> str: def is_auto_fix_eligible( issue_labels: list[str], ) -> bool: - """Return True when the package/check combination qualifies for auto-fix. - - Eligibility requires all of: - 1. The issue does not carry the opt-out label. - """ + """Return True when the package/check combination qualifies for auto-fix.""" if LABEL_AUTO_FIX_DISABLED in issue_labels: return False return True @@ -93,9 +88,7 @@ def find_existing_fix_prs( has_issue_ref = issue_ref in title or issue_ref in body # 2. PR mentions package + check type - has_pkg_and_check = ( - package_name.lower() in search_text and check_type.lower() in search_text - ) + has_pkg_and_check = package_name.lower() in search_text and check_type.lower() in search_text if has_issue_ref or has_pkg_and_check: matches.append(pr) @@ -111,8 +104,7 @@ def build_copilot_instructions(package_path: str, check_type: str) -> str: validation_cmd = f"azpysdk {check_type} ." return ( - f"\n\n{_AUTO_FIX_START}\n" - f"## Copilot auto-fix request\n\n" + f"\n\n## Copilot auto-fix request\n\n" f"Use the `{skill_name}` skill to resolve `{check_type}` failures " f"in `{package_path}`.\n" f"Do not make unrelated formatting, changelog, version, or " @@ -125,21 +117,11 @@ def build_copilot_instructions(package_path: str, check_type: str) -> str: f"in response to a vnext compatibility issue.\n\n" f"If a safe fix is not possible, leave an issue comment with " f"attempted commands, the failure category, and the recommended " - f"manual next step.\n" - f"{_AUTO_FIX_END}" + f"manual next step." ) -def _strip_auto_fix_block(body: str) -> str: - """Remove any existing auto-fix instruction block from the issue body.""" - start = body.find(_AUTO_FIX_START) - end = body.find(_AUTO_FIX_END) - if start != -1 and end != -1: - return body[:start].rstrip() + body[end + len(_AUTO_FIX_END):] - return body - - -def reconcile_auto_fix_labels(issue, check_type: str, eligible: bool) -> None: +def reconcile_auto_fix_labels(issue, eligible: bool) -> None: """Add or verify automation labels on the issue. Preserves all existing labels; only adds auto-fix labels when eligible. @@ -233,21 +215,17 @@ def _try_auto_fix( if not eligible: return - reconcile_auto_fix_labels(issue, check_type, eligible=True) + reconcile_auto_fix_labels(issue, eligible=True) # Duplicate PR detection matching_prs = find_existing_fix_prs(repo, issue.number, package_name, check_type) if matching_prs: pr_urls = ", ".join(pr.html_url for pr in matching_prs) - logging.info( - f"Skipping Copilot assignment for issue #{issue.number}: " - f"matching PR(s) found: {pr_urls}" - ) + logging.info(f"Skipping Copilot assignment for issue #{issue.number}: " f"matching PR(s) found: {pr_urls}") return - # Append / replace Copilot instructions in the issue body + # Append Copilot instructions to the issue body body = issue.body or "" - body = _strip_auto_fix_block(body) instructions = build_copilot_instructions(package_path, check_type) issue.edit(body=body + instructions) diff --git a/eng/tools/azure-sdk-tools/tests/test_vnext_auto_fix.py b/eng/tools/azure-sdk-tools/tests/test_vnext_auto_fix.py index c9f435a52565..2dcf2df7ee23 100644 --- a/eng/tools/azure-sdk-tools/tests/test_vnext_auto_fix.py +++ b/eng/tools/azure-sdk-tools/tests/test_vnext_auto_fix.py @@ -19,12 +19,9 @@ LABEL_AUTO_FIX, LABEL_AUTO_FIX_DISABLED, LABEL_AUTO_FIX_FAILED, - _AUTO_FIX_END, - _AUTO_FIX_START, _copilot_login, _copilot_node_id, _is_copilot_already_assigned, - _strip_auto_fix_block, _try_auto_fix, assign_copilot, build_copilot_instructions, @@ -85,20 +82,13 @@ def _make_pr( class TestIsAutoFixEligible: """Tests for is_auto_fix_eligible.""" - def test_any_check_type_eligible(self): - assert is_auto_fix_eligible("azure-ai-test", "pylint", []) is True - assert is_auto_fix_eligible("azure-ai-test", "mypy", []) is True - assert is_auto_fix_eligible("azure-ai-test", "sphinx", []) is True - assert is_auto_fix_eligible("azure-ai-test", "pyright", []) is True - assert is_auto_fix_eligible("azure-ai-test", "bandit", []) is True - - def test_mgmt_package_eligible(self): - assert is_auto_fix_eligible("azure-mgmt-compute", "pylint", []) is True + def test_eligible_by_default(self): + assert is_auto_fix_eligible([]) is True + assert is_auto_fix_eligible(["pylint"]) is True + assert is_auto_fix_eligible(["mypy", "some-service-label"]) is True def test_opt_out_label(self): - assert is_auto_fix_eligible( - "azure-ai-test", "pylint", [LABEL_AUTO_FIX_DISABLED] - ) is False + assert is_auto_fix_eligible([LABEL_AUTO_FIX_DISABLED]) is False # --------------------------------------------------------------------------- @@ -156,8 +146,6 @@ class TestBuildCopilotInstructions: def test_contains_required_elements(self, check_type): result = build_copilot_instructions("sdk/ai/azure-ai-test", check_type) - assert _AUTO_FIX_START in result - assert _AUTO_FIX_END in result assert f"fix-{check_type}" in result assert f"azpysdk {check_type} ." in result assert "sdk/ai/azure-ai-test" in result @@ -165,24 +153,6 @@ def test_contains_required_elements(self, check_type): assert "Do not make unrelated" in result -# --------------------------------------------------------------------------- -# Strip auto-fix block tests -# --------------------------------------------------------------------------- - -class TestStripAutoFixBlock: - - def test_removes_block(self): - body = f"Hello\n{_AUTO_FIX_START}\ncopilot stuff\n{_AUTO_FIX_END}\ntrailer" - result = _strip_auto_fix_block(body) - assert _AUTO_FIX_START not in result - assert "copilot stuff" not in result - assert "trailer" in result - - def test_no_block_unchanged(self): - body = "No auto-fix block here" - assert _strip_auto_fix_block(body) == body - - # --------------------------------------------------------------------------- # Label reconciliation tests # --------------------------------------------------------------------------- @@ -318,7 +288,7 @@ def test_eligible_no_duplicate_assigns(self): # Instructions appended issue.edit.assert_called_once() body_arg = issue.edit.call_args[1]["body"] - assert _AUTO_FIX_START in body_arg + assert "Copilot auto-fix request" in body_arg # Copilot assigned via GraphQL g._Github__requester.graphql_named_mutation.assert_called_once() From 9752d601525f567d7bb6e70eaf24f5c98d2863fe Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Tue, 5 May 2026 12:54:40 -0700 Subject: [PATCH 03/18] retrigger copilot when version changes --- .../gh_tools/vnext_issue_creator.py | 89 ++++++++++++++++--- 1 file changed, 78 insertions(+), 11 deletions(-) diff --git a/eng/tools/azure-sdk-tools/gh_tools/vnext_issue_creator.py b/eng/tools/azure-sdk-tools/gh_tools/vnext_issue_creator.py index 7c02f179b749..f2b8977db90b 100644 --- a/eng/tools/azure-sdk-tools/gh_tools/vnext_issue_creator.py +++ b/eng/tools/azure-sdk-tools/gh_tools/vnext_issue_creator.py @@ -34,6 +34,10 @@ LABEL_AUTO_FIX_FAILED = "copilot-auto-fix-failed" LABEL_AUTO_FIX_DISABLED = "copilot-auto-fix-disabled" +#: Managed block markers for Copilot instructions in issue bodies. +COPILOT_AUTOFIX_START = "" +COPILOT_AUTOFIX_END = "" + #: Copilot coding-agent bot login and node ID. #: The node ID was retrieved via the suggestedActors GraphQL query on #: Azure/azure-sdk-for-python. Override with env vars if needed. @@ -103,8 +107,8 @@ def build_copilot_instructions(package_path: str, check_type: str) -> str: skill_name = f"fix-{check_type}" validation_cmd = f"azpysdk {check_type} ." - return ( - f"\n\n## Copilot auto-fix request\n\n" + inner = ( + f"## Copilot auto-fix request\n\n" f"Use the `{skill_name}` skill to resolve `{check_type}` failures " f"in `{package_path}`.\n" f"Do not make unrelated formatting, changelog, version, or " @@ -119,6 +123,20 @@ def build_copilot_instructions(package_path: str, check_type: str) -> str: f"attempted commands, the failure category, and the recommended " f"manual next step." ) + return f"\n\n{COPILOT_AUTOFIX_START}\n{inner}\n{COPILOT_AUTOFIX_END}" + + +def _upsert_copilot_instructions(body: str, instructions: str) -> str: + """Replace existing Copilot instruction block or append if absent. + + Uses managed HTML-comment markers to locate the block, preserving any + human-authored content outside the markers. + """ + start_idx = body.find(COPILOT_AUTOFIX_START) + end_idx = body.find(COPILOT_AUTOFIX_END) + if start_idx != -1 and end_idx != -1: + return body[:start_idx].rstrip() + instructions + return body + instructions def reconcile_auto_fix_labels(issue, eligible: bool) -> None: @@ -157,18 +175,50 @@ def _is_copilot_already_assigned(issue) -> bool: return False -def assign_copilot(issue, github_instance, package_name: str, check_type: str) -> bool: +def _unassign_copilot(issue, github_instance) -> bool: + """Remove the Copilot coding agent from the issue assignees. + + Uses the GraphQL ``removeAssigneesFromAssignable`` mutation. + Treats "not currently assigned" as success (idempotent). + """ + login = _copilot_login() + node_id = _copilot_node_id() + issue_node_id = issue.raw_data["node_id"] + try: + github_instance._Github__requester.graphql_named_mutation( + "removeAssigneesFromAssignable", + { + "assignableId": issue_node_id, + "assigneeIds": [node_id], + }, + output="assignable { ... on Issue { id } }", + ) + logging.info(f"Unassigned {login} from issue #{issue.number}") + return True + except Exception as e: + logging.warning(f"Failed to unassign {login} from issue #{issue.number}: {e}") + return False + + +def assign_copilot(issue, github_instance, package_name: str, check_type: str, force_reassign: bool = False) -> bool: """Attempt to assign the Copilot coding agent to the issue. Uses the GraphQL ``addAssigneesToAssignable`` mutation because the Copilot bot (``copilot-swe-agent``) is not assignable via the REST assignees endpoint. + When *force_reassign* is True and Copilot is already assigned, the + agent is first unassigned then reassigned so that a new Copilot + session is triggered (e.g. after a checker version bump). + Returns True on success, False on failure (labels/comments the issue). """ if _is_copilot_already_assigned(issue): - logging.info(f"Copilot already assigned to issue #{issue.number}, skipping") - return True + if not force_reassign: + logging.info(f"Copilot already assigned to issue #{issue.number}, skipping") + return True + logging.info(f"Copilot already assigned to issue #{issue.number}, " f"re-assigning to trigger new session") + _unassign_copilot(issue, github_instance) login = _copilot_login() node_id = _copilot_node_id() @@ -208,8 +258,13 @@ def _try_auto_fix( package_path: str, check_type: str, issue_labels: list[str], + version_changed: bool = False, ) -> None: - """Run the auto-fix eligibility → duplicate check → assign flow.""" + """Run the auto-fix eligibility → duplicate check → assign flow. + + When *version_changed* is True (e.g. checker version bump), Copilot is + unassigned and reassigned so a fresh session picks up the new errors. + """ eligible = is_auto_fix_eligible(issue_labels) if not eligible: @@ -224,13 +279,13 @@ def _try_auto_fix( logging.info(f"Skipping Copilot assignment for issue #{issue.number}: " f"matching PR(s) found: {pr_urls}") return - # Append Copilot instructions to the issue body + # Upsert Copilot instructions (replace existing block or append) body = issue.body or "" instructions = build_copilot_instructions(package_path, check_type) - issue.edit(body=body + instructions) + issue.edit(body=_upsert_copilot_instructions(body, instructions)) - # Assign Copilot - assign_copilot(issue, github_instance, package_name, check_type) + # Assign Copilot (force reassignment on version bumps) + assign_copilot(issue, github_instance, package_name, check_type, force_reassign=version_changed) def get_version_running(check_type: CHECK_TYPE) -> str: @@ -432,10 +487,13 @@ def create_vnext_issue(package_dir: str, check_type: CHECK_TYPE, check_version: labels = [] assignees = [] + # Detect version change so Copilot can be re-triggered + old_title = vnext_issue[0].title vnext_issue[0].edit( title=title, body=template, ) + version_changed = old_title != title # Assign codeowners individually with error handling for assignee in assignees: @@ -447,7 +505,16 @@ def create_vnext_issue(package_dir: str, check_type: CHECK_TYPE, check_version: # Auto-fix: reconcile labels and retry assignment if no matching PR issue_label_names = [lbl.name if hasattr(lbl, "name") else str(lbl) for lbl in vnext_issue[0].labels] - _try_auto_fix(repo, vnext_issue[0], g, package_name, package_path, check_type, issue_label_names) + _try_auto_fix( + repo, + vnext_issue[0], + g, + package_name, + package_path, + check_type, + issue_label_names, + version_changed=version_changed, + ) def close_vnext_issue(package_name: str, check_type: CHECK_TYPE) -> None: From 665014a4edefebb483da89b2925d5dce544080fa Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Tue, 5 May 2026 12:56:58 -0700 Subject: [PATCH 04/18] fix pylance complaints --- eng/tools/azure-sdk-tools/gh_tools/vnext_issue_creator.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/eng/tools/azure-sdk-tools/gh_tools/vnext_issue_creator.py b/eng/tools/azure-sdk-tools/gh_tools/vnext_issue_creator.py index f2b8977db90b..70a917399f8a 100644 --- a/eng/tools/azure-sdk-tools/gh_tools/vnext_issue_creator.py +++ b/eng/tools/azure-sdk-tools/gh_tools/vnext_issue_creator.py @@ -320,7 +320,7 @@ def get_build_link(check_type: CHECK_TYPE) -> str: ) -def get_merge_dates(year: str) -> typing.List[datetime.datetime]: +def get_merge_dates(year: int) -> typing.List[datetime.datetime]: """We'll merge the latest version of the type checker/linter quarterly on the Monday after release week. This function returns those 4 Mondays for the given year. @@ -343,7 +343,7 @@ def get_merge_dates(year: str) -> typing.List[datetime.datetime]: return merge_dates -def get_date_for_version_bump(today: datetime.datetime) -> str: +def get_date_for_version_bump(today: datetime.date) -> str: merge_dates = get_merge_dates(today.year) try: merge_date = min(date for date in merge_dates if date >= today) @@ -416,7 +416,7 @@ def create_vnext_issue(package_dir: str, check_type: CHECK_TYPE, check_version: today = datetime.date.today() repo = g.get_repo("Azure/azure-sdk-for-python") - issues = repo.get_issues(state="open", labels=[check_type], creator="azure-sdk") + issues = repo.get_issues(state="open", labels=[check_type], creator="azure-sdk") # type: ignore[arg-type] vnext_issue = [issue for issue in issues if issue.title.split("needs")[0].strip() == package_name] version = check_version or get_version_running(check_type) @@ -525,7 +525,7 @@ def close_vnext_issue(package_name: str, check_type: CHECK_TYPE) -> None: repo = g.get_repo("Azure/azure-sdk-for-python") - issues = repo.get_issues(state="open", labels=[check_type], creator="azure-sdk") + issues = repo.get_issues(state="open", labels=[check_type], creator="azure-sdk") # type: ignore[arg-type] vnext_issue = [issue for issue in issues if issue.title.split("needs")[0].strip() == package_name] if vnext_issue: logging.info(f"{package_name} passes {check_type}. Closing existing GH issue #{vnext_issue[0].number}...") From 4f276198b642c8a2a54e951bbbf99016c8bb3af7 Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Tue, 5 May 2026 13:01:58 -0700 Subject: [PATCH 05/18] skill fix --- .github/skills/fix-pyright/SKILL.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/skills/fix-pyright/SKILL.md b/.github/skills/fix-pyright/SKILL.md index 70fffdec86f7..4f51830cd3ba 100644 --- a/.github/skills/fix-pyright/SKILL.md +++ b/.github/skills/fix-pyright/SKILL.md @@ -27,10 +27,17 @@ Intelligently fixes pyright issues by: **Command:** ```powershell cd +azpysdk --isolate pyright . +``` + +> **Note:** `azpysdk pyright` runs with a pinned version of pyright at the package level only. To focus on specific files, run the full check and filter the output by file path. + +**Using Latest Pyright:** +```powershell azpysdk --isolate next-pyright . ``` -> **Note:** `azpysdk next-pyright` runs pyright at the package level only. To focus on specific files, run the full check and filter the output by file path. +> Use `azpysdk next-pyright` to run with the latest version of pyright. This is useful for catching issues that may be flagged by newer pyright versions. ## Reference Documentation @@ -117,7 +124,7 @@ git diff --cached --name-only | Select-String "" cd # Run pyright on the package (within activated venv) -azpysdk --isolate next-pyright . +azpysdk --isolate pyright . # Filter output for the specific files/modules from the issue ``` @@ -194,4 +201,3 @@ Create a pull request with a descriptive title and body referencing the issue. I - Some errors may require architectural changes - don't force fixes - Test the code after fixing to ensure functionality is preserved - Avoid using `# pyright: ignore` unless absolutely necessary and document why -- Pyright is stricter than mypy in many cases - ensure fixes satisfy pyright's type narrowing requirements From 83937fbca6a28234c95a5c4d2ce242dc333ac2bf Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Tue, 5 May 2026 13:08:11 -0700 Subject: [PATCH 06/18] test fix --- .../tests/test_vnext_auto_fix.py | 36 +++---------------- 1 file changed, 4 insertions(+), 32 deletions(-) diff --git a/eng/tools/azure-sdk-tools/tests/test_vnext_auto_fix.py b/eng/tools/azure-sdk-tools/tests/test_vnext_auto_fix.py index 2dcf2df7ee23..77913a3bedde 100644 --- a/eng/tools/azure-sdk-tools/tests/test_vnext_auto_fix.py +++ b/eng/tools/azure-sdk-tools/tests/test_vnext_auto_fix.py @@ -161,23 +161,23 @@ class TestReconcileAutoFixLabels: def test_adds_auto_fix_label(self): issue = _make_issue(labels=["pylint"]) - reconcile_auto_fix_labels(issue, "pylint", eligible=True) + reconcile_auto_fix_labels(issue, eligible=True) issue.add_to_labels.assert_called_once_with(LABEL_AUTO_FIX) def test_skips_if_already_labeled(self): issue = _make_issue(labels=["pylint", LABEL_AUTO_FIX]) - reconcile_auto_fix_labels(issue, "pylint", eligible=True) + reconcile_auto_fix_labels(issue, eligible=True) issue.add_to_labels.assert_not_called() def test_removes_failed_label_on_retry(self): issue = _make_issue(labels=["pylint", LABEL_AUTO_FIX_FAILED]) - reconcile_auto_fix_labels(issue, "pylint", eligible=True) + reconcile_auto_fix_labels(issue, eligible=True) issue.remove_from_labels.assert_called_once_with(LABEL_AUTO_FIX_FAILED) issue.add_to_labels.assert_called_once_with(LABEL_AUTO_FIX) def test_not_eligible_no_op(self): issue = _make_issue(labels=["pylint"]) - reconcile_auto_fix_labels(issue, "pylint", eligible=False) + reconcile_auto_fix_labels(issue, eligible=False) issue.add_to_labels.assert_not_called() issue.remove_from_labels.assert_not_called() @@ -222,34 +222,6 @@ def test_configurable_login_and_node_id(self): assert call_args[0][1]["assigneeIds"] == ["BOT_custom"] -# --------------------------------------------------------------------------- -# Copilot login helper tests -# --------------------------------------------------------------------------- - -class TestCopilotLogin: - - def test_default(self): - with patch.dict(os.environ, {}, clear=True): - os.environ.pop("COPILOT_LOGIN", None) - assert _copilot_login() == "copilot-swe-agent" - - @patch.dict(os.environ, {"COPILOT_LOGIN": "my-bot"}) - def test_env_override(self): - assert _copilot_login() == "my-bot" - - -class TestCopilotNodeId: - - def test_default(self): - with patch.dict(os.environ, {}, clear=True): - os.environ.pop("COPILOT_NODE_ID", None) - assert _copilot_node_id() == "BOT_kgDOC9w8XQ" - - @patch.dict(os.environ, {"COPILOT_NODE_ID": "BOT_custom"}) - def test_env_override(self): - assert _copilot_node_id() == "BOT_custom" - - # --------------------------------------------------------------------------- # _is_copilot_already_assigned tests # --------------------------------------------------------------------------- From 98447a27365acff196d9b79ca7397bc3c2524321 Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Tue, 5 May 2026 13:14:40 -0700 Subject: [PATCH 07/18] black --- .../tests/test_vnext_auto_fix.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/eng/tools/azure-sdk-tools/tests/test_vnext_auto_fix.py b/eng/tools/azure-sdk-tools/tests/test_vnext_auto_fix.py index 77913a3bedde..47bb2c25057e 100644 --- a/eng/tools/azure-sdk-tools/tests/test_vnext_auto_fix.py +++ b/eng/tools/azure-sdk-tools/tests/test_vnext_auto_fix.py @@ -35,6 +35,7 @@ # Helpers to build lightweight fakes # --------------------------------------------------------------------------- + def _make_label(name: str) -> SimpleNamespace: return SimpleNamespace(name=name) @@ -79,6 +80,7 @@ def _make_pr( # Eligibility tests # --------------------------------------------------------------------------- + class TestIsAutoFixEligible: """Tests for is_auto_fix_eligible.""" @@ -95,6 +97,7 @@ def test_opt_out_label(self): # Duplicate PR detection tests # --------------------------------------------------------------------------- + class TestFindExistingFixPrs: def test_match_by_issue_ref_in_title(self): @@ -140,6 +143,7 @@ def test_github_exception_returns_empty(self): # Copilot instruction builder tests # --------------------------------------------------------------------------- + class TestBuildCopilotInstructions: @pytest.mark.parametrize("check_type", ["pylint", "mypy", "sphinx", "pyright"]) @@ -157,6 +161,7 @@ def test_contains_required_elements(self, check_type): # Label reconciliation tests # --------------------------------------------------------------------------- + class TestReconcileAutoFixLabels: def test_adds_auto_fix_label(self): @@ -186,6 +191,7 @@ def test_not_eligible_no_op(self): # Copilot assignment tests # --------------------------------------------------------------------------- + class TestAssignCopilot: def test_success(self): @@ -226,6 +232,7 @@ def test_configurable_login_and_node_id(self): # _is_copilot_already_assigned tests # --------------------------------------------------------------------------- + class TestIsCopilotAlreadyAssigned: def test_assigned(self): @@ -245,6 +252,7 @@ def test_case_insensitive(self): # Integration: _try_auto_fix tests # --------------------------------------------------------------------------- + class TestTryAutoFix: def test_eligible_no_duplicate_assigns(self): @@ -283,7 +291,12 @@ def test_opt_out_label_prevents_assignment(self): g = _make_github_instance() _try_auto_fix( - repo, issue, g, "azure-ai-test", "sdk/ai/azure-ai-test", "pylint", + repo, + issue, + g, + "azure-ai-test", + "sdk/ai/azure-ai-test", + "pylint", ["pylint", LABEL_AUTO_FIX_DISABLED], ) @@ -297,8 +310,7 @@ def test_weekly_retry_reassigns_when_no_pr(self): issue = _make_issue(labels=["pylint", LABEL_AUTO_FIX]) g = _make_github_instance() - _try_auto_fix(repo, issue, g, "azure-ai-test", "sdk/ai/azure-ai-test", "pylint", - ["pylint", LABEL_AUTO_FIX]) + _try_auto_fix(repo, issue, g, "azure-ai-test", "sdk/ai/azure-ai-test", "pylint", ["pylint", LABEL_AUTO_FIX]) g._Github__requester.graphql_named_mutation.assert_called_once() From d6882c24c5d3506815dfc57e3630f6ded14acee9 Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Tue, 5 May 2026 13:30:01 -0700 Subject: [PATCH 08/18] minor --- doc/analyze_check_versions.md | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/analyze_check_versions.md b/doc/analyze_check_versions.md index 9ce213687481..62b9c5e54571 100644 --- a/doc/analyze_check_versions.md +++ b/doc/analyze_check_versions.md @@ -11,4 +11,3 @@ MyPy | 1.19.1 | 1.19.1 | 2026-07-13 | Pyright | 1.1.407 | 1.1.407 | 2026-07-13 | Sphinx | 8.2.0 | N/A | N/A | Black | 24.4.0 | N/A | N/A | -Ruff | 0.15.11 | N/A | N/A | From 22ac03cad0b3c022e4118697fdc67c6a640b7474 Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Tue, 5 May 2026 13:44:14 -0700 Subject: [PATCH 09/18] dont comment, graphql fix --- .../gh_tools/vnext_issue_creator.py | 31 +++---------------- 1 file changed, 4 insertions(+), 27 deletions(-) diff --git a/eng/tools/azure-sdk-tools/gh_tools/vnext_issue_creator.py b/eng/tools/azure-sdk-tools/gh_tools/vnext_issue_creator.py index 70a917399f8a..22549d556115 100644 --- a/eng/tools/azure-sdk-tools/gh_tools/vnext_issue_creator.py +++ b/eng/tools/azure-sdk-tools/gh_tools/vnext_issue_creator.py @@ -31,7 +31,6 @@ #: Label constants for auto-fix state management. LABEL_AUTO_FIX = "copilot-auto-fix" -LABEL_AUTO_FIX_FAILED = "copilot-auto-fix-failed" LABEL_AUTO_FIX_DISABLED = "copilot-auto-fix-disabled" #: Managed block markers for Copilot instructions in issue bodies. @@ -105,21 +104,17 @@ def find_existing_fix_prs( def build_copilot_instructions(package_path: str, check_type: str) -> str: """Build the Copilot auto-fix instruction block for the issue body.""" skill_name = f"fix-{check_type}" - validation_cmd = f"azpysdk {check_type} ." inner = ( - f"## Copilot auto-fix request\n\n" + f"## Copilot instructions:\n\n" f"Use the `{skill_name}` skill to resolve `{check_type}` failures " f"in `{package_path}`.\n" f"Do not make unrelated formatting, changelog, version, or " f"generated-code changes.\n\n" - f"Run `{validation_cmd}` from `{package_path}` and attempt to " - f"resolve all outputted errors. Include the final result in the " - f"PR body.\n" f"Open a PR that links this issue and includes:\n\n" f"> Automated Fix: This PR was automatically generated by Copilot " f"in response to a vnext compatibility issue.\n\n" - f"If a safe fix is not possible, leave an issue comment with " + f"If a safe fix is not possible, describe the " f"attempted commands, the failure category, and the recommended " f"manual next step." ) @@ -150,13 +145,6 @@ def reconcile_auto_fix_labels(issue, eligible: bool) -> None: labels_to_add = [] if LABEL_AUTO_FIX not in current_labels: labels_to_add.append(LABEL_AUTO_FIX) - # Remove failure label if present (allows retry) - if LABEL_AUTO_FIX_FAILED in current_labels: - try: - issue.remove_from_labels(LABEL_AUTO_FIX_FAILED) - logging.info(f"Removed {LABEL_AUTO_FIX_FAILED} from issue #{issue.number} for retry") - except GithubException as e: - logging.warning(f"Failed to remove {LABEL_AUTO_FIX_FAILED}: {e}") for label in labels_to_add: try: issue.add_to_labels(label) @@ -191,7 +179,7 @@ def _unassign_copilot(issue, github_instance) -> bool: "assignableId": issue_node_id, "assigneeIds": [node_id], }, - output="assignable { ... on Issue { id } }", + output_schema="assignable { ... on Issue { id } }", ) logging.info(f"Unassigned {login} from issue #{issue.number}") return True @@ -230,23 +218,12 @@ def assign_copilot(issue, github_instance, package_name: str, check_type: str, f "assignableId": issue_node_id, "assigneeIds": [node_id], }, - output="assignable { ... on Issue { id } }", + output_schema="assignable { ... on Issue { id } }", ) logging.info(f"Assigned {login} to issue #{issue.number} for {package_name}/{check_type}") return True except Exception as e: logging.warning(f"Failed to assign {login} to issue #{issue.number}: {e}") - try: - issue.add_to_labels(LABEL_AUTO_FIX_FAILED) - issue.create_comment( - f"⚠️ **Copilot auto-fix assignment failed**\n\n" - f"Could not assign `{login}` to this issue.\n" - f"Error: `{e}`\n\n" - f"Human triage is required. The `{LABEL_AUTO_FIX_FAILED}` label " - f"has been added. Remove it and re-run the vnext pipeline to retry." - ) - except GithubException as comment_err: - logging.warning(f"Failed to add failure label/comment: {comment_err}") return False From b152b77116a0a75fdfda955c60e435c13cc2de2d Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Tue, 5 May 2026 13:56:01 -0700 Subject: [PATCH 10/18] test fix --- .../tests/test_vnext_auto_fix.py | 23 ++++--------------- 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/eng/tools/azure-sdk-tools/tests/test_vnext_auto_fix.py b/eng/tools/azure-sdk-tools/tests/test_vnext_auto_fix.py index 47bb2c25057e..60bddd7ab391 100644 --- a/eng/tools/azure-sdk-tools/tests/test_vnext_auto_fix.py +++ b/eng/tools/azure-sdk-tools/tests/test_vnext_auto_fix.py @@ -18,7 +18,6 @@ DEFAULT_COPILOT_NODE_ID, LABEL_AUTO_FIX, LABEL_AUTO_FIX_DISABLED, - LABEL_AUTO_FIX_FAILED, _copilot_login, _copilot_node_id, _is_copilot_already_assigned, @@ -151,7 +150,6 @@ def test_contains_required_elements(self, check_type): result = build_copilot_instructions("sdk/ai/azure-ai-test", check_type) assert f"fix-{check_type}" in result - assert f"azpysdk {check_type} ." in result assert "sdk/ai/azure-ai-test" in result assert "Automated Fix" in result assert "Do not make unrelated" in result @@ -174,12 +172,6 @@ def test_skips_if_already_labeled(self): reconcile_auto_fix_labels(issue, eligible=True) issue.add_to_labels.assert_not_called() - def test_removes_failed_label_on_retry(self): - issue = _make_issue(labels=["pylint", LABEL_AUTO_FIX_FAILED]) - reconcile_auto_fix_labels(issue, eligible=True) - issue.remove_from_labels.assert_called_once_with(LABEL_AUTO_FIX_FAILED) - issue.add_to_labels.assert_called_once_with(LABEL_AUTO_FIX) - def test_not_eligible_no_op(self): issue = _make_issue(labels=["pylint"]) reconcile_auto_fix_labels(issue, eligible=False) @@ -209,15 +201,11 @@ def test_already_assigned_skips(self): assert assign_copilot(issue, g, "azure-ai-test", "pylint") is True g._Github__requester.graphql_named_mutation.assert_not_called() - def test_failure_adds_label_and_comment(self): + def test_failure_returns_false(self): issue = _make_issue() g = _make_github_instance() g._Github__requester.graphql_named_mutation.side_effect = Exception("mutation failed") assert assign_copilot(issue, g, "azure-ai-test", "pylint") is False - issue.add_to_labels.assert_called_once_with(LABEL_AUTO_FIX_FAILED) - issue.create_comment.assert_called_once() - comment_text = issue.create_comment.call_args[0][0] - assert "auto-fix assignment failed" in comment_text @patch.dict(os.environ, {"COPILOT_LOGIN": "custom-bot", "COPILOT_NODE_ID": "BOT_custom"}) def test_configurable_login_and_node_id(self): @@ -268,7 +256,7 @@ def test_eligible_no_duplicate_assigns(self): # Instructions appended issue.edit.assert_called_once() body_arg = issue.edit.call_args[1]["body"] - assert "Copilot auto-fix request" in body_arg + assert "Copilot instructions" in body_arg # Copilot assigned via GraphQL g._Github__requester.graphql_named_mutation.assert_called_once() @@ -314,7 +302,7 @@ def test_weekly_retry_reassigns_when_no_pr(self): g._Github__requester.graphql_named_mutation.assert_called_once() - def test_assignment_failure_adds_failed_label(self): + def test_assignment_failure_does_not_crash(self): repo = MagicMock() repo.get_pulls.return_value = [] issue = _make_issue(labels=["pylint"]) @@ -323,6 +311,5 @@ def test_assignment_failure_adds_failed_label(self): _try_auto_fix(repo, issue, g, "azure-ai-test", "sdk/ai/azure-ai-test", "pylint", ["pylint"]) - # Should have tried to add the failed label - issue.add_to_labels.assert_any_call(LABEL_AUTO_FIX_FAILED) - issue.create_comment.assert_called_once() + # Should still have reconciled labels before the failed assignment + issue.add_to_labels.assert_any_call(LABEL_AUTO_FIX) From 8b8f1652b68a1a6c9a284384c261259000a1747d Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Tue, 5 May 2026 14:17:31 -0700 Subject: [PATCH 11/18] doc update --- doc/eng_sys_checks.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/eng_sys_checks.md b/doc/eng_sys_checks.md index c774d6800ae1..fc25c691e746 100644 --- a/doc/eng_sys_checks.md +++ b/doc/eng_sys_checks.md @@ -603,8 +603,9 @@ For `pylint`, `mypy`, `sphinx`, and `pyright` failures, the pipeline automatical - **You don't need to do anything** if Copilot opens a PR — just review and merge it like any other PR. - **To opt out**, add the `copilot-auto-fix-disabled` label to the issue. -- **If Copilot fails**, the issue gets a `copilot-auto-fix-failed` label and a comment explaining what happened. Remove the label to allow a retry on the next weekly run. -- **Weekly retry**: if no matching PR exists when the pipeline runs again, Copilot is reassigned automatically. +- **If Copilot fails** to be assigned, the pipeline logs a warning and retries automatically on the next run. +- **Version bumps**: when the checker version changes, Copilot is unassigned and reassigned to trigger a fresh fix attempt with the updated errors. +- **Duplicate detection**: if an open PR already references the issue or mentions the package and check type, Copilot is not reassigned. To test a "next" check locally, use `--next`: From 97c03d744dbda402b314e6cad92cdd5e6fe89f28 Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Tue, 5 May 2026 14:47:41 -0700 Subject: [PATCH 12/18] minor doc update --- doc/eng_sys_checks.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/eng_sys_checks.md b/doc/eng_sys_checks.md index fc25c691e746..7b4834774fca 100644 --- a/doc/eng_sys_checks.md +++ b/doc/eng_sys_checks.md @@ -1,4 +1,4 @@ -# Azure SDK for Python - Engineering System +gi# Azure SDK for Python - Engineering System - [Azure SDK for Python - Engineering System](#azure-sdk-for-python---engineering-system) - [Targeting a specific package at build queue time](#targeting-a-specific-package-at-build-queue-time) @@ -599,9 +599,9 @@ Results are posted as GitHub issues in the repository. These checks run with `co #### Copilot auto-fix -For `pylint`, `mypy`, `sphinx`, and `pyright` failures, the pipeline automatically assigns the Copilot coding agent to open a fix PR. +For `pylint`, `mypy`, `sphinx`, and `pyright` failures, the weekly pipeline automatically assigns the Copilot coding agent to open a fix PR. -- **You don't need to do anything** if Copilot opens a PR — just review and merge it like any other PR. +- **Review the PR**: review and merge it like any other PR. - **To opt out**, add the `copilot-auto-fix-disabled` label to the issue. - **If Copilot fails** to be assigned, the pipeline logs a warning and retries automatically on the next run. - **Version bumps**: when the checker version changes, Copilot is unassigned and reassigned to trigger a fresh fix attempt with the updated errors. From cc13536a1e3411a93baba0366eaded23fe2da6b8 Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Tue, 5 May 2026 15:05:15 -0700 Subject: [PATCH 13/18] dynamically get bot id, and other stuff --- .../gh_tools/vnext_issue_creator.py | 74 ++++++++++++++----- .../tests/test_vnext_auto_fix.py | 35 +++++++-- 2 files changed, 83 insertions(+), 26 deletions(-) diff --git a/eng/tools/azure-sdk-tools/gh_tools/vnext_issue_creator.py b/eng/tools/azure-sdk-tools/gh_tools/vnext_issue_creator.py index 22549d556115..99dc85dff48a 100644 --- a/eng/tools/azure-sdk-tools/gh_tools/vnext_issue_creator.py +++ b/eng/tools/azure-sdk-tools/gh_tools/vnext_issue_creator.py @@ -39,7 +39,7 @@ #: Copilot coding-agent bot login and node ID. #: The node ID was retrieved via the suggestedActors GraphQL query on -#: Azure/azure-sdk-for-python. Override with env vars if needed. +#: Azure/azure-sdk-for-python but could potentially change. DEFAULT_COPILOT_LOGIN = "copilot-swe-agent" DEFAULT_COPILOT_NODE_ID = "BOT_kgDOC9w8XQ" @@ -49,14 +49,54 @@ # --------------------------------------------------------------------------- -def _copilot_login() -> str: - """Return the Copilot assignable login, configurable via env var.""" - return os.getenv("COPILOT_LOGIN", DEFAULT_COPILOT_LOGIN) +def _resolve_copilot_node_id(issue, github_instance) -> str: + """Return the Copilot bot node ID, preferring GitHub's assignable actor data.""" + login = DEFAULT_COPILOT_LOGIN + issue_node_id = issue.raw_data["node_id"] + # dynamically query GitHub for the assignable actor node ID for the Copilot bot + try: + _, data = github_instance._Github__requester.graphql_query( + """ + query($assignableId: ID!, $login: String!) { + node(id: $assignableId) { + ... on Issue { + suggestedActors(first: 10, query: $login) { + nodes { + __typename + ... on Bot { + id + login + } + ... on Mannequin { + id + login + } + ... on Organization { + id + login + } + ... on User { + id + login + } + } + } + } + } + } + """, + {"assignableId": issue_node_id, "login": login}, + ) + actors = data.get("data", {}).get("node", {}).get("suggestedActors", {}).get("nodes", []) + for actor in actors: + if actor.get("login", "").lower() == login.lower() and actor.get("id"): + return actor["id"] + except Exception as e: + logging.warning(f"Failed to resolve Copilot node ID dynamically: {e}") -def _copilot_node_id() -> str: - """Return the Copilot bot node ID, configurable via env var.""" - return os.getenv("COPILOT_NODE_ID", DEFAULT_COPILOT_NODE_ID) + logging.warning("Using fallback Copilot node ID") + return DEFAULT_COPILOT_NODE_ID def is_auto_fix_eligible( @@ -155,10 +195,9 @@ def reconcile_auto_fix_labels(issue, eligible: bool) -> None: def _is_copilot_already_assigned(issue) -> bool: """Check whether the Copilot login is already among the issue assignees.""" - login = _copilot_login() for assignee in issue.assignees: name = assignee.login if hasattr(assignee, "login") else str(assignee) - if name.lower() == login.lower(): + if name.lower() == DEFAULT_COPILOT_LOGIN.lower(): return True return False @@ -169,8 +208,7 @@ def _unassign_copilot(issue, github_instance) -> bool: Uses the GraphQL ``removeAssigneesFromAssignable`` mutation. Treats "not currently assigned" as success (idempotent). """ - login = _copilot_login() - node_id = _copilot_node_id() + node_id = _resolve_copilot_node_id(issue, github_instance) issue_node_id = issue.raw_data["node_id"] try: github_instance._Github__requester.graphql_named_mutation( @@ -181,10 +219,10 @@ def _unassign_copilot(issue, github_instance) -> bool: }, output_schema="assignable { ... on Issue { id } }", ) - logging.info(f"Unassigned {login} from issue #{issue.number}") + logging.info(f"Unassigned {DEFAULT_COPILOT_LOGIN} from issue #{issue.number}") return True except Exception as e: - logging.warning(f"Failed to unassign {login} from issue #{issue.number}: {e}") + logging.warning(f"Failed to unassign {DEFAULT_COPILOT_LOGIN} from issue #{issue.number}: {e}") return False @@ -206,10 +244,10 @@ def assign_copilot(issue, github_instance, package_name: str, check_type: str, f logging.info(f"Copilot already assigned to issue #{issue.number}, skipping") return True logging.info(f"Copilot already assigned to issue #{issue.number}, " f"re-assigning to trigger new session") - _unassign_copilot(issue, github_instance) + if not _unassign_copilot(issue, github_instance): + return False - login = _copilot_login() - node_id = _copilot_node_id() + node_id = _resolve_copilot_node_id(issue, github_instance) issue_node_id = issue.raw_data["node_id"] try: github_instance._Github__requester.graphql_named_mutation( @@ -220,10 +258,10 @@ def assign_copilot(issue, github_instance, package_name: str, check_type: str, f }, output_schema="assignable { ... on Issue { id } }", ) - logging.info(f"Assigned {login} to issue #{issue.number} for {package_name}/{check_type}") + logging.info(f"Assigned {DEFAULT_COPILOT_LOGIN} to issue #{issue.number} for {package_name}/{check_type}") return True except Exception as e: - logging.warning(f"Failed to assign {login} to issue #{issue.number}: {e}") + logging.warning(f"Failed to assign {DEFAULT_COPILOT_LOGIN} to issue #{issue.number}: {e}") return False diff --git a/eng/tools/azure-sdk-tools/tests/test_vnext_auto_fix.py b/eng/tools/azure-sdk-tools/tests/test_vnext_auto_fix.py index 60bddd7ab391..d548ad4496a2 100644 --- a/eng/tools/azure-sdk-tools/tests/test_vnext_auto_fix.py +++ b/eng/tools/azure-sdk-tools/tests/test_vnext_auto_fix.py @@ -7,9 +7,8 @@ from __future__ import annotations -import os from types import SimpleNamespace -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock import pytest from github import GithubException @@ -18,8 +17,6 @@ DEFAULT_COPILOT_NODE_ID, LABEL_AUTO_FIX, LABEL_AUTO_FIX_DISABLED, - _copilot_login, - _copilot_node_id, _is_copilot_already_assigned, _try_auto_fix, assign_copilot, @@ -64,6 +61,10 @@ def _make_github_instance() -> MagicMock: """Create a mock Github instance with a requester that supports graphql_named_mutation.""" g = MagicMock() g._Github__requester = MagicMock() + g._Github__requester.graphql_query.return_value = ( + {}, + {"data": {"node": {"suggestedActors": {"nodes": []}}}}, + ) return g @@ -207,13 +208,31 @@ def test_failure_returns_false(self): g._Github__requester.graphql_named_mutation.side_effect = Exception("mutation failed") assert assign_copilot(issue, g, "azure-ai-test", "pylint") is False - @patch.dict(os.environ, {"COPILOT_LOGIN": "custom-bot", "COPILOT_NODE_ID": "BOT_custom"}) - def test_configurable_login_and_node_id(self): + def test_resolves_node_id_dynamically(self): issue = _make_issue() g = _make_github_instance() + g._Github__requester.graphql_query.return_value = ( + {}, + { + "data": { + "node": { + "suggestedActors": { + "nodes": [{"login": "copilot-swe-agent", "id": "BOT_dynamic"}], + } + } + } + }, + ) assert assign_copilot(issue, g, "azure-ai-test", "pylint") is True call_args = g._Github__requester.graphql_named_mutation.call_args - assert call_args[0][1]["assigneeIds"] == ["BOT_custom"] + assert call_args[0][1]["assigneeIds"] == ["BOT_dynamic"] + + def test_force_reassign_returns_false_when_unassign_fails(self): + issue = _make_issue(assignees=["copilot-swe-agent"]) + g = _make_github_instance() + g._Github__requester.graphql_named_mutation.side_effect = Exception("remove failed") + assert assign_copilot(issue, g, "azure-ai-test", "pylint", force_reassign=True) is False + g._Github__requester.graphql_named_mutation.assert_called_once() # --------------------------------------------------------------------------- @@ -263,7 +282,7 @@ def test_eligible_no_duplicate_assigns(self): def test_eligible_with_duplicate_pr_skips(self): repo = MagicMock() repo.get_pulls.return_value = [ - _make_pr(title="Fix pylint #1"), + _make_pr(body="Fixes #1"), ] issue = _make_issue(number=1, labels=["pylint"]) g = _make_github_instance() From 83757f251db60f6582dab19df56a87a178007cc0 Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Tue, 5 May 2026 15:13:29 -0700 Subject: [PATCH 14/18] always dynamically get bot id --- .../gh_tools/vnext_issue_creator.py | 17 +++++++------- .../tests/test_vnext_auto_fix.py | 23 ++++++++++++++++--- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/eng/tools/azure-sdk-tools/gh_tools/vnext_issue_creator.py b/eng/tools/azure-sdk-tools/gh_tools/vnext_issue_creator.py index 99dc85dff48a..a707c3ef5ddd 100644 --- a/eng/tools/azure-sdk-tools/gh_tools/vnext_issue_creator.py +++ b/eng/tools/azure-sdk-tools/gh_tools/vnext_issue_creator.py @@ -37,11 +37,8 @@ COPILOT_AUTOFIX_START = "" COPILOT_AUTOFIX_END = "" -#: Copilot coding-agent bot login and node ID. -#: The node ID was retrieved via the suggestedActors GraphQL query on -#: Azure/azure-sdk-for-python but could potentially change. +#: Copilot coding-agent bot login. DEFAULT_COPILOT_LOGIN = "copilot-swe-agent" -DEFAULT_COPILOT_NODE_ID = "BOT_kgDOC9w8XQ" # --------------------------------------------------------------------------- @@ -49,8 +46,8 @@ # --------------------------------------------------------------------------- -def _resolve_copilot_node_id(issue, github_instance) -> str: - """Return the Copilot bot node ID, preferring GitHub's assignable actor data.""" +def _resolve_copilot_node_id(issue, github_instance) -> Optional[str]: + """Return the Copilot bot node ID from GitHub's assignable actor data.""" login = DEFAULT_COPILOT_LOGIN issue_node_id = issue.raw_data["node_id"] @@ -95,8 +92,8 @@ def _resolve_copilot_node_id(issue, github_instance) -> str: except Exception as e: logging.warning(f"Failed to resolve Copilot node ID dynamically: {e}") - logging.warning("Using fallback Copilot node ID") - return DEFAULT_COPILOT_NODE_ID + logging.warning(f"Could not find {DEFAULT_COPILOT_LOGIN} in suggested actors for issue #{issue.number}") + return None def is_auto_fix_eligible( @@ -209,6 +206,8 @@ def _unassign_copilot(issue, github_instance) -> bool: Treats "not currently assigned" as success (idempotent). """ node_id = _resolve_copilot_node_id(issue, github_instance) + if not node_id: + return False issue_node_id = issue.raw_data["node_id"] try: github_instance._Github__requester.graphql_named_mutation( @@ -248,6 +247,8 @@ def assign_copilot(issue, github_instance, package_name: str, check_type: str, f return False node_id = _resolve_copilot_node_id(issue, github_instance) + if not node_id: + return False issue_node_id = issue.raw_data["node_id"] try: github_instance._Github__requester.graphql_named_mutation( diff --git a/eng/tools/azure-sdk-tools/tests/test_vnext_auto_fix.py b/eng/tools/azure-sdk-tools/tests/test_vnext_auto_fix.py index d548ad4496a2..896b53ac28e1 100644 --- a/eng/tools/azure-sdk-tools/tests/test_vnext_auto_fix.py +++ b/eng/tools/azure-sdk-tools/tests/test_vnext_auto_fix.py @@ -14,7 +14,6 @@ from github import GithubException from gh_tools.vnext_issue_creator import ( - DEFAULT_COPILOT_NODE_ID, LABEL_AUTO_FIX, LABEL_AUTO_FIX_DISABLED, _is_copilot_already_assigned, @@ -63,7 +62,15 @@ def _make_github_instance() -> MagicMock: g._Github__requester = MagicMock() g._Github__requester.graphql_query.return_value = ( {}, - {"data": {"node": {"suggestedActors": {"nodes": []}}}}, + { + "data": { + "node": { + "suggestedActors": { + "nodes": [{"login": "copilot-swe-agent", "id": "BOT_dynamic"}], + } + } + } + }, ) return g @@ -194,7 +201,7 @@ def test_success(self): g._Github__requester.graphql_named_mutation.assert_called_once() call_args = g._Github__requester.graphql_named_mutation.call_args assert call_args[0][0] == "addAssigneesToAssignable" - assert call_args[0][1]["assigneeIds"] == [DEFAULT_COPILOT_NODE_ID] + assert call_args[0][1]["assigneeIds"] == ["BOT_dynamic"] def test_already_assigned_skips(self): issue = _make_issue(assignees=["copilot-swe-agent"]) @@ -208,6 +215,16 @@ def test_failure_returns_false(self): g._Github__requester.graphql_named_mutation.side_effect = Exception("mutation failed") assert assign_copilot(issue, g, "azure-ai-test", "pylint") is False + def test_returns_false_when_copilot_node_id_not_found(self): + issue = _make_issue() + g = _make_github_instance() + g._Github__requester.graphql_query.return_value = ( + {}, + {"data": {"node": {"suggestedActors": {"nodes": []}}}}, + ) + assert assign_copilot(issue, g, "azure-ai-test", "pylint") is False + g._Github__requester.graphql_named_mutation.assert_not_called() + def test_resolves_node_id_dynamically(self): issue = _make_issue() g = _make_github_instance() From ede5d761f0632fe8a4ada0446a5e7f63607c8e1f Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Tue, 5 May 2026 15:27:07 -0700 Subject: [PATCH 15/18] call get node id once --- .../gh_tools/vnext_issue_creator.py | 29 +++++----- .../tests/test_vnext_auto_fix.py | 53 +++++++------------ 2 files changed, 37 insertions(+), 45 deletions(-) diff --git a/eng/tools/azure-sdk-tools/gh_tools/vnext_issue_creator.py b/eng/tools/azure-sdk-tools/gh_tools/vnext_issue_creator.py index a707c3ef5ddd..9784e83188dc 100644 --- a/eng/tools/azure-sdk-tools/gh_tools/vnext_issue_creator.py +++ b/eng/tools/azure-sdk-tools/gh_tools/vnext_issue_creator.py @@ -199,22 +199,19 @@ def _is_copilot_already_assigned(issue) -> bool: return False -def _unassign_copilot(issue, github_instance) -> bool: +def _unassign_copilot(issue, github_instance, copilot_node_id: str) -> bool: """Remove the Copilot coding agent from the issue assignees. Uses the GraphQL ``removeAssigneesFromAssignable`` mutation. Treats "not currently assigned" as success (idempotent). """ - node_id = _resolve_copilot_node_id(issue, github_instance) - if not node_id: - return False issue_node_id = issue.raw_data["node_id"] try: github_instance._Github__requester.graphql_named_mutation( "removeAssigneesFromAssignable", { "assignableId": issue_node_id, - "assigneeIds": [node_id], + "assigneeIds": [copilot_node_id], }, output_schema="assignable { ... on Issue { id } }", ) @@ -225,7 +222,14 @@ def _unassign_copilot(issue, github_instance) -> bool: return False -def assign_copilot(issue, github_instance, package_name: str, check_type: str, force_reassign: bool = False) -> bool: +def assign_copilot( + issue, + github_instance, + copilot_node_id: str, + package_name: str, + check_type: str, + force_reassign: bool = False, +) -> bool: """Attempt to assign the Copilot coding agent to the issue. Uses the GraphQL ``addAssigneesToAssignable`` mutation because the @@ -243,19 +247,16 @@ def assign_copilot(issue, github_instance, package_name: str, check_type: str, f logging.info(f"Copilot already assigned to issue #{issue.number}, skipping") return True logging.info(f"Copilot already assigned to issue #{issue.number}, " f"re-assigning to trigger new session") - if not _unassign_copilot(issue, github_instance): + if not _unassign_copilot(issue, github_instance, copilot_node_id): return False - node_id = _resolve_copilot_node_id(issue, github_instance) - if not node_id: - return False issue_node_id = issue.raw_data["node_id"] try: github_instance._Github__requester.graphql_named_mutation( "addAssigneesToAssignable", { "assignableId": issue_node_id, - "assigneeIds": [node_id], + "assigneeIds": [copilot_node_id], }, output_schema="assignable { ... on Issue { id } }", ) @@ -300,8 +301,12 @@ def _try_auto_fix( instructions = build_copilot_instructions(package_path, check_type) issue.edit(body=_upsert_copilot_instructions(body, instructions)) + copilot_node_id = _resolve_copilot_node_id(issue, github_instance) + if not copilot_node_id: + return + # Assign Copilot (force reassignment on version bumps) - assign_copilot(issue, github_instance, package_name, check_type, force_reassign=version_changed) + assign_copilot(issue, github_instance, copilot_node_id, package_name, check_type, force_reassign=version_changed) def get_version_running(check_type: CHECK_TYPE) -> str: diff --git a/eng/tools/azure-sdk-tools/tests/test_vnext_auto_fix.py b/eng/tools/azure-sdk-tools/tests/test_vnext_auto_fix.py index 896b53ac28e1..49ad83e23176 100644 --- a/eng/tools/azure-sdk-tools/tests/test_vnext_auto_fix.py +++ b/eng/tools/azure-sdk-tools/tests/test_vnext_auto_fix.py @@ -197,7 +197,7 @@ class TestAssignCopilot: def test_success(self): issue = _make_issue() g = _make_github_instance() - assert assign_copilot(issue, g, "azure-ai-test", "pylint") is True + assert assign_copilot(issue, g, "BOT_dynamic", "azure-ai-test", "pylint") is True g._Github__requester.graphql_named_mutation.assert_called_once() call_args = g._Github__requester.graphql_named_mutation.call_args assert call_args[0][0] == "addAssigneesToAssignable" @@ -206,49 +206,20 @@ def test_success(self): def test_already_assigned_skips(self): issue = _make_issue(assignees=["copilot-swe-agent"]) g = _make_github_instance() - assert assign_copilot(issue, g, "azure-ai-test", "pylint") is True + assert assign_copilot(issue, g, "BOT_dynamic", "azure-ai-test", "pylint") is True g._Github__requester.graphql_named_mutation.assert_not_called() def test_failure_returns_false(self): issue = _make_issue() g = _make_github_instance() g._Github__requester.graphql_named_mutation.side_effect = Exception("mutation failed") - assert assign_copilot(issue, g, "azure-ai-test", "pylint") is False - - def test_returns_false_when_copilot_node_id_not_found(self): - issue = _make_issue() - g = _make_github_instance() - g._Github__requester.graphql_query.return_value = ( - {}, - {"data": {"node": {"suggestedActors": {"nodes": []}}}}, - ) - assert assign_copilot(issue, g, "azure-ai-test", "pylint") is False - g._Github__requester.graphql_named_mutation.assert_not_called() - - def test_resolves_node_id_dynamically(self): - issue = _make_issue() - g = _make_github_instance() - g._Github__requester.graphql_query.return_value = ( - {}, - { - "data": { - "node": { - "suggestedActors": { - "nodes": [{"login": "copilot-swe-agent", "id": "BOT_dynamic"}], - } - } - } - }, - ) - assert assign_copilot(issue, g, "azure-ai-test", "pylint") is True - call_args = g._Github__requester.graphql_named_mutation.call_args - assert call_args[0][1]["assigneeIds"] == ["BOT_dynamic"] + assert assign_copilot(issue, g, "BOT_dynamic", "azure-ai-test", "pylint") is False def test_force_reassign_returns_false_when_unassign_fails(self): issue = _make_issue(assignees=["copilot-swe-agent"]) g = _make_github_instance() g._Github__requester.graphql_named_mutation.side_effect = Exception("remove failed") - assert assign_copilot(issue, g, "azure-ai-test", "pylint", force_reassign=True) is False + assert assign_copilot(issue, g, "BOT_dynamic", "azure-ai-test", "pylint", force_reassign=True) is False g._Github__requester.graphql_named_mutation.assert_called_once() @@ -295,6 +266,7 @@ def test_eligible_no_duplicate_assigns(self): assert "Copilot instructions" in body_arg # Copilot assigned via GraphQL g._Github__requester.graphql_named_mutation.assert_called_once() + g._Github__requester.graphql_query.assert_called_once() def test_eligible_with_duplicate_pr_skips(self): repo = MagicMock() @@ -349,3 +321,18 @@ def test_assignment_failure_does_not_crash(self): # Should still have reconciled labels before the failed assignment issue.add_to_labels.assert_any_call(LABEL_AUTO_FIX) + + def test_missing_copilot_node_id_skips_assignment(self): + repo = MagicMock() + repo.get_pulls.return_value = [] + issue = _make_issue(labels=["pylint"]) + g = _make_github_instance() + g._Github__requester.graphql_query.return_value = ( + {}, + {"data": {"node": {"suggestedActors": {"nodes": []}}}}, + ) + + _try_auto_fix(repo, issue, g, "azure-ai-test", "sdk/ai/azure-ai-test", "pylint", ["pylint"]) + + g._Github__requester.graphql_query.assert_called_once() + g._Github__requester.graphql_named_mutation.assert_not_called() From a5bc5b0416553a222be72fcaa88f8d3997c1dc39 Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Tue, 5 May 2026 15:30:16 -0700 Subject: [PATCH 16/18] add label only if copilot actually got assigned --- .../gh_tools/vnext_issue_creator.py | 17 ++--------------- .../tests/test_vnext_auto_fix.py | 4 ++-- 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/eng/tools/azure-sdk-tools/gh_tools/vnext_issue_creator.py b/eng/tools/azure-sdk-tools/gh_tools/vnext_issue_creator.py index 9784e83188dc..e36be3ecb9df 100644 --- a/eng/tools/azure-sdk-tools/gh_tools/vnext_issue_creator.py +++ b/eng/tools/azure-sdk-tools/gh_tools/vnext_issue_creator.py @@ -65,18 +65,6 @@ def _resolve_copilot_node_id(issue, github_instance) -> Optional[str]: id login } - ... on Mannequin { - id - login - } - ... on Organization { - id - login - } - ... on User { - id - login - } } } } @@ -287,8 +275,6 @@ def _try_auto_fix( if not eligible: return - reconcile_auto_fix_labels(issue, eligible=True) - # Duplicate PR detection matching_prs = find_existing_fix_prs(repo, issue.number, package_name, check_type) if matching_prs: @@ -306,7 +292,8 @@ def _try_auto_fix( return # Assign Copilot (force reassignment on version bumps) - assign_copilot(issue, github_instance, copilot_node_id, package_name, check_type, force_reassign=version_changed) + if assign_copilot(issue, github_instance, copilot_node_id, package_name, check_type, force_reassign=version_changed): + reconcile_auto_fix_labels(issue, eligible=True) def get_version_running(check_type: CHECK_TYPE) -> str: diff --git a/eng/tools/azure-sdk-tools/tests/test_vnext_auto_fix.py b/eng/tools/azure-sdk-tools/tests/test_vnext_auto_fix.py index 49ad83e23176..9579b68b94db 100644 --- a/eng/tools/azure-sdk-tools/tests/test_vnext_auto_fix.py +++ b/eng/tools/azure-sdk-tools/tests/test_vnext_auto_fix.py @@ -319,8 +319,7 @@ def test_assignment_failure_does_not_crash(self): _try_auto_fix(repo, issue, g, "azure-ai-test", "sdk/ai/azure-ai-test", "pylint", ["pylint"]) - # Should still have reconciled labels before the failed assignment - issue.add_to_labels.assert_any_call(LABEL_AUTO_FIX) + issue.add_to_labels.assert_not_called() def test_missing_copilot_node_id_skips_assignment(self): repo = MagicMock() @@ -336,3 +335,4 @@ def test_missing_copilot_node_id_skips_assignment(self): g._Github__requester.graphql_query.assert_called_once() g._Github__requester.graphql_named_mutation.assert_not_called() + issue.add_to_labels.assert_not_called() From a258046bbc3e63c44f5748a8c90409bf25b8b1d9 Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Tue, 5 May 2026 15:34:45 -0700 Subject: [PATCH 17/18] black --- eng/tools/azure-sdk-tools/gh_tools/vnext_issue_creator.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/eng/tools/azure-sdk-tools/gh_tools/vnext_issue_creator.py b/eng/tools/azure-sdk-tools/gh_tools/vnext_issue_creator.py index e36be3ecb9df..5a635883ec3f 100644 --- a/eng/tools/azure-sdk-tools/gh_tools/vnext_issue_creator.py +++ b/eng/tools/azure-sdk-tools/gh_tools/vnext_issue_creator.py @@ -292,7 +292,9 @@ def _try_auto_fix( return # Assign Copilot (force reassignment on version bumps) - if assign_copilot(issue, github_instance, copilot_node_id, package_name, check_type, force_reassign=version_changed): + if assign_copilot( + issue, github_instance, copilot_node_id, package_name, check_type, force_reassign=version_changed + ): reconcile_auto_fix_labels(issue, eligible=True) From 195a3825e9e82a7efcc5c9456f2efcd18ba6b302 Mon Sep 17 00:00:00 2001 From: jenny <63012604+JennyPng@users.noreply.github.com> Date: Tue, 5 May 2026 16:00:34 -0700 Subject: [PATCH 18/18] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- doc/eng_sys_checks.md | 2 +- .../gh_tools/vnext_issue_creator.py | 14 +++++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/doc/eng_sys_checks.md b/doc/eng_sys_checks.md index 7b4834774fca..447ff1c81eda 100644 --- a/doc/eng_sys_checks.md +++ b/doc/eng_sys_checks.md @@ -1,4 +1,4 @@ -gi# Azure SDK for Python - Engineering System +# Azure SDK for Python - Engineering System - [Azure SDK for Python - Engineering System](#azure-sdk-for-python---engineering-system) - [Targeting a specific package at build queue time](#targeting-a-specific-package-at-build-queue-time) diff --git a/eng/tools/azure-sdk-tools/gh_tools/vnext_issue_creator.py b/eng/tools/azure-sdk-tools/gh_tools/vnext_issue_creator.py index 5a635883ec3f..2418036bf514 100644 --- a/eng/tools/azure-sdk-tools/gh_tools/vnext_issue_creator.py +++ b/eng/tools/azure-sdk-tools/gh_tools/vnext_issue_creator.py @@ -228,7 +228,7 @@ def assign_copilot( agent is first unassigned then reassigned so that a new Copilot session is triggered (e.g. after a checker version bump). - Returns True on success, False on failure (labels/comments the issue). + Returns True on success, False on failure after logging a warning. """ if _is_copilot_already_assigned(issue): if not force_reassign: @@ -285,7 +285,15 @@ def _try_auto_fix( # Upsert Copilot instructions (replace existing block or append) body = issue.body or "" instructions = build_copilot_instructions(package_path, check_type) - issue.edit(body=_upsert_copilot_instructions(body, instructions)) + updated_body = _upsert_copilot_instructions(body, instructions) + try: + issue.edit(body=updated_body) + except GithubException as exc: + logging.warning( + "Failed to update Copilot instructions for issue #%s: %s", + issue.number, + exc, + ) copilot_node_id = _resolve_copilot_node_id(issue, github_instance) if not copilot_node_id: @@ -330,7 +338,7 @@ def get_build_link(check_type: CHECK_TYPE) -> str: ) -def get_merge_dates(year: int) -> typing.List[datetime.datetime]: +def get_merge_dates(year: int) -> typing.List[datetime.date]: """We'll merge the latest version of the type checker/linter quarterly on the Monday after release week. This function returns those 4 Mondays for the given year.