@mokoconsulting/mcp-windows 3.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 (184) 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/feature_request.md +51 -0
  8. package/.mokogitea/ISSUE_TEMPLATE/mcp_api_integration.md +48 -0
  9. package/.mokogitea/ISSUE_TEMPLATE/mcp_connection_issue.md +67 -0
  10. package/.mokogitea/ISSUE_TEMPLATE/mcp_tool_request.md +49 -0
  11. package/.mokogitea/ISSUE_TEMPLATE/question.md +82 -0
  12. package/.mokogitea/ISSUE_TEMPLATE/rfc.md +126 -0
  13. package/.mokogitea/ISSUE_TEMPLATE/security.md +51 -0
  14. package/.mokogitea/ISSUE_TEMPLATE/version.md +24 -0
  15. package/.mokogitea/branch-protection.yml +251 -0
  16. package/.mokogitea/workflows/auto-assign.yml +76 -0
  17. package/.mokogitea/workflows/auto-bump.yml +66 -0
  18. package/.mokogitea/workflows/auto-dev-issue.yml +207 -0
  19. package/.mokogitea/workflows/auto-release.yml +421 -0
  20. package/.mokogitea/workflows/branch-cleanup.yml +48 -0
  21. package/.mokogitea/workflows/cascade-dev.yml +10 -0
  22. package/.mokogitea/workflows/changelog-validation.yml +101 -0
  23. package/.mokogitea/workflows/ci-generic.yml +191 -0
  24. package/.mokogitea/workflows/cleanup.yml +87 -0
  25. package/.mokogitea/workflows/codeql-analysis.yml +115 -0
  26. package/.mokogitea/workflows/copilot-agent.yml +44 -0
  27. package/.mokogitea/workflows/deploy-manual.yml +126 -0
  28. package/.mokogitea/workflows/enterprise-firewall-setup.yml +758 -0
  29. package/.mokogitea/workflows/gitleaks.yml +92 -0
  30. package/.mokogitea/workflows/issue-branch.yml +73 -0
  31. package/.mokogitea/workflows/mcp-auto-release.yml +278 -0
  32. package/.mokogitea/workflows/mcp-build-test.yml +65 -0
  33. package/.mokogitea/workflows/mcp-sdk-check.yml +109 -0
  34. package/.mokogitea/workflows/mcp-tool-inventory.yml +61 -0
  35. package/.mokogitea/workflows/notify.yml +70 -0
  36. package/.mokogitea/workflows/npm-publish.yml +113 -0
  37. package/.mokogitea/workflows/pr-check.yml +534 -0
  38. package/.mokogitea/workflows/pre-release.yml +252 -0
  39. package/.mokogitea/workflows/rc-revert.yml +66 -0
  40. package/.mokogitea/workflows/repo-health.yml +712 -0
  41. package/.mokogitea/workflows/repository-cleanup.yml +525 -0
  42. package/.mokogitea/workflows/security-audit.yml +82 -0
  43. package/.mokogitea/workflows/standards-compliance.yml +2614 -0
  44. package/.mokogitea/workflows/sync-version-on-merge.yml +133 -0
  45. package/.mokogitea/workflows/update-server.yml +312 -0
  46. package/.mokogitea/workflows/workflow-sync-trigger.yml +73 -0
  47. package/CHANGELOG.md +130 -0
  48. package/CLAUDE.md +49 -0
  49. package/CONTRIBUTING.md +161 -0
  50. package/ISSUES.md +601 -0
  51. package/Makefile +70 -0
  52. package/README.md +80 -0
  53. package/automation/ci-issue-reporter.sh +237 -0
  54. package/config.example.json +18 -0
  55. package/dist/index.d.ts +3 -0
  56. package/dist/index.js +111 -0
  57. package/dist/shell.d.ts +50 -0
  58. package/dist/shell.js +209 -0
  59. package/dist/tools/apps.d.ts +3 -0
  60. package/dist/tools/apps.js +63 -0
  61. package/dist/tools/audio.d.ts +3 -0
  62. package/dist/tools/audio.js +142 -0
  63. package/dist/tools/audio_apps.d.ts +3 -0
  64. package/dist/tools/audio_apps.js +86 -0
  65. package/dist/tools/automation.d.ts +3 -0
  66. package/dist/tools/automation.js +261 -0
  67. package/dist/tools/bluetooth.d.ts +3 -0
  68. package/dist/tools/bluetooth.js +96 -0
  69. package/dist/tools/clipboard.d.ts +3 -0
  70. package/dist/tools/clipboard.js +118 -0
  71. package/dist/tools/config.d.ts +3 -0
  72. package/dist/tools/config.js +85 -0
  73. package/dist/tools/dialog.d.ts +3 -0
  74. package/dist/tools/dialog.js +72 -0
  75. package/dist/tools/display.d.ts +3 -0
  76. package/dist/tools/display.js +256 -0
  77. package/dist/tools/drives.d.ts +3 -0
  78. package/dist/tools/drives.js +98 -0
  79. package/dist/tools/environment.d.ts +3 -0
  80. package/dist/tools/environment.js +129 -0
  81. package/dist/tools/execute.d.ts +3 -0
  82. package/dist/tools/execute.js +28 -0
  83. package/dist/tools/filesystem.d.ts +3 -0
  84. package/dist/tools/filesystem.js +230 -0
  85. package/dist/tools/firewall.d.ts +3 -0
  86. package/dist/tools/firewall.js +108 -0
  87. package/dist/tools/hosts.d.ts +3 -0
  88. package/dist/tools/hosts.js +119 -0
  89. package/dist/tools/maintenance.d.ts +3 -0
  90. package/dist/tools/maintenance.js +236 -0
  91. package/dist/tools/netstat.d.ts +3 -0
  92. package/dist/tools/netstat.js +56 -0
  93. package/dist/tools/network.d.ts +3 -0
  94. package/dist/tools/network.js +70 -0
  95. package/dist/tools/notification.d.ts +3 -0
  96. package/dist/tools/notification.js +41 -0
  97. package/dist/tools/power.d.ts +3 -0
  98. package/dist/tools/power.js +104 -0
  99. package/dist/tools/printer.d.ts +3 -0
  100. package/dist/tools/printer.js +97 -0
  101. package/dist/tools/process.d.ts +3 -0
  102. package/dist/tools/process.js +54 -0
  103. package/dist/tools/process_kill.d.ts +3 -0
  104. package/dist/tools/process_kill.js +48 -0
  105. package/dist/tools/recycle_bin.d.ts +3 -0
  106. package/dist/tools/recycle_bin.js +108 -0
  107. package/dist/tools/registry.d.ts +3 -0
  108. package/dist/tools/registry.js +136 -0
  109. package/dist/tools/scheduler.d.ts +3 -0
  110. package/dist/tools/scheduler.js +116 -0
  111. package/dist/tools/service.d.ts +3 -0
  112. package/dist/tools/service.js +79 -0
  113. package/dist/tools/startup.d.ts +3 -0
  114. package/dist/tools/startup.js +159 -0
  115. package/dist/tools/storage.d.ts +3 -0
  116. package/dist/tools/storage.js +129 -0
  117. package/dist/tools/system.d.ts +3 -0
  118. package/dist/tools/system.js +84 -0
  119. package/dist/tools/system_mgmt.d.ts +3 -0
  120. package/dist/tools/system_mgmt.js +174 -0
  121. package/dist/tools/terminal.d.ts +3 -0
  122. package/dist/tools/terminal.js +80 -0
  123. package/dist/tools/theme.d.ts +3 -0
  124. package/dist/tools/theme.js +165 -0
  125. package/dist/tools/usb.d.ts +3 -0
  126. package/dist/tools/usb.js +52 -0
  127. package/dist/tools/virtual_desktop.d.ts +3 -0
  128. package/dist/tools/virtual_desktop.js +112 -0
  129. package/dist/tools/wifi.d.ts +3 -0
  130. package/dist/tools/wifi.js +136 -0
  131. package/dist/tools/window.d.ts +3 -0
  132. package/dist/tools/window.js +189 -0
  133. package/dist/tools/winget.d.ts +3 -0
  134. package/dist/tools/winget.js +79 -0
  135. package/dist/tools/wsl.d.ts +3 -0
  136. package/dist/tools/wsl.js +99 -0
  137. package/docs/API.md +63 -0
  138. package/docs/ARCHITECTURE.md +73 -0
  139. package/docs/INSTALLATION.md +102 -0
  140. package/docs/index.md +12 -0
  141. package/package.json +35 -0
  142. package/scripts/setup.mjs +123 -0
  143. package/src/index.ts +125 -0
  144. package/src/shell.ts +253 -0
  145. package/src/tools/apps.ts +76 -0
  146. package/src/tools/audio.ts +161 -0
  147. package/src/tools/audio_apps.ts +98 -0
  148. package/src/tools/automation.ts +297 -0
  149. package/src/tools/bluetooth.ts +114 -0
  150. package/src/tools/clipboard.ts +138 -0
  151. package/src/tools/config.ts +105 -0
  152. package/src/tools/dialog.ts +87 -0
  153. package/src/tools/display.ts +285 -0
  154. package/src/tools/drives.ts +124 -0
  155. package/src/tools/environment.ts +146 -0
  156. package/src/tools/execute.ts +35 -0
  157. package/src/tools/filesystem.ts +273 -0
  158. package/src/tools/firewall.ts +125 -0
  159. package/src/tools/hosts.ts +135 -0
  160. package/src/tools/maintenance.ts +299 -0
  161. package/src/tools/netstat.ts +72 -0
  162. package/src/tools/network.ts +84 -0
  163. package/src/tools/notification.ts +50 -0
  164. package/src/tools/power.ts +123 -0
  165. package/src/tools/printer.ts +114 -0
  166. package/src/tools/process.ts +80 -0
  167. package/src/tools/process_kill.ts +57 -0
  168. package/src/tools/recycle_bin.ts +126 -0
  169. package/src/tools/registry.ts +165 -0
  170. package/src/tools/scheduler.ts +140 -0
  171. package/src/tools/service.ts +102 -0
  172. package/src/tools/startup.ts +180 -0
  173. package/src/tools/storage.ts +141 -0
  174. package/src/tools/system.ts +99 -0
  175. package/src/tools/system_mgmt.ts +190 -0
  176. package/src/tools/terminal.ts +117 -0
  177. package/src/tools/theme.ts +205 -0
  178. package/src/tools/usb.ts +65 -0
  179. package/src/tools/virtual_desktop.ts +122 -0
  180. package/src/tools/wifi.ts +157 -0
  181. package/src/tools/window.ts +211 -0
  182. package/src/tools/winget.ts +100 -0
  183. package/src/tools/wsl.ts +112 -0
  184. package/tsconfig.json +19 -0
@@ -0,0 +1,534 @@
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
+ # ── Secret Scanning ──────────────────────────────────────────────────
100
+ gitleaks:
101
+ name: Secret Scan
102
+ runs-on: ubuntu-latest
103
+ steps:
104
+ - name: Checkout
105
+ uses: actions/checkout@v4
106
+ with:
107
+ fetch-depth: 0
108
+
109
+ - name: Install Gitleaks
110
+ run: |
111
+ GITLEAKS_VERSION="8.21.2"
112
+ curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \
113
+ | tar -xz -C /usr/local/bin gitleaks
114
+
115
+ - name: Scan PR commits for secrets
116
+ run: |
117
+ if gitleaks detect --source . --verbose \
118
+ --log-opts=${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }} 2>&1; then
119
+ echo "**No secrets detected.**" >> $GITHUB_STEP_SUMMARY
120
+ else
121
+ echo "::error::Potential secrets detected in PR commits"
122
+ exit 1
123
+ fi
124
+
125
+ # ── Code Validation ────────────────────────────────────────────────────
126
+ validate:
127
+ name: Validate PR
128
+ runs-on: ubuntu-latest
129
+
130
+ steps:
131
+ - name: Checkout
132
+ uses: actions/checkout@v4
133
+
134
+ - name: Check for merge conflict markers
135
+ run: |
136
+ 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)
137
+ if [ -n "$CONFLICTS" ]; then
138
+ echo "::error::Merge conflict markers found in source files"
139
+ echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY
140
+ echo '```' >> $GITHUB_STEP_SUMMARY
141
+ echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
142
+ echo '```' >> $GITHUB_STEP_SUMMARY
143
+ exit 1
144
+ fi
145
+ echo "No conflict markers found"
146
+
147
+ - name: Detect platform
148
+ id: platform
149
+ run: |
150
+ # Read platform from XML manifest (<platform> tag) or plain text fallback
151
+ PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1)
152
+ [ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]')
153
+ [ -z "$PLATFORM" ] && PLATFORM="generic"
154
+ echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
155
+
156
+ - name: Setup PHP
157
+ if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
158
+ run: |
159
+ if ! command -v php &> /dev/null; then
160
+ sudo apt-get update -qq
161
+ sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
162
+ fi
163
+
164
+ - name: PHP syntax check
165
+ if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
166
+ run: |
167
+ ERRORS=0
168
+ while IFS= read -r -d '' file; do
169
+ if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
170
+ ERRORS=$((ERRORS + 1))
171
+ fi
172
+ done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0)
173
+ echo "PHP lint: ${ERRORS} error(s)"
174
+ [ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
175
+
176
+ - name: Joomla JEXEC guard check
177
+ if: steps.platform.outputs.platform == 'joomla'
178
+ run: |
179
+ ERRORS=0
180
+ while IFS= read -r -d '' file; do
181
+ # Skip vendor, node_modules, and index.html stub files
182
+ case "$file" in ./vendor/*|./node_modules/*) continue ;; esac
183
+ # Check first 10 lines for JEXEC or JPATH guard
184
+ if ! head -20 "$file" | grep -qE "defined\s*\(\s*['\"](_JEXEC|JPATH_BASE|\\\\JPATH_PLATFORM)['\"]"; then
185
+ echo "::error file=${file}::Missing JEXEC guard: ${file}"
186
+ ERRORS=$((ERRORS + 1))
187
+ fi
188
+ done < <(find . -name "*.php" -path "*/src/*" -not -path "./.git/*" -not -path "./vendor/*" -print0)
189
+ if [ "$ERRORS" -gt 0 ]; then
190
+ echo "::error::${ERRORS} PHP file(s) missing defined('_JEXEC') or die guard"
191
+ echo "## JEXEC Guard Check: Failed" >> $GITHUB_STEP_SUMMARY
192
+ echo "${ERRORS} file(s) in src/ are missing the Joomla execution guard." >> $GITHUB_STEP_SUMMARY
193
+ exit 1
194
+ fi
195
+ echo "JEXEC guard: OK"
196
+
197
+ - name: Joomla directory listing protection
198
+ if: steps.platform.outputs.platform == 'joomla'
199
+ run: |
200
+ MISSING=0
201
+ SOURCE_DIR="src"
202
+ [ ! -d "$SOURCE_DIR" ] && exit 0
203
+ while IFS= read -r dir; do
204
+ if [ ! -f "${dir}/index.html" ]; then
205
+ echo "::warning::Missing index.html in ${dir} (directory listing protection)"
206
+ MISSING=$((MISSING + 1))
207
+ fi
208
+ done < <(find "$SOURCE_DIR" -type d -not -path "./.git/*" -not -path "*/vendor/*" -not -path "*/node_modules/*")
209
+ if [ "$MISSING" -gt 0 ]; then
210
+ echo "## Directory Protection" >> $GITHUB_STEP_SUMMARY
211
+ echo "${MISSING} director(ies) missing index.html" >> $GITHUB_STEP_SUMMARY
212
+ fi
213
+ echo "Directory protection: ${MISSING} missing (advisory)"
214
+
215
+ - name: Joomla script file and asset checks
216
+ if: steps.platform.outputs.platform == 'joomla'
217
+ run: |
218
+ ERRORS=0
219
+ MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
220
+ [ -z "$MANIFEST" ] && exit 0
221
+ MANIFEST_DIR=$(dirname "$MANIFEST")
222
+
223
+ # Check scriptfile exists if declared
224
+ SCRIPTFILE=$(sed -n 's/.*<scriptfile>\([^<]*\)<\/scriptfile>.*/\1/p' "$MANIFEST" 2>/dev/null)
225
+ if [ -n "$SCRIPTFILE" ]; then
226
+ if [ ! -f "${MANIFEST_DIR}/${SCRIPTFILE}" ]; then
227
+ echo "::error::Manifest declares <scriptfile>${SCRIPTFILE}</scriptfile> but file not found at ${MANIFEST_DIR}/${SCRIPTFILE}"
228
+ ERRORS=$((ERRORS + 1))
229
+ else
230
+ echo "Script file: ${MANIFEST_DIR}/${SCRIPTFILE} (OK)"
231
+ fi
232
+ fi
233
+
234
+ # Require joomla.asset.json and validate it
235
+ ASSET_JSON=$(find "$MANIFEST_DIR" -name "joomla.asset.json" -not -path "./.git/*" 2>/dev/null | head -1)
236
+ if [ -z "$ASSET_JSON" ]; then
237
+ echo "::error::joomla.asset.json not found — Joomla asset system is required"
238
+ ERRORS=$((ERRORS + 1))
239
+ else
240
+ if command -v php &> /dev/null; then
241
+ 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 || {
242
+ echo "::error::joomla.asset.json is not valid JSON"
243
+ ERRORS=$((ERRORS + 1))
244
+ }
245
+ fi
246
+ echo "joomla.asset.json: valid"
247
+ fi
248
+
249
+ # Validate all XML files in src/ are well-formed
250
+ XML_ERRORS=0
251
+ if command -v php &> /dev/null; then
252
+ while IFS= read -r -d '' xmlfile; do
253
+ 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
254
+ XML_ERRORS=$((XML_ERRORS + 1))
255
+ fi
256
+ done < <(find "$MANIFEST_DIR" -name "*.xml" -not -path "./.git/*" -print0)
257
+ fi
258
+ if [ "$XML_ERRORS" -gt 0 ]; then
259
+ echo "::error::${XML_ERRORS} XML file(s) are malformed"
260
+ ERRORS=$((ERRORS + 1))
261
+ else
262
+ echo "XML well-formedness: OK"
263
+ fi
264
+
265
+ [ "$ERRORS" -gt 0 ] && exit 1
266
+ echo "Joomla asset checks: OK"
267
+
268
+ - name: Validate platform manifest
269
+ run: |
270
+ PLATFORM="${{ steps.platform.outputs.platform }}"
271
+ case "$PLATFORM" in
272
+ joomla)
273
+ MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
274
+ if [ -z "$MANIFEST" ]; then
275
+ echo "::warning::No Joomla manifest found (WaaS site)"
276
+ exit 0
277
+ fi
278
+ echo "Manifest: ${MANIFEST}"
279
+ if command -v php &> /dev/null; then
280
+ 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; }
281
+ fi
282
+ for ELEMENT in name version description; do
283
+ grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; }
284
+ done
285
+ # Block legacy raw/branch update server URLs on MokoGitea
286
+ RAW_URLS=$(grep -n 'raw/branch' "$MANIFEST" | grep -i 'mokoconsulting\|mokogitea\|git\.mokoconsulting\.tech' || true)
287
+ if [ -n "$RAW_URLS" ]; then
288
+ 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)"
289
+ echo "$RAW_URLS"
290
+ exit 1
291
+ fi
292
+ echo "Joomla manifest valid"
293
+ ;;
294
+ dolibarr)
295
+ MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1)
296
+ if [ -z "$MOD_FILE" ]; then
297
+ echo "::error::No mod*.class.php found"
298
+ exit 1
299
+ fi
300
+ echo "Dolibarr module: ${MOD_FILE}"
301
+ ;;
302
+ *)
303
+ echo "Generic platform — no manifest validation"
304
+ ;;
305
+ esac
306
+
307
+ - name: Check update stream format
308
+ run: |
309
+ PLATFORM="${{ steps.platform.outputs.platform }}"
310
+ case "$PLATFORM" in
311
+ joomla)
312
+ if [ -f "updates.xml" ]; then
313
+ if command -v php &> /dev/null; then
314
+ 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; }
315
+ fi
316
+ echo "updates.xml valid"
317
+ fi
318
+ ;;
319
+ dolibarr)
320
+ [ -f "update.txt" ] && echo "update.txt present" || echo "::warning::No update.txt"
321
+ ;;
322
+ esac
323
+
324
+ - name: Validate Joomla language files
325
+ if: steps.platform.outputs.platform == 'joomla'
326
+ run: |
327
+ ERRORS=0
328
+ WARNINGS=0
329
+
330
+ # Require both en-GB and en-US language directories
331
+ LANG_ROOT=$(find . -path "*/language" -type d -not -path "./.git/*" 2>/dev/null | head -1)
332
+ if [ -z "$LANG_ROOT" ]; then
333
+ echo "No language/ directory found — skipping"
334
+ exit 0
335
+ fi
336
+
337
+ if [ ! -d "$LANG_ROOT/en-GB" ]; then
338
+ echo "::error::Missing en-GB language directory (${LANG_ROOT}/en-GB)"
339
+ ERRORS=$((ERRORS + 1))
340
+ fi
341
+ if [ ! -d "$LANG_ROOT/en-US" ]; then
342
+ echo "::error::Missing en-US language directory (${LANG_ROOT}/en-US)"
343
+ ERRORS=$((ERRORS + 1))
344
+ fi
345
+
346
+ # Check that en-GB and en-US have matching .ini files
347
+ if [ -d "$LANG_ROOT/en-GB" ] && [ -d "$LANG_ROOT/en-US" ]; then
348
+ for GB_INI in "$LANG_ROOT/en-GB"/*.ini; do
349
+ [ ! -f "$GB_INI" ] && continue
350
+ US_INI="$LANG_ROOT/en-US/$(basename "$GB_INI")"
351
+ if [ ! -f "$US_INI" ]; then
352
+ echo "::error::$(basename "$GB_INI") exists in en-GB but missing from en-US"
353
+ ERRORS=$((ERRORS + 1))
354
+ fi
355
+ done
356
+ for US_INI in "$LANG_ROOT/en-US"/*.ini; do
357
+ [ ! -f "$US_INI" ] && continue
358
+ GB_INI="$LANG_ROOT/en-GB/$(basename "$US_INI")"
359
+ if [ ! -f "$GB_INI" ]; then
360
+ echo "::error::$(basename "$US_INI") exists in en-US but missing from en-GB"
361
+ ERRORS=$((ERRORS + 1))
362
+ fi
363
+ done
364
+ fi
365
+
366
+ # Find all .ini language files
367
+ INI_FILES=$(find . -path "*/language/*/*.ini" -not -path "./.git/*" 2>/dev/null)
368
+ if [ -z "$INI_FILES" ]; then
369
+ echo "No .ini language files found"
370
+ [ "$ERRORS" -gt 0 ] && exit 1
371
+ exit 0
372
+ fi
373
+
374
+ echo "Found $(echo "$INI_FILES" | wc -l) language file(s)"
375
+
376
+ for FILE in $INI_FILES; do
377
+ FNAME=$(basename "$FILE")
378
+ LINENUM=0
379
+ SEEN_KEYS=""
380
+
381
+ while IFS= read -r line || [ -n "$line" ]; do
382
+ LINENUM=$((LINENUM + 1))
383
+
384
+ # Skip empty lines and comments
385
+ [ -z "$line" ] && continue
386
+ echo "$line" | grep -qE '^\s*;' && continue
387
+ echo "$line" | grep -qE '^\s*$' && continue
388
+
389
+ # Must match KEY="VALUE" format
390
+ if ! echo "$line" | grep -qE '^[A-Z_][A-Z0-9_]*=".*"$'; then
391
+ echo "::error file=${FILE},line=${LINENUM}::Malformed line: ${line}"
392
+ ERRORS=$((ERRORS + 1))
393
+ continue
394
+ fi
395
+
396
+ # Extract key and check for duplicates
397
+ KEY=$(echo "$line" | sed 's/=.*//')
398
+ if echo "$SEEN_KEYS" | grep -qx "$KEY"; then
399
+ echo "::error file=${FILE},line=${LINENUM}::Duplicate key: ${KEY}"
400
+ ERRORS=$((ERRORS + 1))
401
+ fi
402
+ SEEN_KEYS="${SEEN_KEYS}
403
+ ${KEY}"
404
+ done < "$FILE"
405
+
406
+ echo " ${FILE}: checked ${LINENUM} lines"
407
+ done
408
+
409
+ # Cross-check en-GB vs en-US key consistency
410
+ GB_DIR=$(find . -path "*/language/en-GB" -type d -not -path "./.git/*" 2>/dev/null | head -1)
411
+ US_DIR=$(find . -path "*/language/en-US" -type d -not -path "./.git/*" 2>/dev/null | head -1)
412
+
413
+ if [ -n "$GB_DIR" ] && [ -n "$US_DIR" ]; then
414
+ for GB_FILE in "$GB_DIR"/*.ini; do
415
+ [ ! -f "$GB_FILE" ] && continue
416
+ FNAME=$(basename "$GB_FILE")
417
+ US_FILE="$US_DIR/$FNAME"
418
+ [ ! -f "$US_FILE" ] && continue
419
+
420
+ GB_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$GB_FILE" 2>/dev/null | sort)
421
+ US_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$US_FILE" 2>/dev/null | sort)
422
+
423
+ # Keys in en-GB but not en-US
424
+ MISSING_US=$(comm -23 <(echo "$GB_KEYS") <(echo "$US_KEYS"))
425
+ if [ -n "$MISSING_US" ]; then
426
+ echo "::warning::Keys in en-GB/$FNAME but missing from en-US/$FNAME:"
427
+ echo "$MISSING_US" | while read -r k; do echo " - $k"; done
428
+ WARNINGS=$((WARNINGS + 1))
429
+ fi
430
+
431
+ # Keys in en-US but not en-GB
432
+ MISSING_GB=$(comm -13 <(echo "$GB_KEYS") <(echo "$US_KEYS"))
433
+ if [ -n "$MISSING_GB" ]; then
434
+ echo "::warning::Keys in en-US/$FNAME but missing from en-GB/$FNAME:"
435
+ echo "$MISSING_GB" | while read -r k; do echo " - $k"; done
436
+ WARNINGS=$((WARNINGS + 1))
437
+ fi
438
+ done
439
+ fi
440
+
441
+ {
442
+ echo "### Language File Validation"
443
+ echo "| Metric | Count |"
444
+ echo "|---|---|"
445
+ echo "| Files checked | $(echo "$INI_FILES" | wc -l) |"
446
+ echo "| Errors | ${ERRORS} |"
447
+ echo "| Warnings | ${WARNINGS} |"
448
+ } >> $GITHUB_STEP_SUMMARY
449
+
450
+ if [ "$ERRORS" -gt 0 ]; then
451
+ echo "::error::Language validation failed with ${ERRORS} error(s)"
452
+ exit 1
453
+ fi
454
+ echo "Language files: OK (${WARNINGS} warning(s))"
455
+
456
+ - name: Check changelog has unreleased entry
457
+ run: |
458
+ if [ ! -f "CHANGELOG.md" ]; then
459
+ echo "::warning::No CHANGELOG.md found"
460
+ exit 0
461
+ fi
462
+ # Check for content under [Unreleased] section
463
+ if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then
464
+ echo "::error::CHANGELOG.md missing [Unreleased] section"
465
+ exit 1
466
+ fi
467
+ # Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased
468
+ UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true)
469
+ if [ "$UNRELEASED_CONTENT" -eq 0 ]; then
470
+ echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes."
471
+ echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY
472
+ echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY
473
+ echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY
474
+ exit 1
475
+ fi
476
+ echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]"
477
+
478
+ - name: Verify package source
479
+ run: |
480
+ SOURCE_DIR="src"
481
+ [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
482
+ if [ ! -d "$SOURCE_DIR" ]; then
483
+ echo "::warning::No src/ or htdocs/ directory"
484
+ exit 0
485
+ fi
486
+ FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
487
+ echo "Source: ${FILE_COUNT} files"
488
+ [ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; }
489
+
490
+ # ── Pre-Release RC Build ─────────────────────────────────────────────────
491
+ pre-release:
492
+ name: Build RC Package
493
+ runs-on: ubuntu-latest
494
+ needs: [branch-policy, validate]
495
+
496
+ steps:
497
+ - name: Trigger RC pre-release
498
+ env:
499
+ GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
500
+ REPO: ${{ github.repository }}
501
+ BRANCH: ${{ github.head_ref }}
502
+ GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
503
+ run: |
504
+ 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\"}}"
505
+ echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
506
+ echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
507
+
508
+ # ── Issue Reporter ──────────────────────────────────────────────────────
509
+ report-issues:
510
+ name: Report Issues
511
+ runs-on: ubuntu-latest
512
+ needs: [branch-policy, validate]
513
+ if: >-
514
+ always() &&
515
+ needs.validate.result == 'failure'
516
+
517
+ steps:
518
+ - name: Checkout
519
+ uses: actions/checkout@v4
520
+ with:
521
+ sparse-checkout: automation/ci-issue-reporter.sh
522
+ sparse-checkout-cone-mode: false
523
+
524
+ - name: "File issue for PR validation failure"
525
+ env:
526
+ GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
527
+ GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
528
+ run: |
529
+ chmod +x automation/ci-issue-reporter.sh
530
+ ./automation/ci-issue-reporter.sh \
531
+ --gate "PR Validation" \
532
+ --workflow "PR Check" \
533
+ --severity error \
534
+ --details "PR validation failed (syntax, manifest, changelog, or source checks). See the CI run for the specific check that failed."