-
Notifications
You must be signed in to change notification settings - Fork 7
265 lines (243 loc) · 11.9 KB
/
Copy pathci-quality.yml
File metadata and controls
265 lines (243 loc) · 11.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
name: Code Quality
# Fast static-analysis checks: dependency CVEs, translation drift,
# route integrity, Tailwind JIT safety, plugin schema rules.
# No database or browser required — runs in ~60 seconds.
on:
push:
branches: [main]
paths:
- '**.php'
- '**.js'
- 'locale/**'
- 'composer.json'
- 'package.json'
- 'installer/database/schema.sql'
- 'version.json'
- '.github/workflows/ci-quality.yml'
pull_request:
branches: [main]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
quality:
name: Static quality checks
runs-on: ubuntu-latest
# MySQL for the DB-backed unit tests (migration-*, book-field-types,
# loan-edge-cases, session-fixes). Without it those tests can't read a DB
# and would abort — the migration behavioural tests are mandatory per
# updater.md, so CI provides a real database instead of skipping them.
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: pinakes_test
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping"
--health-interval=10s
--health-timeout=5s
--health-retries=5
steps:
- uses: actions/checkout@v6
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
extensions: mysqli, pdo_mysql
coverage: none
github-token: ''
- name: Setup Node
uses: actions/setup-node@v5
with:
node-version: '22'
# ── Dependency CVE scanning ───────────────────────────────────────────
- name: composer audit (known CVEs)
run: |
composer install --no-interaction --prefer-dist --quiet
composer audit --no-dev --abandoned=ignore 2>&1 || { echo "⚠ composer audit: vulnerabilities found (see above)"; exit 1; }
- name: npm audit (known CVEs)
run: |
npm ci --silent 2>/dev/null || npm install --silent
npm audit --audit-level=high 2>&1 || { echo "⚠ npm audit: high/critical vulnerabilities found"; exit 1; }
continue-on-error: true # Warn, don't block — js devDependencies may have unfixed advisories
# ── Translation completeness ──────────────────────────────────────────
- name: Translation key parity (en_US ↔ de_DE, placeholders en/de/it)
run: |
FAILED=0
# 1. Exact key-set parity for non-Italian translations only.
# it_IT is intentionally sparse: __() falls back to the key itself
# (keys are native Italian text), so it_IT.json only needs overrides.
for PAIR in "en_US:de_DE"; do
A=${PAIR%%:*}; B=${PAIR##*:}
MISSING=$(comm -23 <(jq -r 'keys[]' locale/${A}.json | sort) <(jq -r 'keys[]' locale/${B}.json | sort))
EXTRA=$(comm -13 <(jq -r 'keys[]' locale/${A}.json | sort) <(jq -r 'keys[]' locale/${B}.json | sort))
if [ -n "$MISSING" ]; then
echo "✗ ${B}.json manca di chiavi presenti in ${A}.json:"
echo "$MISSING" | sed 's/^/ /'
FAILED=1
fi
if [ -n "$EXTRA" ]; then
echo "✗ ${B}.json ha chiavi extra non presenti in ${A}.json:"
echo "$EXTRA" | sed 's/^/ /'
FAILED=1
fi
done
# 2. Placeholder parity: %s / %d must match count between en_US, de_DE and it_IT
python3 - <<'PYEOF'
import json, re, sys
en = json.load(open('locale/en_US.json'))
de = json.load(open('locale/de_DE.json'))
it = json.load(open('locale/it_IT.json'))
ph = re.compile(r'%(?:\d+\$)?[sd]')
failed = 0
for locale_name, data in [('de_DE', de), ('it_IT', it)]:
for k in en:
if k not in data:
continue
ep = sorted(ph.findall(str(en[k])))
lp = sorted(ph.findall(str(data[k])))
if ep != lp:
print(f'✗ Placeholder mismatch for key "{k[:80]}": en={ep} {locale_name}={lp}')
failed = 1
sys.exit(failed)
PYEOF
[ "$FAILED" -eq 0 ] && echo "✓ Parità chiavi e placeholder confermata" || exit 1
# ── Route key integrity ───────────────────────────────────────────────
- name: Route key integrity (route_path() keys exist in routes_it_IT.json)
run: |
ROUTE_KEYS=$(python3 -c "import json; d=json.load(open('locale/routes_it_IT.json')); [print(k) for k in d.keys()]")
FALLBACK_KEYS=$(php -r "require 'vendor/autoload.php'; echo implode(PHP_EOL, App\\Support\\RouteTranslator::getStaticFallbackKeys());")
ALL_KEYS=$(printf '%s\n%s' "$ROUTE_KEYS" "$FALLBACK_KEYS" | sort -u)
FAILED=0
while IFS= read -r key; do
if ! echo "$ALL_KEYS" | grep -qx "$key"; then
echo " ✗ route_path('$key') usato in views ma mancante in routes_it_IT.json o fallbackRoutes"
FAILED=1
fi
done < <(grep -rohE "route_path\('([^']+)'\)" app/Views/ | grep -oE "'[^']+'" | tr -d "'" | sort -u)
[ "$FAILED" -eq 0 ] && echo "✓ Tutte le chiavi route_path() esistono in routes_it_IT.json o fallbackRoutes" || exit 1
# ── Tailwind JIT safety ───────────────────────────────────────────────
- name: Tailwind JIT — no dynamic class construction
run: |
VIOLATIONS=$(grep -rn \
-e "'bg-'\s*\.\s*\$" \
-e '"bg-"\s*\.\s*\$' \
-e "'text-'\s*\.\s*\$" \
-e '"text-"\s*\.\s*\$' \
-e "'border-'\s*\.\s*\$" \
-e '"border-"\s*\.\s*\$' \
--include="*.php" app/ storage/plugins/ 2>/dev/null || true)
if [ -n "$VIOLATIONS" ]; then
echo "✗ Classi Tailwind costruite dinamicamente (non generate dal JIT):"
echo "$VIOLATIONS"
exit 1
fi
echo "✓ Nessuna classe Tailwind dinamica trovata"
# ── Plugin ensureSchema() rule ────────────────────────────────────────
- name: Plugin ensureSchema() rule (CLAUDE.md absolute rule)
run: |
FAILED=0
for file in storage/plugins/*/*.php; do
[ -f "$file" ] || continue
if grep -q 'CREATE TABLE' "$file"; then
if ! grep -q 'ensureSchema()' "$file"; then
echo " ✗ $(basename $(dirname $file))/$(basename $file): CREATE TABLE senza ensureSchema()"
FAILED=1
elif ! awk '/function onActivate/,/^[[:space:]]*}/' "$file" | grep -q 'ensureSchema'; then
echo " ✗ $(basename $(dirname $file))/$(basename $file): ensureSchema() non in onActivate()"
FAILED=1
elif ! awk '/function onInstall/,/^[[:space:]]*}/' "$file" | grep -q 'ensureSchema'; then
echo " ✗ $(basename $(dirname $file))/$(basename $file): ensureSchema() non in onInstall()"
FAILED=1
else
echo " ✓ $(basename $(dirname $file))/$(basename $file)"
fi
fi
done
[ "$FAILED" -eq 0 ] || exit 1
# ── Soft-delete guard ─────────────────────────────────────────────────
- name: Soft-delete guard (libri queries must include deleted_at IS NULL)
run: |
VIOLATIONS=0
while IFS= read -r file; do
if grep -qiE 'FROM[[:space:]]+`?libri`?[[:space:],)]' "$file" 2>/dev/null; then
if ! grep -qiE 'deleted_at[[:space:]]+IS[[:space:]]+NULL' "$file"; then
echo " ⚠ $file: query FROM libri senza deleted_at IS NULL"
VIOLATIONS=$((VIOLATIONS + 1))
fi
fi
done < <(find app/ -name "*.php" -not -path "*/vendor/*")
if [ "$VIOLATIONS" -gt 0 ]; then
echo "⚠ $VIOLATIONS file potrebbero mancare del soft-delete guard"
echo " Verificare manualmente se le query sono intentenzionali (query admin, conteggi, ecc.)"
else
echo "✓ Soft-delete guard presente in tutti i file con query su libri"
fi
# ── Autoloader safety ─────────────────────────────────────────────────
- name: Autoloader phpstan-free
run: |
if [ -f vendor/composer/autoload_static.php ]; then
COUNT=$(grep -c "phpstan" vendor/composer/autoload_static.php || echo 0)
if [ "$COUNT" -gt 0 ]; then
echo "✗ autoload_static.php contiene $COUNT riferimenti a phpstan (run composer install --no-dev)"
exit 1
fi
echo "✓ autoloader phpstan-free"
else
echo "ℹ vendor/ not installed — skipping"
fi
# ── Migration version guard ───────────────────────────────────────────
- name: Migration version guard (all migrate_*.sql ≤ version.json)
run: |
TARGET=$(php -r "echo json_decode(file_get_contents('version.json'))->version;")
FAILED=0
for f in installer/database/migrations/migrate_*.sql; do
V=$(basename "$f" | sed 's/migrate_//;s/\.sql//')
if ! php -r "exit(version_compare('$V','$TARGET','<=') ? 0 : 1);"; then
echo " ✗ $(basename $f): $V > $TARGET (sarebbe ignorata dall'updater)"
FAILED=1
fi
done
[ "$FAILED" -eq 0 ] && echo "✓ Tutte le migration hanno versione ≤ $TARGET" || exit 1
# DB-backed unit tests (migration-*, book-field-types, loan-edge-cases,
# session-fixes) read credentials from .env and connect over TCP. Provide
# a CI .env pointing at the MySQL service and load the base schema so the
# migration behavioural tests run against a real, current-schema database.
- name: Prepare DB for unit tests (.env + schema)
run: |
cat > .env <<'ENVEOF'
DB_HOST=127.0.0.1
DB_PORT=3306
DB_USER=root
DB_PASS=root
DB_NAME=pinakes_test
ENVEOF
# Wait until MySQL answers, then import the base schema + triggers.
for i in $(seq 1 30); do
mysql -h 127.0.0.1 -u root -proot -e "SELECT 1" >/dev/null 2>&1 && break
sleep 2
done
mysql -h 127.0.0.1 -u root -proot pinakes_test < installer/database/schema.sql
# Copy-occupancy triggers live in a separate file (DELIMITER-based); the
# mysql CLI handles DELIMITER natively. loan-edge-cases relies on them.
mysql -h 127.0.0.1 -u root -proot pinakes_test < installer/database/triggers.sql
- name: PHP unit tests (standalone .unit.php)
run: |
FAILED=0
shopt -s nullglob
for t in tests/*.unit.php; do
echo "── $t"
php "$t" || FAILED=1
done
[ "$FAILED" -eq 0 ] && echo "✓ Tutti gli unit test PHP passati" || { echo "✗ Unit test PHP falliti"; exit 1; }
- name: Shell test — bin/setup-permissions.sh
run: bash tests/setup-permissions.test.sh
- name: Remove CI .env
if: always()
run: rm -f .env