Merge branch 'dev' into version/03

# Conflicts:
#	README.md
This commit is contained in:
2026-04-07 23:15:32 -05:00
52 changed files with 481 additions and 218 deletions

26
.github/CLAUDE.md vendored
View File

@@ -122,13 +122,13 @@ BRIEF: One-line description
### Joomla Version Alignment
The version in `README.md` **must always match** the `<version>` tag in `manifest.xml` and the latest entry in `update.xml`. The `make release` command / release workflow updates all three automatically.
The version in `README.md` **must always match** the `<version>` tag in `manifest.xml` and the latest entry in `updates.xml`. The `make release` command / release workflow updates all three automatically.
```xml
<!-- In manifest.xml — must match README.md version -->
<version>01.02.04</version>
<!-- In update.xml — prepend a new <update> block for every release.
<!-- In updates.xml — prepend a new <update> block for every release.
Note: the backslash in version="4\.[0-9]+" is a literal backslash character
in the XML attribute value. Joomla's update server treats the value as a
regular expression, so \. matches a literal dot. -->
@@ -154,7 +154,7 @@ The version in `README.md` **must always match** the `<version>` tag in `manifes
```
MokoCassiopeia/
├── manifest.xml # Joomla installer manifest (root — required)
├── update.xml # Update server manifest (root — required, see below)
├── updates.xml # Update server manifest (root — required, see below)
├── site/ # Frontend (site) code
│ ├── controller.php
│ ├── controllers/
@@ -183,22 +183,22 @@ MokoCassiopeia/
---
## update.xml — Required in Repo Root
## updates.xml — Required in Repo Root
`update.xml` **must exist at the repository root**. It is the Joomla update server manifest that allows Joomla installations to check for new versions of this extension.
`updates.xml` **must exist at the repository root**. It is the Joomla update server manifest that allows Joomla installations to check for new versions of this extension.
The `manifest.xml` must reference it via:
```xml
<updateservers>
<server type="extension" priority="1" name="{{EXTENSION_NAME}}">
https://github.com/mokoconsulting-tech/MokoCassiopeia/raw/main/update.xml
https://github.com/mokoconsulting-tech/MokoCassiopeia/raw/main/updates.xml
</server>
</updateservers>
```
**Rules:**
- Every release must prepend a new `<update>` block at the top of `update.xml` — old entries must be preserved below.
- The `<version>` in `update.xml` must exactly match `<version>` in `manifest.xml` and the version in `README.md`.
- Every release must prepend a new `<update>` block at the top of `updates.xml` — old entries must be preserved below.
- The `<version>` in `updates.xml` must exactly match `<version>` in `manifest.xml` and the version in `README.md`.
- The `<downloadurl>` must be a publicly accessible direct download link (GitHub Releases asset URL).
- `<targetplatform name="joomla" version="4\.[0-9]+">` — the backslash is a **literal backslash character** in the XML attribute value; Joomla's update-server parser treats the value as a regular expression, so `\.` matches a literal dot and `[0-9]+` matches one or more digits. Do not double-escape it.
@@ -207,8 +207,8 @@ The `manifest.xml` must reference it via:
## manifest.xml Rules
- Lives at the repo root as `manifest.xml` (not inside `site/` or `admin/`).
- `<version>` tag must be kept in sync with `README.md` version and `update.xml`.
- Must include `<updateservers>` block pointing to this repo's `update.xml`.
- `<version>` tag must be kept in sync with `README.md` version and `updates.xml`.
- Must include `<updateservers>` block pointing to this repo's `updates.xml`.
- Must include `<files folder="site">` and `<administration>` sections.
- Joomla 4.x requires `<namespace path="src">Moko\{{EXTENSION_NAME}}</namespace>` for namespaced extensions.
@@ -286,8 +286,8 @@ Approved prefixes: `dev/` · `rc/` · `version/` · `patch/` · `copilot/` · `d
| Change type | Documentation to update |
|-------------|------------------------|
| New or renamed PHP class/method | PHPDoc block; `docs/api/` entry |
| New or changed manifest.xml | Update `update.xml` version; bump README.md version |
| New release | Prepend `<update>` block to `update.xml`; update CHANGELOG.md; bump README.md version |
| New or changed manifest.xml | Update `updates.xml` version; bump README.md version |
| New release | Prepend `<update>` block to `updates.xml`; update CHANGELOG.md; bump README.md version |
| New or changed workflow | `docs/workflows/<workflow-name>.md` |
| Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block |
| **Every PR** | **Bump the patch version** — increment `XX.YY.ZZ` in `README.md`; `sync-version-on-merge` propagates it |
@@ -301,4 +301,4 @@ Approved prefixes: `dev/` · `rc/` · `version/` · `patch/` · `copilot/` · `d
- Never add `defined('_JEXEC') or die;` to CLI scripts or model tests — only to web-accessible PHP files
- Never hardcode version numbers in body text — update `README.md` and let automation propagate
- Never use `github.token` or `secrets.GITHUB_TOKEN` in workflows — always use `secrets.GH_TOKEN`
- Never let `manifest.xml` version, `update.xml` version, and `README.md` version go out of sync
- Never let `manifest.xml` version, `updates.xml` version, and `README.md` version go out of sync

24
.github/CODEOWNERS vendored
View File

@@ -8,8 +8,26 @@
# Combined with branch protection (require PR reviews), this prevents
# unauthorized modifications to workflows, configs, and governance files.
# ── Workflows (synced from MokoStandards — must not be manually edited) ──
/.github/workflows/ @jmiller-moko
# ── Synced workflows (managed by MokoStandards — do not edit manually) ────
/.github/workflows/deploy-dev.yml @jmiller-moko
/.github/workflows/deploy-demo.yml @jmiller-moko
/.github/workflows/deploy-rs.yml @jmiller-moko
/.github/workflows/auto-release.yml @jmiller-moko
/.github/workflows/auto-dev-issue.yml @jmiller-moko
/.github/workflows/auto-assign.yml @jmiller-moko
/.github/workflows/sync-version-on-merge.yml @jmiller-moko
/.github/workflows/enterprise-firewall-setup.yml @jmiller-moko
/.github/workflows/repository-cleanup.yml @jmiller-moko
/.github/workflows/standards-compliance.yml @jmiller-moko
/.github/workflows/codeql-analysis.yml @jmiller-moko
/.github/workflows/repo_health.yml @jmiller-moko
/.github/workflows/ci-joomla.yml @jmiller-moko
/.github/workflows/update-server.yml @jmiller-moko
/.github/workflows/deploy-manual.yml @jmiller-moko
/.github/workflows/ci-dolibarr.yml @jmiller-moko
/.github/workflows/publish-to-mokodolimods.yml @jmiller-moko
/.github/workflows/changelog-validation.yml @jmiller-moko
# Custom workflows in .github/workflows/ not listed above are repo-owned.
# ── GitHub configuration ─────────────────────────────────────────────────
/.github/ISSUE_TEMPLATE/ @jmiller-moko
@@ -23,7 +41,7 @@
/composer.json @jmiller-moko
/phpstan.neon @jmiller-moko
/Makefile @jmiller-moko
/.ftp_ignore @jmiller-moko
/.ftpignore @jmiller-moko
/.gitignore @jmiller-moko
/.gitattributes @jmiller-moko
/.editorconfig @jmiller-moko

View File

@@ -122,13 +122,13 @@ BRIEF: One-line description
### Joomla Version Alignment
The version in `README.md` **must always match** the `<version>` tag in `manifest.xml` and the latest entry in `update.xml`. The `make release` command / release workflow updates all three automatically.
The version in `README.md` **must always match** the `<version>` tag in `manifest.xml` and the latest entry in `updates.xml`. The `make release` command / release workflow updates all three automatically.
```xml
<!-- In manifest.xml — must match README.md version -->
<version>01.02.04</version>
<!-- In update.xml — prepend a new <update> block for every release.
<!-- In updates.xml — prepend a new <update> block for every release.
Note: the backslash in version="4\.[0-9]+" is a literal backslash character
in the XML attribute value. Joomla's update server treats the value as a
regular expression, so \. matches a literal dot. -->
@@ -154,7 +154,7 @@ The version in `README.md` **must always match** the `<version>` tag in `manifes
```
MokoCassiopeia/
├── manifest.xml # Joomla installer manifest (root — required)
├── update.xml # Update server manifest (root — required, see below)
├── updates.xml # Update server manifest (root — required, see below)
├── site/ # Frontend (site) code
│ ├── controller.php
│ ├── controllers/
@@ -183,22 +183,22 @@ MokoCassiopeia/
---
## update.xml — Required in Repo Root
## updates.xml — Required in Repo Root
`update.xml` **must exist at the repository root**. It is the Joomla update server manifest that allows Joomla installations to check for new versions of this extension.
`updates.xml` **must exist at the repository root**. It is the Joomla update server manifest that allows Joomla installations to check for new versions of this extension.
The `manifest.xml` must reference it via:
```xml
<updateservers>
<server type="extension" priority="1" name="{{EXTENSION_NAME}}">
https://github.com/mokoconsulting-tech/MokoCassiopeia/raw/main/update.xml
https://github.com/mokoconsulting-tech/MokoCassiopeia/raw/main/updates.xml
</server>
</updateservers>
```
**Rules:**
- Every release must prepend a new `<update>` block at the top of `update.xml` — old entries must be preserved below.
- The `<version>` in `update.xml` must exactly match `<version>` in `manifest.xml` and the version in `README.md`.
- Every release must prepend a new `<update>` block at the top of `updates.xml` — old entries must be preserved below.
- The `<version>` in `updates.xml` must exactly match `<version>` in `manifest.xml` and the version in `README.md`.
- The `<downloadurl>` must be a publicly accessible direct download link (GitHub Releases asset URL).
- `<targetplatform name="joomla" version="4\.[0-9]+">` — the backslash is a **literal backslash character** in the XML attribute value; Joomla's update-server parser treats the value as a regular expression, so `\.` matches a literal dot and `[0-9]+` matches one or more digits. Do not double-escape it.
@@ -207,8 +207,8 @@ The `manifest.xml` must reference it via:
## manifest.xml Rules
- Lives at the repo root as `manifest.xml` (not inside `site/` or `admin/`).
- `<version>` tag must be kept in sync with `README.md` version and `update.xml`.
- Must include `<updateservers>` block pointing to this repo's `update.xml`.
- `<version>` tag must be kept in sync with `README.md` version and `updates.xml`.
- Must include `<updateservers>` block pointing to this repo's `updates.xml`.
- Must include `<files folder="site">` and `<administration>` sections.
- Joomla 4.x requires `<namespace path="src">Moko\{{EXTENSION_NAME}}</namespace>` for namespaced extensions.
@@ -286,8 +286,8 @@ Approved prefixes: `dev/` · `rc/` · `version/` · `patch/` · `copilot/` · `d
| Change type | Documentation to update |
|-------------|------------------------|
| New or renamed PHP class/method | PHPDoc block; `docs/api/` entry |
| New or changed manifest.xml | Update `update.xml` version; bump README.md version |
| New release | Prepend `<update>` block to `update.xml`; update CHANGELOG.md; bump README.md version |
| New or changed manifest.xml | Update `updates.xml` version; bump README.md version |
| New release | Prepend `<update>` block to `updates.xml`; update CHANGELOG.md; bump README.md version |
| New or changed workflow | `docs/workflows/<workflow-name>.md` |
| Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block |
| **Every PR** | **Bump the patch version** — increment `XX.YY.ZZ` in `README.md`; `sync-version-on-merge` propagates it |
@@ -301,4 +301,4 @@ Approved prefixes: `dev/` · `rc/` · `version/` · `patch/` · `copilot/` · `d
- Never add `defined('_JEXEC') or die;` to CLI scripts or model tests — only to web-accessible PHP files
- Never hardcode version numbers in body text — update `README.md` and let automation propagate
- Never use `github.token` or `secrets.GITHUB_TOKEN` in workflows — always use `secrets.GH_TOKEN`
- Never let `manifest.xml` version, `update.xml` version, and `README.md` version go out of sync
- Never let `manifest.xml` version, `updates.xml` version, and `README.md` version go out of sync

View File

@@ -5,7 +5,7 @@
# INGROUP: MokoStandards.Security
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /.github/dependabot.yml
# VERSION: 01.00.00
# VERSION: 03.09.03
# BRIEF: Dependabot configuration for automated dependency updates and security patches
# NOTE: Monitors GitHub Actions for vulnerabilities and keeps ecosystem secure

View File

@@ -6,7 +6,7 @@
# INGROUP: MokoStandards.Workflows.Shared
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /.github/workflows/auto-assign.yml
# VERSION: 04.05.11
# VERSION: 04.06.00
# BRIEF: Auto-assign jmiller-moko to unassigned issues and PRs every 15 minutes
name: Auto-Assign Issues & PRs

View File

@@ -9,7 +9,7 @@
# INGROUP: MokoStandards.Automation
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /templates/workflows/shared/auto-dev-issue.yml.template
# VERSION: 04.05.13
# VERSION: 04.06.00
# BRIEF: Auto-create tracking issue with sub-issues for dev/rc branch workflow
# NOTE: Synced via bulk-repo-sync to .github/workflows/auto-dev-issue.yml in all governed repos.
@@ -39,7 +39,10 @@ jobs:
runs-on: ubuntu-latest
if: >-
(github.event_name == 'workflow_dispatch') ||
(github.event.ref_type == 'branch' && startsWith(github.event.ref, 'rc/'))
(github.event.ref_type == 'branch' &&
(startsWith(github.event.ref, 'rc/') ||
startsWith(github.event.ref, 'alpha/') ||
startsWith(github.event.ref, 'beta/')))
steps:
- name: Create tracking issue and sub-issues
@@ -62,6 +65,16 @@ jobs:
BRANCH_TYPE="Release Candidate"
LABEL_TYPE="type: release"
TITLE_PREFIX="rc"
elif [[ "$BRANCH" == beta/* ]]; then
VERSION="${BRANCH#beta/}"
BRANCH_TYPE="Beta"
LABEL_TYPE="type: release"
TITLE_PREFIX="beta"
elif [[ "$BRANCH" == alpha/* ]]; then
VERSION="${BRANCH#alpha/}"
BRANCH_TYPE="Alpha"
LABEL_TYPE="type: release"
TITLE_PREFIX="alpha"
else
VERSION="${BRANCH#dev/}"
BRANCH_TYPE="Development"
@@ -80,14 +93,20 @@ jobs:
exit 0
fi
# ── Define sub-issues for the dev workflow ────────────────────────
# ── Define sub-issues for the workflow ────────────────────────
if [[ "$BRANCH" == rc/* ]]; then
SUB_ISSUES=(
"RC Testing|Verify all features work on rc branch|type: test,release-candidate"
"Regression Testing|Run full regression suite before merge to main|type: test,release-candidate"
"Regression Testing|Run full regression suite before merge|type: test,release-candidate"
"Version Bump|Bump version in README.md and all headers|type: version,release-candidate"
"Changelog Update|Update CHANGELOG.md with release notes|documentation,release-candidate"
"Merge to Main|Create PR from rc branch to main|type: release,needs-review"
"Merge to Version Branch|Create PR to version/XX|type: release,needs-review"
)
elif [[ "$BRANCH" == alpha/* ]] || [[ "$BRANCH" == beta/* ]]; then
SUB_ISSUES=(
"Testing|Verify features on ${BRANCH_TYPE} branch|type: test,status: in-progress"
"Bug Fixes|Fix issues found during ${BRANCH_TYPE} testing|type: bug,status: pending"
"Promote to Next Stage|Create PR to promote to next release stage|type: release,needs-review"
)
else
SUB_ISSUES=(
@@ -156,22 +175,26 @@ jobs:
done
fi
# ── RC: Create or update release-candidate release ──────────────
if [[ "$BRANCH" == rc/* ]]; then
RELEASE_TAG="release-candidate"
EXISTING=$(gh release view "$RELEASE_TAG" --json tagName -q .tagName 2>/dev/null || true)
# ── Create or update prerelease for alpha/beta/rc ────────────────
if [[ "$BRANCH" == rc/* ]] || [[ "$BRANCH" == alpha/* ]] || [[ "$BRANCH" == beta/* ]]; then
case "$BRANCH_TYPE" in
Alpha) RELEASE_TAG="alpha" ;;
Beta) RELEASE_TAG="beta" ;;
"Release Candidate") RELEASE_TAG="release-candidate" ;;
esac
EXISTING=$(gh release view "$RELEASE_TAG" --json tagName -q .tagName 2>/dev/null || true)
if [ -z "$EXISTING" ]; then
gh release create "$RELEASE_TAG" \
--title "release-candidate (${VERSION})" \
--notes "## Release Candidate ${VERSION}\n\nRC branch: \`${BRANCH}\`\nTracking issue: ${PARENT_URL}" \
--title "${RELEASE_TAG} (${VERSION})" \
--notes "## ${BRANCH_TYPE} ${VERSION}\n\nBranch: \`${BRANCH}\`\nTracking issue: ${PARENT_URL}" \
--prerelease \
--target main 2>/dev/null || true
echo "RC release created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
echo "${BRANCH_TYPE} release created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
else
gh release edit "$RELEASE_TAG" \
--title "release-candidate (${VERSION})" --prerelease 2>/dev/null || true
echo "RC release updated: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
--title "${RELEASE_TAG} (${VERSION})" --prerelease 2>/dev/null || true
echo "${BRANCH_TYPE} release updated: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
fi
fi

View File

@@ -7,7 +7,7 @@
# INGROUP: MokoStandards.Release
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /templates/workflows/joomla/auto-release.yml.template
# VERSION: 04.05.13
# VERSION: 04.06.00
# BRIEF: Joomla build & release — ZIP package, updates.xml, SHA-256 checksum
#
# +========================================================================+
@@ -316,54 +316,47 @@ jobs:
DOWNLOAD_URL="https://github.com/${REPO}/releases/download/v${VERSION}/${EXT_ELEMENT}-${VERSION}.zip"
INFO_URL="https://github.com/${REPO}/releases/tag/v${VERSION}"
# -- Build stable entry ──────────────────────────────────────
STABLE_ENTRY=$(cat <<XMLEOF
<update>
<name>${EXT_NAME}</name>
<description>${EXT_NAME} update</description>
<element>${EXT_ELEMENT}</element>
<type>${EXT_TYPE}</type>
<version>${VERSION}</version>
$([ -n "$CLIENT_TAG" ] && echo " ${CLIENT_TAG}")
$([ -n "$FOLDER_TAG" ] && echo " ${FOLDER_TAG}")
<tags>
<tag>stable</tag>
</tags>
<infourl title="${EXT_NAME}">${INFO_URL}</infourl>
<downloads>
<downloadurl type="full" format="zip">${DOWNLOAD_URL}</downloadurl>
</downloads>
${TARGET_PLATFORM}
$([ -n "$PHP_TAG" ] && echo " ${PHP_TAG}")
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
</update>
XMLEOF
)
# -- Build stable entry to temp file ─────────────────────────
{
printf '%s\n' ' <update>'
printf '%s\n' " <name>${EXT_NAME}</name>"
printf '%s\n' " <description>${EXT_NAME} update</description>"
printf '%s\n' " <element>${EXT_ELEMENT}</element>"
printf '%s\n' " <type>${EXT_TYPE}</type>"
printf '%s\n' " <version>${VERSION}</version>"
[ -n "$CLIENT_TAG" ] && printf '%s\n' " ${CLIENT_TAG}"
[ -n "$FOLDER_TAG" ] && printf '%s\n' " ${FOLDER_TAG}"
printf '%s\n' ' <tags>'
printf '%s\n' ' <tag>stable</tag>'
printf '%s\n' ' </tags>'
printf '%s\n' " <infourl title=\"${EXT_NAME}\">${INFO_URL}</infourl>"
printf '%s\n' ' <downloads>'
printf '%s\n' " <downloadurl type=\"full\" format=\"zip\">${DOWNLOAD_URL}</downloadurl>"
printf '%s\n' ' </downloads>'
printf '%s\n' " ${TARGET_PLATFORM}"
[ -n "$PHP_TAG" ] && printf '%s\n' " ${PHP_TAG}"
printf '%s\n' ' <maintainer>Moko Consulting</maintainer>'
printf '%s\n' ' <maintainerurl>https://mokoconsulting.tech</maintainerurl>'
printf '%s\n' ' </update>'
} > /tmp/stable_entry.xml
# -- Write updates.xml preserving dev/rc entries ──────────────
# Extract existing dev and rc entries if present
RC_ENTRY=""
DEV_ENTRY=""
if [ -f "updates.xml" ]; then
RC_ENTRY=$(python3 -c "
import re
with open('updates.xml') as f: c = f.read()
m = re.search(r'( <update>.*?<tag>rc</tag>.*?</update>)', c, re.DOTALL)
if m: print(m.group(1))
" 2>/dev/null || true)
DEV_ENTRY=$(python3 -c "
import re
with open('updates.xml') as f: c = f.read()
m = re.search(r'( <update>.*?<tag>development</tag>.*?</update>)', c, re.DOTALL)
if m: print(m.group(1))
" 2>/dev/null || true)
printf 'import re\n' > /tmp/extract.py
printf 'with open("updates.xml") as f: c = f.read()\n' >> /tmp/extract.py
printf 'import sys; tag = sys.argv[1]\n' >> /tmp/extract.py
printf 'm = re.search(r"( <update>.*?<tag>" + re.escape(tag) + r"</tag>.*?</update>)", c, re.DOTALL)\n' >> /tmp/extract.py
printf 'if m: print(m.group(1))\n' >> /tmp/extract.py
RC_ENTRY=$(python3 /tmp/extract.py rc 2>/dev/null || true)
DEV_ENTRY=$(python3 /tmp/extract.py development 2>/dev/null || true)
fi
{
printf '%s\n' '<?xml version="1.0" encoding="utf-8"?>'
printf '%s\n' '<updates>'
echo "$STABLE_ENTRY"
cat /tmp/stable_entry.xml
[ -n "$RC_ENTRY" ] && echo "$RC_ENTRY"
[ -n "$DEV_ENTRY" ] && echo "$DEV_ENTRY"
printf '%s\n' '</updates>'
@@ -484,7 +477,7 @@ if m: print(m.group(1))
[ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ — skipping package"; exit 0; }
cd "$SOURCE_DIR"
zip -r "/tmp/${PACKAGE_NAME}" .
zip -r "/tmp/${PACKAGE_NAME}" . -x '.ftpignore'
cd ..
FILESIZE=$(stat -c%s "/tmp/${PACKAGE_NAME}" 2>/dev/null || stat -f%z "/tmp/${PACKAGE_NAME}" 2>/dev/null || echo "unknown")

View File

@@ -5,7 +5,7 @@
# INGROUP: MokoCassiopeia.Automation
# REPO: https://github.com/mokoconsulting-tech/MokoCassiopeia
# PATH: /.github/workflows/auto-update-sha.yml
# VERSION: 01.00.00
# VERSION: 03.09.03
# BRIEF: Automatically update SHA-256 hash in updates.xml after release
# NOTE: Ensures updates.xml stays synchronized with release packages

View File

@@ -9,7 +9,7 @@
# INGROUP: MokoStandards.CI
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /templates/workflows/shared/changelog-validation.yml.template
# VERSION: 04.05.13
# VERSION: 04.06.00
# BRIEF: Validates CHANGELOG.md format and version consistency
# NOTE: Deployed to .github/workflows/changelog-validation.yml in governed repos.

View File

@@ -9,7 +9,7 @@
# INGROUP: MokoStandards.CI
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /templates/workflows/joomla/ci-joomla.yml.template
# VERSION: 04.05.13
# VERSION: 04.06.00
# BRIEF: CI workflow for Joomla extensions — lint, validate, test
# NOTE: Deployed to .github/workflows/ci-joomla.yml in governed Joomla extension repos.

View File

@@ -9,7 +9,7 @@
# INGROUP: MokoStandards.Security
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /templates/workflows/generic/codeql-analysis.yml.template
# VERSION: 04.05.00
# VERSION: 03.09.03
# BRIEF: CodeQL security scanning workflow (generic — all repo types)
# NOTE: Deployed to .github/workflows/codeql-analysis.yml in governed repos.
# CodeQL does not support PHP directly; JavaScript scans JSON/YAML/shell.

132
.github/workflows/deploy-manual.yml vendored Normal file
View File

@@ -0,0 +1,132 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: GitHub.Workflow
# INGROUP: MokoStandards.Deploy
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /templates/workflows/joomla/deploy-manual.yml.template
# VERSION: 04.06.00
# BRIEF: Manual SFTP deploy to dev server for Joomla repos
# NOTE: Joomla repos use update.xml for distribution. This is for manual
# dev server testing only — triggered via workflow_dispatch.
name: 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
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0
with:
php-version: '8.2'
extensions: json, ssh2
tools: composer
coverage: none
- name: Setup MokoStandards tools
env:
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}'
run: |
git clone --depth 1 --branch version/04 --quiet \
"https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \
/tmp/mokostandards 2>/dev/null || true
if [ -d "/tmp/mokostandards" ] && [ -f "/tmp/mokostandards/composer.json" ]; then
cd /tmp/mokostandards && 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 }}
SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
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%/}"
[ -n "$SUFFIX" ] && REMOTE="${REMOTE}/${SUFFIX#/}"
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

View File

@@ -22,7 +22,7 @@
# INGROUP: MokoStandards.Firewall
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /templates/workflows/shared/enterprise-firewall-setup.yml.template
# VERSION: 04.05.13
# VERSION: 04.06.00
# BRIEF: Enterprise firewall configuration — generates outbound allow-rules including SFTP deployment server
# NOTE: Reads DEV_FTP_HOST / DEV_FTP_PORT variables to include SFTP egress rules alongside HTTPS rules.

View File

@@ -22,7 +22,7 @@
# INGROUP: MokoCassiopeia.Release
# REPO: https://github.com/mokoconsulting-tech/MokoCassiopeia
# PATH: /.github/workflows/release.yml
# VERSION: 01.00.00
# VERSION: 03.09.03
# BRIEF: Automated release workflow for MokoCassiopeia Joomla template
# NOTE: Creates release packages and publishes to GitHub Releases

View File

@@ -10,7 +10,7 @@
# INGROUP: MokoStandards.Validation
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /.github/workflows/repo_health.yml
# VERSION: 04.05.00
# VERSION: 04.06.00
# BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts.
# NOTE: Field is user-managed.
# ============================================================================

View File

@@ -9,7 +9,7 @@
# INGROUP: MokoStandards.Maintenance
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /templates/workflows/shared/repository-cleanup.yml.template
# VERSION: 04.05.13
# VERSION: 04.06.00
# BRIEF: Recurring repository maintenance — labels, branches, workflows, logs, doc indexes
# NOTE: Synced via bulk-repo-sync to .github/workflows/repository-cleanup.yml in all governed repos.
# Runs on the 1st and 15th of each month at 6:00 AM UTC, and on manual dispatch.

View File

@@ -5,7 +5,7 @@
# INGROUP: MokoStandards.Compliance
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /.github/workflows/standards-compliance.yml
# VERSION: 04.05.00
# VERSION: 04.06.00
# BRIEF: MokoStandards compliance validation workflow
# NOTE: Validates repository structure, documentation, and coding standards

View File

@@ -9,7 +9,7 @@
# INGROUP: MokoStandards.Automation
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /templates/workflows/shared/sync-version-on-merge.yml.template
# VERSION: 04.05.13
# VERSION: 04.06.00
# BRIEF: Auto-bump patch version on every push to main and propagate to all file headers
# NOTE: Synced via bulk-repo-sync to .github/workflows/sync-version-on-merge.yml in all governed repos.
# README.md is the single source of truth for the repository version.

View File

@@ -7,7 +7,7 @@
# INGROUP: MokoStandards.Joomla
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /templates/workflows/joomla/update-server.yml.template
# VERSION: 04.05.13
# VERSION: 04.06.00
# BRIEF: Update Joomla update server XML feed with stable/rc/dev entries
#
# Writes updates.xml with multiple <update> entries:
@@ -23,6 +23,8 @@ on:
push:
branches:
- 'dev/**'
- 'alpha/**'
- 'beta/**'
- 'rc/**'
paths:
- 'src/**'
@@ -30,12 +32,14 @@ on:
workflow_dispatch:
inputs:
stability:
description: 'Stability tag (development, rc, stable)'
description: 'Stability tag'
required: true
default: 'development'
type: choice
options:
- development
- alpha
- beta
- rc
- stable
@@ -75,11 +79,29 @@ jobs:
REPO="${{ github.repository }}"
VERSION=$(php /tmp/mokostandards/api/cli/version_read.php --path . 2>/dev/null || echo "0.0.0")
# Auto-bump patch on alpha/beta/rc branches (not dev — dev bumps manually)
if [[ "$BRANCH" != dev/* ]]; then
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
BUMPED=$(php /tmp/mokostandards/api/cli/version_bump.php --path . 2>/dev/null || true)
if [ -n "$BUMPED" ]; then
VERSION=$(php /tmp/mokostandards/api/cli/version_read.php --path . 2>/dev/null || echo "$VERSION")
git add -A
git commit -m "chore(version): auto-bump patch ${VERSION} [skip ci]" \
--author="github-actions[bot] <github-actions[bot]@users.noreply.github.com>" 2>/dev/null || true
git push 2>/dev/null || true
fi
fi
# Determine stability from branch or 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"
elif [[ "$BRANCH" == dev/* ]]; then
STABILITY="development"
else
@@ -116,19 +138,23 @@ jobs:
# Version suffix for non-stable
DISPLAY_VERSION="$VERSION"
[ "$STABILITY" = "rc" ] && DISPLAY_VERSION="${VERSION}-rc"
[ "$STABILITY" = "development" ] && DISPLAY_VERSION="${VERSION}-dev"
case "$STABILITY" in
development) DISPLAY_VERSION="${VERSION}-dev" ;;
alpha) DISPLAY_VERSION="${VERSION}-alpha" ;;
beta) DISPLAY_VERSION="${VERSION}-beta" ;;
rc) DISPLAY_VERSION="${VERSION}-rc" ;;
esac
MAJOR=$(echo "$VERSION" | awk -F. '{print $1}')
# Each stability level has its own release tag
if [ "$STABILITY" = "rc" ]; then
RELEASE_TAG="release-candidate"
elif [ "$STABILITY" = "development" ]; then
RELEASE_TAG="development"
else
RELEASE_TAG="v${MAJOR}"
fi
case "$STABILITY" in
development) RELEASE_TAG="development" ;;
alpha) RELEASE_TAG="alpha" ;;
beta) RELEASE_TAG="beta" ;;
rc) RELEASE_TAG="release-candidate" ;;
*) RELEASE_TAG="v${MAJOR}" ;;
esac
PACKAGE_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.zip"
DOWNLOAD_URL="https://github.com/${REPO}/releases/download/${RELEASE_TAG}/${PACKAGE_NAME}"
@@ -139,7 +165,7 @@ jobs:
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
if [ -d "$SOURCE_DIR" ]; then
cd "$SOURCE_DIR"
zip -r "/tmp/${PACKAGE_NAME}" .
zip -r "/tmp/${PACKAGE_NAME}" . -x '.ftpignore'
cd ..
SHA256=$(sha256sum "/tmp/${PACKAGE_NAME}" | cut -d' ' -f1)
@@ -157,95 +183,63 @@ jobs:
fi
# ── Build the new entry ───────────────────────────────────────
NEW_ENTRY=$(cat <<XMLEOF
<update>
<name>${EXT_NAME}</name>
<description>${EXT_NAME} (${STABILITY})</description>
<element>${EXT_ELEMENT}</element>
<type>${EXT_TYPE}</type>
<version>${DISPLAY_VERSION}</version>
$([ -n "$CLIENT_TAG" ] && echo " ${CLIENT_TAG}")
$([ -n "$FOLDER_TAG" ] && echo " ${FOLDER_TAG}")
<tags>
<tag>${STABILITY}</tag>
</tags>
<infourl title="${EXT_NAME}">${INFO_URL}</infourl>
<downloads>
<downloadurl type="full" format="zip">${DOWNLOAD_URL}</downloadurl>
</downloads>
$([ -n "$SHA256" ] && echo " <sha256>sha256:${SHA256}</sha256>")
${TARGET_PLATFORM}
$([ -n "$PHP_TAG" ] && echo " ${PHP_TAG}")
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
</update>
XMLEOF
)
NEW_ENTRY=""
NEW_ENTRY="${NEW_ENTRY} <update>\n"
NEW_ENTRY="${NEW_ENTRY} <name>${EXT_NAME}</name>\n"
NEW_ENTRY="${NEW_ENTRY} <description>${EXT_NAME} (${STABILITY})</description>\n"
NEW_ENTRY="${NEW_ENTRY} <element>${EXT_ELEMENT}</element>\n"
NEW_ENTRY="${NEW_ENTRY} <type>${EXT_TYPE}</type>\n"
NEW_ENTRY="${NEW_ENTRY} <version>${DISPLAY_VERSION}</version>\n"
[ -n "$CLIENT_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${CLIENT_TAG}\n"
[ -n "$FOLDER_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${FOLDER_TAG}\n"
NEW_ENTRY="${NEW_ENTRY} <tags>\n"
NEW_ENTRY="${NEW_ENTRY} <tag>${STABILITY}</tag>\n"
NEW_ENTRY="${NEW_ENTRY} </tags>\n"
NEW_ENTRY="${NEW_ENTRY} <infourl title=\"${EXT_NAME}\">${INFO_URL}</infourl>\n"
NEW_ENTRY="${NEW_ENTRY} <downloads>\n"
NEW_ENTRY="${NEW_ENTRY} <downloadurl type=\"full\" format=\"zip\">${DOWNLOAD_URL}</downloadurl>\n"
NEW_ENTRY="${NEW_ENTRY} </downloads>\n"
[ -n "$SHA256" ] && NEW_ENTRY="${NEW_ENTRY} <sha256>sha256:${SHA256}</sha256>\n"
NEW_ENTRY="${NEW_ENTRY} ${TARGET_PLATFORM}\n"
[ -n "$PHP_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${PHP_TAG}\n"
NEW_ENTRY="${NEW_ENTRY} <maintainer>Moko Consulting</maintainer>\n"
NEW_ENTRY="${NEW_ENTRY} <maintainerurl>https://mokoconsulting.tech</maintainerurl>\n"
NEW_ENTRY="${NEW_ENTRY} </update>"
# ── Write new entry to temp file ───────────────────────────────
printf '%b' "$NEW_ENTRY" > /tmp/new_entry.xml
# ── Merge into updates.xml ─────────────────────────────────────
if [ ! -f "updates.xml" ]; then
# Create fresh
printf '%s\n' '<?xml version="1.0" encoding="utf-8"?>' > updates.xml
printf '%s\n' '<updates>' >> updates.xml
echo "$NEW_ENTRY" >> updates.xml
printf '%s\n' '</updates>' >> updates.xml
cat /tmp/new_entry.xml >> updates.xml
printf '\n%s\n' '</updates>' >> updates.xml
else
# Remove existing entry for this stability, add new one
# Use python for reliable XML manipulation
python3 -c "
import re, sys
with open('updates.xml', 'r') as f:
content = f.read()
# Remove existing entry with this stability tag
pattern = r' <update>.*?<tag>${STABILITY}</tag>.*?</update>\n?'
content = re.sub(pattern, '', content, flags=re.DOTALL)
# Insert new entry before </updates>
new_entry = '''${NEW_ENTRY}'''
content = content.replace('</updates>', new_entry + '\n</updates>')
# Clean up empty lines
content = re.sub(r'\n{3,}', '\n\n', content)
with open('updates.xml', 'w') as f:
f.write(content)
" 2>/dev/null || {
# Fallback: just rewrite the whole file if python fails
# Keep existing stable entry if present
STABLE_ENTRY=""
if [ "$STABILITY" != "stable" ] && grep -q '<tag>stable</tag>' updates.xml; then
STABLE_ENTRY=$(sed -n '/<update>/,/<\/update>/{ /<tag>stable<\/tag>/,/<\/update>/p; /<update>/,/<tag>stable<\/tag>/p }' updates.xml | sort -u)
fi
RC_ENTRY=""
if [ "$STABILITY" != "rc" ] && grep -q '<tag>rc</tag>' updates.xml; then
RC_ENTRY=$(python3 -c "
import re
with open('updates.xml') as f: c = f.read()
m = re.search(r'(<update>.*?<tag>rc</tag>.*?</update>)', c, re.DOTALL)
if m: print(m.group(1))
" 2>/dev/null || true)
fi
DEV_ENTRY=""
if [ "$STABILITY" != "development" ] && grep -q '<tag>development</tag>' updates.xml; then
DEV_ENTRY=$(python3 -c "
import re
with open('updates.xml') as f: c = f.read()
m = re.search(r'(<update>.*?<tag>development</tag>.*?</update>)', c, re.DOTALL)
if m: print(m.group(1))
" 2>/dev/null || true)
fi
# Remove existing entry for this stability, insert new one
printf 'import re\nstability = "%s"\n' "${STABILITY}" > /tmp/merge_xml.py
printf 'with open("updates.xml") as f: content = f.read()\n' >> /tmp/merge_xml.py
printf 'with open("/tmp/new_entry.xml") as f: new_entry = f.read()\n' >> /tmp/merge_xml.py
printf 'pattern = r" <update>.*?<tag>" + re.escape(stability) + r"</tag>.*?</update>\\n?"\n' >> /tmp/merge_xml.py
printf 'content = re.sub(pattern, "", content, flags=re.DOTALL)\n' >> /tmp/merge_xml.py
printf 'content = content.replace("</updates>", new_entry + "\\n</updates>")\n' >> /tmp/merge_xml.py
printf 'content = re.sub(r"\\n{3,}", "\\n\\n", content)\n' >> /tmp/merge_xml.py
printf 'with open("updates.xml", "w") as f: f.write(content)\n' >> /tmp/merge_xml.py
python3 /tmp/merge_xml.py 2>/dev/null || {
# Fallback: rebuild keeping other stability entries
{
printf '%s\n' '<?xml version="1.0" encoding="utf-8"?>'
printf '%s\n' '<updates>'
[ -n "$STABLE_ENTRY" ] && echo "$STABLE_ENTRY"
[ -n "$RC_ENTRY" ] && echo "$RC_ENTRY"
[ -n "$DEV_ENTRY" ] && echo "$DEV_ENTRY"
echo "$NEW_ENTRY"
printf '%s\n' '</updates>'
} > updates.xml
for TAG in stable rc development; do
[ "$TAG" = "${STABILITY}" ] && continue
if grep -q "<tag>${TAG}</tag>" updates.xml 2>/dev/null; then
sed -n "/<update>/,/<\/update>/{ /<tag>${TAG}<\/tag>/p; }" updates.xml
fi
done
cat /tmp/new_entry.xml
printf '\n%s\n' '</updates>'
} > /tmp/updates_new.xml
mv /tmp/updates_new.xml updates.xml
}
fi
@@ -259,6 +253,64 @@ if m: print(m.group(1))
git push
}
- name: SFTP deploy to dev server
if: contains(github.ref, '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 }}
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
run: |
# ── Permission check: admin or maintain role required ──────
ACTOR="${{ github.actor }}"
REPO="${{ github.repository }}"
PERMISSION=$(gh api "repos/${REPO}/collaborators/${ACTOR}/permission" \
--jq '.permission' 2>/dev/null || \
gh api "repos/${REPO}/collaborators/${ACTOR}" \
--jq '.role' 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 /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 --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
elif [ -f "/tmp/mokostandards/api/deploy/deploy-sftp.php" ]; then
php /tmp/mokostandards/api/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: |
echo "## Joomla Update Server" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY

View File

@@ -13,20 +13,14 @@
BRIEF: Documentation for MokoCassiopeia template
-->
[![Version](https://img.shields.io/badge/version-03.09.03-blue.svg?logo=v&logoColor=white)](https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/tag/v03)
[![License](https://img.shields.io/badge/license-GPL--3.0--or--later-green.svg?logo=gnu&logoColor=white)](LICENSE)
[![Joomla](https://img.shields.io/badge/Joomla-5.x%20%7C%206.x-red.svg?logo=joomla&logoColor=white)](https://www.joomla.org)
[![PHP](https://img.shields.io/badge/PHP-8.1%2B-777BB4.svg?logo=php&logoColor=white)](https://www.php.net)
# MokoCassiopeia
# MokoCassiopeia Template
**A Modern, Lightweight Joomla Template Based on Cassiopeia**
](https://img.shields.io/badge/version-03.09.04-green.svg)](https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/tag/v03)
: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
](https://img.shields.io/badge/Joomla-5.x%20%7C%206.x-blue.svg)](https://www.joomla.org)
](https://img.shields.io/badge/PHP-8.1%2B-blue.svg)](https://www.php.net)
[![Version](https://img.shields.io/badge/version-03.09.07-blue.svg?logo=v&logoColor=white)](https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/tag/v03)
[![License](https://img.shields.io/badge/license-GPL--3.0--or--later-green.svg?logo=gnu&logoColor=white)](LICENSE)
[![Joomla](https://img.shields.io/badge/Joomla-5.x%20%7C%206.x-red.svg?logo=joomla&logoColor=white)](https://www.joomla.org)
[![PHP](https://img.shields.io/badge/PHP-8.1%2B-777BB4.svg?logo=php&logoColor=white)](https://www.php.net)
MokoCassiopeia is a modern, lightweight enhancement layer built on top of Joomla's Cassiopeia template. It adds **Font Awesome 7**, **Bootstrap 5** helpers, an automatic **Table of Contents (TOC)** utility, advanced **Dark Mode** theming, and optional integrations for **Google Tag Manager** and **Google Analytics (GA4)**—all while maintaining minimal core template overrides for maximum upgrade compatibility.

View File

@@ -10,31 +10,62 @@
/**
* Default layout override for mod_breadcrumbs.
* Bootstrap 5 breadcrumb with schema.org BreadcrumbList markup.
* Respects showHome, showLast, homeText module settings.
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Uri\Uri;
Factory::getApplication()->getLanguage()->load('mod_breadcrumbs', JPATH_SITE);
$suffix = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
$headerTag = htmlspecialchars($params->get('header_tag', 'h3'), ENT_COMPAT, 'UTF-8');
$headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'UTF-8');
$showHome = $params->get('showHome', 1);
$showLast = $params->get('showLast', 1);
$homeText = $params->get('homeText', '') ?: Text::_('MOD_BREADCRUMBS_HOME');
// Build filtered list respecting module settings
$items = [];
$count = count($list);
foreach ($list as $key => $item) {
// Skip Home item if showHome is off
if ($key === 0 && !$showHome) {
continue;
}
// Replace Home text if custom homeText is set
if ($key === 0 && $showHome) {
$item->name = $homeText;
}
// Skip last item if showLast is off
if ($key === $count - 1 && !$showLast) {
continue;
}
$items[] = $item;
}
if (empty($items)) {
return;
}
?>
<nav class="mod-breadcrumbs<?php echo $suffix ? ' ' . $suffix : ''; ?>" aria-label="<?php echo Text::_('MOD_BREADCRUMBS_HERE'); ?>">
<?php if ($module->showtitle) : ?>
<<?php echo $headerTag; ?> class="mod-breadcrumbs__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
<?php endif; ?>
<ol class="breadcrumb" itemscope itemtype="https://schema.org/BreadcrumbList">
<?php foreach ($list as $key => $item) : ?>
<?php
$isLast = ($key === array_key_last($list));
?>
<?php foreach ($items as $key => $item) : ?>
<?php $isLast = ($key === array_key_last($items)); ?>
<li class="breadcrumb-item<?php echo $isLast ? ' active' : ''; ?>" itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem"
<?php echo $isLast ? ' aria-current="page"' : ''; ?>>
<?php if (!$isLast && $item->link) : ?>
<?php if (!$isLast && !empty($item->link)) : ?>
<a href="<?php echo $item->link; ?>" itemprop="item">
<span itemprop="name"><?php echo $item->name; ?></span>
</a>

View File

@@ -451,7 +451,7 @@ $wa->useScript('user.js'); // js/user.js
<button class="search-toggler d-lg-none" type="button" data-bs-toggle="collapse" data-bs-target="#headerSearchCollapse" aria-controls="headerSearchCollapse" aria-expanded="false" aria-label="<?php echo Text::_('JSEARCH_FILTER_SUBMIT'); ?>">
<span class="fa-solid fa-magnifying-glass" aria-hidden="true"></span>
</button>
<div class="container-search collapse d-lg-block" id="headerSearchCollapse">
<div class="container-search" id="headerSearchCollapse">
<jdoc:include type="modules" name="search" style="none" />
</div>
<?php endif; ?>

View File

@@ -18672,10 +18672,14 @@ nav[data-toggle=toc] .nav-link.active+ul{
margin-top: 0.5rem;
}
.container-header .container-search.collapse:not(.show) {
.container-header .container-search {
display: none;
}
.container-header .container-search.show {
display: block;
}
.mod-finder__search.input-group {
max-width: 100%;
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -539,6 +539,20 @@
});
}
/**
* Toggle search on mobile via .show class
*/
function initSearchToggle() {
var btn = doc.querySelector(".search-toggler");
var target = doc.getElementById("headerSearchCollapse");
if (!btn || !target) return;
btn.addEventListener("click", function () {
var isOpen = target.classList.toggle("show");
btn.setAttribute("aria-expanded", isOpen ? "true" : "false");
});
}
/**
* Run all template JS initializations
*/
@@ -563,6 +577,7 @@
// Init features
initDrawers();
initBackTop();
initSearchToggle();
initSidebarAccordion();
}

View File

@@ -36,7 +36,7 @@
</server>
</updateservers>
<name>MokoCassiopeia</name>
<version>03.09.07</version>
<version>03.09.09</version>
<scriptfile>script.php</scriptfile>
<creationDate>2026-03-26</creationDate>
<author>Jonathan Miller || Moko Consulting</author>
@@ -66,6 +66,7 @@
<folder>css</folder>
<folder>images</folder>
<folder>fonts</folder>
<folder>vendor</folder>
</media>
<positions>
<position>topbar</position>

View File

@@ -26,7 +26,7 @@
<type>template</type>
<client>site</client>
<version>03.09.04</version>
<version>03.09.07</version>
<creationDate>2026-04-07</creationDate>
<author>Jonathan Miller || Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -35,9 +35,9 @@
<infourl title='MokoCassiopeia'>https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/tag/v03</infourl>
<downloads>
<downloadurl type='full' format='zip'>https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.09.04.zip</downloadurl>
<downloadurl type='full' format='zip'>https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.09.07.zip</downloadurl>
</downloads>
<sha256>da85bf5a34cafadbae26df199ed36c04438e7dab440d046e0e4117d25510feaf</sha256>
<sha256>16a7ac98dd6e26144618a4ba534ea8fae0d115ec8373712743ab6daff0960916</sha256>
<tags>
<tag>stable</tag>
@@ -59,7 +59,7 @@
<type>template</type>
<client>site</client>
<version>03.09.06</version>
<version>03.09.07</version>
<creationDate>2026-04-07</creationDate>
<author>Jonathan Miller || Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -70,7 +70,7 @@
<downloads>
<downloadurl type='full' format='zip'>https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/release-candidate/mokocassiopeia-rc.zip</downloadurl>
</downloads>
<sha256>99b868a54466138d22a544d3e489559e7303960922c8eb3a142c504225b0e647</sha256>
<sha256>31e660078e728e8c9177b5a2d75efd89fea1fd4e9320d77444ab8fe28d3b354d</sha256>
<tags>
<tag>rc</tag>
@@ -92,7 +92,7 @@
<type>template</type>
<client>site</client>
<version>03.09.07</version>
<version>03.09.08</version>
<creationDate>2026-04-07</creationDate>
<author>Jonathan Miller || Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -101,9 +101,9 @@
<infourl title='MokoCassiopeia Dev'>https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/tag/development</infourl>
<downloads>
<downloadurl type='full' format='zip'>https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/development/mokocassiopeia-dev.zip</downloadurl>
<downloadurl type='full' format='zip'>https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/development/mokocassiopeia-03.09.08-dev.zip</downloadurl>
</downloads>
<sha256>b6403918c5f65f4f52b889fbe4bd217c903f0d9c2fcf90f8eb59df1e7b4b7333</sha256>
<sha256>ecff187531e65a40ae958ae91fff74da0c8856d1cc13e17a6e3d6905806b189e</sha256>
<tags>
<tag>development</tag>
@@ -134,9 +134,9 @@
<infourl title='MokoCassiopeia Alpha'>https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/tag/alpha</infourl>
<downloads>
<downloadurl type='full' format='zip'>https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/alpha/mokocassiopeia-alpha.zip</downloadurl>
<downloadurl type='full' format='zip'>https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/alpha/mokocassiopeia-03.09.07-alpha.zip</downloadurl>
</downloads>
<sha256>b5f6d073d126f0041ddd475e61bc4eaa050e060e9b51f4fe0e8a348f188c40b2</sha256>
<sha256>1a32180f8b26749bf5daf0602262e33464bcb3a042a8ff51ec2844cdeef2f9e5</sha256>
<tags>
<tag>alpha</tag>
@@ -167,9 +167,9 @@
<infourl title='MokoCassiopeia Beta'>https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/tag/beta</infourl>
<downloads>
<downloadurl type='full' format='zip'>https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/beta/mokocassiopeia-beta.zip</downloadurl>
<downloadurl type='full' format='zip'>https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/beta/mokocassiopeia-03.09.07-beta.zip</downloadurl>
</downloads>
<sha256>b5f6d073d126f0041ddd475e61bc4eaa050e060e9b51f4fe0e8a348f188c40b2</sha256>
<sha256>1a32180f8b26749bf5daf0602262e33464bcb3a042a8ff51ec2844cdeef2f9e5</sha256>
<tags>
<tag>beta</tag>