diff --git a/.github/backwards-compatibility-check.sh b/.github/backwards-compatibility-check.sh
new file mode 100644
index 00000000000..9467e36a295
--- /dev/null
+++ b/.github/backwards-compatibility-check.sh
@@ -0,0 +1,133 @@
+#!/bin/bash
+# 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
+#
+# http://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.
+
+set -e
+
+# USAGE:
+#
+# backwards-compatibility-check.sh COMPONENT [BASE_REF]
+#
+# COMPONENT: The component directory name to run the backwards compatibility check for.
+# BASE_REF: Optional. The baseline git ref (e.g. 'main', 'origin/main' or a release tag) to compare against. Defaults to 'main'.
+
+if [ "$#" -lt 1 ] || [ "$#" -gt 2 ]; then
+ echo "usage: backwards-compatibility-check.sh COMPONENT [BASE_REF]"
+ exit 1
+fi
+
+COMPONENT=$1
+BASE_REF=${2:-main}
+
+# Exception for 'dev'
+if [ "${COMPONENT}" = "dev" ]; then
+ echo "Skipping dev directory (not a public component)."
+ exit 0
+fi
+
+# Check if component directory exists
+if [ ! -d "${COMPONENT}" ]; then
+ echo "Error: Directory ${COMPONENT} does not exist!" >&2
+ exit 1
+fi
+
+# Check if composer.json exists
+COMP_JSON="${COMPONENT}/composer.json"
+if [ ! -f "${COMP_JSON}" ]; then
+ echo "Error: composer.json not found in ${COMPONENT}!" >&2
+ exit 1
+fi
+
+echo "Checking backwards compatibility for component: ${COMPONENT} against baseline: ${BASE_REF}" >&2
+
+# Check if the component existed in the baseline reference
+if ! git rev-parse --verify "${BASE_REF}:${COMPONENT}" >/dev/null 2>&1; then
+ echo "Component ${COMPONENT} did not exist in baseline ${BASE_REF}. Skipping check (all additions)." >&2
+ exit 0
+fi
+
+# Create a temporary directory
+TMP_DIR=$(mktemp -d)
+
+# Initialize a dummy git repo inside TMP_DIR so roave-backward-compatibility-check can compare revisions
+(
+ cd "${TMP_DIR}"
+ git init -q
+)
+
+# Extract baseline files from the BASE_REF, stripping the prefix folder so they land at the root of TMP_DIR
+if ! git archive "${BASE_REF}" "${COMPONENT}" | tar -x --strip-components=1 -C "${TMP_DIR}" 2>/dev/null; then
+ echo "Error: Failed to archive and extract files for ${COMPONENT} from git ref ${BASE_REF}." >&2
+ rm -rf "${TMP_DIR}"
+ exit 1
+fi
+
+(
+ cd "${TMP_DIR}"
+ git add -A
+ git commit -q -m "Base state from ${BASE_REF}"
+)
+
+# Copy the current local component files over the baseline repository,
+# making sure to exclude vendor directories or composer-local files.
+echo "Applying local changes from ${COMPONENT} to the baseline clone..." >&2
+rsync -a --exclude="vendor/" --exclude="composer-local.json" "${COMPONENT}/" "${TMP_DIR}/"
+
+# Commit the changes in the cloned split repository so we can compare them
+CODE=0
+if (
+ cd "${TMP_DIR}"
+ git add -A
+
+ # Check if there are any changes to commit
+ if ! git diff --cached --quiet; then
+ git commit -q -m "Apply local PR changes"
+ echo "Running Roave Backward Compatibility Check..." >&2
+
+ # Locate the roave binary portably
+ COMPOSER_BIN=$(composer global config bin-dir --absolute 2>/dev/null || echo ~/.composer/vendor/bin)
+ ROAVE_BIN=$(command -v roave-backward-compatibility-check || echo "${COMPOSER_BIN}/roave-backward-compatibility-check")
+
+ FORMAT=${BC_FORMAT:-markdown}
+ if [ "${FORMAT}" = "markdown" ]; then
+ # Run Roave check and capture output
+ OUTPUT=$("${ROAVE_BIN}" --from=HEAD~1 --format=markdown 2>/dev/null || true)
+ if [ -n "${OUTPUT}" ]; then
+ echo ""
+ echo "${COMPONENT}: Backwards Compatibility Breaks Detected
"
+ echo ""
+ echo "${OUTPUT}"
+ echo " "
+ echo ""
+ exit 1
+ fi
+ else
+ if ! "${ROAVE_BIN}" --from=HEAD~1 --format="${FORMAT}"; then
+ echo "❌ BC Breaks detected in ${COMPONENT}!" >&2
+ exit 1
+ else
+ echo "✅ No BC Breaks detected in ${COMPONENT}." >&2
+ fi
+ fi
+ else
+ echo "No files modified for ${COMPONENT} compared to ${BASE_REF}. Skipping check." >&2
+ fi
+); then
+ CODE=0
+else
+ CODE=1
+fi
+
+rm -rf "${TMP_DIR}"
+exit $CODE
diff --git a/.github/workflows/release-checks.yaml b/.github/workflows/release-checks.yaml
index 336a93531f5..0d1a14b5edc 100644
--- a/.github/workflows/release-checks.yaml
+++ b/.github/workflows/release-checks.yaml
@@ -5,6 +5,7 @@ on:
branches: ['main']
permissions:
contents: read
+ pull-requests: write
jobs:
# More info at https://github.com/Roave/BackwardCompatibilityCheck.
backwards-compatibility-check:
@@ -19,28 +20,8 @@ jobs:
uses: shivammathur/setup-php@v2
with:
php-version: "8.1"
- - name: "Rebase PR branch onto main branch"
- if: github.event.pull_request.user.login != 'release-please[bot]'
- run: |
- git config user.name "Github Actions"
- git config user.email "actions@github.com"
- git checkout ${{ github.event.pull_request.head.sha }}
- if ! git rebase origin/main; then
- echo "Failed to rebase PR branch onto main. There may be conflicts. Please resolve conflicts or rebase manually."
- exit 1
- fi
- name: "Install dependencies"
run: composer global require "roave/backward-compatibility-check:^8.2"
- - name: "Check for BC breaks"
- if: github.event.pull_request.user.login != 'release-please[bot]'
- run: |
- ~/.composer/vendor/bin/roave-backward-compatibility-check --from=origin/main --format=github-actions
- - name: "Check for BC label"
- run: |
- if [[ "true" == "${{ contains(github.event.pull_request.title, '!:') }}" ]]; then
- echo "Breaking change label found in PR title"
- exit 1
- fi
- name: Get Latest Release
if: github.event.pull_request.user.login == 'release-please[bot]'
id: latest-release
@@ -48,14 +29,58 @@ jobs:
with:
repository: ${{ github.repository }}
token: ${{ secrets.GITHUB_TOKEN }}
- - name: "Check for BC breaks (Next Release)"
- if: github.event.pull_request.user.login == 'release-please[bot]'
- # We've already approved and justified the breaking changes. Run the check but continue on error
- continue-on-error: true
+ - name: "Check for BC breaks"
+ id: check-bc
run: |
- ~/.composer/vendor/bin/roave-backward-compatibility-check \
- --from=${{ steps.latest-release.outputs.release }} \
- --to=origin/main --format=github-actions
+ git config --global user.name "Github Actions"
+ git config --global user.email "actions@github.com"
+
+ # Determine base ref for git diff
+ BASE_REF="${{ steps.latest-release.outputs.release }}"
+ BASE_REF=${BASE_REF:-origin/main}
+
+ # Find changed component directories
+ CHANGED_COMPONENTS=$(git diff ${BASE_REF} --name-only | grep -E '^[A-Z][a-zA-Z0-9]*/' | cut -d'/' -f1 | sort -u || true)
+ if [ -z "${CHANGED_COMPONENTS}" ]; then
+ echo "No public component directories have changed."
+ exit 0
+ fi
+
+ IS_RELEASE_PR="${{ github.event.pull_request.user.login == 'release-please[bot]' }}"
+
+ if [ "${IS_RELEASE_PR}" = "true" ]; then
+ # Release PR: gather markdown and do not fail the step
+ BC_BREAKS=""
+ for COMPONENT in ${CHANGED_COMPONENTS}; do
+ BC_BREAKS="$BC_BREAKS$(BC_FORMAT="markdown" bash .github/backwards-compatibility-check.sh "${COMPONENT}" "${BASE_REF}" || true)"
+ done
+ printf "bc-breaks<> $GITHUB_OUTPUT
+ else
+ # Normal PR: use github-actions format and fail if there are breaks
+ FAIL=""
+ for COMPONENT in ${CHANGED_COMPONENTS}; do
+ if ! BC_FORMAT="github-actions" bash .github/backwards-compatibility-check.sh "${COMPONENT}" "${BASE_REF}"; then
+ FAIL="true"
+ fi
+ done
+
+ if [ "${FAIL}" = "true" ]; then
+ exit 1
+ elif [[ "true" == "${{ contains(github.event.pull_request.title, '!:') }}" ]]; then
+ echo "❌ Error: You indicated breaking changes in your PR title (!:), but no backwards compatibility breaks were detected by the automated check."
+ exit 1
+ fi
+ fi
+ - name: "Add or update PR comment with BC breaks for Release PRs"
+ if: github.event.pull_request.user.login == 'release-please[bot]' && steps.check-bc.outputs.bc-breaks != ''
+ uses: marocchino/sticky-pull-request-comment@v2
+ with:
+ header: bc-breaking-changes-comment
+ message: |
+ ## ⚠️ Backwards Compatibility (BC) Breaking Changes Notice
+ This PR contains backward compatibility breaking changes detected by the automated BC check against the latest release. Please review the details below:
+
+ ${{ steps.check-bc.outputs.bc-breaks }}
# Ensure the release PR does not contain an unexpected (e.g. 2.0.0) major version release
# Add "MAJOR_VERSION_ALLOWED=component1,component2" to the PR description to allow major version
diff --git a/Gax/.gitignore b/Gax/.gitignore
index 94e81fe0df3..4f6a8850478 100644
--- a/Gax/.gitignore
+++ b/Gax/.gitignore
@@ -6,6 +6,7 @@ composer.lock
composer-test.lock
vendor/
build/artifacts/
+tests/Conformance/showcase.pem
artifacts/
.idea
.metadata