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

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.
@@ -7,7 +7,7 @@
7
7
  # INGROUP: MokoCLI.CI
8
8
  # REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
9
9
  # PATH: /.mokogit/workflows/pr-check.yml
10
- # VERSION: 09.23.00
10
+ # VERSION: 09.24.00
11
11
  # BRIEF: PR gate — branch policy + code validation before merge
12
12
 
13
13
  name: "Universal: PR Check"
@@ -23,6 +23,11 @@ permissions:
23
23
  env:
24
24
  FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
25
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
+
26
31
  jobs:
27
32
  # ── Branch Policy ──────────────────────────────────────────────────────
28
33
  branch-policy:
@@ -145,7 +150,7 @@ jobs:
145
150
  steps:
146
151
  - name: Remind to update the wiki
147
152
  env:
148
- TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
153
+ TOKEN: ${{ secrets.MOKOGIT_TOKEN || github.token }}
149
154
  SERVER: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
150
155
  REPO: ${{ github.repository }}
151
156
  PR: ${{ github.event.pull_request.number }}
@@ -160,16 +165,23 @@ jobs:
160
165
  echo ""
161
166
  echo "_Non-blocking reminder._"
162
167
  } >> "$GITHUB_STEP_SUMMARY"
163
- # Post a single PR comment (idempotent via hidden marker); best-effort, never fails.
164
- API="${SERVER}/api/v1/repos/${REPO}/issues/${PR}/comments"
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)_")"
165
174
  if [ -n "${TOKEN:-}" ] && [ -n "${PR:-}" ]; then
166
- existing="$(curl -sf -H "Authorization: token ${TOKEN}" "$API" 2>/dev/null | grep -c 'wiki-reminder' || true)"
167
- if [ "${existing:-0}" -eq 0 ]; then
168
- curl -sf -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" -X POST "$API" \
169
- -d '{"body":"<!-- wiki-reminder -->\n\n**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)_"}' >/dev/null 2>&1 || true
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
170
182
  fi
171
183
  fi
172
- echo "Wiki reminder emitted (non-blocking)."
184
+ echo "Wiki reminder emitted (non-blocking, sticky)."
173
185
 
174
186
  validate:
175
187
  name: Validate PR
@@ -194,6 +206,28 @@ jobs:
194
206
  fi
195
207
  echo "No conflict markers found"
196
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
+
197
231
  - name: Detect platform
198
232
  id: platform
199
233
  run: |
@@ -237,11 +271,11 @@ jobs:
237
271
  echo "::error file=${file}::Missing JEXEC guard: ${file}"
238
272
  ERRORS=$((ERRORS + 1))
239
273
  fi
240
- done < <(find . -name "*.php" -path "*/src/*" -not -path "./.git/*" -not -path "./vendor/*" -print0)
274
+ done < <(find . -name "*.php" \( -path "*/src/*" -o -path "*/source/*" \) -not -path "./.git/*" -not -path "./vendor/*" -print0)
241
275
  if [ "$ERRORS" -gt 0 ]; then
242
276
  echo "::error::${ERRORS} PHP file(s) missing defined('_JEXEC') or die guard"
243
277
  echo "## JEXEC Guard Check: Failed" >> $GITHUB_STEP_SUMMARY
244
- echo "${ERRORS} file(s) in src/ are missing the Joomla execution guard." >> $GITHUB_STEP_SUMMARY
278
+ echo "${ERRORS} file(s) in src/ or source/ are missing the Joomla execution guard." >> $GITHUB_STEP_SUMMARY
245
279
  exit 1
246
280
  fi
247
281
  echo "JEXEC guard: OK"
@@ -250,7 +284,7 @@ jobs:
250
284
  if: steps.platform.outputs.platform == 'joomla'
251
285
  run: |
252
286
  MISSING=0
253
- SOURCE_DIR="src"
287
+ SOURCE_DIR="src"; [ -d source ] && SOURCE_DIR="source"
254
288
  [ ! -d "$SOURCE_DIR" ] && exit 0
255
289
  while IFS= read -r dir; do
256
290
  if [ ! -f "${dir}/index.html" ]; then
@@ -348,27 +382,16 @@ jobs:
348
382
  ;;
349
383
  esac
350
384
 
351
- - name: Check update stream format
352
- run: |
353
- PLATFORM="${{ steps.platform.outputs.platform }}"
354
- case "$PLATFORM" in
355
- joomla)
356
- if [ -f "updates.xml" ]; then
357
- if command -v php &> /dev/null; then
358
- php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::updates.xml is malformed"; exit 1; }
359
- fi
360
- echo "updates.xml valid"
361
- fi
362
- ;;
363
- esac
364
-
365
385
  - name: Validate Joomla language files
366
386
  if: steps.platform.outputs.platform == 'joomla'
367
387
  run: |
368
388
  ERRORS=0
369
389
  WARNINGS=0
370
390
 
371
- # Require both en-GB and en-US language directories
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.
372
395
  LANG_ROOT=$(find . -path "*/language" -type d -not -path "./.git/*" 2>/dev/null | head -1)
373
396
  if [ -z "$LANG_ROOT" ]; then
374
397
  echo "No language/ directory found — skipping"
@@ -376,30 +399,30 @@ jobs:
376
399
  fi
377
400
 
378
401
  if [ ! -d "$LANG_ROOT/en-GB" ]; then
379
- echo "::error::Missing en-GB language directory (${LANG_ROOT}/en-GB)"
380
- ERRORS=$((ERRORS + 1))
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))
381
404
  fi
382
405
  if [ ! -d "$LANG_ROOT/en-US" ]; then
383
- echo "::error::Missing en-US language directory (${LANG_ROOT}/en-US)"
384
- ERRORS=$((ERRORS + 1))
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))
385
408
  fi
386
409
 
387
- # Check that en-GB and en-US have matching .ini files
410
+ # Warn (don't fail) when en-GB and en-US .ini sets differ — auto-healed at release.
388
411
  if [ -d "$LANG_ROOT/en-GB" ] && [ -d "$LANG_ROOT/en-US" ]; then
389
412
  for GB_INI in "$LANG_ROOT/en-GB"/*.ini; do
390
413
  [ ! -f "$GB_INI" ] && continue
391
414
  US_INI="$LANG_ROOT/en-US/$(basename "$GB_INI")"
392
415
  if [ ! -f "$US_INI" ]; then
393
- echo "::error::$(basename "$GB_INI") exists in en-GB but missing from en-US"
394
- ERRORS=$((ERRORS + 1))
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))
395
418
  fi
396
419
  done
397
420
  for US_INI in "$LANG_ROOT/en-US"/*.ini; do
398
421
  [ ! -f "$US_INI" ] && continue
399
422
  GB_INI="$LANG_ROOT/en-GB/$(basename "$US_INI")"
400
423
  if [ ! -f "$GB_INI" ]; then
401
- echo "::error::$(basename "$US_INI") exists in en-US but missing from en-GB"
402
- ERRORS=$((ERRORS + 1))
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))
403
426
  fi
404
427
  done
405
428
  fi
@@ -518,7 +541,8 @@ jobs:
518
541
 
519
542
  - name: Verify package source
520
543
  run: |
521
- SOURCE_DIR="src"
544
+ SOURCE_DIR="source"
545
+ [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="src"
522
546
  [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
523
547
  if [ ! -d "$SOURCE_DIR" ]; then
524
548
  echo "::warning::No src/ or htdocs/ directory"
@@ -532,15 +556,19 @@ jobs:
532
556
  pre-release:
533
557
  name: Build RC Package
534
558
  runs-on: ubuntu-latest
535
- needs: [branch-policy, validate]
536
- # Run only when both gates succeeded; always() forces evaluation so a skipped
537
- # validate (e.g. template repos) skips this job cleanly instead of hanging.
538
- if: ${{ always() && needs.branch-policy.result == 'success' && needs.validate.result == 'success' }}
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-')
539
567
 
540
568
  steps:
541
569
  - name: Trigger RC pre-release
542
570
  env:
543
- MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
571
+ MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN || github.token }}
544
572
  REPO: ${{ github.repository }}
545
573
  BRANCH: ${{ github.head_ref }}
546
574
  MOKOGIT_URL: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
@@ -552,14 +580,114 @@ jobs:
552
580
  # ── Issue Reporter ──────────────────────────────────────────────────────
553
581
  report-issues:
554
582
  name: Report Issues
555
- needs: [branch-policy, validate]
556
- if: >-
557
- always() &&
558
- needs.validate.result == 'failure'
559
- uses: ./.mokogit/workflows/ci-issue-reporter.yml
560
- with:
561
- gate: "PR Validation"
562
- workflow: "PR Check"
563
- severity: error
564
- details: "PR validation failed (syntax, manifest, changelog, or source checks). See the CI run for the specific check that failed."
565
- secrets: inherit
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