@mokoconsulting/mcp-mokosuite 1.0.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.
Files changed (73) hide show
  1. package/.editorconfig +19 -0
  2. package/.gitattributes +94 -0
  3. package/.gitmessage +9 -0
  4. package/.mokogit/ISSUE_TEMPLATE/adr.md +110 -0
  5. package/.mokogit/ISSUE_TEMPLATE/bug_report.md +48 -0
  6. package/.mokogit/ISSUE_TEMPLATE/config.yml +18 -0
  7. package/.mokogit/ISSUE_TEMPLATE/documentation.md +52 -0
  8. package/.mokogit/ISSUE_TEMPLATE/feature_request.md +51 -0
  9. package/.mokogit/ISSUE_TEMPLATE/mcp_api_integration.md +48 -0
  10. package/.mokogit/ISSUE_TEMPLATE/mcp_connection_issue.md +69 -0
  11. package/.mokogit/ISSUE_TEMPLATE/mcp_tool_request.md +50 -0
  12. package/.mokogit/ISSUE_TEMPLATE/question.md +82 -0
  13. package/.mokogit/ISSUE_TEMPLATE/rfc.md +126 -0
  14. package/.mokogit/ISSUE_TEMPLATE/security.md +51 -0
  15. package/.mokogit/ISSUE_TEMPLATE/version.md +24 -0
  16. package/.mokogit/actions/resolve-source-dir/action.yml +108 -0
  17. package/.mokogit/workflows/auto-assign.yml +76 -0
  18. package/.mokogit/workflows/auto-bump.yml +78 -0
  19. package/.mokogit/workflows/auto-dev-issue.yml +207 -0
  20. package/.mokogit/workflows/auto-release.yml +596 -0
  21. package/.mokogit/workflows/branch-cleanup.yml +60 -0
  22. package/.mokogit/workflows/cascade-dev.yml +198 -0
  23. package/.mokogit/workflows/changelog-validation.yml +101 -0
  24. package/.mokogit/workflows/ci-generic.yml +203 -0
  25. package/.mokogit/workflows/ci-issue-reporter.yml +75 -0
  26. package/.mokogit/workflows/cleanup.yml +87 -0
  27. package/.mokogit/workflows/gitleaks.yml +94 -0
  28. package/.mokogit/workflows/issue-branch.yml +81 -0
  29. package/.mokogit/workflows/notify.yml +73 -0
  30. package/.mokogit/workflows/npm-build-test.yml +83 -0
  31. package/.mokogit/workflows/npm-publish.yml +126 -0
  32. package/.mokogit/workflows/npm-sdk-check.yml +110 -0
  33. package/.mokogit/workflows/npm-tool-inventory.yml +80 -0
  34. package/.mokogit/workflows/pr-branch-check.yml +90 -0
  35. package/.mokogit/workflows/pr-check.yml +565 -0
  36. package/.mokogit/workflows/pre-release.yml +413 -0
  37. package/.mokogit/workflows/push-notify.yml +43 -0
  38. package/.mokogit/workflows/rc-revert.yml +72 -0
  39. package/.mokogit/workflows/repo-health.yml +700 -0
  40. package/.mokogit/workflows/repository-cleanup.yml +525 -0
  41. package/.mokogit/workflows/standards-compliance.yml +2507 -0
  42. package/.mokogit/workflows/sync-version-on-merge.yml +130 -0
  43. package/.mokogit/workflows/version-set.yml +131 -0
  44. package/CHANGELOG.md +18 -0
  45. package/CLAUDE.md +52 -0
  46. package/CODE_OF_CONDUCT.md +70 -0
  47. package/CONTRIBUTING.md +161 -0
  48. package/LICENSE +696 -0
  49. package/Makefile +70 -0
  50. package/README.md +83 -0
  51. package/SECURITY.md +34 -0
  52. package/config.example.json +21 -0
  53. package/dist/client.d.ts +22 -0
  54. package/dist/client.js +124 -0
  55. package/dist/config.d.ts +4 -0
  56. package/dist/config.js +53 -0
  57. package/dist/index.d.ts +3 -0
  58. package/dist/index.js +181 -0
  59. package/dist/signing.d.ts +14 -0
  60. package/dist/signing.js +62 -0
  61. package/dist/types.d.ts +44 -0
  62. package/dist/types.js +16 -0
  63. package/docs/API.md +63 -0
  64. package/docs/ARCHITECTURE.md +73 -0
  65. package/docs/INSTALLATION.md +102 -0
  66. package/docs/index.md +12 -0
  67. package/package.json +35 -0
  68. package/src/client.ts +142 -0
  69. package/src/config.ts +63 -0
  70. package/src/index.ts +336 -0
  71. package/src/signing.ts +67 -0
  72. package/src/types.ts +63 -0
  73. package/tsconfig.json +19 -0
@@ -0,0 +1,90 @@
1
+ # Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
2
+ # SPDX-License-Identifier: GPL-3.0-or-later
3
+ #
4
+ # Enforces branch merge policy:
5
+ # feature/* → dev only
6
+ # fix/* → dev only
7
+ # hotfix/* → dev or main (emergency)
8
+ # dev → main only
9
+ # alpha/* → dev only
10
+ # beta/* → dev only
11
+ # rc/* → main only
12
+
13
+ name: Branch Policy Check
14
+
15
+ on:
16
+ pull_request:
17
+ types: [opened, synchronize, reopened, edited]
18
+
19
+ jobs:
20
+ check-target:
21
+ name: Verify merge target
22
+ runs-on: ubuntu-latest
23
+ steps:
24
+ - name: Check branch policy
25
+ run: |
26
+ HEAD="${{ github.head_ref }}"
27
+ BASE="${{ github.base_ref }}"
28
+
29
+ echo "PR: ${HEAD} → ${BASE}"
30
+
31
+ ALLOWED=true
32
+ REASON=""
33
+
34
+ case "$HEAD" in
35
+ feature/*|feat/*)
36
+ if [ "$BASE" != "dev" ]; then
37
+ ALLOWED=false
38
+ REASON="Feature branches must target 'dev', not '${BASE}'"
39
+ fi
40
+ ;;
41
+ fix/*|bugfix/*)
42
+ if [ "$BASE" != "dev" ]; then
43
+ ALLOWED=false
44
+ REASON="Fix branches must target 'dev', not '${BASE}'"
45
+ fi
46
+ ;;
47
+ hotfix/*)
48
+ if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
49
+ ALLOWED=false
50
+ REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'"
51
+ fi
52
+ ;;
53
+ alpha/*|beta/*)
54
+ if [ "$BASE" != "dev" ]; then
55
+ ALLOWED=false
56
+ REASON="Pre-release branches must target 'dev', not '${BASE}'"
57
+ fi
58
+ ;;
59
+ rc/*)
60
+ if [ "$BASE" != "main" ]; then
61
+ ALLOWED=false
62
+ REASON="Release candidate branches must target 'main', not '${BASE}'"
63
+ fi
64
+ ;;
65
+ dev)
66
+ if [ "$BASE" != "main" ]; then
67
+ ALLOWED=false
68
+ REASON="Dev branch can only merge into 'main', not '${BASE}'"
69
+ fi
70
+ ;;
71
+ esac
72
+
73
+ if [ "$ALLOWED" = false ]; then
74
+ echo "::error::${REASON}"
75
+ echo ""
76
+ echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY
77
+ echo "" >> $GITHUB_STEP_SUMMARY
78
+ echo "${REASON}" >> $GITHUB_STEP_SUMMARY
79
+ echo "" >> $GITHUB_STEP_SUMMARY
80
+ echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
81
+ echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
82
+ echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
83
+ echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
84
+ echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
85
+ echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
86
+ exit 1
87
+ fi
88
+
89
+ echo "Branch policy: OK (${HEAD} → ${BASE})"
90
+ echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
@@ -0,0 +1,565 @@
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.23.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
+ jobs:
27
+ # ── Branch Policy ──────────────────────────────────────────────────────
28
+ branch-policy:
29
+ name: Branch Policy
30
+ runs-on: ubuntu-latest
31
+ steps:
32
+ - name: Check branch merge target
33
+ run: |
34
+ HEAD="${{ github.head_ref }}"
35
+ BASE="${{ github.base_ref }}"
36
+
37
+ echo "PR: ${HEAD} → ${BASE}"
38
+
39
+ ALLOWED=true
40
+ REASON=""
41
+
42
+ case "$HEAD" in
43
+ feature/*|feat/*)
44
+ if [ "$BASE" != "dev" ]; then
45
+ ALLOWED=false
46
+ REASON="Feature branches must target 'dev', not '${BASE}'"
47
+ fi
48
+ ;;
49
+ fix/*|bugfix/*)
50
+ if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
51
+ ALLOWED=false
52
+ REASON="Fix branches must target 'dev' or 'main', not '${BASE}'"
53
+ fi
54
+ ;;
55
+ patch/*)
56
+ if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ] && [ "$BASE" != "main" ]; then
57
+ ALLOWED=false
58
+ REASON="Patch branches must target 'dev', 'rc', or 'main', not '${BASE}'"
59
+ fi
60
+ ;;
61
+ hotfix/*)
62
+ if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
63
+ ALLOWED=false
64
+ REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'"
65
+ fi
66
+ ;;
67
+ rc)
68
+ if [ "$BASE" != "main" ]; then
69
+ ALLOWED=false
70
+ REASON="RC branch can only merge into 'main', not '${BASE}'"
71
+ fi
72
+ ;;
73
+ dev)
74
+ if [ "$BASE" != "main" ]; then
75
+ ALLOWED=false
76
+ REASON="Dev branch can only merge into 'main', not '${BASE}'"
77
+ fi
78
+ ;;
79
+ esac
80
+
81
+ if [ "$ALLOWED" = false ]; then
82
+ echo "::error::${REASON}"
83
+ echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY
84
+ echo "" >> $GITHUB_STEP_SUMMARY
85
+ echo "${REASON}" >> $GITHUB_STEP_SUMMARY
86
+ echo "" >> $GITHUB_STEP_SUMMARY
87
+ echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
88
+ echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
89
+ echo "- \`fix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
90
+ echo "- \`patch/*\` → \`dev\`, \`rc\`, or \`main\`" >> $GITHUB_STEP_SUMMARY
91
+ echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
92
+ echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
93
+ echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
94
+ exit 1
95
+ fi
96
+
97
+ echo "Branch policy: OK (${HEAD} → ${BASE})"
98
+ echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
99
+
100
+ # ── Docs Update Gate (main PRs) ─────────────────────────────────────────
101
+ require-docs:
102
+ name: Require Docs Update
103
+ runs-on: ubuntu-latest
104
+ # Enforce only on PRs merging into main: README.md + CHANGELOG.md must both be updated.
105
+ if: ${{ github.base_ref == 'main' }}
106
+ steps:
107
+ - name: Checkout
108
+ uses: actions/checkout@v4
109
+ with:
110
+ fetch-depth: 0
111
+
112
+ - name: Require README.md and CHANGELOG.md in the PR diff
113
+ run: |
114
+ BASE="${{ github.event.pull_request.base.sha }}"
115
+ HEAD="${{ github.event.pull_request.head.sha }}"
116
+ CHANGED="$(git diff --name-only "$BASE" "$HEAD" 2>/dev/null || true)"
117
+ if [ -z "$CHANGED" ]; then
118
+ git fetch -q origin "${{ github.base_ref }}" 2>/dev/null || true
119
+ CHANGED="$(git diff --name-only "origin/${{ github.base_ref }}...HEAD" 2>/dev/null || true)"
120
+ fi
121
+ echo "Changed files in PR:"
122
+ echo "$CHANGED"
123
+ MISSING=""
124
+ echo "$CHANGED" | grep -qxE 'README\.md' || MISSING="README.md"
125
+ echo "$CHANGED" | grep -qxE 'CHANGELOG\.md' || MISSING="${MISSING:+$MISSING, }CHANGELOG.md"
126
+ if [ -n "$MISSING" ]; then
127
+ echo "::error::PRs into main must update: ${MISSING}"
128
+ {
129
+ echo "## Docs Update Required"
130
+ echo ""
131
+ echo "PRs merging into \`main\` must update both **README.md** and **CHANGELOG.md**."
132
+ echo ""
133
+ echo "Not updated in this PR: **${MISSING}**"
134
+ } >> "$GITHUB_STEP_SUMMARY"
135
+ exit 1
136
+ fi
137
+ echo "Docs update present (README.md + CHANGELOG.md)"
138
+ echo "## Docs Update: Passed" >> "$GITHUB_STEP_SUMMARY"
139
+
140
+ # ── Wiki Update Reminder (main PRs, non-blocking) ───────────────────────
141
+ wiki-reminder:
142
+ name: Wiki Update Reminder
143
+ runs-on: ubuntu-latest
144
+ if: ${{ github.base_ref == 'main' }}
145
+ steps:
146
+ - name: Remind to update the wiki
147
+ env:
148
+ TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
149
+ SERVER: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
150
+ REPO: ${{ github.repository }}
151
+ PR: ${{ github.event.pull_request.number }}
152
+ run: |
153
+ set -uo pipefail
154
+ {
155
+ echo "## Wiki Update Reminder"
156
+ echo ""
157
+ echo "Docs are **wiki-first** at MokoConsulting. If this change affects behavior, usage, configuration, or standards, update the repo wiki:"
158
+ echo ""
159
+ echo "- ${SERVER}/${REPO}/wiki"
160
+ echo ""
161
+ echo "_Non-blocking reminder._"
162
+ } >> "$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"
165
+ 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
170
+ fi
171
+ fi
172
+ echo "Wiki reminder emitted (non-blocking)."
173
+
174
+ validate:
175
+ name: Validate PR
176
+ runs-on: ubuntu-latest
177
+ # Skip on template repos (Template-*) — no real manifest/source/changelog to validate.
178
+ if: ${{ !startsWith(github.event.repository.name, 'Template-') }}
179
+
180
+ steps:
181
+ - name: Checkout
182
+ uses: actions/checkout@v4
183
+
184
+ - name: Check for merge conflict markers
185
+ run: |
186
+ 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)
187
+ if [ -n "$CONFLICTS" ]; then
188
+ echo "::error::Merge conflict markers found in source files"
189
+ echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY
190
+ echo '```' >> $GITHUB_STEP_SUMMARY
191
+ echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
192
+ echo '```' >> $GITHUB_STEP_SUMMARY
193
+ exit 1
194
+ fi
195
+ echo "No conflict markers found"
196
+
197
+ - name: Detect platform
198
+ id: platform
199
+ run: |
200
+ # Platform comes from the MokoGIT metadata API (public GET).
201
+ API="${GITHUB_SERVER_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${GITHUB_REPOSITORY}/metadata"
202
+ PLATFORM="$(curl -sf "$API" 2>/dev/null | python3 -c "import sys, json; print(json.load(sys.stdin).get('platform') or '')" 2>/dev/null || true)"
203
+ [ -z "$PLATFORM" ] && PLATFORM="generic"
204
+ echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
205
+ echo "Detected platform: $PLATFORM"
206
+
207
+ - name: Setup PHP
208
+ if: steps.platform.outputs.platform == 'joomla'
209
+ run: |
210
+ if ! command -v php &> /dev/null; then
211
+ sudo apt-get update -qq
212
+ sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
213
+ fi
214
+
215
+ - name: PHP syntax check
216
+ if: steps.platform.outputs.platform == 'joomla'
217
+ run: |
218
+ ERRORS=0
219
+ while IFS= read -r -d '' file; do
220
+ if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
221
+ ERRORS=$((ERRORS + 1))
222
+ fi
223
+ done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0)
224
+ echo "PHP lint: ${ERRORS} error(s)"
225
+ [ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
226
+
227
+ - name: Joomla JEXEC guard check
228
+ if: steps.platform.outputs.platform == 'joomla'
229
+ run: |
230
+ ERRORS=0
231
+ while IFS= read -r -d '' file; do
232
+ # Skip vendor, node_modules, and index.html stub files
233
+ case "$file" in ./vendor/*|./node_modules/*) continue ;; esac
234
+ # Scan the whole file for the JEXEC/JPATH guard: it is placed after
235
+ # the SPDX/file-header docblock, which commonly runs past 20 lines.
236
+ if ! grep -qE "defined\s*\(\s*['\"](_JEXEC|JPATH_BASE|\\\\JPATH_PLATFORM)['\"]" "$file"; then
237
+ echo "::error file=${file}::Missing JEXEC guard: ${file}"
238
+ ERRORS=$((ERRORS + 1))
239
+ fi
240
+ done < <(find . -name "*.php" -path "*/src/*" -not -path "./.git/*" -not -path "./vendor/*" -print0)
241
+ if [ "$ERRORS" -gt 0 ]; then
242
+ echo "::error::${ERRORS} PHP file(s) missing defined('_JEXEC') or die guard"
243
+ echo "## JEXEC Guard Check: Failed" >> $GITHUB_STEP_SUMMARY
244
+ echo "${ERRORS} file(s) in src/ are missing the Joomla execution guard." >> $GITHUB_STEP_SUMMARY
245
+ exit 1
246
+ fi
247
+ echo "JEXEC guard: OK"
248
+
249
+ - name: Joomla directory listing protection
250
+ if: steps.platform.outputs.platform == 'joomla'
251
+ run: |
252
+ MISSING=0
253
+ SOURCE_DIR="src"
254
+ [ ! -d "$SOURCE_DIR" ] && exit 0
255
+ while IFS= read -r dir; do
256
+ if [ ! -f "${dir}/index.html" ]; then
257
+ echo "::warning::Missing index.html in ${dir} (directory listing protection)"
258
+ MISSING=$((MISSING + 1))
259
+ fi
260
+ done < <(find "$SOURCE_DIR" -type d -not -path "./.git/*" -not -path "*/vendor/*" -not -path "*/node_modules/*")
261
+ if [ "$MISSING" -gt 0 ]; then
262
+ echo "## Directory Protection" >> $GITHUB_STEP_SUMMARY
263
+ echo "${MISSING} director(ies) missing index.html" >> $GITHUB_STEP_SUMMARY
264
+ fi
265
+ echo "Directory protection: ${MISSING} missing (advisory)"
266
+
267
+ - name: Joomla script file and asset checks
268
+ if: steps.platform.outputs.platform == 'joomla'
269
+ run: |
270
+ ERRORS=0
271
+ MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
272
+ [ -z "$MANIFEST" ] && exit 0
273
+ MANIFEST_DIR=$(dirname "$MANIFEST")
274
+
275
+ # Check scriptfile exists if declared
276
+ SCRIPTFILE=$(sed -n 's/.*<scriptfile>\([^<]*\)<\/scriptfile>.*/\1/p' "$MANIFEST" 2>/dev/null)
277
+ if [ -n "$SCRIPTFILE" ]; then
278
+ if [ ! -f "${MANIFEST_DIR}/${SCRIPTFILE}" ]; then
279
+ echo "::error::Manifest declares <scriptfile>${SCRIPTFILE}</scriptfile> but file not found at ${MANIFEST_DIR}/${SCRIPTFILE}"
280
+ ERRORS=$((ERRORS + 1))
281
+ else
282
+ echo "Script file: ${MANIFEST_DIR}/${SCRIPTFILE} (OK)"
283
+ fi
284
+ fi
285
+
286
+ # Require joomla.asset.json and validate it
287
+ ASSET_JSON=$(find "$MANIFEST_DIR" -name "joomla.asset.json" -not -path "./.git/*" 2>/dev/null | head -1)
288
+ if [ -z "$ASSET_JSON" ]; then
289
+ echo "::error::joomla.asset.json not found — Joomla asset system is required"
290
+ ERRORS=$((ERRORS + 1))
291
+ else
292
+ if command -v php &> /dev/null; then
293
+ 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 || {
294
+ echo "::error::joomla.asset.json is not valid JSON"
295
+ ERRORS=$((ERRORS + 1))
296
+ }
297
+ fi
298
+ echo "joomla.asset.json: valid"
299
+ fi
300
+
301
+ # Validate all XML files in src/ are well-formed
302
+ XML_ERRORS=0
303
+ if command -v php &> /dev/null; then
304
+ while IFS= read -r -d '' xmlfile; do
305
+ 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
306
+ XML_ERRORS=$((XML_ERRORS + 1))
307
+ fi
308
+ done < <(find "$MANIFEST_DIR" -name "*.xml" -not -path "./.git/*" -print0)
309
+ fi
310
+ if [ "$XML_ERRORS" -gt 0 ]; then
311
+ echo "::error::${XML_ERRORS} XML file(s) are malformed"
312
+ ERRORS=$((ERRORS + 1))
313
+ else
314
+ echo "XML well-formedness: OK"
315
+ fi
316
+
317
+ [ "$ERRORS" -gt 0 ] && exit 1
318
+ echo "Joomla asset checks: OK"
319
+
320
+ - name: Validate platform manifest
321
+ run: |
322
+ PLATFORM="${{ steps.platform.outputs.platform }}"
323
+ case "$PLATFORM" in
324
+ joomla)
325
+ MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
326
+ if [ -z "$MANIFEST" ]; then
327
+ echo "::warning::No Joomla manifest found (MokoSuite site)"
328
+ exit 0
329
+ fi
330
+ echo "Manifest: ${MANIFEST}"
331
+ if command -v php &> /dev/null; then
332
+ 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; }
333
+ fi
334
+ for ELEMENT in name version description; do
335
+ grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; }
336
+ done
337
+ # Block legacy raw/branch update server URLs on MokoGIT
338
+ RAW_URLS=$(grep -n 'raw/branch' "$MANIFEST" | grep -i 'mokoconsulting\|mokogit\|git\.mokoconsulting\.tech' || true)
339
+ if [ -n "$RAW_URLS" ]; then
340
+ 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)"
341
+ echo "$RAW_URLS"
342
+ exit 1
343
+ fi
344
+ echo "Joomla manifest valid"
345
+ ;;
346
+ *)
347
+ echo "Generic platform — no manifest validation"
348
+ ;;
349
+ esac
350
+
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
+ - name: Validate Joomla language files
366
+ if: steps.platform.outputs.platform == 'joomla'
367
+ run: |
368
+ ERRORS=0
369
+ WARNINGS=0
370
+
371
+ # Require both en-GB and en-US language directories
372
+ LANG_ROOT=$(find . -path "*/language" -type d -not -path "./.git/*" 2>/dev/null | head -1)
373
+ if [ -z "$LANG_ROOT" ]; then
374
+ echo "No language/ directory found — skipping"
375
+ exit 0
376
+ fi
377
+
378
+ if [ ! -d "$LANG_ROOT/en-GB" ]; then
379
+ echo "::error::Missing en-GB language directory (${LANG_ROOT}/en-GB)"
380
+ ERRORS=$((ERRORS + 1))
381
+ fi
382
+ if [ ! -d "$LANG_ROOT/en-US" ]; then
383
+ echo "::error::Missing en-US language directory (${LANG_ROOT}/en-US)"
384
+ ERRORS=$((ERRORS + 1))
385
+ fi
386
+
387
+ # Check that en-GB and en-US have matching .ini files
388
+ if [ -d "$LANG_ROOT/en-GB" ] && [ -d "$LANG_ROOT/en-US" ]; then
389
+ for GB_INI in "$LANG_ROOT/en-GB"/*.ini; do
390
+ [ ! -f "$GB_INI" ] && continue
391
+ US_INI="$LANG_ROOT/en-US/$(basename "$GB_INI")"
392
+ if [ ! -f "$US_INI" ]; then
393
+ echo "::error::$(basename "$GB_INI") exists in en-GB but missing from en-US"
394
+ ERRORS=$((ERRORS + 1))
395
+ fi
396
+ done
397
+ for US_INI in "$LANG_ROOT/en-US"/*.ini; do
398
+ [ ! -f "$US_INI" ] && continue
399
+ GB_INI="$LANG_ROOT/en-GB/$(basename "$US_INI")"
400
+ if [ ! -f "$GB_INI" ]; then
401
+ echo "::error::$(basename "$US_INI") exists in en-US but missing from en-GB"
402
+ ERRORS=$((ERRORS + 1))
403
+ fi
404
+ done
405
+ fi
406
+
407
+ # Find all .ini language files
408
+ INI_FILES=$(find . -path "*/language/*/*.ini" -not -path "./.git/*" 2>/dev/null)
409
+ if [ -z "$INI_FILES" ]; then
410
+ echo "No .ini language files found"
411
+ [ "$ERRORS" -gt 0 ] && exit 1
412
+ exit 0
413
+ fi
414
+
415
+ echo "Found $(echo "$INI_FILES" | wc -l) language file(s)"
416
+
417
+ for FILE in $INI_FILES; do
418
+ FNAME=$(basename "$FILE")
419
+ LINENUM=0
420
+ SEEN_KEYS=""
421
+
422
+ while IFS= read -r line || [ -n "$line" ]; do
423
+ LINENUM=$((LINENUM + 1))
424
+
425
+ # Skip empty lines and comments
426
+ [ -z "$line" ] && continue
427
+ echo "$line" | grep -qE '^\s*;' && continue
428
+ echo "$line" | grep -qE '^\s*$' && continue
429
+
430
+ # Must match KEY="VALUE" format
431
+ if ! echo "$line" | grep -qE '^[A-Z_][A-Z0-9_]*=".*"$'; then
432
+ echo "::error file=${FILE},line=${LINENUM}::Malformed line: ${line}"
433
+ ERRORS=$((ERRORS + 1))
434
+ continue
435
+ fi
436
+
437
+ # Extract key and check for duplicates
438
+ KEY=$(echo "$line" | sed 's/=.*//')
439
+ if echo "$SEEN_KEYS" | grep -qx "$KEY"; then
440
+ echo "::error file=${FILE},line=${LINENUM}::Duplicate key: ${KEY}"
441
+ ERRORS=$((ERRORS + 1))
442
+ fi
443
+ SEEN_KEYS="${SEEN_KEYS}
444
+ ${KEY}"
445
+ done < "$FILE"
446
+
447
+ echo " ${FILE}: checked ${LINENUM} lines"
448
+ done
449
+
450
+ # Cross-check en-GB vs en-US key consistency
451
+ GB_DIR=$(find . -path "*/language/en-GB" -type d -not -path "./.git/*" 2>/dev/null | head -1)
452
+ US_DIR=$(find . -path "*/language/en-US" -type d -not -path "./.git/*" 2>/dev/null | head -1)
453
+
454
+ if [ -n "$GB_DIR" ] && [ -n "$US_DIR" ]; then
455
+ for GB_FILE in "$GB_DIR"/*.ini; do
456
+ [ ! -f "$GB_FILE" ] && continue
457
+ FNAME=$(basename "$GB_FILE")
458
+ US_FILE="$US_DIR/$FNAME"
459
+ [ ! -f "$US_FILE" ] && continue
460
+
461
+ GB_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$GB_FILE" 2>/dev/null | sort)
462
+ US_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$US_FILE" 2>/dev/null | sort)
463
+
464
+ # Keys in en-GB but not en-US
465
+ MISSING_US=$(comm -23 <(echo "$GB_KEYS") <(echo "$US_KEYS"))
466
+ if [ -n "$MISSING_US" ]; then
467
+ echo "::warning::Keys in en-GB/$FNAME but missing from en-US/$FNAME:"
468
+ echo "$MISSING_US" | while read -r k; do echo " - $k"; done
469
+ WARNINGS=$((WARNINGS + 1))
470
+ fi
471
+
472
+ # Keys in en-US but not en-GB
473
+ MISSING_GB=$(comm -13 <(echo "$GB_KEYS") <(echo "$US_KEYS"))
474
+ if [ -n "$MISSING_GB" ]; then
475
+ echo "::warning::Keys in en-US/$FNAME but missing from en-GB/$FNAME:"
476
+ echo "$MISSING_GB" | while read -r k; do echo " - $k"; done
477
+ WARNINGS=$((WARNINGS + 1))
478
+ fi
479
+ done
480
+ fi
481
+
482
+ {
483
+ echo "### Language File Validation"
484
+ echo "| Metric | Count |"
485
+ echo "|---|---|"
486
+ echo "| Files checked | $(echo "$INI_FILES" | wc -l) |"
487
+ echo "| Errors | ${ERRORS} |"
488
+ echo "| Warnings | ${WARNINGS} |"
489
+ } >> $GITHUB_STEP_SUMMARY
490
+
491
+ if [ "$ERRORS" -gt 0 ]; then
492
+ echo "::error::Language validation failed with ${ERRORS} error(s)"
493
+ exit 1
494
+ fi
495
+ echo "Language files: OK (${WARNINGS} warning(s))"
496
+
497
+ - name: Check changelog has unreleased entry
498
+ run: |
499
+ if [ ! -f "CHANGELOG.md" ]; then
500
+ echo "::warning::No CHANGELOG.md found"
501
+ exit 0
502
+ fi
503
+ # Check for content under [Unreleased] section
504
+ if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then
505
+ echo "::error::CHANGELOG.md missing [Unreleased] section"
506
+ exit 1
507
+ fi
508
+ # Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased
509
+ UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true)
510
+ if [ "$UNRELEASED_CONTENT" -eq 0 ]; then
511
+ echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes."
512
+ echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY
513
+ echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY
514
+ echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY
515
+ exit 1
516
+ fi
517
+ echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]"
518
+
519
+ - name: Verify package source
520
+ run: |
521
+ SOURCE_DIR="src"
522
+ [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
523
+ if [ ! -d "$SOURCE_DIR" ]; then
524
+ echo "::warning::No src/ or htdocs/ directory"
525
+ exit 0
526
+ fi
527
+ FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
528
+ echo "Source: ${FILE_COUNT} files"
529
+ [ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; }
530
+
531
+ # ── Pre-Release RC Build ─────────────────────────────────────────────────
532
+ pre-release:
533
+ name: Build RC Package
534
+ 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' }}
539
+
540
+ steps:
541
+ - name: Trigger RC pre-release
542
+ env:
543
+ MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
544
+ REPO: ${{ github.repository }}
545
+ BRANCH: ${{ github.head_ref }}
546
+ MOKOGIT_URL: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
547
+ run: |
548
+ 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\"}}"
549
+ echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
550
+ echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
551
+
552
+ # ── Issue Reporter ──────────────────────────────────────────────────────
553
+ report-issues:
554
+ 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