669 Commits

Author SHA1 Message Date
jmiller e7f03b1bf5 Merge pull request 'chore: 02.31.00 release' (#109) from dev into main 2026-06-01 02:17:45 +00:00
Jonathan Miller 6546592b10 chore: update CHANGELOG for 02.31.00 release
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 21:17:08 -05:00
Jonathan Miller 93849ab7dd fix: skip duplicate inserts, add metadata to categories
Wrap all syncpush inserts in try/catch to skip duplicates instead
of failing the entire batch. Add metadata column to categories.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 21:15:28 -05:00
Jonathan Miller 6fab06cc32 fix: quote fulltext column name, add path to menu sync
fulltext is a MySQL reserved word — must be backtick-quoted.
Menu items require path column for Joomla routing.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 21:11:27 -05:00
Jonathan Miller 8ab81a0085 fix: remove undefined $apiBase/$apiToken — use instance properties
Sync methods now use $this->targetUrl and $this->healthToken instead
of function parameters that no longer exist after the bulk push refactor.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 21:09:24 -05:00
Jonathan Miller cdfe868fdc fix: add metadata field to syncpush article insert
Joomla's #__content table requires metadata column. Also send
metadata from source query.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 21:06:07 -05:00
Jonathan Miller 73ab21bfb0 fix: skip global token check for syncclear/syncpush — they auth via POST body
The global API token check reads from query string/header, but
syncclear and syncpush send the token in the JSON POST body.
Skip the global check for these actions.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 21:02:11 -05:00
Jonathan Miller cb264fac6a fix: healthToken variable used before assignment
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 15:00:30 -05:00
Jonathan Miller 5b2a59972d fix: remove api_token check — only target_url and health_token needed
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 14:58:42 -05:00
Jonathan Miller db419e9afa fix: remove api_token field — sync uses health_token only
All sync communication (syncclear, syncpush) authenticates via the
target's MokoWaaS health token. Joomla API token is not needed.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 14:57:23 -05:00
Jonathan Miller c13450a4ab fix: use target health token from task params, not source
The syncclear/syncpush endpoints authenticate with the target site's
health token. Added health_token field to task config. Removed
api_user field (unused). Removed getHealthToken (read from source).

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 14:53:29 -05:00
Jonathan Miller 97dadce289 feat: rebuild asset table and nested sets after syncpush
After bulk-inserting content, the target site now:
- Rebuilds category and menu nested set trees (lft/rgt/level)
- Creates #__assets entries for articles and categories (ACL)

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 14:51:27 -05:00
Jonathan Miller 2cb28e0286 feat: bulk syncpush endpoint — send all content in batched payloads
Instead of individual Joomla API POST calls per article (84 calls),
the sync task now sends items in batches of 50 to the target's
/?mokowaas=syncpush endpoint which inserts directly via DB.

Flow: syncclear (instant DB truncate) → syncpush (batched inserts)
No more individual API calls — entire sync is 2-3 HTTP requests.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 14:50:28 -05:00
Jonathan Miller 22490abd61 feat: bulk syncclear endpoint + fast sync via MokoWaaS API
Added /?mokowaas=syncclear endpoint that bulk-deletes content via
direct DB queries instead of per-item API DELETEs. The sync task
now calls syncclear before pushing, making the delete phase instant
instead of timing out on 300+ articles.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 14:45:16 -05:00
Jonathan Miller dbf1d79a50 fix: always fetch page 0 when deleting — pagination shift bug
After deleting items, remaining items shift down. Incrementing the
page offset skipped items. Now always fetches page 0 until empty.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 14:18:22 -05:00
Jonathan Miller b89ea25b20 refactor: remove all file sync — API-based DB content only
Removed images/, files/, media/ sync and related infrastructure
(syncDirectory, getHealthToken). Sync is now purely Joomla REST API:
articles, categories, menus, modules.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 14:13:11 -05:00
Jonathan Miller dc1508520f fix: remove files sync option
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 14:11:32 -05:00
Jonathan Miller 00f9bed6a1 fix: paginate article/menu deletion, increase timeouts, remove media sync
- Delete articles/menus in pages of 20 instead of page[limit]=0
- Increase curl timeout to 60s
- Remove /media/ sync option (too large, contains extension assets)

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 14:10:56 -05:00
Jonathan Miller c635a5ac98 fix: wrap sync task fields in <fields name="params"> + fieldset task_params
Joomla's scheduler requires task form fields inside
<fields name="params"><fieldset name="task_params"> to persist params.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 14:01:15 -05:00
Jonathan Miller 280c52fa05 fix: remove required from sync fields — blocks save on existing tasks
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 13:54:33 -05:00
Jonathan Miller f91522e235 fix: change target_url to text type, add required back
The url field type was causing validation issues preventing save.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 13:52:47 -05:00
Jonathan Miller c5e5014405 fix: add fieldset label to sync task form
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 13:40:27 -05:00
Jonathan Miller ca9cb764ed fix: merge sync task fields into single fieldset
Joomla's TaskPluginTrait only saves params from the fieldset matching
the form name. Multiple fieldsets caused fields to not persist.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 13:39:42 -05:00
Jonathan Miller 300b9aee45 fix: remove required attribute from sync task fields
Existing tasks without target_url/api_token filled in could not be
saved. The task handles empty values at runtime instead.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 13:38:41 -05:00
Jonathan Miller ede0711dc6 feat: hardcode dev alias, remove site_aliases config, auto-set primary domain
- Site alias is now hardcoded to dev.{primary_domain}
- dev.* subdomain bypasses offline mode for development access
- dev.* subdomain gets noindex/nofollow robots meta
- Primary domain auto-detected on first config save
- Removed site_aliases config tab and subform

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 13:10:01 -05:00
Jonathan Miller 6d28d83f86 feat: enhanced dev mode — debug, offline, hit suppression, cleanup on off
Dev mode ON:
- Disable caching, enable Joomla debug + MokoOnyx template debug
- Show offline page on primary domain (site aliases bypass for dev work)
- Suppress article hit recording

Dev mode OFF (toggled via config save):
- Clear content version history
- Reset all article hits
- Disable template debug
- Take site back online

Also: merged diagnostics + maintenance into default config tab.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 13:01:21 -05:00
Jonathan Miller d483352939 fix: remove legacy jmiller auto-cleanup from enforceMasterUser
Only enforce users in MASTER_KEYS — don't auto-delete other users.
Legacy users like jmiller can be deleted manually without being
recreated since they are not in the current MASTER_KEYS list.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 12:49:16 -05:00
Jonathan Miller 33a3184dfc chore: bump version 02.30.00 → 02.31.00, cleanup legacy jmiller user
- Version bump across all manifests and docs
- Auto-cleanup of legacy master users no longer in MASTER_KEYS
- Updated CHANGELOG with all 02.31.00 changes

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 12:40:05 -05:00
Jonathan Miller cbf656ff57 feat: hardcode branding, master user, support URL, colors
- Always enforce master user (remove toggle)
- Hardcode master email, support URL, brand name, company name
- Hardcode admin color scheme (primary, sidebar, header, link)
- Always enforce branding (remove enable_branding toggle)
- Remove basic, visual branding, and waas_access config tabs
- Move diagnostics to first tab
- Move emergency access to security tab
- Remove content sync tab (now in scheduled task plugin)

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 12:21:21 -05:00
Jonathan Miller 8a3897664e feat: rewrite content sync to use Joomla REST API with per-task config
Each sync task instance now has its own target URL, API token, and
content type toggles. Sync strategy is delete-then-push via the
Joomla API for articles and menus (avoids duplicates, respects ACL).

Content types: articles, categories, menus, modules
File types: images/, files/, media/ (via MokoWaaS sync-receive endpoint)

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 12:01:15 -05:00
jmiller 7b5148d089 Merge pull request 'chore: 02.30.00 version bump' (#108) from dev into main 2026-05-31 16:25:27 +00:00
Jonathan Miller e8d494d590 feat: license key warning + heartbeat validation + stale update site cleanup
- Persistent admin warning when no download key is set on the MokoWaaS
  update site, with link to System → Update Sites
- Daily heartbeat validates the key against MokoGitea's dynamic endpoint;
  shows error if key is invalid or expired
- Package postflight removes stale/duplicate update site entries and
  orphaned #__updates rows

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 11:23:54 -05:00
Jonathan Miller 47bfdb9206 chore: bump version 02.29.04 → 02.30.00
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 11:18:12 -05:00
jmiller ceb6b8de3d Merge pull request 'feat: integrate MokoGitea license system for update checks' (#107) from dev into main 2026-05-31 15:42:59 +00:00
Jonathan Miller 1b6747d9f9 feat: integrate MokoGitea license system for update checks
Update server now points to MokoGitea's dynamic endpoint which
validates license keys (dlid) and generates update XML from git
releases. Users enter the download key via Joomla's native
System → Update Sites interface. Legacy static URLs are auto-migrated
on install/update.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 10:36:53 -05:00
jmiller 505d9dbdcd Merge pull request 'fix: 02.29.04 - remove secondary master user enforcement' (#106) from dev into main
Merge pull request #106: fix: 02.29.04 - remove secondary master user enforcement
2026-05-31 14:06:30 +00:00
Jonathan Miller 7ba7337054 fix: 02.29.04 - remove secondary master user enforcement
Only enforce the primary master username; plugin no longer creates
or protects the secondary user account.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 09:04:08 -05:00
Jonathan Miller 1ae2ea1c6e chore(release): build 02.29.03 [skip ci]
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 08:47:55 -05:00
jmiller 28deace0b5 Merge pull request 'chore: 02.29.03 version bump' (#105) from dev into main 2026-05-31 13:46:51 +00:00
Jonathan Miller f6aa0e590b chore: bump version 02.29.02 → 02.29.03
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 08:46:27 -05:00
Jonathan Miller 8b09d17576 chore(release): build 02.29.02 [skip ci]
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 08:43:33 -05:00
jmiller a2da6fb7b1 Merge pull request 'fix: 02.29.02 - obfuscate master usernames' (#104) from dev into main 2026-05-31 13:41:27 +00:00
Jonathan Miller a574032570 fix: obfuscate master usernames in source code (XOR + base64)
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 08:41:13 -05:00
Jonathan Miller fc4e99306e chore: update channels for 02.29.01 [skip ci]
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 08:36:57 -05:00
jmiller 828eabbb80 Merge pull request 'chore: sanitize changelog 02.29.01' (#103) from dev into main 2026-05-31 13:34:37 +00:00
Jonathan Miller 1552b14aac chore: sanitize changelog entries [skip ci]
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 08:34:23 -05:00
Jonathan Miller 34f95f186b chore(release): build 02.29.01 [skip ci]
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 08:27:41 -05:00
jmiller a24dda6b73 Merge pull request 'fix: 02.29.01 - hardcode master users, fix IP whitelist' (#102) from dev into main
fix: 02.29.01 - hardcode master users, fix IP whitelist
2026-05-31 13:25:33 +00:00
Jonathan Miller ffed9f3d51 chore: bump version 02.29.00 → 02.29.01
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 08:25:09 -05:00
Jonathan Miller a12ecf96d9 fix: hardcode master usernames, fix IP whitelist blocking all access
- Hardcode mokoconsulting and jmiller as master usernames (no longer
  configurable via params)
- Fix isIpAllowed() reading from global config instead of plugin params
- Fix empty allowed_ips returning false (now allows all IPs)
- Both master users are auto-created and enforced as Super Admins

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 08:21:18 -05:00
Jonathan Miller 33599e99ce chore(release): build 02.29.00 [skip ci]
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 07:52:24 -05:00
jmiller 7700d6f08b Merge pull request 'release: 02.29.00 - separate extension update rights' (#101) from dev into main
release: 02.29.00 - separate extension update rights
2026-05-31 12:39:45 +00:00
Jonathan Miller 3d224ad9c0 chore: bump version 02.28.00 → 02.29.00
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 07:38:40 -05:00
Jonathan Miller 053f503af5 feat: separate extension update rights from installer restrictions
Add `allow_extension_updates` param (default: Yes) so tenants can
update extensions even when the installer is restricted. The update
and updatesites views are now permitted independently of the install
and manage views.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 07:28:41 -05:00
Jonathan Miller 8f5a05bad3 chore: merge main into dev, resolve conflicts for 02.28.00
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 22:56:13 -05:00
Jonathan Miller c8fbbde155 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into feature/sync-task-plugin 2026-05-30 22:52:39 -05:00
gitea-actions[bot] b2b0bc9f94 chore(ci): remove update-server.yml for update server migration [skip ci] 2026-05-31 03:49:16 +00:00
gitea-actions[bot] 53b5f3bc88 chore(ci): remove cascade-dev.yml for update server migration [skip ci] 2026-05-31 03:49:13 +00:00
gitea-actions[bot] 4ec971ec9b chore(ci): remove auto-bump.yml for update server migration [skip ci] 2026-05-31 03:49:10 +00:00
gitea-actions[bot] 75799d8b2f chore(ci): remove pre-release.yml for update server migration [skip ci] 2026-05-31 03:49:07 +00:00
gitea-actions[bot] 27d4409213 chore(ci): remove auto-release.yml for update server migration [skip ci] 2026-05-31 03:49:04 +00:00
Jonathan Miller 639ac84c08 feat: add content sync task plugin, fix countdown, CB tables, remove workflows (02.28.00)
- Add plg_task_mokowaassync scheduled task plugin for automated content sync
- Fix demo banner countdown to show weeks/days/months for longer intervals
- Add Community Builder tables to DemoResetService safe reset list
- Remove all CI/CD workflow files (manual release process)
- Bump version to 02.28.00 across all manifests and updates.xml

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 22:45:54 -05:00
jmiller aa98456554 chore: sync updates.xml 02.27.00 from main [skip ci] 2026-05-31 03:13:40 +00:00
gitea-actions[bot] f352989b9f chore: update channels for 02.27.00 [skip ci] 2026-05-31 03:13:20 +00:00
jmiller 350ccc7ae7 chore: sync updates.xml 02.27.00 from main [skip ci] 2026-05-31 03:13:20 +00:00
gitea-actions[bot] f6f815d377 chore(release): build 02.27.00 [skip ci] 2026-05-31 03:13:18 +00:00
jmiller 1c870410ca Merge pull request 'feat: content-only reset, static banner, asset rebuild, defaults fix' (#99) from dev into main
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-31 03:13:10 +00:00
Jonathan Miller 75851a2cca Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
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) Failing after 3s
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 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 4s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 5s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 5s
Universal: PR Check / Validate PR (pull_request) Successful in 6s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 21s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
2026-05-30 22:10:33 -05:00
Jonathan Miller 231df79840 chore: merge main into dev [skip ci] 2026-05-30 22:10:28 -05:00
jmiller f5d2dc6786 chore: sync updates.xml from development [skip ci] 2026-05-31 02:53:57 +00:00
gitea-actions[bot] 65d46100af chore: update development channel 02.26.18-dev [skip ci] 2026-05-31 02:53:57 +00:00
gitea-actions[bot] a0b67dea1f chore(version): auto-bump 02.26.18-dev [skip ci] 2026-05-31 02:53:55 +00:00
Jonathan Miller 41aeeacbae Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
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) Failing after 3s
Update Server / Update Server (push) Successful in 10s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
2026-05-30 21:53:42 -05:00
Jonathan Miller a00f60327c fix: demo banner is static (not fixed), not dismissable, pushes body down
Banner is now a normal block element at the top of the body instead of
position:fixed. No close button, no sessionStorage dismiss. Content
flows below it naturally.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 21:53:19 -05:00
jmiller 99044f9896 chore: sync updates.xml from development [skip ci] 2026-05-31 02:52:10 +00:00
gitea-actions[bot] 9d2b888cb5 chore: update development channel 02.26.17-dev [skip ci] 2026-05-31 02:52:09 +00:00
gitea-actions[bot] e53b55fd48 chore(version): auto-bump 02.26.17-dev [skip ci] 2026-05-31 02:52:08 +00:00
Jonathan Miller 2e59cbaa3b Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
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 4s
Update Server / Update Server (push) Successful in 10s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
2026-05-30 21:51:51 -05:00
Jonathan Miller 3a47c3b453 fix: default show_countdown to 1 in banner injection
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 21:51:46 -05:00
jmiller e89176ee53 chore: sync updates.xml from development [skip ci] 2026-05-31 02:49:51 +00:00
gitea-actions[bot] 9203e860e1 chore: update development channel 02.26.16-dev [skip ci] 2026-05-31 02:49:50 +00:00
gitea-actions[bot] 51dd09be46 chore(version): auto-bump 02.26.16-dev [skip ci] 2026-05-31 02:49:47 +00:00
Jonathan Miller 5d6871dd2f Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
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) Failing after 5s
Update Server / Update Server (push) Successful in 17s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
2026-05-30 21:49:29 -05:00
Jonathan Miller 905f896f22 fix: default banner/media/countdown to On when task params not yet saved
Task params only contain custom fields after the task is edited and
saved in the Scheduler UI. Before that, they're missing — default to
the form defaults (all On) instead of Off.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 21:49:07 -05:00
jmiller cf48bdb0b4 chore: sync updates.xml 02.27.00 from main [skip ci] 2026-05-31 02:45:23 +00:00
jmiller a39cbc4167 chore: sync updates.xml 02.27.00 from main [skip ci] 2026-05-31 02:45:12 +00:00
gitea-actions[bot] 2e78ac822f chore: update channels for 02.27.00 [skip ci] 2026-05-31 02:45:11 +00:00
gitea-actions[bot] 95fbe46ab4 chore(release): build 02.27.00 [skip ci] 2026-05-31 02:45:10 +00:00
jmiller f1a5f818a5 Merge pull request 'feat: content-only reset with asset rebuild, task-driven settings, snapshot on save' (#98) from dev into main
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
2026-05-31 02:44:58 +00:00
jmiller aad6f944e7 chore: sync updates.xml from development [skip ci] 2026-05-31 02:44:55 +00:00
gitea-actions[bot] c756b0df30 chore: update development channel 02.26.15-dev [skip ci]
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 22s
2026-05-31 02:44:54 +00:00
gitea-actions[bot] 0f89de2003 chore(version): auto-bump 02.26.15-dev [skip ci] 2026-05-31 02:44:52 +00:00
Jonathan Miller 83880ed740 fix: onContentAfterSave receives single event object in Joomla 6
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 4s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 5s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 5s
Universal: Auto Version Bump / Version Bump (push) Failing after 4s
Universal: PR Check / Validate PR (pull_request) Successful in 5s
Update Server / Update Server (push) Successful in 12s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: 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
Generic: Repo Health / Release configuration (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 21:44:39 -05:00
Jonathan Miller 526fe33e10 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 3s
Universal: Auto Version Bump / Version Bump (push) Failing after 5s
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 4s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 5s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 6s
Universal: PR Check / Validate PR (pull_request) Successful in 5s
Universal: Build & Release / Promote to RC (pull_request) Failing after 8s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: 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
Generic: Repo Health / Release configuration (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
2026-05-30 21:41:51 -05:00
Jonathan Miller 4063d08b11 chore: merge main into dev [skip ci] 2026-05-30 21:41:46 -05:00
jmiller b0467e88ec chore: sync updates.xml from development [skip ci] 2026-05-31 02:41:06 +00:00
gitea-actions[bot] ea482939c2 chore: update development channel 02.26.14-dev [skip ci] 2026-05-31 02:41:05 +00:00
gitea-actions[bot] df4548ba8b chore(version): auto-bump 02.26.14-dev [skip ci] 2026-05-31 02:41:03 +00:00
Jonathan Miller 9af7e6bc7c Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 16s
Universal: Auto Version Bump / Version Bump (push) Failing after 18s
Update Server / Update Server (push) Successful in 23s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
2026-05-30 21:40:32 -05:00
Jonathan Miller 1ea9406f1a fix: rebuild assets after content restore to fix ACL
After restoring content tables, deletes stale content-level assets
(articles, categories, modules, contacts, banners) and rebuilds:
- Category nested set tree
- Menu nested set tree
- Asset nested set tree
- Re-creates missing asset entries for articles with asset_id=0

Component and extension-level assets are never touched.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 21:40:28 -05:00
jmiller c5b47bc31a chore: sync updates.xml from development [skip ci] 2026-05-31 02:38:25 +00:00
gitea-actions[bot] 588a17e22c chore: update development channel 02.26.13-dev [skip ci] 2026-05-31 02:38:24 +00:00
gitea-actions[bot] 9c269117c4 chore(version): auto-bump 02.26.13-dev [skip ci] 2026-05-31 02:38:21 +00:00
Jonathan Miller 1032074e5c refactor(demo): content-only snapshot — never touch system tables
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 4s
Update Server / Update Server (push) Successful in 14s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Hardcoded safe table list: content, categories, menus, modules, users,
tags, fields, contacts, banners. Never touches #__extensions, #__assets,
#__schemas, #__session, #__update_sites, or any system tables.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 21:38:08 -05:00
Jonathan Miller b0aa4e8763 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 21s
Universal: Auto Version Bump / Version Bump (push) Failing after 24s
Update Server / Update Server (push) Failing after 11s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
2026-05-30 21:34:55 -05:00
Jonathan Miller 6f19f58033 fix: read all execution rule types and show task params in info card
Supports interval-minutes, interval-hours, interval-days, and
cron-expression rule types. Shows banner, media, countdown, and
baseline snapshot status in the plugin config info card.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 21:34:51 -05:00
jmiller 836dd194ab chore: sync updates.xml from development [skip ci] 2026-05-31 02:33:38 +00:00
gitea-actions[bot] f1c6fbd0b7 chore: update development channel 02.26.12-dev [skip ci] 2026-05-31 02:33:37 +00:00
gitea-actions[bot] fe5f74b640 chore(version): auto-bump 02.26.12-dev [skip ci] 2026-05-31 02:33:35 +00:00
Jonathan Miller 9f779b2dfa Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
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) Failing after 4s
Update Server / Update Server (push) Successful in 11s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
2026-05-30 21:33:19 -05:00
Jonathan Miller 61286748db fix: parse next_execution as UTC — Joomla stores times without timezone suffix
DreamHost server timezone is America/Los_Angeles, so PHP was interpreting
the UTC datetime as Pacific time, causing the display to be hours off.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 21:33:03 -05:00
jmiller c3fefa27aa chore: sync updates.xml from development [skip ci] 2026-05-31 02:31:27 +00:00
gitea-actions[bot] 5abfec2de2 chore: update development channel 02.26.11-dev [skip ci] 2026-05-31 02:31:27 +00:00
gitea-actions[bot] c307e97d57 chore(version): auto-bump 02.26.11-dev [skip ci] 2026-05-31 02:31:24 +00:00
Jonathan Miller 24c76cadbf fix: take snapshot immediately on task save, not on next scheduled run
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) Failing after 4s
Update Server / Update Server (push) Successful in 14s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Snapshot now triggers via onContentAfterSave when the task is saved
with take_snapshot_on_save=1, instead of waiting for the next execution.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 21:31:06 -05:00
Jonathan Miller e1db91ba84 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 13s
Universal: Auto Version Bump / Version Bump (push) Failing after 16s
Update Server / Update Server (push) Failing after 12s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
2026-05-30 21:28:31 -05:00
Jonathan Miller beb52fde99 fix: add fieldset label to task form to prevent untranslated key
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 21:28:27 -05:00
jmiller b9108b15f9 chore: sync updates.xml from development [skip ci] 2026-05-31 02:26:32 +00:00
gitea-actions[bot] 3a2655d5ef chore: update development channel 02.26.10-dev [skip ci] 2026-05-31 02:26:31 +00:00
gitea-actions[bot] f465b49f1f chore(version): auto-bump 02.26.10-dev [skip ci] 2026-05-31 02:26:30 +00:00
Jonathan Miller b3f4df89c5 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
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 4s
Update Server / Update Server (push) Successful in 10s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
2026-05-30 21:26:16 -05:00
Jonathan Miller a650036591 refactor(demo): move all demo settings into the scheduled task form
Demo mode is now controlled entirely through the Joomla Scheduled Task:
- Create a "MokoWaaS Demo Reset" task to enable demo mode
- Task form has: snapshot toggle, media toggle, banner settings
- System plugin reads banner config from the task params
- Plugin config shows task status card with link to manage it
- Removed demo_mode_enabled, schedule, and one-shot toggles from plugin

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 21:26:15 -05:00
jmiller 141265ba4e chore: sync updates.xml 02.27.00 from main [skip ci] 2026-05-31 02:18:38 +00:00
gitea-actions[bot] 16c9f94a9d chore: update channels for 02.27.00 [skip ci] 2026-05-31 02:18:16 +00:00
jmiller 56053746ba chore: sync updates.xml 02.27.00 from main [skip ci] 2026-05-31 02:18:16 +00:00
gitea-actions[bot] 6e6677df70 chore(release): build 02.27.00 [skip ci] 2026-05-31 02:18:15 +00:00
jmiller 399bdb976e Merge pull request 'fix: re-register extensions after full DB restore, pull task time, fix messages' (#97) from dev into main
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
2026-05-31 02:18:06 +00:00
Jonathan Miller 2ede281762 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
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) Failing after 3s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 5s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 5s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 6s
Universal: PR Check / Validate PR (pull_request) Successful in 5s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 20s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: 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
Generic: Repo Health / Release configuration (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
2026-05-30 21:17:31 -05:00
Jonathan Miller 88672dc7a2 chore: merge main into dev [skip ci] 2026-05-30 21:16:28 -05:00
jmiller 7db4839b23 chore: sync updates.xml from development [skip ci] 2026-05-31 02:14:11 +00:00
gitea-actions[bot] b83a0a50f2 chore: update development channel 02.26.09-dev [skip ci] 2026-05-31 02:14:11 +00:00
gitea-actions[bot] 3ce4d8599f chore(version): auto-bump 02.26.09-dev [skip ci] 2026-05-31 02:14:09 +00:00
Jonathan Miller 73130a0824 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 9s
Universal: Auto Version Bump / Version Bump (push) Failing after 12s
Update Server / Update Server (push) Successful in 17s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
2026-05-30 21:13:44 -05:00
Jonathan Miller a26578fea3 fix: re-register MokoWaaS extensions and task after full DB restore
Full database restore overwrites #__extensions and #__scheduler_tasks
with snapshot data, losing the current plugin registrations. After
restore, re-enables all MokoWaaS extensions, re-registers the task
plugin if missing, and re-enables the update server.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 21:13:40 -05:00
jmiller e66e688623 chore: sync updates.xml from development [skip ci] 2026-05-31 02:08:01 +00:00
gitea-actions[bot] a5ab169d14 chore: update development channel 02.26.08-dev [skip ci] 2026-05-31 02:08:00 +00:00
gitea-actions[bot] d957dda7ed chore(version): auto-bump 02.26.08-dev [skip ci] 2026-05-31 02:07:58 +00:00
Jonathan Miller a0092773c7 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
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 4s
Update Server / Update Server (push) Successful in 12s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
2026-05-30 21:07:41 -05:00
Jonathan Miller de14cc2de7 fix: update admin messages for full-DB snapshot (no table count)
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 21:07:28 -05:00
jmiller 4b48036154 chore: sync updates.xml from development [skip ci] 2026-05-31 02:05:06 +00:00
gitea-actions[bot] ee42adf826 chore: update development channel 02.26.07-dev [skip ci] 2026-05-31 02:05:05 +00:00
gitea-actions[bot] 1b3b20d2f1 chore(version): auto-bump 02.26.07-dev [skip ci] 2026-05-31 02:05:03 +00:00
Jonathan Miller 025917d265 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
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 4s
Update Server / Update Server (push) Successful in 12s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
2026-05-30 21:04:52 -05:00
Jonathan Miller ad3bb21b39 feat(demo): pull next reset time from Joomla scheduled task, show last run
NextResetField now queries #__scheduler_tasks.next_execution directly
instead of using a stored param. Also shows last execution time.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 21:04:50 -05:00
gitea-actions[bot] 93649ba539 chore: update development channel 02.26.06-dev [skip ci] 2026-05-31 01:59:10 +00:00
jmiller 3d758bfa08 chore: sync updates.xml 02.27.00 from main [skip ci] 2026-05-31 01:51:38 +00:00
gitea-actions[bot] f2dcc10f7c chore: update channels for 02.27.00 [skip ci] 2026-05-31 01:51:36 +00:00
gitea-actions[bot] 35c4f9b7ec chore(release): build 02.27.00 [skip ci] 2026-05-31 01:51:25 +00:00
jmiller 1ca4778e5f chore: sync updates.xml 02.27.00 from main [skip ci] 2026-05-31 01:49:25 +00:00
jmiller f55210ef92 chore: sync updates.xml 02.27.00-rc from rc [skip ci] 2026-05-31 01:49:07 +00:00
jmiller 9508513042 chore: sync updates.xml 02.27.00-rc from rc [skip ci] 2026-05-31 01:49:06 +00:00
jmiller da26bc6c00 Merge pull request 'feat: auto-create scheduled task, full DB snapshot, single baseline' (#96) from dev into main
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
2026-05-31 01:49:06 +00:00
Jonathan Miller 98629226fd Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
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) Failing after 3s
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
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 4s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 5s
Universal: PR Check / Validate PR (pull_request) Successful in 4s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Successful in 20s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 21s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: 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
Generic: Repo Health / Release configuration (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
2026-05-30 20:48:33 -05:00
Jonathan Miller 509b30a7e4 chore: merge main into dev [skip ci] 2026-05-30 20:48:26 -05:00
jmiller 329316bd4f chore: sync .mokogitea/workflows/cascade-dev.yml from moko-platform [skip ci] 2026-05-31 01:45:15 +00:00
jmiller 60d44b5bd6 chore: sync updates.xml from development [skip ci] 2026-05-31 01:44:30 +00:00
gitea-actions[bot] a34b12e411 chore: update development channel 02.26.06-dev [skip ci] 2026-05-31 01:44:29 +00:00
gitea-actions[bot] 004d26293d chore(version): auto-bump 02.26.06-dev [skip ci] 2026-05-31 01:44:28 +00:00
Jonathan Miller 5f84b3166c Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
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) Failing after 3s
Update Server / Update Server (push) Successful in 10s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
2026-05-30 20:44:13 -05:00
Jonathan Miller 709c2629f8 chore(demo): remove baseline name config — single baseline hardcoded to default
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 20:43:48 -05:00
jmiller 312d2cb328 chore: sync .mokogitea/workflows/cascade-dev.yml from moko-platform [skip ci] 2026-05-31 01:41:38 +00:00
jmiller 9048a3fbc3 chore: sync updates.xml from development [skip ci] 2026-05-31 01:40:51 +00:00
gitea-actions[bot] 4548ccd619 chore: update development channel 02.26.05-dev [skip ci] 2026-05-31 01:40:50 +00:00
gitea-actions[bot] 370314034f chore(version): auto-bump 02.26.05-dev [skip ci] 2026-05-31 01:40:48 +00:00
Jonathan Miller b7fd26fdcd Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 5s
Universal: Auto Version Bump / Version Bump (push) Failing after 7s
Update Server / Update Server (push) Successful in 16s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
2026-05-30 20:40:26 -05:00
Jonathan Miller 5800164ea4 feat(demo): auto-create scheduled task when demo mode is enabled
When demo mode is turned on and saved, automatically creates a Joomla
Scheduler task for the demo reset using the configured cron schedule.
When demo mode is turned off, removes the scheduled task.

No manual task creation needed — just enable demo mode and save.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 20:37:41 -05:00
jmiller 182516da24 chore: sync updates.xml 02.28.00 from main [skip ci] 2026-05-31 01:29:13 +00:00
gitea-actions[bot] 0ae3b25889 chore: update channels for 02.28.00 [skip ci] 2026-05-31 01:29:05 +00:00
gitea-actions[bot] 2521af97fc chore(release): build 02.28.00 [skip ci] 2026-05-31 01:29:03 +00:00
jmiller e20f6dce7e chore: sync updates.xml 02.27.00 from main [skip ci] 2026-05-31 01:24:21 +00:00
gitea-actions[bot] 9c27ef2f20 chore: update channels for 02.27.00 [skip ci] 2026-05-31 01:24:21 +00:00
gitea-actions[bot] 8c3e5ccebb chore(release): build 02.27.00 [skip ci] 2026-05-31 01:24:19 +00:00
jmiller 2bb2e531ee Merge pull request 'refactor: full database snapshot/restore, remove table selection' (#95) from dev into main
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Cascade Main → Dev / Cascade main → branches (push) Failing after 2s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
2026-05-31 01:24:06 +00:00
Jonathan Miller 15820542a6 chore: merge main into dev [skip ci]
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 24s
2026-05-30 20:22:49 -05:00
Jonathan Miller e8783fb43a chore: merge dev (resolve version conflict) [skip ci] 2026-05-30 20:15:19 -05:00
Jonathan Miller 91cb2b2e12 refactor(demo): full database snapshot instead of selective tables
Replaced selective table snapshot with full mysqldump/restore. This
ensures the site returns to an exact known state without the risk of
breaking the installation by restoring stale #__extensions or #__assets.

Uses mysqldump CLI when available, falls back to PHP-based dump.
Removed SnapshotTables field and table selection config — the entire
database is now captured and restored as one unit.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 20:14:54 -05:00
jmiller ef068c1a31 chore: sync CONTRIBUTING.md from moko-platform [skip ci] 2026-05-31 01:09:53 +00:00
jmiller 8a356d205f chore: sync updates.xml from development [skip ci] 2026-05-31 01:02:34 +00:00
gitea-actions[bot] 91a0e14850 chore: update development channel 02.26.04-dev [skip ci] 2026-05-31 01:02:32 +00:00
gitea-actions[bot] 1582d32330 chore(version): auto-bump 02.26.04-dev [skip ci] 2026-05-31 01:02:27 +00:00
Jonathan Miller 954fdb9e3e Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
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 6s
Update Server / Update Server (push) Successful in 28s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
2026-05-30 20:02:01 -05:00
Jonathan Miller e008c9d79d fix: table selections not saving, clear next-reset when demo off
- Remove extra [] from select name (caused nested array storage)
- Flatten nested arrays when reading back broken saved values
- NextResetField shows "Demo mode is off" when disabled
- Clear demo_next_reset param when demo mode is turned off

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 20:01:57 -05:00
jmiller fa51e516fd chore: sync updates.xml 02.27.00 from main [skip ci] 2026-05-31 00:54:46 +00:00
gitea-actions[bot] 133047272d chore: update channels for 02.27.00 [skip ci] 2026-05-31 00:54:35 +00:00
gitea-actions[bot] c93024686e chore(release): build 02.27.00 [skip ci] 2026-05-31 00:54:33 +00:00
jmiller 81d8f2e960 Merge pull request 'feat: multi-select tables, formatted reset timer, copy token, reset defaults link' (#94) from dev into main
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Cascade Main → Dev / Cascade main → branches (push) Failing after 3s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
2026-05-31 00:54:23 +00:00
jmiller 82919aa63c chore: sync updates.xml 02.27.00-rc from rc [skip ci] 2026-05-31 00:54:20 +00:00
jmiller 47d2a2b6f9 chore: sync updates.xml 02.27.00-rc from rc [skip ci]
Universal: Build & Release / Promote 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 26s
2026-05-31 00:54:19 +00:00
Jonathan Miller 5f059013a5 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 5s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 6s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 5s
Universal: PR Check / Validate PR (pull_request) Successful in 6s
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 5s
Universal: Build & Release / Promote to RC (pull_request) Successful in 27s
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
Generic: Repo Health / Release configuration (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
2026-05-30 19:53:11 -05:00
Jonathan Miller fbd3247082 chore: merge main into dev [skip ci] 2026-05-30 19:53:03 -05:00
jmiller 90a78be033 chore: sync updates.xml from development [skip ci] 2026-05-31 00:50:48 +00:00
gitea-actions[bot] e63efd7626 chore: update development channel 02.26.03-dev [skip ci] 2026-05-31 00:50:48 +00:00
gitea-actions[bot] 2c16ac7142 chore(version): auto-bump 02.26.03-dev [skip ci] 2026-05-31 00:50:46 +00:00
Jonathan Miller 6c33fd63a9 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
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) Failing after 3s
Update Server / Update Server (push) Successful in 11s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
2026-05-30 19:50:31 -05:00
Jonathan Miller afacaa9dbb feat(demo): add reset-to-defaults link on snapshot tables multi-select
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 19:50:12 -05:00
jmiller ddddd73e4c chore: sync updates.xml from development [skip ci] 2026-05-31 00:42:12 +00:00
gitea-actions[bot] 286e91853c chore: update development channel 02.26.02-dev [skip ci] 2026-05-31 00:42:11 +00:00
gitea-actions[bot] 1c07a3eb89 chore(version): auto-bump 02.26.02-dev [skip ci] 2026-05-31 00:42:09 +00:00
Jonathan Miller 9edc304eff Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
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) Failing after 3s
Update Server / Update Server (push) Successful in 12s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
2026-05-30 19:41:53 -05:00
Jonathan Miller 3b186f55a8 fix: use raw HTML select with optgroups instead of HTMLHelper::select.optgroup
HTMLHelper::_('select.optgroup') does not exist in Joomla 6. Replaced
with a plain HTML multi-select built manually with <optgroup> tags.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 19:41:06 -05:00
jmiller 4c13da8456 chore: sync updates.xml from development [skip ci] 2026-05-31 00:38:53 +00:00
gitea-actions[bot] 37247a5923 chore: update development channel 02.26.01-dev [skip ci] 2026-05-31 00:38:52 +00:00
gitea-actions[bot] 08cc791db5 chore(version): auto-bump 02.26.01-dev [skip ci] 2026-05-31 00:38:51 +00:00
Jonathan Miller e0eee892d0 feat(demo): multi-select list for tables, formatted next-reset with timezone
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) Failing after 3s
Update Server / Update Server (push) Successful in 10s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
- SnapshotTablesField now renders as a multi-select <select> with
  optgroups (Content, Users, Menus, Modules, Assets, Other) instead
  of individual checkboxes
- NextResetField displays the next reset as a formatted datetime in
  the site's configured timezone with a relative badge (in X hours)

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 19:38:29 -05:00
Jonathan Miller 1713388b7d chore(manifest): fix display-name structure and update CONTRIBUTING.md
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 5s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Standardize manifest.xml identity block: ensure <name> contains only
the machine identifier (PascalCase) and <display-name> contains the
human-readable label with Joomla extension type prefix. Remove duplicate
<version> tags where present. Update CONTRIBUTING.md from moko-platform
default.

Authored-by: Moko Consulting
2026-05-30 19:11:14 -05:00
jmiller 4886fba5e8 chore: sync updates.xml 02.26.00 from main [skip ci] 2026-05-30 23:38:06 +00:00
gitea-actions[bot] 0808962073 chore: update channels for 02.26.00 [skip ci] 2026-05-30 23:37:58 +00:00
gitea-actions[bot] 7ecaa0e7ca chore(release): build 02.26.00 [skip ci] 2026-05-30 23:37:52 +00:00
jmiller e3c9bd3b06 Merge pull request 'feat: snapshot table checkboxes, multi-dir media, countdown fix, copy button' (#93) from dev into main
Universal: Cascade Main → Dev / Cascade main → branches (push) Failing after 3s
2026-05-30 23:37:39 +00:00
jmiller 3ad1d36a8f chore: sync updates.xml 02.26.00-rc from rc [skip ci] 2026-05-30 23:37:38 +00:00
jmiller 6b0919daf3 chore: sync updates.xml 02.26.00-rc from rc [skip ci]
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 25s
2026-05-30 23:37:37 +00:00
Jonathan Miller d2779af818 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 5s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 6s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 6s
Universal: PR Check / Validate PR (pull_request) Successful in 6s
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Repo Health / Site Health (push) Has been skipped
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Universal: Build & Release / Promote to RC (pull_request) Successful in 29s
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
Generic: Repo Health / Release configuration (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
2026-05-30 18:35:17 -05:00
Jonathan Miller 11a217d3b9 chore: merge main into dev [skip ci] 2026-05-30 18:34:31 -05:00
jmiller e943b248e5 chore: sync updates.xml from development [skip ci] 2026-05-30 23:21:43 +00:00
gitea-actions[bot] 440e528786 chore: update development channel 02.25.03-dev [skip ci] 2026-05-30 23:21:42 +00:00
gitea-actions[bot] 5e290a21a1 chore(version): auto-bump 02.25.03-dev [skip ci] 2026-05-30 23:21:41 +00:00
Jonathan Miller 888cd4cb67 feat(demo): auto-load DB tables as checkboxes, multi-directory media snapshots
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) Failing after 3s
Update Server / Update Server (push) Successful in 9s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
- SnapshotTablesField: custom checkbox field that queries DB for all
  tables, groups by type (content/users/menus/modules), pre-selects
  important tables by default
- Media snapshots now support multiple directories (images, media)
  with individual ZIPs per directory and legacy fallback
- Backward compatible with old boolean and textarea param formats

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 18:21:02 -05:00
jmiller f81b895af6 chore: sync updates.xml from development [skip ci] 2026-05-30 23:03:55 +00:00
gitea-actions[bot] c8df9876fe chore: update development channel 02.25.02-dev [skip ci] 2026-05-30 23:03:54 +00:00
gitea-actions[bot] a520b791a3 chore(version): auto-bump 02.25.02-dev [skip ci] 2026-05-30 23:03:53 +00:00
Jonathan Miller cd5a9f7ecb fix: calculate countdown at runtime and add 5/15/30 min presets
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) Successful in 9s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
- Countdown now calculates next reset on the fly from cron schedule if
  demo_next_reset is empty or in the past, instead of only on save
- Added 5min, 15min, 30min presets to the schedule dropdown for testing

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 18:03:16 -05:00
jmiller 36dccc713a chore: sync updates.xml from development [skip ci] 2026-05-30 22:52:08 +00:00
gitea-actions[bot] 4bad7325f1 chore: update development channel 02.25.01-dev [skip ci] 2026-05-30 22:52:08 +00:00
gitea-actions[bot] cb775cdc4c chore(version): auto-bump 02.25.01-dev [skip ci] 2026-05-30 22:52:06 +00:00
Jonathan Miller 96e89d0b0f feat: add copy-to-clipboard button on health API token field
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) Failing after 3s
Update Server / Update Server (push) Successful in 10s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
New CopyableTokenField renders a readonly monospace input with a Copy
button, matching Joomla's native API token UX in user profiles.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 17:51:49 -05:00
jmiller 9a313439ae chore: sync updates.xml 02.25.00 from main [skip ci] 2026-05-30 22:17:40 +00:00
gitea-actions[bot] 9626344e3b chore: update channels for 02.25.00 [skip ci] 2026-05-30 22:17:39 +00:00
gitea-actions[bot] 74e61b00e6 chore(release): build 02.25.00 [skip ci] 2026-05-30 22:17:31 +00:00
jmiller c9cedeb14a Merge pull request 'fix: package display name to Package - MokoWaaS' (#92) from dev into main
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Cascade Main → Dev / Cascade main → branches (push) Failing after 3s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
2026-05-30 22:17:20 +00:00
Jonathan Miller 00b78b9d43 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Failing after 5s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 5s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 6s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 7s
Universal: PR Check / Validate PR (pull_request) Successful in 7s
Universal: Build & Release / Promote 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 30s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: 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
Generic: Repo Health / Release configuration (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
2026-05-30 17:17:00 -05:00
Jonathan Miller c4a1cf356a chore: merge main into dev [skip ci] 2026-05-30 17:16:52 -05:00
jmiller 0be8cc876c chore: sync updates.xml 02.25.00-rc from rc [skip ci] 2026-05-30 22:16:22 +00:00
jmiller 1e3513b714 chore: sync updates.xml 02.25.00-rc from rc [skip ci] 2026-05-30 22:16:21 +00:00
Jonathan Miller 22ccd233c2 fix: update display name in .mokogitea/manifest.xml to match package
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Access control (pull_request) Successful in 3s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 7s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 8s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 8s
Universal: PR Check / Validate PR (pull_request) Successful in 10s
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 6s
Universal: Build & Release / Promote to RC (pull_request) Successful in 39s
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
Generic: Repo Health / Release configuration (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: 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-30 17:03:06 -05:00
jmiller 4985c2e7b4 chore: sync updates.xml from development [skip ci] 2026-05-30 22:01:16 +00:00
gitea-actions[bot] b7b63d8172 chore: update development channel 02.24.01-dev [skip ci] 2026-05-30 22:01:15 +00:00
gitea-actions[bot] e377bef840 chore(version): auto-bump 02.24.01-dev [skip ci] 2026-05-30 22:01:13 +00:00
Jonathan Miller 7d89d77a92 fix: update package display name to "Package - MokoWaaS"
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 4s
Update Server / Update Server (push) Successful in 15s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: 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-30 17:00:51 -05:00
jmiller 1f4d598e38 chore: sync updates.xml 02.24.00 from main [skip ci] 2026-05-30 21:21:27 +00:00
gitea-actions[bot] 2807a54483 chore: update channels for 02.24.00 [skip ci] 2026-05-30 21:21:25 +00:00
gitea-actions[bot] 7b79256318 chore(release): build 02.24.00 [skip ci] 2026-05-30 21:21:10 +00:00
jmiller 22acb25bbe Merge pull request 'fix: docblock syntax errors and crontab reset schedule' (#91) from dev into main
Universal: Cascade Main → Dev / Cascade main → branches (push) Failing after 12s
2026-05-30 21:20:43 +00:00
Jonathan Miller 586b7bc105 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 21s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 11s
Generic: Repo Health / Access control (pull_request) Successful in 4s
Universal: Auto Version Bump / Version Bump (push) Failing after 28s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 34s
Universal: PR Check / Validate PR (pull_request) Successful in 19s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 29s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 1m14s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 17m48s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: 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
Generic: Repo Health / Release configuration (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
2026-05-30 16:20:11 -05:00
Jonathan Miller 6cceb85be6 chore: merge main into dev (resolve version conflicts) [skip ci]
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 16:19:33 -05:00
jmiller 14b45cb36d chore: sync updates.xml from development [skip ci] 2026-05-30 21:17:22 +00:00
gitea-actions[bot] 45cbd5cad4 chore: update development channel 02.23.03-dev [skip ci] 2026-05-30 21:17:21 +00:00
gitea-actions[bot] b1519cf12a chore(version): auto-bump 02.23.03-dev [skip ci] 2026-05-30 21:17:19 +00:00
Jonathan Miller d9012ffddb fix: add plugin attribute to task plugin manifest to set element in DB
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 1s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 6s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 7s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 8s
Universal: Auto Version Bump / Version Bump (push) Failing after 6s
Universal: PR Check / Validate PR (pull_request) Successful in 8s
Update Server / Update Server (push) Successful in 15s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: 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
Generic: Repo Health / Release configuration (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Joomla requires a <filename plugin="mokowaasdemo"> in the files block
to populate the element field in #__extensions. Without it, the DB
INSERT fails with "Field 'element' doesn't have a default value".

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 16:17:01 -05:00
jmiller 0740b495e1 chore: sync updates.xml from development [skip ci] 2026-05-30 21:11:45 +00:00
gitea-actions[bot] 1dff862d2b chore: update development channel 02.23.02-dev [skip ci] 2026-05-30 21:11:44 +00:00
gitea-actions[bot] ccdab8b5da chore(version): auto-bump 02.23.02-dev [skip ci] 2026-05-30 21:11:42 +00:00
Jonathan Miller 5245d15b9d fix: remove all */ sequences from comments to prevent docblock termination
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 8s
Universal: Auto Version Bump / Version Bump (push) Failing after 18s
Update Server / Update Server (push) Successful in 26s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Cron-related comments contained */ in examples like "*/5" and "*/N"
which PHP interprets as closing the block comment, causing syntax errors.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 16:11:14 -05:00
Jonathan Miller 1673616523 fix: docblock */ in (*/N) prematurely closes comment, causing PHP syntax error
The string "*/N" inside a docblock contains */ which PHP interprets as
the end of the block comment, making "N" an unexpected identifier.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 16:11:14 -05:00
jmiller 62654e41a6 chore: sync updates.xml from development [skip ci] 2026-05-30 20:37:01 +00:00
gitea-actions[bot] 88817690e5 chore: update development channel 02.23.01-dev [skip ci] 2026-05-30 20:37:01 +00:00
gitea-actions[bot] 689bf1712f chore(version): auto-bump 02.23.01-dev [skip ci] 2026-05-30 20:36:59 +00:00
Jonathan Miller 65e986344e feat(demo): crontab-style reset schedule with preset dropdown and stored countdown
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 4s
Update Server / Update Server (push) Successful in 13s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
- Replace hours interval with preset schedule dropdown (hourly, 4h, 6h,
  12h, daily, weekly, monthly) plus custom crontab option
- Banner countdown now uses stored demo_next_reset timestamp calculated
  from cron schedule on save, not a relative timer from page load
- Added cron parser (calculateNextCronRun) supporting wildcards, steps,
  ranges, and comma-separated values

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 15:36:43 -05:00
jmiller c37e6d9637 chore: sync updates.xml 02.23.00 from main [skip ci] 2026-05-30 20:27:23 +00:00
gitea-actions[bot] cee142c714 chore: update channels for 02.23.00 [skip ci] 2026-05-30 20:27:22 +00:00
gitea-actions[bot] 1e404e1c7b chore(release): build 02.23.00 [skip ci] 2026-05-30 20:27:17 +00:00
jmiller 87b8c770f3 Merge pull request 'chore: sync dev to main — version bumps and updates.xml' (#90) from dev into main
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Cascade Main → Dev / Cascade main → branches (push) Failing after 2s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
2026-05-30 20:27:05 +00:00
gitea-actions[bot] ab38f96dbc chore: update development channel 02.22.05-dev [skip ci]
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 21s
2026-05-30 20:20:50 +00:00
jmiller 0cb8c3d6e4 chore: sync updates.xml from development [skip ci] 2026-05-30 20:20:50 +00:00
gitea-actions[bot] c94e92a97e chore(version): auto-bump 02.22.05-dev [skip ci] 2026-05-30 20:20:48 +00:00
Jonathan Miller 02149ecc04 fix: rename plg_task_mokowaas → plg_task_mokowaasdemo to match package manifest
Universal: Auto Version Bump / Version Bump (push) Failing after 10s
Update Server / Update Server (push) Successful in 16s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Cascade Main → Dev / Cascade main → branches (push) Failing after 2s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
The package manifest expects plg_task_mokowaasdemo.zip but the directory
was named plg_task_mokowaas, causing "Install path does not exist" during
package installation.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 15:20:25 -05:00
Jonathan Miller abe906b4d7 chore: merge main into dev (manifest display name sync) [skip ci]
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 15:16:16 -05:00
Jonathan Miller 2cd327e002 fix: align display names across all manifests and branches [skip ci]
- updates.xml dev channel: add "Package -" prefix and php_minimum
- Fix "Moko WaaS" → "MokoWaaS" in en-GB .ini, en-GB .sys.ini, en-US .sys.ini
- Normalize single quotes to double quotes in dev channel XML attributes

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 14:17:44 -05:00
jmiller e0ffef12f5 feat: add display-name to manifest.xml [skip ci] 2026-05-30 19:14:26 +00:00
jmiller f8612d55e5 chore: sync updates.xml from development [skip ci] 2026-05-30 19:13:01 +00:00
gitea-actions[bot] 76a9f643c9 chore: update development channel 02.22.04-dev [skip ci] 2026-05-30 19:12:59 +00:00
gitea-actions[bot] e824251c4a chore(version): auto-bump 02.22.04-dev [skip ci] 2026-05-30 19:12:56 +00:00
Jonathan Miller d17544aba2 fix: add php_minimum to dev channel in updates.xml
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 8s
Universal: Auto Version Bump / Version Bump (push) Failing after 14s
Update Server / Update Server (push) Successful in 26s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: 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-30 14:12:28 -05:00
Jonathan Miller 294a06028b fix: align updates.xml with MokoOnyx pattern and fix display name consistency
- updates.xml descriptions now use "Package - MokoWaaS" prefix (matches
  MokoOnyx's "Template - MokoOnyx" convention)
- Added <php_minimum>8.1.0</php_minimum> to all update entries
- Fixed inconsistent quote style in dev channel entry
- Fixed language strings: "Moko WaaS" → "MokoWaaS" in all .ini files

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 14:12:07 -05:00
jmiller 210e1182c5 chore: sync updates.xml from development [skip ci] 2026-05-30 19:07:09 +00:00
gitea-actions[bot] 461d63efca chore: update development channel 02.22.03-dev [skip ci] 2026-05-30 19:07:08 +00:00
gitea-actions[bot] 747a7a4081 chore(version): auto-bump 02.22.03-dev [skip ci] 2026-05-30 19:07:06 +00:00
Jonathan Miller 3975e8e205 feat(api): add extensions list endpoint with filters and update server info
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) Failing after 4s
Update Server / Update Server (push) Successful in 15s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
GET /?mokowaas=extensions and GET /api/v1/mokowaas/extensions returns
all installed extensions with version, enabled/protected/locked status,
and update server details. Supports ?type, ?search, and ?enabled filters.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 14:06:52 -05:00
jmiller ea20256a67 chore: sync updates.xml from development [skip ci] 2026-05-30 19:03:39 +00:00
gitea-actions[bot] 817e00fc75 chore: update development channel 02.22.02-dev [skip ci] 2026-05-30 19:03:36 +00:00
gitea-actions[bot] 2aa69c1fe2 chore(version): auto-bump 02.22.02-dev [skip ci] 2026-05-30 19:03:29 +00:00
Jonathan Miller e1db1149d8 fix: protect all package extensions, keep update server enabled, clean up legacy mokowaasbrand
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 6s
Universal: Auto Version Bump / Version Bump (push) Failing after 10s
Update Server / Update Server (push) Successful in 30s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
- protectExtensions() now covers all MokoWaaS elements (package, system
  plugin, component, webservices, task, perfectpublisher)
- enableUpdateServer() ensures #__update_sites stays enabled for MokoWaaS
- cleanupLegacyExtensions() removes old mokowaasbrand entries from
  #__extensions and deletes plugins/system/mokowaasbrand/ from filesystem

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 14:02:55 -05:00
jmiller 67344f65b2 chore: sync updates.xml from development [skip ci] 2026-05-30 18:53:38 +00:00
gitea-actions[bot] f4caa1821e chore: update development channel 02.22.01-dev [skip ci] 2026-05-30 18:53:37 +00:00
gitea-actions[bot] e25281e130 chore(version): auto-bump 02.22.01-dev [skip ci] 2026-05-30 18:53:35 +00:00
Jonathan Miller 1f25fe310f feat(sync): add one-way content sync — push articles, menus, modules to remote sites
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Site Health (push) Has been skipped
Universal: Auto Version Bump / Version Bump (push) Failing after 5s
Update Server / Update Server (push) Successful in 11s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Adds ContentSyncService (sender) and ContentSyncReceiver (receiver) for
pushing articles, categories, menus, and modules from a dev site to
remote MokoWaaS sites. Content matched by alias (upsert pattern).
Category IDs in menu links encoded as {catid:path} tokens for portable
cross-site resolution.

Closes #89

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 13:53:17 -05:00
jmiller c7d1c71d34 chore: sync updates.xml 02.22.00 from main [skip ci] 2026-05-30 17:58:59 +00:00
gitea-actions[bot] 40fffcb234 chore: update channels for 02.22.00 [skip ci] 2026-05-30 17:58:58 +00:00
gitea-actions[bot] 7a0331b8dd chore(version): auto-bump 02.21.02-dev [skip ci] 2026-05-30 17:58:53 +00:00
gitea-actions[bot] f451fb4d1a chore(release): build 02.22.00 [skip ci] 2026-05-30 17:58:52 +00:00
jmiller 137d51a534 Merge pull request 'feat(api): add install-from-URL endpoint' (#87) from dev into main
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 5s
Universal: Cascade Main → Dev / Cascade main → branches (push) Failing after 6s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
2026-05-30 17:58:34 +00:00
Jonathan Miller 6367572eb1 chore: merge main into dev (resolve version conflicts)
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 4s
Universal: PR Check / Branch Policy (pull_request) Successful in 3s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 3s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 9s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 9s
Universal: Auto Version Bump / Version Bump (push) Failing after 10s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 12s
Universal: PR Check / Validate PR (pull_request) Successful in 12s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Update Server / Update Server (push) Failing after 16s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 30s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: 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
Generic: Repo Health / Release configuration (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 12:58:23 -05:00
jmiller 33053364a9 chore: sync updates.xml from development [skip ci] 2026-05-30 17:51:13 +00:00
gitea-actions[bot] fc30ee6fb2 chore: update development channel 02.21.01-dev [skip ci] 2026-05-30 17:51:12 +00:00
gitea-actions[bot] 06f149a073 chore(version): auto-bump 02.21.01-dev [skip ci] 2026-05-30 17:51:10 +00:00
Jonathan Miller 72308fee53 feat(demo): add demo site reset with baseline snapshots and warning banner
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 4s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 5s
Universal: Auto Version Bump / Version Bump (push) Failing after 4s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 5s
Universal: PR Check / Validate PR (pull_request) Successful in 6s
Update Server / Update Server (push) Successful in 12s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: 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
Generic: Repo Health / Release configuration (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Adds Demo Mode feature for configuring sites as demo instances with
automatic periodic reset to a saved baseline. Includes DemoResetService
for DB table + media snapshots, frontend warning banner, REST and
query-string API endpoints, admin toggles, and a Joomla Scheduled Task
plugin (plg_task_mokowaasdemo) for automatic reset.

Closes #88

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 12:50:55 -05:00
jmiller b3298258d7 chore: sync updates.xml from development [skip ci] 2026-05-30 16:46:48 +00:00
gitea-actions[bot] c4b8aa0fbb chore: update development channel 02.20.02-dev [skip ci] 2026-05-30 16:46:47 +00:00
gitea-actions[bot] 4190e0e9ea chore(version): auto-bump 02.20.02-dev [skip ci] 2026-05-30 16:46:46 +00:00
Jonathan Miller 7a9e3da174 feat(api): add install-from-URL endpoint for remote extension deployment
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) Failing after 3s
Update Server / Update Server (push) Successful in 9s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Adds POST /api/v1/mokowaas/install to install Joomla extensions from a
remote ZIP URL via the REST API, enabling push-deploy from CI pipelines.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 11:45:21 -05:00
jmiller 9eaf2baea6 chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] 2026-05-30 16:03:07 +00:00
jmiller 5f28bc34db chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] 2026-05-30 16:01:24 +00:00
jmiller d93da0b1b7 chore: sync .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] 2026-05-30 15:03:54 +00:00
jmiller 80326664c9 chore: sync .mokogitea/workflows/auto-bump.yml from moko-platform [skip ci] 2026-05-30 15:01:26 +00:00
jmiller 10cb3c67c1 chore: sync CONTRIBUTING.md from moko-platform [skip ci] 2026-05-30 14:59:21 +00:00
jmiller cc0be856d5 chore: sync .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] 2026-05-30 14:55:52 +00:00
jmiller bb8377139b chore: sync .mokogitea/workflows/auto-bump.yml from moko-platform [skip ci] 2026-05-30 14:54:00 +00:00
jmiller 080d3fac6f chore: sync .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] 2026-05-30 05:52:58 +00:00
jmiller 0013a4d0e0 chore: sync .mokogitea/workflows/auto-bump.yml from moko-platform [skip ci] 2026-05-30 05:51:04 +00:00
jmiller 5e2eaaeece chore: sync .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] 2026-05-30 03:40:50 +00:00
jmiller 0c964e84dc chore: sync .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] 2026-05-30 01:16:24 +00:00
jmiller 5d0a560694 chore: sync .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] 2026-05-30 01:14:44 +00:00
jmiller 16943408b7 chore: sync .mokogitea/workflows/auto-bump.yml from moko-platform [skip ci] 2026-05-29 10:31:53 +00:00
jmiller 4422e3f39f chore: add .mokogitea/branch-protection.yml from moko-platform [skip ci] 2026-05-29 10:30:02 +00:00
jmiller ed8c7cadc7 chore: sync CONTRIBUTING.md from moko-platform [skip ci] 2026-05-29 10:27:30 +00:00
jmiller 08aab4a422 chore: sync .mokogitea/workflows/branch-cleanup.yml from moko-platform [skip ci] 2026-05-29 10:25:53 +00:00
jmiller 14e2e5df64 chore: sync .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] 2026-05-29 10:24:23 +00:00
jmiller 2b1eb43345 chore: sync .mokogitea/workflows/auto-bump.yml from moko-platform [skip ci] 2026-05-29 10:22:54 +00:00
gitea-actions[bot] 0033dae38b chore: update stable channel 02.20.00 [skip ci] 2026-05-28 22:32:07 +00:00
gitea-actions[bot] 75cc05c8d5 chore(release): build 02.20.00 [skip ci] 2026-05-28 22:32:04 +00:00
gitea-actions[bot] 7d2ca3607b chore(version): auto-bump patch 02.20.01-dev [skip ci] 2026-05-28 22:31:56 +00:00
jmiller 3d99cb7906 Merge pull request 'fix: test release with restored manifest.xml' (#86) from dev into main
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 3s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
2026-05-28 22:31:50 +00:00
Jonathan Miller c0548740d9 Merge remote-tracking branch 'origin/main' into dev
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: Build & Release / Promote Pre-Release to RC (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Access control (pull_request) Successful in 3s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 7s
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 9s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 8s
Universal: PR Check / Validate PR (pull_request) Successful in 8s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 18s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: 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
Generic: Repo Health / Release configuration (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
# Conflicts:
#	.mokogitea/manifest.xml
2026-05-28 17:31:40 -05:00
jmiller 8334d10d55 fix: restore .mokogitea/manifest.xml [skip ci] 2026-05-28 22:30:54 +00:00
jmiller 8aa081c506 fix: restore .mokogitea/manifest.xml (was truncated to 0 bytes) [skip ci] 2026-05-28 22:30:53 +00:00
gitea-actions[bot] 9410d7f79c chore: update stable channel 02.20.00 [skip ci] 2026-05-28 22:18:06 +00:00
gitea-actions[bot] f621428e0b chore(release): build 02.20.00 [skip ci] 2026-05-28 22:18:04 +00:00
jmiller 171a45a732 Merge pull request 'fix: test release with null guard fix (restored XML manifests)' (#85) from dev into main
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Cascade Main → Dev / Cascade main → branches (push) Failing after 2s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
2026-05-28 22:17:47 +00:00
gitea-actions[bot] 17d48b3e2c chore(version): auto-bump patch 02.19.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 17s
2026-05-28 22:17:33 +00:00
Jonathan Miller 30991f1490 Merge remote-tracking branch 'origin/main' into dev
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 5s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
# Conflicts:
#	README.md
2026-05-28 17:17:24 -05:00
jmiller 4122fdb4bb fix: restore src/packages/plg_webservices_perfectpublisher/perfectpublisher.xml (was truncated to 0 bytes) [skip ci] 2026-05-28 22:02:31 +00:00
jmiller 5834d2d80c fix: restore src/packages/plg_webservices_mokowaas/mokowaas.xml (was truncated to 0 bytes) [skip ci] 2026-05-28 22:02:30 +00:00
jmiller e06e4d1a7b fix: restore src/packages/plg_system_mokowaas/mokowaas.xml (was truncated to 0 bytes) [skip ci] 2026-05-28 22:02:29 +00:00
jmiller a61d9c7beb fix: restore src/packages/com_mokowaas/mokowaas.xml (was truncated to 0 bytes) [skip ci] 2026-05-28 22:02:28 +00:00
jmiller 41ebc7f901 fix: restore src/pkg_mokowaas.xml (was truncated to 0 bytes) [skip ci] 2026-05-28 22:02:27 +00:00
gitea-actions[bot] 2671b9dbb5 chore(release): build 02.19.00 [skip ci] 2026-05-28 21:56:07 +00:00
gitea-actions[bot] a085a564fd chore(version): auto-bump patch 02.18.01-dev [skip ci] 2026-05-28 21:55:53 +00:00
jmiller 500fce12ec Merge pull request 'fix: suffix stripping in auto-release and update-server workflows' (#84) from dev into main
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Cascade Main → Dev / Cascade main → branches (push) Failing after 3s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
2026-05-28 21:55:52 +00:00
Jonathan Miller b0c00b74d9 Merge remote-tracking branch 'origin/main' into dev
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 1s
Joomla: Extension CI / Release Readiness Check (pull_request) Successful in 6s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 6s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 7s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Auto Version Bump / Version Bump (push) Successful in 7s
Universal: Build & Release / Promote Pre-Release to RC (pull_request) Has been skipped
Update Server / Update Server (push) Failing after 11s
Universal: Build & Release / Build & Release Pipeline (pull_request) Failing after 18s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: 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
Generic: Repo Health / Release configuration (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
2026-05-28 16:55:18 -05:00
jmiller 3dcadc130b chore: sync .mokogitea/workflows/update-server.yml from moko-platform [skip ci] 2026-05-28 20:50:23 +00:00
jmiller a92d9bec81 chore: sync .mokogitea/workflows/update-server.yml from moko-platform [skip ci] 2026-05-28 20:48:14 +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 b831992b0d chore: sync .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] 2026-05-28 20:43:02 +00:00
gitea-actions[bot] 197f234087 chore: update stable channel 02.18.00 [skip ci] 2026-05-28 20:33:11 +00:00
gitea-actions[bot] 59d85ad295 chore(release): build 02.18.00 [skip ci] 2026-05-28 20:33:08 +00:00
jmiller 6775d161f7 Merge pull request 'chore: test release with folder=packages build fix' (#83) from dev into main
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Cascade Main → Dev / Cascade main → branches (push) Failing after 3s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
2026-05-28 20:32:50 +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 ce377a76cf fix: restore folder="packages" on files element [skip ci] 2026-05-28 20:30:03 +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] 0aad3a1cb2 chore: update stable channel 02.17.00 [skip ci] 2026-05-28 20:24:21 +00:00
gitea-actions[bot] 8618484895 chore(release): build 02.17.00 [skip ci] 2026-05-28 20:24:19 +00:00
jmiller 824376da43 Merge pull request 'chore: test full release pipeline (detached HEAD fix)' (#82) from dev into main
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Cascade Main → Dev / Cascade main → branches (push) Failing after 2s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
2026-05-28 20:24:08 +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 / 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
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
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 / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
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 / 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
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
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]
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
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
2026-05-28 14:50:59 -05:00
Moko Consulting efb7bf50a5 fix(workflows): GITHUB_TOKEN→GH_MIRROR_TOKEN (reserved name) [skip bump]
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
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
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 / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
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 / 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
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
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 / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
2026-05-28 19:32:36 +00:00
Jonathan Miller aa03286613 Merge remote-tracking branch 'origin/main' into dev
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
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
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: 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
Generic: Repo Health / Release configuration (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
2026-05-28 14:29:06 -05:00
Moko Consulting f20cbdd720 chore(workflows): sync all universal workflows from moko-platform [skip bump]
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
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
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]
Universal: Auto Version Bump / Version Bump (push) Has been skipped
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-28 14:23:14 -05:00
Moko Consulting dfd50cc48d fix(workflows): proper suffix handling — use version_set_platform instead of sed [skip bump]
Universal: Auto Version Bump / Version Bump (push) Has been skipped
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-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 / 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
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 / 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
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
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 / 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
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
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
# 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 / Access control (push) Successful in 2s
Update Server / Update Server (push) Successful in 17s
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 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 / Access control (push) Successful in 2s
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
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
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
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 / 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-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 / 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
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:
#	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 / Access control (push) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Successful in 6s
Update Server / Update Server (push) Failing after 10s
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 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 / 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-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 / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 5s
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
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 / 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-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 / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 6s
Update Server / Update Server (push) Failing after 14s
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
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 / 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-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 / 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
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:
#	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 / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 5s
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
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 / 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-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 / 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
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:
#	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 / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 5s
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
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 / 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
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 / 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
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
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 / 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
- 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
Jonathan Miller 7b5a83c71a fix(ci): strip type prefix from element in auto-release workflow
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 4s
Joomla: Repo Health / Access control (push) Successful in 1s
Joomla: Repo Health / Release configuration (push) Failing after 4s
Joomla: Repo Health / Scripts governance (push) Failing after 4s
Joomla: Repo Health / Repository health (push) Failing after 4s
Prevents pkg_pkg_ duplication in release names, ZIP filenames, and
SHA256 checksums. Uses <packagename> for packages and strips existing
prefixes before prepending type prefix.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 22:59:07 -05:00
gitea-actions[bot] cff932dcbb chore(release): build 02.04.00 [skip ci] 2026-05-25 03:54:55 +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
jmiller 8c9e3e6d44 Merge pull request 'Release 02.03.12: Package structure, site aliases, plugin protection' (#35) 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-24 23:23:39 +00:00
jmiller 74e535c929 Merge pull request 'chore: cascade main → dev (f2b0c2e) [skip ci]' (#34) from main into dev
Universal: Build & Release / Build & Release Pipeline (pull_request) Failing after 11s
chore: cascade main → dev [skip ci]
2026-05-24 22:57:49 +00:00
jmiller efdaaf479a Add RC pre-release trigger to CI workflow
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 2s
Joomla: Repo Health / Access control (push) Successful in 1s
Joomla: Repo Health / Scripts governance (push) Successful in 4s
Joomla: Repo Health / Release configuration (push) Failing after 4s
Joomla: Repo Health / Repository health (push) Failing after 5s
Automatically triggers a release-candidate build when CI lint+tests
pass on a pull request.

Authored-by: Moko Consulting
2026-05-24 22:55:27 +00:00
jmiller f2b0c2e420 Add RC pre-release trigger to PR check workflow
Joomla: Repo Health / Access control (push) Successful in 1s
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 4s
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
Automatically triggers a release-candidate build when a PR passes
branch policy and validation checks.

Authored-by: Moko Consulting
2026-05-24 22:54:37 +00:00
Jonathan Miller 40e6a1f086 chore: cascade main -> dev [skip ci] 2026-05-24 14:49:22 -05:00
Jonathan Miller 7b7dc4a553 chore: slim README to overview, move details to wiki
Joomla: Repo Health / Access control (push) Successful in 1s
Joomla: Repo Health / Release configuration (push) Failing after 6s
Joomla: Repo Health / Scripts governance (push) Successful in 6s
Joomla: Repo Health / Repository health (push) Failing after 7s
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 04:06:09 -05:00
jmiller edfd9fa326 fix: updates.xml targetplatform regex causing Gitea 500 [skip ci] 2026-05-24 09:01:46 +00:00
jmiller 2de4d08430 fix: updates.xml with stable + dev + legacy plugin entries [skip ci] 2026-05-24 08:57:04 +00:00
jmiller 872f55f376 chore: sync updates.xml 02.03.11 from dev [skip ci] 2026-05-24 08:54:22 +00:00
gitea-actions[bot] ee7a42e14b chore: update updates.xml (development: 02.03.12-dev) [skip ci] 2026-05-24 08:54:00 +00:00
jmiller 9f434aefdc chore: sync updates.xml from [skip ci] 2026-05-24 08:54:00 +00:00
gitea-actions[bot] 1000f028d2 chore(version): auto-bump patch 02.03.12 [skip ci] 2026-05-24 08:53:59 +00:00
Jonathan Miller b048b47e7c security: protected status prevents disable/uninstall
Joomla: Repo Health / Access control (push) Successful in 1s
Joomla: Update Server / Update updates.xml (push) Successful in 25s
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 3s
- Set protected=1, locked=0 on MokoWaaS extensions via package script
- Self-healing: plugin checks and restores protected flag each session
- Block non-master disable via plugin list toggle (plugins.publish)
- Block non-master uninstall via installer manage
- Joomla framework natively enforces protected status (greys out toggles)
- Master users can still manage settings and updates

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 03:53:33 -05:00
Jonathan Miller 6e0d5387cf chore: update CHANGELOG for 02.03.10
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 3s
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 03:49:16 -05:00
gitea-actions[bot] 76bbf7ad85 chore: update development channel 02.03.10 [skip ci] 2026-05-24 08:47:22 +00:00
jmiller b1c2b3c92a chore: sync updates.xml 02.03.10 from dev [skip ci] 2026-05-24 08:47:21 +00:00
gitea-actions[bot] a5dc00e056 chore: update development channel 02.03.10 [skip ci] 2026-05-24 08:47:21 +00:00
Jonathan Miller c6475ff29a feat: canonical URLs, alias heartbeats, package migration, cleanup
Joomla: Repo Health / Access control (push) Successful in 1s
Joomla: Update Server / Update updates.xml (push) Failing after 13m55s
Joomla: Repo Health / Scripts governance (push) Successful in 4s
Joomla: Repo Health / Repository health (push) Failing after 4s
Joomla: Repo Health / Release configuration (push) Failing after 5s
- manifest.xml: package-type plugin → package
- Canonical URL injection for alias domains (prevents SEO duplication)
- Heartbeat registration for alias domains (each alias gets Grafana datasource)
- Package script.php: enable plugins on every install/update, heartbeat on postflight
- Remove accidentally committed profile.ps1 and TODO.md
- Add profile.ps1 and TODO.md to .gitignore

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 03:46:23 -05:00
jmiller b7a52cc6a4 chore: sync updates.xml from [skip ci] 2026-05-24 08:38:28 +00:00
gitea-actions[bot] ffd98a19d9 chore: update updates.xml (development: 02.03.10-dev) [skip ci] 2026-05-24 08:38:28 +00:00
gitea-actions[bot] 34469609dd chore(version): auto-bump patch 02.03.10 [skip ci] 2026-05-24 08:38:27 +00:00
Jonathan Miller d766b0568a fix: alias offline uses Joomla native offline mode (template offline.php)
Joomla: Repo Health / Access control (push) Successful in 2s
Joomla: Update Server / Update updates.xml (push) Successful in 25s
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
Instead of rendering a custom HTML page, set config('offline', 1) at
onAfterRoute so Joomla renders the site template's offline.php layout.
Custom offline_message is passed via config if set.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 03:38:01 -05:00
jmiller da4b544da7 chore: sync updates.xml from [skip ci] 2026-05-24 08:35:34 +00:00
gitea-actions[bot] cfea9fac99 chore: update updates.xml (development: 02.03.09-dev) [skip ci] 2026-05-24 08:35:34 +00:00
gitea-actions[bot] 7d6d654d6d chore(version): auto-bump patch 02.03.09 [skip ci] 2026-05-24 08:35:33 +00:00
Jonathan Miller dca452e49d fix: alias detection, offline page, backend redirect working
Joomla: Repo Health / Access control (push) Successful in 1s
Joomla: Update Server / Update updates.xml (push) Successful in 25s
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
- Removed primaryHost check from getCurrentAlias() — just look up
  current host in aliases list directly
- Handle Joomla subform stdClass→array conversion
- Strip trailing slashes from alias domains
- Moved handleSiteAlias() to onAfterRoute (client type resolved)
- Use http_response_code(503) + die() for offline page
- Cast offline value to string for comparison

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 03:35:06 -05:00
jmiller f68a477c56 chore: sync updates.xml 02.03.07 from dev [skip ci] 2026-05-24 08:07:35 +00:00
gitea-actions[bot] b827b3382a chore: update development channel 02.03.07 [skip ci] 2026-05-24 08:07:35 +00:00
gitea-actions[bot] 42841f7335 chore: update development channel 02.03.07 [skip ci] 2026-05-24 08:07:34 +00:00
jmiller 8cfe596754 chore: sync updates.xml from [skip ci] 2026-05-24 04:53:47 +00:00
gitea-actions[bot] 0f354422aa chore: update updates.xml (development: 02.03.07-dev) [skip ci] 2026-05-24 04:53:47 +00:00
gitea-actions[bot] 86aae39be1 chore(version): auto-bump patch 02.03.07 [skip ci] 2026-05-24 04:53:46 +00:00
Jonathan Miller b5e932d78b fix: backend redirect uses primary_domain setting instead of Uri::root
Joomla: Repo Health / Access control (push) Successful in 1s
Joomla: Update Server / Update updates.xml (push) Successful in 25s
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 3s
Uri::root() returns the current request domain, so redirecting from an
alias to Uri::root()/administrator redirects back to the alias. Added
primary_domain field to Site Aliases tab and getPrimaryHost() method
that checks: plugin setting → $live_site → alias exclusion fallback.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 23:53:20 -05:00
jmiller b0e15b8747 fix: correct download URLs and restore stable entry [skip ci] 2026-05-24 04:43:00 +00:00
jmiller 8d232e8c7b chore: sync updates.xml 02.03.05 from dev [skip ci] 2026-05-24 04:40:26 +00:00
jmiller 485322ba08 chore: sync updates.xml from [skip ci] 2026-05-24 04:40:21 +00:00
gitea-actions[bot] c738eb6669 chore: update updates.xml (development: 02.03.06-dev) [skip ci] 2026-05-24 04:40:20 +00:00
gitea-actions[bot] e0f98dc5e2 chore(version): auto-bump patch 02.03.06 [skip ci] 2026-05-24 04:40:19 +00:00
Jonathan Miller ede07c6675 feat: dynamic plugin version + plugin protection (no lock)
Joomla: Repo Health / Access control (push) Successful in 1s
Joomla: Update Server / Update updates.xml (push) Successful in 28s
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
- Read plugin_version from manifest XML instead of hardcoding
- Hide MokoWaaS from plugin/installer list for non-master users
- Block non-master uninstall and disable attempts
- No self-healing lock — master users can still disable if needed

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 23:39:46 -05:00
jmiller fa67ffaa00 fix: correct download URL in updates.xml [skip ci] 2026-05-24 04:27:37 +00:00
gitea-actions[bot] 1fe8422fc0 chore: update development channel 02.03.04 [skip ci] 2026-05-24 04:25:59 +00:00
jmiller f40998fc30 chore: sync updates.xml 02.03.04 from dev [skip ci] 2026-05-24 04:25:58 +00:00
gitea-actions[bot] 6e216de0dc chore: update development channel 02.03.04 [skip ci] 2026-05-24 04:25:58 +00:00
Jonathan Miller 86e40fb978 chore: sync plugin manifest version to 02.03.04 [skip ci]
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 23:25:26 -05:00
gitea-actions[bot] b2f52c191b chore: update development channel 02.03.03 [skip ci] 2026-05-24 04:21:25 +00:00
jmiller fb74a255d3 chore: sync updates.xml 02.03.03 from dev [skip ci] 2026-05-24 04:21:24 +00:00
gitea-actions[bot] 8e1040efee chore: update development channel 02.03.03 [skip ci] 2026-05-24 04:21:24 +00:00
gitea-actions[bot] c203e970b9 chore: update development channel 02.03.02 [skip ci] 2026-05-24 04:16:49 +00:00
jmiller 7e489b072a chore: sync updates.xml 02.03.02 from dev [skip ci] 2026-05-24 04:16:48 +00:00
gitea-actions[bot] bf3c986113 chore: update development channel 02.03.02 [skip ci] 2026-05-24 04:16:48 +00:00
Jonathan Miller 955c08a387 fix(ci): disable pipefail during element detection to prevent SIGPIPE exit
Joomla: Repo Health / Access control (push) Successful in 1s
Joomla: Repo Health / Repository health (push) Failing after 5s
Joomla: Repo Health / Release configuration (push) Failing after 5s
Joomla: Repo Health / Scripts governance (push) Failing after 5s
The find|grep|head pipe under bash pipefail causes SIGPIPE when head
closes the pipe after reading one line, making the step exit with code 1
even though the version bump and push succeeded.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 23:16:22 -05:00
Jonathan Miller 52dbefbb14 fix(ci): version bump after release, not before
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
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 23:13:49 -05:00
jmiller 379ca36613 Merge pull request 'chore: cascade main → dev [skip ci]' (#32) from main into dev
chore: cascade main → dev [skip ci]
2026-05-24 04:11:53 +00:00
jmiller 35ca1af6b8 Merge pull request 'feat: convert to package with webservices API + heartbeat fix' (#31) from dev into main
chore: cascade main → dev [skip ci]
2026-05-24 04:10:09 +00:00
Moko Consulting e9ec664f03 chore: update CHANGELOG for deploy workflow removal
Joomla: Repo Health / Access control (push) Successful in 2s
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 3s
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-24 04:09:59 +00:00
gitea-actions[bot] 6929c636b9 chore(version): bump to 02.03.02 [skip ci] 2026-05-24 04:06:06 +00:00
gitea-actions[bot] 20797d663f chore(version): bump to 02.03.01 [skip ci] 2026-05-24 03:48:44 +00:00
jmiller 11d5fd2019 chore: sync updates.xml from [skip ci] 2026-05-24 03:44:46 +00:00
gitea-actions[bot] 1fa965dddb chore: update updates.xml (development: 02.01.46-dev) [skip ci] 2026-05-24 03:44:45 +00:00
gitea-actions[bot] 5a3ec7d9b1 chore(version): auto-bump patch 02.01.46 [skip ci] 2026-05-24 03:44:44 +00:00
Jonathan Miller aef5ca43f6 chore(version): bump to 02.03.00
Joomla: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 4s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 3s
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Has been skipped
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Has been skipped
Joomla: Extension CI / PHPStan Analysis (pull_request) Has been skipped
Joomla: Repo Health / Access control (pull_request) Successful in 2s
Universal: PR Check / Changelog Updated (pull_request) Failing after 5s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Failing after 7s
Joomla: Update Server / Update updates.xml (push) Successful in 37s
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
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
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 22:44:04 -05:00
jmiller 0631d80fa1 chore: sync updates.xml from [skip ci] 2026-05-24 03:42:32 +00:00
gitea-actions[bot] e84c10b14f chore: update updates.xml (development: 02.01.45-dev) [skip ci] 2026-05-24 03:42:31 +00:00
gitea-actions[bot] 87e543ef1c chore(version): auto-bump patch 02.01.45 [skip ci] 2026-05-24 03:42:31 +00:00
Jonathan Miller 32236ad7ff feat(package): convert to package with webservices API plugin
Joomla: Repo Health / Access control (push) Successful in 1s
Joomla: Update Server / Update updates.xml (push) Successful in 26s
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
Restructure MokoWaaS from a standalone system plugin into a Joomla
package containing:

- plg_system_mokowaas — existing system plugin (moved to packages/)
- com_mokowaas — minimal API-only component with health, cache, and
  update controllers for Joomla Web Services API
- plg_webservices_mokowaas — registers /api/v1/mokowaas/* routes

Package script auto-enables both plugins on fresh install.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 22:42:02 -05:00
jmiller d6e462f3b7 Merge pull request 'chore: cascade main → dev (d470669) [skip ci]' (#30) from main into dev
chore: cascade main → dev [skip ci]
2026-05-24 03:40:53 +00:00
jmiller d470669634 chore: remove deploy workflow — switching to Joomla update server method
Joomla: Repo Health / Access control (push) Successful in 2s
Universal: Cascade Main → Dev / Cascade main → branches (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
2026-05-24 03:40:50 +00:00
jmiller feaccf0758 fix: updates.xml add stable channel entry [skip ci] 2026-05-24 03:39:45 +00:00
Jonathan Miller 9542c88ba4 fix: updates.xml missing stable entry — add both stable and dev channels
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 pre-release workflow overwrites updates.xml with only the dev entry,
removing the stable channel. Joomla updater on sites set to stable channel
cannot see any updates.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 22:38:16 -05:00
jmiller 7e3d366043 chore: sync updates.xml 02.01.44 from dev [skip ci] 2026-05-24 03:33:54 +00:00
gitea-actions[bot] 4c5919f209 chore: update development channel 02.01.44 [skip ci] 2026-05-24 03:33:53 +00:00
gitea-actions[bot] 0b3e699f29 chore(version): bump to 02.01.44 [skip ci] 2026-05-24 03:33:52 +00:00
jmiller 406242fb7d chore: sync updates.xml 02.02.00 [skip ci] 2026-05-24 03:08:12 +00:00
jmiller e6cb6eb531 Merge pull request 'chore: cascade main → dev (bdceb42) [skip ci]' (#28) from main into dev
chore: cascade main → dev [skip ci]
2026-05-23 23:39:10 +00:00
132 changed files with 11197 additions and 10206 deletions
+7
View File
@@ -93,6 +93,11 @@ sftp-settings.json
.replit
replit.md
# ============================================================
# Update server (generated dynamically by MokoGitea)
# ============================================================
updates.xml
# ============================================================
# Archives / release artifacts
# ============================================================
@@ -203,3 +208,5 @@ venv/
*.coverage
hypothesis/
profile.ps1
TODO.md
+25 -13
View File
@@ -154,7 +154,7 @@ The version in `README.md` **must always match** the `<version>` tag in `manifes
```
MokoWaaS/
├── manifest.xml # Joomla installer manifest (root — required)
├── updates.xml # Update server manifest (root — required, see below)
├── (no updates.xml) # Update XML is generated dynamically by MokoGitea
├── site/ # Frontend (site) code
│ ├── controller.php
│ ├── controllers/
@@ -183,24 +183,35 @@ MokoWaaS/
---
## updates.xml — Required in Repo Root
## Update Server — MokoGitea Dynamic Endpoint
`updates.xml` **must exist at the repository root**. It is the Joomla update server manifest that allows Joomla installations to check for new versions of this extension.
`updates.xml` is **NOT** stored in the repo. MokoGitea generates the update XML dynamically from git releases at:
The `manifest.xml` must reference it via:
```
https://git.mokoconsulting.tech/{Owner}/{Repo}/updates.xml
```
The package manifest (`pkg_mokowaas.xml`) references it via:
```xml
<updateservers>
<server type="extension" priority="1" name="{{EXTENSION_NAME}}">
https://github.com/mokoconsulting-tech/MokoWaaS/raw/main/updates.xml
<server type="extension" priority="1" name="MokoWaaS Update Server">
https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/updates.xml
</server>
</updateservers>
```
**License Key (Download Key):**
- MokoGitea's endpoint validates license keys passed as `?dlid=MOKO-XXXX-XXXX-XXXX-XXXX`
- The generated XML includes `<downloadkey prefix="dlid=" suffix="" />` to tell Joomla a key is required
- Users enter the download key via Joomla's native **System → Update Sites** interface
- Joomla stores the key in `#__update_sites.extra_query` and appends it to all update/download requests
- Invalid/expired keys receive an empty `<updates></updates>` response
**Rules:**
- Every release must prepend a new `<update>` block at the top of `updates.xml` — old entries must be preserved below.
- The `<version>` in `updates.xml` must exactly match `<version>` in `manifest.xml` and the version in `README.md`.
- The `<downloadurl>` must be a publicly accessible direct download link (GitHub Releases asset URL).
- `<targetplatform name="joomla" version="4\.[0-9]+">` — the backslash is a **literal backslash character** in the XML attribute value; Joomla's update-server parser treats the value as a regular expression, so `\.` matches a literal dot and `[0-9]+` matches one or more digits. Do not double-escape it.
- Do NOT create or commit a static `updates.xml` — MokoGitea generates it from releases
- The `<version>` in release tags must match `<version>` in the manifest and `README.md`
- Release assets (ZIPs) must be attached to git releases — MokoGitea uses them for `<downloadurl>`
- `<targetplatform name="joomla" version="(5|6)\..*">` — the backslash is a **literal backslash character** in the XML attribute value; Joomla's update-server parser treats the value as a regular expression
---
@@ -286,8 +297,8 @@ Approved prefixes: `dev/` · `rc/` · `version/` · `patch/` · `copilot/` · `d
| Change type | Documentation to update |
|-------------|------------------------|
| New or renamed PHP class/method | PHPDoc block; `docs/api/` entry |
| New or changed manifest.xml | Update `updates.xml` version; bump README.md version |
| New release | Prepend `<update>` block to `updates.xml`; update CHANGELOG.md; bump README.md version |
| New or changed manifest.xml | Bump README.md version |
| New release | Create git release with ZIP asset; update CHANGELOG.md; bump README.md version |
| New or changed workflow | `docs/workflows/<workflow-name>.md` |
| Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block |
| **Every PR** | **Bump the patch version** — increment `XX.YY.ZZ` in `README.md`; `sync-version-on-merge` propagates it |
@@ -301,4 +312,5 @@ Approved prefixes: `dev/` · `rc/` · `version/` · `patch/` · `copilot/` · `d
- Never add `defined('_JEXEC') or die;` to CLI scripts or model tests — only to web-accessible PHP files
- Never hardcode version numbers in body text — update `README.md` and let automation propagate
- Never use `github.token` or `secrets.GITHUB_TOKEN` in workflows — always use `secrets.GH_TOKEN`
- Never let `manifest.xml` version, `updates.xml` version, and `README.md` version go out of sync
- Never let `manifest.xml` version and `README.md` version go out of sync
- Never commit a static `updates.xml` — the update feed is generated dynamically by MokoGitea
-949
View File
@@ -1,949 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Release
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
# PATH: /templates/workflows/joomla/auto-release.yml.template
# VERSION: 04.06.00
# BRIEF: Joomla build & release — ZIP package, updates.xml, SHA-256 checksum
#
# +========================================================================+
# | BUILD & RELEASE PIPELINE (JOOMLA) |
# +========================================================================+
# | |
# | Triggers on push to main (skips bot commits + [skip ci]): |
# | |
# | Every push: |
# | 1. Read version from README.md |
# | 3. Set platform version (Joomla <version>) |
# | 4. Update [VERSION: XX.YY.ZZ] badges in markdown files |
# | 5. Write updates.xml (Joomla update server XML) |
# | 6. Create git tag vXX.YY.ZZ |
# | 7a. Patch: update existing Gitea Release for this minor |
# | 8. Build ZIP, upload asset, write SHA-256 to updates.xml |
# | |
# | Every version change: archives main -> version/XX.YY branch |
# | All patches release (including 00). Patch 00/01 = full pipeline. |
# | First release only (patch == 01): |
# | 7b. Create new Gitea Release |
# | |
# | GitHub mirror: stable/rc releases only (continue-on-error) |
# | |
# +========================================================================+
name: Build & Release
on:
pull_request:
types: [closed]
branches:
- main
paths:
- 'src/**'
- 'htdocs/**'
workflow_dispatch:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
permissions:
contents: write
jobs:
release:
name: Build & Release Pipeline
runs-on: release
if: >-
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch'
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.GA_TOKEN }}
fetch-depth: 0
- name: Setup MokoStandards tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN }}"}}'
run: |
# Ensure PHP + Composer are available
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
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
/tmp/mokostandards-api
cd /tmp/mokostandards-api
composer install --no-dev --no-interaction --quiet
# -- STEP 1: Read version -----------------------------------------------
- name: "Step 1: Read version from README.md"
id: version
run: |
VERSION=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null)
if [ -z "$VERSION" ]; then
echo "No VERSION in README.md — skipping release"
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
# Derive major.minor for branch naming (patches update existing branch)
MINOR=$(echo "$VERSION" | awk -F. '{printf "%s.%s", $1, $2}')
PATCH=$(echo "$VERSION" | awk -F. '{print $3}')
MAJOR=$(echo "$VERSION" | awk -F. '{print $1}')
MINOR_NUM=$(echo "$VERSION" | awk -F. '{print $2}')
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "branch=version/${MAJOR}" >> "$GITHUB_OUTPUT"
echo "minor=$MINOR" >> "$GITHUB_OUTPUT"
echo "major=$MAJOR" >> "$GITHUB_OUTPUT"
echo "release_tag=stable" >> "$GITHUB_OUTPUT"
echo "stability=stable" >> "$GITHUB_OUTPUT"
echo "skip=false" >> "$GITHUB_OUTPUT"
if [ "$PATCH" = "00" ] || [ "$PATCH" = "01" ]; then
echo "is_minor=true" >> "$GITHUB_OUTPUT"
echo "Version: $VERSION (first release for this minor — full pipeline)"
else
echo "is_minor=false" >> "$GITHUB_OUTPUT"
echo "Version: $VERSION (patch — platform version + badges only)"
fi
# -- STEP 1b: Bump minor version (stable = minor bump, reset patch) ------
- name: "Step 1b: Bump minor version for stable release"
if: steps.version.outputs.skip != 'true'
id: bump
run: |
CURRENT=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1)
[ -z "$CURRENT" ] && { echo "skip=true" >> "$GITHUB_OUTPUT"; exit 0; }
MAJOR=$((10#$(echo "$CURRENT" | cut -d. -f1)))
MINOR=$((10#$(echo "$CURRENT" | cut -d. -f2)))
# Minor bump, reset patch. Rollover if minor > 99
MINOR=$((MINOR + 1))
if [ $MINOR -gt 99 ]; then
MINOR=0
MAJOR=$((MAJOR + 1))
fi
VERSION=$(printf "%02d.%02d.00" $MAJOR $MINOR)
TODAY=$(date +%Y-%m-%d)
echo "Stable bump: ${CURRENT} → ${VERSION} (minor)"
# Update README.md
sed -i "s/VERSION:[[:space:]]*${CURRENT}/VERSION: ${VERSION}/" README.md
# Update manifest
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
if [ -n "$MANIFEST" ]; then
MANIFEST_VER=$(sed -n 's/.*<version>\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1)
[ -n "$MANIFEST_VER" ] && sed -i "s|<version>${MANIFEST_VER}</version>|<version>${VERSION}</version>|" "$MANIFEST"
sed -i "s|<creationDate>[^<]*</creationDate>|<creationDate>${TODAY}</creationDate>|" "$MANIFEST"
fi
# Promote [Unreleased] section in CHANGELOG.md to new version
if [ -f "CHANGELOG.md" ] && grep -qi "Unreleased" CHANGELOG.md; then
sed -i "s|## \[Unreleased\]|## [${VERSION}] --- ${TODAY}|" CHANGELOG.md
sed -i "s|## Unreleased|## [${VERSION}] --- ${TODAY}|" CHANGELOG.md
sed -i "2i ## [Unreleased]" CHANGELOG.md
sed -i "3i \\ " CHANGELOG.md
echo "CHANGELOG promoted to [${VERSION}]"
fi
# Commit and push
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
git add -A
git diff --cached --quiet || {
git commit -m "chore(version): bump ${CURRENT} → ${VERSION} [skip ci]"
git push origin HEAD:main 2>&1
}
# Override version output for rest of pipeline
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "major=$(printf "%02d" $MAJOR)" >> "$GITHUB_OUTPUT"
- name: Check if already released
if: steps.version.outputs.skip != 'true'
id: check
run: |
TAG="${{ steps.version.outputs.release_tag }}"
BRANCH="${{ steps.version.outputs.branch }}"
TAG_EXISTS=false
BRANCH_EXISTS=false
git rev-parse "$TAG" >/dev/null 2>&1 && TAG_EXISTS=true
git ls-remote --heads origin "$BRANCH" 2>/dev/null | grep -q "$BRANCH" && BRANCH_EXISTS=true
echo "tag_exists=$TAG_EXISTS" >> "$GITHUB_OUTPUT"
echo "branch_exists=$BRANCH_EXISTS" >> "$GITHUB_OUTPUT"
# Tag and branch may persist across patch releases — never skip
echo "already_released=false" >> "$GITHUB_OUTPUT"
# -- SANITY CHECKS -------------------------------------------------------
- name: "Sanity: Pre-release validation"
if: >-
steps.version.outputs.skip != 'true' &&
steps.check.outputs.already_released != 'true'
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
ERRORS=0
echo "## Pre-Release Sanity Checks (Joomla)" >> $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
# -- Joomla: manifest version drift --------
MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
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
fi
# -- Joomla: XML manifest existence --------
if [ -z "$MANIFEST" ]; then
echo "- No Joomla XML manifest found" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
else
echo "- Manifest: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY
# -- Joomla: extension type check --------
TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" 2>/dev/null)
echo "- Extension type: ${TYPE:-unknown}" >> $GITHUB_STEP_SUMMARY
fi
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 ---------------
# Always runs — every version change on main archives to version/XX.YY
- name: "Step 2: Version archive branch"
if: steps.check.outputs.already_released != 'true'
run: |
BRANCH="${{ steps.version.outputs.branch }}"
IS_MINOR="${{ steps.version.outputs.is_minor }}"
PATCH="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
PATCH_NUM=$(echo "$PATCH" | awk -F. '{print $3}')
# Check if branch exists
if git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then
git push origin HEAD:"$BRANCH" --force
echo "Updated archive branch: ${BRANCH} (patch ${PATCH_NUM})" >> $GITHUB_STEP_SUMMARY
else
git checkout -b "$BRANCH" 2>/dev/null || git checkout "$BRANCH"
git push origin "$BRANCH" --force
echo "Created archive branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
fi
# -- STEP 3: Set platform version ----------------------------------------
- name: "Step 3: Set platform version"
if: >-
steps.version.outputs.skip != 'true' &&
steps.check.outputs.already_released != 'true'
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
php /tmp/mokostandards-api/cli/version_set_platform.php \
--path . --version "$VERSION" --branch main
# -- STEP 4: Update version badges ----------------------------------------
- name: "Step 4: Update version badges"
if: >-
steps.version.outputs.skip != 'true' &&
steps.check.outputs.already_released != 'true'
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
find . -name "*.md" ! -path "./.git/*" ! -path "./vendor/*" | while read -r f; do
if grep -q '\[VERSION:' "$f" 2>/dev/null; then
sed -i "s/\[VERSION:[[:space:]]*[0-9]\{2\}\.[0-9]\{2\}\.[0-9]\{2\}\]/[VERSION: ${VERSION}]/" "$f"
fi
done
# -- STEP 5: Write updates.xml (Joomla update server) ---------------------
- name: "Step 5: Write updates.xml"
id: updates
if: >-
steps.version.outputs.skip != 'true' &&
steps.check.outputs.already_released != 'true'
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
REPO="${{ github.repository }}"
# -- Parse extension metadata from XML manifest ----------------
MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
if [ -z "$MANIFEST" ]; then
echo "Warning: No Joomla XML manifest found — skipping updates.xml" >> $GITHUB_STEP_SUMMARY
exit 0
fi
# Extract fields using sed (portable — no grep -P)
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)
TARGET_PLATFORM=$(sed -n 's/.*\(<targetplatform[^/]*\/>\).*/\1/p' "$MANIFEST" | head -1)
PHP_MINIMUM=$(sed -n 's/.*<php_minimum>\([^<]*\)<\/php_minimum>.*/\1/p' "$MANIFEST" | head -1)
# If EXT_NAME is a language key (e.g. PLG_SYSTEM_MOKOJGDPC), resolve from .ini
if echo "$EXT_NAME" | grep -qE '^[A-Z_]+$'; then
INI_NAME=$(find . -name "*.sys.ini" -path "*/en-GB/*" -exec grep -h "^${EXT_NAME}=" {} \; 2>/dev/null | head -1 | cut -d'"' -f2)
[ -z "$INI_NAME" ] && INI_NAME=$(find . -name "*.sys.ini" -exec grep -h "^${EXT_NAME}=" {} \; 2>/dev/null | head -1 | cut -d'"' -f2)
[ -n "$INI_NAME" ] && EXT_NAME="$INI_NAME"
fi
# Fallbacks
[ -z "$EXT_NAME" ] && EXT_NAME="${{ github.event.repository.name }}"
[ -z "$EXT_TYPE" ] && EXT_TYPE="component"
# Derive element if not in manifest:
# 1. plugin="xxx" attribute (plugins)
# 2. module="xxx" attribute (modules)
# 3. XML filename (components, packages)
# 4. Repo name fallback (templates, anything else)
if [ -z "$EXT_ELEMENT" ]; then
EXT_ELEMENT=$(sed -n 's/.*plugin="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
fi
if [ -z "$EXT_ELEMENT" ]; then
EXT_ELEMENT=$(sed -n 's/.*module="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
fi
if [ -z "$EXT_ELEMENT" ]; then
FNAME=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]')
# If filename is generic (templateDetails, manifest), use repo name
case "$FNAME" in
templatedetails|manifest) EXT_ELEMENT=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;;
*) EXT_ELEMENT="$FNAME" ;;
esac
fi
# Final fallback
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
# Save for Steps 7, 8, 8b
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
echo "ext_name=${EXT_NAME}" >> "$GITHUB_OUTPUT"
echo "ext_type=${EXT_TYPE}" >> "$GITHUB_OUTPUT"
echo "ext_folder=${EXT_FOLDER}" >> "$GITHUB_OUTPUT"
# Build client tag: plugins and frontend modules need <client>site</client>
CLIENT_TAG=""
if [ -n "$EXT_CLIENT" ]; then
CLIENT_TAG="<client>${EXT_CLIENT}</client>"
elif [ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]; then
CLIENT_TAG="<client>site</client>"
fi
# Build folder tag for plugins (required for Joomla to match the update)
FOLDER_TAG=""
if [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ]; then
FOLDER_TAG="<folder>${EXT_FOLDER}</folder>"
fi
# Build targetplatform (fallback to Joomla 5 if not in manifest)
if [ -z "$TARGET_PLATFORM" ]; then
TARGET_PLATFORM=$(printf '<targetplatform name="joomla" version="((5.[0-9])|(6.[0-9]))" %s>' "/")
fi
# Build php_minimum tag
PHP_TAG=""
if [ -n "$PHP_MINIMUM" ]; then
PHP_TAG="<php_minimum>${PHP_MINIMUM}</php_minimum>"
fi
# Build TYPE_PREFIX for download URL
TYPE_PREFIX=""
case "${EXT_TYPE}" in
plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;;
module) TYPE_PREFIX="mod_" ;;
component) TYPE_PREFIX="com_" ;;
template) TYPE_PREFIX="tpl_" ;;
library) TYPE_PREFIX="lib_" ;;
package) TYPE_PREFIX="pkg_" ;;
esac
DOWNLOAD_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/stable/${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip"
INFO_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/stable"
# -- Build update entry for a given stability tag
build_entry() {
local TAG_NAME="$1"
printf '%s\n' ' <update>'
printf '%s\n' " <name>${EXT_NAME}</name>"
printf '%s\n' " <description>${EXT_NAME} update</description>"
printf '%s\n' " <element>${EXT_ELEMENT}</element>"
printf '%s\n' " <type>${EXT_TYPE}</type>"
printf '%s\n' " <version>${VERSION}</version>"
[ -n "$CLIENT_TAG" ] && printf '%s\n' " ${CLIENT_TAG}"
[ -n "$FOLDER_TAG" ] && printf '%s\n' " ${FOLDER_TAG}"
printf '%s\n' " <tags><tag>${TAG_NAME}</tag></tags>"
printf '%s\n' " <infourl title=\"${EXT_NAME}\">${INFO_URL}</infourl>"
printf '%s\n' ' <downloads>'
printf '%s\n' " <downloadurl type=\"full\" format=\"zip\">${DOWNLOAD_URL}</downloadurl>"
printf '%s\n' ' </downloads>'
printf '%s\n' " ${TARGET_PLATFORM}"
[ -n "$PHP_TAG" ] && printf '%s\n' " ${PHP_TAG}"
printf '%s\n' ' <maintainer>Moko Consulting</maintainer>'
printf '%s\n' ' <maintainerurl>https://mokoconsulting.tech</maintainerurl>'
printf '%s\n' ' </update>'
}
# -- Write updates.xml with cascading channels
# Stable release updates ALL channels (development, alpha, beta, rc, stable)
{
printf '%s\n' "<?xml version='1.0' encoding='UTF-8'?>"
printf '%s\n' "<!-- Copyright (C) $(date +%Y) Moko Consulting <hello@mokoconsulting.tech>"
printf '%s\n' " SPDX-License-Identifier: GPL-3.0-or-later"
printf '%s\n' " VERSION: ${VERSION}"
printf '%s\n' " -->"
printf '%s\n' ""
printf '%s\n' '<updates>'
build_entry "development"
build_entry "alpha"
build_entry "beta"
build_entry "rc"
build_entry "stable"
printf '%s\n' '</updates>'
} > updates.xml
echo "updates.xml: ${VERSION} (all channels updated to stable)" >> $GITHUB_STEP_SUMMARY
# -- Commit all changes ---------------------------------------------------
- name: Commit release changes
if: >-
steps.version.outputs.skip != 'true' &&
steps.check.outputs.already_released != 'true'
run: |
if git diff --quiet && git diff --cached --quiet; then
echo "No changes to commit"
exit 0
fi
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 commit -m "chore(release): build ${VERSION} [skip ci]" \
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
git push -u origin HEAD
# -- STEP 6: Create tag ---------------------------------------------------
- name: "Step 6: Create git tag"
if: >-
steps.version.outputs.skip != 'true' &&
steps.check.outputs.tag_exists != 'true' &&
steps.version.outputs.is_minor == 'true'
run: |
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
# Only create the major release tag if it doesn't exist yet
if ! git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then
git tag "$RELEASE_TAG"
git push origin "$RELEASE_TAG"
echo "Tag created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
else
echo "Tag ${RELEASE_TAG} already exists" >> $GITHUB_STEP_SUMMARY
fi
echo "Tag: ${TAG}" >> $GITHUB_STEP_SUMMARY
# -- STEP 7: Create or update Gitea Release --------------------------------
- name: "Step 7: Gitea Release"
if: >-
steps.version.outputs.skip != 'true'
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
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}"
# Reuse metadata from Step 5 (single source of truth)
EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}"
EXT_NAME="${{ steps.updates.outputs.ext_name }}"
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/mokostandards-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)"
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 Joomla package and update checksum"
if: >-
steps.version.outputs.skip != 'true'
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
REPO="${{ github.repository }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
# All ZIPs upload to the major release tag (vXX)
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
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)
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/ — skipping package"; exit 0; }
EXCLUDES=".ftpignore sftp-config* *.ppk *.pem *.key .env*"
# ZIP package
cd "$SOURCE_DIR"
zip -r "/tmp/${ZIP_NAME}" . -x $EXCLUDES
cd ..
# tar.gz package
tar -czf "/tmp/${TAR_NAME}" -C "$SOURCE_DIR" \
--exclude='.ftpignore' --exclude='sftp-config*' \
--exclude='*.ppk' --exclude='*.pem' --exclude='*.key' --exclude='.env*' .
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 "### Joomla 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'
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}"
EXT_TYPE="${{ steps.updates.outputs.ext_type }}"
EXT_FOLDER="${{ steps.updates.outputs.ext_folder }}"
# Build TYPE_PREFIX to match Step 8's ZIP naming
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) --------------------------------
- name: "Step 9: Mirror release to GitHub"
if: >-
steps.version.outputs.skip != 'true' &&
steps.version.outputs.stability == 'stable' &&
secrets.GH_TOKEN != ''
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
MAJOR="${{ steps.version.outputs.major }}"
BRANCH="${{ steps.version.outputs.branch }}"
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
NOTES=$(php /tmp/mokostandards-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null || true)
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
echo "$NOTES" > /tmp/release_notes.md
EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/tags/$RELEASE_TAG" 2>/dev/null | jq -r ".tag_name // empty" || true)
if [ -z "$EXISTING" ]; then
gh release create "$RELEASE_TAG" \
--repo "$GH_REPO" \
--title "v${MAJOR} (latest: ${VERSION})" \
--notes-file /tmp/release_notes.md \
--target "$BRANCH" || true
else
gh release edit "$RELEASE_TAG" \
--repo "$GH_REPO" \
--title "v${MAJOR} (latest: ${VERSION})" || true
fi
# Upload assets to GitHub mirror
for PKG in /tmp/${EXT_ELEMENT:-pkg}-${VERSION}.*; do
if [ -f "$PKG" ]; then
_RELID=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/tags/$RELEASE_TAG" 2>/dev/null | jq -r ".id // empty")
[ -n "$_RELID" ] && curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" -H "Content-Type: application/octet-stream" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/${_RELID}/assets?name=$(basename $PKG)" --data-binary "@$PKG" > /dev/null 2>&1 || true
fi
done
echo "GitHub mirror updated: ${GH_REPO} ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
# -- STEP 10: Sync main branch to GitHub mirror ----------------------------
- name: "Step 10: Push main to GitHub mirror"
if: >-
steps.version.outputs.skip != 'true' &&
secrets.GH_TOKEN != ''
continue-on-error: true
run: |
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
git remote add github "https://x-access-token:${{ secrets.GH_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
git remote set-url github "https://x-access-token:${{ secrets.GH_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
git fetch origin main --depth=1
git push github origin/main:refs/heads/main --force 2>/dev/null \
&& echo "main branch pushed to GitHub mirror" \
|| echo "WARNING: GitHub mirror push failed"
# -- Clean up lesser pre-releases (cascade) ---------------------------------
# stable → deletes all | rc → beta,alpha,dev | beta → alpha,dev | alpha → dev
- name: "Delete lesser pre-release channels"
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.GA_TOKEN }}"
# Stable deletes all pre-release channels
TAGS_TO_DELETE="development alpha beta release-candidate"
DELETED=0
for TAG in $TAGS_TO_DELETE; do
RELEASE_ID=$(curl -sS -H "Authorization: token ${TOKEN}" \
"${API_BASE}/releases/tags/${TAG}" 2>/dev/null | \
python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "None" ]; then
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
"${API_BASE}/releases/${RELEASE_ID}" 2>/dev/null || true
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
"${API_BASE}/tags/${TAG}" 2>/dev/null || true
echo "Deleted: ${TAG} (id: ${RELEASE_ID})"
DELETED=$((DELETED + 1))
fi
done
echo "Cleaned up ${DELETED} pre-release channel(s)" >> $GITHUB_STEP_SUMMARY
# -- STEP 11: Reset dev branch from main ------------------------------------
- name: "Step 11: Delete and recreate dev branch from main"
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.GA_TOKEN }}"
# Delete dev branch
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
"${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
# Recreate dev from main (now includes version bump + changelog promotion)
curl -sf -X POST -H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
"${API_BASE}/branches" \
-d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
echo "Dev branch reset from main (keeps dev ahead after release)" >> $GITHUB_STEP_SUMMARY
# -- Summary --------------------------------------------------------------
- name: Pipeline Summary
if: always()
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
else
echo "" >> $GITHUB_STEP_SUMMARY
echo "## Build & Release Complete (Joomla)" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
fi
-213
View File
@@ -1,213 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Maintenance
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
# PATH: /templates/workflows/cascade-dev.yml.template
# VERSION: 02.00.00
# BRIEF: Forward-merge main → all open branches after every push to main
#
# +========================================================================+
# | CASCADE MAIN → ALL BRANCHES |
# +========================================================================+
# | |
# | Triggers on every push to main (PR merges, bot commits, etc.) |
# | |
# | 1. List all branches matching: dev, rc/*, beta/*, alpha/* |
# | 2. For each: create PR (main → branch), auto-merge if clean |
# | 3. On conflict: leave PR open for manual resolution |
# | |
# +========================================================================+
name: Cascade Main → Dev
on:
push:
branches:
- main
workflow_dispatch:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
permissions:
contents: write
pull-requests: write
jobs:
cascade:
name: Cascade main → branches
runs-on: ubuntu-latest
if: >-
!contains(github.event.head_commit.message, '[skip ci]') &&
!contains(github.event.head_commit.message, '[skip cascade]')
steps:
- name: Discover target branches
id: branches
env:
GA_TOKEN: ${{ secrets.GA_TOKEN }}
run: |
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
# Fetch all branches (paginated)
PAGE=1
ALL_BRANCHES=""
while true; do
BATCH=$(curl -sS \
-H "Authorization: token ${GA_TOKEN}" \
"${API}/branches?page=${PAGE}&limit=50" \
| jq -r '.[].name // empty')
[ -z "$BATCH" ] && break
ALL_BRANCHES="$ALL_BRANCHES $BATCH"
PAGE=$((PAGE + 1))
done
# Filter to cascade targets: dev, dev/*, rc/*, beta/*, alpha/*
TARGETS=""
for BRANCH in $ALL_BRANCHES; do
case "$BRANCH" in
dev|dev/*|rc/*|beta/*|alpha/*)
TARGETS="$TARGETS $BRANCH"
;;
esac
done
TARGETS=$(echo "$TARGETS" | xargs) # trim whitespace
if [ -z "$TARGETS" ]; then
echo "targets=" >> "$GITHUB_OUTPUT"
echo "️ No cascade target branches found"
else
echo "targets=$TARGETS" >> "$GITHUB_OUTPUT"
COUNT=$(echo "$TARGETS" | wc -w)
echo "📋 Found ${COUNT} target branch(es): ${TARGETS}"
fi
- name: Cascade to all target branches
if: steps.branches.outputs.targets != ''
env:
GA_TOKEN: ${{ secrets.GA_TOKEN }}
run: |
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
SHORT_SHA="${GITHUB_SHA:0:7}"
TARGETS="${{ steps.branches.outputs.targets }}"
SUCCESS=0
CONFLICTS=0
SKIPPED=0
FAILED=0
for BRANCH in $TARGETS; do
echo ""
echo "═══ main → ${BRANCH} ═══"
# Check if branch is already up to date
ENCODED_BRANCH=$(echo "$BRANCH" | sed 's|/|%2F|g')
RESPONSE=$(curl -sS \
-H "Authorization: token ${GA_TOKEN}" \
"${API}/compare/${ENCODED_BRANCH}...main")
AHEAD=$(echo "$RESPONSE" | jq '.total_commits // 0')
if [ "$AHEAD" -eq 0 ]; then
echo " ✅ Already up to date"
SKIPPED=$((SKIPPED + 1))
continue
fi
echo " ️ main is ${AHEAD} commit(s) ahead"
# Check for existing cascade PR
EXISTING=$(curl -sS \
-H "Authorization: token ${GA_TOKEN}" \
"${API}/pulls?state=open&head=${GITEA_ORG}:main&base=${ENCODED_BRANCH}&limit=1")
EXISTING_COUNT=$(echo "$EXISTING" | jq 'length')
PR_NUMBER=""
if [ "$EXISTING_COUNT" -gt 0 ]; then
PR_NUMBER=$(echo "$EXISTING" | jq -r '.[0].number')
echo " ️ Reusing existing PR #${PR_NUMBER}"
else
# Create cascade PR
PR_RESPONSE=$(curl -sS -w "\n%{http_code}" \
-X POST \
-H "Authorization: token ${GA_TOKEN}" \
-H "Content-Type: application/json" \
-d "{
\"title\": \"chore: cascade main → ${BRANCH} (${SHORT_SHA}) [skip ci]\",
\"body\": \"## Automatic cascade\\n\\nForward-merging \`main\` (${SHORT_SHA}) into \`${BRANCH}\`.\\n\\nIf conflicts exist, resolve manually and merge.\\n\\n> Auto-created by **Cascade Main → Dev**.\",
\"head\": \"main\",
\"base\": \"${BRANCH}\"
}" \
"${API}/pulls")
HTTP_CODE=$(echo "$PR_RESPONSE" | tail -1)
BODY=$(echo "$PR_RESPONSE" | sed '$d')
PR_NUMBER=$(echo "$BODY" | jq -r '.number // empty')
if [ "$HTTP_CODE" != "201" ] || [ -z "$PR_NUMBER" ]; then
MSG=$(echo "$BODY" | jq -r '.message // .' 2>/dev/null | head -1)
echo " ❌ Failed to create PR (HTTP ${HTTP_CODE}): ${MSG}"
FAILED=$((FAILED + 1))
continue
fi
echo " ✅ Created PR #${PR_NUMBER}"
fi
# Try auto-merge
PR_DATA=$(curl -sS \
-H "Authorization: token ${GA_TOKEN}" \
"${API}/pulls/${PR_NUMBER}")
MERGEABLE=$(echo "$PR_DATA" | jq -r '.mergeable // false')
if [ "$MERGEABLE" != "true" ]; then
echo " ⚠️ Conflicts — PR #${PR_NUMBER} left open"
CONFLICTS=$((CONFLICTS + 1))
continue
fi
MERGE_RESPONSE=$(curl -sS -w "\n%{http_code}" \
-X POST \
-H "Authorization: token ${GA_TOKEN}" \
-H "Content-Type: application/json" \
-d "{
\"Do\": \"merge\",
\"merge_message_field\": \"chore: cascade main → ${BRANCH} [skip ci]\",
\"delete_branch_after_merge\": false
}" \
"${API}/pulls/${PR_NUMBER}/merge")
MERGE_HTTP=$(echo "$MERGE_RESPONSE" | tail -1)
if [ "$MERGE_HTTP" = "200" ] || [ "$MERGE_HTTP" = "204" ]; then
echo " ✅ Merged — ${BRANCH} is in sync"
SUCCESS=$((SUCCESS + 1))
else
MERGE_BODY=$(echo "$MERGE_RESPONSE" | sed '$d')
echo " ⚠️ Merge failed (HTTP ${MERGE_HTTP}) — PR #${PR_NUMBER} left open"
CONFLICTS=$((CONFLICTS + 1))
fi
done
# Summary
echo ""
echo "════════════════════════════════════════"
echo " ✅ Merged: ${SUCCESS}"
echo " ⚠️ Conflicts: ${CONFLICTS}"
echo " ⏭️ Up to date: ${SKIPPED}"
echo " ❌ Failed: ${FAILED}"
echo "════════════════════════════════════════"
if [ "$FAILED" -gt 0 ]; then
exit 1
fi
-450
View File
@@ -1,450 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# This file is part of a Moko Consulting project.
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow.Template
# INGROUP: MokoStandards.CI
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
# PATH: /templates/workflows/joomla/ci-joomla.yml.template
# VERSION: 04.06.00
# BRIEF: CI workflow for Joomla extensions — lint, validate, test
name: Joomla Extension CI
on:
pull_request:
branches:
- main
- 'dev/**'
workflow_dispatch:
permissions:
contents: read
pull-requests: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
lint-and-validate:
name: Lint & Validate
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup PHP
run: |
php -v && composer --version
- name: Clone MokoStandards
env:
GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
run: |
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
/tmp/mokostandards-api
- name: Install dependencies
env:
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
run: |
if [ -f "composer.json" ]; then
composer install \
--no-interaction \
--prefer-dist \
--optimize-autoloader
else
echo "No composer.json found — skipping dependency install"
fi
- name: PHP syntax check
run: |
ERRORS=0
for DIR in src/ htdocs/; do
if [ -d "$DIR" ]; then
FOUND=1
while IFS= read -r -d '' FILE; do
OUTPUT=$(php -l "$FILE" 2>&1)
if echo "$OUTPUT" | grep -q "Parse error"; then
echo "::error file=${FILE}::${OUTPUT}"
ERRORS=$((ERRORS + 1))
fi
done < <(find "$DIR" -name "*.php" -print0)
fi
done
echo "### PHP Syntax Check" >> $GITHUB_STEP_SUMMARY
if [ "${ERRORS}" -gt 0 ]; then
echo "**${ERRORS} syntax error(s) found.**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "All PHP files passed syntax check." >> $GITHUB_STEP_SUMMARY
fi
- name: XML manifest validation
run: |
echo "### XML Manifest Validation" >> $GITHUB_STEP_SUMMARY
ERRORS=0
# Find the extension manifest (XML with <extension tag)
MANIFEST=""
for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
if grep -q "<extension" "$XML_FILE" 2>/dev/null; then
MANIFEST="$XML_FILE"
break
fi
done
if [ -z "$MANIFEST" ]; then
echo "No Joomla extension manifest found (XML file with \`<extension\` tag)." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "Manifest found: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY
# Validate well-formed XML
php -r "
\$xml = @simplexml_load_file('$MANIFEST');
if (\$xml === false) {
echo 'INVALID';
exit(1);
}
echo 'VALID';
" > /tmp/xml_result 2>&1
XML_RESULT=$(cat /tmp/xml_result)
if [ "$XML_RESULT" != "VALID" ]; then
echo "Manifest is not well-formed XML." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "Manifest is well-formed XML." >> $GITHUB_STEP_SUMMARY
fi
# Check required tags: name, version, author, namespace (Joomla 5+)
for TAG in name version author namespace; do
if ! grep -q "<${TAG}>" "$MANIFEST" 2>/dev/null; then
echo "Missing required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "Found required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY
fi
done
fi
if [ "${ERRORS}" -gt 0 ]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "**${ERRORS} manifest issue(s) found.**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Manifest validation passed.**" >> $GITHUB_STEP_SUMMARY
fi
- name: Check language files referenced in manifest
run: |
echo "### Language File Check" >> $GITHUB_STEP_SUMMARY
ERRORS=0
MANIFEST=""
for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
if grep -q "<extension" "$XML_FILE" 2>/dev/null; then
MANIFEST="$XML_FILE"
break
fi
done
if [ -n "$MANIFEST" ]; then
# Extract language file references from manifest
LANG_FILES=$(grep -oP 'language\s+tag="[^"]*"[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null || true)
if [ -z "$LANG_FILES" ]; then
echo "No language file references found in manifest — skipping." >> $GITHUB_STEP_SUMMARY
else
while IFS= read -r LANG_FILE; do
LANG_FILE=$(echo "$LANG_FILE" | xargs)
if [ -z "$LANG_FILE" ]; then
continue
fi
# Check in common locations
FOUND=0
for BASE in "." "src" "htdocs"; do
if [ -f "${BASE}/${LANG_FILE}" ]; then
FOUND=1
break
fi
done
if [ "$FOUND" -eq 0 ]; then
echo "Missing language file: \`${LANG_FILE}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "Language file present: \`${LANG_FILE}\`" >> $GITHUB_STEP_SUMMARY
fi
done <<< "$LANG_FILES"
fi
else
echo "No manifest found — skipping language check." >> $GITHUB_STEP_SUMMARY
fi
if [ "${ERRORS}" -gt 0 ]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "**${ERRORS} missing language file(s).**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Language file check passed.**" >> $GITHUB_STEP_SUMMARY
fi
- name: Check index.html files in directories
run: |
echo "### Index.html Check" >> $GITHUB_STEP_SUMMARY
MISSING=0
CHECKED=0
for DIR in src/ htdocs/; do
if [ -d "$DIR" ]; then
while IFS= read -r -d '' SUBDIR; do
CHECKED=$((CHECKED + 1))
if [ ! -f "${SUBDIR}/index.html" ]; then
echo "Missing index.html in: \`${SUBDIR}\`" >> $GITHUB_STEP_SUMMARY
MISSING=$((MISSING + 1))
fi
done < <(find "$DIR" -type d -print0)
fi
done
if [ "${CHECKED}" -eq 0 ]; then
echo "No src/ or htdocs/ directories found — skipping." >> $GITHUB_STEP_SUMMARY
elif [ "${MISSING}" -gt 0 ]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "**${MISSING} director(ies) missing index.html out of ${CHECKED} checked.**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "All ${CHECKED} directories contain index.html." >> $GITHUB_STEP_SUMMARY
fi
release-readiness:
name: Release Readiness Check
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' && github.base_ref == 'main'
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Validate release readiness
run: |
echo "## Release Readiness" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
ERRORS=0
# Extract version from README.md
README_VERSION=$(grep -oP '^\s*VERSION:\s*\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' README.md | head -1)
if [ -z "$README_VERSION" ]; then
echo "No VERSION found in README.md FILE INFORMATION block." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "README version: \`${README_VERSION}\`" >> $GITHUB_STEP_SUMMARY
fi
# Find the extension manifest
MANIFEST=""
for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
if grep -q "<extension" "$XML_FILE" 2>/dev/null; then
MANIFEST="$XML_FILE"
break
fi
done
if [ -z "$MANIFEST" ]; then
echo "No Joomla extension manifest found." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "Manifest: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY
# Check <version> matches README VERSION
MANIFEST_VERSION=$(grep -oP '<version>\K[^<]+' "$MANIFEST" | head -1)
if [ -z "$MANIFEST_VERSION" ]; then
echo "No \`<version>\` tag in manifest." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
elif [ -n "$README_VERSION" ] && [ "$MANIFEST_VERSION" != "$README_VERSION" ]; then
echo "Manifest version \`${MANIFEST_VERSION}\` does not match README \`${README_VERSION}\`." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "Manifest version: \`${MANIFEST_VERSION}\`" >> $GITHUB_STEP_SUMMARY
fi
# Check extension type, element, client attributes
EXT_TYPE=$(grep -oP '<extension[^>]*\btype="\K[^"]+' "$MANIFEST" | head -1)
if [ -z "$EXT_TYPE" ]; then
echo "Missing \`type\` attribute on \`<extension>\` tag." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "Extension type: \`${EXT_TYPE}\`" >> $GITHUB_STEP_SUMMARY
fi
# Element check (component/module/plugin name)
HAS_ELEMENT=$(grep -cP '<(element|name)>' "$MANIFEST" 2>/dev/null || echo "0")
if [ "$HAS_ELEMENT" -eq 0 ]; then
echo "Missing \`<element>\` or \`<name>\` in manifest." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
# Client attribute for site/admin modules and plugins
if echo "$EXT_TYPE" | grep -qP "^(module|plugin)$"; then
HAS_CLIENT=$(grep -cP '<extension[^>]*\bclient=' "$MANIFEST" 2>/dev/null || echo "0")
if [ "$HAS_CLIENT" -eq 0 ]; then
echo "Missing \`client\` attribute for ${EXT_TYPE} extension." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
fi
fi
# Check updates.xml exists
if [ -f "updates.xml" ] || [ -f "updates.xml" ]; then
echo "Update XML present." >> $GITHUB_STEP_SUMMARY
else
echo "No updates.xml found." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
# Check CHANGELOG.md exists
if [ -f "CHANGELOG.md" ]; then
echo "CHANGELOG.md present." >> $GITHUB_STEP_SUMMARY
else
echo "No CHANGELOG.md found." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
echo "" >> $GITHUB_STEP_SUMMARY
if [ $ERRORS -gt 0 ]; then
echo "**${ERRORS} issue(s) must be resolved before release.**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "**Extension is ready for release.**" >> $GITHUB_STEP_SUMMARY
fi
test:
name: Tests (PHP ${{ matrix.php }})
runs-on: ubuntu-latest
needs: lint-and-validate
strategy:
fail-fast: false
matrix:
php: ['8.2', '8.3']
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup PHP ${{ matrix.php }}
run: |
php -v && composer --version
- name: Install dependencies
env:
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
run: |
if [ -f "composer.json" ]; then
composer install \
--no-interaction \
--prefer-dist \
--optimize-autoloader
else
echo "No composer.json found — skipping dependency install"
fi
- name: Run tests
run: |
echo "### Test Results (PHP ${{ matrix.php }})" >> $GITHUB_STEP_SUMMARY
if [ -f "phpunit.xml" ] || [ -f "phpunit.xml.dist" ]; then
vendor/bin/phpunit --testdox 2>&1 | tee /tmp/test-output.log
EXIT=${PIPESTATUS[0]}
if [ $EXIT -eq 0 ]; then
echo "All tests passed." >> $GITHUB_STEP_SUMMARY
else
echo "Test failures detected — see log." >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
cat /tmp/test-output.log >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
fi
exit $EXIT
else
echo "No phpunit.xml found — skipping tests." >> $GITHUB_STEP_SUMMARY
fi
static-analysis:
name: PHPStan Analysis
runs-on: ubuntu-latest
needs: lint-and-validate
continue-on-error: true
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup PHP
run: php -v && composer --version
- name: Install dependencies
env:
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
run: |
if [ -f "composer.json" ]; then
composer install --no-interaction --prefer-dist --optimize-autoloader
fi
- name: Install PHPStan
run: |
if ! command -v vendor/bin/phpstan &> /dev/null; then
composer require --dev phpstan/phpstan --no-interaction 2>/dev/null || \
composer global require phpstan/phpstan --no-interaction
fi
- name: Run PHPStan
run: |
echo "### PHPStan Static Analysis" >> $GITHUB_STEP_SUMMARY
PHPSTAN="vendor/bin/phpstan"
if [ ! -f "$PHPSTAN" ]; then
PHPSTAN=$(composer global config bin-dir --absolute 2>/dev/null)/phpstan
fi
# Determine source directory
SRC_DIR=""
for DIR in src/ htdocs/ lib/; do
if [ -d "$DIR" ]; then
SRC_DIR="$DIR"
break
fi
done
if [ -z "$SRC_DIR" ]; then
echo "No source directory found (src/, htdocs/, lib/) — skipping." >> $GITHUB_STEP_SUMMARY
exit 0
fi
# Use repo phpstan.neon if present, otherwise use baseline config
ARGS="analyse ${SRC_DIR} --memory-limit=512M --no-progress --error-format=table"
if [ -f "phpstan.neon" ] || [ -f "phpstan.neon.dist" ]; then
echo "Using project PHPStan config." >> $GITHUB_STEP_SUMMARY
else
ARGS="$ARGS --level=3"
echo "No phpstan.neon found — using level 3 (type inference)." >> $GITHUB_STEP_SUMMARY
fi
$PHPSTAN $ARGS 2>&1 | tee /tmp/phpstan-output.txt
EXIT=${PIPESTATUS[0]}
if [ $EXIT -eq 0 ]; then
echo "**No errors found.**" >> $GITHUB_STEP_SUMMARY
else
ERRORS=$(grep -c "ERROR" /tmp/phpstan-output.txt 2>/dev/null || echo "some")
echo "**${ERRORS} error(s) found.** Review output above." >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
tail -30 /tmp/phpstan-output.txt >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
fi
exit $EXIT
-87
View File
@@ -1,87 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Maintenance
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /.gitea/workflows/cleanup.yml
# VERSION: 01.00.00
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
name: Repository Cleanup
on:
schedule:
- cron: '0 3 * * 0' # Weekly on Sunday at 03:00 UTC
workflow_dispatch:
permissions:
contents: write
env:
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
jobs:
cleanup:
name: Clean Merged Branches
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GA_TOKEN }}
- name: Delete merged branches
env:
GA_TOKEN: ${{ secrets.GA_TOKEN }}
run: |
echo "=== Merged Branch Cleanup ==="
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
# List branches via API
BRANCHES=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
"${API}/branches?limit=50" | jq -r '.[].name')
DELETED=0
for BRANCH in $BRANCHES; do
# Skip protected branches
case "$BRANCH" in
main|master|develop|release/*|hotfix/*) continue ;;
esac
# Check if branch is merged into main
if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then
echo " Deleting merged branch: ${BRANCH}"
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
"${API}/branches/${BRANCH}" 2>/dev/null || true
DELETED=$((DELETED + 1))
fi
done
echo "Deleted ${DELETED} merged branch(es)"
- name: Clean old workflow runs
env:
GA_TOKEN: ${{ secrets.GA_TOKEN }}
run: |
echo "=== Workflow Run Cleanup ==="
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)
# Get old completed runs
RUNS=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
"${API}/actions/runs?status=completed&limit=50" | \
jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null)
DELETED=0
for RUN_ID in $RUNS; do
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
"${API}/actions/runs/${RUN_ID}" 2>/dev/null || true
DELETED=$((DELETED + 1))
done
echo "Deleted ${DELETED} old workflow run(s)"
+24 -13
View File
@@ -154,7 +154,7 @@ The version in `README.md` **must always match** the `<version>` tag in `manifes
```
MokoWaaS/
├── manifest.xml # Joomla installer manifest (root — required)
├── updates.xml # Update server manifest (root — required, see below)
├── (no updates.xml) # Update XML is generated dynamically by MokoGitea
├── site/ # Frontend (site) code
│ ├── controller.php
│ ├── controllers/
@@ -183,24 +183,34 @@ MokoWaaS/
---
## updates.xml — Required in Repo Root
## Update Server — MokoGitea Dynamic Endpoint
`updates.xml` **must exist at the repository root**. It is the Joomla update server manifest that allows Joomla installations to check for new versions of this extension.
`updates.xml` is **NOT** stored in the repo. MokoGitea generates the update XML dynamically from git releases at:
The `manifest.xml` must reference it via:
```
https://git.mokoconsulting.tech/{Owner}/{Repo}/updates.xml
```
The package manifest (`pkg_mokowaas.xml`) references it via:
```xml
<updateservers>
<server type="extension" priority="1" name="{{EXTENSION_NAME}}">
https://github.com/mokoconsulting-tech/MokoWaaS/raw/main/updates.xml
<server type="extension" priority="1" name="MokoWaaS Update Server">
https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/updates.xml
</server>
</updateservers>
```
**License Key (Download Key):**
- MokoGitea's endpoint validates license keys passed as `?dlid=MOKO-XXXX-XXXX-XXXX-XXXX`
- The generated XML includes `<downloadkey prefix="dlid=" suffix="" />` to tell Joomla a key is required
- Users enter the download key via Joomla's native **System → Update Sites** interface
- Joomla stores the key in `#__update_sites.extra_query` and appends it to all update/download requests
- Invalid/expired keys receive an empty `<updates></updates>` response
**Rules:**
- Every release must prepend a new `<update>` block at the top of `updates.xml` — old entries must be preserved below.
- The `<version>` in `updates.xml` must exactly match `<version>` in `manifest.xml` and the version in `README.md`.
- The `<downloadurl>` must be a publicly accessible direct download link (GitHub Releases asset URL).
- `<targetplatform name="joomla" version="4\.[0-9]+">` — the backslash is a **literal backslash character** in the XML attribute value; Joomla's update-server parser treats the value as a regular expression, so `\.` matches a literal dot and `[0-9]+` matches one or more digits. Do not double-escape it.
- Do NOT create or commit a static `updates.xml` — MokoGitea generates it from releases
- The `<version>` in release tags must match `<version>` in the manifest and `README.md`
- Release assets (ZIPs) must be attached to git releases — MokoGitea uses them for `<downloadurl>`
---
@@ -286,8 +296,8 @@ Approved prefixes: `dev/` · `rc/` · `version/` · `patch/` · `copilot/` · `d
| Change type | Documentation to update |
|-------------|------------------------|
| New or renamed PHP class/method | PHPDoc block; `docs/api/` entry |
| New or changed manifest.xml | Update `updates.xml` version; bump README.md version |
| New release | Prepend `<update>` block to `updates.xml`; update CHANGELOG.md; bump README.md version |
| New or changed manifest.xml | Bump README.md version |
| New release | Create git release with ZIP asset; update CHANGELOG.md; bump README.md version |
| New or changed workflow | `docs/workflows/<workflow-name>.md` |
| Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block |
| **Every PR** | **Bump the patch version** — increment `XX.YY.ZZ` in `README.md`; `sync-version-on-merge` propagates it |
@@ -301,4 +311,5 @@ Approved prefixes: `dev/` · `rc/` · `version/` · `patch/` · `copilot/` · `d
- Never add `defined('_JEXEC') or die;` to CLI scripts or model tests — only to web-accessible PHP files
- Never hardcode version numbers in body text — update `README.md` and let automation propagate
- Never use `github.token` or `secrets.GITHUB_TOKEN` in workflows — always use `secrets.GH_TOKEN`
- Never let `manifest.xml` version, `updates.xml` version, and `README.md` version go out of sync
- Never let `manifest.xml` version and `README.md` version go out of sync
- Never commit a static `updates.xml` — the update feed is generated dynamically by MokoGitea
-126
View File
@@ -1,126 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Deploy
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
# PATH: /templates/workflows/joomla/deploy-manual.yml.template
# VERSION: 04.07.00
# BRIEF: Manual SFTP deploy to dev server for Joomla repos
name: Deploy to Dev (Manual)
on:
workflow_dispatch:
inputs:
clear_remote:
description: 'Delete all remote files before uploading'
required: false
default: 'false'
type: boolean
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
permissions:
contents: read
jobs:
deploy:
name: SFTP Deploy to Dev
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup PHP
run: |
php -v && composer --version
- name: Setup MokoStandards tools
env:
GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
run: |
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
/tmp/mokostandards-api 2>/dev/null || true
if [ -d "/tmp/mokostandards-api" ] && [ -f "/tmp/mokostandards-api/composer.json" ]; then
cd /tmp/mokostandards-api && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
fi
- name: Check FTP configuration
id: check
env:
HOST: ${{ vars.DEV_FTP_HOST }}
PATH_VAR: ${{ vars.DEV_FTP_PATH }}
PORT: ${{ vars.DEV_FTP_PORT }}
run: |
if [ -z "$HOST" ] || [ -z "$PATH_VAR" ]; then
echo "DEV_FTP_HOST or DEV_FTP_PATH not configured -- cannot deploy"
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "skip=false" >> "$GITHUB_OUTPUT"
echo "host=$HOST" >> "$GITHUB_OUTPUT"
REMOTE="${PATH_VAR%/}"
echo "remote=$REMOTE" >> "$GITHUB_OUTPUT"
[ -z "$PORT" ] && PORT="22"
echo "port=$PORT" >> "$GITHUB_OUTPUT"
- name: Deploy via SFTP
if: steps.check.outputs.skip != 'true'
env:
SFTP_KEY: ${{ secrets.DEV_FTP_KEY }}
SFTP_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
SFTP_USER: ${{ vars.DEV_FTP_USERNAME }}
run: |
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
[ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ -- nothing to deploy"; exit 0; }
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
"${{ steps.check.outputs.host }}" "${{ steps.check.outputs.port }}" "$SFTP_USER" "${{ steps.check.outputs.remote }}" \
> /tmp/sftp-config.json
if [ -n "$SFTP_KEY" ]; then
echo "$SFTP_KEY" > /tmp/deploy_key
chmod 600 /tmp/deploy_key
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
else
printf ',"password":"%s"}' "$SFTP_PASS" >> /tmp/sftp-config.json
fi
DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json)
[ "${{ inputs.clear_remote }}" = "true" ] && DEPLOY_ARGS+=(--clear-remote)
PLATFORM=$(php /tmp/mokostandards-api/cli/platform_detect.php --path . 2>/dev/null || true)
if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards-api/deploy/deploy-joomla.php" ]; then
php /tmp/mokostandards-api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}"
else
php /tmp/mokostandards-api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}"
fi
rm -f /tmp/deploy_key /tmp/sftp-config.json
- name: Summary
if: always()
run: |
if [ "${{ steps.check.outputs.skip }}" = "true" ]; then
echo "### Deploy Skipped -- FTP not configured" >> $GITHUB_STEP_SUMMARY
else
echo "### Manual Dev Deploy Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Host | \`${{ steps.check.outputs.host }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Remote | \`${{ steps.check.outputs.remote }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Clear | ${{ inputs.clear_remote }} |" >> $GITHUB_STEP_SUMMARY
fi
-96
View File
@@ -1,96 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Security
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
# PATH: /templates/workflows/gitleaks.yml.template
# VERSION: 01.00.00
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
#
# +========================================================================+
# | SECRET SCANNING |
# +========================================================================+
# | |
# | Scans commits for leaked secrets using Gitleaks. |
# | |
# | - PR scan: only new commits in the PR |
# | - Scheduled: full repo scan weekly |
# | - Alerts via ntfy on findings |
# | |
# +========================================================================+
name: Secret Scanning
on:
pull_request:
branches:
- main
- 'dev/**'
schedule:
- cron: '0 5 * * 1' # Weekly Monday 05:00 UTC
workflow_dispatch:
permissions:
contents: read
env:
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }}
jobs:
gitleaks:
name: Gitleaks Secret Scan
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Gitleaks
run: |
GITLEAKS_VERSION="8.21.2"
curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \
| tar -xz -C /usr/local/bin gitleaks
gitleaks version
- name: Scan for secrets
id: scan
run: |
echo "### Secret Scanning" >> $GITHUB_STEP_SUMMARY
ARGS="--source . --verbose --report-format json --report-path /tmp/gitleaks-report.json"
if [ "${{ github.event_name }}" = "pull_request" ]; then
# Scan only PR commits
ARGS="$ARGS --log-opts=${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}"
echo "Scanning PR commits only" >> $GITHUB_STEP_SUMMARY
else
echo "Full repository scan" >> $GITHUB_STEP_SUMMARY
fi
if gitleaks detect $ARGS 2>&1; then
echo "result=clean" >> "$GITHUB_OUTPUT"
echo "**No secrets detected.**" >> $GITHUB_STEP_SUMMARY
else
echo "result=found" >> "$GITHUB_OUTPUT"
FINDINGS=$(jq length /tmp/gitleaks-report.json 2>/dev/null || echo "unknown")
echo "**${FINDINGS} potential secret(s) detected.**" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Review the findings and rotate any exposed credentials immediately." >> $GITHUB_STEP_SUMMARY
exit 1
fi
- name: Notify on findings
if: failure() && steps.scan.outputs.result == 'found'
run: |
REPO="${{ github.event.repository.name }}"
curl -sS \
-H "Title: ${REPO} — secrets detected in code" \
-H "Tags: rotating_light,key" \
-H "Priority: urgent" \
-d "Gitleaks found potential secrets. Review and rotate credentials immediately." \
"${NTFY_URL}/${NTFY_TOPIC}" || true
+4 -2
View File
@@ -6,19 +6,21 @@
<moko-platform xmlns="https://standards.mokoconsulting.tech/moko-platform/1.0" schema-version="1.0">
<identity>
<name>MokoWaaS</name>
<display-name>Package - MokoWaaS</display-name>
<org>MokoConsulting</org>
<description>White-label identity, security hardening, and tenant restriction layer for WaaS-managed Joomla environments</description>
<version>02.31.00</version>
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
</identity>
<governance>
<platform>joomla</platform>
<standards-version>05.00.00</standards-version>
<standards-source>https://git.mokoconsulting.tech/MokoConsulting/moko-platform</standards-source>
<last-synced>2026-05-21T20:48:00+00:00</last-synced>
<last-synced>2026-05-28T20:00:00+00:00</last-synced>
</governance>
<build>
<language>PHP</language>
<package-type>plugin</package-type>
<package-type>package</package-type>
<entry-point>src/</entry-point>
</build>
</moko-platform>
-71
View File
@@ -1,71 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Notifications
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /.gitea/workflows/notify.yml
# VERSION: 01.00.00
# BRIEF: Push notifications via ntfy on release success or workflow failure
name: Notifications
on:
workflow_run:
workflows:
- "Joomla Build & Release"
- "Joomla Extension CI"
- "Deploy"
- "Cascade Main → Dev"
types:
- completed
permissions:
contents: read
env:
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-releases' }}
jobs:
notify:
name: Send Notification
runs-on: ubuntu-latest
if: >-
github.event.workflow_run.conclusion == 'success' ||
github.event.workflow_run.conclusion == 'failure'
steps:
- name: Notify on success (releases only)
if: >-
github.event.workflow_run.conclusion == 'success' &&
contains(github.event.workflow_run.name, 'Release')
run: |
REPO="${{ github.event.repository.name }}"
WORKFLOW="${{ github.event.workflow_run.name }}"
URL="${{ github.event.workflow_run.html_url }}"
curl -sS \
-H "Title: ${REPO} released" \
-H "Tags: white_check_mark,package" \
-H "Priority: default" \
-H "Click: ${URL}" \
-d "${WORKFLOW} completed successfully." \
"${NTFY_URL}/${NTFY_TOPIC}"
- name: Notify on failure
if: github.event.workflow_run.conclusion == 'failure'
run: |
REPO="${{ github.event.repository.name }}"
WORKFLOW="${{ github.event.workflow_run.name }}"
URL="${{ github.event.workflow_run.html_url }}"
curl -sS \
-H "Title: ${REPO} workflow failed" \
-H "Tags: x,warning" \
-H "Priority: high" \
-H "Click: ${URL}" \
-d "${WORKFLOW} failed. Check the run for details." \
"${NTFY_URL}/${NTFY_TOPIC}"
-90
View File
@@ -1,90 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# SPDX-License-Identifier: GPL-3.0-or-later
#
# Enforces branch merge policy:
# feature/* → dev only
# fix/* → dev only
# hotfix/* → dev or main (emergency)
# dev → main only
# alpha/* → dev only
# beta/* → dev only
# rc/* → main only
name: Branch Policy Check
on:
pull_request:
types: [opened, synchronize, reopened, edited]
jobs:
check-target:
name: Verify merge target
runs-on: ubuntu-latest
steps:
- name: Check branch policy
run: |
HEAD="${{ github.head_ref }}"
BASE="${{ github.base_ref }}"
echo "PR: ${HEAD} → ${BASE}"
ALLOWED=true
REASON=""
case "$HEAD" in
feature/*|feat/*)
if [ "$BASE" != "dev" ]; then
ALLOWED=false
REASON="Feature branches must target 'dev', not '${BASE}'"
fi
;;
fix/*|bugfix/*)
if [ "$BASE" != "dev" ]; then
ALLOWED=false
REASON="Fix branches must target 'dev', not '${BASE}'"
fi
;;
hotfix/*)
if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'"
fi
;;
alpha/*|beta/*)
if [ "$BASE" != "dev" ]; then
ALLOWED=false
REASON="Pre-release branches must target 'dev', not '${BASE}'"
fi
;;
rc/*)
if [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="Release candidate branches must target 'main', not '${BASE}'"
fi
;;
dev)
if [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="Dev branch can only merge into 'main', not '${BASE}'"
fi
;;
esac
if [ "$ALLOWED" = false ]; then
echo "::error::${REASON}"
echo ""
echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "${REASON}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "Branch policy: OK (${HEAD} → ${BASE})"
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
-106
View File
@@ -1,106 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.CI
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /.gitea/workflows/pr-check.yml
# VERSION: 01.00.00
# BRIEF: PR gate — validates code quality and manifest before merge to main
name: PR Check
on:
pull_request:
branches:
- main
types: [opened, synchronize, reopened]
permissions:
contents: read
pull-requests: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
validate:
name: Validate PR
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- 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 >/dev/null 2>&1
fi
- name: PHP syntax check
run: |
echo "=== PHP Lint ==="
ERRORS=0
while IFS= read -r -d '' file; do
if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
ERRORS=$((ERRORS + 1))
fi
done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0)
echo "Checked files, errors: ${ERRORS}"
[ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
- name: Validate Joomla manifest
run: |
echo "=== Manifest Validation ==="
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
if [ -z "$MANIFEST" ]; then
echo "::warning::No Joomla manifest found"
exit 0
fi
echo "Manifest: ${MANIFEST}"
# Check well-formed XML
if ! php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}"; then
echo "::error::Manifest XML is malformed"
exit 1
fi
# Check required elements
for ELEMENT in name version description; do
if ! grep -q "<${ELEMENT}>" "$MANIFEST"; then
echo "::error::Missing <${ELEMENT}> in manifest"
exit 1
fi
done
echo "Manifest valid"
- name: Check updates.xml format
run: |
if [ ! -f "updates.xml" ]; then
echo "No updates.xml — skipping"
exit 0
fi
echo "=== updates.xml Validation ==="
if ! php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}"; then
echo "::error::updates.xml is malformed"
exit 1
fi
echo "updates.xml valid"
- name: Verify package builds
run: |
echo "=== Package Build Test ==="
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
if [ ! -d "$SOURCE_DIR" ]; then
echo "::warning::No src/ or htdocs/ directory"
exit 0
fi
# Dry-run: ensure zip would succeed
FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
echo "Source contains ${FILE_COUNT} files — package will build"
[ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; }
-341
View File
@@ -1,341 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /.gitea/workflows/pre-release.yml
# VERSION: 01.00.00
# BRIEF: Manual pre-release — builds dev/alpha/beta/rc packages from any branch
name: Pre-Release
on:
workflow_dispatch:
inputs:
stability:
description: 'Pre-release channel'
required: true
type: choice
options:
- development
- alpha
- beta
- release-candidate
permissions:
contents: write
env:
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
jobs:
build:
name: "Build Pre-Release (${{ inputs.stability }})"
runs-on: release
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GA_TOKEN }}
- name: Setup PHP
run: |
if ! command -v php &> /dev/null; then
sudo apt-get update -qq
sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip >/dev/null 2>&1
fi
- name: Resolve metadata
id: meta
run: |
STABILITY="${{ inputs.stability }}"
case "$STABILITY" in
development) SUFFIX="-dev"; TAG="development" ;;
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
beta) SUFFIX="-beta"; TAG="beta" ;;
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
esac
# Read and bump patch version (with rollover)
CURRENT=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1)
[ -z "$CURRENT" ] && CURRENT="00.00.00"
MAJOR=$(echo "$CURRENT" | cut -d. -f1)
MINOR=$(echo "$CURRENT" | cut -d. -f2)
PATCH=$(echo "$CURRENT" | cut -d. -f3)
# Patch bump with rollover: ZZ=99 → bump minor, YY=99 → bump major
NEW_PATCH=$((10#$PATCH + 1))
NEW_MINOR=$((10#$MINOR))
NEW_MAJOR=$((10#$MAJOR))
if [ $NEW_PATCH -gt 99 ]; then
NEW_PATCH=0
NEW_MINOR=$((NEW_MINOR + 1))
fi
if [ $NEW_MINOR -gt 99 ]; then
NEW_MINOR=0
NEW_MAJOR=$((NEW_MAJOR + 1))
fi
VERSION=$(printf "%02d.%02d.%02d" $NEW_MAJOR $NEW_MINOR $NEW_PATCH)
TODAY=$(date +%Y-%m-%d)
echo "Bumping: ${CURRENT} → ${VERSION} (patch)"
# Update README.md
sed -i "s/VERSION:[[:space:]]*${CURRENT}/VERSION: ${VERSION}/" README.md
# Update manifest
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
if [ -n "$MANIFEST" ]; then
MANIFEST_VER=$(sed -n 's/.*<version>\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1)
sed -i "s|<version>${MANIFEST_VER}</version>|<version>${VERSION}</version>|" "$MANIFEST"
sed -i "s|<creationDate>[^<]*</creationDate>|<creationDate>${TODAY}</creationDate>|" "$MANIFEST"
fi
# Commit version bump
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
git add -A
git diff --cached --quiet || {
git commit -m "chore(version): bump ${CURRENT} → ${VERSION} [skip ci]"
git push origin HEAD 2>&1
}
# Auto-detect element from manifest
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
EXT_ELEMENT=""
if [ -n "$MANIFEST" ]; then
EXT_ELEMENT=$(sed -n 's/.*<element>\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
if [ -z "$EXT_ELEMENT" ]; then
EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]')
case "$EXT_ELEMENT" in
templatedetails|manifest) EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;;
esac
fi
else
EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
fi
ZIP_NAME="${EXT_ELEMENT}-${VERSION}${SUFFIX}.zip"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT"
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
- name: Build package
run: |
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
if [ ! -d "$SOURCE_DIR" ]; then
echo "::error::No src/ or htdocs/ directory"
exit 1
fi
mkdir -p build/package
rsync -a \
--exclude='sftp-config*' \
--exclude='.ftpignore' \
--exclude='*.ppk' \
--exclude='*.pem' \
--exclude='*.key' \
--exclude='.env*' \
--exclude='*.local' \
--exclude='.build-trigger' \
"${SOURCE_DIR}/" build/package/
- name: Create ZIP
id: zip
run: |
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
cd build/package
zip -r "../${ZIP_NAME}" .
cd ..
SHA256=$(sha256sum "${ZIP_NAME}" | cut -d' ' -f1)
echo "sha256=${SHA256}" >> "$GITHUB_OUTPUT"
echo "ZIP: ${ZIP_NAME} (SHA: ${SHA256:0:16}...)"
- name: Create or replace Gitea release
id: release
run: |
TAG="${{ steps.meta.outputs.tag }}"
VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
SHA256="${{ steps.zip.outputs.sha256 }}"
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
EXT_ELEMENT="${{ steps.meta.outputs.ext_element }}"
TOKEN="${{ secrets.GA_TOKEN }}"
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
BRANCH=$(git branch --show-current)
BODY="## ${VERSION} ($(date +%Y-%m-%d))
**Channel:** ${STABILITY}
**SHA-256:** \`${SHA256}\`"
# Delete existing release
EXISTING_ID=$(curl -sS -H "Authorization: token ${TOKEN}" \
"${API}/releases/tags/${TAG}" | jq -r '.id // empty' 2>/dev/null)
if [ -n "$EXISTING_ID" ]; then
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
"${API}/releases/${EXISTING_ID}" 2>/dev/null || true
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
"${API}/tags/${TAG}" 2>/dev/null || true
fi
# Create release
RELEASE_ID=$(curl -sS -X POST -H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
"${API}/releases" \
-d "$(jq -n \
--arg tag "$TAG" \
--arg target "$BRANCH" \
--arg name "${EXT_ELEMENT} ${VERSION} (${STABILITY})" \
--arg body "$BODY" \
'{tag_name: $tag, target_commitish: $target, name: $name, body: $body, prerelease: true}'
)" | jq -r '.id')
echo "release_id=${RELEASE_ID}" >> "$GITHUB_OUTPUT"
# Upload ZIP
curl -sS -X POST -H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/octet-stream" \
"${API}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" \
--data-binary "@build/${ZIP_NAME}"
echo "Released: ${EXT_ELEMENT} ${VERSION} (${STABILITY})"
- name: Update updates.xml
run: |
STABILITY="${{ steps.meta.outputs.stability }}"
VERSION="${{ steps.meta.outputs.version }}"
SHA256="${{ steps.zip.outputs.sha256 }}"
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
TAG="${{ steps.meta.outputs.tag }}"
DATE=$(date +%Y-%m-%d)
if [ ! -f "updates.xml" ]; then
echo "No updates.xml — skipping"
exit 0
fi
export PY_STABILITY="$STABILITY" PY_VERSION="$VERSION" PY_SHA256="$SHA256" \
PY_ZIP_NAME="$ZIP_NAME" PY_TAG="$TAG" PY_DATE="$DATE" \
PY_GITEA_ORG="$GITEA_ORG" PY_GITEA_REPO="$GITEA_REPO"
python3 << 'PYEOF'
import re, os
stability = os.environ["PY_STABILITY"]
version = os.environ["PY_VERSION"]
sha256 = os.environ["PY_SHA256"]
zip_name = os.environ["PY_ZIP_NAME"]
tag = os.environ["PY_TAG"]
date = os.environ["PY_DATE"]
gitea_org = os.environ["PY_GITEA_ORG"]
gitea_repo = os.environ["PY_GITEA_REPO"]
download_url = f"https://git.mokoconsulting.tech/{gitea_org}/{gitea_repo}/releases/download/{tag}/{zip_name}"
with open("updates.xml", "r") as f:
content = f.read()
# Map stability to XML tag name
tag_map = {"development": "development", "alpha": "alpha", "beta": "beta", "release-candidate": "rc"}
xml_tag = tag_map.get(stability, stability)
pattern = r"(<update>(?:(?!</update>).)*?<tag>" + re.escape(xml_tag) + r"</tag>.*?</update>)"
match = re.search(pattern, content, re.DOTALL)
if match:
block = match.group(1)
updated = re.sub(r"<version>[^<]*</version>", f"<version>{version}</version>", block)
updated = re.sub(r"<creationDate>[^<]*</creationDate>", f"<creationDate>{date}</creationDate>", updated)
if "<sha256>" in updated:
updated = re.sub(r"<sha256>[^<]*</sha256>", f"<sha256>{sha256}</sha256>", updated)
else:
updated = updated.replace("</downloads>", f"</downloads>\n <sha256>{sha256}</sha256>")
updated = re.sub(r"(<downloadurl[^>]*>)[^<]*(</downloadurl>)", rf"\g<1>{download_url}\g<2>", updated)
content = content.replace(block, updated)
print(f"Updated {xml_tag} channel: version={version}")
else:
print(f"WARNING: No <tag>{xml_tag}</tag> block in updates.xml")
with open("updates.xml", "w") as f:
f.write(content)
PYEOF
# Commit and push to current branch
if ! git diff --quiet updates.xml 2>/dev/null; then
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git add updates.xml
git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
git push origin HEAD 2>&1 || echo "WARNING: push failed"
fi
- name: "Sync updates.xml to all branches"
run: |
CURRENT_BRANCH="${{ github.ref_name }}"
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
# Sync updates.xml to main and dev (whichever isn't current)
for BRANCH in main dev; do
[ "$BRANCH" = "$CURRENT_BRANCH" ] && continue
echo "Syncing updates.xml → ${BRANCH}"
git fetch origin "${BRANCH}" 2>/dev/null || continue
git checkout "origin/${BRANCH}" -- . 2>/dev/null || continue
git checkout "${CURRENT_BRANCH}" -- updates.xml
if ! git diff --quiet updates.xml 2>/dev/null; then
git add updates.xml
git commit -m "chore: sync updates.xml from ${CURRENT_BRANCH} [skip ci]"
git push origin HEAD:refs/heads/${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed"
fi
git checkout "${CURRENT_BRANCH}" 2>/dev/null
done
- name: "Delete lesser pre-release channels (cascade)"
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.GA_TOKEN }}"
STABILITY="${{ steps.meta.outputs.stability }}"
# Cascade: rc → beta,alpha,dev | beta → alpha,dev | alpha → dev | dev → nothing
case "$STABILITY" in
release-candidate) TAGS_TO_DELETE="beta alpha development" ;;
beta) TAGS_TO_DELETE="alpha development" ;;
alpha) TAGS_TO_DELETE="development" ;;
*) TAGS_TO_DELETE="" ;;
esac
[ -z "$TAGS_TO_DELETE" ] && exit 0
for TAG in $TAGS_TO_DELETE; do
RELEASE_ID=$(curl -sS -H "Authorization: token ${TOKEN}" \
"${API_BASE}/releases/tags/${TAG}" 2>/dev/null | \
python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "None" ]; then
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
"${API_BASE}/releases/${RELEASE_ID}" 2>/dev/null || true
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
"${API_BASE}/tags/${TAG}" 2>/dev/null || true
echo "Deleted: ${TAG} (id: ${RELEASE_ID})"
fi
done
-600
View File
@@ -1,600 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# This file is part of a Moko Consulting project.
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Joomla
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /.gitea/workflows/release.yml
# VERSION: 02.00.00
# BRIEF: Generic Joomla release — auto-detects element from manifest, stream tags, cascade
name: Create Release
on:
push:
tags:
- 'stable'
- 'release-candidate'
- 'beta'
- 'alpha'
- 'development'
workflow_dispatch:
inputs:
stability:
description: 'Stability tag'
required: true
default: 'stable'
type: choice
options:
- stable
- release-candidate
- beta
- alpha
- development
permissions:
contents: write
env:
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
jobs:
build:
name: Build Release Package
runs-on: release
steps:
# Always checkout main for tag triggers (avoids detached HEAD).
# For workflow_dispatch, checkout whatever branch was selected.
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'push' && 'main' || github.ref }}
fetch-depth: 0
token: ${{ secrets.GA_TOKEN }}
- name: Setup PHP
run: |
if ! command -v php &> /dev/null; then
sudo apt-get update -qq
sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
fi
echo "PHP: $(php -v | head -1)"
echo "Composer: $(composer --version 2>&1 | head -1)"
- name: Get version and stability
id: meta
run: |
echo "=== Meta ==="
echo "event_name: ${{ github.event_name }}"
echo "ref: ${{ github.ref }}"
echo "ref_name: ${{ github.ref_name }}"
echo "sha: ${{ github.sha }}"
# Derive stability from tag name or dispatch input
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
STABILITY="${{ inputs.stability }}"
else
TAG_PUSHED="${GITHUB_REF#refs/tags/}"
case "$TAG_PUSHED" in
stable) STABILITY="stable" ;;
release-candidate) STABILITY="rc" ;;
beta) STABILITY="beta" ;;
alpha) STABILITY="alpha" ;;
development) STABILITY="development" ;;
*) STABILITY="stable" ;;
esac
fi
# Read version from README.md (will be bumped in next step)
VERSION=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1)
[ -z "$VERSION" ] && VERSION="00.00.00"
# Auto-detect extension element from Joomla manifest
# Search depth 3 covers src/admin/com_xxx.xml and similar nested structures
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "./build/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
EXT_ELEMENT=""
if [ -n "$MANIFEST" ]; then
EXT_ELEMENT=$(sed -n 's/.*<element>\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
# If no <element> tag, derive from manifest filename or repo name
if [ -z "$EXT_ELEMENT" ]; then
EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]')
case "$EXT_ELEMENT" in
templatedetails|manifest) EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;;
esac
fi
echo "Manifest: ${MANIFEST}, element: ${EXT_ELEMENT}"
else
EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
echo "No manifest found, using repo name: ${EXT_ELEMENT}"
fi
case "$STABILITY" in
development) SUFFIX="-dev"; TAG_NAME="development" ;;
alpha) SUFFIX="-alpha"; TAG_NAME="alpha" ;;
beta) SUFFIX="-beta"; TAG_NAME="beta" ;;
rc) SUFFIX="-rc"; TAG_NAME="release-candidate" ;;
stable) SUFFIX=""; TAG_NAME="stable" ;;
*) SUFFIX="-dev"; TAG_NAME="development" ;;
esac
PRERELEASE="true"
[ "$STABILITY" = "stable" ] && PRERELEASE="false"
ZIP_NAME="${EXT_ELEMENT}-${VERSION}${SUFFIX}.zip"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
echo "prerelease=${PRERELEASE}" >> "$GITHUB_OUTPUT"
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
echo "tag_name=${TAG_NAME}" >> "$GITHUB_OUTPUT"
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
echo "=== Resolved ==="
echo "VERSION=${VERSION}"
echo "STABILITY=${STABILITY}"
echo "TAG_NAME=${TAG_NAME}"
echo "ZIP_NAME=${ZIP_NAME}"
echo "Branch: $(git branch --show-current)"
- name: Auto-bump patch version
id: bump
env:
GA_TOKEN: ${{ secrets.GA_TOKEN }}
INPUT_VERSION: ${{ steps.meta.outputs.version }}
INPUT_STABILITY: ${{ steps.meta.outputs.stability }}
INPUT_SUFFIX: ${{ steps.meta.outputs.suffix }}
EXT_ELEMENT: ${{ steps.meta.outputs.ext_element }}
run: |
BRANCH=$(git branch --show-current)
GITEA_API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
echo "=== Version Bump ==="
echo "On branch: ${BRANCH}"
# Read current version from README.md
CURRENT=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1)
echo "Current version in README: ${CURRENT}"
if [ -z "$CURRENT" ]; then
echo "No VERSION in README.md — using input version"
echo "version=${INPUT_VERSION}" >> "$GITHUB_OUTPUT"
echo "zip_name=${EXT_ELEMENT}-${INPUT_VERSION}${INPUT_SUFFIX}.zip" >> "$GITHUB_OUTPUT"
exit 0
fi
# Bump patch: XX.YY.ZZ → XX.YY.(ZZ+1)
MAJOR=$(echo "$CURRENT" | cut -d. -f1)
MINOR=$(echo "$CURRENT" | cut -d. -f2)
PATCH=$(echo "$CURRENT" | cut -d. -f3)
NEW_PATCH=$(printf "%02d" $((10#$PATCH + 1)))
NEW_VERSION="${MAJOR}.${MINOR}.${NEW_PATCH}"
TODAY=$(date +%Y-%m-%d)
echo "Bumping: ${CURRENT} → ${NEW_VERSION} (date: ${TODAY})"
# Update README.md
sed -i "s/VERSION:[[:space:]]*${CURRENT}/VERSION: ${NEW_VERSION}/" README.md
# Update manifest (templateDetails.xml / *.xml with <extension>)
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "./build/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
if [ -n "$MANIFEST" ]; then
echo "Manifest: ${MANIFEST}"
sed -i "s|<version>${CURRENT}</version>|<version>${NEW_VERSION}</version>|" "$MANIFEST"
sed -i "s|<creationDate>[^<]*</creationDate>|<creationDate>${TODAY}</creationDate>|" "$MANIFEST"
fi
# Update matching stability channel in updates.xml
if [ -f "updates.xml" ]; then
export PY_OLD="$CURRENT" PY_NEW="$NEW_VERSION" PY_STABILITY="$INPUT_STABILITY" PY_DATE="$TODAY"
python3 << 'PYEOF'
import re, os
old = os.environ["PY_OLD"]
new = os.environ["PY_NEW"]
stability = os.environ["PY_STABILITY"]
date = os.environ["PY_DATE"]
with open("updates.xml") as f:
content = f.read()
pattern = r"(<update>(?:(?!</update>).)*?<tag>" + re.escape(stability) + r"</tag>.*?</update>)"
match = re.search(pattern, content, re.DOTALL)
if match:
block = match.group(1)
updated = block.replace(old, new)
updated = re.sub(r"<creationDate>[^<]*</creationDate>", f"<creationDate>{date}</creationDate>", updated)
content = content.replace(block, updated)
print(f"Updated {stability} channel: {old} -> {new}")
else:
print(f"WARNING: No <update> block found for <tag>{stability}</tag>")
with open("updates.xml", "w") as f:
f.write(content)
PYEOF
fi
# Commit and push version bump
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git remote set-url origin "https://jmiller:${GA_TOKEN}@git.mokoconsulting.tech/${{ github.repository }}.git"
git add -A
git diff --cached --quiet && echo "No changes to commit" || {
git commit -m "chore(version): bump ${CURRENT} → ${NEW_VERSION} [skip ci]" \
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
echo "Pushing version bump to ${BRANCH}..."
git push origin HEAD:${BRANCH} 2>&1
echo "Push exit code: $?"
}
# For stable releases from non-main: merge to main via Gitea API
if [ "$INPUT_STABILITY" = "stable" ] && [ "$BRANCH" != "main" ]; then
echo "Merging ${BRANCH} → main via Gitea API..."
HTTP_CODE=$(curl -sS -o /tmp/merge_response.json -w "%{http_code}" \
-X POST -H "Authorization: token ${GA_TOKEN}" \
-H "Content-Type: application/json" \
"${GITEA_API}/merges" \
-d "$(jq -n \
--arg base "main" \
--arg head "${BRANCH}" \
--arg msg "chore(release): merge ${BRANCH} for stable ${NEW_VERSION} [skip ci]" \
'{base: $base, head: $head, merge_message_field: $msg}'
)")
echo "Merge response (HTTP ${HTTP_CODE}):"
cat /tmp/merge_response.json 2>/dev/null; echo
fi
echo "version=${NEW_VERSION}" >> "$GITHUB_OUTPUT"
echo "zip_name=${EXT_ELEMENT}-${NEW_VERSION}${INPUT_SUFFIX}.zip" >> "$GITHUB_OUTPUT"
echo "=== Bump complete: ${NEW_VERSION} ==="
- name: Install dependencies
env:
COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.GA_TOKEN }}"}}}'
run: |
if [ -f "composer.json" ]; then
echo "Installing composer dependencies..."
composer install --no-dev --optimize-autoloader --no-interaction 2>&1
else
echo "No composer.json — skipping"
fi
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Minify CSS and JS
run: |
if [ -f "package.json" ] && [ -f "scripts/minify.js" ]; then
npm ci --ignore-scripts
node scripts/minify.js
else
echo "No minify setup — skipping"
fi
- name: Create package
run: |
# Detect source directory (src/ or htdocs/)
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
if [ ! -d "$SOURCE_DIR" ]; then
echo "::error::No src/ or htdocs/ directory found"
exit 1
fi
echo "Source directory: ${SOURCE_DIR}"
mkdir -p build/package
rsync -av \
--exclude='sftp-config*' \
--exclude='.ftpignore' \
--exclude='*.ppk' \
--exclude='*.pem' \
--exclude='*.key' \
--exclude='.env*' \
--exclude='*.local' \
--exclude='.build-trigger' \
--exclude='.beta-trigger' \
--exclude='.rc-trigger' \
"${SOURCE_DIR}/" build/package/
echo "Package contents:"
ls -la build/package/ | head -20
- name: Build ZIP
id: zip
run: |
ZIP_NAME="${{ steps.bump.outputs.zip_name }}"
echo "Building: ${ZIP_NAME}"
cd build/package
zip -r "../${ZIP_NAME}" .
cd ..
SHA256=$(sha256sum "${ZIP_NAME}" | cut -d' ' -f1)
SIZE=$(stat -c%s "${ZIP_NAME}")
echo "sha256=${SHA256}" >> "$GITHUB_OUTPUT"
echo "size=${SIZE}" >> "$GITHUB_OUTPUT"
echo "=== Package Built ==="
echo "ZIP: ${ZIP_NAME}"
echo "SHA-256: ${SHA256}"
echo "Size: ${SIZE} bytes"
# ── Gitea Release (PRIMARY) ─────────────────────────────────────
- name: "Gitea: Create or update release"
id: gitea_release
env:
EXT_ELEMENT: ${{ steps.meta.outputs.ext_element }}
run: |
TAG="${{ steps.meta.outputs.tag_name }}"
VERSION="${{ steps.bump.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
SHA256="${{ steps.zip.outputs.sha256 }}"
TOKEN="${{ secrets.GA_TOKEN }}"
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
BRANCH=$(git branch --show-current)
MAX_HISTORY=5
IS_PRE="true"
[ "$STABILITY" = "stable" ] && IS_PRE="false"
# Build this version's entry
NEW_ENTRY="## ${VERSION} ($(date +%Y-%m-%d))
**SHA-256:** \`${SHA256}\`"
if [ -f "CHANGELOG.md" ]; then
NOTES=$(awk "/## \[${VERSION}\]/,/## \[/{if(/## \[${VERSION}\]/)next;if(/## \[/)exit;print}" CHANGELOG.md)
[ -n "$NOTES" ] && NEW_ENTRY="## ${VERSION} ($(date +%Y-%m-%d))
${NOTES}
**SHA-256:** \`${SHA256}\`"
fi
# Check for existing release — keep last N versions in body
EXISTING_BODY=""
EXISTING_ID=""
RELEASE_JSON=$(curl -sS -H "Authorization: token ${TOKEN}" \
"${API}/releases/tags/${TAG}" 2>/dev/null)
EXISTING_ID=$(echo "$RELEASE_JSON" | jq -r '.id // empty')
if [ -n "$EXISTING_ID" ]; then
echo "Existing release found: id=${EXISTING_ID}"
EXISTING_BODY=$(echo "$RELEASE_JSON" | jq -r '.body // ""')
# Keep only last (MAX_HISTORY - 1) version entries to make room for new one
TRIMMED_BODY=$(echo "$EXISTING_BODY" | python3 -c "
import sys, re
content = sys.stdin.read()
# Split on version headers (## XX.YY.ZZ)
parts = re.split(r'(?=^## \d)', content, flags=re.MULTILINE)
# Keep only version entries (skip any preamble)
versions = [p for p in parts if re.match(r'^## \d', p)]
# Keep last $((MAX_HISTORY - 1)) entries
kept = versions[:$((MAX_HISTORY - 1))]
print('\n---\n'.join(kept))
" 2>/dev/null || echo "")
# Delete old release and tag so we can recreate
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
# Compose full body: new entry + previous entries
if [ -n "$TRIMMED_BODY" ]; then
FULL_BODY="${NEW_ENTRY}
---
${TRIMMED_BODY}"
else
FULL_BODY="${NEW_ENTRY}"
fi
echo "=== Create Release ==="
echo "TAG=${TAG} VERSION=${VERSION} BRANCH=${BRANCH} PRE=${IS_PRE} HISTORY=${MAX_HISTORY}"
HTTP_CODE=$(curl -sS -o /tmp/create_release.json -w "%{http_code}" \
-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 "$FULL_BODY" \
--argjson pre "$IS_PRE" \
'{tag_name: $tag, target_commitish: $target, name: $name, body: $body, prerelease: $pre}'
)")
echo "Response (HTTP ${HTTP_CODE}):"
cat /tmp/create_release.json | jq . 2>/dev/null || cat /tmp/create_release.json
echo
RELEASE_ID=$(jq -r '.id // empty' /tmp/create_release.json)
if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" = "null" ]; then
echo "::error::Failed to create release (HTTP ${HTTP_CODE})"
exit 1
fi
echo "release_id=${RELEASE_ID}" >> "$GITHUB_OUTPUT"
echo "Release created: id=${RELEASE_ID}"
- name: "Gitea: Upload ZIP"
run: |
RELEASE_ID="${{ steps.gitea_release.outputs.release_id }}"
ZIP_NAME="${{ steps.bump.outputs.zip_name }}"
TOKEN="${{ secrets.GA_TOKEN }}"
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
echo "Uploading ${ZIP_NAME} to release ${RELEASE_ID}..."
HTTP_CODE=$(curl -sS -o /tmp/upload_response.json -w "%{http_code}" \
-X POST \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/octet-stream" \
"${API}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" \
--data-binary "@build/${ZIP_NAME}")
echo "Upload response (HTTP ${HTTP_CODE}):"
cat /tmp/upload_response.json | jq . 2>/dev/null || cat /tmp/upload_response.json
echo
if [ "$HTTP_CODE" -ge 400 ]; then
echo "::error::Upload failed (HTTP ${HTTP_CODE})"
exit 1
fi
echo "Uploaded ${ZIP_NAME}"
# ── Update updates.xml ──────────────────────────────────────────
- name: "Update updates.xml with SHA and sync to main"
run: |
STABILITY="${{ steps.meta.outputs.stability }}"
VERSION="${{ steps.bump.outputs.version }}"
SHA256="${{ steps.zip.outputs.sha256 }}"
ZIP_NAME="${{ steps.bump.outputs.zip_name }}"
TAG="${{ steps.meta.outputs.tag_name }}"
DATE=$(date +%Y-%m-%d)
BRANCH=$(git branch --show-current)
echo "=== Update updates.xml ==="
echo "STABILITY=${STABILITY} VERSION=${VERSION} SHA=${SHA256:0:16}..."
if [ ! -f "updates.xml" ] || [ -z "$SHA256" ]; then
echo "No updates.xml or no SHA — skipping"
exit 0
fi
# Cascade map: each stability level updates itself + all lower levels
# stable → all | rc → rc,beta,alpha,dev | beta → beta,alpha,dev | alpha → alpha,dev | dev → dev
case "$STABILITY" in
stable) CASCADE="development,alpha,beta,rc,stable" ;;
rc) CASCADE="development,alpha,beta,rc" ;;
beta) CASCADE="development,alpha,beta" ;;
alpha) CASCADE="development,alpha" ;;
development) CASCADE="development" ;;
*) CASCADE="$STABILITY" ;;
esac
echo "Cascade: ${STABILITY} → ${CASCADE}"
export PY_CASCADE="$CASCADE" PY_VERSION="$VERSION" PY_SHA256="$SHA256" \
PY_ZIP_NAME="$ZIP_NAME" PY_TAG="$TAG" PY_DATE="$DATE" \
PY_GITEA_ORG="$GITEA_ORG" PY_GITEA_REPO="$GITEA_REPO"
python3 << 'PYEOF'
import re, os
cascade = os.environ["PY_CASCADE"].split(",")
version = os.environ["PY_VERSION"]
sha256 = os.environ["PY_SHA256"]
zip_name = os.environ["PY_ZIP_NAME"]
tag = os.environ["PY_TAG"]
date = os.environ["PY_DATE"]
gitea_org = os.environ["PY_GITEA_ORG"]
gitea_repo = os.environ["PY_GITEA_REPO"]
gitea_url = f"https://git.mokoconsulting.tech/{gitea_org}/{gitea_repo}/releases/download/{tag}/{zip_name}"
with open("updates.xml", "r") as f:
content = f.read()
for xml_tag in cascade:
xml_tag = xml_tag.strip()
block_pattern = r"(<update>(?:(?!</update>).)*?<tag>" + re.escape(xml_tag) + r"</tag>.*?</update>)"
match = re.search(block_pattern, content, re.DOTALL)
if not match:
print(f" SKIP: no <tag>{xml_tag}</tag> block found")
continue
block = match.group(1)
original_block = block
# Update version and date
block = re.sub(r"<version>[^<]*</version>", f"<version>{version}</version>", block)
block = re.sub(r"<creationDate>[^<]*</creationDate>", f"<creationDate>{date}</creationDate>", block)
# Set SHA — add if missing, update if present, never leave empty
if "<sha256>" in block:
block = re.sub(r"<sha256>[^<]*</sha256>", f"<sha256>{sha256}</sha256>", block)
else:
block = block.replace("</downloads>", f"</downloads>\n <sha256>{sha256}</sha256>")
# Update download URL
block = re.sub(
r"(<downloadurl[^>]*>)https://git\.mokoconsulting\.tech/[^<]*(</downloadurl>)",
rf"\g<1>{gitea_url}\g<2>",
block
)
content = content.replace(original_block, block)
print(f" OK: {xml_tag} → version={version}, sha={sha256[:16]}...")
with open("updates.xml", "w") as f:
f.write(content)
print(f"Cascaded {len(cascade)} channel(s)")
PYEOF
# Commit and push
if git diff --quiet updates.xml 2>/dev/null; then
echo "No changes to updates.xml"
exit 0
fi
git add updates.xml
git commit -m "chore: update ${STABILITY} SHA-256 for ${VERSION} [skip ci]" \
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
echo "Pushing updates.xml to ${BRANCH}..."
git push origin HEAD:${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed"
# Always sync updates.xml to main via API (Joomla reads from main)
GA_TOKEN="${{ secrets.GA_TOKEN }}"
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
echo "Syncing updates.xml to main via API..."
FILE_SHA=$(curl -sS -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)
HTTP_CODE=$(curl -sS -o /tmp/sync_response.json -w "%{http_code}" \
-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 ${STABILITY} ${VERSION} [skip ci]" \
--arg branch "main" \
'{content: $content, sha: $sha, message: $msg, branch: $branch}'
)")
echo "Sync response (HTTP ${HTTP_CODE}):"
cat /tmp/sync_response.json | jq -r '.content.name // .message // "unknown"' 2>/dev/null
if [ "$HTTP_CODE" -ge 400 ]; then
echo "::warning::Sync to main failed (HTTP ${HTTP_CODE})"
fi
else
echo "::warning::Could not get updates.xml SHA from main"
fi
- name: Summary
if: always()
run: |
VERSION="${{ steps.bump.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
ZIP_NAME="${{ steps.bump.outputs.zip_name }}"
SHA256="${{ steps.zip.outputs.sha256 }}"
TAG="${{ steps.meta.outputs.tag_name }}"
echo "### Release Created" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Stability | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY
echo "| Tag | \`${TAG}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY
echo "| SHA-256 | \`${SHA256}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Gitea | [Release](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${TAG}) |" >> $GITHUB_STEP_SUMMARY
-766
View File
@@ -1,766 +0,0 @@
# ============================================================================
# Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
#
# This file is part of a Moko Consulting project.
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Validation
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
# PATH: /templates/workflows/joomla/repo_health.yml.template
# VERSION: 04.06.00
# BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts.
# ============================================================================
name: Repo Health
concurrency:
group: repo-health-${{ github.repository }}-${{ github.ref }}
cancel-in-progress: true
defaults:
run:
shell: bash
on:
workflow_dispatch:
inputs:
profile:
description: 'Validation profile: all, release, scripts, or repo'
required: true
default: all
type: choice
options:
- all
- release
- scripts
- repo
pull_request:
push:
permissions:
contents: read
env:
# Release policy - Repository Variables Only
RELEASE_REQUIRED_REPO_VARS: RS_FTP_PATH_SUFFIX
RELEASE_OPTIONAL_REPO_VARS: DEV_FTP_SUFFIX
# Scripts governance policy
SCRIPTS_REQUIRED_DIRS:
SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate
# Repo health policy
REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.gitea/workflows/
REPO_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore,README.md,docs/
REPO_DISALLOWED_DIRS:
REPO_DISALLOWED_FILES: TODO.md,todo.md
# Extended checks toggles
EXTENDED_CHECKS: "true"
# File / directory variables
DOCS_INDEX: docs/docs-index.md
SCRIPT_DIR: scripts
WORKFLOWS_DIR: .gitea/workflows
SHELLCHECK_PATTERN: '*.sh'
SPDX_FILE_GLOBS: '*.sh,*.php,*.js,*.ts,*.css,*.xml,*.yml,*.yaml'
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
access_check:
name: Access control
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
outputs:
allowed: ${{ steps.perm.outputs.allowed }}
permission: ${{ steps.perm.outputs.permission }}
steps:
- name: Check actor permission (admin only)
id: perm
env:
TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
REPO: ${{ github.repository }}
ACTOR: ${{ github.actor }}
run: |
set -euo pipefail
ALLOWED=false
PERMISSION=unknown
METHOD=""
# Hardcoded authorized users — always allowed
case "$ACTOR" in
jmiller|gitea-actions[bot])
ALLOWED=true
PERMISSION=admin
METHOD="hardcoded allowlist"
;;
*)
# Detect platform and check permissions via API
API_BASE="${GITHUB_API_URL:-${GITEA_API_URL:-https://api.github.com}}"
RESP=$(curl -sf -H "Authorization: token ${TOKEN}" \
"${API_BASE}/repos/${REPO}/collaborators/${ACTOR}/permission" 2>/dev/null || echo '{}')
PERMISSION=$(echo "$RESP" | grep -oP '"permission"\s*:\s*"\K[^"]+' || echo "unknown")
if [ "$PERMISSION" = "admin" ] || [ "$PERMISSION" = "maintain" ] || [ "$PERMISSION" = "owner" ]; then
ALLOWED=true
fi
METHOD="collaborator API"
;;
esac
echo "permission=${PERMISSION}" >> "$GITHUB_OUTPUT"
echo "allowed=${ALLOWED}" >> "$GITHUB_OUTPUT"
{
echo "## Access Authorization"
echo ""
echo "| Field | Value |"
echo "|-------|-------|"
echo "| **Actor** | \`${ACTOR}\` |"
echo "| **Repository** | \`${REPO}\` |"
echo "| **Permission** | \`${PERMISSION}\` |"
echo "| **Method** | ${METHOD} |"
echo "| **Authorized** | ${ALLOWED} |"
echo ""
if [ "$ALLOWED" = "true" ]; then
echo "${ACTOR} authorized (${METHOD})"
else
echo "${ACTOR} is NOT authorized. Requires admin or maintain role."
fi
} >> "${GITHUB_STEP_SUMMARY}"
- name: Deny execution when not permitted
if: ${{ steps.perm.outputs.allowed != 'true' }}
run: |
set -euo pipefail
printf '%s\n' 'ERROR: Access denied. Admin permission required.' >> "${GITHUB_STEP_SUMMARY}"
exit 1
release_config:
name: Release configuration
needs: access_check
if: ${{ needs.access_check.outputs.allowed == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
- name: Guardrails release vars
env:
PROFILE_RAW: ${{ github.event.inputs.profile }}
RS_FTP_PATH_SUFFIX: ${{ vars.RS_FTP_PATH_SUFFIX }}
DEV_FTP_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
run: |
set -euo pipefail
profile="${PROFILE_RAW:-all}"
case "${profile}" in
all|release|scripts|repo) ;;
*)
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
exit 1
;;
esac
if [ "${profile}" = 'scripts' ] || [ "${profile}" = 'repo' ]; then
{
printf '%s\n' '### Release configuration (Repository Variables)'
printf '%s\n' "Profile: ${profile}"
printf '%s\n' 'Status: SKIPPED'
printf '%s\n' 'Reason: profile excludes release validation'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
exit 0
fi
IFS=',' read -r -a required <<< "${RELEASE_REQUIRED_REPO_VARS}"
IFS=',' read -r -a optional <<< "${RELEASE_OPTIONAL_REPO_VARS}"
missing=()
missing_optional=()
for k in "${required[@]}"; do
v="${!k:-}"
[ -z "${v}" ] && missing+=("${k}")
done
for k in "${optional[@]}"; do
v="${!k:-}"
[ -z "${v}" ] && missing_optional+=("${k}")
done
{
printf '%s\n' '### Release configuration (Repository Variables)'
printf '%s\n' "Profile: ${profile}"
printf '%s\n' '| Variable | Status |'
printf '%s\n' '|---|---|'
printf '%s\n' "| RS_FTP_PATH_SUFFIX | ${RS_FTP_PATH_SUFFIX:-NOT SET} |"
printf '%s\n' "| DEV_FTP_SUFFIX | ${DEV_FTP_SUFFIX:-NOT SET} |"
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
if [ "${#missing_optional[@]}" -gt 0 ]; then
{
printf '%s\n' '### Missing optional repository variables'
for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
if [ "${#missing[@]}" -gt 0 ]; then
{
printf '%s\n' '### Missing required repository variables'
for m in "${missing[@]}"; do printf '%s\n' "- ${m}"; done
printf '%s\n' 'ERROR: Guardrails failed. Missing required repository variables.'
} >> "${GITHUB_STEP_SUMMARY}"
exit 1
fi
{
printf '%s\n' '### Repository variables validation result'
printf '%s\n' 'Status: OK'
printf '%s\n' 'All required repository variables present.'
printf '%s\n' ''
printf '%s\n' '**Note**: Organization secrets (RS_FTP_HOST, RS_FTP_USER, etc.) are validated at deployment time, not in repository health checks.'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
scripts_governance:
name: Scripts governance
needs: access_check
if: ${{ needs.access_check.outputs.allowed == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
- name: Scripts folder checks
env:
PROFILE_RAW: ${{ github.event.inputs.profile }}
run: |
set -euo pipefail
profile="${PROFILE_RAW:-all}"
case "${profile}" in
all|release|scripts|repo) ;;
*)
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
exit 1
;;
esac
if [ "${profile}" = 'release' ] || [ "${profile}" = 'repo' ]; then
{
printf '%s\n' '### Scripts governance'
printf '%s\n' "Profile: ${profile}"
printf '%s\n' 'Status: SKIPPED'
printf '%s\n' 'Reason: profile excludes scripts governance'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
exit 0
fi
if [ ! -d "${SCRIPT_DIR}" ]; then
{
printf '%s\n' '### Scripts governance'
printf '%s\n' 'Status: OK (advisory)'
printf '%s\n' 'scripts/ directory not present. No scripts governance enforced.'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
exit 0
fi
IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}"
IFS=',' read -r -a allowed_dirs <<< "${SCRIPTS_ALLOWED_DIRS}"
missing_dirs=()
unapproved_dirs=()
for d in "${required_dirs[@]}"; do
req="${d%/}"
[ ! -d "${req}" ] && missing_dirs+=("${req}/")
done
while IFS= read -r d; do
allowed=false
for a in "${allowed_dirs[@]}"; do
a_norm="${a%/}"
[ "${d%/}" = "${a_norm}" ] && allowed=true
done
[ "${allowed}" = false ] && unapproved_dirs+=("${d%/}/")
done < <(find "${SCRIPT_DIR}" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sed 's#^\./##')
{
printf '%s\n' '### Scripts governance'
printf '%s\n' "Profile: ${profile}"
printf '%s\n' '| Area | Status | Notes |'
printf '%s\n' '|---|---|---|'
if [ "${#missing_dirs[@]}" -gt 0 ]; then
printf '%s\n' '| Required directories | Warning | Missing required subfolders |'
else
printf '%s\n' '| Required directories | OK | All required subfolders present |'
fi
if [ "${#unapproved_dirs[@]}" -gt 0 ]; then
printf '%s\n' '| Directory policy | Warning | Unapproved directories detected |'
else
printf '%s\n' '| Directory policy | OK | No unapproved directories |'
fi
printf '%s\n' '| Enforcement mode | Advisory | scripts folder is optional |'
printf '\n'
if [ "${#missing_dirs[@]}" -gt 0 ]; then
printf '%s\n' 'Missing required script directories:'
for m in "${missing_dirs[@]}"; do printf '%s\n' "- ${m}"; done
printf '\n'
else
printf '%s\n' 'Missing required script directories: none.'
printf '\n'
fi
if [ "${#unapproved_dirs[@]}" -gt 0 ]; then
printf '%s\n' 'Unapproved script directories detected:'
for m in "${unapproved_dirs[@]}"; do printf '%s\n' "- ${m}"; done
printf '\n'
else
printf '%s\n' 'Unapproved script directories detected: none.'
printf '\n'
fi
printf '%s\n' 'Scripts governance completed in advisory mode.'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
repo_health:
name: Repository health
needs: access_check
if: ${{ needs.access_check.outputs.allowed == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
- name: Repository health checks
env:
PROFILE_RAW: ${{ github.event.inputs.profile }}
run: |
set -euo pipefail
profile="${PROFILE_RAW:-all}"
case "${profile}" in
all|release|scripts|repo) ;;
*)
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
exit 1
;;
esac
if [ "${profile}" = 'release' ] || [ "${profile}" = 'scripts' ]; then
{
printf '%s\n' '### Repository health'
printf '%s\n' "Profile: ${profile}"
printf '%s\n' 'Status: SKIPPED'
printf '%s\n' 'Reason: profile excludes repository health'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
exit 0
fi
# Source directory: src/ or htdocs/ (either is valid)
if [ -d "src" ]; then
SOURCE_DIR="src"
elif [ -d "htdocs" ]; then
SOURCE_DIR="htdocs"
else
missing_required+=("src/ or htdocs/ (source directory required)")
fi
IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}"
IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}"
IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}"
IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES}"
missing_required=()
missing_optional=()
for item in "${required_artifacts[@]}"; do
if printf '%s' "${item}" | grep -q '/$'; then
d="${item%/}"
[ ! -d "${d}" ] && missing_required+=("${item}")
else
[ ! -f "${item}" ] && missing_required+=("${item}")
fi
done
for f in "${optional_files[@]}"; do
if printf '%s' "${f}" | grep -q '/$'; then
d="${f%/}"
[ ! -d "${d}" ] && missing_optional+=("${f}")
else
[ ! -f "${f}" ] && missing_optional+=("${f}")
fi
done
for d in "${disallowed_dirs[@]}"; do
d_norm="${d%/}"
[ -d "${d_norm}" ] && missing_required+=("${d_norm}/ (disallowed)")
done
for f in "${disallowed_files[@]}"; do
[ -f "${f}" ] && missing_required+=("${f} (disallowed)")
done
git fetch origin --prune
dev_paths=()
dev_branches=()
while IFS= read -r b; do
name="${b#origin/}"
if [ "${name}" = 'dev' ]; then
dev_branches+=("${name}")
else
dev_paths+=("${name}")
fi
done < <(git branch -r --list 'origin/dev*' | sed 's/^ *//')
if [ "${#dev_paths[@]}" -eq 0 ]; then
missing_required+=("dev/* branch (e.g. dev/01.00.00)")
fi
if [ "${#dev_branches[@]}" -gt 0 ]; then
missing_required+=("invalid branch dev (must be dev/<version>)")
fi
content_warnings=()
if [ -f 'CHANGELOG.md' ] && ! grep -Eq '^# Changelog' CHANGELOG.md; then
content_warnings+=("CHANGELOG.md missing '# Changelog' header")
fi
if [ -f 'CHANGELOG.md' ] && grep -Eq '^[# ]*Unreleased' CHANGELOG.md; then
content_warnings+=("CHANGELOG.md contains Unreleased section (review release readiness)")
fi
if [ -f 'LICENSE' ] && ! grep -qiE 'GNU GENERAL PUBLIC LICENSE|GPL' LICENSE; then
content_warnings+=("LICENSE does not look like a GPL text")
fi
if [ -f 'README.md' ] && ! grep -qiE 'moko|Moko' README.md; then
content_warnings+=("README.md missing expected brand keyword")
fi
export PROFILE_RAW="${profile}"
export MISSING_REQUIRED="$(printf '%s\n' "${missing_required[@]:-}")"
export MISSING_OPTIONAL="$(printf '%s\n' "${missing_optional[@]:-}")"
export CONTENT_WARNINGS="$(printf '%s\n' "${content_warnings[@]:-}")"
report_json="$(python3 - <<'PY'
import json
import os
profile = os.environ.get('PROFILE_RAW') or 'all'
missing_required = os.environ.get('MISSING_REQUIRED', '').splitlines() if os.environ.get('MISSING_REQUIRED') else []
missing_optional = os.environ.get('MISSING_OPTIONAL', '').splitlines() if os.environ.get('MISSING_OPTIONAL') else []
content_warnings = os.environ.get('CONTENT_WARNINGS', '').splitlines() if os.environ.get('CONTENT_WARNINGS') else []
out = {
'profile': profile,
'missing_required': [x for x in missing_required if x],
'missing_optional': [x for x in missing_optional if x],
'content_warnings': [x for x in content_warnings if x],
}
print(json.dumps(out, indent=2))
PY
)"
{
printf '%s\n' '### Repository health'
printf '%s\n' "Profile: ${profile}"
printf '%s\n' '| Metric | Value |'
printf '%s\n' '|---|---|'
printf '%s\n' "| Missing required | ${#missing_required[@]} |"
printf '%s\n' "| Missing optional | ${#missing_optional[@]} |"
printf '%s\n' "| Content warnings | ${#content_warnings[@]} |"
printf '\n'
printf '%s\n' '### Guardrails report (JSON)'
printf '%s\n' '```json'
printf '%s\n' "${report_json}"
printf '%s\n' '```'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
if [ "${#missing_required[@]}" -gt 0 ]; then
{
printf '%s\n' '### Missing required repo artifacts'
for m in "${missing_required[@]}"; do printf '%s\n' "- ${m}"; done
printf '%s\n' 'ERROR: Guardrails failed. Missing required repository artifacts.'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
exit 1
fi
if [ "${#missing_optional[@]}" -gt 0 ]; then
{
printf '%s\n' '### Missing optional repo artifacts'
for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
if [ "${#content_warnings[@]}" -gt 0 ]; then
{
printf '%s\n' '### Repo content warnings'
for m in "${content_warnings[@]}"; do printf '%s\n' "- ${m}"; done
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
# -- Joomla-specific checks --
joomla_findings=()
MANIFEST="$(find . -maxdepth 2 -name '*.xml' -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true)"
if [ -z "${MANIFEST}" ]; then
joomla_findings+=("Joomla XML manifest not found (no *.xml with <extension> tag)")
else
if ! grep -qP '<version>' "${MANIFEST}"; then
joomla_findings+=("XML manifest: <version> tag missing")
fi
if ! grep -qP 'type="(component|module|plugin|library|package|template|language)"' "${MANIFEST}"; then
joomla_findings+=("XML manifest: type attribute missing or invalid")
fi
if ! grep -qP '<name>' "${MANIFEST}"; then
joomla_findings+=("XML manifest: <name> tag missing")
fi
if ! grep -qP '<author>' "${MANIFEST}"; then
joomla_findings+=("XML manifest: <author> tag missing")
fi
if ! grep -qP '<namespace' "${MANIFEST}"; then
joomla_findings+=("XML manifest: <namespace> missing (required for Joomla 5+)")
fi
fi
INI_COUNT="$(find . -name '*.ini' -type f 2>/dev/null | wc -l)"
if [ "${INI_COUNT}" -eq 0 ]; then
joomla_findings+=("No .ini language files found")
fi
if [ ! -f 'updates.xml' ]; then
joomla_findings+=("updates.xml missing in root (required for Joomla update server)")
fi
INDEX_DIRS=("${SOURCE_DIR}" "${SOURCE_DIR}/admin" "${SOURCE_DIR}/site")
for dir in "${INDEX_DIRS[@]}"; do
if [ -d "${dir}" ] && [ ! -f "${dir}/index.html" ]; then
joomla_findings+=("${dir}/index.html missing (directory listing protection)")
fi
done
if [ "${#joomla_findings[@]}" -gt 0 ]; then
{
printf '%s\n' '### Joomla extension checks'
printf '%s\n' '| Check | Status |'
printf '%s\n' '|---|---|'
for f in "${joomla_findings[@]}"; do
printf '%s\n' "| ${f} | Warning |"
done
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
else
{
printf '%s\n' '### Joomla extension checks'
printf '%s\n' 'All Joomla-specific checks passed.'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
extended_enabled="${EXTENDED_CHECKS:-true}"
extended_findings=()
if [ "${extended_enabled}" = 'true' ]; then
if [ -f '.github/CODEOWNERS' ] || [ -f 'CODEOWNERS' ] || [ -f 'docs/CODEOWNERS' ]; then
:
else
extended_findings+=("CODEOWNERS not found (.github/CODEOWNERS preferred)")
fi
if ls "${WORKFLOWS_DIR}"/*.yml >/dev/null 2>&1 || ls "${WORKFLOWS_DIR}"/*.yaml >/dev/null 2>&1; then
bad_refs="$(grep -RIn --include='*.yml' --include='*.yaml' -E '^[[:space:]]*uses:[[:space:]]*[^#]+@(main|master)\b' "${WORKFLOWS_DIR}" 2>/dev/null || true)"
if [ -n "${bad_refs}" ]; then
extended_findings+=("Workflows reference actions @main/@master (pin versions): see log excerpt")
{
printf '%s\n' '### Workflow pinning advisory'
printf '%s\n' 'Found uses: entries pinned to main/master:'
printf '%s\n' '```'
printf '%s\n' "${bad_refs}"
printf '%s\n' '```'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
fi
if [ -f "${DOCS_INDEX}" ]; then
missing_links="$(python3 - <<'PY'
import os
import re
idx = os.environ.get('DOCS_INDEX', 'docs/docs-index.md')
base = os.getcwd()
bad = []
pat = re.compile(r'\[[^\]]+\]\(([^)]+)\)')
with open(idx, 'r', encoding='utf-8') as f:
for line in f:
for m in pat.findall(line):
link = m.strip()
if link.startswith('http://') or link.startswith('https://') or link.startswith('#') or link.startswith('mailto:'):
continue
if link.startswith('/'):
rel = link.lstrip('/')
else:
rel = os.path.normpath(os.path.join(os.path.dirname(idx), link))
rel = rel.split('#', 1)[0]
rel = rel.split('?', 1)[0]
if not rel:
continue
p = os.path.join(base, rel)
if not os.path.exists(p):
bad.append(rel)
print('\n'.join(sorted(set(bad))))
PY
)"
if [ -n "${missing_links}" ]; then
extended_findings+=("docs/docs-index.md contains broken relative links")
{
printf '%s\n' '### Docs index link integrity'
printf '%s\n' 'Broken relative links:'
while IFS= read -r l; do [ -n "${l}" ] && printf '%s\n' "- ${l}"; done <<< "${missing_links}"
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
fi
if [ -d "${SCRIPT_DIR}" ]; then
if ! command -v shellcheck >/dev/null 2>&1; then
sudo apt-get update -qq
sudo apt-get install -y shellcheck >/dev/null
fi
sc_out=''
while IFS= read -r shf; do
[ -z "${shf}" ] && continue
out_one="$(shellcheck -S warning -x "${shf}" 2>/dev/null || true)"
if [ -n "${out_one}" ]; then
sc_out="${sc_out}${out_one}\n"
fi
done < <(find "${SCRIPT_DIR}" -type f -name "${SHELLCHECK_PATTERN}" 2>/dev/null | sort)
if [ -n "${sc_out}" ]; then
extended_findings+=("ShellCheck warnings detected (advisory)")
sc_head="$(printf '%s' "${sc_out}" | head -n 200)"
{
printf '%s\n' '### ShellCheck (advisory)'
printf '%s\n' '```'
printf '%s\n' "${sc_head}"
printf '%s\n' '```'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
fi
spdx_missing=()
IFS=',' read -r -a spdx_globs <<< "${SPDX_FILE_GLOBS}"
spdx_args=()
for g in "${spdx_globs[@]}"; do spdx_args+=("${g}"); done
while IFS= read -r f; do
[ -z "${f}" ] && continue
if ! head -n 40 "${f}" | grep -q 'SPDX-License-Identifier:'; then
spdx_missing+=("${f}")
fi
done < <(git ls-files "${spdx_args[@]}" 2>/dev/null || true)
if [ "${#spdx_missing[@]}" -gt 0 ]; then
extended_findings+=("SPDX header missing in some tracked files (advisory)")
{
printf '%s\n' '### SPDX header advisory'
printf '%s\n' 'Files missing SPDX-License-Identifier (first 40 lines scan):'
for f in "${spdx_missing[@]}"; do printf '%s\n' "- ${f}"; done
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
stale_cutoff_days=180
stale_branches="$(git for-each-ref --format='%(refname:short) %(committerdate:unix)' refs/remotes/origin 2>/dev/null | awk -v now="$(date +%s)" -v days="${stale_cutoff_days}" '{if (now-$2 > days*86400) print $1}' | head -50)"
if [ -n "${stale_branches}" ]; then
extended_findings+=("Stale remote branches detected (advisory)")
{
printf '%s\n' '### Git hygiene advisory'
printf '%s\n' "Branches with last commit older than ${stale_cutoff_days} days (sample up to 50):"
while IFS= read -r b; do [ -n "${b}" ] && printf '%s\n' "- ${b}"; done <<< "${stale_branches}"
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
fi
{
printf '%s\n' '### Guardrails coverage matrix'
printf '%s\n' '| Domain | Status | Notes |'
printf '%s\n' '|---|---|---|'
printf '%s\n' '| Access control | OK | Admin-only execution gate |'
printf '%s\n' '| Release variables | OK | Repository variables validation |'
printf '%s\n' '| Scripts governance | OK | Directory policy and advisory reporting |'
printf '%s\n' '| Repo required artifacts | OK | Required, optional, disallowed enforcement |'
printf '%s\n' '| Repo content heuristics | OK | Brand, license, changelog structure |'
if [ "${extended_enabled}" = 'true' ]; then
if [ "${#extended_findings[@]}" -gt 0 ]; then
printf '%s\n' '| Extended checks | Warning | See extended findings below |'
else
printf '%s\n' '| Extended checks | OK | No findings |'
fi
else
printf '%s\n' '| Extended checks | SKIPPED | EXTENDED_CHECKS disabled |'
fi
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
if [ "${extended_enabled}" = 'true' ] && [ "${#extended_findings[@]}" -gt 0 ]; then
{
printf '%s\n' '### Extended findings (advisory)'
for f in "${extended_findings[@]}"; do printf '%s\n' "- ${f}"; done
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
printf '%s\n' 'Repository health guardrails passed.' >> "${GITHUB_STEP_SUMMARY}"
-82
View File
@@ -1,82 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Security
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /.gitea/workflows/security-audit.yml
# VERSION: 01.00.00
# BRIEF: Dependency vulnerability scanning for composer and npm packages
name: Security Audit
on:
schedule:
- cron: '0 6 * * 1' # Weekly on Monday at 06:00 UTC
pull_request:
branches:
- main
paths:
- 'composer.json'
- 'composer.lock'
- 'package.json'
- 'package-lock.json'
workflow_dispatch:
permissions:
contents: read
env:
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }}
jobs:
audit:
name: Dependency Audit
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Composer audit
if: hashFiles('composer.lock') != ''
run: |
echo "=== Composer Security Audit ==="
if ! command -v composer &> /dev/null; then
sudo apt-get update -qq
sudo apt-get install -y -qq php-cli composer >/dev/null 2>&1
fi
composer audit --format=plain 2>&1 | tee /tmp/composer-audit.txt
RESULT=$?
if [ $RESULT -ne 0 ]; then
echo "::warning::Composer vulnerabilities found"
echo "composer_vulnerable=true" >> "$GITHUB_ENV"
else
echo "No known vulnerabilities in composer dependencies"
fi
- name: NPM audit
if: hashFiles('package-lock.json') != ''
run: |
echo "=== NPM Security Audit ==="
npm audit --production 2>&1 | tee /tmp/npm-audit.txt || true
if npm audit --production 2>&1 | grep -q "found 0 vulnerabilities"; then
echo "No known vulnerabilities in npm dependencies"
else
echo "::warning::NPM vulnerabilities found"
echo "npm_vulnerable=true" >> "$GITHUB_ENV"
fi
- name: Notify on vulnerabilities
if: env.composer_vulnerable == 'true' || env.npm_vulnerable == 'true'
run: |
REPO="${{ github.event.repository.name }}"
curl -sS \
-H "Title: ${REPO} has vulnerable dependencies" \
-H "Tags: lock,warning" \
-H "Priority: high" \
-d "Security audit found vulnerabilities. Review dependency updates." \
"${NTFY_URL}/${NTFY_TOPIC}" || true
-464
View File
@@ -1,464 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Joomla
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
# PATH: /templates/workflows/joomla/update-server.yml.template
# VERSION: 04.06.00
# BRIEF: Update Joomla update server XML feed with stable/rc/dev entries
#
# Writes updates.xml with multiple <update> entries:
# - <tag>stable</tag> on push to main (from auto-release)
# - <tag>rc</tag> on push to rc/**
# - <tag>development</tag> on push to dev or dev/**
#
# Joomla filters by user's "Minimum Stability" setting.
name: Update Joomla Update Server XML Feed
on:
push:
branches:
- 'dev'
- 'dev/**'
- 'alpha/**'
- 'beta/**'
- 'rc/**'
paths:
- 'src/**'
- 'htdocs/**'
pull_request:
types: [closed]
branches:
- 'dev'
- 'dev/**'
- 'alpha/**'
- 'beta/**'
- 'rc/**'
paths:
- 'src/**'
- 'htdocs/**'
workflow_dispatch:
inputs:
stability:
description: 'Stability tag'
required: true
default: 'development'
type: choice
options:
- development
- alpha
- beta
- rc
- stable
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
permissions:
contents: write
jobs:
update-xml:
name: Update updates.xml
runs-on: release
if: >-
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || github.event_name == 'push'
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.GA_TOKEN }}
fetch-depth: 0
- name: Setup MokoStandards tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.GA_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
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
/tmp/mokostandards-api 2>/dev/null || true
if [ -d "/tmp/mokostandards-api" ] && [ -f "/tmp/mokostandards-api/composer.json" ]; then
cd /tmp/mokostandards-api && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
fi
- name: Generate updates.xml entry
id: update
run: |
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)
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
BUMPED=$(php /tmp/mokostandards-api/cli/version_bump.php --path . 2>/dev/null || true)
if [ -n "$BUMPED" ]; then
VERSION=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null || echo "$VERSION")
git add -A
git commit -m "chore(version): auto-bump patch ${VERSION} [skip ci]" \
--author="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
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
STABILITY="${{ inputs.stability }}"
elif [[ "$BRANCH" == rc/* ]]; then
STABILITY="rc"
elif [[ "$BRANCH" == beta/* ]]; then
STABILITY="beta"
elif [[ "$BRANCH" == alpha/* ]]; then
STABILITY="alpha"
elif [[ "$BRANCH" == dev/* ]] || [[ "$BRANCH" == "dev" ]]; then
STABILITY="development"
else
STABILITY="stable"
fi
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
# Parse manifest (portable — no grep -P)
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "./build/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
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 commit -m "chore: update updates.xml (${STABILITY}: ${DISPLAY_VERSION}) [skip ci]" \
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
git push
}
# -- Sync updates.xml to main (for non-main branches) ----------------------
- name: Sync updates.xml to main
if: github.ref_name != 'main'
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
GA_TOKEN="${{ secrets.GA_TOKEN }}"
FILE_SHA=$(curl -sf -H "Authorization: token ${GA_TOKEN}" \
"${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then
CONTENT=$(base64 -w0 updates.xml)
curl -sf -X PUT -H "Authorization: token ${GA_TOKEN}" \
-H "Content-Type: application/json" \
"${API_BASE}/contents/updates.xml" \
-d "$(python3 -c "import json; print(json.dumps({
'content': '${CONTENT}',
'sha': '${FILE_SHA}',
'message': 'chore: sync updates.xml from ${STABILITY} [skip ci]',
'branch': 'main'
}))")" > /dev/null 2>&1 \
&& echo "updates.xml synced to main (${STABILITY})" >> $GITHUB_STEP_SUMMARY \
|| echo "WARNING: failed to sync updates.xml to main" >> $GITHUB_STEP_SUMMARY
else
echo "WARNING: could not get updates.xml SHA from main" >> $GITHUB_STEP_SUMMARY
fi
- name: SFTP deploy to dev server
if: contains(github.ref, 'dev/') || github.ref == 'refs/heads/dev'
env:
DEV_HOST: ${{ vars.DEV_FTP_HOST }}
DEV_PATH: ${{ vars.DEV_FTP_PATH }}
DEV_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
DEV_USER: ${{ vars.DEV_FTP_USERNAME }}
DEV_PORT: ${{ vars.DEV_FTP_PORT }}
DEV_KEY: ${{ secrets.DEV_FTP_KEY }}
DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
run: |
# -- Permission check: admin or maintain role required --------
ACTOR="${{ github.actor }}"
REPO="${{ github.repository }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
"${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \
python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read")
case "$PERMISSION" in
admin|maintain|write) ;;
*)
echo "Deploy denied: ${ACTOR} has '${PERMISSION}' — requires admin, maintain, or write"
exit 0
;;
esac
[ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured — skipping SFTP"; exit 0; }
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
[ ! -d "$SOURCE_DIR" ] && exit 0
PORT="${DEV_PORT:-22}"
REMOTE="${DEV_PATH%/}"
[ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}"
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
"$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json
if [ -n "$DEV_KEY" ]; then
echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
else
printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json
fi
PLATFORM=$(php /tmp/mokostandards-api/cli/platform_detect.php --path . 2>/dev/null || true)
if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards-api/deploy/deploy-joomla.php" ]; then
php /tmp/mokostandards-api/deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
elif [ -f "/tmp/mokostandards-api/deploy/deploy-sftp.php" ]; then
php /tmp/mokostandards-api/deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
fi
rm -f /tmp/deploy_key /tmp/sftp-config.json
echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY
- name: Summary
if: always()
run: |
echo "## Joomla Update Server" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Version | \`${DISPLAY_VERSION}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Element | \`${EXT_ELEMENT}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Download | [ZIP](${DOWNLOAD_URL}) |" >> $GITHUB_STEP_SUMMARY
-763
View File
@@ -1,763 +0,0 @@
# 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-tech/moko-platform
# PATH: /templates/workflows/universal/auto-release.yml.template
# VERSION: 05.00.00
# BRIEF: Universal build & release detects platform from manifest.xml
#
# +========================================================================+
# | UNIVERSAL BUILD & RELEASE PIPELINE |
# +========================================================================+
# | |
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
# | |
# | Platform-specific: |
# | joomla: XML manifest, updates.xml, type-prefixed packages |
# | dolibarr: mod*.class.php, update.txt, dev version reset |
# | generic: README-only, no update stream |
# | |
# +========================================================================+
name: "Universal: Build & Release"
on:
pull_request:
types: [closed]
branches:
- main
paths:
- 'src/**'
- 'htdocs/**'
workflow_dispatch:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
permissions:
contents: write
jobs:
release:
name: Build & Release Pipeline
runs-on: release
if: >-
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch'
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.GA_TOKEN }}
fetch-depth: 0
- name: Setup moko-platform tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN }}"}}'
run: |
# Ensure PHP + Composer are available
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
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
# -- PLATFORM DETECTION ---------------------------------------------------
- name: Detect platform
id: platform
run: |
php /tmp/moko-platform-api/cli/manifest_read.php --path . --github-output
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true)
MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1 || true)
echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT"
echo "mod_file=${MOD_FILE}" >> "$GITHUB_OUTPUT"
- name: "Step 1: Read version"
id: version
run: |
VERSION=$(php /tmp/moko-platform-api/cli/version_read.php --path .)
if [ -z "$VERSION" ]; then
echo "::error::No VERSION in README.md"
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
MAJOR=$(echo "$VERSION" | cut -d. -f1)
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "release_tag=v${MAJOR}" >> "$GITHUB_OUTPUT"
echo "skip=false" >> "$GITHUB_OUTPUT"
echo "branch=version/${MAJOR}" >> "$GITHUB_OUTPUT"
- name: "Step 1b: Bump version"
id: bump
if: steps.version.outputs.skip != 'true'
run: |
MOKO_API="/tmp/moko-platform-api/cli"
BUMP=$(php ${MOKO_API}/version_bump.php --path . --minor)
VERSION=$(echo "$BUMP" | grep -oP '\d{2}\.\d{2}\.\d{2}$' || true)
[ -z "$VERSION" ] && VERSION=$(php ${MOKO_API}/version_read.php --path .)
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "Bumped to: ${VERSION}"
- name: Check if already released
if: steps.version.outputs.skip != 'true'
id: check
run: |
TAG="${{ steps.version.outputs.release_tag }}"
BRANCH="${{ steps.version.outputs.branch }}"
TAG_EXISTS=false
BRANCH_EXISTS=false
git rev-parse "$TAG" >/dev/null 2>&1 && TAG_EXISTS=true
git ls-remote --heads origin "$BRANCH" 2>/dev/null | grep -q "$BRANCH" && BRANCH_EXISTS=true
echo "tag_exists=$TAG_EXISTS" >> "$GITHUB_OUTPUT"
echo "branch_exists=$BRANCH_EXISTS" >> "$GITHUB_OUTPUT"
# Tag and branch may persist across patch releases — never skip
echo "already_released=false" >> "$GITHUB_OUTPUT"
# -- SANITY CHECKS -------------------------------------------------------
- name: "Sanity: Pre-release validation"
if: >-
steps.version.outputs.skip != 'true' &&
steps.check.outputs.already_released != 'true'
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
ERRORS=0
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 ---------------
# Always runs — every version change on main archives to version/XX.YY
- name: "Step 2: Version archive branch"
if: steps.check.outputs.already_released != 'true'
run: |
BRANCH="${{ steps.version.outputs.branch }}"
IS_MINOR="${{ steps.version.outputs.is_minor }}"
PATCH="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
PATCH_NUM=$(echo "$PATCH" | awk -F. '{print $3}')
# Check if branch exists
if git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then
git push origin HEAD:"$BRANCH" --force
echo "Updated archive branch: ${BRANCH} (patch ${PATCH_NUM})" >> $GITHUB_STEP_SUMMARY
else
git checkout -b "$BRANCH" 2>/dev/null || git checkout "$BRANCH"
git push origin "$BRANCH" --force
echo "Created archive branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
fi
# -- STEP 3: Set platform version ----------------------------------------
- name: "Step 3: Set platform version"
if: >-
steps.version.outputs.skip != 'true' &&
steps.check.outputs.already_released != 'true'
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
php /tmp/moko-platform-api/cli/version_set_platform.php \
--path . --version "$VERSION" --branch main
# -- STEP 4: Update version badges ----------------------------------------
- name: "Step 4: Update version badges"
if: steps.version.outputs.skip != 'true'
run: |
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
- name: "Step 5: Write update stream"
if: >-
steps.version.outputs.skip != 'true' &&
steps.platform.outputs.platform == 'joomla'
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
php /tmp/moko-platform-api/cli/updates_xml_build.php \
--path . --version "${VERSION}" --stability stable \
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
--github-output
- name: Commit release changes
if: >-
steps.version.outputs.skip != 'true' &&
steps.check.outputs.already_released != 'true'
run: |
if git diff --quiet && git diff --cached --quiet; then
echo "No changes to commit"
exit 0
fi
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 commit -m "chore(release): build ${VERSION} [skip ci]" \
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
git push -u origin HEAD
# -- STEP 6: Create tag ---------------------------------------------------
- name: "Step 6: Create git tag"
if: >-
steps.version.outputs.skip != 'true' &&
steps.check.outputs.tag_exists != 'true' &&
steps.version.outputs.is_minor == 'true'
run: |
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
# Only create the major release tag if it doesn't exist yet
if ! git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then
git tag "$RELEASE_TAG"
git push origin "$RELEASE_TAG"
echo "Tag created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
else
echo "Tag ${RELEASE_TAG} already exists" >> $GITHUB_STEP_SUMMARY
fi
echo "Tag: ${TAG}" >> $GITHUB_STEP_SUMMARY
# -- STEP 7: Create or update Gitea Release --------------------------------
- name: "Step 7: Gitea Release"
if: >-
steps.version.outputs.skip != 'true'
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
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}"
# Reuse metadata from Step 5 (single source of truth)
EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}"
EXT_NAME="${{ steps.updates.outputs.ext_name }}"
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)"
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: >-
steps.version.outputs.skip != 'true'
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
REPO="${{ github.repository }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
# All ZIPs upload to the major release tag (vXX)
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
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)
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'
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}"
EXT_TYPE="${{ steps.updates.outputs.ext_type }}"
EXT_FOLDER="${{ steps.updates.outputs.ext_folder }}"
# Build TYPE_PREFIX to match Step 8's ZIP naming
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) --------------------------------
- name: "Step 9: Mirror release to GitHub"
if: >-
steps.version.outputs.skip != 'true' &&
steps.version.outputs.stability == 'stable' &&
secrets.GH_TOKEN != ''
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
MAJOR="${{ steps.version.outputs.major }}"
BRANCH="${{ steps.version.outputs.branch }}"
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
NOTES=$(php /tmp/moko-platform-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null || true)
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
echo "$NOTES" > /tmp/release_notes.md
EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/tags/$RELEASE_TAG" 2>/dev/null | jq -r ".tag_name // empty" || true)
if [ -z "$EXISTING" ]; then
gh release create "$RELEASE_TAG" \
--repo "$GH_REPO" \
--title "v${MAJOR} (latest: ${VERSION})" \
--notes-file /tmp/release_notes.md \
--target "$BRANCH" || true
else
gh release edit "$RELEASE_TAG" \
--repo "$GH_REPO" \
--title "v${MAJOR} (latest: ${VERSION})" || true
fi
# Upload assets to GitHub mirror
for PKG in /tmp/${EXT_ELEMENT:-pkg}-${VERSION}.*; do
if [ -f "$PKG" ]; then
_RELID=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/tags/$RELEASE_TAG" 2>/dev/null | jq -r ".id // empty")
[ -n "$_RELID" ] && curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" -H "Content-Type: application/octet-stream" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/${_RELID}/assets?name=$(basename $PKG)" --data-binary "@$PKG" > /dev/null 2>&1 || true
fi
done
echo "GitHub mirror updated: ${GH_REPO} ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
# -- STEP 10: Sync main branch to GitHub mirror ----------------------------
- name: "Step 10: Push main to GitHub mirror"
if: >-
steps.version.outputs.skip != 'true' &&
secrets.GH_TOKEN != ''
continue-on-error: true
run: |
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
git remote add github "https://x-access-token:${{ secrets.GH_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
git remote set-url github "https://x-access-token:${{ secrets.GH_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
git fetch origin main --depth=1
git push github origin/main:refs/heads/main --force 2>/dev/null \
&& echo "main branch pushed to GitHub mirror" \
|| echo "WARNING: GitHub mirror push failed"
# -- Clean up lesser pre-releases (cascade) ---------------------------------
# stable → deletes all | rc → beta,alpha,dev | beta → alpha,dev | alpha → dev
- name: "Delete lesser pre-release channels"
continue-on-error: true
run: |
php /tmp/moko-platform-api/cli/release_cascade.php \
--stability stable \
--token "${{ secrets.GA_TOKEN }}" \
--org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
--gitea-url "${GITEA_URL}" 2>/dev/null || true
- name: "Step 11: Delete and recreate dev branch from main"
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.GA_TOKEN }}"
# Delete dev branch
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
"${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
# Recreate dev from main (now includes version bump + changelog promotion)
curl -sf -X POST -H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
"${API_BASE}/branches" \
-d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
echo "Dev branch reset from main (keeps dev ahead after release)" >> $GITHUB_STEP_SUMMARY
# -- Dolibarr post-release: Reset dev version -----------------------------
- 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
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.GA_TOKEN }}"
MOD_FILE="${{ steps.platform.outputs.mod_file }}"
ENCODED_PATH=$(echo "$MOD_FILE" | sed 's|^\./||' | python3 -c "import sys,urllib.parse; print(urllib.parse.quote(sys.stdin.read().strip()))")
FILE_RESP=$(curl -sf -H "Authorization: token ${TOKEN}" "${API_BASE}/contents/${ENCODED_PATH}?ref=dev" 2>/dev/null || true)
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)
if [ -n "$FILE_SHA" ] && [ -n "$FILE_CONTENT" ]; then
UPDATED=$(echo "$FILE_CONTENT" | sed "s/\$this->version = '[^']*'/\$this->version = 'development'/")
ENCODED=$(echo "$UPDATED" | base64 -w0)
curl -sf -X PUT -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/contents/${ENCODED_PATH}" \
-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
# -- Summary --------------------------------------------------------------
- name: Pipeline Summary
if: always()
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
PLATFORM="${{ steps.platform.outputs.platform }}"
if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
else
echo "" >> $GITHUB_STEP_SUMMARY
echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
fi
-213
View File
@@ -1,213 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Maintenance
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
# PATH: /templates/workflows/cascade-dev.yml.template
# VERSION: 02.00.00
# BRIEF: Forward-merge main → all open branches after every push to main
#
# +========================================================================+
# | CASCADE MAIN → ALL BRANCHES |
# +========================================================================+
# | |
# | Triggers on every push to main (PR merges, bot commits, etc.) |
# | |
# | 1. List all branches matching: dev, rc/*, beta/*, alpha/* |
# | 2. For each: create PR (main → branch), auto-merge if clean |
# | 3. On conflict: leave PR open for manual resolution |
# | |
# +========================================================================+
name: "Universal: Cascade Main → Dev"
on:
push:
branches:
- main
workflow_dispatch:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
permissions:
contents: write
pull-requests: write
jobs:
cascade:
name: Cascade main → branches
runs-on: ubuntu-latest
if: >-
!contains(github.event.head_commit.message, '[skip ci]') &&
!contains(github.event.head_commit.message, '[skip cascade]')
steps:
- name: Discover target branches
id: branches
env:
GA_TOKEN: ${{ secrets.GA_TOKEN }}
run: |
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
# Fetch all branches (paginated)
PAGE=1
ALL_BRANCHES=""
while true; do
BATCH=$(curl -sS \
-H "Authorization: token ${GA_TOKEN}" \
"${API}/branches?page=${PAGE}&limit=50" \
| jq -r '.[].name // empty')
[ -z "$BATCH" ] && break
ALL_BRANCHES="$ALL_BRANCHES $BATCH"
PAGE=$((PAGE + 1))
done
# Filter to cascade targets: dev, dev/*, rc/*, beta/*, alpha/*
TARGETS=""
for BRANCH in $ALL_BRANCHES; do
case "$BRANCH" in
dev|dev/*|rc/*|beta/*|alpha/*)
TARGETS="$TARGETS $BRANCH"
;;
esac
done
TARGETS=$(echo "$TARGETS" | xargs) # trim whitespace
if [ -z "$TARGETS" ]; then
echo "targets=" >> "$GITHUB_OUTPUT"
echo "️ No cascade target branches found"
else
echo "targets=$TARGETS" >> "$GITHUB_OUTPUT"
COUNT=$(echo "$TARGETS" | wc -w)
echo "📋 Found ${COUNT} target branch(es): ${TARGETS}"
fi
- name: Cascade to all target branches
if: steps.branches.outputs.targets != ''
env:
GA_TOKEN: ${{ secrets.GA_TOKEN }}
run: |
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
SHORT_SHA="${GITHUB_SHA:0:7}"
TARGETS="${{ steps.branches.outputs.targets }}"
SUCCESS=0
CONFLICTS=0
SKIPPED=0
FAILED=0
for BRANCH in $TARGETS; do
echo ""
echo "═══ main → ${BRANCH} ═══"
# Check if branch is already up to date
ENCODED_BRANCH=$(echo "$BRANCH" | sed 's|/|%2F|g')
RESPONSE=$(curl -sS \
-H "Authorization: token ${GA_TOKEN}" \
"${API}/compare/${ENCODED_BRANCH}...main")
AHEAD=$(echo "$RESPONSE" | jq '.total_commits // 0')
if [ "$AHEAD" -eq 0 ]; then
echo " ✅ Already up to date"
SKIPPED=$((SKIPPED + 1))
continue
fi
echo " ️ main is ${AHEAD} commit(s) ahead"
# Check for existing cascade PR
EXISTING=$(curl -sS \
-H "Authorization: token ${GA_TOKEN}" \
"${API}/pulls?state=open&head=${GITEA_ORG}:main&base=${ENCODED_BRANCH}&limit=1")
EXISTING_COUNT=$(echo "$EXISTING" | jq 'length')
PR_NUMBER=""
if [ "$EXISTING_COUNT" -gt 0 ]; then
PR_NUMBER=$(echo "$EXISTING" | jq -r '.[0].number')
echo " ️ Reusing existing PR #${PR_NUMBER}"
else
# Create cascade PR
PR_RESPONSE=$(curl -sS -w "\n%{http_code}" \
-X POST \
-H "Authorization: token ${GA_TOKEN}" \
-H "Content-Type: application/json" \
-d "{
\"title\": \"chore: cascade main → ${BRANCH} (${SHORT_SHA}) [skip ci]\",
\"body\": \"## Automatic cascade\\n\\nForward-merging \`main\` (${SHORT_SHA}) into \`${BRANCH}\`.\\n\\nIf conflicts exist, resolve manually and merge.\\n\\n> Auto-created by **Cascade Main → Dev**.\",
\"head\": \"main\",
\"base\": \"${BRANCH}\"
}" \
"${API}/pulls")
HTTP_CODE=$(echo "$PR_RESPONSE" | tail -1)
BODY=$(echo "$PR_RESPONSE" | sed '$d')
PR_NUMBER=$(echo "$BODY" | jq -r '.number // empty')
if [ "$HTTP_CODE" != "201" ] || [ -z "$PR_NUMBER" ]; then
MSG=$(echo "$BODY" | jq -r '.message // .' 2>/dev/null | head -1)
echo " ❌ Failed to create PR (HTTP ${HTTP_CODE}): ${MSG}"
FAILED=$((FAILED + 1))
continue
fi
echo " ✅ Created PR #${PR_NUMBER}"
fi
# Try auto-merge
PR_DATA=$(curl -sS \
-H "Authorization: token ${GA_TOKEN}" \
"${API}/pulls/${PR_NUMBER}")
MERGEABLE=$(echo "$PR_DATA" | jq -r '.mergeable // false')
if [ "$MERGEABLE" != "true" ]; then
echo " ⚠️ Conflicts — PR #${PR_NUMBER} left open"
CONFLICTS=$((CONFLICTS + 1))
continue
fi
MERGE_RESPONSE=$(curl -sS -w "\n%{http_code}" \
-X POST \
-H "Authorization: token ${GA_TOKEN}" \
-H "Content-Type: application/json" \
-d "{
\"Do\": \"merge\",
\"merge_message_field\": \"chore: cascade main → ${BRANCH} [skip ci]\",
\"delete_branch_after_merge\": false
}" \
"${API}/pulls/${PR_NUMBER}/merge")
MERGE_HTTP=$(echo "$MERGE_RESPONSE" | tail -1)
if [ "$MERGE_HTTP" = "200" ] || [ "$MERGE_HTTP" = "204" ]; then
echo " ✅ Merged — ${BRANCH} is in sync"
SUCCESS=$((SUCCESS + 1))
else
MERGE_BODY=$(echo "$MERGE_RESPONSE" | sed '$d')
echo " ⚠️ Merge failed (HTTP ${MERGE_HTTP}) — PR #${PR_NUMBER} left open"
CONFLICTS=$((CONFLICTS + 1))
fi
done
# Summary
echo ""
echo "════════════════════════════════════════"
echo " ✅ Merged: ${SUCCESS}"
echo " ⚠️ Conflicts: ${CONFLICTS}"
echo " ⏭️ Up to date: ${SKIPPED}"
echo " ❌ Failed: ${FAILED}"
echo "════════════════════════════════════════"
if [ "$FAILED" -gt 0 ]; then
exit 1
fi
-450
View File
@@ -1,450 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# This file is part of a Moko Consulting project.
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow.Template
# INGROUP: MokoStandards.CI
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
# PATH: /templates/workflows/joomla/ci-joomla.yml.template
# VERSION: 04.06.00
# BRIEF: CI workflow for Joomla extensions — lint, validate, test
name: "Joomla: Extension CI"
on:
pull_request:
branches:
- main
- 'dev/**'
workflow_dispatch:
permissions:
contents: read
pull-requests: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
lint-and-validate:
name: Lint & Validate
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup PHP
run: |
php -v && composer --version
- name: Clone MokoStandards
env:
GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
run: |
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
/tmp/mokostandards-api
- name: Install dependencies
env:
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
run: |
if [ -f "composer.json" ]; then
composer install \
--no-interaction \
--prefer-dist \
--optimize-autoloader
else
echo "No composer.json found — skipping dependency install"
fi
- name: PHP syntax check
run: |
ERRORS=0
for DIR in src/ htdocs/; do
if [ -d "$DIR" ]; then
FOUND=1
while IFS= read -r -d '' FILE; do
OUTPUT=$(php -l "$FILE" 2>&1)
if echo "$OUTPUT" | grep -q "Parse error"; then
echo "::error file=${FILE}::${OUTPUT}"
ERRORS=$((ERRORS + 1))
fi
done < <(find "$DIR" -name "*.php" -print0)
fi
done
echo "### PHP Syntax Check" >> $GITHUB_STEP_SUMMARY
if [ "${ERRORS}" -gt 0 ]; then
echo "**${ERRORS} syntax error(s) found.**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "All PHP files passed syntax check." >> $GITHUB_STEP_SUMMARY
fi
- name: XML manifest validation
run: |
echo "### XML Manifest Validation" >> $GITHUB_STEP_SUMMARY
ERRORS=0
# Find the extension manifest (XML with <extension tag)
MANIFEST=""
for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
if grep -q "<extension" "$XML_FILE" 2>/dev/null; then
MANIFEST="$XML_FILE"
break
fi
done
if [ -z "$MANIFEST" ]; then
echo "No Joomla extension manifest found (XML file with \`<extension\` tag)." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "Manifest found: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY
# Validate well-formed XML
php -r "
\$xml = @simplexml_load_file('$MANIFEST');
if (\$xml === false) {
echo 'INVALID';
exit(1);
}
echo 'VALID';
" > /tmp/xml_result 2>&1
XML_RESULT=$(cat /tmp/xml_result)
if [ "$XML_RESULT" != "VALID" ]; then
echo "Manifest is not well-formed XML." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "Manifest is well-formed XML." >> $GITHUB_STEP_SUMMARY
fi
# Check required tags: name, version, author, namespace (Joomla 5+)
for TAG in name version author namespace; do
if ! grep -q "<${TAG}>" "$MANIFEST" 2>/dev/null; then
echo "Missing required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "Found required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY
fi
done
fi
if [ "${ERRORS}" -gt 0 ]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "**${ERRORS} manifest issue(s) found.**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Manifest validation passed.**" >> $GITHUB_STEP_SUMMARY
fi
- name: Check language files referenced in manifest
run: |
echo "### Language File Check" >> $GITHUB_STEP_SUMMARY
ERRORS=0
MANIFEST=""
for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
if grep -q "<extension" "$XML_FILE" 2>/dev/null; then
MANIFEST="$XML_FILE"
break
fi
done
if [ -n "$MANIFEST" ]; then
# Extract language file references from manifest
LANG_FILES=$(grep -oP 'language\s+tag="[^"]*"[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null || true)
if [ -z "$LANG_FILES" ]; then
echo "No language file references found in manifest — skipping." >> $GITHUB_STEP_SUMMARY
else
while IFS= read -r LANG_FILE; do
LANG_FILE=$(echo "$LANG_FILE" | xargs)
if [ -z "$LANG_FILE" ]; then
continue
fi
# Check in common locations
FOUND=0
for BASE in "." "src" "htdocs"; do
if [ -f "${BASE}/${LANG_FILE}" ]; then
FOUND=1
break
fi
done
if [ "$FOUND" -eq 0 ]; then
echo "Missing language file: \`${LANG_FILE}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "Language file present: \`${LANG_FILE}\`" >> $GITHUB_STEP_SUMMARY
fi
done <<< "$LANG_FILES"
fi
else
echo "No manifest found — skipping language check." >> $GITHUB_STEP_SUMMARY
fi
if [ "${ERRORS}" -gt 0 ]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "**${ERRORS} missing language file(s).**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Language file check passed.**" >> $GITHUB_STEP_SUMMARY
fi
- name: Check index.html files in directories
run: |
echo "### Index.html Check" >> $GITHUB_STEP_SUMMARY
MISSING=0
CHECKED=0
for DIR in src/ htdocs/; do
if [ -d "$DIR" ]; then
while IFS= read -r -d '' SUBDIR; do
CHECKED=$((CHECKED + 1))
if [ ! -f "${SUBDIR}/index.html" ]; then
echo "Missing index.html in: \`${SUBDIR}\`" >> $GITHUB_STEP_SUMMARY
MISSING=$((MISSING + 1))
fi
done < <(find "$DIR" -type d -print0)
fi
done
if [ "${CHECKED}" -eq 0 ]; then
echo "No src/ or htdocs/ directories found — skipping." >> $GITHUB_STEP_SUMMARY
elif [ "${MISSING}" -gt 0 ]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "**${MISSING} director(ies) missing index.html out of ${CHECKED} checked.**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "All ${CHECKED} directories contain index.html." >> $GITHUB_STEP_SUMMARY
fi
release-readiness:
name: Release Readiness Check
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' && github.base_ref == 'main'
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Validate release readiness
run: |
echo "## Release Readiness" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
ERRORS=0
# Extract version from README.md
README_VERSION=$(grep -oP '^\s*VERSION:\s*\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' README.md | head -1)
if [ -z "$README_VERSION" ]; then
echo "No VERSION found in README.md FILE INFORMATION block." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "README version: \`${README_VERSION}\`" >> $GITHUB_STEP_SUMMARY
fi
# Find the extension manifest
MANIFEST=""
for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
if grep -q "<extension" "$XML_FILE" 2>/dev/null; then
MANIFEST="$XML_FILE"
break
fi
done
if [ -z "$MANIFEST" ]; then
echo "No Joomla extension manifest found." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "Manifest: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY
# Check <version> matches README VERSION
MANIFEST_VERSION=$(grep -oP '<version>\K[^<]+' "$MANIFEST" | head -1)
if [ -z "$MANIFEST_VERSION" ]; then
echo "No \`<version>\` tag in manifest." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
elif [ -n "$README_VERSION" ] && [ "$MANIFEST_VERSION" != "$README_VERSION" ]; then
echo "Manifest version \`${MANIFEST_VERSION}\` does not match README \`${README_VERSION}\`." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "Manifest version: \`${MANIFEST_VERSION}\`" >> $GITHUB_STEP_SUMMARY
fi
# Check extension type, element, client attributes
EXT_TYPE=$(grep -oP '<extension[^>]*\btype="\K[^"]+' "$MANIFEST" | head -1)
if [ -z "$EXT_TYPE" ]; then
echo "Missing \`type\` attribute on \`<extension>\` tag." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "Extension type: \`${EXT_TYPE}\`" >> $GITHUB_STEP_SUMMARY
fi
# Element check (component/module/plugin name)
HAS_ELEMENT=$(grep -cP '<(element|name)>' "$MANIFEST" 2>/dev/null || echo "0")
if [ "$HAS_ELEMENT" -eq 0 ]; then
echo "Missing \`<element>\` or \`<name>\` in manifest." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
# Client attribute for site/admin modules and plugins
if echo "$EXT_TYPE" | grep -qP "^(module|plugin)$"; then
HAS_CLIENT=$(grep -cP '<extension[^>]*\bclient=' "$MANIFEST" 2>/dev/null || echo "0")
if [ "$HAS_CLIENT" -eq 0 ]; then
echo "Missing \`client\` attribute for ${EXT_TYPE} extension." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
fi
fi
# Check updates.xml exists
if [ -f "updates.xml" ] || [ -f "updates.xml" ]; then
echo "Update XML present." >> $GITHUB_STEP_SUMMARY
else
echo "No updates.xml found." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
# Check CHANGELOG.md exists
if [ -f "CHANGELOG.md" ]; then
echo "CHANGELOG.md present." >> $GITHUB_STEP_SUMMARY
else
echo "No CHANGELOG.md found." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
echo "" >> $GITHUB_STEP_SUMMARY
if [ $ERRORS -gt 0 ]; then
echo "**${ERRORS} issue(s) must be resolved before release.**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "**Extension is ready for release.**" >> $GITHUB_STEP_SUMMARY
fi
test:
name: Tests (PHP ${{ matrix.php }})
runs-on: ubuntu-latest
needs: lint-and-validate
strategy:
fail-fast: false
matrix:
php: ['8.2', '8.3']
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup PHP ${{ matrix.php }}
run: |
php -v && composer --version
- name: Install dependencies
env:
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
run: |
if [ -f "composer.json" ]; then
composer install \
--no-interaction \
--prefer-dist \
--optimize-autoloader
else
echo "No composer.json found — skipping dependency install"
fi
- name: Run tests
run: |
echo "### Test Results (PHP ${{ matrix.php }})" >> $GITHUB_STEP_SUMMARY
if [ -f "phpunit.xml" ] || [ -f "phpunit.xml.dist" ]; then
vendor/bin/phpunit --testdox 2>&1 | tee /tmp/test-output.log
EXIT=${PIPESTATUS[0]}
if [ $EXIT -eq 0 ]; then
echo "All tests passed." >> $GITHUB_STEP_SUMMARY
else
echo "Test failures detected — see log." >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
cat /tmp/test-output.log >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
fi
exit $EXIT
else
echo "No phpunit.xml found — skipping tests." >> $GITHUB_STEP_SUMMARY
fi
static-analysis:
name: PHPStan Analysis
runs-on: ubuntu-latest
needs: lint-and-validate
continue-on-error: true
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup PHP
run: php -v && composer --version
- name: Install dependencies
env:
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
run: |
if [ -f "composer.json" ]; then
composer install --no-interaction --prefer-dist --optimize-autoloader
fi
- name: Install PHPStan
run: |
if ! command -v vendor/bin/phpstan &> /dev/null; then
composer require --dev phpstan/phpstan --no-interaction 2>/dev/null || \
composer global require phpstan/phpstan --no-interaction
fi
- name: Run PHPStan
run: |
echo "### PHPStan Static Analysis" >> $GITHUB_STEP_SUMMARY
PHPSTAN="vendor/bin/phpstan"
if [ ! -f "$PHPSTAN" ]; then
PHPSTAN=$(composer global config bin-dir --absolute 2>/dev/null)/phpstan
fi
# Determine source directory
SRC_DIR=""
for DIR in src/ htdocs/ lib/; do
if [ -d "$DIR" ]; then
SRC_DIR="$DIR"
break
fi
done
if [ -z "$SRC_DIR" ]; then
echo "No source directory found (src/, htdocs/, lib/) — skipping." >> $GITHUB_STEP_SUMMARY
exit 0
fi
# Use repo phpstan.neon if present, otherwise use baseline config
ARGS="analyse ${SRC_DIR} --memory-limit=512M --no-progress --error-format=table"
if [ -f "phpstan.neon" ] || [ -f "phpstan.neon.dist" ]; then
echo "Using project PHPStan config." >> $GITHUB_STEP_SUMMARY
else
ARGS="$ARGS --level=3"
echo "No phpstan.neon found — using level 3 (type inference)." >> $GITHUB_STEP_SUMMARY
fi
$PHPSTAN $ARGS 2>&1 | tee /tmp/phpstan-output.txt
EXIT=${PIPESTATUS[0]}
if [ $EXIT -eq 0 ]; then
echo "**No errors found.**" >> $GITHUB_STEP_SUMMARY
else
ERRORS=$(grep -c "ERROR" /tmp/phpstan-output.txt 2>/dev/null || echo "some")
echo "**${ERRORS} error(s) found.** Review output above." >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
tail -30 /tmp/phpstan-output.txt >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
fi
exit $EXIT
-87
View File
@@ -1,87 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Maintenance
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /.gitea/workflows/cleanup.yml
# VERSION: 01.00.00
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
name: "Universal: Repository Cleanup"
on:
schedule:
- cron: '0 3 * * 0' # Weekly on Sunday at 03:00 UTC
workflow_dispatch:
permissions:
contents: write
env:
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
jobs:
cleanup:
name: Clean Merged Branches
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GA_TOKEN }}
- name: Delete merged branches
env:
GA_TOKEN: ${{ secrets.GA_TOKEN }}
run: |
echo "=== Merged Branch Cleanup ==="
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
# List branches via API
BRANCHES=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
"${API}/branches?limit=50" | jq -r '.[].name')
DELETED=0
for BRANCH in $BRANCHES; do
# Skip protected branches
case "$BRANCH" in
main|master|develop|release/*|hotfix/*) continue ;;
esac
# Check if branch is merged into main
if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then
echo " Deleting merged branch: ${BRANCH}"
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
"${API}/branches/${BRANCH}" 2>/dev/null || true
DELETED=$((DELETED + 1))
fi
done
echo "Deleted ${DELETED} merged branch(es)"
- name: Clean old workflow runs
env:
GA_TOKEN: ${{ secrets.GA_TOKEN }}
run: |
echo "=== Workflow Run Cleanup ==="
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)
# Get old completed runs
RUNS=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
"${API}/actions/runs?status=completed&limit=50" | \
jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null)
DELETED=0
for RUN_ID in $RUNS; do
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
"${API}/actions/runs/${RUN_ID}" 2>/dev/null || true
DELETED=$((DELETED + 1))
done
echo "Deleted ${DELETED} old workflow run(s)"
-126
View File
@@ -1,126 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Deploy
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
# PATH: /templates/workflows/joomla/deploy-manual.yml.template
# VERSION: 04.07.00
# BRIEF: Manual SFTP deploy to dev server for Joomla repos
name: "Universal: Deploy to Dev (Manual)"
on:
workflow_dispatch:
inputs:
clear_remote:
description: 'Delete all remote files before uploading'
required: false
default: 'false'
type: boolean
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
permissions:
contents: read
jobs:
deploy:
name: SFTP Deploy to Dev
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup PHP
run: |
php -v && composer --version
- name: Setup MokoStandards tools
env:
GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
run: |
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
/tmp/mokostandards-api 2>/dev/null || true
if [ -d "/tmp/mokostandards-api" ] && [ -f "/tmp/mokostandards-api/composer.json" ]; then
cd /tmp/mokostandards-api && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
fi
- name: Check FTP configuration
id: check
env:
HOST: ${{ vars.DEV_FTP_HOST }}
PATH_VAR: ${{ vars.DEV_FTP_PATH }}
PORT: ${{ vars.DEV_FTP_PORT }}
run: |
if [ -z "$HOST" ] || [ -z "$PATH_VAR" ]; then
echo "DEV_FTP_HOST or DEV_FTP_PATH not configured -- cannot deploy"
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "skip=false" >> "$GITHUB_OUTPUT"
echo "host=$HOST" >> "$GITHUB_OUTPUT"
REMOTE="${PATH_VAR%/}"
echo "remote=$REMOTE" >> "$GITHUB_OUTPUT"
[ -z "$PORT" ] && PORT="22"
echo "port=$PORT" >> "$GITHUB_OUTPUT"
- name: Deploy via SFTP
if: steps.check.outputs.skip != 'true'
env:
SFTP_KEY: ${{ secrets.DEV_FTP_KEY }}
SFTP_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
SFTP_USER: ${{ vars.DEV_FTP_USERNAME }}
run: |
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
[ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ -- nothing to deploy"; exit 0; }
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
"${{ steps.check.outputs.host }}" "${{ steps.check.outputs.port }}" "$SFTP_USER" "${{ steps.check.outputs.remote }}" \
> /tmp/sftp-config.json
if [ -n "$SFTP_KEY" ]; then
echo "$SFTP_KEY" > /tmp/deploy_key
chmod 600 /tmp/deploy_key
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
else
printf ',"password":"%s"}' "$SFTP_PASS" >> /tmp/sftp-config.json
fi
DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json)
[ "${{ inputs.clear_remote }}" = "true" ] && DEPLOY_ARGS+=(--clear-remote)
PLATFORM=$(php /tmp/mokostandards-api/cli/platform_detect.php --path . 2>/dev/null || true)
if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards-api/deploy/deploy-joomla.php" ]; then
php /tmp/mokostandards-api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}"
else
php /tmp/mokostandards-api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}"
fi
rm -f /tmp/deploy_key /tmp/sftp-config.json
- name: Summary
if: always()
run: |
if [ "${{ steps.check.outputs.skip }}" = "true" ]; then
echo "### Deploy Skipped -- FTP not configured" >> $GITHUB_STEP_SUMMARY
else
echo "### Manual Dev Deploy Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Host | \`${{ steps.check.outputs.host }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Remote | \`${{ steps.check.outputs.remote }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Clear | ${{ inputs.clear_remote }} |" >> $GITHUB_STEP_SUMMARY
fi
-96
View File
@@ -1,96 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Security
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
# PATH: /templates/workflows/gitleaks.yml.template
# VERSION: 01.00.00
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
#
# +========================================================================+
# | SECRET SCANNING |
# +========================================================================+
# | |
# | Scans commits for leaked secrets using Gitleaks. |
# | |
# | - PR scan: only new commits in the PR |
# | - Scheduled: full repo scan weekly |
# | - Alerts via ntfy on findings |
# | |
# +========================================================================+
name: "Universal: Secret Scanning"
on:
pull_request:
branches:
- main
- 'dev/**'
schedule:
- cron: '0 5 * * 1' # Weekly Monday 05:00 UTC
workflow_dispatch:
permissions:
contents: read
env:
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }}
jobs:
gitleaks:
name: Gitleaks Secret Scan
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Gitleaks
run: |
GITLEAKS_VERSION="8.21.2"
curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \
| tar -xz -C /usr/local/bin gitleaks
gitleaks version
- name: Scan for secrets
id: scan
run: |
echo "### Secret Scanning" >> $GITHUB_STEP_SUMMARY
ARGS="--source . --verbose --report-format json --report-path /tmp/gitleaks-report.json"
if [ "${{ github.event_name }}" = "pull_request" ]; then
# Scan only PR commits
ARGS="$ARGS --log-opts=${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}"
echo "Scanning PR commits only" >> $GITHUB_STEP_SUMMARY
else
echo "Full repository scan" >> $GITHUB_STEP_SUMMARY
fi
if gitleaks detect $ARGS 2>&1; then
echo "result=clean" >> "$GITHUB_OUTPUT"
echo "**No secrets detected.**" >> $GITHUB_STEP_SUMMARY
else
echo "result=found" >> "$GITHUB_OUTPUT"
FINDINGS=$(jq length /tmp/gitleaks-report.json 2>/dev/null || echo "unknown")
echo "**${FINDINGS} potential secret(s) detected.**" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Review the findings and rotate any exposed credentials immediately." >> $GITHUB_STEP_SUMMARY
exit 1
fi
- name: Notify on findings
if: failure() && steps.scan.outputs.result == 'found'
run: |
REPO="${{ github.event.repository.name }}"
curl -sS \
-H "Title: ${REPO} — secrets detected in code" \
-H "Tags: rotating_light,key" \
-H "Priority: urgent" \
-d "Gitleaks found potential secrets. Review and rotate credentials immediately." \
"${NTFY_URL}/${NTFY_TOPIC}" || true
-71
View File
@@ -1,71 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Notifications
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /.gitea/workflows/notify.yml
# VERSION: 01.00.00
# BRIEF: Push notifications via ntfy on release success or workflow failure
name: "Universal: Notifications"
on:
workflow_run:
workflows:
- "Joomla Build & Release"
- "Joomla Extension CI"
- "Deploy"
- "Cascade Main → Dev"
types:
- completed
permissions:
contents: read
env:
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-releases' }}
jobs:
notify:
name: Send Notification
runs-on: ubuntu-latest
if: >-
github.event.workflow_run.conclusion == 'success' ||
github.event.workflow_run.conclusion == 'failure'
steps:
- name: Notify on success (releases only)
if: >-
github.event.workflow_run.conclusion == 'success' &&
contains(github.event.workflow_run.name, 'Release')
run: |
REPO="${{ github.event.repository.name }}"
WORKFLOW="${{ github.event.workflow_run.name }}"
URL="${{ github.event.workflow_run.html_url }}"
curl -sS \
-H "Title: ${REPO} released" \
-H "Tags: white_check_mark,package" \
-H "Priority: default" \
-H "Click: ${URL}" \
-d "${WORKFLOW} completed successfully." \
"${NTFY_URL}/${NTFY_TOPIC}"
- name: Notify on failure
if: github.event.workflow_run.conclusion == 'failure'
run: |
REPO="${{ github.event.repository.name }}"
WORKFLOW="${{ github.event.workflow_run.name }}"
URL="${{ github.event.workflow_run.html_url }}"
curl -sS \
-H "Title: ${REPO} workflow failed" \
-H "Tags: x,warning" \
-H "Priority: high" \
-H "Click: ${URL}" \
-d "${WORKFLOW} failed. Check the run for details." \
"${NTFY_URL}/${NTFY_TOPIC}"
-224
View File
@@ -1,224 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.CI
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
# PATH: /templates/workflows/universal/pr-check.yml.template
# VERSION: 05.00.00
# BRIEF: PR gate — branch policy + code validation before merge
name: "Universal: PR Check"
on:
pull_request:
types: [opened, synchronize, reopened, edited]
permissions:
contents: read
pull-requests: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
# ── Branch Policy ──────────────────────────────────────────────────────
branch-policy:
name: Branch Policy
runs-on: ubuntu-latest
steps:
- name: Check branch merge target
run: |
HEAD="${{ github.head_ref }}"
BASE="${{ github.base_ref }}"
echo "PR: ${HEAD} → ${BASE}"
ALLOWED=true
REASON=""
case "$HEAD" in
feature/*|feat/*)
if [ "$BASE" != "dev" ]; then
ALLOWED=false
REASON="Feature branches must target 'dev', not '${BASE}'"
fi
;;
fix/*|bugfix/*)
if [ "$BASE" != "dev" ]; then
ALLOWED=false
REASON="Fix branches must target 'dev', not '${BASE}'"
fi
;;
hotfix/*)
if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'"
fi
;;
alpha/*|beta/*)
if [ "$BASE" != "dev" ]; then
ALLOWED=false
REASON="Pre-release branches must target 'dev', not '${BASE}'"
fi
;;
rc/*)
if [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="Release candidate branches must target 'main', not '${BASE}'"
fi
;;
dev)
if [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="Dev branch can only merge into 'main', not '${BASE}'"
fi
;;
esac
if [ "$ALLOWED" = false ]; then
echo "::error::${REASON}"
echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "${REASON}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "Branch policy: OK (${HEAD} → ${BASE})"
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
# ── Code Validation ────────────────────────────────────────────────────
validate:
name: Validate PR
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Detect platform
id: platform
run: |
# Parse manifest for platform detection
PLATFORM=$(php /tmp/mokostandards-api/cli/manifest_read.php --path . --field platform 2>/dev/null)
[ -z "$PLATFORM" ] && PLATFORM="generic"
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
- name: Setup PHP
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
run: |
if ! command -v php &> /dev/null; then
sudo apt-get update -qq
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
fi
- name: PHP syntax check
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
run: |
ERRORS=0
while IFS= read -r -d '' file; do
if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
ERRORS=$((ERRORS + 1))
fi
done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0)
echo "PHP lint: ${ERRORS} error(s)"
[ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
- name: Validate platform manifest
run: |
PLATFORM="${{ steps.platform.outputs.platform }}"
case "$PLATFORM" in
joomla)
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
if [ -z "$MANIFEST" ]; then
echo "::warning::No Joomla manifest found (WaaS site)"
exit 0
fi
echo "Manifest: ${MANIFEST}"
if command -v php &> /dev/null; then
php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::Manifest XML is malformed"; exit 1; }
fi
for ELEMENT in name version description; do
grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; }
done
echo "Joomla manifest valid"
;;
dolibarr)
MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1)
if [ -z "$MOD_FILE" ]; then
echo "::error::No mod*.class.php found"
exit 1
fi
echo "Dolibarr module: ${MOD_FILE}"
;;
*)
echo "Generic platform — no manifest validation"
;;
esac
- name: Check update stream format
run: |
PLATFORM="${{ steps.platform.outputs.platform }}"
case "$PLATFORM" in
joomla)
if [ -f "updates.xml" ]; then
if command -v php &> /dev/null; then
php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::updates.xml is malformed"; exit 1; }
fi
echo "updates.xml valid"
fi
;;
dolibarr)
[ -f "update.txt" ] && echo "update.txt present" || echo "::warning::No update.txt"
;;
esac
- name: Verify package source
run: |
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
if [ ! -d "$SOURCE_DIR" ]; then
echo "::warning::No src/ or htdocs/ directory"
exit 0
fi
FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
echo "Source: ${FILE_COUNT} files"
[ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; }
# ── Changelog Gate ────────────────────────────────────────────────────
changelog:
name: Changelog Updated
runs-on: ubuntu-latest
if: github.base_ref == 'main'
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check CHANGELOG.md was updated
run: |
BASE="${{ github.event.pull_request.base.sha }}"
HEAD="${{ github.event.pull_request.head.sha }}"
if git diff --name-only "$BASE" "$HEAD" | grep -q "^CHANGELOG.md$"; then
echo "CHANGELOG.md updated"
else
# Allow [skip changelog] in PR title or body
PR_TITLE="${{ github.event.pull_request.title }}"
PR_BODY="${{ github.event.pull_request.body }}"
if echo "$PR_TITLE $PR_BODY" | grep -qi "\[skip changelog\]"; then
echo "::warning::Changelog skip requested via [skip changelog]"
exit 0
fi
echo "::error::CHANGELOG.md must be updated before merging to main. Add [skip changelog] to the PR title to bypass."
exit 1
fi
-246
View File
@@ -1,246 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /templates/workflows/universal/pre-release.yml.template
# VERSION: 05.00.00
# BRIEF: Manual pre-release — builds dev/alpha/beta/rc packages from any branch
name: "Universal: Pre-Release"
on:
workflow_dispatch:
inputs:
stability:
description: 'Pre-release channel'
required: true
type: choice
options:
- development
- alpha
- beta
- release-candidate
permissions:
contents: write
env:
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
jobs:
build:
name: "Build Pre-Release (${{ inputs.stability }})"
runs-on: release
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GA_TOKEN }}
- name: Setup PHP
run: |
if ! command -v php &> /dev/null; then
sudo apt-get update -qq
sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl >/dev/null 2>&1
fi
- name: Setup moko-platform tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
run: |
git clone --depth 1 --branch main --quiet "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" /tmp/moko-platform-api
- name: Detect platform
id: platform
run: |
php /tmp/moko-platform-api/cli/manifest_read.php --path . --github-output
- name: Resolve metadata
id: meta
run: |
STABILITY="${{ inputs.stability }}"
MOKO_API="/tmp/moko-platform-api/cli"
case "$STABILITY" in
development) SUFFIX="-dev"; TAG="development" ;;
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
beta) SUFFIX="-beta"; TAG="beta" ;;
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
esac
# Bump patch version
BUMP_OUTPUT=$(php ${MOKO_API}/version_bump.php --path .)
VERSION=$(echo "$BUMP_OUTPUT" | grep -oP '\d{2}\.\d{2}\.\d{2}$' || true)
[ -z "$VERSION" ] && VERSION=$(php ${MOKO_API}/version_read.php --path .)
echo "Version: ${VERSION}"
# Update platform-specific manifest
php ${MOKO_API}/version_set_platform.php --path . --version "${VERSION}"
# Commit version bump
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
git add -A
git diff --cached --quiet || {
git commit -m "chore(version): bump to ${VERSION} [skip ci]"
git push origin HEAD 2>&1
}
# Detect element from Joomla/Dolibarr manifest
PLATFORM="${{ steps.platform.outputs.platform }}"
EXT_ELEMENT=$(php ${MOKO_API}/manifest_read.php --path . --field name 2>/dev/null | tr -d ' ' | tr '[:upper:]' '[:lower:]' || true)
# For Joomla, prefer <element> tag
if [ "$PLATFORM" = "joomla" ]; then
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true)
if [ -n "$MANIFEST" ]; then
ELEM=$(grep -oP "<element>\K[^<]+" "$MANIFEST" 2>/dev/null | head -1)
[ -n "$ELEM" ] && EXT_ELEMENT="$ELEM"
fi
fi
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
ZIP_NAME="${EXT_ELEMENT}-${VERSION}${SUFFIX}.zip"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
- name: Build package
id: zip
run: |
VERSION="${{ steps.meta.outputs.version }}"
SUFFIX="${{ steps.meta.outputs.suffix }}"
PLATFORM="${{ steps.platform.outputs.platform }}"
if [ "$PLATFORM" = "joomla" ]; then
php /tmp/moko-platform-api/cli/joomla_build.php --path . --version "${VERSION}" --suffix "${SUFFIX}" --output build --github-output
else
# Generic build: zip src/ directory
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
[ ! -d "$SOURCE_DIR" ] && { echo "::error::No src/ or htdocs/"; exit 1; }
EXT_ELEMENT="${{ steps.meta.outputs.ext_element }}"
ZIP_NAME="${EXT_ELEMENT}-${VERSION}${SUFFIX}.zip"
mkdir -p build
cd "$SOURCE_DIR" && zip -r "../build/${ZIP_NAME}" . && cd ..
SHA256=$(sha256sum "build/${ZIP_NAME}" | cut -d' ' -f1)
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
echo "zip_path=build/${ZIP_NAME}" >> "$GITHUB_OUTPUT"
echo "sha256=${SHA256}" >> "$GITHUB_OUTPUT"
fi
- name: Create or replace Gitea release
id: release
continue-on-error: true
run: |
TAG="${{ steps.meta.outputs.tag }}"
VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
SHA256="${{ steps.zip.outputs.sha256 }}"
ZIP_NAME="${{ steps.zip.outputs.zip_name }}"
EXT_ELEMENT="${{ steps.meta.outputs.ext_element }}"
TOKEN="${{ secrets.GA_TOKEN }}"
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
BRANCH=$(git branch --show-current)
BODY="## ${VERSION} ($(date +%Y-%m-%d))
**Channel:** ${STABILITY}
**SHA-256:** \`${SHA256}\`"
# Delete existing release
EXISTING_ID=$(curl -sS -H "Authorization: token ${TOKEN}" \
"${API}/releases/tags/${TAG}" | jq -r '.id // empty' 2>/dev/null)
if [ -n "$EXISTING_ID" ]; then
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
"${API}/releases/${EXISTING_ID}" 2>/dev/null || true
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
"${API}/tags/${TAG}" 2>/dev/null || true
fi
# Create release
RELEASE_ID=$(curl -sS -X POST -H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
"${API}/releases" \
-d "$(jq -n \
--arg tag "$TAG" \
--arg target "$BRANCH" \
--arg name "${EXT_ELEMENT} ${VERSION} (${STABILITY})" \
--arg body "$BODY" \
'{tag_name: $tag, target_commitish: $target, name: $name, body: $body, prerelease: true}'
)" | jq -r '.id')
echo "release_id=${RELEASE_ID}" >> "$GITHUB_OUTPUT"
# Upload ZIP
curl -sS -X POST -H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/octet-stream" \
"${API}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" \
--data-binary "@${{ steps.zip.outputs.zip_path }}"
echo "Released: ${EXT_ELEMENT} ${VERSION} (${STABILITY})"
- name: "Update updates.xml"
if: steps.platform.outputs.platform == 'joomla'
run: |
VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
SHA256="${{ steps.zip.outputs.sha256 }}"
php /tmp/moko-platform-api/cli/updates_xml_build.php --path . --version "$VERSION" --stability "$STABILITY" --sha "$SHA256" --gitea-url "$GITEA_URL" --org "$GITEA_ORG" --repo "$GITEA_REPO"
if ! git diff --quiet updates.xml 2>/dev/null; then
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git add updates.xml
git commit -m "chore: update $STABILITY channel $VERSION [skip ci]"
git push origin HEAD 2>&1 || echo "WARNING: push failed"
fi
- name: "Sync updates.xml to all branches"
if: steps.platform.outputs.platform == 'joomla'
run: |
php /tmp/moko-platform-api/cli/updates_xml_sync.php --path . --current "${{ github.ref_name }}" --branches main,dev --version "${{ steps.meta.outputs.version }}" --token "${{ secrets.GA_TOKEN }}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" --gitea-url "${GITEA_URL}"
- name: "Delete lesser pre-release channels (cascade)"
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.GA_TOKEN }}"
STABILITY="${{ steps.meta.outputs.stability }}"
# Cascade: rc → beta,alpha,dev | beta → alpha,dev | alpha → dev | dev → nothing
case "$STABILITY" in
release-candidate) TAGS_TO_DELETE="beta alpha development" ;;
beta) TAGS_TO_DELETE="alpha development" ;;
alpha) TAGS_TO_DELETE="development" ;;
*) TAGS_TO_DELETE="" ;;
esac
[ -z "$TAGS_TO_DELETE" ] && exit 0
for TAG in $TAGS_TO_DELETE; do
RELEASE_ID=$(curl -sS -H "Authorization: token ${TOKEN}" \
"${API_BASE}/releases/tags/${TAG}" 2>/dev/null | \
python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "None" ]; then
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
"${API_BASE}/releases/${RELEASE_ID}" 2>/dev/null || true
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
"${API_BASE}/tags/${TAG}" 2>/dev/null || true
echo "Deleted: ${TAG} (id: ${RELEASE_ID})"
fi
done
-766
View File
@@ -1,766 +0,0 @@
# ============================================================================
# Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
#
# This file is part of a Moko Consulting project.
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Validation
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
# PATH: /templates/workflows/joomla/repo_health.yml.template
# VERSION: 04.06.00
# BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts.
# ============================================================================
name: "Joomla: Repo Health"
concurrency:
group: repo-health-${{ github.repository }}-${{ github.ref }}
cancel-in-progress: true
defaults:
run:
shell: bash
on:
workflow_dispatch:
inputs:
profile:
description: 'Validation profile: all, release, scripts, or repo'
required: true
default: all
type: choice
options:
- all
- release
- scripts
- repo
pull_request:
push:
permissions:
contents: read
env:
# Release policy - Repository Variables Only
RELEASE_REQUIRED_REPO_VARS: RS_FTP_PATH_SUFFIX
RELEASE_OPTIONAL_REPO_VARS: DEV_FTP_SUFFIX
# Scripts governance policy
SCRIPTS_REQUIRED_DIRS:
SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate
# Repo health policy
REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.gitea/workflows/
REPO_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore,README.md,docs/
REPO_DISALLOWED_DIRS:
REPO_DISALLOWED_FILES: TODO.md,todo.md
# Extended checks toggles
EXTENDED_CHECKS: "true"
# File / directory variables
DOCS_INDEX: docs/docs-index.md
SCRIPT_DIR: scripts
WORKFLOWS_DIR: .gitea/workflows
SHELLCHECK_PATTERN: '*.sh'
SPDX_FILE_GLOBS: '*.sh,*.php,*.js,*.ts,*.css,*.xml,*.yml,*.yaml'
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
access_check:
name: Access control
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
outputs:
allowed: ${{ steps.perm.outputs.allowed }}
permission: ${{ steps.perm.outputs.permission }}
steps:
- name: Check actor permission (admin only)
id: perm
env:
TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
REPO: ${{ github.repository }}
ACTOR: ${{ github.actor }}
run: |
set -euo pipefail
ALLOWED=false
PERMISSION=unknown
METHOD=""
# Hardcoded authorized users — always allowed
case "$ACTOR" in
jmiller|gitea-actions[bot])
ALLOWED=true
PERMISSION=admin
METHOD="hardcoded allowlist"
;;
*)
# Detect platform and check permissions via API
API_BASE="${GITHUB_API_URL:-${GITEA_API_URL:-https://api.github.com}}"
RESP=$(curl -sf -H "Authorization: token ${TOKEN}" \
"${API_BASE}/repos/${REPO}/collaborators/${ACTOR}/permission" 2>/dev/null || echo '{}')
PERMISSION=$(echo "$RESP" | grep -oP '"permission"\s*:\s*"\K[^"]+' || echo "unknown")
if [ "$PERMISSION" = "admin" ] || [ "$PERMISSION" = "maintain" ] || [ "$PERMISSION" = "owner" ]; then
ALLOWED=true
fi
METHOD="collaborator API"
;;
esac
echo "permission=${PERMISSION}" >> "$GITHUB_OUTPUT"
echo "allowed=${ALLOWED}" >> "$GITHUB_OUTPUT"
{
echo "## Access Authorization"
echo ""
echo "| Field | Value |"
echo "|-------|-------|"
echo "| **Actor** | \`${ACTOR}\` |"
echo "| **Repository** | \`${REPO}\` |"
echo "| **Permission** | \`${PERMISSION}\` |"
echo "| **Method** | ${METHOD} |"
echo "| **Authorized** | ${ALLOWED} |"
echo ""
if [ "$ALLOWED" = "true" ]; then
echo "${ACTOR} authorized (${METHOD})"
else
echo "${ACTOR} is NOT authorized. Requires admin or maintain role."
fi
} >> "${GITHUB_STEP_SUMMARY}"
- name: Deny execution when not permitted
if: ${{ steps.perm.outputs.allowed != 'true' }}
run: |
set -euo pipefail
printf '%s\n' 'ERROR: Access denied. Admin permission required.' >> "${GITHUB_STEP_SUMMARY}"
exit 1
release_config:
name: Release configuration
needs: access_check
if: ${{ needs.access_check.outputs.allowed == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
- name: Guardrails release vars
env:
PROFILE_RAW: ${{ github.event.inputs.profile }}
RS_FTP_PATH_SUFFIX: ${{ vars.RS_FTP_PATH_SUFFIX }}
DEV_FTP_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
run: |
set -euo pipefail
profile="${PROFILE_RAW:-all}"
case "${profile}" in
all|release|scripts|repo) ;;
*)
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
exit 1
;;
esac
if [ "${profile}" = 'scripts' ] || [ "${profile}" = 'repo' ]; then
{
printf '%s\n' '### Release configuration (Repository Variables)'
printf '%s\n' "Profile: ${profile}"
printf '%s\n' 'Status: SKIPPED'
printf '%s\n' 'Reason: profile excludes release validation'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
exit 0
fi
IFS=',' read -r -a required <<< "${RELEASE_REQUIRED_REPO_VARS}"
IFS=',' read -r -a optional <<< "${RELEASE_OPTIONAL_REPO_VARS}"
missing=()
missing_optional=()
for k in "${required[@]}"; do
v="${!k:-}"
[ -z "${v}" ] && missing+=("${k}")
done
for k in "${optional[@]}"; do
v="${!k:-}"
[ -z "${v}" ] && missing_optional+=("${k}")
done
{
printf '%s\n' '### Release configuration (Repository Variables)'
printf '%s\n' "Profile: ${profile}"
printf '%s\n' '| Variable | Status |'
printf '%s\n' '|---|---|'
printf '%s\n' "| RS_FTP_PATH_SUFFIX | ${RS_FTP_PATH_SUFFIX:-NOT SET} |"
printf '%s\n' "| DEV_FTP_SUFFIX | ${DEV_FTP_SUFFIX:-NOT SET} |"
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
if [ "${#missing_optional[@]}" -gt 0 ]; then
{
printf '%s\n' '### Missing optional repository variables'
for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
if [ "${#missing[@]}" -gt 0 ]; then
{
printf '%s\n' '### Missing required repository variables'
for m in "${missing[@]}"; do printf '%s\n' "- ${m}"; done
printf '%s\n' 'ERROR: Guardrails failed. Missing required repository variables.'
} >> "${GITHUB_STEP_SUMMARY}"
exit 1
fi
{
printf '%s\n' '### Repository variables validation result'
printf '%s\n' 'Status: OK'
printf '%s\n' 'All required repository variables present.'
printf '%s\n' ''
printf '%s\n' '**Note**: Organization secrets (RS_FTP_HOST, RS_FTP_USER, etc.) are validated at deployment time, not in repository health checks.'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
scripts_governance:
name: Scripts governance
needs: access_check
if: ${{ needs.access_check.outputs.allowed == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
- name: Scripts folder checks
env:
PROFILE_RAW: ${{ github.event.inputs.profile }}
run: |
set -euo pipefail
profile="${PROFILE_RAW:-all}"
case "${profile}" in
all|release|scripts|repo) ;;
*)
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
exit 1
;;
esac
if [ "${profile}" = 'release' ] || [ "${profile}" = 'repo' ]; then
{
printf '%s\n' '### Scripts governance'
printf '%s\n' "Profile: ${profile}"
printf '%s\n' 'Status: SKIPPED'
printf '%s\n' 'Reason: profile excludes scripts governance'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
exit 0
fi
if [ ! -d "${SCRIPT_DIR}" ]; then
{
printf '%s\n' '### Scripts governance'
printf '%s\n' 'Status: OK (advisory)'
printf '%s\n' 'scripts/ directory not present. No scripts governance enforced.'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
exit 0
fi
IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}"
IFS=',' read -r -a allowed_dirs <<< "${SCRIPTS_ALLOWED_DIRS}"
missing_dirs=()
unapproved_dirs=()
for d in "${required_dirs[@]}"; do
req="${d%/}"
[ ! -d "${req}" ] && missing_dirs+=("${req}/")
done
while IFS= read -r d; do
allowed=false
for a in "${allowed_dirs[@]}"; do
a_norm="${a%/}"
[ "${d%/}" = "${a_norm}" ] && allowed=true
done
[ "${allowed}" = false ] && unapproved_dirs+=("${d%/}/")
done < <(find "${SCRIPT_DIR}" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sed 's#^\./##')
{
printf '%s\n' '### Scripts governance'
printf '%s\n' "Profile: ${profile}"
printf '%s\n' '| Area | Status | Notes |'
printf '%s\n' '|---|---|---|'
if [ "${#missing_dirs[@]}" -gt 0 ]; then
printf '%s\n' '| Required directories | Warning | Missing required subfolders |'
else
printf '%s\n' '| Required directories | OK | All required subfolders present |'
fi
if [ "${#unapproved_dirs[@]}" -gt 0 ]; then
printf '%s\n' '| Directory policy | Warning | Unapproved directories detected |'
else
printf '%s\n' '| Directory policy | OK | No unapproved directories |'
fi
printf '%s\n' '| Enforcement mode | Advisory | scripts folder is optional |'
printf '\n'
if [ "${#missing_dirs[@]}" -gt 0 ]; then
printf '%s\n' 'Missing required script directories:'
for m in "${missing_dirs[@]}"; do printf '%s\n' "- ${m}"; done
printf '\n'
else
printf '%s\n' 'Missing required script directories: none.'
printf '\n'
fi
if [ "${#unapproved_dirs[@]}" -gt 0 ]; then
printf '%s\n' 'Unapproved script directories detected:'
for m in "${unapproved_dirs[@]}"; do printf '%s\n' "- ${m}"; done
printf '\n'
else
printf '%s\n' 'Unapproved script directories detected: none.'
printf '\n'
fi
printf '%s\n' 'Scripts governance completed in advisory mode.'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
repo_health:
name: Repository health
needs: access_check
if: ${{ needs.access_check.outputs.allowed == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
- name: Repository health checks
env:
PROFILE_RAW: ${{ github.event.inputs.profile }}
run: |
set -euo pipefail
profile="${PROFILE_RAW:-all}"
case "${profile}" in
all|release|scripts|repo) ;;
*)
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
exit 1
;;
esac
if [ "${profile}" = 'release' ] || [ "${profile}" = 'scripts' ]; then
{
printf '%s\n' '### Repository health'
printf '%s\n' "Profile: ${profile}"
printf '%s\n' 'Status: SKIPPED'
printf '%s\n' 'Reason: profile excludes repository health'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
exit 0
fi
# Source directory: src/ or htdocs/ (either is valid)
if [ -d "src" ]; then
SOURCE_DIR="src"
elif [ -d "htdocs" ]; then
SOURCE_DIR="htdocs"
else
missing_required+=("src/ or htdocs/ (source directory required)")
fi
IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}"
IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}"
IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}"
IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES}"
missing_required=()
missing_optional=()
for item in "${required_artifacts[@]}"; do
if printf '%s' "${item}" | grep -q '/$'; then
d="${item%/}"
[ ! -d "${d}" ] && missing_required+=("${item}")
else
[ ! -f "${item}" ] && missing_required+=("${item}")
fi
done
for f in "${optional_files[@]}"; do
if printf '%s' "${f}" | grep -q '/$'; then
d="${f%/}"
[ ! -d "${d}" ] && missing_optional+=("${f}")
else
[ ! -f "${f}" ] && missing_optional+=("${f}")
fi
done
for d in "${disallowed_dirs[@]}"; do
d_norm="${d%/}"
[ -d "${d_norm}" ] && missing_required+=("${d_norm}/ (disallowed)")
done
for f in "${disallowed_files[@]}"; do
[ -f "${f}" ] && missing_required+=("${f} (disallowed)")
done
git fetch origin --prune
dev_paths=()
dev_branches=()
while IFS= read -r b; do
name="${b#origin/}"
if [ "${name}" = 'dev' ]; then
dev_branches+=("${name}")
else
dev_paths+=("${name}")
fi
done < <(git branch -r --list 'origin/dev*' | sed 's/^ *//')
if [ "${#dev_paths[@]}" -eq 0 ]; then
missing_required+=("dev/* branch (e.g. dev/01.00.00)")
fi
if [ "${#dev_branches[@]}" -gt 0 ]; then
missing_required+=("invalid branch dev (must be dev/<version>)")
fi
content_warnings=()
if [ -f 'CHANGELOG.md' ] && ! grep -Eq '^# Changelog' CHANGELOG.md; then
content_warnings+=("CHANGELOG.md missing '# Changelog' header")
fi
if [ -f 'CHANGELOG.md' ] && grep -Eq '^[# ]*Unreleased' CHANGELOG.md; then
content_warnings+=("CHANGELOG.md contains Unreleased section (review release readiness)")
fi
if [ -f 'LICENSE' ] && ! grep -qiE 'GNU GENERAL PUBLIC LICENSE|GPL' LICENSE; then
content_warnings+=("LICENSE does not look like a GPL text")
fi
if [ -f 'README.md' ] && ! grep -qiE 'moko|Moko' README.md; then
content_warnings+=("README.md missing expected brand keyword")
fi
export PROFILE_RAW="${profile}"
export MISSING_REQUIRED="$(printf '%s\n' "${missing_required[@]:-}")"
export MISSING_OPTIONAL="$(printf '%s\n' "${missing_optional[@]:-}")"
export CONTENT_WARNINGS="$(printf '%s\n' "${content_warnings[@]:-}")"
report_json="$(python3 - <<'PY'
import json
import os
profile = os.environ.get('PROFILE_RAW') or 'all'
missing_required = os.environ.get('MISSING_REQUIRED', '').splitlines() if os.environ.get('MISSING_REQUIRED') else []
missing_optional = os.environ.get('MISSING_OPTIONAL', '').splitlines() if os.environ.get('MISSING_OPTIONAL') else []
content_warnings = os.environ.get('CONTENT_WARNINGS', '').splitlines() if os.environ.get('CONTENT_WARNINGS') else []
out = {
'profile': profile,
'missing_required': [x for x in missing_required if x],
'missing_optional': [x for x in missing_optional if x],
'content_warnings': [x for x in content_warnings if x],
}
print(json.dumps(out, indent=2))
PY
)"
{
printf '%s\n' '### Repository health'
printf '%s\n' "Profile: ${profile}"
printf '%s\n' '| Metric | Value |'
printf '%s\n' '|---|---|'
printf '%s\n' "| Missing required | ${#missing_required[@]} |"
printf '%s\n' "| Missing optional | ${#missing_optional[@]} |"
printf '%s\n' "| Content warnings | ${#content_warnings[@]} |"
printf '\n'
printf '%s\n' '### Guardrails report (JSON)'
printf '%s\n' '```json'
printf '%s\n' "${report_json}"
printf '%s\n' '```'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
if [ "${#missing_required[@]}" -gt 0 ]; then
{
printf '%s\n' '### Missing required repo artifacts'
for m in "${missing_required[@]}"; do printf '%s\n' "- ${m}"; done
printf '%s\n' 'ERROR: Guardrails failed. Missing required repository artifacts.'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
exit 1
fi
if [ "${#missing_optional[@]}" -gt 0 ]; then
{
printf '%s\n' '### Missing optional repo artifacts'
for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
if [ "${#content_warnings[@]}" -gt 0 ]; then
{
printf '%s\n' '### Repo content warnings'
for m in "${content_warnings[@]}"; do printf '%s\n' "- ${m}"; done
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
# -- Joomla-specific checks --
joomla_findings=()
MANIFEST="$(find . -maxdepth 2 -name '*.xml' -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true)"
if [ -z "${MANIFEST}" ]; then
joomla_findings+=("Joomla XML manifest not found (no *.xml with <extension> tag)")
else
if ! grep -qP '<version>' "${MANIFEST}"; then
joomla_findings+=("XML manifest: <version> tag missing")
fi
if ! grep -qP 'type="(component|module|plugin|library|package|template|language)"' "${MANIFEST}"; then
joomla_findings+=("XML manifest: type attribute missing or invalid")
fi
if ! grep -qP '<name>' "${MANIFEST}"; then
joomla_findings+=("XML manifest: <name> tag missing")
fi
if ! grep -qP '<author>' "${MANIFEST}"; then
joomla_findings+=("XML manifest: <author> tag missing")
fi
if ! grep -qP '<namespace' "${MANIFEST}"; then
joomla_findings+=("XML manifest: <namespace> missing (required for Joomla 5+)")
fi
fi
INI_COUNT="$(find . -name '*.ini' -type f 2>/dev/null | wc -l)"
if [ "${INI_COUNT}" -eq 0 ]; then
joomla_findings+=("No .ini language files found")
fi
if [ ! -f 'updates.xml' ]; then
joomla_findings+=("updates.xml missing in root (required for Joomla update server)")
fi
INDEX_DIRS=("${SOURCE_DIR}" "${SOURCE_DIR}/admin" "${SOURCE_DIR}/site")
for dir in "${INDEX_DIRS[@]}"; do
if [ -d "${dir}" ] && [ ! -f "${dir}/index.html" ]; then
joomla_findings+=("${dir}/index.html missing (directory listing protection)")
fi
done
if [ "${#joomla_findings[@]}" -gt 0 ]; then
{
printf '%s\n' '### Joomla extension checks'
printf '%s\n' '| Check | Status |'
printf '%s\n' '|---|---|'
for f in "${joomla_findings[@]}"; do
printf '%s\n' "| ${f} | Warning |"
done
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
else
{
printf '%s\n' '### Joomla extension checks'
printf '%s\n' 'All Joomla-specific checks passed.'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
extended_enabled="${EXTENDED_CHECKS:-true}"
extended_findings=()
if [ "${extended_enabled}" = 'true' ]; then
if [ -f '.github/CODEOWNERS' ] || [ -f 'CODEOWNERS' ] || [ -f 'docs/CODEOWNERS' ]; then
:
else
extended_findings+=("CODEOWNERS not found (.github/CODEOWNERS preferred)")
fi
if ls "${WORKFLOWS_DIR}"/*.yml >/dev/null 2>&1 || ls "${WORKFLOWS_DIR}"/*.yaml >/dev/null 2>&1; then
bad_refs="$(grep -RIn --include='*.yml' --include='*.yaml' -E '^[[:space:]]*uses:[[:space:]]*[^#]+@(main|master)\b' "${WORKFLOWS_DIR}" 2>/dev/null || true)"
if [ -n "${bad_refs}" ]; then
extended_findings+=("Workflows reference actions @main/@master (pin versions): see log excerpt")
{
printf '%s\n' '### Workflow pinning advisory'
printf '%s\n' 'Found uses: entries pinned to main/master:'
printf '%s\n' '```'
printf '%s\n' "${bad_refs}"
printf '%s\n' '```'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
fi
if [ -f "${DOCS_INDEX}" ]; then
missing_links="$(python3 - <<'PY'
import os
import re
idx = os.environ.get('DOCS_INDEX', 'docs/docs-index.md')
base = os.getcwd()
bad = []
pat = re.compile(r'\[[^\]]+\]\(([^)]+)\)')
with open(idx, 'r', encoding='utf-8') as f:
for line in f:
for m in pat.findall(line):
link = m.strip()
if link.startswith('http://') or link.startswith('https://') or link.startswith('#') or link.startswith('mailto:'):
continue
if link.startswith('/'):
rel = link.lstrip('/')
else:
rel = os.path.normpath(os.path.join(os.path.dirname(idx), link))
rel = rel.split('#', 1)[0]
rel = rel.split('?', 1)[0]
if not rel:
continue
p = os.path.join(base, rel)
if not os.path.exists(p):
bad.append(rel)
print('\n'.join(sorted(set(bad))))
PY
)"
if [ -n "${missing_links}" ]; then
extended_findings+=("docs/docs-index.md contains broken relative links")
{
printf '%s\n' '### Docs index link integrity'
printf '%s\n' 'Broken relative links:'
while IFS= read -r l; do [ -n "${l}" ] && printf '%s\n' "- ${l}"; done <<< "${missing_links}"
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
fi
if [ -d "${SCRIPT_DIR}" ]; then
if ! command -v shellcheck >/dev/null 2>&1; then
sudo apt-get update -qq
sudo apt-get install -y shellcheck >/dev/null
fi
sc_out=''
while IFS= read -r shf; do
[ -z "${shf}" ] && continue
out_one="$(shellcheck -S warning -x "${shf}" 2>/dev/null || true)"
if [ -n "${out_one}" ]; then
sc_out="${sc_out}${out_one}\n"
fi
done < <(find "${SCRIPT_DIR}" -type f -name "${SHELLCHECK_PATTERN}" 2>/dev/null | sort)
if [ -n "${sc_out}" ]; then
extended_findings+=("ShellCheck warnings detected (advisory)")
sc_head="$(printf '%s' "${sc_out}" | head -n 200)"
{
printf '%s\n' '### ShellCheck (advisory)'
printf '%s\n' '```'
printf '%s\n' "${sc_head}"
printf '%s\n' '```'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
fi
spdx_missing=()
IFS=',' read -r -a spdx_globs <<< "${SPDX_FILE_GLOBS}"
spdx_args=()
for g in "${spdx_globs[@]}"; do spdx_args+=("${g}"); done
while IFS= read -r f; do
[ -z "${f}" ] && continue
if ! head -n 40 "${f}" | grep -q 'SPDX-License-Identifier:'; then
spdx_missing+=("${f}")
fi
done < <(git ls-files "${spdx_args[@]}" 2>/dev/null || true)
if [ "${#spdx_missing[@]}" -gt 0 ]; then
extended_findings+=("SPDX header missing in some tracked files (advisory)")
{
printf '%s\n' '### SPDX header advisory'
printf '%s\n' 'Files missing SPDX-License-Identifier (first 40 lines scan):'
for f in "${spdx_missing[@]}"; do printf '%s\n' "- ${f}"; done
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
stale_cutoff_days=180
stale_branches="$(git for-each-ref --format='%(refname:short) %(committerdate:unix)' refs/remotes/origin 2>/dev/null | awk -v now="$(date +%s)" -v days="${stale_cutoff_days}" '{if (now-$2 > days*86400) print $1}' | head -50)"
if [ -n "${stale_branches}" ]; then
extended_findings+=("Stale remote branches detected (advisory)")
{
printf '%s\n' '### Git hygiene advisory'
printf '%s\n' "Branches with last commit older than ${stale_cutoff_days} days (sample up to 50):"
while IFS= read -r b; do [ -n "${b}" ] && printf '%s\n' "- ${b}"; done <<< "${stale_branches}"
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
fi
{
printf '%s\n' '### Guardrails coverage matrix'
printf '%s\n' '| Domain | Status | Notes |'
printf '%s\n' '|---|---|---|'
printf '%s\n' '| Access control | OK | Admin-only execution gate |'
printf '%s\n' '| Release variables | OK | Repository variables validation |'
printf '%s\n' '| Scripts governance | OK | Directory policy and advisory reporting |'
printf '%s\n' '| Repo required artifacts | OK | Required, optional, disallowed enforcement |'
printf '%s\n' '| Repo content heuristics | OK | Brand, license, changelog structure |'
if [ "${extended_enabled}" = 'true' ]; then
if [ "${#extended_findings[@]}" -gt 0 ]; then
printf '%s\n' '| Extended checks | Warning | See extended findings below |'
else
printf '%s\n' '| Extended checks | OK | No findings |'
fi
else
printf '%s\n' '| Extended checks | SKIPPED | EXTENDED_CHECKS disabled |'
fi
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
if [ "${extended_enabled}" = 'true' ] && [ "${#extended_findings[@]}" -gt 0 ]; then
{
printf '%s\n' '### Extended findings (advisory)'
for f in "${extended_findings[@]}"; do printf '%s\n' "- ${f}"; done
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
printf '%s\n' 'Repository health guardrails passed.' >> "${GITHUB_STEP_SUMMARY}"
-82
View File
@@ -1,82 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Security
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /.gitea/workflows/security-audit.yml
# VERSION: 01.00.00
# BRIEF: Dependency vulnerability scanning for composer and npm packages
name: "Universal: Security Audit"
on:
schedule:
- cron: '0 6 * * 1' # Weekly on Monday at 06:00 UTC
pull_request:
branches:
- main
paths:
- 'composer.json'
- 'composer.lock'
- 'package.json'
- 'package-lock.json'
workflow_dispatch:
permissions:
contents: read
env:
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }}
jobs:
audit:
name: Dependency Audit
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Composer audit
if: hashFiles('composer.lock') != ''
run: |
echo "=== Composer Security Audit ==="
if ! command -v composer &> /dev/null; then
sudo apt-get update -qq
sudo apt-get install -y -qq php-cli composer >/dev/null 2>&1
fi
composer audit --format=plain 2>&1 | tee /tmp/composer-audit.txt
RESULT=$?
if [ $RESULT -ne 0 ]; then
echo "::warning::Composer vulnerabilities found"
echo "composer_vulnerable=true" >> "$GITHUB_ENV"
else
echo "No known vulnerabilities in composer dependencies"
fi
- name: NPM audit
if: hashFiles('package-lock.json') != ''
run: |
echo "=== NPM Security Audit ==="
npm audit --production 2>&1 | tee /tmp/npm-audit.txt || true
if npm audit --production 2>&1 | grep -q "found 0 vulnerabilities"; then
echo "No known vulnerabilities in npm dependencies"
else
echo "::warning::NPM vulnerabilities found"
echo "npm_vulnerable=true" >> "$GITHUB_ENV"
fi
- name: Notify on vulnerabilities
if: env.composer_vulnerable == 'true' || env.npm_vulnerable == 'true'
run: |
REPO="${{ github.event.repository.name }}"
curl -sS \
-H "Title: ${REPO} has vulnerable dependencies" \
-H "Tags: lock,warning" \
-H "Priority: high" \
-d "Security audit found vulnerabilities. Review dependency updates." \
"${NTFY_URL}/${NTFY_TOPIC}" || true
-464
View File
@@ -1,464 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Joomla
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
# PATH: /templates/workflows/joomla/update-server.yml.template
# VERSION: 04.06.00
# BRIEF: Update Joomla update server XML feed with stable/rc/dev entries
#
# Writes updates.xml with multiple <update> entries:
# - <tag>stable</tag> on push to main (from auto-release)
# - <tag>rc</tag> on push to rc/**
# - <tag>development</tag> on push to dev or dev/**
#
# Joomla filters by user's "Minimum Stability" setting.
name: "Joomla: Update Server"
on:
push:
branches:
- 'dev'
- 'dev/**'
- 'alpha/**'
- 'beta/**'
- 'rc/**'
paths:
- 'src/**'
- 'htdocs/**'
pull_request:
types: [closed]
branches:
- 'dev'
- 'dev/**'
- 'alpha/**'
- 'beta/**'
- 'rc/**'
paths:
- 'src/**'
- 'htdocs/**'
workflow_dispatch:
inputs:
stability:
description: 'Stability tag'
required: true
default: 'development'
type: choice
options:
- development
- alpha
- beta
- rc
- stable
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
permissions:
contents: write
jobs:
update-xml:
name: Update updates.xml
runs-on: release
if: >-
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || github.event_name == 'push'
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.GA_TOKEN }}
fetch-depth: 0
- name: Setup MokoStandards tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.GA_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
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
/tmp/mokostandards-api 2>/dev/null || true
if [ -d "/tmp/mokostandards-api" ] && [ -f "/tmp/mokostandards-api/composer.json" ]; then
cd /tmp/mokostandards-api && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
fi
- name: Generate updates.xml entry
id: update
run: |
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)
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
BUMPED=$(php /tmp/mokostandards-api/cli/version_bump.php --path . 2>/dev/null || true)
if [ -n "$BUMPED" ]; then
VERSION=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null || echo "$VERSION")
git add -A
git commit -m "chore(version): auto-bump patch ${VERSION} [skip ci]" \
--author="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
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
STABILITY="${{ inputs.stability }}"
elif [[ "$BRANCH" == rc/* ]]; then
STABILITY="rc"
elif [[ "$BRANCH" == beta/* ]]; then
STABILITY="beta"
elif [[ "$BRANCH" == alpha/* ]]; then
STABILITY="alpha"
elif [[ "$BRANCH" == dev/* ]] || [[ "$BRANCH" == "dev" ]]; then
STABILITY="development"
else
STABILITY="stable"
fi
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
# Parse manifest (portable — no grep -P)
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "./build/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
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 commit -m "chore: update updates.xml (${STABILITY}: ${DISPLAY_VERSION}) [skip ci]" \
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
git push
}
# -- Sync updates.xml to main (for non-main branches) ----------------------
- name: Sync updates.xml to main
if: github.ref_name != 'main'
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
GA_TOKEN="${{ secrets.GA_TOKEN }}"
FILE_SHA=$(curl -sf -H "Authorization: token ${GA_TOKEN}" \
"${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then
CONTENT=$(base64 -w0 updates.xml)
curl -sf -X PUT -H "Authorization: token ${GA_TOKEN}" \
-H "Content-Type: application/json" \
"${API_BASE}/contents/updates.xml" \
-d "$(python3 -c "import json; print(json.dumps({
'content': '${CONTENT}',
'sha': '${FILE_SHA}',
'message': 'chore: sync updates.xml from ${STABILITY} [skip ci]',
'branch': 'main'
}))")" > /dev/null 2>&1 \
&& echo "updates.xml synced to main (${STABILITY})" >> $GITHUB_STEP_SUMMARY \
|| echo "WARNING: failed to sync updates.xml to main" >> $GITHUB_STEP_SUMMARY
else
echo "WARNING: could not get updates.xml SHA from main" >> $GITHUB_STEP_SUMMARY
fi
- name: SFTP deploy to dev server
if: contains(github.ref, 'dev/') || github.ref == 'refs/heads/dev'
env:
DEV_HOST: ${{ vars.DEV_FTP_HOST }}
DEV_PATH: ${{ vars.DEV_FTP_PATH }}
DEV_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
DEV_USER: ${{ vars.DEV_FTP_USERNAME }}
DEV_PORT: ${{ vars.DEV_FTP_PORT }}
DEV_KEY: ${{ secrets.DEV_FTP_KEY }}
DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
run: |
# -- Permission check: admin or maintain role required --------
ACTOR="${{ github.actor }}"
REPO="${{ github.repository }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
"${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \
python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read")
case "$PERMISSION" in
admin|maintain|write) ;;
*)
echo "Deploy denied: ${ACTOR} has '${PERMISSION}' — requires admin, maintain, or write"
exit 0
;;
esac
[ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured — skipping SFTP"; exit 0; }
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
[ ! -d "$SOURCE_DIR" ] && exit 0
PORT="${DEV_PORT:-22}"
REMOTE="${DEV_PATH%/}"
[ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}"
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
"$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json
if [ -n "$DEV_KEY" ]; then
echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
else
printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json
fi
PLATFORM=$(php /tmp/mokostandards-api/cli/platform_detect.php --path . 2>/dev/null || true)
if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards-api/deploy/deploy-joomla.php" ]; then
php /tmp/mokostandards-api/deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
elif [ -f "/tmp/mokostandards-api/deploy/deploy-sftp.php" ]; then
php /tmp/mokostandards-api/deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
fi
rm -f /tmp/deploy_key /tmp/sftp-config.json
echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY
- name: Summary
if: always()
run: |
echo "## Joomla Update Server" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Version | \`${DISPLAY_VERSION}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Element | \`${EXT_ELEMENT}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Download | [ZIP](${DOWNLOAD_URL}) |" >> $GITHUB_STEP_SUMMARY
+71 -316
View File
@@ -14,328 +14,83 @@
INGROUP: MokoWaaS.Documentation
REPO: https://github.com/mokoconsulting-tech/mokowaas
PATH: ./CHANGELOG.md
VERSION: 02.01.08
VERSION: 02.31.00
BRIEF: Version history using `Keep a Changelog`
-->
# Changelog
## [02.31.00] - 2026-06-01
### Added
- License key support via Joomla's native Update Sites download key system (dlid)
- Update server URL migrated from static XML to MokoGitea's dynamic update feed endpoint
- Legacy static update site URLs auto-migrated to dynamic endpoint on install/update
- Persistent admin warning when no license key is configured in Update Sites
- Daily heartbeat validation of license key against MokoGitea — warns if key is invalid or expired
- Stale/duplicate update site cleanup on install/update (removes old static URL entries and orphaned records)
- Content sync rewritten — bulk MokoWaaS API endpoints (syncclear + syncpush) replace per-item Joomla API calls
- Sync task per-instance config: target URL, health token, content type checkboxes (articles, categories, menus, modules)
- Bulk sync completes in under 5 seconds (clear + push in 2-3 HTTP requests)
- Asset table and nested set tree repair after sync push on target site
- Enhanced dev mode: disables caching, enables Joomla + MokoOnyx debug, suppresses hit recording, shows offline on primary domain
- Dev mode off: clears content versions, resets hits, disables debug, takes site online
- Hardcoded dev alias (dev.{primary_domain}) with noindex/nofollow — bypasses offline mode for development
- Primary domain auto-detected on first config save
### Changed
- Branding, master user, support URL, and admin colors are now hardcoded (no longer configurable)
- Master user enforcement is always active (toggle removed)
- Diagnostics + maintenance merged into default config tab
- Emergency access moved to Security tab
- Content sync configuration moved from system plugin to individual scheduled task instances
### Removed
- Static `updates.xml` — update feed is now generated dynamically by MokoGitea from git releases
- Basic branding config tab (brand name, company name, support URL)
- Visual branding config tab (colors, icon, custom CSS)
- WaaS Access config tab (master user toggle, master email)
- Content Sync config tab (targets now in scheduled tasks)
- Site Aliases config tab (hardcoded to dev.{primary_domain})
- File sync (images/, files/, media/) — sync is API/DB content only
## [02.29.03] - 2026-05-31
### Added
- `allow_extension_updates` param — separate update rights from installer restrictions; tenants can update extensions by default even when the installer is restricted
- Hardcoded master usernames — multiple privileged users supported with identical access
### Fixed
- Emergency access IP whitelist: empty `allowed_ips` now permits all IPs (was blocking everyone)
- Emergency access reads `allowed_ips` from plugin params instead of global config
- `plg_task_mokowaassync` — Joomla Scheduled Task plugin for automatic content sync to remote sites
- Community Builder tables added to demo reset safe table list
- API endpoint `POST /api/index.php/v1/mokowaas/install` — install extensions from a remote ZIP URL
- Demo Mode with configurable warning banner on frontend when enabled
### Fixed
- Demo banner countdown now shows weeks/days/months for longer intervals instead of raw hours
- `DemoResetService` — baseline snapshot and restore for DB tables + media files
- API endpoints `POST /?mokowaas=reset` and `POST /?mokowaas=snapshot` (query-string)
- REST endpoints `POST /api/v1/mokowaas/reset` and `GET/POST /api/v1/mokowaas/snapshot`
- `plg_task_mokowaasdemo` — Joomla Scheduled Task plugin for automatic demo site reset
- Admin toggles: Take Snapshot Now and Restore Baseline Now in plugin config
- Content Sync: one-way push of articles, categories, menus, and modules to remote MokoWaaS sites
- Content Sync: API endpoints `POST /?mokowaas=sync` (sender) and `POST /?mokowaas=sync-receive` (receiver)
- Content Sync: REST endpoints `POST /api/v1/mokowaas/sync` and `POST /api/v1/mokowaas/sync-receive`
- Content Sync: configurable sync targets with URL + API token in plugin settings
- Package installer: protect all MokoWaaS extensions (not just system plugin) and ensure update server stays enabled
- Package installer: clean up legacy `mokowaasbrand` extension entries and files on install/update
- API endpoint `GET /?mokowaas=extensions` and `GET /api/v1/mokowaas/extensions` — list installed extensions with version, status, and update server info
## [02.20.00] --- 2026-05-28
## [02.20.00] --- 2026-05-28
## [02.19.00] --- 2026-05-28
## [02.18.00] --- 2026-05-28
All notable changes to the MokoWaaS plugin will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Planned
- License/subscription check
- System email template branding (DB approach)
## [02.01.43] - 2026-05-23
### Added
- Site Aliases tab with Joomla subform repeatable-table UI
- Per-alias offline toggle with custom maintenance message (503 response)
- Per-alias robots meta directive (index/noindex/follow/nofollow/none)
- Per-alias backend redirect (admin panel redirects to primary domain)
- 6 MokoWaaS API endpoints: health, install, update, cache, backup, info
- Remote plugin install via `/?mokowaas=install` endpoint
- Remote update trigger via `/?mokowaas=update` endpoint
- Remote cache clear via `/?mokowaas=cache` endpoint (site + admin + opcache)
- Remote Akeeba Backup trigger via `/?mokowaas=backup` endpoint
- Compact site info via `/?mokowaas=info` endpoint
### Changed
- Site aliases moved from comma-separated text field to structured subform
- Each alias now stores domain, offline, offline_message, robots, redirect_backend
- Heartbeat provisioning updated for subform alias format
- Grafana datasource names use domain-only (removed "MokoWaaS - " prefix)
### Fixed
- Heartbeat receiver accepts any 200 status (registered/updated/ok)
- script.php uses heartbeat receiver instead of Grafana API (fixes 403 RBAC)
## [02.01.37] - 2026-05-23
### Added
- Health check endpoint at `/?mokowaas=health` with 16 diagnostic checks (#54)
- Core checks: database latency, filesystem writability/size, cache, extensions
- Backup checks: Akeeba Backup last backup date/status/size, days since, frequency
- Security checks: Admin Tools WAF status, blocked requests 24h/7d
- SSL certificate: expiry date, days left, issuer (degraded <30d, error <7d)
- Scheduled tasks: Joomla task scheduler status, failed tasks 24h
- Error log: PHP error log size, recent errors, last error message
- Database size: total MB, table count, top 5 largest tables
- Content stats: articles, categories, menu items, modules
- User activity: total users, active sessions, failed logins 24h, last login
- Mail system: mailer type, from address, SMTP host, queue count
- SEO health: robots.txt, sitemap, htaccess, SEF status
- Template info: site/admin template names, override count
- Configuration drift: debug mode, error reporting, force SSL, caching
- Human-readable `reason` field explaining degraded/error status
- Site size reporting (images, media, tmp, cache, logs directories)
- Heartbeat provisioning via receiver at bench.mokoconsulting.tech
- Grafana datasource auto-provisioning via YAML (no API token needed)
- ntfy notifications on heartbeat registration (mokowaas-heartbeat topic)
- Grafana dashboard with 9 rows covering all 16 health checks
- Auto-generated health API token (separate from Joomla user tokens)
### Changed
- Health endpoint always enabled — no config toggle needed
- Grafana provisioning uses heartbeat receiver pattern (replaces direct API)
- Removed config fields: enable_health_endpoint, grafana_url, grafana_api_key
- Migrated .gitea/ to .mokogitea/ directory standard
- Updated all references from MokoStandards to moko-platform
- Renamed Gitea references to MokoGitea in docs
### Fixed
- SSL verification disabled for Grafana cURL calls (shared hosting)
- cURL follow redirects enabled
- updates.xml download URL uses correct `development` tag
### Security
- Plugin hidden from plugin list for non-master users
- Plugin settings restricted to master user only
- Self-healing lock (enforceLocked) runs every page load
- Uninstall blocked in preflight
- Health endpoint requires HTTPS + bearer token
- Heartbeat shared secret for receiver authentication
## [02.01.08] - 2026-04-07
### Added
- Template-based language overrides with `{{BRAND_NAME}}`, `{{COMPANY_NAME}}`, `{{SUPPORT_URL}}` placeholders
- Configurable brand name, company name, and support URL via plugin params
- Sentinel-block merge pattern that preserves existing site overrides
- Install respects user-defined overrides (non-overwrite)
- ~50 override keys across admin and frontend
- Powered by links with anchor tag to support URL
- Login support URL enforcement (mokoconsulting.tech/support, /kb, /news)
- Atum template branding via params (logoBrandLarge, logoBrandSmall, loginLogo)
- Shipped media assets: logo.png, favicon.ico, favicon.svg, favicon_256.png
- Favicon injection (SVG + ICO + Apple touch icon)
- Admin color scheme via Atum template style params (hue, link-color, special-color)
- Custom CSS textarea injection
- Master user enforcement (persistent super admin — "Webmaster")
- Emergency access (DB password + file verification two-factor)
- IP whitelist via configuration.php (empty blocks access)
- IP whitelist display in plugin config (shows current IPs + your IP)
- All emergency access attempts logged to Joomla Action Logs
- Email notification on successful emergency login
- Tenant restrictions: Extension Installer, System Info, Global Configuration, Template code editor
- Dynamic admin menu hiding via onPreprocessMenuItems
- Disable install-from-URL for all users
- Force HTTPS redirect (supports reverse proxy)
- Admin session idle timeout (default 60 min, master user exempt)
- Password policy (min length, uppercase, number, special character)
- Upload type and size restrictions (default 100MB)
- Maintenance actions: reset all hits, delete all versions
- Auto-enable plugin on first install
- Action log extension registration in #__action_logs_extensions and #__action_log_config
- Custom AllowedIpsField form field for IP whitelist display
- Joomla 5.x and 6.x compatibility
### Fixed
- Column heading overrides removed (broke module/plugin list views)
- RegularLabs Position column workaround
- Nested `<a>` tags in login support overrides
- Emergency access moved from onUserAuthenticate to onAfterInitialise (Joomla uses isolated auth dispatcher)
- Session created directly for emergency login (bypasses auth dispatcher)
- Auto-complete emergency login after verify file deletion (no re-entering credentials)
### Changed
- Version bumped to 02.01.08 across all files
- Configuration guide fully rewritten with all fieldsets documented
- Testing guide with 17 test suites
- README updated with Usage section, new features, Joomla 5/6 badges
## [01.04.00] - 2026-02-22
### Added
- Complete Joomla 5.x system plugin implementation with modern architecture
- Main plugin class (`src/mokowaas.php`) with event handlers:
- `onAfterInitialise` event hook for framework initialization
- `onAfterRoute` event hook for routing integration
- Plugin manifest (`src/mokowaas.xml`) with Joomla 5.x namespace support
- Namespace: `Moko\Plugin\System\MokoWaaS`
- Configuration parameter for enabling/disabling branding
- Dependency injection service provider (`src/services/provider.php`)
- DI container registration for Joomla 5.x compatibility
- Plugin language files in `src/language/en-GB/`:
- `plg_system_mokowaas.ini` - Plugin UI strings
- `plg_system_mokowaas.sys.ini` - System/installation strings
- Enhanced language overrides (57+ strings):
- Installation sample data branding
- Site name labels
- Admin-specific UI elements
- Version and About sections
- Security `index.html` files throughout directory structure
- Comprehensive README.md with:
- Badges for version, license, Joomla, and PHP compatibility
- Table of contents with 12+ major sections
- Detailed installation instructions (2 methods)
- Technical implementation documentation
- Repository structure overview
- Development and build instructions
### Changed
- Updated all documentation to version 01.04.00
- Enhanced language overrides with more comprehensive coverage
- Improved plugin configuration options
### Fixed
- Typo in language override: "ERROR OCCURED" → "ERROR OCCURRED"
- Repository references updated from placeholders to actual GitHub URLs
### Technical
- Integrates with Joomla's native language override system
- No programmatic string loading (performance optimization)
- Event-driven architecture for minimal overhead
- PSR-4 autoloading through service provider
## [01.03.00] - 2025-12-11
### Changed
- General cleanup and code organization
- Documentation structure improvements
## [01.02.01] - 2025-12-11
### Changed
- Version bump for release alignment
## [01.02.00] - 2025-12-11
### Added
- Documentation directory (`/docs/`) with comprehensive guides:
- Installation guide
- Configuration guide
- Build guide
- Operations guide
- Troubleshooting guide
- Upgrade and versioning guide
- Rollback and recovery guide
- GitHub workflow for automated builds (`.github/workflows/build.yml`)
- Image and favicon replacement feature for complete branding
### Changed
- Improved documentation structure and organization
## [01.01.05] - 2025-12-11
### Changed
- Version bump for release coordination
## [01.01.04] - 2025-12-11
### Fixed
- Plugin manifest corrections and validation fixes
## [01.01.03] - 2025-12-11
### Fixed
- Administrator language file location corrected
- Language override path alignment with Joomla standards
## [01.01.02] - 2025-12-11
### Changed
- Moved plugin code to `/src/` directory for better organization
- Aligned repository structure with release deployment pipeline
- Improved packaging workflow
### Added
- Release deployment pipeline integration
- Automated build and validation scripts
## [1.0.0] - 2025-12-11
### Added
- Initial release of MokoWaaS plugin
- Basic language override system for Joomla rebranding
- Frontend language overrides (en-GB, en-US)
- Administrator language overrides (en-GB, en-US)
- Core branding replacements:
- Footer "Powered by" text
- Control panel welcome messages
- Help and documentation links
- Generic Joomla→MokoWaaS replacements
- Basic plugin structure and manifest
- License (GPL-3.0-or-later)
- Contributing guidelines
- Code of conduct
### Technical Details
- Joomla 5.x compatible
- PHP 8.1+ requirement
- Language override mechanism using Joomla's native system
---
## Version History Summary
| Version | Date | Type | Summary |
|------------|------------|-----------|-------------------------------------------|
| 01.04.00 | 2026-02-22 | Major | Complete plugin implementation & enhanced docs |
| 01.03.00 | 2025-12-11 | Minor | Cleanup and organization |
| 01.02.01 | 2025-12-11 | Patch | Version alignment |
| 01.02.00 | 2025-12-11 | Minor | Documentation and build system |
| 01.01.05 | 2025-12-11 | Patch | Version coordination |
| 01.01.04 | 2025-12-11 | Patch | Manifest fixes |
| 01.01.03 | 2025-12-11 | Patch | Language location fix |
| 01.01.02 | 2025-12-11 | Patch | Repository restructuring |
| 1.0.0 | 2025-12-11 | Major | Initial release |
---
## Upgrade Notes
### Upgrading to 01.04.00
**Breaking Changes:** None
**New Features:**
- Complete Joomla 5.x plugin implementation
- Dependency injection support
- Enhanced language overrides (14+ new strings)
**Installation:**
1. Backup your current installation
2. Download the latest release package
3. Install via Joomla Extension Manager
4. Clear Joomla cache
5. Verify branding appears correctly
### Upgrading to 01.02.00
**New Features:**
- Comprehensive documentation in `/docs/`
- Automated build workflows
**Notes:**
- Review new documentation for operational guidance
- Check GitHub workflows for automated builds
---
## Contributing
Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests.
When adding entries to this changelog:
1. Add new changes under `[Unreleased]` section
2. Use categories: Added, Changed, Deprecated, Removed, Fixed, Security
3. Include clear, concise descriptions
4. Reference issue numbers where applicable
5. Move items from Unreleased to versioned section upon release
## Links
- [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards) - Coding and documentation standards
- [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - Changelog format specification
- [Semantic Versioning](https://semver.org/spec/v2.0.0.html) - Version numbering specification
- [Repository](https://github.com/mokoconsulting-tech/mokowaas) - Project repository
---
**Note:** For detailed technical documentation, see the `/docs/` directory and [README.md](README.md).
+1 -1
View File
@@ -14,7 +14,7 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Documentation
REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.01.08
VERSION: 02.31.00
PATH: ./CODE_OF_CONDUCT.md
BRIEF: Reference + packaging repo for Moko Consulting Developer GPT Other Default
-->
+161 -93
View File
@@ -1,93 +1,161 @@
<!--
Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-LICENSE-IDENTIFIER: GPL-3.0-or-later
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License (./LICENSE.md).
# FILE INFORMATION
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Contributing
REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.01.08
PATH: /CONTRIBUTING.md
BRIEF: Contribution guidelines for the MokoWaaS plugin
-->
# Contributing to MokoWaaS (VERSION: 02.01.08)
## Overview
Contributions to the MokoWaaS plugin follow standardized development, governance, and quality control expectations defined by Moko Consulting. This document outlines contribution requirements, acceptable change types, branch management, testing expectations, and release readiness standards.
## 1. Contribution Workflow
All contributions must follow the established workflow:
1. Fork the repository or create a feature branch (if internal).
2. Ensure your environment matches the supported Joomla and PHP versions.
3. Implement changes following coding, documentation, and metadata standards.
4. Validate plugin functionality locally.
5. Submit a Pull Request (PR) for review.
## 2. Branching Model
- `main`: Production stable branch.
- `develop`: Aggregates work for the next minor release.
- `feature/*`: New enhancements or changes.
- `bugfix/*`: Hotfixes and corrections.
Internal teams must coordinate with governance before creating major feature branches.
## 3. Coding and Documentation Standards
All code must:
- Follow [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards) coding standards
- Include the unified SPDX license header
- Include a FILE INFORMATION metadata block
- Avoid deprecated Joomla APIs
- Preserve load order compatibility with other system plugins
Documentation must:
- Include metadata
- Maintain revision history
- Use consistent formatting as defined by Moko documentation standards
## 4. Testing Requirements
Before submitting a PR, contributors must verify:
- Plugin installs successfully in Joomla 5.x
- No load errors appear in logs
- Branding replacements appear as expected
- Terminology strings are correct
- No regressions in administrator UI
Automated testing coverage will expand as part of future roadmap enhancements.
## 5. Pull Request Requirements
A PR must include:
- Description of change
- Screenshots for UI related updates
- Version updates when appropriate
- Notes for documentation changes
- Reference to related issues or tasks
PRs lacking required information may be flagged or delayed.
## 6. Release Versioning
Changes must follow semantic versioning:
- MAJOR: Structural branding or architectural changes
- MINOR: Feature updates or terminology expansion
- PATCH: Bug fixes or language corrections
Version updates must be reflected in:
- Manifest files
- PHP headers
- Documentation metadata
## 7. Code Review Standards
Reviewers validate:
- Code quality and clarity
- Compliance with [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards) coding standards
- Impact to templates and WaaS branding rules
- Backwards compatibility expectations
## Revision History
| Date | Author | Description |
| ------ | -------- | ----------- |
| 2025-12-11 | Jonathan Miller (@jmiller-moko) | Initial creation of contribution guidelines |
# Contributing to Moko Consulting Projects
Thank you for your interest in contributing. All Moko Consulting repositories follow this universal workflow and version policy.
## Branching Workflow
```
feature/* ──PR──> dev ──draft PR──> (renamed to rc) ──merge──> main
```
### Step by step
1. **Create a feature branch** from `dev`:
```bash
git checkout dev && git pull
git checkout -b feature/my-change
```
2. **Work and commit** on your feature branch. Push to origin.
3. **Open a PR**: `feature/my-change` → `dev`. After review and checks, merge it.
4. **When ready for release**, open a **draft PR**: `dev` → `main`.
- This automatically renames the source branch to `rc` (release candidate)
- An RC pre-release is built and uploaded
5. **Alpha and beta branches** are created by manually renaming the branch before the RC stage:
- Rename `dev` to `alpha` for early testing → alpha pre-release is built
- Rename `alpha` to `beta` for feature-complete testing → beta pre-release is built
- When the draft PR is created, the branch is renamed to `rc`
6. **Once PR checks pass** on the `rc` branch, mark the PR as ready and merge to `main`.
7. **Merging to main** triggers the stable release pipeline:
- Minor version bump (e.g., `02.09.xx` → `02.10.00`)
- Stability suffix stripped (clean version)
- Gitea release created with ZIP/tar.gz packages
- `updates.xml` updated (Joomla extensions)
- `dev` branch recreated from `main`
### Branch summary
| Branch | Purpose | Created by |
|--------|---------|-----------|
| `feature/*` | New features and fixes | Developer |
| `dev` | Integration branch | Auto-recreated after release |
| `alpha` | Alpha pre-release testing | Manual rename from `dev` |
| `beta` | Beta pre-release testing | Manual rename from `alpha` |
| `rc` | Release candidate | Auto-renamed on draft PR to main |
| `main` | Stable releases | Protected, merge only |
| `version/XX.YY.ZZ` | Archived release snapshots | Auto-created by CI |
### Protected branches
| Branch | Direct push | Merge via |
|--------|------------|-----------|
| `main` | Blocked (CI bot whitelisted) | PR merge only |
| `dev` | Blocked (CI bot whitelisted) | PR merge from feature/* |
| `rc` | Blocked (CI bot whitelisted) | Auto-created on draft PR |
| `alpha` | Blocked (CI bot whitelisted) | Manual rename |
| `beta` | Blocked (CI bot whitelisted) | Manual rename |
| `feature/*` | Open | N/A (source branch) |
## Version Policy
### Format
All versions use `XX.YY.ZZ` — three two-digit segments, zero-padded:
- **XX** — Major version (breaking changes)
- **YY** — Minor version (new features, bumped on release to main)
- **ZZ** — Patch version (auto-incremented on every push to dev/feature branches)
Rollover: patch `99` → `00` increments minor; minor `99` → `00` increments major.
### Stability suffixes
Each branch appends a suffix to indicate stability:
| Branch | Suffix | Example |
|--------|--------|---------|
| `main` | (none) | `02.09.00` |
| `dev` | `-dev` | `02.09.01-dev` |
| `feature/*` | `-dev` | `02.09.01-dev` |
| `alpha` | `-alpha` | `02.09.01-alpha` |
| `beta` | `-beta` | `02.09.01-beta` |
| `rc` | `-rc` | `02.09.01-rc` |
### Auto version bump
On every push to `dev`, `feature/*`, or `patch/*`:
1. Patch version incremented
2. Stability suffix `-dev` applied
3. All version-bearing files updated (manifests, CHANGELOG, PHP headers, etc.)
4. Commit created with `[skip ci]` to avoid loops
### Release version flow
Version bumps happen at specific release events:
| Event | Bump | Example |
|-------|------|---------|
| Feature merged to dev | Patch bump after dev release | `02.09.01-dev` → release → `02.09.02-dev` |
| Dev promoted to RC | Minor bump | `02.09.02-dev` → `02.10.00-rc` |
| RC merged to main | Minor bump | `02.10.00-rc` → `02.11.00` (stable) |
| Dev recreated from main | Patch bump | `02.11.00` → `02.11.01-dev` |
### Release stream copies
When a higher-stability release is published, copies are created for all lesser streams with the same base version:
- **RC `02.10.00-rc`** also creates: `02.10.00-dev`, `02.10.00-alpha`, `02.10.00-beta`
- **Stable `02.11.00`** also creates: `02.11.00-dev`, `02.11.00-alpha`, `02.11.00-beta`, `02.11.00-rc`
This ensures Joomla sites on ANY stability channel see the update (Joomla only shows versions higher than what's installed).
### Version files
The version tools update all files containing version stamps:
- `.mokogitea/manifest.xml` (canonical source)
- Joomla XML manifests (`<version>` tag)
- `README.md`, `CHANGELOG.md` (`VERSION:` pattern)
- `package.json`, `pyproject.toml`
- Any text file with a `VERSION: XX.YY.ZZ` label
Files synced from other repos (with a `# REPO:` header) are not touched.
## Code Standards
- **PHP**: PSR-12, tabs for indentation
- **Copyright**: all files must include the Moko Consulting copyright header
- **License**: SPDX identifier `GPL-3.0-or-later` (or as specified per repo)
- **Attribution**: use `Authored-by: Moko Consulting` in commits, not individual names
## Commit Messages
Use conventional commit format:
```
type(scope): short description
Optional body with context.
Authored-by: Moko Consulting
```
Types: `feat`, `fix`, `chore`, `docs`, `style`, `refactor`, `test`, `ci`
Special flags in commit messages:
- `[skip ci]` — skip all CI workflows
- `[skip bump]` — skip auto version bump only
## Reporting Issues
Use the repository's issue tracker with the appropriate template.
---
*Moko Consulting <hello@mokoconsulting.tech>*
+1 -1
View File
@@ -19,7 +19,7 @@
DEFGROUP: mokoconsulting-tech.MokoWaaSBrand
INGROUP: MokoStandards.Governance
REPO: https://github.com/mokoconsulting-tech/MokoWaaSBrand
VERSION: 02.01.08
VERSION: 02.31.00
PATH: /GOVERNANCE.md
BRIEF: Project governance rules, roles, and decision process for MokoWaaSBrand
-->
+1 -1
View File
@@ -15,7 +15,7 @@
INGROUP: MokoWaaS.Documentation
REPO: https://github.com/mokoconsulting-tech/mokowaas
PATH: ./LICENSE.md
VERSION: 02.01.08
VERSION: 02.31.00
BRIEF: Project license (GPL-3.0-or-later)
-->
GNU GENERAL PUBLIC LICENSE
+26 -320
View File
@@ -5,356 +5,62 @@
SPDX-LICENSE-IDENTIFIER: GPL-3.0-or-later
This program is free software; you can redistribute it and modify it under the terms of the GNU General Public License version 3 or later.
This program is distributed in the hope that it will be useful but without warranty.
You should have received a copy of the GNU General Public License in LICENSE.md.
# FILE INFORMATION
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS
REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.01.43
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
VERSION: 02.31.00
PATH: /README.md
BRIEF: Rebranding plugin for MokoWaaS platform
NOTE: Internal WaaS identity abstraction layer
BRIEF: MokoWaaS platform plugin for Joomla
-->
# MokoWaaS Plugin
# MokoWaaS
[![Version](https://img.shields.io/badge/version-02.01.43-blue.svg?logo=v&logoColor=white)](https://github.com/mokoconsulting-tech/MokoWaaS/releases/tag/v02)
[![Version](https://img.shields.io/badge/version-02.03.11-blue.svg?logo=v&logoColor=white)](https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases)
[![License](https://img.shields.io/badge/license-GPL--3.0--or--later-green.svg?logo=gnu&logoColor=white)](LICENSE)
[![Joomla](https://img.shields.io/badge/Joomla-5.x%20%7C%206.x-red.svg?logo=joomla&logoColor=white)](https://www.joomla.org)
[![PHP](https://img.shields.io/badge/PHP-8.1%2B-777BB4.svg?logo=php&logoColor=white)](https://www.php.net)
MokoWaaS is a Joomla 5.x / 6.x system plugin that provides a configurable white-label identity layer for the MokoWaaS platform. It replaces all visible Joomla branding with your own brand name, company name, and support URLs — configurable from the plugin admin without code changes.
## Table of Contents
- [Overview](#overview)
- [Features](#features)
- [System Requirements](#system-requirements)
- [Installation](#installation)
- [Usage](#usage)
- [Configuration](#configuration)
- [Technical Implementation](#technical-implementation)
- [Repository Structure](#repository-structure)
- [Development](#development)
- [Documentation](#documentation)
- [Support](#support)
- [License](#license)
- [Changelog](#changelog)
## Overview
The MokoWaaS plugin operationalizes a unified naming convention, brand-controlled visuals, and enforced terminology across all tenant sites. This ensures consistent service delivery within the WaaS (Website as a Service) framework by abstracting all upstream Joomla identifiers behind MokoWaaS-compliant terminology.
MokoWaaS is a Joomla 5.x / 6.x system plugin package that provides white-label branding, security hardening, tenant restrictions, health monitoring, and multi-domain management for the MokoWaaS platform.
## Features
- **Template-Based Overrides**: 50+ language keys with `{{BRAND_NAME}}`, `{{COMPANY_NAME}}`, `{{SUPPORT_URL}}` placeholders
- **Configurable Brand**: Change brand name, company, and support URL from plugin config — takes effect immediately
- **Safe Override Merging**: Sentinel-block pattern preserves existing site overrides during install/update
- **Clean Uninstall**: Only MokoWaaS keys are removed; all other overrides are preserved
- **Joomla 5.x / 6.x Compatible**: Built using modern Joomla plugin architecture with dependency injection
- **Multi-Language Support**: en-GB and en-US locales
- **Admin & Frontend Coverage**: Dashboard, footer, login, installer, system info, update component, error pages, and more
- **Health Monitoring**: 16 diagnostic checks via `/?mokowaas=health` — database, filesystem, cache, extensions, Akeeba Backup, Admin Tools, SSL, cron, errors, DB size, content, users, mail, SEO, templates, config
- **Grafana Integration**: Auto-provisions Infinity datasource via heartbeat receiver — 9-row dashboard with all health metrics
- **ntfy Notifications**: Heartbeat events pushed to `mokowaas-heartbeat` topic
- **Plugin Protection**: Hidden from non-super-admins, self-healing lock, uninstall blocked
- **Governance Compliant**: Aligned with [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform)
- **White-Label Branding** — configurable brand name, company, support URL, colors, favicon, custom CSS
- **Tenant Restrictions** — master user enforcement, installer/sysinfo/config/template access control
- **Health Monitoring** — 16 diagnostic checks via `/?mokowaas=health` with Grafana auto-provisioning
- **Site Aliases** — per-alias offline mode, robots directives, backend redirect, canonical URLs
- **Remote API** — 6 endpoints (health, install, update, cache, backup, info)
- **Security Hardening** — HTTPS enforcement, session timeouts, password policy, upload restrictions
- **Plugin Protection** — protected status, hidden from non-master users, disable/uninstall blocked
## System Requirements
## Requirements
- **Joomla**: 5.0+ or 6.x
- **PHP**: 8.1 or higher (8.3+ for Joomla 6)
- **Extensions**: Standard Joomla PHP extensions
- **Permissions**: Write access to language override directories
- Joomla 5.0+ or 6.x
- PHP 8.1+ (8.3+ for Joomla 6)
## Installation
### Method 1: Via Joomla Extension Manager (Recommended)
Download the latest `pkg_mokowaas-*.zip` from [Releases](https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases) and install via **System → Install → Upload Package File**.
1. Download the latest release package from the releases page
2. Log into your Joomla Administrator panel
3. Navigate to **System → Extensions → Install**
4. Click **Upload Package File**
5. Select the downloaded `.zip` file
6. Click **Upload & Install**
7. Navigate to **System → Plugins**
8. Search for "MokoWaaS Brand"
9. Enable the plugin
10. Clear Joomla cache
### Method 2: Manual Installation
1. Extract the plugin package
2. Upload contents to your Joomla installation's `/tmp` directory
3. Install via Joomla Extension Manager → Install from Folder
4. Enable the plugin as described above
### Post-Installation
After installation, verify the branding is active:
- Check the administrator footer for "Powered by MokoWaaS"
- Verify the control panel shows "Welcome to MokoWaaS!"
- Clear browser cache if branding doesn't appear immediately
### Automatic Updates
This plugin supports Joomla's automatic update system. Once installed:
1. Navigate to **System → Update → Extensions**
2. The plugin will automatically check for updates from the MokoWaaS update server
3. When a new version is available, it will appear in the update list
4. Click **Update** to install the latest version
The update server URL is configured in the plugin manifest and points to:
```
https://raw.githubusercontent.com/mokoconsulting-tech/MokoWaaS/main/updates.xml
```
Updates are published automatically when new releases are created through the GitHub release workflow.
## Usage
Once installed and enabled, the plugin automatically replaces Joomla branding with your configured values. No code changes needed.
### Changing the Brand Name
1. Navigate to **System → Plugins → System - MokoWaaS**
2. Set **Brand Name** to your desired name (e.g., "MyPlatform")
3. Set **Company Name** to your company (e.g., "My Company Inc.")
4. Set **Support URL** to your support site (e.g., "https://support.mycompany.com")
5. Click **Save & Close**
6. The new branding appears immediately across admin and frontend
### What Gets Rebranded
| Area | Example |
| ---- | ------- |
| Admin footer | "Powered by [YourBrand](https://your-url)" |
| Dashboard | "Welcome to YourBrand!" |
| Quick Icons | "YourBrand is up to date." |
| System Info | "YourBrand Version" |
| Login page | "YourBrand Administrator Login" |
| Update component | "YourBrand Update" |
| Frontend footer | "Powered by [YourBrand](https://your-url)" |
| Error pages | No Joomla references |
## Configuration
The plugin provides the following configuration options accessible through **System → Plugins → System - MokoWaaS**:
### Parameters
| Parameter | Type | Default | Description |
| --------- | ---- | ------- | ----------- |
| Enable Branding | Yes/No | Yes | Master toggle for all branding overrides |
| Brand Name | Text | MokoWaaS | Replaces "Joomla" throughout the interface |
| Company Name | Text | Moko Consulting | Used in support/attribution links |
| Support URL | URL | https://mokoconsulting.tech | Destination for help and documentation links |
See the [Configuration Guide](docs/guides/configuration-guide.md) for detailed documentation on how overrides work.
## Technical Implementation
### Architecture
The plugin follows Joomla 5.x system plugin architecture:
```
PlgSystemMokoWaaS
├── Event Handlers
│ ├── onAfterInitialise - Framework initialization hook
│ └── onAfterRoute - Route determination hook
├── Dependency Injection
│ └── ServiceProvider - DI container registration
└── Language Integration
└── Native Override System - Joomla's built-in override mechanism
```
### Core Components
1. **mokowaas.php**
- Main plugin class extending `CMSPlugin`
- Implements system event handlers
- Namespace: `Moko\Plugin\System\MokoWaaS`
2. **mokowaas.xml**
- Plugin manifest defining metadata and structure
- Joomla 5.x namespace configuration
- File and folder definitions
3. **services/provider.php**
- Dependency injection service provider
- Registers plugin with Joomla's DI container
- Joomla 5.x compatibility layer
4. **language/en-GB/**
- Plugin-specific language strings
- Installation and configuration UI text
5. **language/overrides/**
- Frontend language override files
- Replaces Joomla terminology with MokoWaaS branding
6. **administrator/language/overrides/**
- Administrator language override files
- Backend-specific branding replacements
### Language Override Integration
The plugin leverages Joomla's native language override system rather than programmatically loading strings. Language override files are placed in standard Joomla locations:
- Frontend: `language/overrides/{locale}.override.ini`
- Administrator: `administrator/language/overrides/{locale}.override.ini`
Joomla automatically loads these overrides during initialization, ensuring optimal performance and compatibility.
## Repository Structure
```
mokowaas/
├── src/ # Plugin source files
│ ├── mokowaas.php # Main plugin class
│ ├── mokowaas.xml # Plugin manifest
│ ├── services/
│ │ └── provider.php # DI service provider
│ ├── language/
│ │ ├── en-GB/ # Plugin language files
│ │ └── overrides/ # Frontend language overrides
│ └── administrator/
│ └── language/
│ └── overrides/ # Admin language overrides
├── docs/ # Documentation
│ ├── index.md # Documentation index
│ ├── plugin-basic.md # Plugin overview
│ ├── guides/ # Operational guides
│ └── reference/ # Reference materials
├── scripts/ # Build and validation scripts
│ ├── validate_manifest.sh
│ ├── verify_changelog.sh
│ └── update_changelog.sh
├── .github/ # GitHub workflows
│ └── workflows/
│ ├── build.yml
│ ├── ci.yml
│ └── release_from_version.yml
├── CHANGELOG.md # Version history
├── README.md # This file
├── LICENSE.md # GPL-3.0-or-later license
├── CONTRIBUTING.md # Contribution guidelines
└── CODE_OF_CONDUCT.md # Community guidelines
```
## Development
### Building the Plugin
Build the installable plugin package from source:
```bash
cd src
zip -r ../mokowaas_v01.04.00.zip . -x "*.git*"
```
### Running Validation Scripts
```bash
# Validate plugin manifest
./scripts/validate_manifest.sh
# Verify changelog format
./scripts/verify_changelog.sh
```
### PHP Syntax Validation
```bash
cd src
find . -name "*.php" -exec php -l {} \;
```
### Automated Build via GitHub Actions
The repository includes automated workflows:
- **build.yml**: Creates ZIP package on release
- **ci.yml**: Runs validation checks on pull requests
- **release_from_version.yml**: Automates release process
After installation, the package auto-enables and sets protected status.
## Documentation
Comprehensive documentation is available in the `/docs` directory:
Full documentation is available on the [MokoWaaS Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/wiki):
- **[Plugin Overview](docs/plugin-basic.md)**: Detailed plugin documentation
- **[Installation Guide](docs/guides/installation-guide.md)**: Step-by-step installation
- **[Build Guide](docs/guides/build-guide.md)**: Building and packaging
- **[Configuration Guide](docs/guides/configuration-guide.md)**: Configuration options
- **[Operations Guide](docs/guides/operations-guide.md)**: Operational procedures
- **[Troubleshooting Guide](docs/guides/troubleshooting-guide.md)**: Common issues
All documentation follows the [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards) documentation framework.
## Support
### Getting Help
- **Documentation**: Check the `/docs` directory for detailed guides
- **Issues**: Submit issues through the GitHub issue tracker
- **Service Support**: For operational issues, submit a ticket through the Moko Consulting service channel
### Reporting Issues
When reporting issues, include:
- Joomla version
- PHP version
- Plugin version
- Steps to reproduce
- Expected vs actual behavior
- Relevant error messages or logs
- [Configuration Guide](https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/wiki/Configuration)
- [Health Monitoring](https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/wiki/Health-Monitoring)
- [Site Aliases](https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/wiki/Site-Aliases)
- [API Endpoints](https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/wiki/API-Endpoints)
- [Grafana Integration](https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/wiki/Grafana-Integration)
## License
This project is licensed under the GNU General Public License version 3 or later (GPL-3.0-or-later).
See [LICENSE.md](LICENSE.md) for the full license text.
## Versioning
This extension follows the [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards) version governance model using semantic versioning: `MAJOR.MINOR.PATCH`
Current version: **02.01.18**
GPL-3.0-or-later — see [LICENSE.md](LICENSE.md)
## Changelog
See [CHANGELOG.md](CHANGELOG.md) for a complete version history.
### Recent Changes (v02.01.18 - 2026-04-23)
- Always install and lock MokoOnyx template on install/update
- Always unlock MokoCassiopeia on install/update (allow uninstall)
- Bundle MokoOnyx payload (replaces MokoCassiopeia payload)
- Update payload workflow to fetch MokoOnyx from Gitea releases
## Contributing
We welcome contributions! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on:
- Code of conduct
- Development workflow
- Coding standards
- Pull request process
- Documentation requirements
## Acknowledgments
- Built for the MokoWaaS platform
- Follows [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards)
- Designed for Joomla 5.x architecture
- Maintained by Moko Consulting
See [CHANGELOG.md](CHANGELOG.md)
---
+1 -1
View File
@@ -23,7 +23,7 @@ DEFGROUP: [PROJECT_NAME]
INGROUP: [PROJECT_NAME].Documentation
REPO: [REPOSITORY_URL]
PATH: /SECURITY.md
VERSION: 02.01.08
VERSION: 02.31.00
BRIEF: Security vulnerability reporting and handling policy
-->
-12
View File
@@ -1,12 +0,0 @@
# TODO
> **Note:** This file is not tracked in version control (.gitignore). It is for local task tracking only.
## Critical
-
## Normal
-
## Low
-
+2 -2
View File
@@ -11,13 +11,13 @@
INGROUP: MokoWaaS.Build
REPO: https://github.com/mokoconsulting-tech/mokowaas
FILE: build-guide.md
VERSION: 02.01.08
VERSION: 02.31.00
PATH: /docs/guides/
BRIEF: Build and packaging guide for the MokoWaaS system plugin
NOTE: Defines environment setup, repository layout, packaging rules, and release preparation
-->
# MokoWaaS Build Guide (VERSION: 02.01.08)
# MokoWaaS Build Guide (VERSION: 02.31.00)
## 1. Purpose
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.01.08
VERSION: 02.31.00
PATH: /docs/guides/configuration-guide.md
BRIEF: Configuration guide for the MokoWaaS system plugin
NOTE: Defines plugin parameters, expected behaviors, and recommended defaults
-->
# MokoWaaS Configuration Guide (VERSION: 02.01.08)
# MokoWaaS Configuration Guide (VERSION: 02.31.00)
## 1. Objective
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.01.08
VERSION: 02.31.00
PATH: /docs/guides/installation-guide.md
BRIEF: Installation guide for the MokoWaaS system plugin
NOTE: First document in the guide set
-->
# MokoWaaS Installation Guide (VERSION: 02.01.08)
# MokoWaaS Installation Guide (VERSION: 02.31.00)
## Introduction
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.01.08
VERSION: 02.31.00
PATH: /docs/guides/operations-guide.md
BRIEF: Operational guide for administering and managing the MokoWaaS system plugin
NOTE: Defines lifecycle, responsibilities, and operational behaviors
-->
# MokoWaaS Operations Guide (VERSION: 02.01.08)
# MokoWaaS Operations Guide (VERSION: 02.31.00)
## Introduction
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.01.08
VERSION: 02.31.00
PATH: /docs/guides/rollback-and-recovery-guide.md
BRIEF: Rollback and recovery guide for restoring stable operation after plugin related incidents
NOTE: Completes the core guide set for WaaS plugin governance
-->
# MokoWaaS Rollback and Recovery Guide (VERSION: 02.01.08)
# MokoWaaS Rollback and Recovery Guide (VERSION: 02.31.00)
## Introduction
+2 -2
View File
@@ -7,13 +7,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.01.08
VERSION: 02.31.00
PATH: /docs/guides/testing-guide.md
BRIEF: Testing guide for MokoWaaS v02.01.08
NOTE: Covers manual test procedures for language overrides, install/uninstall, and configuration
-->
# MokoWaaS Testing Guide (VERSION: 02.01.08)
# MokoWaaS Testing Guide (VERSION: 02.31.00)
## 1. Prerequisites
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.01.08
VERSION: 02.31.00
PATH: /docs/guides/troubleshooting-guide.md
BRIEF: Troubleshooting guide for diagnosing and resolving issues related to the MokoWaaS plugin
NOTE: Designed for administrators and WaaS operations teams
-->
# MokoWaaS Troubleshooting Guide (VERSION: 02.01.08)
# MokoWaaS Troubleshooting Guide (VERSION: 02.31.00)
## Introduction
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.01.08
VERSION: 02.31.00
PATH: /docs/guides/upgrade-and-versioning-guide.md
BRIEF: Guide for updating, versioning, and maintaining the MokoWaaS plugin
NOTE: Defines release flow, version rules, and upgrade validation
-->
# MokoWaaS Upgrade and Versioning Guide (VERSION: 02.01.08)
# MokoWaaS Upgrade and Versioning Guide (VERSION: 02.31.00)
## Introduction
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Documentation
REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.01.08
VERSION: 02.31.00
PATH: /docs/index.md
BRIEF: Master index of all documentation for the MokoWaaS plugin
NOTE: Automatically maintained index for all guide canvases
-->
# MokoWaaS Documentation Index (VERSION: 02.01.08)
# MokoWaaS Documentation Index (VERSION: 02.31.00)
## Introduction
+2 -2
View File
@@ -11,12 +11,12 @@
INGROUP: MokoWaaS
REPO: https://github.com/mokoconsulting-tech/mokowaas
PATH: /docs/plugin-basic.md
VERSION: 02.01.08
VERSION: 02.31.00
BRIEF: Baseline documentation for the MokoWaaS system plugin
NOTE: Foundational reference for internal and external stakeholders
-->
# MokoWaaS Plugin Overview (VERSION: 02.01.08)
# MokoWaaS Plugin Overview (VERSION: 02.31.00)
## Introduction
+1 -1
View File
@@ -10,7 +10,7 @@ DEFGROUP: MokoWaaS.Documentation
INGROUP: MokoStandards.Templates
REPO: https://github.com/mokoconsulting-tech/MokoWaaS
PATH: /docs/update-server.md
VERSION: 02.01.08
VERSION: 02.31.00
BRIEF: How this extension's Joomla update server file (update.xml) is managed
-->
@@ -0,0 +1,38 @@
<?php
/**
* @package MokoWaaS
* @subpackage com_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
defined('_JEXEC') or die;
use Joomla\CMS\Dispatcher\ComponentDispatcherFactoryInterface;
use Joomla\CMS\Extension\ComponentInterface;
use Joomla\CMS\Extension\Service\Provider\ComponentDispatcherFactory;
use Joomla\CMS\Extension\Service\Provider\MVCFactory;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
return new class implements ServiceProviderInterface
{
public function register(Container $container): void
{
$container->registerServiceProvider(new MVCFactory('\\Moko\\Component\\MokoWaaS'));
$container->registerServiceProvider(new ComponentDispatcherFactory('\\Moko\\Component\\MokoWaaS'));
$container->set(
ComponentInterface::class,
function (Container $container) {
$component = new \Joomla\CMS\Extension\MVCComponent(
$container->get(ComponentDispatcherFactoryInterface::class)
);
$component->setMVCFactory($container->get(MVCFactoryInterface::class));
return $component;
}
);
}
};
@@ -0,0 +1,89 @@
<?php
/**
* @package MokoWaaS
* @subpackage com_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoWaaS\Api\Controller;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Controller\BaseController;
/**
* Cache management API controller.
*
* POST /api/index.php/v1/mokowaas/cache
*
* @since 1.0.0
*/
class CacheController extends BaseController
{
/**
* Clear all Joomla caches.
*
* @return void
*
* @since 1.0.0
*/
public function execute(): void
{
$app = Factory::getApplication();
if ($app->input->getMethod() !== 'POST')
{
$this->sendJson(405, ['error' => 'POST required']);
return;
}
$user = $app->getIdentity();
if (!$user->authorise('core.manage', 'com_plugins'))
{
$this->sendJson(403, ['error' => 'Not authorized']);
return;
}
try
{
$cache = Factory::getCache('');
$cache->clean('');
$adminCache = Factory::getCache('', 'callback', 'administrator');
$adminCache->clean('');
if (function_exists('opcache_reset'))
{
opcache_reset();
}
$this->sendJson(200, [
'status' => 'ok',
'message' => 'Cache cleared',
]);
}
catch (\Throwable $e)
{
$this->sendJson(500, [
'error' => 'Cache clear failed',
'message' => $e->getMessage(),
]);
}
}
/**
* @param int $code HTTP status code
* @param array $payload Response data
* @return void
*/
private function sendJson(int $code, array $payload): void
{
$app = Factory::getApplication();
$app->setHeader('Content-Type', 'application/json', true);
$app->setHeader('Status', (string) $code, true);
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
$app->close();
}
}
@@ -0,0 +1,187 @@
<?php
/**
* @package MokoWaaS
* @subpackage com_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoWaaS\Api\Controller;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Controller\BaseController;
/**
* Extensions list API controller.
*
* GET /api/index.php/v1/mokowaas/extensions
*
* Returns all installed extensions with type, element, folder, version,
* enabled/protected/locked status, and update server info.
*
* Optional filters via query params:
* ?type=plugin — filter by extension type
* ?search=moko — search name or element
* ?enabled=1 — only enabled/disabled
*
* @since 02.21.00
*/
class ExtensionsController extends BaseController
{
/**
* List installed extensions.
*
* @return void
*
* @since 02.21.00
*/
public function displayList(): void
{
$app = Factory::getApplication();
$user = $app->getIdentity();
if (!$user->authorise('core.manage', 'com_installer'))
{
$this->sendJson(403, ['error' => 'Not authorized — requires core.manage on com_installer']);
return;
}
try
{
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select([
$db->quoteName('e.extension_id'),
$db->quoteName('e.name'),
$db->quoteName('e.type'),
$db->quoteName('e.element'),
$db->quoteName('e.folder'),
$db->quoteName('e.client_id'),
$db->quoteName('e.enabled'),
$db->quoteName('e.protected'),
$db->quoteName('e.locked'),
$db->quoteName('e.manifest_cache'),
])
->from($db->quoteName('#__extensions', 'e'))
->order($db->quoteName('e.type') . ' ASC, ' . $db->quoteName('e.name') . ' ASC');
// Filter by type
$typeFilter = $app->input->get('type', '', 'CMD');
if ($typeFilter !== '')
{
$query->where($db->quoteName('e.type') . ' = ' . $db->quote($typeFilter));
}
// Filter by enabled
$enabledFilter = $app->input->get('enabled', '', 'CMD');
if ($enabledFilter !== '')
{
$query->where($db->quoteName('e.enabled') . ' = ' . (int) $enabledFilter);
}
// Search name or element
$search = $app->input->get('search', '', 'STRING');
if ($search !== '')
{
$searchQuoted = $db->quote('%' . $db->escape($search, true) . '%');
$query->where(
'(' . $db->quoteName('e.name') . ' LIKE ' . $searchQuoted
. ' OR ' . $db->quoteName('e.element') . ' LIKE ' . $searchQuoted . ')'
);
}
$db->setQuery($query);
$rows = $db->loadAssocList();
// Get update sites for cross-reference
$usQuery = $db->getQuery(true)
->select([
$db->quoteName('us.update_site_id'),
$db->quoteName('us.name', 'site_name'),
$db->quoteName('us.location'),
$db->quoteName('us.enabled', 'site_enabled'),
$db->quoteName('usm.extension_id'),
])
->from($db->quoteName('#__update_sites', 'us'))
->innerJoin(
$db->quoteName('#__update_sites_extensions', 'usm')
. ' ON ' . $db->quoteName('us.update_site_id')
. ' = ' . $db->quoteName('usm.update_site_id')
);
$db->setQuery($usQuery);
$updateSites = [];
foreach ($db->loadAssocList() ?: [] as $us)
{
$updateSites[(int) $us['extension_id']] = [
'name' => $us['site_name'],
'location' => $us['location'],
'enabled' => (bool) $us['site_enabled'],
];
}
// Build response
$extensions = [];
foreach ($rows as $row)
{
$manifest = json_decode($row['manifest_cache'] ?: '{}', true);
$extId = (int) $row['extension_id'];
$ext = [
'extension_id' => $extId,
'name' => $row['name'],
'type' => $row['type'],
'element' => $row['element'],
'folder' => $row['folder'] ?: null,
'client_id' => (int) $row['client_id'],
'enabled' => (bool) $row['enabled'],
'protected' => (bool) $row['protected'],
'locked' => (bool) $row['locked'],
'version' => $manifest['version'] ?? null,
'author' => $manifest['author'] ?? null,
'description' => $manifest['description'] ?? null,
];
if (isset($updateSites[$extId]))
{
$ext['update_server'] = $updateSites[$extId];
}
$extensions[] = $ext;
}
$this->sendJson(200, [
'status' => 'ok',
'count' => count($extensions),
'extensions' => $extensions,
]);
}
catch (\Throwable $e)
{
$this->sendJson(500, [
'error' => 'Failed to list extensions',
'message' => $e->getMessage(),
]);
}
}
/**
* @param int $code HTTP status code
* @param array $payload Response data
* @return void
*/
private function sendJson(int $code, array $payload): void
{
$app = Factory::getApplication();
$app->setHeader('Content-Type', 'application/json', true);
$app->setHeader('Status', (string) $code, true);
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
$app->close();
}
}
@@ -0,0 +1,139 @@
<?php
/**
* @package MokoWaaS
* @subpackage com_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoWaaS\Api\Controller;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Controller\BaseController;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\CMS\Uri\Uri;
use Joomla\Registry\Registry;
/**
* Health check API controller.
*
* GET /api/index.php/v1/mokowaas/health
*
* Returns full health diagnostics from the MokoWaaS system plugin.
* Requires a Joomla API token with core.manage permissions.
*
* @since 1.0.0
*/
class HealthController extends BaseController
{
/**
* Return full health check data.
*
* @return void
*
* @since 1.0.0
*/
public function displayList(): void
{
$app = Factory::getApplication();
$user = $app->getIdentity();
if (!$user->authorise('core.manage', 'com_plugins'))
{
$this->sendJson(403, ['error' => 'Not authorized']);
return;
}
$plugin = PluginHelper::getPlugin('system', 'mokowaas');
if (!$plugin)
{
$this->sendJson(503, ['error' => 'MokoWaaS system plugin not enabled']);
return;
}
$params = new Registry($plugin->params);
$config = Factory::getConfig();
$db = Factory::getDbo();
// Collect basic health data
$payload = [
'status' => 'ok',
'timestamp' => gmdate('Y-m-d\TH:i:s\Z'),
'site' => [
'name' => $config->get('sitename', ''),
'url' => rtrim(Uri::root(), '/'),
'joomla_version' => JVERSION,
'php_version' => PHP_VERSION,
'db_type' => $db->getName(),
'offline' => (bool) $config->get('offline', 0),
'debug' => (bool) $config->get('debug', 0),
'sef' => (bool) $config->get('sef', 0),
'caching' => (bool) $config->get('caching', 0),
],
'plugin' => [
'brand' => $params->get('brand_name', 'MokoWaaS'),
'company' => $params->get('company_name', 'Moko Consulting'),
],
];
// Database check
try
{
$db->setQuery('SELECT 1');
$db->loadResult();
$payload['checks']['database'] = ['status' => 'ok'];
}
catch (\Throwable $e)
{
$payload['status'] = 'error';
$payload['checks']['database'] = ['status' => 'error', 'message' => $e->getMessage()];
}
// Disk space
$free = @disk_free_space(JPATH_ROOT);
$total = @disk_total_space(JPATH_ROOT);
if ($free !== false && $total !== false)
{
$freeMb = round($free / 1048576);
$payload['checks']['filesystem'] = [
'status' => $freeMb < 100 ? 'degraded' : 'ok',
'free_disk_mb' => $freeMb,
'total_disk_mb' => round($total / 1048576),
];
}
// Content counts
$db->setQuery($db->getQuery(true)->select('COUNT(*)')->from($db->quoteName('#__content')));
$payload['counts']['articles'] = (int) $db->loadResult();
$db->setQuery($db->getQuery(true)->select('COUNT(*)')->from($db->quoteName('#__users')));
$payload['counts']['users'] = (int) $db->loadResult();
$db->setQuery($db->getQuery(true)->select('COUNT(*)')->from($db->quoteName('#__extensions'))->where($db->quoteName('enabled') . ' = 1'));
$payload['counts']['extensions'] = (int) $db->loadResult();
$this->sendJson(200, $payload);
}
/**
* Send a JSON response and close.
*
* @param int $code HTTP status code
* @param array $payload Response data
*
* @return void
*
* @since 1.0.0
*/
private function sendJson(int $code, array $payload): void
{
$app = Factory::getApplication();
$app->setHeader('Content-Type', 'application/json', true);
$app->setHeader('Status', (string) $code, true);
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
$app->close();
}
}
@@ -0,0 +1,283 @@
<?php
/**
* @package MokoWaaS
* @subpackage com_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoWaaS\Api\Controller;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Installer\Installer;
use Joomla\CMS\MVC\Controller\BaseController;
/**
* Extension install-from-URL API controller.
*
* POST /api/index.php/v1/mokowaas/install
* Body: {"url": "https://example.com/path/to/extension.zip"}
*
* Downloads a ZIP from the given URL and installs it via Joomla's Installer.
* Requires a Joomla API token with core.manage on com_installer.
*
* @since 02.21.00
*/
class InstallController extends BaseController
{
/**
* Maximum allowed download size in bytes (64 MB).
*
* @var int
* @since 02.21.00
*/
private const MAX_DOWNLOAD_BYTES = 67108864;
/**
* Install an extension from a remote ZIP URL.
*
* @return void
*
* @since 02.21.00
*/
public function execute(): void
{
$app = Factory::getApplication();
if ($app->input->getMethod() !== 'POST')
{
$this->sendJson(405, ['error' => 'POST required']);
return;
}
$user = $app->getIdentity();
if (!$user->authorise('core.manage', 'com_installer'))
{
$this->sendJson(403, ['error' => 'Not authorized — requires core.manage on com_installer']);
return;
}
// Parse JSON body
$body = json_decode($app->input->json->getRaw(), true);
$url = $body['url'] ?? '';
if ($url === '')
{
$this->sendJson(400, ['error' => 'Missing "url" in request body']);
return;
}
// Validate URL scheme
if (!preg_match('#^https?://#i', $url))
{
$this->sendJson(400, ['error' => 'URL must use http or https scheme']);
return;
}
// Must point to a .zip file
$path = parse_url($url, PHP_URL_PATH);
if (!$path || !str_ends_with(strtolower($path), '.zip'))
{
$this->sendJson(400, ['error' => 'URL must point to a .zip file']);
return;
}
try
{
$result = $this->downloadAndInstall($url);
$this->sendJson(200, $result);
}
catch (\Throwable $e)
{
$this->sendJson(500, [
'error' => 'Installation failed',
'message' => $e->getMessage(),
]);
}
}
/**
* Download ZIP from URL, extract, and install via Joomla Installer.
*
* @param string $url The remote ZIP URL
*
* @return array Result payload
*
* @throws \RuntimeException on failure
*
* @since 02.21.00
*/
private function downloadAndInstall(string $url): array
{
$config = Factory::getConfig();
$tmpPath = $config->get('tmp_path', JPATH_ROOT . '/tmp');
$zipFile = $tmpPath . '/mokowaas_install_' . bin2hex(random_bytes(8)) . '.zip';
// Download
$this->downloadFile($url, $zipFile);
try
{
// Extract
$extractDir = $tmpPath . '/mokowaas_extract_' . bin2hex(random_bytes(8));
if (!mkdir($extractDir, 0755, true))
{
throw new \RuntimeException('Failed to create extraction directory');
}
$archive = new \Joomla\Archive\Archive;
$archive->extract($zipFile, $extractDir);
// Install
$installer = Installer::getInstance();
$result = $installer->install($extractDir);
if (!$result)
{
throw new \RuntimeException('Joomla Installer returned failure — check server logs for details');
}
// Read installed extension info from the installer
$manifest = $installer->getManifest();
$name = $manifest ? (string) $manifest->name : 'Unknown';
$version = $manifest ? (string) $manifest->version : 'Unknown';
$type = $installer->get('extension.type', 'Unknown');
return [
'status' => 'ok',
'message' => 'Extension installed successfully',
'extension' => [
'name' => $name,
'version' => $version,
'type' => $type,
],
'source_url' => $url,
];
}
finally
{
// Clean up temp files
@unlink($zipFile);
if (isset($extractDir) && is_dir($extractDir))
{
$this->removeDirectory($extractDir);
}
}
}
/**
* Download a file from a URL with size limit enforcement.
*
* @param string $url Remote URL
* @param string $destPath Local destination path
*
* @return void
*
* @throws \RuntimeException on failure
*
* @since 02.21.00
*/
private function downloadFile(string $url, string $destPath): void
{
$ch = curl_init($url);
if ($ch === false)
{
throw new \RuntimeException('Failed to initialise cURL');
}
$fp = fopen($destPath, 'wb');
if ($fp === false)
{
curl_close($ch);
throw new \RuntimeException('Failed to open temp file for writing');
}
curl_setopt_array($ch, [
CURLOPT_FILE => $fp,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 5,
CURLOPT_TIMEOUT => 120,
CURLOPT_CONNECTTIMEOUT => 15,
CURLOPT_FAILONERROR => true,
CURLOPT_USERAGENT => 'MokoWaaS-Installer/1.0',
]);
$success = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
$fileSize = curl_getinfo($ch, CURLINFO_SIZE_DOWNLOAD);
curl_close($ch);
fclose($fp);
if (!$success)
{
@unlink($destPath);
throw new \RuntimeException('Download failed (HTTP ' . $httpCode . '): ' . $error);
}
if ($fileSize > self::MAX_DOWNLOAD_BYTES)
{
@unlink($destPath);
throw new \RuntimeException('Download exceeds maximum size of ' . (self::MAX_DOWNLOAD_BYTES / 1048576) . ' MB');
}
}
/**
* Recursively remove a directory and its contents.
*
* @param string $dir Directory path
*
* @return void
*
* @since 02.21.00
*/
private function removeDirectory(string $dir): void
{
$items = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($items as $item)
{
if ($item->isDir())
{
@rmdir($item->getPathname());
}
else
{
@unlink($item->getPathname());
}
}
@rmdir($dir);
}
/**
* Send a JSON response and close.
*
* @param int $code HTTP status code
* @param array $payload Response data
*
* @return void
*
* @since 02.21.00
*/
private function sendJson(int $code, array $payload): void
{
$app = Factory::getApplication();
$app->setHeader('Content-Type', 'application/json', true);
$app->setHeader('Status', (string) $code, true);
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
$app->close();
}
}
@@ -0,0 +1,120 @@
<?php
/**
* @package MokoWaaS
* @subpackage com_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoWaaS\Api\Controller;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Controller\BaseController;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\Registry\Registry;
/**
* Demo site reset API controller.
*
* POST /api/index.php/v1/mokowaas/reset
* Body: {"baseline": "default"}
*
* Restores the site to a named baseline snapshot.
* Requires a Joomla API token with core.manage on com_plugins.
*
* @since 02.21.00
*/
class ResetController extends BaseController
{
/**
* Restore site to a baseline snapshot.
*
* @return void
*
* @since 02.21.00
*/
public function execute(): void
{
$app = Factory::getApplication();
if ($app->input->getMethod() !== 'POST')
{
$this->sendJson(405, ['error' => 'POST required']);
return;
}
$user = $app->getIdentity();
if (!$user->authorise('core.manage', 'com_plugins'))
{
$this->sendJson(403, ['error' => 'Not authorized']);
return;
}
$plugin = PluginHelper::getPlugin('system', 'mokowaas');
if (!$plugin)
{
$this->sendJson(503, ['error' => 'MokoWaaS system plugin not enabled']);
return;
}
$params = new Registry($plugin->params);
try
{
$service = $this->createService($params);
$result = $service->restoreSnapshot('default');
$this->sendJson(200, $result);
}
catch (\Throwable $e)
{
$this->sendJson(500, [
'error' => 'Reset failed',
'message' => $e->getMessage(),
]);
}
}
/**
* Create DemoResetService from plugin params.
*
* @param Registry $params Plugin parameters
*
* @return \Moko\Plugin\System\MokoWaaS\Service\DemoResetService
*
* @since 02.21.00
*/
private function createService(Registry $params)
{
$serviceFile = JPATH_PLUGINS . '/system/mokowaas/Service/DemoResetService.php';
if (!file_exists($serviceFile))
{
throw new \RuntimeException('DemoResetService not found — is the MokoWaaS plugin installed?');
}
require_once $serviceFile;
$media = (bool) $params->get('demo_snapshot_include_media', 1);
return new \Moko\Plugin\System\MokoWaaS\Service\DemoResetService($media);
}
/**
* @param int $code HTTP status code
* @param array $payload Response data
* @return void
*/
private function sendJson(int $code, array $payload): void
{
$app = Factory::getApplication();
$app->setHeader('Content-Type', 'application/json', true);
$app->setHeader('Status', (string) $code, true);
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
$app->close();
}
}
@@ -0,0 +1,151 @@
<?php
/**
* @package MokoWaaS
* @subpackage com_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoWaaS\Api\Controller;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Controller\BaseController;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\Registry\Registry;
/**
* Snapshot management API controller.
*
* GET /api/index.php/v1/mokowaas/snapshot — list snapshots
* POST /api/index.php/v1/mokowaas/snapshot — create snapshot
*
* @since 02.21.00
*/
class SnapshotController extends BaseController
{
/**
* List all available snapshots.
*
* @return void
*
* @since 02.21.00
*/
public function displayList(): void
{
$app = Factory::getApplication();
$user = $app->getIdentity();
if (!$user->authorise('core.manage', 'com_plugins'))
{
$this->sendJson(403, ['error' => 'Not authorized']);
return;
}
try
{
$service = $this->createService();
$this->sendJson(200, [
'status' => 'ok',
'snapshots' => $service->listSnapshots(),
]);
}
catch (\Throwable $e)
{
$this->sendJson(500, [
'error' => 'Failed to list snapshots',
'message' => $e->getMessage(),
]);
}
}
/**
* Create a new snapshot.
*
* @return void
*
* @since 02.21.00
*/
public function execute(): void
{
$app = Factory::getApplication();
if ($app->input->getMethod() !== 'POST')
{
$this->sendJson(405, ['error' => 'POST required']);
return;
}
$user = $app->getIdentity();
if (!$user->authorise('core.manage', 'com_plugins'))
{
$this->sendJson(403, ['error' => 'Not authorized']);
return;
}
try
{
$plugin = PluginHelper::getPlugin('system', 'mokowaas');
$params = $plugin ? new Registry($plugin->params) : new Registry;
$body = json_decode($app->input->json->getRaw(), true);
$name = $body['name']
?? $params->get('demo_active_baseline', 'default');
$service = $this->createService();
$result = $service->createSnapshot($name);
$this->sendJson(200, $result);
}
catch (\Throwable $e)
{
$this->sendJson(500, [
'error' => 'Snapshot failed',
'message' => $e->getMessage(),
]);
}
}
/**
* Create DemoResetService from plugin params.
*
* @return \Moko\Plugin\System\MokoWaaS\Service\DemoResetService
*
* @since 02.21.00
*/
private function createService()
{
$serviceFile = JPATH_PLUGINS . '/system/mokowaas/Service/DemoResetService.php';
if (!file_exists($serviceFile))
{
throw new \RuntimeException('DemoResetService not found');
}
require_once $serviceFile;
$plugin = PluginHelper::getPlugin('system', 'mokowaas');
$params = $plugin ? new Registry($plugin->params) : new Registry;
$media = (bool) $params->get('demo_snapshot_include_media', 1);
return new \Moko\Plugin\System\MokoWaaS\Service\DemoResetService($media);
}
/**
* @param int $code HTTP status code
* @param array $payload Response data
* @return void
*/
private function sendJson(int $code, array $payload): void
{
$app = Factory::getApplication();
$app->setHeader('Content-Type', 'application/json', true);
$app->setHeader('Status', (string) $code, true);
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
$app->close();
}
}
@@ -0,0 +1,82 @@
<?php
/**
* @package MokoWaaS
* @subpackage com_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoWaaS\Api\Controller;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Controller\BaseController;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\Registry\Registry;
/**
* Content sync trigger API controller (sender side).
*
* POST /api/index.php/v1/mokowaas/sync
*
* Pushes content to all configured sync targets.
*
* @since 02.21.00
*/
class SyncController extends BaseController
{
public function execute(): void
{
$app = Factory::getApplication();
if ($app->input->getMethod() !== 'POST')
{
$this->sendJson(405, ['error' => 'POST required']);
return;
}
$user = $app->getIdentity();
if (!$user->authorise('core.manage', 'com_plugins'))
{
$this->sendJson(403, ['error' => 'Not authorized']);
return;
}
$plugin = PluginHelper::getPlugin('system', 'mokowaas');
if (!$plugin)
{
$this->sendJson(503, ['error' => 'MokoWaaS system plugin not enabled']);
return;
}
try
{
$params = new Registry($plugin->params);
$targets = json_decode($params->get('sync_targets', '[]'), true) ?: [];
$serviceFile = JPATH_PLUGINS . '/system/mokowaas/Service/ContentSyncService.php';
require_once $serviceFile;
$service = new \Moko\Plugin\System\MokoWaaS\Service\ContentSyncService();
$result = $service->syncAllTargets($targets);
$this->sendJson(200, $result);
}
catch (\Throwable $e)
{
$this->sendJson(500, ['error' => 'Sync failed', 'message' => $e->getMessage()]);
}
}
private function sendJson(int $code, array $payload): void
{
$app = Factory::getApplication();
$app->setHeader('Content-Type', 'application/json', true);
$app->setHeader('Status', (string) $code, true);
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
$app->close();
}
}
@@ -0,0 +1,77 @@
<?php
/**
* @package MokoWaaS
* @subpackage com_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoWaaS\Api\Controller;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Controller\BaseController;
/**
* Content sync receiver API controller (target side).
*
* POST /api/index.php/v1/mokowaas/sync-receive
*
* Accepts a JSON payload from a source site and applies the content locally.
*
* @since 02.21.00
*/
class SyncReceiveController extends BaseController
{
public function execute(): void
{
$app = Factory::getApplication();
if ($app->input->getMethod() !== 'POST')
{
$this->sendJson(405, ['error' => 'POST required']);
return;
}
$user = $app->getIdentity();
if (!$user->authorise('core.manage', 'com_plugins'))
{
$this->sendJson(403, ['error' => 'Not authorized']);
return;
}
try
{
$payload = json_decode($app->input->json->getRaw(), true);
if (empty($payload['mokowaas_sync']))
{
$this->sendJson(400, ['error' => 'Invalid payload — missing mokowaas_sync version']);
return;
}
$serviceFile = JPATH_PLUGINS . '/system/mokowaas/Service/ContentSyncReceiver.php';
require_once $serviceFile;
$receiver = new \Moko\Plugin\System\MokoWaaS\Service\ContentSyncReceiver();
$result = $receiver->receive($payload);
$this->sendJson(200, $result);
}
catch (\Throwable $e)
{
$this->sendJson(500, ['error' => 'Sync receive failed', 'message' => $e->getMessage()]);
}
}
private function sendJson(int $code, array $payload): void
{
$app = Factory::getApplication();
$app->setHeader('Content-Type', 'application/json', true);
$app->setHeader('Status', (string) $code, true);
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
$app->close();
}
}
@@ -0,0 +1,94 @@
<?php
/**
* @package MokoWaaS
* @subpackage com_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoWaaS\Api\Controller;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Controller\BaseController;
/**
* Update check API controller.
*
* POST /api/index.php/v1/mokowaas/update
*
* @since 1.0.0
*/
class UpdateController extends BaseController
{
/**
* Trigger Joomla update finder and return count of available updates.
*
* @return void
*
* @since 1.0.0
*/
public function execute(): void
{
$app = Factory::getApplication();
if ($app->input->getMethod() !== 'POST')
{
$this->sendJson(405, ['error' => 'POST required']);
return;
}
$user = $app->getIdentity();
if (!$user->authorise('core.manage', 'com_installer'))
{
$this->sendJson(403, ['error' => 'Not authorized']);
return;
}
try
{
$db = Factory::getDbo();
$db->setQuery($db->getQuery(true)->delete($db->quoteName('#__updates')));
$db->execute();
\Joomla\CMS\Updater\Updater::getInstance()->findUpdates();
$db->setQuery(
$db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__updates'))
->where($db->quoteName('extension_id') . ' != 0')
);
$count = (int) $db->loadResult();
$this->sendJson(200, [
'status' => 'ok',
'updates_found' => $count,
'message' => $count . ' update(s) available',
]);
}
catch (\Throwable $e)
{
$this->sendJson(500, [
'error' => 'Update check failed',
'message' => $e->getMessage(),
]);
}
}
/**
* @param int $code HTTP status code
* @param array $payload Response data
* @return void
*/
private function sendJson(int $code, array $payload): void
{
$app = Factory::getApplication();
$app->setHeader('Content-Type', 'application/json', true);
$app->setHeader('Status', (string) $code, true);
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
$app->close();
}
}
+24
View File
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<extension type="component" method="upgrade">
<name>MokoWaaS API</name>
<author>Moko Consulting</author>
<creationDate>2026-05-23</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.31.00</version>
<version>02.31.00</version>
<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>
<administration>
<files folder="admin">
<folder>services</folder>
</files>
</administration>
<api>
<files folder="api">
<folder>src</folder>
</files>
</api>
</extension>
File diff suppressed because it is too large Load Diff
@@ -7,7 +7,7 @@
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* VERSION: 02.01.08
* VERSION: 02.31.00
* PATH: /src/Field/AllowedIpsField.php
* BRIEF: Custom form field that displays the current IP whitelist
*/
@@ -0,0 +1,63 @@
<?php
/**
* @package MokoWaaS
* @subpackage plg_system_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* VERSION: 02.31.00
* PATH: /src/Field/CopyableTokenField.php
* BRIEF: Read-only token field with a copy-to-clipboard button
*/
namespace Moko\Plugin\System\MokoWaaS\Field;
defined('_JEXEC') or die;
use Joomla\CMS\Form\FormField;
/**
* Renders a read-only text input with a "Copy" button, similar to
* Joomla's API token field in the user profile.
*
* @since 02.25.00
*/
class CopyableTokenField extends FormField
{
protected $type = 'CopyableToken';
protected function getInput()
{
$value = htmlspecialchars($this->value ?? '', ENT_QUOTES, 'UTF-8');
$id = $this->id;
if (empty($this->value))
{
return '<div class="alert alert-warning mb-0 py-2">Token will be generated automatically on first save.</div>';
}
return <<<HTML
<div class="input-group">
<input type="text" id="{$id}" name="{$this->name}" value="{$value}"
class="form-control" readonly="readonly" style="font-family:monospace;font-size:0.85em" />
<button type="button" class="btn btn-outline-secondary" onclick="
var inp = document.getElementById('{$id}');
if (navigator.clipboard) {
navigator.clipboard.writeText(inp.value).then(function() {
var btn = inp.nextElementSibling;
var orig = btn.innerHTML;
btn.innerHTML = '<span class=&quot;icon-check&quot; aria-hidden=&quot;true&quot;></span> Copied';
btn.classList.replace('btn-outline-secondary','btn-success');
setTimeout(function(){ btn.innerHTML = orig; btn.classList.replace('btn-success','btn-outline-secondary'); }, 2000);
});
} else {
inp.select(); document.execCommand('copy');
}
"><span class="icon-copy" aria-hidden="true"></span> Copy</button>
</div>
HTML;
}
}
@@ -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.31.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,237 @@
<?php
/**
* @package MokoWaaS
* @subpackage plg_system_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* VERSION: 02.31.00
* PATH: /src/Field/DemoTaskInfoField.php
* BRIEF: Read-only field showing scheduled task info with link to manage it
*/
namespace Moko\Plugin\System\MokoWaaS\Field;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Form\FormField;
use Joomla\CMS\Router\Route;
/**
* Displays the demo reset scheduled task status: schedule, next run,
* last run, and a direct link to edit the task in Joomla's Scheduler.
*
* @since 02.29.00
*/
class DemoTaskInfoField extends FormField
{
protected $type = 'DemoTaskInfo';
protected function getInput()
{
// Query the scheduled task — if it exists and is enabled, demo mode is on
try
{
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__scheduler_tasks'))
->where($db->quoteName('type') . ' = ' . $db->quote('mokowaas.demo.reset'));
$db->setQuery($query);
$task = $db->loadAssoc();
}
catch (\Throwable $e)
{
$task = null;
}
$newTaskLink = Route::_('index.php?option=com_scheduler&task=task.add');
if (!$task)
{
return '<div class="alert alert-info mb-0 py-2">'
. 'No demo reset task configured. '
. '<a href="' . $newTaskLink . '" class="alert-link">Create a Scheduled Task</a> '
. 'and select <strong>MokoWaaS Demo Reset</strong> to enable demo mode.</div>';
}
$taskId = (int) $task['id'];
$state = (int) $task['state'];
$siteTimezone = Factory::getApplication()->get('offset', 'UTC');
// Parse schedule from execution_rules
$rules = json_decode($task['execution_rules'] ?? '{}', true);
$ruleType = $rules['rule-type'] ?? '';
switch ($ruleType)
{
case 'cron-expression':
$schedule = $rules['cron-expression'] ?? '';
$friendlySchedule = $this->friendlySchedule($schedule);
break;
case 'interval-minutes':
$mins = (int) ($rules['interval-minutes'] ?? 0);
if ($mins >= 1440 && $mins % 1440 === 0)
{
$days = $mins / 1440;
$schedule = 'Every ' . $days . ' day' . ($days > 1 ? 's' : '');
}
elseif ($mins >= 60 && $mins % 60 === 0)
{
$hours = $mins / 60;
$schedule = 'Every ' . $hours . ' hour' . ($hours > 1 ? 's' : '');
}
else
{
$schedule = 'Every ' . $mins . ' minute' . ($mins !== 1 ? 's' : '');
}
$friendlySchedule = $schedule;
break;
case 'interval-hours':
$hours = (int) ($rules['interval-hours'] ?? 0);
$schedule = 'Every ' . $hours . ' hour' . ($hours !== 1 ? 's' : '');
$friendlySchedule = $schedule;
break;
case 'interval-days':
$days = (int) ($rules['interval-days'] ?? 0);
$schedule = 'Every ' . $days . ' day' . ($days !== 1 ? 's' : '');
$friendlySchedule = $schedule;
break;
default:
$schedule = $ruleType ?: 'Not set';
$friendlySchedule = 'Custom';
}
// Next execution
$nextExec = $task['next_execution'] ?? '';
$nextFormatted = 'Not scheduled';
$nextBadge = '';
if (!empty($nextExec) && $nextExec !== '0000-00-00 00:00:00')
{
try
{
$dt = new \DateTime($nextExec, new \DateTimeZone('UTC'));
$dt->setTimezone(new \DateTimeZone($siteTimezone));
$nextFormatted = $dt->format('M j, Y g:i A T');
}
catch (\Throwable $e)
{
$nextFormatted = $nextExec;
}
$diff = strtotime($nextExec . ' UTC') - time();
if ($diff <= 0)
{
$nextBadge = '<span class="badge bg-warning text-dark">DUE</span>';
}
elseif ($diff < 3600)
{
$nextBadge = '<span class="badge bg-info">in ' . (int) ceil($diff / 60) . ' min</span>';
}
elseif ($diff < 86400)
{
$nextBadge = '<span class="badge bg-info">in ' . round($diff / 3600, 1) . 'h</span>';
}
else
{
$nextBadge = '<span class="badge bg-secondary">in ' . round($diff / 86400, 1) . 'd</span>';
}
}
// Last execution
$lastExec = $task['last_execution'] ?? '';
$lastFormatted = 'Never';
if (!empty($lastExec) && $lastExec !== '0000-00-00 00:00:00')
{
try
{
$dt = new \DateTime($lastExec, new \DateTimeZone('UTC'));
$dt->setTimezone(new \DateTimeZone($siteTimezone));
$lastFormatted = $dt->format('M j, Y g:i A T');
}
catch (\Throwable $e)
{
$lastFormatted = $lastExec;
}
}
// State badge
$stateBadge = $state === 1
? '<span class="badge bg-success">Enabled</span>'
: '<span class="badge bg-danger">Disabled</span>';
// Link to edit the task
$editLink = Route::_('index.php?option=com_scheduler&task=task.edit&id=' . $taskId);
// Task params — default to On when keys are missing (matches form defaults)
$taskParams = json_decode($task['params'] ?? '{}', true) ?: [];
$bannerOn = !isset($taskParams['banner_enabled']) || (int) $taskParams['banner_enabled'] === 1;
$mediaOn = !isset($taskParams['include_media']) || (int) $taskParams['include_media'] === 1;
$countdownOn = !isset($taskParams['show_countdown']) || (int) $taskParams['show_countdown'] === 1;
// Check if snapshot exists
$snapshotExists = is_dir(JPATH_ROOT . '/mokowaas-snapshots/default');
// Build info card
return '<div class="card card-body bg-light py-2 px-3 mb-0">'
. '<table class="table table-sm table-borderless mb-1" style="max-width:550px">'
. '<tr><td class="text-muted" style="width:130px">Status</td><td>' . $stateBadge . '</td></tr>'
. '<tr><td class="text-muted">Schedule</td><td>' . htmlspecialchars($friendlySchedule) . '</td></tr>'
. '<tr><td class="text-muted">Next Reset</td><td>' . htmlspecialchars($nextFormatted) . ' ' . $nextBadge . '</td></tr>'
. '<tr><td class="text-muted">Last Reset</td><td>' . htmlspecialchars($lastFormatted) . '</td></tr>'
. '<tr><td class="text-muted">Runs</td><td>' . (int) ($task['times_executed'] ?? 0) . ' executed, ' . (int) ($task['times_failed'] ?? 0) . ' failed</td></tr>'
. '<tr><td class="text-muted">Baseline</td><td>' . ($snapshotExists ? '<span class="badge bg-success">Saved</span>' : '<span class="badge bg-warning text-dark">Not taken yet</span>') . '</td></tr>'
. '<tr><td class="text-muted">Banner</td><td>' . ($bannerOn ? 'On' : 'Off') . ($countdownOn ? ' + countdown' : '') . '</td></tr>'
. '<tr><td class="text-muted">Images</td><td>' . ($mediaOn ? 'Included' : 'Excluded') . '</td></tr>'
. '</table>'
. '<a href="' . $editLink . '" class="btn btn-sm btn-outline-primary">'
. '<span class="icon-cog" aria-hidden="true"></span> Manage Scheduled Task</a>'
. '</div>';
}
protected function getLabel()
{
return '<label class="form-label"><strong>Scheduled Reset</strong></label>';
}
/**
* Convert a cron expression to a human-readable string.
*
* @param string $cron Cron expression
*
* @return string
*/
private function friendlySchedule(string $cron): string
{
$map = [
'* * * * *' => 'Every minute',
'*/5 * * * *' => 'Every 5 minutes',
'*/15 * * * *' => 'Every 15 minutes',
'*/30 * * * *' => 'Every 30 minutes',
'0 */1 * * *' => 'Every hour',
'0 */4 * * *' => 'Every 4 hours',
'0 */6 * * *' => 'Every 6 hours',
'0 */12 * * *' => 'Every 12 hours',
'0 0 * * *' => 'Daily at midnight',
'0 6 * * *' => 'Daily at 6:00 AM',
'0 0 * * 0' => 'Weekly (Sunday)',
'0 0 1 * *' => 'Monthly (1st)',
];
return $map[$cron] ?? 'Custom';
}
}
@@ -0,0 +1,156 @@
<?php
/**
* @package MokoWaaS
* @subpackage plg_system_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* VERSION: 02.31.00
* PATH: /src/Field/NextResetField.php
* BRIEF: Read-only field showing next reset time from Joomla scheduled task
*/
namespace Moko\Plugin\System\MokoWaaS\Field;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Form\FormField;
/**
* Pulls the next execution time directly from the Joomla scheduled task
* (#__scheduler_tasks) and displays it formatted in the site timezone.
*
* @since 02.29.00
*/
class NextResetField extends FormField
{
protected $type = 'NextReset';
protected function getInput()
{
// Check if demo mode is enabled
$demoEnabled = false;
if ($this->form)
{
$demoEnabled = (int) $this->form->getValue('demo_mode_enabled', 'params', 0) === 1;
}
if (!$demoEnabled)
{
return '<span class="form-control-plaintext text-muted">Demo mode is off</span>'
. '<input type="hidden" name="' . $this->name . '" value="" />';
}
// Query the actual next_execution from the scheduled task
try
{
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select([
$db->quoteName('next_execution'),
$db->quoteName('last_execution'),
$db->quoteName('state'),
])
->from($db->quoteName('#__scheduler_tasks'))
->where($db->quoteName('type') . ' = ' . $db->quote('mokowaas.demo.reset'));
$db->setQuery($query);
$task = $db->loadAssoc();
}
catch (\Throwable $e)
{
$task = null;
}
if (!$task)
{
return '<div class="alert alert-secondary mb-0 py-2">No scheduled task found — save to create one automatically.</div>'
. '<input type="hidden" name="' . $this->name . '" value="" />';
}
if ((int) $task['state'] !== 1)
{
return '<div class="alert alert-warning mb-0 py-2">Scheduled task is disabled.</div>'
. '<input type="hidden" name="' . $this->name . '" value="" />';
}
$nextExec = $task['next_execution'];
$lastExec = $task['last_execution'];
if (empty($nextExec) || $nextExec === '0000-00-00 00:00:00')
{
return '<div class="alert alert-secondary mb-0 py-2">Waiting for first run...</div>'
. '<input type="hidden" name="' . $this->name . '" value="" />';
}
// Convert to site timezone
$utcTimestamp = strtotime($nextExec);
$siteTimezone = Factory::getApplication()->get('offset', 'UTC');
try
{
$dt = new \DateTime('@' . $utcTimestamp);
$dt->setTimezone(new \DateTimeZone($siteTimezone));
$formatted = $dt->format('l, F j, Y \a\t g:i A T');
}
catch (\Throwable $e)
{
$formatted = $nextExec . ' UTC';
}
// Relative time
$diff = $utcTimestamp - time();
$relative = '';
if ($diff <= 0)
{
$relative = '<span class="badge bg-warning text-dark">overdue</span>';
}
elseif ($diff < 3600)
{
$mins = (int) ceil($diff / 60);
$relative = '<span class="badge bg-info">in ' . $mins . ' min</span>';
}
elseif ($diff < 86400)
{
$hours = round($diff / 3600, 1);
$relative = '<span class="badge bg-info">in ' . $hours . 'h</span>';
}
else
{
$days = round($diff / 86400, 1);
$relative = '<span class="badge bg-secondary">in ' . $days . 'd</span>';
}
// Last run info
$lastInfo = '';
if (!empty($lastExec) && $lastExec !== '0000-00-00 00:00:00')
{
try
{
$lastDt = new \DateTime($lastExec);
$lastDt->setTimezone(new \DateTimeZone($siteTimezone));
$lastInfo = '<small class="text-muted ms-2">Last run: ' . $lastDt->format('M j, g:i A') . '</small>';
}
catch (\Throwable $e)
{
// skip
}
}
return '<div class="d-flex align-items-center gap-2 flex-wrap">'
. '<span class="form-control-plaintext" style="font-weight:500">'
. '<span class="icon-calendar" aria-hidden="true"></span> '
. htmlspecialchars($formatted) . '</span> '
. $relative
. $lastInfo
. '<input type="hidden" name="' . $this->name . '" value="' . htmlspecialchars($nextExec) . '" />'
. '</div>';
}
}
@@ -0,0 +1,175 @@
<?php
/**
* @package MokoWaaS
* @subpackage plg_system_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* VERSION: 02.31.00
* PATH: /src/Field/SnapshotTablesField.php
* BRIEF: Multi-select list field that loads DB tables with sensible defaults
*/
namespace Moko\Plugin\System\MokoWaaS\Field;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Form\FormField;
/**
* Renders a multi-select list box of all Joomla database tables, with
* content-related tables pre-selected by default.
*
* @since 02.26.00
*/
class SnapshotTablesField extends FormField
{
protected $type = 'SnapshotTables';
/**
* Tables selected by default when no value is stored yet.
*
* @var array
* @since 02.25.00
*/
private const DEFAULT_TABLES = [
'#__content',
'#__categories',
'#__fields',
'#__fields_values',
'#__fields_groups',
'#__menu',
'#__menu_types',
'#__modules',
'#__modules_menu',
'#__users',
'#__user_usergroup_map',
'#__user_profiles',
'#__tags',
'#__contentitem_tag_map',
'#__assets',
];
/**
* Table suffixes grouped by category.
*
* @var array
* @since 02.25.00
*/
private const TABLE_GROUPS = [
'Content' => ['content', 'categories', 'fields', 'fields_values', 'fields_groups', 'tags', 'contentitem_tag_map', 'ucm_content', 'ucm_history'],
'Users' => ['users', 'user_usergroup_map', 'user_profiles', 'usergroups', 'user_keys', 'user_mfa'],
'Menus' => ['menu', 'menu_types'],
'Modules' => ['modules', 'modules_menu'],
'Assets' => ['assets'],
];
protected function getInput()
{
$db = Factory::getDbo();
$prefix = $db->getPrefix();
$tables = $db->getTableList();
// Resolve selected values
$selected = $this->value;
if ($selected === null || $selected === '')
{
$selected = self::DEFAULT_TABLES;
}
elseif (is_string($selected))
{
$selected = array_filter(array_map('trim', explode("\n", $selected)));
}
$selected = (array) $selected;
// Flatten nested arrays from broken save format [["#__content"],["#__categories"]]
$selected = array_map(function ($v) {
return is_array($v) ? reset($v) : $v;
}, $selected);
// Group tables
$grouped = [];
foreach ($tables as $table)
{
if (strpos($table, $prefix) !== 0)
{
continue;
}
$suffix = substr($table, strlen($prefix));
$logical = '#__' . $suffix;
$group = 'Other';
foreach (self::TABLE_GROUPS as $groupName => $patterns)
{
if (in_array($suffix, $patterns, true))
{
$group = $groupName;
break;
}
}
$grouped[$group][] = $logical;
}
// Build HTML select with optgroups
$size = (int) ($this->element['size'] ?? 15);
$html = '<select name="' . $this->name . '" id="' . $this->id . '"'
. ' multiple="multiple" size="' . $size . '"'
. ' class="form-select">';
$priority = ['Content', 'Users', 'Menus', 'Modules', 'Assets'];
foreach ($priority as $g)
{
if (!empty($grouped[$g]))
{
$html .= '<optgroup label="' . $g . '">';
foreach ($grouped[$g] as $t)
{
$sel = in_array($t, $selected, true) ? ' selected="selected"' : '';
$html .= '<option value="' . htmlspecialchars($t) . '"' . $sel . '>' . htmlspecialchars($t) . '</option>';
}
$html .= '</optgroup>';
unset($grouped[$g]);
}
}
if (!empty($grouped['Other']))
{
$html .= '<optgroup label="Other">';
foreach ($grouped['Other'] as $t)
{
$sel = in_array($t, $selected, true) ? ' selected="selected"' : '';
$html .= '<option value="' . htmlspecialchars($t) . '"' . $sel . '>' . htmlspecialchars($t) . '</option>';
}
$html .= '</optgroup>';
}
$html .= '</select>';
// "Reset to defaults" link
$defaultsJson = htmlspecialchars(json_encode(self::DEFAULT_TABLES), ENT_QUOTES, 'UTF-8');
$html .= '<div class="mt-1">'
. '<a href="#" class="small" onclick="'
. 'var sel=document.getElementById(\'' . $this->id . '\');'
. 'var defs=' . $defaultsJson . ';'
. 'Array.from(sel.options).forEach(function(o){o.selected=defs.indexOf(o.value)!==-1;});'
. 'return false;'
. '"><span class="icon-refresh" aria-hidden="true"></span> Reset to defaults</a>'
. '</div>';
return $html;
}
}
@@ -0,0 +1,819 @@
<?php
/**
* @package MokoWaaS
* @subpackage plg_system_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
* PATH: /src/packages/plg_system_mokowaas/Service/ContentSyncReceiver.php
* VERSION: 02.31.00
* BRIEF: Receiver-side content sync — applies incoming payload to local DB
*/
namespace Moko\Plugin\System\MokoWaaS\Service;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Log\Log;
/**
* Content Sync Receiver — applies incoming sync payload to the local site.
*
* Processes categories, articles, menu types, menu items, and modules
* from a JSON payload sent by a source MokoWaaS site. Content is matched
* by alias (upsert pattern): existing content is updated, new content
* is inserted.
*
* @since 02.21.00
*/
class ContentSyncReceiver
{
/**
* @var \Joomla\Database\DatabaseInterface
* @since 02.21.00
*/
private $db;
/**
* Warnings collected during sync.
*
* @var array
* @since 02.21.00
*/
private array $warnings = [];
/**
* Cache of resolved category paths → local IDs.
*
* @var array
* @since 02.21.00
*/
private array $catPathCache = [];
/**
* Constructor.
*
* @param \Joomla\Database\DatabaseInterface|null $db Database driver
*
* @since 02.21.00
*/
public function __construct($db = null)
{
$this->db = $db ?: Factory::getDbo();
}
/**
* Process an incoming sync payload.
*
* @param array $payload Decoded JSON payload from the source site
*
* @return array Result summary with per-type counts and warnings
*
* @since 02.21.00
*/
public function receive(array $payload): array
{
if (empty($payload['mokowaas_sync']))
{
return ['status' => 'error', 'message' => 'Invalid payload — missing mokowaas_sync version'];
}
$this->warnings = [];
$results = [];
// Apply in dependency order
try
{
$results['categories'] = $this->applyCategories($payload['categories'] ?? []);
}
catch (\Throwable $e)
{
$results['categories'] = ['error' => $e->getMessage()];
$this->warnings[] = 'Categories failed: ' . $e->getMessage();
}
try
{
$results['articles'] = $this->applyArticles($payload['articles'] ?? []);
}
catch (\Throwable $e)
{
$results['articles'] = ['error' => $e->getMessage()];
$this->warnings[] = 'Articles failed: ' . $e->getMessage();
}
try
{
$results['menu_types'] = $this->applyMenuTypes($payload['menu_types'] ?? []);
}
catch (\Throwable $e)
{
$results['menu_types'] = ['error' => $e->getMessage()];
$this->warnings[] = 'Menu types failed: ' . $e->getMessage();
}
try
{
$results['menu_items'] = $this->applyMenuItems($payload['menu_items'] ?? []);
}
catch (\Throwable $e)
{
$results['menu_items'] = ['error' => $e->getMessage()];
$this->warnings[] = 'Menu items failed: ' . $e->getMessage();
}
try
{
$results['modules'] = $this->applyModules($payload['modules'] ?? []);
}
catch (\Throwable $e)
{
$results['modules'] = ['error' => $e->getMessage()];
$this->warnings[] = 'Modules failed: ' . $e->getMessage();
}
Log::add(
sprintf('Content sync received from %s', $payload['source_site'] ?? 'unknown'),
Log::INFO,
'mokowaas'
);
return [
'status' => 'ok',
'message' => 'Sync applied',
'source_site' => $payload['source_site'] ?? '',
'results' => $results,
'warnings' => $this->warnings,
];
}
/**
* Apply categories — sorted by path depth (shallow first).
*
* @param array $categories Category data from payload
*
* @return array ['inserted' => N, 'updated' => N]
*
* @since 02.21.00
*/
private function applyCategories(array $categories): array
{
$db = $this->db;
$inserted = 0;
$updated = 0;
// Sort by path depth — parents before children
usort($categories, function ($a, $b) {
return substr_count($a['path'], '/') - substr_count($b['path'], '/');
});
foreach ($categories as $cat)
{
$alias = $cat['alias'] ?? '';
$path = $cat['path'] ?? $alias;
if (empty($alias) || !preg_match('/^[a-z0-9\-\/]+$/i', $path))
{
$this->warnings[] = 'Skipped category with invalid alias/path: ' . $alias;
continue;
}
// Resolve parent ID from path
$parentId = 1; // Root
$pathParts = explode('/', $path);
if (count($pathParts) > 1)
{
$parentPath = implode('/', array_slice($pathParts, 0, -1));
$parentId = $this->resolveCategoryPath($parentPath);
if ($parentId === 0)
{
$this->warnings[] = 'Parent category not found for: ' . $path;
$parentId = 1;
}
}
// Check if category exists
$query = $db->getQuery(true)
->select($db->quoteName('id'))
->from($db->quoteName('#__categories'))
->where($db->quoteName('alias') . ' = ' . $db->quote($alias))
->where($db->quoteName('extension') . ' = ' . $db->quote('com_content'))
->where($db->quoteName('parent_id') . ' = ' . (int) $parentId);
$db->setQuery($query);
$existingId = (int) $db->loadResult();
$now = Factory::getDate()->toSql();
if ($existingId)
{
$query = $db->getQuery(true)
->update($db->quoteName('#__categories'))
->set($db->quoteName('title') . ' = ' . $db->quote($cat['title']))
->set($db->quoteName('description') . ' = ' . $db->quote($cat['description'] ?? ''))
->set($db->quoteName('published') . ' = ' . (int) ($cat['published'] ?? 1))
->set($db->quoteName('access') . ' = ' . (int) ($cat['access'] ?? 1))
->set($db->quoteName('language') . ' = ' . $db->quote($cat['language'] ?? '*'))
->set($db->quoteName('params') . ' = ' . $db->quote(json_encode($cat['params'] ?? new \stdClass)))
->set($db->quoteName('metadata') . ' = ' . $db->quote(json_encode($cat['metadata'] ?? new \stdClass)))
->set($db->quoteName('modified_time') . ' = ' . $db->quote($now))
->where($db->quoteName('id') . ' = ' . $existingId);
$db->setQuery($query);
$db->execute();
$updated++;
$this->catPathCache[$path] = $existingId;
}
else
{
$obj = (object) [
'title' => $cat['title'],
'alias' => $alias,
'path' => $path,
'extension' => 'com_content',
'description' => $cat['description'] ?? '',
'published' => (int) ($cat['published'] ?? 1),
'access' => (int) ($cat['access'] ?? 1),
'language' => $cat['language'] ?? '*',
'params' => json_encode($cat['params'] ?? new \stdClass),
'metadata' => json_encode($cat['metadata'] ?? new \stdClass),
'parent_id' => $parentId,
'level' => count($pathParts),
'lft' => 0,
'rgt' => 0,
'created_time' => $now,
'modified_time' => $now,
];
$db->insertObject('#__categories', $obj, 'id');
$inserted++;
$this->catPathCache[$path] = (int) $obj->id;
// Rebuild category tree for this extension
$this->rebuildCategoryTree();
}
}
return ['inserted' => $inserted, 'updated' => $updated];
}
/**
* Apply articles — resolve category by alias path, upsert by alias.
*
* @param array $articles Article data from payload
*
* @return array ['inserted' => N, 'updated' => N]
*
* @since 02.21.00
*/
private function applyArticles(array $articles): array
{
$db = $this->db;
$inserted = 0;
$updated = 0;
foreach ($articles as $article)
{
$alias = $article['alias'] ?? '';
if (empty($alias))
{
continue;
}
// Resolve category
$catPath = $article['catid_alias_path'] ?? 'uncategorised';
$catId = $this->resolveCategoryPath($catPath);
if ($catId === 0)
{
$catId = 2; // Joomla's built-in Uncategorised
$this->warnings[] = 'Category "' . $catPath . '" not found for article "' . $alias . '" — assigned to Uncategorised';
}
// Check if article exists
$query = $db->getQuery(true)
->select($db->quoteName('id'))
->from($db->quoteName('#__content'))
->where($db->quoteName('alias') . ' = ' . $db->quote($alias));
$db->setQuery($query);
$existingId = (int) $db->loadResult();
$now = Factory::getDate()->toSql();
if ($existingId)
{
$query = $db->getQuery(true)
->update($db->quoteName('#__content'))
->set($db->quoteName('title') . ' = ' . $db->quote($article['title']))
->set($db->quoteName('introtext') . ' = ' . $db->quote($article['introtext'] ?? ''))
->set($db->quoteName('fulltext') . ' = ' . $db->quote($article['fulltext'] ?? ''))
->set($db->quoteName('state') . ' = ' . (int) ($article['state'] ?? 1))
->set($db->quoteName('catid') . ' = ' . $catId)
->set($db->quoteName('access') . ' = ' . (int) ($article['access'] ?? 1))
->set($db->quoteName('language') . ' = ' . $db->quote($article['language'] ?? '*'))
->set($db->quoteName('featured') . ' = ' . (int) ($article['featured'] ?? 0))
->set($db->quoteName('metadata') . ' = ' . $db->quote(json_encode($article['metadata'] ?? new \stdClass)))
->set($db->quoteName('attribs') . ' = ' . $db->quote(json_encode($article['attribs'] ?? new \stdClass)))
->set($db->quoteName('images') . ' = ' . $db->quote(json_encode($article['images'] ?? new \stdClass)))
->set($db->quoteName('urls') . ' = ' . $db->quote(json_encode($article['urls'] ?? new \stdClass)))
->set($db->quoteName('modified') . ' = ' . $db->quote($now))
->where($db->quoteName('id') . ' = ' . $existingId);
$db->setQuery($query);
$db->execute();
$updated++;
}
else
{
$obj = (object) [
'title' => $article['title'],
'alias' => $alias,
'introtext' => $article['introtext'] ?? '',
'fulltext' => $article['fulltext'] ?? '',
'state' => (int) ($article['state'] ?? 1),
'catid' => $catId,
'access' => (int) ($article['access'] ?? 1),
'language' => $article['language'] ?? '*',
'featured' => (int) ($article['featured'] ?? 0),
'publish_up' => $article['publish_up'] ?? $now,
'publish_down' => $article['publish_down'],
'metadata' => json_encode($article['metadata'] ?? new \stdClass),
'attribs' => json_encode($article['attribs'] ?? new \stdClass),
'images' => json_encode($article['images'] ?? new \stdClass),
'urls' => json_encode($article['urls'] ?? new \stdClass),
'created' => $now,
'modified' => $now,
];
$db->insertObject('#__content', $obj, 'id');
$inserted++;
}
}
return ['inserted' => $inserted, 'updated' => $updated];
}
/**
* Apply menu types — insert if not exists.
*
* @param array $menuTypes Menu type data from payload
*
* @return array ['inserted' => N, 'updated' => N]
*
* @since 02.21.00
*/
private function applyMenuTypes(array $menuTypes): array
{
$db = $this->db;
$inserted = 0;
$updated = 0;
foreach ($menuTypes as $mt)
{
$menutype = $mt['menutype'] ?? '';
if (empty($menutype))
{
continue;
}
$query = $db->getQuery(true)
->select($db->quoteName('id'))
->from($db->quoteName('#__menu_types'))
->where($db->quoteName('menutype') . ' = ' . $db->quote($menutype));
$db->setQuery($query);
if ($db->loadResult())
{
$query = $db->getQuery(true)
->update($db->quoteName('#__menu_types'))
->set($db->quoteName('title') . ' = ' . $db->quote($mt['title'] ?? $menutype))
->set($db->quoteName('description') . ' = ' . $db->quote($mt['description'] ?? ''))
->where($db->quoteName('menutype') . ' = ' . $db->quote($menutype));
$db->setQuery($query);
$db->execute();
$updated++;
}
else
{
$obj = (object) [
'title' => $mt['title'] ?? $menutype,
'menutype' => $menutype,
'description' => $mt['description'] ?? '',
];
$db->insertObject('#__menu_types', $obj);
$inserted++;
}
}
return ['inserted' => $inserted, 'updated' => $updated];
}
/**
* Apply menu items — resolve parent aliases and {catid:path} tokens.
*
* @param array $items Menu item data from payload
*
* @return array ['inserted' => N, 'updated' => N]
*
* @since 02.21.00
*/
private function applyMenuItems(array $items): array
{
$db = $this->db;
$inserted = 0;
$updated = 0;
// Sort: root items first, then children
usort($items, function ($a, $b) {
$aIsRoot = empty($a['parent_alias']);
$bIsRoot = empty($b['parent_alias']);
if ($aIsRoot && !$bIsRoot) return -1;
if (!$aIsRoot && $bIsRoot) return 1;
return 0;
});
// Resolve component IDs
$compQuery = $db->getQuery(true)
->select([$db->quoteName('extension_id'), $db->quoteName('element')])
->from($db->quoteName('#__extensions'))
->where($db->quoteName('type') . ' = ' . $db->quote('component'));
$db->setQuery($compQuery);
$compMap = [];
foreach ($db->loadAssocList() ?: [] as $c)
{
$compMap[$c['element']] = (int) $c['extension_id'];
}
foreach ($items as $item)
{
$alias = $item['alias'] ?? '';
$menutype = $item['menutype'] ?? '';
if (empty($alias) || empty($menutype))
{
continue;
}
// Resolve parent
$parentId = 1; // Root menu item
if (!empty($item['parent_alias']))
{
$parentId = $this->resolveMenuAlias($menutype, $item['parent_alias']);
if ($parentId === 0)
{
$this->warnings[] = 'Parent menu item "' . $item['parent_alias'] . '" not found for "' . $alias . '"';
$parentId = 1;
}
}
// Resolve {catid:path} tokens in link
$link = $item['link'] ?? '';
if (preg_match_all('/\{catid:([^}]+)\}/', $link, $matches))
{
foreach ($matches[1] as $i => $catPath)
{
$localCatId = $this->resolveCategoryPath($catPath);
if ($localCatId > 0)
{
$link = str_replace($matches[0][$i], (string) $localCatId, $link);
}
else
{
$this->warnings[] = 'Could not resolve {catid:' . $catPath . '} in menu item "' . $alias . '"';
$link = str_replace($matches[0][$i], '0', $link);
}
}
}
$componentId = $compMap[$item['component_name'] ?? ''] ?? 0;
// Check if menu item exists
$query = $db->getQuery(true)
->select($db->quoteName('id'))
->from($db->quoteName('#__menu'))
->where($db->quoteName('alias') . ' = ' . $db->quote($alias))
->where($db->quoteName('menutype') . ' = ' . $db->quote($menutype))
->where($db->quoteName('client_id') . ' = 0');
$db->setQuery($query);
$existingId = (int) $db->loadResult();
if ($existingId)
{
$query = $db->getQuery(true)
->update($db->quoteName('#__menu'))
->set($db->quoteName('title') . ' = ' . $db->quote($item['title']))
->set($db->quoteName('link') . ' = ' . $db->quote($link))
->set($db->quoteName('type') . ' = ' . $db->quote($item['type'] ?? 'component'))
->set($db->quoteName('published') . ' = ' . (int) ($item['published'] ?? 1))
->set($db->quoteName('access') . ' = ' . (int) ($item['access'] ?? 1))
->set($db->quoteName('language') . ' = ' . $db->quote($item['language'] ?? '*'))
->set($db->quoteName('params') . ' = ' . $db->quote(json_encode($item['params'] ?? new \stdClass)))
->set($db->quoteName('parent_id') . ' = ' . $parentId)
->set($db->quoteName('component_id') . ' = ' . $componentId)
->set($db->quoteName('home') . ' = ' . (int) ($item['home'] ?? 0))
->where($db->quoteName('id') . ' = ' . $existingId);
$db->setQuery($query);
$db->execute();
$updated++;
}
else
{
$obj = (object) [
'menutype' => $menutype,
'title' => $item['title'],
'alias' => $alias,
'path' => $alias,
'link' => $link,
'type' => $item['type'] ?? 'component',
'published' => (int) ($item['published'] ?? 1),
'parent_id' => $parentId,
'level' => $parentId <= 1 ? 1 : 2,
'component_id' => $componentId,
'access' => (int) ($item['access'] ?? 1),
'language' => $item['language'] ?? '*',
'params' => json_encode($item['params'] ?? new \stdClass),
'home' => (int) ($item['home'] ?? 0),
'client_id' => 0,
'lft' => 0,
'rgt' => 0,
];
$db->insertObject('#__menu', $obj, 'id');
$inserted++;
}
}
// Rebuild menu tree for each affected menutype
$affectedMenuTypes = array_unique(array_column($items, 'menutype'));
foreach ($affectedMenuTypes as $mt)
{
$this->rebuildMenuTree($mt);
}
return ['inserted' => $inserted, 'updated' => $updated];
}
/**
* Apply modules — upsert by title+position+client_id, rebuild menu assignments.
*
* @param array $modules Module data from payload
*
* @return array ['inserted' => N, 'updated' => N]
*
* @since 02.21.00
*/
private function applyModules(array $modules): array
{
$db = $this->db;
$inserted = 0;
$updated = 0;
foreach ($modules as $mod)
{
$title = $mod['title'] ?? '';
$position = $mod['position'] ?? '';
$clientId = (int) ($mod['client_id'] ?? 0);
if (empty($title))
{
continue;
}
// Check existence by title + position + client_id
$query = $db->getQuery(true)
->select($db->quoteName('id'))
->from($db->quoteName('#__modules'))
->where($db->quoteName('title') . ' = ' . $db->quote($title))
->where($db->quoteName('position') . ' = ' . $db->quote($position))
->where($db->quoteName('client_id') . ' = ' . $clientId);
$db->setQuery($query);
$existingId = (int) $db->loadResult();
if ($existingId)
{
$query = $db->getQuery(true)
->update($db->quoteName('#__modules'))
->set($db->quoteName('module') . ' = ' . $db->quote($mod['module'] ?? ''))
->set($db->quoteName('content') . ' = ' . $db->quote($mod['content'] ?? ''))
->set($db->quoteName('published') . ' = ' . (int) ($mod['published'] ?? 1))
->set($db->quoteName('access') . ' = ' . (int) ($mod['access'] ?? 1))
->set($db->quoteName('language') . ' = ' . $db->quote($mod['language'] ?? '*'))
->set($db->quoteName('params') . ' = ' . $db->quote(json_encode($mod['params'] ?? new \stdClass)))
->where($db->quoteName('id') . ' = ' . $existingId);
$db->setQuery($query);
$db->execute();
$updated++;
$moduleId = $existingId;
}
else
{
$obj = (object) [
'title' => $title,
'module' => $mod['module'] ?? '',
'position' => $position,
'content' => $mod['content'] ?? '',
'published' => (int) ($mod['published'] ?? 1),
'access' => (int) ($mod['access'] ?? 1),
'language' => $mod['language'] ?? '*',
'params' => json_encode($mod['params'] ?? new \stdClass),
'client_id' => $clientId,
'ordering' => 0,
];
$db->insertObject('#__modules', $obj, 'id');
$inserted++;
$moduleId = (int) $obj->id;
}
// Rebuild menu assignments
$query = $db->getQuery(true)
->delete($db->quoteName('#__modules_menu'))
->where($db->quoteName('moduleid') . ' = ' . $moduleId);
$db->setQuery($query);
$db->execute();
$assignment = $mod['menu_assignment'] ?? [];
$assignType = (int) ($assignment['assignment'] ?? 0);
$aliases = $assignment['menu_item_aliases'] ?? [];
if ($assignType === 0 || empty($aliases))
{
// All pages
$obj = (object) ['moduleid' => $moduleId, 'menuid' => 0];
$db->insertObject('#__modules_menu', $obj);
}
else
{
foreach ($aliases as $aliasRef)
{
// Format: "menutype:alias"
$parts = explode(':', $aliasRef, 2);
if (count($parts) !== 2)
{
continue;
}
$menuId = $this->resolveMenuAlias($parts[0], $parts[1]);
if ($menuId > 0)
{
$menuidValue = $assignType === -1 ? -$menuId : $menuId;
$obj = (object) ['moduleid' => $moduleId, 'menuid' => $menuidValue];
$db->insertObject('#__modules_menu', $obj);
}
else
{
$this->warnings[] = 'Module "' . $title . '": menu item "' . $aliasRef . '" not found';
}
}
}
}
return ['inserted' => $inserted, 'updated' => $updated];
}
/**
* Resolve a category alias path to a local category ID.
*
* @param string $path Slash-delimited alias path (e.g. "blog/news")
*
* @return int Category ID, or 0 if not found
*
* @since 02.21.00
*/
private function resolveCategoryPath(string $path): int
{
if (isset($this->catPathCache[$path]))
{
return $this->catPathCache[$path];
}
$db = $this->db;
$segments = explode('/', $path);
$parentId = 1;
foreach ($segments as $segment)
{
$query = $db->getQuery(true)
->select($db->quoteName('id'))
->from($db->quoteName('#__categories'))
->where($db->quoteName('alias') . ' = ' . $db->quote($segment))
->where($db->quoteName('parent_id') . ' = ' . $parentId)
->where($db->quoteName('extension') . ' = ' . $db->quote('com_content'));
$db->setQuery($query);
$id = (int) $db->loadResult();
if ($id === 0)
{
$this->catPathCache[$path] = 0;
return 0;
}
$parentId = $id;
}
$this->catPathCache[$path] = $parentId;
return $parentId;
}
/**
* Resolve a menu item alias to a local menu ID.
*
* @param string $menutype Menu type key
* @param string $alias Menu item alias
*
* @return int Menu item ID, or 0 if not found
*
* @since 02.21.00
*/
private function resolveMenuAlias(string $menutype, string $alias): int
{
$db = $this->db;
$query = $db->getQuery(true)
->select($db->quoteName('id'))
->from($db->quoteName('#__menu'))
->where($db->quoteName('alias') . ' = ' . $db->quote($alias))
->where($db->quoteName('menutype') . ' = ' . $db->quote($menutype))
->where($db->quoteName('client_id') . ' = 0');
$db->setQuery($query);
return (int) $db->loadResult();
}
/**
* Rebuild the nested set (lft/rgt) for the category tree.
*
* Uses Joomla's built-in Table rebuild method.
*
* @return void
*
* @since 02.21.00
*/
private function rebuildCategoryTree(): void
{
try
{
$table = \Joomla\CMS\Table\Table::getInstance('Category');
$table->rebuild();
}
catch (\Throwable $e)
{
$this->warnings[] = 'Category tree rebuild failed: ' . $e->getMessage();
}
}
/**
* Rebuild the nested set (lft/rgt) for a menu type.
*
* @param string $menutype Menu type to rebuild
*
* @return void
*
* @since 02.21.00
*/
private function rebuildMenuTree(string $menutype): void
{
try
{
$table = \Joomla\CMS\Table\Table::getInstance('Menu');
$table->rebuild();
}
catch (\Throwable $e)
{
$this->warnings[] = 'Menu tree rebuild failed for "' . $menutype . '": ' . $e->getMessage();
}
}
}
@@ -0,0 +1,634 @@
<?php
/**
* @package MokoWaaS
* @subpackage plg_system_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
* PATH: /src/packages/plg_system_mokowaas/Service/ContentSyncService.php
* VERSION: 02.31.00
* BRIEF: Sender-side content sync — builds payload and pushes to remote sites
*/
namespace Moko\Plugin\System\MokoWaaS\Service;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Log\Log;
use Joomla\CMS\Uri\Uri;
/**
* Content Sync Service — builds a JSON payload of site content and pushes
* it to one or more remote MokoWaaS sites.
*
* Content is matched by alias on the receiving end (upsert-by-alias).
* Category IDs in menu item links are encoded as {catid:alias/path} tokens
* so the receiver can resolve them to local IDs.
*
* @since 02.21.00
*/
class ContentSyncService
{
/**
* Maximum items per content type to prevent unbounded memory.
*
* @var int
* @since 02.21.00
*/
private const MAX_ITEMS = 2000;
/**
* HTTP timeout for push requests in seconds.
*
* @var int
* @since 02.21.00
*/
private const HTTP_TIMEOUT = 60;
/**
* @var \Joomla\Database\DatabaseInterface
* @since 02.21.00
*/
private $db;
/**
* Category ID → alias path map cache.
*
* @var array
* @since 02.21.00
*/
private array $catPathMap = [];
/**
* Constructor.
*
* @param \Joomla\Database\DatabaseInterface|null $db Database driver
*
* @since 02.21.00
*/
public function __construct($db = null)
{
$this->db = $db ?: Factory::getDbo();
}
/**
* Build the full sync payload from local content.
*
* @return array Structured payload ready for JSON encoding
*
* @since 02.21.00
*/
public function buildPayload(): array
{
$this->catPathMap = $this->buildCategoryPathMap();
return [
'mokowaas_sync' => '1.0',
'source_site' => rtrim(Uri::root(), '/'),
'generated_at' => gmdate('Y-m-d\TH:i:s\Z'),
'categories' => $this->buildCategoryPayload(),
'articles' => $this->buildArticlePayload(),
'menu_types' => $this->buildMenuTypePayload(),
'menu_items' => $this->buildMenuItemPayload(),
'modules' => $this->buildModulePayload(),
];
}
/**
* Push the sync payload to a single target site.
*
* @param string $targetUrl Base URL of the target site
* @param string $token health_api_token for the target
*
* @return array Result with status, message, and per-type counts
*
* @since 02.21.00
*/
public function pushToTarget(string $targetUrl, string $token): array
{
$payload = $this->buildPayload();
$jsonBody = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$endpoint = rtrim($targetUrl, '/') . '/?mokowaas=sync-receive';
$context = stream_context_create([
'http' => [
'method' => 'POST',
'header' => "Authorization: Bearer {$token}\r\nContent-Type: application/json\r\n",
'content' => $jsonBody,
'timeout' => self::HTTP_TIMEOUT,
'ignore_errors' => true,
],
'ssl' => [
'verify_peer' => false,
'verify_peer_name' => false,
],
]);
$response = @file_get_contents($endpoint, false, $context);
if ($response === false)
{
return [
'status' => 'error',
'target' => $targetUrl,
'message' => 'Connection failed — target unreachable',
];
}
// Parse HTTP status from response headers
$httpCode = 0;
if (isset($http_response_header[0]))
{
preg_match('/\d{3}/', $http_response_header[0], $matches);
$httpCode = (int) ($matches[0] ?? 0);
}
$result = json_decode($response, true);
if ($httpCode >= 400 || !$result)
{
return [
'status' => 'error',
'target' => $targetUrl,
'http_code' => $httpCode,
'message' => $result['error'] ?? $result['message'] ?? 'Unknown error from target',
];
}
$result['target'] = $targetUrl;
return $result;
}
/**
* Push content to all configured sync targets.
*
* @param array $targets Array of ['url' => ..., 'token' => ..., 'label' => ...]
*
* @return array Per-target results
*
* @since 02.21.00
*/
public function syncAllTargets(array $targets): array
{
$results = [];
foreach ($targets as $target)
{
$url = $target['url'] ?? '';
$token = $target['token'] ?? '';
$label = $target['label'] ?? $url;
if (empty($url) || empty($token))
{
$results[] = [
'status' => 'skipped',
'target' => $label,
'message' => 'Missing URL or token',
];
continue;
}
try
{
$result = $this->pushToTarget($url, $token);
$result['label'] = $label;
$results[] = $result;
}
catch (\Throwable $e)
{
$results[] = [
'status' => 'error',
'target' => $label,
'message' => $e->getMessage(),
];
}
}
Log::add(
sprintf('Content sync pushed to %d target(s)', count($targets)),
Log::INFO,
'mokowaas'
);
return [
'status' => 'ok',
'message' => sprintf('Sync completed for %d target(s)', count($results)),
'targets' => $results,
];
}
/**
* Build category ID → alias path map.
*
* @return array [id => 'parent-alias/child-alias']
*
* @since 02.21.00
*/
private function buildCategoryPathMap(): array
{
$db = $this->db;
$query = $db->getQuery(true)
->select([$db->quoteName('id'), $db->quoteName('alias'), $db->quoteName('parent_id')])
->from($db->quoteName('#__categories'))
->where($db->quoteName('extension') . ' = ' . $db->quote('com_content'))
->where($db->quoteName('published') . ' != -2')
->where($db->quoteName('id') . ' > 1');
$db->setQuery($query);
$rows = $db->loadAssocList('id');
$map = [];
foreach ($rows as $id => $row)
{
$map[$id] = $this->resolvePathFromRows($id, $rows);
}
return $map;
}
/**
* Recursively build alias path for a category ID.
*
* @param int $id Category ID
* @param array $rows All category rows keyed by ID
*
* @return string Slash-delimited alias path
*
* @since 02.21.00
*/
private function resolvePathFromRows(int $id, array $rows): string
{
if (!isset($rows[$id]))
{
return '';
}
$row = $rows[$id];
$parentId = (int) $row['parent_id'];
if ($parentId <= 1 || !isset($rows[$parentId]))
{
return $row['alias'];
}
return $this->resolvePathFromRows($parentId, $rows) . '/' . $row['alias'];
}
/**
* Build category payload.
*
* @return array
*
* @since 02.21.00
*/
private function buildCategoryPayload(): array
{
$db = $this->db;
$query = $db->getQuery(true)
->select([
$db->quoteName('id'),
$db->quoteName('title'),
$db->quoteName('alias'),
$db->quoteName('description'),
$db->quoteName('published'),
$db->quoteName('access'),
$db->quoteName('language'),
$db->quoteName('params'),
$db->quoteName('metadata'),
])
->from($db->quoteName('#__categories'))
->where($db->quoteName('extension') . ' = ' . $db->quote('com_content'))
->where($db->quoteName('published') . ' != -2')
->where($db->quoteName('id') . ' > 1')
->order($db->quoteName('lft') . ' ASC')
->setLimit(self::MAX_ITEMS);
$db->setQuery($query);
$rows = $db->loadAssocList();
$categories = [];
foreach ($rows as $row)
{
$categories[] = [
'title' => $row['title'],
'alias' => $row['alias'],
'path' => $this->catPathMap[(int) $row['id']] ?? $row['alias'],
'description' => $row['description'] ?? '',
'published' => (int) $row['published'],
'access' => (int) $row['access'],
'language' => $row['language'],
'params' => json_decode($row['params'] ?: '{}', true),
'metadata' => json_decode($row['metadata'] ?: '{}', true),
];
}
return $categories;
}
/**
* Build article payload.
*
* @return array
*
* @since 02.21.00
*/
private function buildArticlePayload(): array
{
$db = $this->db;
$query = $db->getQuery(true)
->select([
$db->quoteName('title'),
$db->quoteName('alias'),
$db->quoteName('introtext'),
$db->quoteName('fulltext'),
$db->quoteName('state'),
$db->quoteName('catid'),
$db->quoteName('access'),
$db->quoteName('language'),
$db->quoteName('featured'),
$db->quoteName('publish_up'),
$db->quoteName('publish_down'),
$db->quoteName('metadata'),
$db->quoteName('attribs'),
$db->quoteName('images'),
$db->quoteName('urls'),
])
->from($db->quoteName('#__content'))
->where($db->quoteName('state') . ' != -2')
->order($db->quoteName('id') . ' ASC')
->setLimit(self::MAX_ITEMS);
$db->setQuery($query);
$rows = $db->loadAssocList();
$articles = [];
foreach ($rows as $row)
{
$articles[] = [
'title' => $row['title'],
'alias' => $row['alias'],
'introtext' => $row['introtext'],
'fulltext' => $row['fulltext'],
'state' => (int) $row['state'],
'catid_alias_path' => $this->catPathMap[(int) $row['catid']] ?? 'uncategorised',
'access' => (int) $row['access'],
'language' => $row['language'],
'featured' => (int) $row['featured'],
'publish_up' => $row['publish_up'],
'publish_down' => $row['publish_down'],
'metadata' => json_decode($row['metadata'] ?: '{}', true),
'attribs' => json_decode($row['attribs'] ?: '{}', true),
'images' => json_decode($row['images'] ?: '{}', true),
'urls' => json_decode($row['urls'] ?: '{}', true),
];
}
return $articles;
}
/**
* Build menu type payload.
*
* @return array
*
* @since 02.21.00
*/
private function buildMenuTypePayload(): array
{
$db = $this->db;
$query = $db->getQuery(true)
->select([
$db->quoteName('title'),
$db->quoteName('menutype'),
$db->quoteName('description'),
])
->from($db->quoteName('#__menu_types'));
$db->setQuery($query);
return $db->loadAssocList() ?: [];
}
/**
* Build menu item payload with {catid:path} tokens in links.
*
* @return array
*
* @since 02.21.00
*/
private function buildMenuItemPayload(): array
{
$db = $this->db;
$query = $db->getQuery(true)
->select([
$db->quoteName('a.title'),
$db->quoteName('a.alias'),
$db->quoteName('a.menutype'),
$db->quoteName('a.parent_id'),
$db->quoteName('a.link'),
$db->quoteName('a.type'),
$db->quoteName('a.published'),
$db->quoteName('a.access'),
$db->quoteName('a.language'),
$db->quoteName('a.params'),
$db->quoteName('a.home'),
$db->quoteName('a.component_id'),
$db->quoteName('b.alias', 'parent_alias'),
])
->from($db->quoteName('#__menu', 'a'))
->leftJoin(
$db->quoteName('#__menu', 'b') . ' ON '
. $db->quoteName('a.parent_id') . ' = ' . $db->quoteName('b.id')
)
->where($db->quoteName('a.published') . ' != -2')
->where($db->quoteName('a.client_id') . ' = 0')
->where($db->quoteName('a.level') . ' >= 1')
->order($db->quoteName('a.lft') . ' ASC')
->setLimit(self::MAX_ITEMS);
$db->setQuery($query);
$rows = $db->loadAssocList();
// Get component name map
$compQuery = $db->getQuery(true)
->select([$db->quoteName('extension_id'), $db->quoteName('element')])
->from($db->quoteName('#__extensions'))
->where($db->quoteName('type') . ' = ' . $db->quote('component'));
$db->setQuery($compQuery);
$components = $db->loadAssocList('extension_id') ?: [];
$items = [];
foreach ($rows as $row)
{
$link = $row['link'];
// Encode category IDs in com_content links as {catid:path} tokens
if (preg_match('/option=com_content/', $link) && preg_match('/&id=(\d+)/', $link, $m))
{
$catId = (int) $m[1];
if (isset($this->catPathMap[$catId]))
{
$link = preg_replace('/&id=\d+/', '&id={catid:' . $this->catPathMap[$catId] . '}', $link);
}
}
$compName = '';
if (!empty($row['component_id']) && isset($components[$row['component_id']]))
{
$compName = $components[$row['component_id']]['element'];
}
// Root-level items have parent_id=1 (Joomla's root menu item)
$parentAlias = ((int) $row['parent_id'] <= 1) ? '' : ($row['parent_alias'] ?? '');
$items[] = [
'title' => $row['title'],
'alias' => $row['alias'],
'menutype' => $row['menutype'],
'parent_alias' => $parentAlias,
'link' => $link,
'type' => $row['type'],
'component_name' => $compName,
'published' => (int) $row['published'],
'access' => (int) $row['access'],
'language' => $row['language'],
'params' => json_decode($row['params'] ?: '{}', true),
'home' => (int) $row['home'],
];
}
return $items;
}
/**
* Build module payload with menu assignments.
*
* @return array
*
* @since 02.21.00
*/
private function buildModulePayload(): array
{
$db = $this->db;
$query = $db->getQuery(true)
->select([
$db->quoteName('id'),
$db->quoteName('title'),
$db->quoteName('module'),
$db->quoteName('position'),
$db->quoteName('content'),
$db->quoteName('published'),
$db->quoteName('access'),
$db->quoteName('language'),
$db->quoteName('params'),
$db->quoteName('client_id'),
])
->from($db->quoteName('#__modules'))
->where($db->quoteName('client_id') . ' = 0')
->where($db->quoteName('published') . ' != -2')
->order($db->quoteName('ordering') . ' ASC')
->setLimit(self::MAX_ITEMS);
$db->setQuery($query);
$rows = $db->loadAssocList();
// Get all module-menu assignments
$mmQuery = $db->getQuery(true)
->select([
$db->quoteName('mm.moduleid'),
$db->quoteName('mm.menuid'),
$db->quoteName('m.alias', 'menu_alias'),
$db->quoteName('m.menutype'),
])
->from($db->quoteName('#__modules_menu', 'mm'))
->leftJoin(
$db->quoteName('#__menu', 'm') . ' ON '
. $db->quoteName('mm.menuid') . ' = ' . $db->quoteName('m.id')
);
$db->setQuery($mmQuery);
$allAssignments = $db->loadAssocList();
// Group assignments by module ID
$assignmentsByModule = [];
foreach ($allAssignments as $a)
{
$assignmentsByModule[(int) $a['moduleid']][] = $a;
}
$modules = [];
foreach ($rows as $row)
{
$moduleId = (int) $row['id'];
$assignments = $assignmentsByModule[$moduleId] ?? [];
// Determine assignment type: 0 = all pages, positive = selected, negative = excluded
$menuAliases = [];
$assignType = 0;
if (!empty($assignments))
{
$firstMenuId = (int) $assignments[0]['menuid'];
if ($firstMenuId === 0)
{
$assignType = 0; // All pages
}
elseif ($firstMenuId < 0)
{
$assignType = -1; // All except selected
foreach ($assignments as $a)
{
if (!empty($a['menu_alias']))
{
$menuAliases[] = $a['menutype'] . ':' . $a['menu_alias'];
}
}
}
else
{
$assignType = 1; // Selected only
foreach ($assignments as $a)
{
if (!empty($a['menu_alias']))
{
$menuAliases[] = $a['menutype'] . ':' . $a['menu_alias'];
}
}
}
}
$modules[] = [
'title' => $row['title'],
'module' => $row['module'],
'position' => $row['position'],
'content' => $row['content'],
'published' => (int) $row['published'],
'access' => (int) $row['access'],
'language' => $row['language'],
'params' => json_decode($row['params'] ?: '{}', true),
'client_id' => (int) $row['client_id'],
'menu_assignment' => [
'assignment' => $assignType,
'menu_item_aliases' => $menuAliases,
],
];
}
return $modules;
}
}
@@ -0,0 +1,606 @@
<?php
/**
* @package MokoWaaS
* @subpackage plg_system_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
* PATH: /src/packages/plg_system_mokowaas/Service/DemoResetService.php
* VERSION: 02.31.00
* BRIEF: Content-only snapshot/restore for demo site reset
*/
namespace Moko\Plugin\System\MokoWaaS\Service;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Log\Log;
/**
* Demo Reset Service — content-only snapshot and restore.
*
* Only touches safe content tables (articles, categories, menus, modules,
* users, tags, fields). Never touches extensions, assets, sessions,
* schemas, update sites, or any system tables.
*
* @since 02.31.00
*/
class DemoResetService
{
private const MAX_NAME_LENGTH = 64;
private const BATCH_SIZE = 500;
/**
* Safe content tables to snapshot/restore.
* These can be wiped and restored without breaking the Joomla installation.
*/
private const SAFE_TABLES = [
// Content
'#__content',
'#__content_frontpage',
'#__categories',
'#__fields',
'#__fields_values',
'#__fields_groups',
'#__tags',
'#__contentitem_tag_map',
'#__ucm_content',
// Menus
'#__menu',
'#__menu_types',
// Modules
'#__modules',
'#__modules_menu',
// Users
'#__users',
'#__user_usergroup_map',
'#__user_profiles',
// Contact
'#__contact_details',
// Banners
'#__banners',
'#__banner_clients',
'#__banner_tracks',
// Community Builder
'#__comprofiler',
'#__comprofiler_fields',
'#__comprofiler_field_values',
'#__comprofiler_tabs',
'#__comprofiler_members',
'#__comprofiler_lists',
'#__comprofiler_plugin',
'#__comprofiler_userreports',
];
/**
* @var string
*/
private string $snapshotDir;
/**
* @var bool
*/
private bool $includeMedia;
/**
* @param bool $includeMedia Include /images/ directory
* @param string $baseDir Override snapshot root
*/
public function __construct(bool $includeMedia = true, string $baseDir = '')
{
$this->includeMedia = $includeMedia;
$this->snapshotDir = $baseDir ?: JPATH_ROOT . '/mokowaas-snapshots';
}
/**
* List all available snapshots.
*
* @return array
*/
public function listSnapshots(): array
{
$snapshots = [];
if (!is_dir($this->snapshotDir))
{
return $snapshots;
}
foreach (glob($this->snapshotDir . '/*/manifest.json') as $path)
{
$data = json_decode(file_get_contents($path), true);
if ($data && isset($data['name']))
{
$snapshots[$data['name']] = $data;
}
}
return $snapshots;
}
/**
* Create a content snapshot.
*
* @param string $name Snapshot name
*
* @return array Result
*/
public function createSnapshot(string $name): array
{
$this->validateSnapshotName($name);
$this->ensureSnapshotDir();
$path = $this->getSnapshotPath($name);
if (is_dir($path))
{
$this->removeDirectory($path);
}
mkdir($path, 0755, true);
$db = Factory::getDbo();
$prefix = $db->getPrefix();
$allTables = $db->getTableList();
$dumped = 0;
foreach (self::SAFE_TABLES as $logicalName)
{
$realName = str_replace('#__', $prefix, $logicalName);
if (!in_array($realName, $allTables))
{
continue;
}
$this->dumpTable($logicalName, $realName, $path, $db);
$dumped++;
}
// Media
$hasMedia = false;
if ($this->includeMedia && is_dir(JPATH_ROOT . '/images'))
{
$hasMedia = $this->zipDirectory(JPATH_ROOT . '/images', $path . '/media.zip');
}
$manifest = [
'name' => $name,
'created_at' => gmdate('Y-m-d\TH:i:s\Z'),
'type' => 'content-only',
'tables' => $dumped,
'has_media' => $hasMedia,
'joomla_version' => JVERSION,
];
file_put_contents($path . '/manifest.json', json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
Log::add(sprintf('Demo snapshot "%s" created (%d tables, media=%s)', $name, $dumped, $hasMedia ? 'yes' : 'no'), Log::INFO, 'mokowaas');
return [
'status' => 'ok',
'message' => 'Snapshot created',
'name' => $name,
'tables' => $dumped,
'has_media' => $hasMedia,
];
}
/**
* Restore from a snapshot.
*
* @param string $name Snapshot name
*
* @return array Result
*/
public function restoreSnapshot(string $name): array
{
$this->validateSnapshotName($name);
$path = $this->getSnapshotPath($name);
$manifest = $path . '/manifest.json';
if (!file_exists($manifest))
{
throw new \RuntimeException('Snapshot not found: ' . $name);
}
$manifestData = json_decode(file_get_contents($manifest), true);
// Clear cache
try { Factory::getCache('')->clean(''); } catch (\Throwable $e) {}
$db = Factory::getDbo();
$prefix = $db->getPrefix();
$restored = 0;
$sqlFiles = glob($path . '/*.sql');
foreach ($sqlFiles as $sqlFile)
{
try
{
$this->restoreTable($sqlFile, $db, $prefix);
$restored++;
}
catch (\Throwable $e)
{
Log::add('Demo reset: failed to restore ' . basename($sqlFile) . ': ' . $e->getMessage(), Log::ERROR, 'mokowaas');
}
}
// Restore /images/
$mediaRestored = false;
if (($manifestData['has_media'] ?? false) && file_exists($path . '/media.zip'))
{
$this->clearDirectory(JPATH_ROOT . '/images');
$zip = new \ZipArchive();
if ($zip->open($path . '/media.zip') === true)
{
$zip->extractTo(JPATH_ROOT . '/images');
$zip->close();
$mediaRestored = true;
}
}
// Rebuild assets table to fix ACL after content restore
$this->rebuildAssets();
Log::add(sprintf('Demo site reset (%d tables, media=%s)', $restored, $mediaRestored ? 'yes' : 'no'), Log::WARNING, 'mokowaas');
return [
'status' => 'ok',
'message' => 'Site content restored',
'baseline' => $name,
'restored_tables' => $restored,
'media_restored' => $mediaRestored,
];
}
/**
* Delete a snapshot.
*/
public function deleteSnapshot(string $name): bool
{
$this->validateSnapshotName($name);
$path = $this->getSnapshotPath($name);
if (!is_dir($path))
{
return false;
}
$this->removeDirectory($path);
return true;
}
/**
* Rebuild the assets table after content restore.
*
* Deletes content-related asset entries (which now have stale IDs)
* and rebuilds them using Joomla's Table classes. Extension and
* component-level assets are left untouched.
*
* @return void
*/
private function rebuildAssets(): void
{
try
{
$db = Factory::getDbo();
// Delete content-level assets (articles, categories, modules, etc.)
// Keep component-level and root assets intact
$contentAssetPrefixes = [
'com_content.article.%',
'com_content.category.%',
'com_contact.contact.%',
'com_banners.banner.%',
'com_banners.category.%',
'com_modules.module.%',
'com_menus.menu.%',
'com_users.user.%',
];
foreach ($contentAssetPrefixes as $prefix)
{
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__assets'))
->where($db->quoteName('name') . ' LIKE ' . $db->quote($prefix))
);
$db->execute();
}
// Rebuild category tree (also fixes category assets)
$catTable = \Joomla\CMS\Table\Table::getInstance('Category');
if ($catTable)
{
$catTable->rebuild();
}
// Rebuild menu tree
$menuTable = \Joomla\CMS\Table\Table::getInstance('Menu');
if ($menuTable)
{
$menuTable->rebuild();
}
// Rebuild asset tree
$assetTable = \Joomla\CMS\Table\Table::getInstance('Asset');
if ($assetTable)
{
$assetTable->rebuild();
}
// Re-create assets for content items that lost theirs
$this->fixContentAssets($db);
}
catch (\Throwable $e)
{
Log::add('Asset rebuild warning: ' . $e->getMessage(), Log::WARNING, 'mokowaas');
}
}
/**
* Re-create missing asset entries for content items.
*
* After deleting stale assets and restoring content, some items
* may reference asset_id=0. This creates new asset rows for them.
*
* @param \Joomla\Database\DatabaseInterface $db
*
* @return void
*/
private function fixContentAssets($db): void
{
// Fix articles with missing assets
$query = $db->getQuery(true)
->select([$db->quoteName('id'), $db->quoteName('title'), $db->quoteName('alias')])
->from($db->quoteName('#__content'))
->where($db->quoteName('asset_id') . ' = 0');
$db->setQuery($query);
$articles = $db->loadAssocList() ?: [];
// Find the com_content component asset as parent
$db->setQuery(
$db->getQuery(true)
->select($db->quoteName('id'))
->from($db->quoteName('#__assets'))
->where($db->quoteName('name') . ' = ' . $db->quote('com_content'))
);
$contentAssetId = (int) $db->loadResult();
foreach ($articles as $article)
{
$assetName = 'com_content.article.' . (int) $article['id'];
$asset = (object) [
'parent_id' => $contentAssetId ?: 1,
'lft' => 0,
'rgt' => 0,
'level' => 0,
'name' => $assetName,
'title' => $article['title'],
'rules' => '{}',
];
$db->insertObject('#__assets', $asset, 'id');
// Update content row with new asset_id
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__content'))
->set($db->quoteName('asset_id') . ' = ' . (int) $asset->id)
->where($db->quoteName('id') . ' = ' . (int) $article['id'])
);
$db->execute();
}
// Rebuild asset tree again after inserts
try
{
$assetTable = \Joomla\CMS\Table\Table::getInstance('Asset');
if ($assetTable)
{
$assetTable->rebuild();
}
}
catch (\Throwable $e)
{
// Best effort
}
}
// ------------------------------------------------------------------
// Private helpers
// ------------------------------------------------------------------
private function dumpTable(string $logicalName, string $realName, string $dir, $db): void
{
$safeFileName = str_replace('#__', 'jml__', $logicalName);
$fp = fopen($dir . '/' . $safeFileName . '.sql', 'w');
$columns = $db->getTableColumns($realName, false);
$colNames = array_keys($columns);
$quotedCols = array_map([$db, 'quoteName'], $colNames);
$colList = implode(', ', $quotedCols);
$offset = 0;
while (true)
{
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName($realName))
->setLimit(self::BATCH_SIZE, $offset);
$db->setQuery($query);
$rows = $db->loadAssocList();
if (empty($rows))
{
break;
}
$values = [];
foreach ($rows as $row)
{
$vals = [];
foreach ($colNames as $col)
{
$vals[] = $row[$col] === null ? 'NULL' : $db->quote($row[$col]);
}
$values[] = '(' . implode(', ', $vals) . ')';
}
fwrite($fp, 'INSERT INTO ' . $db->quoteName($realName) . ' (' . $colList . ') VALUES ' . "\n" . implode(",\n", $values) . ";\n\n");
$offset += self::BATCH_SIZE;
if (count($rows) < self::BATCH_SIZE)
{
break;
}
}
fclose($fp);
}
private function restoreTable(string $sqlFile, $db, string $prefix): void
{
$baseName = basename($sqlFile, '.sql');
$realTable = str_replace('jml__', $prefix, $baseName);
$db->setQuery('TRUNCATE TABLE ' . $db->quoteName($realTable));
$db->execute();
$sql = file_get_contents($sqlFile);
if (empty(trim($sql)))
{
return;
}
$statements = array_filter(
array_map('trim', explode(";\n", $sql)),
function ($s) { return !empty($s) && $s !== ';'; }
);
foreach ($statements as $statement)
{
$statement = rtrim($statement, ';');
if (empty($statement))
{
continue;
}
$db->setQuery($statement);
$db->execute();
}
}
private function zipDirectory(string $sourceDir, string $zipPath): bool
{
$zip = new \ZipArchive();
if ($zip->open($zipPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true)
{
return false;
}
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($sourceDir, \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iterator as $item)
{
$rel = str_replace('\\', '/', substr($item->getPathname(), strlen($sourceDir) + 1));
$item->isDir() ? $zip->addEmptyDir($rel) : $zip->addFile($item->getPathname(), $rel);
}
$zip->close();
return true;
}
private function ensureSnapshotDir(): void
{
if (!is_dir($this->snapshotDir))
{
mkdir($this->snapshotDir, 0755, true);
}
if (!file_exists($this->snapshotDir . '/.htaccess'))
{
file_put_contents($this->snapshotDir . '/.htaccess', "Deny from all\n");
}
}
private function getSnapshotPath(string $name): string
{
return $this->snapshotDir . '/' . $name;
}
private function validateSnapshotName(string $name): void
{
if ($name === '' || strlen($name) > self::MAX_NAME_LENGTH || !preg_match('/^[a-zA-Z0-9_-]+$/', $name))
{
throw new \InvalidArgumentException('Invalid snapshot name');
}
}
private function removeDirectory(string $dir): void
{
$items = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($items as $item)
{
$item->isDir() ? @rmdir($item->getPathname()) : @unlink($item->getPathname());
}
@rmdir($dir);
}
private function clearDirectory(string $dir): void
{
if (!is_dir($dir)) return;
$items = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($items as $item)
{
$item->isDir() ? @rmdir($item->getPathname()) : @unlink($item->getPathname());
}
}
}
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<form>
<field name="url" type="url"
label="PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_URL_LABEL"
description="PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_URL_DESC"
required="true" hint="https://client.example.com" />
<field name="token" type="text"
label="PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_TOKEN_LABEL"
description="PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_TOKEN_DESC"
required="true" hint="health_api_token from target site" />
<field name="label" type="text"
label="PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_LABEL_LABEL"
description="PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_LABEL_DESC"
hint="e.g. Client A" />
</form>
@@ -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>
@@ -88,6 +88,8 @@ PLG_SYSTEM_MOKOWAAS_FIELDSET_TENANT_DESC="Restrict admin features for non-master
PLG_SYSTEM_MOKOWAAS_RESTRICT_INSTALLER_LABEL="Restrict Extension Installer"
PLG_SYSTEM_MOKOWAAS_RESTRICT_INSTALLER_DESC="Block non-master users from installing or removing extensions."
PLG_SYSTEM_MOKOWAAS_ALLOW_UPDATES_LABEL="Allow Extension Updates"
PLG_SYSTEM_MOKOWAAS_ALLOW_UPDATES_DESC="When the installer is restricted, still allow non-master users to update extensions."
PLG_SYSTEM_MOKOWAAS_HIDE_SYSINFO_LABEL="Hide System Information"
PLG_SYSTEM_MOKOWAAS_HIDE_SYSINFO_DESC="Block non-master users from viewing PHP, database, and server information."
PLG_SYSTEM_MOKOWAAS_RESTRICT_CONFIG_LABEL="Restrict Global Configuration"
@@ -99,6 +101,20 @@ PLG_SYSTEM_MOKOWAAS_DISABLE_INSTALL_URL_DESC="Block installing extensions from U
PLG_SYSTEM_MOKOWAAS_HIDDEN_MENUS_LABEL="Hidden Menu Items"
PLG_SYSTEM_MOKOWAAS_HIDDEN_MENUS_DESC="Components to hide from admin menu for non-master users. One per line (e.g., com_installer)."
; ===== Content Sync fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_SYNC_LABEL="Content Sync"
PLG_SYSTEM_MOKOWAAS_FIELDSET_SYNC_DESC="One-way content push to remote MokoWaaS sites. Syncs articles, categories, menus, and modules by alias."
PLG_SYSTEM_MOKOWAAS_SYNC_TARGETS_LABEL="Sync Targets"
PLG_SYSTEM_MOKOWAAS_SYNC_TARGETS_DESC="Remote sites to push content to. Each target requires the site URL and that site's health API token."
PLG_SYSTEM_MOKOWAAS_SYNC_PUSH_NOW_LABEL="Push Content Now"
PLG_SYSTEM_MOKOWAAS_SYNC_PUSH_NOW_DESC="Set to Yes and save to immediately push all content to all configured targets. Resets to No automatically."
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_URL_LABEL="Site URL"
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_URL_DESC="Full URL of the remote Joomla site (e.g. https://client.example.com)."
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_TOKEN_LABEL="API Token"
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_TOKEN_DESC="The health_api_token from the remote site's MokoWaaS plugin settings."
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_LABEL_LABEL="Label"
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_LABEL_DESC="Friendly name for this target (for identification only)."
; ===== Diagnostics fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_DIAGNOSTICS_LABEL="Diagnostics & Monitoring"
PLG_SYSTEM_MOKOWAAS_FIELDSET_DIAGNOSTICS_DESC="Health check endpoint for external monitoring systems (e.g. Grafana). Exposes system status via a token-authenticated JSON API."
@@ -120,6 +136,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_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_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_DESC="Minimum number of characters required for user passwords."
PLG_SYSTEM_MOKOWAAS_PASSWORD_UPPER_LABEL="Require Uppercase"
@@ -130,9 +153,40 @@ PLG_SYSTEM_MOKOWAAS_UPLOAD_TYPES_DESC="Comma-separated list of allowed file exte
PLG_SYSTEM_MOKOWAAS_UPLOAD_SIZE_LABEL="Max Upload Size (MB)"
PLG_SYSTEM_MOKOWAAS_UPLOAD_SIZE_DESC="Maximum file upload size in megabytes."
; ===== Demo Mode fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_DEMO_LABEL="Demo Mode"
PLG_SYSTEM_MOKOWAAS_FIELDSET_DEMO_DESC="Configure demo site behavior with baseline snapshots and automatic periodic reset. When enabled, a warning banner is shown on the frontend."
PLG_SYSTEM_MOKOWAAS_DEMO_ENABLED_LABEL="Enable Demo Mode"
PLG_SYSTEM_MOKOWAAS_DEMO_ENABLED_DESC="When enabled, shows a warning banner on the frontend and enables snapshot/restore functionality."
PLG_SYSTEM_MOKOWAAS_DEMO_BANNER_MSG_LABEL="Banner Message"
PLG_SYSTEM_MOKOWAAS_DEMO_BANNER_MSG_DESC="Message displayed in the demo warning banner on the frontend."
PLG_SYSTEM_MOKOWAAS_DEMO_BANNER_COLOR_LABEL="Banner Color"
PLG_SYSTEM_MOKOWAAS_DEMO_BANNER_COLOR_DESC="Background color for the demo warning banner."
PLG_SYSTEM_MOKOWAAS_DEMO_COUNTDOWN_LABEL="Show Reset Countdown"
PLG_SYSTEM_MOKOWAAS_DEMO_COUNTDOWN_DESC="Display a countdown timer in the banner showing time until the next scheduled reset."
PLG_SYSTEM_MOKOWAAS_DEMO_SCHEDULE_LABEL="Reset Schedule"
PLG_SYSTEM_MOKOWAAS_DEMO_SCHEDULE_DESC="How often the demo site resets. Select a preset or choose Custom to enter a crontab expression."
PLG_SYSTEM_MOKOWAAS_DEMO_CRON_LABEL="Custom Crontab"
PLG_SYSTEM_MOKOWAAS_DEMO_CRON_DESC="Crontab expression for the reset schedule. Format: minute hour day month weekday (e.g. 0 */6 * * * for every 6 hours)."
PLG_SYSTEM_MOKOWAAS_DEMO_NEXT_RESET_LABEL="Next Scheduled Reset"
PLG_SYSTEM_MOKOWAAS_DEMO_NEXT_RESET_DESC="Calculated automatically from the reset schedule. The banner countdown uses this timestamp."
PLG_SYSTEM_MOKOWAAS_DEMO_TABLES_LABEL="Snapshot Tables"
PLG_SYSTEM_MOKOWAAS_DEMO_TABLES_DESC="Database tables to include in snapshots. One per line, using #__ prefix. These tables will be truncated and restored during a reset."
PLG_SYSTEM_MOKOWAAS_DEMO_MEDIA_LABEL="Include Directories"
PLG_SYSTEM_MOKOWAAS_DEMO_MEDIA_DESC="Select which directories to include in the snapshot. Images contains uploaded media, Media contains extension assets."
PLG_SYSTEM_MOKOWAAS_DEMO_ACTIVE_BASELINE_LABEL="Active Baseline Name"
PLG_SYSTEM_MOKOWAAS_DEMO_ACTIVE_BASELINE_DESC="Name of the baseline snapshot used by admin toggles and scheduled tasks. Alphanumeric, hyphens, and underscores only."
PLG_SYSTEM_MOKOWAAS_DEMO_TAKE_SNAPSHOT_LABEL="Take Snapshot Now"
PLG_SYSTEM_MOKOWAAS_DEMO_TAKE_SNAPSHOT_DESC="Save the current site state as a baseline snapshot. Uses the Active Baseline Name above. Resets to No after execution."
PLG_SYSTEM_MOKOWAAS_DEMO_RESTORE_NOW_LABEL="Restore Baseline Now"
PLG_SYSTEM_MOKOWAAS_DEMO_RESTORE_NOW_DESC="Immediately restore the site to the active baseline snapshot. WARNING: This will overwrite current content. Resets to No after execution."
; ===== Site Aliases fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_ALIASES_LABEL="Site Aliases"
PLG_SYSTEM_MOKOWAAS_FIELDSET_ALIASES_DESC="Configure additional domains that mirror this site. Each alias can have its own offline status, robots directive, and backend redirect behavior."
PLG_SYSTEM_MOKOWAAS_PRIMARY_DOMAIN_LABEL="Primary Domain"
PLG_SYSTEM_MOKOWAAS_PRIMARY_DOMAIN_DESC="The primary domain for this site (e.g. waas.dev.mokoconsulting.tech). Used for backend redirect on alias domains. Do not include https:// prefix."
PLG_SYSTEM_MOKOWAAS_SITE_ALIASES_LABEL="Domain Aliases"
PLG_SYSTEM_MOKOWAAS_SITE_ALIASES_DESC="Add domain aliases that serve as mirrors of this site. Each alias gets its own Grafana monitoring datasource."
PLG_SYSTEM_MOKOWAAS_ALIAS_DOMAIN_LABEL="Domain"
@@ -15,5 +15,5 @@
; Variables: (none)
; -----------------------------------------------------------------------------
PLG_SYSTEM_MOKOWAAS="System - Moko WaaS"
PLG_SYSTEM_MOKOWAAS="System - MokoWaaS"
PLG_SYSTEM_MOKOWAAS_XML_DESCRIPTION="This plugin rebrands the Joomla system interface with MokoWaaS identity. It applies language overrides and ensures consistent branding across the platform."
@@ -16,7 +16,7 @@
; Variables: (none)
; -----------------------------------------------------------------------------
PLG_SYSTEM_MOKOWAAS="System - Moko WaaS"
PLG_SYSTEM_MOKOWAAS="System - MokoWaaS"
PLG_SYSTEM_MOKOWAAS_XML_DESCRIPTION="This plugin rebrands the Joomla system interface with MokoWaaS identity. It applies language overrides and ensures consistent branding across the platform."
PLG_SYSTEM_MOKOWAAS_ENABLE_BRANDING_LABEL="Enable Branding"
@@ -88,6 +88,8 @@ PLG_SYSTEM_MOKOWAAS_FIELDSET_TENANT_DESC="Restrict admin features for non-master
PLG_SYSTEM_MOKOWAAS_RESTRICT_INSTALLER_LABEL="Restrict Extension Installer"
PLG_SYSTEM_MOKOWAAS_RESTRICT_INSTALLER_DESC="Block non-master users from installing or removing extensions."
PLG_SYSTEM_MOKOWAAS_ALLOW_UPDATES_LABEL="Allow Extension Updates"
PLG_SYSTEM_MOKOWAAS_ALLOW_UPDATES_DESC="When the installer is restricted, still allow non-master users to update extensions."
PLG_SYSTEM_MOKOWAAS_HIDE_SYSINFO_LABEL="Hide System Information"
PLG_SYSTEM_MOKOWAAS_HIDE_SYSINFO_DESC="Block non-master users from viewing PHP, database, and server information."
PLG_SYSTEM_MOKOWAAS_RESTRICT_CONFIG_LABEL="Restrict Global Configuration"
@@ -99,6 +101,20 @@ PLG_SYSTEM_MOKOWAAS_DISABLE_INSTALL_URL_DESC="Block installing extensions from U
PLG_SYSTEM_MOKOWAAS_HIDDEN_MENUS_LABEL="Hidden Menu Items"
PLG_SYSTEM_MOKOWAAS_HIDDEN_MENUS_DESC="Components to hide from admin menu for non-master users. One per line (e.g., com_installer)."
; ===== Content Sync fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_SYNC_LABEL="Content Sync"
PLG_SYSTEM_MOKOWAAS_FIELDSET_SYNC_DESC="One-way content push to remote MokoWaaS sites. Syncs articles, categories, menus, and modules by alias."
PLG_SYSTEM_MOKOWAAS_SYNC_TARGETS_LABEL="Sync Targets"
PLG_SYSTEM_MOKOWAAS_SYNC_TARGETS_DESC="Remote sites to push content to. Each target requires the site URL and that site's health API token."
PLG_SYSTEM_MOKOWAAS_SYNC_PUSH_NOW_LABEL="Push Content Now"
PLG_SYSTEM_MOKOWAAS_SYNC_PUSH_NOW_DESC="Set to Yes and save to immediately push all content to all configured targets. Resets to No automatically."
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_URL_LABEL="Site URL"
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_URL_DESC="Full URL of the remote Joomla site (e.g. https://client.example.com)."
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_TOKEN_LABEL="API Token"
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_TOKEN_DESC="The health_api_token from the remote site's MokoWaaS plugin settings."
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_LABEL_LABEL="Label"
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_LABEL_DESC="Friendly name for this target (for identification only)."
; ===== Diagnostics fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_DIAGNOSTICS_LABEL="Diagnostics & Monitoring"
PLG_SYSTEM_MOKOWAAS_FIELDSET_DIAGNOSTICS_DESC="Health check endpoint for external monitoring systems (e.g. Grafana). Exposes system status via a token-authenticated JSON API."
@@ -120,6 +136,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_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_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_DESC="Minimum number of characters required for user passwords."
PLG_SYSTEM_MOKOWAAS_PASSWORD_UPPER_LABEL="Require Uppercase"
@@ -130,9 +153,40 @@ PLG_SYSTEM_MOKOWAAS_UPLOAD_TYPES_DESC="Comma-separated list of allowed file exte
PLG_SYSTEM_MOKOWAAS_UPLOAD_SIZE_LABEL="Max Upload Size (MB)"
PLG_SYSTEM_MOKOWAAS_UPLOAD_SIZE_DESC="Maximum file upload size in megabytes."
; ===== Demo Mode fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_DEMO_LABEL="Demo Mode"
PLG_SYSTEM_MOKOWAAS_FIELDSET_DEMO_DESC="Configure demo site behavior with baseline snapshots and automatic periodic reset. When enabled, a warning banner is shown on the frontend."
PLG_SYSTEM_MOKOWAAS_DEMO_ENABLED_LABEL="Enable Demo Mode"
PLG_SYSTEM_MOKOWAAS_DEMO_ENABLED_DESC="When enabled, shows a warning banner on the frontend and enables snapshot/restore functionality."
PLG_SYSTEM_MOKOWAAS_DEMO_BANNER_MSG_LABEL="Banner Message"
PLG_SYSTEM_MOKOWAAS_DEMO_BANNER_MSG_DESC="Message displayed in the demo warning banner on the frontend."
PLG_SYSTEM_MOKOWAAS_DEMO_BANNER_COLOR_LABEL="Banner Color"
PLG_SYSTEM_MOKOWAAS_DEMO_BANNER_COLOR_DESC="Background color for the demo warning banner."
PLG_SYSTEM_MOKOWAAS_DEMO_COUNTDOWN_LABEL="Show Reset Countdown"
PLG_SYSTEM_MOKOWAAS_DEMO_COUNTDOWN_DESC="Display a countdown timer in the banner showing time until the next scheduled reset."
PLG_SYSTEM_MOKOWAAS_DEMO_SCHEDULE_LABEL="Reset Schedule"
PLG_SYSTEM_MOKOWAAS_DEMO_SCHEDULE_DESC="How often the demo site resets. Select a preset or choose Custom to enter a crontab expression."
PLG_SYSTEM_MOKOWAAS_DEMO_CRON_LABEL="Custom Crontab"
PLG_SYSTEM_MOKOWAAS_DEMO_CRON_DESC="Crontab expression for the reset schedule. Format: minute hour day month weekday (e.g. 0 */6 * * * for every 6 hours)."
PLG_SYSTEM_MOKOWAAS_DEMO_NEXT_RESET_LABEL="Next Scheduled Reset"
PLG_SYSTEM_MOKOWAAS_DEMO_NEXT_RESET_DESC="Calculated automatically from the reset schedule. The banner countdown uses this timestamp."
PLG_SYSTEM_MOKOWAAS_DEMO_TABLES_LABEL="Snapshot Tables"
PLG_SYSTEM_MOKOWAAS_DEMO_TABLES_DESC="Database tables to include in snapshots. One per line, using #__ prefix. These tables will be truncated and restored during a reset."
PLG_SYSTEM_MOKOWAAS_DEMO_MEDIA_LABEL="Include Directories"
PLG_SYSTEM_MOKOWAAS_DEMO_MEDIA_DESC="Select which directories to include in the snapshot. Images contains uploaded media, Media contains extension assets."
PLG_SYSTEM_MOKOWAAS_DEMO_ACTIVE_BASELINE_LABEL="Active Baseline Name"
PLG_SYSTEM_MOKOWAAS_DEMO_ACTIVE_BASELINE_DESC="Name of the baseline snapshot used by admin toggles and scheduled tasks. Alphanumeric, hyphens, and underscores only."
PLG_SYSTEM_MOKOWAAS_DEMO_TAKE_SNAPSHOT_LABEL="Take Snapshot Now"
PLG_SYSTEM_MOKOWAAS_DEMO_TAKE_SNAPSHOT_DESC="Save the current site state as a baseline snapshot. Uses the Active Baseline Name above. Resets to No after execution."
PLG_SYSTEM_MOKOWAAS_DEMO_RESTORE_NOW_LABEL="Restore Baseline Now"
PLG_SYSTEM_MOKOWAAS_DEMO_RESTORE_NOW_DESC="Immediately restore the site to the active baseline snapshot. WARNING: This will overwrite current content. Resets to No after execution."
; ===== Site Aliases fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_ALIASES_LABEL="Site Aliases"
PLG_SYSTEM_MOKOWAAS_FIELDSET_ALIASES_DESC="Configure additional domains that mirror this site. Each alias can have its own offline status, robots directive, and backend redirect behavior."
PLG_SYSTEM_MOKOWAAS_PRIMARY_DOMAIN_LABEL="Primary Domain"
PLG_SYSTEM_MOKOWAAS_PRIMARY_DOMAIN_DESC="The primary domain for this site (e.g. waas.dev.mokoconsulting.tech). Used for backend redirect on alias domains. Do not include https:// prefix."
PLG_SYSTEM_MOKOWAAS_SITE_ALIASES_LABEL="Domain Aliases"
PLG_SYSTEM_MOKOWAAS_SITE_ALIASES_DESC="Add domain aliases that serve as mirrors of this site. Each alias gets its own Grafana monitoring datasource."
PLG_SYSTEM_MOKOWAAS_ALIAS_DOMAIN_LABEL="Domain"
@@ -15,5 +15,5 @@
; Variables: (none)
; -----------------------------------------------------------------------------
PLG_SYSTEM_MOKOWAAS="System - Moko WaaS"
PLG_SYSTEM_MOKOWAAS="System - MokoWaaS"
PLG_SYSTEM_MOKOWAAS_XML_DESCRIPTION="This plugin rebrands the Joomla system interface with MokoWaaS identity. It applies language overrides and ensures consistent branding across the platform."
@@ -0,0 +1 @@
<!DOCTYPE html><title></title>

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Before

Width:  |  Height:  |  Size: 137 KiB

After

Width:  |  Height:  |  Size: 137 KiB

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

@@ -16,7 +16,7 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS
REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.01.11
VERSION: 02.31.00
PATH: /src/mokowaas.xml
BRIEF: Plugin manifest for MokoWaaS system plugin
NOTE: Defines installation metadata, files, and configuration for Joomla
@@ -30,20 +30,17 @@
<license>GNU General Public License version 3 or later; see LICENSE.md</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.01.43</version>
<version>02.31.00</version>
<version>02.31.00</version>
<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>
<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>
<filename plugin="mokowaas">script.php</filename>
<folder>Extension</folder>
<folder>Field</folder>
<folder>Service</folder>
<folder>forms</folder>
<folder>payload</folder>
<folder>services</folder>
@@ -76,92 +73,19 @@
</administration>
<config>
<fields name="params">
<fields name="params"
addfieldprefix="Moko\Plugin\System\MokoWaaS\Field"
>
<fieldset name="basic">
<field
name="enable_branding"
type="radio"
label="PLG_SYSTEM_MOKOWAAS_ENABLE_BRANDING_LABEL"
description="PLG_SYSTEM_MOKOWAAS_ENABLE_BRANDING_DESC"
default="1"
class="btn-group btn-group-yesno"
>
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field
name="brand_name"
type="text"
label="PLG_SYSTEM_MOKOWAAS_BRAND_NAME_LABEL"
description="PLG_SYSTEM_MOKOWAAS_BRAND_NAME_DESC"
default="MokoWaaS"
name="health_api_token"
type="CopyableToken"
label="PLG_SYSTEM_MOKOWAAS_HEALTH_TOKEN_LABEL"
description="PLG_SYSTEM_MOKOWAAS_HEALTH_TOKEN_DESC"
default=""
filter="raw"
readonly="true"
/>
<field
name="company_name"
type="text"
label="PLG_SYSTEM_MOKOWAAS_COMPANY_NAME_LABEL"
description="PLG_SYSTEM_MOKOWAAS_COMPANY_NAME_DESC"
default="Moko Consulting"
/>
<field
name="support_url"
type="url"
label="PLG_SYSTEM_MOKOWAAS_SUPPORT_URL_LABEL"
description="PLG_SYSTEM_MOKOWAAS_SUPPORT_URL_DESC"
default="https://mokoconsulting.tech"
/>
</fieldset>
<fieldset name="waas_access"
label="PLG_SYSTEM_MOKOWAAS_FIELDSET_WAAS_ACCESS_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FIELDSET_WAAS_ACCESS_DESC"
addfieldprefix="Moko\Plugin\System\MokoWaaS\Field"
>
<field
name="enforce_master_user"
type="radio"
label="PLG_SYSTEM_MOKOWAAS_ENFORCE_MASTER_USER_LABEL"
description="PLG_SYSTEM_MOKOWAAS_ENFORCE_MASTER_USER_DESC"
default="1"
class="btn-group btn-group-yesno"
>
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field
name="master_username"
type="text"
label="PLG_SYSTEM_MOKOWAAS_MASTER_USERNAME_LABEL"
description="PLG_SYSTEM_MOKOWAAS_MASTER_USERNAME_DESC"
default="mokoconsulting"
/>
<field
name="master_email"
type="email"
label="PLG_SYSTEM_MOKOWAAS_MASTER_EMAIL_LABEL"
description="PLG_SYSTEM_MOKOWAAS_MASTER_EMAIL_DESC"
default="webmaster@mokoconsulting.tech"
/>
<field
name="emergency_access"
type="radio"
label="PLG_SYSTEM_MOKOWAAS_EMERGENCY_ACCESS_LABEL"
description="PLG_SYSTEM_MOKOWAAS_EMERGENCY_ACCESS_DESC"
default="1"
class="btn-group btn-group-yesno"
>
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field
name="allowed_ips_display"
type="AllowedIps"
label=""
/>
</fieldset>
<fieldset name="maintenance"
label="PLG_SYSTEM_MOKOWAAS_FIELDSET_MAINTENANCE_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FIELDSET_MAINTENANCE_DESC"
>
<field name="dev_mode" type="radio" default="0"
label="PLG_SYSTEM_MOKOWAAS_DEV_MODE_LABEL"
description="PLG_SYSTEM_MOKOWAAS_DEV_MODE_DESC"
@@ -192,39 +116,6 @@
<option value="0">JNO</option>
</field>
</fieldset>
<fieldset name="visual_branding"
label="PLG_SYSTEM_MOKOWAAS_FIELDSET_VISUAL_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FIELDSET_VISUAL_DESC"
>
<field name="branding_note" type="note"
label="PLG_SYSTEM_MOKOWAAS_BRANDING_NOTE_LABEL"
description="PLG_SYSTEM_MOKOWAAS_BRANDING_NOTE_DESC"
class="alert alert-info" />
<field name="color_primary" type="color"
label="PLG_SYSTEM_MOKOWAAS_COLOR_PRIMARY_LABEL"
description="PLG_SYSTEM_MOKOWAAS_COLOR_PRIMARY_DESC"
default="#1a2744" />
<field name="color_sidebar" type="color"
label="PLG_SYSTEM_MOKOWAAS_COLOR_SIDEBAR_LABEL"
description="PLG_SYSTEM_MOKOWAAS_COLOR_SIDEBAR_DESC"
default="#0f1b2d" />
<field name="color_header" type="color"
label="PLG_SYSTEM_MOKOWAAS_COLOR_HEADER_LABEL"
description="PLG_SYSTEM_MOKOWAAS_COLOR_HEADER_DESC"
default="#1a2744" />
<field name="color_link" type="color"
label="PLG_SYSTEM_MOKOWAAS_COLOR_LINK_LABEL"
description="PLG_SYSTEM_MOKOWAAS_COLOR_LINK_DESC"
default="#0051ad" />
<field name="brand_icon" type="text"
label="PLG_SYSTEM_MOKOWAAS_BRAND_ICON_LABEL"
description="PLG_SYSTEM_MOKOWAAS_BRAND_ICON_DESC"
default="" hint="e.g. f6d5 (FontAwesome unicode)" />
<field name="custom_css" type="textarea"
label="PLG_SYSTEM_MOKOWAAS_CUSTOM_CSS_LABEL"
description="PLG_SYSTEM_MOKOWAAS_CUSTOM_CSS_DESC"
rows="10" filter="raw" />
</fieldset>
<fieldset name="tenant_restrictions"
label="PLG_SYSTEM_MOKOWAAS_FIELDSET_TENANT_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FIELDSET_TENANT_DESC"
@@ -236,6 +127,14 @@
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="allow_extension_updates" type="radio" default="1"
label="PLG_SYSTEM_MOKOWAAS_ALLOW_UPDATES_LABEL"
description="PLG_SYSTEM_MOKOWAAS_ALLOW_UPDATES_DESC"
class="btn-group btn-group-yesno"
showon="restrict_installer:1">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="hide_sysinfo" type="radio" default="1"
label="PLG_SYSTEM_MOKOWAAS_HIDE_SYSINFO_LABEL"
description="PLG_SYSTEM_MOKOWAAS_HIDE_SYSINFO_DESC"
@@ -269,40 +168,36 @@
description="PLG_SYSTEM_MOKOWAAS_HIDDEN_MENUS_DESC"
rows="5" filter="raw" />
</fieldset>
<fieldset name="site_aliases"
label="PLG_SYSTEM_MOKOWAAS_FIELDSET_ALIASES_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FIELDSET_ALIASES_DESC"
>
<field
name="site_aliases"
type="subform"
label="PLG_SYSTEM_MOKOWAAS_SITE_ALIASES_LABEL"
description="PLG_SYSTEM_MOKOWAAS_SITE_ALIASES_DESC"
formsource="plugins/system/mokowaas/forms/alias_entry.xml"
multiple="true"
layout="joomla.form.field.subform.repeatable-table"
groupByFieldset="false"
buttons="add,remove,move"
/>
</fieldset>
<fieldset name="diagnostics"
label="PLG_SYSTEM_MOKOWAAS_FIELDSET_DIAGNOSTICS_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FIELDSET_DIAGNOSTICS_DESC"
>
<field
name="health_api_token"
type="text"
label="PLG_SYSTEM_MOKOWAAS_HEALTH_TOKEN_LABEL"
description="PLG_SYSTEM_MOKOWAAS_HEALTH_TOKEN_DESC"
default=""
filter="raw"
readonly="true"
/>
</fieldset>
<fieldset name="security"
<fieldset name="demo_mode"
label="PLG_SYSTEM_MOKOWAAS_FIELDSET_DEMO_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FIELDSET_DEMO_DESC"
addfieldprefix="Moko\Plugin\System\MokoWaaS\Field"
>
<field name="demo_scheduled_task" type="DemoTaskInfo"
label="PLG_SYSTEM_MOKOWAAS_DEMO_TASK_INFO_LABEL"
/>
</fieldset>
<fieldset name="security"
label="PLG_SYSTEM_MOKOWAAS_FIELDSET_SECURITY_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FIELDSET_SECURITY_DESC"
addfieldprefix="Moko\Plugin\System\MokoWaaS\Field"
>
<field
name="emergency_access"
type="radio"
label="PLG_SYSTEM_MOKOWAAS_EMERGENCY_ACCESS_LABEL"
description="PLG_SYSTEM_MOKOWAAS_EMERGENCY_ACCESS_DESC"
default="1"
class="btn-group btn-group-yesno"
>
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field
name="allowed_ips_display"
type="AllowedIps"
label=""
/>
<field name="force_https" type="radio" default="1"
label="PLG_SYSTEM_MOKOWAAS_FORCE_HTTPS_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FORCE_HTTPS_DESC"
@@ -314,6 +209,22 @@
label="PLG_SYSTEM_MOKOWAAS_SESSION_TIMEOUT_LABEL"
description="PLG_SYSTEM_MOKOWAAS_SESSION_TIMEOUT_DESC"
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"
label="PLG_SYSTEM_MOKOWAAS_PASSWORD_LENGTH_LABEL"
description="PLG_SYSTEM_MOKOWAAS_PASSWORD_LENGTH_DESC" />
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More