Compare commits

..

126 Commits

Author SHA1 Message Date
Jonathan Miller 1894abcf90 security: add CSRF and ACL checks (#104, #105)
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (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
- #104: Add checkToken('get') and core.manage ACL check to CSV export
- #105: Add checkToken() to migration action (ACL was already present)

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 06:40:19 -05:00
gitea-actions[bot] b6a15ae409 chore(version): pre-release bump to 01.00.27-dev [skip ci] 2026-06-06 11:31:14 +00:00
Jonathan Miller 62e6c80d28 feat: resolve 6 enhancement issues (#96, #97, #98, #99, #102, #103)
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (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
- #96: Move Facebook token from URL query string to Authorization
  header in both publish() and validateCredentials()
- #97: Cache Bluesky AT Protocol session tokens in static property
  with 90-minute TTL to avoid re-authenticating on every post
- #98: Add auto_send parameter to Mailchimp — calls campaign send
  endpoint after creation when enabled (default: off for safety)
- #99: Batch duplicate guard and template loading in
  CrossPostDispatcher — reduces N*M queries to 2 bulk queries
- #102: Remove duplicated renderTemplate() from DispatchController,
  delegate to CrossPostDispatcher::renderTemplate() (now public)
- #103: Replace deprecated Sidebar API with Joomla 5 toolbar submenu
  API, with legacy fallback for Joomla 4 compatibility

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 06:27:24 -05:00
Jonathan Miller 486a8ac38a fix: resolve 5 open bugs (#92, #94, #95, #100, #101)
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (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
- #92: Replace MySQL-only GET_LOCK with db-agnostic locking — adds
  PostgreSQL pg_advisory_lock and timestamp-based fallback
- #94: retryFailed() now includes permanently_failed and cancelled
  statuses, not just failed
- #95: Validate scheduled_at datetime via Factory::getDate() in both
  PostModel::prepareTable() and PostsController::schedule()
- #100: Add clarifying comment to update SQL for duplicate table def
- #101: Replace fragile LIKE query with JSON_EXTRACT() for evergreen
  article detection

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 06:04:38 -05:00
gitea-actions[bot] 0ea55601d2 chore(version): pre-release bump to 01.00.26-dev [skip ci] 2026-06-05 04:08:57 +00:00
Jonathan Miller f46a657534 fix(manifest): fix dlid and blockChildUninstall placement and indentation
Generic: Repo Health / Access control (push) Has been cancelled
Generic: Repo Health / Site Health (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
Move dlid and blockChildUninstall elements before updateservers and
fix tab indentation to match the 4-space convention used in the rest
of the manifest.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 23:07:34 -05:00
Jonathan Miller b37a7dd1be feat(license): warn admins when no download key is configured
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (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
Adds onAfterRoute hook that checks the update site for a valid
MOKO-XXXX license key and shows a warning message once per session
if missing — directs users to System > Update Sites.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 23:04:18 -05:00
Jonathan Miller 0b3bd2bda6 chore(changelog): consolidate duplicate Added/Fixed sections
Merge duplicate ### Added and ### Fixed headings into single sections
and fix version format from 01.00.00 to 01.00.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 23:04:18 -05:00
gitea-actions[bot] 9d48a23e0b chore(version): pre-release bump to 01.00.25-dev [skip ci] 2026-06-05 03:11:46 +00:00
Jonathan Miller ca940c3b70 chore(ci): add pre-release.yml from moko-platform v05.01.00
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (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
Restores the pre-release workflow without update-stream steps —
updates.xml is now generated dynamically by the license server.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 22:09:54 -05:00
jmiller dfd7dd1c9e chore: add dlid and blockChildUninstall to package manifest [skip ci] 2026-06-04 22:02:36 +00:00
jmiller 78a4fa1778 chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] 2026-06-04 15:56:52 +00:00
jmiller 47f6061b6f chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] 2026-06-04 15:39:20 +00:00
jmiller bf13bd5075 chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] 2026-06-04 15:30:27 +00:00
jmiller b2459dc6cd chore: remove updates.xml [skip ci] 2026-06-04 15:27:09 +00:00
jmiller 09211b6e3d chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] 2026-06-04 15:14:27 +00:00
jmiller 9f8ebaeb5c feat(update): migrate update server URL to Gitea Pages [skip ci] 2026-06-04 14:34:02 +00:00
jmiller 7cd81a8ae5 chore: sync .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] 2026-06-04 14:21:16 +00:00
jmiller aebd01a5c4 chore: sync .mokogitea/workflows/repo-health.yml from moko-platform [skip ci] 2026-06-03 09:37:36 +00:00
jmiller 2b433fd569 chore: sync .mokogitea/workflows/repo-health.yml from moko-platform [skip ci] 2026-06-03 03:11:23 +00:00
jmiller b97c5bb8d4 chore: add .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] 2026-06-02 23:48:01 +00:00
jmiller cff210ec96 chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] 2026-06-02 21:52:14 +00:00
Moko Consulting c1a063be27 chore(ci): add CI issue reporter for auto-filing gate failures
Generic: Repo Health / Access control (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-02 20:38:42 +00:00
Moko Consulting bd03dbab09 chore(ci): add CI issue reporter for auto-filing gate failures
Generic: Repo Health / Access control (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-02 20:38:41 +00:00
Moko Consulting 0ac43185e5 chore(ci): add CI issue reporter for auto-filing gate failures
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-02 20:38:40 +00:00
gitea-actions[bot] 66704c9cee chore(ci): remove update-server.yml for update server migration [skip ci] 2026-05-31 03:46:56 +00:00
gitea-actions[bot] dc2497a513 chore(ci): remove cascade-dev.yml for update server migration [skip ci] 2026-05-31 03:46:55 +00:00
gitea-actions[bot] 38fba65a3d chore(ci): remove auto-bump.yml for update server migration [skip ci] 2026-05-31 03:46:53 +00:00
gitea-actions[bot] c00a658c0b chore(ci): remove pre-release.yml for update server migration [skip ci] 2026-05-31 03:46:51 +00:00
gitea-actions[bot] f47a928cd5 chore(ci): remove auto-release.yml for update server migration [skip ci] 2026-05-31 03:46:49 +00:00
jmiller 1ee8269b8e chore: sync .mokogitea/workflows/cascade-dev.yml from moko-platform [skip ci] 2026-05-31 01:46:12 +00:00
Jonathan Miller 2c480e8d31 chore(manifest): fix display-name structure and update CONTRIBUTING.md
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Standardize manifest.xml identity block: ensure <name> contains only
the machine identifier (PascalCase) and <display-name> contains the
human-readable label with Joomla extension type prefix. Remove duplicate
<version> tags where present. Update CONTRIBUTING.md from moko-platform
default.

Authored-by: Moko Consulting
2026-05-30 19:11:09 -05:00
jmiller 5d2c32422a chore: add .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] 2026-05-30 16:03:58 +00:00
jmiller 49c0484061 chore: sync .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] 2026-05-30 15:05:05 +00:00
jmiller 4708bb66fd chore: sync .mokogitea/workflows/auto-bump.yml from moko-platform [skip ci] 2026-05-30 15:02:30 +00:00
jmiller 17be4ff0f1 chore: sync .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] 2026-05-30 01:17:13 +00:00
jmiller 0ad4963115 chore: sync .mokogitea/workflows/auto-bump.yml from moko-platform [skip ci] 2026-05-29 10:32:38 +00:00
gitea-actions[bot] f88c760c0f chore: update development channel 01.00.06-dev-dev [skip ci] 2026-05-29 05:29:02 +00:00
gitea-actions[bot] 40e540461e chore(version): auto-bump 01.00.06-dev-dev [skip ci] 2026-05-29 05:29:01 +00:00
Jonathan Miller 8dd6fdd926 fix: critical and high severity audit fixes
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Update Server / Update Server (push) Has been cancelled
C-1: CSRF nonce on OAuth authorize/callback flow
C-2: POST method enforcement on REST dispatch endpoint
C-5: Service credential fields now saved from form to JSON column
     (collect cred_* fields, strip prefix, JSON encode on save;
      expand back on load for editing)
H-1: Joomla 5 event ArrayAccess pattern for service plugin collection
     (reads from Event indices instead of broken by-reference)
H-4: ServiceTable::check() with alias generation, required validation
H-9: WebhookService credential keys match form XML field names,
     Bearer/Basic auth headers implemented correctly
M-4: XSS fix — escape $extraClass in ServiceIconHelper::renderIcon()
M-5: Article history HTML injection via setFieldAttribute() instead
     of double-escaped XML description attribute

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-29 00:28:36 -05:00
gitea-actions[bot] 353c037907 chore: update development channel 01.00.06-dev-dev [skip ci] 2026-05-29 05:13:42 +00:00
gitea-actions[bot] fcb332ea00 chore(version): auto-bump 01.00.06-dev-dev [skip ci] 2026-05-29 05:13:40 +00:00
Jonathan Miller 20ee39f54b feat: 5 final features — category routing, char counter, service icons, drill-down analytics, article history
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Update Server / Update Server (push) Has been cancelled
1. Category routing rules — new DB table #__mokojoomcross_category_rules
   maps Joomla categories to specific services (whitelist). Integrated
   into CrossPostDispatcher before per-article filters.
2. Character counter — live JS counter in template editor shows
   remaining chars per platform with color coding (green/yellow/red)
3. Service type icons — ServiceIconHelper maps 34 types to Bootstrap
   icons, used in services list, posts list, and dashboard
4. Per-service analytics drill-down — ServiceStats view with stats
   cards, daily trend chart, recent posts, top articles. Dashboard
   service rows are now clickable links.
5. Article editor cross-post history — read-only panel in the
   Cross-Posting fieldset showing last 10 post results with status
   badges, service names, and timestamps

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-29 00:13:30 -05:00
gitea-actions[bot] 57f34f0fc7 chore: update development channel 01.00.06-dev-dev [skip ci] 2026-05-29 03:56:02 +00:00
gitea-actions[bot] 58a9641b94 chore(version): auto-bump 01.00.06-dev-dev [skip ci] 2026-05-29 03:56:01 +00:00
Jonathan Miller 16c068b4b0 feat: getSupportedMediaTypes() on all 36 service plugins
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Update Server / Update Server (push) Has been cancelled
Add media capability reporting to MokoJoomCrossServiceInterface.
Each plugin now returns its supported media types:
- image, video, gif, document (per platform capability)
- Empty array for text-only services (Nostr, Ntfy, ConvertKit)

Enables the dispatcher to skip media attachments for text-only
services and choose appropriate media types per platform.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-28 22:55:51 -05:00
gitea-actions[bot] de869c2b5d chore: update development channel 01.00.06-dev-dev [skip ci] 2026-05-29 03:52:43 +00:00
gitea-actions[bot] b8cd0253e1 chore(version): auto-bump 01.00.06-dev-dev [skip ci] 2026-05-29 03:52:42 +00:00
Jonathan Miller c3899b65d3 feat: bulk re-queue, purge posted, CSV export, package manifest update
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Update Server / Update Server (push) Has been cancelled
- PostsController: retryFailed(), purgePosted(), exportCsv() tasks
- Posts HtmlView: Retry Failed, Purge Posted, Export CSV toolbar buttons
- pkg_mokojoomcross.xml: added new sub-extension entries

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-28 22:52:30 -05:00
gitea-actions[bot] 7777ffca32 chore: update development channel 01.00.06-dev-dev [skip ci] 2026-05-29 03:48:32 +00:00
gitea-actions[bot] 90340dd499 chore(version): auto-bump 01.00.06-dev-dev [skip ci] 2026-05-29 03:48:31 +00:00
Jonathan Miller 7747fef50e refactor: split content-type dispatch into pluggable source plugins
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Update Server / Update Server (push) Has been cancelled
Extract dispatch logic from monolithic system plugin into a shared
CrossPostDispatcher helper. Each content type now has its own plugin:

- plg_content_mokojoomcross — articles (onContentAfterSave/ChangeState)
- plg_system_mokojoomcross_events — MokoJoomCalendar events
- plg_system_mokojoomcross_gallery — MokoJoomGallery galleries/images
- plg_mokojoomcross_mokojoomcalendar — calendar service enrichment
- plg_mokojoomcross_mokojoomgallery — gallery service enrichment

System plugin stripped to page-load queue processing only.

Also fixes: onContentBeforeDisplay Joomla 5/6 BeforeDisplayEvent
compatibility (was crashing with wrong argument type).

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-28 22:48:18 -05:00
gitea-actions[bot] 3586ce7661 chore: update development channel 01.00.06-dev-dev [skip ci] 2026-05-29 03:33:54 +00:00
gitea-actions[bot] 83dc2fa013 chore(version): auto-bump 01.00.06-dev-dev [skip ci] 2026-05-29 03:33:52 +00:00
Jonathan Miller 9544f4f0bb feat: 7 medium-effort enhancements
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Update Server / Update Server (push) Has been cancelled
1. Image attachment pipeline — article intro image passed to publish()
   via $media array (system plugin + QueueProcessor)
2. Custom fields as placeholders — {field:xxx} resolves Joomla custom
   fields in templates
3. Lifecycle events — onMokoJoomCrossBeforePost (cancellable),
   AfterPost, PostFailed for third-party hooks
4. Token auto-refresh — OAuthHelper::refreshTokenIfNeeded() checks
   token_expires and refreshes via refresh_token before each publish
5. DB lock race fix — MySQL GET_LOCK() replaces read-then-write pattern
6. WordPress canonical URL — appends source link to cross-posted content
7. REST API dispatch — POST /api/v1/mokojoomcross/dispatch triggers
   cross-posting programmatically with article_id + optional service_ids

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-28 22:33:42 -05:00
gitea-actions[bot] 288dbb2240 chore: update development channel 01.00.06-dev-dev [skip ci] 2026-05-29 00:14:13 +00:00
gitea-actions[bot] 37b32a56b3 chore(version): auto-bump 01.00.06-dev-dev [skip ci] 2026-05-29 00:14:12 +00:00
Jonathan Miller 3b501719ff feat: 10 quick-win enhancements
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Update Server / Update Server (push) Has been cancelled
1. Test Connection button — AJAX validation on service edit sidebar
2. Bulk re-queue failed + purge posted — toolbar buttons on Posts list
3. Exponential backoff — retry_delay * 2^retry_count replaces fixed delay
4. Queue depth warning — dashboard alert when queued > 50
5. First-publish-only toggle — skip cross-posting on article re-saves
6. Dashboard trend chart — Chart.js line chart for daily posted/failed
7. Hashtag injection — {tags} and {hashtags} template placeholders
8. Posts list filters — service dropdown + search by article/message
9. CSV export — download filtered post history as spreadsheet
10. Dashboard date range — 7d/30d/90d/all filter on analytics

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-28 19:14:03 -05:00
gitea-actions[bot] 5325293db4 chore: update development channel 01.00.06-dev-dev [skip ci] 2026-05-28 23:56:27 +00:00
gitea-actions[bot] 06b27095ab chore(version): auto-bump 01.00.06-dev-dev [skip ci] 2026-05-28 23:56:26 +00:00
Jonathan Miller 362ce47e71 feat: post edit form, manual post creator, and scheduled posts
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Update Server / Update Server (push) Has been cancelled
Complete the Post CRUD that was previously stub-only:
- PostModel (AdminModel) for loading/saving individual posts
- Post HtmlView with toolbar (apply, save, cancel, dashboard)
- post.xml form with article selector, service selector, message
  textarea, status dropdown, and scheduled_at calendar picker
- Post edit template with results sidebar and re-queue button
- Posts list: New button in toolbar, clickable article titles,
  scheduled_at display with clock icon
- 20 new language strings for the post edit UI

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-28 18:56:13 -05:00
gitea-actions[bot] 911aac785b chore: update development channel 01.00.06-dev-dev [skip ci] 2026-05-28 23:47:05 +00:00
gitea-actions[bot] 865a877f99 chore(version): auto-bump 01.00.06-dev-dev [skip ci] 2026-05-28 23:47:04 +00:00
Jonathan Miller 1acb7f3778 feat: evergreen content re-sharing
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Update Server / Update Server (push) Has been cancelled
Articles can be marked as "evergreen" in the Cross-Posting fieldset,
with a configurable re-share interval (default 30 days). The queue
processor checks for due articles and re-queues them automatically,
bypassing the duplicate guard for articles whose last successful post
exceeds the interval.

- Per-article: evergreen toggle + interval (days) in article editor
- Global config: enable/disable, default interval, max per run
- QueueProcessor::processEvergreen() finds and re-queues due articles
- Task plugin calls processEvergreen() before processQueue()

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-28 18:46:56 -05:00
gitea-actions[bot] f8d1934d14 chore: update development channel 01.00.06-dev-dev [skip ci] 2026-05-28 22:40:18 +00:00
gitea-actions[bot] 435d4e8392 chore(version): auto-bump 01.00.06-dev-dev [skip ci] 2026-05-28 22:40:15 +00:00
Jonathan Miller dc53ef48d1 feat: per-service help links to KB articles on mokoconsulting.tech
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Update Server / Update Server (push) Has been cancelled
Service edit sidebar now shows a contextual "Setup Guide" button when
a service type is selected. Links to the matching KB article on the
live site (e.g., /kb/mokojoomcross/service-twitter-mokojoomcross).
All 34 service types mapped.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-28 17:40:03 -05:00
gitea-actions[bot] 1f76d7d2e9 chore: update development channel 01.00.06-dev-dev [skip ci] 2026-05-28 22:05:04 +00:00
gitea-actions[bot] 7262506d8e chore(version): auto-bump 01.00.06-dev-dev [skip ci] 2026-05-28 22:05:02 +00:00
Jonathan Miller 12074b71c3 fix: rewrite 13 broken service plugins with correct API implementations
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Update Server / Update Server (push) Has been cancelled
All 13 plugins had copy-paste stub code with literal placeholder URLs
(e.g. '{site_url}/api/endpoint') that were never substituted with
actual credential values. Each plugin now has correct:
- URL construction from credentials
- Auth method (Basic Auth for WP, JWT for Ghost, GraphQL for Hashnode)
- API payload format per platform spec
- Credential validation with live API checks

Fixed: ActivityPub, Blogger, Ghost, Google Business, Hashnode, Matrix,
Medium, Nostr (stub), RSS Feed, Threads, Tumblr, WhatsApp, WordPress.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-28 17:04:52 -05:00
gitea-actions[bot] 949f58506c chore: update development channel 01.00.13-dev [skip ci] 2026-05-28 21:51:14 +00:00
gitea-actions[bot] e23ddf9344 chore(version): auto-bump 01.00.13-dev [skip ci] 2026-05-28 21:51:12 +00:00
Jonathan Miller 5c86bdc24c fix: add missing Toolbar and Route imports in 5 admin views
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Update Server / Update Server (push) Has been cancelled
Dashboard link buttons in Logs, Posts, Services, Template, and
Templates views used Toolbar::getInstance() and Route::_ without
importing the classes — causing fatal errors on page load.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-28 16:50:58 -05:00
jmiller e25c6a9885 chore: sync .mokogitea/workflows/update-server.yml from moko-platform [skip ci] 2026-05-28 20:51:51 +00:00
jmiller 39d9d6fe1d chore: sync .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] 2026-05-28 20:47:01 +00:00
gitea-actions[bot] b57910f63b chore: update development channel 01.00.12-dev [skip ci] 2026-05-28 20:38:48 +00:00
gitea-actions[bot] 77cac8c8c3 chore(version): auto-bump 01.00.12-dev [skip ci] 2026-05-28 20:38:46 +00:00
Jonathan Miller 430d6a79f4 feat: complete service credential fields + fix Twitter OAuth 1.0a
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Update Server / Update Server (push) Has been cancelled
Fix Twitter posting by replacing Bearer token (app-only, read-only)
with OAuth 1.0a HMAC-SHA1 signing using all 4 keys. Add credential
fields for 19 previously missing services and optional fields for
7 existing services. Add Developer Guide wiki page.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-28 15:37:25 -05:00
jmiller bf835e9063 chore: sync .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] 2026-05-28 20:29:05 +00:00
jmiller 044bcdae76 chore: sync .mokogitea/workflows/pre-release.yml from moko-platform [skip ci] 2026-05-28 20:09:56 +00:00
gitea-actions[bot] 226fb84dd4 chore: update development channel 01.00.11-dev [skip ci] 2026-05-28 20:07:05 +00:00
jmiller b2e2630d44 chore: sync .mokogitea/workflows/update-server.yml from moko-platform [skip ci] 2026-05-28 20:06:52 +00:00
gitea-actions[bot] f0c79b3f32 chore: update development channel 01.00.11-dev [skip ci] 2026-05-28 20:06:51 +00:00
gitea-actions[bot] fe3ac2f54a chore(version): auto-bump 01.00.11-dev [skip ci] 2026-05-28 20:06:48 +00:00
Jonathan Miller dabce55cc7 feat: user-friendly service form + Dashboard toolbar button
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Update Server / Update Server (push) Has been cancelled
Replaced raw JSON credentials textarea with individual form fields
per service type using Joomla showon directives. Each service now
has labeled inputs with help descriptions:

- Telegram: Chat ID + Bot Token (custom mode only)
- Discord/Slack/Teams/Google Chat: Webhook URL
- Facebook: Page ID + Page Access Token
- Twitter: Bearer Token + API Key + API Secret
- LinkedIn: Access Token + Organization ID
- Mastodon: Instance URL + Access Token
- Bluesky: Handle + App Password
- WhatsApp: Access Token + Phone Number ID + Recipient
- Mailchimp/SendGrid: API Key + List ID
- WordPress: Site URL + Username + App Password
- Webhook: URL + HTTP Method
- Matrix: Homeserver + Token + Room ID
- Ntfy: Server + Topic + Token
- Reddit: Client ID + Secret + Username + Subreddit
- Medium/Dev.to/Ghost/Blogger: API keys/tokens

Default/Custom mode selector for services with MokoWaaS bot support.

Authorize button for OAuth services (Facebook, LinkedIn, Twitter,
Threads) — visible after first save.

Dashboard button added to toolbar on ALL views (Services, Posts,
Templates, Logs, Service edit, Template edit).

Help panel sidebar in service edit with setup steps.
90+ new language strings for credential fields and help text.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-28 15:06:32 -05:00
jmiller 6d477d9f23 chore: sync .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] 2026-05-28 20:03:31 +00:00
gitea-actions[bot] 5d3da335f3 chore: update development channel 01.00.10-dev [skip ci] 2026-05-28 19:53:56 +00:00
gitea-actions[bot] c463950990 chore: update development channel 01.00.10-dev [skip ci] 2026-05-28 19:53:46 +00:00
gitea-actions[bot] f90e0954f0 chore(version): auto-bump 01.00.10-dev [skip ci] 2026-05-28 19:53:44 +00:00
Jonathan Miller 7a57b001e3 fix: add Service edit view + more default templates
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Update Server / Update Server (push) Has been cancelled
- Added View/Service/HtmlView.php and tmpl/service/edit.php for the
  service edit form (was causing 404 "View not found" error)
- Added credential hint panel in service edit sidebar
- Added 16 more default templates (telegram, discord, slack, facebook,
  linkedin, bluesky, threads, teams, medium, wordpress, webhook,
  sendgrid, brevo, ntfy, reddit, pinterest) — total 20 default templates
- Added credential hint language strings

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-28 14:53:31 -05:00
Moko Consulting 6cba84bde5 fix(workflows): rename remaining old secrets in repo-specific workflows [skip bump]
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
2026-05-28 14:48:22 -05:00
gitea-actions[bot] 5b8aa86357 chore: update development channel 01.00.09-dev [skip ci] 2026-05-28 19:42:06 +00:00
gitea-actions[bot] 9982a4bffd chore: update development channel 01.00.09-dev [skip ci] 2026-05-28 19:41:41 +00:00
gitea-actions[bot] cf7fd55eca chore(version): auto-bump 01.00.09-dev [skip ci] 2026-05-28 19:41:40 +00:00
Jonathan Miller 994cbf2701 fix: filterForm null error + PrepareFormEvent compat + API routes
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Update Server / Update Server (push) Has been cancelled
Fixes three issues found during dev site testing:

1. All 4 list views (Services, Posts, Logs, Templates) missing
   filterForm and activeFilters properties. Joomla searchtools
   layout calls getGroup() on null filterForm. Added get('FilterForm')
   and get('ActiveFilters') to all list HtmlView classes.

2. Content plugin onContentPrepareForm typed as Form but Joomla 5/6
   passes PrepareFormEvent. Now accepts both: extracts Form from
   PrepareFormEvent when available, falls back to legacy Form type.

3. WebServices API routes expanded: added templates and logs CRUD
   endpoints alongside posts and services.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-28 14:41:24 -05:00
Moko Consulting 4e0c776c70 fix(workflows): GITHUB_TOKEN→GH_MIRROR_TOKEN (reserved name) [skip bump]
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
2026-05-28 14:37:15 -05:00
gitea-actions[bot] 207ad9c2c6 chore: update development channel 01.00.08-dev [skip ci] 2026-05-28 19:33:40 +00:00
gitea-actions[bot] 973f83dc32 chore: update development channel 01.00.08-dev [skip ci] 2026-05-28 19:33:23 +00:00
gitea-actions[bot] 55596a1024 chore(version): auto-bump 01.00.08-dev [skip ci] 2026-05-28 19:33:21 +00:00
Jonathan Miller 73d8425130 fix: remove DEFAULT '' from TEXT column error_message
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Update Server / Update Server (push) Has been cancelled
MySQL strict mode does not allow default values on TEXT/BLOB columns.
Removes DEFAULT '' from error_message in #__mokojoomcross_posts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-28 14:33:09 -05:00
Moko Consulting 50152524b9 chore(workflows): sync all universal workflows from moko-platform [skip bump]
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
2026-05-28 14:25:15 -05:00
Moko Consulting b4d8ff3336 refactor(workflows): rename secrets MOKOGITEA_TOKEN/GITHUB_TOKEN, use x-access-token [skip bump]
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
2026-05-28 14:23:40 -05:00
Moko Consulting 003bd1624a fix(workflows): proper suffix handling — use version_set_platform instead of sed [skip bump]
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
2026-05-28 14:15:20 -05:00
gitea-actions[bot] 13b6cf2016 chore: update development channel 01.00.07-dev [skip ci] 2026-05-28 19:04:24 +00:00
Jonathan Miller d70acbc35d docs: restructure CHANGELOG by feature area
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Reorganized from chronological bullet dump into structured sections:
Core Engine, Admin Component, Queue Processing, Per-Article Controls,
OAuth, Migration, Service Plugins (34 platforms by category), Plugin
Configuration, Infrastructure.

Authored-by: Moko Consulting

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-28 14:02:00 -05:00
gitea-actions[bot] 3b089f8d72 chore: update development channel 01.00.07-dev [skip ci] 2026-05-28 19:00:06 +00:00
gitea-actions[bot] fc57c51004 chore(version): auto-bump 01.00.07-dev [skip ci] 2026-05-28 19:00:04 +00:00
Jonathan Miller 3b1b0e8844 feat: 25 expansion service plugins (#23-#47)
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Update Server / Update Server (push) Has been cancelled
Social Media: Threads (Meta), Pinterest, Reddit, Tumblr, TikTok,
Nostr, ActivityPub (generic Fediverse)

Chat/Messaging: Microsoft Teams, Google Chat, WhatsApp Business,
Matrix/Element, Ntfy (push notifications)

Email/Newsletter: SendGrid, Brevo (Sendinblue), ConvertKit,
Constant Contact

Publishing/Blogging: Medium, WordPress, Dev.to, Ghost, Hashnode,
Google Blogger

Business: Google Business Profile

Universal: Generic Webhook (IFTTT/Zapier/n8n/Make/custom endpoints),
RSS Feed (dedicated cross-post feed)

Each plugin implements MokoJoomCrossServiceInterface with publish(),
validateCredentials(), getServiceType(), getServiceName(),
getMaxLength(), supportsMedia(). Teams and Threads have default
MokoWaaS bot modes.

Package now contains 40 sub-extensions (1 component + 5 core
plugins + 34 service plugins). Service type dropdown organized
by category with all 34 platforms.

Authored-by: Moko Consulting

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-28 13:59:54 -05:00
gitea-actions[bot] e31552259d chore: update development channel 01.00.06-dev-dev [skip ci] 2026-05-28 18:42:22 +00:00
gitea-actions[bot] 97915d9f30 chore(version): auto-bump 01.00.06-dev-dev [skip ci] 2026-05-28 18:42:20 +00:00
Jonathan Miller 2872ae2b97 feat: low-priority issues #19-#22
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Update Server / Update Server (push) Has been cancelled
#19 Per-article cross-posting: Content plugin injects "Cross-Posting"
    fieldset into article editor via onContentPrepareForm. Dynamic
    checkbox list of all enabled services. Skip toggle. System plugin
    reads article attribs for mokojoomcross_services (array of service
    IDs) and mokojoomcross_skip (boolean). Unchecked = post to all.

#20 Analytics dashboard: Posts-by-service breakdown table with
    success rate column (color-coded). Top cross-posted articles
    list. DashboardModel methods: getServiceBreakdown(),
    getDailyTrend(), getTopArticles().

#21 OAuth flows: OAuthHelper with authorize URL generation (Facebook,
    LinkedIn, Twitter), PKCE for Twitter, code→token exchange, token
    storage in service credentials. OauthController with authorize
    and callback actions. Reads client ID/secret from plugin params.

#22 Wiki documentation: Services guide (all 9 platforms, default vs
    custom mode), REST API reference, Message Templates guide with
    examples per platform, Troubleshooting guide.

Authored-by: Moko Consulting

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-28 13:42:07 -05:00
Moko Consulting 3664f547ee feat(workflows): append stability suffix to manifest versions [skip bump]
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
2026-05-28 13:41:34 -05:00
gitea-actions[bot] 6521edaab9 chore: update updates.xml (development: 01.00.05-dev-dev) [skip ci] 2026-05-28 18:36:31 +00:00
gitea-actions[bot] 3f6f286ffe chore(version): auto-bump patch 01.00.05-dev [skip ci] 2026-05-28 18:36:29 +00:00
Jonathan Miller 342f6fa3b8 feat: medium-priority issues #12-#18
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Update Server / Update updates.xml (push) Has been cancelled
#12 LinkedIn: plugin config form (OAuth client ID/secret, redirect URI)
#13 Mastodon: plugin config (default instance, visibility, hashtags)
#14 Bluesky: plugin config (default PDS URL, auto link cards)
#15 Mailchimp: plugin config (sender name/email, auto-send toggle)

#17 Template management: full CRUD with TemplatesController,
    TemplateController, TemplatesModel, TemplateModel, TemplateTable.
    List view with service type badges and body preview. Edit view
    with placeholder reference panel showing all 8 placeholders.
    Filter form with search, published, service_type filters.
    Added Templates submenu item and dashboard quick link.

#18 Logs: added filter form with level and search filters.

#16 WebServices: implementation already in place from scaffold,
    routes registered for posts and services CRUD.

Admin component now has 5 submenu items: Dashboard, Post Queue,
Services, Templates, Activity Logs.

Authored-by: Moko Consulting

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-28 13:36:11 -05:00
gitea-actions[bot] 2f60ede713 chore: update updates.xml (development: 01.00.04-dev-dev) [skip ci] 2026-05-28 18:29:39 +00:00
gitea-actions[bot] 8fba003d64 chore(version): auto-bump patch 01.00.04-dev [skip ci] 2026-05-28 18:29:37 +00:00
Jonathan Miller 76dfa177c4 feat: high-priority issues #6-#10 — migration + service plugins
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Update Server / Update updates.xml (push) Has been cancelled
#6 PP Pro migration rewritten to read #__autotweet_channels table
   directly, mapping channeltype names to MokoJoomCross service types.
   Credential extraction per platform (Facebook page tokens, Twitter
   OAuth, Telegram bot tokens, Discord/Slack webhooks). Falls back to
   component params extraction when channel table doesn't exist.

#7 Facebook plugin: config form with default_page_access_token and
   default_page_id. resolveToken() reads from plugin params.

#8 Discord plugin: config form with default_webhook_url and
   embed_color. resolveWebhook() reads from plugin params.

#9 Twitter plugin: implementation already complete from scaffold.

#10 Slack plugin: config form with default_webhook_url.
    resolveWebhook() reads from plugin params.

All service plugins with universal bot support now store default
credentials in their own plugin params (Extensions → Plugins)
rather than component params. This keeps sensitive tokens scoped
to the plugin that uses them.

Authored-by: Moko Consulting

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-28 13:29:22 -05:00
gitea-actions[bot] 4edc5a4765 chore: update updates.xml (development: 01.00.03-dev-dev) [skip ci] 2026-05-28 18:19:51 +00:00
gitea-actions[bot] a67a2a3c5d chore(version): auto-bump patch 01.00.03-dev [skip ci] 2026-05-28 18:19:49 +00:00
Jonathan Miller 9bbf2a74fb feat: queue processor — scheduled task + page-load fallback (#11)
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Update Server / Update updates.xml (push) Has been cancelled
Three-pronged queue processing:

1. Joomla Scheduled Task (preferred): New plg_task_mokojoomcross plugin
   registers "MokoJoomCross - Process Queue" task type. Admin creates
   a scheduled task in System → Scheduled Tasks with desired interval.

2. Page-load fallback: System plugin onAfterRender with configurable
   throttle interval. Runs on backend, frontend, or both. Small batch
   size (5) to avoid slowing page loads.

3. Both can run simultaneously — QueueProcessor uses DB-based lock
   to prevent concurrent execution (120s safety timeout).

Shared QueueProcessor helper handles:
- Queued post dispatch to service plugins
- Failed post retry with configurable max retries + delay
- Scheduled post firing (when scheduled_at <= now)
- Log cleanup based on retention period

Dashboard shows warning banner when page-load processing is active,
recommending switch to Joomla Scheduled Tasks for production.

Config options: queue_processing (scheduler/pageload/both),
pageload_client (admin/site/both), pageload_interval (seconds).

Authored-by: Moko Consulting

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-28 13:19:31 -05:00
gitea-actions[bot] 04e7720268 chore: update updates.xml (development: 01.00.02-dev-dev) [skip ci] 2026-05-28 18:11:32 +00:00
gitea-actions[bot] d4c2ff00c3 chore(version): auto-bump patch 01.00.02-dev [skip ci] 2026-05-28 18:11:31 +00:00
Jonathan Miller 559b9ca30c feat: implement critical issues #1-#5
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Update Server / Update updates.xml (push) Has been cancelled
#1 Core engine: System plugin now dispatches to service plugins via
onMokoJoomCrossGetServices event, executes publish() immediately,
handles success/failure, duplicate guard prevents re-posting, listens
to both onContentAfterSave and onContentChangeState events. Template
rendering now resolves {category}, {author}, {date}, {fulltext}.

#3 Services CRUD: Admin list template with service type icons,
default/custom mode badges, publish toggle. Service edit form template.

#4 Post Queue: Admin list template with status badges (color-coded),
article title, service, message preview, platform post ID, error
messages, retry count, timestamps.

#5 Dashboard: Enhanced with recent activity feed from logs table,
migration controller action for PP Pro import, quick links sidebar.

#2 Telegram: Already implemented in scaffold, provider.php fixed.

Also fixes: All 9 service plugin provider.php files had broken
namespace references from bash heredoc escaping.

Authored-by: Moko Consulting

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-28 13:11:20 -05:00
gitea-actions[bot] 7e4cce51de chore: update updates.xml (development: 01.00.01-dev-dev) [skip ci] 2026-05-28 17:55:19 +00:00
gitea-actions[bot] a2e2a60dea chore(version): auto-bump patch 01.00.01-dev [skip ci] 2026-05-28 17:55:17 +00:00
741 changed files with 4925 additions and 8299 deletions
-83
View File
@@ -1,83 +0,0 @@
# MokoSuiteCross
Cross-posting Joomla content to social media, email marketing, and chat platforms with plugin-based services.
## Quick Reference
| Field | Value |
|---|---|
| **Package** | `pkg_mokosuitecross` |
| **Language** | PHP 8.1+ |
| **Branch** | develop on `dev`, merge to `main` (protected) |
| **Wiki** | [MokoSuiteCross Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteCross/wiki) |
## Commands
```bash
make build # Build package ZIP
make lint # Run linters
make validate # Validate structure
make release # Full release pipeline
make clean # Clean build artifacts
composer install # Install PHP dependencies
```
## Architecture
Joomla **package** with core extensions + pluggable service plugins:
### com_mokosuitecross (Component)
- Admin backend: dashboard, services, post queue, templates, logs
- Joomla 5/6 MVC: Dashboard, Services, Posts, Logs (list/edit each)
- Namespace: `Joomla\Component\MokoSuiteCross\Administrator`
### plg_system_mokosuitecross (System Plugin)
- Hooks `onContentAfterSave` to trigger cross-posting on article publish
- Dispatches to registered service plugins via `mokosuitecross` plugin group
### plg_content_mokosuitecross (Content Plugin)
- Adds cross-post status badges to articles via `onContentBeforeDisplay`
### plg_webservices_mokosuitecross (WebServices Plugin)
- REST API endpoints for posts and services
### Service Plugins (mokosuitecross group)
Each platform is a separate plugin implementing `MokoSuiteCrossServiceInterface`:
- `plg_mokosuitecross_facebook` — Facebook/Meta Graph API
- `plg_mokosuitecross_twitter` — X/Twitter API v2
- `plg_mokosuitecross_linkedin` — LinkedIn Share API
- `plg_mokosuitecross_mastodon` — Mastodon API
- `plg_mokosuitecross_bluesky` — Bluesky AT Protocol
- `plg_mokosuitecross_mailchimp` — Mailchimp Campaigns API
- `plg_mokosuitecross_telegram` — Telegram Bot API
- `plg_mokosuitecross_discord` — Discord Webhooks
- `plg_mokosuitecross_slack` — Slack Incoming Webhooks
### Database Schema
- `#__mokosuitecross_services` — service configs (credentials as individual fields, not JSON)
- `#__mokosuitecross_posts` — post queue (status: queued/posting/posted/failed/scheduled)
- `#__mokosuitecross_templates` — message templates per service type
- `#__mokosuitecross_logs` — activity logs with level and context
## Rules
- **Never commit** `.claude/`, `.mcp.json`, `TODO.md`, `*.min.css`/`*.min.js`
- **Never commit** API keys, tokens, or credentials — these go in Joomla's encrypted params
- **Attribution**: `Authored-by: Moko Consulting`
- **Workflow directory**: `.mokogitea/` (not `.gitea/` or `.github/`)
- **Minification**: handled at build time (CI)
- **Wiki**: documentation lives in the Gitea wiki, not `docs/` files
- **Standards**: [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/mokoplatform/wiki/Home)
- **UX**: service credentials as individual form fields, not JSON blobs; dashboard link in toolbar
## Coding Standards
- PHP 8.1+ minimum
- Joomla 5/6 DI container pattern: `services/provider.php` → Extension class
- Legacy stub `.php` file required for plugin loader but empty
- `SubscriberInterface` for event subscription (not `on*` method naming)
- `bind() → check() → store()` for Table operations (not `save()`)
- Language file placement: site (no `folder`) vs admin (`folder="administrator"`)
- SPDX license headers on all PHP files
- Service plugins MUST implement `MokoSuiteCrossServiceInterface`
-251
View File
@@ -1,251 +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.Automation
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /.gitea/workflows/branch-protection.yml
# BRIEF: Apply standardised branch protection rules to all governed repositories
#
# +========================================================================+
# | BRANCH PROTECTION SETUP |
# +========================================================================+
# | |
# | Applies protection rules for: main, dev, rc, beta, alpha |
# | |
# | main — Require PR, block rejected reviews, no force push |
# | dev — Allow push, no force push, no delete |
# | rc — Allow push, no force push, no delete |
# | beta — Allow push, no force push, no delete |
# | alpha — Allow push, no force push, no delete |
# | |
# | jmiller has override authority on all branches. |
# | |
# +========================================================================+
name: Branch Protection Setup
on:
schedule:
- cron: '0 2 * * 1' # Weekly Monday 02:00 UTC
workflow_dispatch:
inputs:
dry_run:
description: 'Preview mode (no changes)'
required: false
type: boolean
default: false
repos:
description: 'Comma-separated repo names (empty = all governed repos)'
required: false
type: string
default: ''
env:
GITEA_URL: https://git.mokoconsulting.tech
GITEA_ORG: MokoConsulting
permissions:
contents: read
jobs:
protect:
name: Apply Branch Protection Rules
runs-on: ubuntu-latest
steps:
- name: Determine target repos
id: repos
env:
GA_TOKEN: ${{ secrets.GA_TOKEN }}
run: |
API="${GITEA_URL}/api/v1"
# Platform/standards/infra repos to exclude
EXCLUDE="gitea-org-config org-profile gitea-private .mokogitea-private MokoStandards moko-platform MokoTesting"
EXCLUDE="$EXCLUDE MokoStandards-Template-Client MokoStandards-Template-Dolibarr MokoStandards-Template-Generic MokoStandards-Template-Joomla MokoDoliProjTemplate"
if [ -n "${{ inputs.repos }}" ]; then
# User-specified repos
REPOS=$(echo "${{ inputs.repos }}" | tr ',' ' ')
else
# Fetch all org repos
PAGE=1
REPOS=""
while true; do
BATCH=$(curl -sS \
-H "Authorization: token ${GA_TOKEN}" \
"${API}/orgs/${GITEA_ORG}/repos?page=${PAGE}&limit=50" \
| jq -r '.[].name // empty')
[ -z "$BATCH" ] && break
REPOS="$REPOS $BATCH"
PAGE=$((PAGE + 1))
done
# Filter out excluded repos
FILTERED=""
for REPO in $REPOS; do
SKIP=false
for EX in $EXCLUDE; do
if [ "$REPO" = "$EX" ]; then
SKIP=true
break
fi
done
if [ "$SKIP" = "false" ]; then
FILTERED="$FILTERED $REPO"
fi
done
REPOS="$FILTERED"
fi
echo "repos=$REPOS" >> "$GITHUB_OUTPUT"
COUNT=$(echo "$REPOS" | wc -w)
echo "📋 Target repos (${COUNT}): $REPOS"
- name: Apply protection rules
env:
GA_TOKEN: ${{ secrets.GA_TOKEN }}
DRY_RUN: ${{ inputs.dry_run || 'false' }}
run: |
API="${GITEA_URL}/api/v1"
REPOS="${{ steps.repos.outputs.repos }}"
SUCCESS=0
FAILED=0
SKIPPED=0
# ── Rule definitions ──────────────────────────────────────
# Only the CI bot (jmiller token) can push directly.
# All human contributors must use PRs.
# Force push disabled on all branches.
RULE_MAIN='{
"rule_name": "main",
"enable_push": true,
"enable_push_whitelist": true,
"push_whitelist_usernames": ["jmiller"],
"enable_force_push": false,
"enable_force_push_allowlist": false,
"force_push_allowlist_usernames": [],
"enable_merge_whitelist": false,
"required_approvals": 0,
"dismiss_stale_approvals": true,
"block_on_rejected_reviews": true,
"block_on_outdated_branch": false,
"priority": 1
}'
RULE_DEV='{
"rule_name": "dev",
"enable_push": true,
"enable_push_whitelist": true,
"push_whitelist_usernames": ["jmiller"],
"enable_force_push": false,
"enable_force_push_allowlist": false,
"force_push_allowlist_usernames": [],
"enable_merge_whitelist": false,
"required_approvals": 0,
"block_on_rejected_reviews": false,
"priority": 2
}'
RULE_RC='{
"rule_name": "rc",
"enable_push": true,
"enable_push_whitelist": true,
"push_whitelist_usernames": ["jmiller"],
"enable_force_push": false,
"enable_force_push_allowlist": false,
"force_push_allowlist_usernames": [],
"enable_merge_whitelist": false,
"required_approvals": 0,
"block_on_rejected_reviews": false,
"priority": 3
}'
RULE_BETA='{
"rule_name": "beta",
"enable_push": true,
"enable_push_whitelist": true,
"push_whitelist_usernames": ["jmiller"],
"enable_force_push": false,
"enable_force_push_allowlist": false,
"force_push_allowlist_usernames": [],
"enable_merge_whitelist": false,
"required_approvals": 0,
"block_on_rejected_reviews": false,
"priority": 4
}'
RULE_ALPHA='{
"rule_name": "alpha",
"enable_push": true,
"enable_push_whitelist": true,
"push_whitelist_usernames": ["jmiller"],
"enable_force_push": false,
"enable_force_push_allowlist": false,
"force_push_allowlist_usernames": [],
"enable_merge_whitelist": false,
"required_approvals": 0,
"block_on_rejected_reviews": false,
"priority": 5
}'
RULES=("$RULE_MAIN" "$RULE_DEV" "$RULE_RC" "$RULE_BETA" "$RULE_ALPHA")
RULE_NAMES=("main" "dev" "rc" "beta" "alpha")
# ── Apply rules to each repo ──────────────────────────────
for REPO in $REPOS; do
echo ""
echo "═══ ${REPO} ═══"
for i in "${!RULES[@]}"; do
RULE="${RULES[$i]}"
NAME="${RULE_NAMES[$i]}"
if [ "$DRY_RUN" = "true" ]; then
echo " [DRY RUN] Would apply rule: ${NAME}"
SKIPPED=$((SKIPPED + 1))
continue
fi
# Delete existing rule if present (idempotent recreate)
ENCODED_NAME=$(echo "$NAME" | sed 's|/|%2F|g')
curl -sS -o /dev/null -w "" \
-X DELETE \
-H "Authorization: token ${GA_TOKEN}" \
"${API}/repos/${GITEA_ORG}/${REPO}/branch_protections/${ENCODED_NAME}" 2>/dev/null || true
# Create rule
RESPONSE=$(curl -sS -w "\n%{http_code}" \
-X POST \
-H "Authorization: token ${GA_TOKEN}" \
-H "Content-Type: application/json" \
-d "$RULE" \
"${API}/repos/${GITEA_ORG}/${REPO}/branch_protections")
HTTP=$(echo "$RESPONSE" | tail -1)
BODY=$(echo "$RESPONSE" | sed '$d')
if [ "$HTTP" = "201" ]; then
echo " ✅ ${NAME}"
SUCCESS=$((SUCCESS + 1))
else
echo " ❌ ${NAME} (HTTP ${HTTP}): $(echo "$BODY" | jq -r '.message // .' 2>/dev/null | head -1)"
FAILED=$((FAILED + 1))
fi
done
done
# ── Summary ───────────────────────────────────────────────
echo ""
echo "════════════════════════════════════════"
echo " ✅ Success: ${SUCCESS}"
echo " ❌ Failed: ${FAILED}"
echo " ⏭️ Skipped: ${SKIPPED}"
echo "════════════════════════════════════════"
if [ "$FAILED" -gt 0 ]; then
echo "::warning::${FAILED} rule(s) failed to apply"
fi
+7 -12
View File
@@ -1,26 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<mokoplatform xmlns="https://standards.mokoconsulting.tech/mokoplatform/1.0" schema-version="1.0">
<moko-platform xmlns="https://standards.mokoconsulting.tech/moko-platform/1.0" schema-version="1.0">
<identity>
<name>MokoSuiteCross</name>
<display-name>Package - MokoSuiteCross</display-name>
<name>MokoJoomCross</name>
<display-name>Package - MokoJoomCross</display-name>
<org>MokoConsulting</org>
<description>Cross-posting Joomla content to social media, email marketing, and chat platforms</description>
<version>01.04.01</version>
<version>01.00.27</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>
<standards-source>https://git.mokoconsulting.tech/MokoConsulting/moko-platform</standards-source>
</governance>
<build>
<language>PHP</language>
<package-type>joomla-extension</package-type>
<entry-point>source/</entry-point>
<entry-point>src/</entry-point>
</build>
<licensing>
<enabled>true</enabled>
<dlid>true</dlid>
<update-server>https://git.mokoconsulting.tech/{org}/{repo}/updates.xml</update-server>
</licensing>
</mokoplatform>
</moko-platform>
-66
View File
@@ -1,66 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /.mokogitea/workflows/auto-bump.yml
# VERSION: 09.02.00
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
name: "Universal: Auto Version Bump"
on:
push:
branches:
- dev
- rc
- 'feature/**'
- 'patch/**'
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
permissions:
contents: write
jobs:
bump:
name: Version Bump
runs-on: release
if: >-
!contains(github.event.head_commit.message, '[skip ci]') &&
!contains(github.event.head_commit.message, '[skip bump]') &&
!startsWith(github.event.head_commit.message, 'Merge pull request')
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 1
- name: Setup mokocli tools
run: |
if ! command -v composer &> /dev/null; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
fi
if [ -d "/opt/mokocli/cli" ]; then
echo "MOKO_CLI=/opt/mokocli/cli" >> "$GITHUB_ENV"
else
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/mokocli.git" \
/tmp/mokocli
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
echo "MOKO_CLI=/tmp/mokocli/cli" >> "$GITHUB_ENV"
fi
- name: Bump version
run: |
php ${MOKO_CLI}/version_auto_bump.php \
--path . --branch "${GITHUB_REF_NAME}" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
--repo-url "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
+285 -457
View File
@@ -1,457 +1,285 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Release
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli
# PATH: /templates/workflows/universal/auto-release.yml.template
# VERSION: 05.00.00
# BRIEF: Universal build & release detects platform from manifest.xml
#
# +=======================================================================+
# | UNIVERSAL BUILD & RELEASE PIPELINE |
# +=======================================================================+
# | |
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
# | |
# | Platform-specific: |
# | joomla: XML manifest, type-prefixed packages |
# | dolibarr: mod*.class.php, update.txt, dev version reset |
# | generic: README-only, no update stream |
# | |
# +=======================================================================+
name: "Universal: Build & Release"
on:
pull_request:
types: [opened, closed]
branches:
- main
workflow_dispatch:
inputs:
action:
description: 'Action to perform'
required: false
type: choice
default: release
options:
- release
- promote-rc
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:
# ── PR Opened → Rename branch to RC and build RC release ─────────────────────────
promote-rc:
name: Promote to RC
runs-on: release
if: >-
(github.event.action == 'opened' && github.event.pull_request.merged != true) ||
(github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 1
- name: Setup mokocli tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
run: |
if [ -f /opt/mokocli/cli/version_bump.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: Rename branch to rc
run: |
php ${MOKO_CLI}/branch_rename.php \
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
--pr "${{ github.event.pull_request.number }}"
- name: Checkout rc and configure git
run: |
git fetch origin rc
git checkout rc
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
- name: Publish RC release
run: |
php ${MOKO_CLI}/release_publish.php \
--path . --stability rc --bump minor --branch rc \
--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
if: always()
run: |
echo "## Promoted to Release Candidate" >> $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) ─────────────────────────
release:
name: Build & Release Pipeline
runs-on: release
if: >-
github.event.pull_request.merged == true ||
(github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 0
- name: Configure git for bot pushes
run: |
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
- name: Check for merge conflict markers
run: |
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
if [ -n "$CONFLICTS" ]; then
echo "::error::Merge conflict markers found — aborting release"
echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "No conflict markers found"
- name: Setup mokocli tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
run: |
if [ -f /opt/mokocli/cli/version_bump.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: "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"
run: |
BUMP_FLAG=""
if [ "${{ steps.bump.outputs.level }}" != "none" ]; then
BUMP_FLAG="--bump ${{ steps.bump.outputs.level }}"
fi
php ${MOKO_CLI}/release_publish.php \
--path . --stability stable ${BUMP_FLAG} --branch main \
--token "${{ secrets.MOKOGITEA_TOKEN }}"
- name: "Read published version"
id: version
run: |
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "")
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
[ -z "$VERSION" ] && VERSION="00.00.00" && echo "skip=true" >> "$GITHUB_OUTPUT"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
PLATFORM="${{ steps.platform.outputs.platform }}"
if [[ "$PLATFORM" == joomla* ]]; then
echo "tag=stable" >> "$GITHUB_OUTPUT"
echo "release_tag=stable" >> "$GITHUB_OUTPUT"
else
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
echo "semver_tag=${SEMVER_TAG}" >> "$GITHUB_OUTPUT"
- 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
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 "Release notes updated from CHANGELOG.md"
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) --------------------------------
- name: "Step 9: Mirror release to GitHub"
if: >-
steps.version.outputs.skip != 'true' &&
secrets.GH_MIRROR_TOKEN != ''
continue-on-error: true
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php ${MOKO_CLI}/release_mirror.php \
--version "$VERSION" --tag "$RELEASE_TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
--branch main 2>&1 || true
echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY
# -- STEP 10: Sync main branch to GitHub mirror ----------------------------
- name: "Step 10: Push main to GitHub mirror"
if: >-
steps.version.outputs.skip != 'true' &&
secrets.GH_MIRROR_TOKEN != ''
continue-on-error: true
run: |
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
git fetch origin main --depth=1
git push github origin/main:refs/heads/main --force 2>/dev/null \
&& echo "main branch pushed to GitHub mirror" \
|| echo "WARNING: GitHub mirror push failed"
- name: "Step 11: Delete rc branch and recreate dev from main"
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
# Delete rc branch (ephemeral — created by promote-rc)
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
"${API_BASE}/branches/rc" 2>/dev/null \
&& echo "Deleted rc branch" || echo "rc branch not found"
# Delete dev branch
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
"${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
# Recreate dev from main (now includes version bump + changelog promotion)
curl -sf -X POST -H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
"${API_BASE}/branches" \
-d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY
- name: "Step 12: Create version branch from main"
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
BRANCH_NAME="version/${VERSION}"
MAIN_SHA=$(git rev-parse HEAD)
# Delete old version branch if it exists (same version re-release)
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}"
# Create version/XX.YY.ZZ from main
curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed"
echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY
# -- Dolibarr post-release: Reset dev version -----------------------------
- name: "Post-release: Reset dev version"
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php ${MOKO_CLI}/version_reset_dev.php \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
--branch dev --path . 2>&1 || true
# -- Summary --------------------------------------------------------------
- name: Pipeline Summary
if: always()
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
PLATFORM="${{ steps.platform.outputs.platform }}"
if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
else
echo "" >> $GITHUB_STEP_SUMMARY
echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
fi
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Release
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# PATH: /templates/workflows/universal/auto-release.yml.template
# VERSION: 05.00.00
# BRIEF: Universal build & release detects platform from manifest.xml
#
# +========================================================================+
# | UNIVERSAL BUILD & RELEASE PIPELINE |
# +========================================================================+
# | |
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
# | |
# | Platform-specific: |
# | joomla: XML manifest, updates.xml, type-prefixed packages |
# | dolibarr: mod*.class.php, update.txt, dev version reset |
# | generic: README-only, no update stream |
# | |
# +========================================================================+
name: "Universal: Build & Release"
on:
pull_request:
types: [opened, closed]
branches:
- main
workflow_dispatch:
inputs:
action:
description: 'Action to perform'
required: false
type: choice
default: release
options:
- release
- promote-rc
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:
# ── PR Opened → Rename branch to RC and build RC release ─────────────────────
promote-rc:
name: Promote to RC
runs-on: release
if: >-
(github.event.action == 'opened' && github.event.pull_request.merged != true) ||
(github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 1
- name: Setup moko-platform tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
run: |
if ! command -v composer &> /dev/null; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
fi
# Always fetch latest CLI tools — never use stale cache from previous runs
rm -rf /tmp/moko-platform-api
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
/tmp/moko-platform-api
cd /tmp/moko-platform-api
composer install --no-dev --no-interaction --quiet
- name: Rename branch to rc
run: |
php /tmp/moko-platform-api/cli/branch_rename.php \
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
--pr "${{ github.event.pull_request.number }}"
- name: Checkout rc and configure git
run: |
git fetch origin rc
git checkout rc
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
- name: Publish RC release
run: |
php /tmp/moko-platform-api/cli/release_publish.php \
--path . --stability rc --bump minor --branch rc \
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
--skip-update-stream
- name: Summary
if: always()
run: |
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
echo "Branch renamed to rc, minor bump, RC release built (updates.xml managed by Gitea Pages)" >> $GITHUB_STEP_SUMMARY
# ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
release:
name: Build & Release Pipeline
runs-on: release
if: >-
github.event.pull_request.merged == true ||
(github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 0
- name: Configure git for bot pushes
run: |
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
- name: Check for merge conflict markers
run: |
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
if [ -n "$CONFLICTS" ]; then
echo "::error::Merge conflict markers found — aborting release"
echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "No conflict markers found"
- name: Setup moko-platform tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
run: |
# Ensure PHP + Composer are available
if ! command -v composer &> /dev/null; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
fi
# Always fetch latest CLI tools — never use stale cache from previous runs
rm -rf /tmp/moko-platform-api
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
/tmp/moko-platform-api
cd /tmp/moko-platform-api
composer install --no-dev --no-interaction --quiet
- name: "Publish stable release"
run: |
php /tmp/moko-platform-api/cli/release_publish.php \
--path . --stability stable --bump minor --branch main \
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
--skip-update-stream
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
- name: "Step 9: Mirror release to GitHub"
if: >-
steps.version.outputs.skip != 'true' &&
secrets.GH_MIRROR_TOKEN != ''
continue-on-error: true
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php /tmp/moko-platform-api/cli/release_mirror.php \
--version "$VERSION" --tag "$RELEASE_TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
--branch main 2>&1 || true
echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY
# -- STEP 10: Sync main branch to GitHub mirror ----------------------------
- name: "Step 10: Push main to GitHub mirror"
if: >-
steps.version.outputs.skip != 'true' &&
secrets.GH_MIRROR_TOKEN != ''
continue-on-error: true
run: |
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
git fetch origin main --depth=1
git push github origin/main:refs/heads/main --force 2>/dev/null \
&& echo "main branch pushed to GitHub mirror" \
|| echo "WARNING: GitHub mirror push failed"
- name: "Step 11: Delete rc branch and recreate dev from main"
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
# Delete rc branch (ephemeral — created by promote-rc)
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
"${API_BASE}/branches/rc" 2>/dev/null \
&& echo "Deleted rc branch" || echo "rc branch not found"
# Delete dev branch
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
"${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
# Recreate dev from main (now includes version bump + changelog promotion)
curl -sf -X POST -H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
"${API_BASE}/branches" \
-d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY
- name: "Step 12: Create version branch from main"
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
BRANCH_NAME="version/${VERSION}"
MAIN_SHA=$(git rev-parse HEAD)
# Delete old version branch if it exists (same version re-release)
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}"
# Create version/XX.YY.ZZ from main
curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed"
echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY
# -- Dolibarr post-release: Reset dev version -----------------------------
- name: "Post-release: Reset dev version"
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php /tmp/moko-platform-api/cli/version_reset_dev.php \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
--branch dev --path . 2>&1 || true
# -- Summary --------------------------------------------------------------
- name: Pipeline Summary
if: always()
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
PLATFORM="${{ steps.platform.outputs.platform }}"
if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
else
echo "" >> $GITHUB_STEP_SUMMARY
echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
fi
-48
View File
@@ -1,48 +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.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /.mokogitea/workflows/branch-cleanup.yml
# VERSION: 01.00.00
# BRIEF: Delete feature branches after PR merge
name: "Branch Cleanup"
on:
pull_request:
types: [closed]
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
cleanup:
name: Delete merged branch
runs-on: ubuntu-latest
if: >-
github.event.pull_request.merged == true &&
github.event.pull_request.head.ref != 'dev' &&
github.event.pull_request.head.ref != 'main'
steps:
- name: Delete source branch
run: |
BRANCH="${{ github.event.pull_request.head.ref }}"
API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches"
ENCODED=$(php -r "echo rawurlencode('${BRANCH}');")
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \
-H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
"${API}/${ENCODED}" 2>/dev/null || true)
if [ "$STATUS" = "204" ]; then
echo "Deleted branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
elif [ "$STATUS" = "404" ]; then
echo "Branch already deleted: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
else
echo "::warning::Failed to delete branch ${BRANCH} (HTTP ${STATUS})"
fi
-10
View File
@@ -1,10 +0,0 @@
# DISABLED — auto-release Step 11 recreates dev from main after every release.
# Cascade-dev is redundant and causes version conflicts when both main and dev
# have different version numbers in templateDetails.xml / manifest.xml.
name: "Cascade Main → Dev (DISABLED)"
on: workflow_dispatch
jobs:
noop:
runs-on: ubuntu-latest
steps:
- run: echo "Cascade disabled — auto-release handles dev recreation"
-191
View File
@@ -1,191 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.CI
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
# PATH: /.gitea/workflows/ci-generic.yml
# VERSION: 01.00.00
# BRIEF: CI pipeline — lint, validate, and test for generic projects (PHP + Node.js)
name: "Generic: Project CI"
on:
workflow_dispatch:
permissions:
contents: read
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
# ── Lint & Validate ───────────────────────────────────────────────────
lint:
name: Lint & Validate
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Detect toolchain
id: detect
run: |
HAS_PHP=false
HAS_NODE=false
[ -f "composer.json" ] && HAS_PHP=true
[ -f "package.json" ] && HAS_NODE=true
echo "has_php=$HAS_PHP" >> "$GITHUB_OUTPUT"
echo "has_node=$HAS_NODE" >> "$GITHUB_OUTPUT"
echo "Toolchain: PHP=$HAS_PHP Node=$HAS_NODE"
- name: Setup PHP
if: steps.detect.outputs.has_php == 'true'
run: |
if ! command -v php &> /dev/null; then
sudo apt-get update -qq
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
fi
php -v
- name: Setup Node.js
if: steps.detect.outputs.has_node == 'true'
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install PHP dependencies
if: steps.detect.outputs.has_php == 'true'
run: |
if [ -f "composer.json" ]; then
composer install --no-interaction --prefer-dist --quiet 2>/dev/null || true
fi
- name: Install Node.js dependencies
if: steps.detect.outputs.has_node == 'true'
run: |
if [ -f "package.json" ]; then
npm ci --quiet 2>/dev/null || npm install --quiet 2>/dev/null || true
fi
- name: PHP syntax check
if: steps.detect.outputs.has_php == 'true'
run: |
ERRORS=0
while IFS= read -r -d '' file; do
if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
echo "::error file=${file}::PHP syntax error"
ERRORS=$((ERRORS + 1))
fi
done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -not -path "./node_modules/*" -print0)
echo "## PHP Lint" >> $GITHUB_STEP_SUMMARY
if [ "$ERRORS" -eq 0 ]; then
echo "All PHP files passed syntax check." >> $GITHUB_STEP_SUMMARY
else
echo "${ERRORS} file(s) with syntax errors." >> $GITHUB_STEP_SUMMARY
exit 1
fi
- name: TypeScript/JavaScript lint
if: steps.detect.outputs.has_node == 'true'
run: |
if [ -f "node_modules/.bin/eslint" ]; then
npx eslint src/ --quiet 2>&1 || { echo "::error::ESLint errors found"; exit 1; }
echo "## ESLint" >> $GITHUB_STEP_SUMMARY
echo "All files passed ESLint." >> $GITHUB_STEP_SUMMARY
elif [ -f ".eslintrc.json" ] || [ -f ".eslintrc.js" ] || [ -f "eslint.config.js" ]; then
echo "::warning::ESLint config found but eslint not installed"
else
echo "No ESLint configured — skipping"
fi
- name: TypeScript compile check
if: steps.detect.outputs.has_node == 'true'
run: |
if [ -f "tsconfig.json" ] && [ -f "node_modules/.bin/tsc" ]; then
npx tsc --noEmit 2>&1 || { echo "::error::TypeScript compilation errors"; exit 1; }
echo "## TypeScript" >> $GITHUB_STEP_SUMMARY
echo "TypeScript compilation passed." >> $GITHUB_STEP_SUMMARY
fi
- name: PHPStan static analysis
if: steps.detect.outputs.has_php == 'true'
run: |
if [ -f "phpstan.neon" ] && [ -f "vendor/bin/phpstan" ]; then
vendor/bin/phpstan analyse --no-progress 2>&1 || { echo "::warning::PHPStan found issues"; }
fi
# ── Tests ─────────────────────────────────────────────────────────────
test:
name: Tests
runs-on: ubuntu-latest
needs: lint
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Detect toolchain
id: detect
run: |
HAS_PHP=false
HAS_NODE=false
[ -f "composer.json" ] && HAS_PHP=true
[ -f "package.json" ] && HAS_NODE=true
echo "has_php=$HAS_PHP" >> "$GITHUB_OUTPUT"
echo "has_node=$HAS_NODE" >> "$GITHUB_OUTPUT"
- name: Setup PHP
if: steps.detect.outputs.has_php == 'true'
run: |
if ! command -v php &> /dev/null; then
sudo apt-get update -qq
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
fi
- name: Setup Node.js
if: steps.detect.outputs.has_node == 'true'
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: |
[ -f "composer.json" ] && composer install --no-interaction --prefer-dist --quiet 2>/dev/null || true
[ -f "package.json" ] && { npm ci --quiet 2>/dev/null || npm install --quiet 2>/dev/null || true; }
- name: Run PHP tests
if: steps.detect.outputs.has_php == 'true'
run: |
if [ -f "vendor/bin/phpunit" ]; then
vendor/bin/phpunit --testdox 2>&1
echo "## PHPUnit" >> $GITHUB_STEP_SUMMARY
echo "Tests passed." >> $GITHUB_STEP_SUMMARY
elif [ -f "phpunit.xml" ] || [ -f "phpunit.xml.dist" ]; then
echo "::warning::PHPUnit config found but phpunit not installed"
else
echo "No PHPUnit configured — skipping"
fi
- name: Run Node.js tests
if: steps.detect.outputs.has_node == 'true'
run: |
if jq -e '.scripts.test' package.json > /dev/null 2>&1; then
npm test 2>&1
echo "## Node.js Tests" >> $GITHUB_STEP_SUMMARY
echo "Tests passed." >> $GITHUB_STEP_SUMMARY
else
echo "No test script in package.json — skipping"
fi
- name: Build check
run: |
if [ -f "Makefile" ]; then
make build 2>&1 || echo "::warning::Build failed or not configured"
elif [ -f "package.json" ] && jq -e '.scripts.build' package.json > /dev/null 2>&1; then
npm run build 2>&1 || echo "::warning::Build failed"
fi
+11 -451
View File
@@ -45,22 +45,19 @@ jobs:
fi
php -v && composer --version
- name: Setup mokocli tools
- name: Clone MokoStandards
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.GA_TOKEN || github.token }}
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}
MOKO_CLONE_HOST: ${{ secrets.MOKOGITEA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
run: |
if [ -d "/opt/mokocli" ] || [ -d "/tmp/mokocli" ]; then
echo "mokocli already available on runner — skipping clone"
else
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git" \
/tmp/mokocli 2>/dev/null || echo "mokocli clone skipped — continuing without it"
fi
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
/tmp/mokostandards-api
- name: Install dependencies
env:
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || secrets.GA_TOKEN || github.token }}"}}'
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}'
run: |
if [ -f "composer.json" ]; then
composer install \
@@ -131,8 +128,8 @@ jobs:
echo "Manifest is well-formed XML." >> $GITHUB_STEP_SUMMARY
fi
# Check required tags: name, version, author
for TAG in name version author; do
# Check required tags: name, version, author, namespace (Joomla 5+)
for TAG in name version author namespace; do
if ! grep -q "<${TAG}>" "$MANIFEST" 2>/dev/null; then
echo "Missing required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
@@ -140,19 +137,6 @@ jobs:
echo "Found required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY
fi
done
# Namespace is required for components/plugins but not packages
EXT_TYPE=$(grep -oP '<extension[^>]*\btype="\K[^"]+' "$MANIFEST" | head -1)
if [ "$EXT_TYPE" != "package" ]; then
if ! grep -q "<namespace" "$MANIFEST" 2>/dev/null; then
echo "Missing required tag: \`<namespace>\` (required for Joomla 5+ ${EXT_TYPE} extensions)" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "Found required tag: \`<namespace>\`" >> $GITHUB_STEP_SUMMARY
fi
else
echo "Package extension — \`<namespace>\` not required." >> $GITHUB_STEP_SUMMARY
fi
fi
if [ "${ERRORS}" -gt 0 ]; then
@@ -245,413 +229,10 @@ jobs:
echo "All ${CHECKED} directories contain index.html." >> $GITHUB_STEP_SUMMARY
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
release-readiness:
name: Release Readiness Check
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' && github.base_ref == 'main'
continue-on-error: true
steps:
- name: Checkout repository
@@ -773,7 +354,7 @@ jobs:
- name: Install dependencies
env:
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || secrets.GA_TOKEN || github.token }}"}}'
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}'
run: |
if [ -f "composer.json" ]; then
composer install \
@@ -823,7 +404,7 @@ jobs:
- name: Install dependencies
env:
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || secrets.GA_TOKEN || github.token }}"}}'
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}'
run: |
if [ -f "composer.json" ]; then
composer install --no-interaction --prefer-dist --optimize-autoloader
@@ -880,24 +461,3 @@ jobs:
echo '```' >> $GITHUB_STEP_SUMMARY
fi
exit $EXIT
pre-release:
name: Build RC Pre-Release
runs-on: ubuntu-latest
needs: [lint-and-validate, test]
if: github.event_name == 'pull_request'
steps:
- name: Trigger pre-release build
env:
GA_TOKEN: ${{ secrets.GA_TOKEN }}
REPO: ${{ github.repository }}
BRANCH: ${{ github.head_ref }}
run: |
curl -s -X POST \
"${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" \
-H "Authorization: token ${GA_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
-87
View File
@@ -1,87 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Maintenance
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /.gitea/workflows/cleanup.yml
# VERSION: 01.00.00
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
name: "Universal: Repository Cleanup"
on:
schedule:
- cron: '0 3 * * 0' # Weekly on Sunday at 03:00 UTC
workflow_dispatch:
permissions:
contents: write
env:
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
jobs:
cleanup:
name: Clean Merged Branches
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GA_TOKEN }}
- name: Delete merged branches
env:
GA_TOKEN: ${{ secrets.GA_TOKEN }}
run: |
echo "=== Merged Branch Cleanup ==="
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
# List branches via API
BRANCHES=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
"${API}/branches?limit=50" | jq -r '.[].name')
DELETED=0
for BRANCH in $BRANCHES; do
# Skip protected branches
case "$BRANCH" in
main|master|develop|release/*|hotfix/*) continue ;;
esac
# Check if branch is merged into main
if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then
echo " Deleting merged branch: ${BRANCH}"
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
"${API}/branches/${BRANCH}" 2>/dev/null || true
DELETED=$((DELETED + 1))
fi
done
echo "Deleted ${DELETED} merged branch(es)"
- name: Clean old workflow runs
env:
GA_TOKEN: ${{ secrets.GA_TOKEN }}
run: |
echo "=== Workflow Run Cleanup ==="
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ)
# Get old completed runs
RUNS=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
"${API}/actions/runs?status=completed&limit=50" | \
jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null)
DELETED=0
for RUN_ID in $RUNS; do
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
"${API}/actions/runs/${RUN_ID}" 2>/dev/null || true
DELETED=$((DELETED + 1))
done
echo "Deleted ${DELETED} old workflow run(s)"
-76
View File
@@ -1,76 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# SPDX-License-Identifier: GPL-3.0-or-later
name: "Publish to Composer"
on:
push:
tags:
- 'v*'
- '[0-9]*.[0-9]*.[0-9]*'
release:
types: [published]
workflow_dispatch:
env:
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
jobs:
publish:
name: Publish Package
runs-on: ubuntu-latest
if: >-
!contains(github.event.head_commit.message, '[skip ci]') &&
!contains(github.event.head_commit.message, '[skip publish]')
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup PHP
run: |
if ! command -v php &> /dev/null; then
sudo apt-get update -qq
sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
fi
- name: Install dependencies
run: composer install --no-dev --no-interaction --prefer-dist --quiet
- name: Determine version
id: version
run: |
VERSION=$(php -r "echo json_decode(file_get_contents('composer.json'))->version;")
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "Package version: ${VERSION}"
# Gitea Composer Registry — auto-publishes from tags
# The tag push itself registers the package at:
# https://git.mokoconsulting.tech/api/packages/MokoConsulting/composer
- name: Verify Gitea registry
run: |
echo "Gitea Composer registry auto-publishes from tags."
echo "Package available at: ${GITEA_URL}/api/packages/MokoConsulting/composer"
echo "Install: composer require mokoconsulting/mokocli"
# Packagist — notify of new version
- name: Notify Packagist
if: secrets.PACKAGIST_TOKEN != ''
run: |
VERSION="${{ steps.version.outputs.version }}"
echo "Notifying Packagist of version ${VERSION}..."
curl -sf -X POST \
-H "Content-Type: application/json" \
-d '{"repository":{"url":"https://git.mokoconsulting.tech/MokoConsulting/mokocli"}}' \
"https://packagist.org/api/update-package?username=mokoconsulting&apiToken=${{ secrets.PACKAGIST_TOKEN }}" \
&& echo "Packagist notified" \
|| echo "::warning::Packagist notification failed (package may not be registered yet)"
- name: Summary
run: |
VERSION="${{ steps.version.outputs.version }}"
echo "## Composer Package Published" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Registry | Status |" >> $GITHUB_STEP_SUMMARY
echo "|----------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Gitea | \`composer require mokoconsulting/mokocli:${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Packagist | \`composer require mokoconsulting/mokocli\` |" >> $GITHUB_STEP_SUMMARY
-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
-92
View File
@@ -1,92 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Security
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
# PATH: /templates/workflows/gitleaks.yml.template
# VERSION: 01.00.00
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
#
# +========================================================================+
# | SECRET SCANNING |
# +========================================================================+
# | |
# | Scans commits for leaked secrets using Gitleaks. |
# | |
# | - PR scan: only new commits in the PR |
# | - Scheduled: full repo scan weekly |
# | - Alerts via ntfy on findings |
# | |
# +========================================================================+
name: "Universal: Secret Scanning"
on:
schedule:
- cron: '0 5 * * 1' # Weekly Monday 05:00 UTC
workflow_dispatch:
permissions:
contents: read
env:
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }}
jobs:
gitleaks:
name: Gitleaks Secret Scan
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Gitleaks
run: |
GITLEAKS_VERSION="8.21.2"
curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \
| tar -xz -C /usr/local/bin gitleaks
gitleaks version
- name: Scan for secrets
id: scan
run: |
echo "### Secret Scanning" >> $GITHUB_STEP_SUMMARY
ARGS="--source . --verbose --report-format json --report-path /tmp/gitleaks-report.json"
if [ "${{ github.event_name }}" = "pull_request" ]; then
# Scan only PR commits
ARGS="$ARGS --log-opts=${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}"
echo "Scanning PR commits only" >> $GITHUB_STEP_SUMMARY
else
echo "Full repository scan" >> $GITHUB_STEP_SUMMARY
fi
if gitleaks detect $ARGS 2>&1; then
echo "result=clean" >> "$GITHUB_OUTPUT"
echo "**No secrets detected.**" >> $GITHUB_STEP_SUMMARY
else
echo "result=found" >> "$GITHUB_OUTPUT"
FINDINGS=$(jq length /tmp/gitleaks-report.json 2>/dev/null || echo "unknown")
echo "**${FINDINGS} potential secret(s) detected.**" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Review the findings and rotate any exposed credentials immediately." >> $GITHUB_STEP_SUMMARY
exit 1
fi
- name: Notify on findings
if: failure() && steps.scan.outputs.result == 'found'
run: |
REPO="${{ github.event.repository.name }}"
curl -sS \
-H "Title: ${REPO} — secrets detected in code" \
-H "Tags: rotating_light,key" \
-H "Priority: urgent" \
-d "Gitleaks found potential secrets. Review and rotate credentials immediately." \
"${NTFY_URL}/${NTFY_TOPIC}" || true
+3 -3
View File
@@ -4,8 +4,8 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Automation
# VERSION: 01.00.00
# INGROUP: moko-platform.Automation
# VERSION: 01.00.27
# BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch"
@@ -28,7 +28,7 @@ jobs:
steps:
- name: Create branch and comment
run: |
TOKEN="${{ secrets.GA_TOKEN }}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
ISSUE_NUM="${{ github.event.issue.number }}"
ISSUE_TITLE="${{ github.event.issue.title }}"
-70
View File
@@ -1,70 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Notifications
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /.gitea/workflows/notify.yml
# VERSION: 01.00.00
# BRIEF: Push notifications via ntfy on release success or workflow failure
name: "Universal: Notifications"
on:
workflow_run:
workflows:
- "Joomla Build & Release"
- "Joomla Extension CI"
- "Deploy"
types:
- completed
permissions:
contents: read
env:
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-releases' }}
jobs:
notify:
name: Send Notification
runs-on: ubuntu-latest
if: >-
github.event.workflow_run.conclusion == 'success' ||
github.event.workflow_run.conclusion == 'failure'
steps:
- name: Notify on success (releases only)
if: >-
github.event.workflow_run.conclusion == 'success' &&
contains(github.event.workflow_run.name, 'Release')
run: |
REPO="${{ github.event.repository.name }}"
WORKFLOW="${{ github.event.workflow_run.name }}"
URL="${{ github.event.workflow_run.html_url }}"
curl -sS \
-H "Title: ${REPO} released" \
-H "Tags: white_check_mark,package" \
-H "Priority: default" \
-H "Click: ${URL}" \
-d "${WORKFLOW} completed successfully." \
"${NTFY_URL}/${NTFY_TOPIC}"
- name: Notify on failure
if: github.event.workflow_run.conclusion == 'failure'
run: |
REPO="${{ github.event.repository.name }}"
WORKFLOW="${{ github.event.workflow_run.name }}"
URL="${{ github.event.workflow_run.html_url }}"
curl -sS \
-H "Title: ${REPO} workflow failed" \
-H "Tags: x,warning" \
-H "Priority: high" \
-H "Click: ${URL}" \
-d "${WORKFLOW} failed. Check the run for details." \
"${NTFY_URL}/${NTFY_TOPIC}"
-26
View File
@@ -96,32 +96,6 @@ jobs:
echo "Branch policy: OK (${HEAD} → ${BASE})"
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 ────────────────────────────────────────────────────
validate:
name: Validate PR
@@ -1,71 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: 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
+26 -44
View File
@@ -4,26 +4,23 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# INGROUP: moko-platform.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /templates/workflows/universal/pre-release.yml.template
# VERSION: 05.01.00
# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches
# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch
name: "Universal: Pre-Release"
on:
push:
pull_request:
types: [closed]
branches:
- dev
- 'fix/**'
- 'patch/**'
- 'hotfix/**'
- 'bugfix/**'
- 'chore/**'
- alpha
- beta
- rc
pull_request_target:
types: [synchronize, opened, reopened]
branches:
- main
workflow_dispatch:
inputs:
stability:
@@ -46,11 +43,12 @@ env:
jobs:
build:
name: "Build Pre-Release (${{ inputs.stability || github.ref_name }})"
name: "Build Pre-Release (${{ inputs.stability || 'development' }})"
runs-on: release
if: >-
github.event_name == 'workflow_dispatch' ||
github.event_name == 'push'
(github.event_name == 'pull_request' && github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev') ||
(github.event_name == 'pull_request_target' && github.event.pull_request.base.ref == 'main')
steps:
- name: Checkout
@@ -58,47 +56,34 @@ jobs:
with:
fetch-depth: 0
token: ${{ secrets.MOKOGITEA_TOKEN }}
ref: ${{ github.ref_name }}
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || '' }}
- name: Setup mokocli tools
- name: Setup moko-platform tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
run: |
# Use pre-installed /opt/mokocli if available (updated by cron every 6h)
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/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
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
rm -rf /tmp/moko-platform-api
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
/tmp/moko-platform-api
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
- name: Detect platform
id: platform
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
- name: Resolve metadata and bump version
id: meta
run: |
# Auto-detect stability from branch name on push, or use input on dispatch
if [ "${{ github.event_name }}" = "push" ]; then
case "${{ github.ref_name }}" in
rc) STABILITY="release-candidate" ;;
alpha) STABILITY="alpha" ;;
beta) STABILITY="beta" ;;
*) STABILITY="development" ;;
esac
# Auto-detect stability: RC for PRs targeting main, else use input or default to development
if [ "${{ github.event_name }}" = "pull_request_target" ] && [ "${{ github.event.pull_request.base.ref }}" = "main" ]; then
STABILITY="release-candidate"
else
STABILITY="${{ inputs.stability || 'development' }}"
fi
@@ -126,9 +111,6 @@ jobs:
--path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
# Ensure licensing tags (updateservers, dlid) if enabled in manifest.xml
php ${MOKO_CLI}/manifest_licensing.php --path . --fix 2>/dev/null || true
# Append suffix for output
if [ -n "$SUFFIX" ]; then
VERSION="${VERSION}${SUFFIX}"
@@ -173,7 +155,7 @@ jobs:
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
--repo "${GITEA_REPO}" --branch dev --prerelease
- name: Update release notes from CHANGELOG.md
run: |
-66
View File
@@ -1,66 +0,0 @@
# 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
+3 -4
View File
@@ -7,8 +7,8 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Validation
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli
# INGROUP: moko-platform.Validation
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# PATH: /templates/workflows/joomla/repo_health.yml.template
# VERSION: 09.23.00
# BRIEF: Enforces repository guardrails by validating scripts governance, tooling availability, and core repository health artifacts.
@@ -33,8 +33,7 @@ on:
- scripts
- repo
pull_request:
branches:
- main
push:
permissions:
contents: read
-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
@@ -1,73 +0,0 @@
# 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}
+18 -60
View File
@@ -1,50 +1,18 @@
# Changelog
## [Unreleased]
## [01.04.01] --- 2026-06-21
<!-- VERSION: 01.00.27 -->
## [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.04.01 -->
All notable changes to MokoSuiteCross will be documented in this file.
All notable changes to MokoJoomCross will be documented in this file.
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
## [Unreleased]
### 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-1 Event pattern**: Fixed Joomla 5 SubscriberInterface incompatibility where `onMokoJoomCrossGetServices` 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
@@ -73,17 +41,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- **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)
- **Category routing rules**: New `#__mokojoomcross_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
- **CrossPostDispatcher**: New static helper (`com_mokojoomcross/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
- **plg_system_mokojoomcross_events**: New source plugin for MokoJoomCalendar — cross-posts calendar events when published
- **plg_system_mokojoomcross_gallery**: New source plugin for MokoJoomGallery — 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
@@ -112,7 +80,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- **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
- **REST API dispatch endpoint**: `POST /api/v1/mokojoomcross/dispatch` — trigger cross-posts for an article via API with optional service filtering, duplicate guard, and template rendering
### Added (original)
@@ -122,8 +90,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- 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
- Custom `mokojoomcross` plugin group for extensible service architecture
- `MokoJoomCrossServiceInterface` 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
@@ -133,7 +101,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- **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
- Joomla Scheduled Task plugin (`plg_task_mokojoomcross`) — 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
@@ -161,7 +129,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
#### Service Plugins (34 platforms)
**Social Media (12)**
- Facebook / Meta — Graph API v19.0, default MokoSuite app mode, page feed posting
- Facebook / Meta — Graph API v19.0, default MokoWaaS 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
@@ -175,9 +143,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- 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
- Telegram — Bot API, default @MokoWaaSBot + custom bot, HTML/Markdown, 4096 chars
- Discord — Webhooks, default MokoWaaS webhook mode, embeds, 2000 chars
- Slack — Incoming Webhooks, default MokoWaaS 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
@@ -228,17 +196,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- Windows Terminal profile in Joomla dropdown
## [01.01.00] - 2026-06-19
## [01.00] - 2026-05-28
### 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
- Initial release
+29 -29
View File
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code when working with this repository.
## Project Overview
**MokoSuiteCross** -- Cross-posting Joomla content to social media, email marketing, and chat platforms
**MokoJoomCross** -- Cross-posting Joomla content to social media, email marketing, and chat platforms
| Field | Value |
|---|---|
@@ -12,7 +12,7 @@ This file provides guidance to Claude Code when working with this repository.
| **Language** | PHP |
| **Default branch** | main |
| **License** | GPL-3.0-or-later |
| **Wiki** | [MokoSuiteCross Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteCross/wiki) |
| **Wiki** | [MokoJoomCross Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoJoomCross/wiki) |
| **Standards** | [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home) |
## Common Commands
@@ -32,60 +32,60 @@ composer install # Install PHP dependencies
## Architecture
This is a Joomla **package** extension (`pkg_mokosuitecross`) containing sub-extensions:
This is a Joomla **package** extension (`pkg_mokojoomcross`) containing sub-extensions:
### com_mokosuitecross (Component)
### com_mokojoomcross (Component)
- Admin backend for managing services, post queue, templates, and logs
- Joomla 5/6 MVC: Dashboard, Services, Posts, Logs (list/edit each)
- Namespace: `Joomla\Component\MokoSuiteCross\Administrator`
- Database tables: `#__mokosuitecross_services`, `#__mokosuitecross_posts`, `#__mokosuitecross_templates`, `#__mokosuitecross_logs`
- Namespace: `Joomla\Component\MokoJoomCross\Administrator`
- Database tables: `#__mokojoomcross_services`, `#__mokojoomcross_posts`, `#__mokojoomcross_templates`, `#__mokojoomcross_logs`
### plg_system_mokosuitecross (System Plugin)
### plg_system_mokojoomcross (System Plugin)
- Hooks `onContentAfterSave` to trigger cross-posting when articles are published
- Dispatches to registered service plugins via the `mokosuitecross` plugin group
- Namespace: `Joomla\Plugin\System\MokoSuiteCross`
- Dispatches to registered service plugins via the `mokojoomcross` plugin group
- Namespace: `Joomla\Plugin\System\MokoJoomCross`
### plg_content_mokosuitecross (Content Plugin)
### plg_content_mokojoomcross (Content Plugin)
- Hooks `onContentBeforeDisplay` to add cross-post status badges to articles
- Namespace: `Joomla\Plugin\Content\MokoSuiteCross`
- Namespace: `Joomla\Plugin\Content\MokoJoomCross`
### plg_webservices_mokosuitecross (WebServices Plugin)
### plg_webservices_mokojoomcross (WebServices Plugin)
- REST API endpoints for posts and services
- Namespace: `Joomla\Plugin\WebServices\MokoSuiteCross`
- Namespace: `Joomla\Plugin\WebServices\MokoJoomCross`
### Service Plugins (mokosuitecross group)
Each platform is a separate plugin in the custom `mokosuitecross` plugin group:
- `plg_mokosuitecross_facebook` — Facebook/Meta Graph API
- `plg_mokosuitecross_twitter` — X/Twitter API v2
- `plg_mokosuitecross_linkedin` — LinkedIn Share API
- `plg_mokosuitecross_mastodon` — Mastodon API
- `plg_mokosuitecross_bluesky` — Bluesky AT Protocol
- `plg_mokosuitecross_mailchimp` — Mailchimp Campaigns API
- `plg_mokosuitecross_telegram` — Telegram Bot API (default @mokosuite_bot + custom bot)
- `plg_mokosuitecross_discord` — Discord Webhooks
- `plg_mokosuitecross_slack` — Slack Incoming Webhooks
### Service Plugins (mokojoomcross group)
Each platform is a separate plugin in the custom `mokojoomcross` plugin group:
- `plg_mokojoomcross_facebook` — Facebook/Meta Graph API
- `plg_mokojoomcross_twitter` — X/Twitter API v2
- `plg_mokojoomcross_linkedin` — LinkedIn Share API
- `plg_mokojoomcross_mastodon` — Mastodon API
- `plg_mokojoomcross_bluesky` — Bluesky AT Protocol
- `plg_mokojoomcross_mailchimp` — Mailchimp Campaigns API
- `plg_mokojoomcross_telegram` — Telegram Bot API (default @MokoWaaSBot + custom bot)
- `plg_mokojoomcross_discord` — Discord Webhooks
- `plg_mokojoomcross_slack` — Slack Incoming Webhooks
### Database Schema
Four tables:
`#__mokosuitecross_services`:
`#__mokojoomcross_services`:
- `id`, `title`, `alias`, `service_type` (facebook, twitter, etc.)
- `credentials` (JSON encrypted), `params` (JSON)
- `published`, `ordering`, `created`, `modified`, `created_by`
`#__mokosuitecross_posts`:
`#__mokojoomcross_posts`:
- `id`, `article_id` (FK to #__content), `service_id` (FK)
- `status` (queued/posting/posted/failed/scheduled)
- `message`, `platform_post_id`, `platform_response` (JSON)
- `scheduled_at`, `posted_at`, `retry_count`
- `created`, `modified`
`#__mokosuitecross_templates`:
`#__mokojoomcross_templates`:
- `id`, `service_type`, `title`, `template_body`
- `published`, `ordering`, `created`, `modified`
`#__mokosuitecross_logs`:
`#__mokojoomcross_logs`:
- `id`, `post_id` (FK), `service_id` (FK)
- `level` (info/warning/error), `message`, `context` (JSON)
- `created`
@@ -109,4 +109,4 @@ Four tables:
- `bind() → check() → store()` for Table operations (not `save()`)
- Language file placement: site (no `folder`) vs admin (`folder="administrator"`)
- SPDX license headers on all PHP files
- Service plugins MUST implement `MokoSuiteCrossServiceInterface`
- Service plugins MUST implement `MokoJoomCrossServiceInterface`
+161 -161
View File
@@ -1,161 +1,161 @@
# Contributing to Moko Consulting Projects
Thank you for your interest in contributing. All Moko Consulting repositories follow this universal workflow and version policy.
## Branching Workflow
```
feature/* ──PR──> dev ──draft PR──> (renamed to rc) ──merge──> main
```
### Step by step
1. **Create a feature branch** from `dev`:
```bash
git checkout dev && git pull
git checkout -b feature/my-change
```
2. **Work and commit** on your feature branch. Push to origin.
3. **Open a PR**: `feature/my-change` → `dev`. After review and checks, merge it.
4. **When ready for release**, open a **draft PR**: `dev` → `main`.
- This automatically renames the source branch to `rc` (release candidate)
- An RC pre-release is built and uploaded
5. **Alpha and beta branches** are created by manually renaming the branch before the RC stage:
- Rename `dev` to `alpha` for early testing → alpha pre-release is built
- Rename `alpha` to `beta` for feature-complete testing → beta pre-release is built
- When the draft PR is created, the branch is renamed to `rc`
6. **Once PR checks pass** on the `rc` branch, mark the PR as ready and merge to `main`.
7. **Merging to main** triggers the stable release pipeline:
- Minor version bump (e.g., `02.09.xx` → `02.10.00`)
- Stability suffix stripped (clean version)
- Gitea release created with ZIP/tar.gz packages
- `updates.xml` updated (Joomla extensions)
- `dev` branch recreated from `main`
### Branch summary
| Branch | Purpose | Created by |
|--------|---------|-----------|
| `feature/*` | New features and fixes | Developer |
| `dev` | Integration branch | Auto-recreated after release |
| `alpha` | Alpha pre-release testing | Manual rename from `dev` |
| `beta` | Beta pre-release testing | Manual rename from `alpha` |
| `rc` | Release candidate | Auto-renamed on draft PR to main |
| `main` | Stable releases | Protected, merge only |
| `version/XX.YY.ZZ` | Archived release snapshots | Auto-created by CI |
### Protected branches
| Branch | Direct push | Merge via |
|--------|------------|-----------|
| `main` | Blocked (CI bot whitelisted) | PR merge only |
| `dev` | Blocked (CI bot whitelisted) | PR merge from feature/* |
| `rc` | Blocked (CI bot whitelisted) | Auto-created on draft PR |
| `alpha` | Blocked (CI bot whitelisted) | Manual rename |
| `beta` | Blocked (CI bot whitelisted) | Manual rename |
| `feature/*` | Open | N/A (source branch) |
## Version Policy
### Format
All versions use `XX.YY.ZZ` — three two-digit segments, zero-padded:
- **XX** — Major version (breaking changes)
- **YY** — Minor version (new features, bumped on release to main)
- **ZZ** — Patch version (auto-incremented on every push to dev/feature branches)
Rollover: patch `99` → `00` increments minor; minor `99` → `00` increments major.
### Stability suffixes
Each branch appends a suffix to indicate stability:
| Branch | Suffix | Example |
|--------|--------|---------|
| `main` | (none) | `02.09.00` |
| `dev` | `-dev` | `02.09.01-dev` |
| `feature/*` | `-dev` | `02.09.01-dev` |
| `alpha` | `-alpha` | `02.09.01-alpha` |
| `beta` | `-beta` | `02.09.01-beta` |
| `rc` | `-rc` | `02.09.01-rc` |
### Auto version bump
On every push to `dev`, `feature/*`, or `patch/*`:
1. Patch version incremented
2. Stability suffix `-dev` applied
3. All version-bearing files updated (manifests, CHANGELOG, PHP headers, etc.)
4. Commit created with `[skip ci]` to avoid loops
### Release version flow
Version bumps happen at specific release events:
| Event | Bump | Example |
|-------|------|---------|
| Feature merged to dev | Patch bump after dev release | `02.09.01-dev` → release → `02.09.02-dev` |
| Dev promoted to RC | Minor bump | `02.09.02-dev` → `02.10.00-rc` |
| RC merged to main | Minor bump | `02.10.00-rc` → `02.11.00` (stable) |
| Dev recreated from main | Patch bump | `02.11.00` → `02.11.01-dev` |
### Release stream copies
When a higher-stability release is published, copies are created for all lesser streams with the same base version:
- **RC `02.10.00-rc`** also creates: `02.10.00-dev`, `02.10.00-alpha`, `02.10.00-beta`
- **Stable `02.11.00`** also creates: `02.11.00-dev`, `02.11.00-alpha`, `02.11.00-beta`, `02.11.00-rc`
This ensures Joomla sites on ANY stability channel see the update (Joomla only shows versions higher than what's installed).
### Version files
The version tools update all files containing version stamps:
- `.mokogitea/manifest.xml` (canonical source)
- Joomla XML manifests (`<version>` tag)
- `README.md`, `CHANGELOG.md` (`VERSION:` pattern)
- `package.json`, `pyproject.toml`
- Any text file with a `VERSION: XX.YY.ZZ` label
Files synced from other repos (with a `# REPO:` header) are not touched.
## Code Standards
- **PHP**: PSR-12, tabs for indentation
- **Copyright**: all files must include the Moko Consulting copyright header
- **License**: SPDX identifier `GPL-3.0-or-later` (or as specified per repo)
- **Attribution**: use `Authored-by: Moko Consulting` in commits, not individual names
## Commit Messages
Use conventional commit format:
```
type(scope): short description
Optional body with context.
Authored-by: Moko Consulting
```
Types: `feat`, `fix`, `chore`, `docs`, `style`, `refactor`, `test`, `ci`
Special flags in commit messages:
- `[skip ci]` — skip all CI workflows
- `[skip bump]` — skip auto version bump only
## Reporting Issues
Use the repository's issue tracker with the appropriate template.
---
*Moko Consulting <hello@mokoconsulting.tech>*
# Contributing to Moko Consulting Projects
Thank you for your interest in contributing. All Moko Consulting repositories follow this universal workflow and version policy.
## Branching Workflow
```
feature/* ──PR──> dev ──draft PR──> (renamed to rc) ──merge──> main
```
### Step by step
1. **Create a feature branch** from `dev`:
```bash
git checkout dev && git pull
git checkout -b feature/my-change
```
2. **Work and commit** on your feature branch. Push to origin.
3. **Open a PR**: `feature/my-change` → `dev`. After review and checks, merge it.
4. **When ready for release**, open a **draft PR**: `dev` → `main`.
- This automatically renames the source branch to `rc` (release candidate)
- An RC pre-release is built and uploaded
5. **Alpha and beta branches** are created by manually renaming the branch before the RC stage:
- Rename `dev` to `alpha` for early testing → alpha pre-release is built
- Rename `alpha` to `beta` for feature-complete testing → beta pre-release is built
- When the draft PR is created, the branch is renamed to `rc`
6. **Once PR checks pass** on the `rc` branch, mark the PR as ready and merge to `main`.
7. **Merging to main** triggers the stable release pipeline:
- Minor version bump (e.g., `02.09.xx` → `02.10.00`)
- Stability suffix stripped (clean version)
- Gitea release created with ZIP/tar.gz packages
- `updates.xml` updated (Joomla extensions)
- `dev` branch recreated from `main`
### Branch summary
| Branch | Purpose | Created by |
|--------|---------|-----------|
| `feature/*` | New features and fixes | Developer |
| `dev` | Integration branch | Auto-recreated after release |
| `alpha` | Alpha pre-release testing | Manual rename from `dev` |
| `beta` | Beta pre-release testing | Manual rename from `alpha` |
| `rc` | Release candidate | Auto-renamed on draft PR to main |
| `main` | Stable releases | Protected, merge only |
| `version/XX.YY.ZZ` | Archived release snapshots | Auto-created by CI |
### Protected branches
| Branch | Direct push | Merge via |
|--------|------------|-----------|
| `main` | Blocked (CI bot whitelisted) | PR merge only |
| `dev` | Blocked (CI bot whitelisted) | PR merge from feature/* |
| `rc` | Blocked (CI bot whitelisted) | Auto-created on draft PR |
| `alpha` | Blocked (CI bot whitelisted) | Manual rename |
| `beta` | Blocked (CI bot whitelisted) | Manual rename |
| `feature/*` | Open | N/A (source branch) |
## Version Policy
### Format
All versions use `XX.YY.ZZ` — three two-digit segments, zero-padded:
- **XX** — Major version (breaking changes)
- **YY** — Minor version (new features, bumped on release to main)
- **ZZ** — Patch version (auto-incremented on every push to dev/feature branches)
Rollover: patch `99` → `00` increments minor; minor `99` → `00` increments major.
### Stability suffixes
Each branch appends a suffix to indicate stability:
| Branch | Suffix | Example |
|--------|--------|---------|
| `main` | (none) | `02.09.00` |
| `dev` | `-dev` | `02.09.01-dev` |
| `feature/*` | `-dev` | `02.09.01-dev` |
| `alpha` | `-alpha` | `02.09.01-alpha` |
| `beta` | `-beta` | `02.09.01-beta` |
| `rc` | `-rc` | `02.09.01-rc` |
### Auto version bump
On every push to `dev`, `feature/*`, or `patch/*`:
1. Patch version incremented
2. Stability suffix `-dev` applied
3. All version-bearing files updated (manifests, CHANGELOG, PHP headers, etc.)
4. Commit created with `[skip ci]` to avoid loops
### Release version flow
Version bumps happen at specific release events:
| Event | Bump | Example |
|-------|------|---------|
| Feature merged to dev | Patch bump after dev release | `02.09.01-dev` → release → `02.09.02-dev` |
| Dev promoted to RC | Minor bump | `02.09.02-dev` → `02.10.00-rc` |
| RC merged to main | Minor bump | `02.10.00-rc` → `02.11.00` (stable) |
| Dev recreated from main | Patch bump | `02.11.00` → `02.11.01-dev` |
### Release stream copies
When a higher-stability release is published, copies are created for all lesser streams with the same base version:
- **RC `02.10.00-rc`** also creates: `02.10.00-dev`, `02.10.00-alpha`, `02.10.00-beta`
- **Stable `02.11.00`** also creates: `02.11.00-dev`, `02.11.00-alpha`, `02.11.00-beta`, `02.11.00-rc`
This ensures Joomla sites on ANY stability channel see the update (Joomla only shows versions higher than what's installed).
### Version files
The version tools update all files containing version stamps:
- `.mokogitea/manifest.xml` (canonical source)
- Joomla XML manifests (`<version>` tag)
- `README.md`, `CHANGELOG.md` (`VERSION:` pattern)
- `package.json`, `pyproject.toml`
- Any text file with a `VERSION: XX.YY.ZZ` label
Files synced from other repos (with a `# REPO:` header) are not touched.
## Code Standards
- **PHP**: PSR-12, tabs for indentation
- **Copyright**: all files must include the Moko Consulting copyright header
- **License**: SPDX identifier `GPL-3.0-or-later` (or as specified per repo)
- **Attribution**: use `Authored-by: Moko Consulting` in commits, not individual names
## Commit Messages
Use conventional commit format:
```
type(scope): short description
Optional body with context.
Authored-by: Moko Consulting
```
Types: `feat`, `fix`, `chore`, `docs`, `style`, `refactor`, `test`, `ci`
Special flags in commit messages:
- `[skip ci]` — skip all CI workflows
- `[skip bump]` — skip auto version bump only
## Reporting Issues
Use the repository's issue tracker with the appropriate template.
---
*Moko Consulting <hello@mokoconsulting.tech>*
+2 -2
View File
@@ -2,14 +2,14 @@
# 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
# MokoJoomCross — Cross-posting Joomla content to social media, email marketing, and chat platforms
# ==============================================================================
# CONFIGURATION - Customize these for your extension
# ==============================================================================
# Extension Configuration
EXTENSION_NAME := mokosuitecross
EXTENSION_NAME := mokojoomcross
EXTENSION_TYPE := package
# Options: module, plugin, component, package, template
EXTENSION_VERSION := 1.0.0
+19 -68
View File
@@ -1,99 +1,50 @@
# MokoSuiteCross
# MokoJoomCross
<!-- VERSION: 01.04.01 -->
<!-- VERSION: 01.00.27 -->
Cross-posting Joomla content to social media, email marketing, and chat platforms for Joomla 5/6.
## Overview
MokoSuiteCross automatically publishes your Joomla articles to multiple platforms when you hit publish. Connect your social media accounts, email marketing tools, and chat channels — then cross-post with one click. Each platform is a separate plugin, so you only install what you need and third-party developers can add new services.
MokoJoomCross automatically publishes your Joomla articles to multiple platforms when you hit publish. Connect your social media accounts, email marketing tools, and chat channels — then cross-post with one click. Each platform is a separate plugin, so you only install what you need and third-party developers can add new services.
## Features
- **One-click cross-posting** — Publish to all connected platforms when an article goes live
- **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
- **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}, {intro}, {image})
- **Post history** — Track what was posted where, with platform response data
- **Evergreen re-sharing** — Automatically re-share articles on a configurable interval
- **Category routing** — Route articles to specific services by Joomla category
- **Migration** — Import settings from Perfect Publisher Pro
- **REST API** — WebServices plugin for headless/external integration
### Supported Platforms (36)
### Supported Platforms
#### Social Media
| Platform | Plugin | Status |
|----------|--------|--------|
| Facebook / Meta | `plg_mokosuitecross_facebook` | Implemented |
| X / Twitter | `plg_mokosuitecross_twitter` | Implemented |
| LinkedIn | `plg_mokosuitecross_linkedin` | Implemented |
| Mastodon | `plg_mokosuitecross_mastodon` | Implemented |
| Bluesky | `plg_mokosuitecross_bluesky` | Implemented |
| Threads | `plg_mokosuitecross_threads` | Implemented |
| Pinterest | `plg_mokosuitecross_pinterest` | Implemented |
| Reddit | `plg_mokosuitecross_reddit` | Implemented |
| TikTok | `plg_mokosuitecross_tiktok` | Implemented |
| Tumblr | `plg_mokosuitecross_tumblr` | Implemented |
#### Email Marketing
| Platform | Plugin | Status |
|----------|--------|--------|
| Mailchimp | `plg_mokosuitecross_mailchimp` | Implemented |
| SendGrid | `plg_mokosuitecross_sendgrid` | Implemented |
| Brevo | `plg_mokosuitecross_brevo` | Implemented |
| Constant Contact | `plg_mokosuitecross_constantcontact` | Implemented |
| ConvertKit | `plg_mokosuitecross_convertkit` | Implemented |
#### Chat / Messaging
| Platform | Plugin | Status |
|----------|--------|--------|
| Telegram | `plg_mokosuitecross_telegram` | Implemented |
| Discord | `plg_mokosuitecross_discord` | Implemented |
| Slack | `plg_mokosuitecross_slack` | Implemented |
| Microsoft Teams | `plg_mokosuitecross_teams` | Implemented |
| WhatsApp | `plg_mokosuitecross_whatsapp` | Implemented |
| Google Chat | `plg_mokosuitecross_googlechat` | Implemented |
| Matrix | `plg_mokosuitecross_matrix` | Implemented |
| Ntfy | `plg_mokosuitecross_ntfy` | Implemented |
#### Publishing Platforms
| Platform | Plugin | Status |
|----------|--------|--------|
| WordPress | `plg_mokosuitecross_wordpress` | Implemented |
| Medium | `plg_mokosuitecross_medium` | Implemented |
| Dev.to | `plg_mokosuitecross_devto` | Implemented |
| Ghost | `plg_mokosuitecross_ghost` | Implemented |
| Hashnode | `plg_mokosuitecross_hashnode` | Implemented |
| Blogger | `plg_mokosuitecross_blogger` | Implemented |
#### Other
| Platform | Plugin | Status |
|----------|--------|--------|
| Webhook | `plg_mokosuitecross_webhook` | Implemented |
| RSS Feed | `plg_mokosuitecross_rssfeed` | Implemented |
| ActivityPub | `plg_mokosuitecross_activitypub` | Implemented |
| Google Business | `plg_mokosuitecross_googlebusiness` | Implemented |
| Nostr | `plg_mokosuitecross_nostr` | Stub (WebSocket deferred) |
| Facebook / Meta | `plg_mokojoomcross_facebook` | Planned |
| X / Twitter | `plg_mokojoomcross_twitter` | Planned |
| LinkedIn | `plg_mokojoomcross_linkedin` | Planned |
| Mastodon | `plg_mokojoomcross_mastodon` | Planned |
| Bluesky | `plg_mokojoomcross_bluesky` | Planned |
| Mailchimp | `plg_mokojoomcross_mailchimp` | Planned |
| Telegram | `plg_mokojoomcross_telegram` | Planned |
| Discord | `plg_mokojoomcross_discord` | Planned |
| Slack | `plg_mokojoomcross_slack` | Planned |
## Installation
1. Download the latest `pkg_mokosuitecross-*.zip` from [Releases](https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteCross/releases)
1. Download the latest `pkg_mokojoomcross-*.zip` from [Releases](https://git.mokoconsulting.tech/MokoConsulting/MokoJoomCross/releases)
2. In Joomla Administrator → Extensions → Install → Upload Package File
3. System and content plugins are enabled automatically on install
4. Navigate to Components → MokoSuiteCross to connect your first service
## Documentation
See the [Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteCross/wiki) for full documentation.
4. Navigate to Components → MokoJoomCross to connect your first service
## Migrating from Perfect Publisher Pro
MokoSuiteCross includes a built-in migration tool:
MokoJoomCross includes a built-in migration tool:
1. Install MokoSuiteCross (Perfect Publisher Pro can remain installed)
2. Navigate to Components → MokoSuiteCross → Dashboard
1. Install MokoJoomCross (Perfect Publisher Pro can remain installed)
2. Navigate to Components → MokoJoomCross → Dashboard
3. Click "Migrate from Perfect Publisher Pro"
4. Review detected services and confirm import
+237
View File
@@ -0,0 +1,237 @@
#!/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
+1 -1
View File
@@ -1,5 +1,5 @@
{
"name": "mokoconsulting/mokosuitecross",
"name": "mokoconsulting/mokojoomcross",
"description": "Cross-posting Joomla content to social media, email marketing, and chat platforms",
"type": "joomla-package",
"version": "01.00.00",
@@ -1,8 +0,0 @@
; MokoSuiteCross - Package System Language File
; Copyright (C) 2026 Moko Consulting. All rights reserved.
; License: GPL-3.0-or-later
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_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."
@@ -1,146 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<config>
<fieldset name="component" label="COM_MOKOSUITECROSS_CONFIG_COMPONENT">
<field
name="auto_post_on_publish"
type="radio"
label="COM_MOKOSUITECROSS_CONFIG_AUTO_POST"
description="COM_MOKOSUITECROSS_CONFIG_AUTO_POST_DESC"
default="1"
class="btn-group">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field
name="post_on_first_publish_only"
type="radio"
label="COM_MOKOSUITECROSS_CONFIG_FIRST_PUBLISH_ONLY"
description="COM_MOKOSUITECROSS_CONFIG_FIRST_PUBLISH_ONLY_DESC"
default="0"
class="btn-group"
showon="auto_post_on_publish:1">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field
name="retry_max"
type="number"
label="COM_MOKOSUITECROSS_CONFIG_RETRY_MAX"
description="COM_MOKOSUITECROSS_CONFIG_RETRY_MAX_DESC"
default="3"
min="0"
max="10"
/>
<field
name="retry_delay"
type="number"
label="COM_MOKOSUITECROSS_CONFIG_RETRY_DELAY"
description="COM_MOKOSUITECROSS_CONFIG_RETRY_DELAY_DESC"
default="300"
min="60"
max="3600"
/>
<field
name="log_retention_days"
type="number"
label="COM_MOKOSUITECROSS_CONFIG_LOG_RETENTION"
description="COM_MOKOSUITECROSS_CONFIG_LOG_RETENTION_DESC"
default="90"
min="7"
max="365"
/>
<field
name="default_template"
type="textarea"
label="COM_MOKOSUITECROSS_CONFIG_DEFAULT_TEMPLATE"
description="COM_MOKOSUITECROSS_CONFIG_DEFAULT_TEMPLATE_DESC"
default="{title}\n\n{introtext}\n\n{url}"
rows="4"
/>
</fieldset>
<fieldset name="evergreen" label="COM_MOKOSUITECROSS_CONFIG_EVERGREEN">
<field
name="evergreen_enabled"
type="radio"
label="COM_MOKOSUITECROSS_CONFIG_EVERGREEN_ENABLED"
description="COM_MOKOSUITECROSS_CONFIG_EVERGREEN_ENABLED_DESC"
default="1"
class="btn-group">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field
name="evergreen_default_interval"
type="number"
label="COM_MOKOSUITECROSS_CONFIG_EVERGREEN_DEFAULT_INTERVAL"
description="COM_MOKOSUITECROSS_CONFIG_EVERGREEN_DEFAULT_INTERVAL_DESC"
default="30"
min="1"
max="365"
showon="evergreen_enabled:1"
/>
<field
name="evergreen_max_per_run"
type="number"
label="COM_MOKOSUITECROSS_CONFIG_EVERGREEN_MAX_PER_RUN"
description="COM_MOKOSUITECROSS_CONFIG_EVERGREEN_MAX_PER_RUN_DESC"
default="3"
min="1"
max="20"
showon="evergreen_enabled:1"
/>
</fieldset>
<fieldset name="queue" label="COM_MOKOSUITECROSS_CONFIG_QUEUE">
<field
name="queue_processing"
type="list"
label="COM_MOKOSUITECROSS_CONFIG_QUEUE_PROCESSING"
description="COM_MOKOSUITECROSS_CONFIG_QUEUE_PROCESSING_DESC"
default="scheduler">
<option value="scheduler">COM_MOKOSUITECROSS_CONFIG_QUEUE_SCHEDULER</option>
<option value="pageload">COM_MOKOSUITECROSS_CONFIG_QUEUE_PAGELOAD</option>
<option value="both">COM_MOKOSUITECROSS_CONFIG_QUEUE_BOTH</option>
</field>
<field
name="pageload_client"
type="list"
label="COM_MOKOSUITECROSS_CONFIG_PAGELOAD_CLIENT"
description="COM_MOKOSUITECROSS_CONFIG_PAGELOAD_CLIENT_DESC"
default="both"
showon="queue_processing:pageload,both">
<option value="both">COM_MOKOSUITECROSS_CONFIG_PAGELOAD_BOTH</option>
<option value="admin">COM_MOKOSUITECROSS_CONFIG_PAGELOAD_ADMIN</option>
<option value="site">COM_MOKOSUITECROSS_CONFIG_PAGELOAD_SITE</option>
</field>
<field
name="pageload_interval"
type="number"
label="COM_MOKOSUITECROSS_CONFIG_PAGELOAD_INTERVAL"
description="COM_MOKOSUITECROSS_CONFIG_PAGELOAD_INTERVAL_DESC"
default="300"
min="60"
max="3600"
showon="queue_processing:pageload,both"
/>
</fieldset>
<fieldset name="category_rules" label="COM_MOKOSUITECROSS_CONFIG_CATEGORY_RULES">
<field
name="category_rules_note"
type="note"
label="COM_MOKOSUITECROSS_CONFIG_CATEGORY_RULES_NOTE"
description="COM_MOKOSUITECROSS_CONFIG_CATEGORY_RULES_NOTE_DESC"
/>
</fieldset>
</config>
@@ -1,513 +0,0 @@
; MokoSuiteCross — Admin Backend Language File
; Copyright (C) 2026 Moko Consulting. All rights reserved.
; License: GPL-3.0-or-later
COM_MOKOSUITECROSS="MokoSuiteCross"
COM_MOKOSUITECROSS_DESCRIPTION="Cross-posting Joomla content to social media, email marketing, and chat platforms"
; Submenu
COM_MOKOSUITECROSS_SUBMENU_DASHBOARD="Dashboard"
COM_MOKOSUITECROSS_SUBMENU_POSTS="Post Queue"
COM_MOKOSUITECROSS_SUBMENU_SERVICES="Services"
COM_MOKOSUITECROSS_SUBMENU_LOGS="Activity Logs"
; Dashboard
COM_MOKOSUITECROSS_DASHBOARD_ACTIVE_SERVICES="Active Services"
COM_MOKOSUITECROSS_DASHBOARD_QUEUED="Queued"
COM_MOKOSUITECROSS_DASHBOARD_POSTED="Posted"
COM_MOKOSUITECROSS_DASHBOARD_FAILED="Failed"
COM_MOKOSUITECROSS_DASHBOARD_QUICK_LINKS="Quick Links"
; Migration
COM_MOKOSUITECROSS_MIGRATION_TITLE="Migrate from Perfect Publisher Pro"
COM_MOKOSUITECROSS_MIGRATION_DESCRIPTION="We detected Perfect Publisher Pro settings. Import your service configurations to MokoSuiteCross."
COM_MOKOSUITECROSS_MIGRATION_BUTTON="Start Migration"
COM_MOKOSUITECROSS_MIGRATION_SUCCESS="Migration complete: %d service(s) imported, %d skipped."
COM_MOKOSUITECROSS_MIGRATION_ERROR="Migration encountered errors: %s"
; Services
COM_MOKOSUITECROSS_FIELD_SERVICE_TYPE="Service Type"
COM_MOKOSUITECROSS_SELECT_SERVICE_TYPE="- Select Service Type -"
COM_MOKOSUITECROSS_FIELDSET_CREDENTIALS="API Credentials"
COM_MOKOSUITECROSS_FIELD_CREDENTIALS="Credentials (JSON)"
COM_MOKOSUITECROSS_FIELD_CREDENTIALS_DESC="JSON object with API keys and tokens for this service. Keys vary by platform."
; Posts
COM_MOKOSUITECROSS_FILTER_SEARCH="Search"
COM_MOKOSUITECROSS_FILTER_STATUS="Status"
COM_MOKOSUITECROSS_SELECT_STATUS="- Select Status -"
COM_MOKOSUITECROSS_FILTER_SERVICE_TYPE="Service Type"
COM_MOKOSUITECROSS_CREATED_ASC="Created ascending"
COM_MOKOSUITECROSS_CREATED_DESC="Created descending"
COM_MOKOSUITECROSS_STATUS_ASC="Status ascending"
COM_MOKOSUITECROSS_STATUS_DESC="Status descending"
; Actions
COM_MOKOSUITECROSS_ACTION_CROSSPOST="Cross-post"
COM_MOKOSUITECROSS_ACTION_MIGRATE="Migrate"
; Configuration
COM_MOKOSUITECROSS_CONFIG_COMPONENT="MokoSuiteCross Settings"
COM_MOKOSUITECROSS_CONFIG_AUTO_POST="Auto-post on Publish"
COM_MOKOSUITECROSS_CONFIG_AUTO_POST_DESC="Automatically cross-post articles when they are published"
COM_MOKOSUITECROSS_CONFIG_RETRY_MAX="Max Retries"
COM_MOKOSUITECROSS_CONFIG_RETRY_MAX_DESC="Maximum number of retry attempts for failed posts"
COM_MOKOSUITECROSS_CONFIG_RETRY_DELAY="Retry Delay (seconds)"
COM_MOKOSUITECROSS_CONFIG_RETRY_DELAY_DESC="Seconds to wait before retrying a failed post"
COM_MOKOSUITECROSS_CONFIG_LOG_RETENTION="Log Retention (days)"
COM_MOKOSUITECROSS_CONFIG_LOG_RETENTION_DESC="Number of days to keep activity logs"
COM_MOKOSUITECROSS_CONFIG_DEFAULT_TEMPLATE="Default Message Template"
COM_MOKOSUITECROSS_CONFIG_DEFAULT_TEMPLATE_DESC="Default template for cross-posts. Placeholders: {title}, {url}, {introtext}, {image}, {category}, {author}"
; Table headings
COM_MOKOSUITECROSS_HEADING_STATUS="Status"
COM_MOKOSUITECROSS_HEADING_ARTICLE="Article"
COM_MOKOSUITECROSS_HEADING_SERVICE="Service"
COM_MOKOSUITECROSS_HEADING_MESSAGE="Message"
COM_MOKOSUITECROSS_HEADING_POSTED_AT="Posted"
COM_MOKOSUITECROSS_HEADING_CREATED="Created"
COM_MOKOSUITECROSS_HEADING_LEVEL="Level"
COM_MOKOSUITECROSS_HEADING_MODE="Mode"
; Dashboard
COM_MOKOSUITECROSS_DASHBOARD_RECENT_ACTIVITY="Recent Activity"
COM_MOKOSUITECROSS_DASHBOARD_NO_RECENT="No recent activity."
COM_MOKOSUITECROSS_DASHBOARD_TOTAL_POSTS="Total Posts"
COM_MOKOSUITECROSS_DASHBOARD_PAGELOAD_WARNING_TITLE="Page-load queue processing is active"
COM_MOKOSUITECROSS_DASHBOARD_PAGELOAD_WARNING="You are using page-load processing for the cross-post queue. This is a fallback method and may be unreliable on low-traffic sites. For production use, switch to Joomla Scheduled Tasks: create a task of type <strong>MokoSuiteCross - Process Queue</strong> in System → Scheduled Tasks, then set queue processing to <strong>Scheduler only</strong> in component options."
; Evergreen Configuration
COM_MOKOSUITECROSS_CONFIG_EVERGREEN="Evergreen Re-sharing"
COM_MOKOSUITECROSS_CONFIG_EVERGREEN_ENABLED="Enable Evergreen"
COM_MOKOSUITECROSS_CONFIG_EVERGREEN_ENABLED_DESC="Allow articles marked as evergreen to be automatically re-shared on a recurring schedule."
COM_MOKOSUITECROSS_CONFIG_EVERGREEN_DEFAULT_INTERVAL="Default Interval (days)"
COM_MOKOSUITECROSS_CONFIG_EVERGREEN_DEFAULT_INTERVAL_DESC="Default number of days between re-shares when no per-article interval is set."
COM_MOKOSUITECROSS_CONFIG_EVERGREEN_MAX_PER_RUN="Max Re-shares Per Run"
COM_MOKOSUITECROSS_CONFIG_EVERGREEN_MAX_PER_RUN_DESC="Maximum number of evergreen articles to re-share in a single queue processing run. Prevents flooding platforms."
; Queue Processing Configuration
COM_MOKOSUITECROSS_CONFIG_QUEUE="Queue Processing"
COM_MOKOSUITECROSS_CONFIG_QUEUE_PROCESSING="Processing Method"
COM_MOKOSUITECROSS_CONFIG_QUEUE_PROCESSING_DESC="How queued posts, retries, and scheduled posts are processed. Scheduler (recommended) uses Joomla's built-in Task Scheduler. Page-load piggybacks on page requests."
COM_MOKOSUITECROSS_CONFIG_QUEUE_SCHEDULER="Scheduler only (recommended)"
COM_MOKOSUITECROSS_CONFIG_QUEUE_PAGELOAD="Page-load only (fallback)"
COM_MOKOSUITECROSS_CONFIG_QUEUE_BOTH="Both (scheduler + page-load)"
COM_MOKOSUITECROSS_CONFIG_PAGELOAD_CLIENT="Page-load Client"
COM_MOKOSUITECROSS_CONFIG_PAGELOAD_CLIENT_DESC="Which Joomla application triggers page-load processing."
COM_MOKOSUITECROSS_CONFIG_PAGELOAD_BOTH="Backend and Frontend"
COM_MOKOSUITECROSS_CONFIG_PAGELOAD_ADMIN="Backend only"
COM_MOKOSUITECROSS_CONFIG_PAGELOAD_SITE="Frontend only"
COM_MOKOSUITECROSS_CONFIG_PAGELOAD_INTERVAL="Page-load Interval (seconds)"
COM_MOKOSUITECROSS_CONFIG_PAGELOAD_INTERVAL_DESC="Minimum seconds between page-load queue runs. Lower = more responsive but more DB queries per page load."
; Submenu (extended)
COM_MOKOSUITECROSS_SUBMENU_TEMPLATES="Templates"
; Template Management
COM_MOKOSUITECROSS_TEMPLATE_BODY="Template Body"
COM_MOKOSUITECROSS_TEMPLATE_BODY_DESC="Message template with placeholders. Use the reference panel on the right for available placeholders."
COM_MOKOSUITECROSS_TEMPLATE_SERVICE_TYPE_DESC="Which platform this template is for. 'Default' is the fallback when no platform-specific template exists."
COM_MOKOSUITECROSS_TEMPLATE_TYPE_DEFAULT="Default (fallback)"
COM_MOKOSUITECROSS_TEMPLATE_PREVIEW="Preview"
COM_MOKOSUITECROSS_TEMPLATE_PLACEHOLDERS="Available Placeholders"
; Placeholders
COM_MOKOSUITECROSS_PLACEHOLDER_TITLE="Article title"
COM_MOKOSUITECROSS_PLACEHOLDER_URL="Article URL"
COM_MOKOSUITECROSS_PLACEHOLDER_INTROTEXT="Intro text (280 chars, no HTML)"
COM_MOKOSUITECROSS_PLACEHOLDER_FULLTEXT="Full text (500 chars, no HTML)"
COM_MOKOSUITECROSS_PLACEHOLDER_IMAGE="Intro image URL"
COM_MOKOSUITECROSS_PLACEHOLDER_CATEGORY="Category name"
COM_MOKOSUITECROSS_PLACEHOLDER_AUTHOR="Author name"
COM_MOKOSUITECROSS_PLACEHOLDER_DATE="Publish date (YYYY-MM-DD)"
; Logs
COM_MOKOSUITECROSS_FILTER_LEVEL="Level"
COM_MOKOSUITECROSS_SELECT_LEVEL="- Select Level -"
COM_MOKOSUITECROSS_LEVEL_ASC="Level ascending"
COM_MOKOSUITECROSS_LEVEL_DESC="Level descending"
; Analytics Dashboard
COM_MOKOSUITECROSS_DASHBOARD_SERVICE_BREAKDOWN="Posts by Service"
COM_MOKOSUITECROSS_DASHBOARD_TOP_ARTICLES="Most Cross-Posted Articles"
COM_MOKOSUITECROSS_DASHBOARD_SUCCESS_RATE="Success Rate"
; OAuth
COM_MOKOSUITECROSS_OAUTH_NO_SERVICE="No service specified for OAuth authorization."
COM_MOKOSUITECROSS_OAUTH_SERVICE_NOT_FOUND="Service not found."
COM_MOKOSUITECROSS_OAUTH_NO_CLIENT_ID="No OAuth Client ID configured for %s. Set it in Extensions → Plugins → MokoSuiteCross - %s."
COM_MOKOSUITECROSS_OAUTH_NOT_SUPPORTED="OAuth is not supported for %s."
COM_MOKOSUITECROSS_OAUTH_PLATFORM_ERROR="Platform returned error: %s"
COM_MOKOSUITECROSS_OAUTH_INVALID_CALLBACK="Invalid OAuth callback — missing code or state."
COM_MOKOSUITECROSS_OAUTH_INVALID_STATE="Invalid OAuth state parameter."
COM_MOKOSUITECROSS_OAUTH_TOKEN_ERROR="Token exchange failed: %s"
COM_MOKOSUITECROSS_OAUTH_SUCCESS="%s connected successfully! Access token stored."
; Post edit
COM_MOKOSUITECROSS_NEW_POST="New Post"
COM_MOKOSUITECROSS_EDIT_POST="Edit Post"
COM_MOKOSUITECROSS_POST_ARTICLE="Article"
COM_MOKOSUITECROSS_POST_ARTICLE_DESC="The Joomla article to cross-post."
COM_MOKOSUITECROSS_SELECT_ARTICLE="- Select Article -"
COM_MOKOSUITECROSS_POST_SERVICE="Service"
COM_MOKOSUITECROSS_POST_SERVICE_DESC="The service to post to."
COM_MOKOSUITECROSS_SELECT_SERVICE="- Select Service -"
COM_MOKOSUITECROSS_POST_MESSAGE="Message"
COM_MOKOSUITECROSS_POST_MESSAGE_DESC="The message to send to the platform. Use template placeholders or write a custom message."
COM_MOKOSUITECROSS_POST_STATUS="Status"
COM_MOKOSUITECROSS_STATUS_QUEUED="Queued"
COM_MOKOSUITECROSS_STATUS_SCHEDULED="Scheduled"
COM_MOKOSUITECROSS_STATUS_POSTED="Posted"
COM_MOKOSUITECROSS_STATUS_FAILED="Failed"
COM_MOKOSUITECROSS_POST_SCHEDULED_AT="Scheduled Date/Time"
COM_MOKOSUITECROSS_POST_SCHEDULED_AT_DESC="When to send this post. Leave empty to process immediately. Set a future date to schedule."
COM_MOKOSUITECROSS_POST_RESULTS="Post Results"
COM_MOKOSUITECROSS_POST_PLATFORM_ID="Platform Post ID"
COM_MOKOSUITECROSS_POST_ERROR="Error Message"
COM_MOKOSUITECROSS_POST_RETRY_COUNT="Retry Count"
COM_MOKOSUITECROSS_POST_POSTED_AT="Posted At"
COM_MOKOSUITECROSS_POST_CREATE_HELP="Create a manual cross-post. Select an article and service, write your message, and optionally set a scheduled date. Leave the schedule empty to queue for immediate processing."
COM_MOKOSUITECROSS_POST_REQUEUE="Re-queue for Posting"
COM_MOKOSUITECROSS_POST_REQUEUE_HELP="Reset this post to queued status so it will be processed again on the next queue run."
; Service edit
COM_MOKOSUITECROSS_NEW_SERVICE="New Service"
COM_MOKOSUITECROSS_EDIT_SERVICE="Edit Service"
COM_MOKOSUITECROSS_SERVICE_DETAILS="Service Details"
COM_MOKOSUITECROSS_CREDENTIALS_HELP="Fill in the connection details for the selected platform. Fields change based on the service type you choose above."
; Credential mode
COM_MOKOSUITECROSS_FIELD_CRED_MODE="Connection Mode"
COM_MOKOSUITECROSS_FIELD_CRED_MODE_DESC="Default uses the pre-configured MokoSuite account. Custom lets you use your own API credentials."
COM_MOKOSUITECROSS_CRED_MODE_DEFAULT="Default (MokoSuite)"
COM_MOKOSUITECROSS_CRED_MODE_CUSTOM="Custom (your own credentials)"
; Telegram
COM_MOKOSUITECROSS_CRED_TELEGRAM_CHAT_ID="Chat ID"
COM_MOKOSUITECROSS_CRED_TELEGRAM_CHAT_ID_DESC="Telegram channel, group, or user chat ID. Channel IDs start with -100. Get yours from @userinfobot."
COM_MOKOSUITECROSS_CRED_TELEGRAM_BOT_TOKEN="Bot Token"
COM_MOKOSUITECROSS_CRED_TELEGRAM_BOT_TOKEN_DESC="Your custom Telegram bot token from @BotFather. Only needed in Custom mode."
; Discord
COM_MOKOSUITECROSS_CRED_DISCORD_WEBHOOK="Webhook URL"
COM_MOKOSUITECROSS_CRED_DISCORD_WEBHOOK_DESC="Discord channel webhook URL. Create one in Channel Settings → Integrations → Webhooks."
; Slack
COM_MOKOSUITECROSS_CRED_SLACK_WEBHOOK="Webhook URL"
COM_MOKOSUITECROSS_CRED_SLACK_WEBHOOK_DESC="Slack Incoming Webhook URL. Create one at api.slack.com/apps."
; Teams
COM_MOKOSUITECROSS_CRED_TEAMS_WEBHOOK="Webhook URL"
COM_MOKOSUITECROSS_CRED_TEAMS_WEBHOOK_DESC="Microsoft Teams Incoming Webhook URL. Create in channel Connectors."
; Google Chat
COM_MOKOSUITECROSS_CRED_GOOGLECHAT_WEBHOOK="Webhook URL"
COM_MOKOSUITECROSS_CRED_GOOGLECHAT_WEBHOOK_DESC="Google Chat space webhook URL."
; Facebook
COM_MOKOSUITECROSS_CRED_FACEBOOK_PAGE_ID="Facebook Page ID"
COM_MOKOSUITECROSS_CRED_FACEBOOK_PAGE_ID_DESC="Your Facebook Page numeric ID. Find it in Page Settings → About."
COM_MOKOSUITECROSS_CRED_FACEBOOK_TOKEN="Page Access Token"
COM_MOKOSUITECROSS_CRED_FACEBOOK_TOKEN_DESC="Long-lived Page Access Token. Use the Authorize button below or generate via Meta Business Suite."
; Threads
COM_MOKOSUITECROSS_CRED_THREADS_USER_ID="Threads User ID"
COM_MOKOSUITECROSS_CRED_THREADS_TOKEN="Access Token"
; Twitter (OAuth 1.0a)
COM_MOKOSUITECROSS_CRED_TWITTER_API_KEY="API Key (Consumer Key)"
COM_MOKOSUITECROSS_CRED_TWITTER_API_KEY_DESC="Consumer Key from the Twitter Developer Portal → Keys and Tokens."
COM_MOKOSUITECROSS_CRED_TWITTER_API_SECRET="API Secret (Consumer Secret)"
COM_MOKOSUITECROSS_CRED_TWITTER_API_SECRET_DESC="Consumer Secret from the Twitter Developer Portal → Keys and Tokens."
COM_MOKOSUITECROSS_CRED_TWITTER_ACCESS_TOKEN="Access Token"
COM_MOKOSUITECROSS_CRED_TWITTER_ACCESS_TOKEN_DESC="User access token from the Developer Portal → Keys and Tokens → Authentication Tokens."
COM_MOKOSUITECROSS_CRED_TWITTER_ACCESS_TOKEN_SECRET="Access Token Secret"
COM_MOKOSUITECROSS_CRED_TWITTER_ACCESS_TOKEN_SECRET_DESC="User access token secret from the Developer Portal → Keys and Tokens → Authentication Tokens."
; LinkedIn
COM_MOKOSUITECROSS_CRED_LINKEDIN_TOKEN="Access Token"
COM_MOKOSUITECROSS_CRED_LINKEDIN_ORG_ID="Organization ID"
COM_MOKOSUITECROSS_CRED_LINKEDIN_ORG_ID_DESC="LinkedIn Company Page ID. Leave empty to post as yourself."
; Mastodon
COM_MOKOSUITECROSS_CRED_MASTODON_INSTANCE="Instance URL"
COM_MOKOSUITECROSS_CRED_MASTODON_INSTANCE_DESC="Your Mastodon server (e.g. https://mastodon.social)"
COM_MOKOSUITECROSS_CRED_MASTODON_TOKEN="Access Token"
; Bluesky
COM_MOKOSUITECROSS_CRED_BLUESKY_HANDLE="Handle"
COM_MOKOSUITECROSS_CRED_BLUESKY_HANDLE_DESC="Your Bluesky handle (e.g. user.bsky.social)"
COM_MOKOSUITECROSS_CRED_BLUESKY_APP_PWD="App Password"
COM_MOKOSUITECROSS_CRED_BLUESKY_APP_PWD_DESC="Generate in Bluesky Settings → Advanced → App Passwords."
; WhatsApp
COM_MOKOSUITECROSS_CRED_WHATSAPP_TOKEN="Access Token"
COM_MOKOSUITECROSS_CRED_WHATSAPP_PHONE_ID="Phone Number ID"
COM_MOKOSUITECROSS_CRED_WHATSAPP_RECIPIENT="Recipient Number"
COM_MOKOSUITECROSS_CRED_WHATSAPP_RECIPIENT_DESC="Phone number to send to, with country code (e.g. +1234567890)"
; Mailchimp
COM_MOKOSUITECROSS_CRED_MAILCHIMP_KEY="API Key"
COM_MOKOSUITECROSS_CRED_MAILCHIMP_KEY_DESC="Mailchimp API key (ends with -us1, -us2, etc.)"
COM_MOKOSUITECROSS_CRED_MAILCHIMP_LIST="Audience/List ID"
COM_MOKOSUITECROSS_CRED_MAILCHIMP_LIST_DESC="The audience to send campaigns to. Find in Audience → Settings → Audience ID."
; SendGrid
COM_MOKOSUITECROSS_CRED_SENDGRID_KEY="API Key"
COM_MOKOSUITECROSS_CRED_SENDGRID_LIST="Contact List ID"
; Webhook
COM_MOKOSUITECROSS_CRED_WEBHOOK_URL="Webhook URL"
COM_MOKOSUITECROSS_CRED_WEBHOOK_URL_DESC="The URL to send article data to. Works with Zapier, IFTTT, n8n, Make, or any custom endpoint."
COM_MOKOSUITECROSS_CRED_WEBHOOK_METHOD="HTTP Method"
; Matrix
COM_MOKOSUITECROSS_CRED_MATRIX_HOMESERVER="Homeserver URL"
COM_MOKOSUITECROSS_CRED_MATRIX_TOKEN="Access Token"
COM_MOKOSUITECROSS_CRED_MATRIX_ROOM="Room ID"
COM_MOKOSUITECROSS_CRED_MATRIX_ROOM_DESC="Matrix room ID (e.g. !abc123:matrix.org)"
; Ntfy
COM_MOKOSUITECROSS_CRED_NTFY_SERVER="Server URL"
COM_MOKOSUITECROSS_CRED_NTFY_TOPIC="Topic Name"
COM_MOKOSUITECROSS_CRED_NTFY_TOPIC_DESC="The notification topic (e.g. my-site-updates). Subscribers use this to receive push notifications."
COM_MOKOSUITECROSS_CRED_NTFY_TOKEN="Auth Token"
COM_MOKOSUITECROSS_CRED_NTFY_TOKEN_DESC="Optional authentication token if your ntfy server requires it."
; WordPress
COM_MOKOSUITECROSS_CRED_WP_SITE="WordPress Site URL"
COM_MOKOSUITECROSS_CRED_WP_USER="Username"
COM_MOKOSUITECROSS_CRED_WP_APP_PWD="Application Password"
COM_MOKOSUITECROSS_CRED_WP_APP_PWD_DESC="Generate in WordPress → Users → Profile → Application Passwords."
; Medium
COM_MOKOSUITECROSS_CRED_MEDIUM_TOKEN="Integration Token"
; Dev.to
COM_MOKOSUITECROSS_CRED_DEVTO_KEY="API Key"
; Ghost
COM_MOKOSUITECROSS_CRED_GHOST_SITE="Ghost Site URL"
COM_MOKOSUITECROSS_CRED_GHOST_KEY="Admin API Key"
; Reddit
COM_MOKOSUITECROSS_CRED_REDDIT_CLIENT_ID="App Client ID"
COM_MOKOSUITECROSS_CRED_REDDIT_SECRET="App Secret"
COM_MOKOSUITECROSS_CRED_REDDIT_USER="Reddit Username"
COM_MOKOSUITECROSS_CRED_REDDIT_SUBREDDIT="Subreddit"
COM_MOKOSUITECROSS_CRED_REDDIT_SUBREDDIT_DESC="Subreddit to post to (without r/ prefix)"
; Authorize / OAuth
COM_MOKOSUITECROSS_AUTHORIZE_BUTTON="Connect to %s"
COM_MOKOSUITECROSS_AUTHORIZE_HELP="Click to open the authorization page. You'll be redirected back after granting access. Your token will be saved automatically."
COM_MOKOSUITECROSS_OAUTH_HELP_TITLE="Authorization Required"
COM_MOKOSUITECROSS_OAUTH_HELP_BODY="This service requires OAuth authorization. Save the service first, then click the Connect button below to authorize access."
; LinkedIn (additional)
COM_MOKOSUITECROSS_CRED_LINKEDIN_REFRESH_TOKEN="Refresh Token"
COM_MOKOSUITECROSS_CRED_LINKEDIN_REFRESH_TOKEN_DESC="OAuth refresh token for automatic access token renewal."
; Bluesky (additional)
COM_MOKOSUITECROSS_CRED_BLUESKY_PDS_URL="PDS URL"
COM_MOKOSUITECROSS_CRED_BLUESKY_PDS_URL_DESC="Personal Data Server URL. Default is https://bsky.social. Only change for self-hosted PDS."
; Discord (additional)
COM_MOKOSUITECROSS_CRED_DISCORD_USERNAME="Display Name Override"
COM_MOKOSUITECROSS_CRED_DISCORD_USERNAME_DESC="Override the webhook's default display name. Leave empty to use the webhook name."
COM_MOKOSUITECROSS_CRED_DISCORD_AVATAR="Avatar URL Override"
COM_MOKOSUITECROSS_CRED_DISCORD_AVATAR_DESC="Override the webhook's default avatar with a custom image URL."
; Mailchimp (additional)
COM_MOKOSUITECROSS_CRED_MAILCHIMP_FROM_NAME="From Name"
COM_MOKOSUITECROSS_CRED_MAILCHIMP_FROM_NAME_DESC="Sender name for campaigns. Leave empty to use the audience default."
COM_MOKOSUITECROSS_CRED_MAILCHIMP_FROM_EMAIL="From Email"
COM_MOKOSUITECROSS_CRED_MAILCHIMP_FROM_EMAIL_DESC="Sender email for campaigns. Must be a verified sending domain."
; SendGrid (additional)
COM_MOKOSUITECROSS_CRED_SENDGRID_FROM_EMAIL="From Email"
COM_MOKOSUITECROSS_CRED_SENDGRID_FROM_EMAIL_DESC="Verified sender email address for Single Sends."
COM_MOKOSUITECROSS_CRED_SENDGRID_FROM_NAME="From Name"
COM_MOKOSUITECROSS_CRED_SENDGRID_FROM_NAME_DESC="Display name for the sender."
; Reddit (additional)
COM_MOKOSUITECROSS_CRED_REDDIT_PASSWORD="Account Password"
COM_MOKOSUITECROSS_CRED_REDDIT_PASSWORD_DESC="Required for Reddit script-type OAuth. The password for the Reddit account."
; WordPress (additional)
COM_MOKOSUITECROSS_CRED_WP_DEFAULT_STATUS="Default Post Status"
COM_MOKOSUITECROSS_CRED_WP_DEFAULT_STATUS_DESC="Whether cross-posted articles appear as drafts or are published immediately."
; Dev.to (additional)
COM_MOKOSUITECROSS_CRED_DEVTO_ORG_ID="Organization ID"
COM_MOKOSUITECROSS_CRED_DEVTO_ORG_ID_DESC="Optional. Publish under a Dev.to organization instead of your personal account."
; Ghost (additional)
COM_MOKOSUITECROSS_CRED_GHOST_DEFAULT_STATUS="Default Post Status"
COM_MOKOSUITECROSS_CRED_GHOST_DEFAULT_STATUS_DESC="Whether cross-posted articles are saved as drafts or published immediately."
; Status options (shared)
COM_MOKOSUITECROSS_STATUS_DRAFT="Draft"
COM_MOKOSUITECROSS_STATUS_PUBLISH="Publish"
COM_MOKOSUITECROSS_STATUS_PUBLISHED="Published"
; Pinterest
COM_MOKOSUITECROSS_CRED_PINTEREST_TOKEN="Access Token"
COM_MOKOSUITECROSS_CRED_PINTEREST_TOKEN_DESC="Pinterest API v5 access token from the Developer Portal."
COM_MOKOSUITECROSS_CRED_PINTEREST_BOARD="Board ID"
COM_MOKOSUITECROSS_CRED_PINTEREST_BOARD_DESC="The board to pin to. Find the ID in the board URL or via the API."
; Tumblr
COM_MOKOSUITECROSS_CRED_TUMBLR_TOKEN="Access Token"
COM_MOKOSUITECROSS_CRED_TUMBLR_TOKEN_DESC="Tumblr OAuth access token."
COM_MOKOSUITECROSS_CRED_TUMBLR_BLOG="Blog Name"
COM_MOKOSUITECROSS_CRED_TUMBLR_BLOG_DESC="Your Tumblr blog name (e.g. myblog — without .tumblr.com)."
; TikTok
COM_MOKOSUITECROSS_CRED_TIKTOK_TOKEN="Access Token"
COM_MOKOSUITECROSS_CRED_TIKTOK_REFRESH_TOKEN="Refresh Token"
COM_MOKOSUITECROSS_CRED_TIKTOK_OPEN_ID="Open ID"
COM_MOKOSUITECROSS_CRED_TIKTOK_OPEN_ID_DESC="Your TikTok Open ID from the developer app authorization."
; Nostr
COM_MOKOSUITECROSS_CRED_NOSTR_PRIVKEY="Private Key"
COM_MOKOSUITECROSS_CRED_NOSTR_PRIVKEY_DESC="Nostr private key in hex or nsec format. Used to sign events."
COM_MOKOSUITECROSS_CRED_NOSTR_RELAYS="Relay URLs"
COM_MOKOSUITECROSS_CRED_NOSTR_RELAYS_DESC="Comma-separated list of relay WebSocket URLs (e.g. wss://relay.damus.io, wss://nos.lol)."
; ActivityPub
COM_MOKOSUITECROSS_CRED_ACTIVITYPUB_INSTANCE="Instance URL"
COM_MOKOSUITECROSS_CRED_ACTIVITYPUB_INSTANCE_DESC="Fediverse instance URL (Pleroma, Akkoma, Misskey, Pixelfed, etc.)."
COM_MOKOSUITECROSS_CRED_ACTIVITYPUB_TOKEN="Access Token"
COM_MOKOSUITECROSS_CRED_ACTIVITYPUB_TOKEN_DESC="API access token from the instance's developer settings."
; Brevo (Sendinblue)
COM_MOKOSUITECROSS_CRED_BREVO_KEY="API Key"
COM_MOKOSUITECROSS_CRED_BREVO_LIST="Contact List ID"
COM_MOKOSUITECROSS_CRED_BREVO_LIST_DESC="Brevo contact list ID to send campaigns to."
COM_MOKOSUITECROSS_CRED_BREVO_SENDER_EMAIL="Sender Email"
COM_MOKOSUITECROSS_CRED_BREVO_SENDER_EMAIL_DESC="Must be a verified sender in your Brevo account."
COM_MOKOSUITECROSS_CRED_BREVO_SENDER_NAME="Sender Name"
; ConvertKit
COM_MOKOSUITECROSS_CRED_CONVERTKIT_KEY="API Key"
COM_MOKOSUITECROSS_CRED_CONVERTKIT_SECRET="API Secret"
; Constant Contact
COM_MOKOSUITECROSS_CRED_CONSTANTCONTACT_TOKEN="Access Token"
COM_MOKOSUITECROSS_CRED_CONSTANTCONTACT_REFRESH_TOKEN="Refresh Token"
COM_MOKOSUITECROSS_CRED_CONSTANTCONTACT_LISTS="Contact List IDs"
COM_MOKOSUITECROSS_CRED_CONSTANTCONTACT_LISTS_DESC="Comma-separated list IDs to include in the campaign."
; Hashnode
COM_MOKOSUITECROSS_CRED_HASHNODE_TOKEN="Personal Access Token"
COM_MOKOSUITECROSS_CRED_HASHNODE_PUB_ID="Publication ID"
COM_MOKOSUITECROSS_CRED_HASHNODE_PUB_ID_DESC="Your Hashnode publication ID. Find in Dashboard → General settings."
; Google Blogger
COM_MOKOSUITECROSS_CRED_BLOGGER_TOKEN="Access Token"
COM_MOKOSUITECROSS_CRED_BLOGGER_REFRESH_TOKEN="Refresh Token"
COM_MOKOSUITECROSS_CRED_BLOGGER_BLOG_ID="Blog ID"
COM_MOKOSUITECROSS_CRED_BLOGGER_BLOG_ID_DESC="Numeric Blog ID from Blogger settings or the Blogger API."
; Google Business Profile
COM_MOKOSUITECROSS_CRED_GBUSINESS_TOKEN="Access Token"
COM_MOKOSUITECROSS_CRED_GBUSINESS_REFRESH_TOKEN="Refresh Token"
COM_MOKOSUITECROSS_CRED_GBUSINESS_LOCATION="Location ID"
COM_MOKOSUITECROSS_CRED_GBUSINESS_LOCATION_DESC="Google Business location ID (e.g. locations/1234567890)."
COM_MOKOSUITECROSS_CRED_GBUSINESS_ACCOUNT="Account ID"
COM_MOKOSUITECROSS_CRED_GBUSINESS_ACCOUNT_DESC="Google Business account ID (e.g. accounts/1234567890)."
; RSS Feed
COM_MOKOSUITECROSS_CRED_RSSFEED_TITLE="Feed Title"
COM_MOKOSUITECROSS_CRED_RSSFEED_TITLE_DESC="Title for the generated RSS feed. Defaults to the site name."
COM_MOKOSUITECROSS_CRED_RSSFEED_MAX_ITEMS="Max Feed Items"
COM_MOKOSUITECROSS_CRED_RSSFEED_MAX_ITEMS_DESC="Maximum number of items to include in the feed."
; Webhook (additional)
COM_MOKOSUITECROSS_CRED_WEBHOOK_AUTH_TYPE="Authentication"
COM_MOKOSUITECROSS_CRED_WEBHOOK_AUTH_TYPE_DESC="Authentication method for the webhook endpoint."
COM_MOKOSUITECROSS_WEBHOOK_AUTH_NONE="None"
COM_MOKOSUITECROSS_WEBHOOK_AUTH_BEARER="Bearer Token"
COM_MOKOSUITECROSS_WEBHOOK_AUTH_BASIC="Basic Auth"
COM_MOKOSUITECROSS_CRED_WEBHOOK_BEARER_TOKEN="Bearer Token"
COM_MOKOSUITECROSS_CRED_WEBHOOK_BEARER_TOKEN_DESC="Authentication token sent as Authorization: Bearer {token}."
COM_MOKOSUITECROSS_CRED_WEBHOOK_BASIC_USER="Username"
COM_MOKOSUITECROSS_CRED_WEBHOOK_BASIC_PWD="Password"
COM_MOKOSUITECROSS_CRED_WEBHOOK_CONTENT_TYPE="Content Type"
; Service help link
COM_MOKOSUITECROSS_SERVICE_HELP_LINK="%s Setup Guide"
; Setup help panel
COM_MOKOSUITECROSS_SETUP_HELP_TITLE="How to set up"
COM_MOKOSUITECROSS_SETUP_HELP_INTRO="Setting up a new service is easy:"
COM_MOKOSUITECROSS_SETUP_STEP1="Choose a service type from the dropdown"
COM_MOKOSUITECROSS_SETUP_STEP2="Fill in the connection details that appear"
COM_MOKOSUITECROSS_SETUP_STEP3="For OAuth services, save first, then click Connect"
COM_MOKOSUITECROSS_SETUP_STEP4="Set status to Published and save"
; Test Connection
COM_MOKOSUITECROSS_TEST_CONNECTION_TITLE="Test Connection"
COM_MOKOSUITECROSS_TEST_CONNECTION_DESC="Verify that your credentials are valid and the service is reachable."
COM_MOKOSUITECROSS_TEST_CONNECTION_BUTTON="Test Connection"
COM_MOKOSUITECROSS_TEST_CONNECTION_TESTING="Testing..."
COM_MOKOSUITECROSS_TEST_CONNECTION_SUCCESS="Connection successful"
COM_MOKOSUITECROSS_TEST_CONNECTION_FAILED="Connection failed"
COM_MOKOSUITECROSS_TEST_CONNECTION_ERROR="Could not reach the server. Please try again."
COM_MOKOSUITECROSS_TEST_CONNECTION_NO_SERVICE="No service specified for test."
COM_MOKOSUITECROSS_TEST_CONNECTION_NOT_FOUND="Service record not found."
COM_MOKOSUITECROSS_TEST_CONNECTION_NO_PLUGIN="No service plugin available for type '%s'."
; Bulk Queue Actions
COM_MOKOSUITECROSS_TOOLBAR_RETRY_FAILED="Retry Failed"
COM_MOKOSUITECROSS_TOOLBAR_PURGE_POSTED="Purge Posted"
COM_MOKOSUITECROSS_POSTS_N_RETRIED="%d failed post(s) re-queued for retry."
COM_MOKOSUITECROSS_POSTS_N_RETRIED_1="1 failed post re-queued for retry."
COM_MOKOSUITECROSS_POSTS_N_PURGED="%d posted record(s) purged."
COM_MOKOSUITECROSS_POSTS_N_PURGED_1="1 posted record purged."
COM_MOKOSUITECROSS_POSTS_N_SCHEDULED="%d post(s) scheduled."
COM_MOKOSUITECROSS_POSTS_NO_ITEM_SELECTED="No posts selected."
COM_MOKOSUITECROSS_SCHEDULE_NO_DATE="Please select a date and time for scheduling."
COM_MOKOSUITECROSS_TOOLBAR_SCHEDULE="Schedule"
COM_MOKOSUITECROSS_TOOLBAR_RETRY_SELECTED="Retry Selected"
; Queue Depth Warning
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."
; 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."
; Trend Chart
COM_MOKOSUITECROSS_DASHBOARD_TREND_CHART="Daily Post Trend"
; Date Range Period Filter
COM_MOKOSUITECROSS_PERIOD_7_DAYS="Last 7 days"
COM_MOKOSUITECROSS_PERIOD_30_DAYS="Last 30 days"
COM_MOKOSUITECROSS_PERIOD_90_DAYS="Last 90 days"
COM_MOKOSUITECROSS_PERIOD_ALL_TIME="All time"
; Hashtag Placeholders
COM_MOKOSUITECROSS_PLACEHOLDER_TAGS="Article tags (comma-separated)"
COM_MOKOSUITECROSS_PLACEHOLDER_HASHTAGS="Article tags as hashtags (#Tag1 #Tag2)"
COM_MOKOSUITECROSS_PLACEHOLDER_CUSTOM_FIELD="Custom field value (replace xxx with field name)"
; CSV Export
COM_MOKOSUITECROSS_EXPORT_CSV="Export CSV"
; Service Stats (drill-down)
COM_MOKOSUITECROSS_SERVICESTATS_RECENT_POSTS="Recent Posts"
COM_MOKOSUITECROSS_SERVICESTATS_NO_POSTS="No posts for this service yet."
COM_MOKOSUITECROSS_SERVICESTATS_TOP_ARTICLES="Top Articles for This Service"
; API Dispatch
COM_MOKOSUITECROSS_DISPATCH_MISSING_ARTICLE="Missing or invalid article_id in request body."
COM_MOKOSUITECROSS_DISPATCH_INVALID_SERVICES="service_ids must be a non-empty array of service IDs."
COM_MOKOSUITECROSS_DISPATCH_ARTICLE_NOT_FOUND="Article not found."
COM_MOKOSUITECROSS_DISPATCH_NO_SERVICES="No enabled services found matching the request."
; Category Rules
COM_MOKOSUITECROSS_CONFIG_CATEGORY_RULES="Category Rules"
COM_MOKOSUITECROSS_CONFIG_CATEGORY_RULES_NOTE="Category Routing"
COM_MOKOSUITECROSS_CONFIG_CATEGORY_RULES_NOTE_DESC="Category routing rules let you map Joomla categories to specific cross-post services. When rules exist for a category, only those services receive posts. When no rules exist, all services are used (default behaviour). Rules are managed in the database table #__mokosuitecross_category_rules. A full admin UI will be added in a future release."
@@ -1,11 +0,0 @@
; MokoSuiteCross — System Language File
; Copyright (C) 2026 Moko Consulting. All rights reserved.
; License: GPL-3.0-or-later
COM_MOKOSUITECROSS="MokoSuiteCross"
COM_MOKOSUITECROSS_DESCRIPTION="Cross-posting Joomla content to social media, email marketing, and chat platforms"
COM_MOKOSUITECROSS_SUBMENU_DASHBOARD="Dashboard"
COM_MOKOSUITECROSS_SUBMENU_POSTS="Post Queue"
COM_MOKOSUITECROSS_SUBMENU_SERVICES="Services"
COM_MOKOSUITECROSS_SUBMENU_TEMPLATES="Templates"
COM_MOKOSUITECROSS_SUBMENU_LOGS="Activity Logs"
@@ -1,5 +0,0 @@
-- MokoSuiteCross — Uninstall
DROP TABLE IF EXISTS `#__mokosuitecross_logs`;
DROP TABLE IF EXISTS `#__mokosuitecross_posts`;
DROP TABLE IF EXISTS `#__mokosuitecross_templates`;
DROP TABLE IF EXISTS `#__mokosuitecross_services`;
@@ -1,110 +0,0 @@
<?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\Helper;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
/**
* Encrypts and decrypts service credentials using libsodium.
*
* Uses Joomla's $secret from configuration.php as the key source.
* Falls back to plaintext JSON if sodium is unavailable or decryption
* fails (backward compat with existing unencrypted credentials).
*/
class CredentialHelper
{
private const PREFIX = 'enc:sodium:';
/**
* Encrypt a credentials array to a storable string.
*
* @param array $credentials Credentials to encrypt
*
* @return string Encrypted string prefixed with "enc:sodium:", or plain JSON as fallback
*/
public static function encrypt(array $credentials): string
{
$json = json_encode($credentials);
if (!function_exists('sodium_crypto_secretbox')) {
return $json;
}
try {
$key = self::deriveKey();
$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
$cipher = sodium_crypto_secretbox($json, $nonce, $key);
return self::PREFIX . base64_encode($nonce . $cipher);
} catch (\Throwable $e) {
return $json;
}
}
/**
* Decrypt a credentials string back to an array.
*
* Handles both encrypted (prefixed) and legacy plaintext JSON.
*
* @param string $stored Stored credential string
*
* @return array Decoded credentials
*/
public static function decrypt(string $stored): array
{
if (empty($stored)) {
return [];
}
// Legacy plaintext JSON — no prefix
if (!str_starts_with($stored, self::PREFIX)) {
return json_decode($stored, true) ?: [];
}
if (!function_exists('sodium_crypto_secretbox_open')) {
return [];
}
try {
$key = self::deriveKey();
$payload = base64_decode(substr($stored, strlen(self::PREFIX)));
if ($payload === false || strlen($payload) < SODIUM_CRYPTO_SECRETBOX_NONCEBYTES) {
return [];
}
$nonce = substr($payload, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
$cipher = substr($payload, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
$plain = sodium_crypto_secretbox_open($cipher, $nonce, $key);
if ($plain === false) {
return [];
}
return json_decode($plain, true) ?: [];
} catch (\Throwable $e) {
return [];
}
}
/**
* Derive a 32-byte encryption key from Joomla's secret.
*/
private static function deriveKey(): string
{
$secret = Factory::getApplication()->get('secret', '');
return sodium_crypto_generichash($secret, '', SODIUM_CRYPTO_SECRETBOX_KEYBYTES);
}
}
@@ -1,13 +0,0 @@
PLG_CONTENT_MOKOSUITECROSS="Content - MokoSuiteCross"
PLG_CONTENT_MOKOSUITECROSS_DESCRIPTION="Adds cross-post status badges and per-article service selection to the article editor."
PLG_CONTENT_MOKOSUITECROSS_FIELDSET_CROSSPOST="Cross-Posting"
PLG_CONTENT_MOKOSUITECROSS_SKIP="Skip Cross-Posting"
PLG_CONTENT_MOKOSUITECROSS_SKIP_DESC="Skip all cross-posting for this article."
PLG_CONTENT_MOKOSUITECROSS_SERVICES="Post to Services"
PLG_CONTENT_MOKOSUITECROSS_SERVICES_DESC="Select which services to cross-post to. Leave all unchecked to post to all enabled services."
PLG_CONTENT_MOKOSUITECROSS_EVERGREEN="Evergreen Content"
PLG_CONTENT_MOKOSUITECROSS_EVERGREEN_DESC="Automatically re-share this article on a recurring schedule. Great for high-value content that stays relevant."
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_HISTORY="Cross-Post History"
@@ -1,2 +0,0 @@
PLG_CONTENT_MOKOSUITECROSS="Content - MokoSuiteCross"
PLG_CONTENT_MOKOSUITECROSS_DESCRIPTION="Adds cross-post status badges to articles in the admin backend."
@@ -1,2 +0,0 @@
PLG_MOKOSUITECROSS_ACTIVITYPUB="MokoSuiteCross - ActivityPub (Fediverse)"
PLG_MOKOSUITECROSS_ACTIVITYPUB_DESCRIPTION="Cross-post Joomla articles to ActivityPub (Fediverse)."
@@ -1,2 +0,0 @@
PLG_MOKOSUITECROSS_ACTIVITYPUB="MokoSuiteCross - ActivityPub (Fediverse)"
PLG_MOKOSUITECROSS_ACTIVITYPUB_DESCRIPTION="Cross-post Joomla articles to ActivityPub (Fediverse)."
@@ -1,2 +0,0 @@
PLG_MOKOSUITECROSS_BLOGGER="MokoSuiteCross - Google Blogger"
PLG_MOKOSUITECROSS_BLOGGER_DESCRIPTION="Cross-post Joomla articles to Google Blogger."
@@ -1,2 +0,0 @@
PLG_MOKOSUITECROSS_BLOGGER="MokoSuiteCross - Google Blogger"
PLG_MOKOSUITECROSS_BLOGGER_DESCRIPTION="Cross-post Joomla articles to Google Blogger."
@@ -1,7 +0,0 @@
PLG_MOKOSUITECROSS_BLUESKY="MokoSuiteCross - Bluesky"
PLG_MOKOSUITECROSS_BLUESKY_DESCRIPTION="Cross-post Joomla articles to Bluesky."
PLG_MOKOSUITECROSS_BLUESKY_FIELDSET_DEFAULTS="Bluesky Defaults"
PLG_MOKOSUITECROSS_BLUESKY_DEFAULT_PDS_URL="Default PDS URL"
PLG_MOKOSUITECROSS_BLUESKY_DEFAULT_PDS_URL_DESC="Default Bluesky PDS URL (e.g. https://bsky.social)."
PLG_MOKOSUITECROSS_BLUESKY_AUTO_LINK_CARD="Auto Link Card"
PLG_MOKOSUITECROSS_BLUESKY_AUTO_LINK_CARD_DESC="Automatically detect URLs and create link cards in posts."
@@ -1,2 +0,0 @@
PLG_MOKOSUITECROSS_BLUESKY="MokoSuiteCross - Bluesky"
PLG_MOKOSUITECROSS_BLUESKY_DESCRIPTION="Cross-post Joomla articles to Bluesky."
@@ -1,2 +0,0 @@
PLG_MOKOSUITECROSS_BREVO="MokoSuiteCross - Brevo (Sendinblue)"
PLG_MOKOSUITECROSS_BREVO_DESCRIPTION="Cross-post Joomla articles to Brevo (Sendinblue)."
@@ -1,2 +0,0 @@
PLG_MOKOSUITECROSS_BREVO="MokoSuiteCross - Brevo (Sendinblue)"
PLG_MOKOSUITECROSS_BREVO_DESCRIPTION="Cross-post Joomla articles to Brevo (Sendinblue)."
@@ -1,139 +0,0 @@
<?php
/**
* @package MokoSuiteCross
* @subpackage plg_mokosuitecross_brevo
* @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\Plugin\MokoSuiteCross\Brevo\Extension;
defined('_JEXEC') or die;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface;
use Joomla\Event\SubscriberInterface;
/**
* Brevo (Sendinblue) service plugin for MokoSuiteCross.
*
* API: https://api.brevo.com/v3/emailCampaigns
*/
class BrevoService extends CMSPlugin implements SubscriberInterface, MokoSuiteCrossServiceInterface
{
public static function getSubscribedEvents(): array
{
return ['onMokoSuiteCrossGetServices' => 'onMokoSuiteCrossGetServices'];
}
public function onMokoSuiteCrossGetServices(&$services): void
{
$services[] = $this;
}
public function getServiceType(): string { return 'brevo'; }
public function getServiceName(): string { return 'Brevo (Sendinblue)'; }
public function getMaxLength(): int { return 0; }
public function supportsMedia(): bool { return true; }
public function publish(string $message, array $media, array $credentials, array $params): array
{
$apiKey = $credentials['api_key'] ?? '';
$listId = (int) ($credentials['list_id'] ?? 0);
$senderName = $credentials['sender_name'] ?? 'Newsletter';
$senderEmail = $credentials['sender_email'] ?? '';
if (empty($apiKey) || empty($listId) || empty($senderEmail)) {
return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Missing API key, list ID, or sender email']];
}
$subject = $params['subject'] ?? mb_substr(strip_tags($message), 0, 150);
$postData = json_encode([
'name' => $subject,
'subject' => $subject,
'sender' => ['name' => $senderName, 'email' => $senderEmail],
'htmlContent' => $message,
'recipients' => ['listIds' => [$listId]],
]);
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => 'https://api.brevo.com/v3/emailCampaigns',
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $postData,
CURLOPT_HTTPHEADER => ['api-key: ' . $apiKey, 'Content-Type: application/json'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
]);
curl_setopt_array($ch, [
CURLOPT_URL => 'https://api.brevo.com/v3/emailCampaigns',
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $postData,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, '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 && $httpCode < 300) {
return ['success' => true, 'platform_post_id' => $data['id'] ?? $data['uri'] ?? '', 'response' => $data];
}
return ['success' => false, 'platform_post_id' => '', 'response' => $data];
}
public function validateCredentials(array $credentials): array
{
$key = $credentials['api_key'] ?? '';
if (empty($key)) {
return ['valid' => false, 'message' => 'Missing API key', 'account_name' => ''];
}
$ch = curl_init('https://api.brevo.com/v3/account');
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ['api-key: ' . $key],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
$response = curl_exec($ch);
if ($response === false) {
$curlError = curl_error($ch);
curl_close($ch);
return ['valid' => false, 'message' => 'Connection error: ' . $curlError, 'account_name' => ''];
}
curl_close($ch);
$data = json_decode($response, true) ?: [];
if (!empty($data['companyName'])) {
return ['valid' => true, 'message' => 'Connected', 'account_name' => $data['companyName']];
}
return ['valid' => false, 'message' => $data['message'] ?? 'Invalid API key', 'account_name' => ''];
}
public function getSupportedMediaTypes(): array
{
return ['image'];
}
}
@@ -1,2 +0,0 @@
PLG_MOKOSUITECROSS_CONSTANTCONTACT="MokoSuiteCross - Constant Contact"
PLG_MOKOSUITECROSS_CONSTANTCONTACT_DESCRIPTION="Cross-post Joomla articles to Constant Contact."
@@ -1,2 +0,0 @@
PLG_MOKOSUITECROSS_CONSTANTCONTACT="MokoSuiteCross - Constant Contact"
PLG_MOKOSUITECROSS_CONSTANTCONTACT_DESCRIPTION="Cross-post Joomla articles to Constant Contact."
@@ -1,142 +0,0 @@
<?php
/**
* @package MokoSuiteCross
* @subpackage plg_mokosuitecross_constantcontact
* @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\Plugin\MokoSuiteCross\Constantcontact\Extension;
defined('_JEXEC') or die;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface;
use Joomla\Event\SubscriberInterface;
/**
* Constant Contact service plugin for MokoSuiteCross.
*
* API: https://api.cc.email/v3/emails
*/
class ConstantcontactService extends CMSPlugin implements SubscriberInterface, MokoSuiteCrossServiceInterface
{
public static function getSubscribedEvents(): array
{
return ['onMokoSuiteCrossGetServices' => 'onMokoSuiteCrossGetServices'];
}
public function onMokoSuiteCrossGetServices(&$services): void
{
$services[] = $this;
}
public function getServiceType(): string { return 'constantcontact'; }
public function getServiceName(): string { return 'Constant Contact'; }
public function getMaxLength(): int { return 0; }
public function supportsMedia(): bool { return true; }
public function publish(string $message, array $media, array $credentials, array $params): array
{
$token = $credentials['access_token'] ?? '';
$listId = $credentials['list_id'] ?? '';
$fromName = $credentials['from_name'] ?? 'Newsletter';
$fromEmail = $credentials['from_email'] ?? '';
if (empty($token) || empty($fromEmail)) {
return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Missing access token or sender email']];
}
$subject = $params['subject'] ?? mb_substr(strip_tags($message), 0, 150);
$postData = json_encode([
'name' => $subject,
'email_campaign_activities' => [[
'format_type' => 5,
'from_name' => $fromName,
'from_email' => $fromEmail,
'subject' => $subject,
'html_content' => $message,
]],
]);
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => 'https://api.cc.email/v3/emails',
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $postData,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, 'Content-Type: application/json'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
]);
curl_setopt_array($ch, [
CURLOPT_URL => 'https://api.cc.email/v3/emails',
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $postData,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, '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 && $httpCode < 300) {
return ['success' => true, 'platform_post_id' => $data['id'] ?? $data['uri'] ?? '', 'response' => $data];
}
return ['success' => false, 'platform_post_id' => '', 'response' => $data];
}
public function validateCredentials(array $credentials): array
{
$token = $credentials['access_token'] ?? '';
if (empty($token)) {
return ['valid' => false, 'message' => 'Missing access token', 'account_name' => ''];
}
$ch = curl_init('https://api.cc.email/v3/account/summary');
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
$response = curl_exec($ch);
if ($response === false) {
$curlError = curl_error($ch);
curl_close($ch);
return ['valid' => false, 'message' => 'Connection error: ' . $curlError, 'account_name' => ''];
}
curl_close($ch);
$data = json_decode($response, true) ?: [];
if (!empty($data['organization_name'])) {
return ['valid' => true, 'message' => 'Connected', 'account_name' => $data['organization_name']];
}
return ['valid' => false, 'message' => 'Invalid token', 'account_name' => ''];
}
public function getSupportedMediaTypes(): array
{
return ['image'];
}
}
@@ -1,2 +0,0 @@
PLG_MOKOSUITECROSS_CONVERTKIT="MokoSuiteCross - ConvertKit"
PLG_MOKOSUITECROSS_CONVERTKIT_DESCRIPTION="Cross-post Joomla articles to ConvertKit."
@@ -1,2 +0,0 @@
PLG_MOKOSUITECROSS_CONVERTKIT="MokoSuiteCross - ConvertKit"
PLG_MOKOSUITECROSS_CONVERTKIT_DESCRIPTION="Cross-post Joomla articles to ConvertKit."
@@ -1,134 +0,0 @@
<?php
/**
* @package MokoSuiteCross
* @subpackage plg_mokosuitecross_convertkit
* @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\Plugin\MokoSuiteCross\Convertkit\Extension;
defined('_JEXEC') or die;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface;
use Joomla\Event\SubscriberInterface;
/**
* ConvertKit service plugin for MokoSuiteCross.
*
* API: https://api.convertkit.com/v3/broadcasts
*/
class ConvertkitService extends CMSPlugin implements SubscriberInterface, MokoSuiteCrossServiceInterface
{
public static function getSubscribedEvents(): array
{
return ['onMokoSuiteCrossGetServices' => 'onMokoSuiteCrossGetServices'];
}
public function onMokoSuiteCrossGetServices(&$services): void
{
$services[] = $this;
}
public function getServiceType(): string { return 'convertkit'; }
public function getServiceName(): string { return 'ConvertKit'; }
public function getMaxLength(): int { return 0; }
public function supportsMedia(): bool { return true; }
public function publish(string $message, array $media, array $credentials, array $params): array
{
$apiSecret = $credentials['api_secret'] ?? '';
if (empty($apiSecret)) {
return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Missing API secret']];
}
$subject = $params['subject'] ?? mb_substr(strip_tags($message), 0, 150);
$postData = json_encode([
'api_secret' => $apiSecret,
'content' => $message,
'subject' => $subject,
'description' => mb_substr(strip_tags($message), 0, 200),
]);
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => 'https://api.convertkit.com/v3/broadcasts',
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $postData,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
]);
curl_setopt_array($ch, [
CURLOPT_URL => 'https://api.convertkit.com/v3/broadcasts',
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $postData,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, '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 && $httpCode < 300) {
return ['success' => true, 'platform_post_id' => $data['id'] ?? $data['uri'] ?? '', 'response' => $data];
}
return ['success' => false, 'platform_post_id' => '', 'response' => $data];
}
public function validateCredentials(array $credentials): array
{
$apiSecret = $credentials['api_secret'] ?? '';
if (empty($apiSecret)) {
return ['valid' => false, 'message' => 'Missing API secret', 'account_name' => ''];
}
$ch = curl_init('https://api.convertkit.com/v3/account?api_secret=' . urlencode($apiSecret));
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
$response = curl_exec($ch);
if ($response === false) {
$curlError = curl_error($ch);
curl_close($ch);
return ['valid' => false, 'message' => 'Connection error: ' . $curlError, 'account_name' => ''];
}
curl_close($ch);
$data = json_decode($response, true) ?: [];
if (!empty($data['name'])) {
return ['valid' => true, 'message' => 'Connected', 'account_name' => $data['name']];
}
return ['valid' => false, 'message' => 'Invalid API secret', 'account_name' => ''];
}
public function getSupportedMediaTypes(): array
{
return [];
}
}
@@ -1,2 +0,0 @@
PLG_MOKOSUITECROSS_DEVTO="MokoSuiteCross - Dev.to"
PLG_MOKOSUITECROSS_DEVTO_DESCRIPTION="Cross-post Joomla articles to Dev.to."
@@ -1,2 +0,0 @@
PLG_MOKOSUITECROSS_DEVTO="MokoSuiteCross - Dev.to"
PLG_MOKOSUITECROSS_DEVTO_DESCRIPTION="Cross-post Joomla articles to Dev.to."
@@ -1,134 +0,0 @@
<?php
/**
* @package MokoSuiteCross
* @subpackage plg_mokosuitecross_devto
* @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\Plugin\MokoSuiteCross\Devto\Extension;
defined('_JEXEC') or die;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface;
use Joomla\Event\SubscriberInterface;
/**
* Dev.to service plugin for MokoSuiteCross.
*
* API: https://dev.to/api/articles
*/
class DevtoService extends CMSPlugin implements SubscriberInterface, MokoSuiteCrossServiceInterface
{
public static function getSubscribedEvents(): array
{
return ['onMokoSuiteCrossGetServices' => 'onMokoSuiteCrossGetServices'];
}
public function onMokoSuiteCrossGetServices(&$services): void
{
$services[] = $this;
}
public function getServiceType(): string { return 'devto'; }
public function getServiceName(): string { return 'Dev.to'; }
public function getMaxLength(): int { return 0; }
public function supportsMedia(): bool { return true; }
public function publish(string $message, array $media, array $credentials, array $params): array
{
$token = $credentials['api_key'] ?? '';
if (empty($token)) {
return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Missing API key']];
}
$title = mb_substr(strip_tags($message), 0, 150);
$body = $message;
// Prepend image in markdown if available
if (!empty($media[0])) {
$body = '![Cover image](' . $media[0] . ")\n\n" . $body;
}
$postData = json_encode([
'article' => [
'title' => $title,
'body_markdown' => $body,
'published' => true,
],
]);
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => 'https://dev.to/api/articles',
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $postData,
CURLOPT_HTTPHEADER => ['api-key: ' . $token, '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 && $httpCode < 300) {
return ['success' => true, 'platform_post_id' => $data['id'] ?? $data['uri'] ?? '', 'response' => $data];
}
return ['success' => false, 'platform_post_id' => '', 'response' => $data];
}
public function validateCredentials(array $credentials): array
{
$key = $credentials['api_key'] ?? '';
if (empty($key)) {
return ['valid' => false, 'message' => 'Missing API key', 'account_name' => ''];
}
$ch = curl_init('https://dev.to/api/users/me');
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ['api-key: ' . $key],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
$response = curl_exec($ch);
if ($response === false) {
$curlError = curl_error($ch);
curl_close($ch);
return ['valid' => false, 'message' => 'Connection error: ' . $curlError, 'account_name' => ''];
}
curl_close($ch);
$data = json_decode($response, true) ?: [];
if (!empty($data['username'])) {
return ['valid' => true, 'message' => 'Connected', 'account_name' => $data['username']];
}
return ['valid' => false, 'message' => $data['error'] ?? 'Invalid API key', 'account_name' => ''];
}
public function getSupportedMediaTypes(): array
{
return ['image'];
}
}
@@ -1,7 +0,0 @@
PLG_MOKOSUITECROSS_DISCORD="MokoSuiteCross - Discord"
PLG_MOKOSUITECROSS_DISCORD_DESCRIPTION="Cross-post Joomla articles to Discord."
PLG_MOKOSUITECROSS_DISCORD_FIELDSET_DEFAULTS="Default Settings"
PLG_MOKOSUITECROSS_DISCORD_DEFAULT_WEBHOOK_URL="Default Webhook URL"
PLG_MOKOSUITECROSS_DISCORD_DEFAULT_WEBHOOK_URL_DESC="The default MokoSuite Discord webhook URL used when a service is set to 'default' mode."
PLG_MOKOSUITECROSS_DISCORD_EMBED_COLOR="Embed Color"
PLG_MOKOSUITECROSS_DISCORD_EMBED_COLOR_DESC="Default color for Discord embed messages. Defaults to Discord blurple (#5865F2)."
@@ -1,2 +0,0 @@
PLG_MOKOSUITECROSS_DISCORD="MokoSuiteCross - Discord"
PLG_MOKOSUITECROSS_DISCORD_DESCRIPTION="Cross-post Joomla articles to Discord."
@@ -1,7 +0,0 @@
PLG_MOKOSUITECROSS_FACEBOOK="MokoSuiteCross - Facebook / Meta"
PLG_MOKOSUITECROSS_FACEBOOK_DESCRIPTION="Cross-post Joomla articles to Facebook / Meta."
PLG_MOKOSUITECROSS_FACEBOOK_FIELDSET_DEFAULTS="Default Settings"
PLG_MOKOSUITECROSS_FACEBOOK_DEFAULT_PAGE_ACCESS_TOKEN="Default Page Access Token"
PLG_MOKOSUITECROSS_FACEBOOK_DEFAULT_PAGE_ACCESS_TOKEN_DESC="The default MokoSuite Facebook Page Access Token used when a service is set to 'default' mode."
PLG_MOKOSUITECROSS_FACEBOOK_DEFAULT_PAGE_ID="Default Page ID"
PLG_MOKOSUITECROSS_FACEBOOK_DEFAULT_PAGE_ID_DESC="The default Facebook Page ID used when a service is set to 'default' mode."
@@ -1,2 +0,0 @@
PLG_MOKOSUITECROSS_FACEBOOK="MokoSuiteCross - Facebook / Meta"
PLG_MOKOSUITECROSS_FACEBOOK_DESCRIPTION="Cross-post Joomla articles to Facebook / Meta."
@@ -1,2 +0,0 @@
PLG_MOKOSUITECROSS_GHOST="MokoSuiteCross - Ghost"
PLG_MOKOSUITECROSS_GHOST_DESCRIPTION="Cross-post Joomla articles to Ghost."
@@ -1,2 +0,0 @@
PLG_MOKOSUITECROSS_GHOST="MokoSuiteCross - Ghost"
PLG_MOKOSUITECROSS_GHOST_DESCRIPTION="Cross-post Joomla articles to Ghost."
@@ -1,2 +0,0 @@
PLG_MOKOSUITECROSS_GOOGLEBUSINESS="MokoSuiteCross - Google Business Profile"
PLG_MOKOSUITECROSS_GOOGLEBUSINESS_DESCRIPTION="Cross-post Joomla articles to Google Business Profile."
@@ -1,2 +0,0 @@
PLG_MOKOSUITECROSS_GOOGLEBUSINESS="MokoSuiteCross - Google Business Profile"
PLG_MOKOSUITECROSS_GOOGLEBUSINESS_DESCRIPTION="Cross-post Joomla articles to Google Business Profile."
@@ -1,2 +0,0 @@
PLG_MOKOSUITECROSS_GOOGLECHAT="MokoSuiteCross - Google Chat"
PLG_MOKOSUITECROSS_GOOGLECHAT_DESCRIPTION="Cross-post Joomla articles to Google Chat."
@@ -1,2 +0,0 @@
PLG_MOKOSUITECROSS_GOOGLECHAT="MokoSuiteCross - Google Chat"
PLG_MOKOSUITECROSS_GOOGLECHAT_DESCRIPTION="Cross-post Joomla articles to Google Chat."
@@ -1,2 +0,0 @@
PLG_MOKOSUITECROSS_HASHNODE="MokoSuiteCross - Hashnode"
PLG_MOKOSUITECROSS_HASHNODE_DESCRIPTION="Cross-post Joomla articles to Hashnode."
@@ -1,2 +0,0 @@
PLG_MOKOSUITECROSS_HASHNODE="MokoSuiteCross - Hashnode"
PLG_MOKOSUITECROSS_HASHNODE_DESCRIPTION="Cross-post Joomla articles to Hashnode."
@@ -1,9 +0,0 @@
PLG_MOKOSUITECROSS_LINKEDIN="MokoSuiteCross - LinkedIn"
PLG_MOKOSUITECROSS_LINKEDIN_DESCRIPTION="Cross-post Joomla articles to LinkedIn."
PLG_MOKOSUITECROSS_LINKEDIN_FIELDSET_DEFAULTS="LinkedIn Defaults"
PLG_MOKOSUITECROSS_LINKEDIN_CLIENT_ID="Client ID"
PLG_MOKOSUITECROSS_LINKEDIN_CLIENT_ID_DESC="LinkedIn App Client ID."
PLG_MOKOSUITECROSS_LINKEDIN_CLIENT_SECRET="Client Secret"
PLG_MOKOSUITECROSS_LINKEDIN_CLIENT_SECRET_DESC="LinkedIn App Client Secret."
PLG_MOKOSUITECROSS_LINKEDIN_REDIRECT_URI="Redirect URI"
PLG_MOKOSUITECROSS_LINKEDIN_REDIRECT_URI_DESC="OAuth callback URL for LinkedIn authentication."
@@ -1,2 +0,0 @@
PLG_MOKOSUITECROSS_LINKEDIN="MokoSuiteCross - LinkedIn"
PLG_MOKOSUITECROSS_LINKEDIN_DESCRIPTION="Cross-post Joomla articles to LinkedIn."
@@ -1,9 +0,0 @@
PLG_MOKOSUITECROSS_MAILCHIMP="MokoSuiteCross - Mailchimp"
PLG_MOKOSUITECROSS_MAILCHIMP_DESCRIPTION="Cross-post Joomla articles to Mailchimp."
PLG_MOKOSUITECROSS_MAILCHIMP_FIELDSET_DEFAULTS="Mailchimp Defaults"
PLG_MOKOSUITECROSS_MAILCHIMP_DEFAULT_FROM_NAME="Default From Name"
PLG_MOKOSUITECROSS_MAILCHIMP_DEFAULT_FROM_NAME_DESC="Default sender name for Mailchimp campaigns."
PLG_MOKOSUITECROSS_MAILCHIMP_DEFAULT_FROM_EMAIL="Default From Email"
PLG_MOKOSUITECROSS_MAILCHIMP_DEFAULT_FROM_EMAIL_DESC="Default sender email address for Mailchimp campaigns."
PLG_MOKOSUITECROSS_MAILCHIMP_AUTO_SEND="Auto Send"
PLG_MOKOSUITECROSS_MAILCHIMP_AUTO_SEND_DESC="Automatically send the campaign on creation instead of saving as draft."
@@ -1,2 +0,0 @@
PLG_MOKOSUITECROSS_MAILCHIMP="MokoSuiteCross - Mailchimp"
PLG_MOKOSUITECROSS_MAILCHIMP_DESCRIPTION="Cross-post Joomla articles to Mailchimp."
@@ -1,13 +0,0 @@
PLG_MOKOSUITECROSS_MASTODON="MokoSuiteCross - Mastodon"
PLG_MOKOSUITECROSS_MASTODON_DESCRIPTION="Cross-post Joomla articles to Mastodon."
PLG_MOKOSUITECROSS_MASTODON_FIELDSET_DEFAULTS="Mastodon Defaults"
PLG_MOKOSUITECROSS_MASTODON_DEFAULT_INSTANCE_URL="Default Instance URL"
PLG_MOKOSUITECROSS_MASTODON_DEFAULT_INSTANCE_URL_DESC="Default Mastodon instance URL (e.g. https://mastodon.social)."
PLG_MOKOSUITECROSS_MASTODON_DEFAULT_VISIBILITY="Default Visibility"
PLG_MOKOSUITECROSS_MASTODON_DEFAULT_VISIBILITY_DESC="Default post visibility for Mastodon toots."
PLG_MOKOSUITECROSS_MASTODON_VISIBILITY_PUBLIC="Public"
PLG_MOKOSUITECROSS_MASTODON_VISIBILITY_UNLISTED="Unlisted"
PLG_MOKOSUITECROSS_MASTODON_VISIBILITY_PRIVATE="Private"
PLG_MOKOSUITECROSS_MASTODON_VISIBILITY_DIRECT="Direct"
PLG_MOKOSUITECROSS_MASTODON_APPEND_HASHTAGS="Append Hashtags"
PLG_MOKOSUITECROSS_MASTODON_APPEND_HASHTAGS_DESC="Default hashtags to append to posts (e.g. #Joomla #MokoSuite)."
@@ -1,2 +0,0 @@
PLG_MOKOSUITECROSS_MASTODON="MokoSuiteCross - Mastodon"
PLG_MOKOSUITECROSS_MASTODON_DESCRIPTION="Cross-post Joomla articles to Mastodon."
@@ -1,57 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="mokosuitecross" method="upgrade">
<name>MokoSuiteCross - Mastodon</name>
<version>01.04.01</version>
<creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
<license>GPL-3.0-or-later</license>
<description>PLG_MOKOSUITECROSS_MASTODON_DESCRIPTION</description>
<namespace path="src">Joomla\Plugin\MokoSuiteCross${CLASS_NAME}</namespace>
<files>
<filename plugin="mastodon">mastodon.php</filename>
<folder>src</folder>
<folder>services</folder>
<folder>language</folder>
</files>
<languages>
<language tag="en-GB">language/en-GB/plg_mokosuitecross_mastodon.ini</language>
<language tag="en-GB">language/en-GB/plg_mokosuitecross_mastodon.sys.ini</language>
</languages>
<config>
<fields name="params">
<fieldset name="basic" label="PLG_MOKOSUITECROSS_MASTODON_FIELDSET_DEFAULTS">
<field
name="default_instance_url"
type="url"
label="PLG_MOKOSUITECROSS_MASTODON_DEFAULT_INSTANCE_URL"
description="PLG_MOKOSUITECROSS_MASTODON_DEFAULT_INSTANCE_URL_DESC"
/>
<field
name="default_visibility"
type="list"
label="PLG_MOKOSUITECROSS_MASTODON_DEFAULT_VISIBILITY"
description="PLG_MOKOSUITECROSS_MASTODON_DEFAULT_VISIBILITY_DESC"
default="public"
>
<option value="public">PLG_MOKOSUITECROSS_MASTODON_VISIBILITY_PUBLIC</option>
<option value="unlisted">PLG_MOKOSUITECROSS_MASTODON_VISIBILITY_UNLISTED</option>
<option value="private">PLG_MOKOSUITECROSS_MASTODON_VISIBILITY_PRIVATE</option>
<option value="direct">PLG_MOKOSUITECROSS_MASTODON_VISIBILITY_DIRECT</option>
</field>
<field
name="append_hashtags"
type="text"
label="PLG_MOKOSUITECROSS_MASTODON_APPEND_HASHTAGS"
description="PLG_MOKOSUITECROSS_MASTODON_APPEND_HASHTAGS_DESC"
/>
</fieldset>
</fields>
</config>
</extension>
@@ -1,2 +0,0 @@
PLG_MOKOSUITECROSS_MATRIX="MokoSuiteCross - Matrix / Element"
PLG_MOKOSUITECROSS_MATRIX_DESCRIPTION="Cross-post Joomla articles to Matrix / Element."
@@ -1,2 +0,0 @@
PLG_MOKOSUITECROSS_MATRIX="MokoSuiteCross - Matrix / Element"
PLG_MOKOSUITECROSS_MATRIX_DESCRIPTION="Cross-post Joomla articles to Matrix / Element."
@@ -1,2 +0,0 @@
PLG_MOKOSUITECROSS_MEDIUM="MokoSuiteCross - Medium"
PLG_MOKOSUITECROSS_MEDIUM_DESCRIPTION="Cross-post Joomla articles to Medium."
@@ -1,2 +0,0 @@
PLG_MOKOSUITECROSS_MEDIUM="MokoSuiteCross - Medium"
PLG_MOKOSUITECROSS_MEDIUM_DESCRIPTION="Cross-post Joomla articles to Medium."
@@ -1,14 +0,0 @@
; MokoSuiteCross - MokoSuiteCalendar Events Service
; Copyright (C) 2026 Moko Consulting. All rights reserved.
; License: GPL-3.0-or-later
PLG_MOKOSUITECROSS_MOKOJOOMCALENDAR="MokoSuiteCross - MokoSuiteCalendar Events"
PLG_MOKOSUITECROSS_MOKOJOOMCALENDAR_DESCRIPTION="Cross-posts MokoSuiteCalendar events to connected platforms. Enriches messages with event date, time, location, and calendar details."
PLG_MOKOSUITECROSS_MOKOJOOMCALENDAR_FIELDSET_DEFAULTS="Event Cross-Post Settings"
PLG_MOKOSUITECROSS_MOKOJOOMCALENDAR_INCLUDE_LOCATION="Include Location"
PLG_MOKOSUITECROSS_MOKOJOOMCALENDAR_INCLUDE_LOCATION_DESC="Append the event location to the cross-post message."
PLG_MOKOSUITECROSS_MOKOJOOMCALENDAR_INCLUDE_DATE="Include Date/Time"
PLG_MOKOSUITECROSS_MOKOJOOMCALENDAR_INCLUDE_DATE_DESC="Append the event date and time to the cross-post message."
PLG_MOKOSUITECROSS_MOKOJOOMCALENDAR_DATE_FORMAT="Date Format"
PLG_MOKOSUITECROSS_MOKOJOOMCALENDAR_DATE_FORMAT_DESC="PHP date format string for event dates. Default: l, F j, Y at g:ia"
@@ -1,6 +0,0 @@
; MokoSuiteCross - MokoSuiteCalendar Events Service
; Copyright (C) 2026 Moko Consulting. All rights reserved.
; License: GPL-3.0-or-later
PLG_MOKOSUITECROSS_MOKOJOOMCALENDAR="Plugin - MokoSuiteCross MokoSuiteCalendar Events"
PLG_MOKOSUITECROSS_MOKOJOOMCALENDAR_DESCRIPTION="Cross-posts MokoSuiteCalendar events to connected platforms."
@@ -1,16 +0,0 @@
; MokoSuiteCross - MokoSuiteGallery Service
; Copyright (C) 2026 Moko Consulting. All rights reserved.
; License: GPL-3.0-or-later
PLG_MOKOSUITECROSS_MOKOJOOMGALLERY="MokoSuiteCross - MokoSuiteGallery"
PLG_MOKOSUITECROSS_MOKOJOOMGALLERY_DESCRIPTION="Cross-posts MokoSuiteGallery content to connected platforms. Supports gallery announcements with preview images and individual image posts."
PLG_MOKOSUITECROSS_MOKOJOOMGALLERY_FIELDSET_DEFAULTS="Gallery Cross-Post Settings"
PLG_MOKOSUITECROSS_MOKOJOOMGALLERY_POST_MODE="Post Mode"
PLG_MOKOSUITECROSS_MOKOJOOMGALLERY_POST_MODE_DESC="Gallery mode posts when a gallery is published (with preview images). Image mode posts each individual image."
PLG_MOKOSUITECROSS_MOKOJOOMGALLERY_MODE_GALLERY="Gallery (with preview images)"
PLG_MOKOSUITECROSS_MOKOJOOMGALLERY_MODE_IMAGE="Individual Images"
PLG_MOKOSUITECROSS_MOKOJOOMGALLERY_MAX_IMAGES="Max Preview Images"
PLG_MOKOSUITECROSS_MOKOJOOMGALLERY_MAX_IMAGES_DESC="Maximum number of preview images to attach when cross-posting a gallery."
PLG_MOKOSUITECROSS_MOKOJOOMGALLERY_INCLUDE_DESCRIPTION="Include Description"
PLG_MOKOSUITECROSS_MOKOJOOMGALLERY_INCLUDE_DESCRIPTION_DESC="Append the gallery or image description to the cross-post message."
@@ -1,6 +0,0 @@
; MokoSuiteCross - MokoSuiteGallery Service
; Copyright (C) 2026 Moko Consulting. All rights reserved.
; License: GPL-3.0-or-later
PLG_MOKOSUITECROSS_MOKOJOOMGALLERY="Plugin - MokoSuiteCross MokoSuiteGallery"
PLG_MOKOSUITECROSS_MOKOJOOMGALLERY_DESCRIPTION="Cross-posts MokoSuiteGallery galleries and images to connected platforms."
@@ -1,63 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="mokosuitecross" method="upgrade">
<name>MokoSuiteCross - MokoSuiteGallery</name>
<version>01.04.01</version>
<creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
<license>GPL-3.0-or-later</license>
<description>PLG_MOKOSUITECROSS_MOKOJOOMGALLERY_DESCRIPTION</description>
<namespace path="src">Joomla\Plugin\MokoSuiteCross\MokoSuiteGallery</namespace>
<files>
<filename plugin="mokosuitegallery">mokosuitegallery.php</filename>
<folder>src</folder>
<folder>services</folder>
<folder>language</folder>
</files>
<languages>
<language tag="en-GB">language/en-GB/plg_mokosuitecross_mokosuitegallery.ini</language>
<language tag="en-GB">language/en-GB/plg_mokosuitecross_mokosuitegallery.sys.ini</language>
</languages>
<config>
<fields name="params">
<fieldset name="basic" label="PLG_MOKOSUITECROSS_MOKOJOOMGALLERY_FIELDSET_DEFAULTS">
<field
name="post_mode"
type="list"
label="PLG_MOKOSUITECROSS_MOKOJOOMGALLERY_POST_MODE"
description="PLG_MOKOSUITECROSS_MOKOJOOMGALLERY_POST_MODE_DESC"
default="gallery"
>
<option value="gallery">PLG_MOKOSUITECROSS_MOKOJOOMGALLERY_MODE_GALLERY</option>
<option value="image">PLG_MOKOSUITECROSS_MOKOJOOMGALLERY_MODE_IMAGE</option>
</field>
<field
name="max_images"
type="number"
label="PLG_MOKOSUITECROSS_MOKOJOOMGALLERY_MAX_IMAGES"
description="PLG_MOKOSUITECROSS_MOKOJOOMGALLERY_MAX_IMAGES_DESC"
default="4"
min="1"
max="20"
/>
<field
name="include_description"
type="radio"
label="PLG_MOKOSUITECROSS_MOKOJOOMGALLERY_INCLUDE_DESCRIPTION"
description="PLG_MOKOSUITECROSS_MOKOJOOMGALLERY_INCLUDE_DESCRIPTION_DESC"
default="1"
class="btn-group"
>
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
</fieldset>
</fields>
</config>
</extension>
@@ -1,2 +0,0 @@
PLG_MOKOSUITECROSS_NOSTR="MokoSuiteCross - Nostr"
PLG_MOKOSUITECROSS_NOSTR_DESCRIPTION="Cross-post Joomla articles to Nostr."
@@ -1,2 +0,0 @@
PLG_MOKOSUITECROSS_NOSTR="MokoSuiteCross - Nostr"
PLG_MOKOSUITECROSS_NOSTR_DESCRIPTION="Cross-post Joomla articles to Nostr."
@@ -1,2 +0,0 @@
PLG_MOKOSUITECROSS_NTFY="MokoSuiteCross - Ntfy Push Notifications"
PLG_MOKOSUITECROSS_NTFY_DESCRIPTION="Cross-post Joomla articles to Ntfy Push Notifications."
@@ -1,2 +0,0 @@
PLG_MOKOSUITECROSS_NTFY="MokoSuiteCross - Ntfy Push Notifications"
PLG_MOKOSUITECROSS_NTFY_DESCRIPTION="Cross-post Joomla articles to Ntfy Push Notifications."
@@ -1,2 +0,0 @@
PLG_MOKOSUITECROSS_PINTEREST="MokoSuiteCross - Pinterest"
PLG_MOKOSUITECROSS_PINTEREST_DESCRIPTION="Cross-post Joomla articles to Pinterest."
@@ -1,2 +0,0 @@
PLG_MOKOSUITECROSS_PINTEREST="MokoSuiteCross - Pinterest"
PLG_MOKOSUITECROSS_PINTEREST_DESCRIPTION="Cross-post Joomla articles to Pinterest."
@@ -1,132 +0,0 @@
<?php
/**
* @package MokoSuiteCross
* @subpackage plg_mokosuitecross_pinterest
* @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\Plugin\MokoSuiteCross\Pinterest\Extension;
defined('_JEXEC') or die;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface;
use Joomla\Event\SubscriberInterface;
/**
* Pinterest service plugin for MokoSuiteCross.
*
* API: https://api.pinterest.com/v5/pins
*/
class PinterestService extends CMSPlugin implements SubscriberInterface, MokoSuiteCrossServiceInterface
{
public static function getSubscribedEvents(): array
{
return ['onMokoSuiteCrossGetServices' => 'onMokoSuiteCrossGetServices'];
}
public function onMokoSuiteCrossGetServices(&$services): void
{
$services[] = $this;
}
public function getServiceType(): string { return 'pinterest'; }
public function getServiceName(): string { return 'Pinterest'; }
public function getMaxLength(): int { return 500; }
public function supportsMedia(): bool { return true; }
public function publish(string $message, array $media, array $credentials, array $params): array
{
$token = $credentials['access_token'] ?? '';
$boardId = $credentials['board_id'] ?? '';
if (empty($token) || empty($boardId)) {
return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Missing access token or board ID']];
}
if (empty($media[0])) {
return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Pinterest requires an image']];
}
$postData = json_encode([
'board_id' => $boardId,
'title' => mb_substr(strip_tags($message), 0, 100),
'description' => mb_substr($message, 0, 500),
'media_source' => [
'source_type' => 'image_url',
'url' => $media[0],
],
]);
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => 'https://api.pinterest.com/v5/pins',
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $postData,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, '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 && $httpCode < 300) {
return ['success' => true, 'platform_post_id' => $data['id'] ?? $data['uri'] ?? '', 'response' => $data];
}
return ['success' => false, 'platform_post_id' => '', 'response' => $data];
}
public function validateCredentials(array $credentials): array
{
$token = $credentials['access_token'] ?? '';
if (empty($token)) {
return ['valid' => false, 'message' => 'Missing access token', 'account_name' => ''];
}
$ch = curl_init('https://api.pinterest.com/v5/user_account');
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
$response = curl_exec($ch);
if ($response === false) {
$curlError = curl_error($ch);
curl_close($ch);
return ['valid' => false, 'message' => 'Connection error: ' . $curlError, 'account_name' => ''];
}
curl_close($ch);
$data = json_decode($response, true) ?: [];
if (!empty($data['username'])) {
return ['valid' => true, 'message' => 'Connected', 'account_name' => $data['username']];
}
return ['valid' => false, 'message' => 'Invalid token', 'account_name' => ''];
}
public function getSupportedMediaTypes(): array
{
return ['image'];
}
}
@@ -1,2 +0,0 @@
PLG_MOKOSUITECROSS_REDDIT="MokoSuiteCross - Reddit"
PLG_MOKOSUITECROSS_REDDIT_DESCRIPTION="Cross-post Joomla articles to Reddit."
@@ -1,2 +0,0 @@
PLG_MOKOSUITECROSS_REDDIT="MokoSuiteCross - Reddit"
PLG_MOKOSUITECROSS_REDDIT_DESCRIPTION="Cross-post Joomla articles to Reddit."
@@ -1,130 +0,0 @@
<?php
/**
* @package MokoSuiteCross
* @subpackage plg_mokosuitecross_reddit
* @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\Plugin\MokoSuiteCross\Reddit\Extension;
defined('_JEXEC') or die;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface;
use Joomla\Event\SubscriberInterface;
/**
* Reddit service plugin for MokoSuiteCross.
*
* API: https://oauth.reddit.com/api/submit
*/
class RedditService extends CMSPlugin implements SubscriberInterface, MokoSuiteCrossServiceInterface
{
public static function getSubscribedEvents(): array
{
return ['onMokoSuiteCrossGetServices' => 'onMokoSuiteCrossGetServices'];
}
public function onMokoSuiteCrossGetServices(&$services): void
{
$services[] = $this;
}
public function getServiceType(): string { return 'reddit'; }
public function getServiceName(): string { return 'Reddit'; }
public function getMaxLength(): int { return 300; }
public function supportsMedia(): bool { return true; }
public function publish(string $message, array $media, array $credentials, array $params): array
{
$accessToken = $credentials['access_token'] ?? '';
$subreddit = $credentials['subreddit'] ?? '';
if (empty($accessToken) || empty($subreddit)) {
return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Missing access token or subreddit']];
}
$title = $params['title'] ?? mb_substr(strip_tags($message), 0, 300);
$postData = http_build_query([
'sr' => $subreddit,
'kind' => 'self',
'title' => $title,
'text' => $message,
]);
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => 'https://oauth.reddit.com/api/submit',
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $postData,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $accessToken,
'User-Agent: MokoSuiteCross/1.0',
],
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 && $httpCode < 300) {
return ['success' => true, 'platform_post_id' => $data['id'] ?? $data['uri'] ?? '', 'response' => $data];
}
return ['success' => false, 'platform_post_id' => '', 'response' => $data];
}
public function validateCredentials(array $credentials): array
{
$token = $credentials['access_token'] ?? '';
if (empty($token)) {
return ['valid' => false, 'message' => 'Missing access token', 'account_name' => ''];
}
$ch = curl_init('https://oauth.reddit.com/api/v1/me');
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, 'User-Agent: MokoSuiteCross/1.0'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
$response = curl_exec($ch);
if ($response === false) {
$curlError = curl_error($ch);
curl_close($ch);
return ['valid' => false, 'message' => 'Connection error: ' . $curlError, 'account_name' => ''];
}
curl_close($ch);
$data = json_decode($response, true) ?: [];
if (!empty($data['name'])) {
return ['valid' => true, 'message' => 'Connected', 'account_name' => 'u/' . $data['name']];
}
return ['valid' => false, 'message' => 'Invalid token', 'account_name' => ''];
}
public function getSupportedMediaTypes(): array
{
return ['image'];
}
}
@@ -1,2 +0,0 @@
PLG_MOKOSUITECROSS_RSSFEED="MokoSuiteCross - RSS Feed"
PLG_MOKOSUITECROSS_RSSFEED_DESCRIPTION="Cross-post Joomla articles to RSS Feed."
@@ -1,2 +0,0 @@
PLG_MOKOSUITECROSS_RSSFEED="MokoSuiteCross - RSS Feed"
PLG_MOKOSUITECROSS_RSSFEED_DESCRIPTION="Cross-post Joomla articles to RSS Feed."
@@ -1,2 +0,0 @@
PLG_MOKOSUITECROSS_SENDGRID="MokoSuiteCross - SendGrid"
PLG_MOKOSUITECROSS_SENDGRID_DESCRIPTION="Cross-post Joomla articles to SendGrid."
@@ -1,2 +0,0 @@
PLG_MOKOSUITECROSS_SENDGRID="MokoSuiteCross - SendGrid"
PLG_MOKOSUITECROSS_SENDGRID_DESCRIPTION="Cross-post Joomla articles to SendGrid."
@@ -1,133 +0,0 @@
<?php
/**
* @package MokoSuiteCross
* @subpackage plg_mokosuitecross_sendgrid
* @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\Plugin\MokoSuiteCross\Sendgrid\Extension;
defined('_JEXEC') or die;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface;
use Joomla\Event\SubscriberInterface;
/**
* SendGrid service plugin for MokoSuiteCross.
*
* API: https://api.sendgrid.com/v3/marketing/singlesends
*/
class SendgridService extends CMSPlugin implements SubscriberInterface, MokoSuiteCrossServiceInterface
{
public static function getSubscribedEvents(): array
{
return ['onMokoSuiteCrossGetServices' => 'onMokoSuiteCrossGetServices'];
}
public function onMokoSuiteCrossGetServices(&$services): void
{
$services[] = $this;
}
public function getServiceType(): string { return 'sendgrid'; }
public function getServiceName(): string { return 'SendGrid'; }
public function getMaxLength(): int { return 0; }
public function supportsMedia(): bool { return true; }
public function publish(string $message, array $media, array $credentials, array $params): array
{
$apiKey = $credentials['api_key'] ?? '';
$listId = $credentials['list_id'] ?? '';
$senderEmail = $credentials['sender_email'] ?? '';
$senderName = $credentials['sender_name'] ?? 'Newsletter';
if (empty($apiKey) || empty($senderEmail)) {
return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Missing API key or sender email']];
}
$subject = $params['subject'] ?? mb_substr(strip_tags($message), 0, 150);
$postData = json_encode([
'name' => $subject,
'send_to' => !empty($listId) ? ['list_ids' => [$listId]] : ['all' => true],
'email_config' => [
'subject' => $subject,
'html_content' => $message,
'sender_id' => null,
'custom_unsubscribe_url' => '',
],
]);
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => 'https://api.sendgrid.com/v3/marketing/singlesends',
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $postData,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey, '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 && $httpCode < 300) {
return ['success' => true, 'platform_post_id' => $data['id'] ?? $data['uri'] ?? '', 'response' => $data];
}
return ['success' => false, 'platform_post_id' => '', 'response' => $data];
}
public function validateCredentials(array $credentials): array
{
$key = $credentials['api_key'] ?? '';
if (empty($key)) {
return ['valid' => false, 'message' => 'Missing API key', 'account_name' => ''];
}
$ch = curl_init('https://api.sendgrid.com/v3/user/profile');
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $key],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
$response = curl_exec($ch);
if ($response === false) {
$curlError = curl_error($ch);
curl_close($ch);
return ['valid' => false, 'message' => 'Connection error: ' . $curlError, 'account_name' => ''];
}
curl_close($ch);
$data = json_decode($response, true) ?: [];
if (!empty($data['first_name'])) {
return ['valid' => true, 'message' => 'Connected', 'account_name' => $data['first_name'] . ' ' . ($data['last_name'] ?? '')];
}
return ['valid' => false, 'message' => 'Invalid API key', 'account_name' => ''];
}
public function getSupportedMediaTypes(): array
{
return ['image'];
}
}
@@ -1,5 +0,0 @@
PLG_MOKOSUITECROSS_SLACK="MokoSuiteCross - Slack"
PLG_MOKOSUITECROSS_SLACK_DESCRIPTION="Cross-post Joomla articles to Slack."
PLG_MOKOSUITECROSS_SLACK_FIELDSET_DEFAULTS="Default Settings"
PLG_MOKOSUITECROSS_SLACK_DEFAULT_WEBHOOK_URL="Default Webhook URL"
PLG_MOKOSUITECROSS_SLACK_DEFAULT_WEBHOOK_URL_DESC="The default MokoSuite Slack webhook URL used when a service is set to 'default' mode."

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