@mokoconsulting/mcp-mokosuite 1.1.0-dev.372 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,693 +0,0 @@
1
- # Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
2
- #
3
- # SPDX-License-Identifier: GPL-3.0-or-later
4
- #
5
- # FILE INFORMATION
6
- # DEFGROUP: MokoGIT.Workflow
7
- # INGROUP: MokoCLI.CI
8
- # REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
9
- # PATH: /.mokogit/workflows/pr-check.yml
10
- # VERSION: 09.24.00
11
- # BRIEF: PR gate — branch policy + code validation before merge
12
-
13
- name: "Universal: PR Check"
14
-
15
- on:
16
- pull_request:
17
- types: [opened, synchronize, reopened, edited]
18
-
19
- permissions:
20
- contents: read
21
- pull-requests: write
22
-
23
- env:
24
- FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
25
-
26
- concurrency:
27
- # storm-reduction: collapse superseded validation runs on the same ref
28
- group: ${{ github.workflow }}-${{ github.ref }}
29
- cancel-in-progress: true
30
-
31
- jobs:
32
- # ── Branch Policy ──────────────────────────────────────────────────────
33
- branch-policy:
34
- name: Branch Policy
35
- runs-on: ubuntu-latest
36
- steps:
37
- - name: Check branch merge target
38
- run: |
39
- HEAD="${{ github.head_ref }}"
40
- BASE="${{ github.base_ref }}"
41
-
42
- echo "PR: ${HEAD} → ${BASE}"
43
-
44
- ALLOWED=true
45
- REASON=""
46
-
47
- case "$HEAD" in
48
- feature/*|feat/*)
49
- if [ "$BASE" != "dev" ]; then
50
- ALLOWED=false
51
- REASON="Feature branches must target 'dev', not '${BASE}'"
52
- fi
53
- ;;
54
- fix/*|bugfix/*)
55
- if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
56
- ALLOWED=false
57
- REASON="Fix branches must target 'dev' or 'main', not '${BASE}'"
58
- fi
59
- ;;
60
- patch/*)
61
- if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ] && [ "$BASE" != "main" ]; then
62
- ALLOWED=false
63
- REASON="Patch branches must target 'dev', 'rc', or 'main', not '${BASE}'"
64
- fi
65
- ;;
66
- hotfix/*)
67
- if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
68
- ALLOWED=false
69
- REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'"
70
- fi
71
- ;;
72
- rc)
73
- if [ "$BASE" != "main" ]; then
74
- ALLOWED=false
75
- REASON="RC branch can only merge into 'main', not '${BASE}'"
76
- fi
77
- ;;
78
- dev)
79
- if [ "$BASE" != "main" ]; then
80
- ALLOWED=false
81
- REASON="Dev branch can only merge into 'main', not '${BASE}'"
82
- fi
83
- ;;
84
- esac
85
-
86
- if [ "$ALLOWED" = false ]; then
87
- echo "::error::${REASON}"
88
- echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY
89
- echo "" >> $GITHUB_STEP_SUMMARY
90
- echo "${REASON}" >> $GITHUB_STEP_SUMMARY
91
- echo "" >> $GITHUB_STEP_SUMMARY
92
- echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
93
- echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
94
- echo "- \`fix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
95
- echo "- \`patch/*\` → \`dev\`, \`rc\`, or \`main\`" >> $GITHUB_STEP_SUMMARY
96
- echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
97
- echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
98
- echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
99
- exit 1
100
- fi
101
-
102
- echo "Branch policy: OK (${HEAD} → ${BASE})"
103
- echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
104
-
105
- # ── Docs Update Gate (main PRs) ─────────────────────────────────────────
106
- require-docs:
107
- name: Require Docs Update
108
- runs-on: ubuntu-latest
109
- # Enforce only on PRs merging into main: README.md + CHANGELOG.md must both be updated.
110
- if: ${{ github.base_ref == 'main' }}
111
- steps:
112
- - name: Checkout
113
- uses: actions/checkout@v4
114
- with:
115
- fetch-depth: 0
116
-
117
- - name: Require README.md and CHANGELOG.md in the PR diff
118
- run: |
119
- BASE="${{ github.event.pull_request.base.sha }}"
120
- HEAD="${{ github.event.pull_request.head.sha }}"
121
- CHANGED="$(git diff --name-only "$BASE" "$HEAD" 2>/dev/null || true)"
122
- if [ -z "$CHANGED" ]; then
123
- git fetch -q origin "${{ github.base_ref }}" 2>/dev/null || true
124
- CHANGED="$(git diff --name-only "origin/${{ github.base_ref }}...HEAD" 2>/dev/null || true)"
125
- fi
126
- echo "Changed files in PR:"
127
- echo "$CHANGED"
128
- MISSING=""
129
- echo "$CHANGED" | grep -qxE 'README\.md' || MISSING="README.md"
130
- echo "$CHANGED" | grep -qxE 'CHANGELOG\.md' || MISSING="${MISSING:+$MISSING, }CHANGELOG.md"
131
- if [ -n "$MISSING" ]; then
132
- echo "::error::PRs into main must update: ${MISSING}"
133
- {
134
- echo "## Docs Update Required"
135
- echo ""
136
- echo "PRs merging into \`main\` must update both **README.md** and **CHANGELOG.md**."
137
- echo ""
138
- echo "Not updated in this PR: **${MISSING}**"
139
- } >> "$GITHUB_STEP_SUMMARY"
140
- exit 1
141
- fi
142
- echo "Docs update present (README.md + CHANGELOG.md)"
143
- echo "## Docs Update: Passed" >> "$GITHUB_STEP_SUMMARY"
144
-
145
- # ── Wiki Update Reminder (main PRs, non-blocking) ───────────────────────
146
- wiki-reminder:
147
- name: Wiki Update Reminder
148
- runs-on: ubuntu-latest
149
- if: ${{ github.base_ref == 'main' }}
150
- steps:
151
- - name: Remind to update the wiki
152
- env:
153
- TOKEN: ${{ secrets.MOKOGIT_TOKEN || github.token }}
154
- SERVER: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
155
- REPO: ${{ github.repository }}
156
- PR: ${{ github.event.pull_request.number }}
157
- run: |
158
- set -uo pipefail
159
- {
160
- echo "## Wiki Update Reminder"
161
- echo ""
162
- echo "Docs are **wiki-first** at MokoConsulting. If this change affects behavior, usage, configuration, or standards, update the repo wiki:"
163
- echo ""
164
- echo "- ${SERVER}/${REPO}/wiki"
165
- echo ""
166
- echo "_Non-blocking reminder._"
167
- } >> "$GITHUB_STEP_SUMMARY"
168
- # Sticky PR comment: create-or-UPDATE a single comment keyed by a hidden
169
- # marker (no duplicate reminders across pushes). Best-effort, never fails.
170
- LIST="${SERVER}/api/v1/repos/${REPO}/issues/${PR}/comments"
171
- EDIT="${SERVER}/api/v1/repos/${REPO}/issues/comments"
172
- MARKER="<!-- wiki-reminder -->"
173
- BODY="$(printf '%s\n\n%s' "${MARKER}" "**Wiki reminder:** docs are wiki-first -- if this PR changes behavior, usage, config, or standards, please update the repo wiki before/after merge. _(non-blocking)_")"
174
- if [ -n "${TOKEN:-}" ] && [ -n "${PR:-}" ]; then
175
- CID="$(curl -sf -H "Authorization: token ${TOKEN}" "${LIST}?limit=100" 2>/dev/null \
176
- | python3 -c "import sys,json; c=[x for x in json.load(sys.stdin) if '${MARKER}' in (x.get('body') or '')]; print(c[-1]['id'] if c else '')" 2>/dev/null || true)"
177
- PAYLOAD="$(python3 -c "import json,sys; print(json.dumps({'body': sys.stdin.read()}))" <<<"${BODY}")"
178
- if [ -n "${CID}" ]; then
179
- curl -sf -X PATCH -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" -d "${PAYLOAD}" "${EDIT}/${CID}" >/dev/null 2>&1 || true
180
- else
181
- curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" -d "${PAYLOAD}" "${LIST}" >/dev/null 2>&1 || true
182
- fi
183
- fi
184
- echo "Wiki reminder emitted (non-blocking, sticky)."
185
-
186
- validate:
187
- name: Validate PR
188
- runs-on: ubuntu-latest
189
- # Skip on template repos (Template-*) — no real manifest/source/changelog to validate.
190
- if: ${{ !startsWith(github.event.repository.name, 'Template-') }}
191
-
192
- steps:
193
- - name: Checkout
194
- uses: actions/checkout@v4
195
-
196
- - name: Check for merge conflict markers
197
- run: |
198
- CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --exclude-dir='.git' --exclude-dir='.mokogit' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
199
- if [ -n "$CONFLICTS" ]; then
200
- echo "::error::Merge conflict markers found in source files"
201
- echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY
202
- echo '```' >> $GITHUB_STEP_SUMMARY
203
- echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
204
- echo '```' >> $GITHUB_STEP_SUMMARY
205
- exit 1
206
- fi
207
- echo "No conflict markers found"
208
-
209
- - name: Reject tracked updates.xml
210
- run: |
211
- # updates.xml is generated and served by the MokoGIT update server
212
- # (derived from the published stream releases). It must never be
213
- # committed to the repo. Fail the PR if a tracked copy appears.
214
- HITS=$(git ls-files | grep -iE '(^|/)updates\.xml$' || true)
215
- if [ -n "$HITS" ]; then
216
- echo "::error::updates.xml must not be committed — it is generated by the MokoGIT update server."
217
- printf '%s\n' "$HITS" | while IFS= read -r f; do
218
- [ -n "$f" ] && echo "::error file=${f}::Remove ${f} (served by the update server, not stored in-repo)"
219
- done
220
- {
221
- echo "## Blocked: tracked updates.xml"
222
- echo "The MokoGIT update server generates this file. Remove it from the repo:"
223
- echo '```'
224
- printf '%s\n' "$HITS"
225
- echo '```'
226
- } >> "$GITHUB_STEP_SUMMARY"
227
- exit 1
228
- fi
229
- echo "No tracked updates.xml — OK"
230
-
231
- - name: Detect platform
232
- id: platform
233
- run: |
234
- # Platform comes from the MokoGIT metadata API (public GET).
235
- API="${GITHUB_SERVER_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${GITHUB_REPOSITORY}/metadata"
236
- PLATFORM="$(curl -sf "$API" 2>/dev/null | python3 -c "import sys, json; print(json.load(sys.stdin).get('platform') or '')" 2>/dev/null || true)"
237
- [ -z "$PLATFORM" ] && PLATFORM="generic"
238
- echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
239
- echo "Detected platform: $PLATFORM"
240
-
241
- - name: Setup PHP
242
- if: steps.platform.outputs.platform == 'joomla'
243
- run: |
244
- if ! command -v php &> /dev/null; then
245
- sudo apt-get update -qq
246
- sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
247
- fi
248
-
249
- - name: PHP syntax check
250
- if: steps.platform.outputs.platform == 'joomla'
251
- run: |
252
- ERRORS=0
253
- while IFS= read -r -d '' file; do
254
- if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
255
- ERRORS=$((ERRORS + 1))
256
- fi
257
- done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0)
258
- echo "PHP lint: ${ERRORS} error(s)"
259
- [ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
260
-
261
- - name: Joomla JEXEC guard check
262
- if: steps.platform.outputs.platform == 'joomla'
263
- run: |
264
- ERRORS=0
265
- while IFS= read -r -d '' file; do
266
- # Skip vendor, node_modules, and index.html stub files
267
- case "$file" in ./vendor/*|./node_modules/*) continue ;; esac
268
- # Scan the whole file for the JEXEC/JPATH guard: it is placed after
269
- # the SPDX/file-header docblock, which commonly runs past 20 lines.
270
- if ! grep -qE "defined\s*\(\s*['\"](_JEXEC|JPATH_BASE|\\\\JPATH_PLATFORM)['\"]" "$file"; then
271
- echo "::error file=${file}::Missing JEXEC guard: ${file}"
272
- ERRORS=$((ERRORS + 1))
273
- fi
274
- done < <(find . -name "*.php" \( -path "*/src/*" -o -path "*/source/*" \) -not -path "./.git/*" -not -path "./vendor/*" -print0)
275
- if [ "$ERRORS" -gt 0 ]; then
276
- echo "::error::${ERRORS} PHP file(s) missing defined('_JEXEC') or die guard"
277
- echo "## JEXEC Guard Check: Failed" >> $GITHUB_STEP_SUMMARY
278
- echo "${ERRORS} file(s) in src/ or source/ are missing the Joomla execution guard." >> $GITHUB_STEP_SUMMARY
279
- exit 1
280
- fi
281
- echo "JEXEC guard: OK"
282
-
283
- - name: Joomla directory listing protection
284
- if: steps.platform.outputs.platform == 'joomla'
285
- run: |
286
- MISSING=0
287
- SOURCE_DIR="src"; [ -d source ] && SOURCE_DIR="source"
288
- [ ! -d "$SOURCE_DIR" ] && exit 0
289
- while IFS= read -r dir; do
290
- if [ ! -f "${dir}/index.html" ]; then
291
- echo "::warning::Missing index.html in ${dir} (directory listing protection)"
292
- MISSING=$((MISSING + 1))
293
- fi
294
- done < <(find "$SOURCE_DIR" -type d -not -path "./.git/*" -not -path "*/vendor/*" -not -path "*/node_modules/*")
295
- if [ "$MISSING" -gt 0 ]; then
296
- echo "## Directory Protection" >> $GITHUB_STEP_SUMMARY
297
- echo "${MISSING} director(ies) missing index.html" >> $GITHUB_STEP_SUMMARY
298
- fi
299
- echo "Directory protection: ${MISSING} missing (advisory)"
300
-
301
- - name: Joomla script file and asset checks
302
- if: steps.platform.outputs.platform == 'joomla'
303
- run: |
304
- ERRORS=0
305
- MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
306
- [ -z "$MANIFEST" ] && exit 0
307
- MANIFEST_DIR=$(dirname "$MANIFEST")
308
-
309
- # Check scriptfile exists if declared
310
- SCRIPTFILE=$(sed -n 's/.*<scriptfile>\([^<]*\)<\/scriptfile>.*/\1/p' "$MANIFEST" 2>/dev/null)
311
- if [ -n "$SCRIPTFILE" ]; then
312
- if [ ! -f "${MANIFEST_DIR}/${SCRIPTFILE}" ]; then
313
- echo "::error::Manifest declares <scriptfile>${SCRIPTFILE}</scriptfile> but file not found at ${MANIFEST_DIR}/${SCRIPTFILE}"
314
- ERRORS=$((ERRORS + 1))
315
- else
316
- echo "Script file: ${MANIFEST_DIR}/${SCRIPTFILE} (OK)"
317
- fi
318
- fi
319
-
320
- # Require joomla.asset.json and validate it
321
- ASSET_JSON=$(find "$MANIFEST_DIR" -name "joomla.asset.json" -not -path "./.git/*" 2>/dev/null | head -1)
322
- if [ -z "$ASSET_JSON" ]; then
323
- echo "::error::joomla.asset.json not found — Joomla asset system is required"
324
- ERRORS=$((ERRORS + 1))
325
- else
326
- if command -v php &> /dev/null; then
327
- php -r "json_decode(file_get_contents('$ASSET_JSON')); if(json_last_error()!==JSON_ERROR_NONE){echo json_last_error_msg();exit(1);}" 2>&1 || {
328
- echo "::error::joomla.asset.json is not valid JSON"
329
- ERRORS=$((ERRORS + 1))
330
- }
331
- fi
332
- echo "joomla.asset.json: valid"
333
- fi
334
-
335
- # Validate all XML files in src/ are well-formed
336
- XML_ERRORS=0
337
- if command -v php &> /dev/null; then
338
- while IFS= read -r -d '' xmlfile; do
339
- if ! php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$xmlfile'); if(!\$x){foreach(libxml_get_errors() as \$e) echo trim(\$e->message) . ' in $xmlfile'; exit(1);}" 2>&1; then
340
- XML_ERRORS=$((XML_ERRORS + 1))
341
- fi
342
- done < <(find "$MANIFEST_DIR" -name "*.xml" -not -path "./.git/*" -print0)
343
- fi
344
- if [ "$XML_ERRORS" -gt 0 ]; then
345
- echo "::error::${XML_ERRORS} XML file(s) are malformed"
346
- ERRORS=$((ERRORS + 1))
347
- else
348
- echo "XML well-formedness: OK"
349
- fi
350
-
351
- [ "$ERRORS" -gt 0 ] && exit 1
352
- echo "Joomla asset checks: OK"
353
-
354
- - name: Validate platform manifest
355
- run: |
356
- PLATFORM="${{ steps.platform.outputs.platform }}"
357
- case "$PLATFORM" in
358
- joomla)
359
- MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
360
- if [ -z "$MANIFEST" ]; then
361
- echo "::warning::No Joomla manifest found (MokoSuite site)"
362
- exit 0
363
- fi
364
- echo "Manifest: ${MANIFEST}"
365
- if command -v php &> /dev/null; then
366
- php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::Manifest XML is malformed"; exit 1; }
367
- fi
368
- for ELEMENT in name version description; do
369
- grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; }
370
- done
371
- # Block legacy raw/branch update server URLs on MokoGIT
372
- RAW_URLS=$(grep -n 'raw/branch' "$MANIFEST" | grep -i 'mokoconsulting\|mokogit\|git\.mokoconsulting\.tech' || true)
373
- if [ -n "$RAW_URLS" ]; then
374
- echo "::error::Manifest contains legacy raw/branch update server URL on MokoGIT. Use the MokoGIT Pages URL instead (e.g. /{REPO}/updates.xml not /{REPO}/raw/branch/main/updates.xml)"
375
- echo "$RAW_URLS"
376
- exit 1
377
- fi
378
- echo "Joomla manifest valid"
379
- ;;
380
- *)
381
- echo "Generic platform — no manifest validation"
382
- ;;
383
- esac
384
-
385
- - name: Validate Joomla language files
386
- if: steps.platform.outputs.platform == 'joomla'
387
- run: |
388
- ERRORS=0
389
- WARNINGS=0
390
-
391
- # en-GB / en-US locale parity. This is NON-FATAL: the release packager
392
- # (mokocli release_package.php) AUTO-CREATES any missing sibling locale
393
- # file/dir from the "familiar" locale at build time, so a mismatch here
394
- # is a heads-up, not a blocker. Genuine .ini syntax errors below stay fatal.
395
- LANG_ROOT=$(find . -path "*/language" -type d -not -path "./.git/*" 2>/dev/null | head -1)
396
- if [ -z "$LANG_ROOT" ]; then
397
- echo "No language/ directory found — skipping"
398
- exit 0
399
- fi
400
-
401
- if [ ! -d "$LANG_ROOT/en-GB" ]; then
402
- echo "::warning::Missing en-GB language directory (${LANG_ROOT}/en-GB) — the release will auto-create it from en-US"
403
- WARNINGS=$((WARNINGS + 1))
404
- fi
405
- if [ ! -d "$LANG_ROOT/en-US" ]; then
406
- echo "::warning::Missing en-US language directory (${LANG_ROOT}/en-US) — the release will auto-create it from en-GB"
407
- WARNINGS=$((WARNINGS + 1))
408
- fi
409
-
410
- # Warn (don't fail) when en-GB and en-US .ini sets differ — auto-healed at release.
411
- if [ -d "$LANG_ROOT/en-GB" ] && [ -d "$LANG_ROOT/en-US" ]; then
412
- for GB_INI in "$LANG_ROOT/en-GB"/*.ini; do
413
- [ ! -f "$GB_INI" ] && continue
414
- US_INI="$LANG_ROOT/en-US/$(basename "$GB_INI")"
415
- if [ ! -f "$US_INI" ]; then
416
- echo "::warning::$(basename "$GB_INI") exists in en-GB but missing from en-US — the release will auto-create it"
417
- WARNINGS=$((WARNINGS + 1))
418
- fi
419
- done
420
- for US_INI in "$LANG_ROOT/en-US"/*.ini; do
421
- [ ! -f "$US_INI" ] && continue
422
- GB_INI="$LANG_ROOT/en-GB/$(basename "$US_INI")"
423
- if [ ! -f "$GB_INI" ]; then
424
- echo "::warning::$(basename "$US_INI") exists in en-US but missing from en-GB — the release will auto-create it"
425
- WARNINGS=$((WARNINGS + 1))
426
- fi
427
- done
428
- fi
429
-
430
- # Find all .ini language files
431
- INI_FILES=$(find . -path "*/language/*/*.ini" -not -path "./.git/*" 2>/dev/null)
432
- if [ -z "$INI_FILES" ]; then
433
- echo "No .ini language files found"
434
- [ "$ERRORS" -gt 0 ] && exit 1
435
- exit 0
436
- fi
437
-
438
- echo "Found $(echo "$INI_FILES" | wc -l) language file(s)"
439
-
440
- for FILE in $INI_FILES; do
441
- FNAME=$(basename "$FILE")
442
- LINENUM=0
443
- SEEN_KEYS=""
444
-
445
- while IFS= read -r line || [ -n "$line" ]; do
446
- LINENUM=$((LINENUM + 1))
447
-
448
- # Skip empty lines and comments
449
- [ -z "$line" ] && continue
450
- echo "$line" | grep -qE '^\s*;' && continue
451
- echo "$line" | grep -qE '^\s*$' && continue
452
-
453
- # Must match KEY="VALUE" format
454
- if ! echo "$line" | grep -qE '^[A-Z_][A-Z0-9_]*=".*"$'; then
455
- echo "::error file=${FILE},line=${LINENUM}::Malformed line: ${line}"
456
- ERRORS=$((ERRORS + 1))
457
- continue
458
- fi
459
-
460
- # Extract key and check for duplicates
461
- KEY=$(echo "$line" | sed 's/=.*//')
462
- if echo "$SEEN_KEYS" | grep -qx "$KEY"; then
463
- echo "::error file=${FILE},line=${LINENUM}::Duplicate key: ${KEY}"
464
- ERRORS=$((ERRORS + 1))
465
- fi
466
- SEEN_KEYS="${SEEN_KEYS}
467
- ${KEY}"
468
- done < "$FILE"
469
-
470
- echo " ${FILE}: checked ${LINENUM} lines"
471
- done
472
-
473
- # Cross-check en-GB vs en-US key consistency
474
- GB_DIR=$(find . -path "*/language/en-GB" -type d -not -path "./.git/*" 2>/dev/null | head -1)
475
- US_DIR=$(find . -path "*/language/en-US" -type d -not -path "./.git/*" 2>/dev/null | head -1)
476
-
477
- if [ -n "$GB_DIR" ] && [ -n "$US_DIR" ]; then
478
- for GB_FILE in "$GB_DIR"/*.ini; do
479
- [ ! -f "$GB_FILE" ] && continue
480
- FNAME=$(basename "$GB_FILE")
481
- US_FILE="$US_DIR/$FNAME"
482
- [ ! -f "$US_FILE" ] && continue
483
-
484
- GB_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$GB_FILE" 2>/dev/null | sort)
485
- US_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$US_FILE" 2>/dev/null | sort)
486
-
487
- # Keys in en-GB but not en-US
488
- MISSING_US=$(comm -23 <(echo "$GB_KEYS") <(echo "$US_KEYS"))
489
- if [ -n "$MISSING_US" ]; then
490
- echo "::warning::Keys in en-GB/$FNAME but missing from en-US/$FNAME:"
491
- echo "$MISSING_US" | while read -r k; do echo " - $k"; done
492
- WARNINGS=$((WARNINGS + 1))
493
- fi
494
-
495
- # Keys in en-US but not en-GB
496
- MISSING_GB=$(comm -13 <(echo "$GB_KEYS") <(echo "$US_KEYS"))
497
- if [ -n "$MISSING_GB" ]; then
498
- echo "::warning::Keys in en-US/$FNAME but missing from en-GB/$FNAME:"
499
- echo "$MISSING_GB" | while read -r k; do echo " - $k"; done
500
- WARNINGS=$((WARNINGS + 1))
501
- fi
502
- done
503
- fi
504
-
505
- {
506
- echo "### Language File Validation"
507
- echo "| Metric | Count |"
508
- echo "|---|---|"
509
- echo "| Files checked | $(echo "$INI_FILES" | wc -l) |"
510
- echo "| Errors | ${ERRORS} |"
511
- echo "| Warnings | ${WARNINGS} |"
512
- } >> $GITHUB_STEP_SUMMARY
513
-
514
- if [ "$ERRORS" -gt 0 ]; then
515
- echo "::error::Language validation failed with ${ERRORS} error(s)"
516
- exit 1
517
- fi
518
- echo "Language files: OK (${WARNINGS} warning(s))"
519
-
520
- - name: Check changelog has unreleased entry
521
- run: |
522
- if [ ! -f "CHANGELOG.md" ]; then
523
- echo "::warning::No CHANGELOG.md found"
524
- exit 0
525
- fi
526
- # Check for content under [Unreleased] section
527
- if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then
528
- echo "::error::CHANGELOG.md missing [Unreleased] section"
529
- exit 1
530
- fi
531
- # Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased
532
- UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true)
533
- if [ "$UNRELEASED_CONTENT" -eq 0 ]; then
534
- echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes."
535
- echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY
536
- echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY
537
- echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY
538
- exit 1
539
- fi
540
- echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]"
541
-
542
- - name: Verify package source
543
- run: |
544
- SOURCE_DIR="source"
545
- [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="src"
546
- [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
547
- if [ ! -d "$SOURCE_DIR" ]; then
548
- echo "::warning::No src/ or htdocs/ directory"
549
- exit 0
550
- fi
551
- FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
552
- echo "Source: ${FILE_COUNT} files"
553
- [ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; }
554
-
555
- # ── Pre-Release RC Build ─────────────────────────────────────────────────
556
- pre-release:
557
- name: Build RC Package
558
- runs-on: ubuntu-latest
559
- # Independent job (no `needs:`) — needs-dependent jobs stall unscheduled on
560
- # the MokoGIT runner (they sit in `waiting` with started_at=epoch and never
561
- # run, leaving the check permanently pending). The Joomla Extension CI
562
- # "Build RC Pre-Release" job uses this same independent pattern for exactly
563
- # this reason. Guard template repos directly; branch policy is enforced by
564
- # its own required check, and the pre-release workflow re-validates before
565
- # publishing, so gating here on validate is not load-bearing.
566
- if: github.event_name == 'pull_request' && !startsWith(github.event.repository.name, 'Template-')
567
-
568
- steps:
569
- - name: Trigger RC pre-release
570
- env:
571
- MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN || github.token }}
572
- REPO: ${{ github.repository }}
573
- BRANCH: ${{ github.head_ref }}
574
- MOKOGIT_URL: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
575
- run: |
576
- curl -s -X POST "${MOKOGIT_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${MOKOGIT_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
577
- echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
578
- echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
579
-
580
- # ── Issue Reporter ──────────────────────────────────────────────────────
581
- report-issues:
582
- name: Report Issues
583
- # No `needs:` — the MokoGIT Actions scheduler does not offer needs-dependent
584
- # jobs to runners (they stall "waiting" and are reaped by
585
- # ABANDONED_JOB_TIMEOUT). This job runs independently and polls the run API
586
- # for the Validate PR result, then files an issue only when it failed.
587
- if: always()
588
- runs-on: ubuntu-latest
589
- steps:
590
- - name: Resolve Validate PR result (run API)
591
- id: gate
592
- env:
593
- TOKEN: ${{ secrets.MOKOGIT_TOKEN || github.token }}
594
- SERVER: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
595
- REPO: ${{ github.repository }}
596
- RUN_ID: ${{ github.run_id }}
597
- run: |
598
- set -u
599
- api="${SERVER}/api/v1/repos/${REPO}/actions/runs/${RUN_ID}/jobs"
600
- val="unknown"
601
- for i in $(seq 1 120); do
602
- resp=$(curl -sf -H "Authorization: token ${TOKEN}" "$api" 2>/dev/null || echo '{}')
603
- done_n=$(printf '%s' "$resp" | jq -r '[.jobs[]?|select(.name=="Validate PR")|select(.status=="completed")]|length' 2>/dev/null || echo 0)
604
- if [ "${done_n:-0}" -ge 1 ]; then
605
- val=$(printf '%s' "$resp" | jq -r '[.jobs[]?|select(.name=="Validate PR")][0].conclusion // "unknown"' 2>/dev/null || echo unknown)
606
- break
607
- fi
608
- sleep 15
609
- done
610
- echo "validate=${val}" >> "$GITHUB_OUTPUT"
611
- echo "Validate PR result: ${val}"
612
- - name: Clone MokoCLI
613
- if: steps.gate.outputs.validate == 'failure'
614
- env:
615
- MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
616
- MOKOGIT_URL: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
617
- run: |
618
- git clone --depth 1 --filter=blob:none --sparse "${MOKOGIT_URL}/MokoConsulting/mokocli.git" /tmp/mokocli
619
- cd /tmp/mokocli && git sparse-checkout set cli
620
- - name: Report CI failure
621
- if: steps.gate.outputs.validate == 'failure'
622
- env:
623
- MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
624
- MOKOGIT_URL: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
625
- # Route inputs through env vars (avoid ${{ }} shell-injection in run:).
626
- INPUT_GATE: "PR Validation"
627
- INPUT_DETAILS: "PR validation failed (syntax, manifest, changelog, or source checks). See the CI run for the specific check that failed."
628
- INPUT_SEVERITY: error
629
- INPUT_WORKFLOW: "PR Check"
630
- run: |
631
- chmod +x /tmp/mokocli/cli/ci_issue_reporter.sh
632
- /tmp/mokocli/cli/ci_issue_reporter.sh \
633
- --gate "$INPUT_GATE" \
634
- --details "$INPUT_DETAILS" \
635
- --severity "$INPUT_SEVERITY" \
636
- --workflow "$INPUT_WORKFLOW"
637
-
638
- # ── PR Check Summary (sticky comment) ──────────────────────────────────────
639
- # Create-or-update ONE comment (hidden marker) reflecting the gate outcome, so
640
- # the PR carries a live status line refreshed on every push. Best-effort.
641
- pr-summary:
642
- name: PR Check Summary (sticky comment)
643
- runs-on: ubuntu-latest
644
- # No `needs:` — poll the run API for gate results (the scheduler does not
645
- # dispatch needs-dependent jobs; they stall and are reaped).
646
- if: always()
647
- steps:
648
- - name: Upsert sticky summary comment
649
- env:
650
- TOKEN: ${{ secrets.MOKOGIT_TOKEN || github.token }}
651
- SERVER: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
652
- REPO: ${{ github.repository }}
653
- PR: ${{ github.event.pull_request.number }}
654
- RUN_ID: ${{ github.run_id }}
655
- RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
656
- HEAD_REF: ${{ github.head_ref }}
657
- BASE_REF: ${{ github.base_ref }}
658
- run: |
659
- set -uo pipefail
660
- { [ -n "${TOKEN:-}" ] && [ -n "${PR:-}" ]; } || { echo "no token/PR; skip"; exit 0; }
661
- # Poll the run API for Branch Policy + Validate PR results (no `needs:`).
662
- api="${SERVER}/api/v1/repos/${REPO}/actions/runs/${RUN_ID}/jobs"
663
- BP=unknown; VAL=unknown
664
- for i in $(seq 1 120); do
665
- resp=$(curl -sf -H "Authorization: token ${TOKEN}" "$api" 2>/dev/null || echo '{}')
666
- pend=$(printf '%s' "$resp" | jq -r '[.jobs[]?|select(.name=="Branch Policy" or .name=="Validate PR")|select(.status!="completed")]|length' 2>/dev/null || echo 1)
667
- tot=$(printf '%s' "$resp" | jq -r '[.jobs[]?|select(.name=="Branch Policy" or .name=="Validate PR")]|length' 2>/dev/null || echo 0)
668
- if [ "${tot:-0}" -ge 2 ] && [ "${pend:-1}" = "0" ]; then
669
- BP=$(printf '%s' "$resp" | jq -r '[.jobs[]?|select(.name=="Branch Policy")][0].conclusion // "unknown"')
670
- VAL=$(printf '%s' "$resp" | jq -r '[.jobs[]?|select(.name=="Validate PR")][0].conclusion // "unknown"')
671
- break
672
- fi
673
- sleep 15
674
- done
675
- icon() { case "$1" in success) printf '✅';; failure) printf '❌';; skipped) printf '⏭️';; *) printf '⚪';; esac; }
676
- MARKER="<!-- pr-check-summary -->"
677
- BODY="$(printf '%s\n## PR Check Summary\n\n- %s Branch policy (`%s` → `%s`)\n- %s Validation\n\n_Updated %s · [run](%s)_' \
678
- "${MARKER}" \
679
- "$(icon "${BP}")" "${HEAD_REF}" "${BASE_REF}" \
680
- "$(icon "${VAL}")" \
681
- "$(date -u +%FT%TZ)" "${RUN_URL}")"
682
- LIST="${SERVER}/api/v1/repos/${REPO}/issues/${PR}/comments"
683
- EDIT="${SERVER}/api/v1/repos/${REPO}/issues/comments"
684
- CID="$(curl -sf -H "Authorization: token ${TOKEN}" "${LIST}?limit=100" 2>/dev/null \
685
- | python3 -c "import sys,json; c=[x for x in json.load(sys.stdin) if '${MARKER}' in (x.get('body') or '')]; print(c[-1]['id'] if c else '')" 2>/dev/null || true)"
686
- PAYLOAD="$(python3 -c "import json,sys; print(json.dumps({'body': sys.stdin.read()}))" <<<"${BODY}")"
687
- if [ -n "${CID}" ]; then
688
- curl -sf -X PATCH -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" -d "${PAYLOAD}" "${EDIT}/${CID}" >/dev/null 2>&1 || true
689
- echo "updated sticky summary #${CID} on PR #${PR}"
690
- else
691
- curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" -d "${PAYLOAD}" "${LIST}" >/dev/null 2>&1 || true
692
- echo "created sticky summary on PR #${PR}"
693
- fi