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 f346e6449c6 Skip the docs CI image build when that ref was already 
built (#70675)
f346e6449c6 is described below

commit f346e6449c608d62aabdb61686338020bc4b897e
Author: Jarek Potiuk <[email protected]>
AuthorDate: Wed Jul 29 13:04:08 2026 +0200

    Skip the docs CI image build when that ref was already built (#70675)
    
    Docs for a ref are published more than once - an RC, then the final docs
    after the vote - and the second publish rebuilds an image the first one
    already built from the very same commit. Seeding the build cache with that
    image, as #70650 does, still pays for a docker load, a registry push, a
    build that hits cache on every layer, an export and two multi-gigabyte
    stash uploads, none of which can produce anything the first publish did not
    already produce.
    
    Reusing the image outright rather than as a cache turns on knowing it was
    built from this commit. A ref does not say that: a branch documented twice
    moves between the two publishes, so its image is a good cache and a wrong
    answer. The commit each image is stashed with is what tells those apart,
    and it is stashed on its own so that deciding costs a few bytes rather than
    the image the decision may make unnecessary.
    
    Reusing it also means publishing nothing, which the shared per-branch stash
    could not support: it is written by every build on this branch, so whoever
    restores it next would get sources they never asked for. The ref's own
    stash is the one this run's docs and registry jobs now read, so a run with
    nothing to build leaves them reading what the previous publish left there.
    The mount cache moves with it for the same reason - it holds the dependency
    set the sources resolve to, and a ref's and the branch tip's are exactly
    what differ.
---
 .../actions/prepare_breeze_and_image/action.yml    |  13 +-
 .github/workflows/ci-image-build.yml               | 154 ++++++++++++++++-----
 .github/workflows/publish-docs-to-s3.yml           |  22 +--
 .github/workflows/registry-build.yml               |   9 ++
 4 files changed, 150 insertions(+), 48 deletions(-)

diff --git a/.github/actions/prepare_breeze_and_image/action.yml 
b/.github/actions/prepare_breeze_and_image/action.yml
index 6e6c4efd205..ce116477567 100644
--- a/.github/actions/prepare_breeze_and_image/action.yml
+++ b/.github/actions/prepare_breeze_and_image/action.yml
@@ -34,6 +34,12 @@ inputs:
   make-mnt-writeable-and-cleanup:
     description: 'Whether to cleanup /mnt'
     required: true
+  image-stash-ref:
+    description: >
+      Ref whose image stash to restore (empty = the per-branch one). An image 
built for a ref
+      other than the branch tip is stashed under a key of its own by 
ci-image-build.yml.
+    required: false
+    default: ""
 outputs:
   host-python-version:
     description: Python version used in host
@@ -56,10 +62,13 @@ runs:
       run: |
         echo "Checking free space!"
         df -H
-    - name: "Restore ${{ inputs.image-type }} docker image ${{ inputs.platform 
}}:${{ inputs.python }}"
+    - name: >
+        Restore ${{ inputs.image-type }} docker image ${{ inputs.platform 
}}:${{ inputs.python }}
+        ${{ inputs.image-stash-ref != '' && format('built for ref {0}', 
inputs.image-stash-ref) || '' }}
       uses: 
apache/infrastructure-actions/stash/restore@49df447b39b18354895520e0a63731b7cad7cbec
       with:
-        key: ${{ inputs.image-type }}-image-save-v3-${{ inputs.platform }}-${{ 
inputs.python }}
+        key: "${{ inputs.image-type }}-image-save-v3-${{ inputs.platform 
}}-${{ inputs.python }}\
+          ${{ inputs.image-stash-ref != '' && format('-{0}', 
inputs.image-stash-ref) || '' }}"
         path: "/mnt/"
         only-current-branch: 'true'
         fail-on-download: 'true'
diff --git a/.github/workflows/ci-image-build.yml 
b/.github/workflows/ci-image-build.yml
index df116423717..3af9c02fa39 100644
--- a/.github/workflows/ci-image-build.yml
+++ b/.github/workflows/ci-image-build.yml
@@ -112,13 +112,13 @@ on:  # yamllint disable-line rule:truthy
         required: false
         default: "false"
         type: string
-      image-stash-suffix:
+      image-stash-ref:
         description: >
-          Appended verbatim to the key of an extra copy of the image stash (so 
it needs its own
-          leading separator). Callers that build many different refs from one 
branch pass the ref
-          here to keep a copy per ref: they all share the plain per-branch 
stash and would
-          otherwise seed each other's builds, and refs far enough apart make 
poor caches for one
-          another. Seeding prefers this copy and falls back to the per-branch 
one.
+          Stash the image under a key of its own for this ref instead of the 
per-branch one.
+          Callers that build many different refs from one branch pass the ref 
here: sharing the
+          per-branch stash would make them seed each other's builds, and refs 
far enough apart
+          make poor caches for one another. Seeding prefers this stash and 
falls back to the
+          per-branch one; whoever consumes the image afterwards must restore 
the same key.
         required: false
         default: ""
         type: string
@@ -160,12 +160,68 @@ jobs:
         run: ./scripts/ci/move_docker_to_mnt.sh
       - name: "Install Breeze"
         uses: ./.github/actions/breeze
-      - name: "Restore ci-cache mount image ${{ inputs.platform }}:${{ 
env.PYTHON_MAJOR_MINOR_VERSION }}"
+      # A ref is not a commit: a branch documented twice moves between the two 
publishes, and
+      # an image built before it moved is a cache, not an answer. The commit 
each stashed image
+      # is saved with tells the two apart, and it is stashed separately so 
that deciding costs
+      # a few bytes rather than the multi-gigabyte image the decision may make 
unnecessary.
+      - name: >
+          Restore the commit the CI image stashed for ref ${{ 
inputs.image-stash-ref }}
+          was built from
         uses: 
apache/infrastructure-actions/stash/restore@49df447b39b18354895520e0a63731b7cad7cbec
         with:
-          key: "ci-cache-mount-save-v3-${{ inputs.platform }}-${{ 
env.PYTHON_MAJOR_MINOR_VERSION }}"
+          key: "ci-image-commit-v3-${{ inputs.platform }}-${{ 
env.PYTHON_MAJOR_MINOR_VERSION }}\
+            -${{ inputs.image-stash-ref }}"
+          path: "/mnt/"
+          only-current-branch: 'true'
+        id: restore-image-commit
+        if: >
+          inputs.seed-cache-from-stashed-image == 'true' && 
inputs.image-stash-ref != '' &&
+          inputs.push-image != 'true'
+      - name: >
+          Check whether the CI image stashed for ref ${{ 
inputs.image-stash-ref }} was built
+          from this commit
+        id: stashed-image
+        env:
+          COMMIT_FILE: "/mnt/ci-image-commit-${{ 
env.PYTHON_MAJOR_MINOR_VERSION }}.txt"
+        run: |
+          head_sha="$(git rev-parse HEAD)"
+          stashed_sha="$(cat "${COMMIT_FILE}" 2>/dev/null || true)"
+          if [[ "${stashed_sha}" == "${head_sha}" ]]; then
+            echo "The stashed image was built from ${head_sha} - reusing it, 
without a build."
+            echo "reusable=true" >> "${GITHUB_OUTPUT}"
+          else
+            echo "The stashed image was built from '${stashed_sha:-unknown}', 
not ${head_sha}."
+            echo "It can only seed the build cache."
+          fi
+        shell: bash
+        if: steps.restore-image-commit.outputs.stash-hit == 'true'
+      # Restored ahead of the caches that feed the build, so that a build made 
unnecessary
+      # above skips them too.
+      - name: >
+          Restore CI docker image built for ref ${{ inputs.image-stash-ref }}
+          ${{ inputs.platform }}:${{ env.PYTHON_MAJOR_MINOR_VERSION }}
+        uses: 
apache/infrastructure-actions/stash/restore@49df447b39b18354895520e0a63731b7cad7cbec
+        with:
+          key: "ci-image-save-v3-${{ inputs.platform }}-${{ 
env.PYTHON_MAJOR_MINOR_VERSION }}\
+            -${{ inputs.image-stash-ref }}"
+          path: "/mnt/"
+          only-current-branch: 'true'
+        id: restore-cache-image-for-ref
+        if: >
+          inputs.seed-cache-from-stashed-image == 'true' && 
inputs.image-stash-ref != '' &&
+          steps.stashed-image.outputs.reusable != 'true'
+      # Scoped to the ref for the same reason the image is: the mount cache 
holds the dependency
+      # set the sources resolve to, and a ref's and the branch tip's are 
exactly what differ.
+      - name: >
+          Restore ci-cache mount image ${{ inputs.platform }}:${{ 
env.PYTHON_MAJOR_MINOR_VERSION }}
+          ${{ inputs.image-stash-ref != '' && format('for ref {0}', 
inputs.image-stash-ref) || '' }}
+        uses: 
apache/infrastructure-actions/stash/restore@49df447b39b18354895520e0a63731b7cad7cbec
+        with:
+          key: "ci-cache-mount-save-v3-${{ inputs.platform }}-${{ 
env.PYTHON_MAJOR_MINOR_VERSION }}\
+            ${{ inputs.image-stash-ref != '' && format('-{0}', 
inputs.image-stash-ref) || '' }}"
           path: "/tmp/"
         id: restore-cache-mount
+        if: steps.stashed-image.outputs.reusable != 'true'
       - name: "Verify ci-cache file exists"
         if: steps.restore-cache-mount.outputs.stash-hit == 'true'
         env:
@@ -185,20 +241,6 @@ jobs:
           breeze ci-image import-mount-cache
           --cache-file 
/tmp/ci-cache-mount-save-v3-${PYTHON_MAJOR_MINOR_VERSION}.tar.gz
         if: steps.restore-cache-mount.outputs.stash-hit == 'true'
-      # BuildKit reads an image's recorded layer cache only when it can pull 
that image's layers
-      # from a registry - one merely loaded into the docker engine contributes 
nothing. So the
-      # image an earlier run stashed is served back to the build from a 
registry on localhost.
-      - name: >
-          Restore CI docker image for this ref
-          ${{ inputs.platform }}:${{ env.PYTHON_MAJOR_MINOR_VERSION }}
-        uses: 
apache/infrastructure-actions/stash/restore@49df447b39b18354895520e0a63731b7cad7cbec
-        with:
-          key: "ci-image-save-v3-${{ inputs.platform }}-${{ 
env.PYTHON_MAJOR_MINOR_VERSION }}\
-            ${{ inputs.image-stash-suffix }}"
-          path: "/mnt/"
-          only-current-branch: 'true'
-        id: restore-cache-image-for-ref
-        if: inputs.seed-cache-from-stashed-image == 'true' && 
inputs.image-stash-suffix != ''
       - name: "Restore CI docker image ${{ inputs.platform }}:${{ 
env.PYTHON_MAJOR_MINOR_VERSION }}"
         uses: 
apache/infrastructure-actions/stash/restore@49df447b39b18354895520e0a63731b7cad7cbec
         with:
@@ -208,7 +250,11 @@ jobs:
         id: restore-cache-image
         if: >
           inputs.seed-cache-from-stashed-image == 'true' &&
+          steps.stashed-image.outputs.reusable != 'true' &&
           steps.restore-cache-image-for-ref.outputs.stash-hit != 'true'
+      # BuildKit reads an image's recorded layer cache only when it can pull 
that image's layers
+      # from a registry - one merely loaded into the docker engine contributes 
nothing. So the
+      # image an earlier run stashed is served back to the build from a 
registry on localhost.
       - name: "Serve stashed image as cache ${{ inputs.platform }}:${{ 
env.PYTHON_MAJOR_MINOR_VERSION }}"
         env:
           PLATFORM: ${{ inputs.platform }}
@@ -221,13 +267,15 @@ jobs:
           echo "CACHE_FROM_IMAGE=${CACHE_IMAGE}" >> "${GITHUB_ENV}"
         shell: bash
         if: >
+          steps.stashed-image.outputs.reusable != 'true' && (
           steps.restore-cache-image-for-ref.outputs.stash-hit == 'true' ||
-          steps.restore-cache-image.outputs.stash-hit == 'true'
+          steps.restore-cache-image.outputs.stash-hit == 'true')
       - 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
+        if: steps.stashed-image.outputs.reusable != 'true'
       - name: >
           Build ${{ inputs.push-image == 'true' && ' & push ' || '' }}
           ${{ inputs.platform }}:${{ env.PYTHON_MAJOR_MINOR_VERSION }} image
@@ -260,6 +308,7 @@ jobs:
           PUSH: ${{ inputs.push-image }}
           VERBOSE: "true"
           PLATFORM: ${{ inputs.platform }}
+        if: steps.stashed-image.outputs.reusable != 'true'
       - name: "Stop serving the cache image"
         # The registry holds a second copy of a multi-gigabyte image and the 
export below needs
         # the room.
@@ -271,8 +320,12 @@ jobs:
       - name: "Export CI docker image ${{ env.PYTHON_MAJOR_MINOR_VERSION }}"
         env:
           PLATFORM: ${{ inputs.platform }}
-        run: breeze ci-image save --platform "${PLATFORM}" --image-file-dir 
"/mnt"
-        if: inputs.upload-image-artifact == 'true'
+          COMMIT_FILE: "/mnt/ci-image-commit-${{ 
env.PYTHON_MAJOR_MINOR_VERSION }}.txt"
+        run: |
+          breeze ci-image save --platform "${PLATFORM}" --image-file-dir "/mnt"
+          git rev-parse HEAD > "${COMMIT_FILE}"
+        shell: bash
+        if: inputs.upload-image-artifact == 'true' && 
steps.stashed-image.outputs.reusable != 'true'
       - name: "Stash CI docker image ${{ env.PYTHON_MAJOR_MINOR_VERSION }}"
         uses: 
apache/infrastructure-actions/stash/save@49df447b39b18354895520e0a63731b7cad7cbec
         with:
@@ -280,34 +333,59 @@ jobs:
           path: "/mnt/ci-image-save-*-${{ env.PYTHON_MAJOR_MINOR_VERSION 
}}.tar"
           if-no-files-found: 'error'
           retention-days: '2'
-        if: inputs.upload-image-artifact == 'true'
-      # Everything in this run reads the stash above; this copy exists only so 
the next run
-      # built from the same ref can seed its cache from sources it actually 
matches. It outlives
-      # that one by days - the same ref is rebuilt three days apart at the 
very least, so the
-      # two-day retention the shared stash gets would expire it before it is 
ever read.
-      - name: "Stash CI docker image for this ref ${{ 
env.PYTHON_MAJOR_MINOR_VERSION }}"
+        if: inputs.upload-image-artifact == 'true' && inputs.image-stash-ref 
== ''
+      # The per-branch stash above is written by every build on this branch, 
so an image built
+      # from a ref other than the branch tip cannot live there: whoever 
restores it next would
+      # get sources they never asked for. The ref keeps its own stash instead, 
which is also
+      # what a later publish of that same ref reads - hence a retention that 
outlives the shared
+      # one, the same ref being rebuilt three days apart at the very least.
+      - name: "Stash CI docker image built for ref ${{ inputs.image-stash-ref 
}}"
         uses: 
apache/infrastructure-actions/stash/save@49df447b39b18354895520e0a63731b7cad7cbec
         with:
           key: "ci-image-save-v3-${{ inputs.platform }}-${{ 
env.PYTHON_MAJOR_MINOR_VERSION }}\
-            ${{ inputs.image-stash-suffix }}"
+            -${{ inputs.image-stash-ref }}"
           path: "/mnt/ci-image-save-*-${{ env.PYTHON_MAJOR_MINOR_VERSION 
}}.tar"
           if-no-files-found: 'error'
           retention-days: '6'
-        if: inputs.upload-image-artifact == 'true' && 
inputs.image-stash-suffix != ''
+        if: >
+          inputs.upload-image-artifact == 'true' && inputs.image-stash-ref != 
'' &&
+          steps.stashed-image.outputs.reusable != 'true'
+      # Saved last and with the image's retention, so that finding this commit 
is enough to
+      # know the image it describes is there to be restored.
+      - name: "Stash the commit the CI image for ref ${{ 
inputs.image-stash-ref }} was built from"
+        uses: 
apache/infrastructure-actions/stash/save@49df447b39b18354895520e0a63731b7cad7cbec
+        with:
+          key: "ci-image-commit-v3-${{ inputs.platform }}-${{ 
env.PYTHON_MAJOR_MINOR_VERSION }}\
+            -${{ inputs.image-stash-ref }}"
+          path: "/mnt/ci-image-commit-${{ env.PYTHON_MAJOR_MINOR_VERSION 
}}.txt"
+          if-no-files-found: 'error'
+          retention-days: '6'
+        if: >
+          inputs.upload-image-artifact == 'true' && inputs.image-stash-ref != 
'' &&
+          steps.stashed-image.outputs.reusable != 'true'
       - name: "Export mount cache ${{ inputs.platform }}:${{ 
env.PYTHON_MAJOR_MINOR_VERSION }}"
         env:
           PYTHON_MAJOR_MINOR_VERSION: ${{ env.PYTHON_MAJOR_MINOR_VERSION }}
         run: >
           breeze ci-image export-mount-cache
           --cache-file 
/tmp/ci-cache-mount-save-v3-${PYTHON_MAJOR_MINOR_VERSION}.tar.gz
-        if: inputs.upload-mount-cache-artifact == 'true'
-      - name: "Stash cache mount ${{ inputs.platform }}:${{ 
env.PYTHON_MAJOR_MINOR_VERSION }}"
+        if: >
+          inputs.upload-mount-cache-artifact == 'true' &&
+          steps.stashed-image.outputs.reusable != 'true'
+      - name: >
+          Stash cache mount ${{ inputs.platform }}:${{ 
env.PYTHON_MAJOR_MINOR_VERSION }}
+          ${{ inputs.image-stash-ref != '' && format('for ref {0}', 
inputs.image-stash-ref) || '' }}
         uses: 
apache/infrastructure-actions/stash/save@49df447b39b18354895520e0a63731b7cad7cbec
         with:
-          key: "ci-cache-mount-save-v3-${{ inputs.platform }}-${{ 
env.PYTHON_MAJOR_MINOR_VERSION }}"
+          key: "ci-cache-mount-save-v3-${{ inputs.platform }}-${{ 
env.PYTHON_MAJOR_MINOR_VERSION }}\
+            ${{ inputs.image-stash-ref != '' && format('-{0}', 
inputs.image-stash-ref) || '' }}"
           path: "/tmp/ci-cache-mount-save-v3-${{ 
env.PYTHON_MAJOR_MINOR_VERSION }}.tar.gz"
           if-no-files-found: 'error'
-          retention-days: 2
-        if: inputs.upload-mount-cache-artifact == 'true'
+          # A ref's cache is read by the next publish of that same ref, days 
rather than hours
+          # later, so it gets the retention the ref's image gets rather than 
the branch's.
+          retention-days: ${{ inputs.image-stash-ref != '' && '6' || '2' }}
+        if: >
+          inputs.upload-mount-cache-artifact == 'true' &&
+          steps.stashed-image.outputs.reusable != 'true'
       - name: "Check disk space after build"
         run: df -H
diff --git a/.github/workflows/publish-docs-to-s3.yml 
b/.github/workflows/publish-docs-to-s3.yml
index 5f375004d1f..1b8c76b284c 100644
--- a/.github/workflows/publish-docs-to-s3.yml
+++ b/.github/workflows/publish-docs-to-s3.yml
@@ -243,17 +243,17 @@ jobs:
       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;
+      # Leaves the BuildKit mount cache behind for the next publish of this 
same ref;
       # ci-image-build.yml always restores it, so consecutive publishes stop 
re-downloading
       # the whole dependency set.
       upload-mount-cache-artifact: "true"
       # Docs are published from refs cut days or weeks before main, and main's 
registry cache
       # stops matching at the first Dockerfile.ci change made since the cut - 
#70285 alone
       # costs a from-scratch Python build. The image the previous publish 
stashed was built
-      # from these very sources, so it is the cache main's cannot be. 
Publishes of different
-      # refs share one branch-scoped stash, hence the per-ref copy to prefer 
over it.
+      # from these very sources, so it is the cache main's cannot be. 
Publishing the same ref
+      # twice - an RC and then the final docs - skips the build outright.
       seed-cache-from-stashed-image: "true"
-      image-stash-suffix: "-${{ inputs.ref }}"
+      image-stash-ref: ${{ inputs.ref }}
       python-versions: ${{ format('["{0}"]', 
needs.build-info.outputs.default-python-version) }}
       branch: "main"
       constraints-branch: "constraints-main"
@@ -334,11 +334,16 @@ jobs:
         with:
           python-version: "${{ needs.build-info.outputs.default-python-version 
}}"
       # 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 }}"
+      # stashes it under the ref's own key - the shared per-branch stash holds 
main's image,
+      # not this ref's. The registry build restores the very same stash, so a 
run builds it
+      # once, and a run that had nothing to build restores what the previous 
publish left.
+      - name: >
+          Restore CI docker image built for ref ${{ inputs.ref }}
+          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 }}
+          key: "ci-image-save-v3-linux/amd64-\
+            ${{ needs.build-info.outputs.default-python-version }}-${{ 
inputs.ref }}"
           path: "/mnt/"
           only-current-branch: 'true'
           fail-on-download: 'true'
@@ -650,8 +655,9 @@ jobs:
       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.
+      # `build-ci-image` already built and stashed the image this run - under 
the ref's key.
       ci-image-already-built: true
+      image-stash-ref: ${{ inputs.ref }}
     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 e1932365173..6db2b891cd2 100644
--- a/.github/workflows/registry-build.yml
+++ b/.github/workflows/registry-build.yml
@@ -60,6 +60,14 @@ on:  # yamllint disable-line rule:truthy
         required: false
         type: boolean
         default: false
+      image-stash-ref:
+        description: >
+          Ref whose CI image stash to use (empty = the per-branch one). Set it 
to whatever the
+          caller passed to ci-image-build.yml, which stashes an image built 
for a ref under a
+          key of its own rather than in the branch-wide stash.
+        required: false
+        type: string
+        default: ""
     secrets:
       DOCS_AWS_ACCESS_KEY_ID:
         required: true
@@ -169,6 +177,7 @@ jobs:
           platform: "linux/amd64"
           use-uv: "true"
           make-mnt-writeable-and-cleanup: "true"
+          image-stash-ref: "${{ inputs.image-stash-ref }}"
 
       - name: "Install AWS CLI v2"
         run: |

Reply via email to