@mokoconsulting/mcp-mokosuite 1.1.0-dev.372 → 1.1.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.
@@ -7,8 +7,8 @@
7
7
  # INGROUP: MokoCLI.Release
8
8
  # REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
9
9
  # PATH: /.mokogit/workflows/pre-release.yml
10
- # VERSION: 06.01.00
11
- # BRIEF: Batched pre-release on nightly schedule + manual dispatch (rolling dev/rc asset, build-metadata version — #404); non-Joomla MokoGIT release + cosmetic release-notes are best-effort. npm publishing moved OUT of this universal workflow into Template-NPM's dedicated npm-publish.yml so it can't leak into non-npm repos via sync.
10
+ # VERSION: 06.03.00
11
+ # BRIEF: Batched pre-release on nightly schedule + manual dispatch (rolling dev/rc asset, build-metadata version — #404); non-Joomla MokoGIT release + cosmetic release-notes are best-effort. npm publishing moved OUT of this universal workflow into Template-NPM's dedicated npm-publish.yml so it can't leak into non-npm repos via sync. The dev push trigger now skips [skip ci]/[skip bump] commits (bump-on-dev: release_bump_dev.php's [skip ci] base-bump commit on dev must not spawn a redundant dev-channel build).
12
12
 
13
13
  name: "Universal: Pre-Release"
14
14
 
@@ -93,7 +93,11 @@ jobs:
93
93
  if: >-
94
94
  !startsWith(github.event.repository.name, 'Template-') &&
95
95
  (
96
- github.event_name == 'push' ||
96
+ (
97
+ github.event_name == 'push' &&
98
+ !contains(github.event.head_commit.message, '[skip ci]') &&
99
+ !contains(github.event.head_commit.message, '[skip bump]')
100
+ ) ||
97
101
  github.event_name == 'schedule' ||
98
102
  (
99
103
  github.event_name == 'workflow_dispatch' &&
@@ -305,6 +309,19 @@ jobs:
305
309
  echo "::notice::Platform '$PLATFORM' eligible for pre-release (joomla_family=${IS_JOOMLA_FAMILY})"
306
310
  fi
307
311
 
312
+ - name: "Require Makefile (pre-release gate)"
313
+ if: steps.eligibility.outputs.proceed == 'true'
314
+ # Enforce the same Makefile requirement as auto-release, but only when the
315
+ # platform is eligible to build/release (pre-release legitimately no-ops for
316
+ # ineligible platforms). Post-checkout step: a job-level if: cannot see the workspace.
317
+ run: |
318
+ if [ ! -f Makefile ] && [ ! -f makefile ] && [ ! -f GNUmakefile ]; then
319
+ echo "::error::pre-release requires a Makefile at the repository root — none found. A release-bearing repo must ship a Makefile (build/package targets). Aborting."
320
+ echo "## Pre-release blocked: no Makefile at repo root" >> "$GITHUB_STEP_SUMMARY"
321
+ exit 1
322
+ fi
323
+ echo "Makefile present — pre-release may proceed."
324
+
308
325
  - name: Resolve metadata and bump version
309
326
  id: meta
310
327
  if: steps.eligibility.outputs.proceed == 'true'
@@ -16,6 +16,9 @@ on:
16
16
  pull_request:
17
17
  types: [closed]
18
18
 
19
+ permissions:
20
+ contents: write
21
+
19
22
  env:
20
23
  FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
21
24
 
@@ -34,7 +37,7 @@ jobs:
34
37
  BRANCH: ${{ github.event.pull_request.head.ref }}
35
38
  REPO: ${{ github.repository }}
36
39
  GIT_URL: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
37
- TOKEN: ${{ secrets.MOKOGIT_TOKEN || github.token }}
40
+ TOKEN: ${{ github.token }}
38
41
  run: |
39
42
  set -euo pipefail
40
43
  # BRANCH is attacker-controlled (PR head ref). Strict allowlist before ANY use.
package/CHANGELOG.md CHANGED
@@ -4,23 +4,6 @@ All notable changes to this project are documented here, following [Keep a Chang
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
- ## [01.01.00] - 2026-08-15
8
-
9
- ### Added
10
- - **Layer 1 (Extensions Store / MokoSuiteExtensions)** — 5 MCP tools against the
11
- `/v1/mokosuiteextensions` base path:
12
- - Reads: `store_catalog_list` (`GET /catalog`), `store_catalog_get`
13
- (`GET /catalog/{id}`).
14
- - Writes: `store_install` (`POST /catalog/{id}/install`), `store_update`
15
- (`POST /catalog/{id}/update`), `store_uninstall`
16
- (`POST /catalog/{id}/uninstall`, `confirm: true` guard — destructive).
17
- - Bumped the MCP server advertised version to `1.1.0`.
18
-
19
- ### Note
20
- - The store write routes (`/install`, `/update`, `/uninstall`) are being added to
21
- the store REST API in parallel and may not be live on a site until that ships and
22
- deploys. The read routes exist. The tools are defined now regardless.
23
-
24
7
  ## [01.00.00] - 2026-07-26
25
8
 
26
9
  ### Added
package/Makefile CHANGED
@@ -1,70 +1,94 @@
1
- # MCP Server Makefile
1
+ # NPM / Node Project Makefile
2
2
  # Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
3
3
  # SPDX-License-Identifier: GPL-3.0-or-later
4
+ #
5
+ # Native build/test/lint/package logic for an npm package (incl. MCP servers). The
6
+ # identity block below is the version source of truth — auto-filled per repo by the
7
+ # mokocli workflow sync (--makefiles).
4
8
 
5
- PROJECT_NAME := mcp-mokosuite
6
- PROJECT_VERSION := 1.0.0
9
+ # ==============================================================================
10
+ # Identity (auto-filled by workflow sync — the Makefile is the version source of truth)
11
+ # ==============================================================================
12
+ PROJECT_NAME := mcp-mokosuite
13
+ PROJECT_VERSION := 1.1.0
14
+ PLATFORM := npm
7
15
 
8
- NPM := npm
16
+ # ==============================================================================
17
+ # Configuration
18
+ # ==============================================================================
19
+ SRC_DIR := src/
20
+ BUILD_DIR := build
21
+ NPM := npm
22
+ NODE := node
23
+ # Prefer `npm ci` when a lockfile is present (reproducible), else `npm install`.
24
+ NPM_INSTALL := $(shell [ -f package-lock.json ] && echo ci || echo install)
9
25
 
10
- COLOR_RESET := \033[0m
11
- COLOR_GREEN := \033[32m
12
- COLOR_BLUE := \033[34m
26
+ COLOR_RESET := \033[0m
27
+ COLOR_GREEN := \033[32m
28
+ COLOR_YELLOW := \033[33m
29
+ COLOR_BLUE := \033[34m
30
+ COLOR_RED := \033[31m
13
31
 
32
+ # ==============================================================================
33
+ # Targets
34
+ # ==============================================================================
14
35
  .PHONY: help
15
36
  help: ## Show this help message
16
- @echo "$(COLOR_BLUE)$(PROJECT_NAME) v$(PROJECT_VERSION)$(COLOR_RESET)"
17
- @echo ""
18
- @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " $(COLOR_BLUE)%-20s$(COLOR_RESET) %s\n", $$1, $$2}'
37
+ @echo "$(COLOR_BLUE)$(PROJECT_NAME) v$(PROJECT_VERSION) — npm package$(COLOR_RESET)"
19
38
  @echo ""
39
+ @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " $(COLOR_BLUE)%-16s$(COLOR_RESET) %s\n", $$1, $$2}'
20
40
 
21
41
  .PHONY: install-deps
22
- install-deps: ## Install dependencies
23
- @$(NPM) install
24
- @echo "$(COLOR_GREEN)✓ Dependencies installed$(COLOR_RESET)"
25
-
26
- MOKO_PLATFORM ?= $(or $(wildcard ../mokocli),$(wildcard $(HOME)/mokocli),$(wildcard /opt/mokocli))
27
- MINIFY_SCRIPT := $(MOKO_PLATFORM)/build/minify.js
42
+ install-deps: ## Install dependencies (npm ci if lockfile present, else install)
43
+ @$(NPM) $(NPM_INSTALL)
44
+ @echo "$(COLOR_GREEN)✓ dependencies installed$(COLOR_RESET)"
28
45
 
29
- .PHONY: minify
30
- minify: ## Minify CSS/JS assets
31
- @echo "Minifying assets..."
32
- @if [ -f "$(MINIFY_SCRIPT)" ]; then \
33
- node "$(MINIFY_SCRIPT)" $(SRC_DIR); \
34
- elif [ -f "scripts/minify.js" ]; then \
35
- node scripts/minify.js; \
36
- else \
37
- echo "No minify script found"; \
38
- fi
46
+ .PHONY: lint
47
+ lint: ## Run the package lint script when defined
48
+ @if grep -q '"lint"' package.json 2>/dev/null; then $(NPM) run lint; \
49
+ else echo "$(COLOR_YELLOW)⚠ no lint script — skipping$(COLOR_RESET)"; fi
39
50
 
40
- .PHONY: build
41
- build: minify ## Build TypeScript
42
- @$(NPM) run build
43
- @echo "$(COLOR_GREEN)✓ Build complete$(COLOR_RESET)"
51
+ .PHONY: validate
52
+ validate: lint ## Alias: static validation (lint)
44
53
 
45
- .PHONY: dev
46
- dev: ## Watch and rebuild on changes
47
- @$(NPM) run dev
54
+ .PHONY: test
55
+ test: ## Run the package test script when defined
56
+ @if grep -q '"test"' package.json 2>/dev/null; then $(NPM) test; \
57
+ else echo "$(COLOR_YELLOW)⚠ no test script — skipping$(COLOR_RESET)"; fi
48
58
 
49
59
  .PHONY: clean
50
- clean: ## Clean build artifacts
51
- @rm -rf dist
52
- @echo "$(COLOR_GREEN)✓ Cleaned$(COLOR_RESET)"
60
+ clean: ## Remove build artifacts
61
+ @rm -rf $(BUILD_DIR)
62
+ @echo "$(COLOR_GREEN)✓ cleaned $(BUILD_DIR)/$(COLOR_RESET)"
53
63
 
54
- .PHONY: setup
55
- setup: ## Run interactive setup wizard
56
- @$(NPM) run setup
64
+ .PHONY: build
65
+ build: ## Run the package build script when defined
66
+ @if grep -q '"build"' package.json 2>/dev/null; then $(NPM) run build; \
67
+ echo "$(COLOR_GREEN)✓ built$(COLOR_RESET)"; \
68
+ else echo "$(COLOR_YELLOW)⚠ no build script — nothing to build$(COLOR_RESET)"; fi
69
+
70
+ .PHONY: minify
71
+ minify: ## Minify assets under $(SRC_DIR) via the mokocli build agent when available
72
+ @MOKO=$(or $(wildcard ../mokocli),$(wildcard $(HOME)/mokocli),$(wildcard /opt/mokocli)); \
73
+ if [ -n "$$MOKO" ] && [ -f "$$MOKO/build/minify.js" ]; then $(NODE) "$$MOKO/build/minify.js" $(SRC_DIR); \
74
+ elif [ -f scripts/minify.js ]; then $(NODE) scripts/minify.js; \
75
+ else echo "$(COLOR_YELLOW)⚠ no minify script found$(COLOR_RESET)"; fi
57
76
 
58
- .PHONY: start
59
- start: ## Start the MCP server
60
- @$(NPM) run start
77
+ .PHONY: package
78
+ package: build ## Create the npm tarball (npm pack) + .sha256
79
+ @echo "$(COLOR_BLUE)Packaging $(PROJECT_NAME)-$(PROJECT_VERSION)...$(COLOR_RESET)"
80
+ @mkdir -p $(BUILD_DIR)
81
+ @$(NPM) pack --pack-destination $(BUILD_DIR) >/dev/null
82
+ @tgz=$$(ls -t $(BUILD_DIR)/*.tgz | head -1); \
83
+ sha256sum "$$tgz" | awk '{print $$1}' > "$$tgz.sha256"; \
84
+ echo "$(COLOR_GREEN)✓ packaged: $$tgz$(COLOR_RESET)"
61
85
 
62
- .PHONY: lint
63
- lint: ## Run linter
64
- @$(NPM) run lint
86
+ .PHONY: release
87
+ release: validate test package ## Validate, test, and package for release
88
+ @echo "$(COLOR_GREEN)✓ release artifacts ready for $(PROJECT_NAME) v$(PROJECT_VERSION)$(COLOR_RESET)"
65
89
 
66
- .PHONY: ci
67
- ci: install-deps build ## Run CI pipeline
68
- @echo "$(COLOR_GREEN)✓ CI pipeline complete$(COLOR_RESET)"
90
+ .PHONY: version
91
+ version: ## Print the resolved version (Makefile is the source of truth)
92
+ @echo "$(PROJECT_NAME) $(PROJECT_VERSION) ($(PLATFORM))"
69
93
 
70
94
  .DEFAULT_GOAL := help
package/dist/index.js CHANGED
@@ -21,8 +21,6 @@ import { ApiClient } from './client.js';
21
21
  import { hasRemoteControl } from './signing.js';
22
22
  /** Base path for MokoSuiteClient Web Services (appended after /api/index.php). */
23
23
  const MSC = '/v1/mokosuiteclient';
24
- /** Base path for MokoSuiteExtensions (store) Web Services (appended after /api/index.php). */
25
- const MSE = '/v1/mokosuiteextensions';
26
24
  let config;
27
25
  function clientFor(connection) {
28
26
  return new ApiClient(getConnection(config, connection));
@@ -53,7 +51,7 @@ function requireConfirm(confirm) {
53
51
  // ── Server ──────────────────────────────────────────────────────────────
54
52
  const server = new McpServer({
55
53
  name: 'mcp-mokosuite',
56
- version: '1.1.0',
54
+ version: '1.0.0',
57
55
  });
58
56
  // ════════════════════════════════════════════════════════════════════════
59
57
  // FOUNDATION — connectivity & generic access
@@ -170,31 +168,6 @@ server.tool('client_users_force_logout', 'Terminate ALL user sessions on a MokoS
170
168
  return guard;
171
169
  return formatResponse(await clientFor(connection).remoteControl('POST', `${MSC}/users/force-logout`));
172
170
  });
173
- // ════════════════════════════════════════════════════════════════════════
174
- // LAYER 1 — Extensions Store (MokoSuiteExtensions)
175
- // ════════════════════════════════════════════════════════════════════════
176
- server.tool('store_catalog_list', 'List the products available in the MokoSuite Extensions store catalog for a site.', { ...ConnectionParam }, async ({ connection }) => formatResponse(await clientFor(connection).get(`${MSE}/catalog`)));
177
- server.tool('store_catalog_get', 'Get a single MokoSuite Extensions store product by its catalog id.', {
178
- id: z.number().describe('Catalog product id'),
179
- ...ConnectionParam,
180
- }, async ({ id, connection }) => formatResponse(await clientFor(connection).get(`${MSE}/catalog/${id}`)));
181
- server.tool('store_install', 'Install a MokoSuite Extensions store product on a site by its catalog id.', {
182
- id: z.number().describe('Catalog product id to install'),
183
- ...ConnectionParam,
184
- }, async ({ id, connection }) => formatResponse(await clientFor(connection).post(`${MSE}/catalog/${id}/install`)));
185
- server.tool('store_update', 'Update an installed MokoSuite Extensions store product on a site by its catalog id.', {
186
- id: z.number().describe('Catalog product id to update'),
187
- ...ConnectionParam,
188
- }, async ({ id, connection }) => formatResponse(await clientFor(connection).post(`${MSE}/catalog/${id}/update`)));
189
- server.tool('store_uninstall', 'Uninstall a MokoSuite Extensions store product from a site by its catalog id. Destructive — requires `confirm: true`.', {
190
- id: z.number().describe('Catalog product id to uninstall'),
191
- confirm: z.boolean().describe('Must be true — uninstalling removes the product from the target site'),
192
- ...ConnectionParam,
193
- }, async ({ id, confirm, connection }) => {
194
- if (!confirm)
195
- return text('Refused: set `confirm: true` to uninstall this store product.');
196
- return formatResponse(await clientFor(connection).post(`${MSE}/catalog/${id}/uninstall`));
197
- });
198
171
  // ── Start Server ────────────────────────────────────────────────────────
199
172
  async function main() {
200
173
  config = await loadConfig();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mokoconsulting/mcp-mokosuite",
3
- "version": "1.1.0-dev.372",
3
+ "version": "1.1.0",
4
4
  "description": "MCP server for MokoSuite (Joomla) REST API v1 operations",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/index.ts CHANGED
@@ -25,9 +25,6 @@ import type { ApiConfig, ApiResponse } from './types.js';
25
25
  /** Base path for MokoSuiteClient Web Services (appended after /api/index.php). */
26
26
  const MSC = '/v1/mokosuiteclient';
27
27
 
28
- /** Base path for MokoSuiteExtensions (store) Web Services (appended after /api/index.php). */
29
- const MSE = '/v1/mokosuiteextensions';
30
-
31
28
  let config: ApiConfig;
32
29
 
33
30
  function clientFor(connection?: string): ApiClient {
@@ -67,7 +64,7 @@ function requireConfirm(confirm: boolean): { content: Array<{ type: 'text'; text
67
64
 
68
65
  const server = new McpServer({
69
66
  name: 'mcp-mokosuite',
70
- version: '1.1.0',
67
+ version: '1.0.0',
71
68
  });
72
69
 
73
70
  // ════════════════════════════════════════════════════════════════════════
@@ -325,61 +322,6 @@ server.tool(
325
322
  },
326
323
  );
327
324
 
328
- // ════════════════════════════════════════════════════════════════════════
329
- // LAYER 1 — Extensions Store (MokoSuiteExtensions)
330
- // ════════════════════════════════════════════════════════════════════════
331
-
332
- server.tool(
333
- 'store_catalog_list',
334
- 'List the products available in the MokoSuite Extensions store catalog for a site.',
335
- { ...ConnectionParam },
336
- async ({ connection }) => formatResponse(await clientFor(connection).get(`${MSE}/catalog`)),
337
- );
338
-
339
- server.tool(
340
- 'store_catalog_get',
341
- 'Get a single MokoSuite Extensions store product by its catalog id.',
342
- {
343
- id: z.number().describe('Catalog product id'),
344
- ...ConnectionParam,
345
- },
346
- async ({ id, connection }) => formatResponse(await clientFor(connection).get(`${MSE}/catalog/${id}`)),
347
- );
348
-
349
- server.tool(
350
- 'store_install',
351
- 'Install a MokoSuite Extensions store product on a site by its catalog id.',
352
- {
353
- id: z.number().describe('Catalog product id to install'),
354
- ...ConnectionParam,
355
- },
356
- async ({ id, connection }) => formatResponse(await clientFor(connection).post(`${MSE}/catalog/${id}/install`)),
357
- );
358
-
359
- server.tool(
360
- 'store_update',
361
- 'Update an installed MokoSuite Extensions store product on a site by its catalog id.',
362
- {
363
- id: z.number().describe('Catalog product id to update'),
364
- ...ConnectionParam,
365
- },
366
- async ({ id, connection }) => formatResponse(await clientFor(connection).post(`${MSE}/catalog/${id}/update`)),
367
- );
368
-
369
- server.tool(
370
- 'store_uninstall',
371
- 'Uninstall a MokoSuite Extensions store product from a site by its catalog id. Destructive — requires `confirm: true`.',
372
- {
373
- id: z.number().describe('Catalog product id to uninstall'),
374
- confirm: z.boolean().describe('Must be true — uninstalling removes the product from the target site'),
375
- ...ConnectionParam,
376
- },
377
- async ({ id, confirm, connection }) => {
378
- if (!confirm) return text('Refused: set `confirm: true` to uninstall this store product.');
379
- return formatResponse(await clientFor(connection).post(`${MSE}/catalog/${id}/uninstall`));
380
- },
381
- );
382
-
383
325
  // ── Start Server ────────────────────────────────────────────────────────
384
326
 
385
327
  async function main(): Promise<void> {
@@ -1,266 +0,0 @@
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.Cascade
8
- # REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
9
- # PATH: /.mokogit/workflows/cascade-dev.yml
10
- # VERSION: 02.04.00
11
- # BRIEF: Cascade main -> dev; branch-motivated (no schedule); skips dev-only repos (no main); auto-merge clean, auto-resolve routine conflicts INLINE (manifest version / .mokogit-.gitea-.github / docs-stamps) with no mokocli dependency, fall back to cascade_resolve.php then notify
12
-
13
- name: "Cascade Main -> Dev"
14
-
15
- on:
16
- # Branch-motivated (no time-based schedule): any real push to main cascades to
17
- # dev immediately. Sync commits from workflow_sync.php carry [skip ci] and are
18
- # skipped by the platform (they fire no trigger at all), so workflow_sync.php
19
- # dispatches this workflow directly (workflow_dispatch) after it changes a repo
20
- # — that is what carries .mokogit drift down to dev, replacing the old daily cron.
21
- push:
22
- branches:
23
- - main
24
- workflow_dispatch:
25
-
26
- permissions:
27
- contents: write
28
- pull-requests: write
29
-
30
- env:
31
- MOKOGIT_URL: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
32
- # ntfy destination is configured via repo or org variables (org vars are inherited).
33
- NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
34
- NTFY_TOPIC: ${{ vars.CASCADE_NTFY_TOPIC || vars.NTFY_TOPIC || 'git-releases' }}
35
-
36
- jobs:
37
- cascade:
38
- name: Cascade main -> dev
39
- runs-on: ubuntu-latest
40
- steps:
41
- - name: Checkout (full history for merge/resolve)
42
- uses: actions/checkout@v4
43
- with:
44
- fetch-depth: 0
45
- token: ${{ github.token }}
46
-
47
- - name: Cascade main -> dev (auto-resolve routine conflicts, else notify)
48
- env:
49
- TOKEN: ${{ github.token }}
50
- REPO: ${{ github.repository }}
51
- MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
52
- run: |
53
- set -uo pipefail
54
- API="${MOKOGIT_URL}/api/v1/repos/${REPO}"
55
- AUTH="Authorization: token ${TOKEN}"
56
- jqget() { python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('$1',''))" 2>/dev/null; }
57
-
58
- # 0. dev must exist
59
- if ! curl -sf -H "$AUTH" "${API}/branches/dev" >/dev/null 2>&1; then
60
- echo "No dev branch - nothing to cascade."; exit 0
61
- fi
62
-
63
- # 0b. main must exist too. A dev-only repo (no main branch) has nothing to
64
- # cascade FROM, so skip immediately instead of spinning up on the daily
65
- # schedule / manual dispatch and no-op'ing later via a failed compare.
66
- if ! curl -sf -H "$AUTH" "${API}/branches/main" >/dev/null 2>&1; then
67
- echo "No main branch (dev-only repo) - nothing to cascade."; exit 0
68
- fi
69
-
70
- # 1. is main ahead of dev?
71
- AHEAD=$(curl -sf -H "$AUTH" "${API}/compare/dev...main" \
72
- | python3 -c "import sys,json; print(json.load(sys.stdin).get('total_commits',0))" 2>/dev/null || echo 0)
73
- if [ "${AHEAD:-0}" -eq 0 ]; then
74
- echo "dev already up to date with main."; exit 0
75
- fi
76
- echo "main is ${AHEAD} commit(s) ahead of dev."
77
-
78
- # 2. reuse an open main->dev PR, else create one
79
- PR=$(curl -sf -H "$AUTH" "${API}/pulls?state=open&base=dev" \
80
- | python3 -c "import sys,json; d=json.load(sys.stdin); print(next((str(p['number']) for p in d if p.get('head',{}).get('ref')=='main'), ''))" 2>/dev/null || echo "")
81
- if [ -z "$PR" ]; then
82
- RESP=$(curl -s -H "$AUTH" -H "Content-Type: application/json" -X POST "${API}/pulls" \
83
- -d '{"head":"main","base":"dev","title":"chore(sync): cascade main -> dev","body":"Automated cascade of main into dev. Auto-merges when conflict-free, auto-resolves VERSION-stamp-only conflicts, otherwise left open for manual resolution."}')
84
- PR=$(printf '%s' "$RESP" | jqget number)
85
- if [ -z "$PR" ]; then
86
- echo "::warning::Could not open cascade PR: $RESP"; exit 0
87
- fi
88
- echo "Opened cascade PR #${PR}"
89
- else
90
- echo "Reusing open cascade PR #${PR}"
91
- fi
92
-
93
- notify() {
94
- curl -sS \
95
- -H "Title: ${REPO}: dev cascade needs manual merge" \
96
- -H "Tags: warning,twisted_rightwards_arrows" \
97
- -H "Priority: high" \
98
- -H "Click: ${MOKOGIT_URL}/${REPO}/pulls/${PR}" \
99
- -d "main -> dev cascade PR #${PR} $1 It was NOT auto-merged; resolve it manually." \
100
- "${NTFY_URL}/${NTFY_TOPIC}" || true
101
- }
102
-
103
- # 3. wait for MokoGIT to compute mergeability (conflict detection)
104
- MERGEABLE=""
105
- for _ in 1 2 3 4 5 6; do
106
- MERGEABLE=$(curl -sf -H "$AUTH" "${API}/pulls/${PR}" | jqget mergeable)
107
- case "$MERGEABLE" in True|False) break ;; esac
108
- sleep 3
109
- done
110
- echo "mergeable=${MERGEABLE}"
111
-
112
- # 4a. conflict-free -> merge via API (existing behaviour)
113
- if [ "$MERGEABLE" = "True" ]; then
114
- CODE=$(curl -s -o /tmp/merge.json -w "%{http_code}" -H "$AUTH" -H "Content-Type: application/json" \
115
- -X POST "${API}/pulls/${PR}/merge" -d '{"Do":"merge","merge_when_checks_succeed":true}')
116
- if [ "$CODE" -ge 200 ] && [ "$CODE" -lt 300 ]; then
117
- echo "Cascade PR #${PR} merged (or scheduled to merge when checks pass)."
118
- exit 0
119
- fi
120
- echo "::warning::Auto-merge returned HTTP ${CODE}: $(cat /tmp/merge.json)"
121
- notify "could not be auto-merged (HTTP ${CODE})."
122
- exit 0
123
- fi
124
-
125
- # 4b. conflicts -> try to auto-resolve if they are ONLY VERSION-stamp lines.
126
- echo "PR not cleanly mergeable; checking whether conflicts are VERSION-stamp-only..."
127
- git config user.name "MokoGIT Cascade"
128
- git config user.email "actions@mokoconsulting.tech"
129
- git fetch --quiet origin main dev
130
- git checkout -B dev origin/dev
131
-
132
- if git merge --no-ff --no-commit origin/main >/dev/null 2>&1; then
133
- # Became clean at git level (e.g. mergeability was still computing) -> commit + push.
134
- git commit -m "chore(sync): cascade main -> dev [skip ci]" >/dev/null
135
- git push origin dev
136
- echo "Cascade merged cleanly at git level and pushed to dev."
137
- exit 0
138
- fi
139
-
140
- CONFLICTS=$(git diff --name-only --diff-filter=U)
141
- echo "Conflicted files:"; echo "${CONFLICTS}"
142
-
143
- # --- Inline resolver (pure bash/git, NO mokocli dependency) -------------
144
- # The common cascade churn is routine and deterministic, so resolve it here
145
- # BEFORE reaching for cascade_resolve.php. This is what keeps the version
146
- # ladder moving even when mokocli is unreachable on an ephemeral runner
147
- # (the failure mode this workflow was hardened against): a pure
148
- # version-stamp conflict must never stall dev behind main.
149
- #
150
- # * manifest <version> conflicts (*.xml containing <extension) ->
151
- # version = max(dev, main) via `sort -V`, rewriting the tag while
152
- # preserving the file's own indentation (repos MIX tab and 2-space).
153
- # * .mokogit/** .gitea/** .github/** -> take main/canonical (--theirs).
154
- # * docs / CHANGELOG / stamp files -> keep dev (--ours).
155
- # Anything still conflicted afterwards falls through to cascade_resolve.php
156
- # (if available) and finally to a human, exactly as before.
157
- resolve_manifest_version() {
158
- # $1 = path to a conflicted *.xml manifest. Resolves ONLY the <version>
159
- # conflict to max(dev,main); returns 0 if fully resolved, 1 otherwise.
160
- local f="$1"
161
- # Bail if the conflict touches anything other than a single <version> line
162
- # on each side (i.e. a real content conflict, not just the stamp).
163
- local nonver
164
- nonver=$(awk '
165
- /^<<<<<<< /{c=1;next} /^=======$/{c=2;next} /^>>>>>>> /{c=0;next}
166
- (c==1||c==2) && $0 !~ /<version>[^<]*<\/version>/ {print "X"}
167
- ' "$f")
168
- [ -n "$nonver" ] && return 1
169
- # Collect the version value from each side of every conflict hunk.
170
- local ours theirs maxv
171
- ours=$(awk '
172
- /^<<<<<<< /{c=1;next} /^=======$/{c=2;next} /^>>>>>>> /{c=0;next}
173
- c==1' "$f" | sed -n 's/.*<version>\([^<]*\)<\/version>.*/\1/p' | head -n1)
174
- theirs=$(awk '
175
- /^<<<<<<< /{c=1;next} /^=======$/{c=2;next} /^>>>>>>> /{c=0;next}
176
- c==2' "$f" | sed -n 's/.*<version>\([^<]*\)<\/version>.*/\1/p' | head -n1)
177
- [ -z "$ours" ] && [ -z "$theirs" ] && return 1
178
- maxv=$(printf '%s\n%s\n' "$ours" "$theirs" | grep -v '^$' | sort -V | tail -n1)
179
- [ -z "$maxv" ] && return 1
180
- # Rewrite: drop conflict markers, keep OUR side's <version> line but with
181
- # the max value substituted (preserves this file's leading indentation,
182
- # tab or spaces). Drop the theirs-side version line.
183
- awk -v maxv="$maxv" '
184
- /^<<<<<<< /{c=1;next}
185
- /^=======$/{c=2;next}
186
- /^>>>>>>> /{c=0;next}
187
- {
188
- if (c==2) next
189
- if (($0 ~ /<version>[^<]*<\/version>/) && (c==1)) {
190
- sub(/<version>[^<]*<\/version>/, "<version>" maxv "</version>")
191
- }
192
- print
193
- }' "$f" > "${f}.resolved" && mv "${f}.resolved" "$f"
194
- # Fully resolved only if no markers remain.
195
- ! grep -qE '^(<<<<<<< |=======$|>>>>>>> )' "$f"
196
- }
197
-
198
- for f in ${CONFLICTS}; do
199
- case "$f" in
200
- .mokogit/*|.gitea/*|.github/*)
201
- git checkout --theirs -- "$f" && git add -- "$f" \
202
- && echo "resolved (theirs/main): $f" ;;
203
- *CHANGELOG*|*/docs/*|docs/*|*.md|*VERSION*|*.stamp)
204
- git checkout --ours -- "$f" && git add -- "$f" \
205
- && echo "resolved (ours/dev): $f" ;;
206
- *.xml)
207
- if grep -q '<extension' "$f" 2>/dev/null && resolve_manifest_version "$f"; then
208
- git add -- "$f"; echo "resolved (manifest version=max): $f"
209
- else
210
- echo "unresolved (xml, not a simple version stamp): $f"
211
- fi ;;
212
- *)
213
- echo "unresolved (needs deeper resolver): $f" ;;
214
- esac
215
- done
216
-
217
- # If the inline pass cleared every conflict, commit + push and we are done —
218
- # no mokocli needed.
219
- if [ -z "$(git diff --name-only --diff-filter=U)" ]; then
220
- git commit -m "chore(sync): cascade main -> dev (auto-resolved inline) [skip ci]" >/dev/null
221
- git push origin dev
222
- echo "Cascade auto-resolved inline (no mokocli) and pushed to dev."
223
- curl -s -H "$AUTH" -H "Content-Type: application/json" -X POST "${API}/issues/${PR}/comments" \
224
- -d '{"body":"Cascade auto-resolved inline (manifest version=max(dev,main), .mokogit/.gitea/.github -> main, docs/stamps -> dev) with no mokocli dependency, and pushed to dev. Closing."}' >/dev/null || true
225
- curl -s -H "$AUTH" -H "Content-Type: application/json" -X PATCH "${API}/pulls/${PR}" \
226
- -d '{"state":"closed"}' >/dev/null || true
227
- exit 0
228
- fi
229
-
230
- echo "Inline pass left conflicts; falling back to cascade_resolve.php..."
231
- echo "Still conflicted:"; git diff --name-only --diff-filter=U
232
-
233
- # --- Fallback: cascade_resolve.php (existing behaviour) -----------------
234
- # Handles the messier cases (modify/delete, plugin renames, etc.). It BAILS
235
- # (exit 2) on real code conflicts, so those still get a human. ubuntu-latest
236
- # has no /opt/mokocli, so clone mokocli on demand.
237
- RESOLVER=/opt/mokocli/cli/cascade_resolve.php
238
- if [ ! -f "$RESOLVER" ]; then
239
- if git clone --depth 1 --quiet \
240
- "https://x-access-token:${MOKOGIT_TOKEN}@${MOKOGIT_URL#https://}/MokoConsulting/mokocli.git" \
241
- /tmp/mokocli 2>/dev/null \
242
- || git clone --depth 1 --quiet "${MOKOGIT_URL}/MokoConsulting/mokocli.git" /tmp/mokocli 2>/dev/null; then
243
- RESOLVER=/tmp/mokocli/cli/cascade_resolve.php
244
- else
245
- # Do NOT swallow this silently: a degraded run must be visible so the
246
- # operator knows the fallback resolver was unavailable.
247
- echo "::warning::On-demand mokocli clone failed; cascade_resolve.php is UNAVAILABLE. Inline resolver handled the routine classes; any remaining conflicts need a human."
248
- RESOLVER=/tmp/mokocli/cli/cascade_resolve.php
249
- fi
250
- fi
251
-
252
- if [ -f "$RESOLVER" ] && php "$RESOLVER" --path . --theirs-ref origin/main; then
253
- git commit -m "chore(sync): cascade main -> dev (auto-resolved) [skip ci]" >/dev/null
254
- git push origin dev
255
- echo "Cascade auto-resolved and pushed to dev."
256
- curl -s -H "$AUTH" -H "Content-Type: application/json" -X POST "${API}/issues/${PR}/comments" \
257
- -d '{"body":"cascade_resolve auto-resolved the merge (manifests/workflows/docs/stamps, modify-delete) and pushed to dev. Closing."}' >/dev/null || true
258
- curl -s -H "$AUTH" -H "Content-Type: application/json" -X PATCH "${API}/pulls/${PR}" \
259
- -d '{"state":"closed"}' >/dev/null || true
260
- exit 0
261
- fi
262
-
263
- # Resolver unavailable, or it bailed on a real (code) conflict -> human.
264
- git merge --abort || true
265
- notify "has code conflicts cascade_resolve could not safely auto-resolve."
266
- exit 0