This is an automated email from the ASF dual-hosted git repository. potiuk pushed a commit to branch skip-docs-image-build-when-ref-unchanged in repository https://gitbox.apache.org/repos/asf/airflow.git
commit 84b86d3583574680878fae27e566b597a285a7ab Author: Jarek Potiuk <[email protected]> AuthorDate: Wed Jul 29 12:25:45 2026 +0200 Skip the docs CI image build when that ref was already built 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: |
