@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,700 @@
1
+ # Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
2
+ #
3
+ # This file is part of a Moko Consulting project.
4
+ #
5
+ # SPDX-License-Identifier: GPL-3.0-or-later
6
+ #
7
+ # This program is free software: you can redistribute it and/or modify
8
+ # it under the terms of the GNU General Public License as published by
9
+ # the Free Software Foundation, either version 3 of the License, or
10
+ # (at your option) any later version.
11
+ #
12
+ # This program is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU General Public License
18
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
19
+ #
20
+ # FILE INFORMATION
21
+ # DEFGROUP: GitHub.Workflow
22
+ # INGROUP: MokoStandards.Deploy
23
+ # REPO: https://github.com/mokoconsulting-tech/MokoStandards
24
+ # PATH: /templates/workflows/shared/deploy-dev.yml.template
25
+ # VERSION: 04.06.00
26
+ # BRIEF: SFTP deployment workflow for development server — synced to all governed repos
27
+ # NOTE: Synced via bulk-repo-sync to .github/workflows/deploy-dev.yml in all governed repos.
28
+ # Port is resolved in order: DEV_FTP_PORT variable → :port suffix in DEV_FTP_HOST → 22.
29
+
30
+ name: Deploy to Dev Server (SFTP)
31
+
32
+ # Deploys the contents of the src/ directory to the development server via SFTP.
33
+ # Triggers on every pull_request to development branches (so the dev server always
34
+ # reflects the latest PR state) and on push/merge to main branches.
35
+ #
36
+ # Required org-level variables: DEV_FTP_HOST, DEV_FTP_PATH, DEV_FTP_USERNAME
37
+ # Optional org-level variable: DEV_FTP_PORT (auto-detected from host or defaults to 22)
38
+ # Optional org/repo variable: DEV_FTP_SUFFIX — when set, appended to DEV_FTP_PATH to form the
39
+ # full remote destination: DEV_FTP_PATH/DEV_FTP_SUFFIX
40
+ # Ignore rules: Place a .ftpignore file in the src/ directory. Each non-empty,
41
+ # non-comment line is a glob pattern tested against the relative path
42
+ # of each file (e.g. "subdir/file.txt"). The .gitignore is NOT used.
43
+ # Required org-level secret: DEV_FTP_KEY (preferred) or DEV_FTP_PASSWORD
44
+ #
45
+ # Access control: only users with admin or maintain role on the repository may deploy.
46
+
47
+ on:
48
+ push:
49
+ branches:
50
+ - 'dev/**'
51
+ - 'rc/**'
52
+ - develop
53
+ - development
54
+ paths:
55
+ - 'src/**'
56
+ - 'htdocs/**'
57
+ pull_request:
58
+ types: [opened, synchronize, reopened, closed]
59
+ branches:
60
+ - 'dev/**'
61
+ - 'rc/**'
62
+ - develop
63
+ - development
64
+ paths:
65
+ - 'src/**'
66
+ - 'htdocs/**'
67
+ workflow_dispatch:
68
+ inputs:
69
+ clear_remote:
70
+ description: 'Delete all files inside the remote destination folder before uploading'
71
+ required: false
72
+ default: false
73
+ type: boolean
74
+
75
+ permissions:
76
+ contents: read
77
+ pull-requests: write
78
+
79
+ env:
80
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
81
+
82
+ jobs:
83
+ check-permission:
84
+ name: Verify Deployment Permission
85
+ runs-on: ubuntu-latest
86
+ steps:
87
+ - name: Check actor permission
88
+ env:
89
+ # Prefer the org-scoped GH_TOKEN secret (needed for the org membership
90
+ # fallback). Falls back to the built-in github.token so the collaborator
91
+ # endpoint still works even if GH_TOKEN is not configured.
92
+ GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
93
+ run: |
94
+ ACTOR="${{ github.actor }}"
95
+ REPO="${{ github.repository }}"
96
+ ORG="${{ github.repository_owner }}"
97
+
98
+ METHOD=""
99
+ AUTHORIZED="false"
100
+
101
+ # Hardcoded authorized users — always allowed to deploy
102
+ AUTHORIZED_USERS="jmiller github-actions[bot]"
103
+ for user in $AUTHORIZED_USERS; do
104
+ if [ "$ACTOR" = "$user" ]; then
105
+ AUTHORIZED="true"
106
+ METHOD="hardcoded allowlist"
107
+ PERMISSION="admin"
108
+ break
109
+ fi
110
+ done
111
+
112
+ # For other actors, check repo/org permissions via API
113
+ if [ "$AUTHORIZED" != "true" ]; then
114
+ PERMISSION=$(gh api "repos/${REPO}/collaborators/${ACTOR}/permission" \
115
+ --jq '.permission' 2>/dev/null)
116
+ METHOD="repo collaborator API"
117
+
118
+ if [ -z "$PERMISSION" ]; then
119
+ ORG_ROLE=$(gh api "orgs/${ORG}/memberships/${ACTOR}" \
120
+ --jq '.role' 2>/dev/null)
121
+ METHOD="org membership API"
122
+ if [ "$ORG_ROLE" = "owner" ]; then
123
+ PERMISSION="admin"
124
+ else
125
+ PERMISSION="none"
126
+ fi
127
+ fi
128
+
129
+ case "$PERMISSION" in
130
+ admin|maintain) AUTHORIZED="true" ;;
131
+ esac
132
+ fi
133
+
134
+ # Write detailed summary
135
+ {
136
+ echo "## 🔐 Deploy Authorization"
137
+ echo ""
138
+ echo "| Field | Value |"
139
+ echo "|-------|-------|"
140
+ echo "| **Actor** | \`${ACTOR}\` |"
141
+ echo "| **Repository** | \`${REPO}\` |"
142
+ echo "| **Permission** | \`${PERMISSION}\` |"
143
+ echo "| **Method** | ${METHOD} |"
144
+ echo "| **Authorized** | ${AUTHORIZED} |"
145
+ echo "| **Trigger** | \`${{ github.event_name }}\` |"
146
+ echo "| **Branch** | \`${{ github.ref_name }}\` |"
147
+ echo ""
148
+ } >> "$GITHUB_STEP_SUMMARY"
149
+
150
+ if [ "$AUTHORIZED" = "true" ]; then
151
+ echo "✅ ${ACTOR} authorized to deploy (${METHOD})" >> "$GITHUB_STEP_SUMMARY"
152
+ else
153
+ echo "❌ ${ACTOR} is NOT authorized to deploy." >> "$GITHUB_STEP_SUMMARY"
154
+ echo "" >> "$GITHUB_STEP_SUMMARY"
155
+ echo "Deployment requires one of:" >> "$GITHUB_STEP_SUMMARY"
156
+ echo "- Being in the hardcoded allowlist" >> "$GITHUB_STEP_SUMMARY"
157
+ echo "- Having \`admin\` or \`maintain\` role on the repository" >> "$GITHUB_STEP_SUMMARY"
158
+ exit 1
159
+ fi
160
+
161
+ deploy:
162
+ name: SFTP Deploy → Dev
163
+ runs-on: ubuntu-latest
164
+ needs: [check-permission]
165
+ if: >-
166
+ !startsWith(github.head_ref || github.ref_name, 'chore/') &&
167
+ (github.event_name == 'workflow_dispatch' ||
168
+ github.event_name == 'push' ||
169
+ (github.event_name == 'pull_request' &&
170
+ (github.event.action == 'opened' ||
171
+ github.event.action == 'synchronize' ||
172
+ github.event.action == 'reopened' ||
173
+ github.event.pull_request.merged == true)))
174
+
175
+ steps:
176
+ - name: Checkout repository
177
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
178
+
179
+ - name: Resolve source directory
180
+ id: source
181
+ run: |
182
+ # Resolve source directory: src/ preferred, htdocs/ as fallback
183
+ if [ -d "src" ]; then
184
+ SRC="src"
185
+ elif [ -d "htdocs" ]; then
186
+ SRC="htdocs"
187
+ else
188
+ echo "⚠️ No src/ or htdocs/ directory found — skipping deployment"
189
+ echo "skip=true" >> "$GITHUB_OUTPUT"
190
+ exit 0
191
+ fi
192
+ COUNT=$(find "$SRC" -type f | wc -l)
193
+ echo "✅ Source: ${SRC}/ (${COUNT} file(s))"
194
+ echo "skip=false" >> "$GITHUB_OUTPUT"
195
+ echo "dir=${SRC}" >> "$GITHUB_OUTPUT"
196
+
197
+ - name: Preview files to deploy
198
+ if: steps.source.outputs.skip == 'false'
199
+ env:
200
+ SOURCE_DIR: ${{ steps.source.outputs.dir }}
201
+ run: |
202
+ # ── Convert a ftpignore-style glob line to an ERE pattern ──────────────
203
+ ftpignore_to_regex() {
204
+ local line="$1"
205
+ local anchored=false
206
+ # Strip inline comments and whitespace
207
+ line=$(printf '%s' "$line" | sed 's/[[:space:]]*#.*$//' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
208
+ [ -z "$line" ] && return
209
+ # Skip negation patterns (not supported)
210
+ [[ "$line" == !* ]] && return
211
+ # Trailing slash = directory marker; strip it
212
+ line="${line%/}"
213
+ # Leading slash = anchored to root; strip it
214
+ if [[ "$line" == /* ]]; then
215
+ anchored=true
216
+ line="${line#/}"
217
+ fi
218
+ # Escape ERE special chars, then restore glob semantics
219
+ local regex
220
+ regex=$(printf '%s' "$line" \
221
+ | sed 's/[.+^${}()|[\\]/\\&/g' \
222
+ | sed 's/\\\*\\\*/\x01/g' \
223
+ | sed 's/\\\*/[^\/]*/g' \
224
+ | sed 's/\x01/.*/g' \
225
+ | sed 's/\\\?/[^\/]/g')
226
+ if $anchored; then
227
+ printf '^%s(/|$)' "$regex"
228
+ else
229
+ printf '(^|/)%s(/|$)' "$regex"
230
+ fi
231
+ }
232
+
233
+ # ── Read .ftpignore (ftpignore-style globs) ─────────────────────────
234
+ IGNORE_PATTERNS=()
235
+ IGNORE_SOURCES=()
236
+ if [ -f "${SOURCE_DIR}/.ftpignore" ]; then
237
+ while IFS= read -r line; do
238
+ [[ "$line" =~ ^[[:space:]]*$ || "$line" =~ ^[[:space:]]*# ]] && continue
239
+ regex=$(ftpignore_to_regex "$line")
240
+ [ -n "$regex" ] && IGNORE_PATTERNS+=("$regex") && IGNORE_SOURCES+=("$line")
241
+ done < "${SOURCE_DIR}/.ftpignore"
242
+ fi
243
+
244
+ # ── Walk src/ and classify every file ────────────────────────────────
245
+ WILL_UPLOAD=()
246
+ IGNORED_FILES=()
247
+ while IFS= read -r -d '' file; do
248
+ rel="${file#${SOURCE_DIR}/}"
249
+ SKIP=false
250
+ for i in "${!IGNORE_PATTERNS[@]}"; do
251
+ if echo "$rel" | grep -qE "${IGNORE_PATTERNS[$i]}" 2>/dev/null; then
252
+ IGNORED_FILES+=("$rel | .ftpignore \`${IGNORE_SOURCES[$i]}\`")
253
+ SKIP=true; break
254
+ fi
255
+ done
256
+ $SKIP && continue
257
+ WILL_UPLOAD+=("$rel")
258
+ done < <(find "$SOURCE_DIR" -type f -print0 | sort -z)
259
+
260
+ UPLOAD_COUNT="${#WILL_UPLOAD[@]}"
261
+ IGNORE_COUNT="${#IGNORED_FILES[@]}"
262
+
263
+ echo "ℹ️ ${UPLOAD_COUNT} file(s) will be uploaded, ${IGNORE_COUNT} ignored"
264
+
265
+ # ── Write deployment preview to step summary ──────────────────────────
266
+ {
267
+ echo "## 📋 Deployment Preview"
268
+ echo ""
269
+ echo "| Field | Value |"
270
+ echo "|---|---|"
271
+ echo "| Source | \`${SOURCE_DIR}/\` |"
272
+ echo "| Files to upload | **${UPLOAD_COUNT}** |"
273
+ echo "| Files ignored | **${IGNORE_COUNT}** |"
274
+ echo ""
275
+ if [ "${UPLOAD_COUNT}" -gt 0 ]; then
276
+ echo "### 📂 Files that will be uploaded"
277
+ echo '```'
278
+ printf '%s\n' "${WILL_UPLOAD[@]}"
279
+ echo '```'
280
+ echo ""
281
+ fi
282
+ if [ "${IGNORE_COUNT}" -gt 0 ]; then
283
+ echo "### ⏭️ Files excluded"
284
+ echo "| File | Reason |"
285
+ echo "|---|---|"
286
+ for entry in "${IGNORED_FILES[@]}"; do
287
+ f="${entry% | *}"; r="${entry##* | }"
288
+ echo "| \`${f}\` | ${r} |"
289
+ done
290
+ echo ""
291
+ fi
292
+ } >> "$GITHUB_STEP_SUMMARY"
293
+
294
+ - name: Resolve SFTP host and port
295
+ if: steps.source.outputs.skip == 'false'
296
+ id: conn
297
+ env:
298
+ HOST_RAW: ${{ vars.DEV_FTP_HOST }}
299
+ PORT_VAR: ${{ vars.DEV_FTP_PORT }}
300
+ run: |
301
+ HOST="$HOST_RAW"
302
+ PORT="$PORT_VAR"
303
+
304
+ # Priority 1 — explicit DEV_FTP_PORT variable
305
+ if [ -n "$PORT" ]; then
306
+ echo "ℹ️ Using explicit DEV_FTP_PORT=${PORT}"
307
+
308
+ # Priority 2 — port embedded in DEV_FTP_HOST (host:port)
309
+ elif [[ "$HOST" == *:* ]]; then
310
+ PORT="${HOST##*:}"
311
+ HOST="${HOST%:*}"
312
+ echo "ℹ️ Extracted port ${PORT} from DEV_FTP_HOST"
313
+
314
+ # Priority 3 — SFTP default
315
+ else
316
+ PORT="22"
317
+ echo "ℹ️ No port specified — defaulting to SFTP port 22"
318
+ fi
319
+
320
+ echo "host=${HOST}" >> "$GITHUB_OUTPUT"
321
+ echo "port=${PORT}" >> "$GITHUB_OUTPUT"
322
+ echo "SFTP target: ${HOST}:${PORT}"
323
+
324
+ - name: Build remote path
325
+ if: steps.source.outputs.skip == 'false'
326
+ id: remote
327
+ env:
328
+ DEV_FTP_PATH: ${{ vars.DEV_FTP_PATH }}
329
+ DEV_FTP_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
330
+ run: |
331
+ BASE="$DEV_FTP_PATH"
332
+
333
+ if [ -z "$BASE" ]; then
334
+ echo "❌ DEV_FTP_PATH is not set."
335
+ echo " Configure it as an org-level variable (Settings → Variables) and"
336
+ echo " ensure this repository has been granted access to it."
337
+ exit 1
338
+ fi
339
+
340
+ # DEV_FTP_SUFFIX is required — it identifies the remote subdirectory for this repo.
341
+ # Without it we cannot safely determine the deployment target.
342
+ if [ -z "$DEV_FTP_SUFFIX" ]; then
343
+ echo "⏭️ DEV_FTP_SUFFIX variable is not set — skipping deployment."
344
+ echo " Set DEV_FTP_SUFFIX as a repo or org variable to enable deploy-dev."
345
+ echo "skip=true" >> "$GITHUB_OUTPUT"
346
+ echo "path=" >> "$GITHUB_OUTPUT"
347
+ exit 0
348
+ fi
349
+
350
+ REMOTE="${BASE%/}/${DEV_FTP_SUFFIX#/}"
351
+
352
+ # ── Platform-specific path safety guards ──────────────────────────────
353
+ PLATFORM=""
354
+ MOKO_FILE=".github/.mokostandards"; [ ! -f "$MOKO_FILE" ] && MOKO_FILE=".mokostandards"; if [ -f "$MOKO_FILE" ]; then
355
+ PLATFORM=$(grep -oP '^platform:.*' "$MOKO_FILE" 2>/dev/null || true)
356
+ fi
357
+
358
+ if [ "$PLATFORM" = "crm-module" ]; then
359
+ # Dolibarr modules must deploy under htdocs/custom/ — guard against
360
+ # accidentally overwriting server root or unrelated directories.
361
+ if [[ "$REMOTE" != *custom* ]]; then
362
+ echo "❌ Safety check failed: Dolibarr (crm-module) remote path must contain 'custom'."
363
+ echo " Current path: ${REMOTE}"
364
+ echo " Set DEV_FTP_SUFFIX to the module's htdocs/custom/ subdirectory."
365
+ exit 1
366
+ fi
367
+ fi
368
+
369
+ if [ "$PLATFORM" = "waas-component" ]; then
370
+ # Joomla extensions may only deploy to the server's tmp/ directory.
371
+ if [[ "$REMOTE" != *tmp* ]]; then
372
+ echo "❌ Safety check failed: Joomla (waas-component) remote path must contain 'tmp'."
373
+ echo " Current path: ${REMOTE}"
374
+ echo " Set DEV_FTP_SUFFIX to a path under the server tmp/ directory."
375
+ exit 1
376
+ fi
377
+ fi
378
+
379
+ echo "ℹ️ Remote path: ${REMOTE}"
380
+ echo "path=${REMOTE}" >> "$GITHUB_OUTPUT"
381
+
382
+ - name: Detect SFTP authentication method
383
+ if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true'
384
+ id: auth
385
+ env:
386
+ HAS_KEY: ${{ secrets.DEV_FTP_KEY }}
387
+ HAS_PASSWORD: ${{ secrets.DEV_FTP_PASSWORD }}
388
+ run: |
389
+ if [ -n "$HAS_KEY" ] && [ -n "$HAS_PASSWORD" ]; then
390
+ # Both set: key auth with password as passphrase; falls back to password-only if key fails
391
+ echo "method=key" >> "$GITHUB_OUTPUT"
392
+ echo "use_passphrase=true" >> "$GITHUB_OUTPUT"
393
+ echo "has_password=true" >> "$GITHUB_OUTPUT"
394
+ echo "ℹ️ Primary: SSH key + passphrase (DEV_FTP_KEY / DEV_FTP_PASSWORD)"
395
+ echo "ℹ️ Fallback: password-only auth if key authentication fails"
396
+ elif [ -n "$HAS_KEY" ]; then
397
+ # Key only: no passphrase, no password fallback
398
+ echo "method=key" >> "$GITHUB_OUTPUT"
399
+ echo "use_passphrase=false" >> "$GITHUB_OUTPUT"
400
+ echo "has_password=false" >> "$GITHUB_OUTPUT"
401
+ echo "ℹ️ Using SSH key authentication (DEV_FTP_KEY, no passphrase, no fallback)"
402
+ elif [ -n "$HAS_PASSWORD" ]; then
403
+ # Password only: direct SFTP password auth
404
+ echo "method=password" >> "$GITHUB_OUTPUT"
405
+ echo "use_passphrase=false" >> "$GITHUB_OUTPUT"
406
+ echo "has_password=true" >> "$GITHUB_OUTPUT"
407
+ echo "ℹ️ Using password authentication (DEV_FTP_PASSWORD)"
408
+ else
409
+ echo "❌ No SFTP credentials configured."
410
+ echo " Set DEV_FTP_KEY (preferred) or DEV_FTP_PASSWORD as an org-level secret."
411
+ exit 1
412
+ fi
413
+
414
+ - name: Setup PHP
415
+ if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true'
416
+ uses: shivammathur/setup-php@fcafdd6392932010c2bd5094439b8e33be2a8a09 # v2.37.0
417
+ with:
418
+ php-version: '8.1'
419
+ tools: composer
420
+
421
+ - name: Setup MokoStandards deploy tools
422
+ if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true'
423
+ env:
424
+ GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
425
+ COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}'
426
+ run: |
427
+ git clone --depth 1 --branch version/04 --quiet \
428
+ "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \
429
+ /tmp/mokostandards
430
+ cd /tmp/mokostandards
431
+ composer install --no-dev --no-interaction --quiet
432
+
433
+ - name: Clear remote destination folder (manual only)
434
+ if: >-
435
+ steps.source.outputs.skip == 'false' &&
436
+ steps.remote.outputs.skip != 'true' &&
437
+ inputs.clear_remote == true
438
+ env:
439
+ SFTP_HOST: ${{ steps.conn.outputs.host }}
440
+ SFTP_PORT: ${{ steps.conn.outputs.port }}
441
+ SFTP_USER: ${{ vars.DEV_FTP_USERNAME }}
442
+ SFTP_KEY: ${{ secrets.DEV_FTP_KEY }}
443
+ SFTP_PASSWORD: ${{ secrets.DEV_FTP_PASSWORD }}
444
+ AUTH_METHOD: ${{ steps.auth.outputs.method }}
445
+ USE_PASSPHRASE: ${{ steps.auth.outputs.use_passphrase }}
446
+ HAS_PASSWORD: ${{ steps.auth.outputs.has_password }}
447
+ REMOTE_PATH: ${{ steps.remote.outputs.path }}
448
+ run: |
449
+ cat > /tmp/moko_clear.php << 'PHPEOF'
450
+ <?php
451
+ declare(strict_types=1);
452
+ require '/tmp/mokostandards/vendor/autoload.php';
453
+
454
+ use phpseclib3\Net\SFTP;
455
+ use phpseclib3\Crypt\PublicKeyLoader;
456
+
457
+ $host = (string) getenv('SFTP_HOST');
458
+ $port = (int) getenv('SFTP_PORT');
459
+ $username = (string) getenv('SFTP_USER');
460
+ $authMethod = (string) getenv('AUTH_METHOD');
461
+ $usePassphrase = getenv('USE_PASSPHRASE') === 'true';
462
+ $hasPassword = getenv('HAS_PASSWORD') === 'true';
463
+ $remotePath = rtrim((string) getenv('REMOTE_PATH'), '/');
464
+
465
+ echo "⚠️ Clearing remote folder: {$remotePath}\n";
466
+
467
+ $sftp = new SFTP($host, $port);
468
+
469
+ // ── Authentication ──────────────────────────────────────────────
470
+ if ($authMethod === 'key') {
471
+ $keyData = (string) getenv('SFTP_KEY');
472
+ $passphrase = $usePassphrase ? (string) getenv('SFTP_PASSWORD') : false;
473
+ $password = $hasPassword ? (string) getenv('SFTP_PASSWORD') : '';
474
+ $key = PublicKeyLoader::load($keyData, $passphrase);
475
+ if (!$sftp->login($username, $key)) {
476
+ if ($password !== '') {
477
+ echo "⚠️ Key auth failed — falling back to password\n";
478
+ if (!$sftp->login($username, $password)) {
479
+ fwrite(STDERR, "❌ Both key and password authentication failed\n");
480
+ exit(1);
481
+ }
482
+ echo "✅ Connected via password authentication (key fallback)\n";
483
+ } else {
484
+ fwrite(STDERR, "❌ Key authentication failed and no password fallback is available\n");
485
+ exit(1);
486
+ }
487
+ } else {
488
+ echo "✅ Connected via SSH key authentication\n";
489
+ }
490
+ } else {
491
+ if (!$sftp->login($username, (string) getenv('SFTP_PASSWORD'))) {
492
+ fwrite(STDERR, "❌ Password authentication failed\n");
493
+ exit(1);
494
+ }
495
+ echo "✅ Connected via password authentication\n";
496
+ }
497
+
498
+ // ── Recursive delete ────────────────────────────────────────────
499
+ function rmrf(SFTP $sftp, string $path): void
500
+ {
501
+ $entries = $sftp->nlist($path);
502
+ if ($entries === false) {
503
+ return; // path does not exist — nothing to clear
504
+ }
505
+ foreach ($entries as $name) {
506
+ if ($name === '.' || $name === '..') {
507
+ continue;
508
+ }
509
+ $entry = "{$path}/{$name}";
510
+ if ($sftp->is_dir($entry)) {
511
+ rmrf($sftp, $entry);
512
+ $sftp->rmdir($entry);
513
+ echo " 🗑️ Removed dir: {$entry}\n";
514
+ } else {
515
+ $sftp->delete($entry);
516
+ echo " 🗑️ Removed file: {$entry}\n";
517
+ }
518
+ }
519
+ }
520
+
521
+ // ── Create remote directory tree ────────────────────────────────
522
+ function sftpMakedirs(SFTP $sftp, string $path): void
523
+ {
524
+ $parts = array_values(array_filter(explode('/', $path), fn(string $p) => $p !== ''));
525
+ $current = str_starts_with($path, '/') ? '' : '';
526
+ foreach ($parts as $part) {
527
+ $current .= '/' . $part;
528
+ $sftp->mkdir($current); // silently returns false if already exists
529
+ }
530
+ }
531
+
532
+ rmrf($sftp, $remotePath);
533
+ sftpMakedirs($sftp, $remotePath);
534
+ echo "✅ Remote folder ready: {$remotePath}\n";
535
+ PHPEOF
536
+ php /tmp/moko_clear.php
537
+
538
+ - name: Deploy via SFTP
539
+ if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true'
540
+ env:
541
+ SFTP_HOST: ${{ steps.conn.outputs.host }}
542
+ SFTP_PORT: ${{ steps.conn.outputs.port }}
543
+ SFTP_USER: ${{ vars.DEV_FTP_USERNAME }}
544
+ SFTP_KEY: ${{ secrets.DEV_FTP_KEY }}
545
+ SFTP_PASSWORD: ${{ secrets.DEV_FTP_PASSWORD }}
546
+ AUTH_METHOD: ${{ steps.auth.outputs.method }}
547
+ USE_PASSPHRASE: ${{ steps.auth.outputs.use_passphrase }}
548
+ REMOTE_PATH: ${{ steps.remote.outputs.path }}
549
+ SOURCE_DIR: ${{ steps.source.outputs.dir }}
550
+ run: |
551
+ # ── Write SSH key to temp file (key auth only) ────────────────────────
552
+ if [ "$AUTH_METHOD" = "key" ]; then
553
+ printf '%s' "$SFTP_KEY" > /tmp/deploy_key
554
+ chmod 600 /tmp/deploy_key
555
+ fi
556
+
557
+ # ── Generate sftp-config.json safely via jq ───────────────────────────
558
+ if [ "$AUTH_METHOD" = "key" ]; then
559
+ jq -n \
560
+ --arg host "$SFTP_HOST" \
561
+ --argjson port "${SFTP_PORT:-22}" \
562
+ --arg user "$SFTP_USER" \
563
+ --arg path "$REMOTE_PATH" \
564
+ --arg key "/tmp/deploy_key" \
565
+ '{host:$host, port:$port, user:$user, remote_path:$path, ssh_key_file:$key}' \
566
+ > /tmp/sftp-config.json
567
+ else
568
+ jq -n \
569
+ --arg host "$SFTP_HOST" \
570
+ --argjson port "${SFTP_PORT:-22}" \
571
+ --arg user "$SFTP_USER" \
572
+ --arg path "$REMOTE_PATH" \
573
+ --arg pass "$SFTP_PASSWORD" \
574
+ '{host:$host, port:$port, user:$user, remote_path:$path, password:$pass}' \
575
+ > /tmp/sftp-config.json
576
+ fi
577
+
578
+ # Dev deploys skip minified files — use unminified sources for debugging
579
+ echo "*.min.js" >> "${SOURCE_DIR}/.ftpignore"
580
+ echo "*.min.css" >> "${SOURCE_DIR}/.ftpignore"
581
+
582
+ # ── Run deploy-sftp.php from MokoStandards ────────────────────────────
583
+ DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json)
584
+ if [ "$USE_PASSPHRASE" = "true" ]; then
585
+ DEPLOY_ARGS+=(--key-passphrase "$SFTP_PASSWORD")
586
+ fi
587
+
588
+ # Set platform version to "development" before deploy (Dolibarr + Joomla)
589
+ php /tmp/mokostandards/api/cli/version_set_platform.php --path . --version development
590
+
591
+ # Write update files — dev/** = development, rc/** = rc
592
+ PLATFORM=$(php /tmp/mokostandards/api/cli/platform_detect.php --path . 2>/dev/null || true)
593
+ REPO="${{ github.repository }}"
594
+ BRANCH="${{ github.ref_name }}"
595
+
596
+ # Determine stability tag from branch prefix
597
+ STABILITY="development"
598
+ VERSION_LABEL="development"
599
+ if [[ "$BRANCH" == rc/* ]]; then
600
+ STABILITY="rc"
601
+ VERSION_LABEL=$(php /tmp/mokostandards/api/cli/version_read.php --path . 2>/dev/null || echo "${BRANCH#rc/}")-rc
602
+ fi
603
+
604
+ if [ "$PLATFORM" = "crm-module" ]; then
605
+ printf '%s' "$VERSION_LABEL" > update.txt
606
+ fi
607
+
608
+ if [ "$PLATFORM" = "waas-component" ]; then
609
+ MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true)
610
+ if [ -n "$MANIFEST" ]; then
611
+ EXT_NAME=$(grep -oP '<name>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || echo "${{ github.event.repository.name }}")
612
+ EXT_TYPE=$(grep -oP '<extension[^>]+type="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "component")
613
+ EXT_ELEMENT=$(grep -oP '<element>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || basename "$MANIFEST" .xml)
614
+ EXT_CLIENT=$(grep -oP '<extension[^>]+client="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "")
615
+ EXT_FOLDER=$(grep -oP '<extension[^>]+group="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "")
616
+ TARGET_PLATFORM=$(grep -oP '<targetplatform[^/]*/' "$MANIFEST" 2>/dev/null | head -1 || true)
617
+ [ -n "$TARGET_PLATFORM" ] && TARGET_PLATFORM="${TARGET_PLATFORM}>"
618
+ [ -z "$TARGET_PLATFORM" ] && TARGET_PLATFORM=$(printf '<targetplatform name="joomla" version="5.*" %s>' "/")
619
+
620
+ CLIENT_TAG=""
621
+ if [ -n "$EXT_CLIENT" ]; then
622
+ CLIENT_TAG="<client>${EXT_CLIENT}</client>"
623
+ elif [ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]; then
624
+ CLIENT_TAG="<client>site</client>"
625
+ fi
626
+
627
+ FOLDER_TAG=""
628
+ if [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ]; then
629
+ FOLDER_TAG="<folder>${EXT_FOLDER}</folder>"
630
+ fi
631
+
632
+ DOWNLOAD_URL="https://github.com/${REPO}/archive/refs/heads/${BRANCH}.zip"
633
+
634
+ {
635
+ printf '%s\n' '<?xml version="1.0" encoding="utf-8"?>'
636
+ printf '%s\n' '<updates>'
637
+ printf '%s\n' ' <update>'
638
+ printf '%s\n' " <name>${EXT_NAME}</name>"
639
+ printf '%s\n' " <description>${EXT_NAME} ${STABILITY} build</description>"
640
+ printf '%s\n' " <element>${EXT_ELEMENT}</element>"
641
+ printf '%s\n' " <type>${EXT_TYPE}</type>"
642
+ printf '%s\n' " <version>${VERSION_LABEL}</version>"
643
+ [ -n "$CLIENT_TAG" ] && printf '%s\n' " ${CLIENT_TAG}"
644
+ [ -n "$FOLDER_TAG" ] && printf '%s\n' " ${FOLDER_TAG}"
645
+ printf '%s\n' ' <tags>'
646
+ printf '%s\n' " <tag>${STABILITY}</tag>"
647
+ printf '%s\n' ' </tags>'
648
+ printf '%s\n' " <infourl title=\"${EXT_NAME}\">https://github.com/${REPO}/tree/${BRANCH}</infourl>"
649
+ printf '%s\n' ' <downloads>'
650
+ printf '%s\n' " <downloadurl type=\"full\" format=\"zip\">${DOWNLOAD_URL}</downloadurl>"
651
+ printf '%s\n' ' </downloads>'
652
+ printf '%s\n' " ${TARGET_PLATFORM}"
653
+ printf '%s\n' ' <maintainer>Moko Consulting</maintainer>'
654
+ printf '%s\n' ' <maintainerurl>https://mokoconsulting.tech</maintainerurl>'
655
+ printf '%s\n' ' </update>'
656
+ printf '%s\n' '</updates>'
657
+ } > updates.xml
658
+ sed -i '/^[[:space:]]*$/d' updates.xml
659
+ fi
660
+ fi
661
+
662
+ # Use Joomla-aware deploy for waas-component (routes files to correct Joomla dirs)
663
+ # Use standard SFTP deploy for everything else
664
+ PLATFORM=$(php /tmp/mokostandards/api/cli/platform_detect.php --path . 2>/dev/null || true)
665
+ if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards/api/deploy/deploy-joomla.php" ]; then
666
+ php /tmp/mokostandards/api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}"
667
+ else
668
+ php /tmp/mokostandards/api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}"
669
+ fi
670
+ # (both scripts handle dotfile skipping and .ftpignore natively)
671
+ # Remove temp files that should never be left behind
672
+ rm -f /tmp/deploy_key /tmp/sftp-config.json
673
+
674
+ # Dev deploys fail silently — no issue creation.
675
+ # Demo and RS deploys create failure issues (production-facing).
676
+
677
+ - name: Deployment summary
678
+ if: always()
679
+ run: |
680
+ if [ "${{ steps.source.outputs.skip }}" == "true" ]; then
681
+ echo "### ⏭️ Deployment Skipped" >> "$GITHUB_STEP_SUMMARY"
682
+ echo "" >> "$GITHUB_STEP_SUMMARY"
683
+ echo "No \`src/\` directory found in this repository." >> "$GITHUB_STEP_SUMMARY"
684
+ elif [ "${{ job.status }}" == "success" ]; then
685
+ echo "" >> "$GITHUB_STEP_SUMMARY"
686
+ echo "### ✅ Dev Deployment Successful" >> "$GITHUB_STEP_SUMMARY"
687
+ echo "" >> "$GITHUB_STEP_SUMMARY"
688
+ echo "| Field | Value |" >> "$GITHUB_STEP_SUMMARY"
689
+ echo "|-------|-------|" >> "$GITHUB_STEP_SUMMARY"
690
+ echo "| Host | \`${{ steps.conn.outputs.host }}:${{ steps.conn.outputs.port }}\` |" >> "$GITHUB_STEP_SUMMARY"
691
+ echo "| Remote path | \`${{ steps.remote.outputs.path }}\` |" >> "$GITHUB_STEP_SUMMARY"
692
+ echo "| Source | \`src/\` |" >> "$GITHUB_STEP_SUMMARY"
693
+ echo "| Trigger | ${{ github.event_name }} |" >> "$GITHUB_STEP_SUMMARY"
694
+ echo "| Auth | ${{ steps.auth.outputs.method }} |" >> "$GITHUB_STEP_SUMMARY"
695
+ echo "| Clear remote | ${{ inputs.clear_remote || 'false' }} |" >> "$GITHUB_STEP_SUMMARY"
696
+ else
697
+ echo "### ❌ Dev Deployment Failed" >> "$GITHUB_STEP_SUMMARY"
698
+ echo "" >> "$GITHUB_STEP_SUMMARY"
699
+ echo "Check the job log above for error details." >> "$GITHUB_STEP_SUMMARY"
700
+ fi