Compare commits

..

1 Commits

Author SHA1 Message Date
gitea-actions[bot] 97b78244f5 chore(version): auto-bump patch 01.08.42-dev [skip ci] 2026-06-28 16:12:21 +00:00
164 changed files with 1626 additions and 5219 deletions
+6 -6
View File
@@ -7,7 +7,7 @@ Cross-posting Joomla content to social media, email marketing, and chat platform
| Field | Value | | Field | Value |
|---|---| |---|---|
| **Package** | `pkg_mokosuitecross` | | **Package** | `pkg_mokosuitecross` |
| **Language** | PHP 8.3+ | | **Language** | PHP 8.1+ |
| **Branch** | develop on `dev`, merge to `main` (protected) | | **Branch** | develop on `dev`, merge to `main` (protected) |
| **Wiki** | [MokoSuiteCross Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteCross/wiki) | | **Wiki** | [MokoSuiteCross Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteCross/wiki) |
@@ -28,7 +28,7 @@ Joomla **package** with core extensions + pluggable service plugins:
### com_mokosuitecross (Component) ### com_mokosuitecross (Component)
- Admin backend: dashboard, services, post queue, templates, logs - Admin backend: dashboard, services, post queue, templates, logs
- Joomla 6 MVC: Dashboard, Services, Posts, Logs (list/edit each) - Joomla 5/6 MVC: Dashboard, Services, Posts, Logs (list/edit each)
- Namespace: `Joomla\Component\MokoSuiteCross\Administrator` - Namespace: `Joomla\Component\MokoSuiteCross\Administrator`
### plg_system_mokosuitecross (System Plugin) ### plg_system_mokosuitecross (System Plugin)
@@ -67,14 +67,14 @@ Each platform is a separate plugin implementing `MokoSuiteCrossServiceInterface`
- **Attribution**: `Authored-by: Moko Consulting` - **Attribution**: `Authored-by: Moko Consulting`
- **Workflow directory**: `.mokogitea/` (not `.gitea/` or `.github/`) - **Workflow directory**: `.mokogitea/` (not `.gitea/` or `.github/`)
- **Minification**: handled at build time (CI) - **Minification**: handled at build time (CI)
- **Wiki**: documentation lives in the MokoGitea wiki, not `docs/` files - **Wiki**: documentation lives in the Gitea wiki, not `docs/` files
- **Standards**: [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/.mokogitea/wiki) - **Standards**: [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/mokoplatform/wiki/Home)
- **UX**: service credentials as individual form fields, not JSON blobs; dashboard link in toolbar - **UX**: service credentials as individual form fields, not JSON blobs; dashboard link in toolbar
## Coding Standards ## Coding Standards
- PHP 8.3+ minimum - PHP 8.1+ minimum
- Joomla 6 DI container pattern: `services/provider.php` → Extension class - Joomla 5/6 DI container pattern: `services/provider.php` → Extension class
- Legacy stub `.php` file required for plugin loader but empty - Legacy stub `.php` file required for plugin loader but empty
- `SubscriberInterface` for event subscription (not `on*` method naming) - `SubscriberInterface` for event subscription (not `on*` method naming)
- `bind() → check() → store()` for Table operations (not `save()`) - `bind() → check() → store()` for Table operations (not `save()`)
+1 -1
View File
@@ -8,7 +8,7 @@ contact_links:
url: https://mokoconsulting.tech/ url: https://mokoconsulting.tech/
about: Get help or ask questions through our website about: Get help or ask questions through our website
- name: 📚 MokoStandards Documentation - name: 📚 MokoStandards Documentation
url: https://git.mokoconsulting.tech/MokoConsulting/.mokogitea/wiki url: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
about: View our coding standards and best practices about: View our coding standards and best practices
- name: 🔒 Report a Security Vulnerability - name: 🔒 Report a Security Vulnerability
url: https://git.mokoconsulting.tech/mokoconsulting-tech/.github-private/security/advisories/new url: https://git.mokoconsulting.tech/mokoconsulting-tech/.github-private/security/advisories/new
+1 -1
View File
@@ -37,7 +37,7 @@ If you have ideas about how this could be implemented, share them here:
Add any other context, mockups, or screenshots about the feature request here. Add any other context, mockups, or screenshots about the feature request here.
## Relevant Standards ## Relevant Standards
Does this relate to any standards in [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/.mokogitea/wiki)? Does this relate to any standards in [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards)?
- [ ] Accessibility (WCAG 2.1 AA) - [ ] Accessibility (WCAG 2.1 AA)
- [ ] Localization (en_US/en_GB) - [ ] Localization (en_US/en_GB)
- [ ] Security best practices - [ ] Security best practices
+1 -1
View File
@@ -35,7 +35,7 @@ Use this template only for:
<!-- Describe how this could be addressed --> <!-- Describe how this could be addressed -->
## Standards Reference ## Standards Reference
Does this relate to security standards in [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/.mokogitea/wiki)? Does this relate to security standards in [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards)?
- [ ] SPDX license identifiers - [ ] SPDX license identifiers
- [ ] Secret management - [ ] Secret management
- [ ] Dependency security - [ ] Dependency security
+5 -6
View File
@@ -3,9 +3,9 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: MokoGitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Release # INGROUP: mokocli.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic # REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /.mokogitea/workflows/auto-bump.yml # PATH: /.mokogitea/workflows/auto-bump.yml
# VERSION: 09.02.00 # VERSION: 09.02.00
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits) # BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
@@ -22,7 +22,7 @@ on:
env: env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
permissions: permissions:
contents: write contents: write
@@ -34,8 +34,7 @@ jobs:
if: >- if: >-
!contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[skip ci]') &&
!contains(github.event.head_commit.message, '[skip bump]') && !contains(github.event.head_commit.message, '[skip bump]') &&
!startsWith(github.event.head_commit.message, 'Merge pull request') && !startsWith(github.event.head_commit.message, 'Merge pull request')
!startsWith(github.event.repository.name, 'Template-')
steps: steps:
- name: Checkout - name: Checkout
@@ -53,7 +52,7 @@ jobs:
echo "MOKO_CLI=/opt/mokocli/cli" >> "$GITHUB_ENV" echo "MOKO_CLI=/opt/mokocli/cli" >> "$GITHUB_ENV"
else else
git clone --depth 1 --branch main --quiet \ git clone --depth 1 --branch main --quiet \
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/MokoCLI.git" \ "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/mokocli.git" \
/tmp/mokocli /tmp/mokocli
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
echo "MOKO_CLI=/tmp/mokocli/cli" >> "$GITHUB_ENV" echo "MOKO_CLI=/tmp/mokocli/cli" >> "$GITHUB_ENV"
+38 -69
View File
@@ -3,11 +3,11 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: MokoGitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Release # INGROUP: mokocli.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic # REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli
# PATH: /.mokogitea/workflows/auto-release.yml # PATH: /templates/workflows/universal/auto-release.yml.template
# VERSION: 05.01.00 # VERSION: 05.00.00
# BRIEF: Universal build & release detects platform from manifest.xml # BRIEF: Universal build & release detects platform from manifest.xml
# #
# +=======================================================================+ # +=======================================================================+
@@ -27,7 +27,7 @@ name: "Universal: Build & Release"
on: on:
pull_request: pull_request:
types: [opened, synchronize, closed] types: [opened, closed]
branches: branches:
- main - main
paths-ignore: paths-ignore:
@@ -52,7 +52,7 @@ on:
env: env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
@@ -64,14 +64,9 @@ jobs:
promote-rc: promote-rc:
name: Promote to RC name: Promote to RC
runs-on: release runs-on: release
# Skip on template repos (Template-*) — they scaffold other repos and do not release.
if: >- if: >-
!startsWith(github.event.repository.name, 'Template-') &&
(
(github.event.action == 'opened' && github.event.pull_request.merged != true) || (github.event.action == 'opened' && github.event.pull_request.merged != true) ||
(github.event.action == 'synchronize' && github.event.pull_request.merged != true) ||
(github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc') (github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
)
steps: steps:
- name: Checkout repository - name: Checkout repository
@@ -79,7 +74,6 @@ jobs:
with: with:
token: ${{ secrets.MOKOGITEA_TOKEN }} token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 1 fetch-depth: 1
submodules: recursive
- name: Setup mokocli tools - name: Setup mokocli tools
env: env:
@@ -95,7 +89,7 @@ jobs:
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 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 fi
rm -rf /tmp/mokocli rm -rf /tmp/mokocli
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoCLI.git 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 git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
cd /tmp/mokocli cd /tmp/mokocli
composer install --no-dev --no-interaction --quiet composer install --no-dev --no-interaction --quiet
@@ -104,46 +98,18 @@ jobs:
- name: Rename branch to rc - name: Rename branch to rc
run: | run: |
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" php ${MOKO_CLI}/branch_rename.php \
AUTH="Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" --from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
FROM="${{ github.event.pull_request.head.ref || 'dev' }}" --token "${{ secrets.MOKOGITEA_TOKEN }}" \
PR="${{ github.event.pull_request.number }}" --api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
--pr "${{ github.event.pull_request.number }}"
# Resolve the source branch HEAD commit.
SRC_JSON=$(curl -sf -H "$AUTH" "${API_BASE}/branches/${FROM}") \
|| { echo "::error::Source branch ${FROM} not found"; exit 1; }
SRC_SHA=$(printf '%s' "$SRC_JSON" | python3 -c "import sys, json; print(json.load(sys.stdin)['commit']['id'])" 2>/dev/null || true)
[ -n "$SRC_SHA" ] || { echo "::error::Could not resolve HEAD of ${FROM}"; exit 1; }
# Point rc at the source commit. If rc already exists (a protected branch that
# cannot be deleted), force-update its ref in place instead of delete+recreate:
# deleting a protected branch fails, which then makes the recreate return HTTP 409.
if curl -sf -o /dev/null -H "$AUTH" "${API_BASE}/branches/rc"; then
echo "rc exists - force-updating to ${FROM} (${SRC_SHA})"
curl -sf -X PATCH -H "$AUTH" -H "Content-Type: application/json" \
"${API_BASE}/git/refs/heads/rc" -d "{\"sha\":\"${SRC_SHA}\",\"force\":true}" \
|| { echo "::error::Failed to force-update rc (CI token needs force-push on the protected rc branch)"; exit 1; }
else
echo "Creating rc from ${FROM}"
curl -sf -X POST -H "$AUTH" -H "Content-Type: application/json" \
"${API_BASE}/branches" -d "{\"new_branch_name\":\"rc\",\"old_branch_name\":\"${FROM}\"}" \
|| { echo "::error::Failed to create rc from ${FROM}"; exit 1; }
fi
# Repoint the PR at rc, then delete the old source branch (non-fatal).
if [ -n "$PR" ]; then
curl -s -X PATCH -H "$AUTH" -H "Content-Type: application/json" \
"${API_BASE}/pulls/${PR}" -d '{"head":"rc"}' >/dev/null || true
fi
curl -s -X DELETE -H "$AUTH" "${API_BASE}/branches/${FROM}" >/dev/null || true
echo "Renamed ${FROM} -> rc"
- name: Checkout rc and configure git - name: Checkout rc and configure git
run: | run: |
git fetch origin rc git fetch origin rc
git checkout rc git checkout rc
git config --local user.email "mokogitea-actions[bot]@mokoconsulting.tech" git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "mokogitea-actions[bot]" git config --local user.name "gitea-actions[bot]"
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
- name: Publish RC release - name: Publish RC release
@@ -154,7 +120,7 @@ jobs:
- name: Update RC release notes from CHANGELOG.md - name: Update RC release notes from CHANGELOG.md
run: | run: |
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
# Extract [Unreleased] section from changelog # Extract [Unreleased] section from changelog
@@ -196,13 +162,9 @@ jobs:
release: release:
name: Build & Release Pipeline name: Build & Release Pipeline
runs-on: release runs-on: release
# Skip on template repos (Template-*) — they scaffold other repos and do not release.
if: >- if: >-
!startsWith(github.event.repository.name, 'Template-') &&
(
github.event.pull_request.merged == true || github.event.pull_request.merged == true ||
(github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc') (github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
)
steps: steps:
- name: Checkout repository - name: Checkout repository
@@ -210,12 +172,11 @@ jobs:
with: with:
token: ${{ secrets.MOKOGITEA_TOKEN }} token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 0 fetch-depth: 0
submodules: recursive
- name: Configure git for bot pushes - name: Configure git for bot pushes
run: | run: |
git config --local user.email "mokogitea-actions[bot]@mokoconsulting.tech" git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "mokogitea-actions[bot]" git config --local user.name "gitea-actions[bot]"
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
- name: Check for merge conflict markers - name: Check for merge conflict markers
@@ -246,7 +207,7 @@ jobs:
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 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 fi
rm -rf /tmp/mokocli rm -rf /tmp/mokocli
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoCLI.git 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 git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
cd /tmp/mokocli cd /tmp/mokocli
composer install --no-dev --no-interaction --quiet composer install --no-dev --no-interaction --quiet
@@ -307,7 +268,7 @@ jobs:
!startsWith(steps.platform.outputs.platform, 'joomla') !startsWith(steps.platform.outputs.platform, 'joomla')
run: | run: |
VERSION="${{ steps.version.outputs.version }}" VERSION="${{ steps.version.outputs.version }}"
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
SEMVER_TAG="v${VERSION}" SEMVER_TAG="v${VERSION}"
@@ -332,7 +293,7 @@ jobs:
- name: Update release notes and promote changelog - name: Update release notes and promote changelog
run: | run: |
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
# Get the stable release info (version and ID) # Get the stable release info (version and ID)
@@ -382,7 +343,7 @@ jobs:
content = open('CHANGELOG.md').read() content = open('CHANGELOG.md').read()
old = '## [Unreleased]' old = '## [Unreleased]'
new = f'## [Unreleased]\n\n## [{version}] --- {date}' new = f'## [Unreleased]\n\n## [{version}] --- {date}'
content = content if ('## [' + version + ']') in content else content.replace(old, new, 1) content = content.replace(old, new, 1)
open('CHANGELOG.md', 'w').write(content) open('CHANGELOG.md', 'w').write(content)
" "$VERSION" "$DATE" " "$VERSION" "$DATE"
git add CHANGELOG.md git add CHANGELOG.md
@@ -401,7 +362,7 @@ jobs:
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}" RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php ${MOKO_CLI}/release_mirror.php \ php ${MOKO_CLI}/release_mirror.php \
--version "$VERSION" --tag "$RELEASE_TAG" \ --version "$VERSION" --tag "$RELEASE_TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
@@ -426,11 +387,11 @@ jobs:
&& echo "main branch pushed to GitHub mirror" \ && echo "main branch pushed to GitHub mirror" \
|| echo "WARNING: GitHub mirror push failed" || echo "WARNING: GitHub mirror push failed"
- name: "Step 11: Delete rc branch (dev reset moved to cascade-dev.yml)" - name: "Step 11: Delete rc branch and recreate dev from main"
if: steps.version.outputs.skip != 'true' if: steps.version.outputs.skip != 'true'
continue-on-error: true continue-on-error: true
run: | run: |
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
# Delete rc branch (ephemeral — created by promote-rc) # Delete rc branch (ephemeral — created by promote-rc)
@@ -438,15 +399,23 @@ jobs:
"${API_BASE}/branches/rc" 2>/dev/null \ "${API_BASE}/branches/rc" 2>/dev/null \
&& echo "Deleted rc branch" || echo "rc branch not found" && echo "Deleted rc branch" || echo "rc branch not found"
# dev is reset from main by the dedicated "Cascade Main -> Dev" workflow # Delete dev branch
# (cascade-dev.yml), which runs after this release completes. curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
echo "rc cleaned; dev reset handled by cascade-dev.yml" >> $GITHUB_STEP_SUMMARY "${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
# Recreate dev from main (now includes version bump + changelog promotion)
curl -sf -X POST -H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
"${API_BASE}/branches" \
-d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY
- name: "Step 12: Create version branch from main" - name: "Step 12: Create version branch from main"
if: steps.version.outputs.skip != 'true' if: steps.version.outputs.skip != 'true'
continue-on-error: true continue-on-error: true
run: | run: |
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
BRANCH_NAME="version/${VERSION}" BRANCH_NAME="version/${VERSION}"
@@ -467,7 +436,7 @@ jobs:
if: steps.version.outputs.skip != 'true' if: steps.version.outputs.skip != 'true'
continue-on-error: true continue-on-error: true
run: | run: |
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php ${MOKO_CLI}/version_reset_dev.php \ php ${MOKO_CLI}/version_reset_dev.php \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \ --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
--branch dev --path . 2>&1 || true --branch dev --path . 2>&1 || true
@@ -493,5 +462,5 @@ jobs:
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Release | [View](${MOKOGITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
fi fi
+3 -4
View File
@@ -3,9 +3,9 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: MokoGitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Universal # INGROUP: MokoStandards.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic # REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /.mokogitea/workflows/branch-cleanup.yml # PATH: /.mokogitea/workflows/branch-cleanup.yml
# VERSION: 01.00.00 # VERSION: 01.00.00
# BRIEF: Delete feature branches after PR merge # BRIEF: Delete feature branches after PR merge
@@ -33,8 +33,7 @@ jobs:
run: | run: |
BRANCH="${{ github.event.pull_request.head.ref }}" BRANCH="${{ github.event.pull_request.head.ref }}"
API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches" API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches"
# URL-encode the branch name's slashes (no PHP dependency on the runner) ENCODED=$(php -r "echo rawurlencode('${BRANCH}');")
ENCODED=$(printf '%s' "${BRANCH}" | sed 's|/|%2F|g')
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \ STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \
-H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \ -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
+7 -103
View File
@@ -1,106 +1,10 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> # DISABLED — auto-release Step 11 recreates dev from main after every release.
# # Cascade-dev is redundant and causes version conflicts when both main and dev
# SPDX-License-Identifier: GPL-3.0-or-later # have different version numbers in templateDetails.xml / manifest.xml.
# name: "Cascade Main → Dev (DISABLED)"
# FILE INFORMATION on: workflow_dispatch
# DEFGROUP: MokoGitea.Workflow
# INGROUP: MokoStandards.Cascade
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
# PATH: /.mokogitea/workflows/cascade-dev.yml
# VERSION: 02.00.00
# BRIEF: Cascade main -> dev via PR; auto-merge only if conflict-free, else notify
name: "Cascade Main -> Dev"
on:
push:
branches:
- main
workflow_dispatch:
permissions:
contents: write
pull-requests: write
env:
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
# ntfy destination is configured via repo or org variables (org vars are inherited).
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
NTFY_TOPIC: ${{ vars.CASCADE_NTFY_TOPIC || vars.NTFY_TOPIC || 'gitea-releases' }}
jobs: jobs:
cascade: noop:
name: Cascade main -> dev
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Open main -> dev PR (auto-merge if clean, else notify) - run: echo "Cascade disabled — auto-release handles dev recreation"
env:
TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
REPO: ${{ github.repository }}
run: |
set -uo pipefail
API="${MOKOGITEA_URL}/api/v1/repos/${REPO}"
AUTH="Authorization: token ${TOKEN}"
jqnum() { python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('$1',''))" 2>/dev/null; }
# 0. dev must exist
if ! curl -sf -H "$AUTH" "${API}/branches/dev" >/dev/null 2>&1; then
echo "No dev branch - nothing to cascade."; exit 0
fi
# 1. is main ahead of dev?
AHEAD=$(curl -sf -H "$AUTH" "${API}/compare/dev...main" \
| python3 -c "import sys,json; print(json.load(sys.stdin).get('total_commits',0))" 2>/dev/null || echo 0)
if [ "${AHEAD:-0}" -eq 0 ]; then
echo "dev already up to date with main."; exit 0
fi
echo "main is ${AHEAD} commit(s) ahead of dev."
# 2. reuse an open main->dev PR, else create one
PR=$(curl -sf -H "$AUTH" "${API}/pulls?state=open&base=dev" \
| python3 -c "import sys,json; d=json.load(sys.stdin); print(next((str(p['number']) for p in d if p.get('head',{}).get('ref')=='main'), ''))" 2>/dev/null || echo "")
if [ -z "$PR" ]; then
RESP=$(curl -s -H "$AUTH" -H "Content-Type: application/json" -X POST "${API}/pulls" \
-d '{"head":"main","base":"dev","title":"chore(sync): cascade main -> dev","body":"Automated cascade of main into dev. Auto-merges only if conflict-free; otherwise left open for manual resolution."}')
PR=$(printf '%s' "$RESP" | jqnum number)
if [ -z "$PR" ]; then
echo "::warning::Could not open cascade PR: $RESP"; exit 0
fi
echo "Opened cascade PR #${PR}"
else
echo "Reusing open cascade PR #${PR}"
fi
# 3. wait for MokoGitea to compute mergeability (conflict detection)
MERGEABLE=""
for _ in 1 2 3 4 5 6; do
MERGEABLE=$(curl -sf -H "$AUTH" "${API}/pulls/${PR}" | jqnum mergeable)
case "$MERGEABLE" in True|False) break ;; esac
sleep 3
done
echo "mergeable=${MERGEABLE}"
notify() {
curl -sS \
-H "Title: ${REPO}: dev cascade needs manual merge" \
-H "Tags: warning,twisted_rightwards_arrows" \
-H "Priority: high" \
-H "Click: ${MOKOGITEA_URL}/${REPO}/pulls/${PR}" \
-d "main -> dev cascade PR #${PR} $1 It was NOT auto-merged; resolve it manually." \
"${NTFY_URL}/${NTFY_TOPIC}" || true
}
# 4. auto-merge only if conflict-free; otherwise notify
if [ "$MERGEABLE" = "True" ]; then
CODE=$(curl -s -o /tmp/merge.json -w "%{http_code}" -H "$AUTH" -H "Content-Type: application/json" \
-X POST "${API}/pulls/${PR}/merge" -d '{"Do":"merge","merge_when_checks_succeed":true}')
if [ "$CODE" -ge 200 ] && [ "$CODE" -lt 300 ]; then
echo "Cascade PR #${PR} merged (or scheduled to merge when checks pass)."
else
echo "::warning::Auto-merge returned HTTP ${CODE}: $(cat /tmp/merge.json)"
notify "could not be auto-merged (HTTP ${CODE})."
fi
else
echo "::warning::Cascade PR #${PR} has conflicts (mergeable=${MERGEABLE}); sending notification."
notify "has conflicts and cannot be merged automatically."
fi
+3 -15
View File
@@ -3,22 +3,16 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: MokoGitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.CI # INGROUP: MokoStandards.CI
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic # REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
# PATH: /.mokogitea/workflows/ci-generic.yml # PATH: /.gitea/workflows/ci-generic.yml
# VERSION: 01.00.00 # VERSION: 01.00.00
# BRIEF: CI pipeline — lint, validate, and test for generic projects (PHP + Node.js) # BRIEF: CI pipeline — lint, validate, and test for generic projects (PHP + Node.js)
name: "Generic: Project CI" name: "Generic: Project CI"
on: on:
pull_request:
branches:
- main
- dev
- dev/**
- rc/**
workflow_dispatch: workflow_dispatch:
permissions: permissions:
@@ -32,8 +26,6 @@ jobs:
lint: lint:
name: Lint & Validate name: Lint & Validate
runs-on: ubuntu-latest runs-on: ubuntu-latest
# Skip on template repos (Template-*) — they hold placeholder scaffolding, not buildable source.
if: ${{ !startsWith(github.event.repository.name, 'Template-') }}
steps: steps:
- name: Checkout - name: Checkout
@@ -131,11 +123,7 @@ jobs:
test: test:
name: Tests name: Tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
# Independent job (no `needs: lint`): the Gitea Actions scheduler does not needs: lint
# offer the dependent 2nd job of a needs-chain to runners, so it stalls in
# "waiting" and is reaped by ABANDONED_JOB_TIMEOUT. Guard template repos
# directly (same condition lint uses) instead of gating on lint's result.
if: ${{ !startsWith(github.event.repository.name, 'Template-') }}
steps: steps:
- name: Checkout - name: Checkout
+1 -68
View File
@@ -1,68 +1 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> IyBDb3B5cmlnaHQgKEMpIDIwMjYgTW9rbyBDb25zdWx0aW5nIDxoZWxsb0Btb2tvY29uc3VsdGluZy50ZWNoPgojCiMgU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IEdQTC0zLjAtb3ItbGF0ZXIKIwojIEZJTEUgSU5GT1JNQVRJT04KIyBERUZHUk9VUDogR2l0ZWEuV29ya2Zsb3cKIyBJTkdST1VQOiBtb2tvY2xpLlVuaXZlcnNhbAojIFJFUE86IGh0dHBzOi8vZ2l0Lm1va29jb25zdWx0aW5nLnRlY2gvTW9rb0NvbnN1bHRpbmcvbW9rb2NsaQojIFBBVEg6IC8ubW9rb2dpdGVhL3dvcmtmbG93cy9jaS1pc3N1ZS1yZXBvcnRlci55bWwKIyBWRVJTSU9OOiAwMS4wMC4wMAojIEJSSUVGOiBSZXVzYWJsZSB3b3JrZmxvdyDigJQgY3JlYXRlcy91cGRhdGVzIGEgR2l0ZWEgaXNzdWUgd2hlbiBhIENJIGdhdGUgZmFpbHMuCiMgICAgICAgIENsb25lcyBNb2tvQ0xJIGFuZCBydW5zIGNsaS9jaV9pc3N1ZV9yZXBvcnRlci5zaC4KCm5hbWU6ICJVbml2ZXJzYWw6IENJIElzc3VlIFJlcG9ydGVyIgoKb246CiAgd29ya2Zsb3dfY2FsbDoKICAgIGlucHV0czoKICAgICAgZ2F0ZToKICAgICAgICBkZXNjcmlwdGlvbjogIkNJIGdhdGUgbmFtZSAoZS5nLiBQUiBWYWxpZGF0aW9uLCBSZXBvc2l0b3J5IEhlYWx0aCkiCiAgICAgICAgcmVxdWlyZWQ6IHRydWUKICAgICAgICB0eXBlOiBzdHJpbmcKICAgICAgZGV0YWlsczoKICAgICAgICBkZXNjcmlwdGlvbjogIkh1bWFuLXJlYWRhYmxlIGZhaWx1cmUgZGVzY3JpcHRpb24iCiAgICAgICAgcmVxdWlyZWQ6IHRydWUKICAgICAgICB0eXBlOiBzdHJpbmcKICAgICAgc2V2ZXJpdHk6CiAgICAgICAgZGVzY3JpcHRpb246ICJlcnJvciBvciB3YXJuaW5nIgogICAgICAgIHJlcXVpcmVkOiBmYWxzZQogICAgICAgIHR5cGU6IHN0cmluZwogICAgICAgIGRlZmF1bHQ6ICJlcnJvciIKICAgICAgd29ya2Zsb3c6CiAgICAgICAgZGVzY3JpcHRpb246ICJXb3JrZmxvdyBuYW1lIGZvciB0aGUgaXNzdWUgdGl0bGUiCiAgICAgICAgcmVxdWlyZWQ6IGZhbHNlCiAgICAgICAgdHlwZTogc3RyaW5nCiAgICAgICAgZGVmYXVsdDogIiIKICAgIHNlY3JldHM6CiAgICAgIE1PS09HSVRFQV9UT0tFTjoKICAgICAgICByZXF1aXJlZDogdHJ1ZQoKZW52OgogIEZPUkNFX0pBVkFTQ1JJUFRfQUNUSU9OU19UT19OT0RFMjQ6IHRydWUKCmpvYnM6CiAgcmVwb3J0OgogICAgbmFtZTogIlJlcG9ydDogJHt7IGlucHV0cy5nYXRlIH19IgogICAgcnVucy1vbjogdWJ1bnR1LWxhdGVzdAoKICAgIHN0ZXBzOgogICAgICAtIG5hbWU6IENsb25lIE1va29DTEkKICAgICAgICBlbnY6CiAgICAgICAgICBNT0tPR0lURUFfVE9LRU46ICR7eyBzZWNyZXRzLk1PS09HSVRFQV9UT0tFTiB9fQogICAgICAgIHJ1bjogfAogICAgICAgICAgTU9LT0dJVEVBX1VSTD0iJHt7IHZhcnMuR0lURUFfVVJMIHx8ICdodHRwczovL2dpdC5tb2tvY29uc3VsdGluZy50ZWNoJyB9fSIKICAgICAgICAgIGdpdCBjbG9uZSAtLWRlcHRoIDEgLS1maWx0ZXI9YmxvYjpub25lIC0tc3BhcnNlICIke01PS09HSVRFQV9VUkx9L01va29Db25zdWx0aW5nL01va29DTEkuZ2l0IiAvdG1wL21va29jbGkKICAgICAgICAgIGNkIC90bXAvbW9rb2NsaSAmJiBnaXQgc3BhcnNlLWNoZWNrb3V0IHNldCBjbGkvY2lfaXNzdWVfcmVwb3J0ZXIuc2gKCiAgICAgIC0gbmFtZTogUmVwb3J0IENJIGZhaWx1cmUKICAgICAgICBlbnY6CiAgICAgICAgICBNT0tPR0lURUFfVE9LRU46ICR7eyBzZWNyZXRzLk1PS09HSVRFQV9UT0tFTiB9fQogICAgICAgICAgTU9LT0dJVEVBX1VSTDogJHt7IHZhcnMuR0lURUFfVVJMIHx8ICdodHRwczovL2dpdC5tb2tvY29uc3VsdGluZy50ZWNoJyB9fQogICAgICAgIHJ1bjogfAogICAgICAgICAgY2htb2QgK3ggL3RtcC9tb2tvY2xpL2NsaS9jaV9pc3N1ZV9yZXBvcnRlci5zaAogICAgICAgICAgL3RtcC9tb2tvY2xpL2NsaS9jaV9pc3N1ZV9yZXBvcnRlci5zaCBcCiAgICAgICAgICAgIC0tZ2F0ZSAiJHt7IGlucHV0cy5nYXRlIH19IiBcCiAgICAgICAgICAgIC0tZGV0YWlscyAiJHt7IGlucHV0cy5kZXRhaWxzIH19IiBcCiAgICAgICAgICAgIC0tc2V2ZXJpdHkgIiR7eyBpbnB1dHMuc2V2ZXJpdHkgfX0iIFwKICAgICAgICAgICAgLS13b3JrZmxvdyAiJHt7IGlucHV0cy53b3JrZmxvdyB9fSIK
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: MokoGitea.Workflow
# INGROUP: mokocli.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
# PATH: /.mokogitea/workflows/ci-issue-reporter.yml
# VERSION: 01.00.00
# BRIEF: Reusable workflow — creates/updates a MokoGitea issue when a CI gate fails.
# Clones MokoCLI and runs cli/ci_issue_reporter.sh.
name: "Universal: CI Issue Reporter"
on:
workflow_call:
inputs:
gate:
description: "CI gate name (e.g. PR Validation, Repository Health)"
required: true
type: string
details:
description: "Human-readable failure description"
required: true
type: string
severity:
description: "error or warning"
required: false
type: string
default: "error"
workflow:
description: "Workflow name for the issue title"
required: false
type: string
default: ""
secrets:
MOKOGITEA_TOKEN:
required: true
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
report:
name: "Report: ${{ inputs.gate }}"
runs-on: ubuntu-latest
steps:
- name: Clone MokoCLI
env:
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
run: |
MOKOGITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}"
git clone --depth 1 --filter=blob:none --sparse "${MOKOGITEA_URL}/MokoConsulting/MokoCLI.git" /tmp/mokocli
cd /tmp/mokocli && git sparse-checkout set cli/ci_issue_reporter.sh
- name: Report CI failure
env:
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
run: |
chmod +x /tmp/mokocli/cli/ci_issue_reporter.sh
/tmp/mokocli/cli/ci_issue_reporter.sh \
--gate "${{ inputs.gate }}" \
--details "${{ inputs.details }}" \
--severity "${{ inputs.severity }}" \
--workflow "${{ inputs.workflow }}"
+13 -13
View File
@@ -3,10 +3,10 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: MokoGitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Maintenance # INGROUP: MokoStandards.Maintenance
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards # REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /.mokogitea/workflows/cleanup.yml # PATH: /.gitea/workflows/cleanup.yml
# VERSION: 01.00.00 # VERSION: 01.00.00
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs # BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
@@ -21,7 +21,7 @@ permissions:
contents: write contents: write
env: env:
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
jobs: jobs:
cleanup: cleanup:
@@ -33,30 +33,30 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
token: ${{ secrets.MOKOGITEA_TOKEN }} token: ${{ secrets.GA_TOKEN }}
- name: Delete merged branches - name: Delete merged branches
env: env:
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} GA_TOKEN: ${{ secrets.GA_TOKEN }}
run: | run: |
echo "=== Merged Branch Cleanup ===" echo "=== Merged Branch Cleanup ==="
API="${MOKOGITEA_URL}/api/v1/repos/${{ github.repository }}" API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
# List branches via API # List branches via API
BRANCHES=$(curl -sS -H "Authorization: token ${MOKOGITEA_TOKEN}" \ BRANCHES=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
"${API}/branches?limit=50" | jq -r '.[].name') "${API}/branches?limit=50" | jq -r '.[].name')
DELETED=0 DELETED=0
for BRANCH in $BRANCHES; do for BRANCH in $BRANCHES; do
# Skip protected branches # Skip protected branches
case "$BRANCH" in case "$BRANCH" in
main|master|dev|develop|rc|beta|alpha|release|release/*|production|stable|staging|hotfix/*|version/*) continue ;; main|master|develop|release/*|hotfix/*) continue ;;
esac esac
# Check if branch is merged into main # Check if branch is merged into main
if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then
echo " Deleting merged branch: ${BRANCH}" echo " Deleting merged branch: ${BRANCH}"
curl -sS -X DELETE -H "Authorization: token ${MOKOGITEA_TOKEN}" \ curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
"${API}/branches/${BRANCH}" 2>/dev/null || true "${API}/branches/${BRANCH}" 2>/dev/null || true
DELETED=$((DELETED + 1)) DELETED=$((DELETED + 1))
fi fi
@@ -66,20 +66,20 @@ jobs:
- name: Clean old workflow runs - name: Clean old workflow runs
env: env:
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} GA_TOKEN: ${{ secrets.GA_TOKEN }}
run: | run: |
echo "=== Workflow Run Cleanup ===" echo "=== Workflow Run Cleanup ==="
API="${MOKOGITEA_URL}/api/v1/repos/${{ github.repository }}" API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ) CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ)
# Get old completed runs # Get old completed runs
RUNS=$(curl -sS -H "Authorization: token ${MOKOGITEA_TOKEN}" \ RUNS=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
"${API}/actions/runs?status=completed&limit=50" | \ "${API}/actions/runs?status=completed&limit=50" | \
jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null) jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null)
DELETED=0 DELETED=0
for RUN_ID in $RUNS; do for RUN_ID in $RUNS; do
curl -sS -X DELETE -H "Authorization: token ${MOKOGITEA_TOKEN}" \ curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
"${API}/actions/runs/${RUN_ID}" 2>/dev/null || true "${API}/actions/runs/${RUN_ID}" 2>/dev/null || true
DELETED=$((DELETED + 1)) DELETED=$((DELETED + 1))
done done
+3 -3
View File
@@ -3,10 +3,10 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: MokoGitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Security # INGROUP: MokoStandards.Security
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API # REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
# PATH: /.mokogitea/workflows/gitleaks.yml # PATH: /templates/workflows/gitleaks.yml.template
# VERSION: 01.00.00 # VERSION: 01.00.00
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens # BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
# #
+6 -6
View File
@@ -3,9 +3,9 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: MokoGitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Automation # INGROUP: mokocli.Automation
# VERSION: 01.14.00 # VERSION: 01.08.42
# BRIEF: Auto-create feature branch when an issue is opened # BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch" name: "Universal: Issue Branch"
@@ -19,7 +19,7 @@ permissions:
issues: write issues: write
env: env:
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
jobs: jobs:
create-branch: create-branch:
@@ -28,8 +28,8 @@ jobs:
steps: steps:
- name: Create branch and comment - name: Create branch and comment
run: | run: |
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" TOKEN="${{ secrets.GA_TOKEN }}"
API="${MOKOGITEA_URL}/api/v1/repos/${{ github.repository }}" API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
ISSUE_NUM="${{ github.event.issue.number }}" ISSUE_NUM="${{ github.event.issue.number }}"
ISSUE_TITLE="${{ github.event.issue.title }}" ISSUE_TITLE="${{ github.event.issue.title }}"
@@ -58,7 +58,7 @@ jobs:
echo "Created branch: ${BRANCH}" echo "Created branch: ${BRANCH}"
# Comment on issue with branch link # Comment on issue with branch link
REPO_URL="${MOKOGITEA_URL}/${{ github.repository }}" REPO_URL="${GITEA_URL}/${{ github.repository }}"
BODY="Branch created: [\`${BRANCH}\`](${REPO_URL}/src/branch/${BRANCH})\n\n\`\`\`bash\ngit fetch origin\ngit checkout ${BRANCH}\n\`\`\`" BODY="Branch created: [\`${BRANCH}\`](${REPO_URL}/src/branch/${BRANCH})\n\n\`\`\`bash\ngit fetch origin\ngit checkout ${BRANCH}\n\`\`\`"
curl -sf -X POST \ curl -sf -X POST \
+9 -9
View File
@@ -3,10 +3,10 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: MokoGitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Notifications # INGROUP: MokoStandards.Notifications
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards # REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /.mokogitea/workflows/notify.yml # PATH: /.gitea/workflows/notify.yml
# VERSION: 01.00.00 # VERSION: 01.00.00
# BRIEF: Push notifications via ntfy on release success or workflow failure # BRIEF: Push notifications via ntfy on release success or workflow failure
@@ -15,9 +15,9 @@ name: "Universal: Notifications"
on: on:
workflow_run: workflow_run:
workflows: workflows:
- "Universal: Build & Release" - "Joomla Build & Release"
- "Joomla: Extension CI" - "Joomla Extension CI"
- "Generic: Project CI" - "Deploy"
types: types:
- completed - completed
@@ -46,13 +46,13 @@ jobs:
WORKFLOW="${{ github.event.workflow_run.name }}" WORKFLOW="${{ github.event.workflow_run.name }}"
URL="${{ github.event.workflow_run.html_url }}" URL="${{ github.event.workflow_run.html_url }}"
curl -sS --retry 3 --retry-connrefused --retry-delay 2 --max-time 20 \ curl -sS \
-H "Title: ${REPO} released" \ -H "Title: ${REPO} released" \
-H "Tags: white_check_mark,package" \ -H "Tags: white_check_mark,package" \
-H "Priority: default" \ -H "Priority: default" \
-H "Click: ${URL}" \ -H "Click: ${URL}" \
-d "${WORKFLOW} completed successfully." \ -d "${WORKFLOW} completed successfully." \
"${NTFY_URL}/${NTFY_TOPIC}" || echo "::warning::ntfy notification could not be delivered (non-fatal)" "${NTFY_URL}/${NTFY_TOPIC}"
- name: Notify on failure - name: Notify on failure
if: github.event.workflow_run.conclusion == 'failure' if: github.event.workflow_run.conclusion == 'failure'
@@ -61,10 +61,10 @@ jobs:
WORKFLOW="${{ github.event.workflow_run.name }}" WORKFLOW="${{ github.event.workflow_run.name }}"
URL="${{ github.event.workflow_run.html_url }}" URL="${{ github.event.workflow_run.html_url }}"
curl -sS --retry 3 --retry-connrefused --retry-delay 2 --max-time 20 \ curl -sS \
-H "Title: ${REPO} workflow failed" \ -H "Title: ${REPO} workflow failed" \
-H "Tags: x,warning" \ -H "Tags: x,warning" \
-H "Priority: high" \ -H "Priority: high" \
-H "Click: ${URL}" \ -H "Click: ${URL}" \
-d "${WORKFLOW} failed. Check the run for details." \ -d "${WORKFLOW} failed. Check the run for details." \
"${NTFY_URL}/${NTFY_TOPIC}" || echo "::warning::ntfy notification could not be delivered (non-fatal)" "${NTFY_URL}/${NTFY_TOPIC}"
+39 -108
View File
@@ -3,10 +3,10 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: MokoGitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.CI # INGROUP: moko-platform.CI
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic # REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# PATH: /.mokogitea/workflows/pr-check.yml # PATH: /templates/workflows/universal/pr-check.yml.template
# VERSION: 09.23.00 # VERSION: 09.23.00
# BRIEF: PR gate — branch policy + code validation before merge # BRIEF: PR gate — branch policy + code validation before merge
@@ -47,15 +47,15 @@ jobs:
fi fi
;; ;;
fix/*|bugfix/*) fix/*|bugfix/*)
if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then if [ "$BASE" != "dev" ]; then
ALLOWED=false ALLOWED=false
REASON="Fix branches must target 'dev' or 'main', not '${BASE}'" REASON="Fix branches must target 'dev', not '${BASE}'"
fi fi
;; ;;
patch/*) patch/*)
if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ] && [ "$BASE" != "main" ]; then if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then
ALLOWED=false ALLOWED=false
REASON="Patch branches must target 'dev', 'rc', or 'main', not '${BASE}'" REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'"
fi fi
;; ;;
hotfix/*) hotfix/*)
@@ -86,8 +86,7 @@ jobs:
echo "" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY
echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
echo "- \`fix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
echo "- \`patch/*\` → \`dev\`, \`rc\`, or \`main\`" >> $GITHUB_STEP_SUMMARY
echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
@@ -97,80 +96,6 @@ jobs:
echo "Branch policy: OK (${HEAD} → ${BASE})" echo "Branch policy: OK (${HEAD} → ${BASE})"
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
# ── Docs Update Gate (main PRs) ─────────────────────────────────────────
require-docs:
name: Require Docs Update
runs-on: ubuntu-latest
# Enforce only on PRs merging into main: README.md + CHANGELOG.md must both be updated.
if: ${{ github.base_ref == 'main' }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Require README.md and CHANGELOG.md in the PR diff
run: |
BASE="${{ github.event.pull_request.base.sha }}"
HEAD="${{ github.event.pull_request.head.sha }}"
CHANGED="$(git diff --name-only "$BASE" "$HEAD" 2>/dev/null || true)"
if [ -z "$CHANGED" ]; then
git fetch -q origin "${{ github.base_ref }}" 2>/dev/null || true
CHANGED="$(git diff --name-only "origin/${{ github.base_ref }}...HEAD" 2>/dev/null || true)"
fi
echo "Changed files in PR:"
echo "$CHANGED"
MISSING=""
echo "$CHANGED" | grep -qxE 'README\.md' || MISSING="README.md"
echo "$CHANGED" | grep -qxE 'CHANGELOG\.md' || MISSING="${MISSING:+$MISSING, }CHANGELOG.md"
if [ -n "$MISSING" ]; then
echo "::error::PRs into main must update: ${MISSING}"
{
echo "## Docs Update Required"
echo ""
echo "PRs merging into \`main\` must update both **README.md** and **CHANGELOG.md**."
echo ""
echo "Not updated in this PR: **${MISSING}**"
} >> "$GITHUB_STEP_SUMMARY"
exit 1
fi
echo "Docs update present (README.md + CHANGELOG.md)"
echo "## Docs Update: Passed" >> "$GITHUB_STEP_SUMMARY"
# ── Wiki Update Reminder (main PRs, non-blocking) ───────────────────────
wiki-reminder:
name: Wiki Update Reminder
runs-on: ubuntu-latest
if: ${{ github.base_ref == 'main' }}
steps:
- name: Remind to update the wiki
env:
TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
SERVER: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
REPO: ${{ github.repository }}
PR: ${{ github.event.pull_request.number }}
run: |
set -uo pipefail
{
echo "## Wiki Update Reminder"
echo ""
echo "Docs are **wiki-first** at MokoConsulting. If this change affects behavior, usage, configuration, or standards, update the repo wiki:"
echo ""
echo "- ${SERVER}/${REPO}/wiki"
echo ""
echo "_Non-blocking reminder._"
} >> "$GITHUB_STEP_SUMMARY"
# Post a single PR comment (idempotent via hidden marker); best-effort, never fails.
API="${SERVER}/api/v1/repos/${REPO}/issues/${PR}/comments"
if [ -n "${TOKEN:-}" ] && [ -n "${PR:-}" ]; then
existing="$(curl -sf -H "Authorization: token ${TOKEN}" "$API" 2>/dev/null | grep -c 'wiki-reminder' || true)"
if [ "${existing:-0}" -eq 0 ]; then
curl -sf -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" -X POST "$API" \
-d '{"body":"<!-- wiki-reminder -->\n\n**Wiki reminder:** docs are wiki-first -- if this PR changes behavior, usage, config, or standards, please update the repo wiki before/after merge. _(non-blocking)_"}' >/dev/null 2>&1 || true
fi
fi
echo "Wiki reminder emitted (non-blocking)."
# ── Secret Scanning ────────────────────────────────────────────────── # ── Secret Scanning ──────────────────────────────────────────────────
gitleaks: gitleaks:
name: Secret Scan name: Secret Scan
@@ -201,8 +126,6 @@ jobs:
validate: validate:
name: Validate PR name: Validate PR
runs-on: ubuntu-latest runs-on: ubuntu-latest
# Skip on template repos (Template-*) — no real manifest/source/changelog to validate.
if: ${{ !startsWith(github.event.repository.name, 'Template-') }}
steps: steps:
- name: Checkout - name: Checkout
@@ -210,7 +133,7 @@ jobs:
- name: Check for merge conflict markers - name: Check for merge conflict markers
run: | run: |
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --exclude-dir='.git' --exclude-dir='.mokogitea' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true) CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
if [ -n "$CONFLICTS" ]; then if [ -n "$CONFLICTS" ]; then
echo "::error::Merge conflict markers found in source files" echo "::error::Merge conflict markers found in source files"
echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY
@@ -224,12 +147,11 @@ jobs:
- name: Detect platform - name: Detect platform
id: platform id: platform
run: | run: |
# Platform comes from the MokoGitea metadata API (public GET); manifest.xml is no longer used. # Read platform from XML manifest (<platform> tag) or plain text fallback
API="${GITHUB_SERVER_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${GITHUB_REPOSITORY}/metadata" PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1)
PLATFORM="$(curl -sf "$API" 2>/dev/null | python3 -c "import sys, json; print(json.load(sys.stdin).get('platform') or '')" 2>/dev/null || true)" [ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]')
[ -z "$PLATFORM" ] && PLATFORM="generic" [ -z "$PLATFORM" ] && PLATFORM="generic"
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT" echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
echo "Detected platform: $PLATFORM"
- name: Setup PHP - name: Setup PHP
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr' if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
@@ -258,9 +180,8 @@ jobs:
while IFS= read -r -d '' file; do while IFS= read -r -d '' file; do
# Skip vendor, node_modules, and index.html stub files # Skip vendor, node_modules, and index.html stub files
case "$file" in ./vendor/*|./node_modules/*) continue ;; esac case "$file" in ./vendor/*|./node_modules/*) continue ;; esac
# Scan the whole file for the JEXEC/JPATH guard: it is placed after # Check first 10 lines for JEXEC or JPATH guard
# the SPDX/file-header docblock, which commonly runs past 20 lines. if ! head -20 "$file" | grep -qE "defined\s*\(\s*['\"](_JEXEC|JPATH_BASE|\\\\JPATH_PLATFORM)['\"]"; then
if ! grep -qE "defined\s*\(\s*['\"](_JEXEC|JPATH_BASE|\\\\JPATH_PLATFORM)['\"]" "$file"; then
echo "::error file=${file}::Missing JEXEC guard: ${file}" echo "::error file=${file}::Missing JEXEC guard: ${file}"
ERRORS=$((ERRORS + 1)) ERRORS=$((ERRORS + 1))
fi fi
@@ -351,7 +272,7 @@ jobs:
joomla) joomla)
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1) MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
if [ -z "$MANIFEST" ]; then if [ -z "$MANIFEST" ]; then
echo "::warning::No Joomla manifest found (MokoSuite site)" echo "::warning::No Joomla manifest found (WaaS site)"
exit 0 exit 0
fi fi
echo "Manifest: ${MANIFEST}" echo "Manifest: ${MANIFEST}"
@@ -364,7 +285,7 @@ jobs:
# Block legacy raw/branch update server URLs on MokoGitea # Block legacy raw/branch update server URLs on MokoGitea
RAW_URLS=$(grep -n 'raw/branch' "$MANIFEST" | grep -i 'mokoconsulting\|mokogitea\|git\.mokoconsulting\.tech' || true) RAW_URLS=$(grep -n 'raw/branch' "$MANIFEST" | grep -i 'mokoconsulting\|mokogitea\|git\.mokoconsulting\.tech' || true)
if [ -n "$RAW_URLS" ]; then if [ -n "$RAW_URLS" ]; then
echo "::error::Manifest contains legacy raw/branch update server URL on MokoGitea. Use the MokoGitea Pages URL instead (e.g. /{REPO}/updates.xml not /{REPO}/raw/branch/main/updates.xml)" echo "::error::Manifest contains legacy raw/branch update server URL on MokoGitea. Use the Gitea Pages URL instead (e.g. /{REPO}/updates.xml not /{REPO}/raw/branch/main/updates.xml)"
echo "$RAW_URLS" echo "$RAW_URLS"
exit 1 exit 1
fi fi
@@ -571,33 +492,43 @@ jobs:
name: Build RC Package name: Build RC Package
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [branch-policy, validate] needs: [branch-policy, validate]
# Run only when both gates succeeded; always() forces evaluation so a skipped
# validate (e.g. template repos) skips this job cleanly instead of hanging.
if: ${{ always() && needs.branch-policy.result == 'success' && needs.validate.result == 'success' }}
steps: steps:
- name: Trigger RC pre-release - name: Trigger RC pre-release
env: env:
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
REPO: ${{ github.repository }} REPO: ${{ github.repository }}
BRANCH: ${{ github.head_ref }} BRANCH: ${{ github.head_ref }}
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
run: | run: |
curl -s -X POST "${MOKOGITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${MOKOGITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}" curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
# ── Issue Reporter ────────────────────────────────────────────────────── # ── Issue Reporter ──────────────────────────────────────────────────────
report-issues: report-issues:
name: Report Issues name: Report Issues
runs-on: ubuntu-latest
needs: [branch-policy, validate] needs: [branch-policy, validate]
if: >- if: >-
always() && always() &&
needs.validate.result == 'failure' needs.validate.result == 'failure'
uses: ./.mokogitea/workflows/ci-issue-reporter.yml
steps:
- name: Checkout
uses: actions/checkout@v4
with: with:
gate: "PR Validation" sparse-checkout: automation/ci-issue-reporter.sh
workflow: "PR Check" sparse-checkout-cone-mode: false
severity: error
details: "PR validation failed (syntax, manifest, changelog, or source checks). See the CI run for the specific check that failed." - name: "File issue for PR validation failure"
secrets: inherit env:
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
run: |
chmod +x automation/ci-issue-reporter.sh
./automation/ci-issue-reporter.sh \
--gate "PR Validation" \
--workflow "PR Check" \
--severity error \
--details "PR validation failed (syntax, manifest, changelog, or source checks). See the CI run for the specific check that failed."
+4 -4
View File
@@ -20,7 +20,7 @@ permissions:
contents: read contents: read
env: env:
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
@@ -55,14 +55,14 @@ jobs:
- name: Validate metadata against Joomla manifest - name: Validate metadata against Joomla manifest
env: env:
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
run: | run: |
php ${MOKO_CLI}/joomla_metadata_validate.php \ php ${MOKO_CLI}/joomla_metadata_validate.php \
--path . \ --path . \
--token "${MOKOGITEA_TOKEN}" \ --token "${GITEA_TOKEN}" \
--org "${GITEA_ORG}" \ --org "${GITEA_ORG}" \
--repo "${GITEA_REPO}" \ --repo "${GITEA_REPO}" \
--api-base "${MOKOGITEA_URL}/api/v1" \ --api-base "${GITEA_URL}/api/v1" \
--ci --ci
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
+8 -23
View File
@@ -3,11 +3,11 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: MokoGitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Release # INGROUP: mokocli.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic # REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /.mokogitea/workflows/pre-release.yml # PATH: /templates/workflows/universal/pre-release.yml.template
# VERSION: 05.02.01 # VERSION: 05.01.00
# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches # BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches
name: "Universal: Pre-Release" name: "Universal: Pre-Release"
@@ -48,13 +48,9 @@ jobs:
build: build:
name: "Build Pre-Release (${{ inputs.stability || github.ref_name }})" name: "Build Pre-Release (${{ inputs.stability || github.ref_name }})"
runs-on: release runs-on: release
# Skip on template repos (Template-*) — they scaffold other repos and do not release.
if: >- if: >-
!startsWith(github.event.repository.name, 'Template-') &&
(
github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_dispatch' ||
github.event_name == 'push' github.event_name == 'push'
)
steps: steps:
- name: Checkout - name: Checkout
@@ -63,11 +59,6 @@ jobs:
fetch-depth: 0 fetch-depth: 0
token: ${{ secrets.MOKOGITEA_TOKEN }} token: ${{ secrets.MOKOGITEA_TOKEN }}
ref: ${{ github.ref_name }} ref: ${{ github.ref_name }}
submodules: recursive
- name: Update submodules to main
run: |
git submodule foreach --quiet 'git checkout main && git pull --quiet origin main' 2>/dev/null || true
- name: Setup mokocli tools - name: Setup mokocli tools
env: env:
@@ -84,7 +75,7 @@ jobs:
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 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 fi
rm -rf /tmp/mokocli rm -rf /tmp/mokocli
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoCLI.git 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 git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
@@ -156,19 +147,13 @@ jobs:
fi fi
# Commit version bump # Commit version bump
git config --local user.email "mokogitea-actions[bot]@mokoconsulting.tech" git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "mokogitea-actions[bot]" git config --local user.name "gitea-actions[bot]"
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
git add -A git add -A
git diff --cached --quiet || { git diff --cached --quiet || {
git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]" git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]"
# Push the bump commit, but do NOT fail the release if the target branch git push origin HEAD 2>&1
# is protected and the release identity is not on the push allowlist.
# The build proceeds from the in-tree bumped version regardless; if the
# push is rejected, the next run simply re-bumps from the same base.
if ! git push origin HEAD 2>&1; then
echo "::warning::Version-bump commit could not be pushed (protected branch?). Building from in-tree version ${VERSION} anyway."
fi
} }
# Auto-detect element via manifest_element.php # Auto-detect element via manifest_element.php
+16 -22
View File
@@ -3,9 +3,9 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: MokoGitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Universal # INGROUP: mokocli.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic # REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /.mokogitea/workflows/rc-revert.yml # PATH: /.mokogitea/workflows/rc-revert.yml
# VERSION: 09.23.00 # VERSION: 09.23.00
# BRIEF: Rename rc/ branch back to dev/ when PR is closed without merge # BRIEF: Rename rc/ branch back to dev/ when PR is closed without merge
@@ -25,25 +25,16 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: >- if: >-
github.event.pull_request.merged == false && github.event.pull_request.merged == false &&
startsWith(github.event.pull_request.head.ref, 'rc/') && startsWith(github.event.pull_request.head.ref, 'rc/')
!startsWith(github.event.repository.name, 'Template-')
steps: steps:
- name: Rename branch - name: Rename branch
env:
BRANCH: ${{ github.event.pull_request.head.ref }}
REPO: ${{ github.repository }}
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
run: | run: |
set -euo pipefail BRANCH="${{ github.event.pull_request.head.ref }}"
# BRANCH is attacker-controlled (PR head ref). Strict allowlist before ANY use.
if ! printf '%s' "$BRANCH" | grep -Eq '^rc/[A-Za-z0-9._/-]+$'; then
echo "::error::Refusing unsafe branch name: $BRANCH"; exit 1
fi
SUFFIX="${BRANCH#rc/}" SUFFIX="${BRANCH#rc/}"
DEV_BRANCH="dev/${SUFFIX}" DEV_BRANCH="dev/${SUFFIX}"
API="${GITEA_URL}/api/v1/repos/${REPO}/branches" API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
# Create dev/ branch from rc/ branch # Create dev/ branch from rc/ branch
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X POST \ STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X POST \
@@ -51,22 +42,25 @@ jobs:
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d "{\"new_branch_name\": \"${DEV_BRANCH}\", \"old_branch_name\": \"${BRANCH}\"}" \ -d "{\"new_branch_name\": \"${DEV_BRANCH}\", \"old_branch_name\": \"${BRANCH}\"}" \
"${API}" 2>/dev/null || true) "${API}" 2>/dev/null || true)
if [ "$STATUS" = "201" ]; then if [ "$STATUS" = "201" ]; then
echo "Created branch: ${DEV_BRANCH}" >> "$GITHUB_STEP_SUMMARY" echo "Created branch: ${DEV_BRANCH}" >> $GITHUB_STEP_SUMMARY
else else
echo "::error::Failed to create ${DEV_BRANCH} from ${BRANCH} (HTTP ${STATUS})"; exit 1 echo "::error::Failed to create ${DEV_BRANCH} from ${BRANCH} (HTTP ${STATUS})"
exit 1
fi fi
# Read BRANCH from the environment inside PHP (getenv, no string interpolation -> no PHP injection) # Delete rc/ branch
ENCODED=$(php -r 'echo rawurlencode(getenv("BRANCH"));') ENCODED=$(php -r "echo rawurlencode('${BRANCH}');")
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \ STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \
-H "Authorization: token ${TOKEN}" \ -H "Authorization: token ${TOKEN}" \
"${API}/${ENCODED}" 2>/dev/null || true) "${API}/${ENCODED}" 2>/dev/null || true)
if [ "$STATUS" = "204" ]; then if [ "$STATUS" = "204" ]; then
echo "Deleted branch: ${BRANCH}" >> "$GITHUB_STEP_SUMMARY" echo "Deleted branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
else else
echo "::warning::Failed to delete ${BRANCH} (HTTP ${STATUS})" echo "::warning::Failed to delete ${BRANCH} (HTTP ${STATUS})"
fi fi
echo "### RC Reverted" >> "$GITHUB_STEP_SUMMARY" echo "### RC Reverted" >> $GITHUB_STEP_SUMMARY
echo "${BRANCH} → ${DEV_BRANCH}" >> "$GITHUB_STEP_SUMMARY" echo "${BRANCH} → ${DEV_BRANCH}" >> $GITHUB_STEP_SUMMARY
+40 -28
View File
@@ -6,10 +6,10 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: MokoGitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Validation # INGROUP: mokocli.Validation
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic # REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli
# PATH: /.mokogitea/workflows/repo-health.yml # PATH: /templates/workflows/joomla/repo_health.yml.template
# VERSION: 09.23.00 # VERSION: 09.23.00
# BRIEF: Enforces repository guardrails by validating scripts governance, tooling availability, and core repository health artifacts. # BRIEF: Enforces repository guardrails by validating scripts governance, tooling availability, and core repository health artifacts.
# ============================================================================ # ============================================================================
@@ -77,7 +77,7 @@ jobs:
- name: Check actor permission (admin only) - name: Check actor permission (admin only)
id: perm id: perm
env: env:
TOKEN: ${{ secrets.MOKOGITEA_TOKEN || github.token }} TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}
REPO: ${{ github.repository }} REPO: ${{ github.repository }}
ACTOR: ${{ github.actor }} ACTOR: ${{ github.actor }}
run: | run: |
@@ -88,7 +88,7 @@ jobs:
# Hardcoded authorized users — always allowed # Hardcoded authorized users — always allowed
case "$ACTOR" in case "$ACTOR" in
jmiller|mokogitea-actions[bot]) jmiller|gitea-actions[bot])
ALLOWED=true ALLOWED=true
PERMISSION=admin PERMISSION=admin
METHOD="hardcoded allowlist" METHOD="hardcoded allowlist"
@@ -671,30 +671,42 @@ jobs:
# ═══════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════
# Issue Reporter — file issues for failed gates # Issue Reporter — file issues for failed gates
# ═══════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════
report-scripts: report-issues:
name: "Report: Scripts Governance" name: "Report Issues"
needs: [access_check, scripts_governance] runs-on: ubuntu-latest
needs: [access_check, scripts_governance, repo_health]
if: >- if: >-
always() && always() &&
needs.scripts_governance.result == 'failure' (needs.scripts_governance.result == 'failure' ||
uses: ./.mokogitea/workflows/ci-issue-reporter.yml needs.repo_health.result == 'failure')
with:
gate: "Scripts Governance"
workflow: "Repo Health"
severity: error
details: "Scripts directory policy violations detected. Review required and allowed directories."
secrets: inherit
report-health: steps:
name: "Report: Repository Health" - name: Checkout
needs: [access_check, repo_health] uses: actions/checkout@v4
if: >-
always() &&
needs.repo_health.result == 'failure'
uses: ./.mokogitea/workflows/ci-issue-reporter.yml
with: with:
gate: "Repository Health" sparse-checkout: automation/ci-issue-reporter.sh
workflow: "Repo Health" sparse-checkout-cone-mode: false
severity: error
details: "Repository health checks failed — missing required artifacts, disallowed files, or content warnings. Check the CI run summary." - name: "File issues for failed gates"
secrets: inherit env:
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
run: |
chmod +x automation/ci-issue-reporter.sh
REPORTER="./automation/ci-issue-reporter.sh"
WF="Repo Health"
report_gate() {
local gate="$1" result="$2" details="$3"
if [ "$result" = "failure" ]; then
"$REPORTER" --gate "$gate" --details "$details" --workflow "$WF" --severity error
fi
}
report_gate "Scripts Governance" \
"${{ needs.scripts_governance.result }}" \
"Scripts directory policy violations detected. Review required and allowed directories."
report_gate "Repository Health" \
"${{ needs.repo_health.result }}" \
"Repository health checks failed — missing required artifacts, disallowed files, or content warnings. Check the CI run summary."
-32
View File
@@ -1,32 +0,0 @@
name: Sync Workflows to Repos
on:
push:
branches:
- main
paths:
- '.mokogitea/workflows/**'
jobs:
sync:
runs-on: ubuntu-latest
if: ${{ startsWith(github.event.repository.name, 'Template-') }}
steps:
- name: Checkout mokocli
uses: actions/checkout@v4
with:
repository: MokoConsulting/MokoCLI
token: ${{ secrets.MOKOGITEA_TOKEN }}
- name: Setup PHP
uses: https://git.mokoconsulting.tech/MokoConsulting/.mokogitea/raw/branch/main/actions/setup-php@v1
with:
php-version: '8.1'
- name: Install dependencies
run: composer install --no-dev --no-interaction
- name: Sync workflows to generic repos
run: php automation/bulk_sync.php --platform generic --org MokoConsulting --workflows-only --auto-merge --token "${{ secrets.MOKOGITEA_TOKEN }}"
env:
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
+2 -3
View File
@@ -3,7 +3,7 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: MokoGitea.Workflow.Template # DEFGROUP: Gitea.Workflow.Template
# INGROUP: MokoStandards.CI # INGROUP: MokoStandards.CI
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla # REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla
# PATH: /.mokogitea/workflows/version-set.yml # PATH: /.mokogitea/workflows/version-set.yml
@@ -34,7 +34,6 @@ jobs:
set-version: set-version:
name: Set Version to ${{ inputs.version }} name: Set Version to ${{ inputs.version }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ !startsWith(github.event.repository.name, 'Template-') }}
steps: steps:
- name: Validate version format - name: Validate version format
@@ -49,7 +48,7 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
token: ${{ secrets.MOKOGITEA_TOKEN || github.token }} token: ${{ secrets.MOKOGITEA_TOKEN || secrets.GA_TOKEN || github.token }}
ref: ${{ inputs.branch || github.ref }} ref: ${{ inputs.branch || github.ref }}
fetch-depth: 1 fetch-depth: 1
+6 -15
View File
@@ -3,9 +3,9 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: MokoGitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Universal # INGROUP: mokocli.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic # REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /.mokogitea/workflows/workflow-sync-trigger.yml # PATH: /.mokogitea/workflows/workflow-sync-trigger.yml
# VERSION: 01.01.00 # VERSION: 01.01.00
# BRIEF: Trigger workflow sync to live repos when a PR is merged to main # BRIEF: Trigger workflow sync to live repos when a PR is merged to main
@@ -13,7 +13,6 @@
name: "Universal: Workflow Sync Trigger" name: "Universal: Workflow Sync Trigger"
on: on:
workflow_dispatch:
pull_request: pull_request:
types: [closed] types: [closed]
branches: branches:
@@ -27,10 +26,8 @@ jobs:
name: Sync workflows to live repos name: Sync workflows to live repos
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: >- if: >-
startsWith(github.event.repository.name, 'Template-') && github.event.pull_request.merged == true &&
(github.event_name == 'workflow_dispatch' || !contains(github.event.pull_request.title, '[skip sync]')
(github.event.pull_request.merged == true &&
!contains(github.event.pull_request.title, '[skip sync]')))
steps: steps:
- name: Determine platform from repo name - name: Determine platform from repo name
@@ -52,14 +49,8 @@ jobs:
env: env:
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
run: | run: |
MOKOGITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}" GITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}"
git clone --depth 1 "${MOKOGITEA_URL}/MokoConsulting/MokoCLI.git" /tmp/mokocli git clone --depth 1 "${GITEA_URL}/MokoConsulting/mokocli.git" /tmp/mokocli
- name: Install PHP
run: |
if ! command -v php &> /dev/null; then
apt-get update -qq && apt-get install -y -qq php-cli php-json php-curl > /dev/null 2>&1
fi
- name: Install dependencies - name: Install dependencies
run: | run: |
+71 -36
View File
@@ -1,32 +1,7 @@
# Changelog # Changelog
## [Unreleased] ## [Unreleased]
## [01.14.00] --- 2026-07-13
### Added ### Added
- **Admin menu short-name constant** — the Administrator → Components menu now shows a compact label via `COM_MOKOSUITECROSS_SHORT` ("Cross"); defined in the admin sys.ini (#255)
## [01.13.00] --- 2026-06-29
## [01.13.00] --- 2026-06-29
## [01.12.00] --- 2026-06-28
## [01.12.00] --- 2026-06-28
### Added
- **Visual post calendar**: Monthly calendar grid view showing scheduled, queued, and posted cross-posts with status badges (#160)
- **Calendar navigation**: Month-by-month navigation with today highlighting (#160)
- **Posting analytics**: Best time to post heatmap with day-of-week and hour-of-day breakdown (#165)
- **Analytics service filter**: Filter heatmap and stats by service type with configurable date range
- **Analytics service breakdown**: Per-service success rate, failure count, and average posts per day
- **Analytics AJAX endpoint**: JSON heatmap data for dynamic filtering without page reload
- **Social image generator**: Generate branded 1200x630 OG images with article title overlay using PHP GD (#157)
- **Social image config**: Background color, text color, font size, and site name branding options (#157)
- **Generate Social Image button**: One-click image generation in the Share Content panel (#157)
- **AI caption generation**: Generate platform-optimized cross-post captions from article content using Claude or OpenAI (#161)
- **AI provider config**: New "AI Caption Generation" fieldset in component options with provider, API key, model, and tone settings
- **AI Generate button**: One-click AI generation button in the Share Content panel that fills all caption fields
- **X/Twitter threads**: Auto-split messages exceeding 280 chars into reply chains at sentence boundaries - **X/Twitter threads**: Auto-split messages exceeding 280 chars into reply chains at sentence boundaries
- **X/Twitter cost-optimized posting**: Optional mode to post text-only tweet first ($0.015) with URL as separate reply ($0.20) - **X/Twitter cost-optimized posting**: Optional mode to post text-only tweet first ($0.015) with URL as separate reply ($0.20)
- **X/Twitter cost warning**: Language string documenting X API pricing for text vs URL posts - **X/Twitter cost warning**: Language string documenting X API pricing for text vs URL posts
@@ -46,16 +21,76 @@
- **Facebook Stories**: Publish image and video Stories via photo_stories/video_stories endpoints (#162) - **Facebook Stories**: Publish image and video Stories via photo_stories/video_stories endpoints (#162)
- **Facebook scheduled posts**: Schedule feed posts with scheduled_publish_time parameter (#162) - **Facebook scheduled posts**: Schedule feed posts with scheduled_publish_time parameter (#162)
- **Facebook draft posts**: Save feed posts as unpublished drafts (#162) - **Facebook draft posts**: Save feed posts as unpublished drafts (#162)
- **TikTok video upload**: PULL_FROM_URL video publishing via video/init endpoint with status polling (#164)
- **TikTok photo carousel**: Up to 35 image carousel posts via content/init endpoint (#164)
- **TikTok posting mode**: Configurable DIRECT_POST or MEDIA_UPLOAD (sends to TikTok inbox for in-app editing) (#164)
- **TikTok audit warning**: Language string explaining that unverified apps can only create private posts (#164)
- **Link shortening**: Shorten URLs via Bitly, Rebrandly, or YOURLS with {url_short} placeholder (#159)
- **Site frontend**: Public-facing cross-post list and detail views for site visitors (#133)
- **Social preview**: AJAX platform mockups (Twitter, Facebook, LinkedIn, Instagram, Mastodon, Bluesky) in article editor (#156)
- **PHPUnit test suite**: Unit tests for models, helpers, and service plugins (#132)
### Fixed ### Fixed
- **Content plugin**: Remove Joomla 5 typed event hints -- Joomla 6 dispatches `Model\AfterSaveEvent` instead of `Content\AfterSaveEvent`, causing fatal TypeError on article save - Webservices plugin Joomla 6 compatibility — `onBeforeApiRoute` receives `BeforeApiRouteEvent` object, extract router via `$event->getRouter()`
- **PreviewController**: Add ACL check and parameterized query to prevent unauthorized article access (IDOR)
- Webservices plugin Joomla 6 compatibility -- `onBeforeApiRoute` receives `BeforeApiRouteEvent` object, extract router via `$event->getRouter()` ## [01.07.00] --- 2026-06-23
## [01.07.00] --- 2026-06-23
### Added
- **Full ACL system**: 12 granular permissions in access.xml with permissions fieldset in config.xml
- **ACL enforcement**: All controllers and views check permissions before allowing actions
- **MokoSuiteCrossHelper::getActions()**: Centralized ACL helper for toolbar and view logic
### Fixed
- **License warning**: Removed duplicate from system plugin (install script already shows it)
- **Content plugin**: Fixed func_get_arg crash when non-article content is saved (e.g. update sites, installer)
## [01.05.00] --- 2026-06-23
## [01.05.00] --- 2026-06-23
### Added
- **Instagram plugin**: Cross-post to Instagram via Meta Content Publishing API (2-step container flow)
- **YouTube plugin**: Cross-post to YouTube via Data API v3 channel bulletins
- **Share Content panel**: Per-article editor panel with platform-specific share text fields
- **New placeholders**: {social}, {short}, {chat}, {email_subject}, {email_body} for platform-optimized templates
- **Share image control**: Choose intro image, fulltext image, custom image, or no image per article
- **Mailchimp templates**: Support Mailchimp saved templates with section injection, plus responsive email wrapper fallback
- **Delete from platforms**: New MokoSuiteCrossDeleteInterface for removing cross-posted content from remote platforms
- **Delete support**: Twitter, Mastodon, Bluesky, Facebook, LinkedIn, Telegram, Discord (7 of 38 plugins)
- **Auto-delete on unpublish**: Component config option to delete from platforms when articles are unpublished or trashed
- **UTM auto-tagging**: Append utm_source, utm_medium, utm_campaign to shared URLs with {platform} token support
- **Caption rotation**: {random:opt1|opt2|opt3} placeholder picks a random option per post
- **{url_raw} placeholder**: Clean article URL without UTM parameters
- **Mastodon enhancements**: Visibility levels, content warnings, scheduled posts, polls, language tags
- **Bluesky threads**: Auto-split long messages into reply chains at sentence boundaries
- **Bluesky link cards**: External link card embeds with article title and description
- **Ntfy default server**: Default server changed to ntfy.mokoconsulting.tech with configurable plugin params
### Changed
- **Default templates**: Updated to use platform-specific placeholders (social/short/chat/email) with graceful fallback
### Fixed
- **Mailchimp**: Fixed broken namespace placeholder in XML manifest
- **ConvertKit**: Removed duplicate curl_setopt_array with undefined $token
- **Brevo**: Removed duplicate curl_setopt_array with undefined $token and wrong auth header
- **Constant Contact**: Removed duplicate curl_setopt_array
- **Mailchimp**: Fixed campaign creation checking HTTP 200 instead of 2xx range
- **Medium**: Fixed getUserId() returning array instead of string on error
- **Bluesky**: Replaced md5() with hash('sha256', ...) for cache key
- **ServiceController**: Exception details no longer exposed to client
- **License warning**: Removed duplicate from system plugin -- install script already shows it with direct edit link
## [01.04.01] --- 2026-06-21
## [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.08.42 -->
All notable changes to MokoSuiteCross will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
+6 -6
View File
@@ -13,7 +13,7 @@ This file provides guidance to Claude Code when working with this repository.
| **Default branch** | main | | **Default branch** | main |
| **License** | GPL-3.0-or-later | | **License** | GPL-3.0-or-later |
| **Wiki** | [MokoSuiteCross Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteCross/wiki) | | **Wiki** | [MokoSuiteCross Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteCross/wiki) |
| **Standards** | [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/.mokogitea/wiki) | | **Standards** | [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home) |
## Common Commands ## Common Commands
@@ -36,7 +36,7 @@ This is a Joomla **package** extension (`pkg_mokosuitecross`) containing sub-ext
### com_mokosuitecross (Component) ### com_mokosuitecross (Component)
- Admin backend for managing services, post queue, templates, and logs - Admin backend for managing services, post queue, templates, and logs
- Joomla 6 MVC: Dashboard, Services, Posts, Logs (list/edit each) - Joomla 5/6 MVC: Dashboard, Services, Posts, Logs (list/edit each)
- Namespace: `Joomla\Component\MokoSuiteCross\Administrator` - Namespace: `Joomla\Component\MokoSuiteCross\Administrator`
- Database tables: `#__mokosuitecross_services`, `#__mokosuitecross_posts`, `#__mokosuitecross_templates`, `#__mokosuitecross_logs` - Database tables: `#__mokosuitecross_services`, `#__mokosuitecross_posts`, `#__mokosuitecross_templates`, `#__mokosuitecross_logs`
@@ -97,13 +97,13 @@ Four tables:
- **Attribution**: use `Authored-by: Moko Consulting` in commits - **Attribution**: use `Authored-by: Moko Consulting` in commits
- **Branch strategy**: develop on `dev`, merge to `main` for release - **Branch strategy**: develop on `dev`, merge to `main` for release
- **Minification**: handled at build time (CI) - **Minification**: handled at build time (CI)
- **Wiki**: documentation lives in the MokoGitea wiki, not in `docs/` files - **Wiki**: documentation lives in the Gitea wiki, not in `docs/` files
- **Standards**: this repo follows [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/.mokogitea/wiki) - **Standards**: this repo follows [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)
## Coding Standards ## Coding Standards
- PHP 8.3+ minimum - PHP 8.1+ minimum
- Joomla 6 DI container pattern: `services/provider.php` → Extension class - Joomla 5/6 DI container pattern: `services/provider.php` → Extension class
- Legacy stub `.php` file required for plugin loader but empty - Legacy stub `.php` file required for plugin loader but empty
- `SubscriberInterface` for event subscription (not `on*` method naming) - `SubscriberInterface` for event subscription (not `on*` method naming)
- `bind() → check() → store()` for Table operations (not `save()`) - `bind() → check() → store()` for Table operations (not `save()`)
+1 -1
View File
@@ -14,7 +14,7 @@
DEFGROUP: Template-Joomla DEFGROUP: Template-Joomla
INGROUP: Template-Joomla.Documentation INGROUP: Template-Joomla.Documentation
REPO: https://github.com/mokoconsulting-tech/Template-Joomla/ REPO: https://github.com/mokoconsulting-tech/Template-Joomla/
VERSION: 01.14.00 VERSION: 01.08.42
PATH: ./CODE_OF_CONDUCT.md PATH: ./CODE_OF_CONDUCT.md
BRIEF: Community expectations and enforcement guidelines BRIEF: Community expectations and enforcement guidelines
NOTE: Adapted with attribution from the Contributor Covenant v2.1 NOTE: Adapted with attribution from the Contributor Covenant v2.1
+1 -1
View File
@@ -34,7 +34,7 @@ feature/* ──PR──> dev ──draft PR──> (renamed to rc) ──merge
7. **Merging to main** triggers the stable release pipeline: 7. **Merging to main** triggers the stable release pipeline:
- Minor version bump (e.g., `02.09.xx` → `02.10.00`) - Minor version bump (e.g., `02.09.xx` → `02.10.00`)
- Stability suffix stripped (clean version) - Stability suffix stripped (clean version)
- MokoGitea release created with ZIP/tar.gz packages - Gitea release created with ZIP/tar.gz packages
- `updates.xml` updated (Joomla extensions) - `updates.xml` updated (Joomla extensions)
- `dev` branch recreated from `main` - `dev` branch recreated from `main`
+1 -119
View File
File diff suppressed because one or more lines are too long
+3 -9
View File
@@ -1,8 +1,8 @@
# MokoSuiteCross # MokoSuiteCross
<!-- VERSION: 01.14.00 --> <!-- VERSION: 01.08.42 -->
Cross-posting Joomla content to social media, email marketing, and chat platforms for Joomla 6. Cross-posting Joomla content to social media, email marketing, and chat platforms for Joomla 5/6.
## Overview ## Overview
@@ -16,14 +16,8 @@ MokoSuiteCross automatically publishes your Joomla articles to multiple platform
- **Post queue** — Scheduled posting, retry on failure, detailed delivery logs - **Post queue** — Scheduled posting, retry on failure, detailed delivery logs
- **Message templates** — Customize post format per platform with placeholders ({title}, {url}, {social}, {short}, {chat}, {email_subject}, {email_body}, {field:xxx}) - **Message templates** — Customize post format per platform with placeholders ({title}, {url}, {social}, {short}, {chat}, {email_subject}, {email_body}, {field:xxx})
- **Share Content panel** — Per-article fields for platform-optimized text (social, short, chat, email) with image picker - **Share Content panel** — Per-article fields for platform-optimized text (social, short, chat, email) with image picker
- **AI caption generation** — Generate platform-optimized captions using Claude or OpenAI with one click
- **Social preview** — AJAX platform mockups (Twitter, Facebook, LinkedIn, Instagram, Mastodon, Bluesky) in the article editor
- **Social image generator** — Generate Open Graph images with article title overlay using PHP GD
- **Link shortening** — Shorten URLs via Bitly, Rebrandly, or YOURLS with {url_short} placeholder
- **Caption rotation** — {random:opt1|opt2|opt3} placeholder for varying evergreen re-shares - **Caption rotation** — {random:opt1|opt2|opt3} placeholder for varying evergreen re-shares
- **UTM tracking** — Auto-append UTM parameters to shared links with {platform} token - **UTM tracking** — Auto-append UTM parameters to shared links with {platform} token
- **Post calendar** — Visual monthly calendar view of scheduled and completed cross-posts
- **Posting analytics** — Best time to post heatmap with per-service breakdown and recommendations
- **Delete from platforms** — Remove cross-posted content when articles are unpublished/trashed (7 platforms) - **Delete from platforms** — Remove cross-posted content when articles are unpublished/trashed (7 platforms)
- **Post history** — Track what was posted where, with platform response data - **Post history** — Track what was posted where, with platform response data
- **Evergreen re-sharing** — Automatically re-share articles on a configurable interval - **Evergreen re-sharing** — Automatically re-share articles on a configurable interval
@@ -88,7 +82,7 @@ MokoSuiteCross automatically publishes your Joomla articles to multiple platform
| RSS Feed | `plg_mokosuitecross_rssfeed` | Implemented | | RSS Feed | `plg_mokosuitecross_rssfeed` | Implemented |
| ActivityPub | `plg_mokosuitecross_activitypub` | Implemented | | ActivityPub | `plg_mokosuitecross_activitypub` | Implemented |
| Google Business | `plg_mokosuitecross_googlebusiness` | Implemented | | Google Business | `plg_mokosuitecross_googlebusiness` | Implemented |
| Nostr | `plg_mokosuitecross_nostr` | Implemented | | Nostr | `plg_mokosuitecross_nostr` | Stub (WebSocket deferred) |
## Installation ## Installation
+1 -1
View File
@@ -23,7 +23,7 @@ DEFGROUP: Template-Joomla
INGROUP: Template-Joomla.Documentation INGROUP: Template-Joomla.Documentation
REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla
PATH: /SECURITY.md PATH: /SECURITY.md
VERSION: 01.14.00 VERSION: 01.08.42
BRIEF: Security vulnerability reporting and handling policy BRIEF: Security vulnerability reporting and handling policy
--> -->
+1 -1
View File
@@ -12,7 +12,7 @@
} }
], ],
"require": { "require": {
"php": ">=8.3" "php": ">=8.1"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^10.5", "phpunit/phpunit": "^10.5",
@@ -120,42 +120,6 @@
/> />
</fieldset> </fieldset>
<fieldset name="link_shortening" label="COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENING">
<field
name="link_shortener"
type="list"
label="COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENER"
description="COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENER_DESC"
default="none">
<option value="none">COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENER_NONE</option>
<option value="bitly">COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENER_BITLY</option>
<option value="rebrandly">COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENER_REBRANDLY</option>
<option value="yourls">COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENER_YOURLS</option>
</field>
<field
name="link_shortener_api_key"
type="text"
label="COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENER_API_KEY"
description="COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENER_API_KEY_DESC"
showon="link_shortener:bitly,rebrandly"
/>
<field
name="link_shortener_yourls_url"
type="url"
label="COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENER_YOURLS_URL"
description="COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENER_YOURLS_URL_DESC"
hint="https://short.example.com/yourls-api.php"
showon="link_shortener:yourls"
/>
<field
name="link_shortener_yourls_token"
type="text"
label="COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENER_YOURLS_TOKEN"
description="COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENER_YOURLS_TOKEN_DESC"
showon="link_shortener:yourls"
/>
</fieldset>
<fieldset name="evergreen" label="COM_MOKOSUITECROSS_CONFIG_EVERGREEN"> <fieldset name="evergreen" label="COM_MOKOSUITECROSS_CONFIG_EVERGREEN">
<field <field
name="evergreen_enabled" name="evergreen_enabled"
@@ -227,95 +191,6 @@
/> />
</fieldset> </fieldset>
<fieldset name="ai" label="COM_MOKOSUITECROSS_CONFIG_AI">
<field
name="ai_provider"
type="list"
label="COM_MOKOSUITECROSS_CONFIG_AI_PROVIDER"
description="COM_MOKOSUITECROSS_CONFIG_AI_PROVIDER_DESC"
default="none">
<option value="none">COM_MOKOSUITECROSS_CONFIG_AI_PROVIDER_NONE</option>
<option value="claude">COM_MOKOSUITECROSS_CONFIG_AI_PROVIDER_CLAUDE</option>
<option value="openai">COM_MOKOSUITECROSS_CONFIG_AI_PROVIDER_OPENAI</option>
</field>
<field
name="ai_api_key"
type="text"
label="COM_MOKOSUITECROSS_CONFIG_AI_API_KEY"
description="COM_MOKOSUITECROSS_CONFIG_AI_API_KEY_DESC"
showon="ai_provider:claude,openai"
/>
<field
name="ai_model"
type="text"
label="COM_MOKOSUITECROSS_CONFIG_AI_MODEL"
description="COM_MOKOSUITECROSS_CONFIG_AI_MODEL_DESC"
hint="claude-haiku-4-5 / gpt-4o-mini"
showon="ai_provider:claude,openai"
/>
<field
name="ai_tone"
type="list"
label="COM_MOKOSUITECROSS_CONFIG_AI_TONE"
description="COM_MOKOSUITECROSS_CONFIG_AI_TONE_DESC"
default="professional"
showon="ai_provider:claude,openai">
<option value="professional">COM_MOKOSUITECROSS_CONFIG_AI_TONE_PROFESSIONAL</option>
<option value="friendly">COM_MOKOSUITECROSS_CONFIG_AI_TONE_FRIENDLY</option>
<option value="casual">COM_MOKOSUITECROSS_CONFIG_AI_TONE_CASUAL</option>
</field>
</fieldset>
<fieldset name="social_image" label="COM_MOKOSUITECROSS_CONFIG_SOCIAL_IMAGE">
<field
name="social_image_enabled"
type="radio"
label="COM_MOKOSUITECROSS_CONFIG_SOCIAL_IMAGE_ENABLED"
description="COM_MOKOSUITECROSS_CONFIG_SOCIAL_IMAGE_ENABLED_DESC"
default="0"
class="btn-group">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field
name="social_image_bg_color"
type="text"
label="COM_MOKOSUITECROSS_CONFIG_SOCIAL_IMAGE_BG_COLOR"
description="COM_MOKOSUITECROSS_CONFIG_SOCIAL_IMAGE_BG_COLOR_DESC"
default="#1a1a2e"
showon="social_image_enabled:1"
/>
<field
name="social_image_text_color"
type="text"
label="COM_MOKOSUITECROSS_CONFIG_SOCIAL_IMAGE_TEXT_COLOR"
description="COM_MOKOSUITECROSS_CONFIG_SOCIAL_IMAGE_TEXT_COLOR_DESC"
default="#ffffff"
showon="social_image_enabled:1"
/>
<field
name="social_image_font_size"
type="number"
label="COM_MOKOSUITECROSS_CONFIG_SOCIAL_IMAGE_FONT_SIZE"
description="COM_MOKOSUITECROSS_CONFIG_SOCIAL_IMAGE_FONT_SIZE_DESC"
default="48"
min="24"
max="96"
showon="social_image_enabled:1"
/>
<field
name="social_image_show_site_name"
type="radio"
label="COM_MOKOSUITECROSS_CONFIG_SOCIAL_IMAGE_SHOW_SITE_NAME"
description="COM_MOKOSUITECROSS_CONFIG_SOCIAL_IMAGE_SHOW_SITE_NAME_DESC"
default="1"
class="btn-group"
showon="social_image_enabled:1">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
</fieldset>
<fieldset name="category_rules" label="COM_MOKOSUITECROSS_CONFIG_CATEGORY_RULES"> <fieldset name="category_rules" label="COM_MOKOSUITECROSS_CONFIG_CATEGORY_RULES">
<field <field
name="category_rules_note" name="category_rules_note"
@@ -534,58 +534,7 @@ COM_MOKOSUITECROSS_DISPATCH_INVALID_SERVICES="service_ids must be a non-empty ar
COM_MOKOSUITECROSS_DISPATCH_ARTICLE_NOT_FOUND="Article not found." COM_MOKOSUITECROSS_DISPATCH_ARTICLE_NOT_FOUND="Article not found."
COM_MOKOSUITECROSS_DISPATCH_NO_SERVICES="No enabled services found matching the request." COM_MOKOSUITECROSS_DISPATCH_NO_SERVICES="No enabled services found matching the request."
; Link Shortening
COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENING="Link Shortening"
COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENER="Link Shortener"
COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENER_DESC="Select a link shortening service. Shortened URLs are available via the {url_short} placeholder in templates."
COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENER_NONE="None (disabled)"
COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENER_BITLY="Bitly"
COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENER_REBRANDLY="Rebrandly"
COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENER_YOURLS="YOURLS (self-hosted)"
COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENER_API_KEY="API Key"
COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENER_API_KEY_DESC="API key for Bitly or Rebrandly."
COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENER_YOURLS_URL="YOURLS API URL"
COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENER_YOURLS_URL_DESC="Full URL to your YOURLS API endpoint (e.g. https://short.example.com/yourls-api.php)."
COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENER_YOURLS_TOKEN="YOURLS Signature Token"
COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENER_YOURLS_TOKEN_DESC="Secret signature token from your YOURLS installation."
; AI Caption Generation
COM_MOKOSUITECROSS_CONFIG_AI="AI Caption Generation"
COM_MOKOSUITECROSS_CONFIG_AI_PROVIDER="AI Provider"
COM_MOKOSUITECROSS_CONFIG_AI_PROVIDER_DESC="Select an AI provider to generate cross-post captions from article content. The API key is stored in Joomla component params (encrypted at rest)."
COM_MOKOSUITECROSS_CONFIG_AI_PROVIDER_NONE="None (disabled)"
COM_MOKOSUITECROSS_CONFIG_AI_PROVIDER_CLAUDE="Anthropic Claude"
COM_MOKOSUITECROSS_CONFIG_AI_PROVIDER_OPENAI="OpenAI"
COM_MOKOSUITECROSS_CONFIG_AI_API_KEY="API Key"
COM_MOKOSUITECROSS_CONFIG_AI_API_KEY_DESC="API key for the selected AI provider."
COM_MOKOSUITECROSS_CONFIG_AI_MODEL="Model"
COM_MOKOSUITECROSS_CONFIG_AI_MODEL_DESC="AI model to use. Leave blank for the default (Claude Haiku 4.5 or GPT-4o Mini)."
COM_MOKOSUITECROSS_CONFIG_AI_TONE="Tone"
COM_MOKOSUITECROSS_CONFIG_AI_TONE_DESC="The writing tone for generated captions."
COM_MOKOSUITECROSS_CONFIG_AI_TONE_PROFESSIONAL="Professional"
COM_MOKOSUITECROSS_CONFIG_AI_TONE_FRIENDLY="Friendly"
COM_MOKOSUITECROSS_CONFIG_AI_TONE_CASUAL="Casual"
COM_MOKOSUITECROSS_AI_GENERATE="Generate with AI"
COM_MOKOSUITECROSS_AI_GENERATE_DESC="Generate platform-optimized captions from the article content using AI."
COM_MOKOSUITECROSS_AI_GENERATING="Generating captions..."
COM_MOKOSUITECROSS_AI_GENERATED="AI captions generated successfully."
COM_MOKOSUITECROSS_AI_ERROR="AI generation failed: %s"
COM_MOKOSUITECROSS_AI_NOT_CONFIGURED="AI is not configured. Go to Options to set up a provider and API key."
; Category Rules ; Category Rules
COM_MOKOSUITECROSS_CONFIG_CATEGORY_RULES="Category Rules" COM_MOKOSUITECROSS_CONFIG_CATEGORY_RULES="Category Rules"
COM_MOKOSUITECROSS_CONFIG_CATEGORY_RULES_NOTE="Category Routing" COM_MOKOSUITECROSS_CONFIG_CATEGORY_RULES_NOTE="Category Routing"
COM_MOKOSUITECROSS_CONFIG_CATEGORY_RULES_NOTE_DESC="Category routing rules let you map Joomla categories to specific cross-post services. When rules exist for a category, only those services receive posts. When no rules exist, all services are used (default behaviour). Rules are managed in the database table #__mokosuitecross_category_rules. A full admin UI will be added in a future release." COM_MOKOSUITECROSS_CONFIG_CATEGORY_RULES_NOTE_DESC="Category routing rules let you map Joomla categories to specific cross-post services. When rules exist for a category, only those services receive posts. When no rules exist, all services are used (default behaviour). Rules are managed in the database table #__mokosuitecross_category_rules. A full admin UI will be added in a future release."
; Post Calendar
COM_MOKOSUITECROSS_CALENDAR="Post Calendar"
COM_MOKOSUITECROSS_CALENDAR_DESC="Visual calendar of scheduled and posted content"
COM_MOKOSUITECROSS_SUBMENU_CALENDAR="Calendar"
COM_MOKOSUITECROSS_CALENDAR_TODAY="Today"
COM_MOKOSUITECROSS_CALENDAR_MONTH="Month"
COM_MOKOSUITECROSS_CALENDAR_WEEK="Week"
COM_MOKOSUITECROSS_CALENDAR_LIST="List"
COM_MOKOSUITECROSS_CALENDAR_RESCHEDULE_SUCCESS="Post rescheduled successfully"
COM_MOKOSUITECROSS_CALENDAR_RESCHEDULE_ERROR="Failed to reschedule post"
COM_MOKOSUITECROSS_CALENDAR_CANNOT_RESCHEDULE="Only scheduled or queued posts can be rescheduled"
COM_MOKOSUITECROSS_CALENDAR_LOAD_ERROR="Failed to load calendar events"
@@ -3,7 +3,6 @@
; License: GPL-3.0-or-later ; License: GPL-3.0-or-later
COM_MOKOSUITECROSS="MokoSuiteCross" COM_MOKOSUITECROSS="MokoSuiteCross"
COM_MOKOSUITECROSS_SHORT="Cross"
COM_MOKOSUITECROSS_DESCRIPTION="Cross-posting Joomla content to social media, email marketing, and chat platforms" COM_MOKOSUITECROSS_DESCRIPTION="Cross-posting Joomla content to social media, email marketing, and chat platforms"
COM_MOKOSUITECROSS_SUBMENU_DASHBOARD="Dashboard" COM_MOKOSUITECROSS_SUBMENU_DASHBOARD="Dashboard"
COM_MOKOSUITECROSS_SUBMENU_POSTS="Post Queue" COM_MOKOSUITECROSS_SUBMENU_POSTS="Post Queue"
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<extension type="component" method="upgrade"> <extension type="component" method="upgrade">
<name>com_mokosuitecross</name> <name>com_mokosuitecross</name>
<version>01.14.00</version> <version>01.08.42</version>
<creationDate>2026-05-28</creationDate> <creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author> <author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -43,7 +43,7 @@
</languages> </languages>
<administration> <administration>
<menu img="class:share-alt">COM_MOKOSUITECROSS_SHORT</menu> <menu img="class:share-alt">COM_MOKOSUITECROSS</menu>
<submenu> <submenu>
<menu link="option=com_mokosuitecross&amp;view=dashboard">COM_MOKOSUITECROSS_SUBMENU_DASHBOARD</menu> <menu link="option=com_mokosuitecross&amp;view=dashboard">COM_MOKOSUITECROSS_SUBMENU_DASHBOARD</menu>
<menu link="option=com_mokosuitecross&amp;view=posts">COM_MOKOSUITECROSS_SUBMENU_POSTS</menu> <menu link="option=com_mokosuitecross&amp;view=posts">COM_MOKOSUITECROSS_SUBMENU_POSTS</menu>
@@ -11,7 +11,6 @@
defined('_JEXEC') or die; defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Installer\InstallerAdapter; use Joomla\CMS\Installer\InstallerAdapter;
class Com_MokoSuiteCrossInstallerScript class Com_MokoSuiteCrossInstallerScript
@@ -23,77 +22,5 @@ class Com_MokoSuiteCrossInstallerScript
public function postflight(string $type, InstallerAdapter $parent): void public function postflight(string $type, InstallerAdapter $parent): void
{ {
if ($type !== 'install') {
return;
}
// Be smart: verify EVERY table the component's own install SQL declares is
// actually present. If any are missing the install did not complete — say
// so instead of implying "installed successfully".
$missing = $this->missingTables();
if ($missing !== []) {
$detail = count($missing) > 6
? count($missing) . ' expected database tables are missing'
: 'missing tables: ' . htmlspecialchars(implode(', ', $missing), ENT_QUOTES, 'UTF-8');
Factory::getApplication()->enqueueMessage(
'<strong>MokoSuiteCross did not install correctly.</strong> ' . $detail
. ' — review the errors above, then reinstall.',
'error'
);
return;
}
// Success path: schema installed correctly.
Factory::getApplication()->enqueueMessage(
'<h4>MokoSuiteCross installed successfully!</h4>'
. '<p>To enable automatic updates, enter your license key in the '
. '<a href="index.php?option=com_config&view=component&component=com_mokosuitecross">component configuration</a>.</p>',
'info'
);
}
/**
* Smart schema check: parse the component's own installed install SQL for every
* CREATE TABLE, then return the ones missing from the database. An empty array
* means the full schema installed. Fails open (returns []) if the SQL can't be
* read, so a good install is never flagged as failed.
*
* @return string[] Names (without the #__ prefix) of missing tables.
*/
private function missingTables(): array
{
try {
$sqlFile = JPATH_ADMINISTRATOR . '/components/com_mokosuitecross/sql/install.mysql.sql';
if (!is_file($sqlFile)) {
return [];
}
$sql = file_get_contents($sqlFile);
if ($sql === false
|| !preg_match_all('/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"\']?(#__[A-Za-z0-9_]+)/i', $sql, $m)) {
return [];
}
$db = Factory::getDbo();
$prefix = strtolower($db->getPrefix());
$have = array_map('strtolower', $db->getTableList());
$missing = [];
foreach (array_unique($m[1]) as $decl) {
$bare = substr($decl, 3); // strip "#__"
if (!in_array($prefix . strtolower($bare), $have, true)) {
$missing[] = $bare;
}
}
return $missing;
} catch (\Throwable $e) {
return [];
}
} }
} }
@@ -1,14 +1,5 @@
; MokoSuiteCross -- Site Frontend Language File ; MokoSuiteCross Site Frontend Language File
; Copyright (C) 2026 Moko Consulting. All rights reserved. ; Copyright (C) 2026 Moko Consulting. All rights reserved.
; License: GPL-3.0-or-later ; License: GPL-3.0-or-later
COM_MOKOSUITECROSS="MokoSuiteCross" COM_MOKOSUITECROSS="MokoSuiteCross"
COM_MOKOSUITECROSS_POSTS_LIST_TITLE="Cross-Posted Content"
COM_MOKOSUITECROSS_POST_DETAIL_TITLE="Cross-Post History"
COM_MOKOSUITECROSS_COLUMN_ARTICLE="Article"
COM_MOKOSUITECROSS_COLUMN_PLATFORMS="Platforms"
COM_MOKOSUITECROSS_COLUMN_LAST_POSTED="Last Posted"
COM_MOKOSUITECROSS_COLUMN_STATUS="Status"
COM_MOKOSUITECROSS_COLUMN_POSTED_DATE="Posted Date"
COM_MOKOSUITECROSS_COLUMN_LINK="Platform Link"
COM_MOKOSUITECROSS_NO_POSTS="No cross-posted content found."
@@ -17,5 +17,5 @@ use Joomla\CMS\MVC\Controller\BaseController;
class DisplayController extends BaseController class DisplayController extends BaseController
{ {
protected $default_view = 'posts'; protected $default_view = 'post';
} }
@@ -1,64 +0,0 @@
<?php
/**
* @package MokoSuiteCross
* @subpackage com_mokosuitecross
* @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\MokoSuiteCross\Site\Model;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
class PostModel extends BaseDatabaseModel
{
public function getArticle(int $articleId): ?object
{
$db = $this->getDatabase();
$user = Factory::getApplication()->getIdentity();
$query = $db->getQuery(true)
->select('a.id, a.title, a.alias, a.catid, a.access')
->from($db->quoteName('#__content', 'a'))
->where('a.id = ' . (int) $articleId)
->where('a.state = 1');
$groups = $user->getAuthorisedViewLevels();
$query->where('a.access IN (' . implode(',', array_map('intval', $groups)) . ')');
$db->setQuery($query);
return $db->loadObject() ?: null;
}
public function getPosts(int $articleId): array
{
$db = $this->getDatabase();
$query = $db->getQuery(true)
->select([
'p.id',
'p.status',
'p.platform_post_id',
'p.posted_at',
'p.error_message',
'p.created',
's.title AS service_title',
's.service_type',
])
->from($db->quoteName('#__mokosuitecross_posts', 'p'))
->join('INNER', $db->quoteName('#__mokosuitecross_services', 's') . ' ON s.id = p.service_id')
->where('p.article_id = ' . (int) $articleId)
->order('p.created DESC');
$db->setQuery($query, 0, 50);
return $db->loadObjectList() ?: [];
}
}
@@ -1,51 +0,0 @@
<?php
/**
* @package MokoSuiteCross
* @subpackage com_mokosuitecross
* @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\MokoSuiteCross\Site\Model;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Model\ListModel;
class PostsModel extends ListModel
{
protected function getListQuery()
{
$db = $this->getDatabase();
$user = Factory::getApplication()->getIdentity();
$query = $db->getQuery(true);
$query->select([
'a.id AS article_id',
'a.title AS article_title',
'a.alias AS article_alias',
'a.catid',
'MAX(p.posted_at) AS last_posted',
'COUNT(p.id) AS post_count',
'GROUP_CONCAT(DISTINCT s.service_type ORDER BY s.service_type SEPARATOR \',\') AS service_types',
])
->from($db->quoteName('#__mokosuitecross_posts', 'p'))
->join('INNER', $db->quoteName('#__content', 'a') . ' ON a.id = p.article_id')
->join('INNER', $db->quoteName('#__mokosuitecross_services', 's') . ' ON s.id = p.service_id')
->where('p.status = ' . $db->quote('posted'))
->where('a.state = 1');
// Access filtering
$groups = $user->getAuthorisedViewLevels();
$query->where('a.access IN (' . implode(',', array_map('intval', $groups)) . ')');
$query->group('a.id, a.title, a.alias, a.catid')
->order('last_posted DESC');
return $query;
}
}
@@ -1,33 +0,0 @@
<?php
/**
* @package MokoSuiteCross
* @subpackage com_mokosuitecross
* @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\MokoSuiteCross\Site\View\Post;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
class HtmlView extends BaseHtmlView
{
protected $article;
protected $posts;
public function display($tpl = null): void
{
$articleId = Factory::getApplication()->getInput()->getInt('id', 0);
$model = $this->getModel();
$this->article = $model->getArticle($articleId);
$this->posts = $model->getPosts($articleId);
parent::display($tpl);
}
}
@@ -1,30 +0,0 @@
<?php
/**
* @package MokoSuiteCross
* @subpackage com_mokosuitecross
* @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\MokoSuiteCross\Site\View\Posts;
defined('_JEXEC') or die;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
class HtmlView extends BaseHtmlView
{
protected $items;
protected $pagination;
public function display($tpl = null): void
{
$this->items = $this->get('Items');
$this->pagination = $this->get('Pagination');
parent::display($tpl);
}
}
@@ -1,84 +0,0 @@
<?php
/**
* @package MokoSuiteCross
* @subpackage com_mokosuitecross
* @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;
$statusClasses = [
'posted' => 'bg-success',
'failed' => 'bg-danger',
'permanently_failed' => 'bg-danger',
'queued' => 'bg-warning text-dark',
'posting' => 'bg-info',
'scheduled' => 'bg-primary',
'deleted' => 'bg-secondary',
'cancelled' => 'bg-secondary',
];
?>
<div class="com-mokosuitecross-post">
<?php if (!$this->article) : ?>
<div class="alert alert-warning">
<?php echo Text::_('COM_MOKOSUITECROSS_NO_POSTS'); ?>
</div>
<?php else : ?>
<h2><?php echo Text::_('COM_MOKOSUITECROSS_POST_DETAIL_TITLE'); ?></h2>
<p>
<strong><?php echo $this->escape($this->article->title); ?></strong>
</p>
<?php if (empty($this->posts)) : ?>
<div class="alert alert-info">
<?php echo Text::_('COM_MOKOSUITECROSS_NO_POSTS'); ?>
</div>
<?php else : ?>
<table class="table table-striped">
<thead>
<tr>
<th><?php echo Text::_('COM_MOKOSUITECROSS_HEADING_SERVICE'); ?></th>
<th><?php echo Text::_('COM_MOKOSUITECROSS_COLUMN_STATUS'); ?></th>
<th><?php echo Text::_('COM_MOKOSUITECROSS_COLUMN_POSTED_DATE'); ?></th>
<th><?php echo Text::_('COM_MOKOSUITECROSS_COLUMN_LINK'); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($this->posts as $post) : ?>
<tr>
<td>
<span class="badge bg-secondary"><?php echo $this->escape($post->service_type); ?></span>
<?php echo $this->escape($post->service_title); ?>
</td>
<td>
<span class="badge <?php echo $statusClasses[$post->status] ?? 'bg-secondary'; ?>">
<?php echo $this->escape(ucfirst($post->status)); ?>
</span>
</td>
<td><?php echo $post->posted_at ? $this->escape($post->posted_at) : $this->escape($post->created); ?></td>
<td>
<?php if (!empty($post->platform_post_id)) : ?>
<span class="text-muted small"><?php echo $this->escape($post->platform_post_id); ?></span>
<?php else : ?>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
<a href="<?php echo Route::_('index.php?option=com_mokosuitecross&view=posts'); ?>" class="btn btn-secondary">
&larr; <?php echo Text::_('COM_MOKOSUITECROSS_POSTS_LIST_TITLE'); ?>
</a>
<?php endif; ?>
</div>
@@ -1,66 +0,0 @@
<?php
/**
* @package MokoSuiteCross
* @subpackage com_mokosuitecross
* @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;
?>
<div class="com-mokosuitecross-posts">
<h2><?php echo Text::_('COM_MOKOSUITECROSS_POSTS_LIST_TITLE'); ?></h2>
<?php if (empty($this->items)) : ?>
<div class="alert alert-info">
<?php echo Text::_('COM_MOKOSUITECROSS_NO_POSTS'); ?>
</div>
<?php else : ?>
<table class="table table-striped">
<thead>
<tr>
<th><?php echo Text::_('COM_MOKOSUITECROSS_COLUMN_ARTICLE'); ?></th>
<th><?php echo Text::_('COM_MOKOSUITECROSS_COLUMN_PLATFORMS'); ?></th>
<th><?php echo Text::_('COM_MOKOSUITECROSS_COLUMN_LAST_POSTED'); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($this->items as $item) : ?>
<tr>
<td>
<a href="<?php echo Route::_('index.php?option=com_mokosuitecross&view=post&id=' . (int) $item->article_id); ?>">
<?php echo $this->escape($item->article_title); ?>
</a>
</td>
<td>
<?php
$types = explode(',', $item->service_types ?? '');
foreach ($types as $type) :
$type = trim($type);
if (empty($type)) continue;
?>
<span class="badge bg-secondary"><?php echo $this->escape($type); ?></span>
<?php endforeach; ?>
</td>
<td>
<?php echo $item->last_posted ? $this->escape($item->last_posted) : '—'; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php if ($this->pagination->pagesTotal > 1) : ?>
<div class="com-mokosuitecross-posts__pagination">
<?php echo $this->pagination->getListFooter(); ?>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
@@ -96,27 +96,6 @@ INSERT INTO `#__mokosuitecross_templates` (`service_type`, `title`, `template_bo
('instagram', 'Instagram Default', '{social}\n\n{hashtags}', 1, 21, NOW()), ('instagram', 'Instagram Default', '{social}\n\n{hashtags}', 1, 21, NOW()),
('youtube', 'YouTube Default', '{social}\n\n{url}', 1, 22, NOW()); ('youtube', 'YouTube Default', '{social}\n\n{url}', 1, 22, NOW());
CREATE TABLE IF NOT EXISTS `#__mokosuitecross_analytics` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`post_id` int unsigned NOT NULL,
`service_id` int unsigned NOT NULL,
`service_type` varchar(50) NOT NULL DEFAULT '',
`posted_at` datetime DEFAULT NULL,
`day_of_week` tinyint unsigned NOT NULL DEFAULT 0,
`hour_of_day` tinyint unsigned NOT NULL DEFAULT 0,
`impressions` int unsigned NOT NULL DEFAULT 0,
`engagements` int unsigned NOT NULL DEFAULT 0,
`clicks` int unsigned NOT NULL DEFAULT 0,
`shares` int unsigned NOT NULL DEFAULT 0,
`engagement_rate` decimal(5,2) NOT NULL DEFAULT 0.00,
`created` datetime NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_service_type` (`service_type`),
KEY `idx_day_hour` (`day_of_week`, `hour_of_day`),
KEY `idx_post` (`post_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `#__mokosuitecross_category_rules` ( CREATE TABLE IF NOT EXISTS `#__mokosuitecross_category_rules` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT, `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`category_id` int(10) unsigned NOT NULL, `category_id` int(10) unsigned NOT NULL,
@@ -1,7 +1,5 @@
-- MokoSuiteCross -- Uninstall -- MokoSuiteCross Uninstall
DROP TABLE IF EXISTS `#__mokosuitecross_logs`; DROP TABLE IF EXISTS `#__mokosuitecross_logs`;
DROP TABLE IF EXISTS `#__mokosuitecross_analytics`;
DROP TABLE IF EXISTS `#__mokosuitecross_category_rules`;
DROP TABLE IF EXISTS `#__mokosuitecross_posts`; DROP TABLE IF EXISTS `#__mokosuitecross_posts`;
DROP TABLE IF EXISTS `#__mokosuitecross_templates`; DROP TABLE IF EXISTS `#__mokosuitecross_templates`;
DROP TABLE IF EXISTS `#__mokosuitecross_services`; DROP TABLE IF EXISTS `#__mokosuitecross_services`;
@@ -0,0 +1 @@
/* 01.08.42 — no schema changes */
@@ -1 +0,0 @@
/* 01.08.43 — no schema changes */
@@ -1 +0,0 @@
/* 01.08.44 — no schema changes */
@@ -1 +0,0 @@
/* 01.08.45 — no schema changes */
@@ -1 +0,0 @@
/* 01.08.46 — no schema changes */
@@ -1 +0,0 @@
/* 01.08.47 — no schema changes */
@@ -1 +0,0 @@
/* 01.08.49 — no schema changes */
@@ -1 +0,0 @@
/* 01.08.50 — no schema changes */
@@ -1 +0,0 @@
/* 01.08.51 — no schema changes */
@@ -1 +0,0 @@
/* 01.08.52 — no schema changes */
@@ -1 +0,0 @@
/* 01.08.53 — no schema changes */
@@ -1,23 +0,0 @@
-- MokoSuiteCross 01.08.54 -- Best time to post analytics
-- Copyright (C) 2026 Moko Consulting. All rights reserved.
-- SPDX-License-Identifier: GPL-3.0-or-later
CREATE TABLE IF NOT EXISTS `#__mokosuitecross_analytics` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`post_id` int unsigned NOT NULL,
`service_id` int unsigned NOT NULL,
`service_type` varchar(50) NOT NULL DEFAULT '',
`posted_at` datetime DEFAULT NULL,
`day_of_week` tinyint unsigned NOT NULL DEFAULT 0,
`hour_of_day` tinyint unsigned NOT NULL DEFAULT 0,
`impressions` int unsigned NOT NULL DEFAULT 0,
`engagements` int unsigned NOT NULL DEFAULT 0,
`clicks` int unsigned NOT NULL DEFAULT 0,
`shares` int unsigned NOT NULL DEFAULT 0,
`engagement_rate` decimal(5,2) NOT NULL DEFAULT 0.00,
`created` datetime NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_service_type` (`service_type`),
KEY `idx_day_hour` (`day_of_week`, `hour_of_day`),
KEY `idx_post` (`post_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
@@ -1 +0,0 @@
/* 01.08.55 — no schema changes */
@@ -1 +0,0 @@
/* 01.08.56 — no schema changes */
@@ -1 +0,0 @@
/* 01.08.57 — no schema changes */
@@ -1 +0,0 @@
/* 01.08.58 — no schema changes */
@@ -1 +0,0 @@
/* 01.08.59 — no schema changes */
@@ -1 +0,0 @@
/* 01.08.61 — no schema changes */
@@ -1 +0,0 @@
/* 01.11.00 — no schema changes */
@@ -1 +0,0 @@
/* 01.11.01 — no schema changes */
@@ -1 +0,0 @@
/* 01.11.02 — no schema changes */
@@ -1 +0,0 @@
/* 01.11.03 — no schema changes */
@@ -1 +0,0 @@
/* 01.12.00 — no schema changes */
@@ -1 +0,0 @@
/* 01.12.01 — no schema changes */
@@ -1 +0,0 @@
/* 01.12.03 — no schema changes */
@@ -1 +0,0 @@
/* 01.12.04 — no schema changes */
@@ -1 +0,0 @@
/* 01.12.05 — no schema changes */
@@ -1 +0,0 @@
/* 01.12.06 — no schema changes */
@@ -1 +0,0 @@
/* 01.13.00 — no schema changes */
@@ -1 +0,0 @@
/* 01.13.01 — no schema changes */
@@ -1 +0,0 @@
/* 01.13.02 — no schema changes */
@@ -1 +0,0 @@
/* 01.13.03 — no schema changes */
@@ -1 +0,0 @@
/* 01.13.04 — no schema changes */
@@ -1 +0,0 @@
/* 01.13.06 — no schema changes */
@@ -1 +0,0 @@
/* 01.13.08 — no schema changes */
@@ -1 +0,0 @@
/* 01.13.09 — no schema changes */
@@ -1 +0,0 @@
/* 01.13.10 — no schema changes */
@@ -1 +0,0 @@
/* 01.13.11 — no schema changes */
@@ -1 +0,0 @@
/* 01.13.12 — no schema changes */
@@ -1 +0,0 @@
/* 01.13.13 — no schema changes */
@@ -1 +0,0 @@
/* 01.13.14 — no schema changes */
@@ -1 +0,0 @@
/* 01.13.15 — no schema changes */
@@ -1 +0,0 @@
/* 01.13.16 — no schema changes */
@@ -1 +0,0 @@
/* 01.13.17 — no schema changes */
@@ -1 +0,0 @@
/* 01.13.18 — no schema changes */
@@ -1 +0,0 @@
/* 01.13.19 — no schema changes */
@@ -1 +0,0 @@
/* 01.13.20 — no schema changes */
@@ -1 +0,0 @@
/* 01.13.21 — no schema changes */
@@ -1 +0,0 @@
/* 01.14.00 — no schema changes */
@@ -1,100 +0,0 @@
<?php
/**
* @package MokoSuiteCross
* @subpackage com_mokosuitecross
* @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\MokoSuiteCross\Administrator\Controller;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Controller\BaseController;
use Joomla\CMS\Session\Session;
use Joomla\Component\MokoSuiteCross\Administrator\Helper\AiGeneratorHelper;
class AiController extends BaseController
{
public function generate(): void
{
if (!Session::checkToken('get')) {
echo json_encode(['success' => false, 'error' => 'Invalid token']);
$this->app->close();
return;
}
$user = $this->app->getIdentity();
if (!$user->authorise('core.edit', 'com_mokosuitecross')) {
echo json_encode(['success' => false, 'error' => 'Permission denied']);
$this->app->close();
return;
}
$articleId = $this->input->getInt('article_id', 0);
if ($articleId < 1) {
echo json_encode(['success' => false, 'error' => 'Missing article ID']);
$this->app->close();
return;
}
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName(['id', 'title', 'introtext', 'catid']))
->from($db->quoteName('#__content'))
->where($db->quoteName('id') . ' = ' . $articleId);
$db->setQuery($query);
$article = $db->loadObject();
if (!$article) {
echo json_encode(['success' => false, 'error' => 'Article not found']);
$this->app->close();
return;
}
$category = '';
$catQuery = $db->getQuery(true)
->select($db->quoteName('title'))
->from($db->quoteName('#__categories'))
->where($db->quoteName('id') . ' = ' . (int) $article->catid);
$db->setQuery($catQuery);
$category = $db->loadResult() ?: '';
$tagQuery = $db->getQuery(true)
->select($db->quoteName('t.title'))
->from($db->quoteName('#__tags', 't'))
->join('INNER', $db->quoteName('#__contentitem_tag_map', 'm') . ' ON ' . $db->quoteName('m.tag_id') . ' = ' . $db->quoteName('t.id'))
->where($db->quoteName('m.content_item_id') . ' = ' . $articleId)
->where($db->quoteName('m.type_alias') . ' = ' . $db->quote('com_content.article'));
$db->setQuery($tagQuery);
$tags = $db->loadColumn() ?: [];
$introtext = strip_tags($article->introtext ?? '');
$introtext = mb_substr($introtext, 0, 500);
$params = \Joomla\CMS\Component\ComponentHelper::getParams('com_mokosuitecross');
$config = [
'ai_provider' => $params->get('ai_provider', 'none'),
'ai_api_key' => $params->get('ai_api_key', ''),
'ai_model' => $params->get('ai_model', ''),
'ai_tone' => $params->get('ai_tone', 'professional'),
];
$result = AiGeneratorHelper::generate($article->title, $introtext, $category, $tags, $config);
$this->app->setHeader('Content-Type', 'application/json; charset=utf-8');
echo json_encode($result);
$this->app->close();
}
}
@@ -1,97 +0,0 @@
<?php
/**
* @package MokoSuiteCross
* @subpackage com_mokosuitecross
* @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\MokoSuiteCross\Administrator\Controller;
defined('_JEXEC') or die;
use Joomla\CMS\MVC\Controller\BaseController;
use Joomla\CMS\Session\Session;
use Joomla\Component\MokoSuiteCross\Administrator\Helper\AnalyticsHelper;
class AnalyticsController extends BaseController
{
/**
* Return heatmap grid data as JSON.
*
* Query params: service_type (string), days (int, default 90)
*/
public function heatmap(): void
{
if (!Session::checkToken('get')) {
echo json_encode(['success' => false, 'error' => 'Invalid token']);
$this->app->close();
return;
}
$user = $this->app->getIdentity();
if (!$user->authorise('core.manage', 'com_mokosuitecross')) {
echo json_encode(['success' => false, 'error' => 'Permission denied']);
$this->app->close();
return;
}
$serviceType = $this->input->getCmd('service_type', '');
$days = $this->input->getInt('days', 90);
$grid = AnalyticsHelper::getHeatmapData($serviceType, $days);
$bestTimes = AnalyticsHelper::getBestTimes($serviceType, 3);
$this->app->setHeader('Content-Type', 'application/json; charset=utf-8');
echo json_encode([
'success' => true,
'grid' => $grid,
'best_times' => $bestTimes,
]);
$this->app->close();
}
/**
* Return the top posting times as JSON.
*
* Query params: service_type (string), limit (int, default 5)
*/
public function besttimes(): void
{
if (!Session::checkToken('get')) {
echo json_encode(['success' => false, 'error' => 'Invalid token']);
$this->app->close();
return;
}
$user = $this->app->getIdentity();
if (!$user->authorise('core.manage', 'com_mokosuitecross')) {
echo json_encode(['success' => false, 'error' => 'Permission denied']);
$this->app->close();
return;
}
$serviceType = $this->input->getCmd('service_type', '');
$limit = $this->input->getInt('limit', 5);
$bestTimes = AnalyticsHelper::getBestTimes($serviceType, $limit);
$serviceBreakdown = AnalyticsHelper::getServiceBreakdown();
$this->app->setHeader('Content-Type', 'application/json; charset=utf-8');
echo json_encode([
'success' => true,
'best_times' => $bestTimes,
'service_breakdown' => $serviceBreakdown,
]);
$this->app->close();
}
}
@@ -1,268 +0,0 @@
<?php
/**
* @package MokoSuiteCross
* @subpackage com_mokosuitecross
* @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\MokoSuiteCross\Administrator\Controller;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Controller\BaseController;
use Joomla\CMS\Session\Session;
use Joomla\Component\MokoSuiteCross\Administrator\Table\PostTable;
/**
* Calendar controller -- provides AJAX endpoints for the visual post calendar.
*
* Endpoints:
* task=calendar.events -- GET JSON feed for FullCalendar (filtered by start/end)
* task=calendar.reschedule -- POST reschedule a post to a new date/time
*/
class CalendarController extends BaseController
{
/**
* Return posts as FullCalendar-compatible JSON events.
*
* Query params: start, end (ISO 8601 date range from FullCalendar).
*
* @return void
*/
public function events(): void
{
$app = $this->app;
$db = Factory::getDbo();
// ACL check
if (!$app->getIdentity()->authorise('core.manage', 'com_mokosuitecross')) {
$this->sendJsonResponse(['error' => 'Forbidden'], 403);
return;
}
// FullCalendar sends start/end as ISO date strings
$start = $this->input->getString('start', '');
$end = $this->input->getString('end', '');
$query = $db->getQuery(true)
->select([
'p.' . $db->quoteName('id'),
'p.' . $db->quoteName('article_id'),
'p.' . $db->quoteName('service_id'),
'p.' . $db->quoteName('status'),
'p.' . $db->quoteName('scheduled_at'),
'p.' . $db->quoteName('posted_at'),
'p.' . $db->quoteName('created'),
'p.' . $db->quoteName('message'),
'a.' . $db->quoteName('title', 'article_title'),
's.' . $db->quoteName('title', 'service_title'),
's.' . $db->quoteName('service_type'),
])
->from($db->quoteName('#__mokosuitecross_posts', 'p'))
->leftJoin(
$db->quoteName('#__content', 'a')
. ' ON ' . $db->quoteName('a.id') . ' = ' . $db->quoteName('p.article_id')
)
->leftJoin(
$db->quoteName('#__mokosuitecross_services', 's')
. ' ON ' . $db->quoteName('s.id') . ' = ' . $db->quoteName('p.service_id')
)
->order($db->quoteName('p.created') . ' DESC');
// Filter by date range when provided
if ($start !== '') {
$dateExpr = 'COALESCE(p.scheduled_at, p.posted_at, p.created)';
$query->where($dateExpr . ' >= ' . $db->quote($start));
}
if ($end !== '') {
$dateExpr = 'COALESCE(p.scheduled_at, p.posted_at, p.created)';
$query->where($dateExpr . ' <= ' . $db->quote($end));
}
$db->setQuery($query);
$rows = $db->loadObjectList() ?: [];
// Map status to colour
$statusColors = [
'posted' => '#28a745',
'scheduled' => '#007bff',
'queued' => '#ffc107',
'failed' => '#dc3545',
'posting' => '#17a2b8',
];
$events = [];
foreach ($rows as $row) {
// Pick the best date for the calendar event
$eventDate = $row->scheduled_at ?: ($row->posted_at ?: $row->created);
// Skip rows with no usable date
if (empty($eventDate) || $eventDate === '0000-00-00 00:00:00') {
continue;
}
$title = ($row->article_title ?: 'Post #' . $row->id);
if ($row->service_title) {
$title .= ' - ' . $row->service_title;
}
$events[] = [
'id' => (int) $row->id,
'title' => $title,
'start' => $eventDate,
'color' => $statusColors[$row->status] ?? '#6c757d',
'url' => 'index.php?option=com_mokosuitecross&task=post.edit&id=' . (int) $row->id,
'extendedProps' => [
'status' => $row->status,
'service_type' => $row->service_type ?? '',
'article_id' => (int) $row->article_id,
'service_id' => (int) $row->service_id,
'message' => mb_substr($row->message ?? '', 0, 200),
],
];
}
$this->sendJsonResponse($events, 200);
}
/**
* Reschedule a post to a new date/time via drag-drop.
*
* POST params: post_id (int), new_date (ISO 8601 datetime string).
*
* @return void
*/
public function reschedule(): void
{
$app = $this->app;
// CSRF check
if (!Session::checkToken('post')) {
$this->sendJsonResponse(['error' => Text::_('JINVALID_TOKEN')], 403);
return;
}
// ACL check
if (!$app->getIdentity()->authorise('core.edit', 'com_mokosuitecross')) {
$this->sendJsonResponse(['error' => 'Forbidden'], 403);
return;
}
$postId = $this->input->getInt('post_id', 0);
$newDate = $this->input->getString('new_date', '');
if ($postId < 1 || $newDate === '') {
$this->sendJsonResponse(
['error' => Text::_('COM_MOKOSUITECROSS_CALENDAR_RESCHEDULE_ERROR')],
400
);
return;
}
// Validate the date format
try {
$dateObj = Factory::getDate($newDate);
} catch (\Exception $e) {
$this->sendJsonResponse(
['error' => Text::_('COM_MOKOSUITECROSS_CALENDAR_RESCHEDULE_ERROR')],
400
);
return;
}
// Load the post using Table bind/check/store pattern
$db = Factory::getDbo();
$table = new PostTable($db);
if (!$table->load($postId)) {
$this->sendJsonResponse(
['error' => Text::_('COM_MOKOSUITECROSS_CALENDAR_RESCHEDULE_ERROR')],
404
);
return;
}
// Only allow rescheduling of scheduled or queued posts
$allowedStatuses = ['scheduled', 'queued'];
if (!in_array($table->status, $allowedStatuses, true)) {
$this->sendJsonResponse(
['error' => Text::_('COM_MOKOSUITECROSS_CALENDAR_RESCHEDULE_ERROR')],
400
);
return;
}
// Update the post
$data = [
'scheduled_at' => $dateObj->toSql(),
'status' => 'scheduled',
'modified' => Factory::getDate()->toSql(),
];
if (!$table->bind($data) || !$table->check() || !$table->store()) {
$this->sendJsonResponse(
['error' => Text::_('COM_MOKOSUITECROSS_CALENDAR_RESCHEDULE_ERROR')],
500
);
return;
}
// Log the reschedule
$log = (object) [
'post_id' => $postId,
'service_id' => (int) $table->service_id,
'level' => 'info',
'message' => sprintf('Post rescheduled to %s via calendar drag-drop', $dateObj->toSql()),
'context' => '{}',
'created' => Factory::getDate()->toSql(),
];
$db->insertObject('#__mokosuitecross_logs', $log);
$this->sendJsonResponse(
[
'success' => true,
'message' => Text::_('COM_MOKOSUITECROSS_CALENDAR_RESCHEDULE_SUCCESS'),
],
200
);
}
/**
* Send a JSON response and close the application.
*
* @param array $data Response data
* @param int $httpCode HTTP status code
*
* @return void
*/
private function sendJsonResponse(array $data, int $httpCode): void
{
$app = $this->app;
$app->setHeader('Content-Type', 'application/json; charset=utf-8');
$app->setHeader('Status', (string) $httpCode);
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$app->close();
}
}
@@ -130,10 +130,6 @@ class PostsController extends AdminController
{ {
$this->checkToken(); $this->checkToken();
if (!$this->app->getIdentity()->authorise('mokosuitecross.queue.manage', 'com_mokosuitecross')) {
throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403);
}
$db = Factory::getDbo(); $db = Factory::getDbo();
$query = $db->getQuery(true) $query = $db->getQuery(true)
@@ -242,10 +238,6 @@ class PostsController extends AdminController
{ {
$this->checkToken(); $this->checkToken();
if (!$this->app->getIdentity()->authorise('mokosuitecross.queue.manage', 'com_mokosuitecross')) {
throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403);
}
$db = Factory::getDbo(); $db = Factory::getDbo();
$query = $db->getQuery(true) $query = $db->getQuery(true)
@@ -31,17 +31,6 @@ class PreviewController extends BaseController
return; return;
} }
$user = $this->app->getIdentity();
if (!$user->authorise('core.manage', 'com_mokosuitecross')
&& !$user->authorise('core.edit', 'com_content')
&& !$user->authorise('core.edit.own', 'com_content')) {
echo json_encode(['error' => 'Permission denied']);
$this->app->close();
return;
}
$articleId = $this->input->getInt('article_id', 0); $articleId = $this->input->getInt('article_id', 0);
$platform = $this->input->getCmd('platform', 'twitter'); $platform = $this->input->getCmd('platform', 'twitter');
@@ -54,14 +43,10 @@ class PreviewController extends BaseController
$db = Factory::getDbo(); $db = Factory::getDbo();
$groups = $user->getAuthorisedViewLevels();
$query = $db->getQuery(true) $query = $db->getQuery(true)
->select('*') ->select('*')
->from($db->quoteName('#__content')) ->from($db->quoteName('#__content'))
->where($db->quoteName('id') . ' = :id') ->where($db->quoteName('id') . ' = ' . $articleId);
->where($db->quoteName('access') . ' IN (' . implode(',', array_map('intval', $groups)) . ')')
->bind(':id', $articleId, \Joomla\Database\ParameterType::INTEGER);
$db->setQuery($query); $db->setQuery($query);
$article = $db->loadObject(); $article = $db->loadObject();
@@ -1,90 +0,0 @@
<?php
/**
* @package MokoSuiteCross
* @subpackage com_mokosuitecross
* @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\MokoSuiteCross\Administrator\Controller;
defined('_JEXEC') or die;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Controller\BaseController;
use Joomla\CMS\Session\Session;
use Joomla\Component\MokoSuiteCross\Administrator\Helper\SocialImageHelper;
class SocialImageController extends BaseController
{
public function generate(): void
{
if (!Session::checkToken('get')) {
echo json_encode(['success' => false, 'error' => 'Invalid token']);
$this->app->close();
return;
}
$user = $this->app->getIdentity();
if (!$user->authorise('core.edit', 'com_mokosuitecross')) {
echo json_encode(['success' => false, 'error' => 'Permission denied']);
$this->app->close();
return;
}
$articleId = $this->input->getInt('article_id', 0);
if ($articleId < 1) {
echo json_encode(['success' => false, 'error' => 'Missing article ID']);
$this->app->close();
return;
}
$params = ComponentHelper::getParams('com_mokosuitecross');
if (!(int) $params->get('social_image_enabled', 0)) {
echo json_encode(['success' => false, 'error' => 'Social image generator is not enabled']);
$this->app->close();
return;
}
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName('title'))
->from($db->quoteName('#__content'))
->where($db->quoteName('id') . ' = ' . $articleId);
$db->setQuery($query);
$title = $db->loadResult();
if (!$title) {
echo json_encode(['success' => false, 'error' => 'Article not found']);
$this->app->close();
return;
}
$siteName = $this->app->get('sitename', '');
$config = [
'bg_color' => $params->get('social_image_bg_color', '#1a1a2e'),
'text_color' => $params->get('social_image_text_color', '#ffffff'),
'font_size' => $params->get('social_image_font_size', 48),
'show_site_name' => (bool) $params->get('social_image_show_site_name', 1),
];
$result = SocialImageHelper::generate($title, $siteName, $config);
$this->app->setHeader('Content-Type', 'application/json; charset=utf-8');
echo json_encode($result);
$this->app->close();
}
}

Some files were not shown because too many files have changed in this diff Show More