From 5fbb40df2c8bd8f9ced644f41bacc549c732f5da Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Wed, 6 May 2026 15:06:33 -0700 Subject: [PATCH 1/3] Warm CLI E2E image cache on main Add a main-branch cache warmer for the reusable CLI E2E image build so pull requests can restore default-branch BuildKit layers without relying on sibling PR caches. Make artifact upload optional for cache-only runs and document the cross-PR cache behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/build-cli-e2e-image.yml | 22 ++++++++---- .../workflows/warm-cli-e2e-image-cache.yml | 35 +++++++++++++++++++ docs/ci/cli-e2e-images.md | 15 ++++++++ 3 files changed, 65 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/warm-cli-e2e-image-cache.yml diff --git a/.github/workflows/build-cli-e2e-image.yml b/.github/workflows/build-cli-e2e-image.yml index 6d1ec2a5a6d..aa7a21e2f1c 100644 --- a/.github/workflows/build-cli-e2e-image.yml +++ b/.github/workflows/build-cli-e2e-image.yml @@ -10,6 +10,10 @@ on: required: false type: boolean default: true + uploadArtifacts: + required: false + type: boolean + default: true env: CLI_E2E_DOTNET_IMAGE_ARTIFACT: cli-e2e-dotnet-image @@ -22,6 +26,7 @@ env: CLI_E2E_POLYGLOT_BASE_IMAGE_TAG: aspire-e2e-polyglot-base:latest CLI_E2E_POLYGLOT_BASE_IMAGE_CACHE_SCOPE: cli-e2e-polyglot-base CLI_E2E_INCLUDE_POLYGLOT_IMAGES: ${{ inputs.includePolyglotImages }} + CLI_E2E_UPLOAD_ARTIFACTS: ${{ inputs.uploadArtifacts }} jobs: build: @@ -123,14 +128,17 @@ jobs: fi fi - mkdir -p artifacts/cli-e2e-image - docker save "$CLI_E2E_DOTNET_IMAGE_TAG" | gzip > artifacts/cli-e2e-image/aspire-cli-e2e-dotnet.tar.gz - if [[ "$CLI_E2E_INCLUDE_POLYGLOT_IMAGES" == "true" ]]; then - docker save "$CLI_E2E_POLYGLOT_IMAGE_TAG" | gzip > artifacts/cli-e2e-image/aspire-cli-e2e-polyglot.tar.gz - docker save "$CLI_E2E_POLYGLOT_JAVA_IMAGE_TAG" | gzip > artifacts/cli-e2e-image/aspire-cli-e2e-polyglot-java.tar.gz + if [[ "$CLI_E2E_UPLOAD_ARTIFACTS" == "true" ]]; then + mkdir -p artifacts/cli-e2e-image + docker save "$CLI_E2E_DOTNET_IMAGE_TAG" | gzip > artifacts/cli-e2e-image/aspire-cli-e2e-dotnet.tar.gz + if [[ "$CLI_E2E_INCLUDE_POLYGLOT_IMAGES" == "true" ]]; then + docker save "$CLI_E2E_POLYGLOT_IMAGE_TAG" | gzip > artifacts/cli-e2e-image/aspire-cli-e2e-polyglot.tar.gz + docker save "$CLI_E2E_POLYGLOT_JAVA_IMAGE_TAG" | gzip > artifacts/cli-e2e-image/aspire-cli-e2e-polyglot-java.tar.gz + fi fi - name: Upload CLI E2E .NET Docker image + if: ${{ inputs.uploadArtifacts }} uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: ${{ env.CLI_E2E_DOTNET_IMAGE_ARTIFACT }} @@ -138,7 +146,7 @@ jobs: retention-days: 1 - name: Upload CLI E2E polyglot Docker image - if: ${{ inputs.includePolyglotImages }} + if: ${{ inputs.uploadArtifacts && inputs.includePolyglotImages }} uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: ${{ env.CLI_E2E_POLYGLOT_IMAGE_ARTIFACT }} @@ -146,7 +154,7 @@ jobs: retention-days: 1 - name: Upload CLI E2E Java Docker image - if: ${{ inputs.includePolyglotImages }} + if: ${{ inputs.uploadArtifacts && inputs.includePolyglotImages }} uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: ${{ env.CLI_E2E_POLYGLOT_JAVA_IMAGE_ARTIFACT }} diff --git a/.github/workflows/warm-cli-e2e-image-cache.yml b/.github/workflows/warm-cli-e2e-image-cache.yml new file mode 100644 index 00000000000..f1a4dc7d0d3 --- /dev/null +++ b/.github/workflows/warm-cli-e2e-image-cache.yml @@ -0,0 +1,35 @@ +# Warms the BuildKit cache used by pull request CLI E2E image builds. +name: Warm CLI E2E Image Cache + +on: + workflow_dispatch: + + schedule: + - cron: '30 5 * * *' + + push: + branches: + - main + paths: + - '.github/workflows/build-cli-e2e-image.yml' + - '.github/workflows/warm-cli-e2e-image-cache.yml' + - 'eng/scripts/get-aspire-cli.sh' + - 'eng/scripts/get-aspire-cli-pr.sh' + - 'tests/Shared/Docker/Dockerfile.e2e' + - 'tests/Shared/Docker/Dockerfile.e2e-polyglot-base' + - 'tests/Shared/Docker/Dockerfile.e2e-polyglot-java' + - 'tests/Shared/Docker/NuGet.DotnetTool.config' + - 'tests/Shared/Docker/configure-ubuntu-apt-mirror.sh' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +jobs: + warm_cli_e2e_image_cache: + name: Warm CLI E2E image cache + if: ${{ github.repository_owner == 'microsoft' }} + uses: ./.github/workflows/build-cli-e2e-image.yml + with: + includePolyglotImages: true + uploadArtifacts: false diff --git a/docs/ci/cli-e2e-images.md b/docs/ci/cli-e2e-images.md index 2c2cc8fae61..11e8b78136b 100644 --- a/docs/ci/cli-e2e-images.md +++ b/docs/ci/cli-e2e-images.md @@ -26,6 +26,21 @@ The image build workflow has an `includePolyglotImages` input. It defaults to `t Consumer workflows download image artifacts into `${{ github.workspace }}/cli-e2e-image` and call `eng/scripts/load-cli-e2e-images.sh` to load the images and export the matching environment variables. Regular split CLI E2E jobs always require DotNet and Polyglot images. Java image download and loading is conditional on Java test jobs to avoid transferring the larger Java tarball to every split job. +The reusable image build workflow also has an `uploadArtifacts` input. It defaults to `true` for test workflows because each isolated test job needs the image tarballs. Cache-warming workflows set it to `false` so they only build the images and export BuildKit cache layers. + +## Cross-PR BuildKit cache + +The image build workflow exports BuildKit layers to the GitHub Actions cache with stable scopes: + +| Variant | Cache scope | +| --- | --- | +| DotNet | `cli-e2e-dotnet` | +| Polyglot | `cli-e2e-polyglot-base` | + +GitHub Actions does not let one pull request restore cache entries written by a sibling pull request. Pull request runs can restore cache entries from the current PR ref and from the base/default branch. The `.github/workflows/warm-cli-e2e-image-cache.yml` workflow runs on `main` by schedule, manual dispatch, and relevant `main` pushes so new PRs can restore layers seeded from `main`. + +The Java image is built from the local polyglot base image and does not use a separate shared BuildKit cache scope. + ## Adding another variant When adding a new shared CLI E2E Dockerfile variant: From b69b2065f31ef93874ea030942d947c391b315c7 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Wed, 6 May 2026 15:14:54 -0700 Subject: [PATCH 2/3] Document CLI E2E cache warmer schedule Add a human-readable UTC schedule comment beside the cache warmer cron expression. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/warm-cli-e2e-image-cache.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/warm-cli-e2e-image-cache.yml b/.github/workflows/warm-cli-e2e-image-cache.yml index f1a4dc7d0d3..2a9e37f167b 100644 --- a/.github/workflows/warm-cli-e2e-image-cache.yml +++ b/.github/workflows/warm-cli-e2e-image-cache.yml @@ -5,7 +5,7 @@ on: workflow_dispatch: schedule: - - cron: '30 5 * * *' + - cron: '30 5 * * *' # Daily at 05:30 UTC push: branches: From df73179efbf6fca720adc8c3a05ebe2600623888 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Thu, 7 May 2026 07:07:53 -0700 Subject: [PATCH 3/3] Address CLI E2E cache warmer review Apply review feedback by adding .dockerignore to warm-cache triggers, renaming the image artifact input, splitting tarball saves into a separate skipped step for cache-only runs, and documenting the cache-warmer call-site rationale. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/build-cli-e2e-image.yml | 27 ++++++++++--------- .../workflows/warm-cli-e2e-image-cache.yml | 7 ++++- docs/ci/cli-e2e-images.md | 2 +- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build-cli-e2e-image.yml b/.github/workflows/build-cli-e2e-image.yml index aa7a21e2f1c..bfd5c5050ae 100644 --- a/.github/workflows/build-cli-e2e-image.yml +++ b/.github/workflows/build-cli-e2e-image.yml @@ -10,7 +10,7 @@ on: required: false type: boolean default: true - uploadArtifacts: + uploadImageArtifacts: required: false type: boolean default: true @@ -26,7 +26,6 @@ env: CLI_E2E_POLYGLOT_BASE_IMAGE_TAG: aspire-e2e-polyglot-base:latest CLI_E2E_POLYGLOT_BASE_IMAGE_CACHE_SCOPE: cli-e2e-polyglot-base CLI_E2E_INCLUDE_POLYGLOT_IMAGES: ${{ inputs.includePolyglotImages }} - CLI_E2E_UPLOAD_ARTIFACTS: ${{ inputs.uploadArtifacts }} jobs: build: @@ -128,17 +127,21 @@ jobs: fi fi - if [[ "$CLI_E2E_UPLOAD_ARTIFACTS" == "true" ]]; then - mkdir -p artifacts/cli-e2e-image - docker save "$CLI_E2E_DOTNET_IMAGE_TAG" | gzip > artifacts/cli-e2e-image/aspire-cli-e2e-dotnet.tar.gz - if [[ "$CLI_E2E_INCLUDE_POLYGLOT_IMAGES" == "true" ]]; then - docker save "$CLI_E2E_POLYGLOT_IMAGE_TAG" | gzip > artifacts/cli-e2e-image/aspire-cli-e2e-polyglot.tar.gz - docker save "$CLI_E2E_POLYGLOT_JAVA_IMAGE_TAG" | gzip > artifacts/cli-e2e-image/aspire-cli-e2e-polyglot-java.tar.gz - fi + - name: Save CLI E2E Docker image tarballs + if: ${{ inputs.uploadImageArtifacts }} + shell: bash + run: | + set -euo pipefail + + mkdir -p artifacts/cli-e2e-image + docker save "$CLI_E2E_DOTNET_IMAGE_TAG" | gzip > artifacts/cli-e2e-image/aspire-cli-e2e-dotnet.tar.gz + if [[ "$CLI_E2E_INCLUDE_POLYGLOT_IMAGES" == "true" ]]; then + docker save "$CLI_E2E_POLYGLOT_IMAGE_TAG" | gzip > artifacts/cli-e2e-image/aspire-cli-e2e-polyglot.tar.gz + docker save "$CLI_E2E_POLYGLOT_JAVA_IMAGE_TAG" | gzip > artifacts/cli-e2e-image/aspire-cli-e2e-polyglot-java.tar.gz fi - name: Upload CLI E2E .NET Docker image - if: ${{ inputs.uploadArtifacts }} + if: ${{ inputs.uploadImageArtifacts }} uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: ${{ env.CLI_E2E_DOTNET_IMAGE_ARTIFACT }} @@ -146,7 +149,7 @@ jobs: retention-days: 1 - name: Upload CLI E2E polyglot Docker image - if: ${{ inputs.uploadArtifacts && inputs.includePolyglotImages }} + if: ${{ inputs.uploadImageArtifacts && inputs.includePolyglotImages }} uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: ${{ env.CLI_E2E_POLYGLOT_IMAGE_ARTIFACT }} @@ -154,7 +157,7 @@ jobs: retention-days: 1 - name: Upload CLI E2E Java Docker image - if: ${{ inputs.uploadArtifacts && inputs.includePolyglotImages }} + if: ${{ inputs.uploadImageArtifacts && inputs.includePolyglotImages }} uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: ${{ env.CLI_E2E_POLYGLOT_JAVA_IMAGE_ARTIFACT }} diff --git a/.github/workflows/warm-cli-e2e-image-cache.yml b/.github/workflows/warm-cli-e2e-image-cache.yml index 2a9e37f167b..f332d07293f 100644 --- a/.github/workflows/warm-cli-e2e-image-cache.yml +++ b/.github/workflows/warm-cli-e2e-image-cache.yml @@ -13,6 +13,7 @@ on: paths: - '.github/workflows/build-cli-e2e-image.yml' - '.github/workflows/warm-cli-e2e-image-cache.yml' + - '.dockerignore' - 'eng/scripts/get-aspire-cli.sh' - 'eng/scripts/get-aspire-cli-pr.sh' - 'tests/Shared/Docker/Dockerfile.e2e' @@ -32,4 +33,8 @@ jobs: uses: ./.github/workflows/build-cli-e2e-image.yml with: includePolyglotImages: true - uploadArtifacts: false + # Cache-warmer mode: this run only needs to seed the GHA BuildKit cache + # scopes that sibling PR builds restore from. The cache export happens + # during `docker buildx build` itself, so no tarball is needed. No test + # job consumes artifacts from this workflow, so skip the upload steps. + uploadImageArtifacts: false diff --git a/docs/ci/cli-e2e-images.md b/docs/ci/cli-e2e-images.md index 11e8b78136b..26d7f6d93a1 100644 --- a/docs/ci/cli-e2e-images.md +++ b/docs/ci/cli-e2e-images.md @@ -26,7 +26,7 @@ The image build workflow has an `includePolyglotImages` input. It defaults to `t Consumer workflows download image artifacts into `${{ github.workspace }}/cli-e2e-image` and call `eng/scripts/load-cli-e2e-images.sh` to load the images and export the matching environment variables. Regular split CLI E2E jobs always require DotNet and Polyglot images. Java image download and loading is conditional on Java test jobs to avoid transferring the larger Java tarball to every split job. -The reusable image build workflow also has an `uploadArtifacts` input. It defaults to `true` for test workflows because each isolated test job needs the image tarballs. Cache-warming workflows set it to `false` so they only build the images and export BuildKit cache layers. +The reusable image build workflow also has an `uploadImageArtifacts` input. It defaults to `true` for test workflows because each isolated test job needs the image tarballs. Cache-warming workflows set it to `false` so they only build the images and export BuildKit cache layers. ## Cross-PR BuildKit cache