Compare commits

..

73 Commits

Author SHA1 Message Date
jmiller c2b88e9a94 chore: sync auto-release.yml from Template-Joomla [skip ci] 2026-06-22 00:35:32 +00:00
jmiller 845ed4b53d chore: remove unused Makefile - builds handled by CI auto-release 2026-06-21 23:55:27 +00:00
gitea-actions[bot] 7281cd1500 chore: promote changelog [Unreleased] → [01.04.01]
Universal: Auto Version Bump / Version Bump (push) Successful in 11s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 24s
2026-06-21 23:25:06 +00:00
gitea-actions[bot] a1b2bf40ce chore(release): build 01.04.01 [skip ci]
Publish to Composer / Publish Package (release) Failing after 5s
2026-06-21 23:25:01 +00:00
jmiller 854dbc6350 chore: sync issue-branch.yml from Template-Joomla [skip ci] 2026-06-21 23:24:00 +00:00
jmiller 1bcbe800e9 Merge pull request 'chore: remove automation directory' (#148) from fix/remove-automation into main 2026-06-21 23:10:38 +00:00
gitea-actions[bot] c8918df03e chore(version): pre-release bump to 01.04.01-dev [skip ci]
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 25s
Publish to Composer / Publish Package (release) Failing after 49s
Branch Cleanup / Delete merged branch (pull_request) Failing after 1s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Failing after 4s
2026-06-21 23:09:49 +00:00
Jonathan Miller 54236c0d73 chore: remove automation directory
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 18s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 11s
Universal: PR Check / Branch Policy (pull_request) Failing after 1s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 4s
Universal: PR Check / Secret Scan (pull_request) Successful in 8s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Validate PR (pull_request) Failing after 16s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 50s
Universal: Build & Release / Promote to RC (pull_request) Failing after 13s
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
2026-06-21 18:03:44 -05:00
jmiller 33ce9784cc chore: sync repo-health.yml from Template-Joomla [skip ci] 2026-06-21 22:55:54 +00:00
jmiller 582a16e132 chore: sync pre-release.yml from Template-Joomla [skip ci] 2026-06-21 22:55:53 +00:00
jmiller e97388c119 chore: sync pr-check.yml from Template-Joomla [skip ci] 2026-06-21 22:55:52 +00:00
jmiller 954cdaa2ae chore: sync issue-branch.yml from Template-Joomla [skip ci] 2026-06-21 22:55:51 +00:00
jmiller c60be2bf3c chore: sync auto-release.yml from Template-Joomla [skip ci] 2026-06-21 22:55:50 +00:00
gitea-actions[bot] cec436f90e chore(release): build 01.04.00 [skip ci]
Publish to Composer / Publish Package (release) Failing after 4s
2026-06-21 22:48:33 +00:00
jmiller a4e39df6ed Fix: add missing system plugins to package manifest, remove old src/ (#137, #145)
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
2026-06-21 22:48:16 +00:00
gitea-actions[bot] 70dbb65173 chore(version): auto-bump 01.03.01-dev [skip ci]
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Failing after 4s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 17s
2026-06-21 22:46:05 +00:00
Jonathan Miller 6a00d7ddf9 fix: add missing events/gallery system plugins to package manifest (#137)
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Successful in 9s
Update Server / Update Server (push) Successful in 11s
Added plg_system_mokosuitecross_events and plg_system_mokosuitecross_gallery
to pkg_mokosuitecross.xml. These content source plugins hook into Joomla
system events to cross-post calendar events and gallery images but were
not being installed with the package.

Also removed the old src/ directory (pre-rename mokojoomcross cruft).

Closes #137
2026-06-21 17:45:31 -05:00
gitea-actions[bot] 78c7b99c6a chore(release): build 01.03.00 [skip ci]
Publish to Composer / Publish Package (release) Failing after 55s
2026-06-21 22:26:57 +00:00
jmiller 6d56949452 Release 01.02.00: MokoSuiteCross rebrand, bug fixes, infrastructure (#144)
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
2026-06-21 22:24:22 +00:00
gitea-actions[bot] 137b2556ac chore(release): build 01.02.00-rc [skip ci]
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Failing after 1s
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Failing after 5s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 22s
2026-06-21 17:23:04 -05:00
Jonathan Miller 9b711d2309 docs: update CHANGELOG with PR workflow check, fix duplicate header 2026-06-21 17:23:04 -05:00
Jonathan Miller 122c7b630a feat: Telegram @mokosuite_bot default, wiki folders, README/CHANGELOG update
- Telegram: updated default bot from @MokoWaaSBot to @mokosuite_bot
- Telegram: embedded obfuscated bot token in plugin PHP (XOR + base64)
- Telegram: added <config> section to plugin XML for parse_mode/preview
- Telegram: removed bot token from admin-visible plugin params
- Branding: replaced all MokoWaaS references with MokoSuite
- Wiki: reorganized into getting-started/, user-guide/, services/, developer/
- README: updated with all 36 service plugins and current features
- CHANGELOG: added entries for recent fixes and changes
2026-06-21 17:23:03 -05:00
gitea-actions[bot] 8ab62abf29 chore(version): auto-bump 01.01.02-dev [skip ci] 2026-06-21 17:23:03 -05:00
Jonathan Miller 27505f7501 fix: rename all MOKOJOOMCROSS language keys and events to MOKOSUITECROSS (#128, #138)
Completes the MokoJoomCross → MokoSuiteCross rebrand across all language
string keys, Joomla event names, documentation, and wiki pages.

- 1,151 language key references renamed (COM_, PLG_, PKG_ prefixes)
- Event names renamed (onMokoJoomCross* → onMokoSuiteCross*)
- CLAUDE.md, CHANGELOG.md, wiki docs updated
- Zero mokojoomcross references remaining in codebase

Closes #128, closes #138
2026-06-21 17:23:02 -05:00
gitea-actions[bot] 65bba1f561 chore(version): auto-bump patch 01.01.01-dev [skip ci] 2026-06-21 17:22:32 -05:00
Jonathan Miller 28db9a67b6 fix: remove duplicate curl_setopt_array calls in 4 service plugins (#139)
SendGrid and Reddit had a second curl_setopt_array that referenced an
undefined $token variable, silently breaking auth. TikTok and Pinterest
had identical duplicates (no variable bug but dead code).

Removes the duplicate block from each plugin's publish() method.
2026-06-21 17:22:31 -05:00
jmiller b9b0c88ad5 chore: sync auto-release.yml from Template-Generic [skip ci] 2026-06-21 22:03:18 +00:00
jmiller 370fa86f59 chore: sync pre-release.yml from Template-Generic [skip ci] 2026-06-21 16:05:59 +00:00
jmiller b6bed1e6df chore: sync composer-publish.yml from Template-Generic [skip ci] 2026-06-21 06:35:21 +00:00
jmiller acf599b25e chore: sync workflow-sync-trigger.yml from Template-Generic [skip ci] 2026-06-21 01:29:14 +00:00
jmiller a1dd54db72 chore: sync auto-release.yml from Template-Generic [skip ci] 2026-06-21 01:29:12 +00:00
jmiller 3403785e1f ci: sync rc-revert.yml from Template-Joomla [skip ci] 2026-06-21 00:15:06 +00:00
jmiller 7d1f30aaaa ci: sync issue-branch.yml from Template-Joomla [skip ci] 2026-06-21 00:14:37 +00:00
jmiller 282fe5fce1 ci: sync ci-joomla.yml from Template-Joomla [skip ci] 2026-06-21 00:14:12 +00:00
jmiller 1430b18583 chore: sync pr-check.yml from Template-Generic [skip ci] 2026-06-20 23:46:57 +00:00
jmiller 54bcd044be chore: sync gitleaks.yml from Template-Generic [skip ci] 2026-06-20 23:46:56 +00:00
jmiller 1b719a6216 chore: sync ci-generic.yml from Template-Generic [skip ci] 2026-06-20 23:46:55 +00:00
jmiller c825e800e0 chore: sync repo-health.yml from Template-Generic [skip ci] 2026-06-20 22:30:23 +00:00
jmiller 81103615a4 chore: sync rc-revert.yml from Template-Generic [skip ci] 2026-06-20 22:30:23 +00:00
jmiller 93c3c5b214 chore: sync pr-check.yml from Template-Generic [skip ci] 2026-06-20 22:30:22 +00:00
jmiller c0d5a884a4 chore: sync cleanup.yml from Template-Generic [skip ci] 2026-06-20 22:30:21 +00:00
jmiller f5eed45566 ci: sync security-audit.yml from Template-Joomla [skip ci] 2026-06-20 22:26:32 +00:00
jmiller 6633e38d8f ci: sync repo-health.yml from Template-Joomla [skip ci] 2026-06-20 22:26:03 +00:00
jmiller 8b88c1f368 ci: sync rc-revert.yml from Template-Joomla [skip ci] 2026-06-20 22:25:54 +00:00
jmiller b5705ffffe ci: sync pr-check.yml from Template-Joomla [skip ci] 2026-06-20 22:24:47 +00:00
jmiller 311178278a ci: sync issue-branch.yml from Template-Joomla [skip ci] 2026-06-20 22:22:22 +00:00
jmiller c1732e6932 ci: sync cleanup.yml from Template-Joomla [skip ci] 2026-06-20 22:15:37 +00:00
jmiller 4444b116d1 chore: sync ci-generic.yml from Template-Generic [skip ci] 2026-06-20 21:35:43 +00:00
jmiller bfb2b9f925 ci: sync ci-generic.yml from Template-Joomla [skip ci] 2026-06-20 21:34:03 +00:00
jmiller 88b3d0df0f ci: sync cascade-dev.yml from Template-Joomla [skip ci] 2026-06-20 21:31:35 +00:00
jmiller b97b76eb0d ci: sync branch-cleanup.yml from Template-Joomla [skip ci] 2026-06-20 21:28:10 +00:00
jmiller 7571b26969 ci: sync auto-release.yml from Template-Joomla [skip ci] 2026-06-20 21:26:58 +00:00
jmiller 4eba3d2be4 chore: sync workflow-sync-trigger.yml from Template-Generic [skip ci] 2026-06-20 20:53:59 +00:00
jmiller 238dc29535 chore: sync rc-revert.yml from Template-Generic [skip ci] 2026-06-20 20:53:58 +00:00
jmiller 6765c2406e chore: sync issue-branch.yml from Template-Generic [skip ci] 2026-06-20 20:53:56 +00:00
jmiller da67260991 ci: sync ci-generic.yml from Template-Joomla [skip ci] 2026-06-20 20:35:06 +00:00
jmiller b485cc6fb5 ci: sync cascade-dev.yml from Template-Joomla [skip ci] 2026-06-20 20:32:53 +00:00
jmiller 4d6f76acde ci: sync branch-cleanup.yml from Template-Joomla [skip ci] 2026-06-20 20:31:54 +00:00
jmiller e106b6d4be ci: sync auto-release.yml from Template-Joomla [skip ci] 2026-06-20 20:31:00 +00:00
jmiller 566b6c2e6e ci: sync auto-bump.yml from Template-Joomla [skip ci] 2026-06-20 19:59:09 +00:00
jmiller 625e7d1337 ci: sync ci-generic.yml from Template-Joomla [skip ci] 2026-06-20 19:06:00 +00:00
jmiller 14f3f4a17c ci: sync cascade-dev.yml from Template-Joomla [skip ci] 2026-06-20 19:03:18 +00:00
jmiller a5066645d8 ci: sync branch-cleanup.yml from Template-Joomla [skip ci] 2026-06-20 19:02:45 +00:00
jmiller 47678a892c ci: sync auto-release.yml from Template-Joomla [skip ci] 2026-06-20 19:01:05 +00:00
jmiller f07806d3dc ci: sync auto-bump.yml from Template-Joomla [skip ci] 2026-06-20 18:53:51 +00:00
jmiller 99fd758900 ci: sync pre-release workflow from Template-Joomla
Generic: Project CI / Lint & Validate (push) Failing after 8s
Generic: Project CI / Tests (push) Has been cancelled
2026-06-20 18:49:30 +00:00
jmiller ff4cdf3c93 ci: add Joomla metadata validation workflow for PRs
Generic: Project CI / Lint & Validate (push) Failing after 40s
Generic: Project CI / Tests (push) Has been cancelled
2026-06-20 18:39:09 +00:00
jmiller f1e7f0dd18 fix: rename moko-platform to mokocli + changelog promotion in workflows
Generic: Project CI / Lint & Validate (push) Failing after 34s
Generic: Project CI / Tests (push) Has been cancelled
2026-06-20 17:16:55 +00:00
jmiller 86427f9b44 fix: rename moko-platform to mokocli + changelog promotion in workflows
Generic: Project CI / Lint & Validate (push) Failing after 34s
Generic: Project CI / Tests (push) Has been cancelled
2026-06-20 17:16:54 +00:00
jmiller 4003f53acc fix: rename moko-platform to mokocli + changelog promotion in workflows
Generic: Project CI / Lint & Validate (push) Failing after 11s
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Project CI / Tests (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-20 17:16:54 +00:00
jmiller 4bec6c4cfd fix: rename moko-platform to mokocli + changelog promotion in workflows
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Project CI / Lint & Validate (push) Failing after 10s
Generic: Project CI / Tests (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-20 17:16:53 +00:00
jmiller 5cdc8f533d fix: rename moko-platform to mokocli + changelog promotion in workflows
Generic: Project CI / Lint & Validate (push) Failing after 1m6s
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Project CI / Tests (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-20 17:16:52 +00:00
jmiller a40dfa7e69 fix: rename moko-platform to mokocli + changelog promotion in workflows
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Project CI / Lint & Validate (push) Failing after 57s
Generic: Project CI / Tests (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-20 17:16:51 +00:00
89 changed files with 1532 additions and 2269 deletions
+1 -1
View File
@@ -5,7 +5,7 @@
<display-name>Package - MokoSuiteCross</display-name>
<org>MokoConsulting</org>
<description>Cross-posting Joomla content to social media, email marketing, and chat platforms</description>
<version>01.02.00</version>
<version>01.04.01</version>
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
</identity>
<governance>
+9 -9
View File
@@ -4,8 +4,8 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# INGROUP: mokocli.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /.mokogitea/workflows/auto-bump.yml
# VERSION: 09.02.00
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
@@ -43,19 +43,19 @@ jobs:
token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 1
- name: Setup moko-platform tools
- name: Setup mokocli tools
run: |
if ! command -v composer &> /dev/null; then
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
fi
if [ -d "/opt/moko-platform/cli" ]; then
echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV"
if [ -d "/opt/mokocli/cli" ]; then
echo "MOKO_CLI=/opt/mokocli/cli" >> "$GITHUB_ENV"
else
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \
/tmp/moko-platform-api
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/mokocli.git" \
/tmp/mokocli
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
echo "MOKO_CLI=/tmp/mokocli/cli" >> "$GITHUB_ENV"
fi
- name: Bump version
+180 -38
View File
@@ -4,15 +4,15 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: mokoplatform.Release
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokoplatform
# INGROUP: mokocli.Release
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli
# PATH: /templates/workflows/universal/auto-release.yml.template
# VERSION: 05.00.00
# BRIEF: Universal build & release detects platform from manifest.xml
#
# +========================================================================+
# +=======================================================================+
# | UNIVERSAL BUILD & RELEASE PIPELINE |
# +========================================================================+
# +=======================================================================+
# | |
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
# | |
@@ -21,7 +21,7 @@
# | dolibarr: mod*.class.php, update.txt, dev version reset |
# | generic: README-only, no update stream |
# | |
# +========================================================================+
# +=======================================================================+
name: "Universal: Build & Release"
@@ -30,6 +30,15 @@ on:
types: [opened, closed]
branches:
- main
paths-ignore:
- '.mokogitea/workflows/**'
- '*.md'
- 'wiki/**'
- '.editorconfig'
- '.gitignore'
- '.gitattributes'
- '.gitmessage'
- 'LICENSE'
workflow_dispatch:
inputs:
action:
@@ -51,7 +60,7 @@ permissions:
contents: write
jobs:
# ── PR Opened → Rename branch to RC and build RC release ─────────────────────
# ── PR Opened → Rename branch to RC and build RC release ─────────────────────────
promote-rc:
name: Promote to RC
runs-on: release
@@ -66,25 +75,25 @@ jobs:
token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 1
- name: Setup mokoplatform tools
- name: Setup mokocli tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
run: |
if [ -f /opt/mokoplatform/cli/version_bump.php ] && [ -f /opt/mokoplatform/vendor/autoload.php ]; then
echo Using pre-installed /opt/mokoplatform
echo MOKO_CLI=/opt/mokoplatform/cli >> $GITHUB_ENV
if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then
echo Using pre-installed /opt/mokocli
echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV
else
echo Falling back to fresh clone
if ! command -v composer > /dev/null 2>&1; then
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
fi
rm -rf /tmp/mokoplatform-api
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokoplatform.git
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokoplatform-api
cd /tmp/mokoplatform-api
rm -rf /tmp/mokocli
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
cd /tmp/mokocli
composer install --no-dev --no-interaction --quiet
echo MOKO_CLI=/tmp/mokoplatform-api/cli >> $GITHUB_ENV
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
fi
- name: Rename branch to rc
@@ -109,13 +118,47 @@ jobs:
--path . --stability rc --bump minor --branch rc \
--token "${{ secrets.MOKOGITEA_TOKEN }}"
- name: Update RC release notes from CHANGELOG.md
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
# Extract [Unreleased] section from changelog
NOTES=""
if [ -f "CHANGELOG.md" ]; then
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
fi
[ -z "$NOTES" ] && NOTES="Release candidate"
# Find the RC release and update its body
RELEASE_ID=$(curl -sf -H "Authorization: token ${TOKEN}" \
"${API_BASE}/releases/tags/release-candidate" \
| python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
if [ -n "$RELEASE_ID" ]; then
python3 -c "
import json, urllib.request
body = open('/dev/stdin').read()
payload = json.dumps({'body': body}).encode()
req = urllib.request.Request(
'${API_BASE}/releases/${RELEASE_ID}',
data=payload, method='PATCH',
headers={
'Authorization': 'token ${TOKEN}',
'Content-Type': 'application/json'
})
urllib.request.urlopen(req)
" <<< "$NOTES"
echo "RC release notes updated from CHANGELOG.md"
fi
- name: Summary
if: always()
run: |
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY
# ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
# ── Merged PR → Build & Release (or promote RC to stable) ─────────────────────────
release:
name: Build & Release Pipeline
runs-on: release
@@ -149,50 +192,131 @@ jobs:
fi
echo "No conflict markers found"
- name: Setup mokoplatform tools
- name: Setup mokocli tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
run: |
if [ -f /opt/mokoplatform/cli/version_bump.php ] && [ -f /opt/mokoplatform/vendor/autoload.php ]; then
echo Using pre-installed /opt/mokoplatform
echo MOKO_CLI=/opt/mokoplatform/cli >> $GITHUB_ENV
if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then
echo Using pre-installed /opt/mokocli
echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV
else
echo Falling back to fresh clone
if ! command -v composer > /dev/null 2>&1; then
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
fi
rm -rf /tmp/mokoplatform-api
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokoplatform.git
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokoplatform-api
cd /tmp/mokoplatform-api
rm -rf /tmp/mokocli
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
cd /tmp/mokocli
composer install --no-dev --no-interaction --quiet
echo MOKO_CLI=/tmp/mokoplatform-api/cli >> $GITHUB_ENV
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
fi
- name: "Detect platform"
id: platform
run: |
php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true
php ${MOKO_CLI}/manifest_read.php --path . --github-output 2>/dev/null || true
- name: "Determine version bump level"
id: bump
run: |
# Fix/patch branches: version was already bumped by pre-release, just strip suffix
# Feature/dev branches: bump minor for the new stable release
HEAD_REF="${{ github.event.pull_request.head.ref || 'dev' }}"
case "$HEAD_REF" in
fix/*|patch/*|hotfix/*|bugfix/*) BUMP="none" ;;
*) BUMP="minor" ;;
esac
echo "level=${BUMP}" >> "$GITHUB_OUTPUT"
echo "Bump level: ${BUMP} (from branch: ${HEAD_REF})"
- name: "Publish stable release"
run: |
BUMP_FLAG=""
if [ "${{ steps.bump.outputs.level }}" != "none" ]; then
BUMP_FLAG="--bump ${{ steps.bump.outputs.level }}"
fi
php ${MOKO_CLI}/release_publish.php \
--path . --stability stable --bump minor --branch main \
--path . --stability stable ${BUMP_FLAG} --branch main \
--token "${{ secrets.MOKOGITEA_TOKEN }}"
- name: Update release notes from CHANGELOG.md
- name: "Read published version"
id: version
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
# Extract [Unreleased] section from changelog
if [ -f "CHANGELOG.md" ]; then
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
[ -z "$NOTES" ] && NOTES="Stable release"
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "")
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
[ -z "$VERSION" ] && VERSION="00.00.00" && echo "skip=true" >> "$GITHUB_OUTPUT"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
PLATFORM="${{ steps.platform.outputs.platform }}"
if [[ "$PLATFORM" == joomla* ]]; then
echo "tag=stable" >> "$GITHUB_OUTPUT"
echo "release_tag=stable" >> "$GITHUB_OUTPUT"
else
NOTES="Stable release"
echo "tag=v${VERSION}" >> "$GITHUB_OUTPUT"
echo "release_tag=v${VERSION}" >> "$GITHUB_OUTPUT"
fi
echo "branch=main" >> "$GITHUB_OUTPUT"
echo "Published version: ${VERSION}"
- name: "Create semver tag for non-Joomla repos"
id: semver
if: |
steps.version.outputs.skip != 'true' &&
!startsWith(steps.platform.outputs.platform, 'joomla')
run: |
VERSION="${{ steps.version.outputs.version }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
SEMVER_TAG="v${VERSION}"
echo "Creating semver tag: ${SEMVER_TAG}"
# Create the git tag via API
HTTP_CODE=$(curl -sf -o /dev/null -w "%{http_code}" \
-X POST -H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
"${API_BASE}/tags" \
-d "{\"tag_name\":\"${SEMVER_TAG}\",\"target\":\"main\",\"message\":\"Release ${VERSION}\"}" 2>/dev/null || echo "000")
if [ "$HTTP_CODE" = "201" ] || [ "$HTTP_CODE" = "200" ]; then
echo "Created semver tag: ${SEMVER_TAG}"
elif [ "$HTTP_CODE" = "409" ]; then
echo "Semver tag ${SEMVER_TAG} already exists (skipped)"
else
echo "::warning::Failed to create semver tag ${SEMVER_TAG} (HTTP ${HTTP_CODE})"
fi
# Update release body via API
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
"${API_BASE}/releases/tags/stable" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
echo "semver_tag=${SEMVER_TAG}" >> "$GITHUB_OUTPUT"
- name: Update release notes and promote changelog
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
# Get the stable release info (version and ID)
RELEASE_JSON=$(curl -sf -H "Authorization: token ${TOKEN}" \
"${API_BASE}/releases/tags/stable" 2>/dev/null || echo '{}')
RELEASE_ID=$(python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" <<< "$RELEASE_JSON" 2>/dev/null || true)
# Extract version from release name (e.g. "06.17.00" or "v06.17.00")
VERSION=$(python3 -c "
import json, sys, re
r = json.load(sys.stdin)
name = r.get('name', '')
m = re.search(r'(\d+\.\d+\.\d+)', name)
print(m.group(1) if m else '')
" <<< "$RELEASE_JSON" 2>/dev/null || true)
# Extract [Unreleased] section from changelog
NOTES=""
if [ -f "CHANGELOG.md" ]; then
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
fi
[ -z "$NOTES" ] && NOTES="Stable release"
# Update release body via API
if [ -n "$RELEASE_ID" ]; then
python3 -c "
import json, urllib.request
@@ -202,7 +326,7 @@ jobs:
'${API_BASE}/releases/${RELEASE_ID}',
data=payload, method='PATCH',
headers={
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
'Authorization': 'token ${TOKEN}',
'Content-Type': 'application/json'
})
urllib.request.urlopen(req)
@@ -210,6 +334,24 @@ jobs:
echo "Release notes updated from CHANGELOG.md"
fi
# Promote [Unreleased] → [version] in CHANGELOG.md and reset
if [ -n "$VERSION" ] && [ -f "CHANGELOG.md" ]; then
DATE=$(date +%Y-%m-%d)
python3 -c "
import sys
version, date = sys.argv[1], sys.argv[2]
content = open('CHANGELOG.md').read()
old = '## [Unreleased]'
new = f'## [Unreleased]\n\n## [{version}] --- {date}'
content = content.replace(old, new, 1)
open('CHANGELOG.md', 'w').write(content)
" "$VERSION" "$DATE"
git add CHANGELOG.md
git commit -m "chore: promote changelog [Unreleased] → [${VERSION}]" || true
git push origin main || true
echo "Changelog promoted: [Unreleased] → [${VERSION}]"
fi
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
- name: "Step 9: Mirror release to GitHub"
if: >-
+1 -1
View File
@@ -5,7 +5,7 @@
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /.mokogitea/workflows/branch-cleanup.yml
# VERSION: 01.00.00
# BRIEF: Delete feature branches after PR merge
-13
View File
@@ -13,19 +13,6 @@
name: "Generic: Project CI"
on:
push:
branches:
- main
- dev
- dev/**
- rc/**
- version/**
pull_request:
branches:
- main
- dev
- dev/**
- rc/**
workflow_dispatch:
permissions:
+410 -7
View File
@@ -45,17 +45,17 @@ jobs:
fi
php -v && composer --version
- name: Setup moko-platform tools
- name: Setup mokocli tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || github.token }}
MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.GA_TOKEN || github.token }}
MOKO_CLONE_HOST: ${{ secrets.MOKOGITEA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
run: |
if [ -d "/tmp/moko-platform" ] || [ -d "/opt/moko-platform" ]; then
echo "moko-platform already available on runner — skipping clone"
if [ -d "/opt/mokocli" ] || [ -d "/tmp/mokocli" ]; then
echo "mokocli already available on runner — skipping clone"
else
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
/tmp/moko-platform 2>/dev/null || echo "moko-platform clone skipped — continuing without it"
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git" \
/tmp/mokocli 2>/dev/null || echo "mokocli clone skipped — continuing without it"
fi
- name: Install dependencies
@@ -245,10 +245,413 @@ jobs:
echo "All ${CHECKED} directories contain index.html." >> $GITHUB_STEP_SUMMARY
fi
- name: Check config.xml and access.xml for components
run: |
echo "### Component Config & ACL Check" >> $GITHUB_STEP_SUMMARY
ERRORS=0
# Find all component manifests (XML with type="component")
COMP_MANIFESTS=$(find . -maxdepth 4 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*" -exec grep -l '<extension[^>]*type="component"' {} ; 2>/dev/null || true)
if [ -z "$COMP_MANIFESTS" ]; then
echo "No component extensions found — skipping." >> $GITHUB_STEP_SUMMARY
else
for MANIFEST in $COMP_MANIFESTS; do
COMP_DIR=$(dirname "$MANIFEST")
COMP_NAME=$(basename "$COMP_DIR")
echo "Component: `${COMP_NAME}` (manifest: `${MANIFEST}`)" >> $GITHUB_STEP_SUMMARY
# Check access.xml exists
ACCESS_FILE=$(find "$COMP_DIR" -name "access.xml" -not -path "./.git/*" 2>/dev/null | head -1)
if [ -z "$ACCESS_FILE" ]; then
echo "- Missing `access.xml` — ACL permissions will not work." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
if command -v php &> /dev/null; then
if ! php -r "@simplexml_load_file('$ACCESS_FILE') ?: exit(1);" 2>/dev/null; then
echo "- `access.xml` is not well-formed XML." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
for ACTION in core.admin core.manage; do
if ! grep -q "name=\"${ACTION}\"" "$ACCESS_FILE" 2>/dev/null; then
echo "- `access.xml` missing required action: `${ACTION}`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
done
echo "- `access.xml`: valid" >> $GITHUB_STEP_SUMMARY
fi
fi
fi
# Check config.xml exists
CONFIG_FILE=$(find "$COMP_DIR" -name "config.xml" -not -path "./.git/*" 2>/dev/null | head -1)
if [ -z "$CONFIG_FILE" ]; then
echo "- Missing `config.xml` — component Options page will be empty." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
if command -v php &> /dev/null; then
if ! php -r "@simplexml_load_file('$CONFIG_FILE') ?: exit(1);" 2>/dev/null; then
echo "- `config.xml` is not well-formed XML." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "- `config.xml`: valid" >> $GITHUB_STEP_SUMMARY
fi
fi
fi
done
fi
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${ERRORS}" -gt 0 ]; then
echo "**${ERRORS} config/ACL issue(s) found.**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "**Component config & ACL check passed.**" >> $GITHUB_STEP_SUMMARY
fi
- name: SQL schema validation
run: |
echo "### SQL Schema Validation" >> $GITHUB_STEP_SUMMARY
ERRORS=0
# Find SQL files in source/htdocs
SQL_FILES=$(find . -name "*.sql" -path "*/sql/*" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null)
if [ -z "$SQL_FILES" ]; then
echo "No SQL files found — skipping." >> $GITHUB_STEP_SUMMARY
else
echo "Found $(echo "$SQL_FILES" | wc -l) SQL file(s)" >> $GITHUB_STEP_SUMMARY
for FILE in $SQL_FILES; do
# Basic syntax check: balanced parentheses, no empty files
SIZE=$(wc -c < "$FILE" | tr -d ' ')
if [ "$SIZE" -eq 0 ]; then
echo "- Empty SQL file: \`${FILE}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
continue
fi
# Check for common SQL errors
if grep -qP '^\s*$' "$FILE" && [ "$SIZE" -lt 5 ]; then
echo "- Whitespace-only SQL file: \`${FILE}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
continue
fi
echo "- \`${FILE}\`: ${SIZE} bytes" >> $GITHUB_STEP_SUMMARY
done
# Check update SQL files follow version numbering pattern
UPDATE_DIR=$(find . -path "*/sql/updates/mysql" -type d -not -path "./.git/*" 2>/dev/null | head -1)
if [ -n "$UPDATE_DIR" ]; then
BAD_NAMES=0
for UFILE in "$UPDATE_DIR"/*.sql; do
[ ! -f "$UFILE" ] && continue
BASENAME=$(basename "$UFILE" .sql)
if ! echo "$BASENAME" | grep -qP '^\d+\.\d+\.\d+'; then
echo "- Update file \`${UFILE}\` does not follow version naming (expected X.Y.Z.sql)" >> $GITHUB_STEP_SUMMARY
BAD_NAMES=$((BAD_NAMES + 1))
fi
done
if [ "$BAD_NAMES" -gt 0 ]; then
ERRORS=$((ERRORS + BAD_NAMES))
fi
fi
fi
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${ERRORS}" -gt 0 ]; then
echo "**${ERRORS} SQL issue(s) found.**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "**SQL schema validation passed.**" >> $GITHUB_STEP_SUMMARY
fi
- name: Manifest file references check
run: |
echo "### Manifest File References" >> $GITHUB_STEP_SUMMARY
ERRORS=0
MANIFEST=""
for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
if grep -q "<extension" "$XML_FILE" 2>/dev/null; then
MANIFEST="$XML_FILE"
break
fi
done
if [ -z "$MANIFEST" ]; then
echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY
else
MANIFEST_DIR=$(dirname "$MANIFEST")
# Check <filename> references
FILENAMES=$(grep -oP '<filename[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null || true)
for F in $FILENAMES; do
if [ ! -f "${MANIFEST_DIR}/${F}" ] && [ ! -d "${MANIFEST_DIR}/${F}" ]; then
echo "- Missing: \`${F}\` (referenced in manifest)" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
done
# Check <folder> references
FOLDERS=$(grep -oP '<folder[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null || true)
for F in $FOLDERS; do
if [ ! -d "${MANIFEST_DIR}/${F}" ]; then
echo "- Missing folder: \`${F}\` (referenced in manifest)" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
done
# Check <file> references in package manifests (ZIP files won't exist in source)
EXT_TYPE=$(grep -oP '<extension[^>]*\btype="\K[^"]+' "$MANIFEST" | head -1)
if [ "$EXT_TYPE" != "package" ]; then
FILES=$(grep -oP '<file[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null || true)
for F in $FILES; do
if [ ! -f "${MANIFEST_DIR}/${F}" ]; then
echo "- Missing file: \`${F}\` (referenced in manifest)" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
done
fi
fi
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${ERRORS}" -gt 0 ]; then
echo "**${ERRORS} missing file reference(s).**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "**Manifest file references check passed.**" >> $GITHUB_STEP_SUMMARY
fi
- name: Form XML validation
run: |
echo "### Form XML Validation" >> $GITHUB_STEP_SUMMARY
ERRORS=0
FORM_FILES=$(find . -name "*.xml" -path "*/forms/*" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null)
if [ -z "$FORM_FILES" ]; then
echo "No form XML files found — skipping." >> $GITHUB_STEP_SUMMARY
else
echo "Found $(echo "$FORM_FILES" | wc -l) form file(s)" >> $GITHUB_STEP_SUMMARY
for FILE in $FORM_FILES; do
if command -v php &> /dev/null; then
if ! php -r "@simplexml_load_file('$FILE') ?: exit(1);" 2>/dev/null; then
echo "- \`${FILE}\`: malformed XML" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
# Check for valid Joomla form structure
if ! grep -qE '<form|<field|<fieldset' "$FILE" 2>/dev/null; then
echo "- \`${FILE}\`: no \`<form>\`, \`<field>\`, or \`<fieldset>\` elements found" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "- \`${FILE}\`: valid" >> $GITHUB_STEP_SUMMARY
fi
fi
fi
done
fi
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${ERRORS}" -gt 0 ]; then
echo "**${ERRORS} form XML issue(s).**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "**Form XML validation passed.**" >> $GITHUB_STEP_SUMMARY
fi
- name: Deprecated Joomla API check
continue-on-error: true
run: |
echo "### Deprecated Joomla API Check" >> $GITHUB_STEP_SUMMARY
WARNINGS=0
SRC_DIR=""
for DIR in source/ src/ htdocs/; do
[ -d "$DIR" ] && SRC_DIR="$DIR" && break
done
if [ -z "$SRC_DIR" ]; then
echo "No source directory found — skipping." >> $GITHUB_STEP_SUMMARY
else
# Joomla 3/4 deprecated patterns that break in Joomla 6
PATTERNS=(
'JFactory::'
'JText::'
'JHtml::'
'JRoute::'
'JUri::'
'JLog::'
'JTable::'
'JInput'
'CMSFactory::\$application'
'JApplicationCms'
)
for PATTERN in "${PATTERNS[@]}"; do
HITS=$(grep -rnl "$PATTERN" "$SRC_DIR" --include="*.php" 2>/dev/null || true)
if [ -n "$HITS" ]; then
COUNT=$(echo "$HITS" | wc -l)
echo "- \`${PATTERN}\` found in ${COUNT} file(s)" >> $GITHUB_STEP_SUMMARY
WARNINGS=$((WARNINGS + COUNT))
fi
done
echo "" >> $GITHUB_STEP_SUMMARY
if [ "$WARNINGS" -gt 0 ]; then
echo "**${WARNINGS} deprecated API usage(s) found.** These will break in Joomla 6." >> $GITHUB_STEP_SUMMARY
else
echo "**No deprecated APIs found.**" >> $GITHUB_STEP_SUMMARY
fi
fi
- name: Template output escaping check
continue-on-error: true
run: |
echo "### Template Output Escaping" >> $GITHUB_STEP_SUMMARY
WARNINGS=0
TMPL_FILES=$(find . -name "*.php" -path "*/tmpl/*" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null)
if [ -z "$TMPL_FILES" ]; then
echo "No template files found — skipping." >> $GITHUB_STEP_SUMMARY
else
echo "Found $(echo "$TMPL_FILES" | wc -l) template file(s)" >> $GITHUB_STEP_SUMMARY
for FILE in $TMPL_FILES; do
# Check for unescaped output: <?= $var ?> or echo $var without escape()
UNESCAPED=$(grep -nP '<\?=\s*\$(?!this->escape)' "$FILE" 2>/dev/null || true)
if [ -n "$UNESCAPED" ]; then
HITS=$(echo "$UNESCAPED" | wc -l)
echo "- \`${FILE}\`: ${HITS} unescaped \`<?= \$var ?>\` output(s) — use \`<?= \$this->escape(\$var) ?>\`" >> $GITHUB_STEP_SUMMARY
WARNINGS=$((WARNINGS + HITS))
fi
# Check for echo without escaping in template context
RAW_ECHO=$(grep -nP '^\s*echo\s+\$(?!this->escape)' "$FILE" 2>/dev/null || true)
if [ -n "$RAW_ECHO" ]; then
HITS=$(echo "$RAW_ECHO" | wc -l)
echo "- \`${FILE}\`: ${HITS} raw \`echo \$var\` — consider \`echo \$this->escape(\$var)\`" >> $GITHUB_STEP_SUMMARY
WARNINGS=$((WARNINGS + HITS))
fi
done
echo "" >> $GITHUB_STEP_SUMMARY
if [ "$WARNINGS" -gt 0 ]; then
echo "**${WARNINGS} potential XSS risk(s) in templates.** Review unescaped output." >> $GITHUB_STEP_SUMMARY
else
echo "**All template output appears properly escaped.**" >> $GITHUB_STEP_SUMMARY
fi
fi
- name: Namespace consistency check
run: |
echo "### Namespace Consistency" >> $GITHUB_STEP_SUMMARY
ERRORS=0
# Find component/plugin manifests with <namespace> tags
MANIFESTS=$(find . -maxdepth 4 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*" -exec grep -l '<namespace' {} \; 2>/dev/null || true)
if [ -z "$MANIFESTS" ]; then
echo "No manifests with \`<namespace>\` found — skipping." >> $GITHUB_STEP_SUMMARY
else
for MANIFEST in $MANIFESTS; do
NS_PATH=$(grep -oP '<namespace[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1)
[ -z "$NS_PATH" ] && continue
MANIFEST_DIR=$(dirname "$MANIFEST")
echo "Manifest: \`${MANIFEST}\` → namespace \`${NS_PATH}\`" >> $GITHUB_STEP_SUMMARY
# Check PHP files have matching namespace
while IFS= read -r -d '' PHP_FILE; do
FILE_NS=$(grep -oP '^\s*namespace\s+\K[^;]+' "$PHP_FILE" 2>/dev/null | head -1)
[ -z "$FILE_NS" ] && continue
# Namespace should start with the manifest namespace path
if ! echo "$FILE_NS" | grep -qF "${NS_PATH}"; then
echo "- \`${PHP_FILE}\`: namespace \`${FILE_NS}\` doesn't match manifest \`${NS_PATH}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
done < <(find "$MANIFEST_DIR" -name "*.php" -path "*/src/*" -not -path "./vendor/*" -print0 2>/dev/null)
done
fi
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${ERRORS}" -gt 0 ]; then
echo "**${ERRORS} namespace mismatch(es).**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "**Namespace consistency check passed.**" >> $GITHUB_STEP_SUMMARY
fi
- name: SPDX license header check
continue-on-error: true
run: |
echo "### SPDX License Headers" >> $GITHUB_STEP_SUMMARY
MISSING=0
SRC_DIR=""
for DIR in source/ src/ htdocs/; do
[ -d "$DIR" ] && SRC_DIR="$DIR" && break
done
if [ -z "$SRC_DIR" ]; then
echo "No source directory found — skipping." >> $GITHUB_STEP_SUMMARY
else
TOTAL=0
while IFS= read -r -d '' FILE; do
TOTAL=$((TOTAL + 1))
if ! head -10 "$FILE" | grep -qi "SPDX"; then
echo "- Missing SPDX header: \`${FILE}\`" >> $GITHUB_STEP_SUMMARY
MISSING=$((MISSING + 1))
fi
done < <(find "$SRC_DIR" -name "*.php" -not -path "./vendor/*" -print0)
echo "" >> $GITHUB_STEP_SUMMARY
if [ "$MISSING" -gt 0 ]; then
echo "**${MISSING}/${TOTAL} PHP file(s) missing SPDX license header.**" >> $GITHUB_STEP_SUMMARY
else
echo "**All ${TOTAL} PHP files have SPDX headers.**" >> $GITHUB_STEP_SUMMARY
fi
fi
- name: Service provider check
run: |
echo "### Service Provider Check" >> $GITHUB_STEP_SUMMARY
ERRORS=0
PROVIDERS=$(find . -name "provider.php" -path "*/services/*" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null)
if [ -z "$PROVIDERS" ]; then
echo "No service providers found — skipping." >> $GITHUB_STEP_SUMMARY
else
for FILE in $PROVIDERS; do
# Must return a ServiceProviderInterface
if ! grep -qP 'ServiceProviderInterface|ComponentInterface|MVCFactoryInterface|DispatcherInterface' "$FILE" 2>/dev/null; then
echo "- \`${FILE}\`: does not reference ServiceProviderInterface or component interfaces" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "- \`${FILE}\`: valid service provider" >> $GITHUB_STEP_SUMMARY
fi
# Must have return statement
if ! grep -qP '^\s*return\s+new\s+' "$FILE" 2>/dev/null; then
echo "- \`${FILE}\`: missing \`return new ...\` statement" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
done
fi
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${ERRORS}" -gt 0 ]; then
echo "**${ERRORS} service provider issue(s).**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "**Service provider check passed.**" >> $GITHUB_STEP_SUMMARY
fi
release-readiness:
name: Release Readiness Check
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' && github.base_ref == 'main'
continue-on-error: true
steps:
- name: Checkout repository
+76
View File
@@ -0,0 +1,76 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# SPDX-License-Identifier: GPL-3.0-or-later
name: "Publish to Composer"
on:
push:
tags:
- 'v*'
- '[0-9]*.[0-9]*.[0-9]*'
release:
types: [published]
workflow_dispatch:
env:
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
jobs:
publish:
name: Publish Package
runs-on: ubuntu-latest
if: >-
!contains(github.event.head_commit.message, '[skip ci]') &&
!contains(github.event.head_commit.message, '[skip publish]')
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup PHP
run: |
if ! command -v php &> /dev/null; then
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
fi
- name: Install dependencies
run: composer install --no-dev --no-interaction --prefer-dist --quiet
- name: Determine version
id: version
run: |
VERSION=$(php -r "echo json_decode(file_get_contents('composer.json'))->version;")
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "Package version: ${VERSION}"
# Gitea Composer Registry — auto-publishes from tags
# The tag push itself registers the package at:
# https://git.mokoconsulting.tech/api/packages/MokoConsulting/composer
- name: Verify Gitea registry
run: |
echo "Gitea Composer registry auto-publishes from tags."
echo "Package available at: ${GITEA_URL}/api/packages/MokoConsulting/composer"
echo "Install: composer require mokoconsulting/mokocli"
# Packagist — notify of new version
- name: Notify Packagist
if: secrets.PACKAGIST_TOKEN != ''
run: |
VERSION="${{ steps.version.outputs.version }}"
echo "Notifying Packagist of version ${VERSION}..."
curl -sf -X POST \
-H "Content-Type: application/json" \
-d '{"repository":{"url":"https://git.mokoconsulting.tech/MokoConsulting/mokocli"}}' \
"https://packagist.org/api/update-package?username=mokoconsulting&apiToken=${{ secrets.PACKAGIST_TOKEN }}" \
&& echo "Packagist notified" \
|| echo "::warning::Packagist notification failed (package may not be registered yet)"
- name: Summary
run: |
VERSION="${{ steps.version.outputs.version }}"
echo "## Composer Package Published" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Registry | Status |" >> $GITHUB_STEP_SUMMARY
echo "|----------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Gitea | \`composer require mokoconsulting/mokocli:${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Packagist | \`composer require mokoconsulting/mokocli\` |" >> $GITHUB_STEP_SUMMARY
-4
View File
@@ -25,10 +25,6 @@
name: "Universal: Secret Scanning"
on:
pull_request:
branches:
- main
- 'dev/**'
schedule:
- cron: '0 5 * * 1' # Weekly Monday 05:00 UTC
workflow_dispatch:
+3 -3
View File
@@ -4,8 +4,8 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: mokoplatform.Automation
# VERSION: 01.02.00
# INGROUP: mokocli.Automation
# VERSION: 01.00.00
# BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch"
@@ -28,7 +28,7 @@ jobs:
steps:
- name: Create branch and comment
run: |
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
TOKEN="${{ secrets.GA_TOKEN }}"
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
ISSUE_NUM="${{ github.event.issue.number }}"
ISSUE_TITLE="${{ github.event.issue.title }}"
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,71 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Validation
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /templates/workflows/joomla/pr-metadata-check.yml.template
# VERSION: 01.00.00
# BRIEF: Validate MokoGitea metadata matches Joomla extension manifest on PRs
name: "Joomla: Metadata Validation"
on:
pull_request:
types: [opened, synchronize, reopened, converted_to_draft, ready_for_review]
permissions:
contents: read
env:
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
jobs:
validate-metadata:
name: "Validate Joomla Metadata"
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup mokocli tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
run: |
if [ -f /opt/mokocli/cli/joomla_metadata_validate.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then
echo Using pre-installed /opt/mokocli
echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV
else
echo Falling back to fresh clone
if ! command -v composer > /dev/null 2>&1; then
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
fi
rm -rf /tmp/mokocli
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
fi
- name: Validate metadata against Joomla manifest
env:
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
run: |
php ${MOKO_CLI}/joomla_metadata_validate.php \
--path . \
--token "${GITEA_TOKEN}" \
--org "${GITEA_ORG}" \
--repo "${GITEA_REPO}" \
--api-base "${GITEA_URL}/api/v1" \
--ci
if [ $? -ne 0 ]; then
echo "::error::Joomla metadata mismatch — update delivery will fail. Run 'php cli/joomla_metadata_validate.php' locally to see details."
exit 1
fi
+36 -27
View File
@@ -4,23 +4,26 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: mokoplatform.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
# INGROUP: mokocli.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /templates/workflows/universal/pre-release.yml.template
# VERSION: 05.01.00
# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch
# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches
name: "Universal: Pre-Release"
on:
pull_request:
types: [closed]
push:
branches:
- dev
pull_request_target:
types: [synchronize, opened, reopened]
branches:
- main
- 'fix/**'
- 'patch/**'
- 'hotfix/**'
- 'bugfix/**'
- 'chore/**'
- alpha
- beta
- rc
workflow_dispatch:
inputs:
stability:
@@ -43,12 +46,11 @@ env:
jobs:
build:
name: "Build Pre-Release (${{ inputs.stability || 'development' }})"
name: "Build Pre-Release (${{ inputs.stability || github.ref_name }})"
runs-on: release
if: >-
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'pull_request' && github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev') ||
(github.event_name == 'pull_request_target' && github.event.pull_request.base.ref == 'main')
github.event_name == 'push'
steps:
- name: Checkout
@@ -56,40 +58,47 @@ jobs:
with:
fetch-depth: 0
token: ${{ secrets.MOKOGITEA_TOKEN }}
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || '' }}
ref: ${{ github.ref_name }}
- name: Setup mokoplatform tools
- name: Setup mokocli tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
run: |
# Use pre-installed /opt/mokoplatform if available (updated by cron every 6h)
if [ -f /opt/mokoplatform/cli/version_bump.php ] && [ -f /opt/mokoplatform/cli/manifest_element.php ] && [ -f /opt/mokoplatform/vendor/autoload.php ]; then
echo Using pre-installed /opt/mokoplatform
echo MOKO_CLI=/opt/mokoplatform/cli >> $GITHUB_ENV
# Use pre-installed /opt/mokocli if available (updated by cron every 6h)
if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/cli/manifest_element.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then
echo Using pre-installed /opt/mokocli
echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV
else
echo Falling back to fresh clone
if ! command -v composer > /dev/null 2>&1; then
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
fi
rm -rf /tmp/mokoplatform-api
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokoplatform.git
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokoplatform-api
cd /tmp/mokoplatform-api && composer install --no-dev --no-interaction --quiet
echo MOKO_CLI=/tmp/mokoplatform-api/cli >> $GITHUB_ENV
rm -rf /tmp/mokocli
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
fi
- name: Detect platform
id: platform
run: |
# Auto-detect and update platform if not set in manifest
php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true
php ${MOKO_CLI}/manifest_read.php --path . --github-output
- name: Resolve metadata and bump version
id: meta
run: |
# Auto-detect stability: RC for PRs targeting main, else use input or default to development
if [ "${{ github.event_name }}" = "pull_request_target" ] && [ "${{ github.event.pull_request.base.ref }}" = "main" ]; then
STABILITY="release-candidate"
# Auto-detect stability from branch name on push, or use input on dispatch
if [ "${{ github.event_name }}" = "push" ]; then
case "${{ github.ref_name }}" in
rc) STABILITY="release-candidate" ;;
alpha) STABILITY="alpha" ;;
beta) STABILITY="beta" ;;
*) STABILITY="development" ;;
esac
else
STABILITY="${{ inputs.stability || 'development' }}"
fi
@@ -164,7 +173,7 @@ jobs:
php ${MOKO_CLI}/release_create.php \
--path . --version "$VERSION" --tag "$TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --branch dev --prerelease
--repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease
- name: Update release notes from CHANGELOG.md
run: |
+66
View File
@@ -0,0 +1,66 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /.mokogitea/workflows/rc-revert.yml
# VERSION: 09.23.00
# BRIEF: Rename rc/ branch back to dev/ when PR is closed without merge
name: "RC Revert"
on:
pull_request:
types: [closed]
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
revert:
name: Rename rc/ back to dev/
runs-on: ubuntu-latest
if: >-
github.event.pull_request.merged == false &&
startsWith(github.event.pull_request.head.ref, 'rc/')
steps:
- name: Rename branch
run: |
BRANCH="${{ github.event.pull_request.head.ref }}"
SUFFIX="${BRANCH#rc/}"
DEV_BRANCH="dev/${SUFFIX}"
API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
# Create dev/ branch from rc/ branch
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X POST \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"new_branch_name\": \"${DEV_BRANCH}\", \"old_branch_name\": \"${BRANCH}\"}" \
"${API}" 2>/dev/null || true)
if [ "$STATUS" = "201" ]; then
echo "Created branch: ${DEV_BRANCH}" >> $GITHUB_STEP_SUMMARY
else
echo "::error::Failed to create ${DEV_BRANCH} from ${BRANCH} (HTTP ${STATUS})"
exit 1
fi
# Delete rc/ branch
ENCODED=$(php -r "echo rawurlencode('${BRANCH}');")
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \
-H "Authorization: token ${TOKEN}" \
"${API}/${ENCODED}" 2>/dev/null || true)
if [ "$STATUS" = "204" ]; then
echo "Deleted branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
else
echo "::warning::Failed to delete ${BRANCH} (HTTP ${STATUS})"
fi
echo "### RC Reverted" >> $GITHUB_STEP_SUMMARY
echo "${BRANCH} → ${DEV_BRANCH}" >> $GITHUB_STEP_SUMMARY
+8 -9
View File
@@ -7,8 +7,8 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: mokoplatform.Validation
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokoplatform
# INGROUP: mokocli.Validation
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli
# PATH: /templates/workflows/joomla/repo_health.yml.template
# VERSION: 09.23.00
# BRIEF: Enforces repository guardrails by validating scripts governance, tooling availability, and core repository health artifacts.
@@ -33,7 +33,8 @@ on:
- scripts
- repo
pull_request:
push:
branches:
- main
permissions:
contents: read
@@ -296,19 +297,17 @@ jobs:
missing_required=()
missing_optional=()
# Source directory: source/, src/, or htdocs/ (any is valid for extension repos)
# Source directory: src/ or htdocs/ (either is valid for extension repos)
SOURCE_DIR=""
if [ -d "source" ]; then
SOURCE_DIR="source"
elif [ -d "src" ]; then
if [ -d "src" ]; then
SOURCE_DIR="src"
elif [ -d "htdocs" ]; then
SOURCE_DIR="htdocs"
elif [ -d "deploy" ] || [ -d "cli" ] || [ -d "monitoring" ]; then
# Platform/tooling repos don't need source/
# Platform/tooling repos don't need src/
SOURCE_DIR=""
else
missing_required+=("source/ or src/ or htdocs/ (source directory required)")
missing_required+=("src/ or htdocs/ (source directory required)")
fi
for item in "${required_artifacts[@]}"; do
@@ -0,0 +1,73 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /.mokogitea/workflows/workflow-sync-trigger.yml
# VERSION: 01.01.00
# BRIEF: Trigger workflow sync to live repos when a PR is merged to main
name: "Universal: Workflow Sync Trigger"
on:
pull_request:
types: [closed]
branches:
- main
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
sync:
name: Sync workflows to live repos
runs-on: ubuntu-latest
if: >-
github.event.pull_request.merged == true &&
!contains(github.event.pull_request.title, '[skip sync]')
steps:
- name: Determine platform from repo name
id: platform
run: |
REPO="${{ github.event.repository.name }}"
case "$REPO" in
Template-Joomla) PLATFORM="joomla" ;;
Template-Dolibarr) PLATFORM="dolibarr" ;;
Template-Go) PLATFORM="go" ;;
Template-MCP) PLATFORM="mcp" ;;
Template-Generic) PLATFORM="" ;;
*) PLATFORM="" ;;
esac
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
echo "Platform: ${PLATFORM:-all}"
- name: Clone mokocli
env:
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
run: |
GITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}"
git clone --depth 1 "${GITEA_URL}/MokoConsulting/mokocli.git" /tmp/mokocli
- name: Install dependencies
run: |
cd /tmp/mokocli
composer install --no-dev --no-interaction --quiet 2>/dev/null || true
- name: Run workflow sync
env:
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
run: |
ARGS="--token ${MOKOGITEA_TOKEN}"
ARGS="${ARGS} --org ${{ vars.GITEA_ORG || github.repository_owner }}"
ARGS="${ARGS} --phase repos"
PLATFORM="${{ steps.platform.outputs.platform }}"
if [ -n "$PLATFORM" ]; then
ARGS="${ARGS} --platform-filter ${PLATFORM}"
fi
php /tmp/mokocli/cli/workflow_sync.php ${ARGS}
+15 -7
View File
@@ -1,9 +1,22 @@
# Changelog
## [Unreleased]
## [01.04.01] --- 2026-06-21
<!-- VERSION: 01.02.00 -->
## [01.04.01] --- 2026-06-21
## [01.04.00] --- 2026-06-21
### Fixed
- **Package manifest**: Added missing `plg_system_mokosuitecross_events` and `plg_system_mokosuitecross_gallery` to `pkg_mokosuitecross.xml` — these system plugins were not installed with the package
- **Cleanup**: Removed old `src/` directory (pre-rename cruft with `mokojoomcross` files)
## [01.03.00] --- 2026-06-21
<!-- VERSION: 01.04.01 -->
All notable changes to MokoSuiteCross will be documented in this file.
@@ -229,8 +242,3 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- Perfect Publisher Pro migration tool in installer script
- Message template system with per-platform placeholders
- Post queue with scheduled posting, retry logic, and delivery tracking
## [01.00] - 2026-05-28
### Added
- Initial release
-203
View File
@@ -1,203 +0,0 @@
# Makefile for Joomla Extensions
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# SPDX-License-Identifier: GPL-3.0-or-later
#
# MokoSuiteCross — Cross-posting Joomla content to social media, email marketing, and chat platforms
# ==============================================================================
# CONFIGURATION - Customize these for your extension
# ==============================================================================
# Extension Configuration
EXTENSION_NAME := mokosuitecross
EXTENSION_TYPE := package
# Options: module, plugin, component, package, template
EXTENSION_VERSION := 1.0.0
# Module Configuration (for modules only)
MODULE_TYPE := site
# Options: site, admin
# Plugin Configuration (for plugins only)
PLUGIN_GROUP := system
# Options: system, content, user, authentication, etc.
# Directories
SRC_DIR := src
BUILD_DIR := build
DIST_DIR := dist
DOCS_DIR := docs
# Joomla Installation (for local testing - customize paths)
JOOMLA_ROOT := /var/www/html/joomla
JOOMLA_VERSION := 4
# Tools
PHP := php
COMPOSER := composer
NPM := npm
PHPCS := vendor/bin/phpcs
PHPCBF := vendor/bin/phpcbf
PHPUNIT := vendor/bin/phpunit
ZIP := zip
# Coding Standards
PHPCS_STANDARD := Joomla
# Colors for output
COLOR_RESET := \033[0m
COLOR_GREEN := \033[32m
COLOR_YELLOW := \033[33m
COLOR_BLUE := \033[34m
COLOR_RED := \033[31m
# ==============================================================================
# TARGETS
# ==============================================================================
.PHONY: help
help: ## Show this help message
@echo "$(COLOR_BLUE)╔════════════════════════════════════════════════════════════╗$(COLOR_RESET)"
@echo "$(COLOR_BLUE)║ Joomla Extension Makefile ║$(COLOR_RESET)"
@echo "$(COLOR_BLUE)╚════════════════════════════════════════════════════════════╝$(COLOR_RESET)"
@echo ""
@echo "Extension: $(EXTENSION_NAME) ($(EXTENSION_TYPE)) v$(EXTENSION_VERSION)"
@echo ""
@echo "$(COLOR_GREEN)Available targets:$(COLOR_RESET)"
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " $(COLOR_BLUE)%-20s$(COLOR_RESET) %s\n", $$1, $$2}'
@echo ""
.PHONY: install-deps
install-deps: ## Install all dependencies (Composer + npm)
@echo "$(COLOR_BLUE)Installing dependencies...$(COLOR_RESET)"
@if [ -f "composer.json" ]; then \
$(COMPOSER) install; \
echo "$(COLOR_GREEN)✓ Composer dependencies installed$(COLOR_RESET)"; \
fi
.PHONY: lint
lint: ## Run PHP linter (syntax check)
@echo "$(COLOR_BLUE)Running PHP linter...$(COLOR_RESET)"
@find . -name "*.php" ! -path "./vendor/*" ! -path "./node_modules/*" ! -path "./$(BUILD_DIR)/*" \
-exec $(PHP) -l {} \; | grep -v "No syntax errors" || true
@echo "$(COLOR_GREEN)✓ PHP linting complete$(COLOR_RESET)"
.PHONY: phpcs
phpcs: ## Run PHP CodeSniffer (Joomla standards)
@echo "$(COLOR_BLUE)Running PHP CodeSniffer...$(COLOR_RESET)"
@if [ -f "$(PHPCS)" ]; then \
$(PHPCS) --standard=$(PHPCS_STANDARD) --extensions=php --ignore=vendor,node_modules,$(BUILD_DIR) .; \
else \
echo "$(COLOR_YELLOW)⚠ PHP CodeSniffer not installed. Run: make install-deps$(COLOR_RESET)"; \
fi
.PHONY: validate
validate: lint phpcs ## Run all validation checks
@echo "$(COLOR_GREEN)✓ All validation checks passed$(COLOR_RESET)"
.PHONY: clean
clean: ## Clean build artifacts
@echo "$(COLOR_BLUE)Cleaning build artifacts...$(COLOR_RESET)"
@rm -rf $(BUILD_DIR) $(DIST_DIR)
@echo "$(COLOR_GREEN)✓ Build artifacts cleaned$(COLOR_RESET)"
MOKO_PLATFORM ?= $(or $(wildcard ../moko-platform),$(wildcard $(HOME)/moko-platform),$(wildcard /opt/moko-platform))
MINIFY_SCRIPT := $(MOKO_PLATFORM)/build/minify.js
.PHONY: minify
minify: ## Minify CSS/JS assets
@echo "Minifying assets..."
@if [ -f "$(MINIFY_SCRIPT)" ]; then \
node "$(MINIFY_SCRIPT)" $(SRC_DIR); \
elif [ -f "scripts/minify.js" ]; then \
node scripts/minify.js; \
else \
echo "No minify script found"; \
fi
.PHONY: build
build: clean validate minify ## Build extension package
@echo "$(COLOR_BLUE)Building Joomla extension package...$(COLOR_RESET)"
@mkdir -p $(DIST_DIR) $(BUILD_DIR)
# Determine package prefix based on extension type
@case "$(EXTENSION_TYPE)" in \
module) \
PACKAGE_PREFIX="mod_$(EXTENSION_NAME)"; \
BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \
;; \
plugin) \
PACKAGE_PREFIX="plg_$(PLUGIN_GROUP)_$(EXTENSION_NAME)"; \
BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \
;; \
component) \
PACKAGE_PREFIX="com_$(EXTENSION_NAME)"; \
BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \
;; \
package) \
PACKAGE_PREFIX="pkg_$(EXTENSION_NAME)"; \
BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \
;; \
template) \
PACKAGE_PREFIX="tpl_$(EXTENSION_NAME)"; \
BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \
;; \
*) \
echo "$(COLOR_RED)✗ Unknown extension type: $(EXTENSION_TYPE)$(COLOR_RESET)"; \
exit 1; \
;; \
esac; \
\
mkdir -p "$$BUILD_TARGET"; \
\
echo "Building $$PACKAGE_PREFIX..."; \
\
rsync -av --progress \
--exclude='$(BUILD_DIR)' \
--exclude='$(DIST_DIR)' \
--exclude='.git*' \
--exclude='vendor/' \
--exclude='node_modules/' \
--exclude='tests/' \
--exclude='Makefile' \
--exclude='composer.json' \
--exclude='composer.lock' \
--exclude='package.json' \
--exclude='package-lock.json' \
--exclude='phpunit.xml' \
--exclude='*.md' \
--exclude='.editorconfig' \
. "$$BUILD_TARGET/"; \
\
cd $(BUILD_DIR) && $(ZIP) -r "../$(DIST_DIR)/$${PACKAGE_PREFIX}-$(EXTENSION_VERSION).zip" "$${PACKAGE_PREFIX}"; \
\
echo "$(COLOR_GREEN)✓ Package created: $(DIST_DIR)/$${PACKAGE_PREFIX}-$(EXTENSION_VERSION).zip$(COLOR_RESET)"
.PHONY: package
package: build ## Alias for build
@echo "$(COLOR_GREEN)✓ Package ready for distribution$(COLOR_RESET)"
.PHONY: release
release: validate build ## Create a release (validate + build)
@echo "$(COLOR_GREEN)✓ Release package ready$(COLOR_RESET)"
.PHONY: version
version: ## Display version information
@echo "$(COLOR_BLUE)Extension Information:$(COLOR_RESET)"
@echo " Name: $(EXTENSION_NAME)"
@echo " Type: $(EXTENSION_TYPE)"
@echo " Version: $(EXTENSION_VERSION)"
.PHONY: security-check
security-check: ## Run security checks on dependencies
@echo "$(COLOR_BLUE)Running security checks...$(COLOR_RESET)"
@if [ -f "composer.json" ]; then \
$(COMPOSER) audit || echo "$(COLOR_YELLOW)⚠ Vulnerabilities found$(COLOR_RESET)"; \
fi
.PHONY: all
all: install-deps validate build ## Run complete build pipeline
@echo "$(COLOR_GREEN)✓ Complete build pipeline finished$(COLOR_RESET)"
# Default target
.DEFAULT_GOAL := help
+1 -1
View File
@@ -1,6 +1,6 @@
# MokoSuiteCross
<!-- VERSION: 01.02.00 -->
<!-- VERSION: 01.04.01 -->
Cross-posting Joomla content to social media, email marketing, and chat platforms for Joomla 5/6.
-237
View File
@@ -1,237 +0,0 @@
#!/usr/bin/env bash
# ============================================================================
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Automation.CI
# INGROUP: moko-platform.Automation
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /automation/ci-issue-reporter.sh
# VERSION: 09.23.00
# BRIEF: Creates or updates a Gitea issue when a CI gate fails.
# Deduplicates by searching open issues with the "ci-auto" label
# whose title matches the gate. If a matching issue exists, a comment
# is appended instead of opening a duplicate.
# ============================================================================
set -euo pipefail
# ── Defaults ────────────────────────────────────────────────────────────────
GITEA_URL="${GITEA_URL:-https://git.mokoconsulting.tech}"
GITEA_TOKEN="${GITEA_TOKEN:-}"
REPO="${GITHUB_REPOSITORY:-}"
RUN_URL="${GITHUB_SERVER_URL:-${GITEA_URL}}/${REPO}/actions/runs/${GITHUB_RUN_ID:-0}"
LABEL_NAME="ci-auto"
LABEL_COLOR="#e11d48"
GATE=""
DETAILS=""
SEVERITY="error"
WORKFLOW=""
# ── Parse arguments ─────────────────────────────────────────────────────────
usage() {
cat <<EOF
Usage: ci-issue-reporter.sh --gate NAME --details TEXT [OPTIONS]
Required:
--gate CI gate name (e.g. "Code Quality", "Self-Health")
--details Human-readable failure description
Optional:
--severity "error" (default) or "warning"
--workflow Workflow name for the issue title
--repo owner/repo (default: \$GITHUB_REPOSITORY)
--run-url URL to the CI run (auto-detected from env)
--token Gitea API token (default: \$GITEA_TOKEN)
--url Gitea base URL (default: \$GITEA_URL)
EOF
exit 1
}
while [[ $# -gt 0 ]]; do
case "$1" in
--gate) GATE="$2"; shift 2 ;;
--details) DETAILS="$2"; shift 2 ;;
--severity) SEVERITY="$2"; shift 2 ;;
--workflow) WORKFLOW="$2"; shift 2 ;;
--repo) REPO="$2"; shift 2 ;;
--run-url) RUN_URL="$2"; shift 2 ;;
--token) GITEA_TOKEN="$2"; shift 2 ;;
--url) GITEA_URL="$2"; shift 2 ;;
-h|--help) usage ;;
*) echo "Unknown option: $1"; usage ;;
esac
done
[[ -z "$GATE" ]] && { echo "ERROR: --gate is required"; usage; }
[[ -z "$DETAILS" ]] && { echo "ERROR: --details is required"; usage; }
[[ -z "$GITEA_TOKEN" ]] && { echo "ERROR: GITEA_TOKEN not set"; exit 1; }
[[ -z "$REPO" ]] && { echo "ERROR: GITHUB_REPOSITORY not set"; exit 1; }
API="${GITEA_URL}/api/v1/repos/${REPO}"
# ── Build title ─────────────────────────────────────────────────────────────
if [[ -n "$WORKFLOW" ]]; then
TITLE="[CI] ${WORKFLOW}: ${GATE} failed"
else
TITLE="[CI] ${GATE} failed"
fi
# ── Ensure label exists ─────────────────────────────────────────────────────
ensure_label() {
local exists
exists=$(curl -sf -o /dev/null -w '%{http_code}' \
-H "Authorization: token ${GITEA_TOKEN}" \
"${API}/labels" 2>/dev/null || echo "000")
if [[ "$exists" == "200" ]]; then
# Check if label already exists
local found
found=$(curl -sf \
-H "Authorization: token ${GITEA_TOKEN}" \
"${API}/labels" 2>/dev/null \
| grep -o "\"name\":\"${LABEL_NAME}\"" || true)
if [[ -z "$found" ]]; then
curl -sf -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/labels" \
-d "{\"name\":\"${LABEL_NAME}\",\"color\":\"${LABEL_COLOR}\",\"description\":\"Auto-created by CI issue reporter\"}" \
> /dev/null 2>&1 || true
fi
fi
}
# ── Search for existing open issue ──────────────────────────────────────────
find_existing_issue() {
# URL-encode the gate name for the query
local query
query=$(printf '%s' "[CI] ${GATE}" | sed 's/ /%20/g; s/\[/%5B/g; s/\]/%5D/g')
local response
response=$(curl -sf \
-H "Authorization: token ${GITEA_TOKEN}" \
"${API}/issues?type=issues&state=open&labels=${LABEL_NAME}&q=${query}&limit=5" \
2>/dev/null || echo "[]")
# Extract the first matching issue number
echo "$response" \
| grep -oP '"number":\s*\K[0-9]+' \
| head -1
}
# ── Build issue body ────────────────────────────────────────────────────────
build_body() {
local severity_badge
if [[ "$SEVERITY" == "error" ]]; then
severity_badge="**Severity:** Error"
else
severity_badge="**Severity:** Warning"
fi
cat <<BODY
## CI Gate Failure: ${GATE}
${severity_badge}
**Workflow:** ${WORKFLOW:-unknown}
**Branch:** ${GITHUB_REF_NAME:-unknown}
**Commit:** \`${GITHUB_SHA:0:8}\`
**Run:** [View CI run](${RUN_URL})
### Details
${DETAILS}
### Resolution
Fix the issue described above and push a new commit. This issue will be closed automatically when the gate passes, or can be closed manually.
---
*Auto-created by [ci-issue-reporter](${GITEA_URL}/${REPO}/src/branch/main/automation/ci-issue-reporter.sh)*
BODY
}
# ── Build comment body (for existing issues) ────────────────────────────────
build_comment() {
cat <<COMMENT
### CI failure recurrence
**Branch:** ${GITHUB_REF_NAME:-unknown}
**Commit:** \`${GITHUB_SHA:0:8}\`
**Run:** [View CI run](${RUN_URL})
${DETAILS}
COMMENT
}
# ── Main ────────────────────────────────────────────────────────────────────
ensure_label
EXISTING=$(find_existing_issue)
if [[ -n "$EXISTING" ]]; then
# Append comment to existing issue
COMMENT_BODY=$(build_comment)
COMMENT_JSON=$(printf '%s' "$COMMENT_BODY" | python3 -c "
import sys, json
print(json.dumps({'body': sys.stdin.read()}))" 2>/dev/null)
HTTP=$(curl -sf -o /dev/null -w '%{http_code}' -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/issues/${EXISTING}/comments" \
-d "${COMMENT_JSON}" 2>/dev/null || echo "000")
if [[ "$HTTP" == "201" ]]; then
echo "Commented on existing issue #${EXISTING}"
else
echo "WARNING: Failed to comment on issue #${EXISTING} (HTTP ${HTTP})"
fi
else
# Create new issue
ISSUE_BODY=$(build_body)
ISSUE_JSON=$(python3 -c "
import sys, json
body = sys.stdin.read()
print(json.dumps({
'title': sys.argv[1],
'body': body,
'labels': []
}))" "$TITLE" <<< "$ISSUE_BODY" 2>/dev/null)
# Create the issue
RESPONSE=$(curl -sf -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/issues" \
-d "${ISSUE_JSON}" 2>/dev/null || echo "{}")
ISSUE_NUM=$(echo "$RESPONSE" | grep -oP '"number":\s*\K[0-9]+' | head -1)
if [[ -n "$ISSUE_NUM" ]]; then
# Apply label (separate call — more reliable across Gitea versions)
LABEL_ID=$(curl -sf \
-H "Authorization: token ${GITEA_TOKEN}" \
"${API}/labels" 2>/dev/null \
| grep -oP "\"id\":\s*\K[0-9]+(?=[^}]*\"name\":\s*\"${LABEL_NAME}\")" \
| head -1 || true)
if [[ -n "$LABEL_ID" ]]; then
curl -sf -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/issues/${ISSUE_NUM}/labels" \
-d "{\"labels\":[${LABEL_ID}]}" \
> /dev/null 2>&1 || true
fi
echo "Created issue #${ISSUE_NUM}: ${TITLE}"
else
echo "WARNING: Failed to create issue"
echo "Response: ${RESPONSE}"
fi
fi
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="component" method="upgrade">
<name>com_mokosuitecross</name>
<version>01.02.00-rc</version>
<version>01.04.01</version>
<creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="content" method="upgrade">
<name>Content - MokoSuiteCross</name>
<version>01.02.00-rc</version>
<version>01.04.01</version>
<creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="mokosuitecross" method="upgrade">
<name>MokoSuiteCross - ActivityPub (Fediverse)</name>
<version>01.02.00-rc</version>
<version>01.04.01</version>
<creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="mokosuitecross" method="upgrade">
<name>MokoSuiteCross - Google Blogger</name>
<version>01.02.00-rc</version>
<version>01.04.01</version>
<creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="mokosuitecross" method="upgrade">
<name>MokoSuiteCross - Bluesky</name>
<version>01.02.00-rc</version>
<version>01.04.01</version>
<creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="mokosuitecross" method="upgrade">
<name>MokoSuiteCross - Brevo (Sendinblue)</name>
<version>01.02.00-rc</version>
<version>01.04.01</version>
<creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="mokosuitecross" method="upgrade">
<name>MokoSuiteCross - Constant Contact</name>
<version>01.02.00-rc</version>
<version>01.04.01</version>
<creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="mokosuitecross" method="upgrade">
<name>MokoSuiteCross - ConvertKit</name>
<version>01.02.00-rc</version>
<version>01.04.01</version>
<creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="mokosuitecross" method="upgrade">
<name>MokoSuiteCross - Dev.to</name>
<version>01.02.00-rc</version>
<version>01.04.01</version>
<creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="mokosuitecross" method="upgrade">
<name>MokoSuiteCross - Discord</name>
<version>01.02.00-rc</version>
<version>01.04.01</version>
<creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="mokosuitecross" method="upgrade">
<name>MokoSuiteCross - Facebook / Meta</name>
<version>01.02.00-rc</version>
<version>01.04.01</version>
<creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="mokosuitecross" method="upgrade">
<name>MokoSuiteCross - Ghost</name>
<version>01.02.00-rc</version>
<version>01.04.01</version>
<creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="mokosuitecross" method="upgrade">
<name>MokoSuiteCross - Google Business Profile</name>
<version>01.02.00-rc</version>
<version>01.04.01</version>
<creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="mokosuitecross" method="upgrade">
<name>MokoSuiteCross - Google Chat</name>
<version>01.02.00-rc</version>
<version>01.04.01</version>
<creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="mokosuitecross" method="upgrade">
<name>MokoSuiteCross - Hashnode</name>
<version>01.02.00-rc</version>
<version>01.04.01</version>
<creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="mokosuitecross" method="upgrade">
<name>MokoSuiteCross - LinkedIn</name>
<version>01.02.00-rc</version>
<version>01.04.01</version>
<creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="mokosuitecross" method="upgrade">
<name>MokoSuiteCross - Mailchimp</name>
<version>01.02.00-rc</version>
<version>01.04.01</version>
<creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="mokosuitecross" method="upgrade">
<name>MokoSuiteCross - Mastodon</name>
<version>01.02.00-rc</version>
<version>01.04.01</version>
<creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="mokosuitecross" method="upgrade">
<name>MokoSuiteCross - Matrix / Element</name>
<version>01.02.00-rc</version>
<version>01.04.01</version>
<creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="mokosuitecross" method="upgrade">
<name>MokoSuiteCross - Medium</name>
<version>01.02.00-rc</version>
<version>01.04.01</version>
<creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="mokosuitecross" method="upgrade">
<name>MokoSuiteCross - MokoSuiteCalendar Events</name>
<version>01.02.00-rc</version>
<version>01.04.01</version>
<creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="mokosuitecross" method="upgrade">
<name>MokoSuiteCross - MokoSuiteGallery</name>
<version>01.02.00-rc</version>
<version>01.04.01</version>
<creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="mokosuitecross" method="upgrade">
<name>MokoSuiteCross - Nostr</name>
<version>01.02.00-rc</version>
<version>01.04.01</version>
<creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="mokosuitecross" method="upgrade">
<name>MokoSuiteCross - Ntfy Push Notifications</name>
<version>01.02.00-rc</version>
<version>01.04.01</version>
<creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="mokosuitecross" method="upgrade">
<name>MokoSuiteCross - Pinterest</name>
<version>01.02.00-rc</version>
<version>01.04.01</version>
<creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="mokosuitecross" method="upgrade">
<name>MokoSuiteCross - Reddit</name>
<version>01.02.00-rc</version>
<version>01.04.01</version>
<creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="mokosuitecross" method="upgrade">
<name>MokoSuiteCross - RSS Feed</name>
<version>01.02.00-rc</version>
<version>01.04.01</version>
<creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="mokosuitecross" method="upgrade">
<name>MokoSuiteCross - SendGrid</name>
<version>01.02.00-rc</version>
<version>01.04.01</version>
<creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="mokosuitecross" method="upgrade">
<name>MokoSuiteCross - Slack</name>
<version>01.02.00-rc</version>
<version>01.04.01</version>
<creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="mokosuitecross" method="upgrade">
<name>MokoSuiteCross - Microsoft Teams</name>
<version>01.02.00-rc</version>
<version>01.04.01</version>
<creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="mokosuitecross" method="upgrade">
<name>MokoSuiteCross - Telegram</name>
<version>01.02.00-rc</version>
<version>01.04.01</version>
<creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="mokosuitecross" method="upgrade">
<name>MokoSuiteCross - Threads (Meta)</name>
<version>01.02.00-rc</version>
<version>01.04.01</version>
<creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="mokosuitecross" method="upgrade">
<name>MokoSuiteCross - TikTok</name>
<version>01.02.00-rc</version>
<version>01.04.01</version>
<creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="mokosuitecross" method="upgrade">
<name>MokoSuiteCross - Tumblr</name>
<version>01.02.00-rc</version>
<version>01.04.01</version>
<creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="mokosuitecross" method="upgrade">
<name>MokoSuiteCross - X / Twitter</name>
<version>01.02.00-rc</version>
<version>01.04.01</version>
<creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="mokosuitecross" method="upgrade">
<name>MokoSuiteCross - Generic Webhook</name>
<version>01.02.00-rc</version>
<version>01.04.01</version>
<creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="mokosuitecross" method="upgrade">
<name>MokoSuiteCross - WhatsApp Business</name>
<version>01.02.00-rc</version>
<version>01.04.01</version>
<creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="mokosuitecross" method="upgrade">
<name>MokoSuiteCross - WordPress</name>
<version>01.02.00-rc</version>
<version>01.04.01</version>
<creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="system" method="upgrade">
<name>System - MokoSuiteCross</name>
<version>01.02.00-rc</version>
<version>01.04.01</version>
<creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="system" method="upgrade">
<name>System - MokoSuiteCross Events</name>
<version>01.02.00-rc</version>
<version>01.04.01</version>
<creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="system" method="upgrade">
<name>System - MokoSuiteCross Gallery</name>
<version>01.02.00-rc</version>
<version>01.04.01</version>
<creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="task" method="upgrade">
<name>Task - MokoSuiteCross Queue Processor</name>
<version>01.02.00-rc</version>
<version>01.04.01</version>
<creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="webservices" method="upgrade">
<name>Web Services - MokoSuiteCross</name>
<version>01.02.00-rc</version>
<version>01.04.01</version>
<creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
+5 -1
View File
@@ -2,7 +2,7 @@
<extension type="package" method="upgrade">
<name>MokoSuiteCross</name>
<packagename>mokosuitecross</packagename>
<version>01.02.00-rc</version>
<version>01.04.01</version>
<creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -60,6 +60,10 @@
<file type="plugin" id="tiktok" group="mokosuitecross">plg_mokosuitecross_tiktok.zip</file>
<file type="plugin" id="mokosuitecalendar" group="mokosuitecross">plg_mokosuitecross_mokosuitecalendar.zip</file>
<file type="plugin" id="mokosuitegallery" group="mokosuitecross">plg_mokosuitecross_mokosuitegallery.zip</file>
<!-- Content Source Plugins (system group) -->
<file type="plugin" id="mokosuitecross_events" group="system">plg_system_mokosuitecross_events.zip</file>
<file type="plugin" id="mokosuitecross_gallery" group="system">plg_system_mokosuitecross_gallery.zip</file>
</files>
<languages>
-1
View File
@@ -1 +0,0 @@
<\!DOCTYPE html><title></title>
-1
View File
@@ -1 +0,0 @@
<\!DOCTYPE html><title></title>
-1
View File
@@ -1 +0,0 @@
<\!DOCTYPE html><title></title>
-54
View File
@@ -1,54 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<config>
<fieldset name="component" label="COM_MOKOJOOMCROSS_CONFIG_COMPONENT">
<field
name="auto_post_on_publish"
type="radio"
label="COM_MOKOJOOMCROSS_CONFIG_AUTO_POST"
description="COM_MOKOJOOMCROSS_CONFIG_AUTO_POST_DESC"
default="1"
class="btn-group">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field
name="retry_max"
type="number"
label="COM_MOKOJOOMCROSS_CONFIG_RETRY_MAX"
description="COM_MOKOJOOMCROSS_CONFIG_RETRY_MAX_DESC"
default="3"
min="0"
max="10"
/>
<field
name="retry_delay"
type="number"
label="COM_MOKOJOOMCROSS_CONFIG_RETRY_DELAY"
description="COM_MOKOJOOMCROSS_CONFIG_RETRY_DELAY_DESC"
default="300"
min="60"
max="3600"
/>
<field
name="log_retention_days"
type="number"
label="COM_MOKOJOOMCROSS_CONFIG_LOG_RETENTION"
description="COM_MOKOJOOMCROSS_CONFIG_LOG_RETENTION_DESC"
default="90"
min="7"
max="365"
/>
<field
name="default_template"
type="textarea"
label="COM_MOKOJOOMCROSS_CONFIG_DEFAULT_TEMPLATE"
description="COM_MOKOJOOMCROSS_CONFIG_DEFAULT_TEMPLATE_DESC"
default="{title}\n\n{introtext}\n\n{url}"
rows="4"
/>
</fieldset>
</config>
@@ -1,60 +0,0 @@
; MokoJoomCross — Admin Backend Language File
; Copyright (C) 2026 Moko Consulting. All rights reserved.
; License: GPL-3.0-or-later
COM_MOKOJOOMCROSS="MokoJoomCross"
COM_MOKOJOOMCROSS_DESCRIPTION="Cross-posting Joomla content to social media, email marketing, and chat platforms"
; Submenu
COM_MOKOJOOMCROSS_SUBMENU_DASHBOARD="Dashboard"
COM_MOKOJOOMCROSS_SUBMENU_POSTS="Post Queue"
COM_MOKOJOOMCROSS_SUBMENU_SERVICES="Services"
COM_MOKOJOOMCROSS_SUBMENU_LOGS="Activity Logs"
; Dashboard
COM_MOKOJOOMCROSS_DASHBOARD_ACTIVE_SERVICES="Active Services"
COM_MOKOJOOMCROSS_DASHBOARD_QUEUED="Queued"
COM_MOKOJOOMCROSS_DASHBOARD_POSTED="Posted"
COM_MOKOJOOMCROSS_DASHBOARD_FAILED="Failed"
COM_MOKOJOOMCROSS_DASHBOARD_QUICK_LINKS="Quick Links"
; Migration
COM_MOKOJOOMCROSS_MIGRATION_TITLE="Migrate from Perfect Publisher Pro"
COM_MOKOJOOMCROSS_MIGRATION_DESCRIPTION="We detected Perfect Publisher Pro settings. Import your service configurations to MokoJoomCross."
COM_MOKOJOOMCROSS_MIGRATION_BUTTON="Start Migration"
COM_MOKOJOOMCROSS_MIGRATION_SUCCESS="Migration complete: %d service(s) imported, %d skipped."
COM_MOKOJOOMCROSS_MIGRATION_ERROR="Migration encountered errors: %s"
; Services
COM_MOKOJOOMCROSS_FIELD_SERVICE_TYPE="Service Type"
COM_MOKOJOOMCROSS_SELECT_SERVICE_TYPE="- Select Service Type -"
COM_MOKOJOOMCROSS_FIELDSET_CREDENTIALS="API Credentials"
COM_MOKOJOOMCROSS_FIELD_CREDENTIALS="Credentials (JSON)"
COM_MOKOJOOMCROSS_FIELD_CREDENTIALS_DESC="JSON object with API keys and tokens for this service. Keys vary by platform."
; Posts
COM_MOKOJOOMCROSS_FILTER_SEARCH="Search"
COM_MOKOJOOMCROSS_FILTER_STATUS="Status"
COM_MOKOJOOMCROSS_SELECT_STATUS="- Select Status -"
COM_MOKOJOOMCROSS_FILTER_SERVICE_TYPE="Service Type"
COM_MOKOJOOMCROSS_CREATED_ASC="Created ascending"
COM_MOKOJOOMCROSS_CREATED_DESC="Created descending"
COM_MOKOJOOMCROSS_STATUS_ASC="Status ascending"
COM_MOKOJOOMCROSS_STATUS_DESC="Status descending"
; Actions
COM_MOKOJOOMCROSS_ACTION_CROSSPOST="Cross-post"
COM_MOKOJOOMCROSS_ACTION_MIGRATE="Migrate"
; Configuration
COM_MOKOJOOMCROSS_CONFIG_COMPONENT="MokoJoomCross Settings"
COM_MOKOJOOMCROSS_CONFIG_AUTO_POST="Auto-post on Publish"
COM_MOKOJOOMCROSS_CONFIG_AUTO_POST_DESC="Automatically cross-post articles when they are published"
COM_MOKOJOOMCROSS_CONFIG_RETRY_MAX="Max Retries"
COM_MOKOJOOMCROSS_CONFIG_RETRY_MAX_DESC="Maximum number of retry attempts for failed posts"
COM_MOKOJOOMCROSS_CONFIG_RETRY_DELAY="Retry Delay (seconds)"
COM_MOKOJOOMCROSS_CONFIG_RETRY_DELAY_DESC="Seconds to wait before retrying a failed post"
COM_MOKOJOOMCROSS_CONFIG_LOG_RETENTION="Log Retention (days)"
COM_MOKOJOOMCROSS_CONFIG_LOG_RETENTION_DESC="Number of days to keep activity logs"
COM_MOKOJOOMCROSS_CONFIG_DEFAULT_TEMPLATE="Default Message Template"
COM_MOKOJOOMCROSS_CONFIG_DEFAULT_TEMPLATE_DESC="Default template for cross-posts. Placeholders: {title}, {url}, {introtext}, {image}, {category}, {author}"
@@ -1,149 +0,0 @@
<?php
/**
* @package MokoJoomCross
* @subpackage com_mokojoomcross
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Joomla\Component\MokoJoomCross\Administrator\Helper;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
/**
* Migration helper for importing settings from Perfect Publisher Pro.
*
* Reads Perfect Publisher Pro's component params and plugin configurations
* and maps them to MokoJoomCross service records.
*/
class MigrationHelper
{
/**
* Service type mapping from Perfect Publisher Pro to MokoJoomCross.
*
* @var array
*/
private const SERVICE_MAP = [
'facebook' => 'facebook',
'twitter' => 'twitter',
'linkedin' => 'linkedin',
'telegram' => 'telegram',
];
/**
* Run the full migration from Perfect Publisher Pro.
*
* @return array ['migrated' => int, 'skipped' => int, 'errors' => string[]]
*/
public static function migrate(): array
{
$db = Factory::getDbo();
$result = ['migrated' => 0, 'skipped' => 0, 'errors' => []];
// Read Perfect Publisher Pro component params
$query = $db->getQuery(true)
->select($db->quoteName('params'))
->from($db->quoteName('#__extensions'))
->where($db->quoteName('element') . ' LIKE ' . $db->quote('%perfectpublisher%'))
->where($db->quoteName('type') . ' = ' . $db->quote('component'));
$db->setQuery($query);
$rawParams = $db->loadResult();
if (!$rawParams) {
$result['errors'][] = 'Perfect Publisher Pro not found or has no configuration.';
return $result;
}
$params = json_decode($rawParams, true);
if (!is_array($params)) {
$result['errors'][] = 'Could not parse Perfect Publisher Pro configuration.';
return $result;
}
// Iterate known service mappings and create MokoJoomCross service records
foreach (self::SERVICE_MAP as $ppKey => $mjcType) {
$credentials = self::extractCredentials($params, $ppKey);
if (empty($credentials)) {
$result['skipped']++;
continue;
}
// Check if service already exists
$query = $db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__mokojoomcross_services'))
->where($db->quoteName('service_type') . ' = ' . $db->quote($mjcType));
$db->setQuery($query);
if ((int) $db->loadResult() > 0) {
$result['skipped']++;
continue;
}
// Insert new service record
$service = (object) [
'title' => ucfirst($mjcType) . ' (migrated from PP Pro)',
'alias' => $mjcType . '-migrated',
'service_type' => $mjcType,
'credentials' => json_encode($credentials),
'params' => '{}',
'published' => 0, // Disabled until user verifies
'ordering' => 0,
'created' => Factory::getDate()->toSql(),
'modified' => Factory::getDate()->toSql(),
'created_by' => Factory::getApplication()->getIdentity()->id ?? 0,
];
$db->insertObject('#__mokojoomcross_services', $service);
$result['migrated']++;
}
// Clear migration flag
$query = $db->getQuery(true)
->update($db->quoteName('#__extensions'))
->set($db->quoteName('params') . ' = ' . $db->quote('{}'))
->where($db->quoteName('type') . ' = ' . $db->quote('component'))
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokojoomcross'));
$db->setQuery($query);
$db->execute();
return $result;
}
/**
* Extract credentials for a specific service from PP Pro params.
*
* @param array $params PP Pro component params
* @param string $serviceKey Service key in PP Pro params
*
* @return array Credential key/value pairs (empty if none found)
*/
private static function extractCredentials(array $params, string $serviceKey): array
{
$credentials = [];
// PP Pro uses various key patterns: {service}_app_id, {service}_api_key, etc.
$prefixes = [$serviceKey . '_', $serviceKey . 'api_', $serviceKey . '-'];
foreach ($params as $key => $value) {
foreach ($prefixes as $prefix) {
if (str_starts_with($key, $prefix) && !empty($value)) {
$cleanKey = str_replace($prefix, '', $key);
$credentials[$cleanKey] = $value;
}
}
}
return $credentials;
}
}
@@ -1,73 +0,0 @@
<?php
/**
* @package MokoJoomCross
* @subpackage com_mokojoomcross
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Joomla\Component\MokoJoomCross\Administrator\Model;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
class DashboardModel extends BaseDatabaseModel
{
/**
* Get summary statistics for the dashboard.
*
* @return object Stats object with counts
*/
public function getStats(): object
{
$db = $this->getDatabase();
$stats = new \stdClass();
// Active services count
$query = $db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__mokojoomcross_services'))
->where($db->quoteName('published') . ' = 1');
$db->setQuery($query);
$stats->active_services = (int) $db->loadResult();
// Posts by status
foreach (['queued', 'posted', 'failed'] as $status) {
$query = $db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__mokojoomcross_posts'))
->where($db->quoteName('status') . ' = ' . $db->quote($status));
$db->setQuery($query);
$stats->{$status . '_count'} = (int) $db->loadResult();
}
return $stats;
}
/**
* Check if Perfect Publisher Pro migration is available.
*
* @return bool
*/
public function isMigrationAvailable(): bool
{
$db = $this->getDatabase();
$query = $db->getQuery(true)
->select($db->quoteName('params'))
->from($db->quoteName('#__extensions'))
->where($db->quoteName('type') . ' = ' . $db->quote('component'))
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokojoomcross'));
$db->setQuery($query);
$params = json_decode($db->loadResult() ?: '{}', true);
return !empty($params['migration_available']);
}
}
@@ -1,39 +0,0 @@
<?php
/**
* @package MokoJoomCross
* @subpackage com_mokojoomcross
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Joomla\Component\MokoJoomCross\Administrator\View\Dashboard;
defined('_JEXEC') or die;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Toolbar\ToolbarHelper;
class HtmlView extends BaseHtmlView
{
protected $stats;
protected $migrationAvailable;
public function display($tpl = null): void
{
$this->stats = $this->get('Stats');
$this->migrationAvailable = $this->get('MigrationAvailable');
$this->addToolbar();
parent::display($tpl);
}
protected function addToolbar(): void
{
ToolbarHelper::title('MokoJoomCross — Dashboard', 'share-alt');
ToolbarHelper::preferences('com_mokojoomcross');
}
}
@@ -1,41 +0,0 @@
<?php
/**
* @package MokoJoomCross
* @subpackage com_mokojoomcross
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Joomla\Component\MokoJoomCross\Administrator\View\Posts;
defined('_JEXEC') or die;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Toolbar\ToolbarHelper;
class HtmlView extends BaseHtmlView
{
protected $items;
protected $pagination;
protected $state;
public function display($tpl = null): void
{
$this->items = $this->get('Items');
$this->pagination = $this->get('Pagination');
$this->state = $this->get('State');
$this->addToolbar();
parent::display($tpl);
}
protected function addToolbar(): void
{
ToolbarHelper::title('MokoJoomCross — Post Queue', 'share-alt');
ToolbarHelper::deleteList('', 'posts.delete', 'JTOOLBAR_DELETE');
}
}
@@ -1,90 +0,0 @@
<?php
/**
* @package MokoJoomCross
* @subpackage com_mokojoomcross
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
* SPDX-License-Identifier: GPL-3.0-or-later
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
/** @var \Joomla\Component\MokoJoomCross\Administrator\View\Dashboard\HtmlView $this */
$stats = $this->stats;
?>
<div class="row">
<div class="col-lg-9">
<div class="row">
<div class="col-sm-6 col-md-3">
<div class="card text-center mb-3">
<div class="card-body">
<h5 class="card-title"><?php echo Text::_('COM_MOKOJOOMCROSS_DASHBOARD_ACTIVE_SERVICES'); ?></h5>
<p class="display-4"><?php echo $stats->active_services; ?></p>
</div>
</div>
</div>
<div class="col-sm-6 col-md-3">
<div class="card text-center mb-3">
<div class="card-body">
<h5 class="card-title"><?php echo Text::_('COM_MOKOJOOMCROSS_DASHBOARD_QUEUED'); ?></h5>
<p class="display-4 text-warning"><?php echo $stats->queued_count; ?></p>
</div>
</div>
</div>
<div class="col-sm-6 col-md-3">
<div class="card text-center mb-3">
<div class="card-body">
<h5 class="card-title"><?php echo Text::_('COM_MOKOJOOMCROSS_DASHBOARD_POSTED'); ?></h5>
<p class="display-4 text-success"><?php echo $stats->posted_count; ?></p>
</div>
</div>
</div>
<div class="col-sm-6 col-md-3">
<div class="card text-center mb-3">
<div class="card-body">
<h5 class="card-title"><?php echo Text::_('COM_MOKOJOOMCROSS_DASHBOARD_FAILED'); ?></h5>
<p class="display-4 text-danger"><?php echo $stats->failed_count; ?></p>
</div>
</div>
</div>
</div>
<?php if ($this->migrationAvailable) : ?>
<div class="alert alert-info">
<h4 class="alert-heading"><?php echo Text::_('COM_MOKOJOOMCROSS_MIGRATION_TITLE'); ?></h4>
<p><?php echo Text::_('COM_MOKOJOOMCROSS_MIGRATION_DESCRIPTION'); ?></p>
<a href="<?php echo Route::_('index.php?option=com_mokojoomcross&task=dashboard.migrate'); ?>"
class="btn btn-primary">
<?php echo Text::_('COM_MOKOJOOMCROSS_MIGRATION_BUTTON'); ?>
</a>
</div>
<?php endif; ?>
</div>
<div class="col-lg-3">
<div class="card">
<div class="card-body">
<h5 class="card-title"><?php echo Text::_('COM_MOKOJOOMCROSS_DASHBOARD_QUICK_LINKS'); ?></h5>
<div class="list-group list-group-flush">
<a href="<?php echo Route::_('index.php?option=com_mokojoomcross&view=services'); ?>"
class="list-group-item list-group-item-action">
<?php echo Text::_('COM_MOKOJOOMCROSS_SUBMENU_SERVICES'); ?>
</a>
<a href="<?php echo Route::_('index.php?option=com_mokojoomcross&view=posts'); ?>"
class="list-group-item list-group-item-action">
<?php echo Text::_('COM_MOKOJOOMCROSS_SUBMENU_POSTS'); ?>
</a>
<a href="<?php echo Route::_('index.php?option=com_mokojoomcross&view=logs'); ?>"
class="list-group-item list-group-item-action">
<?php echo Text::_('COM_MOKOJOOMCROSS_SUBMENU_LOGS'); ?>
</a>
</div>
</div>
</div>
</div>
</div>
@@ -1 +0,0 @@
<\!DOCTYPE html><title></title>
@@ -1,2 +0,0 @@
PLG_CONTENT_MOKOJOOMCROSS="Content - MokoJoomCross"
PLG_CONTENT_MOKOJOOMCROSS_DESCRIPTION="Adds cross-post status badges to articles in the admin backend."
@@ -1,85 +0,0 @@
<?php
/**
* @package MokoJoomCross
* @subpackage plg_content_mokojoomcross
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Joomla\Plugin\Content\MokoJoomCross\Extension;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Event\SubscriberInterface;
/**
* Content plugin that adds cross-post status badges to article views.
*/
class MokoJoomCrossContent extends CMSPlugin implements SubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
'onContentBeforeDisplay' => 'onContentBeforeDisplay',
];
}
/**
* Add cross-post status indicator before article content in admin.
*
* @param string $context The context
* @param object $article The article
* @param object $params The article params
* @param int $page The page number
*
* @return string HTML to prepend to the article
*/
public function onContentBeforeDisplay(string $context, &$article, &$params, int $page = 0): string
{
if ($context !== 'com_content.article') {
return '';
}
$app = $this->getApplication();
if (!$app->isClient('administrator')) {
return '';
}
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select('p.status, s.service_type')
->from($db->quoteName('#__mokojoomcross_posts', 'p'))
->join('LEFT', $db->quoteName('#__mokojoomcross_services', 's')
. ' ON ' . $db->quoteName('s.id') . ' = ' . $db->quoteName('p.service_id'))
->where($db->quoteName('p.article_id') . ' = ' . (int) $article->id)
->order($db->quoteName('p.created') . ' DESC');
$db->setQuery($query, 0, 10);
$posts = $db->loadObjectList();
if (empty($posts)) {
return '';
}
$badges = '';
foreach ($posts as $post) {
$class = match ($post->status) {
'posted' => 'badge bg-success',
'failed' => 'badge bg-danger',
'queued' => 'badge bg-warning',
default => 'badge bg-secondary',
};
$badges .= '<span class="' . $class . ' me-1">' . htmlspecialchars($post->service_type) . '</span>';
}
return '<div class="mokojoomcross-status mb-2">' . $badges . '</div>';
}
}
@@ -1,2 +0,0 @@
PLG_MOKOJOOMCROSS_BLUESKY="MokoJoomCross - Bluesky"
PLG_MOKOJOOMCROSS_BLUESKY_DESCRIPTION="Cross-post Joomla articles to Bluesky."
@@ -1,2 +0,0 @@
PLG_MOKOJOOMCROSS_DISCORD="MokoJoomCross - Discord"
PLG_MOKOJOOMCROSS_DISCORD_DESCRIPTION="Cross-post Joomla articles to Discord."
@@ -1,2 +0,0 @@
PLG_MOKOJOOMCROSS_FACEBOOK="MokoJoomCross - Facebook / Meta"
PLG_MOKOJOOMCROSS_FACEBOOK_DESCRIPTION="Cross-post Joomla articles to Facebook / Meta."
@@ -1,2 +0,0 @@
PLG_MOKOJOOMCROSS_LINKEDIN="MokoJoomCross - LinkedIn"
PLG_MOKOJOOMCROSS_LINKEDIN_DESCRIPTION="Cross-post Joomla articles to LinkedIn."
@@ -1,2 +0,0 @@
PLG_MOKOJOOMCROSS_MAILCHIMP="MokoJoomCross - Mailchimp"
PLG_MOKOJOOMCROSS_MAILCHIMP_DESCRIPTION="Cross-post Joomla articles to Mailchimp."
@@ -1,2 +0,0 @@
PLG_MOKOJOOMCROSS_MASTODON="MokoJoomCross - Mastodon"
PLG_MOKOJOOMCROSS_MASTODON_DESCRIPTION="Cross-post Joomla articles to Mastodon."
@@ -1,2 +0,0 @@
PLG_MOKOJOOMCROSS_SLACK="MokoJoomCross - Slack"
PLG_MOKOJOOMCROSS_SLACK_DESCRIPTION="Cross-post Joomla articles to Slack."
@@ -1,2 +0,0 @@
PLG_MOKOJOOMCROSS_TELEGRAM="MokoJoomCross - Telegram"
PLG_MOKOJOOMCROSS_TELEGRAM_DESCRIPTION="Cross-post Joomla articles to Telegram."
@@ -1,122 +0,0 @@
<?php
/**
* @package MokoJoomCross
* @subpackage plg_mokojoomcross_twitter
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Joomla\Plugin\MokoJoomCross\Twitter\Extension;
defined('_JEXEC') or die;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Component\MokoJoomCross\Administrator\Service\MokoJoomCrossServiceInterface;
use Joomla\Event\SubscriberInterface;
/**
* X/Twitter service plugin for MokoJoomCross.
*
* Uses Twitter API v2 with OAuth 2.0 Bearer Token.
*
* Credentials format:
* {
* "bearer_token": "...",
* "api_key": "...",
* "api_secret": "...",
* "access_token": "...",
* "access_token_secret": "..."
* }
*/
class TwitterService extends CMSPlugin implements SubscriberInterface, MokoJoomCrossServiceInterface
{
public static function getSubscribedEvents(): array
{
return [
'onMokoJoomCrossGetServices' => 'onMokoJoomCrossGetServices',
];
}
public function onMokoJoomCrossGetServices(&$services): void
{
$services[] = $this;
}
public function getServiceType(): string
{
return 'twitter';
}
public function getServiceName(): string
{
return 'X / Twitter';
}
public function publish(string $message, array $media, array $credentials, array $params): array
{
// Twitter API v2 tweet creation
$apiUrl = 'https://api.twitter.com/2/tweets';
$postData = json_encode(['text' => mb_substr($message, 0, 280)]);
$ch = curl_init($apiUrl);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $postData,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . ($credentials['bearer_token'] ?? ''),
],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$data = json_decode($response, true) ?: [];
if ($httpCode === 201 && !empty($data['data']['id'])) {
return ['success' => true, 'platform_post_id' => $data['data']['id'], 'response' => $data];
}
return ['success' => false, 'platform_post_id' => '', 'response' => $data];
}
public function validateCredentials(array $credentials): array
{
$apiUrl = 'https://api.twitter.com/2/users/me';
$ch = curl_init($apiUrl);
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . ($credentials['bearer_token'] ?? '')],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true) ?: [];
if (!empty($data['data']['username'])) {
return ['valid' => true, 'message' => 'Connected', 'account_name' => '@' . $data['data']['username']];
}
return ['valid' => false, 'message' => $data['detail'] ?? 'Failed', 'account_name' => ''];
}
public function getMaxLength(): int
{
return 280;
}
public function supportsMedia(): bool
{
return true;
}
}
@@ -1,166 +0,0 @@
<?php
/**
* @package MokoJoomCross
* @subpackage plg_system_mokojoomcross
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Joomla\Plugin\System\MokoJoomCross\Extension;
defined('_JEXEC') or die;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\Component\MokoJoomCross\Administrator\Service\MokoJoomCrossServiceInterface;
use Joomla\Event\SubscriberInterface;
/**
* System plugin that triggers cross-posting when Joomla articles are published.
*
* Listens for onContentAfterSave events on com_content articles. When an article
* transitions to published state, it dispatches the post to all enabled service
* plugins in the `mokojoomcross` plugin group.
*/
class MokoJoomCross extends CMSPlugin implements SubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
'onContentAfterSave' => 'onContentAfterSave',
];
}
/**
* Triggered after a content item is saved.
*
* @param string $context The context (e.g. 'com_content.article')
* @param object $article The article object
* @param bool $isNew Whether this is a new article
*
* @return void
*/
public function onContentAfterSave(string $context, $article, bool $isNew): void
{
// Only process Joomla articles
if ($context !== 'com_content.article') {
return;
}
// Only cross-post when article is published
if ((int) ($article->state ?? 0) !== 1) {
return;
}
// Check global auto-post setting
$componentParams = ComponentHelper::getParams('com_mokojoomcross');
if (!$componentParams->get('auto_post_on_publish', 1)) {
return;
}
$this->dispatchCrossPost($article);
}
/**
* Dispatch article to all enabled service plugins.
*
* @param object $article The article object
*
* @return void
*/
private function dispatchCrossPost(object $article): void
{
$db = Factory::getDbo();
// Load all enabled services
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokojoomcross_services'))
->where($db->quoteName('published') . ' = 1')
->order($db->quoteName('ordering') . ' ASC');
$db->setQuery($query);
$services = $db->loadObjectList();
if (empty($services)) {
return;
}
// Import service plugins
PluginHelper::importPlugin('mokojoomcross');
foreach ($services as $service) {
// Queue the post
$post = (object) [
'article_id' => $article->id,
'service_id' => $service->id,
'status' => 'queued',
'message' => $this->renderTemplate($article, $service),
'created' => Factory::getDate()->toSql(),
'modified' => Factory::getDate()->toSql(),
];
$db->insertObject('#__mokojoomcross_posts', $post);
// Log the queue action
$log = (object) [
'post_id' => $db->insertid(),
'service_id' => $service->id,
'level' => 'info',
'message' => sprintf('Article "%s" queued for %s', $article->title, $service->service_type),
'context' => json_encode(['article_id' => $article->id]),
'created' => Factory::getDate()->toSql(),
];
$db->insertObject('#__mokojoomcross_logs', $log);
}
}
/**
* Render the message template for a service.
*
* @param object $article The article
* @param object $service The service record
*
* @return string Rendered message
*/
private function renderTemplate(object $article, object $service): string
{
$db = Factory::getDbo();
// Try service-specific template first, fall back to default
$query = $db->getQuery(true)
->select($db->quoteName('template_body'))
->from($db->quoteName('#__mokojoomcross_templates'))
->where($db->quoteName('published') . ' = 1')
->where('(' . $db->quoteName('service_type') . ' = ' . $db->quote($service->service_type)
. ' OR ' . $db->quoteName('service_type') . ' = ' . $db->quote('default') . ')')
->order('CASE WHEN ' . $db->quoteName('service_type') . ' = '
. $db->quote($service->service_type) . ' THEN 0 ELSE 1 END')
->setLimit(1);
$db->setQuery($query);
$template = $db->loadResult() ?: '{title}\n\n{url}';
// Build article URL
$url = \Joomla\CMS\Uri\Uri::root() . 'index.php?option=com_content&view=article&id=' . $article->id;
// Replace placeholders
$replacements = [
'{title}' => $article->title ?? '',
'{introtext}' => strip_tags(mb_substr($article->introtext ?? '', 0, 280)),
'{url}' => $url,
'{image}' => json_decode($article->images ?? '{}')->image_intro ?? '',
'{category}' => '',
'{author}' => '',
];
return str_replace(array_keys($replacements), array_values($replacements), $template);
}
}
@@ -1,52 +0,0 @@
<?php
/**
* @package MokoJoomCross
* @subpackage plg_webservices_mokojoomcross
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Joomla\Plugin\WebServices\MokoJoomCross\Extension;
defined('_JEXEC') or die;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Router\ApiRouter;
use Joomla\Event\SubscriberInterface;
/**
* WebServices plugin providing REST API endpoints for MokoJoomCross.
*
* Endpoints:
* GET /api/index.php/v1/mokojoomcross/posts — List cross-posts
* GET /api/index.php/v1/mokojoomcross/posts/:id — Get single post
* GET /api/index.php/v1/mokojoomcross/services — List services
* POST /api/index.php/v1/mokojoomcross/crosspost — Trigger cross-post for an article
*/
class MokoJoomCrossWebServices extends CMSPlugin implements SubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
'onBeforeApiRoute' => 'onBeforeApiRoute',
];
}
public function onBeforeApiRoute(&$router): void
{
$router->createCRUDRoutes(
'v1/mokojoomcross/posts',
'posts',
['component' => 'com_mokojoomcross']
);
$router->createCRUDRoutes(
'v1/mokojoomcross/services',
'services',
['component' => 'com_mokojoomcross']
);
}
}
-44
View File
@@ -1,44 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="package" method="upgrade">
<name>MokoJoomCross</name>
<packagename>mokojoomcross</packagename>
<version>01.01.00</version>
<creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
<license>GPL-3.0-or-later</license>
<description>PKG_MOKOJOOMCROSS_DESCRIPTION</description>
<scriptfile>script.php</scriptfile>
<files folder="packages">
<!-- Core -->
<file type="component" id="com_mokojoomcross">com_mokojoomcross.zip</file>
<file type="plugin" id="mokojoomcross" group="system">plg_system_mokojoomcross.zip</file>
<file type="plugin" id="mokojoomcross" group="content">plg_content_mokojoomcross.zip</file>
<file type="plugin" id="mokojoomcross" group="webservices">plg_webservices_mokojoomcross.zip</file>
<!-- Service Plugins (mokojoomcross group) -->
<file type="plugin" id="facebook" group="mokojoomcross">plg_mokojoomcross_facebook.zip</file>
<file type="plugin" id="twitter" group="mokojoomcross">plg_mokojoomcross_twitter.zip</file>
<file type="plugin" id="linkedin" group="mokojoomcross">plg_mokojoomcross_linkedin.zip</file>
<file type="plugin" id="mastodon" group="mokojoomcross">plg_mokojoomcross_mastodon.zip</file>
<file type="plugin" id="bluesky" group="mokojoomcross">plg_mokojoomcross_bluesky.zip</file>
<file type="plugin" id="mailchimp" group="mokojoomcross">plg_mokojoomcross_mailchimp.zip</file>
<file type="plugin" id="telegram" group="mokojoomcross">plg_mokojoomcross_telegram.zip</file>
<file type="plugin" id="discord" group="mokojoomcross">plg_mokojoomcross_discord.zip</file>
<file type="plugin" id="slack" group="mokojoomcross">plg_mokojoomcross_slack.zip</file>
</files>
<languages>
<language tag="en-GB">language/en-GB/pkg_mokojoomcross.sys.ini</language>
</languages>
<updateservers>
<server type="extension" name="MokoJoomCross Updates">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomCross/updates.xml</server>
</updateservers>
<dlid prefix="dlid=" suffix=""/>
<blockChildUninstall>true</blockChildUninstall>
</extension>
-133
View File
@@ -1,133 +0,0 @@
<?php
/**
* @package MokoJoomCross
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
* SPDX-License-Identifier: GPL-3.0-or-later
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Installer\InstallerAdapter;
use Joomla\CMS\Language\Text;
class Pkg_MokoJoomCrossInstallerScript
{
/**
* Minimum PHP version required
*
* @var string
*/
protected $minimumPhp = '8.1.0';
/**
* Called before any install/update/uninstall action.
*
* @param string $type Action type (install, update, uninstall)
* @param InstallerAdapter $parent Installer adapter
*
* @return bool
*/
public function preflight(string $type, InstallerAdapter $parent): bool
{
if (version_compare(PHP_VERSION, $this->minimumPhp, '<')) {
Factory::getApplication()->enqueueMessage(
Text::sprintf('PKG_MOKOJOOMCROSS_PHP_VERSION_ERROR', $this->minimumPhp),
'error'
);
return false;
}
return true;
}
/**
* Called after install/update.
*
* @param string $type Action type
* @param InstallerAdapter $parent Installer adapter
*
* @return void
*/
public function postflight(string $type, InstallerAdapter $parent): void
{
$db = Factory::getDbo();
if ($type === 'install') {
// Enable core plugins automatically on fresh install
$corePlugins = [
['system', 'mokojoomcross'],
['content', 'mokojoomcross'],
['webservices', 'mokojoomcross'],
];
foreach ($corePlugins as [$folder, $element]) {
$query = $db->getQuery(true)
->update($db->quoteName('#__extensions'))
->set($db->quoteName('enabled') . ' = 1')
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where($db->quoteName('folder') . ' = ' . $db->quote($folder))
->where($db->quoteName('element') . ' = ' . $db->quote($element));
$db->setQuery($query);
$db->execute();
}
// Enable all service plugins in the mokojoomcross group
$query = $db->getQuery(true)
->update($db->quoteName('#__extensions'))
->set($db->quoteName('enabled') . ' = 1')
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where($db->quoteName('folder') . ' = ' . $db->quote('mokojoomcross'));
$db->setQuery($query);
$db->execute();
// Check for Perfect Publisher Pro and offer migration
$this->detectPerfectPublisherPro($db);
}
}
/**
* Detect Perfect Publisher Pro installation and store migration flag.
*
* @param \Joomla\Database\DatabaseInterface $db Database driver
*
* @return void
*/
private function detectPerfectPublisherPro($db): void
{
$query = $db->getQuery(true)
->select($db->quoteName(['element', 'params']))
->from($db->quoteName('#__extensions'))
->where($db->quoteName('element') . ' LIKE ' . $db->quote('%perfectpublisher%'))
->where($db->quoteName('type') . ' = ' . $db->quote('component'));
$db->setQuery($query);
$result = $db->loadObject();
if ($result) {
Factory::getApplication()->enqueueMessage(
Text::_('PKG_MOKOJOOMCROSS_MIGRATION_DETECTED'),
'notice'
);
// Store migration availability in component params
$query = $db->getQuery(true)
->update($db->quoteName('#__extensions'))
->set($db->quoteName('params') . ' = ' . $db->quote(json_encode([
'migration_available' => 'perfectpublisher',
'migration_source_params' => $result->params,
])))
->where($db->quoteName('type') . ' = ' . $db->quote('component'))
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokojoomcross'));
$db->setQuery($query);
$db->execute();
}
}
}