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

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

commit aefef1f4ab4f902672b689328a7b2af3ac8b6e1d
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Sun Nov 1 21:36:39 2026 +0100

    [v3-4-test] Skip CI runs deleted while the Spot interruption notices handle 
them (#76249) (#76254)
    
    A run can be deleted between listing the runs and reading its jobs. The 404 
that follows failed the whole scheduled job, although nothing is left to report 
on a deleted run. A 404 still fails the job when the run exists, as that points 
at a real problem.
    (cherry picked from commit e109bfdb45ec5a23f1d2afbbf4b0f23df8164f5a)
    
    
    Generated-by: Claude Opus 5
    
    Co-authored-by: Jarek Potiuk <[email protected]>
---
 scripts/ci/spot_interruptions.py            | 16 +++++++++++
 scripts/tests/ci/test_spot_interruptions.py | 44 +++++++++++++++++++++++++++--
 2 files changed, 57 insertions(+), 3 deletions(-)

diff --git a/scripts/ci/spot_interruptions.py b/scripts/ci/spot_interruptions.py
index e78344fe074..d344e423c47 100755
--- a/scripts/ci/spot_interruptions.py
+++ b/scripts/ci/spot_interruptions.py
@@ -84,6 +84,7 @@ FAILED_CONCLUSIONS = ("failure", "cancelled", "timed_out")
 TRANSIENT_ERROR = re.compile(
     r"\(HTTP 5\d\d\)|stream error|connection reset by peer|unexpected EOF|TLS 
handshake timeout|i/o timeout"
 )
+NOT_FOUND_ERROR = re.compile(r"\(HTTP 404\)")
 GH_API_ATTEMPTS = 3
 GH_API_RETRY_DELAY_SECONDS = 5
 
@@ -101,6 +102,10 @@ def is_transient(error: subprocess.CalledProcessError) -> 
bool:
     return bool(TRANSIENT_ERROR.search(error.stderr or ""))
 
 
+def is_not_found(error: subprocess.CalledProcessError) -> bool:
+    return bool(NOT_FOUND_ERROR.search(error.stderr or ""))
+
+
 def gh_api(*args: str) -> str:
     command = ["gh", "api", *args]
     # A POST that failed with a server error may still have created the 
comment.
@@ -384,6 +389,14 @@ def update_notice_on(repo: str, pr_number: int, run: dict, 
jobs: list[FailedJob]
     write_comment(repo, pr_number, existing, body)
 
 
+def is_run_deleted(repo: str, run_id: int) -> bool:
+    try:
+        gh_api(f"repos/{repo}/actions/runs/{run_id}", "--jq", ".id")
+    except subprocess.CalledProcessError as e:
+        return is_not_found(e)
+    return False
+
+
 def describe_error(error: subprocess.CalledProcessError) -> str:
     return " ".join((error.stderr or str(error)).split())
 
@@ -409,6 +422,9 @@ def run_pr_comments(repo: str, now: datetime) -> bool:
         except subprocess.CalledProcessError as e:
             if is_transient(e):
                 print(f"::warning::Skipped run {run['html_url']}, GitHub keeps 
failing: {describe_error(e)}")
+            elif is_not_found(e) and is_run_deleted(repo, run["id"]):
+                # Runs are deleted at any time, also between listing them and 
reading their jobs.
+                print(f"Skipped run {run['html_url']}: it was deleted")
             else:
                 print(f"Could not handle run {run['html_url']}: 
{describe_error(e)}", file=sys.stderr)
                 ok = False
diff --git a/scripts/tests/ci/test_spot_interruptions.py 
b/scripts/tests/ci/test_spot_interruptions.py
index d516890e658..72cfee8b3ad 100644
--- a/scripts/tests/ci/test_spot_interruptions.py
+++ b/scripts/tests/ci/test_spot_interruptions.py
@@ -303,20 +303,58 @@ STREAM_ERROR = subprocess.CalledProcessError(
 
 
 @pytest.mark.parametrize(
-    ("error", "expected_ok"), [(SERVER_ERROR, True), (STREAM_ERROR, True), 
(NOT_FOUND_ERROR, False)]
+    ("error", "run_deleted", "expected_ok", "expected_out", "expected_err"),
+    [
+        pytest.param(SERVER_ERROR, None, True, "::warning::Skipped run", "", 
id="server-error"),
+        pytest.param(STREAM_ERROR, None, True, "::warning::Skipped run", "", 
id="dropped-connection"),
+        pytest.param(
+            NOT_FOUND_ERROR, True, True, f"Skipped run {RUN_URL}: it was 
deleted", "", id="deleted-run"
+        ),
+        pytest.param(
+            NOT_FOUND_ERROR,
+            False,
+            False,
+            "",
+            f"Could not handle run {RUN_URL}",
+            id="not-found-in-existing-run",
+        ),
+    ],
 )
-def test_run_pr_comments_carries_on_past_a_failing_run(spot, capsys, error, 
expected_ok):
+def test_run_pr_comments_carries_on_past_a_failing_run(
+    spot, capsys, error, run_deleted, expected_ok, expected_out, expected_err
+):
     runs = [_run(branch="a"), _run(branch="b")]
 
     with (
         mock.patch.object(spot, "list_pull_request_runs", autospec=True, 
return_value=runs),
         mock.patch.object(spot, "update_notice", autospec=True, 
side_effect=[error, None]) as mock_update,
+        mock.patch.object(spot, "is_run_deleted", autospec=True, 
return_value=run_deleted) as mock_deleted,
     ):
         ok = spot.run_pr_comments("apache/airflow", datetime(2026, 10, 25, 
tzinfo=UTC))
 
     assert ok is expected_ok
     assert mock_update.call_count == 2
-    assert capsys.readouterr().out.startswith("::warning::") is expected_ok
+    captured = capsys.readouterr()
+    assert captured.out.startswith(expected_out) if expected_out else 
captured.out == ""
+    assert captured.err.startswith(expected_err) if expected_err else 
captured.err == ""
+    if run_deleted is None:
+        mock_deleted.assert_not_called()
+    else:
+        mock_deleted.assert_called_once_with("apache/airflow", 7)
+
+
[email protected](
+    ("error", "expected"),
+    [
+        pytest.param(None, False, id="run-exists"),
+        pytest.param(NOT_FOUND_ERROR, True, id="run-deleted"),
+        pytest.param(SERVER_ERROR, False, id="unknown"),
+    ],
+)
+def test_is_run_deleted(spot, error, expected):
+    with mock.patch.object(spot, "gh_api", autospec=True, side_effect=error, 
return_value="7") as mock_gh:
+        assert spot.is_run_deleted("apache/airflow", 7) is expected
+    mock_gh.assert_called_once_with("repos/apache/airflow/actions/runs/7", 
"--jq", ".id")
 
 
 @pytest.mark.parametrize(("error", "expected_ok"), [(SERVER_ERROR, True), 
(NOT_FOUND_ERROR, None)])

Reply via email to