From d64fc8d4db0f9cc2a2a6e6d15cf576f36edf9ee6 Mon Sep 17 00:00:00 2001 From: l0lawrence Date: Thu, 23 Apr 2026 16:03:01 -0700 Subject: [PATCH 1/5] Enable verifytypes for azure-ai-transcription Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- sdk/transcription/azure-ai-transcription/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/transcription/azure-ai-transcription/pyproject.toml b/sdk/transcription/azure-ai-transcription/pyproject.toml index f9b70f2fd467..715895dfa4a2 100644 --- a/sdk/transcription/azure-ai-transcription/pyproject.toml +++ b/sdk/transcription/azure-ai-transcription/pyproject.toml @@ -61,4 +61,4 @@ exclude = [ pytyped = ["py.typed"] [tool.azure-sdk-build] -verifytypes = false +verifytypes = true From 8b46a038fb9dbc1cb4c261425db70ba59cb2ea4d Mon Sep 17 00:00:00 2001 From: Libba Lawrence Date: Tue, 7 Jul 2026 09:00:51 -0700 Subject: [PATCH 2/5] Skip vnext issue creation for deprecated packages Deprecated/inactive packages (Development Status :: 7 - Inactive) that still have a tests-weekly pipeline were getting next-* issues auto-created. Add a deprecation guard in create_vnext_issue that skips issue creation and closes any existing issue for inactive packages. Fixes #39143 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../gh_tools/vnext_issue_creator.py | 21 ++++++ .../tests/test_vnext_issue_creator.py | 66 +++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 eng/tools/azure-sdk-tools/tests/test_vnext_issue_creator.py 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..df45b085cef0 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 @@ -20,12 +20,26 @@ from github import Github, Auth, GithubException from ci_tools.variables import discover_repo_root +from ci_tools.parsing import ParsedSetup +from ci_tools.functions import is_package_active logging.getLogger().setLevel(logging.INFO) CHECK_TYPE = Literal["mypy", "pylint", "pyright", "sphinx"] +def is_package_deprecated(package_dir: str) -> bool: + """Returns True if the package is deprecated/inactive (e.g. carries the + 'Development Status :: 7 - Inactive' classifier). Deprecated packages should + not have vnext issues created against them.""" + try: + parsed = ParsedSetup.from_path(package_dir) + except Exception as e: # pragma: no cover - defensive + logging.warning(f"Unable to parse metadata for {package_dir} to determine deprecation status: {e}") + return False + return not is_package_active(parsed) + + def get_version_running(check_type: CHECK_TYPE) -> str: commands = [sys.executable, "-m", check_type, "--version"] version = subprocess.run( @@ -148,6 +162,13 @@ def create_vnext_issue(package_dir: str, check_type: CHECK_TYPE, check_version: package_path = pathlib.Path(package_dir) package_name = package_path.name service_directory = package_path.parent.name + + # Deprecated/inactive packages should never have vnext issues created against them. + if is_package_deprecated(package_dir): + logging.info(f"Package {package_name} is deprecated/inactive. Skipping vnext issue creation for {check_type}.") + close_vnext_issue(package_name, check_type) + return + auth = Auth.Token(os.environ["GH_TOKEN"]) g = Github(auth=auth) diff --git a/eng/tools/azure-sdk-tools/tests/test_vnext_issue_creator.py b/eng/tools/azure-sdk-tools/tests/test_vnext_issue_creator.py new file mode 100644 index 000000000000..6da99797a7c5 --- /dev/null +++ b/eng/tools/azure-sdk-tools/tests/test_vnext_issue_creator.py @@ -0,0 +1,66 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from unittest import mock + +import pytest + +from gh_tools import vnext_issue_creator + + +class _FakeParsedSetup: + def __init__(self, name, classifiers): + self.name = name + self.classifiers = classifiers + + +@pytest.mark.parametrize( + "classifiers, expected_deprecated", + [ + (["Development Status :: 5 - Production/Stable"], False), + (["Development Status :: 4 - Beta"], False), + (["Development Status :: 7 - Inactive"], True), + (["Development Status :: 5 - Production/Stable", "Development Status :: 7 - Inactive"], True), + ], +) +def test_is_package_deprecated(classifiers, expected_deprecated): + parsed = _FakeParsedSetup("azure-mgmt-fake", classifiers) + with mock.patch.object(vnext_issue_creator.ParsedSetup, "from_path", return_value=parsed): + assert vnext_issue_creator.is_package_deprecated("some/path") is expected_deprecated + + +def test_is_package_deprecated_parse_failure_returns_false(): + with mock.patch.object(vnext_issue_creator.ParsedSetup, "from_path", side_effect=RuntimeError("boom")): + assert vnext_issue_creator.is_package_deprecated("some/path") is False + + +def test_create_vnext_issue_skips_and_closes_for_deprecated_package(): + with mock.patch.object(vnext_issue_creator, "is_package_deprecated", return_value=True) as mock_deprecated, \ + mock.patch.object(vnext_issue_creator, "close_vnext_issue") as mock_close, \ + mock.patch.object(vnext_issue_creator, "Github") as mock_github: + vnext_issue_creator.create_vnext_issue("sdk/mixedreality/azure-mgmt-mixedreality", "mypy") + + mock_deprecated.assert_called_once() + mock_close.assert_called_once_with("azure-mgmt-mixedreality", "mypy") + # No GitHub client should be constructed when short-circuiting on a deprecated package. + mock_github.assert_not_called() + + +def test_create_vnext_issue_proceeds_for_active_package(): + with mock.patch.object(vnext_issue_creator, "is_package_deprecated", return_value=False), \ + mock.patch.object(vnext_issue_creator, "close_vnext_issue") as mock_close, \ + mock.patch.dict("os.environ", {"GH_TOKEN": "fake-token"}), \ + mock.patch.object(vnext_issue_creator, "Github") as mock_github: + repo = mock.MagicMock() + repo.get_issues.return_value = [] + mock_github.return_value.get_repo.return_value = repo + with mock.patch.object(vnext_issue_creator, "get_labels", return_value=([], [])), \ + mock.patch.object(vnext_issue_creator, "get_build_link", return_value="http://build"), \ + mock.patch.object(vnext_issue_creator, "get_date_for_version_bump", return_value="2026-04-13"): + vnext_issue_creator.create_vnext_issue("sdk/fake/azure-mgmt-fake", "mypy", check_version="1.0.0") + + # Active package: we do not close an issue, we create one. + mock_close.assert_not_called() + repo.create_issue.assert_called_once() From 0c416a5ecccecf77323e92a8b3e5ae8d11438811 Mon Sep 17 00:00:00 2001 From: Libba Lawrence Date: Tue, 7 Jul 2026 09:18:26 -0700 Subject: [PATCH 3/5] Revert unrelated azure-ai-transcription pyproject.toml change Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- sdk/transcription/azure-ai-transcription/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/transcription/azure-ai-transcription/pyproject.toml b/sdk/transcription/azure-ai-transcription/pyproject.toml index 715895dfa4a2..f9b70f2fd467 100644 --- a/sdk/transcription/azure-ai-transcription/pyproject.toml +++ b/sdk/transcription/azure-ai-transcription/pyproject.toml @@ -61,4 +61,4 @@ exclude = [ pytyped = ["py.typed"] [tool.azure-sdk-build] -verifytypes = true +verifytypes = false From 44bebcd3e7f8352a8c071946bca99d579db25aff Mon Sep 17 00:00:00 2001 From: Libba Lawrence Date: Tue, 7 Jul 2026 09:23:56 -0700 Subject: [PATCH 4/5] Apply black formatting to test file Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../tests/test_vnext_issue_creator.py | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/eng/tools/azure-sdk-tools/tests/test_vnext_issue_creator.py b/eng/tools/azure-sdk-tools/tests/test_vnext_issue_creator.py index 6da99797a7c5..b1e21c52363e 100644 --- a/eng/tools/azure-sdk-tools/tests/test_vnext_issue_creator.py +++ b/eng/tools/azure-sdk-tools/tests/test_vnext_issue_creator.py @@ -37,9 +37,11 @@ def test_is_package_deprecated_parse_failure_returns_false(): def test_create_vnext_issue_skips_and_closes_for_deprecated_package(): - with mock.patch.object(vnext_issue_creator, "is_package_deprecated", return_value=True) as mock_deprecated, \ - mock.patch.object(vnext_issue_creator, "close_vnext_issue") as mock_close, \ - mock.patch.object(vnext_issue_creator, "Github") as mock_github: + with mock.patch.object( + vnext_issue_creator, "is_package_deprecated", return_value=True + ) as mock_deprecated, mock.patch.object(vnext_issue_creator, "close_vnext_issue") as mock_close, mock.patch.object( + vnext_issue_creator, "Github" + ) as mock_github: vnext_issue_creator.create_vnext_issue("sdk/mixedreality/azure-mgmt-mixedreality", "mypy") mock_deprecated.assert_called_once() @@ -49,16 +51,17 @@ def test_create_vnext_issue_skips_and_closes_for_deprecated_package(): def test_create_vnext_issue_proceeds_for_active_package(): - with mock.patch.object(vnext_issue_creator, "is_package_deprecated", return_value=False), \ - mock.patch.object(vnext_issue_creator, "close_vnext_issue") as mock_close, \ - mock.patch.dict("os.environ", {"GH_TOKEN": "fake-token"}), \ - mock.patch.object(vnext_issue_creator, "Github") as mock_github: + with mock.patch.object(vnext_issue_creator, "is_package_deprecated", return_value=False), mock.patch.object( + vnext_issue_creator, "close_vnext_issue" + ) as mock_close, mock.patch.dict("os.environ", {"GH_TOKEN": "fake-token"}), mock.patch.object( + vnext_issue_creator, "Github" + ) as mock_github: repo = mock.MagicMock() repo.get_issues.return_value = [] mock_github.return_value.get_repo.return_value = repo - with mock.patch.object(vnext_issue_creator, "get_labels", return_value=([], [])), \ - mock.patch.object(vnext_issue_creator, "get_build_link", return_value="http://build"), \ - mock.patch.object(vnext_issue_creator, "get_date_for_version_bump", return_value="2026-04-13"): + with mock.patch.object(vnext_issue_creator, "get_labels", return_value=([], [])), mock.patch.object( + vnext_issue_creator, "get_build_link", return_value="http://build" + ), mock.patch.object(vnext_issue_creator, "get_date_for_version_bump", return_value="2026-04-13"): vnext_issue_creator.create_vnext_issue("sdk/fake/azure-mgmt-fake", "mypy", check_version="1.0.0") # Active package: we do not close an issue, we create one. From f991bbadc104b4623fd4c5e175c179e50f2338d9 Mon Sep 17 00:00:00 2001 From: Libba Lawrence Date: Tue, 7 Jul 2026 11:42:04 -0700 Subject: [PATCH 5/5] Fix duplicate vnext issues from automation identity change The dedup lookup filtered on creator=azure-sdk, but the automation now runs as the azure-sdk-automation[bot] GitHub App. That filter never matched the bot's existing issues, so a new duplicate was created on every run. Match issues from all known automation creators (azure-sdk and azure-sdk-automation[bot]) via a shared find_vnext_issues helper, update the newest existing issue, and close any older duplicates. close_vnext_issue now closes all matching duplicates. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../gh_tools/vnext_issue_creator.py | 45 ++++++++--- .../tests/test_vnext_issue_creator.py | 74 +++++++++++++++++++ 2 files changed, 109 insertions(+), 10 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 df45b085cef0..6396e1cadca2 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 @@ -5,7 +5,7 @@ # This script is used to create issues for client libraries failing the vnext of mypy, pyright, and pylint. from __future__ import annotations -from typing import Optional +from typing import Optional, TYPE_CHECKING import sys import os @@ -23,10 +23,31 @@ from ci_tools.parsing import ParsedSetup from ci_tools.functions import is_package_active +if TYPE_CHECKING: + from github.Repository import Repository + logging.getLogger().setLevel(logging.INFO) CHECK_TYPE = Literal["mypy", "pylint", "pyright", "sphinx"] +# The automation that files vnext issues has run under different identities over time +# (the `azure-sdk` user account and the `azure-sdk-automation[bot]` GitHub App). We match +# issues from any known automation creator so existing issues are correctly found and +# de-duplicated regardless of which identity created them. Filtering by creator (rather +# than title alone) avoids ever editing/closing an issue a human happened to open. +VNEXT_ISSUE_CREATORS = ["azure-sdk", "azure-sdk-automation[bot]"] + + +def find_vnext_issues(repo: "Repository", check_type: CHECK_TYPE, package_name: str) -> list: + """Return all open vnext issues for the given package and check_type, across every + known automation creator identity, sorted oldest-first (by issue number).""" + matches = [ + issue + for issue in repo.get_issues(state="open", labels=[check_type]) + if issue.title.split("needs")[0].strip() == package_name and issue.user.login in VNEXT_ISSUE_CREATORS + ] + return sorted(matches, key=lambda issue: issue.number) + def is_package_deprecated(package_dir: str) -> bool: """Returns True if the package is deprecated/inactive (e.g. carries the @@ -175,8 +196,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") - vnext_issue = [issue for issue in issues if issue.title.split("needs")[0].strip() == package_name] + vnext_issue = find_vnext_issues(repo, check_type, package_name) version = check_version or get_version_running(check_type) build_link = get_build_link(check_type) @@ -240,7 +260,13 @@ def create_vnext_issue(package_dir: str, check_type: CHECK_TYPE, check_version: labels = [] assignees = [] - vnext_issue[0].edit( + # Update the most recent issue and close any older duplicates so we converge on a single issue. + primary_issue = vnext_issue[-1] + for duplicate in vnext_issue[:-1]: + logging.info(f"Closing duplicate vnext issue #{duplicate.number} for {package_name} ({check_type}).") + duplicate.edit(state="closed") + + primary_issue.edit( title=title, body=template, ) @@ -248,7 +274,7 @@ def create_vnext_issue(package_dir: str, check_type: CHECK_TYPE, check_version: # Assign codeowners individually with error handling for assignee in assignees: try: - vnext_issue[0].add_to_assignees(assignee) + primary_issue.add_to_assignees(assignee) 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}") @@ -262,8 +288,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") - 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}...") - vnext_issue[0].edit(state="closed") + vnext_issues = find_vnext_issues(repo, check_type, package_name) + for issue in vnext_issues: + logging.info(f"{package_name} passes {check_type}. Closing existing GH issue #{issue.number}...") + issue.edit(state="closed") diff --git a/eng/tools/azure-sdk-tools/tests/test_vnext_issue_creator.py b/eng/tools/azure-sdk-tools/tests/test_vnext_issue_creator.py index b1e21c52363e..1e8ed3eb504d 100644 --- a/eng/tools/azure-sdk-tools/tests/test_vnext_issue_creator.py +++ b/eng/tools/azure-sdk-tools/tests/test_vnext_issue_creator.py @@ -16,6 +16,16 @@ def __init__(self, name, classifiers): self.classifiers = classifiers +class _FakeIssue: + def __init__(self, number, title, login): + self.number = number + self.title = title + self.user = mock.MagicMock() + self.user.login = login + self.edit = mock.MagicMock() + self.add_to_assignees = mock.MagicMock() + + @pytest.mark.parametrize( "classifiers, expected_deprecated", [ @@ -67,3 +77,67 @@ def test_create_vnext_issue_proceeds_for_active_package(): # Active package: we do not close an issue, we create one. mock_close.assert_not_called() repo.create_issue.assert_called_once() + + +def test_find_vnext_issues_matches_all_automation_creators_and_ignores_others(): + repo = mock.MagicMock() + repo.get_issues.return_value = [ + _FakeIssue(100, "azure-ai-textanalytics needs typing updates for mypy version 1.19.1", "azure-sdk"), + _FakeIssue( + 200, "azure-ai-textanalytics needs typing updates for mypy version 1.19.1", "azure-sdk-automation[bot]" + ), + # Same title but opened by a human -> must be ignored. + _FakeIssue(300, "azure-ai-textanalytics needs typing updates for mypy version 1.19.1", "some-human"), + # Different package -> must be ignored. + _FakeIssue(400, "azure-core needs typing updates for mypy version 1.19.1", "azure-sdk"), + ] + + result = vnext_issue_creator.find_vnext_issues(repo, "mypy", "azure-ai-textanalytics") + + assert [issue.number for issue in result] == [100, 200] + + +def test_close_vnext_issue_closes_all_duplicates(): + dup_a = _FakeIssue(100, "azure-ai-textanalytics needs typing updates for mypy version 1.19.1", "azure-sdk") + dup_b = _FakeIssue( + 200, "azure-ai-textanalytics needs typing updates for mypy version 1.19.1", "azure-sdk-automation[bot]" + ) + repo = mock.MagicMock() + with mock.patch.dict("os.environ", {"GH_TOKEN": "fake-token"}), mock.patch.object( + vnext_issue_creator, "Github" + ) as mock_github, mock.patch.object(vnext_issue_creator, "find_vnext_issues", return_value=[dup_a, dup_b]): + mock_github.return_value.get_repo.return_value = repo + vnext_issue_creator.close_vnext_issue("azure-ai-textanalytics", "mypy") + + dup_a.edit.assert_called_once_with(state="closed") + dup_b.edit.assert_called_once_with(state="closed") + + +def test_create_vnext_issue_updates_newest_and_closes_older_duplicates(): + dup_a = _FakeIssue(100, "azure-ai-textanalytics needs typing updates for mypy version 1.19.1", "azure-sdk") + dup_b = _FakeIssue( + 200, "azure-ai-textanalytics needs typing updates for mypy version 1.19.1", "azure-sdk-automation[bot]" + ) + repo = mock.MagicMock() + with mock.patch.object(vnext_issue_creator, "is_package_deprecated", return_value=False), mock.patch.dict( + "os.environ", {"GH_TOKEN": "fake-token"} + ), mock.patch.object(vnext_issue_creator, "Github") as mock_github, mock.patch.object( + vnext_issue_creator, "find_vnext_issues", return_value=[dup_a, dup_b] + ), mock.patch.object( + vnext_issue_creator, "get_labels", return_value=([], []) + ), mock.patch.object( + vnext_issue_creator, "get_build_link", return_value="http://build" + ), mock.patch.object( + vnext_issue_creator, "get_date_for_version_bump", return_value="2026-04-13" + ): + mock_github.return_value.get_repo.return_value = repo + vnext_issue_creator.create_vnext_issue( + "sdk/textanalytics/azure-ai-textanalytics", "mypy", check_version="1.19.1" + ) + + # No new issue created when duplicates already exist. + repo.create_issue.assert_not_called() + # Older duplicate is closed; newest is updated (edited but not closed). + dup_a.edit.assert_called_once_with(state="closed") + dup_b.edit.assert_called_once() + assert dup_b.edit.call_args.kwargs.get("state") != "closed"