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 e69c1881b32 Refresh the image registry cache in a workflow that cannot 
be cancelled (#71171)
e69c1881b32 is described below

commit e69c1881b32c36abb827bae3717eaf46424427bd
Author: Jarek Potiuk <[email protected]>
AuthorDate: Wed Aug 5 18:49:10 2026 +0800

    Refresh the image registry cache in a workflow that cannot be cancelled 
(#71171)
    
    * Refresh the image registry cache in a workflow that cannot be cancelled
    
    A branch built on push refreshed its registry cache from inside the CI run, 
which
    cancels in progress on the next push. With no cache the build runs cold, 
which widens
    the window the next push cancels in, which leaves the cache missing -- 
v3-3-test held
    only the default Python version's amd64 cache because that was the one 
matrix entry
    that finished in time.
    
    The refresh also has to cover every Python version: a version with no cache 
entry
    builds from scratch on every run, and selective checks narrows the list on 
a text-only
    push.
    
    * Refresh constraints from a workflow of its own, covering every flavour
    
    The manual constraints refresh shared a workflow with the automatic uv.lock 
push, so it
    was filtered by that workflow's paths and buried under its name in the 
Actions list.
    
    The documented procedures pointed at local builds -- refresh_images.sh for 
the image
    cache, breeze commands for the constraints -- which need a buildx/qemu 
setup, a
    committer login to ghcr.io, and enough bandwidth to push the layers. The 
workflows need
    none of that, and unlike a local run they always cover every Python version 
and every
    constraint flavour.
    
    * Exempt a manually started cache refresh from the push cancellation
    
    A push-triggered refresh should give way to the next one -- it is building 
the cache of a
    commit that is no longer the tip. A manual run should not: it is started 
because the
    branch has no cache and the push runs are the thing that keeps getting 
cancelled.
---
 .github/workflows/additional-ci-image-checks.yml   |  73 +----------
 .github/workflows/ci-amd.yml                       |   7 -
 .github/workflows/ci-arm.yml                       |   7 -
 .github/workflows/refresh-constraints.yml          |  72 ++++++++++
 .github/workflows/refresh-image-registry-cache.yml | 145 +++++++++++++++++++++
 .github/workflows/update-constraints-on-push.yml   |  18 ++-
 dev/MANUALLY_BUILDING_IMAGES.md                    |   6 +-
 ...UALLY_GENERATING_IMAGE_CACHE_AND_CONSTRAINTS.md |  78 +++++++----
 dev/breeze/tests/test_selective_checks.py          |  21 +++
 9 files changed, 308 insertions(+), 119 deletions(-)

diff --git a/.github/workflows/additional-ci-image-checks.yml 
b/.github/workflows/additional-ci-image-checks.yml
index d0a4cb55fa6..42b71e1f32b 100644
--- a/.github/workflows/additional-ci-image-checks.yml
+++ b/.github/workflows/additional-ci-image-checks.yml
@@ -28,18 +28,10 @@ on:  # yamllint disable-line rule:truthy
         description: "Platform for the build - 'linux/amd64' or 'linux/arm64'"
         required: true
         type: string
-      python-versions:
-        description: "The list of python versions (stringified JSON array) to 
run the tests on."
-        required: true
-        type: string
       branch:
         description: "Branch used to run the CI jobs in (main/v*_*_test)."
         required: true
         type: string
-      constraints-branch:
-        description: "Branch used to get constraints from"
-        required: true
-        type: string
       default-python-version:
         description: "Which version of python should be used by default"
         required: true
@@ -52,80 +44,19 @@ on:  # yamllint disable-line rule:truthy
         description: "Whether to skip prek hooks (true/false)"
         required: true
         type: string
-      docker-cache:
-        description: "Docker cache specification to build the image (registry, 
local, disabled)."
-        required: true
-        type: string
-      disable-airflow-repo-cache:
-        description: "Disable airflow repo cache read from main."
-        required: true
-        type: string
-      canary-run:
-        description: "Whether this is a canary run (true/false)"
-        required: true
-        type: string
       latest-versions-only:
         description: "Whether to run only latest versions (true/false)"
         required: true
         type: string
-      include-success-outputs:
-        description: "Whether to include success outputs (true/false)"
-        required: true
-        type: string
       debug-resources:
         description: "Whether to debug resources (true/false)"
         required: true
         type: string
-      use-uv:
-        description: "Whether to use uv to build the image (true/false)"
-        required: true
-        type: string
 permissions:
   contents: read
 jobs:
-  # Push early BuildX cache to GitHub Registry in Apache repository. This 
cache does not wait for all the
-  # tests to complete - it is run very early in the build process in order to 
refresh cache using the
-  # current constraints. This will speed up cache refresh in cases when 
pyproject.toml changes or in case
-  # of Dockerfile changes. Failure in this step is not a problem (at most it 
will delay cache refresh).
-  # It does not attempt to upgrade to newer dependencies.
-  # We only push CI cache as PROD cache usually does not gain as much from 
fresh cache because
-  # it uses prepared airflow and provider distributions that invalidate the 
cache anyway most of the time
-  #
-  # Runs on `push` as well, not only on the scheduled canary. Release branches 
(`v*-*-test`) are built
-  # by `push` only, and their other refresh path -- the Regular cache push in 
`finalize-tests.yml` -- sits
-  # behind the whole test matrix. A branch that is red therefore never 
refreshes its cache at all, and the
-  # staler the cache gets the slower it is to turn green again. `main` is 
unaffected either way: it is not
-  # built on push, so it keeps refreshing through the scheduled canary.
-  #
-  # Pull requests stay excluded -- `canary-run` is also true for a PR carrying 
the `canary` label, and
-  # pushing cache from an unmerged branch is not something a label should 
authorise. This is the same
-  # condition the Regular cache push uses.
-  push-early-buildx-cache-to-github-registry:
-    name: Push Early Image Cache
-    uses: ./.github/workflows/push-image-cache.yml
-    permissions:
-      contents: read
-      # This write is only given here for `push` events from "apache/airflow" 
repo. It is not given for PRs
-      # from forks. This is to prevent malicious PRs from creating images in 
the "apache/airflow" repo.
-      packages: write
-    with:
-      runners: ${{ inputs.runners }}
-      cache-type: "Early"
-      include-prod-images: "false"
-      push-latest-images: "false"
-      platform: ${{ inputs.platform }}
-      python-versions: ${{ inputs.python-versions }}
-      branch: ${{ inputs.branch }}
-      constraints-branch: ${{ inputs.constraints-branch }}
-      use-uv: ${{ inputs.use-uv }}
-      include-success-outputs: ${{ inputs.include-success-outputs }}
-      docker-cache: ${{ inputs.docker-cache }}
-      disable-airflow-repo-cache: ${{ inputs.disable-airflow-repo-cache }}
-    if: inputs.canary-run == 'true' && github.event_name != 'pull_request'
-
-  # Check that after earlier cache push, breeze command will build quickly
-  # This build is a bit slow from in-the scratch builds, so we should run it 
only in
-  # regular PRs
+  # Check that the image builds quickly from the registry cache. This build is 
slow from
+  # scratch, so we only run it on regular PRs.
   check-that-image-builds-quickly:
     timeout-minutes: 25
     name: Check that image builds quickly
diff --git a/.github/workflows/ci-amd.yml b/.github/workflows/ci-amd.yml
index 72ef23494d7..63b07e4ce6d 100644
--- a/.github/workflows/ci-amd.yml
+++ b/.github/workflows/ci-amd.yml
@@ -297,19 +297,12 @@ jobs:
     with:
       runners: ${{ needs.build-info.outputs.runner-type }}
       platform: ${{ needs.build-info.outputs.platform }}
-      python-versions: ${{ needs.build-info.outputs.python-versions }}
       branch: ${{ needs.build-info.outputs.default-branch }}
-      constraints-branch: ${{ 
needs.build-info.outputs.default-constraints-branch }}
       default-python-version: "${{ 
needs.build-info.outputs.default-python-version }}"
       upgrade-to-newer-dependencies: ${{ 
needs.build-info.outputs.upgrade-to-newer-dependencies }}
       skip-prek-hooks: ${{ needs.build-info.outputs.skip-prek-hooks }}
-      docker-cache: ${{ needs.build-info.outputs.docker-cache }}
-      disable-airflow-repo-cache: ${{ 
needs.build-info.outputs.disable-airflow-repo-cache }}
-      canary-run: ${{ needs.build-info.outputs.canary-run }}
       latest-versions-only: ${{ needs.build-info.outputs.latest-versions-only 
}}
-      include-success-outputs: ${{ 
needs.build-info.outputs.include-success-outputs }}
       debug-resources: ${{ needs.build-info.outputs.debug-resources }}
-      use-uv: ${{ needs.build-info.outputs.use-uv }}
 
   generate-constraints:
     name: "Generate constraints"
diff --git a/.github/workflows/ci-arm.yml b/.github/workflows/ci-arm.yml
index 8c62297ea7e..0c179002e87 100644
--- a/.github/workflows/ci-arm.yml
+++ b/.github/workflows/ci-arm.yml
@@ -286,19 +286,12 @@ jobs:
     with:
       runners: ${{ needs.build-info.outputs.runner-type }}
       platform: ${{ needs.build-info.outputs.platform }}
-      python-versions: ${{ needs.build-info.outputs.python-versions }}
       branch: ${{ needs.build-info.outputs.default-branch }}
-      constraints-branch: ${{ 
needs.build-info.outputs.default-constraints-branch }}
       default-python-version: "${{ 
needs.build-info.outputs.default-python-version }}"
       upgrade-to-newer-dependencies: ${{ 
needs.build-info.outputs.upgrade-to-newer-dependencies }}
       skip-prek-hooks: ${{ needs.build-info.outputs.skip-prek-hooks }}
-      docker-cache: ${{ needs.build-info.outputs.docker-cache }}
-      disable-airflow-repo-cache: ${{ 
needs.build-info.outputs.disable-airflow-repo-cache }}
-      canary-run: ${{ needs.build-info.outputs.canary-run }}
       latest-versions-only: ${{ needs.build-info.outputs.latest-versions-only 
}}
-      include-success-outputs: ${{ 
needs.build-info.outputs.include-success-outputs }}
       debug-resources: ${{ needs.build-info.outputs.debug-resources }}
-      use-uv: ${{ needs.build-info.outputs.use-uv }}
 
   generate-constraints:
     name: "Generate constraints"
diff --git a/.github/workflows/refresh-constraints.yml 
b/.github/workflows/refresh-constraints.yml
new file mode 100644
index 00000000000..58295b6c092
--- /dev/null
+++ b/.github/workflows/refresh-constraints.yml
@@ -0,0 +1,72 @@
+# 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.
+#
+---
+name: Refresh constraints
+# Refreshes the pinned constraint files on demand, when waiting for the 
automatic path is not an
+# option -- a `main` / `vX-Y-test` build that cannot be made green, or newly 
released providers
+# that a release candidate has to pick up before it is promoted.
+#
+# All three constraint flavours are refreshed, not just the PyPI one:
+#
+#   * `constraints-source-providers-X.Y.txt` -- providers resolved from this 
checkout's sources,
+#     which is what a contributor's Breeze environment and CI install from;
+#   * `constraints-no-providers-X.Y.txt` -- core dependencies alone, used to 
install Airflow
+#     without any provider pinned;
+#   * `constraints-X.Y.txt` -- providers as published on PyPI, which is what 
users install with.
+#
+# Refreshing only the PyPI flavour would leave CI and Breeze on the older 
pins, so the three
+# would disagree about the same dependency.
+#
+# This does the same work as the automatic `uv.lock` push (whose workflow it 
calls), against the
+# ref you name rather than the branch the push landed on. Run it from `main`: 
the workflow
+# definition and `breeze` come from `main`, while the sources come from `ref`, 
so refreshing a
+# release line needs no cherry-pick to `vX-Y-test` / `vX-Y-stable` first. The 
`constraints-X-Y`
+# branch written to is derived from that ref's `branch_defaults.py`.
+#
+# See dev/MANUALLY_GENERATING_IMAGE_CACHE_AND_CONSTRAINTS.md.
+on:  # yamllint disable-line rule:truthy
+  workflow_dispatch:
+    inputs:
+      ref:
+        description: "Repo reference to refresh constraints from (e.g. 
v3-3-stable). Empty = this branch."
+        required: false
+        default: ""
+        type: string
+      upgrade-to-newer-dependencies:
+        description: "Upgrade deps to newest from PyPI"
+        required: false
+        type: boolean
+        default: true
+permissions:
+  contents: read
+concurrency:
+  # Two refreshes of the same ref would race each other onto the same 
`constraints-X-Y` branch.
+  group: refresh-constraints-${{ inputs.ref || github.ref }}
+  cancel-in-progress: false
+jobs:
+  refresh-constraints:
+    name: "Refresh constraints"
+    uses: ./.github/workflows/update-constraints-on-push.yml
+    permissions:
+      contents: write
+      packages: write
+    with:
+      ref: ${{ inputs.ref }}
+      upgrade-to-newer-dependencies: ${{ inputs.upgrade-to-newer-dependencies 
}}
+    secrets:
+      SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
diff --git a/.github/workflows/refresh-image-registry-cache.yml 
b/.github/workflows/refresh-image-registry-cache.yml
new file mode 100644
index 00000000000..23b3e6221dc
--- /dev/null
+++ b/.github/workflows/refresh-image-registry-cache.yml
@@ -0,0 +1,145 @@
+# 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.
+#
+---
+name: Refresh image registry cache
+# Refreshes the CI image cache in the GitHub registry, in a run of its own.
+#
+# It has to be its own workflow rather than a job inside `ci-amd.yml` / 
`ci-arm.yml`, because a
+# job cannot opt out of its run being cancelled, and those runs are cancelled 
by the next push.
+# With no cache a branch builds cold (~20 minutes per Python version), which 
widens the window in
+# which the next push cancels the run, which leaves the cache missing. 
`v3-3-test` sat in that
+# loop -- its `linux/amd64` cache covered the default Python version alone, 
the single matrix
+# entry that finished before the next push killed the other four seconds 
later. Being a separate
+# workflow is what lets a manual run escape that (see the concurrency group 
below).
+#
+# Every Python version is refreshed in both trigger paths. That is what makes 
the cache useful:
+# a version with no cache entry builds from scratch on every run, whichever 
way it got skipped.
+on:  # yamllint disable-line rule:truthy
+  workflow_dispatch:
+    inputs:
+      platform:
+        description: "Platform to refresh the cache for"
+        type: choice
+        options:
+          - "both"
+          - "linux/amd64"
+          - "linux/arm64"
+        default: "both"
+  push:
+    # Release-prep and providers branches only -- they are built on push, so 
the CI run that
+    # would have refreshed their cache is also the run that keeps being 
cancelled. `main` is not
+    # built on push; its cache is refreshed by the Push Image Cache job at the 
end of the
+    # scheduled canary, which runs to completion.
+    branches:
+      - v[0-9]+-[0-9]+-test
+      - providers-[a-z]+-?[a-z]*/v[0-9]+-[0-9]+
+permissions:
+  contents: read
+concurrency:
+  # Push runs are never cancelled -- being cancelled by the next merge is the 
whole failure this
+  # workflow exists to escape, so a newer one queues behind the one in flight 
rather than
+  # replacing it. GitHub keeps at most one run pending per group, so the queue 
cannot grow.
+  #
+  # Manual runs are grouped separately, which is what keeps a merge from 
cancelling one, and they
+  # do supersede each other: firing the workflow again for the same branch 
says the run in flight
+  # is not the one you want.
+  group: refresh-image-registry-cache-${{ github.ref }}-${{ github.event_name 
}}
+  cancel-in-progress: ${{ github.event_name == 'workflow_dispatch' }}
+jobs:
+  build-info:
+    name: "Build info"
+    # No outputs yet at this stage, so the runner has to be hard-coded to a 
public one.
+    runs-on: ["ubuntu-22.04"]
+    outputs:
+      default-branch: ${{ steps.selective-checks.outputs.default-branch }}
+      default-constraints-branch: ${{ 
steps.selective-checks.outputs.default-constraints-branch }}
+      disable-airflow-repo-cache: ${{ 
steps.selective-checks.outputs.disable-airflow-repo-cache }}
+      docker-cache: ${{ steps.selective-checks.outputs.docker-cache }}
+      include-success-outputs: ${{ 
steps.selective-checks.outputs.include-success-outputs }}
+      python-versions: ${{ steps.selective-checks.outputs.python-versions }}
+      use-uv: ${{ steps.selective-checks.outputs.force-pip == 'true' && 
'false' || 'true' }}
+    steps:
+      - name: "Cleanup repo"
+        shell: bash
+        run: sudo rm -rf ${GITHUB_WORKSPACE}/*
+      - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
+        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1  # 
v7.0.1
+        with:
+          persist-credentials: false
+      - name: "Install Breeze"
+        uses: ./.github/actions/breeze
+      - name: Selective checks
+        id: selective-checks
+        env:
+          # Forces every Python version. Selective checks narrows 
`python-versions` down to the
+          # default one on a push where only text files changed
+          # (`_should_run_all_tests_and_versions`), which would leave the rest 
of the versions
+          # with no cache -- the failure mode this workflow exists to prevent.
+          PR_LABELS: '["all versions"]'
+          COMMIT_REF: "${{ github.sha }}"
+          VERBOSE: "false"
+        run: breeze ci selective-check 2>> ${GITHUB_OUTPUT}
+
+  refresh-ci-cache-amd:
+    name: "Refresh cache linux/amd64"
+    needs: [build-info]
+    uses: ./.github/workflows/push-image-cache.yml
+    permissions:
+      contents: read
+      # Only granted for `push` / `workflow_dispatch` on apache/airflow 
itself, never for forks.
+      packages: write
+    with:
+      runners: '["ubuntu-22.04"]'
+      platform: "linux/amd64"
+      cache-type: "Refresh"
+      # The PROD cache is built from distributions prepared earlier in a CI 
run, and a standalone
+      # refresh has no such artifact. The CI cache is also where the build 
time goes.
+      include-prod-images: "false"
+      # `latest` is what a build pulls when it has no cache at all. Moving it 
belongs to a canary
+      # run that has passed its tests, not to a cache refresh.
+      push-latest-images: "false"
+      python-versions: ${{ needs.build-info.outputs.python-versions }}
+      branch: ${{ needs.build-info.outputs.default-branch }}
+      constraints-branch: ${{ 
needs.build-info.outputs.default-constraints-branch }}
+      use-uv: ${{ needs.build-info.outputs.use-uv }}
+      include-success-outputs: ${{ 
needs.build-info.outputs.include-success-outputs }}
+      docker-cache: ${{ needs.build-info.outputs.docker-cache }}
+      disable-airflow-repo-cache: ${{ 
needs.build-info.outputs.disable-airflow-repo-cache }}
+    if: github.event_name == 'push' || inputs.platform == 'both' || 
inputs.platform == 'linux/amd64'
+
+  refresh-ci-cache-arm:
+    name: "Refresh cache linux/arm64"
+    needs: [build-info]
+    uses: ./.github/workflows/push-image-cache.yml
+    permissions:
+      contents: read
+      packages: write
+    with:
+      runners: '["ubuntu-22.04-arm"]'
+      platform: "linux/arm64"
+      cache-type: "Refresh"
+      include-prod-images: "false"
+      push-latest-images: "false"
+      python-versions: ${{ needs.build-info.outputs.python-versions }}
+      branch: ${{ needs.build-info.outputs.default-branch }}
+      constraints-branch: ${{ 
needs.build-info.outputs.default-constraints-branch }}
+      use-uv: ${{ needs.build-info.outputs.use-uv }}
+      include-success-outputs: ${{ 
needs.build-info.outputs.include-success-outputs }}
+      docker-cache: ${{ needs.build-info.outputs.docker-cache }}
+      disable-airflow-repo-cache: ${{ 
needs.build-info.outputs.disable-airflow-repo-cache }}
+    if: github.event_name == 'push' || inputs.platform == 'both' || 
inputs.platform == 'linux/arm64'
diff --git a/.github/workflows/update-constraints-on-push.yml 
b/.github/workflows/update-constraints-on-push.yml
index e7d273f319b..c0fc95b3e16 100644
--- a/.github/workflows/update-constraints-on-push.yml
+++ b/.github/workflows/update-constraints-on-push.yml
@@ -16,14 +16,15 @@
 # under the License.
 #
 ---
-name: Update constraints (on uv.lock push or manual dispatch)
+name: Update constraints (on uv.lock push)
 # This workflow refreshes the pinned constraint files stored in the
 # `constraints-*` branches. It runs automatically whenever `uv.lock` changes on
-# `main` or a `vX-Y-test` branch, and can also be triggered manually via the
-# "Run workflow" button (workflow_dispatch) - for example to pick up newly
-# released providers/dependencies from PyPI just before promoting an RC.
+# `main` or a `vX-Y-test` branch. The manual path lives in
+# `refresh-constraints.yml`, which calls this workflow -- keeping the two 
triggers
+# in separate workflows means a manual refresh is not filtered by `paths: 
uv.lock`
+# and shows up in the Actions list under its own name.
 #
-# The manual run is always launched from `main` (so the workflow definition and
+# A manual run is always launched from `main` (so the workflow definition and
 # `breeze` come from `main`), and the `ref` input selects the commit-ish
 # (branch, tag or commit hash) whose sources the constraints are refreshed 
from.
 # The `constraints-X-Y` branch to push to is derived from that ref's
@@ -37,7 +38,7 @@ on:  # yamllint disable-line rule:truthy
       - v[0-9]+-[0-9]+-test
     paths:
       - 'uv.lock'
-  workflow_dispatch:
+  workflow_call:
     inputs:
       # `ref` is optional. Leave it empty to refresh constraints for the branch
       # you run the workflow from (normally `main` -> `constraints-main`). Set 
a
@@ -51,8 +52,13 @@ on:  # yamllint disable-line rule:truthy
         type: string
       upgrade-to-newer-dependencies:
         description: "Upgrade deps to newest from PyPI"
+        required: false
         type: boolean
         default: true
+    secrets:
+      SLACK_BOT_TOKEN:
+        description: "Slack bot token used to post provider-downgrade alerts 
(optional)."
+        required: false
 permissions:
   contents: read
 
diff --git a/dev/MANUALLY_BUILDING_IMAGES.md b/dev/MANUALLY_BUILDING_IMAGES.md
index 18ba10dd210..aa0c8a4c14f 100644
--- a/dev/MANUALLY_BUILDING_IMAGES.md
+++ b/dev/MANUALLY_BUILDING_IMAGES.md
@@ -94,7 +94,7 @@ The benefit of this method is that the images can be built 
and pushed in a singl
 to build and push them separately and perform the additional manifest merge 
step.
 
 ```bash
-docker buildx create --name airflow_cache --driver docker-container 
unix:///var/run/docker.sock  # your local builder (you might want to use
+docker buildx create --name airflow_cache --driver docker-container 
unix:///var/run/docker.sock  # your local builder
 docker buildx create --name airflow_cache --append HOST:PORT  # your remote 
builder
 ```
 
@@ -167,7 +167,7 @@ with appropriate versions).
 You can then merge the images into a single multi-platform image with:
 
 ```bash
-docker release-management merge-prod-images --airflow-version "${VERSION}" 
--metadata-folder dist
+breeze release-management merge-prod-images --airflow-version "${VERSION}" 
--metadata-folder dist
 ```
 
 The same can be repeated for slim images by adding `--slim-images` option to 
the command, the manifests are
@@ -189,5 +189,5 @@ breeze release-management release-prod-images --slim-images 
--airflow-version "$
 Merging the images:
 
 ```bash
-docker release-management merge-prod-images --slim-images --airflow-version 
"${VERSION}" --metadata-folder dist
+breeze release-management merge-prod-images --slim-images --airflow-version 
"${VERSION}" --metadata-folder dist
 ```
diff --git a/dev/MANUALLY_GENERATING_IMAGE_CACHE_AND_CONSTRAINTS.md 
b/dev/MANUALLY_GENERATING_IMAGE_CACHE_AND_CONSTRAINTS.md
index 01bdd7f7504..cd07deec80b 100644
--- a/dev/MANUALLY_GENERATING_IMAGE_CACHE_AND_CONSTRAINTS.md
+++ b/dev/MANUALLY_GENERATING_IMAGE_CACHE_AND_CONSTRAINTS.md
@@ -25,8 +25,9 @@
 - [Automated image cache and constraints refreshing in 
CI](#automated-image-cache-and-constraints-refreshing-in-ci)
 - [Manually refreshing the image cache](#manually-refreshing-the-image-cache)
   - [Why we need to update image cache 
manually](#why-we-need-to-update-image-cache-manually)
-  - [Prerequisites](#prerequisites)
-  - [How to refresh the image cache](#how-to-refresh-the-image-cache)
+  - [How to refresh the image cache via the CI workflow 
(recommended)](#how-to-refresh-the-image-cache-via-the-ci-workflow-recommended)
+  - [Prerequisites for refreshing 
locally](#prerequisites-for-refreshing-locally)
+  - [How to refresh the image cache 
locally](#how-to-refresh-the-image-cache-locally)
   - [Is it safe to refresh the image 
cache?](#is-it-safe-to-refresh-the-image-cache)
   - [What the command does](#what-the-command-does)
 - [Manually generating constraint files](#manually-generating-constraint-files)
@@ -69,17 +70,17 @@ rebuilding of [Breeze](./breeze/doc/README.rst) images for 
development purpose.
 * The latest 
[constraints](/contributing-docs/13_airflow_dependencies_and_extras.rst#pinned-constraint-files)
 are pushed to appropriate branch after all tests succeed in the
   `canary` build.
 
-* The [images](breeze/doc/ci/02_images.md) in `ghcr.io` registry are refreshed 
early at the beginning of the
-  `canary` build. This is done twice during the canary build:
-   * By the `Push Early Image Cache` job that is run at the beginning of the 
`canary` build. This cover the
-     case when there are new dependencies added or Dockerfile/scripts change. 
Thanks to that step, subsequent
-     PRs will be faster when they use the new Dockerfile/script. Those jobs 
**might fail** occasionally,
-     if the latest PR added some conflicting dependencies with current 
constraints. This is not a problem
-     and when it happens, it will be fixed by the next step.
-   * By the `Push Image Cache` job that is run at the end of the `canary` 
build. This covers the case when
-     cache is also refreshed after than `main` build succeeds after the new 
constraints are pushed. This
-     step makes sure that constraints are committed and pushed just before the 
cache is refreshed, so
-     there is no problem with conflicting dependencies.
+* The [images](breeze/doc/ci/02_images.md) in `ghcr.io` registry are refreshed 
by the `Push Image Cache`
+  job at the end of the `canary` build, once the tests have passed. Running it 
last is what makes the
+  constraints and the cache agree: the constraints are committed and pushed 
just before the cache is
+  refreshed from them.
+
+* Branches that are built on `push` rather than by the scheduled `canary` -- 
the release-prep
+  (`vX-Y-test`) and providers branches -- are refreshed by the
+  [`Refresh image registry 
cache`](../.github/workflows/refresh-image-registry-cache.yml) workflow
+  instead. It runs on its own rather than inside the CI run, because the CI 
run is cancelled by the
+  next push and a cache refresh that keeps being cancelled never lands. It 
covers every Python version
+  on both platforms, and it is the same workflow you run by hand (see below).
 
 
 # Manually refreshing the image cache
@@ -87,14 +88,35 @@ rebuilding of [Breeze](./breeze/doc/README.rst) images for 
development purpose.
 ## Why we need to update image cache manually
 
 Sometimes, when we have a problem with our CI running and flakiness of GitHub 
Actions runners or our
-tests, the refresh might not be triggered. This has been mitigated by "Push 
Early Image Cache" job added in
-our CI, but there are other reasons you might want to refresh the cache. 
Sometimes we want to refresh the
+tests, the refresh might not be triggered. Sometimes we want to refresh the
 image cache in `vX_Y_test` branch (following our convention of branch names 
`vX_Y_test` branch is the branch
 used to release all `X.Y.*` versions of airflow) before we attempt to push a 
change there.
 There are no PRs happening in this branch, so manual refresh before we make a 
PR might speed up the PR build.
 Or sometimes we just refreshed the constraints (see below) and we want the 
cache to include those.
 
-## Prerequisites
+## How to refresh the image cache via the CI workflow (recommended)
+
+Run the [`Refresh image registry 
cache`](../.github/workflows/refresh-image-registry-cache.yml) workflow
+from the Actions tab, selecting the branch whose cache you want to refresh in 
GitHub's branch dropdown --
+the cache is namespaced per branch, so the branch you pick is the namespace 
written to. The `platform`
+input refreshes `linux/amd64`, `linux/arm64`, or both (the default).
+
+Prefer this over the local route below. The workflow needs no buildx / qemu 
setup on your machine, it
+does not depend on your upload bandwidth, it always covers every Python 
version, and it runs with the
+registry credentials the CI already has, so you do not need to be logged in to 
`ghcr.io` as a committer.
+
+A run you start by hand is not cancelled by merges landing while it works -- 
it uses a concurrency
+group of its own for that reason. Starting it again for the same branch does 
replace the run in
+flight. Push-triggered refreshes are never cancelled either; a newer one waits 
for the one in
+flight to finish.
+
+The workflow definition has to exist on the branch you select, so a release 
branch needs the workflow
+backported before it can be refreshed there.
+
+## Prerequisites for refreshing locally
+
+The rest of this chapter describes refreshing the cache from your own machine. 
It is a fallback for when
+the workflow cannot be used at all -- it is slower, and easy to get subtly 
wrong.
 
 Note that in order to refresh images you have to not only have `buildx` 
command installed for docker,
 but you should also make sure that you have the buildkit builder configured 
and set.
@@ -146,7 +168,7 @@ docker buildx ls
        airflow_cache1    tcp://127.0.0.1:2375
 ```
 
-## How to refresh the image cache
+## How to refresh the image cache locally
 
 The images can be rebuilt and refreshed after the constraints are pushed. 
Refreshing image for all
 python version is as simple as running the 
[refresh_images.sh](refresh_images.sh) script which will
@@ -217,11 +239,17 @@ and you need to be sure what you are doing, but you can 
always do it manually if
 ## How to refresh constraints via the CI workflow (recommended)
 
 The easiest way to refresh the constraints - for example to pick up newly 
released
-providers/dependencies from PyPI just before promoting an RC - is to trigger 
the
-[`Update constraints`](../.github/workflows/update-constraints-on-push.yml) 
workflow manually
-instead of running the `breeze` commands locally. The workflow runs exactly 
the same steps
-that run automatically when `uv.lock` changes, builds the CI images, generates 
all constraint
-flavours and commits/pushes them to the matching `constraints-*` branch.
+providers/dependencies from PyPI just before promoting an RC - is to run the
+[`Refresh constraints`](../.github/workflows/refresh-constraints.yml) workflow 
instead of
+running the `breeze` commands locally. It runs exactly the same steps that run 
automatically
+when `uv.lock` changes (it calls that workflow), builds the CI images, and 
commits/pushes to the
+matching `constraints-*` branch.
+
+It refreshes **all three constraint flavours**, not only the PyPI one:
+`constraints-source-providers-X.Y.txt` (providers from the sources, which is 
what CI and Breeze
+install), `constraints-no-providers-X.Y.txt` (core alone) and 
`constraints-X.Y.txt` (providers as
+published on PyPI, which is what users install). Refreshing only the PyPI 
flavour would leave CI
+and Breeze pinned to the older versions, so the three would disagree about the 
same dependency.
 
 The manual run is always launched from `main` and takes a `ref` input that 
selects the
 commit-ish (branch, tag or commit hash) whose sources the constraints are 
refreshed from. You
@@ -236,13 +264,13 @@ workflow); the automatic `uv.lock`-push runs are not 
restricted.
 
 To run it:
 
-![Run the Update constraints 
workflow](images/update_constraints_run_workflow.png)
+![Run the Refresh constraints 
workflow](images/update_constraints_run_workflow.png)
 
-1. Go to the [`Update 
constraints`](https://github.com/apache/airflow/actions/workflows/update-constraints-on-push.yml)
+1. Go to the [`Refresh 
constraints`](https://github.com/apache/airflow/actions/workflows/refresh-constraints.yml)
    workflow in the Actions tab.
 2. Click **Run workflow** and keep the branch set to `main` (this is where the 
workflow runs
    from - it is not the branch whose constraints get refreshed).
-3. In the **Optional repo reference to build constraints** field, enter the 
ref to refresh
+3. In the **Repo reference to refresh constraints from** field, enter the ref 
to refresh
    constraints from - for example `v3-3-test`, `v3-3-stable`, 
`constraints-3-3`, an RC tag, or a
    commit hash. Leave it empty to refresh the branch you run from (`main` -> 
`constraints-main`).
    The matching `constraints-X-Y` branch to push to is derived automatically 
from that ref's
diff --git a/dev/breeze/tests/test_selective_checks.py 
b/dev/breeze/tests/test_selective_checks.py
index a99d5c7bf86..d516aaeee4f 100644
--- a/dev/breeze/tests/test_selective_checks.py
+++ b/dev/breeze/tests/test_selective_checks.py
@@ -29,6 +29,7 @@ from airflow_breeze.global_constants import (
     ALLOWED_PYTHON_MAJOR_MINOR_VERSIONS,
     CI_AMD_PLATFORM,
     CI_ARM_PLATFORM,
+    CURRENT_PYTHON_MAJOR_MINOR_VERSIONS,
     DEFAULT_KUBERNETES_VERSION,
     DEFAULT_PYTHON_MAJOR_MINOR_VERSION,
     JAVA_SDK_VERSION,
@@ -56,6 +57,9 @@ ALL_KUBERNETES_VERSIONS_AS_STRING = " 
".join(ALLOWED_KUBERNETES_VERSIONS)
 ALL_KUBERNETES_VERSIONS_AS_LIST = "[" + ", ".join([f"'{v}'" for v in 
ALLOWED_KUBERNETES_VERSIONS]) + "]"
 ALL_PYTHON_VERSIONS_AS_STRING = " ".join(ALLOWED_PYTHON_MAJOR_MINOR_VERSIONS)
 ALL_PYTHON_VERSIONS_AS_LIST = "[" + ", ".join([f"'{v}'" for v in 
ALLOWED_PYTHON_MAJOR_MINOR_VERSIONS]) + "]"
+CURRENT_PYTHON_VERSIONS_AS_LIST = (
+    "[" + ", ".join([f"'{v}'" for v in CURRENT_PYTHON_MAJOR_MINOR_VERSIONS]) + 
"]"
+)
 
 DEFAULT_HELM_K8S_VERSION = ALLOWED_KUBERNETES_VERSIONS[0].lstrip("v")
 LAST_HELM_K8S_VERSION = ALLOWED_KUBERNETES_VERSIONS[-1].lstrip("v")
@@ -2884,6 +2888,23 @@ def 
test_no_commit_provided_trigger_full_build_for_any_event_type(github_event):
     )
 
 
+# The image cache is pushed for `python-versions`, so a narrowed list leaves 
the remaining
+# versions with no cache and they build from scratch on every run. 
`refresh-image-registry-cache.yml`
+# forces the label below for exactly that reason; this pins the behaviour it 
relies on.
+
+
+def test_all_python_versions_with_all_versions_label():
+    """Set on the event that would otherwise narrow: a text-only push."""
+    stderr = SelectiveChecks(
+        files=("INTHEWILD.md",),
+        commit_ref=NEUTRAL_COMMIT,
+        github_event=GithubEvents.PUSH,
+        pr_labels=("all versions",),
+        default_branch="main",
+    )
+    assert_outputs_are_printed({"python-versions": 
CURRENT_PYTHON_VERSIONS_AS_LIST}, str(stderr))
+
+
 @pytest.mark.parametrize(
     "github_event",
     [

Reply via email to