@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,734 @@
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-demo.yml.template
25
+ # VERSION: 04.06.00
26
+ # BRIEF: SFTP deployment workflow for demo server — synced to all governed repos
27
+ # NOTE: Synced via bulk-repo-sync to .github/workflows/deploy-demo.yml in all governed repos.
28
+ # Port is resolved in order: DEMO_FTP_PORT variable → :port suffix in DEMO_FTP_HOST → 22.
29
+
30
+ name: Deploy to Demo Server (SFTP)
31
+
32
+ # Deploys the contents of the src/ directory to the demo server via SFTP.
33
+ # Triggers on push/merge to main — deploys the production-ready build to the demo server.
34
+ #
35
+ # Required org-level variables: DEMO_FTP_HOST, DEMO_FTP_PATH, DEMO_FTP_USERNAME
36
+ # Optional org-level variable: DEMO_FTP_PORT (auto-detected from host or defaults to 22)
37
+ # Optional org/repo variable: DEMO_FTP_SUFFIX — when set, appended to DEMO_FTP_PATH to form the
38
+ # full remote destination: DEMO_FTP_PATH/DEMO_FTP_SUFFIX
39
+ # Ignore rules: Place a .ftpignore file in the src/ directory. Each non-empty,
40
+ # non-comment line is a glob pattern tested against the relative path
41
+ # of each file (e.g. "subdir/file.txt"). The .gitignore is NOT used.
42
+ # Required org-level secret: DEMO_FTP_KEY (preferred) or DEMO_FTP_PASSWORD
43
+ #
44
+ # Access control: only users with admin or maintain role on the repository may deploy.
45
+
46
+ on:
47
+ push:
48
+ branches:
49
+ - main
50
+ - master
51
+ paths:
52
+ - 'src/**'
53
+ - 'htdocs/**'
54
+ pull_request:
55
+ types: [opened, synchronize, reopened, closed]
56
+ branches:
57
+ - main
58
+ - master
59
+ paths:
60
+ - 'src/**'
61
+ - 'htdocs/**'
62
+ workflow_dispatch:
63
+ inputs:
64
+ clear_remote:
65
+ description: 'Delete all files inside the remote destination folder before uploading'
66
+ required: false
67
+ default: false
68
+ type: boolean
69
+
70
+ permissions:
71
+ contents: read
72
+ pull-requests: write
73
+
74
+ env:
75
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
76
+
77
+ jobs:
78
+ check-permission:
79
+ name: Verify Deployment Permission
80
+ runs-on: ubuntu-latest
81
+ steps:
82
+ - name: Check actor permission
83
+ env:
84
+ # Prefer the org-scoped GH_TOKEN secret (needed for the org membership
85
+ # fallback). Falls back to the built-in github.token so the collaborator
86
+ # endpoint still works even if GH_TOKEN is not configured.
87
+ GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
88
+ run: |
89
+ ACTOR="${{ github.actor }}"
90
+ REPO="${{ github.repository }}"
91
+ ORG="${{ github.repository_owner }}"
92
+
93
+ METHOD=""
94
+ AUTHORIZED="false"
95
+
96
+ # Hardcoded authorized users — always allowed to deploy
97
+ AUTHORIZED_USERS="jmiller github-actions[bot]"
98
+ for user in $AUTHORIZED_USERS; do
99
+ if [ "$ACTOR" = "$user" ]; then
100
+ AUTHORIZED="true"
101
+ METHOD="hardcoded allowlist"
102
+ PERMISSION="admin"
103
+ break
104
+ fi
105
+ done
106
+
107
+ # For other actors, check repo/org permissions via API
108
+ if [ "$AUTHORIZED" != "true" ]; then
109
+ PERMISSION=$(gh api "repos/${REPO}/collaborators/${ACTOR}/permission" \
110
+ --jq '.permission' 2>/dev/null)
111
+ METHOD="repo collaborator API"
112
+
113
+ if [ -z "$PERMISSION" ]; then
114
+ ORG_ROLE=$(gh api "orgs/${ORG}/memberships/${ACTOR}" \
115
+ --jq '.role' 2>/dev/null)
116
+ METHOD="org membership API"
117
+ if [ "$ORG_ROLE" = "owner" ]; then
118
+ PERMISSION="admin"
119
+ else
120
+ PERMISSION="none"
121
+ fi
122
+ fi
123
+
124
+ case "$PERMISSION" in
125
+ admin|maintain) AUTHORIZED="true" ;;
126
+ esac
127
+ fi
128
+
129
+ # Write detailed summary
130
+ {
131
+ echo "## 🔐 Deploy Authorization"
132
+ echo ""
133
+ echo "| Field | Value |"
134
+ echo "|-------|-------|"
135
+ echo "| **Actor** | \`${ACTOR}\` |"
136
+ echo "| **Repository** | \`${REPO}\` |"
137
+ echo "| **Permission** | \`${PERMISSION}\` |"
138
+ echo "| **Method** | ${METHOD} |"
139
+ echo "| **Authorized** | ${AUTHORIZED} |"
140
+ echo "| **Trigger** | \`${{ github.event_name }}\` |"
141
+ echo "| **Branch** | \`${{ github.ref_name }}\` |"
142
+ echo ""
143
+ } >> "$GITHUB_STEP_SUMMARY"
144
+
145
+ if [ "$AUTHORIZED" = "true" ]; then
146
+ echo "✅ ${ACTOR} authorized to deploy (${METHOD})" >> "$GITHUB_STEP_SUMMARY"
147
+ else
148
+ echo "❌ ${ACTOR} is NOT authorized to deploy." >> "$GITHUB_STEP_SUMMARY"
149
+ echo "" >> "$GITHUB_STEP_SUMMARY"
150
+ echo "Deployment requires one of:" >> "$GITHUB_STEP_SUMMARY"
151
+ echo "- Being in the hardcoded allowlist" >> "$GITHUB_STEP_SUMMARY"
152
+ echo "- Having \`admin\` or \`maintain\` role on the repository" >> "$GITHUB_STEP_SUMMARY"
153
+ exit 1
154
+ fi
155
+
156
+ deploy:
157
+ name: SFTP Deploy → Demo
158
+ runs-on: ubuntu-latest
159
+ needs: [check-permission]
160
+ if: >-
161
+ !startsWith(github.head_ref || github.ref_name, 'chore/') &&
162
+ (github.event_name == 'workflow_dispatch' ||
163
+ github.event_name == 'push' ||
164
+ (github.event_name == 'pull_request' &&
165
+ (github.event.action == 'opened' ||
166
+ github.event.action == 'synchronize' ||
167
+ github.event.action == 'reopened' ||
168
+ github.event.pull_request.merged == true)))
169
+
170
+ steps:
171
+ - name: Checkout repository
172
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
173
+
174
+ - name: Resolve source directory
175
+ id: source
176
+ run: |
177
+ # Resolve source directory: src/ preferred, htdocs/ as fallback
178
+ if [ -d "src" ]; then
179
+ SRC="src"
180
+ elif [ -d "htdocs" ]; then
181
+ SRC="htdocs"
182
+ else
183
+ echo "⚠️ No src/ or htdocs/ directory found — skipping deployment"
184
+ echo "skip=true" >> "$GITHUB_OUTPUT"
185
+ exit 0
186
+ fi
187
+ COUNT=$(find "$SRC" -type f | wc -l)
188
+ echo "✅ Source: ${SRC}/ (${COUNT} file(s))"
189
+ echo "skip=false" >> "$GITHUB_OUTPUT"
190
+ echo "dir=${SRC}" >> "$GITHUB_OUTPUT"
191
+
192
+ - name: Preview files to deploy
193
+ if: steps.source.outputs.skip == 'false'
194
+ env:
195
+ SOURCE_DIR: ${{ steps.source.outputs.dir }}
196
+ run: |
197
+ # ── Convert a ftpignore-style glob line to an ERE pattern ──────────────
198
+ ftpignore_to_regex() {
199
+ local line="$1"
200
+ local anchored=false
201
+ # Strip inline comments and whitespace
202
+ line=$(printf '%s' "$line" | sed 's/[[:space:]]*#.*$//' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
203
+ [ -z "$line" ] && return
204
+ # Skip negation patterns (not supported)
205
+ [[ "$line" == !* ]] && return
206
+ # Trailing slash = directory marker; strip it
207
+ line="${line%/}"
208
+ # Leading slash = anchored to root; strip it
209
+ if [[ "$line" == /* ]]; then
210
+ anchored=true
211
+ line="${line#/}"
212
+ fi
213
+ # Escape ERE special chars, then restore glob semantics
214
+ local regex
215
+ regex=$(printf '%s' "$line" \
216
+ | sed 's/[.+^${}()|[\\]/\\&/g' \
217
+ | sed 's/\\\*\\\*/\x01/g' \
218
+ | sed 's/\\\*/[^\/]*/g' \
219
+ | sed 's/\x01/.*/g' \
220
+ | sed 's/\\\?/[^\/]/g')
221
+ if $anchored; then
222
+ printf '^%s(/|$)' "$regex"
223
+ else
224
+ printf '(^|/)%s(/|$)' "$regex"
225
+ fi
226
+ }
227
+
228
+ # ── Read .ftpignore (ftpignore-style globs) ─────────────────────────
229
+ IGNORE_PATTERNS=()
230
+ IGNORE_SOURCES=()
231
+ if [ -f "${SOURCE_DIR}/.ftpignore" ]; then
232
+ while IFS= read -r line; do
233
+ [[ "$line" =~ ^[[:space:]]*$ || "$line" =~ ^[[:space:]]*# ]] && continue
234
+ regex=$(ftpignore_to_regex "$line")
235
+ [ -n "$regex" ] && IGNORE_PATTERNS+=("$regex") && IGNORE_SOURCES+=("$line")
236
+ done < "${SOURCE_DIR}/.ftpignore"
237
+ fi
238
+
239
+ # ── Walk src/ and classify every file ────────────────────────────────
240
+ WILL_UPLOAD=()
241
+ IGNORED_FILES=()
242
+ while IFS= read -r -d '' file; do
243
+ rel="${file#${SOURCE_DIR}/}"
244
+ SKIP=false
245
+ for i in "${!IGNORE_PATTERNS[@]}"; do
246
+ if echo "$rel" | grep -qE "${IGNORE_PATTERNS[$i]}" 2>/dev/null; then
247
+ IGNORED_FILES+=("$rel | .ftpignore \`${IGNORE_SOURCES[$i]}\`")
248
+ SKIP=true; break
249
+ fi
250
+ done
251
+ $SKIP && continue
252
+ WILL_UPLOAD+=("$rel")
253
+ done < <(find "$SOURCE_DIR" -type f -print0 | sort -z)
254
+
255
+ UPLOAD_COUNT="${#WILL_UPLOAD[@]}"
256
+ IGNORE_COUNT="${#IGNORED_FILES[@]}"
257
+
258
+ echo "ℹ️ ${UPLOAD_COUNT} file(s) will be uploaded, ${IGNORE_COUNT} ignored"
259
+
260
+ # ── Write deployment preview to step summary ──────────────────────────
261
+ {
262
+ echo "## 📋 Deployment Preview"
263
+ echo ""
264
+ echo "| Field | Value |"
265
+ echo "|---|---|"
266
+ echo "| Source | \`${SOURCE_DIR}/\` |"
267
+ echo "| Files to upload | **${UPLOAD_COUNT}** |"
268
+ echo "| Files ignored | **${IGNORE_COUNT}** |"
269
+ echo ""
270
+ if [ "${UPLOAD_COUNT}" -gt 0 ]; then
271
+ echo "### 📂 Files that will be uploaded"
272
+ echo '```'
273
+ printf '%s\n' "${WILL_UPLOAD[@]}"
274
+ echo '```'
275
+ echo ""
276
+ fi
277
+ if [ "${IGNORE_COUNT}" -gt 0 ]; then
278
+ echo "### ⏭️ Files excluded"
279
+ echo "| File | Reason |"
280
+ echo "|---|---|"
281
+ for entry in "${IGNORED_FILES[@]}"; do
282
+ f="${entry% | *}"; r="${entry##* | }"
283
+ echo "| \`${f}\` | ${r} |"
284
+ done
285
+ echo ""
286
+ fi
287
+ } >> "$GITHUB_STEP_SUMMARY"
288
+
289
+ - name: Resolve SFTP host and port
290
+ if: steps.source.outputs.skip == 'false'
291
+ id: conn
292
+ env:
293
+ HOST_RAW: ${{ vars.DEMO_FTP_HOST }}
294
+ PORT_VAR: ${{ vars.DEMO_FTP_PORT }}
295
+ run: |
296
+ HOST="$HOST_RAW"
297
+ PORT="$PORT_VAR"
298
+
299
+ if [ -z "$HOST" ]; then
300
+ echo "⏭️ DEMO_FTP_HOST not configured — skipping demo deployment."
301
+ echo "skip=true" >> "$GITHUB_OUTPUT"
302
+ exit 0
303
+ fi
304
+
305
+ # Priority 1 — explicit DEMO_FTP_PORT variable
306
+ if [ -n "$PORT" ]; then
307
+ echo "ℹ️ Using explicit DEMO_FTP_PORT=${PORT}"
308
+
309
+ # Priority 2 — port embedded in DEMO_FTP_HOST (host:port)
310
+ elif [[ "$HOST" == *:* ]]; then
311
+ PORT="${HOST##*:}"
312
+ HOST="${HOST%:*}"
313
+ echo "ℹ️ Extracted port ${PORT} from DEMO_FTP_HOST"
314
+
315
+ # Priority 3 — SFTP default
316
+ else
317
+ PORT="22"
318
+ echo "ℹ️ No port specified — defaulting to SFTP port 22"
319
+ fi
320
+
321
+ echo "host=${HOST}" >> "$GITHUB_OUTPUT"
322
+ echo "port=${PORT}" >> "$GITHUB_OUTPUT"
323
+ echo "SFTP target: ${HOST}:${PORT}"
324
+
325
+ - name: Build remote path
326
+ if: steps.source.outputs.skip == 'false' && steps.conn.outputs.skip != 'true'
327
+ id: remote
328
+ env:
329
+ DEMO_FTP_PATH: ${{ vars.DEMO_FTP_PATH }}
330
+ DEMO_FTP_SUFFIX: ${{ vars.DEMO_FTP_SUFFIX }}
331
+ run: |
332
+ BASE="$DEMO_FTP_PATH"
333
+
334
+ if [ -z "$BASE" ]; then
335
+ echo "⏭️ DEMO_FTP_PATH not configured — skipping demo deployment."
336
+ echo "skip=true" >> "$GITHUB_OUTPUT"
337
+ exit 0
338
+ fi
339
+
340
+ # DEMO_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 "$DEMO_FTP_SUFFIX" ]; then
343
+ echo "⏭️ DEMO_FTP_SUFFIX variable is not set — skipping deployment."
344
+ echo " Set DEMO_FTP_SUFFIX as a repo or org variable to enable deploy-demo."
345
+ echo "skip=true" >> "$GITHUB_OUTPUT"
346
+ echo "path=" >> "$GITHUB_OUTPUT"
347
+ exit 0
348
+ fi
349
+
350
+ REMOTE="${BASE%/}/${DEMO_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 -E '^platform:' "$MOKO_FILE" | sed 's/.*:[[:space:]]*//' | tr -d '"')
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 DEMO_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 DEMO_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.DEMO_FTP_KEY }}
387
+ HAS_PASSWORD: ${{ secrets.DEMO_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 (DEMO_FTP_KEY / DEMO_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 (DEMO_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 (DEMO_FTP_PASSWORD)"
408
+ else
409
+ echo "❌ No SFTP credentials configured."
410
+ echo " Set DEMO_FTP_KEY (preferred) or DEMO_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.DEMO_FTP_USERNAME }}
442
+ SFTP_KEY: ${{ secrets.DEMO_FTP_KEY }}
443
+ SFTP_PASSWORD: ${{ secrets.DEMO_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.DEMO_FTP_USERNAME }}
544
+ SFTP_KEY: ${{ secrets.DEMO_FTP_KEY }}
545
+ SFTP_PASSWORD: ${{ secrets.DEMO_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
+ # ── Write update files (demo = stable) ─────────────────────────────
579
+ PLATFORM=$(php /tmp/mokostandards/api/cli/platform_detect.php --path . 2>/dev/null || true)
580
+ VERSION=$(php /tmp/mokostandards/api/cli/version_read.php --path . 2>/dev/null || echo "unknown")
581
+ REPO="${{ github.repository }}"
582
+
583
+ if [ "$PLATFORM" = "crm-module" ]; then
584
+ printf '%s' "$VERSION" > update.txt
585
+ fi
586
+
587
+ if [ "$PLATFORM" = "waas-component" ]; then
588
+ MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true)
589
+ if [ -n "$MANIFEST" ]; then
590
+ EXT_NAME=$(grep -oP '<name>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || echo "${{ github.event.repository.name }}")
591
+ EXT_TYPE=$(grep -oP '<extension[^>]+type="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "component")
592
+ EXT_ELEMENT=$(grep -oP '<element>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || basename "$MANIFEST" .xml)
593
+ EXT_CLIENT=$(grep -oP '<extension[^>]+client="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "")
594
+ EXT_FOLDER=$(grep -oP '<extension[^>]+group="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "")
595
+ TARGET_PLATFORM=$(grep -oP '<targetplatform[^/]*/' "$MANIFEST" 2>/dev/null | head -1 || true)
596
+ [ -n "$TARGET_PLATFORM" ] && TARGET_PLATFORM="${TARGET_PLATFORM}>"
597
+ [ -z "$TARGET_PLATFORM" ] && TARGET_PLATFORM=$(printf '<targetplatform name="joomla" version="5.*" %s>' "/")
598
+
599
+ CLIENT_TAG=""
600
+ if [ -n "$EXT_CLIENT" ]; then CLIENT_TAG="<client>${EXT_CLIENT}</client>"; elif [ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]; then CLIENT_TAG="<client>site</client>"; fi
601
+ FOLDER_TAG=""
602
+ if [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ]; then FOLDER_TAG="<folder>${EXT_FOLDER}</folder>"; fi
603
+
604
+ DOWNLOAD_URL="https://github.com/${REPO}/releases/download/v${VERSION}/${EXT_ELEMENT}-${VERSION}.zip"
605
+ {
606
+ printf '%s\n' '<?xml version="1.0" encoding="utf-8"?>'
607
+ printf '%s\n' '<updates>'
608
+ printf '%s\n' ' <update>'
609
+ printf '%s\n' " <name>${EXT_NAME}</name>"
610
+ printf '%s\n' " <description>${EXT_NAME} update</description>"
611
+ printf '%s\n' " <element>${EXT_ELEMENT}</element>"
612
+ printf '%s\n' " <type>${EXT_TYPE}</type>"
613
+ printf '%s\n' " <version>${VERSION}</version>"
614
+ [ -n "$CLIENT_TAG" ] && printf '%s\n' " ${CLIENT_TAG}"
615
+ [ -n "$FOLDER_TAG" ] && printf '%s\n' " ${FOLDER_TAG}"
616
+ printf '%s\n' ' <tags>'
617
+ printf '%s\n' ' <tag>stable</tag>'
618
+ printf '%s\n' ' </tags>'
619
+ printf '%s\n' " <infourl title=\"${EXT_NAME}\">https://github.com/${REPO}</infourl>"
620
+ printf '%s\n' ' <downloads>'
621
+ printf '%s\n' " <downloadurl type=\"full\" format=\"zip\">${DOWNLOAD_URL}</downloadurl>"
622
+ printf '%s\n' ' </downloads>'
623
+ printf '%s\n' " ${TARGET_PLATFORM}"
624
+ printf '%s\n' ' <maintainer>Moko Consulting</maintainer>'
625
+ printf '%s\n' ' <maintainerurl>https://mokoconsulting.tech</maintainerurl>'
626
+ printf '%s\n' ' </update>'
627
+ printf '%s\n' '</updates>'
628
+ } > updates.xml
629
+ fi
630
+ fi
631
+
632
+ # ── Run deploy-sftp.php from MokoStandards ────────────────────────────
633
+ DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json)
634
+ if [ "$USE_PASSPHRASE" = "true" ]; then
635
+ DEPLOY_ARGS+=(--key-passphrase "$SFTP_PASSWORD")
636
+ fi
637
+
638
+ PLATFORM=$(php /tmp/mokostandards/api/cli/platform_detect.php --path . 2>/dev/null || true)
639
+ if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards/api/deploy/deploy-joomla.php" ]; then
640
+ php /tmp/mokostandards/api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}"
641
+ else
642
+ php /tmp/mokostandards/api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}"
643
+ fi
644
+ # Remove temp files that should never be left behind
645
+ rm -f /tmp/deploy_key /tmp/sftp-config.json
646
+
647
+ - name: Create or update failure issue
648
+ if: failure() && steps.remote.outputs.skip != 'true' && steps.conn.outputs.skip != 'true'
649
+ env:
650
+ GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
651
+ run: |
652
+ REPO="${{ github.repository }}"
653
+ RUN_URL="${{ github.server_url }}/${REPO}/actions/runs/${{ github.run_id }}"
654
+ ACTOR="${{ github.actor }}"
655
+ BRANCH="${{ github.ref_name }}"
656
+ EVENT="${{ github.event_name }}"
657
+ NOW=$(date -u '+%Y-%m-%d %H:%M:%S UTC')
658
+ LABEL="deploy-failure"
659
+
660
+ TITLE="fix: Demo deployment failed — ${REPO}"
661
+ BODY="## Demo Deployment Failed
662
+
663
+ A deployment to the demo server failed and requires attention.
664
+
665
+ | Field | Value |
666
+ |-------|-------|
667
+ | **Repository** | \`${REPO}\` |
668
+ | **Branch** | \`${BRANCH}\` |
669
+ | **Trigger** | ${EVENT} |
670
+ | **Actor** | @${ACTOR} |
671
+ | **Failed at** | ${NOW} |
672
+ | **Run** | [View workflow run](${RUN_URL}) |
673
+
674
+ ### Next steps
675
+ 1. Review the [workflow run log](${RUN_URL}) for the specific error.
676
+ 2. Fix the underlying issue (credentials, SFTP connectivity, permissions).
677
+ 3. Re-trigger the deployment via **Actions → Deploy to Demo Server → Run workflow**.
678
+
679
+ ---
680
+ *Auto-created by deploy-demo.yml — close this issue once the deployment is resolved.*"
681
+
682
+ # Ensure the label exists (idempotent — no-op if already present)
683
+ gh label create "$LABEL" \
684
+ --repo "$REPO" \
685
+ --color "CC0000" \
686
+ --description "Automated deploy failure tracking" \
687
+ --force 2>/dev/null || true
688
+
689
+ # Look for an existing open deploy-failure issue
690
+ EXISTING=$(gh api "repos/${REPO}/issues?labels=${LABEL}&state=all&per_page=1&sort=created&direction=desc" \
691
+ --jq '.[0].number' 2>/dev/null)
692
+
693
+ if [ -n "$EXISTING" ] && [ "$EXISTING" != "null" ]; then
694
+ gh api "repos/${REPO}/issues/${EXISTING}" \
695
+ -X PATCH \
696
+ -f title="$TITLE" \
697
+ -f body="$BODY" \
698
+ -f state="open" \
699
+ --silent
700
+ echo "📋 Failure issue #${EXISTING} updated/reopened: ${REPO}" >> "$GITHUB_STEP_SUMMARY"
701
+ else
702
+ gh issue create \
703
+ --repo "$REPO" \
704
+ --title "$TITLE" \
705
+ --body "$BODY" \
706
+ --label "$LABEL" \
707
+ --assignee "jmiller" \
708
+ | tee -a "$GITHUB_STEP_SUMMARY"
709
+ fi
710
+
711
+ - name: Deployment summary
712
+ if: always()
713
+ run: |
714
+ if [ "${{ steps.source.outputs.skip }}" == "true" ]; then
715
+ echo "### ⏭️ Deployment Skipped" >> "$GITHUB_STEP_SUMMARY"
716
+ echo "" >> "$GITHUB_STEP_SUMMARY"
717
+ echo "No \`src/\` directory found in this repository." >> "$GITHUB_STEP_SUMMARY"
718
+ elif [ "${{ job.status }}" == "success" ]; then
719
+ echo "" >> "$GITHUB_STEP_SUMMARY"
720
+ echo "### ✅ Demo Deployment Successful" >> "$GITHUB_STEP_SUMMARY"
721
+ echo "" >> "$GITHUB_STEP_SUMMARY"
722
+ echo "| Field | Value |" >> "$GITHUB_STEP_SUMMARY"
723
+ echo "|-------|-------|" >> "$GITHUB_STEP_SUMMARY"
724
+ echo "| Host | \`${{ steps.conn.outputs.host }}:${{ steps.conn.outputs.port }}\` |" >> "$GITHUB_STEP_SUMMARY"
725
+ echo "| Remote path | \`${{ steps.remote.outputs.path }}\` |" >> "$GITHUB_STEP_SUMMARY"
726
+ echo "| Source | \`src/\` |" >> "$GITHUB_STEP_SUMMARY"
727
+ echo "| Trigger | ${{ github.event_name }} |" >> "$GITHUB_STEP_SUMMARY"
728
+ echo "| Auth | ${{ steps.auth.outputs.method }} |" >> "$GITHUB_STEP_SUMMARY"
729
+ echo "| Clear remote | ${{ inputs.clear_remote || 'false' }} |" >> "$GITHUB_STEP_SUMMARY"
730
+ else
731
+ echo "### ❌ Demo Deployment Failed" >> "$GITHUB_STEP_SUMMARY"
732
+ echo "" >> "$GITHUB_STEP_SUMMARY"
733
+ echo "Check the job log above for error details." >> "$GITHUB_STEP_SUMMARY"
734
+ fi