@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,413 @@
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/pre-release.yml
10
+ # VERSION: 06.00.00
11
+ # BRIEF: Batched pre-release on nightly schedule + manual dispatch (rolling dev/rc asset, build-metadata version — #404)
12
+
13
+ name: "Universal: Pre-Release"
14
+
15
+ # #404 — moved OFF every-push to a nightly schedule + manual dispatch. Building a
16
+ # dev/rc pre-release on every push (and committing a version bump back) caused
17
+ # manifest <version> merge conflicts and wasted CI builds + void versioned dev
18
+ # assets. Batching to nightly removes most of that load; the version now advances
19
+ # as BUILD METADATA (version_build_metadata.php) instead of a committed bump, and
20
+ # the asset is published under a FIXED rolling name replaced in place.
21
+ on:
22
+ schedule:
23
+ # Nightly at 07:00 UTC — batches the day's dev merges into one dev build.
24
+ - cron: '0 7 * * *'
25
+ workflow_dispatch:
26
+ inputs:
27
+ stability:
28
+ description: 'Pre-release channel'
29
+ required: true
30
+ default: development
31
+ type: choice
32
+ options:
33
+ - development
34
+ - alpha
35
+ - beta
36
+ - release-candidate
37
+ ref:
38
+ description: 'Branch to build (default: dev)'
39
+ required: false
40
+ default: dev
41
+ type: string
42
+
43
+ permissions:
44
+ contents: write
45
+
46
+ env:
47
+ GIT_URL: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
48
+ GIT_ORG: ${{ vars.MOKOGIT_ORG || github.repository_owner }}
49
+ GIT_REPO: ${{ vars.MOKOGIT_REPO || github.event.repository.name }}
50
+
51
+ jobs:
52
+ build:
53
+ name: "Build Pre-Release (${{ inputs.stability || 'development' }})"
54
+ runs-on: release
55
+ # Skip on template repos (Template-*) — they scaffold other repos and do not release.
56
+ # Trigger-loop guard (#404 follow-up): this workflow already runs ONLY on `schedule` +
57
+ # `workflow_dispatch` (the every-push trigger that caused the ~150x pre-release storm on
58
+ # chore/mokoonyx-template-sync-* branches was removed in #404) — so a template-sync push
59
+ # can no longer reach it. As defence-in-depth we also refuse a manual dispatch aimed at a
60
+ # bot/sync ref, so provisioning tooling cannot fan out dispatches against those branches.
61
+ if: >-
62
+ !startsWith(github.event.repository.name, 'Template-') &&
63
+ (
64
+ github.event_name == 'schedule' ||
65
+ (
66
+ github.event_name == 'workflow_dispatch' &&
67
+ !startsWith(inputs.ref, 'chore/') &&
68
+ !startsWith(inputs.ref, 'sync/') &&
69
+ !startsWith(inputs.ref, 'bot/') &&
70
+ !startsWith(inputs.ref, 'mokoonyx-template-sync') &&
71
+ !startsWith(inputs.ref, 'template-sync')
72
+ )
73
+ )
74
+
75
+ steps:
76
+ # #404 — no push event any more. The build ref is the dispatch input (default
77
+ # dev) or, for the nightly schedule, dev. On schedule github.ref_name is the
78
+ # default branch (main), so we resolve the pre-release branch explicitly here.
79
+ - name: Resolve build ref
80
+ id: ref
81
+ run: echo "ref=${{ inputs.ref || 'dev' }}" >> "$GITHUB_OUTPUT"
82
+
83
+ - name: Checkout
84
+ uses: actions/checkout@v4
85
+ with:
86
+ fetch-depth: 0
87
+ token: ${{ secrets.MOKOGIT_TOKEN }}
88
+ ref: ${{ steps.ref.outputs.ref }}
89
+ submodules: recursive
90
+
91
+ - name: Update submodules to main
92
+ run: |
93
+ git submodule foreach --quiet 'git checkout main && git pull --quiet origin main' 2>/dev/null || true
94
+
95
+ - name: Setup MokoCLI tools
96
+ env:
97
+ MOKO_CLONE_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
98
+ MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
99
+ run: |
100
+ # Use pre-installed /opt/mokocli if available (updated by cron every 6h)
101
+ if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/cli/manifest_element.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then
102
+ echo Using pre-installed /opt/mokocli
103
+ echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV
104
+ else
105
+ echo Falling back to fresh clone
106
+ if ! command -v composer > /dev/null 2>&1; then
107
+ 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
108
+ fi
109
+ rm -rf /tmp/mokocli
110
+ CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git
111
+ git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
112
+ cd /tmp/mokocli
113
+ # Authenticate to the MokoGIT private Composer registry (git.mokoconsulting.tech)
114
+ # declared in mokocli composer.json (preferred-install=dist) before installing deps.
115
+ composer config -g http-basic.git.mokoconsulting.tech x-access-token "${MOKO_CLONE_TOKEN}"
116
+ composer install --no-dev --no-interaction --no-progress --optimize-autoloader --quiet
117
+ echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
118
+ fi
119
+
120
+ - name: Detect platform
121
+ id: platform
122
+ run: |
123
+ # Authoritative source of truth (#122): the repo's org-customizable
124
+ # platform lives in the MokoGIT metadata API. Read it FIRST and, when
125
+ # present, emit it as the single `platform` output. This avoids the
126
+ # previous clobber where platform_detect.php + manifest_read.php both
127
+ # appended `platform=` under this same id (last-write-wins), and where
128
+ # manifest_read.php hit the removed /manifest route (404) and fell back
129
+ # to generic autodetect — misclassifying an mcp repo as nodejs and
130
+ # skipping the whole release.
131
+ PLATFORM=$(curl -sf \
132
+ -H "Authorization: token ${{ secrets.MOKOGIT_TOKEN }}" \
133
+ "${GIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}/metadata" \
134
+ | python3 -c "import json,sys; print((json.load(sys.stdin).get('platform') or '').strip())" 2>/dev/null || true)
135
+
136
+ if [ -n "$PLATFORM" ]; then
137
+ # Authoritative metadata wins — do NOT run the file-based detectors,
138
+ # so nothing can overwrite this value in $GITHUB_OUTPUT.
139
+ echo "platform=${PLATFORM}" >> "$GITHUB_OUTPUT"
140
+ echo "::notice::Platform '${PLATFORM}' from authoritative metadata API"
141
+ else
142
+ # Fallback (metadata empty/unreachable): retain the existing
143
+ # detection so repos without metadata still resolve a platform.
144
+ echo "::notice::No metadata.platform — falling back to file-based detection"
145
+ php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true
146
+ php ${MOKO_CLI}/manifest_read.php --path . --github-output
147
+ fi
148
+
149
+ - name: Check platform eligibility
150
+ id: eligibility
151
+ run: |
152
+ # Shared release engine (#122): the pre-release path runs for every
153
+ # artifact platform that produces releases — joomla, npm, mcp, and client. Deploy-only platforms (go) are
154
+ # EXCLUDED: they consume a tag but never bump/release here.
155
+ #
156
+ # is_joomla_family selects the Joomla/PHP-specific steps (manifest
157
+ # element/zip resolution, release_package.php zip, updates.xml). Those
158
+ # steps stay TRUE for the Joomla family so Joomla behavior is unchanged,
159
+ # and are skipped for npm/mcp/client, which ride only the platform-
160
+ # agnostic steps (version, tag, changelog, release-notes, release,
161
+ # cascade). Build/publish stay in the platform shim (ADR #124).
162
+ PLATFORM="${{ steps.platform.outputs.platform }}"
163
+ IS_JOOMLA_FAMILY=false
164
+ # STEP 1 — Authoritative baseline (unchanged): the hardcoded case gate
165
+ # remains the source of truth for `proceed` and the default value of
166
+ # is_joomla_family. Everything below (the registry lookup) is PURELY
167
+ # ADDITIVE and can only *refine* is_joomla_family for a cleanly-matched
168
+ # platform — it never flips `proceed`, never downgrades, and any
169
+ # failure/absence leaves this decision exactly as it is today.
170
+ case "$PLATFORM" in
171
+ joomla*)
172
+ PROCEED=true
173
+ IS_JOOMLA_FAMILY=true
174
+ ;;
175
+ npm*|mcp*|client*)
176
+ PROCEED=true
177
+ ;;
178
+ *)
179
+ PROCEED=false
180
+ echo "::notice::Platform '$PLATFORM' — not an artifact release platform, skipping pre-release auto-bump"
181
+ ;;
182
+ esac
183
+
184
+ # STEP 2 — Registry enhancement (#125): classify the Joomla family from
185
+ # the live platform registry (GET /api/v1/platforms) so newly-registered
186
+ # manifest/packaging platforms get correct family classification without
187
+ # editing this workflow. We key on the registry `family` field: family
188
+ # `joomla` is exactly the Joomla-style manifest/packaging
189
+ # path (the manifest_element + release_package steps). We deliberately do
190
+ # NOT key on `manifest_based`, because npm and mcp are also
191
+ # manifest_based:true yet must ride the platform-agnostic path (family
192
+ # `node`) — so `manifest_based` is ambiguous here and `family` is the
193
+ # clean discriminator.
194
+ #
195
+ # Fail-safe contract: the curl is captured into a var (never allowed to
196
+ # crash the step under set -e -o pipefail), the JSON is parsed with a
197
+ # guarded python3 one-liner, and the result is only applied when it is
198
+ # exactly "true"/"false". On ANY of {non-200, curl error, empty/malformed
199
+ # JSON, parse failure, platform key absent} REGISTRY_FAMILY stays empty
200
+ # and we keep the STEP 1 hardcoded value untouched.
201
+ REGISTRY_JSON="$(curl -sf \
202
+ -H "Authorization: token ${{ secrets.MOKOGIT_TOKEN }}" \
203
+ "${GIT_URL}/api/v1/platforms" 2>/dev/null || true)"
204
+ REGISTRY_FAMILY=""
205
+ if [ -n "$REGISTRY_JSON" ]; then
206
+ # Single physical line of python3 so we stay safely inside this YAML
207
+ # block scalar (a column-0 continuation would terminate the scalar).
208
+ # Emits exactly: "true" -> matched key, family joomla;
209
+ # "false" -> matched key, some other family;
210
+ # "" -> key absent (fall back to hardcoded).
211
+ # On malformed/empty JSON the parse raises, 2>/dev/null hides the
212
+ # traceback and the trailing "|| true" keeps set -e -o pipefail from
213
+ # crashing the step, so REGISTRY_FAMILY stays "" and we fall back.
214
+ REGISTRY_FAMILY="$(printf '%s' "$REGISTRY_JSON" | PLATFORM="$PLATFORM" python3 -c 'import json,os,sys; p=os.environ.get("PLATFORM",""); f=next((x.get("family","") for x in json.load(sys.stdin) if x.get("key")==p), None); sys.stdout.write("true" if f == "joomla" else ("false" if f is not None else ""))' 2>/dev/null || true)"
215
+ fi
216
+ if [ "$REGISTRY_FAMILY" = "true" ]; then
217
+ IS_JOOMLA_FAMILY=true
218
+ echo "::notice::Registry classified '$PLATFORM' as Joomla family (via /api/v1/platforms)"
219
+ elif [ "$REGISTRY_FAMILY" = "false" ]; then
220
+ IS_JOOMLA_FAMILY=false
221
+ echo "::notice::Registry classified '$PLATFORM' as non-Joomla family (via /api/v1/platforms)"
222
+ else
223
+ echo "::notice::Registry lookup unavailable/absent for '$PLATFORM' — using hardcoded family classification"
224
+ fi
225
+
226
+ echo "proceed=${PROCEED}" >> "$GITHUB_OUTPUT"
227
+ echo "is_joomla_family=${IS_JOOMLA_FAMILY}" >> "$GITHUB_OUTPUT"
228
+ if [ "$PROCEED" = "true" ]; then
229
+ echo "::notice::Platform '$PLATFORM' eligible for pre-release (joomla_family=${IS_JOOMLA_FAMILY})"
230
+ fi
231
+
232
+ - name: Resolve metadata and bump version
233
+ id: meta
234
+ if: steps.eligibility.outputs.proceed == 'true'
235
+ run: |
236
+ # #404 — stability comes from the dispatch input; the nightly schedule
237
+ # always builds the development channel. There is no push event.
238
+ if [ "${{ github.event_name }}" = "schedule" ]; then
239
+ STABILITY="development"
240
+ else
241
+ STABILITY="${{ inputs.stability || 'development' }}"
242
+ fi
243
+
244
+ case "$STABILITY" in
245
+ development) SUFFIX="-dev"; TAG="development" ;;
246
+ alpha) SUFFIX="-alpha"; TAG="alpha" ;;
247
+ beta) SUFFIX="-beta"; TAG="beta" ;;
248
+ release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
249
+ esac
250
+
251
+ # #404 — VERSION AS BUILD METADATA, NOT A COMMITTED BUMP.
252
+ # version_build_metadata.php derives a monotonic version from the CI run
253
+ # number and stamps it into the in-tree manifest ONLY (no git add/commit/
254
+ # push). This keeps <version> advancing so Joomla's updater still detects
255
+ # new dev/rc builds, while removing the pushed bump commit that caused
256
+ # manifest <version> merge conflicts on every branch->dev merge.
257
+ php ${MOKO_CLI}/version_build_metadata.php \
258
+ --path . --stability "$STABILITY" --github-output 2>/dev/null || true
259
+
260
+ # Read back the derived base (suffix stripped) and cascade into the
261
+ # platform-specific manifest files — still IN-TREE ONLY, never committed.
262
+ VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "00.00.01")
263
+ VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
264
+
265
+ php ${MOKO_CLI}/version_set_platform.php \
266
+ --path . --version "$VERSION" --branch "${{ steps.ref.outputs.ref }}" --stability "$STABILITY" 2>/dev/null || true
267
+ php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
268
+
269
+ # Ensure licensing tags (updateservers, dlid) if enabled in manifest.xml
270
+ php ${MOKO_CLI}/manifest_licensing.php --path . --fix 2>/dev/null || true
271
+
272
+ # Append suffix for output
273
+ if [ -n "$SUFFIX" ]; then
274
+ VERSION="${VERSION}${SUFFIX}"
275
+ fi
276
+
277
+ # NOTE: no commit/push here (#404). The derived version lives only in the
278
+ # built artifact + the dynamic updates feed; the branch tip is untouched.
279
+
280
+ # Auto-detect element via manifest_element.php (Joomla-family only:
281
+ # element/manifest resolution is Joomla-specific). For npm/mcp/client
282
+ # the generic fallback below derives ext_element/zip_name from the repo
283
+ # name — no Joomla manifest is present or required.
284
+ if [ "${{ steps.eligibility.outputs.is_joomla_family }}" = "true" ]; then
285
+ php ${MOKO_CLI}/manifest_element.php \
286
+ --path . --version "$VERSION" --stability "$STABILITY" \
287
+ --repo "${GIT_REPO}" --github-output
288
+ fi
289
+
290
+ # Read back element outputs
291
+ EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2 || true)
292
+ ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2 || true)
293
+ [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GIT_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
294
+ [ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip"
295
+
296
+ echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
297
+ echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
298
+ echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
299
+ echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
300
+ echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
301
+ echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
302
+
303
+ echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
304
+
305
+ - name: Create release
306
+ id: release
307
+ if: steps.eligibility.outputs.proceed == 'true'
308
+ run: |
309
+ TAG="${{ steps.meta.outputs.tag }}"
310
+ VERSION="${{ steps.meta.outputs.version }}"
311
+ API_BASE="${GIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
312
+ php ${MOKO_CLI}/release_create.php \
313
+ --path . --version "$VERSION" --tag "$TAG" \
314
+ --token "${{ secrets.MOKOGIT_TOKEN }}" --api-base "$API_BASE" \
315
+ --repo "${GIT_REPO}" --branch "${{ steps.ref.outputs.ref }}" --prerelease
316
+
317
+ - name: Update release notes from CHANGELOG.md
318
+ if: steps.eligibility.outputs.proceed == 'true'
319
+ run: |
320
+ TAG="${{ steps.meta.outputs.tag }}"
321
+ VERSION="${{ steps.meta.outputs.version }}"
322
+ API_BASE="${GIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
323
+
324
+ # Extract [Unreleased] section via the shared mokocli command (#364 centralization).
325
+ if [ -f "CHANGELOG.md" ]; then
326
+ NOTES=$(php ${MOKO_CLI}/release_notes.php --path . --version Unreleased 2>/dev/null || true)
327
+ # release_notes.php echoes "Release Unreleased" when the section is empty; normalize.
328
+ [ "$NOTES" = "Release Unreleased" ] && NOTES=""
329
+ [ -z "$NOTES" ] && NOTES="Release ${VERSION}"
330
+ else
331
+ NOTES="Release ${VERSION}"
332
+ fi
333
+
334
+ # Update release body via API
335
+ RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGIT_TOKEN }}" \
336
+ "${API_BASE}/releases/tags/${TAG}" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
337
+
338
+ if [ -n "$RELEASE_ID" ]; then
339
+ python3 -c "
340
+ import json, urllib.request
341
+ body = open('/dev/stdin').read()
342
+ payload = json.dumps({'body': body}).encode()
343
+ req = urllib.request.Request(
344
+ '${API_BASE}/releases/${RELEASE_ID}',
345
+ data=payload, method='PATCH',
346
+ headers={
347
+ 'Authorization': 'token ${{ secrets.MOKOGIT_TOKEN }}',
348
+ 'Content-Type': 'application/json'
349
+ })
350
+ urllib.request.urlopen(req)
351
+ " <<< "$NOTES"
352
+ echo "Release notes updated from CHANGELOG.md"
353
+ fi
354
+
355
+ # Joomla-family only: mokocli release_package.php builds the extension zip
356
+ # (+ sha256, attach). npm/mcp/client package/publish in their platform shim
357
+ # (trust boundary, ADR #124), so this PHP zip step is skipped for them.
358
+ - name: Build package and upload
359
+ id: package
360
+ if: >-
361
+ steps.eligibility.outputs.proceed == 'true' &&
362
+ steps.eligibility.outputs.is_joomla_family == 'true'
363
+ run: |
364
+ VERSION="${{ steps.meta.outputs.version }}"
365
+ TAG="${{ steps.meta.outputs.tag }}"
366
+ STABILITY="${{ steps.meta.outputs.stability }}"
367
+ API_BASE="${GIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
368
+ # #404 — publish the pre-release asset under a FIXED rolling name
369
+ # (pkg_<ext>-dev.zip / -rc.zip) replaced in place, instead of a new
370
+ # versioned file each build. --stability drives the channel slug; the
371
+ # in-package manifest <version> still advances (build metadata) so the
372
+ # updater keeps detecting new builds.
373
+ php ${MOKO_CLI}/release_package.php \
374
+ --path . --version "$VERSION" --tag "$TAG" \
375
+ --token "${{ secrets.MOKOGIT_TOKEN }}" --api-base "$API_BASE" \
376
+ --repo "${GIT_REPO}" --output /tmp \
377
+ --stability "$STABILITY" --rolling || true
378
+
379
+ # updates.xml is generated dynamically by MokoGIT license server.
380
+ # #404 — the dynamic feed must point <downloadurl> at the FIXED rolling asset
381
+ # name for dev/rc (pkg_<ext>-dev.zip), matching what release_package.php now
382
+ # publishes. mokocli's updates_xml_build.php grew a --rolling flag for this;
383
+ # the server-side generator (MokoGIT license server) needs the equivalent
384
+ # change so the served updates.xml references the fixed filename, not the old
385
+ # versioned one. See #404 report — flagged for human/server-side follow-up.
386
+
387
+ - name: "Delete lesser pre-release channels (cascade)"
388
+ if: steps.eligibility.outputs.proceed == 'true'
389
+ continue-on-error: true
390
+ run: |
391
+ API_BASE="${GIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
392
+ TOKEN="${{ secrets.MOKOGIT_TOKEN }}"
393
+
394
+ php ${MOKO_CLI}/release_cascade.php \
395
+ --stability "${{ steps.meta.outputs.stability }}" \
396
+ --token "${TOKEN}" \
397
+ --api-base "${API_BASE}"
398
+
399
+ - name: Summary
400
+ if: always()
401
+ run: |
402
+ VERSION="${{ steps.meta.outputs.version }}"
403
+ STABILITY="${{ steps.meta.outputs.stability }}"
404
+ ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
405
+ SHA256="${{ steps.package.outputs.sha256_zip }}"
406
+ echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY
407
+ echo "" >> $GITHUB_STEP_SUMMARY
408
+ echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
409
+ echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
410
+ echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
411
+ echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY
412
+ echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY
413
+ echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY
@@ -0,0 +1,43 @@
1
+ # +========================================================================+
2
+ # | Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> |
3
+ # | SPDX-License-Identifier: GPL-3.0-or-later |
4
+ # | |
5
+ # | BRIEF: Clean, formatted ntfy notification on push to a default branch. |
6
+ # | Issues use an org-level webhook; releases use notify.yml. |
7
+ # +========================================================================+
8
+ name: "Universal: Push Notifications"
9
+
10
+ on:
11
+ push:
12
+ branches: [main, master]
13
+
14
+ permissions:
15
+ contents: read
16
+
17
+ env:
18
+ NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
19
+ NTFY_TOKEN: ${{ secrets.NTFY_TOKEN }}
20
+
21
+ jobs:
22
+ push-notify:
23
+ runs-on: ubuntu-latest
24
+ steps:
25
+ - name: Notify push
26
+ # Untrusted values (commit message, actor) passed via env — never
27
+ # interpolated directly into the shell — to avoid command injection.
28
+ env:
29
+ REPO: ${{ github.repository }}
30
+ BRANCH: ${{ github.ref_name }}
31
+ ACTOR: ${{ github.actor }}
32
+ MSG: ${{ github.event.head_commit.message }}
33
+ CLICK: ${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}
34
+ run: |
35
+ SUMMARY=$(printf '%s\n' "$MSG" | head -n1)
36
+ curl -sS --retry 3 --retry-connrefused --retry-delay 2 --max-time 20 \
37
+ -H "Title: ${REPO}: push to ${BRANCH}" \
38
+ -H "Tags: package" \
39
+ -H "Priority: default" \
40
+ -H "Click: ${CLICK}" \
41
+ -H "Authorization: Bearer ${NTFY_TOKEN}" \
42
+ -d "${ACTOR}: ${SUMMARY}" \
43
+ "${NTFY_URL}/mokogit-push" || echo "::warning::ntfy notification could not be delivered (non-fatal)"
@@ -0,0 +1,72 @@
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/rc-revert.yml
10
+ # VERSION: 09.23.00
11
+ # BRIEF: Rename rc/ branch back to dev/ when PR is closed without merge
12
+
13
+ name: "RC Revert"
14
+
15
+ on:
16
+ pull_request:
17
+ types: [closed]
18
+
19
+ env:
20
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
21
+
22
+ jobs:
23
+ revert:
24
+ name: Rename rc/ back to dev/
25
+ runs-on: ubuntu-latest
26
+ if: >-
27
+ github.event.pull_request.merged == false &&
28
+ startsWith(github.event.pull_request.head.ref, 'rc/') &&
29
+ !startsWith(github.event.repository.name, 'Template-')
30
+
31
+ steps:
32
+ - name: Rename branch
33
+ env:
34
+ BRANCH: ${{ github.event.pull_request.head.ref }}
35
+ REPO: ${{ github.repository }}
36
+ GIT_URL: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
37
+ TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
38
+ run: |
39
+ set -euo pipefail
40
+ # BRANCH is attacker-controlled (PR head ref). Strict allowlist before ANY use.
41
+ if ! printf '%s' "$BRANCH" | grep -Eq '^rc/[A-Za-z0-9._/-]+$'; then
42
+ echo "::error::Refusing unsafe branch name: $BRANCH"; exit 1
43
+ fi
44
+ SUFFIX="${BRANCH#rc/}"
45
+ DEV_BRANCH="dev/${SUFFIX}"
46
+ API="${GIT_URL}/api/v1/repos/${REPO}/branches"
47
+
48
+ # Create dev/ branch from rc/ branch
49
+ STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X POST \
50
+ -H "Authorization: token ${TOKEN}" \
51
+ -H "Content-Type: application/json" \
52
+ -d "{\"new_branch_name\": \"${DEV_BRANCH}\", \"old_branch_name\": \"${BRANCH}\"}" \
53
+ "${API}" 2>/dev/null || true)
54
+ if [ "$STATUS" = "201" ]; then
55
+ echo "Created branch: ${DEV_BRANCH}" >> "$GITHUB_STEP_SUMMARY"
56
+ else
57
+ echo "::error::Failed to create ${DEV_BRANCH} from ${BRANCH} (HTTP ${STATUS})"; exit 1
58
+ fi
59
+
60
+ # Read BRANCH from the environment inside PHP (getenv, no string interpolation -> no PHP injection)
61
+ ENCODED=$(php -r 'echo rawurlencode(getenv("BRANCH"));')
62
+ STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \
63
+ -H "Authorization: token ${TOKEN}" \
64
+ "${API}/${ENCODED}" 2>/dev/null || true)
65
+ if [ "$STATUS" = "204" ]; then
66
+ echo "Deleted branch: ${BRANCH}" >> "$GITHUB_STEP_SUMMARY"
67
+ else
68
+ echo "::warning::Failed to delete ${BRANCH} (HTTP ${STATUS})"
69
+ fi
70
+
71
+ echo "### RC Reverted" >> "$GITHUB_STEP_SUMMARY"
72
+ echo "${BRANCH} → ${DEV_BRANCH}" >> "$GITHUB_STEP_SUMMARY"