Skip to content

Commit 73e9a02

Browse files
authored
chore(ci): use get_package_shards.py in import-profiler for dynamic sharding (#18079)
### Summary Updates the `import-profiler` workflow to use `ci/get_package_shards.py` for dynamic sharding matrix generation, bringing it in line with the sharding pattern used in `lint.yml` and `unittest.yml`. ### Changes * **`initialize` Job**: Added an initialization job running `ci/get_package_shards.py` with `MAX_SHARDS: 8` to dynamically calculate shard distribution based on modified packages. * **Dynamic Shards**: Replaced static 8-shard matrix (`[0..7]`) with dynamic shard matrix evaluation. If few or no packages changed, only the necessary number of shards are spawned instead of always spinning up 8 runners. * **Code Cleanliness**: Removed manual git diff parsing and modulo arithmetic from the workflow step, passing `PACKAGE_LIST` directly to `ci/run_conditional_tests.sh`. * **Workflow Status Check**: Updated `all-import-profiles` to depend on `[initialize, import-profile]` and verify both job outcomes.
1 parent 43b786c commit 73e9a02

3 files changed

Lines changed: 87 additions & 36 deletions

File tree

.github/workflows/import-profiler.yml

Lines changed: 56 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,62 @@ permissions:
1313
contents: read
1414

1515
jobs:
16+
initialize:
17+
runs-on: ubuntu-latest
18+
outputs:
19+
matrix: ${{ steps.set-matrix.outputs.matrix }}
20+
is_full_run: ${{ steps.check-label.outputs.is_full_run }}
21+
env:
22+
MAX_SHARDS: 8
23+
steps:
24+
- name: Checkout
25+
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
26+
# Use a fetch-depth of 2 to avoid error `fatal: origin/main...HEAD: no merge base`
27+
# See https://github.com/googleapis/google-cloud-python/issues/12013
28+
# and https://github.com/actions/checkout#checkout-head.
29+
with:
30+
fetch-depth: 2
31+
persist-credentials: false
32+
- name: Check for unit_test:all_packages label
33+
id: check-label
34+
run: |
35+
if [[ "${{ contains(github.event.pull_request.labels.*.name, 'unit_test:all_packages') }}" == "true" ]]; then
36+
echo "is_full_run=true" >> $GITHUB_OUTPUT
37+
else
38+
echo "is_full_run=false" >> $GITHUB_OUTPUT
39+
fi
40+
- name: Setup Python
41+
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
42+
with:
43+
python-version: "3.10"
44+
- name: Get package shards
45+
id: set-matrix
46+
env:
47+
BUILD_TYPE: presubmit
48+
TARGET_BRANCH: ${{ github.base_ref || github.event.merge_group.base_ref }}
49+
TEST_ALL_PACKAGES: ${{ steps.check-label.outputs.is_full_run }}
50+
MAX_SHARDS: ${{ env.MAX_SHARDS }}
51+
run: |
52+
if [ -n "$TARGET_BRANCH" ]; then
53+
git fetch origin "$TARGET_BRANCH" --depth=1 || true
54+
fi
55+
python3 ci/get_package_shards.py
56+
1657
import-profile:
58+
needs: initialize
59+
if: needs.initialize.outputs.matrix != '[]' && needs.initialize.outputs.matrix != ''
1760
runs-on: ubuntu-latest
1861
timeout-minutes: 60
1962
strategy:
2063
fail-fast: false
2164
matrix:
22-
shard: [0, 1, 2, 3, 4, 5, 6, 7] # 8 parallel shards
23-
name: import-profile (Shard ${{ matrix.shard }})
65+
package_shard: ${{ fromJson(needs.initialize.outputs.matrix) }}
66+
name: ${{ matrix.package_shard.is_sharded && format('import-profile ({0})', matrix.package_shard.name) || format('import-profile ({0})', matrix.package_shard.description) }}
2467
steps:
2568
- name: Checkout
26-
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
69+
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
2770
with:
28-
fetch-depth: 0 # Fetch git history to find changed packages
71+
fetch-depth: 0 # Fetch git history to find changed packages / baseline
2972
persist-credentials: false
3073
- name: Setup Python
3174
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
@@ -38,51 +81,30 @@ jobs:
3881
python -m pip install --upgrade pip
3982
pip install pytest pytest-cov setuptools
4083
pytest scripts/import_profiler/test_profiler.py --cov=profiler --cov-report=term-missing --cov-fail-under=100
41-
- name: Run import profiler
84+
- name: Run import profiler for ${{ matrix.package_shard.description }}
4285
env:
4386
BUILD_TYPE: presubmit
4487
TARGET_BRANCH: ${{ github.base_ref || github.event.merge_group.base_ref }}
4588
TEST_TYPE: import_profile
4689
PY_VERSION: "3.15"
4790
# Workaround: Allows libcst to compile on Python 3.15+ while PyO3 catches up
4891
PYO3_USE_ABI3_FORWARD_COMPATIBILITY: "1"
49-
SHARD_INDEX: ${{ matrix.shard }}
50-
TOTAL_SHARDS: 8
92+
PACKAGE_LIST: ${{ matrix.package_shard.packages }}
5193
run: |
52-
TARGET_BRANCH=${TARGET_BRANCH:-main}
53-
git fetch origin "${TARGET_BRANCH}" --deepen=200 || true
54-
55-
# Get unique list of modified packages under packages/
56-
modified_packages=$(git diff --name-only origin/"${TARGET_BRANCH}"... | grep '^packages/' | cut -d/ -f1,2 | sort -u)
57-
58-
# Filter packages assigned to this specific shard index
59-
idx=0
60-
packages_to_test=""
61-
for pkg in $modified_packages; do
62-
if [ -d "$pkg" ]; then
63-
if [ "$((idx % TOTAL_SHARDS))" -eq "${SHARD_INDEX}" ]; then
64-
packages_to_test="$packages_to_test $pkg"
65-
fi
66-
idx=$((idx + 1))
67-
fi
68-
done
69-
70-
# Run tests on the assigned packages
71-
if [ -n "$packages_to_test" ]; then
72-
echo "Shard ${{ matrix.shard }} running packages: $packages_to_test"
73-
PACKAGE_LIST="$packages_to_test" ci/run_conditional_tests.sh
74-
else
75-
echo "No packages assigned to Shard ${{ matrix.shard }}."
76-
fi
94+
ci/run_conditional_tests.sh
7795
7896
all-import-profiles:
79-
needs: import-profile
97+
needs: [initialize, import-profile]
8098
if: always()
8199
runs-on: ubuntu-latest
82100
steps:
83101
- name: Check import profile results
84102
run: |
85-
if [[ "${{ needs.import-profile.result }}" != "success" && "${{ needs.import-profile.result }}" != "skipped" ]]; then
103+
if [[ "${{ needs.initialize.result }}" != "success" ]]; then
104+
echo "Error: The initialize job status was: ${{ needs.initialize.result }}"
105+
exit 1
106+
fi
107+
if [[ "${{ needs['import-profile'].result }}" != "success" && "${{ needs['import-profile'].result }}" != "skipped" ]]; then
86108
echo "Import profiles failed"
87109
exit 1
88110
fi

scripts/import_profiler/profiler.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,9 @@ def find_module_from_package(pkg):
385385
try:
386386
files = importlib.metadata.files(pkg)
387387
if files:
388-
ignored_parts = ('tests', 'testing', 'samples', 'examples', 'benchmark', 'benchmarks')
388+
ignored_parts = {'tests', 'testing', 'samples', 'examples', 'benchmark', 'benchmarks', 'third_party', 'test_utils', 'docs', 'build', 'dist', 'bin', 'ci', 'scripts', 'cloudbuild', 'notebooks', 'assets', 'scratch', 'specs'}
389+
if pkg == "google-cloud-testutils":
390+
ignored_parts.discard('test_utils')
389391
init_files = [str(f) for f in files if str(f).endswith('__init__.py') and '__pycache__' not in str(f) and not any(part in ignored_parts for part in str(f).replace('\\', '/').split('/'))]
390392
if init_files:
391393
from pathlib import Path
@@ -406,13 +408,20 @@ def find_module_from_package(pkg):
406408
import os
407409
if os.path.exists('setup.py') or os.path.exists('pyproject.toml'):
408410
where_dir = "src" if os.path.isdir("src") else "."
411+
abs_where_dir = os.path.abspath(where_dir)
412+
if abs_where_dir not in sys.path:
413+
sys.path.insert(0, abs_where_dir)
409414
pkgs = setuptools.find_namespace_packages(where=where_dir)
410415
ignored_prefixes = ("tests", "samples", "examples", "benchmark", "benchmarks", "third_party", "testing", "test_utils", "docs", "build", "dist", "bin", "ci", "scripts", "cloudbuild", "notebooks", "assets", "scratch", "specs")
416+
ignored_starts = ("test_", "tests_", "sample_", "samples_", "bench_", "benchmarks_", "example_", "examples_", "doc_", "docs_", "notebook_", "notebooks_")
411417

412418
filtered = []
413419
for p in pkgs:
414420
top = p.split(".")[0]
415-
if top in ignored_prefixes or top.startswith(("test_", "sample_", "bench_", "example_", "doc_", "notebook_")) or p in ("google", "google.cloud"):
421+
is_ignored_top = top in ignored_prefixes or top.startswith(ignored_starts)
422+
if is_ignored_top and pkg == "google-cloud-testutils" and top == "test_utils":
423+
is_ignored_top = False
424+
if is_ignored_top or p in ("google", "google.cloud"):
416425
continue
417426
filtered.append(p)
418427

scripts/import_profiler/test_profiler.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -641,6 +641,13 @@ def test_find_module_from_package_metadata_init():
641641
assert res == "foo.bar"
642642

643643

644+
def test_find_module_from_package_metadata_test_utils():
645+
with patch("importlib.metadata.files", return_value=["test_utils/__init__.py"]), \
646+
patch("importlib.util.find_spec", return_value=True):
647+
res = find_module_from_package("google-cloud-testutils")
648+
assert res == "test_utils"
649+
650+
644651
def test_find_module_from_package_setuptools():
645652
sys.modules.setdefault("setuptools", MagicMock())
646653
with patch("importlib.metadata.files", side_effect=Exception), \
@@ -652,6 +659,19 @@ def test_find_module_from_package_setuptools():
652659
assert res == "my_pkg"
653660

654661

662+
def test_find_module_from_package_setuptools_test_utils():
663+
sys.modules.setdefault("setuptools", MagicMock())
664+
with patch("importlib.metadata.files", side_effect=Exception), \
665+
patch("profiler.os.path.exists", return_value=True), \
666+
patch("profiler.os.path.isdir", return_value=True), \
667+
patch("setuptools.find_namespace_packages", return_value=["test_utils", "tests"]) as mock_find, \
668+
patch("profiler.os.path.isfile", return_value=True), \
669+
patch("importlib.util.find_spec", return_value=True):
670+
res = find_module_from_package("google-cloud-testutils")
671+
assert res == "test_utils"
672+
mock_find.assert_called_once_with(where="src")
673+
674+
655675
def test_find_module_from_package_setuptools_not_file_and_exception():
656676
sys.modules.setdefault("setuptools", MagicMock())
657677
def mock_isfile(path):

0 commit comments

Comments
 (0)