Skip to content
Closed
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
242 changes: 242 additions & 0 deletions .github/workflows/callable-fabbot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
name: Fabbot

on:
workflow_call:
inputs:
package:
required: true
type: string

env:
GH_TOKEN: ${{ github.token }}

jobs:
check:
name: Checks
runs-on: ubuntu-24.04
steps:
- name: Checkout code
run: |
# Check out patched files using the REST API and install dependencies concurrently
PR_NUMBER="${{ github.event.pull_request.number }}"
PR_HEAD_SHA="${{ github.event.pull_request.head.sha }}"
REPO_OWNER="${{ github.repository_owner }}"
REPO_NAME="${{ github.event.repository.name }}"

pip install codespell &
composer global require -q friendsofphp/php-cs-fixer seld/jsonlint symfony/yaml &

mkdir a

gh api -H "Accept: application/vnd.github.v3.raw" \
"/repos/$REPO_OWNER/$REPO_NAME/contents/.php-cs-fixer.dist.php?ref=$PR_HEAD_SHA" \
> a/.php-cs-fixer.dist.php || rm a/.php-cs-fixer.dist.php &

gh api --paginate "/repos/$REPO_OWNER/$REPO_NAME/pulls/$PR_NUMBER/files" \
| jq -c '.[] | select(.status != "removed") | {filename, sha}' \
| while read -r FILE_OBJ; do
FILENAME=$(echo "$FILE_OBJ" | jq -r '.filename')
FILE_SHA=$(echo "$FILE_OBJ" | jq -r '.sha')

mkdir -p "a/$(dirname "$FILENAME")"
gh api -H "Accept: application/vnd.github.raw" \
"/repos/$REPO_OWNER/$REPO_NAME/git/blobs/$FILE_SHA" \
> "a/$FILENAME" &
done

wait

- name: Check code style
if: always()
run: |
# Run PHP-CS-Fixer
cp -a a b && cd b
~/.composer/vendor/bin/php-cs-fixer fix --using-cache no --show-progress none
cd ..

if ! diff -qr --no-dereference a/ b/ >/dev/null; then
echo "::error::PHP-CS-Fixer found style issues. Please apply the patch below."
echo -e "\n \n git apply - <<'EOF_PATCH'"
diff -pru2 --no-dereference --color=always a/ b/ || true
echo -e "EOF_PATCH\n \n"
echo "Then commit the changes and push to your PR branch."
exit 1
fi

- name: Check for common typos
if: always()
run: |
# Run codespell
rm -rf b && cp -a a b && cd b
codespell -L invokable --check-filenames -w || true
cd ..

if ! diff -qr --no-dereference a/ b/ >/dev/null; then
echo "::error::PHP-CS-Fixer found typos. Please apply the patch below."
echo -e "\n \n git apply - <<'EOF_PATCH'"
diff -pru2 --no-dereference --color=always a/ b/ || true
echo -e "EOF_PATCH\n \n"
echo "Then commit the changes and push to your PR branch."
exit 1
fi

- name: Check for merge commits
if: always()
run: |
# If a PR contains merge commits, fail the job
gh api -H "Accept: application/vnd.github.v3+json" \
"/repos/${{ github.repository_owner }}/${{ github.event.repository.name }}/pulls/${{ github.event.pull_request.number }}/commits" \
| jq -r '.[].parents | length > 1' | grep true > /dev/null && {
echo "::error::Merge commits are not allowed in pull requests."
echo "Please rebase your branch."
exit 1
} || true

- name: Check test-case methods
if: always()
run: |
# Test method names should not have a return type
rm -rf b && cp -a a b && cd b
find -wholename '**/Tests/**.php' \
| while read -r FILE; do
sed -i -E 's/^( public function test.*): void$/\1/' "$FILE"
done
cd ..

if ! diff -qr --no-dereference a/ b/ >/dev/null; then
echo "::error::Test case methods should not have a return type. Please apply the patch below."
echo -e "\n \n git apply - <<'EOF_PATCH'"
diff -pru2 --no-dereference --color=always a/ b/ || true
echo -e "EOF_PATCH\n \n"
echo "Then commit the changes and push to your PR branch."
exit 1
fi


- name: Check @deprecated annotations
if: always()
run: |
# @deprecated annotations should mention ${{ inputs.package }}
rm -rf b && cp -a a b && cd b
find -name '*.php' \
| while read -r FILE; do
sed -i -E 's/(@deprecated since )([0-9])/\1${{ inputs.package }} \2/' "$FILE"
done
cd ..

if ! diff -qr --no-dereference a/ b/ >/dev/null; then
echo "::error::@deprecated annotations should mention ${{ inputs.package }}. Please apply the patch below."
echo -e "\n \n git apply - <<'EOF_PATCH'"
diff -pru2 --no-dereference --color=always a/ b/ || true
echo -e "EOF_PATCH\n \n"
echo "Then commit the changes and push to your PR branch."
exit 1
fi

- name: Check PR header
if: always()
run: |
# Check if the PR title and body follow the Symfony contribution guidelines
PR_TITLE="${{ github.event.pull_request.title }}"
PR_BODY="${{ github.event.pull_request.body }}"

if [[ ! "$PR_BODY" =~ \|\ License[\ ]+\|\ MIT ]]; then
echo "::error::You must add the standard contribution header in the PR description"
echo "See https://symfony.com/doc/current/contributing/code/patches.html#make-a-pull-request"
exit 1
fi

if [[ "$PR_TITLE" =~ (feat|fix|docs|style|refactor|perf|test|chore|revert|build|ci|types?|wip)[:()] ]]; then
echo "::error::Don't use conventional commits in PR titles."
echo "We'll add the appropriate prefix while merging."
echo "Use the component name instead, e.g., [Component] Description."
exit 1
fi

- name: Check YAML files
if: always()
run: |
# Check YAML files for syntax errors
rm -rf b && cp -a a b && cd b
find . -name '*.yml' -o -name '*.yaml' \
| while read -r FILE; do php -r '
use Symfony\Component\Yaml\{Parser,Yaml};
require $_SERVER["HOME"]."/.composer/vendor/autoload.php";
try { (new Parser())->parse(file_get_contents($argv[1]), Yaml::PARSE_CUSTOM_TAGS); }
catch (Exception $e) { echo "::error::in $argv[1]:\n{$e->getMessage()}\n"; exit(1); }
' "$FILE"
done
cd ..

- name: Check JSON files
if: always()
run: |
# Check JSON files for syntax errors
rm -rf b && cp -a a b && cd b
find . -name '*.json' \
| while read -r FILE; do php -r '
use Seld\JsonLint\JsonParser;
require $_SERVER["HOME"]."/.composer/vendor/autoload.php";
try { (new JsonParser())->parse(file_get_contents($argv[1])); }
catch (Exception $e) { echo "::error:: in $argv[1]: {$e->getMessage()}\n"; exit(1); }
' "$FILE"
done
cd ..

- name: Check exception messages
if: always()
run: |
# Placeholders should be enclosed in double-quotes and messages should end with a dot
rm -rf b && cp -a a b && cd b
find -name '*.php' \
| while read -r FILE; do php -r "$(cat <<'EOF'
$new = preg_replace_callback('{throw new ([^\(]+)\((.+?)\);}', function ($match) {
$contents = $match[2];

// %s::%s() -> "%s::%s()"
$contents = preg_replace('{(?<= )%s\:\:%s(\(\))?}', '"%s::%s()"', $contents);
$contents = preg_replace('{\(\'%s\:\:%s(\(\))?}', '(\'"%s::%s()"', $contents);

// %s() -> "%s()"
$contents = preg_replace('{(?<= )%s(\(\))}', '"%s$1"', $contents);
$contents = preg_replace('{\(\'%s(\(\))}', '(\'"%s$1"', $contents);

// %s -> "%s" after a space
$contents = preg_replace('{(?<= )%s}', '"%s"', $contents);
$contents = preg_replace('{\(\'%s}', '(\'"%s"', $contents);

return sprintf('throw new %s(%s);', $match[1], $contents);
}, $old = file_get_contents($argv[1]));

// ensure there is a dot at the end of the exception message
// except for files under Tests/
if (false === strpos($argv[1], '/Tests/')) {
$new = preg_replace_callback('{throw new ([^\(]+)\((sprintf\()?(\'|")(.+?)(?<!\\\)(\3)}', function ($match) {
if ('UnexpectedTypeException' === $match[1]) {
return $match[0];
}

return sprintf('throw new %s(%s%s%s%s%s', $match[1], $match[2], $match[3], $match[4], \in_array($match[4][\strlen($match[4]) - 1], ['.', '!', '?', ' ']) ? '' : '.', $match[5]);
}, $new);
}

if ($new !== $old) {
file_put_contents($argv[1], $new);
}
EOF
)" "$FILE"
done
cd ..

if ! diff -qr --no-dereference a/ b/ >/dev/null; then
echo "::error::Some exception messages might need a tweak. Please consider the patch below."
echo -e "\n \n git apply - <<'EOF_PATCH'"
diff -pru2 --no-dereference --color=always a/ b/ || true
echo -e "EOF_PATCH\n \n"
echo "Then commit the changes and push to your PR branch."
exit 1
fi

- name: 🧠 Fabbot can generate false-positives. Cherry-pick as fits 🍒. Reviewers will help.
if: always()
run: exit 0
14 changes: 14 additions & 0 deletions .github/workflows/fabbot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: Fabbot

on:
pull_request:

permissions:
contents: read

jobs:
call-fabbot:
name: CS
uses: ./.github/workflows/callable-fabbot.yml
with:
package: Symfony
Loading
Loading