Compare commits

...

155 Commits

Author SHA1 Message Date
gitea-actions[bot] 7d1a939b6a chore(version): pre-release bump to 01.03.01-dev [skip ci]
Publish to Composer / Publish Package (release) Successful in 5s
2026-06-21 22:55:44 +00:00
gitea-actions[bot] 6a3f9c126e chore(version): auto-bump patch 01.02.04-dev [skip ci] 2026-06-21 22:55:32 +00:00
Jonathan Miller ddb378a042 chore: remove automation/ directory
Universal: Auto Version Bump / Version Bump (push) Successful in 10s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 14s
2026-06-21 17:54:50 -05:00
gitea-actions[bot] 20b62b95d8 chore(version): pre-release bump to 01.02.03-dev [skip ci]
Publish to Composer / Publish Package (release) Successful in 6s
2026-06-21 22:27:35 +00:00
gitea-actions[bot] 437a23cec2 chore(version): auto-bump patch 01.02.02-dev [skip ci] 2026-06-21 22:27:19 +00:00
Jonathan Miller 68eab6fdb2 Merge remote-tracking branch 'origin/main' into dev
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Universal: PR Check / Branch Policy (pull_request) Successful in 3s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 10s
Universal: PR Check / Validate PR (pull_request) Failing after 11s
Universal: PR Check / Secret Scan (pull_request) Successful in 15s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Failing after 11s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 1m48s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Failing after 1m43s
Universal: Auto Version Bump / Version Bump (push) Successful in 15s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 19s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 24s
# Conflicts:
#	.mokogitea/workflows/issue-branch.yml
#	CHANGELOG.md
#	README.md
#	source/packages/com_mokoog/mokoog.xml
#	source/packages/plg_content_mokoog/mokoog.xml
#	source/packages/plg_system_mokoog/mokoog.xml
#	source/packages/plg_webservices_mokoog/mokoog.xml
#	source/pkg_mokoog.xml
2026-06-21 17:24:32 -05:00
gitea-actions[bot] b033cfe4e2 chore(version): pre-release bump to 01.02.01-dev [skip ci]
Publish to Composer / Publish Package (release) Successful in 4s
2026-06-21 22:22:24 +00:00
gitea-actions[bot] e86bb5906b chore(version): auto-bump patch 01.01.02-dev [skip ci] 2026-06-21 22:22:14 +00:00
jmiller b310ddfab2 fix: restore updateservers to package manifest for Joomla update site registration
Universal: Auto Version Bump / Version Bump (push) Successful in 9s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 14s
2026-06-21 22:21:48 +00:00
jmiller fa12fa5937 chore: sync security-audit.yml from Template-Generic [skip ci] 2026-06-21 22:03:15 +00:00
jmiller b52867614c chore: sync repo-health.yml from Template-Generic [skip ci] 2026-06-21 22:03:14 +00:00
jmiller b140bc9000 chore: sync pr-check.yml from Template-Generic [skip ci] 2026-06-21 22:03:13 +00:00
jmiller 1a16f9ef8e chore: sync notify.yml from Template-Generic [skip ci] 2026-06-21 22:03:12 +00:00
jmiller 7cdf8b4693 chore: sync issue-branch.yml from Template-Generic [skip ci] 2026-06-21 22:03:12 +00:00
jmiller d4b24fb57e chore: sync gitleaks.yml from Template-Generic [skip ci] 2026-06-21 22:03:11 +00:00
jmiller 6169716154 chore: sync cleanup.yml from Template-Generic [skip ci] 2026-06-21 22:03:10 +00:00
jmiller 5904bea91d chore: sync auto-release.yml from Template-Generic [skip ci] 2026-06-21 22:03:08 +00:00
jmiller 6ef4331f4c chore: sync auto-bump.yml from Template-Generic [skip ci] 2026-06-21 22:03:08 +00:00
gitea-actions[bot] 1533efc2e0 chore: promote changelog [Unreleased] → [01.02.00] 2026-06-21 22:02:01 +00:00
gitea-actions[bot] 86aa6f22b7 chore(release): build 01.02.00 [skip ci]
Publish to Composer / Publish Package (release) Successful in 35s
2026-06-21 22:01:53 +00:00
jmiller f1aa3867d8 v1.0 assessment: fix all blockers, add MokoSuiteShop, close 18 issues (#54) 2026-06-21 22:01:41 +00:00
gitea-actions[bot] 549a3b5599 chore(version): pre-release bump to 01.01.01-dev [skip ci]
Publish to Composer / Publish Package (release) Successful in 26s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 25s
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Failing after 2m51s
2026-06-21 22:00:50 +00:00
gitea-actions[bot] 19e177f1a4 chore(version): auto-bump patch 01.00.09-dev [skip ci] 2026-06-21 22:00:38 +00:00
Jonathan Miller 66b78fd712 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteOpenGraph into dev
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 4s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Universal: PR Check / Secret Scan (pull_request) Successful in 6s
Universal: Security Audit / Dependency Audit (pull_request) Successful in 5s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 12s
Universal: Auto Version Bump / Version Bump (push) Successful in 12s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 14s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Failing after 35s
# Conflicts:
#	.mokogitea/manifest.xml
2026-06-21 17:00:19 -05:00
Jonathan Miller e66e003748 Merge remote-tracking branch 'origin/main' into dev
# Conflicts:
#	.mokogitea/manifest.xml
#	.mokogitea/workflows/auto-release.yml
#	.mokogitea/workflows/cascade-dev.yml
#	.mokogitea/workflows/ci-joomla.yml
#	.mokogitea/workflows/pr-check.yml
#	.mokogitea/workflows/pre-release.yml
#	.mokogitea/workflows/repo-health.yml
#	.mokogitea/workflows/update-server.yml
#	CHANGELOG.md
#	CONTRIBUTING.md
#	README.md
#	source/packages/com_mokoog/mokoog.xml
#	source/packages/plg_content_mokoog/mokoog.xml
#	source/packages/plg_system_mokoog/mokoog.xml
#	source/pkg_mokoog.xml
2026-06-21 16:58:36 -05:00
gitea-actions[bot] dfd0fef3b8 chore(version): pre-release bump to 01.00.08-dev [skip ci] 2026-06-21 21:43:08 +00:00
Jonathan Miller 26328d530e Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteOpenGraph into dev
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Repo Health / Site Health (push) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 5s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 6s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 9s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 12s
Universal: PR Check / Validate PR (pull_request) Successful in 11s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: Security Audit / Dependency Audit (pull_request) Successful in 4s
2026-06-21 16:42:56 -05:00
Jonathan Miller 77da0c5517 fix: remove remaining @ suppression, check Folder::create() returns
- Remove @getimagesize() suppression in ImageHelper, ImageGenerator,
  MokoOG — let PHP report warnings for corrupt/unreadable images
- Add Log::add() when ImageHelper::resize() cannot read image dimensions
- Check Folder::create() return value in ImageGenerator and ImageHelper,
  return graceful fallback if directory creation fails
2026-06-21 16:42:54 -05:00
gitea-actions[bot] 6a928f856f chore(version): pre-release bump to 01.00.07-dev [skip ci] 2026-06-21 21:26:34 +00:00
Jonathan Miller fa75b7d9c4 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteOpenGraph into dev
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 8s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 14s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 9s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 7s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: Security Audit / Dependency Audit (pull_request) Successful in 4s
Universal: PR Check / Validate PR (pull_request) Successful in 25s
2026-06-21 16:26:18 -05:00
Jonathan Miller 46e30c950b fix: address PR review findings — error handling and data integrity
- Add missing language field to batch-generated records
- Wrap batch insert in try-catch to handle duplicate key races
- Add logging to all empty catch blocks (script.php, MokoOG license check)
- Guard loadShopProduct() with try-catch for missing MokoSuiteShop tables
- Guard reviews query in JsonLdBuilder for missing #__mokoshop_reviews
2026-06-21 16:26:13 -05:00
gitea-actions[bot] 77148d2401 chore(version): pre-release bump to 01.00.06-dev [skip ci] 2026-06-21 20:40:44 +00:00
Jonathan Miller 38af92b876 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteOpenGraph into dev
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Site Health (push) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 1s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 4s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 5s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 8s
Universal: Security Audit / Dependency Audit (pull_request) Successful in 4s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 12s
Universal: PR Check / Validate PR (pull_request) Successful in 32s
2026-06-21 15:40:35 -05:00
Jonathan Miller 28d44d6884 fix: undefined $db in findImage(), pass cached product to buildProduct()
- Add missing Factory::getDbo() in findImage() category fallback — would
  cause fatal error on article pages with no images (found in PR review)
- Pass cached product to JsonLdBuilder::buildProduct() to avoid duplicate
  DB query (same pattern as buildArticle with cachedArticle)
- Fix orphaned PHPDoc block for getImageDimensions()
2026-06-21 15:40:01 -05:00
gitea-actions[bot] 3d2d91ace5 chore(version): pre-release bump to 01.00.05-dev [skip ci] 2026-06-21 20:32:13 +00:00
Jonathan Miller 0cc69b7d77 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteOpenGraph into dev
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 6s
2026-06-21 15:32:06 -05:00
Jonathan Miller 1375c5820e docs: update README and CHANGELOG for v1.0 assessment
- Rename MokoJoomOpenGraph to MokoSuiteOpenGraph throughout
- Add MokoSuiteShop integration, Product JSON-LD to feature lists
- Remove dead adapter references (K2, VirtueMart, HikaShop)
- Document all fixes: DB caching, TagTable validation, CSV language,
  batch limit, GD logging, canonical URL API, language filters
2026-06-21 15:31:18 -05:00
gitea-actions[bot] 0e6137b064 chore(version): pre-release bump to 01.00.04-dev [skip ci] 2026-06-21 20:27:36 +00:00
Jonathan Miller e105474c68 Merge remote-tracking branch 'origin/dev' into dev
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 6s
# Conflicts:
#	.mokogitea/workflows/pre-release.yml
2026-06-21 15:25:56 -05:00
Jonathan Miller 7fd716f3a4 chore: normalize workflow line endings for merge 2026-06-21 15:23:30 -05:00
Jonathan Miller ca06c86328 perf: consolidate article DB queries into single cached lookup (#38)
- Add loadArticle() with static per-request cache for article data
- Refactor getArticleDate(), getArticleAuthor() to use cached article
- Refactor findImage() for com_content to use cached article
- Pass cached article to JsonLdBuilder::buildArticle() to skip its query
- Reduces article page DB queries from 5 to 1 for OG tag generation
2026-06-21 11:09:52 -05:00
jmiller 4492d1cbf8 chore: sync pre-release.yml from Template-Generic [skip ci] 2026-06-21 16:05:54 +00:00
Jonathan Miller 7a7041c7f3 fix: remove updateservers from package manifest (#44)
Update server is managed externally, not via static updates.xml.
2026-06-21 11:01:32 -05:00
Jonathan Miller f484675300 fix: batch limit cap, TagTable validation, CSV language column (#42, #43, #52)
- Cap batch process limit to 200 per request to prevent DoS (#42)
- Add TagTable::check() validation: og_type enum, field max lengths,
  canonical_url format, robots directives, content_type pattern (#43)
- Add language column to CSV export headers and data (#52)
- Parse language column on CSV import with format validation
- Include language in duplicate check query to match unique key
2026-06-21 10:57:38 -05:00
Jonathan Miller 8793e6b3f4 feat: add MokoSuiteShop product OG tag support (#53)
- Detect com_mokoshop product views and set og:type to 'product'
- Auto-generate OG tags from CRM product data (name, description, image)
- Add product:price:amount and product:price:currency meta tags
- Add JSON-LD Product schema with offers, SKU, and aggregate ratings
- Load product images from linked #__content article images
- Cache product DB lookups to avoid duplicate queries per request
2026-06-21 10:20:38 -05:00
Jonathan Miller 0afc8b135a fix: replace GD error suppression with logging, remove dead adapters (#49, #36)
- Replace @ error suppression in ImageGenerator with Log::add() warnings
  for missing GD, missing font, corrupt images (#49)
- Add GD extension pre-check before attempting image generation
- Add WebP function_exists() guard for servers without WebP support
- Remove @ suppression from ImageHelper::loadImage() with logging
- Remove unused ContentType adapters (HikaShop, K2, VirtueMart) and
  ContentTypeInterface — not targeting these platforms (#36)
2026-06-21 10:12:00 -05:00
Jonathan Miller 433ecfea71 fix: resolve 3 v1.0 release blockers (#47, #48, #39)
- Add TagsController extending AdminController for admin list
  delete/publish/unpublish operations (#48)
- Add language filter to loadOgDataByType() and loadOgDataByMenu()
  matching the pattern already used in loadOgData() (#47)
- Replace direct $doc->_links access with getHeadData()/setHeadData()
  public API for Joomla forward compatibility (#39)
- Update ISSUES.md with full 2026-06-21 assessment
2026-06-21 10:02:03 -05:00
jmiller d4de07ffd0 chore: sync composer-publish.yml from Template-Generic [skip ci] 2026-06-21 06:35:16 +00:00
jmiller cf372c7fc7 chore: sync workflow-sync-trigger.yml from Template-Generic [skip ci] 2026-06-21 01:29:07 +00:00
jmiller 1721e0b17d chore: sync auto-release.yml from Template-Generic [skip ci] 2026-06-21 01:29:05 +00:00
jmiller 444959617b ci: sync rc-revert.yml from Template-Joomla [skip ci] 2026-06-21 00:15:05 +00:00
jmiller 5db4c7902c ci: sync issue-branch.yml from Template-Joomla [skip ci] 2026-06-21 00:14:36 +00:00
jmiller 8e34b1359a ci: sync ci-joomla.yml from Template-Joomla [skip ci] 2026-06-21 00:14:12 +00:00
jmiller ed57371d80 chore: sync pr-check.yml from Template-Generic [skip ci] 2026-06-20 23:46:47 +00:00
jmiller 2131e7b975 chore: sync gitleaks.yml from Template-Generic [skip ci] 2026-06-20 23:46:46 +00:00
jmiller 04940f502a chore: sync ci-generic.yml from Template-Generic [skip ci] 2026-06-20 23:46:45 +00:00
jmiller 806539f684 chore: sync repo-health.yml from Template-Generic [skip ci] 2026-06-20 22:30:17 +00:00
jmiller 3ea0d001e1 chore: sync rc-revert.yml from Template-Generic [skip ci] 2026-06-20 22:30:17 +00:00
jmiller ec1003db23 chore: sync pr-check.yml from Template-Generic [skip ci] 2026-06-20 22:30:16 +00:00
jmiller 8f30eab945 chore: sync cleanup.yml from Template-Generic [skip ci] 2026-06-20 22:30:15 +00:00
jmiller 8801e2761a ci: sync security-audit.yml from Template-Joomla [skip ci] 2026-06-20 22:26:31 +00:00
jmiller b26864509b ci: sync repo-health.yml from Template-Joomla [skip ci] 2026-06-20 22:26:03 +00:00
jmiller 227779d7b9 ci: sync rc-revert.yml from Template-Joomla [skip ci] 2026-06-20 22:25:54 +00:00
jmiller 8e0afc0b14 ci: sync pr-check.yml from Template-Joomla [skip ci] 2026-06-20 22:24:47 +00:00
jmiller 0fbb3ccc46 ci: sync issue-branch.yml from Template-Joomla [skip ci] 2026-06-20 22:22:21 +00:00
jmiller d6debe63f1 ci: sync cleanup.yml from Template-Joomla [skip ci] 2026-06-20 22:15:36 +00:00
jmiller c95b284791 chore: sync ci-generic.yml from Template-Generic [skip ci] 2026-06-20 21:35:35 +00:00
jmiller 1ea8b55711 ci: sync ci-generic.yml from Template-Joomla [skip ci] 2026-06-20 21:34:02 +00:00
jmiller e226cc9a92 ci: sync cascade-dev.yml from Template-Joomla [skip ci] 2026-06-20 21:31:34 +00:00
jmiller 2d7fd7583b ci: sync branch-cleanup.yml from Template-Joomla [skip ci] 2026-06-20 21:28:09 +00:00
jmiller 10e84d75c7 ci: sync auto-release.yml from Template-Joomla [skip ci] 2026-06-20 21:26:57 +00:00
jmiller b92eb553fc chore: sync workflow-sync-trigger.yml from Template-Generic [skip ci] 2026-06-20 20:53:43 +00:00
jmiller 7df858a263 chore: sync rc-revert.yml from Template-Generic [skip ci] 2026-06-20 20:53:41 +00:00
jmiller a3e7644bdf chore: sync issue-branch.yml from Template-Generic [skip ci] 2026-06-20 20:53:39 +00:00
jmiller 6782ccd26d ci: sync ci-generic.yml from Template-Joomla [skip ci] 2026-06-20 20:35:05 +00:00
jmiller 9f850042fa ci: sync cascade-dev.yml from Template-Joomla [skip ci] 2026-06-20 20:32:52 +00:00
jmiller 3932a33122 ci: sync branch-cleanup.yml from Template-Joomla [skip ci] 2026-06-20 20:31:54 +00:00
jmiller 89eb668b13 ci: sync auto-release.yml from Template-Joomla [skip ci] 2026-06-20 20:30:59 +00:00
jmiller 4808aaa37d ci: sync auto-bump.yml from Template-Joomla [skip ci] 2026-06-20 19:59:09 +00:00
jmiller fe7d2d16c7 ci: sync ci-generic.yml from Template-Joomla [skip ci] 2026-06-20 19:06:00 +00:00
jmiller d627f5c82f ci: sync cascade-dev.yml from Template-Joomla [skip ci] 2026-06-20 19:03:18 +00:00
jmiller dc81e3de33 ci: sync branch-cleanup.yml from Template-Joomla [skip ci] 2026-06-20 19:02:45 +00:00
jmiller 16622fc27c ci: sync auto-release.yml from Template-Joomla [skip ci] 2026-06-20 19:01:05 +00:00
jmiller 8684444478 ci: sync auto-bump.yml from Template-Joomla [skip ci] 2026-06-20 18:53:50 +00:00
jmiller 703290fea8 ci: sync pre-release workflow from Template-Joomla
Generic: Project CI / Lint & Validate (push) Successful in 36s
Generic: Project CI / Tests (push) Has been cancelled
2026-06-20 18:49:30 +00:00
jmiller 1c6c305fb2 ci: add Joomla metadata validation workflow for PRs
Generic: Project CI / Lint & Validate (push) Successful in 6s
Generic: Project CI / Tests (push) Has been cancelled
2026-06-20 18:39:08 +00:00
jmiller 8e24fa353b fix: rename moko-platform to mokocli + changelog promotion in workflows
Generic: Project CI / Lint & Validate (push) Successful in 34s
Generic: Project CI / Tests (push) Has been cancelled
2026-06-20 17:16:47 +00:00
jmiller c9bff7c8a3 fix: rename moko-platform to mokocli + changelog promotion in workflows
Generic: Project CI / Lint & Validate (push) Successful in 4s
Generic: Project CI / Tests (push) Has been cancelled
2026-06-20 17:16:46 +00:00
jmiller e9d0e3a123 fix: rename moko-platform to mokocli + changelog promotion in workflows
Generic: Project CI / Lint & Validate (push) Successful in 33s
Generic: Project CI / Tests (push) Has been cancelled
2026-06-20 17:16:45 +00:00
jmiller 660c01441a fix: rename moko-platform to mokocli + changelog promotion in workflows
Generic: Project CI / Lint & Validate (push) Successful in 5s
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Project CI / Tests (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-20 17:16:45 +00:00
jmiller 3ba82cd272 fix: rename moko-platform to mokocli + changelog promotion in workflows
Generic: Project CI / Lint & Validate (push) Successful in 5s
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Project CI / Tests (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-20 17:16:44 +00:00
jmiller f1a5737739 fix: rename moko-platform to mokocli + changelog promotion in workflows
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Project CI / Lint & Validate (push) Successful in 5s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Project CI / Tests (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-20 17:16:43 +00:00
gitea-actions[bot] 7e9e0ec842 chore(release): build 01.01.00 [skip ci] 2026-06-19 07:15:30 +00:00
jmiller 48facf09d7 Merge pull request 'fix: remove deprecated .mokogitea/manifest.xml' (#46) from fix into main
Generic: Project CI / Lint & Validate (push) Successful in 7s
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Project CI / Tests (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-19 07:10:07 +00:00
Jonathan Miller 3f4b8a0a3d fix: remove deprecated .mokogitea/manifest.xml — metadata managed via API
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 8s
Generic: Project CI / Lint & Validate (pull_request) Successful in 11s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 11s
Universal: PR Check / Validate PR (pull_request) Failing after 8s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 3s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 1m17s
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 9s
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
Generic: Project CI / Tests (pull_request) Has been cancelled
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Has been cancelled
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Has been cancelled
Joomla: Extension CI / PHPStan Analysis (pull_request) Has been cancelled
Joomla: Extension CI / Build RC Pre-Release (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled
2026-06-19 02:05:00 -05:00
jmiller 3612ef2504 chore: sync pre-release.yml from Template-Joomla [skip ci] 2026-06-07 17:58:44 +00:00
jmiller 57f68c5402 chore: sync notify.yml from Template-Joomla [skip ci] 2026-06-07 17:58:43 +00:00
jmiller 93bf02d1d5 chore: sync deploy-manual.yml from Template-Joomla [skip ci] 2026-06-07 17:58:43 +00:00
jmiller d4d3ddd25c chore: sync ci-joomla.yml from Template-Joomla [skip ci] 2026-06-07 17:58:42 +00:00
jmiller f002f30580 chore: sync ci-generic.yml from Template-Joomla [skip ci] 2026-06-07 17:58:42 +00:00
jmiller 0dd7c2120e chore: sync auto-release.yml from Template-Joomla [skip ci] 2026-06-07 17:58:41 +00:00
Jonathan Miller a67cd6da76 fix: gitignore site/ should be /site/ to avoid matching tmpl/site/ 2026-06-06 22:20:08 -05:00
jmiller 537f4539c8 chore: add dlid and blockChildUninstall to package manifest [skip ci] 2026-06-04 22:02:37 +00:00
jmiller 036f8b9877 chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] 2026-06-04 15:58:45 +00:00
jmiller ba22067c56 chore: standardize updateservers URL [skip ci] 2026-06-04 15:48:27 +00:00
jmiller 2c0cbc5a13 chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] 2026-06-04 15:41:27 +00:00
jmiller 6beea230a8 chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] 2026-06-04 15:32:43 +00:00
jmiller cd76449f79 chore: remove updates.xml [skip ci] 2026-06-04 15:27:10 +00:00
jmiller cd0590cee4 chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] 2026-06-04 15:19:32 +00:00
jmiller 05914c0c70 feat(update): migrate update server URL to Gitea Pages [skip ci] 2026-06-04 14:33:58 +00:00
jmiller 6b752babd3 chore: sync .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] 2026-06-04 14:23:54 +00:00
jmiller 466eb7da3c chore: sync .mokogitea/workflows/repo-health.yml from moko-platform [skip ci] 2026-06-04 13:47:36 +00:00
Moko Consulting 2dbb285fdf chore(ci): add CI issue reporter for auto-filing gate failures
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-02 20:38:30 +00:00
Moko Consulting 52d67f5fb1 chore(ci): add CI issue reporter for auto-filing gate failures
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-02 20:38:29 +00:00
Moko Consulting 42ca6325c7 chore(ci): add CI issue reporter for auto-filing gate failures
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-02 20:38:29 +00:00
jmiller ee060243f5 chore: sync .mokogitea/workflows/cascade-dev.yml from moko-platform [skip ci] 2026-05-31 01:42:43 +00:00
jmiller 9bd14d3547 chore: sync updates.xml from development [skip ci] 2026-05-31 01:40:51 +00:00
jmiller b75e7ccf10 chore: sync CONTRIBUTING.md from moko-platform [skip ci] 2026-05-31 01:10:44 +00:00
jmiller 8fd232c959 chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] 2026-05-30 16:02:10 +00:00
jmiller fde6df7398 chore: sync CONTRIBUTING.md from moko-platform [skip ci] 2026-05-30 15:00:19 +00:00
jmiller ec8545c7d3 chore: sync .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] 2026-05-30 14:56:47 +00:00
jmiller 63e87a0c4d chore: sync .mokogitea/workflows/auto-bump.yml from moko-platform [skip ci] 2026-05-30 14:54:49 +00:00
jmiller 12143fc4b1 chore: sync .mokogitea/workflows/auto-bump.yml from moko-platform [skip ci] 2026-05-30 05:51:55 +00:00
jmiller abb9238ebe chore: sync .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] 2026-05-30 03:41:46 +00:00
jmiller d162f2317c chore: sync .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] 2026-05-30 01:15:29 +00:00
jmiller c02bb54759 chore: add .mokogitea/branch-protection.yml from moko-platform [skip ci] 2026-05-29 10:30:43 +00:00
jmiller 4b682c5ebd chore: add CONTRIBUTING.md from moko-platform [skip ci] 2026-05-29 10:28:08 +00:00
jmiller e0b4008ac7 chore: add .mokogitea/workflows/branch-cleanup.yml from moko-platform [skip ci] 2026-05-29 10:26:31 +00:00
jmiller bd220a7d7c chore: sync .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] 2026-05-29 10:25:03 +00:00
jmiller 9d79830b02 chore: sync .mokogitea/workflows/auto-bump.yml from moko-platform [skip ci] 2026-05-29 10:23:34 +00:00
jmiller 8610aa5fcd chore: sync .mokogitea/workflows/pre-release.yml from moko-platform [skip ci] 2026-05-28 20:54:19 +00:00
jmiller 821a3398a5 chore: sync .mokogitea/workflows/update-server.yml from moko-platform [skip ci] 2026-05-28 20:49:12 +00:00
jmiller 354081d7a5 chore: sync .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] 2026-05-28 20:44:29 +00:00
jmiller 1ad79be9b2 chore: sync .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] 2026-05-28 20:38:34 +00:00
gitea-actions[bot] 8fb6a84e81 feat(ci): add version branch creation on stable release [skip ci] 2026-05-27 02:19:25 +00:00
jmiller 7da249ea73 chore(ci): update pre-release.yml from moko-platform [skip ci] 2026-05-26 22:51:25 +00:00
jmiller 9d628412e0 chore(ci): update auto-bump.yml from moko-platform [skip ci] 2026-05-26 22:50:14 +00:00
jmiller 516f7b4832 chore(ci): update auto-release.yml from moko-platform [skip ci] 2026-05-26 22:49:02 +00:00
jmiller d35660b4cf chore(ci): update pre-release.yml from moko-platform [skip ci] 2026-05-26 22:37:36 +00:00
jmiller be05c56d29 chore(ci): update auto-release.yml from moko-platform [skip ci] 2026-05-26 22:36:08 +00:00
jmiller 608ad43242 chore(ci): update auto-bump.yml from moko-platform [skip ci] 2026-05-26 22:25:47 +00:00
jmiller f92b53d24e chore(ci): update auto-release.yml from moko-platform [skip ci] 2026-05-26 22:24:31 +00:00
jmiller 6d6840c68b chore(ci): update pre-release.yml from moko-platform [skip ci] 2026-05-26 22:13:53 +00:00
jmiller 485b0cf696 chore(ci): add auto-bump.yml from moko-platform [skip ci] 2026-05-26 22:12:40 +00:00
jmiller a8805d16f1 chore: sync .mokogitea/workflows/update-server.yml from moko-platform [skip ci] 2026-05-26 20:13:20 +00:00
jmiller e4dad451b4 chore: sync .mokogitea/workflows/pre-release.yml from moko-platform [skip ci] 2026-05-26 20:11:27 +00:00
jmiller 42d9de47d5 fix(ci): use release_package.php for Joomla package builds [skip ci] 2026-05-26 19:54:39 +00:00
jmiller 9753088798 chore(ci): update pre-release.yml from moko-platform [skip ci] 2026-05-26 19:36:23 +00:00
jmiller b01107d6e6 chore(ci): update auto-release.yml from moko-platform [skip ci] 2026-05-26 19:36:22 +00:00
jmiller fd7ccfb927 chore: sync .mokogitea/workflows/update-server.yml from moko-platform [skip ci] 2026-05-26 19:04:39 +00:00
gitea-actions[bot] 2fd22c6030 refactor(ci): clean up auto-release, move logic to CLI [skip ci] 2026-05-25 22:21:03 -05:00
jmiller 8054c6c80b chore: sync .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] 2026-05-26 03:08:06 +00:00
jmiller 7c90f05e9d chore: sync .mokogitea/workflows/pre-release.yml from moko-platform [skip ci] 2026-05-26 03:06:09 +00:00
gitea-actions[bot] 648549ea66 fix(ci): auto-release preserves all update channels [skip ci] 2026-05-25 21:59:27 -05:00
jmiller 52b2d89bc5 feat(ci): add issue-branch.yml [skip ci] 2026-05-25 05:13:01 +00:00
45 changed files with 2882 additions and 844 deletions
+1 -1
View File
@@ -113,7 +113,7 @@ releases/
build/
dist/
out/
site/
/site/
*.map
*.css.map
*.js.map
+251
View File
@@ -0,0 +1,251 @@
# 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
-26
View File
@@ -1,26 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Moko Platform Repository Manifest
See: https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home
-->
<moko-platform xmlns="https://standards.mokoconsulting.tech/moko-platform/1.0" schema-version="1.0">
<identity>
<name>MokoJoomOpenGraph</name>
<display-name>Package - MokoJoomOpenGraph</display-name>
<org>MokoConsulting</org>
<description>Open Graph, SEO meta tags, and social sharing image management for Joomla articles and menu items</description>
<version>01.00.03</version>
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
</identity>
<governance>
<platform>joomla</platform>
<standards-version>05.00.00</standards-version>
<standards-source>https://git.mokoconsulting.tech/MokoConsulting/moko-platform</standards-source>
<last-synced>2026-05-23T22:16:00+00:00</last-synced>
</governance>
<build>
<language>PHP</language>
<package-type>joomla-extension</package-type>
<entry-point>source/</entry-point>
</build>
</moko-platform>
+66
View File
@@ -0,0 +1,66 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.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"
+171 -38
View File
@@ -4,15 +4,15 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Release
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# 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. |
# | |
@@ -21,7 +21,7 @@
# | dolibarr: mod*.class.php, update.txt, dev version reset |
# | generic: README-only, no update stream |
# | |
# +========================================================================+
# +=======================================================================+
name: "Universal: Build & Release"
@@ -51,7 +51,7 @@ permissions:
contents: write
jobs:
# ── PR Opened → Rename branch to RC and build RC release ─────────────────────
# ── PR Opened → Rename branch to RC and build RC release ─────────────────────────
promote-rc:
name: Promote to RC
runs-on: release
@@ -66,25 +66,25 @@ jobs:
token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 1
- name: Setup moko-platform tools
- name: Setup mokocli tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
run: |
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
echo Using pre-installed /opt/moko-platform
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
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/moko-platform-api
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/moko-platform-api
cd /tmp/moko-platform-api
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/moko-platform-api/cli >> $GITHUB_ENV
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
fi
- name: Rename branch to rc
@@ -109,13 +109,47 @@ jobs:
--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) ────────────────────
# ── Merged PR → Build & Release (or promote RC to stable) ─────────────────────────
release:
name: Build & Release Pipeline
runs-on: release
@@ -149,50 +183,131 @@ jobs:
fi
echo "No conflict markers found"
- name: Setup moko-platform tools
- 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/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
echo Using pre-installed /opt/moko-platform
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
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/moko-platform-api
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/moko-platform-api
cd /tmp/moko-platform-api
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/moko-platform-api/cli >> $GITHUB_ENV
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 minor --branch main \
--path . --stability stable ${BUMP_FLAG} --branch main \
--token "${{ secrets.MOKOGITEA_TOKEN }}"
- name: Update release notes from CHANGELOG.md
- name: "Read published version"
id: version
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
# Extract [Unreleased] section from changelog
if [ -f "CHANGELOG.md" ]; then
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
[ -z "$NOTES" ] && NOTES="Stable release"
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
NOTES="Stable release"
echo "tag=v${VERSION}" >> "$GITHUB_OUTPUT"
echo "release_tag=v${VERSION}" >> "$GITHUB_OUTPUT"
fi
echo "branch=main" >> "$GITHUB_OUTPUT"
echo "Published version: ${VERSION}"
- name: "Create semver tag for non-Joomla repos"
id: semver
if: |
steps.version.outputs.skip != 'true' &&
!startsWith(steps.platform.outputs.platform, 'joomla')
run: |
VERSION="${{ steps.version.outputs.version }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
SEMVER_TAG="v${VERSION}"
echo "Creating semver tag: ${SEMVER_TAG}"
# Create the git tag via API
HTTP_CODE=$(curl -sf -o /dev/null -w "%{http_code}" \
-X POST -H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
"${API_BASE}/tags" \
-d "{\"tag_name\":\"${SEMVER_TAG}\",\"target\":\"main\",\"message\":\"Release ${VERSION}\"}" 2>/dev/null || echo "000")
if [ "$HTTP_CODE" = "201" ] || [ "$HTTP_CODE" = "200" ]; then
echo "Created semver tag: ${SEMVER_TAG}"
elif [ "$HTTP_CODE" = "409" ]; then
echo "Semver tag ${SEMVER_TAG} already exists (skipped)"
else
echo "::warning::Failed to create semver tag ${SEMVER_TAG} (HTTP ${HTTP_CODE})"
fi
# Update release body via API
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
"${API_BASE}/releases/tags/stable" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
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
@@ -202,7 +317,7 @@ jobs:
'${API_BASE}/releases/${RELEASE_ID}',
data=payload, method='PATCH',
headers={
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
'Authorization': 'token ${TOKEN}',
'Content-Type': 'application/json'
})
urllib.request.urlopen(req)
@@ -210,6 +325,24 @@ jobs:
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: >-
+48
View File
@@ -0,0 +1,48 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/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
@@ -0,0 +1,10 @@
# 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
@@ -0,0 +1,191 @@
# 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
+477 -60
View File
@@ -35,25 +35,32 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
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
php -v && composer --version
- name: Clone MokoStandards
- name: Setup mokocli tools
env:
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.GA_TOKEN || github.token }}
MOKO_CLONE_HOST: ${{ secrets.MOKOGITEA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
run: |
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
/tmp/mokostandards-api
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
- name: Install dependencies
env:
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN || github.token }}"}}'
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || secrets.GA_TOKEN || github.token }}"}}'
run: |
if [ -f "composer.json" ]; then
composer install \
@@ -67,7 +74,7 @@ jobs:
- name: PHP syntax check
run: |
ERRORS=0
for DIR in source/ src/ htdocs/; do
for DIR in src/ htdocs/; do
if [ -d "$DIR" ]; then
FOUND=1
while IFS= read -r -d '' FILE; do
@@ -124,13 +131,8 @@ jobs:
echo "Manifest is well-formed XML." >> $GITHUB_STEP_SUMMARY
fi
# Check required tags
REQUIRED_TAGS="name version author"
# namespace is only required for non-package extensions
if ! grep -q 'type="package"' "$MANIFEST" 2>/dev/null; then
REQUIRED_TAGS="$REQUIRED_TAGS namespace"
fi
for TAG in $REQUIRED_TAGS; do
# Check required tags: name, version, author
for TAG in name version author; do
if ! grep -q "<${TAG}>" "$MANIFEST" 2>/dev/null; then
echo "Missing required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
@@ -138,6 +140,19 @@ 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
@@ -202,44 +217,13 @@ jobs:
echo "**Language file check passed.**" >> $GITHUB_STEP_SUMMARY
fi
- name: Check en-GB and en-US language directories exist
run: |
echo "### Language Directory Check" >> $GITHUB_STEP_SUMMARY
ERRORS=0
for DIR in source/ src/ htdocs/; do
[ -d "$DIR" ] || continue
# Find all language directories
while IFS= read -r -d '' LANG_DIR; do
HAS_GB=false
HAS_US=false
[ -d "${LANG_DIR}/en-GB" ] && HAS_GB=true
[ -d "${LANG_DIR}/en-US" ] && HAS_US=true
if [ "$HAS_GB" = false ]; then
echo "Missing \`en-GB\` in: \`${LANG_DIR}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
if [ "$HAS_US" = false ]; then
echo "Missing \`en-US\` in: \`${LANG_DIR}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
done < <(find "$DIR" -type d -name "language" -print0)
done
if [ "${ERRORS}" -gt 0 ]; then
echo "**${ERRORS} missing language director(ies).**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "All language directories have en-GB and en-US." >> $GITHUB_STEP_SUMMARY
fi
- name: Check index.html files in directories
run: |
echo "### Index.html Check" >> $GITHUB_STEP_SUMMARY
MISSING=0
CHECKED=0
for DIR in source/ src/ htdocs/; do
for DIR in src/ htdocs/; do
if [ -d "$DIR" ]; then
while IFS= read -r -d '' SUBDIR; do
CHECKED=$((CHECKED + 1))
@@ -252,7 +236,7 @@ jobs:
done
if [ "${CHECKED}" -eq 0 ]; then
echo "No source/, src/, or htdocs/ directories found — skipping." >> $GITHUB_STEP_SUMMARY
echo "No src/ or htdocs/ directories found — skipping." >> $GITHUB_STEP_SUMMARY
elif [ "${MISSING}" -gt 0 ]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "**${MISSING} director(ies) missing index.html out of ${CHECKED} checked.**" >> $GITHUB_STEP_SUMMARY
@@ -261,14 +245,417 @@ 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
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@v4
- name: Validate release readiness
run: |
@@ -276,8 +663,8 @@ jobs:
echo "" >> $GITHUB_STEP_SUMMARY
ERRORS=0
# Extract version from README.md (supports both FILE INFORMATION block and HTML comment format)
README_VERSION=$(sed -n 's/.*VERSION:\s*\([0-9]\{2\}\.[0-9]\{2\}\.[0-9]\{2\}\).*/\1/p' README.md | head -1)
# Extract version from README.md
README_VERSION=$(grep -oP '^\s*VERSION:\s*\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' README.md | head -1)
if [ -z "$README_VERSION" ]; then
echo "No VERSION found in README.md FILE INFORMATION block." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
@@ -301,7 +688,7 @@ jobs:
echo "Manifest: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY
# Check <version> matches README VERSION
MANIFEST_VERSION=$(sed -n 's/.*<version>\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1)
MANIFEST_VERSION=$(grep -oP '<version>\K[^<]+' "$MANIFEST" | head -1)
if [ -z "$MANIFEST_VERSION" ]; then
echo "No \`<version>\` tag in manifest." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
@@ -374,15 +761,19 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@v4
- name: Setup PHP ${{ matrix.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
php -v && composer --version
- name: Install dependencies
env:
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN || github.token }}"}}'
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || secrets.GA_TOKEN || github.token }}"}}'
run: |
if [ -f "composer.json" ]; then
composer install \
@@ -420,14 +811,19 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@v4
- name: Setup PHP
run: php -v && composer --version
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
php -v && composer --version
- name: Install dependencies
env:
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN || github.token }}"}}'
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || secrets.GA_TOKEN || github.token }}"}}'
run: |
if [ -f "composer.json" ]; then
composer install --no-interaction --prefer-dist --optimize-autoloader
@@ -450,7 +846,7 @@ jobs:
# Determine source directory
SRC_DIR=""
for DIR in source/ src/ htdocs/ lib/; do
for DIR in src/ htdocs/ lib/; do
if [ -d "$DIR" ]; then
SRC_DIR="$DIR"
break
@@ -458,7 +854,7 @@ jobs:
done
if [ -z "$SRC_DIR" ]; then
echo "No source directory found (source/, src/, htdocs/, lib/) — skipping." >> $GITHUB_STEP_SUMMARY
echo "No source directory found (src/, htdocs/, lib/) — skipping." >> $GITHUB_STEP_SUMMARY
exit 0
fi
@@ -484,3 +880,24 @@ 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
+9 -9
View File
@@ -4,8 +4,8 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Maintenance
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# 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
@@ -33,17 +33,17 @@ jobs:
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.MOKOGITEA_TOKEN }}
token: ${{ secrets.GA_TOKEN }}
- name: Delete merged branches
env:
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
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 ${GITEA_TOKEN}" \
BRANCHES=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
"${API}/branches?limit=50" | jq -r '.[].name')
DELETED=0
@@ -56,7 +56,7 @@ jobs:
# 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 ${GITEA_TOKEN}" \
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
"${API}/branches/${BRANCH}" 2>/dev/null || true
DELETED=$((DELETED + 1))
fi
@@ -66,20 +66,20 @@ jobs:
- name: Clean old workflow runs
env:
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
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 ${GITEA_TOKEN}" \
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 ${GITEA_TOKEN}" \
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
"${API}/actions/runs/${RUN_ID}" 2>/dev/null || true
DELETED=$((DELETED + 1))
done
+76
View File
@@ -0,0 +1,76 @@
# 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
@@ -0,0 +1,126 @@
# 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
+2 -6
View File
@@ -4,8 +4,8 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Security
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# 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
@@ -25,10 +25,6 @@
name: "Universal: Secret Scanning"
on:
pull_request:
branches:
- main
- 'dev/**'
schedule:
- cron: '0 5 * * 1' # Weekly Monday 05:00 UTC
workflow_dispatch:
+73
View File
@@ -0,0 +1,73 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Automation
# VERSION: 01.03.01
# BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch"
on:
issues:
types: [opened]
permissions:
contents: write
issues: write
env:
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
jobs:
create-branch:
name: Create feature branch
runs-on: ubuntu-latest
steps:
- name: Create branch and comment
run: |
TOKEN="${{ secrets.GA_TOKEN }}"
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
ISSUE_NUM="${{ github.event.issue.number }}"
ISSUE_TITLE="${{ github.event.issue.title }}"
# Build slug from title: lowercase, replace non-alnum with dash, trim
SLUG=$(echo "${ISSUE_TITLE}" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//' | cut -c1-40)
BRANCH="feature/${ISSUE_NUM}-${SLUG}"
# Check dev branch exists
DEV_EXISTS=$(curl -sf -o /dev/null -w '%{http_code}' \
-H "Authorization: token ${TOKEN}" \
"${API}/branches/dev" 2>/dev/null || echo "000")
if [ "${DEV_EXISTS}" != "200" ]; then
echo "No dev branch -- skipping"
exit 0
fi
# Create branch from dev
HTTP=$(curl -sf -o /dev/null -w '%{http_code}' -X POST \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
"${API}/branches" \
-d "{\"new_branch_name\":\"${BRANCH}\",\"old_branch_name\":\"dev\"}" 2>/dev/null || echo "000")
if [ "${HTTP}" = "201" ]; then
echo "Created branch: ${BRANCH}"
# Comment on issue with branch link
REPO_URL="${GITEA_URL}/${{ github.repository }}"
BODY="Branch created: [\`${BRANCH}\`](${REPO_URL}/src/branch/${BRANCH})\n\n\`\`\`bash\ngit fetch origin\ngit checkout ${BRANCH}\n\`\`\`"
curl -sf -X POST \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
"${API}/issues/${ISSUE_NUM}/comments" \
-d "{\"body\":\"${BODY}\"}" > /dev/null 2>&1
echo "Commented on issue #${ISSUE_NUM}"
else
echo "Failed to create branch (HTTP ${HTTP}) -- may already exist"
fi
+2 -2
View File
@@ -4,8 +4,8 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Notifications
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# 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
+31 -6
View File
@@ -96,6 +96,32 @@ 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
@@ -159,11 +185,11 @@ jobs:
echo "::error file=${file}::Missing JEXEC guard: ${file}"
ERRORS=$((ERRORS + 1))
fi
done < <(find . -name "*.php" \( -path "*/source/*" -o -path "*/src/*" \) -not -path "./.git/*" -not -path "./vendor/*" -print0)
done < <(find . -name "*.php" -path "*/src/*" -not -path "./.git/*" -not -path "./vendor/*" -print0)
if [ "$ERRORS" -gt 0 ]; then
echo "::error::${ERRORS} PHP file(s) missing defined('_JEXEC') or die guard"
echo "## JEXEC Guard Check: Failed" >> $GITHUB_STEP_SUMMARY
echo "${ERRORS} file(s) in source/ are missing the Joomla execution guard." >> $GITHUB_STEP_SUMMARY
echo "${ERRORS} file(s) in src/ are missing the Joomla execution guard." >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "JEXEC guard: OK"
@@ -220,7 +246,7 @@ jobs:
echo "joomla.asset.json: valid"
fi
# Validate all XML files in source/ are well-formed
# Validate all XML files in src/ are well-formed
XML_ERRORS=0
if command -v php &> /dev/null; then
while IFS= read -r -d '' xmlfile; do
@@ -451,11 +477,10 @@ jobs:
- name: Verify package source
run: |
SOURCE_DIR="source"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="src"
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
if [ ! -d "$SOURCE_DIR" ]; then
echo "::warning::No source/, src/, or htdocs/ directory"
echo "::warning::No src/ or htdocs/ directory"
exit 0
fi
FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
@@ -0,0 +1,71 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Validation
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /templates/workflows/joomla/pr-metadata-check.yml.template
# VERSION: 01.00.00
# BRIEF: Validate MokoGitea metadata matches Joomla extension manifest on PRs
name: "Joomla: Metadata Validation"
on:
pull_request:
types: [opened, synchronize, reopened, converted_to_draft, ready_for_review]
permissions:
contents: read
env:
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
jobs:
validate-metadata:
name: "Validate Joomla Metadata"
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup mokocli tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
run: |
if [ -f /opt/mokocli/cli/joomla_metadata_validate.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then
echo Using pre-installed /opt/mokocli
echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV
else
echo Falling back to fresh clone
if ! command -v composer > /dev/null 2>&1; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
fi
rm -rf /tmp/mokocli
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
fi
- name: Validate metadata against Joomla manifest
env:
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
run: |
php ${MOKO_CLI}/joomla_metadata_validate.php \
--path . \
--token "${GITEA_TOKEN}" \
--org "${GITEA_ORG}" \
--repo "${GITEA_REPO}" \
--api-base "${GITEA_URL}/api/v1" \
--ci
if [ $? -ne 0 ]; then
echo "::error::Joomla metadata mismatch — update delivery will fail. Run 'php cli/joomla_metadata_validate.php' locally to see details."
exit 1
fi
+28 -12
View File
@@ -4,8 +4,8 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# INGROUP: mokocli.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /templates/workflows/universal/pre-release.yml.template
# VERSION: 05.01.00
# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches
@@ -60,25 +60,25 @@ jobs:
token: ${{ secrets.MOKOGITEA_TOKEN }}
ref: ${{ github.ref_name }}
- name: Setup moko-platform tools
- name: Setup mokocli tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
run: |
# Use pre-installed /opt/moko-platform if available (updated by cron every 6h)
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/cli/manifest_element.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
echo Using pre-installed /opt/moko-platform
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
# 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/moko-platform-api
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git
git clone --depth 1 --branch main --quiet $CLONE_URL /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
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
@@ -88,8 +88,20 @@ jobs:
php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true
php ${MOKO_CLI}/manifest_read.php --path . --github-output
- name: Check platform eligibility (Joomla only)
id: eligibility
run: |
PLATFORM="${{ steps.platform.outputs.platform }}"
if [[ "$PLATFORM" == joomla* ]] || [[ "$PLATFORM" == "joomla" ]]; then
echo "proceed=true" >> "$GITHUB_OUTPUT"
else
echo "proceed=false" >> "$GITHUB_OUTPUT"
echo "::notice::Platform '$PLATFORM' — non-Joomla, skipping pre-release auto-bump"
fi
- name: Resolve metadata and bump version
id: meta
if: steps.eligibility.outputs.proceed == 'true'
run: |
# Auto-detect stability from branch name on push, or use input on dispatch
if [ "${{ github.event_name }}" = "push" ]; then
@@ -166,6 +178,7 @@ jobs:
- name: Create release
id: release
if: steps.eligibility.outputs.proceed == 'true'
run: |
TAG="${{ steps.meta.outputs.tag }}"
VERSION="${{ steps.meta.outputs.version }}"
@@ -176,6 +189,7 @@ jobs:
--repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease
- name: Update release notes from CHANGELOG.md
if: steps.eligibility.outputs.proceed == 'true'
run: |
TAG="${{ steps.meta.outputs.tag }}"
VERSION="${{ steps.meta.outputs.version }}"
@@ -212,6 +226,7 @@ jobs:
- name: Build package and upload
id: package
if: steps.eligibility.outputs.proceed == 'true'
run: |
VERSION="${{ steps.meta.outputs.version }}"
TAG="${{ steps.meta.outputs.tag }}"
@@ -225,6 +240,7 @@ jobs:
# No need to build, commit, or sync updates.xml from workflows
- name: "Delete lesser pre-release channels (cascade)"
if: steps.eligibility.outputs.proceed == 'true'
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
+66
View File
@@ -0,0 +1,66 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /.mokogitea/workflows/rc-revert.yml
# VERSION: 09.23.00
# BRIEF: Rename rc/ branch back to dev/ when PR is closed without merge
name: "RC Revert"
on:
pull_request:
types: [closed]
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
revert:
name: Rename rc/ back to dev/
runs-on: ubuntu-latest
if: >-
github.event.pull_request.merged == false &&
startsWith(github.event.pull_request.head.ref, 'rc/')
steps:
- name: Rename branch
run: |
BRANCH="${{ github.event.pull_request.head.ref }}"
SUFFIX="${BRANCH#rc/}"
DEV_BRANCH="dev/${SUFFIX}"
API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
# Create dev/ branch from rc/ branch
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X POST \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"new_branch_name\": \"${DEV_BRANCH}\", \"old_branch_name\": \"${BRANCH}\"}" \
"${API}" 2>/dev/null || true)
if [ "$STATUS" = "201" ]; then
echo "Created branch: ${DEV_BRANCH}" >> $GITHUB_STEP_SUMMARY
else
echo "::error::Failed to create ${DEV_BRANCH} from ${BRANCH} (HTTP ${STATUS})"
exit 1
fi
# Delete rc/ branch
ENCODED=$(php -r "echo rawurlencode('${BRANCH}');")
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \
-H "Authorization: token ${TOKEN}" \
"${API}/${ENCODED}" 2>/dev/null || true)
if [ "$STATUS" = "204" ]; then
echo "Deleted branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
else
echo "::warning::Failed to delete ${BRANCH} (HTTP ${STATUS})"
fi
echo "### RC Reverted" >> $GITHUB_STEP_SUMMARY
echo "${BRANCH} → ${DEV_BRANCH}" >> $GITHUB_STEP_SUMMARY
+8 -9
View File
@@ -7,8 +7,8 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Validation
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# INGROUP: mokocli.Validation
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli
# 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,7 +33,8 @@ on:
- scripts
- repo
pull_request:
push:
branches:
- main
permissions:
contents: read
@@ -296,19 +297,17 @@ jobs:
missing_required=()
missing_optional=()
# Source directory: source/, src/, or htdocs/ (any is valid for extension repos)
# Source directory: src/ or htdocs/ (either is valid for extension repos)
SOURCE_DIR=""
if [ -d "source" ]; then
SOURCE_DIR="source"
elif [ -d "src" ]; then
if [ -d "src" ]; then
SOURCE_DIR="src"
elif [ -d "htdocs" ]; then
SOURCE_DIR="htdocs"
elif [ -d "deploy" ] || [ -d "cli" ] || [ -d "monitoring" ]; then
# Platform/tooling repos don't need source/
# Platform/tooling repos don't need src/
SOURCE_DIR=""
else
missing_required+=("source/, src/, or htdocs/ (source directory required)")
missing_required+=("src/ or htdocs/ (source directory required)")
fi
for item in "${required_artifacts[@]}"; do
+2 -18
View File
@@ -4,8 +4,8 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Security
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# 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
@@ -80,19 +80,3 @@ jobs:
-H "Priority: high" \
-d "Security audit found vulnerabilities. Review dependency updates." \
"${NTFY_URL}/${NTFY_TOPIC}" || true
- name: Joomla version audit
if: always()
run: |
if [ -f "monitoring/joomla-version-audit.php" ] && [ -n "$JOOMLA_SITES" ]; then
echo "$JOOMLA_SITES" > /tmp/sites.json
php monitoring/joomla-version-audit.php --sites /tmp/sites.json || true
echo "### Joomla Version Audit" >> $GITHUB_STEP_SUMMARY
rm -f /tmp/sites.json
else
echo "Joomla audit skipped (no script or JOOMLA_SITES_JSON not configured)"
fi
env:
JOOMLA_SITES: ${{ vars.JOOMLA_SITES_JSON }}
+312
View File
@@ -0,0 +1,312 @@
# 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
@@ -0,0 +1,73 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /.mokogitea/workflows/workflow-sync-trigger.yml
# VERSION: 01.01.00
# BRIEF: Trigger workflow sync to live repos when a PR is merged to main
name: "Universal: Workflow Sync Trigger"
on:
pull_request:
types: [closed]
branches:
- main
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
sync:
name: Sync workflows to live repos
runs-on: ubuntu-latest
if: >-
github.event.pull_request.merged == true &&
!contains(github.event.pull_request.title, '[skip sync]')
steps:
- name: Determine platform from repo name
id: platform
run: |
REPO="${{ github.event.repository.name }}"
case "$REPO" in
Template-Joomla) PLATFORM="joomla" ;;
Template-Dolibarr) PLATFORM="dolibarr" ;;
Template-Go) PLATFORM="go" ;;
Template-MCP) PLATFORM="mcp" ;;
Template-Generic) PLATFORM="" ;;
*) PLATFORM="" ;;
esac
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
echo "Platform: ${PLATFORM:-all}"
- name: Clone mokocli
env:
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
run: |
GITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}"
git clone --depth 1 "${GITEA_URL}/MokoConsulting/mokocli.git" /tmp/mokocli
- name: Install dependencies
run: |
cd /tmp/mokocli
composer install --no-dev --no-interaction --quiet 2>/dev/null || true
- name: Run workflow sync
env:
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
run: |
ARGS="--token ${MOKOGITEA_TOKEN}"
ARGS="${ARGS} --org ${{ vars.GITEA_ORG || github.repository_owner }}"
ARGS="${ARGS} --phase repos"
PLATFORM="${{ steps.platform.outputs.platform }}"
if [ -n "$PLATFORM" ]; then
ARGS="${ARGS} --platform-filter ${PLATFORM}"
fi
php /tmp/mokocli/cli/workflow_sync.php ${ARGS}
+17 -5
View File
@@ -1,8 +1,8 @@
# Changelog
<!-- VERSION: 01.00.03 -->
<!-- VERSION: 01.03.01 -->
All notable changes to MokoJoomOpenGraph will be documented in this file.
All notable changes to MokoSuiteOpenGraph will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
@@ -25,9 +25,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- CSV import/export for bulk OG tag management (#12)
- OG image text overlay generator (#7)
- Multilingual OG tag support with per-language records (#11)
- JSON-LD structured data: Article, WebPage, BreadcrumbList schemas (#6)
- JSON-LD structured data: Article, Product, WebPage, BreadcrumbList schemas (#6)
- Social platform debugger quick links (Facebook, LinkedIn, Google) (#9)
- Content type adapter architecture for K2, VirtueMart, HikaShop (#5)
- MokoSuiteShop product OG tag support with pricing meta and JSON-LD Product schema (#53)
- WhatsApp and Telegram link preview optimization (#10)
- Category-level OG tag support (#4)
- Batch OG tag generation for existing articles (#1)
@@ -40,5 +40,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- Facebook App ID and Telegram channel support
- Database table `#__mokoog_tags` with multilingual unique key
### Changed
- Consolidated article DB queries into single cached lookup — 5 queries reduced to 1 (#38)
- Dynamic `og:image:width`/`og:image:height` from actual image dimensions instead of hardcoded (#39)
- Replace GD `@` error suppression with `Log::add()` warnings (#49)
- TagTable::check() validates og_type, field lengths, canonical_url, robots directives (#43)
- CSV import/export now includes language column for multilingual support (#52)
- Batch process limit capped at 200 per request (#42)
- Canonical URL replacement uses public `getHeadData()`/`setHeadData()` API (#39)
- Language-aware queries on `loadOgDataByType()` and `loadOgDataByMenu()` (#47)
### Removed
- Removed deploy-manual.yml workflow — using Joomla update server for distribution
- Removed dead ContentType adapters (K2, VirtueMart, HikaShop) — not targeting these platforms (#36)
- Removed `<updateservers>` from package manifest — managed externally (#44)
- Removed deploy-manual.yml workflow
+318
View File
@@ -0,0 +1,318 @@
# MokoSuiteOpenGraph — Code Assessment Issues
Generated: 2026-06-06
Updated: 2026-06-21
Reviewed: Full codebase (all PHP, SQL, XML, JS, CSS, templates)
---
## Status Legend
- FIXED — Verified resolved in codebase
- OPEN — Still present, needs work
- WONTFIX — Intentional or acceptable as-is
---
## Bugs
### BUG-01: Batch generation offset pagination skips articles — FIXED
**Severity:** High
**File:** `source/packages/com_mokoog/src/Controller/BatchController.php:89`
The `process()` method now correctly uses `$db->setQuery($query, 0, $limit)` with a comment explaining that processed articles are automatically excluded by the LEFT JOIN filter.
---
### BUG-02: License key session flag set before check completes — FIXED
**Severity:** Medium
**File:** `source/packages/plg_system_mokoog/src/Extension/MokoOG.php:543`
Session flag is now set after the DB query succeeds, inside the try block but after query setup. If the query throws, the catch block runs without the flag being set.
---
### BUG-03: Hardcoded og:image dimensions are often wrong — FIXED
**Severity:** Medium
**File:** `source/packages/plg_system_mokoog/src/Extension/MokoOG.php:129-134`
Now uses `$this->getImageDimensions($image)` which calls `getimagesize()` to detect actual dimensions. Dimension meta tags only emitted when dimensions are successfully detected.
---
### BUG-04: `strlen()` vs `mb_strlen()` inconsistency in truncation — FIXED
**Severity:** Low
**Files:** MokoOG.php, BatchController.php, HikaShopAdapter.php, K2Adapter.php
All instances now consistently use `mb_strlen()` for length checks with `mb_substr()` for truncation.
---
### BUG-05: `ImageGenerator::wrapText()` can produce broken output — FIXED
**Severity:** Low
**File:** `source/packages/plg_system_mokoog/src/Helper/ImageGenerator.php:156`
Now checks `mb_strlen($lines[2]) > 3` before truncating. Short lines get `'...'` appended instead.
---
## Potential Issues
### ISSUE-01: ContentType adapters exist but are never wired up — OPEN
**Severity:** High (wasted code)
**Files:**
- `source/packages/com_mokoog/src/ContentType/ContentTypeInterface.php`
- `source/packages/com_mokoog/src/ContentType/HikaShopAdapter.php`
- `source/packages/com_mokoog/src/ContentType/K2Adapter.php`
- `source/packages/com_mokoog/src/ContentType/VirtueMartAdapter.php`
The system plugin (`MokoOG.php`) still never references or loads these adapters. The `findImage()` and `loadOgData()` methods only handle `com_content`. Third-party content types get no auto-generated OG tags.
**Action:** Wire adapters into the system plugin's `onBeforeCompileHead` flow, or remove them if not planned for v1.
---
### ISSUE-02: `applySeoTags()` accesses internal `$doc->_links` property — OPEN
**Severity:** Medium
**File:** `source/packages/plg_system_mokoog/src/Extension/MokoOG.php:257-259`
Still directly accessing `$doc->_links` (protected/internal property). Fragile across Joomla versions.
**Fix:** Use `$doc->getHeadData()` to read links and `$doc->addHeadLink()` with proper clearing logic.
---
### ISSUE-03: No input sanitization on OG values before output — OPEN
**Severity:** Medium
**File:** `source/packages/plg_content_mokoog/src/Extension/MokoOGContent.php`
No `htmlspecialchars()` or `InputFilter` found in the content plugin's save path. While Joomla's `setMetaData()` escapes on output, defense-in-depth recommends sanitizing on input.
**Fix:** Apply `htmlspecialchars()` or Joomla's `InputFilter` when saving OG data.
---
### ISSUE-04: `loadOgDataByType()` and `loadOgDataByMenu()` ignore language — OPEN
**Severity:** Medium
**Files:**
- `source/packages/plg_system_mokoog/src/Extension/MokoOG.php:324-337` (`loadOgDataByType`)
- `source/packages/plg_system_mokoog/src/Extension/MokoOG.php:346-359` (`loadOgDataByMenu`)
These methods still have no language filter. On multilingual sites, category fallback or menu OG data could come from any language. The unique key is now `(content_type, content_id, language)` but these queries don't filter by language, so `loadObject()` returns an arbitrary match.
**Fix:** Add the same language filter pattern used in `loadOgData()`.
---
### ISSUE-05: VirtueMart adapter interpolates language into table name — OPEN (low risk)
**Severity:** Low (defense-in-depth)
**File:** `source/packages/com_mokoog/src/ContentType/VirtueMartAdapter.php:34,47`
Language tag is interpolated into the table name. While `quoteName()` wraps the result, the language tag itself is not validated against an allowlist.
**Fix:** Validate tag format with a regex before interpolation.
---
### ISSUE-06: No admin list controller for publish/delete operations — OPEN
**Severity:** Medium
**File:** `source/packages/com_mokoog/src/Controller/`
No `TagsController extends AdminController` exists. The admin list view toolbar buttons for delete/publish/unpublish will produce task routing errors.
**Fix:** Add a `TagsController extends AdminController` with proper CSRF and ACL checks.
---
### ISSUE-07: CSV import/export does not handle `language` column — OPEN
**Severity:** Low
**File:** `source/packages/com_mokoog/src/Controller/ImportExportController.php`
No reference to `language` found in the controller. Export omits the column, import creates records with default `*` language. Multilingual sites cannot bulk import/export language-specific OG data.
**Fix:** Add `language` as a column in export, and parse it on import with a fallback to `*`.
---
### ISSUE-08: No ACL check in content plugin form injection — WONTFIX
**Severity:** Low
**File:** `source/packages/plg_content_mokoog/src/Extension/MokoOGContent.php:49`
Any user who can edit an article can modify OG tags. This is acceptable behavior for most sites — if you can edit the article, you should be able to control its social sharing appearance.
---
## New Issues (Found 2026-06-21)
### ISSUE-09: ImageGenerator uses @ error suppression on GD functions
**Severity:** Medium
**File:** `source/packages/plg_system_mokoog/src/Helper/ImageGenerator.php`
All GD library calls use the `@` suppression operator, making debugging difficult. If the GD extension is missing or a font file is not found, failures are completely silent.
**Fix:** Replace `@` suppression with proper error checking and logging via `Log::add()`.
---
### ISSUE-10: No TTF font file bundled or documented
**Severity:** Medium
**File:** `source/packages/plg_system_mokoog/src/Helper/ImageGenerator.php`
The image generator requires a TTF font file for text overlay, but no font is included in the package and no fallback or documentation exists for configuring the font path.
**Fix:** Bundle a permissively-licensed font (e.g., Open Sans, Noto Sans) or document the required configuration.
---
### ISSUE-11: ImageGenerator cache grows unbounded
**Severity:** Low
**File:** `source/packages/plg_system_mokoog/src/Helper/ImageGenerator.php`
Generated images in `images/mokoog/generated/` are never cleaned up. On sites with many articles, this directory grows indefinitely.
**Fix:** Add a cleanup CLI command or admin button (see FEAT-07), or implement LRU/TTL-based cache eviction.
---
### ISSUE-12: JSON-LD missing common schema types
**Severity:** Low
**File:** `source/packages/plg_system_mokoog/src/Helper/JsonLdBuilder.php`
Only 4 schema types are implemented (Article, WebPage, BreadcrumbList, Organization). Missing: NewsArticle, BlogPosting, Product, VideoObject, Event — some of which correspond to existing `og_type` dropdown values.
**Fix:** Add at least NewsArticle and BlogPosting as Article subtypes.
---
### ISSUE-13: No API input validation beyond field whitelisting
**Severity:** Low
**Files:**
- `source/packages/com_mokoog/api/src/Controller/TagsController.php`
- `source/packages/com_mokoog/api/src/View/Tags/JsonapiView.php`
The REST API exposes full CRUD but has no validation for field content (e.g., max lengths, valid URLs for og_image/canonical_url, valid og_type values).
**Fix:** Add validation rules matching the form XML constraints.
---
## Feature Expansion Opportunities
### FEAT-01: Wire up ContentType adapter system — NOT IMPLEMENTED
Connect the existing `ContentTypeInterface` adapters to the system plugin so HikaShop products, K2 items, and VirtueMart products automatically get OG tags. Blocked by ISSUE-01.
---
### FEAT-02: Admin edit view for individual OG tag records — NOT IMPLEMENTED
A `TagModel` and `tag.xml` form exist but there's no edit template (`tmpl/tag/`) or `TagController`. Users can only manage OG tags through article/menu editors.
---
### FEAT-03: Publish/unpublish toggle in admin list — NOT IMPLEMENTED
Blocked by ISSUE-06 (no TagsController). The list view shows published status as text but has no clickable toggle.
---
### FEAT-04: Actual image dimension detection for og:image meta — FIXED
Implemented via `getImageDimensions()` method using `getimagesize()`. See BUG-03.
---
### FEAT-05: Duplicate OG tag detection — NOT IMPLEMENTED
No detection for conflicting OG meta tags from other extensions.
---
### FEAT-06: Support og:video and og:audio URLs — NOT IMPLEMENTED
No `og_video` or `og_audio` columns, form fields, or rendering logic found anywhere in the codebase.
---
### FEAT-07: Generated image cache cleanup — NOT IMPLEMENTED
No CLI command or admin purge button. See ISSUE-11.
---
### FEAT-08: Sitemap integration — NOT IMPLEMENTED
No sitemap generation or integration exists.
---
### FEAT-09: Social share preview in admin list — NOT IMPLEMENTED
No thumbnails or inline validation in the admin list view. Live preview only exists in the article/menu editor (via plg_content_mokoog).
---
### FEAT-10: Bulk OG tag editing — NOT IMPLEMENTED
No batch edit modal for selecting multiple items and changing common fields.
---
## Security Fixes (from CHANGELOG [Unreleased])
All 4 claimed security fixes have been **verified as implemented**:
| Fix | Status | Evidence |
|-----|--------|----------|
| JSON-LD XSS (#34) | IMPLEMENTED | `</` escaping in `JsonLdBuilder::toScriptTag()` |
| ACL on Batch/ImportExport (#37) | IMPLEMENTED | `authorise()` checks on all controller methods |
| CSV import validation (#35) | IMPLEMENTED | File type, MIME, size (2MB), content_type regex |
| Multilingual data corruption (#41) | IMPLEMENTED | Language-aware load/save in content plugin |
Additional security review found **no vulnerabilities** for: SQL injection, CSRF, file upload, path traversal, code injection, or XSS in output.
---
## Summary
| Category | Total | Fixed | Open | Won't Fix |
|----------|-------|-------|------|-----------|
| Bugs | 5 | 5 | 0 | 0 |
| Issues | 13 | 0 | 12 | 1 |
| Features | 10 | 1 | 9 | 0 |
| Security | 4 | 4 | 0 | 0 |
### Priority for v1.0.0 Release
**Must fix:**
- ISSUE-06: TagsController for admin list operations (publish/delete broken)
- ISSUE-04: Language filter on loadOgDataByType/loadOgDataByMenu (data integrity on multilingual sites)
**Should fix:**
- ISSUE-02: Replace `$doc->_links` access (Joomla version fragility)
- ISSUE-03: Input sanitization on save (defense-in-depth)
- ISSUE-09: GD error suppression (debuggability)
- ISSUE-10: Bundle or document TTF font requirement
**Nice to have for v1.0.0:**
- FEAT-02: Admin edit view
- FEAT-03: Publish/unpublish toggle
- ISSUE-07: Language column in CSV import/export
+7 -7
View File
@@ -1,12 +1,12 @@
# MokoJoomOpenGraph
# MokoSuiteOpenGraph
<!-- VERSION: 01.00.03 -->
<!-- VERSION: 01.03.01 -->
Open Graph, Twitter Card, and social sharing meta tag management for Joomla 4/5/6.
## Overview
MokoJoomOpenGraph gives you full control over how your Joomla content appears when shared on Facebook, Twitter/X, LinkedIn, Discord, WhatsApp, Telegram, and other social platforms. Set custom titles, descriptions, and images per article, menu item, and category — or let the extension auto-generate them from your existing content.
MokoSuiteOpenGraph gives you full control over how your Joomla content appears when shared on Facebook, Twitter/X, LinkedIn, Discord, WhatsApp, Telegram, and other social platforms. Set custom titles, descriptions, and images per article, menu item, and category — or let the extension auto-generate them from your existing content.
## Features
@@ -31,7 +31,7 @@ MokoJoomOpenGraph gives you full control over how your Joomla content appears wh
- **Meta description** — Per-page meta description control
- **Robots directive** — Per-page noindex/nofollow settings
- **Canonical URL** — Custom canonical URL overrides
- **JSON-LD structured data** — Article, WebPage, BreadcrumbList, Organization schemas
- **JSON-LD structured data** — Article, Product, WebPage, BreadcrumbList, Organization schemas
### Admin Tools
- **Tag manager dashboard** — View and manage all OG records centrally
@@ -43,19 +43,19 @@ MokoJoomOpenGraph gives you full control over how your Joomla content appears wh
### Developer Features
- **REST API** — Full CRUD via Joomla Web Services (`/api/v1/mokoog/tags`)
- **Content type adapters** — Extensible architecture for K2, VirtueMart, HikaShop
- **MokoSuiteShop integration** — Auto-generated OG/JSON-LD for product pages with pricing meta
- **Plugin event** — `onMokoOGAfterRender` for third-party plugins to add custom social tags
- **OG image generator** — Text overlay on template backgrounds with auto-resize to 1200x630
## Installation
1. Download the latest `pkg_mokoog-*.zip` from [Releases](https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/releases)
1. Download the latest `pkg_mokoog-*.zip` from [Releases](https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteOpenGraph/releases)
2. In Joomla Administrator → Extensions → Install → Upload Package File
3. All plugins are enabled automatically on install
## Configuration
Navigate to **Extensions → Plugins → System - MokoJoomOpenGraph** to configure:
Navigate to **Extensions → Plugins → System - MokoSuiteOpenGraph** to configure:
- Site name override
- Default OG title and description (site-wide fallback)
- Default fallback image
-237
View File
@@ -1,237 +0,0 @@
#!/usr/bin/env bash
# ============================================================================
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Automation.CI
# INGROUP: moko-platform.Automation
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /automation/ci-issue-reporter.sh
# VERSION: 09.23.00
# BRIEF: Creates or updates a Gitea issue when a CI gate fails.
# Deduplicates by searching open issues with the "ci-auto" label
# whose title matches the gate. If a matching issue exists, a comment
# is appended instead of opening a duplicate.
# ============================================================================
set -euo pipefail
# ── Defaults ────────────────────────────────────────────────────────────────
GITEA_URL="${GITEA_URL:-https://git.mokoconsulting.tech}"
GITEA_TOKEN="${GITEA_TOKEN:-}"
REPO="${GITHUB_REPOSITORY:-}"
RUN_URL="${GITHUB_SERVER_URL:-${GITEA_URL}}/${REPO}/actions/runs/${GITHUB_RUN_ID:-0}"
LABEL_NAME="ci-auto"
LABEL_COLOR="#e11d48"
GATE=""
DETAILS=""
SEVERITY="error"
WORKFLOW=""
# ── Parse arguments ─────────────────────────────────────────────────────────
usage() {
cat <<EOF
Usage: ci-issue-reporter.sh --gate NAME --details TEXT [OPTIONS]
Required:
--gate CI gate name (e.g. "Code Quality", "Self-Health")
--details Human-readable failure description
Optional:
--severity "error" (default) or "warning"
--workflow Workflow name for the issue title
--repo owner/repo (default: \$GITHUB_REPOSITORY)
--run-url URL to the CI run (auto-detected from env)
--token Gitea API token (default: \$GITEA_TOKEN)
--url Gitea base URL (default: \$GITEA_URL)
EOF
exit 1
}
while [[ $# -gt 0 ]]; do
case "$1" in
--gate) GATE="$2"; shift 2 ;;
--details) DETAILS="$2"; shift 2 ;;
--severity) SEVERITY="$2"; shift 2 ;;
--workflow) WORKFLOW="$2"; shift 2 ;;
--repo) REPO="$2"; shift 2 ;;
--run-url) RUN_URL="$2"; shift 2 ;;
--token) GITEA_TOKEN="$2"; shift 2 ;;
--url) GITEA_URL="$2"; shift 2 ;;
-h|--help) usage ;;
*) echo "Unknown option: $1"; usage ;;
esac
done
[[ -z "$GATE" ]] && { echo "ERROR: --gate is required"; usage; }
[[ -z "$DETAILS" ]] && { echo "ERROR: --details is required"; usage; }
[[ -z "$GITEA_TOKEN" ]] && { echo "ERROR: GITEA_TOKEN not set"; exit 1; }
[[ -z "$REPO" ]] && { echo "ERROR: GITHUB_REPOSITORY not set"; exit 1; }
API="${GITEA_URL}/api/v1/repos/${REPO}"
# ── Build title ─────────────────────────────────────────────────────────────
if [[ -n "$WORKFLOW" ]]; then
TITLE="[CI] ${WORKFLOW}: ${GATE} failed"
else
TITLE="[CI] ${GATE} failed"
fi
# ── Ensure label exists ─────────────────────────────────────────────────────
ensure_label() {
local exists
exists=$(curl -sf -o /dev/null -w '%{http_code}' \
-H "Authorization: token ${GITEA_TOKEN}" \
"${API}/labels" 2>/dev/null || echo "000")
if [[ "$exists" == "200" ]]; then
# Check if label already exists
local found
found=$(curl -sf \
-H "Authorization: token ${GITEA_TOKEN}" \
"${API}/labels" 2>/dev/null \
| grep -o "\"name\":\"${LABEL_NAME}\"" || true)
if [[ -z "$found" ]]; then
curl -sf -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/labels" \
-d "{\"name\":\"${LABEL_NAME}\",\"color\":\"${LABEL_COLOR}\",\"description\":\"Auto-created by CI issue reporter\"}" \
> /dev/null 2>&1 || true
fi
fi
}
# ── Search for existing open issue ──────────────────────────────────────────
find_existing_issue() {
# URL-encode the gate name for the query
local query
query=$(printf '%s' "[CI] ${GATE}" | sed 's/ /%20/g; s/\[/%5B/g; s/\]/%5D/g')
local response
response=$(curl -sf \
-H "Authorization: token ${GITEA_TOKEN}" \
"${API}/issues?type=issues&state=open&labels=${LABEL_NAME}&q=${query}&limit=5" \
2>/dev/null || echo "[]")
# Extract the first matching issue number
echo "$response" \
| grep -oP '"number":\s*\K[0-9]+' \
| head -1
}
# ── Build issue body ────────────────────────────────────────────────────────
build_body() {
local severity_badge
if [[ "$SEVERITY" == "error" ]]; then
severity_badge="**Severity:** Error"
else
severity_badge="**Severity:** Warning"
fi
cat <<BODY
## CI Gate Failure: ${GATE}
${severity_badge}
**Workflow:** ${WORKFLOW:-unknown}
**Branch:** ${GITHUB_REF_NAME:-unknown}
**Commit:** \`${GITHUB_SHA:0:8}\`
**Run:** [View CI run](${RUN_URL})
### Details
${DETAILS}
### Resolution
Fix the issue described above and push a new commit. This issue will be closed automatically when the gate passes, or can be closed manually.
---
*Auto-created by [ci-issue-reporter](${GITEA_URL}/${REPO}/src/branch/main/automation/ci-issue-reporter.sh)*
BODY
}
# ── Build comment body (for existing issues) ────────────────────────────────
build_comment() {
cat <<COMMENT
### CI failure recurrence
**Branch:** ${GITHUB_REF_NAME:-unknown}
**Commit:** \`${GITHUB_SHA:0:8}\`
**Run:** [View CI run](${RUN_URL})
${DETAILS}
COMMENT
}
# ── Main ────────────────────────────────────────────────────────────────────
ensure_label
EXISTING=$(find_existing_issue)
if [[ -n "$EXISTING" ]]; then
# Append comment to existing issue
COMMENT_BODY=$(build_comment)
COMMENT_JSON=$(printf '%s' "$COMMENT_BODY" | python3 -c "
import sys, json
print(json.dumps({'body': sys.stdin.read()}))" 2>/dev/null)
HTTP=$(curl -sf -o /dev/null -w '%{http_code}' -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/issues/${EXISTING}/comments" \
-d "${COMMENT_JSON}" 2>/dev/null || echo "000")
if [[ "$HTTP" == "201" ]]; then
echo "Commented on existing issue #${EXISTING}"
else
echo "WARNING: Failed to comment on issue #${EXISTING} (HTTP ${HTTP})"
fi
else
# Create new issue
ISSUE_BODY=$(build_body)
ISSUE_JSON=$(python3 -c "
import sys, json
body = sys.stdin.read()
print(json.dumps({
'title': sys.argv[1],
'body': body,
'labels': []
}))" "$TITLE" <<< "$ISSUE_BODY" 2>/dev/null)
# Create the issue
RESPONSE=$(curl -sf -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/issues" \
-d "${ISSUE_JSON}" 2>/dev/null || echo "{}")
ISSUE_NUM=$(echo "$RESPONSE" | grep -oP '"number":\s*\K[0-9]+' | head -1)
if [[ -n "$ISSUE_NUM" ]]; then
# Apply label (separate call — more reliable across Gitea versions)
LABEL_ID=$(curl -sf \
-H "Authorization: token ${GITEA_TOKEN}" \
"${API}/labels" 2>/dev/null \
| grep -oP "\"id\":\s*\K[0-9]+(?=[^}]*\"name\":\s*\"${LABEL_NAME}\")" \
| head -1 || true)
if [[ -n "$LABEL_ID" ]]; then
curl -sf -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/issues/${ISSUE_NUM}/labels" \
-d "{\"labels\":[${LABEL_ID}]}" \
> /dev/null 2>&1 || true
fi
echo "Created issue #${ISSUE_NUM}: ${TITLE}"
else
echo "WARNING: Failed to create issue"
echo "Response: ${RESPONSE}"
fi
fi
+1 -1
View File
@@ -8,7 +8,7 @@
-->
<extension type="component" method="upgrade">
<name>com_mokoog</name>
<version>01.00.03-dev</version>
<version>01.03.01</version>
<creationDate>2026-05-23</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,60 +0,0 @@
<?php
/**
* @package MokoJoomOpenGraph
* @subpackage com_mokoog
* @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
*/
namespace Joomla\Component\MokoOG\Administrator\ContentType;
defined('_JEXEC') or die;
interface ContentTypeInterface
{
/**
* Check if this adapter can handle the given component/view.
*
* @param string $option Component option (e.g. com_virtuemart)
* @param string $view View name (e.g. productdetails)
*
* @return bool
*/
public function canHandle(string $option, string $view): bool;
/**
* Get the content type identifier for database storage.
*
* @return string
*/
public function getContentType(): string;
/**
* Get the title for the content item.
*
* @param int $id Content item ID
*
* @return string
*/
public function getTitle(int $id): string;
/**
* Get a description for the content item.
*
* @param int $id Content item ID
*
* @return string
*/
public function getDescription(int $id): string;
/**
* Get the primary image for the content item.
*
* @param int $id Content item ID
*
* @return string Image path relative to JPATH_ROOT, or empty string
*/
public function getImage(int $id): string;
}
@@ -1,76 +0,0 @@
<?php
/**
* @package MokoJoomOpenGraph
* @subpackage com_mokoog
* @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
*/
namespace Joomla\Component\MokoOG\Administrator\ContentType;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
class HikaShopAdapter implements ContentTypeInterface
{
public function canHandle(string $option, string $view): bool
{
return $option === 'com_hikashop' && $view === 'product';
}
public function getContentType(): string
{
return 'com_hikashop';
}
public function getTitle(int $id): string
{
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName('product_name'))
->from($db->quoteName('#__hikashop_product'))
->where($db->quoteName('product_id') . ' = ' . $id);
$db->setQuery($query);
return $db->loadResult() ?: '';
}
public function getDescription(int $id): string
{
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName('product_description'))
->from($db->quoteName('#__hikashop_product'))
->where($db->quoteName('product_id') . ' = ' . $id);
$db->setQuery($query);
$text = $db->loadResult() ?: '';
$text = strip_tags($text);
$text = trim(preg_replace('/\s+/', ' ', $text));
if (mb_strlen($text) > 160) {
$text = mb_substr($text, 0, 157) . '...';
}
return $text;
}
public function getImage(int $id): string
{
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName('f.file_path'))
->from($db->quoteName('#__hikashop_file', 'f'))
->where($db->quoteName('f.file_ref_id') . ' = ' . $id)
->where($db->quoteName('f.file_type') . ' = ' . $db->quote('product'))
->order($db->quoteName('f.file_ordering') . ' ASC');
$db->setQuery($query, 0, 1);
return $db->loadResult() ?: '';
}
}
@@ -1,73 +0,0 @@
<?php
/**
* @package MokoJoomOpenGraph
* @subpackage com_mokoog
* @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
*/
namespace Joomla\Component\MokoOG\Administrator\ContentType;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
class K2Adapter implements ContentTypeInterface
{
public function canHandle(string $option, string $view): bool
{
return $option === 'com_k2' && $view === 'item';
}
public function getContentType(): string
{
return 'com_k2';
}
public function getTitle(int $id): string
{
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName('title'))
->from($db->quoteName('#__k2_items'))
->where($db->quoteName('id') . ' = ' . $id);
$db->setQuery($query);
return $db->loadResult() ?: '';
}
public function getDescription(int $id): string
{
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName('introtext'))
->from($db->quoteName('#__k2_items'))
->where($db->quoteName('id') . ' = ' . $id);
$db->setQuery($query);
$text = $db->loadResult() ?: '';
$text = strip_tags($text);
$text = trim(preg_replace('/\s+/', ' ', $text));
if (mb_strlen($text) > 160) {
$text = mb_substr($text, 0, 157) . '...';
}
return $text;
}
public function getImage(int $id): string
{
// K2 stores images as media/k2/items/cache/{md5}_L.jpg
$imagePath = 'media/k2/items/cache/' . md5('Image' . $id) . '_L.jpg';
if (is_file(JPATH_ROOT . '/' . $imagePath)) {
return $imagePath;
}
return '';
}
}
@@ -1,82 +0,0 @@
<?php
/**
* @package MokoJoomOpenGraph
* @subpackage com_mokoog
* @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
*/
namespace Joomla\Component\MokoOG\Administrator\ContentType;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
class VirtueMartAdapter implements ContentTypeInterface
{
public function canHandle(string $option, string $view): bool
{
return $option === 'com_virtuemart' && $view === 'productdetails';
}
public function getContentType(): string
{
return 'com_virtuemart';
}
public function getTitle(int $id): string
{
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName('product_name'))
->from($db->quoteName('#__virtuemart_products_' . $this->getLangTag()))
->where($db->quoteName('virtuemart_product_id') . ' = ' . $id);
$db->setQuery($query);
return $db->loadResult() ?: '';
}
public function getDescription(int $id): string
{
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName('product_s_desc'))
->from($db->quoteName('#__virtuemart_products_' . $this->getLangTag()))
->where($db->quoteName('virtuemart_product_id') . ' = ' . $id);
$db->setQuery($query);
$desc = $db->loadResult() ?: '';
return strip_tags($desc);
}
public function getImage(int $id): string
{
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName('m.file_url'))
->from($db->quoteName('#__virtuemart_product_medias', 'pm'))
->join('INNER', $db->quoteName('#__virtuemart_medias', 'm') . ' ON ' . $db->quoteName('m.virtuemart_media_id') . ' = ' . $db->quoteName('pm.virtuemart_media_id'))
->where($db->quoteName('pm.virtuemart_product_id') . ' = ' . $id)
->order($db->quoteName('pm.ordering') . ' ASC');
$db->setQuery($query, 0, 1);
return $db->loadResult() ?: '';
}
/**
* Get the VirtueMart language table suffix.
*
* @return string
*/
private function getLangTag(): string
{
$lang = Factory::getLanguage()->getTag();
return strtolower(str_replace('-', '_', $lang));
}
}
@@ -1,7 +1,7 @@
<?php
/**
* @package MokoJoomOpenGraph
* @package MokoSuiteOpenGraph
* @subpackage com_mokoog
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
@@ -67,7 +67,7 @@ class BatchController extends BaseController
}
$app = Factory::getApplication();
$limit = $app->getInput()->getInt('limit', 50);
$limit = min($app->getInput()->getInt('limit', 50), 200);
$db = Factory::getDbo();
$query = $db->getQuery(true)
@@ -90,6 +90,7 @@ class BatchController extends BaseController
$articles = $db->loadObjectList();
$created = 0;
$skipped = 0;
$now = Factory::getDate()->toSql();
foreach ($articles as $article) {
@@ -98,23 +99,28 @@ class BatchController extends BaseController
$ogImage = $this->extractImage($article);
$record = (object) [
'content_type' => 'com_content',
'content_id' => (int) $article->id,
'og_title' => $ogTitle,
'og_description' => $ogDescription,
'og_image' => $ogImage,
'og_type' => 'article',
'seo_title' => '',
'content_type' => 'com_content',
'content_id' => (int) $article->id,
'og_title' => $ogTitle,
'og_description' => $ogDescription,
'og_image' => $ogImage,
'og_type' => 'article',
'seo_title' => '',
'meta_description' => $article->metadesc ?: '',
'robots' => '',
'canonical_url' => '',
'published' => 1,
'created' => $now,
'modified' => $now,
'robots' => '',
'canonical_url' => '',
'language' => '*',
'published' => 1,
'created' => $now,
'modified' => $now,
];
$db->insertObject('#__mokoog_tags', $record);
$created++;
try {
$db->insertObject('#__mokoog_tags', $record);
$created++;
} catch (\RuntimeException $e) {
$skipped++;
}
}
echo new JsonResponse([
@@ -1,7 +1,7 @@
<?php
/**
* @package MokoJoomOpenGraph
* @package MokoSuiteOpenGraph
* @subpackage com_mokoog
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
@@ -59,6 +59,7 @@ class ImportExportController extends BaseController
$db->quoteName('t.meta_description'),
$db->quoteName('t.robots'),
$db->quoteName('t.canonical_url'),
$db->quoteName('t.language'),
])
->from($db->quoteName('#__mokoog_tags', 't'))
->leftJoin(
@@ -83,6 +84,7 @@ class ImportExportController extends BaseController
'content_type', 'content_id', 'article_title',
'og_title', 'og_description', 'og_image', 'og_type',
'seo_title', 'meta_description', 'robots', 'canonical_url',
'language',
]);
foreach ($rows as $row) {
@@ -184,6 +186,12 @@ class ImportExportController extends BaseController
$metaDesc = trim($row[8] ?? '');
$robots = trim($row[9] ?? '');
$canonicalUrl = trim($row[10] ?? '');
$language = trim($row[11] ?? '*');
// Validate language tag format (e.g., 'en-GB', '*')
if ($language !== '*' && !preg_match('/^[a-z]{2,3}-[A-Z]{2}$/', $language)) {
$language = '*';
}
if (empty($contentType) || $contentId <= 0) {
$skipped++;
@@ -198,12 +206,13 @@ class ImportExportController extends BaseController
continue;
}
// Check for existing record
// Check for existing record (unique key includes language)
$query = $db->getQuery(true)
->select($db->quoteName('id'))
->from($db->quoteName('#__mokoog_tags'))
->where($db->quoteName('content_type') . ' = ' . $db->quote($contentType))
->where($db->quoteName('content_id') . ' = ' . $contentId);
->where($db->quoteName('content_id') . ' = ' . $contentId)
->where($db->quoteName('language') . ' = ' . $db->quote($language));
$db->setQuery($query);
$existingId = $db->loadResult();
@@ -219,6 +228,7 @@ class ImportExportController extends BaseController
'meta_description' => $metaDesc,
'robots' => $robots,
'canonical_url' => $canonicalUrl,
'language' => $language,
'published' => 1,
'modified' => $now,
];
@@ -0,0 +1,33 @@
<?php
/**
* @package MokoSuiteOpenGraph
* @subpackage com_mokoog
* @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
*/
namespace Joomla\Component\MokoOG\Administrator\Controller;
defined('_JEXEC') or die;
use Joomla\CMS\MVC\Controller\AdminController;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
class TagsController extends AdminController
{
/**
* Proxy for getModel.
*
* @param string $name Model name
* @param string $prefix Model prefix
* @param array $config Configuration array
*
* @return BaseDatabaseModel
*/
public function getModel($name = 'Tag', $prefix = 'Administrator', $config = ['ignore_request' => true])
{
return parent::getModel($name, $prefix, $config);
}
}
@@ -1,7 +1,7 @@
<?php
/**
* @package MokoJoomOpenGraph
* @package MokoSuiteOpenGraph
* @subpackage com_mokoog
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
@@ -32,6 +32,16 @@ class TagTable extends Table
*
* @return bool
*/
private const VALID_OG_TYPES = [
'article', 'website', 'product', 'profile', 'book', 'music.song',
'music.album', 'video.movie', 'video.episode', 'video.other',
];
private const VALID_ROBOTS = [
'index', 'noindex', 'follow', 'nofollow', 'none', 'noarchive',
'nosnippet', 'noimageindex', 'max-snippet', 'max-image-preview',
];
public function check(): bool
{
if (empty($this->content_type)) {
@@ -40,12 +50,58 @@ class TagTable extends Table
return false;
}
if (!preg_match('/^[a-z][a-z0-9_.]*$/', $this->content_type)) {
$this->setError('Content type contains invalid characters.');
return false;
}
if (empty($this->content_id)) {
$this->setError('Content ID is required.');
return false;
}
// Validate og_type against known values
if (!empty($this->og_type) && !\in_array($this->og_type, self::VALID_OG_TYPES, true)) {
$this->og_type = 'article';
}
// Truncate fields to schema max lengths
if (mb_strlen($this->og_title ?? '') > 255) {
$this->og_title = mb_substr($this->og_title, 0, 255);
}
if (mb_strlen($this->seo_title ?? '') > 70) {
$this->seo_title = mb_substr($this->seo_title, 0, 70);
}
if (mb_strlen($this->meta_description ?? '') > 200) {
$this->meta_description = mb_substr($this->meta_description, 0, 200);
}
// Validate canonical_url format if non-empty
if (!empty($this->canonical_url) && !filter_var($this->canonical_url, FILTER_VALIDATE_URL)) {
$this->canonical_url = '';
}
// Validate robots directives
if (!empty($this->robots)) {
$parts = array_map('trim', explode(',', strtolower($this->robots)));
$valid = array_filter($parts, function ($part) {
// Allow directives with values like "max-snippet:-1"
$directive = explode(':', $part)[0];
return \in_array($directive, self::VALID_ROBOTS, true);
});
$this->robots = $valid ? implode(', ', $valid) : '';
}
// Default language to '*' if not set
if (empty($this->language)) {
$this->language = '*';
}
return true;
}
}
@@ -8,7 +8,7 @@
-->
<extension type="plugin" group="content" method="upgrade">
<name>Content - MokoJoomOpenGraph</name>
<version>01.00.03-dev</version>
<version>01.03.01</version>
<creationDate>2026-05-23</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
+1 -1
View File
@@ -8,7 +8,7 @@
-->
<extension type="plugin" group="system" method="upgrade">
<name>System - MokoJoomOpenGraph</name>
<version>01.00.03-dev</version>
<version>01.03.01</version>
<creationDate>2026-05-23</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?php
/**
* @package MokoJoomOpenGraph
* @package MokoSuiteOpenGraph
* @subpackage plg_system_mokoog
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
@@ -112,7 +112,8 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
$image = $ogData->og_image ?: $this->findImage($option, $view, $id);
$url = Uri::getInstance()->toString();
$siteName = $this->params->get('og_site_name', $app->get('sitename', ''));
$type = $ogData->og_type ?: 'article';
$defaultType = ($option === 'com_mokoshop' && $view === 'product') ? 'product' : 'article';
$type = $ogData->og_type ?: $defaultType;
// Open Graph tags
$doc->setMetaData('og:title', $title, 'property');
@@ -188,6 +189,16 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
}
}
// MokoSuiteShop product meta tags
if ($option === 'com_mokoshop' && $view === 'product' && $id > 0) {
$productData = $this->loadShopProduct($id);
if ($productData) {
$doc->setMetaData('product:price:amount', number_format((float) $productData->price, 2, '.', ''), 'property');
$doc->setMetaData('product:price:currency', $productData->currency ?: 'USD', 'property');
}
}
// Fire event so third-party plugins can add custom OG/social tags
$eventData = [
'subject' => $doc,
@@ -206,8 +217,10 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
if ($this->params->get('jsonld_enabled', 1)) {
$imageUrl = $image ? $this->resolveImageUrl($image) : '';
if ($option === 'com_content' && $view === 'article' && $id > 0) {
$schema = JsonLdBuilder::buildArticle($id, $title, $description, $imageUrl);
if ($option === 'com_mokoshop' && $view === 'product' && $id > 0) {
$schema = JsonLdBuilder::buildProduct($id, $title, $description, $imageUrl, $this->loadShopProduct($id));
} elseif ($option === 'com_content' && $view === 'article' && $id > 0) {
$schema = JsonLdBuilder::buildArticle($id, $title, $description, $imageUrl, $this->loadArticle($id));
} else {
$schema = JsonLdBuilder::buildWebPage($title, $description);
}
@@ -253,11 +266,17 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
// Canonical URL
if (!empty($ogData->canonical_url)) {
// Remove any existing canonical link first
foreach ($doc->_links as $link => $attribs) {
if (isset($attribs['relation']) && $attribs['relation'] === 'canonical') {
unset($doc->_links[$link]);
// Remove any existing canonical link via public API
$headData = $doc->getHeadData();
if (!empty($headData['links'])) {
foreach ($headData['links'] as $link => $attribs) {
if (isset($attribs['relation']) && $attribs['relation'] === 'canonical') {
unset($headData['links'][$link]);
}
}
$doc->setHeadData($headData);
}
$doc->addHeadLink($ogData->canonical_url, 'canonical');
@@ -323,15 +342,20 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
*/
private function loadOgDataByType(string $contentType, int $contentId): ?object
{
$db = Factory::getDbo();
$db = Factory::getDbo();
$lang = Factory::getLanguage()->getTag();
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokoog_tags'))
->where($db->quoteName('content_type') . ' = ' . $db->quote($contentType))
->where($db->quoteName('content_id') . ' = ' . $contentId)
->where($db->quoteName('published') . ' = 1');
->where($db->quoteName('published') . ' = 1')
->where('(' . $db->quoteName('language') . ' = ' . $db->quote($lang)
. ' OR ' . $db->quoteName('language') . ' = ' . $db->quote('*') . ')')
->order('CASE WHEN ' . $db->quoteName('language') . ' = ' . $db->quote('*') . ' THEN 1 ELSE 0 END ASC');
$db->setQuery($query);
$db->setQuery($query, 0, 1);
return $db->loadObject();
}
@@ -345,15 +369,20 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
*/
private function loadOgDataByMenu(int $menuId): ?object
{
$db = Factory::getDbo();
$db = Factory::getDbo();
$lang = Factory::getLanguage()->getTag();
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokoog_tags'))
->where($db->quoteName('content_type') . ' = ' . $db->quote('menu'))
->where($db->quoteName('content_id') . ' = ' . $menuId)
->where($db->quoteName('published') . ' = 1');
->where($db->quoteName('published') . ' = 1')
->where('(' . $db->quoteName('language') . ' = ' . $db->quote($lang)
. ' OR ' . $db->quoteName('language') . ' = ' . $db->quote('*') . ')')
->order('CASE WHEN ' . $db->quoteName('language') . ' = ' . $db->quote('*') . ' THEN 1 ELSE 0 END ASC');
$db->setQuery($query);
$db->setQuery($query, 0, 1);
return $db->loadObject();
}
@@ -398,19 +427,31 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
return $this->params->get('default_image', '');
}
// For MokoSuiteShop products, look at the linked article's images
if ($option === 'com_mokoshop' && $id > 0) {
$productData = $this->loadShopProduct($id);
if ($productData && !empty($productData->images)) {
$imagesData = json_decode($productData->images, true);
if (!empty($imagesData['image_fulltext'])) {
return $imagesData['image_fulltext'];
}
if (!empty($imagesData['image_intro'])) {
return $imagesData['image_intro'];
}
}
return $this->params->get('default_image', '');
}
// For Joomla articles, look at the intro/full image fields
if ($option === 'com_content' && $id > 0) {
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName('images'))
->from($db->quoteName('#__content'))
->where($db->quoteName('id') . ' = ' . (int) $id);
$article = $this->loadArticle($id);
$db->setQuery($query);
$images = $db->loadResult();
if ($images) {
$imagesData = json_decode($images, true);
if ($article && !empty($article->images)) {
$imagesData = json_decode($article->images, true);
if (!empty($imagesData['image_fulltext'])) {
return $imagesData['image_fulltext'];
@@ -423,6 +464,7 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
// Fallback: check the article's category for an image
if ($view === 'article') {
$db = Factory::getDbo();
$catQuery = $db->getQuery(true)
->select($db->quoteName('cat.params'))
->from($db->quoteName('#__categories', 'cat'))
@@ -466,6 +508,38 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
return rtrim(Uri::root(), '/') . '/' . ltrim($image, '/');
}
/**
* Load and cache a full article record with author for the current request.
*
* @param int $id Article ID
*
* @return object|null
*/
private function loadArticle(int $id): ?object
{
static $cache = [];
if (isset($cache[$id])) {
return $cache[$id];
}
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName([
'a.title', 'a.introtext', 'a.fulltext', 'a.images',
'a.created', 'a.modified', 'a.publish_up', 'a.metadesc',
]))
->select($db->quoteName('u.name', 'author_name'))
->from($db->quoteName('#__content', 'a'))
->join('LEFT', $db->quoteName('#__users', 'u') . ' ON ' . $db->quoteName('u.id') . ' = ' . $db->quoteName('a.created_by'))
->where($db->quoteName('a.id') . ' = ' . $id);
$db->setQuery($query);
$cache[$id] = $db->loadObject();
return $cache[$id];
}
/**
* Get a date field from an article.
*
@@ -476,16 +550,9 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
*/
private function getArticleDate(int $id, string $field): string
{
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName($field))
->from($db->quoteName('#__content'))
->where($db->quoteName('id') . ' = ' . $id);
$article = $this->loadArticle($id);
$db->setQuery($query);
$date = $db->loadResult();
return $date ?: '';
return $article->$field ?? '';
}
/**
@@ -497,16 +564,9 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
*/
private function getArticleAuthor(int $id): string
{
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName('u.name'))
->from($db->quoteName('#__content', 'a'))
->join('LEFT', $db->quoteName('#__users', 'u') . ' ON ' . $db->quoteName('u.id') . ' = ' . $db->quoteName('a.created_by'))
->where($db->quoteName('a.id') . ' = ' . $id);
$article = $this->loadArticle($id);
$db->setQuery($query);
return $db->loadResult() ?: '';
return $article->author_name ?? '';
}
/**
@@ -534,7 +594,7 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
$query = $db->getQuery(true)
->select($db->quoteName('extra_query'))
->from($db->quoteName('#__update_sites'))
->where($db->quoteName('name') . ' = ' . $db->quote('MokoJoomOpenGraph Updates'))
->where($db->quoteName('name') . ' = ' . $db->quote('MokoSuiteOpenGraph Updates'))
->setLimit(1);
$db->setQuery($query);
$extraQuery = (string) $db->loadResult();
@@ -555,18 +615,52 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
. 'No download key is configured. Updates will not be available until a valid license key is entered. '
. 'Go to <a href="index.php?option=com_installer&view=updatesites">System → Update Sites</a> '
. 'and enter your license key (<code>MOKO-XXXX-XXXX-XXXX-XXXX</code>) in the Download Key field '
. 'for the MokoJoomOpenGraph update site.',
. 'for the MokoSuiteOpenGraph update site.',
'warning'
);
} catch (\Throwable $e) {
// Don't break admin over a license check
// Don't break admin over a license check, but log for debugging
\Joomla\CMS\Log\Log::add('MokoOG license check: ' . $e->getMessage(), \Joomla\CMS\Log\Log::WARNING, 'mokoog');
}
}
/**
* Get the actual pixel dimensions of a local image.
* Load MokoSuiteShop product data by product ID.
*
* Returns [width, height] or null for external URLs or unreadable images.
* @param int $productId CRM product ID
*
* @return object|null Product with name, description, images, price, currency, sku
*/
private function loadShopProduct(int $productId): ?object
{
static $cache = [];
if (isset($cache[$productId])) {
return $cache[$productId];
}
try {
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select('p.id, p.sku, p.price, p.currency, p.stock_qty')
->select('c.title AS name, c.introtext AS description, c.images')
->from($db->quoteName('#__mokosuite_crm_products', 'p'))
->join('LEFT', $db->quoteName('#__content', 'c') . ' ON c.id = p.article_id')
->where($db->quoteName('p.id') . ' = ' . $productId)
->where($db->quoteName('p.published') . ' = 1');
$db->setQuery($query);
$cache[$productId] = $db->loadObject();
} catch (\RuntimeException $e) {
// MokoSuiteShop tables may not exist
$cache[$productId] = null;
}
return $cache[$productId];
}
/**
* Get the actual pixel dimensions of a local image.
*
* @param string $image Image path (relative or absolute URL)
*
@@ -592,7 +686,7 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
return null;
}
$info = @getimagesize($absPath);
$info = getimagesize($absPath);
if (!$info) {
return null;
@@ -1,7 +1,7 @@
<?php
/**
* @package MokoJoomOpenGraph
* @package MokoSuiteOpenGraph
* @subpackage plg_system_mokoog
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
@@ -13,6 +13,7 @@ namespace Joomla\Plugin\System\MokoOG\Helper;
defined('_JEXEC') or die;
use Joomla\CMS\Filesystem\Folder;
use Joomla\CMS\Log\Log;
class ImageGenerator
{
@@ -40,20 +41,32 @@ class ImageGenerator
array $fontColor = [255, 255, 255],
int $quality = 90
): string {
if (!\extension_loaded('gd')) {
Log::add('MokoOG ImageGenerator: GD extension is not loaded. Image generation disabled.', Log::WARNING, 'mokoog');
return '';
}
$templateAbs = JPATH_ROOT . '/' . ltrim($templateImage, '/');
if (!is_file($templateAbs)) {
Log::add('MokoOG ImageGenerator: Template image not found: ' . $templateImage, Log::WARNING, 'mokoog');
return '';
}
if (!$fontFile || !is_file($fontFile)) {
Log::add('MokoOG ImageGenerator: TTF font file not found: ' . ($fontFile ?: '(not configured)'), Log::WARNING, 'mokoog');
return '';
}
$outputDir = JPATH_ROOT . '/' . self::OUTPUT_DIR;
if (!is_dir($outputDir)) {
Folder::create($outputDir);
if (!is_dir($outputDir) && !Folder::create($outputDir)) {
Log::add('MokoOG ImageGenerator: Cannot create output directory: ' . self::OUTPUT_DIR, Log::WARNING, 'mokoog');
return '';
}
$hash = md5($title . $templateImage . $fontSize);
@@ -67,20 +80,24 @@ class ImageGenerator
}
// Load template image
$imageInfo = @getimagesize($templateAbs);
$imageInfo = getimagesize($templateAbs);
if (!$imageInfo) {
Log::add('MokoOG ImageGenerator: Cannot read image dimensions: ' . $templateImage, Log::WARNING, 'mokoog');
return '';
}
$source = match ($imageInfo[2]) {
IMAGETYPE_JPEG => @imagecreatefromjpeg($templateAbs),
IMAGETYPE_PNG => @imagecreatefrompng($templateAbs),
IMAGETYPE_WEBP => @imagecreatefromwebp($templateAbs),
IMAGETYPE_JPEG => imagecreatefromjpeg($templateAbs),
IMAGETYPE_PNG => imagecreatefrompng($templateAbs),
IMAGETYPE_WEBP => function_exists('imagecreatefromwebp') ? imagecreatefromwebp($templateAbs) : false,
default => false,
};
if (!$source) {
Log::add('MokoOG ImageGenerator: Failed to load image (unsupported type or corrupt): ' . $templateImage, Log::WARNING, 'mokoog');
return '';
}
@@ -1,7 +1,7 @@
<?php
/**
* @package MokoJoomOpenGraph
* @package MokoSuiteOpenGraph
* @subpackage plg_system_mokoog
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
@@ -14,6 +14,7 @@ defined('_JEXEC') or die;
use Joomla\CMS\Filesystem\File;
use Joomla\CMS\Filesystem\Folder;
use Joomla\CMS\Log\Log;
class ImageHelper
{
@@ -63,9 +64,11 @@ class ImageHelper
return $imagePath;
}
$imageInfo = @getimagesize($absPath);
$imageInfo = getimagesize($absPath);
if (!$imageInfo) {
Log::add('MokoOG ImageHelper: Cannot read image dimensions: ' . basename($absPath), Log::WARNING, 'mokoog');
return $imagePath;
}
@@ -79,8 +82,10 @@ class ImageHelper
// Ensure output directory exists
$outputDir = JPATH_ROOT . '/' . self::OUTPUT_DIR;
if (!is_dir($outputDir)) {
Folder::create($outputDir);
if (!is_dir($outputDir) && !Folder::create($outputDir)) {
Log::add('MokoOG ImageHelper: Cannot create output directory: ' . self::OUTPUT_DIR, Log::WARNING, 'mokoog');
return $imagePath;
}
// Generate output filename based on source hash + dimensions
@@ -179,7 +184,7 @@ class ImageHelper
return ['valid' => false, 'width' => 0, 'height' => 0, 'message' => 'File not found'];
}
$imageInfo = @getimagesize($absPath);
$imageInfo = getimagesize($absPath);
if (!$imageInfo) {
return ['valid' => false, 'width' => 0, 'height' => 0, 'message' => 'Not a valid image'];
@@ -211,12 +216,18 @@ class ImageHelper
*/
private static function loadImage(string $path, int $type)
{
return match ($type) {
IMAGETYPE_JPEG => @imagecreatefromjpeg($path),
IMAGETYPE_PNG => @imagecreatefrompng($path),
IMAGETYPE_GIF => @imagecreatefromgif($path),
IMAGETYPE_WEBP => @imagecreatefromwebp($path),
$image = match ($type) {
IMAGETYPE_JPEG => imagecreatefromjpeg($path),
IMAGETYPE_PNG => imagecreatefrompng($path),
IMAGETYPE_GIF => imagecreatefromgif($path),
IMAGETYPE_WEBP => function_exists('imagecreatefromwebp') ? imagecreatefromwebp($path) : false,
default => false,
};
if (!$image) {
Log::add('MokoOG ImageHelper: Failed to load image: ' . basename($path), Log::WARNING, 'mokoog');
}
return $image;
}
}
@@ -1,7 +1,7 @@
<?php
/**
* @package MokoJoomOpenGraph
* @package MokoSuiteOpenGraph
* @subpackage plg_system_mokoog
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
@@ -20,31 +20,36 @@ class JsonLdBuilder
/**
* Build Article schema for a com_content article.
*
* @param int $articleId Article ID
* @param string $title Page title
* @param string $description Page description
* @param string $image Image URL (absolute)
* @param int $articleId Article ID
* @param string $title Page title
* @param string $description Page description
* @param string $image Image URL (absolute)
* @param object|null $cachedArticle Pre-loaded article data (avoids duplicate query)
*
* @return array|null
*/
public static function buildArticle(int $articleId, string $title, string $description, string $image): ?array
public static function buildArticle(int $articleId, string $title, string $description, string $image, ?object $cachedArticle = null): ?array
{
if ($articleId <= 0) {
return null;
}
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName([
'a.created', 'a.modified', 'a.publish_up',
'u.name',
]))
->from($db->quoteName('#__content', 'a'))
->join('LEFT', $db->quoteName('#__users', 'u') . ' ON ' . $db->quoteName('u.id') . ' = ' . $db->quoteName('a.created_by'))
->where($db->quoteName('a.id') . ' = ' . $articleId);
$article = $cachedArticle;
$db->setQuery($query);
$article = $db->loadObject();
if (!$article) {
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName([
'a.created', 'a.modified', 'a.publish_up',
]))
->select($db->quoteName('u.name', 'author_name'))
->from($db->quoteName('#__content', 'a'))
->join('LEFT', $db->quoteName('#__users', 'u') . ' ON ' . $db->quoteName('u.id') . ' = ' . $db->quoteName('a.created_by'))
->where($db->quoteName('a.id') . ' = ' . $articleId);
$db->setQuery($query);
$article = $db->loadObject();
}
if (!$article) {
return null;
@@ -60,10 +65,12 @@ class JsonLdBuilder
'dateModified' => $article->modified ?: $article->created,
];
if (!empty($article->name)) {
$authorName = $article->author_name ?? '';
if (!empty($authorName)) {
$schema['author'] = [
'@type' => 'Person',
'name' => $article->name,
'name' => $authorName,
];
}
@@ -152,6 +159,95 @@ class JsonLdBuilder
];
}
/**
* Build Product schema for a MokoSuiteShop product.
*
* @param int $productId CRM product ID
* @param string $title Product title
* @param string $description Product description
* @param string $image Image URL (absolute)
* @param object|null $cachedProduct Pre-loaded product data (avoids duplicate query)
*
* @return array|null
*/
public static function buildProduct(int $productId, string $title, string $description, string $image, ?object $cachedProduct = null): ?array
{
if ($productId <= 0) {
return null;
}
$product = $cachedProduct;
if (!$product) {
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select('p.sku, p.price, p.currency, p.stock_qty')
->from($db->quoteName('#__mokosuite_crm_products', 'p'))
->where($db->quoteName('p.id') . ' = ' . $productId)
->where($db->quoteName('p.published') . ' = 1');
$db->setQuery($query);
$product = $db->loadObject();
}
if (!$product) {
return null;
}
$schema = [
'@context' => 'https://schema.org',
'@type' => 'Product',
'name' => $title,
'description' => $description,
'url' => Uri::getInstance()->toString(),
];
if (!empty($product->sku)) {
$schema['sku'] = $product->sku;
}
if (!empty($image)) {
$schema['image'] = $image;
}
// Offers (pricing and availability)
$availability = ((float) $product->stock_qty > 0)
? 'https://schema.org/InStock'
: 'https://schema.org/OutOfStock';
$schema['offers'] = [
'@type' => 'Offer',
'price' => number_format((float) $product->price, 2, '.', ''),
'priceCurrency' => $product->currency ?: 'USD',
'availability' => $availability,
'url' => Uri::getInstance()->toString(),
];
// Aggregate rating from reviews if available
try {
$reviewQuery = $db->getQuery(true)
->select('COUNT(*) AS review_count, AVG(rating) AS avg_rating')
->from($db->quoteName('#__mokoshop_reviews'))
->where($db->quoteName('product_id') . ' = ' . $productId)
->where($db->quoteName('status') . ' = ' . $db->quote('approved'));
$db->setQuery($reviewQuery);
$rating = $db->loadObject();
if ($rating && (int) $rating->review_count > 0) {
$schema['aggregateRating'] = [
'@type' => 'AggregateRating',
'ratingValue' => round((float) $rating->avg_rating, 1),
'reviewCount' => (int) $rating->review_count,
];
}
} catch (\RuntimeException $e) {
// Reviews table may not exist if MokoSuiteShop reviews module not installed
}
return $schema;
}
/**
* Encode a schema array to a JSON-LD script tag string.
*
@@ -8,7 +8,7 @@
-->
<extension type="plugin" group="webservices" method="upgrade">
<name>Web Services - MokoJoomOpenGraph</name>
<version>01.00.03-dev</version>
<version>01.03.01</version>
<creationDate>2026-05-23</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
+6 -6
View File
@@ -1,14 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
* @package MokoJoomOpenGraph
* @package MokoSuiteOpenGraph
* @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
-->
<extension type="package" method="upgrade">
<name>Package - MokoJoomOpenGraph</name>
<name>Package - MokoSuiteOpenGraph</name>
<packagename>mokoog</packagename>
<version>01.00.03-dev</version>
<version>01.03.01</version>
<creationDate>2026-05-23</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -31,8 +31,8 @@
</languages>
<updateservers>
<server type="extension" name="MokoJoomOpenGraph Updates">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/updates.xml</server>
<server type="extension" name="MokoSuiteOpenGraph Updates">https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteOpenGraph/updates.xml</server>
</updateservers>
<dlid prefix="dlid=" suffix=""/>
<blockChildUninstall>true</blockChildUninstall>
<dlid prefix="dlid=" suffix=""/>
<blockChildUninstall>true</blockChildUninstall>
</extension>
+9 -3
View File
@@ -82,7 +82,9 @@ class Pkg_MokoOGInstallerScript
$key = $db->loadResult();
if (!empty($key)) { $this->savedDownloadKey = $key; }
}
catch (\Throwable $e) {}
catch (\Throwable $e) {
\Joomla\CMS\Log\Log::add('MokoOG saveDownloadKey: ' . $e->getMessage(), \Joomla\CMS\Log\Log::WARNING, 'mokoog');
}
}
private function restoreDownloadKey(): void
@@ -112,7 +114,9 @@ class Pkg_MokoOGInstallerScript
)->execute();
}
}
catch (\Throwable $e) {}
catch (\Throwable $e) {
\Joomla\CMS\Log\Log::add('MokoOG restoreDownloadKey: ' . $e->getMessage(), \Joomla\CMS\Log\Log::WARNING, 'mokoog');
}
}
private function warnMissingLicenseKey(): void
@@ -147,6 +151,8 @@ class Pkg_MokoOGInstallerScript
'warning'
);
}
catch (\Throwable $e) {}
catch (\Throwable $e) {
\Joomla\CMS\Log\Log::add('MokoOG warnMissingLicenseKey: ' . $e->getMessage(), \Joomla\CMS\Log\Log::WARNING, 'mokoog');
}
}
}