This is an automated email from the ASF dual-hosted git repository.

vatsrahul1001 pushed a commit to branch v3-2-test
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit e97f41091b36c03839d1c8bd304e80eb4060286f
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Mon May 25 19:13:18 2026 +0530

    [v3-2-test] CI: fix milestone-tag-assistant race when labels change 
post-merge (#67337) (#67468)
    
    The `milestone-tag-assistant.yml` workflow snapshots PR labels at the
    `get-pr-info` job (via `listPullRequestsAssociatedWithCommit`) and then
    spends ~1.5 minutes installing Breeze and running `breeze ci
    set-milestone`. If a maintainer adds and removes a backport label
    inside that window, the action commits to the stale-snapshot decision
    and sets the wrong milestone — see the incident on PR #67301 where a
    backport label that lived for 49 seconds caused an Airflow-3.2.3
    milestone to be set on a `main`-only documentation PR.
    
    Re-read `issue.labels` from the freshly-fetched issue before computing
    the milestone. If the labels changed since the snapshot:
    
    - Honour any skip label that appeared after the snapshot.
    - Re-run `_determine_milestone_version` with the current labels and
      use the fresh decision; if the decision flips to "no milestone",
      bail out before posting the comment.
    
    Adds three regression tests covering the three race-window cases
    (backport label removed, replaced, skip label added) and updates two
    existing happy-path tests to populate `mock_issue.labels` so the
    re-read sees the same labels as the snapshot.
    (cherry picked from commit 6ecae6853e650fbcf8a67225eec8915eb91523f5)
    
    Co-authored-by: Jarek Potiuk <[email protected]>
---
 .../src/airflow_breeze/commands/ci_commands.py     |  34 +++++
 dev/breeze/tests/test_set_milestone.py             | 147 +++++++++++++++++++++
 2 files changed, 181 insertions(+)

diff --git a/dev/breeze/src/airflow_breeze/commands/ci_commands.py 
b/dev/breeze/src/airflow_breeze/commands/ci_commands.py
index 1e3746f410f..c81620252b0 100644
--- a/dev/breeze/src/airflow_breeze/commands/ci_commands.py
+++ b/dev/breeze/src/airflow_breeze/commands/ci_commands.py
@@ -1283,6 +1283,40 @@ def set_milestone(
         console_print(f"[error]Failed to check existing milestone: {e}[/]")
         return
 
+    # Re-read labels from the freshly-fetched issue to close the race between
+    # the workflow's initial label snapshot (taken by the get-pr-info job a
+    # couple of minutes ago) and the actual milestone-set step. Maintainers
+    # sometimes add and remove a backport label inside that window; honour
+    # the latest state, not the stale snapshot.
+    try:
+        current_labels = [label.name for label in issue.labels]
+    except Exception as e:
+        console_print(f"[warning]Could not re-read PR labels; falling back to 
snapshot decision: {e}[/]")
+        current_labels = labels
+
+    if set(current_labels) != set(labels):
+        console_print("[info]Labels changed since workflow snapshot; 
re-evaluating.[/]")
+        console_print(f"[info]Snapshot labels: {sorted(labels)}[/]")
+        console_print(f"[info]Current labels:  {sorted(current_labels)}[/]")
+
+        if _should_skip_milestone_tagging(current_labels):
+            console_print(
+                f"[info]Skipping milestone tagging - PR now has skip label(s): 
"
+                f"{set(current_labels) & MILESTONE_SKIP_LABELS}[/]"
+            )
+            return
+
+        new_version, new_reason = _determine_milestone_version(current_labels, 
pr_title, base_branch)
+        if (new_version, new_reason) != (version, reason):
+            console_print(
+                f"[info]Determination changed after re-read: was ({version}, 
{reason!r}); "
+                f"now ({new_version}, {new_reason!r}). Using current 
labels.[/]"
+            )
+            version, reason = new_version, new_reason
+            if version is None:
+                console_print(f"[info]No milestone to set after re-evaluation: 
{reason}[/]")
+                return
+
     major, minor = version
     milestone_prefix = _get_milestone_prefix(major, minor)
     console_print(f"[info]Looking for milestone with prefix: 
{milestone_prefix}[/]")
diff --git a/dev/breeze/tests/test_set_milestone.py 
b/dev/breeze/tests/test_set_milestone.py
index 78aa8f19325..7c1f8b433ff 100644
--- a/dev/breeze/tests/test_set_milestone.py
+++ b/dev/breeze/tests/test_set_milestone.py
@@ -38,6 +38,13 @@ from airflow_breeze.commands.ci_commands import (
 )
 
 
+def _label(name: str) -> MagicMock:
+    """Build a mock that quacks like a PyGithub ``Label`` for 
``issue.labels``."""
+    m = MagicMock()
+    m.name = name
+    return m
+
+
 class TestParseVersionFromBranch:
     """Test cases for _parse_version_from_branch."""
 
@@ -420,6 +427,8 @@ class TestSetMilestoneCommand:
 
         mock_gh, mock_repo, mock_issue = mock_github_setup
         mock_issue.milestone = None
+        # Fresh-issue labels match the workflow snapshot — no race, no 
re-evaluation.
+        mock_issue.labels = [_label(name) for name in pr_labels]
         mock_milestone = MagicMock()
         mock_milestone.title = milestone_title
         mock_milestone.number = 42
@@ -530,6 +539,8 @@ If this milestone is not correct, please update it to the 
appropriate milestone.
 
         mock_gh, mock_repo, mock_issue = mock_github_setup
         mock_issue.milestone = None
+        # Fresh-issue labels match the workflow snapshot — no race, no 
re-evaluation.
+        mock_issue.labels = [_label(name) for name in pr_labels]
         captured_comments: list[str] = []
         mock_issue.create_comment.side_effect = lambda c: 
captured_comments.append(c)
 
@@ -571,3 +582,139 @@ However, **no open milestone was found** matching: 
{expected_search_criteria}
 """
         assert captured_comments[0] == expected_comment
         assert "No open milestone found" in result.output
+
+    @patch("airflow_breeze.commands.ci_commands._get_github_client")
+    def test_backport_label_removed_after_snapshot_should_skip(
+        self, mock_get_client, cli_runner, mock_github_setup
+    ):
+        """If a backport label is removed between the workflow snapshot and 
the action,
+        the action must re-read labels from the issue and honour the current 
state —
+        skip milestone-set when the only signal that triggered it (the 
backport label)
+        is gone. Regression test for PR #67301 race.
+        """
+        from airflow_breeze.commands.ci_commands import ci_group
+
+        mock_gh, mock_repo, mock_issue = mock_github_setup
+        mock_issue.milestone = None
+        mock_issue.labels = [_label("kind:documentation")]
+        mock_get_client.return_value = mock_gh
+
+        result = cli_runner.invoke(
+            ci_group,
+            [
+                "set-milestone",
+                "--pr-number",
+                "67301",
+                "--pr-title",
+                "fix: typo",
+                "--pr-labels",
+                json.dumps(["backport-to-v3-2-test", "kind:documentation"]),
+                "--base-branch",
+                "main",
+                "--merged-by",
+                "shahar1",
+                "--github-token",
+                "fake-token",
+                "--github-repository",
+                "apache/airflow",
+            ],
+        )
+
+        # Snapshot still has the backport label, but the fresh issue.labels 
does not.
+        # The action must re-read, notice the change, and skip the 
milestone-set.
+        mock_issue.edit.assert_not_called()
+        mock_issue.create_comment.assert_not_called()
+        assert "Labels changed since workflow snapshot" in result.output
+        assert "No milestone to set after re-evaluation" in result.output
+        assert result.exit_code == 0
+
+    @patch("airflow_breeze.commands.ci_commands._get_github_client")
+    def test_backport_label_changed_after_snapshot_should_use_current(
+        self, mock_get_client, cli_runner, mock_github_setup
+    ):
+        """If the backport label is replaced with a different version between
+        snapshot and action (e.g. someone fixes the version target), the action
+        must re-determine the version using the current label, not the stale 
one.
+        """
+        from airflow_breeze.commands.ci_commands import ci_group
+
+        mock_gh, mock_repo, mock_issue = mock_github_setup
+        mock_issue.milestone = None
+        # Fresh state: now targets v3-2-test, not v3-1-test.
+        mock_issue.labels = [_label("backport-to-v3-2-test"), 
_label("kind:bug")]
+        mock_milestone = MagicMock()
+        mock_milestone.title = "Airflow 3.2.3"
+        mock_milestone.number = 140
+        mock_get_client.return_value = mock_gh
+        mock_repo.get_milestones.return_value = [mock_milestone]
+
+        captured_comments: list[str] = []
+        mock_issue.create_comment.side_effect = lambda c: 
captured_comments.append(c)
+
+        result = cli_runner.invoke(
+            ci_group,
+            [
+                "set-milestone",
+                "--pr-number",
+                "12345",
+                "--pr-title",
+                "Fix: scheduler issue",
+                "--pr-labels",
+                json.dumps(["backport-to-v3-1-test", "kind:bug"]),
+                "--base-branch",
+                "main",
+                "--merged-by",
+                "testuser",
+                "--github-token",
+                "fake-token",
+                "--github-repository",
+                "apache/airflow",
+            ],
+        )
+
+        mock_issue.edit.assert_called_once_with(milestone=mock_milestone)
+        assert "Labels changed since workflow snapshot" in result.output
+        assert "Determination changed after re-read" in result.output
+        assert "Airflow 3.2.3" in captured_comments[0]
+        assert "backport label targeting v3-2-test" in captured_comments[0]
+        assert result.exit_code == 0
+
+    @patch("airflow_breeze.commands.ci_commands._get_github_client")
+    def test_skip_label_added_after_snapshot_should_skip(
+        self, mock_get_client, cli_runner, mock_github_setup
+    ):
+        """A skip label added after the snapshot must also halt the action."""
+        from airflow_breeze.commands.ci_commands import ci_group
+
+        mock_gh, mock_repo, mock_issue = mock_github_setup
+        mock_issue.milestone = None
+        # Snapshot had no skip label; fresh state added area:CI.
+        mock_issue.labels = [_label("backport-to-v3-1-test"), 
_label("area:CI")]
+        mock_get_client.return_value = mock_gh
+
+        result = cli_runner.invoke(
+            ci_group,
+            [
+                "set-milestone",
+                "--pr-number",
+                "12345",
+                "--pr-title",
+                "CI tweak",
+                "--pr-labels",
+                json.dumps(["backport-to-v3-1-test"]),
+                "--base-branch",
+                "main",
+                "--merged-by",
+                "testuser",
+                "--github-token",
+                "fake-token",
+                "--github-repository",
+                "apache/airflow",
+            ],
+        )
+
+        mock_issue.edit.assert_not_called()
+        mock_issue.create_comment.assert_not_called()
+        assert "Skipping milestone tagging" in result.output
+        assert "area:CI" in result.output
+        assert result.exit_code == 0

Reply via email to