Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f6e8c2f171 |
+1
-1
@@ -113,7 +113,7 @@ releases/
|
|||||||
build/
|
build/
|
||||||
dist/
|
dist/
|
||||||
out/
|
out/
|
||||||
/site/
|
site/
|
||||||
*.map
|
*.map
|
||||||
*.css.map
|
*.css.map
|
||||||
*.js.map
|
*.js.map
|
||||||
|
|||||||
@@ -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,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:
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: mokocli.Release
|
# INGROUP: moko-platform.Release
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
# PATH: /.mokogitea/workflows/auto-bump.yml
|
# PATH: /.mokogitea/workflows/auto-bump.yml
|
||||||
# VERSION: 09.02.00
|
# VERSION: 09.02.00
|
||||||
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
|
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
|
||||||
@@ -43,19 +43,19 @@ jobs:
|
|||||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
- name: Setup mokocli tools
|
- name: Setup moko-platform tools
|
||||||
run: |
|
run: |
|
||||||
if ! command -v composer &> /dev/null; then
|
if ! command -v composer &> /dev/null; then
|
||||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||||
fi
|
fi
|
||||||
if [ -d "/opt/mokocli/cli" ]; then
|
if [ -d "/opt/moko-platform/cli" ]; then
|
||||||
echo "MOKO_CLI=/opt/mokocli/cli" >> "$GITHUB_ENV"
|
echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV"
|
||||||
else
|
else
|
||||||
git clone --depth 1 --branch main --quiet \
|
git clone --depth 1 --branch main --quiet \
|
||||||
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/mokocli.git" \
|
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \
|
||||||
/tmp/mokocli
|
/tmp/moko-platform-api
|
||||||
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
|
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
|
||||||
echo "MOKO_CLI=/tmp/mokocli/cli" >> "$GITHUB_ENV"
|
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Bump version
|
- name: Bump version
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: mokocli.Release
|
# INGROUP: moko-platform.Release
|
||||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
||||||
# PATH: /templates/workflows/universal/auto-release.yml.template
|
# PATH: /templates/workflows/universal/auto-release.yml.template
|
||||||
# VERSION: 05.00.00
|
# VERSION: 05.00.00
|
||||||
# BRIEF: Universal build & release � detects platform from manifest.xml
|
# BRIEF: Universal build & release � detects platform from manifest.xml
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
# | 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 |
|
||||||
# | |
|
# | |
|
||||||
@@ -66,30 +66,25 @@ jobs:
|
|||||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
- name: Setup mokocli tools
|
- name: Setup moko-platform tools
|
||||||
env:
|
env:
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
run: |
|
run: |
|
||||||
if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then
|
if ! command -v composer &> /dev/null; then
|
||||||
echo Using pre-installed /opt/mokocli
|
|
||||||
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
|
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||||
fi
|
fi
|
||||||
rm -rf /tmp/mokocli
|
# Always fetch latest CLI tools — never use stale cache from previous runs
|
||||||
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git
|
rm -rf /tmp/moko-platform-api
|
||||||
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
|
git clone --depth 1 --branch main --quiet \
|
||||||
cd /tmp/mokocli
|
"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
|
composer install --no-dev --no-interaction --quiet
|
||||||
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Rename branch to rc
|
- name: Rename branch to rc
|
||||||
run: |
|
run: |
|
||||||
php ${MOKO_CLI}/branch_rename.php \
|
php /tmp/moko-platform-api/cli/branch_rename.php \
|
||||||
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
|
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
|
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
|
||||||
@@ -105,49 +100,15 @@ jobs:
|
|||||||
|
|
||||||
- name: Publish RC release
|
- name: Publish RC release
|
||||||
run: |
|
run: |
|
||||||
php ${MOKO_CLI}/release_publish.php \
|
php /tmp/moko-platform-api/cli/release_publish.php \
|
||||||
--path . --stability rc --bump minor --branch rc \
|
--path . --stability rc --bump minor --branch rc \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
- name: Update RC release notes from CHANGELOG.md
|
|
||||||
run: |
|
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
|
||||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
|
||||||
|
|
||||||
# Extract [Unreleased] section from changelog
|
|
||||||
NOTES=""
|
|
||||||
if [ -f "CHANGELOG.md" ]; then
|
|
||||||
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
|
|
||||||
fi
|
|
||||||
[ -z "$NOTES" ] && NOTES="Release candidate"
|
|
||||||
|
|
||||||
# Find the RC release and update its body
|
|
||||||
RELEASE_ID=$(curl -sf -H "Authorization: token ${TOKEN}" \
|
|
||||||
"${API_BASE}/releases/tags/release-candidate" \
|
|
||||||
| python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
|
||||||
|
|
||||||
if [ -n "$RELEASE_ID" ]; then
|
|
||||||
python3 -c "
|
|
||||||
import json, urllib.request
|
|
||||||
body = open('/dev/stdin').read()
|
|
||||||
payload = json.dumps({'body': body}).encode()
|
|
||||||
req = urllib.request.Request(
|
|
||||||
'${API_BASE}/releases/${RELEASE_ID}',
|
|
||||||
data=payload, method='PATCH',
|
|
||||||
headers={
|
|
||||||
'Authorization': 'token ${TOKEN}',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
})
|
|
||||||
urllib.request.urlopen(req)
|
|
||||||
" <<< "$NOTES"
|
|
||||||
echo "RC release notes updated from CHANGELOG.md"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Summary
|
- name: Summary
|
||||||
if: always()
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
|
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY
|
echo "Branch renamed to rc, minor bump, RC + lesser stream releases built, updates.xml synced" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
# ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
|
# ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
|
||||||
release:
|
release:
|
||||||
@@ -170,143 +131,31 @@ jobs:
|
|||||||
git config --local user.name "gitea-actions[bot]"
|
git config --local user.name "gitea-actions[bot]"
|
||||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||||
|
|
||||||
- name: Check for merge conflict markers
|
- name: Setup moko-platform tools
|
||||||
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 — aborting release"
|
|
||||||
echo "## Release Blocked: Conflict Markers" >> $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: 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
|
||||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
|
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
|
||||||
run: |
|
run: |
|
||||||
if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then
|
# Ensure PHP + Composer are available
|
||||||
echo Using pre-installed /opt/mokocli
|
if ! command -v composer &> /dev/null; then
|
||||||
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
|
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||||
fi
|
fi
|
||||||
rm -rf /tmp/mokocli
|
# Always fetch latest CLI tools — never use stale cache from previous runs
|
||||||
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git
|
rm -rf /tmp/moko-platform-api
|
||||||
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
|
git clone --depth 1 --branch main --quiet \
|
||||||
cd /tmp/mokocli
|
"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
|
composer install --no-dev --no-interaction --quiet
|
||||||
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: "Detect platform"
|
|
||||||
id: platform
|
|
||||||
run: |
|
|
||||||
php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true
|
|
||||||
php ${MOKO_CLI}/manifest_read.php --path . --github-output 2>/dev/null || true
|
|
||||||
|
|
||||||
- name: "Determine version bump level"
|
|
||||||
id: bump
|
|
||||||
run: |
|
|
||||||
# Fix/patch branches: version was already bumped by pre-release, just strip suffix
|
|
||||||
# Feature/dev branches: bump minor for the new stable release
|
|
||||||
HEAD_REF="${{ github.event.pull_request.head.ref || 'dev' }}"
|
|
||||||
case "$HEAD_REF" in
|
|
||||||
fix/*|patch/*|hotfix/*|bugfix/*) BUMP="none" ;;
|
|
||||||
*) BUMP="minor" ;;
|
|
||||||
esac
|
|
||||||
echo "level=${BUMP}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "Bump level: ${BUMP} (from branch: ${HEAD_REF})"
|
|
||||||
|
|
||||||
- name: "Publish stable release"
|
- name: "Publish stable release"
|
||||||
run: |
|
run: |
|
||||||
BUMP_FLAG=""
|
php /tmp/moko-platform-api/cli/release_publish.php \
|
||||||
if [ "${{ steps.bump.outputs.level }}" != "none" ]; then
|
--path . --stability stable --bump minor --branch main \
|
||||||
BUMP_FLAG="--bump ${{ steps.bump.outputs.level }}"
|
|
||||||
fi
|
|
||||||
php ${MOKO_CLI}/release_publish.php \
|
|
||||||
--path . --stability stable ${BUMP_FLAG} --branch main \
|
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
- name: "Read published version"
|
|
||||||
id: version
|
|
||||||
run: |
|
|
||||||
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "")
|
|
||||||
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
|
||||||
[ -z "$VERSION" ] && VERSION="00.00.00" && echo "skip=true" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "tag=stable" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "release_tag=stable" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "branch=main" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "Published version: ${VERSION}"
|
|
||||||
|
|
||||||
- name: Update release notes and promote changelog
|
|
||||||
run: |
|
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
|
||||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
|
||||||
|
|
||||||
# Get the stable release info (version and ID)
|
|
||||||
RELEASE_JSON=$(curl -sf -H "Authorization: token ${TOKEN}" \
|
|
||||||
"${API_BASE}/releases/tags/stable" 2>/dev/null || echo '{}')
|
|
||||||
RELEASE_ID=$(python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" <<< "$RELEASE_JSON" 2>/dev/null || true)
|
|
||||||
# Extract version from release name (e.g. "06.17.00" or "v06.17.00")
|
|
||||||
VERSION=$(python3 -c "
|
|
||||||
import json, sys, re
|
|
||||||
r = json.load(sys.stdin)
|
|
||||||
name = r.get('name', '')
|
|
||||||
m = re.search(r'(\d+\.\d+\.\d+)', name)
|
|
||||||
print(m.group(1) if m else '')
|
|
||||||
" <<< "$RELEASE_JSON" 2>/dev/null || true)
|
|
||||||
|
|
||||||
# Extract [Unreleased] section from changelog
|
|
||||||
NOTES=""
|
|
||||||
if [ -f "CHANGELOG.md" ]; then
|
|
||||||
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
|
|
||||||
fi
|
|
||||||
[ -z "$NOTES" ] && NOTES="Stable release"
|
|
||||||
|
|
||||||
# Update release body via API
|
|
||||||
if [ -n "$RELEASE_ID" ]; then
|
|
||||||
python3 -c "
|
|
||||||
import json, urllib.request
|
|
||||||
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) --------------------------------
|
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
|
||||||
- name: "Step 9: Mirror release to GitHub"
|
- name: "Step 9: Mirror release to GitHub"
|
||||||
if: >-
|
if: >-
|
||||||
@@ -318,7 +167,7 @@ jobs:
|
|||||||
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
||||||
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
php ${MOKO_CLI}/release_mirror.php \
|
php /tmp/moko-platform-api/cli/release_mirror.php \
|
||||||
--version "$VERSION" --tag "$RELEASE_TAG" \
|
--version "$VERSION" --tag "$RELEASE_TAG" \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
|
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
|
||||||
@@ -392,7 +241,7 @@ jobs:
|
|||||||
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}"
|
||||||
php ${MOKO_CLI}/version_reset_dev.php \
|
php /tmp/moko-platform-api/cli/version_reset_dev.php \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
|
||||||
--branch dev --path . 2>&1 || true
|
--branch dev --path . 2>&1 || true
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,191 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: Gitea.Workflow
|
|
||||||
# INGROUP: MokoStandards.CI
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
|
||||||
# PATH: /.gitea/workflows/ci-generic.yml
|
|
||||||
# VERSION: 01.00.00
|
|
||||||
# BRIEF: CI pipeline — lint, validate, and test for generic projects (PHP + Node.js)
|
|
||||||
|
|
||||||
name: "Generic: Project CI"
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
env:
|
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
# ── Lint & Validate ───────────────────────────────────────────────────
|
|
||||||
lint:
|
|
||||||
name: Lint & Validate
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Detect toolchain
|
|
||||||
id: detect
|
|
||||||
run: |
|
|
||||||
HAS_PHP=false
|
|
||||||
HAS_NODE=false
|
|
||||||
[ -f "composer.json" ] && HAS_PHP=true
|
|
||||||
[ -f "package.json" ] && HAS_NODE=true
|
|
||||||
echo "has_php=$HAS_PHP" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "has_node=$HAS_NODE" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "Toolchain: PHP=$HAS_PHP Node=$HAS_NODE"
|
|
||||||
|
|
||||||
- name: Setup PHP
|
|
||||||
if: steps.detect.outputs.has_php == 'true'
|
|
||||||
run: |
|
|
||||||
if ! command -v php &> /dev/null; then
|
|
||||||
sudo apt-get update -qq
|
|
||||||
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
|
|
||||||
fi
|
|
||||||
php -v
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
if: steps.detect.outputs.has_node == 'true'
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '20'
|
|
||||||
|
|
||||||
- name: Install PHP dependencies
|
|
||||||
if: steps.detect.outputs.has_php == 'true'
|
|
||||||
run: |
|
|
||||||
if [ -f "composer.json" ]; then
|
|
||||||
composer install --no-interaction --prefer-dist --quiet 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Install Node.js dependencies
|
|
||||||
if: steps.detect.outputs.has_node == 'true'
|
|
||||||
run: |
|
|
||||||
if [ -f "package.json" ]; then
|
|
||||||
npm ci --quiet 2>/dev/null || npm install --quiet 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: PHP syntax check
|
|
||||||
if: steps.detect.outputs.has_php == 'true'
|
|
||||||
run: |
|
|
||||||
ERRORS=0
|
|
||||||
while IFS= read -r -d '' file; do
|
|
||||||
if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
|
|
||||||
echo "::error file=${file}::PHP syntax error"
|
|
||||||
ERRORS=$((ERRORS + 1))
|
|
||||||
fi
|
|
||||||
done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -not -path "./node_modules/*" -print0)
|
|
||||||
|
|
||||||
echo "## PHP Lint" >> $GITHUB_STEP_SUMMARY
|
|
||||||
if [ "$ERRORS" -eq 0 ]; then
|
|
||||||
echo "All PHP files passed syntax check." >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "${ERRORS} file(s) with syntax errors." >> $GITHUB_STEP_SUMMARY
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: TypeScript/JavaScript lint
|
|
||||||
if: steps.detect.outputs.has_node == 'true'
|
|
||||||
run: |
|
|
||||||
if [ -f "node_modules/.bin/eslint" ]; then
|
|
||||||
npx eslint src/ --quiet 2>&1 || { echo "::error::ESLint errors found"; exit 1; }
|
|
||||||
echo "## ESLint" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "All files passed ESLint." >> $GITHUB_STEP_SUMMARY
|
|
||||||
elif [ -f ".eslintrc.json" ] || [ -f ".eslintrc.js" ] || [ -f "eslint.config.js" ]; then
|
|
||||||
echo "::warning::ESLint config found but eslint not installed"
|
|
||||||
else
|
|
||||||
echo "No ESLint configured — skipping"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: TypeScript compile check
|
|
||||||
if: steps.detect.outputs.has_node == 'true'
|
|
||||||
run: |
|
|
||||||
if [ -f "tsconfig.json" ] && [ -f "node_modules/.bin/tsc" ]; then
|
|
||||||
npx tsc --noEmit 2>&1 || { echo "::error::TypeScript compilation errors"; exit 1; }
|
|
||||||
echo "## TypeScript" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "TypeScript compilation passed." >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: PHPStan static analysis
|
|
||||||
if: steps.detect.outputs.has_php == 'true'
|
|
||||||
run: |
|
|
||||||
if [ -f "phpstan.neon" ] && [ -f "vendor/bin/phpstan" ]; then
|
|
||||||
vendor/bin/phpstan analyse --no-progress 2>&1 || { echo "::warning::PHPStan found issues"; }
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Tests ─────────────────────────────────────────────────────────────
|
|
||||||
test:
|
|
||||||
name: Tests
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: lint
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Detect toolchain
|
|
||||||
id: detect
|
|
||||||
run: |
|
|
||||||
HAS_PHP=false
|
|
||||||
HAS_NODE=false
|
|
||||||
[ -f "composer.json" ] && HAS_PHP=true
|
|
||||||
[ -f "package.json" ] && HAS_NODE=true
|
|
||||||
echo "has_php=$HAS_PHP" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "has_node=$HAS_NODE" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
- name: Setup PHP
|
|
||||||
if: steps.detect.outputs.has_php == 'true'
|
|
||||||
run: |
|
|
||||||
if ! command -v php &> /dev/null; then
|
|
||||||
sudo apt-get update -qq
|
|
||||||
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
if: steps.detect.outputs.has_node == 'true'
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '20'
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
[ -f "composer.json" ] && composer install --no-interaction --prefer-dist --quiet 2>/dev/null || true
|
|
||||||
[ -f "package.json" ] && { npm ci --quiet 2>/dev/null || npm install --quiet 2>/dev/null || true; }
|
|
||||||
|
|
||||||
- name: Run PHP tests
|
|
||||||
if: steps.detect.outputs.has_php == 'true'
|
|
||||||
run: |
|
|
||||||
if [ -f "vendor/bin/phpunit" ]; then
|
|
||||||
vendor/bin/phpunit --testdox 2>&1
|
|
||||||
echo "## PHPUnit" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "Tests passed." >> $GITHUB_STEP_SUMMARY
|
|
||||||
elif [ -f "phpunit.xml" ] || [ -f "phpunit.xml.dist" ]; then
|
|
||||||
echo "::warning::PHPUnit config found but phpunit not installed"
|
|
||||||
else
|
|
||||||
echo "No PHPUnit configured — skipping"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Run Node.js tests
|
|
||||||
if: steps.detect.outputs.has_node == 'true'
|
|
||||||
run: |
|
|
||||||
if jq -e '.scripts.test' package.json > /dev/null 2>&1; then
|
|
||||||
npm test 2>&1
|
|
||||||
echo "## Node.js Tests" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "Tests passed." >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "No test script in package.json — skipping"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Build check
|
|
||||||
run: |
|
|
||||||
if [ -f "Makefile" ]; then
|
|
||||||
make build 2>&1 || echo "::warning::Build failed or not configured"
|
|
||||||
elif [ -f "package.json" ] && jq -e '.scripts.build' package.json > /dev/null 2>&1; then
|
|
||||||
npm run build 2>&1 || echo "::warning::Build failed"
|
|
||||||
fi
|
|
||||||
@@ -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
|
|
||||||
echo "mokocli already available on runner — skipping clone"
|
|
||||||
else
|
|
||||||
git clone --depth 1 --branch main --quiet \
|
git clone --depth 1 --branch main --quiet \
|
||||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git" \
|
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
|
||||||
/tmp/mokocli 2>/dev/null || echo "mokocli clone skipped — continuing without it"
|
/tmp/mokostandards-api
|
||||||
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
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: MokoStandards.Maintenance
|
# INGROUP: moko-platform.Maintenance
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
# PATH: /.gitea/workflows/cleanup.yml
|
# PATH: /.gitea/workflows/cleanup.yml
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
|
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
|
||||||
@@ -33,17 +33,17 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ secrets.GA_TOKEN }}
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
|
||||||
- name: Delete merged branches
|
- name: Delete merged branches
|
||||||
env:
|
env:
|
||||||
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
echo "=== Merged Branch Cleanup ==="
|
echo "=== Merged Branch Cleanup ==="
|
||||||
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||||
|
|
||||||
# List branches via API
|
# List branches via API
|
||||||
BRANCHES=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
|
BRANCHES=$(curl -sS -H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
"${API}/branches?limit=50" | jq -r '.[].name')
|
"${API}/branches?limit=50" | jq -r '.[].name')
|
||||||
|
|
||||||
DELETED=0
|
DELETED=0
|
||||||
@@ -56,7 +56,7 @@ jobs:
|
|||||||
# Check if branch is merged into main
|
# Check if branch is merged into main
|
||||||
if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then
|
if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then
|
||||||
echo " Deleting merged branch: ${BRANCH}"
|
echo " Deleting merged branch: ${BRANCH}"
|
||||||
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
|
curl -sS -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
"${API}/branches/${BRANCH}" 2>/dev/null || true
|
"${API}/branches/${BRANCH}" 2>/dev/null || true
|
||||||
DELETED=$((DELETED + 1))
|
DELETED=$((DELETED + 1))
|
||||||
fi
|
fi
|
||||||
@@ -66,20 +66,20 @@ jobs:
|
|||||||
|
|
||||||
- name: Clean old workflow runs
|
- name: Clean old workflow runs
|
||||||
env:
|
env:
|
||||||
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
echo "=== Workflow Run Cleanup ==="
|
echo "=== Workflow Run Cleanup ==="
|
||||||
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||||
CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ)
|
CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ)
|
||||||
|
|
||||||
# Get old completed runs
|
# Get old completed runs
|
||||||
RUNS=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
|
RUNS=$(curl -sS -H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
"${API}/actions/runs?status=completed&limit=50" | \
|
"${API}/actions/runs?status=completed&limit=50" | \
|
||||||
jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null)
|
jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null)
|
||||||
|
|
||||||
DELETED=0
|
DELETED=0
|
||||||
for RUN_ID in $RUNS; do
|
for RUN_ID in $RUNS; do
|
||||||
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
|
curl -sS -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
"${API}/actions/runs/${RUN_ID}" 2>/dev/null || true
|
"${API}/actions/runs/${RUN_ID}" 2>/dev/null || true
|
||||||
DELETED=$((DELETED + 1))
|
DELETED=$((DELETED + 1))
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -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:
|
|
||||||
GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
|
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
|
|
||||||
MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
|
|
||||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_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
|
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -4,8 +4,18 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: mokocli.Automation
|
# INGROUP: moko-platform.Automation
|
||||||
# VERSION: 02.26.02
|
<<<<<<< HEAD
|
||||||
|
<<<<<<< HEAD
|
||||||
|
# VERSION: 02.17.00
|
||||||
|
=======
|
||||||
|
# VERSION: 02.17.00
|
||||||
|
=======
|
||||||
|
# VERSION: 02.17.00
|
||||||
|
=======
|
||||||
|
# VERSION: 02.17.00
|
||||||
|
>>>>>>> origin/main
|
||||||
|
>>>>>>> origin/main
|
||||||
# BRIEF: Auto-create feature branch when an issue is opened
|
# BRIEF: Auto-create feature branch when an issue is opened
|
||||||
|
|
||||||
name: "Universal: Issue Branch"
|
name: "Universal: Issue Branch"
|
||||||
@@ -28,7 +38,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Create branch and comment
|
- name: Create branch and comment
|
||||||
run: |
|
run: |
|
||||||
TOKEN="${{ secrets.GA_TOKEN }}"
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||||
ISSUE_NUM="${{ github.event.issue.number }}"
|
ISSUE_NUM="${{ github.event.issue.number }}"
|
||||||
ISSUE_TITLE="${{ github.event.issue.title }}"
|
ISSUE_TITLE="${{ github.event.issue.title }}"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -504,31 +234,3 @@ jobs:
|
|||||||
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\"}}"
|
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
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: [branch-policy, validate]
|
|
||||||
if: >-
|
|
||||||
always() &&
|
|
||||||
needs.validate.result == 'failure'
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
sparse-checkout: automation/ci-issue-reporter.sh
|
|
||||||
sparse-checkout-cone-mode: false
|
|
||||||
|
|
||||||
- name: "File issue for PR validation failure"
|
|
||||||
env:
|
|
||||||
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
|
||||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
|
||||||
run: |
|
|
||||||
chmod +x automation/ci-issue-reporter.sh
|
|
||||||
./automation/ci-issue-reporter.sh \
|
|
||||||
--gate "PR Validation" \
|
|
||||||
--workflow "PR Check" \
|
|
||||||
--severity error \
|
|
||||||
--details "PR validation failed (syntax, manifest, changelog, or source checks). See the CI run for the specific check that failed."
|
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: Gitea.Workflow
|
|
||||||
# INGROUP: mokocli.Validation
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
|
||||||
# PATH: /templates/workflows/joomla/pr-metadata-check.yml.template
|
|
||||||
# VERSION: 01.00.00
|
|
||||||
# BRIEF: Validate MokoGitea metadata matches Joomla extension manifest on PRs
|
|
||||||
|
|
||||||
name: "Joomla: Metadata Validation"
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [opened, synchronize, reopened, converted_to_draft, ready_for_review]
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
env:
|
|
||||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
|
||||||
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
|
||||||
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
validate-metadata:
|
|
||||||
name: "Validate Joomla Metadata"
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup mokocli tools
|
|
||||||
env:
|
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
|
||||||
run: |
|
|
||||||
if [ -f /opt/mokocli/cli/joomla_metadata_validate.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then
|
|
||||||
echo Using pre-installed /opt/mokocli
|
|
||||||
echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV
|
|
||||||
else
|
|
||||||
echo Falling back to fresh clone
|
|
||||||
if ! command -v composer > /dev/null 2>&1; then
|
|
||||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
|
|
||||||
fi
|
|
||||||
rm -rf /tmp/mokocli
|
|
||||||
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git
|
|
||||||
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
|
|
||||||
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
|
|
||||||
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Validate metadata against Joomla manifest
|
|
||||||
env:
|
|
||||||
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
|
||||||
run: |
|
|
||||||
php ${MOKO_CLI}/joomla_metadata_validate.php \
|
|
||||||
--path . \
|
|
||||||
--token "${GITEA_TOKEN}" \
|
|
||||||
--org "${GITEA_ORG}" \
|
|
||||||
--repo "${GITEA_REPO}" \
|
|
||||||
--api-base "${GITEA_URL}/api/v1" \
|
|
||||||
--ci
|
|
||||||
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
echo "::error::Joomla metadata mismatch — update delivery will fail. Run 'php cli/joomla_metadata_validate.php' locally to see details."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
@@ -4,26 +4,19 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: mokocli.Release
|
# INGROUP: moko-platform.Release
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
# PATH: /templates/workflows/universal/pre-release.yml.template
|
# PATH: /templates/workflows/universal/pre-release.yml.template
|
||||||
# VERSION: 05.01.00
|
# VERSION: 05.01.00
|
||||||
# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches
|
# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch
|
||||||
|
|
||||||
name: "Universal: Pre-Release"
|
name: "Universal: Pre-Release"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
pull_request:
|
||||||
|
types: [closed]
|
||||||
branches:
|
branches:
|
||||||
- dev
|
- dev
|
||||||
- 'fix/**'
|
|
||||||
- 'patch/**'
|
|
||||||
- 'hotfix/**'
|
|
||||||
- 'bugfix/**'
|
|
||||||
- 'chore/**'
|
|
||||||
- alpha
|
|
||||||
- beta
|
|
||||||
- rc
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
stability:
|
stability:
|
||||||
@@ -46,11 +39,11 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: "Build Pre-Release (${{ inputs.stability || github.ref_name }})"
|
name: "Build Pre-Release (${{ inputs.stability || 'development' }})"
|
||||||
runs-on: release
|
runs-on: release
|
||||||
if: >-
|
if: >-
|
||||||
github.event_name == 'workflow_dispatch' ||
|
github.event_name == 'workflow_dispatch' ||
|
||||||
github.event_name == 'push'
|
(github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev')
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -58,50 +51,32 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
ref: ${{ github.ref_name }}
|
|
||||||
|
|
||||||
- name: Setup mokocli tools
|
- name: Setup moko-platform tools
|
||||||
env:
|
env:
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
run: |
|
run: |
|
||||||
# Use pre-installed /opt/mokocli if available (updated by cron every 6h)
|
if ! command -v composer &> /dev/null; then
|
||||||
if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/cli/manifest_element.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then
|
|
||||||
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
|
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||||
fi
|
fi
|
||||||
rm -rf /tmp/mokocli
|
# Always fetch latest CLI tools — never use stale cache from previous runs
|
||||||
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git
|
rm -rf /tmp/moko-platform-api
|
||||||
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
|
git clone --depth 1 --branch main --quiet \
|
||||||
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
|
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
||||||
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
|
/tmp/moko-platform-api
|
||||||
fi
|
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
|
||||||
|
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
- name: Detect platform
|
- name: Detect platform
|
||||||
id: platform
|
id: platform
|
||||||
run: |
|
run: |
|
||||||
# Auto-detect and update platform if not set in manifest
|
|
||||||
php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true
|
|
||||||
php ${MOKO_CLI}/manifest_read.php --path . --github-output
|
php ${MOKO_CLI}/manifest_read.php --path . --github-output
|
||||||
|
|
||||||
- name: Resolve metadata and bump version
|
- name: Resolve metadata and bump version
|
||||||
id: meta
|
id: meta
|
||||||
run: |
|
run: |
|
||||||
# Auto-detect stability from branch name on push, or use input on dispatch
|
|
||||||
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' }}"
|
STABILITY="${{ inputs.stability || 'development' }}"
|
||||||
fi
|
|
||||||
|
|
||||||
case "$STABILITY" in
|
case "$STABILITY" in
|
||||||
development) SUFFIX="-dev"; TAG="development" ;;
|
development) SUFFIX="-dev"; TAG="development" ;;
|
||||||
@@ -110,26 +85,20 @@ jobs:
|
|||||||
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
|
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# Bump version via CLI: patch for dev/alpha/beta, minor for RC
|
# Read current version (bump already handled by push workflow)
|
||||||
case "$STABILITY" in
|
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null)
|
||||||
release-candidate) BUMP="minor" ;;
|
[ -z "$VERSION" ] && VERSION="00.00.01"
|
||||||
*) BUMP="patch" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
php ${MOKO_CLI}/version_bump.php --path . $([ "$BUMP" = "minor" ] && echo "--minor") 2>/dev/null || true
|
# Strip any existing suffix from version before applying stability
|
||||||
|
|
||||||
# Set stability suffix and verify consistency
|
|
||||||
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "00.00.01")
|
|
||||||
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
||||||
|
|
||||||
php ${MOKO_CLI}/version_set_platform.php \
|
php ${MOKO_CLI}/version_set_platform.php \
|
||||||
--path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true
|
--path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Verify version consistency across all files
|
||||||
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
|
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
|
||||||
|
|
||||||
# Ensure licensing tags (updateservers, dlid) if enabled in manifest.xml
|
# Update VERSION variable with suffix
|
||||||
php ${MOKO_CLI}/manifest_licensing.php --path . --fix 2>/dev/null || true
|
|
||||||
|
|
||||||
# Append suffix for output
|
|
||||||
if [ -n "$SUFFIX" ]; then
|
if [ -n "$SUFFIX" ]; then
|
||||||
VERSION="${VERSION}${SUFFIX}"
|
VERSION="${VERSION}${SUFFIX}"
|
||||||
fi
|
fi
|
||||||
@@ -173,42 +142,7 @@ jobs:
|
|||||||
php ${MOKO_CLI}/release_create.php \
|
php ${MOKO_CLI}/release_create.php \
|
||||||
--path . --version "$VERSION" --tag "$TAG" \
|
--path . --version "$VERSION" --tag "$TAG" \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
--repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease
|
--repo "${GITEA_REPO}" --branch dev --prerelease
|
||||||
|
|
||||||
- name: Update release notes from CHANGELOG.md
|
|
||||||
run: |
|
|
||||||
TAG="${{ steps.meta.outputs.tag }}"
|
|
||||||
VERSION="${{ steps.meta.outputs.version }}"
|
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
|
||||||
|
|
||||||
# Extract [Unreleased] section from changelog (everything between [Unreleased] and next ## heading)
|
|
||||||
if [ -f "CHANGELOG.md" ]; then
|
|
||||||
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
|
|
||||||
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
|
|
||||||
else
|
|
||||||
NOTES="Release ${VERSION}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Update release body via API
|
|
||||||
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
|
||||||
"${API_BASE}/releases/tags/${TAG}" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
|
||||||
|
|
||||||
if [ -n "$RELEASE_ID" ]; then
|
|
||||||
python3 -c "
|
|
||||||
import json, urllib.request
|
|
||||||
body = open('/dev/stdin').read()
|
|
||||||
payload = json.dumps({'body': body}).encode()
|
|
||||||
req = urllib.request.Request(
|
|
||||||
'${API_BASE}/releases/${RELEASE_ID}',
|
|
||||||
data=payload, method='PATCH',
|
|
||||||
headers={
|
|
||||||
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
})
|
|
||||||
urllib.request.urlopen(req)
|
|
||||||
" <<< "$NOTES"
|
|
||||||
echo "Release notes updated from CHANGELOG.md"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Build package and upload
|
- name: Build package and upload
|
||||||
id: package
|
id: package
|
||||||
@@ -221,8 +155,55 @@ jobs:
|
|||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
--repo "${GITEA_REPO}" --output /tmp || true
|
--repo "${GITEA_REPO}" --output /tmp || true
|
||||||
|
|
||||||
# updates.xml is generated dynamically by MokoGitea license server
|
- name: Update updates.xml
|
||||||
# No need to build, commit, or sync updates.xml from workflows
|
if: steps.platform.outputs.platform == 'joomla'
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
|
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||||
|
SHA256="${{ steps.package.outputs.sha256_zip }}"
|
||||||
|
|
||||||
|
if [ ! -f "updates.xml" ]; then
|
||||||
|
echo "No updates.xml -- skipping"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
SHA_FLAG=""
|
||||||
|
[ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
|
||||||
|
|
||||||
|
php ${MOKO_CLI}/updates_xml_build.php \
|
||||||
|
--path . --version "${VERSION}" --stability "${STABILITY}" \
|
||||||
|
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
|
||||||
|
${SHA_FLAG}
|
||||||
|
|
||||||
|
# Commit and push
|
||||||
|
if ! git diff --quiet updates.xml 2>/dev/null; then
|
||||||
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
|
git config --local user.name "gitea-actions[bot]"
|
||||||
|
git add updates.xml
|
||||||
|
git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
|
||||||
|
git push origin HEAD 2>&1 || echo "WARNING: push failed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: "Sync updates.xml to all branches"
|
||||||
|
if: steps.platform.outputs.platform == 'joomla'
|
||||||
|
run: |
|
||||||
|
CURRENT_BRANCH="${{ github.ref_name }}"
|
||||||
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
|
git config --local user.name "gitea-actions[bot]"
|
||||||
|
|
||||||
|
for BRANCH in main dev; do
|
||||||
|
[ "$BRANCH" = "$CURRENT_BRANCH" ] && continue
|
||||||
|
echo "Syncing updates.xml -> ${BRANCH}"
|
||||||
|
git fetch origin "${BRANCH}" 2>/dev/null || continue
|
||||||
|
git checkout "origin/${BRANCH}" -- updates.xml 2>/dev/null || continue
|
||||||
|
git checkout "${CURRENT_BRANCH}" -- updates.xml
|
||||||
|
if ! git diff --quiet updates.xml 2>/dev/null; then
|
||||||
|
git add updates.xml
|
||||||
|
git commit -m "chore: sync updates.xml from ${CURRENT_BRANCH} [skip ci]"
|
||||||
|
git push origin HEAD:refs/heads/${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed"
|
||||||
|
fi
|
||||||
|
git checkout "${CURRENT_BRANCH}" 2>/dev/null
|
||||||
|
done
|
||||||
|
|
||||||
- name: "Delete lesser pre-release channels (cascade)"
|
- name: "Delete lesser pre-release channels (cascade)"
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: Gitea.Workflow
|
|
||||||
# INGROUP: mokocli.Universal
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
|
||||||
# PATH: /.mokogitea/workflows/rc-revert.yml
|
|
||||||
# VERSION: 09.23.00
|
|
||||||
# BRIEF: Rename rc/ branch back to dev/ when PR is closed without merge
|
|
||||||
|
|
||||||
name: "RC Revert"
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [closed]
|
|
||||||
|
|
||||||
env:
|
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
revert:
|
|
||||||
name: Rename rc/ back to dev/
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: >-
|
|
||||||
github.event.pull_request.merged == false &&
|
|
||||||
startsWith(github.event.pull_request.head.ref, 'rc/')
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Rename branch
|
|
||||||
run: |
|
|
||||||
BRANCH="${{ github.event.pull_request.head.ref }}"
|
|
||||||
SUFFIX="${BRANCH#rc/}"
|
|
||||||
DEV_BRANCH="dev/${SUFFIX}"
|
|
||||||
API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches"
|
|
||||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
|
||||||
|
|
||||||
# Create dev/ branch from rc/ branch
|
|
||||||
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X POST \
|
|
||||||
-H "Authorization: token ${TOKEN}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "{\"new_branch_name\": \"${DEV_BRANCH}\", \"old_branch_name\": \"${BRANCH}\"}" \
|
|
||||||
"${API}" 2>/dev/null || true)
|
|
||||||
|
|
||||||
if [ "$STATUS" = "201" ]; then
|
|
||||||
echo "Created branch: ${DEV_BRANCH}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "::error::Failed to create ${DEV_BRANCH} from ${BRANCH} (HTTP ${STATUS})"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Delete rc/ branch
|
|
||||||
ENCODED=$(php -r "echo rawurlencode('${BRANCH}');")
|
|
||||||
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \
|
|
||||||
-H "Authorization: token ${TOKEN}" \
|
|
||||||
"${API}/${ENCODED}" 2>/dev/null || true)
|
|
||||||
|
|
||||||
if [ "$STATUS" = "204" ]; then
|
|
||||||
echo "Deleted branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "::warning::Failed to delete ${BRANCH} (HTTP ${STATUS})"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "### RC Reverted" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "${BRANCH} → ${DEV_BRANCH}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
@@ -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
|
||||||
@@ -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,45 +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-issues:
|
|
||||||
name: "Report Issues"
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: [access_check, scripts_governance, repo_health]
|
|
||||||
if: >-
|
|
||||||
always() &&
|
|
||||||
(needs.scripts_governance.result == 'failure' ||
|
|
||||||
needs.repo_health.result == 'failure')
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
sparse-checkout: automation/ci-issue-reporter.sh
|
|
||||||
sparse-checkout-cone-mode: false
|
|
||||||
|
|
||||||
- name: "File issues for failed gates"
|
|
||||||
env:
|
|
||||||
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
|
||||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
|
||||||
run: |
|
|
||||||
chmod +x automation/ci-issue-reporter.sh
|
|
||||||
REPORTER="./automation/ci-issue-reporter.sh"
|
|
||||||
WF="Repo Health"
|
|
||||||
|
|
||||||
report_gate() {
|
|
||||||
local gate="$1" result="$2" details="$3"
|
|
||||||
if [ "$result" = "failure" ]; then
|
|
||||||
"$REPORTER" --gate "$gate" --details "$details" --workflow "$WF" --severity error
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
report_gate "Scripts Governance" \
|
|
||||||
"${{ needs.scripts_governance.result }}" \
|
|
||||||
"Scripts directory policy violations detected. Review required and allowed directories."
|
|
||||||
|
|
||||||
report_gate "Repository Health" \
|
|
||||||
"${{ needs.repo_health.result }}" \
|
|
||||||
"Repository health checks failed — missing required artifacts, disallowed files, or content warnings. Check the CI run summary."
|
|
||||||
|
|||||||
@@ -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/MokoStandards
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
# PATH: /.gitea/workflows/security-audit.yml
|
# PATH: /.gitea/workflows/security-audit.yml
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Dependency vulnerability scanning for composer and npm packages
|
# BRIEF: Dependency vulnerability scanning for composer and npm packages
|
||||||
@@ -80,3 +80,19 @@ jobs:
|
|||||||
-H "Priority: high" \
|
-H "Priority: high" \
|
||||||
-d "Security audit found vulnerabilities. Review dependency updates." \
|
-d "Security audit found vulnerabilities. Review dependency updates." \
|
||||||
"${NTFY_URL}/${NTFY_TOPIC}" || true
|
"${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 }}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,312 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: moko-platform.Universal
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
|
# PATH: /templates/workflows/update-server.yml
|
||||||
|
# VERSION: 05.00.00
|
||||||
|
# BRIEF: Pre-release build + update server XML for dev/alpha/beta/rc branches
|
||||||
|
#
|
||||||
|
# Thin wrapper around moko-platform CLI tools.
|
||||||
|
# Builds packages, updates updates.xml, and optionally deploys via SFTP.
|
||||||
|
#
|
||||||
|
# Joomla filters update entries by the user's "Minimum Stability" setting.
|
||||||
|
|
||||||
|
name: "Update Server"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'dev'
|
||||||
|
- 'dev/**'
|
||||||
|
- 'alpha/**'
|
||||||
|
- 'beta/**'
|
||||||
|
- 'rc/**'
|
||||||
|
paths:
|
||||||
|
- 'src/**'
|
||||||
|
- 'htdocs/**'
|
||||||
|
pull_request:
|
||||||
|
types: [closed]
|
||||||
|
branches:
|
||||||
|
- 'dev'
|
||||||
|
- 'dev/**'
|
||||||
|
- 'alpha/**'
|
||||||
|
- 'beta/**'
|
||||||
|
- 'rc/**'
|
||||||
|
paths:
|
||||||
|
- 'src/**'
|
||||||
|
- 'htdocs/**'
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
stability:
|
||||||
|
description: 'Stability tag'
|
||||||
|
required: true
|
||||||
|
default: 'development'
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- development
|
||||||
|
- alpha
|
||||||
|
- beta
|
||||||
|
- rc
|
||||||
|
- stable
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||||||
|
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update-xml:
|
||||||
|
name: Update Server
|
||||||
|
runs-on: release
|
||||||
|
if: >-
|
||||||
|
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || github.event_name == 'push'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup moko-platform tools
|
||||||
|
env:
|
||||||
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
|
COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.MOKOGITEA_TOKEN }}"}}}'
|
||||||
|
run: |
|
||||||
|
if ! command -v composer &> /dev/null; then
|
||||||
|
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
# Always fetch latest CLI tools — never use stale cache from previous runs
|
||||||
|
rm -rf /tmp/moko-platform
|
||||||
|
git clone --depth 1 --branch main --quiet \
|
||||||
|
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
||||||
|
/tmp/moko-platform 2>/dev/null || true
|
||||||
|
if [ -d "/tmp/moko-platform" ] && [ -f "/tmp/moko-platform/composer.json" ]; then
|
||||||
|
cd /tmp/moko-platform && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
echo "MOKO_CLI=/tmp/moko-platform/cli" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Detect platform
|
||||||
|
id: platform
|
||||||
|
run: php ${MOKO_CLI}/manifest_read.php --path . --github-output
|
||||||
|
|
||||||
|
- name: Resolve stability and bump version
|
||||||
|
id: meta
|
||||||
|
run: |
|
||||||
|
BRANCH="${{ github.ref_name }}"
|
||||||
|
|
||||||
|
# Configure git for bot pushes
|
||||||
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
|
git config --local user.name "gitea-actions[bot]"
|
||||||
|
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||||
|
|
||||||
|
# Auto-bump patch version
|
||||||
|
php ${MOKO_CLI}/version_bump.php --path . 2>/dev/null || true
|
||||||
|
|
||||||
|
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "0.0.0")
|
||||||
|
|
||||||
|
# Strip any existing suffix before applying stability
|
||||||
|
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
||||||
|
|
||||||
|
# Determine stability from branch or manual input
|
||||||
|
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||||
|
STABILITY="${{ inputs.stability }}"
|
||||||
|
elif [[ "$BRANCH" == rc/* ]]; then
|
||||||
|
STABILITY="rc"
|
||||||
|
elif [[ "$BRANCH" == beta/* ]]; then
|
||||||
|
STABILITY="beta"
|
||||||
|
elif [[ "$BRANCH" == alpha/* ]]; then
|
||||||
|
STABILITY="alpha"
|
||||||
|
else
|
||||||
|
STABILITY="development"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Version suffix per stability stream
|
||||||
|
case "$STABILITY" in
|
||||||
|
development) SUFFIX="-dev"; TAG="development" ;;
|
||||||
|
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
|
||||||
|
beta) SUFFIX="-beta"; TAG="beta" ;;
|
||||||
|
rc) SUFFIX="-rc"; TAG="release-candidate" ;;
|
||||||
|
*) SUFFIX=""; TAG="stable" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Propagate version with stability suffix to all manifest files
|
||||||
|
php ${MOKO_CLI}/version_set_platform.php \
|
||||||
|
--path . --version "$VERSION" --branch "$BRANCH" --stability "$STABILITY" 2>/dev/null || true
|
||||||
|
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
|
||||||
|
|
||||||
|
# Re-read version (now includes suffix from version_set_platform)
|
||||||
|
if [ -n "$SUFFIX" ]; then
|
||||||
|
VERSION="${VERSION}${SUFFIX}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "display_version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
# Commit version bump if changed
|
||||||
|
git add -A
|
||||||
|
git diff --cached --quiet || {
|
||||||
|
git commit -m "chore(version): auto-bump ${VERSION} [skip ci]" \
|
||||||
|
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
|
||||||
|
git push
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Create release and upload package
|
||||||
|
id: package
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
|
TAG="${{ steps.meta.outputs.tag }}"
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
|
||||||
|
# Create or update Gitea release
|
||||||
|
php ${MOKO_CLI}/release_create.php \
|
||||||
|
--path . --version "$VERSION" --tag "$TAG" \
|
||||||
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
|
--repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease
|
||||||
|
|
||||||
|
# Build package and upload
|
||||||
|
php ${MOKO_CLI}/release_package.php \
|
||||||
|
--path . --version "$VERSION" --tag "$TAG" \
|
||||||
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
|
--repo "${GITEA_REPO}" --output /tmp || true
|
||||||
|
|
||||||
|
- name: Update updates.xml
|
||||||
|
if: steps.platform.outputs.platform == 'joomla'
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
|
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||||
|
SHA256="${{ steps.package.outputs.sha256_zip }}"
|
||||||
|
|
||||||
|
if [ ! -f "updates.xml" ]; then
|
||||||
|
echo "No updates.xml — skipping"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
SHA_FLAG=""
|
||||||
|
[ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
|
||||||
|
|
||||||
|
php ${MOKO_CLI}/updates_xml_build.php \
|
||||||
|
--path . --version "${VERSION}" --stability "${STABILITY}" \
|
||||||
|
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
|
||||||
|
${SHA_FLAG}
|
||||||
|
|
||||||
|
# Commit and push updates.xml
|
||||||
|
git add updates.xml
|
||||||
|
git diff --cached --quiet || {
|
||||||
|
git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
|
||||||
|
git push
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Sync updates.xml to main
|
||||||
|
if: github.ref_name != 'main' && steps.platform.outputs.platform == 'joomla'
|
||||||
|
run: |
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
GITEA_TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
|
FILE_SHA=$(curl -sf -H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
"${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then
|
||||||
|
python3 -c "
|
||||||
|
import base64, json, urllib.request, sys
|
||||||
|
with open('updates.xml', 'rb') as f:
|
||||||
|
content = base64.b64encode(f.read()).decode()
|
||||||
|
payload = json.dumps({
|
||||||
|
'content': content,
|
||||||
|
'sha': '${FILE_SHA}',
|
||||||
|
'message': 'chore: sync updates.xml from ${{ steps.meta.outputs.stability }} [skip ci]',
|
||||||
|
'branch': 'main'
|
||||||
|
}).encode()
|
||||||
|
req = urllib.request.Request(
|
||||||
|
'${API_BASE}/contents/updates.xml',
|
||||||
|
data=payload, method='PUT',
|
||||||
|
headers={
|
||||||
|
'Authorization': 'token ${GITEA_TOKEN}',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
})
|
||||||
|
try:
|
||||||
|
urllib.request.urlopen(req)
|
||||||
|
print('updates.xml synced to main')
|
||||||
|
except Exception as e:
|
||||||
|
print(f'WARNING: sync to main failed: {e}', file=sys.stderr)
|
||||||
|
"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: SFTP deploy to dev server
|
||||||
|
if: contains(github.ref, 'dev/') || github.ref == 'refs/heads/dev'
|
||||||
|
env:
|
||||||
|
DEV_HOST: ${{ vars.DEV_FTP_HOST }}
|
||||||
|
DEV_PATH: ${{ vars.DEV_FTP_PATH }}
|
||||||
|
DEV_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
|
||||||
|
DEV_USER: ${{ vars.DEV_FTP_USERNAME }}
|
||||||
|
DEV_PORT: ${{ vars.DEV_FTP_PORT }}
|
||||||
|
DEV_KEY: ${{ secrets.DEV_FTP_KEY }}
|
||||||
|
DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
# Permission check: admin or maintain role required
|
||||||
|
ACTOR="${{ github.actor }}"
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
|
||||||
|
PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
|
"${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \
|
||||||
|
python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read")
|
||||||
|
case "$PERMISSION" in
|
||||||
|
admin|maintain|write) ;;
|
||||||
|
*)
|
||||||
|
echo "Deploy denied: ${ACTOR} has '${PERMISSION}' — requires admin, maintain, or write"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
[ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured — skipping SFTP"; exit 0; }
|
||||||
|
|
||||||
|
SOURCE_DIR="src"
|
||||||
|
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
||||||
|
[ ! -d "$SOURCE_DIR" ] && exit 0
|
||||||
|
|
||||||
|
PORT="${DEV_PORT:-22}"
|
||||||
|
REMOTE="${DEV_PATH%/}"
|
||||||
|
[ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}"
|
||||||
|
|
||||||
|
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
|
||||||
|
"$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json
|
||||||
|
if [ -n "$DEV_KEY" ]; then
|
||||||
|
echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key
|
||||||
|
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
|
||||||
|
else
|
||||||
|
printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json
|
||||||
|
fi
|
||||||
|
|
||||||
|
PLATFORM=$(php ${MOKO_CLI}/platform_detect.php --path . 2>/dev/null || true)
|
||||||
|
if [ "$PLATFORM" = "waas-component" ] && [ -f "${MOKO_CLI}/../deploy/deploy-joomla.php" ]; then
|
||||||
|
php ${MOKO_CLI}/../deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
|
||||||
|
elif [ -f "${MOKO_CLI}/../deploy/deploy-sftp.php" ]; then
|
||||||
|
php ${MOKO_CLI}/../deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
|
||||||
|
fi
|
||||||
|
rm -f /tmp/deploy_key /tmp/sftp-config.json
|
||||||
|
echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
- name: Summary
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
|
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||||
|
DISPLAY="${{ steps.meta.outputs.display_version }}"
|
||||||
|
echo "## Update Server" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Version | \`${DISPLAY}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: Gitea.Workflow
|
|
||||||
# INGROUP: mokocli.Universal
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
|
||||||
# PATH: /.mokogitea/workflows/workflow-sync-trigger.yml
|
|
||||||
# VERSION: 01.01.00
|
|
||||||
# BRIEF: Trigger workflow sync to live repos when a PR is merged to main
|
|
||||||
|
|
||||||
name: "Universal: Workflow Sync Trigger"
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [closed]
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
env:
|
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
sync:
|
|
||||||
name: Sync workflows to live repos
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: >-
|
|
||||||
github.event.pull_request.merged == true &&
|
|
||||||
!contains(github.event.pull_request.title, '[skip sync]')
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Determine platform from repo name
|
|
||||||
id: platform
|
|
||||||
run: |
|
|
||||||
REPO="${{ github.event.repository.name }}"
|
|
||||||
case "$REPO" in
|
|
||||||
Template-Joomla) PLATFORM="joomla" ;;
|
|
||||||
Template-Dolibarr) PLATFORM="dolibarr" ;;
|
|
||||||
Template-Go) PLATFORM="go" ;;
|
|
||||||
Template-MCP) PLATFORM="mcp" ;;
|
|
||||||
Template-Generic) PLATFORM="" ;;
|
|
||||||
*) PLATFORM="" ;;
|
|
||||||
esac
|
|
||||||
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "Platform: ${PLATFORM:-all}"
|
|
||||||
|
|
||||||
- name: Clone mokocli
|
|
||||||
env:
|
|
||||||
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
|
||||||
run: |
|
|
||||||
GITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}"
|
|
||||||
git clone --depth 1 "${GITEA_URL}/MokoConsulting/mokocli.git" /tmp/mokocli
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
cd /tmp/mokocli
|
|
||||||
composer install --no-dev --no-interaction --quiet 2>/dev/null || true
|
|
||||||
|
|
||||||
- name: Run workflow sync
|
|
||||||
env:
|
|
||||||
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
|
||||||
run: |
|
|
||||||
ARGS="--token ${MOKOGITEA_TOKEN}"
|
|
||||||
ARGS="${ARGS} --org ${{ vars.GITEA_ORG || github.repository_owner }}"
|
|
||||||
ARGS="${ARGS} --phase repos"
|
|
||||||
|
|
||||||
PLATFORM="${{ steps.platform.outputs.platform }}"
|
|
||||||
if [ -n "$PLATFORM" ]; then
|
|
||||||
ARGS="${ARGS} --platform-filter ${PLATFORM}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
php /tmp/mokocli/cli/workflow_sync.php ${ARGS}
|
|
||||||
+53
-15
@@ -8,27 +8,65 @@
|
|||||||
DEFGROUP: Joomla.Template.Site
|
DEFGROUP: Joomla.Template.Site
|
||||||
INGROUP: MokoOnyx.Documentation
|
INGROUP: MokoOnyx.Documentation
|
||||||
PATH: ./CHANGELOG.md
|
PATH: ./CHANGELOG.md
|
||||||
VERSION: 02.26.02
|
<<<<<<< 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.26.02)
|
# Changelog — MokoOnyx (VERSION: 02.17.00)
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
=======
|
||||||
|
VERSION: 02.17.00
|
||||||
|
BRIEF: Changelog file documenting version history of MokoOnyx
|
||||||
|
-->
|
||||||
|
|
||||||
|
# Changelog — MokoOnyx (VERSION: 02.17.00)
|
||||||
|
>>>>>>> origin/main
|
||||||
|
=======
|
||||||
|
VERSION: 02.17.00
|
||||||
|
BRIEF: Changelog file documenting version history of MokoOnyx
|
||||||
|
-->
|
||||||
|
|
||||||
|
# Changelog — MokoOnyx (VERSION: 02.17.00)
|
||||||
|
>>>>>>> origin/main
|
||||||
|
## [02.17.00] --- 2026-05-30
|
||||||
|
|
||||||
|
## [02.15.00] --- 2026-05-30
|
||||||
|
|
||||||
|
=======
|
||||||
|
VERSION: 02.17.00
|
||||||
|
BRIEF: Changelog file documenting version history of MokoOnyx
|
||||||
|
-->
|
||||||
|
|
||||||
|
# Changelog — MokoOnyx (VERSION: 02.17.00)
|
||||||
|
>>>>>>> origin/main
|
||||||
|
## [02.14.00] --- 2026-05-30
|
||||||
|
|
||||||
|
## [02.13.00] --- 2026-05-30
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- Collapsible floating social bar with toggle button and localStorage persistence
|
- Hero image (`hero.jpg`) to template images
|
||||||
- Bootstrap tooltips on all social icon links showing platform name on hover
|
- `.fa-solid`, `.fa-regular`, `.fa-brands`, `.fa-light` icon margin spacing
|
||||||
- URL validation on social platform fields (browser, Joomla server-side, and PHP layout logging)
|
- `.blog-item .item-image` fixed 250px height with object-fit cover
|
||||||
|
- Hide header on home page option (`hideHeaderHome` template param)
|
||||||
|
- Hide main menu on home page option (`hideMenuHome` template param)
|
||||||
|
- Three distinct menu overrides: mainmenu (collapsible), horizontal (always visible), default (vertical)
|
||||||
|
- `<php_minimum>8.1.0</php_minimum>` to templateDetails.xml
|
||||||
|
- `<changelogurl>` support in updates.xml
|
||||||
|
- CONTRIBUTING.md with universal workflow and version policy
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Release pipeline rework: independent update streams, CLI-driven workflows
|
||||||
|
- Version bumps only trigger on `src/` changes (not docs/config)
|
||||||
|
- Branch protection: CI bot only for push, force push disabled
|
||||||
|
- Auto-bump supports dev, rc, feature/*, patch/* branches
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Moved footer social icons below footer-menu and footer module positions for correct display order
|
- Joomla update loop caused by version mismatch between ZIP and updates.xml
|
||||||
|
- Duplicate hamburger menu on mobile
|
||||||
|
- Stacked version suffixes (-dev-dev-dev)
|
||||||
|
- Template name doubling (Template - Template - MokoOnyx)
|
||||||
|
|
||||||
## [02.26.00] --- 2026-06-20
|
## [02.08.00] --- 2026-05-29
|
||||||
|
|
||||||
## [02.25.00] --- 2026-06-20
|
|
||||||
|
|
||||||
## [02.25.00] --- 2026-06-20
|
|
||||||
|
|
||||||
## [02.24.00] --- 2026-06-20
|
|
||||||
|
|
||||||
## [02.24.00] --- 2026-06-20
|
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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
@@ -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.26.02
|
<<<<<<< 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; ?>
|
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
+11
-1
@@ -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.26.02
|
<<<<<<< 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
-1
@@ -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.26.02
|
<<<<<<< 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
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<!DOCTYPE html><title></title>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<!DOCTYPE html><title></title>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<!DOCTYPE html><title></title>
|
||||||
@@ -33,8 +33,6 @@ if ($item->anchor_rel) {
|
|||||||
$linktype = $item->title;
|
$linktype = $item->title;
|
||||||
|
|
||||||
if ($item->menu_icon) {
|
if ($item->menu_icon) {
|
||||||
// Strip Joomla-injected padding classes that conflict with FA icon sizing
|
|
||||||
$item->menu_icon = trim(preg_replace('/\bp-[0-5]\b/', '', $item->menu_icon));
|
|
||||||
if ($itemParams->get('menu_text', 1)) {
|
if ($itemParams->get('menu_text', 1)) {
|
||||||
$linktype = '<span class="' . $item->menu_icon . '" aria-hidden="true"></span>' . $item->title;
|
$linktype = '<span class="' . $item->menu_icon . '" aria-hidden="true"></span>' . $item->title;
|
||||||
} else {
|
} else {
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user