diff --git a/.github/workflows/prerelease-deps.yml b/.github/workflows/prerelease-deps.yml new file mode 100644 index 000000000000..ba816dddbb25 --- /dev/null +++ b/.github/workflows/prerelease-deps.yml @@ -0,0 +1,186 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: prerelease-deps + +on: + pull_request: + paths: + - '.github/workflows/prerelease-deps.yml' + - 'ci/**' + schedule: + # Run daily at midnight UTC + - cron: '0 0 * * *' + workflow_dispatch: + +permissions: + contents: read + issues: write + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} + cancel-in-progress: true + +env: + PACKAGE_DIRS: packages + NOX_DEFAULT_VENV_BACKEND: "virtualenv" + UV_PRERELEASE: "allow" + # Workaround: Allows libcst/Rust extensions to compile on Python 3.15+ while PyO3 catches up + PYO3_USE_ABI3_FORWARD_COMPATIBILITY: "1" + +jobs: + prerelease: + runs-on: ubuntu-22.04 + strategy: + matrix: + python: ["3.15"] + name: Nightly Pre-release Dependency Issue Reporter (${{ matrix.python }}) + env: + PY_VERSION: ${{ matrix.python }} + FAILURE_LOG_DIR: "/tmp/test_failures" + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + # Use a fetch-depth of 2 to avoid error `fatal: origin/main...HEAD: no merge base` + # See https://github.com/googleapis/google-cloud-python/issues/12013 + # and https://github.com/actions/checkout#checkout-head. + with: + fetch-depth: 2 + persist-credentials: false + + - name: Setup Python + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 + with: + python-version: ${{ env.PY_VERSION }} + allow-prereleases: true + + - name: Cache pip wheels (including compiled source builds) + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 + with: + path: ~/.cache/pip + # Save a fresh cache snapshot for each run so newly compiled wheels are persisted + key: ${{ runner.os }}-pip-prerelease-${{ env.PY_VERSION }}-${{ github.run_id }} + # Restore the most recent cache from previous runs for this Python version + restore-keys: | + ${{ runner.os }}-pip-prerelease-${{ env.PY_VERSION }}- + + - name: Install nox + run: | + pip install nox + + - name: Get handwritten packages to test + id: get-packages + env: + BUILD_TYPE: continuous + TEST_ALL_PACKAGES: "true" + HANDWRITTEN_ONLY: "true" + run: | + python3 ci/get_package_shards.py + + - name: Run prerelease_deps across handwritten packages + id: run-tests + # Do not fail the job when pre-release tests fail; instead, report via GitHub issue below. + continue-on-error: true + env: + BUILD_TYPE: continuous + TEST_TYPE: prerelease + PACKAGE_LIST: ${{ steps.get-packages.outputs.packages }} + NOXFORCEPYTHON: ${{ env.PY_VERSION }} + CONTINUE_ON_ERROR: "true" + PARALLEL_WORKERS: "1" + run: | + ci/run_conditional_tests.sh + + - name: Prepare issue body on failure + if: ${{ steps.run-tests.outcome == 'failure' }} + id: issue-body + run: | + shopt -s nullglob + log_files=("${FAILURE_LOG_DIR}"/*.log.txt) + if [ ${#log_files[@]} -gt 0 ]; then + COUNT=${#log_files[@]} + PKG_LIST="" + DETAILS="" + TRUNCATED_NOTE="" + for log_file in "${log_files[@]}"; do + pkg=$(basename "$log_file" .log.txt) + pip_file="${FAILURE_LOG_DIR}/${pkg}.pip.txt" + log_tail=$(cat "$log_file" 2>/dev/null || echo "Unavailable") + pip_list=$(cat "$pip_file" 2>/dev/null || echo "No dependencies installed (failed during build)") + PKG_LIST="${PKG_LIST}- \`${pkg}\`"$'\n' + + entry=$(cat < + ${pkg} + + **Test Failure Output (last 50 lines):** + \`\`\`text + ${log_tail} + \`\`\` + +
+ Installed Dependencies (pip list) + + \`\`\`text + ${pip_list} + \`\`\` +
+ + EOF + ) + if [ $(( ${#DETAILS} + ${#entry} )) -le 48000 ]; then + DETAILS="${DETAILS}"$'\n'"${entry}"$'\n' + elif [ -z "${TRUNCATED_NOTE}" ]; then + TRUNCATED_NOTE=$'\n\n_Note: Detailed logs for remaining packages were omitted to fit GitHub issue length limits. Please check the workflow run logs for full details._' + fi + done + else + COUNT="unknown" + PKG_LIST="- (Workflow failed outside package test execution)" + DETAILS="" + TRUNCATED_NOTE="" + fi + + echo "::warning title=Pre-release Dependency Failures::Detected pre-release test failures in ${COUNT} package(s). See job summary or GitHub issues for details." + + { + echo "### ⚠️ Pre-release Dependency Failures (${COUNT} packages)" + echo "" + echo "The pre-release check detected failures on Python \`${PY_VERSION}\`:" + echo "" + printf "%s" "${PKG_LIST}" + } >> "$GITHUB_STEP_SUMMARY" + + { + echo "body<> "$GITHUB_OUTPUT" + + - name: Create issue on failure + if: ${{ steps.run-tests.outcome == 'failure' && github.repository == 'googleapis/google-cloud-python' }} + uses: googleapis/librarian/.github/actions/create-issue-on-failure@bb2ee61752bd0f5387195b54c509e4d01a52b41d + with: + title: "prerelease_deps check failed" + labels: "priority: p1,nightly" + body: ${{ steps.issue-body.outputs.body }} diff --git a/.github/workflows/regenerate-all.yml b/.github/workflows/regenerate-all.yml index b185e3b4dab5..85e64d8c9312 100644 --- a/.github/workflows/regenerate-all.yml +++ b/.github/workflows/regenerate-all.yml @@ -78,6 +78,10 @@ jobs: elif [ -n "$PACKAGES" ]; then for pkg_path in $PACKAGES; do pkg=$(basename "$pkg_path") + if ! grep -q "^ - name: ${pkg}$" librarian.yaml; then + echo "Skipping $pkg (not in librarian.yaml)..." + continue + fi echo "Regenerating $pkg..." librarian generate "$pkg" done diff --git a/.kokoro/continuous/prerelease.cfg b/.kokoro/continuous/prerelease.cfg deleted file mode 100644 index 3595fb43f5c0..000000000000 --- a/.kokoro/continuous/prerelease.cfg +++ /dev/null @@ -1,7 +0,0 @@ -# Format: //devtools/kokoro/config/proto/build.proto - -# Only run this nox session. -env_vars: { - key: "NOX_SESSION" - value: "prerelease_deps" -} diff --git a/.kokoro/system.sh b/.kokoro/system.sh index 2b06d445964a..bdb56214026a 100755 --- a/.kokoro/system.sh +++ b/.kokoro/system.sh @@ -57,7 +57,7 @@ run_package_test() { local PROJECT_ID local GOOGLE_APPLICATION_CREDENTIALS local NOX_FILE - # Inherit NOX_SESSION from environment to allow configs (like prerelease.cfg) to pass it in + # Inherit NOX_SESSION from environment to allow configs to pass it in local NOX_SESSION="${NOX_SESSION}" # ISOLATION: Create a unique gcloud config dir for this run @@ -77,9 +77,8 @@ run_package_test() { PROJECT_ID=$(cat "${KOKORO_GFILE_DIR}/google-auth-project-id.json") GOOGLE_APPLICATION_CREDENTIALS="${KOKORO_GFILE_DIR}/google-auth-service-account.json" - # Note: system.sh is also reused for monorepo-wide continuous unit test jobs - # like `core_deps_from_source` and `prerelease_deps`. For google-auth, we only - # want to override NOX_FILE to system_tests/noxfile.py when running actual system tests. + # Note: For google-auth, we only want to override NOX_FILE to system_tests/noxfile.py + # when running actual system tests. if [[ -z "${NOX_SESSION}" || "${NOX_SESSION}" == "system-"* ]]; then NOX_FILE="system_tests/noxfile.py" NOX_SESSION="" diff --git a/ci/get_package_shards.py b/ci/get_package_shards.py index 67ded9e4aa06..d0dd4e7d4ea2 100644 --- a/ci/get_package_shards.py +++ b/ci/get_package_shards.py @@ -148,8 +148,9 @@ def get_packages_to_test(): build_type = os.environ.get("BUILD_TYPE", "presubmit") target_branch = os.environ.get("TARGET_BRANCH", "main") test_all_packages = os.environ.get("TEST_ALL_PACKAGES", "false").lower() == "true" + handwritten_only = os.environ.get("HANDWRITTEN_ONLY", "false").lower() == "true" - all_packages = get_packages() + all_packages = get_packages(handwritten_only=handwritten_only) if test_all_packages: return all_packages diff --git a/ci/run_conditional_tests.sh b/ci/run_conditional_tests.sh index e276b54b2a0d..19a5dd8850a2 100755 --- a/ci/run_conditional_tests.sh +++ b/ci/run_conditional_tests.sh @@ -78,6 +78,14 @@ run_test_in_dir() { local log_file="/tmp/test_log_${PY_VERSION}_${pkg_name_clean}.log" export COVERAGE_FILE="${PROJECT_ROOT}/.coverage.${PY_VERSION}.${pkg_name_clean}" + # Isolate setuptools build directories per worker to prevent parallel build collisions. + local worker_build_dir="/tmp/build_${PY_VERSION}_${pkg_name_clean}" + local dist_cfg="/tmp/dist_cfg_${PY_VERSION}_${pkg_name_clean}.cfg" + mkdir -p "${worker_build_dir}" + printf "[build]\nbuild_base = %s/build\n[bdist_wheel]\nbdist_dir = %s/bdist\n[egg_info]\negg_base = %s\n" \ + "${worker_build_dir}" "${worker_build_dir}" "${worker_build_dir}" > "${dist_cfg}" + export DIST_EXTRA_CONFIG="${dist_cfg}" + local div="============================================================" local header="\n${div}\nRunning tests in ${d}\n${div}" local footer @@ -97,6 +105,8 @@ run_test_in_dir() { set -e popd > /dev/null + rm -rf "${worker_build_dir}" "${dist_cfg}" + if [ ${ret} -ne 0 ]; then footer="❌ Tests failed in ${d} with exit code ${ret}" else @@ -114,13 +124,26 @@ run_test_in_dir() { echo "${footer}" fi + if [ ${ret} -ne 0 ] && [ -n "${FAILURE_LOG_DIR}" ]; then + mkdir -p "${FAILURE_LOG_DIR}" + local pkg_name=$(basename "${d}") + if grep -q "short test summary info" "${log_file}" 2>/dev/null; then + grep -A 35 -B 2 "short test summary info" "${log_file}" 2>/dev/null | sed $'s/\x1b\\[[0-9;]*[a-zA-Z]//g' > "${FAILURE_LOG_DIR}/${pkg_name}.log.txt" || true + else + tail -n 50 "${log_file}" 2>/dev/null | sed $'s/\x1b\\[[0-9;]*[a-zA-Z]//g' > "${FAILURE_LOG_DIR}/${pkg_name}.log.txt" || true + fi + fi + rm -f "${log_file}" if [ ${ret} -ne 0 ]; then + if [ "${CONTINUE_ON_ERROR}" = "true" ]; then + exit 1 + fi exit 255 # Cancel xargs parallel jobs fi } export -f run_test_in_dir -export test_script PROJECT_ROOT PY_VERSION TEST_TYPE +export test_script PROJECT_ROOT PY_VERSION TEST_TYPE CONTINUE_ON_ERROR FAILURE_LOG_DIR dirs_to_test=() diff --git a/ci/run_single_test.sh b/ci/run_single_test.sh index d0dc6f612c0e..ced52a121eae 100755 --- a/ci/run_single_test.sh +++ b/ci/run_single_test.sh @@ -65,7 +65,11 @@ case ${TEST_TYPE} in retval=$? ;; prerelease) - nox -s prerelease_deps-3.14 + if [[ "$(pwd)" == */preview-packages/* ]]; then + echo "Skipping prerelease for preview package $(pwd)" + exit 0 + fi + nox --stop-on-first-error -s prerelease_deps retval=$? ;; core_deps_from_source) @@ -241,9 +245,24 @@ case ${TEST_TYPE} in ;; esac -# Clean up `__pycache__` and `.nox` directories to avoid error -# `No space left on device` seen when running tests in Github Actions +if [ ${retval} -ne 0 ] && [ -n "${FAILURE_LOG_DIR}" ]; then + mkdir -p "${FAILURE_LOG_DIR}" + pkg_name=$(basename "$(pwd)") + for pip_bin in "${NOX_ENVDIR:-.nox}"/*/bin/pip; do + if [ -x "$pip_bin" ]; then + pip_out=$("$pip_bin" list 2>/dev/null || true) + if echo "$pip_out" | grep -qvE "^(Package|-+|pip|setuptools|wheel)[[:space:]]"; then + echo "$pip_out" > "${FAILURE_LOG_DIR}/${pkg_name}.pip.txt" + fi + break + fi + done +fi + +# Clean up `__pycache__`, `.nox`, and build artifact directories to avoid error +# `No space left on device` and prevent leftover build/ files from polluting +# downstream local dependency builds. find . | grep -E "(__pycache__)" | xargs rm -rf -rm -rf .nox +rm -rf .nox build *.egg-info exit ${retval} diff --git a/packages/google-auth/setup.py b/packages/google-auth/setup.py index cff8a7bdbda9..8807e7eb342b 100644 --- a/packages/google-auth/setup.py +++ b/packages/google-auth/setup.py @@ -106,7 +106,7 @@ long_description=long_description, url="https://github.com/googleapis/google-cloud-python/tree/main/packages/google-auth", packages=find_namespace_packages( - exclude=("tests*", "system_tests*", "docs*", "samples*") + exclude=("tests*", "system_tests*", "docs*", "samples*", "build*") ), package_data={"google.auth": ["py.typed"], "google.oauth2": ["py.typed"]}, install_requires=DEPENDENCIES,