145 Commits

Author SHA1 Message Date
gitea-actions[bot] 80be62026f chore(release): build 04.03.00 [skip ci] 2026-05-24 04:32:37 +00:00
jmiller 9d5d60fadf fix: resolve all open bugs, promote CHANGELOG, harden error handling (#115)
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 3s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 5s
Generic: Repo Health / Scripts governance (push) Successful in 5s
Generic: Repo Health / Release configuration (push) Failing after 6s
Generic: Repo Health / Repository health (push) Failing after 5s
fix: resolve all open bugs, promote CHANGELOG, harden error handling (#115)

Closes #98, #100, #111, #112, #113
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 04:31:17 +00:00
jmiller a0a9b4c204 chore: sync updates.xml from [skip ci] 2026-05-24 04:17:23 +00:00
Moko Consulting c6ecee1917 chore: update CHANGELOG for deploy workflow removal
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 4s
Generic: Repo Health / Release configuration (push) Failing after 6s
Generic: Repo Health / Scripts governance (push) Successful in 6s
Generic: Repo Health / Repository health (push) Failing after 6s
2026-05-24 04:09:52 +00:00
jmiller b3aa838da6 chore: sync updates.xml from [skip ci] 2026-05-24 03:53:14 +00:00
jmiller 4c62eac923 Merge pull request 'fix: resolve version mismatches, missing lang keys, and error handling' (#109) from dev into main
chore: cascade main → dev [skip ci]
2026-05-24 03:44:15 +00:00
jmiller cb64d7371d chore: remove deploy workflow — switching to Joomla update server method
Generic: Repo Health / Site Health (push) Has been skipped
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 4s
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Repo Health / Release configuration (push) Failing after 4s
Generic: Repo Health / Scripts governance (push) Successful in 4s
Generic: Repo Health / Repository health (push) Failing after 4s
2026-05-24 03:44:05 +00:00
jmiller 312ba8072f chore: sync updates.xml from [skip ci] 2026-05-23 23:17:24 +00:00
gitea-actions[bot] 5b378b564b chore: update updates.xml (development: 04.02.01-dev) [skip ci] 2026-05-23 23:17:23 +00:00
Jonathan Miller d6a1e77453 feat(install): auto-select default menu slugs on fresh install
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 3s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 3s
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Has been skipped
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Has been skipped
Joomla: Extension CI / PHPStan Analysis (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 4s
Generic: Repo Health / Access control (pull_request) Successful in 3s
Generic: Repo Health / Release configuration (push) Failing after 5s
Generic: Repo Health / Scripts governance (push) Successful in 4s
Generic: Repo Health / Repository health (push) Failing after 5s
Generic: Repo Health / Release configuration (pull_request) Failing after 4s
Generic: Repo Health / Scripts governance (pull_request) Successful in 3s
Generic: Repo Health / Repository health (pull_request) Failing after 4s
Universal: PR Check / Validate PR (pull_request) Successful in 18s
Joomla: Update Server / Update updates.xml (push) Successful in 26s
After creating the TOS article and menu item, automatically sets
terms-of-service and privacy-policy as the default tos_slug params
so the plugin works immediately with zero configuration. Only sets
defaults if no slugs are already configured.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 18:16:55 -05:00
jmiller 2062575736 chore: sync updates.xml from [skip ci] 2026-05-23 23:12:00 +00:00
gitea-actions[bot] 06d9499b39 chore: update updates.xml (development: 04.02.01-dev) [skip ci] 2026-05-23 23:11:59 +00:00
Jonathan Miller 39177bf78b fix: resolve version mismatches, missing lang keys, and error handling
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Release configuration (push) Failing after 2s
Generic: Repo Health / Scripts governance (push) Successful in 3s
Generic: Repo Health / Repository health (push) Failing after 3s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 4s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 3s
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Has been skipped
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Has been skipped
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 3s
Joomla: Extension CI / PHPStan Analysis (pull_request) Has been skipped
Generic: Repo Health / Release configuration (pull_request) Failing after 4s
Generic: Repo Health / Scripts governance (pull_request) Successful in 4s
Generic: Repo Health / Repository health (pull_request) Failing after 4s
Universal: PR Check / Validate PR (pull_request) Successful in 23s
Joomla: Update Server / Update updates.xml (push) Successful in 36s
- Sync VERSION header in mokojoomtos.xml to 04.02.01 (#105)
- Add SEF_WARNING language key to site-side .ini files (#106)
- Update updates.xml: version to 04.02.01, add Joomla 4.x to
  targetplatform regex, fix download URLs (#107)
- Change catch(Exception) to catch(Throwable) in script.php to
  handle TypeError from chained null calls (#108)

Closes: #105, #106, #107, #108
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 18:11:13 -05:00
jmiller 97cc0acdf3 Merge pull request 'chore: cascade main → dev (fcacd01) [skip ci]' (#104) from main into dev
chore: cascade main → dev [skip ci]
2026-05-23 22:46:01 +00:00
jmiller fcacd01af9 feat: include children toggle, manifest fix, and bug fixes (#88)
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 3s
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Release configuration (push) Failing after 5s
Generic: Repo Health / Repository health (push) Failing after 5s
Generic: Repo Health / Scripts governance (push) Successful in 6s
feat: include children toggle, manifest fix, and bug fixes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 22:45:56 +00:00
Jonathan Miller bab1187da3 feat(plugin): add include children toggle + hardcode manifest description
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 3s
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Has been skipped
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Has been skipped
Joomla: Extension CI / PHPStan Analysis (pull_request) Has been skipped
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 4s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 4s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 3s
Universal: PR Check / Changelog Updated (pull_request) Successful in 3s
Universal: Build & Release / Build & Release Pipeline (pull_request) Failing after 51s
Adds a Yes/No radio field to control whether child menu items under
selected slugs are also accessible during offline mode. Defaults to
Yes for backwards compatibility. Also hardcodes the manifest
description to prevent raw language keys during installation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 17:44:32 -05:00
jmiller 940caa3d2d fix(ci): add branch output to auto-release [skip ci] 2026-05-23 19:47:50 +00:00
Jonathan Miller b61a1eff6d feat: SEF warning in slug field + CHANGELOG cleanup
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 4s
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Has been skipped
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Has been skipped
Joomla: Extension CI / PHPStan Analysis (pull_request) Has been skipped
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 4s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 5s
Universal: PR Check / Validate PR (pull_request) Failing after 4s
Universal: PR Check / Changelog Updated (pull_request) Successful in 3s
- #97: Add disabled warning option in MenuslugField when SEF URLs
  are off, with translatable language key in en-GB and en-US
- #100: Remove duplicate [03.09.00] entry, merge into single entry,
  update comparison links to Gitea, fix VERSION header to 04.02.01,
  add [Unreleased] section with current fixes

Closes: #97, #100

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 22:41:05 -05:00
Jonathan Miller a49fe2add8 fix: resolve critical bugs and code quality issues
Critical fixes:
- #89: Move enablePlugin() to postflight() — called unconditionally
- #90: Replace Table::addIncludePath() with bootComponent()->getMVCFactory()
       for Joomla 5 compatibility (was causing fatal crash on install)
- #91: Add non-SEF fallback — match by Itemid when SEF URLs are disabled

High priority fixes:
- #92: enablePlugin() now called on upgrade path too
- #93: Remove $_GET['tmpl'] superglobal mutation, use $input->set() only
- #94: Add params, metadata, attribs defaults to article creation data
- #95: Apply urldecode() to URI path before slug comparison
- #96: Cast Registry return to array before iterating slugs

Medium fixes:
- #99: Fix MenuslugField separator 'disable' → 'disabled' property

Code quality:
- #101: Strip legacy mokojoomtos.php to minimal stub (dead code under Joomla 5)
- #102: Convert script.php from spaces to tabs
- #103: Rename installer class to PlgSystemMokojoomtosInstallerScript

Closes: #89, #90, #91, #92, #93, #94, #95, #96, #99, #101, #102, #103

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 22:33:39 -05:00
jmiller eec803edac fix(ci): pre-release php-curl + continue-on-error + CLI updates.xml [skip ci] 2026-05-22 03:31:11 +00:00
jmiller cf5400a047 fix(update): update SHA-256 for repackaged stable ZIP [skip ci] 2026-05-22 03:15:16 +00:00
jmiller 436a3b1029 refactor(ci): sync auto-release.yml — CLI-based workflow [skip ci] 2026-05-22 02:56:12 +00:00
jmiller 9b07bfa167 fix(update): point dev channel to stable artifact [skip ci] 2026-05-22 02:55:51 +00:00
Jonathan Miller f7a0c3672d fix(update): point dev channel to stable release artifact
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 3s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 3s
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Has been skipped
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Has been skipped
Joomla: Extension CI / PHPStan Analysis (pull_request) Has been skipped
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 4s
Universal: PR Check / Validate PR (pull_request) Failing after 3s
Universal: PR Check / Changelog Updated (pull_request) Failing after 3s
The development channel was pointing to a non-existent dev release
tag, causing download failures. All channels now point to the stable
release (04.01.00) until a dev pre-release is actually built.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 21:55:26 -05:00
jmiller d2139df514 refactor(ci): pre-release uses CLI tools [skip ci] 2026-05-22 02:49:37 +00:00
Jonathan Miller 159b71b1bd fix(update): merge dev to main — correct versions and CLI workflows
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 3s
Generic: Repo Health / Release configuration (push) Failing after 3s
Generic: Repo Health / Scripts governance (push) Successful in 4s
Generic: Repo Health / Repository health (push) Failing after 4s
- Fix update loop: manifest 04.02.01, stable 04.01.00, dev 04.02.01
- Replace inline bash in workflows with moko-platform CLI calls

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 21:48:02 -05:00
jmiller e1b0fafd70 fix(ci): sync pre-release.yml — CLI-based updates.xml sync [skip ci] 2026-05-22 02:40:06 +00:00
jmiller 64e0aec932 fix(ci): sync pre-release.yml — updates.xml API sync (#34) [skip ci] 2026-05-22 02:35:52 +00:00
Jonathan Miller 52b1f33f3e fix(update): correct version numbers to stop update loop
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 4s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 4s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 4s
Universal: PR Check / Changelog Updated (pull_request) Failing after 3s
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Has been skipped
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Has been skipped
Joomla: Extension CI / PHPStan Analysis (pull_request) Has been skipped
- Set manifest version to 04.02.01 (dev stream)
- Set updates.xml stable stream to 04.01.00
- Set updates.xml dev stream to 04.02.01
- Cascade stable version through alpha/beta/rc channels
- Remove stale SHA-256 hashes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 21:31:03 -05:00
Jonathan Miller 9e55617f0f chore: migrate to .mokogitea and remove legacy standards files
- Rename .gitea/ → .mokogitea/ (where applicable)
- Remove .moko-standards and .mokostandards files
- Manifest.xml is the single source of repo metadata

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 17:43:19 -05:00
jmiller 7228bb9744 chore: sync security-audit.yml from moko-platform [skip ci] 2026-05-21 22:29:35 +00:00
jmiller 20922e7a1c chore: sync repo-health.yml from moko-platform [skip ci] 2026-05-21 22:29:34 +00:00
jmiller 4579cf684b chore: sync pre-release.yml from moko-platform [skip ci] 2026-05-21 22:29:33 +00:00
jmiller f193d62652 chore: sync pr-check.yml from moko-platform [skip ci] 2026-05-21 22:29:32 +00:00
jmiller 7b0aa534ed chore: sync notify.yml from moko-platform [skip ci] 2026-05-21 22:29:31 +00:00
jmiller 416725c212 chore: sync gitleaks.yml from moko-platform [skip ci] 2026-05-21 22:29:30 +00:00
jmiller a4e6bdd2af chore: sync deploy-manual.yml from moko-platform [skip ci] 2026-05-21 22:29:29 +00:00
jmiller b6b212e7d2 chore: sync cleanup.yml from moko-platform [skip ci] 2026-05-21 22:29:28 +00:00
jmiller a984b03e07 chore: sync cascade-dev.yml from moko-platform [skip ci] 2026-05-21 22:29:27 +00:00
jmiller 6f92653ed7 chore: sync auto-release.yml from moko-platform [skip ci] 2026-05-21 22:29:26 +00:00
Jonathan Miller 53da1d7457 chore: fix manifest.xml to proper moko-platform format
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 17:26:06 -05:00
Jonathan Miller 6ec75f2b5d chore(ci): use manifest.xml for platform detection, remove .moko-platform
Authored-by: Moko Consulting

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 17:19:01 -05:00
Jonathan Miller 07a4841dec chore(ci): migrate .gitea to .mokogitea and update workflows
Renamed .gitea/ to .mokogitea/ for MokoConsulting convention.
Updated release workflows with Joomla package type support.

Authored-by: Moko Consulting

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 15:53:39 -05:00
Jonathan Miller fff64be76d chore(ci): update release workflows with package type support
Synced from Template-Joomla: pre-release.yml and auto-release.yml now
handle Joomla package extensions (type="package" with sub-extensions).

Authored-by: Moko Consulting

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 15:53:38 -05:00
jmiller f01d7ba140 chore: update CLAUDE.md to reference .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 20:09:07 +00:00
jmiller 0b691be220 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:28 +00:00
jmiller d9c8752081 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:28 +00:00
jmiller 9373459301 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:28 +00:00
jmiller d3494e1f36 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:27 +00:00
jmiller 978e1774aa chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:27 +00:00
jmiller bbf9ef3310 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:27 +00:00
jmiller 010a467f5c chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:26 +00:00
jmiller 8cb83668e1 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:26 +00:00
jmiller 01e6778664 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:26 +00:00
jmiller 2b5734207c chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:25 +00:00
jmiller 33979440b4 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:25 +00:00
jmiller 983e56fc16 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:25 +00:00
jmiller 54b27702e9 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:24 +00:00
jmiller 67ee1d8282 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:24 +00:00
jmiller 2c86454989 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:24 +00:00
jmiller 0856361ebc chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:23 +00:00
jmiller 802bab7777 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:23 +00:00
jmiller f4ed92c0c3 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:23 +00:00
jmiller 0ee3b181a5 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:22 +00:00
jmiller 3226a75086 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:22 +00:00
jmiller d4ac99c7d9 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:22 +00:00
jmiller e65ff7e422 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:21 +00:00
jmiller 998e2f0622 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:21 +00:00
jmiller e0a7348e2c chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:21 +00:00
jmiller a47d229cbb chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:20 +00:00
jmiller 693ab27fe7 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:20 +00:00
jmiller 93cf3ff81a chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:20 +00:00
jmiller 694947586f chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:19 +00:00
jmiller f2c1ce0764 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:19 +00:00
jmiller 20d085f91c chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:19 +00:00
jmiller fc359ec5a5 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:18 +00:00
jmiller 63e76c27fd chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:18 +00:00
jmiller 87853baf75 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:18 +00:00
jmiller f35ccb197a chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:17 +00:00
jmiller 3e19ca7408 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:17 +00:00
jmiller 65d156e15b chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:17 +00:00
jmiller d38766d0b8 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:16 +00:00
jmiller c3bb1a1295 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:16 +00:00
jmiller d6f0e1fe0c chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:16 +00:00
jmiller a1ba923155 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:15 +00:00
jmiller 60b2ee2436 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:15 +00:00
jmiller 3efcba855d chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:15 +00:00
jmiller 2614e6d930 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:14 +00:00
jmiller 71bf7c78b7 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:14 +00:00
jmiller 3a29a94920 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:14 +00:00
jmiller abdc861dfb chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:13 +00:00
jmiller d42f23dae9 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:13 +00:00
jmiller 63613aa48c chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:13 +00:00
jmiller f33e0f2701 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:13 +00:00
jmiller fae7614c29 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:12 +00:00
jmiller 5b4d5e4d42 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:12 +00:00
jmiller 0e7a022478 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:12 +00:00
jmiller da96e87969 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:11 +00:00
jmiller af3e53b734 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:11 +00:00
jmiller d9b14431e6 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:11 +00:00
jmiller e8d9755a2d chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:10 +00:00
jmiller ea347ea41b chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:10 +00:00
jmiller 6e28f9c89a chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:10 +00:00
jmiller 785bfca673 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:09 +00:00
jmiller eef5d54774 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:09 +00:00
jmiller 1d99daf4a5 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:09 +00:00
jmiller 81eaf0d4e3 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:08 +00:00
jmiller fb376ae368 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:08 +00:00
jmiller 5bd15387c1 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:08 +00:00
jmiller 6594bb6239 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:07 +00:00
jmiller f2930c401f chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:07 +00:00
jmiller f2f91f2f2c chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:07 +00:00
jmiller 649b5c1f23 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:06 +00:00
jmiller 2923918e0b chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:06 +00:00
jmiller 45e57510f4 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:06 +00:00
jmiller f9aa1b91ce chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:05 +00:00
jmiller 92491d5559 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:05 +00:00
jmiller d9db104dd7 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:05 +00:00
jmiller 660d65aee4 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:16:04 +00:00
jmiller 22d8965be1 chore: add issue templates [skip ci] 2026-05-20 00:36:17 +00:00
jmiller 4d104b8dc7 chore: sync issue templates from template repo [skip ci] 2026-05-20 00:29:32 +00:00
jmiller 7b4f03d1f2 chore: sync issue templates from template repo [skip ci] 2026-05-20 00:29:30 +00:00
jmiller d201fa0384 chore: sync issue templates from template repo [skip ci] 2026-05-20 00:29:28 +00:00
jmiller a1a722c267 chore: sync issue templates from template repo [skip ci] 2026-05-20 00:29:27 +00:00
jmiller 739b9f974a chore: sync issue templates from template repo [skip ci] 2026-05-20 00:29:26 +00:00
jmiller f65afd370c chore: sync issue templates from template repo [skip ci] 2026-05-20 00:29:25 +00:00
jmiller 92609bbdf3 chore: sync issue templates from template repo [skip ci] 2026-05-20 00:29:24 +00:00
jmiller 9bae409466 chore: sync issue templates from template repo [skip ci] 2026-05-20 00:29:23 +00:00
jmiller ccd9e3c0e4 chore: sync issue templates from template repo [skip ci] 2026-05-20 00:29:21 +00:00
jmiller cd780ce8a3 chore: sync issue templates from template repo [skip ci] 2026-05-20 00:29:20 +00:00
jmiller 687fdb9ed7 Merge pull request 'chore: cascade main → dev (20b56b9) [skip ci]' (#87) from main into dev 2026-05-16 22:58:36 +00:00
jmiller 20b56b9b00 Merge pull request 'chore: v04.01.00 changelog + pretty name for stable release' (#86) from dev into main
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 2s
Joomla: Repo Health / Access control (push) Successful in 1s
Joomla: Repo Health / Scripts governance (push) Successful in 3s
Joomla: Repo Health / Release configuration (push) Failing after 3s
Joomla: Repo Health / Repository health (push) Failing after 3s
Universal: Repository Cleanup / Clean Merged Branches (push) Successful in 7s
Universal: Secret Scanning / Gitleaks Secret Scan (push) Successful in 5s
Universal: Security Audit / Dependency Audit (push) Successful in 2s
2026-05-16 22:57:54 +00:00
Jonathan Miller afdd7b4fe8 Merge remote-tracking branch 'origin/main' into dev
Joomla: Repo Health / Access control (push) Successful in 2s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 4s
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Has been skipped
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Has been skipped
Joomla: Extension CI / PHPStan Analysis (pull_request) Has been skipped
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 2s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 3s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 3s
Universal: PR Check / Changelog Updated (pull_request) Successful in 3s
Joomla: Repo Health / Access control (pull_request) Successful in 1s
Joomla: Repo Health / Release configuration (push) Failing after 2s
Joomla: Repo Health / Scripts governance (push) Successful in 4s
Joomla: Repo Health / Repository health (push) Failing after 4s
Joomla: Repo Health / Release configuration (pull_request) Failing after 3s
Joomla: Repo Health / Scripts governance (pull_request) Successful in 3s
Joomla: Repo Health / Repository health (pull_request) Failing after 3s
# Conflicts:
#	CHANGELOG.md
2026-05-16 17:57:25 -05:00
Jonathan Miller e65ef8a140 chore: add v04.01.00 changelog entry [skip ci]
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-16 17:54:49 -05:00
jmiller 7800ca140f chore(version): bump 04.01.00 → 04.02.00 (dev) [skip ci] 2026-05-16 22:52:51 +00:00
jmiller f269dfd441 chore(version): bump 04.01.00 → 04.02.00 (dev) [skip ci] 2026-05-16 22:52:50 +00:00
jmiller 7af5c44406 chore: sync updates.xml 04.01.00 [skip ci] 2026-05-16 22:52:49 +00:00
jmiller 1d6854b4be Merge pull request 'chore: cascade main → dev (147bcc8) [skip ci]' (#85) from main into dev 2026-05-16 22:51:04 +00:00
Jonathan Miller c4b1983e9e docs(lang): update plugin help text for multi-select workflow [skip ci]
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 1m0s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-16 17:49:12 -05:00
Jonathan Miller ad3d46180b fix(lang): update pretty name to Joomla convention [skip ci]
Use literal display name in <name> tag instead of language key.
Joomla stores this directly in the DB, avoiding translation lookup
issues during CLI installs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-16 17:44:42 -05:00
jmiller 5dfcf926be chore(version): bump 04.00.00 → 04.01.00 (dev) [skip ci] 2026-05-16 22:38:06 +00:00
jmiller 50ee63ef80 chore(version): bump 04.00.00 → 04.01.00 (dev) [skip ci] 2026-05-16 22:38:05 +00:00
jmiller 6371c7240c chore(version): bump 04.00.00 → 04.01.00 (dev) [skip ci] 2026-05-16 22:34:30 +00:00
jmiller 2810caa810 chore(version): bump 04.00.00 → 04.01.00 (dev) [skip ci] 2026-05-16 22:34:29 +00:00
jmiller 4efce6b8ef Merge pull request 'chore: cascade main → dev (0db469f) [skip ci]' (#79) from main into dev 2026-05-16 22:33:24 +00:00
52 changed files with 1293 additions and 1680 deletions
-126
View File
@@ -1,126 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Deploy
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
# PATH: /templates/workflows/joomla/deploy-manual.yml.template
# VERSION: 04.07.00
# BRIEF: Manual SFTP deploy to dev server for Joomla repos
name: "Universal: Deploy to Dev (Manual)"
on:
workflow_dispatch:
inputs:
clear_remote:
description: 'Delete all remote files before uploading'
required: false
default: 'false'
type: boolean
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
permissions:
contents: read
jobs:
deploy:
name: SFTP Deploy to Dev
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup PHP
run: |
php -v && composer --version
- name: Setup MokoStandards tools
env:
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
-383
View File
@@ -1,383 +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.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /templates/workflows/universal/pre-release.yml.template
# VERSION: 05.00.00
# BRIEF: Manual pre-release — builds dev/alpha/beta/rc packages from any branch
name: "Universal: Pre-Release"
on:
workflow_dispatch:
inputs:
stability:
description: 'Pre-release channel'
required: true
type: choice
options:
- development
- alpha
- beta
- release-candidate
permissions:
contents: write
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:
build:
name: "Build Pre-Release (${{ inputs.stability }})"
runs-on: release
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GA_TOKEN }}
- name: Setup PHP
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 >/dev/null 2>&1
fi
- name: Detect platform
id: platform
run: |
# Read platform from .gitea/manifest.xml
PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .gitea/manifest.xml 2>/dev/null | head -1)
[ -z "$PLATFORM" ] && PLATFORM="generic"
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "./.gitea/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1)
echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT"
echo "mod_file=${MOD_FILE}" >> "$GITHUB_OUTPUT"
- name: Resolve metadata
id: meta
run: |
STABILITY="${{ inputs.stability }}"
case "$STABILITY" in
development) SUFFIX="-dev"; TAG="development" ;;
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
beta) SUFFIX="-beta"; TAG="beta" ;;
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
esac
# Read and bump patch version (with rollover)
CURRENT=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1)
[ -z "$CURRENT" ] && CURRENT="00.00.00"
MAJOR=$(echo "$CURRENT" | cut -d. -f1)
MINOR=$(echo "$CURRENT" | cut -d. -f2)
PATCH=$(echo "$CURRENT" | cut -d. -f3)
# Patch bump with rollover: ZZ=99 → bump minor, YY=99 → bump major
NEW_PATCH=$((10#$PATCH + 1))
NEW_MINOR=$((10#$MINOR))
NEW_MAJOR=$((10#$MAJOR))
if [ $NEW_PATCH -gt 99 ]; then
NEW_PATCH=0
NEW_MINOR=$((NEW_MINOR + 1))
fi
if [ $NEW_MINOR -gt 99 ]; then
NEW_MINOR=0
NEW_MAJOR=$((NEW_MAJOR + 1))
fi
VERSION=$(printf "%02d.%02d.%02d" $NEW_MAJOR $NEW_MINOR $NEW_PATCH)
TODAY=$(date +%Y-%m-%d)
echo "Bumping: ${CURRENT} → ${VERSION} (patch)"
# Update README.md
sed -i "s/VERSION:[[:space:]]*${CURRENT}/VERSION: ${VERSION}/" README.md
# Update platform-specific manifest
PLATFORM="${{ steps.platform.outputs.platform }}"
MANIFEST="${{ steps.platform.outputs.manifest }}"
MOD_FILE="${{ steps.platform.outputs.mod_file }}"
case "$PLATFORM" in
joomla)
if [ -n "$MANIFEST" ]; then
MANIFEST_VER=$(sed -n 's/.*<version>\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1)
sed -i "s|<version>${MANIFEST_VER}</version>|<version>${VERSION}</version>|" "$MANIFEST"
sed -i "s|<creationDate>[^<]*</creationDate>|<creationDate>${TODAY}</creationDate>|" "$MANIFEST"
fi
;;
dolibarr)
if [ -n "$MOD_FILE" ]; then
sed -i "s/\$this->version = '[^']*'/\$this->version = '${VERSION}'/" "$MOD_FILE"
fi
;;
*) ;;
esac
# Commit version bump
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
git add -A
git diff --cached --quiet || {
git commit -m "chore(version): bump ${CURRENT} → ${VERSION} [skip ci]"
git push origin HEAD 2>&1
}
# Auto-detect element (platform-aware)
case "$PLATFORM" in
joomla)
MANIFEST="${{ steps.platform.outputs.manifest }}"
EXT_ELEMENT=""
if [ -n "$MANIFEST" ]; then
EXT_ELEMENT=$(sed -n 's/.*<element>\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
if [ -z "$EXT_ELEMENT" ]; then
EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]')
case "$EXT_ELEMENT" in
templatedetails|manifest) EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;;
esac
fi
else
EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
fi
;;
dolibarr)
MOD_FILE="${{ steps.platform.outputs.mod_file }}"
if [ -n "$MOD_FILE" ]; then
MOD_BASENAME=$(basename "$MOD_FILE" .class.php)
EXT_ELEMENT=$(echo "$MOD_BASENAME" | sed 's/^mod//' | tr '[:upper:]' '[:lower:]')
else
EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
fi
;;
*)
EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
;;
esac
ZIP_NAME="${EXT_ELEMENT}-${VERSION}${SUFFIX}.zip"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT"
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
- name: Build package
run: |
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
if [ ! -d "$SOURCE_DIR" ]; then
echo "::error::No src/ or htdocs/ directory"
exit 1
fi
mkdir -p build/package
rsync -a \
--exclude='sftp-config*' \
--exclude='.ftpignore' \
--exclude='*.ppk' \
--exclude='*.pem' \
--exclude='*.key' \
--exclude='.env*' \
--exclude='*.local' \
--exclude='.build-trigger' \
"${SOURCE_DIR}/" build/package/
- name: Create ZIP
id: zip
run: |
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
cd build/package
zip -r "../${ZIP_NAME}" .
cd ..
SHA256=$(sha256sum "${ZIP_NAME}" | cut -d' ' -f1)
echo "sha256=${SHA256}" >> "$GITHUB_OUTPUT"
echo "ZIP: ${ZIP_NAME} (SHA: ${SHA256:0:16}...)"
- name: Create or replace Gitea release
id: release
run: |
TAG="${{ steps.meta.outputs.tag }}"
VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
SHA256="${{ steps.zip.outputs.sha256 }}"
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
EXT_ELEMENT="${{ steps.meta.outputs.ext_element }}"
TOKEN="${{ secrets.GA_TOKEN }}"
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
BRANCH=$(git branch --show-current)
BODY="## ${VERSION} ($(date +%Y-%m-%d))
**Channel:** ${STABILITY}
**SHA-256:** \`${SHA256}\`"
# Delete existing release
EXISTING_ID=$(curl -sS -H "Authorization: token ${TOKEN}" \
"${API}/releases/tags/${TAG}" | jq -r '.id // empty' 2>/dev/null)
if [ -n "$EXISTING_ID" ]; then
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
"${API}/releases/${EXISTING_ID}" 2>/dev/null || true
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
"${API}/tags/${TAG}" 2>/dev/null || true
fi
# Create release
RELEASE_ID=$(curl -sS -X POST -H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
"${API}/releases" \
-d "$(jq -n \
--arg tag "$TAG" \
--arg target "$BRANCH" \
--arg name "${EXT_ELEMENT} ${VERSION} (${STABILITY})" \
--arg body "$BODY" \
'{tag_name: $tag, target_commitish: $target, name: $name, body: $body, prerelease: true}'
)" | jq -r '.id')
echo "release_id=${RELEASE_ID}" >> "$GITHUB_OUTPUT"
# Upload ZIP
curl -sS -X POST -H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/octet-stream" \
"${API}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" \
--data-binary "@build/${ZIP_NAME}"
echo "Released: ${EXT_ELEMENT} ${VERSION} (${STABILITY})"
- name: Update updates.xml
if: steps.platform.outputs.platform == 'joomla'
run: |
STABILITY="${{ steps.meta.outputs.stability }}"
VERSION="${{ steps.meta.outputs.version }}"
SHA256="${{ steps.zip.outputs.sha256 }}"
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
TAG="${{ steps.meta.outputs.tag }}"
DATE=$(date +%Y-%m-%d)
if [ ! -f "updates.xml" ]; then
echo "No updates.xml — skipping"
exit 0
fi
export PY_STABILITY="$STABILITY" PY_VERSION="$VERSION" PY_SHA256="$SHA256" \
PY_ZIP_NAME="$ZIP_NAME" PY_TAG="$TAG" PY_DATE="$DATE" \
PY_GITEA_ORG="$GITEA_ORG" PY_GITEA_REPO="$GITEA_REPO"
python3 << 'PYEOF'
import re, os
stability = os.environ["PY_STABILITY"]
version = os.environ["PY_VERSION"]
sha256 = os.environ["PY_SHA256"]
zip_name = os.environ["PY_ZIP_NAME"]
tag = os.environ["PY_TAG"]
date = os.environ["PY_DATE"]
gitea_org = os.environ["PY_GITEA_ORG"]
gitea_repo = os.environ["PY_GITEA_REPO"]
download_url = f"https://git.mokoconsulting.tech/{gitea_org}/{gitea_repo}/releases/download/{tag}/{zip_name}"
with open("updates.xml", "r") as f:
content = f.read()
# Map stability to XML tag name
tag_map = {"development": "development", "alpha": "alpha", "beta": "beta", "release-candidate": "rc"}
xml_tag = tag_map.get(stability, stability)
pattern = r"(<update>(?:(?!</update>).)*?<tag>" + re.escape(xml_tag) + r"</tag>.*?</update>)"
match = re.search(pattern, content, re.DOTALL)
if match:
block = match.group(1)
updated = re.sub(r"<version>[^<]*</version>", f"<version>{version}</version>", block)
updated = re.sub(r"<creationDate>[^<]*</creationDate>", f"<creationDate>{date}</creationDate>", updated)
if "<sha256>" in updated:
updated = re.sub(r"<sha256>[^<]*</sha256>", f"<sha256>{sha256}</sha256>", updated)
else:
updated = updated.replace("</downloads>", f"</downloads>\n <sha256>{sha256}</sha256>")
updated = re.sub(r"(<downloadurl[^>]*>)[^<]*(</downloadurl>)", rf"\g<1>{download_url}\g<2>", updated)
content = content.replace(block, updated)
print(f"Updated {xml_tag} channel: version={version}")
else:
print(f"WARNING: No <tag>{xml_tag}</tag> block in updates.xml")
with open("updates.xml", "w") as f:
f.write(content)
PYEOF
# Commit and push to current branch
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]"
# Sync updates.xml to main and dev (whichever isn't current)
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}" -- . 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)"
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.GA_TOKEN }}"
STABILITY="${{ steps.meta.outputs.stability }}"
# Cascade: rc → beta,alpha,dev | beta → alpha,dev | alpha → dev | dev → nothing
case "$STABILITY" in
release-candidate) TAGS_TO_DELETE="beta alpha development" ;;
beta) TAGS_TO_DELETE="alpha development" ;;
alpha) TAGS_TO_DELETE="development" ;;
*) TAGS_TO_DELETE="" ;;
esac
[ -z "$TAGS_TO_DELETE" ] && exit 0
for TAG in $TAGS_TO_DELETE; do
RELEASE_ID=$(curl -sS -H "Authorization: token ${TOKEN}" \
"${API_BASE}/releases/tags/${TAG}" 2>/dev/null | \
python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "None" ]; then
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
"${API_BASE}/releases/${RELEASE_ID}" 2>/dev/null || true
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
"${API_BASE}/tags/${TAG}" 2>/dev/null || true
echo "Deleted: ${TAG} (id: ${RELEASE_ID})"
fi
done
-20
View File
@@ -1,20 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# SPDX-License-Identifier: GPL-3.0-or-later
# FILE INFORMATION
# DEFGROUP: MokoStandards.Templates.Config
# INGROUP: MokoStandards.Templates
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /templates/configs/moko-standards.yml
# VERSION: 04.01.00
# BRIEF: Governance attachment template — synced to .moko-standards in every governed repository
# NOTE: Tokens replaced at sync time: mokoconsulting-tech, MokoJoomTOS, waas-component, 04.00.04
#
# This file is managed automatically by MokoStandards bulk sync.
# Do not edit manually — changes will be overwritten on the next sync.
# To update governance settings, open a PR in MokoStandards instead:
# https://github.com/mokoconsulting-tech/MokoStandards
standards_source: "https://github.com/mokoconsulting-tech/MokoStandards"
standards_version: "04.00.04"
platform: "waas-component"
governed_repo: "mokoconsulting-tech/MokoJoomTOS"
@@ -0,0 +1,77 @@
---
name: WaaS Client Site Issue
about: Report an issue with a WaaS client site (branding, deployment, media sync)
title: '[WAAS] '
labels: 'waas, client-site'
assignees: ''
---
## Site Issue Type
- [ ] Branding / CSS not applying
- [ ] Deployment failure
- [ ] Media sync issue
- [ ] Template override not working
- [ ] Module positioning issue
- [ ] Mobile / responsive layout
- [ ] Performance issue
## Client Site
- **Client Org**: [e.g., ClarksvilleFurs]
- **Repo**: [e.g., client-waas-clarksvillefurs]
- **Environment**: [Dev / Production]
- **Site URL**: [dev or production URL — omit if private]
## Issue Description
Describe the issue clearly.
## Steps to Reproduce
1. Visit [page URL]
2. Look at [element]
3. See error
## Expected Behavior
What the site should look like or how it should behave.
## Actual Behavior
What is happening instead.
## Screenshots
Attach screenshots showing the issue (desktop and mobile if relevant).
## Deployment Status
- **Last deploy**: [date or "unknown"]
- **Deploy workflow**: [succeeded / failed / not run]
- **Branch**: [dev / main]
## Media Sync
- [ ] Images missing after sync
- [ ] Sync direction: [dev-to-prod / prod-to-dev / bidirectional]
- [ ] Last sync: [date]
## Template Details
- **Joomla Version**: [e.g., 5.x]
- **Template Name**: [e.g., clienttemplate]
- **MokoWaaS Plugin**: [Active / Inactive]
- **MokoOnyx Admin**: [Active / Inactive]
## CSS Custom Properties
If branding issue, list the relevant CSS variables:
```css
:root {
--client-primary: #...;
--client-secondary: #...;
}
```
## Browser / Device
- **Browser**: [e.g., Chrome 120, Safari 17]
- **Device**: [Desktop / Tablet / Mobile]
- **Screen Width**: [e.g., 1920px, 768px, 375px]
## Checklist
- [ ] I have cleared Joomla cache
- [ ] I have hard-refreshed the browser (Ctrl+Shift+R)
- [ ] I have checked the deploy workflow completed
- [ ] I have verified the change is on the correct branch
- [ ] No credentials or PII are included in this issue
@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
MokoStandards Repository Manifest
Auto-generated by cleanup script.
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">
@@ -4,8 +4,8 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Release
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
# INGROUP: moko-platform.Release
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# PATH: /templates/workflows/universal/auto-release.yml.template
# VERSION: 05.00.00
# BRIEF: Universal build & release detects platform from manifest.xml
@@ -58,7 +58,7 @@ jobs:
token: ${{ secrets.GA_TOKEN }}
fetch-depth: 0
- name: Setup moko-platform
- name: Setup moko-platform tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
@@ -69,132 +69,47 @@ jobs:
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
fi
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 ]; then
cd /tmp/mokostandards-api
composer install --no-dev --no-interaction --quiet 2>/dev/null || true
fi
echo "MokoStandards tools: $([ -d /tmp/mokostandards-api ] && echo 'available' || echo 'unavailable (using fallback)')"
"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
# -- PLATFORM DETECTION ---------------------------------------------------
- name: Detect platform
id: platform
run: |
# 1. Try explicit platform from manifest (.mokogitea takes precedence)
if [ -f ".mokogitea/manifest.xml" ]; then
PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1)
elif [ -f ".gitea/manifest.xml" ]; then
PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .gitea/manifest.xml 2>/dev/null | head -1)
fi
# 2. Auto-sense from file structure if no manifest
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
CB_MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<cbplugin' {} \; 2>/dev/null | head -1)
MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1)
if [ -z "$PLATFORM" ]; then
if [ -n "$CB_MANIFEST" ]; then
PLATFORM="joomla"
MANIFEST="$CB_MANIFEST"
elif [ -n "$MANIFEST" ]; then
PLATFORM="joomla"
elif [ -n "$MOD_FILE" ]; then
PLATFORM="dolibarr"
else
PLATFORM="generic"
fi
fi
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
echo "Platform detected: ${PLATFORM}"
php /tmp/moko-platform-api/cli/manifest_read.php --path . --github-output
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true)
MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1 || true)
echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT"
echo "mod_file=${MOD_FILE}" >> "$GITHUB_OUTPUT"
# Detect update file methodology
if [ "$PLATFORM" = "joomla" ]; then
echo "update_method=updates.xml" >> "$GITHUB_OUTPUT"
echo "Update method: Joomla updates.xml"
elif [ "$PLATFORM" = "dolibarr" ]; then
echo "update_method=update.txt" >> "$GITHUB_OUTPUT"
echo "Update method: Dolibarr update.txt"
else
echo "update_method=none" >> "$GITHUB_OUTPUT"
echo "Update method: none (generic)"
fi
# -- STEP 1: Read version -----------------------------------------------
- name: "Step 1: Read version from README.md"
- name: "Step 1: Read version"
id: version
run: |
# Try MokoStandards PHP tool first, fall back to direct parsing
VERSION=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null || true)
VERSION=$(php /tmp/moko-platform-api/cli/version_read.php --path .)
if [ -z "$VERSION" ]; then
# Fallback: read from README table format "| **Version** | XX.YY.ZZ |"
VERSION=$(sed -n 's/.*\*\*Version\*\*[[:space:]]*|[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1)
fi
if [ -z "$VERSION" ]; then
# Fallback: read VERSION: XX.YY.ZZ pattern from any file
VERSION=$(grep -rh "VERSION:[[:space:]]*[0-9]" README.md CHANGELOG.md 2>/dev/null | sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' | head -1)
fi
if [ -z "$VERSION" ]; then
echo "No VERSION in README.md — skipping release"
echo "::error::No VERSION in README.md"
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
# Derive major.minor for branch naming (patches update existing branch)
MINOR=$(echo "$VERSION" | awk -F. '{printf "%s.%s", $1, $2}')
PATCH=$(echo "$VERSION" | awk -F. '{print $3}')
MAJOR=$(echo "$VERSION" | awk -F. '{print $1}')
MINOR_NUM=$(echo "$VERSION" | awk -F. '{print $2}')
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "branch=version/${MAJOR}" >> "$GITHUB_OUTPUT"
echo "minor=$MINOR" >> "$GITHUB_OUTPUT"
echo "major=$MAJOR" >> "$GITHUB_OUTPUT"
echo "release_tag=stable" >> "$GITHUB_OUTPUT"
echo "stability=stable" >> "$GITHUB_OUTPUT"
echo "skip=false" >> "$GITHUB_OUTPUT"
if [ "$PATCH" = "00" ] || [ "$PATCH" = "01" ]; then
echo "is_minor=true" >> "$GITHUB_OUTPUT"
echo "Version: $VERSION (first release for this minor — full pipeline)"
else
echo "is_minor=false" >> "$GITHUB_OUTPUT"
echo "Version: $VERSION (patch — platform version + badges only)"
fi
# -- STEP 1b: Promote CHANGELOG [Unreleased] to current version -----------
- name: "Step 1b: Promote CHANGELOG for release"
if: steps.version.outputs.skip != 'true'
id: bump
run: |
VERSION="${{ steps.version.outputs.version }}"
TODAY=$(date +%Y-%m-%d)
# Promote [Unreleased] section in CHANGELOG.md to release version
if [ -f "CHANGELOG.md" ] && grep -qi "Unreleased" CHANGELOG.md; then
sed -i "s|## \[Unreleased\]|## [${VERSION}] --- ${TODAY}|" CHANGELOG.md
sed -i "s|## Unreleased|## [${VERSION}] --- ${TODAY}|" CHANGELOG.md
sed -i "2i ## [Unreleased]" CHANGELOG.md
sed -i "3i \\ " CHANGELOG.md
echo "CHANGELOG promoted to [${VERSION}]"
fi
# Commit changelog promotion if changed
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
git add -A
git diff --cached --quiet || {
git commit -m "chore(release): promote CHANGELOG ${VERSION} [skip ci]"
git push origin HEAD:main 2>&1
}
# Pass through version (no bump — release uses version as-is from dev)
MAJOR=$(echo "$VERSION" | cut -d. -f1)
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "major=${{ steps.version.outputs.major }}" >> "$GITHUB_OUTPUT"
echo "release_tag=v${MAJOR}" >> "$GITHUB_OUTPUT"
echo "skip=false" >> "$GITHUB_OUTPUT"
echo "branch=version/${MAJOR}" >> "$GITHUB_OUTPUT"
- name: "Step 1b: Bump version"
id: bump
if: steps.version.outputs.skip != 'true'
run: |
MOKO_API="/tmp/moko-platform-api/cli"
BUMP=$(php ${MOKO_API}/version_bump.php --path . --minor)
VERSION=$(echo "$BUMP" | grep -oP '\d{2}\.\d{2}\.\d{2}$' || true)
[ -z "$VERSION" ] && VERSION=$(php ${MOKO_API}/version_read.php --path .)
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "Bumped to: ${VERSION}"
- name: Check if already released
if: steps.version.outputs.skip != 'true'
@@ -339,199 +254,27 @@ jobs:
steps.check.outputs.already_released != 'true'
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
if [ -f /tmp/mokostandards-api/cli/version_set_platform.php ]; then
php /tmp/mokostandards-api/cli/version_set_platform.php \
--path . --version "$VERSION" --branch main
else
# Fallback: update version in templateDetails.xml directly
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
if [ -n "$MANIFEST" ]; then
sed -i "s|<version>[^<]*</version>|<version>${VERSION}</version>|" "$MANIFEST"
sed -i "s|<creationDate>[^<]*</creationDate>|<creationDate>$(date +%Y-%m-%d)</creationDate>|" "$MANIFEST"
fi
# Update README version table
sed -i "s/|\s*\*\*Version\*\*\s*|[^|]*/| **Version** | ${VERSION} /" README.md 2>/dev/null || true
fi
php /tmp/moko-platform-api/cli/version_set_platform.php \
--path . --version "$VERSION" --branch main
# -- STEP 4: Update version badges ----------------------------------------
- name: "Step 4: Update version badges"
if: >-
steps.version.outputs.skip != 'true' &&
steps.check.outputs.already_released != 'true'
if: steps.version.outputs.skip != 'true'
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
find . -name "*.md" ! -path "./.git/*" ! -path "./vendor/*" | while read -r f; do
if grep -q '\[VERSION:' "$f" 2>/dev/null; then
sed -i "s/\[VERSION:[[:space:]]*[0-9]\{2\}\.[0-9]\{2\}\.[0-9]\{2\}\]/[VERSION: ${VERSION}]/" "$f"
fi
done
php /tmp/moko-platform-api/cli/badge_update.php --path . --version "${VERSION}" 2>/dev/null || true
# -- STEP 5: Write updates.xml (Joomla update server) ---------------------
- name: "Step 5: Write update stream"
id: updates
if: >-
steps.version.outputs.skip != 'true' &&
steps.check.outputs.already_released != 'true'
steps.platform.outputs.platform == 'joomla'
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
REPO="${{ github.repository }}"
php /tmp/moko-platform-api/cli/updates_xml_build.php \
--path . --version "${VERSION}" --stability stable \
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
--github-output
# -- Parse extension metadata from XML manifest ----------------
# Check for Joomla <extension> or Community Builder <cbplugin>
MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
CB_MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<cbplugin' {} \; 2>/dev/null | head -1)
IS_CB="false"
if [ -z "$MANIFEST" ] && [ -z "$CB_MANIFEST" ]; then
echo "Warning: No Joomla/CB XML manifest found — skipping updates.xml" >> $GITHUB_STEP_SUMMARY
exit 0
fi
# CB plugin takes precedence if no standard manifest
if [ -z "$MANIFEST" ] && [ -n "$CB_MANIFEST" ]; then
MANIFEST="$CB_MANIFEST"
IS_CB="true"
fi
# Extract fields using sed (portable — no grep -P)
EXT_NAME=$(sed -n 's/.*<name>\([^<]*\)<\/name>.*/\1/p' "$MANIFEST" | head -1)
if [ "$IS_CB" = "true" ]; then
EXT_TYPE="cb_plugin"
EXT_ELEMENT=$(sed -n 's/.*<cbplugin[^>]*plugin="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
EXT_CLIENT=""
EXT_FOLDER="cb"
else
EXT_TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
EXT_ELEMENT=$(sed -n 's/.*<element>\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" | head -1)
EXT_CLIENT=$(sed -n 's/.*<extension[^>]*client="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
EXT_FOLDER=$(sed -n 's/.*<extension[^>]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
fi
TARGET_PLATFORM=$(sed -n 's/.*\(<targetplatform[^/]*\/>\).*/\1/p' "$MANIFEST" | head -1)
PHP_MINIMUM=$(sed -n 's/.*<php_minimum>\([^<]*\)<\/php_minimum>.*/\1/p' "$MANIFEST" | head -1)
# If EXT_NAME is a language key (e.g. PLG_SYSTEM_MOKOJGDPC), resolve from .ini
if echo "$EXT_NAME" | grep -qE '^[A-Z_]+$'; then
INI_NAME=$(find . -name "*.sys.ini" -path "*/en-GB/*" -exec grep -h "^${EXT_NAME}=" {} \; 2>/dev/null | head -1 | cut -d'"' -f2)
[ -z "$INI_NAME" ] && INI_NAME=$(find . -name "*.sys.ini" -exec grep -h "^${EXT_NAME}=" {} \; 2>/dev/null | head -1 | cut -d'"' -f2)
[ -n "$INI_NAME" ] && EXT_NAME="$INI_NAME"
fi
# Fallbacks
[ -z "$EXT_NAME" ] && EXT_NAME="${{ github.event.repository.name }}"
[ -z "$EXT_TYPE" ] && EXT_TYPE="component"
# Derive element if not in manifest:
# 1. plugin="xxx" attribute (plugins)
# 2. module="xxx" attribute (modules)
# 3. XML filename (components, packages)
# 4. Repo name fallback (templates, anything else)
if [ -z "$EXT_ELEMENT" ]; then
EXT_ELEMENT=$(sed -n 's/.*plugin="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
fi
if [ -z "$EXT_ELEMENT" ]; then
EXT_ELEMENT=$(sed -n 's/.*module="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
fi
if [ -z "$EXT_ELEMENT" ]; then
FNAME=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]')
# If filename is generic (templateDetails, manifest), use repo name
case "$FNAME" in
templatedetails|manifest) EXT_ELEMENT=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;;
*) EXT_ELEMENT="$FNAME" ;;
esac
fi
# Final fallback
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
# Save for Steps 7, 8, 8b
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
echo "ext_name=${EXT_NAME}" >> "$GITHUB_OUTPUT"
echo "ext_type=${EXT_TYPE}" >> "$GITHUB_OUTPUT"
echo "ext_folder=${EXT_FOLDER}" >> "$GITHUB_OUTPUT"
# Build client tag: plugins and frontend modules need <client>site</client>
CLIENT_TAG=""
if [ -n "$EXT_CLIENT" ]; then
CLIENT_TAG="<client>${EXT_CLIENT}</client>"
elif [ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]; then
CLIENT_TAG="<client>site</client>"
fi
# Build folder tag for plugins (required for Joomla to match the update)
FOLDER_TAG=""
if [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ]; then
FOLDER_TAG="<folder>${EXT_FOLDER}</folder>"
fi
# Build targetplatform (fallback to Joomla 5 if not in manifest)
if [ -z "$TARGET_PLATFORM" ]; then
TARGET_PLATFORM=$(printf '<targetplatform name="joomla" version="((5.[0-9])|(6.[0-9]))" %s>' "/")
fi
# Build php_minimum tag
PHP_TAG=""
if [ -n "$PHP_MINIMUM" ]; then
PHP_TAG="<php_minimum>${PHP_MINIMUM}</php_minimum>"
fi
# Build TYPE_PREFIX for download URL
TYPE_PREFIX=""
case "${EXT_TYPE}" in
plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;;
module) TYPE_PREFIX="mod_" ;;
component) TYPE_PREFIX="com_" ;;
template) TYPE_PREFIX="tpl_" ;;
library) TYPE_PREFIX="lib_" ;;
package) TYPE_PREFIX="pkg_" ;;
cb_plugin) TYPE_PREFIX="cb_" ;;
esac
DOWNLOAD_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/stable/${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip"
INFO_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/stable"
# -- Build update entry for a given stability tag
build_entry() {
local TAG_NAME="$1"
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><tag>${TAG_NAME}</tag></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>'
}
# -- Write updates.xml with cascading channels
# Stable release updates ALL channels (development, alpha, beta, rc, stable)
{
printf '%s\n' "<?xml version='1.0' encoding='UTF-8'?>"
printf '%s\n' "<!-- Copyright (C) $(date +%Y) Moko Consulting <hello@mokoconsulting.tech>"
printf '%s\n' " SPDX-License-Identifier: GPL-3.0-or-later"
printf '%s\n' " VERSION: ${VERSION}"
printf '%s\n' " -->"
printf '%s\n' ""
printf '%s\n' '<updates>'
build_entry "development"
build_entry "alpha"
build_entry "beta"
build_entry "rc"
build_entry "stable"
printf '%s\n' '</updates>'
} > updates.xml
echo "updates.xml: ${VERSION} (all channels updated to stable)" >> $GITHUB_STEP_SUMMARY
# -- Commit all changes ---------------------------------------------------
- name: Commit release changes
if: >-
steps.version.outputs.skip != 'true' &&
@@ -592,7 +335,7 @@ jobs:
fi
[ -z "$EXT_NAME" ] && EXT_NAME="${GITEA_REPO}"
NOTES=$(php /tmp/mokostandards-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null || true)
NOTES=$(php /tmp/moko-platform-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null)
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
# Build release name: "Pretty Name VERSION (type_element-VERSION)"
@@ -604,7 +347,6 @@ jobs:
template) TYPE_PREFIX="tpl_" ;;
library) TYPE_PREFIX="lib_" ;;
package) TYPE_PREFIX="pkg_" ;;
cb_plugin) TYPE_PREFIX="cb_" ;;
esac
RELEASE_NAME="${EXT_NAME} ${VERSION} (${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION})"
@@ -675,7 +417,6 @@ jobs:
template) TYPE_PREFIX="tpl_" ;;
library) TYPE_PREFIX="lib_" ;;
package) TYPE_PREFIX="pkg_" ;;
cb_plugin) TYPE_PREFIX="cb_" ;;
esac
ZIP_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip"
TAR_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.tar.gz"
@@ -683,19 +424,18 @@ jobs:
# -- Build install packages from src/ ----------------------------
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
[ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ — skipping package"; exit 0; }
[ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/"; exit 0; }
EXCLUDES=".ftpignore sftp-config* *.ppk *.pem *.key .env*"
# ZIP package (type-aware via moko-platform PHP API)
php /tmp/moko-platform-api/cli/joomla_build.php --path . --version "${VERSION}" --output /tmp
# Match the expected ZIP_NAME for upload
BUILT_ZIP=$(ls /tmp/${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip 2>/dev/null | head -1 || true)
if [ -n "$BUILT_ZIP" ] && [ "$BUILT_ZIP" != "/tmp/${ZIP_NAME}" ]; then
mv "$BUILT_ZIP" "/tmp/${ZIP_NAME}"
fi
# ZIP package
cd "$SOURCE_DIR"
zip -r "/tmp/${ZIP_NAME}" . -x $EXCLUDES
cd ..
# tar.gz package
tar -czf "/tmp/${TAR_NAME}" -C "$SOURCE_DIR" \
--exclude='.ftpignore' --exclude='sftp-config*' \
--exclude='*.ppk' --exclude='*.pem' --exclude='*.key' --exclude='.env*' .
# tar.gz package (flat source archive)
tar -czf "/tmp/${TAR_NAME}" -C "$SOURCE_DIR" --exclude='.ftpignore' --exclude='sftp-config*' --exclude='*.ppk' --exclude='*.pem' --exclude='*.key' --exclude='.env*' .
ZIP_SIZE=$(stat -c%s "/tmp/${ZIP_NAME}" 2>/dev/null || stat -f%z "/tmp/${ZIP_NAME}" 2>/dev/null || echo "unknown")
TAR_SIZE=$(stat -c%s "/tmp/${TAR_NAME}" 2>/dev/null || stat -f%z "/tmp/${TAR_NAME}" 2>/dev/null || echo "unknown")
@@ -838,7 +578,6 @@ jobs:
template) TYPE_PREFIX="tpl_" ;;
library) TYPE_PREFIX="lib_" ;;
package) TYPE_PREFIX="pkg_" ;;
cb_plugin) TYPE_PREFIX="cb_" ;;
esac
ZIP_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip"
TAR_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.tar.gz"
@@ -886,105 +625,95 @@ jobs:
" 2>/dev/null && echo "Release body updated with changelog + SHA" >> $GITHUB_STEP_SUMMARY
fi
# -- (GitHub mirror removed — Gitea is the sole release platform) ----------
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
- name: "Step 9: Mirror release to GitHub"
if: >-
steps.version.outputs.skip != 'true' &&
steps.version.outputs.stability == 'stable' &&
secrets.GH_TOKEN != ''
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
MAJOR="${{ steps.version.outputs.major }}"
BRANCH="${{ steps.version.outputs.branch }}"
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
NOTES=$(php /tmp/moko-platform-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null || true)
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
echo "$NOTES" > /tmp/release_notes.md
EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/tags/$RELEASE_TAG" 2>/dev/null | jq -r ".tag_name // empty" || true)
if [ -z "$EXISTING" ]; then
gh release create "$RELEASE_TAG" \
--repo "$GH_REPO" \
--title "v${MAJOR} (latest: ${VERSION})" \
--notes-file /tmp/release_notes.md \
--target "$BRANCH" || true
else
gh release edit "$RELEASE_TAG" \
--repo "$GH_REPO" \
--title "v${MAJOR} (latest: ${VERSION})" || true
fi
# Upload assets to GitHub mirror
for PKG in /tmp/${EXT_ELEMENT:-pkg}-${VERSION}.*; do
if [ -f "$PKG" ]; then
_RELID=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/tags/$RELEASE_TAG" 2>/dev/null | jq -r ".id // empty")
[ -n "$_RELID" ] && curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" -H "Content-Type: application/octet-stream" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/${_RELID}/assets?name=$(basename $PKG)" --data-binary "@$PKG" > /dev/null 2>&1 || true
fi
done
echo "GitHub mirror updated: ${GH_REPO} ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
# -- STEP 10: Sync main branch to GitHub mirror ----------------------------
- name: "Step 10: Push main to GitHub mirror"
if: >-
steps.version.outputs.skip != 'true' &&
secrets.GH_TOKEN != ''
continue-on-error: true
run: |
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
git remote add github "https://x-access-token:${{ secrets.GH_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
git remote set-url github "https://x-access-token:${{ secrets.GH_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
git fetch origin main --depth=1
git push github origin/main:refs/heads/main --force 2>/dev/null \
&& echo "main branch pushed to GitHub mirror" \
|| echo "WARNING: GitHub mirror push failed"
# -- Clean up lesser pre-releases (cascade) ---------------------------------
# stable → deletes all | rc → beta,alpha,dev | beta → alpha,dev | alpha → dev
- name: "Delete lesser pre-release channels"
continue-on-error: true
run: |
php /tmp/moko-platform-api/cli/release_cascade.php \
--stability stable \
--token "${{ secrets.GA_TOKEN }}" \
--org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
--gitea-url "${GITEA_URL}" 2>/dev/null || true
- name: "Step 11: Delete and recreate dev branch from main"
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.GA_TOKEN }}"
# Stable deletes all pre-release channels
TAGS_TO_DELETE="development alpha beta release-candidate"
DELETED=0
for TAG in $TAGS_TO_DELETE; do
RELEASE_ID=$(curl -sS -H "Authorization: token ${TOKEN}" \
"${API_BASE}/releases/tags/${TAG}" 2>/dev/null | \
python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "None" ]; then
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
"${API_BASE}/releases/${RELEASE_ID}" 2>/dev/null || true
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
"${API_BASE}/tags/${TAG}" 2>/dev/null || true
echo "Deleted: ${TAG} (id: ${RELEASE_ID})"
DELETED=$((DELETED + 1))
fi
done
echo "Cleaned up ${DELETED} pre-release channel(s)" >> $GITHUB_STEP_SUMMARY
# -- STEP 11: Reset dev branch and bump to next minor -------------------------
- name: "Step 11: Reset dev and bump to next minor"
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.GA_TOKEN }}"
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
PLATFORM="${{ steps.platform.outputs.platform }}"
# Delete dev branch
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
"${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
# Recreate dev from main
# Recreate dev from main (now includes version bump + changelog promotion)
curl -sf -X POST -H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
"${API_BASE}/branches" \
-d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
# Calculate next minor version for dev
MAJOR=$((10#$(echo "$VERSION" | cut -d. -f1)))
MINOR=$((10#$(echo "$VERSION" | cut -d. -f2)))
MINOR=$((MINOR + 1))
if [ $MINOR -gt 99 ]; then
MINOR=0
MAJOR=$((MAJOR + 1))
fi
NEXT=$(printf "%02d.%02d.00" $MAJOR $MINOR)
# Bump version on dev via API (README + manifest)
# Update README.md on dev
README_RESP=$(curl -sf -H "Authorization: token ${TOKEN}" "${API_BASE}/contents/README.md?ref=dev" 2>/dev/null || true)
README_SHA=$(echo "$README_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
README_CONTENT=$(echo "$README_RESP" | python3 -c "import sys,json,base64; print(base64.b64decode(json.load(sys.stdin).get('content','')).decode())" 2>/dev/null || true)
if [ -n "$README_SHA" ] && [ -n "$README_CONTENT" ]; then
UPDATED=$(echo "$README_CONTENT" | sed "s/${VERSION}/${NEXT}/g")
ENCODED=$(echo "$UPDATED" | base64 -w0)
curl -sf -X PUT -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" \
"${API_BASE}/contents/README.md" \
-d "$(python3 -c "import json; print(json.dumps({'content':'${ENCODED}','sha':'${README_SHA}','message':'chore(version): bump ${VERSION} → ${NEXT} (dev) [skip ci]','branch':'dev'}))")" > /dev/null 2>&1 || true
fi
# Update manifest on dev (Joomla or Dolibarr)
case "$PLATFORM" in
joomla)
MANIFEST_PATH="${{ steps.platform.outputs.manifest }}"
[ -n "$MANIFEST_PATH" ] && MANIFEST_PATH=$(echo "$MANIFEST_PATH" | sed 's|^\./||')
if [ -n "$MANIFEST_PATH" ]; then
ENCODED_PATH=$(python3 -c "import urllib.parse; print(urllib.parse.quote('${MANIFEST_PATH}'))")
MF_RESP=$(curl -sf -H "Authorization: token ${TOKEN}" "${API_BASE}/contents/${ENCODED_PATH}?ref=dev" 2>/dev/null || true)
MF_SHA=$(echo "$MF_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
MF_CONTENT=$(echo "$MF_RESP" | python3 -c "import sys,json,base64; print(base64.b64decode(json.load(sys.stdin).get('content','')).decode())" 2>/dev/null || true)
if [ -n "$MF_SHA" ] && [ -n "$MF_CONTENT" ]; then
UPDATED=$(echo "$MF_CONTENT" | sed "s|<version>${VERSION}</version>|<version>${NEXT}</version>|")
ENCODED=$(echo "$UPDATED" | base64 -w0)
curl -sf -X PUT -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" \
"${API_BASE}/contents/${ENCODED_PATH}" \
-d "$(python3 -c "import json; print(json.dumps({'content':'${ENCODED}','sha':'${MF_SHA}','message':'chore(version): bump ${VERSION} → ${NEXT} (dev) [skip ci]','branch':'dev'}))")" > /dev/null 2>&1 || true
fi
fi
;;
dolibarr)
# Dolibarr handled by separate step below
;;
esac
echo "Dev branch bumped to ${NEXT}" >> $GITHUB_STEP_SUMMARY
echo "Dev branch reset from main (keeps dev ahead after release)" >> $GITHUB_STEP_SUMMARY
# -- Dolibarr post-release: Reset dev version -----------------------------
@@ -4,8 +4,8 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Maintenance
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
# INGROUP: moko-platform.Maintenance
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# PATH: /templates/workflows/cascade-dev.yml.template
# VERSION: 02.00.00
# BRIEF: Forward-merge main → all open branches after every push to main
@@ -4,8 +4,8 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Maintenance
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# INGROUP: moko-platform.Maintenance
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /.gitea/workflows/cleanup.yml
# VERSION: 01.00.00
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
@@ -4,8 +4,8 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Security
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
# INGROUP: moko-platform.Security
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# PATH: /templates/workflows/gitleaks.yml.template
# VERSION: 01.00.00
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
@@ -4,8 +4,8 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Notifications
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# INGROUP: moko-platform.Notifications
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /.gitea/workflows/notify.yml
# VERSION: 01.00.00
# BRIEF: Push notifications via ntfy on release success or workflow failure
@@ -18,7 +18,6 @@ on:
- "Joomla Build & Release"
- "Joomla Extension CI"
- "Deploy"
- "Cascade Main → Dev"
types:
- completed
@@ -4,8 +4,8 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.CI
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
# INGROUP: moko-platform.CI
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# PATH: /templates/workflows/universal/pr-check.yml.template
# VERSION: 05.00.00
# BRIEF: PR gate — branch policy + code validation before merge
@@ -108,8 +108,9 @@ jobs:
- name: Detect platform
id: platform
run: |
# Parse manifest for platform detection
PLATFORM=$(php /tmp/mokostandards-api/cli/manifest_read.php --path . --field platform 2>/dev/null)
# Read platform from XML manifest (<platform> tag) or plain text fallback
PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1)
[ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]')
[ -z "$PLATFORM" ] && PLATFORM="generic"
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
@@ -193,32 +194,3 @@ jobs:
FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
echo "Source: ${FILE_COUNT} files"
[ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; }
# ── Changelog Gate ────────────────────────────────────────────────────
changelog:
name: Changelog Updated
runs-on: ubuntu-latest
if: github.base_ref == 'main'
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check CHANGELOG.md was updated
run: |
BASE="${{ github.event.pull_request.base.sha }}"
HEAD="${{ github.event.pull_request.head.sha }}"
if git diff --name-only "$BASE" "$HEAD" | grep -q "^CHANGELOG.md$"; then
echo "CHANGELOG.md updated"
else
# Allow [skip changelog] in PR title or body
PR_TITLE="${{ github.event.pull_request.title }}"
PR_BODY="${{ github.event.pull_request.body }}"
if echo "$PR_TITLE $PR_BODY" | grep -qi "\[skip changelog\]"; then
echo "::warning::Changelog skip requested via [skip changelog]"
exit 0
fi
echo "::error::CHANGELOG.md must be updated before merging to main. Add [skip changelog] to the PR title to bypass."
exit 1
fi
+246
View File
@@ -0,0 +1,246 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /templates/workflows/universal/pre-release.yml.template
# VERSION: 05.00.00
# BRIEF: Manual pre-release — builds dev/alpha/beta/rc packages from any branch
name: "Universal: Pre-Release"
on:
workflow_dispatch:
inputs:
stability:
description: 'Pre-release channel'
required: true
type: choice
options:
- development
- alpha
- beta
- release-candidate
permissions:
contents: write
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:
build:
name: "Build Pre-Release (${{ inputs.stability }})"
runs-on: release
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GA_TOKEN }}
- name: Setup PHP
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 >/dev/null 2>&1
fi
- name: Setup moko-platform tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
run: |
git clone --depth 1 --branch main --quiet "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" /tmp/moko-platform-api
- name: Detect platform
id: platform
run: |
php /tmp/moko-platform-api/cli/manifest_read.php --path . --github-output
- name: Resolve metadata
id: meta
run: |
STABILITY="${{ inputs.stability }}"
MOKO_API="/tmp/moko-platform-api/cli"
case "$STABILITY" in
development) SUFFIX="-dev"; TAG="development" ;;
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
beta) SUFFIX="-beta"; TAG="beta" ;;
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
esac
# Bump patch version
BUMP_OUTPUT=$(php ${MOKO_API}/version_bump.php --path .)
VERSION=$(echo "$BUMP_OUTPUT" | grep -oP '\d{2}\.\d{2}\.\d{2}$' || true)
[ -z "$VERSION" ] && VERSION=$(php ${MOKO_API}/version_read.php --path .)
echo "Version: ${VERSION}"
# Update platform-specific manifest
php ${MOKO_API}/version_set_platform.php --path . --version "${VERSION}"
# Commit version bump
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
git add -A
git diff --cached --quiet || {
git commit -m "chore(version): bump to ${VERSION} [skip ci]"
git push origin HEAD 2>&1
}
# Detect element from Joomla/Dolibarr manifest
PLATFORM="${{ steps.platform.outputs.platform }}"
EXT_ELEMENT=$(php ${MOKO_API}/manifest_read.php --path . --field name 2>/dev/null | tr -d ' ' | tr '[:upper:]' '[:lower:]' || true)
# For Joomla, prefer <element> tag
if [ "$PLATFORM" = "joomla" ]; then
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true)
if [ -n "$MANIFEST" ]; then
ELEM=$(grep -oP "<element>\K[^<]+" "$MANIFEST" 2>/dev/null | head -1)
[ -n "$ELEM" ] && EXT_ELEMENT="$ELEM"
fi
fi
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
ZIP_NAME="${EXT_ELEMENT}-${VERSION}${SUFFIX}.zip"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
- name: Build package
id: zip
run: |
VERSION="${{ steps.meta.outputs.version }}"
SUFFIX="${{ steps.meta.outputs.suffix }}"
PLATFORM="${{ steps.platform.outputs.platform }}"
if [ "$PLATFORM" = "joomla" ]; then
php /tmp/moko-platform-api/cli/joomla_build.php --path . --version "${VERSION}" --suffix "${SUFFIX}" --output build --github-output
else
# Generic build: zip src/ directory
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
[ ! -d "$SOURCE_DIR" ] && { echo "::error::No src/ or htdocs/"; exit 1; }
EXT_ELEMENT="${{ steps.meta.outputs.ext_element }}"
ZIP_NAME="${EXT_ELEMENT}-${VERSION}${SUFFIX}.zip"
mkdir -p build
cd "$SOURCE_DIR" && zip -r "../build/${ZIP_NAME}" . && cd ..
SHA256=$(sha256sum "build/${ZIP_NAME}" | cut -d' ' -f1)
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
echo "zip_path=build/${ZIP_NAME}" >> "$GITHUB_OUTPUT"
echo "sha256=${SHA256}" >> "$GITHUB_OUTPUT"
fi
- name: Create or replace Gitea release
id: release
continue-on-error: true
run: |
TAG="${{ steps.meta.outputs.tag }}"
VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
SHA256="${{ steps.zip.outputs.sha256 }}"
ZIP_NAME="${{ steps.zip.outputs.zip_name }}"
EXT_ELEMENT="${{ steps.meta.outputs.ext_element }}"
TOKEN="${{ secrets.GA_TOKEN }}"
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
BRANCH=$(git branch --show-current)
BODY="## ${VERSION} ($(date +%Y-%m-%d))
**Channel:** ${STABILITY}
**SHA-256:** \`${SHA256}\`"
# Delete existing release
EXISTING_ID=$(curl -sS -H "Authorization: token ${TOKEN}" \
"${API}/releases/tags/${TAG}" | jq -r '.id // empty' 2>/dev/null)
if [ -n "$EXISTING_ID" ]; then
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
"${API}/releases/${EXISTING_ID}" 2>/dev/null || true
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
"${API}/tags/${TAG}" 2>/dev/null || true
fi
# Create release
RELEASE_ID=$(curl -sS -X POST -H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
"${API}/releases" \
-d "$(jq -n \
--arg tag "$TAG" \
--arg target "$BRANCH" \
--arg name "${EXT_ELEMENT} ${VERSION} (${STABILITY})" \
--arg body "$BODY" \
'{tag_name: $tag, target_commitish: $target, name: $name, body: $body, prerelease: true}'
)" | jq -r '.id')
echo "release_id=${RELEASE_ID}" >> "$GITHUB_OUTPUT"
# Upload ZIP
curl -sS -X POST -H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/octet-stream" \
"${API}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" \
--data-binary "@${{ steps.zip.outputs.zip_path }}"
echo "Released: ${EXT_ELEMENT} ${VERSION} (${STABILITY})"
- name: "Update updates.xml"
if: steps.platform.outputs.platform == 'joomla'
run: |
VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
SHA256="${{ steps.zip.outputs.sha256 }}"
php /tmp/moko-platform-api/cli/updates_xml_build.php --path . --version "$VERSION" --stability "$STABILITY" --sha "$SHA256" --gitea-url "$GITEA_URL" --org "$GITEA_ORG" --repo "$GITEA_REPO"
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: |
php /tmp/moko-platform-api/cli/updates_xml_sync.php --path . --current "${{ github.ref_name }}" --branches main,dev --version "${{ steps.meta.outputs.version }}" --token "${{ secrets.GA_TOKEN }}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" --gitea-url "${GITEA_URL}"
- name: "Delete lesser pre-release channels (cascade)"
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.GA_TOKEN }}"
STABILITY="${{ steps.meta.outputs.stability }}"
# Cascade: rc → beta,alpha,dev | beta → alpha,dev | alpha → dev | dev → nothing
case "$STABILITY" in
release-candidate) TAGS_TO_DELETE="beta alpha development" ;;
beta) TAGS_TO_DELETE="alpha development" ;;
alpha) TAGS_TO_DELETE="development" ;;
*) TAGS_TO_DELETE="" ;;
esac
[ -z "$TAGS_TO_DELETE" ] && exit 0
for TAG in $TAGS_TO_DELETE; do
RELEASE_ID=$(curl -sS -H "Authorization: token ${TOKEN}" \
"${API_BASE}/releases/tags/${TAG}" 2>/dev/null | \
python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "None" ]; then
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
"${API_BASE}/releases/${RELEASE_ID}" 2>/dev/null || true
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
"${API_BASE}/tags/${TAG}" 2>/dev/null || true
echo "Deleted: ${TAG} (id: ${RELEASE_ID})"
fi
done
@@ -7,18 +7,14 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Validation
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
# INGROUP: moko-platform.Validation
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# PATH: /templates/workflows/joomla/repo_health.yml.template
# VERSION: 04.06.00
# BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts.
# ============================================================================
name: "Joomla: Repo Health"
concurrency:
group: repo-health-${{ github.repository }}-${{ github.ref }}
cancel-in-progress: true
name: "Generic: Repo Health"
defaults:
run:
@@ -288,7 +284,7 @@ jobs:
exit 0
fi
IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}"
if [ -n "${SCRIPTS_REQUIRED_DIRS:-}" ]; then IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}"; else required_dirs=(); fi
IFS=',' read -r -a allowed_dirs <<< "${SCRIPTS_ALLOWED_DIRS}"
missing_dirs=()
@@ -392,23 +388,27 @@ jobs:
exit 0
fi
# Source directory: src/ or htdocs/ (either is valid)
IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}"
IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}"
if [ -n "${REPO_DISALLOWED_DIRS:-}" ]; then IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}"; else disallowed_dirs=(); fi
IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES:-}"
missing_required=()
missing_optional=()
# Source directory: src/ or htdocs/ (either is valid for extension repos)
SOURCE_DIR=""
if [ -d "src" ]; then
SOURCE_DIR="src"
elif [ -d "htdocs" ]; then
SOURCE_DIR="htdocs"
elif [ -d "deploy" ] || [ -d "cli" ] || [ -d "monitoring" ]; then
# Platform/tooling repos don't need src/
SOURCE_DIR=""
else
missing_required+=("src/ or htdocs/ (source directory required)")
fi
IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}"
IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}"
IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}"
IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES}"
missing_required=()
missing_optional=()
for item in "${required_artifacts[@]}"; do
if printf '%s' "${item}" | grep -q '/$'; then
d="${item%/}"
@@ -450,12 +450,8 @@ jobs:
fi
done < <(git branch -r --list 'origin/dev*' | sed 's/^ *//')
if [ "${#dev_paths[@]}" -eq 0 ]; then
missing_required+=("dev/* branch (e.g. dev/01.00.00)")
fi
if [ "${#dev_branches[@]}" -gt 0 ]; then
missing_required+=("invalid branch dev (must be dev/<version>)")
if [ "${#dev_paths[@]}" -eq 0 ] && [ "${#dev_branches[@]}" -eq 0 ]; then
missing_required+=("dev or dev/* branch")
fi
content_warnings=()
@@ -481,26 +477,7 @@ jobs:
export MISSING_OPTIONAL="$(printf '%s\n' "${missing_optional[@]:-}")"
export CONTENT_WARNINGS="$(printf '%s\n' "${content_warnings[@]:-}")"
report_json="$(python3 - <<'PY'
import json
import os
profile = os.environ.get('PROFILE_RAW') or 'all'
missing_required = os.environ.get('MISSING_REQUIRED', '').splitlines() if os.environ.get('MISSING_REQUIRED') else []
missing_optional = os.environ.get('MISSING_OPTIONAL', '').splitlines() if os.environ.get('MISSING_OPTIONAL') else []
content_warnings = os.environ.get('CONTENT_WARNINGS', '').splitlines() if os.environ.get('CONTENT_WARNINGS') else []
out = {
'profile': profile,
'missing_required': [x for x in missing_required if x],
'missing_optional': [x for x in missing_optional if x],
'content_warnings': [x for x in content_warnings if x],
}
print(json.dumps(out, indent=2))
PY
)"
report_json=$(printf '{"profile":"%s","missing_required":%d,"missing_optional":%d,"content_warnings":%d}' "$profile" "${#missing_required[@]}" "${#missing_optional[@]}" "${#content_warnings[@]}")
{
printf '%s\n' '### Repository health'
@@ -578,12 +555,14 @@ jobs:
joomla_findings+=("updates.xml missing in root (required for Joomla update server)")
fi
INDEX_DIRS=("${SOURCE_DIR}" "${SOURCE_DIR}/admin" "${SOURCE_DIR}/site")
for dir in "${INDEX_DIRS[@]}"; do
if [ -d "${dir}" ] && [ ! -f "${dir}/index.html" ]; then
joomla_findings+=("${dir}/index.html missing (directory listing protection)")
fi
done
if [ -n "${SOURCE_DIR}" ]; then
INDEX_DIRS=("${SOURCE_DIR}" "${SOURCE_DIR}/admin" "${SOURCE_DIR}/site")
for dir in "${INDEX_DIRS[@]}"; do
if [ -d "${dir}" ] && [ ! -f "${dir}/index.html" ]; then
joomla_findings+=("${dir}/index.html missing (directory listing protection)")
fi
done
fi
if [ "${#joomla_findings[@]}" -gt 0 ]; then
{
@@ -629,43 +608,29 @@ jobs:
fi
if [ -f "${DOCS_INDEX}" ]; then
missing_links="$(python3 - <<'PY'
import os
import re
idx = os.environ.get('DOCS_INDEX', 'docs/docs-index.md')
base = os.getcwd()
bad = []
pat = re.compile(r'\[[^\]]+\]\(([^)]+)\)')
with open(idx, 'r', encoding='utf-8') as f:
for line in f:
for m in pat.findall(line):
link = m.strip()
if link.startswith('http://') or link.startswith('https://') or link.startswith('#') or link.startswith('mailto:'):
continue
if link.startswith('/'):
rel = link.lstrip('/')
else:
rel = os.path.normpath(os.path.join(os.path.dirname(idx), link))
rel = rel.split('#', 1)[0]
rel = rel.split('?', 1)[0]
if not rel:
continue
p = os.path.join(base, rel)
if not os.path.exists(p):
bad.append(rel)
print('\n'.join(sorted(set(bad))))
PY
)"
missing_links=""
while IFS= read -r docline; do
for link in $(echo "$docline" | grep -oE '\]\([^)]+\)' | sed 's/\](//' | sed 's/)$//' || true); do
case "$link" in http://*|https://*|"#"*|mailto:*) continue ;; esac
linkpath="${link%%#*}"
linkpath="${linkpath%%\?*}"
[ -z "$linkpath" ] && continue
if [ "${linkpath:0:1}" = "/" ]; then
testpath="${linkpath#/}"
else
testpath="$(dirname "${DOCS_INDEX}")/${linkpath}"
fi
[ ! -e "$testpath" ] && missing_links="${missing_links}${testpath} "
done
done < "${DOCS_INDEX}"
if [ -n "${missing_links}" ]; then
extended_findings+=("docs/docs-index.md contains broken relative links")
{
printf '%s\n' '### Docs index link integrity'
printf '%s\n' 'Broken relative links:'
while IFS= read -r l; do [ -n "${l}" ] && printf '%s\n' "- ${l}"; done <<< "${missing_links}"
for bl in ${missing_links}; do
printf '%s\n' "- ${bl}"
done
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
@@ -764,3 +729,41 @@ jobs:
fi
printf '%s\n' 'Repository health guardrails passed.' >> "${GITHUB_STEP_SUMMARY}"
site-health:
name: Site Health
runs-on: ubuntu-latest
if: github.event_name == 'workflow_dispatch'
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
- name: Uptime check
if: env.URLS != ''
run: |
echo "$URLS" > /tmp/urls.txt
php monitoring/uptime-probe.php --urls /tmp/urls.txt --timeout 15 || echo "::warning::Some sites are down"
rm -f /tmp/urls.txt
env:
URLS: ${{ vars.MONITORED_URLS }}
- name: SSL certificate check
if: env.DOMAINS != ''
run: |
echo "$DOMAINS" > /tmp/domains.txt
php monitoring/ssl-check.php --domains /tmp/domains.txt --warn-days 30 || echo "::warning::SSL certificates expiring soon"
rm -f /tmp/domains.txt
env:
DOMAINS: ${{ vars.MONITORED_DOMAINS }}
- name: Summary
if: always()
run: |
echo "### Site Health" >> $GITHUB_STEP_SUMMARY
echo "Uptime and SSL checks completed." >> $GITHUB_STEP_SUMMARY
@@ -4,8 +4,8 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Security
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# INGROUP: moko-platform.Security
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /.gitea/workflows/security-audit.yml
# VERSION: 01.00.00
# BRIEF: Dependency vulnerability scanning for composer and npm packages
@@ -80,3 +80,19 @@ jobs:
-H "Priority: high" \
-d "Security audit found vulnerabilities. Review dependency updates." \
"${NTFY_URL}/${NTFY_TOPIC}" || true
- name: Joomla version audit
if: always()
run: |
if [ -f "monitoring/joomla-version-audit.php" ] && [ -n "$JOOMLA_SITES" ]; then
echo "$JOOMLA_SITES" > /tmp/sites.json
php monitoring/joomla-version-audit.php --sites /tmp/sites.json || true
echo "### Joomla Version Audit" >> $GITHUB_STEP_SUMMARY
rm -f /tmp/sites.json
else
echo "Joomla audit skipped (no script or JOOMLA_SITES_JSON not configured)"
fi
env:
JOOMLA_SITES: ${{ vars.JOOMLA_SITES_JSON }}
+65 -113
View File
@@ -1,10 +1,4 @@
<!--
## [Unreleased]
## [04.01.00] --- 2026-05-16
## [04.00.00] --- 2026-05-16
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
@@ -27,8 +21,8 @@
# FILE INFORMATION
DEFGROUP: MokoJoomTOS
INGROUP: plg_system_mokojoomtos
REPO: https://github.com/mokoconsulting-tech/MokoJoomTOS
VERSION: 04.00.00
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS
VERSION: 04.02.01
PATH: ./CHANGELOG.md
BRIEF: Version history and release notes
-->
@@ -40,7 +34,52 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [04.00.00] --- 2026-05-16
## [Unreleased]
## [04.02.01] - 2026-05-24
### Fixed
- enablePlugin() now called unconditionally in postflight() (#89)
- Replaced Table::addIncludePath() with bootComponent()->getMVCFactory() for Joomla 5 (#90)
- Added non-SEF URL fallback via Itemid matching (#91)
- enablePlugin() now fires on upgrade path (#92)
- Removed $_GET superglobal mutation (#93)
- Added params, metadata, attribs defaults to article creation (#94)
- Applied urldecode() to URI path before slug comparison (#95)
- Cast Registry return to array before iterating slugs (#96)
- Fixed MenuslugField separator `disable` to `disabled` property (#99)
- Hardcode description in XML manifest (language variables don't resolve during install)
- Synced VERSION header in manifest to 04.02.01 (#105)
- Added SEF_WARNING language key to site-side .ini files (#106)
- Fixed updates.xml version mismatch and Joomla 4.x targetplatform (#107)
- Changed catch(Exception) to catch(Throwable) in script.php and Extension class (#108, #113)
- Fixed dev channel targetplatform to include Joomla 4.x (#111)
- Fixed misleading article duplicate check comment (#98)
### Added
- SEF disabled warning in MenuslugField dropdown (#97)
- Include Children toggle for offline-accessible menu items (defaults to Yes)
- Auto-select default menu slugs (terms-of-service, privacy-policy) on fresh install
### Removed
- Removed deploy-manual.yml workflow — switching to Joomla update server method for extension distribution
### Changed
- Stripped legacy mokojoomtos.php to minimal stub (#101)
- Converted script.php indentation from spaces to tabs (#102)
- Renamed installer class to PlgSystemMokojoomtosInstallerScript (#103)
- Promoted CHANGELOG [Unreleased] to versioned section (#112)
## [04.01.00] - 2026-05-16
### Fixed
- Use literal display name in manifest `<name>` tag so Joomla stores "System - Moko Terms of Service" directly in the DB
- Updated plugin help text to describe multi-select workflow
## [04.00.00] - 2026-05-16
@@ -54,7 +93,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Match against full menu route path instead of alias only (fixes nested routes like `/legal/terms-of-service`)
- Plugin pretty name updated to Joomla convention: "System - Moko Terms of Service"
- Renamed `.mokogitea/` to `.gitea/` for standard Gitea compatibility
- MenuslugField now stores and displays full route paths (e.g., `legal/terms-of-service`)
### Removed
@@ -76,119 +114,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Added Gitea update server URL to plugin manifest (`updates.xml` on `main`)
- Removed obsolete `src/plugins/` legacy directory
- Fixed template chrome loading issue by changing event hook from onAfterInitialise to onAfterRoute
- Component-only view now properly applied when site is offline
### Removed
- Legacy duplicate manifest at `src/plugins/system/mokojoomtos/mokojoomtos.xml`
## [03.09.00] - 2026-02-28
### Changed
- Updated version number to 03.09.00 across all files
- Fixed template chrome loading issue by changing event hook from onAfterInitialise to onAfterRoute
### Fixed
- Template chrome (header, footer, modules) no longer loads when accessing TOS page in offline mode
- Component-only view now properly applied when site is offline
## [1.0.0] - 2026-01-16
### Added
- Initial template repository structure
- Comprehensive Makefile with 30+ build targets
- Complete README.md with usage documentation
- CONTRIBUTING.md with contribution guidelines
- SECURITY.md with security policy and best practices
- CHANGELOG.md for version tracking
- LICENSE file (GPL-3.0-or-later)
- EditorConfig for consistent code formatting
- Documentation structure in `docs/` directory
- Placeholders for component directories (`admin/`, `site/`, `media/`)
- MokoStandards compliance:
- File header standards with copyright and SPDX identifiers
- Joomla coding standards configuration
- PHPStan static analysis setup
- Dependency security auditing
- Makefile targets for:
- Dependency management (install-deps, update-deps)
- Code validation (lint, phpcs, phpstan)
- Testing (test, test-coverage)
- Building (build, build-assets)
- Development (dev-install, watch-assets)
- Release management (release, bump-version)
- Utility commands (clean, version, help)
- Documentation index files with MokoStandards metadata
### Documentation
- Comprehensive README with:
- Quick start guide
- Prerequisites and installation
- Usage examples
- Project structure overview
- Makefile command reference
- Standards compliance information
- Detailed CONTRIBUTING guide with:
- Commit message conventions
- DCO sign-off requirements
- Development setup instructions
- Code review process
- Security policy with:
- Vulnerability reporting procedures
- Security best practices
- Code examples for secure development
- Template structure for Joomla 4.x and 5.x compatibility
## Version Guidelines
### Version Numbering
This template follows [Semantic Versioning](https://semver.org/):
- **MAJOR** version for incompatible changes (e.g., 2.0.0)
- **MINOR** version for new features (e.g., 1.1.0)
- **PATCH** version for bug fixes (e.g., 1.0.1)
### Change Categories
- **Added**: New features
- **Changed**: Changes to existing functionality
- **Deprecated**: Soon-to-be removed features
- **Removed**: Removed features
- **Fixed**: Bug fixes
- **Security**: Security fixes
## Release Notes
### [1.0.0] Release Notes
This is the initial release of the MokoJoomTOS plugin. It provides a production-ready Joomla system plugin for offline TOS access with:
- **Complete build system** via comprehensive Makefile
- **MokoStandards compliance** out of the box
- **Developer-friendly** workflow with automation
- **Security-focused** with built-in best practices
- **Well-documented** with clear usage instructions
This template is ready for use in creating new Joomla components that follow organizational coding standards and best practices.
### Migration Guide
**For New Projects**: Simply use this template to create your new component repository.
**For Existing Projects**: Review the Makefile, documentation structure, and standards compliance files to gradually adopt features that benefit your project.
- Initial plugin release
- Offline mode bypass for configured menu slug
- Auto-provisioning installer (creates article, menu type, menu item)
- Component-only rendering (`tmpl=component`)
- Language files for en-GB and en-US
- MokoStandards compliance
## Links
- [Repository](https://github.com/mokoconsulting-tech/MokoJoomTOS)
- [Issues](https://github.com/mokoconsulting-tech/MokoJoomTOS/issues)
- [Pull Requests](https://github.com/mokoconsulting-tech/MokoJoomTOS/pulls)
- [MokoStandards](https://github.com/mokoconsulting-tech/MokoCodingDefaults)
- [Repository](https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS)
- [Issues](https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/issues)
- [Releases](https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases)
[Unreleased]: https://github.com/mokoconsulting-tech/MokoJoomTOS/compare/v03.09.00...HEAD
[03.09.00]: https://github.com/mokoconsulting-tech/MokoJoomTOS/releases/tag/v03.09.00
[1.0.0]: https://github.com/mokoconsulting-tech/MokoJoomTOS/releases/tag/v1.0.0
[Unreleased]: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/compare/stable...dev
[04.02.01]: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/tag/stable
[04.01.00]: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/compare/v04.01.00...stable
[04.00.00]: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/compare/v03.09.00...stable
[03.09.00]: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/tag/v03.09.00
[1.0.0]: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/tag/v1.0.0
+2 -2
View File
@@ -6,7 +6,7 @@ MokoJoomTOS is a lightweight Joomla 4.x/5.x system plugin that allows Terms of S
```
/
├── .github/ # GitHub workflows, issue templates, copilot-instructions.md
├── .mokogitea/ # GitHub workflows, issue templates, copilot-instructions.md
├── docs/ # Detailed documentation (currently minimal with index.md)
├── scripts/ # Build and utility scripts (validate/, package scripts)
├── src/ # Plugin source code at root level (NOT nested under plugins/)
@@ -469,6 +469,6 @@ This repository follows minimal documentation structure with essential docs in r
2. **SECURITY.md** - Security policy and vulnerability reporting procedures
3. **CODE_OF_CONDUCT.md** - Community standards and behavior expectations
4. **CHANGELOG.md** - Version history following Keep a Changelog format
5. **.github/copilot-instructions.md** - Comprehensive guidance for GitHub Copilot (includes all Joomla patterns)
5. **.mokogitea/copilot-instructions.md** - Comprehensive guidance for GitHub Copilot (includes all Joomla patterns)
Currently no `docs/policy/` directory exists - all policy is in root-level markdown files.
+2 -2
View File
@@ -2,14 +2,14 @@
A Joomla system plugin that keeps your Terms of Service, Privacy Policy, or any legal page accessible to visitors -- even when the site is in offline (maintenance) mode.
![Joomla](https://img.shields.io/badge/Joomla-5.x%20%7C%206.x-blue?style=flat-square&logo=joomla&logoColor=white) ![PHP](https://img.shields.io/badge/PHP-%E2%89%A58.1-777BB4?style=flat-square&logo=php&logoColor=white) ![License](https://img.shields.io/badge/license-GPL--3.0--or--later-green?style=flat-square) ![Version](https://img.shields.io/badge/version-04.01.00-orange?style=flat-square) ![Type](https://img.shields.io/badge/type-system%20plugin-blueviolet?style=flat-square)
![Joomla](https://img.shields.io/badge/Joomla-5.x%20%7C%206.x-blue?style=flat-square&logo=joomla&logoColor=white) ![PHP](https://img.shields.io/badge/PHP-%E2%89%A58.1-777BB4?style=flat-square&logo=php&logoColor=white) ![License](https://img.shields.io/badge/license-GPL--3.0--or--later-green?style=flat-square) ![Version](https://img.shields.io/badge/version-04.02.00-orange?style=flat-square) ![Type](https://img.shields.io/badge/type-system%20plugin-blueviolet?style=flat-square)
| Field | Value |
|---|---|
| **Author** | [Moko Consulting](https://mokoconsulting.tech) |
| **License** | GPL-3.0-or-later |
| **Platform** | [Gitea](https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS) |
| **Version** | 04.01.00 |
| **Version** | 04.02.00 |
---
@@ -11,10 +11,16 @@ PLG_SYSTEM_MOKOJOOMTOS_FIELDSET_BASIC="Basic Settings"
PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_LABEL="Offline-Accessible Menu Items"
PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_DESC="Select one or more menu items that should remain accessible when the site is in offline mode. Hold Ctrl/Cmd to select multiple items."
PLG_SYSTEM_MOKOJOOMTOS_FIELD_INCLUDE_CHILDREN_LABEL="Include Child Menu Items"
PLG_SYSTEM_MOKOJOOMTOS_FIELD_INCLUDE_CHILDREN_DESC="When enabled, child menu items under the selected items will also be accessible during offline mode. For example, selecting 'legal' will also allow access to 'legal/terms-of-service' and 'legal/privacy-policy'."
; Help
PLG_SYSTEM_MOKOJOOMTOS_HELP_LABEL="How to Use This Plugin"
PLG_SYSTEM_MOKOJOOMTOS_HELP_DESC="<strong>Step 1:</strong> Create articles for your legal pages (Terms of Service, Privacy Policy, etc.).<br/><strong>Step 2:</strong> Create menu items pointing to those articles.<br/><strong>Step 3:</strong> Select the menu items above (hold Ctrl/Cmd to select multiple).<br/><strong>Step 4:</strong> When your site goes offline, visitors can still access the selected pages.<br/><br/><em>Tip:</em> The dropdown shows the full URL path for each menu item (e.g., /legal/terms-of-service)."
; Warnings
PLG_SYSTEM_MOKOJOOMTOS_FIELD_SEF_WARNING="⚠ SEF URLs are disabled — path matching requires SEF. Itemid fallback is active."
; Errors
PLG_SYSTEM_MOKOJOOMTOS_ERROR_LOADING_MENU_ITEMS="Error loading menu items: %s"
@@ -11,10 +11,16 @@ PLG_SYSTEM_MOKOJOOMTOS_FIELDSET_BASIC="Basic Settings"
PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_LABEL="Offline-Accessible Menu Items"
PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_DESC="Select one or more menu items that should remain accessible when the site is in offline mode. Hold Ctrl/Cmd to select multiple items."
PLG_SYSTEM_MOKOJOOMTOS_FIELD_INCLUDE_CHILDREN_LABEL="Include Child Menu Items"
PLG_SYSTEM_MOKOJOOMTOS_FIELD_INCLUDE_CHILDREN_DESC="When enabled, child menu items under the selected items will also be accessible during offline mode. For example, selecting 'legal' will also allow access to 'legal/terms-of-service' and 'legal/privacy-policy'."
; Help
PLG_SYSTEM_MOKOJOOMTOS_HELP_LABEL="How to Use This Plugin"
PLG_SYSTEM_MOKOJOOMTOS_HELP_DESC="<strong>Step 1:</strong> Create articles for your legal pages (Terms of Service, Privacy Policy, etc.).<br/><strong>Step 2:</strong> Create menu items pointing to those articles.<br/><strong>Step 3:</strong> Select the menu items above (hold Ctrl/Cmd to select multiple).<br/><strong>Step 4:</strong> When your site goes offline, visitors can still access the selected pages.<br/><br/><em>Tip:</em> The dropdown shows the full URL path for each menu item (e.g., /legal/terms-of-service)."
; Warnings
PLG_SYSTEM_MOKOJOOMTOS_FIELD_SEF_WARNING="⚠ SEF URLs are disabled — path matching requires SEF. Itemid fallback is active."
; Errors
PLG_SYSTEM_MOKOJOOMTOS_ERROR_LOADING_MENU_ITEMS="Error loading menu items: %s"
@@ -11,6 +11,12 @@ PLG_SYSTEM_MOKOJOOMTOS_FIELDSET_BASIC="Basic Settings"
PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_LABEL="Offline-Accessible Menu Items"
PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_DESC="Select one or more menu items that should remain accessible when the site is in offline mode. Hold Ctrl/Cmd to select multiple items."
PLG_SYSTEM_MOKOJOOMTOS_FIELD_INCLUDE_CHILDREN_LABEL="Include Child Menu Items"
PLG_SYSTEM_MOKOJOOMTOS_FIELD_INCLUDE_CHILDREN_DESC="When enabled, child menu items under the selected items will also be accessible during offline mode. For example, selecting 'legal' will also allow access to 'legal/terms-of-service' and 'legal/privacy-policy'."
; Warnings
PLG_SYSTEM_MOKOJOOMTOS_FIELD_SEF_WARNING="⚠ SEF URLs are disabled — path matching requires SEF. Itemid fallback is active."
; Help
PLG_SYSTEM_MOKOJOOMTOS_HELP_LABEL="How to Use This Plugin"
PLG_SYSTEM_MOKOJOOMTOS_HELP_DESC="<strong>Step 1:</strong> Create articles for your legal pages (Terms of Service, Privacy Policy, etc.).<br/><strong>Step 2:</strong> Create menu items pointing to those articles.<br/><strong>Step 3:</strong> Select the menu items above (hold Ctrl/Cmd to select multiple).<br/><strong>Step 4:</strong> When your site goes offline, visitors can still access the selected pages.<br/><br/><em>Tip:</em> The dropdown shows the full URL path for each menu item (e.g., /legal/terms-of-service)."
@@ -11,6 +11,12 @@ PLG_SYSTEM_MOKOJOOMTOS_FIELDSET_BASIC="Basic Settings"
PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_LABEL="Offline-Accessible Menu Items"
PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_DESC="Select one or more menu items that should remain accessible when the site is in offline mode. Hold Ctrl/Cmd to select multiple items."
PLG_SYSTEM_MOKOJOOMTOS_FIELD_INCLUDE_CHILDREN_LABEL="Include Child Menu Items"
PLG_SYSTEM_MOKOJOOMTOS_FIELD_INCLUDE_CHILDREN_DESC="When enabled, child menu items under the selected items will also be accessible during offline mode. For example, selecting 'legal' will also allow access to 'legal/terms-of-service' and 'legal/privacy-policy'."
; Warnings
PLG_SYSTEM_MOKOJOOMTOS_FIELD_SEF_WARNING="⚠ SEF URLs are disabled — path matching requires SEF. Itemid fallback is active."
; Help
PLG_SYSTEM_MOKOJOOMTOS_HELP_LABEL="How to Use This Plugin"
PLG_SYSTEM_MOKOJOOMTOS_HELP_DESC="<strong>Step 1:</strong> Create articles for your legal pages (Terms of Service, Privacy Policy, etc.).<br/><strong>Step 2:</strong> Create menu items pointing to those articles.<br/><strong>Step 3:</strong> Select the menu items above (hold Ctrl/Cmd to select multiple).<br/><strong>Step 4:</strong> When your site goes offline, visitors can still access the selected pages.<br/><br/><em>Tip:</em> The dropdown shows the full URL path for each menu item (e.g., /legal/terms-of-service)."
+4 -94
View File
@@ -9,106 +9,16 @@
defined('_JEXEC') or die;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Uri\Uri;
/**
* MokoJoomTOS Offline Mode Bypass Plugin (Legacy)
* MokoJoomTOS Legacy Entry Point
*
* Allows configured menu items to remain accessible when the site
* is in offline mode.
* This file is required by Joomla's plugin loader (<filename plugin="mokojoomtos">)
* but is NOT executed under Joomla 5's DI container. The actual plugin logic lives
* in src/Extension/MokoJoomTOS.php, bootstrapped via services/provider.php.
*
* @since 1.0.0
*/
class PlgSystemMokojoomtos extends CMSPlugin
{
/**
* Load the language file on instantiation.
*
* @var boolean
* @since 1.0.0
*/
protected $autoloadLanguage = true;
/**
* Application object
*
* @var \Joomla\CMS\Application\CMSApplication
* @since 1.0.0
*/
protected $app;
/**
* After route event handler
*
* @return void
*
* @since 04.00.00
*/
public function onAfterRoute()
{
// Only process for site application
if (!$this->app->isClient('site'))
{
return;
}
// Get the global configuration
$config = $this->app->getConfig();
// Only proceed if site is offline
if (!$config->get('offline'))
{
return;
}
// Get the configured slugs (stored as array for multi-select)
$slugs = $this->params->get('tos_slug', []);
// Handle legacy single-value string format
if (is_string($slugs))
{
$slugs = array_filter([trim($slugs)]);
}
if (empty($slugs))
{
return;
}
// Get the current URI path
$uri = Uri::getInstance();
$path = trim($uri->getPath(), '/');
// Remove the base path if present
$base = trim(Uri::base(true), '/');
if (!empty($base) && strpos($path, $base) === 0)
{
$path = trim(substr($path, strlen($base)), '/');
}
// Check if the path matches any configured slug
foreach ($slugs as $slug)
{
$slug = trim($slug);
if (empty($slug))
{
continue;
}
if ($path === $slug || strpos($path, $slug . '/') === 0)
{
// Temporarily disable offline mode for this request
$config->set('offline', 0);
// Set component-only view (no template chrome)
$input = $this->app->input;
$input->set('tmpl', 'component');
// Also set in GET superglobal to ensure recognition
$_GET['tmpl'] = 'component';
return;
}
}
}
}
+15 -3
View File
@@ -25,7 +25,7 @@
DEFGROUP: MokoJoomTOS
INGROUP: plg_system_mokojoomtos
PATH: src/mokojoomtos.xml
VERSION: 04.00.00
VERSION: 04.02.01
BRIEF: Plugin manifest XML file for MokoJoomTOS system plugin
=========================================================================
-->
@@ -37,8 +37,8 @@
<license>GNU General Public License version 3 or later; see LICENSE</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>00.00.01</version>
<description>PLG_SYSTEM_MOKOJOOMTOS_XML_DESCRIPTION</description>
<version>04.03.00</version>
<description>Allows Terms of Service to be accessible via menu slug when site is offline</description>
<namespace path="src">Joomla\Plugin\System\MokoJoomTOS</namespace>
@@ -71,6 +71,18 @@
description="PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_DESC"
multiple="true"
/>
<field
name="include_children"
type="radio"
label="PLG_SYSTEM_MOKOJOOMTOS_FIELD_INCLUDE_CHILDREN_LABEL"
description="PLG_SYSTEM_MOKOJOOMTOS_FIELD_INCLUDE_CHILDREN_DESC"
default="1"
class="btn-group btn-group-yesno"
>
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field
name="help_spacer"
+440 -364
View File
@@ -13,394 +13,470 @@ use Joomla\CMS\Installer\InstallerAdapter;
use Joomla\CMS\Installer\InstallerScript;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Log\Log;
use Joomla\CMS\Table\Table;
/**
* Installation script for MokoJoomTOS Offline Plugin
* Installation script for MokoJoomTOS Plugin
*
* @since 1.0.0
*/
class PlgSystemMokojoomtosOfflineInstallerScript extends InstallerScript
class PlgSystemMokojoomtosInstallerScript extends InstallerScript
{
/**
* Minimum Joomla version required to install the plugin
*
* @var string
* @since 1.0.0
*/
protected $minimumJoomla = '4.0.0';
/**
* Minimum Joomla version required to install the plugin
*
* @var string
* @since 1.0.0
*/
protected $minimumJoomla = '4.0.0';
/**
* Minimum PHP version required to install the plugin
*
* @var string
* @since 1.0.0
*/
protected $minimumPhp = '7.4.0';
/**
* Minimum PHP version required to install the plugin
*
* @var string
* @since 1.0.0
*/
protected $minimumPhp = '7.4.0';
/**
* Extension type (used by parent class)
*
* @var string
* @since 1.0.0
*/
protected $extension = 'plg_system_mokojoomtos';
/**
* Extension type (used by parent class)
*
* @var string
* @since 1.0.0
*/
protected $extension = 'plg_system_mokojoomtos';
/**
* Function called before plugin installation/update/uninstall
*
* @param string $type Installation type (install, update, discover_install)
* @param InstallerAdapter $parent Parent installer adapter
*
* @return boolean True on success
*
* @since 1.0.0
*/
public function preflight($type, $parent)
{
// Check minimum requirements
if (!parent::preflight($type, $parent)) {
return false;
}
/**
* Function called before plugin installation/update/uninstall
*
* @param string $type Installation type (install, update, discover_install)
* @param InstallerAdapter $parent Parent installer adapter
*
* @return boolean True on success
*
* @since 1.0.0
*/
public function preflight($type, $parent)
{
if (!parent::preflight($type, $parent)) {
return false;
}
return true;
}
return true;
}
/**
* Function called after plugin installation
*
* @param InstallerAdapter $parent Parent installer adapter
*
* @return void
*
* @since 1.0.0
*/
public function install($parent)
{
echo '<p>' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_INSTALL_SUCCESS') . '</p>';
}
/**
* Function called after plugin installation
*
* @param InstallerAdapter $parent Parent installer adapter
*
* @return void
*
* @since 1.0.0
*/
public function install($parent)
{
echo '<p>' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_INSTALL_SUCCESS') . '</p>';
}
/**
* Function called after plugin update
*
* @param InstallerAdapter $parent Parent installer adapter
*
* @return void
*
* @since 1.0.0
*/
public function update($parent)
{
echo '<p>' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_UPDATE_SUCCESS') . '</p>';
}
/**
* Function called after plugin update
*
* @param InstallerAdapter $parent Parent installer adapter
*
* @return void
*
* @since 1.0.0
*/
public function update($parent)
{
echo '<p>' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_UPDATE_SUCCESS') . '</p>';
}
/**
* Function called after plugin uninstallation
*
* @param InstallerAdapter $parent Parent installer adapter
*
* @return void
*
* @since 1.0.0
*/
public function uninstall($parent)
{
echo '<p>' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_UNINSTALL_SUCCESS') . '</p>';
}
/**
* Function called after plugin uninstallation
*
* @param InstallerAdapter $parent Parent installer adapter
*
* @return void
*
* @since 1.0.0
*/
public function uninstall($parent)
{
echo '<p>' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_UNINSTALL_SUCCESS') . '</p>';
}
/**
* Function called after extension installation/update/discover_install
*
* @param string $type Installation type (install, update, discover_install)
* @param InstallerAdapter $parent Parent installer adapter
*
* @return void
*
* @since 1.0.0
*/
public function postflight($type, $parent)
{
if ($type === 'install' || $type === 'discover_install') {
// Create Terms of Service article and menu item
$this->createTermsOfServiceSetup();
echo '<div class="alert alert-success">';
echo '<h4>' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_POSTINSTALL_TITLE') . '</h4>';
echo '<p>' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_POSTINSTALL_DESC') . '</p>';
echo '</div>';
}
}
/**
* Function called after extension installation/update/discover_install
*
* Fixes #89: enablePlugin() is now called unconditionally for both
* install and upgrade paths.
* Fixes #92: enablePlugin() is called on upgrade to re-enable if disabled.
*
* @param string $type Installation type (install, update, discover_install)
* @param InstallerAdapter $parent Parent installer adapter
*
* @return void
*
* @since 1.0.0
*/
public function postflight($type, $parent)
{
// Always enable the plugin on install or upgrade
$this->enablePlugin();
/**
* Create Terms of Service article and menu item
*
* @return void
*
* @since 1.0.0
*/
private function createTermsOfServiceSetup()
{
try {
$db = Factory::getDbo();
// Check if Terms of Service article already exists
$query = $db->getQuery(true)
->select('id')
->from($db->quoteName('#__content'))
->where($db->quoteName('alias') . ' = ' . $db->quote('terms-of-service'));
$db->setQuery($query);
$articleId = $db->loadResult();
// Create article if it doesn't exist
if (!$articleId) {
$articleId = $this->createTermsArticle();
}
if ($articleId) {
// Check if menu item already exists
$query = $db->getQuery(true)
->select('id')
->from($db->quoteName('#__menu'))
->where($db->quoteName('alias') . ' = ' . $db->quote('terms-of-service'))
->where($db->quoteName('published') . ' >= 0');
$db->setQuery($query);
$menuId = $db->loadResult();
// Create menu item if it doesn't exist
if (!$menuId) {
$this->createTermsMenuItem($articleId);
}
}
} catch (\Exception $e) {
Log::add('Error creating Terms of Service setup: ' . $e->getMessage(), Log::WARNING, 'jerror');
}
}
if ($type === 'install' || $type === 'discover_install') {
$this->createTermsOfServiceSetup();
$this->setDefaultSlugs();
/**
* Create Terms of Service article
*
* @return int|null Article ID or null on failure
*
* @since 1.0.0
*/
private function createTermsArticle()
{
try {
$db = Factory::getDbo();
echo '<div class="alert alert-success">';
echo '<h4>' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_POSTINSTALL_TITLE') . '</h4>';
echo '<p>' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_POSTINSTALL_DESC') . '</p>';
echo '</div>';
}
}
Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_content/tables');
$table = Table::getInstance('Content', 'Joomla\\Component\\Content\\Administrator\\Table\\');
/**
* Create Terms of Service article and menu item
*
* @return void
*
* @since 1.0.0
*/
private function createTermsOfServiceSetup()
{
try {
$db = Factory::getDbo();
if (!$table) {
Log::add('Failed to get Content table instance', Log::WARNING, 'jerror');
return null;
}
// Check if Terms of Service article already exists (by alias, any category)
$query = $db->getQuery(true)
->select('id')
->from($db->quoteName('#__content'))
->where($db->quoteName('alias') . ' = ' . $db->quote('terms-of-service'))
->where($db->quoteName('state') . ' >= 0');
$db->setQuery($query);
$articleId = $db->loadResult();
// Get Uncategorised category ID dynamically
$query = $db->getQuery(true)
->select('id')
->from($db->quoteName('#__categories'))
->where($db->quoteName('extension') . ' = ' . $db->quote('com_content'))
->where($db->quoteName('alias') . ' = ' . $db->quote('uncategorised'))
->where($db->quoteName('published') . ' = 1');
$db->setQuery($query);
$catId = (int) $db->loadResult();
if (!$articleId) {
$articleId = $this->createTermsArticle();
}
if (!$catId) {
Log::add('Could not find Uncategorised category for com_content', Log::WARNING, 'jerror');
return null;
}
if ($articleId) {
$query = $db->getQuery(true)
->select('id')
->from($db->quoteName('#__menu'))
->where($db->quoteName('alias') . ' = ' . $db->quote('terms-of-service'))
->where($db->quoteName('published') . ' >= 0');
$db->setQuery($query);
$menuId = $db->loadResult();
// Get current user ID for article ownership
$createdBy = Factory::getApplication()->getIdentity()->id ?: 0;
if (!$menuId) {
$this->createTermsMenuItem($articleId);
}
}
} catch (\Throwable $e) {
Log::add('Error creating Terms of Service setup: ' . $e->getMessage(), Log::WARNING, 'jerror');
}
}
$data = [
'title' => 'Terms of Service',
'alias' => 'terms-of-service',
'introtext' => '<h2>Terms of Service</h2><p>Welcome to our Terms of Service page.</p><p>This page will remain accessible even when the site is in offline/maintenance mode.</p>',
'fulltext' => '',
'state' => 1,
'catid' => $catId,
'created' => Factory::getDate()->toSql(),
'created_by' => $createdBy,
'language' => '*',
'access' => 1, // Public
];
// Bind data to table object first
if (!$table->bind($data)) {
Log::add('Failed to bind data to Content table: ' . $table->getError(), Log::WARNING, 'jerror');
return null;
}
// Check data validity
if (!$table->check()) {
Log::add('Content table check failed: ' . $table->getError(), Log::WARNING, 'jerror');
return null;
}
// Save the table
if (!$table->store()) {
Log::add('Failed to store Content table: ' . $table->getError(), Log::WARNING, 'jerror');
return null;
}
echo '<p class="alert alert-info">✓ Created Terms of Service article</p>';
return $table->id;
} catch (\Exception $e) {
Log::add('Error creating Terms of Service article: ' . $e->getMessage(), Log::WARNING, 'jerror');
}
return null;
}
/**
* Create Terms of Service article using Joomla 5 MVCFactory
*
* Fixes #90: Uses bootComponent()->getMVCFactory() instead of
* the removed Table::addIncludePath() / Table::getInstance().
* Fixes #94: Includes params, metadata, and attribs defaults.
*
* @return int|null Article ID or null on failure
*
* @since 1.0.0
*/
private function createTermsArticle()
{
try {
$db = Factory::getDbo();
$app = Factory::getApplication();
/**
* Create Terms of Service menu item
*
* @param int $articleId The article ID to link to
*
* @return void
*
* @since 1.0.0
*/
private function createTermsMenuItem($articleId)
{
try {
$db = Factory::getDbo();
// Check if "Legal" menu type exists
$query = $db->getQuery(true)
->select('id')
->from($db->quoteName('#__menu_types'))
->where($db->quoteName('menutype') . ' = ' . $db->quote('legal'));
$db->setQuery($query);
$legalMenuExists = $db->loadResult();
// Create "Legal" menu type if it doesn't exist
if (!$legalMenuExists) {
$this->createLegalMenuType();
}
// Get com_content component ID dynamically
$query = $db->getQuery(true)
->select('extension_id')
->from($db->quoteName('#__extensions'))
->where($db->quoteName('type') . ' = ' . $db->quote('component'))
->where($db->quoteName('element') . ' = ' . $db->quote('com_content'));
$db->setQuery($query);
$componentId = (int) $db->loadResult();
// Get content table via MVCFactory (Joomla 4/5 compatible)
$table = $app->bootComponent('com_content')
->getMVCFactory()
->createTable('Article', 'Administrator');
if (!$componentId) {
Log::add('Could not determine com_content component ID', Log::WARNING, 'jerror');
return;
}
Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_menus/tables');
$table = Table::getInstance('Menu', 'Joomla\\Component\\Menus\\Administrator\\Table\\');
if (!$table) {
Log::add('Failed to get Menu table instance', Log::WARNING, 'jerror');
return;
}
$data = [
'menutype' => 'legal',
'title' => 'Terms of Service',
'alias' => 'terms-of-service',
'link' => 'index.php?option=com_content&view=article&id=' . $articleId,
'type' => 'component',
'published' => 1,
'parent_id' => 1,
'component_id' => $componentId,
'level' => 1,
'language' => '*',
'access' => 1, // Public
'params' => '{"show_title":"1","link_titles":"0","show_intro":"","info_block_position":"","show_category":"0","link_category":"0","show_parent_category":"0","link_parent_category":"0","show_author":"0","link_author":"0","show_create_date":"0","show_modify_date":"0","show_publish_date":"0","show_item_navigation":"0","show_icons":"0","show_print_icon":"0","show_email_icon":"0","show_hits":"0","show_noauth":"0","urls_position":"","menu-anchor_title":"","menu-anchor_css":"","menu_image":"","menu_text":1,"page_title":"","show_page_heading":0,"page_heading":"","pageclass_sfx":"","menu-meta_description":"","menu-meta_keywords":"","robots":"","secure":0}',
];
// Set the location in the menu tree
$table->setLocation($data['parent_id'], 'last-child');
// Bind data to table object
if (!$table->bind($data)) {
Log::add('Failed to bind data to Menu table: ' . $table->getError(), Log::WARNING, 'jerror');
return;
}
// Check data validity
if (!$table->check()) {
Log::add('Menu table check failed: ' . $table->getError(), Log::WARNING, 'jerror');
return;
}
// Save the menu item
if (!$table->store()) {
Log::add('Failed to store Menu table: ' . $table->getError(), Log::WARNING, 'jerror');
return;
}
echo '<p class="alert alert-info">✓ Created Terms of Service menu item in Legal menu with slug: terms-of-service</p>';
// Enable the plugin
$this->enablePlugin();
} catch (\Exception $e) {
Log::add('Error creating Terms of Service menu item: ' . $e->getMessage(), Log::WARNING, 'jerror');
}
}
if (!$table) {
Log::add('Failed to get Content table instance', Log::WARNING, 'jerror');
return null;
}
/**
* Create Legal menu type
*
* @return void
*
* @since 1.0.0
*/
private function createLegalMenuType()
{
try {
$db = Factory::getDbo();
// Insert the Legal menu type
$query = $db->getQuery(true)
->insert($db->quoteName('#__menu_types'))
->columns($db->quoteName(['menutype', 'title', 'description']))
->values(
$db->quote('legal') . ', ' .
$db->quote('Legal') . ', ' .
$db->quote('Legal documents and policies menu')
);
$db->setQuery($query);
$db->execute();
echo '<p class="alert alert-info">✓ Created Legal menu type</p>';
} catch (\Exception $e) {
Log::add('Error creating Legal menu type: ' . $e->getMessage(), Log::WARNING, 'jerror');
}
}
// Get Uncategorised category ID dynamically
$query = $db->getQuery(true)
->select('id')
->from($db->quoteName('#__categories'))
->where($db->quoteName('extension') . ' = ' . $db->quote('com_content'))
->where($db->quoteName('alias') . ' = ' . $db->quote('uncategorised'))
->where($db->quoteName('published') . ' = 1');
$db->setQuery($query);
$catId = (int) $db->loadResult();
/**
* Enable the plugin after installation
*
* @return void
*
* @since 1.0.0
*/
private function enablePlugin()
{
try {
$db = Factory::getDbo();
$query = $db->getQuery(true)
->update($db->quoteName('#__extensions'))
->set($db->quoteName('enabled') . ' = 1')
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
->where($db->quoteName('element') . ' = ' . $db->quote('mokojoomtos'));
$db->setQuery($query);
$db->execute();
echo '<p class="alert alert-success">✓ Plugin enabled automatically</p>';
} catch (\Exception $e) {
Log::add('Error enabling plugin: ' . $e->getMessage(), Log::WARNING, 'jerror');
}
}
if (!$catId) {
Log::add('Could not find Uncategorised category for com_content', Log::WARNING, 'jerror');
return null;
}
$createdBy = $app->getIdentity()->id ?: 0;
$data = [
'title' => 'Terms of Service',
'alias' => 'terms-of-service',
'introtext' => '<h2>Terms of Service</h2><p>Welcome to our Terms of Service page.</p><p>This page will remain accessible even when the site is in offline/maintenance mode.</p>',
'fulltext' => '',
'state' => 1,
'catid' => $catId,
'created' => Factory::getDate()->toSql(),
'created_by' => $createdBy,
'language' => '*',
'access' => 1,
'params' => '{}',
'metadata' => '{"robots":"","author":"","rights":""}',
'attribs' => '{}',
];
if (!$table->bind($data)) {
Log::add('Failed to bind data to Content table: ' . $table->getError(), Log::WARNING, 'jerror');
return null;
}
if (!$table->check()) {
Log::add('Content table check failed: ' . $table->getError(), Log::WARNING, 'jerror');
return null;
}
if (!$table->store()) {
Log::add('Failed to store Content table: ' . $table->getError(), Log::WARNING, 'jerror');
return null;
}
echo '<p class="alert alert-info">Created Terms of Service article</p>';
return $table->id;
} catch (\Throwable $e) {
Log::add('Error creating Terms of Service article: ' . $e->getMessage(), Log::WARNING, 'jerror');
}
return null;
}
/**
* Create Terms of Service menu item using Joomla 5 MVCFactory
*
* Fixes #90: Uses bootComponent()->getMVCFactory() instead of
* the removed Table::addIncludePath() / Table::getInstance().
*
* @param int $articleId The article ID to link to
*
* @return void
*
* @since 1.0.0
*/
private function createTermsMenuItem($articleId)
{
try {
$db = Factory::getDbo();
$app = Factory::getApplication();
// Check if "Legal" menu type exists
$query = $db->getQuery(true)
->select('id')
->from($db->quoteName('#__menu_types'))
->where($db->quoteName('menutype') . ' = ' . $db->quote('legal'));
$db->setQuery($query);
$legalMenuExists = $db->loadResult();
if (!$legalMenuExists) {
$this->createLegalMenuType();
}
// Get com_content component ID dynamically
$query = $db->getQuery(true)
->select('extension_id')
->from($db->quoteName('#__extensions'))
->where($db->quoteName('type') . ' = ' . $db->quote('component'))
->where($db->quoteName('element') . ' = ' . $db->quote('com_content'));
$db->setQuery($query);
$componentId = (int) $db->loadResult();
if (!$componentId) {
Log::add('Could not determine com_content component ID', Log::WARNING, 'jerror');
return;
}
// Get menu table via MVCFactory (Joomla 4/5 compatible)
$table = $app->bootComponent('com_menus')
->getMVCFactory()
->createTable('Menu', 'Administrator');
if (!$table) {
Log::add('Failed to get Menu table instance', Log::WARNING, 'jerror');
return;
}
$data = [
'menutype' => 'legal',
'title' => 'Terms of Service',
'alias' => 'terms-of-service',
'link' => 'index.php?option=com_content&view=article&id=' . $articleId,
'type' => 'component',
'published' => 1,
'parent_id' => 1,
'component_id' => $componentId,
'level' => 1,
'language' => '*',
'access' => 1,
'params' => '{"show_title":"1","link_titles":"0","show_intro":"","info_block_position":"","show_category":"0","link_category":"0","show_parent_category":"0","link_parent_category":"0","show_author":"0","link_author":"0","show_create_date":"0","show_modify_date":"0","show_publish_date":"0","show_item_navigation":"0","show_icons":"0","show_print_icon":"0","show_email_icon":"0","show_hits":"0","show_noauth":"0","urls_position":"","menu-anchor_title":"","menu-anchor_css":"","menu_image":"","menu_text":1,"page_title":"","show_page_heading":0,"page_heading":"","pageclass_sfx":"","menu-meta_description":"","menu-meta_keywords":"","robots":"","secure":0}',
];
$table->setLocation($data['parent_id'], 'last-child');
if (!$table->bind($data)) {
Log::add('Failed to bind data to Menu table: ' . $table->getError(), Log::WARNING, 'jerror');
return;
}
if (!$table->check()) {
Log::add('Menu table check failed: ' . $table->getError(), Log::WARNING, 'jerror');
return;
}
if (!$table->store()) {
Log::add('Failed to store Menu table: ' . $table->getError(), Log::WARNING, 'jerror');
return;
}
echo '<p class="alert alert-info">Created Terms of Service menu item in Legal menu</p>';
} catch (\Throwable $e) {
Log::add('Error creating Terms of Service menu item: ' . $e->getMessage(), Log::WARNING, 'jerror');
}
}
/**
* Create Legal menu type
*
* @return void
*
* @since 1.0.0
*/
private function createLegalMenuType()
{
try {
$db = Factory::getDbo();
$query = $db->getQuery(true)
->insert($db->quoteName('#__menu_types'))
->columns($db->quoteName(['menutype', 'title', 'description']))
->values(
$db->quote('legal') . ', ' .
$db->quote('Legal') . ', ' .
$db->quote('Legal documents and policies menu')
);
$db->setQuery($query);
$db->execute();
echo '<p class="alert alert-info">Created Legal menu type</p>';
} catch (\Throwable $e) {
// Duplicate key is expected if race condition — safe to ignore
Log::add('Error creating Legal menu type: ' . $e->getMessage(), Log::WARNING, 'jerror');
}
}
/**
* Auto-select default menu slugs (terms-of-service, privacy-policy)
*
* Looks up menu items matching common legal page aliases and sets
* them as the default tos_slug parameter so the plugin works
* immediately after install with zero configuration.
*
* @return void
*
* @since 4.2.1
*/
private function setDefaultSlugs()
{
try {
$db = Factory::getDbo();
$defaultAliases = ['terms-of-service', 'privacy-policy'];
$slugs = [];
foreach ($defaultAliases as $alias) {
$query = $db->getQuery(true)
->select($db->quoteName('path'))
->from($db->quoteName('#__menu'))
->where($db->quoteName('alias') . ' = ' . $db->quote($alias))
->where($db->quoteName('published') . ' = 1')
->where($db->quoteName('client_id') . ' = 0');
$db->setQuery($query);
$path = $db->loadResult();
if ($path) {
$slugs[] = trim($path, '/');
}
}
if (empty($slugs)) {
return;
}
// Load current plugin params
$query = $db->getQuery(true)
->select($db->quoteName('params'))
->from($db->quoteName('#__extensions'))
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
->where($db->quoteName('element') . ' = ' . $db->quote('mokojoomtos'));
$db->setQuery($query);
$paramsJson = $db->loadResult();
$params = json_decode($paramsJson ?: '{}', true) ?: [];
// Only set defaults if no slugs are already configured
$existing = $params['tos_slug'] ?? [];
if (!empty($existing)) {
return;
}
$params['tos_slug'] = $slugs;
$query = $db->getQuery(true)
->update($db->quoteName('#__extensions'))
->set($db->quoteName('params') . ' = ' . $db->quote(json_encode($params)))
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
->where($db->quoteName('element') . ' = ' . $db->quote('mokojoomtos'));
$db->setQuery($query);
$db->execute();
} catch (\Throwable $e) {
Log::add('Error setting default slugs: ' . $e->getMessage(), Log::WARNING, 'jerror');
}
}
/**
* Enable the plugin after installation
*
* @return void
*
* @since 1.0.0
*/
private function enablePlugin()
{
try {
$db = Factory::getDbo();
$query = $db->getQuery(true)
->update($db->quoteName('#__extensions'))
->set($db->quoteName('enabled') . ' = 1')
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
->where($db->quoteName('element') . ' = ' . $db->quote('mokojoomtos'));
$db->setQuery($query);
$db->execute();
} catch (\Throwable $e) {
Log::add('Error enabling plugin: ' . $e->getMessage(), Log::WARNING, 'jerror');
}
}
}
+139 -25
View File
@@ -10,6 +10,7 @@ namespace Joomla\Plugin\System\MokoJoomTOS\Extension;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Uri\Uri;
use Joomla\Event\SubscriberInterface;
@@ -53,23 +54,21 @@ final class MokoJoomTOS extends CMSPlugin implements SubscriberInterface
* the site is in offline mode. If both conditions are met, temporarily
* disables offline mode and sets component-only view for this request.
*
* This event fires after routing but before template selection, making it
* the correct place to set tmpl=component to prevent template chrome loading.
*
* @return void
*
* @since 1.0.0
*/
public function onAfterRoute()
{
$app = $this->getApplication();
// Only process for site application
if (!$this->getApplication()->isClient('site'))
if (!$app->isClient('site'))
{
return;
}
// Get the global configuration
$config = $this->getApplication()->getConfig();
$config = $app->getConfig();
// Only proceed if site is offline
if (!$config->get('offline'))
@@ -77,7 +76,7 @@ final class MokoJoomTOS extends CMSPlugin implements SubscriberInterface
return;
}
// Get the configured slugs (stored as array for multi-select)
// Get the configured slugs — cast to array to handle stdClass from Registry (#96)
$slugs = $this->params->get('tos_slug', []);
// Handle legacy single-value string format
@@ -85,46 +84,161 @@ final class MokoJoomTOS extends CMSPlugin implements SubscriberInterface
{
$slugs = array_filter([trim($slugs)]);
}
else
{
$slugs = (array) $slugs;
}
if (empty($slugs))
{
return;
}
// Get the current URI path
$uri = Uri::getInstance();
$path = trim($uri->getPath(), '/');
$includeChildren = (int) $this->params->get('include_children', 1);
// Remove the base path if present
// Try SEF path matching first, then fall back to Itemid matching (#91)
if ($this->matchByPath($slugs, $config, $app, $includeChildren))
{
return;
}
$this->matchByItemId($slugs, $config, $app, $includeChildren);
}
/**
* Match the current request path against configured slugs (SEF mode)
*
* @param array $slugs Configured slug values
* @param object $config Joomla configuration object
* @param object $app Application instance
* @param integer $includeChildren Whether to include child menu items
*
* @return boolean True if a match was found and offline mode was bypassed
*
* @since 4.1.0
*/
private function matchByPath(array $slugs, $config, $app, int $includeChildren = 1): bool
{
$uri = Uri::getInstance();
$path = urldecode(trim($uri->getPath(), '/'));
// Remove the base path if present (subdirectory installs)
$base = trim(Uri::base(true), '/');
if (!empty($base) && strpos($path, $base) === 0)
{
$path = trim(substr($path, strlen($base)), '/');
}
// Check if the path matches any configured slug
// Skip if path is empty or just index.php (non-SEF)
if (empty($path) || $path === 'index.php')
{
return false;
}
foreach ($slugs as $slug)
{
$slug = trim($slug);
$slug = trim((string) $slug);
if (empty($slug))
{
continue;
}
if ($path === $slug || strpos($path, $slug . '/') === 0)
$isMatch = ($path === $slug)
|| ($includeChildren && strpos($path, $slug . '/') === 0);
if ($isMatch)
{
// Temporarily disable offline mode for this request
$config->set('offline', 0);
// Set component-only view (no template chrome)
$input = $this->getApplication()->input;
$input->set('tmpl', 'component');
// Also set in GET superglobal to ensure recognition
$_GET['tmpl'] = 'component';
return;
$this->bypassOffline($config, $app);
return true;
}
}
return false;
}
/**
* Match the current request Itemid against menu items for configured slugs (non-SEF fallback)
*
* When SEF URLs are disabled, the path is just index.php so we match by
* checking if the requested Itemid belongs to a menu item whose path
* matches a configured slug.
*
* @param array $slugs Configured slug values
* @param object $config Joomla configuration object
* @param object $app Application instance
* @param integer $includeChildren Whether to include child menu items
*
* @return boolean True if a match was found and offline mode was bypassed
*
* @since 4.1.0
*/
private function matchByItemId(array $slugs, $config, $app, int $includeChildren = 1): bool
{
$itemId = (int) $app->input->getInt('Itemid', 0);
if (!$itemId)
{
return false;
}
try
{
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName('path'))
->from($db->quoteName('#__menu'))
->where($db->quoteName('id') . ' = ' . $itemId)
->where($db->quoteName('published') . ' = 1')
->where($db->quoteName('client_id') . ' = 0');
$db->setQuery($query);
$menuPath = $db->loadResult();
if (!$menuPath)
{
return false;
}
$menuPath = trim($menuPath, '/');
foreach ($slugs as $slug)
{
$slug = trim((string) $slug);
if (empty($slug))
{
continue;
}
$isMatch = ($menuPath === $slug)
|| ($includeChildren && strpos($menuPath, $slug . '/') === 0);
if ($isMatch)
{
$this->bypassOffline($config, $app);
return true;
}
}
}
catch (\Throwable $e)
{
// Silently fail — do not bypass offline mode on error
}
return false;
}
/**
* Bypass offline mode and set component-only view for this request
*
* @param object $config Joomla configuration object
* @param object $app Application instance
*
* @return void
*
* @since 4.1.0
*/
private function bypassOffline($config, $app): void
{
$config->set('offline', 0);
$app->input->set('tmpl', 'component');
}
}
+19 -1
View File
@@ -42,6 +42,24 @@ class MenuslugField extends ListField
{
$options = parent::getOptions();
// Warn if SEF URLs are disabled (#97)
try
{
$sef = Factory::getApplication()->get('sef', true);
if (!$sef)
{
$options[] = (object) [
'value' => '',
'text' => Text::_('PLG_SYSTEM_MOKOJOOMTOS_FIELD_SEF_WARNING'),
'disabled' => true
];
}
}
catch (\Exception $e)
{
// Ignore — field still works without the warning
}
try
{
$db = Factory::getDbo();
@@ -70,7 +88,7 @@ class MenuslugField extends ListField
$options[] = (object) [
'value' => '',
'text' => '──────────────',
'disable' => true
'disabled' => true
];
}
$lastMenuType = $item->menutype;
+26 -31
View File
@@ -1,96 +1,91 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
SPDX-License-Identifier: GPL-3.0-or-later
VERSION: 04.00.00
VERSION: 04.03.00
-->
<updates>
<update>
<name>plg_system_mokojoomtos</name>
<description>plg_system_mokojoomtos update</description>
<name>System - Moko Terms of Service</name>
<description>System - Moko Terms of Service update</description>
<element>mokojoomtos</element>
<type>plugin</type>
<version>04.00.00</version>
<version>04.03.00-dev</version>
<client>site</client>
<folder>system</folder>
<tags><tag>development</tag></tags>
<infourl title="plg_system_mokojoomtos">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/tag/stable</infourl>
<infourl title="System - Moko Terms of Service">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/tag/development</infourl>
<downloads>
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/download/stable/plg_system_mokojoomtos-04.00.00.zip</downloadurl>
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/download/development/plg_system_mokojoomtos-04.03.00-dev.zip</downloadurl>
</downloads>
<sha256>6d8c6a03d6dc0a784a4442c9c46e1a79e8f65b3da7091c5857958653c5b6efe3</sha256>
<targetplatform name="joomla" version="((5.[0-9])|(6.[0-9]))" />
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
</update>
<update>
<name>plg_system_mokojoomtos</name>
<description>plg_system_mokojoomtos update</description>
<name>System - Moko Terms of Service</name>
<description>System - Moko Terms of Service update</description>
<element>mokojoomtos</element>
<type>plugin</type>
<version>04.00.00</version>
<version>04.03.00-alpha</version>
<client>site</client>
<folder>system</folder>
<tags><tag>alpha</tag></tags>
<infourl title="plg_system_mokojoomtos">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/tag/stable</infourl>
<infourl title="System - Moko Terms of Service">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/tag/alpha</infourl>
<downloads>
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/download/stable/plg_system_mokojoomtos-04.00.00.zip</downloadurl>
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/download/alpha/plg_system_mokojoomtos-04.03.00-alpha.zip</downloadurl>
</downloads>
<sha256>6d8c6a03d6dc0a784a4442c9c46e1a79e8f65b3da7091c5857958653c5b6efe3</sha256>
<targetplatform name="joomla" version="((5.[0-9])|(6.[0-9]))" />
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
</update>
<update>
<name>plg_system_mokojoomtos</name>
<description>plg_system_mokojoomtos update</description>
<name>System - Moko Terms of Service</name>
<description>System - Moko Terms of Service update</description>
<element>mokojoomtos</element>
<type>plugin</type>
<version>04.00.00</version>
<version>04.03.00-beta</version>
<client>site</client>
<folder>system</folder>
<tags><tag>beta</tag></tags>
<infourl title="plg_system_mokojoomtos">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/tag/stable</infourl>
<infourl title="System - Moko Terms of Service">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/tag/beta</infourl>
<downloads>
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/download/stable/plg_system_mokojoomtos-04.00.00.zip</downloadurl>
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/download/beta/plg_system_mokojoomtos-04.03.00-beta.zip</downloadurl>
</downloads>
<sha256>6d8c6a03d6dc0a784a4442c9c46e1a79e8f65b3da7091c5857958653c5b6efe3</sha256>
<targetplatform name="joomla" version="((5.[0-9])|(6.[0-9]))" />
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
</update>
<update>
<name>plg_system_mokojoomtos</name>
<description>plg_system_mokojoomtos update</description>
<name>System - Moko Terms of Service</name>
<description>System - Moko Terms of Service update</description>
<element>mokojoomtos</element>
<type>plugin</type>
<version>04.00.00</version>
<version>04.03.00-rc</version>
<client>site</client>
<folder>system</folder>
<tags><tag>rc</tag></tags>
<infourl title="plg_system_mokojoomtos">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/tag/stable</infourl>
<infourl title="System - Moko Terms of Service">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/tag/rc</infourl>
<downloads>
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/download/stable/plg_system_mokojoomtos-04.00.00.zip</downloadurl>
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/download/rc/plg_system_mokojoomtos-04.03.00-rc.zip</downloadurl>
</downloads>
<sha256>6d8c6a03d6dc0a784a4442c9c46e1a79e8f65b3da7091c5857958653c5b6efe3</sha256>
<targetplatform name="joomla" version="((5.[0-9])|(6.[0-9]))" />
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
</update>
<update>
<name>plg_system_mokojoomtos</name>
<description>plg_system_mokojoomtos update</description>
<name>System - Moko Terms of Service</name>
<description>System - Moko Terms of Service update</description>
<element>mokojoomtos</element>
<type>plugin</type>
<version>04.00.00</version>
<version>04.03.00</version>
<client>site</client>
<folder>system</folder>
<tags><tag>stable</tag></tags>
<infourl title="plg_system_mokojoomtos">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/tag/stable</infourl>
<infourl title="System - Moko Terms of Service">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/tag/stable</infourl>
<downloads>
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/download/stable/plg_system_mokojoomtos-04.00.00.zip</downloadurl>
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/download/stable/plg_system_mokojoomtos-04.03.00.zip</downloadurl>
</downloads>
<sha256>6d8c6a03d6dc0a784a4442c9c46e1a79e8f65b3da7091c5857958653c5b6efe3</sha256>
<targetplatform name="joomla" version="((5.[0-9])|(6.[0-9]))" />
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>