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