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 6425fb72203 Improve error message when publish-docs ref is a branch
not a tag (#66250)
6425fb72203 is described below
commit 6425fb72203a394a8d76354ebeca482cfe341762
Author: Shahar Epstein <[email protected]>
AuthorDate: Tue May 12 17:58:26 2026 +0300
Improve error message when publish-docs ref is a branch not a tag (#66250)
---
.../airflow_breeze/commands/workflow_commands.py | 24 ++++-
dev/breeze/tests/test_workflow_commands.py | 101 +++++++++++++++++++++
2 files changed, 122 insertions(+), 3 deletions(-)
diff --git a/dev/breeze/src/airflow_breeze/commands/workflow_commands.py
b/dev/breeze/src/airflow_breeze/commands/workflow_commands.py
index 5822a8ceb08..1620a09c90b 100644
--- a/dev/breeze/src/airflow_breeze/commands/workflow_commands.py
+++ b/dev/breeze/src/airflow_breeze/commands/workflow_commands.py
@@ -145,10 +145,28 @@ def workflow_run_publish(
tag_respo = json.loads(stdout)
if not tag_respo.get("ref"):
- console_print(
- f"[red]Error: Ref {ref} does not exists in repo apache/airflow
.[/red]",
+ # Check whether the ref exists as a branch (common case for -docs
branches)
+ branch_result = run_gh_command(
+ ["gh", "api", f"repos/apache/airflow/git/refs/heads/{ref}"],
+ capture_output=True,
+ check=False,
)
- console_print("\nYou can add --skip-tag-validation to skip this
validation.")
+ branch_stdout = branch_result.stdout.decode("utf-8")
+ branch_respo = json.loads(branch_stdout)
+
+ if branch_respo.get("ref"):
+ console_print(
+ f"[red]Error: Ref {ref} exists as a branch but not as a
tag.[/red]",
+ )
+ console_print(
+ "\nTo publish docs from a branch (e.g. a
`providers/YYYY-MM-DD-docs` "
+ "post-release fix branch), add
[bold]--skip-tag-validation[/bold] to your command."
+ )
+ else:
+ console_print(
+ f"[red]Error: Ref {ref} does not exist as a tag or branch
in repo apache/airflow.[/red]",
+ )
+ console_print("\nYou can add --skip-tag-validation to skip
this validation.")
sys.exit(1)
console_print(
diff --git a/dev/breeze/tests/test_workflow_commands.py
b/dev/breeze/tests/test_workflow_commands.py
new file mode 100644
index 00000000000..8a30e2a40e4
--- /dev/null
+++ b/dev/breeze/tests/test_workflow_commands.py
@@ -0,0 +1,101 @@
+# 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
+
+import json
+from subprocess import CompletedProcess
+from unittest.mock import MagicMock, patch
+
+import pytest
+from click.testing import CliRunner
+
+from airflow_breeze.commands.workflow_commands import workflow_run_publish
+
+
+def _make_gh_response(ref: str | None) -> CompletedProcess:
+ """Return a fake CompletedProcess whose stdout mimics a gh api response."""
+ body = {"ref": ref} if ref else {"message": "Not Found"}
+ return MagicMock(spec=CompletedProcess, stdout=json.dumps(body).encode())
+
+
+class TestPublishDocsTagValidation:
+ """Tests for the ref validation logic in `breeze workflow-run
publish-docs`."""
+
+ @pytest.fixture
+ def runner(self):
+ return CliRunner()
+
+ def _invoke(self, runner: CliRunner, ref: str, extra_args: list[str] |
None = None) -> object:
+ args = ["--ref", ref, "--site-env", "staging", "apache-airflow"]
+ if extra_args:
+ args = extra_args + args
+ return runner.invoke(workflow_run_publish, args,
catch_exceptions=False)
+
+ @patch("airflow_breeze.commands.workflow_commands.run_gh_command")
+ def test_ref_is_valid_tag_passes_validation(self, mock_run_gh_command,
runner):
+ """When the ref exists as a tag, validation passes and the workflow is
triggered."""
+ tag_response = _make_gh_response("refs/tags/providers/2026-04-26")
+ mock_run_gh_command.return_value = tag_response
+
+ with
patch("airflow_breeze.commands.workflow_commands.trigger_workflow_and_monitor")
as mock_trigger:
+ result = self._invoke(runner, "providers/2026-04-26")
+
+ assert result.exit_code == 0
+ mock_trigger.assert_called()
+ # Only one gh api call (tag check), no branch check needed
+ mock_run_gh_command.assert_called_once()
+
+ @patch("airflow_breeze.commands.workflow_commands.run_gh_command")
+ def test_ref_is_branch_shows_actionable_message(self, mock_run_gh_command,
runner):
+ """When the ref exists as a branch but not a tag, the error names the
branch
+ and points to --skip-tag-validation."""
+ tag_response = _make_gh_response(None)
+ branch_response = _make_gh_response("refs/heads/main")
+ mock_run_gh_command.side_effect = [tag_response, branch_response]
+
+ result = self._invoke(runner, "main")
+
+ assert result.exit_code == 1
+ assert "exists as a branch but not as a tag" in result.output
+ assert "--skip-tag-validation" in result.output
+ # Both tag and branch checks must have been made
+ assert mock_run_gh_command.call_count == 2
+ calls = mock_run_gh_command.call_args_list
+ assert "refs/tags/main" in calls[0][0][0][2]
+ assert "refs/heads/main" in calls[1][0][0][2]
+
+ @patch("airflow_breeze.commands.workflow_commands.run_gh_command")
+ def test_ref_not_found_anywhere_shows_generic_message(self,
mock_run_gh_command, runner):
+ """When the ref exists neither as a tag nor a branch, a generic error
is shown."""
+ mock_run_gh_command.return_value = _make_gh_response(None)
+
+ result = self._invoke(runner, "nonexistent-ref-xyz")
+
+ assert result.exit_code == 1
+ assert "does not exist as a tag or branch" in result.output
+ assert "--skip-tag-validation" in result.output
+ assert mock_run_gh_command.call_count == 2
+
+ @patch("airflow_breeze.commands.workflow_commands.run_gh_command")
+ def test_skip_tag_validation_bypasses_checks(self, mock_run_gh_command,
runner):
+ """With --skip-tag-validation, no gh api calls are made and the
workflow proceeds."""
+ with
patch("airflow_breeze.commands.workflow_commands.trigger_workflow_and_monitor")
as mock_trigger:
+ result = self._invoke(runner, "some-branch",
extra_args=["--skip-tag-validation"])
+
+ assert result.exit_code == 0
+ mock_run_gh_command.assert_not_called()
+ mock_trigger.assert_called()