Skip to content

Commit d423ddb

Browse files
authored
Merge pull request #328 from codesnippetspro/feat/snippets-quicknav
[CWA-269] feat: Native Admin Bar QuickNav (Snippets) by DECKERWEB
2 parents 6b16867 + 75fe453 commit d423ddb

40 files changed

Lines changed: 3398 additions & 399 deletions

.github/workflows/phpunit-test.yml

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ jobs:
1313
phpunit-test:
1414
name: PHPUnit tests (PHP ${{ inputs.php-version }})
1515
runs-on: ubuntu-22.04
16-
16+
env:
17+
WP_CORE_DIR: /tmp/wordpress
18+
WP_TESTS_DIR: /tmp/wordpress-tests-lib
19+
1720
services:
1821
mysql:
1922
image: mysql:8.0
@@ -59,9 +62,7 @@ jobs:
5962
6063
- name: Install Composer dependencies
6164
if: steps.composer-cache.outputs.cache-hit != 'true'
62-
run: |
63-
cd src
64-
composer install --no-progress --prefer-dist --optimize-autoloader
65+
run: composer install -d src --no-progress --prefer-dist --optimize-autoloader
6566

6667
- name: Save Composer cache
6768
if: steps.composer-cache.outputs.cache-hit != 'true'
@@ -70,21 +71,52 @@ jobs:
7071
path: src/vendor
7172
key: ${{ runner.os }}-php-${{ inputs.php-version }}-composer-${{ steps.deps-hash.outputs.deps_hash }}
7273

74+
- name: Resolve WordPress version metadata
75+
id: wp-meta
76+
run: |
77+
set -euo pipefail
78+
wp_version=$(curl -s http://api.wordpress.org/core/version-check/1.7/ | grep -o '"version":"[^"]*' | head -1 | sed 's/"version":"//')
79+
if [ -z "$wp_version" ]; then
80+
wp_version="latest-unknown"
81+
fi
82+
echo "wp_version=$wp_version" >> "$GITHUB_OUTPUT"
83+
84+
- name: Get WordPress test suite cache
85+
id: wp-tests-cache
86+
uses: actions/cache/restore@v4
87+
with:
88+
path: |
89+
${{ env.WP_CORE_DIR }}
90+
${{ env.WP_TESTS_DIR }}
91+
key: ${{ runner.os }}-php-${{ inputs.php-version }}-wp-tests-${{ hashFiles('tests/install-wp-tests.sh') }}-wp-${{ steps.wp-meta.outputs.wp_version }}
92+
7393
- name: Install WordPress test suite
7494
run: |
7595
bash tests/install-wp-tests.sh wordpress_test root root 127.0.0.1:3306 latest true
7696
97+
- name: Save WordPress test suite cache
98+
if: steps.wp-tests-cache.outputs.cache-hit != 'true'
99+
uses: actions/cache/save@v4
100+
with:
101+
path: |
102+
${{ env.WP_CORE_DIR }}
103+
${{ env.WP_TESTS_DIR }}
104+
key: ${{ steps.wp-tests-cache.outputs.cache-primary-key }}
105+
77106
- name: Run PHPUnit tests
78107
run: |
108+
set -euo pipefail
109+
mkdir -p test-results/phpunit
79110
cd src
80-
vendor/bin/phpunit -c ../phpunit.xml --testdox
111+
vendor/bin/phpunit -c ../phpunit.xml --testdox --log-junit ../test-results/phpunit/phpunit-${{ inputs.php-version }}.xml 2>&1 | tee ../test-results/phpunit/phpunit-${{ inputs.php-version }}.log
81112
82113
- uses: actions/upload-artifact@v4
83114
if: always()
84115
with:
85116
name: phpunit-test-results-php-${{ inputs.php-version }}
86117
path: |
87-
.phpunit.result.cache
118+
src/.phpunit.result.cache
119+
test-results/phpunit/
88120
if-no-files-found: ignore
89121
retention-days: 2
90122

.github/workflows/phpunit.yml

Lines changed: 233 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -24,64 +24,250 @@ concurrency:
2424
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
2525

2626
jobs:
27-
phpunit-php-7-4:
27+
phpunit:
2828
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-tests')
29+
strategy:
30+
fail-fast: false
31+
matrix:
32+
php-version: ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4']
2933
uses: ./.github/workflows/phpunit-test.yml
3034
with:
31-
php-version: '7.4'
32-
33-
phpunit-php-8-0:
34-
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-tests')
35-
uses: ./.github/workflows/phpunit-test.yml
36-
with:
37-
php-version: '8.0'
38-
39-
phpunit-php-8-1:
40-
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-tests')
41-
uses: ./.github/workflows/phpunit-test.yml
42-
with:
43-
php-version: '8.1'
44-
45-
phpunit-php-8-2:
46-
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-tests')
47-
uses: ./.github/workflows/phpunit-test.yml
48-
with:
49-
php-version: '8.2'
50-
51-
phpunit-php-8-3:
52-
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-tests')
53-
uses: ./.github/workflows/phpunit-test.yml
54-
with:
55-
php-version: '8.3'
56-
57-
phpunit-php-8-4:
58-
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-tests')
59-
uses: ./.github/workflows/phpunit-test.yml
60-
with:
61-
php-version: '8.4'
35+
php-version: ${{ matrix.php-version }}
6236

6337
test-result:
64-
needs: [phpunit-php-7-4, phpunit-php-8-0, phpunit-php-8-1, phpunit-php-8-2, phpunit-php-8-3, phpunit-php-8-4]
65-
if: always() && (needs.phpunit-php-7-4.result != 'skipped' || needs.phpunit-php-8-0.result != 'skipped' || needs.phpunit-php-8-1.result != 'skipped' || needs.phpunit-php-8-2.result != 'skipped' || needs.phpunit-php-8-3.result != 'skipped' || needs.phpunit-php-8-4.result != 'skipped')
38+
needs: [phpunit]
39+
if: always() && needs.phpunit.result != 'skipped'
6640
runs-on: ubuntu-22.04
6741
name: PHPUnit - Test Results Summary
42+
permissions:
43+
pull-requests: write
44+
issues: write
6845
steps:
6946
- name: Test status summary
7047
run: |
71-
echo "PHP 7.4: ${{ needs.phpunit-php-7-4.result }}"
72-
echo "PHP 8.0: ${{ needs.phpunit-php-8-0.result }}"
73-
echo "PHP 8.1: ${{ needs.phpunit-php-8-1.result }}"
74-
echo "PHP 8.2: ${{ needs.phpunit-php-8-2.result }}"
75-
echo "PHP 8.3: ${{ needs.phpunit-php-8-3.result }}"
76-
echo "PHP 8.4: ${{ needs.phpunit-php-8-4.result }}"
48+
echo "PHPUnit matrix result: ${{ needs.phpunit.result }}"
49+
50+
- name: Delete previous PR failure comment on success
51+
if: github.event_name == 'pull_request' && needs.phpunit.result == 'success'
52+
uses: actions/github-script@v7
53+
with:
54+
script: |
55+
const marker = '<!-- phpunit-result-comment -->';
56+
57+
const { data: comments } = await github.rest.issues.listComments({
58+
owner: context.repo.owner,
59+
repo: context.repo.repo,
60+
issue_number: context.issue.number,
61+
per_page: 100,
62+
});
63+
64+
const existing = comments.filter(c => typeof c.body === 'string' && c.body.startsWith(marker));
65+
66+
for (const comment of existing) {
67+
await github.rest.issues.deleteComment({
68+
owner: context.repo.owner,
69+
repo: context.repo.repo,
70+
comment_id: comment.id,
71+
});
72+
}
73+
74+
- name: Download PHPUnit artifacts
75+
if: github.event_name == 'pull_request' && needs.phpunit.result == 'failure'
76+
uses: actions/download-artifact@v5
77+
with:
78+
pattern: phpunit-test-results-php-*
79+
path: phpunit-artifacts
80+
merge-multiple: true
81+
82+
- name: Build distinct error summary
83+
if: github.event_name == 'pull_request' && needs.phpunit.result == 'failure'
84+
run: |
85+
set -euo pipefail
86+
python3 - <<'PY'
87+
import glob
88+
import os
89+
import re
90+
import xml.etree.ElementTree as ET
91+
from collections import defaultdict
92+
93+
artifacts_dir = 'phpunit-artifacts'
94+
95+
def version_from_path(path: str) -> str:
96+
base = os.path.basename(path)
97+
m = re.search(r'phpunit-([0-9]+\.[0-9]+)\.xml$', base)
98+
if m:
99+
return m.group(1)
100+
m = re.search(r'phpunit-([0-9]+\.[0-9]+)\.log$', base)
101+
if m:
102+
return m.group(1)
103+
return 'unknown'
104+
105+
def add_entry(grouped, key, version, message):
106+
if not message.strip():
107+
return
108+
grouped[key]['versions'].add(version)
109+
# Keep the first representative message we see for this key.
110+
if not grouped[key]['message']:
111+
grouped[key]['message'] = message.strip()
112+
113+
grouped = defaultdict(lambda: {'versions': set(), 'message': ''})
114+
versions_seen = set()
115+
116+
# Prefer JUnit XML when present.
117+
for xml_path in sorted(glob.glob(os.path.join(artifacts_dir, '**', '*.xml'), recursive=True)):
118+
version = version_from_path(xml_path)
119+
versions_seen.add(version)
120+
try:
121+
root = ET.parse(xml_path).getroot()
122+
except Exception:
123+
continue
124+
125+
for testcase in root.iter('testcase'):
126+
for tag in ('error', 'failure'):
127+
for node in testcase.findall(tag):
128+
etype = (node.attrib.get('type') or tag).strip()
129+
msg = (node.attrib.get('message') or '').strip()
130+
details = (node.text or '').strip()
131+
combined = f"{etype}: {msg}".strip(': ')
132+
if details:
133+
combined = combined + "\n" + details
134+
135+
testcase_id = (
136+
(testcase.attrib.get('classname') or '').strip() +
137+
'::' +
138+
(testcase.attrib.get('name') or '').strip()
139+
).strip(':')
140+
141+
extracted = ''
142+
if not msg and details:
143+
for line in details.splitlines():
144+
line = line.strip()
145+
if line.startswith('CI demo:'):
146+
extracted = line
147+
break
148+
if not extracted:
149+
extracted = details.splitlines()[0].strip()
150+
151+
# Dedupe key: prefer explicit message; otherwise use extracted details + testcase id.
152+
key_parts = [etype]
153+
if msg:
154+
key_parts.append(msg)
155+
elif extracted:
156+
key_parts.append(extracted)
157+
if testcase_id:
158+
key_parts.append(testcase_id)
159+
key = "\n".join([p for p in key_parts if p]).strip() or (combined.splitlines()[0] if combined else 'unknown')
160+
add_entry(grouped, key, version, combined)
161+
162+
# Fallback: scan logs for fatals if XML missing.
163+
fatal_re = re.compile(r'^(PHP\s+Fatal\s+error:.*|Fatal\s+error:.*)$', re.MULTILINE)
164+
for log_path in sorted(glob.glob(os.path.join(artifacts_dir, '**', '*.log'), recursive=True)):
165+
version = version_from_path(log_path)
166+
versions_seen.add(version)
167+
try:
168+
log = open(log_path, 'r', encoding='utf-8', errors='replace').read()
169+
except Exception:
170+
continue
171+
m = fatal_re.search(log)
172+
if m:
173+
msg = m.group(1).strip()
174+
key = 'Fatal error\n' + msg
175+
add_entry(grouped, key, version, msg)
176+
177+
versions = sorted(v for v in versions_seen if v != 'unknown')
178+
179+
def versions_label(affected):
180+
affected = sorted(v for v in affected if v != 'unknown')
181+
if versions and affected == versions:
182+
return 'all'
183+
return ', '.join(affected) if affected else 'unknown'
184+
185+
items = sorted(grouped.items(), key=lambda kv: (-len(kv[1]['versions']), kv[0]))
186+
blocks = []
187+
for idx, (key, info) in enumerate(items):
188+
affected = versions_label(info['versions'])
189+
message = info['message']
190+
block = (
191+
"-----\n"
192+
f"Affected PHP version: `{affected}`\n"
193+
"```php\n"
194+
f"{message}\n"
195+
"```"
196+
)
197+
if idx == len(items) - 1:
198+
block += "\n-----"
199+
blocks.append(block)
200+
201+
if blocks:
202+
details = "\n\n".join(blocks)
203+
else:
204+
details = "No PHPUnit error details could be parsed from artifacts."
205+
206+
md = "\n".join([
207+
"<details>",
208+
"<summary>See all PHPUnit errors (click to expand)</summary>",
209+
"",
210+
details,
211+
"",
212+
"</details>",
213+
"",
214+
])
215+
216+
with open('phpunit-errors.md', 'w', encoding='utf-8') as f:
217+
f.write(md)
218+
PY
219+
220+
- name: Post PR comment on failure
221+
if: github.event_name == 'pull_request' && needs.phpunit.result == 'failure'
222+
uses: actions/github-script@v7
223+
with:
224+
script: |
225+
const marker = '<!-- phpunit-result-comment -->';
226+
227+
const fs = require('fs');
228+
let details = '';
229+
try {
230+
details = fs.readFileSync('phpunit-errors.md', 'utf8').trim();
231+
} catch (e) {
232+
details = '';
233+
}
234+
235+
const body = [
236+
marker,
237+
'## PHPUnit Test Failure',
238+
'',
239+
`One or more PHP version targets failed in [this workflow run](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}).`,
240+
'',
241+
details || '_No parsed error details found._',
242+
'',
243+
'Please review the failing jobs and fix the issues before merging.',
244+
].join('\n');
245+
246+
const { data: comments } = await github.rest.issues.listComments({
247+
owner: context.repo.owner,
248+
repo: context.repo.repo,
249+
issue_number: context.issue.number,
250+
per_page: 100,
251+
});
252+
253+
const existing = comments.filter(c => typeof c.body === 'string' && c.body.startsWith(marker));
254+
255+
for (const comment of existing) {
256+
await github.rest.issues.deleteComment({
257+
owner: context.repo.owner,
258+
repo: context.repo.repo,
259+
comment_id: comment.id,
260+
});
261+
}
262+
263+
await github.rest.issues.createComment({
264+
owner: context.repo.owner,
265+
repo: context.repo.repo,
266+
issue_number: context.issue.number,
267+
body,
268+
});
77269
78270
- name: Check overall status
79-
if: |
80-
(needs.phpunit-php-7-4.result != 'success' && needs.phpunit-php-7-4.result != 'skipped') ||
81-
(needs.phpunit-php-8-0.result != 'success' && needs.phpunit-php-8-0.result != 'skipped') ||
82-
(needs.phpunit-php-8-1.result != 'success' && needs.phpunit-php-8-1.result != 'skipped') ||
83-
(needs.phpunit-php-8-2.result != 'success' && needs.phpunit-php-8-2.result != 'skipped') ||
84-
(needs.phpunit-php-8-3.result != 'success' && needs.phpunit-php-8-3.result != 'skipped') ||
85-
(needs.phpunit-php-8-4.result != 'success' && needs.phpunit-php-8-4.result != 'skipped')
271+
if: needs.phpunit.result != 'success'
86272
run: exit 1
87273

0 commit comments

Comments
 (0)