Compare commits

..

213 Commits

Author SHA1 Message Date
gitea-actions[bot] 4cfde99e7f chore(version): pre-release bump to 01.08.37-dev [skip ci] 2026-06-28 08:37:47 +00:00
gitea-actions[bot] 1e105d6c7b chore(version): auto-bump patch 01.08.36-dev [skip ci] 2026-06-28 08:37:31 +00:00
jmiller 2140c9e07f chore: sync CODE_OF_CONDUCT.md from Template-Joomla
Universal: Auto Version Bump / Version Bump (push) Successful in 13s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 19s
Authored-by: Moko Consulting
2026-06-28 08:36:55 +00:00
gitea-actions[bot] 5cb6dd8008 chore(version): pre-release bump to 01.08.35-dev [skip ci] 2026-06-28 08:36:43 +00:00
gitea-actions[bot] 2264b00828 chore(version): auto-bump patch 01.08.34-dev [skip ci] 2026-06-28 08:36:27 +00:00
jmiller f87086bd0f chore: sync phpstan.neon from Template-Joomla
Universal: Auto Version Bump / Version Bump (push) Successful in 13s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 20s
Authored-by: Moko Consulting
2026-06-28 08:35:55 +00:00
gitea-actions[bot] fc23c771c0 chore(version): pre-release bump to 01.08.33-dev [skip ci] 2026-06-28 08:35:44 +00:00
gitea-actions[bot] 96299a6b9a chore(version): auto-bump patch 01.08.32-dev [skip ci] 2026-06-28 08:35:29 +00:00
jmiller 1961585e83 chore: sync .editorconfig from Template-Joomla
Universal: Auto Version Bump / Version Bump (push) Successful in 11s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 18s
Authored-by: Moko Consulting
2026-06-28 08:35:17 +00:00
gitea-actions[bot] 14f5407820 chore(version): pre-release bump to 01.08.31-dev [skip ci] 2026-06-28 08:31:16 +00:00
gitea-actions[bot] 407b30a437 chore(version): pre-release bump to 01.08.30-dev [skip ci] 2026-06-28 08:30:11 +00:00
jmiller 49041565eb chore: sync version.md from Template-Joomla
Universal: Auto Version Bump / Version Bump (push) Successful in 9s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 11s
Authored-by: Moko Consulting
2026-06-28 08:29:18 +00:00
jmiller 1d1026f7e7 chore: sync security.md from Template-Joomla
Universal: Auto Version Bump / Version Bump (push) Successful in 9s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 13s
Authored-by: Moko Consulting
2026-06-28 08:28:58 +00:00
gitea-actions[bot] 1f7329272d chore(version): pre-release bump to 01.08.29-dev [skip ci] 2026-06-28 08:28:41 +00:00
jmiller 4c855ac7c8 chore: sync rfc.md from Template-Joomla
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Failing after 9s
Authored-by: Moko Consulting
2026-06-28 08:28:19 +00:00
gitea-actions[bot] a350d02d08 chore(version): pre-release bump to 01.08.28-dev [skip ci] 2026-06-28 08:27:56 +00:00
gitea-actions[bot] a860d414bd chore(version): pre-release bump to 01.08.27-dev [skip ci] 2026-06-28 08:27:27 +00:00
jmiller e03c86f2c6 chore: sync question.md from Template-Joomla
Universal: Auto Version Bump / Version Bump (push) Successful in 10s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 14s
Authored-by: Moko Consulting
2026-06-28 08:27:13 +00:00
jmiller 1b0025e55f chore: sync joomla_issue.md from Template-Joomla
Universal: Auto Version Bump / Version Bump (push) Successful in 10s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 12s
Authored-by: Moko Consulting
2026-06-28 08:26:33 +00:00
gitea-actions[bot] ffe599ee92 chore(version): pre-release bump to 01.08.26-dev [skip ci] 2026-06-28 08:26:31 +00:00
gitea-actions[bot] ac56b3a776 chore(version): pre-release bump to 01.08.25-dev [skip ci] 2026-06-28 08:25:52 +00:00
jmiller 95badba96e chore: sync feature_request.md from Template-Joomla
Universal: Auto Version Bump / Version Bump (push) Successful in 9s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 16s
Authored-by: Moko Consulting
2026-06-28 08:25:25 +00:00
gitea-actions[bot] de66983cda chore(version): pre-release bump to 01.08.24-dev [skip ci]
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 14s
2026-06-28 08:25:23 +00:00
jmiller 89a59f8a8e chore: sync documentation.md from Template-Joomla
Universal: Auto Version Bump / Version Bump (push) Successful in 10s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 14s
Authored-by: Moko Consulting
2026-06-28 08:24:42 +00:00
jmiller 34367ae93c chore: sync config.yml from Template-Joomla
Universal: Auto Version Bump / Version Bump (push) Successful in 10s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 12s
Authored-by: Moko Consulting
2026-06-28 08:24:12 +00:00
gitea-actions[bot] b2d4071193 chore(version): auto-bump patch 01.08.23-dev [skip ci] 2026-06-28 08:24:05 +00:00
jmiller 3bc5678768 chore: sync bug_report.md from Template-Joomla
Universal: Auto Version Bump / Version Bump (push) Successful in 9s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Failing after 10s
Authored-by: Moko Consulting
2026-06-28 08:23:47 +00:00
gitea-actions[bot] ca0cfd9a6d chore(version): pre-release bump to 01.08.22-dev [skip ci] 2026-06-28 08:23:32 +00:00
gitea-actions[bot] 9cc4b90b78 chore(version): auto-bump patch 01.08.21-dev [skip ci] 2026-06-28 08:23:24 +00:00
jmiller 4ebb9e30d6 chore: sync adr.md from Template-Joomla
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 9s
Authored-by: Moko Consulting
2026-06-28 08:23:11 +00:00
gitea-actions[bot] 0f164b607c chore(version): pre-release bump to 01.08.20-dev [skip ci] 2026-06-28 08:08:25 +00:00
gitea-actions[bot] 6762764006 chore(version): pre-release bump to 01.08.19-dev [skip ci] 2026-06-28 08:05:57 +00:00
jmiller efdcaa712f chore: sync GOVERNANCE.md from Template-Joomla
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 13s
Authored-by: Moko Consulting
2026-06-28 07:59:33 +00:00
jmiller d3581564cf chore: sync ci-issue-reporter.yml from Template-Joomla
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 8s
Authored-by: Moko Consulting
2026-06-28 07:47:00 +00:00
jmiller a29d8f4e12 chore: add SECURITY.md from Template-Joomla
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Has been cancelled
2026-06-28 07:25:42 +00:00
gitea-actions[bot] 109ca703ef chore(version): pre-release bump to 01.08.17-dev [skip ci] 2026-06-28 01:43:20 +00:00
gitea-actions[bot] 794746e20d chore(version): pre-release bump to 01.08.16-dev [skip ci] 2026-06-28 01:43:02 +00:00
jmiller 85848c2d6c Merge pull request 'feat: add Threads carousel, polls, and spoiler support' (#192) from feature/153-threads-enhancements into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 22s
2026-06-28 01:42:51 +00:00
jmiller 86d4681fcd Merge pull request 'feat: add Instagram carousel, Reels, and Stories support' (#191) from feature/151-instagram-enhancements into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 18s
2026-06-28 01:42:44 +00:00
gitea-actions[bot] 0a14a29ac6 chore(version): auto-bump patch 01.08.15-dev [skip ci]
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
2026-06-28 01:27:20 +00:00
gitea-actions[bot] df07b4b672 chore(version): auto-bump patch 01.08.15-dev [skip ci]
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
2026-06-28 01:27:10 +00:00
jmiller 7bd151ad62 feat: add Threads carousel, polls, and spoiler support (#153)
Universal: Auto Version Bump / Version Bump (push) Successful in 10s
Authored-by: Moko Consulting
2026-06-27 20:25:33 -05:00
jmiller ddc867ad06 feat: add Instagram carousel, Reels, and Stories support (#151)
Universal: Auto Version Bump / Version Bump (push) Successful in 10s
Authored-by: Moko Consulting
2026-06-27 20:25:26 -05:00
gitea-actions[bot] a111f5b5e9 chore(version): pre-release bump to 01.08.14-dev [skip ci] 2026-06-28 00:38:06 +00:00
jmiller 1897805483 Merge pull request 'fix: update package description to list all 38 platforms' (#190) from fix/package-description into dev
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 19s
2026-06-28 00:37:13 +00:00
gitea-actions[bot] 8919db6fc3 chore(version): pre-release bump to 01.08.13-dev [skip ci]
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Failing after 3s
2026-06-28 00:27:20 +00:00
jmiller d69b26af51 fix: update package description to list all 38 platforms and key features
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Universal: PR Check / Secret Scan (pull_request) Successful in 6s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 12s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 36s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Authored-by: Moko Consulting
2026-06-27 19:26:44 -05:00
gitea-actions[bot] a8dae85f42 chore(version): pre-release bump to 01.08.12-dev [skip ci] 2026-06-27 20:34:25 +00:00
jmiller d3bc62f810 Merge pull request 'feat: implement Nostr NIP-01 WebSocket relay publishing' (#189) from feature/129-nostr-implementation into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 18s
2026-06-27 20:33:36 +00:00
gitea-actions[bot] 13683adfba chore(version): auto-bump patch 01.08.11-dev [skip ci]
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Failing after 3s
2026-06-27 20:22:09 +00:00
jmiller e183b62aba feat: implement Nostr NIP-01 WebSocket relay publishing (#129)
Universal: Auto Version Bump / Version Bump (push) Successful in 9s
- BIP-340 Schnorr signatures over secp256k1 (pure PHP, requires ext-gmp)
- Kind-1 text note events with SHA-256 event ID and tagged hashes
- Raw WebSocket client via stream_socket_client (zero external deps)
- Multi-relay failover: tries each relay until one accepts
- Public key derivation from private key for account display
- Validates 64-char hex private key format and wss:// relay URLs

Authored-by: Moko Consulting
2026-06-27 15:21:45 -05:00
gitea-actions[bot] ce9d72b50d chore(version): pre-release bump to 01.08.10-dev [skip ci] 2026-06-27 00:11:16 +00:00
gitea-actions[bot] 92358a673b chore(version): pre-release bump to 01.08.09-dev [skip ci] 2026-06-25 19:46:34 +00:00
gitea-actions[bot] 99308cd7a4 chore(version): pre-release bump to 01.08.08-dev [skip ci] 2026-06-25 17:09:50 +00:00
jmiller 561ba24090 Merge pull request 'fix: use typed Joomla 6 event parameters, remove legacy fallbacks' (#184) from fix/joomla6-event-handlers into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 11s
2026-06-25 17:09:13 +00:00
jmiller 3e1cb9a500 fix: use typed Joomla 6 event parameters, remove legacy fallbacks
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Failing after 4s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Universal: PR Check / Secret Scan (pull_request) Successful in 5s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 27s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Remove func_get_arg() legacy fallbacks from onContentBeforeDisplay,
onContentAfterSave, and onContentChangeState. All methods now use
typed event parameters (Joomla 6 only).
2026-06-25 12:08:54 -05:00
gitea-actions[bot] 5ae8e3e001 chore(version): pre-release bump to 01.08.07-dev [skip ci] 2026-06-25 16:27:32 +00:00
jmiller faea3637e0 Merge pull request 'fix: add SQL update file to match manifest version' (#183) from fix/schema-version-file into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 11s
fix: add SQL update file to match manifest version
2026-06-25 16:26:37 +00:00
jmiller 79eaa5217d fix: add SQL update file to match manifest version
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Universal: PR Check / Secret Scan (pull_request) Successful in 6s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 12s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Failing after 2s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 16s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Joomla's Database view requires a SQL update file matching the manifest
version. Missing file causes persistent schema version mismatch warning.
2026-06-25 11:25:24 -05:00
gitea-actions[bot] 0e0891f1a8 chore(version): pre-release bump to 01.08.05-dev [skip ci] 2026-06-25 16:15:51 +00:00
gitea-actions[bot] 33aaf666ae chore(version): pre-release bump to 01.08.04-dev [skip ci] 2026-06-25 16:15:19 +00:00
gitea-actions[bot] a634938799 chore(version): auto-bump patch 01.08.03-dev [skip ci] 2026-06-25 16:15:03 +00:00
jmiller 14ff4ab2f1 chore: update changelog with Joomla 6 webservices fix
Universal: Auto Version Bump / Version Bump (push) Successful in 17s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 16s
2026-06-25 11:13:41 -05:00
gitea-actions[bot] b3de21e7d1 chore(version): pre-release bump to 01.08.02-dev [skip ci] 2026-06-25 14:54:01 +00:00
gitea-actions[bot] 72a373b17c chore(version): auto-bump patch 01.07.04-dev [skip ci] 2026-06-25 14:53:44 +00:00
jmiller bc290f3bed fix: Joomla 6 compat for webservices API route event
Universal: Auto Version Bump / Version Bump (push) Successful in 10s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 22s
Joomla 6 passes a BeforeApiRouteEvent object instead of the router
directly. Extract the router from the event for Joomla 5/6 dual compat.
2026-06-25 09:53:29 -05:00
gitea-actions[bot] a4704ad267 chore(version): pre-release bump to 01.07.03-dev [skip ci] 2026-06-23 22:53:09 +00:00
gitea-actions[bot] d1762ad5df chore(version): auto-bump patch 01.07.02-dev [skip ci] 2026-06-23 22:52:52 +00:00
Jonathan Miller df1467c518 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteCross into dev
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 3s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 7s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Universal: PR Check / Secret Scan (pull_request) Successful in 10s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 16s
Universal: Auto Version Bump / Version Bump (push) Successful in 17s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 17s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 38s
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
Universal: PR Check / Report Issues (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 / Report Issues (pull_request) Has been cancelled
2026-06-23 17:52:30 -05:00
Jonathan Miller 7cdd97ca59 chore: re-remove deploy-manual.yml synced from template 2026-06-23 17:51:24 -05:00
Jonathan Miller 5b36d10b04 Merge remote-tracking branch 'origin/main' into dev 2026-06-23 17:34:43 -05:00
gitea-actions[bot] 56699fdd4d chore(version): pre-release bump to 01.07.01-dev [skip ci] 2026-06-23 22:27:51 +00:00
gitea-actions[bot] fcf1cc41c8 chore(version): auto-bump patch 01.06.10-dev [skip ci] 2026-06-23 22:27:41 +00:00
Jonathan Miller b8640ccb1d fix: content plugin func_get_arg crash on non-article saves
Universal: Auto Version Bump / Version Bump (push) Successful in 10s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 12s
onContentAfterSave and onContentChangeState fire for ALL content types
in Joomla (articles, update sites, installer, etc). The legacy fallback
used func_get_arg() without checking argument count, crashing when
com_installer saved update sites (only 1 arg, not 3).

Fix: check context early in the event object path, and guard legacy
path with func_num_args() >= N before calling func_get_arg().
2026-06-23 17:25:59 -05:00
jmiller 4b51e2dd9a chore: sync workflow-sync-trigger.yml from Template-Generic [skip ci] 2026-06-23 21:52:31 +00:00
jmiller e068e14004 chore: sync pre-release.yml from Template-Generic [skip ci] 2026-06-23 21:52:31 +00:00
jmiller 941fd4c6cd chore: sync issue-branch.yml from Template-Generic [skip ci] 2026-06-23 21:52:30 +00:00
jmiller f2021d478e chore: sync deploy-manual.yml from Template-Generic [skip ci] 2026-06-23 21:52:29 +00:00
gitea-actions[bot] 900ceb2bb5 chore: promote changelog [Unreleased] → [01.07.00] 2026-06-23 21:51:45 +00:00
gitea-actions[bot] 9b498e6786 chore(release): build 01.07.00 [skip ci] 2026-06-23 21:51:42 +00:00
gitea-actions[bot] ca06298e64 chore(version): pre-release bump to 01.06.09-dev [skip ci] 2026-06-23 21:51:30 +00:00
jmiller dc8d7d59d4 Release v01.06.00: Full ACL system, workflow cleanup, housekeeping 2026-06-23 21:51:24 +00:00
gitea-actions[bot] 55d2123c33 chore(version): auto-bump patch 01.06.08-dev [skip ci]
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
RC Revert / Rename rc/ back to dev/ (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 16s
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Failing after 3m7s
2026-06-23 21:51:18 +00:00
Jonathan Miller 274c1f34af Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteCross into dev
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (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
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Universal: PR Check / Secret Scan (pull_request) Successful in 7s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 11s
Universal: Auto Version Bump / Version Bump (push) Successful in 12s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 14s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 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
Universal: PR Check / Report Issues (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 / Report Issues (pull_request) Has been cancelled
2026-06-23 16:51:01 -05:00
Jonathan Miller 75c878507e Merge remote-tracking branch 'origin/main' into dev
# Conflicts:
#	.mokogitea/manifest.xml
2026-06-23 16:50:28 -05:00
gitea-actions[bot] f1ea8ead74 chore(version): pre-release bump to 01.06.07-dev [skip ci] 2026-06-23 19:22:40 +00:00
gitea-actions[bot] 23de84610e chore(version): auto-bump patch 01.06.06-dev [skip ci] 2026-06-23 19:22:17 +00:00
Jonathan Miller 0cb24b4759 feat: full ACL system with 12 granular permissions across entire codebase
Universal: Auto Version Bump / Version Bump (push) Successful in 17s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 24s
access.xml: 12 component-specific actions added:
- mokosuitecross.crosspost (auto cross-post on publish)
- mokosuitecross.crosspost.manual (manually create posts)
- mokosuitecross.delete.remote (delete from remote platforms)
- mokosuitecross.services.manage (create/edit/delete services)
- mokosuitecross.services.credentials (view decrypted credentials)
- mokosuitecross.templates.manage (create/edit/delete templates)
- mokosuitecross.logs.view (view activity logs)
- mokosuitecross.logs.purge (purge old logs)
- mokosuitecross.queue.manage (manage post queue)
- mokosuitecross.queue.export (export posts as CSV)
- mokosuitecross.dispatch (trigger REST API dispatch)
- mokosuitecross.migrate (run Perfect Publisher migration)

config.xml: permissions fieldset with rules field for admin UI.

Enforcement:
- DisplayController: core.manage gate on all views
- ServicesController: publish/delete ACL checks
- TemplatesController: publish/delete ACL checks
- PostsController: queue.export permission
- ServiceController: services.manage for test connection
- DispatchController: dispatch permission for REST API
- All list views: preferences button gated by core.admin
- All edit views: save/apply buttons gated by section permission
- MokoSuiteCrossHelper::getActions() centralizes ACL lookups
2026-06-23 14:21:55 -05:00
gitea-actions[bot] 7fa97231a1 chore(version): pre-release bump to 01.06.05-dev [skip ci] 2026-06-23 18:09:37 +00:00
gitea-actions[bot] 2291db32c5 chore(version): auto-bump patch 01.06.04-dev [skip ci] 2026-06-23 18:09:18 +00:00
Jonathan Miller 491bd3b858 chore: remove security-audit.yml -- handled by MokoGitea
Universal: Auto Version Bump / Version Bump (push) Successful in 30s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 18s
2026-06-23 13:02:32 -05:00
gitea-actions[bot] 064d5e3ab1 chore(version): pre-release bump to 01.06.03-dev [skip ci] 2026-06-23 17:57:35 +00:00
gitea-actions[bot] c512829cd4 chore(version): auto-bump patch 01.06.02-dev [skip ci] 2026-06-23 17:57:19 +00:00
Jonathan Miller 69c728cd5a chore: remove composer-publish, update-server, deploy-manual workflows
Universal: Auto Version Bump / Version Bump (push) Successful in 21s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 15s
2026-06-23 12:56:40 -05:00
jmiller fca6d4f25f chore: sync pre-release.yml from Template-Joomla [skip ci] 2026-06-23 17:43:02 +00:00
jmiller b668e1d4ed chore: remove deprecated .mokogitea/workflows/composer-publish.yml [skip ci] 2026-06-23 17:37:02 +00:00
jmiller 5b760a1b74 chore: remove deprecated .mokogitea/workflows/update-server.yml [skip ci] 2026-06-23 17:36:58 +00:00
jmiller 1f8dccf898 chore: remove deprecated .mokogitea/workflows/deploy-manual.yml [skip ci] 2026-06-23 17:36:55 +00:00
gitea-actions[bot] b9d8eb3950 chore(version): pre-release bump to 01.06.01-dev [skip ci]
Publish to Composer / Publish Package (release) Failing after 8s
2026-06-23 17:35:55 +00:00
gitea-actions[bot] 053fe2d52c chore(version): auto-bump patch 01.05.04-dev [skip ci] 2026-06-23 17:35:28 +00:00
Jonathan Miller b3846fa633 chore: remove .mokogitea/manifest.xml -- metadata via API
Universal: Auto Version Bump / Version Bump (push) Successful in 19s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 36s
2026-06-23 12:34:51 -05:00
jmiller a8ef4cfb77 chore: sync workflow-sync-trigger.yml from Template-Joomla [skip ci] 2026-06-23 17:31:49 +00:00
jmiller 58317cd205 chore: sync version-set.yml from Template-Joomla [skip ci] 2026-06-23 17:11:27 +00:00
jmiller 642e2bffe7 chore: sync issue-branch.yml from Template-Joomla [skip ci] 2026-06-23 17:11:25 +00:00
jmiller d6d423b946 chore: sync ci-joomla.yml from Template-Joomla [skip ci] 2026-06-23 17:11:24 +00:00
gitea-actions[bot] e92a963088 chore: promote changelog [Unreleased] → [01.06.00] 2026-06-23 17:09:39 +00:00
gitea-actions[bot] f3a8246e34 chore(release): build 01.06.00 [skip ci]
Publish to Composer / Publish Package (release) Failing after 5s
2026-06-23 17:09:35 +00:00
gitea-actions[bot] f65d261598 chore(version): pre-release bump to 01.05.03-dev [skip ci]
Publish to Composer / Publish Package (release) Failing after 6s
2026-06-23 17:09:09 +00:00
jmiller c9f50e452b Fix: duplicate license warning, wiki folder cleanup 2026-06-23 17:07:59 +00:00
Jonathan Miller c820d015e7 Merge remote-tracking branch 'origin/main' into dev
Universal: Auto Version Bump / Version Bump (push) Successful in 11s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 27s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 23s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 3s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 7s
Generic: Repo Health / Access control (pull_request) Successful in 3s
Universal: PR Check / Secret Scan (pull_request) Successful in 8s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 16s
Universal: PR Check / Validate PR (pull_request) Failing after 45s
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Failing after 3m46s
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
Universal: PR Check / Report Issues (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 / Report Issues (pull_request) Has been cancelled
2026-06-23 12:07:01 -05:00
gitea-actions[bot] 78cbd1f370 chore(version): pre-release bump to 01.05.02-dev [skip ci]
Publish to Composer / Publish Package (release) Failing after 38s
2026-06-23 17:01:51 +00:00
Jonathan Miller 70d2bab52d fix: remove duplicate license warning from system plugin
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 18s
The license key warning was firing twice:
1. Package install script (source/script.php) - has direct "Enter Key" button
2. System plugin (onAfterRoute) - every admin session, less actionable

Removed #2 entirely. The install script version is better UX (button links
directly to update site edit page) and only fires during install/update.
Also eliminates the uninstall bug where the warning fired during removal.
2026-06-23 11:59:56 -05:00
gitea-actions[bot] 166a6366f8 chore(version): pre-release bump to 01.05.01-dev [skip ci]
Publish to Composer / Publish Package (release) Failing after 33s
2026-06-23 16:45:37 +00:00
Jonathan Miller ac8a64c4c1 fix: license key warning no longer shows during uninstall
Universal: Auto Version Bump / Version Bump (push) Successful in 12s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 12s
Check if com_mokosuitecross is still in #__extensions before warning
about missing license key. Prevents the warning from firing during
package uninstall when the update site row is already deleted.
2026-06-23 11:45:05 -05:00
Jonathan Miller 2ee8a5e286 chore: remove wiki/ folder -- content migrated to Gitea wiki feature 2026-06-23 11:45:04 -05:00
jmiller 9d2620faea chore: sync issue-branch.yml from Template-Joomla [skip ci] 2026-06-23 16:23:09 +00:00
gitea-actions[bot] c79e8bed73 chore: promote changelog [Unreleased] → [01.05.00] 2026-06-23 16:08:01 +00:00
gitea-actions[bot] 4ddf02c7af chore(release): build 01.05.00 [skip ci]
Publish to Composer / Publish Package (release) Failing after 41s
2026-06-23 16:07:52 +00:00
jmiller 0fb95ced3f Release v01.05.00: Instagram, YouTube, Share Content, delete, templates, competitive roadmap 2026-06-23 16:07:34 +00:00
gitea-actions[bot] 62a8e9bd99 chore(version): pre-release bump to 01.04.12-dev [skip ci]
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Failing after 3s
Publish to Composer / Publish Package (release) Failing after 35s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 32s
2026-06-23 16:07:11 +00:00
Jonathan Miller de0b588be0 fix: resolve broken namespace placeholders in 7 plugin XML manifests
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 7s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 4s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Secret Scan (pull_request) Successful in 7s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Successful in 12s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 24s
Universal: PR Check / Validate PR (pull_request) Failing after 48s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 49s
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
Universal: PR Check / Report Issues (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 / Report Issues (pull_request) Has been cancelled
2026-06-23 11:06:34 -05:00
gitea-actions[bot] 4650f9ba46 chore(version): pre-release bump to 01.04.11-dev [skip ci]
Publish to Composer / Publish Package (release) Failing after 5s
2026-06-23 15:46:32 +00:00
Jonathan Miller be58716391 feat: ntfy default server ntfy.mokoconsulting.tech with config params
Universal: Auto Version Bump / Version Bump (push) Successful in 10s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 12s
- Default server URL changed from ntfy.sh to ntfy.mokoconsulting.tech
- Added plugin config fieldset with default_server_url and default_topic
- Server URL reads from plugin params, overridable per-service in credentials
- Updated language strings
2026-06-23 10:46:10 -05:00
gitea-actions[bot] 03870dce33 chore(version): pre-release bump to 01.04.10-dev [skip ci]
Publish to Composer / Publish Package (release) Failing after 34s
2026-06-23 15:38:31 +00:00
Jonathan Miller 5fee5d7810 feat: Mastodon polls/visibility/scheduling + Bluesky threads (closes #152, #158)
Universal: Auto Version Bump / Version Bump (push) Successful in 12s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 12s
Mastodon (#152):
- Visibility levels (public/unlisted/private/direct) from params or config
- Content warnings via spoiler_text param
- Scheduled posts via scheduled_at param
- Poll creation (options, expires_in, multiple) mutually exclusive with media
- Language tag support
- Fixed broken ${CLASS_NAME} namespace in mastodon.xml

Bluesky (#158):
- Auto-thread: split long messages at sentence boundaries into reply chains
- External link card embed (app.bsky.embed.external) with article title/description
- Link card attached to last post in thread
- Dispatcher now passes article_title to service plugins

Closes #152, closes #158
2026-06-23 10:38:04 -05:00
gitea-actions[bot] c13c2a372e chore(version): pre-release bump to 01.04.09-dev [skip ci]
Publish to Composer / Publish Package (release) Failing after 7s
2026-06-23 14:08:10 +00:00
Jonathan Miller 09074e3c00 docs: update README with 38 platforms, new features, Instagram + YouTube
Universal: Auto Version Bump / Version Bump (push) Successful in 12s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 22s
2026-06-23 09:07:11 -05:00
gitea-actions[bot] 9bfbf36090 chore(version): pre-release bump to 01.04.08-dev [skip ci]
Publish to Composer / Publish Package (release) Failing after 3s
2026-06-23 13:48:28 +00:00
Jonathan Miller 7e5ff12d03 feat: UTM auto-tagging and caption rotation (closes #154, closes #155)
Universal: Auto Version Bump / Version Bump (push) Successful in 7s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 9s
UTM tracking (#154):
- New config fieldset with utm_source, utm_medium, utm_campaign, utm_content
- {platform} token in UTM values auto-replaced with service type
- {url} gets UTM params appended when enabled
- {url_raw} placeholder for clean URLs without UTM

Caption rotation (#155):
- {random:option1|option2|option3} placeholder in templates
- Picks one option at random per post render
- Great for evergreen re-shares to vary messaging

Closes #154, closes #155
2026-06-23 08:48:08 -05:00
gitea-actions[bot] 42f7a09bb3 chore(version): pre-release bump to 01.04.07-dev [skip ci]
Publish to Composer / Publish Package (release) Failing after 6s
2026-06-23 13:25:07 +00:00
Jonathan Miller 6ad536c0ef chore: remove Makefile -- builds handled by CI
Universal: Auto Version Bump / Version Bump (push) Successful in 14s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 21s
2026-06-23 08:24:33 -05:00
gitea-actions[bot] eb1b112a93 chore(version): pre-release bump to 01.04.06-dev [skip ci]
Publish to Composer / Publish Package (release) Failing after 37s
2026-06-23 13:21:41 +00:00
Jonathan Miller 4918879eec feat: delete/unpublish from remote platforms (closes #131)
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 18s
New MokoSuiteCrossDeleteInterface (separate from main interface to avoid
breaking all 38 plugins). Plugins that support deletion implement both.

deletePost() implemented for 7 platforms:
- Twitter: DELETE /2/tweets/{id} with OAuth 1.0a
- Mastodon: DELETE /api/v1/statuses/{id}
- Bluesky: com.atproto.repo.deleteRecord
- Facebook: DELETE /{post_id} via Graph API
- LinkedIn: DELETE /v2/ugcPosts/{urn}
- Telegram: POST /deleteMessage
- Discord: DELETE webhook /messages/{id}

Infrastructure:
- CrossPostDispatcher::deleteFromPlatforms() finds posted entries and
  calls deletePost() on plugins that implement the delete interface
- Content plugin hooks onContentChangeState for unpublish/trash
- New component config: 'Delete from Platforms on Unpublish'
- Post status 'deleted' added to schema

Closes #131
2026-06-23 08:21:18 -05:00
gitea-actions[bot] add973771b chore(version): pre-release bump to 01.04.05-dev [skip ci]
Publish to Composer / Publish Package (release) Failing after 7s
2026-06-23 13:07:08 +00:00
Jonathan Miller 5753c307c6 feat: Mailchimp template support + responsive email wrapper (closes #142)
Universal: Auto Version Bump / Version Bump (push) Successful in 14s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 19s
- Add template_id and template_section config fields
- When template_id set, inject content into Mailchimp template sections
- When empty, wrap HTML in responsive email skeleton (600px table layout)
- Fix broken ${CLASS_NAME} namespace placeholder in mailchimp.xml
- New language strings for template fieldset

Closes #142
2026-06-23 08:06:34 -05:00
gitea-actions[bot] bfe4432c78 chore(version): pre-release bump to 01.04.04-dev [skip ci]
Publish to Composer / Publish Package (release) Failing after 8s
2026-06-23 12:58:31 +00:00
Jonathan Miller 3c1f3a2421 feat: add per-article Share Content panel with platform-specific placeholders
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 13s
New article editor fieldset 'Share Content' with fields:
- Social Media Text ({social}) - Facebook, LinkedIn, Threads, Mastodon
- Short Text ({short}) - Twitter/X (280), Bluesky (300)
- Chat Text ({chat}) - Telegram, Discord, Slack, Teams
- Email Subject ({email_subject}) + Email Body ({email_body}) - Mailchimp, SendGrid, Brevo
- Share Image picker (intro/fulltext/custom/none)

All placeholders fall back gracefully to introtext/title if empty.
Default templates updated to use platform-specific placeholders.
2026-06-23 07:58:08 -05:00
gitea-actions[bot] 9017b06c7d chore(version): pre-release bump to 01.04.03-dev [skip ci]
Publish to Composer / Publish Package (release) Failing after 26s
2026-06-23 12:39:07 +00:00
Jonathan Miller 1cb5c77bec feat: add Instagram + YouTube plugins, re-apply deep scan fixes (#140, #141)
Universal: Auto Version Bump / Version Bump (push) Successful in 12s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 11s
New plugins:
- plg_mokosuitecross_instagram: Meta Content Publishing API (2-step flow)
- plg_mokosuitecross_youtube: YouTube Data API v3 channel bulletins

Bug fixes (re-applied after rebase loss):
- ConvertKit/Brevo/ConstantContact: duplicate curl_setopt_array removed
- Mailchimp: campaign creation accepts 2xx range (not just 200)
- Medium: getUserId() returns '' on error (not array)
- Bluesky: sha256 instead of md5 for cache key
- ServiceController: generic error message instead of exception details

Closes #140, closes #141
2026-06-23 07:38:41 -05:00
jmiller c2b88e9a94 chore: sync auto-release.yml from Template-Joomla [skip ci] 2026-06-22 00:35:32 +00:00
jmiller 845ed4b53d chore: remove unused Makefile - builds handled by CI auto-release 2026-06-21 23:55:27 +00:00
gitea-actions[bot] dc5feaa9aa chore(version): pre-release bump to 01.04.02-dev [skip ci]
Publish to Composer / Publish Package (release) Failing after 48s
2026-06-21 23:40:48 +00:00
gitea-actions[bot] 7281cd1500 chore: promote changelog [Unreleased] → [01.04.01]
Universal: Auto Version Bump / Version Bump (push) Successful in 11s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 24s
2026-06-21 23:25:06 +00:00
gitea-actions[bot] a1b2bf40ce chore(release): build 01.04.01 [skip ci]
Publish to Composer / Publish Package (release) Failing after 5s
2026-06-21 23:25:01 +00:00
jmiller 854dbc6350 chore: sync issue-branch.yml from Template-Joomla [skip ci] 2026-06-21 23:24:00 +00:00
jmiller 1bcbe800e9 Merge pull request 'chore: remove automation directory' (#148) from fix/remove-automation into main 2026-06-21 23:10:38 +00:00
gitea-actions[bot] c8918df03e chore(version): pre-release bump to 01.04.01-dev [skip ci]
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 25s
Publish to Composer / Publish Package (release) Failing after 49s
Branch Cleanup / Delete merged branch (pull_request) Failing after 1s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Failing after 4s
2026-06-21 23:09:49 +00:00
Jonathan Miller 54236c0d73 chore: remove automation directory
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 18s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 11s
Universal: PR Check / Branch Policy (pull_request) Failing after 1s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 4s
Universal: PR Check / Secret Scan (pull_request) Successful in 8s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Validate PR (pull_request) Failing after 16s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 50s
Universal: Build & Release / Promote to RC (pull_request) Failing after 13s
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
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
Universal: PR Check / Report Issues (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 / Report Issues (pull_request) Has been cancelled
2026-06-21 18:03:44 -05:00
jmiller 33ce9784cc chore: sync repo-health.yml from Template-Joomla [skip ci] 2026-06-21 22:55:54 +00:00
jmiller 582a16e132 chore: sync pre-release.yml from Template-Joomla [skip ci] 2026-06-21 22:55:53 +00:00
jmiller e97388c119 chore: sync pr-check.yml from Template-Joomla [skip ci] 2026-06-21 22:55:52 +00:00
jmiller 954cdaa2ae chore: sync issue-branch.yml from Template-Joomla [skip ci] 2026-06-21 22:55:51 +00:00
jmiller c60be2bf3c chore: sync auto-release.yml from Template-Joomla [skip ci] 2026-06-21 22:55:50 +00:00
gitea-actions[bot] cec436f90e chore(release): build 01.04.00 [skip ci]
Publish to Composer / Publish Package (release) Failing after 4s
2026-06-21 22:48:33 +00:00
jmiller a4e39df6ed Fix: add missing system plugins to package manifest, remove old src/ (#137, #145)
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-21 22:48:16 +00:00
gitea-actions[bot] 70dbb65173 chore(version): auto-bump 01.03.01-dev [skip ci]
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Failing after 4s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 17s
2026-06-21 22:46:05 +00:00
Jonathan Miller 6a00d7ddf9 fix: add missing events/gallery system plugins to package manifest (#137)
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 9s
Update Server / Update Server (push) Successful in 11s
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Added plg_system_mokosuitecross_events and plg_system_mokosuitecross_gallery
to pkg_mokosuitecross.xml. These content source plugins hook into Joomla
system events to cross-post calendar events and gallery images but were
not being installed with the package.

Also removed the old src/ directory (pre-rename mokojoomcross cruft).

Closes #137
2026-06-21 17:45:31 -05:00
gitea-actions[bot] 78c7b99c6a chore(release): build 01.03.00 [skip ci]
Publish to Composer / Publish Package (release) Failing after 55s
2026-06-21 22:26:57 +00:00
jmiller 6d56949452 Release 01.02.00: MokoSuiteCross rebrand, bug fixes, infrastructure (#144)
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-21 22:24:22 +00:00
gitea-actions[bot] 137b2556ac chore(release): build 01.02.00-rc [skip ci]
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Failing after 1s
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Failing after 5s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 22s
2026-06-21 17:23:04 -05:00
Jonathan Miller 9b711d2309 docs: update CHANGELOG with PR workflow check, fix duplicate header 2026-06-21 17:23:04 -05:00
Jonathan Miller 122c7b630a feat: Telegram @mokosuite_bot default, wiki folders, README/CHANGELOG update
- Telegram: updated default bot from @MokoWaaSBot to @mokosuite_bot
- Telegram: embedded obfuscated bot token in plugin PHP (XOR + base64)
- Telegram: added <config> section to plugin XML for parse_mode/preview
- Telegram: removed bot token from admin-visible plugin params
- Branding: replaced all MokoWaaS references with MokoSuite
- Wiki: reorganized into getting-started/, user-guide/, services/, developer/
- README: updated with all 36 service plugins and current features
- CHANGELOG: added entries for recent fixes and changes
2026-06-21 17:23:03 -05:00
gitea-actions[bot] 8ab62abf29 chore(version): auto-bump 01.01.02-dev [skip ci] 2026-06-21 17:23:03 -05:00
Jonathan Miller 27505f7501 fix: rename all MOKOJOOMCROSS language keys and events to MOKOSUITECROSS (#128, #138)
Completes the MokoJoomCross → MokoSuiteCross rebrand across all language
string keys, Joomla event names, documentation, and wiki pages.

- 1,151 language key references renamed (COM_, PLG_, PKG_ prefixes)
- Event names renamed (onMokoJoomCross* → onMokoSuiteCross*)
- CLAUDE.md, CHANGELOG.md, wiki docs updated
- Zero mokojoomcross references remaining in codebase

Closes #128, closes #138
2026-06-21 17:23:02 -05:00
gitea-actions[bot] 65bba1f561 chore(version): auto-bump patch 01.01.01-dev [skip ci] 2026-06-21 17:22:32 -05:00
Jonathan Miller 28db9a67b6 fix: remove duplicate curl_setopt_array calls in 4 service plugins (#139)
SendGrid and Reddit had a second curl_setopt_array that referenced an
undefined $token variable, silently breaking auth. TikTok and Pinterest
had identical duplicates (no variable bug but dead code).

Removes the duplicate block from each plugin's publish() method.
2026-06-21 17:22:31 -05:00
jmiller b9b0c88ad5 chore: sync auto-release.yml from Template-Generic [skip ci] 2026-06-21 22:03:18 +00:00
jmiller 370fa86f59 chore: sync pre-release.yml from Template-Generic [skip ci] 2026-06-21 16:05:59 +00:00
jmiller b6bed1e6df chore: sync composer-publish.yml from Template-Generic [skip ci] 2026-06-21 06:35:21 +00:00
jmiller acf599b25e chore: sync workflow-sync-trigger.yml from Template-Generic [skip ci] 2026-06-21 01:29:14 +00:00
jmiller a1dd54db72 chore: sync auto-release.yml from Template-Generic [skip ci] 2026-06-21 01:29:12 +00:00
jmiller 3403785e1f ci: sync rc-revert.yml from Template-Joomla [skip ci] 2026-06-21 00:15:06 +00:00
jmiller 7d1f30aaaa ci: sync issue-branch.yml from Template-Joomla [skip ci] 2026-06-21 00:14:37 +00:00
jmiller 282fe5fce1 ci: sync ci-joomla.yml from Template-Joomla [skip ci] 2026-06-21 00:14:12 +00:00
jmiller 1430b18583 chore: sync pr-check.yml from Template-Generic [skip ci] 2026-06-20 23:46:57 +00:00
jmiller 54bcd044be chore: sync gitleaks.yml from Template-Generic [skip ci] 2026-06-20 23:46:56 +00:00
jmiller 1b719a6216 chore: sync ci-generic.yml from Template-Generic [skip ci] 2026-06-20 23:46:55 +00:00
jmiller c825e800e0 chore: sync repo-health.yml from Template-Generic [skip ci] 2026-06-20 22:30:23 +00:00
jmiller 81103615a4 chore: sync rc-revert.yml from Template-Generic [skip ci] 2026-06-20 22:30:23 +00:00
jmiller 93c3c5b214 chore: sync pr-check.yml from Template-Generic [skip ci] 2026-06-20 22:30:22 +00:00
jmiller c0d5a884a4 chore: sync cleanup.yml from Template-Generic [skip ci] 2026-06-20 22:30:21 +00:00
jmiller f5eed45566 ci: sync security-audit.yml from Template-Joomla [skip ci] 2026-06-20 22:26:32 +00:00
jmiller 6633e38d8f ci: sync repo-health.yml from Template-Joomla [skip ci] 2026-06-20 22:26:03 +00:00
jmiller 8b88c1f368 ci: sync rc-revert.yml from Template-Joomla [skip ci] 2026-06-20 22:25:54 +00:00
jmiller b5705ffffe ci: sync pr-check.yml from Template-Joomla [skip ci] 2026-06-20 22:24:47 +00:00
jmiller 311178278a ci: sync issue-branch.yml from Template-Joomla [skip ci] 2026-06-20 22:22:22 +00:00
jmiller c1732e6932 ci: sync cleanup.yml from Template-Joomla [skip ci] 2026-06-20 22:15:37 +00:00
jmiller 4444b116d1 chore: sync ci-generic.yml from Template-Generic [skip ci] 2026-06-20 21:35:43 +00:00
jmiller bfb2b9f925 ci: sync ci-generic.yml from Template-Joomla [skip ci] 2026-06-20 21:34:03 +00:00
jmiller 88b3d0df0f ci: sync cascade-dev.yml from Template-Joomla [skip ci] 2026-06-20 21:31:35 +00:00
jmiller b97b76eb0d ci: sync branch-cleanup.yml from Template-Joomla [skip ci] 2026-06-20 21:28:10 +00:00
jmiller 7571b26969 ci: sync auto-release.yml from Template-Joomla [skip ci] 2026-06-20 21:26:58 +00:00
jmiller 4eba3d2be4 chore: sync workflow-sync-trigger.yml from Template-Generic [skip ci] 2026-06-20 20:53:59 +00:00
jmiller 238dc29535 chore: sync rc-revert.yml from Template-Generic [skip ci] 2026-06-20 20:53:58 +00:00
jmiller 6765c2406e chore: sync issue-branch.yml from Template-Generic [skip ci] 2026-06-20 20:53:56 +00:00
jmiller da67260991 ci: sync ci-generic.yml from Template-Joomla [skip ci] 2026-06-20 20:35:06 +00:00
jmiller b485cc6fb5 ci: sync cascade-dev.yml from Template-Joomla [skip ci] 2026-06-20 20:32:53 +00:00
jmiller 4d6f76acde ci: sync branch-cleanup.yml from Template-Joomla [skip ci] 2026-06-20 20:31:54 +00:00
jmiller e106b6d4be ci: sync auto-release.yml from Template-Joomla [skip ci] 2026-06-20 20:31:00 +00:00
jmiller 566b6c2e6e ci: sync auto-bump.yml from Template-Joomla [skip ci] 2026-06-20 19:59:09 +00:00
jmiller 625e7d1337 ci: sync ci-generic.yml from Template-Joomla [skip ci] 2026-06-20 19:06:00 +00:00
jmiller 14f3f4a17c ci: sync cascade-dev.yml from Template-Joomla [skip ci] 2026-06-20 19:03:18 +00:00
jmiller a5066645d8 ci: sync branch-cleanup.yml from Template-Joomla [skip ci] 2026-06-20 19:02:45 +00:00
jmiller 47678a892c ci: sync auto-release.yml from Template-Joomla [skip ci] 2026-06-20 19:01:05 +00:00
jmiller f07806d3dc ci: sync auto-bump.yml from Template-Joomla [skip ci] 2026-06-20 18:53:51 +00:00
jmiller 99fd758900 ci: sync pre-release workflow from Template-Joomla
Generic: Project CI / Lint & Validate (push) Failing after 8s
Generic: Project CI / Tests (push) Has been cancelled
2026-06-20 18:49:30 +00:00
jmiller ff4cdf3c93 ci: add Joomla metadata validation workflow for PRs
Generic: Project CI / Lint & Validate (push) Failing after 40s
Generic: Project CI / Tests (push) Has been cancelled
2026-06-20 18:39:09 +00:00
jmiller f1e7f0dd18 fix: rename moko-platform to mokocli + changelog promotion in workflows
Generic: Project CI / Lint & Validate (push) Failing after 34s
Generic: Project CI / Tests (push) Has been cancelled
2026-06-20 17:16:55 +00:00
jmiller 86427f9b44 fix: rename moko-platform to mokocli + changelog promotion in workflows
Generic: Project CI / Lint & Validate (push) Failing after 34s
Generic: Project CI / Tests (push) Has been cancelled
2026-06-20 17:16:54 +00:00
jmiller 4003f53acc fix: rename moko-platform to mokocli + changelog promotion in workflows
Generic: Project CI / Lint & Validate (push) Failing after 11s
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Project CI / Tests (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-20 17:16:54 +00:00
jmiller 4bec6c4cfd fix: rename moko-platform to mokocli + changelog promotion in workflows
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Project CI / Lint & Validate (push) Failing after 10s
Generic: Project CI / Tests (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-20 17:16:53 +00:00
jmiller 5cdc8f533d fix: rename moko-platform to mokocli + changelog promotion in workflows
Generic: Project CI / Lint & Validate (push) Failing after 1m6s
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Project CI / Tests (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-20 17:16:52 +00:00
jmiller a40dfa7e69 fix: rename moko-platform to mokocli + changelog promotion in workflows
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Project CI / Lint & Validate (push) Failing after 57s
Generic: Project CI / Tests (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-20 17:16:51 +00:00
217 changed files with 5361 additions and 4171 deletions
+41
View File
@@ -0,0 +1,41 @@
# EditorConfig helps maintain consistent coding styles across different editors and IDEs
# https://editorconfig.org/
root = true
# Default settings — Tabs preferred, width = 2 spaces
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = tab
tab_width = 2
# PowerShell scripts — tabs, 2-space visual width
[*.ps1]
indent_style = tab
tab_width = 2
end_of_line = crlf
# Markdown files — keep trailing whitespace for line breaks
[*.md]
trim_trailing_whitespace = false
# JSON / YAML files — tabs, 2-space visual width
[*.{json,yml,yaml}]
indent_style = tab
tab_width = 2
# Makefiles — always tabs, default width
[Makefile]
indent_style = tab
tab_width = 2
# Windows batch scripts — keep CRLF endings
[*.{bat,cmd}]
end_of_line = crlf
# Shell scripts — ensure LF endings
[*.sh]
end_of_line = lf
+110
View File
@@ -0,0 +1,110 @@
---
name: Architecture Decision Record (ADR)
about: Propose or document an architectural decision
title: '[ADR] '
labels: 'architecture, decision'
assignees: ''
---
## ADR Number
ADR-XXXX
## Status
- [ ] Proposed
- [ ] Accepted
- [ ] Deprecated
- [ ] Superseded by ADR-XXXX
## Context
Describe the issue or problem that motivates this decision.
## Decision
State the architecture decision and provide rationale.
## Consequences
### Positive
- List positive consequences
### Negative
- List negative consequences or trade-offs
### Neutral
- List neutral aspects
## Alternatives Considered
### Alternative 1
- Description
- Pros
- Cons
- Why not chosen
### Alternative 2
- Description
- Pros
- Cons
- Why not chosen
## Implementation Plan
1. Step 1
2. Step 2
3. Step 3
## Stakeholders
- **Decision Makers**: @user1, @user2
- **Consulted**: @user3, @user4
- **Informed**: team-name
## Technical Details
### Architecture Diagram
```
[Add diagram or link]
```
### Dependencies
- Dependency 1
- Dependency 2
### Impact Analysis
- **Performance**: [Impact description]
- **Security**: [Impact description]
- **Scalability**: [Impact description]
- **Maintainability**: [Impact description]
## Testing Strategy
- [ ] Unit tests
- [ ] Integration tests
- [ ] Performance tests
- [ ] Security tests
## Documentation
- [ ] Architecture documentation updated
- [ ] API documentation updated
- [ ] Developer guide updated
- [ ] Runbook created
## Migration Path
Describe how to migrate from current state to new architecture.
## Rollback Plan
Describe how to rollback if issues occur.
## Timeline
- **Proposal Date**:
- **Decision Date**:
- **Implementation Start**:
- **Expected Completion**:
## References
- Related ADRs:
- External resources:
- RFCs:
## Review Checklist
- [ ] Aligns with enterprise architecture principles
- [ ] Security implications reviewed
- [ ] Performance implications reviewed
- [ ] Cost implications reviewed
- [ ] Compliance requirements met
- [ ] Team consensus achieved
+48
View File
@@ -0,0 +1,48 @@
---
name: Bug Report
about: Report a bug or issue with the project
title: '[BUG] '
labels: 'bug'
assignees: ''
---
## Bug Description
A clear and concise description of what the bug is.
## Steps to Reproduce
1. Go to '...'
2. Click on '...'
3. Scroll down to '...'
4. See error
## Expected Behavior
A clear and concise description of what you expected to happen.
## Actual Behavior
A clear and concise description of what actually happened.
## Screenshots
If applicable, add screenshots to help explain your problem.
## Environment
- **Project**: [e.g., MokoDoliTools, moko-cassiopeia]
- **Version**: [e.g., 1.2.3]
- **Platform**: [e.g., Dolibarr 18.0, Joomla 5.0]
- **PHP Version**: [e.g., 8.1]
- **Database**: [e.g., MySQL 8.0, PostgreSQL 14]
- **Browser** (if applicable): [e.g., Chrome 120, Firefox 121]
- **OS**: [e.g., Ubuntu 22.04, Windows 11]
## Additional Context
Add any other context about the problem here.
## Possible Solution
If you have suggestions on how to fix the issue, please describe them here.
## Checklist
- [ ] I have searched for similar issues before creating this one
- [ ] I have provided all the requested information
- [ ] I have tested this on the latest stable version
- [ ] I have checked the documentation and couldn't find a solution
+18
View File
@@ -0,0 +1,18 @@
---
blank_issues_enabled: true
contact_links:
- name: 💼 Enterprise Support
url: https://mokoconsulting.tech/enterprise
about: Enterprise-level support and consultation services
- name: 💬 Ask a Question
url: https://mokoconsulting.tech/
about: Get help or ask questions through our website
- name: 📚 MokoStandards Documentation
url: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
about: View our coding standards and best practices
- name: 🔒 Report a Security Vulnerability
url: https://git.mokoconsulting.tech/mokoconsulting-tech/.github-private/security/advisories/new
about: Report security vulnerabilities privately (for critical issues)
- name: 💡 Community Discussions
url: https://github.com/orgs/mokoconsulting-tech/discussions
about: Join community discussions and Q&A
@@ -0,0 +1,52 @@
---
name: Documentation Issue
about: Report an issue with documentation
title: '[DOCS] '
labels: 'documentation'
assignees: ''
---
## Documentation Issue
**Location**:
<!-- Specify the file, page, or section with the issue -->
## Issue Type
<!-- Mark the relevant option with an "x" -->
- [ ] Typo or grammar error
- [ ] Outdated information
- [ ] Missing documentation
- [ ] Unclear explanation
- [ ] Broken links
- [ ] Missing examples
- [ ] Other (specify below)
## Description
<!-- Clearly describe the documentation issue -->
## Current Content
<!-- Quote or describe the current documentation (if applicable) -->
```
Current text here
```
## Suggested Improvement
<!-- Provide your suggestion for how to improve the documentation -->
```
Suggested text here
```
## Additional Context
<!-- Add any other context, screenshots, or references -->
## Standards Alignment
- [ ] Follows MokoStandards documentation guidelines
- [ ] Uses en_US/en_GB localization
- [ ] Includes proper SPDX headers where applicable
## Checklist
- [ ] I have searched for similar documentation issues
- [ ] I have provided a clear description
- [ ] I have suggested an improvement (if applicable)
@@ -0,0 +1,51 @@
---
name: Feature Request
about: Suggest a new feature or enhancement
title: '[FEATURE] '
labels: 'enhancement'
assignees: ''
---
## Feature Description
A clear and concise description of the feature you'd like to see.
## Problem or Use Case
Describe the problem this feature would solve or the use case it addresses.
Ex. I'm always frustrated when [...]
## Proposed Solution
A clear and concise description of what you want to happen.
## Alternative Solutions
A clear and concise description of any alternative solutions or features you've considered.
## Benefits
Describe how this feature would benefit users:
- Who would use this feature?
- What problems does it solve?
- What value does it add?
## Implementation Details (Optional)
If you have ideas about how this could be implemented, share them here:
- Technical approach
- Files/components that might need changes
- Any concerns or challenges you foresee
## Additional Context
Add any other context, mockups, or screenshots about the feature request here.
## Relevant Standards
Does this relate to any standards in [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards)?
- [ ] Accessibility (WCAG 2.1 AA)
- [ ] Localization (en_US/en_GB)
- [ ] Security best practices
- [ ] Code quality standards
- [ ] Other: [specify]
## Checklist
- [ ] I have searched for similar feature requests before creating this one
- [ ] I have clearly described the use case and benefits
- [ ] I have considered alternative solutions
- [ ] This feature aligns with the project's goals and scope
+87
View File
@@ -0,0 +1,87 @@
---
name: Joomla Extension Issue
about: Report an issue with a Joomla extension
title: '[JOOMLA] '
labels: 'joomla'
assignees: ''
---
## Issue Type
- [ ] Component Issue
- [ ] Module Issue
- [ ] Plugin Issue
- [ ] Template Issue
## Extension Details
- **Extension Name**: [e.g., moko-cassiopeia]
- **Extension Version**: [e.g., 1.2.3]
- **Extension Type**: [Component / Module / Plugin / Template]
## Joomla Environment
- **Joomla Version**: [e.g., 4.4.0, 5.0.0]
- **PHP Version**: [e.g., 8.1.0]
- **Database**: [MySQL / PostgreSQL / MariaDB]
- **Database Version**: [e.g., 8.0]
- **Server**: [Apache / Nginx / IIS]
- **Hosting**: [Shared / VPS / Dedicated / Cloud]
## Issue Description
Provide a clear and detailed description of the issue.
## Steps to Reproduce
1. Go to '...'
2. Click on '...'
3. Configure '...'
4. See error
## Expected Behavior
What you expected to happen.
## Actual Behavior
What actually happened.
## Error Messages
```
# Paste any error messages from Joomla error logs
# Location: administrator/logs/error.php
```
## Browser Console Errors
```javascript
// Paste any JavaScript console errors (F12 in browser)
```
## Screenshots
Add screenshots to help explain the issue.
## Configuration
```ini
# Paste extension configuration (sanitize sensitive data)
```
## Installed Extensions
List other installed extensions that might conflict:
- Extension 1 (version)
- Extension 2 (version)
## Template Overrides
- [ ] Using template overrides
- [ ] Custom CSS
- [ ] Custom JavaScript
## Additional Context
- **Multilingual Site**: [Yes / No]
- **Cache Enabled**: [Yes / No]
- **Debug Mode**: [Yes / No]
- **SEF URLs**: [Yes / No]
## Checklist
- [ ] I have cleared Joomla cache
- [ ] I have disabled other extensions to test for conflicts
- [ ] I have checked Joomla error logs
- [ ] I have tested with a default Joomla template
- [ ] I have checked browser console for JavaScript errors
- [ ] I have searched for similar issues
- [ ] I am using a supported Joomla version
+82
View File
@@ -0,0 +1,82 @@
---
name: Question
about: Ask a question about usage, features, or best practices
title: '[QUESTION] '
labels: ['question']
assignees: ['jmiller']
---
## Question
**Your question:**
## Context
**What are you trying to accomplish?**
**What have you already tried?**
**Category**:
- [ ] Script usage
- [ ] Configuration
- [ ] Workflow setup
- [ ] Documentation interpretation
- [ ] Best practices
- [ ] Integration
- [ ] Other: __________
## Environment (if relevant)
**Your setup**:
- Operating System:
- Version:
## What You've Researched
**Documentation reviewed**:
- [ ] README.md
- [ ] Project documentation
- [ ] Other (specify): __________
**Similar issues/questions found**:
- #
- #
## Expected Outcome
**What result are you hoping for?**
## Code/Configuration Samples
**Relevant code or configuration** (if applicable):
```bash
# Your code here
```
## Additional Context
**Any other relevant information:**
**Screenshots** (if helpful):
## Urgency
- [ ] Urgent (blocking work)
- [ ] Normal (can work on other things meanwhile)
- [ ] Low priority (just curious)
## Checklist
- [ ] I have searched existing issues and discussions
- [ ] I have reviewed relevant documentation
- [ ] I have provided sufficient context
- [ ] I have included code/configuration samples if relevant
- [ ] This is a genuine question (not a bug report or feature request)
+126
View File
@@ -0,0 +1,126 @@
---
name: Request for Comments (RFC)
about: Propose a significant change for community discussion
title: '[RFC] '
labels: 'rfc, discussion'
assignees: ''
---
## RFC Summary
One-paragraph summary of the proposal.
## Motivation
Why are we doing this? What use cases does it support? What is the expected outcome?
## Detailed Design
### Overview
Provide a detailed explanation of the proposed change.
### API Changes (if applicable)
```php
// Before
function oldApi($param1) { }
// After
function newApi($param1, $param2) { }
```
### User Experience Changes
Describe how users will interact with this change.
### Implementation Approach
High-level implementation strategy.
## Drawbacks
Why should we *not* do this?
## Alternatives
What other designs have been considered? What is the impact of not doing this?
### Alternative 1
- Description
- Trade-offs
### Alternative 2
- Description
- Trade-offs
## Adoption Strategy
How will existing users adopt this? Is this a breaking change?
### Migration Guide
```bash
# Steps to migrate
```
### Deprecation Timeline
- **Announcement**:
- **Deprecation**:
- **Removal**:
## Unresolved Questions
- Question 1
- Question 2
## Future Possibilities
What future work does this enable?
## Impact Assessment
### Performance
Expected performance impact.
### Security
Security considerations and implications.
### Compatibility
- **Backward Compatible**: [Yes / No]
- **Breaking Changes**: [List]
### Maintenance
Long-term maintenance considerations.
## Community Input
### Stakeholders
- [ ] Core team
- [ ] Module developers
- [ ] End users
- [ ] Enterprise customers
### Feedback Period
**Duration**: [e.g., 2 weeks]
**Deadline**: [date]
## Implementation Timeline
### Phase 1: Design
- [ ] RFC discussion
- [ ] Design finalization
- [ ] Approval
### Phase 2: Implementation
- [ ] Core implementation
- [ ] Tests
- [ ] Documentation
### Phase 3: Release
- [ ] Beta release
- [ ] Feedback collection
- [ ] Stable release
## Success Metrics
How will we measure success?
- Metric 1
- Metric 2
## References
- Related RFCs:
- External documentation:
- Prior art:
## Open Questions for Community
1. Question 1?
2. Question 2?
---
**Note**: This RFC is open for community discussion. Please provide feedback in the comments below.
+51
View File
@@ -0,0 +1,51 @@
---
name: Security Vulnerability Report
about: Report a security vulnerability (use only for non-critical issues)
title: '[SECURITY] '
labels: 'security'
assignees: ''
---
## ⚠️ IMPORTANT: Private Disclosure Required
**For critical security vulnerabilities, DO NOT use this template.**
Follow the process in [SECURITY.md](../SECURITY.md) for responsible disclosure.
Use this template only for:
- Security improvements
- Non-critical security suggestions
- Security documentation updates
---
## Security Issue
**Severity**:
<!-- Low, Medium, or informational only -->
## Description
<!-- Describe the security concern or improvement suggestion -->
## Affected Components
<!-- List the affected files, features, or components -->
## Suggested Mitigation
<!-- Describe how this could be addressed -->
## Standards Reference
Does this relate to security standards in [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards)?
- [ ] SPDX license identifiers
- [ ] Secret management
- [ ] Dependency security
- [ ] Access control
- [ ] Other: [specify]
## Additional Context
<!-- Add any other context about the security concern -->
## Checklist
- [ ] This is NOT a critical vulnerability requiring private disclosure
- [ ] I have reviewed the SECURITY.md policy
- [ ] I have provided sufficient detail for evaluation
+24
View File
@@ -0,0 +1,24 @@
---
name: Version Bump
about: Request or track a version change
title: '[VERSION] '
labels: 'version, type: version'
assignees: 'jmiller'
---
## Version Change
**Current version**: <!-- e.g., 01.02.03 -->
**Requested version**: <!-- e.g., 01.03.00 -->
**Change type**: <!-- patch / minor / major -->
## Reason
<!-- Why is this version bump needed? -->
## Checklist
- [ ] README.md `VERSION:` field updated
- [ ] CHANGELOG.md entry added
- [ ] Module descriptor version updated (Dolibarr: `$this->version`, Joomla: `<version>`)
- [ ] All file headers will be auto-propagated by `sync-version-on-merge` workflow
-26
View File
@@ -1,26 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<mokoplatform xmlns="https://standards.mokoconsulting.tech/mokoplatform/1.0" schema-version="1.0">
<identity>
<name>MokoSuiteCross</name>
<display-name>Package - MokoSuiteCross</display-name>
<org>MokoConsulting</org>
<description>Cross-posting Joomla content to social media, email marketing, and chat platforms</description>
<version>01.02.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/mokoplatform</standards-source>
</governance>
<build>
<language>PHP</language>
<package-type>joomla-extension</package-type>
<entry-point>source/</entry-point>
</build>
<licensing>
<enabled>true</enabled>
<dlid>true</dlid>
<update-server>https://git.mokoconsulting.tech/{org}/{repo}/updates.xml</update-server>
</licensing>
</mokoplatform>
+9 -9
View File
@@ -4,8 +4,8 @@
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Release # INGROUP: mokocli.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform # REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /.mokogitea/workflows/auto-bump.yml # PATH: /.mokogitea/workflows/auto-bump.yml
# VERSION: 09.02.00 # VERSION: 09.02.00
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits) # BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
@@ -43,19 +43,19 @@ jobs:
token: ${{ secrets.MOKOGITEA_TOKEN }} token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 1 fetch-depth: 1
- name: Setup moko-platform tools - name: Setup mokocli tools
run: | run: |
if ! command -v composer &> /dev/null; then if ! command -v composer &> /dev/null; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
fi fi
if [ -d "/opt/moko-platform/cli" ]; then if [ -d "/opt/mokocli/cli" ]; then
echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV" echo "MOKO_CLI=/opt/mokocli/cli" >> "$GITHUB_ENV"
else else
git clone --depth 1 --branch main --quiet \ git clone --depth 1 --branch main --quiet \
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \ "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/mokocli.git" \
/tmp/moko-platform-api /tmp/mokocli
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV" echo "MOKO_CLI=/tmp/mokocli/cli" >> "$GITHUB_ENV"
fi fi
- name: Bump version - name: Bump version
+180 -38
View File
@@ -4,15 +4,15 @@
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: mokoplatform.Release # INGROUP: mokocli.Release
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokoplatform # REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli
# PATH: /templates/workflows/universal/auto-release.yml.template # PATH: /templates/workflows/universal/auto-release.yml.template
# VERSION: 05.00.00 # VERSION: 05.00.00
# BRIEF: Universal build & release detects platform from manifest.xml # BRIEF: Universal build & release detects platform from manifest.xml
# #
# +========================================================================+ # +=======================================================================+
# | UNIVERSAL BUILD & RELEASE PIPELINE | # | UNIVERSAL BUILD & RELEASE PIPELINE |
# +========================================================================+ # +=======================================================================+
# | | # | |
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. | # | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
# | | # | |
@@ -21,7 +21,7 @@
# | dolibarr: mod*.class.php, update.txt, dev version reset | # | dolibarr: mod*.class.php, update.txt, dev version reset |
# | generic: README-only, no update stream | # | generic: README-only, no update stream |
# | | # | |
# +========================================================================+ # +=======================================================================+
name: "Universal: Build & Release" name: "Universal: Build & Release"
@@ -30,6 +30,15 @@ on:
types: [opened, closed] types: [opened, closed]
branches: branches:
- main - main
paths-ignore:
- '.mokogitea/workflows/**'
- '*.md'
- 'wiki/**'
- '.editorconfig'
- '.gitignore'
- '.gitattributes'
- '.gitmessage'
- 'LICENSE'
workflow_dispatch: workflow_dispatch:
inputs: inputs:
action: action:
@@ -51,7 +60,7 @@ permissions:
contents: write contents: write
jobs: jobs:
# ── PR Opened → Rename branch to RC and build RC release ───────────────────── # ── PR Opened → Rename branch to RC and build RC release ─────────────────────────
promote-rc: promote-rc:
name: Promote to RC name: Promote to RC
runs-on: release runs-on: release
@@ -66,25 +75,25 @@ jobs:
token: ${{ secrets.MOKOGITEA_TOKEN }} token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 1 fetch-depth: 1
- name: Setup mokoplatform tools - name: Setup mokocli tools
env: env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
run: | run: |
if [ -f /opt/mokoplatform/cli/version_bump.php ] && [ -f /opt/mokoplatform/vendor/autoload.php ]; then if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then
echo Using pre-installed /opt/mokoplatform echo Using pre-installed /opt/mokocli
echo MOKO_CLI=/opt/mokoplatform/cli >> $GITHUB_ENV echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV
else else
echo Falling back to fresh clone echo Falling back to fresh clone
if ! command -v composer > /dev/null 2>&1; then if ! command -v composer > /dev/null 2>&1; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1 sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
fi fi
rm -rf /tmp/mokoplatform-api rm -rf /tmp/mokocli
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokoplatform.git CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokoplatform-api git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
cd /tmp/mokoplatform-api cd /tmp/mokocli
composer install --no-dev --no-interaction --quiet composer install --no-dev --no-interaction --quiet
echo MOKO_CLI=/tmp/mokoplatform-api/cli >> $GITHUB_ENV echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
fi fi
- name: Rename branch to rc - name: Rename branch to rc
@@ -109,13 +118,47 @@ jobs:
--path . --stability rc --bump minor --branch rc \ --path . --stability rc --bump minor --branch rc \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --token "${{ secrets.MOKOGITEA_TOKEN }}"
- name: Update RC release notes from CHANGELOG.md
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
# Extract [Unreleased] section from changelog
NOTES=""
if [ -f "CHANGELOG.md" ]; then
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
fi
[ -z "$NOTES" ] && NOTES="Release candidate"
# Find the RC release and update its body
RELEASE_ID=$(curl -sf -H "Authorization: token ${TOKEN}" \
"${API_BASE}/releases/tags/release-candidate" \
| python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
if [ -n "$RELEASE_ID" ]; then
python3 -c "
import json, urllib.request
body = open('/dev/stdin').read()
payload = json.dumps({'body': body}).encode()
req = urllib.request.Request(
'${API_BASE}/releases/${RELEASE_ID}',
data=payload, method='PATCH',
headers={
'Authorization': 'token ${TOKEN}',
'Content-Type': 'application/json'
})
urllib.request.urlopen(req)
" <<< "$NOTES"
echo "RC release notes updated from CHANGELOG.md"
fi
- name: Summary - name: Summary
if: always() if: always()
run: | run: |
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY
# ── Merged PR → Build & Release (or promote RC to stable) ──────────────────── # ── Merged PR → Build & Release (or promote RC to stable) ─────────────────────────
release: release:
name: Build & Release Pipeline name: Build & Release Pipeline
runs-on: release runs-on: release
@@ -149,50 +192,131 @@ jobs:
fi fi
echo "No conflict markers found" echo "No conflict markers found"
- name: Setup mokoplatform tools - name: Setup mokocli tools
env: env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}' COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
run: | run: |
if [ -f /opt/mokoplatform/cli/version_bump.php ] && [ -f /opt/mokoplatform/vendor/autoload.php ]; then if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then
echo Using pre-installed /opt/mokoplatform echo Using pre-installed /opt/mokocli
echo MOKO_CLI=/opt/mokoplatform/cli >> $GITHUB_ENV echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV
else else
echo Falling back to fresh clone echo Falling back to fresh clone
if ! command -v composer > /dev/null 2>&1; then if ! command -v composer > /dev/null 2>&1; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1 sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
fi fi
rm -rf /tmp/mokoplatform-api rm -rf /tmp/mokocli
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokoplatform.git CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokoplatform-api git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
cd /tmp/mokoplatform-api cd /tmp/mokocli
composer install --no-dev --no-interaction --quiet composer install --no-dev --no-interaction --quiet
echo MOKO_CLI=/tmp/mokoplatform-api/cli >> $GITHUB_ENV echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
fi fi
- name: "Detect platform"
id: platform
run: |
php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true
php ${MOKO_CLI}/manifest_read.php --path . --github-output 2>/dev/null || true
- name: "Determine version bump level"
id: bump
run: |
# Fix/patch branches: version was already bumped by pre-release, just strip suffix
# Feature/dev branches: bump minor for the new stable release
HEAD_REF="${{ github.event.pull_request.head.ref || 'dev' }}"
case "$HEAD_REF" in
fix/*|patch/*|hotfix/*|bugfix/*) BUMP="none" ;;
*) BUMP="minor" ;;
esac
echo "level=${BUMP}" >> "$GITHUB_OUTPUT"
echo "Bump level: ${BUMP} (from branch: ${HEAD_REF})"
- name: "Publish stable release" - name: "Publish stable release"
run: | run: |
BUMP_FLAG=""
if [ "${{ steps.bump.outputs.level }}" != "none" ]; then
BUMP_FLAG="--bump ${{ steps.bump.outputs.level }}"
fi
php ${MOKO_CLI}/release_publish.php \ php ${MOKO_CLI}/release_publish.php \
--path . --stability stable --bump minor --branch main \ --path . --stability stable ${BUMP_FLAG} --branch main \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --token "${{ secrets.MOKOGITEA_TOKEN }}"
- name: Update release notes from CHANGELOG.md - name: "Read published version"
id: version
run: | run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "")
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
# Extract [Unreleased] section from changelog [ -z "$VERSION" ] && VERSION="00.00.00" && echo "skip=true" >> "$GITHUB_OUTPUT"
if [ -f "CHANGELOG.md" ]; then echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md) PLATFORM="${{ steps.platform.outputs.platform }}"
[ -z "$NOTES" ] && NOTES="Stable release" if [[ "$PLATFORM" == joomla* ]]; then
echo "tag=stable" >> "$GITHUB_OUTPUT"
echo "release_tag=stable" >> "$GITHUB_OUTPUT"
else else
NOTES="Stable release" echo "tag=v${VERSION}" >> "$GITHUB_OUTPUT"
echo "release_tag=v${VERSION}" >> "$GITHUB_OUTPUT"
fi
echo "branch=main" >> "$GITHUB_OUTPUT"
echo "Published version: ${VERSION}"
- name: "Create semver tag for non-Joomla repos"
id: semver
if: |
steps.version.outputs.skip != 'true' &&
!startsWith(steps.platform.outputs.platform, 'joomla')
run: |
VERSION="${{ steps.version.outputs.version }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
SEMVER_TAG="v${VERSION}"
echo "Creating semver tag: ${SEMVER_TAG}"
# Create the git tag via API
HTTP_CODE=$(curl -sf -o /dev/null -w "%{http_code}" \
-X POST -H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
"${API_BASE}/tags" \
-d "{\"tag_name\":\"${SEMVER_TAG}\",\"target\":\"main\",\"message\":\"Release ${VERSION}\"}" 2>/dev/null || echo "000")
if [ "$HTTP_CODE" = "201" ] || [ "$HTTP_CODE" = "200" ]; then
echo "Created semver tag: ${SEMVER_TAG}"
elif [ "$HTTP_CODE" = "409" ]; then
echo "Semver tag ${SEMVER_TAG} already exists (skipped)"
else
echo "::warning::Failed to create semver tag ${SEMVER_TAG} (HTTP ${HTTP_CODE})"
fi fi
# Update release body via API echo "semver_tag=${SEMVER_TAG}" >> "$GITHUB_OUTPUT"
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
"${API_BASE}/releases/tags/stable" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
- name: Update release notes and promote changelog
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
# Get the stable release info (version and ID)
RELEASE_JSON=$(curl -sf -H "Authorization: token ${TOKEN}" \
"${API_BASE}/releases/tags/stable" 2>/dev/null || echo '{}')
RELEASE_ID=$(python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" <<< "$RELEASE_JSON" 2>/dev/null || true)
# Extract version from release name (e.g. "06.17.00" or "v06.17.00")
VERSION=$(python3 -c "
import json, sys, re
r = json.load(sys.stdin)
name = r.get('name', '')
m = re.search(r'(\d+\.\d+\.\d+)', name)
print(m.group(1) if m else '')
" <<< "$RELEASE_JSON" 2>/dev/null || true)
# Extract [Unreleased] section from changelog
NOTES=""
if [ -f "CHANGELOG.md" ]; then
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
fi
[ -z "$NOTES" ] && NOTES="Stable release"
# Update release body via API
if [ -n "$RELEASE_ID" ]; then if [ -n "$RELEASE_ID" ]; then
python3 -c " python3 -c "
import json, urllib.request import json, urllib.request
@@ -202,7 +326,7 @@ jobs:
'${API_BASE}/releases/${RELEASE_ID}', '${API_BASE}/releases/${RELEASE_ID}',
data=payload, method='PATCH', data=payload, method='PATCH',
headers={ headers={
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}', 'Authorization': 'token ${TOKEN}',
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}) })
urllib.request.urlopen(req) urllib.request.urlopen(req)
@@ -210,6 +334,24 @@ jobs:
echo "Release notes updated from CHANGELOG.md" echo "Release notes updated from CHANGELOG.md"
fi fi
# Promote [Unreleased] → [version] in CHANGELOG.md and reset
if [ -n "$VERSION" ] && [ -f "CHANGELOG.md" ]; then
DATE=$(date +%Y-%m-%d)
python3 -c "
import sys
version, date = sys.argv[1], sys.argv[2]
content = open('CHANGELOG.md').read()
old = '## [Unreleased]'
new = f'## [Unreleased]\n\n## [{version}] --- {date}'
content = content.replace(old, new, 1)
open('CHANGELOG.md', 'w').write(content)
" "$VERSION" "$DATE"
git add CHANGELOG.md
git commit -m "chore: promote changelog [Unreleased] → [${VERSION}]" || true
git push origin main || true
echo "Changelog promoted: [Unreleased] → [${VERSION}]"
fi
# -- STEP 9: Mirror to GitHub (stable only) -------------------------------- # -- STEP 9: Mirror to GitHub (stable only) --------------------------------
- name: "Step 9: Mirror release to GitHub" - name: "Step 9: Mirror release to GitHub"
if: >- if: >-
+1 -1
View File
@@ -5,7 +5,7 @@
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Universal # INGROUP: MokoStandards.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform # REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /.mokogitea/workflows/branch-cleanup.yml # PATH: /.mokogitea/workflows/branch-cleanup.yml
# VERSION: 01.00.00 # VERSION: 01.00.00
# BRIEF: Delete feature branches after PR merge # BRIEF: Delete feature branches after PR merge
-13
View File
@@ -13,19 +13,6 @@
name: "Generic: Project CI" name: "Generic: Project CI"
on: on:
push:
branches:
- main
- dev
- dev/**
- rc/**
- version/**
pull_request:
branches:
- main
- dev
- dev/**
- rc/**
workflow_dispatch: workflow_dispatch:
permissions: permissions:
@@ -0,0 +1 @@
IyBDb3B5cmlnaHQgKEMpIDIwMjYgTW9rbyBDb25zdWx0aW5nIDxoZWxsb0Btb2tvY29uc3VsdGluZy50ZWNoPgojCiMgU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IEdQTC0zLjAtb3ItbGF0ZXIKIwojIEZJTEUgSU5GT1JNQVRJT04KIyBERUZHUk9VUDogR2l0ZWEuV29ya2Zsb3cKIyBJTkdST1VQOiBtb2tvY2xpLlVuaXZlcnNhbAojIFJFUE86IGh0dHBzOi8vZ2l0Lm1va29jb25zdWx0aW5nLnRlY2gvTW9rb0NvbnN1bHRpbmcvbW9rb2NsaQojIFBBVEg6IC8ubW9rb2dpdGVhL3dvcmtmbG93cy9jaS1pc3N1ZS1yZXBvcnRlci55bWwKIyBWRVJTSU9OOiAwMS4wMC4wMAojIEJSSUVGOiBSZXVzYWJsZSB3b3JrZmxvdyDigJQgY3JlYXRlcy91cGRhdGVzIGEgR2l0ZWEgaXNzdWUgd2hlbiBhIENJIGdhdGUgZmFpbHMuCiMgICAgICAgIENsb25lcyBNb2tvQ0xJIGFuZCBydW5zIGNsaS9jaV9pc3N1ZV9yZXBvcnRlci5zaC4KCm5hbWU6ICJVbml2ZXJzYWw6IENJIElzc3VlIFJlcG9ydGVyIgoKb246CiAgd29ya2Zsb3dfY2FsbDoKICAgIGlucHV0czoKICAgICAgZ2F0ZToKICAgICAgICBkZXNjcmlwdGlvbjogIkNJIGdhdGUgbmFtZSAoZS5nLiBQUiBWYWxpZGF0aW9uLCBSZXBvc2l0b3J5IEhlYWx0aCkiCiAgICAgICAgcmVxdWlyZWQ6IHRydWUKICAgICAgICB0eXBlOiBzdHJpbmcKICAgICAgZGV0YWlsczoKICAgICAgICBkZXNjcmlwdGlvbjogIkh1bWFuLXJlYWRhYmxlIGZhaWx1cmUgZGVzY3JpcHRpb24iCiAgICAgICAgcmVxdWlyZWQ6IHRydWUKICAgICAgICB0eXBlOiBzdHJpbmcKICAgICAgc2V2ZXJpdHk6CiAgICAgICAgZGVzY3JpcHRpb246ICJlcnJvciBvciB3YXJuaW5nIgogICAgICAgIHJlcXVpcmVkOiBmYWxzZQogICAgICAgIHR5cGU6IHN0cmluZwogICAgICAgIGRlZmF1bHQ6ICJlcnJvciIKICAgICAgd29ya2Zsb3c6CiAgICAgICAgZGVzY3JpcHRpb246ICJXb3JrZmxvdyBuYW1lIGZvciB0aGUgaXNzdWUgdGl0bGUiCiAgICAgICAgcmVxdWlyZWQ6IGZhbHNlCiAgICAgICAgdHlwZTogc3RyaW5nCiAgICAgICAgZGVmYXVsdDogIiIKICAgIHNlY3JldHM6CiAgICAgIE1PS09HSVRFQV9UT0tFTjoKICAgICAgICByZXF1aXJlZDogdHJ1ZQoKZW52OgogIEZPUkNFX0pBVkFTQ1JJUFRfQUNUSU9OU19UT19OT0RFMjQ6IHRydWUKCmpvYnM6CiAgcmVwb3J0OgogICAgbmFtZTogIlJlcG9ydDogJHt7IGlucHV0cy5nYXRlIH19IgogICAgcnVucy1vbjogdWJ1bnR1LWxhdGVzdAoKICAgIHN0ZXBzOgogICAgICAtIG5hbWU6IENsb25lIE1va29DTEkKICAgICAgICBlbnY6CiAgICAgICAgICBNT0tPR0lURUFfVE9LRU46ICR7eyBzZWNyZXRzLk1PS09HSVRFQV9UT0tFTiB9fQogICAgICAgIHJ1bjogfAogICAgICAgICAgTU9LT0dJVEVBX1VSTD0iJHt7IHZhcnMuR0lURUFfVVJMIHx8ICdodHRwczovL2dpdC5tb2tvY29uc3VsdGluZy50ZWNoJyB9fSIKICAgICAgICAgIGdpdCBjbG9uZSAtLWRlcHRoIDEgLS1maWx0ZXI9YmxvYjpub25lIC0tc3BhcnNlICIke01PS09HSVRFQV9VUkx9L01va29Db25zdWx0aW5nL01va29DTEkuZ2l0IiAvdG1wL21va29jbGkKICAgICAgICAgIGNkIC90bXAvbW9rb2NsaSAmJiBnaXQgc3BhcnNlLWNoZWNrb3V0IHNldCBjbGkvY2lfaXNzdWVfcmVwb3J0ZXIuc2gKCiAgICAgIC0gbmFtZTogUmVwb3J0IENJIGZhaWx1cmUKICAgICAgICBlbnY6CiAgICAgICAgICBNT0tPR0lURUFfVE9LRU46ICR7eyBzZWNyZXRzLk1PS09HSVRFQV9UT0tFTiB9fQogICAgICAgICAgTU9LT0dJVEVBX1VSTDogJHt7IHZhcnMuR0lURUFfVVJMIHx8ICdodHRwczovL2dpdC5tb2tvY29uc3VsdGluZy50ZWNoJyB9fQogICAgICAgIHJ1bjogfAogICAgICAgICAgY2htb2QgK3ggL3RtcC9tb2tvY2xpL2NsaS9jaV9pc3N1ZV9yZXBvcnRlci5zaAogICAgICAgICAgL3RtcC9tb2tvY2xpL2NsaS9jaV9pc3N1ZV9yZXBvcnRlci5zaCBcCiAgICAgICAgICAgIC0tZ2F0ZSAiJHt7IGlucHV0cy5nYXRlIH19IiBcCiAgICAgICAgICAgIC0tZGV0YWlscyAiJHt7IGlucHV0cy5kZXRhaWxzIH19IiBcCiAgICAgICAgICAgIC0tc2V2ZXJpdHkgIiR7eyBpbnB1dHMuc2V2ZXJpdHkgfX0iIFwKICAgICAgICAgICAgLS13b3JrZmxvdyAiJHt7IGlucHV0cy53b3JrZmxvdyB9fSIK
+741 -7
View File
@@ -45,17 +45,17 @@ jobs:
fi fi
php -v && composer --version php -v && composer --version
- name: Setup moko-platform tools - name: Setup mokocli tools
env: env:
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || github.token }} MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.GA_TOKEN || github.token }}
MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }} MOKO_CLONE_HOST: ${{ secrets.MOKOGITEA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
run: | run: |
if [ -d "/tmp/moko-platform" ] || [ -d "/opt/moko-platform" ]; then if [ -d "/opt/mokocli" ] || [ -d "/tmp/mokocli" ]; then
echo "moko-platform already available on runner — skipping clone" echo "mokocli already available on runner — skipping clone"
else else
git clone --depth 1 --branch main --quiet \ git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git" \
/tmp/moko-platform 2>/dev/null || echo "moko-platform clone skipped — continuing without it" /tmp/mokocli 2>/dev/null || echo "mokocli clone skipped — continuing without it"
fi fi
- name: Install dependencies - name: Install dependencies
@@ -164,6 +164,75 @@ jobs:
echo "**Manifest validation passed.**" >> $GITHUB_STEP_SUMMARY echo "**Manifest validation passed.**" >> $GITHUB_STEP_SUMMARY
fi fi
- name: Update server & packaging checks
continue-on-error: true
run: |
echo "### Update Server & Packaging" >> $GITHUB_STEP_SUMMARY
WARNINGS=0
# 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 manifest found — skipping." >> $GITHUB_STEP_SUMMARY
else
EXT_TYPE=$(grep -oP '<extension[^>]*\btype="\K[^"]+' "$MANIFEST" | head -1)
# 1. Check <updateservers> exists and uses MokoGitea update server
if ! grep -q '<updateservers>' "$MANIFEST" 2>/dev/null; then
echo "::warning file=${MANIFEST}::Missing \`<updateservers>\` tag — extension will not receive OTA updates"
echo "- **Missing** \`<updateservers>\` — extension will not receive OTA updates" >> $GITHUB_STEP_SUMMARY
WARNINGS=$((WARNINGS + 1))
else
SERVER_URL=$(grep -oP '<server[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1)
if [ -z "$SERVER_URL" ]; then
echo "::warning file=${MANIFEST}::\`<updateservers>\` is empty — no server URL defined"
echo "- **Empty** \`<updateservers>\` — no server URL defined" >> $GITHUB_STEP_SUMMARY
WARNINGS=$((WARNINGS + 1))
elif ! echo "$SERVER_URL" | grep -q 'git\.mokoconsulting\.tech'; then
echo "::warning file=${MANIFEST}::Update server does not use MokoGitea engine: ${SERVER_URL}"
echo "- **Non-MokoGitea update server:** \`${SERVER_URL}\`" >> $GITHUB_STEP_SUMMARY
echo " Expected: \`https://git.mokoconsulting.tech/{org}/{repo}/updates.xml\`" >> $GITHUB_STEP_SUMMARY
WARNINGS=$((WARNINGS + 1))
else
echo "- \`<updateservers>\`: MokoGitea engine ✓" >> $GITHUB_STEP_SUMMARY
fi
fi
# 2. Check <dlid> tag exists
if ! grep -q '<dlid' "$MANIFEST" 2>/dev/null; then
echo "::warning file=${MANIFEST}::Missing \`<dlid>\` tag — download ID authentication is not configured"
echo "- **Missing** \`<dlid>\` — download ID authentication not configured" >> $GITHUB_STEP_SUMMARY
WARNINGS=$((WARNINGS + 1))
else
echo "- \`<dlid>\`: present ✓" >> $GITHUB_STEP_SUMMARY
fi
# 3. For packages: check <childuninstall> tag
if [ "$EXT_TYPE" = "package" ]; then
if ! grep -q '<childuninstall>' "$MANIFEST" 2>/dev/null; then
echo "::warning file=${MANIFEST}::Package is missing \`<childuninstall>\` — child extensions will not be removed on uninstall"
echo "- **Missing** \`<childuninstall>\` — child extensions will remain when package is uninstalled" >> $GITHUB_STEP_SUMMARY
WARNINGS=$((WARNINGS + 1))
else
echo "- \`<childuninstall>\`: present ✓" >> $GITHUB_STEP_SUMMARY
fi
fi
fi
echo "" >> $GITHUB_STEP_SUMMARY
if [ "$WARNINGS" -gt 0 ]; then
echo "**${WARNINGS} packaging warning(s).** These won't block CI but should be addressed." >> $GITHUB_STEP_SUMMARY
else
echo "**Update server & packaging checks passed.**" >> $GITHUB_STEP_SUMMARY
fi
- name: Check language files referenced in manifest - name: Check language files referenced in manifest
run: | run: |
echo "### Language File Check" >> $GITHUB_STEP_SUMMARY echo "### Language File Check" >> $GITHUB_STEP_SUMMARY
@@ -245,10 +314,675 @@ jobs:
echo "All ${CHECKED} directories contain index.html." >> $GITHUB_STEP_SUMMARY echo "All ${CHECKED} directories contain index.html." >> $GITHUB_STEP_SUMMARY
fi fi
- name: Check config.xml and access.xml for components
run: |
echo "### Component Config & ACL Check" >> $GITHUB_STEP_SUMMARY
ERRORS=0
# Find all component manifests (XML with type="component")
COMP_MANIFESTS=$(find . -maxdepth 4 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*" -exec grep -l '<extension[^>]*type="component"' {} ; 2>/dev/null || true)
if [ -z "$COMP_MANIFESTS" ]; then
echo "No component extensions found — skipping." >> $GITHUB_STEP_SUMMARY
else
for MANIFEST in $COMP_MANIFESTS; do
COMP_DIR=$(dirname "$MANIFEST")
COMP_NAME=$(basename "$COMP_DIR")
echo "Component: `${COMP_NAME}` (manifest: `${MANIFEST}`)" >> $GITHUB_STEP_SUMMARY
# Check access.xml exists
ACCESS_FILE=$(find "$COMP_DIR" -name "access.xml" -not -path "./.git/*" 2>/dev/null | head -1)
if [ -z "$ACCESS_FILE" ]; then
echo "- Missing `access.xml` — ACL permissions will not work." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
if command -v php &> /dev/null; then
if ! php -r "@simplexml_load_file('$ACCESS_FILE') ?: exit(1);" 2>/dev/null; then
echo "- `access.xml` is not well-formed XML." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
for ACTION in core.admin core.manage; do
if ! grep -q "name=\"${ACTION}\"" "$ACCESS_FILE" 2>/dev/null; then
echo "- `access.xml` missing required action: `${ACTION}`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
done
echo "- `access.xml`: valid" >> $GITHUB_STEP_SUMMARY
fi
fi
fi
# Check config.xml exists
CONFIG_FILE=$(find "$COMP_DIR" -name "config.xml" -not -path "./.git/*" 2>/dev/null | head -1)
if [ -z "$CONFIG_FILE" ]; then
echo "- Missing `config.xml` — component Options page will be empty." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
if command -v php &> /dev/null; then
if ! php -r "@simplexml_load_file('$CONFIG_FILE') ?: exit(1);" 2>/dev/null; then
echo "- `config.xml` is not well-formed XML." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "- `config.xml`: valid" >> $GITHUB_STEP_SUMMARY
fi
fi
fi
done
fi
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${ERRORS}" -gt 0 ]; then
echo "**${ERRORS} config/ACL issue(s) found.**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "**Component config & ACL check passed.**" >> $GITHUB_STEP_SUMMARY
fi
- name: SQL schema validation
run: |
echo "### SQL Schema Validation" >> $GITHUB_STEP_SUMMARY
ERRORS=0
# Find SQL files in source/htdocs
SQL_FILES=$(find . -name "*.sql" -path "*/sql/*" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null)
if [ -z "$SQL_FILES" ]; then
echo "No SQL files found — skipping." >> $GITHUB_STEP_SUMMARY
else
echo "Found $(echo "$SQL_FILES" | wc -l) SQL file(s)" >> $GITHUB_STEP_SUMMARY
for FILE in $SQL_FILES; do
# Basic syntax check: balanced parentheses, no empty files
SIZE=$(wc -c < "$FILE" | tr -d ' ')
if [ "$SIZE" -eq 0 ]; then
echo "- Empty SQL file: \`${FILE}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
continue
fi
# Check for common SQL errors
if grep -qP '^\s*$' "$FILE" && [ "$SIZE" -lt 5 ]; then
echo "- Whitespace-only SQL file: \`${FILE}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
continue
fi
echo "- \`${FILE}\`: ${SIZE} bytes" >> $GITHUB_STEP_SUMMARY
done
# Check update SQL files follow version numbering pattern
UPDATE_DIR=$(find . -path "*/sql/updates/mysql" -type d -not -path "./.git/*" 2>/dev/null | head -1)
if [ -n "$UPDATE_DIR" ]; then
BAD_NAMES=0
for UFILE in "$UPDATE_DIR"/*.sql; do
[ ! -f "$UFILE" ] && continue
BASENAME=$(basename "$UFILE" .sql)
if ! echo "$BASENAME" | grep -qP '^\d+\.\d+\.\d+'; then
echo "- Update file \`${UFILE}\` does not follow version naming (expected X.Y.Z.sql)" >> $GITHUB_STEP_SUMMARY
BAD_NAMES=$((BAD_NAMES + 1))
fi
done
if [ "$BAD_NAMES" -gt 0 ]; then
ERRORS=$((ERRORS + BAD_NAMES))
fi
fi
fi
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${ERRORS}" -gt 0 ]; then
echo "**${ERRORS} SQL issue(s) found.**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "**SQL schema validation passed.**" >> $GITHUB_STEP_SUMMARY
fi
- name: Manifest file references check
run: |
echo "### Manifest File References" >> $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 [ -z "$MANIFEST" ]; then
echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY
else
MANIFEST_DIR=$(dirname "$MANIFEST")
# Check <filename> references
FILENAMES=$(grep -oP '<filename[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null || true)
for F in $FILENAMES; do
if [ ! -f "${MANIFEST_DIR}/${F}" ] && [ ! -d "${MANIFEST_DIR}/${F}" ]; then
echo "- Missing: \`${F}\` (referenced in manifest)" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
done
# Check <folder> references
FOLDERS=$(grep -oP '<folder[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null || true)
for F in $FOLDERS; do
if [ ! -d "${MANIFEST_DIR}/${F}" ]; then
echo "- Missing folder: \`${F}\` (referenced in manifest)" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
done
# Check <file> references in package manifests (ZIP files won't exist in source)
EXT_TYPE=$(grep -oP '<extension[^>]*\btype="\K[^"]+' "$MANIFEST" | head -1)
if [ "$EXT_TYPE" != "package" ]; then
FILES=$(grep -oP '<file[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null || true)
for F in $FILES; do
if [ ! -f "${MANIFEST_DIR}/${F}" ]; then
echo "- Missing file: \`${F}\` (referenced in manifest)" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
done
fi
fi
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${ERRORS}" -gt 0 ]; then
echo "**${ERRORS} missing file reference(s).**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "**Manifest file references check passed.**" >> $GITHUB_STEP_SUMMARY
fi
- name: Form XML validation
run: |
echo "### Form XML Validation" >> $GITHUB_STEP_SUMMARY
ERRORS=0
FORM_FILES=$(find . -name "*.xml" -path "*/forms/*" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null)
if [ -z "$FORM_FILES" ]; then
echo "No form XML files found — skipping." >> $GITHUB_STEP_SUMMARY
else
echo "Found $(echo "$FORM_FILES" | wc -l) form file(s)" >> $GITHUB_STEP_SUMMARY
for FILE in $FORM_FILES; do
if command -v php &> /dev/null; then
if ! php -r "@simplexml_load_file('$FILE') ?: exit(1);" 2>/dev/null; then
echo "- \`${FILE}\`: malformed XML" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
# Check for valid Joomla form structure
if ! grep -qE '<form|<field|<fieldset' "$FILE" 2>/dev/null; then
echo "- \`${FILE}\`: no \`<form>\`, \`<field>\`, or \`<fieldset>\` elements found" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "- \`${FILE}\`: valid" >> $GITHUB_STEP_SUMMARY
fi
fi
fi
done
fi
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${ERRORS}" -gt 0 ]; then
echo "**${ERRORS} form XML issue(s).**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "**Form XML validation passed.**" >> $GITHUB_STEP_SUMMARY
fi
- name: Deprecated Joomla API check
continue-on-error: true
run: |
echo "### Deprecated Joomla API Check" >> $GITHUB_STEP_SUMMARY
WARNINGS=0
SRC_DIR=""
for DIR in source/ src/ htdocs/; do
[ -d "$DIR" ] && SRC_DIR="$DIR" && break
done
if [ -z "$SRC_DIR" ]; then
echo "No source directory found — skipping." >> $GITHUB_STEP_SUMMARY
else
# Joomla 3/4 deprecated patterns that break in Joomla 6
PATTERNS=(
'JFactory::'
'JText::'
'JHtml::'
'JRoute::'
'JUri::'
'JLog::'
'JTable::'
'JInput'
'CMSFactory::\$application'
'JApplicationCms'
)
for PATTERN in "${PATTERNS[@]}"; do
HITS=$(grep -rnl "$PATTERN" "$SRC_DIR" --include="*.php" 2>/dev/null || true)
if [ -n "$HITS" ]; then
COUNT=$(echo "$HITS" | wc -l)
echo "- \`${PATTERN}\` found in ${COUNT} file(s)" >> $GITHUB_STEP_SUMMARY
WARNINGS=$((WARNINGS + COUNT))
fi
done
echo "" >> $GITHUB_STEP_SUMMARY
if [ "$WARNINGS" -gt 0 ]; then
echo "**${WARNINGS} deprecated API usage(s) found.** These will break in Joomla 6." >> $GITHUB_STEP_SUMMARY
else
echo "**No deprecated APIs found.**" >> $GITHUB_STEP_SUMMARY
fi
fi
- name: Template output escaping check
continue-on-error: true
run: |
echo "### Template Output Escaping" >> $GITHUB_STEP_SUMMARY
WARNINGS=0
TMPL_FILES=$(find . -name "*.php" -path "*/tmpl/*" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null)
if [ -z "$TMPL_FILES" ]; then
echo "No template files found — skipping." >> $GITHUB_STEP_SUMMARY
else
echo "Found $(echo "$TMPL_FILES" | wc -l) template file(s)" >> $GITHUB_STEP_SUMMARY
for FILE in $TMPL_FILES; do
# Check for unescaped output: <?= $var ?> or echo $var without escape()
UNESCAPED=$(grep -nP '<\?=\s*\$(?!this->escape)' "$FILE" 2>/dev/null || true)
if [ -n "$UNESCAPED" ]; then
HITS=$(echo "$UNESCAPED" | wc -l)
echo "- \`${FILE}\`: ${HITS} unescaped \`<?= \$var ?>\` output(s) — use \`<?= \$this->escape(\$var) ?>\`" >> $GITHUB_STEP_SUMMARY
WARNINGS=$((WARNINGS + HITS))
fi
# Check for echo without escaping in template context
RAW_ECHO=$(grep -nP '^\s*echo\s+\$(?!this->escape)' "$FILE" 2>/dev/null || true)
if [ -n "$RAW_ECHO" ]; then
HITS=$(echo "$RAW_ECHO" | wc -l)
echo "- \`${FILE}\`: ${HITS} raw \`echo \$var\` — consider \`echo \$this->escape(\$var)\`" >> $GITHUB_STEP_SUMMARY
WARNINGS=$((WARNINGS + HITS))
fi
done
echo "" >> $GITHUB_STEP_SUMMARY
if [ "$WARNINGS" -gt 0 ]; then
echo "**${WARNINGS} potential XSS risk(s) in templates.** Review unescaped output." >> $GITHUB_STEP_SUMMARY
else
echo "**All template output appears properly escaped.**" >> $GITHUB_STEP_SUMMARY
fi
fi
- name: Namespace consistency check
run: |
echo "### Namespace Consistency" >> $GITHUB_STEP_SUMMARY
ERRORS=0
# Find component/plugin manifests with <namespace> tags
MANIFESTS=$(find . -maxdepth 4 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*" -exec grep -l '<namespace' {} \; 2>/dev/null || true)
if [ -z "$MANIFESTS" ]; then
echo "No manifests with \`<namespace>\` found — skipping." >> $GITHUB_STEP_SUMMARY
else
for MANIFEST in $MANIFESTS; do
NS_PATH=$(grep -oP '<namespace[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1)
[ -z "$NS_PATH" ] && continue
MANIFEST_DIR=$(dirname "$MANIFEST")
echo "Manifest: \`${MANIFEST}\` → namespace \`${NS_PATH}\`" >> $GITHUB_STEP_SUMMARY
# Check PHP files have matching namespace
while IFS= read -r -d '' PHP_FILE; do
FILE_NS=$(grep -oP '^\s*namespace\s+\K[^;]+' "$PHP_FILE" 2>/dev/null | head -1)
[ -z "$FILE_NS" ] && continue
# Namespace should start with the manifest namespace path
if ! echo "$FILE_NS" | grep -qF "${NS_PATH}"; then
echo "- \`${PHP_FILE}\`: namespace \`${FILE_NS}\` doesn't match manifest \`${NS_PATH}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
done < <(find "$MANIFEST_DIR" -name "*.php" -path "*/src/*" -not -path "./vendor/*" -print0 2>/dev/null)
done
fi
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${ERRORS}" -gt 0 ]; then
echo "**${ERRORS} namespace mismatch(es).**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "**Namespace consistency check passed.**" >> $GITHUB_STEP_SUMMARY
fi
- name: SPDX license header check
continue-on-error: true
run: |
echo "### SPDX License Headers" >> $GITHUB_STEP_SUMMARY
MISSING=0
SRC_DIR=""
for DIR in source/ src/ htdocs/; do
[ -d "$DIR" ] && SRC_DIR="$DIR" && break
done
if [ -z "$SRC_DIR" ]; then
echo "No source directory found — skipping." >> $GITHUB_STEP_SUMMARY
else
TOTAL=0
while IFS= read -r -d '' FILE; do
TOTAL=$((TOTAL + 1))
if ! head -10 "$FILE" | grep -qi "SPDX"; then
echo "- Missing SPDX header: \`${FILE}\`" >> $GITHUB_STEP_SUMMARY
MISSING=$((MISSING + 1))
fi
done < <(find "$SRC_DIR" -name "*.php" -not -path "./vendor/*" -print0)
echo "" >> $GITHUB_STEP_SUMMARY
if [ "$MISSING" -gt 0 ]; then
echo "**${MISSING}/${TOTAL} PHP file(s) missing SPDX license header.**" >> $GITHUB_STEP_SUMMARY
else
echo "**All ${TOTAL} PHP files have SPDX headers.**" >> $GITHUB_STEP_SUMMARY
fi
fi
- name: Service provider check
run: |
echo "### Service Provider Check" >> $GITHUB_STEP_SUMMARY
ERRORS=0
PROVIDERS=$(find . -name "provider.php" -path "*/services/*" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null)
if [ -z "$PROVIDERS" ]; then
echo "No service providers found — skipping." >> $GITHUB_STEP_SUMMARY
else
for FILE in $PROVIDERS; do
# Must return a ServiceProviderInterface
if ! grep -qP 'ServiceProviderInterface|ComponentInterface|MVCFactoryInterface|DispatcherInterface' "$FILE" 2>/dev/null; then
echo "- \`${FILE}\`: does not reference ServiceProviderInterface or component interfaces" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "- \`${FILE}\`: valid service provider" >> $GITHUB_STEP_SUMMARY
fi
# Must have return statement
if ! grep -qP '^\s*return\s+new\s+' "$FILE" 2>/dev/null; then
echo "- \`${FILE}\`: missing \`return new ...\` statement" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
done
fi
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${ERRORS}" -gt 0 ]; then
echo "**${ERRORS} service provider issue(s).**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "**Service provider check passed.**" >> $GITHUB_STEP_SUMMARY
fi
- name: Script file reference check
run: |
echo "### Script File Reference" >> $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 [ -z "$MANIFEST" ]; then
echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY
else
MANIFEST_DIR=$(dirname "$MANIFEST")
SCRIPT_FILE=$(grep -oP '<scriptfile>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1)
if [ -z "$SCRIPT_FILE" ]; then
echo "No \`<scriptfile>\` referenced — skipping." >> $GITHUB_STEP_SUMMARY
elif [ ! -f "${MANIFEST_DIR}/${SCRIPT_FILE}" ]; then
echo "::error file=${MANIFEST}::Manifest references \`<scriptfile>${SCRIPT_FILE}</scriptfile>\` but file does not exist"
echo "- **Missing** \`${SCRIPT_FILE}\` — referenced in \`<scriptfile>\` but not found" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "- \`${SCRIPT_FILE}\`: present ✓" >> $GITHUB_STEP_SUMMARY
fi
fi
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${ERRORS}" -gt 0 ]; then
echo "**${ERRORS} script file issue(s).**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "**Script file reference check passed.**" >> $GITHUB_STEP_SUMMARY
fi
- name: Media folder validation
run: |
echo "### Media Folder Validation" >> $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 [ -z "$MANIFEST" ]; then
echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY
else
MANIFEST_DIR=$(dirname "$MANIFEST")
# Check <media> tag and its folder/filename children
MEDIA_DEST=$(grep -oP '<media[^>]*\bdestination="\K[^"]+' "$MANIFEST" 2>/dev/null | head -1)
MEDIA_FOLDER=$(grep -oP '<media[^>]*\bfolder="\K[^"]+' "$MANIFEST" 2>/dev/null | head -1)
if [ -z "$MEDIA_DEST" ] && [ -z "$MEDIA_FOLDER" ]; then
echo "No \`<media>\` tag found — skipping." >> $GITHUB_STEP_SUMMARY
else
if [ -n "$MEDIA_FOLDER" ] && [ ! -d "${MANIFEST_DIR}/${MEDIA_FOLDER}" ]; then
echo "::error file=${MANIFEST}::\`<media folder=\"${MEDIA_FOLDER}\">\` references missing directory"
echo "- **Missing** media folder \`${MEDIA_FOLDER}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "- Media folder \`${MEDIA_FOLDER:-(inline)}\`: present ✓" >> $GITHUB_STEP_SUMMARY
# Check child references inside <media> block
if [ -n "$MEDIA_FOLDER" ]; then
MEDIA_FOLDERS=$(sed -n '/<media /,/<\/media>/p' "$MANIFEST" | grep -oP '<folder>\K[^<]+' 2>/dev/null || true)
for F in $MEDIA_FOLDERS; do
if [ ! -d "${MANIFEST_DIR}/${MEDIA_FOLDER}/${F}" ]; then
echo "- **Missing** media subfolder \`${MEDIA_FOLDER}/${F}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
done
MEDIA_FILES=$(sed -n '/<media /,/<\/media>/p' "$MANIFEST" | grep -oP '<filename>\K[^<]+' 2>/dev/null || true)
for F in $MEDIA_FILES; do
if [ ! -f "${MANIFEST_DIR}/${MEDIA_FOLDER}/${F}" ]; then
echo "- **Missing** media file \`${MEDIA_FOLDER}/${F}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
done
fi
fi
fi
fi
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${ERRORS}" -gt 0 ]; then
echo "**${ERRORS} media reference issue(s).**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "**Media folder validation passed.**" >> $GITHUB_STEP_SUMMARY
fi
- name: Target platform check
continue-on-error: true
run: |
echo "### Target Platform Check" >> $GITHUB_STEP_SUMMARY
WARNINGS=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 [ -z "$MANIFEST" ]; then
echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY
else
# Check updates.xml for targetplatform if it exists
if [ -f "updates.xml" ]; then
if ! grep -q '<targetplatform' "updates.xml" 2>/dev/null; then
echo "::warning file=updates.xml::No \`<targetplatform>\` found — Joomla updater cannot filter by compatible version"
echo "- **Missing** \`<targetplatform>\` in updates.xml" >> $GITHUB_STEP_SUMMARY
WARNINGS=$((WARNINGS + 1))
else
echo "- \`<targetplatform>\` in updates.xml: present ✓" >> $GITHUB_STEP_SUMMARY
fi
fi
# Check manifest for minimum PHP/Joomla version hints
if ! grep -qP '<php_minimum>|targetplatform|joomla.*version' "$MANIFEST" 2>/dev/null; then
echo "::warning file=${MANIFEST}::No minimum Joomla or PHP version constraint found in manifest"
echo "- **Missing** version constraints (\`<php_minimum>\` or \`<targetplatform>\`)" >> $GITHUB_STEP_SUMMARY
WARNINGS=$((WARNINGS + 1))
else
echo "- Version constraints in manifest: present ✓" >> $GITHUB_STEP_SUMMARY
fi
fi
echo "" >> $GITHUB_STEP_SUMMARY
if [ "$WARNINGS" -gt 0 ]; then
echo "**${WARNINGS} target platform warning(s).**" >> $GITHUB_STEP_SUMMARY
else
echo "**Target platform check passed.**" >> $GITHUB_STEP_SUMMARY
fi
- name: Changelog URL check
continue-on-error: true
run: |
echo "### Changelog URL Check" >> $GITHUB_STEP_SUMMARY
WARNINGS=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 [ -z "$MANIFEST" ]; then
echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY
else
if ! grep -q '<changelogurl>' "$MANIFEST" 2>/dev/null; then
echo "::warning file=${MANIFEST}::Missing \`<changelogurl>\` — Joomla updater will not display changelogs"
echo "- **Missing** \`<changelogurl>\` — Joomla 4+ shows changelogs in the update manager when this is set" >> $GITHUB_STEP_SUMMARY
WARNINGS=$((WARNINGS + 1))
else
CHANGELOG_URL=$(grep -oP '<changelogurl>\K[^<]+' "$MANIFEST" | head -1)
echo "- \`<changelogurl>\`: \`${CHANGELOG_URL}\` ✓" >> $GITHUB_STEP_SUMMARY
fi
fi
echo "" >> $GITHUB_STEP_SUMMARY
if [ "$WARNINGS" -gt 0 ]; then
echo "**${WARNINGS} changelog URL warning(s).**" >> $GITHUB_STEP_SUMMARY
else
echo "**Changelog URL check passed.**" >> $GITHUB_STEP_SUMMARY
fi
- name: Duplicate file references check
continue-on-error: true
run: |
echo "### Duplicate File References" >> $GITHUB_STEP_SUMMARY
WARNINGS=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 [ -z "$MANIFEST" ]; then
echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY
else
# Extract all <filename> and <folder> references
ALL_REFS=$(grep -oP '<(filename|folder)[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null | sort || true)
if [ -z "$ALL_REFS" ]; then
echo "No file/folder references found — skipping." >> $GITHUB_STEP_SUMMARY
else
DUPES=$(echo "$ALL_REFS" | uniq -d)
if [ -n "$DUPES" ]; then
while IFS= read -r DUP; do
COUNT=$(echo "$ALL_REFS" | grep -cx "$DUP")
echo "::warning file=${MANIFEST}::Duplicate reference: \`${DUP}\` appears ${COUNT} times (may be valid if in different sections)"
echo "- **Duplicate:** \`${DUP}\` (${COUNT}x) — check if cross-section" >> $GITHUB_STEP_SUMMARY
WARNINGS=$((WARNINGS + 1))
done <<< "$DUPES"
else
TOTAL=$(echo "$ALL_REFS" | wc -l)
echo "All ${TOTAL} file/folder references are unique." >> $GITHUB_STEP_SUMMARY
fi
fi
fi
echo "" >> $GITHUB_STEP_SUMMARY
if [ "$WARNINGS" -gt 0 ]; then
echo "**${WARNINGS} duplicate reference(s) found.** Review for cross-section validity." >> $GITHUB_STEP_SUMMARY
else
echo "**Duplicate file references check passed.**" >> $GITHUB_STEP_SUMMARY
fi
- name: Empty language keys check
continue-on-error: true
run: |
echo "### Empty Language Keys" >> $GITHUB_STEP_SUMMARY
WARNINGS=0
LANG_FILES=$(find . -name "*.ini" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null)
if [ -z "$LANG_FILES" ]; then
echo "No .ini language files found — skipping." >> $GITHUB_STEP_SUMMARY
else
TOTAL_FILES=0
for FILE in $LANG_FILES; do
TOTAL_FILES=$((TOTAL_FILES + 1))
# Find lines with KEY= but no value (empty or whitespace-only after =)
EMPTY_KEYS=$(grep -nP '^[A-Z_]+=\s*$' "$FILE" 2>/dev/null || true)
if [ -n "$EMPTY_KEYS" ]; then
COUNT=$(echo "$EMPTY_KEYS" | wc -l)
echo "::warning file=${FILE}::${COUNT} empty language key(s)"
echo "- \`${FILE}\`: ${COUNT} empty key(s)" >> $GITHUB_STEP_SUMMARY
while IFS= read -r LINE; do
LINE_NUM=$(echo "$LINE" | cut -d: -f1)
KEY=$(echo "$LINE" | cut -d: -f2 | cut -d= -f1)
echo " - Line ${LINE_NUM}: \`${KEY}\`" >> $GITHUB_STEP_SUMMARY
done <<< "$EMPTY_KEYS"
WARNINGS=$((WARNINGS + COUNT))
fi
done
if [ "$WARNINGS" -eq 0 ]; then
echo "All ${TOTAL_FILES} language file(s) have populated keys." >> $GITHUB_STEP_SUMMARY
fi
fi
echo "" >> $GITHUB_STEP_SUMMARY
if [ "$WARNINGS" -gt 0 ]; then
echo "**${WARNINGS} empty language key(s) across ${TOTAL_FILES} file(s).**" >> $GITHUB_STEP_SUMMARY
else
echo "**Empty language keys check passed.**" >> $GITHUB_STEP_SUMMARY
fi
release-readiness: release-readiness:
name: Release Readiness Check name: Release Readiness Check
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event_name == 'pull_request' && github.base_ref == 'main' if: github.event_name == 'pull_request' && github.base_ref == 'main'
continue-on-error: true
steps: steps:
- name: Checkout repository - name: Checkout repository
-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
-4
View File
@@ -25,10 +25,6 @@
name: "Universal: Secret Scanning" name: "Universal: Secret Scanning"
on: on:
pull_request:
branches:
- main
- 'dev/**'
schedule: schedule:
- cron: '0 5 * * 1' # Weekly Monday 05:00 UTC - cron: '0 5 * * 1' # Weekly Monday 05:00 UTC
workflow_dispatch: workflow_dispatch:
+3 -3
View File
@@ -4,8 +4,8 @@
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: mokoplatform.Automation # INGROUP: mokocli.Automation
# VERSION: 01.02.00 # VERSION: 01.08.37
# BRIEF: Auto-create feature branch when an issue is opened # BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch" name: "Universal: Issue Branch"
@@ -28,7 +28,7 @@ jobs:
steps: steps:
- name: Create branch and comment - name: Create branch and comment
run: | run: |
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" TOKEN="${{ secrets.GA_TOKEN }}"
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
ISSUE_NUM="${{ github.event.issue.number }}" ISSUE_NUM="${{ github.event.issue.number }}"
ISSUE_TITLE="${{ github.event.issue.title }}" ISSUE_TITLE="${{ github.event.issue.title }}"
+34 -35
View File
@@ -4,8 +4,8 @@
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: mokoplatform.CI # INGROUP: moko-platform.CI
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokoplatform # REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# PATH: /templates/workflows/universal/pr-check.yml.template # PATH: /templates/workflows/universal/pr-check.yml.template
# VERSION: 09.23.00 # VERSION: 09.23.00
# BRIEF: PR gate — branch policy + code validation before merge # BRIEF: PR gate — branch policy + code validation before merge
@@ -96,6 +96,32 @@ jobs:
echo "Branch policy: OK (${HEAD} → ${BASE})" echo "Branch policy: OK (${HEAD} → ${BASE})"
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
# ── Secret Scanning ──────────────────────────────────────────────────
gitleaks:
name: 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
- name: Scan PR commits for secrets
run: |
if gitleaks detect --source . --verbose \
--log-opts=${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }} 2>&1; then
echo "**No secrets detected.**" >> $GITHUB_STEP_SUMMARY
else
echo "::error::Potential secrets detected in PR commits"
exit 1
fi
# ── Code Validation ──────────────────────────────────────────────────── # ── Code Validation ────────────────────────────────────────────────────
validate: validate:
name: Validate PR name: Validate PR
@@ -159,11 +185,11 @@ jobs:
echo "::error file=${file}::Missing JEXEC guard: ${file}" echo "::error file=${file}::Missing JEXEC guard: ${file}"
ERRORS=$((ERRORS + 1)) ERRORS=$((ERRORS + 1))
fi fi
done < <(find . -name "*.php" \( -path "*/source/*" -o -path "*/src/*" \) -not -path "./.git/*" -not -path "./vendor/*" -print0) done < <(find . -name "*.php" -path "*/src/*" -not -path "./.git/*" -not -path "./vendor/*" -print0)
if [ "$ERRORS" -gt 0 ]; then if [ "$ERRORS" -gt 0 ]; then
echo "::error::${ERRORS} PHP file(s) missing defined('_JEXEC') or die guard" echo "::error::${ERRORS} PHP file(s) missing defined('_JEXEC') or die guard"
echo "## JEXEC Guard Check: Failed" >> $GITHUB_STEP_SUMMARY echo "## JEXEC Guard Check: Failed" >> $GITHUB_STEP_SUMMARY
echo "${ERRORS} file(s) are missing the Joomla execution guard." >> $GITHUB_STEP_SUMMARY echo "${ERRORS} file(s) in src/ are missing the Joomla execution guard." >> $GITHUB_STEP_SUMMARY
exit 1 exit 1
fi fi
echo "JEXEC guard: OK" echo "JEXEC guard: OK"
@@ -172,8 +198,7 @@ jobs:
if: steps.platform.outputs.platform == 'joomla' if: steps.platform.outputs.platform == 'joomla'
run: | run: |
MISSING=0 MISSING=0
SOURCE_DIR="source" SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && exit 0 [ ! -d "$SOURCE_DIR" ] && exit 0
while IFS= read -r dir; do while IFS= read -r dir; do
if [ ! -f "${dir}/index.html" ]; then if [ ! -f "${dir}/index.html" ]; then
@@ -221,7 +246,7 @@ jobs:
echo "joomla.asset.json: valid" echo "joomla.asset.json: valid"
fi fi
# Validate all XML files in source/src are well-formed # Validate all XML files in src/ are well-formed
XML_ERRORS=0 XML_ERRORS=0
if command -v php &> /dev/null; then if command -v php &> /dev/null; then
while IFS= read -r -d '' xmlfile; do while IFS= read -r -d '' xmlfile; do
@@ -450,38 +475,12 @@ jobs:
fi fi
echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]" echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]"
- name: Require README and CHANGELOG in PR diff
run: |
BASE="${{ github.base_ref }}"
HEAD="${{ github.head_ref }}"
CHANGED=$(git diff --name-only "origin/${BASE}...HEAD" 2>/dev/null || git diff --name-only HEAD~1 2>/dev/null || echo "")
SOURCE_CHANGED=$(echo "$CHANGED" | grep -E '^source/|^src/' || true)
if [ -z "$SOURCE_CHANGED" ]; then
echo "No source changes — skipping README/CHANGELOG diff check"
exit 0
fi
ERRORS=0
if ! echo "$CHANGED" | grep -q '^CHANGELOG.md$'; then
echo "::error::Source code was modified but CHANGELOG.md was not updated."
ERRORS=$((ERRORS + 1))
fi
if ! echo "$CHANGED" | grep -q '^README.md$'; then
echo "::warning::Source code was modified but README.md was not updated."
fi
if [ "$ERRORS" -gt 0 ]; then
echo "## Documentation Check: Failed" >> $GITHUB_STEP_SUMMARY
echo "Source code was modified but CHANGELOG.md was not updated." >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "Documentation diff check: OK"
- name: Verify package source - name: Verify package source
run: | run: |
SOURCE_DIR="source" SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
if [ ! -d "$SOURCE_DIR" ]; then if [ ! -d "$SOURCE_DIR" ]; then
echo "::warning::No source/, src/, or htdocs/ directory" echo "::warning::No src/ or htdocs/ directory"
exit 0 exit 0
fi fi
FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l) FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
@@ -0,0 +1,71 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Validation
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /templates/workflows/joomla/pr-metadata-check.yml.template
# VERSION: 01.00.00
# BRIEF: Validate MokoGitea metadata matches Joomla extension manifest on PRs
name: "Joomla: Metadata Validation"
on:
pull_request:
types: [opened, synchronize, reopened, converted_to_draft, ready_for_review]
permissions:
contents: read
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:
validate-metadata:
name: "Validate Joomla Metadata"
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup mokocli tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
run: |
if [ -f /opt/mokocli/cli/joomla_metadata_validate.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then
echo Using pre-installed /opt/mokocli
echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV
else
echo Falling back to fresh clone
if ! command -v composer > /dev/null 2>&1; 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
rm -rf /tmp/mokocli
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
fi
- name: Validate metadata against Joomla manifest
env:
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
run: |
php ${MOKO_CLI}/joomla_metadata_validate.php \
--path . \
--token "${GITEA_TOKEN}" \
--org "${GITEA_ORG}" \
--repo "${GITEA_REPO}" \
--api-base "${GITEA_URL}/api/v1" \
--ci
if [ $? -ne 0 ]; then
echo "::error::Joomla metadata mismatch — update delivery will fail. Run 'php cli/joomla_metadata_validate.php' locally to see details."
exit 1
fi
+52 -27
View File
@@ -4,23 +4,26 @@
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: mokoplatform.Release # INGROUP: mokocli.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform # REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /templates/workflows/universal/pre-release.yml.template # PATH: /templates/workflows/universal/pre-release.yml.template
# VERSION: 05.01.00 # VERSION: 05.01.00
# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch # BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches
name: "Universal: Pre-Release" name: "Universal: Pre-Release"
on: on:
pull_request: push:
types: [closed]
branches: branches:
- dev - dev
pull_request_target: - 'fix/**'
types: [synchronize, opened, reopened] - 'patch/**'
branches: - 'hotfix/**'
- main - 'bugfix/**'
- 'chore/**'
- alpha
- beta
- rc
workflow_dispatch: workflow_dispatch:
inputs: inputs:
stability: stability:
@@ -43,12 +46,11 @@ env:
jobs: jobs:
build: build:
name: "Build Pre-Release (${{ inputs.stability || 'development' }})" name: "Build Pre-Release (${{ inputs.stability || github.ref_name }})"
runs-on: release runs-on: release
if: >- if: >-
github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_dispatch' ||
(github.event_name == 'pull_request' && github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev') || github.event_name == 'push'
(github.event_name == 'pull_request_target' && github.event.pull_request.base.ref == 'main')
steps: steps:
- name: Checkout - name: Checkout
@@ -56,40 +58,59 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
token: ${{ secrets.MOKOGITEA_TOKEN }} token: ${{ secrets.MOKOGITEA_TOKEN }}
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || '' }} ref: ${{ github.ref_name }}
- name: Setup mokoplatform tools - name: Setup mokocli tools
env: env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
run: | run: |
# Use pre-installed /opt/mokoplatform if available (updated by cron every 6h) # Use pre-installed /opt/mokocli if available (updated by cron every 6h)
if [ -f /opt/mokoplatform/cli/version_bump.php ] && [ -f /opt/mokoplatform/cli/manifest_element.php ] && [ -f /opt/mokoplatform/vendor/autoload.php ]; then if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/cli/manifest_element.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then
echo Using pre-installed /opt/mokoplatform echo Using pre-installed /opt/mokocli
echo MOKO_CLI=/opt/mokoplatform/cli >> $GITHUB_ENV echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV
else else
echo Falling back to fresh clone echo Falling back to fresh clone
if ! command -v composer > /dev/null 2>&1; then if ! command -v composer > /dev/null 2>&1; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1 sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
fi fi
rm -rf /tmp/mokoplatform-api rm -rf /tmp/mokocli
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokoplatform.git CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokoplatform-api git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
cd /tmp/mokoplatform-api && composer install --no-dev --no-interaction --quiet cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
echo MOKO_CLI=/tmp/mokoplatform-api/cli >> $GITHUB_ENV echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
fi fi
- name: Detect platform - name: Detect platform
id: platform id: platform
run: | run: |
# Auto-detect and update platform if not set in manifest
php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true
php ${MOKO_CLI}/manifest_read.php --path . --github-output php ${MOKO_CLI}/manifest_read.php --path . --github-output
- name: Check platform eligibility (Joomla only)
id: eligibility
run: |
PLATFORM="${{ steps.platform.outputs.platform }}"
if [[ "$PLATFORM" == joomla* ]] || [[ "$PLATFORM" == "joomla" ]]; then
echo "proceed=true" >> "$GITHUB_OUTPUT"
else
echo "proceed=false" >> "$GITHUB_OUTPUT"
echo "::notice::Platform '$PLATFORM' — non-Joomla, skipping pre-release auto-bump"
fi
- name: Resolve metadata and bump version - name: Resolve metadata and bump version
id: meta id: meta
if: steps.eligibility.outputs.proceed == 'true'
run: | run: |
# Auto-detect stability: RC for PRs targeting main, else use input or default to development # Auto-detect stability from branch name on push, or use input on dispatch
if [ "${{ github.event_name }}" = "pull_request_target" ] && [ "${{ github.event.pull_request.base.ref }}" = "main" ]; then if [ "${{ github.event_name }}" = "push" ]; then
STABILITY="release-candidate" case "${{ github.ref_name }}" in
rc) STABILITY="release-candidate" ;;
alpha) STABILITY="alpha" ;;
beta) STABILITY="beta" ;;
*) STABILITY="development" ;;
esac
else else
STABILITY="${{ inputs.stability || 'development' }}" STABILITY="${{ inputs.stability || 'development' }}"
fi fi
@@ -157,6 +178,7 @@ jobs:
- name: Create release - name: Create release
id: release id: release
if: steps.eligibility.outputs.proceed == 'true'
run: | run: |
TAG="${{ steps.meta.outputs.tag }}" TAG="${{ steps.meta.outputs.tag }}"
VERSION="${{ steps.meta.outputs.version }}" VERSION="${{ steps.meta.outputs.version }}"
@@ -164,9 +186,10 @@ jobs:
php ${MOKO_CLI}/release_create.php \ php ${MOKO_CLI}/release_create.php \
--path . --version "$VERSION" --tag "$TAG" \ --path . --version "$VERSION" --tag "$TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --branch dev --prerelease --repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease
- name: Update release notes from CHANGELOG.md - name: Update release notes from CHANGELOG.md
if: steps.eligibility.outputs.proceed == 'true'
run: | run: |
TAG="${{ steps.meta.outputs.tag }}" TAG="${{ steps.meta.outputs.tag }}"
VERSION="${{ steps.meta.outputs.version }}" VERSION="${{ steps.meta.outputs.version }}"
@@ -203,6 +226,7 @@ jobs:
- name: Build package and upload - name: Build package and upload
id: package id: package
if: steps.eligibility.outputs.proceed == 'true'
run: | run: |
VERSION="${{ steps.meta.outputs.version }}" VERSION="${{ steps.meta.outputs.version }}"
TAG="${{ steps.meta.outputs.tag }}" TAG="${{ steps.meta.outputs.tag }}"
@@ -216,6 +240,7 @@ jobs:
# No need to build, commit, or sync updates.xml from workflows # No need to build, commit, or sync updates.xml from workflows
- name: "Delete lesser pre-release channels (cascade)" - name: "Delete lesser pre-release channels (cascade)"
if: steps.eligibility.outputs.proceed == 'true'
continue-on-error: true continue-on-error: true
run: | run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
+66
View File
@@ -0,0 +1,66 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /.mokogitea/workflows/rc-revert.yml
# VERSION: 09.23.00
# BRIEF: Rename rc/ branch back to dev/ when PR is closed without merge
name: "RC Revert"
on:
pull_request:
types: [closed]
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
revert:
name: Rename rc/ back to dev/
runs-on: ubuntu-latest
if: >-
github.event.pull_request.merged == false &&
startsWith(github.event.pull_request.head.ref, 'rc/')
steps:
- name: Rename branch
run: |
BRANCH="${{ github.event.pull_request.head.ref }}"
SUFFIX="${BRANCH#rc/}"
DEV_BRANCH="dev/${SUFFIX}"
API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
# Create dev/ branch from rc/ branch
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X POST \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"new_branch_name\": \"${DEV_BRANCH}\", \"old_branch_name\": \"${BRANCH}\"}" \
"${API}" 2>/dev/null || true)
if [ "$STATUS" = "201" ]; then
echo "Created branch: ${DEV_BRANCH}" >> $GITHUB_STEP_SUMMARY
else
echo "::error::Failed to create ${DEV_BRANCH} from ${BRANCH} (HTTP ${STATUS})"
exit 1
fi
# Delete rc/ branch
ENCODED=$(php -r "echo rawurlencode('${BRANCH}');")
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \
-H "Authorization: token ${TOKEN}" \
"${API}/${ENCODED}" 2>/dev/null || true)
if [ "$STATUS" = "204" ]; then
echo "Deleted branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
else
echo "::warning::Failed to delete ${BRANCH} (HTTP ${STATUS})"
fi
echo "### RC Reverted" >> $GITHUB_STEP_SUMMARY
echo "${BRANCH} → ${DEV_BRANCH}" >> $GITHUB_STEP_SUMMARY
+8 -9
View File
@@ -7,8 +7,8 @@
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: mokoplatform.Validation # INGROUP: mokocli.Validation
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokoplatform # REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli
# PATH: /templates/workflows/joomla/repo_health.yml.template # PATH: /templates/workflows/joomla/repo_health.yml.template
# VERSION: 09.23.00 # VERSION: 09.23.00
# BRIEF: Enforces repository guardrails by validating scripts governance, tooling availability, and core repository health artifacts. # BRIEF: Enforces repository guardrails by validating scripts governance, tooling availability, and core repository health artifacts.
@@ -33,7 +33,8 @@ on:
- scripts - scripts
- repo - repo
pull_request: pull_request:
push: branches:
- main
permissions: permissions:
contents: read contents: read
@@ -296,19 +297,17 @@ jobs:
missing_required=() missing_required=()
missing_optional=() missing_optional=()
# Source directory: source/, src/, or htdocs/ (any is valid for extension repos) # Source directory: src/ or htdocs/ (either is valid for extension repos)
SOURCE_DIR="" SOURCE_DIR=""
if [ -d "source" ]; then if [ -d "src" ]; then
SOURCE_DIR="source"
elif [ -d "src" ]; then
SOURCE_DIR="src" SOURCE_DIR="src"
elif [ -d "htdocs" ]; then elif [ -d "htdocs" ]; then
SOURCE_DIR="htdocs" SOURCE_DIR="htdocs"
elif [ -d "deploy" ] || [ -d "cli" ] || [ -d "monitoring" ]; then elif [ -d "deploy" ] || [ -d "cli" ] || [ -d "monitoring" ]; then
# Platform/tooling repos don't need source/ # Platform/tooling repos don't need src/
SOURCE_DIR="" SOURCE_DIR=""
else else
missing_required+=("source/ or src/ or htdocs/ (source directory required)") missing_required+=("src/ or htdocs/ (source directory required)")
fi fi
for item in "${required_artifacts[@]}"; do for item in "${required_artifacts[@]}"; do
-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
-312
View File
@@ -1,312 +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.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /templates/workflows/update-server.yml
# VERSION: 05.00.00
# BRIEF: Pre-release build + update server XML for dev/alpha/beta/rc branches
#
# Thin wrapper around moko-platform CLI tools.
# Builds packages, updates updates.xml, and optionally deploys via SFTP.
#
# Joomla filters update entries by the user's "Minimum Stability" setting.
name: "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 Server
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@v4
with:
token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 0
- name: Setup moko-platform tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.MOKOGITEA_TOKEN }}"}}}'
run: |
if ! command -v composer &> /dev/null; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
fi
# Always fetch latest CLI tools — never use stale cache from previous runs
rm -rf /tmp/moko-platform
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
/tmp/moko-platform 2>/dev/null || true
if [ -d "/tmp/moko-platform" ] && [ -f "/tmp/moko-platform/composer.json" ]; then
cd /tmp/moko-platform && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
fi
echo "MOKO_CLI=/tmp/moko-platform/cli" >> "$GITHUB_ENV"
- name: Detect platform
id: platform
run: php ${MOKO_CLI}/manifest_read.php --path . --github-output
- name: Resolve stability and bump version
id: meta
run: |
BRANCH="${{ github.ref_name }}"
# Configure git for bot pushes
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
# Auto-bump patch version
php ${MOKO_CLI}/version_bump.php --path . 2>/dev/null || true
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "0.0.0")
# Strip any existing suffix before applying stability
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
# Determine stability from branch or manual input
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
STABILITY="${{ inputs.stability }}"
elif [[ "$BRANCH" == rc/* ]]; then
STABILITY="rc"
elif [[ "$BRANCH" == beta/* ]]; then
STABILITY="beta"
elif [[ "$BRANCH" == alpha/* ]]; then
STABILITY="alpha"
else
STABILITY="development"
fi
# Version suffix per stability stream
case "$STABILITY" in
development) SUFFIX="-dev"; TAG="development" ;;
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
beta) SUFFIX="-beta"; TAG="beta" ;;
rc) SUFFIX="-rc"; TAG="release-candidate" ;;
*) SUFFIX=""; TAG="stable" ;;
esac
# Propagate version with stability suffix to all manifest files
php ${MOKO_CLI}/version_set_platform.php \
--path . --version "$VERSION" --branch "$BRANCH" --stability "$STABILITY" 2>/dev/null || true
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
# Re-read version (now includes suffix from version_set_platform)
if [ -n "$SUFFIX" ]; then
VERSION="${VERSION}${SUFFIX}"
fi
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "display_version=${VERSION}" >> "$GITHUB_OUTPUT"
# Commit version bump if changed
git add -A
git diff --cached --quiet || {
git commit -m "chore(version): auto-bump ${VERSION} [skip ci]" \
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
git push
}
- name: Create release and upload package
id: package
run: |
VERSION="${{ steps.meta.outputs.version }}"
TAG="${{ steps.meta.outputs.tag }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
# Create or update Gitea release
php ${MOKO_CLI}/release_create.php \
--path . --version "$VERSION" --tag "$TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease
# Build package and upload
php ${MOKO_CLI}/release_package.php \
--path . --version "$VERSION" --tag "$TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --output /tmp || true
- name: Update updates.xml
if: steps.platform.outputs.platform == 'joomla'
run: |
VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
SHA256="${{ steps.package.outputs.sha256_zip }}"
if [ ! -f "updates.xml" ]; then
echo "No updates.xml — skipping"
exit 0
fi
SHA_FLAG=""
[ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
php ${MOKO_CLI}/updates_xml_build.php \
--path . --version "${VERSION}" --stability "${STABILITY}" \
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
${SHA_FLAG}
# Commit and push updates.xml
git add updates.xml
git diff --cached --quiet || {
git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
git push
}
- name: Sync updates.xml to main
if: github.ref_name != 'main' && steps.platform.outputs.platform == 'joomla'
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
GITEA_TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
FILE_SHA=$(curl -sf -H "Authorization: token ${GITEA_TOKEN}" \
"${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then
python3 -c "
import base64, json, urllib.request, sys
with open('updates.xml', 'rb') as f:
content = base64.b64encode(f.read()).decode()
payload = json.dumps({
'content': content,
'sha': '${FILE_SHA}',
'message': 'chore: sync updates.xml from ${{ steps.meta.outputs.stability }} [skip ci]',
'branch': 'main'
}).encode()
req = urllib.request.Request(
'${API_BASE}/contents/updates.xml',
data=payload, method='PUT',
headers={
'Authorization': 'token ${GITEA_TOKEN}',
'Content-Type': 'application/json'
})
try:
urllib.request.urlopen(req)
print('updates.xml synced to main')
except Exception as e:
print(f'WARNING: sync to main failed: {e}', file=sys.stderr)
"
fi
- 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 }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_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 ${MOKO_CLI}/platform_detect.php --path . 2>/dev/null || true)
if [ "$PLATFORM" = "waas-component" ] && [ -f "${MOKO_CLI}/../deploy/deploy-joomla.php" ]; then
php ${MOKO_CLI}/../deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
elif [ -f "${MOKO_CLI}/../deploy/deploy-sftp.php" ]; then
php ${MOKO_CLI}/../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: |
VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
DISPLAY="${{ steps.meta.outputs.display_version }}"
echo "## Update Server" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Version | \`${DISPLAY}\` |" >> $GITHUB_STEP_SUMMARY
+130
View File
@@ -0,0 +1,130 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow.Template
# INGROUP: MokoStandards.CI
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla
# PATH: /.mokogitea/workflows/version-set.yml
# VERSION: 01.00.00
# BRIEF: Set or reset the extension version across all version-bearing files
name: "Joomla: Set Version"
on:
workflow_dispatch:
inputs:
version:
description: "Version number (e.g. 01.00.00)"
required: true
type: string
branch:
description: "Branch to update (default: current)"
required: false
type: string
permissions:
contents: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
set-version:
name: Set Version to ${{ inputs.version }}
runs-on: ubuntu-latest
steps:
- name: Validate version format
run: |
VERSION="${{ inputs.version }}"
if ! echo "$VERSION" | grep -qP '^\d{2}\.\d{2}\.\d{2}$'; then
echo "::error::Invalid version format '${VERSION}' — expected XX.YY.ZZ (e.g. 01.00.00)"
exit 1
fi
echo "VERSION=${VERSION}" >> "$GITHUB_ENV"
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.MOKOGITEA_TOKEN || secrets.GA_TOKEN || github.token }}
ref: ${{ inputs.branch || github.ref }}
fetch-depth: 1
- name: Update manifest version
run: |
MANIFEST=""
for XML_FILE in $(find . -maxdepth 3 -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 "::warning::No Joomla extension manifest found — skipping manifest update"
else
OLD_VER=$(grep -oP '<version>\K[^<]+' "$MANIFEST" | head -1)
sed -i "s|<version>${OLD_VER}</version>|<version>${VERSION}</version>|" "$MANIFEST"
echo "Manifest: ${OLD_VER} → ${VERSION} (${MANIFEST})"
fi
- name: Update README.md version
run: |
if [ -f "README.md" ]; then
if grep -qP '^\s*VERSION:\s*\d' README.md; then
sed -i -E "s/(VERSION:\s*)[0-9]{2}\.[0-9]{2}\.[0-9]{2}/\1${VERSION}/" README.md
echo "README.md version updated to ${VERSION}"
else
echo "::warning::No VERSION line found in README.md — skipping"
fi
fi
- name: Update CHANGELOG.md
run: |
if [ -f "CHANGELOG.md" ]; then
DATE=$(date +%Y-%m-%d)
# Check if this version already has an entry
if grep -q "^\#\# \[${VERSION}\]" CHANGELOG.md; then
echo "CHANGELOG.md already has entry for ${VERSION} — skipping"
else
# Insert new version entry after [Unreleased] or at the top after header
if grep -q '^\#\# \[Unreleased\]' CHANGELOG.md; then
sed -i "/^\#\# \[Unreleased\]/a\\\\n## [${VERSION}] --- ${DATE}" CHANGELOG.md
else
sed -i "/^\# Changelog/a\\\\n## [Unreleased]\n\n## [${VERSION}] --- ${DATE}" CHANGELOG.md
fi
echo "CHANGELOG.md: added entry for ${VERSION}"
fi
else
echo "::warning::No CHANGELOG.md found — skipping"
fi
- name: Update FILE INFORMATION blocks
run: |
# Update VERSION in file header blocks (# VERSION: XX.YY.ZZ)
find . -maxdepth 1 -type f \( -name "*.yml" -o -name "*.yaml" -o -name "*.php" -o -name "*.md" \) \
-not -path "./.git/*" -not -path "./vendor/*" -print0 2>/dev/null | \
while IFS= read -r -d '' FILE; do
if head -20 "$FILE" | grep -qP '^\s*#?\s*VERSION:\s*\d{2}\.\d{2}\.\d{2}'; then
sed -i -E "s/(#?\s*VERSION:\s*)[0-9]{2}\.[0-9]{2}\.[0-9]{2}/\1${VERSION}/" "$FILE"
echo "Updated FILE INFORMATION VERSION in ${FILE}"
fi
done
- name: Commit and push
run: |
git config user.name "Moko Consulting [bot]"
git config user.email "hello@mokoconsulting.tech"
git add -A
if git diff --cached --quiet; then
echo "No version changes detected — nothing to commit"
else
git commit -m "chore: set version to ${VERSION} [skip bump]
Authored-by: Moko Consulting"
git push
echo "### Version Set" >> $GITHUB_STEP_SUMMARY
echo "Version updated to \`${VERSION}\` on branch \`${GITHUB_REF_NAME}\`" >> $GITHUB_STEP_SUMMARY
fi
@@ -0,0 +1,73 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /.mokogitea/workflows/workflow-sync-trigger.yml
# VERSION: 01.01.00
# BRIEF: Trigger workflow sync to live repos when a PR is merged to main
name: "Universal: Workflow Sync Trigger"
on:
pull_request:
types: [closed]
branches:
- main
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
sync:
name: Sync workflows to live repos
runs-on: ubuntu-latest
if: >-
github.event.pull_request.merged == true &&
!contains(github.event.pull_request.title, '[skip sync]')
steps:
- name: Determine platform from repo name
id: platform
run: |
REPO="${{ github.event.repository.name }}"
case "$REPO" in
Template-Joomla) PLATFORM="joomla" ;;
Template-Dolibarr) PLATFORM="dolibarr" ;;
Template-Go) PLATFORM="go" ;;
Template-MCP) PLATFORM="mcp" ;;
Template-Generic) PLATFORM="" ;;
*) PLATFORM="" ;;
esac
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
echo "Platform: ${PLATFORM:-all}"
- name: Clone mokocli
env:
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
run: |
GITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}"
git clone --depth 1 "${GITEA_URL}/MokoConsulting/mokocli.git" /tmp/mokocli
- name: Install dependencies
run: |
cd /tmp/mokocli
composer install --no-dev --no-interaction --quiet 2>/dev/null || true
- name: Run workflow sync
env:
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
run: |
ARGS="--token ${MOKOGITEA_TOKEN}"
ARGS="${ARGS} --org ${{ vars.GITEA_ORG || github.repository_owner }}"
ARGS="${ARGS} --phase repos"
PLATFORM="${{ steps.platform.outputs.platform }}"
if [ -n "$PLATFORM" ]; then
ARGS="${ARGS} --platform-filter ${PLATFORM}"
fi
php /tmp/mokocli/cli/workflow_sync.php ${ARGS}
+81 -228
View File
@@ -1,236 +1,89 @@
# Changelog # Changelog
## [Unreleased] ## [Unreleased]
### Added
- **Instagram carousel**: Multi-image/video posts via Meta carousel container flow (up to 10 items)
- **Instagram Reels**: Short-form video publishing via REELS media type
- **Instagram Stories**: Image and video story publishing via STORIES media type
- **Instagram alt text**: Alt text support for image containers
- **Nostr plugin**: Full NIP-01 WebSocket relay publishing with BIP-340 Schnorr signatures (pure PHP, requires ext-gmp)
- **Nostr**: Publishes kind-1 text note events to multiple relays with automatic failover
- **Nostr**: Raw WebSocket client using stream_socket_client (no external dependencies)
- **Nostr**: Public key derivation and event signing via secp256k1 elliptic curve math
- **Threads carousel**: Support up to 20-item carousel posts via Threads API multi-container flow
- **Threads polls**: Poll creation support via poll_options parameter (2-4 options)
- **Threads spoiler tags**: Content warning / spoiler flag support for Threads posts
- **Threads text-only optimization**: Simplified single-step flow for text-only posts without media
<!-- VERSION: 01.02.00 --> ### Fixed
- Webservices plugin Joomla 6 compatibility — `onBeforeApiRoute` receives `BeforeApiRouteEvent` object, extract router via `$event->getRouter()`
## [01.07.00] --- 2026-06-23
## [01.07.00] --- 2026-06-23
### Added
- **Full ACL system**: 12 granular permissions in access.xml with permissions fieldset in config.xml
- **ACL enforcement**: All controllers and views check permissions before allowing actions
- **MokoSuiteCrossHelper::getActions()**: Centralized ACL helper for toolbar and view logic
### Fixed
- **License warning**: Removed duplicate from system plugin (install script already shows it)
- **Content plugin**: Fixed func_get_arg crash when non-article content is saved (e.g. update sites, installer)
## [01.05.00] --- 2026-06-23
## [01.05.00] --- 2026-06-23
### Added
- **Instagram plugin**: Cross-post to Instagram via Meta Content Publishing API (2-step container flow)
- **YouTube plugin**: Cross-post to YouTube via Data API v3 channel bulletins
- **Share Content panel**: Per-article editor panel with platform-specific share text fields
- **New placeholders**: {social}, {short}, {chat}, {email_subject}, {email_body} for platform-optimized templates
- **Share image control**: Choose intro image, fulltext image, custom image, or no image per article
- **Mailchimp templates**: Support Mailchimp saved templates with section injection, plus responsive email wrapper fallback
- **Delete from platforms**: New MokoSuiteCrossDeleteInterface for removing cross-posted content from remote platforms
- **Delete support**: Twitter, Mastodon, Bluesky, Facebook, LinkedIn, Telegram, Discord (7 of 38 plugins)
- **Auto-delete on unpublish**: Component config option to delete from platforms when articles are unpublished or trashed
- **UTM auto-tagging**: Append utm_source, utm_medium, utm_campaign to shared URLs with {platform} token support
- **Caption rotation**: {random:opt1|opt2|opt3} placeholder picks a random option per post
- **{url_raw} placeholder**: Clean article URL without UTM parameters
- **Mastodon enhancements**: Visibility levels, content warnings, scheduled posts, polls, language tags
- **Bluesky threads**: Auto-split long messages into reply chains at sentence boundaries
- **Bluesky link cards**: External link card embeds with article title and description
- **Ntfy default server**: Default server changed to ntfy.mokoconsulting.tech with configurable plugin params
### Changed
- **Default templates**: Updated to use platform-specific placeholders (social/short/chat/email) with graceful fallback
### Fixed
- **Mailchimp**: Fixed broken namespace placeholder in XML manifest
- **ConvertKit**: Removed duplicate curl_setopt_array with undefined $token
- **Brevo**: Removed duplicate curl_setopt_array with undefined $token and wrong auth header
- **Constant Contact**: Removed duplicate curl_setopt_array
- **Mailchimp**: Fixed campaign creation checking HTTP 200 instead of 2xx range
- **Medium**: Fixed getUserId() returning array instead of string on error
- **Bluesky**: Replaced md5() with hash('sha256', ...) for cache key
- **ServiceController**: Exception details no longer exposed to client
- **License warning**: Removed duplicate from system plugin -- install script already shows it with direct edit link
## [01.04.01] --- 2026-06-21
## [01.04.01] --- 2026-06-21
## [01.04.00] --- 2026-06-21
### Fixed
- **Package manifest**: Added missing `plg_system_mokosuitecross_events` and `plg_system_mokosuitecross_gallery` to `pkg_mokosuitecross.xml` — these system plugins were not installed with the package
- **Cleanup**: Removed old `src/` directory (pre-rename cruft with `mokojoomcross` files)
## [01.03.00] --- 2026-06-21
<!-- VERSION: 01.08.37 -->
All notable changes to MokoSuiteCross will be documented in this file. All notable changes to MokoSuiteCross will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## [01.02.00] --- 2026-06-21
### Changed
- **Rebrand complete**: All 1,151 language key references renamed from `MOKOJOOMCROSS` to `MOKOSUITECROSS` across .ini, .xml, and .php files
- **Event names**: All Joomla events renamed from `onMokoJoomCross*` to `onMokoSuiteCross*`
- **Telegram default bot**: Updated from @MokoWaaSBot to @mokosuite_bot with obfuscated embedded token
- **Branding**: All `MokoWaaS` references updated to `MokoSuite` across codebase, wiki, and docs
- **Wiki**: Reorganized into folder structure (getting-started/, user-guide/, services/, developer/)
- **README**: Updated with all 36 implemented service plugins and current feature list
- **PR workflow**: Added README/CHANGELOG diff check — blocks PRs that modify source without updating CHANGELOG
### Fixed
- **SendGrid**: Removed duplicate `curl_setopt_array` with undefined `$token` variable in `publish()`
- **Reddit**: Removed duplicate `curl_setopt_array` with undefined `$token` variable in `publish()`
- **TikTok**: Removed duplicate `curl_setopt_array` in `publish()`
- **Pinterest**: Removed duplicate `curl_setopt_array` in `publish()`
- **Telegram**: Added missing `<config>` section to plugin XML for parse_mode and disable_preview settings
### Fixed (previous)
- **C-1 OauthController**: Added CSRF nonce validation to OAuth callback — session-based nonce is generated during `authorize()`, embedded in the state parameter, and verified in `callback()` to prevent CSRF attacks
- **C-2 DispatchController**: Added POST method enforcement — rejects non-POST requests with 405 status
- **C-5 ServiceModel**: Credential form fields (`cred_*`) are now collected into the `credentials` JSON column on save, and expanded back into individual fields on load — previously these fields were silently discarded
- **H-1 Event pattern**: Fixed Joomla 5 SubscriberInterface incompatibility where `onMokoSuiteCrossGetServices` by-reference pattern silently lost all service plugins — dispatchers now read plugin instances from Event ArrayAccess indices after dispatch
- **H-4 ServiceTable**: Added `check()` method with alias generation, required field validation (title, service_type), timestamp management, and JSON defaults for credentials/params
- **H-9 WebhookService**: Fixed credential key mismatch — `publish()` and `validateCredentials()` now use keys matching the service.xml form fields (`url`, `method`, `auth_type`, `bearer_token`, `basic_username`, `basic_password`, `content_type`) and properly apply Bearer/Basic auth headers
- **M-4 ServiceIconHelper**: Escaped `$extraClass` parameter in `renderIcon()` with `htmlspecialchars()` to prevent XSS
- **M-5 Content plugin**: Fixed double-escaped HTML in cross-post history panel — uses `setFieldAttribute()` to inject history HTML into the note field description after XML load, avoiding XML attribute encoding
- **Content plugin**: Fixed `onContentBeforeDisplay` signature for Joomla 5/6 — now accepts `BeforeDisplayEvent` object instead of individual parameters
- **QueueProcessor**: Replaced read-then-write DB lock with MySQL advisory locks (`GET_LOCK`/`RELEASE_LOCK`) to eliminate race condition
- **Twitter/X**: Replaced Bearer token auth with OAuth 1.0a (HMAC-SHA1) — Bearer tokens are app-only and cannot create tweets
- **service.xml**: Fixed missing closing `</field>` tag on webhook method field
- **Views**: Added missing `Toolbar` and `Route` imports in Logs, Posts, Services, Template, Templates HtmlView files
- **13 service plugins**: Fixed broken `publish()` methods that had literal placeholder URLs instead of using credential values — ActivityPub, Blogger, Ghost, Google Business, Hashnode, Matrix, Medium, Nostr, RSS Feed, Threads, Tumblr, WhatsApp, WordPress
- **Ghost**: Proper JWT auth from `{id}:{secret}` admin API key format
- **WordPress**: Correct Basic Auth (not Bearer) with Application Passwords
- **Medium**: 2-step flow — fetch user ID via /v1/me, then post
- **Matrix**: PUT with transaction ID for idempotent message sending
- **Hashnode**: GraphQL mutation with proper query structure
- **Threads**: 2-step container creation + publish flow
- **WhatsApp**: Meta Cloud API with messaging_product payload
- **Nostr**: Stub with clear "not yet implemented" message (requires WebSocket)
- **RSS Feed**: Local service — no external API, always succeeds
### Added
- **ServiceIconHelper**: Centralised icon mapping for all 34 service types — replaces per-template icon arrays with `ServiceIconHelper::getIcon()` / `::renderIcon()`
- **Service Stats drill-down**: New `servicestats` view with per-service analytics — post counts, success rate, daily trend chart, recent posts table, and top articles list
- **Dashboard service links**: Service breakdown table rows now link to the per-service stats view with service type icons
- **Posts list icons**: Service type column in the posts list now shows the service icon
- **Category routing rules**: New `#__mokosuitecross_category_rules` table to whitelist services per Joomla category — if rules exist for a category, only those services receive posts; no rules = all services (backward compatible)
- **CrossPostDispatcher**: Category rule filtering integrated before per-article service filter in the dispatch loop
- **Template editor**: Live character counter below template body textarea with platform-aware limits (green/yellow/red badges)
- **Template editor**: Added `{tags}`, `{hashtags}`, and `{field:xxx}` rows to the placeholder reference table
- **Content plugin**: Cross-post history panel in article editor showing last 10 posts with status badges, service names, timestamps, and error messages
- **Config**: New "Category Rules" fieldset with explanatory note about the feature
- **CrossPostDispatcher**: New static helper (`com_mokosuitecross/Helper/CrossPostDispatcher`) centralising dispatch logic for reuse by all source plugins
- **Content plugin**: Added `onContentAfterSave` and `onContentChangeState` handlers with Joomla 5/6 event compatibility, dispatching via `CrossPostDispatcher`
- **plg_system_mokosuitecross_events**: New source plugin for MokoSuiteCalendar — cross-posts calendar events when published
- **plg_system_mokosuitecross_gallery**: New source plugin for MokoSuiteGallery — cross-posts galleries and images when published
- **Credential fields**: Added fields for 19 previously missing services (Pinterest, Tumblr, TikTok, Nostr, ActivityPub, Brevo, ConvertKit, Constant Contact, Hashnode, Blogger, Google Business, RSS Feed config)
- **Twitter**: Access Token and Access Token Secret fields for OAuth 1.0a
- **LinkedIn**: Refresh token field for automatic token renewal
- **Bluesky**: PDS URL field for self-hosted instances
- **Discord**: Username and avatar URL override fields
- **Mailchimp**: From name and from email fields
- **SendGrid**: From email and from name fields
- **Reddit**: Account password field for script-type OAuth
- **WordPress**: Default post status selector (draft/publish)
- **Dev.to**: Organization ID field
- **Ghost**: Default post status selector (draft/published)
- **Webhook**: Auth type selector (none/bearer/basic), auth token field, content type selector (JSON/form)
- **RSS Feed**: Feed title and max items config fields
- **OAuth services**: Added Pinterest, Tumblr, TikTok, Constant Contact, Blogger, Google Business to OAuth authorize flow
- **Developer Guide**: Comprehensive wiki page for building new service plugins
- **Help articles**: 42 KB articles on mokoconsulting.tech (overview, installation, 34 per-service guides, templates, queue, troubleshooting)
- **Service help link**: Per-service "Setup Guide" button in service edit sidebar links to the matching KB article
- **Evergreen re-sharing**: Articles can be marked as evergreen for automatic recurring cross-posts on a configurable interval (default 30 days)
- **Post edit form**: Full CRUD for queue posts — edit message, reschedule, change status, re-queue failed posts
- **Manual post creator**: New button in Post Queue toolbar to create manual cross-posts with article/service selection, custom message, and optional scheduling
- **Scheduled posts**: Calendar picker for scheduling posts to specific date/time; scheduled_at shown in queue list
- **Dashboard trend chart**: Chart.js line chart showing daily posted vs failed counts between stat cards and service breakdown
- **Dashboard date range filter**: Period selector (7/30/90 days, all time) filters service breakdown, top articles, and trend chart
- **Hashtag placeholders**: `{tags}` (comma-separated) and `{hashtags}` (#-prefixed space-separated) template placeholders from article tags
- **Posts service filter**: SQL-driven service dropdown filter in posts list, plus search filter by article title or message content
- **CSV export**: "Export CSV" toolbar button on posts list to download filtered post data as CSV
- **WordPress canonical URL**: WordPress cross-posts now include an "Originally published at" source link appended to content with the Joomla article URL
- **REST API dispatch endpoint**: `POST /api/v1/mokosuitecross/dispatch` — trigger cross-posts for an article via API with optional service filtering, duplicate guard, and template rendering
### Added (original)
#### Core Engine
- Cross-posting engine dispatches articles to service plugins on publish
- System plugin hooks `onContentAfterSave` and `onContentChangeState`
- Duplicate guard prevents re-posting to services that already received an article
- Message template rendering with 8 placeholders: `{title}`, `{url}`, `{introtext}`, `{fulltext}`, `{image}`, `{category}`, `{author}`, `{date}`
- Custom `mokosuitecross` plugin group for extensible service architecture
- `MokoSuiteCrossServiceInterface` contract for all service plugins
#### Admin Component (5 views)
- **Dashboard** — summary cards, posts-by-service analytics with success rates, top cross-posted articles, recent activity feed, PP Pro migration banner, page-load processing warning
- **Post Queue** — list with color-coded status badges, error messages, retry counts, platform post IDs, article/service columns, date filters
- **Services** — CRUD with service type selector (34 platforms organized by category), default/custom mode badges, publish toggle, credential editor
- **Templates** — CRUD for message templates, per-platform assignment, placeholder reference panel, template body preview
- **Activity Logs** — list with level badges (info/warning/error), service column, context data, level and search filters
#### Queue Processing (3 methods)
- Joomla Scheduled Task plugin (`plg_task_mokosuitecross`) — preferred, processes 20 posts per run
- Page-load fallback via system plugin `onAfterRender` — configurable throttle interval, backend/frontend/both
- Shared `QueueProcessor` helper with DB lock to prevent concurrent execution
- Failed post retry with configurable max retries and exponential delay
- Scheduled post support (`scheduled_at` column)
- Automatic log cleanup based on configurable retention period
#### Per-Article Controls
- "Cross-Posting" fieldset injected into article editor via `onContentPrepareForm`
- Skip cross-posting toggle per article
- Service selection checkboxes (unchecked = post to all enabled services)
#### OAuth 2.0
- `OAuthHelper` with authorization URL generation, code-to-token exchange, token storage
- Twitter PKCE flow support
- `OauthController` with authorize and callback endpoints
- Reads client ID/secret from service plugin params
#### Perfect Publisher Pro Migration
- Reads `#__autotweet_channels` table with per-platform credential mapping
- Fallback extraction from component params when channel table missing
- Maps Facebook, Twitter, LinkedIn, Telegram, Discord, Slack, Mastodon
- Creates services in disabled state for manual verification
- One-click migration from dashboard
#### Service Plugins (34 platforms)
**Social Media (12)**
- Facebook / Meta — Graph API v19.0, default MokoSuite app mode, page feed posting
- X / Twitter — API v2, OAuth 2.0 Bearer Token, 280 char limit
- LinkedIn — Share API v2, organization + personal profile, 3000 char limit
- Mastodon — API v1, multi-instance, hashtags, 500 char limit
- Bluesky — AT Protocol, session auth, app passwords, 300 char limit
- Threads (Meta) — Threads Publishing API, default app mode, 500 char limit
- Pinterest — Pins API v5, board selection, image-focused
- Reddit — OAuth2 link submission, subreddit selection
- Tumblr — API v2, link/text posts, OAuth 1.0a
- TikTok — Content Posting API, photo slideshows
- Nostr — NIP-01 event publishing, configurable relays
- ActivityPub — generic Fediverse (Pleroma, Akkoma, Misskey, Pixelfed)
**Chat / Messaging (8)**
- Telegram — Bot API, default @mokosuite_bot + custom bot, HTML/Markdown, 4096 chars
- Discord — Webhooks, default MokoSuite webhook mode, embeds, 2000 chars
- Slack — Incoming Webhooks, default MokoSuite webhook mode, Block Kit
- Microsoft Teams — Incoming Webhooks, default mode, Adaptive Cards
- Google Chat — Webhook API, card formatting
- WhatsApp Business — Meta Cloud API, template + free-form messages
- Matrix / Element — Client-Server API, self-hosted homeserver support
- Ntfy — Push notifications, priority levels, action buttons
**Email / Newsletter (5)**
- Mailchimp — Campaigns API, audience selection, send/draft modes
- SendGrid — Marketing Campaigns API v3, Single Send creation
- Brevo (Sendinblue) — API v3, campaign creation
- ConvertKit — API v3, broadcast creation
- Constant Contact — API v3, campaign creation
**Publishing / Blogging (6)**
- Medium — Publishing API, full HTML, canonical URL, tags
- WordPress — REST API v2, Application Passwords, category mapping
- Dev.to — Forem API, markdown, series support
- Ghost — Admin API v5, JWT auth, full HTML
- Hashnode — GraphQL API, cover image, tags
- Google Blogger — Blogger API v3, labels from categories
**Business (1)**
- Google Business Profile — API v1, local posts (UPDATE/EVENT/OFFER)
**Universal (2)**
- Generic Webhook — POST/PUT to any URL, JSON/form body, custom headers (IFTTT, Zapier, n8n, Make)
- RSS Feed — dedicated cross-post feed generation
#### Plugin Configuration
- Telegram: default bot token, parse mode, link preview toggle
- Facebook: default page access token, default page ID
- Discord: default webhook URL, embed color
- Slack: default webhook URL
- LinkedIn: OAuth client ID/secret, redirect URI
- Mastodon: default instance URL, visibility, hashtags
- Bluesky: default PDS URL, auto link cards
- Mailchimp: default sender name/email, auto-send toggle
- Microsoft Teams: default webhook URL
- Threads: default webhook URL
#### Infrastructure
- 7 CI/CD workflows: CI, auto-release, pre-release, auto-bump, update-server, cascade-dev, issue-branch
- Joomla update server (`updates.xml`) with development channel
- WebServices REST API plugin with CRUD routes for posts and services
- Database: 4 tables (services, posts, templates, logs) with default templates
- Package installer with auto-enable for core + task + service plugins
- 9 wiki documentation pages
- Windows Terminal profile in Joomla dropdown
## [01.01.00] - 2026-06-19
### Added
- Initial package structure with component, system plugin, content plugin, and webservices plugin
- Admin component with dashboard, post queue, services management, and activity logs
- System plugin triggering cross-post on article publish via `onContentAfterSave`
- Content plugin adding cross-post controls to article editor
- WebServices API plugin with REST endpoints for posts and services
- Custom `mokosuitecross` plugin group for extensible service architecture
- Service plugins: Facebook, X/Twitter, LinkedIn, Mastodon, Bluesky, Mailchimp, Telegram, Discord, Slack
- Database tables: services, posts, templates, logs
- Perfect Publisher Pro migration tool in installer script
- Message template system with per-platform placeholders
- Post queue with scheduled posting, retry logic, and delivery tracking
## [01.00] - 2026-05-28
### Added
- Initial release
+46
View File
@@ -0,0 +1,46 @@
<!-- 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
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).
# FILE INFORMATION
DEFGROUP: Template-Joomla
INGROUP: Template-Joomla.Documentation
REPO: https://github.com/mokoconsulting-tech/Template-Joomla/
VERSION: 01.08.37
PATH: ./CODE_OF_CONDUCT.md
BRIEF: Community expectations and enforcement guidelines
NOTE: Adapted with attribution from the Contributor Covenant v2.1
-->
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone.
## Our Standards
- Be empathetic and kind
- Be respectful of differing opinions
- Accept constructive feedback
- Own mistakes and learn from them
Unacceptable behavior includes sexualized language/imagery, trolling, harassment, doxing, and other inappropriate conduct.
## Enforcement
Report incidents to **hello@mokoconsulting.tech** or through GitHub Discussions if you prefer a community-visible approach. Private complaints will be reviewed promptly and fairly.
## Enforcement Guidelines
1. **Correction** — Private warning
2. **Warning** — Formal warning and limited interaction
3. **Temporary Ban** — Time-boxed exclusion
4. **Permanent Ban** — Removal from the community
## Attribution
Adapted from the Contributor Covenant v2.1.
+1
View File
File diff suppressed because one or more lines are too long
-203
View File
@@ -1,203 +0,0 @@
# Makefile for Joomla Extensions
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# SPDX-License-Identifier: GPL-3.0-or-later
#
# MokoSuiteCross — Cross-posting Joomla content to social media, email marketing, and chat platforms
# ==============================================================================
# CONFIGURATION - Customize these for your extension
# ==============================================================================
# Extension Configuration
EXTENSION_NAME := mokosuitecross
EXTENSION_TYPE := package
# Options: module, plugin, component, package, template
EXTENSION_VERSION := 1.0.0
# Module Configuration (for modules only)
MODULE_TYPE := site
# Options: site, admin
# Plugin Configuration (for plugins only)
PLUGIN_GROUP := system
# Options: system, content, user, authentication, etc.
# Directories
SRC_DIR := src
BUILD_DIR := build
DIST_DIR := dist
DOCS_DIR := docs
# Joomla Installation (for local testing - customize paths)
JOOMLA_ROOT := /var/www/html/joomla
JOOMLA_VERSION := 4
# Tools
PHP := php
COMPOSER := composer
NPM := npm
PHPCS := vendor/bin/phpcs
PHPCBF := vendor/bin/phpcbf
PHPUNIT := vendor/bin/phpunit
ZIP := zip
# Coding Standards
PHPCS_STANDARD := Joomla
# Colors for output
COLOR_RESET := \033[0m
COLOR_GREEN := \033[32m
COLOR_YELLOW := \033[33m
COLOR_BLUE := \033[34m
COLOR_RED := \033[31m
# ==============================================================================
# TARGETS
# ==============================================================================
.PHONY: help
help: ## Show this help message
@echo "$(COLOR_BLUE)╔════════════════════════════════════════════════════════════╗$(COLOR_RESET)"
@echo "$(COLOR_BLUE)║ Joomla Extension Makefile ║$(COLOR_RESET)"
@echo "$(COLOR_BLUE)╚════════════════════════════════════════════════════════════╝$(COLOR_RESET)"
@echo ""
@echo "Extension: $(EXTENSION_NAME) ($(EXTENSION_TYPE)) v$(EXTENSION_VERSION)"
@echo ""
@echo "$(COLOR_GREEN)Available targets:$(COLOR_RESET)"
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " $(COLOR_BLUE)%-20s$(COLOR_RESET) %s\n", $$1, $$2}'
@echo ""
.PHONY: install-deps
install-deps: ## Install all dependencies (Composer + npm)
@echo "$(COLOR_BLUE)Installing dependencies...$(COLOR_RESET)"
@if [ -f "composer.json" ]; then \
$(COMPOSER) install; \
echo "$(COLOR_GREEN)✓ Composer dependencies installed$(COLOR_RESET)"; \
fi
.PHONY: lint
lint: ## Run PHP linter (syntax check)
@echo "$(COLOR_BLUE)Running PHP linter...$(COLOR_RESET)"
@find . -name "*.php" ! -path "./vendor/*" ! -path "./node_modules/*" ! -path "./$(BUILD_DIR)/*" \
-exec $(PHP) -l {} \; | grep -v "No syntax errors" || true
@echo "$(COLOR_GREEN)✓ PHP linting complete$(COLOR_RESET)"
.PHONY: phpcs
phpcs: ## Run PHP CodeSniffer (Joomla standards)
@echo "$(COLOR_BLUE)Running PHP CodeSniffer...$(COLOR_RESET)"
@if [ -f "$(PHPCS)" ]; then \
$(PHPCS) --standard=$(PHPCS_STANDARD) --extensions=php --ignore=vendor,node_modules,$(BUILD_DIR) .; \
else \
echo "$(COLOR_YELLOW)⚠ PHP CodeSniffer not installed. Run: make install-deps$(COLOR_RESET)"; \
fi
.PHONY: validate
validate: lint phpcs ## Run all validation checks
@echo "$(COLOR_GREEN)✓ All validation checks passed$(COLOR_RESET)"
.PHONY: clean
clean: ## Clean build artifacts
@echo "$(COLOR_BLUE)Cleaning build artifacts...$(COLOR_RESET)"
@rm -rf $(BUILD_DIR) $(DIST_DIR)
@echo "$(COLOR_GREEN)✓ Build artifacts cleaned$(COLOR_RESET)"
MOKO_PLATFORM ?= $(or $(wildcard ../moko-platform),$(wildcard $(HOME)/moko-platform),$(wildcard /opt/moko-platform))
MINIFY_SCRIPT := $(MOKO_PLATFORM)/build/minify.js
.PHONY: minify
minify: ## Minify CSS/JS assets
@echo "Minifying assets..."
@if [ -f "$(MINIFY_SCRIPT)" ]; then \
node "$(MINIFY_SCRIPT)" $(SRC_DIR); \
elif [ -f "scripts/minify.js" ]; then \
node scripts/minify.js; \
else \
echo "No minify script found"; \
fi
.PHONY: build
build: clean validate minify ## Build extension package
@echo "$(COLOR_BLUE)Building Joomla extension package...$(COLOR_RESET)"
@mkdir -p $(DIST_DIR) $(BUILD_DIR)
# Determine package prefix based on extension type
@case "$(EXTENSION_TYPE)" in \
module) \
PACKAGE_PREFIX="mod_$(EXTENSION_NAME)"; \
BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \
;; \
plugin) \
PACKAGE_PREFIX="plg_$(PLUGIN_GROUP)_$(EXTENSION_NAME)"; \
BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \
;; \
component) \
PACKAGE_PREFIX="com_$(EXTENSION_NAME)"; \
BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \
;; \
package) \
PACKAGE_PREFIX="pkg_$(EXTENSION_NAME)"; \
BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \
;; \
template) \
PACKAGE_PREFIX="tpl_$(EXTENSION_NAME)"; \
BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \
;; \
*) \
echo "$(COLOR_RED)✗ Unknown extension type: $(EXTENSION_TYPE)$(COLOR_RESET)"; \
exit 1; \
;; \
esac; \
\
mkdir -p "$$BUILD_TARGET"; \
\
echo "Building $$PACKAGE_PREFIX..."; \
\
rsync -av --progress \
--exclude='$(BUILD_DIR)' \
--exclude='$(DIST_DIR)' \
--exclude='.git*' \
--exclude='vendor/' \
--exclude='node_modules/' \
--exclude='tests/' \
--exclude='Makefile' \
--exclude='composer.json' \
--exclude='composer.lock' \
--exclude='package.json' \
--exclude='package-lock.json' \
--exclude='phpunit.xml' \
--exclude='*.md' \
--exclude='.editorconfig' \
. "$$BUILD_TARGET/"; \
\
cd $(BUILD_DIR) && $(ZIP) -r "../$(DIST_DIR)/$${PACKAGE_PREFIX}-$(EXTENSION_VERSION).zip" "$${PACKAGE_PREFIX}"; \
\
echo "$(COLOR_GREEN)✓ Package created: $(DIST_DIR)/$${PACKAGE_PREFIX}-$(EXTENSION_VERSION).zip$(COLOR_RESET)"
.PHONY: package
package: build ## Alias for build
@echo "$(COLOR_GREEN)✓ Package ready for distribution$(COLOR_RESET)"
.PHONY: release
release: validate build ## Create a release (validate + build)
@echo "$(COLOR_GREEN)✓ Release package ready$(COLOR_RESET)"
.PHONY: version
version: ## Display version information
@echo "$(COLOR_BLUE)Extension Information:$(COLOR_RESET)"
@echo " Name: $(EXTENSION_NAME)"
@echo " Type: $(EXTENSION_TYPE)"
@echo " Version: $(EXTENSION_VERSION)"
.PHONY: security-check
security-check: ## Run security checks on dependencies
@echo "$(COLOR_BLUE)Running security checks...$(COLOR_RESET)"
@if [ -f "composer.json" ]; then \
$(COMPOSER) audit || echo "$(COLOR_YELLOW)⚠ Vulnerabilities found$(COLOR_RESET)"; \
fi
.PHONY: all
all: install-deps validate build ## Run complete build pipeline
@echo "$(COLOR_GREEN)✓ Complete build pipeline finished$(COLOR_RESET)"
# Default target
.DEFAULT_GOAL := help
+10 -3
View File
@@ -1,6 +1,6 @@
# MokoSuiteCross # MokoSuiteCross
<!-- VERSION: 01.02.00 --> <!-- VERSION: 01.08.37 -->
Cross-posting Joomla content to social media, email marketing, and chat platforms for Joomla 5/6. Cross-posting Joomla content to social media, email marketing, and chat platforms for Joomla 5/6.
@@ -14,20 +14,27 @@ MokoSuiteCross automatically publishes your Joomla articles to multiple platform
- **Plugin-based services** — Each platform is a separate plugin; install only what you need - **Plugin-based services** — Each platform is a separate plugin; install only what you need
- **Default bot mode** — Pre-configured bots for Telegram (@mokosuite_bot), Discord, and Slack — just add your channel - **Default bot mode** — Pre-configured bots for Telegram (@mokosuite_bot), Discord, and Slack — just add your channel
- **Post queue** — Scheduled posting, retry on failure, detailed delivery logs - **Post queue** — Scheduled posting, retry on failure, detailed delivery logs
- **Message templates** — Customize post format per platform with placeholders ({title}, {url}, {intro}, {image}, {tags}, {field:xxx}) - **Message templates** — Customize post format per platform with placeholders ({title}, {url}, {social}, {short}, {chat}, {email_subject}, {email_body}, {field:xxx})
- **Share Content panel** — Per-article fields for platform-optimized text (social, short, chat, email) with image picker
- **Caption rotation** — {random:opt1|opt2|opt3} placeholder for varying evergreen re-shares
- **UTM tracking** — Auto-append UTM parameters to shared links with {platform} token
- **Delete from platforms** — Remove cross-posted content when articles are unpublished/trashed (7 platforms)
- **Post history** — Track what was posted where, with platform response data - **Post history** — Track what was posted where, with platform response data
- **Evergreen re-sharing** — Automatically re-share articles on a configurable interval - **Evergreen re-sharing** — Automatically re-share articles on a configurable interval
- **Category routing** — Route articles to specific services by Joomla category - **Category routing** — Route articles to specific services by Joomla category
- **Mailchimp templates** — Use saved Mailchimp templates with section injection, or built-in responsive email wrapper
- **Migration** — Import settings from Perfect Publisher Pro - **Migration** — Import settings from Perfect Publisher Pro
- **REST API** — WebServices plugin for headless/external integration - **REST API** — WebServices plugin for headless/external integration
### Supported Platforms (36) ### Supported Platforms (38)
#### Social Media #### Social Media
| Platform | Plugin | Status | | Platform | Plugin | Status |
|----------|--------|--------| |----------|--------|--------|
| Facebook / Meta | `plg_mokosuitecross_facebook` | Implemented | | Facebook / Meta | `plg_mokosuitecross_facebook` | Implemented |
| X / Twitter | `plg_mokosuitecross_twitter` | Implemented | | X / Twitter | `plg_mokosuitecross_twitter` | Implemented |
| Instagram | `plg_mokosuitecross_instagram` | Implemented |
| YouTube | `plg_mokosuitecross_youtube` | Implemented |
| LinkedIn | `plg_mokosuitecross_linkedin` | Implemented | | LinkedIn | `plg_mokosuitecross_linkedin` | Implemented |
| Mastodon | `plg_mokosuitecross_mastodon` | Implemented | | Mastodon | `plg_mokosuitecross_mastodon` | Implemented |
| Bluesky | `plg_mokosuitecross_bluesky` | Implemented | | Bluesky | `plg_mokosuitecross_bluesky` | Implemented |
+241
View File
@@ -0,0 +1,241 @@
<!--
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
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
# FILE INFORMATION
DEFGROUP: Template-Joomla
INGROUP: Template-Joomla.Documentation
REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla
PATH: /SECURITY.md
VERSION: 01.08.37
BRIEF: Security vulnerability reporting and handling policy
-->
# Security Policy
## Purpose and Scope
This document defines the security vulnerability reporting, response, and disclosure policy for this Joomla Plugin template repository. It establishes the authoritative process for responsible disclosure, assessment, remediation, and communication of security issues.
## Supported Versions
Security updates are provided for the following versions:
| Version | Supported |
| ------- | ------------------ |
| 01.x.x | :white_check_mark: |
| < 01.0 | :x: |
Only the current major version receives security updates. Users should upgrade to the latest supported version to receive security patches.
## Reporting a Vulnerability
### Where to Report
**DO NOT** create public GitHub issues for security vulnerabilities.
Report security vulnerabilities privately to:
**Email**: `security@mokoconsulting.tech`
**Subject Line**: `[SECURITY] Template-Joomla - Brief Description`
### What to Include
A complete vulnerability report should include:
1. **Description**: Clear explanation of the vulnerability
2. **Impact**: Potential security impact and severity assessment
3. **Affected Versions**: Which versions are vulnerable
4. **Reproduction Steps**: Detailed steps to reproduce the issue
5. **Proof of Concept**: Code, configuration, or demonstration (if applicable)
6. **Suggested Fix**: Proposed remediation (if known)
7. **Disclosure Timeline**: Your expectations for public disclosure
### Response Timeline
* **Initial Response**: Within 3 business days
* **Assessment Complete**: Within 7 business days
* **Fix Timeline**: Depends on severity (see below)
* **Disclosure**: Coordinated with reporter
## Severity Classification
Vulnerabilities are classified using the following severity levels:
### Critical
* Remote code execution
* Authentication bypass
* Data breach or exposure of sensitive information
* **Fix Timeline**: 7 days
### High
* Privilege escalation
* SQL injection or command injection
* Cross-site scripting (XSS) with significant impact
* **Fix Timeline**: 14 days
### Medium
* Information disclosure (limited scope)
* Denial of service
* Security misconfigurations with moderate impact
* **Fix Timeline**: 30 days
### Low
* Security best practice violations
* Minor information leaks
* Issues requiring user interaction or complex preconditions
* **Fix Timeline**: 60 days or next release
## Remediation Process
1. **Acknowledgment**: Security team confirms receipt and begins investigation
2. **Assessment**: Vulnerability is validated, severity assigned, and impact analyzed
3. **Development**: Security patch is developed and tested
4. **Review**: Patch undergoes security review and validation
5. **Release**: Fixed version is released with security advisory
6. **Disclosure**: Public disclosure follows coordinated timeline
## Security Advisories
Security advisories are published via:
* GitHub Security Advisories
* Release notes and CHANGELOG.md
* Email notification to project users (if mailing list is established)
Advisories include:
* CVE identifier (if applicable)
* Severity rating
* Affected versions
* Fixed versions
* Mitigation steps
* Attribution (with reporter consent)
## Security Best Practices
For projects using this template:
### Required Controls
* Enable GitHub security features (Dependabot, code scanning)
* Implement branch protection on `main`
* Require code review for all changes
* Enforce signed commits (recommended)
* Use secrets management (never commit credentials)
* Maintain security documentation
* Follow secure coding standards defined in MokoStandards
### Joomla Plugin Security
* Follow Joomla security best practices
* Validate and sanitize all user input
* Use Joomla's database API to prevent SQL injection
* Properly escape output to prevent XSS
* Implement proper access control checks
* Use Joomla's session and authentication APIs
* Keep Joomla and dependencies up to date
### CI/CD Security
* Validate all inputs
* Sanitize outputs
* Use least privilege access
* Pin dependencies with hash verification
* Scan for vulnerabilities in dependencies
* Audit third-party actions and tools
#### Automated Security Scanning
All repositories SHOULD implement:
**CodeQL Analysis**:
* Enabled for PHP and other supported languages
* Runs on: push to main, pull requests, weekly schedule
* Query sets: `security-extended` and `security-and-quality`
* Configuration: `.github/workflows/codeql-analysis.yml`
**Dependabot Security Updates**:
* Weekly scans for vulnerable dependencies
* Automated pull requests for security patches
* Configuration: `.github/dependabot.yml`
**Secret Scanning**:
* Enabled by default with push protection
* Prevents accidental credential commits
### Dependency Management
* Keep dependencies up to date
* Monitor security advisories for dependencies
* Remove unused dependencies
* Audit new dependencies before adoption
* Document security-critical dependencies
## Compliance and Governance
This security policy is aligned with MokoStandards. Deviations require documented justification.
Security policies are reviewed and updated at least annually or following significant security incidents.
## Attribution and Recognition
We acknowledge and appreciate responsible disclosure. With your permission, we will:
* Credit you in security advisories
* List you in CHANGELOG.md for the fix release
* Recognize your contribution publicly (if desired)
## Contact and Escalation
* **Security Team**: security@mokoconsulting.tech
* **Primary Contact**: hello@mokoconsulting.tech
* **Escalation**: For urgent matters requiring immediate attention, contact the maintainer directly via GitHub
## Out of Scope
The following are explicitly out of scope:
* Issues in third-party dependencies (report directly to maintainers)
* Social engineering attacks
* Physical security issues
* Denial of service via resource exhaustion without amplification
* Issues requiring physical access to systems
* Theoretical vulnerabilities without proof of exploitability
---
## Metadata
| Field | Value |
| ------------ | ------------------------------------------------------------------------------------------------------------ |
| Document | Security Policy |
| Path | /SECURITY.md |
| Repository | [https://github.com/mokoconsulting-tech/Template-Joomla](https://github.com/mokoconsulting-tech/Template-Joomla) |
| Owner | Moko Consulting |
| Scope | Security vulnerability handling |
| Status | Active |
| Effective | 2026-01-16 |
## Revision History
| Date | Change Description | Author |
| ---------- | ------------------------------------------------- | --------------- |
| 2026-01-16 | Initial creation for template repository | Moko Consulting |
-237
View File
@@ -1,237 +0,0 @@
#!/usr/bin/env bash
# ============================================================================
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Automation.CI
# INGROUP: moko-platform.Automation
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /automation/ci-issue-reporter.sh
# VERSION: 09.23.00
# BRIEF: Creates or updates a Gitea issue when a CI gate fails.
# Deduplicates by searching open issues with the "ci-auto" label
# whose title matches the gate. If a matching issue exists, a comment
# is appended instead of opening a duplicate.
# ============================================================================
set -euo pipefail
# ── Defaults ────────────────────────────────────────────────────────────────
GITEA_URL="${GITEA_URL:-https://git.mokoconsulting.tech}"
GITEA_TOKEN="${GITEA_TOKEN:-}"
REPO="${GITHUB_REPOSITORY:-}"
RUN_URL="${GITHUB_SERVER_URL:-${GITEA_URL}}/${REPO}/actions/runs/${GITHUB_RUN_ID:-0}"
LABEL_NAME="ci-auto"
LABEL_COLOR="#e11d48"
GATE=""
DETAILS=""
SEVERITY="error"
WORKFLOW=""
# ── Parse arguments ─────────────────────────────────────────────────────────
usage() {
cat <<EOF
Usage: ci-issue-reporter.sh --gate NAME --details TEXT [OPTIONS]
Required:
--gate CI gate name (e.g. "Code Quality", "Self-Health")
--details Human-readable failure description
Optional:
--severity "error" (default) or "warning"
--workflow Workflow name for the issue title
--repo owner/repo (default: \$GITHUB_REPOSITORY)
--run-url URL to the CI run (auto-detected from env)
--token Gitea API token (default: \$GITEA_TOKEN)
--url Gitea base URL (default: \$GITEA_URL)
EOF
exit 1
}
while [[ $# -gt 0 ]]; do
case "$1" in
--gate) GATE="$2"; shift 2 ;;
--details) DETAILS="$2"; shift 2 ;;
--severity) SEVERITY="$2"; shift 2 ;;
--workflow) WORKFLOW="$2"; shift 2 ;;
--repo) REPO="$2"; shift 2 ;;
--run-url) RUN_URL="$2"; shift 2 ;;
--token) GITEA_TOKEN="$2"; shift 2 ;;
--url) GITEA_URL="$2"; shift 2 ;;
-h|--help) usage ;;
*) echo "Unknown option: $1"; usage ;;
esac
done
[[ -z "$GATE" ]] && { echo "ERROR: --gate is required"; usage; }
[[ -z "$DETAILS" ]] && { echo "ERROR: --details is required"; usage; }
[[ -z "$GITEA_TOKEN" ]] && { echo "ERROR: GITEA_TOKEN not set"; exit 1; }
[[ -z "$REPO" ]] && { echo "ERROR: GITHUB_REPOSITORY not set"; exit 1; }
API="${GITEA_URL}/api/v1/repos/${REPO}"
# ── Build title ─────────────────────────────────────────────────────────────
if [[ -n "$WORKFLOW" ]]; then
TITLE="[CI] ${WORKFLOW}: ${GATE} failed"
else
TITLE="[CI] ${GATE} failed"
fi
# ── Ensure label exists ─────────────────────────────────────────────────────
ensure_label() {
local exists
exists=$(curl -sf -o /dev/null -w '%{http_code}' \
-H "Authorization: token ${GITEA_TOKEN}" \
"${API}/labels" 2>/dev/null || echo "000")
if [[ "$exists" == "200" ]]; then
# Check if label already exists
local found
found=$(curl -sf \
-H "Authorization: token ${GITEA_TOKEN}" \
"${API}/labels" 2>/dev/null \
| grep -o "\"name\":\"${LABEL_NAME}\"" || true)
if [[ -z "$found" ]]; then
curl -sf -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/labels" \
-d "{\"name\":\"${LABEL_NAME}\",\"color\":\"${LABEL_COLOR}\",\"description\":\"Auto-created by CI issue reporter\"}" \
> /dev/null 2>&1 || true
fi
fi
}
# ── Search for existing open issue ──────────────────────────────────────────
find_existing_issue() {
# URL-encode the gate name for the query
local query
query=$(printf '%s' "[CI] ${GATE}" | sed 's/ /%20/g; s/\[/%5B/g; s/\]/%5D/g')
local response
response=$(curl -sf \
-H "Authorization: token ${GITEA_TOKEN}" \
"${API}/issues?type=issues&state=open&labels=${LABEL_NAME}&q=${query}&limit=5" \
2>/dev/null || echo "[]")
# Extract the first matching issue number
echo "$response" \
| grep -oP '"number":\s*\K[0-9]+' \
| head -1
}
# ── Build issue body ────────────────────────────────────────────────────────
build_body() {
local severity_badge
if [[ "$SEVERITY" == "error" ]]; then
severity_badge="**Severity:** Error"
else
severity_badge="**Severity:** Warning"
fi
cat <<BODY
## CI Gate Failure: ${GATE}
${severity_badge}
**Workflow:** ${WORKFLOW:-unknown}
**Branch:** ${GITHUB_REF_NAME:-unknown}
**Commit:** \`${GITHUB_SHA:0:8}\`
**Run:** [View CI run](${RUN_URL})
### Details
${DETAILS}
### Resolution
Fix the issue described above and push a new commit. This issue will be closed automatically when the gate passes, or can be closed manually.
---
*Auto-created by [ci-issue-reporter](${GITEA_URL}/${REPO}/src/branch/main/automation/ci-issue-reporter.sh)*
BODY
}
# ── Build comment body (for existing issues) ────────────────────────────────
build_comment() {
cat <<COMMENT
### CI failure recurrence
**Branch:** ${GITHUB_REF_NAME:-unknown}
**Commit:** \`${GITHUB_SHA:0:8}\`
**Run:** [View CI run](${RUN_URL})
${DETAILS}
COMMENT
}
# ── Main ────────────────────────────────────────────────────────────────────
ensure_label
EXISTING=$(find_existing_issue)
if [[ -n "$EXISTING" ]]; then
# Append comment to existing issue
COMMENT_BODY=$(build_comment)
COMMENT_JSON=$(printf '%s' "$COMMENT_BODY" | python3 -c "
import sys, json
print(json.dumps({'body': sys.stdin.read()}))" 2>/dev/null)
HTTP=$(curl -sf -o /dev/null -w '%{http_code}' -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/issues/${EXISTING}/comments" \
-d "${COMMENT_JSON}" 2>/dev/null || echo "000")
if [[ "$HTTP" == "201" ]]; then
echo "Commented on existing issue #${EXISTING}"
else
echo "WARNING: Failed to comment on issue #${EXISTING} (HTTP ${HTTP})"
fi
else
# Create new issue
ISSUE_BODY=$(build_body)
ISSUE_JSON=$(python3 -c "
import sys, json
body = sys.stdin.read()
print(json.dumps({
'title': sys.argv[1],
'body': body,
'labels': []
}))" "$TITLE" <<< "$ISSUE_BODY" 2>/dev/null)
# Create the issue
RESPONSE=$(curl -sf -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/issues" \
-d "${ISSUE_JSON}" 2>/dev/null || echo "{}")
ISSUE_NUM=$(echo "$RESPONSE" | grep -oP '"number":\s*\K[0-9]+' | head -1)
if [[ -n "$ISSUE_NUM" ]]; then
# Apply label (separate call — more reliable across Gitea versions)
LABEL_ID=$(curl -sf \
-H "Authorization: token ${GITEA_TOKEN}" \
"${API}/labels" 2>/dev/null \
| grep -oP "\"id\":\s*\K[0-9]+(?=[^}]*\"name\":\s*\"${LABEL_NAME}\")" \
| head -1 || true)
if [[ -n "$LABEL_ID" ]]; then
curl -sf -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/issues/${ISSUE_NUM}/labels" \
-d "{\"labels\":[${LABEL_ID}]}" \
> /dev/null 2>&1 || true
fi
echo "Created issue #${ISSUE_NUM}: ${TITLE}"
else
echo "WARNING: Failed to create issue"
echo "Response: ${RESPONSE}"
fi
fi
+32
View File
@@ -0,0 +1,32 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# SPDX-License-Identifier: GPL-3.0-or-later
#
# PHPStan configuration for Joomla extension repositories.
# Extends the base MokoStandards config and adds Joomla framework class stubs
# so PHPStan can resolve Factory, CMSApplication, User, Table, etc.
# without requiring a full Joomla installation.
parameters:
level: 5
paths:
- src
excludePaths:
- vendor
- node_modules
# Joomla framework stubs — resolved via the enterprise package from vendor/
stubFiles:
- vendor/mokoconsulting-tech/enterprise/templates/stubs/joomla.php
# Suppress errors that are structural in Joomla's service-container architecture
ignoreErrors:
# Joomla's service-based dependency injection returns mixed from getApplication()
- '#Cannot call method .+ on Joomla\\CMS\\Application\\CMSApplication\|null#'
# Factory::getX() patterns are safe at runtime even when nullable in stubs
- '#Call to static method [a-zA-Z]+\(\) on an interface#'
reportUnmatchedIgnoredErrors: false
checkMissingIterableValueType: false
checkGenericClassInNonGenericObjectType: false
@@ -3,6 +3,6 @@
; License: GPL-3.0-or-later ; License: GPL-3.0-or-later
PKG_MOKOSUITECROSS="MokoSuiteCross" PKG_MOKOSUITECROSS="MokoSuiteCross"
PKG_MOKOSUITECROSS_DESCRIPTION="Cross-posting Joomla content to social media, email marketing, and chat platforms. Automatically publish articles to Facebook, X/Twitter, LinkedIn, Mastodon, Bluesky, Mailchimp, Telegram, Discord, and Slack." PKG_MOKOSUITECROSS_DESCRIPTION="Cross-post Joomla articles to 38 platforms including Facebook, Instagram, X/Twitter, LinkedIn, Threads, Mastodon, Bluesky, Nostr, TikTok, YouTube, Pinterest, Reddit, Medium, Telegram, Discord, Slack, Teams, Mailchimp, SendGrid, Brevo, and more. Features scheduled posting, template placeholders, UTM tagging, link shortening, caption rotation, and per-article service selection."
PKG_MOKOSUITECROSS_PHP_VERSION_ERROR="MokoSuiteCross requires PHP %s or later." PKG_MOKOSUITECROSS_PHP_VERSION_ERROR="MokoSuiteCross requires PHP %s or later."
PKG_MOKOSUITECROSS_MIGRATION_DETECTED="Perfect Publisher Pro detected! Navigate to Components → MokoSuiteCross → Dashboard to migrate your settings." PKG_MOKOSUITECROSS_MIGRATION_DETECTED="Perfect Publisher Pro detected! Navigate to Components → MokoSuiteCross → Dashboard to migrate your settings."
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<access component="com_mokosuitecross"> <access component="com_mokosuitecross">
<section name="component"> <section name="component">
<!-- Joomla core actions -->
<action name="core.admin" title="JACTION_ADMIN" /> <action name="core.admin" title="JACTION_ADMIN" />
<action name="core.options" title="JACTION_OPTIONS" /> <action name="core.options" title="JACTION_OPTIONS" />
<action name="core.manage" title="JACTION_MANAGE" /> <action name="core.manage" title="JACTION_MANAGE" />
@@ -8,7 +9,18 @@
<action name="core.delete" title="JACTION_DELETE" /> <action name="core.delete" title="JACTION_DELETE" />
<action name="core.edit" title="JACTION_EDIT" /> <action name="core.edit" title="JACTION_EDIT" />
<action name="core.edit.state" title="JACTION_EDITSTATE" /> <action name="core.edit.state" title="JACTION_EDITSTATE" />
<!-- Component-specific actions -->
<action name="mokosuitecross.crosspost" title="COM_MOKOSUITECROSS_ACTION_CROSSPOST" /> <action name="mokosuitecross.crosspost" title="COM_MOKOSUITECROSS_ACTION_CROSSPOST" />
<action name="mokosuitecross.crosspost.manual" title="COM_MOKOSUITECROSS_ACTION_CROSSPOST_MANUAL" />
<action name="mokosuitecross.delete.remote" title="COM_MOKOSUITECROSS_ACTION_DELETE_REMOTE" />
<action name="mokosuitecross.services.manage" title="COM_MOKOSUITECROSS_ACTION_SERVICES_MANAGE" />
<action name="mokosuitecross.services.credentials" title="COM_MOKOSUITECROSS_ACTION_SERVICES_CREDENTIALS" />
<action name="mokosuitecross.templates.manage" title="COM_MOKOSUITECROSS_ACTION_TEMPLATES_MANAGE" />
<action name="mokosuitecross.logs.view" title="COM_MOKOSUITECROSS_ACTION_LOGS_VIEW" />
<action name="mokosuitecross.logs.purge" title="COM_MOKOSUITECROSS_ACTION_LOGS_PURGE" />
<action name="mokosuitecross.queue.manage" title="COM_MOKOSUITECROSS_ACTION_QUEUE_MANAGE" />
<action name="mokosuitecross.queue.export" title="COM_MOKOSUITECROSS_ACTION_QUEUE_EXPORT" />
<action name="mokosuitecross.dispatch" title="COM_MOKOSUITECROSS_ACTION_DISPATCH" />
<action name="mokosuitecross.migrate" title="COM_MOKOSUITECROSS_ACTION_MIGRATE" /> <action name="mokosuitecross.migrate" title="COM_MOKOSUITECROSS_ACTION_MIGRATE" />
</section> </section>
</access> </access>
@@ -24,6 +24,17 @@
<option value="0">JNO</option> <option value="0">JNO</option>
</field> </field>
<field
name="delete_on_unpublish"
type="radio"
label="COM_MOKOSUITECROSS_CONFIG_DELETE_ON_UNPUBLISH"
description="COM_MOKOSUITECROSS_CONFIG_DELETE_ON_UNPUBLISH_DESC"
default="0"
class="btn-group">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field <field
name="retry_max" name="retry_max"
type="number" type="number"
@@ -64,6 +75,51 @@
/> />
</fieldset> </fieldset>
<fieldset name="utm" label="COM_MOKOSUITECROSS_CONFIG_UTM">
<field
name="utm_enabled"
type="radio"
label="COM_MOKOSUITECROSS_CONFIG_UTM_ENABLED"
description="COM_MOKOSUITECROSS_CONFIG_UTM_ENABLED_DESC"
default="0"
class="btn-group">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field
name="utm_source"
type="text"
label="COM_MOKOSUITECROSS_CONFIG_UTM_SOURCE"
description="COM_MOKOSUITECROSS_CONFIG_UTM_SOURCE_DESC"
default="{platform}"
showon="utm_enabled:1"
/>
<field
name="utm_medium"
type="text"
label="COM_MOKOSUITECROSS_CONFIG_UTM_MEDIUM"
description="COM_MOKOSUITECROSS_CONFIG_UTM_MEDIUM_DESC"
default="social"
showon="utm_enabled:1"
/>
<field
name="utm_campaign"
type="text"
label="COM_MOKOSUITECROSS_CONFIG_UTM_CAMPAIGN"
description="COM_MOKOSUITECROSS_CONFIG_UTM_CAMPAIGN_DESC"
default="mokosuitecross"
showon="utm_enabled:1"
/>
<field
name="utm_content"
type="text"
label="COM_MOKOSUITECROSS_CONFIG_UTM_CONTENT"
description="COM_MOKOSUITECROSS_CONFIG_UTM_CONTENT_DESC"
hint="Optional"
showon="utm_enabled:1"
/>
</fieldset>
<fieldset name="evergreen" label="COM_MOKOSUITECROSS_CONFIG_EVERGREEN"> <fieldset name="evergreen" label="COM_MOKOSUITECROSS_CONFIG_EVERGREEN">
<field <field
name="evergreen_enabled" name="evergreen_enabled"
@@ -143,4 +199,19 @@
description="COM_MOKOSUITECROSS_CONFIG_CATEGORY_RULES_NOTE_DESC" description="COM_MOKOSUITECROSS_CONFIG_CATEGORY_RULES_NOTE_DESC"
/> />
</fieldset> </fieldset>
<fieldset
name="permissions"
label="JCONFIG_PERMISSIONS_LABEL"
description="JCONFIG_PERMISSIONS_DESC">
<field
name="rules"
type="rules"
label="JCONFIG_PERMISSIONS_LABEL"
component="com_mokosuitecross"
filter="rules"
validate="rules"
section="component"
/>
</fieldset>
</config> </config>
@@ -5,6 +5,20 @@
COM_MOKOSUITECROSS="MokoSuiteCross" COM_MOKOSUITECROSS="MokoSuiteCross"
COM_MOKOSUITECROSS_DESCRIPTION="Cross-posting Joomla content to social media, email marketing, and chat platforms" COM_MOKOSUITECROSS_DESCRIPTION="Cross-posting Joomla content to social media, email marketing, and chat platforms"
; ACL Actions
COM_MOKOSUITECROSS_ACTION_CROSSPOST="Cross-Post Articles"
COM_MOKOSUITECROSS_ACTION_CROSSPOST_MANUAL="Manually Create Posts"
COM_MOKOSUITECROSS_ACTION_DELETE_REMOTE="Delete from Remote Platforms"
COM_MOKOSUITECROSS_ACTION_SERVICES_MANAGE="Manage Services"
COM_MOKOSUITECROSS_ACTION_SERVICES_CREDENTIALS="View Service Credentials"
COM_MOKOSUITECROSS_ACTION_TEMPLATES_MANAGE="Manage Templates"
COM_MOKOSUITECROSS_ACTION_LOGS_VIEW="View Activity Logs"
COM_MOKOSUITECROSS_ACTION_LOGS_PURGE="Purge Activity Logs"
COM_MOKOSUITECROSS_ACTION_QUEUE_MANAGE="Manage Post Queue"
COM_MOKOSUITECROSS_ACTION_QUEUE_EXPORT="Export Post Queue"
COM_MOKOSUITECROSS_ACTION_DISPATCH="Trigger API Dispatch"
COM_MOKOSUITECROSS_ACTION_MIGRATE="Run Migration"
; Submenu ; Submenu
COM_MOKOSUITECROSS_SUBMENU_DASHBOARD="Dashboard" COM_MOKOSUITECROSS_SUBMENU_DASHBOARD="Dashboard"
COM_MOKOSUITECROSS_SUBMENU_POSTS="Post Queue" COM_MOKOSUITECROSS_SUBMENU_POSTS="Post Queue"
@@ -476,6 +490,19 @@ COM_MOKOSUITECROSS_DASHBOARD_QUEUE_DEPTH_WARNING_TITLE="Large queue backlog"
COM_MOKOSUITECROSS_DASHBOARD_QUEUE_DEPTH_WARNING="There are %d posts waiting in the queue. Please verify that the Joomla Task Scheduler is running and the MokoSuiteCross scheduled task is enabled in System → Scheduled Tasks." COM_MOKOSUITECROSS_DASHBOARD_QUEUE_DEPTH_WARNING="There are %d posts waiting in the queue. Please verify that the Joomla Task Scheduler is running and the MokoSuiteCross scheduled task is enabled in System → Scheduled Tasks."
; First-Publish-Only ; First-Publish-Only
COM_MOKOSUITECROSS_CONFIG_DELETE_ON_UNPUBLISH="Delete from Platforms on Unpublish"
COM_MOKOSUITECROSS_CONFIG_DELETE_ON_UNPUBLISH_DESC="When an article is unpublished or trashed, automatically delete the cross-posted content from remote platforms (where supported)."
COM_MOKOSUITECROSS_CONFIG_UTM="UTM Tracking"
COM_MOKOSUITECROSS_CONFIG_UTM_ENABLED="Enable UTM Parameters"
COM_MOKOSUITECROSS_CONFIG_UTM_ENABLED_DESC="Append UTM tracking parameters to article URLs in cross-posted content for Google Analytics tracking."
COM_MOKOSUITECROSS_CONFIG_UTM_SOURCE="UTM Source"
COM_MOKOSUITECROSS_CONFIG_UTM_SOURCE_DESC="Value for utm_source. Use {platform} to auto-insert the service type (e.g. facebook, twitter, telegram)."
COM_MOKOSUITECROSS_CONFIG_UTM_MEDIUM="UTM Medium"
COM_MOKOSUITECROSS_CONFIG_UTM_MEDIUM_DESC="Value for utm_medium. Default: social."
COM_MOKOSUITECROSS_CONFIG_UTM_CAMPAIGN="UTM Campaign"
COM_MOKOSUITECROSS_CONFIG_UTM_CAMPAIGN_DESC="Value for utm_campaign. Default: mokosuitecross."
COM_MOKOSUITECROSS_CONFIG_UTM_CONTENT="UTM Content"
COM_MOKOSUITECROSS_CONFIG_UTM_CONTENT_DESC="Optional value for utm_content. Leave empty to omit."
COM_MOKOSUITECROSS_CONFIG_FIRST_PUBLISH_ONLY="First Publish Only" COM_MOKOSUITECROSS_CONFIG_FIRST_PUBLISH_ONLY="First Publish Only"
COM_MOKOSUITECROSS_CONFIG_FIRST_PUBLISH_ONLY_DESC="When enabled, articles are only cross-posted on their first save as published. Subsequent edits to already-published articles will not trigger new cross-posts." COM_MOKOSUITECROSS_CONFIG_FIRST_PUBLISH_ONLY_DESC="When enabled, articles are only cross-posted on their first save as published. Subsequent edits to already-published articles will not trigger new cross-posts."
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<extension type="component" method="upgrade"> <extension type="component" method="upgrade">
<name>com_mokosuitecross</name> <name>com_mokosuitecross</name>
<version>01.02.00-rc</version> <version>01.08.37</version>
<creationDate>2026-05-28</creationDate> <creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author> <author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -25,7 +25,7 @@ CREATE TABLE IF NOT EXISTS `#__mokosuitecross_posts` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT, `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`article_id` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'FK to #__content.id', `article_id` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'FK to #__content.id',
`service_id` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'FK to #__mokosuitecross_services.id', `service_id` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'FK to #__mokosuitecross_services.id',
`status` varchar(20) NOT NULL DEFAULT 'queued' COMMENT 'queued, posting, posted, failed, scheduled', `status` varchar(20) NOT NULL DEFAULT 'queued' COMMENT 'queued, posting, posted, failed, scheduled, deleted',
`message` text NOT NULL COMMENT 'Rendered message sent to platform', `message` text NOT NULL COMMENT 'Rendered message sent to platform',
`platform_post_id` varchar(255) NOT NULL DEFAULT '' COMMENT 'Post ID returned by platform', `platform_post_id` varchar(255) NOT NULL DEFAULT '' COMMENT 'Post ID returned by platform',
`platform_response` text NOT NULL COMMENT 'JSON — full API response from platform', `platform_response` text NOT NULL COMMENT 'JSON — full API response from platform',
@@ -74,25 +74,27 @@ CREATE TABLE IF NOT EXISTS `#__mokosuitecross_logs` (
-- Insert default templates -- Insert default templates
INSERT INTO `#__mokosuitecross_templates` (`service_type`, `title`, `template_body`, `published`, `ordering`, `created`) VALUES INSERT INTO `#__mokosuitecross_templates` (`service_type`, `title`, `template_body`, `published`, `ordering`, `created`) VALUES
('default', 'Default Template', '{title}\n\n{introtext}\n\n{url}', 1, 1, NOW()), ('default', 'Default Template', '{title}\n\n{introtext}\n\n{url}', 1, 1, NOW()),
('twitter', 'Twitter/X Default', '{title}\n\n{url}', 1, 2, NOW()), ('twitter', 'Twitter/X Default', '{short}\n\n{url}', 1, 2, NOW()),
('mastodon', 'Mastodon Default', '{title}\n\n{introtext}\n\n{url}\n\n#Joomla', 1, 3, NOW()), ('mastodon', 'Mastodon Default', '{social}\n\n{url}\n\n{hashtags}', 1, 3, NOW()),
('mailchimp', 'Mailchimp Default', '<h1>{title}</h1>\n<p>{introtext}</p>\n<p><a href=\"{url}\">Read more</a></p>', 1, 4, NOW()), ('mailchimp', 'Mailchimp Default', '<h1>{email_subject}</h1>\n{email_body}\n<p><a href=\"{url}\">Read more</a></p>', 1, 4, NOW()),
('telegram', 'Telegram Default', '<b>{title}</b>\n\n{introtext}\n\n<a href=\"{url}\">Read more</a>', 1, 5, NOW()), ('telegram', 'Telegram Default', '<b>{title}</b>\n\n{chat}\n\n<a href=\"{url}\">Read more</a>', 1, 5, NOW()),
('discord', 'Discord Default', '**{title}**\n\n{introtext}\n\n{url}', 1, 6, NOW()), ('discord', 'Discord Default', '**{title}**\n\n{chat}\n\n{url}', 1, 6, NOW()),
('slack', 'Slack Default', '*{title}*\n\n{introtext}\n\n{url}', 1, 7, NOW()), ('slack', 'Slack Default', '*{title}*\n\n{chat}\n\n{url}', 1, 7, NOW()),
('facebook', 'Facebook Default', '{title}\n\n{introtext}\n\n{url}', 1, 8, NOW()), ('facebook', 'Facebook Default', '{social}\n\n{url}', 1, 8, NOW()),
('linkedin', 'LinkedIn Default', '{title}\n\n{introtext}\n\n{url}', 1, 9, NOW()), ('linkedin', 'LinkedIn Default', '{social}\n\n{url}\n\n{hashtags}', 1, 9, NOW()),
('bluesky', 'Bluesky Default', '{title}\n\n{url}', 1, 10, NOW()), ('bluesky', 'Bluesky Default', '{short}\n\n{url}', 1, 10, NOW()),
('threads', 'Threads Default', '{title}\n\n{introtext}\n\n{url}', 1, 11, NOW()), ('threads', 'Threads Default', '{social}\n\n{url}', 1, 11, NOW()),
('teams', 'Teams Default', '**{title}**\n\n{introtext}\n\n[Read more]({url})', 1, 12, NOW()), ('teams', 'Teams Default', '**{title}**\n\n{chat}\n\n[Read more]({url})', 1, 12, NOW()),
('medium', 'Medium Default', '{title}\n\n{introtext}\n\n{url}', 1, 13, NOW()), ('medium', 'Medium Default', '{title}\n\n{social}\n\n{url}', 1, 13, NOW()),
('wordpress', 'WordPress Default', '{title}\n\n{introtext}\n\n{url}', 1, 14, NOW()), ('wordpress', 'WordPress Default', '{title}\n\n{introtext}\n\n{url}', 1, 14, NOW()),
('webhook', 'Webhook Default', '{title}\n\n{introtext}\n\n{url}', 1, 15, NOW()), ('webhook', 'Webhook Default', '{title}\n\n{introtext}\n\n{url}', 1, 15, NOW()),
('sendgrid', 'SendGrid Default', '<h1>{title}</h1>\n<p>{introtext}</p>\n<p><a href=\"{url}\">Read more</a></p>', 1, 16, NOW()), ('sendgrid', 'SendGrid Default', '<h1>{email_subject}</h1>\n{email_body}\n<p><a href=\"{url}\">Read more</a></p>', 1, 16, NOW()),
('brevo', 'Brevo Default', '<h1>{title}</h1>\n<p>{introtext}</p>\n<p><a href=\"{url}\">Read more</a></p>', 1, 17, NOW()), ('brevo', 'Brevo Default', '<h1>{email_subject}</h1>\n{email_body}\n<p><a href=\"{url}\">Read more</a></p>', 1, 17, NOW()),
('ntfy', 'Ntfy Default', '{title}: {introtext}', 1, 18, NOW()), ('ntfy', 'Ntfy Default', '{title}: {short}', 1, 18, NOW()),
('reddit', 'Reddit Default', '{title}', 1, 19, NOW()), ('reddit', 'Reddit Default', '{title}', 1, 19, NOW()),
('pinterest', 'Pinterest Default', '{title} - {introtext}', 1, 20, NOW()); ('pinterest', 'Pinterest Default', '{title} - {social}', 1, 20, NOW()),
('instagram', 'Instagram Default', '{social}\n\n{hashtags}', 1, 21, NOW()),
('youtube', 'YouTube Default', '{social}\n\n{url}', 1, 22, NOW());
CREATE TABLE IF NOT EXISTS `#__mokosuitecross_category_rules` ( CREATE TABLE IF NOT EXISTS `#__mokosuitecross_category_rules` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT, `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
@@ -0,0 +1 @@
/* 01.08.05 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.07 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.08 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.09 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.10 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.11 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.12 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.13 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.14 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.15 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.16 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.17 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.19 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.20 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.21 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.22 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.23 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.24 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.25 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.26 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.27 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.28 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.29 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.30 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.31 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.32 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.33 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.34 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.35 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.36 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.37 — no schema changes */
@@ -56,7 +56,7 @@ class DispatchController extends BaseController
} }
// ACL check — require core.manage on the component // ACL check — require core.manage on the component
if (!Factory::getApplication()->getIdentity()->authorise('core.manage', 'com_mokosuitecross')) { if (!Factory::getApplication()->getIdentity()->authorise('mokosuitecross.dispatch', 'com_mokosuitecross')) {
$this->sendJsonResponse(['error' => 'Forbidden'], 403); $this->sendJsonResponse(['error' => 'Forbidden'], 403);
return; return;
@@ -13,6 +13,7 @@ namespace Joomla\Component\MokoSuiteCross\Administrator\Controller;
defined('_JEXEC') or die; defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Controller\BaseController; use Joomla\CMS\MVC\Controller\BaseController;
class DisplayController extends BaseController class DisplayController extends BaseController
@@ -23,4 +24,13 @@ class DisplayController extends BaseController
* @var string * @var string
*/ */
protected $default_view = 'dashboard'; protected $default_view = 'dashboard';
public function display($cachable = false, $urlparams = [])
{
if (!$this->app->getIdentity()->authorise('core.manage', 'com_mokosuitecross')) {
throw new \Joomla\CMS\Access\Exception\NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403);
}
return parent::display($cachable, $urlparams);
}
} }
@@ -161,7 +161,7 @@ class PostsController extends AdminController
{ {
$this->checkToken('get'); $this->checkToken('get');
if (!$this->app->getIdentity()->authorise('core.manage', 'com_mokosuitecross')) { if (!$this->app->getIdentity()->authorise('mokosuitecross.queue.export', 'com_mokosuitecross')) {
throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403);
} }
@@ -31,7 +31,7 @@ class ServiceController extends FormController
{ {
$this->checkToken(); $this->checkToken();
if (!$this->app->getIdentity()->authorise('core.manage', 'com_mokosuitecross')) { if (!$this->app->getIdentity()->authorise('mokosuitecross.services.manage', 'com_mokosuitecross')) {
throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403);
} }
@@ -96,7 +96,7 @@ class ServiceController extends FormController
$app->mimeType = 'application/json'; $app->mimeType = 'application/json';
$app->setHeader('Content-Type', 'application/json; charset=utf-8'); $app->setHeader('Content-Type', 'application/json; charset=utf-8');
echo new JsonResponse($e); echo new JsonResponse(['error' => $e->getMessage()]);
} }
$app->close(); $app->close();
@@ -21,4 +21,22 @@ class ServicesController extends AdminController
{ {
return parent::getModel($name, $prefix, $config); return parent::getModel($name, $prefix, $config);
} }
public function publish(): void
{
if (!$this->app->getIdentity()->authorise('core.edit.state', 'com_mokosuitecross')) {
throw new \Joomla\CMS\Access\Exception\NotAllowed(\Joomla\CMS\Language\Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 403);
}
parent::publish();
}
public function delete(): void
{
if (!$this->app->getIdentity()->authorise('core.delete', 'com_mokosuitecross')) {
throw new \Joomla\CMS\Access\Exception\NotAllowed(\Joomla\CMS\Language\Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), 403);
}
parent::delete();
}
} }
@@ -21,4 +21,22 @@ class TemplatesController extends AdminController
{ {
return parent::getModel($name, $prefix, $config); return parent::getModel($name, $prefix, $config);
} }
public function publish(): void
{
if (!$this->app->getIdentity()->authorise('mokosuitecross.templates.manage', 'com_mokosuitecross')) {
throw new \Joomla\CMS\Access\Exception\NotAllowed(\Joomla\CMS\Language\Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 403);
}
parent::publish();
}
public function delete(): void
{
if (!$this->app->getIdentity()->authorise('mokosuitecross.templates.manage', 'com_mokosuitecross')) {
throw new \Joomla\CMS\Access\Exception\NotAllowed(\Joomla\CMS\Language\Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), 403);
}
parent::delete();
}
} }
@@ -17,6 +17,7 @@ use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Factory; use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\PluginHelper; use Joomla\CMS\Plugin\PluginHelper;
use Joomla\CMS\Uri\Uri; use Joomla\CMS\Uri\Uri;
use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossDeleteInterface;
use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface;
/** /**
@@ -243,7 +244,21 @@ class CrossPostDispatcher
$params = json_decode($service->params ?: '{}', true) ?: []; $params = json_decode($service->params ?: '{}', true) ?: [];
if (!empty($articleUrl)) { if (!empty($articleUrl)) {
$params['_article_url'] = $articleUrl; $params['article_url'] = $articleUrl;
}
// Pass article title for platforms that need it (e.g. Bluesky link cards)
$db2 = Factory::getDbo();
$postRow = $db2->setQuery(
$db2->getQuery(true)->select('article_id')->from('#__mokosuitecross_posts')->where('id = ' . $postId)
)->loadObject();
if ($postRow && $postRow->article_id) {
$articleTitle = $db2->setQuery(
$db2->getQuery(true)->select('title')->from('#__content')->where('id = ' . (int) $postRow->article_id)
)->loadResult();
if ($articleTitle) {
$params['article_title'] = $articleTitle;
}
} }
// Lifecycle event: before post // Lifecycle event: before post
@@ -383,12 +398,34 @@ class CrossPostDispatcher
$authorName = $db->loadResult() ?: ''; $authorName = $db->loadResult() ?: '';
} }
$introImage = ''; // Resolve share image from article attribs
$attribs = json_decode($article->attribs ?? '{}', true) ?: [];
$imageMode = $attribs['mokosuitecross_share_image'] ?? 'intro';
$images = json_decode($article->images ?? '{}'); $images = json_decode($article->images ?? '{}');
$introImage = '';
switch ($imageMode) {
case 'fulltext':
if (!empty($images->image_fulltext)) {
$introImage = Uri::root() . ltrim($images->image_fulltext, '/');
}
break;
case 'custom':
$customImg = $attribs['mokosuitecross_custom_image'] ?? '';
if (!empty($customImg)) {
$introImage = Uri::root() . ltrim($customImg, '/');
}
break;
case 'none':
$introImage = '';
break;
case 'intro':
default:
if (!empty($images->image_intro)) { if (!empty($images->image_intro)) {
$introImage = Uri::root() . ltrim($images->image_intro, '/'); $introImage = Uri::root() . ltrim($images->image_intro, '/');
} }
break;
}
$tagNames = []; $tagNames = [];
@@ -410,17 +447,54 @@ class CrossPostDispatcher
return '#' . preg_replace('/\s+/', '', $tag); return '#' . preg_replace('/\s+/', '', $tag);
}, $tagNames)); }, $tagNames));
// Per-article share text (from article editor Share Content panel)
$socialText = $attribs['mokosuitecross_social_text'] ?? '';
$shortText = $attribs['mokosuitecross_short_text'] ?? '';
$chatText = $attribs['mokosuitecross_chat_text'] ?? '';
$emailSubject = $attribs['mokosuitecross_email_subject'] ?? '';
$emailBody = $attribs['mokosuitecross_email_body'] ?? '';
$introStripped = strip_tags(mb_substr($article->introtext ?? '', 0, 280));
$titleText = $article->title ?? '';
// UTM auto-tagging (#154)
$componentParams = ComponentHelper::getParams('com_mokosuitecross');
$urlRaw = $url;
if ($componentParams->get('utm_enabled', 0)) {
$utmParams = [
'utm_source' => $componentParams->get('utm_source', '{platform}'),
'utm_medium' => $componentParams->get('utm_medium', 'social'),
'utm_campaign' => $componentParams->get('utm_campaign', 'mokosuitecross'),
];
$utmContent = $componentParams->get('utm_content', '');
if (!empty($utmContent)) {
$utmParams['utm_content'] = $utmContent;
}
$separator = (strpos($url, '?') !== false) ? '&' : '?';
$url = $url . $separator . http_build_query($utmParams);
}
return [ return [
'{title}' => $article->title ?? '', '{title}' => $titleText,
'{introtext}' => strip_tags(mb_substr($article->introtext ?? '', 0, 280)), '{introtext}' => $introStripped,
'{fulltext}' => strip_tags(mb_substr($article->fulltext ?? '', 0, 500)), '{fulltext}' => strip_tags(mb_substr($article->fulltext ?? '', 0, 500)),
'{url}' => $url, '{url}' => $url,
'{url_raw}' => $urlRaw,
'{image}' => $introImage, '{image}' => $introImage,
'{category}' => $categoryName, '{category}' => $categoryName,
'{author}' => $authorName, '{author}' => $authorName,
'{date}' => Factory::getDate($article->publish_up ?? 'now')->format('Y-m-d'), '{date}' => Factory::getDate($article->publish_up ?? 'now')->format('Y-m-d'),
'{tags}' => $tagsComma, '{tags}' => $tagsComma,
'{hashtags}' => $hashtags, '{hashtags}' => $hashtags,
// Platform-specific share content (falls back to introtext/title if empty)
'{social}' => !empty($socialText) ? $socialText : $introStripped,
'{short}' => !empty($shortText) ? $shortText : mb_substr($titleText, 0, 250),
'{chat}' => !empty($chatText) ? $chatText : $introStripped,
'{email_subject}' => !empty($emailSubject) ? $emailSubject : $titleText,
'{email_body}' => !empty($emailBody) ? $emailBody : ($article->fulltext ?? $article->introtext ?? ''),
]; ];
} }
@@ -459,6 +533,15 @@ class CrossPostDispatcher
$message = str_replace(array_keys($replacements), array_values($replacements), $template); $message = str_replace(array_keys($replacements), array_values($replacements), $template);
// Resolve {platform} token in UTM params (replaced with service_type)
$message = str_replace('{platform}', $service->service_type, $message);
// Resolve caption rotation: {random:option1|option2|option3} (#155)
$message = preg_replace_callback('/\{random:([^}]+)\}/', function ($matches) {
$options = explode('|', $matches[1]);
return $options[array_rand($options)];
}, $message);
// Resolve custom field placeholders: {field:field_name} // Resolve custom field placeholders: {field:field_name}
$message = preg_replace_callback('/\{field:([a-zA-Z0-9_-]+)\}/', function ($matches) use ($db, $article) { $message = preg_replace_callback('/\{field:([a-zA-Z0-9_-]+)\}/', function ($matches) use ($db, $article) {
$fieldName = $matches[1]; $fieldName = $matches[1];
@@ -478,6 +561,82 @@ class CrossPostDispatcher
/** /**
* Write an entry to the activity log. * Write an entry to the activity log.
*/ */
/**
* Delete cross-posted content from remote platforms for a given article.
*
* Finds all posts with status 'posted' for this article, resolves the
* service plugin, and calls deletePost() if the plugin supports it.
*
* @param int $articleId The Joomla article ID
*/
public static function deleteFromPlatforms(int $articleId): void
{
$db = Factory::getDbo();
// Find all successfully posted entries for this article
$query = $db->getQuery(true)
->select('p.*, s.service_type, s.credentials')
->from($db->quoteName('#__mokosuitecross_posts', 'p'))
->join('INNER', $db->quoteName('#__mokosuitecross_services', 's')
. ' ON ' . $db->quoteName('s.id') . ' = ' . $db->quoteName('p.service_id'))
->where($db->quoteName('p.article_id') . ' = ' . $articleId)
->where($db->quoteName('p.status') . ' = ' . $db->quote('posted'))
->where($db->quoteName('p.platform_post_id') . ' != ' . $db->quote(''));
$db->setQuery($query);
$posts = $db->loadObjectList();
if (empty($posts)) {
return;
}
// Load service plugins
PluginHelper::importPlugin('mokosuitecross');
$plugins = [];
Factory::getApplication()->triggerEvent('onMokoSuiteCrossGetServices', [&$plugins]);
$pluginMap = [];
foreach ($plugins as $plugin) {
$pluginMap[$plugin->getServiceType()] = $plugin;
}
foreach ($posts as $post) {
$plugin = $pluginMap[$post->service_type] ?? null;
if (!$plugin instanceof MokoSuiteCrossDeleteInterface) {
self::log($db, $post->id, $post->service_id, 'info',
'Delete not supported for ' . $post->service_type);
continue;
}
$credentials = json_decode($post->credentials, true) ?: [];
try {
$result = $plugin->deletePost($post->platform_post_id, $credentials);
if (!empty($result['success'])) {
// Mark as deleted
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__mokosuitecross_posts'))
->set($db->quoteName('status') . ' = ' . $db->quote('deleted'))
->where($db->quoteName('id') . ' = ' . (int) $post->id)
);
$db->execute();
self::log($db, $post->id, $post->service_id, 'info',
'Deleted from ' . $post->service_type . ': ' . ($result['message'] ?? 'OK'));
} else {
self::log($db, $post->id, $post->service_id, 'warning',
'Delete failed on ' . $post->service_type . ': ' . ($result['message'] ?? 'Unknown error'));
}
} catch (\Throwable $e) {
self::log($db, $post->id, $post->service_id, 'error',
'Delete exception on ' . $post->service_type . ': ' . $e->getMessage());
}
}
}
private static function log($db, ?int $postId, ?int $serviceId, string $level, string $message): void private static function log($db, ?int $postId, ?int $serviceId, string $level, string $message): void
{ {
$log = (object) [ $log = (object) [
@@ -71,4 +71,26 @@ class MokoSuiteCrossHelper
); );
} }
} }
/**
* Get a list of ACL actions for the component.
*
* @return \Joomla\CMS\Object\CMSObject
*/
public static function getActions(): \Joomla\CMS\Object\CMSObject
{
$user = \Joomla\CMS\Factory::getApplication()->getIdentity();
$result = new \Joomla\CMS\Object\CMSObject();
$actions = \Joomla\CMS\Access\Access::getActionsFromFile(
JPATH_ADMINISTRATOR . '/components/com_mokosuitecross/access.xml',
'/access/section[@name="component"]/'
);
foreach ($actions as $action) {
$result->set($action->name, $user->authorise($action->name, 'com_mokosuitecross'));
}
return $result;
}
} }
@@ -0,0 +1,35 @@
<?php
/**
* @package MokoSuiteCross
* @subpackage com_mokosuitecross
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Joomla\Component\MokoSuiteCross\Administrator\Service;
defined('_JEXEC') or die;
/**
* Optional interface for service plugins that support deleting posts
* from the remote platform.
*
* Plugins that implement this can be invoked when a Joomla article
* is unpublished or trashed, or when a user manually requests deletion
* from the Post Queue view.
*/
interface MokoSuiteCrossDeleteInterface
{
/**
* Delete a previously published post from the remote platform.
*
* @param string $platformPostId The platform-specific post ID
* @param array $credentials Decrypted credentials for this service
*
* @return array ['success' => bool, 'message' => string]
*/
public function deletePost(string $platformPostId, array $credentials): array;
}
@@ -65,7 +65,11 @@ class HtmlView extends BaseHtmlView
protected function addToolbar(): void protected function addToolbar(): void
{ {
$canDo = MokoSuiteCrossHelper::getActions();
ToolbarHelper::title('MokoSuiteCross — Dashboard', 'share-alt'); ToolbarHelper::title('MokoSuiteCross — Dashboard', 'share-alt');
if ($canDo->get('core.admin')) {
ToolbarHelper::preferences('com_mokosuitecross'); ToolbarHelper::preferences('com_mokosuitecross');
} }
} }
}
@@ -41,6 +41,8 @@ class HtmlView extends BaseHtmlView
protected function addToolbar(): void protected function addToolbar(): void
{ {
$canDo = MokoSuiteCrossHelper::getActions();
ToolbarHelper::title('MokoSuiteCross — Activity Logs', 'share-alt'); ToolbarHelper::title('MokoSuiteCross — Activity Logs', 'share-alt');
ToolbarHelper::deleteList('JGLOBAL_CONFIRM_DELETE', 'logs.delete', 'JTOOLBAR_DELETE'); ToolbarHelper::deleteList('JGLOBAL_CONFIRM_DELETE', 'logs.delete', 'JTOOLBAR_DELETE');
@@ -18,6 +18,7 @@ use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Router\Route; use Joomla\CMS\Router\Route;
use Joomla\CMS\Toolbar\Toolbar; use Joomla\CMS\Toolbar\Toolbar;
use Joomla\CMS\Toolbar\ToolbarHelper; use Joomla\CMS\Toolbar\ToolbarHelper;
use Joomla\Component\MokoSuiteCross\Administrator\Helper\MokoSuiteCrossHelper;
class HtmlView extends BaseHtmlView class HtmlView extends BaseHtmlView
{ {
@@ -37,14 +38,17 @@ class HtmlView extends BaseHtmlView
protected function addToolbar(): void protected function addToolbar(): void
{ {
$isNew = empty($this->item->id); $isNew = empty($this->item->id);
$canDo = MokoSuiteCrossHelper::getActions();
ToolbarHelper::title( ToolbarHelper::title(
'MokoSuiteCross — ' . ($isNew ? Text::_('COM_MOKOSUITECROSS_NEW_POST') : Text::_('COM_MOKOSUITECROSS_EDIT_POST')), 'MokoSuiteCross — ' . ($isNew ? Text::_('COM_MOKOSUITECROSS_NEW_POST') : Text::_('COM_MOKOSUITECROSS_EDIT_POST')),
'share-alt' 'share-alt'
); );
if ($canDo->get('mokosuitecross.queue.manage')) {
ToolbarHelper::apply('post.apply'); ToolbarHelper::apply('post.apply');
ToolbarHelper::save('post.save'); ToolbarHelper::save('post.save');
}
$toolbar = Toolbar::getInstance('toolbar'); $toolbar = Toolbar::getInstance('toolbar');
$toolbar->appendButton( $toolbar->appendButton(
@@ -43,6 +43,8 @@ class HtmlView extends BaseHtmlView
protected function addToolbar(): void protected function addToolbar(): void
{ {
$canDo = MokoSuiteCrossHelper::getActions();
ToolbarHelper::title('MokoSuiteCross — Post Queue', 'share-alt'); ToolbarHelper::title('MokoSuiteCross — Post Queue', 'share-alt');
ToolbarHelper::addNew('post.add'); ToolbarHelper::addNew('post.add');
@@ -19,6 +19,7 @@ use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Router\Route; use Joomla\CMS\Router\Route;
use Joomla\CMS\Toolbar\Toolbar; use Joomla\CMS\Toolbar\Toolbar;
use Joomla\CMS\Toolbar\ToolbarHelper; use Joomla\CMS\Toolbar\ToolbarHelper;
use Joomla\Component\MokoSuiteCross\Administrator\Helper\MokoSuiteCrossHelper;
class HtmlView extends BaseHtmlView class HtmlView extends BaseHtmlView
{ {
@@ -38,14 +39,17 @@ class HtmlView extends BaseHtmlView
protected function addToolbar(): void protected function addToolbar(): void
{ {
$isNew = empty($this->item->id); $isNew = empty($this->item->id);
$canDo = MokoSuiteCrossHelper::getActions();
ToolbarHelper::title( ToolbarHelper::title(
'MokoSuiteCross — ' . ($isNew ? Text::_('COM_MOKOSUITECROSS_NEW_SERVICE') : Text::_('COM_MOKOSUITECROSS_EDIT_SERVICE')), 'MokoSuiteCross — ' . ($isNew ? Text::_('COM_MOKOSUITECROSS_NEW_SERVICE') : Text::_('COM_MOKOSUITECROSS_EDIT_SERVICE')),
'share-alt' 'share-alt'
); );
if ($canDo->get('mokosuitecross.services.manage')) {
ToolbarHelper::apply('service.apply'); ToolbarHelper::apply('service.apply');
ToolbarHelper::save('service.save'); ToolbarHelper::save('service.save');
}
// Dashboard button in toolbar // Dashboard button in toolbar
$toolbar = Toolbar::getInstance('toolbar'); $toolbar = Toolbar::getInstance('toolbar');
@@ -41,6 +41,8 @@ class HtmlView extends BaseHtmlView
protected function addToolbar(): void protected function addToolbar(): void
{ {
$canDo = MokoSuiteCrossHelper::getActions();
ToolbarHelper::title('MokoSuiteCross — Services', 'share-alt'); ToolbarHelper::title('MokoSuiteCross — Services', 'share-alt');
ToolbarHelper::addNew('service.add'); ToolbarHelper::addNew('service.add');
ToolbarHelper::editList('service.edit'); ToolbarHelper::editList('service.edit');
@@ -17,6 +17,7 @@ use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Router\Route; use Joomla\CMS\Router\Route;
use Joomla\CMS\Toolbar\Toolbar; use Joomla\CMS\Toolbar\Toolbar;
use Joomla\CMS\Toolbar\ToolbarHelper; use Joomla\CMS\Toolbar\ToolbarHelper;
use Joomla\Component\MokoSuiteCross\Administrator\Helper\MokoSuiteCrossHelper;
class HtmlView extends BaseHtmlView class HtmlView extends BaseHtmlView
{ {
@@ -36,13 +37,17 @@ class HtmlView extends BaseHtmlView
protected function addToolbar(): void protected function addToolbar(): void
{ {
$isNew = empty($this->item->id); $isNew = empty($this->item->id);
$canDo = MokoSuiteCrossHelper::getActions();
ToolbarHelper::title( ToolbarHelper::title(
'MokoSuiteCross — ' . ($isNew ? 'New Template' : 'Edit Template'), 'MokoSuiteCross — ' . ($isNew ? 'New Template' : 'Edit Template'),
'share-alt' 'share-alt'
); );
if ($canDo->get('mokosuitecross.templates.manage')) {
ToolbarHelper::apply('template.apply'); ToolbarHelper::apply('template.apply');
ToolbarHelper::save('template.save'); ToolbarHelper::save('template.save');
}
ToolbarHelper::cancel('template.cancel'); ToolbarHelper::cancel('template.cancel');
// Dashboard link in toolbar // Dashboard link in toolbar
@@ -41,6 +41,8 @@ class HtmlView extends BaseHtmlView
protected function addToolbar(): void protected function addToolbar(): void
{ {
$canDo = MokoSuiteCrossHelper::getActions();
ToolbarHelper::title('MokoSuiteCross — Message Templates', 'share-alt'); ToolbarHelper::title('MokoSuiteCross — Message Templates', 'share-alt');
ToolbarHelper::addNew('template.add'); ToolbarHelper::addNew('template.add');
ToolbarHelper::editList('template.edit'); ToolbarHelper::editList('template.edit');
@@ -11,3 +11,23 @@ PLG_CONTENT_MOKOSUITECROSS_EVERGREEN_DESC="Automatically re-share this article o
PLG_CONTENT_MOKOSUITECROSS_EVERGREEN_INTERVAL="Re-share Interval (days)" PLG_CONTENT_MOKOSUITECROSS_EVERGREEN_INTERVAL="Re-share Interval (days)"
PLG_CONTENT_MOKOSUITECROSS_EVERGREEN_INTERVAL_DESC="How many days to wait between automatic re-shares. Default: 30 days." PLG_CONTENT_MOKOSUITECROSS_EVERGREEN_INTERVAL_DESC="How many days to wait between automatic re-shares. Default: 30 days."
PLG_CONTENT_MOKOSUITECROSS_HISTORY="Cross-Post History" PLG_CONTENT_MOKOSUITECROSS_HISTORY="Cross-Post History"
PLG_CONTENT_MOKOSUITECROSS_FIELDSET_SHARE="Share Content"
PLG_CONTENT_MOKOSUITECROSS_SOCIAL_TEXT="Social Media Text"
PLG_CONTENT_MOKOSUITECROSS_SOCIAL_TEXT_DESC="Custom text for Facebook, LinkedIn, Threads. Use {social} placeholder in templates. Falls back to intro text if empty."
PLG_CONTENT_MOKOSUITECROSS_SHORT_TEXT="Short Text (Twitter/Bluesky)"
PLG_CONTENT_MOKOSUITECROSS_SHORT_TEXT_DESC="Optimized text for character-limited platforms (Twitter 280, Bluesky 300). Use {short} placeholder. Falls back to truncated title."
PLG_CONTENT_MOKOSUITECROSS_CHAT_TEXT="Chat Text"
PLG_CONTENT_MOKOSUITECROSS_CHAT_TEXT_DESC="Custom text for Telegram, Discord, Slack, Teams. Use {chat} placeholder. Falls back to intro text."
PLG_CONTENT_MOKOSUITECROSS_EMAIL_SUBJECT="Email Subject"
PLG_CONTENT_MOKOSUITECROSS_EMAIL_SUBJECT_DESC="Subject line for Mailchimp, SendGrid, Brevo campaigns. Use {email_subject} placeholder. Falls back to article title."
PLG_CONTENT_MOKOSUITECROSS_EMAIL_BODY="Email Body"
PLG_CONTENT_MOKOSUITECROSS_EMAIL_BODY_DESC="HTML content for email campaigns. Use {email_body} placeholder. Falls back to full article text."
PLG_CONTENT_MOKOSUITECROSS_SHARE_IMAGE="Share Image"
PLG_CONTENT_MOKOSUITECROSS_SHARE_IMAGE_DESC="Which image to use when cross-posting this article."
PLG_CONTENT_MOKOSUITECROSS_SHARE_IMAGE_INTRO="Intro Image"
PLG_CONTENT_MOKOSUITECROSS_SHARE_IMAGE_FULLTEXT="Full Text Image"
PLG_CONTENT_MOKOSUITECROSS_SHARE_IMAGE_CUSTOM="Custom Image"
PLG_CONTENT_MOKOSUITECROSS_SHARE_IMAGE_NONE="No Image"
PLG_CONTENT_MOKOSUITECROSS_CUSTOM_IMAGE="Custom Share Image"
PLG_CONTENT_MOKOSUITECROSS_CUSTOM_IMAGE_DESC="Select an image from the media manager to use for cross-posting."
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="content" method="upgrade"> <extension type="plugin" group="content" method="upgrade">
<name>Content - MokoSuiteCross</name> <name>Content - MokoSuiteCross</name>
<version>01.02.00-rc</version> <version>01.08.37</version>
<creationDate>2026-05-28</creationDate> <creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author> <author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -140,6 +140,71 @@ class MokoSuiteCrossContent extends CMSPlugin implements SubscriberInterface
showon="mokosuitecross_skip:0[AND]mokosuitecross_evergreen:1" showon="mokosuitecross_skip:0[AND]mokosuitecross_evergreen:1"
/> />
</fieldset> </fieldset>
<fieldset name="mokosuitecross_share" label="PLG_CONTENT_MOKOSUITECROSS_FIELDSET_SHARE">
<field
name="mokosuitecross_social_text"
type="textarea"
label="PLG_CONTENT_MOKOSUITECROSS_SOCIAL_TEXT"
description="PLG_CONTENT_MOKOSUITECROSS_SOCIAL_TEXT_DESC"
rows="3"
hint="Optimized for Facebook, LinkedIn, Threads. Leave empty to use intro text."
filter="string"
/>
<field
name="mokosuitecross_short_text"
type="textarea"
label="PLG_CONTENT_MOKOSUITECROSS_SHORT_TEXT"
description="PLG_CONTENT_MOKOSUITECROSS_SHORT_TEXT_DESC"
rows="2"
hint="For Twitter (280), Bluesky (300). Leave empty for auto-truncated title."
filter="string"
/>
<field
name="mokosuitecross_chat_text"
type="textarea"
label="PLG_CONTENT_MOKOSUITECROSS_CHAT_TEXT"
description="PLG_CONTENT_MOKOSUITECROSS_CHAT_TEXT_DESC"
rows="3"
hint="For Telegram, Discord, Slack, Teams. Leave empty to use intro text."
filter="string"
/>
<field
name="mokosuitecross_email_subject"
type="text"
label="PLG_CONTENT_MOKOSUITECROSS_EMAIL_SUBJECT"
description="PLG_CONTENT_MOKOSUITECROSS_EMAIL_SUBJECT_DESC"
hint="For Mailchimp, SendGrid, Brevo. Leave empty to use article title."
filter="string"
/>
<field
name="mokosuitecross_email_body"
type="editor"
label="PLG_CONTENT_MOKOSUITECROSS_EMAIL_BODY"
description="PLG_CONTENT_MOKOSUITECROSS_EMAIL_BODY_DESC"
filter="safehtml"
buttons="true"
hide="readmore,pagebreak"
height="200"
/>
<field
name="mokosuitecross_share_image"
type="list"
label="PLG_CONTENT_MOKOSUITECROSS_SHARE_IMAGE"
description="PLG_CONTENT_MOKOSUITECROSS_SHARE_IMAGE_DESC"
default="intro">
<option value="intro">PLG_CONTENT_MOKOSUITECROSS_SHARE_IMAGE_INTRO</option>
<option value="fulltext">PLG_CONTENT_MOKOSUITECROSS_SHARE_IMAGE_FULLTEXT</option>
<option value="custom">PLG_CONTENT_MOKOSUITECROSS_SHARE_IMAGE_CUSTOM</option>
<option value="none">PLG_CONTENT_MOKOSUITECROSS_SHARE_IMAGE_NONE</option>
</field>
<field
name="mokosuitecross_custom_image"
type="media"
label="PLG_CONTENT_MOKOSUITECROSS_CUSTOM_IMAGE"
description="PLG_CONTENT_MOKOSUITECROSS_CUSTOM_IMAGE_DESC"
showon="mokosuitecross_share_image:custom"
/>
</fieldset>
</fields> </fields>
</form> </form>
XML; XML;
@@ -205,25 +270,11 @@ XML;
/** /**
* Add cross-post status badges before article content in admin. * Add cross-post status badges before article content in admin.
*
* Joomla 5/6 compatible — accepts both BeforeDisplayEvent and legacy parameters.
*/ */
public function onContentBeforeDisplay($event): string public function onContentBeforeDisplay(\Joomla\CMS\Event\Content\BeforeDisplayEvent $event): string
{ {
// Joomla 5/6 compatibility
if ($event instanceof \Joomla\CMS\Event\Content\BeforeDisplayEvent) {
$context = $event->getContext(); $context = $event->getContext();
$article = $event->getItem(); $article = $event->getItem();
} elseif (is_string($event)) {
$context = $event;
$article = func_get_arg(1);
} else {
return '';
}
if ($context !== 'com_content.article') {
return '';
}
$app = $this->getApplication(); $app = $this->getApplication();
@@ -265,26 +316,18 @@ XML;
/** /**
* Dispatch cross-post when an article is saved and published. * Dispatch cross-post when an article is saved and published.
*
* Joomla 5/6 compatible — accepts both AfterSaveEvent and legacy parameters.
*/ */
public function onContentAfterSave($event): void public function onContentAfterSave(\Joomla\CMS\Event\Content\AfterSaveEvent $event): void
{ {
// Joomla 5/6 compatibility
if ($event instanceof \Joomla\CMS\Event\Content\AfterSaveEvent) {
$context = $event->getContext(); $context = $event->getContext();
$article = $event->getItem();
$isNew = $event->getIsNew();
} else {
$context = $event;
$article = func_get_arg(1);
$isNew = func_get_arg(2);
}
if ($context !== 'com_content.article') { if ($context !== 'com_content.article') {
return; return;
} }
$article = $event->getItem();
$isNew = $event->getIsNew();
if ((int) ($article->state ?? 0) !== 1) { if ((int) ($article->state ?? 0) !== 1) {
return; return;
} }
@@ -310,27 +353,36 @@ XML;
/** /**
* Dispatch cross-post when article state changes to published. * Dispatch cross-post when article state changes to published.
*
* Joomla 5/6 compatible — accepts both ContentChangeStateEvent and legacy parameters.
*/ */
public function onContentChangeState($event): void public function onContentChangeState(\Joomla\CMS\Event\Content\ContentChangeStateEvent $event): void
{ {
if ($event instanceof \Joomla\CMS\Event\Content\ContentChangeStateEvent) {
$context = $event->getContext(); $context = $event->getContext();
$pks = $event->getPks();
$value = $event->getValue();
} else {
$context = $event;
$pks = func_get_arg(1);
$value = func_get_arg(2);
}
if ($context !== 'com_content.article' || $value !== 1) { if ($context !== 'com_content.article') {
return; return;
} }
$pks = $event->getPks();
$value = $event->getValue();
$params = ComponentHelper::getParams('com_mokosuitecross'); $params = ComponentHelper::getParams('com_mokosuitecross');
// Unpublish/trash: delete from platforms if configured
if ($value === 0 || $value === -2) {
if ($params->get('delete_on_unpublish', 0)) {
foreach ($pks as $pk) {
CrossPostDispatcher::deleteFromPlatforms((int) $pk);
}
}
return;
}
// Publish: auto-post if configured
if ($value !== 1) {
return;
}
if (!$params->get('auto_post_on_publish', 1)) { if (!$params->get('auto_post_on_publish', 1)) {
return; return;
} }
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="mokosuitecross" method="upgrade"> <extension type="plugin" group="mokosuitecross" method="upgrade">
<name>MokoSuiteCross - ActivityPub (Fediverse)</name> <name>MokoSuiteCross - ActivityPub (Fediverse)</name>
<version>01.02.00-rc</version> <version>01.08.37</version>
<creationDate>2026-05-28</creationDate> <creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author> <author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="mokosuitecross" method="upgrade"> <extension type="plugin" group="mokosuitecross" method="upgrade">
<name>MokoSuiteCross - Google Blogger</name> <name>MokoSuiteCross - Google Blogger</name>
<version>01.02.00-rc</version> <version>01.08.37</version>
<creationDate>2026-05-28</creationDate> <creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author> <author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="mokosuitecross" method="upgrade"> <extension type="plugin" group="mokosuitecross" method="upgrade">
<name>MokoSuiteCross - Bluesky</name> <name>MokoSuiteCross - Bluesky</name>
<version>01.02.00-rc</version> <version>01.08.37</version>
<creationDate>2026-05-28</creationDate> <creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author> <author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -10,7 +10,7 @@
<license>GPL-3.0-or-later</license> <license>GPL-3.0-or-later</license>
<description>PLG_MOKOSUITECROSS_BLUESKY_DESCRIPTION</description> <description>PLG_MOKOSUITECROSS_BLUESKY_DESCRIPTION</description>
<namespace path="src">Joomla\Plugin\MokoSuiteCross${CLASS_NAME}</namespace> <namespace path="src">Joomla\Plugin\MokoSuiteCross\Bluesky</namespace>
<files> <files>
<filename plugin="bluesky">bluesky.php</filename> <filename plugin="bluesky">bluesky.php</filename>
@@ -14,6 +14,7 @@ namespace Joomla\Plugin\MokoSuiteCross\Bluesky\Extension;
defined('_JEXEC') or die; defined('_JEXEC') or die;
use Joomla\CMS\Plugin\CMSPlugin; use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossDeleteInterface;
use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface;
use Joomla\Event\SubscriberInterface; use Joomla\Event\SubscriberInterface;
@@ -29,7 +30,7 @@ use Joomla\Event\SubscriberInterface;
* "pds_url": "https://bsky.social" // Optional, defaults to bsky.social * "pds_url": "https://bsky.social" // Optional, defaults to bsky.social
* } * }
*/ */
class BlueskyService extends CMSPlugin implements SubscriberInterface, MokoSuiteCrossServiceInterface class BlueskyService extends CMSPlugin implements SubscriberInterface, MokoSuiteCrossServiceInterface, MokoSuiteCrossDeleteInterface
{ {
public static function getSubscribedEvents(): array public static function getSubscribedEvents(): array
{ {
@@ -65,15 +66,59 @@ class BlueskyService extends CMSPlugin implements SubscriberInterface, MokoSuite
return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Authentication failed']]; return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Authentication failed']];
} }
// Create post // Build external link card embed if URL is in params
$embed = null;
$articleUrl = $params['article_url'] ?? '';
if (!empty($articleUrl)) {
$embed = [
'$type' => 'app.bsky.embed.external',
'external' => [
'uri' => $articleUrl,
'title' => $params['article_title'] ?? '',
'description' => mb_substr(strip_tags($message), 0, 200),
],
];
}
// Auto-thread: split long messages at sentence boundaries
$chunks = $this->splitIntoThread($message, 300);
if (count($chunks) === 1) {
// Single post
return $this->createPost($pds, $authData, $chunks[0], $embed);
}
// Thread: post each chunk as a reply to the previous
$rootUri = null;
$rootCid = null;
$parentUri = null;
$parentCid = null;
$lastResult = [];
foreach ($chunks as $i => $chunk) {
$record = [
'$type' => 'app.bsky.feed.post',
'text' => $chunk,
'createdAt' => gmdate('Y-m-d\TH:i:s\Z'),
];
// Add reply reference for thread posts after the first
if ($rootUri !== null) {
$record['reply'] = [
'root' => ['uri' => $rootUri, 'cid' => $rootCid],
'parent' => ['uri' => $parentUri, 'cid' => $parentCid],
];
}
// Attach link card embed to last post only
if ($embed !== null && $i === count($chunks) - 1) {
$record['embed'] = $embed;
}
$postData = json_encode([ $postData = json_encode([
'repo' => $authData['did'], 'repo' => $authData['did'],
'collection' => 'app.bsky.feed.post', 'collection' => 'app.bsky.feed.post',
'record' => [ 'record' => $record,
'$type' => 'app.bsky.feed.post',
'text' => mb_substr($message, 0, 300),
'createdAt' => gmdate('Y-m-d\TH:i:s\Z'),
],
]); ]);
$ch = curl_init($pds . '/xrpc/com.atproto.repo.createRecord'); $ch = curl_init($pds . '/xrpc/com.atproto.repo.createRecord');
@@ -83,31 +128,33 @@ class BlueskyService extends CMSPlugin implements SubscriberInterface, MokoSuite
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $authData['accessJwt'], 'Content-Type: application/json'], CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $authData['accessJwt'], 'Content-Type: application/json'],
CURLOPT_RETURNTRANSFER => true, CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30, CURLOPT_TIMEOUT => 30,
CURLOPT_PROTOCOLS => CURLPROTO_HTTPS | CURLPROTO_HTTP,
CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTPS | CURLPROTO_HTTP,
]); ]);
$response = curl_exec($ch); $response = curl_exec($ch);
if ($response === false) { if ($response === false) {
$curlError = curl_error($ch); $curlError = curl_error($ch);
curl_close($ch); curl_close($ch);
return ['success' => false, 'platform_post_id' => $rootUri ?? '', 'response' => ['error' => 'Thread error at post ' . ($i + 1) . ': ' . $curlError]];
return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Connection error: ' . $curlError]];
} }
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch); curl_close($ch);
$data = json_decode($response, true) ?: []; $data = json_decode($response, true) ?: [];
if ($httpCode === 200 && !empty($data['uri'])) { if ($httpCode !== 200 || empty($data['uri'])) {
return ['success' => true, 'platform_post_id' => $data['uri'], 'response' => $data]; return ['success' => false, 'platform_post_id' => $rootUri ?? '', 'response' => $data];
} }
return ['success' => false, 'platform_post_id' => '', 'response' => $data]; if ($rootUri === null) {
$rootUri = $data['uri'];
$rootCid = $data['cid'];
}
$parentUri = $data['uri'];
$parentCid = $data['cid'];
$lastResult = $data;
}
return ['success' => true, 'platform_post_id' => $rootUri, 'response' => array_merge($lastResult, ['thread_count' => count($chunks)])];
} }
public function validateCredentials(array $credentials): array public function validateCredentials(array $credentials): array
@@ -127,7 +174,7 @@ class BlueskyService extends CMSPlugin implements SubscriberInterface, MokoSuite
private function authenticateWithCache(string $pds, string $handle, string $appPwd): array private function authenticateWithCache(string $pds, string $handle, string $appPwd): array
{ {
$cacheKey = md5($pds . $handle); $cacheKey = hash('sha256', $pds . $handle);
if (isset(self::$sessionCache[$cacheKey])) { if (isset(self::$sessionCache[$cacheKey])) {
$cached = self::$sessionCache[$cacheKey]; $cached = self::$sessionCache[$cacheKey];
@@ -175,6 +222,157 @@ class BlueskyService extends CMSPlugin implements SubscriberInterface, MokoSuite
return json_decode($response, true) ?: []; return json_decode($response, true) ?: [];
} }
/**
* Create a single Bluesky post (used for non-threaded messages).
*/
private function createPost(string $pds, array $authData, string $text, ?array $embed = null): array
{
$record = [
'$type' => 'app.bsky.feed.post',
'text' => $text,
'createdAt' => gmdate('Y-m-d\TH:i:s\Z'),
];
if ($embed !== null) {
$record['embed'] = $embed;
}
$postData = json_encode([
'repo' => $authData['did'],
'collection' => 'app.bsky.feed.post',
'record' => $record,
]);
$ch = curl_init($pds . '/xrpc/com.atproto.repo.createRecord');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $postData,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $authData['accessJwt'], 'Content-Type: application/json'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
]);
$response = curl_exec($ch);
if ($response === false) {
$curlError = curl_error($ch);
curl_close($ch);
return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Connection error: ' . $curlError]];
}
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$data = json_decode($response, true) ?: [];
if ($httpCode === 200 && !empty($data['uri'])) {
return ['success' => true, 'platform_post_id' => $data['uri'], 'response' => $data];
}
return ['success' => false, 'platform_post_id' => '', 'response' => $data];
}
/**
* Split a long message into thread-sized chunks at sentence boundaries.
*/
private function splitIntoThread(string $message, int $maxLength): array
{
if (mb_strlen($message) <= $maxLength) {
return [$message];
}
$chunks = [];
$remaining = $message;
while (mb_strlen($remaining) > $maxLength) {
$segment = mb_substr($remaining, 0, $maxLength);
// Try to break at last sentence boundary (. ! ? followed by space)
$breakPos = max(
mb_strrpos($segment, '. ') ?: 0,
mb_strrpos($segment, '! ') ?: 0,
mb_strrpos($segment, '? ') ?: 0
);
if ($breakPos < $maxLength * 0.3) {
// No good sentence break; try last space
$breakPos = mb_strrpos($segment, ' ') ?: $maxLength;
} else {
$breakPos += 1; // Include the punctuation
}
$chunks[] = trim(mb_substr($remaining, 0, $breakPos));
$remaining = trim(mb_substr($remaining, $breakPos));
}
if (!empty($remaining)) {
$chunks[] = $remaining;
}
return $chunks;
}
public function deletePost(string $platformPostId, array $credentials): array
{
$pds = rtrim($credentials['pds_url'] ?? 'https://bsky.social', '/');
$handle = $credentials['handle'] ?? '';
$appPwd = $credentials['app_password'] ?? '';
if (empty($handle) || empty($appPwd)) {
return ['success' => false, 'message' => 'Missing credentials.'];
}
// Parse AT URI: at://did:plc:xxx/app.bsky.feed.post/rkey
$parts = explode('/', $platformPostId);
$rkey = end($parts);
if (empty($rkey)) {
return ['success' => false, 'message' => 'Invalid AT URI -- could not extract rkey.'];
}
// Authenticate (uses cached session if still valid)
$authData = $this->authenticateWithCache($pds, $handle, $appPwd);
if (empty($authData['accessJwt'])) {
return ['success' => false, 'message' => 'Authentication failed.'];
}
$postData = json_encode([
'repo' => $authData['did'],
'collection' => 'app.bsky.feed.post',
'rkey' => $rkey,
]);
$ch = curl_init($pds . '/xrpc/com.atproto.repo.deleteRecord');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $postData,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $authData['accessJwt'], 'Content-Type: application/json'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_PROTOCOLS => CURLPROTO_HTTPS | CURLPROTO_HTTP,
CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTPS | CURLPROTO_HTTP,
]);
$response = curl_exec($ch);
if ($response === false) {
$curlError = curl_error($ch);
curl_close($ch);
return ['success' => false, 'message' => 'Connection error: ' . $curlError];
}
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode === 200) {
return ['success' => true, 'message' => 'Post deleted successfully.'];
}
$data = json_decode($response, true) ?: [];
return ['success' => false, 'message' => $data['message'] ?? 'Delete failed with HTTP ' . $httpCode];
}
public function getSupportedMediaTypes(): array public function getSupportedMediaTypes(): array
{ {
return ['image']; return ['image'];
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="mokosuitecross" method="upgrade"> <extension type="plugin" group="mokosuitecross" method="upgrade">
<name>MokoSuiteCross - Brevo (Sendinblue)</name> <name>MokoSuiteCross - Brevo (Sendinblue)</name>
<version>01.02.00-rc</version> <version>01.08.37</version>
<creationDate>2026-05-28</creationDate> <creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author> <author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>

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