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
