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

potiuk pushed a commit to branch fix-ci-passed-cancelled-runs
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit 6adedfa1c2faabc277e3884da1e7dea0ab2ad555
Author: Jarek Potiuk <[email protected]>
AuthorDate: Sat Oct 24 20:30:17 2026 +0200

    Do not fail CI passed for a cancelled duplicate CI run
    
    A run that concurrency cancels still runs the always() CI passed job, and
    that job failed the required check. When a stacked pull request's base and
    head are force-pushed together, GitHub sends two synchronize events for the
    same commit, so one run is cancelled seconds in and a failed CI passed sits
    next to the passing one of its twin. A cancelled run now reports under a
    name the rulesets do not require, so nothing is red, while a run that was
    only cancelled still never satisfies CI passed and the merge stays blocked.
    
    Generated-by: Claude Opus 5
---
 .github/workflows/ci-amd.yml | 23 ++++++++++++++++++-----
 .github/workflows/ci-arm.yml | 23 ++++++++++++++++++-----
 dev/README_AIRFLOW3_DEV.md   |  9 ++++++---
 3 files changed, 42 insertions(+), 13 deletions(-)

diff --git a/.github/workflows/ci-amd.yml b/.github/workflows/ci-amd.yml
index 2f7fde8dffd..37f87ca866b 100644
--- a/.github/workflows/ci-amd.yml
+++ b/.github/workflows/ci-amd.yml
@@ -1461,11 +1461,18 @@ jobs:
     # pull request to CI in the contributor's fork, and every job here is 
skipped. Reporting
     # "CI passed" then would let it merge on no CI, so the job reports under 
another name and
     # the fork CI reconciler posts "CI passed" from the fork run instead.
+    # A run with cancelled jobs and no failed one -- usually a duplicate run 
of the same commit that
+    # concurrency cancelled, such as the two `synchronize` events GitHub sends 
when a stacked pull
+    # request's base and head are force-pushed together -- reports under 
another name too, so it
+    # never puts a failed "CI passed" next to the passing one of its twin. "CI 
passed" is then
+    # missing for that run, which keeps the merge blocked until another run of 
the commit reports it.
     name: >-
-      ${{ needs.build-info.result == 'skipped' && 'CI passed (project CI not 
run)' || 'CI passed' }}
+      ${{ (!contains(needs.*.result, 'failure') && contains(needs.*.result, 
'cancelled') &&
+      'CI passed (run cancelled)') ||
+      (needs.build-info.result == 'skipped' && 'CI passed (project CI not 
run)') || 'CI passed' }}
     # The one check main's ruleset requires, so auto-merge waits for CI to 
finish. GitHub treats a
     # skipped required check as passed, so this job always runs and fails 
itself when any job it
-    # needs failed or was cancelled. Jobs skipped by selective checks count as 
passed.
+    # needs failed. Jobs skipped by selective checks count as passed.
     if: always()
     needs:
       - additional-ci-image-checks
@@ -1508,12 +1515,18 @@ jobs:
       - tests-with-lowest-direct-resolution-providers
     runs-on: ["ubuntu-slim"]
     steps:
-      - name: "Fail if any CI job failed or was cancelled"
-        if: contains(needs.*.result, 'failure') || contains(needs.*.result, 
'cancelled')
+      - name: "Fail if any CI job failed"
+        if: contains(needs.*.result, 'failure')
         run: |
-          echo "At least one CI job failed or was cancelled."
+          echo "At least one CI job failed."
           exit 1
+      - name: "Report that CI jobs were cancelled"
+        if: contains(needs.*.result, 'cancelled')
+        run: |
+          echo "At least one CI job was cancelled, so this run does not report 
CI passed."
+          echo 'CI jobs were cancelled: this run does not report `CI passed`.' 
>> "${GITHUB_STEP_SUMMARY}"
       - name: "All CI jobs passed or were skipped"
+        if: "!contains(needs.*.result, 'cancelled')"
         run: echo "All CI jobs passed or were skipped."
 
   notify-slack:
diff --git a/.github/workflows/ci-arm.yml b/.github/workflows/ci-arm.yml
index 5eab899803e..d583b202688 100644
--- a/.github/workflows/ci-arm.yml
+++ b/.github/workflows/ci-arm.yml
@@ -1343,11 +1343,18 @@ jobs:
     # pull request to CI in the contributor's fork, and every job here is 
skipped. Reporting
     # "CI passed" then would let it merge on no CI, so the job reports under 
another name and
     # the fork CI reconciler posts "CI passed" from the fork run instead.
+    # A run with cancelled jobs and no failed one -- usually a duplicate run 
of the same commit that
+    # concurrency cancelled, such as the two `synchronize` events GitHub sends 
when a stacked pull
+    # request's base and head are force-pushed together -- reports under 
another name too, so it
+    # never puts a failed "CI passed" next to the passing one of its twin. "CI 
passed" is then
+    # missing for that run, which keeps the merge blocked until another run of 
the commit reports it.
     name: >-
-      ${{ needs.build-info.result == 'skipped' && 'CI passed (project CI not 
run)' || 'CI passed' }}
+      ${{ (!contains(needs.*.result, 'failure') && contains(needs.*.result, 
'cancelled') &&
+      'CI passed (run cancelled)') ||
+      (needs.build-info.result == 'skipped' && 'CI passed (project CI not 
run)') || 'CI passed' }}
     # The one check main's ruleset requires, so auto-merge waits for CI to 
finish. GitHub treats a
     # skipped required check as passed, so this job always runs and fails 
itself when any job it
-    # needs failed or was cancelled. Jobs skipped by selective checks count as 
passed.
+    # needs failed. Jobs skipped by selective checks count as passed.
     if: always()
     needs:
       - additional-ci-image-checks
@@ -1389,12 +1396,18 @@ jobs:
       - tests-with-lowest-direct-resolution-providers
     runs-on: ["ubuntu-slim"]
     steps:
-      - name: "Fail if any CI job failed or was cancelled"
-        if: contains(needs.*.result, 'failure') || contains(needs.*.result, 
'cancelled')
+      - name: "Fail if any CI job failed"
+        if: contains(needs.*.result, 'failure')
         run: |
-          echo "At least one CI job failed or was cancelled."
+          echo "At least one CI job failed."
           exit 1
+      - name: "Report that CI jobs were cancelled"
+        if: contains(needs.*.result, 'cancelled')
+        run: |
+          echo "At least one CI job was cancelled, so this run does not report 
CI passed."
+          echo 'CI jobs were cancelled: this run does not report `CI passed`.' 
>> "${GITHUB_STEP_SUMMARY}"
       - name: "All CI jobs passed or were skipped"
+        if: "!contains(needs.*.result, 'cancelled')"
         run: echo "All CI jobs passed or were skipped."
 
   notify-slack:
diff --git a/dev/README_AIRFLOW3_DEV.md b/dev/README_AIRFLOW3_DEV.md
index 51216378097..2f85f7482ee 100644
--- a/dev/README_AIRFLOW3_DEV.md
+++ b/dev/README_AIRFLOW3_DEV.md
@@ -196,9 +196,12 @@ Auto-merge is enabled for the whole repository, so every 
branch that PRs target
 then, its PRs never report the check, and a committer merges them by bypassing 
the rule.
 
 The `main` ruleset requires two checks. **CI passed** is a job in the `Tests 
(AMD)` workflow that always
-runs and fails when any other CI job failed or was cancelled; jobs that 
selective checks skip count as
-passed, so PRs that run only part of the tests still get a result. The 
`check-ci-gate-needs` prek hook
-makes sure every new CI job is added to it. `Tests (ARM)`, which runs on 
pushes and on a schedule rather
+runs and fails when any other CI job failed; jobs that selective checks skip 
count as passed, so PRs that
+run only part of the tests still get a result. A run with cancelled jobs and 
no failed one reports as
+**CI passed (run cancelled)** instead, so a duplicate run of the same commit 
that concurrency cancelled
+does not show a failed **CI passed** next to its twin's result. Such a run 
does not report **CI passed**
+at all, so the PR stays blocked until another run of the commit, or a re-run, 
reports it. The
+`check-ci-gate-needs` prek hook makes sure every new CI job is added to it. 
`Tests (ARM)`, which runs on pushes and on a schedule rather
 than on PRs, has the same job, kept identical by the 
`check-ci-workflows-in-sync` hook. For a PR that the
 fork CI gate (AIP-120) leaves to CI in the contributor's fork, project CI does 
not run, so the job reports
 as **CI passed (project CI not run)** instead, and the fork CI reconciler 
posts **CI passed** once the

Reply via email to