@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,70 @@
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: Gitea.Workflow
7
+ # INGROUP: MokoStandards.Notifications
8
+ # REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
9
+ # PATH: /.gitea/workflows/notify.yml
10
+ # VERSION: 01.00.00
11
+ # BRIEF: Push notifications via ntfy on release success or workflow failure
12
+
13
+ name: "Universal: Notifications"
14
+
15
+ on:
16
+ workflow_run:
17
+ workflows:
18
+ - "Joomla Build & Release"
19
+ - "Joomla Extension CI"
20
+ - "Deploy"
21
+ types:
22
+ - completed
23
+
24
+ permissions:
25
+ contents: read
26
+
27
+ env:
28
+ NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
29
+ NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-releases' }}
30
+
31
+ jobs:
32
+ notify:
33
+ name: Send Notification
34
+ runs-on: ubuntu-latest
35
+ if: >-
36
+ github.event.workflow_run.conclusion == 'success' ||
37
+ github.event.workflow_run.conclusion == 'failure'
38
+
39
+ steps:
40
+ - name: Notify on success (releases only)
41
+ if: >-
42
+ github.event.workflow_run.conclusion == 'success' &&
43
+ contains(github.event.workflow_run.name, 'Release')
44
+ run: |
45
+ REPO="${{ github.event.repository.name }}"
46
+ WORKFLOW="${{ github.event.workflow_run.name }}"
47
+ URL="${{ github.event.workflow_run.html_url }}"
48
+
49
+ curl -sS \
50
+ -H "Title: ${REPO} released" \
51
+ -H "Tags: white_check_mark,package" \
52
+ -H "Priority: default" \
53
+ -H "Click: ${URL}" \
54
+ -d "${WORKFLOW} completed successfully." \
55
+ "${NTFY_URL}/${NTFY_TOPIC}"
56
+
57
+ - name: Notify on failure
58
+ if: github.event.workflow_run.conclusion == 'failure'
59
+ run: |
60
+ REPO="${{ github.event.repository.name }}"
61
+ WORKFLOW="${{ github.event.workflow_run.name }}"
62
+ URL="${{ github.event.workflow_run.html_url }}"
63
+
64
+ curl -sS \
65
+ -H "Title: ${REPO} workflow failed" \
66
+ -H "Tags: x,warning" \
67
+ -H "Priority: high" \
68
+ -H "Click: ${URL}" \
69
+ -d "${WORKFLOW} failed. Check the run for details." \
70
+ "${NTFY_URL}/${NTFY_TOPIC}"
@@ -0,0 +1,51 @@
1
+ # Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
2
+ # SPDX-License-Identifier: GPL-3.0-or-later
3
+
4
+ name: "Publish to npm"
5
+
6
+ on:
7
+ push:
8
+ branches:
9
+ - main
10
+ workflow_dispatch:
11
+
12
+ jobs:
13
+ publish:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - name: Checkout
17
+ uses: actions/checkout@v4
18
+
19
+ - name: Setup Node.js
20
+ uses: actions/setup-node@v4
21
+ with:
22
+ node-version: '20'
23
+ registry-url: 'https://registry.npmjs.org'
24
+
25
+ - name: Install dependencies
26
+ run: npm install
27
+
28
+ - name: Build
29
+ run: npm run build
30
+
31
+ - name: Check if version changed
32
+ id: version
33
+ run: |
34
+ PKG_NAME=$(node -p "require('./package.json').name")
35
+ PKG_VERSION=$(node -p "require('./package.json').version")
36
+ PUBLISHED=$(npm view "${PKG_NAME}@${PKG_VERSION}" version 2>/dev/null || echo "")
37
+ if [ "$PUBLISHED" = "$PKG_VERSION" ]; then
38
+ echo "skip=true" >> "$GITHUB_OUTPUT"
39
+ echo "Version ${PKG_VERSION} already published, skipping."
40
+ else
41
+ echo "skip=false" >> "$GITHUB_OUTPUT"
42
+ echo "Publishing ${PKG_NAME}@${PKG_VERSION}"
43
+ fi
44
+ env:
45
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
46
+
47
+ - name: Publish
48
+ if: steps.version.outputs.skip != 'true'
49
+ run: npm publish --access public
50
+ env:
51
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -0,0 +1,508 @@
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: Gitea.Workflow
7
+ # INGROUP: moko-platform.CI
8
+ # REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
9
+ # PATH: /templates/workflows/universal/pr-check.yml.template
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" ]; then
51
+ ALLOWED=false
52
+ REASON="Fix branches must target 'dev', not '${BASE}'"
53
+ fi
54
+ ;;
55
+ patch/*)
56
+ if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then
57
+ ALLOWED=false
58
+ REASON="Patch branches must target 'dev' or 'rc', 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\`" >> $GITHUB_STEP_SUMMARY
90
+ echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
91
+ echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
92
+ echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
93
+ exit 1
94
+ fi
95
+
96
+ echo "Branch policy: OK (${HEAD} → ${BASE})"
97
+ echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
98
+
99
+ # ── Code Validation ────────────────────────────────────────────────────
100
+ validate:
101
+ name: Validate PR
102
+ runs-on: ubuntu-latest
103
+
104
+ steps:
105
+ - name: Checkout
106
+ uses: actions/checkout@v4
107
+
108
+ - name: Check for merge conflict markers
109
+ run: |
110
+ CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --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)
111
+ if [ -n "$CONFLICTS" ]; then
112
+ echo "::error::Merge conflict markers found in source files"
113
+ echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY
114
+ echo '```' >> $GITHUB_STEP_SUMMARY
115
+ echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
116
+ echo '```' >> $GITHUB_STEP_SUMMARY
117
+ exit 1
118
+ fi
119
+ echo "No conflict markers found"
120
+
121
+ - name: Detect platform
122
+ id: platform
123
+ run: |
124
+ # Read platform from XML manifest (<platform> tag) or plain text fallback
125
+ PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1)
126
+ [ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]')
127
+ [ -z "$PLATFORM" ] && PLATFORM="generic"
128
+ echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
129
+
130
+ - name: Setup PHP
131
+ if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
132
+ run: |
133
+ if ! command -v php &> /dev/null; then
134
+ sudo apt-get update -qq
135
+ sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
136
+ fi
137
+
138
+ - name: PHP syntax check
139
+ if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
140
+ run: |
141
+ ERRORS=0
142
+ while IFS= read -r -d '' file; do
143
+ if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
144
+ ERRORS=$((ERRORS + 1))
145
+ fi
146
+ done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0)
147
+ echo "PHP lint: ${ERRORS} error(s)"
148
+ [ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
149
+
150
+ - name: Joomla JEXEC guard check
151
+ if: steps.platform.outputs.platform == 'joomla'
152
+ run: |
153
+ ERRORS=0
154
+ while IFS= read -r -d '' file; do
155
+ # Skip vendor, node_modules, and index.html stub files
156
+ case "$file" in ./vendor/*|./node_modules/*) continue ;; esac
157
+ # Check first 10 lines for JEXEC or JPATH guard
158
+ if ! head -20 "$file" | grep -qE "defined\s*\(\s*['\"](_JEXEC|JPATH_BASE|\\\\JPATH_PLATFORM)['\"]"; then
159
+ echo "::error file=${file}::Missing JEXEC guard: ${file}"
160
+ ERRORS=$((ERRORS + 1))
161
+ fi
162
+ done < <(find . -name "*.php" -path "*/src/*" -not -path "./.git/*" -not -path "./vendor/*" -print0)
163
+ if [ "$ERRORS" -gt 0 ]; then
164
+ echo "::error::${ERRORS} PHP file(s) missing defined('_JEXEC') or die guard"
165
+ echo "## JEXEC Guard Check: Failed" >> $GITHUB_STEP_SUMMARY
166
+ echo "${ERRORS} file(s) in src/ are missing the Joomla execution guard." >> $GITHUB_STEP_SUMMARY
167
+ exit 1
168
+ fi
169
+ echo "JEXEC guard: OK"
170
+
171
+ - name: Joomla directory listing protection
172
+ if: steps.platform.outputs.platform == 'joomla'
173
+ run: |
174
+ MISSING=0
175
+ SOURCE_DIR="src"
176
+ [ ! -d "$SOURCE_DIR" ] && exit 0
177
+ while IFS= read -r dir; do
178
+ if [ ! -f "${dir}/index.html" ]; then
179
+ echo "::warning::Missing index.html in ${dir} (directory listing protection)"
180
+ MISSING=$((MISSING + 1))
181
+ fi
182
+ done < <(find "$SOURCE_DIR" -type d -not -path "./.git/*" -not -path "*/vendor/*" -not -path "*/node_modules/*")
183
+ if [ "$MISSING" -gt 0 ]; then
184
+ echo "## Directory Protection" >> $GITHUB_STEP_SUMMARY
185
+ echo "${MISSING} director(ies) missing index.html" >> $GITHUB_STEP_SUMMARY
186
+ fi
187
+ echo "Directory protection: ${MISSING} missing (advisory)"
188
+
189
+ - name: Joomla script file and asset checks
190
+ if: steps.platform.outputs.platform == 'joomla'
191
+ run: |
192
+ ERRORS=0
193
+ MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
194
+ [ -z "$MANIFEST" ] && exit 0
195
+ MANIFEST_DIR=$(dirname "$MANIFEST")
196
+
197
+ # Check scriptfile exists if declared
198
+ SCRIPTFILE=$(sed -n 's/.*<scriptfile>\([^<]*\)<\/scriptfile>.*/\1/p' "$MANIFEST" 2>/dev/null)
199
+ if [ -n "$SCRIPTFILE" ]; then
200
+ if [ ! -f "${MANIFEST_DIR}/${SCRIPTFILE}" ]; then
201
+ echo "::error::Manifest declares <scriptfile>${SCRIPTFILE}</scriptfile> but file not found at ${MANIFEST_DIR}/${SCRIPTFILE}"
202
+ ERRORS=$((ERRORS + 1))
203
+ else
204
+ echo "Script file: ${MANIFEST_DIR}/${SCRIPTFILE} (OK)"
205
+ fi
206
+ fi
207
+
208
+ # Require joomla.asset.json and validate it
209
+ ASSET_JSON=$(find "$MANIFEST_DIR" -name "joomla.asset.json" -not -path "./.git/*" 2>/dev/null | head -1)
210
+ if [ -z "$ASSET_JSON" ]; then
211
+ echo "::error::joomla.asset.json not found — Joomla asset system is required"
212
+ ERRORS=$((ERRORS + 1))
213
+ else
214
+ if command -v php &> /dev/null; then
215
+ 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 || {
216
+ echo "::error::joomla.asset.json is not valid JSON"
217
+ ERRORS=$((ERRORS + 1))
218
+ }
219
+ fi
220
+ echo "joomla.asset.json: valid"
221
+ fi
222
+
223
+ # Validate all XML files in src/ are well-formed
224
+ XML_ERRORS=0
225
+ if command -v php &> /dev/null; then
226
+ while IFS= read -r -d '' xmlfile; do
227
+ 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
228
+ XML_ERRORS=$((XML_ERRORS + 1))
229
+ fi
230
+ done < <(find "$MANIFEST_DIR" -name "*.xml" -not -path "./.git/*" -print0)
231
+ fi
232
+ if [ "$XML_ERRORS" -gt 0 ]; then
233
+ echo "::error::${XML_ERRORS} XML file(s) are malformed"
234
+ ERRORS=$((ERRORS + 1))
235
+ else
236
+ echo "XML well-formedness: OK"
237
+ fi
238
+
239
+ [ "$ERRORS" -gt 0 ] && exit 1
240
+ echo "Joomla asset checks: OK"
241
+
242
+ - name: Validate platform manifest
243
+ run: |
244
+ PLATFORM="${{ steps.platform.outputs.platform }}"
245
+ case "$PLATFORM" in
246
+ joomla)
247
+ MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
248
+ if [ -z "$MANIFEST" ]; then
249
+ echo "::warning::No Joomla manifest found (WaaS site)"
250
+ exit 0
251
+ fi
252
+ echo "Manifest: ${MANIFEST}"
253
+ if command -v php &> /dev/null; then
254
+ 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; }
255
+ fi
256
+ for ELEMENT in name version description; do
257
+ grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; }
258
+ done
259
+ # Block legacy raw/branch update server URLs on MokoGitea
260
+ RAW_URLS=$(grep -n 'raw/branch' "$MANIFEST" | grep -i 'mokoconsulting\|mokogitea\|git\.mokoconsulting\.tech' || true)
261
+ if [ -n "$RAW_URLS" ]; then
262
+ echo "::error::Manifest contains legacy raw/branch update server URL on MokoGitea. Use the Gitea Pages URL instead (e.g. /{REPO}/updates.xml not /{REPO}/raw/branch/main/updates.xml)"
263
+ echo "$RAW_URLS"
264
+ exit 1
265
+ fi
266
+ echo "Joomla manifest valid"
267
+ ;;
268
+ dolibarr)
269
+ MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1)
270
+ if [ -z "$MOD_FILE" ]; then
271
+ echo "::error::No mod*.class.php found"
272
+ exit 1
273
+ fi
274
+ echo "Dolibarr module: ${MOD_FILE}"
275
+ ;;
276
+ *)
277
+ echo "Generic platform — no manifest validation"
278
+ ;;
279
+ esac
280
+
281
+ - name: Check update stream format
282
+ run: |
283
+ PLATFORM="${{ steps.platform.outputs.platform }}"
284
+ case "$PLATFORM" in
285
+ joomla)
286
+ if [ -f "updates.xml" ]; then
287
+ if command -v php &> /dev/null; then
288
+ 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; }
289
+ fi
290
+ echo "updates.xml valid"
291
+ fi
292
+ ;;
293
+ dolibarr)
294
+ [ -f "update.txt" ] && echo "update.txt present" || echo "::warning::No update.txt"
295
+ ;;
296
+ esac
297
+
298
+ - name: Validate Joomla language files
299
+ if: steps.platform.outputs.platform == 'joomla'
300
+ run: |
301
+ ERRORS=0
302
+ WARNINGS=0
303
+
304
+ # Require both en-GB and en-US language directories
305
+ LANG_ROOT=$(find . -path "*/language" -type d -not -path "./.git/*" 2>/dev/null | head -1)
306
+ if [ -z "$LANG_ROOT" ]; then
307
+ echo "No language/ directory found — skipping"
308
+ exit 0
309
+ fi
310
+
311
+ if [ ! -d "$LANG_ROOT/en-GB" ]; then
312
+ echo "::error::Missing en-GB language directory (${LANG_ROOT}/en-GB)"
313
+ ERRORS=$((ERRORS + 1))
314
+ fi
315
+ if [ ! -d "$LANG_ROOT/en-US" ]; then
316
+ echo "::error::Missing en-US language directory (${LANG_ROOT}/en-US)"
317
+ ERRORS=$((ERRORS + 1))
318
+ fi
319
+
320
+ # Check that en-GB and en-US have matching .ini files
321
+ if [ -d "$LANG_ROOT/en-GB" ] && [ -d "$LANG_ROOT/en-US" ]; then
322
+ for GB_INI in "$LANG_ROOT/en-GB"/*.ini; do
323
+ [ ! -f "$GB_INI" ] && continue
324
+ US_INI="$LANG_ROOT/en-US/$(basename "$GB_INI")"
325
+ if [ ! -f "$US_INI" ]; then
326
+ echo "::error::$(basename "$GB_INI") exists in en-GB but missing from en-US"
327
+ ERRORS=$((ERRORS + 1))
328
+ fi
329
+ done
330
+ for US_INI in "$LANG_ROOT/en-US"/*.ini; do
331
+ [ ! -f "$US_INI" ] && continue
332
+ GB_INI="$LANG_ROOT/en-GB/$(basename "$US_INI")"
333
+ if [ ! -f "$GB_INI" ]; then
334
+ echo "::error::$(basename "$US_INI") exists in en-US but missing from en-GB"
335
+ ERRORS=$((ERRORS + 1))
336
+ fi
337
+ done
338
+ fi
339
+
340
+ # Find all .ini language files
341
+ INI_FILES=$(find . -path "*/language/*/*.ini" -not -path "./.git/*" 2>/dev/null)
342
+ if [ -z "$INI_FILES" ]; then
343
+ echo "No .ini language files found"
344
+ [ "$ERRORS" -gt 0 ] && exit 1
345
+ exit 0
346
+ fi
347
+
348
+ echo "Found $(echo "$INI_FILES" | wc -l) language file(s)"
349
+
350
+ for FILE in $INI_FILES; do
351
+ FNAME=$(basename "$FILE")
352
+ LINENUM=0
353
+ SEEN_KEYS=""
354
+
355
+ while IFS= read -r line || [ -n "$line" ]; do
356
+ LINENUM=$((LINENUM + 1))
357
+
358
+ # Skip empty lines and comments
359
+ [ -z "$line" ] && continue
360
+ echo "$line" | grep -qE '^\s*;' && continue
361
+ echo "$line" | grep -qE '^\s*$' && continue
362
+
363
+ # Must match KEY="VALUE" format
364
+ if ! echo "$line" | grep -qE '^[A-Z_][A-Z0-9_]*=".*"$'; then
365
+ echo "::error file=${FILE},line=${LINENUM}::Malformed line: ${line}"
366
+ ERRORS=$((ERRORS + 1))
367
+ continue
368
+ fi
369
+
370
+ # Extract key and check for duplicates
371
+ KEY=$(echo "$line" | sed 's/=.*//')
372
+ if echo "$SEEN_KEYS" | grep -qx "$KEY"; then
373
+ echo "::error file=${FILE},line=${LINENUM}::Duplicate key: ${KEY}"
374
+ ERRORS=$((ERRORS + 1))
375
+ fi
376
+ SEEN_KEYS="${SEEN_KEYS}
377
+ ${KEY}"
378
+ done < "$FILE"
379
+
380
+ echo " ${FILE}: checked ${LINENUM} lines"
381
+ done
382
+
383
+ # Cross-check en-GB vs en-US key consistency
384
+ GB_DIR=$(find . -path "*/language/en-GB" -type d -not -path "./.git/*" 2>/dev/null | head -1)
385
+ US_DIR=$(find . -path "*/language/en-US" -type d -not -path "./.git/*" 2>/dev/null | head -1)
386
+
387
+ if [ -n "$GB_DIR" ] && [ -n "$US_DIR" ]; then
388
+ for GB_FILE in "$GB_DIR"/*.ini; do
389
+ [ ! -f "$GB_FILE" ] && continue
390
+ FNAME=$(basename "$GB_FILE")
391
+ US_FILE="$US_DIR/$FNAME"
392
+ [ ! -f "$US_FILE" ] && continue
393
+
394
+ GB_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$GB_FILE" 2>/dev/null | sort)
395
+ US_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$US_FILE" 2>/dev/null | sort)
396
+
397
+ # Keys in en-GB but not en-US
398
+ MISSING_US=$(comm -23 <(echo "$GB_KEYS") <(echo "$US_KEYS"))
399
+ if [ -n "$MISSING_US" ]; then
400
+ echo "::warning::Keys in en-GB/$FNAME but missing from en-US/$FNAME:"
401
+ echo "$MISSING_US" | while read -r k; do echo " - $k"; done
402
+ WARNINGS=$((WARNINGS + 1))
403
+ fi
404
+
405
+ # Keys in en-US but not en-GB
406
+ MISSING_GB=$(comm -13 <(echo "$GB_KEYS") <(echo "$US_KEYS"))
407
+ if [ -n "$MISSING_GB" ]; then
408
+ echo "::warning::Keys in en-US/$FNAME but missing from en-GB/$FNAME:"
409
+ echo "$MISSING_GB" | while read -r k; do echo " - $k"; done
410
+ WARNINGS=$((WARNINGS + 1))
411
+ fi
412
+ done
413
+ fi
414
+
415
+ {
416
+ echo "### Language File Validation"
417
+ echo "| Metric | Count |"
418
+ echo "|---|---|"
419
+ echo "| Files checked | $(echo "$INI_FILES" | wc -l) |"
420
+ echo "| Errors | ${ERRORS} |"
421
+ echo "| Warnings | ${WARNINGS} |"
422
+ } >> $GITHUB_STEP_SUMMARY
423
+
424
+ if [ "$ERRORS" -gt 0 ]; then
425
+ echo "::error::Language validation failed with ${ERRORS} error(s)"
426
+ exit 1
427
+ fi
428
+ echo "Language files: OK (${WARNINGS} warning(s))"
429
+
430
+ - name: Check changelog has unreleased entry
431
+ run: |
432
+ if [ ! -f "CHANGELOG.md" ]; then
433
+ echo "::warning::No CHANGELOG.md found"
434
+ exit 0
435
+ fi
436
+ # Check for content under [Unreleased] section
437
+ if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then
438
+ echo "::error::CHANGELOG.md missing [Unreleased] section"
439
+ exit 1
440
+ fi
441
+ # Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased
442
+ UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true)
443
+ if [ "$UNRELEASED_CONTENT" -eq 0 ]; then
444
+ echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes."
445
+ echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY
446
+ echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY
447
+ echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY
448
+ exit 1
449
+ fi
450
+ echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]"
451
+
452
+ - name: Verify package source
453
+ run: |
454
+ SOURCE_DIR="src"
455
+ [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
456
+ if [ ! -d "$SOURCE_DIR" ]; then
457
+ echo "::warning::No src/ or htdocs/ directory"
458
+ exit 0
459
+ fi
460
+ FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
461
+ echo "Source: ${FILE_COUNT} files"
462
+ [ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; }
463
+
464
+ # ── Pre-Release RC Build ─────────────────────────────────────────────────
465
+ pre-release:
466
+ name: Build RC Package
467
+ runs-on: ubuntu-latest
468
+ needs: [branch-policy, validate]
469
+
470
+ steps:
471
+ - name: Trigger RC pre-release
472
+ env:
473
+ GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
474
+ REPO: ${{ github.repository }}
475
+ BRANCH: ${{ github.head_ref }}
476
+ GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
477
+ run: |
478
+ curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
479
+ echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
480
+ echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
481
+
482
+ # ── Issue Reporter ──────────────────────────────────────────────────────
483
+ report-issues:
484
+ name: Report Issues
485
+ runs-on: ubuntu-latest
486
+ needs: [branch-policy, validate]
487
+ if: >-
488
+ always() &&
489
+ needs.validate.result == 'failure'
490
+
491
+ steps:
492
+ - name: Checkout
493
+ uses: actions/checkout@v4
494
+ with:
495
+ sparse-checkout: automation/ci-issue-reporter.sh
496
+ sparse-checkout-cone-mode: false
497
+
498
+ - name: "File issue for PR validation failure"
499
+ env:
500
+ GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
501
+ GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
502
+ run: |
503
+ chmod +x automation/ci-issue-reporter.sh
504
+ ./automation/ci-issue-reporter.sh \
505
+ --gate "PR Validation" \
506
+ --workflow "PR Check" \
507
+ --severity error \
508
+ --details "PR validation failed (syntax, manifest, changelog, or source checks). See the CI run for the specific check that failed."
@@ -0,0 +1,11 @@
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: Gitea.Workflow
7
+ # INGROUP: moko-platform.Release
8
+ # REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
9
+ # PATH: /templates/workflows/universal/pre-release.yml.template
10
+ # VERSION: 05.01.00
11
+ # BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches