127 Commits

Author SHA1 Message Date
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
jmiller 940caa3d2d fix(ci): add branch output to auto-release [skip ci] 2026-05-23 19:47:50 +00: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
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 1226 additions and 1564 deletions
-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.Deploy
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
# INGROUP: moko-platform.Deploy
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /templates/workflows/joomla/deploy-manual.yml.template
# VERSION: 04.07.00
# BRIEF: Manual SFTP deploy to dev server for Joomla repos
@@ -40,7 +40,7 @@ jobs:
run: |
php -v && composer --version
- name: Setup MokoStandards tools
- name: Setup moko-platform tools
env:
GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
@@ -48,10 +48,10 @@ jobs:
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
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
/tmp/moko-platform-api 2>/dev/null || true
if [ -d "/tmp/moko-platform-api" ] && [ -f "/tmp/moko-platform-api/composer.json" ]; then
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
fi
- name: Check FTP configuration
@@ -101,15 +101,28 @@ jobs:
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[@]}"
PLATFORM=$(php /tmp/moko-platform-api/cli/platform_detect.php --path . 2>/dev/null || true)
if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/moko-platform-api/deploy/deploy-joomla.php" ]; then
php /tmp/moko-platform-api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}"
else
php /tmp/mokostandards-api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}"
php /tmp/moko-platform-api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}"
fi
rm -f /tmp/deploy_key /tmp/sftp-config.json
- name: Post-deploy health check
if: success() && steps.check.outputs.skip != 'true'
run: |
if [ -f "deploy/health-check.php" ]; then
SITE_URL="${{ vars.DEV_SITE_URL }}"
if [ -n "$SITE_URL" ]; then
php deploy/health-check.php --url "$SITE_URL" --checks http --timeout 30 || echo "::warning::Health check failed after deploy"
else
echo "DEV_SITE_URL not configured, skipping health check"
fi
fi
- name: Summary
if: always()
run: |
@@ -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 }}
+53 -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,41 @@ 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]
### 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)
### Added
- SEF disabled warning in MenuslugField dropdown (#97)
- Include Children toggle for offline-accessible menu items (defaults to Yes)
### Fixed (Manifest)
- Hardcode description in XML manifest (language variables don't resolve during install)
### Changed
- Stripped legacy mokojoomtos.php to minimal stub (#101)
- Converted script.php indentation from spaces to tabs (#102)
- Renamed installer class to PlgSystemMokojoomtosInstallerScript (#103)
## [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 +82,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 +103,32 @@ 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.01.00]: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/tag/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,9 @@ 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)."
@@ -11,6 +11,9 @@ 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)."
+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;
}
}
}
}
+14 -2
View File
@@ -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.02.01</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"
+368 -364
View File
@@ -13,394 +13,398 @@ 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();
/**
* 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 (with catid filter)
$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 (\Exception $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 (\Exception $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 (\Exception $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 (\Exception $e) {
// Duplicate key is expected if race condition — safe to ignore
Log::add('Error creating Legal menu type: ' . $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 (\Exception $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 (\Exception $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.01.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.01.00</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/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.01.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>
</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.01.00</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/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.01.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>
</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.01.00</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/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.01.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>
</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.01.00</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/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.01.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>
</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.01.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.01.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>