1 Commits

Author SHA1 Message Date
gitea-actions[bot] f6e8c2f171 chore(release): build 02.17.00-rc [skip ci] 2026-05-30 23:37:22 +00:00
222 changed files with 3006 additions and 3888 deletions
+1 -1
View File
@@ -113,7 +113,7 @@ releases/
build/ build/
dist/ dist/
out/ out/
/site/ site/
*.map *.map
*.css.map *.css.map
*.js.map *.js.map
-63
View File
@@ -1,63 +0,0 @@
# MokoOnyx
Joomla site template — successor to MokoCassiopeia. Base template for all WaaS client deployments.
## Quick Reference
| Field | Value |
|---|---|
| **Element** | `tpl_mokoonyx` |
| **Type** | Joomla site template |
| **Language** | PHP 8.1+ / CSS / JS |
| **Branch** | develop on `dev`, merge to `main` (protected) |
| **Wiki** | [MokoOnyx Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/wiki) |
## Commands
```bash
make build # Build template ZIP
make lint # Run linters
make validate # Validate structure
make release # Full release pipeline
make minify # Minify CSS/JS assets
make clean # Clean build artifacts
composer install # Install PHP dependencies
```
## Architecture
Joomla **site template** — the base layer that client theme packages override:
- `src/templateDetails.xml` — template manifest
- `src/index.php` — main template entry point
- `src/error.php` — error page
- `src/offline.php` — maintenance page
- `src/component.php` — print/component-only layout
- `src/html/` — template overrides for core components
- `src/media/css/` — base stylesheets
- `src/media/js/` — base scripts
- `src/media/images/` — template images
- `src/language/` — translations
### Client Theme Packages
Client repos (`client-clarksvillefurs`, `client-optainfunding`, etc.) install `type="file"` packages that overlay client-specific CSS, images, and JS into the MokoOnyx media directory. MokoOnyx provides the structure; client themes customize the appearance.
### Minification
`MokoMinifyHelper` handles runtime CSS/JS minification in Joomla. Build-time minification via `make minify`. Never commit `*.min.css`/`*.min.js` — they're generated.
## Rules
- **Never commit** `.claude/`, `.mcp.json`, `TODO.md`, `*.min.css`/`*.min.js`
- **Attribution**: `Authored-by: Moko Consulting`
- **Workflow directory**: `.mokogitea/` (not `.gitea/` or `.github/`)
- **Minification**: handled at build time (CI) and runtime (MokoMinifyHelper)
- **Wiki**: documentation lives in the Gitea wiki, not `docs/` files
- **Standards**: [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)
## Coding Standards
- PHP 8.1+ minimum
- SPDX license headers on all PHP files
- `defined('_JEXEC') or die;` on all PHP files
+1 -1
View File
@@ -1,4 +1,4 @@
# DISABLED - auto-release handles dev recreation # DISABLED - auto-release handles dev recreation from main
name: Cascade (DISABLED) name: Cascade (DISABLED)
on: workflow_dispatch on: workflow_dispatch
jobs: jobs:
+26
View File
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Moko Platform Repository Manifest
See: https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home
-->
<moko-platform xmlns="https://standards.mokoconsulting.tech/moko-platform/1.0" schema-version="1.0">
<identity>
<name>MokoOnyx</name>
<display-name>Template - MokoOnyx</display-name>
<org>MokoConsulting</org>
<description>MokoOnyx - Joomla site template (successor to MokoCassiopeia)</description>
<version>02.17.00</version>
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
</identity>
<governance>
<platform>joomla</platform>
<standards-version>04.07.00</standards-version>
<standards-source>https://git.mokoconsulting.tech/MokoConsulting/moko-platform</standards-source>
<last-synced>2026-05-10T19:51:09+00:00</last-synced>
</governance>
<build>
<language>PHP</language>
<package-type>joomla</package-type>
<entry-point>src/</entry-point>
</build>
</moko-platform>
+9 -9
View File
@@ -4,8 +4,8 @@
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Release # INGROUP: moko-platform.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli # REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# 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)
@@ -43,19 +43,19 @@ jobs:
token: ${{ secrets.MOKOGITEA_TOKEN }} token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 1 fetch-depth: 1
- name: Setup mokocli tools - name: Setup moko-platform tools
run: | run: |
if ! command -v composer &> /dev/null; then if ! command -v composer &> /dev/null; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 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
if [ -d "/opt/mokocli/cli" ]; then if [ -d "/opt/moko-platform/cli" ]; then
echo "MOKO_CLI=/opt/mokocli/cli" >> "$GITHUB_ENV" echo "MOKO_CLI=/opt/moko-platform/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/moko-platform.git" \
/tmp/mokocli /tmp/moko-platform-api
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
echo "MOKO_CLI=/tmp/mokocli/cli" >> "$GITHUB_ENV" echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
fi fi
- name: Bump version - name: Bump version
+270 -421
View File
@@ -1,421 +1,270 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> # Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# #
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Release # INGROUP: moko-platform.Release
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli # REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# PATH: /templates/workflows/universal/auto-release.yml.template # PATH: /templates/workflows/universal/auto-release.yml.template
# VERSION: 05.00.00 # VERSION: 05.00.00
# BRIEF: Universal build & release detects platform from manifest.xml # BRIEF: Universal build & release detects platform from manifest.xml
# #
# +========================================================================+ # +========================================================================+
# | UNIVERSAL BUILD & RELEASE PIPELINE | # | UNIVERSAL BUILD & RELEASE PIPELINE |
# +========================================================================+ # +========================================================================+
# | | # | |
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. | # | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
# | | # | |
# | Platform-specific: | # | Platform-specific: |
# | joomla: XML manifest, type-prefixed packages | # | joomla: XML manifest, updates.xml, type-prefixed packages |
# | dolibarr: mod*.class.php, update.txt, dev version reset | # | dolibarr: mod*.class.php, update.txt, dev version reset |
# | generic: README-only, no update stream | # | generic: README-only, no update stream |
# | | # | |
# +========================================================================+ # +========================================================================+
name: "Universal: Build & Release" name: "Universal: Build & Release"
on: on:
pull_request: pull_request:
types: [opened, closed] types: [opened, closed]
branches: branches:
- main - main
workflow_dispatch: workflow_dispatch:
inputs: inputs:
action: action:
description: 'Action to perform' description: 'Action to perform'
required: false required: false
type: choice type: choice
default: release default: release
options: options:
- release - release
- promote-rc - promote-rc
env: env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
GITEA_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 }}
permissions: permissions:
contents: write contents: write
jobs: jobs:
# ── PR Opened → Rename branch to RC and build RC release ───────────────────── # ── PR Opened → Rename branch to RC and build RC release ─────────────────────
promote-rc: promote-rc:
name: Promote to RC name: Promote to RC
runs-on: release runs-on: release
if: >- if: >-
(github.event.action == 'opened' && github.event.pull_request.merged != true) || (github.event.action == 'opened' && 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
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
token: ${{ secrets.MOKOGITEA_TOKEN }} token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 1 fetch-depth: 1
- name: Setup mokocli tools - name: Setup moko-platform tools
env: env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
run: | run: |
if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then if ! command -v composer &> /dev/null; then
echo Using pre-installed /opt/mokocli 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
echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV fi
else # Always fetch latest CLI tools — never use stale cache from previous runs
echo Falling back to fresh clone rm -rf /tmp/moko-platform-api
if ! command -v composer > /dev/null 2>&1; then git clone --depth 1 --branch main --quiet \
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 "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
fi /tmp/moko-platform-api
rm -rf /tmp/mokocli cd /tmp/moko-platform-api
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git composer install --no-dev --no-interaction --quiet
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
cd /tmp/mokocli - name: Rename branch to rc
composer install --no-dev --no-interaction --quiet run: |
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV php /tmp/moko-platform-api/cli/branch_rename.php \
fi --from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
- name: Rename branch to rc --api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
run: | --pr "${{ github.event.pull_request.number }}"
php ${MOKO_CLI}/branch_rename.php \
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \ - name: Checkout rc and configure git
--token "${{ secrets.MOKOGITEA_TOKEN }}" \ run: |
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \ git fetch origin rc
--pr "${{ github.event.pull_request.number }}" git checkout rc
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
- name: Checkout rc and configure git git config --local user.name "gitea-actions[bot]"
run: | git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
git fetch origin rc
git checkout rc - name: Publish RC release
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" run: |
git config --local user.name "gitea-actions[bot]" php /tmp/moko-platform-api/cli/release_publish.php \
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" --path . --stability rc --bump minor --branch rc \
--token "${{ secrets.MOKOGITEA_TOKEN }}"
- name: Publish RC release
run: | - name: Summary
php ${MOKO_CLI}/release_publish.php \ if: always()
--path . --stability rc --bump minor --branch rc \ run: |
--token "${{ secrets.MOKOGITEA_TOKEN }}" echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
echo "Branch renamed to rc, minor bump, RC + lesser stream releases built, updates.xml synced" >> $GITHUB_STEP_SUMMARY
- name: Update RC release notes from CHANGELOG.md
run: | # ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" release:
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" name: Build & Release Pipeline
runs-on: release
# Extract [Unreleased] section from changelog if: >-
NOTES="" github.event.pull_request.merged == true ||
if [ -f "CHANGELOG.md" ]; then (github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
fi steps:
[ -z "$NOTES" ] && NOTES="Release candidate" - name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
# Find the RC release and update its body with:
RELEASE_ID=$(curl -sf -H "Authorization: token ${TOKEN}" \ token: ${{ secrets.MOKOGITEA_TOKEN }}
"${API_BASE}/releases/tags/release-candidate" \ fetch-depth: 0
| python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
- name: Configure git for bot pushes
if [ -n "$RELEASE_ID" ]; then run: |
python3 -c " git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
import json, urllib.request git config --local user.name "gitea-actions[bot]"
body = open('/dev/stdin').read() git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
payload = json.dumps({'body': body}).encode()
req = urllib.request.Request( - name: Setup moko-platform tools
'${API_BASE}/releases/${RELEASE_ID}', env:
data=payload, method='PATCH', MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
headers={ MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
'Authorization': 'token ${TOKEN}', COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
'Content-Type': 'application/json' run: |
}) # Ensure PHP + Composer are available
urllib.request.urlopen(req) if ! command -v composer &> /dev/null; then
" <<< "$NOTES" 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
echo "RC release notes updated from CHANGELOG.md" fi
fi # Always fetch latest CLI tools — never use stale cache from previous runs
rm -rf /tmp/moko-platform-api
- name: Summary git clone --depth 1 --branch main --quiet \
if: always() "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
run: | /tmp/moko-platform-api
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY cd /tmp/moko-platform-api
echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY composer install --no-dev --no-interaction --quiet
# ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
release: - name: "Publish stable release"
name: Build & Release Pipeline run: |
runs-on: release php /tmp/moko-platform-api/cli/release_publish.php \
if: >- --path . --stability stable --bump minor --branch main \
github.event.pull_request.merged == true || --token "${{ secrets.MOKOGITEA_TOKEN }}"
(github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
steps: - name: "Step 9: Mirror release to GitHub"
- name: Checkout repository if: >-
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 steps.version.outputs.skip != 'true' &&
with: secrets.GH_MIRROR_TOKEN != ''
token: ${{ secrets.MOKOGITEA_TOKEN }} continue-on-error: true
fetch-depth: 0 run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- name: Configure git for bot pushes RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
run: | GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
git config --local user.name "gitea-actions[bot]" php /tmp/moko-platform-api/cli/release_mirror.php \
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" --version "$VERSION" --tag "$RELEASE_TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
- name: Check for merge conflict markers --gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
run: | --branch main 2>&1 || 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) echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY
if [ -n "$CONFLICTS" ]; then
echo "::error::Merge conflict markers found — aborting release" # -- STEP 10: Sync main branch to GitHub mirror ----------------------------
echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY - name: "Step 10: Push main to GitHub mirror"
echo '```' >> $GITHUB_STEP_SUMMARY if: >-
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY steps.version.outputs.skip != 'true' &&
echo '```' >> $GITHUB_STEP_SUMMARY secrets.GH_MIRROR_TOKEN != ''
exit 1 continue-on-error: true
fi run: |
echo "No conflict markers found" GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
- name: Setup mokocli tools GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
env: git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting git fetch origin main --depth=1
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}' git push github origin/main:refs/heads/main --force 2>/dev/null \
run: | && echo "main branch pushed to GitHub mirror" \
if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then || echo "WARNING: GitHub mirror push failed"
echo Using pre-installed /opt/mokocli
echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV - name: "Step 11: Delete rc branch and recreate dev from main"
else if: steps.version.outputs.skip != 'true'
echo Falling back to fresh clone continue-on-error: true
if ! command -v composer > /dev/null 2>&1; then run: |
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 API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
fi TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
rm -rf /tmp/mokocli
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git # Delete rc branch (ephemeral — created by promote-rc)
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
cd /tmp/mokocli "${API_BASE}/branches/rc" 2>/dev/null \
composer install --no-dev --no-interaction --quiet && echo "Deleted rc branch" || echo "rc branch not found"
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
fi # Delete dev branch
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
- name: "Detect platform" "${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
id: platform
run: | # Recreate dev from main (now includes version bump + changelog promotion)
php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true curl -sf -X POST -H "Authorization: token ${TOKEN}" \
php ${MOKO_CLI}/manifest_read.php --path . --github-output 2>/dev/null || true -H "Content-Type: application/json" \
"${API_BASE}/branches" \
- name: "Determine version bump level" -d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
id: bump
run: | echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY
# Fix/patch branches: version was already bumped by pre-release, just strip suffix
# Feature/dev branches: bump minor for the new stable release - name: "Step 12: Create version branch from main"
HEAD_REF="${{ github.event.pull_request.head.ref || 'dev' }}" if: steps.version.outputs.skip != 'true'
case "$HEAD_REF" in continue-on-error: true
fix/*|patch/*|hotfix/*|bugfix/*) BUMP="none" ;; run: |
*) BUMP="minor" ;; API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
esac TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
echo "level=${BUMP}" >> "$GITHUB_OUTPUT" VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
echo "Bump level: ${BUMP} (from branch: ${HEAD_REF})" BRANCH_NAME="version/${VERSION}"
MAIN_SHA=$(git rev-parse HEAD)
- name: "Publish stable release"
run: | # Delete old version branch if it exists (same version re-release)
BUMP_FLAG="" curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}"
if [ "${{ steps.bump.outputs.level }}" != "none" ]; then
BUMP_FLAG="--bump ${{ steps.bump.outputs.level }}" # Create version/XX.YY.ZZ from main
fi curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed"
php ${MOKO_CLI}/release_publish.php \
--path . --stability stable ${BUMP_FLAG} --branch main \ echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY
--token "${{ secrets.MOKOGITEA_TOKEN }}"
- name: "Read published version"
id: version # -- Dolibarr post-release: Reset dev version -----------------------------
run: | - name: "Post-release: Reset dev version"
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "") if: steps.version.outputs.skip != 'true'
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//') continue-on-error: true
[ -z "$VERSION" ] && VERSION="00.00.00" && echo "skip=true" >> "$GITHUB_OUTPUT" run: |
echo "version=${VERSION}" >> "$GITHUB_OUTPUT" API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
echo "tag=stable" >> "$GITHUB_OUTPUT" php /tmp/moko-platform-api/cli/version_reset_dev.php \
echo "release_tag=stable" >> "$GITHUB_OUTPUT" --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
echo "branch=main" >> "$GITHUB_OUTPUT" --branch dev --path . 2>&1 || true
echo "Published version: ${VERSION}"
# -- Summary --------------------------------------------------------------
- name: Update release notes and promote changelog - name: Pipeline Summary
run: | if: always()
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" run: |
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
PLATFORM="${{ steps.platform.outputs.platform }}"
# Get the stable release info (version and ID) if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
RELEASE_JSON=$(curl -sf -H "Authorization: token ${TOKEN}" \ echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
"${API_BASE}/releases/tags/stable" 2>/dev/null || echo '{}') echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
RELEASE_ID=$(python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" <<< "$RELEASE_JSON" 2>/dev/null || true) elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
# Extract version from release name (e.g. "06.17.00" or "v06.17.00") echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
VERSION=$(python3 -c " else
import json, sys, re echo "" >> $GITHUB_STEP_SUMMARY
r = json.load(sys.stdin) echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
name = r.get('name', '') echo "" >> $GITHUB_STEP_SUMMARY
m = re.search(r'(\d+\.\d+\.\d+)', name) echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
print(m.group(1) if m else '') echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
" <<< "$RELEASE_JSON" 2>/dev/null || true) echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
# Extract [Unreleased] section from changelog echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
NOTES="" echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
if [ -f "CHANGELOG.md" ]; then echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md) fi
fi
[ -z "$NOTES" ] && NOTES="Stable release"
# Update release body via API
if [ -n "$RELEASE_ID" ]; then
python3 -c "
import json, urllib.request
body = open('/dev/stdin').read()
payload = json.dumps({'body': body}).encode()
req = urllib.request.Request(
'${API_BASE}/releases/${RELEASE_ID}',
data=payload, method='PATCH',
headers={
'Authorization': 'token ${TOKEN}',
'Content-Type': 'application/json'
})
urllib.request.urlopen(req)
" <<< "$NOTES"
echo "Release notes updated from CHANGELOG.md"
fi
# Promote [Unreleased] → [version] in CHANGELOG.md and reset
if [ -n "$VERSION" ] && [ -f "CHANGELOG.md" ]; then
DATE=$(date +%Y-%m-%d)
python3 -c "
import sys
version, date = sys.argv[1], sys.argv[2]
content = open('CHANGELOG.md').read()
old = '## [Unreleased]'
new = f'## [Unreleased]\n\n## [{version}] --- {date}'
content = content.replace(old, new, 1)
open('CHANGELOG.md', 'w').write(content)
" "$VERSION" "$DATE"
git add CHANGELOG.md
git commit -m "chore: promote changelog [Unreleased] → [${VERSION}]" || true
git push origin main || true
echo "Changelog promoted: [Unreleased] → [${VERSION}]"
fi
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
- name: "Step 9: Mirror release to GitHub"
if: >-
steps.version.outputs.skip != 'true' &&
secrets.GH_MIRROR_TOKEN != ''
continue-on-error: true
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php ${MOKO_CLI}/release_mirror.php \
--version "$VERSION" --tag "$RELEASE_TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
--branch main 2>&1 || true
echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY
# -- STEP 10: Sync main branch to GitHub mirror ----------------------------
- name: "Step 10: Push main to GitHub mirror"
if: >-
steps.version.outputs.skip != 'true' &&
secrets.GH_MIRROR_TOKEN != ''
continue-on-error: true
run: |
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
git fetch origin main --depth=1
git push github origin/main:refs/heads/main --force 2>/dev/null \
&& echo "main branch pushed to GitHub mirror" \
|| echo "WARNING: GitHub mirror push failed"
- name: "Step 11: Delete rc branch and recreate dev from main"
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
# Delete rc branch (ephemeral — created by promote-rc)
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
"${API_BASE}/branches/rc" 2>/dev/null \
&& echo "Deleted rc branch" || echo "rc branch not found"
# Delete dev branch
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
"${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"
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
BRANCH_NAME="version/${VERSION}"
MAIN_SHA=$(git rev-parse HEAD)
# Delete old version branch if it exists (same version re-release)
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}"
# Create version/XX.YY.ZZ from main
curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed"
echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY
# -- Dolibarr post-release: Reset dev version -----------------------------
- name: "Post-release: Reset dev version"
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php ${MOKO_CLI}/version_reset_dev.php \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
--branch dev --path . 2>&1 || true
# -- Summary --------------------------------------------------------------
- name: Pipeline Summary
if: always()
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
PLATFORM="${{ steps.platform.outputs.platform }}"
if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
else
echo "" >> $GITHUB_STEP_SUMMARY
echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| 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
+1 -1
View File
@@ -5,7 +5,7 @@
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Universal # INGROUP: MokoStandards.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli # REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# 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
+3 -5
View File
@@ -1,10 +1,8 @@
# DISABLED auto-release Step 11 recreates dev from main after every release. # DISABLED - auto-release handles dev recreation from main
# Cascade-dev is redundant and causes version conflicts when both main and dev name: Cascade (DISABLED)
# have different version numbers in templateDetails.xml / manifest.xml.
name: "Cascade Main → Dev (DISABLED)"
on: workflow_dispatch on: workflow_dispatch
jobs: jobs:
noop: noop:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- run: echo "Cascade disabled — auto-release handles dev recreation" - run: echo disabled
-191
View File
@@ -1,191 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.CI
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
# PATH: /.gitea/workflows/ci-generic.yml
# VERSION: 01.00.00
# BRIEF: CI pipeline — lint, validate, and test for generic projects (PHP + Node.js)
name: "Generic: Project CI"
on:
workflow_dispatch:
permissions:
contents: read
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
# ── Lint & Validate ───────────────────────────────────────────────────
lint:
name: Lint & Validate
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Detect toolchain
id: detect
run: |
HAS_PHP=false
HAS_NODE=false
[ -f "composer.json" ] && HAS_PHP=true
[ -f "package.json" ] && HAS_NODE=true
echo "has_php=$HAS_PHP" >> "$GITHUB_OUTPUT"
echo "has_node=$HAS_NODE" >> "$GITHUB_OUTPUT"
echo "Toolchain: PHP=$HAS_PHP Node=$HAS_NODE"
- name: Setup PHP
if: steps.detect.outputs.has_php == 'true'
run: |
if ! command -v php &> /dev/null; then
sudo apt-get update -qq
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
fi
php -v
- name: Setup Node.js
if: steps.detect.outputs.has_node == 'true'
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install PHP dependencies
if: steps.detect.outputs.has_php == 'true'
run: |
if [ -f "composer.json" ]; then
composer install --no-interaction --prefer-dist --quiet 2>/dev/null || true
fi
- name: Install Node.js dependencies
if: steps.detect.outputs.has_node == 'true'
run: |
if [ -f "package.json" ]; then
npm ci --quiet 2>/dev/null || npm install --quiet 2>/dev/null || true
fi
- name: PHP syntax check
if: steps.detect.outputs.has_php == 'true'
run: |
ERRORS=0
while IFS= read -r -d '' file; do
if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
echo "::error file=${file}::PHP syntax error"
ERRORS=$((ERRORS + 1))
fi
done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -not -path "./node_modules/*" -print0)
echo "## PHP Lint" >> $GITHUB_STEP_SUMMARY
if [ "$ERRORS" -eq 0 ]; then
echo "All PHP files passed syntax check." >> $GITHUB_STEP_SUMMARY
else
echo "${ERRORS} file(s) with syntax errors." >> $GITHUB_STEP_SUMMARY
exit 1
fi
- name: TypeScript/JavaScript lint
if: steps.detect.outputs.has_node == 'true'
run: |
if [ -f "node_modules/.bin/eslint" ]; then
npx eslint src/ --quiet 2>&1 || { echo "::error::ESLint errors found"; exit 1; }
echo "## ESLint" >> $GITHUB_STEP_SUMMARY
echo "All files passed ESLint." >> $GITHUB_STEP_SUMMARY
elif [ -f ".eslintrc.json" ] || [ -f ".eslintrc.js" ] || [ -f "eslint.config.js" ]; then
echo "::warning::ESLint config found but eslint not installed"
else
echo "No ESLint configured — skipping"
fi
- name: TypeScript compile check
if: steps.detect.outputs.has_node == 'true'
run: |
if [ -f "tsconfig.json" ] && [ -f "node_modules/.bin/tsc" ]; then
npx tsc --noEmit 2>&1 || { echo "::error::TypeScript compilation errors"; exit 1; }
echo "## TypeScript" >> $GITHUB_STEP_SUMMARY
echo "TypeScript compilation passed." >> $GITHUB_STEP_SUMMARY
fi
- name: PHPStan static analysis
if: steps.detect.outputs.has_php == 'true'
run: |
if [ -f "phpstan.neon" ] && [ -f "vendor/bin/phpstan" ]; then
vendor/bin/phpstan analyse --no-progress 2>&1 || { echo "::warning::PHPStan found issues"; }
fi
# ── Tests ─────────────────────────────────────────────────────────────
test:
name: Tests
runs-on: ubuntu-latest
needs: lint
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Detect toolchain
id: detect
run: |
HAS_PHP=false
HAS_NODE=false
[ -f "composer.json" ] && HAS_PHP=true
[ -f "package.json" ] && HAS_NODE=true
echo "has_php=$HAS_PHP" >> "$GITHUB_OUTPUT"
echo "has_node=$HAS_NODE" >> "$GITHUB_OUTPUT"
- name: Setup PHP
if: steps.detect.outputs.has_php == 'true'
run: |
if ! command -v php &> /dev/null; then
sudo apt-get update -qq
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
fi
- name: Setup Node.js
if: steps.detect.outputs.has_node == 'true'
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: |
[ -f "composer.json" ] && composer install --no-interaction --prefer-dist --quiet 2>/dev/null || true
[ -f "package.json" ] && { npm ci --quiet 2>/dev/null || npm install --quiet 2>/dev/null || true; }
- name: Run PHP tests
if: steps.detect.outputs.has_php == 'true'
run: |
if [ -f "vendor/bin/phpunit" ]; then
vendor/bin/phpunit --testdox 2>&1
echo "## PHPUnit" >> $GITHUB_STEP_SUMMARY
echo "Tests passed." >> $GITHUB_STEP_SUMMARY
elif [ -f "phpunit.xml" ] || [ -f "phpunit.xml.dist" ]; then
echo "::warning::PHPUnit config found but phpunit not installed"
else
echo "No PHPUnit configured — skipping"
fi
- name: Run Node.js tests
if: steps.detect.outputs.has_node == 'true'
run: |
if jq -e '.scripts.test' package.json > /dev/null 2>&1; then
npm test 2>&1
echo "## Node.js Tests" >> $GITHUB_STEP_SUMMARY
echo "Tests passed." >> $GITHUB_STEP_SUMMARY
else
echo "No test script in package.json — skipping"
fi
- name: Build check
run: |
if [ -f "Makefile" ]; then
make build 2>&1 || echo "::warning::Build failed or not configured"
elif [ -f "package.json" ] && jq -e '.scripts.build' package.json > /dev/null 2>&1; then
npm run build 2>&1 || echo "::warning::Build failed"
fi
+28 -464
View File
@@ -10,7 +10,7 @@
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API # REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
# PATH: /templates/workflows/joomla/ci-joomla.yml.template # PATH: /templates/workflows/joomla/ci-joomla.yml.template
# VERSION: 04.06.00 # VERSION: 04.06.00
# BRIEF: CI workflow for Joomla extensions lint, validate, test # BRIEF: CI workflow for Joomla extensions -- lint, validate, test
name: "Joomla: Extension CI" name: "Joomla: Extension CI"
@@ -35,32 +35,25 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup PHP - name: Setup PHP
run: | run: |
if ! command -v php &> /dev/null; then
sudo apt-get update -qq
sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
fi
php -v && composer --version php -v && composer --version
- name: Setup mokocli tools - name: Clone MokoStandards
env: env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.GA_TOKEN || github.token }} GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}
MOKO_CLONE_HOST: ${{ secrets.MOKOGITEA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }} MOKO_CLONE_HOST: ${{ secrets.MOKOGITEA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
run: | run: |
if [ -d "/opt/mokocli" ] || [ -d "/tmp/mokocli" ]; then git clone --depth 1 --branch main --quiet \
echo "mokocli already available on runner — skipping clone" "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
else /tmp/mokostandards-api
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git" \
/tmp/mokocli 2>/dev/null || echo "mokocli clone skipped — continuing without it"
fi
- name: Install dependencies - name: Install dependencies
env: env:
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || secrets.GA_TOKEN || github.token }}"}}' COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}'
run: | run: |
if [ -f "composer.json" ]; then if [ -f "composer.json" ]; then
composer install \ composer install \
@@ -68,7 +61,7 @@ jobs:
--prefer-dist \ --prefer-dist \
--optimize-autoloader --optimize-autoloader
else else
echo "No composer.json found skipping dependency install" echo "No composer.json found -- skipping dependency install"
fi fi
- name: PHP syntax check - name: PHP syntax check
@@ -131,8 +124,8 @@ jobs:
echo "Manifest is well-formed XML." >> $GITHUB_STEP_SUMMARY echo "Manifest is well-formed XML." >> $GITHUB_STEP_SUMMARY
fi fi
# Check required tags: name, version, author # Check required tags: name, version, author, namespace (Joomla 5+)
for TAG in name version author; do for TAG in name version author namespace; do
if ! grep -q "<${TAG}>" "$MANIFEST" 2>/dev/null; then if ! grep -q "<${TAG}>" "$MANIFEST" 2>/dev/null; then
echo "Missing required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY echo "Missing required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1)) ERRORS=$((ERRORS + 1))
@@ -140,19 +133,6 @@ jobs:
echo "Found required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY echo "Found required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY
fi fi
done done
# Namespace is required for components/plugins but not packages
EXT_TYPE=$(grep -oP '<extension[^>]*\btype="\K[^"]+' "$MANIFEST" | head -1)
if [ "$EXT_TYPE" != "package" ]; then
if ! grep -q "<namespace" "$MANIFEST" 2>/dev/null; then
echo "Missing required tag: \`<namespace>\` (required for Joomla 5+ ${EXT_TYPE} extensions)" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "Found required tag: \`<namespace>\`" >> $GITHUB_STEP_SUMMARY
fi
else
echo "Package extension — \`<namespace>\` not required." >> $GITHUB_STEP_SUMMARY
fi
fi fi
if [ "${ERRORS}" -gt 0 ]; then if [ "${ERRORS}" -gt 0 ]; then
@@ -181,7 +161,7 @@ jobs:
# Extract language file references from manifest # Extract language file references from manifest
LANG_FILES=$(grep -oP 'language\s+tag="[^"]*"[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null || true) LANG_FILES=$(grep -oP 'language\s+tag="[^"]*"[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null || true)
if [ -z "$LANG_FILES" ]; then if [ -z "$LANG_FILES" ]; then
echo "No language file references found in manifest skipping." >> $GITHUB_STEP_SUMMARY echo "No language file references found in manifest -- skipping." >> $GITHUB_STEP_SUMMARY
else else
while IFS= read -r LANG_FILE; do while IFS= read -r LANG_FILE; do
LANG_FILE=$(echo "$LANG_FILE" | xargs) LANG_FILE=$(echo "$LANG_FILE" | xargs)
@@ -205,7 +185,7 @@ jobs:
done <<< "$LANG_FILES" done <<< "$LANG_FILES"
fi fi
else else
echo "No manifest found skipping language check." >> $GITHUB_STEP_SUMMARY echo "No manifest found -- skipping language check." >> $GITHUB_STEP_SUMMARY
fi fi
if [ "${ERRORS}" -gt 0 ]; then if [ "${ERRORS}" -gt 0 ]; then
@@ -236,7 +216,7 @@ jobs:
done done
if [ "${CHECKED}" -eq 0 ]; then if [ "${CHECKED}" -eq 0 ]; then
echo "No src/ or htdocs/ directories found skipping." >> $GITHUB_STEP_SUMMARY echo "No src/ or htdocs/ directories found -- skipping." >> $GITHUB_STEP_SUMMARY
elif [ "${MISSING}" -gt 0 ]; then elif [ "${MISSING}" -gt 0 ]; then
echo "" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY
echo "**${MISSING} director(ies) missing index.html out of ${CHECKED} checked.**" >> $GITHUB_STEP_SUMMARY echo "**${MISSING} director(ies) missing index.html out of ${CHECKED} checked.**" >> $GITHUB_STEP_SUMMARY
@@ -245,417 +225,14 @@ jobs:
echo "All ${CHECKED} directories contain index.html." >> $GITHUB_STEP_SUMMARY echo "All ${CHECKED} directories contain index.html." >> $GITHUB_STEP_SUMMARY
fi fi
- name: Check config.xml and access.xml for components
run: |
echo "### Component Config & ACL Check" >> $GITHUB_STEP_SUMMARY
ERRORS=0
# Find all component manifests (XML with type="component")
COMP_MANIFESTS=$(find . -maxdepth 4 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*" -exec grep -l '<extension[^>]*type="component"' {} ; 2>/dev/null || true)
if [ -z "$COMP_MANIFESTS" ]; then
echo "No component extensions found — skipping." >> $GITHUB_STEP_SUMMARY
else
for MANIFEST in $COMP_MANIFESTS; do
COMP_DIR=$(dirname "$MANIFEST")
COMP_NAME=$(basename "$COMP_DIR")
echo "Component: `${COMP_NAME}` (manifest: `${MANIFEST}`)" >> $GITHUB_STEP_SUMMARY
# Check access.xml exists
ACCESS_FILE=$(find "$COMP_DIR" -name "access.xml" -not -path "./.git/*" 2>/dev/null | head -1)
if [ -z "$ACCESS_FILE" ]; then
echo "- Missing `access.xml` — ACL permissions will not work." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
if command -v php &> /dev/null; then
if ! php -r "@simplexml_load_file('$ACCESS_FILE') ?: exit(1);" 2>/dev/null; then
echo "- `access.xml` is not well-formed XML." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
for ACTION in core.admin core.manage; do
if ! grep -q "name=\"${ACTION}\"" "$ACCESS_FILE" 2>/dev/null; then
echo "- `access.xml` missing required action: `${ACTION}`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
done
echo "- `access.xml`: valid" >> $GITHUB_STEP_SUMMARY
fi
fi
fi
# Check config.xml exists
CONFIG_FILE=$(find "$COMP_DIR" -name "config.xml" -not -path "./.git/*" 2>/dev/null | head -1)
if [ -z "$CONFIG_FILE" ]; then
echo "- Missing `config.xml` — component Options page will be empty." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
if command -v php &> /dev/null; then
if ! php -r "@simplexml_load_file('$CONFIG_FILE') ?: exit(1);" 2>/dev/null; then
echo "- `config.xml` is not well-formed XML." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "- `config.xml`: valid" >> $GITHUB_STEP_SUMMARY
fi
fi
fi
done
fi
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${ERRORS}" -gt 0 ]; then
echo "**${ERRORS} config/ACL issue(s) found.**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "**Component config & ACL check passed.**" >> $GITHUB_STEP_SUMMARY
fi
- name: SQL schema validation
run: |
echo "### SQL Schema Validation" >> $GITHUB_STEP_SUMMARY
ERRORS=0
# Find SQL files in source/htdocs
SQL_FILES=$(find . -name "*.sql" -path "*/sql/*" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null)
if [ -z "$SQL_FILES" ]; then
echo "No SQL files found — skipping." >> $GITHUB_STEP_SUMMARY
else
echo "Found $(echo "$SQL_FILES" | wc -l) SQL file(s)" >> $GITHUB_STEP_SUMMARY
for FILE in $SQL_FILES; do
# Basic syntax check: balanced parentheses, no empty files
SIZE=$(wc -c < "$FILE" | tr -d ' ')
if [ "$SIZE" -eq 0 ]; then
echo "- Empty SQL file: \`${FILE}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
continue
fi
# Check for common SQL errors
if grep -qP '^\s*$' "$FILE" && [ "$SIZE" -lt 5 ]; then
echo "- Whitespace-only SQL file: \`${FILE}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
continue
fi
echo "- \`${FILE}\`: ${SIZE} bytes" >> $GITHUB_STEP_SUMMARY
done
# Check update SQL files follow version numbering pattern
UPDATE_DIR=$(find . -path "*/sql/updates/mysql" -type d -not -path "./.git/*" 2>/dev/null | head -1)
if [ -n "$UPDATE_DIR" ]; then
BAD_NAMES=0
for UFILE in "$UPDATE_DIR"/*.sql; do
[ ! -f "$UFILE" ] && continue
BASENAME=$(basename "$UFILE" .sql)
if ! echo "$BASENAME" | grep -qP '^\d+\.\d+\.\d+'; then
echo "- Update file \`${UFILE}\` does not follow version naming (expected X.Y.Z.sql)" >> $GITHUB_STEP_SUMMARY
BAD_NAMES=$((BAD_NAMES + 1))
fi
done
if [ "$BAD_NAMES" -gt 0 ]; then
ERRORS=$((ERRORS + BAD_NAMES))
fi
fi
fi
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${ERRORS}" -gt 0 ]; then
echo "**${ERRORS} SQL issue(s) found.**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "**SQL schema validation passed.**" >> $GITHUB_STEP_SUMMARY
fi
- name: Manifest file references check
run: |
echo "### Manifest File References" >> $GITHUB_STEP_SUMMARY
ERRORS=0
MANIFEST=""
for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
if grep -q "<extension" "$XML_FILE" 2>/dev/null; then
MANIFEST="$XML_FILE"
break
fi
done
if [ -z "$MANIFEST" ]; then
echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY
else
MANIFEST_DIR=$(dirname "$MANIFEST")
# Check <filename> references
FILENAMES=$(grep -oP '<filename[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null || true)
for F in $FILENAMES; do
if [ ! -f "${MANIFEST_DIR}/${F}" ] && [ ! -d "${MANIFEST_DIR}/${F}" ]; then
echo "- Missing: \`${F}\` (referenced in manifest)" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
done
# Check <folder> references
FOLDERS=$(grep -oP '<folder[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null || true)
for F in $FOLDERS; do
if [ ! -d "${MANIFEST_DIR}/${F}" ]; then
echo "- Missing folder: \`${F}\` (referenced in manifest)" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
done
# Check <file> references in package manifests (ZIP files won't exist in source)
EXT_TYPE=$(grep -oP '<extension[^>]*\btype="\K[^"]+' "$MANIFEST" | head -1)
if [ "$EXT_TYPE" != "package" ]; then
FILES=$(grep -oP '<file[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null || true)
for F in $FILES; do
if [ ! -f "${MANIFEST_DIR}/${F}" ]; then
echo "- Missing file: \`${F}\` (referenced in manifest)" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
done
fi
fi
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${ERRORS}" -gt 0 ]; then
echo "**${ERRORS} missing file reference(s).**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "**Manifest file references check passed.**" >> $GITHUB_STEP_SUMMARY
fi
- name: Form XML validation
run: |
echo "### Form XML Validation" >> $GITHUB_STEP_SUMMARY
ERRORS=0
FORM_FILES=$(find . -name "*.xml" -path "*/forms/*" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null)
if [ -z "$FORM_FILES" ]; then
echo "No form XML files found — skipping." >> $GITHUB_STEP_SUMMARY
else
echo "Found $(echo "$FORM_FILES" | wc -l) form file(s)" >> $GITHUB_STEP_SUMMARY
for FILE in $FORM_FILES; do
if command -v php &> /dev/null; then
if ! php -r "@simplexml_load_file('$FILE') ?: exit(1);" 2>/dev/null; then
echo "- \`${FILE}\`: malformed XML" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
# Check for valid Joomla form structure
if ! grep -qE '<form|<field|<fieldset' "$FILE" 2>/dev/null; then
echo "- \`${FILE}\`: no \`<form>\`, \`<field>\`, or \`<fieldset>\` elements found" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "- \`${FILE}\`: valid" >> $GITHUB_STEP_SUMMARY
fi
fi
fi
done
fi
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${ERRORS}" -gt 0 ]; then
echo "**${ERRORS} form XML issue(s).**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "**Form XML validation passed.**" >> $GITHUB_STEP_SUMMARY
fi
- name: Deprecated Joomla API check
continue-on-error: true
run: |
echo "### Deprecated Joomla API Check" >> $GITHUB_STEP_SUMMARY
WARNINGS=0
SRC_DIR=""
for DIR in source/ src/ htdocs/; do
[ -d "$DIR" ] && SRC_DIR="$DIR" && break
done
if [ -z "$SRC_DIR" ]; then
echo "No source directory found — skipping." >> $GITHUB_STEP_SUMMARY
else
# Joomla 3/4 deprecated patterns that break in Joomla 6
PATTERNS=(
'JFactory::'
'JText::'
'JHtml::'
'JRoute::'
'JUri::'
'JLog::'
'JTable::'
'JInput'
'CMSFactory::\$application'
'JApplicationCms'
)
for PATTERN in "${PATTERNS[@]}"; do
HITS=$(grep -rnl "$PATTERN" "$SRC_DIR" --include="*.php" 2>/dev/null || true)
if [ -n "$HITS" ]; then
COUNT=$(echo "$HITS" | wc -l)
echo "- \`${PATTERN}\` found in ${COUNT} file(s)" >> $GITHUB_STEP_SUMMARY
WARNINGS=$((WARNINGS + COUNT))
fi
done
echo "" >> $GITHUB_STEP_SUMMARY
if [ "$WARNINGS" -gt 0 ]; then
echo "**${WARNINGS} deprecated API usage(s) found.** These will break in Joomla 6." >> $GITHUB_STEP_SUMMARY
else
echo "**No deprecated APIs found.**" >> $GITHUB_STEP_SUMMARY
fi
fi
- name: Template output escaping check
continue-on-error: true
run: |
echo "### Template Output Escaping" >> $GITHUB_STEP_SUMMARY
WARNINGS=0
TMPL_FILES=$(find . -name "*.php" -path "*/tmpl/*" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null)
if [ -z "$TMPL_FILES" ]; then
echo "No template files found — skipping." >> $GITHUB_STEP_SUMMARY
else
echo "Found $(echo "$TMPL_FILES" | wc -l) template file(s)" >> $GITHUB_STEP_SUMMARY
for FILE in $TMPL_FILES; do
# Check for unescaped output: <?= $var ?> or echo $var without escape()
UNESCAPED=$(grep -nP '<\?=\s*\$(?!this->escape)' "$FILE" 2>/dev/null || true)
if [ -n "$UNESCAPED" ]; then
HITS=$(echo "$UNESCAPED" | wc -l)
echo "- \`${FILE}\`: ${HITS} unescaped \`<?= \$var ?>\` output(s) — use \`<?= \$this->escape(\$var) ?>\`" >> $GITHUB_STEP_SUMMARY
WARNINGS=$((WARNINGS + HITS))
fi
# Check for echo without escaping in template context
RAW_ECHO=$(grep -nP '^\s*echo\s+\$(?!this->escape)' "$FILE" 2>/dev/null || true)
if [ -n "$RAW_ECHO" ]; then
HITS=$(echo "$RAW_ECHO" | wc -l)
echo "- \`${FILE}\`: ${HITS} raw \`echo \$var\` — consider \`echo \$this->escape(\$var)\`" >> $GITHUB_STEP_SUMMARY
WARNINGS=$((WARNINGS + HITS))
fi
done
echo "" >> $GITHUB_STEP_SUMMARY
if [ "$WARNINGS" -gt 0 ]; then
echo "**${WARNINGS} potential XSS risk(s) in templates.** Review unescaped output." >> $GITHUB_STEP_SUMMARY
else
echo "**All template output appears properly escaped.**" >> $GITHUB_STEP_SUMMARY
fi
fi
- name: Namespace consistency check
run: |
echo "### Namespace Consistency" >> $GITHUB_STEP_SUMMARY
ERRORS=0
# Find component/plugin manifests with <namespace> tags
MANIFESTS=$(find . -maxdepth 4 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*" -exec grep -l '<namespace' {} \; 2>/dev/null || true)
if [ -z "$MANIFESTS" ]; then
echo "No manifests with \`<namespace>\` found — skipping." >> $GITHUB_STEP_SUMMARY
else
for MANIFEST in $MANIFESTS; do
NS_PATH=$(grep -oP '<namespace[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1)
[ -z "$NS_PATH" ] && continue
MANIFEST_DIR=$(dirname "$MANIFEST")
echo "Manifest: \`${MANIFEST}\` → namespace \`${NS_PATH}\`" >> $GITHUB_STEP_SUMMARY
# Check PHP files have matching namespace
while IFS= read -r -d '' PHP_FILE; do
FILE_NS=$(grep -oP '^\s*namespace\s+\K[^;]+' "$PHP_FILE" 2>/dev/null | head -1)
[ -z "$FILE_NS" ] && continue
# Namespace should start with the manifest namespace path
if ! echo "$FILE_NS" | grep -qF "${NS_PATH}"; then
echo "- \`${PHP_FILE}\`: namespace \`${FILE_NS}\` doesn't match manifest \`${NS_PATH}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
done < <(find "$MANIFEST_DIR" -name "*.php" -path "*/src/*" -not -path "./vendor/*" -print0 2>/dev/null)
done
fi
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${ERRORS}" -gt 0 ]; then
echo "**${ERRORS} namespace mismatch(es).**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "**Namespace consistency check passed.**" >> $GITHUB_STEP_SUMMARY
fi
- name: SPDX license header check
continue-on-error: true
run: |
echo "### SPDX License Headers" >> $GITHUB_STEP_SUMMARY
MISSING=0
SRC_DIR=""
for DIR in source/ src/ htdocs/; do
[ -d "$DIR" ] && SRC_DIR="$DIR" && break
done
if [ -z "$SRC_DIR" ]; then
echo "No source directory found — skipping." >> $GITHUB_STEP_SUMMARY
else
TOTAL=0
while IFS= read -r -d '' FILE; do
TOTAL=$((TOTAL + 1))
if ! head -10 "$FILE" | grep -qi "SPDX"; then
echo "- Missing SPDX header: \`${FILE}\`" >> $GITHUB_STEP_SUMMARY
MISSING=$((MISSING + 1))
fi
done < <(find "$SRC_DIR" -name "*.php" -not -path "./vendor/*" -print0)
echo "" >> $GITHUB_STEP_SUMMARY
if [ "$MISSING" -gt 0 ]; then
echo "**${MISSING}/${TOTAL} PHP file(s) missing SPDX license header.**" >> $GITHUB_STEP_SUMMARY
else
echo "**All ${TOTAL} PHP files have SPDX headers.**" >> $GITHUB_STEP_SUMMARY
fi
fi
- name: Service provider check
run: |
echo "### Service Provider Check" >> $GITHUB_STEP_SUMMARY
ERRORS=0
PROVIDERS=$(find . -name "provider.php" -path "*/services/*" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null)
if [ -z "$PROVIDERS" ]; then
echo "No service providers found — skipping." >> $GITHUB_STEP_SUMMARY
else
for FILE in $PROVIDERS; do
# Must return a ServiceProviderInterface
if ! grep -qP 'ServiceProviderInterface|ComponentInterface|MVCFactoryInterface|DispatcherInterface' "$FILE" 2>/dev/null; then
echo "- \`${FILE}\`: does not reference ServiceProviderInterface or component interfaces" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "- \`${FILE}\`: valid service provider" >> $GITHUB_STEP_SUMMARY
fi
# Must have return statement
if ! grep -qP '^\s*return\s+new\s+' "$FILE" 2>/dev/null; then
echo "- \`${FILE}\`: missing \`return new ...\` statement" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
done
fi
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${ERRORS}" -gt 0 ]; then
echo "**${ERRORS} service provider issue(s).**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "**Service provider check passed.**" >> $GITHUB_STEP_SUMMARY
fi
release-readiness: release-readiness:
name: Release Readiness Check name: Release Readiness Check
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event_name == 'pull_request' && github.base_ref == 'main' if: github.event_name == 'pull_request' && github.base_ref == 'main'
continue-on-error: true
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Validate release readiness - name: Validate release readiness
run: | run: |
@@ -761,19 +338,15 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup PHP ${{ matrix.php }} - name: Setup PHP ${{ matrix.php }}
run: | run: |
if ! command -v php &> /dev/null; then
sudo apt-get update -qq
sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
fi
php -v && composer --version php -v && composer --version
- name: Install dependencies - name: Install dependencies
env: env:
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || secrets.GA_TOKEN || github.token }}"}}' COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}'
run: | run: |
if [ -f "composer.json" ]; then if [ -f "composer.json" ]; then
composer install \ composer install \
@@ -781,7 +354,7 @@ jobs:
--prefer-dist \ --prefer-dist \
--optimize-autoloader --optimize-autoloader
else else
echo "No composer.json found skipping dependency install" echo "No composer.json found -- skipping dependency install"
fi fi
- name: Run tests - name: Run tests
@@ -793,14 +366,14 @@ jobs:
if [ $EXIT -eq 0 ]; then if [ $EXIT -eq 0 ]; then
echo "All tests passed." >> $GITHUB_STEP_SUMMARY echo "All tests passed." >> $GITHUB_STEP_SUMMARY
else else
echo "Test failures detected see log." >> $GITHUB_STEP_SUMMARY echo "Test failures detected -- see log." >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY
cat /tmp/test-output.log >> $GITHUB_STEP_SUMMARY cat /tmp/test-output.log >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY
fi fi
exit $EXIT exit $EXIT
else else
echo "No phpunit.xml found skipping tests." >> $GITHUB_STEP_SUMMARY echo "No phpunit.xml found -- skipping tests." >> $GITHUB_STEP_SUMMARY
fi fi
static-analysis: static-analysis:
@@ -811,19 +384,14 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup PHP - name: Setup PHP
run: | run: php -v && composer --version
if ! command -v php &> /dev/null; then
sudo apt-get update -qq
sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
fi
php -v && composer --version
- name: Install dependencies - name: Install dependencies
env: env:
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || secrets.GA_TOKEN || github.token }}"}}' COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}'
run: | run: |
if [ -f "composer.json" ]; then if [ -f "composer.json" ]; then
composer install --no-interaction --prefer-dist --optimize-autoloader composer install --no-interaction --prefer-dist --optimize-autoloader
@@ -854,7 +422,7 @@ jobs:
done done
if [ -z "$SRC_DIR" ]; then if [ -z "$SRC_DIR" ]; then
echo "No source directory found (src/, htdocs/, lib/) skipping." >> $GITHUB_STEP_SUMMARY echo "No source directory found (src/, htdocs/, lib/) -- skipping." >> $GITHUB_STEP_SUMMARY
exit 0 exit 0
fi fi
@@ -864,7 +432,7 @@ jobs:
echo "Using project PHPStan config." >> $GITHUB_STEP_SUMMARY echo "Using project PHPStan config." >> $GITHUB_STEP_SUMMARY
else else
ARGS="$ARGS --level=3" ARGS="$ARGS --level=3"
echo "No phpstan.neon found using level 3 (type inference)." >> $GITHUB_STEP_SUMMARY echo "No phpstan.neon found -- using level 3 (type inference)." >> $GITHUB_STEP_SUMMARY
fi fi
$PHPSTAN $ARGS 2>&1 | tee /tmp/phpstan-output.txt $PHPSTAN $ARGS 2>&1 | tee /tmp/phpstan-output.txt
@@ -890,14 +458,10 @@ jobs:
steps: steps:
- name: Trigger pre-release build - name: Trigger pre-release build
env: env:
GA_TOKEN: ${{ secrets.GA_TOKEN }} GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
REPO: ${{ github.repository }} REPO: ${{ github.repository }}
BRANCH: ${{ github.head_ref }} BRANCH: ${{ github.head_ref }}
run: | run: |
curl -s -X POST \ curl -s -X POST "${GITEA_URL:-https://git.mokoconsulting.tech}/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\"}}"
"${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" \
-H "Authorization: token ${GA_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
+9 -9
View File
@@ -4,8 +4,8 @@
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Maintenance # INGROUP: moko-platform.Maintenance
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards # REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /.gitea/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
@@ -33,17 +33,17 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
token: ${{ secrets.GA_TOKEN }} token: ${{ secrets.MOKOGITEA_TOKEN }}
- name: Delete merged branches - name: Delete merged branches
env: env:
GA_TOKEN: ${{ secrets.GA_TOKEN }} GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
run: | run: |
echo "=== Merged Branch Cleanup ===" echo "=== Merged Branch Cleanup ==="
API="${GITEA_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 ${GA_TOKEN}" \ BRANCHES=$(curl -sS -H "Authorization: token ${GITEA_TOKEN}" \
"${API}/branches?limit=50" | jq -r '.[].name') "${API}/branches?limit=50" | jq -r '.[].name')
DELETED=0 DELETED=0
@@ -56,7 +56,7 @@ jobs:
# 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 ${GA_TOKEN}" \ curl -sS -X DELETE -H "Authorization: token ${GITEA_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:
GA_TOKEN: ${{ secrets.GA_TOKEN }} GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
run: | run: |
echo "=== Workflow Run Cleanup ===" echo "=== Workflow Run Cleanup ==="
API="${GITEA_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 ${GA_TOKEN}" \ RUNS=$(curl -sS -H "Authorization: token ${GITEA_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 ${GA_TOKEN}" \ curl -sS -X DELETE -H "Authorization: token ${GITEA_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
+6 -2
View File
@@ -4,8 +4,8 @@
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Security # INGROUP: moko-platform.Security
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API # REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# PATH: /templates/workflows/gitleaks.yml.template # 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
@@ -25,6 +25,10 @@
name: "Universal: Secret Scanning" name: "Universal: Secret Scanning"
on: on:
pull_request:
branches:
- main
- 'dev/**'
schedule: schedule:
- cron: '0 5 * * 1' # Weekly Monday 05:00 UTC - cron: '0 5 * * 1' # Weekly Monday 05:00 UTC
workflow_dispatch: workflow_dispatch:
+13 -3
View File
@@ -4,8 +4,18 @@
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Automation # INGROUP: moko-platform.Automation
# VERSION: 02.27.03 <<<<<<< HEAD
<<<<<<< HEAD
# VERSION: 02.17.00
=======
# VERSION: 02.17.00
=======
# VERSION: 02.17.00
=======
# VERSION: 02.17.00
>>>>>>> origin/main
>>>>>>> origin/main
# 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"
@@ -28,7 +38,7 @@ jobs:
steps: steps:
- name: Create branch and comment - name: Create branch and comment
run: | run: |
TOKEN="${{ secrets.GA_TOKEN }}" TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
API="${GITEA_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 }}"
+2 -2
View File
@@ -4,8 +4,8 @@
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Notifications # INGROUP: moko-platform.Notifications
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards # REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /.gitea/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
+236 -534
View File
@@ -1,534 +1,236 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> # Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# #
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.CI # INGROUP: moko-platform.CI
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform # REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# PATH: /templates/workflows/universal/pr-check.yml.template # PATH: /templates/workflows/universal/pr-check.yml.template
# VERSION: 09.23.00 # VERSION: 05.00.00
# BRIEF: PR gate — branch policy + code validation before merge # BRIEF: PR gate — branch policy + code validation before merge
name: "Universal: PR Check" name: "Universal: PR Check"
on: on:
pull_request: pull_request:
types: [opened, synchronize, reopened, edited] types: [opened, synchronize, reopened, edited]
permissions: permissions:
contents: read contents: read
pull-requests: write pull-requests: write
env: env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs: jobs:
# ── Branch Policy ────────────────────────────────────────────────────── # ── Branch Policy ──────────────────────────────────────────────────────
branch-policy: branch-policy:
name: Branch Policy name: Branch Policy
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check branch merge target - name: Check branch merge target
run: | run: |
HEAD="${{ github.head_ref }}" HEAD="${{ github.head_ref }}"
BASE="${{ github.base_ref }}" BASE="${{ github.base_ref }}"
echo "PR: ${HEAD} → ${BASE}" echo "PR: ${HEAD} → ${BASE}"
ALLOWED=true ALLOWED=true
REASON="" REASON=""
case "$HEAD" in case "$HEAD" in
feature/*|feat/*) feature/*|feat/*)
if [ "$BASE" != "dev" ]; then if [ "$BASE" != "dev" ]; then
ALLOWED=false ALLOWED=false
REASON="Feature branches must target 'dev', not '${BASE}'" REASON="Feature branches must target 'dev', not '${BASE}'"
fi fi
;; ;;
fix/*|bugfix/*) fix/*|bugfix/*)
if [ "$BASE" != "dev" ]; then if [ "$BASE" != "dev" ]; then
ALLOWED=false ALLOWED=false
REASON="Fix branches must target 'dev', not '${BASE}'" REASON="Fix branches must target 'dev', not '${BASE}'"
fi fi
;; ;;
patch/*) patch/*)
if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then
ALLOWED=false ALLOWED=false
REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'" REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'"
fi fi
;; ;;
hotfix/*) hotfix/*)
if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
ALLOWED=false ALLOWED=false
REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'" REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'"
fi fi
;; ;;
rc) rc)
if [ "$BASE" != "main" ]; then if [ "$BASE" != "main" ]; then
ALLOWED=false ALLOWED=false
REASON="RC branch can only merge into 'main', not '${BASE}'" REASON="RC branch can only merge into 'main', not '${BASE}'"
fi fi
;; ;;
dev) dev)
if [ "$BASE" != "main" ]; then if [ "$BASE" != "main" ]; then
ALLOWED=false ALLOWED=false
REASON="Dev branch can only merge into 'main', not '${BASE}'" REASON="Dev branch can only merge into 'main', not '${BASE}'"
fi fi
;; ;;
esac esac
if [ "$ALLOWED" = false ]; then if [ "$ALLOWED" = false ]; then
echo "::error::${REASON}" echo "::error::${REASON}"
echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY
echo "${REASON}" >> $GITHUB_STEP_SUMMARY echo "${REASON}" >> $GITHUB_STEP_SUMMARY
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\`" >> $GITHUB_STEP_SUMMARY echo "- \`fix/*\` → \`dev\`" >> $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
exit 1 exit 1
fi fi
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
# ── Secret Scanning ────────────────────────────────────────────────── # ── Code Validation ────────────────────────────────────────────────────
gitleaks: validate:
name: Secret Scan name: Validate PR
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps:
- name: Checkout steps:
uses: actions/checkout@v4 - name: Checkout
with: uses: actions/checkout@v4
fetch-depth: 0
- name: Detect platform
- name: Install Gitleaks id: platform
run: | run: |
GITLEAKS_VERSION="8.21.2" # Read platform from XML manifest (<platform> tag) or plain text fallback
curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \ PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1)
| tar -xz -C /usr/local/bin gitleaks [ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]')
[ -z "$PLATFORM" ] && PLATFORM="generic"
- name: Scan PR commits for secrets echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
run: |
if gitleaks detect --source . --verbose \ - name: Setup PHP
--log-opts=${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }} 2>&1; then if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
echo "**No secrets detected.**" >> $GITHUB_STEP_SUMMARY run: |
else if ! command -v php &> /dev/null; then
echo "::error::Potential secrets detected in PR commits" sudo apt-get update -qq
exit 1 sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
fi fi
# ── Code Validation ──────────────────────────────────────────────────── - name: PHP syntax check
validate: if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
name: Validate PR run: |
runs-on: ubuntu-latest ERRORS=0
while IFS= read -r -d '' file; do
steps: if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
- name: Checkout ERRORS=$((ERRORS + 1))
uses: actions/checkout@v4 fi
done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0)
- name: Check for merge conflict markers echo "PHP lint: ${ERRORS} error(s)"
run: | [ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
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 - name: Validate platform manifest
echo "::error::Merge conflict markers found in source files" run: |
echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY PLATFORM="${{ steps.platform.outputs.platform }}"
echo '```' >> $GITHUB_STEP_SUMMARY case "$PLATFORM" in
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY joomla)
echo '```' >> $GITHUB_STEP_SUMMARY MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
exit 1 if [ -z "$MANIFEST" ]; then
fi echo "::warning::No Joomla manifest found (WaaS site)"
echo "No conflict markers found" exit 0
fi
- name: Detect platform echo "Manifest: ${MANIFEST}"
id: platform if command -v php &> /dev/null; then
run: | php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::Manifest XML is malformed"; exit 1; }
# Read platform from XML manifest (<platform> tag) or plain text fallback fi
PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1) for ELEMENT in name version description; do
[ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]') grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; }
[ -z "$PLATFORM" ] && PLATFORM="generic" done
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT" echo "Joomla manifest valid"
;;
- name: Setup PHP dolibarr)
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr' MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1)
run: | if [ -z "$MOD_FILE" ]; then
if ! command -v php &> /dev/null; then echo "::error::No mod*.class.php found"
sudo apt-get update -qq exit 1
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1 fi
fi echo "Dolibarr module: ${MOD_FILE}"
;;
- name: PHP syntax check *)
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr' echo "Generic platform — no manifest validation"
run: | ;;
ERRORS=0 esac
while IFS= read -r -d '' file; do
if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then - name: Check update stream format
ERRORS=$((ERRORS + 1)) run: |
fi PLATFORM="${{ steps.platform.outputs.platform }}"
done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0) case "$PLATFORM" in
echo "PHP lint: ${ERRORS} error(s)" joomla)
[ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; } if [ -f "updates.xml" ]; then
if command -v php &> /dev/null; then
- name: Joomla JEXEC guard check php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::updates.xml is malformed"; exit 1; }
if: steps.platform.outputs.platform == 'joomla' fi
run: | echo "updates.xml valid"
ERRORS=0 fi
while IFS= read -r -d '' file; do ;;
# Skip vendor, node_modules, and index.html stub files dolibarr)
case "$file" in ./vendor/*|./node_modules/*) continue ;; esac [ -f "update.txt" ] && echo "update.txt present" || echo "::warning::No update.txt"
# Check first 10 lines for JEXEC or JPATH guard ;;
if ! head -20 "$file" | grep -qE "defined\s*\(\s*['\"](_JEXEC|JPATH_BASE|\\\\JPATH_PLATFORM)['\"]"; then esac
echo "::error file=${file}::Missing JEXEC guard: ${file}"
ERRORS=$((ERRORS + 1)) - name: Check changelog has unreleased entry
fi run: |
done < <(find . -name "*.php" -path "*/src/*" -not -path "./.git/*" -not -path "./vendor/*" -print0) if [ ! -f "CHANGELOG.md" ]; then
if [ "$ERRORS" -gt 0 ]; then echo "::warning::No CHANGELOG.md found"
echo "::error::${ERRORS} PHP file(s) missing defined('_JEXEC') or die guard" exit 0
echo "## JEXEC Guard Check: Failed" >> $GITHUB_STEP_SUMMARY fi
echo "${ERRORS} file(s) in src/ are missing the Joomla execution guard." >> $GITHUB_STEP_SUMMARY # Check for content under [Unreleased] section
exit 1 if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then
fi echo "::error::CHANGELOG.md missing [Unreleased] section"
echo "JEXEC guard: OK" exit 1
fi
- name: Joomla directory listing protection # Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased
if: steps.platform.outputs.platform == 'joomla' UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true)
run: | if [ "$UNRELEASED_CONTENT" -eq 0 ]; then
MISSING=0 echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes."
SOURCE_DIR="src" echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY
[ ! -d "$SOURCE_DIR" ] && exit 0 echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY
while IFS= read -r dir; do echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY
if [ ! -f "${dir}/index.html" ]; then exit 1
echo "::warning::Missing index.html in ${dir} (directory listing protection)" fi
MISSING=$((MISSING + 1)) echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]"
fi
done < <(find "$SOURCE_DIR" -type d -not -path "./.git/*" -not -path "*/vendor/*" -not -path "*/node_modules/*") - name: Verify package source
if [ "$MISSING" -gt 0 ]; then run: |
echo "## Directory Protection" >> $GITHUB_STEP_SUMMARY SOURCE_DIR="src"
echo "${MISSING} director(ies) missing index.html" >> $GITHUB_STEP_SUMMARY [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
fi if [ ! -d "$SOURCE_DIR" ]; then
echo "Directory protection: ${MISSING} missing (advisory)" echo "::warning::No src/ or htdocs/ directory"
exit 0
- name: Joomla script file and asset checks fi
if: steps.platform.outputs.platform == 'joomla' FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
run: | echo "Source: ${FILE_COUNT} files"
ERRORS=0 [ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; }
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
[ -z "$MANIFEST" ] && exit 0 # ── Pre-Release RC Build ─────────────────────────────────────────────────
MANIFEST_DIR=$(dirname "$MANIFEST") pre-release:
name: Build RC Package
# Check scriptfile exists if declared runs-on: ubuntu-latest
SCRIPTFILE=$(sed -n 's/.*<scriptfile>\([^<]*\)<\/scriptfile>.*/\1/p' "$MANIFEST" 2>/dev/null) needs: [branch-policy, validate]
if [ -n "$SCRIPTFILE" ]; then
if [ ! -f "${MANIFEST_DIR}/${SCRIPTFILE}" ]; then steps:
echo "::error::Manifest declares <scriptfile>${SCRIPTFILE}</scriptfile> but file not found at ${MANIFEST_DIR}/${SCRIPTFILE}" - name: Trigger RC pre-release
ERRORS=$((ERRORS + 1)) env:
else GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
echo "Script file: ${MANIFEST_DIR}/${SCRIPTFILE} (OK)" REPO: ${{ github.repository }}
fi BRANCH: ${{ github.head_ref }}
fi GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
run: |
# Require joomla.asset.json and validate it 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\"}}"
ASSET_JSON=$(find "$MANIFEST_DIR" -name "joomla.asset.json" -not -path "./.git/*" 2>/dev/null | head -1) echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
if [ -z "$ASSET_JSON" ]; then echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
echo "::error::joomla.asset.json not found — Joomla asset system is required"
ERRORS=$((ERRORS + 1))
else
if command -v php &> /dev/null; then
php -r "json_decode(file_get_contents('$ASSET_JSON')); if(json_last_error()!==JSON_ERROR_NONE){echo json_last_error_msg();exit(1);}" 2>&1 || {
echo "::error::joomla.asset.json is not valid JSON"
ERRORS=$((ERRORS + 1))
}
fi
echo "joomla.asset.json: valid"
fi
# Validate all XML files in src/ are well-formed
XML_ERRORS=0
if command -v php &> /dev/null; then
while IFS= read -r -d '' xmlfile; do
if ! php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$xmlfile'); if(!\$x){foreach(libxml_get_errors() as \$e) echo trim(\$e->message) . ' in $xmlfile'; exit(1);}" 2>&1; then
XML_ERRORS=$((XML_ERRORS + 1))
fi
done < <(find "$MANIFEST_DIR" -name "*.xml" -not -path "./.git/*" -print0)
fi
if [ "$XML_ERRORS" -gt 0 ]; then
echo "::error::${XML_ERRORS} XML file(s) are malformed"
ERRORS=$((ERRORS + 1))
else
echo "XML well-formedness: OK"
fi
[ "$ERRORS" -gt 0 ] && exit 1
echo "Joomla asset checks: OK"
- name: Validate platform manifest
run: |
PLATFORM="${{ steps.platform.outputs.platform }}"
case "$PLATFORM" in
joomla)
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
if [ -z "$MANIFEST" ]; then
echo "::warning::No Joomla manifest found (WaaS site)"
exit 0
fi
echo "Manifest: ${MANIFEST}"
if command -v php &> /dev/null; then
php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::Manifest XML is malformed"; exit 1; }
fi
for ELEMENT in name version description; do
grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; }
done
# Block legacy raw/branch update server URLs on MokoGitea
RAW_URLS=$(grep -n 'raw/branch' "$MANIFEST" | grep -i 'mokoconsulting\|mokogitea\|git\.mokoconsulting\.tech' || true)
if [ -n "$RAW_URLS" ]; then
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"
exit 1
fi
echo "Joomla manifest valid"
;;
dolibarr)
MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1)
if [ -z "$MOD_FILE" ]; then
echo "::error::No mod*.class.php found"
exit 1
fi
echo "Dolibarr module: ${MOD_FILE}"
;;
*)
echo "Generic platform — no manifest validation"
;;
esac
- name: Check update stream format
run: |
PLATFORM="${{ steps.platform.outputs.platform }}"
case "$PLATFORM" in
joomla)
if [ -f "updates.xml" ]; then
if command -v php &> /dev/null; then
php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::updates.xml is malformed"; exit 1; }
fi
echo "updates.xml valid"
fi
;;
dolibarr)
[ -f "update.txt" ] && echo "update.txt present" || echo "::warning::No update.txt"
;;
esac
- name: Validate Joomla language files
if: steps.platform.outputs.platform == 'joomla'
run: |
ERRORS=0
WARNINGS=0
# Require both en-GB and en-US language directories
LANG_ROOT=$(find . -path "*/language" -type d -not -path "./.git/*" 2>/dev/null | head -1)
if [ -z "$LANG_ROOT" ]; then
echo "No language/ directory found — skipping"
exit 0
fi
if [ ! -d "$LANG_ROOT/en-GB" ]; then
echo "::error::Missing en-GB language directory (${LANG_ROOT}/en-GB)"
ERRORS=$((ERRORS + 1))
fi
if [ ! -d "$LANG_ROOT/en-US" ]; then
echo "::error::Missing en-US language directory (${LANG_ROOT}/en-US)"
ERRORS=$((ERRORS + 1))
fi
# Check that en-GB and en-US have matching .ini files
if [ -d "$LANG_ROOT/en-GB" ] && [ -d "$LANG_ROOT/en-US" ]; then
for GB_INI in "$LANG_ROOT/en-GB"/*.ini; do
[ ! -f "$GB_INI" ] && continue
US_INI="$LANG_ROOT/en-US/$(basename "$GB_INI")"
if [ ! -f "$US_INI" ]; then
echo "::error::$(basename "$GB_INI") exists in en-GB but missing from en-US"
ERRORS=$((ERRORS + 1))
fi
done
for US_INI in "$LANG_ROOT/en-US"/*.ini; do
[ ! -f "$US_INI" ] && continue
GB_INI="$LANG_ROOT/en-GB/$(basename "$US_INI")"
if [ ! -f "$GB_INI" ]; then
echo "::error::$(basename "$US_INI") exists in en-US but missing from en-GB"
ERRORS=$((ERRORS + 1))
fi
done
fi
# Find all .ini language files
INI_FILES=$(find . -path "*/language/*/*.ini" -not -path "./.git/*" 2>/dev/null)
if [ -z "$INI_FILES" ]; then
echo "No .ini language files found"
[ "$ERRORS" -gt 0 ] && exit 1
exit 0
fi
echo "Found $(echo "$INI_FILES" | wc -l) language file(s)"
for FILE in $INI_FILES; do
FNAME=$(basename "$FILE")
LINENUM=0
SEEN_KEYS=""
while IFS= read -r line || [ -n "$line" ]; do
LINENUM=$((LINENUM + 1))
# Skip empty lines and comments
[ -z "$line" ] && continue
echo "$line" | grep -qE '^\s*;' && continue
echo "$line" | grep -qE '^\s*$' && continue
# Must match KEY="VALUE" format
if ! echo "$line" | grep -qE '^[A-Z_][A-Z0-9_]*=".*"$'; then
echo "::error file=${FILE},line=${LINENUM}::Malformed line: ${line}"
ERRORS=$((ERRORS + 1))
continue
fi
# Extract key and check for duplicates
KEY=$(echo "$line" | sed 's/=.*//')
if echo "$SEEN_KEYS" | grep -qx "$KEY"; then
echo "::error file=${FILE},line=${LINENUM}::Duplicate key: ${KEY}"
ERRORS=$((ERRORS + 1))
fi
SEEN_KEYS="${SEEN_KEYS}
${KEY}"
done < "$FILE"
echo " ${FILE}: checked ${LINENUM} lines"
done
# Cross-check en-GB vs en-US key consistency
GB_DIR=$(find . -path "*/language/en-GB" -type d -not -path "./.git/*" 2>/dev/null | head -1)
US_DIR=$(find . -path "*/language/en-US" -type d -not -path "./.git/*" 2>/dev/null | head -1)
if [ -n "$GB_DIR" ] && [ -n "$US_DIR" ]; then
for GB_FILE in "$GB_DIR"/*.ini; do
[ ! -f "$GB_FILE" ] && continue
FNAME=$(basename "$GB_FILE")
US_FILE="$US_DIR/$FNAME"
[ ! -f "$US_FILE" ] && continue
GB_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$GB_FILE" 2>/dev/null | sort)
US_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$US_FILE" 2>/dev/null | sort)
# Keys in en-GB but not en-US
MISSING_US=$(comm -23 <(echo "$GB_KEYS") <(echo "$US_KEYS"))
if [ -n "$MISSING_US" ]; then
echo "::warning::Keys in en-GB/$FNAME but missing from en-US/$FNAME:"
echo "$MISSING_US" | while read -r k; do echo " - $k"; done
WARNINGS=$((WARNINGS + 1))
fi
# Keys in en-US but not en-GB
MISSING_GB=$(comm -13 <(echo "$GB_KEYS") <(echo "$US_KEYS"))
if [ -n "$MISSING_GB" ]; then
echo "::warning::Keys in en-US/$FNAME but missing from en-GB/$FNAME:"
echo "$MISSING_GB" | while read -r k; do echo " - $k"; done
WARNINGS=$((WARNINGS + 1))
fi
done
fi
{
echo "### Language File Validation"
echo "| Metric | Count |"
echo "|---|---|"
echo "| Files checked | $(echo "$INI_FILES" | wc -l) |"
echo "| Errors | ${ERRORS} |"
echo "| Warnings | ${WARNINGS} |"
} >> $GITHUB_STEP_SUMMARY
if [ "$ERRORS" -gt 0 ]; then
echo "::error::Language validation failed with ${ERRORS} error(s)"
exit 1
fi
echo "Language files: OK (${WARNINGS} warning(s))"
- name: Check changelog has unreleased entry
run: |
if [ ! -f "CHANGELOG.md" ]; then
echo "::warning::No CHANGELOG.md found"
exit 0
fi
# Check for content under [Unreleased] section
if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then
echo "::error::CHANGELOG.md missing [Unreleased] section"
exit 1
fi
# Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased
UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true)
if [ "$UNRELEASED_CONTENT" -eq 0 ]; then
echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes."
echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY
echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY
echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]"
- name: Verify package source
run: |
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
if [ ! -d "$SOURCE_DIR" ]; then
echo "::warning::No src/ or htdocs/ directory"
exit 0
fi
FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
echo "Source: ${FILE_COUNT} files"
[ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; }
# ── Pre-Release RC Build ─────────────────────────────────────────────────
pre-release:
name: Build RC Package
runs-on: ubuntu-latest
needs: [branch-policy, validate]
steps:
- name: Trigger RC pre-release
env:
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
REPO: ${{ github.repository }}
BRANCH: ${{ github.head_ref }}
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
run: |
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 "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
# ── Issue Reporter ──────────────────────────────────────────────────────
report-issues:
name: Report Issues
runs-on: ubuntu-latest
needs: [branch-policy, validate]
if: >-
always() &&
needs.validate.result == 'failure'
steps:
- name: Checkout
uses: actions/checkout@v4
with:
sparse-checkout: automation/ci-issue-reporter.sh
sparse-checkout-cone-mode: false
- name: "File issue for PR validation failure"
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."
@@ -1,71 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Validation
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /templates/workflows/joomla/pr-metadata-check.yml.template
# VERSION: 01.00.00
# BRIEF: Validate MokoGitea metadata matches Joomla extension manifest on PRs
name: "Joomla: Metadata Validation"
on:
pull_request:
types: [opened, synchronize, reopened, converted_to_draft, ready_for_review]
permissions:
contents: read
env:
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
jobs:
validate-metadata:
name: "Validate Joomla Metadata"
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup mokocli tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
run: |
if [ -f /opt/mokocli/cli/joomla_metadata_validate.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then
echo Using pre-installed /opt/mokocli
echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV
else
echo Falling back to fresh clone
if ! command -v composer > /dev/null 2>&1; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
fi
rm -rf /tmp/mokocli
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
fi
- name: Validate metadata against Joomla manifest
env:
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
run: |
php ${MOKO_CLI}/joomla_metadata_validate.php \
--path . \
--token "${GITEA_TOKEN}" \
--org "${GITEA_ORG}" \
--repo "${GITEA_REPO}" \
--api-base "${GITEA_URL}/api/v1" \
--ci
if [ $? -ne 0 ]; then
echo "::error::Joomla metadata mismatch — update delivery will fail. Run 'php cli/joomla_metadata_validate.php' locally to see details."
exit 1
fi
+75 -94
View File
@@ -4,26 +4,19 @@
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Release # INGROUP: moko-platform.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli # REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /templates/workflows/universal/pre-release.yml.template # PATH: /templates/workflows/universal/pre-release.yml.template
# VERSION: 05.01.00 # VERSION: 05.01.00
# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches # BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch
name: "Universal: Pre-Release" name: "Universal: Pre-Release"
on: on:
push: pull_request:
types: [closed]
branches: branches:
- dev - dev
- 'fix/**'
- 'patch/**'
- 'hotfix/**'
- 'bugfix/**'
- 'chore/**'
- alpha
- beta
- rc
workflow_dispatch: workflow_dispatch:
inputs: inputs:
stability: stability:
@@ -46,11 +39,11 @@ env:
jobs: jobs:
build: build:
name: "Build Pre-Release (${{ inputs.stability || github.ref_name }})" name: "Build Pre-Release (${{ inputs.stability || 'development' }})"
runs-on: release runs-on: release
if: >- if: >-
github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_dispatch' ||
github.event_name == 'push' (github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev')
steps: steps:
- name: Checkout - name: Checkout
@@ -58,50 +51,32 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
token: ${{ secrets.MOKOGITEA_TOKEN }} token: ${{ secrets.MOKOGITEA_TOKEN }}
ref: ${{ github.ref_name }}
- name: Setup mokocli tools - name: Setup moko-platform tools
env: env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
run: | run: |
# Use pre-installed /opt/mokocli if available (updated by cron every 6h) if ! command -v composer &> /dev/null; then
if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/cli/manifest_element.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
echo Using pre-installed /opt/mokocli
echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV
else
echo Falling back to fresh clone
if ! command -v composer > /dev/null 2>&1; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
fi
rm -rf /tmp/mokocli
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
fi fi
# Always fetch latest CLI tools — never use stale cache from previous runs
rm -rf /tmp/moko-platform-api
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
/tmp/moko-platform-api
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
- name: Detect platform - name: Detect platform
id: platform id: platform
run: | run: |
# Auto-detect and update platform if not set in manifest
php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true
php ${MOKO_CLI}/manifest_read.php --path . --github-output php ${MOKO_CLI}/manifest_read.php --path . --github-output
- name: Resolve metadata and bump version - name: Resolve metadata and bump version
id: meta id: meta
run: | run: |
# Auto-detect stability from branch name on push, or use input on dispatch STABILITY="${{ inputs.stability || 'development' }}"
if [ "${{ github.event_name }}" = "push" ]; then
case "${{ github.ref_name }}" in
rc) STABILITY="release-candidate" ;;
alpha) STABILITY="alpha" ;;
beta) STABILITY="beta" ;;
*) STABILITY="development" ;;
esac
else
STABILITY="${{ inputs.stability || 'development' }}"
fi
case "$STABILITY" in case "$STABILITY" in
development) SUFFIX="-dev"; TAG="development" ;; development) SUFFIX="-dev"; TAG="development" ;;
@@ -110,26 +85,20 @@ jobs:
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;; release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
esac esac
# Bump version via CLI: patch for dev/alpha/beta, minor for RC # Read current version (bump already handled by push workflow)
case "$STABILITY" in VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null)
release-candidate) BUMP="minor" ;; [ -z "$VERSION" ] && VERSION="00.00.01"
*) BUMP="patch" ;;
esac
php ${MOKO_CLI}/version_bump.php --path . $([ "$BUMP" = "minor" ] && echo "--minor") 2>/dev/null || true # Strip any existing suffix from version before applying stability
# Set stability suffix and verify consistency
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "00.00.01")
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//') VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
php ${MOKO_CLI}/version_set_platform.php \ php ${MOKO_CLI}/version_set_platform.php \
--path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true --path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true
# Verify version consistency across all files
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
# Ensure licensing tags (updateservers, dlid) if enabled in manifest.xml # Update VERSION variable with suffix
php ${MOKO_CLI}/manifest_licensing.php --path . --fix 2>/dev/null || true
# Append suffix for output
if [ -n "$SUFFIX" ]; then if [ -n "$SUFFIX" ]; then
VERSION="${VERSION}${SUFFIX}" VERSION="${VERSION}${SUFFIX}"
fi fi
@@ -173,42 +142,7 @@ jobs:
php ${MOKO_CLI}/release_create.php \ php ${MOKO_CLI}/release_create.php \
--path . --version "$VERSION" --tag "$TAG" \ --path . --version "$VERSION" --tag "$TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease --repo "${GITEA_REPO}" --branch dev --prerelease
- name: Update release notes from CHANGELOG.md
run: |
TAG="${{ steps.meta.outputs.tag }}"
VERSION="${{ steps.meta.outputs.version }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
# Extract [Unreleased] section from changelog (everything between [Unreleased] and next ## heading)
if [ -f "CHANGELOG.md" ]; then
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
else
NOTES="Release ${VERSION}"
fi
# Update release body via API
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
"${API_BASE}/releases/tags/${TAG}" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
if [ -n "$RELEASE_ID" ]; then
python3 -c "
import json, urllib.request
body = open('/dev/stdin').read()
payload = json.dumps({'body': body}).encode()
req = urllib.request.Request(
'${API_BASE}/releases/${RELEASE_ID}',
data=payload, method='PATCH',
headers={
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
'Content-Type': 'application/json'
})
urllib.request.urlopen(req)
" <<< "$NOTES"
echo "Release notes updated from CHANGELOG.md"
fi
- name: Build package and upload - name: Build package and upload
id: package id: package
@@ -221,8 +155,55 @@ jobs:
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --output /tmp || true --repo "${GITEA_REPO}" --output /tmp || true
# updates.xml is generated dynamically by MokoGitea license server - name: Update updates.xml
# No need to build, commit, or sync updates.xml from workflows if: steps.platform.outputs.platform == 'joomla'
run: |
VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
SHA256="${{ steps.package.outputs.sha256_zip }}"
if [ ! -f "updates.xml" ]; then
echo "No updates.xml -- skipping"
exit 0
fi
SHA_FLAG=""
[ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
php ${MOKO_CLI}/updates_xml_build.php \
--path . --version "${VERSION}" --stability "${STABILITY}" \
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
${SHA_FLAG}
# Commit and push
if ! git diff --quiet updates.xml 2>/dev/null; then
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git add updates.xml
git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
git push origin HEAD 2>&1 || echo "WARNING: push failed"
fi
- name: "Sync updates.xml to all branches"
if: steps.platform.outputs.platform == 'joomla'
run: |
CURRENT_BRANCH="${{ github.ref_name }}"
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
for BRANCH in main dev; do
[ "$BRANCH" = "$CURRENT_BRANCH" ] && continue
echo "Syncing updates.xml -> ${BRANCH}"
git fetch origin "${BRANCH}" 2>/dev/null || continue
git checkout "origin/${BRANCH}" -- updates.xml 2>/dev/null || continue
git checkout "${CURRENT_BRANCH}" -- updates.xml
if ! git diff --quiet updates.xml 2>/dev/null; then
git add updates.xml
git commit -m "chore: sync updates.xml from ${CURRENT_BRANCH} [skip ci]"
git push origin HEAD:refs/heads/${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed"
fi
git checkout "${CURRENT_BRANCH}" 2>/dev/null
done
- name: "Delete lesser pre-release channels (cascade)" - name: "Delete lesser pre-release channels (cascade)"
continue-on-error: true continue-on-error: true
-66
View File
@@ -1,66 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /.mokogitea/workflows/rc-revert.yml
# VERSION: 09.23.00
# BRIEF: Rename rc/ branch back to dev/ when PR is closed without merge
name: "RC Revert"
on:
pull_request:
types: [closed]
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
revert:
name: Rename rc/ back to dev/
runs-on: ubuntu-latest
if: >-
github.event.pull_request.merged == false &&
startsWith(github.event.pull_request.head.ref, 'rc/')
steps:
- name: Rename branch
run: |
BRANCH="${{ github.event.pull_request.head.ref }}"
SUFFIX="${BRANCH#rc/}"
DEV_BRANCH="dev/${SUFFIX}"
API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
# Create dev/ branch from rc/ branch
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X POST \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"new_branch_name\": \"${DEV_BRANCH}\", \"old_branch_name\": \"${BRANCH}\"}" \
"${API}" 2>/dev/null || true)
if [ "$STATUS" = "201" ]; then
echo "Created branch: ${DEV_BRANCH}" >> $GITHUB_STEP_SUMMARY
else
echo "::error::Failed to create ${DEV_BRANCH} from ${BRANCH} (HTTP ${STATUS})"
exit 1
fi
# Delete rc/ branch
ENCODED=$(php -r "echo rawurlencode('${BRANCH}');")
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \
-H "Authorization: token ${TOKEN}" \
"${API}/${ENCODED}" 2>/dev/null || true)
if [ "$STATUS" = "204" ]; then
echo "Deleted branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
else
echo "::warning::Failed to delete ${BRANCH} (HTTP ${STATUS})"
fi
echo "### RC Reverted" >> $GITHUB_STEP_SUMMARY
echo "${BRANCH} → ${DEV_BRANCH}" >> $GITHUB_STEP_SUMMARY
File diff suppressed because it is too large Load Diff
+98
View File
@@ -0,0 +1,98 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Security
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /.gitea/workflows/security-audit.yml
# VERSION: 01.00.00
# BRIEF: Dependency vulnerability scanning for composer and npm packages
name: "Universal: Security Audit"
on:
schedule:
- cron: '0 6 * * 1' # Weekly on Monday at 06:00 UTC
pull_request:
branches:
- main
paths:
- 'composer.json'
- 'composer.lock'
- 'package.json'
- 'package-lock.json'
workflow_dispatch:
permissions:
contents: read
env:
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }}
jobs:
audit:
name: Dependency Audit
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Composer audit
if: hashFiles('composer.lock') != ''
run: |
echo "=== Composer Security Audit ==="
if ! command -v composer &> /dev/null; then
sudo apt-get update -qq
sudo apt-get install -y -qq php-cli composer >/dev/null 2>&1
fi
composer audit --format=plain 2>&1 | tee /tmp/composer-audit.txt
RESULT=$?
if [ $RESULT -ne 0 ]; then
echo "::warning::Composer vulnerabilities found"
echo "composer_vulnerable=true" >> "$GITHUB_ENV"
else
echo "No known vulnerabilities in composer dependencies"
fi
- name: NPM audit
if: hashFiles('package-lock.json') != ''
run: |
echo "=== NPM Security Audit ==="
npm audit --production 2>&1 | tee /tmp/npm-audit.txt || true
if npm audit --production 2>&1 | grep -q "found 0 vulnerabilities"; then
echo "No known vulnerabilities in npm dependencies"
else
echo "::warning::NPM vulnerabilities found"
echo "npm_vulnerable=true" >> "$GITHUB_ENV"
fi
- name: Notify on vulnerabilities
if: env.composer_vulnerable == 'true' || env.npm_vulnerable == 'true'
run: |
REPO="${{ github.event.repository.name }}"
curl -sS \
-H "Title: ${REPO} has vulnerable dependencies" \
-H "Tags: lock,warning" \
-H "Priority: high" \
-d "Security audit found vulnerabilities. Review dependency updates." \
"${NTFY_URL}/${NTFY_TOPIC}" || true
- name: Joomla version audit
if: always()
run: |
if [ -f "monitoring/joomla-version-audit.php" ] && [ -n "$JOOMLA_SITES" ]; then
echo "$JOOMLA_SITES" > /tmp/sites.json
php monitoring/joomla-version-audit.php --sites /tmp/sites.json || true
echo "### Joomla Version Audit" >> $GITHUB_STEP_SUMMARY
rm -f /tmp/sites.json
else
echo "Joomla audit skipped (no script or JOOMLA_SITES_JSON not configured)"
fi
env:
JOOMLA_SITES: ${{ vars.JOOMLA_SITES_JSON }}
+312
View File
@@ -0,0 +1,312 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /templates/workflows/update-server.yml
# VERSION: 05.00.00
# BRIEF: Pre-release build + update server XML for dev/alpha/beta/rc branches
#
# Thin wrapper around moko-platform CLI tools.
# Builds packages, updates updates.xml, and optionally deploys via SFTP.
#
# Joomla filters update entries by the user's "Minimum Stability" setting.
name: "Update Server"
on:
push:
branches:
- 'dev'
- 'dev/**'
- 'alpha/**'
- 'beta/**'
- 'rc/**'
paths:
- 'src/**'
- 'htdocs/**'
pull_request:
types: [closed]
branches:
- 'dev'
- 'dev/**'
- 'alpha/**'
- 'beta/**'
- 'rc/**'
paths:
- 'src/**'
- 'htdocs/**'
workflow_dispatch:
inputs:
stability:
description: 'Stability tag'
required: true
default: 'development'
type: choice
options:
- development
- alpha
- beta
- rc
- stable
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
permissions:
contents: write
jobs:
update-xml:
name: Update Server
runs-on: release
if: >-
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || github.event_name == 'push'
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 0
- name: Setup moko-platform tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.MOKOGITEA_TOKEN }}"}}}'
run: |
if ! command -v composer &> /dev/null; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
fi
# Always fetch latest CLI tools — never use stale cache from previous runs
rm -rf /tmp/moko-platform
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
/tmp/moko-platform 2>/dev/null || true
if [ -d "/tmp/moko-platform" ] && [ -f "/tmp/moko-platform/composer.json" ]; then
cd /tmp/moko-platform && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
fi
echo "MOKO_CLI=/tmp/moko-platform/cli" >> "$GITHUB_ENV"
- name: Detect platform
id: platform
run: php ${MOKO_CLI}/manifest_read.php --path . --github-output
- name: Resolve stability and bump version
id: meta
run: |
BRANCH="${{ github.ref_name }}"
# Configure git for bot pushes
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
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"
# Auto-bump patch version
php ${MOKO_CLI}/version_bump.php --path . 2>/dev/null || true
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "0.0.0")
# Strip any existing suffix before applying stability
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
# Determine stability from branch or manual input
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
STABILITY="${{ inputs.stability }}"
elif [[ "$BRANCH" == rc/* ]]; then
STABILITY="rc"
elif [[ "$BRANCH" == beta/* ]]; then
STABILITY="beta"
elif [[ "$BRANCH" == alpha/* ]]; then
STABILITY="alpha"
else
STABILITY="development"
fi
# Version suffix per stability stream
case "$STABILITY" in
development) SUFFIX="-dev"; TAG="development" ;;
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
beta) SUFFIX="-beta"; TAG="beta" ;;
rc) SUFFIX="-rc"; TAG="release-candidate" ;;
*) SUFFIX=""; TAG="stable" ;;
esac
# Propagate version with stability suffix to all manifest files
php ${MOKO_CLI}/version_set_platform.php \
--path . --version "$VERSION" --branch "$BRANCH" --stability "$STABILITY" 2>/dev/null || true
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
# Re-read version (now includes suffix from version_set_platform)
if [ -n "$SUFFIX" ]; then
VERSION="${VERSION}${SUFFIX}"
fi
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "display_version=${VERSION}" >> "$GITHUB_OUTPUT"
# Commit version bump if changed
git add -A
git diff --cached --quiet || {
git commit -m "chore(version): auto-bump ${VERSION} [skip ci]" \
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
git push
}
- name: Create release and upload package
id: package
run: |
VERSION="${{ steps.meta.outputs.version }}"
TAG="${{ steps.meta.outputs.tag }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
# Create or update Gitea release
php ${MOKO_CLI}/release_create.php \
--path . --version "$VERSION" --tag "$TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease
# Build package and upload
php ${MOKO_CLI}/release_package.php \
--path . --version "$VERSION" --tag "$TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --output /tmp || true
- name: Update updates.xml
if: steps.platform.outputs.platform == 'joomla'
run: |
VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
SHA256="${{ steps.package.outputs.sha256_zip }}"
if [ ! -f "updates.xml" ]; then
echo "No updates.xml — skipping"
exit 0
fi
SHA_FLAG=""
[ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
php ${MOKO_CLI}/updates_xml_build.php \
--path . --version "${VERSION}" --stability "${STABILITY}" \
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
${SHA_FLAG}
# Commit and push updates.xml
git add updates.xml
git diff --cached --quiet || {
git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
git push
}
- name: Sync updates.xml to main
if: github.ref_name != 'main' && steps.platform.outputs.platform == 'joomla'
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
GITEA_TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
FILE_SHA=$(curl -sf -H "Authorization: token ${GITEA_TOKEN}" \
"${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then
python3 -c "
import base64, json, urllib.request, sys
with open('updates.xml', 'rb') as f:
content = base64.b64encode(f.read()).decode()
payload = json.dumps({
'content': content,
'sha': '${FILE_SHA}',
'message': 'chore: sync updates.xml from ${{ steps.meta.outputs.stability }} [skip ci]',
'branch': 'main'
}).encode()
req = urllib.request.Request(
'${API_BASE}/contents/updates.xml',
data=payload, method='PUT',
headers={
'Authorization': 'token ${GITEA_TOKEN}',
'Content-Type': 'application/json'
})
try:
urllib.request.urlopen(req)
print('updates.xml synced to main')
except Exception as e:
print(f'WARNING: sync to main failed: {e}', file=sys.stderr)
"
fi
- name: SFTP deploy to dev server
if: contains(github.ref, 'dev/') || github.ref == 'refs/heads/dev'
env:
DEV_HOST: ${{ vars.DEV_FTP_HOST }}
DEV_PATH: ${{ vars.DEV_FTP_PATH }}
DEV_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
DEV_USER: ${{ vars.DEV_FTP_USERNAME }}
DEV_PORT: ${{ vars.DEV_FTP_PORT }}
DEV_KEY: ${{ secrets.DEV_FTP_KEY }}
DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
run: |
# Permission check: admin or maintain role required
ACTOR="${{ github.actor }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
"${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \
python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read")
case "$PERMISSION" in
admin|maintain|write) ;;
*)
echo "Deploy denied: ${ACTOR} has '${PERMISSION}' — requires admin, maintain, or write"
exit 0
;;
esac
[ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured — skipping SFTP"; exit 0; }
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
[ ! -d "$SOURCE_DIR" ] && exit 0
PORT="${DEV_PORT:-22}"
REMOTE="${DEV_PATH%/}"
[ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}"
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
"$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json
if [ -n "$DEV_KEY" ]; then
echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
else
printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json
fi
PLATFORM=$(php ${MOKO_CLI}/platform_detect.php --path . 2>/dev/null || true)
if [ "$PLATFORM" = "waas-component" ] && [ -f "${MOKO_CLI}/../deploy/deploy-joomla.php" ]; then
php ${MOKO_CLI}/../deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
elif [ -f "${MOKO_CLI}/../deploy/deploy-sftp.php" ]; then
php ${MOKO_CLI}/../deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
fi
rm -f /tmp/deploy_key /tmp/sftp-config.json
echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY
- name: Summary
if: always()
run: |
VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
DISPLAY="${{ steps.meta.outputs.display_version }}"
echo "## Update Server" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Version | \`${DISPLAY}\` |" >> $GITHUB_STEP_SUMMARY
@@ -1,73 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /.mokogitea/workflows/workflow-sync-trigger.yml
# VERSION: 01.01.00
# BRIEF: Trigger workflow sync to live repos when a PR is merged to main
name: "Universal: Workflow Sync Trigger"
on:
pull_request:
types: [closed]
branches:
- main
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
sync:
name: Sync workflows to live repos
runs-on: ubuntu-latest
if: >-
github.event.pull_request.merged == true &&
!contains(github.event.pull_request.title, '[skip sync]')
steps:
- name: Determine platform from repo name
id: platform
run: |
REPO="${{ github.event.repository.name }}"
case "$REPO" in
Template-Joomla) PLATFORM="joomla" ;;
Template-Dolibarr) PLATFORM="dolibarr" ;;
Template-Go) PLATFORM="go" ;;
Template-MCP) PLATFORM="mcp" ;;
Template-Generic) PLATFORM="" ;;
*) PLATFORM="" ;;
esac
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
echo "Platform: ${PLATFORM:-all}"
- name: Clone mokocli
env:
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
run: |
GITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}"
git clone --depth 1 "${GITEA_URL}/MokoConsulting/mokocli.git" /tmp/mokocli
- name: Install dependencies
run: |
cd /tmp/mokocli
composer install --no-dev --no-interaction --quiet 2>/dev/null || true
- name: Run workflow sync
env:
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
run: |
ARGS="--token ${MOKOGITEA_TOKEN}"
ARGS="${ARGS} --org ${{ vars.GITEA_ORG || github.repository_owner }}"
ARGS="${ARGS} --phase repos"
PLATFORM="${{ steps.platform.outputs.platform }}"
if [ -n "$PLATFORM" ]; then
ARGS="${ARGS} --platform-filter ${PLATFORM}"
fi
php /tmp/mokocli/cli/workflow_sync.php ${ARGS}
+53 -15
View File
@@ -8,27 +8,65 @@
DEFGROUP: Joomla.Template.Site DEFGROUP: Joomla.Template.Site
INGROUP: MokoOnyx.Documentation INGROUP: MokoOnyx.Documentation
PATH: ./CHANGELOG.md PATH: ./CHANGELOG.md
VERSION: 02.27.03 <<<<<<< HEAD
<<<<<<< HEAD
VERSION: 02.17.00
BRIEF: Changelog file documenting version history of MokoOnyx BRIEF: Changelog file documenting version history of MokoOnyx
--> -->
# Changelog — MokoOnyx (VERSION: 02.27.03) # Changelog — MokoOnyx (VERSION: 02.17.00)
## [Unreleased] ## [Unreleased]
=======
VERSION: 02.17.00
BRIEF: Changelog file documenting version history of MokoOnyx
-->
# Changelog — MokoOnyx (VERSION: 02.17.00)
>>>>>>> origin/main
=======
VERSION: 02.17.00
BRIEF: Changelog file documenting version history of MokoOnyx
-->
# Changelog — MokoOnyx (VERSION: 02.17.00)
>>>>>>> origin/main
## [02.17.00] --- 2026-05-30
## [02.15.00] --- 2026-05-30
=======
VERSION: 02.17.00
BRIEF: Changelog file documenting version history of MokoOnyx
-->
# Changelog — MokoOnyx (VERSION: 02.17.00)
>>>>>>> origin/main
## [02.14.00] --- 2026-05-30
## [02.13.00] --- 2026-05-30
### Added ### Added
- Collapsible floating social bar with toggle button and localStorage persistence - Hero image (`hero.jpg`) to template images
- Bootstrap tooltips on all social icon links showing platform name on hover - `.fa-solid`, `.fa-regular`, `.fa-brands`, `.fa-light` icon margin spacing
- URL validation on social platform fields (browser, Joomla server-side, and PHP layout logging) - `.blog-item .item-image` fixed 250px height with object-fit cover
- Hide header on home page option (`hideHeaderHome` template param)
- Hide main menu on home page option (`hideMenuHome` template param)
- Three distinct menu overrides: mainmenu (collapsible), horizontal (always visible), default (vertical)
- `<php_minimum>8.1.0</php_minimum>` to templateDetails.xml
- `<changelogurl>` support in updates.xml
- CONTRIBUTING.md with universal workflow and version policy
### Changed
- Release pipeline rework: independent update streams, CLI-driven workflows
- Version bumps only trigger on `src/` changes (not docs/config)
- Branch protection: CI bot only for push, force push disabled
- Auto-bump supports dev, rc, feature/*, patch/* branches
### Fixed ### Fixed
- Moved footer social icons below footer-menu and footer module positions for correct display order - Joomla update loop caused by version mismatch between ZIP and updates.xml
- Duplicate hamburger menu on mobile
- Stacked version suffixes (-dev-dev-dev)
- Template name doubling (Template - Template - MokoOnyx)
## [02.26.00] --- 2026-06-20 ## [02.08.00] --- 2026-05-29
## [02.25.00] --- 2026-06-20
## [02.25.00] --- 2026-06-20
## [02.24.00] --- 2026-06-20
## [02.24.00] --- 2026-06-20
+42
View File
@@ -0,0 +1,42 @@
# CLAUDE.md
This file provides guidance to Claude Code when working with this repository.
## Project Overview
**MokoOnyx** -- MokoOnyx - Joomla site template (successor to MokoCassiopeia)
| Field | Value |
|---|---|
| **Platform** | joomla |
| **Language** | PHP |
| **Default branch** | main |
| **License** | GPL-3.0-or-later |
| **Wiki** | [MokoOnyx Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/wiki) |
| **Standards** | [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home) |
## Common Commands
```bash
composer install # Install PHP dependencies
```
## Architecture
This is a Joomla extension. Key directories:
- `src/` -- extension source (deployed to Joomla)
- `src/*.xml` -- manifest file (version, files, params)
- `src/src/` or `src/services/` -- PHP classes
- `src/language/` -- translation strings
- `src/media/` -- CSS/JS/images
## Rules
- **Workflow directory**: `.mokogitea/` (not `.gitea/` or `.github/`)
- **Never commit** `.claude/`, `.mcp.json`, `TODO.md`, or `*.min.css`/`*.min.js`
- **Attribution**: use `Authored-by: Moko Consulting` in commits
- **Branch strategy**: develop on `dev`, merge to `main` for release
- **Minification**: handled at build time (CI) and runtime (MokoMinifyHelper for Joomla templates)
- **Wiki**: documentation lives in the Gitea wiki, not in `docs/` files
- **Standards**: this repo follows [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)
+161 -161
View File
@@ -1,161 +1,161 @@
# Contributing to Moko Consulting Projects # Contributing to Moko Consulting Projects
Thank you for your interest in contributing. All Moko Consulting repositories follow this universal workflow and version policy. Thank you for your interest in contributing. All Moko Consulting repositories follow this universal workflow and version policy.
## Branching Workflow ## Branching Workflow
``` ```
feature/* ──PR──> dev ──draft PR──> (renamed to rc) ──merge──> main feature/* ──PR──> dev ──draft PR──> (renamed to rc) ──merge──> main
``` ```
### Step by step ### Step by step
1. **Create a feature branch** from `dev`: 1. **Create a feature branch** from `dev`:
```bash ```bash
git checkout dev && git pull git checkout dev && git pull
git checkout -b feature/my-change git checkout -b feature/my-change
``` ```
2. **Work and commit** on your feature branch. Push to origin. 2. **Work and commit** on your feature branch. Push to origin.
3. **Open a PR**: `feature/my-change` → `dev`. After review and checks, merge it. 3. **Open a PR**: `feature/my-change` → `dev`. After review and checks, merge it.
4. **When ready for release**, open a **draft PR**: `dev` → `main`. 4. **When ready for release**, open a **draft PR**: `dev` → `main`.
- This automatically renames the source branch to `rc` (release candidate) - This automatically renames the source branch to `rc` (release candidate)
- An RC pre-release is built and uploaded - An RC pre-release is built and uploaded
5. **Alpha and beta branches** are created by manually renaming the branch before the RC stage: 5. **Alpha and beta branches** are created by manually renaming the branch before the RC stage:
- Rename `dev` to `alpha` for early testing → alpha pre-release is built - Rename `dev` to `alpha` for early testing → alpha pre-release is built
- Rename `alpha` to `beta` for feature-complete testing → beta pre-release is built - Rename `alpha` to `beta` for feature-complete testing → beta pre-release is built
- When the draft PR is created, the branch is renamed to `rc` - When the draft PR is created, the branch is renamed to `rc`
6. **Once PR checks pass** on the `rc` branch, mark the PR as ready and merge to `main`. 6. **Once PR checks pass** on the `rc` branch, mark the PR as ready and merge to `main`.
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)
- Gitea 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`
### Branch summary ### Branch summary
| Branch | Purpose | Created by | | Branch | Purpose | Created by |
|--------|---------|-----------| |--------|---------|-----------|
| `feature/*` | New features and fixes | Developer | | `feature/*` | New features and fixes | Developer |
| `dev` | Integration branch | Auto-recreated after release | | `dev` | Integration branch | Auto-recreated after release |
| `alpha` | Alpha pre-release testing | Manual rename from `dev` | | `alpha` | Alpha pre-release testing | Manual rename from `dev` |
| `beta` | Beta pre-release testing | Manual rename from `alpha` | | `beta` | Beta pre-release testing | Manual rename from `alpha` |
| `rc` | Release candidate | Auto-renamed on draft PR to main | | `rc` | Release candidate | Auto-renamed on draft PR to main |
| `main` | Stable releases | Protected, merge only | | `main` | Stable releases | Protected, merge only |
| `version/XX.YY.ZZ` | Archived release snapshots | Auto-created by CI | | `version/XX.YY.ZZ` | Archived release snapshots | Auto-created by CI |
### Protected branches ### Protected branches
| Branch | Direct push | Merge via | | Branch | Direct push | Merge via |
|--------|------------|-----------| |--------|------------|-----------|
| `main` | Blocked (CI bot whitelisted) | PR merge only | | `main` | Blocked (CI bot whitelisted) | PR merge only |
| `dev` | Blocked (CI bot whitelisted) | PR merge from feature/* | | `dev` | Blocked (CI bot whitelisted) | PR merge from feature/* |
| `rc` | Blocked (CI bot whitelisted) | Auto-created on draft PR | | `rc` | Blocked (CI bot whitelisted) | Auto-created on draft PR |
| `alpha` | Blocked (CI bot whitelisted) | Manual rename | | `alpha` | Blocked (CI bot whitelisted) | Manual rename |
| `beta` | Blocked (CI bot whitelisted) | Manual rename | | `beta` | Blocked (CI bot whitelisted) | Manual rename |
| `feature/*` | Open | N/A (source branch) | | `feature/*` | Open | N/A (source branch) |
## Version Policy ## Version Policy
### Format ### Format
All versions use `XX.YY.ZZ` — three two-digit segments, zero-padded: All versions use `XX.YY.ZZ` — three two-digit segments, zero-padded:
- **XX** — Major version (breaking changes) - **XX** — Major version (breaking changes)
- **YY** — Minor version (new features, bumped on release to main) - **YY** — Minor version (new features, bumped on release to main)
- **ZZ** — Patch version (auto-incremented on every push to dev/feature branches) - **ZZ** — Patch version (auto-incremented on every push to dev/feature branches)
Rollover: patch `99` → `00` increments minor; minor `99` → `00` increments major. Rollover: patch `99` → `00` increments minor; minor `99` → `00` increments major.
### Stability suffixes ### Stability suffixes
Each branch appends a suffix to indicate stability: Each branch appends a suffix to indicate stability:
| Branch | Suffix | Example | | Branch | Suffix | Example |
|--------|--------|---------| |--------|--------|---------|
| `main` | (none) | `02.09.00` | | `main` | (none) | `02.09.00` |
| `dev` | `-dev` | `02.09.01-dev` | | `dev` | `-dev` | `02.09.01-dev` |
| `feature/*` | `-dev` | `02.09.01-dev` | | `feature/*` | `-dev` | `02.09.01-dev` |
| `alpha` | `-alpha` | `02.09.01-alpha` | | `alpha` | `-alpha` | `02.09.01-alpha` |
| `beta` | `-beta` | `02.09.01-beta` | | `beta` | `-beta` | `02.09.01-beta` |
| `rc` | `-rc` | `02.09.01-rc` | | `rc` | `-rc` | `02.09.01-rc` |
### Auto version bump ### Auto version bump
On every push to `dev`, `feature/*`, or `patch/*`: On every push to `dev`, `feature/*`, or `patch/*`:
1. Patch version incremented 1. Patch version incremented
2. Stability suffix `-dev` applied 2. Stability suffix `-dev` applied
3. All version-bearing files updated (manifests, CHANGELOG, PHP headers, etc.) 3. All version-bearing files updated (manifests, CHANGELOG, PHP headers, etc.)
4. Commit created with `[skip ci]` to avoid loops 4. Commit created with `[skip ci]` to avoid loops
### Release version flow ### Release version flow
Version bumps happen at specific release events: Version bumps happen at specific release events:
| Event | Bump | Example | | Event | Bump | Example |
|-------|------|---------| |-------|------|---------|
| Feature merged to dev | Patch bump after dev release | `02.09.01-dev` → release → `02.09.02-dev` | | Feature merged to dev | Patch bump after dev release | `02.09.01-dev` → release → `02.09.02-dev` |
| Dev promoted to RC | Minor bump | `02.09.02-dev` → `02.10.00-rc` | | Dev promoted to RC | Minor bump | `02.09.02-dev` → `02.10.00-rc` |
| RC merged to main | Minor bump | `02.10.00-rc` → `02.11.00` (stable) | | RC merged to main | Minor bump | `02.10.00-rc` → `02.11.00` (stable) |
| Dev recreated from main | Patch bump | `02.11.00` → `02.11.01-dev` | | Dev recreated from main | Patch bump | `02.11.00` → `02.11.01-dev` |
### Release stream copies ### Release stream copies
When a higher-stability release is published, copies are created for all lesser streams with the same base version: When a higher-stability release is published, copies are created for all lesser streams with the same base version:
- **RC `02.10.00-rc`** also creates: `02.10.00-dev`, `02.10.00-alpha`, `02.10.00-beta` - **RC `02.10.00-rc`** also creates: `02.10.00-dev`, `02.10.00-alpha`, `02.10.00-beta`
- **Stable `02.11.00`** also creates: `02.11.00-dev`, `02.11.00-alpha`, `02.11.00-beta`, `02.11.00-rc` - **Stable `02.11.00`** also creates: `02.11.00-dev`, `02.11.00-alpha`, `02.11.00-beta`, `02.11.00-rc`
This ensures Joomla sites on ANY stability channel see the update (Joomla only shows versions higher than what's installed). This ensures Joomla sites on ANY stability channel see the update (Joomla only shows versions higher than what's installed).
### Version files ### Version files
The version tools update all files containing version stamps: The version tools update all files containing version stamps:
- `.mokogitea/manifest.xml` (canonical source) - `.mokogitea/manifest.xml` (canonical source)
- Joomla XML manifests (`<version>` tag) - Joomla XML manifests (`<version>` tag)
- `README.md`, `CHANGELOG.md` (`VERSION:` pattern) - `README.md`, `CHANGELOG.md` (`VERSION:` pattern)
- `package.json`, `pyproject.toml` - `package.json`, `pyproject.toml`
- Any text file with a `VERSION: XX.YY.ZZ` label - Any text file with a `VERSION: XX.YY.ZZ` label
Files synced from other repos (with a `# REPO:` header) are not touched. Files synced from other repos (with a `# REPO:` header) are not touched.
## Code Standards ## Code Standards
- **PHP**: PSR-12, tabs for indentation - **PHP**: PSR-12, tabs for indentation
- **Copyright**: all files must include the Moko Consulting copyright header - **Copyright**: all files must include the Moko Consulting copyright header
- **License**: SPDX identifier `GPL-3.0-or-later` (or as specified per repo) - **License**: SPDX identifier `GPL-3.0-or-later` (or as specified per repo)
- **Attribution**: use `Authored-by: Moko Consulting` in commits, not individual names - **Attribution**: use `Authored-by: Moko Consulting` in commits, not individual names
## Commit Messages ## Commit Messages
Use conventional commit format: Use conventional commit format:
``` ```
type(scope): short description type(scope): short description
Optional body with context. Optional body with context.
Authored-by: Moko Consulting Authored-by: Moko Consulting
``` ```
Types: `feat`, `fix`, `chore`, `docs`, `style`, `refactor`, `test`, `ci` Types: `feat`, `fix`, `chore`, `docs`, `style`, `refactor`, `test`, `ci`
Special flags in commit messages: Special flags in commit messages:
- `[skip ci]` — skip all CI workflows - `[skip ci]` — skip all CI workflows
- `[skip bump]` — skip auto version bump only - `[skip bump]` — skip auto version bump only
## Reporting Issues ## Reporting Issues
Use the repository's issue tracker with the appropriate template. Use the repository's issue tracker with the appropriate template.
--- ---
*Moko Consulting <hello@mokoconsulting.tech>* *Moko Consulting <hello@mokoconsulting.tech>*
+1 -3
View File
@@ -12,7 +12,7 @@ A modern, lightweight Joomla site template built on Cassiopeia with Font Awesome
| | | | | |
|---|---| |---|---|
| **Type** | Joomla Site Template | | **Type** | Joomla Site Template |
| **Version** | 02.22.00 | | **Version** | 02.07.00 |
| **Joomla** | 5.x / 6.x | | **Joomla** | 5.x / 6.x |
| **PHP** | 8.1+ | | **PHP** | 8.1+ |
| **License** | GPL-3.0-or-later | | **License** | GPL-3.0-or-later |
@@ -39,7 +39,6 @@ On install, MokoOnyx automatically migrates settings, content references, and cu
| **Table of Contents** | Automatic TOC generation for long articles | | **Table of Contents** | Automatic TOC generation for long articles |
| **GTM / GA4** | Google Tag Manager and Analytics integration with smart visitor detection (login status, user group, page type) | | **GTM / GA4** | Google Tag Manager and Analytics integration with smart visitor detection (login status, user group, page type) |
| **Template Overrides** | Overrides for all core Joomla modules, Community Builder, and DPCalendar | | **Template Overrides** | Overrides for all core Joomla modules, Community Builder, and DPCalendar |
| **Social Icons** | Configurable social media links in topbar, footer, or collapsible floating sidebar with tooltips and URL validation |
| **Cassiopeia Base** | Minimal core overrides for maximum Joomla upgrade compatibility | | **Cassiopeia Base** | Minimal core overrides for maximum Joomla upgrade compatibility |
--- ---
@@ -80,7 +79,6 @@ Key parameters include:
- **Google Tag Manager**: GTM container ID - **Google Tag Manager**: GTM container ID
- **GA4**: Measurement ID and tracking options - **GA4**: Measurement ID and tracking options
- **Table of Contents**: Auto-generate TOC for articles with heading threshold - **Table of Contents**: Auto-generate TOC for articles with heading threshold
- **Social Icons**: Display positions, icon style, and platform URLs
--- ---
+11 -1
View File
@@ -10,7 +10,17 @@
INGROUP: MokoOnyx.Governance INGROUP: MokoOnyx.Governance
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
FILE: SECURITY.md FILE: SECURITY.md
VERSION: 02.27.03 <<<<<<< HEAD
<<<<<<< HEAD
VERSION: 02.17.00
=======
VERSION: 02.17.00
=======
VERSION: 02.17.00
=======
VERSION: 02.17.00
>>>>>>> origin/main
>>>>>>> origin/main
BRIEF: Security policy and vulnerability reporting process for MokoOnyx. BRIEF: Security policy and vulnerability reporting process for MokoOnyx.
PATH: /SECURITY.md PATH: /SECURITY.md
NOTE: This policy is process oriented and does not replace secure engineering practices. NOTE: This policy is process oriented and does not replace secure engineering practices.
@@ -1,126 +0,0 @@
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
/**
* Social icons layout — rendered by index.php in topbar and/or footer.
*
* Expected $displayData keys:
* 'params' => Joomla\Registry\Registry (template params)
* 'position' => string ('topbar' | 'footer' | 'floating')
*/
$params = $displayData['params'];
$position = $displayData['position'] ?? 'footer';
// Platform definitions: key => [FA icon class, language key for aria-label]
$platforms = [
'facebook' => ['fa-brands fa-facebook-f', 'TPL_MOKOONYX_SOCIAL_FACEBOOK'],
'twitter' => ['fa-brands fa-x-twitter', 'TPL_MOKOONYX_SOCIAL_TWITTER'],
'instagram' => ['fa-brands fa-instagram', 'TPL_MOKOONYX_SOCIAL_INSTAGRAM'],
'linkedin' => ['fa-brands fa-linkedin-in', 'TPL_MOKOONYX_SOCIAL_LINKEDIN'],
'youtube' => ['fa-brands fa-youtube', 'TPL_MOKOONYX_SOCIAL_YOUTUBE'],
'github' => ['fa-brands fa-github', 'TPL_MOKOONYX_SOCIAL_GITHUB'],
'bluesky' => ['fa-brands fa-bluesky', 'TPL_MOKOONYX_SOCIAL_BLUESKY'],
'threads' => ['fa-brands fa-threads', 'TPL_MOKOONYX_SOCIAL_THREADS'],
'discord' => ['fa-brands fa-discord', 'TPL_MOKOONYX_SOCIAL_DISCORD'],
'tiktok' => ['fa-brands fa-tiktok', 'TPL_MOKOONYX_SOCIAL_TIKTOK'],
'reddit' => ['fa-brands fa-reddit-alien', 'TPL_MOKOONYX_SOCIAL_REDDIT'],
'pinterest' => ['fa-brands fa-pinterest-p', 'TPL_MOKOONYX_SOCIAL_PINTEREST'],
'snapchat' => ['fa-brands fa-snapchat', 'TPL_MOKOONYX_SOCIAL_SNAPCHAT'],
'telegram' => ['fa-brands fa-telegram', 'TPL_MOKOONYX_SOCIAL_TELEGRAM'],
'whatsapp' => ['fa-brands fa-whatsapp', 'TPL_MOKOONYX_SOCIAL_WHATSAPP'],
'tumblr' => ['fa-brands fa-tumblr', 'TPL_MOKOONYX_SOCIAL_TUMBLR'],
'twitch' => ['fa-brands fa-twitch', 'TPL_MOKOONYX_SOCIAL_TWITCH'],
'spotify' => ['fa-brands fa-spotify', 'TPL_MOKOONYX_SOCIAL_SPOTIFY'],
'soundcloud' => ['fa-brands fa-soundcloud', 'TPL_MOKOONYX_SOCIAL_SOUNDCLOUD'],
'flickr' => ['fa-brands fa-flickr', 'TPL_MOKOONYX_SOCIAL_FLICKR'],
'vimeo' => ['fa-brands fa-vimeo-v', 'TPL_MOKOONYX_SOCIAL_VIMEO'],
'linktree' => ['fa-solid fa-link', 'TPL_MOKOONYX_SOCIAL_LINKTREE'],
'mail' => ['fa-solid fa-envelope', 'TPL_MOKOONYX_SOCIAL_MAIL'],
];
// Collect enabled platforms (those with a non-empty URL)
$active = [];
foreach ($platforms as $key => [$iconClass, $langKey]) {
$url = trim((string) $params->get('social_' . $key . '_url', ''));
if ($url === '') {
continue;
}
if (!preg_match('#^(https?://[^\s<>"]+|mailto:[^\s<>"]+|/[^\s<>"]*)$#i', $url)) {
\Joomla\CMS\Log\Log::add(
'MokoOnyx social: skipped invalid URL for "' . $key . '": ' . $url,
\Joomla\CMS\Log\Log::WARNING,
'template'
);
continue;
}
$active[] = [
'url' => $url,
'iconClass' => $iconClass,
'label' => Text::_($langKey),
];
}
if (empty($active)) {
return;
}
$style = in_array($params->get('social_icon_style'), ['plain', 'square', 'circle', 'rounded'], true)
? $params->get('social_icon_style') : 'plain';
$size = in_array($params->get('social_icon_size'), ['sm', 'md', 'lg'], true)
? $params->get('social_icon_size') : 'md';
$floatingPos = in_array($params->get('social_floating_pos'), ['left', 'right'], true)
? $params->get('social_floating_pos') : 'left';
$colorMode = in_array($params->get('social_icon_color'), ['theme', 'brand', 'black', 'white'], true)
? $params->get('social_icon_color') : 'theme';
$listClass = 'moko-social-icons';
$listClass .= ' moko-social-icons--' . htmlspecialchars($style, ENT_QUOTES, 'UTF-8');
$listClass .= ' moko-social-icons--' . htmlspecialchars($size, ENT_QUOTES, 'UTF-8');
$listClass .= ' moko-social-icons--' . htmlspecialchars($position, ENT_QUOTES, 'UTF-8');
$listClass .= ' moko-social-icons--color-' . $colorMode;
if ($position === 'floating') {
$listClass .= ' moko-social-icons--floating-' . $floatingPos;
}
?>
<?php if ($position === 'floating') : ?>
<div class="moko-social-floating-wrap moko-social-floating-wrap--<?php echo htmlspecialchars($floatingPos, ENT_QUOTES, 'UTF-8'); ?>"
id="mokoSocialFloating">
<?php endif; ?>
<nav class="<?php echo $listClass; ?>" aria-label="<?php echo Text::_('TPL_MOKOONYX_SOCIAL_NAV_LABEL'); ?>">
<ul>
<?php foreach ($active as $item) : ?>
<li>
<a href="<?php echo htmlspecialchars($item['url'], ENT_QUOTES, 'UTF-8'); ?>"
target="_blank"
rel="noopener noreferrer"
aria-label="<?php echo htmlspecialchars($item['label'], ENT_QUOTES, 'UTF-8'); ?>"
data-bs-toggle="tooltip"
data-bs-placement="<?php echo $position === 'floating' ? ($floatingPos === 'left' ? 'right' : 'left') : 'top'; ?>"
title="<?php echo htmlspecialchars($item['label'], ENT_QUOTES, 'UTF-8'); ?>">
<span class="<?php echo htmlspecialchars($item['iconClass'], ENT_QUOTES, 'UTF-8'); ?>" aria-hidden="true"></span>
</a>
</li>
<?php endforeach; ?>
</ul>
</nav>
<?php if ($position === 'floating') : ?>
<button type="button"
class="moko-social-floating-toggle"
id="mokoSocialFloatingToggle"
aria-label="<?php echo Text::_('TPL_MOKOONYX_SOCIAL_FLOATING_TOGGLE'); ?>"
aria-expanded="true">
<span class="fa-solid fa-chevron-<?php echo $floatingPos === 'left' ? 'left' : 'right'; ?>" aria-hidden="true"></span>
</button>
</div>
<?php endif; ?>
View File
@@ -236,11 +236,11 @@ use Joomla\CMS\Log\Log;
// Update the update server // Update the update server
try { try {
$onyxUpdatesUrl = 'https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/updates.xml'; $onyxUpdatesUrl = 'https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/main/updates.xml';
$query = $db->getQuery(true) $query = $db->getQuery(true)
->update('#__update_sites') ->update('#__update_sites')
->set($db->quoteName('location') . ' = ' . $db->quote($onyxUpdatesUrl)) ->set($db->quoteName('location') . ' = ' . $db->quote($onyxUpdatesUrl))
->set($db->quoteName('name') . ' = ' . $db->quote('Template - MokoOnyx')) ->set($db->quoteName('name') . ' = ' . $db->quote($newDisplay))
->where($db->quoteName('location') . ' LIKE ' . $db->quote('%MokoCassiopeia%')); ->where($db->quoteName('location') . ' LIKE ' . $db->quote('%MokoCassiopeia%'));
$db->setQuery($query)->execute(); $db->setQuery($query)->execute();
$n = $db->getAffectedRows(); $n = $db->getAffectedRows();
@@ -161,7 +161,7 @@ class MokoMinifyHelper
$js = preg_replace('/\s*([{}();,=+\-*\/<>!&|?:])\s*/', '$1', $js); $js = preg_replace('/\s*([{}();,=+\-*\/<>!&|?:])\s*/', '$1', $js);
// Restore necessary spaces (after keywords) // Restore necessary spaces (after keywords)
$js = preg_replace('/\b(var|let|const|return|typeof|instanceof|new|delete|throw|case|in|of)\b([^\s;})><=!&|?:,])/', '$1 $2', $js); $js = preg_replace('/(var|let|const|return|typeof|instanceof|new|delete|throw|case|in|of)([^\s;})><=!&|?:,])/', '$1 $2', $js);
return trim($js); return trim($js);
} }
@@ -0,0 +1,61 @@
<?php
/**
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: MokoOnyx.Override
* INGROUP: MokoOnyx
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
* PATH: /html/com_joomgallery/category/default.php
<<<<<<< HEAD
<<<<<<< HEAD
* VERSION: 02.17.00
=======
* VERSION: 02.17.00
=======
* VERSION: 02.17.00
=======
* VERSION: 02.17.00
>>>>>>> origin/main
>>>>>>> origin/main
* BRIEF: Category view override — password gate then loads default_cat sub-layout
*/
// No direct access
defined('_JEXEC') or die;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Language\Text;
use Joomla\CMS\HTML\HTMLHelper;
// Import CSS & JS
$wa = $this->document->getWebAssetManager();
$wa->useStyle('com_joomgallery.site');
$wa->useStyle('com_joomgallery.jg-icon-font');
?>
<?php if ($this->item->pw_protected) : ?>
<div class="com-joomgallery-category--locked">
<form action="<?php echo Route::_('index.php?task=category.unlock&catid=' . $this->item->id); ?>" method="post" class="row g-3 align-items-end" autocomplete="off">
<div class="col-12">
<h3><i class="jg-icon-lock me-2" aria-hidden="true"></i><?php echo Text::_('COM_JOOMGALLERY_CATEGORY_PASSWORD_PROTECTED'); ?></h3>
</div>
<div class="col-auto">
<label for="jg_password" class="form-label"><?php echo Text::_('JGLOBAL_PASSWORD'); ?></label>
<input type="password" name="password" id="jg_password" class="form-control" required />
</div>
<div class="col-auto">
<button type="submit" class="btn btn-primary" id="jg_unlock_button">
<i class="jg-icon-unlock me-1" aria-hidden="true"></i><?php echo Text::_('COM_JOOMGALLERY_CATEGORY_BUTTON_UNLOCK'); ?>
</button>
</div>
<?php echo HTMLHelper::_('form.token'); ?>
</form>
</div>
<?php else : ?>
<?php echo $this->loadTemplate('cat'); ?>
<?php endif; ?>
@@ -0,0 +1,229 @@
<?php
/**
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: MokoOnyx.Override
* INGROUP: MokoOnyx
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
* PATH: /html/com_joomgallery/category/default_cat.php
<<<<<<< HEAD
<<<<<<< HEAD
* VERSION: 02.17.00
=======
* VERSION: 02.17.00
=======
* VERSION: 02.17.00
=======
* VERSION: 02.17.00
>>>>>>> origin/main
>>>>>>> origin/main
* BRIEF: Category sub-layout — subcategories grid + images grid with pagination
*/
// No direct access
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Session\Session;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Layout\LayoutHelper;
use Joomgallery\Component\Joomgallery\Administrator\Helper\JoomHelper;
// Image params
$image_type = $this->params['configs']->get('jg_category_view_type_image', 'thumbnail', 'STRING');
$gallery_class = $this->params['configs']->get('jg_category_view_class', 'masonry', 'STRING');
$num_columns = $this->params['configs']->get('jg_category_view_num_columns', 3, 'INT');
$image_class = $this->params['configs']->get('jg_category_view_image_class', 0, 'INT');
$justified_height = $this->params['configs']->get('jg_category_view_justified_height', 200, 'INT');
$justified_gap = $this->params['configs']->get('jg_category_view_justified_gap', 5, 'INT');
$image_link = $this->params['configs']->get('jg_category_view_image_link', 'defaultview', 'STRING');
$lightbox_image = $this->params['configs']->get('jg_category_view_lightbox_image', 'detail', 'STRING');
$pagination_type = $this->params['configs']->get('jg_category_view_pagination', 0, 'INT');
$show_subcategories = $this->params['configs']->get('jg_category_view_subcategories', 1, 'INT');
$subcategory_type_image = $this->params['configs']->get('jg_category_view_type_subcategory_image', 'thumbnail', 'STRING');
$num_columns_subcats = $this->params['configs']->get('jg_category_view_subcategories_num_columns', 3, 'INT');
// Import CSS & JS
$wa = $this->document->getWebAssetManager();
if ($gallery_class == 'masonry') {
$wa->useScript('com_joomgallery.masonry');
}
if ($gallery_class == 'justified') {
$wa->useScript('com_joomgallery.justified');
$wa->addInlineStyle('.jg-images[class*=" justified-"] .jg-image-caption-hover { right: ' . $justified_gap . 'px; }');
}
$lightbox = false;
if ($image_link == 'lightgallery') {
$lightbox = true;
$wa->useScript('com_joomgallery.lightgallery');
$wa->useScript('com_joomgallery.lg-thumbnail');
$wa->useStyle('com_joomgallery.lightgallery-bundle');
}
// Initialise the grid script
$iniJS = 'window.joomGrid = {';
$iniJS .= ' itemid: ' . $this->item->id . ',';
$iniJS .= ' pagination: ' . $pagination_type . ',';
$iniJS .= ' layout: "' . $gallery_class . '",';
$iniJS .= ' num_columns: ' . $num_columns . ',';
$iniJS .= ' lightbox: ' . ($lightbox ? 'true' : 'false') . ',';
$iniJS .= ' justified: {height: ' . $justified_height . ', gap: ' . $justified_gap . '}';
$iniJS .= '};';
$wa->addInlineScript($iniJS, ['position' => 'before'], [], ['com_joomgallery.joomgrid']);
$wa->useScript('com_joomgallery.joomgrid');
// Access check
$canEdit = $this->getAcl()->checkACL('edit', 'com_joomgallery.category', $this->item->id, $this->item->parent_id, true);
$canAdd = $this->getAcl()->checkACL('add', 'com_joomgallery.image', 0, $this->item->id, true);
$canDelete = $this->getAcl()->checkACL('delete', 'com_joomgallery.category', $this->item->id, $this->item->parent_id, true);
$canCheckin = $this->getAcl()->checkACL('editstate', 'com_joomgallery.category', $this->item->id, $this->item->parent_id, true) || $this->item->checked_out == Factory::getUser()->id;
$returnURL = base64_encode(JoomHelper::getViewRoute('category', $this->item->id, $this->item->parent_id, $this->item->language, $this->getLayout()));
$hasSubcats = $show_subcategories && !empty($this->item->children->items) && count($this->item->children->items) > 0;
$hasImages = !empty($this->item->images->items) && count($this->item->images->items) > 0;
?>
<div class="com-joomgallery-category" itemscope itemtype="https://schema.org/ImageGallery">
<?php // Page heading ?>
<?php if ($this->params['menu']->get('show_page_heading')) : ?>
<div class="page-header">
<h1><?php echo $this->escape($this->params['menu']->get('page_heading')); ?></h1>
</div>
<?php endif; ?>
<?php // Category title ?>
<h2 itemprop="name"><?php echo $this->escape($this->item->title); ?></h2>
<?php // Category description ?>
<?php if (!empty($this->item->description)) : ?>
<div class="com-joomgallery-category__description mb-3" itemprop="description">
<?php echo $this->item->description; ?>
</div>
<?php endif; ?>
<?php // Admin buttons ?>
<?php if ($canEdit || $canCheckin || $canAdd || $canDelete) : ?>
<div class="com-joomgallery-category__actions btn-toolbar mb-3" role="toolbar" aria-label="<?php echo Text::_('JTOOLBAR'); ?>">
<?php if ($canCheckin && $this->item->checked_out > 0) : ?>
<a class="btn btn-outline-secondary btn-sm me-2" href="<?php echo Route::_('index.php?option=com_joomgallery&task=category.checkin&id=' . $this->item->id . '&return=' . $returnURL . '&' . Session::getFormToken() . '=1'); ?>">
<i class="jg-icon-checkin me-1" aria-hidden="true"></i><?php echo Text::_('JLIB_HTML_CHECKIN'); ?>
</a>
<?php endif; ?>
<?php if ($canEdit) : ?>
<a class="btn btn-outline-primary btn-sm me-2<?php echo ($this->item->checked_out > 0) ? ' disabled' : ''; ?>" href="<?php echo Route::_('index.php?option=com_joomgallery&task=category.edit&id=' . $this->item->id . '&return=' . $returnURL); ?>">
<i class="jg-icon-edit me-1" aria-hidden="true"></i><?php echo Text::_('JGLOBAL_EDIT'); ?>
</a>
<?php endif; ?>
<?php if ($canAdd) : ?>
<a class="btn btn-outline-success btn-sm me-2<?php echo ($this->item->checked_out > 0) ? ' disabled' : ''; ?>" href="<?php echo Route::_('index.php?option=com_joomgallery&task=image.add&catid=' . $this->item->id . '&return=' . $returnURL); ?>">
<i class="jg-icon-upload me-1" aria-hidden="true"></i><?php echo Text::_('COM_JOOMGALLERY_IMG_UPLOAD_IMAGE'); ?>
</a>
<?php endif; ?>
<?php if ($canDelete) : ?>
<a class="btn btn-outline-danger btn-sm<?php echo ($this->item->checked_out > 0) ? ' disabled' : ''; ?>" href="#deleteCatModal" role="button" data-bs-toggle="modal">
<i class="jg-icon-delete me-1" aria-hidden="true"></i><?php echo Text::_('JACTION_DELETE'); ?>
</a>
<?php echo HTMLHelper::_(
'bootstrap.renderModal',
'deleteCatModal',
[
'title' => Text::_('JACTION_DELETE'),
'modalWidth' => '50',
'bodyHeight' => '100',
'footer' => '<button class="btn btn-secondary" data-bs-dismiss="modal">' . Text::_('JCANCEL') . '</button>'
. '<a href="' . Route::_('index.php?option=com_joomgallery&task=category.remove&id=' . $this->item->id . '&return=' . $returnURL . '&' . Session::getFormToken() . '=1', false, 2) . '" class="btn btn-danger">' . Text::_('JACTION_DELETE') . '</a>',
],
Text::_('COM_JOOMGALLERY_COMMON_ALERT_SURE_DELETE_SELECTED_ITEM')
); ?>
<?php endif; ?>
</div>
<?php endif; ?>
<?php // Subcategories ?>
<?php if ($hasSubcats) : ?>
<section class="com-joomgallery-category__subcategories mb-4" aria-label="<?php echo Text::_('COM_JOOMGALLERY_SUBCATEGORIES'); ?>">
<h3><?php echo Text::_('COM_JOOMGALLERY_SUBCATEGORIES'); ?></h3>
<?php
$catsData = [
'id' => (int) $this->item->id,
'items' => $this->item->children->items,
'num_columns' => (int) $num_columns_subcats,
'image_type' => $subcategory_type_image,
];
echo LayoutHelper::render('joomgallery.grids.categories', $catsData);
?>
</section>
<?php endif; ?>
<?php // Images ?>
<?php if ($hasImages) : ?>
<section class="com-joomgallery-category__images" aria-label="<?php echo Text::_('COM_JOOMGALLERY_IMAGES'); ?>">
<?php if ($hasSubcats) : ?>
<h3><?php echo Text::_('COM_JOOMGALLERY_IMAGES'); ?></h3>
<?php endif; ?>
<?php
$imgsData = [
'id' => (int) $this->item->id,
'layout' => $gallery_class,
'items' => $this->item->images->items,
'num_columns' => (int) $num_columns,
'caption_align' => 'center',
'image_class' => $image_class,
'image_type' => $image_type,
'lightbox_type' => $lightbox_image,
'image_link' => $image_link,
'image_title' => false,
'title_link' => 'defaultview',
'image_desc' => false,
'image_date' => false,
'image_author' => false,
'image_tags' => false,
];
echo LayoutHelper::render('joomgallery.grids.images', $imgsData);
?>
<?php // Pagination ?>
<nav class="mt-4" aria-label="<?php echo Text::_('JLIB_HTML_PAGINATION'); ?>">
<?php echo $this->item->images->pagination->getListFooter(); ?>
</nav>
</section>
<?php elseif (!$hasSubcats) : ?>
<div class="alert alert-info" role="alert">
<p class="mb-0"><?php echo Text::_('COM_JOOMGALLERY_GALLERY_NO_IMAGES'); ?></p>
</div>
<?php endif; ?>
<?php // Back to parent category ?>
<?php if ($this->item->parent_id > 0 && $this->item->parent_id != 1) : ?>
<div class="mt-4">
<a class="btn btn-outline-secondary" href="<?php echo Route::_('index.php?option=com_joomgallery&view=category&id=' . (int) $this->item->parent_id); ?>">
<i class="jg-icon-arrow-left-alt me-1" aria-hidden="true"></i><?php echo Text::_('COM_JOOMGALLERY_BACK'); ?>
</a>
</div>
<?php endif; ?>
<script>
if (window.joomGrid.layout != 'justified') {
document.querySelectorAll('.' + window.joomGrid.imgclass).forEach(function(image) {
image.addEventListener('load', function() {
this.closest('.' + window.joomGrid.imgboxclass).classList.add('loaded');
});
});
}
</script>
</div>
@@ -0,0 +1,148 @@
<?php
/**
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: MokoOnyx.Override
* INGROUP: MokoOnyx
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
* PATH: /html/com_joomgallery/gallery/default.php
<<<<<<< HEAD
<<<<<<< HEAD
* VERSION: 02.17.00
=======
* VERSION: 02.17.00
=======
* VERSION: 02.17.00
=======
* VERSION: 02.17.00
>>>>>>> origin/main
>>>>>>> origin/main
* BRIEF: Gallery view override — main image grid with masonry/justified layout
*/
// No direct access
defined('_JEXEC') or die;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Layout\LayoutHelper;
// Image params
$image_type = $this->params['configs']->get('jg_gallery_view_type_image', 'thumbnail', 'STRING');
$gallery_class = $this->params['configs']->get('jg_gallery_view_class', 'masonry', 'STRING');
$num_columns = $this->params['configs']->get('jg_gallery_view_num_columns', 3, 'INT');
$image_class = $this->params['configs']->get('jg_gallery_view_image_class', 0, 'INT');
$justified_height = $this->params['configs']->get('jg_gallery_view_justified_height', 200, 'INT');
$justified_gap = $this->params['configs']->get('jg_gallery_view_justified_gap', 5, 'INT');
$image_link = $this->params['configs']->get('jg_gallery_view_image_link', 'defaultview', 'STRING');
$lightbox_image = $this->params['configs']->get('jg_category_view_lightbox_image', 'detail', 'STRING');
$browse_categories_link = $this->params['configs']->get('jg_gallery_view_browse_categories_link', 1, 'INT');
// Import CSS & JS
$wa = $this->document->getWebAssetManager();
$wa->useStyle('com_joomgallery.site');
$wa->useStyle('com_joomgallery.jg-icon-font');
if ($gallery_class == 'masonry') {
$wa->useScript('com_joomgallery.masonry');
}
if ($gallery_class == 'justified') {
$wa->useScript('com_joomgallery.justified');
$wa->addInlineStyle('.jg-images[class*=" justified-"] .jg-image-caption-hover { right: ' . $justified_gap . 'px; }');
}
$lightbox = false;
if ($image_link == 'lightgallery') {
$lightbox = true;
$wa->useScript('com_joomgallery.lightgallery');
$wa->useScript('com_joomgallery.lg-thumbnail');
$wa->useStyle('com_joomgallery.lightgallery-bundle');
}
// Initialise the grid script
$iniJS = 'window.joomGrid = {';
$iniJS .= ' itemid: ' . $this->item->id . ',';
$iniJS .= ' pagination: 0,';
$iniJS .= ' layout: "' . $gallery_class . '",';
$iniJS .= ' num_columns: ' . $num_columns . ',';
$iniJS .= ' lightbox: ' . ($lightbox ? 'true' : 'false') . ',';
$iniJS .= ' justified: {height: ' . $justified_height . ', gap: ' . $justified_gap . '}';
$iniJS .= '};';
$wa->addInlineScript($iniJS, ['position' => 'before'], [], ['com_joomgallery.joomgrid']);
$wa->useScript('com_joomgallery.joomgrid');
?>
<div class="com-joomgallery-gallery" itemscope itemtype="https://schema.org/ImageGallery">
<?php if ($this->params['menu']->get('show_page_heading')) : ?>
<div class="page-header">
<h1><?php echo $this->escape($this->params['menu']->get('page_heading')); ?></h1>
</div>
<?php endif; ?>
<?php // Browse categories link (top) ?>
<?php if ($browse_categories_link == '1') : ?>
<div class="text-center mb-4">
<a class="btn btn-outline-primary" href="<?php echo Route::_('index.php?option=com_joomgallery&view=category&id=1'); ?>">
<i class="jg-icon-folder me-1" aria-hidden="true"></i><?php echo Text::_('COM_JOOMGALLERY_GALLERY_VIEW_BROWSE_CATEGORIES'); ?>
</a>
</div>
<?php endif; ?>
<?php if (count($this->item->images->items) == 0) : ?>
<div class="alert alert-info" role="alert">
<p class="mb-0"><?php echo Text::_('COM_JOOMGALLERY_GALLERY_NO_IMAGES'); ?></p>
</div>
<?php else : ?>
<?php
$imgsData = [
'id' => (int) $this->item->id,
'layout' => $gallery_class,
'items' => $this->item->images->items,
'num_columns' => (int) $num_columns,
'caption_align' => 'center',
'image_class' => $image_class,
'image_type' => $image_type,
'lightbox_type' => $lightbox_image,
'image_link' => $image_link,
'image_title' => false,
'title_link' => 'defaultview',
'image_desc' => false,
'image_date' => false,
'image_author' => false,
'image_tags' => false,
];
?>
<?php echo LayoutHelper::render('joomgallery.grids.images', $imgsData); ?>
<?php // Pagination ?>
<nav class="mt-4" aria-label="<?php echo Text::_('JLIB_HTML_PAGINATION'); ?>">
<?php echo $this->item->images->pagination->getListFooter(); ?>
</nav>
<?php endif; ?>
<?php // Browse categories link (bottom) ?>
<?php if ($browse_categories_link == '2') : ?>
<div class="text-center mt-4">
<a class="btn btn-outline-primary" href="<?php echo Route::_('index.php?option=com_joomgallery&view=category&id=1'); ?>">
<i class="jg-icon-folder me-1" aria-hidden="true"></i><?php echo Text::_('COM_JOOMGALLERY_GALLERY_VIEW_BROWSE_CATEGORIES'); ?>
</a>
</div>
<?php endif; ?>
<script>
if (window.joomGrid.layout != 'justified') {
document.querySelectorAll('.' + window.joomGrid.imgclass).forEach(function(image) {
image.addEventListener('load', function() {
this.closest('.' + window.joomGrid.imgboxclass).classList.add('loaded');
});
});
}
</script>
</div>
+259
View File
@@ -0,0 +1,259 @@
<?php
/**
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: MokoOnyx.Override
* INGROUP: MokoOnyx
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
* PATH: /html/com_joomgallery/image/default.php
<<<<<<< HEAD
<<<<<<< HEAD
* VERSION: 02.17.00
=======
* VERSION: 02.17.00
=======
* VERSION: 02.17.00
=======
* VERSION: 02.17.00
>>>>>>> origin/main
>>>>>>> origin/main
* BRIEF: Image detail view override — single image with metadata, tags, custom fields
*/
// No direct access
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Session\Session;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Layout\FileLayout;
use Joomla\CMS\User\UserFactoryInterface;
use Joomla\Component\Fields\Administrator\Helper\FieldsHelper;
use Joomgallery\Component\Joomgallery\Administrator\Helper\JoomHelper;
// Image params
$image_type = $this->params['configs']->get('jg_detail_view_type_image', 'detail', 'STRING');
$show_title = $this->params['configs']->get('jg_detail_view_show_title', 0, 'INT');
$show_category = $this->params['configs']->get('jg_detail_view_show_category', 0, 'INT');
$show_description = $this->params['configs']->get('jg_detail_view_show_description', 0, 'INT');
$show_imgdate = $this->params['configs']->get('jg_detail_view_show_imgdate', 0, 'INT');
$show_imgauthor = $this->params['configs']->get('jg_detail_view_show_imgauthor', 0, 'INT');
$show_created_by = $this->params['configs']->get('jg_detail_view_show_created_by', 0, 'INT');
$show_votes = $this->params['configs']->get('jg_detail_view_show_votes', 0, 'INT');
$show_rating = $this->params['configs']->get('jg_detail_view_show_rating', 0, 'INT');
$show_hits = $this->params['configs']->get('jg_detail_view_show_hits', 0, 'INT');
$show_downloads = $this->params['configs']->get('jg_detail_view_show_downloads', 0, 'INT');
$show_tags = $this->params['configs']->get('jg_detail_view_show_tags', 0, 'INT');
$show_metadata = $this->params['configs']->get('jg_detail_view_show_metadata', 0, 'INT');
// Import CSS & JS
$wa = $this->document->getWebAssetManager();
$wa->useStyle('com_joomgallery.site');
$wa->useStyle('com_joomgallery.jg-icon-font');
// Access check
$canEdit = $this->getAcl()->checkACL('edit', 'com_joomgallery.image', $this->item->id, $this->item->catid, true);
$canDelete = $this->getAcl()->checkACL('delete', 'com_joomgallery.image', $this->item->id, $this->item->catid, true);
$canCheckin = $this->getAcl()->checkACL('editstate', 'com_joomgallery.image', $this->item->id, $this->item->catid, true) || $this->item->checked_out == Factory::getUser()->id;
$returnURL = base64_encode(JoomHelper::getViewRoute('image', $this->item->id, $this->item->catid, $this->item->language, $this->getLayout()));
// Tags
$tagLayout = new FileLayout('joomgallery.content.tags');
$tags = $tagLayout->render($this->item->tags);
// Metadata
$metadataLayout = new FileLayout('joomgallery.content.metadata');
$metadata = $metadataLayout->render($this->item->imgmetadata);
// Custom Fields
$fields = FieldsHelper::getFields('com_joomgallery.image', $this->item);
// Check if we have any info rows to show
$hasInfo = $show_category || $show_imgdate || $show_imgauthor || $show_created_by
|| $show_votes || $show_rating || $show_hits || $show_downloads
|| $show_tags || $show_metadata || count($fields) > 0;
?>
<div class="com-joomgallery-image" itemscope itemtype="https://schema.org/ImageObject">
<?php if ($show_title) : ?>
<h2 itemprop="name"><?php echo $this->escape($this->item->title); ?></h2>
<?php endif; ?>
<?php // Back to category ?>
<a class="btn btn-outline-primary btn-sm mb-3" href="<?php echo Route::_('index.php?option=com_joomgallery&view=category&id=' . (int) $this->item->catid); ?>">
<i class="jg-icon-arrow-left-alt me-1" aria-hidden="true"></i><?php echo Text::_('COM_JOOMGALLERY_IMAGE_BACK_TO_CATEGORY') . ' ' . $this->escape($this->item->cattitle); ?>
</a>
<?php // Admin buttons ?>
<?php if ($canEdit || $canCheckin || $canDelete) : ?>
<div class="com-joomgallery-image__actions btn-toolbar mb-3" role="toolbar" aria-label="<?php echo Text::_('JTOOLBAR'); ?>">
<?php if ($canCheckin && $this->item->checked_out > 0) : ?>
<a class="btn btn-outline-secondary btn-sm me-2" href="<?php echo Route::_('index.php?option=com_joomgallery&task=image.checkin&id=' . $this->item->id . '&return=' . $returnURL . '&' . Session::getFormToken() . '=1'); ?>">
<i class="jg-icon-checkin me-1" aria-hidden="true"></i><?php echo Text::_('JLIB_HTML_CHECKIN'); ?>
</a>
<?php endif; ?>
<?php if ($canEdit) : ?>
<a class="btn btn-outline-primary btn-sm me-2<?php echo ($this->item->checked_out > 0) ? ' disabled' : ''; ?>" href="<?php echo Route::_('index.php?option=com_joomgallery&task=image.edit&id=' . $this->item->id . '&return=' . $returnURL); ?>">
<i class="jg-icon-edit me-1" aria-hidden="true"></i><?php echo Text::_('JGLOBAL_EDIT'); ?>
</a>
<?php endif; ?>
<?php if ($canDelete) : ?>
<a class="btn btn-outline-danger btn-sm<?php echo ($this->item->checked_out > 0) ? ' disabled' : ''; ?>" href="#deleteImgModal" role="button" data-bs-toggle="modal">
<i class="jg-icon-delete me-1" aria-hidden="true"></i><?php echo Text::_('JACTION_DELETE'); ?>
</a>
<?php echo HTMLHelper::_(
'bootstrap.renderModal',
'deleteImgModal',
[
'title' => Text::_('JACTION_DELETE'),
'modalWidth' => '50',
'bodyHeight' => '100',
'footer' => '<button class="btn btn-secondary" data-bs-dismiss="modal">' . Text::_('JCANCEL') . '</button>'
. '<a href="' . Route::_('index.php?option=com_joomgallery&task=image.remove&id=' . $this->item->id . '&return=' . $returnURL . '&' . Session::getFormToken() . '=1', false, 2) . '" class="btn btn-danger">' . Text::_('COM_JOOMGALLERY_COMMON_DELETE_IMAGE_TIPCAPTION') . '</a>',
],
Text::_('COM_JOOMGALLERY_COMMON_ALERT_SURE_DELETE_SELECTED_ITEM')
); ?>
<?php endif; ?>
</div>
<?php endif; ?>
<?php // Image ?>
<figure class="figure com-joomgallery-image__figure text-center w-100 mb-4">
<div id="jg-loader"></div>
<img
src="<?php echo JoomHelper::getImg($this->item, $image_type); ?>"
class="figure-img img-fluid rounded"
alt="<?php echo $this->escape($this->item->title); ?>"
style="width:auto;"
itemprop="contentUrl"
loading="lazy"
/>
<?php if ($show_description && !empty($this->item->description)) : ?>
<figcaption class="figure-caption" itemprop="description"><?php echo $this->item->description; ?></figcaption>
<?php endif; ?>
</figure>
<?php // Image info table ?>
<?php if ($hasInfo) : ?>
<div class="com-joomgallery-image__info">
<h3><?php echo Text::_('COM_JOOMGALLERY_IMAGE_INFO'); ?></h3>
<table class="table table-striped">
<tbody>
<?php if ($show_category) : ?>
<tr>
<th scope="row"><?php echo Text::_('JCATEGORY'); ?></th>
<td>
<a href="<?php echo Route::_('index.php?option=com_joomgallery&view=category&id=' . (int) $this->item->catid); ?>">
<?php echo $this->escape($this->item->cattitle); ?>
</a>
</td>
</tr>
<?php endif; ?>
<?php if ($show_imgdate) : ?>
<tr>
<th scope="row"><?php echo Text::_('COM_JOOMGALLERY_DATE'); ?></th>
<td>
<time datetime="<?php echo HTMLHelper::_('date', $this->item->date, 'c'); ?>" itemprop="dateCreated">
<?php echo HTMLHelper::_('date', $this->item->date, Text::_('DATE_FORMAT_LC4')); ?>
</time>
</td>
</tr>
<?php endif; ?>
<?php if ($show_imgauthor) : ?>
<tr>
<th scope="row"><?php echo Text::_('JAUTHOR'); ?></th>
<td itemprop="author"><?php echo $this->escape($this->item->author); ?></td>
</tr>
<?php endif; ?>
<?php if ($show_created_by) : ?>
<?php $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($this->item->created_by); ?>
<tr>
<th scope="row"><?php echo Text::_('COM_JOOMGALLERY_OWNER'); ?></th>
<td><?php echo $this->escape($user->name); ?></td>
</tr>
<?php endif; ?>
<?php if ($show_votes) : ?>
<tr>
<th scope="row"><?php echo Text::_('COM_JOOMGALLERY_VOTES'); ?></th>
<td><?php echo $this->escape($this->item->votes); ?></td>
</tr>
<?php endif; ?>
<?php if ($show_rating) : ?>
<tr>
<th scope="row"><?php echo Text::_('COM_JOOMGALLERY_IMAGE_RATING'); ?></th>
<td><?php echo $this->escape($this->item->rating); ?></td>
</tr>
<?php endif; ?>
<?php if ($show_hits) : ?>
<tr>
<th scope="row"><?php echo Text::_('JGLOBAL_HITS'); ?></th>
<td><?php echo (int) $this->item->hits; ?></td>
</tr>
<?php endif; ?>
<?php if ($show_downloads) : ?>
<tr>
<th scope="row"><?php echo Text::_('COM_JOOMGALLERY_DOWNLOADS'); ?></th>
<td><?php echo (int) $this->item->downloads; ?></td>
</tr>
<?php endif; ?>
<?php if ($show_tags) : ?>
<tr>
<th scope="row"><?php echo Text::_('COM_JOOMGALLERY_TAGS'); ?></th>
<td><?php echo $tags; ?></td>
</tr>
<?php endif; ?>
<?php if ($show_metadata) : ?>
<tr>
<th scope="row"><?php echo Text::_('COM_JOOMGALLERY_IMGMETADATA'); ?></th>
<td><?php echo $metadata; ?></td>
</tr>
<?php endif; ?>
<?php // Custom fields ?>
<?php if (count($fields) > 0) : ?>
<tr>
<th scope="row" colspan="2"><strong><?php echo Text::_('JGLOBAL_FIELDS'); ?></strong></th>
</tr>
<?php foreach ($fields as $field) : ?>
<?php if ($this->component->getAccess()->checkViewLevel($field->access) && $field->params->get('display') > 0) : ?>
<tr class="<?php echo $this->escape($field->params->get('render_class')); ?>">
<?php if ($field->params->get('showlabel', true)) : ?>
<th scope="row" class="<?php echo $this->escape($field->params->get('label_render_class')); ?>"><?php echo $this->escape($field->title); ?></th>
<?php else : ?>
<th scope="row"></th>
<?php endif; ?>
<td class="<?php echo $this->escape($field->params->get('value_render_class')); ?>"><?php echo $field->value; ?></td>
</tr>
<?php endif; ?>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<?php endif; ?>
<script>
window.onload = function() {
var el = document.querySelector('#jg-loader');
if (el) el.classList.add('hidden');
};
</script>
</div>
@@ -10,7 +10,17 @@
* INGROUP: MokoOnyx * INGROUP: MokoOnyx
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx * REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
* PATH: /html/layouts/joomla/module/card.php * PATH: /html/layouts/joomla/module/card.php
* VERSION: 02.27.03 <<<<<<< HEAD
<<<<<<< HEAD
* VERSION: 02.17.00
=======
* VERSION: 02.17.00
=======
* VERSION: 02.17.00
=======
* VERSION: 02.17.00
>>>>>>> origin/main
>>>>>>> origin/main
* BRIEF: Custom card module chrome renders module titles for all modules * BRIEF: Custom card module chrome renders module titles for all modules
*/ */
@@ -11,7 +11,17 @@
* INGROUP: MokoOnyx.Layouts * INGROUP: MokoOnyx.Layouts
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx * REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
* PATH: /src/html/layouts/mokoonyx/article-metadata.php * PATH: /src/html/layouts/mokoonyx/article-metadata.php
* VERSION: 02.27.03 <<<<<<< HEAD
<<<<<<< HEAD
* VERSION: 02.17.00
=======
* VERSION: 02.17.00
=======
* VERSION: 02.17.00
=======
* VERSION: 02.17.00
>>>>>>> origin/main
>>>>>>> origin/main
* BRIEF: Article metadata footer layout -- renders jcfields grouped by field group * BRIEF: Article metadata footer layout -- renders jcfields grouped by field group
*/ */
+1
View File
@@ -0,0 +1 @@
<!DOCTYPE html><title></title>
+1
View File
@@ -0,0 +1 @@
<!DOCTYPE html><title></title>
@@ -0,0 +1 @@
<!DOCTYPE html><title></title>
@@ -33,8 +33,6 @@ if ($item->anchor_rel) {
$linktype = $item->title; $linktype = $item->title;
if ($item->menu_icon) { if ($item->menu_icon) {
// Strip Joomla-injected padding classes that conflict with FA icon sizing
$item->menu_icon = trim(preg_replace('/\bp-[0-5]\b/', '', $item->menu_icon));
if ($itemParams->get('menu_text', 1)) { if ($itemParams->get('menu_text', 1)) {
$linktype = '<span class="' . $item->menu_icon . '" aria-hidden="true"></span>' . $item->title; $linktype = '<span class="' . $item->menu_icon . '" aria-hidden="true"></span>' . $item->title;
} else { } else {

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