diff --git a/.github/backwards-compatibility-check.sh b/.github/backwards-compatibility-check.sh new file mode 100644 index 00000000000..6eb418db05a --- /dev/null +++ b/.github/backwards-compatibility-check.sh @@ -0,0 +1,118 @@ +#!/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") + + if ! "${ROAVE_BIN}" --from=HEAD~1 --format=markdown; then + echo "❌ BC Breaks detected in ${COMPONENT}!" >&2 + exit 1 + else + echo "✅ No BC Breaks detected in ${COMPONENT}." >&2 + 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..3022cf903fd 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,55 @@ 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: | + git config --global user.name "Github Actions" + git config --global user.email "actions@github.com" + BASE_REF="${{ steps.latest-release.outputs.release }}" + BASE_REF=${BASE_REF:-origin/main} + CHANGED_COMPONENTS=$(git diff ${BASE_REF} --name-only | grep -E '^[A-Z][a-zA-Z0-9]*/' | cut -d'/' -f1 | sort -u) + if [ -z "${CHANGED_COMPONENTS}" ]; then + echo "No public component directories have changed. Skipping backwards compatibility checks." + exit 0 + fi + BC_BREAKS="" + for COMPONENT in ${CHANGED_COMPONENTS}; do + BC_BREAKS="$BC_BREAKS$(bash .github/backwards-compatibility-check.sh "${COMPONENT}" "${BASE_REF}")" + done + # Set GITHUB_OUTPUT + printf "bc-breaks<> $GITHUB_OUTPUT + - name: "Prepare BC break report" + id: format-comment + run: | + BC_BREAKS="${{ steps.check-bc.outputs.bc-breaks }}" + if [ -n "$BC_BREAKS" ]; then + COMMENT_BODY="This PR contains backward compatibility breaking changes detected by the automated BC check against the latest release. Please review the details below:" + COMMENT_BODY="$COMMENT_BODY"$'\n\n'"$BC_BREAKS" + else + COMMENT_BODY="✅ No backwards compatibility breaking changes were detected." + fi + printf "comment-body<> $GITHUB_OUTPUT + - name: "Validate standard PR results" + if: github.event.pull_request.user.login != 'release-please[bot]' run: | - ~/.composer/vendor/bin/roave-backward-compatibility-check \ - --from=${{ steps.latest-release.outputs.release }} \ - --to=origin/main --format=github-actions + if [ -n "${{ steps.check-bc.outputs.bc-breaks }}" ]; then + echo "❌ ${{ steps.format-comment.outputs.comment-body }}" + 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 + else + echo "✅ No backwards compatibility breaks detected." + fi + - name: "Add or update PR comment with BC breaks for Release PRs" + if: github.event.pull_request.user.login == 'release-please[bot]' + uses: marocchino/sticky-pull-request-comment@v2 + with: + header: bc-breaking-changes-comment + message: | + ## ⚠️ Backwards Compatibility (BC) Breaking Changes Notice + ${{ steps.format-comment.outputs.comment-body }} # 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