Skip to content

Commit 91ad242

Browse files
Add reusable fabbot.yml github workflow
1 parent 6f0c510 commit 91ad242

File tree

1 file changed

+247
-0
lines changed

1 file changed

+247
-0
lines changed

.github/workflows/fabbot.yml

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
name: Fabbot
2+
3+
on:
4+
workflow_call:
5+
inputs:
6+
package:
7+
required: true
8+
type: string
9+
check_license:
10+
required: false
11+
type: boolean
12+
default: false
13+
14+
env:
15+
GH_TOKEN: ${{ github.token }}
16+
17+
jobs:
18+
check:
19+
name: Checks
20+
runs-on: ubuntu-24.04
21+
steps:
22+
- name: Checkout code
23+
run: |
24+
# Checkout patched files using the REST API and install dependencies concurrently
25+
PR_NUMBER="${{ github.event.pull_request.number }}"
26+
PR_HEAD_SHA="${{ github.event.pull_request.head.sha }}"
27+
REPO_OWNER="${{ github.repository_owner }}"
28+
REPO_NAME="${{ github.event.repository.name }}"
29+
30+
pip install codespell &
31+
composer global require -q friendsofphp/php-cs-fixer seld/jsonlint symfony/yaml &
32+
33+
mkdir a
34+
35+
gh api -H "Accept: application/vnd.github.v3.raw" \
36+
"/repos/$REPO_OWNER/$REPO_NAME/contents/.php-cs-fixer.dist.php?ref=$PR_HEAD_SHA" \
37+
> a/.php-cs-fixer.dist.php || rm a/.php-cs-fixer.dist.php &
38+
39+
gh api --paginate "/repos/$REPO_OWNER/$REPO_NAME/pulls/$PR_NUMBER/files" \
40+
| jq -c '.[] | select(.status != "removed") | {filename, sha}' \
41+
| while read -r FILE_OBJ; do
42+
FILENAME=$(echo "$FILE_OBJ" | jq -r '.filename')
43+
FILE_SHA=$(echo "$FILE_OBJ" | jq -r '.sha')
44+
45+
mkdir -p "a/$(dirname "$FILENAME")"
46+
gh api -H "Accept: application/vnd.github.raw" \
47+
"/repos/$REPO_OWNER/$REPO_NAME/git/blobs/$FILE_SHA" \
48+
> "a/$FILENAME" &
49+
done
50+
51+
wait
52+
53+
- name: Check code style
54+
if: always()
55+
run: |
56+
# Run PHP-CS-Fixer
57+
cp -a a b && cd b
58+
~/.composer/vendor/bin/php-cs-fixer fix --using-cache no --show-progress none
59+
cd ..
60+
61+
if ! diff -qr --no-dereference a/ b/ >/dev/null; then
62+
echo "::error::PHP-CS-Fixer found style issues. Please apply the patch below."
63+
echo -e "\n \n git apply - <<'EOF_PATCH'"
64+
diff -pru2 --no-dereference --color=always a/ b/ || true
65+
echo -e "EOF_PATCH\n \n"
66+
echo "Then commit the changes and push to your PR branch."
67+
exit 1
68+
fi
69+
70+
- name: Check for common typos
71+
if: always()
72+
run: |
73+
# Run codespell
74+
rm -rf b && cp -a a b && cd b
75+
codespell -L invokable --check-filenames -w || true
76+
cd ..
77+
78+
if ! diff -qr --no-dereference a/ b/ >/dev/null; then
79+
echo "::error::PHP-CS-Fixer found typos. Please apply the patch below."
80+
echo -e "\n \n git apply - <<'EOF_PATCH'"
81+
diff -pru2 --no-dereference --color=always a/ b/ || true
82+
echo -e "EOF_PATCH\n \n"
83+
echo "Then commit the changes and push to your PR branch."
84+
exit 1
85+
fi
86+
87+
- name: Check for merge commits
88+
if: always()
89+
run: |
90+
# If a PR contains merge commits, fail the job
91+
gh api -H "Accept: application/vnd.github.v3+json" \
92+
"/repos/${{ github.repository_owner }}/${{ github.event.repository.name }}/pulls/${{ github.event.pull_request.number }}/commits" \
93+
| jq -r '.[].parents | length > 1' | grep true > /dev/null && {
94+
echo "::error::Merge commits are not allowed in pull requests."
95+
echo "Please rebase your branch."
96+
exit 1
97+
} || true
98+
99+
- name: Check test-case methods
100+
if: always()
101+
run: |
102+
# Test method names should not have a return type
103+
rm -rf b && cp -a a b && cd b
104+
find -wholename '**/Tests/**.php' \
105+
| while read -r FILE; do
106+
sed -i -E 's/^( public function test.*): void$/\1/' "$FILE"
107+
done
108+
cd ..
109+
110+
if ! diff -qr --no-dereference a/ b/ >/dev/null; then
111+
echo "::error::Test case methods should not have a return type. Please apply the patch below."
112+
echo -e "\n \n git apply - <<'EOF_PATCH'"
113+
diff -pru2 --no-dereference --color=always a/ b/ || true
114+
echo -e "EOF_PATCH\n \n"
115+
echo "Then commit the changes and push to your PR branch."
116+
exit 1
117+
fi
118+
119+
- name: Check @deprecated annotations
120+
if: always()
121+
run: |
122+
# @deprecated annotations should mention ${{ inputs.package }}
123+
rm -rf b && cp -a a b && cd b
124+
find -name '*.php' \
125+
| while read -r FILE; do
126+
sed -i -E 's/(@deprecated since )([0-9])/\1${{ inputs.package }} \2/' "$FILE"
127+
done
128+
cd ..
129+
130+
if ! diff -qr --no-dereference a/ b/ >/dev/null; then
131+
echo "::error::@deprecated annotations should mention ${{ inputs.package }}. Please apply the patch below."
132+
echo -e "\n \n git apply - <<'EOF_PATCH'"
133+
diff -pru2 --no-dereference --color=always a/ b/ || true
134+
echo -e "EOF_PATCH\n \n"
135+
echo "Then commit the changes and push to your PR branch."
136+
exit 1
137+
fi
138+
139+
- name: Check PR header
140+
if: always()
141+
run: |
142+
# Check if the PR title and body follow the Symfony contribution guidelines
143+
PR_TITLE="${{ github.event.pull_request.title }}"
144+
PR_BODY="${{ github.event.pull_request.body }}"
145+
146+
if [[ "${{ inputs.check_license }}" == "true" ]]; then
147+
if [[ ! "$PR_BODY" =~ \|\ License[\ ]+\|\ MIT ]]; then
148+
echo "::error::You must add the standard contribution header in the PR description"
149+
echo "See https://symfony.com/doc/current/contributing/code/patches.html#make-a-pull-request"
150+
exit 1
151+
fi
152+
fi
153+
154+
if [[ "$PR_TITLE" =~ (feat|fix|docs|style|refactor|perf|test|chore|revert|build|ci|types?|wip)[:()] ]]; then
155+
echo "::error::Don't use conventional commits in PR titles."
156+
echo "We'll add the appropriate prefix while merging."
157+
echo "Use the component name instead, e.g., [Component] Description."
158+
exit 1
159+
fi
160+
161+
- name: Check YAML files
162+
if: always()
163+
run: |
164+
# Check YAML files for syntax errors
165+
rm -rf b && cp -a a b && cd b
166+
find . -name '*.yml' -o -name '*.yaml' \
167+
| while read -r FILE; do php -r '
168+
use Symfony\Component\Yaml\{Parser,Yaml};
169+
require $_SERVER["HOME"]."/.composer/vendor/autoload.php";
170+
try { (new Parser())->parse(file_get_contents($argv[1]), Yaml::PARSE_CUSTOM_TAGS); }
171+
catch (Exception $e) { echo "::error::in $argv[1]:\n{$e->getMessage()}\n"; exit(1); }
172+
' "$FILE"
173+
done
174+
cd ..
175+
176+
- name: Check JSON files
177+
if: always()
178+
run: |
179+
# Check JSON files for syntax errors
180+
rm -rf b && cp -a a b && cd b
181+
find . -name '*.json' \
182+
| while read -r FILE; do php -r '
183+
use Seld\JsonLint\JsonParser;
184+
require $_SERVER["HOME"]."/.composer/vendor/autoload.php";
185+
try { (new JsonParser())->parse(file_get_contents($argv[1])); }
186+
catch (Exception $e) { echo "::error:: in $argv[1]: {$e->getMessage()}\n"; exit(1); }
187+
' "$FILE"
188+
done
189+
cd ..
190+
191+
- name: Check exception messages
192+
if: always()
193+
run: |
194+
# Placeholders should be enclosed in double-quotes and messages should end with a dot
195+
rm -rf b && cp -a a b && cd b
196+
find -name '*.php' \
197+
| while read -r FILE; do php -r "$(cat <<'EOPHP'
198+
$new = preg_replace_callback('{throw new ([^\(]+)\((.+?)\);}', function ($match) {
199+
$contents = $match[2];
200+
201+
// %s::%s() -> "%s::%s()"
202+
$contents = preg_replace('{(?<= )%s\:\:%s(\(\))?}', '"%s::%s()"', $contents);
203+
$contents = preg_replace('{\(\'%s\:\:%s(\(\))?}', '(\'"%s::%s()"', $contents);
204+
205+
// %s() -> "%s()"
206+
$contents = preg_replace('{(?<= )%s(\(\))}', '"%s$1"', $contents);
207+
$contents = preg_replace('{\(\'%s(\(\))}', '(\'"%s$1"', $contents);
208+
209+
// %s -> "%s" after a space
210+
$contents = preg_replace('{(?<= )%s}', '"%s"', $contents);
211+
$contents = preg_replace('{\(\'%s}', '(\'"%s"', $contents);
212+
213+
return sprintf('throw new %s(%s);', $match[1], $contents);
214+
}, $old = file_get_contents($argv[1]));
215+
216+
// ensure there is a dot at the end of the exception message
217+
// except for files under Tests/
218+
if (false === strpos($argv[1], '/Tests/')) {
219+
$new = preg_replace_callback('{throw new ([^\(]+)\((sprintf\()?(\'|")(.+?)(?<!\\\)(\3)}', function ($match) {
220+
if ('UnexpectedTypeException' === $match[1]) {
221+
return $match[0];
222+
}
223+
224+
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]);
225+
}, $new);
226+
}
227+
228+
if ($new !== $old) {
229+
file_put_contents($argv[1], $new);
230+
}
231+
EOPHP
232+
)" "$FILE"
233+
done
234+
cd ..
235+
236+
if ! diff -qr --no-dereference a/ b/ >/dev/null; then
237+
echo "::error::Some exception messages might need a tweak. Please consider the patch below."
238+
echo -e "\n \n git apply - <<'EOF_PATCH'"
239+
diff -pru2 --no-dereference --color=always a/ b/ || true
240+
echo -e "EOF_PATCH\n \n"
241+
echo "Then commit the changes and push to your PR branch."
242+
exit 1
243+
fi
244+
245+
- name: 🧠 Fabbot can generate false-positives. Cherry-pick as fits 🍒. Reviewers will help.
246+
if: always()
247+
run: exit 0

0 commit comments

Comments
 (0)