This is an automated email from the ASF dual-hosted git repository.
potiuk 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 d18b831afc2 Show staging-only commits before resetting staging to main
(#74116)
d18b831afc2 is described below
commit d18b831afc23eef632139a14884b19878dddbb92
Author: Shahar Epstein <[email protected]>
AuthorDate: Tue Oct 6 10:58:42 2026 +0300
Show staging-only commits before resetting staging to main (#74116)
* Show staging-only commits before resetting staging to main
breeze workflow-run sync-staging-to-main force-resets the staging branches
of apache/airflow-site and apache/airflow-site-archive. List the commits that
exist only on staging, and the count in the prompt, so the release manager can
see what would be dropped. If the comparison fails, fall back to the previous
prompt.
closes: #74114
* Show commit date next to staging-only commits
---
dev/breeze/doc/09_release_management_tasks.rst | 4 +-
.../airflow_breeze/commands/workflow_commands.py | 23 ++++++++-
.../src/airflow_breeze/utils/gh_workflow_utils.py | 16 ++++++
dev/breeze/tests/test_gh_workflow_utils.py | 31 ++++++++++++
dev/breeze/tests/test_workflow_commands.py | 57 +++++++++++++++++++++-
5 files changed, 127 insertions(+), 4 deletions(-)
diff --git a/dev/breeze/doc/09_release_management_tasks.rst
b/dev/breeze/doc/09_release_management_tasks.rst
index 52ec6559771..d2f186875ac 100644
--- a/dev/breeze/doc/09_release_management_tasks.rst
+++ b/dev/breeze/doc/09_release_management_tasks.rst
@@ -1087,7 +1087,9 @@ Before publishing release candidate docs to staging,
reset the ``staging`` branc
``apache/airflow-site`` and ``apache/airflow-site-archive`` to ``main`` with
the
``breeze workflow-run sync-staging-to-main`` command. It triggers the
``reset-staging.yml`` workflow in both
repositories, which force-updates each ``staging`` branch to the current
``main`` commit (``main`` itself is
-not changed); in ``apache/airflow-site`` it also rebuilds the staging site.
+not changed); in ``apache/airflow-site`` it also rebuilds the staging site.
Before asking for confirmation,
+the command lists the commits that exist only on each ``staging`` branch (and
would be dropped), so you can
+tell whether the branch holds docs prepared for another release.
.. warning::
diff --git a/dev/breeze/src/airflow_breeze/commands/workflow_commands.py
b/dev/breeze/src/airflow_breeze/commands/workflow_commands.py
index acd13c2e763..55c3363fae9 100644
--- a/dev/breeze/src/airflow_breeze/commands/workflow_commands.py
+++ b/dev/breeze/src/airflow_breeze/commands/workflow_commands.py
@@ -19,6 +19,7 @@ from __future__ import annotations
import json
import os
import re
+import subprocess
import sys
import click
@@ -28,7 +29,7 @@ from airflow_breeze.utils.click_utils import BreezeGroup
from airflow_breeze.utils.confirm import Answer, user_confirm
from airflow_breeze.utils.console import console_print
from airflow_breeze.utils.custom_param_types import BetterChoice
-from airflow_breeze.utils.gh_workflow_utils import trigger_workflow_and_monitor
+from airflow_breeze.utils.gh_workflow_utils import get_staging_only_commits,
trigger_workflow_and_monitor
from airflow_breeze.utils.github import run_gh_command
WORKFLOW_NAME_MAPS = {
@@ -333,7 +334,25 @@ def workflow_run_sync_staging_to_main():
"[warning]If a vote for ANY other release is in progress, its staging
docs live on those "
"branches - SKIP this step, or you will overwrite the docs prepared
for that vote.[/warning]"
)
- answer = user_confirm("Is no other release vote in progress, and should
staging be reset to main?")
+ dropped_commits_count = 0
+ for repo in (APACHE_AIRFLOW_SITE_REPO, APACHE_AIRFLOW_SITE_ARCHIVE_REPO):
+ try:
+ staging_only_commits = get_staging_only_commits(repo)
+ except (subprocess.CalledProcessError, json.JSONDecodeError, KeyError)
as e:
+ console_print(f"[warning]Could not compare staging to main in
{repo}: {e!r}[/warning]")
+ continue
+ if staging_only_commits:
+ dropped_commits_count += len(staging_only_commits)
+ console_print(f"[warning]Commits only on `staging` in {repo} that
will be dropped:[/warning]")
+ for commit in staging_only_commits:
+ console_print(f" {commit}")
+ prompt = "Is no other release vote in progress, and should staging be
reset to main?"
+ if dropped_commits_count:
+ prompt = (
+ f"{dropped_commits_count} commit(s) only on staging will be
dropped. "
+ "Is no other release vote in progress, and should staging be reset
to main?"
+ )
+ answer = user_confirm(prompt)
if answer != Answer.YES:
console_print("[info]Skipping the reset of staging to main.[/info]")
sys.exit(0 if answer == Answer.NO else 1)
diff --git a/dev/breeze/src/airflow_breeze/utils/gh_workflow_utils.py
b/dev/breeze/src/airflow_breeze/utils/gh_workflow_utils.py
index bb60f49ed20..01841cab7ca 100644
--- a/dev/breeze/src/airflow_breeze/utils/gh_workflow_utils.py
+++ b/dev/breeze/src/airflow_breeze/utils/gh_workflow_utils.py
@@ -274,3 +274,19 @@ def trigger_workflow_and_monitor(
run_id=str(workflow_run_id),
repo=repo,
)
+
+
+def get_staging_only_commits(repo: str) -> list[str]:
+ """Return ``<short sha> <date> <subject>`` for each commit on ``staging``
that is not on ``main``."""
+ result = run_gh_command(
+ ["gh", "api", f"repos/{repo}/compare/main...staging"],
+ check=True,
+ capture_output=True,
+ text=True,
+ )
+ commits = json.loads(result.stdout)["commits"]
+ return [
+ f"{commit['sha'][:7]} {commit['commit']['committer']['date'][:10]} "
+ f"{commit['commit']['message'].splitlines()[0]}"
+ for commit in commits
+ ]
diff --git a/dev/breeze/tests/test_gh_workflow_utils.py
b/dev/breeze/tests/test_gh_workflow_utils.py
index 2bc2d54267e..a06df96d10e 100644
--- a/dev/breeze/tests/test_gh_workflow_utils.py
+++ b/dev/breeze/tests/test_gh_workflow_utils.py
@@ -17,6 +17,7 @@
from __future__ import annotations
import contextlib
+import json
import subprocess
from unittest import mock
@@ -25,6 +26,7 @@ import pytest
from airflow_breeze.utils.gh_workflow_utils import (
NEW_RUN_TIMEOUT_SECONDS,
get_latest_workflow_run_id,
+ get_staging_only_commits,
monitor_workflow_run,
trigger_workflow_and_monitor,
wait_for_new_workflow_run,
@@ -132,3 +134,32 @@ def
test_monitor_workflow_run_fails_on_any_unsuccessful_conclusion(mock_info, co
monitor_workflow_run(run_id="123", repo="apache/airflow")
assert exc_info.value.code == 1
+
+
[email protected]("airflow_breeze.utils.gh_workflow_utils.run_gh_command")
+def
test_get_staging_only_commits_formats_sha_date_and_subject(mock_run_gh_command):
+ mock_run_gh_command.return_value = subprocess.CompletedProcess(
+ args=[],
+ returncode=0,
+ stdout=json.dumps(
+ {
+ "commits": [
+ {
+ "sha": "abc1234567890",
+ "commit": {
+ "message": "Add staged docs\n\nLonger body",
+ "committer": {"date": "2026-09-30T10:11:12Z"},
+ },
+ }
+ ]
+ }
+ ),
+ stderr="",
+ )
+
+ assert get_staging_only_commits("apache/airflow-site") == ["abc1234
2026-09-30 Add staged docs"]
+ assert mock_run_gh_command.call_args.args[0] == [
+ "gh",
+ "api",
+ "repos/apache/airflow-site/compare/main...staging",
+ ]
diff --git a/dev/breeze/tests/test_workflow_commands.py
b/dev/breeze/tests/test_workflow_commands.py
index dea188d19e4..cee0bf9ae88 100644
--- a/dev/breeze/tests/test_workflow_commands.py
+++ b/dev/breeze/tests/test_workflow_commands.py
@@ -17,6 +17,8 @@
from __future__ import annotations
import json
+import re
+import subprocess
from subprocess import CompletedProcess
from unittest.mock import MagicMock, call, patch
@@ -27,6 +29,7 @@ from airflow_breeze.commands.workflow_commands import (
workflow_run_publish,
workflow_run_sync_staging_to_main,
)
+from airflow_breeze.utils.confirm import Answer
def _make_gh_response(ref: str | None) -> CompletedProcess:
@@ -113,10 +116,12 @@ class TestSyncStagingToMain:
pytest.param("q", 1, False, id="quit"),
],
)
+
@patch("airflow_breeze.commands.workflow_commands.get_staging_only_commits",
autospec=True)
@patch("airflow_breeze.commands.workflow_commands.trigger_workflow_and_monitor")
def test_reset_staging_is_triggered_only_when_confirmed(
- self, mock_trigger, answer, expected_exit_code, expected_triggered
+ self, mock_trigger, mock_get_commits, answer, expected_exit_code,
expected_triggered
):
+ mock_get_commits.return_value = []
result = CliRunner().invoke(
workflow_run_sync_staging_to_main, ["--answer", answer],
catch_exceptions=False
)
@@ -129,3 +134,53 @@ class TestSyncStagingToMain:
]
else:
mock_trigger.assert_not_called()
+
+
@patch("airflow_breeze.commands.workflow_commands.trigger_workflow_and_monitor",
autospec=True)
+ @patch("airflow_breeze.commands.workflow_commands.user_confirm",
autospec=True)
+
@patch("airflow_breeze.commands.workflow_commands.get_staging_only_commits",
autospec=True)
+ def test_staging_only_commits_are_listed_and_counted(self,
mock_get_commits, mock_confirm, mock_trigger):
+ mock_get_commits.side_effect = [
+ ["abc1234 2026-09-30 Add staged docs", "def5678 2026-10-01 Fix
index"],
+ [],
+ ]
+ mock_confirm.return_value = Answer.YES
+
+ result = CliRunner().invoke(workflow_run_sync_staging_to_main, [],
catch_exceptions=False)
+
+ assert result.exit_code == 0
+ # rich highlights digits, so compare against the output with the ANSI
codes removed
+ plain_output = re.sub(r"\x1b\[[0-9;]*m", "", result.output)
+ assert "abc1234 2026-09-30 Add staged docs" in plain_output
+ assert "def5678 2026-10-01 Fix index" in plain_output
+ assert "2 commit(s) only on staging will be dropped" in
mock_confirm.call_args.args[0]
+ assert mock_trigger.call_count == 2
+
+
@patch("airflow_breeze.commands.workflow_commands.trigger_workflow_and_monitor",
autospec=True)
+ @patch("airflow_breeze.commands.workflow_commands.user_confirm",
autospec=True)
+
@patch("airflow_breeze.commands.workflow_commands.get_staging_only_commits",
autospec=True)
+ def test_no_deviation_output_when_staging_is_not_ahead(
+ self, mock_get_commits, mock_confirm, mock_trigger
+ ):
+ mock_get_commits.return_value = []
+ mock_confirm.return_value = Answer.YES
+
+ result = CliRunner().invoke(workflow_run_sync_staging_to_main, [],
catch_exceptions=False)
+
+ assert result.exit_code == 0
+ assert "will be dropped:" not in result.output
+ assert "commit(s)" not in mock_confirm.call_args.args[0]
+ assert mock_trigger.call_count == 2
+
+
@patch("airflow_breeze.commands.workflow_commands.trigger_workflow_and_monitor",
autospec=True)
+ @patch("airflow_breeze.commands.workflow_commands.user_confirm",
autospec=True)
+
@patch("airflow_breeze.commands.workflow_commands.get_staging_only_commits",
autospec=True)
+ def test_compare_failure_falls_back_to_plain_prompt(self,
mock_get_commits, mock_confirm, mock_trigger):
+ mock_get_commits.side_effect = subprocess.CalledProcessError(1, ["gh"])
+ mock_confirm.return_value = Answer.YES
+
+ result = CliRunner().invoke(workflow_run_sync_staging_to_main, [],
catch_exceptions=False)
+
+ assert result.exit_code == 0
+ assert "Could not compare staging to main in apache/airflow-site" in
result.output
+ assert "commit(s)" not in mock_confirm.call_args.args[0]
+ assert mock_trigger.call_count == 2