@mokoconsulting/mcp-mokosuite 1.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 (73) hide show
  1. package/.editorconfig +19 -0
  2. package/.gitattributes +94 -0
  3. package/.gitmessage +9 -0
  4. package/.mokogit/ISSUE_TEMPLATE/adr.md +110 -0
  5. package/.mokogit/ISSUE_TEMPLATE/bug_report.md +48 -0
  6. package/.mokogit/ISSUE_TEMPLATE/config.yml +18 -0
  7. package/.mokogit/ISSUE_TEMPLATE/documentation.md +52 -0
  8. package/.mokogit/ISSUE_TEMPLATE/feature_request.md +51 -0
  9. package/.mokogit/ISSUE_TEMPLATE/mcp_api_integration.md +48 -0
  10. package/.mokogit/ISSUE_TEMPLATE/mcp_connection_issue.md +69 -0
  11. package/.mokogit/ISSUE_TEMPLATE/mcp_tool_request.md +50 -0
  12. package/.mokogit/ISSUE_TEMPLATE/question.md +82 -0
  13. package/.mokogit/ISSUE_TEMPLATE/rfc.md +126 -0
  14. package/.mokogit/ISSUE_TEMPLATE/security.md +51 -0
  15. package/.mokogit/ISSUE_TEMPLATE/version.md +24 -0
  16. package/.mokogit/actions/resolve-source-dir/action.yml +108 -0
  17. package/.mokogit/workflows/auto-assign.yml +76 -0
  18. package/.mokogit/workflows/auto-bump.yml +78 -0
  19. package/.mokogit/workflows/auto-dev-issue.yml +207 -0
  20. package/.mokogit/workflows/auto-release.yml +596 -0
  21. package/.mokogit/workflows/branch-cleanup.yml +60 -0
  22. package/.mokogit/workflows/cascade-dev.yml +198 -0
  23. package/.mokogit/workflows/changelog-validation.yml +101 -0
  24. package/.mokogit/workflows/ci-generic.yml +203 -0
  25. package/.mokogit/workflows/ci-issue-reporter.yml +75 -0
  26. package/.mokogit/workflows/cleanup.yml +87 -0
  27. package/.mokogit/workflows/gitleaks.yml +94 -0
  28. package/.mokogit/workflows/issue-branch.yml +81 -0
  29. package/.mokogit/workflows/notify.yml +73 -0
  30. package/.mokogit/workflows/npm-build-test.yml +83 -0
  31. package/.mokogit/workflows/npm-publish.yml +126 -0
  32. package/.mokogit/workflows/npm-sdk-check.yml +110 -0
  33. package/.mokogit/workflows/npm-tool-inventory.yml +80 -0
  34. package/.mokogit/workflows/pr-branch-check.yml +90 -0
  35. package/.mokogit/workflows/pr-check.yml +565 -0
  36. package/.mokogit/workflows/pre-release.yml +413 -0
  37. package/.mokogit/workflows/push-notify.yml +43 -0
  38. package/.mokogit/workflows/rc-revert.yml +72 -0
  39. package/.mokogit/workflows/repo-health.yml +700 -0
  40. package/.mokogit/workflows/repository-cleanup.yml +525 -0
  41. package/.mokogit/workflows/standards-compliance.yml +2507 -0
  42. package/.mokogit/workflows/sync-version-on-merge.yml +130 -0
  43. package/.mokogit/workflows/version-set.yml +131 -0
  44. package/CHANGELOG.md +18 -0
  45. package/CLAUDE.md +52 -0
  46. package/CODE_OF_CONDUCT.md +70 -0
  47. package/CONTRIBUTING.md +161 -0
  48. package/LICENSE +696 -0
  49. package/Makefile +70 -0
  50. package/README.md +83 -0
  51. package/SECURITY.md +34 -0
  52. package/config.example.json +21 -0
  53. package/dist/client.d.ts +22 -0
  54. package/dist/client.js +124 -0
  55. package/dist/config.d.ts +4 -0
  56. package/dist/config.js +53 -0
  57. package/dist/index.d.ts +3 -0
  58. package/dist/index.js +181 -0
  59. package/dist/signing.d.ts +14 -0
  60. package/dist/signing.js +62 -0
  61. package/dist/types.d.ts +44 -0
  62. package/dist/types.js +16 -0
  63. package/docs/API.md +63 -0
  64. package/docs/ARCHITECTURE.md +73 -0
  65. package/docs/INSTALLATION.md +102 -0
  66. package/docs/index.md +12 -0
  67. package/package.json +35 -0
  68. package/src/client.ts +142 -0
  69. package/src/config.ts +63 -0
  70. package/src/index.ts +336 -0
  71. package/src/signing.ts +67 -0
  72. package/src/types.ts +63 -0
  73. package/tsconfig.json +19 -0
@@ -0,0 +1,596 @@
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: MokoGIT.Workflow
7
+ # INGROUP: MokoCLI.Release
8
+ # REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
9
+ # PATH: /.mokogit/workflows/auto-release.yml
10
+ # VERSION: 05.02.00
11
+ # BRIEF: Universal build & release � detects platform from metadata API
12
+ #
13
+ # +=======================================================================+
14
+ # | UNIVERSAL BUILD & RELEASE PIPELINE |
15
+ # +=======================================================================+
16
+ # | |
17
+ # | Reads metadata API (joomla|generic) to branch logic. |
18
+ # | |
19
+ # | Platform-specific: |
20
+ # | joomla: XML manifest, type-prefixed packages |
21
+ # | generic: README-only, no update stream |
22
+ # | |
23
+ # +=======================================================================+
24
+
25
+ name: "Universal: Build & Release"
26
+
27
+ on:
28
+ pull_request:
29
+ types: [opened, synchronize, closed]
30
+ branches:
31
+ - main
32
+ paths-ignore:
33
+ - '.mokogit/workflows/**'
34
+ - '*.md'
35
+ - 'wiki/**'
36
+ - '.editorconfig'
37
+ - '.gitignore'
38
+ - '.gitattributes'
39
+ - '.gitmessage'
40
+ - 'LICENSE'
41
+ # Daily safety-net: catch merges whose pull_request event never created a run
42
+ # (e.g. this workflow file being re-synced from the template concurrently with the
43
+ # merge). Off-round minute to avoid a fleet-wide spike. The safety-net job below
44
+ # only dispatches a release when main actually has unreleased changes.
45
+ schedule:
46
+ - cron: '37 8 * * *'
47
+ workflow_dispatch:
48
+ inputs:
49
+ action:
50
+ description: 'Action to perform'
51
+ required: false
52
+ type: choice
53
+ default: release
54
+ options:
55
+ - release
56
+ - promote-rc
57
+
58
+ env:
59
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
60
+ MOKOGIT_URL: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
61
+ GIT_ORG: ${{ vars.MOKOGIT_ORG || github.repository_owner }}
62
+ GIT_REPO: ${{ vars.MOKOGIT_REPO || github.event.repository.name }}
63
+
64
+ permissions:
65
+ contents: write
66
+ pull-requests: write
67
+
68
+ jobs:
69
+ # ── PR Opened → Rename branch to RC and build RC release ─────────────────────────
70
+ promote-rc:
71
+ name: Promote to RC
72
+ runs-on: release
73
+ # Skip on template repos (Template-*) — they scaffold other repos and do not release.
74
+ # Trigger-loop guard (#404 follow-up): NEVER run on bot/sync branches. A
75
+ # template-sync/provisioning push (e.g. chore/mokoonyx-template-sync-*) that opens or
76
+ # updates a PR to main must not spawn an RC-promote storm. We gate on the PR HEAD ref
77
+ # prefix and on [skip ci]/[skip sync] markers in the PR title. Legitimate release PRs
78
+ # from dev/feature/fix/patch/hotfix branches are unaffected. workflow_dispatch is exempt
79
+ # from the ref guard because a human explicitly chose to run it.
80
+ if: >-
81
+ !startsWith(github.event.repository.name, 'Template-') &&
82
+ (
83
+ (
84
+ (
85
+ (github.event.action == 'opened' && github.event.pull_request.merged != true) ||
86
+ (github.event.action == 'synchronize' && github.event.pull_request.merged != true)
87
+ ) &&
88
+ !startsWith(github.event.pull_request.head.ref, 'chore/') &&
89
+ !startsWith(github.event.pull_request.head.ref, 'sync/') &&
90
+ !startsWith(github.event.pull_request.head.ref, 'bot/') &&
91
+ !startsWith(github.event.pull_request.head.ref, 'mokoonyx-template-sync') &&
92
+ !startsWith(github.event.pull_request.head.ref, 'template-sync') &&
93
+ !contains(github.event.pull_request.title, '[skip ci]') &&
94
+ !contains(github.event.pull_request.title, '[skip sync]')
95
+ ) ||
96
+ (github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
97
+ )
98
+
99
+ steps:
100
+ - name: Checkout repository
101
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
102
+ with:
103
+ token: ${{ secrets.MOKOGIT_TOKEN }}
104
+ fetch-depth: 1
105
+ submodules: recursive
106
+
107
+ - name: Setup MokoCLI tools
108
+ env:
109
+ MOKO_CLONE_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
110
+ MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
111
+ run: |
112
+ if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then
113
+ echo Using pre-installed /opt/mokocli
114
+ echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV
115
+ else
116
+ echo Falling back to fresh clone
117
+ if ! command -v composer > /dev/null 2>&1; then
118
+ sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
119
+ fi
120
+ rm -rf /tmp/mokocli
121
+ CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git
122
+ git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
123
+ cd /tmp/mokocli
124
+ # Authenticate to the MokoGIT private Composer registry (git.mokoconsulting.tech)
125
+ # declared in mokocli composer.json (preferred-install=dist) before installing deps.
126
+ composer config -g http-basic.git.mokoconsulting.tech x-access-token "${MOKO_CLONE_TOKEN}"
127
+ composer install --no-dev --no-interaction --no-progress --optimize-autoloader --quiet
128
+ echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
129
+ fi
130
+
131
+ - name: Rename branch to rc
132
+ run: |
133
+ API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
134
+ AUTH="Authorization: token ${{ github.token }}"
135
+ FROM="${{ github.event.pull_request.head.ref || 'dev' }}"
136
+ PR="${{ github.event.pull_request.number }}"
137
+
138
+ # Resolve the source branch HEAD commit.
139
+ SRC_JSON=$(curl -sf -H "$AUTH" "${API_BASE}/branches/${FROM}") \
140
+ || { echo "::error::Source branch ${FROM} not found"; exit 1; }
141
+ SRC_SHA=$(printf '%s' "$SRC_JSON" | python3 -c "import sys, json; print(json.load(sys.stdin)['commit']['id'])" 2>/dev/null || true)
142
+ [ -n "$SRC_SHA" ] || { echo "::error::Could not resolve HEAD of ${FROM}"; exit 1; }
143
+
144
+ # Point rc at the source commit via git push. Git's git/refs PATCH API
145
+ # returns HTTP 405 on ANY protected branch (force or not, even for a user in
146
+ # the force-push allowlist), so it cannot move a protected rc. git push honors
147
+ # the push + force-push allowlists and creates rc if it is absent.
148
+ PUSH_URL="https://x-access-token:${{ github.token }}@${MOKOGIT_URL#https://}/${GIT_ORG}/${GIT_REPO}.git"
149
+ git config --global user.name "mokogit-actions[bot]"
150
+ git config --global user.email "actions@mokoconsulting.tech"
151
+ git fetch --no-tags "$PUSH_URL" "${FROM}"
152
+ git push --force "$PUSH_URL" "FETCH_HEAD:refs/heads/rc" \
153
+ || { echo "::error::Failed to point rc at ${FROM} (${SRC_SHA}) via git push"; exit 1; }
154
+ echo "rc set to ${FROM} (${SRC_SHA})"
155
+
156
+ # Repoint the PR at rc, then delete the old source branch (non-fatal).
157
+ if [ -n "$PR" ]; then
158
+ curl -s -X PATCH -H "$AUTH" -H "Content-Type: application/json" \
159
+ "${API_BASE}/pulls/${PR}" -d '{"head":"rc"}' >/dev/null || true
160
+ fi
161
+ # Never delete permanent branches (dev/main/rc/...); only ephemeral feature branches.
162
+ case "$FROM" in
163
+ dev|main|master|rc|stable|production|release|develop|staging|beta|alpha)
164
+ echo "Keeping permanent branch ${FROM} (not deleting)" ;;
165
+ *)
166
+ curl -s -X DELETE -H "$AUTH" "${API_BASE}/branches/${FROM}" >/dev/null || true ;;
167
+ esac
168
+ echo "Renamed ${FROM} -> rc"
169
+
170
+ - name: Trigger RC deploy
171
+ run: |
172
+ # Workflow-token pushes do NOT wake downstream workflows; dispatch deploy-rc explicitly.
173
+ curl -sf -X POST -H "Authorization: token ${{ github.token }}" \n -H "Content-Type: application/json" \n "${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}/actions/workflows/deploy-rc.yml/dispatches" \n -d '{"ref":"rc"}' \n && echo "Dispatched deploy-rc on rc" \n || echo "::warning::deploy-rc dispatch failed (no deploy-rc.yml on rc? non-go platform)"
174
+
175
+ - name: Checkout rc and configure git
176
+ run: |
177
+ git fetch origin rc
178
+ git checkout rc
179
+ git config --local user.email "mokogit-actions[bot]@mokoconsulting.tech"
180
+ git config --local user.name "mokogit-actions[bot]"
181
+ git remote set-url origin "https://x-access-token:${{ github.token }}@git.mokoconsulting.tech/${{ github.repository }}.git"
182
+
183
+ - name: Publish RC release
184
+ continue-on-error: true
185
+ run: |
186
+ php ${MOKO_CLI}/release_publish.php \
187
+ --path . --stability rc --bump minor --branch rc \
188
+ --token "${{ github.token }}"
189
+
190
+ - name: Update RC release notes from CHANGELOG.md
191
+ continue-on-error: true
192
+ run: |
193
+ API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
194
+ TOKEN="${{ github.token }}"
195
+
196
+ # Extract [Unreleased] section via the shared mokocli command (#364 centralization).
197
+ NOTES=""
198
+ if [ -f "CHANGELOG.md" ]; then
199
+ NOTES=$(php ${MOKO_CLI}/release_notes.php --path . --version Unreleased 2>/dev/null || true)
200
+ # release_notes.php echoes "Release Unreleased" when the section is empty; normalize
201
+ # to empty so the platform fallback below applies.
202
+ [ "$NOTES" = "Release Unreleased" ] && NOTES=""
203
+ fi
204
+ [ -z "$NOTES" ] && NOTES="Release candidate"
205
+
206
+ # Find the RC release and update its body
207
+ RELEASE_ID=$(curl -sf -H "Authorization: token ${TOKEN}" \
208
+ "${API_BASE}/releases/tags/release-candidate" \
209
+ | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
210
+
211
+ if [ -n "$RELEASE_ID" ]; then
212
+ python3 -c "
213
+ import json, urllib.request
214
+ body = open('/dev/stdin').read()
215
+ payload = json.dumps({'body': body}).encode()
216
+ req = urllib.request.Request(
217
+ '${API_BASE}/releases/${RELEASE_ID}',
218
+ data=payload, method='PATCH',
219
+ headers={
220
+ 'Authorization': 'token ${TOKEN}',
221
+ 'Content-Type': 'application/json'
222
+ })
223
+ urllib.request.urlopen(req)
224
+ " <<< "$NOTES"
225
+ echo "RC release notes updated from CHANGELOG.md"
226
+ fi
227
+
228
+ - name: Summary
229
+ if: always()
230
+ run: |
231
+ echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
232
+ echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY
233
+
234
+ # ── Merged PR → Build & Release (or promote RC to stable) ─────────────────────────
235
+ release:
236
+ name: Build & Release Pipeline
237
+ runs-on: release
238
+ # Skip on template repos (Template-*) — they scaffold other repos and do not release.
239
+ # Trigger-loop guard (#404 follow-up): a merged bot/sync PR (template-sync/provisioning)
240
+ # must not fire a release. We gate the merged-PR path on the PR HEAD ref prefix and on
241
+ # [skip ci]/[skip sync] title markers; workflow_dispatch stays exempt (explicit human run).
242
+ if: >-
243
+ !startsWith(github.event.repository.name, 'Template-') &&
244
+ (
245
+ (
246
+ github.event.pull_request.merged == true &&
247
+ !startsWith(github.event.pull_request.head.ref, 'chore/') &&
248
+ !startsWith(github.event.pull_request.head.ref, 'sync/') &&
249
+ !startsWith(github.event.pull_request.head.ref, 'bot/') &&
250
+ !startsWith(github.event.pull_request.head.ref, 'mokoonyx-template-sync') &&
251
+ !startsWith(github.event.pull_request.head.ref, 'template-sync') &&
252
+ !contains(github.event.pull_request.title, '[skip ci]') &&
253
+ !contains(github.event.pull_request.title, '[skip sync]')
254
+ ) ||
255
+ (github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
256
+ )
257
+
258
+ steps:
259
+ - name: Checkout repository
260
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
261
+ with:
262
+ token: ${{ secrets.MOKOGIT_TOKEN }}
263
+ fetch-depth: 0
264
+ submodules: recursive
265
+
266
+ - name: Configure git for bot pushes
267
+ run: |
268
+ git config --local user.email "mokogit-actions[bot]@mokoconsulting.tech"
269
+ git config --local user.name "mokogit-actions[bot]"
270
+ git remote set-url origin "https://x-access-token:${{ github.token }}@git.mokoconsulting.tech/${{ github.repository }}.git"
271
+
272
+ - name: Check for merge conflict markers
273
+ run: |
274
+ 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)
275
+ if [ -n "$CONFLICTS" ]; then
276
+ echo "::error::Merge conflict markers found — aborting release"
277
+ echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY
278
+ echo '```' >> $GITHUB_STEP_SUMMARY
279
+ echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
280
+ echo '```' >> $GITHUB_STEP_SUMMARY
281
+ exit 1
282
+ fi
283
+ echo "No conflict markers found"
284
+
285
+ - name: Setup MokoCLI tools
286
+ env:
287
+ MOKO_CLONE_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
288
+ MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
289
+ COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
290
+ run: |
291
+ if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then
292
+ echo Using pre-installed /opt/mokocli
293
+ echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV
294
+ else
295
+ echo Falling back to fresh clone
296
+ if ! command -v composer > /dev/null 2>&1; then
297
+ sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
298
+ fi
299
+ rm -rf /tmp/mokocli
300
+ CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git
301
+ git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
302
+ cd /tmp/mokocli
303
+ # Authenticate to the MokoGIT private Composer registry (git.mokoconsulting.tech)
304
+ # declared in mokocli composer.json (preferred-install=dist) before installing deps.
305
+ composer config -g http-basic.git.mokoconsulting.tech x-access-token "${MOKO_CLONE_TOKEN}"
306
+ composer install --no-dev --no-interaction --no-progress --optimize-autoloader --quiet
307
+ echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
308
+ fi
309
+
310
+ - name: "Detect platform"
311
+ id: platform
312
+ run: |
313
+ php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true
314
+ php ${MOKO_CLI}/manifest_read.php --path . --github-output 2>/dev/null || true
315
+
316
+ - name: "Determine version bump level"
317
+ id: bump
318
+ run: |
319
+ # Fix/patch branches: version was already bumped by pre-release, just strip suffix
320
+ # Feature/dev branches: bump minor for the new stable release
321
+ HEAD_REF="${{ github.event.pull_request.head.ref || 'dev' }}"
322
+ case "$HEAD_REF" in
323
+ fix/*|patch/*|hotfix/*|bugfix/*) BUMP="none" ;;
324
+ *) BUMP="minor" ;;
325
+ esac
326
+ echo "level=${BUMP}" >> "$GITHUB_OUTPUT"
327
+ echo "Bump level: ${BUMP} (from branch: ${HEAD_REF})"
328
+
329
+ - name: "Publish stable release"
330
+ run: |
331
+ BUMP_FLAG=""
332
+ if [ "${{ steps.bump.outputs.level }}" != "none" ]; then
333
+ BUMP_FLAG="--bump ${{ steps.bump.outputs.level }}"
334
+ fi
335
+ php ${MOKO_CLI}/release_publish.php \
336
+ --path . --stability stable ${BUMP_FLAG} --branch main \
337
+ --token "${{ github.token }}"
338
+
339
+ - name: "Read published version"
340
+ id: version
341
+ run: |
342
+ VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "")
343
+ VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
344
+ [ -z "$VERSION" ] && VERSION="00.00.00" && echo "skip=true" >> "$GITHUB_OUTPUT"
345
+ echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
346
+ PLATFORM="${{ steps.platform.outputs.platform }}"
347
+ if [[ "$PLATFORM" == joomla* ]]; then
348
+ echo "tag=stable" >> "$GITHUB_OUTPUT"
349
+ echo "release_tag=stable" >> "$GITHUB_OUTPUT"
350
+ else
351
+ echo "tag=v${VERSION}" >> "$GITHUB_OUTPUT"
352
+ echo "release_tag=v${VERSION}" >> "$GITHUB_OUTPUT"
353
+ fi
354
+ echo "branch=main" >> "$GITHUB_OUTPUT"
355
+ echo "Published version: ${VERSION}"
356
+
357
+ - name: "Create semver tag for non-Joomla repos"
358
+ id: semver
359
+ if: |
360
+ steps.version.outputs.skip != 'true' &&
361
+ !startsWith(steps.platform.outputs.platform, 'joomla')
362
+ run: |
363
+ VERSION="${{ steps.version.outputs.version }}"
364
+ API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
365
+ TOKEN="${{ github.token }}"
366
+ SEMVER_TAG="v${VERSION}"
367
+
368
+ echo "Creating semver tag: ${SEMVER_TAG}"
369
+
370
+ # Create the git tag via API
371
+ HTTP_CODE=$(curl -sf -o /dev/null -w "%{http_code}" \
372
+ -X POST -H "Authorization: token ${TOKEN}" \
373
+ -H "Content-Type: application/json" \
374
+ "${API_BASE}/tags" \
375
+ -d "{\"tag_name\":\"${SEMVER_TAG}\",\"target\":\"main\",\"message\":\"Release ${VERSION}\"}" 2>/dev/null || echo "000")
376
+
377
+ if [ "$HTTP_CODE" = "201" ] || [ "$HTTP_CODE" = "200" ]; then
378
+ echo "Created semver tag: ${SEMVER_TAG}"
379
+ elif [ "$HTTP_CODE" = "409" ]; then
380
+ echo "Semver tag ${SEMVER_TAG} already exists (skipped)"
381
+ else
382
+ echo "::warning::Failed to create semver tag ${SEMVER_TAG} (HTTP ${HTTP_CODE})"
383
+ fi
384
+
385
+ echo "semver_tag=${SEMVER_TAG}" >> "$GITHUB_OUTPUT"
386
+
387
+ - name: Update release notes and promote changelog
388
+ run: |
389
+ API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
390
+ TOKEN="${{ github.token }}"
391
+
392
+ # Get the stable release info (version and ID)
393
+ RELEASE_JSON=$(curl -sf -H "Authorization: token ${TOKEN}" \
394
+ "${API_BASE}/releases/tags/stable" 2>/dev/null || echo '{}')
395
+ RELEASE_ID=$(python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" <<< "$RELEASE_JSON" 2>/dev/null || true)
396
+ # Extract version from release name (e.g. "06.17.00" or "v06.17.00")
397
+ VERSION=$(python3 -c "
398
+ import json, sys, re
399
+ r = json.load(sys.stdin)
400
+ name = r.get('name', '')
401
+ m = re.search(r'(\d+\.\d+\.\d+)', name)
402
+ print(m.group(1) if m else '')
403
+ " <<< "$RELEASE_JSON" 2>/dev/null || true)
404
+
405
+ # Extract [Unreleased] section via the shared mokocli command (#364 centralization).
406
+ NOTES=""
407
+ if [ -f "CHANGELOG.md" ]; then
408
+ NOTES=$(php ${MOKO_CLI}/release_notes.php --path . --version Unreleased 2>/dev/null || true)
409
+ # release_notes.php echoes "Release Unreleased" when the section is empty; normalize
410
+ # to empty so the platform fallback below applies.
411
+ [ "$NOTES" = "Release Unreleased" ] && NOTES=""
412
+ fi
413
+ [ -z "$NOTES" ] && NOTES="Stable release"
414
+
415
+ # Update release body via API
416
+ if [ -n "$RELEASE_ID" ]; then
417
+ python3 -c "
418
+ import json, urllib.request
419
+ body = open('/dev/stdin').read()
420
+ payload = json.dumps({'body': body}).encode()
421
+ req = urllib.request.Request(
422
+ '${API_BASE}/releases/${RELEASE_ID}',
423
+ data=payload, method='PATCH',
424
+ headers={
425
+ 'Authorization': 'token ${TOKEN}',
426
+ 'Content-Type': 'application/json'
427
+ })
428
+ urllib.request.urlopen(req)
429
+ " <<< "$NOTES"
430
+ echo "Release notes updated from CHANGELOG.md"
431
+ fi
432
+
433
+ # Promote [Unreleased] → [version] in CHANGELOG.md via the shared mokocli command (#364).
434
+ # changelog_promote.php is idempotent (won't re-promote an existing version), merges
435
+ # duplicate headings, drops blank release sections, and collapses whitespace — verified
436
+ # byte-identical to the former inline promoter across 8 fixtures.
437
+ if [ -n "$VERSION" ] && [ -f "CHANGELOG.md" ]; then
438
+ php ${MOKO_CLI}/changelog_promote.php --path . --version "$VERSION"
439
+ git add CHANGELOG.md
440
+ git commit -m "chore: promote changelog [Unreleased] → [${VERSION}]" || true
441
+ git push origin main || true
442
+ echo "Changelog promoted: [Unreleased] → [${VERSION}]"
443
+ fi
444
+
445
+ # -- STEP 9: Mirror to GitHub (stable only) --------------------------------
446
+ - name: "Step 9: Mirror release to GitHub"
447
+ if: >-
448
+ steps.version.outputs.skip != 'true' &&
449
+ secrets.GH_MIRROR_TOKEN != ''
450
+ continue-on-error: true
451
+ run: |
452
+ VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
453
+ RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
454
+ GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
455
+ API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
456
+ php ${MOKO_CLI}/release_mirror.php \
457
+ --version "$VERSION" --tag "$RELEASE_TAG" \
458
+ --token "${{ github.token }}" --api-base "$API_BASE" \
459
+ --gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
460
+ --branch main 2>&1 || true
461
+ echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY
462
+
463
+ # -- STEP 10: Sync main branch to GitHub mirror ----------------------------
464
+ - name: "Step 10: Push main to GitHub mirror"
465
+ if: >-
466
+ steps.version.outputs.skip != 'true' &&
467
+ secrets.GH_MIRROR_TOKEN != ''
468
+ continue-on-error: true
469
+ run: |
470
+ GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
471
+ GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
472
+ GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
473
+ git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
474
+ git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
475
+ git fetch origin main --depth=1
476
+ git push github origin/main:refs/heads/main --force 2>/dev/null \
477
+ && echo "main branch pushed to GitHub mirror" \
478
+ || echo "WARNING: GitHub mirror push failed"
479
+
480
+ - name: "Step 11: Delete rc branch (dev reset moved to cascade-dev.yml)"
481
+ if: steps.version.outputs.skip != 'true'
482
+ continue-on-error: true
483
+ run: |
484
+ API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
485
+ TOKEN="${{ github.token }}"
486
+
487
+ # Delete rc branch (ephemeral — created by promote-rc)
488
+ curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
489
+ "${API_BASE}/branches/rc" 2>/dev/null \
490
+ && echo "Deleted rc branch" || echo "rc branch not found"
491
+
492
+ # dev is reset from main by the dedicated "Cascade Main -> Dev" workflow
493
+ # (cascade-dev.yml), which runs after this release completes.
494
+ echo "rc cleaned; dev reset handled by cascade-dev.yml" >> $GITHUB_STEP_SUMMARY
495
+
496
+ - name: "Step 12: Create version branch from main"
497
+ if: steps.version.outputs.skip != 'true'
498
+ continue-on-error: true
499
+ run: |
500
+ API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
501
+ TOKEN="${{ github.token }}"
502
+ VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
503
+ BRANCH_NAME="version/${VERSION}"
504
+ MAIN_SHA=$(git rev-parse HEAD)
505
+
506
+ # Delete old version branch if it exists (same version re-release)
507
+ curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}"
508
+
509
+ # Create version/XX.YY.ZZ from main
510
+ curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed"
511
+
512
+ echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY
513
+
514
+
515
+
516
+ # -- Post-release: Reset dev version -----------------------------
517
+ - name: "Post-release: Reset dev version"
518
+ if: steps.version.outputs.skip != 'true'
519
+ continue-on-error: true
520
+ run: |
521
+ API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
522
+ php ${MOKO_CLI}/version_reset_dev.php \
523
+ --token "${{ github.token }}" --api-base "${API_BASE}" \
524
+ --branch dev --path . 2>&1 || true
525
+
526
+ # -- Summary --------------------------------------------------------------
527
+ - name: Pipeline Summary
528
+ if: always()
529
+ run: |
530
+ VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
531
+ PLATFORM="${{ steps.platform.outputs.platform }}"
532
+ if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
533
+ echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
534
+ echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
535
+ elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
536
+ echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
537
+ else
538
+ echo "" >> $GITHUB_STEP_SUMMARY
539
+ echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
540
+ echo "" >> $GITHUB_STEP_SUMMARY
541
+ echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
542
+ echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
543
+ echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY
544
+ echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
545
+ echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
546
+ echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
547
+ echo "| Release | [View](${MOKOGIT_URL}/${GIT_ORG}/${GIT_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
548
+ fi
549
+
550
+ # ── Scheduled safety-net ─────────────────────────────────────────────────────────
551
+ # A merge to main normally fires the `release` job via the pull_request `closed`
552
+ # event. If that event never creates a run (observed when this workflow file is
553
+ # being re-synced from the template in the same window as the merge), the release
554
+ # is silently skipped. This daily job self-heals that: if main's CHANGELOG
555
+ # [Unreleased] section still has content — which the release job empties on a
556
+ # successful promote — it dispatches a normal release. Dependency-free (awk only)
557
+ # so it runs on any runner; never touches the release job's own conditions.
558
+ scheduled-safety-net:
559
+ name: Scheduled release safety-net
560
+ runs-on: ubuntu-latest
561
+ if: github.event_name == 'schedule' && !startsWith(github.event.repository.name, 'Template-')
562
+ permissions:
563
+ contents: read
564
+ steps:
565
+ - name: Checkout main
566
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
567
+ with:
568
+ ref: main
569
+ fetch-depth: 1
570
+
571
+ - name: Dispatch release if main has unreleased changes
572
+ env:
573
+ MOKOGIT_TOKEN: ${{ github.token }}
574
+ run: |
575
+ # [Unreleased] is non-empty precisely when changes were merged but not yet
576
+ # released (the release job promotes it to a version and empties it).
577
+ NOTES=""
578
+ if [ -f CHANGELOG.md ]; then
579
+ NOTES=$(awk '/^## \[Unreleased\]/{f=1;next} /^## \[/{if(f)exit} f{print}' CHANGELOG.md)
580
+ fi
581
+ if [ -z "$(printf '%s' "$NOTES" | tr -d '[:space:]')" ]; then
582
+ echo "No unreleased changes on main — safety-net has nothing to do."
583
+ exit 0
584
+ fi
585
+ echo "Unreleased changes detected on main — a merge release was likely missed."
586
+ echo "Dispatching a release via the safety-net."
587
+ HTTP=$(curl -s -o /dev/null -w '%{http_code}' -X POST \
588
+ -H "Authorization: token ${MOKOGIT_TOKEN}" \
589
+ -H "Content-Type: application/json" \
590
+ -d '{"ref":"main","inputs":{"action":"release"}}' \
591
+ "${MOKOGIT_URL}/api/v1/repos/${{ github.repository }}/actions/workflows/auto-release.yml/dispatches")
592
+ if [ "$HTTP" = "204" ] || [ "$HTTP" = "201" ] || [ "$HTTP" = "200" ]; then
593
+ echo "Release dispatched (HTTP $HTTP)."
594
+ else
595
+ echo "::warning::Safety-net release dispatch failed (HTTP $HTTP)"
596
+ fi
@@ -0,0 +1,60 @@
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: MokoGIT.Workflow
7
+ # INGROUP: MokoCLI.Universal
8
+ # REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
9
+ # PATH: /.mokogit/workflows/branch-cleanup.yml
10
+ # VERSION: 01.00.00
11
+ # BRIEF: Delete feature branches after PR merge
12
+
13
+ name: "Branch Cleanup"
14
+
15
+ on:
16
+ pull_request:
17
+ types: [closed]
18
+
19
+ env:
20
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
21
+
22
+ jobs:
23
+ cleanup:
24
+ name: Delete merged branch
25
+ runs-on: ubuntu-latest
26
+ if: >-
27
+ github.event.pull_request.merged == true &&
28
+ github.event.pull_request.head.ref != 'dev' &&
29
+ github.event.pull_request.head.ref != 'main'
30
+
31
+ steps:
32
+ - name: Delete source branch
33
+ # SECURITY: the PR head ref is attacker-controllable. Pass it (and the
34
+ # repo/token) through env so the Actions engine cannot splice it into the
35
+ # shell source, and validate it to a safe charset before use.
36
+ env:
37
+ MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
38
+ REPO: ${{ github.repository }}
39
+ API_BASE: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
40
+ BRANCH: ${{ github.event.pull_request.head.ref }}
41
+ run: |
42
+ set -euo pipefail
43
+ if ! printf '%s' "${BRANCH}" | grep -Eq '^[A-Za-z0-9._/-]+$'; then
44
+ echo "::error::unsafe branch name; refusing to proceed"; exit 1
45
+ fi
46
+ API="${API_BASE}/api/v1/repos/${REPO}/branches"
47
+ # URL-encode the branch name's slashes (no PHP dependency on the runner)
48
+ ENCODED=$(printf '%s' "${BRANCH}" | sed 's|/|%2F|g')
49
+
50
+ STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \
51
+ -H "Authorization: token ${MOKOGIT_TOKEN}" \
52
+ "${API}/${ENCODED}" 2>/dev/null || true)
53
+
54
+ if [ "$STATUS" = "204" ]; then
55
+ echo "Deleted branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
56
+ elif [ "$STATUS" = "404" ]; then
57
+ echo "Branch already deleted: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
58
+ else
59
+ echo "::warning::Failed to delete branch ${BRANCH} (HTTP ${STATUS})"
60
+ fi