Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions .github/backwards-compatibility-check.sh
Original file line number Diff line number Diff line change
@@ -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
76 changes: 49 additions & 27 deletions .github/workflows/release-checks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
branches: ['main']
permissions:
contents: read
pull-requests: write

Check failure on line 8 in .github/workflows/release-checks.yaml

View workflow job for this annotation

GitHub Actions / zizmor-output

excessive-permissions

release-checks.yaml:8: overly broad permissions: pull-requests: write is overly broad at the workflow level
jobs:
# More info at https://github.com/Roave/BackwardCompatibilityCheck.
backwards-compatibility-check:
Expand All @@ -12,50 +13,71 @@
runs-on: ubuntu-latest
if: ${{ !contains(github.event.pull_request.body, 'BREAKING_CHANGE_REASON=') }}
steps:
- uses: actions/checkout@v7

Check failure on line 16 in .github/workflows/release-checks.yaml

View workflow job for this annotation

GitHub Actions / zizmor-output

zizmor/unpinned-uses

unpinned action reference: action is not pinned to a hash (required by blanket policy)

Check failure on line 16 in .github/workflows/release-checks.yaml

View workflow job for this annotation

GitHub Actions / zizmor-output

unpinned-uses

release-checks.yaml:16: unpinned action reference: action is not pinned to a hash (required by blanket policy)
with:
fetch-depth: 0
- name: "Install PHP"
uses: shivammathur/setup-php@v2

Check failure on line 20 in .github/workflows/release-checks.yaml

View workflow job for this annotation

GitHub Actions / zizmor-output

zizmor/unpinned-uses

unpinned action reference: action is not pinned to a hash (required by blanket policy)

Check failure on line 20 in .github/workflows/release-checks.yaml

View workflow job for this annotation

GitHub Actions / zizmor-output

unpinned-uses

release-checks.yaml:20: unpinned action reference: action is not pinned to a hash (required by blanket policy)
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
uses: pozetroninc/github-action-get-latest-release@master

Check failure on line 28 in .github/workflows/release-checks.yaml

View workflow job for this annotation

GitHub Actions / zizmor-output

zizmor/unpinned-uses

unpinned action reference: action is not pinned to a hash (required by blanket policy)

Check failure on line 28 in .github/workflows/release-checks.yaml

View workflow job for this annotation

GitHub Actions / zizmor-output

unpinned-uses

release-checks.yaml:28: unpinned action reference: action is not pinned to a hash (required by blanket policy)
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 }}"

Check failure on line 37 in .github/workflows/release-checks.yaml

View workflow job for this annotation

GitHub Actions / zizmor-output

zizmor/template-injection

code injection via template expansion: may expand into attacker-controllable code
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<<EOF\n%s\nEOF\n" "$BC_BREAKS" >> $GITHUB_OUTPUT
- name: "Prepare BC break report"
id: format-comment
run: |
BC_BREAKS="${{ steps.check-bc.outputs.bc-breaks }}"

Check failure on line 53 in .github/workflows/release-checks.yaml

View workflow job for this annotation

GitHub Actions / zizmor-output

zizmor/template-injection

code injection via template expansion: may expand into attacker-controllable code
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<<EOF\n%s\nEOF\n" "$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

Check failure on line 64 in .github/workflows/release-checks.yaml

View workflow job for this annotation

GitHub Actions / zizmor-output

zizmor/template-injection

code injection via template expansion: may expand into attacker-controllable code
echo "❌ ${{ steps.format-comment.outputs.comment-body }}"

Check failure on line 65 in .github/workflows/release-checks.yaml

View workflow job for this annotation

GitHub Actions / zizmor-output

zizmor/template-injection

code injection via template expansion: may expand into attacker-controllable code
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

Check failure on line 75 in .github/workflows/release-checks.yaml

View workflow job for this annotation

GitHub Actions / zizmor-output

zizmor/unpinned-uses

unpinned action reference: action is not pinned to a hash (required by blanket policy)

Check failure on line 75 in .github/workflows/release-checks.yaml

View workflow job for this annotation

GitHub Actions / zizmor-output

unpinned-uses

release-checks.yaml:75: unpinned action reference: action is not pinned to a hash (required by blanket policy)
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
Expand All @@ -65,11 +87,11 @@
runs-on: ubuntu-latest
if: github.event.pull_request.user.login == 'release-please[bot]'
steps:
- uses: actions/checkout@v7

Check failure on line 90 in .github/workflows/release-checks.yaml

View workflow job for this annotation

GitHub Actions / zizmor-output

unpinned-uses

release-checks.yaml:90: unpinned action reference: action is not pinned to a hash (required by blanket policy)
with:
fetch-depth: 0
- name: Parse allowed major versions
uses: actions-ecosystem/action-regex-match@v2

Check failure on line 94 in .github/workflows/release-checks.yaml

View workflow job for this annotation

GitHub Actions / zizmor-output

unpinned-uses

release-checks.yaml:94: unpinned action reference: action is not pinned to a hash (required by blanket policy)
id: allowed-major-versions
with:
text: ${{ github.event.pull_request.body }}
Expand All @@ -78,7 +100,7 @@
- name: "Check for unexpected major version"
run: |
# parse allowed major versions into an array
IFS=', ' read -r -a ALLOWED_MAJOR_VERSIONS <<< "${{ steps.allowed-major-versions.outputs.group1 }}"

Check failure on line 103 in .github/workflows/release-checks.yaml

View workflow job for this annotation

GitHub Actions / zizmor-output

zizmor/template-injection

code injection via template expansion: may expand into attacker-controllable code
# get all changed components
COMPONENTS=$(git diff origin/main --name-only | grep VERSION | xargs dirname)
FAIL=""
Expand All @@ -105,7 +127,7 @@

next-release-label-check:
name: Check for "next release" label
uses: GoogleCloudPlatform/php-tools/.github/workflows/release-checks.yml@main

Check failure on line 130 in .github/workflows/release-checks.yaml

View workflow job for this annotation

GitHub Actions / zizmor-output

unpinned-uses

release-checks.yaml:130: unpinned action reference: action is not pinned to a hash (required by blanket policy)
if: github.event.pull_request.user.login == 'release-please[bot]'
with:
next-release-label-check: true
Expand All @@ -116,11 +138,11 @@
runs-on: ubuntu-latest
if: github.event.pull_request.user.login == 'release-please[bot]'
steps:
- uses: actions/checkout@v7

Check failure on line 141 in .github/workflows/release-checks.yaml

View workflow job for this annotation

GitHub Actions / zizmor-output

unpinned-uses

release-checks.yaml:141: unpinned action reference: action is not pinned to a hash (required by blanket policy)
with:
fetch-depth: 0
- name: "Install PHP"
uses: shivammathur/setup-php@v2

Check failure on line 145 in .github/workflows/release-checks.yaml

View workflow job for this annotation

GitHub Actions / zizmor-output

unpinned-uses

release-checks.yaml:145: unpinned action reference: action is not pinned to a hash (required by blanket policy)
with:
php-version: "8.2"
- name: "Install dependencies"
Expand Down
Loading