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
226 changed files with 2430 additions and 3448 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>
+66 -66
View File
@@ -1,66 +1,66 @@
# 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/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)
name: "Universal: Auto Version Bump" name: "Universal: Auto Version Bump"
on: on:
push: push:
branches: branches:
- dev - dev
- rc - rc
- 'feature/**' - 'feature/**'
- 'patch/**' - 'patch/**'
env: env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
permissions: permissions:
contents: write contents: write
jobs: jobs:
bump: bump:
name: Version Bump name: Version Bump
runs-on: release runs-on: release
if: >- if: >-
!contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[skip ci]') &&
!contains(github.event.head_commit.message, '[skip bump]') && !contains(github.event.head_commit.message, '[skip bump]') &&
!startsWith(github.event.head_commit.message, 'Merge pull request') !startsWith(github.event.head_commit.message, 'Merge pull request')
steps: steps:
- name: Checkout - name: Checkout
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
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
run: | run: |
php ${MOKO_CLI}/version_auto_bump.php \ php ${MOKO_CLI}/version_auto_bump.php \
--path . --branch "${GITHUB_REF_NAME}" \ --path . --branch "${GITHUB_REF_NAME}" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" \ --token "${{ secrets.MOKOGITEA_TOKEN }}" \
--repo-url "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" --repo-url "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
+270 -469
View File
@@ -1,469 +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.01.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, synchronize, closed] types: [opened, closed]
branches: branches:
- main - main
paths-ignore: workflow_dispatch:
- '.mokogitea/workflows/**' inputs:
- '*.md' action:
- 'wiki/**' description: 'Action to perform'
- '.editorconfig' required: false
- '.gitignore' type: choice
- '.gitattributes' default: release
- '.gitmessage' options:
- 'LICENSE' - release
workflow_dispatch: - promote-rc
inputs:
action: env:
description: 'Action to perform' FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
required: false GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
type: choice GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
default: release GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
options:
- release permissions:
- promote-rc contents: write
env: jobs:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true # ── PR Opened → Rename branch to RC and build RC release ─────────────────────
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} promote-rc:
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} name: Promote to RC
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} runs-on: release
if: >-
permissions: (github.event.action == 'opened' && github.event.pull_request.merged != true) ||
contents: write (github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
jobs: steps:
# ── PR Opened → Rename branch to RC and build RC release ───────────────────────── - name: Checkout repository
promote-rc: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
name: Promote to RC with:
runs-on: release token: ${{ secrets.MOKOGITEA_TOKEN }}
if: >- fetch-depth: 1
(github.event.action == 'opened' && github.event.pull_request.merged != true) ||
(github.event.action == 'synchronize' && github.event.pull_request.merged != true) || - name: Setup moko-platform tools
(github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc') env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
steps: MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
- name: Checkout repository run: |
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 if ! command -v composer &> /dev/null; then
with: 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
token: ${{ secrets.MOKOGITEA_TOKEN }} fi
fetch-depth: 1 # Always fetch latest CLI tools — never use stale cache from previous runs
submodules: recursive rm -rf /tmp/moko-platform-api
git clone --depth 1 --branch main --quiet \
- name: Setup mokocli tools "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
env: /tmp/moko-platform-api
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} cd /tmp/moko-platform-api
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting composer install --no-dev --no-interaction --quiet
run: |
if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then - name: Rename branch to rc
echo Using pre-installed /opt/mokocli run: |
echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV php /tmp/moko-platform-api/cli/branch_rename.php \
else --from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
echo Falling back to fresh clone --token "${{ secrets.MOKOGITEA_TOKEN }}" \
if ! command -v composer > /dev/null 2>&1; then --api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
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 --pr "${{ github.event.pull_request.number }}"
fi
rm -rf /tmp/mokocli - name: Checkout rc and configure git
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git run: |
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli git fetch origin rc
cd /tmp/mokocli git checkout rc
composer install --no-dev --no-interaction --quiet git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV git config --local user.name "gitea-actions[bot]"
fi git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
- name: Rename branch to rc - name: Publish RC release
run: | run: |
php ${MOKO_CLI}/branch_rename.php \ php /tmp/moko-platform-api/cli/release_publish.php \
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \ --path . --stability rc --bump minor --branch rc \
--token "${{ secrets.MOKOGITEA_TOKEN }}" \ --token "${{ secrets.MOKOGITEA_TOKEN }}"
--api-base "${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
--pr "${{ github.event.pull_request.number }}" - name: Summary
if: always()
- name: Checkout rc and configure git run: |
run: | echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
git fetch origin rc echo "Branch renamed to rc, minor bump, RC + lesser stream releases built, updates.xml synced" >> $GITHUB_STEP_SUMMARY
git checkout rc
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" # ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
git config --local user.name "gitea-actions[bot]" release:
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" name: Build & Release Pipeline
runs-on: release
- name: Publish RC release if: >-
run: | github.event.pull_request.merged == true ||
php ${MOKO_CLI}/release_publish.php \ (github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
--path . --stability rc --bump minor --branch rc \
--token "${{ secrets.MOKOGITEA_TOKEN }}" steps:
- name: Checkout repository
- name: Update RC release notes from CHANGELOG.md uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
run: | with:
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" token: ${{ secrets.MOKOGITEA_TOKEN }}
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" fetch-depth: 0
# Extract [Unreleased] section from changelog - name: Configure git for bot pushes
NOTES="" run: |
if [ -f "CHANGELOG.md" ]; then git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md) git config --local user.name "gitea-actions[bot]"
fi git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
[ -z "$NOTES" ] && NOTES="Release candidate"
- name: Setup moko-platform tools
# Find the RC release and update its body env:
RELEASE_ID=$(curl -sf -H "Authorization: token ${TOKEN}" \ MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
"${API_BASE}/releases/tags/release-candidate" \ MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
| python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
run: |
if [ -n "$RELEASE_ID" ]; then # Ensure PHP + Composer are available
python3 -c " if ! command -v composer &> /dev/null; then
import json, urllib.request 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
body = open('/dev/stdin').read() fi
payload = json.dumps({'body': body}).encode() # Always fetch latest CLI tools — never use stale cache from previous runs
req = urllib.request.Request( rm -rf /tmp/moko-platform-api
'${API_BASE}/releases/${RELEASE_ID}', git clone --depth 1 --branch main --quiet \
data=payload, method='PATCH', "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
headers={ /tmp/moko-platform-api
'Authorization': 'token ${TOKEN}', cd /tmp/moko-platform-api
'Content-Type': 'application/json' composer install --no-dev --no-interaction --quiet
})
urllib.request.urlopen(req)
" <<< "$NOTES" - name: "Publish stable release"
echo "RC release notes updated from CHANGELOG.md" run: |
fi php /tmp/moko-platform-api/cli/release_publish.php \
--path . --stability stable --bump minor --branch main \
- name: Summary --token "${{ secrets.MOKOGITEA_TOKEN }}"
if: always()
run: | # -- STEP 9: Mirror to GitHub (stable only) --------------------------------
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY - name: "Step 9: Mirror release to GitHub"
echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY if: >-
steps.version.outputs.skip != 'true' &&
# ── Merged PR → Build & Release (or promote RC to stable) ───────────────────────── secrets.GH_MIRROR_TOKEN != ''
release: continue-on-error: true
name: Build & Release Pipeline run: |
runs-on: release VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
if: >- RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
github.event.pull_request.merged == true || GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
(github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc') API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php /tmp/moko-platform-api/cli/release_mirror.php \
steps: --version "$VERSION" --tag "$RELEASE_TAG" \
- name: Checkout repository --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 --gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
with: --branch main 2>&1 || true
token: ${{ secrets.MOKOGITEA_TOKEN }} echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY
fetch-depth: 0
submodules: recursive # -- STEP 10: Sync main branch to GitHub mirror ----------------------------
- name: "Step 10: Push main to GitHub mirror"
- name: Configure git for bot pushes if: >-
run: | steps.version.outputs.skip != 'true' &&
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" secrets.GH_MIRROR_TOKEN != ''
git config --local user.name "gitea-actions[bot]" continue-on-error: true
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" run: |
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
- name: Check for merge conflict markers GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
run: | GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
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) git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
if [ -n "$CONFLICTS" ]; then git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
echo "::error::Merge conflict markers found — aborting release" git fetch origin main --depth=1
echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY git push github origin/main:refs/heads/main --force 2>/dev/null \
echo '```' >> $GITHUB_STEP_SUMMARY && echo "main branch pushed to GitHub mirror" \
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY || echo "WARNING: GitHub mirror push failed"
echo '```' >> $GITHUB_STEP_SUMMARY
exit 1 - name: "Step 11: Delete rc branch and recreate dev from main"
fi if: steps.version.outputs.skip != 'true'
echo "No conflict markers found" continue-on-error: true
run: |
- name: Setup mokocli tools API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
env: TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting # Delete rc branch (ephemeral — created by promote-rc)
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}' curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
run: | "${API_BASE}/branches/rc" 2>/dev/null \
if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then && echo "Deleted rc branch" || echo "rc branch not found"
echo Using pre-installed /opt/mokocli
echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV # Delete dev branch
else curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
echo Falling back to fresh clone "${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
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 # Recreate dev from main (now includes version bump + changelog promotion)
fi curl -sf -X POST -H "Authorization: token ${TOKEN}" \
rm -rf /tmp/mokocli -H "Content-Type: application/json" \
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git "${API_BASE}/branches" \
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli -d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
cd /tmp/mokocli
composer install --no-dev --no-interaction --quiet echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
fi - name: "Step 12: Create version branch from main"
if: steps.version.outputs.skip != 'true'
- name: "Detect platform" continue-on-error: true
id: platform run: |
run: | API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
php ${MOKO_CLI}/manifest_read.php --path . --github-output 2>/dev/null || true VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
BRANCH_NAME="version/${VERSION}"
- name: "Determine version bump level" MAIN_SHA=$(git rev-parse HEAD)
id: bump
run: | # Delete old version branch if it exists (same version re-release)
# Fix/patch branches: version was already bumped by pre-release, just strip suffix curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}"
# Feature/dev branches: bump minor for the new stable release
HEAD_REF="${{ github.event.pull_request.head.ref || 'dev' }}" # Create version/XX.YY.ZZ from main
case "$HEAD_REF" in 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"
fix/*|patch/*|hotfix/*|bugfix/*) BUMP="none" ;;
*) BUMP="minor" ;; echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY
esac
echo "level=${BUMP}" >> "$GITHUB_OUTPUT"
echo "Bump level: ${BUMP} (from branch: ${HEAD_REF})"
# -- Dolibarr post-release: Reset dev version -----------------------------
- name: "Publish stable release" - name: "Post-release: Reset dev version"
run: | if: steps.version.outputs.skip != 'true'
BUMP_FLAG="" continue-on-error: true
if [ "${{ steps.bump.outputs.level }}" != "none" ]; then run: |
BUMP_FLAG="--bump ${{ steps.bump.outputs.level }}" API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
fi php /tmp/moko-platform-api/cli/version_reset_dev.php \
php ${MOKO_CLI}/release_publish.php \ --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
--path . --stability stable ${BUMP_FLAG} --branch main \ --branch dev --path . 2>&1 || true
--token "${{ secrets.MOKOGITEA_TOKEN }}"
# -- Summary --------------------------------------------------------------
- name: "Read published version" - name: Pipeline Summary
id: version if: always()
run: | run: |
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "") VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//') PLATFORM="${{ steps.platform.outputs.platform }}"
[ -z "$VERSION" ] && VERSION="00.00.00" && echo "skip=true" >> "$GITHUB_OUTPUT" if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
echo "version=${VERSION}" >> "$GITHUB_OUTPUT" echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
PLATFORM="${{ steps.platform.outputs.platform }}" echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
if [[ "$PLATFORM" == joomla* ]]; then elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
echo "tag=stable" >> "$GITHUB_OUTPUT" echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
echo "release_tag=stable" >> "$GITHUB_OUTPUT" else
else echo "" >> $GITHUB_STEP_SUMMARY
echo "tag=v${VERSION}" >> "$GITHUB_OUTPUT" echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
echo "release_tag=v${VERSION}" >> "$GITHUB_OUTPUT" echo "" >> $GITHUB_STEP_SUMMARY
fi echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
echo "branch=main" >> "$GITHUB_OUTPUT" echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
echo "Published version: ${VERSION}" echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
- name: "Create semver tag for non-Joomla repos" echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
id: semver echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
if: | echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
steps.version.outputs.skip != 'true' && fi
!startsWith(steps.platform.outputs.platform, 'joomla')
run: |
VERSION="${{ steps.version.outputs.version }}"
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
SEMVER_TAG="v${VERSION}"
echo "Creating semver tag: ${SEMVER_TAG}"
# Create the git tag via API
HTTP_CODE=$(curl -sf -o /dev/null -w "%{http_code}" \
-X POST -H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
"${API_BASE}/tags" \
-d "{\"tag_name\":\"${SEMVER_TAG}\",\"target\":\"main\",\"message\":\"Release ${VERSION}\"}" 2>/dev/null || echo "000")
if [ "$HTTP_CODE" = "201" ] || [ "$HTTP_CODE" = "200" ]; then
echo "Created semver tag: ${SEMVER_TAG}"
elif [ "$HTTP_CODE" = "409" ]; then
echo "Semver tag ${SEMVER_TAG} already exists (skipped)"
else
echo "::warning::Failed to create semver tag ${SEMVER_TAG} (HTTP ${HTTP_CODE})"
fi
echo "semver_tag=${SEMVER_TAG}" >> "$GITHUB_OUTPUT"
- name: Update release notes and promote changelog
run: |
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
# Get the stable release info (version and ID)
RELEASE_JSON=$(curl -sf -H "Authorization: token ${TOKEN}" \
"${API_BASE}/releases/tags/stable" 2>/dev/null || echo '{}')
RELEASE_ID=$(python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" <<< "$RELEASE_JSON" 2>/dev/null || true)
# Extract version from release name (e.g. "06.17.00" or "v06.17.00")
VERSION=$(python3 -c "
import json, sys, re
r = json.load(sys.stdin)
name = r.get('name', '')
m = re.search(r'(\d+\.\d+\.\d+)', name)
print(m.group(1) if m else '')
" <<< "$RELEASE_JSON" 2>/dev/null || true)
# Extract [Unreleased] section from changelog
NOTES=""
if [ -f "CHANGELOG.md" ]; then
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
fi
[ -z "$NOTES" ] && NOTES="Stable release"
# Update release body via API
if [ -n "$RELEASE_ID" ]; then
python3 -c "
import json, urllib.request
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="${MOKOGITEA_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="${MOKOGITEA_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="${MOKOGITEA_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="${MOKOGITEA_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](${MOKOGITEA_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
-197
View File
@@ -1,197 +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:
pull_request:
branches:
- main
- dev
- dev/**
- rc/**
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
@@ -1,68 +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/ci-issue-reporter.yml
# VERSION: 01.00.00
# BRIEF: Reusable workflow — creates/updates a Gitea issue when a CI gate fails.
# Clones MokoCLI and runs cli/ci_issue_reporter.sh.
name: "Universal: CI Issue Reporter"
on:
workflow_call:
inputs:
gate:
description: "CI gate name (e.g. PR Validation, Repository Health)"
required: true
type: string
details:
description: "Human-readable failure description"
required: true
type: string
severity:
description: "error or warning"
required: false
type: string
default: "error"
workflow:
description: "Workflow name for the issue title"
required: false
type: string
default: ""
secrets:
MOKOGITEA_TOKEN:
required: true
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
report:
name: "Report: ${{ inputs.gate }}"
runs-on: ubuntu-latest
steps:
- name: Clone MokoCLI
env:
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
run: |
MOKOGITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}"
git clone --depth 1 --filter=blob:none --sparse "${MOKOGITEA_URL}/MokoConsulting/MokoCLI.git" /tmp/mokocli
cd /tmp/mokocli && git sparse-checkout set cli/ci_issue_reporter.sh
- name: Report CI failure
env:
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
run: |
chmod +x /tmp/mokocli/cli/ci_issue_reporter.sh
/tmp/mokocli/cli/ci_issue_reporter.sh \
--gate "${{ inputs.gate }}" \
--details "${{ inputs.details }}" \
--severity "${{ inputs.severity }}" \
--workflow "${{ inputs.workflow }}"
+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
+11 -11
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
@@ -21,7 +21,7 @@ permissions:
contents: write contents: write
env: env:
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
jobs: jobs:
cleanup: cleanup:
@@ -37,13 +37,13 @@ jobs:
- name: Delete merged branches - name: Delete merged branches
env: env:
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
run: | run: |
echo "=== Merged Branch Cleanup ===" echo "=== Merged Branch Cleanup ==="
API="${MOKOGITEA_URL}/api/v1/repos/${{ github.repository }}" API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
# List branches via API # List branches via API
BRANCHES=$(curl -sS -H "Authorization: token ${MOKOGITEA_TOKEN}" \ BRANCHES=$(curl -sS -H "Authorization: token ${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 ${MOKOGITEA_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:
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
run: | run: |
echo "=== Workflow Run Cleanup ===" echo "=== Workflow Run Cleanup ==="
API="${MOKOGITEA_URL}/api/v1/repos/${{ github.repository }}" API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ) CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ)
# Get old completed runs # Get old completed runs
RUNS=$(curl -sS -H "Authorization: token ${MOKOGITEA_TOKEN}" \ RUNS=$(curl -sS -H "Authorization: token ${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 ${MOKOGITEA_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
-126
View File
@@ -1,126 +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.Deploy
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
# PATH: /templates/workflows/joomla/deploy-manual.yml.template
# VERSION: 04.07.00
# BRIEF: Manual SFTP deploy to dev server for Joomla repos
name: "Universal: Deploy to Dev (Manual)"
on:
workflow_dispatch:
inputs:
clear_remote:
description: 'Delete all remote files before uploading'
required: false
default: 'false'
type: boolean
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
permissions:
contents: read
jobs:
deploy:
name: SFTP Deploy to Dev
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup PHP
run: |
php -v && composer --version
- name: Setup MokoStandards tools
env:
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || github.token }}
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || github.token }}
MOKO_CLONE_HOST: ${{ secrets.MOKOGITEA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}'
run: |
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
/tmp/mokostandards-api 2>/dev/null || true
if [ -d "/tmp/mokostandards-api" ] && [ -f "/tmp/mokostandards-api/composer.json" ]; then
cd /tmp/mokostandards-api && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
fi
- name: Check FTP configuration
id: check
env:
HOST: ${{ vars.DEV_FTP_HOST }}
PATH_VAR: ${{ vars.DEV_FTP_PATH }}
PORT: ${{ vars.DEV_FTP_PORT }}
run: |
if [ -z "$HOST" ] || [ -z "$PATH_VAR" ]; then
echo "DEV_FTP_HOST or DEV_FTP_PATH not configured -- cannot deploy"
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "skip=false" >> "$GITHUB_OUTPUT"
echo "host=$HOST" >> "$GITHUB_OUTPUT"
REMOTE="${PATH_VAR%/}"
echo "remote=$REMOTE" >> "$GITHUB_OUTPUT"
[ -z "$PORT" ] && PORT="22"
echo "port=$PORT" >> "$GITHUB_OUTPUT"
- name: Deploy via SFTP
if: steps.check.outputs.skip != 'true'
env:
SFTP_KEY: ${{ secrets.DEV_FTP_KEY }}
SFTP_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
SFTP_USER: ${{ vars.DEV_FTP_USERNAME }}
run: |
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
[ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ -- nothing to deploy"; exit 0; }
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
"${{ steps.check.outputs.host }}" "${{ steps.check.outputs.port }}" "$SFTP_USER" "${{ steps.check.outputs.remote }}" \
> /tmp/sftp-config.json
if [ -n "$SFTP_KEY" ]; then
echo "$SFTP_KEY" > /tmp/deploy_key
chmod 600 /tmp/deploy_key
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
else
printf ',"password":"%s"}' "$SFTP_PASS" >> /tmp/sftp-config.json
fi
DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json)
[ "${{ inputs.clear_remote }}" = "true" ] && DEPLOY_ARGS+=(--clear-remote)
PLATFORM=$(php /tmp/mokostandards-api/cli/platform_detect.php --path . 2>/dev/null || true)
if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards-api/deploy/deploy-joomla.php" ]; then
php /tmp/mokostandards-api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}"
else
php /tmp/mokostandards-api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}"
fi
rm -f /tmp/deploy_key /tmp/sftp-config.json
- name: Summary
if: always()
run: |
if [ "${{ steps.check.outputs.skip }}" = "true" ]; then
echo "### Deploy Skipped -- FTP not configured" >> $GITHUB_STEP_SUMMARY
else
echo "### Manual Dev Deploy Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Host | \`${{ steps.check.outputs.host }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Remote | \`${{ steps.check.outputs.remote }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Clear | ${{ inputs.clear_remote }} |" >> $GITHUB_STEP_SUMMARY
fi
+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:
+15 -5
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.06 <<<<<<< 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"
@@ -19,7 +29,7 @@ permissions:
issues: write issues: write
env: env:
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
jobs: jobs:
create-branch: create-branch:
@@ -29,7 +39,7 @@ jobs:
- name: Create branch and comment - name: Create branch and comment
run: | run: |
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
API="${MOKOGITEA_URL}/api/v1/repos/${{ github.repository }}" API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
ISSUE_NUM="${{ github.event.issue.number }}" ISSUE_NUM="${{ github.event.issue.number }}"
ISSUE_TITLE="${{ github.event.issue.title }}" ISSUE_TITLE="${{ github.event.issue.title }}"
@@ -58,7 +68,7 @@ jobs:
echo "Created branch: ${BRANCH}" echo "Created branch: ${BRANCH}"
# Comment on issue with branch link # Comment on issue with branch link
REPO_URL="${MOKOGITEA_URL}/${{ github.repository }}" REPO_URL="${GITEA_URL}/${{ github.repository }}"
BODY="Branch created: [\`${BRANCH}\`](${REPO_URL}/src/branch/${BRANCH})\n\n\`\`\`bash\ngit fetch origin\ngit checkout ${BRANCH}\n\`\`\`" BODY="Branch created: [\`${BRANCH}\`](${REPO_URL}/src/branch/${BRANCH})\n\n\`\`\`bash\ngit fetch origin\ngit checkout ${BRANCH}\n\`\`\`"
curl -sf -X POST \ curl -sf -X POST \
+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
+4 -289
View File
@@ -7,7 +7,7 @@
# 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"
@@ -96,32 +96,6 @@ jobs:
echo "Branch policy: OK (${HEAD} → ${BASE})" echo "Branch policy: OK (${HEAD} → ${BASE})"
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
# ── Secret Scanning ──────────────────────────────────────────────────
gitleaks:
name: Secret Scan
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Gitleaks
run: |
GITLEAKS_VERSION="8.21.2"
curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \
| tar -xz -C /usr/local/bin gitleaks
- name: Scan PR commits for secrets
run: |
if gitleaks detect --source . --verbose \
--log-opts=${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }} 2>&1; then
echo "**No secrets detected.**" >> $GITHUB_STEP_SUMMARY
else
echo "::error::Potential secrets detected in PR commits"
exit 1
fi
# ── Code Validation ──────────────────────────────────────────────────── # ── Code Validation ────────────────────────────────────────────────────
validate: validate:
name: Validate PR name: Validate PR
@@ -131,19 +105,6 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Check for merge conflict markers
run: |
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
echo "::error::Merge conflict markers found in source files"
echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "No conflict markers found"
- name: Detect platform - name: Detect platform
id: platform id: platform
run: | run: |
@@ -173,98 +134,6 @@ jobs:
echo "PHP lint: ${ERRORS} error(s)" echo "PHP lint: ${ERRORS} error(s)"
[ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; } [ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
- name: Joomla JEXEC guard check
if: steps.platform.outputs.platform == 'joomla'
run: |
ERRORS=0
while IFS= read -r -d '' file; do
# Skip vendor, node_modules, and index.html stub files
case "$file" in ./vendor/*|./node_modules/*) continue ;; esac
# Check first 10 lines for JEXEC or JPATH guard
if ! head -20 "$file" | grep -qE "defined\s*\(\s*['\"](_JEXEC|JPATH_BASE|\\\\JPATH_PLATFORM)['\"]"; then
echo "::error file=${file}::Missing JEXEC guard: ${file}"
ERRORS=$((ERRORS + 1))
fi
done < <(find . -name "*.php" -path "*/src/*" -not -path "./.git/*" -not -path "./vendor/*" -print0)
if [ "$ERRORS" -gt 0 ]; then
echo "::error::${ERRORS} PHP file(s) missing defined('_JEXEC') or die guard"
echo "## JEXEC Guard Check: Failed" >> $GITHUB_STEP_SUMMARY
echo "${ERRORS} file(s) in src/ are missing the Joomla execution guard." >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "JEXEC guard: OK"
- name: Joomla directory listing protection
if: steps.platform.outputs.platform == 'joomla'
run: |
MISSING=0
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && exit 0
while IFS= read -r dir; do
if [ ! -f "${dir}/index.html" ]; then
echo "::warning::Missing index.html in ${dir} (directory listing protection)"
MISSING=$((MISSING + 1))
fi
done < <(find "$SOURCE_DIR" -type d -not -path "./.git/*" -not -path "*/vendor/*" -not -path "*/node_modules/*")
if [ "$MISSING" -gt 0 ]; then
echo "## Directory Protection" >> $GITHUB_STEP_SUMMARY
echo "${MISSING} director(ies) missing index.html" >> $GITHUB_STEP_SUMMARY
fi
echo "Directory protection: ${MISSING} missing (advisory)"
- name: Joomla script file and asset checks
if: steps.platform.outputs.platform == 'joomla'
run: |
ERRORS=0
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
[ -z "$MANIFEST" ] && exit 0
MANIFEST_DIR=$(dirname "$MANIFEST")
# Check scriptfile exists if declared
SCRIPTFILE=$(sed -n 's/.*<scriptfile>\([^<]*\)<\/scriptfile>.*/\1/p' "$MANIFEST" 2>/dev/null)
if [ -n "$SCRIPTFILE" ]; then
if [ ! -f "${MANIFEST_DIR}/${SCRIPTFILE}" ]; then
echo "::error::Manifest declares <scriptfile>${SCRIPTFILE}</scriptfile> but file not found at ${MANIFEST_DIR}/${SCRIPTFILE}"
ERRORS=$((ERRORS + 1))
else
echo "Script file: ${MANIFEST_DIR}/${SCRIPTFILE} (OK)"
fi
fi
# Require joomla.asset.json and validate it
ASSET_JSON=$(find "$MANIFEST_DIR" -name "joomla.asset.json" -not -path "./.git/*" 2>/dev/null | head -1)
if [ -z "$ASSET_JSON" ]; then
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 - name: Validate platform manifest
run: | run: |
PLATFORM="${{ steps.platform.outputs.platform }}" PLATFORM="${{ steps.platform.outputs.platform }}"
@@ -282,13 +151,6 @@ jobs:
for ELEMENT in name version description; do for ELEMENT in name version description; do
grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; } grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; }
done 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" echo "Joomla manifest valid"
;; ;;
dolibarr) dolibarr)
@@ -321,138 +183,6 @@ jobs:
;; ;;
esac 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 - name: Check changelog has unreleased entry
run: | run: |
if [ ! -f "CHANGELOG.md" ]; then if [ ! -f "CHANGELOG.md" ]; then
@@ -496,26 +226,11 @@ jobs:
steps: steps:
- name: Trigger RC pre-release - name: Trigger RC pre-release
env: env:
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
REPO: ${{ github.repository }} REPO: ${{ github.repository }}
BRANCH: ${{ github.head_ref }} BRANCH: ${{ github.head_ref }}
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
run: | run: |
curl -s -X POST "${MOKOGITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${MOKOGITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}" curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
# ── Issue Reporter ──────────────────────────────────────────────────────
report-issues:
name: Report Issues
needs: [branch-policy, validate]
if: >-
always() &&
needs.validate.result == 'failure'
uses: ./.mokogitea/workflows/ci-issue-reporter.yml
with:
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."
secrets: inherit
@@ -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
+76 -116
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.02.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,67 +51,32 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
token: ${{ secrets.MOKOGITEA_TOKEN }} token: ${{ secrets.MOKOGITEA_TOKEN }}
ref: ${{ github.ref_name }}
submodules: recursive
- name: Update submodules to main - name: Setup moko-platform tools
run: |
git submodule foreach --quiet 'git checkout main && git pull --quiet origin main' 2>/dev/null || true
- name: Setup mokocli 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: Check platform eligibility (Joomla only)
id: eligibility
run: |
PLATFORM="${{ steps.platform.outputs.platform }}"
if [[ "$PLATFORM" == joomla* ]] || [[ "$PLATFORM" == "joomla" ]]; then
echo "proceed=true" >> "$GITHUB_OUTPUT"
else
echo "proceed=false" >> "$GITHUB_OUTPUT"
echo "::notice::Platform '$PLATFORM' — non-Joomla, skipping pre-release auto-bump"
fi
- name: Resolve metadata and bump version - name: Resolve metadata and bump version
id: meta id: meta
if: steps.eligibility.outputs.proceed == 'true'
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" ;;
@@ -127,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
@@ -183,7 +135,6 @@ jobs:
- name: Create release - name: Create release
id: release id: release
if: steps.eligibility.outputs.proceed == 'true'
run: | run: |
TAG="${{ steps.meta.outputs.tag }}" TAG="${{ steps.meta.outputs.tag }}"
VERSION="${{ steps.meta.outputs.version }}" VERSION="${{ steps.meta.outputs.version }}"
@@ -191,47 +142,10 @@ 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
if: steps.eligibility.outputs.proceed == 'true'
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
if: steps.eligibility.outputs.proceed == 'true'
run: | run: |
VERSION="${{ steps.meta.outputs.version }}" VERSION="${{ steps.meta.outputs.version }}"
TAG="${{ steps.meta.outputs.tag }}" TAG="${{ steps.meta.outputs.tag }}"
@@ -241,11 +155,57 @@ 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)"
if: steps.eligibility.outputs.proceed == 'true'
continue-on-error: true continue-on-error: true
run: | run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
-71
View File
@@ -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.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
env:
BRANCH: ${{ github.event.pull_request.head.ref }}
REPO: ${{ github.repository }}
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
run: |
set -euo pipefail
# BRANCH is attacker-controlled (PR head ref). Strict allowlist before ANY use.
if ! printf '%s' "$BRANCH" | grep -Eq '^rc/[A-Za-z0-9._/-]+$'; then
echo "::error::Refusing unsafe branch name: $BRANCH"; exit 1
fi
SUFFIX="${BRANCH#rc/}"
DEV_BRANCH="dev/${SUFFIX}"
API="${GITEA_URL}/api/v1/repos/${REPO}/branches"
# 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
# Read BRANCH from the environment inside PHP (getenv, no string interpolation -> no PHP injection)
ENCODED=$(php -r 'echo rawurlencode(getenv("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"
+112 -43
View File
@@ -7,11 +7,11 @@
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Validation # INGROUP: moko-platform.Validation
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli # REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# PATH: /templates/workflows/joomla/repo_health.yml.template # PATH: /templates/workflows/joomla/repo_health.yml.template
# VERSION: 09.23.00 # VERSION: 04.06.00
# BRIEF: Enforces repository guardrails by validating scripts governance, tooling availability, and core repository health artifacts. # BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts.
# ============================================================================ # ============================================================================
name: "Generic: Repo Health" name: "Generic: Repo Health"
@@ -24,22 +24,26 @@ on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
profile: profile:
description: 'Validation profile: all, scripts, or repo' description: 'Validation profile: all, release, scripts, or repo'
required: true required: true
default: all default: all
type: choice type: choice
options: options:
- all - all
- release
- scripts - scripts
- repo - repo
pull_request: pull_request:
branches: push:
- main
permissions: permissions:
contents: read contents: read
env: env:
# Release policy - Repository Variables Only
RELEASE_REQUIRED_REPO_VARS: RS_FTP_PATH_SUFFIX
RELEASE_OPTIONAL_REPO_VARS: DEV_FTP_SUFFIX
# Scripts governance policy # Scripts governance policy
SCRIPTS_REQUIRED_DIRS: SCRIPTS_REQUIRED_DIRS:
SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate
@@ -77,7 +81,7 @@ jobs:
- name: Check actor permission (admin only) - name: Check actor permission (admin only)
id: perm id: perm
env: env:
TOKEN: ${{ secrets.MOKOGITEA_TOKEN || github.token }} TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}
REPO: ${{ github.repository }} REPO: ${{ github.repository }}
ACTOR: ${{ github.actor }} ACTOR: ${{ github.actor }}
run: | run: |
@@ -134,6 +138,101 @@ jobs:
printf '%s\n' 'ERROR: Access denied. Admin permission required.' >> "${GITHUB_STEP_SUMMARY}" printf '%s\n' 'ERROR: Access denied. Admin permission required.' >> "${GITHUB_STEP_SUMMARY}"
exit 1 exit 1
release_config:
name: Release configuration
needs: access_check
if: ${{ needs.access_check.outputs.allowed == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
- name: Guardrails release vars
env:
PROFILE_RAW: ${{ github.event.inputs.profile }}
RS_FTP_PATH_SUFFIX: ${{ vars.RS_FTP_PATH_SUFFIX }}
DEV_FTP_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
run: |
set -euo pipefail
profile="${PROFILE_RAW:-all}"
case "${profile}" in
all|release|scripts|repo) ;;
*)
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
exit 1
;;
esac
if [ "${profile}" = 'scripts' ] || [ "${profile}" = 'repo' ]; then
{
printf '%s\n' '### Release configuration (Repository Variables)'
printf '%s\n' "Profile: ${profile}"
printf '%s\n' 'Status: SKIPPED'
printf '%s\n' 'Reason: profile excludes release validation'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
exit 0
fi
IFS=',' read -r -a required <<< "${RELEASE_REQUIRED_REPO_VARS}"
IFS=',' read -r -a optional <<< "${RELEASE_OPTIONAL_REPO_VARS}"
missing=()
missing_optional=()
for k in "${required[@]}"; do
v="${!k:-}"
[ -z "${v}" ] && missing+=("${k}")
done
for k in "${optional[@]}"; do
v="${!k:-}"
[ -z "${v}" ] && missing_optional+=("${k}")
done
{
printf '%s\n' '### Release configuration (Repository Variables)'
printf '%s\n' "Profile: ${profile}"
printf '%s\n' '| Variable | Status |'
printf '%s\n' '|---|---|'
printf '%s\n' "| RS_FTP_PATH_SUFFIX | ${RS_FTP_PATH_SUFFIX:-NOT SET} |"
printf '%s\n' "| DEV_FTP_SUFFIX | ${DEV_FTP_SUFFIX:-NOT SET} |"
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
if [ "${#missing_optional[@]}" -gt 0 ]; then
{
printf '%s\n' '### Missing optional repository variables'
for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
if [ "${#missing[@]}" -gt 0 ]; then
{
printf '%s\n' '### Missing required repository variables'
for m in "${missing[@]}"; do printf '%s\n' "- ${m}"; done
printf '%s\n' 'ERROR: Guardrails failed. Missing required repository variables.'
} >> "${GITHUB_STEP_SUMMARY}"
exit 1
fi
{
printf '%s\n' '### Repository variables validation result'
printf '%s\n' 'Status: OK'
printf '%s\n' 'All required repository variables present.'
printf '%s\n' ''
printf '%s\n' '**Note**: Organization secrets (RS_FTP_HOST, RS_FTP_USER, etc.) are validated at deployment time, not in repository health checks.'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
scripts_governance: scripts_governance:
name: Scripts governance name: Scripts governance
needs: access_check needs: access_check
@@ -157,14 +256,14 @@ jobs:
profile="${PROFILE_RAW:-all}" profile="${PROFILE_RAW:-all}"
case "${profile}" in case "${profile}" in
all|scripts|repo) ;; all|release|scripts|repo) ;;
*) *)
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
exit 1 exit 1
;; ;;
esac esac
if [ "${profile}" = 'repo' ]; then if [ "${profile}" = 'release' ] || [ "${profile}" = 'repo' ]; then
{ {
printf '%s\n' '### Scripts governance' printf '%s\n' '### Scripts governance'
printf '%s\n' "Profile: ${profile}" printf '%s\n' "Profile: ${profile}"
@@ -271,14 +370,14 @@ jobs:
profile="${PROFILE_RAW:-all}" profile="${PROFILE_RAW:-all}"
case "${profile}" in case "${profile}" in
all|scripts|repo) ;; all|release|scripts|repo) ;;
*) *)
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
exit 1 exit 1
;; ;;
esac esac
if [ "${profile}" = 'scripts' ]; then if [ "${profile}" = 'release' ] || [ "${profile}" = 'scripts' ]; then
{ {
printf '%s\n' '### Repository health' printf '%s\n' '### Repository health'
printf '%s\n' "Profile: ${profile}" printf '%s\n' "Profile: ${profile}"
@@ -605,7 +704,7 @@ jobs:
printf '%s\n' '| Domain | Status | Notes |' printf '%s\n' '| Domain | Status | Notes |'
printf '%s\n' '|---|---|---|' printf '%s\n' '|---|---|---|'
printf '%s\n' '| Access control | OK | Admin-only execution gate |' printf '%s\n' '| Access control | OK | Admin-only execution gate |'
printf '%s\n' '| Release policy | N/A | Releases handled by MokoGitea |' printf '%s\n' '| Release variables | OK | Repository variables validation |'
printf '%s\n' '| Scripts governance | OK | Directory policy and advisory reporting |' printf '%s\n' '| Scripts governance | OK | Directory policy and advisory reporting |'
printf '%s\n' '| Repo required artifacts | OK | Required, optional, disallowed enforcement |' printf '%s\n' '| Repo required artifacts | OK | Required, optional, disallowed enforcement |'
printf '%s\n' '| Repo content heuristics | OK | Brand, license, changelog structure |' printf '%s\n' '| Repo content heuristics | OK | Brand, license, changelog structure |'
@@ -668,33 +767,3 @@ jobs:
echo "### Site Health" >> $GITHUB_STEP_SUMMARY echo "### Site Health" >> $GITHUB_STEP_SUMMARY
echo "Uptime and SSL checks completed." >> $GITHUB_STEP_SUMMARY echo "Uptime and SSL checks completed." >> $GITHUB_STEP_SUMMARY
# ═══════════════════════════════════════════════════════════════════════
# Issue Reporter — file issues for failed gates
# ═══════════════════════════════════════════════════════════════════════
report-scripts:
name: "Report: Scripts Governance"
needs: [access_check, scripts_governance]
if: >-
always() &&
needs.scripts_governance.result == 'failure'
uses: ./.mokogitea/workflows/ci-issue-reporter.yml
with:
gate: "Scripts Governance"
workflow: "Repo Health"
severity: error
details: "Scripts directory policy violations detected. Review required and allowed directories."
secrets: inherit
report-health:
name: "Report: Repository Health"
needs: [access_check, repo_health]
if: >-
always() &&
needs.repo_health.result == 'failure'
uses: ./.mokogitea/workflows/ci-issue-reporter.yml
with:
gate: "Repository Health"
workflow: "Repo Health"
severity: error
details: "Repository health checks failed — missing required artifacts, disallowed files, or content warnings. Check the CI run summary."
secrets: inherit
+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
-130
View File
@@ -1,130 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow.Template
# INGROUP: MokoStandards.CI
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla
# PATH: /.mokogitea/workflows/version-set.yml
# VERSION: 01.00.00
# BRIEF: Set or reset the extension version across all version-bearing files
name: "Joomla: Set Version"
on:
workflow_dispatch:
inputs:
version:
description: "Version number (e.g. 01.00.00)"
required: true
type: string
branch:
description: "Branch to update (default: current)"
required: false
type: string
permissions:
contents: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
set-version:
name: Set Version to ${{ inputs.version }}
runs-on: ubuntu-latest
steps:
- name: Validate version format
run: |
VERSION="${{ inputs.version }}"
if ! echo "$VERSION" | grep -qP '^\d{2}\.\d{2}\.\d{2}$'; then
echo "::error::Invalid version format '${VERSION}' — expected XX.YY.ZZ (e.g. 01.00.00)"
exit 1
fi
echo "VERSION=${VERSION}" >> "$GITHUB_ENV"
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.MOKOGITEA_TOKEN || github.token }}
ref: ${{ inputs.branch || github.ref }}
fetch-depth: 1
- name: Update manifest version
run: |
MANIFEST=""
for XML_FILE in $(find . -maxdepth 3 -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 "::warning::No Joomla extension manifest found — skipping manifest update"
else
OLD_VER=$(grep -oP '<version>\K[^<]+' "$MANIFEST" | head -1)
sed -i "s|<version>${OLD_VER}</version>|<version>${VERSION}</version>|" "$MANIFEST"
echo "Manifest: ${OLD_VER} → ${VERSION} (${MANIFEST})"
fi
- name: Update README.md version
run: |
if [ -f "README.md" ]; then
if grep -qP '^\s*VERSION:\s*\d' README.md; then
sed -i -E "s/(VERSION:\s*)[0-9]{2}\.[0-9]{2}\.[0-9]{2}/\1${VERSION}/" README.md
echo "README.md version updated to ${VERSION}"
else
echo "::warning::No VERSION line found in README.md — skipping"
fi
fi
- name: Update CHANGELOG.md
run: |
if [ -f "CHANGELOG.md" ]; then
DATE=$(date +%Y-%m-%d)
# Check if this version already has an entry
if grep -q "^\#\# \[${VERSION}\]" CHANGELOG.md; then
echo "CHANGELOG.md already has entry for ${VERSION} — skipping"
else
# Insert new version entry after [Unreleased] or at the top after header
if grep -q '^\#\# \[Unreleased\]' CHANGELOG.md; then
sed -i "/^\#\# \[Unreleased\]/a\\\\n## [${VERSION}] --- ${DATE}" CHANGELOG.md
else
sed -i "/^\# Changelog/a\\\\n## [Unreleased]\n\n## [${VERSION}] --- ${DATE}" CHANGELOG.md
fi
echo "CHANGELOG.md: added entry for ${VERSION}"
fi
else
echo "::warning::No CHANGELOG.md found — skipping"
fi
- name: Update FILE INFORMATION blocks
run: |
# Update VERSION in file header blocks (# VERSION: XX.YY.ZZ)
find . -maxdepth 1 -type f \( -name "*.yml" -o -name "*.yaml" -o -name "*.php" -o -name "*.md" \) \
-not -path "./.git/*" -not -path "./vendor/*" -print0 2>/dev/null | \
while IFS= read -r -d '' FILE; do
if head -20 "$FILE" | grep -qP '^\s*#?\s*VERSION:\s*\d{2}\.\d{2}\.\d{2}'; then
sed -i -E "s/(#?\s*VERSION:\s*)[0-9]{2}\.[0-9]{2}\.[0-9]{2}/\1${VERSION}/" "$FILE"
echo "Updated FILE INFORMATION VERSION in ${FILE}"
fi
done
- name: Commit and push
run: |
git config user.name "Moko Consulting [bot]"
git config user.email "hello@mokoconsulting.tech"
git add -A
if git diff --cached --quiet; then
echo "No version changes detected — nothing to commit"
else
git commit -m "chore: set version to ${VERSION} [skip bump]
Authored-by: Moko Consulting"
git push
echo "### Version Set" >> $GITHUB_STEP_SUMMARY
echo "Version updated to \`${VERSION}\` on branch \`${GITHUB_REF_NAME}\`" >> $GITHUB_STEP_SUMMARY
fi
@@ -1,81 +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:
workflow_dispatch:
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_name == 'workflow_dispatch' ||
(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: |
MOKOGITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}"
git clone --depth 1 "${MOKOGITEA_URL}/MokoConsulting/mokocli.git" /tmp/mokocli
- name: Install PHP
run: |
if ! command -v php &> /dev/null; then
apt-get update -qq && apt-get install -y -qq php-cli php-json php-curl > /dev/null 2>&1
fi
- name: Install dependencies
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}
+51 -15
View File
@@ -8,29 +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.06 <<<<<<< 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.06) # Changelog — MokoOnyx (VERSION: 02.17.00)
## [Unreleased] ## [Unreleased]
## [02.27.00] --- 2026-06-21 =======
VERSION: 02.17.00
BRIEF: Changelog file documenting version history of MokoOnyx
-->
## [02.27.00] --- 2026-06-21 # 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
+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>*
+213
View File
@@ -0,0 +1,213 @@
# Makefile for MokoOnyx Joomla Template
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# SPDX-License-Identifier: GPL-3.0-or-later
#
# Build and validation powered by MokoStandards Enterprise API
# Install: composer install
# ==============================================================================
# CONFIGURATION
# ==============================================================================
EXTENSION_NAME := mokoonyx
EXTENSION_TYPE := template
EXTENSION_VERSION := $(shell grep -oP 'VERSION:\s*\K[0-9.]+' README.md 2>/dev/null || echo "0.0.0")
SRC_DIR := src
BUILD_DIR := build
DIST_DIR := dist
PHP := php
COMPOSER := composer
MOKO := vendor/bin/moko
# Colors
COLOR_RESET := \033[0m
COLOR_GREEN := \033[32m
COLOR_YELLOW := \033[33m
COLOR_BLUE := \033[34m
COLOR_RED := \033[31m
# ==============================================================================
# TARGETS
# ==============================================================================
.PHONY: help
help: ## Show this help message
@echo "$(COLOR_BLUE)╔════════════════════════════════════════════════════════════╗$(COLOR_RESET)"
@echo "$(COLOR_BLUE)║ MokoOnyx Template Build ║$(COLOR_RESET)"
@echo "$(COLOR_BLUE)╚════════════════════════════════════════════════════════════╝$(COLOR_RESET)"
@echo ""
@echo "Extension: $(EXTENSION_NAME) ($(EXTENSION_TYPE)) v$(EXTENSION_VERSION)"
@echo "Powered by: MokoStandards Enterprise API"
@echo ""
@echo "$(COLOR_GREEN)Available targets:$(COLOR_RESET)"
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " $(COLOR_BLUE)%-20s$(COLOR_RESET) %s\n", $$1, $$2}'
@echo ""
# ── Dependencies ──────────────────────────────────────────────────────────────
.PHONY: install-deps
install-deps: ## Install Composer dependencies (includes MokoStandards API)
@echo "$(COLOR_BLUE)Installing dependencies...$(COLOR_RESET)"
@$(COMPOSER) install
@echo "$(COLOR_GREEN)✓ Dependencies installed$(COLOR_RESET)"
.PHONY: update-deps
update-deps: ## Update Composer dependencies
@echo "$(COLOR_BLUE)Updating dependencies...$(COLOR_RESET)"
@$(COMPOSER) update
@echo "$(COLOR_GREEN)✓ Dependencies updated$(COLOR_RESET)"
# ── Validation (MokoStandards API) ────────────────────────────────────────────
.PHONY: check-moko
check-moko:
@if [ ! -f "$(MOKO)" ]; then \
echo "$(COLOR_RED)✗ MokoStandards CLI not found. Run: make install-deps$(COLOR_RESET)"; \
exit 1; \
fi
.PHONY: lint
lint: check-moko ## PHP syntax check via MokoStandards
@echo "$(COLOR_BLUE)Running PHP syntax check...$(COLOR_RESET)"
@$(PHP) $(MOKO) check:syntax -- --path .
@echo "$(COLOR_GREEN)✓ PHP syntax OK$(COLOR_RESET)"
.PHONY: check-joomla
check-joomla: check-moko ## Validate Joomla manifest via MokoStandards
@echo "$(COLOR_BLUE)Validating Joomla manifest...$(COLOR_RESET)"
@$(PHP) $(MOKO) check:joomla -- --path .
@echo "$(COLOR_GREEN)✓ Joomla manifest valid$(COLOR_RESET)"
.PHONY: check-version
check-version: check-moko ## Verify version consistency across files
@echo "$(COLOR_BLUE)Checking version consistency...$(COLOR_RESET)"
@$(PHP) $(MOKO) check:version -- --path .
@echo "$(COLOR_GREEN)✓ Versions consistent$(COLOR_RESET)"
.PHONY: check-headers
check-headers: check-moko ## Check license headers on source files
@echo "$(COLOR_BLUE)Checking license headers...$(COLOR_RESET)"
@$(PHP) $(MOKO) check:headers -- --path .
@echo "$(COLOR_GREEN)✓ Headers OK$(COLOR_RESET)"
.PHONY: check-secrets
check-secrets: check-moko ## Scan for leaked credentials
@echo "$(COLOR_BLUE)Scanning for secrets...$(COLOR_RESET)"
@$(PHP) $(MOKO) check:secrets -- --path .
@echo "$(COLOR_GREEN)✓ No secrets found$(COLOR_RESET)"
.PHONY: check-xml
check-xml: check-moko ## Validate XML files are well-formed
@echo "$(COLOR_BLUE)Checking XML files...$(COLOR_RESET)"
@$(PHP) $(MOKO) check:xml -- --path .
@echo "$(COLOR_GREEN)✓ XML well-formed$(COLOR_RESET)"
.PHONY: validate
validate: lint check-joomla check-version check-xml check-headers check-secrets ## Run all MokoStandards validation checks
@echo "$(COLOR_GREEN)✓ All validation checks passed$(COLOR_RESET)"
.PHONY: health
health: check-moko ## Full repository health check via MokoStandards
@echo "$(COLOR_BLUE)Running full health check...$(COLOR_RESET)"
@$(PHP) $(MOKO) health -- --path .
# ── Build ─────────────────────────────────────────────────────────────────────
.PHONY: clean
clean: ## Clean build artifacts
@echo "$(COLOR_BLUE)Cleaning build artifacts...$(COLOR_RESET)"
@rm -rf $(BUILD_DIR) $(DIST_DIR)
@echo "$(COLOR_GREEN)✓ Build artifacts cleaned$(COLOR_RESET)"
MOKO_PLATFORM ?= $(or $(wildcard ../moko-platform),$(wildcard $(HOME)/moko-platform),$(wildcard /opt/moko-platform))
MINIFY_SCRIPT := $(MOKO_PLATFORM)/build/minify.js
.PHONY: minify
minify: ## Minify CSS/JS assets (requires terser + clean-css)
@echo "$(COLOR_BLUE)Minifying assets...$(COLOR_RESET)"
@if [ -f "$(MINIFY_SCRIPT)" ]; then \
node "$(MINIFY_SCRIPT)" $(SRC_DIR); \
elif [ -f "scripts/minify.js" ]; then \
node scripts/minify.js; \
else \
echo "$(COLOR_YELLOW)⚠ No minify script found$(COLOR_RESET)"; \
fi
.PHONY: build
build: clean minify ## Build template installable ZIP from src/
@echo "$(COLOR_BLUE)Building $(EXTENSION_NAME) v$(EXTENSION_VERSION)...$(COLOR_RESET)"
@mkdir -p $(BUILD_DIR)/package $(DIST_DIR)
@cp -r $(SRC_DIR)/* $(BUILD_DIR)/package/
@cd $(BUILD_DIR)/package && \
if command -v zip >/dev/null 2>&1; then \
zip -r "../../$(DIST_DIR)/$(EXTENSION_NAME)-$(EXTENSION_VERSION).zip" .; \
elif command -v pwsh >/dev/null 2>&1; then \
pwsh -Command "Compress-Archive -Path '*' -DestinationPath '../../$(DIST_DIR)/$(EXTENSION_NAME)-$(EXTENSION_VERSION).zip' -Force"; \
elif command -v powershell >/dev/null 2>&1; then \
powershell -Command "Compress-Archive -Path '*' -DestinationPath '../../$(DIST_DIR)/$(EXTENSION_NAME)-$(EXTENSION_VERSION).zip' -Force"; \
else \
echo "$(COLOR_RED)✗ No zip tool found (zip, pwsh, powershell)$(COLOR_RESET)"; \
exit 1; \
fi
@echo "$(COLOR_GREEN)✓ Package: $(DIST_DIR)/$(EXTENSION_NAME)-$(EXTENSION_VERSION).zip$(COLOR_RESET)"
.PHONY: build-beta
build-beta: clean ## Build beta release ZIP
@echo "$(COLOR_BLUE)Building $(EXTENSION_NAME) v$(EXTENSION_VERSION)-beta...$(COLOR_RESET)"
@mkdir -p $(BUILD_DIR)/package $(DIST_DIR)
@cp -r $(SRC_DIR)/* $(BUILD_DIR)/package/
@cd $(BUILD_DIR)/package && \
if command -v zip >/dev/null 2>&1; then \
zip -r "../../$(DIST_DIR)/$(EXTENSION_NAME)-$(EXTENSION_VERSION)-beta.zip" .; \
elif command -v pwsh >/dev/null 2>&1; then \
pwsh -Command "Compress-Archive -Path '*' -DestinationPath '../../$(DIST_DIR)/$(EXTENSION_NAME)-$(EXTENSION_VERSION)-beta.zip' -Force"; \
elif command -v powershell >/dev/null 2>&1; then \
powershell -Command "Compress-Archive -Path '*' -DestinationPath '../../$(DIST_DIR)/$(EXTENSION_NAME)-$(EXTENSION_VERSION)-beta.zip' -Force"; \
else \
echo "$(COLOR_RED)✗ No zip tool found$(COLOR_RESET)"; \
exit 1; \
fi
@echo "$(COLOR_GREEN)✓ Package: $(DIST_DIR)/$(EXTENSION_NAME)-$(EXTENSION_VERSION)-beta.zip$(COLOR_RESET)"
.PHONY: checksum
checksum: ## Generate SHA-256 checksums for dist packages
@echo "$(COLOR_BLUE)Generating checksums...$(COLOR_RESET)"
@for f in $(DIST_DIR)/*.zip; do \
sha256sum "$$f" | tee "$${f}.sha256"; \
done
@echo "$(COLOR_GREEN)✓ Checksums generated$(COLOR_RESET)"
# ── Release ───────────────────────────────────────────────────────────────────
.PHONY: release
release: validate build checksum ## Full release pipeline (validate + build + checksum)
@echo "$(COLOR_GREEN)✓ Release package ready$(COLOR_RESET)"
@echo ""
@echo "$(COLOR_BLUE)Next steps:$(COLOR_RESET)"
@echo " 1. Tag: git tag $(EXTENSION_VERSION)"
@echo " 2. Push: git push origin --tags"
@echo " 3. Create Gitea release and attach $(DIST_DIR)/$(EXTENSION_NAME)-$(EXTENSION_VERSION).zip"
@echo ""
# ── Info ──────────────────────────────────────────────────────────────────────
.PHONY: version
version: ## Display version and extension info
@echo "$(COLOR_BLUE)Extension Information:$(COLOR_RESET)"
@echo " Name: $(EXTENSION_NAME)"
@echo " Type: $(EXTENSION_TYPE)"
@echo " Version: $(EXTENSION_VERSION)"
.PHONY: security-check
security-check: ## Run Composer security audit
@echo "$(COLOR_BLUE)Running security checks...$(COLOR_RESET)"
@$(COMPOSER) audit
@echo "$(COLOR_GREEN)✓ Security check complete$(COLOR_RESET)"
.PHONY: all
all: install-deps validate build checksum ## Full pipeline: deps → validate → build → checksum
@echo "$(COLOR_GREEN)✓ Complete build pipeline finished$(COLOR_RESET)"
.DEFAULT_GOAL := help
+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.06 <<<<<<< 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.06 <<<<<<< 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.06 <<<<<<< 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>

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