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 c6449eb429f Build the docs CI image once and let the registry share it 
(#70618)
c6449eb429f is described below

commit c6449eb429fd045037644dbc42986875fccec1f6
Author: Jarek Potiuk <[email protected]>
AuthorDate: Tue Jul 28 21:19:05 2026 +0200

    Build the docs CI image once and let the registry share it (#70618)
    
    * Build the docs CI image once and let the registry share it
    
    A docs publish paid for two CI image builds of the same sources, one for
    the docs and one for the provider registry, and only started the second
    once the docs were already live. Building it once up front and handing the
    same image to both lets them run side by side, which is most of the wait a
    release manager sits through after the docs have landed.
    
    Keeping the mount cache means consecutive publishes stop re-downloading the
    whole dependency set, and pinning both to one Python removes the second
    image that only differed by interpreter.
    
    * Retry the docs image build without a cache when the cached one fails
    
    The build the docs job used to run inline fell back to a plain buildx build
    whenever an old ref's pyproject.toml had drifted far enough from main that
    main's registry cache could not satisfy it. Moving the build into its own
    job dropped that safety net, which would turn a slow-but-correct build of
    an old ref into a hard failure.
    
    * Retry inside the image build instead of chaining a second build job
    
    Expressing the no-cache retry as its own job put a permanently-skipped job
    in every docs publish and forced both consumers to reason about which of
    two builds produced the image. The retry belongs where the build is, and
    callers that do not want it are unaffected because it is off by default.
---
 .github/workflows/ci-image-build.yml     | 19 +++++++-
 .github/workflows/publish-docs-to-s3.yml | 77 ++++++++++++++++++++++----------
 .github/workflows/registry-build.yml     | 54 +++++++++++++++++++---
 3 files changed, 118 insertions(+), 32 deletions(-)

diff --git a/.github/workflows/ci-image-build.yml 
b/.github/workflows/ci-image-build.yml
index e7c644568a6..f9aaa3aa473 100644
--- a/.github/workflows/ci-image-build.yml
+++ b/.github/workflows/ci-image-build.yml
@@ -94,6 +94,11 @@ on:  # yamllint disable-line rule:truthy
         description: "Docker cache specification to build the image (registry, 
local, disabled)."
         required: true
         type: string
+      retry-without-cache:
+        description: "Retry the build once with the cache disabled if the 
cached build fails (true/false)"
+        required: false
+        default: "false"
+        type: string
       disable-airflow-repo-cache:
         description: "Disable airflow repo cache read from main."
         required: true
@@ -169,9 +174,19 @@ jobs:
       - name: >
           Build ${{ inputs.push-image == 'true' && ' & push ' || '' }}
           ${{ inputs.platform }}:${{ env.PYTHON_MAJOR_MINOR_VERSION }} image
-        run: >
-          breeze ci-image build --platform "${PLATFORM}"
+        run: |
+          if breeze ci-image build --platform "${PLATFORM}"; then
+            exit 0
+          fi
+          if [[ "${RETRY_WITHOUT_CACHE}" != "true" ]]; then
+            exit 1
+          fi
+          # Reached when the ref's pyproject.toml has drifted far enough from 
the branch the
+          # cache was built for that the cached layers cannot satisfy it. 
Slow, but correct.
+          echo "Build with ${DOCKER_CACHE} cache failed - retrying with the 
cache disabled."
+          DOCKER_CACHE="disabled" breeze ci-image build --platform 
"${PLATFORM}"
         env:
+          RETRY_WITHOUT_CACHE: ${{ inputs.retry-without-cache }}
           DOCKER_CACHE: ${{ inputs.docker-cache }}
           DISABLE_AIRFLOW_REPO_CACHE: ${{ inputs.disable-airflow-repo-cache }}
           INSTALL_MYSQL_CLIENT_TYPE: ${{ inputs.install-mysql-client-type }}
diff --git a/.github/workflows/publish-docs-to-s3.yml 
b/.github/workflows/publish-docs-to-s3.yml
index eeb58b1759d..da8c17f1085 100644
--- a/.github/workflows/publish-docs-to-s3.yml
+++ b/.github/workflows/publish-docs-to-s3.yml
@@ -98,7 +98,9 @@ jobs:
       publish-supervisor-schema: ${{ 
steps.parameters.outputs.publish-supervisor-schema }}
       # yamllint disable rule:line-length
       skip-write-to-stable-folder: ${{ inputs.skip-write-to-stable-folder && 
'--skip-write-to-stable-folder' || '' }}
-      default-python-version: "3.10"
+      # The docs build and the registry extraction share one CI image, so they 
share its
+      # Python too. 3.12 is what the registry has always extracted on.
+      default-python-version: "3.12"
       registry-providers: ${{ 
steps.derive_registry_inputs.outputs.registry-providers }}
       registry-full-build: ${{ 
steps.derive_registry_inputs.outputs.registry-full-build }}
     if: contains(fromJSON('[
@@ -226,8 +228,39 @@ jobs:
           echo "publish-execution-api-schema=${PUBLISH_EXEC}" >> 
${GITHUB_OUTPUT}
           echo "publish-supervisor-schema=${PUBLISH_SUP}" >> ${GITHUB_OUTPUT}
 
-  build-docs:
+  build-ci-image:
+    name: "Build CI image"
     needs: [build-info]
+    uses: ./.github/workflows/ci-image-build.yml
+    permissions:
+      contents: read
+      packages: write
+    with:
+      runners: '["ubuntu-22.04"]'
+      platform: "linux/amd64"
+      # Built from the docs ref so the image matches the sources being 
documented, and
+      # cached against main's registry cache, which the regular Test workflow 
keeps warm.
+      checkout-ref: ${{ inputs.ref }}
+      push-image: "false"
+      upload-image-artifact: "true"
+      # Leaves the BuildKit mount cache behind for the next docs/registry run 
on this branch;
+      # ci-image-build.yml always restores it, so consecutive publishes stop 
re-downloading
+      # the whole dependency set.
+      upload-mount-cache-artifact: "true"
+      python-versions: ${{ format('["{0}"]', 
needs.build-info.outputs.default-python-version) }}
+      branch: "main"
+      constraints-branch: "constraints-main"
+      use-uv: "true"
+      upgrade-to-newer-dependencies: "false"
+      docker-cache: "registry"
+      disable-airflow-repo-cache: "false"
+      # Building against main's cache can fail outright when the ref's 
pyproject.toml has
+      # diverged from main — the case the docs job's inline `breeze ci-image 
build || docker
+      # buildx build` fallback used to cover.
+      retry-without-cache: "true"
+
+  build-docs:
+    needs: [build-info, build-ci-image]
     timeout-minutes: 150
     name: "Build documentation"
     runs-on: ubuntu-latest
@@ -293,29 +326,20 @@ jobs:
         uses: ./.github/actions/breeze
         with:
           python-version: "${{ needs.build-info.outputs.default-python-version 
}}"
-      - name: "Login to ghcr.io"
-        env:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-          ACTOR: ${{ github.actor }}
-        run: echo "${GITHUB_TOKEN}" | docker login ghcr.io -u "${ACTOR}" 
--password-stdin
-      - name: "Building image from the ${{ inputs.ref }} reference"
+      # The image comes from the `build-ci-image` job, which builds it from 
this same ref and
+      # stashes it. The registry build restores the very same stash, so a run 
builds it once.
+      - name: "Restore CI docker image linux/amd64:${{ 
needs.build-info.outputs.default-python-version }}"
+        uses: 
apache/infrastructure-actions/stash/restore@49df447b39b18354895520e0a63731b7cad7cbec
+        with:
+          key: ci-image-save-v3-linux/amd64-${{ 
needs.build-info.outputs.default-python-version }}
+          path: "/mnt/"
+          only-current-branch: 'true'
+          fail-on-download: 'true'
+      - name: "Load CI image linux/amd64:${{ 
needs.build-info.outputs.default-python-version }}"
         env:
-          INCLUDE_DOCS: ${{ needs.build-info.outputs.include-docs }}
-          INCLUDE_COMMITS: ${{ startsWith(inputs.ref, 'providers') && 'true' 
|| 'false' }}
-        # The regular `breeze ci-image build` path uses the registry cache 
pushed by the last
-        # successful "main" Test workflow (DOCKER_CACHE=registry, 
--cache-from=<main>:cache-<platform>),
-        # which is the fast path for builds from main or main-like refs. When 
building from an old
-        # ref whose pyproject.toml has diverged from main, reusing the main 
cache can fail the build,
-        # so we fall back to a plain buildx build with no cache-from.
+          PYTHON: ${{ needs.build-info.outputs.default-python-version }}
         run: >
-          breeze ci-image build ||
-          docker buildx build --load --builder default --progress=auto --pull
-          --build-arg AIRFLOW_EXTRAS=devel-ci --build-arg 
AIRFLOW_PRE_CACHED_PIP_PACKAGES=false
-          --build-arg AIRFLOW_USE_UV=true
-          --build-arg BUILD_PROGRESS=auto --build-arg 
INSTALL_MYSQL_CLIENT_TYPE=mariadb
-          --build-arg VERSION_SUFFIX_FOR_PYPI=dev0
-          -t 
"ghcr.io/apache/airflow/main/ci/python${PYTHON_MAJOR_MINOR_VERSION}:latest" 
--target main .
-          -f Dockerfile.ci --platform linux/amd64
+          breeze ci-image load --platform "linux/amd64" --python "${PYTHON}" 
--image-file-dir "/mnt"
       - name: "Restore docs inventory cache"
         uses: 
apache/infrastructure-actions/stash/restore@49df447b39b18354895520e0a63731b7cad7cbec
         with:
@@ -606,7 +630,9 @@ jobs:
             --destination-location "${SCHEMAS_DESTINATION}" "${args[@]}"
 
   update-registry:
-    needs: [publish-docs-to-s3, build-info]
+    # Runs alongside the docs build rather than after it: the registry reads 
nothing the docs
+    # publish produces, so waiting only added its whole duration to the 
release manager's wait.
+    needs: [build-info, build-ci-image]
     if: needs.build-info.outputs.registry-providers != '' || 
needs.build-info.outputs.registry-full-build == 'true'
     name: "Update Provider Registry"
     permissions:
@@ -616,6 +642,9 @@ jobs:
     with:
       destination: ${{ needs.build-info.outputs.destination }}
       provider: ${{ needs.build-info.outputs.registry-providers }}
+      python-version: ${{ needs.build-info.outputs.default-python-version }}
+      # `build-ci-image` already built and stashed the image this run.
+      ci-image-already-built: true
     secrets:
       DOCS_AWS_ACCESS_KEY_ID: ${{ secrets.DOCS_AWS_ACCESS_KEY_ID }}
       DOCS_AWS_SECRET_ACCESS_KEY: ${{ secrets.DOCS_AWS_SECRET_ACCESS_KEY }}
diff --git a/.github/workflows/registry-build.yml 
b/.github/workflows/registry-build.yml
index 472e0c185c1..e1932365173 100644
--- a/.github/workflows/registry-build.yml
+++ b/.github/workflows/registry-build.yml
@@ -33,6 +33,11 @@ on:  # yamllint disable-line rule:truthy
         required: false
         type: string
         default: ""
+      python-version:
+        description: "Python version of the CI image the extraction runs in"
+        required: false
+        type: string
+        default: "3.12"
   workflow_call:
     inputs:
       destination:
@@ -45,6 +50,16 @@ on:  # yamllint disable-line rule:truthy
         required: false
         type: string
         default: ""
+      python-version:
+        description: "Python version of the CI image the extraction runs in"
+        required: false
+        type: string
+        default: "3.12"
+      ci-image-already-built:
+        description: "Caller has already built and stashed the CI image for 
this run"
+        required: false
+        type: boolean
+        default: false
     secrets:
       DOCS_AWS_ACCESS_KEY_ID:
         required: true
@@ -62,7 +77,10 @@ jobs:
     permissions:
       contents: read
       packages: write
+    # Skipped when the caller stashed an image for this run; a standalone 
dispatch has no
+    # such image and still builds its own.
     if: >
+      (inputs.ci-image-already-built != true) && (
       github.event_name == 'workflow_call' ||
       contains(fromJSON('[
         "ashb",
@@ -78,14 +96,15 @@ jobs:
         "uranusjr",
         "utkarsharma2",
         "vincbeck"
-        ]'), github.event.sender.login)
+        ]'), github.event.sender.login))
     with:
       runners: '["ubuntu-22.04"]'
       platform: "linux/amd64"
       push-image: "false"
       upload-image-artifact: "true"
-      upload-mount-cache-artifact: "false"
-      python-versions: '["3.12"]'
+      # Kept so a standalone registry dispatch reuses the mount cache its last 
run left behind.
+      upload-mount-cache-artifact: "true"
+      python-versions: ${{ format('["{0}"]', inputs.python-version) }}
       branch: "main"
       constraints-branch: "constraints-main"
       use-uv: "true"
@@ -97,6 +116,27 @@ jobs:
     timeout-minutes: 45
     name: "Build & Publish Registry"
     needs: [build-ci-image]
+    # `build-ci-image` is skipped when the caller stashed the image, so this 
cannot simply
+    # inherit its result — but it must keep enforcing the same committer 
allowlist that
+    # skipping that job used to enforce for us on a dispatch.
+    if: >
+      !cancelled() && needs.build-ci-image.result != 'failure' && (
+      github.event_name == 'workflow_call' ||
+      contains(fromJSON('[
+        "ashb",
+        "bugraoz93",
+        "eladkal",
+        "ephraimbuddy",
+        "jedcunningham",
+        "jscheffl",
+        "kaxil",
+        "pierrejeambrun",
+        "shahar1",
+        "potiuk",
+        "uranusjr",
+        "utkarsharma2",
+        "vincbeck"
+        ]'), github.event.sender.login))
     runs-on: ubuntu-latest
     env:
       SCARF_ANALYTICS: "false"
@@ -125,7 +165,7 @@ jobs:
       - name: "Prepare breeze & CI image"
         uses: ./.github/actions/prepare_breeze_and_image
         with:
-          python: "3.12"
+          python: "${{ inputs.python-version }}"
           platform: "linux/amd64"
           use-uv: "true"
           make-mnt-writeable-and-cleanup: "true"
@@ -183,6 +223,7 @@ jobs:
         env:
           PROVIDER: ${{ inputs.provider }}
           DESTINATION: ${{ inputs.destination }}
+          PYTHON_VERSION: ${{ inputs.python-version }}
         run: |
           # Staging dispatches preview unreleased providers (maintainers want 
to
           # verify newly-bumped versions look right before tagging). Live 
builds
@@ -193,9 +234,10 @@ jobs:
             ALLOW_UNRELEASED="--allow-unreleased"
           fi
           if [[ -n "${PROVIDER}" ]]; then
-            breeze registry extract-data --python 3.12 --provider 
"${PROVIDER}" ${ALLOW_UNRELEASED}
+            breeze registry extract-data --python "${PYTHON_VERSION}" \
+              --provider "${PROVIDER}" ${ALLOW_UNRELEASED}
           else
-            breeze registry extract-data --python 3.12 ${ALLOW_UNRELEASED}
+            breeze registry extract-data --python "${PYTHON_VERSION}" 
${ALLOW_UNRELEASED}
           fi
 
       # --- Incremental: merge new data with existing ---

Reply via email to