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

shahar1 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 613c9d3330d Stop backporting the scheduled CI environment upgrade 
(#73319)
613c9d3330d is described below

commit 613c9d3330d5f3989ef9bb854fa021808aa7ce4a
Author: Jarek Potiuk <[email protected]>
AuthorDate: Fri Sep 18 17:42:20 2026 +0200

    Stop backporting the scheduled CI environment upgrade (#73319)
    
    Each maintenance branch upgrades its own CI environment on its own
    schedule, so backporting main's upgrade races that with a change that
    conflicts on the very files it regenerates — which is why none of the
    labelled upgrade PRs has ever produced a backport. boring-cyborg labels
    by path and cannot tell this PR from a hand-written one touching dev/ or
    .github/, so the label has to come off afterwards instead.
    
    Generated-by: Claude Code (Opus 5)
---
 .github/workflows/upgrade-check.yml                | 23 ++++++
 .../src/airflow_breeze/commands/ci_commands.py     | 46 ++++++++++++
 dev/breeze/tests/test_remove_backport_labels.py    | 85 ++++++++++++++++++++++
 3 files changed, 154 insertions(+)

diff --git a/.github/workflows/upgrade-check.yml 
b/.github/workflows/upgrade-check.yml
index 572f1d7e6b6..0fbaff6c2c9 100644
--- a/.github/workflows/upgrade-check.yml
+++ b/.github/workflows/upgrade-check.yml
@@ -97,6 +97,29 @@ jobs:
             --json url \
             --jq '.[0].url' 2>/dev/null || true)
           echo "pr-url=${PR_URL}" >> "${GITHUB_OUTPUT}"
+      - name: >-
+          [${{ inputs.target-branch }}] Drop backport labels
+        # breeze already removed these, but boring-cyborg labels on a webhook 
and can land
+        # after it exits. Each branch upgrades itself on its own schedule, so 
a backport
+        # would race that with a change conflicting on the files it 
regenerates.
+        if: steps.find-pr.outputs.pr-url != ''
+        env:
+          PR_URL: ${{ steps.find-pr.outputs.pr-url }}
+        run: |
+          sleep 30
+          mapfile -t LABELS < <(gh pr view "${PR_URL}" \
+            --json labels \
+            --jq '.labels[].name | select(startswith("backport-to-"))')
+          if [[ ${#LABELS[@]} -eq 0 ]]; then
+            echo "No backport labels to remove."
+            exit 0
+          fi
+          ARGS=()
+          for label in "${LABELS[@]}"; do
+            ARGS+=(--remove-label "${label}")
+          done
+          gh pr edit "${PR_URL}" "${ARGS[@]}"
+          echo "Removed: ${LABELS[*]}"
       - name: >-
           [${{ inputs.target-branch }}] Notify Slack on success
         if: success() && steps.find-pr.outputs.pr-url != ''
diff --git a/dev/breeze/src/airflow_breeze/commands/ci_commands.py 
b/dev/breeze/src/airflow_breeze/commands/ci_commands.py
index 00c961d2c63..c75861334d0 100644
--- a/dev/breeze/src/airflow_breeze/commands/ci_commands.py
+++ b/dev/breeze/src/airflow_breeze/commands/ci_commands.py
@@ -549,6 +549,50 @@ def _sync_k8s_schemas_to_airflow_site(airflow_site: Path, 
force: bool, command_e
     run_command(cmd, check=False, env=command_env)
 
 
+def remove_backport_labels(*, branch_name: str, command_env: dict[str, str]) 
-> None:
+    """Drop any ``backport-to-*`` label boring-cyborg put on the upgrade PR.
+
+    The CI environment is upgraded on each maintenance branch by that branch's 
own scheduled
+    run, so backporting main's upgrade would race it with a change that 
conflicts on the very
+    files it regenerates. boring-cyborg labels by path and cannot tell this PR 
apart from a
+    hand-written one touching ``dev/`` or ``.github/``, so the label is 
removed here instead.
+
+    Labelling happens on a webhook and may land after this runs; 
``upgrade-check.yml`` sweeps
+    again once the PR has settled, so a miss here is not the last word.
+    """
+    labels_result = run_command(
+        [
+            "gh",
+            "pr",
+            "view",
+            branch_name,
+            "--repo",
+            "apache/airflow",
+            "--json",
+            "labels",
+            "--jq",
+            '[.labels[].name | select(startswith("backport-to-"))] | 
join(",")',
+        ],
+        capture_output=True,
+        text=True,
+        check=False,
+        env=command_env,
+    )
+    if labels_result.returncode != 0:
+        console_print("[warning]Could not read PR labels - leaving any 
backport labels in place.[/]")
+        return
+    labels = [label for label in labels_result.stdout.strip().split(",") if 
label]
+    if not labels:
+        return
+    remove_cmd = ["gh", "pr", "edit", branch_name, "--repo", "apache/airflow"]
+    for label in labels:
+        remove_cmd.extend(["--remove-label", label])
+    if run_command(remove_cmd, capture_output=True, text=True, check=False, 
env=command_env).returncode:
+        console_print(f"[warning]Could not remove backport labels: {', 
'.join(labels)}[/]")
+    else:
+        console_print(f"[success]Removed backport labels: {', 
'.join(labels)}.[/]")
+
+
 @ci_group.command(
     name="upgrade",
     help="Perform important upgrade steps of the CI environment. And create a 
PR",
@@ -1019,6 +1063,8 @@ def upgrade(
             else:
                 console_print(f"[success]PR created successfully: 
{pr_result.stdout.strip()}.[/]")
 
+        remove_backport_labels(branch_name=branch_name, 
command_env=command_env)
+
         # Switch back to appropriate branch and delete the temporary branch
         console_print(f"[info]Cleaning up temporary branch 
{branch_name}...[/]")
         if user_switched_to_target:
diff --git a/dev/breeze/tests/test_remove_backport_labels.py 
b/dev/breeze/tests/test_remove_backport_labels.py
new file mode 100644
index 00000000000..369669bab3e
--- /dev/null
+++ b/dev/breeze/tests/test_remove_backport_labels.py
@@ -0,0 +1,85 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+from unittest.mock import MagicMock, patch
+
+import pytest
+
+from airflow_breeze.commands.ci_commands import remove_backport_labels
+
+MODULE = "airflow_breeze.commands.ci_commands"
+
+
+def _result(returncode: int = 0, stdout: str = "") -> MagicMock:
+    return MagicMock(returncode=returncode, stdout=stdout)
+
+
+def _call(run_command, **kwargs):
+    with patch(f"{MODULE}.run_command", run_command):
+        remove_backport_labels(branch_name="ci-upgrade-main", command_env={}, 
**kwargs)
+
+
+def _edit_call(run_command):
+    """Return the argv of the `gh pr edit` invocation, or None when it never 
ran."""
+    for call in run_command.call_args_list:
+        argv = call.args[0]
+        if "edit" in argv:
+            return argv
+    return None
+
+
[email protected](
+    "labels",
+    ["backport-to-v3-3-test", 
"backport-to-v3-3-test,backport-to-airflow-ctl/v0-1-test"],
+)
+def test_every_backport_label_is_removed_in_one_call(labels):
+    run_command = MagicMock(side_effect=[_result(stdout=labels), _result()])
+
+    _call(run_command)
+
+    argv = _edit_call(run_command)
+    assert argv is not None
+    removed = {argv[index + 1] for index, arg in enumerate(argv) if arg == 
"--remove-label"}
+    assert removed == set(labels.split(","))
+    assert run_command.call_count == 2
+
+
+def test_nothing_is_edited_when_no_backport_label_is_present():
+    run_command = MagicMock(side_effect=[_result(stdout="")])
+
+    _call(run_command)
+
+    assert _edit_call(run_command) is None
+
+
+def test_labels_are_left_alone_when_they_cannot_be_read():
+    """Without the label list we cannot tell which to remove - never guess and 
edit blindly."""
+    run_command = MagicMock(side_effect=[_result(returncode=1)])
+
+    _call(run_command)
+
+    assert _edit_call(run_command) is None
+
+
+def test_a_failed_edit_does_not_raise():
+    """The upgrade PR itself is already pushed; a label left behind must not 
fail the run."""
+    run_command = 
MagicMock(side_effect=[_result(stdout="backport-to-v3-3-test"), 
_result(returncode=1)])
+
+    _call(run_command)
+
+    assert _edit_call(run_command) is not None

Reply via email to