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 5d886dd7414 Build only required prek hook environments in CI jobs on a
cache miss (#76111)
5d886dd7414 is described below
commit 5d886dd7414db79d3388a58e1c1e072720ca6ec2
Author: PoAn Yang <[email protected]>
AuthorDate: Mon Nov 2 04:44:07 2026 +0800
Build only required prek hook environments in CI jobs on a cache miss
(#76111)
Signed-off-by: PoAn Yang <[email protected]>
---
.github/actions/install-prek/action.yml | 26 ++++++-
.github/workflows/ci-amd.yml | 2 +
.github/workflows/ci-arm.yml | 2 +
.github/workflows/generate-constraints.yml | 2 +
.github/workflows/prod-image-build.yml | 2 +
.github/workflows/test-providers.yml | 4 ++
scripts/tests/ci/test_prek_cache_optimizations.py | 84 ++++++++++++++++++++++-
7 files changed, 119 insertions(+), 3 deletions(-)
diff --git a/.github/actions/install-prek/action.yml
b/.github/actions/install-prek/action.yml
index 6ce8776d179..a9e51a95377 100644
--- a/.github/actions/install-prek/action.yml
+++ b/.github/actions/install-prek/action.yml
@@ -25,6 +25,14 @@ inputs:
platform:
description: 'Platform for the build - linux/amd64 or linux/arm64'
required: true
+ hooks:
+ description: >-
+ Set this input to the space-separated ids of the hooks that the job
runs. When this input is set,
+ the action still restores the shared cache of hook environments, but it
prepares only these hooks and
+ never saves the cache. A hook left out of the list still runs, but prek
creates its environment when
+ the hook first runs.
+ required: false
+ default: ''
runs:
using: "composite"
steps:
@@ -116,6 +124,8 @@ runs:
- name: Validate and install prek hooks
id: install-hooks
shell: bash
+ env:
+ HOOKS: ${{ inputs.hooks }}
# A successfully extracted archive can still contain a partial or
unusable environment.
# This is idempotent on a healthy hit; prek validates/reuses it and
repairs misses normally.
# Keep the existing bounded retries for transient toolchain download
failures.
@@ -131,10 +141,16 @@ runs:
change_detection_uncertain=true
fi
+ # A YAML block scalar puts each hook id on its own line, and `read`
stops at the first newline.
+ read -ra hook_selection <<< "${HOOKS//$'\n'/ }"
+ if [[ "${#hook_selection[@]}" -eq 0 ]]; then
+ hook_selection=(--skip run-skill-eval --skip run-skill-eval-codex
--skip view-skill-eval)
+ fi
+
started_at=${SECONDS}
max_attempts=4
for attempt in $(seq 1 "${max_attempts}"); do
- if prek install-hooks --skip run-skill-eval --skip
run-skill-eval-codex --skip view-skill-eval; then
+ if prek install-hooks "${hook_selection[@]}"; then
installation_succeeded=true
break
fi
@@ -175,6 +191,7 @@ runs:
env:
EVENT_NAME: ${{ github.event_name }}
SAVE_CACHE: ${{ inputs.save-cache }}
+ HOOKS: ${{ inputs.hooks }}
STASH_HIT: ${{ steps.restore-prek-cache.outputs.stash-hit }}
TAR_RESTORED: ${{ steps.restore-prek-tar.outputs.tar-restored }}
CACHE_CHANGED: ${{ steps.install-hooks.outputs.cache-changed }}
@@ -182,7 +199,12 @@ runs:
run: |
SAVE=false
REASON=unchanged
- if [[ "${SAVE_CACHE}" != "true" ]]; then
+ # If this job saved its archive after a cache miss, the archive would
hold only the selected
+ # environments under the cache key of the full archive. A later job
that looks up that key could
+ # restore an archive that lacks most environments.
+ if [[ -n "${HOOKS//[[:space:]]/}" ]]; then
+ REASON=selected-hooks
+ elif [[ "${SAVE_CACHE}" != "true" ]]; then
REASON=reader
elif [[ "${STASH_HIT}" != "true" ]]; then
SAVE=true
diff --git a/.github/workflows/ci-amd.yml b/.github/workflows/ci-amd.yml
index 6acd387f3f8..71619921033 100644
--- a/.github/workflows/ci-amd.yml
+++ b/.github/workflows/ci-amd.yml
@@ -643,6 +643,7 @@ jobs:
with:
platform: ${{ needs.build-info.outputs.platform }}
save-cache: false
+ hooks: mypy-providers
- name: "MyPy checks for providers"
run: prek --color always --verbose --stage manual mypy-providers
--all-files
env:
@@ -684,6 +685,7 @@ jobs:
with:
platform: ${{ needs.build-info.outputs.platform }}
save-cache: false
+ hooks: migration-round-trip
- name: "Migration round-trip check"
run: prek --color always --verbose --stage manual migration-round-trip
--all-files
env:
diff --git a/.github/workflows/ci-arm.yml b/.github/workflows/ci-arm.yml
index 09b0967b490..e6072ea6fcf 100644
--- a/.github/workflows/ci-arm.yml
+++ b/.github/workflows/ci-arm.yml
@@ -519,6 +519,7 @@ jobs:
with:
platform: ${{ needs.build-info.outputs.platform }}
save-cache: false
+ hooks: mypy-providers
- name: "MyPy checks for providers"
run: prek --color always --verbose --stage manual mypy-providers
--all-files
env:
@@ -560,6 +561,7 @@ jobs:
with:
platform: ${{ needs.build-info.outputs.platform }}
save-cache: false
+ hooks: migration-round-trip
- name: "Migration round-trip check"
run: prek --color always --verbose --stage manual migration-round-trip
--all-files
env:
diff --git a/.github/workflows/generate-constraints.yml
b/.github/workflows/generate-constraints.yml
index fc801924ccd..b388af7196f 100644
--- a/.github/workflows/generate-constraints.yml
+++ b/.github/workflows/generate-constraints.yml
@@ -131,6 +131,8 @@ jobs:
with:
platform: ${{ inputs.platform }}
save-cache: false
+ # The hatch_build.py files of the fab, edge3 and common.ai providers
run these hooks.
+ hooks: compile-fab-assets compile-edge-assets
compile-common-ai-provider-assets
# Resolves the providers from the sources in this checkout. Skipped for
a release, which
# ships only the PyPI constraints and must not have a source tree answer
for a provider.
- name: "Source constraints"
diff --git a/.github/workflows/prod-image-build.yml
b/.github/workflows/prod-image-build.yml
index b2ce6b25f7d..5cac2e2817d 100644
--- a/.github/workflows/prod-image-build.yml
+++ b/.github/workflows/prod-image-build.yml
@@ -151,6 +151,8 @@ jobs:
with:
platform: ${{ inputs.platform }}
save-cache: false
+ # The hatch_build.py files of the fab, edge3 and common.ai providers
run these hooks.
+ hooks: compile-fab-assets compile-edge-assets
compile-common-ai-provider-assets
if: inputs.upload-package-artifact == 'true'
- name: "Prepare providers packages - all providers built from sources"
shell: bash
diff --git a/.github/workflows/test-providers.yml
b/.github/workflows/test-providers.yml
index ccf9dc12304..ce0f7f62df1 100644
--- a/.github/workflows/test-providers.yml
+++ b/.github/workflows/test-providers.yml
@@ -105,6 +105,8 @@ jobs:
with:
platform: ${{ inputs.platform }}
save-cache: false
+ # The hatch_build.py files of the fab, edge3 and common.ai providers
run these hooks.
+ hooks: compile-fab-assets compile-edge-assets
compile-common-ai-provider-assets
- name: "Cleanup dist files"
run: rm -fv ./dist/*
- name: "Set current date as RELEASE_DATE variable"
@@ -214,6 +216,8 @@ jobs:
with:
platform: ${{ inputs.platform }}
save-cache: false
+ # The hatch_build.py files of the fab, edge3 and common.ai providers
run these hooks.
+ hooks: compile-fab-assets compile-edge-assets
compile-common-ai-provider-assets
- name: "Cleanup dist files"
run: rm -fv ./dist/*
- name: "Prepare provider distributions: wheel"
diff --git a/scripts/tests/ci/test_prek_cache_optimizations.py
b/scripts/tests/ci/test_prek_cache_optimizations.py
index 5da98527896..8d8449153ce 100644
--- a/scripts/tests/ci/test_prek_cache_optimizations.py
+++ b/scripts/tests/ci/test_prek_cache_optimizations.py
@@ -18,6 +18,7 @@
from __future__ import annotations
import os
+import re
import shlex
import shutil
import subprocess
@@ -65,7 +66,7 @@ def sandbox(tmp_path):
home.mkdir()
output = tmp_path / "output"
output.touch()
- return {"HOME": str(home), "GITHUB_OUTPUT": str(output)}
+ return {"HOME": str(home), "GITHUB_OUTPUT": str(output), "HOOKS": ""}
def read_outputs(env):
@@ -157,6 +158,8 @@ def test_cache_key_keeps_only_environment_inputs():
"change-detection-uncertain",
),
({"SAVE_CACHE": "true"}, False, "unchanged"),
+ ({"HOOKS": "mypy-providers"}, False, "selected-hooks"),
+ ({"HOOKS": "mypy-providers", "SAVE_CACHE": "true", "STASH_HIT":
"false"}, False, "selected-hooks"),
],
)
def test_cache_refresh_policy(sandbox, inputs, expected_save, expected_reason):
@@ -198,6 +201,85 @@ def test_restored_hooks_are_always_validated(sandbox,
fake_tools):
assert read_outputs(sandbox)["change-detection-uncertain"] == "false"
[email protected](
+ ("hooks", "expected_arguments", "expected_reason"),
+ [
+ (
+ "compile-fab-assets compile-edge-assets",
+ ["compile-fab-assets", "compile-edge-assets"],
+ "selected-hooks",
+ ),
+ (
+ "compile-fab-assets\ncompile-edge-assets\n",
+ ["compile-fab-assets", "compile-edge-assets"],
+ "selected-hooks",
+ ),
+ (
+ " \n",
+ ["--skip", "run-skill-eval", "--skip", "run-skill-eval-codex",
"--skip", "view-skill-eval"],
+ "cache-miss",
+ ),
+ ],
+ ids=("space-separated", "newline-separated", "whitespace-only"),
+)
+def test_hooks_input_decides_what_is_prepared_and_saved(
+ sandbox, fake_tools, hooks, expected_arguments, expected_reason
+):
+ install_step = find_step(PREK_ACTION, step_id="install-hooks")
+ policy_step = find_step(PREK_ACTION, step_id="cache-policy")
+ assert install_step["env"]["HOOKS"] == policy_step["env"]["HOOKS"] == "${{
inputs.hooks }}"
+ result = run_shell(install_step["run"], {**sandbox, **fake_tools, "HOOKS":
hooks})
+ assert result.returncode == 0, result.stderr
+ assert read_commands(fake_tools) == [["prek", "install-hooks",
*expected_arguments]]
+ result = run_shell(
+ policy_step["run"], {**sandbox, "HOOKS": hooks, "SAVE_CACHE": "true",
"STASH_HIT": "false"}
+ )
+ assert result.returncode == 0, result.stderr
+ assert read_outputs(sandbox)["reason"] == expected_reason
+
+
+def find_hatch_build_hooks():
+ return {
+ hook
+ for path in (*ROOT.glob("providers/*/hatch_build.py"),
*ROOT.glob("providers/*/*/hatch_build.py"))
+ for hook in re.findall(r'"prek", "run", "([\w-]+)"', path.read_text())
+ }
+
+
+def find_hooks_run_by_job(job):
+ hooks = set()
+ for step in job["steps"]:
+ command = step.get("run", "")
+ if "prepare-provider-distributions" in command:
+ hooks |= find_hatch_build_hooks()
+ hooks.update(re.findall(r"^prek .* ([\w-]+) --all-files$", command,
re.MULTILINE))
+ return hooks
+
+
+def test_selected_hooks_match_the_hooks_each_job_runs():
+ selections = {
+ f"{workflow.name}:{job_id}": (set(step["with"]["hooks"].split()),
find_hooks_run_by_job(job))
+ for workflow in (ROOT / ".github/workflows").glob("*.yml")
+ for job_id, job in load_yaml(workflow)["jobs"].items()
+ for step in job.get("steps", [])
+ if step.get("uses") == "./.github/actions/install-prek" and "hooks" in
step.get("with", {})
+ }
+ assert sorted(selections) == [
+ "ci-amd.yml:migration-round-trip",
+ "ci-amd.yml:mypy-providers",
+ "ci-arm.yml:migration-round-trip",
+ "ci-arm.yml:mypy-providers",
+ "generate-constraints.yml:generate-constraints-matrix",
+ "prod-image-build.yml:build-prod-packages",
+ "test-providers.yml:prepare-install-verify-provider-distributions",
+ "test-providers.yml:providers-compatibility-tests-matrix",
+ ]
+ for site, (selected, run) in selections.items():
+ assert selected == run, (
+ f"Set the hooks input of the install-prek step in {site} to: {'
'.join(sorted(run))}"
+ )
+
+
@pytest.mark.parametrize("cache_state", ("missing", "empty",
"renamed-markers", "log-only"))
@pytest.mark.parametrize("save_cache", ("true", "false"))
def test_markerless_cache_refresh_policy(sandbox, fake_tools, cache_state,
save_cache):