@mokoconsulting/mcp-mokogitea-api 1.2.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 (86) hide show
  1. package/.gitattributes +94 -0
  2. package/.gitmessage +9 -0
  3. package/.mokogitea/ISSUE_TEMPLATE/adr.md +110 -0
  4. package/.mokogitea/ISSUE_TEMPLATE/bug_report.md +48 -0
  5. package/.mokogitea/ISSUE_TEMPLATE/config.yml +18 -0
  6. package/.mokogitea/ISSUE_TEMPLATE/documentation.md +52 -0
  7. package/.mokogitea/ISSUE_TEMPLATE/enterprise_support.md +85 -0
  8. package/.mokogitea/ISSUE_TEMPLATE/feature_request.md +51 -0
  9. package/.mokogitea/ISSUE_TEMPLATE/firewall-request.md +190 -0
  10. package/.mokogitea/ISSUE_TEMPLATE/mcp_api_integration.md +48 -0
  11. package/.mokogitea/ISSUE_TEMPLATE/mcp_connection_issue.md +67 -0
  12. package/.mokogitea/ISSUE_TEMPLATE/mcp_tool_request.md +49 -0
  13. package/.mokogitea/ISSUE_TEMPLATE/question.md +82 -0
  14. package/.mokogitea/ISSUE_TEMPLATE/rfc.md +126 -0
  15. package/.mokogitea/ISSUE_TEMPLATE/security.md +51 -0
  16. package/.mokogitea/ISSUE_TEMPLATE/version.md +24 -0
  17. package/.mokogitea/auto-assign.yml +76 -0
  18. package/.mokogitea/auto-dev-issue.yml +207 -0
  19. package/.mokogitea/auto-release.yml +337 -0
  20. package/.mokogitea/branch-protection.yml +251 -0
  21. package/.mokogitea/changelog-validation.yml +101 -0
  22. package/.mokogitea/codeql-analysis.yml +115 -0
  23. package/.mokogitea/copilot-agent.yml +44 -0
  24. package/.mokogitea/deploy-demo.yml +734 -0
  25. package/.mokogitea/deploy-dev.yml +700 -0
  26. package/.mokogitea/enterprise-firewall-setup.yml +758 -0
  27. package/.mokogitea/manifest.xml +25 -0
  28. package/.mokogitea/mcp-auto-release.yml +278 -0
  29. package/.mokogitea/mcp-build-test.yml +65 -0
  30. package/.mokogitea/mcp-sdk-check.yml +109 -0
  31. package/.mokogitea/mcp-tool-inventory.yml +61 -0
  32. package/.mokogitea/pr-branch-check.yml +90 -0
  33. package/.mokogitea/repository-cleanup.yml +525 -0
  34. package/.mokogitea/standards-compliance.yml +2614 -0
  35. package/.mokogitea/sync-version-on-merge.yml +133 -0
  36. package/.mokogitea/workflows/auto-assign.yml +76 -0
  37. package/.mokogitea/workflows/auto-bump.yml +66 -0
  38. package/.mokogitea/workflows/auto-dev-issue.yml +207 -0
  39. package/.mokogitea/workflows/auto-release.yml +341 -0
  40. package/.mokogitea/workflows/branch-cleanup.yml +48 -0
  41. package/.mokogitea/workflows/cascade-dev.yml +10 -0
  42. package/.mokogitea/workflows/changelog-validation.yml +101 -0
  43. package/.mokogitea/workflows/ci-generic.yml +204 -0
  44. package/.mokogitea/workflows/cleanup.yml +87 -0
  45. package/.mokogitea/workflows/codeql-analysis.yml +115 -0
  46. package/.mokogitea/workflows/copilot-agent.yml +44 -0
  47. package/.mokogitea/workflows/deploy-manual.yml +126 -0
  48. package/.mokogitea/workflows/enterprise-firewall-setup.yml +758 -0
  49. package/.mokogitea/workflows/gitleaks.yml +96 -0
  50. package/.mokogitea/workflows/issue-branch.yml +73 -0
  51. package/.mokogitea/workflows/mcp-auto-release.yml +280 -0
  52. package/.mokogitea/workflows/mcp-build-test.yml +65 -0
  53. package/.mokogitea/workflows/mcp-sdk-check.yml +109 -0
  54. package/.mokogitea/workflows/mcp-tool-inventory.yml +61 -0
  55. package/.mokogitea/workflows/notify.yml +70 -0
  56. package/.mokogitea/workflows/npm-publish.yml +51 -0
  57. package/.mokogitea/workflows/pr-check.yml +508 -0
  58. package/.mokogitea/workflows/pre-release.yml +11 -0
  59. package/.mokogitea/workflows/repo-health.yml +711 -0
  60. package/.mokogitea/workflows/repository-cleanup.yml +525 -0
  61. package/.mokogitea/workflows/security-audit.yml +82 -0
  62. package/.mokogitea/workflows/standards-compliance.yml +2614 -0
  63. package/.mokogitea/workflows/sync-version-on-merge.yml +130 -0
  64. package/.mokogitea/workflows/update-server.yml +312 -0
  65. package/CHANGELOG.md +145 -0
  66. package/CLAUDE.md +43 -0
  67. package/CONTRIBUTING.md +161 -0
  68. package/README.md +286 -0
  69. package/SECURITY.md +91 -0
  70. package/automation/ci-issue-reporter.sh +237 -0
  71. package/config.example.json +13 -0
  72. package/dist/client.d.ts +15 -0
  73. package/dist/client.js +104 -0
  74. package/dist/config.d.ts +4 -0
  75. package/dist/config.js +48 -0
  76. package/dist/index.d.ts +3 -0
  77. package/dist/index.js +1119 -0
  78. package/dist/types.d.ts +20 -0
  79. package/dist/types.js +16 -0
  80. package/package.json +34 -0
  81. package/scripts/setup.mjs +40 -0
  82. package/src/client.ts +120 -0
  83. package/src/config.ts +58 -0
  84. package/src/index.ts +1712 -0
  85. package/src/types.ts +37 -0
  86. package/tsconfig.json +19 -0
@@ -0,0 +1,711 @@
1
+ # ============================================================================
2
+ # Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
3
+ #
4
+ # This file is part of a Moko Consulting project.
5
+ #
6
+ # SPDX-License-Identifier: GPL-3.0-or-later
7
+ #
8
+ # FILE INFORMATION
9
+ # DEFGROUP: Gitea.Workflow
10
+ # INGROUP: moko-platform.Validation
11
+ # REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
12
+ # PATH: /templates/workflows/joomla/repo_health.yml.template
13
+ # VERSION: 09.23.00
14
+ # BRIEF: Enforces repository guardrails by validating scripts governance, tooling availability, and core repository health artifacts.
15
+ # ============================================================================
16
+
17
+ name: "Generic: Repo Health"
18
+
19
+ defaults:
20
+ run:
21
+ shell: bash
22
+
23
+ on:
24
+ workflow_dispatch:
25
+ inputs:
26
+ profile:
27
+ description: 'Validation profile: all, scripts, or repo'
28
+ required: true
29
+ default: all
30
+ type: choice
31
+ options:
32
+ - all
33
+ - scripts
34
+ - repo
35
+ pull_request:
36
+ push:
37
+
38
+ permissions:
39
+ contents: read
40
+
41
+ env:
42
+ # Scripts governance policy
43
+ SCRIPTS_REQUIRED_DIRS:
44
+ SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate
45
+
46
+ # Repo health policy
47
+ REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.mokogitea/workflows/
48
+ REPO_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore,README.md,docs/
49
+ REPO_DISALLOWED_DIRS:
50
+ REPO_DISALLOWED_FILES: TODO.md,todo.md
51
+
52
+ # Extended checks toggles
53
+ EXTENDED_CHECKS: "true"
54
+
55
+ # File / directory variables
56
+ DOCS_INDEX: docs/docs-index.md
57
+ SCRIPT_DIR: scripts
58
+ WORKFLOWS_DIR: .mokogitea/workflows
59
+ SHELLCHECK_PATTERN: '*.sh'
60
+ SPDX_FILE_GLOBS: '*.sh,*.php,*.js,*.ts,*.css,*.xml,*.yml,*.yaml'
61
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
62
+
63
+ jobs:
64
+ access_check:
65
+ name: Access control
66
+ runs-on: ubuntu-latest
67
+ timeout-minutes: 10
68
+ permissions:
69
+ contents: read
70
+
71
+ outputs:
72
+ allowed: ${{ steps.perm.outputs.allowed }}
73
+ permission: ${{ steps.perm.outputs.permission }}
74
+
75
+ steps:
76
+ - name: Check actor permission (admin only)
77
+ id: perm
78
+ env:
79
+ TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}
80
+ REPO: ${{ github.repository }}
81
+ ACTOR: ${{ github.actor }}
82
+ run: |
83
+ set -euo pipefail
84
+ ALLOWED=false
85
+ PERMISSION=unknown
86
+ METHOD=""
87
+
88
+ # Hardcoded authorized users — always allowed
89
+ case "$ACTOR" in
90
+ jmiller|gitea-actions[bot])
91
+ ALLOWED=true
92
+ PERMISSION=admin
93
+ METHOD="hardcoded allowlist"
94
+ ;;
95
+ *)
96
+ # Detect platform and check permissions via API
97
+ API_BASE="${GITHUB_API_URL:-${GITEA_API_URL:-https://api.github.com}}"
98
+ RESP=$(curl -sf -H "Authorization: token ${TOKEN}" \
99
+ "${API_BASE}/repos/${REPO}/collaborators/${ACTOR}/permission" 2>/dev/null || echo '{}')
100
+ PERMISSION=$(echo "$RESP" | grep -oP '"permission"\s*:\s*"\K[^"]+' || echo "unknown")
101
+ if [ "$PERMISSION" = "admin" ] || [ "$PERMISSION" = "maintain" ] || [ "$PERMISSION" = "owner" ]; then
102
+ ALLOWED=true
103
+ fi
104
+ METHOD="collaborator API"
105
+ ;;
106
+ esac
107
+
108
+ echo "permission=${PERMISSION}" >> "$GITHUB_OUTPUT"
109
+ echo "allowed=${ALLOWED}" >> "$GITHUB_OUTPUT"
110
+
111
+ {
112
+ echo "## Access Authorization"
113
+ echo ""
114
+ echo "| Field | Value |"
115
+ echo "|-------|-------|"
116
+ echo "| **Actor** | \`${ACTOR}\` |"
117
+ echo "| **Repository** | \`${REPO}\` |"
118
+ echo "| **Permission** | \`${PERMISSION}\` |"
119
+ echo "| **Method** | ${METHOD} |"
120
+ echo "| **Authorized** | ${ALLOWED} |"
121
+ echo ""
122
+ if [ "$ALLOWED" = "true" ]; then
123
+ echo "${ACTOR} authorized (${METHOD})"
124
+ else
125
+ echo "${ACTOR} is NOT authorized. Requires admin or maintain role."
126
+ fi
127
+ } >> "${GITHUB_STEP_SUMMARY}"
128
+
129
+ - name: Deny execution when not permitted
130
+ if: ${{ steps.perm.outputs.allowed != 'true' }}
131
+ run: |
132
+ set -euo pipefail
133
+ printf '%s\n' 'ERROR: Access denied. Admin permission required.' >> "${GITHUB_STEP_SUMMARY}"
134
+ exit 1
135
+
136
+ scripts_governance:
137
+ name: Scripts governance
138
+ needs: access_check
139
+ if: ${{ needs.access_check.outputs.allowed == 'true' }}
140
+ runs-on: ubuntu-latest
141
+ timeout-minutes: 15
142
+ permissions:
143
+ contents: read
144
+
145
+ steps:
146
+ - name: Checkout
147
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
148
+ with:
149
+ fetch-depth: 0
150
+
151
+ - name: Scripts folder checks
152
+ env:
153
+ PROFILE_RAW: ${{ github.event.inputs.profile }}
154
+ run: |
155
+ set -euo pipefail
156
+
157
+ profile="${PROFILE_RAW:-all}"
158
+ case "${profile}" in
159
+ all|scripts|repo) ;;
160
+ *)
161
+ printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
162
+ exit 1
163
+ ;;
164
+ esac
165
+
166
+ if [ "${profile}" = 'repo' ]; then
167
+ {
168
+ printf '%s\n' '### Scripts governance'
169
+ printf '%s\n' "Profile: ${profile}"
170
+ printf '%s\n' 'Status: SKIPPED'
171
+ printf '%s\n' 'Reason: profile excludes scripts governance'
172
+ printf '\n'
173
+ } >> "${GITHUB_STEP_SUMMARY}"
174
+ exit 0
175
+ fi
176
+
177
+ if [ ! -d "${SCRIPT_DIR}" ]; then
178
+ {
179
+ printf '%s\n' '### Scripts governance'
180
+ printf '%s\n' 'Status: OK (advisory)'
181
+ printf '%s\n' 'scripts/ directory not present. No scripts governance enforced.'
182
+ printf '\n'
183
+ } >> "${GITHUB_STEP_SUMMARY}"
184
+ exit 0
185
+ fi
186
+
187
+ if [ -n "${SCRIPTS_REQUIRED_DIRS:-}" ]; then IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}"; else required_dirs=(); fi
188
+ IFS=',' read -r -a allowed_dirs <<< "${SCRIPTS_ALLOWED_DIRS}"
189
+
190
+ missing_dirs=()
191
+ unapproved_dirs=()
192
+
193
+ for d in "${required_dirs[@]}"; do
194
+ req="${d%/}"
195
+ [ ! -d "${req}" ] && missing_dirs+=("${req}/")
196
+ done
197
+
198
+ while IFS= read -r d; do
199
+ allowed=false
200
+ for a in "${allowed_dirs[@]}"; do
201
+ a_norm="${a%/}"
202
+ [ "${d%/}" = "${a_norm}" ] && allowed=true
203
+ done
204
+ [ "${allowed}" = false ] && unapproved_dirs+=("${d%/}/")
205
+ done < <(find "${SCRIPT_DIR}" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sed 's#^\./##')
206
+
207
+ {
208
+ printf '%s\n' '### Scripts governance'
209
+ printf '%s\n' "Profile: ${profile}"
210
+ printf '%s\n' '| Area | Status | Notes |'
211
+ printf '%s\n' '|---|---|---|'
212
+
213
+ if [ "${#missing_dirs[@]}" -gt 0 ]; then
214
+ printf '%s\n' '| Required directories | Warning | Missing required subfolders |'
215
+ else
216
+ printf '%s\n' '| Required directories | OK | All required subfolders present |'
217
+ fi
218
+
219
+ if [ "${#unapproved_dirs[@]}" -gt 0 ]; then
220
+ printf '%s\n' '| Directory policy | Warning | Unapproved directories detected |'
221
+ else
222
+ printf '%s\n' '| Directory policy | OK | No unapproved directories |'
223
+ fi
224
+
225
+ printf '%s\n' '| Enforcement mode | Advisory | scripts folder is optional |'
226
+ printf '\n'
227
+
228
+ if [ "${#missing_dirs[@]}" -gt 0 ]; then
229
+ printf '%s\n' 'Missing required script directories:'
230
+ for m in "${missing_dirs[@]}"; do printf '%s\n' "- ${m}"; done
231
+ printf '\n'
232
+ else
233
+ printf '%s\n' 'Missing required script directories: none.'
234
+ printf '\n'
235
+ fi
236
+
237
+ if [ "${#unapproved_dirs[@]}" -gt 0 ]; then
238
+ printf '%s\n' 'Unapproved script directories detected:'
239
+ for m in "${unapproved_dirs[@]}"; do printf '%s\n' "- ${m}"; done
240
+ printf '\n'
241
+ else
242
+ printf '%s\n' 'Unapproved script directories detected: none.'
243
+ printf '\n'
244
+ fi
245
+
246
+ printf '%s\n' 'Scripts governance completed in advisory mode.'
247
+ printf '\n'
248
+ } >> "${GITHUB_STEP_SUMMARY}"
249
+
250
+ repo_health:
251
+ name: Repository health
252
+ needs: access_check
253
+ if: ${{ needs.access_check.outputs.allowed == 'true' }}
254
+ runs-on: ubuntu-latest
255
+ timeout-minutes: 20
256
+ permissions:
257
+ contents: read
258
+
259
+ steps:
260
+ - name: Checkout
261
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
262
+ with:
263
+ fetch-depth: 0
264
+
265
+ - name: Repository health checks
266
+ env:
267
+ PROFILE_RAW: ${{ github.event.inputs.profile }}
268
+ run: |
269
+ set -euo pipefail
270
+
271
+ profile="${PROFILE_RAW:-all}"
272
+ case "${profile}" in
273
+ all|scripts|repo) ;;
274
+ *)
275
+ printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
276
+ exit 1
277
+ ;;
278
+ esac
279
+
280
+ if [ "${profile}" = 'scripts' ]; then
281
+ {
282
+ printf '%s\n' '### Repository health'
283
+ printf '%s\n' "Profile: ${profile}"
284
+ printf '%s\n' 'Status: SKIPPED'
285
+ printf '%s\n' 'Reason: profile excludes repository health'
286
+ printf '\n'
287
+ } >> "${GITHUB_STEP_SUMMARY}"
288
+ exit 0
289
+ fi
290
+
291
+ IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}"
292
+ IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}"
293
+ if [ -n "${REPO_DISALLOWED_DIRS:-}" ]; then IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}"; else disallowed_dirs=(); fi
294
+ IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES:-}"
295
+
296
+ missing_required=()
297
+ missing_optional=()
298
+
299
+ # Source directory: src/ or htdocs/ (either is valid for extension repos)
300
+ SOURCE_DIR=""
301
+ if [ -d "src" ]; then
302
+ SOURCE_DIR="src"
303
+ elif [ -d "htdocs" ]; then
304
+ SOURCE_DIR="htdocs"
305
+ elif [ -d "deploy" ] || [ -d "cli" ] || [ -d "monitoring" ]; then
306
+ # Platform/tooling repos don't need src/
307
+ SOURCE_DIR=""
308
+ else
309
+ missing_required+=("src/ or htdocs/ (source directory required)")
310
+ fi
311
+
312
+ for item in "${required_artifacts[@]}"; do
313
+ if printf '%s' "${item}" | grep -q '/$'; then
314
+ d="${item%/}"
315
+ [ ! -d "${d}" ] && missing_required+=("${item}")
316
+ else
317
+ [ ! -f "${item}" ] && missing_required+=("${item}")
318
+ fi
319
+ done
320
+
321
+ for f in "${optional_files[@]}"; do
322
+ if printf '%s' "${f}" | grep -q '/$'; then
323
+ d="${f%/}"
324
+ [ ! -d "${d}" ] && missing_optional+=("${f}")
325
+ else
326
+ [ ! -f "${f}" ] && missing_optional+=("${f}")
327
+ fi
328
+ done
329
+
330
+ for d in "${disallowed_dirs[@]}"; do
331
+ d_norm="${d%/}"
332
+ [ -d "${d_norm}" ] && missing_required+=("${d_norm}/ (disallowed)")
333
+ done
334
+
335
+ for f in "${disallowed_files[@]}"; do
336
+ [ -f "${f}" ] && missing_required+=("${f} (disallowed)")
337
+ done
338
+
339
+ git fetch origin --prune
340
+
341
+ dev_paths=()
342
+ dev_branches=()
343
+
344
+ while IFS= read -r b; do
345
+ name="${b#origin/}"
346
+ if [ "${name}" = 'dev' ]; then
347
+ dev_branches+=("${name}")
348
+ else
349
+ dev_paths+=("${name}")
350
+ fi
351
+ done < <(git branch -r --list 'origin/dev*' | sed 's/^ *//')
352
+
353
+ if [ "${#dev_paths[@]}" -eq 0 ] && [ "${#dev_branches[@]}" -eq 0 ]; then
354
+ missing_required+=("dev or dev/* branch")
355
+ fi
356
+
357
+ content_warnings=()
358
+
359
+ if [ -f 'CHANGELOG.md' ] && ! grep -Eq '^# Changelog' CHANGELOG.md; then
360
+ content_warnings+=("CHANGELOG.md missing '# Changelog' header")
361
+ fi
362
+
363
+ if [ -f 'CHANGELOG.md' ] && grep -Eq '^[# ]*Unreleased' CHANGELOG.md; then
364
+ content_warnings+=("CHANGELOG.md contains Unreleased section (review release readiness)")
365
+ fi
366
+
367
+ if [ -f 'LICENSE' ] && ! grep -qiE 'GNU GENERAL PUBLIC LICENSE|GPL' LICENSE; then
368
+ content_warnings+=("LICENSE does not look like a GPL text")
369
+ fi
370
+
371
+ if [ -f 'README.md' ] && ! grep -qiE 'moko|Moko' README.md; then
372
+ content_warnings+=("README.md missing expected brand keyword")
373
+ fi
374
+
375
+ export PROFILE_RAW="${profile}"
376
+ export MISSING_REQUIRED="$(printf '%s\n' "${missing_required[@]:-}")"
377
+ export MISSING_OPTIONAL="$(printf '%s\n' "${missing_optional[@]:-}")"
378
+ export CONTENT_WARNINGS="$(printf '%s\n' "${content_warnings[@]:-}")"
379
+
380
+ report_json=$(printf '{"profile":"%s","missing_required":%d,"missing_optional":%d,"content_warnings":%d}' "$profile" "${#missing_required[@]}" "${#missing_optional[@]}" "${#content_warnings[@]}")
381
+
382
+ {
383
+ printf '%s\n' '### Repository health'
384
+ printf '%s\n' "Profile: ${profile}"
385
+ printf '%s\n' '| Metric | Value |'
386
+ printf '%s\n' '|---|---|'
387
+ printf '%s\n' "| Missing required | ${#missing_required[@]} |"
388
+ printf '%s\n' "| Missing optional | ${#missing_optional[@]} |"
389
+ printf '%s\n' "| Content warnings | ${#content_warnings[@]} |"
390
+ printf '\n'
391
+
392
+ printf '%s\n' '### Guardrails report (JSON)'
393
+ printf '%s\n' '```json'
394
+ printf '%s\n' "${report_json}"
395
+ printf '%s\n' '```'
396
+ printf '\n'
397
+ } >> "${GITHUB_STEP_SUMMARY}"
398
+
399
+ if [ "${#missing_required[@]}" -gt 0 ]; then
400
+ {
401
+ printf '%s\n' '### Missing required repo artifacts'
402
+ for m in "${missing_required[@]}"; do printf '%s\n' "- ${m}"; done
403
+ printf '%s\n' 'ERROR: Guardrails failed. Missing required repository artifacts.'
404
+ printf '\n'
405
+ } >> "${GITHUB_STEP_SUMMARY}"
406
+ exit 1
407
+ fi
408
+
409
+ if [ "${#missing_optional[@]}" -gt 0 ]; then
410
+ {
411
+ printf '%s\n' '### Missing optional repo artifacts'
412
+ for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done
413
+ printf '\n'
414
+ } >> "${GITHUB_STEP_SUMMARY}"
415
+ fi
416
+
417
+ if [ "${#content_warnings[@]}" -gt 0 ]; then
418
+ {
419
+ printf '%s\n' '### Repo content warnings'
420
+ for m in "${content_warnings[@]}"; do printf '%s\n' "- ${m}"; done
421
+ printf '\n'
422
+ } >> "${GITHUB_STEP_SUMMARY}"
423
+ fi
424
+
425
+ # -- Joomla-specific checks --
426
+ joomla_findings=()
427
+
428
+ MANIFEST="$(find . -maxdepth 2 -name '*.xml' -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true)"
429
+ if [ -z "${MANIFEST}" ]; then
430
+ joomla_findings+=("Joomla XML manifest not found (no *.xml with <extension> tag)")
431
+ else
432
+ if ! grep -qP '<version>' "${MANIFEST}"; then
433
+ joomla_findings+=("XML manifest: <version> tag missing")
434
+ fi
435
+ if ! grep -qP 'type="(component|module|plugin|library|package|template|language)"' "${MANIFEST}"; then
436
+ joomla_findings+=("XML manifest: type attribute missing or invalid")
437
+ fi
438
+ if ! grep -qP '<name>' "${MANIFEST}"; then
439
+ joomla_findings+=("XML manifest: <name> tag missing")
440
+ fi
441
+ if ! grep -qP '<author>' "${MANIFEST}"; then
442
+ joomla_findings+=("XML manifest: <author> tag missing")
443
+ fi
444
+ if ! grep -qP '<namespace' "${MANIFEST}"; then
445
+ joomla_findings+=("XML manifest: <namespace> missing (required for Joomla 5+)")
446
+ fi
447
+ fi
448
+
449
+ INI_COUNT="$(find . -name '*.ini' -type f 2>/dev/null | wc -l)"
450
+ if [ "${INI_COUNT}" -eq 0 ]; then
451
+ joomla_findings+=("No .ini language files found")
452
+ fi
453
+
454
+ if [ ! -f 'updates.xml' ]; then
455
+ joomla_findings+=("updates.xml missing in root (required for Joomla update server)")
456
+ fi
457
+
458
+ if [ -n "${SOURCE_DIR}" ]; then
459
+ INDEX_DIRS=("${SOURCE_DIR}" "${SOURCE_DIR}/admin" "${SOURCE_DIR}/site")
460
+ for dir in "${INDEX_DIRS[@]}"; do
461
+ if [ -d "${dir}" ] && [ ! -f "${dir}/index.html" ]; then
462
+ joomla_findings+=("${dir}/index.html missing (directory listing protection)")
463
+ fi
464
+ done
465
+ fi
466
+
467
+ if [ "${#joomla_findings[@]}" -gt 0 ]; then
468
+ {
469
+ printf '%s\n' '### Joomla extension checks'
470
+ printf '%s\n' '| Check | Status |'
471
+ printf '%s\n' '|---|---|'
472
+ for f in "${joomla_findings[@]}"; do
473
+ printf '%s\n' "| ${f} | Warning |"
474
+ done
475
+ printf '\n'
476
+ } >> "${GITHUB_STEP_SUMMARY}"
477
+ else
478
+ {
479
+ printf '%s\n' '### Joomla extension checks'
480
+ printf '%s\n' 'All Joomla-specific checks passed.'
481
+ printf '\n'
482
+ } >> "${GITHUB_STEP_SUMMARY}"
483
+ fi
484
+
485
+ extended_enabled="${EXTENDED_CHECKS:-true}"
486
+ extended_findings=()
487
+
488
+ if [ "${extended_enabled}" = 'true' ]; then
489
+ if [ -f '.github/CODEOWNERS' ] || [ -f 'CODEOWNERS' ] || [ -f 'docs/CODEOWNERS' ]; then
490
+ :
491
+ else
492
+ extended_findings+=("CODEOWNERS not found (.github/CODEOWNERS preferred)")
493
+ fi
494
+
495
+ if ls "${WORKFLOWS_DIR}"/*.yml >/dev/null 2>&1 || ls "${WORKFLOWS_DIR}"/*.yaml >/dev/null 2>&1; then
496
+ bad_refs="$(grep -RIn --include='*.yml' --include='*.yaml' -E '^[[:space:]]*uses:[[:space:]]*[^#]+@(main|master)\b' "${WORKFLOWS_DIR}" 2>/dev/null || true)"
497
+ if [ -n "${bad_refs}" ]; then
498
+ extended_findings+=("Workflows reference actions @main/@master (pin versions): see log excerpt")
499
+ {
500
+ printf '%s\n' '### Workflow pinning advisory'
501
+ printf '%s\n' 'Found uses: entries pinned to main/master:'
502
+ printf '%s\n' '```'
503
+ printf '%s\n' "${bad_refs}"
504
+ printf '%s\n' '```'
505
+ printf '\n'
506
+ } >> "${GITHUB_STEP_SUMMARY}"
507
+ fi
508
+ fi
509
+
510
+ if [ -f "${DOCS_INDEX}" ]; then
511
+ missing_links=""
512
+ while IFS= read -r docline; do
513
+ for link in $(echo "$docline" | grep -oE '\]\([^)]+\)' | sed 's/\](//' | sed 's/)$//' || true); do
514
+ case "$link" in http://*|https://*|"#"*|mailto:*) continue ;; esac
515
+ linkpath="${link%%#*}"
516
+ linkpath="${linkpath%%\?*}"
517
+ [ -z "$linkpath" ] && continue
518
+ if [ "${linkpath:0:1}" = "/" ]; then
519
+ testpath="${linkpath#/}"
520
+ else
521
+ testpath="$(dirname "${DOCS_INDEX}")/${linkpath}"
522
+ fi
523
+ [ ! -e "$testpath" ] && missing_links="${missing_links}${testpath} "
524
+ done
525
+ done < "${DOCS_INDEX}"
526
+ if [ -n "${missing_links}" ]; then
527
+ extended_findings+=("docs/docs-index.md contains broken relative links")
528
+ {
529
+ printf '%s\n' '### Docs index link integrity'
530
+ printf '%s\n' 'Broken relative links:'
531
+ for bl in ${missing_links}; do
532
+ printf '%s\n' "- ${bl}"
533
+ done
534
+ printf '\n'
535
+ } >> "${GITHUB_STEP_SUMMARY}"
536
+ fi
537
+ fi
538
+
539
+ if [ -d "${SCRIPT_DIR}" ]; then
540
+ if ! command -v shellcheck >/dev/null 2>&1; then
541
+ sudo apt-get update -qq
542
+ sudo apt-get install -y shellcheck >/dev/null
543
+ fi
544
+
545
+ sc_out=''
546
+ while IFS= read -r shf; do
547
+ [ -z "${shf}" ] && continue
548
+ out_one="$(shellcheck -S warning -x "${shf}" 2>/dev/null || true)"
549
+ if [ -n "${out_one}" ]; then
550
+ sc_out="${sc_out}${out_one}\n"
551
+ fi
552
+ done < <(find "${SCRIPT_DIR}" -type f -name "${SHELLCHECK_PATTERN}" 2>/dev/null | sort)
553
+
554
+ if [ -n "${sc_out}" ]; then
555
+ extended_findings+=("ShellCheck warnings detected (advisory)")
556
+ sc_head="$(printf '%s' "${sc_out}" | head -n 200)"
557
+ {
558
+ printf '%s\n' '### ShellCheck (advisory)'
559
+ printf '%s\n' '```'
560
+ printf '%s\n' "${sc_head}"
561
+ printf '%s\n' '```'
562
+ printf '\n'
563
+ } >> "${GITHUB_STEP_SUMMARY}"
564
+ fi
565
+ fi
566
+
567
+ spdx_missing=()
568
+ IFS=',' read -r -a spdx_globs <<< "${SPDX_FILE_GLOBS}"
569
+ spdx_args=()
570
+ for g in "${spdx_globs[@]}"; do spdx_args+=("${g}"); done
571
+
572
+ while IFS= read -r f; do
573
+ [ -z "${f}" ] && continue
574
+ if ! head -n 40 "${f}" | grep -q 'SPDX-License-Identifier:'; then
575
+ spdx_missing+=("${f}")
576
+ fi
577
+ done < <(git ls-files "${spdx_args[@]}" 2>/dev/null || true)
578
+
579
+ if [ "${#spdx_missing[@]}" -gt 0 ]; then
580
+ extended_findings+=("SPDX header missing in some tracked files (advisory)")
581
+ {
582
+ printf '%s\n' '### SPDX header advisory'
583
+ printf '%s\n' 'Files missing SPDX-License-Identifier (first 40 lines scan):'
584
+ for f in "${spdx_missing[@]}"; do printf '%s\n' "- ${f}"; done
585
+ printf '\n'
586
+ } >> "${GITHUB_STEP_SUMMARY}"
587
+ fi
588
+
589
+ stale_cutoff_days=180
590
+ stale_branches="$(git for-each-ref --format='%(refname:short) %(committerdate:unix)' refs/remotes/origin 2>/dev/null | awk -v now="$(date +%s)" -v days="${stale_cutoff_days}" '{if (now-$2 > days*86400) print $1}' | head -50)"
591
+ if [ -n "${stale_branches}" ]; then
592
+ extended_findings+=("Stale remote branches detected (advisory)")
593
+ {
594
+ printf '%s\n' '### Git hygiene advisory'
595
+ printf '%s\n' "Branches with last commit older than ${stale_cutoff_days} days (sample up to 50):"
596
+ while IFS= read -r b; do [ -n "${b}" ] && printf '%s\n' "- ${b}"; done <<< "${stale_branches}"
597
+ printf '\n'
598
+ } >> "${GITHUB_STEP_SUMMARY}"
599
+ fi
600
+ fi
601
+
602
+ {
603
+ printf '%s\n' '### Guardrails coverage matrix'
604
+ printf '%s\n' '| Domain | Status | Notes |'
605
+ printf '%s\n' '|---|---|---|'
606
+ printf '%s\n' '| Access control | OK | Admin-only execution gate |'
607
+ printf '%s\n' '| Release policy | N/A | Releases handled by MokoGitea |'
608
+ printf '%s\n' '| Scripts governance | OK | Directory policy and advisory reporting |'
609
+ printf '%s\n' '| Repo required artifacts | OK | Required, optional, disallowed enforcement |'
610
+ printf '%s\n' '| Repo content heuristics | OK | Brand, license, changelog structure |'
611
+ if [ "${extended_enabled}" = 'true' ]; then
612
+ if [ "${#extended_findings[@]}" -gt 0 ]; then
613
+ printf '%s\n' '| Extended checks | Warning | See extended findings below |'
614
+ else
615
+ printf '%s\n' '| Extended checks | OK | No findings |'
616
+ fi
617
+ else
618
+ printf '%s\n' '| Extended checks | SKIPPED | EXTENDED_CHECKS disabled |'
619
+ fi
620
+ printf '\n'
621
+ } >> "${GITHUB_STEP_SUMMARY}"
622
+
623
+ if [ "${extended_enabled}" = 'true' ] && [ "${#extended_findings[@]}" -gt 0 ]; then
624
+ {
625
+ printf '%s\n' '### Extended findings (advisory)'
626
+ for f in "${extended_findings[@]}"; do printf '%s\n' "- ${f}"; done
627
+ printf '\n'
628
+ } >> "${GITHUB_STEP_SUMMARY}"
629
+ fi
630
+
631
+ printf '%s\n' 'Repository health guardrails passed.' >> "${GITHUB_STEP_SUMMARY}"
632
+
633
+
634
+ site-health:
635
+ name: Site Health
636
+ runs-on: ubuntu-latest
637
+ if: github.event_name == 'workflow_dispatch'
638
+ steps:
639
+ - uses: actions/checkout@v4
640
+
641
+ - name: Setup PHP
642
+ uses: shivammathur/setup-php@v2
643
+ with:
644
+ php-version: '8.3'
645
+
646
+ - name: Uptime check
647
+ if: env.URLS != ''
648
+ run: |
649
+ echo "$URLS" > /tmp/urls.txt
650
+ php monitoring/uptime-probe.php --urls /tmp/urls.txt --timeout 15 || echo "::warning::Some sites are down"
651
+ rm -f /tmp/urls.txt
652
+ env:
653
+ URLS: ${{ vars.MONITORED_URLS }}
654
+
655
+ - name: SSL certificate check
656
+ if: env.DOMAINS != ''
657
+ run: |
658
+ echo "$DOMAINS" > /tmp/domains.txt
659
+ php monitoring/ssl-check.php --domains /tmp/domains.txt --warn-days 30 || echo "::warning::SSL certificates expiring soon"
660
+ rm -f /tmp/domains.txt
661
+ env:
662
+ DOMAINS: ${{ vars.MONITORED_DOMAINS }}
663
+
664
+ - name: Summary
665
+ if: always()
666
+ run: |
667
+ echo "### Site Health" >> $GITHUB_STEP_SUMMARY
668
+ echo "Uptime and SSL checks completed." >> $GITHUB_STEP_SUMMARY
669
+
670
+ # ═══════════════════════════════════════════════════════════════════════
671
+ # Issue Reporter — file issues for failed gates
672
+ # ═══════════════════════════════════════════════════════════════════════
673
+ report-issues:
674
+ name: "Report Issues"
675
+ runs-on: ubuntu-latest
676
+ needs: [access_check, scripts_governance, repo_health]
677
+ if: >-
678
+ always() &&
679
+ (needs.scripts_governance.result == 'failure' ||
680
+ needs.repo_health.result == 'failure')
681
+
682
+ steps:
683
+ - name: Checkout
684
+ uses: actions/checkout@v4
685
+ with:
686
+ sparse-checkout: automation/ci-issue-reporter.sh
687
+ sparse-checkout-cone-mode: false
688
+
689
+ - name: "File issues for failed gates"
690
+ env:
691
+ GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
692
+ GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
693
+ run: |
694
+ chmod +x automation/ci-issue-reporter.sh
695
+ REPORTER="./automation/ci-issue-reporter.sh"
696
+ WF="Repo Health"
697
+
698
+ report_gate() {
699
+ local gate="$1" result="$2" details="$3"
700
+ if [ "$result" = "failure" ]; then
701
+ "$REPORTER" --gate "$gate" --details "$details" --workflow "$WF" --severity error
702
+ fi
703
+ }
704
+
705
+ report_gate "Scripts Governance" \
706
+ "${{ needs.scripts_governance.result }}" \
707
+ "Scripts directory policy violations detected. Review required and allowed directories."
708
+
709
+ report_gate "Repository Health" \
710
+ "${{ needs.repo_health.result }}" \
711
+ "Repository health checks failed — missing required artifacts, disallowed files, or content warnings. Check the CI run summary."