Compare commits

..

246 Commits

Author SHA1 Message Date
jmiller 3dcadc130b chore: sync .mokogitea/workflows/update-server.yml from moko-platform [skip ci] 2026-05-28 20:50:23 +00:00
jmiller eaf686b37e chore: sync .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] 2026-05-28 20:45:49 +00:00
jmiller 3ab70375d1 fix: restore folder="packages" on files element [skip ci]
Universal: Build & Release / Promote Pre-Release to RC (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 21s
2026-05-28 20:30:04 +00:00
jmiller 4aa30180bb chore: sync .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] 2026-05-28 20:27:40 +00:00
gitea-actions[bot] 1cd79d4d5f chore(version): auto-bump patch 02.16.03-dev [skip ci]
Universal: Build & Release / Promote Pre-Release to RC (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 13s
2026-05-28 20:23:48 +00:00
Jonathan Miller a084dc4d72 chore: trigger release pipeline test
Generic: Repo Health / Release configuration (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 3s
2026-05-28 15:23:41 -05:00
jmiller 8ea8a96766 fix: git push HEAD:refs/heads/main in auto-release [skip ci] 2026-05-28 20:18:47 +00:00
jmiller 94bddceda4 Merge pull request 'chore: test release pipeline with fresh CLI tools' (#80) from dev into main
Universal: Cascade Main → Dev / Cascade main → branches (push) Failing after 2s
Generic: Repo Health / Release configuration (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
2026-05-28 20:14:39 +00:00
gitea-actions[bot] 42c21a62fe chore(version): auto-bump patch 02.16.02-dev [skip ci]
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote Pre-Release to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 18s
2026-05-28 20:14:26 +00:00
Jonathan Miller ec13a78444 Merge remote-tracking branch 'origin/main' into dev
Generic: Repo Health / Release configuration (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Successful in 5s
2026-05-28 15:13:39 -05:00
jmiller 7a1ed7548b chore: sync .mokogitea/workflows/pre-release.yml from moko-platform [skip ci] 2026-05-28 20:08:46 +00:00
jmiller e53918bc6a chore: sync .mokogitea/workflows/update-server.yml from moko-platform [skip ci] 2026-05-28 20:05:29 +00:00
jmiller 0d705b968f chore: sync .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] 2026-05-28 20:02:01 +00:00
Moko Consulting a9573e1b21 fix(workflows): rename remaining old secrets in repo-specific workflows [skip bump]
Generic: Repo Health / Release configuration (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
2026-05-28 14:50:59 -05:00
Moko Consulting efb7bf50a5 fix(workflows): GITHUB_TOKEN→GH_MIRROR_TOKEN (reserved name) [skip bump]
Generic: Repo Health / Release configuration (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
2026-05-28 14:38:07 -05:00
jmiller 658fdcca75 Merge pull request 'fix: strip -dev suffix from stable release version' (#79) from dev into main
Universal: Cascade Main → Dev / Cascade main → branches (push) Failing after 2s
Generic: Repo Health / Release configuration (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
2026-05-28 19:36:19 +00:00
gitea-actions[bot] 66c433db2c chore(version): auto-bump patch 02.16.01-dev [skip ci]
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote Pre-Release to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 39s
2026-05-28 19:35:24 +00:00
Jonathan Miller e425d0f898 fix: strip -dev suffix from stable release version
Generic: Repo Health / Release configuration (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Successful in 5s
version_bump.php preserves the suffix from .mokogitea/manifest.xml,
so when dev merges to main with a -dev suffix, the stable release
gets named 02.17.00-dev instead of 02.17.00.

Fix: run version_set_platform --stability stable after bump to strip
the suffix, and also bash-strip any remaining suffix from the VERSION
output variable.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-28 14:35:14 -05:00
jmiller d48415ab5a Merge pull request 'chore: sync workflows and suffix handling fixes from dev' (#78) from dev into main
Universal: Cascade Main → Dev / Cascade main → branches (push) Failing after 3s
Generic: Repo Health / Release configuration (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
2026-05-28 19:32:36 +00:00
Jonathan Miller aa03286613 Merge remote-tracking branch 'origin/main' into dev
Generic: Repo Health / Release configuration (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Update Server / Update Server (push) Failing after 4s
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Generic: Repo Health / Release configuration (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 4s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 7s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 6s
Universal: PR Check / Validate PR (pull_request) Successful in 6s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote Pre-Release to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 35s
2026-05-28 14:29:06 -05:00
Moko Consulting f20cbdd720 chore(workflows): sync all universal workflows from moko-platform [skip bump]
Generic: Repo Health / Release configuration (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
2026-05-28 14:24:54 -05:00
Moko Consulting ac8594f43a refactor(workflows): rename secrets MOKOGITEA_TOKEN/GITHUB_TOKEN, use x-access-token [skip bump]
Joomla: Repo Health / Release configuration (push) Blocked by required conditions
Joomla: Repo Health / Scripts governance (push) Blocked by required conditions
Joomla: Repo Health / Repository health (push) Blocked by required conditions
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Joomla: Repo Health / Access control (push) Successful in 1s
2026-05-28 14:23:14 -05:00
Moko Consulting dfd50cc48d fix(workflows): proper suffix handling — use version_set_platform instead of sed [skip bump]
Joomla: Repo Health / Release configuration (push) Blocked by required conditions
Joomla: Repo Health / Scripts governance (push) Blocked by required conditions
Joomla: Repo Health / Repository health (push) Blocked by required conditions
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Joomla: Repo Health / Access control (push) Successful in 2s
2026-05-28 14:14:42 -05:00
gitea-actions[bot] c58539bd90 chore(release): build 02.16.00-dev [skip ci] 2026-05-28 19:01:51 +00:00
jmiller 7745d98bdc Merge pull request 'chore: cascade main → dev (29eb66e) [skip ci]' (#77) from main into dev
chore: cascade main → dev [skip ci]
2026-05-28 19:01:45 +00:00
jmiller 29eb66e921 Merge pull request 'fix: alias offline timing - merge dev to main' (#76) from dev into main
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 2s
Joomla: Repo Health / Release configuration (push) Blocked by required conditions
Joomla: Repo Health / Scripts governance (push) Blocked by required conditions
Joomla: Repo Health / Repository health (push) Blocked by required conditions
Joomla: Repo Health / Access control (push) Successful in 1s
Release 02.15 - workflow suffix changes
2026-05-28 19:01:41 +00:00
gitea-actions[bot] 73e5c51b69 chore(version): auto-bump patch 02.15.02-dev [skip ci]
Universal: Build & Release / Promote Pre-Release to RC (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 13s
2026-05-28 18:36:24 +00:00
Jonathan Miller 43b3e204d3 feat: append stability suffix to manifest version fields and zip filenames
Joomla: Repo Health / Release configuration (push) Blocked by required conditions
Joomla: Repo Health / Scripts governance (push) Blocked by required conditions
Joomla: Repo Health / Repository health (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Joomla: Repo Health / Release configuration (pull_request) Blocked by required conditions
Joomla: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Joomla: Repo Health / Repository health (pull_request) Blocked by required conditions
Joomla: Repo Health / Access control (push) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 3s
Universal: Auto Version Bump / Version Bump (push) Successful in 6s
Joomla: Extension CI / Release Readiness Check (pull_request) Successful in 5s
Joomla: Repo Health / Access control (pull_request) Successful in 3s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 7s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 6s
Universal: PR Check / Changelog Updated (pull_request) Failing after 6s
All pre-release streams (dev, alpha, beta, rc) now write the suffix
directly into manifest <version> tags (e.g., 01.02.20-dev) rather than
only showing it in updates.xml display_version. This ensures:
- Manifest version field matches the release channel
- Zip filenames include the suffix
- Joomla extension manager shows the full versioned name

Workflows updated: auto-bump.yml, update-server.yml, pre-release.yml
Stable releases (main) are unaffected — no suffix appended.

Authored-by: Moko Consulting

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-28 13:36:17 -05:00
gitea-actions[bot] a992215ba5 chore(version): patch bump to 02.15.01 [skip ci] 2026-05-28 18:24:16 +00:00
Jonathan Miller 1eecb79289 Merge remote-tracking branch 'origin/dev'
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 3s
Joomla: Repo Health / Release configuration (push) Blocked by required conditions
Joomla: Repo Health / Scripts governance (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Repo Health / Repository health (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Joomla: Repo Health / Release configuration (pull_request) Blocked by required conditions
Joomla: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Joomla: Repo Health / Repository health (pull_request) Blocked by required conditions
Joomla: Repo Health / Access control (push) Successful in 3s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Joomla: Repo Health / Access control (pull_request) Successful in 3s
Joomla: Extension CI / Release Readiness Check (pull_request) Successful in 6s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 8s
Universal: PR Check / Changelog Updated (pull_request) Failing after 8s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 9s
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
Update Server / Update Server (push) Failing after 12s
# Conflicts:
#	.mokogitea/manifest.xml
#	README.md
#	src/packages/com_mokowaas/mokowaas.xml
#	src/packages/plg_system_mokowaas/mokowaas.xml
#	src/packages/plg_webservices_mokowaas/mokowaas.xml
#	src/packages/plg_webservices_perfectpublisher/perfectpublisher.xml
#	src/pkg_mokowaas.xml
2026-05-28 13:23:51 -05:00
jmiller c312761148 chore: sync updates.xml from development [skip ci] 2026-05-28 18:22:27 +00:00
gitea-actions[bot] 1b64b0d156 chore: update development channel 02.14.02 [skip ci] 2026-05-28 18:22:25 +00:00
gitea-actions[bot] 319b43d63d chore(version): auto-bump 02.14.02 [skip ci] 2026-05-28 18:22:23 +00:00
jmiller 479daf4a43 Merge pull request 'fix: move handleSiteAlias() to onAfterInitialise for offline timing' (#75) from fix/alias-offline-timing into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Joomla: Repo Health / Release configuration (push) Blocked by required conditions
Joomla: Repo Health / Scripts governance (push) Blocked by required conditions
Joomla: Repo Health / Repository health (push) Blocked by required conditions
Joomla: Repo Health / Access control (push) Successful in 2s
Update Server / Update Server (push) Successful in 17s
Merge fix/alias-offline-timing into dev

Fixes #72
2026-05-28 18:22:07 +00:00
Jonathan Miller e932cccbf6 fix: move handleSiteAlias() to onAfterInitialise for offline timing
Joomla: Repo Health / Release configuration (push) Blocked by required conditions
Joomla: Repo Health / Scripts governance (push) Blocked by required conditions
Joomla: Repo Health / Repository health (push) Blocked by required conditions
Joomla: Repo Health / Access control (push) Successful in 2s
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Joomla: Repo Health / Release configuration (pull_request) Blocked by required conditions
Joomla: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Joomla: Repo Health / Repository health (pull_request) Blocked by required conditions
Universal: PR Check / Changelog Updated (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Joomla: Repo Health / Access control (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 4s
Branch Cleanup / Delete merged branch (pull_request) Successful in 3s
Update Server / Update Server (pull_request) Failing after 16s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Successful in 16s
The alias offline parameter was not working because handleSiteAlias()
ran in onAfterRoute(), but Joomla's SiteApplication::doExecute() checks
the offline config before that event fires. Moving it to
onAfterInitialise() ensures the config is set before Joomla's check.

Fixes #72

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-28 13:20:37 -05:00
gitea-actions[bot] 1a4c02a098 chore(release): build 02.15.00 [skip ci] 2026-05-28 18:14:13 +00:00
jmiller 33b34e6250 Merge pull request 'chore: cascade main → dev (99f3bd4) [skip ci]' (#74) from main into dev
chore: cascade main → dev [skip ci]
2026-05-28 18:14:03 +00:00
jmiller 99f3bd47e0 Merge pull request 'fix: package install path - add folder=packages to manifest' (#73) from dev into main
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 3s
Joomla: Repo Health / Release configuration (push) Blocked by required conditions
Joomla: Repo Health / Scripts governance (push) Blocked by required conditions
Joomla: Repo Health / Repository health (push) Blocked by required conditions
Joomla: Repo Health / Access control (push) Successful in 1s
2026-05-28 18:13:59 +00:00
gitea-actions[bot] 6907046dae chore(version): patch bump to 02.14.01 [skip ci]
Universal: Build & Release / Promote Pre-Release to RC (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 17s
2026-05-28 18:13:30 +00:00
Jonathan Miller 7e597674ac Merge remote-tracking branch 'origin/main' into dev
Joomla: Repo Health / Release configuration (push) Blocked by required conditions
Joomla: Repo Health / Scripts governance (push) Blocked by required conditions
Joomla: Repo Health / Repository health (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Joomla: Repo Health / Release configuration (pull_request) Blocked by required conditions
Joomla: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Joomla: Repo Health / Repository health (pull_request) Blocked by required conditions
Joomla: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Joomla: Repo Health / Access control (pull_request) Successful in 2s
Joomla: Extension CI / Release Readiness Check (pull_request) Successful in 6s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 7s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 7s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Universal: PR Check / Changelog Updated (pull_request) Failing after 6s
Universal: Auto Version Bump / Version Bump (push) Successful in 6s
Update Server / Update Server (push) Failing after 10s
# Conflicts:
#	README.md
#	src/packages/com_mokowaas/mokowaas.xml
#	src/packages/plg_system_mokowaas/mokowaas.xml
#	src/packages/plg_webservices_mokowaas/mokowaas.xml
#	src/packages/plg_webservices_perfectpublisher/perfectpublisher.xml
#	src/pkg_mokowaas.xml
2026-05-28 13:13:20 -05:00
gitea-actions[bot] 47aeb98201 chore(version): patch bump to 02.13.04 [skip ci] 2026-05-28 18:10:20 +00:00
Jonathan Miller 58f2571dc4 fix: add folder=\"packages\" to pkg_mokowaas.xml files element
Joomla: Repo Health / Release configuration (push) Blocked by required conditions
Joomla: Repo Health / Scripts governance (push) Blocked by required conditions
Joomla: Repo Health / Repository health (push) Blocked by required conditions
Joomla: Repo Health / Access control (push) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Successful in 6s
Update Server / Update Server (push) Failing after 10s
The release_package.php builds sub-package ZIPs into a packages/
subdirectory, but the manifest referenced them at root level. Joomla's
package installer needs the folder attribute to find the ZIPs.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-28 13:10:09 -05:00
gitea-actions[bot] e3ba98499e chore(release): build 02.14.00 [skip ci] 2026-05-28 17:41:26 +00:00
jmiller 5b17f5c5ec Merge pull request 'chore: cascade main → dev (9b9e876) [skip ci]' (#71) from main into dev
chore: cascade main → dev [skip ci]
2026-05-28 17:41:16 +00:00
jmiller 9b9e8764da Merge pull request 'fix: push release commit to main instead of detached HEAD' (#70) from dev into main
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 4s
Joomla: Repo Health / Release configuration (push) Blocked by required conditions
Joomla: Repo Health / Scripts governance (push) Blocked by required conditions
Joomla: Repo Health / Repository health (push) Blocked by required conditions
Joomla: Repo Health / Access control (push) Successful in 1s
2026-05-28 17:41:11 +00:00
gitea-actions[bot] 26646eac57 chore(version): patch bump to 02.13.03 [skip ci]
Universal: Build & Release / Promote Pre-Release to RC (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 19s
2026-05-28 17:38:42 +00:00
Jonathan Miller e0518c20fe fix: push release commit to refs/heads/main instead of detached HEAD
Joomla: Repo Health / Release configuration (push) Blocked by required conditions
Joomla: Repo Health / Scripts governance (push) Blocked by required conditions
Joomla: Repo Health / Repository health (push) Blocked by required conditions
Joomla: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 5s
Root cause: Gitea Actions checks out the merge commit SHA for PR events,
putting git in detached HEAD state. "git push origin HEAD" then creates
a dangling ref instead of updating main.

Fix: use "git push origin HEAD:refs/heads/main" to explicitly target
the main branch.

Also: fail the workflow if .mokogitea/manifest.xml is missing.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-28 12:38:35 -05:00
jmiller 278e5d45f6 Merge pull request 'chore: cascade main → dev (0d24862) [skip ci]' (#69) from main into dev
chore: cascade main → dev [skip ci]
2026-05-28 17:30:08 +00:00
jmiller 0d24862302 Merge pull request 'feat: add Perfect Publisher web services API plugin' (#68) from dev into main
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 4s
Joomla: Repo Health / Release configuration (push) Blocked by required conditions
Joomla: Repo Health / Scripts governance (push) Blocked by required conditions
Joomla: Repo Health / Repository health (push) Blocked by required conditions
Joomla: Repo Health / Access control (push) Successful in 1s
2026-05-28 17:30:01 +00:00
gitea-actions[bot] 94d45169ef chore(version): patch bump to 02.13.02 [skip ci]
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote Pre-Release to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 15s
2026-05-28 17:24:51 +00:00
Jonathan Miller 17fd3d6b0e feat: add Perfect Publisher web services API plugin
Joomla: Repo Health / Release configuration (push) Blocked by required conditions
Joomla: Repo Health / Scripts governance (push) Blocked by required conditions
Joomla: Repo Health / Repository health (push) Blocked by required conditions
Joomla: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 6s
Update Server / Update Server (push) Failing after 14s
New plg_webservices_perfectpublisher provides REST API for Perfect
Publisher (com_autotweet):

- GET  /v1/perfectpublisher/channels      List social channels
- GET  /v1/perfectpublisher/channels/:id   Channel detail (OAuth redacted)
- GET  /v1/perfectpublisher/posts          List posts (filter by status/channel)
- GET  /v1/perfectpublisher/posts/:id      Post detail
- GET  /v1/perfectpublisher/requests       Pending publish requests
- POST /v1/perfectpublisher/requests       Create publish request
- GET  /v1/perfectpublisher/rules          Publishing rules
- GET  /v1/perfectpublisher/feeds          RSS feeds
- GET  /v1/perfectpublisher/channeltypes   Channel type definitions
- GET  /v1/perfectpublisher/stats          Dashboard statistics

Added to pkg_mokowaas.xml package manifest.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-28 12:24:33 -05:00
jmiller f26595bed4 Merge pull request 'chore: cascade main → dev (7074893) [skip ci]' (#67) from main into dev
chore: cascade main → dev [skip ci]
2026-05-28 16:15:58 +00:00
jmiller 70748938d2 Merge pull request 'fix: second version_set_platform pass before release commit' (#66) from dev into main
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 4s
Joomla: Repo Health / Release configuration (push) Blocked by required conditions
Joomla: Repo Health / Scripts governance (push) Blocked by required conditions
Joomla: Repo Health / Repository health (push) Blocked by required conditions
Joomla: Repo Health / Access control (push) Successful in 2s
2026-05-28 16:15:52 +00:00
gitea-actions[bot] dcdc3debb8 chore(version): patch bump to 02.13.01 [skip ci]
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote Pre-Release to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 18s
2026-05-28 16:15:10 +00:00
Jonathan Miller e2782b4fb7 Merge remote-tracking branch 'origin/main' into dev
Joomla: Repo Health / Release configuration (push) Blocked by required conditions
Joomla: Repo Health / Scripts governance (push) Blocked by required conditions
Joomla: Repo Health / Repository health (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Joomla: Repo Health / Release configuration (pull_request) Blocked by required conditions
Joomla: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Joomla: Repo Health / Repository health (pull_request) Blocked by required conditions
Joomla: Repo Health / Access control (push) Successful in 3s
Universal: PR Check / Branch Policy (pull_request) Successful in 3s
Joomla: Repo Health / Access control (pull_request) Successful in 2s
Joomla: Extension CI / Release Readiness Check (pull_request) Successful in 6s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 7s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 8s
Universal: PR Check / Changelog Updated (pull_request) Failing after 7s
Universal: Auto Version Bump / Version Bump (push) Successful in 7s
Update Server / Update Server (push) Failing after 11s
# Conflicts:
#	README.md
#	src/packages/com_mokowaas/mokowaas.xml
#	src/packages/plg_system_mokowaas/mokowaas.xml
#	src/packages/plg_webservices_mokowaas/mokowaas.xml
#	src/pkg_mokowaas.xml
2026-05-28 11:14:57 -05:00
gitea-actions[bot] 178ca0499e chore(version): patch bump to 02.12.02 [skip ci] 2026-05-28 16:12:17 +00:00
Jonathan Miller 324baff9b9 fix: add second version_set_platform pass before release commit
Joomla: Repo Health / Release configuration (push) Blocked by required conditions
Joomla: Repo Health / Scripts governance (push) Blocked by required conditions
Joomla: Repo Health / Repository health (push) Blocked by required conditions
Joomla: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 5s
Run version_set_platform.php and version_check.php --fix a second time
right before the commit to catch any manifest files that got stale
during the build/release steps. Also add debug output to diagnose
commit step issues.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-28 11:12:06 -05:00
jmiller 22f0bb9a6f fix: updates.xml all channels at 02.13.00 with SHA [skip ci] 2026-05-28 16:10:17 +00:00
jmiller 616e82ae26 fix: updates.xml to 02.13.00 with SHA [skip ci] 2026-05-28 16:09:54 +00:00
jmiller ec5a22b37f fix: align README.md to 02.13.00 release [skip ci] 2026-05-28 16:09:51 +00:00
jmiller 445f5e7060 fix: align src/packages/plg_webservices_mokowaas/mokowaas.xml to 02.13.00 release [skip ci] 2026-05-28 16:09:48 +00:00
jmiller eaf46e7ea3 fix: align src/packages/com_mokowaas/mokowaas.xml to 02.13.00 release [skip ci] 2026-05-28 16:09:46 +00:00
jmiller 303af17971 fix: align src/packages/plg_system_mokowaas/mokowaas.xml to 02.13.00 release [skip ci] 2026-05-28 16:09:42 +00:00
jmiller 7e0aa36ffa fix: align src/pkg_mokowaas.xml to 02.13.00 release [skip ci] 2026-05-28 16:09:40 +00:00
jmiller 102bea980b Merge pull request 'chore: cascade main → dev (ed95dcb) [skip ci]' (#65) from main into dev
chore: cascade main → dev [skip ci]
2026-05-28 16:01:36 +00:00
jmiller ed95dcb7af Merge pull request 'fix: rewrite release workflows and fix version propagation' (#64) from dev into main
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 4s
Joomla: Repo Health / Release configuration (push) Blocked by required conditions
Joomla: Repo Health / Scripts governance (push) Blocked by required conditions
Joomla: Repo Health / Repository health (push) Blocked by required conditions
Joomla: Repo Health / Access control (push) Successful in 1s
2026-05-28 16:01:29 +00:00
gitea-actions[bot] 56abe3af7f chore(version): patch bump to 02.12.01 [skip ci]
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote Pre-Release to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 18s
2026-05-28 16:01:25 +00:00
Jonathan Miller 5b5245c170 Merge remote-tracking branch 'origin/main' into dev
Joomla: Repo Health / Release configuration (push) Blocked by required conditions
Joomla: Repo Health / Scripts governance (push) Blocked by required conditions
Joomla: Repo Health / Repository health (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Joomla: Repo Health / Release configuration (pull_request) Blocked by required conditions
Joomla: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Joomla: Repo Health / Repository health (pull_request) Blocked by required conditions
Joomla: Repo Health / Access control (push) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Joomla: Repo Health / Access control (pull_request) Successful in 3s
Joomla: Extension CI / Release Readiness Check (pull_request) Successful in 7s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 6s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 9s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Universal: PR Check / Changelog Updated (pull_request) Failing after 7s
Universal: Auto Version Bump / Version Bump (push) Successful in 7s
Update Server / Update Server (push) Failing after 12s
# Conflicts:
#	README.md
#	src/packages/com_mokowaas/mokowaas.xml
#	src/packages/plg_system_mokowaas/mokowaas.xml
#	src/packages/plg_webservices_mokowaas/mokowaas.xml
#	src/pkg_mokowaas.xml
2026-05-28 11:01:13 -05:00
gitea-actions[bot] 167a7c0dfd chore(version): patch bump to 02.12.01 [skip ci] 2026-05-28 15:59:48 +00:00
Jonathan Miller 62788853ea fix: rewrite release workflows to use CLI and fix version propagation
Joomla: Repo Health / Release configuration (push) Blocked by required conditions
Joomla: Repo Health / Scripts governance (push) Blocked by required conditions
Joomla: Repo Health / Repository health (push) Blocked by required conditions
Joomla: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 5s
auto-release.yml:
- Defer commit until AFTER updates.xml is written so version bump +
  manifests + updates.xml all go in one atomic push
- Add final version_check.php --fix before commit to catch missed files
- Remove || true from critical git push

update-server.yml:
- Replace ~400 lines of inline bash/python with CLI tool calls
- Use updates_xml_build.php for XML generation (handles name prefix,
  SHA cascade, creationDate, all 5 channels)
- Use release_create.php + release_package.php for Gitea releases
- Keep SFTP deploy and main sync as-is

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-28 10:59:38 -05:00
jmiller 6f7cb11e39 fix: align README version to 02.12.00 release [skip ci] 2026-05-28 15:49:37 +00:00
jmiller df22d7f7c0 fix: align manifest version to 02.12.00 release [skip ci] 2026-05-28 15:49:24 +00:00
jmiller 5984529569 fix: align manifest version to 02.12.00 release [skip ci] 2026-05-28 15:49:21 +00:00
jmiller 8be05b75b7 fix: align manifest version to 02.12.00 release [skip ci] 2026-05-28 15:49:18 +00:00
jmiller a02e466456 fix: align manifest version to 02.12.00 release [skip ci] 2026-05-28 15:49:15 +00:00
jmiller 2ede62b8b9 fix: updates.xml with all 5 channels at 02.12.00 [skip ci] 2026-05-28 15:44:09 +00:00
jmiller f5d06e6e25 fix: updates.xml with all 5 channels at 02.12.00 [skip ci] 2026-05-28 15:43:54 +00:00
jmiller 7370757e46 Merge pull request 'chore: cascade main → dev (3aa7364) [skip ci]' (#63) from main into dev
chore: cascade main → dev [skip ci]
2026-05-28 15:31:49 +00:00
jmiller 3aa7364783 Merge pull request 'fix: clean up updates.xml and remove duplicate update server' (#62) from dev into main
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 3s
Joomla: Repo Health / Release configuration (push) Blocked by required conditions
Joomla: Repo Health / Scripts governance (push) Blocked by required conditions
Joomla: Repo Health / Repository health (push) Blocked by required conditions
Joomla: Repo Health / Access control (push) Successful in 1s
fix: clean up updates.xml, remove duplicate update server, auto-cleanup stale plugin update sites
2026-05-28 15:31:44 +00:00
gitea-actions[bot] df711f9a17 chore(version): patch bump to 02.11.04 [skip ci]
Universal: Build & Release / Promote Pre-Release to RC (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 13s
2026-05-28 15:30:15 +00:00
Jonathan Miller 1d2252e8b4 fix: auto-cleanup stale plugin update site on install/update
Joomla: Repo Health / Release configuration (push) Blocked by required conditions
Joomla: Repo Health / Scripts governance (push) Blocked by required conditions
Joomla: Repo Health / Repository health (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Joomla: Repo Health / Release configuration (pull_request) Blocked by required conditions
Joomla: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Joomla: Repo Health / Repository health (pull_request) Blocked by required conditions
Joomla: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Joomla: Repo Health / Access control (pull_request) Successful in 2s
Joomla: Extension CI / Release Readiness Check (pull_request) Successful in 4s
Universal: Auto Version Bump / Version Bump (push) Successful in 5s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 5s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 6s
Universal: PR Check / Changelog Updated (pull_request) Successful in 6s
Update Server / Update updates.xml (push) Failing after 8s
Add cleanupPluginUpdateSite() to script.php postflight that removes
the plugin-level update site entry from #__update_sites and
#__update_sites_extensions. This prevents Joomla from checking for
plugin-level updates that don't exist, which caused download failures
on stable-only sites.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-28 10:30:08 -05:00
gitea-actions[bot] c19d4da411 chore(version): patch bump to 02.11.03 [skip ci] 2026-05-28 15:24:46 +00:00
Jonathan Miller 45e9091fd0 fix: clean up updates.xml and remove duplicate plugin update server
Joomla: Repo Health / Release configuration (push) Blocked by required conditions
Joomla: Repo Health / Scripts governance (push) Blocked by required conditions
Joomla: Repo Health / Repository health (push) Blocked by required conditions
Joomla: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 4s
Update Server / Update updates.xml (push) Failing after 8s
- Remove stale pre-release entries pointing to non-existent dev artifacts
- Remove legacy plugin update entry (element=mokowaas, type=plugin) that
  caused stable-only sites to attempt dev downloads
- Remove <updateservers> from inner plugin manifest — only the package-level
  manifest (pkg_mokowaas.xml) should register the update server
- Keep only the stable channel entry (only release that exists on server)

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-28 10:24:12 -05:00
jmiller fb0ca184b9 docs: add infrastructure changes to CHANGELOG [skip ci] 2026-05-27 05:46:02 +00:00
jmiller c1e668e644 docs: update CHANGELOG with infrastructure changes [skip ci] 2026-05-27 05:28:33 +00:00
jmiller 985034650b chore: add branch-cleanup workflow [skip ci] 2026-05-27 03:52:36 +00:00
gitea-actions[bot] 0fdc91d50c feat(ci): add version branch creation on stable release [skip ci] 2026-05-27 02:19:27 +00:00
jmiller 9a7d5b8359 Merge pull request 'chore: cascade main → dev (9c9a1a7) [skip ci]' (#61) from main into dev
chore: cascade main → dev [skip ci]
2026-05-26 23:47:22 +00:00
jmiller 9c9a1a7b52 Merge pull request 'fix: trusted IP session bypass + CI workflow syncs' (#60) from dev into main
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 3s
Joomla: Repo Health / Access control (push) Successful in 1s
Joomla: Repo Health / Release configuration (push) Has been cancelled
Joomla: Repo Health / Scripts governance (push) Has been cancelled
Joomla: Repo Health / Repository health (push) Has been cancelled
2026-05-26 23:47:18 +00:00
gitea-actions[bot] 21de2fa115 chore(version): patch bump to 02.11.02 [skip ci]
Universal: Build & Release / Promote Pre-Release to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 13s
2026-05-26 23:46:20 +00:00
Jonathan Miller 9f1848d218 fix: move trusted IP session bypass to boot() for early execution
Joomla: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 4s
Update Server / Update updates.xml (push) Failing after 8s
Joomla: Repo Health / Release configuration (push) Has been cancelled
Joomla: Repo Health / Scripts governance (push) Has been cancelled
Joomla: Repo Health / Repository health (push) Has been cancelled
Joomla validates sessions during initialise(), before onAfterInitialise
fires. The previous ini_set approach ran too late — the session was
already expired. Now implements BootableExtensionInterface so the
session lifetime is extended before Joomla's session handler runs.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 18:43:13 -05:00
jmiller ecb456d91e chore(ci): update pre-release.yml from moko-platform [skip ci] 2026-05-26 22:50:50 +00:00
jmiller d9ce74cf38 chore(ci): update pre-release.yml from moko-platform [skip ci] 2026-05-26 22:50:50 +00:00
jmiller 91e9465233 chore(ci): update auto-bump.yml from moko-platform [skip ci] 2026-05-26 22:49:38 +00:00
jmiller 3bbaee7c86 chore(ci): update auto-bump.yml from moko-platform [skip ci] 2026-05-26 22:49:37 +00:00
jmiller d494e7366e chore(ci): update auto-release.yml from moko-platform [skip ci] 2026-05-26 22:48:25 +00:00
jmiller 05c3f5fd1f chore(ci): update auto-release.yml from moko-platform [skip ci] 2026-05-26 22:48:24 +00:00
jmiller c91b44ad34 chore(ci): update pre-release.yml from moko-platform [skip ci] 2026-05-26 22:36:53 +00:00
jmiller e86cc2b48b chore(ci): update pre-release.yml from moko-platform [skip ci] 2026-05-26 22:36:53 +00:00
jmiller c28c2de936 chore(ci): update auto-release.yml from moko-platform [skip ci] 2026-05-26 22:35:32 +00:00
jmiller 1a81267d38 chore(ci): update auto-release.yml from moko-platform [skip ci] 2026-05-26 22:35:31 +00:00
jmiller 343ef64ea2 chore(ci): update auto-bump.yml from moko-platform [skip ci] 2026-05-26 22:25:08 +00:00
jmiller f47a4d3c77 chore(ci): update auto-bump.yml from moko-platform [skip ci] 2026-05-26 22:25:08 +00:00
jmiller cfb05c5964 chore(ci): update auto-release.yml from moko-platform [skip ci] 2026-05-26 22:23:51 +00:00
jmiller ebbd1058f3 chore(ci): update auto-release.yml from moko-platform [skip ci] 2026-05-26 22:23:51 +00:00
jmiller 9e356fa4b5 chore(ci): update pre-release.yml from moko-platform [skip ci] 2026-05-26 22:13:16 +00:00
jmiller e030d85886 chore(ci): update pre-release.yml from moko-platform [skip ci] 2026-05-26 22:13:15 +00:00
jmiller ea9ac21d1a chore(ci): add auto-bump.yml from moko-platform [skip ci] 2026-05-26 22:12:03 +00:00
jmiller e256acbcbb chore(ci): add auto-bump.yml from moko-platform [skip ci] 2026-05-26 22:12:03 +00:00
jmiller b4d11df2a2 Merge pull request 'chore: cascade main → dev (2c0ed08) [skip ci]' (#59) from main into dev
chore: cascade main → dev [skip ci]
2026-05-26 21:44:02 +00:00
jmiller 2c0ed08368 Merge pull request 'feat: show current IP in security tab' (#58) from dev into main
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 2s
Joomla: Repo Health / Access control (push) Successful in 2s
Joomla: Repo Health / Release configuration (push) Has been cancelled
Joomla: Repo Health / Scripts governance (push) Has been cancelled
Joomla: Repo Health / Repository health (push) Has been cancelled
2026-05-26 21:43:59 +00:00
jmiller 12fe6c196d chore: sync updates.xml from [skip ci] 2026-05-26 21:42:28 +00:00
gitea-actions[bot] 0415972c7d chore: update updates.xml (development: 02.11.01-dev) [skip ci]
Universal: Build & Release / Promote Pre-Release to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 12s
2026-05-26 21:42:27 +00:00
gitea-actions[bot] 6c7bb35ac3 chore(version): auto-bump patch 02.11.01 [skip ci] 2026-05-26 21:42:26 +00:00
Jonathan Miller 834b1325b5 Merge remote-tracking branch 'origin/main' into dev
Joomla: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Joomla: Repo Health / Access control (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 4s
Joomla: Extension CI / Release Readiness Check (pull_request) Successful in 5s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 4s
Universal: PR Check / Changelog Updated (pull_request) Successful in 4s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 6s
Update Server / Update updates.xml (push) Failing after 11s
Joomla: Repo Health / Release configuration (push) Has been cancelled
Joomla: Repo Health / Scripts governance (push) Has been cancelled
Joomla: Repo Health / Repository health (push) Has been cancelled
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Has been cancelled
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Has been cancelled
Joomla: Extension CI / PHPStan Analysis (pull_request) Has been cancelled
Joomla: Extension CI / Build RC Pre-Release (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Joomla: Repo Health / Release configuration (pull_request) Has been cancelled
Joomla: Repo Health / Scripts governance (pull_request) Has been cancelled
Joomla: Repo Health / Repository health (pull_request) Has been cancelled
# Conflicts:
#	.mokogitea/manifest.xml
#	.mokogitea/workflows/update-server.yml
#	README.md
#	src/packages/com_mokowaas/mokowaas.xml
#	src/packages/plg_system_mokowaas/mokowaas.xml
#	src/packages/plg_webservices_mokowaas/mokowaas.xml
#	src/pkg_mokowaas.xml
2026-05-26 16:42:01 -05:00
jmiller 4a1b2ea143 chore: sync updates.xml from [skip ci] 2026-05-26 21:30:16 +00:00
gitea-actions[bot] a748ee863c chore: update updates.xml (development: 02.10.06-dev) [skip ci] 2026-05-26 21:30:15 +00:00
gitea-actions[bot] 0546e1eaae chore(version): auto-bump patch 02.10.06 [skip ci] 2026-05-26 21:30:15 +00:00
Jonathan Miller 4595db209e feat: show current IP above trusted IPs table
Joomla: Repo Health / Access control (push) Successful in 2s
Update Server / Update updates.xml (push) Failing after 7s
Joomla: Repo Health / Release configuration (push) Has been cancelled
Joomla: Repo Health / Scripts governance (push) Has been cancelled
Joomla: Repo Health / Repository health (push) Has been cancelled
Display the admin's current IP address with a hint to add it,
making it easy to configure trusted IP entries without looking
up the IP separately.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 16:29:56 -05:00
gitea-actions[bot] 3f3ff49573 chore(release): build 02.11.00 [skip ci] 2026-05-26 20:16:41 +00:00
jmiller 14318c90c2 chore: sync .mokogitea/workflows/update-server.yml from moko-platform [skip ci] 2026-05-26 20:12:32 +00:00
gitea-actions[bot] 3d79fe9aeb chore(release): build 02.10.05 [skip ci] 2026-05-26 20:12:26 +00:00
jmiller d2e24741af chore: sync .mokogitea/workflows/pre-release.yml from moko-platform [skip ci] 2026-05-26 20:10:36 +00:00
jmiller cb6582ef16 fix(ci): use release_package.php for Joomla package builds [skip ci] 2026-05-26 19:54:40 +00:00
jmiller d0c3a563d1 fix(ci): use release_package.php for Joomla package builds [skip ci] 2026-05-26 19:54:40 +00:00
jmiller 70b5c8de08 Merge pull request 'chore: cascade main → dev (a2eaf54) [skip ci]' (#56) from main into dev
chore: cascade main → dev [skip ci]
2026-05-26 19:44:26 +00:00
jmiller a2eaf549af Merge pull request 'feat: trusted IPs bypass admin session timeout' (#55) from dev into main
Joomla: Repo Health / Access control (push) Successful in 1s
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 2s
Joomla: Repo Health / Release configuration (push) Has been cancelled
Joomla: Repo Health / Scripts governance (push) Has been cancelled
Joomla: Repo Health / Repository health (push) Has been cancelled
Merge pull request #55: feat: trusted IPs bypass admin session timeout
2026-05-26 19:44:22 +00:00
gitea-actions[bot] c97c29f9ed chore: update updates.xml (development: 02.10.05-dev) [skip ci]
Universal: Build & Release / Promote Pre-Release to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Failing after 9s
2026-05-26 19:40:54 +00:00
jmiller ea48f61f8c chore: sync updates.xml from [skip ci] 2026-05-26 19:40:54 +00:00
gitea-actions[bot] d92df704c4 chore(version): auto-bump patch 02.10.05 [skip ci] 2026-05-26 19:40:52 +00:00
Jonathan Miller ad4c658b3d feat: trusted IPs bypass admin session timeout
Joomla: Repo Health / Access control (push) Successful in 1s
Update Server / Update updates.xml (push) Failing after 11s
Joomla: Repo Health / Release configuration (push) Has been cancelled
Joomla: Repo Health / Scripts governance (push) Has been cancelled
Joomla: Repo Health / Repository health (push) Has been cancelled
Add configurable repeatable rows of trusted IP addresses that bypass
the admin session timeout. Supports exact IPs, CIDR ranges, and
wildcard patterns with per-entry labels and enabled toggles.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 14:40:32 -05:00
jmiller 0788e8e2ab Merge pull request 'chore: cascade main → dev (a68e90d) [skip ci]' (#54) from main into dev
chore: cascade main → dev [skip ci]
2026-05-26 19:34:05 +00:00
Jonathan Miller a68e90df9d Merge dev: fix(joomla6) webservices plugin + updated workflows
Joomla: Repo Health / Access control (push) Successful in 1s
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 2s
Joomla: Repo Health / Release configuration (push) Has been cancelled
Joomla: Repo Health / Scripts governance (push) Has been cancelled
Joomla: Repo Health / Repository health (push) Has been cancelled
2026-05-26 14:34:00 -05:00
jmiller bacc0eba19 chore: sync .mokogitea/workflows/update-server.yml from moko-platform [skip ci] 2026-05-26 19:03:56 +00:00
gitea-actions[bot] c8f4e38f6b chore(release): build 02.10.03 [skip ci] 2026-05-26 18:58:54 +00:00
jmiller 0dcb8a4a1d chore(ci): update pre-release.yml from moko-platform [skip ci] 2026-05-26 18:52:46 +00:00
jmiller fa31455619 chore(ci): update auto-release.yml from moko-platform [skip ci] 2026-05-26 18:52:45 +00:00
jmiller bf4dfac2a0 chore(ci): update pre-release.yml — remove paths filter [skip ci] 2026-05-26 18:51:33 +00:00
jmiller d3ceea0e80 chore(ci): update auto-release.yml — remove paths filter [skip ci] 2026-05-26 18:51:32 +00:00
jmiller 49a7418830 chore: sync updates.xml from [skip ci] 2026-05-26 18:42:43 +00:00
gitea-actions[bot] 1b9fc4e0f8 chore: update updates.xml (development: 02.10.04-dev) [skip ci] 2026-05-26 18:42:43 +00:00
gitea-actions[bot] 426853aef7 chore(version): auto-bump patch 02.10.04 [skip ci] 2026-05-26 18:42:41 +00:00
Jonathan Miller 3f20ad985c fix(joomla6): update webservices plugin for Joomla 6 typed event API
Joomla: Repo Health / Access control (push) Successful in 1s
Joomla: Update Server / Update updates.xml (push) Failing after 9s
Joomla: Repo Health / Release configuration (push) Has been cancelled
Joomla: Repo Health / Scripts governance (push) Has been cancelled
Joomla: Repo Health / Repository health (push) Has been cancelled
Joomla 6 changed SubscriberInterface event handlers to receive a typed
event object instead of pass-by-reference parameters. Update
onBeforeApiRoute() to accept BeforeApiRouteEvent and extract the
router via $event->getRouter().

Fixes #48

Authored-by: Moko Consulting
2026-05-26 13:42:32 -05:00
jmiller ffa50f6460 fix: updates.xml all channels 02.11.00 [skip ci] 2026-05-26 17:41:23 +00:00
jmiller 08e2f171eb chore(ci): update auto-release.yml from moko-platform [skip ci] 2026-05-26 17:36:27 +00:00
jmiller 9d49968272 chore(ci): update pre-release.yml from moko-platform [skip ci] 2026-05-26 17:35:16 +00:00
jmiller be98c55e46 Merge pull request 'chore: cascade main → dev (c6c9b21) [skip ci]' (#47) from main into dev
chore: cascade main → dev [skip ci]
2026-05-26 17:27:08 +00:00
jmiller c6c9b217a1 Merge pull request 'Release 02.11.00: Help menu redirect, support URL fix, CI tag fixes' (#46) from dev into main
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 3s
Joomla: Repo Health / Access control (push) Successful in 1s
Joomla: Repo Health / Release configuration (push) Has been cancelled
Joomla: Repo Health / Scripts governance (push) Has been cancelled
Joomla: Repo Health / Repository health (push) Has been cancelled
2026-05-26 17:27:03 +00:00
jmiller 657928a01a fix: stable download URL [skip ci]
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 12s
2026-05-26 12:12:33 -05:00
jmiller 91ad0353a6 fix: updates.xml version 02.09.00 [skip ci] 2026-05-26 12:12:33 -05:00
jmiller 431c907391 chore: sync updates.xml from [skip ci] 2026-05-26 17:11:07 +00:00
gitea-actions[bot] 38d5a8eb90 chore: update updates.xml (development: 02.10.03-dev) [skip ci] 2026-05-26 17:11:07 +00:00
gitea-actions[bot] 19ab206f56 chore(version): auto-bump patch 02.10.03 [skip ci] 2026-05-26 17:11:06 +00:00
Jonathan Miller 642aca10fe fix: default support URL to mokoconsulting.tech/support
Joomla: Repo Health / Access control (push) Successful in 1s
Joomla: Update Server / Update updates.xml (push) Failing after 9s
Joomla: Repo Health / Release configuration (push) Has been cancelled
Joomla: Repo Health / Scripts governance (push) Has been cancelled
Joomla: Repo Health / Repository health (push) Has been cancelled
Updated manifest default, PHP fallbacks in MokoWaaS.php and script.php.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 12:10:57 -05:00
jmiller a5b6d7a42a chore: sync updates.xml from [skip ci] 2026-05-26 17:01:34 +00:00
gitea-actions[bot] 9003570c5a chore: update updates.xml (development: 02.10.02-dev) [skip ci] 2026-05-26 17:01:33 +00:00
gitea-actions[bot] c241463bb1 chore(version): auto-bump patch 02.10.02 [skip ci] 2026-05-26 17:01:33 +00:00
Jonathan Miller 317c4e900a feat: redirect admin Help menu to configured support URL
Joomla: Repo Health / Access control (push) Successful in 2s
Joomla: Update Server / Update updates.xml (push) Failing after 9s
Joomla: Repo Health / Release configuration (push) Has been cancelled
Joomla: Repo Health / Scripts governance (push) Has been cancelled
Joomla: Repo Health / Repository health (push) Has been cancelled
Replaces hardcoded help.joomla.org and docs.joomla.org links in
the Atum template header with the WaaS support URL from plugin
config. Uses JS injection in onBeforeCompileHead.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 12:01:21 -05:00
jmiller e948074c6a chore: sync updates.xml from [skip ci] 2026-05-26 16:12:53 +00:00
gitea-actions[bot] 848f07429c chore: update updates.xml (development: 02.10.01-dev) [skip ci] 2026-05-26 16:12:53 +00:00
gitea-actions[bot] 203327f5ed chore(version): auto-bump patch 02.10.01 [skip ci] 2026-05-26 16:12:52 +00:00
Jonathan Miller 92261be464 chore: bump dev to 02.10.00 (ahead of main 02.09.00)
Joomla: Repo Health / Access control (push) Successful in 2s
Joomla: Update Server / Update updates.xml (push) Failing after 9s
Joomla: Repo Health / Release configuration (push) Has been cancelled
Joomla: Repo Health / Scripts governance (push) Has been cancelled
Joomla: Repo Health / Repository health (push) Has been cancelled
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 11:12:40 -05:00
gitea-actions[bot] 28e61b8f8a chore(version): pre-release bump to 02.08.04 [skip ci] 2026-05-26 07:06:54 +00:00
jmiller 188db2d4b8 sync: update-server.yml with updates.xml integrity check [skip ci] 2026-05-26 04:47:46 +00:00
jmiller 1f0b4596ff sync: update-server.yml with updates.xml integrity check [skip ci] 2026-05-26 04:47:46 +00:00
jmiller 1ed11dca03 fix: stable download URL [skip ci] 2026-05-26 04:40:33 +00:00
jmiller ecc5d624d5 fix: updates.xml version 02.09.00 [skip ci] 2026-05-26 04:40:04 +00:00
jmiller dac39212d7 Merge pull request 'chore: cascade main → dev (43abc65) [skip ci]' (#45) from main into dev
chore: cascade main → dev [skip ci]
2026-05-26 04:37:41 +00:00
jmiller 43abc6514e Merge pull request 'Release 02.09.00: CI fixes, update server standard, Joomla skill' (#44) from dev into main
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 3s
Joomla: Repo Health / Access control (push) Successful in 1s
Joomla: Repo Health / Release configuration (push) Has been cancelled
Joomla: Repo Health / Scripts governance (push) Has been cancelled
Joomla: Repo Health / Repository health (push) Has been cancelled
2026-05-26 04:37:36 +00:00
Jonathan Miller 8d42ef40c5 Merge remote-tracking branch 'origin/main' into dev
Joomla: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Joomla: Repo Health / Access control (pull_request) Successful in 1s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 4s
Universal: PR Check / Validate PR (pull_request) Failing after 3s
Joomla: Extension CI / Release Readiness Check (pull_request) Successful in 4s
Universal: PR Check / Changelog Updated (pull_request) Successful in 3s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 5s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 11s
Joomla: Repo Health / Release configuration (push) Has been cancelled
Joomla: Repo Health / Scripts governance (push) Has been cancelled
Joomla: Repo Health / Repository health (push) Has been cancelled
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Has been cancelled
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Has been cancelled
Joomla: Extension CI / PHPStan Analysis (pull_request) Has been cancelled
Joomla: Extension CI / Build RC Pre-Release (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Joomla: Repo Health / Release configuration (pull_request) Has been cancelled
Joomla: Repo Health / Scripts governance (pull_request) Has been cancelled
Joomla: Repo Health / Repository health (pull_request) Has been cancelled
# Conflicts:
#	.mokogitea/workflows/auto-release.yml
#	.mokogitea/workflows/pre-release.yml
#	README.md
#	src/packages/com_mokowaas/mokowaas.xml
#	src/packages/plg_system_mokowaas/mokowaas.xml
#	src/packages/plg_webservices_mokowaas/mokowaas.xml
#	src/pkg_mokowaas.xml
#	updates.xml
2026-05-25 23:37:11 -05:00
gitea-actions[bot] 0546dde89f chore(version): pre-release bump to 02.08.03 [skip ci] 2026-05-26 04:00:46 +00:00
jmiller 598ec0712c fix: updates.xml tag dev + client site [skip ci] 2026-05-26 03:52:18 +00:00
Jonathan Miller 6f9df77f79 fix(ci): remove dead XML_TAG code, CLI handles tag mapping
Joomla: Repo Health / Access control (push) Successful in 3s
Joomla: Repo Health / Release configuration (push) Has been cancelled
Joomla: Repo Health / Scripts governance (push) Has been cancelled
Joomla: Repo Health / Repository health (push) Has been cancelled
The updates_xml_build.php CLI now maps 'development' → 'dev' internally.
Removed the dead shell case block that was setting XML_TAG (unused since
the inline PHP updater was replaced with the CLI).

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-25 22:51:19 -05:00
jmiller 89aaef14e7 fix: updates.xml same version all channels + client tag [skip ci] 2026-05-26 03:33:21 +00:00
gitea-actions[bot] f72cafe4d7 chore(version): pre-release bump to 02.08.02 [skip ci] 2026-05-26 03:26:36 +00:00
Jonathan Miller a965bcf0ef refactor(ci): clean up auto-release, move logic to CLI tools
Joomla: Repo Health / Access control (push) Successful in 1s
Joomla: Repo Health / Release configuration (push) Has been cancelled
Joomla: Repo Health / Scripts governance (push) Has been cancelled
Joomla: Repo Health / Repository health (push) Has been cancelled
- Remove duplicate asset deletion loop (new loop handles all assets)
- Remove inline Python updates.xml updater (Step 5 CLI handles it)
- Remove dead Step 8 sync-to-main code (Step 5 commit+push handles it)
- Step 8b uses release_body_update.php CLI with fallback
- Step 6 tag creation always runs (removed never-set is_minor gate)
- Net: -154 lines, +25 lines

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-25 22:20:28 -05:00
Jonathan Miller bd6eec88af feat(ci): checksums as [filename].sha256 assets, not in release body
Joomla: Repo Health / Access control (push) Successful in 4s
Joomla: Repo Health / Release configuration (push) Has been cancelled
Joomla: Repo Health / Scripts governance (push) Has been cancelled
Joomla: Repo Health / Repository health (push) Has been cancelled
Upload per-file .sha256 checksum files alongside packages instead of
embedding SHA-256 in the release description. Format: "hash  filename"
matching standard sha256sum output.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-25 22:11:53 -05:00
Jonathan Miller ce7e36f779 feat(ci): add version_check.php to pre-release and auto-release workflows
Joomla: Repo Health / Access control (push) Successful in 1s
Joomla: Repo Health / Release configuration (push) Has been cancelled
Joomla: Repo Health / Scripts governance (push) Has been cancelled
Joomla: Repo Health / Repository health (push) Has been cancelled
Runs --fix mode after version bump to auto-correct any drift between
README.md and manifest XML files before building.

Refs: moko-platform#122

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-25 22:08:40 -05:00
jmiller 46b1469121 chore: sync .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] 2026-05-26 03:07:18 +00:00
jmiller 1e936a67c4 chore: sync .mokogitea/workflows/pre-release.yml from moko-platform [skip ci] 2026-05-26 03:05:25 +00:00
jmiller 0903a4b335 fix: sync updates.xml with all channels and correct versions [skip ci] 2026-05-26 03:05:10 +00:00
gitea-actions[bot] a7823c6440 chore: update updates.xml (development: 02.08.01-dev) [skip ci] 2026-05-26 03:04:40 +00:00
jmiller ed720b2ea9 chore: sync updates.xml from [skip ci] 2026-05-26 03:04:40 +00:00
gitea-actions[bot] 263ac78515 chore(version): auto-bump patch 02.08.01 [skip ci] 2026-05-26 03:04:39 +00:00
Jonathan Miller b9f83c43bc chore: reset versions to 02.08.00, sync all update streams
Joomla: Repo Health / Access control (push) Successful in 1s
Joomla: Update Server / Update updates.xml (push) Successful in 9s
Joomla: Repo Health / Release configuration (push) Has been cancelled
Joomla: Repo Health / Scripts governance (push) Has been cancelled
Joomla: Repo Health / Repository health (push) Has been cancelled
Dev is now ahead of main (02.07.00). All manifests, README, and
updates.xml aligned. Updates.xml has all 6 channels with correct
pkg_mokowaas element.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-25 22:04:25 -05:00
Jonathan Miller f4609088e3 fix(ci): auto-release preserves all update channels [skip ci]
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-25 21:56:10 -05:00
Jonathan Miller d9326ea34b fix(ci): auto-release fetches updates.xml from main before building
Joomla: Repo Health / Access control (push) Successful in 1s
Joomla: Repo Health / Release configuration (push) Failing after 8s
Joomla: Repo Health / Repository health (push) Failing after 9s
Joomla: Repo Health / Scripts governance (push) Successful in 12s
Preserves all existing channel entries (dev, alpha, beta, rc) when
adding/updating the stable entry. Previously the file on disk was
empty or stale, so the preserve logic had nothing to keep.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-25 21:54:42 -05:00
jmiller 6589adcf75 fix: restore all channel entries in updates.xml [skip ci] 2026-05-26 02:51:58 +00:00
jmiller 2e2c1b82b3 chore: sync updates.xml 02.07.00 [skip ci] 2026-05-26 02:50:57 +00:00
gitea-actions[bot] 0451fa2138 chore(version): pre-release bump to 02.06.04 [skip ci] 2026-05-26 02:50:52 +00:00
jmiller 66b90754f8 Merge pull request 'chore: cascade main → dev (e66b7e9) [skip ci]' (#43) from main into dev
chore: cascade main → dev [skip ci]
2026-05-26 02:50:37 +00:00
jmiller e66b7e9a79 Merge pull request 'Release 02.07.00: Update site fix, settings protection, master god privs' (#42) from dev into main
Joomla: Repo Health / Access control (push) Successful in 3s
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 6s
Joomla: Repo Health / Release configuration (push) Has been cancelled
Joomla: Repo Health / Scripts governance (push) Has been cancelled
Joomla: Repo Health / Repository health (push) Has been cancelled
2026-05-26 02:49:08 +00:00
Jonathan Miller 4f056763e9 Merge remote-tracking branch 'origin/main' into dev
Joomla: Repo Health / Access control (push) Successful in 2s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 5s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 6s
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Has been skipped
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Has been skipped
Joomla: Extension CI / PHPStan Analysis (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 9s
Joomla: Repo Health / Access control (pull_request) Successful in 3s
Universal: PR Check / Validate PR (pull_request) Failing after 9s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 15s
Universal: PR Check / Changelog Updated (pull_request) Successful in 10s
Universal: PR Check / Build RC Package (pull_request) Has been skipped
Joomla: Update Server / Update updates.xml (push) Failing after 25s
Joomla: Extension CI / Build RC Pre-Release (pull_request) Has been skipped
Joomla: Repo Health / Release configuration (push) Failing after 6s
Joomla: Repo Health / Scripts governance (push) Successful in 5s
Joomla: Repo Health / Repository health (push) Failing after 5s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 24s
Joomla: Repo Health / Release configuration (pull_request) Has been cancelled
Joomla: Repo Health / Scripts governance (pull_request) Has been cancelled
Joomla: Repo Health / Repository health (pull_request) Has been cancelled
# Conflicts:
#	README.md
#	updates.xml
2026-05-25 21:48:49 -05:00
jmiller de70224728 fix: updates.xml all entries use pkg_mokowaas element [skip ci] 2026-05-26 02:36:40 +00:00
gitea-actions[bot] 6f69af666f chore: update updates.xml (development: 02.05.05-dev) [skip ci] 2026-05-26 02:34:05 +00:00
jmiller 1f7278022c chore: sync updates.xml from [skip ci] 2026-05-26 02:34:05 +00:00
gitea-actions[bot] b5e8d3dfe2 chore(version): auto-bump patch 02.05.05 [skip ci] 2026-05-26 02:34:04 +00:00
Jonathan Miller 3edec0687c fix: re-enable update site disabled by Joomla's protected flag
Joomla: Repo Health / Access control (push) Successful in 1s
Joomla: Update Server / Update updates.xml (push) Successful in 8s
Joomla: Repo Health / Release configuration (push) Failing after 8s
Joomla: Repo Health / Scripts governance (push) Successful in 8s
Joomla: Repo Health / Repository health (push) Failing after 6s
Joomla automatically disables update sites for extensions with
protected=1. ensureProtectedFlag() now also checks and re-enables
the MokoWaaS update site each admin session.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-25 21:33:53 -05:00
jmiller a503e12ef9 fix: updates.xml with all channels (stable, rc, beta, alpha, dev, legacy) [skip ci] 2026-05-26 02:20:26 +00:00
Jonathan Miller ea60ac60ba fix(ci): pre-release uses updates_xml_build CLI with preserve logic
Joomla: Repo Health / Access control (push) Successful in 1s
Joomla: Repo Health / Release configuration (push) Has been cancelled
Joomla: Repo Health / Scripts governance (push) Has been cancelled
Joomla: Repo Health / Repository health (push) Has been cancelled
The inline PHP regex approach only updated existing entries — if no
matching channel entry existed, nothing was added. Now uses the
moko-platform CLI which creates new entries and preserves other channels.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-25 21:18:22 -05:00
jmiller 825820f7b9 fix: add RC and legacy entries to updates.xml [skip ci] 2026-05-26 02:17:42 +00:00
gitea-actions[bot] ba4a806cd7 chore(version): pre-release bump to 02.06.03 [skip ci] 2026-05-26 02:09:25 +00:00
Jonathan Miller effd1fd588 fix(ci): use absolute paths in package build step [skip ci]
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-25 21:09:09 -05:00
Jonathan Miller bf2b01df2d fix(ci): use absolute paths in package build step
Joomla: Repo Health / Access control (push) Successful in 1s
Joomla: Repo Health / Release configuration (push) Has been cancelled
Joomla: Repo Health / Scripts governance (push) Has been cancelled
Joomla: Repo Health / Repository health (push) Has been cancelled
Relative path ../../build/package/ broke when cd'd 3 levels deep into
src/packages/ext_name/. Now captures REPO_ROOT=$(pwd) and uses it
for zip output and cd back.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-25 21:08:41 -05:00
gitea-actions[bot] 4581088a0a chore(version): pre-release bump to 02.06.02 [skip ci] 2026-05-26 02:05:10 +00:00
gitea-actions[bot] 863dbb02f4 chore(version): pre-release bump to 02.06.01 [skip ci] 2026-05-26 02:01:47 +00:00
jmiller 8fd8015b19 chore: sync updates.xml 02.06.00 [skip ci] 2026-05-26 01:58:56 +00:00
jmiller 83ddbf0d73 Merge pull request 'chore: cascade main → dev (fad0170) [skip ci]' (#41) from main into dev
chore: cascade main → dev [skip ci]
2026-05-26 01:58:45 +00:00
jmiller fad0170cef Merge pull request 'Release 02.07.00-rc: Master god privs, settings protection, install API fix' (#40) from dev into main
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 6s
Joomla: Repo Health / Access control (push) Successful in 2s
Joomla: Repo Health / Release configuration (push) Has been cancelled
Joomla: Repo Health / Scripts governance (push) Has been cancelled
Joomla: Repo Health / Repository health (push) Has been cancelled
2026-05-26 01:58:39 +00:00
jmiller a734d381ac chore: sync updates.xml from [skip ci] 2026-05-26 01:55:08 +00:00
gitea-actions[bot] 0b8f492613 chore: update updates.xml (development: 02.05.04-dev) [skip ci]
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 18s
2026-05-26 01:55:07 +00:00
gitea-actions[bot] 11c3488438 chore(version): auto-bump patch 02.05.04 [skip ci] 2026-05-26 01:55:06 +00:00
Jonathan Miller cc709a0231 security: master user bypasses all tenant restrictions
Joomla: Repo Health / Access control (push) Successful in 1s
Joomla: Update Server / Update updates.xml (push) Successful in 8s
Joomla: Repo Health / Release configuration (push) Has been cancelled
Joomla: Repo Health / Scripts governance (push) Has been cancelled
Joomla: Repo Health / Repository health (push) Has been cancelled
Moved isMasterUser() check to top of enforceAdminRestrictions() so
master user is never blocked by any restriction including install
from URL, global config, sysinfo, installer, and template editing.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-25 20:54:58 -05:00
jmiller 0a0d998208 chore: sync updates.xml from [skip ci] 2026-05-26 01:38:26 +00:00
gitea-actions[bot] 03839601bb chore: update updates.xml (development: 02.05.03-dev) [skip ci] 2026-05-26 01:38:25 +00:00
gitea-actions[bot] 3e28dd4fae chore(version): auto-bump patch 02.05.03 [skip ci] 2026-05-26 01:38:24 +00:00
Jonathan Miller 2674111e0b security: block non-master users from editing MokoWaaS settings
Joomla: Repo Health / Access control (push) Successful in 2s
Joomla: Update Server / Update updates.xml (push) Successful in 8s
Joomla: Repo Health / Release configuration (push) Failing after 4s
Joomla: Repo Health / Scripts governance (push) Successful in 4s
Joomla: Repo Health / Repository health (push) Failing after 4s
Non-master users navigating to the plugin edit page are redirected
back to the plugins list with a warning message.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-25 20:38:14 -05:00
gitea-actions[bot] 7488225aa6 chore(version): pre-release bump to 02.05.02 [skip ci] 2026-05-26 01:30:19 +00:00
jmiller c1a9816c57 fix: updates.xml element=pkg_mokowaas + legacy plugin entry [skip ci] 2026-05-26 01:25:32 +00:00
jmiller 2d1932719a chore: sync updates.xml 02.06.00 [skip ci] 2026-05-26 01:12:15 +00:00
jmiller 315be81e20 Merge pull request 'chore: cascade main → dev (65d9aa3) [skip ci]' (#39) from main into dev
chore: cascade main → dev [skip ci]
2026-05-26 01:12:07 +00:00
jmiller 65d9aa3e9f Merge pull request 'Release 02.06.00: Alias offline bypass, install API fix, stream tags' (#38) from dev into main
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 3s
Joomla: Repo Health / Access control (push) Successful in 1s
Joomla: Repo Health / Release configuration (push) Failing after 3s
Joomla: Repo Health / Scripts governance (push) Successful in 4s
Joomla: Repo Health / Repository health (push) Failing after 4s
2026-05-26 01:12:02 +00:00
jmiller 8243e8c49d chore: sync updates.xml from [skip ci] 2026-05-26 01:11:51 +00:00
gitea-actions[bot] c9d31b3ba4 chore: update updates.xml (development: 02.05.01-dev) [skip ci]
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 13s
2026-05-26 01:11:50 +00:00
gitea-actions[bot] 29cfee7154 chore(version): auto-bump patch 02.05.01 [skip ci] 2026-05-26 01:11:49 +00:00
Jonathan Miller bbae842fdb Merge remote-tracking branch 'origin/main' into dev
Joomla: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Joomla: Repo Health / Access control (pull_request) Successful in 1s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 4s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 5s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 5s
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Has been skipped
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Has been skipped
Joomla: Extension CI / PHPStan Analysis (pull_request) Has been skipped
Universal: PR Check / Changelog Updated (pull_request) Successful in 5s
Universal: PR Check / Validate PR (pull_request) Failing after 4s
Universal: PR Check / Build RC Package (pull_request) Has been skipped
Joomla: Extension CI / Build RC Pre-Release (pull_request) Has been skipped
Joomla: Update Server / Update updates.xml (push) Successful in 10s
Joomla: Repo Health / Release configuration (push) Failing after 4s
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 4s
Joomla: Repo Health / Scripts governance (pull_request) Successful in 4s
Joomla: Repo Health / Repository health (pull_request) Failing after 5s
# Conflicts:
#	README.md
2026-05-25 20:11:38 -05:00
jmiller 85e966a3f4 chore: sync updates.xml from [skip ci] 2026-05-26 01:04:00 +00:00
gitea-actions[bot] 3d8bfb6112 chore: update updates.xml (development: 02.04.02-dev) [skip ci] 2026-05-26 01:03:59 +00:00
gitea-actions[bot] 7822064045 chore(version): auto-bump patch 02.04.02 [skip ci] 2026-05-26 01:03:59 +00:00
Jonathan Miller 906861638f fix: install API extracts ZIP before passing to Joomla Installer
Joomla: Repo Health / Access control (push) Successful in 1s
Joomla: Update Server / Update updates.xml (push) Successful in 8s
Joomla: Repo Health / Release configuration (push) Failing after 5s
Joomla: Repo Health / Scripts governance (push) Successful in 5s
Joomla: Repo Health / Repository health (push) Failing after 6s
Installer::install() expects a directory path, not a ZIP file. Now
downloads → extracts to tmp dir → installs from extracted dir → cleans up.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-25 20:03:47 -05:00
Jonathan Miller 78dd453a9b chore: promote CHANGELOG for 02.06.00 release
Joomla: Repo Health / Access control (push) Successful in 3s
Joomla: Repo Health / Release configuration (push) Failing after 4s
Joomla: Repo Health / Scripts governance (push) Successful in 4s
Joomla: Repo Health / Repository health (push) Failing after 5s
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-25 20:01:14 -05:00
jmiller 204520d9c9 chore: sync updates.xml from [skip ci] 2026-05-26 00:59:11 +00:00
gitea-actions[bot] 72b967c0ab chore: update updates.xml (development: 02.04.01-dev) [skip ci] 2026-05-26 00:59:10 +00:00
gitea-actions[bot] 781266885f chore(version): auto-bump patch 02.04.01 [skip ci] 2026-05-26 00:59:09 +00:00
Jonathan Miller a869619fcd feat: alias offline=No bypasses Joomla global offline setting
Joomla: Repo Health / Access control (push) Successful in 1s
Joomla: Update Server / Update updates.xml (push) Successful in 9s
Joomla: Repo Health / Release configuration (push) Failing after 3s
Joomla: Repo Health / Scripts governance (push) Successful in 4s
Joomla: Repo Health / Repository health (push) Failing after 4s
When an alias domain has offline=No, the plugin overrides Joomla's
configuration.php offline=1 setting. This allows the dev/staging alias
to remain accessible while the main site shows the offline page.

Use case: put clarksvillefurs.com offline for maintenance while
clarksvillefurs.dev.mokoconsulting.tech stays accessible for testing.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-25 19:58:52 -05:00
Jonathan Miller 625965e129 fix(ci): use stream tag 'stable' instead of version tag 'vXX'
Joomla: Repo Health / Access control (push) Successful in 1s
Joomla: Repo Health / Release configuration (push) Failing after 3s
Joomla: Repo Health / Scripts governance (push) Successful in 4s
Joomla: Repo Health / Repository health (push) Failing after 4s
Release tags should use update stream names (stable, development,
release-candidate) not version-based tags (v02, v03). This aligns
with the pre-release workflow which already uses stream tags.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-25 19:22:05 -05:00
jmiller 91504c663b fix: use stream tag 'stable' not version tag in updates.xml [skip ci] 2026-05-26 00:21:50 +00:00
jmiller 6cd690b737 feat(ci): add issue-branch.yml [skip ci] 2026-05-25 05:12:32 +00:00
Jonathan Miller 3b2fe37ce1 chore: update CHANGELOG for 02.05.00 stable release
Joomla: Repo Health / Access control (push) Successful in 1s
Joomla: Repo Health / Release configuration (push) Failing after 3s
Joomla: Repo Health / Scripts governance (push) Successful in 3s
Joomla: Repo Health / Repository health (push) Failing after 4s
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 23:15:29 -05:00
jmiller 8fea27e8b6 fix(ci): update pre-release.yml - PHP CLI tools, fix broken platform detection [skip ci] 2026-05-25 04:12:59 +00:00
jmiller cbea5752d1 fix: correct updates.xml stable entries [skip ci] 2026-05-25 04:07:11 +00:00
jmiller 34e789298b chore: sync updates.xml 02.05.00 [skip ci] 2026-05-25 03:59:36 +00:00
gitea-actions[bot] 62c49eab5a chore(release): ZIP + tar.gz for 02.05.00 [skip ci] 2026-05-25 03:59:35 +00:00
gitea-actions[bot] 2f8c81792d chore(release): build 02.05.00 [skip ci] 2026-05-25 03:59:33 +00:00
jmiller 9a356cdd04 Merge pull request 'chore: cascade main → dev (7b5a83c) [skip ci]' (#37) from main into dev
chore: cascade main → dev [skip ci]
2026-05-25 03:59:26 +00:00
jmiller 69ff510bac Merge pull request 'chore: cascade main → dev (8c9e3e6) [skip ci]' (#36) from main into dev
chore: cascade main → dev [skip ci]
2026-05-24 23:23:45 +00:00
41 changed files with 3084 additions and 1182 deletions
+1
View File
@@ -8,6 +8,7 @@
<name>MokoWaaS</name> <name>MokoWaaS</name>
<org>MokoConsulting</org> <org>MokoConsulting</org>
<description>White-label identity, security hardening, and tenant restriction layer for WaaS-managed Joomla environments</description> <description>White-label identity, security hardening, and tenant restriction layer for WaaS-managed Joomla environments</description>
<version>02.16.00-dev</version>
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license> <license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
</identity> </identity>
<governance> <governance>
+85
View File
@@ -0,0 +1,85 @@
# 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: /.mokogitea/workflows/auto-bump.yml
# VERSION: 09.02.00
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
name: "Universal: Auto Version Bump"
on:
push:
branches:
- dev
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
permissions:
contents: write
jobs:
bump:
name: Version Bump
runs-on: release
if: >-
!contains(github.event.head_commit.message, '[skip ci]') &&
!contains(github.event.head_commit.message, '[skip bump]') &&
!startsWith(github.event.head_commit.message, 'Merge pull request')
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 1
- name: Setup moko-platform tools
run: |
if ! command -v composer &> /dev/null; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
fi
if [ -d "/opt/moko-platform/cli" ]; then
echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV"
else
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \
/tmp/moko-platform-api
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
fi
- name: Bump version
run: |
BUMP=$(php ${MOKO_CLI}/version_bump.php --path . 2>&1) || true
echo "$BUMP"
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null) || true
[ -z "$VERSION" ] && { echo "No version found — skipping"; exit 0; }
# Propagate to platform manifests with -dev suffix
php ${MOKO_CLI}/version_set_platform.php \
--path . --version "$VERSION" --branch dev --stability dev 2>/dev/null || true
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
VERSION="${VERSION}-dev"
# Commit if anything changed
if git diff --quiet && git diff --cached --quiet; then
echo "No version changes to commit"
exit 0
fi
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
git add -A
git commit -m "chore(version): auto-bump patch ${VERSION} [skip ci]" \
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
git push origin dev
echo "Bumped to ${VERSION}" >> $GITHUB_STEP_SUMMARY
+244 -486
View File
@@ -27,13 +27,19 @@ name: "Universal: Build & Release"
on: on:
pull_request: pull_request:
types: [closed] types: [opened, closed]
branches: branches:
- main - main
paths:
- 'src/**'
- 'htdocs/**'
workflow_dispatch: workflow_dispatch:
inputs:
action:
description: 'Action to perform'
required: false
type: choice
default: release
options:
- release
- promote-rc
env: env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
@@ -45,29 +51,94 @@ permissions:
contents: write contents: write
jobs: jobs:
release: # ── Draft PR → Promote highest pre-release to RC ─────────────────────────────
name: Build & Release Pipeline promote-rc:
name: Promote Pre-Release to RC
runs-on: release runs-on: release
if: >- if: >-
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' (github.event.action == 'opened' && github.event.pull_request.draft == true) ||
(github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
token: ${{ secrets.GA_TOKEN }} token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 0 fetch-depth: 1
- name: Setup moko-platform tools - name: Setup moko-platform tools
env: env:
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }} MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN }}"}}' run: |
if ! command -v composer &> /dev/null; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
fi
# Always fetch latest CLI tools — never use stale cache from previous runs
rm -rf /tmp/moko-platform-api
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
/tmp/moko-platform-api
cd /tmp/moko-platform-api
composer install --no-dev --no-interaction --quiet
- name: Promote to release-candidate
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php /tmp/moko-platform-api/cli/release_promote.php \
--from auto --to release-candidate \
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
--api-base "${API_BASE}" \
--branch "${{ github.event.pull_request.head.ref || 'dev' }}"
- name: Cascade lesser channels
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php /tmp/moko-platform-api/cli/release_cascade.php \
--stability release-candidate \
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
--api-base "${API_BASE}"
- name: Summary
if: always()
run: |
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
echo "Draft PR opened — promoted highest pre-release to RC" >> $GITHUB_STEP_SUMMARY
# ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
release:
name: Build & Release Pipeline
runs-on: release
if: >-
github.event.pull_request.merged == true ||
(github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 0
- name: Configure git for bot pushes
run: |
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
- name: Setup moko-platform tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
run: | run: |
# Ensure PHP + Composer are available # Ensure PHP + Composer are available
if ! command -v composer &> /dev/null; then if ! command -v composer &> /dev/null; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
fi fi
# Always fetch latest CLI tools — never use stale cache from previous runs
rm -rf /tmp/moko-platform-api
git clone --depth 1 --branch main --quiet \ git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
/tmp/moko-platform-api /tmp/moko-platform-api
@@ -94,20 +165,44 @@ jobs:
echo "skip=true" >> "$GITHUB_OUTPUT" echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0 exit 0
fi fi
# Strip any pre-release suffix merged from dev (e.g. 01.02.20-dev → 01.02.20)
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
MAJOR=$(echo "$VERSION" | cut -d. -f1) MAJOR=$(echo "$VERSION" | cut -d. -f1)
echo "version=${VERSION}" >> "$GITHUB_OUTPUT" echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "release_tag=v${MAJOR}" >> "$GITHUB_OUTPUT" echo "release_tag=stable" >> "$GITHUB_OUTPUT"
echo "skip=false" >> "$GITHUB_OUTPUT" echo "skip=false" >> "$GITHUB_OUTPUT"
echo "branch=version/${MAJOR}" >> "$GITHUB_OUTPUT" echo "branch=main" >> "$GITHUB_OUTPUT"
- name: "Step 1b: Bump version" # -- CHECK FOR RC PROMOTION ------------------------------------------------
id: bump - name: "Check for RC release"
id: rc
if: steps.version.outputs.skip != 'true' if: steps.version.outputs.skip != 'true'
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
RC_JSON=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
"${API_BASE}/releases/tags/release-candidate" 2>/dev/null || echo "{}")
RC_ID=$(echo "$RC_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('id',''))" 2>/dev/null || true)
if [ -n "$RC_ID" ] && [ "$RC_ID" != "None" ] && [ "$RC_ID" != "" ]; then
echo "promote=true" >> "$GITHUB_OUTPUT"
echo "release_id=${RC_ID}" >> "$GITHUB_OUTPUT"
echo "::notice::RC release found (id: ${RC_ID}) — will promote to stable"
else
echo "promote=false" >> "$GITHUB_OUTPUT"
echo "::notice::No RC release — full build pipeline"
fi
- name: "Step 1b: Minor bump version"
id: bump
if: >-
steps.version.outputs.skip != 'true' &&
steps.rc.outputs.promote != 'true'
run: | run: |
MOKO_API="/tmp/moko-platform-api/cli" MOKO_API="/tmp/moko-platform-api/cli"
BUMP=$(php ${MOKO_API}/version_bump.php --path . --minor) php ${MOKO_API}/version_bump.php --path . --minor 2>&1 || true
VERSION=$(echo "$BUMP" | grep -oP '\d{2}\.\d{2}\.\d{2}$' || true) VERSION=$(php ${MOKO_API}/version_read.php --path .)
[ -z "$VERSION" ] && VERSION=$(php ${MOKO_API}/version_read.php --path .) # Strip any pre-release suffix — stable releases have no suffix
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
echo "version=${VERSION}" >> "$GITHUB_OUTPUT" echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "Bumped to: ${VERSION}" echo "Bumped to: ${VERSION}"
@@ -137,95 +232,8 @@ jobs:
steps.check.outputs.already_released != 'true' steps.check.outputs.already_released != 'true'
run: | run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
ERRORS=0 php /tmp/moko-platform-api/cli/release_validate.php \
--path . --version "$VERSION" --output-summary --github-output || true
PLATFORM="${{ steps.platform.outputs.platform }}"
MANIFEST="${{ steps.platform.outputs.manifest }}"
MOD_FILE="${{ steps.platform.outputs.mod_file }}"
echo "## Pre-Release Sanity Checks (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# -- Version drift check (must pass before release) --------
README_VER=$(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)
if [ "$README_VER" != "$VERSION" ]; then
echo "- Version drift: README says \`${README_VER}\` but releasing \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
else
echo "- Version consistent: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
fi
# Check CHANGELOG version matches
CL_VER=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' CHANGELOG.md 2>/dev/null | head -1)
if [ -n "$CL_VER" ] && [ "$CL_VER" != "$VERSION" ]; then
echo "- CHANGELOG drift: \`${CL_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
fi
# Check composer.json version if present
if [ -f "composer.json" ]; then
COMP_VER=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' composer.json 2>/dev/null | head -1)
if [ -n "$COMP_VER" ] && [ "$COMP_VER" != "$VERSION" ]; then
echo "- composer.json drift: \`${COMP_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
fi
fi
# Common checks
if [ ! -f "LICENSE" ]; then
echo "- Missing LICENSE file" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
else
echo "- LICENSE present" >> $GITHUB_STEP_SUMMARY
fi
if [ ! -d "src" ] && [ ! -d "htdocs" ]; then
echo "- Warning: No src/ or htdocs/ directory" >> $GITHUB_STEP_SUMMARY
else
echo "- Source directory present" >> $GITHUB_STEP_SUMMARY
fi
# -- Platform-specific checks --------
case "$PLATFORM" in
joomla)
if [ -n "$MANIFEST" ]; then
XML_VER=$(sed -n 's/.*<version>\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
if [ -n "$XML_VER" ] && [ "$XML_VER" != "$VERSION" ]; then
echo "- Manifest drift: \`${XML_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
else
echo "- Manifest version: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
fi
TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" 2>/dev/null)
echo "- Extension type: ${TYPE:-unknown}" >> $GITHUB_STEP_SUMMARY
else
echo "- No Joomla XML manifest (WaaS site)" >> $GITHUB_STEP_SUMMARY
fi ;;
dolibarr)
if [ -n "$MOD_FILE" ]; then
MOD_VER=$(sed -n "s/.*\\\$this->version = '\([^']*\)'.*/\1/p" "$MOD_FILE" 2>/dev/null | head -1)
if [ -n "$MOD_VER" ] && [ "$MOD_VER" != "$VERSION" ]; then
echo "- Module drift: \`${MOD_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
else
echo "- Module version: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
fi
else
echo "- No mod*.class.php found" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
fi
if [ ! -f "update.txt" ]; then
echo "- Missing update.txt" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
fi ;;
*) echo "- Generic platform no manifest checks" >> $GITHUB_STEP_SUMMARY ;;
esac
echo "" >> $GITHUB_STEP_SUMMARY
if [ "$ERRORS" -gt 0 ]; then
echo "**${ERRORS} error(s) — release may be incomplete**" >> $GITHUB_STEP_SUMMARY
else
echo "**All sanity checks passed**" >> $GITHUB_STEP_SUMMARY
fi
# -- STEP 2: Create or update version/XX.YY archive branch --------------- # -- STEP 2: Create or update version/XX.YY archive branch ---------------
# Always runs — every version change on main archives to version/XX.YY # Always runs — every version change on main archives to version/XX.YY
@@ -263,17 +271,21 @@ jobs:
run: | run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
php /tmp/moko-platform-api/cli/badge_update.php --path . --version "${VERSION}" 2>/dev/null || true php /tmp/moko-platform-api/cli/badge_update.php --path . --version "${VERSION}" 2>/dev/null || true
php /tmp/moko-platform-api/cli/version_check.php --path . --fix 2>/dev/null || true
- name: "Step 5: Write update stream" # Step 5 (updates.xml) moved after Step 8 to include SHA-256 checksum
- name: "Step 4b: Promote and prune CHANGELOG"
if: >- if: >-
steps.version.outputs.skip != 'true' && steps.version.outputs.skip != 'true' &&
steps.platform.outputs.platform == 'joomla' steps.check.outputs.already_released != 'true'
run: | run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
php /tmp/moko-platform-api/cli/updates_xml_build.php \ MOKO_API="/tmp/moko-platform-api/cli"
--path . --version "${VERSION}" --stability stable \ if [ -f "CHANGELOG.md" ]; then
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \ php ${MOKO_API}/changelog_promote.php --path . --version "$VERSION" 2>&1 || true
--github-output php ${MOKO_API}/changelog_prune.php --path . --keep 5 2>&1 || true
fi
- name: Commit release changes - name: Commit release changes
if: >- if: >-
@@ -285,21 +297,16 @@ jobs:
exit 0 exit 0
fi fi
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
# Set push URL with token for branch-protected repos
git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
git add -A git add -A
git commit -m "chore(release): build ${VERSION} [skip ci]" \ git commit -m "chore(release): build ${VERSION} [skip ci]" \
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" --author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
git push -u origin HEAD # Detached HEAD on PR merge — push explicitly to main
git push origin HEAD:refs/heads/main
# -- STEP 6: Create tag --------------------------------------------------- # -- STEP 6: Create tag ---------------------------------------------------
- name: "Step 6: Create git tag" - name: "Step 6: Create git tag"
if: >- if: >-
steps.version.outputs.skip != 'true' && steps.version.outputs.skip != 'true'
steps.check.outputs.tag_exists != 'true' &&
steps.version.outputs.is_minor == 'true'
run: | run: |
RELEASE_TAG="${{ steps.version.outputs.release_tag }}" RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
# Only create the major release tag if it doesn't exist yet # Only create the major release tag if it doesn't exist yet
@@ -312,384 +319,126 @@ jobs:
fi fi
echo "Tag: ${TAG}" >> $GITHUB_STEP_SUMMARY echo "Tag: ${TAG}" >> $GITHUB_STEP_SUMMARY
# -- STEP 7: Create or update Gitea Release -------------------------------- # -- STEP 7a: Promote RC to stable (skip build) ----------------------------
- name: "Step 7: Gitea Release" - name: "Step 7a: Promote RC to stable"
if: >- if: >-
steps.version.outputs.skip != 'true' steps.version.outputs.skip != 'true' &&
steps.rc.outputs.promote == 'true'
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php /tmp/moko-platform-api/cli/release_promote.php \
--from release-candidate --to stable \
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
--api-base "${API_BASE}" \
--path . --branch main
echo "Promoted RC → stable (${VERSION})" >> $GITHUB_STEP_SUMMARY
# -- STEP 7b: Create or update Gitea Release (full build path) -------------
- name: "Step 7b: Gitea Release"
if: >-
steps.version.outputs.skip != 'true' &&
steps.rc.outputs.promote != 'true'
run: | run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}" RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
BRANCH="${{ steps.version.outputs.branch }}"
MAJOR="${{ steps.version.outputs.major }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php /tmp/moko-platform-api/cli/release_create.php \
--path . --version "$VERSION" --tag "$RELEASE_TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --branch main
echo "Release created: ${VERSION}" >> $GITHUB_STEP_SUMMARY
# Reuse metadata from Step 5 (single source of truth) # -- STEP 8: Build packages and upload to release ----------------------------
EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}" - name: "Step 8: Build package and upload"
EXT_NAME="${{ steps.updates.outputs.ext_name }}" id: package
EXT_TYPE="${{ steps.updates.outputs.ext_type }}"
EXT_FOLDER="${{ steps.updates.outputs.ext_folder }}"
# Fallbacks if Step 5 was skipped
if [ -z "$EXT_ELEMENT" ]; then
EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
fi
[ -z "$EXT_NAME" ] && EXT_NAME="${GITEA_REPO}"
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)"
# Strip existing type prefix to prevent duplication
EXT_ELEMENT=$(echo "$EXT_ELEMENT" | sed -E 's/^(pkg_|com_|mod_|plg_[a-z]+_|tpl_|lib_)//')
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_" ;;
esac
RELEASE_NAME="${EXT_NAME} ${VERSION} (${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION})"
# Delete existing release if present (overwrite, not append)
EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
"${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true)
EXISTING_ID=$(echo "$EXISTING" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('id',''))" 2>/dev/null || true)
if [ -n "$EXISTING_ID" ]; then
curl -sS -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
"${API_BASE}/releases/${EXISTING_ID}" 2>/dev/null || true
curl -sS -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
"${API_BASE}/tags/${RELEASE_TAG}" 2>/dev/null || true
echo "Deleted previous stable release (id: ${EXISTING_ID})"
fi
# Create fresh release
curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
-H "Content-Type: application/json" \
"${API_BASE}/releases" \
-d "$(python3 -c "import json; print(json.dumps({
'tag_name': '${RELEASE_TAG}',
'name': '${RELEASE_NAME}',
'body': '''## ${VERSION} ($(date +%Y-%m-%d))\n${NOTES}''',
'target_commitish': '${BRANCH}'
}))")"
echo "Release created: ${RELEASE_NAME}" >> $GITHUB_STEP_SUMMARY
# -- STEP 8: Build Joomla install ZIP + SHA-256 checksum ------------------
- name: "Step 8: Build package and update checksum"
if: >- if: >-
steps.version.outputs.skip != 'true' steps.version.outputs.skip != 'true' &&
steps.rc.outputs.promote != 'true'
run: | run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}" RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
REPO="${{ github.repository }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php /tmp/moko-platform-api/cli/release_package.php \
--path . --version "$VERSION" --tag "$RELEASE_TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --output /tmp || true
# All ZIPs upload to the major release tag (vXX) # -- STEP 5: Write update stream (after build so SHA-256 is available) -----
RELEASE_JSON=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ - name: "Step 5: Write update stream"
"${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true)
RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
if [ -z "$RELEASE_ID" ]; then
echo "No release ${RELEASE_TAG} found — skipping ZIP upload"
exit 0
fi
# Find extension element name from manifest
MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true)
[ -z "$MANIFEST" ] && exit 0
# Reuse element from Step 5, with same fallback chain
EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}"
if [ -z "$EXT_ELEMENT" ]; then
EXT_ELEMENT=$(sed -n 's/.*<element>\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(sed -n 's/.*plugin="\([^"]*\)".*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]')
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
fi
# ZIP name: type_folder_element-VERSION (e.g. plg_system_mokojgdpc-01.01.00.zip)
EXT_TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
EXT_FOLDER=$(sed -n 's/.*<extension[^>]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
# For packages, prefer <packagename> over filename-derived element
if [ "$EXT_TYPE" = "package" ]; then
PKG_NAME=$(sed -n 's/.*<packagename>\([^<]*\)<\/packagename>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
[ -n "$PKG_NAME" ] && EXT_ELEMENT="$PKG_NAME"
fi
# Strip existing type prefix to prevent duplication (e.g. pkg_mokowaas → mokowaas)
EXT_ELEMENT=$(echo "$EXT_ELEMENT" | sed -E 's/^(pkg_|com_|mod_|plg_[a-z]+_|tpl_|lib_)//')
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_" ;;
esac
ZIP_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip"
TAR_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.tar.gz"
# -- Build install packages from src/ ----------------------------
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
[ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/"; exit 0; }
# 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
# 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")
# -- Calculate SHA-256 for both ----------------------------------
SHA256_ZIP=$(sha256sum "/tmp/${ZIP_NAME}" | cut -d' ' -f1)
SHA256_TAR=$(sha256sum "/tmp/${TAR_NAME}" | cut -d' ' -f1)
# -- Delete existing assets with same name before uploading ------
ASSETS=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
"${API_BASE}/releases/${RELEASE_ID}/assets" 2>/dev/null || echo "[]")
for ASSET_NAME in "$ZIP_NAME" "$TAR_NAME"; do
ASSET_ID=$(echo "$ASSETS" | python3 -c "
import sys,json
assets = json.load(sys.stdin)
for a in assets:
if a['name'] == '${ASSET_NAME}':
print(a['id']); break
" 2>/dev/null || true)
if [ -n "$ASSET_ID" ]; then
curl -sf -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
"${API_BASE}/releases/${RELEASE_ID}/assets/${ASSET_ID}" 2>/dev/null || true
fi
done
# -- Upload both to release tag ----------------------------------
curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
-H "Content-Type: application/octet-stream" \
--data-binary @"/tmp/${ZIP_NAME}" \
"${API_BASE}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" > /dev/null 2>&1 || true
curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
-H "Content-Type: application/octet-stream" \
--data-binary @"/tmp/${TAR_NAME}" \
"${API_BASE}/releases/${RELEASE_ID}/assets?name=${TAR_NAME}" > /dev/null 2>&1 || true
# -- Update updates.xml with both download formats ---------------
if [ -f "updates.xml" ]; then
ZIP_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${ZIP_NAME}"
TAR_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${TAR_NAME}"
# Use Python to update only the stable entry's downloads + sha256
export PY_ZIP_URL="$ZIP_URL" PY_TAR_URL="$TAR_URL" PY_SHA="$SHA256_ZIP"
python3 << 'PYEOF'
import re, os
with open("updates.xml") as f:
content = f.read()
zip_url = os.environ["PY_ZIP_URL"]
tar_url = os.environ["PY_TAR_URL"]
sha = os.environ["PY_SHA"]
# Find the stable update block and replace its downloads + sha256
def replace_stable(m):
block = m.group(0)
# Replace downloads block
new_downloads = (
" <downloads>\n"
f" <downloadurl type=\"full\" format=\"zip\">{zip_url}</downloadurl>\n"
" </downloads>"
)
block = re.sub(r' <downloads>.*?</downloads>', new_downloads, block, flags=re.DOTALL)
# Add or replace sha256
if '<sha256>' in block:
block = re.sub(r' <sha256>.*?</sha256>', f' <sha256>{sha}</sha256>', block)
else:
block = block.replace('</downloads>', f'</downloads>\n <sha256>{sha}</sha256>')
return block
content = re.sub(
r' <update>.*?<tag>stable</tag>.*?</update>',
replace_stable,
content,
flags=re.DOTALL
)
with open("updates.xml", "w") as f:
f.write(content)
PYEOF
CURRENT_BRANCH="${{ github.ref_name }}"
git add updates.xml
git commit -m "chore(release): ZIP + tar.gz for ${VERSION} [skip ci]" \
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" || true
git push || true
# Sync updates.xml to main via direct API (always runs — may be on version/XX branch)
GA_TOKEN="${{ secrets.GA_TOKEN }}"
API="${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}"
FILE_SHA=$(curl -sf -H "Authorization: token ${GA_TOKEN}" \
"${API}/contents/updates.xml?ref=main" | jq -r '.sha // empty')
if [ -n "$FILE_SHA" ]; then
CONTENT=$(base64 -w0 updates.xml)
curl -sf -X PUT -H "Authorization: token ${GA_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/contents/updates.xml" \
-d "$(jq -n \
--arg content "$CONTENT" \
--arg sha "$FILE_SHA" \
--arg msg "chore: sync updates.xml ${VERSION} [skip ci]" \
--arg branch "main" \
'{content: $content, sha: $sha, message: $msg, branch: $branch}'
)" > /dev/null 2>&1 \
&& echo "updates.xml synced to main via API" \
|| echo "WARNING: failed to sync updates.xml to main"
else
echo "WARNING: could not get updates.xml SHA from main"
fi
fi
echo "### Packages" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Package | Size | SHA-256 |" >> $GITHUB_STEP_SUMMARY
echo "|---------|------|---------|" >> $GITHUB_STEP_SUMMARY
echo "| \`${ZIP_NAME}\` | ${ZIP_SIZE} | \`${SHA256_ZIP}\` |" >> $GITHUB_STEP_SUMMARY
echo "| \`${TAR_NAME}\` | ${TAR_SIZE} | \`${SHA256_TAR}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Release | \`${RELEASE_TAG}\` | |" >> $GITHUB_STEP_SUMMARY
echo "| Download | [${ZIP_NAME}](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${ZIP_NAME}) |" >> $GITHUB_STEP_SUMMARY
# -- STEP 8b: Update release description with changelog + SHA ----------------
- name: "Step 8b: Update release body with changelog and SHA"
if: steps.version.outputs.skip != 'true' if: steps.version.outputs.skip != 'true'
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
SHA256="${{ steps.package.outputs.sha256_zip }}"
# Fetch latest updates.xml from main so preserve logic has all channels
GITEA_TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
curl -sf -H "Authorization: token ${GITEA_TOKEN}" \
"${API}/contents/updates.xml?ref=main" 2>/dev/null | \
python3 -c "import sys,json,base64; print(base64.b64decode(json.load(sys.stdin)['content']).decode())" \
> updates.xml 2>/dev/null || true
SHA_FLAG=""
[ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
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}" \
${SHA_FLAG} --github-output
# Commit updates.xml if changed
if ! git diff --quiet updates.xml 2>/dev/null; then
git add updates.xml
git commit -m "chore: update stable channel ${VERSION} [skip ci]" \
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
git push origin HEAD:refs/heads/main 2>&1 || true
fi
# -- STEP 8b: Update release description with changelog ----------------------
- name: "Step 8b: Update release body"
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: | run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}" RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" php /tmp/moko-platform-api/cli/release_body_update.php \
EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}" --path . --version "${VERSION}" --tag "${RELEASE_TAG}" \
EXT_TYPE="${{ steps.updates.outputs.ext_type }}" --token "${{ secrets.MOKOGITEA_TOKEN }}" \
EXT_FOLDER="${{ steps.updates.outputs.ext_folder }}" --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
2>&1 || true
# Build TYPE_PREFIX to match Step 8's ZIP naming echo "Release body updated" >> $GITHUB_STEP_SUMMARY
EXT_ELEMENT=$(echo "$EXT_ELEMENT" | sed -E 's/^(pkg_|com_|mod_|plg_[a-z]+_|tpl_|lib_)//')
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_" ;;
esac
ZIP_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip"
TAR_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.tar.gz"
# Get SHA from the built files
SHA256_ZIP=""
[ -f "/tmp/${ZIP_NAME}" ] && SHA256_ZIP=$(sha256sum "/tmp/${ZIP_NAME}" | cut -d' ' -f1)
SHA256_TAR=""
[ -f "/tmp/${TAR_NAME}" ] && SHA256_TAR=$(sha256sum "/tmp/${TAR_NAME}" | cut -d' ' -f1)
# Extract latest changelog entry (strip the ## header to avoid duplicate)
CHANGELOG=""
if [ -f "CHANGELOG.md" ]; then
CHANGELOG=$(sed -n "/^## \[*${VERSION}/,/^## \[*[0-9]/p" CHANGELOG.md | sed '$d' | sed '1d')
[ -z "$CHANGELOG" ] && CHANGELOG=$(sed -n '/^## /,/^## /p' CHANGELOG.md | sed '$d' | sed '1d' | head -30)
fi
# Build release body (single header, no duplicate from changelog)
BODY="## ${VERSION} ($(date +%Y-%m-%d))\n\n"
if [ -n "$CHANGELOG" ]; then
BODY="${BODY}${CHANGELOG}\n\n"
fi
BODY="${BODY}---\n\n### Checksums\n\n"
BODY="${BODY}| File | SHA-256 |\n|------|--------|\n"
[ -n "$SHA256_ZIP" ] && BODY="${BODY}| \`${ZIP_NAME}\` | \`${SHA256_ZIP}\` |\n"
[ -n "$SHA256_TAR" ] && BODY="${BODY}| \`${TAR_NAME}\` | \`${SHA256_TAR}\` |\n"
# Get release ID and update body
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
"${API_BASE}/releases/tags/${RELEASE_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
python3 -c "
import json, urllib.request
body = '''$(printf '%b' "$BODY")'''
data = json.dumps({'body': body}).encode()
req = urllib.request.Request(
'${API_BASE}/releases/${RELEASE_ID}',
data=data,
headers={'Authorization': 'token ${{ secrets.GA_TOKEN }}', 'Content-Type': 'application/json'},
method='PATCH'
)
urllib.request.urlopen(req)
" 2>/dev/null && echo "Release body updated with changelog + SHA" >> $GITHUB_STEP_SUMMARY
fi
# -- STEP 9: Mirror to GitHub (stable only) -------------------------------- # -- STEP 9: Mirror to GitHub (stable only) --------------------------------
- name: "Step 9: Mirror release to GitHub" - name: "Step 9: Mirror release to GitHub"
if: >- if: >-
steps.version.outputs.skip != 'true' && steps.version.outputs.skip != 'true' &&
steps.version.outputs.stability == 'stable' && secrets.GH_MIRROR_TOKEN != ''
secrets.GH_TOKEN != ''
continue-on-error: true continue-on-error: true
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
run: | run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}" 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 }}" GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
NOTES=$(php /tmp/moko-platform-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null || true) php /tmp/moko-platform-api/cli/release_mirror.php \
[ -z "$NOTES" ] && NOTES="Release ${VERSION}" --version "$VERSION" --tag "$RELEASE_TAG" \
echo "$NOTES" > /tmp/release_notes.md --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
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) --branch main 2>&1 || true
echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY
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 ---------------------------- # -- STEP 10: Sync main branch to GitHub mirror ----------------------------
- name: "Step 10: Push main to GitHub mirror" - name: "Step 10: Push main to GitHub mirror"
if: >- if: >-
steps.version.outputs.skip != 'true' && steps.version.outputs.skip != 'true' &&
secrets.GH_TOKEN != '' secrets.GH_MIRROR_TOKEN != ''
continue-on-error: true continue-on-error: true
run: | run: |
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1) GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2) 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 add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
git remote set-url github "https://x-access-token:${{ secrets.GH_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
git fetch origin main --depth=1 git fetch origin main --depth=1
git push github origin/main:refs/heads/main --force 2>/dev/null \ git push github origin/main:refs/heads/main --force 2>/dev/null \
&& echo "main branch pushed to GitHub mirror" \ && echo "main branch pushed to GitHub mirror" \
@@ -700,18 +449,20 @@ jobs:
- name: "Delete lesser pre-release channels" - name: "Delete lesser pre-release channels"
continue-on-error: true continue-on-error: true
run: | run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php /tmp/moko-platform-api/cli/release_cascade.php \ php /tmp/moko-platform-api/cli/release_cascade.php \
--stability stable \ --stability stable \
--token "${{ secrets.GA_TOKEN }}" \ --version "${VERSION}" \
--org "${GITEA_ORG}" --repo "${GITEA_REPO}" \ --token "${{ secrets.MOKOGITEA_TOKEN }}" \
--gitea-url "${GITEA_URL}" 2>/dev/null || true --api-base "${API_BASE}" 2>/dev/null || true
- name: "Step 11: Delete and recreate dev branch from main" - name: "Step 11: Delete and recreate dev branch from main"
if: steps.version.outputs.skip != 'true' if: steps.version.outputs.skip != 'true'
continue-on-error: true continue-on-error: true
run: | run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.GA_TOKEN }}" TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
# Delete dev branch # Delete dev branch
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
@@ -725,28 +476,35 @@ jobs:
echo "Dev branch reset from main (keeps dev ahead after release)" >> $GITHUB_STEP_SUMMARY echo "Dev branch reset from main (keeps dev ahead after release)" >> $GITHUB_STEP_SUMMARY
- name: "Step 12: Create version branch from main"
# -- Dolibarr post-release: Reset dev version ----------------------------- if: steps.version.outputs.skip != 'true'
- name: "Dolibarr: Reset dev version"
if: >-
steps.version.outputs.skip != 'true' &&
steps.platform.outputs.platform == 'dolibarr' &&
steps.platform.outputs.mod_file != ''
continue-on-error: true continue-on-error: true
run: | run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.GA_TOKEN }}" TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
MOD_FILE="${{ steps.platform.outputs.mod_file }}" VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
ENCODED_PATH=$(echo "$MOD_FILE" | sed 's|^\./||' | python3 -c "import sys,urllib.parse; print(urllib.parse.quote(sys.stdin.read().strip()))") BRANCH_NAME="version/${VERSION}"
FILE_RESP=$(curl -sf -H "Authorization: token ${TOKEN}" "${API_BASE}/contents/${ENCODED_PATH}?ref=dev" 2>/dev/null || true) MAIN_SHA=$(git rev-parse HEAD)
FILE_SHA=$(echo "$FILE_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
FILE_CONTENT=$(echo "$FILE_RESP" | python3 -c "import sys,json,base64; print(base64.b64decode(json.load(sys.stdin).get('content','')).decode())" 2>/dev/null || true) # Delete old version branch if it exists (same version re-release)
if [ -n "$FILE_SHA" ] && [ -n "$FILE_CONTENT" ]; then curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}"
UPDATED=$(echo "$FILE_CONTENT" | sed "s/\$this->version = '[^']*'/\$this->version = 'development'/")
ENCODED=$(echo "$UPDATED" | base64 -w0) # Create version/XX.YY.ZZ from main
curl -sf -X PUT -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/contents/${ENCODED_PATH}" \ curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed"
-d "$(jq -n --arg content \"$ENCODED\" --arg sha \"$FILE_SHA\" --arg msg \"chore(version): reset dev version [skip ci]\" --arg branch \"dev\" '{content:$content,sha:$sha,message:$msg,branch:$branch}')" > /dev/null 2>&1 || true
fi echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY
# -- Dolibarr post-release: Reset dev version -----------------------------
- name: "Post-release: Reset dev version"
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php /tmp/moko-platform-api/cli/version_reset_dev.php \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
--branch dev --path . 2>&1 || true
# -- Summary -------------------------------------------------------------- # -- Summary --------------------------------------------------------------
- name: Pipeline Summary - name: Pipeline Summary
+48
View File
@@ -0,0 +1,48 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /.mokogitea/workflows/branch-cleanup.yml
# VERSION: 01.00.00
# BRIEF: Delete feature branches after PR merge
name: "Branch Cleanup"
on:
pull_request:
types: [closed]
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
cleanup:
name: Delete merged branch
runs-on: ubuntu-latest
if: >-
github.event.pull_request.merged == true &&
github.event.pull_request.head.ref != 'dev' &&
github.event.pull_request.head.ref != 'main'
steps:
- name: Delete source branch
run: |
BRANCH="${{ github.event.pull_request.head.ref }}"
API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches"
ENCODED=$(python3 -c "import urllib.parse; print(urllib.parse.quote('${BRANCH}', safe=''))")
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \
-H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
"${API}/${ENCODED}" 2>/dev/null || true)
if [ "$STATUS" = "204" ]; then
echo "Deleted branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
elif [ "$STATUS" = "404" ]; then
echo "Branch already deleted: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
else
echo "::warning::Failed to delete branch ${BRANCH} (HTTP ${STATUS})"
fi
+10 -10
View File
@@ -4,8 +4,8 @@
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Maintenance # INGROUP: moko-platform.Maintenance
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API # REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# PATH: /templates/workflows/cascade-dev.yml.template # PATH: /templates/workflows/cascade-dev.yml.template
# VERSION: 02.00.00 # VERSION: 02.00.00
# BRIEF: Forward-merge main → all open branches after every push to main # BRIEF: Forward-merge main → all open branches after every push to main
@@ -52,7 +52,7 @@ jobs:
- name: Discover target branches - name: Discover target branches
id: branches id: branches
env: env:
GA_TOKEN: ${{ secrets.GA_TOKEN }} GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
run: | run: |
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
@@ -61,7 +61,7 @@ jobs:
ALL_BRANCHES="" ALL_BRANCHES=""
while true; do while true; do
BATCH=$(curl -sS \ BATCH=$(curl -sS \
-H "Authorization: token ${GA_TOKEN}" \ -H "Authorization: token ${GITEA_TOKEN}" \
"${API}/branches?page=${PAGE}&limit=50" \ "${API}/branches?page=${PAGE}&limit=50" \
| jq -r '.[].name // empty') | jq -r '.[].name // empty')
[ -z "$BATCH" ] && break [ -z "$BATCH" ] && break
@@ -93,7 +93,7 @@ jobs:
- name: Cascade to all target branches - name: Cascade to all target branches
if: steps.branches.outputs.targets != '' if: steps.branches.outputs.targets != ''
env: env:
GA_TOKEN: ${{ secrets.GA_TOKEN }} GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
run: | run: |
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
SHORT_SHA="${GITHUB_SHA:0:7}" SHORT_SHA="${GITHUB_SHA:0:7}"
@@ -111,7 +111,7 @@ jobs:
# Check if branch is already up to date # Check if branch is already up to date
ENCODED_BRANCH=$(echo "$BRANCH" | sed 's|/|%2F|g') ENCODED_BRANCH=$(echo "$BRANCH" | sed 's|/|%2F|g')
RESPONSE=$(curl -sS \ RESPONSE=$(curl -sS \
-H "Authorization: token ${GA_TOKEN}" \ -H "Authorization: token ${GITEA_TOKEN}" \
"${API}/compare/${ENCODED_BRANCH}...main") "${API}/compare/${ENCODED_BRANCH}...main")
AHEAD=$(echo "$RESPONSE" | jq '.total_commits // 0') AHEAD=$(echo "$RESPONSE" | jq '.total_commits // 0')
@@ -126,7 +126,7 @@ jobs:
# Check for existing cascade PR # Check for existing cascade PR
EXISTING=$(curl -sS \ EXISTING=$(curl -sS \
-H "Authorization: token ${GA_TOKEN}" \ -H "Authorization: token ${GITEA_TOKEN}" \
"${API}/pulls?state=open&head=${GITEA_ORG}:main&base=${ENCODED_BRANCH}&limit=1") "${API}/pulls?state=open&head=${GITEA_ORG}:main&base=${ENCODED_BRANCH}&limit=1")
EXISTING_COUNT=$(echo "$EXISTING" | jq 'length') EXISTING_COUNT=$(echo "$EXISTING" | jq 'length')
@@ -139,7 +139,7 @@ jobs:
# Create cascade PR # Create cascade PR
PR_RESPONSE=$(curl -sS -w "\n%{http_code}" \ PR_RESPONSE=$(curl -sS -w "\n%{http_code}" \
-X POST \ -X POST \
-H "Authorization: token ${GA_TOKEN}" \ -H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d "{ -d "{
\"title\": \"chore: cascade main → ${BRANCH} (${SHORT_SHA}) [skip ci]\", \"title\": \"chore: cascade main → ${BRANCH} (${SHORT_SHA}) [skip ci]\",
@@ -165,7 +165,7 @@ jobs:
# Try auto-merge # Try auto-merge
PR_DATA=$(curl -sS \ PR_DATA=$(curl -sS \
-H "Authorization: token ${GA_TOKEN}" \ -H "Authorization: token ${GITEA_TOKEN}" \
"${API}/pulls/${PR_NUMBER}") "${API}/pulls/${PR_NUMBER}")
MERGEABLE=$(echo "$PR_DATA" | jq -r '.mergeable // false') MERGEABLE=$(echo "$PR_DATA" | jq -r '.mergeable // false')
@@ -178,7 +178,7 @@ jobs:
MERGE_RESPONSE=$(curl -sS -w "\n%{http_code}" \ MERGE_RESPONSE=$(curl -sS -w "\n%{http_code}" \
-X POST \ -X POST \
-H "Authorization: token ${GA_TOKEN}" \ -H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d "{ -d "{
\"Do\": \"merge\", \"Do\": \"merge\",
+8 -8
View File
@@ -43,9 +43,9 @@ jobs:
- name: Clone MokoStandards - name: Clone MokoStandards
env: env:
GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }} GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }} MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}
MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }} MOKO_CLONE_HOST: ${{ secrets.MOKOGITEA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
run: | run: |
git clone --depth 1 --branch main --quiet \ git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \ "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
@@ -53,7 +53,7 @@ jobs:
- name: Install dependencies - name: Install dependencies
env: env:
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}' COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}'
run: | run: |
if [ -f "composer.json" ]; then if [ -f "composer.json" ]; then
composer install \ composer install \
@@ -346,7 +346,7 @@ jobs:
- name: Install dependencies - name: Install dependencies
env: env:
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}' COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}'
run: | run: |
if [ -f "composer.json" ]; then if [ -f "composer.json" ]; then
composer install \ composer install \
@@ -391,7 +391,7 @@ jobs:
- name: Install dependencies - name: Install dependencies
env: env:
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}' COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}'
run: | run: |
if [ -f "composer.json" ]; then if [ -f "composer.json" ]; then
composer install --no-interaction --prefer-dist --optimize-autoloader composer install --no-interaction --prefer-dist --optimize-autoloader
@@ -458,10 +458,10 @@ jobs:
steps: steps:
- name: Trigger pre-release build - name: Trigger pre-release build
env: env:
GA_TOKEN: ${{ secrets.GA_TOKEN }} GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
REPO: ${{ github.repository }} REPO: ${{ github.repository }}
BRANCH: ${{ github.head_ref }} BRANCH: ${{ github.head_ref }}
run: | run: |
curl -s -X POST "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}" curl -s -X POST "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
+9 -9
View File
@@ -4,8 +4,8 @@
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Maintenance # INGROUP: moko-platform.Maintenance
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards # REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /.gitea/workflows/cleanup.yml # PATH: /.gitea/workflows/cleanup.yml
# VERSION: 01.00.00 # VERSION: 01.00.00
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs # BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
@@ -33,17 +33,17 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
token: ${{ secrets.GA_TOKEN }} token: ${{ secrets.MOKOGITEA_TOKEN }}
- name: Delete merged branches - name: Delete merged branches
env: env:
GA_TOKEN: ${{ secrets.GA_TOKEN }} GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
run: | run: |
echo "=== Merged Branch Cleanup ===" echo "=== Merged Branch Cleanup ==="
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
# List branches via API # List branches via API
BRANCHES=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \ BRANCHES=$(curl -sS -H "Authorization: token ${GITEA_TOKEN}" \
"${API}/branches?limit=50" | jq -r '.[].name') "${API}/branches?limit=50" | jq -r '.[].name')
DELETED=0 DELETED=0
@@ -56,7 +56,7 @@ jobs:
# Check if branch is merged into main # Check if branch is merged into main
if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then
echo " Deleting merged branch: ${BRANCH}" echo " Deleting merged branch: ${BRANCH}"
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \ curl -sS -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
"${API}/branches/${BRANCH}" 2>/dev/null || true "${API}/branches/${BRANCH}" 2>/dev/null || true
DELETED=$((DELETED + 1)) DELETED=$((DELETED + 1))
fi fi
@@ -66,20 +66,20 @@ jobs:
- name: Clean old workflow runs - name: Clean old workflow runs
env: env:
GA_TOKEN: ${{ secrets.GA_TOKEN }} GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
run: | run: |
echo "=== Workflow Run Cleanup ===" echo "=== Workflow Run Cleanup ==="
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ) CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ)
# Get old completed runs # Get old completed runs
RUNS=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \ RUNS=$(curl -sS -H "Authorization: token ${GITEA_TOKEN}" \
"${API}/actions/runs?status=completed&limit=50" | \ "${API}/actions/runs?status=completed&limit=50" | \
jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null) jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null)
DELETED=0 DELETED=0
for RUN_ID in $RUNS; do for RUN_ID in $RUNS; do
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \ curl -sS -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
"${API}/actions/runs/${RUN_ID}" 2>/dev/null || true "${API}/actions/runs/${RUN_ID}" 2>/dev/null || true
DELETED=$((DELETED + 1)) DELETED=$((DELETED + 1))
done done
+2 -2
View File
@@ -4,8 +4,8 @@
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Security # INGROUP: moko-platform.Security
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API # REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# PATH: /templates/workflows/gitleaks.yml.template # PATH: /templates/workflows/gitleaks.yml.template
# VERSION: 01.00.00 # VERSION: 01.00.00
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens # BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
+73
View File
@@ -0,0 +1,73 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Automation
# VERSION: 01.00.00
# BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch"
on:
issues:
types: [opened]
permissions:
contents: write
issues: write
env:
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
jobs:
create-branch:
name: Create feature branch
runs-on: ubuntu-latest
steps:
- name: Create branch and comment
run: |
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
ISSUE_NUM="${{ github.event.issue.number }}"
ISSUE_TITLE="${{ github.event.issue.title }}"
# Build slug from title: lowercase, replace non-alnum with dash, trim
SLUG=$(echo "${ISSUE_TITLE}" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//' | cut -c1-40)
BRANCH="feature/${ISSUE_NUM}-${SLUG}"
# Check dev branch exists
DEV_EXISTS=$(curl -sf -o /dev/null -w '%{http_code}' \
-H "Authorization: token ${TOKEN}" \
"${API}/branches/dev" 2>/dev/null || echo "000")
if [ "${DEV_EXISTS}" != "200" ]; then
echo "No dev branch -- skipping"
exit 0
fi
# Create branch from dev
HTTP=$(curl -sf -o /dev/null -w '%{http_code}' -X POST \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
"${API}/branches" \
-d "{\"new_branch_name\":\"${BRANCH}\",\"old_branch_name\":\"dev\"}" 2>/dev/null || echo "000")
if [ "${HTTP}" = "201" ]; then
echo "Created branch: ${BRANCH}"
# Comment on issue with branch link
REPO_URL="${GITEA_URL}/${{ github.repository }}"
BODY="Branch created: [\`${BRANCH}\`](${REPO_URL}/src/branch/${BRANCH})\n\n\`\`\`bash\ngit fetch origin\ngit checkout ${BRANCH}\n\`\`\`"
curl -sf -X POST \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
"${API}/issues/${ISSUE_NUM}/comments" \
-d "{\"body\":\"${BODY}\"}" > /dev/null 2>&1
echo "Commented on issue #${ISSUE_NUM}"
else
echo "Failed to create branch (HTTP ${HTTP}) -- may already exist"
fi
+2 -3
View File
@@ -4,8 +4,8 @@
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Notifications # INGROUP: moko-platform.Notifications
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards # REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /.gitea/workflows/notify.yml # PATH: /.gitea/workflows/notify.yml
# VERSION: 01.00.00 # VERSION: 01.00.00
# BRIEF: Push notifications via ntfy on release success or workflow failure # BRIEF: Push notifications via ntfy on release success or workflow failure
@@ -18,7 +18,6 @@ on:
- "Joomla Build & Release" - "Joomla Build & Release"
- "Joomla Extension CI" - "Joomla Extension CI"
- "Deploy" - "Deploy"
- "Cascade Main → Dev"
types: types:
- completed - completed
+7 -35
View File
@@ -4,8 +4,8 @@
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.CI # INGROUP: moko-platform.CI
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API # REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# PATH: /templates/workflows/universal/pr-check.yml.template # PATH: /templates/workflows/universal/pr-check.yml.template
# VERSION: 05.00.00 # VERSION: 05.00.00
# BRIEF: PR gate — branch policy + code validation before merge # BRIEF: PR gate — branch policy + code validation before merge
@@ -108,8 +108,9 @@ jobs:
- name: Detect platform - name: Detect platform
id: platform id: platform
run: | run: |
# Parse manifest for platform detection # Read platform from XML manifest (<platform> tag) or plain text fallback
PLATFORM=$(php /tmp/mokostandards-api/cli/manifest_read.php --path . --field platform 2>/dev/null) 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" [ -z "$PLATFORM" ] && PLATFORM="generic"
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT" echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
@@ -194,35 +195,6 @@ jobs:
echo "Source: ${FILE_COUNT} files" echo "Source: ${FILE_COUNT} files"
[ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; } [ "$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
# ── Pre-Release RC Build ───────────────────────────────────────────────── # ── Pre-Release RC Build ─────────────────────────────────────────────────
pre-release: pre-release:
name: Build RC Package name: Build RC Package
@@ -232,11 +204,11 @@ jobs:
steps: steps:
- name: Trigger RC pre-release - name: Trigger RC pre-release
env: env:
GA_TOKEN: ${{ secrets.GA_TOKEN }} GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
REPO: ${{ github.repository }} REPO: ${{ github.repository }}
BRANCH: ${{ github.head_ref }} BRANCH: ${{ github.head_ref }}
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
run: | run: |
curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}" curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
+126 -153
View File
@@ -7,12 +7,16 @@
# INGROUP: moko-platform.Release # INGROUP: moko-platform.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform # REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /templates/workflows/universal/pre-release.yml.template # PATH: /templates/workflows/universal/pre-release.yml.template
# VERSION: 05.00.00 # VERSION: 05.01.00
# BRIEF: Manual pre-release builds dev/alpha/beta/rc packages from any branch # BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch
name: "Universal: Pre-Release" name: "Universal: Pre-Release"
on: on:
pull_request:
types: [closed]
branches:
- dev
workflow_dispatch: workflow_dispatch:
inputs: inputs:
stability: stability:
@@ -35,40 +39,44 @@ env:
jobs: jobs:
build: build:
name: "Build Pre-Release (${{ inputs.stability }})" name: "Build Pre-Release (${{ inputs.stability || 'development' }})"
runs-on: release runs-on: release
if: >-
github.event_name == 'workflow_dispatch' ||
(github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev')
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
token: ${{ secrets.GA_TOKEN }} token: ${{ secrets.MOKOGITEA_TOKEN }}
- name: 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 - name: Setup moko-platform tools
env: env:
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }} MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
run: | 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 if ! command -v composer &> /dev/null; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
fi
# Always fetch latest CLI tools — never use stale cache from previous runs
rm -rf /tmp/moko-platform-api
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
/tmp/moko-platform-api
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
- name: Detect platform - name: Detect platform
id: platform id: platform
run: | run: |
php /tmp/moko-platform-api/cli/manifest_read.php --path . --github-output php ${MOKO_CLI}/manifest_read.php --path . --github-output
- name: Resolve metadata - name: Resolve metadata and bump version
id: meta id: meta
run: | run: |
STABILITY="${{ inputs.stability }}" STABILITY="${{ inputs.stability || 'development' }}"
MOKO_API="/tmp/moko-platform-api/cli"
case "$STABILITY" in case "$STABILITY" in
development) SUFFIX="-dev"; TAG="development" ;; development) SUFFIX="-dev"; TAG="development" ;;
@@ -77,34 +85,44 @@ jobs:
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;; release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
esac esac
# Read current version from manifest (priority) or README — no bump yet # Read current version (bump already handled by push workflow)
VERSION=$(php ${MOKO_API}/version_read.php --path .) VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null)
echo "Version: ${VERSION}" [ -z "$VERSION" ] && VERSION="00.00.01"
# Ensure platform-specific manifest matches # Strip any existing suffix from version before applying stability
php ${MOKO_API}/version_set_platform.php --path . --version "${VERSION}" VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
# Git setup for later commits php ${MOKO_CLI}/version_set_platform.php \
--path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true
# Verify version consistency across all files
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
# Update VERSION variable with suffix
if [ -n "$SUFFIX" ]; then
VERSION="${VERSION}${SUFFIX}"
fi
# Commit version bump
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]" 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 remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
git add -A
git diff --cached --quiet || {
git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]"
git push origin HEAD 2>&1
}
# Detect element from Joomla/Dolibarr manifest # Auto-detect element via manifest_element.php
set +o pipefail php ${MOKO_CLI}/manifest_element.php \
PLATFORM="${{ steps.platform.outputs.platform }}" --path . --version "$VERSION" --stability "$STABILITY" \
EXT_ELEMENT=$(php ${MOKO_API}/manifest_read.php --path . --field name 2>/dev/null | tr -d ' ' | tr '[:upper:]' '[:lower:]' || true) --repo "${GITEA_REPO}" --github-output
# For Joomla, prefer <element> tag
if [ "$PLATFORM" = "joomla" ]; then # Read back element outputs
MANIFEST=$(find . -maxdepth 4 -name "*.xml" ! -path "./.git/*" -print0 2>/dev/null | xargs -0 grep -l '<extension' 2>/dev/null | head -1 || true) EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
if [ -n "$MANIFEST" ]; then ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
ELEM=$(grep -oP "<element>\K[^<]+" "$MANIFEST" 2>/dev/null | head -1 || true)
[ -n "$ELEM" ] && EXT_ELEMENT="$ELEM"
fi
fi
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
set -o pipefail [ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip"
ZIP_NAME="${EXT_ELEMENT}-${VERSION}${SUFFIX}.zip"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT" echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT" echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
@@ -115,146 +133,101 @@ jobs:
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ===" echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
- name: Build package - name: Create release
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 id: release
continue-on-error: true
run: | run: |
TAG="${{ steps.meta.outputs.tag }}" TAG="${{ steps.meta.outputs.tag }}"
VERSION="${{ steps.meta.outputs.version }}" VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}" API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
SHA256="${{ steps.zip.outputs.sha256 }}" php ${MOKO_CLI}/release_create.php \
ZIP_NAME="${{ steps.zip.outputs.zip_name }}" --path . --version "$VERSION" --tag "$TAG" \
EXT_ELEMENT="${{ steps.meta.outputs.ext_element }}" --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
TOKEN="${{ secrets.GA_TOKEN }}" --repo "${GITEA_REPO}" --branch dev --prerelease
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
BRANCH=$(git branch --show-current)
BODY="## ${VERSION} ($(date +%Y-%m-%d)) - name: Build package and upload
**Channel:** ${STABILITY} id: package
**SHA-256:** \`${SHA256}\`" run: |
VERSION="${{ steps.meta.outputs.version }}"
TAG="${{ steps.meta.outputs.tag }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php ${MOKO_CLI}/release_package.php \
--path . --version "$VERSION" --tag "$TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --output /tmp || true
# Delete existing release - name: Update updates.xml
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' if: steps.platform.outputs.platform == 'joomla'
run: | run: |
VERSION="${{ steps.meta.outputs.version }}" VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}" STABILITY="${{ steps.meta.outputs.stability }}"
SHA256="${{ steps.zip.outputs.sha256 }}" SHA256="${{ steps.package.outputs.sha256_zip }}"
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 [ ! -f "updates.xml" ]; then
echo "No updates.xml -- skipping"
exit 0
fi
SHA_FLAG=""
[ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
php ${MOKO_CLI}/updates_xml_build.php \
--path . --version "${VERSION}" --stability "${STABILITY}" \
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
${SHA_FLAG}
# Commit and push
if ! git diff --quiet updates.xml 2>/dev/null; then if ! git diff --quiet updates.xml 2>/dev/null; then
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]" git config --local user.name "gitea-actions[bot]"
git add updates.xml git add updates.xml
git commit -m "chore: update $STABILITY channel $VERSION [skip ci]" git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
git push origin HEAD 2>&1 || echo "WARNING: push failed" git push origin HEAD 2>&1 || echo "WARNING: push failed"
fi fi
- name: "Sync updates.xml to all branches" - name: "Sync updates.xml to all branches"
if: steps.platform.outputs.platform == 'joomla' if: steps.platform.outputs.platform == 'joomla'
run: | 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}" CURRENT_BRANCH="${{ github.ref_name }}"
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
for BRANCH in main dev; do
[ "$BRANCH" = "$CURRENT_BRANCH" ] && continue
echo "Syncing updates.xml -> ${BRANCH}"
git fetch origin "${BRANCH}" 2>/dev/null || continue
git checkout "origin/${BRANCH}" -- updates.xml 2>/dev/null || continue
git checkout "${CURRENT_BRANCH}" -- updates.xml
if ! git diff --quiet updates.xml 2>/dev/null; then
git add updates.xml
git commit -m "chore: sync updates.xml from ${CURRENT_BRANCH} [skip ci]"
git push origin HEAD:refs/heads/${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed"
fi
git checkout "${CURRENT_BRANCH}" 2>/dev/null
done
- name: "Delete lesser pre-release channels (cascade)" - name: "Delete lesser pre-release channels (cascade)"
continue-on-error: true continue-on-error: true
run: | run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.GA_TOKEN }}" TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
STABILITY="${{ steps.meta.outputs.stability }}"
# Cascade: rc → beta,alpha,dev | beta → alpha,dev | alpha → dev | dev → nothing php ${MOKO_CLI}/release_cascade.php \
case "$STABILITY" in --stability "${{ steps.meta.outputs.stability }}" \
release-candidate) TAGS_TO_DELETE="beta alpha development" ;; --token "${TOKEN}" \
beta) TAGS_TO_DELETE="alpha development" ;; --api-base "${API_BASE}"
alpha) TAGS_TO_DELETE="development" ;;
*) TAGS_TO_DELETE="" ;;
esac
[ -z "$TAGS_TO_DELETE" ] && exit 0 - name: Summary
if: always()
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
- name: "Post-release version bump"
run: | run: |
MOKO_API="/tmp/moko-platform-api/cli"
VERSION="${{ steps.meta.outputs.version }}" VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
# Bump patch for next dev cycle ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
BUMP_OUTPUT=$(php ${MOKO_API}/version_bump.php --path .) SHA256="${{ steps.package.outputs.sha256_zip }}"
NEXT=$(echo "$BUMP_OUTPUT" | grep -oP '\d{2}\.\d{2}\.\d{2}$' || true) echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY
[ -z "$NEXT" ] && exit 0 echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
# Update platform-specific manifest to next version echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
php ${MOKO_API}/version_set_platform.php --path . --version "${NEXT}" echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY
git add -A echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY
git diff --cached --quiet || { echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY
git commit -m "chore: update development channel ${VERSION} [skip ci]"
git push origin HEAD 2>&1
}
+87 -84
View File
@@ -7,18 +7,14 @@
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Validation # INGROUP: moko-platform.Validation
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API # REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# PATH: /templates/workflows/joomla/repo_health.yml.template # PATH: /templates/workflows/joomla/repo_health.yml.template
# VERSION: 04.06.00 # VERSION: 04.06.00
# BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts. # BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts.
# ============================================================================ # ============================================================================
name: "Joomla: Repo Health" name: "Generic: Repo Health"
concurrency:
group: repo-health-${{ github.repository }}-${{ github.ref }}
cancel-in-progress: true
defaults: defaults:
run: run:
@@ -53,7 +49,7 @@ env:
SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate
# Repo health policy # Repo health policy
REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.gitea/workflows/ REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.mokogitea/workflows/
REPO_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore,README.md,docs/ REPO_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore,README.md,docs/
REPO_DISALLOWED_DIRS: REPO_DISALLOWED_DIRS:
REPO_DISALLOWED_FILES: TODO.md,todo.md REPO_DISALLOWED_FILES: TODO.md,todo.md
@@ -64,7 +60,7 @@ env:
# File / directory variables # File / directory variables
DOCS_INDEX: docs/docs-index.md DOCS_INDEX: docs/docs-index.md
SCRIPT_DIR: scripts SCRIPT_DIR: scripts
WORKFLOWS_DIR: .gitea/workflows WORKFLOWS_DIR: .mokogitea/workflows
SHELLCHECK_PATTERN: '*.sh' SHELLCHECK_PATTERN: '*.sh'
SPDX_FILE_GLOBS: '*.sh,*.php,*.js,*.ts,*.css,*.xml,*.yml,*.yaml' SPDX_FILE_GLOBS: '*.sh,*.php,*.js,*.ts,*.css,*.xml,*.yml,*.yaml'
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
@@ -85,7 +81,7 @@ jobs:
- name: Check actor permission (admin only) - name: Check actor permission (admin only)
id: perm id: perm
env: env:
TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }} TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}
REPO: ${{ github.repository }} REPO: ${{ github.repository }}
ACTOR: ${{ github.actor }} ACTOR: ${{ github.actor }}
run: | run: |
@@ -288,7 +284,7 @@ jobs:
exit 0 exit 0
fi 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}" IFS=',' read -r -a allowed_dirs <<< "${SCRIPTS_ALLOWED_DIRS}"
missing_dirs=() missing_dirs=()
@@ -392,23 +388,27 @@ jobs:
exit 0 exit 0
fi 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 if [ -d "src" ]; then
SOURCE_DIR="src" SOURCE_DIR="src"
elif [ -d "htdocs" ]; then elif [ -d "htdocs" ]; then
SOURCE_DIR="htdocs" SOURCE_DIR="htdocs"
elif [ -d "deploy" ] || [ -d "cli" ] || [ -d "monitoring" ]; then
# Platform/tooling repos don't need src/
SOURCE_DIR=""
else else
missing_required+=("src/ or htdocs/ (source directory required)") missing_required+=("src/ or htdocs/ (source directory required)")
fi 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 for item in "${required_artifacts[@]}"; do
if printf '%s' "${item}" | grep -q '/$'; then if printf '%s' "${item}" | grep -q '/$'; then
d="${item%/}" d="${item%/}"
@@ -450,12 +450,8 @@ jobs:
fi fi
done < <(git branch -r --list 'origin/dev*' | sed 's/^ *//') done < <(git branch -r --list 'origin/dev*' | sed 's/^ *//')
if [ "${#dev_paths[@]}" -eq 0 ]; then if [ "${#dev_paths[@]}" -eq 0 ] && [ "${#dev_branches[@]}" -eq 0 ]; then
missing_required+=("dev/* branch (e.g. dev/01.00.00)") missing_required+=("dev or dev/* branch")
fi
if [ "${#dev_branches[@]}" -gt 0 ]; then
missing_required+=("invalid branch dev (must be dev/<version>)")
fi fi
content_warnings=() content_warnings=()
@@ -481,26 +477,7 @@ jobs:
export MISSING_OPTIONAL="$(printf '%s\n' "${missing_optional[@]:-}")" export MISSING_OPTIONAL="$(printf '%s\n' "${missing_optional[@]:-}")"
export CONTENT_WARNINGS="$(printf '%s\n' "${content_warnings[@]:-}")" export CONTENT_WARNINGS="$(printf '%s\n' "${content_warnings[@]:-}")"
report_json="$(python3 - <<'PY' report_json=$(printf '{"profile":"%s","missing_required":%d,"missing_optional":%d,"content_warnings":%d}' "$profile" "${#missing_required[@]}" "${#missing_optional[@]}" "${#content_warnings[@]}")
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
)"
{ {
printf '%s\n' '### Repository health' printf '%s\n' '### Repository health'
@@ -578,12 +555,14 @@ jobs:
joomla_findings+=("updates.xml missing in root (required for Joomla update server)") joomla_findings+=("updates.xml missing in root (required for Joomla update server)")
fi fi
INDEX_DIRS=("${SOURCE_DIR}" "${SOURCE_DIR}/admin" "${SOURCE_DIR}/site") if [ -n "${SOURCE_DIR}" ]; then
for dir in "${INDEX_DIRS[@]}"; do INDEX_DIRS=("${SOURCE_DIR}" "${SOURCE_DIR}/admin" "${SOURCE_DIR}/site")
if [ -d "${dir}" ] && [ ! -f "${dir}/index.html" ]; then for dir in "${INDEX_DIRS[@]}"; do
joomla_findings+=("${dir}/index.html missing (directory listing protection)") if [ -d "${dir}" ] && [ ! -f "${dir}/index.html" ]; then
fi joomla_findings+=("${dir}/index.html missing (directory listing protection)")
done fi
done
fi
if [ "${#joomla_findings[@]}" -gt 0 ]; then if [ "${#joomla_findings[@]}" -gt 0 ]; then
{ {
@@ -629,43 +608,29 @@ jobs:
fi fi
if [ -f "${DOCS_INDEX}" ]; then if [ -f "${DOCS_INDEX}" ]; then
missing_links="$(python3 - <<'PY' missing_links=""
import os while IFS= read -r docline; do
import re for link in $(echo "$docline" | grep -oE '\]\([^)]+\)' | sed 's/\](//' | sed 's/)$//' || true); do
case "$link" in http://*|https://*|"#"*|mailto:*) continue ;; esac
idx = os.environ.get('DOCS_INDEX', 'docs/docs-index.md') linkpath="${link%%#*}"
base = os.getcwd() linkpath="${linkpath%%\?*}"
[ -z "$linkpath" ] && continue
bad = [] if [ "${linkpath:0:1}" = "/" ]; then
pat = re.compile(r'\[[^\]]+\]\(([^)]+)\)') testpath="${linkpath#/}"
else
with open(idx, 'r', encoding='utf-8') as f: testpath="$(dirname "${DOCS_INDEX}")/${linkpath}"
for line in f: fi
for m in pat.findall(line): [ ! -e "$testpath" ] && missing_links="${missing_links}${testpath} "
link = m.strip() done
if link.startswith('http://') or link.startswith('https://') or link.startswith('#') or link.startswith('mailto:'): done < "${DOCS_INDEX}"
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
)"
if [ -n "${missing_links}" ]; then if [ -n "${missing_links}" ]; then
extended_findings+=("docs/docs-index.md contains broken relative links") extended_findings+=("docs/docs-index.md contains broken relative links")
{ {
printf '%s\n' '### Docs index link integrity' printf '%s\n' '### Docs index link integrity'
printf '%s\n' 'Broken relative links:' 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' printf '\n'
} >> "${GITHUB_STEP_SUMMARY}" } >> "${GITHUB_STEP_SUMMARY}"
fi fi
@@ -764,3 +729,41 @@ jobs:
fi fi
printf '%s\n' 'Repository health guardrails passed.' >> "${GITHUB_STEP_SUMMARY}" 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
+18 -2
View File
@@ -4,8 +4,8 @@
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Security # INGROUP: moko-platform.Security
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards # REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /.gitea/workflows/security-audit.yml # PATH: /.gitea/workflows/security-audit.yml
# VERSION: 01.00.00 # VERSION: 01.00.00
# BRIEF: Dependency vulnerability scanning for composer and npm packages # BRIEF: Dependency vulnerability scanning for composer and npm packages
@@ -80,3 +80,19 @@ jobs:
-H "Priority: high" \ -H "Priority: high" \
-d "Security audit found vulnerabilities. Review dependency updates." \ -d "Security audit found vulnerabilities. Review dependency updates." \
"${NTFY_URL}/${NTFY_TOPIC}" || true "${NTFY_URL}/${NTFY_TOPIC}" || true
- name: Joomla version audit
if: always()
run: |
if [ -f "monitoring/joomla-version-audit.php" ] && [ -n "$JOOMLA_SITES" ]; then
echo "$JOOMLA_SITES" > /tmp/sites.json
php monitoring/joomla-version-audit.php --sites /tmp/sites.json || true
echo "### Joomla Version Audit" >> $GITHUB_STEP_SUMMARY
rm -f /tmp/sites.json
else
echo "Joomla audit skipped (no script or JOOMLA_SITES_JSON not configured)"
fi
env:
JOOMLA_SITES: ${{ vars.JOOMLA_SITES_JSON }}
+150 -302
View File
@@ -4,20 +4,18 @@
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Joomla # INGROUP: moko-platform.Universal
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API # REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /templates/workflows/joomla/update-server.yml.template # PATH: /templates/workflows/update-server.yml
# VERSION: 04.06.00 # VERSION: 05.00.00
# BRIEF: Update Joomla update server XML feed with stable/rc/dev entries # BRIEF: Pre-release build + update server XML for dev/alpha/beta/rc branches
# #
# Writes updates.xml with multiple <update> entries: # Thin wrapper around moko-platform CLI tools.
# - <tag>stable</tag> on push to main (from auto-release) # Builds packages, updates updates.xml, and optionally deploys via SFTP.
# - <tag>rc</tag> on push to rc/**
# - <tag>development</tag> on push to dev or dev/**
# #
# Joomla filters by user's "Minimum Stability" setting. # Joomla filters update entries by the user's "Minimum Stability" setting.
name: "Joomla: Update Server" name: "Update Server"
on: on:
push: push:
@@ -66,55 +64,60 @@ permissions:
jobs: jobs:
update-xml: update-xml:
name: Update updates.xml name: Update Server
runs-on: release runs-on: release
if: >- if: >-
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || github.event_name == 'push' github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || github.event_name == 'push'
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 uses: actions/checkout@v4
with: with:
token: ${{ secrets.GA_TOKEN }} token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 0 fetch-depth: 0
- name: Setup MokoStandards tools - name: Setup moko-platform tools
env: env:
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }} MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.GA_TOKEN }}"}}}' COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.MOKOGITEA_TOKEN }}"}}}'
run: | run: |
if ! command -v composer &> /dev/null; then if ! command -v composer &> /dev/null; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
fi fi
# Always fetch latest CLI tools — never use stale cache from previous runs
rm -rf /tmp/moko-platform
git clone --depth 1 --branch main --quiet \ git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \ "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
/tmp/mokostandards-api 2>/dev/null || true /tmp/moko-platform 2>/dev/null || true
if [ -d "/tmp/mokostandards-api" ] && [ -f "/tmp/mokostandards-api/composer.json" ]; then if [ -d "/tmp/moko-platform" ] && [ -f "/tmp/moko-platform/composer.json" ]; then
cd /tmp/mokostandards-api && composer install --no-dev --no-interaction --quiet 2>/dev/null || true cd /tmp/moko-platform && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
fi fi
echo "MOKO_CLI=/tmp/moko-platform/cli" >> "$GITHUB_ENV"
- name: Generate updates.xml entry - name: Detect platform
id: update id: platform
run: php ${MOKO_CLI}/manifest_read.php --path . --github-output
- name: Resolve stability and bump version
id: meta
run: | run: |
BRANCH="${{ github.ref_name }}" BRANCH="${{ github.ref_name }}"
REPO="${{ github.repository }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
VERSION=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null || echo "0.0.0")
# Auto-bump patch on all branches (dev, alpha, beta, rc) # Configure git for bot pushes
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]" git config --local user.name "gitea-actions[bot]"
BUMPED=$(php /tmp/mokostandards-api/cli/version_bump.php --path . 2>/dev/null || true) git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
if [ -n "$BUMPED" ]; then
VERSION=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null || echo "$VERSION")
git add -A
git commit -m "chore(version): auto-bump patch ${VERSION} [skip ci]" \
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" 2>/dev/null || true
git push 2>/dev/null || true
fi
# Determine stability from branch or input # Auto-bump patch version
php ${MOKO_CLI}/version_bump.php --path . 2>/dev/null || true
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "0.0.0")
# Strip any existing suffix before applying stability
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
# Determine stability from branch or manual input
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
STABILITY="${{ inputs.stability }}" STABILITY="${{ inputs.stability }}"
elif [[ "$BRANCH" == rc/* ]]; then elif [[ "$BRANCH" == rc/* ]]; then
@@ -123,277 +126,122 @@ jobs:
STABILITY="beta" STABILITY="beta"
elif [[ "$BRANCH" == alpha/* ]]; then elif [[ "$BRANCH" == alpha/* ]]; then
STABILITY="alpha" STABILITY="alpha"
elif [[ "$BRANCH" == dev/* ]] || [[ "$BRANCH" == "dev" ]]; then else
STABILITY="development" STABILITY="development"
else
STABILITY="stable"
fi fi
# Version suffix per stability stream
case "$STABILITY" in
development) SUFFIX="-dev"; TAG="development" ;;
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
beta) SUFFIX="-beta"; TAG="beta" ;;
rc) SUFFIX="-rc"; TAG="release-candidate" ;;
*) SUFFIX=""; TAG="stable" ;;
esac
# Propagate version with stability suffix to all manifest files
php ${MOKO_CLI}/version_set_platform.php \
--path . --version "$VERSION" --branch "$BRANCH" --stability "$STABILITY" 2>/dev/null || true
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
# Re-read version (now includes suffix from version_set_platform)
if [ -n "$SUFFIX" ]; then
VERSION="${VERSION}${SUFFIX}"
fi
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT" echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "display_version=${VERSION}" >> "$GITHUB_OUTPUT"
# Parse manifest (portable — no grep -P) # Commit version bump if changed
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "./build/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1) git add -A
if [ -z "$MANIFEST" ]; then
echo "No Joomla manifest found — skipping"
exit 0
fi
# Extract fields using sed (works on all runners)
EXT_NAME=$(sed -n 's/.*<name>\([^<]*\)<\/name>.*/\1/p' "$MANIFEST" | head -1)
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)
EXT_VERSION=$(sed -n 's/.*<version>\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1)
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)
# Fallbacks
[ -z "$EXT_NAME" ] && EXT_NAME="${{ github.event.repository.name }}"
[ -z "$EXT_TYPE" ] && EXT_TYPE="component"
# Derive element if not in manifest: try XML filename, then repo name
if [ -z "$EXT_ELEMENT" ]; then
EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]')
case "$EXT_ELEMENT" in
templatedetails|manifest|*.xml) EXT_ELEMENT=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;;
esac
fi
# Use manifest version if README version is empty
[ "$VERSION" = "0.0.0" ] && [ -n "$EXT_VERSION" ] && VERSION="$EXT_VERSION"
[ -z "$TARGET_PLATFORM" ] && TARGET_PLATFORM=$(printf '<targetplatform name="joomla" version="((5.[0-9])|(6.[0-9]))" %s>' "/")
CLIENT_TAG=""
[ -n "$EXT_CLIENT" ] && CLIENT_TAG="<client>${EXT_CLIENT}</client>"
[ -z "$CLIENT_TAG" ] && ([ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]) && CLIENT_TAG="<client>site</client>"
FOLDER_TAG=""
[ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ] && FOLDER_TAG="<folder>${EXT_FOLDER}</folder>"
PHP_TAG=""
[ -n "$PHP_MINIMUM" ] && PHP_TAG="<php_minimum>${PHP_MINIMUM}</php_minimum>"
# Version suffix for non-stable
DISPLAY_VERSION="$VERSION"
case "$STABILITY" in
development) DISPLAY_VERSION="${VERSION}-dev" ;;
alpha) DISPLAY_VERSION="${VERSION}-alpha" ;;
beta) DISPLAY_VERSION="${VERSION}-beta" ;;
rc) DISPLAY_VERSION="${VERSION}-rc" ;;
esac
MAJOR=$(echo "$VERSION" | awk -F. '{print $1}')
# Each stability level has its own release tag
case "$STABILITY" in
development) RELEASE_TAG="development" ;;
alpha) RELEASE_TAG="alpha" ;;
beta) RELEASE_TAG="beta" ;;
rc) RELEASE_TAG="release-candidate" ;;
*) RELEASE_TAG="v${MAJOR}" ;;
esac
PACKAGE_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.zip"
DOWNLOAD_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${PACKAGE_NAME}"
INFO_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}"
# -- Build install packages (ZIP + tar.gz) --------------------
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
if [ -d "$SOURCE_DIR" ]; then
EXCLUDES=".ftpignore sftp-config* *.ppk *.pem *.key .env*"
TAR_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.tar.gz"
cd "$SOURCE_DIR"
zip -r "/tmp/${PACKAGE_NAME}" . -x $EXCLUDES
cd ..
tar -czf "/tmp/${TAR_NAME}" -C "$SOURCE_DIR" \
--exclude='.ftpignore' --exclude='sftp-config*' \
--exclude='*.ppk' --exclude='*.pem' --exclude='*.key' --exclude='.env*' .
SHA256=$(sha256sum "/tmp/${PACKAGE_NAME}" | cut -d' ' -f1)
# Ensure release exists on Gitea
RELEASE_JSON=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
"${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true)
RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
if [ -z "$RELEASE_ID" ]; then
# Create release
RELEASE_JSON=$(curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
-H "Content-Type: application/json" \
"${API_BASE}/releases" \
-d "$(python3 -c "import json; print(json.dumps({
'tag_name': '${RELEASE_TAG}',
'name': '${RELEASE_TAG} (${DISPLAY_VERSION})',
'body': '${STABILITY} release',
'prerelease': True,
'target_commitish': 'main'
}))")" 2>/dev/null || true)
RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
fi
if [ -n "$RELEASE_ID" ]; then
# Delete existing assets with same name before uploading
ASSETS=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
"${API_BASE}/releases/${RELEASE_ID}/assets" 2>/dev/null || echo "[]")
for ASSET_FILE in "$PACKAGE_NAME" "$TAR_NAME"; do
ASSET_ID=$(echo "$ASSETS" | python3 -c "
import sys,json
assets = json.load(sys.stdin)
for a in assets:
if a['name'] == '${ASSET_FILE}':
print(a['id']); break
" 2>/dev/null || true)
if [ -n "$ASSET_ID" ]; then
curl -sf -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
"${API_BASE}/releases/${RELEASE_ID}/assets/${ASSET_ID}" 2>/dev/null || true
fi
done
# Upload both formats
curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
-H "Content-Type: application/octet-stream" \
--data-binary @"/tmp/${PACKAGE_NAME}" \
"${API_BASE}/releases/${RELEASE_ID}/assets?name=${PACKAGE_NAME}" > /dev/null 2>&1 || true
curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
-H "Content-Type: application/octet-stream" \
--data-binary @"/tmp/${TAR_NAME}" \
"${API_BASE}/releases/${RELEASE_ID}/assets?name=${TAR_NAME}" > /dev/null 2>&1 || true
fi
echo "Packages: ${PACKAGE_NAME} + ${TAR_NAME} (SHA: ${SHA256})" >> $GITHUB_STEP_SUMMARY
else
SHA256=""
fi
# -- Build the new entry (canonical format matching release.yml) --
NEW_ENTRY=""
NEW_ENTRY="${NEW_ENTRY} <update>\n"
NEW_ENTRY="${NEW_ENTRY} <name>${EXT_NAME}</name>\n"
NEW_ENTRY="${NEW_ENTRY} <description>${EXT_NAME} ${STABILITY} build.</description>\n"
NEW_ENTRY="${NEW_ENTRY} <element>${EXT_ELEMENT}</element>\n"
NEW_ENTRY="${NEW_ENTRY} <type>${EXT_TYPE}</type>\n"
[ -n "$CLIENT_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${CLIENT_TAG}\n"
[ -n "$FOLDER_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${FOLDER_TAG}\n"
NEW_ENTRY="${NEW_ENTRY} <version>${VERSION}</version>\n"
NEW_ENTRY="${NEW_ENTRY} <creationDate>$(date +%Y-%m-%d)</creationDate>\n"
NEW_ENTRY="${NEW_ENTRY} <infourl title='${EXT_NAME}'>https://git.mokoconsulting.tech/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${RELEASE_TAG}</infourl>\n"
NEW_ENTRY="${NEW_ENTRY} <downloads>\n"
NEW_ENTRY="${NEW_ENTRY} <downloadurl type='full' format='zip'>${DOWNLOAD_URL}</downloadurl>\n"
NEW_ENTRY="${NEW_ENTRY} </downloads>\n"
[ -n "$SHA256" ] && NEW_ENTRY="${NEW_ENTRY} <sha256>${SHA256}</sha256>\n"
NEW_ENTRY="${NEW_ENTRY} <tags><tag>${STABILITY}</tag></tags>\n"
NEW_ENTRY="${NEW_ENTRY} <maintainer>Moko Consulting</maintainer>\n"
NEW_ENTRY="${NEW_ENTRY} <maintainerurl>https://mokoconsulting.tech</maintainerurl>\n"
NEW_ENTRY="${NEW_ENTRY} <targetplatform name='joomla' version='(5|6).*'/>\n"
[ -n "$PHP_MINIMUM" ] && NEW_ENTRY="${NEW_ENTRY} <php_minimum>${PHP_MINIMUM}</php_minimum>\n"
NEW_ENTRY="${NEW_ENTRY} </update>"
# -- Write new entry to temp file --------------------------------
printf '%b' "$NEW_ENTRY" > /tmp/new_entry.xml
# -- Merge into updates.xml ----------------------------------------
# Cascade: stable→all | rc→rc+lower | beta→beta+lower | alpha→alpha+dev | dev→dev
CASCADE_MAP="stable:development,alpha,beta,rc,stable rc:development,alpha,beta,rc beta:development,alpha,beta alpha:development,alpha development:development"
TARGETS=""
for entry in $CASCADE_MAP; do
key="${entry%%:*}"
vals="${entry#*:}"
if [ "$key" = "${STABILITY}" ]; then
TARGETS="$vals"
break
fi
done
[ -z "$TARGETS" ] && TARGETS="${STABILITY}"
echo "Cascade: ${STABILITY} → ${TARGETS}"
# Create updates.xml if missing
if [ ! -f "updates.xml" ]; then
printf '%s\n' "<?xml version='1.0' encoding='UTF-8'?>" > updates.xml
printf '%s\n' "<!-- Copyright (C) $(date +%Y) Moko Consulting -->" >> updates.xml
printf '%s\n' "<updates>" >> updates.xml
printf '%s\n' "</updates>" >> updates.xml
fi
# Update existing blocks or create missing ones
export PY_TARGETS="$TARGETS" PY_VERSION="$VERSION" PY_DATE="$(date +%Y-%m-%d)"
python3 << 'PYEOF'
import re, os
targets = os.environ["PY_TARGETS"].split(",")
version = os.environ["PY_VERSION"]
date = os.environ["PY_DATE"]
with open("updates.xml") as f:
content = f.read()
with open("/tmp/new_entry.xml") as f:
new_entry_template = f.read()
for tag in targets:
tag = tag.strip()
# Build entry with this tag's name
new_entry = re.sub(r"<tag>[^<]*</tag>", f"<tag>{tag}</tag>", new_entry_template)
# Try to find existing block (handles both single-line and multi-line <tags>)
block_pattern = r"(<update>(?:(?!</update>).)*?<tag>" + re.escape(tag) + r"</tag>.*?</update>)"
match = re.search(block_pattern, content, re.DOTALL)
if match:
# Update in place — replace entire block
content = content.replace(match.group(1), new_entry.strip())
print(f" UPDATED: <tag>{tag}</tag> → {version}")
else:
# Create — insert before </updates>
content = content.replace("</updates>", "\n" + new_entry.strip() + "\n\n</updates>")
print(f" CREATED: <tag>{tag}</tag> → {version}")
# Clean up excessive blank lines
content = re.sub(r"\n{3,}", "\n\n", content)
with open("updates.xml", "w") as f:
f.write(content)
PYEOF
# Commit
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git add updates.xml
git diff --cached --quiet || { git diff --cached --quiet || {
git commit -m "chore: update updates.xml (${STABILITY}: ${DISPLAY_VERSION}) [skip ci]" \ git commit -m "chore(version): auto-bump ${VERSION} [skip ci]" \
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" --author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
git push git push
} }
# -- Sync updates.xml to main (for non-main branches) ---------------------- - name: Create release and upload package
id: package
run: |
VERSION="${{ steps.meta.outputs.version }}"
TAG="${{ steps.meta.outputs.tag }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
# Create or update Gitea release
php ${MOKO_CLI}/release_create.php \
--path . --version "$VERSION" --tag "$TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease
# Build package and upload
php ${MOKO_CLI}/release_package.php \
--path . --version "$VERSION" --tag "$TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --output /tmp || true
- name: Update updates.xml
if: steps.platform.outputs.platform == 'joomla'
run: |
VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
SHA256="${{ steps.package.outputs.sha256_zip }}"
if [ ! -f "updates.xml" ]; then
echo "No updates.xml — skipping"
exit 0
fi
SHA_FLAG=""
[ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
php ${MOKO_CLI}/updates_xml_build.php \
--path . --version "${VERSION}" --stability "${STABILITY}" \
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
${SHA_FLAG}
# Commit and push updates.xml
git add updates.xml
git diff --cached --quiet || {
git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
git push
}
- name: Sync updates.xml to main - name: Sync updates.xml to main
if: github.ref_name != 'main' if: github.ref_name != 'main' && steps.platform.outputs.platform == 'joomla'
run: | run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
GA_TOKEN="${{ secrets.GA_TOKEN }}" GITEA_TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
FILE_SHA=$(curl -sf -H "Authorization: token ${GA_TOKEN}" \ FILE_SHA=$(curl -sf -H "Authorization: token ${GITEA_TOKEN}" \
"${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true) "${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then
CONTENT=$(base64 -w0 updates.xml) python3 -c "
curl -sf -X PUT -H "Authorization: token ${GA_TOKEN}" \ import base64, json, urllib.request, sys
-H "Content-Type: application/json" \ with open('updates.xml', 'rb') as f:
"${API_BASE}/contents/updates.xml" \ content = base64.b64encode(f.read()).decode()
-d "$(python3 -c "import json; print(json.dumps({ payload = json.dumps({
'content': '${CONTENT}', 'content': content,
'sha': '${FILE_SHA}', 'sha': '${FILE_SHA}',
'message': 'chore: sync updates.xml from ${STABILITY} [skip ci]', 'message': 'chore: sync updates.xml from ${{ steps.meta.outputs.stability }} [skip ci]',
'branch': 'main' 'branch': 'main'
}))")" > /dev/null 2>&1 \ }).encode()
&& echo "updates.xml synced to main (${STABILITY})" >> $GITHUB_STEP_SUMMARY \ req = urllib.request.Request(
|| echo "WARNING: failed to sync updates.xml to main" >> $GITHUB_STEP_SUMMARY '${API_BASE}/contents/updates.xml',
else data=payload, method='PUT',
echo "WARNING: could not get updates.xml SHA from main" >> $GITHUB_STEP_SUMMARY headers={
'Authorization': 'token ${GITEA_TOKEN}',
'Content-Type': 'application/json'
})
try:
urllib.request.urlopen(req)
print('updates.xml synced to main')
except Exception as e:
print(f'WARNING: sync to main failed: {e}', file=sys.stderr)
"
fi fi
- name: SFTP deploy to dev server - name: SFTP deploy to dev server
@@ -407,12 +255,11 @@ jobs:
DEV_KEY: ${{ secrets.DEV_FTP_KEY }} DEV_KEY: ${{ secrets.DEV_FTP_KEY }}
DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }} DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
run: | run: |
# -- Permission check: admin or maintain role required -------- # Permission check: admin or maintain role required
ACTOR="${{ github.actor }}" ACTOR="${{ github.actor }}"
REPO="${{ github.repository }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
"${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \ "${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \
python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read") python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read")
case "$PERMISSION" in case "$PERMISSION" in
@@ -442,11 +289,11 @@ jobs:
printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json
fi fi
PLATFORM=$(php /tmp/mokostandards-api/cli/platform_detect.php --path . 2>/dev/null || true) PLATFORM=$(php ${MOKO_CLI}/platform_detect.php --path . 2>/dev/null || true)
if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards-api/deploy/deploy-joomla.php" ]; then if [ "$PLATFORM" = "waas-component" ] && [ -f "${MOKO_CLI}/../deploy/deploy-joomla.php" ]; then
php /tmp/mokostandards-api/deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json php ${MOKO_CLI}/../deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
elif [ -f "/tmp/mokostandards-api/deploy/deploy-sftp.php" ]; then elif [ -f "${MOKO_CLI}/../deploy/deploy-sftp.php" ]; then
php /tmp/mokostandards-api/deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json php ${MOKO_CLI}/../deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
fi fi
rm -f /tmp/deploy_key /tmp/sftp-config.json rm -f /tmp/deploy_key /tmp/sftp-config.json
echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY
@@ -454,11 +301,12 @@ jobs:
- name: Summary - name: Summary
if: always() if: always()
run: | run: |
echo "## Joomla Update Server" >> $GITHUB_STEP_SUMMARY VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
DISPLAY="${{ steps.meta.outputs.display_version }}"
echo "## Update Server" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Version | \`${DISPLAY_VERSION}\` |" >> $GITHUB_STEP_SUMMARY echo "| Version | \`${DISPLAY}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Element | \`${EXT_ELEMENT}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Download | [ZIP](${DOWNLOAD_URL}) |" >> $GITHUB_STEP_SUMMARY
+62
View File
@@ -27,10 +27,71 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Changed
- Migrated all workflow and template paths from `.github/` to `.mokogitea/`
- Template source paths updated: `templates/gitea/` to `templates/mokogitea/`
- HCL definition files removed -- Template repos are now the canonical source
### Added
- `branch-cleanup.yml`: auto-delete merged feature branches after PR merge
- `plg_webservices_perfectpublisher`: REST API for Perfect Publisher (com_autotweet) — channels, posts, requests, rules, feeds, and stats
### Planned ### Planned
- License/subscription check - License/subscription check
- System email template branding (DB approach) - System email template branding (DB approach)
### Added
- Trusted IPs: configurable repeatable rows of IP addresses, CIDR ranges, and wildcards that bypass admin session timeout
- Supports exact IPs (192.168.1.100), CIDR (10.0.0.0/24), and wildcards (192.168.1.*)
- Each entry has a label and enabled toggle for easy management
- Current IP display above trusted IPs table so admins can easily add their own IP
### Fixed
- Trusted IP session bypass: moved from `onAfterInitialise` to `boot()` so Joomla's session lifetime is extended before the session handler validates it (was too late, Joomla expired the session first)
- updates.xml: removed stale pre-release entries pointing to non-existent dev artifacts, legacy plugin update entry that caused stable sites to attempt dev downloads
- Removed duplicate `<updateservers>` from inner plugin manifest — only the package-level manifest should register the update server
- Auto-cleanup of stale plugin-level update site entries on install/update (cleans `#__update_sites` and `#__update_sites_extensions`)
## [02.06.00] - 2026-05-25
### Added
- Alias offline bypass: aliases with offline=No override Joomla's global offline setting, allowing access via alias domain while main site is down
- Block non-master users from viewing or editing MokoWaaS plugin settings
- Master user bypasses ALL tenant restrictions (install from URL, global config, sysinfo, installer, templates)
- Admin Help menu redirected to configured support URL (replaces help.joomla.org)
### Fixed
- Install API endpoint: extract ZIP to temp directory before passing to Joomla Installer (was passing ZIP path directly)
- Clean up extracted temp directory on success or failure
- Update site disabled by Joomla when protected=1 — ensureProtectedFlag() now re-enables it
- CI: auto-release fetches updates.xml from main before building (preserves all channels)
- CI: version_check.php --fix runs after version bump in both workflows
- CI: checksums attached as `[filename].sha256` files instead of in release body
- CI: auto-release cleaned up — removed duplicate asset loops, inline Python updater, dead code
- CI: Step 8b uses `release_body_update.php` CLI instead of inline Python
- CI: Step 6 tag creation no longer gated by `is_minor` (was never set)
### Changed
- CI: auto-release uses stream tag `stable` instead of version tag `vXX`
- CI: pre-release package build uses absolute paths (fixes relative path zip error)
## [02.05.00] - 2026-05-24
### Added
- Joomla `protected=1` flag on all MokoWaaS extensions (framework-level disable/uninstall prevention)
- Self-healing protected flag — restored each admin session if cleared
- Block non-master disable via plugin list toggle (`plugins.publish`)
- Package script sets `protected=1, locked=0` on every install/update
- Legacy plugin entry in updates.xml for sites upgrading from standalone plugin
### Fixed
- CI: auto-release workflow `pkg_pkg_` duplication in release names, ZIP filenames, and SHA256 paths
- CI: auto-release now strips existing type prefix and uses `<packagename>` for packages
- CI: `updates_xml_build` was cascading entries for all lower channels on stable release — now writes only current channel
- CI: `targetplatform` regex `((5.[0-9])|(6.[0-9]))` caused Gitea 500 on XML render — simplified to `(5|6)\..*`
- updates.xml stable entry now has correct `<tag>stable</tag>` and download URL
- README slimmed to overview, detailed content moved to wiki
## [02.03.10] - 2026-05-24 ## [02.03.10] - 2026-05-24
### Added ### Added
@@ -368,3 +429,4 @@ When adding entries to this changelog:
--- ---
**Note:** For detailed technical documentation, see the `/docs/` directory and [README.md](README.md). **Note:** For detailed technical documentation, see the `/docs/` directory and [README.md](README.md).
+1 -1
View File
@@ -9,7 +9,7 @@
DEFGROUP: Joomla.Plugin DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS INGROUP: MokoWaaS
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
VERSION: 02.04.00 VERSION: 02.16.03-dev
PATH: /README.md PATH: /README.md
BRIEF: MokoWaaS platform plugin for Joomla BRIEF: MokoWaaS platform plugin for Joomla
--> -->
+1 -1
View File
@@ -7,7 +7,7 @@
<license>GPL-3.0-or-later</license> <license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl> <authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.04.00</version> <version>02.16.03-dev</version>
<description>Minimal API-only component for MokoWaaS. Provides REST endpoints for site health, cache, updates, and backups.</description> <description>Minimal API-only component for MokoWaaS. Provides REST endpoints for site health, cache, updates, and backups.</description>
<namespace path="api/src">Moko\Component\MokoWaaS\Api</namespace> <namespace path="api/src">Moko\Component\MokoWaaS\Api</namespace>
<administration> <administration>
@@ -31,6 +31,7 @@ namespace Moko\Plugin\System\MokoWaaS\Extension;
defined('_JEXEC') or die; defined('_JEXEC') or die;
use Joomla\CMS\Extension\BootableExtensionInterface;
use Joomla\CMS\Factory; use Joomla\CMS\Factory;
use Joomla\CMS\Log\Log; use Joomla\CMS\Log\Log;
use Joomla\CMS\Plugin\CMSPlugin; use Joomla\CMS\Plugin\CMSPlugin;
@@ -38,6 +39,7 @@ use Joomla\CMS\Router\Route;
use Joomla\CMS\Language\Language; use Joomla\CMS\Language\Language;
use Joomla\CMS\Uri\Uri; use Joomla\CMS\Uri\Uri;
use Joomla\CMS\User\UserHelper; use Joomla\CMS\User\UserHelper;
use Psr\Container\ContainerInterface;
/** /**
* MokoWaaS Brand System Plugin * MokoWaaS Brand System Plugin
@@ -47,7 +49,7 @@ use Joomla\CMS\User\UserHelper;
* *
* @since 01.04.00 * @since 01.04.00
*/ */
class MokoWaaS extends CMSPlugin class MokoWaaS extends CMSPlugin implements BootableExtensionInterface
{ {
/** /**
* Obfuscated Grafana URL (XOR + base64). * Obfuscated Grafana URL (XOR + base64).
@@ -114,6 +116,37 @@ class MokoWaaS extends CMSPlugin
*/ */
protected $app; protected $app;
/**
* Boot the extension — runs BEFORE Joomla creates the session.
*
* Extends the Joomla session lifetime for trusted IPs so the
* session handler does not destroy the session before
* onAfterInitialise can run.
*
* @param ContainerInterface $container The DI container.
*
* @return void
*
* @since 02.11.00
*/
public function boot(ContainerInterface $container): void
{
$timeout = (int) $this->params->get('admin_session_timeout', 0);
if ($timeout <= 0)
{
return;
}
if ($this->ipIsTrusted())
{
// Set both PHP and Joomla session lifetimes before the
// session handler runs its expiry check.
ini_set('session.gc_maxlifetime', 315360000);
Factory::getConfig()->set('lifetime', 525600);
}
}
/** /**
* Event triggered after the framework has loaded and the application initialise method has been called. * Event triggered after the framework has loaded and the application initialise method has been called.
* *
@@ -129,6 +162,11 @@ class MokoWaaS extends CMSPlugin
// Security: HTTPS redirect (runs for all clients) // Security: HTTPS redirect (runs for all clients)
$this->enforceHttps(); $this->enforceHttps();
// Site alias handling: offline page and backend redirect.
// Must run in onAfterInitialise (not onAfterRoute) so that
// Joomla's offline check in doExecute() sees the updated config.
$this->handleSiteAlias();
// MokoWaaS API endpoints (run before routing) // MokoWaaS API endpoints (run before routing)
$mokoAction = $this->app->input->get('mokowaas', ''); $mokoAction = $this->app->input->get('mokowaas', '');
@@ -653,7 +691,7 @@ class MokoWaaS extends CMSPlugin
return [ return [
'{{BRAND_NAME}}' => $this->params->get('brand_name', 'MokoWaaS'), '{{BRAND_NAME}}' => $this->params->get('brand_name', 'MokoWaaS'),
'{{COMPANY_NAME}}' => $this->params->get('company_name', 'Moko Consulting'), '{{COMPANY_NAME}}' => $this->params->get('company_name', 'Moko Consulting'),
'{{SUPPORT_URL}}' => $this->params->get('support_url', 'https://mokoconsulting.tech'), '{{SUPPORT_URL}}' => $this->params->get('support_url', 'https://mokoconsulting.tech/support'),
]; ];
} }
@@ -893,9 +931,6 @@ class MokoWaaS extends CMSPlugin
*/ */
public function onAfterRoute() public function onAfterRoute()
{ {
// Site alias handling: offline page and backend redirect
$this->handleSiteAlias();
if (!$this->app->isClient('administrator')) if (!$this->app->isClient('administrator'))
{ {
return; return;
@@ -936,6 +971,7 @@ class MokoWaaS extends CMSPlugin
} }
$this->injectFavicon($doc); $this->injectFavicon($doc);
$this->redirectHelpMenu($doc);
// Hide MokoWaaS from plugin list for non-master users // Hide MokoWaaS from plugin list for non-master users
if (!$this->isMasterUser()) if (!$this->isMasterUser())
@@ -975,6 +1011,32 @@ class MokoWaaS extends CMSPlugin
"); ");
} }
/**
* Redirect the admin Help menu link to the configured support URL.
*
* Joomla's Atum template hardcodes the Help link to help.joomla.org.
* This replaces it with the WaaS support URL via JS injection.
*
* @param \Joomla\CMS\Document\HtmlDocument $doc Document object
*
* @return void
*
* @since 02.10.00
*/
protected function redirectHelpMenu($doc)
{
$supportUrl = $this->params->get('support_url', 'https://mokoconsulting.tech/support');
$doc->addScriptDeclaration("
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('a[href*=\"help.joomla.org\"], a[href*=\"docs.joomla.org\"]').forEach(function(link) {
link.href = " . json_encode($supportUrl) . ";
link.target = '_blank';
});
});
");
}
/** /**
* Protect the plugin from being disabled or uninstalled by non-master users. * Protect the plugin from being disabled or uninstalled by non-master users.
* Does NOT self-heal (no lock) — master users can still disable if needed. * Does NOT self-heal (no lock) — master users can still disable if needed.
@@ -1025,6 +1087,31 @@ class MokoWaaS extends CMSPlugin
$this->app->redirect('index.php?option=com_plugins'); $this->app->redirect('index.php?option=com_plugins');
} }
} }
// Block non-master from viewing or editing MokoWaaS plugin settings
if ($option === 'com_plugins')
{
$view = $this->app->input->get('view', '');
$layout = $this->app->input->get('layout', '');
$extensionId = (int) $this->app->input->get('extension_id', 0);
if (($view === 'plugin' || $layout === 'edit') && $extensionId > 0)
{
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__extensions'))
->where($db->quoteName('extension_id') . ' = ' . $extensionId)
->where($db->quoteName('element') . ' = ' . $db->quote('mokowaas'))
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'));
if ((int) $db->setQuery($query)->loadResult() > 0)
{
$this->app->enqueueMessage('MokoWaaS settings are restricted to the master user.', 'warning');
$this->app->redirect('index.php?option=com_plugins');
}
}
}
} }
/** /**
@@ -1042,6 +1129,8 @@ class MokoWaaS extends CMSPlugin
try try
{ {
$db = Factory::getDbo(); $db = Factory::getDbo();
// Set protected=1, locked=0 on MokoWaaS extensions
$query = $db->getQuery(true) $query = $db->getQuery(true)
->update($db->quoteName('#__extensions')) ->update($db->quoteName('#__extensions'))
->set($db->quoteName('protected') . ' = 1') ->set($db->quoteName('protected') . ' = 1')
@@ -1051,6 +1140,18 @@ class MokoWaaS extends CMSPlugin
->where($db->quoteName('protected') . ' = 0'); ->where($db->quoteName('protected') . ' = 0');
$db->setQuery($query); $db->setQuery($query);
$db->execute(); $db->execute();
// Ensure update site stays enabled (protected extensions get their update site disabled by Joomla)
$query = $db->getQuery(true)
->update($db->quoteName('#__update_sites') . ' AS us')
->join('INNER', $db->quoteName('#__update_sites_extensions') . ' AS use2 ON us.update_site_id = use2.update_site_id')
->join('INNER', $db->quoteName('#__extensions') . ' AS e ON use2.extension_id = e.extension_id')
->set('us.enabled = 1')
->where('us.enabled = 0')
->where('(' . $db->quoteName('e.element') . ' = ' . $db->quote('mokowaas')
. ' OR ' . $db->quoteName('e.element') . ' = ' . $db->quote('pkg_mokowaas') . ')');
$db->setQuery($query);
$db->execute();
} }
catch (\Throwable $e) catch (\Throwable $e)
{ {
@@ -2747,11 +2848,36 @@ class MokoWaaS extends CMSPlugin
file_put_contents($tmpFile, $zipData); file_put_contents($tmpFile, $zipData);
// Extract ZIP to temp directory
$extractDir = $this->app->getConfig()->get('tmp_path', JPATH_ROOT . '/tmp')
. '/mokowaas_extract_' . md5($url);
if (is_dir($extractDir))
{
$this->rmdirRecursive($extractDir);
}
mkdir($extractDir, 0755, true);
$zip = new \ZipArchive();
if ($zip->open($tmpFile) !== true)
{
@unlink($tmpFile);
$this->sendHealthResponse(500, ['error' => 'Failed to open ZIP']);
return;
}
$zip->extractTo($extractDir);
$zip->close();
@unlink($tmpFile);
// Install using Joomla's installer // Install using Joomla's installer
$installer = \Joomla\CMS\Installer\Installer::getInstance(); $installer = \Joomla\CMS\Installer\Installer::getInstance();
$result = $installer->install($tmpFile); $result = $installer->install($extractDir);
@unlink($tmpFile); $this->rmdirRecursive($extractDir);
if ($result) if ($result)
{ {
@@ -2774,6 +2900,11 @@ class MokoWaaS extends CMSPlugin
{ {
@unlink($tmpFile ?? ''); @unlink($tmpFile ?? '');
if (!empty($extractDir) && is_dir($extractDir))
{
$this->rmdirRecursive($extractDir);
}
$this->sendHealthResponse(500, [ $this->sendHealthResponse(500, [
'error' => 'Install exception', 'error' => 'Install exception',
'message' => $e->getMessage(), 'message' => $e->getMessage(),
@@ -2782,6 +2913,42 @@ class MokoWaaS extends CMSPlugin
} }
} }
/**
* Recursively remove a directory.
*
* @param string $dir Directory path
*
* @return void
*
* @since 02.06.00
*/
protected function rmdirRecursive(string $dir): void
{
if (!is_dir($dir))
{
return;
}
$items = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS),
\RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($items as $item)
{
if ($item->isDir())
{
rmdir($item->getPathname());
}
else
{
unlink($item->getPathname());
}
}
rmdir($dir);
}
// ------------------------------------------------------------------ // ------------------------------------------------------------------
// Site Alias handling // Site Alias handling
// ------------------------------------------------------------------ // ------------------------------------------------------------------
@@ -2916,7 +3083,8 @@ class MokoWaaS extends CMSPlugin
/** /**
* Handle site alias logic: offline page and backend redirect. * Handle site alias logic: offline page and backend redirect.
* *
* Runs early in onAfterInitialise before routing occurs. * Runs in onAfterInitialise so that Joomla's offline check in
* SiteApplication::doExecute() sees the updated config value.
* *
* @return void * @return void
* *
@@ -2944,25 +3112,33 @@ class MokoWaaS extends CMSPlugin
} }
// Offline: use Joomla's native offline mode for frontend requests // Offline: use Joomla's native offline mode for frontend requests
if (!empty($alias->offline) && (string) $alias->offline === '1' if ($this->app->isClient('site'))
&& $this->app->isClient('site'))
{ {
// Allow health API to still respond if (!empty($alias->offline) && (string) $alias->offline === '1')
if ($this->app->input->get('mokowaas', '') !== '')
{ {
return; // Allow health API to still respond
if ($this->app->input->get('mokowaas', '') !== '')
{
return;
}
// Set custom offline message if provided
$message = $alias->offline_message ?? '';
if (!empty($message))
{
$this->app->getConfig()->set('offline_message', $message);
}
// Enable Joomla's native offline mode
$this->app->getConfig()->set('offline', 1);
} }
else
// Set custom offline message if provided
$message = $alias->offline_message ?? '';
if (!empty($message))
{ {
$this->app->getConfig()->set('offline_message', $message); // Alias is NOT offline — override Joomla's global offline setting
// This allows access via the alias domain even when the main site is offline
$this->app->getConfig()->set('offline', 0);
} }
// Enable Joomla's native offline mode — renders through the template's offline.php
$this->app->getConfig()->set('offline', 1);
} }
} }
@@ -3203,6 +3379,12 @@ class MokoWaaS extends CMSPlugin
return; return;
} }
// Trusted IPs — session lifetime already extended in boot()
if ($this->ipIsTrusted())
{
return;
}
$session = Factory::getSession(); $session = Factory::getSession();
$lastHit = $session->get('mokowaas.last_activity', 0); $lastHit = $session->get('mokowaas.last_activity', 0);
$now = time(); $now = time();
@@ -3220,6 +3402,93 @@ class MokoWaaS extends CMSPlugin
$session->set('mokowaas.last_activity', $now); $session->set('mokowaas.last_activity', $now);
} }
/**
* Check whether the current request IP matches any trusted IP entry.
*
* Supports exact IPs, CIDR notation (e.g. 10.0.0.0/8), and
* wildcard patterns (e.g. 192.168.1.*).
*
* @return bool True if the current IP is in the trusted list.
*
* @since 02.11.00
*/
protected function ipIsTrusted(): bool
{
$entries = $this->params->get('trusted_ips', '');
if (empty($entries))
{
return false;
}
// Subform stores as JSON string or array
if (\is_string($entries))
{
$entries = json_decode($entries, true);
}
if (!\is_array($entries))
{
return false;
}
$ip = $this->app
? $this->app->input->server->getString('REMOTE_ADDR', '')
: ($_SERVER['REMOTE_ADDR'] ?? '');
$ipLong = ip2long($ip);
if ($ipLong === false)
{
return false;
}
foreach ($entries as $entry)
{
if (empty($entry['enabled']) || empty($entry['ip']))
{
continue;
}
$range = trim($entry['ip']);
// Wildcard: 192.168.1.*
if (str_contains($range, '*'))
{
$pattern = '/^' . str_replace(['.', '*'], ['\\.', '\\d+'], $range) . '$/';
if (preg_match($pattern, $ip))
{
return true;
}
continue;
}
// CIDR: 10.0.0.0/8
if (str_contains($range, '/'))
{
[$subnet, $bits] = explode('/', $range, 2);
$subnetLong = ip2long($subnet);
$mask = -1 << (32 - (int) $bits);
if ($subnetLong !== false && ($ipLong & $mask) === ($subnetLong & $mask))
{
return true;
}
continue;
}
// Exact match
if ($ip === $range)
{
return true;
}
}
return false;
}
/** /**
* Override Joomla upload restrictions at runtime. * Override Joomla upload restrictions at runtime.
@@ -3328,12 +3597,18 @@ class MokoWaaS extends CMSPlugin
*/ */
protected function enforceAdminRestrictions() protected function enforceAdminRestrictions()
{ {
// Master user bypasses ALL restrictions
if ($this->isMasterUser())
{
return;
}
$input = $this->app->input; $input = $this->app->input;
$option = $input->get('option', ''); $option = $input->get('option', '');
$view = $input->get('view', ''); $view = $input->get('view', '');
$task = $input->get('task', ''); $task = $input->get('task', '');
// Disable install-from-URL for ALL users (safety net) // Disable install-from-URL for non-master users
if ($this->params->get('disable_install_url', 1) if ($this->params->get('disable_install_url', 1)
&& $option === 'com_installer' && $option === 'com_installer'
&& stripos($task, 'install') !== false && stripos($task, 'install') !== false
@@ -3344,12 +3619,6 @@ class MokoWaaS extends CMSPlugin
return; return;
} }
// Remaining restrictions only apply to non-master users
if ($this->isMasterUser())
{
return;
}
$blocked = []; $blocked = [];
if ($this->params->get('restrict_installer', 1)) if ($this->params->get('restrict_installer', 1))
@@ -0,0 +1,40 @@
<?php
/**
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* SPDX-LICENSE-IDENTIFIER: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* VERSION: 02.11.00
* PATH: /src/Field/CurrentIpField.php
* BRIEF: Read-only field that displays the current user's IP address
*/
namespace Moko\Plugin\System\MokoWaaS\Field;
defined('_JEXEC') or die;
use Joomla\CMS\Form\FormField;
class CurrentIpField extends FormField
{
protected $type = 'CurrentIp';
protected function getInput()
{
$currentIp = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
return '<div class="alert alert-info mb-0 py-2">'
. '<strong>Your current IP:</strong> '
. '<code>' . htmlspecialchars($currentIp) . '</code> '
. '<small class="text-muted">&mdash; add this to the table below to keep your session alive.</small>'
. '</div>';
}
protected function getLabel()
{
return '';
}
}
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<form>
<field
name="ip"
type="text"
label="PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_ADDR_LABEL"
description="PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_ADDR_DESC"
required="true"
hint="e.g. 192.168.1.100 or 10.0.0.0/24"
/>
<field
name="label"
type="text"
label="PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_LABEL_LABEL"
description="PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_LABEL_DESC"
hint="e.g. Office network"
/>
<field
name="enabled"
type="radio"
label="PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_ENABLED_LABEL"
default="1"
class="btn-group btn-group-yesno"
>
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
</form>
@@ -120,6 +120,13 @@ PLG_SYSTEM_MOKOWAAS_FORCE_HTTPS_LABEL="Force HTTPS"
PLG_SYSTEM_MOKOWAAS_FORCE_HTTPS_DESC="Redirect all HTTP requests to HTTPS. Supports reverse proxy setups." PLG_SYSTEM_MOKOWAAS_FORCE_HTTPS_DESC="Redirect all HTTP requests to HTTPS. Supports reverse proxy setups."
PLG_SYSTEM_MOKOWAAS_SESSION_TIMEOUT_LABEL="Admin Session Timeout" PLG_SYSTEM_MOKOWAAS_SESSION_TIMEOUT_LABEL="Admin Session Timeout"
PLG_SYSTEM_MOKOWAAS_SESSION_TIMEOUT_DESC="Minutes of idle time before admin sessions expire. 0 uses the Joomla default." PLG_SYSTEM_MOKOWAAS_SESSION_TIMEOUT_DESC="Minutes of idle time before admin sessions expire. 0 uses the Joomla default."
PLG_SYSTEM_MOKOWAAS_TRUSTED_IPS_LABEL="Trusted IPs (No Session Timeout)"
PLG_SYSTEM_MOKOWAAS_TRUSTED_IPS_DESC="Sessions from these IP addresses or ranges will never time out. Supports exact IPs, CIDR notation (e.g. 10.0.0.0/24), and wildcards (e.g. 192.168.1.*)."
PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_ADDR_LABEL="IP / CIDR"
PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_ADDR_DESC="An IP address, CIDR range, or wildcard pattern."
PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_LABEL_LABEL="Label"
PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_LABEL_DESC="A descriptive label for this entry (e.g. Office, VPN)."
PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_ENABLED_LABEL="Enabled"
PLG_SYSTEM_MOKOWAAS_PASSWORD_LENGTH_LABEL="Minimum Password Length" PLG_SYSTEM_MOKOWAAS_PASSWORD_LENGTH_LABEL="Minimum Password Length"
PLG_SYSTEM_MOKOWAAS_PASSWORD_LENGTH_DESC="Minimum number of characters required for user passwords." PLG_SYSTEM_MOKOWAAS_PASSWORD_LENGTH_DESC="Minimum number of characters required for user passwords."
PLG_SYSTEM_MOKOWAAS_PASSWORD_UPPER_LABEL="Require Uppercase" PLG_SYSTEM_MOKOWAAS_PASSWORD_UPPER_LABEL="Require Uppercase"
@@ -120,6 +120,13 @@ PLG_SYSTEM_MOKOWAAS_FORCE_HTTPS_LABEL="Force HTTPS"
PLG_SYSTEM_MOKOWAAS_FORCE_HTTPS_DESC="Redirect all HTTP requests to HTTPS. Supports reverse proxy setups." PLG_SYSTEM_MOKOWAAS_FORCE_HTTPS_DESC="Redirect all HTTP requests to HTTPS. Supports reverse proxy setups."
PLG_SYSTEM_MOKOWAAS_SESSION_TIMEOUT_LABEL="Admin Session Timeout" PLG_SYSTEM_MOKOWAAS_SESSION_TIMEOUT_LABEL="Admin Session Timeout"
PLG_SYSTEM_MOKOWAAS_SESSION_TIMEOUT_DESC="Minutes of idle time before admin sessions expire. 0 uses the Joomla default." PLG_SYSTEM_MOKOWAAS_SESSION_TIMEOUT_DESC="Minutes of idle time before admin sessions expire. 0 uses the Joomla default."
PLG_SYSTEM_MOKOWAAS_TRUSTED_IPS_LABEL="Trusted IPs (No Session Timeout)"
PLG_SYSTEM_MOKOWAAS_TRUSTED_IPS_DESC="Sessions from these IP addresses or ranges will never time out. Supports exact IPs, CIDR notation (e.g. 10.0.0.0/24), and wildcards (e.g. 192.168.1.*)."
PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_ADDR_LABEL="IP / CIDR"
PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_ADDR_DESC="An IP address, CIDR range, or wildcard pattern."
PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_LABEL_LABEL="Label"
PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_LABEL_DESC="A descriptive label for this entry (e.g. Office, VPN)."
PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_ENABLED_LABEL="Enabled"
PLG_SYSTEM_MOKOWAAS_PASSWORD_LENGTH_LABEL="Minimum Password Length" PLG_SYSTEM_MOKOWAAS_PASSWORD_LENGTH_LABEL="Minimum Password Length"
PLG_SYSTEM_MOKOWAAS_PASSWORD_LENGTH_DESC="Minimum number of characters required for user passwords." PLG_SYSTEM_MOKOWAAS_PASSWORD_LENGTH_DESC="Minimum number of characters required for user passwords."
PLG_SYSTEM_MOKOWAAS_PASSWORD_UPPER_LABEL="Require Uppercase" PLG_SYSTEM_MOKOWAAS_PASSWORD_UPPER_LABEL="Require Uppercase"
+19 -7
View File
@@ -30,16 +30,11 @@
<license>GNU General Public License version 3 or later; see LICENSE.md</license> <license>GNU General Public License version 3 or later; see LICENSE.md</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl> <authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.04.00</version> <version>02.16.03-dev</version>
<description>This plugin rebrands the Joomla system interface with MokoWaaS identity. It applies language overrides and ensures consistent branding across the platform.</description> <description>This plugin rebrands the Joomla system interface with MokoWaaS identity. It applies language overrides and ensures consistent branding across the platform.</description>
<namespace path=".">Moko\Plugin\System\MokoWaaS</namespace> <namespace path=".">Moko\Plugin\System\MokoWaaS</namespace>
<scriptfile>script.php</scriptfile> <scriptfile>script.php</scriptfile>
<!-- Update server configuration -->
<updateservers>
<server type="extension" priority="1" name="MokoWaaS Update Server (Gitea)">https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/raw/branch/main/updates.xml</server>
</updateservers>
<files> <files>
<filename plugin="mokowaas">script.php</filename> <filename plugin="mokowaas">script.php</filename>
<folder>Extension</folder> <folder>Extension</folder>
@@ -108,7 +103,7 @@
type="url" type="url"
label="PLG_SYSTEM_MOKOWAAS_SUPPORT_URL_LABEL" label="PLG_SYSTEM_MOKOWAAS_SUPPORT_URL_LABEL"
description="PLG_SYSTEM_MOKOWAAS_SUPPORT_URL_DESC" description="PLG_SYSTEM_MOKOWAAS_SUPPORT_URL_DESC"
default="https://mokoconsulting.tech" default="https://mokoconsulting.tech/support"
/> />
</fieldset> </fieldset>
<fieldset name="waas_access" <fieldset name="waas_access"
@@ -310,6 +305,7 @@
<fieldset name="security" <fieldset name="security"
label="PLG_SYSTEM_MOKOWAAS_FIELDSET_SECURITY_LABEL" label="PLG_SYSTEM_MOKOWAAS_FIELDSET_SECURITY_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FIELDSET_SECURITY_DESC" description="PLG_SYSTEM_MOKOWAAS_FIELDSET_SECURITY_DESC"
addfieldprefix="Moko\Plugin\System\MokoWaaS\Field"
> >
<field name="force_https" type="radio" default="1" <field name="force_https" type="radio" default="1"
label="PLG_SYSTEM_MOKOWAAS_FORCE_HTTPS_LABEL" label="PLG_SYSTEM_MOKOWAAS_FORCE_HTTPS_LABEL"
@@ -322,6 +318,22 @@
label="PLG_SYSTEM_MOKOWAAS_SESSION_TIMEOUT_LABEL" label="PLG_SYSTEM_MOKOWAAS_SESSION_TIMEOUT_LABEL"
description="PLG_SYSTEM_MOKOWAAS_SESSION_TIMEOUT_DESC" description="PLG_SYSTEM_MOKOWAAS_SESSION_TIMEOUT_DESC"
default="60" hint="Minutes (0 = Joomla default)" /> default="60" hint="Minutes (0 = Joomla default)" />
<field
name="current_ip_display"
type="CurrentIp"
label=""
/>
<field
name="trusted_ips"
type="subform"
label="PLG_SYSTEM_MOKOWAAS_TRUSTED_IPS_LABEL"
description="PLG_SYSTEM_MOKOWAAS_TRUSTED_IPS_DESC"
formsource="plugins/system/mokowaas/forms/trusted_ip_entry.xml"
multiple="true"
layout="joomla.form.field.subform.repeatable-table"
groupByFieldset="false"
buttons="add,remove,move"
/>
<field name="password_min_length" type="number" default="12" <field name="password_min_length" type="number" default="12"
label="PLG_SYSTEM_MOKOWAAS_PASSWORD_LENGTH_LABEL" label="PLG_SYSTEM_MOKOWAAS_PASSWORD_LENGTH_LABEL"
description="PLG_SYSTEM_MOKOWAAS_PASSWORD_LENGTH_DESC" /> description="PLG_SYSTEM_MOKOWAAS_PASSWORD_LENGTH_DESC" />
+73 -1
View File
@@ -123,6 +123,7 @@ class plgSystemMokoWaaSInstallerScript implements InstallerScriptInterface
if ($type === 'install' || $type === 'update') if ($type === 'install' || $type === 'update')
{ {
$this->enableAndLockPlugin(); $this->enableAndLockPlugin();
$this->cleanupPluginUpdateSite();
$this->ensureMokoCassiopeia(); $this->ensureMokoCassiopeia();
$this->installLanguageOverrides(); $this->installLanguageOverrides();
$this->updateLoginSupportUrls(); $this->updateLoginSupportUrls();
@@ -210,6 +211,77 @@ class plgSystemMokoWaaSInstallerScript implements InstallerScriptInterface
$db->execute(); $db->execute();
} }
/**
* Remove the plugin-level update site so only the package-level one remains.
*
* Earlier versions registered an update server in the plugin manifest
* (plg_system_mokowaas) in addition to the package manifest (pkg_mokowaas).
* This caused Joomla to check for plugin-level updates that don't exist,
* leading to failed downloads. Only the package update site should exist.
*
* @return void
*
* @since 02.11.02
*/
private function cleanupPluginUpdateSite()
{
$db = Factory::getDbo();
// Find the extension_id for the plugin
$query = $db->getQuery(true)
->select($db->quoteName('extension_id'))
->from($db->quoteName('#__extensions'))
->where($db->quoteName('element') . ' = ' . $db->quote('mokowaas'))
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where($db->quoteName('folder') . ' = ' . $db->quote('system'));
$db->setQuery($query);
$pluginId = (int) $db->loadResult();
if (!$pluginId)
{
return;
}
// Find update_site_ids linked to the plugin (not the package)
$query = $db->getQuery(true)
->select($db->quoteName('update_site_id'))
->from($db->quoteName('#__update_sites_extensions'))
->where($db->quoteName('extension_id') . ' = ' . $pluginId);
$db->setQuery($query);
$siteIds = $db->loadColumn();
if (empty($siteIds))
{
return;
}
// Delete the link rows
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__update_sites_extensions'))
->where($db->quoteName('extension_id') . ' = ' . $pluginId)
)->execute();
// Delete orphaned update_sites rows (only if no other extension uses them)
foreach ($siteIds as $siteId)
{
$query = $db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__update_sites_extensions'))
->where($db->quoteName('update_site_id') . ' = ' . (int) $siteId);
$db->setQuery($query);
if ((int) $db->loadResult() === 0)
{
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__update_sites'))
->where($db->quoteName('update_site_id') . ' = ' . (int) $siteId)
)->execute();
}
}
}
/** /**
* Ensure MokoOnyx is installed, locked, and set as default. * Ensure MokoOnyx is installed, locked, and set as default.
* *
@@ -482,7 +554,7 @@ class plgSystemMokoWaaSInstallerScript implements InstallerScriptInterface
return [ return [
'{{BRAND_NAME}}' => $params->get('brand_name', 'MokoWaaS'), '{{BRAND_NAME}}' => $params->get('brand_name', 'MokoWaaS'),
'{{COMPANY_NAME}}' => $params->get('company_name', 'Moko Consulting'), '{{COMPANY_NAME}}' => $params->get('company_name', 'Moko Consulting'),
'{{SUPPORT_URL}}' => $params->get('support_url', 'https://mokoconsulting.tech'), '{{SUPPORT_URL}}' => $params->get('support_url', 'https://mokoconsulting.tech/support'),
]; ];
} }
@@ -7,7 +7,7 @@
<license>GPL-3.0-or-later</license> <license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl> <authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.04.00</version> <version>02.16.03-dev</version>
<description>Joomla Web Services API routes for MokoWaaS site management — health checks, cache, updates, backups, and site info.</description> <description>Joomla Web Services API routes for MokoWaaS site management — health checks, cache, updates, backups, and site info.</description>
<namespace path="src">Moko\Plugin\WebServices\MokoWaaS</namespace> <namespace path="src">Moko\Plugin\WebServices\MokoWaaS</namespace>
<files> <files>
@@ -11,6 +11,7 @@ namespace Moko\Plugin\WebServices\MokoWaaS\Extension;
defined('_JEXEC') or die; defined('_JEXEC') or die;
use Joomla\CMS\Plugin\CMSPlugin; use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Event\Application\BeforeApiRouteEvent;
use Joomla\CMS\Router\ApiRouter; use Joomla\CMS\Router\ApiRouter;
use Joomla\Event\SubscriberInterface; use Joomla\Event\SubscriberInterface;
@@ -36,14 +37,16 @@ final class MokoWaaSApi extends CMSPlugin implements SubscriberInterface
/** /**
* Register API routes for MokoWaaS. * Register API routes for MokoWaaS.
* *
* @param ApiRouter $router The API router * @param BeforeApiRouteEvent $event The API route event (Joomla 6 typed event)
* *
* @return void * @return void
* *
* @since 1.0.0 * @since 1.0.0
*/ */
public function onBeforeApiRoute(&$router): void public function onBeforeApiRoute(BeforeApiRouteEvent $event): void
{ {
$router = $event->getRouter();
$router->createCRUDRoutes( $router->createCRUDRoutes(
'v1/mokowaas/health', 'v1/mokowaas/health',
'health', 'health',
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<extension type="plugin" group="webservices" method="upgrade">
<name>Web Services - Perfect Publisher</name>
<author>Moko Consulting</author>
<creationDate>2026-05-28</creationDate>
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.16.03-dev</version>
<description>Joomla Web Services API routes for Perfect Publisher (com_autotweet) — channels, posts, requests, rules, and feeds.</description>
<namespace path="src">Moko\Plugin\WebServices\PerfectPublisher</namespace>
<files>
<folder plugin="perfectpublisher">services</folder>
<folder>src</folder>
</files>
</extension>
@@ -0,0 +1,42 @@
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
* PATH: /src/packages/plg_webservices_perfectpublisher/services/provider.php
* VERSION: 02.13.01
* BRIEF: DI service provider for Perfect Publisher Web Services plugin
*/
defined('_JEXEC') or die;
use Joomla\CMS\Extension\PluginInterface;
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Joomla\Event\DispatcherInterface;
use Moko\Plugin\WebServices\PerfectPublisher\Extension\PerfectPublisherApi;
return new class implements ServiceProviderInterface
{
public function register(Container $container): void
{
$container->set(
PluginInterface::class,
function (Container $container) {
$dispatcher = $container->get(DispatcherInterface::class);
$plugin = new PerfectPublisherApi(
$dispatcher,
(array) PluginHelper::getPlugin('webservices', 'perfectpublisher')
);
$plugin->setApplication(Factory::getApplication());
return $plugin;
}
);
}
};
@@ -0,0 +1,539 @@
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
* PATH: /src/packages/plg_webservices_perfectpublisher/src/Extension/PerfectPublisherApi.php
* VERSION: 02.13.01
* BRIEF: Web Services API plugin for Perfect Publisher (com_autotweet)
*/
namespace Moko\Plugin\WebServices\PerfectPublisher\Extension;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Event\Application\BeforeApiRouteEvent;
use Joomla\CMS\Router\ApiRouter;
use Joomla\Event\SubscriberInterface;
/**
* Perfect Publisher Web Services API Plugin
*
* Registers REST API routes for Perfect Publisher (com_autotweet) data.
* Provides read access to channels, posts, requests, rules, and feeds.
* Provides write access to create publish requests.
*
* Routes:
* GET /v1/perfectpublisher/channels List social channels
* GET /v1/perfectpublisher/channels/:id Get channel detail
* GET /v1/perfectpublisher/posts List published posts
* GET /v1/perfectpublisher/posts/:id Get post detail
* GET /v1/perfectpublisher/requests List pending requests
* POST /v1/perfectpublisher/requests Create a publish request
* GET /v1/perfectpublisher/rules List publishing rules
* GET /v1/perfectpublisher/feeds List RSS feeds
* GET /v1/perfectpublisher/channeltypes List channel type definitions
* GET /v1/perfectpublisher/stats Dashboard statistics
*
* @since 02.13.01
*/
final class PerfectPublisherApi extends CMSPlugin implements SubscriberInterface
{
/**
* @return array
*/
public static function getSubscribedEvents(): array
{
return [
'onBeforeApiRoute' => 'onBeforeApiRoute',
];
}
/**
* Register API routes.
*
* @param BeforeApiRouteEvent $event The API route event
*
* @return void
*/
public function onBeforeApiRoute(BeforeApiRouteEvent $event): void
{
$router = $event->getRouter();
// All routes are handled by this plugin directly via custom callbacks
// because com_autotweet uses FOF, not standard Joomla MVC
$router->addRoute(
new \Joomla\Router\Route(
['GET'],
'v1/perfectpublisher/channels',
[$this, 'getChannels']
)
);
$router->addRoute(
new \Joomla\Router\Route(
['GET'],
'v1/perfectpublisher/channels/:id',
[$this, 'getChannel']
)
);
$router->addRoute(
new \Joomla\Router\Route(
['GET'],
'v1/perfectpublisher/posts',
[$this, 'getPosts']
)
);
$router->addRoute(
new \Joomla\Router\Route(
['GET'],
'v1/perfectpublisher/posts/:id',
[$this, 'getPost']
)
);
$router->addRoute(
new \Joomla\Router\Route(
['GET'],
'v1/perfectpublisher/requests',
[$this, 'getRequests']
)
);
$router->addRoute(
new \Joomla\Router\Route(
['POST'],
'v1/perfectpublisher/requests',
[$this, 'createRequest']
)
);
$router->addRoute(
new \Joomla\Router\Route(
['GET'],
'v1/perfectpublisher/rules',
[$this, 'getRules']
)
);
$router->addRoute(
new \Joomla\Router\Route(
['GET'],
'v1/perfectpublisher/feeds',
[$this, 'getFeeds']
)
);
$router->addRoute(
new \Joomla\Router\Route(
['GET'],
'v1/perfectpublisher/channeltypes',
[$this, 'getChannelTypes']
)
);
$router->addRoute(
new \Joomla\Router\Route(
['GET'],
'v1/perfectpublisher/stats',
[$this, 'getStats']
)
);
}
/**
* GET /v1/perfectpublisher/channels
*
* @return void
*/
public function getChannels(): void
{
$db = Factory::getDbo();
$app = Factory::getApplication();
$limit = (int) $app->input->get('limit', 20);
$offset = (int) $app->input->get('offset', 0);
$query = $db->getQuery(true)
->select('c.*, ct.name AS channeltype_name, ct.max_chars')
->from($db->quoteName('#__autotweet_channels', 'c'))
->leftJoin(
$db->quoteName('#__autotweet_channeltypes', 'ct')
. ' ON ' . $db->quoteName('c.channeltype_id')
. ' = ' . $db->quoteName('ct.id')
)
->order($db->quoteName('c.ordering') . ' ASC');
$published = $app->input->get('published', null);
if ($published !== null) {
$query->where($db->quoteName('c.published') . ' = ' . (int) $published);
}
$db->setQuery($query, $offset, $limit);
$this->sendJsonResponse($db->loadObjectList());
}
/**
* GET /v1/perfectpublisher/channels/:id
*
* @return void
*/
public function getChannel(): void
{
$id = (int) Factory::getApplication()->input->get('id', 0);
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select('c.*, ct.name AS channeltype_name, ct.max_chars, ct.description AS channeltype_desc')
->from($db->quoteName('#__autotweet_channels', 'c'))
->leftJoin(
$db->quoteName('#__autotweet_channeltypes', 'ct')
. ' ON ' . $db->quoteName('c.channeltype_id')
. ' = ' . $db->quoteName('ct.id')
)
->where($db->quoteName('c.id') . ' = ' . $id);
$db->setQuery($query);
$result = $db->loadObject();
if (!$result) {
$this->sendJsonError('Channel not found', 404);
return;
}
// Strip sensitive OAuth params
if (isset($result->params)) {
$params = json_decode($result->params, true);
if (is_array($params)) {
foreach (['access_token', 'access_secret', 'client_secret', 'api_secret', 'password'] as $key) {
if (isset($params[$key])) {
$params[$key] = '***';
}
}
$result->params = json_encode($params);
}
}
$this->sendJsonResponse($result);
}
/**
* GET /v1/perfectpublisher/posts
*
* @return void
*/
public function getPosts(): void
{
$db = Factory::getDbo();
$app = Factory::getApplication();
$limit = (int) $app->input->get('limit', 20);
$offset = (int) $app->input->get('offset', 0);
$query = $db->getQuery(true)
->select('p.*, c.name AS channel_name')
->from($db->quoteName('#__autotweet_posts', 'p'))
->leftJoin(
$db->quoteName('#__autotweet_channels', 'c')
. ' ON ' . $db->quoteName('p.channel_id')
. ' = ' . $db->quoteName('c.id')
)
->order($db->quoteName('p.postdate') . ' DESC');
$pubstate = $app->input->get('pubstate', '');
if ($pubstate !== '') {
$query->where($db->quoteName('p.pubstate') . ' = ' . $db->quote($pubstate));
}
$channel = (int) $app->input->get('channel_id', 0);
if ($channel > 0) {
$query->where($db->quoteName('p.channel_id') . ' = ' . $channel);
}
$db->setQuery($query, $offset, $limit);
$this->sendJsonResponse($db->loadObjectList());
}
/**
* GET /v1/perfectpublisher/posts/:id
*
* @return void
*/
public function getPost(): void
{
$id = (int) Factory::getApplication()->input->get('id', 0);
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select('p.*, c.name AS channel_name, ct.name AS channeltype_name')
->from($db->quoteName('#__autotweet_posts', 'p'))
->leftJoin(
$db->quoteName('#__autotweet_channels', 'c')
. ' ON ' . $db->quoteName('p.channel_id')
. ' = ' . $db->quoteName('c.id')
)
->leftJoin(
$db->quoteName('#__autotweet_channeltypes', 'ct')
. ' ON ' . $db->quoteName('c.channeltype_id')
. ' = ' . $db->quoteName('ct.id')
)
->where($db->quoteName('p.id') . ' = ' . $id);
$db->setQuery($query);
$result = $db->loadObject();
if (!$result) {
$this->sendJsonError('Post not found', 404);
return;
}
$this->sendJsonResponse($result);
}
/**
* GET /v1/perfectpublisher/requests
*
* @return void
*/
public function getRequests(): void
{
$db = Factory::getDbo();
$app = Factory::getApplication();
$limit = (int) $app->input->get('limit', 20);
$offset = (int) $app->input->get('offset', 0);
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__autotweet_requests'))
->order($db->quoteName('publish_up') . ' ASC');
$published = $app->input->get('published', null);
if ($published !== null) {
$query->where($db->quoteName('published') . ' = ' . (int) $published);
}
$db->setQuery($query, $offset, $limit);
$this->sendJsonResponse($db->loadObjectList());
}
/**
* POST /v1/perfectpublisher/requests
*
* Create a new publish request. Required fields: description.
* Optional: url, image_url, publish_up, plugin, priority.
*
* @return void
*/
public function createRequest(): void
{
$app = Factory::getApplication();
$db = Factory::getDbo();
$data = json_decode($app->input->json->getRaw(), true);
if (empty($data['description'])) {
$this->sendJsonError('Field "description" is required', 400);
return;
}
$now = Factory::getDate()->toSql();
$user = Factory::getUser();
$row = (object) [
'ref_id' => $data['ref_id'] ?? null,
'plugin' => $data['plugin'] ?? 'manual-api',
'priority' => (int) ($data['priority'] ?? 5),
'publish_up' => $data['publish_up'] ?? $now,
'description' => $data['description'],
'typeinfo' => (int) ($data['typeinfo'] ?? 0),
'url' => $data['url'] ?? null,
'image_url' => $data['image_url'] ?? null,
'created' => $now,
'created_by' => $user->id,
'params' => json_encode($data['params'] ?? []),
'published' => (int) ($data['published'] ?? 1),
];
$db->insertObject('#__autotweet_requests', $row, 'id');
$this->sendJsonResponse(
['id' => $row->id, 'status' => 'created'],
201
);
}
/**
* GET /v1/perfectpublisher/rules
*
* @return void
*/
public function getRules(): void
{
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select('r.*, rt.name AS ruletype_name, rt.description AS ruletype_desc, c.name AS channel_name')
->from($db->quoteName('#__autotweet_rules', 'r'))
->leftJoin(
$db->quoteName('#__autotweet_ruletypes', 'rt')
. ' ON ' . $db->quoteName('r.ruletype_id')
. ' = ' . $db->quoteName('rt.id')
)
->leftJoin(
$db->quoteName('#__autotweet_channels', 'c')
. ' ON ' . $db->quoteName('r.channel_id')
. ' = ' . $db->quoteName('c.id')
)
->order($db->quoteName('r.ordering') . ' ASC');
$db->setQuery($query);
$this->sendJsonResponse($db->loadObjectList());
}
/**
* GET /v1/perfectpublisher/feeds
*
* @return void
*/
public function getFeeds(): void
{
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__autotweet_feeds'))
->order($db->quoteName('ordering') . ' ASC');
$db->setQuery($query);
$this->sendJsonResponse($db->loadObjectList());
}
/**
* GET /v1/perfectpublisher/channeltypes
*
* @return void
*/
public function getChannelTypes(): void
{
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__autotweet_channeltypes'))
->order($db->quoteName('id') . ' ASC');
$db->setQuery($query);
$this->sendJsonResponse($db->loadObjectList());
}
/**
* GET /v1/perfectpublisher/stats
*
* Dashboard statistics: post counts by status, channel counts, recent activity.
*
* @return void
*/
public function getStats(): void
{
$db = Factory::getDbo();
// Posts by status
$db->setQuery(
$db->getQuery(true)
->select('pubstate, COUNT(*) AS total')
->from($db->quoteName('#__autotweet_posts'))
->group($db->quoteName('pubstate'))
);
$postsByStatus = $db->loadObjectList('pubstate');
// Active channels
$db->setQuery(
$db->getQuery(true)
->select('COUNT(*) AS total')
->from($db->quoteName('#__autotweet_channels'))
->where($db->quoteName('published') . ' = 1')
);
$activeChannels = (int) $db->loadResult();
// Pending requests
$db->setQuery(
$db->getQuery(true)
->select('COUNT(*) AS total')
->from($db->quoteName('#__autotweet_requests'))
->where($db->quoteName('published') . ' = 1')
);
$pendingRequests = (int) $db->loadResult();
// Posts last 24h
$db->setQuery(
$db->getQuery(true)
->select('COUNT(*) AS total')
->from($db->quoteName('#__autotweet_posts'))
->where($db->quoteName('postdate') . ' >= DATE_SUB(NOW(), INTERVAL 1 DAY)')
);
$posts24h = (int) $db->loadResult();
// Posts last 7d
$db->setQuery(
$db->getQuery(true)
->select('COUNT(*) AS total')
->from($db->quoteName('#__autotweet_posts'))
->where($db->quoteName('postdate') . ' >= DATE_SUB(NOW(), INTERVAL 7 DAY)')
);
$posts7d = (int) $db->loadResult();
$this->sendJsonResponse([
'posts_by_status' => $postsByStatus,
'active_channels' => $activeChannels,
'pending_requests' => $pendingRequests,
'posts_24h' => $posts24h,
'posts_7d' => $posts7d,
]);
}
/**
* Send a JSON API response.
*
* @param mixed $data Response data
* @param int $status HTTP status code
*
* @return void
*/
private function sendJsonResponse($data, int $status = 200): void
{
$app = Factory::getApplication();
$app->setHeader('Content-Type', 'application/json; charset=utf-8');
$app->setHeader('Status', (string) $status);
echo json_encode(['data' => $data], JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
$app->close();
}
/**
* Send a JSON error response.
*
* @param string $message Error message
* @param int $status HTTP status code
*
* @return void
*/
private function sendJsonError(string $message, int $status = 400): void
{
$app = Factory::getApplication();
$app->setHeader('Content-Type', 'application/json; charset=utf-8');
$app->setHeader('Status', (string) $status);
echo json_encode(['error' => $message], JSON_UNESCAPED_SLASHES);
$app->close();
}
}
+3 -2
View File
@@ -2,7 +2,7 @@
<extension type="package" method="upgrade"> <extension type="package" method="upgrade">
<name>MokoWaaS</name> <name>MokoWaaS</name>
<packagename>mokowaas</packagename> <packagename>mokowaas</packagename>
<version>02.04.00</version> <version>02.16.03-dev</version>
<creationDate>2026-05-23</creationDate> <creationDate>2026-05-23</creationDate>
<author>Moko Consulting</author> <author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -12,10 +12,11 @@
<description>MokoWaaS site management suite — branding, health monitoring, tenant restrictions, and REST API.</description> <description>MokoWaaS site management suite — branding, health monitoring, tenant restrictions, and REST API.</description>
<scriptfile>script.php</scriptfile> <scriptfile>script.php</scriptfile>
<files> <files folder="packages">
<file type="plugin" id="plg_system_mokowaas" group="system">plg_system_mokowaas.zip</file> <file type="plugin" id="plg_system_mokowaas" group="system">plg_system_mokowaas.zip</file>
<file type="component" id="com_mokowaas">com_mokowaas.zip</file> <file type="component" id="com_mokowaas">com_mokowaas.zip</file>
<file type="plugin" id="plg_webservices_mokowaas" group="webservices">plg_webservices_mokowaas.zip</file> <file type="plugin" id="plg_webservices_mokowaas" group="webservices">plg_webservices_mokowaas.zip</file>
<file type="plugin" id="plg_webservices_perfectpublisher" group="webservices">plg_webservices_perfectpublisher.zip</file>
</files> </files>
<updateservers> <updateservers>
+59 -44
View File
@@ -1,83 +1,98 @@
<?xml version='1.0' encoding='UTF-8'?> <?xml version='1.0' encoding='UTF-8'?>
<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> <!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
SPDX-License-Identifier: GPL-3.0-or-later SPDX-License-Identifier: GPL-3.0-or-later
VERSION: 02.04.00 VERSION: 02.16.00-dev
--> -->
<updates> <updates>
<update> <update>
<name>MokoWaaS</name> <name>Package - MokoWaaS</name>
<description>MokoWaaS update</description> <description>Package - MokoWaaS development build.</description>
<element>mokowaas</element> <element>pkg_mokowaas</element>
<type>package</type> <type>package</type>
<version>02.04.00-dev</version> <client>site</client>
<tags><tag>development</tag></tags> <version>02.16.00-dev</version>
<infourl title="MokoWaaS">https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/development</infourl> <creationDate>2026-05-28</creationDate>
<infourl title='Package - MokoWaaS'>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/stable</infourl>
<downloads> <downloads>
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/development/pkg_mokowaas-02.04.00-dev.zip</downloadurl> <downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/stable/pkg_mokowaas-02.16.00-dev.zip</downloadurl>
</downloads> </downloads>
<targetplatform name="joomla" version="(5|6)\..*" /> <sha256>f233e73694b4e4ce0b4d71e828a158438257c1aec6fdb63d4b0482d23fd3d431</sha256>
<tags><tag>dev</tag></tags>
<maintainer>Moko Consulting</maintainer> <maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl> <maintainerurl>https://mokoconsulting.tech</maintainerurl>
<targetplatform name="joomla" version="(5|6)\..*" />
</update> </update>
<update> <update>
<name>MokoWaaS</name> <name>Package - MokoWaaS</name>
<description>MokoWaaS update</description> <description>Package - MokoWaaS alpha build.</description>
<element>mokowaas</element> <element>pkg_mokowaas</element>
<type>package</type> <type>package</type>
<version>02.04.00-alpha</version> <client>site</client>
<version>02.16.00-dev</version>
<creationDate>2026-05-28</creationDate>
<infourl title='Package - MokoWaaS'>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/stable</infourl>
<downloads>
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/stable/pkg_mokowaas-02.16.00-dev.zip</downloadurl>
</downloads>
<sha256>f233e73694b4e4ce0b4d71e828a158438257c1aec6fdb63d4b0482d23fd3d431</sha256>
<tags><tag>alpha</tag></tags> <tags><tag>alpha</tag></tags>
<infourl title="MokoWaaS">https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/alpha</infourl>
<downloads>
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/alpha/pkg_mokowaas-02.04.00-alpha.zip</downloadurl>
</downloads>
<targetplatform name="joomla" version="(5|6)\..*" />
<maintainer>Moko Consulting</maintainer> <maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl> <maintainerurl>https://mokoconsulting.tech</maintainerurl>
<targetplatform name="joomla" version="(5|6)\..*" />
</update> </update>
<update> <update>
<name>MokoWaaS</name> <name>Package - MokoWaaS</name>
<description>MokoWaaS update</description> <description>Package - MokoWaaS beta build.</description>
<element>mokowaas</element> <element>pkg_mokowaas</element>
<type>package</type> <type>package</type>
<version>02.04.00-beta</version> <client>site</client>
<version>02.16.00-dev</version>
<creationDate>2026-05-28</creationDate>
<infourl title='Package - MokoWaaS'>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/stable</infourl>
<downloads>
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/stable/pkg_mokowaas-02.16.00-dev.zip</downloadurl>
</downloads>
<sha256>f233e73694b4e4ce0b4d71e828a158438257c1aec6fdb63d4b0482d23fd3d431</sha256>
<tags><tag>beta</tag></tags> <tags><tag>beta</tag></tags>
<infourl title="MokoWaaS">https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/beta</infourl>
<downloads>
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/beta/pkg_mokowaas-02.04.00-beta.zip</downloadurl>
</downloads>
<targetplatform name="joomla" version="(5|6)\..*" />
<maintainer>Moko Consulting</maintainer> <maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl> <maintainerurl>https://mokoconsulting.tech</maintainerurl>
<targetplatform name="joomla" version="(5|6)\..*" />
</update> </update>
<update> <update>
<name>MokoWaaS</name> <name>Package - MokoWaaS</name>
<description>MokoWaaS update</description> <description>Package - MokoWaaS rc build.</description>
<element>mokowaas</element> <element>pkg_mokowaas</element>
<type>package</type> <type>package</type>
<version>02.04.00-rc</version> <client>site</client>
<version>02.16.00-dev</version>
<creationDate>2026-05-28</creationDate>
<infourl title='Package - MokoWaaS'>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/stable</infourl>
<downloads>
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/stable/pkg_mokowaas-02.16.00-dev.zip</downloadurl>
</downloads>
<sha256>f233e73694b4e4ce0b4d71e828a158438257c1aec6fdb63d4b0482d23fd3d431</sha256>
<tags><tag>rc</tag></tags> <tags><tag>rc</tag></tags>
<infourl title="MokoWaaS">https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/rc</infourl>
<downloads>
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/rc/pkg_mokowaas-02.04.00-rc.zip</downloadurl>
</downloads>
<targetplatform name="joomla" version="(5|6)\..*" />
<maintainer>Moko Consulting</maintainer> <maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl> <maintainerurl>https://mokoconsulting.tech</maintainerurl>
<targetplatform name="joomla" version="(5|6)\..*" />
</update> </update>
<update> <update>
<name>MokoWaaS</name> <name>Package - MokoWaaS</name>
<description>MokoWaaS update</description> <description>Package - MokoWaaS stable build.</description>
<element>mokowaas</element> <element>pkg_mokowaas</element>
<type>package</type> <type>package</type>
<version>02.04.00</version> <client>site</client>
<tags><tag>stable</tag></tags> <version>02.16.00-dev</version>
<infourl title="MokoWaaS">https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/stable</infourl> <creationDate>2026-05-28</creationDate>
<infourl title='Package - MokoWaaS'>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/stable</infourl>
<downloads> <downloads>
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/stable/pkg_mokowaas-02.04.00.zip</downloadurl> <downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/stable/pkg_mokowaas-02.16.00-dev.zip</downloadurl>
</downloads> </downloads>
<targetplatform name="joomla" version="(5|6)\..*" /> <sha256>f233e73694b4e4ce0b4d71e828a158438257c1aec6fdb63d4b0482d23fd3d431</sha256>
<tags><tag>stable</tag></tags>
<maintainer>Moko Consulting</maintainer> <maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl> <maintainerurl>https://mokoconsulting.tech</maintainerurl>
<targetplatform name="joomla" version="(5|6)\..*" />
</update> </update>
</updates> </updates>
+181
View File
@@ -0,0 +1,181 @@
# API Endpoints
MokoWaaS provides 6 remote management endpoints accessible via query string parameter. All endpoints require HTTPS and Bearer token authentication.
## Authentication
All endpoints require the `health_api_token` as a Bearer token in the Authorization header:
```
Authorization: Bearer <health_api_token>
```
The token is auto-generated during plugin installation and stored as a read-only parameter in the plugin configuration. It can also be passed as a `token` query parameter as a fallback.
Token validation uses `hash_equals()` for timing-safe comparison. If no token is configured, the endpoint returns HTTP 503. An invalid token returns HTTP 401.
## Endpoints
### 1. Health Check
```
GET /?mokowaas=health
```
Runs 16 diagnostic checks and returns a comprehensive health report. See [Health Monitoring](Health-Monitoring) for full documentation of all checks and response format.
**Response**: JSON object with `status` (`ok`/`degraded`/`error`), `reason`, `timestamp`, `checks`, and `meta`.
**HTTP Status**: 200 (ok/degraded), 503 (error).
---
### 2. Site Info
```
GET /?mokowaas=info
```
Returns a compact summary of the Joomla site.
**Response**:
| Field | Description |
|---|---|
| `site_name` | Joomla site name |
| `site_url` | Site root URL |
| `joomla_version` | Joomla CMS version |
| `php_version` | PHP version |
| `db_type` | Database driver (e.g. `pdomysql`) |
| `debug` | Whether debug mode is on |
| `sef` | Whether SEF URLs are enabled |
| `caching` | Whether caching is enabled |
| `articles` | Total article count |
| `users` | Total user count |
| `extensions` | Number of enabled extensions |
| `brand` | Configured brand name |
| `plugin_version` | MokoWaaS plugin version |
---
### 3. Remote Install
```
POST /?mokowaas=install
Content-Type: application/json
{"url": "https://example.com/extension.zip"}
```
Downloads and installs a Joomla extension from the provided URL. The extension is downloaded to a temporary directory, extracted, and installed using Joomla's installer API.
**Response**: JSON object with `status`, `extension` name, and `message`.
**HTTP Status**: 200 (success), 400 (missing URL), 405 (not POST), 500 (install failed).
---
### 4. Update Check
```
POST /?mokowaas=update
```
Clears the Joomla update cache and triggers a fresh update check via `Updater::findUpdates()`.
**Response**:
| Field | Description |
|---|---|
| `status` | `ok` |
| `updates_found` | Number of available updates |
| `message` | Human-readable summary |
**HTTP Status**: 200 (success), 405 (not POST), 500 (failed).
---
### 5. Cache Clear
```
POST /?mokowaas=cache
```
Clears the Joomla site cache, admin cache, and PHP OPcache (if available).
**Response**:
| Field | Description |
|---|---|
| `status` | `ok` |
| `message` | `Cache cleared` |
**HTTP Status**: 200 (success), 405 (not POST), 500 (failed).
---
### 6. Backup (Akeeba)
```
POST /?mokowaas=backup
Content-Type: application/json
{"profile": 1}
```
Triggers an Akeeba Backup using the specified profile (defaults to profile 1). Requires Akeeba Backup to be installed.
**Response**:
| Field | Description |
|---|---|
| `status` | `started` |
| `profile` | Backup profile ID used |
| `message` | `Backup started` |
**HTTP Status**: 200 (started), 404 (Akeeba not installed), 405 (not POST), 500 (failed), 501 (Akeeba Engine not loadable).
## Error Responses
All endpoints return errors in a consistent format:
```json
{
"error": "Error description",
"message": "Additional detail (optional)"
}
```
### Common Error Codes
| HTTP Status | Meaning |
|---|---|
| 400 | Bad request (unknown action, missing parameters) |
| 401 | Invalid or missing authentication token |
| 405 | Wrong HTTP method (e.g. GET when POST is required) |
| 500 | Server error during operation |
| 503 | No API token configured |
## Unknown Actions
Requesting an unknown action returns HTTP 400 with the list of available actions:
```json
{
"error": "Unknown action",
"action": "invalid",
"available": ["health", "install", "update", "cache", "backup", "info"]
}
```
## Joomla REST API Routes
In addition to the query-string endpoints above, MokoWaaS registers standard Joomla API routes via the `plg_webservices_mokowaas` plugin:
| Route | Controller |
|---|---|
| `GET /api/v1/mokowaas/health` | HealthController |
| `POST /api/v1/mokowaas/cache` | CacheController |
| `POST /api/v1/mokowaas/update` | UpdateController |
These routes use Joomla's standard API authentication (API token in `X-Joomla-Token` header) and are useful for integrations that already use the Joomla API framework.
+94
View File
@@ -0,0 +1,94 @@
# Configuration
All MokoWaaS settings are managed in the Joomla plugin configuration under **System > Plugins > System - MokoWaaS**. Settings are organized into tabs (fieldsets).
## Basic (Branding)
| Parameter | Type | Default | Description |
|---|---|---|---|
| `enable_branding` | Yes/No | Yes | Enable white-label branding (language overrides, logos, colors) |
| `brand_name` | Text | `MokoWaaS` | Brand name displayed throughout the admin interface |
| `company_name` | Text | `Moko Consulting` | Company name used in footers and copyright notices |
| `support_url` | URL | `https://mokoconsulting.tech` | Support link shown on the admin login page and dashboard |
## WaaS Access
Controls the master user system that designates a single operator account.
| Parameter | Type | Default | Description |
|---|---|---|---|
| `enforce_master_user` | Yes/No | Yes | Enable master user enforcement; non-master Super Admins are restricted |
| `master_username` | Text | `mokoconsulting` | Username of the designated master operator |
| `master_email` | Email | `webmaster@mokoconsulting.tech` | Email address of the master user (for verification) |
| `emergency_access` | Yes/No | Yes | Enable emergency access via database password + file-based 2FA |
| `allowed_ips_display` | Display | -- | Read-only display of whitelisted IP addresses for emergency access |
## Maintenance
| Parameter | Type | Default | Description |
|---|---|---|---|
| `dev_mode` | Yes/No | No | Disable Joomla caching at runtime (does not modify `configuration.php`) |
| `reset_hits` | Yes/No | No | Reset article hit counters on next admin load |
| `delete_versions` | Yes/No | No | Purge content version history on next admin load |
## Visual Branding
| Parameter | Type | Default | Description |
|---|---|---|---|
| `color_primary` | Color | `#1a2744` | Primary brand color (buttons, accents) |
| `color_sidebar` | Color | `#0f1b2d` | Admin sidebar background color |
| `color_header` | Color | `#1a2744` | Admin header bar color |
| `color_link` | Color | `#0051ad` | Link text color |
| `brand_icon` | Text | -- | FontAwesome unicode code point (e.g. `f6d5`) for the brand icon |
| `custom_css` | Textarea | -- | Custom CSS injected into every admin page |
## Tenant Restrictions
Controls what non-master Super Admin users can access.
| Parameter | Type | Default | Description |
|---|---|---|---|
| `restrict_installer` | Yes/No | Yes | Block access to Extension Manager for non-master users |
| `hide_sysinfo` | Yes/No | Yes | Hide System Information page from non-master users |
| `restrict_global_config` | Yes/No | Yes | Block access to Global Configuration for non-master users |
| `restrict_template_editing` | Yes/No | Yes | Prevent non-master users from editing template files |
| `disable_install_url` | Yes/No | Yes | Remove the "Install from URL" tab in Extension Manager |
| `hidden_menu_items` | Textarea | -- | Comma-separated list of admin menu item IDs to hide from non-master users |
## Site Aliases
Multi-domain support. See [Site Aliases](Site-Aliases) for full documentation.
| Parameter | Type | Default | Description |
|---|---|---|---|
| `primary_domain` | Text | -- | The canonical domain for the site (e.g. `waas.dev.mokoconsulting.tech`) |
| `site_aliases` | Subform | -- | Repeatable table of alias domains with per-alias settings |
Each alias entry contains:
| Field | Type | Default | Description |
|---|---|---|---|
| `domain` | Text | -- | Alias domain name (e.g. `www.example.com`) |
| `offline` | Yes/No | No | Show offline page for this alias |
| `offline_message` | Textarea | -- | Custom offline message (shown when `offline` is Yes) |
| `robots` | List | `index, follow` | Robots meta directive for this alias |
| `redirect_backend` | Yes/No | Yes | Redirect admin requests on this alias to the primary domain |
## Diagnostics
| Parameter | Type | Default | Description |
|---|---|---|---|
| `health_api_token` | Text (read-only) | -- | Auto-generated Bearer token for API authentication. Provisioned on install/update. Cannot be manually edited. |
## Security
| Parameter | Type | Default | Description |
|---|---|---|---|
| `force_https` | Yes/No | Yes | Redirect all HTTP requests to HTTPS (301 redirect) |
| `admin_session_timeout` | Number | `60` | Idle timeout in minutes for admin sessions (0 = use Joomla default). Master user is exempt. |
| `password_min_length` | Number | `12` | Minimum password length for user accounts |
| `password_require_uppercase` | Yes/No | Yes | Require at least one uppercase letter |
| `password_require_number` | Yes/No | Yes | Require at least one digit |
| `password_require_special` | Yes/No | Yes | Require at least one special character |
| `upload_allowed_types` | Text | `jpg,jpeg,png,gif,webp,svg,pdf,doc,docx,xls,xlsx` | Comma-separated list of allowed upload file extensions |
| `upload_max_size_mb` | Number | `100` | Maximum upload file size in megabytes |
+127
View File
@@ -0,0 +1,127 @@
# Grafana Integration
MokoWaaS integrates with a Grafana monitoring stack hosted at `bench.mokoconsulting.tech`. The integration is automatic: on install or update, the plugin sends a heartbeat that provisions a Grafana datasource for the site.
## Architecture
```
MokoWaaS Plugin (Joomla)
|
| POST /api/waas-heartbeat/register
v
Heartbeat Receiver (bench.mokoconsulting.tech)
|
|-- Writes Grafana Infinity datasource YAML
|-- Restarts Grafana to pick up new datasource
|-- Sends ntfy notification (mokowaas-heartbeat topic)
v
Grafana Dashboard
|
| GET /?mokowaas=health (per site, on schedule)
v
Health JSON from each registered site
```
## Heartbeat Registration
### When It Fires
The heartbeat is sent automatically during:
- Plugin installation (`postflight` with type `install`)
- Plugin update (`postflight` with type `update`)
- Package installation (via `Pkg_MokowaasInstallerScript::sendHeartbeat()`)
### Payload
The plugin sends a POST request to `https://bench.mokoconsulting.tech/api/waas-heartbeat/register` with:
```json
{
"site_url": "https://example.com",
"site_name": "Example Site",
"health_token": "<health_api_token>",
"action": "register"
}
```
Authentication uses a shared secret sent in the `X-MokoWaaS-Key` header.
### What the Receiver Does
On receiving a registration request, the heartbeat receiver:
1. Validates the `X-MokoWaaS-Key` header
2. Generates a unique datasource UID from the site URL
3. Writes a Grafana Infinity datasource YAML file to the Grafana provisioning directory
4. Restarts Grafana to load the new datasource
5. Sends an ntfy notification to the `mokowaas-heartbeat` topic with registration details
The datasource YAML configures a Grafana Infinity datasource that polls `/?mokowaas=health` on the registered site using the provided Bearer token.
### Response
On success (HTTP 200):
```json
{
"status": "ok",
"ds_uid": "mokowaas-example-com"
}
```
The `ds_uid` is logged in the Joomla admin message queue for reference.
## Grafana Dashboard
The MokoWaaS Grafana dashboard is organized into 9 rows covering all health metrics:
| Row | Panels |
|---|---|
| 1. Overview | Overall status, uptime, plugin version, Joomla version |
| 2. Database | Connectivity, latency, driver, user count |
| 3. Filesystem | Disk space, writable directories, site size |
| 4. Extensions | Extension counts by type, pending updates |
| 5. Backup | Last backup status, age, Akeeba health |
| 6. Security | Admin Tools WAF, SSL certificate, blocked requests |
| 7. Content | Article counts, categories, user activity |
| 8. Infrastructure | Cache status, mail config, scheduled tasks, error log |
| 9. Configuration | SEO settings, template info, config drift |
Each row contains panels that query the site's Infinity datasource using JSONPath expressions to extract values from the health check response.
## ntfy Notifications
Registration events trigger a notification to the `mokowaas-heartbeat` ntfy topic. Notifications include:
- Site URL
- Site name
- Registration action (new or update)
- Datasource UID
Subscribe to notifications at `https://ntfy.sh/mokowaas-heartbeat` or use the ntfy app.
## Troubleshooting
### Heartbeat failed: connection error
The receiver at `bench.mokoconsulting.tech` may be unreachable. Check:
- DNS resolution for `bench.mokoconsulting.tech`
- Outbound HTTPS connectivity from the Joomla server
- Firewall rules allowing outbound port 443
Heartbeat failures are logged as warnings in Joomla's log and displayed in the admin message queue. They do not block plugin installation.
### Datasource not appearing in Grafana
- Verify the heartbeat completed successfully (check Joomla admin messages after install)
- Check the Grafana provisioning directory on `bench.mokoconsulting.tech`
- Ensure Grafana was restarted after provisioning
- Verify the health endpoint is accessible from the Grafana server
### Health data not loading in dashboard
- Confirm the `health_api_token` matches between the plugin configuration and the Grafana datasource
- Test the health endpoint directly: `curl -sk -H "Authorization: Bearer <token>" "https://example.com/?mokowaas=health"`
- Check for SSL certificate issues between the Grafana server and the monitored site
+33
View File
@@ -0,0 +1,33 @@
# Health Endpoint
## Stable Release: 02.01.37
16 diagnostic checks via /?mokowaas=health (token-authenticated, HTTPS-only).
### Checks
Core: database, filesystem, cache, extensions
Security: backup (Akeeba), security (Admin Tools), SSL certificate
Operations: scheduled tasks, error log, database size, mail
Content: articles, categories, users, sessions, failed logins
Config: SEO, templates, debug mode, force SSL, caching
### Grafana Dashboard (9 rows)
Site Overview | Health Metrics | Infrastructure | Backup | Security | SSL/Cron | Content/Users | Mail/SEO/Config | DB/Errors
### Heartbeat
Auto-registers with Grafana via bench.mokoconsulting.tech/api/waas-heartbeat/register
ntfy notifications on mokowaas-heartbeat topic
### Plugin Protection
Hidden from non-master users, settings blocked, self-healing lock, uninstall blocked.
---
| Field | Value |
|---|---|
| Minimum Version | 02.01.37 |
| Platform | joomla |
+267
View File
@@ -0,0 +1,267 @@
# Health Monitoring
MokoWaaS includes a built-in health monitoring system that runs 16 diagnostic checks against the Joomla site. Results are returned as a JSON payload via the `/?mokowaas=health` endpoint.
## Endpoint
```
GET https://example.com/?mokowaas=health
Authorization: Bearer <health_api_token>
```
The `health_api_token` is auto-generated during plugin installation and stored as a read-only plugin parameter. See [API Endpoints](API-Endpoints) for authentication details.
## Response Structure
The response includes an overall status, a human-readable reason string, a UTC timestamp, individual check results, and instance metadata.
| Field | Description |
|---|---|
| `status` | Overall status: `ok`, `degraded`, or `error` |
| `reason` | Human-readable summary of issues (null when status is `ok`) |
| `timestamp` | ISO 8601 UTC timestamp |
| `checks` | Object containing all 16 check results |
| `meta` | Instance metadata (brand, versions, server name) |
### Status Determination
- If any check returns `error`, the overall status is `error` and the HTTP status code is **503**.
- If any check returns `degraded` (and none are `error`), the overall status is `degraded` with HTTP **200**.
- Otherwise the overall status is `ok` with HTTP **200**.
## The 16 Checks
### 1. database
Tests database connectivity and query latency.
| Field | Description |
|---|---|
| `status` | `ok` or `error` |
| `latency_ms` | Query round-trip time in milliseconds |
| `driver` | Database driver name (e.g. `mysqli`, `pdomysql`) |
| `users` | Total user count (sanity check) |
### 2. filesystem
Checks writable directories and disk space.
| Field | Description |
|---|---|
| `status` | `ok`, `degraded` (low disk), or `error` (not writable) |
| `tmp_writable` | Whether `/tmp` is writable |
| `log_writable` | Whether `/administrator/logs` is writable |
| `cache_writable` | Whether `/cache` is writable |
| `free_disk_mb` | Free disk space in MB |
| `total_disk_mb` | Total disk space in MB |
| `site_size_mb` | Estimated site size in MB (images, media, tmp, cache, logs) |
Degraded when free disk is below 100 MB. Error when required directories are not writable.
### 3. cache
Reports Joomla cache configuration.
| Field | Description |
|---|---|
| `status` | Always `ok` |
| `enabled` | Whether Joomla caching is active |
| `handler` | Cache handler type (e.g. `file`, `redis`) |
### 4. extensions
Counts enabled extensions by type and checks for pending updates.
| Field | Description |
|---|---|
| `status` | `ok` or `degraded` (pending updates) |
| `by_type` | Object with counts per extension type |
| `pending_updates` | Number of available extension updates |
### 5. backup (Akeeba)
Checks Akeeba Backup status.
| Field | Description |
|---|---|
| `status` | `ok`, `degraded`, or `error` |
| `last_status` | Status of the last backup record (`complete`, `fail`, etc.) |
| `days_since` | Days since the last backup |
| `message` | Human-readable backup status |
Degraded when the last backup is older than 7 days or did not complete successfully.
### 6. security (Admin Tools)
Checks Admin Tools WAF status if installed.
| Field | Description |
|---|---|
| `status` | `ok`, `degraded`, or `error` |
| `waf_enabled` | Whether the Web Application Firewall is active |
| `blocked_24h` | Number of blocked requests in the last 24 hours |
### 7. ssl
Checks SSL certificate validity and expiration.
| Field | Description |
|---|---|
| `status` | `ok`, `degraded`, or `error` |
| `days_left` | Days until certificate expiration |
| `issuer` | Certificate issuer |
| `valid_from` | Certificate start date |
| `valid_to` | Certificate expiration date |
Degraded when the certificate expires within 30 days.
### 8. cron (Scheduled Tasks)
Checks Joomla scheduled task execution.
| Field | Description |
|---|---|
| `status` | `ok` or `degraded` |
| `total_tasks` | Total number of scheduled tasks |
| `failed_24h` | Tasks that failed in the last 24 hours |
### 9. errors (Error Log)
Analyzes recent Joomla error log entries.
| Field | Description |
|---|---|
| `status` | `ok` or `degraded` |
| `recent_errors` | Count of recent error log entries |
| `last_error` | Most recent error message |
### 10. db_size
Reports database size metrics.
| Field | Description |
|---|---|
| `status` | `ok` or `degraded` |
| `total_mb` | Total database size in MB |
| `tables` | Number of database tables |
### 11. content
Reports content statistics.
| Field | Description |
|---|---|
| `status` | Always `ok` |
| `articles` | Total article count |
| `categories` | Total category count |
| `published` | Number of published articles |
| `unpublished` | Number of unpublished articles |
### 12. users (User Activity)
Reports user statistics.
| Field | Description |
|---|---|
| `status` | Always `ok` |
| `total` | Total user count |
| `active_30d` | Users active in the last 30 days |
| `blocked` | Number of blocked user accounts |
### 13. mail
Checks Joomla mail configuration.
| Field | Description |
|---|---|
| `status` | `ok` or `degraded` |
| `mailer` | Mail handler type (e.g. `smtp`, `mail`, `sendmail`) |
| `from` | Configured sender address |
### 14. seo
Checks SEO configuration.
| Field | Description |
|---|---|
| `status` | `ok` or `degraded` |
| `sef` | Whether SEF URLs are enabled |
| `sef_rewrite` | Whether URL rewriting is enabled |
| `sitemap` | Whether a sitemap is detected |
### 15. template
Reports active template information.
| Field | Description |
|---|---|
| `status` | Always `ok` |
| `site_template` | Active frontend template name |
| `admin_template` | Active admin template name |
### 16. config (Config Drift)
Detects configuration anomalies.
| Field | Description |
|---|---|
| `status` | `ok` or `degraded` |
| `issues` | Array of detected configuration problems |
Checks for issues such as debug mode enabled in production, error reporting set too high, or default database prefix still in use.
## Example Response
```json
{
"status": "degraded",
"reason": "2 extension updates available; SSL expires in 14 days",
"timestamp": "2026-05-24T12:00:00Z",
"checks": {
"database": {
"status": "ok",
"latency_ms": 1.23,
"driver": "pdomysql",
"users": 5
},
"filesystem": {
"status": "ok",
"tmp_writable": true,
"log_writable": true,
"cache_writable": true,
"free_disk_mb": 4500,
"total_disk_mb": 20000,
"site_size_mb": 320
},
"ssl": {
"status": "degraded",
"days_left": 14,
"issuer": "Let's Encrypt",
"valid_to": "2026-06-07"
}
},
"meta": {
"brand": "MokoWaaS",
"plugin_version": "02.03.11",
"joomla_version": "5.2.4",
"php_version": "8.2.20",
"server_name": "Example Site",
"server_time": "2026-05-24T12:00:00Z"
}
}
```
(Remaining checks omitted for brevity.)
## Metadata
The `meta` object is included in every health response:
| Field | Description |
|---|---|
| `brand` | Configured brand name |
| `plugin_version` | MokoWaaS plugin version |
| `joomla_version` | Joomla CMS version |
| `php_version` | PHP version |
| `server_name` | Joomla site name |
| `server_time` | Server UTC time |
+52
View File
@@ -0,0 +1,52 @@
# MokoWaaS
MokoWaaS is a Joomla 5.x / 6.x extension package that provides a configurable white-label identity layer, tenant management, health monitoring, and remote administration API for the MokoWaaS platform.
Developed by [Moko Consulting](https://mokoconsulting.tech). Licensed under GPL-3.0-or-later.
## Features
- **White-label branding** -- customizable brand name, colors, favicon, login page, and admin template theming
- **Master user enforcement** -- designate a single operator account with elevated privileges; restrict other Super Admins
- **Tenant restrictions** -- hide system info, block installer access, restrict global config and template editing, hide menu items
- **Site aliases** -- multi-domain support with per-alias offline mode, robots directives, and backend redirects
- **Health monitoring** -- 16 diagnostic checks exposed via authenticated JSON API
- **Remote management API** -- 6 endpoints for health, info, install, update, cache clear, and backup
- **Grafana integration** -- automatic heartbeat registration with Grafana Infinity datasource provisioning
- **Plugin protection** -- protected flag, self-healing, hidden from non-master users, blocks disable/uninstall
- **Security hardening** -- forced HTTPS, session timeouts, password policy, upload restrictions
- **Emergency access** -- file-based two-factor verification for master user recovery
- **Automatic updates** -- via Joomla update server hosted on Gitea
## Requirements
| Requirement | Minimum |
|---|---|
| Joomla | 5.0.0+ (5.x and 6.x supported) |
| PHP | 8.1.0+ |
## Package Contents
The `pkg_mokowaas` package installs three extensions:
| Extension | Type | Purpose |
|---|---|---|
| `plg_system_mokowaas` | System Plugin | Core branding, restrictions, health checks, API endpoints |
| `com_mokowaas` | Component | REST API controllers (health, cache, update) |
| `plg_webservices_mokowaas` | Webservices Plugin | Registers Joomla API routes for `v1/mokowaas/*` |
## Wiki Pages
- [Configuration](Configuration) -- All plugin settings organized by tab
- [Health Monitoring](Health-Monitoring) -- The 16 diagnostic checks
- [Site Aliases](Site-Aliases) -- Multi-domain management
- [API Endpoints](API-Endpoints) -- The 6 remote management endpoints
- [Grafana Integration](Grafana-Integration) -- Monitoring dashboard setup
- [Plugin Protection](Plugin-Protection) -- Security measures preventing disable/uninstall
- [Installation](Installation) -- Step-by-step install and upgrade guide
## Links
- **Repository**: [MokoWaaS on Gitea](https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS)
- **Update Server**: [updates.xml](https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/raw/branch/main/updates.xml)
- **Author**: [Moko Consulting](https://mokoconsulting.tech)
+100
View File
@@ -0,0 +1,100 @@
# Installation
MokoWaaS is distributed as a Joomla package (`pkg_mokowaas`) containing three extensions. It can be installed via the standard Joomla Extension Manager.
## Requirements
| Requirement | Minimum |
|---|---|
| Joomla | 5.0.0+ |
| PHP | 8.1.0+ |
The installer checks both requirements during `preflight` and aborts with an error message if either is not met.
## Method 1: Upload Package File
1. Download the latest `pkg_mokowaas.zip` from the [Gitea Releases](https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases) page.
2. In Joomla admin, navigate to **System > Install > Extensions**.
3. On the **Upload Package File** tab, drag and drop or browse to select `pkg_mokowaas.zip`.
4. Click **Upload & Install**.
5. Verify the success message. The package installs three extensions:
- `plg_system_mokowaas` (System Plugin)
- `com_mokowaas` (Component)
- `plg_webservices_mokowaas` (Webservices Plugin)
## Method 2: Install from URL
1. In Joomla admin, navigate to **System > Install > Extensions**.
2. On the **Install from URL** tab, enter the direct download URL for the package ZIP, e.g.:
```
https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/stable/pkg_mokowaas-latest.zip
```
3. Click **Install**.
4. Verify the success message.
## Post-Installation Verification
After installation, verify the following:
1. **Plugin enabled**: Navigate to **System > Plugins** and confirm "System - MokoWaaS" is enabled (it is auto-enabled during installation).
2. **Health token generated**: Open the plugin configuration and check the **Diagnostics** tab. The `health_api_token` field should contain an auto-generated token.
3. **Branding applied**: The admin login page and dashboard should reflect MokoWaaS branding (logo, colors, footer text).
4. **Health endpoint**: Test the health endpoint:
```
curl -sk -H "Authorization: Bearer <token>" "https://yoursite.com/?mokowaas=health"
```
5. **Grafana heartbeat**: Check the admin message queue for a Grafana heartbeat confirmation message.
## What the Installer Does
During `postflight`, the installer script performs these operations:
| Step | Description |
|---|---|
| Enable and protect plugin | Sets `enabled=1`, `protected=1`, `locked=0` in `#__extensions` |
| Install MokoOnyx template | Installs the bundled MokoOnyx site template from the plugin payload, sets it as default |
| Language overrides | Deploys language override files for en-GB and en-US |
| Login support URLs | Updates `mod_loginsupport` to point to Moko Consulting support/docs/news URLs |
| Atum branding | Applies brand colors to the Atum admin template |
| Action log registration | Registers MokoWaaS in the Joomla action log system |
| Health token provisioning | Generates a random API token if one does not exist |
| Heartbeat | Sends a registration heartbeat to the Grafana monitoring receiver |
## Automatic Updates
MokoWaaS includes an update server configuration that enables automatic update notifications through Joomla's built-in update system.
The update server URL is:
```
https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/raw/branch/main/updates.xml
```
When a new version is available:
1. Joomla's update checker detects the new version from `updates.xml`.
2. A notification appears in the admin dashboard.
3. Navigate to **System > Update > Extensions** to install the update.
4. The `postflight` script runs again, re-applying all configuration steps.
## Upgrading from Standalone Plugin to Package
If you previously installed the standalone `plg_system_mokowaas` plugin (before it was packaged as `pkg_mokowaas`), the package installer handles migration automatically:
1. Install the `pkg_mokowaas.zip` package using either method above.
2. The package installer detects the existing standalone plugin and upgrades it in place.
3. The additional extensions (`com_mokowaas`, `plg_webservices_mokowaas`) are installed alongside.
4. All existing plugin settings are preserved.
5. The `protected` flag is set on all package extensions.
No manual cleanup of the old standalone plugin is required.
## Uninstallation
Uninstallation is restricted to the master user. See [Plugin Protection](Plugin-Protection) for details.
When the master user uninstalls via the Extension Manager:
1. Language override files are removed from Joomla's global override directories.
2. Action log registration is cleaned up.
3. An uninstall notification is sent to the monitoring system.
4. The package and all sub-extensions are removed.
+89
View File
@@ -0,0 +1,89 @@
# Plugin Protection
MokoWaaS uses multiple layers of protection to prevent accidental or unauthorized disabling and uninstallation. The master user retains full control over the plugin at all times.
## Protection Layers
### 1. Protected Flag (`protected=1`)
During installation and on every admin session, MokoWaaS sets the `protected` column to `1` in the `#__extensions` database table for both `mokowaas` and `pkg_mokowaas` entries.
The Joomla framework itself enforces this flag: protected extensions cannot be disabled or uninstalled through the standard admin interface.
The `locked` column is set to `0` so the extension can still receive updates and configuration changes.
### 2. Self-Healing
The `ensureProtectedFlag()` method runs once per admin session (using a static flag to avoid repeated queries). If the `protected` column has been reset to `0` (e.g., by a database modification), it is automatically restored to `1`.
This runs in the `protectPlugin()` method, which is called from `onBeforeRender()` on every admin page load.
### 3. Hidden from Plugin List
For non-master users, MokoWaaS injects JavaScript on the `com_plugins` and `com_installer` pages that hides any table row containing "mokowaas" or "MokoWaaS". This prevents non-master users from seeing the plugin in the extension list.
The `hidePluginFromList()` method adds an inline script that runs on `DOMContentLoaded`:
```javascript
document.querySelectorAll('tr').forEach(function(row) {
var text = row.textContent || '';
if (text.indexOf('mokowaas') !== -1 || text.indexOf('MokoWaaS') !== -1) {
row.style.display = 'none';
}
});
```
### 4. onExtensionBeforeSave Interception
The `onExtensionBeforeSave` event handler intercepts save attempts on the plugin configuration. If a non-master user attempts to save the plugin with `enabled = 0`, the handler:
1. Displays an error message: "MokoWaaS cannot be disabled."
2. Forces `enabled` back to `1` on the table object
3. Returns `true` to allow the save to proceed (with the corrected value)
### 5. protectPlugin() -- Uninstall and Disable Blocking
The `protectPlugin()` method runs on every admin page request and checks for active uninstall or disable attempts:
**Uninstall blocking**: If the current request is to `com_installer` with task `manage.remove`, and the extension IDs include any MokoWaaS extension, the request is blocked with an error message and a redirect back to the installer manage view.
**Disable blocking**: If the current request is to `com_plugins` with task `plugins.publish`, and the extension IDs include MokoWaaS, the request is blocked with an error message and a redirect back to the plugins list.
The `isOurExtension()` helper method checks extension IDs against the database to determine if they belong to MokoWaaS (matching on element name `mokowaas` or `pkg_mokowaas`).
## Master User Exemption
All protection checks call `isMasterUser()` first. If the current user is the designated master user (matching the configured `master_username`), all protections are bypassed. The master user can:
- See MokoWaaS in the plugin and extension lists
- Disable the plugin via the configuration page
- Uninstall the plugin via the Extension Manager
- Modify all plugin settings
## Installation-Time Protection
The package installer script (`Pkg_MokowaasInstallerScript`) and the plugin installer script both set `protected=1` during `postflight`:
- `enableAndLockPlugin()` sets `enabled=1`, `locked=1`, `protected=1` on the system plugin
- `protectExtensions()` sets `protected=1`, `locked=0` on all MokoWaaS extensions (plugin and package)
This ensures protection is active immediately after installation, before the first admin page load triggers the self-healing logic.
## Summary of Protection Flow
```
Installation
-> postflight sets protected=1, enabled=1
Every admin page load (onBeforeRender)
-> protectPlugin()
-> ensureProtectedFlag() (once per session, restores protected=1 if needed)
-> if not master user:
-> block uninstall attempts (com_installer manage.remove)
-> block disable attempts (com_plugins plugins.publish)
-> hidePluginFromList() for non-master users
Plugin config save (onExtensionBeforeSave)
-> if not master user and enabled=0:
-> force enabled=1, show error
```
+69
View File
@@ -0,0 +1,69 @@
# Site Aliases
MokoWaaS supports multi-domain configurations through the Site Aliases system. This allows a single Joomla installation to respond to multiple domain names, each with independent settings for offline mode, robots directives, and backend access.
## Configuration
Site aliases are configured in the plugin settings under the **Site Aliases** tab.
### Primary Domain
Set `primary_domain` to the canonical domain for the site (e.g. `waas.dev.mokoconsulting.tech`). This is the domain that:
- Serves as the canonical URL for SEO purposes
- Hosts the admin backend (when `redirect_backend` is enabled on aliases)
- Is used in heartbeat registration with Grafana
### Alias Entries
Add alias domains using the repeatable subform table. Each alias entry has the following options:
| Field | Type | Default | Description |
|---|---|---|---|
| `domain` | Text | (required) | The alias domain name, e.g. `www.example.com` |
| `offline` | Yes/No | No | When Yes, visitors to this alias see the offline page |
| `offline_message` | Textarea | -- | Custom message displayed when the alias is offline (only shown when `offline` is Yes) |
| `robots` | List | `index, follow` | Robots meta tag directive for this alias |
| `redirect_backend` | Yes/No | Yes | When Yes, admin URLs (`/administrator`) on this alias redirect to the primary domain |
### Robots Options
Each alias can have its own robots directive:
| Value | Effect |
|---|---|
| `index, follow` | Normal indexing (default) |
| `noindex, follow` | Do not index pages, but follow links |
| `index, nofollow` | Index pages, do not follow links |
| `noindex, nofollow` | Do not index or follow |
| `none` | Equivalent to `noindex, nofollow` |
## How Canonical URLs Work
When a request arrives on an alias domain, MokoWaaS:
1. Matches the `HTTP_HOST` against configured alias domains
2. Applies the alias-specific robots meta tag
3. If the alias is marked offline, renders the offline page with the custom message
4. If `redirect_backend` is enabled and the request is for `/administrator`, issues a 301 redirect to the primary domain's admin
5. Sets the canonical URL to the primary domain equivalent of the current page
This prevents duplicate content issues when the same site is accessible from multiple domains.
## Grafana Monitoring for Aliases
When the plugin sends a heartbeat to the Grafana monitoring receiver, it registers both the primary domain and all alias domains. The monitoring dashboard can then track health status for each domain independently.
Each alias appears as a separate entry in the Grafana Infinity datasource, pointing to the same health endpoint but accessed via the alias domain. This ensures SSL certificate checks and DNS resolution are validated per-domain.
## DreamHost Mirror Setup
For sites hosted on DreamHost, alias domains are typically configured as "mirror" domains in the DreamHost panel:
1. In DreamHost panel, add the alias domain as a **Mirror** of the primary domain
2. Ensure DNS for the alias domain points to the DreamHost server
3. Add the alias domain to the MokoWaaS Site Aliases configuration
4. Set `redirect_backend` to Yes (recommended) so admin access always uses the primary domain
5. Set `robots` to `noindex, nofollow` if the alias is a staging or preview domain
DreamHost mirrors serve the same filesystem, so no additional Joomla configuration is needed beyond the MokoWaaS alias entry.