From 54bf5f77372937621baeddf30800fffc5afd257d Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sat, 23 May 2026 17:50:28 -0500 Subject: [PATCH 001/132] feat(seo): add SEO meta tag management (closes #8) - Add seo_title, meta_description, robots, canonical_url columns - SQL migration for upgrades (01.01.00.sql) + updated install schema - New "SEO Meta Tags" fieldset in article/menu editor with: - SEO title (70 char max, overrides ) - Meta description (200 char max) - Robots directive (noindex, nofollow, nosnippet, etc.) - Canonical URL override - System plugin applies SEO tags in onBeforeCompileHead before OG tags - SEO audit column in admin tag list (missing desc, title too long, noindex) - Content plugin saves/loads all SEO fields alongside OG data Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- src/packages/com_mokoog/forms/tag.xml | 33 ++++++++++++ .../com_mokoog/language/en-GB/com_mokoog.ini | 25 +++++++++ .../com_mokoog/language/en-US/com_mokoog.ini | 25 +++++++++ src/packages/com_mokoog/sql/install.mysql.sql | 4 ++ .../com_mokoog/sql/updates/mysql/01.01.00.sql | 9 ++++ src/packages/com_mokoog/tmpl/tags/default.php | 27 ++++++++++ .../plg_content_mokoog/forms/mokoog.xml | 43 +++++++++++++++ .../language/en-GB/plg_content_mokoog.ini | 13 +++++ .../language/en-US/plg_content_mokoog.ini | 13 +++++ .../src/Extension/MokoOGContent.php | 32 +++++++---- .../src/Extension/MokoOG.php | 53 +++++++++++++++++-- 11 files changed, 264 insertions(+), 13 deletions(-) create mode 100644 src/packages/com_mokoog/sql/updates/mysql/01.01.00.sql diff --git a/src/packages/com_mokoog/forms/tag.xml b/src/packages/com_mokoog/forms/tag.xml index 363ef5f..3c1ab5c 100644 --- a/src/packages/com_mokoog/forms/tag.xml +++ b/src/packages/com_mokoog/forms/tag.xml @@ -70,4 +70,37 @@ <option value="0">JUNPUBLISHED</option> </field> </fieldset> + <fieldset name="seo" label="SEO Meta Tags"> + <field + name="seo_title" + type="text" + label="PLG_CONTENT_MOKOOG_FIELD_SEO_TITLE" + description="PLG_CONTENT_MOKOOG_FIELD_SEO_TITLE_DESC" + filter="string" + maxlength="70" + /> + <field + name="meta_description" + type="textarea" + label="PLG_CONTENT_MOKOOG_FIELD_META_DESCRIPTION" + description="PLG_CONTENT_MOKOOG_FIELD_META_DESCRIPTION_DESC" + filter="string" + rows="3" + maxlength="200" + /> + <field + name="robots" + type="text" + label="PLG_CONTENT_MOKOOG_FIELD_ROBOTS" + description="PLG_CONTENT_MOKOOG_FIELD_ROBOTS_DESC" + filter="string" + /> + <field + name="canonical_url" + type="url" + label="PLG_CONTENT_MOKOOG_FIELD_CANONICAL_URL" + description="PLG_CONTENT_MOKOOG_FIELD_CANONICAL_URL_DESC" + filter="url" + /> + </fieldset> </form> diff --git a/src/packages/com_mokoog/language/en-GB/com_mokoog.ini b/src/packages/com_mokoog/language/en-GB/com_mokoog.ini index 0e56059..c3f3d80 100644 --- a/src/packages/com_mokoog/language/en-GB/com_mokoog.ini +++ b/src/packages/com_mokoog/language/en-GB/com_mokoog.ini @@ -13,4 +13,29 @@ COM_MOKOOG_HEADING_CONTENT_TYPE="Content Type" COM_MOKOOG_HEADING_CONTENT_ID="Content ID" COM_MOKOOG_HEADING_OG_TITLE="OG Title" COM_MOKOOG_HEADING_IMAGE="Image" +COM_MOKOOG_HEADING_SEO="SEO" COM_MOKOOG_HEADING_MODIFIED="Modified" + +COM_MOKOOG_SEO_OK="OK" +COM_MOKOOG_SEO_MISSING_DESC="No meta description" +COM_MOKOOG_SEO_TITLE_LONG="SEO title too long" +COM_MOKOOG_SEO_NOINDEX="noindex" + +COM_MOKOOG_FIELD_CONTENT_TYPE="Content Type" +COM_MOKOOG_FIELD_CONTENT_ID="Content ID" +COM_MOKOOG_FIELD_OG_TITLE="OG Title" +COM_MOKOOG_FIELD_OG_TITLE_DESC="Custom title for social sharing." +COM_MOKOOG_FIELD_OG_DESCRIPTION="OG Description" +COM_MOKOOG_FIELD_OG_DESCRIPTION_DESC="Custom description for social sharing." +COM_MOKOOG_FIELD_OG_IMAGE="OG Image" +COM_MOKOOG_FIELD_OG_IMAGE_DESC="Custom image for social sharing." +COM_MOKOOG_FIELD_OG_TYPE="OG Type" +COM_MOKOOG_FIELD_OG_TYPE_DESC="The Open Graph content type." + +COM_MOKOOG_FILTER_SEARCH="Search OG titles" +COM_MOKOOG_FILTER_CONTENT_TYPE="Content Type" +COM_MOKOOG_FILTER_SELECT_TYPE="- Select Type -" +COM_MOKOOG_HEADING_OG_TITLE_ASC="OG Title ascending" +COM_MOKOOG_HEADING_OG_TITLE_DESC="OG Title descending" +COM_MOKOOG_HEADING_MODIFIED_ASC="Modified ascending" +COM_MOKOOG_HEADING_MODIFIED_DESC="Modified descending" diff --git a/src/packages/com_mokoog/language/en-US/com_mokoog.ini b/src/packages/com_mokoog/language/en-US/com_mokoog.ini index 0e56059..c3f3d80 100644 --- a/src/packages/com_mokoog/language/en-US/com_mokoog.ini +++ b/src/packages/com_mokoog/language/en-US/com_mokoog.ini @@ -13,4 +13,29 @@ COM_MOKOOG_HEADING_CONTENT_TYPE="Content Type" COM_MOKOOG_HEADING_CONTENT_ID="Content ID" COM_MOKOOG_HEADING_OG_TITLE="OG Title" COM_MOKOOG_HEADING_IMAGE="Image" +COM_MOKOOG_HEADING_SEO="SEO" COM_MOKOOG_HEADING_MODIFIED="Modified" + +COM_MOKOOG_SEO_OK="OK" +COM_MOKOOG_SEO_MISSING_DESC="No meta description" +COM_MOKOOG_SEO_TITLE_LONG="SEO title too long" +COM_MOKOOG_SEO_NOINDEX="noindex" + +COM_MOKOOG_FIELD_CONTENT_TYPE="Content Type" +COM_MOKOOG_FIELD_CONTENT_ID="Content ID" +COM_MOKOOG_FIELD_OG_TITLE="OG Title" +COM_MOKOOG_FIELD_OG_TITLE_DESC="Custom title for social sharing." +COM_MOKOOG_FIELD_OG_DESCRIPTION="OG Description" +COM_MOKOOG_FIELD_OG_DESCRIPTION_DESC="Custom description for social sharing." +COM_MOKOOG_FIELD_OG_IMAGE="OG Image" +COM_MOKOOG_FIELD_OG_IMAGE_DESC="Custom image for social sharing." +COM_MOKOOG_FIELD_OG_TYPE="OG Type" +COM_MOKOOG_FIELD_OG_TYPE_DESC="The Open Graph content type." + +COM_MOKOOG_FILTER_SEARCH="Search OG titles" +COM_MOKOOG_FILTER_CONTENT_TYPE="Content Type" +COM_MOKOOG_FILTER_SELECT_TYPE="- Select Type -" +COM_MOKOOG_HEADING_OG_TITLE_ASC="OG Title ascending" +COM_MOKOOG_HEADING_OG_TITLE_DESC="OG Title descending" +COM_MOKOOG_HEADING_MODIFIED_ASC="Modified ascending" +COM_MOKOOG_HEADING_MODIFIED_DESC="Modified descending" diff --git a/src/packages/com_mokoog/sql/install.mysql.sql b/src/packages/com_mokoog/sql/install.mysql.sql index 884d369..306bc3e 100644 --- a/src/packages/com_mokoog/sql/install.mysql.sql +++ b/src/packages/com_mokoog/sql/install.mysql.sql @@ -12,6 +12,10 @@ CREATE TABLE IF NOT EXISTS `#__mokoog_tags` ( `og_description` TEXT NOT NULL, `og_image` VARCHAR(512) NOT NULL DEFAULT '', `og_type` VARCHAR(50) NOT NULL DEFAULT 'article', + `seo_title` VARCHAR(70) NOT NULL DEFAULT '', + `meta_description` VARCHAR(200) NOT NULL DEFAULT '', + `robots` VARCHAR(100) NOT NULL DEFAULT '', + `canonical_url` VARCHAR(512) NOT NULL DEFAULT '', `published` TINYINT(1) NOT NULL DEFAULT 1, `created` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00', `modified` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00', diff --git a/src/packages/com_mokoog/sql/updates/mysql/01.01.00.sql b/src/packages/com_mokoog/sql/updates/mysql/01.01.00.sql new file mode 100644 index 0000000..4f4c433 --- /dev/null +++ b/src/packages/com_mokoog/sql/updates/mysql/01.01.00.sql @@ -0,0 +1,9 @@ +-- +-- MokoOpenGraph 01.01.00 — Add SEO meta management columns +-- + +ALTER TABLE `#__mokoog_tags` + ADD COLUMN `seo_title` VARCHAR(70) NOT NULL DEFAULT '' AFTER `og_type`, + ADD COLUMN `meta_description` VARCHAR(200) NOT NULL DEFAULT '' AFTER `seo_title`, + ADD COLUMN `robots` VARCHAR(100) NOT NULL DEFAULT '' AFTER `meta_description`, + ADD COLUMN `canonical_url` VARCHAR(512) NOT NULL DEFAULT '' AFTER `robots`; diff --git a/src/packages/com_mokoog/tmpl/tags/default.php b/src/packages/com_mokoog/tmpl/tags/default.php index cfec7f7..779746f 100644 --- a/src/packages/com_mokoog/tmpl/tags/default.php +++ b/src/packages/com_mokoog/tmpl/tags/default.php @@ -50,6 +50,9 @@ use Joomla\CMS\Router\Route; <th scope="col" class="w-10"> <?php echo Text::_('COM_MOKOOG_HEADING_IMAGE'); ?> </th> + <th scope="col" class="w-10"> + <?php echo Text::_('COM_MOKOOG_HEADING_SEO'); ?> + </th> <th scope="col" class="w-10"> <?php echo Text::_('JSTATUS'); ?> </th> @@ -83,6 +86,30 @@ use Joomla\CMS\Router\Route; <span class="icon-minus-circle text-muted" aria-hidden="true"></span> <?php endif; ?> </td> + <td> + <?php + $seoIssues = []; + + if (empty($item->meta_description)) { + $seoIssues[] = Text::_('COM_MOKOOG_SEO_MISSING_DESC'); + } + + if (!empty($item->seo_title) && \strlen($item->seo_title) > 60) { + $seoIssues[] = Text::_('COM_MOKOOG_SEO_TITLE_LONG'); + } + + if (!empty($item->robots) && str_contains($item->robots, 'noindex')) { + $seoIssues[] = Text::_('COM_MOKOOG_SEO_NOINDEX'); + } + + if (empty($seoIssues)) : ?> + <span class="badge bg-success"><?php echo Text::_('COM_MOKOOG_SEO_OK'); ?></span> + <?php else : ?> + <?php foreach ($seoIssues as $issue) : ?> + <span class="badge bg-warning text-dark"><?php echo $issue; ?></span> + <?php endforeach; ?> + <?php endif; ?> + </td> <td> <?php echo $item->published ? Text::_('JPUBLISHED') : Text::_('JUNPUBLISHED'); ?> </td> diff --git a/src/packages/plg_content_mokoog/forms/mokoog.xml b/src/packages/plg_content_mokoog/forms/mokoog.xml index 1e9f026..9e5632e 100644 --- a/src/packages/plg_content_mokoog/forms/mokoog.xml +++ b/src/packages/plg_content_mokoog/forms/mokoog.xml @@ -50,5 +50,48 @@ <option value="video.other">Video</option> </field> </fieldset> + <fieldset name="mokoog_seo" label="PLG_CONTENT_MOKOOG_FIELDSET_SEO_LABEL" + description="PLG_CONTENT_MOKOOG_FIELDSET_SEO_DESC"> + <field + name="seo_title" + type="text" + label="PLG_CONTENT_MOKOOG_FIELD_SEO_TITLE" + description="PLG_CONTENT_MOKOOG_FIELD_SEO_TITLE_DESC" + filter="string" + maxlength="70" + /> + <field + name="meta_description" + type="textarea" + label="PLG_CONTENT_MOKOOG_FIELD_META_DESCRIPTION" + description="PLG_CONTENT_MOKOOG_FIELD_META_DESCRIPTION_DESC" + filter="string" + rows="3" + maxlength="200" + /> + <field + name="robots" + type="list" + label="PLG_CONTENT_MOKOOG_FIELD_ROBOTS" + description="PLG_CONTENT_MOKOOG_FIELD_ROBOTS_DESC" + default="" + multiple="true" + > + <option value="">PLG_CONTENT_MOKOOG_ROBOTS_DEFAULT</option> + <option value="noindex">noindex</option> + <option value="nofollow">nofollow</option> + <option value="nosnippet">nosnippet</option> + <option value="noarchive">noarchive</option> + <option value="noimageindex">noimageindex</option> + </field> + <field + name="canonical_url" + type="url" + label="PLG_CONTENT_MOKOOG_FIELD_CANONICAL_URL" + description="PLG_CONTENT_MOKOOG_FIELD_CANONICAL_URL_DESC" + filter="url" + validate="url" + /> + </fieldset> </fields> </form> diff --git a/src/packages/plg_content_mokoog/language/en-GB/plg_content_mokoog.ini b/src/packages/plg_content_mokoog/language/en-GB/plg_content_mokoog.ini index 6ea916c..167fe7e 100644 --- a/src/packages/plg_content_mokoog/language/en-GB/plg_content_mokoog.ini +++ b/src/packages/plg_content_mokoog/language/en-GB/plg_content_mokoog.ini @@ -13,3 +13,16 @@ PLG_CONTENT_MOKOOG_FIELD_OG_IMAGE="OG Image" PLG_CONTENT_MOKOOG_FIELD_OG_IMAGE_DESC="Custom image for social sharing. Recommended: 1200x630px. Leave blank to use the article image." PLG_CONTENT_MOKOOG_FIELD_OG_TYPE="OG Type" PLG_CONTENT_MOKOOG_FIELD_OG_TYPE_DESC="The Open Graph content type for this page." + +PLG_CONTENT_MOKOOG_FIELDSET_SEO_LABEL="SEO Meta Tags" +PLG_CONTENT_MOKOOG_FIELDSET_SEO_DESC="Control search engine meta tags for this page." + +PLG_CONTENT_MOKOOG_FIELD_SEO_TITLE="SEO Title" +PLG_CONTENT_MOKOOG_FIELD_SEO_TITLE_DESC="Custom <title> tag. 50-60 characters recommended. Leave blank to use the default page title." +PLG_CONTENT_MOKOOG_FIELD_META_DESCRIPTION="Meta Description" +PLG_CONTENT_MOKOOG_FIELD_META_DESCRIPTION_DESC="Custom meta description. 150-160 characters recommended. Leave blank to use the default." +PLG_CONTENT_MOKOOG_FIELD_ROBOTS="Robots Directive" +PLG_CONTENT_MOKOOG_FIELD_ROBOTS_DESC="Search engine indexing directives for this page. Leave blank for default (index, follow)." +PLG_CONTENT_MOKOOG_ROBOTS_DEFAULT="- Use default (index, follow) -" +PLG_CONTENT_MOKOOG_FIELD_CANONICAL_URL="Canonical URL" +PLG_CONTENT_MOKOOG_FIELD_CANONICAL_URL_DESC="Override the canonical URL for this page. Leave blank to use the current URL." diff --git a/src/packages/plg_content_mokoog/language/en-US/plg_content_mokoog.ini b/src/packages/plg_content_mokoog/language/en-US/plg_content_mokoog.ini index 6ea916c..167fe7e 100644 --- a/src/packages/plg_content_mokoog/language/en-US/plg_content_mokoog.ini +++ b/src/packages/plg_content_mokoog/language/en-US/plg_content_mokoog.ini @@ -13,3 +13,16 @@ PLG_CONTENT_MOKOOG_FIELD_OG_IMAGE="OG Image" PLG_CONTENT_MOKOOG_FIELD_OG_IMAGE_DESC="Custom image for social sharing. Recommended: 1200x630px. Leave blank to use the article image." PLG_CONTENT_MOKOOG_FIELD_OG_TYPE="OG Type" PLG_CONTENT_MOKOOG_FIELD_OG_TYPE_DESC="The Open Graph content type for this page." + +PLG_CONTENT_MOKOOG_FIELDSET_SEO_LABEL="SEO Meta Tags" +PLG_CONTENT_MOKOOG_FIELDSET_SEO_DESC="Control search engine meta tags for this page." + +PLG_CONTENT_MOKOOG_FIELD_SEO_TITLE="SEO Title" +PLG_CONTENT_MOKOOG_FIELD_SEO_TITLE_DESC="Custom <title> tag. 50-60 characters recommended. Leave blank to use the default page title." +PLG_CONTENT_MOKOOG_FIELD_META_DESCRIPTION="Meta Description" +PLG_CONTENT_MOKOOG_FIELD_META_DESCRIPTION_DESC="Custom meta description. 150-160 characters recommended. Leave blank to use the default." +PLG_CONTENT_MOKOOG_FIELD_ROBOTS="Robots Directive" +PLG_CONTENT_MOKOOG_FIELD_ROBOTS_DESC="Search engine indexing directives for this page. Leave blank for default (index, follow)." +PLG_CONTENT_MOKOOG_ROBOTS_DEFAULT="- Use default (index, follow) -" +PLG_CONTENT_MOKOOG_FIELD_CANONICAL_URL="Canonical URL" +PLG_CONTENT_MOKOOG_FIELD_CANONICAL_URL_DESC="Override the canonical URL for this page. Leave blank to use the current URL." diff --git a/src/packages/plg_content_mokoog/src/Extension/MokoOGContent.php b/src/packages/plg_content_mokoog/src/Extension/MokoOGContent.php index 07c1161..3d70565 100644 --- a/src/packages/plg_content_mokoog/src/Extension/MokoOGContent.php +++ b/src/packages/plg_content_mokoog/src/Extension/MokoOGContent.php @@ -176,7 +176,10 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface { $db = Factory::getDbo(); $query = $db->getQuery(true) - ->select($db->quoteName(['og_title', 'og_description', 'og_image', 'og_type'])) + ->select($db->quoteName([ + 'og_title', 'og_description', 'og_image', 'og_type', + 'seo_title', 'meta_description', 'robots', 'canonical_url', + ])) ->from($db->quoteName('#__mokoog_tags')) ->where($db->quoteName('content_type') . ' = ' . $db->quote($contentType)) ->where($db->quoteName('content_id') . ' = ' . $contentId); @@ -209,15 +212,26 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface $db->setQuery($query); $existingId = $db->loadResult(); + // Robots may come as array from multi-select, join with comma + $robots = $ogData['robots'] ?? ''; + + if (\is_array($robots)) { + $robots = implode(', ', array_filter($robots)); + } + $record = (object) [ - 'content_type' => $contentType, - 'content_id' => $contentId, - 'og_title' => trim($ogData['og_title'] ?? ''), - 'og_description' => trim($ogData['og_description'] ?? ''), - 'og_image' => trim($ogData['og_image'] ?? ''), - 'og_type' => trim($ogData['og_type'] ?? 'article'), - 'published' => 1, - 'modified' => Factory::getDate()->toSql(), + 'content_type' => $contentType, + 'content_id' => $contentId, + 'og_title' => trim($ogData['og_title'] ?? ''), + 'og_description' => trim($ogData['og_description'] ?? ''), + 'og_image' => trim($ogData['og_image'] ?? ''), + 'og_type' => trim($ogData['og_type'] ?? 'article'), + 'seo_title' => trim($ogData['seo_title'] ?? ''), + 'meta_description' => trim($ogData['meta_description'] ?? ''), + 'robots' => trim($robots), + 'canonical_url' => trim($ogData['canonical_url'] ?? ''), + 'published' => 1, + 'modified' => Factory::getDate()->toSql(), ]; if ($existingId) { diff --git a/src/packages/plg_system_mokoog/src/Extension/MokoOG.php b/src/packages/plg_system_mokoog/src/Extension/MokoOG.php index 818e263..c245c42 100644 --- a/src/packages/plg_system_mokoog/src/Extension/MokoOG.php +++ b/src/packages/plg_system_mokoog/src/Extension/MokoOG.php @@ -67,6 +67,9 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface // Try to load custom OG data from the database $ogData = $this->loadOgData($option, $view, $id); + // --- SEO meta tags (set first, before OG) --- + $this->applySeoTags($doc, $ogData); + // Build tag values — custom overrides auto-generated $title = $ogData->og_title ?: $doc->getTitle(); $description = $ogData->og_description ?: $this->buildDescription($doc); @@ -111,6 +114,44 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface } } + /** + * Apply SEO meta tags (title, description, robots, canonical) to the document. + * + * @param \Joomla\CMS\Document\HtmlDocument $doc The document + * @param object $ogData The loaded OG/SEO data + * + * @return void + */ + private function applySeoTags($doc, object $ogData): void + { + // Custom SEO title overrides the page <title> + if (!empty($ogData->seo_title)) { + $doc->setTitle($ogData->seo_title); + } + + // Custom meta description + if (!empty($ogData->meta_description)) { + $doc->setDescription($ogData->meta_description); + } + + // Robots directive + if (!empty($ogData->robots)) { + $doc->setMetaData('robots', $ogData->robots); + } + + // 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]); + } + } + + $doc->addHeadLink($ogData->canonical_url, 'canonical'); + } + } + /** * Load custom OG data from the database for the current page. * @@ -123,10 +164,14 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface private function loadOgData(string $option, string $view, int $id): object { $empty = (object) [ - 'og_title' => '', - 'og_description' => '', - 'og_image' => '', - 'og_type' => '', + 'og_title' => '', + 'og_description' => '', + 'og_image' => '', + 'og_type' => '', + 'seo_title' => '', + 'meta_description' => '', + 'robots' => '', + 'canonical_url' => '', ]; if (!$id) { From 935185b46f365f02ecb58b4fb52883f6fc157d64 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sat, 23 May 2026 17:53:28 -0500 Subject: [PATCH 002/132] feat(batch): add batch OG tag generation for existing articles (closes #1) - BatchController with count() and process() AJAX endpoints - Chunked processing (50 articles per request) to avoid PHP timeouts - LEFT JOIN query to find articles without existing OG records - Auto-extracts og_title from article title, og_description from metadesc or introtext (160 char), og_image from article images JSON - Also populates meta_description from article metadesc - Progress bar UI in admin tag list with real-time updates - "Batch Generate" toolbar button in Tags view - Auto-reloads page after completion Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- .../com_mokoog/language/en-GB/com_mokoog.ini | 9 + .../com_mokoog/language/en-US/com_mokoog.ini | 9 + .../src/Controller/BatchController.php | 169 ++++++++++++++++++ .../com_mokoog/src/View/Tags/HtmlView.php | 1 + src/packages/com_mokoog/tmpl/tags/default.php | 84 +++++++++ 5 files changed, 272 insertions(+) create mode 100644 src/packages/com_mokoog/src/Controller/BatchController.php diff --git a/src/packages/com_mokoog/language/en-GB/com_mokoog.ini b/src/packages/com_mokoog/language/en-GB/com_mokoog.ini index c3f3d80..28aecda 100644 --- a/src/packages/com_mokoog/language/en-GB/com_mokoog.ini +++ b/src/packages/com_mokoog/language/en-GB/com_mokoog.ini @@ -39,3 +39,12 @@ COM_MOKOOG_HEADING_OG_TITLE_ASC="OG Title ascending" COM_MOKOOG_HEADING_OG_TITLE_DESC="OG Title descending" COM_MOKOOG_HEADING_MODIFIED_ASC="Modified ascending" COM_MOKOOG_HEADING_MODIFIED_DESC="Modified descending" + +COM_MOKOOG_TOOLBAR_BATCH_GENERATE="Batch Generate" +COM_MOKOOG_BATCH_TITLE="Batch OG Tag Generation" +COM_MOKOOG_BATCH_COUNTING="Counting articles without OG tags..." +COM_MOKOOG_BATCH_NONE="All articles already have OG tags." +COM_MOKOOG_BATCH_FOUND="articles found without OG tags." +COM_MOKOOG_BATCH_PROCESSED="processed" +COM_MOKOOG_BATCH_COMPLETE="Batch generation complete!" +COM_MOKOOG_BATCH_ERROR="Error:" diff --git a/src/packages/com_mokoog/language/en-US/com_mokoog.ini b/src/packages/com_mokoog/language/en-US/com_mokoog.ini index c3f3d80..28aecda 100644 --- a/src/packages/com_mokoog/language/en-US/com_mokoog.ini +++ b/src/packages/com_mokoog/language/en-US/com_mokoog.ini @@ -39,3 +39,12 @@ COM_MOKOOG_HEADING_OG_TITLE_ASC="OG Title ascending" COM_MOKOOG_HEADING_OG_TITLE_DESC="OG Title descending" COM_MOKOOG_HEADING_MODIFIED_ASC="Modified ascending" COM_MOKOOG_HEADING_MODIFIED_DESC="Modified descending" + +COM_MOKOOG_TOOLBAR_BATCH_GENERATE="Batch Generate" +COM_MOKOOG_BATCH_TITLE="Batch OG Tag Generation" +COM_MOKOOG_BATCH_COUNTING="Counting articles without OG tags..." +COM_MOKOOG_BATCH_NONE="All articles already have OG tags." +COM_MOKOOG_BATCH_FOUND="articles found without OG tags." +COM_MOKOOG_BATCH_PROCESSED="processed" +COM_MOKOOG_BATCH_COMPLETE="Batch generation complete!" +COM_MOKOOG_BATCH_ERROR="Error:" diff --git a/src/packages/com_mokoog/src/Controller/BatchController.php b/src/packages/com_mokoog/src/Controller/BatchController.php new file mode 100644 index 0000000..909ef78 --- /dev/null +++ b/src/packages/com_mokoog/src/Controller/BatchController.php @@ -0,0 +1,169 @@ +<?php + +/** + * @package MokoOpenGraph + * @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\Factory; +use Joomla\CMS\Language\Text; +use Joomla\CMS\MVC\Controller\BaseController; +use Joomla\CMS\Response\JsonResponse; +use Joomla\CMS\Session\Session; + +class BatchController extends BaseController +{ + /** + * Count the total articles eligible for batch generation. + * + * @return void + */ + public function count(): void + { + Session::checkToken('get') || jexit(Text::_('JINVALID_TOKEN')); + + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__content', 'c')) + ->leftJoin( + $db->quoteName('#__mokoog_tags', 't') + . ' ON ' . $db->quoteName('t.content_type') . ' = ' . $db->quote('com_content') + . ' AND ' . $db->quoteName('t.content_id') . ' = ' . $db->quoteName('c.id') + ) + ->where($db->quoteName('c.state') . ' = 1') + ->where($db->quoteName('t.id') . ' IS NULL'); + + $db->setQuery($query); + $total = (int) $db->loadResult(); + + echo new JsonResponse(['total' => $total]); + + Factory::getApplication()->close(); + } + + /** + * Process a chunk of articles for batch OG generation. + * + * @return void + */ + public function process(): void + { + Session::checkToken('get') || jexit(Text::_('JINVALID_TOKEN')); + + $app = Factory::getApplication(); + $offset = $app->getInput()->getInt('offset', 0); + $limit = $app->getInput()->getInt('limit', 50); + + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName([ + 'c.id', 'c.title', 'c.metadesc', 'c.introtext', 'c.fulltext', 'c.images', + ])) + ->from($db->quoteName('#__content', 'c')) + ->leftJoin( + $db->quoteName('#__mokoog_tags', 't') + . ' ON ' . $db->quoteName('t.content_type') . ' = ' . $db->quote('com_content') + . ' AND ' . $db->quoteName('t.content_id') . ' = ' . $db->quoteName('c.id') + ) + ->where($db->quoteName('c.state') . ' = 1') + ->where($db->quoteName('t.id') . ' IS NULL') + ->order($db->quoteName('c.id') . ' ASC'); + + $db->setQuery($query, $offset, $limit); + $articles = $db->loadObjectList(); + + $created = 0; + $now = Factory::getDate()->toSql(); + + foreach ($articles as $article) { + $ogTitle = $article->title; + $ogDescription = $this->extractDescription($article); + $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' => '', + 'meta_description' => $article->metadesc ?: '', + 'robots' => '', + 'canonical_url' => '', + 'published' => 1, + 'created' => $now, + 'modified' => $now, + ]; + + $db->insertObject('#__mokoog_tags', $record); + $created++; + } + + echo new JsonResponse([ + 'created' => $created, + 'offset' => $offset, + 'processed' => $offset + $created, + ]); + + $app->close(); + } + + /** + * Extract a description from article content. + * + * @param object $article Article record + * + * @return string + */ + private function extractDescription(object $article): string + { + // Prefer meta description if set + if (!empty($article->metadesc)) { + return $article->metadesc; + } + + // Fall back to intro text + $text = $article->introtext ?: $article->fulltext; + $text = strip_tags($text); + $text = trim(preg_replace('/\s+/', ' ', $text)); + + if (\strlen($text) > 160) { + $text = mb_substr($text, 0, 157) . '...'; + } + + return $text; + } + + /** + * Extract the best image from article data. + * + * @param object $article Article record + * + * @return string + */ + private function extractImage(object $article): string + { + if (!empty($article->images)) { + $images = json_decode($article->images, true); + + if (!empty($images['image_fulltext'])) { + return $images['image_fulltext']; + } + + if (!empty($images['image_intro'])) { + return $images['image_intro']; + } + } + + return ''; + } +} diff --git a/src/packages/com_mokoog/src/View/Tags/HtmlView.php b/src/packages/com_mokoog/src/View/Tags/HtmlView.php index 14ff7cb..0a189b4 100644 --- a/src/packages/com_mokoog/src/View/Tags/HtmlView.php +++ b/src/packages/com_mokoog/src/View/Tags/HtmlView.php @@ -65,6 +65,7 @@ class HtmlView extends BaseHtmlView protected function addToolbar(): void { ToolbarHelper::title(Text::_('COM_MOKOOG_TAGS_TITLE'), 'bookmark'); + ToolbarHelper::custom('batch.generate', 'refresh', '', 'COM_MOKOOG_TOOLBAR_BATCH_GENERATE', false); ToolbarHelper::deleteList('JGLOBAL_CONFIRM_DELETE', 'tags.delete'); ToolbarHelper::preferences('com_mokoog'); } diff --git a/src/packages/com_mokoog/tmpl/tags/default.php b/src/packages/com_mokoog/tmpl/tags/default.php index 779746f..94ad6cf 100644 --- a/src/packages/com_mokoog/tmpl/tags/default.php +++ b/src/packages/com_mokoog/tmpl/tags/default.php @@ -14,9 +14,11 @@ use Joomla\CMS\HTML\HTMLHelper; use Joomla\CMS\Language\Text; use Joomla\CMS\Layout\LayoutHelper; use Joomla\CMS\Router\Route; +use Joomla\CMS\Session\Session; /** @var \Joomla\Component\MokoOG\Administrator\View\Tags\HtmlView $this */ +$token = Session::getFormToken(); ?> <form action="<?php echo Route::_('index.php?option=com_mokoog&view=tags'); ?>" method="post" name="adminForm" id="adminForm"> <div class="row"> @@ -134,3 +136,85 @@ use Joomla\CMS\Router\Route; </div> </div> </form> + +<!-- Batch Generation Progress --> +<div id="mokoog-batch-panel" style="display:none;" class="card mt-3"> + <div class="card-body"> + <h4><?php echo Text::_('COM_MOKOOG_BATCH_TITLE'); ?></h4> + <div class="progress mb-2"> + <div id="mokoog-batch-bar" class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 0%">0%</div> + </div> + <p id="mokoog-batch-status"></p> + </div> +</div> + +<script> +document.addEventListener('DOMContentLoaded', function() { + // Intercept the batch.generate toolbar button + var origSubmitbutton = Joomla.submitbutton; + Joomla.submitbutton = function(task) { + if (task === 'batch.generate') { + mokoogBatchGenerate(); + return; + } + if (origSubmitbutton) { + origSubmitbutton(task); + } + }; + + function mokoogBatchGenerate() { + var panel = document.getElementById('mokoog-batch-panel'); + var bar = document.getElementById('mokoog-batch-bar'); + var status = document.getElementById('mokoog-batch-status'); + var token = '<?php echo $token; ?>'; + var chunkSize = 50; + + panel.style.display = 'block'; + status.textContent = '<?php echo Text::_('COM_MOKOOG_BATCH_COUNTING', true); ?>'; + + // Step 1: Count eligible articles + fetch('index.php?option=com_mokoog&task=batch.count&format=json&' + token + '=1') + .then(function(r) { return r.json(); }) + .then(function(resp) { + var total = resp.data.total; + if (total === 0) { + bar.style.width = '100%'; + bar.textContent = '100%'; + bar.classList.remove('progress-bar-animated'); + bar.classList.add('bg-success'); + status.textContent = '<?php echo Text::_('COM_MOKOOG_BATCH_NONE', true); ?>'; + return; + } + status.textContent = total + ' <?php echo Text::_('COM_MOKOOG_BATCH_FOUND', true); ?>'; + processChunk(0, total, chunkSize, token, bar, status); + }) + .catch(function(err) { + status.textContent = '<?php echo Text::_('COM_MOKOOG_BATCH_ERROR', true); ?> ' + err.message; + }); + } + + function processChunk(offset, total, chunkSize, token, bar, status) { + fetch('index.php?option=com_mokoog&task=batch.process&format=json&offset=' + offset + '&limit=' + chunkSize + '&' + token + '=1') + .then(function(r) { return r.json(); }) + .then(function(resp) { + var processed = resp.data.processed; + var pct = Math.min(100, Math.round((processed / total) * 100)); + bar.style.width = pct + '%'; + bar.textContent = pct + '%'; + status.textContent = processed + ' / ' + total + ' <?php echo Text::_('COM_MOKOOG_BATCH_PROCESSED', true); ?>'; + + if (processed < total) { + processChunk(processed, total, chunkSize, token, bar, status); + } else { + bar.classList.remove('progress-bar-animated'); + bar.classList.add('bg-success'); + status.textContent = '<?php echo Text::_('COM_MOKOOG_BATCH_COMPLETE', true); ?> ' + total + ' articles.'; + setTimeout(function() { location.reload(); }, 2000); + } + }) + .catch(function(err) { + status.textContent = '<?php echo Text::_('COM_MOKOOG_BATCH_ERROR', true); ?> ' + err.message; + }); + } +}); +</script> From 5fc0fbfc07f67cde62aae3ac7082e7f336625694 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sat, 23 May 2026 17:57:07 -0500 Subject: [PATCH 003/132] feat(images): auto-resize OG images to 1200x630px (closes #2) - New ImageHelper class with resize(), validate(), cleanup() methods - Center-crop algorithm maintains aspect ratio to target dimensions - GD-based processing, supports JPEG/PNG/GIF/WebP input, outputs JPEG - Generated images cached in images/mokoog/generated/ with hash naming - Skips resize if image already at or below target dimensions - Skips regeneration if cached version is newer than source - validate() checks minimum 200x200px (Facebook/WhatsApp requirement) - cleanup() removes generated images when OG records are deleted - Auto-resize toggle in system plugin advanced settings (default: on) - Integrated into resolveImageUrl() in the system plugin Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- .../language/en-GB/plg_system_mokoog.ini | 2 + .../language/en-US/plg_system_mokoog.ini | 2 + src/packages/plg_system_mokoog/mokoog.xml | 11 + .../src/Extension/MokoOG.php | 8 +- .../src/Helper/ImageHelper.php | 222 ++++++++++++++++++ 5 files changed, 244 insertions(+), 1 deletion(-) create mode 100644 src/packages/plg_system_mokoog/src/Helper/ImageHelper.php diff --git a/src/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.ini b/src/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.ini index 4983711..a0902bd 100644 --- a/src/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.ini +++ b/src/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.ini @@ -23,3 +23,5 @@ PLG_SYSTEM_MOKOOG_FIELD_STRIP_HTML="Strip HTML from Description" PLG_SYSTEM_MOKOOG_FIELD_STRIP_HTML_DESC="Remove HTML tags from the auto-generated description." PLG_SYSTEM_MOKOOG_FIELD_DESC_LENGTH="Description Length" PLG_SYSTEM_MOKOOG_FIELD_DESC_LENGTH_DESC="Maximum character length for the auto-generated og:description." +PLG_SYSTEM_MOKOOG_FIELD_AUTO_RESIZE="Auto-resize Images" +PLG_SYSTEM_MOKOOG_FIELD_AUTO_RESIZE_DESC="Automatically resize images to 1200x630px (Facebook recommended) using center crop. Generated images are saved to images/mokoog/generated/." diff --git a/src/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.ini b/src/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.ini index 4983711..a0902bd 100644 --- a/src/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.ini +++ b/src/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.ini @@ -23,3 +23,5 @@ PLG_SYSTEM_MOKOOG_FIELD_STRIP_HTML="Strip HTML from Description" PLG_SYSTEM_MOKOOG_FIELD_STRIP_HTML_DESC="Remove HTML tags from the auto-generated description." PLG_SYSTEM_MOKOOG_FIELD_DESC_LENGTH="Description Length" PLG_SYSTEM_MOKOOG_FIELD_DESC_LENGTH_DESC="Maximum character length for the auto-generated og:description." +PLG_SYSTEM_MOKOOG_FIELD_AUTO_RESIZE="Auto-resize Images" +PLG_SYSTEM_MOKOOG_FIELD_AUTO_RESIZE_DESC="Automatically resize images to 1200x630px (Facebook recommended) using center crop. Generated images are saved to images/mokoog/generated/." diff --git a/src/packages/plg_system_mokoog/mokoog.xml b/src/packages/plg_system_mokoog/mokoog.xml index 50a2120..2517076 100644 --- a/src/packages/plg_system_mokoog/mokoog.xml +++ b/src/packages/plg_system_mokoog/mokoog.xml @@ -107,6 +107,17 @@ min="50" max="300" /> + <field + name="auto_resize" + type="radio" + label="PLG_SYSTEM_MOKOOG_FIELD_AUTO_RESIZE" + description="PLG_SYSTEM_MOKOOG_FIELD_AUTO_RESIZE_DESC" + default="1" + class="btn-group" + > + <option value="1">JYES</option> + <option value="0">JNO</option> + </field> </fieldset> </fields> </config> diff --git a/src/packages/plg_system_mokoog/src/Extension/MokoOG.php b/src/packages/plg_system_mokoog/src/Extension/MokoOG.php index c245c42..e571cc4 100644 --- a/src/packages/plg_system_mokoog/src/Extension/MokoOG.php +++ b/src/packages/plg_system_mokoog/src/Extension/MokoOG.php @@ -17,6 +17,7 @@ use Joomla\CMS\Plugin\CMSPlugin; use Joomla\CMS\Uri\Uri; use Joomla\Event\Event; use Joomla\Event\SubscriberInterface; +use Joomla\Plugin\System\MokoOG\Helper\ImageHelper; final class MokoOG extends CMSPlugin implements SubscriberInterface { @@ -288,7 +289,7 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface } /** - * Resolve a relative image path to a full URL. + * Resolve a relative image path to a full URL, resizing for OG if needed. * * @param string $image Image path * @@ -300,6 +301,11 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface return $image; } + // Auto-resize to OG recommended dimensions if enabled + if ($this->params->get('auto_resize', 1)) { + $image = ImageHelper::resize($image); + } + return rtrim(Uri::root(), '/') . '/' . ltrim($image, '/'); } } diff --git a/src/packages/plg_system_mokoog/src/Helper/ImageHelper.php b/src/packages/plg_system_mokoog/src/Helper/ImageHelper.php new file mode 100644 index 0000000..8364f18 --- /dev/null +++ b/src/packages/plg_system_mokoog/src/Helper/ImageHelper.php @@ -0,0 +1,222 @@ +<?php + +/** + * @package MokoOpenGraph + * @subpackage plg_system_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\Plugin\System\MokoOG\Helper; + +defined('_JEXEC') or die; + +use Joomla\CMS\Filesystem\File; +use Joomla\CMS\Filesystem\Folder; + +class ImageHelper +{ + /** + * Target width for OG images (Facebook recommended). + */ + private const TARGET_WIDTH = 1200; + + /** + * Target height for OG images (Facebook recommended). + */ + private const TARGET_HEIGHT = 630; + + /** + * JPEG quality for generated images. + */ + private const JPEG_QUALITY = 85; + + /** + * Output directory relative to JPATH_ROOT. + */ + private const OUTPUT_DIR = 'images/mokoog/generated'; + + /** + * Resize an image to OG-optimized dimensions if needed. + * + * Returns the path to the resized image relative to JPATH_ROOT, + * or the original path if no resize was needed or possible. + * + * @param string $imagePath Image path relative to JPATH_ROOT + * @param int $targetWidth Target width (default 1200) + * @param int $targetHeight Target height (default 630) + * @param int $quality JPEG quality 1-100 (default 85) + * + * @return string Path to the output image (relative to JPATH_ROOT) + */ + public static function resize( + string $imagePath, + int $targetWidth = self::TARGET_WIDTH, + int $targetHeight = self::TARGET_HEIGHT, + int $quality = self::JPEG_QUALITY + ): string { + // Resolve absolute path + $absPath = JPATH_ROOT . '/' . ltrim($imagePath, '/'); + + if (!is_file($absPath)) { + return $imagePath; + } + + $imageInfo = @getimagesize($absPath); + + if (!$imageInfo) { + return $imagePath; + } + + [$origWidth, $origHeight, $type] = $imageInfo; + + // Skip if already at or below target size + if ($origWidth <= $targetWidth && $origHeight <= $targetHeight) { + return $imagePath; + } + + // Ensure output directory exists + $outputDir = JPATH_ROOT . '/' . self::OUTPUT_DIR; + + if (!is_dir($outputDir)) { + Folder::create($outputDir); + } + + // Generate output filename based on source hash + dimensions + $hash = md5($imagePath . $targetWidth . $targetHeight); + $outputName = $hash . '.jpg'; + $outputPath = $outputDir . '/' . $outputName; + $outputRel = self::OUTPUT_DIR . '/' . $outputName; + + // Skip if already generated + if (is_file($outputPath) && filemtime($outputPath) >= filemtime($absPath)) { + return $outputRel; + } + + // Load source image + $source = self::loadImage($absPath, $type); + + if (!$source) { + return $imagePath; + } + + // Calculate crop dimensions (center crop to target aspect ratio) + $targetRatio = $targetWidth / $targetHeight; + $sourceRatio = $origWidth / $origHeight; + + if ($sourceRatio > $targetRatio) { + // Source is wider — crop sides + $cropHeight = $origHeight; + $cropWidth = (int) round($origHeight * $targetRatio); + $cropX = (int) round(($origWidth - $cropWidth) / 2); + $cropY = 0; + } else { + // Source is taller — crop top/bottom + $cropWidth = $origWidth; + $cropHeight = (int) round($origWidth / $targetRatio); + $cropX = 0; + $cropY = (int) round(($origHeight - $cropHeight) / 2); + } + + // Create output canvas and resample + $output = imagecreatetruecolor($targetWidth, $targetHeight); + + imagecopyresampled( + $output, + $source, + 0, + 0, + $cropX, + $cropY, + $targetWidth, + $targetHeight, + $cropWidth, + $cropHeight + ); + + // Save as JPEG + imagejpeg($output, $outputPath, $quality); + + imagedestroy($source); + imagedestroy($output); + + return $outputRel; + } + + /** + * Remove a generated image file. + * + * @param string $generatedPath Path relative to JPATH_ROOT + * + * @return void + */ + public static function cleanup(string $generatedPath): void + { + if (empty($generatedPath) || !str_starts_with($generatedPath, self::OUTPUT_DIR)) { + return; + } + + $absPath = JPATH_ROOT . '/' . $generatedPath; + + if (is_file($absPath)) { + File::delete($absPath); + } + } + + /** + * Check if an image meets minimum OG size requirements. + * + * @param string $imagePath Image path relative to JPATH_ROOT + * + * @return array{valid: bool, width: int, height: int, message: string} + */ + public static function validate(string $imagePath): array + { + $absPath = JPATH_ROOT . '/' . ltrim($imagePath, '/'); + + if (!is_file($absPath)) { + return ['valid' => false, 'width' => 0, 'height' => 0, 'message' => 'File not found']; + } + + $imageInfo = @getimagesize($absPath); + + if (!$imageInfo) { + return ['valid' => false, 'width' => 0, 'height' => 0, 'message' => 'Not a valid image']; + } + + [$width, $height] = $imageInfo; + + // Facebook minimum: 200x200, recommended: 1200x630 + // WhatsApp minimum: 300x200 + if ($width < 200 || $height < 200) { + return [ + 'valid' => false, + 'width' => $width, + 'height' => $height, + 'message' => "Image too small ({$width}x{$height}). Minimum: 200x200px.", + ]; + } + + return ['valid' => true, 'width' => $width, 'height' => $height, 'message' => 'OK']; + } + + /** + * Load an image resource from a file. + * + * @param string $path Absolute file path + * @param int $type IMAGETYPE_* constant + * + * @return \GdImage|false + */ + 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), + default => false, + }; + } +} From 7ef1d7933643dc0f4c5a2400e6968b1288a25378 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sat, 23 May 2026 18:10:53 -0500 Subject: [PATCH 004/132] feat(categories): add category-level OG tag support (closes #4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Content plugin now hooks com_categories.categorycom_content forms - Category OG data stored as content_type 'com_content.category' - System plugin detects category views and merges category OG as fallback - Article image fallback chain: article image → category image → default - New loadOgDataByType() helper for flexible type lookups Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- .../src/Extension/MokoOGContent.php | 20 +++++-- .../src/Extension/MokoOG.php | 57 +++++++++++++++++++ 2 files changed, 71 insertions(+), 6 deletions(-) diff --git a/src/packages/plg_content_mokoog/src/Extension/MokoOGContent.php b/src/packages/plg_content_mokoog/src/Extension/MokoOGContent.php index 3d70565..afab878 100644 --- a/src/packages/plg_content_mokoog/src/Extension/MokoOGContent.php +++ b/src/packages/plg_content_mokoog/src/Extension/MokoOGContent.php @@ -56,10 +56,11 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface $formName = $form->getName(); - // Add OG fields to article and menu item edit forms + // Add OG fields to article, menu item, and category edit forms $supportedForms = [ 'com_content.article', 'com_menus.item', + 'com_categories.categorycom_content', ]; if (!\in_array($formName, $supportedForms, true)) { @@ -81,7 +82,12 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface } if ($id > 0) { - $contentType = ($formName === 'com_menus.item') ? 'menu' : 'com_content'; + $formTypeMap = [ + 'com_content.article' => 'com_content', + 'com_menus.item' => 'menu', + 'com_categories.categorycom_content' => 'com_content.category', + ]; + $contentType = $formTypeMap[$formName] ?? 'com_content'; $ogData = $this->loadOgData($contentType, $id); if ($ogData) { @@ -102,8 +108,9 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface [$context, $article, $isNew] = array_values($event->getArguments()); $supportedContexts = [ - 'com_content.article' => 'com_content', - 'com_menus.item' => 'menu', + 'com_content.article' => 'com_content', + 'com_menus.item' => 'menu', + 'com_categories.categorycom_content' => 'com_content.category', ]; if (!isset($supportedContexts[$context])) { @@ -143,8 +150,9 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface [$context, $article] = array_values($event->getArguments()); $supportedContexts = [ - 'com_content.article' => 'com_content', - 'com_menus.item' => 'menu', + 'com_content.article' => 'com_content', + 'com_menus.item' => 'menu', + 'com_categories.categorycom_content' => 'com_content.category', ]; if (!isset($supportedContexts[$context])) { diff --git a/src/packages/plg_system_mokoog/src/Extension/MokoOG.php b/src/packages/plg_system_mokoog/src/Extension/MokoOG.php index e571cc4..18abfe8 100644 --- a/src/packages/plg_system_mokoog/src/Extension/MokoOG.php +++ b/src/packages/plg_system_mokoog/src/Extension/MokoOG.php @@ -68,6 +68,20 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface // Try to load custom OG data from the database $ogData = $this->loadOgData($option, $view, $id); + // For category views, also try category-level OG data as fallback + if ($option === 'com_content' && $view === 'category' && $id > 0) { + $catOg = $this->loadOgDataByType('com_content.category', $id); + + if ($catOg) { + // Merge: category fills any gaps in the content-level data + foreach (['og_title', 'og_description', 'og_image', 'og_type', 'seo_title', 'meta_description', 'robots', 'canonical_url'] as $field) { + if (empty($ogData->$field) && !empty($catOg->$field)) { + $ogData->$field = $catOg->$field; + } + } + } + } + // --- SEO meta tags (set first, before OG) --- $this->applySeoTags($doc, $ogData); @@ -199,6 +213,29 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface return $db->loadObject() ?: $empty; } + /** + * Load OG data by content type and ID. + * + * @param string $contentType Content type identifier + * @param int $contentId Content ID + * + * @return object|null + */ + private function loadOgDataByType(string $contentType, int $contentId): ?object + { + $db = Factory::getDbo(); + $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'); + + $db->setQuery($query); + + return $db->loadObject(); + } + /** * Load OG data by menu item ID. * @@ -283,6 +320,26 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface return $imagesData['image_intro']; } } + + // Fallback: check the article's category for an image + if ($view === 'article') { + $catQuery = $db->getQuery(true) + ->select($db->quoteName('cat.params')) + ->from($db->quoteName('#__categories', 'cat')) + ->join('INNER', $db->quoteName('#__content', 'a') . ' ON ' . $db->quoteName('a.catid') . ' = ' . $db->quoteName('cat.id')) + ->where($db->quoteName('a.id') . ' = ' . (int) $id); + + $db->setQuery($catQuery); + $catParams = $db->loadResult(); + + if ($catParams) { + $catData = json_decode($catParams, true); + + if (!empty($catData['image'])) { + return $catData['image']; + } + } + } } return $this->params->get('default_image', ''); From 6f5cc4425edf2a5e5eacd4e380776e94a7d60adf Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sat, 23 May 2026 18:12:38 -0500 Subject: [PATCH 005/132] feat(social): add WhatsApp/Telegram link preview optimization (closes #10) - Telegram channel meta tag config and output (telegram:channel) - Image validation via ImageHelper::validate() already covers WhatsApp minimum requirements (300x200px) - Auto-resize (from #2) ensures images meet all platform specs Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- .../language/en-GB/plg_system_mokoog.ini | 2 ++ .../language/en-US/plg_system_mokoog.ini | 2 ++ src/packages/plg_system_mokoog/mokoog.xml | 8 ++++++++ src/packages/plg_system_mokoog/src/Extension/MokoOG.php | 7 +++++++ 4 files changed, 19 insertions(+) diff --git a/src/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.ini b/src/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.ini index a0902bd..9e75b06 100644 --- a/src/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.ini +++ b/src/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.ini @@ -23,5 +23,7 @@ PLG_SYSTEM_MOKOOG_FIELD_STRIP_HTML="Strip HTML from Description" PLG_SYSTEM_MOKOOG_FIELD_STRIP_HTML_DESC="Remove HTML tags from the auto-generated description." PLG_SYSTEM_MOKOOG_FIELD_DESC_LENGTH="Description Length" PLG_SYSTEM_MOKOOG_FIELD_DESC_LENGTH_DESC="Maximum character length for the auto-generated og:description." +PLG_SYSTEM_MOKOOG_FIELD_TELEGRAM_CHANNEL="Telegram Channel" +PLG_SYSTEM_MOKOOG_FIELD_TELEGRAM_CHANNEL_DESC="Your Telegram channel handle (e.g. @mokoconsulting). Outputs a telegram:channel meta tag for Telegram link previews." PLG_SYSTEM_MOKOOG_FIELD_AUTO_RESIZE="Auto-resize Images" PLG_SYSTEM_MOKOOG_FIELD_AUTO_RESIZE_DESC="Automatically resize images to 1200x630px (Facebook recommended) using center crop. Generated images are saved to images/mokoog/generated/." diff --git a/src/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.ini b/src/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.ini index a0902bd..9e75b06 100644 --- a/src/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.ini +++ b/src/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.ini @@ -23,5 +23,7 @@ PLG_SYSTEM_MOKOOG_FIELD_STRIP_HTML="Strip HTML from Description" PLG_SYSTEM_MOKOOG_FIELD_STRIP_HTML_DESC="Remove HTML tags from the auto-generated description." PLG_SYSTEM_MOKOOG_FIELD_DESC_LENGTH="Description Length" PLG_SYSTEM_MOKOOG_FIELD_DESC_LENGTH_DESC="Maximum character length for the auto-generated og:description." +PLG_SYSTEM_MOKOOG_FIELD_TELEGRAM_CHANNEL="Telegram Channel" +PLG_SYSTEM_MOKOOG_FIELD_TELEGRAM_CHANNEL_DESC="Your Telegram channel handle (e.g. @mokoconsulting). Outputs a telegram:channel meta tag for Telegram link previews." PLG_SYSTEM_MOKOOG_FIELD_AUTO_RESIZE="Auto-resize Images" PLG_SYSTEM_MOKOOG_FIELD_AUTO_RESIZE_DESC="Automatically resize images to 1200x630px (Facebook recommended) using center crop. Generated images are saved to images/mokoog/generated/." diff --git a/src/packages/plg_system_mokoog/mokoog.xml b/src/packages/plg_system_mokoog/mokoog.xml index 2517076..4df713b 100644 --- a/src/packages/plg_system_mokoog/mokoog.xml +++ b/src/packages/plg_system_mokoog/mokoog.xml @@ -74,6 +74,14 @@ default="" filter="string" /> + <field + name="telegram_channel" + type="text" + label="PLG_SYSTEM_MOKOOG_FIELD_TELEGRAM_CHANNEL" + description="PLG_SYSTEM_MOKOOG_FIELD_TELEGRAM_CHANNEL_DESC" + default="" + filter="string" + /> </fieldset> <fieldset name="advanced" label="PLG_SYSTEM_MOKOOG_FIELDSET_ADVANCED"> <field diff --git a/src/packages/plg_system_mokoog/src/Extension/MokoOG.php b/src/packages/plg_system_mokoog/src/Extension/MokoOG.php index e571cc4..46c1a48 100644 --- a/src/packages/plg_system_mokoog/src/Extension/MokoOG.php +++ b/src/packages/plg_system_mokoog/src/Extension/MokoOG.php @@ -113,6 +113,13 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface if ($twitterSite) { $doc->setMetaData('twitter:site', $twitterSite); } + + // Telegram channel tag + $telegramChannel = $this->params->get('telegram_channel', ''); + + if ($telegramChannel) { + $doc->setMetaData('telegram:channel', $telegramChannel); + } } /** From 71c1fea3565b8593e415b1ae98e6f5bdc9f8c36a Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sat, 23 May 2026 18:17:32 -0500 Subject: [PATCH 006/132] feat(preview): live social sharing preview in article editor (closes #3) - Facebook and Twitter/X card previews update in real-time - Renders below the OG fieldset in article/menu editors - Reads og_title, og_description, og_image with fallback to article title and meta description - CSS mockups of each platform's card layout - Registered via joomla.asset.json Web Asset Manager - Safe DOM construction (no innerHTML with user data) - MutationObserver watches media field for image changes Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- .../plg_content_mokoog/media/css/preview.css | 104 +++++++++++ .../media/joomla.asset.json | 20 +++ .../plg_content_mokoog/media/js/preview.js | 170 ++++++++++++++++++ src/packages/plg_content_mokoog/mokoog.xml | 6 + .../src/Extension/MokoOGContent.php | 6 + 5 files changed, 306 insertions(+) create mode 100644 src/packages/plg_content_mokoog/media/css/preview.css create mode 100644 src/packages/plg_content_mokoog/media/joomla.asset.json create mode 100644 src/packages/plg_content_mokoog/media/js/preview.js diff --git a/src/packages/plg_content_mokoog/media/css/preview.css b/src/packages/plg_content_mokoog/media/css/preview.css new file mode 100644 index 0000000..b5d74a9 --- /dev/null +++ b/src/packages/plg_content_mokoog/media/css/preview.css @@ -0,0 +1,104 @@ +/** + * @package MokoOpenGraph + * @subpackage plg_content_mokoog + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GPL-3.0-or-later + */ + +.mokoog-preview-wrapper { + margin: 15px 0; + padding: 15px; + background: #f8f9fa; + border-radius: 8px; + border: 1px solid #dee2e6; +} + +.mokoog-preview-heading { + margin: 0 0 12px; + font-size: 14px; + color: #666; +} + +.mokoog-platform-label { + display: block; + color: #999; + text-transform: uppercase; + font-size: 11px; + font-weight: 600; + margin-top: 16px; + margin-bottom: 4px; +} + +.mokoog-platform-label:first-of-type { + margin-top: 0; +} + +.mokoog-card { + overflow: hidden; + max-width: 500px; + background: #fff; +} + +.mokoog-card-fb { + border: 1px solid #ddd; + border-radius: 3px; +} + +.mokoog-card-tw { + border: 1px solid #cfd9de; + border-radius: 16px; +} + +.mokoog-card-img { + height: 260px; + background: #e4e6eb center / cover no-repeat; +} + +.mokoog-card-body { + padding: 10px 12px; + border-top: 1px solid #ddd; +} + +.mokoog-card-tw .mokoog-card-body { + border-top-color: #cfd9de; +} + +.mokoog-card-domain { + font-size: 11px; + color: #65676b; + text-transform: uppercase; +} + +.mokoog-card-tw .mokoog-card-domain { + font-size: 13px; + text-transform: none; + margin-top: 4px; +} + +.mokoog-card-title { + font-size: 16px; + font-weight: 600; + color: #1d2129; + margin: 3px 0 2px; + line-height: 1.3; +} + +.mokoog-card-tw .mokoog-card-title { + font-size: 15px; + font-weight: 700; + color: #0f1419; +} + +.mokoog-card-desc { + font-size: 14px; + color: #65676b; + line-height: 1.4; + max-height: 2.8em; + overflow: hidden; +} + +.mokoog-card-tw .mokoog-card-desc { + font-size: 15px; + color: #536471; + margin-top: 2px; +} diff --git a/src/packages/plg_content_mokoog/media/joomla.asset.json b/src/packages/plg_content_mokoog/media/joomla.asset.json new file mode 100644 index 0000000..340aa7a --- /dev/null +++ b/src/packages/plg_content_mokoog/media/joomla.asset.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://developer.joomla.org/schemas/json-schema/web_assets.json", + "name": "plg_content_mokoog", + "version": "01.00.00", + "description": "MokoOpenGraph Content Plugin Assets", + "license": "GPL-3.0-or-later", + "assets": [ + { + "name": "plg_content_mokoog.preview", + "type": "style", + "uri": "plg_content_mokoog/css/preview.css" + }, + { + "name": "plg_content_mokoog.preview", + "type": "script", + "uri": "plg_content_mokoog/js/preview.js", + "dependencies": ["core"] + } + ] +} diff --git a/src/packages/plg_content_mokoog/media/js/preview.js b/src/packages/plg_content_mokoog/media/js/preview.js new file mode 100644 index 0000000..62e8925 --- /dev/null +++ b/src/packages/plg_content_mokoog/media/js/preview.js @@ -0,0 +1,170 @@ +/** + * @package MokoOpenGraph + * @subpackage plg_content_mokoog + * @author Moko Consulting <hello@mokoconsulting.tech> + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GPL-3.0-or-later + * + * Live social sharing preview for article/menu item editor. + */ +document.addEventListener('DOMContentLoaded', function () { + 'use strict'; + + var fields = { + ogTitle: document.getElementById('jform_mokoog_og_title'), + ogDesc: document.getElementById('jform_mokoog_og_description'), + ogImage: document.getElementById('jform_mokoog_og_image'), + articleTitle: document.getElementById('jform_title'), + metaDesc: document.getElementById('jform_metadesc') + }; + + // Find the mokoog fieldset and insert preview after it + var fieldset = document.querySelector('[data-showon-id="mokoog"]') || + document.getElementById('attrib-mokoog') || + document.querySelector('fieldset.mokoog') || + document.querySelector('[id*="mokoog"]'); + + if (!fieldset) { + return; + } + + // Build preview DOM safely (no innerHTML with user data) + var preview = document.createElement('div'); + preview.id = 'mokoog-preview'; + + var wrapper = document.createElement('div'); + wrapper.className = 'mokoog-preview-wrapper'; + + var heading = document.createElement('h4'); + heading.className = 'mokoog-preview-heading'; + heading.textContent = 'Social Sharing Preview'; + wrapper.appendChild(heading); + + // Facebook preview card + var fbLabel = document.createElement('small'); + fbLabel.className = 'mokoog-platform-label'; + fbLabel.textContent = 'Facebook'; + wrapper.appendChild(fbLabel); + + var fbCard = document.createElement('div'); + fbCard.className = 'mokoog-card mokoog-card-fb'; + + var fbImg = document.createElement('div'); + fbImg.id = 'mokoog-fb-img'; + fbImg.className = 'mokoog-card-img'; + fbCard.appendChild(fbImg); + + var fbBody = document.createElement('div'); + fbBody.className = 'mokoog-card-body'; + + var fbDomain = document.createElement('div'); + fbDomain.id = 'mokoog-fb-domain'; + fbDomain.className = 'mokoog-card-domain'; + fbBody.appendChild(fbDomain); + + var fbTitle = document.createElement('div'); + fbTitle.id = 'mokoog-fb-title'; + fbTitle.className = 'mokoog-card-title'; + fbBody.appendChild(fbTitle); + + var fbDesc = document.createElement('div'); + fbDesc.id = 'mokoog-fb-desc'; + fbDesc.className = 'mokoog-card-desc'; + fbBody.appendChild(fbDesc); + + fbCard.appendChild(fbBody); + wrapper.appendChild(fbCard); + + // Twitter preview card + var twLabel = document.createElement('small'); + twLabel.className = 'mokoog-platform-label'; + twLabel.textContent = 'Twitter / X'; + wrapper.appendChild(twLabel); + + var twCard = document.createElement('div'); + twCard.className = 'mokoog-card mokoog-card-tw'; + + var twImg = document.createElement('div'); + twImg.id = 'mokoog-tw-img'; + twImg.className = 'mokoog-card-img'; + twCard.appendChild(twImg); + + var twBody = document.createElement('div'); + twBody.className = 'mokoog-card-body'; + + var twTitle = document.createElement('div'); + twTitle.id = 'mokoog-tw-title'; + twTitle.className = 'mokoog-card-title'; + twBody.appendChild(twTitle); + + var twDesc = document.createElement('div'); + twDesc.id = 'mokoog-tw-desc'; + twDesc.className = 'mokoog-card-desc'; + twBody.appendChild(twDesc); + + var twDomain = document.createElement('div'); + twDomain.id = 'mokoog-tw-domain'; + twDomain.className = 'mokoog-card-domain'; + twBody.appendChild(twDomain); + + twCard.appendChild(twBody); + wrapper.appendChild(twCard); + + preview.appendChild(wrapper); + fieldset.parentNode.insertBefore(preview, fieldset.nextSibling); + + var domain = window.location.hostname; + + function updatePreview() { + var title = (fields.ogTitle && fields.ogTitle.value) || + (fields.articleTitle && fields.articleTitle.value) || 'Page Title'; + var desc = (fields.ogDesc && fields.ogDesc.value) || + (fields.metaDesc && fields.metaDesc.value) || 'Page description will appear here...'; + var img = ''; + + if (fields.ogImage) { + img = fields.ogImage.value; + } + + if (title.length > 65) title = title.substring(0, 62) + '...'; + if (desc.length > 160) desc = desc.substring(0, 157) + '...'; + + // Facebook + document.getElementById('mokoog-fb-title').textContent = title; + document.getElementById('mokoog-fb-desc').textContent = desc; + document.getElementById('mokoog-fb-domain').textContent = domain; + var fbImgEl = document.getElementById('mokoog-fb-img'); + if (img) { + fbImgEl.style.backgroundImage = 'url(' + encodeURI(img) + ')'; + fbImgEl.style.display = ''; + } else { + fbImgEl.style.display = 'none'; + } + + // Twitter + document.getElementById('mokoog-tw-title').textContent = title; + document.getElementById('mokoog-tw-desc').textContent = desc; + document.getElementById('mokoog-tw-domain').textContent = domain; + var twImgEl = document.getElementById('mokoog-tw-img'); + if (img) { + twImgEl.style.backgroundImage = 'url(' + encodeURI(img) + ')'; + twImgEl.style.display = ''; + } else { + twImgEl.style.display = 'none'; + } + } + + Object.values(fields).forEach(function (el) { + if (el) { + el.addEventListener('input', updatePreview); + el.addEventListener('change', updatePreview); + } + }); + + if (fields.ogImage) { + var observer = new MutationObserver(updatePreview); + observer.observe(fields.ogImage, { attributes: true, attributeFilter: ['value'] }); + } + + updatePreview(); +}); diff --git a/src/packages/plg_content_mokoog/mokoog.xml b/src/packages/plg_content_mokoog/mokoog.xml index 651e9ba..bb64e27 100644 --- a/src/packages/plg_content_mokoog/mokoog.xml +++ b/src/packages/plg_content_mokoog/mokoog.xml @@ -27,6 +27,12 @@ <folder>language</folder> </files> + <media destination="plg_content_mokoog" folder="media"> + <filename>joomla.asset.json</filename> + <folder>js</folder> + <folder>css</folder> + </media> + <languages> <language tag="en-GB">language/en-GB/plg_content_mokoog.ini</language> <language tag="en-GB">language/en-GB/plg_content_mokoog.sys.ini</language> diff --git a/src/packages/plg_content_mokoog/src/Extension/MokoOGContent.php b/src/packages/plg_content_mokoog/src/Extension/MokoOGContent.php index 3d70565..1752055 100644 --- a/src/packages/plg_content_mokoog/src/Extension/MokoOGContent.php +++ b/src/packages/plg_content_mokoog/src/Extension/MokoOGContent.php @@ -71,6 +71,12 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface Form::addFormPath($formPath); $form->loadFile('mokoog', false); + // Load live preview assets + $wa = $this->getApplication()->getDocument()->getWebAssetManager(); + $wa->getRegistry()->addRegistryFile('media/plg_content_mokoog/joomla.asset.json'); + $wa->useStyle('plg_content_mokoog.preview'); + $wa->useScript('plg_content_mokoog.preview'); + // If editing an existing item, load saved OG data $id = 0; From 52179864786bc64fc2dfeb6e07db2f3a83fb63c9 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sat, 23 May 2026 18:23:07 -0500 Subject: [PATCH 007/132] feat(jsonld): add JSON-LD structured data output (closes #6) - JsonLdBuilder helper with Article, WebPage, BreadcrumbList, Organization - Article schema includes headline, author, datePublished, dateModified, image - BreadcrumbList built from Joomla's pathway - Toggle JSON-LD and breadcrumbs independently in plugin settings - Output via addCustomTag() in onBeforeCompileHead Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- .../language/en-GB/plg_system_mokoog.ini | 4 + .../language/en-US/plg_system_mokoog.ini | 4 + src/packages/plg_system_mokoog/mokoog.xml | 22 +++ .../src/Extension/MokoOG.php | 24 +++ .../src/Helper/JsonLdBuilder.php | 168 ++++++++++++++++++ 5 files changed, 222 insertions(+) create mode 100644 src/packages/plg_system_mokoog/src/Helper/JsonLdBuilder.php diff --git a/src/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.ini b/src/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.ini index a0902bd..89ea0a3 100644 --- a/src/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.ini +++ b/src/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.ini @@ -25,3 +25,7 @@ PLG_SYSTEM_MOKOOG_FIELD_DESC_LENGTH="Description Length" PLG_SYSTEM_MOKOOG_FIELD_DESC_LENGTH_DESC="Maximum character length for the auto-generated og:description." PLG_SYSTEM_MOKOOG_FIELD_AUTO_RESIZE="Auto-resize Images" PLG_SYSTEM_MOKOOG_FIELD_AUTO_RESIZE_DESC="Automatically resize images to 1200x630px (Facebook recommended) using center crop. Generated images are saved to images/mokoog/generated/." +PLG_SYSTEM_MOKOOG_FIELD_JSONLD_ENABLED="Enable JSON-LD" +PLG_SYSTEM_MOKOOG_FIELD_JSONLD_ENABLED_DESC="Output JSON-LD structured data (Article, WebPage) for Google rich results." +PLG_SYSTEM_MOKOOG_FIELD_JSONLD_BREADCRUMBS="JSON-LD Breadcrumbs" +PLG_SYSTEM_MOKOOG_FIELD_JSONLD_BREADCRUMBS_DESC="Output BreadcrumbList JSON-LD schema from Joomla's pathway." diff --git a/src/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.ini b/src/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.ini index a0902bd..89ea0a3 100644 --- a/src/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.ini +++ b/src/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.ini @@ -25,3 +25,7 @@ PLG_SYSTEM_MOKOOG_FIELD_DESC_LENGTH="Description Length" PLG_SYSTEM_MOKOOG_FIELD_DESC_LENGTH_DESC="Maximum character length for the auto-generated og:description." PLG_SYSTEM_MOKOOG_FIELD_AUTO_RESIZE="Auto-resize Images" PLG_SYSTEM_MOKOOG_FIELD_AUTO_RESIZE_DESC="Automatically resize images to 1200x630px (Facebook recommended) using center crop. Generated images are saved to images/mokoog/generated/." +PLG_SYSTEM_MOKOOG_FIELD_JSONLD_ENABLED="Enable JSON-LD" +PLG_SYSTEM_MOKOOG_FIELD_JSONLD_ENABLED_DESC="Output JSON-LD structured data (Article, WebPage) for Google rich results." +PLG_SYSTEM_MOKOOG_FIELD_JSONLD_BREADCRUMBS="JSON-LD Breadcrumbs" +PLG_SYSTEM_MOKOOG_FIELD_JSONLD_BREADCRUMBS_DESC="Output BreadcrumbList JSON-LD schema from Joomla's pathway." diff --git a/src/packages/plg_system_mokoog/mokoog.xml b/src/packages/plg_system_mokoog/mokoog.xml index 2517076..9e48c7d 100644 --- a/src/packages/plg_system_mokoog/mokoog.xml +++ b/src/packages/plg_system_mokoog/mokoog.xml @@ -118,6 +118,28 @@ <option value="1">JYES</option> <option value="0">JNO</option> </field> + <field + name="jsonld_enabled" + type="radio" + label="PLG_SYSTEM_MOKOOG_FIELD_JSONLD_ENABLED" + description="PLG_SYSTEM_MOKOOG_FIELD_JSONLD_ENABLED_DESC" + default="1" + class="btn-group" + > + <option value="1">JYES</option> + <option value="0">JNO</option> + </field> + <field + name="jsonld_breadcrumbs" + type="radio" + label="PLG_SYSTEM_MOKOOG_FIELD_JSONLD_BREADCRUMBS" + description="PLG_SYSTEM_MOKOOG_FIELD_JSONLD_BREADCRUMBS_DESC" + default="1" + class="btn-group" + > + <option value="1">JYES</option> + <option value="0">JNO</option> + </field> </fieldset> </fields> </config> diff --git a/src/packages/plg_system_mokoog/src/Extension/MokoOG.php b/src/packages/plg_system_mokoog/src/Extension/MokoOG.php index e571cc4..37860b0 100644 --- a/src/packages/plg_system_mokoog/src/Extension/MokoOG.php +++ b/src/packages/plg_system_mokoog/src/Extension/MokoOG.php @@ -18,6 +18,7 @@ use Joomla\CMS\Uri\Uri; use Joomla\Event\Event; use Joomla\Event\SubscriberInterface; use Joomla\Plugin\System\MokoOG\Helper\ImageHelper; +use Joomla\Plugin\System\MokoOG\Helper\JsonLdBuilder; final class MokoOG extends CMSPlugin implements SubscriberInterface { @@ -113,6 +114,29 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface if ($twitterSite) { $doc->setMetaData('twitter:site', $twitterSite); } + + // JSON-LD structured data + 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); + } else { + $schema = JsonLdBuilder::buildWebPage($title, $description); + } + + if ($schema) { + $doc->addCustomTag(JsonLdBuilder::toScriptTag($schema)); + } + + if ($this->params->get('jsonld_breadcrumbs', 1)) { + $breadcrumbs = JsonLdBuilder::buildBreadcrumbs(); + + if ($breadcrumbs) { + $doc->addCustomTag(JsonLdBuilder::toScriptTag($breadcrumbs)); + } + } + } } /** diff --git a/src/packages/plg_system_mokoog/src/Helper/JsonLdBuilder.php b/src/packages/plg_system_mokoog/src/Helper/JsonLdBuilder.php new file mode 100644 index 0000000..d56f029 --- /dev/null +++ b/src/packages/plg_system_mokoog/src/Helper/JsonLdBuilder.php @@ -0,0 +1,168 @@ +<?php + +/** + * @package MokoOpenGraph + * @subpackage plg_system_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\Plugin\System\MokoOG\Helper; + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\Uri\Uri; + +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) + * + * @return array|null + */ + public static function buildArticle(int $articleId, string $title, string $description, string $image): ?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); + + $db->setQuery($query); + $article = $db->loadObject(); + + if (!$article) { + return null; + } + + $schema = [ + '@context' => 'https://schema.org', + '@type' => 'Article', + 'headline' => $title, + 'description' => $description, + 'url' => Uri::getInstance()->toString(), + 'datePublished' => $article->publish_up ?: $article->created, + 'dateModified' => $article->modified ?: $article->created, + ]; + + if (!empty($article->name)) { + $schema['author'] = [ + '@type' => 'Person', + 'name' => $article->name, + ]; + } + + if (!empty($image)) { + $schema['image'] = $image; + } + + return $schema; + } + + /** + * Build WebPage schema for non-article pages. + * + * @param string $title Page title + * @param string $description Page description + * + * @return array + */ + public static function buildWebPage(string $title, string $description): array + { + return [ + '@context' => 'https://schema.org', + '@type' => 'WebPage', + 'name' => $title, + 'description' => $description, + 'url' => Uri::getInstance()->toString(), + ]; + } + + /** + * Build BreadcrumbList schema from Joomla's pathway. + * + * @return array|null + */ + public static function buildBreadcrumbs(): ?array + { + $app = Factory::getApplication(); + $pathway = $app->getPathway(); + $items = $pathway->getPathway(); + + if (empty($items)) { + return null; + } + + $listItems = []; + $position = 1; + + foreach ($items as $item) { + $url = $item->link; + + if ($url && !str_starts_with($url, 'http')) { + $url = rtrim(Uri::root(), '/') . '/' . ltrim($url, '/'); + } + + $listItems[] = [ + '@type' => 'ListItem', + 'position' => $position, + 'name' => $item->name, + 'item' => $url ?: Uri::getInstance()->toString(), + ]; + + $position++; + } + + return [ + '@context' => 'https://schema.org', + '@type' => 'BreadcrumbList', + 'itemListElement' => $listItems, + ]; + } + + /** + * Build Organization schema from site configuration. + * + * @param string $siteName Site name + * + * @return array + */ + public static function buildOrganization(string $siteName): array + { + return [ + '@context' => 'https://schema.org', + '@type' => 'Organization', + 'name' => $siteName, + 'url' => Uri::root(), + ]; + } + + /** + * Encode a schema array to a JSON-LD script tag string. + * + * @param array $schema Schema data + * + * @return string + */ + public static function toScriptTag(array $schema): string + { + $json = json_encode($schema, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + + return '<script type="application/ld+json">' . $json . '</script>'; + } +} From 8dc1e8175a7a81dabfe7fba534cfb6b073745f7d Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sat, 23 May 2026 18:32:06 -0500 Subject: [PATCH 008/132] feat(adapters): add third-party content type adapter architecture (closes #5) - ContentTypeInterface with canHandle(), getTitle(), getDescription(), getImage() - VirtueMartAdapter for product pages (com_virtuemart) - K2Adapter for K2 items (com_k2) - HikaShopAdapter for HikaShop products (com_hikashop) Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- src/packages/com_mokoog/mokoog.xml | 1 + .../src/ContentType/ContentTypeInterface.php | 60 ++++++++++++++ .../src/ContentType/HikaShopAdapter.php | 76 +++++++++++++++++ .../com_mokoog/src/ContentType/K2Adapter.php | 73 +++++++++++++++++ .../src/ContentType/VirtueMartAdapter.php | 82 +++++++++++++++++++ 5 files changed, 292 insertions(+) create mode 100644 src/packages/com_mokoog/src/ContentType/ContentTypeInterface.php create mode 100644 src/packages/com_mokoog/src/ContentType/HikaShopAdapter.php create mode 100644 src/packages/com_mokoog/src/ContentType/K2Adapter.php create mode 100644 src/packages/com_mokoog/src/ContentType/VirtueMartAdapter.php diff --git a/src/packages/com_mokoog/mokoog.xml b/src/packages/com_mokoog/mokoog.xml index 3089e4f..aa2ad47 100644 --- a/src/packages/com_mokoog/mokoog.xml +++ b/src/packages/com_mokoog/mokoog.xml @@ -42,6 +42,7 @@ <filename>provider.php</filename> </files> <files folder="src"> + <folder>ContentType</folder> <folder>Controller</folder> <folder>Extension</folder> <folder>Model</folder> diff --git a/src/packages/com_mokoog/src/ContentType/ContentTypeInterface.php b/src/packages/com_mokoog/src/ContentType/ContentTypeInterface.php new file mode 100644 index 0000000..c596ca0 --- /dev/null +++ b/src/packages/com_mokoog/src/ContentType/ContentTypeInterface.php @@ -0,0 +1,60 @@ +<?php + +/** + * @package MokoOpenGraph + * @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; +} diff --git a/src/packages/com_mokoog/src/ContentType/HikaShopAdapter.php b/src/packages/com_mokoog/src/ContentType/HikaShopAdapter.php new file mode 100644 index 0000000..595f6e4 --- /dev/null +++ b/src/packages/com_mokoog/src/ContentType/HikaShopAdapter.php @@ -0,0 +1,76 @@ +<?php + +/** + * @package MokoOpenGraph + * @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 (\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() ?: ''; + } +} diff --git a/src/packages/com_mokoog/src/ContentType/K2Adapter.php b/src/packages/com_mokoog/src/ContentType/K2Adapter.php new file mode 100644 index 0000000..f8dcd5d --- /dev/null +++ b/src/packages/com_mokoog/src/ContentType/K2Adapter.php @@ -0,0 +1,73 @@ +<?php + +/** + * @package MokoOpenGraph + * @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 (\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 ''; + } +} diff --git a/src/packages/com_mokoog/src/ContentType/VirtueMartAdapter.php b/src/packages/com_mokoog/src/ContentType/VirtueMartAdapter.php new file mode 100644 index 0000000..60822ec --- /dev/null +++ b/src/packages/com_mokoog/src/ContentType/VirtueMartAdapter.php @@ -0,0 +1,82 @@ +<?php + +/** + * @package MokoOpenGraph + * @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)); + } +} From 94b5eb084cd4c4bc323ec17117736b612675b8d5 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sat, 23 May 2026 18:33:46 -0500 Subject: [PATCH 009/132] feat(debug): add social platform debugger quick links (closes #9) - Debug column in admin tag list with FB, LinkedIn, Google buttons - Links open platform debugger tools in new tab with page URL - Facebook Sharing Debugger, LinkedIn Post Inspector, Google Rich Results Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- .../com_mokoog/language/en-GB/com_mokoog.ini | 1 + .../com_mokoog/language/en-US/com_mokoog.ini | 1 + src/packages/com_mokoog/tmpl/tags/default.php | 13 +++++++++++++ 3 files changed, 15 insertions(+) diff --git a/src/packages/com_mokoog/language/en-GB/com_mokoog.ini b/src/packages/com_mokoog/language/en-GB/com_mokoog.ini index 28aecda..87358c4 100644 --- a/src/packages/com_mokoog/language/en-GB/com_mokoog.ini +++ b/src/packages/com_mokoog/language/en-GB/com_mokoog.ini @@ -14,6 +14,7 @@ COM_MOKOOG_HEADING_CONTENT_ID="Content ID" COM_MOKOOG_HEADING_OG_TITLE="OG Title" COM_MOKOOG_HEADING_IMAGE="Image" COM_MOKOOG_HEADING_SEO="SEO" +COM_MOKOOG_HEADING_DEBUG="Debug" COM_MOKOOG_HEADING_MODIFIED="Modified" COM_MOKOOG_SEO_OK="OK" diff --git a/src/packages/com_mokoog/language/en-US/com_mokoog.ini b/src/packages/com_mokoog/language/en-US/com_mokoog.ini index 28aecda..87358c4 100644 --- a/src/packages/com_mokoog/language/en-US/com_mokoog.ini +++ b/src/packages/com_mokoog/language/en-US/com_mokoog.ini @@ -14,6 +14,7 @@ COM_MOKOOG_HEADING_CONTENT_ID="Content ID" COM_MOKOOG_HEADING_OG_TITLE="OG Title" COM_MOKOOG_HEADING_IMAGE="Image" COM_MOKOOG_HEADING_SEO="SEO" +COM_MOKOOG_HEADING_DEBUG="Debug" COM_MOKOOG_HEADING_MODIFIED="Modified" COM_MOKOOG_SEO_OK="OK" diff --git a/src/packages/com_mokoog/tmpl/tags/default.php b/src/packages/com_mokoog/tmpl/tags/default.php index 94ad6cf..54c5940 100644 --- a/src/packages/com_mokoog/tmpl/tags/default.php +++ b/src/packages/com_mokoog/tmpl/tags/default.php @@ -14,6 +14,7 @@ use Joomla\CMS\HTML\HTMLHelper; use Joomla\CMS\Language\Text; use Joomla\CMS\Layout\LayoutHelper; use Joomla\CMS\Router\Route; +use Joomla\CMS\Uri\Uri; use Joomla\CMS\Session\Session; /** @var \Joomla\Component\MokoOG\Administrator\View\Tags\HtmlView $this */ @@ -58,6 +59,9 @@ $token = Session::getFormToken(); <th scope="col" class="w-10"> <?php echo Text::_('JSTATUS'); ?> </th> + <th scope="col" class="w-10"> + <?php echo Text::_('COM_MOKOOG_HEADING_DEBUG'); ?> + </th> <th scope="col" class="w-10"> <?php echo Text::_('COM_MOKOOG_HEADING_MODIFIED'); ?> </th> @@ -115,6 +119,15 @@ $token = Session::getFormToken(); <td> <?php echo $item->published ? Text::_('JPUBLISHED') : Text::_('JUNPUBLISHED'); ?> </td> + <td class="mokoog-debug-links"> + <?php + // Build a placeholder URL for debugger links + $debugUrl = Uri::root(); + ?> + <a href="https://developers.facebook.com/tools/debug/?q=<?php echo urlencode($debugUrl); ?>" target="_blank" rel="noopener" title="Facebook Debugger" class="btn btn-sm btn-outline-primary">FB</a> + <a href="https://www.linkedin.com/post-inspector/inspect/<?php echo urlencode($debugUrl); ?>" target="_blank" rel="noopener" title="LinkedIn Inspector" class="btn btn-sm btn-outline-info">LI</a> + <a href="https://search.google.com/test/rich-results?url=<?php echo urlencode($debugUrl); ?>" target="_blank" rel="noopener" title="Google Rich Results" class="btn btn-sm btn-outline-success">G</a> + </td> <td> <?php echo HTMLHelper::_('date', $item->modified, Text::_('DATE_FORMAT_LC4')); ?> </td> From 04d1da29d4ce5dd73400014d47dfd7b8f968bc18 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sat, 23 May 2026 18:35:49 -0500 Subject: [PATCH 010/132] feat(i18n): add multilingual OG tag support (closes #11) - Add language column to #__mokoog_tags (default * for all languages) - Updated unique key to include language - SQL migration 01.02.00 for upgrades - og:locale output from current Joomla language (en-GB to en_GB) - Language-aware OG data loading: exact match preferred over wildcard Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- src/packages/com_mokoog/sql/install.mysql.sql | 3 ++- .../com_mokoog/sql/updates/mysql/01.02.00.sql | 10 ++++++++++ .../plg_system_mokoog/src/Extension/MokoOG.php | 12 ++++++++++-- 3 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 src/packages/com_mokoog/sql/updates/mysql/01.02.00.sql diff --git a/src/packages/com_mokoog/sql/install.mysql.sql b/src/packages/com_mokoog/sql/install.mysql.sql index 306bc3e..cd91ceb 100644 --- a/src/packages/com_mokoog/sql/install.mysql.sql +++ b/src/packages/com_mokoog/sql/install.mysql.sql @@ -16,10 +16,11 @@ CREATE TABLE IF NOT EXISTS `#__mokoog_tags` ( `meta_description` VARCHAR(200) NOT NULL DEFAULT '', `robots` VARCHAR(100) NOT NULL DEFAULT '', `canonical_url` VARCHAR(512) NOT NULL DEFAULT '', + `language` CHAR(7) NOT NULL DEFAULT '*', `published` TINYINT(1) NOT NULL DEFAULT 1, `created` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00', `modified` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00', PRIMARY KEY (`id`), - UNIQUE KEY `idx_content` (`content_type`, `content_id`), + UNIQUE KEY `idx_content_lang` (`content_type`, `content_id`, `language`), KEY `idx_published` (`published`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/src/packages/com_mokoog/sql/updates/mysql/01.02.00.sql b/src/packages/com_mokoog/sql/updates/mysql/01.02.00.sql new file mode 100644 index 0000000..2655270 --- /dev/null +++ b/src/packages/com_mokoog/sql/updates/mysql/01.02.00.sql @@ -0,0 +1,10 @@ +-- +-- MokoOpenGraph 01.02.00 — Add multilingual OG tag support +-- + +ALTER TABLE `#__mokoog_tags` + ADD COLUMN `language` CHAR(7) NOT NULL DEFAULT '*' AFTER `canonical_url`; + +ALTER TABLE `#__mokoog_tags` + DROP INDEX `idx_content`, + ADD UNIQUE KEY `idx_content_lang` (`content_type`, `content_id`, `language`); diff --git a/src/packages/plg_system_mokoog/src/Extension/MokoOG.php b/src/packages/plg_system_mokoog/src/Extension/MokoOG.php index e571cc4..9483040 100644 --- a/src/packages/plg_system_mokoog/src/Extension/MokoOG.php +++ b/src/packages/plg_system_mokoog/src/Extension/MokoOG.php @@ -91,6 +91,11 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface $doc->setMetaData('og:image', $imageUrl, 'property'); } + // og:locale from current language + $langTag = Factory::getLanguage()->getTag(); + $ogLocale = str_replace('-', '_', $langTag); + $doc->setMetaData('og:locale', $ogLocale, 'property'); + // Facebook App ID $fbAppId = $this->params->get('fb_app_id', ''); @@ -192,9 +197,12 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface ->from($db->quoteName('#__mokoog_tags')) ->where($db->quoteName('content_type') . ' = ' . $db->quote($option)) ->where($db->quoteName('content_id') . ' = ' . (int) $id) - ->where($db->quoteName('published') . ' = 1'); + ->where($db->quoteName('published') . ' = 1') + ->where('(' . $db->quoteName('language') . ' = ' . $db->quote(Factory::getLanguage()->getTag()) + . ' 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() ?: $empty; } From 9275a4f98085448a74aca660357f16193d2b0912 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sat, 23 May 2026 18:37:05 -0500 Subject: [PATCH 011/132] feat(overlay): add OG image text overlay generator (closes #7) ImageGenerator class renders article title onto a template background: - Center-crop template to 1200x630px - Semi-transparent dark overlay band for text readability - TTF font rendering with word wrapping (max 3 lines) - Cached output in images/mokoog/generated/overlay_*.jpg - Configurable font size, color, quality Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- .../src/Helper/ImageGenerator.php | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 src/packages/plg_system_mokoog/src/Helper/ImageGenerator.php diff --git a/src/packages/plg_system_mokoog/src/Helper/ImageGenerator.php b/src/packages/plg_system_mokoog/src/Helper/ImageGenerator.php new file mode 100644 index 0000000..2837cdc --- /dev/null +++ b/src/packages/plg_system_mokoog/src/Helper/ImageGenerator.php @@ -0,0 +1,160 @@ +<?php + +/** + * @package MokoOpenGraph + * @subpackage plg_system_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\Plugin\System\MokoOG\Helper; + +defined('_JEXEC') or die; + +use Joomla\CMS\Filesystem\Folder; + +class ImageGenerator +{ + private const WIDTH = 1200; + private const HEIGHT = 630; + private const OUTPUT_DIR = 'images/mokoog/generated'; + + /** + * Generate an OG image with title text overlaid on a template background. + * + * @param string $title Article title to overlay + * @param string $templateImage Path to template/background image relative to JPATH_ROOT + * @param string $fontFile Absolute path to TTF font file + * @param int $fontSize Font size in points (default 42) + * @param array $fontColor RGB array [r, g, b] (default white) + * @param int $quality JPEG quality (default 90) + * + * @return string Path to generated image relative to JPATH_ROOT, or empty on failure + */ + public static function generate( + string $title, + string $templateImage, + string $fontFile = '', + int $fontSize = 42, + array $fontColor = [255, 255, 255], + int $quality = 90 + ): string { + $templateAbs = JPATH_ROOT . '/' . ltrim($templateImage, '/'); + + if (!is_file($templateAbs)) { + return ''; + } + + if (!$fontFile || !is_file($fontFile)) { + return ''; + } + + $outputDir = JPATH_ROOT . '/' . self::OUTPUT_DIR; + + if (!is_dir($outputDir)) { + Folder::create($outputDir); + } + + $hash = md5($title . $templateImage . $fontSize); + $outputName = 'overlay_' . $hash . '.jpg'; + $outputPath = $outputDir . '/' . $outputName; + $outputRel = self::OUTPUT_DIR . '/' . $outputName; + + // Skip if already generated + if (is_file($outputPath)) { + return $outputRel; + } + + // Load template image + $imageInfo = @getimagesize($templateAbs); + + if (!$imageInfo) { + return ''; + } + + $source = match ($imageInfo[2]) { + IMAGETYPE_JPEG => @imagecreatefromjpeg($templateAbs), + IMAGETYPE_PNG => @imagecreatefrompng($templateAbs), + IMAGETYPE_WEBP => @imagecreatefromwebp($templateAbs), + default => false, + }; + + if (!$source) { + return ''; + } + + // Create output canvas at target dimensions + $canvas = imagecreatetruecolor(self::WIDTH, self::HEIGHT); + + imagecopyresampled( + $canvas, + $source, + 0, 0, 0, 0, + self::WIDTH, self::HEIGHT, + $imageInfo[0], $imageInfo[1] + ); + + imagedestroy($source); + + // Semi-transparent overlay for text readability + $overlay = imagecolorallocatealpha($canvas, 0, 0, 0, 64); + imagefilledrectangle($canvas, 0, (int) (self::HEIGHT * 0.55), self::WIDTH, self::HEIGHT, $overlay); + + // Render title text with word wrapping + $textColor = imagecolorallocate($canvas, $fontColor[0], $fontColor[1], $fontColor[2]); + $wrappedTitle = self::wrapText($title, $fontFile, $fontSize, (int) (self::WIDTH * 0.85)); + $textX = (int) (self::WIDTH * 0.075); + $textY = (int) (self::HEIGHT * 0.72); + + imagettftext($canvas, $fontSize, 0, $textX, $textY, $textColor, $fontFile, $wrappedTitle); + + // Save + imagejpeg($canvas, $outputPath, $quality); + imagedestroy($canvas); + + return $outputRel; + } + + /** + * Wrap text to fit within a maximum pixel width. + * + * @param string $text Text to wrap + * @param string $fontFile Path to TTF font + * @param int $fontSize Font size in points + * @param int $maxWidth Maximum width in pixels + * + * @return string Wrapped text with newlines + */ + private static function wrapText(string $text, string $fontFile, int $fontSize, int $maxWidth): string + { + $words = explode(' ', $text); + $lines = []; + $line = ''; + + foreach ($words as $word) { + $testLine = $line ? $line . ' ' . $word : $word; + $bbox = imagettfbbox($fontSize, 0, $fontFile, $testLine); + $lineWidth = abs($bbox[4] - $bbox[0]); + + if ($lineWidth > $maxWidth && $line !== '') { + $lines[] = $line; + $line = $word; + } else { + $line = $testLine; + } + } + + if ($line !== '') { + $lines[] = $line; + } + + // Limit to 3 lines, truncate last line if needed + if (\count($lines) > 3) { + $lines = \array_slice($lines, 0, 3); + $lines[2] = mb_substr($lines[2], 0, -3) . '...'; + } + + return implode("\n", $lines); + } +} From fdf4835051676e328363b9ef449bf0f4a820f450 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sat, 23 May 2026 18:38:18 -0500 Subject: [PATCH 012/132] feat(csv): add CSV import/export for OG tags (closes #12) ImportExportController with export() and import() actions: - Export: CSV with all OG + SEO fields, joined with article titles - Import: Upload CSV, match on content_type + content_id - Upsert logic: creates new or updates existing records - Reports created/updated/skipped counts - Toolbar buttons ready for integration Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- .../com_mokoog/language/en-GB/com_mokoog.ini | 6 + .../com_mokoog/language/en-US/com_mokoog.ini | 6 + .../src/Controller/ImportExportController.php | 188 ++++++++++++++++++ 3 files changed, 200 insertions(+) create mode 100644 src/packages/com_mokoog/src/Controller/ImportExportController.php diff --git a/src/packages/com_mokoog/language/en-GB/com_mokoog.ini b/src/packages/com_mokoog/language/en-GB/com_mokoog.ini index 28aecda..53ece95 100644 --- a/src/packages/com_mokoog/language/en-GB/com_mokoog.ini +++ b/src/packages/com_mokoog/language/en-GB/com_mokoog.ini @@ -48,3 +48,9 @@ COM_MOKOOG_BATCH_FOUND="articles found without OG tags." COM_MOKOOG_BATCH_PROCESSED="processed" COM_MOKOOG_BATCH_COMPLETE="Batch generation complete!" COM_MOKOOG_BATCH_ERROR="Error:" + +COM_MOKOOG_TOOLBAR_EXPORT="Export CSV" +COM_MOKOOG_TOOLBAR_IMPORT="Import CSV" +COM_MOKOOG_IMPORT_NO_FILE="No CSV file was uploaded." +COM_MOKOOG_IMPORT_READ_ERROR="Could not read the uploaded CSV file." +COM_MOKOOG_IMPORT_RESULT="Import complete: %d created, %d updated, %d skipped." diff --git a/src/packages/com_mokoog/language/en-US/com_mokoog.ini b/src/packages/com_mokoog/language/en-US/com_mokoog.ini index 28aecda..53ece95 100644 --- a/src/packages/com_mokoog/language/en-US/com_mokoog.ini +++ b/src/packages/com_mokoog/language/en-US/com_mokoog.ini @@ -48,3 +48,9 @@ COM_MOKOOG_BATCH_FOUND="articles found without OG tags." COM_MOKOOG_BATCH_PROCESSED="processed" COM_MOKOOG_BATCH_COMPLETE="Batch generation complete!" COM_MOKOOG_BATCH_ERROR="Error:" + +COM_MOKOOG_TOOLBAR_EXPORT="Export CSV" +COM_MOKOOG_TOOLBAR_IMPORT="Import CSV" +COM_MOKOOG_IMPORT_NO_FILE="No CSV file was uploaded." +COM_MOKOOG_IMPORT_READ_ERROR="Could not read the uploaded CSV file." +COM_MOKOOG_IMPORT_RESULT="Import complete: %d created, %d updated, %d skipped." diff --git a/src/packages/com_mokoog/src/Controller/ImportExportController.php b/src/packages/com_mokoog/src/Controller/ImportExportController.php new file mode 100644 index 0000000..aec4584 --- /dev/null +++ b/src/packages/com_mokoog/src/Controller/ImportExportController.php @@ -0,0 +1,188 @@ +<?php + +/** + * @package MokoOpenGraph + * @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\Factory; +use Joomla\CMS\Language\Text; +use Joomla\CMS\MVC\Controller\BaseController; +use Joomla\CMS\Session\Session; + +class ImportExportController extends BaseController +{ + /** + * Export all OG tags as CSV. + * + * @return void + */ + public function export(): void + { + Session::checkToken('get') || jexit(Text::_('JINVALID_TOKEN')); + + $app = Factory::getApplication(); + $db = Factory::getDbo(); + + // Join with #__content to get article titles for reference + $query = $db->getQuery(true) + ->select([ + $db->quoteName('t.content_type'), + $db->quoteName('t.content_id'), + 'COALESCE(' . $db->quoteName('c.title') . ', ' . $db->quote('') . ') AS ' . $db->quoteName('article_title'), + $db->quoteName('t.og_title'), + $db->quoteName('t.og_description'), + $db->quoteName('t.og_image'), + $db->quoteName('t.og_type'), + $db->quoteName('t.seo_title'), + $db->quoteName('t.meta_description'), + $db->quoteName('t.robots'), + $db->quoteName('t.canonical_url'), + ]) + ->from($db->quoteName('#__mokoog_tags', 't')) + ->leftJoin( + $db->quoteName('#__content', 'c') + . ' ON ' . $db->quoteName('t.content_type') . ' = ' . $db->quote('com_content') + . ' AND ' . $db->quoteName('t.content_id') . ' = ' . $db->quoteName('c.id') + ) + ->order($db->quoteName('t.content_type') . ', ' . $db->quoteName('t.content_id')); + + $db->setQuery($query); + $rows = $db->loadAssocList(); + + // Send CSV headers + $app->setHeader('Content-Type', 'text/csv; charset=utf-8'); + $app->setHeader('Content-Disposition', 'attachment; filename="mokoog_tags_export.csv"'); + $app->sendHeaders(); + + $output = fopen('php://output', 'w'); + + // Header row + fputcsv($output, [ + 'content_type', 'content_id', 'article_title', + 'og_title', 'og_description', 'og_image', 'og_type', + 'seo_title', 'meta_description', 'robots', 'canonical_url', + ]); + + foreach ($rows as $row) { + fputcsv($output, $row); + } + + fclose($output); + $app->close(); + } + + /** + * Import OG tags from uploaded CSV. + * + * @return void + */ + public function import(): void + { + Session::checkToken() || jexit(Text::_('JINVALID_TOKEN')); + + $app = Factory::getApplication(); + $input = $app->getInput(); + $files = $input->files->get('jform', [], 'array'); + + if (empty($files['csv_file']['tmp_name'])) { + $app->enqueueMessage(Text::_('COM_MOKOOG_IMPORT_NO_FILE'), 'error'); + $app->redirect('index.php?option=com_mokoog&view=tags'); + + return; + } + + $tmpFile = $files['csv_file']['tmp_name']; + $handle = fopen($tmpFile, 'r'); + + if (!$handle) { + $app->enqueueMessage(Text::_('COM_MOKOOG_IMPORT_READ_ERROR'), 'error'); + $app->redirect('index.php?option=com_mokoog&view=tags'); + + return; + } + + $db = Factory::getDbo(); + $header = fgetcsv($handle); + $created = 0; + $updated = 0; + $skipped = 0; + $now = Factory::getDate()->toSql(); + + while (($row = fgetcsv($handle)) !== false) { + if (\count($row) < 7) { + $skipped++; + + continue; + } + + $contentType = trim($row[0]); + $contentId = (int) $row[1]; + // $row[2] = article_title (informational, skip) + $ogTitle = trim($row[3] ?? ''); + $ogDescription = trim($row[4] ?? ''); + $ogImage = trim($row[5] ?? ''); + $ogType = trim($row[6] ?? 'article'); + $seoTitle = trim($row[7] ?? ''); + $metaDesc = trim($row[8] ?? ''); + $robots = trim($row[9] ?? ''); + $canonicalUrl = trim($row[10] ?? ''); + + if (empty($contentType) || $contentId <= 0) { + $skipped++; + + continue; + } + + // Check for existing record + $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); + + $db->setQuery($query); + $existingId = $db->loadResult(); + + $record = (object) [ + 'content_type' => $contentType, + 'content_id' => $contentId, + 'og_title' => $ogTitle, + 'og_description' => $ogDescription, + 'og_image' => $ogImage, + 'og_type' => $ogType, + 'seo_title' => $seoTitle, + 'meta_description' => $metaDesc, + 'robots' => $robots, + 'canonical_url' => $canonicalUrl, + 'published' => 1, + 'modified' => $now, + ]; + + if ($existingId) { + $record->id = $existingId; + $db->updateObject('#__mokoog_tags', $record, 'id'); + $updated++; + } else { + $record->created = $now; + $db->insertObject('#__mokoog_tags', $record); + $created++; + } + } + + fclose($handle); + + $app->enqueueMessage( + Text::sprintf('COM_MOKOOG_IMPORT_RESULT', $created, $updated, $skipped), + 'success' + ); + $app->redirect('index.php?option=com_mokoog&view=tags'); + } +} From ce0bbe821ab47ef6e5a96c58b730a614c20b28f1 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sat, 23 May 2026 22:43:18 -0500 Subject: [PATCH 013/132] feat(api): add Joomla Web Services API for OG tags (closes #27) New sub-extension: plg_webservices_mokoog - Registers REST API routes via onBeforeApiRoute - CRUD endpoints at /api/index.php/v1/mokoog/tags - Lookup endpoint at /api/index.php/v1/mokoog/lookup/:type/:id - SubscriberInterface pattern with DI container Component API layer (com_mokoog/api/): - TagsController extending ApiController for CRUD operations - JsonapiView with whitelisted fields for JSON:API output - TagModel (AdminModel) for single-item operations Package updates: - pkg_mokoog.xml includes plg_webservices_mokoog - Install script auto-enables webservices plugin - Component manifest declares api/ directory Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- .../api/src/Controller/TagsController.php | 68 ++++++++++++++++ .../api/src/View/Tags/JsonapiView.php | 66 +++++++++++++++ src/packages/com_mokoog/mokoog.xml | 6 ++ .../com_mokoog/src/Model/TagModel.php | 68 ++++++++++++++++ .../language/en-GB/plg_webservices_mokoog.ini | 5 ++ .../en-GB/plg_webservices_mokoog.sys.ini | 6 ++ .../language/en-US/plg_webservices_mokoog.ini | 5 ++ .../en-US/plg_webservices_mokoog.sys.ini | 6 ++ .../plg_webservices_mokoog/mokoog.php | 19 +++++ .../plg_webservices_mokoog/mokoog.xml | 33 ++++++++ .../services/provider.php | 44 ++++++++++ .../src/Extension/MokoOGWebServices.php | 81 +++++++++++++++++++ src/pkg_mokoog.xml | 1 + src/script.php | 11 +++ 14 files changed, 419 insertions(+) create mode 100644 src/packages/com_mokoog/api/src/Controller/TagsController.php create mode 100644 src/packages/com_mokoog/api/src/View/Tags/JsonapiView.php create mode 100644 src/packages/com_mokoog/src/Model/TagModel.php create mode 100644 src/packages/plg_webservices_mokoog/language/en-GB/plg_webservices_mokoog.ini create mode 100644 src/packages/plg_webservices_mokoog/language/en-GB/plg_webservices_mokoog.sys.ini create mode 100644 src/packages/plg_webservices_mokoog/language/en-US/plg_webservices_mokoog.ini create mode 100644 src/packages/plg_webservices_mokoog/language/en-US/plg_webservices_mokoog.sys.ini create mode 100644 src/packages/plg_webservices_mokoog/mokoog.php create mode 100644 src/packages/plg_webservices_mokoog/mokoog.xml create mode 100644 src/packages/plg_webservices_mokoog/services/provider.php create mode 100644 src/packages/plg_webservices_mokoog/src/Extension/MokoOGWebServices.php diff --git a/src/packages/com_mokoog/api/src/Controller/TagsController.php b/src/packages/com_mokoog/api/src/Controller/TagsController.php new file mode 100644 index 0000000..e0d895f --- /dev/null +++ b/src/packages/com_mokoog/api/src/Controller/TagsController.php @@ -0,0 +1,68 @@ +<?php + +/** + * @package MokoOpenGraph + * @subpackage com_mokoog.api + * @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\Api\Controller; + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\MVC\Controller\ApiController; + +class TagsController extends ApiController +{ + /** + * The content type for JSON:API output. + * + * @var string + */ + protected $contentType = 'mokoogtags'; + + /** + * The default view for the API. + * + * @var string + */ + protected $default_view = 'tags'; + + /** + * Lookup an OG tag by content_type and content_id. + * + * GET /api/index.php/v1/mokoog/lookup/:content_type/:content_id + * + * @return static + */ + public function lookup(): static + { + $contentType = $this->input->getString('content_type', ''); + $contentId = $this->input->getInt('content_id', 0); + + if (empty($contentType) || $contentId <= 0) { + throw new \RuntimeException('content_type and content_id are required', 400); + } + + $db = Factory::getDbo(); + $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); + + $db->setQuery($query); + $id = $db->loadResult(); + + if (!$id) { + throw new \RuntimeException('OG tag not found for ' . $contentType . ':' . $contentId, 404); + } + + $this->input->set('id', $id); + + return $this->displayItem(); + } +} diff --git a/src/packages/com_mokoog/api/src/View/Tags/JsonapiView.php b/src/packages/com_mokoog/api/src/View/Tags/JsonapiView.php new file mode 100644 index 0000000..c103df2 --- /dev/null +++ b/src/packages/com_mokoog/api/src/View/Tags/JsonapiView.php @@ -0,0 +1,66 @@ +<?php + +/** + * @package MokoOpenGraph + * @subpackage com_mokoog.api + * @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\Api\View\Tags; + +defined('_JEXEC') or die; + +use Joomla\CMS\MVC\View\JsonApiView as BaseApiView; + +class JsonapiView extends BaseApiView +{ + /** + * The fields to render in the API response. + * + * Whitelist of fields from #__mokoog_tags that are safe to expose. + * + * @var array + */ + protected $fieldsToRenderItem = [ + 'id', + 'content_type', + 'content_id', + 'og_title', + 'og_description', + 'og_image', + 'og_type', + 'seo_title', + 'meta_description', + 'robots', + 'canonical_url', + 'language', + 'published', + 'created', + 'modified', + ]; + + /** + * The fields to render in list responses. + * + * @var array + */ + protected $fieldsToRenderList = [ + 'id', + 'content_type', + 'content_id', + 'og_title', + 'og_description', + 'og_image', + 'og_type', + 'seo_title', + 'meta_description', + 'robots', + 'canonical_url', + 'language', + 'published', + 'created', + 'modified', + ]; +} diff --git a/src/packages/com_mokoog/mokoog.xml b/src/packages/com_mokoog/mokoog.xml index aa2ad47..f5baeb3 100644 --- a/src/packages/com_mokoog/mokoog.xml +++ b/src/packages/com_mokoog/mokoog.xml @@ -69,4 +69,10 @@ <menu link="option=com_mokoog&view=tags">COM_MOKOOG_SUBMENU_TAGS</menu> </submenu> </administration> + + <api> + <files folder="api"> + <folder>src</folder> + </files> + </api> </extension> diff --git a/src/packages/com_mokoog/src/Model/TagModel.php b/src/packages/com_mokoog/src/Model/TagModel.php new file mode 100644 index 0000000..62d14b9 --- /dev/null +++ b/src/packages/com_mokoog/src/Model/TagModel.php @@ -0,0 +1,68 @@ +<?php + +/** + * @package MokoOpenGraph + * @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\Model; + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\MVC\Model\AdminModel; + +class TagModel extends AdminModel +{ + /** + * Get the form for the item. + * + * @param array $data Form data + * @param bool $loadData Load data from state + * + * @return \Joomla\CMS\Form\Form|false + */ + public function getForm($data = [], $loadData = true) + { + $form = $this->loadForm( + 'com_mokoog.tag', + 'tag', + ['control' => 'jform', 'load_data' => $loadData] + ); + + return $form ?: false; + } + + /** + * Load the form data. + * + * @return object + */ + protected function loadFormData(): object + { + $data = Factory::getApplication()->getUserState('com_mokoog.edit.tag.data', []); + + if (empty($data)) { + $data = $this->getItem(); + } + + return $data; + } + + /** + * Get the table class name. + * + * @param string $name Table name + * @param string $prefix Table prefix + * @param array $options Table options + * + * @return \Joomla\CMS\Table\Table + */ + public function getTable($name = 'Tag', $prefix = 'Administrator', $options = []) + { + return parent::getTable($name, $prefix, $options); + } +} diff --git a/src/packages/plg_webservices_mokoog/language/en-GB/plg_webservices_mokoog.ini b/src/packages/plg_webservices_mokoog/language/en-GB/plg_webservices_mokoog.ini new file mode 100644 index 0000000..e397923 --- /dev/null +++ b/src/packages/plg_webservices_mokoog/language/en-GB/plg_webservices_mokoog.ini @@ -0,0 +1,5 @@ +; MokoOpenGraph - Web Services Plugin Language File +; Copyright (C) 2026 Moko Consulting. All rights reserved. +; License: GPL-3.0-or-later + +PLG_WEBSERVICES_MOKOOG="Web Services - MokoOpenGraph" diff --git a/src/packages/plg_webservices_mokoog/language/en-GB/plg_webservices_mokoog.sys.ini b/src/packages/plg_webservices_mokoog/language/en-GB/plg_webservices_mokoog.sys.ini new file mode 100644 index 0000000..903c909 --- /dev/null +++ b/src/packages/plg_webservices_mokoog/language/en-GB/plg_webservices_mokoog.sys.ini @@ -0,0 +1,6 @@ +; MokoOpenGraph - Web Services Plugin System Language File +; Copyright (C) 2026 Moko Consulting. All rights reserved. +; License: GPL-3.0-or-later + +PLG_WEBSERVICES_MOKOOG="Web Services - MokoOpenGraph" +PLG_WEBSERVICES_MOKOOG_DESCRIPTION="Exposes MokoOpenGraph OG tag data via Joomla's REST API at /api/index.php/v1/mokoog/tags." diff --git a/src/packages/plg_webservices_mokoog/language/en-US/plg_webservices_mokoog.ini b/src/packages/plg_webservices_mokoog/language/en-US/plg_webservices_mokoog.ini new file mode 100644 index 0000000..e397923 --- /dev/null +++ b/src/packages/plg_webservices_mokoog/language/en-US/plg_webservices_mokoog.ini @@ -0,0 +1,5 @@ +; MokoOpenGraph - Web Services Plugin Language File +; Copyright (C) 2026 Moko Consulting. All rights reserved. +; License: GPL-3.0-or-later + +PLG_WEBSERVICES_MOKOOG="Web Services - MokoOpenGraph" diff --git a/src/packages/plg_webservices_mokoog/language/en-US/plg_webservices_mokoog.sys.ini b/src/packages/plg_webservices_mokoog/language/en-US/plg_webservices_mokoog.sys.ini new file mode 100644 index 0000000..903c909 --- /dev/null +++ b/src/packages/plg_webservices_mokoog/language/en-US/plg_webservices_mokoog.sys.ini @@ -0,0 +1,6 @@ +; MokoOpenGraph - Web Services Plugin System Language File +; Copyright (C) 2026 Moko Consulting. All rights reserved. +; License: GPL-3.0-or-later + +PLG_WEBSERVICES_MOKOOG="Web Services - MokoOpenGraph" +PLG_WEBSERVICES_MOKOOG_DESCRIPTION="Exposes MokoOpenGraph OG tag data via Joomla's REST API at /api/index.php/v1/mokoog/tags." diff --git a/src/packages/plg_webservices_mokoog/mokoog.php b/src/packages/plg_webservices_mokoog/mokoog.php new file mode 100644 index 0000000..899de00 --- /dev/null +++ b/src/packages/plg_webservices_mokoog/mokoog.php @@ -0,0 +1,19 @@ +<?php + +/** + * @package MokoOpenGraph + * @subpackage plg_webservices_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 + * + * Legacy entry point — not executed under DI container. + */ + +defined('_JEXEC') or die; + +use Joomla\CMS\Plugin\CMSPlugin; + +class PlgWebservicesMokoog extends CMSPlugin +{ +} diff --git a/src/packages/plg_webservices_mokoog/mokoog.xml b/src/packages/plg_webservices_mokoog/mokoog.xml new file mode 100644 index 0000000..5f862ab --- /dev/null +++ b/src/packages/plg_webservices_mokoog/mokoog.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + * @package MokoOpenGraph + * @subpackage plg_webservices_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 + --> +<extension type="plugin" group="webservices" method="upgrade"> + <name>Web Services - MokoOpenGraph</name> + <version>01.00.00</version> + <creationDate>2026-05-23</creationDate> + <author>Moko Consulting</author> + <authorEmail>hello@mokoconsulting.tech</authorEmail> + <authorUrl>https://mokoconsulting.tech</authorUrl> + <copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright> + <license>GPL-3.0-or-later</license> + <description>PLG_WEBSERVICES_MOKOOG_DESCRIPTION</description> + + <namespace path="src">Joomla\Plugin\WebServices\MokoOG</namespace> + + <files> + <filename plugin="mokoog">mokoog.php</filename> + <folder>src</folder> + <folder>services</folder> + <folder>language</folder> + </files> + + <languages> + <language tag="en-GB">language/en-GB/plg_webservices_mokoog.ini</language> + <language tag="en-GB">language/en-GB/plg_webservices_mokoog.sys.ini</language> + </languages> +</extension> diff --git a/src/packages/plg_webservices_mokoog/services/provider.php b/src/packages/plg_webservices_mokoog/services/provider.php new file mode 100644 index 0000000..2aa760d --- /dev/null +++ b/src/packages/plg_webservices_mokoog/services/provider.php @@ -0,0 +1,44 @@ +<?php + +/** + * @package MokoOpenGraph + * @subpackage plg_webservices_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 + */ + +defined('_JEXEC') or die; + +use Joomla\CMS\Extension\PluginInterface; +use Joomla\CMS\Factory; +use Joomla\CMS\Plugin\PluginHelper; +use Joomla\DI\Container; +use Joomla\DI\ServiceProviderInterface; +use Joomla\Event\DispatcherInterface; +use Joomla\Plugin\WebServices\MokoOG\Extension\MokoOGWebServices; + +return new class () implements ServiceProviderInterface { + /** + * Register the service provider. + * + * @param Container $container The DI container + * + * @return void + */ + public function register(Container $container): void + { + $container->set( + PluginInterface::class, + function (Container $container) { + $plugin = new MokoOGWebServices( + $container->get(DispatcherInterface::class), + (array) PluginHelper::getPlugin('webservices', 'mokoog') + ); + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/src/packages/plg_webservices_mokoog/src/Extension/MokoOGWebServices.php b/src/packages/plg_webservices_mokoog/src/Extension/MokoOGWebServices.php new file mode 100644 index 0000000..887e3b8 --- /dev/null +++ b/src/packages/plg_webservices_mokoog/src/Extension/MokoOGWebServices.php @@ -0,0 +1,81 @@ +<?php + +/** + * @package MokoOpenGraph + * @subpackage plg_webservices_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\Plugin\WebServices\MokoOG\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\CMS\Router\ApiRouter; +use Joomla\Event\Event; +use Joomla\Event\SubscriberInterface; +use Joomla\Router\Route; + +final class MokoOGWebServices extends CMSPlugin implements SubscriberInterface +{ + /** + * @var bool + */ + protected $autoloadLanguage = true; + + /** + * Returns the events this plugin subscribes to. + * + * @return array<string, string> + */ + public static function getSubscribedEvents(): array + { + return [ + 'onBeforeApiRoute' => 'onBeforeApiRoute', + ]; + } + + /** + * Register API routes for MokoOpenGraph. + * + * Endpoints: + * GET /api/index.php/v1/mokoog/tags - List all OG tags + * GET /api/index.php/v1/mokoog/tags/:id - Get single OG tag + * POST /api/index.php/v1/mokoog/tags - Create OG tag + * PATCH /api/index.php/v1/mokoog/tags/:id - Update OG tag + * DELETE /api/index.php/v1/mokoog/tags/:id - Delete OG tag + * + * @param Event $event The event object + * + * @return void + */ + public function onBeforeApiRoute(Event $event): void + { + [$router] = array_values($event->getArguments()); + + $defaults = [ + 'component' => 'com_mokoog', + 'public' => false, + ]; + + // CRUD routes for OG tags + $router->createCRUDRoutes( + 'v1/mokoog/tags', + 'tags', + $defaults + ); + + // GET by content type + content ID (lookup endpoint) + $router->addRoute( + new Route( + ['GET'], + 'v1/mokoog/lookup/:content_type/:content_id', + 'tags.lookup', + ['content_type' => '[a-z_.]+', 'content_id' => '(\d+)'], + $defaults + ) + ); + } +} diff --git a/src/pkg_mokoog.xml b/src/pkg_mokoog.xml index 355f556..783c779 100644 --- a/src/pkg_mokoog.xml +++ b/src/pkg_mokoog.xml @@ -23,6 +23,7 @@ <file type="component" id="com_mokoog">com_mokoog.zip</file> <file type="plugin" id="mokoog" group="system">plg_system_mokoog.zip</file> <file type="plugin" id="mokoog" group="content">plg_content_mokoog.zip</file> + <file type="plugin" id="mokoog" group="webservices">plg_webservices_mokoog.zip</file> </files> <languages> diff --git a/src/script.php b/src/script.php index c5a505f..0d173ba 100644 --- a/src/script.php +++ b/src/script.php @@ -73,6 +73,17 @@ class Pkg_MokoOGInstallerScript $db->setQuery($query); $db->execute(); + + // Enable the webservices plugin automatically + $query = $db->getQuery(true) + ->update($db->quoteName('#__extensions')) + ->set($db->quoteName('enabled') . ' = 1') + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('webservices')) + ->where($db->quoteName('element') . ' = ' . $db->quote('mokoog')); + + $db->setQuery($query); + $db->execute(); } } } From 44d9ccfda6c7a132ccd84f1e9e3b87c89a47005f Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" <gitea-actions[bot]@mokoconsulting.tech> Date: Sun, 24 May 2026 03:45:20 +0000 Subject: [PATCH 014/132] chore: update updates.xml (development: 01.00.00-dev) [skip ci] --- updates.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/updates.xml b/updates.xml index 03eebe2..fc93688 100644 --- a/updates.xml +++ b/updates.xml @@ -4,16 +4,16 @@ <update> <name>MokoOpenGraph</name> - <description>MokoOpenGraph stable build.</description> + <description>MokoOpenGraph development build.</description> <element>pkg_mokoog</element> <type>package</type> <version>01.00.00</version> - <creationDate>2026-05-23</creationDate> - <infourl title='MokoOpenGraph'>https://git.mokoconsulting.tech/MokoConsulting/MokoOpenGraph/releases/tag/v01</infourl> + <creationDate>2026-05-24</creationDate> + <infourl title='MokoOpenGraph'>https://git.mokoconsulting.tech/MokoConsulting/MokoOpenGraph/releases/tag/development</infourl> <downloads> - <downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoOpenGraph/releases/download/v01/pkg_mokoog-01.00.00.zip</downloadurl> + <downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoOpenGraph/releases/download/development/pkg_mokoog-01.00.00-dev.zip</downloadurl> </downloads> - <sha256>e2262d9e515af284d6837627d63fb7ef7376aea5c50bebd1f4ea4e92a4ae4a6d</sha256> + <sha256>6a58f2ccbccf6f383394e31af6094993b9f5a6a4e799ca1c3ab9bb9d8195f168</sha256> <tags><tag>development</tag></tags> <maintainer>Moko Consulting</maintainer> <maintainerurl>https://mokoconsulting.tech</maintainerurl> From 97bc2526c0a6d1796a7bab4f2e5515e28d9f0f4c Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sat, 23 May 2026 23:22:02 -0500 Subject: [PATCH 015/132] fix: production readiness improvements for admin panel and installer Add search/filter tools to tags list view, fix API content type naming, build proper frontend URLs for social debugger links, and auto-enable the content plugin on package install. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- .../api/src/Controller/TagsController.php | 2 +- .../com_mokoog/src/View/Tags/HtmlView.php | 23 ++++++++++++++++--- src/packages/com_mokoog/tmpl/tags/default.php | 13 +++++++++-- src/script.php | 11 +++++++++ 4 files changed, 43 insertions(+), 6 deletions(-) diff --git a/src/packages/com_mokoog/api/src/Controller/TagsController.php b/src/packages/com_mokoog/api/src/Controller/TagsController.php index e0d895f..28457f4 100644 --- a/src/packages/com_mokoog/api/src/Controller/TagsController.php +++ b/src/packages/com_mokoog/api/src/Controller/TagsController.php @@ -22,7 +22,7 @@ class TagsController extends ApiController * * @var string */ - protected $contentType = 'mokoogtags'; + protected $contentType = 'tags'; /** * The default view for the API. diff --git a/src/packages/com_mokoog/src/View/Tags/HtmlView.php b/src/packages/com_mokoog/src/View/Tags/HtmlView.php index 0a189b4..cf93aff 100644 --- a/src/packages/com_mokoog/src/View/Tags/HtmlView.php +++ b/src/packages/com_mokoog/src/View/Tags/HtmlView.php @@ -39,6 +39,20 @@ class HtmlView extends BaseHtmlView */ protected $state; + /** + * The filter form. + * + * @var \Joomla\CMS\Form\Form|null + */ + public $filterForm; + + /** + * The active filters. + * + * @var array + */ + public $activeFilters = []; + /** * Display the view. * @@ -48,9 +62,11 @@ class HtmlView extends BaseHtmlView */ public function display($tpl = null): void { - $this->items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); $this->addToolbar(); @@ -66,6 +82,7 @@ class HtmlView extends BaseHtmlView { ToolbarHelper::title(Text::_('COM_MOKOOG_TAGS_TITLE'), 'bookmark'); ToolbarHelper::custom('batch.generate', 'refresh', '', 'COM_MOKOOG_TOOLBAR_BATCH_GENERATE', false); + ToolbarHelper::custom('importexport.export', 'download', '', 'COM_MOKOOG_TOOLBAR_EXPORT', false); ToolbarHelper::deleteList('JGLOBAL_CONFIRM_DELETE', 'tags.delete'); ToolbarHelper::preferences('com_mokoog'); } diff --git a/src/packages/com_mokoog/tmpl/tags/default.php b/src/packages/com_mokoog/tmpl/tags/default.php index 54c5940..094de26 100644 --- a/src/packages/com_mokoog/tmpl/tags/default.php +++ b/src/packages/com_mokoog/tmpl/tags/default.php @@ -25,6 +25,7 @@ $token = Session::getFormToken(); <div class="row"> <div class="col-md-12"> <div id="j-main-container" class="j-main-container"> + <?php echo LayoutHelper::render('joomla.searchtools.default', ['view' => $this]); ?> <?php if (empty($this->items)) : ?> <div class="alert alert-info"> @@ -121,8 +122,16 @@ $token = Session::getFormToken(); </td> <td class="mokoog-debug-links"> <?php - // Build a placeholder URL for debugger links - $debugUrl = Uri::root(); + // Build frontend URL for this content item + if ($item->content_type === 'com_content') { + $debugUrl = Uri::root() . 'index.php?option=com_content&view=article&id=' . (int) $item->content_id; + } elseif ($item->content_type === 'menu') { + $debugUrl = Uri::root() . 'index.php?Itemid=' . (int) $item->content_id; + } elseif ($item->content_type === 'com_content.category') { + $debugUrl = Uri::root() . 'index.php?option=com_content&view=category&id=' . (int) $item->content_id; + } else { + $debugUrl = Uri::root(); + } ?> <a href="https://developers.facebook.com/tools/debug/?q=<?php echo urlencode($debugUrl); ?>" target="_blank" rel="noopener" title="Facebook Debugger" class="btn btn-sm btn-outline-primary">FB</a> <a href="https://www.linkedin.com/post-inspector/inspect/<?php echo urlencode($debugUrl); ?>" target="_blank" rel="noopener" title="LinkedIn Inspector" class="btn btn-sm btn-outline-info">LI</a> diff --git a/src/script.php b/src/script.php index 0d173ba..3f76f33 100644 --- a/src/script.php +++ b/src/script.php @@ -74,6 +74,17 @@ class Pkg_MokoOGInstallerScript $db->setQuery($query); $db->execute(); + // Enable the content plugin automatically + $query = $db->getQuery(true) + ->update($db->quoteName('#__extensions')) + ->set($db->quoteName('enabled') . ' = 1') + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('content')) + ->where($db->quoteName('element') . ' = ' . $db->quote('mokoog')); + + $db->setQuery($query); + $db->execute(); + // Enable the webservices plugin automatically $query = $db->getQuery(true) ->update($db->quoteName('#__extensions')) From 52a4bf7e5dd37b88f28bc0ba3f6c34e155521cf1 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" <gitea-actions[bot]@mokoconsulting.tech> Date: Sun, 24 May 2026 04:25:14 +0000 Subject: [PATCH 016/132] chore: update updates.xml (development: 01.00.00-dev) [skip ci] --- updates.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/updates.xml b/updates.xml index fc93688..3c7fe4e 100644 --- a/updates.xml +++ b/updates.xml @@ -13,7 +13,7 @@ <downloads> <downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoOpenGraph/releases/download/development/pkg_mokoog-01.00.00-dev.zip</downloadurl> </downloads> - <sha256>6a58f2ccbccf6f383394e31af6094993b9f5a6a4e799ca1c3ab9bb9d8195f168</sha256> + <sha256>3a910f6c075d2cddbda905c175e666b9cbf1ae79e465132f18293ac3be15266e</sha256> <tags><tag>development</tag></tags> <maintainer>Moko Consulting</maintainer> <maintainerurl>https://mokoconsulting.tech</maintainerurl> From 15ec388b524168e1e6b11d03bf118835b8c00725 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sat, 23 May 2026 17:50:28 -0500 Subject: [PATCH 017/132] feat(seo): add SEO meta tag management (closes #8) - Add seo_title, meta_description, robots, canonical_url columns - SQL migration for upgrades (01.01.00.sql) + updated install schema - New "SEO Meta Tags" fieldset in article/menu editor with: - SEO title (70 char max, overrides <title>) - Meta description (200 char max) - Robots directive (noindex, nofollow, nosnippet, etc.) - Canonical URL override - System plugin applies SEO tags in onBeforeCompileHead before OG tags - SEO audit column in admin tag list (missing desc, title too long, noindex) - Content plugin saves/loads all SEO fields alongside OG data Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- src/packages/com_mokoog/forms/tag.xml | 33 ++++++++++++ .../com_mokoog/language/en-GB/com_mokoog.ini | 25 +++++++++ .../com_mokoog/language/en-US/com_mokoog.ini | 25 +++++++++ src/packages/com_mokoog/sql/install.mysql.sql | 4 ++ .../com_mokoog/sql/updates/mysql/01.01.00.sql | 9 ++++ src/packages/com_mokoog/tmpl/tags/default.php | 27 ++++++++++ .../plg_content_mokoog/forms/mokoog.xml | 43 +++++++++++++++ .../language/en-GB/plg_content_mokoog.ini | 13 +++++ .../language/en-US/plg_content_mokoog.ini | 13 +++++ .../src/Extension/MokoOGContent.php | 32 +++++++---- .../src/Extension/MokoOG.php | 53 +++++++++++++++++-- 11 files changed, 264 insertions(+), 13 deletions(-) create mode 100644 src/packages/com_mokoog/sql/updates/mysql/01.01.00.sql diff --git a/src/packages/com_mokoog/forms/tag.xml b/src/packages/com_mokoog/forms/tag.xml index 363ef5f..3c1ab5c 100644 --- a/src/packages/com_mokoog/forms/tag.xml +++ b/src/packages/com_mokoog/forms/tag.xml @@ -70,4 +70,37 @@ <option value="0">JUNPUBLISHED</option> </field> </fieldset> + <fieldset name="seo" label="SEO Meta Tags"> + <field + name="seo_title" + type="text" + label="PLG_CONTENT_MOKOOG_FIELD_SEO_TITLE" + description="PLG_CONTENT_MOKOOG_FIELD_SEO_TITLE_DESC" + filter="string" + maxlength="70" + /> + <field + name="meta_description" + type="textarea" + label="PLG_CONTENT_MOKOOG_FIELD_META_DESCRIPTION" + description="PLG_CONTENT_MOKOOG_FIELD_META_DESCRIPTION_DESC" + filter="string" + rows="3" + maxlength="200" + /> + <field + name="robots" + type="text" + label="PLG_CONTENT_MOKOOG_FIELD_ROBOTS" + description="PLG_CONTENT_MOKOOG_FIELD_ROBOTS_DESC" + filter="string" + /> + <field + name="canonical_url" + type="url" + label="PLG_CONTENT_MOKOOG_FIELD_CANONICAL_URL" + description="PLG_CONTENT_MOKOOG_FIELD_CANONICAL_URL_DESC" + filter="url" + /> + </fieldset> </form> diff --git a/src/packages/com_mokoog/language/en-GB/com_mokoog.ini b/src/packages/com_mokoog/language/en-GB/com_mokoog.ini index 0e56059..c3f3d80 100644 --- a/src/packages/com_mokoog/language/en-GB/com_mokoog.ini +++ b/src/packages/com_mokoog/language/en-GB/com_mokoog.ini @@ -13,4 +13,29 @@ COM_MOKOOG_HEADING_CONTENT_TYPE="Content Type" COM_MOKOOG_HEADING_CONTENT_ID="Content ID" COM_MOKOOG_HEADING_OG_TITLE="OG Title" COM_MOKOOG_HEADING_IMAGE="Image" +COM_MOKOOG_HEADING_SEO="SEO" COM_MOKOOG_HEADING_MODIFIED="Modified" + +COM_MOKOOG_SEO_OK="OK" +COM_MOKOOG_SEO_MISSING_DESC="No meta description" +COM_MOKOOG_SEO_TITLE_LONG="SEO title too long" +COM_MOKOOG_SEO_NOINDEX="noindex" + +COM_MOKOOG_FIELD_CONTENT_TYPE="Content Type" +COM_MOKOOG_FIELD_CONTENT_ID="Content ID" +COM_MOKOOG_FIELD_OG_TITLE="OG Title" +COM_MOKOOG_FIELD_OG_TITLE_DESC="Custom title for social sharing." +COM_MOKOOG_FIELD_OG_DESCRIPTION="OG Description" +COM_MOKOOG_FIELD_OG_DESCRIPTION_DESC="Custom description for social sharing." +COM_MOKOOG_FIELD_OG_IMAGE="OG Image" +COM_MOKOOG_FIELD_OG_IMAGE_DESC="Custom image for social sharing." +COM_MOKOOG_FIELD_OG_TYPE="OG Type" +COM_MOKOOG_FIELD_OG_TYPE_DESC="The Open Graph content type." + +COM_MOKOOG_FILTER_SEARCH="Search OG titles" +COM_MOKOOG_FILTER_CONTENT_TYPE="Content Type" +COM_MOKOOG_FILTER_SELECT_TYPE="- Select Type -" +COM_MOKOOG_HEADING_OG_TITLE_ASC="OG Title ascending" +COM_MOKOOG_HEADING_OG_TITLE_DESC="OG Title descending" +COM_MOKOOG_HEADING_MODIFIED_ASC="Modified ascending" +COM_MOKOOG_HEADING_MODIFIED_DESC="Modified descending" diff --git a/src/packages/com_mokoog/language/en-US/com_mokoog.ini b/src/packages/com_mokoog/language/en-US/com_mokoog.ini index 0e56059..c3f3d80 100644 --- a/src/packages/com_mokoog/language/en-US/com_mokoog.ini +++ b/src/packages/com_mokoog/language/en-US/com_mokoog.ini @@ -13,4 +13,29 @@ COM_MOKOOG_HEADING_CONTENT_TYPE="Content Type" COM_MOKOOG_HEADING_CONTENT_ID="Content ID" COM_MOKOOG_HEADING_OG_TITLE="OG Title" COM_MOKOOG_HEADING_IMAGE="Image" +COM_MOKOOG_HEADING_SEO="SEO" COM_MOKOOG_HEADING_MODIFIED="Modified" + +COM_MOKOOG_SEO_OK="OK" +COM_MOKOOG_SEO_MISSING_DESC="No meta description" +COM_MOKOOG_SEO_TITLE_LONG="SEO title too long" +COM_MOKOOG_SEO_NOINDEX="noindex" + +COM_MOKOOG_FIELD_CONTENT_TYPE="Content Type" +COM_MOKOOG_FIELD_CONTENT_ID="Content ID" +COM_MOKOOG_FIELD_OG_TITLE="OG Title" +COM_MOKOOG_FIELD_OG_TITLE_DESC="Custom title for social sharing." +COM_MOKOOG_FIELD_OG_DESCRIPTION="OG Description" +COM_MOKOOG_FIELD_OG_DESCRIPTION_DESC="Custom description for social sharing." +COM_MOKOOG_FIELD_OG_IMAGE="OG Image" +COM_MOKOOG_FIELD_OG_IMAGE_DESC="Custom image for social sharing." +COM_MOKOOG_FIELD_OG_TYPE="OG Type" +COM_MOKOOG_FIELD_OG_TYPE_DESC="The Open Graph content type." + +COM_MOKOOG_FILTER_SEARCH="Search OG titles" +COM_MOKOOG_FILTER_CONTENT_TYPE="Content Type" +COM_MOKOOG_FILTER_SELECT_TYPE="- Select Type -" +COM_MOKOOG_HEADING_OG_TITLE_ASC="OG Title ascending" +COM_MOKOOG_HEADING_OG_TITLE_DESC="OG Title descending" +COM_MOKOOG_HEADING_MODIFIED_ASC="Modified ascending" +COM_MOKOOG_HEADING_MODIFIED_DESC="Modified descending" diff --git a/src/packages/com_mokoog/sql/install.mysql.sql b/src/packages/com_mokoog/sql/install.mysql.sql index 884d369..306bc3e 100644 --- a/src/packages/com_mokoog/sql/install.mysql.sql +++ b/src/packages/com_mokoog/sql/install.mysql.sql @@ -12,6 +12,10 @@ CREATE TABLE IF NOT EXISTS `#__mokoog_tags` ( `og_description` TEXT NOT NULL, `og_image` VARCHAR(512) NOT NULL DEFAULT '', `og_type` VARCHAR(50) NOT NULL DEFAULT 'article', + `seo_title` VARCHAR(70) NOT NULL DEFAULT '', + `meta_description` VARCHAR(200) NOT NULL DEFAULT '', + `robots` VARCHAR(100) NOT NULL DEFAULT '', + `canonical_url` VARCHAR(512) NOT NULL DEFAULT '', `published` TINYINT(1) NOT NULL DEFAULT 1, `created` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00', `modified` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00', diff --git a/src/packages/com_mokoog/sql/updates/mysql/01.01.00.sql b/src/packages/com_mokoog/sql/updates/mysql/01.01.00.sql new file mode 100644 index 0000000..4f4c433 --- /dev/null +++ b/src/packages/com_mokoog/sql/updates/mysql/01.01.00.sql @@ -0,0 +1,9 @@ +-- +-- MokoOpenGraph 01.01.00 — Add SEO meta management columns +-- + +ALTER TABLE `#__mokoog_tags` + ADD COLUMN `seo_title` VARCHAR(70) NOT NULL DEFAULT '' AFTER `og_type`, + ADD COLUMN `meta_description` VARCHAR(200) NOT NULL DEFAULT '' AFTER `seo_title`, + ADD COLUMN `robots` VARCHAR(100) NOT NULL DEFAULT '' AFTER `meta_description`, + ADD COLUMN `canonical_url` VARCHAR(512) NOT NULL DEFAULT '' AFTER `robots`; diff --git a/src/packages/com_mokoog/tmpl/tags/default.php b/src/packages/com_mokoog/tmpl/tags/default.php index cfec7f7..779746f 100644 --- a/src/packages/com_mokoog/tmpl/tags/default.php +++ b/src/packages/com_mokoog/tmpl/tags/default.php @@ -50,6 +50,9 @@ use Joomla\CMS\Router\Route; <th scope="col" class="w-10"> <?php echo Text::_('COM_MOKOOG_HEADING_IMAGE'); ?> </th> + <th scope="col" class="w-10"> + <?php echo Text::_('COM_MOKOOG_HEADING_SEO'); ?> + </th> <th scope="col" class="w-10"> <?php echo Text::_('JSTATUS'); ?> </th> @@ -83,6 +86,30 @@ use Joomla\CMS\Router\Route; <span class="icon-minus-circle text-muted" aria-hidden="true"></span> <?php endif; ?> </td> + <td> + <?php + $seoIssues = []; + + if (empty($item->meta_description)) { + $seoIssues[] = Text::_('COM_MOKOOG_SEO_MISSING_DESC'); + } + + if (!empty($item->seo_title) && \strlen($item->seo_title) > 60) { + $seoIssues[] = Text::_('COM_MOKOOG_SEO_TITLE_LONG'); + } + + if (!empty($item->robots) && str_contains($item->robots, 'noindex')) { + $seoIssues[] = Text::_('COM_MOKOOG_SEO_NOINDEX'); + } + + if (empty($seoIssues)) : ?> + <span class="badge bg-success"><?php echo Text::_('COM_MOKOOG_SEO_OK'); ?></span> + <?php else : ?> + <?php foreach ($seoIssues as $issue) : ?> + <span class="badge bg-warning text-dark"><?php echo $issue; ?></span> + <?php endforeach; ?> + <?php endif; ?> + </td> <td> <?php echo $item->published ? Text::_('JPUBLISHED') : Text::_('JUNPUBLISHED'); ?> </td> diff --git a/src/packages/plg_content_mokoog/forms/mokoog.xml b/src/packages/plg_content_mokoog/forms/mokoog.xml index 1e9f026..9e5632e 100644 --- a/src/packages/plg_content_mokoog/forms/mokoog.xml +++ b/src/packages/plg_content_mokoog/forms/mokoog.xml @@ -50,5 +50,48 @@ <option value="video.other">Video</option> </field> </fieldset> + <fieldset name="mokoog_seo" label="PLG_CONTENT_MOKOOG_FIELDSET_SEO_LABEL" + description="PLG_CONTENT_MOKOOG_FIELDSET_SEO_DESC"> + <field + name="seo_title" + type="text" + label="PLG_CONTENT_MOKOOG_FIELD_SEO_TITLE" + description="PLG_CONTENT_MOKOOG_FIELD_SEO_TITLE_DESC" + filter="string" + maxlength="70" + /> + <field + name="meta_description" + type="textarea" + label="PLG_CONTENT_MOKOOG_FIELD_META_DESCRIPTION" + description="PLG_CONTENT_MOKOOG_FIELD_META_DESCRIPTION_DESC" + filter="string" + rows="3" + maxlength="200" + /> + <field + name="robots" + type="list" + label="PLG_CONTENT_MOKOOG_FIELD_ROBOTS" + description="PLG_CONTENT_MOKOOG_FIELD_ROBOTS_DESC" + default="" + multiple="true" + > + <option value="">PLG_CONTENT_MOKOOG_ROBOTS_DEFAULT</option> + <option value="noindex">noindex</option> + <option value="nofollow">nofollow</option> + <option value="nosnippet">nosnippet</option> + <option value="noarchive">noarchive</option> + <option value="noimageindex">noimageindex</option> + </field> + <field + name="canonical_url" + type="url" + label="PLG_CONTENT_MOKOOG_FIELD_CANONICAL_URL" + description="PLG_CONTENT_MOKOOG_FIELD_CANONICAL_URL_DESC" + filter="url" + validate="url" + /> + </fieldset> </fields> </form> diff --git a/src/packages/plg_content_mokoog/language/en-GB/plg_content_mokoog.ini b/src/packages/plg_content_mokoog/language/en-GB/plg_content_mokoog.ini index 6ea916c..167fe7e 100644 --- a/src/packages/plg_content_mokoog/language/en-GB/plg_content_mokoog.ini +++ b/src/packages/plg_content_mokoog/language/en-GB/plg_content_mokoog.ini @@ -13,3 +13,16 @@ PLG_CONTENT_MOKOOG_FIELD_OG_IMAGE="OG Image" PLG_CONTENT_MOKOOG_FIELD_OG_IMAGE_DESC="Custom image for social sharing. Recommended: 1200x630px. Leave blank to use the article image." PLG_CONTENT_MOKOOG_FIELD_OG_TYPE="OG Type" PLG_CONTENT_MOKOOG_FIELD_OG_TYPE_DESC="The Open Graph content type for this page." + +PLG_CONTENT_MOKOOG_FIELDSET_SEO_LABEL="SEO Meta Tags" +PLG_CONTENT_MOKOOG_FIELDSET_SEO_DESC="Control search engine meta tags for this page." + +PLG_CONTENT_MOKOOG_FIELD_SEO_TITLE="SEO Title" +PLG_CONTENT_MOKOOG_FIELD_SEO_TITLE_DESC="Custom <title> tag. 50-60 characters recommended. Leave blank to use the default page title." +PLG_CONTENT_MOKOOG_FIELD_META_DESCRIPTION="Meta Description" +PLG_CONTENT_MOKOOG_FIELD_META_DESCRIPTION_DESC="Custom meta description. 150-160 characters recommended. Leave blank to use the default." +PLG_CONTENT_MOKOOG_FIELD_ROBOTS="Robots Directive" +PLG_CONTENT_MOKOOG_FIELD_ROBOTS_DESC="Search engine indexing directives for this page. Leave blank for default (index, follow)." +PLG_CONTENT_MOKOOG_ROBOTS_DEFAULT="- Use default (index, follow) -" +PLG_CONTENT_MOKOOG_FIELD_CANONICAL_URL="Canonical URL" +PLG_CONTENT_MOKOOG_FIELD_CANONICAL_URL_DESC="Override the canonical URL for this page. Leave blank to use the current URL." diff --git a/src/packages/plg_content_mokoog/language/en-US/plg_content_mokoog.ini b/src/packages/plg_content_mokoog/language/en-US/plg_content_mokoog.ini index 6ea916c..167fe7e 100644 --- a/src/packages/plg_content_mokoog/language/en-US/plg_content_mokoog.ini +++ b/src/packages/plg_content_mokoog/language/en-US/plg_content_mokoog.ini @@ -13,3 +13,16 @@ PLG_CONTENT_MOKOOG_FIELD_OG_IMAGE="OG Image" PLG_CONTENT_MOKOOG_FIELD_OG_IMAGE_DESC="Custom image for social sharing. Recommended: 1200x630px. Leave blank to use the article image." PLG_CONTENT_MOKOOG_FIELD_OG_TYPE="OG Type" PLG_CONTENT_MOKOOG_FIELD_OG_TYPE_DESC="The Open Graph content type for this page." + +PLG_CONTENT_MOKOOG_FIELDSET_SEO_LABEL="SEO Meta Tags" +PLG_CONTENT_MOKOOG_FIELDSET_SEO_DESC="Control search engine meta tags for this page." + +PLG_CONTENT_MOKOOG_FIELD_SEO_TITLE="SEO Title" +PLG_CONTENT_MOKOOG_FIELD_SEO_TITLE_DESC="Custom <title> tag. 50-60 characters recommended. Leave blank to use the default page title." +PLG_CONTENT_MOKOOG_FIELD_META_DESCRIPTION="Meta Description" +PLG_CONTENT_MOKOOG_FIELD_META_DESCRIPTION_DESC="Custom meta description. 150-160 characters recommended. Leave blank to use the default." +PLG_CONTENT_MOKOOG_FIELD_ROBOTS="Robots Directive" +PLG_CONTENT_MOKOOG_FIELD_ROBOTS_DESC="Search engine indexing directives for this page. Leave blank for default (index, follow)." +PLG_CONTENT_MOKOOG_ROBOTS_DEFAULT="- Use default (index, follow) -" +PLG_CONTENT_MOKOOG_FIELD_CANONICAL_URL="Canonical URL" +PLG_CONTENT_MOKOOG_FIELD_CANONICAL_URL_DESC="Override the canonical URL for this page. Leave blank to use the current URL." diff --git a/src/packages/plg_content_mokoog/src/Extension/MokoOGContent.php b/src/packages/plg_content_mokoog/src/Extension/MokoOGContent.php index 07c1161..3d70565 100644 --- a/src/packages/plg_content_mokoog/src/Extension/MokoOGContent.php +++ b/src/packages/plg_content_mokoog/src/Extension/MokoOGContent.php @@ -176,7 +176,10 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface { $db = Factory::getDbo(); $query = $db->getQuery(true) - ->select($db->quoteName(['og_title', 'og_description', 'og_image', 'og_type'])) + ->select($db->quoteName([ + 'og_title', 'og_description', 'og_image', 'og_type', + 'seo_title', 'meta_description', 'robots', 'canonical_url', + ])) ->from($db->quoteName('#__mokoog_tags')) ->where($db->quoteName('content_type') . ' = ' . $db->quote($contentType)) ->where($db->quoteName('content_id') . ' = ' . $contentId); @@ -209,15 +212,26 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface $db->setQuery($query); $existingId = $db->loadResult(); + // Robots may come as array from multi-select, join with comma + $robots = $ogData['robots'] ?? ''; + + if (\is_array($robots)) { + $robots = implode(', ', array_filter($robots)); + } + $record = (object) [ - 'content_type' => $contentType, - 'content_id' => $contentId, - 'og_title' => trim($ogData['og_title'] ?? ''), - 'og_description' => trim($ogData['og_description'] ?? ''), - 'og_image' => trim($ogData['og_image'] ?? ''), - 'og_type' => trim($ogData['og_type'] ?? 'article'), - 'published' => 1, - 'modified' => Factory::getDate()->toSql(), + 'content_type' => $contentType, + 'content_id' => $contentId, + 'og_title' => trim($ogData['og_title'] ?? ''), + 'og_description' => trim($ogData['og_description'] ?? ''), + 'og_image' => trim($ogData['og_image'] ?? ''), + 'og_type' => trim($ogData['og_type'] ?? 'article'), + 'seo_title' => trim($ogData['seo_title'] ?? ''), + 'meta_description' => trim($ogData['meta_description'] ?? ''), + 'robots' => trim($robots), + 'canonical_url' => trim($ogData['canonical_url'] ?? ''), + 'published' => 1, + 'modified' => Factory::getDate()->toSql(), ]; if ($existingId) { diff --git a/src/packages/plg_system_mokoog/src/Extension/MokoOG.php b/src/packages/plg_system_mokoog/src/Extension/MokoOG.php index 818e263..c245c42 100644 --- a/src/packages/plg_system_mokoog/src/Extension/MokoOG.php +++ b/src/packages/plg_system_mokoog/src/Extension/MokoOG.php @@ -67,6 +67,9 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface // Try to load custom OG data from the database $ogData = $this->loadOgData($option, $view, $id); + // --- SEO meta tags (set first, before OG) --- + $this->applySeoTags($doc, $ogData); + // Build tag values — custom overrides auto-generated $title = $ogData->og_title ?: $doc->getTitle(); $description = $ogData->og_description ?: $this->buildDescription($doc); @@ -111,6 +114,44 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface } } + /** + * Apply SEO meta tags (title, description, robots, canonical) to the document. + * + * @param \Joomla\CMS\Document\HtmlDocument $doc The document + * @param object $ogData The loaded OG/SEO data + * + * @return void + */ + private function applySeoTags($doc, object $ogData): void + { + // Custom SEO title overrides the page <title> + if (!empty($ogData->seo_title)) { + $doc->setTitle($ogData->seo_title); + } + + // Custom meta description + if (!empty($ogData->meta_description)) { + $doc->setDescription($ogData->meta_description); + } + + // Robots directive + if (!empty($ogData->robots)) { + $doc->setMetaData('robots', $ogData->robots); + } + + // 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]); + } + } + + $doc->addHeadLink($ogData->canonical_url, 'canonical'); + } + } + /** * Load custom OG data from the database for the current page. * @@ -123,10 +164,14 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface private function loadOgData(string $option, string $view, int $id): object { $empty = (object) [ - 'og_title' => '', - 'og_description' => '', - 'og_image' => '', - 'og_type' => '', + 'og_title' => '', + 'og_description' => '', + 'og_image' => '', + 'og_type' => '', + 'seo_title' => '', + 'meta_description' => '', + 'robots' => '', + 'canonical_url' => '', ]; if (!$id) { From 3ec4b163bb532d9542385572b4c9d0f51731250d Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sat, 23 May 2026 17:57:07 -0500 Subject: [PATCH 018/132] feat(images): auto-resize OG images to 1200x630px (closes #2) - New ImageHelper class with resize(), validate(), cleanup() methods - Center-crop algorithm maintains aspect ratio to target dimensions - GD-based processing, supports JPEG/PNG/GIF/WebP input, outputs JPEG - Generated images cached in images/mokoog/generated/ with hash naming - Skips resize if image already at or below target dimensions - Skips regeneration if cached version is newer than source - validate() checks minimum 200x200px (Facebook/WhatsApp requirement) - cleanup() removes generated images when OG records are deleted - Auto-resize toggle in system plugin advanced settings (default: on) - Integrated into resolveImageUrl() in the system plugin Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- .../language/en-GB/plg_system_mokoog.ini | 2 + .../language/en-US/plg_system_mokoog.ini | 2 + src/packages/plg_system_mokoog/mokoog.xml | 11 + .../src/Extension/MokoOG.php | 8 +- .../src/Helper/ImageHelper.php | 222 ++++++++++++++++++ 5 files changed, 244 insertions(+), 1 deletion(-) create mode 100644 src/packages/plg_system_mokoog/src/Helper/ImageHelper.php diff --git a/src/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.ini b/src/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.ini index 4983711..a0902bd 100644 --- a/src/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.ini +++ b/src/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.ini @@ -23,3 +23,5 @@ PLG_SYSTEM_MOKOOG_FIELD_STRIP_HTML="Strip HTML from Description" PLG_SYSTEM_MOKOOG_FIELD_STRIP_HTML_DESC="Remove HTML tags from the auto-generated description." PLG_SYSTEM_MOKOOG_FIELD_DESC_LENGTH="Description Length" PLG_SYSTEM_MOKOOG_FIELD_DESC_LENGTH_DESC="Maximum character length for the auto-generated og:description." +PLG_SYSTEM_MOKOOG_FIELD_AUTO_RESIZE="Auto-resize Images" +PLG_SYSTEM_MOKOOG_FIELD_AUTO_RESIZE_DESC="Automatically resize images to 1200x630px (Facebook recommended) using center crop. Generated images are saved to images/mokoog/generated/." diff --git a/src/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.ini b/src/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.ini index 4983711..a0902bd 100644 --- a/src/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.ini +++ b/src/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.ini @@ -23,3 +23,5 @@ PLG_SYSTEM_MOKOOG_FIELD_STRIP_HTML="Strip HTML from Description" PLG_SYSTEM_MOKOOG_FIELD_STRIP_HTML_DESC="Remove HTML tags from the auto-generated description." PLG_SYSTEM_MOKOOG_FIELD_DESC_LENGTH="Description Length" PLG_SYSTEM_MOKOOG_FIELD_DESC_LENGTH_DESC="Maximum character length for the auto-generated og:description." +PLG_SYSTEM_MOKOOG_FIELD_AUTO_RESIZE="Auto-resize Images" +PLG_SYSTEM_MOKOOG_FIELD_AUTO_RESIZE_DESC="Automatically resize images to 1200x630px (Facebook recommended) using center crop. Generated images are saved to images/mokoog/generated/." diff --git a/src/packages/plg_system_mokoog/mokoog.xml b/src/packages/plg_system_mokoog/mokoog.xml index 50a2120..2517076 100644 --- a/src/packages/plg_system_mokoog/mokoog.xml +++ b/src/packages/plg_system_mokoog/mokoog.xml @@ -107,6 +107,17 @@ min="50" max="300" /> + <field + name="auto_resize" + type="radio" + label="PLG_SYSTEM_MOKOOG_FIELD_AUTO_RESIZE" + description="PLG_SYSTEM_MOKOOG_FIELD_AUTO_RESIZE_DESC" + default="1" + class="btn-group" + > + <option value="1">JYES</option> + <option value="0">JNO</option> + </field> </fieldset> </fields> </config> diff --git a/src/packages/plg_system_mokoog/src/Extension/MokoOG.php b/src/packages/plg_system_mokoog/src/Extension/MokoOG.php index c245c42..e571cc4 100644 --- a/src/packages/plg_system_mokoog/src/Extension/MokoOG.php +++ b/src/packages/plg_system_mokoog/src/Extension/MokoOG.php @@ -17,6 +17,7 @@ use Joomla\CMS\Plugin\CMSPlugin; use Joomla\CMS\Uri\Uri; use Joomla\Event\Event; use Joomla\Event\SubscriberInterface; +use Joomla\Plugin\System\MokoOG\Helper\ImageHelper; final class MokoOG extends CMSPlugin implements SubscriberInterface { @@ -288,7 +289,7 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface } /** - * Resolve a relative image path to a full URL. + * Resolve a relative image path to a full URL, resizing for OG if needed. * * @param string $image Image path * @@ -300,6 +301,11 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface return $image; } + // Auto-resize to OG recommended dimensions if enabled + if ($this->params->get('auto_resize', 1)) { + $image = ImageHelper::resize($image); + } + return rtrim(Uri::root(), '/') . '/' . ltrim($image, '/'); } } diff --git a/src/packages/plg_system_mokoog/src/Helper/ImageHelper.php b/src/packages/plg_system_mokoog/src/Helper/ImageHelper.php new file mode 100644 index 0000000..8364f18 --- /dev/null +++ b/src/packages/plg_system_mokoog/src/Helper/ImageHelper.php @@ -0,0 +1,222 @@ +<?php + +/** + * @package MokoOpenGraph + * @subpackage plg_system_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\Plugin\System\MokoOG\Helper; + +defined('_JEXEC') or die; + +use Joomla\CMS\Filesystem\File; +use Joomla\CMS\Filesystem\Folder; + +class ImageHelper +{ + /** + * Target width for OG images (Facebook recommended). + */ + private const TARGET_WIDTH = 1200; + + /** + * Target height for OG images (Facebook recommended). + */ + private const TARGET_HEIGHT = 630; + + /** + * JPEG quality for generated images. + */ + private const JPEG_QUALITY = 85; + + /** + * Output directory relative to JPATH_ROOT. + */ + private const OUTPUT_DIR = 'images/mokoog/generated'; + + /** + * Resize an image to OG-optimized dimensions if needed. + * + * Returns the path to the resized image relative to JPATH_ROOT, + * or the original path if no resize was needed or possible. + * + * @param string $imagePath Image path relative to JPATH_ROOT + * @param int $targetWidth Target width (default 1200) + * @param int $targetHeight Target height (default 630) + * @param int $quality JPEG quality 1-100 (default 85) + * + * @return string Path to the output image (relative to JPATH_ROOT) + */ + public static function resize( + string $imagePath, + int $targetWidth = self::TARGET_WIDTH, + int $targetHeight = self::TARGET_HEIGHT, + int $quality = self::JPEG_QUALITY + ): string { + // Resolve absolute path + $absPath = JPATH_ROOT . '/' . ltrim($imagePath, '/'); + + if (!is_file($absPath)) { + return $imagePath; + } + + $imageInfo = @getimagesize($absPath); + + if (!$imageInfo) { + return $imagePath; + } + + [$origWidth, $origHeight, $type] = $imageInfo; + + // Skip if already at or below target size + if ($origWidth <= $targetWidth && $origHeight <= $targetHeight) { + return $imagePath; + } + + // Ensure output directory exists + $outputDir = JPATH_ROOT . '/' . self::OUTPUT_DIR; + + if (!is_dir($outputDir)) { + Folder::create($outputDir); + } + + // Generate output filename based on source hash + dimensions + $hash = md5($imagePath . $targetWidth . $targetHeight); + $outputName = $hash . '.jpg'; + $outputPath = $outputDir . '/' . $outputName; + $outputRel = self::OUTPUT_DIR . '/' . $outputName; + + // Skip if already generated + if (is_file($outputPath) && filemtime($outputPath) >= filemtime($absPath)) { + return $outputRel; + } + + // Load source image + $source = self::loadImage($absPath, $type); + + if (!$source) { + return $imagePath; + } + + // Calculate crop dimensions (center crop to target aspect ratio) + $targetRatio = $targetWidth / $targetHeight; + $sourceRatio = $origWidth / $origHeight; + + if ($sourceRatio > $targetRatio) { + // Source is wider — crop sides + $cropHeight = $origHeight; + $cropWidth = (int) round($origHeight * $targetRatio); + $cropX = (int) round(($origWidth - $cropWidth) / 2); + $cropY = 0; + } else { + // Source is taller — crop top/bottom + $cropWidth = $origWidth; + $cropHeight = (int) round($origWidth / $targetRatio); + $cropX = 0; + $cropY = (int) round(($origHeight - $cropHeight) / 2); + } + + // Create output canvas and resample + $output = imagecreatetruecolor($targetWidth, $targetHeight); + + imagecopyresampled( + $output, + $source, + 0, + 0, + $cropX, + $cropY, + $targetWidth, + $targetHeight, + $cropWidth, + $cropHeight + ); + + // Save as JPEG + imagejpeg($output, $outputPath, $quality); + + imagedestroy($source); + imagedestroy($output); + + return $outputRel; + } + + /** + * Remove a generated image file. + * + * @param string $generatedPath Path relative to JPATH_ROOT + * + * @return void + */ + public static function cleanup(string $generatedPath): void + { + if (empty($generatedPath) || !str_starts_with($generatedPath, self::OUTPUT_DIR)) { + return; + } + + $absPath = JPATH_ROOT . '/' . $generatedPath; + + if (is_file($absPath)) { + File::delete($absPath); + } + } + + /** + * Check if an image meets minimum OG size requirements. + * + * @param string $imagePath Image path relative to JPATH_ROOT + * + * @return array{valid: bool, width: int, height: int, message: string} + */ + public static function validate(string $imagePath): array + { + $absPath = JPATH_ROOT . '/' . ltrim($imagePath, '/'); + + if (!is_file($absPath)) { + return ['valid' => false, 'width' => 0, 'height' => 0, 'message' => 'File not found']; + } + + $imageInfo = @getimagesize($absPath); + + if (!$imageInfo) { + return ['valid' => false, 'width' => 0, 'height' => 0, 'message' => 'Not a valid image']; + } + + [$width, $height] = $imageInfo; + + // Facebook minimum: 200x200, recommended: 1200x630 + // WhatsApp minimum: 300x200 + if ($width < 200 || $height < 200) { + return [ + 'valid' => false, + 'width' => $width, + 'height' => $height, + 'message' => "Image too small ({$width}x{$height}). Minimum: 200x200px.", + ]; + } + + return ['valid' => true, 'width' => $width, 'height' => $height, 'message' => 'OK']; + } + + /** + * Load an image resource from a file. + * + * @param string $path Absolute file path + * @param int $type IMAGETYPE_* constant + * + * @return \GdImage|false + */ + 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), + default => false, + }; + } +} From 9a6cd6fce835139859b020f2fb29d048062567e3 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sat, 23 May 2026 17:53:28 -0500 Subject: [PATCH 019/132] feat(batch): add batch OG tag generation for existing articles (closes #1) - BatchController with count() and process() AJAX endpoints - Chunked processing (50 articles per request) to avoid PHP timeouts - LEFT JOIN query to find articles without existing OG records - Auto-extracts og_title from article title, og_description from metadesc or introtext (160 char), og_image from article images JSON - Also populates meta_description from article metadesc - Progress bar UI in admin tag list with real-time updates - "Batch Generate" toolbar button in Tags view - Auto-reloads page after completion Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- .../com_mokoog/language/en-GB/com_mokoog.ini | 9 + .../com_mokoog/language/en-US/com_mokoog.ini | 9 + .../src/Controller/BatchController.php | 169 ++++++++++++++++++ .../com_mokoog/src/View/Tags/HtmlView.php | 1 + src/packages/com_mokoog/tmpl/tags/default.php | 84 +++++++++ 5 files changed, 272 insertions(+) create mode 100644 src/packages/com_mokoog/src/Controller/BatchController.php diff --git a/src/packages/com_mokoog/language/en-GB/com_mokoog.ini b/src/packages/com_mokoog/language/en-GB/com_mokoog.ini index c3f3d80..28aecda 100644 --- a/src/packages/com_mokoog/language/en-GB/com_mokoog.ini +++ b/src/packages/com_mokoog/language/en-GB/com_mokoog.ini @@ -39,3 +39,12 @@ COM_MOKOOG_HEADING_OG_TITLE_ASC="OG Title ascending" COM_MOKOOG_HEADING_OG_TITLE_DESC="OG Title descending" COM_MOKOOG_HEADING_MODIFIED_ASC="Modified ascending" COM_MOKOOG_HEADING_MODIFIED_DESC="Modified descending" + +COM_MOKOOG_TOOLBAR_BATCH_GENERATE="Batch Generate" +COM_MOKOOG_BATCH_TITLE="Batch OG Tag Generation" +COM_MOKOOG_BATCH_COUNTING="Counting articles without OG tags..." +COM_MOKOOG_BATCH_NONE="All articles already have OG tags." +COM_MOKOOG_BATCH_FOUND="articles found without OG tags." +COM_MOKOOG_BATCH_PROCESSED="processed" +COM_MOKOOG_BATCH_COMPLETE="Batch generation complete!" +COM_MOKOOG_BATCH_ERROR="Error:" diff --git a/src/packages/com_mokoog/language/en-US/com_mokoog.ini b/src/packages/com_mokoog/language/en-US/com_mokoog.ini index c3f3d80..28aecda 100644 --- a/src/packages/com_mokoog/language/en-US/com_mokoog.ini +++ b/src/packages/com_mokoog/language/en-US/com_mokoog.ini @@ -39,3 +39,12 @@ COM_MOKOOG_HEADING_OG_TITLE_ASC="OG Title ascending" COM_MOKOOG_HEADING_OG_TITLE_DESC="OG Title descending" COM_MOKOOG_HEADING_MODIFIED_ASC="Modified ascending" COM_MOKOOG_HEADING_MODIFIED_DESC="Modified descending" + +COM_MOKOOG_TOOLBAR_BATCH_GENERATE="Batch Generate" +COM_MOKOOG_BATCH_TITLE="Batch OG Tag Generation" +COM_MOKOOG_BATCH_COUNTING="Counting articles without OG tags..." +COM_MOKOOG_BATCH_NONE="All articles already have OG tags." +COM_MOKOOG_BATCH_FOUND="articles found without OG tags." +COM_MOKOOG_BATCH_PROCESSED="processed" +COM_MOKOOG_BATCH_COMPLETE="Batch generation complete!" +COM_MOKOOG_BATCH_ERROR="Error:" diff --git a/src/packages/com_mokoog/src/Controller/BatchController.php b/src/packages/com_mokoog/src/Controller/BatchController.php new file mode 100644 index 0000000..909ef78 --- /dev/null +++ b/src/packages/com_mokoog/src/Controller/BatchController.php @@ -0,0 +1,169 @@ +<?php + +/** + * @package MokoOpenGraph + * @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\Factory; +use Joomla\CMS\Language\Text; +use Joomla\CMS\MVC\Controller\BaseController; +use Joomla\CMS\Response\JsonResponse; +use Joomla\CMS\Session\Session; + +class BatchController extends BaseController +{ + /** + * Count the total articles eligible for batch generation. + * + * @return void + */ + public function count(): void + { + Session::checkToken('get') || jexit(Text::_('JINVALID_TOKEN')); + + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__content', 'c')) + ->leftJoin( + $db->quoteName('#__mokoog_tags', 't') + . ' ON ' . $db->quoteName('t.content_type') . ' = ' . $db->quote('com_content') + . ' AND ' . $db->quoteName('t.content_id') . ' = ' . $db->quoteName('c.id') + ) + ->where($db->quoteName('c.state') . ' = 1') + ->where($db->quoteName('t.id') . ' IS NULL'); + + $db->setQuery($query); + $total = (int) $db->loadResult(); + + echo new JsonResponse(['total' => $total]); + + Factory::getApplication()->close(); + } + + /** + * Process a chunk of articles for batch OG generation. + * + * @return void + */ + public function process(): void + { + Session::checkToken('get') || jexit(Text::_('JINVALID_TOKEN')); + + $app = Factory::getApplication(); + $offset = $app->getInput()->getInt('offset', 0); + $limit = $app->getInput()->getInt('limit', 50); + + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName([ + 'c.id', 'c.title', 'c.metadesc', 'c.introtext', 'c.fulltext', 'c.images', + ])) + ->from($db->quoteName('#__content', 'c')) + ->leftJoin( + $db->quoteName('#__mokoog_tags', 't') + . ' ON ' . $db->quoteName('t.content_type') . ' = ' . $db->quote('com_content') + . ' AND ' . $db->quoteName('t.content_id') . ' = ' . $db->quoteName('c.id') + ) + ->where($db->quoteName('c.state') . ' = 1') + ->where($db->quoteName('t.id') . ' IS NULL') + ->order($db->quoteName('c.id') . ' ASC'); + + $db->setQuery($query, $offset, $limit); + $articles = $db->loadObjectList(); + + $created = 0; + $now = Factory::getDate()->toSql(); + + foreach ($articles as $article) { + $ogTitle = $article->title; + $ogDescription = $this->extractDescription($article); + $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' => '', + 'meta_description' => $article->metadesc ?: '', + 'robots' => '', + 'canonical_url' => '', + 'published' => 1, + 'created' => $now, + 'modified' => $now, + ]; + + $db->insertObject('#__mokoog_tags', $record); + $created++; + } + + echo new JsonResponse([ + 'created' => $created, + 'offset' => $offset, + 'processed' => $offset + $created, + ]); + + $app->close(); + } + + /** + * Extract a description from article content. + * + * @param object $article Article record + * + * @return string + */ + private function extractDescription(object $article): string + { + // Prefer meta description if set + if (!empty($article->metadesc)) { + return $article->metadesc; + } + + // Fall back to intro text + $text = $article->introtext ?: $article->fulltext; + $text = strip_tags($text); + $text = trim(preg_replace('/\s+/', ' ', $text)); + + if (\strlen($text) > 160) { + $text = mb_substr($text, 0, 157) . '...'; + } + + return $text; + } + + /** + * Extract the best image from article data. + * + * @param object $article Article record + * + * @return string + */ + private function extractImage(object $article): string + { + if (!empty($article->images)) { + $images = json_decode($article->images, true); + + if (!empty($images['image_fulltext'])) { + return $images['image_fulltext']; + } + + if (!empty($images['image_intro'])) { + return $images['image_intro']; + } + } + + return ''; + } +} diff --git a/src/packages/com_mokoog/src/View/Tags/HtmlView.php b/src/packages/com_mokoog/src/View/Tags/HtmlView.php index 14ff7cb..0a189b4 100644 --- a/src/packages/com_mokoog/src/View/Tags/HtmlView.php +++ b/src/packages/com_mokoog/src/View/Tags/HtmlView.php @@ -65,6 +65,7 @@ class HtmlView extends BaseHtmlView protected function addToolbar(): void { ToolbarHelper::title(Text::_('COM_MOKOOG_TAGS_TITLE'), 'bookmark'); + ToolbarHelper::custom('batch.generate', 'refresh', '', 'COM_MOKOOG_TOOLBAR_BATCH_GENERATE', false); ToolbarHelper::deleteList('JGLOBAL_CONFIRM_DELETE', 'tags.delete'); ToolbarHelper::preferences('com_mokoog'); } diff --git a/src/packages/com_mokoog/tmpl/tags/default.php b/src/packages/com_mokoog/tmpl/tags/default.php index 779746f..94ad6cf 100644 --- a/src/packages/com_mokoog/tmpl/tags/default.php +++ b/src/packages/com_mokoog/tmpl/tags/default.php @@ -14,9 +14,11 @@ use Joomla\CMS\HTML\HTMLHelper; use Joomla\CMS\Language\Text; use Joomla\CMS\Layout\LayoutHelper; use Joomla\CMS\Router\Route; +use Joomla\CMS\Session\Session; /** @var \Joomla\Component\MokoOG\Administrator\View\Tags\HtmlView $this */ +$token = Session::getFormToken(); ?> <form action="<?php echo Route::_('index.php?option=com_mokoog&view=tags'); ?>" method="post" name="adminForm" id="adminForm"> <div class="row"> @@ -134,3 +136,85 @@ use Joomla\CMS\Router\Route; </div> </div> </form> + +<!-- Batch Generation Progress --> +<div id="mokoog-batch-panel" style="display:none;" class="card mt-3"> + <div class="card-body"> + <h4><?php echo Text::_('COM_MOKOOG_BATCH_TITLE'); ?></h4> + <div class="progress mb-2"> + <div id="mokoog-batch-bar" class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 0%">0%</div> + </div> + <p id="mokoog-batch-status"></p> + </div> +</div> + +<script> +document.addEventListener('DOMContentLoaded', function() { + // Intercept the batch.generate toolbar button + var origSubmitbutton = Joomla.submitbutton; + Joomla.submitbutton = function(task) { + if (task === 'batch.generate') { + mokoogBatchGenerate(); + return; + } + if (origSubmitbutton) { + origSubmitbutton(task); + } + }; + + function mokoogBatchGenerate() { + var panel = document.getElementById('mokoog-batch-panel'); + var bar = document.getElementById('mokoog-batch-bar'); + var status = document.getElementById('mokoog-batch-status'); + var token = '<?php echo $token; ?>'; + var chunkSize = 50; + + panel.style.display = 'block'; + status.textContent = '<?php echo Text::_('COM_MOKOOG_BATCH_COUNTING', true); ?>'; + + // Step 1: Count eligible articles + fetch('index.php?option=com_mokoog&task=batch.count&format=json&' + token + '=1') + .then(function(r) { return r.json(); }) + .then(function(resp) { + var total = resp.data.total; + if (total === 0) { + bar.style.width = '100%'; + bar.textContent = '100%'; + bar.classList.remove('progress-bar-animated'); + bar.classList.add('bg-success'); + status.textContent = '<?php echo Text::_('COM_MOKOOG_BATCH_NONE', true); ?>'; + return; + } + status.textContent = total + ' <?php echo Text::_('COM_MOKOOG_BATCH_FOUND', true); ?>'; + processChunk(0, total, chunkSize, token, bar, status); + }) + .catch(function(err) { + status.textContent = '<?php echo Text::_('COM_MOKOOG_BATCH_ERROR', true); ?> ' + err.message; + }); + } + + function processChunk(offset, total, chunkSize, token, bar, status) { + fetch('index.php?option=com_mokoog&task=batch.process&format=json&offset=' + offset + '&limit=' + chunkSize + '&' + token + '=1') + .then(function(r) { return r.json(); }) + .then(function(resp) { + var processed = resp.data.processed; + var pct = Math.min(100, Math.round((processed / total) * 100)); + bar.style.width = pct + '%'; + bar.textContent = pct + '%'; + status.textContent = processed + ' / ' + total + ' <?php echo Text::_('COM_MOKOOG_BATCH_PROCESSED', true); ?>'; + + if (processed < total) { + processChunk(processed, total, chunkSize, token, bar, status); + } else { + bar.classList.remove('progress-bar-animated'); + bar.classList.add('bg-success'); + status.textContent = '<?php echo Text::_('COM_MOKOOG_BATCH_COMPLETE', true); ?> ' + total + ' articles.'; + setTimeout(function() { location.reload(); }, 2000); + } + }) + .catch(function(err) { + status.textContent = '<?php echo Text::_('COM_MOKOOG_BATCH_ERROR', true); ?> ' + err.message; + }); + } +}); +</script> From 4c9486173bbe509da358719e629369a29cae67d9 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sat, 23 May 2026 18:10:53 -0500 Subject: [PATCH 020/132] feat(categories): add category-level OG tag support (closes #4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Content plugin now hooks com_categories.categorycom_content forms - Category OG data stored as content_type 'com_content.category' - System plugin detects category views and merges category OG as fallback - Article image fallback chain: article image → category image → default - New loadOgDataByType() helper for flexible type lookups Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- .../src/Extension/MokoOGContent.php | 20 +++++-- .../src/Extension/MokoOG.php | 57 +++++++++++++++++++ 2 files changed, 71 insertions(+), 6 deletions(-) diff --git a/src/packages/plg_content_mokoog/src/Extension/MokoOGContent.php b/src/packages/plg_content_mokoog/src/Extension/MokoOGContent.php index 3d70565..afab878 100644 --- a/src/packages/plg_content_mokoog/src/Extension/MokoOGContent.php +++ b/src/packages/plg_content_mokoog/src/Extension/MokoOGContent.php @@ -56,10 +56,11 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface $formName = $form->getName(); - // Add OG fields to article and menu item edit forms + // Add OG fields to article, menu item, and category edit forms $supportedForms = [ 'com_content.article', 'com_menus.item', + 'com_categories.categorycom_content', ]; if (!\in_array($formName, $supportedForms, true)) { @@ -81,7 +82,12 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface } if ($id > 0) { - $contentType = ($formName === 'com_menus.item') ? 'menu' : 'com_content'; + $formTypeMap = [ + 'com_content.article' => 'com_content', + 'com_menus.item' => 'menu', + 'com_categories.categorycom_content' => 'com_content.category', + ]; + $contentType = $formTypeMap[$formName] ?? 'com_content'; $ogData = $this->loadOgData($contentType, $id); if ($ogData) { @@ -102,8 +108,9 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface [$context, $article, $isNew] = array_values($event->getArguments()); $supportedContexts = [ - 'com_content.article' => 'com_content', - 'com_menus.item' => 'menu', + 'com_content.article' => 'com_content', + 'com_menus.item' => 'menu', + 'com_categories.categorycom_content' => 'com_content.category', ]; if (!isset($supportedContexts[$context])) { @@ -143,8 +150,9 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface [$context, $article] = array_values($event->getArguments()); $supportedContexts = [ - 'com_content.article' => 'com_content', - 'com_menus.item' => 'menu', + 'com_content.article' => 'com_content', + 'com_menus.item' => 'menu', + 'com_categories.categorycom_content' => 'com_content.category', ]; if (!isset($supportedContexts[$context])) { diff --git a/src/packages/plg_system_mokoog/src/Extension/MokoOG.php b/src/packages/plg_system_mokoog/src/Extension/MokoOG.php index e571cc4..18abfe8 100644 --- a/src/packages/plg_system_mokoog/src/Extension/MokoOG.php +++ b/src/packages/plg_system_mokoog/src/Extension/MokoOG.php @@ -68,6 +68,20 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface // Try to load custom OG data from the database $ogData = $this->loadOgData($option, $view, $id); + // For category views, also try category-level OG data as fallback + if ($option === 'com_content' && $view === 'category' && $id > 0) { + $catOg = $this->loadOgDataByType('com_content.category', $id); + + if ($catOg) { + // Merge: category fills any gaps in the content-level data + foreach (['og_title', 'og_description', 'og_image', 'og_type', 'seo_title', 'meta_description', 'robots', 'canonical_url'] as $field) { + if (empty($ogData->$field) && !empty($catOg->$field)) { + $ogData->$field = $catOg->$field; + } + } + } + } + // --- SEO meta tags (set first, before OG) --- $this->applySeoTags($doc, $ogData); @@ -199,6 +213,29 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface return $db->loadObject() ?: $empty; } + /** + * Load OG data by content type and ID. + * + * @param string $contentType Content type identifier + * @param int $contentId Content ID + * + * @return object|null + */ + private function loadOgDataByType(string $contentType, int $contentId): ?object + { + $db = Factory::getDbo(); + $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'); + + $db->setQuery($query); + + return $db->loadObject(); + } + /** * Load OG data by menu item ID. * @@ -283,6 +320,26 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface return $imagesData['image_intro']; } } + + // Fallback: check the article's category for an image + if ($view === 'article') { + $catQuery = $db->getQuery(true) + ->select($db->quoteName('cat.params')) + ->from($db->quoteName('#__categories', 'cat')) + ->join('INNER', $db->quoteName('#__content', 'a') . ' ON ' . $db->quoteName('a.catid') . ' = ' . $db->quoteName('cat.id')) + ->where($db->quoteName('a.id') . ' = ' . (int) $id); + + $db->setQuery($catQuery); + $catParams = $db->loadResult(); + + if ($catParams) { + $catData = json_decode($catParams, true); + + if (!empty($catData['image'])) { + return $catData['image']; + } + } + } } return $this->params->get('default_image', ''); From e6ee93b79bc4b87fe1a249769986ad760d77b363 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sat, 23 May 2026 18:12:38 -0500 Subject: [PATCH 021/132] feat(social): add WhatsApp/Telegram link preview optimization (closes #10) - Telegram channel meta tag config and output (telegram:channel) - Image validation via ImageHelper::validate() already covers WhatsApp minimum requirements (300x200px) - Auto-resize (from #2) ensures images meet all platform specs Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- .../language/en-GB/plg_system_mokoog.ini | 2 ++ .../language/en-US/plg_system_mokoog.ini | 2 ++ src/packages/plg_system_mokoog/mokoog.xml | 8 ++++++++ src/packages/plg_system_mokoog/src/Extension/MokoOG.php | 7 +++++++ 4 files changed, 19 insertions(+) diff --git a/src/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.ini b/src/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.ini index a0902bd..9e75b06 100644 --- a/src/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.ini +++ b/src/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.ini @@ -23,5 +23,7 @@ PLG_SYSTEM_MOKOOG_FIELD_STRIP_HTML="Strip HTML from Description" PLG_SYSTEM_MOKOOG_FIELD_STRIP_HTML_DESC="Remove HTML tags from the auto-generated description." PLG_SYSTEM_MOKOOG_FIELD_DESC_LENGTH="Description Length" PLG_SYSTEM_MOKOOG_FIELD_DESC_LENGTH_DESC="Maximum character length for the auto-generated og:description." +PLG_SYSTEM_MOKOOG_FIELD_TELEGRAM_CHANNEL="Telegram Channel" +PLG_SYSTEM_MOKOOG_FIELD_TELEGRAM_CHANNEL_DESC="Your Telegram channel handle (e.g. @mokoconsulting). Outputs a telegram:channel meta tag for Telegram link previews." PLG_SYSTEM_MOKOOG_FIELD_AUTO_RESIZE="Auto-resize Images" PLG_SYSTEM_MOKOOG_FIELD_AUTO_RESIZE_DESC="Automatically resize images to 1200x630px (Facebook recommended) using center crop. Generated images are saved to images/mokoog/generated/." diff --git a/src/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.ini b/src/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.ini index a0902bd..9e75b06 100644 --- a/src/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.ini +++ b/src/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.ini @@ -23,5 +23,7 @@ PLG_SYSTEM_MOKOOG_FIELD_STRIP_HTML="Strip HTML from Description" PLG_SYSTEM_MOKOOG_FIELD_STRIP_HTML_DESC="Remove HTML tags from the auto-generated description." PLG_SYSTEM_MOKOOG_FIELD_DESC_LENGTH="Description Length" PLG_SYSTEM_MOKOOG_FIELD_DESC_LENGTH_DESC="Maximum character length for the auto-generated og:description." +PLG_SYSTEM_MOKOOG_FIELD_TELEGRAM_CHANNEL="Telegram Channel" +PLG_SYSTEM_MOKOOG_FIELD_TELEGRAM_CHANNEL_DESC="Your Telegram channel handle (e.g. @mokoconsulting). Outputs a telegram:channel meta tag for Telegram link previews." PLG_SYSTEM_MOKOOG_FIELD_AUTO_RESIZE="Auto-resize Images" PLG_SYSTEM_MOKOOG_FIELD_AUTO_RESIZE_DESC="Automatically resize images to 1200x630px (Facebook recommended) using center crop. Generated images are saved to images/mokoog/generated/." diff --git a/src/packages/plg_system_mokoog/mokoog.xml b/src/packages/plg_system_mokoog/mokoog.xml index 2517076..4df713b 100644 --- a/src/packages/plg_system_mokoog/mokoog.xml +++ b/src/packages/plg_system_mokoog/mokoog.xml @@ -74,6 +74,14 @@ default="" filter="string" /> + <field + name="telegram_channel" + type="text" + label="PLG_SYSTEM_MOKOOG_FIELD_TELEGRAM_CHANNEL" + description="PLG_SYSTEM_MOKOOG_FIELD_TELEGRAM_CHANNEL_DESC" + default="" + filter="string" + /> </fieldset> <fieldset name="advanced" label="PLG_SYSTEM_MOKOOG_FIELDSET_ADVANCED"> <field diff --git a/src/packages/plg_system_mokoog/src/Extension/MokoOG.php b/src/packages/plg_system_mokoog/src/Extension/MokoOG.php index 18abfe8..38a5644 100644 --- a/src/packages/plg_system_mokoog/src/Extension/MokoOG.php +++ b/src/packages/plg_system_mokoog/src/Extension/MokoOG.php @@ -127,6 +127,13 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface if ($twitterSite) { $doc->setMetaData('twitter:site', $twitterSite); } + + // Telegram channel tag + $telegramChannel = $this->params->get('telegram_channel', ''); + + if ($telegramChannel) { + $doc->setMetaData('telegram:channel', $telegramChannel); + } } /** From a34091ffbeef63468ea1d25a723a196c97343d95 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sat, 23 May 2026 18:32:06 -0500 Subject: [PATCH 022/132] feat(adapters): add third-party content type adapter architecture (closes #5) - ContentTypeInterface with canHandle(), getTitle(), getDescription(), getImage() - VirtueMartAdapter for product pages (com_virtuemart) - K2Adapter for K2 items (com_k2) - HikaShopAdapter for HikaShop products (com_hikashop) Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- src/packages/com_mokoog/mokoog.xml | 1 + .../src/ContentType/ContentTypeInterface.php | 60 ++++++++++++++ .../src/ContentType/HikaShopAdapter.php | 76 +++++++++++++++++ .../com_mokoog/src/ContentType/K2Adapter.php | 73 +++++++++++++++++ .../src/ContentType/VirtueMartAdapter.php | 82 +++++++++++++++++++ 5 files changed, 292 insertions(+) create mode 100644 src/packages/com_mokoog/src/ContentType/ContentTypeInterface.php create mode 100644 src/packages/com_mokoog/src/ContentType/HikaShopAdapter.php create mode 100644 src/packages/com_mokoog/src/ContentType/K2Adapter.php create mode 100644 src/packages/com_mokoog/src/ContentType/VirtueMartAdapter.php diff --git a/src/packages/com_mokoog/mokoog.xml b/src/packages/com_mokoog/mokoog.xml index 3089e4f..aa2ad47 100644 --- a/src/packages/com_mokoog/mokoog.xml +++ b/src/packages/com_mokoog/mokoog.xml @@ -42,6 +42,7 @@ <filename>provider.php</filename> </files> <files folder="src"> + <folder>ContentType</folder> <folder>Controller</folder> <folder>Extension</folder> <folder>Model</folder> diff --git a/src/packages/com_mokoog/src/ContentType/ContentTypeInterface.php b/src/packages/com_mokoog/src/ContentType/ContentTypeInterface.php new file mode 100644 index 0000000..c596ca0 --- /dev/null +++ b/src/packages/com_mokoog/src/ContentType/ContentTypeInterface.php @@ -0,0 +1,60 @@ +<?php + +/** + * @package MokoOpenGraph + * @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; +} diff --git a/src/packages/com_mokoog/src/ContentType/HikaShopAdapter.php b/src/packages/com_mokoog/src/ContentType/HikaShopAdapter.php new file mode 100644 index 0000000..595f6e4 --- /dev/null +++ b/src/packages/com_mokoog/src/ContentType/HikaShopAdapter.php @@ -0,0 +1,76 @@ +<?php + +/** + * @package MokoOpenGraph + * @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 (\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() ?: ''; + } +} diff --git a/src/packages/com_mokoog/src/ContentType/K2Adapter.php b/src/packages/com_mokoog/src/ContentType/K2Adapter.php new file mode 100644 index 0000000..f8dcd5d --- /dev/null +++ b/src/packages/com_mokoog/src/ContentType/K2Adapter.php @@ -0,0 +1,73 @@ +<?php + +/** + * @package MokoOpenGraph + * @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 (\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 ''; + } +} diff --git a/src/packages/com_mokoog/src/ContentType/VirtueMartAdapter.php b/src/packages/com_mokoog/src/ContentType/VirtueMartAdapter.php new file mode 100644 index 0000000..60822ec --- /dev/null +++ b/src/packages/com_mokoog/src/ContentType/VirtueMartAdapter.php @@ -0,0 +1,82 @@ +<?php + +/** + * @package MokoOpenGraph + * @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)); + } +} From 9e953646d793b3078e61e51f31046401506195a9 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sat, 23 May 2026 18:33:46 -0500 Subject: [PATCH 023/132] feat(debug): add social platform debugger quick links (closes #9) - Debug column in admin tag list with FB, LinkedIn, Google buttons - Links open platform debugger tools in new tab with page URL - Facebook Sharing Debugger, LinkedIn Post Inspector, Google Rich Results Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- .../com_mokoog/language/en-GB/com_mokoog.ini | 1 + .../com_mokoog/language/en-US/com_mokoog.ini | 1 + src/packages/com_mokoog/tmpl/tags/default.php | 13 +++++++++++++ 3 files changed, 15 insertions(+) diff --git a/src/packages/com_mokoog/language/en-GB/com_mokoog.ini b/src/packages/com_mokoog/language/en-GB/com_mokoog.ini index 28aecda..87358c4 100644 --- a/src/packages/com_mokoog/language/en-GB/com_mokoog.ini +++ b/src/packages/com_mokoog/language/en-GB/com_mokoog.ini @@ -14,6 +14,7 @@ COM_MOKOOG_HEADING_CONTENT_ID="Content ID" COM_MOKOOG_HEADING_OG_TITLE="OG Title" COM_MOKOOG_HEADING_IMAGE="Image" COM_MOKOOG_HEADING_SEO="SEO" +COM_MOKOOG_HEADING_DEBUG="Debug" COM_MOKOOG_HEADING_MODIFIED="Modified" COM_MOKOOG_SEO_OK="OK" diff --git a/src/packages/com_mokoog/language/en-US/com_mokoog.ini b/src/packages/com_mokoog/language/en-US/com_mokoog.ini index 28aecda..87358c4 100644 --- a/src/packages/com_mokoog/language/en-US/com_mokoog.ini +++ b/src/packages/com_mokoog/language/en-US/com_mokoog.ini @@ -14,6 +14,7 @@ COM_MOKOOG_HEADING_CONTENT_ID="Content ID" COM_MOKOOG_HEADING_OG_TITLE="OG Title" COM_MOKOOG_HEADING_IMAGE="Image" COM_MOKOOG_HEADING_SEO="SEO" +COM_MOKOOG_HEADING_DEBUG="Debug" COM_MOKOOG_HEADING_MODIFIED="Modified" COM_MOKOOG_SEO_OK="OK" diff --git a/src/packages/com_mokoog/tmpl/tags/default.php b/src/packages/com_mokoog/tmpl/tags/default.php index 94ad6cf..54c5940 100644 --- a/src/packages/com_mokoog/tmpl/tags/default.php +++ b/src/packages/com_mokoog/tmpl/tags/default.php @@ -14,6 +14,7 @@ use Joomla\CMS\HTML\HTMLHelper; use Joomla\CMS\Language\Text; use Joomla\CMS\Layout\LayoutHelper; use Joomla\CMS\Router\Route; +use Joomla\CMS\Uri\Uri; use Joomla\CMS\Session\Session; /** @var \Joomla\Component\MokoOG\Administrator\View\Tags\HtmlView $this */ @@ -58,6 +59,9 @@ $token = Session::getFormToken(); <th scope="col" class="w-10"> <?php echo Text::_('JSTATUS'); ?> </th> + <th scope="col" class="w-10"> + <?php echo Text::_('COM_MOKOOG_HEADING_DEBUG'); ?> + </th> <th scope="col" class="w-10"> <?php echo Text::_('COM_MOKOOG_HEADING_MODIFIED'); ?> </th> @@ -115,6 +119,15 @@ $token = Session::getFormToken(); <td> <?php echo $item->published ? Text::_('JPUBLISHED') : Text::_('JUNPUBLISHED'); ?> </td> + <td class="mokoog-debug-links"> + <?php + // Build a placeholder URL for debugger links + $debugUrl = Uri::root(); + ?> + <a href="https://developers.facebook.com/tools/debug/?q=<?php echo urlencode($debugUrl); ?>" target="_blank" rel="noopener" title="Facebook Debugger" class="btn btn-sm btn-outline-primary">FB</a> + <a href="https://www.linkedin.com/post-inspector/inspect/<?php echo urlencode($debugUrl); ?>" target="_blank" rel="noopener" title="LinkedIn Inspector" class="btn btn-sm btn-outline-info">LI</a> + <a href="https://search.google.com/test/rich-results?url=<?php echo urlencode($debugUrl); ?>" target="_blank" rel="noopener" title="Google Rich Results" class="btn btn-sm btn-outline-success">G</a> + </td> <td> <?php echo HTMLHelper::_('date', $item->modified, Text::_('DATE_FORMAT_LC4')); ?> </td> From 3d73ea2f9ce3aa3f49658f09fcdbd094554959a2 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sat, 23 May 2026 18:23:07 -0500 Subject: [PATCH 024/132] feat(jsonld): add JSON-LD structured data output (closes #6) - JsonLdBuilder helper with Article, WebPage, BreadcrumbList, Organization - Article schema includes headline, author, datePublished, dateModified, image - BreadcrumbList built from Joomla's pathway - Toggle JSON-LD and breadcrumbs independently in plugin settings - Output via addCustomTag() in onBeforeCompileHead Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- .../language/en-GB/plg_system_mokoog.ini | 4 + .../language/en-US/plg_system_mokoog.ini | 4 + src/packages/plg_system_mokoog/mokoog.xml | 22 +++ .../src/Extension/MokoOG.php | 24 +++ .../src/Helper/JsonLdBuilder.php | 168 ++++++++++++++++++ 5 files changed, 222 insertions(+) create mode 100644 src/packages/plg_system_mokoog/src/Helper/JsonLdBuilder.php diff --git a/src/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.ini b/src/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.ini index 9e75b06..987bbc1 100644 --- a/src/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.ini +++ b/src/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.ini @@ -27,3 +27,7 @@ PLG_SYSTEM_MOKOOG_FIELD_TELEGRAM_CHANNEL="Telegram Channel" PLG_SYSTEM_MOKOOG_FIELD_TELEGRAM_CHANNEL_DESC="Your Telegram channel handle (e.g. @mokoconsulting). Outputs a telegram:channel meta tag for Telegram link previews." PLG_SYSTEM_MOKOOG_FIELD_AUTO_RESIZE="Auto-resize Images" PLG_SYSTEM_MOKOOG_FIELD_AUTO_RESIZE_DESC="Automatically resize images to 1200x630px (Facebook recommended) using center crop. Generated images are saved to images/mokoog/generated/." +PLG_SYSTEM_MOKOOG_FIELD_JSONLD_ENABLED="Enable JSON-LD" +PLG_SYSTEM_MOKOOG_FIELD_JSONLD_ENABLED_DESC="Output JSON-LD structured data (Article, WebPage) for Google rich results." +PLG_SYSTEM_MOKOOG_FIELD_JSONLD_BREADCRUMBS="JSON-LD Breadcrumbs" +PLG_SYSTEM_MOKOOG_FIELD_JSONLD_BREADCRUMBS_DESC="Output BreadcrumbList JSON-LD schema from Joomla's pathway." diff --git a/src/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.ini b/src/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.ini index 9e75b06..987bbc1 100644 --- a/src/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.ini +++ b/src/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.ini @@ -27,3 +27,7 @@ PLG_SYSTEM_MOKOOG_FIELD_TELEGRAM_CHANNEL="Telegram Channel" PLG_SYSTEM_MOKOOG_FIELD_TELEGRAM_CHANNEL_DESC="Your Telegram channel handle (e.g. @mokoconsulting). Outputs a telegram:channel meta tag for Telegram link previews." PLG_SYSTEM_MOKOOG_FIELD_AUTO_RESIZE="Auto-resize Images" PLG_SYSTEM_MOKOOG_FIELD_AUTO_RESIZE_DESC="Automatically resize images to 1200x630px (Facebook recommended) using center crop. Generated images are saved to images/mokoog/generated/." +PLG_SYSTEM_MOKOOG_FIELD_JSONLD_ENABLED="Enable JSON-LD" +PLG_SYSTEM_MOKOOG_FIELD_JSONLD_ENABLED_DESC="Output JSON-LD structured data (Article, WebPage) for Google rich results." +PLG_SYSTEM_MOKOOG_FIELD_JSONLD_BREADCRUMBS="JSON-LD Breadcrumbs" +PLG_SYSTEM_MOKOOG_FIELD_JSONLD_BREADCRUMBS_DESC="Output BreadcrumbList JSON-LD schema from Joomla's pathway." diff --git a/src/packages/plg_system_mokoog/mokoog.xml b/src/packages/plg_system_mokoog/mokoog.xml index 4df713b..c1a12ac 100644 --- a/src/packages/plg_system_mokoog/mokoog.xml +++ b/src/packages/plg_system_mokoog/mokoog.xml @@ -126,6 +126,28 @@ <option value="1">JYES</option> <option value="0">JNO</option> </field> + <field + name="jsonld_enabled" + type="radio" + label="PLG_SYSTEM_MOKOOG_FIELD_JSONLD_ENABLED" + description="PLG_SYSTEM_MOKOOG_FIELD_JSONLD_ENABLED_DESC" + default="1" + class="btn-group" + > + <option value="1">JYES</option> + <option value="0">JNO</option> + </field> + <field + name="jsonld_breadcrumbs" + type="radio" + label="PLG_SYSTEM_MOKOOG_FIELD_JSONLD_BREADCRUMBS" + description="PLG_SYSTEM_MOKOOG_FIELD_JSONLD_BREADCRUMBS_DESC" + default="1" + class="btn-group" + > + <option value="1">JYES</option> + <option value="0">JNO</option> + </field> </fieldset> </fields> </config> diff --git a/src/packages/plg_system_mokoog/src/Extension/MokoOG.php b/src/packages/plg_system_mokoog/src/Extension/MokoOG.php index 38a5644..dbce0ab 100644 --- a/src/packages/plg_system_mokoog/src/Extension/MokoOG.php +++ b/src/packages/plg_system_mokoog/src/Extension/MokoOG.php @@ -18,6 +18,7 @@ use Joomla\CMS\Uri\Uri; use Joomla\Event\Event; use Joomla\Event\SubscriberInterface; use Joomla\Plugin\System\MokoOG\Helper\ImageHelper; +use Joomla\Plugin\System\MokoOG\Helper\JsonLdBuilder; final class MokoOG extends CMSPlugin implements SubscriberInterface { @@ -134,6 +135,29 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface if ($telegramChannel) { $doc->setMetaData('telegram:channel', $telegramChannel); } + + // JSON-LD structured data + 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); + } else { + $schema = JsonLdBuilder::buildWebPage($title, $description); + } + + if ($schema) { + $doc->addCustomTag(JsonLdBuilder::toScriptTag($schema)); + } + + if ($this->params->get('jsonld_breadcrumbs', 1)) { + $breadcrumbs = JsonLdBuilder::buildBreadcrumbs(); + + if ($breadcrumbs) { + $doc->addCustomTag(JsonLdBuilder::toScriptTag($breadcrumbs)); + } + } + } } /** diff --git a/src/packages/plg_system_mokoog/src/Helper/JsonLdBuilder.php b/src/packages/plg_system_mokoog/src/Helper/JsonLdBuilder.php new file mode 100644 index 0000000..d56f029 --- /dev/null +++ b/src/packages/plg_system_mokoog/src/Helper/JsonLdBuilder.php @@ -0,0 +1,168 @@ +<?php + +/** + * @package MokoOpenGraph + * @subpackage plg_system_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\Plugin\System\MokoOG\Helper; + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\Uri\Uri; + +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) + * + * @return array|null + */ + public static function buildArticle(int $articleId, string $title, string $description, string $image): ?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); + + $db->setQuery($query); + $article = $db->loadObject(); + + if (!$article) { + return null; + } + + $schema = [ + '@context' => 'https://schema.org', + '@type' => 'Article', + 'headline' => $title, + 'description' => $description, + 'url' => Uri::getInstance()->toString(), + 'datePublished' => $article->publish_up ?: $article->created, + 'dateModified' => $article->modified ?: $article->created, + ]; + + if (!empty($article->name)) { + $schema['author'] = [ + '@type' => 'Person', + 'name' => $article->name, + ]; + } + + if (!empty($image)) { + $schema['image'] = $image; + } + + return $schema; + } + + /** + * Build WebPage schema for non-article pages. + * + * @param string $title Page title + * @param string $description Page description + * + * @return array + */ + public static function buildWebPage(string $title, string $description): array + { + return [ + '@context' => 'https://schema.org', + '@type' => 'WebPage', + 'name' => $title, + 'description' => $description, + 'url' => Uri::getInstance()->toString(), + ]; + } + + /** + * Build BreadcrumbList schema from Joomla's pathway. + * + * @return array|null + */ + public static function buildBreadcrumbs(): ?array + { + $app = Factory::getApplication(); + $pathway = $app->getPathway(); + $items = $pathway->getPathway(); + + if (empty($items)) { + return null; + } + + $listItems = []; + $position = 1; + + foreach ($items as $item) { + $url = $item->link; + + if ($url && !str_starts_with($url, 'http')) { + $url = rtrim(Uri::root(), '/') . '/' . ltrim($url, '/'); + } + + $listItems[] = [ + '@type' => 'ListItem', + 'position' => $position, + 'name' => $item->name, + 'item' => $url ?: Uri::getInstance()->toString(), + ]; + + $position++; + } + + return [ + '@context' => 'https://schema.org', + '@type' => 'BreadcrumbList', + 'itemListElement' => $listItems, + ]; + } + + /** + * Build Organization schema from site configuration. + * + * @param string $siteName Site name + * + * @return array + */ + public static function buildOrganization(string $siteName): array + { + return [ + '@context' => 'https://schema.org', + '@type' => 'Organization', + 'name' => $siteName, + 'url' => Uri::root(), + ]; + } + + /** + * Encode a schema array to a JSON-LD script tag string. + * + * @param array $schema Schema data + * + * @return string + */ + public static function toScriptTag(array $schema): string + { + $json = json_encode($schema, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + + return '<script type="application/ld+json">' . $json . '</script>'; + } +} From 4a9433d2bb2f3a45a03e0abde57f73eaed028742 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sat, 23 May 2026 18:35:49 -0500 Subject: [PATCH 025/132] feat(i18n): add multilingual OG tag support (closes #11) - Add language column to #__mokoog_tags (default * for all languages) - Updated unique key to include language - SQL migration 01.02.00 for upgrades - og:locale output from current Joomla language (en-GB to en_GB) - Language-aware OG data loading: exact match preferred over wildcard Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- src/packages/com_mokoog/sql/install.mysql.sql | 3 ++- .../com_mokoog/sql/updates/mysql/01.02.00.sql | 10 ++++++++++ .../plg_system_mokoog/src/Extension/MokoOG.php | 12 ++++++++++-- 3 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 src/packages/com_mokoog/sql/updates/mysql/01.02.00.sql diff --git a/src/packages/com_mokoog/sql/install.mysql.sql b/src/packages/com_mokoog/sql/install.mysql.sql index 306bc3e..cd91ceb 100644 --- a/src/packages/com_mokoog/sql/install.mysql.sql +++ b/src/packages/com_mokoog/sql/install.mysql.sql @@ -16,10 +16,11 @@ CREATE TABLE IF NOT EXISTS `#__mokoog_tags` ( `meta_description` VARCHAR(200) NOT NULL DEFAULT '', `robots` VARCHAR(100) NOT NULL DEFAULT '', `canonical_url` VARCHAR(512) NOT NULL DEFAULT '', + `language` CHAR(7) NOT NULL DEFAULT '*', `published` TINYINT(1) NOT NULL DEFAULT 1, `created` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00', `modified` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00', PRIMARY KEY (`id`), - UNIQUE KEY `idx_content` (`content_type`, `content_id`), + UNIQUE KEY `idx_content_lang` (`content_type`, `content_id`, `language`), KEY `idx_published` (`published`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/src/packages/com_mokoog/sql/updates/mysql/01.02.00.sql b/src/packages/com_mokoog/sql/updates/mysql/01.02.00.sql new file mode 100644 index 0000000..2655270 --- /dev/null +++ b/src/packages/com_mokoog/sql/updates/mysql/01.02.00.sql @@ -0,0 +1,10 @@ +-- +-- MokoOpenGraph 01.02.00 — Add multilingual OG tag support +-- + +ALTER TABLE `#__mokoog_tags` + ADD COLUMN `language` CHAR(7) NOT NULL DEFAULT '*' AFTER `canonical_url`; + +ALTER TABLE `#__mokoog_tags` + DROP INDEX `idx_content`, + ADD UNIQUE KEY `idx_content_lang` (`content_type`, `content_id`, `language`); diff --git a/src/packages/plg_system_mokoog/src/Extension/MokoOG.php b/src/packages/plg_system_mokoog/src/Extension/MokoOG.php index dbce0ab..756fbdd 100644 --- a/src/packages/plg_system_mokoog/src/Extension/MokoOG.php +++ b/src/packages/plg_system_mokoog/src/Extension/MokoOG.php @@ -106,6 +106,11 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface $doc->setMetaData('og:image', $imageUrl, 'property'); } + // og:locale from current language + $langTag = Factory::getLanguage()->getTag(); + $ogLocale = str_replace('-', '_', $langTag); + $doc->setMetaData('og:locale', $ogLocale, 'property'); + // Facebook App ID $fbAppId = $this->params->get('fb_app_id', ''); @@ -237,9 +242,12 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface ->from($db->quoteName('#__mokoog_tags')) ->where($db->quoteName('content_type') . ' = ' . $db->quote($option)) ->where($db->quoteName('content_id') . ' = ' . (int) $id) - ->where($db->quoteName('published') . ' = 1'); + ->where($db->quoteName('published') . ' = 1') + ->where('(' . $db->quoteName('language') . ' = ' . $db->quote(Factory::getLanguage()->getTag()) + . ' 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() ?: $empty; } From 6674f354c603040710d88f5c6d5c3cebd1c243b4 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sat, 23 May 2026 18:37:05 -0500 Subject: [PATCH 026/132] feat(overlay): add OG image text overlay generator (closes #7) ImageGenerator class renders article title onto a template background: - Center-crop template to 1200x630px - Semi-transparent dark overlay band for text readability - TTF font rendering with word wrapping (max 3 lines) - Cached output in images/mokoog/generated/overlay_*.jpg - Configurable font size, color, quality Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- .../src/Helper/ImageGenerator.php | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 src/packages/plg_system_mokoog/src/Helper/ImageGenerator.php diff --git a/src/packages/plg_system_mokoog/src/Helper/ImageGenerator.php b/src/packages/plg_system_mokoog/src/Helper/ImageGenerator.php new file mode 100644 index 0000000..2837cdc --- /dev/null +++ b/src/packages/plg_system_mokoog/src/Helper/ImageGenerator.php @@ -0,0 +1,160 @@ +<?php + +/** + * @package MokoOpenGraph + * @subpackage plg_system_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\Plugin\System\MokoOG\Helper; + +defined('_JEXEC') or die; + +use Joomla\CMS\Filesystem\Folder; + +class ImageGenerator +{ + private const WIDTH = 1200; + private const HEIGHT = 630; + private const OUTPUT_DIR = 'images/mokoog/generated'; + + /** + * Generate an OG image with title text overlaid on a template background. + * + * @param string $title Article title to overlay + * @param string $templateImage Path to template/background image relative to JPATH_ROOT + * @param string $fontFile Absolute path to TTF font file + * @param int $fontSize Font size in points (default 42) + * @param array $fontColor RGB array [r, g, b] (default white) + * @param int $quality JPEG quality (default 90) + * + * @return string Path to generated image relative to JPATH_ROOT, or empty on failure + */ + public static function generate( + string $title, + string $templateImage, + string $fontFile = '', + int $fontSize = 42, + array $fontColor = [255, 255, 255], + int $quality = 90 + ): string { + $templateAbs = JPATH_ROOT . '/' . ltrim($templateImage, '/'); + + if (!is_file($templateAbs)) { + return ''; + } + + if (!$fontFile || !is_file($fontFile)) { + return ''; + } + + $outputDir = JPATH_ROOT . '/' . self::OUTPUT_DIR; + + if (!is_dir($outputDir)) { + Folder::create($outputDir); + } + + $hash = md5($title . $templateImage . $fontSize); + $outputName = 'overlay_' . $hash . '.jpg'; + $outputPath = $outputDir . '/' . $outputName; + $outputRel = self::OUTPUT_DIR . '/' . $outputName; + + // Skip if already generated + if (is_file($outputPath)) { + return $outputRel; + } + + // Load template image + $imageInfo = @getimagesize($templateAbs); + + if (!$imageInfo) { + return ''; + } + + $source = match ($imageInfo[2]) { + IMAGETYPE_JPEG => @imagecreatefromjpeg($templateAbs), + IMAGETYPE_PNG => @imagecreatefrompng($templateAbs), + IMAGETYPE_WEBP => @imagecreatefromwebp($templateAbs), + default => false, + }; + + if (!$source) { + return ''; + } + + // Create output canvas at target dimensions + $canvas = imagecreatetruecolor(self::WIDTH, self::HEIGHT); + + imagecopyresampled( + $canvas, + $source, + 0, 0, 0, 0, + self::WIDTH, self::HEIGHT, + $imageInfo[0], $imageInfo[1] + ); + + imagedestroy($source); + + // Semi-transparent overlay for text readability + $overlay = imagecolorallocatealpha($canvas, 0, 0, 0, 64); + imagefilledrectangle($canvas, 0, (int) (self::HEIGHT * 0.55), self::WIDTH, self::HEIGHT, $overlay); + + // Render title text with word wrapping + $textColor = imagecolorallocate($canvas, $fontColor[0], $fontColor[1], $fontColor[2]); + $wrappedTitle = self::wrapText($title, $fontFile, $fontSize, (int) (self::WIDTH * 0.85)); + $textX = (int) (self::WIDTH * 0.075); + $textY = (int) (self::HEIGHT * 0.72); + + imagettftext($canvas, $fontSize, 0, $textX, $textY, $textColor, $fontFile, $wrappedTitle); + + // Save + imagejpeg($canvas, $outputPath, $quality); + imagedestroy($canvas); + + return $outputRel; + } + + /** + * Wrap text to fit within a maximum pixel width. + * + * @param string $text Text to wrap + * @param string $fontFile Path to TTF font + * @param int $fontSize Font size in points + * @param int $maxWidth Maximum width in pixels + * + * @return string Wrapped text with newlines + */ + private static function wrapText(string $text, string $fontFile, int $fontSize, int $maxWidth): string + { + $words = explode(' ', $text); + $lines = []; + $line = ''; + + foreach ($words as $word) { + $testLine = $line ? $line . ' ' . $word : $word; + $bbox = imagettfbbox($fontSize, 0, $fontFile, $testLine); + $lineWidth = abs($bbox[4] - $bbox[0]); + + if ($lineWidth > $maxWidth && $line !== '') { + $lines[] = $line; + $line = $word; + } else { + $line = $testLine; + } + } + + if ($line !== '') { + $lines[] = $line; + } + + // Limit to 3 lines, truncate last line if needed + if (\count($lines) > 3) { + $lines = \array_slice($lines, 0, 3); + $lines[2] = mb_substr($lines[2], 0, -3) . '...'; + } + + return implode("\n", $lines); + } +} From 3d855f829f6e1d2d071171e92e8116779dd35421 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sat, 23 May 2026 18:38:18 -0500 Subject: [PATCH 027/132] feat(csv): add CSV import/export for OG tags (closes #12) ImportExportController with export() and import() actions: - Export: CSV with all OG + SEO fields, joined with article titles - Import: Upload CSV, match on content_type + content_id - Upsert logic: creates new or updates existing records - Reports created/updated/skipped counts - Toolbar buttons ready for integration Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- .../com_mokoog/language/en-GB/com_mokoog.ini | 6 + .../com_mokoog/language/en-US/com_mokoog.ini | 6 + .../src/Controller/ImportExportController.php | 188 ++++++++++++++++++ 3 files changed, 200 insertions(+) create mode 100644 src/packages/com_mokoog/src/Controller/ImportExportController.php diff --git a/src/packages/com_mokoog/language/en-GB/com_mokoog.ini b/src/packages/com_mokoog/language/en-GB/com_mokoog.ini index 87358c4..c7dca09 100644 --- a/src/packages/com_mokoog/language/en-GB/com_mokoog.ini +++ b/src/packages/com_mokoog/language/en-GB/com_mokoog.ini @@ -49,3 +49,9 @@ COM_MOKOOG_BATCH_FOUND="articles found without OG tags." COM_MOKOOG_BATCH_PROCESSED="processed" COM_MOKOOG_BATCH_COMPLETE="Batch generation complete!" COM_MOKOOG_BATCH_ERROR="Error:" + +COM_MOKOOG_TOOLBAR_EXPORT="Export CSV" +COM_MOKOOG_TOOLBAR_IMPORT="Import CSV" +COM_MOKOOG_IMPORT_NO_FILE="No CSV file was uploaded." +COM_MOKOOG_IMPORT_READ_ERROR="Could not read the uploaded CSV file." +COM_MOKOOG_IMPORT_RESULT="Import complete: %d created, %d updated, %d skipped." diff --git a/src/packages/com_mokoog/language/en-US/com_mokoog.ini b/src/packages/com_mokoog/language/en-US/com_mokoog.ini index 87358c4..c7dca09 100644 --- a/src/packages/com_mokoog/language/en-US/com_mokoog.ini +++ b/src/packages/com_mokoog/language/en-US/com_mokoog.ini @@ -49,3 +49,9 @@ COM_MOKOOG_BATCH_FOUND="articles found without OG tags." COM_MOKOOG_BATCH_PROCESSED="processed" COM_MOKOOG_BATCH_COMPLETE="Batch generation complete!" COM_MOKOOG_BATCH_ERROR="Error:" + +COM_MOKOOG_TOOLBAR_EXPORT="Export CSV" +COM_MOKOOG_TOOLBAR_IMPORT="Import CSV" +COM_MOKOOG_IMPORT_NO_FILE="No CSV file was uploaded." +COM_MOKOOG_IMPORT_READ_ERROR="Could not read the uploaded CSV file." +COM_MOKOOG_IMPORT_RESULT="Import complete: %d created, %d updated, %d skipped." diff --git a/src/packages/com_mokoog/src/Controller/ImportExportController.php b/src/packages/com_mokoog/src/Controller/ImportExportController.php new file mode 100644 index 0000000..aec4584 --- /dev/null +++ b/src/packages/com_mokoog/src/Controller/ImportExportController.php @@ -0,0 +1,188 @@ +<?php + +/** + * @package MokoOpenGraph + * @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\Factory; +use Joomla\CMS\Language\Text; +use Joomla\CMS\MVC\Controller\BaseController; +use Joomla\CMS\Session\Session; + +class ImportExportController extends BaseController +{ + /** + * Export all OG tags as CSV. + * + * @return void + */ + public function export(): void + { + Session::checkToken('get') || jexit(Text::_('JINVALID_TOKEN')); + + $app = Factory::getApplication(); + $db = Factory::getDbo(); + + // Join with #__content to get article titles for reference + $query = $db->getQuery(true) + ->select([ + $db->quoteName('t.content_type'), + $db->quoteName('t.content_id'), + 'COALESCE(' . $db->quoteName('c.title') . ', ' . $db->quote('') . ') AS ' . $db->quoteName('article_title'), + $db->quoteName('t.og_title'), + $db->quoteName('t.og_description'), + $db->quoteName('t.og_image'), + $db->quoteName('t.og_type'), + $db->quoteName('t.seo_title'), + $db->quoteName('t.meta_description'), + $db->quoteName('t.robots'), + $db->quoteName('t.canonical_url'), + ]) + ->from($db->quoteName('#__mokoog_tags', 't')) + ->leftJoin( + $db->quoteName('#__content', 'c') + . ' ON ' . $db->quoteName('t.content_type') . ' = ' . $db->quote('com_content') + . ' AND ' . $db->quoteName('t.content_id') . ' = ' . $db->quoteName('c.id') + ) + ->order($db->quoteName('t.content_type') . ', ' . $db->quoteName('t.content_id')); + + $db->setQuery($query); + $rows = $db->loadAssocList(); + + // Send CSV headers + $app->setHeader('Content-Type', 'text/csv; charset=utf-8'); + $app->setHeader('Content-Disposition', 'attachment; filename="mokoog_tags_export.csv"'); + $app->sendHeaders(); + + $output = fopen('php://output', 'w'); + + // Header row + fputcsv($output, [ + 'content_type', 'content_id', 'article_title', + 'og_title', 'og_description', 'og_image', 'og_type', + 'seo_title', 'meta_description', 'robots', 'canonical_url', + ]); + + foreach ($rows as $row) { + fputcsv($output, $row); + } + + fclose($output); + $app->close(); + } + + /** + * Import OG tags from uploaded CSV. + * + * @return void + */ + public function import(): void + { + Session::checkToken() || jexit(Text::_('JINVALID_TOKEN')); + + $app = Factory::getApplication(); + $input = $app->getInput(); + $files = $input->files->get('jform', [], 'array'); + + if (empty($files['csv_file']['tmp_name'])) { + $app->enqueueMessage(Text::_('COM_MOKOOG_IMPORT_NO_FILE'), 'error'); + $app->redirect('index.php?option=com_mokoog&view=tags'); + + return; + } + + $tmpFile = $files['csv_file']['tmp_name']; + $handle = fopen($tmpFile, 'r'); + + if (!$handle) { + $app->enqueueMessage(Text::_('COM_MOKOOG_IMPORT_READ_ERROR'), 'error'); + $app->redirect('index.php?option=com_mokoog&view=tags'); + + return; + } + + $db = Factory::getDbo(); + $header = fgetcsv($handle); + $created = 0; + $updated = 0; + $skipped = 0; + $now = Factory::getDate()->toSql(); + + while (($row = fgetcsv($handle)) !== false) { + if (\count($row) < 7) { + $skipped++; + + continue; + } + + $contentType = trim($row[0]); + $contentId = (int) $row[1]; + // $row[2] = article_title (informational, skip) + $ogTitle = trim($row[3] ?? ''); + $ogDescription = trim($row[4] ?? ''); + $ogImage = trim($row[5] ?? ''); + $ogType = trim($row[6] ?? 'article'); + $seoTitle = trim($row[7] ?? ''); + $metaDesc = trim($row[8] ?? ''); + $robots = trim($row[9] ?? ''); + $canonicalUrl = trim($row[10] ?? ''); + + if (empty($contentType) || $contentId <= 0) { + $skipped++; + + continue; + } + + // Check for existing record + $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); + + $db->setQuery($query); + $existingId = $db->loadResult(); + + $record = (object) [ + 'content_type' => $contentType, + 'content_id' => $contentId, + 'og_title' => $ogTitle, + 'og_description' => $ogDescription, + 'og_image' => $ogImage, + 'og_type' => $ogType, + 'seo_title' => $seoTitle, + 'meta_description' => $metaDesc, + 'robots' => $robots, + 'canonical_url' => $canonicalUrl, + 'published' => 1, + 'modified' => $now, + ]; + + if ($existingId) { + $record->id = $existingId; + $db->updateObject('#__mokoog_tags', $record, 'id'); + $updated++; + } else { + $record->created = $now; + $db->insertObject('#__mokoog_tags', $record); + $created++; + } + } + + fclose($handle); + + $app->enqueueMessage( + Text::sprintf('COM_MOKOOG_IMPORT_RESULT', $created, $updated, $skipped), + 'success' + ); + $app->redirect('index.php?option=com_mokoog&view=tags'); + } +} From 78fbe252cb8b2ee024a44c7abcf1a6009d35a670 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sat, 23 May 2026 18:17:32 -0500 Subject: [PATCH 028/132] feat(preview): live social sharing preview in article editor (closes #3) - Facebook and Twitter/X card previews update in real-time - Renders below the OG fieldset in article/menu editors - Reads og_title, og_description, og_image with fallback to article title and meta description - CSS mockups of each platform's card layout - Registered via joomla.asset.json Web Asset Manager - Safe DOM construction (no innerHTML with user data) - MutationObserver watches media field for image changes Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- .../plg_content_mokoog/media/css/preview.css | 104 +++++++++++ .../media/joomla.asset.json | 20 +++ .../plg_content_mokoog/media/js/preview.js | 170 ++++++++++++++++++ src/packages/plg_content_mokoog/mokoog.xml | 6 + .../src/Extension/MokoOGContent.php | 6 + 5 files changed, 306 insertions(+) create mode 100644 src/packages/plg_content_mokoog/media/css/preview.css create mode 100644 src/packages/plg_content_mokoog/media/joomla.asset.json create mode 100644 src/packages/plg_content_mokoog/media/js/preview.js diff --git a/src/packages/plg_content_mokoog/media/css/preview.css b/src/packages/plg_content_mokoog/media/css/preview.css new file mode 100644 index 0000000..b5d74a9 --- /dev/null +++ b/src/packages/plg_content_mokoog/media/css/preview.css @@ -0,0 +1,104 @@ +/** + * @package MokoOpenGraph + * @subpackage plg_content_mokoog + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GPL-3.0-or-later + */ + +.mokoog-preview-wrapper { + margin: 15px 0; + padding: 15px; + background: #f8f9fa; + border-radius: 8px; + border: 1px solid #dee2e6; +} + +.mokoog-preview-heading { + margin: 0 0 12px; + font-size: 14px; + color: #666; +} + +.mokoog-platform-label { + display: block; + color: #999; + text-transform: uppercase; + font-size: 11px; + font-weight: 600; + margin-top: 16px; + margin-bottom: 4px; +} + +.mokoog-platform-label:first-of-type { + margin-top: 0; +} + +.mokoog-card { + overflow: hidden; + max-width: 500px; + background: #fff; +} + +.mokoog-card-fb { + border: 1px solid #ddd; + border-radius: 3px; +} + +.mokoog-card-tw { + border: 1px solid #cfd9de; + border-radius: 16px; +} + +.mokoog-card-img { + height: 260px; + background: #e4e6eb center / cover no-repeat; +} + +.mokoog-card-body { + padding: 10px 12px; + border-top: 1px solid #ddd; +} + +.mokoog-card-tw .mokoog-card-body { + border-top-color: #cfd9de; +} + +.mokoog-card-domain { + font-size: 11px; + color: #65676b; + text-transform: uppercase; +} + +.mokoog-card-tw .mokoog-card-domain { + font-size: 13px; + text-transform: none; + margin-top: 4px; +} + +.mokoog-card-title { + font-size: 16px; + font-weight: 600; + color: #1d2129; + margin: 3px 0 2px; + line-height: 1.3; +} + +.mokoog-card-tw .mokoog-card-title { + font-size: 15px; + font-weight: 700; + color: #0f1419; +} + +.mokoog-card-desc { + font-size: 14px; + color: #65676b; + line-height: 1.4; + max-height: 2.8em; + overflow: hidden; +} + +.mokoog-card-tw .mokoog-card-desc { + font-size: 15px; + color: #536471; + margin-top: 2px; +} diff --git a/src/packages/plg_content_mokoog/media/joomla.asset.json b/src/packages/plg_content_mokoog/media/joomla.asset.json new file mode 100644 index 0000000..340aa7a --- /dev/null +++ b/src/packages/plg_content_mokoog/media/joomla.asset.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://developer.joomla.org/schemas/json-schema/web_assets.json", + "name": "plg_content_mokoog", + "version": "01.00.00", + "description": "MokoOpenGraph Content Plugin Assets", + "license": "GPL-3.0-or-later", + "assets": [ + { + "name": "plg_content_mokoog.preview", + "type": "style", + "uri": "plg_content_mokoog/css/preview.css" + }, + { + "name": "plg_content_mokoog.preview", + "type": "script", + "uri": "plg_content_mokoog/js/preview.js", + "dependencies": ["core"] + } + ] +} diff --git a/src/packages/plg_content_mokoog/media/js/preview.js b/src/packages/plg_content_mokoog/media/js/preview.js new file mode 100644 index 0000000..62e8925 --- /dev/null +++ b/src/packages/plg_content_mokoog/media/js/preview.js @@ -0,0 +1,170 @@ +/** + * @package MokoOpenGraph + * @subpackage plg_content_mokoog + * @author Moko Consulting <hello@mokoconsulting.tech> + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GPL-3.0-or-later + * + * Live social sharing preview for article/menu item editor. + */ +document.addEventListener('DOMContentLoaded', function () { + 'use strict'; + + var fields = { + ogTitle: document.getElementById('jform_mokoog_og_title'), + ogDesc: document.getElementById('jform_mokoog_og_description'), + ogImage: document.getElementById('jform_mokoog_og_image'), + articleTitle: document.getElementById('jform_title'), + metaDesc: document.getElementById('jform_metadesc') + }; + + // Find the mokoog fieldset and insert preview after it + var fieldset = document.querySelector('[data-showon-id="mokoog"]') || + document.getElementById('attrib-mokoog') || + document.querySelector('fieldset.mokoog') || + document.querySelector('[id*="mokoog"]'); + + if (!fieldset) { + return; + } + + // Build preview DOM safely (no innerHTML with user data) + var preview = document.createElement('div'); + preview.id = 'mokoog-preview'; + + var wrapper = document.createElement('div'); + wrapper.className = 'mokoog-preview-wrapper'; + + var heading = document.createElement('h4'); + heading.className = 'mokoog-preview-heading'; + heading.textContent = 'Social Sharing Preview'; + wrapper.appendChild(heading); + + // Facebook preview card + var fbLabel = document.createElement('small'); + fbLabel.className = 'mokoog-platform-label'; + fbLabel.textContent = 'Facebook'; + wrapper.appendChild(fbLabel); + + var fbCard = document.createElement('div'); + fbCard.className = 'mokoog-card mokoog-card-fb'; + + var fbImg = document.createElement('div'); + fbImg.id = 'mokoog-fb-img'; + fbImg.className = 'mokoog-card-img'; + fbCard.appendChild(fbImg); + + var fbBody = document.createElement('div'); + fbBody.className = 'mokoog-card-body'; + + var fbDomain = document.createElement('div'); + fbDomain.id = 'mokoog-fb-domain'; + fbDomain.className = 'mokoog-card-domain'; + fbBody.appendChild(fbDomain); + + var fbTitle = document.createElement('div'); + fbTitle.id = 'mokoog-fb-title'; + fbTitle.className = 'mokoog-card-title'; + fbBody.appendChild(fbTitle); + + var fbDesc = document.createElement('div'); + fbDesc.id = 'mokoog-fb-desc'; + fbDesc.className = 'mokoog-card-desc'; + fbBody.appendChild(fbDesc); + + fbCard.appendChild(fbBody); + wrapper.appendChild(fbCard); + + // Twitter preview card + var twLabel = document.createElement('small'); + twLabel.className = 'mokoog-platform-label'; + twLabel.textContent = 'Twitter / X'; + wrapper.appendChild(twLabel); + + var twCard = document.createElement('div'); + twCard.className = 'mokoog-card mokoog-card-tw'; + + var twImg = document.createElement('div'); + twImg.id = 'mokoog-tw-img'; + twImg.className = 'mokoog-card-img'; + twCard.appendChild(twImg); + + var twBody = document.createElement('div'); + twBody.className = 'mokoog-card-body'; + + var twTitle = document.createElement('div'); + twTitle.id = 'mokoog-tw-title'; + twTitle.className = 'mokoog-card-title'; + twBody.appendChild(twTitle); + + var twDesc = document.createElement('div'); + twDesc.id = 'mokoog-tw-desc'; + twDesc.className = 'mokoog-card-desc'; + twBody.appendChild(twDesc); + + var twDomain = document.createElement('div'); + twDomain.id = 'mokoog-tw-domain'; + twDomain.className = 'mokoog-card-domain'; + twBody.appendChild(twDomain); + + twCard.appendChild(twBody); + wrapper.appendChild(twCard); + + preview.appendChild(wrapper); + fieldset.parentNode.insertBefore(preview, fieldset.nextSibling); + + var domain = window.location.hostname; + + function updatePreview() { + var title = (fields.ogTitle && fields.ogTitle.value) || + (fields.articleTitle && fields.articleTitle.value) || 'Page Title'; + var desc = (fields.ogDesc && fields.ogDesc.value) || + (fields.metaDesc && fields.metaDesc.value) || 'Page description will appear here...'; + var img = ''; + + if (fields.ogImage) { + img = fields.ogImage.value; + } + + if (title.length > 65) title = title.substring(0, 62) + '...'; + if (desc.length > 160) desc = desc.substring(0, 157) + '...'; + + // Facebook + document.getElementById('mokoog-fb-title').textContent = title; + document.getElementById('mokoog-fb-desc').textContent = desc; + document.getElementById('mokoog-fb-domain').textContent = domain; + var fbImgEl = document.getElementById('mokoog-fb-img'); + if (img) { + fbImgEl.style.backgroundImage = 'url(' + encodeURI(img) + ')'; + fbImgEl.style.display = ''; + } else { + fbImgEl.style.display = 'none'; + } + + // Twitter + document.getElementById('mokoog-tw-title').textContent = title; + document.getElementById('mokoog-tw-desc').textContent = desc; + document.getElementById('mokoog-tw-domain').textContent = domain; + var twImgEl = document.getElementById('mokoog-tw-img'); + if (img) { + twImgEl.style.backgroundImage = 'url(' + encodeURI(img) + ')'; + twImgEl.style.display = ''; + } else { + twImgEl.style.display = 'none'; + } + } + + Object.values(fields).forEach(function (el) { + if (el) { + el.addEventListener('input', updatePreview); + el.addEventListener('change', updatePreview); + } + }); + + if (fields.ogImage) { + var observer = new MutationObserver(updatePreview); + observer.observe(fields.ogImage, { attributes: true, attributeFilter: ['value'] }); + } + + updatePreview(); +}); diff --git a/src/packages/plg_content_mokoog/mokoog.xml b/src/packages/plg_content_mokoog/mokoog.xml index 651e9ba..bb64e27 100644 --- a/src/packages/plg_content_mokoog/mokoog.xml +++ b/src/packages/plg_content_mokoog/mokoog.xml @@ -27,6 +27,12 @@ <folder>language</folder> </files> + <media destination="plg_content_mokoog" folder="media"> + <filename>joomla.asset.json</filename> + <folder>js</folder> + <folder>css</folder> + </media> + <languages> <language tag="en-GB">language/en-GB/plg_content_mokoog.ini</language> <language tag="en-GB">language/en-GB/plg_content_mokoog.sys.ini</language> diff --git a/src/packages/plg_content_mokoog/src/Extension/MokoOGContent.php b/src/packages/plg_content_mokoog/src/Extension/MokoOGContent.php index afab878..d89afb4 100644 --- a/src/packages/plg_content_mokoog/src/Extension/MokoOGContent.php +++ b/src/packages/plg_content_mokoog/src/Extension/MokoOGContent.php @@ -72,6 +72,12 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface Form::addFormPath($formPath); $form->loadFile('mokoog', false); + // Load live preview assets + $wa = $this->getApplication()->getDocument()->getWebAssetManager(); + $wa->getRegistry()->addRegistryFile('media/plg_content_mokoog/joomla.asset.json'); + $wa->useStyle('plg_content_mokoog.preview'); + $wa->useScript('plg_content_mokoog.preview'); + // If editing an existing item, load saved OG data $id = 0; From 400d3759e5242da0c274f01a930323ae5c84eb00 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sat, 23 May 2026 22:43:18 -0500 Subject: [PATCH 029/132] feat(api): add Joomla Web Services API for OG tags (closes #27) New sub-extension: plg_webservices_mokoog - Registers REST API routes via onBeforeApiRoute - CRUD endpoints at /api/index.php/v1/mokoog/tags - Lookup endpoint at /api/index.php/v1/mokoog/lookup/:type/:id - SubscriberInterface pattern with DI container Component API layer (com_mokoog/api/): - TagsController extending ApiController for CRUD operations - JsonapiView with whitelisted fields for JSON:API output - TagModel (AdminModel) for single-item operations Package updates: - pkg_mokoog.xml includes plg_webservices_mokoog - Install script auto-enables webservices plugin - Component manifest declares api/ directory Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- .../api/src/Controller/TagsController.php | 68 ++++++++++++++++ .../api/src/View/Tags/JsonapiView.php | 66 +++++++++++++++ src/packages/com_mokoog/mokoog.xml | 6 ++ .../com_mokoog/src/Model/TagModel.php | 68 ++++++++++++++++ .../language/en-GB/plg_webservices_mokoog.ini | 5 ++ .../en-GB/plg_webservices_mokoog.sys.ini | 6 ++ .../language/en-US/plg_webservices_mokoog.ini | 5 ++ .../en-US/plg_webservices_mokoog.sys.ini | 6 ++ .../plg_webservices_mokoog/mokoog.php | 19 +++++ .../plg_webservices_mokoog/mokoog.xml | 33 ++++++++ .../services/provider.php | 44 ++++++++++ .../src/Extension/MokoOGWebServices.php | 81 +++++++++++++++++++ src/pkg_mokoog.xml | 1 + src/script.php | 11 +++ 14 files changed, 419 insertions(+) create mode 100644 src/packages/com_mokoog/api/src/Controller/TagsController.php create mode 100644 src/packages/com_mokoog/api/src/View/Tags/JsonapiView.php create mode 100644 src/packages/com_mokoog/src/Model/TagModel.php create mode 100644 src/packages/plg_webservices_mokoog/language/en-GB/plg_webservices_mokoog.ini create mode 100644 src/packages/plg_webservices_mokoog/language/en-GB/plg_webservices_mokoog.sys.ini create mode 100644 src/packages/plg_webservices_mokoog/language/en-US/plg_webservices_mokoog.ini create mode 100644 src/packages/plg_webservices_mokoog/language/en-US/plg_webservices_mokoog.sys.ini create mode 100644 src/packages/plg_webservices_mokoog/mokoog.php create mode 100644 src/packages/plg_webservices_mokoog/mokoog.xml create mode 100644 src/packages/plg_webservices_mokoog/services/provider.php create mode 100644 src/packages/plg_webservices_mokoog/src/Extension/MokoOGWebServices.php diff --git a/src/packages/com_mokoog/api/src/Controller/TagsController.php b/src/packages/com_mokoog/api/src/Controller/TagsController.php new file mode 100644 index 0000000..e0d895f --- /dev/null +++ b/src/packages/com_mokoog/api/src/Controller/TagsController.php @@ -0,0 +1,68 @@ +<?php + +/** + * @package MokoOpenGraph + * @subpackage com_mokoog.api + * @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\Api\Controller; + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\MVC\Controller\ApiController; + +class TagsController extends ApiController +{ + /** + * The content type for JSON:API output. + * + * @var string + */ + protected $contentType = 'mokoogtags'; + + /** + * The default view for the API. + * + * @var string + */ + protected $default_view = 'tags'; + + /** + * Lookup an OG tag by content_type and content_id. + * + * GET /api/index.php/v1/mokoog/lookup/:content_type/:content_id + * + * @return static + */ + public function lookup(): static + { + $contentType = $this->input->getString('content_type', ''); + $contentId = $this->input->getInt('content_id', 0); + + if (empty($contentType) || $contentId <= 0) { + throw new \RuntimeException('content_type and content_id are required', 400); + } + + $db = Factory::getDbo(); + $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); + + $db->setQuery($query); + $id = $db->loadResult(); + + if (!$id) { + throw new \RuntimeException('OG tag not found for ' . $contentType . ':' . $contentId, 404); + } + + $this->input->set('id', $id); + + return $this->displayItem(); + } +} diff --git a/src/packages/com_mokoog/api/src/View/Tags/JsonapiView.php b/src/packages/com_mokoog/api/src/View/Tags/JsonapiView.php new file mode 100644 index 0000000..c103df2 --- /dev/null +++ b/src/packages/com_mokoog/api/src/View/Tags/JsonapiView.php @@ -0,0 +1,66 @@ +<?php + +/** + * @package MokoOpenGraph + * @subpackage com_mokoog.api + * @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\Api\View\Tags; + +defined('_JEXEC') or die; + +use Joomla\CMS\MVC\View\JsonApiView as BaseApiView; + +class JsonapiView extends BaseApiView +{ + /** + * The fields to render in the API response. + * + * Whitelist of fields from #__mokoog_tags that are safe to expose. + * + * @var array + */ + protected $fieldsToRenderItem = [ + 'id', + 'content_type', + 'content_id', + 'og_title', + 'og_description', + 'og_image', + 'og_type', + 'seo_title', + 'meta_description', + 'robots', + 'canonical_url', + 'language', + 'published', + 'created', + 'modified', + ]; + + /** + * The fields to render in list responses. + * + * @var array + */ + protected $fieldsToRenderList = [ + 'id', + 'content_type', + 'content_id', + 'og_title', + 'og_description', + 'og_image', + 'og_type', + 'seo_title', + 'meta_description', + 'robots', + 'canonical_url', + 'language', + 'published', + 'created', + 'modified', + ]; +} diff --git a/src/packages/com_mokoog/mokoog.xml b/src/packages/com_mokoog/mokoog.xml index aa2ad47..f5baeb3 100644 --- a/src/packages/com_mokoog/mokoog.xml +++ b/src/packages/com_mokoog/mokoog.xml @@ -69,4 +69,10 @@ <menu link="option=com_mokoog&view=tags">COM_MOKOOG_SUBMENU_TAGS</menu> </submenu> </administration> + + <api> + <files folder="api"> + <folder>src</folder> + </files> + </api> </extension> diff --git a/src/packages/com_mokoog/src/Model/TagModel.php b/src/packages/com_mokoog/src/Model/TagModel.php new file mode 100644 index 0000000..62d14b9 --- /dev/null +++ b/src/packages/com_mokoog/src/Model/TagModel.php @@ -0,0 +1,68 @@ +<?php + +/** + * @package MokoOpenGraph + * @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\Model; + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\MVC\Model\AdminModel; + +class TagModel extends AdminModel +{ + /** + * Get the form for the item. + * + * @param array $data Form data + * @param bool $loadData Load data from state + * + * @return \Joomla\CMS\Form\Form|false + */ + public function getForm($data = [], $loadData = true) + { + $form = $this->loadForm( + 'com_mokoog.tag', + 'tag', + ['control' => 'jform', 'load_data' => $loadData] + ); + + return $form ?: false; + } + + /** + * Load the form data. + * + * @return object + */ + protected function loadFormData(): object + { + $data = Factory::getApplication()->getUserState('com_mokoog.edit.tag.data', []); + + if (empty($data)) { + $data = $this->getItem(); + } + + return $data; + } + + /** + * Get the table class name. + * + * @param string $name Table name + * @param string $prefix Table prefix + * @param array $options Table options + * + * @return \Joomla\CMS\Table\Table + */ + public function getTable($name = 'Tag', $prefix = 'Administrator', $options = []) + { + return parent::getTable($name, $prefix, $options); + } +} diff --git a/src/packages/plg_webservices_mokoog/language/en-GB/plg_webservices_mokoog.ini b/src/packages/plg_webservices_mokoog/language/en-GB/plg_webservices_mokoog.ini new file mode 100644 index 0000000..e397923 --- /dev/null +++ b/src/packages/plg_webservices_mokoog/language/en-GB/plg_webservices_mokoog.ini @@ -0,0 +1,5 @@ +; MokoOpenGraph - Web Services Plugin Language File +; Copyright (C) 2026 Moko Consulting. All rights reserved. +; License: GPL-3.0-or-later + +PLG_WEBSERVICES_MOKOOG="Web Services - MokoOpenGraph" diff --git a/src/packages/plg_webservices_mokoog/language/en-GB/plg_webservices_mokoog.sys.ini b/src/packages/plg_webservices_mokoog/language/en-GB/plg_webservices_mokoog.sys.ini new file mode 100644 index 0000000..903c909 --- /dev/null +++ b/src/packages/plg_webservices_mokoog/language/en-GB/plg_webservices_mokoog.sys.ini @@ -0,0 +1,6 @@ +; MokoOpenGraph - Web Services Plugin System Language File +; Copyright (C) 2026 Moko Consulting. All rights reserved. +; License: GPL-3.0-or-later + +PLG_WEBSERVICES_MOKOOG="Web Services - MokoOpenGraph" +PLG_WEBSERVICES_MOKOOG_DESCRIPTION="Exposes MokoOpenGraph OG tag data via Joomla's REST API at /api/index.php/v1/mokoog/tags." diff --git a/src/packages/plg_webservices_mokoog/language/en-US/plg_webservices_mokoog.ini b/src/packages/plg_webservices_mokoog/language/en-US/plg_webservices_mokoog.ini new file mode 100644 index 0000000..e397923 --- /dev/null +++ b/src/packages/plg_webservices_mokoog/language/en-US/plg_webservices_mokoog.ini @@ -0,0 +1,5 @@ +; MokoOpenGraph - Web Services Plugin Language File +; Copyright (C) 2026 Moko Consulting. All rights reserved. +; License: GPL-3.0-or-later + +PLG_WEBSERVICES_MOKOOG="Web Services - MokoOpenGraph" diff --git a/src/packages/plg_webservices_mokoog/language/en-US/plg_webservices_mokoog.sys.ini b/src/packages/plg_webservices_mokoog/language/en-US/plg_webservices_mokoog.sys.ini new file mode 100644 index 0000000..903c909 --- /dev/null +++ b/src/packages/plg_webservices_mokoog/language/en-US/plg_webservices_mokoog.sys.ini @@ -0,0 +1,6 @@ +; MokoOpenGraph - Web Services Plugin System Language File +; Copyright (C) 2026 Moko Consulting. All rights reserved. +; License: GPL-3.0-or-later + +PLG_WEBSERVICES_MOKOOG="Web Services - MokoOpenGraph" +PLG_WEBSERVICES_MOKOOG_DESCRIPTION="Exposes MokoOpenGraph OG tag data via Joomla's REST API at /api/index.php/v1/mokoog/tags." diff --git a/src/packages/plg_webservices_mokoog/mokoog.php b/src/packages/plg_webservices_mokoog/mokoog.php new file mode 100644 index 0000000..899de00 --- /dev/null +++ b/src/packages/plg_webservices_mokoog/mokoog.php @@ -0,0 +1,19 @@ +<?php + +/** + * @package MokoOpenGraph + * @subpackage plg_webservices_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 + * + * Legacy entry point — not executed under DI container. + */ + +defined('_JEXEC') or die; + +use Joomla\CMS\Plugin\CMSPlugin; + +class PlgWebservicesMokoog extends CMSPlugin +{ +} diff --git a/src/packages/plg_webservices_mokoog/mokoog.xml b/src/packages/plg_webservices_mokoog/mokoog.xml new file mode 100644 index 0000000..5f862ab --- /dev/null +++ b/src/packages/plg_webservices_mokoog/mokoog.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + * @package MokoOpenGraph + * @subpackage plg_webservices_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 + --> +<extension type="plugin" group="webservices" method="upgrade"> + <name>Web Services - MokoOpenGraph</name> + <version>01.00.00</version> + <creationDate>2026-05-23</creationDate> + <author>Moko Consulting</author> + <authorEmail>hello@mokoconsulting.tech</authorEmail> + <authorUrl>https://mokoconsulting.tech</authorUrl> + <copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright> + <license>GPL-3.0-or-later</license> + <description>PLG_WEBSERVICES_MOKOOG_DESCRIPTION</description> + + <namespace path="src">Joomla\Plugin\WebServices\MokoOG</namespace> + + <files> + <filename plugin="mokoog">mokoog.php</filename> + <folder>src</folder> + <folder>services</folder> + <folder>language</folder> + </files> + + <languages> + <language tag="en-GB">language/en-GB/plg_webservices_mokoog.ini</language> + <language tag="en-GB">language/en-GB/plg_webservices_mokoog.sys.ini</language> + </languages> +</extension> diff --git a/src/packages/plg_webservices_mokoog/services/provider.php b/src/packages/plg_webservices_mokoog/services/provider.php new file mode 100644 index 0000000..2aa760d --- /dev/null +++ b/src/packages/plg_webservices_mokoog/services/provider.php @@ -0,0 +1,44 @@ +<?php + +/** + * @package MokoOpenGraph + * @subpackage plg_webservices_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 + */ + +defined('_JEXEC') or die; + +use Joomla\CMS\Extension\PluginInterface; +use Joomla\CMS\Factory; +use Joomla\CMS\Plugin\PluginHelper; +use Joomla\DI\Container; +use Joomla\DI\ServiceProviderInterface; +use Joomla\Event\DispatcherInterface; +use Joomla\Plugin\WebServices\MokoOG\Extension\MokoOGWebServices; + +return new class () implements ServiceProviderInterface { + /** + * Register the service provider. + * + * @param Container $container The DI container + * + * @return void + */ + public function register(Container $container): void + { + $container->set( + PluginInterface::class, + function (Container $container) { + $plugin = new MokoOGWebServices( + $container->get(DispatcherInterface::class), + (array) PluginHelper::getPlugin('webservices', 'mokoog') + ); + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/src/packages/plg_webservices_mokoog/src/Extension/MokoOGWebServices.php b/src/packages/plg_webservices_mokoog/src/Extension/MokoOGWebServices.php new file mode 100644 index 0000000..887e3b8 --- /dev/null +++ b/src/packages/plg_webservices_mokoog/src/Extension/MokoOGWebServices.php @@ -0,0 +1,81 @@ +<?php + +/** + * @package MokoOpenGraph + * @subpackage plg_webservices_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\Plugin\WebServices\MokoOG\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\CMS\Router\ApiRouter; +use Joomla\Event\Event; +use Joomla\Event\SubscriberInterface; +use Joomla\Router\Route; + +final class MokoOGWebServices extends CMSPlugin implements SubscriberInterface +{ + /** + * @var bool + */ + protected $autoloadLanguage = true; + + /** + * Returns the events this plugin subscribes to. + * + * @return array<string, string> + */ + public static function getSubscribedEvents(): array + { + return [ + 'onBeforeApiRoute' => 'onBeforeApiRoute', + ]; + } + + /** + * Register API routes for MokoOpenGraph. + * + * Endpoints: + * GET /api/index.php/v1/mokoog/tags - List all OG tags + * GET /api/index.php/v1/mokoog/tags/:id - Get single OG tag + * POST /api/index.php/v1/mokoog/tags - Create OG tag + * PATCH /api/index.php/v1/mokoog/tags/:id - Update OG tag + * DELETE /api/index.php/v1/mokoog/tags/:id - Delete OG tag + * + * @param Event $event The event object + * + * @return void + */ + public function onBeforeApiRoute(Event $event): void + { + [$router] = array_values($event->getArguments()); + + $defaults = [ + 'component' => 'com_mokoog', + 'public' => false, + ]; + + // CRUD routes for OG tags + $router->createCRUDRoutes( + 'v1/mokoog/tags', + 'tags', + $defaults + ); + + // GET by content type + content ID (lookup endpoint) + $router->addRoute( + new Route( + ['GET'], + 'v1/mokoog/lookup/:content_type/:content_id', + 'tags.lookup', + ['content_type' => '[a-z_.]+', 'content_id' => '(\d+)'], + $defaults + ) + ); + } +} diff --git a/src/pkg_mokoog.xml b/src/pkg_mokoog.xml index 355f556..783c779 100644 --- a/src/pkg_mokoog.xml +++ b/src/pkg_mokoog.xml @@ -23,6 +23,7 @@ <file type="component" id="com_mokoog">com_mokoog.zip</file> <file type="plugin" id="mokoog" group="system">plg_system_mokoog.zip</file> <file type="plugin" id="mokoog" group="content">plg_content_mokoog.zip</file> + <file type="plugin" id="mokoog" group="webservices">plg_webservices_mokoog.zip</file> </files> <languages> diff --git a/src/script.php b/src/script.php index c5a505f..0d173ba 100644 --- a/src/script.php +++ b/src/script.php @@ -73,6 +73,17 @@ class Pkg_MokoOGInstallerScript $db->setQuery($query); $db->execute(); + + // Enable the webservices plugin automatically + $query = $db->getQuery(true) + ->update($db->quoteName('#__extensions')) + ->set($db->quoteName('enabled') . ' = 1') + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('webservices')) + ->where($db->quoteName('element') . ' = ' . $db->quote('mokoog')); + + $db->setQuery($query); + $db->execute(); } } } From f7826ca60a136cc54058b8c913ebad3663109d34 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" <gitea-actions[bot]@mokoconsulting.tech> Date: Sun, 24 May 2026 03:45:20 +0000 Subject: [PATCH 030/132] chore: update updates.xml (development: 01.00.00-dev) [skip ci] --- updates.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/updates.xml b/updates.xml index 3c7fe4e..fc93688 100644 --- a/updates.xml +++ b/updates.xml @@ -13,7 +13,7 @@ <downloads> <downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoOpenGraph/releases/download/development/pkg_mokoog-01.00.00-dev.zip</downloadurl> </downloads> - <sha256>3a910f6c075d2cddbda905c175e666b9cbf1ae79e465132f18293ac3be15266e</sha256> + <sha256>6a58f2ccbccf6f383394e31af6094993b9f5a6a4e799ca1c3ab9bb9d8195f168</sha256> <tags><tag>development</tag></tags> <maintainer>Moko Consulting</maintainer> <maintainerurl>https://mokoconsulting.tech</maintainerurl> From 0509980b5033503c5c31abaef7f9575fcb557ac6 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sat, 23 May 2026 23:22:02 -0500 Subject: [PATCH 031/132] fix: production readiness improvements for admin panel and installer Add search/filter tools to tags list view, fix API content type naming, build proper frontend URLs for social debugger links, and auto-enable the content plugin on package install. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- .../api/src/Controller/TagsController.php | 2 +- .../com_mokoog/src/View/Tags/HtmlView.php | 23 ++++++++++++++++--- src/packages/com_mokoog/tmpl/tags/default.php | 13 +++++++++-- src/script.php | 11 +++++++++ 4 files changed, 43 insertions(+), 6 deletions(-) diff --git a/src/packages/com_mokoog/api/src/Controller/TagsController.php b/src/packages/com_mokoog/api/src/Controller/TagsController.php index e0d895f..28457f4 100644 --- a/src/packages/com_mokoog/api/src/Controller/TagsController.php +++ b/src/packages/com_mokoog/api/src/Controller/TagsController.php @@ -22,7 +22,7 @@ class TagsController extends ApiController * * @var string */ - protected $contentType = 'mokoogtags'; + protected $contentType = 'tags'; /** * The default view for the API. diff --git a/src/packages/com_mokoog/src/View/Tags/HtmlView.php b/src/packages/com_mokoog/src/View/Tags/HtmlView.php index 0a189b4..cf93aff 100644 --- a/src/packages/com_mokoog/src/View/Tags/HtmlView.php +++ b/src/packages/com_mokoog/src/View/Tags/HtmlView.php @@ -39,6 +39,20 @@ class HtmlView extends BaseHtmlView */ protected $state; + /** + * The filter form. + * + * @var \Joomla\CMS\Form\Form|null + */ + public $filterForm; + + /** + * The active filters. + * + * @var array + */ + public $activeFilters = []; + /** * Display the view. * @@ -48,9 +62,11 @@ class HtmlView extends BaseHtmlView */ public function display($tpl = null): void { - $this->items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); $this->addToolbar(); @@ -66,6 +82,7 @@ class HtmlView extends BaseHtmlView { ToolbarHelper::title(Text::_('COM_MOKOOG_TAGS_TITLE'), 'bookmark'); ToolbarHelper::custom('batch.generate', 'refresh', '', 'COM_MOKOOG_TOOLBAR_BATCH_GENERATE', false); + ToolbarHelper::custom('importexport.export', 'download', '', 'COM_MOKOOG_TOOLBAR_EXPORT', false); ToolbarHelper::deleteList('JGLOBAL_CONFIRM_DELETE', 'tags.delete'); ToolbarHelper::preferences('com_mokoog'); } diff --git a/src/packages/com_mokoog/tmpl/tags/default.php b/src/packages/com_mokoog/tmpl/tags/default.php index 54c5940..094de26 100644 --- a/src/packages/com_mokoog/tmpl/tags/default.php +++ b/src/packages/com_mokoog/tmpl/tags/default.php @@ -25,6 +25,7 @@ $token = Session::getFormToken(); <div class="row"> <div class="col-md-12"> <div id="j-main-container" class="j-main-container"> + <?php echo LayoutHelper::render('joomla.searchtools.default', ['view' => $this]); ?> <?php if (empty($this->items)) : ?> <div class="alert alert-info"> @@ -121,8 +122,16 @@ $token = Session::getFormToken(); </td> <td class="mokoog-debug-links"> <?php - // Build a placeholder URL for debugger links - $debugUrl = Uri::root(); + // Build frontend URL for this content item + if ($item->content_type === 'com_content') { + $debugUrl = Uri::root() . 'index.php?option=com_content&view=article&id=' . (int) $item->content_id; + } elseif ($item->content_type === 'menu') { + $debugUrl = Uri::root() . 'index.php?Itemid=' . (int) $item->content_id; + } elseif ($item->content_type === 'com_content.category') { + $debugUrl = Uri::root() . 'index.php?option=com_content&view=category&id=' . (int) $item->content_id; + } else { + $debugUrl = Uri::root(); + } ?> <a href="https://developers.facebook.com/tools/debug/?q=<?php echo urlencode($debugUrl); ?>" target="_blank" rel="noopener" title="Facebook Debugger" class="btn btn-sm btn-outline-primary">FB</a> <a href="https://www.linkedin.com/post-inspector/inspect/<?php echo urlencode($debugUrl); ?>" target="_blank" rel="noopener" title="LinkedIn Inspector" class="btn btn-sm btn-outline-info">LI</a> diff --git a/src/script.php b/src/script.php index 0d173ba..3f76f33 100644 --- a/src/script.php +++ b/src/script.php @@ -74,6 +74,17 @@ class Pkg_MokoOGInstallerScript $db->setQuery($query); $db->execute(); + // Enable the content plugin automatically + $query = $db->getQuery(true) + ->update($db->quoteName('#__extensions')) + ->set($db->quoteName('enabled') . ' = 1') + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('content')) + ->where($db->quoteName('element') . ' = ' . $db->quote('mokoog')); + + $db->setQuery($query); + $db->execute(); + // Enable the webservices plugin automatically $query = $db->getQuery(true) ->update($db->quoteName('#__extensions')) From fc148084a19822393219232ca707ecc525631156 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" <gitea-actions[bot]@mokoconsulting.tech> Date: Sun, 24 May 2026 04:37:34 +0000 Subject: [PATCH 032/132] =?UTF-8?q?chore(version):=20bump=2001.00.00=20?= =?UTF-8?q?=E2=86=92=2001.00.00-dev=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/packages/com_mokoog/mokoog.xml | 2 +- src/packages/plg_content_mokoog/mokoog.xml | 2 +- src/packages/plg_system_mokoog/mokoog.xml | 2 +- src/packages/plg_webservices_mokoog/mokoog.xml | 2 +- src/pkg_mokoog.xml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/packages/com_mokoog/mokoog.xml b/src/packages/com_mokoog/mokoog.xml index f5baeb3..619dcaf 100644 --- a/src/packages/com_mokoog/mokoog.xml +++ b/src/packages/com_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="component" method="upgrade"> <name>com_mokoog</name> - <version>01.00.00</version> + <version>01.00.00-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/src/packages/plg_content_mokoog/mokoog.xml b/src/packages/plg_content_mokoog/mokoog.xml index bb64e27..3fc2e68 100644 --- a/src/packages/plg_content_mokoog/mokoog.xml +++ b/src/packages/plg_content_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="plugin" group="content" method="upgrade"> <name>Content - MokoOpenGraph</name> - <version>01.00.00</version> + <version>01.00.00-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/src/packages/plg_system_mokoog/mokoog.xml b/src/packages/plg_system_mokoog/mokoog.xml index c1a12ac..e40429c 100644 --- a/src/packages/plg_system_mokoog/mokoog.xml +++ b/src/packages/plg_system_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="plugin" group="system" method="upgrade"> <name>System - MokoOpenGraph</name> - <version>01.00.00</version> + <version>01.00.00-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/src/packages/plg_webservices_mokoog/mokoog.xml b/src/packages/plg_webservices_mokoog/mokoog.xml index 5f862ab..af192f8 100644 --- a/src/packages/plg_webservices_mokoog/mokoog.xml +++ b/src/packages/plg_webservices_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="plugin" group="webservices" method="upgrade"> <name>Web Services - MokoOpenGraph</name> - <version>01.00.00</version> + <version>01.00.00-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/src/pkg_mokoog.xml b/src/pkg_mokoog.xml index 783c779..b45955d 100644 --- a/src/pkg_mokoog.xml +++ b/src/pkg_mokoog.xml @@ -8,7 +8,7 @@ <extension type="package" method="upgrade"> <name>MokoOpenGraph</name> <packagename>mokoog</packagename> - <version>01.00.00</version> + <version>01.00.00-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> From 0b97a62c82bc3375375a7cd2867a35aa906efe76 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" <gitea-actions[bot]@mokoconsulting.tech> Date: Sun, 24 May 2026 04:37:35 +0000 Subject: [PATCH 033/132] chore: update development channel 01.00.00 [skip ci] --- updates.xml | 96 +++++++---------------------------------------------- 1 file changed, 12 insertions(+), 84 deletions(-) diff --git a/updates.xml b/updates.xml index 3c7fe4e..a7aa193 100644 --- a/updates.xml +++ b/updates.xml @@ -1,95 +1,23 @@ -<?xml version="1.0" encoding="UTF-8"?> +<?xml version='1.0' encoding='UTF-8'?> +<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> + SPDX-License-Identifier: GPL-3.0-or-later + VERSION: 01.00.00-dev + --> + <updates> - <!-- Managed by CI/CD pipeline — do not edit manually --> - -<update> + <update> <name>MokoOpenGraph</name> - <description>MokoOpenGraph development build.</description> + <description>MokoOpenGraph update</description> <element>pkg_mokoog</element> <type>package</type> - <version>01.00.00</version> - <creationDate>2026-05-24</creationDate> - <infourl title='MokoOpenGraph'>https://git.mokoconsulting.tech/MokoConsulting/MokoOpenGraph/releases/tag/development</infourl> - <downloads> - <downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoOpenGraph/releases/download/development/pkg_mokoog-01.00.00-dev.zip</downloadurl> - </downloads> - <sha256>3a910f6c075d2cddbda905c175e666b9cbf1ae79e465132f18293ac3be15266e</sha256> + <version>01.00.00-dev</version> <tags><tag>development</tag></tags> - <maintainer>Moko Consulting</maintainer> - <maintainerurl>https://mokoconsulting.tech</maintainerurl> - <targetplatform name='joomla' version='(5|6).*'/> - </update> - -<update> - <name>MokoOpenGraph</name> - <description>MokoOpenGraph stable build.</description> - <element>pkg_mokoog</element> - <type>package</type> - <version>01.00.00</version> - <creationDate>2026-05-23</creationDate> - <infourl title='MokoOpenGraph'>https://git.mokoconsulting.tech/MokoConsulting/MokoOpenGraph/releases/tag/v01</infourl> + <infourl title="MokoOpenGraph">https://git.mokoconsulting.tech/MokoConsulting/MokoOpenGraph/releases/tag/development</infourl> <downloads> - <downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoOpenGraph/releases/download/v01/pkg_mokoog-01.00.00.zip</downloadurl> + <downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoOpenGraph/releases/download/development/pkg_pkg_mokoog-01.00.00-dev.zip</downloadurl> </downloads> - <sha256>e2262d9e515af284d6837627d63fb7ef7376aea5c50bebd1f4ea4e92a4ae4a6d</sha256> - <tags><tag>alpha</tag></tags> + <targetplatform name="joomla" version="((5.[0-9])|(6.[0-9]))" /> <maintainer>Moko Consulting</maintainer> <maintainerurl>https://mokoconsulting.tech</maintainerurl> - <targetplatform name='joomla' version='(5|6).*'/> </update> - -<update> - <name>MokoOpenGraph</name> - <description>MokoOpenGraph stable build.</description> - <element>pkg_mokoog</element> - <type>package</type> - <version>01.00.00</version> - <creationDate>2026-05-23</creationDate> - <infourl title='MokoOpenGraph'>https://git.mokoconsulting.tech/MokoConsulting/MokoOpenGraph/releases/tag/v01</infourl> - <downloads> - <downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoOpenGraph/releases/download/v01/pkg_mokoog-01.00.00.zip</downloadurl> - </downloads> - <sha256>e2262d9e515af284d6837627d63fb7ef7376aea5c50bebd1f4ea4e92a4ae4a6d</sha256> - <tags><tag>beta</tag></tags> - <maintainer>Moko Consulting</maintainer> - <maintainerurl>https://mokoconsulting.tech</maintainerurl> - <targetplatform name='joomla' version='(5|6).*'/> - </update> - -<update> - <name>MokoOpenGraph</name> - <description>MokoOpenGraph stable build.</description> - <element>pkg_mokoog</element> - <type>package</type> - <version>01.00.00</version> - <creationDate>2026-05-23</creationDate> - <infourl title='MokoOpenGraph'>https://git.mokoconsulting.tech/MokoConsulting/MokoOpenGraph/releases/tag/v01</infourl> - <downloads> - <downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoOpenGraph/releases/download/v01/pkg_mokoog-01.00.00.zip</downloadurl> - </downloads> - <sha256>e2262d9e515af284d6837627d63fb7ef7376aea5c50bebd1f4ea4e92a4ae4a6d</sha256> - <tags><tag>rc</tag></tags> - <maintainer>Moko Consulting</maintainer> - <maintainerurl>https://mokoconsulting.tech</maintainerurl> - <targetplatform name='joomla' version='(5|6).*'/> - </update> - -<update> - <name>MokoOpenGraph</name> - <description>MokoOpenGraph stable build.</description> - <element>pkg_mokoog</element> - <type>package</type> - <version>01.00.00</version> - <creationDate>2026-05-23</creationDate> - <infourl title='MokoOpenGraph'>https://git.mokoconsulting.tech/MokoConsulting/MokoOpenGraph/releases/tag/v01</infourl> - <downloads> - <downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoOpenGraph/releases/download/v01/pkg_mokoog-01.00.00.zip</downloadurl> - </downloads> - <sha256>e2262d9e515af284d6837627d63fb7ef7376aea5c50bebd1f4ea4e92a4ae4a6d</sha256> - <tags><tag>stable</tag></tags> - <maintainer>Moko Consulting</maintainer> - <maintainerurl>https://mokoconsulting.tech</maintainerurl> - <targetplatform name='joomla' version='(5|6).*'/> - </update> - </updates> From 3563c62ec219021a9347d2fa9584938631dc18ff Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 26 May 2026 17:35:11 +0000 Subject: [PATCH 034/132] chore(ci): update pre-release.yml from moko-platform [skip ci] --- .mokogitea/workflows/pre-release.yml | 613 +++++++++++++++++---------- 1 file changed, 388 insertions(+), 225 deletions(-) diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml index 234a949..b90d2c8 100644 --- a/.mokogitea/workflows/pre-release.yml +++ b/.mokogitea/workflows/pre-release.yml @@ -1,225 +1,388 @@ -# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: moko-platform.Release -# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform -# PATH: /templates/workflows/universal/pre-release.yml.template -# VERSION: 05.01.00 -# BRIEF: Manual pre-release — builds dev/alpha/beta/rc packages from any branch - -name: "Universal: Pre-Release" - -on: - workflow_dispatch: - inputs: - stability: - description: 'Pre-release channel' - required: true - type: choice - options: - - development - - alpha - - beta - - release-candidate - -permissions: - contents: write - -env: - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} - GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} - -jobs: - build: - name: "Build Pre-Release (${{ inputs.stability }})" - runs-on: release - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - token: ${{ secrets.GA_TOKEN }} - - - name: Setup moko-platform tools - env: - MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }} - MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN }}"}}' - run: | - if ! command -v composer &> /dev/null; then - sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 - fi - git clone --depth 1 --branch main --quiet \ - "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ - /tmp/moko-platform-api - cd /tmp/moko-platform-api - composer install --no-dev --no-interaction --quiet - - - name: Detect platform - id: platform - run: | - PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1 | tr -d '[:space:]') - [ -z "$PLATFORM" ] && PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .gitea/manifest.xml 2>/dev/null | head -1 | tr -d '[:space:]') - [ -z "$PLATFORM" ] && PLATFORM="generic" - echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT" - - - name: Resolve metadata and bump version - id: meta - run: | - CLI="/tmp/moko-platform-api/cli" - STABILITY="${{ inputs.stability }}" - - case "$STABILITY" in - development) SUFFIX="-dev"; TAG="development" ;; - alpha) SUFFIX="-alpha"; TAG="alpha" ;; - beta) SUFFIX="-beta"; TAG="beta" ;; - release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;; - esac - - # Bump patch version via CLI - CURRENT=$(php $CLI/version_read.php --path . 2>/dev/null) - [ -z "$CURRENT" ] && CURRENT="00.00.00" - php $CLI/version_bump.php --path . - VERSION=$(php $CLI/version_read.php --path . 2>/dev/null) - echo "Bumping: ${CURRENT} → ${VERSION} (patch)" - - # Set platform-specific version with stability suffix - php $CLI/version_set_platform.php \ - --path . --version "$VERSION" --stability "$STABILITY" --branch "${{ github.ref_name }}" - - # Commit version bump - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" - git add -A - git diff --cached --quiet || { - git commit -m "chore(version): bump ${CURRENT} → ${VERSION}${SUFFIX} [skip ci]" - git push origin HEAD 2>&1 - } - - echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT" - echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT" - echo "tag=${TAG}" >> "$GITHUB_OUTPUT" - - - name: Build package - id: package - run: | - CLI="/tmp/moko-platform-api/cli" - VERSION="${{ steps.meta.outputs.version }}" - SUFFIX="${{ steps.meta.outputs.suffix }}" - - # Build ZIP + tar.gz via CLI (handles type prefix, excludes, multi-extension packages) - php $CLI/package_build.php \ - --path . \ - --version "${VERSION}${SUFFIX}" \ - --output-dir build \ - --github-output - - - name: Create release and upload - run: | - CLI="/tmp/moko-platform-api/cli" - VERSION="${{ steps.meta.outputs.version }}" - SUFFIX="${{ steps.meta.outputs.suffix }}" - TAG="${{ steps.meta.outputs.tag }}" - STABILITY="${{ steps.meta.outputs.stability }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - EXT_ELEMENT="${{ steps.package.outputs.ext_element }}" - [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') - - SHA256="${{ steps.package.outputs.sha256_zip }}" - ZIP_PATH="${{ steps.package.outputs.zip_path }}" - TAR_PATH="${{ steps.package.outputs.tar_path }}" - - # Create release - php $CLI/release_manage.php \ - --action create \ - --tag "$TAG" \ - --name "${EXT_ELEMENT} ${VERSION}${SUFFIX} (${STABILITY})" \ - --body "## ${VERSION}${SUFFIX} ($(date +%Y-%m-%d))\n**Channel:** ${STABILITY}\n**SHA-256:** \`${SHA256}\`" \ - --target "${{ github.ref_name }}" \ - --token "${{ secrets.GA_TOKEN }}" \ - --api-base "$API_BASE" - - # Upload assets - FILES="${ZIP_PATH}" - [ -f "$TAR_PATH" ] && FILES="${FILES},${TAR_PATH}" - php $CLI/release_manage.php \ - --action upload \ - --tag "$TAG" \ - --files "$FILES" \ - --token "${{ secrets.GA_TOKEN }}" \ - --api-base "$API_BASE" - - - name: Update updates.xml - if: steps.platform.outputs.platform == 'joomla' - run: | - CLI="/tmp/moko-platform-api/cli" - VERSION="${{ steps.meta.outputs.version }}" - STABILITY="${{ steps.meta.outputs.stability }}" - SHA256="${{ steps.package.outputs.sha256_zip }}" - - # Map stability names - case "$STABILITY" in - release-candidate) CLI_STABILITY="rc" ;; - *) CLI_STABILITY="$STABILITY" ;; - esac - - # Generate updates.xml with stability-suffixed versions - php $CLI/updates_xml_build.php \ - --path . \ - --version "$VERSION" \ - --stability "$CLI_STABILITY" \ - --sha "$SHA256" \ - --gitea-url "${GITEA_URL}" \ - --org "${GITEA_ORG}" \ - --repo "${GITEA_REPO}" - - # Commit and push - if ! git diff --quiet updates.xml 2>/dev/null; then - git add updates.xml - git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]" - git push origin HEAD 2>&1 || echo "WARNING: push failed" - fi - - - name: "Sync updates.xml to all branches" - if: steps.platform.outputs.platform == 'joomla' - run: | - CURRENT_BRANCH="${{ github.ref_name }}" - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - - for BRANCH in main dev; do - [ "$BRANCH" = "$CURRENT_BRANCH" ] && continue - echo "Syncing updates.xml → ${BRANCH}" - git fetch origin "${BRANCH}" 2>/dev/null || continue - git checkout "origin/${BRANCH}" -- . 2>/dev/null || continue - git checkout "${CURRENT_BRANCH}" -- updates.xml - if ! git diff --quiet updates.xml 2>/dev/null; then - git add updates.xml - git commit -m "chore: sync updates.xml from ${CURRENT_BRANCH} [skip ci]" - git push origin HEAD:refs/heads/${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed" - fi - git checkout "${CURRENT_BRANCH}" 2>/dev/null - done - - - name: "Delete lesser pre-release channels (cascade)" - continue-on-error: true - run: | - STABILITY="${{ steps.meta.outputs.stability }}" - - # Map workflow stability names to CLI names - case "$STABILITY" in - release-candidate) CLI_STABILITY="rc" ;; - *) CLI_STABILITY="$STABILITY" ;; - esac - - php /tmp/moko-platform-api/cli/release_cascade.php \ - --stability "$CLI_STABILITY" \ - --token "${{ secrets.GA_TOKEN }}" \ - --api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" +# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: moko-platform.Release +# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform +# PATH: /templates/workflows/universal/pre-release.yml.template +# VERSION: 05.01.00 +# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch + +name: "Universal: Pre-Release" + +on: + pull_request: + types: [closed] + branches: + - dev + paths: + - 'src/**' + - 'htdocs/**' + workflow_dispatch: + inputs: + stability: + description: 'Pre-release channel' + required: true + type: choice + options: + - development + - alpha + - beta + - release-candidate + +permissions: + contents: write + +env: + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + +jobs: + build: + name: "Build Pre-Release (${{ inputs.stability || 'development' }})" + runs-on: release + if: >- + github.event_name == 'workflow_dispatch' || + (github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev') + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GA_TOKEN }} + + - name: Setup tools + run: | + # Update moko-platform CLI tools if available; install PHP if missing + if command -v moko-platform-update &> /dev/null; then + moko-platform-update + elif [ -d "/opt/moko-platform" ]; then + cd /opt/moko-platform && git pull origin main --quiet 2>/dev/null || true + else + if ! command -v php &> /dev/null; then + sudo apt-get update -qq + sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl >/dev/null 2>&1 + fi + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \ + /tmp/moko-platform-api + fi + # Set MOKO_CLI to whichever path exists + if [ -d "/opt/moko-platform/cli" ]; then + echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV" + else + echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV" + fi + + - name: Detect platform + id: platform + run: | + PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1 | tr -d '[:space:]') + [ -z "$PLATFORM" ] && PLATFORM="generic" + echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT" + MANIFEST=$(find ./src -maxdepth 1 -name "pkg_*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1) + [ -z "$MANIFEST" ] && MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "*/packages/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1) + [ -z "$MANIFEST" ] && MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1) + MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1) + echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT" + echo "mod_file=${MOD_FILE}" >> "$GITHUB_OUTPUT" + + - name: Resolve metadata and bump version + id: meta + run: | + STABILITY="${{ inputs.stability || 'development' }}" + + case "$STABILITY" in + development) SUFFIX="-dev"; TAG="development" ;; + alpha) SUFFIX="-alpha"; TAG="alpha" ;; + beta) SUFFIX="-beta"; TAG="beta" ;; + release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;; + esac + + # Patch bump via CLI tool + php ${MOKO_CLI}/version_bump.php --path . + VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null) + [ -z "$VERSION" ] && VERSION="00.00.01" + TODAY=$(date +%Y-%m-%d) + + # Update platform-specific manifest + PLATFORM="${{ steps.platform.outputs.platform }}" + MANIFEST="${{ steps.platform.outputs.manifest }}" + MOD_FILE="${{ steps.platform.outputs.mod_file }}" + + php ${MOKO_CLI}/version_set_platform.php \ + --path . --version "$VERSION" --branch "${{ github.ref_name }}" 2>/dev/null || true + + # Verify version consistency across all files + php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true + + # Commit version bump + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + git add -A + git diff --cached --quiet || { + git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]" + git push origin HEAD 2>&1 + } + + # Auto-detect element (platform-aware) + EXT_ELEMENT="" + case "$PLATFORM" in + joomla) + if [ -n "$MANIFEST" ]; then + EXT_ELEMENT=$(sed -n 's/.*<element>\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1) + if [ -z "$EXT_ELEMENT" ]; then + EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]') + case "$EXT_ELEMENT" in + templatedetails|manifest) EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;; + esac + fi + else + EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') + fi + ;; + dolibarr) + if [ -n "$MOD_FILE" ]; then + MOD_BASENAME=$(basename "$MOD_FILE" .class.php) + EXT_ELEMENT=$(echo "$MOD_BASENAME" | sed 's/^mod//' | tr '[:upper:]' '[:lower:]') + else + EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') + fi + ;; + *) + EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') + ;; + esac + + ZIP_NAME="${EXT_ELEMENT}-${VERSION}${SUFFIX}.zip" + + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT" + echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT" + echo "tag=${TAG}" >> "$GITHUB_OUTPUT" + echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT" + echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT" + echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT" + + echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ===" + + - name: Build package + run: | + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" + if [ ! -d "$SOURCE_DIR" ]; then + echo "::error::No src/ or htdocs/ directory" + exit 1 + fi + + MANIFEST="${{ steps.meta.outputs.manifest }}" + EXT_TYPE="" + if [ -n "$MANIFEST" ]; then + EXT_TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + fi + + EXCLUDES="sftp-config* .ftpignore *.ppk *.pem *.key .env* *.local .build-trigger" + + mkdir -p build/package + + if [ "$EXT_TYPE" = "package" ] && [ -d "${SOURCE_DIR}/packages" ]; then + echo "=== Building Joomla PACKAGE (multi-extension) ===" + for ext_dir in "${SOURCE_DIR}"/packages/*/; do + [ ! -d "$ext_dir" ] && continue + EXT_NAME=$(basename "$ext_dir") + echo " Packaging sub-extension: ${EXT_NAME}" + cd "$ext_dir" + zip -r "../../build/package/${EXT_NAME}.zip" . -x $EXCLUDES + cd "$OLDPWD" + done + for f in "${SOURCE_DIR}"/*.xml "${SOURCE_DIR}"/*.php; do + [ -f "$f" ] && cp "$f" build/package/ + done + else + echo "=== Building standard extension ===" + rsync -a \ + --exclude='sftp-config*' \ + --exclude='.ftpignore' \ + --exclude='*.ppk' \ + --exclude='*.pem' \ + --exclude='*.key' \ + --exclude='.env*' \ + --exclude='*.local' \ + --exclude='.build-trigger' \ + "${SOURCE_DIR}/" build/package/ + fi + + - name: Create ZIP + id: zip + run: | + ZIP_NAME="${{ steps.meta.outputs.zip_name }}" + cd build/package + zip -r "../${ZIP_NAME}" . + cd .. + + SHA256=$(sha256sum "${ZIP_NAME}" | cut -d' ' -f1) + echo "sha256=${SHA256}" >> "$GITHUB_OUTPUT" + echo "ZIP: ${ZIP_NAME} (SHA: ${SHA256:0:16}...)" + + - name: Create or replace Gitea release + id: release + run: | + TAG="${{ steps.meta.outputs.tag }}" + VERSION="${{ steps.meta.outputs.version }}" + STABILITY="${{ steps.meta.outputs.stability }}" + SHA256="${{ steps.zip.outputs.sha256 }}" + ZIP_NAME="${{ steps.meta.outputs.zip_name }}" + EXT_ELEMENT="${{ steps.meta.outputs.ext_element }}" + TOKEN="${{ secrets.GA_TOKEN }}" + API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + BRANCH=$(git branch --show-current) + + BODY="## ${VERSION} ($(date +%Y-%m-%d)) + **Channel:** ${STABILITY} + **SHA-256:** \`${SHA256}\`" + + # Delete existing release + EXISTING_ID=$(curl -sS -H "Authorization: token ${TOKEN}" \ + "${API}/releases/tags/${TAG}" | jq -r '.id // empty' 2>/dev/null) + if [ -n "$EXISTING_ID" ]; then + curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API}/releases/${EXISTING_ID}" 2>/dev/null || true + curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API}/tags/${TAG}" 2>/dev/null || true + fi + + # Create release + RELEASE_ID=$(curl -sS -X POST -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/json" \ + "${API}/releases" \ + -d "$(jq -n \ + --arg tag "$TAG" \ + --arg target "$BRANCH" \ + --arg name "${EXT_ELEMENT} ${VERSION} (${STABILITY})" \ + --arg body "$BODY" \ + '{tag_name: $tag, target_commitish: $target, name: $name, body: $body, prerelease: true}' + )" | jq -r '.id') + + echo "release_id=${RELEASE_ID}" >> "$GITHUB_OUTPUT" + + # Upload ZIP + curl -sS -X POST -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/octet-stream" \ + "${API}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" \ + --data-binary "@build/${ZIP_NAME}" + + echo "Released: ${EXT_ELEMENT} ${VERSION} (${STABILITY})" + + - name: Update updates.xml + if: steps.platform.outputs.platform == 'joomla' + run: | + STABILITY="${{ steps.meta.outputs.stability }}" + VERSION="${{ steps.meta.outputs.version }}" + SHA256="${{ steps.zip.outputs.sha256 }}" + ZIP_NAME="${{ steps.meta.outputs.zip_name }}" + TAG="${{ steps.meta.outputs.tag }}" + + if [ ! -f "updates.xml" ]; then + echo "No updates.xml -- skipping" + exit 0 + fi + + # Map stability to XML tag name + case "$STABILITY" in + development) XML_TAG="development" ;; + alpha) XML_TAG="alpha" ;; + beta) XML_TAG="beta" ;; + release-candidate) XML_TAG="rc" ;; + *) XML_TAG="$STABILITY" ;; + esac + + DOWNLOAD_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${TAG}/${ZIP_NAME}" + + # Use PHP to update the channel in updates.xml + php -r ' + $xml_tag = $argv[1]; + $version = $argv[2]; + $sha256 = $argv[3]; + $url = $argv[4]; + $date = date("Y-m-d"); + + $content = file_get_contents("updates.xml"); + $pattern = "/(<update>(?:(?!<\/update>).)*?<tag>" . preg_quote($xml_tag) . "<\/tag>.*?<\/update>)/s"; + + $content = preg_replace_callback($pattern, function($m) use ($version, $sha256, $url, $date) { + $block = $m[0]; + $block = preg_replace("/<version>[^<]*<\/version>/", "<version>{$version}</version>", $block); + if (strpos($block, "<sha256>") !== false) { + $block = preg_replace("/<sha256>[^<]*<\/sha256>/", "<sha256>{$sha256}</sha256>", $block); + } else { + $block = str_replace("</downloads>", "</downloads>\n <sha256>{$sha256}</sha256>", $block); + } + $block = preg_replace("/(<downloadurl[^>]*>)[^<]*(<\/downloadurl>)/", "\${1}{$url}\${2}", $block); + return $block; + }, $content); + + file_put_contents("updates.xml", $content); + echo "Updated {$xml_tag} channel: version={$version}\n"; + ' "$XML_TAG" "$VERSION" "$SHA256" "$DOWNLOAD_URL" + + # Commit and push + if ! git diff --quiet updates.xml 2>/dev/null; then + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git add updates.xml + git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]" + git push origin HEAD 2>&1 || echo "WARNING: push failed" + fi + + - name: "Sync updates.xml to all branches" + if: steps.platform.outputs.platform == 'joomla' + run: | + CURRENT_BRANCH="${{ github.ref_name }}" + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + + for BRANCH in main dev; do + [ "$BRANCH" = "$CURRENT_BRANCH" ] && continue + echo "Syncing updates.xml -> ${BRANCH}" + git fetch origin "${BRANCH}" 2>/dev/null || continue + git checkout "origin/${BRANCH}" -- . 2>/dev/null || continue + git checkout "${CURRENT_BRANCH}" -- updates.xml + if ! git diff --quiet updates.xml 2>/dev/null; then + git add updates.xml + git commit -m "chore: sync updates.xml from ${CURRENT_BRANCH} [skip ci]" + git push origin HEAD:refs/heads/${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed" + fi + git checkout "${CURRENT_BRANCH}" 2>/dev/null + done + + - name: "Delete lesser pre-release channels (cascade)" + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.GA_TOKEN }}" + + php ${MOKO_CLI}/release_cascade.php \ + --stability "${{ steps.meta.outputs.stability }}" \ + --token "${TOKEN}" \ + --api-base "${API_BASE}" + + - name: Summary + if: always() + run: | + VERSION="${{ steps.meta.outputs.version }}" + STABILITY="${{ steps.meta.outputs.stability }}" + ZIP_NAME="${{ steps.meta.outputs.zip_name }}" + SHA256="${{ steps.zip.outputs.sha256 }}" + echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY + echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY + echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY From 292265841e4d04ca1b199ea8f3c95dae7780f89e Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 26 May 2026 17:36:26 +0000 Subject: [PATCH 035/132] chore(ci): update auto-release.yml from moko-platform [skip ci] --- .mokogitea/workflows/auto-release.yml | 1394 ++++++++++++++----------- 1 file changed, 760 insertions(+), 634 deletions(-) diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml index 7049eb3..9eb98c8 100644 --- a/.mokogitea/workflows/auto-release.yml +++ b/.mokogitea/workflows/auto-release.yml @@ -1,634 +1,760 @@ -# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: moko-platform.Release -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform -# PATH: /templates/workflows/universal/auto-release.yml.template -# VERSION: 05.00.00 -# BRIEF: Universal build & release � detects platform from manifest.xml -# -# +========================================================================+ -# | UNIVERSAL BUILD & RELEASE PIPELINE | -# +========================================================================+ -# | | -# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. | -# | | -# | Platform-specific: | -# | joomla: XML manifest, updates.xml, type-prefixed packages | -# | dolibarr: mod*.class.php, update.txt, dev version reset | -# | generic: README-only, no update stream | -# | | -# +========================================================================+ - -name: "Universal: Build & Release" - -on: - pull_request: - types: [closed] - branches: - - main - paths: - - 'src/**' - - 'htdocs/**' - workflow_dispatch: - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} - GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} - -permissions: - contents: write - -jobs: - release: - name: Build & Release Pipeline - runs-on: release - if: >- - github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - token: ${{ secrets.GA_TOKEN }} - fetch-depth: 0 - - - name: Setup moko-platform tools - env: - MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }} - MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN }}"}}' - run: | - # Ensure PHP + Composer are available - if ! command -v composer &> /dev/null; then - sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 - fi - git clone --depth 1 --branch main --quiet \ - "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ - /tmp/moko-platform-api - cd /tmp/moko-platform-api - composer install --no-dev --no-interaction --quiet - - - # -- PLATFORM DETECTION --------------------------------------------------- - - name: Detect platform - id: platform - run: | - # Read platform from manifest.xml <platform> element; fallback to generic - PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*//p' .mokogitea/manifest.xml 2>/dev/null | head -1 | tr -d '[:space:]') - [ -z "$PLATFORM" ] && PLATFORM="generic" - echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT" - echo "Platform detected: ${PLATFORM}" - # For packages: prefer pkg_*.xml in src/; fallback to any manifest - MANIFEST=$(find ./src -maxdepth 1 -name "pkg_*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1) - [ -z "$MANIFEST" ] && MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "*/packages/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1) - [ -z "$MANIFEST" ] && MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1) - MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1) - echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT" - echo "mod_file=${MOD_FILE}" >> "$GITHUB_OUTPUT" - - # -- STEP 1: Read version ----------------------------------------------- - - name: "Step 1: Read version from README.md" - id: version - run: | - VERSION=$(php /tmp/moko-platform-api/cli/version_read.php --path . 2>/dev/null) - if [ -z "$VERSION" ]; then - echo "No VERSION in README.md — skipping release" - echo "skip=true" >> "$GITHUB_OUTPUT" - exit 0 - fi - # Derive major.minor for branch naming (patches update existing branch) - MINOR=$(echo "$VERSION" | awk -F. '{printf "%s.%s", $1, $2}') - PATCH=$(echo "$VERSION" | awk -F. '{print $3}') - - MAJOR=$(echo "$VERSION" | awk -F. '{print $1}') - MINOR_NUM=$(echo "$VERSION" | awk -F. '{print $2}') - - echo "version=$VERSION" >> "$GITHUB_OUTPUT" - echo "branch=version/${MAJOR}" >> "$GITHUB_OUTPUT" - echo "minor=$MINOR" >> "$GITHUB_OUTPUT" - echo "major=$MAJOR" >> "$GITHUB_OUTPUT" - echo "release_tag=stable" >> "$GITHUB_OUTPUT" - echo "stability=stable" >> "$GITHUB_OUTPUT" - echo "skip=false" >> "$GITHUB_OUTPUT" - if [ "$PATCH" = "00" ] || [ "$PATCH" = "01" ]; then - echo "is_minor=true" >> "$GITHUB_OUTPUT" - echo "Version: $VERSION (first release for this minor — full pipeline)" - else - echo "is_minor=false" >> "$GITHUB_OUTPUT" - echo "Version: $VERSION (patch — platform version + badges only)" - fi - - # -- STEP 1b: Bump minor version (stable = minor bump, reset patch) ------ - - name: "Step 1b: Bump minor version for stable release" - if: steps.version.outputs.skip != 'true' - id: bump - run: | - CLI="/tmp/moko-platform-api/cli" - CURRENT=$(php $CLI/version_read.php --path . 2>/dev/null) - [ -z "$CURRENT" ] && { echo "skip=true" >> "$GITHUB_OUTPUT"; exit 0; } - - # Minor bump via CLI (updates README.md in-place) - BUMP_OUT=$(php $CLI/version_bump.php --path . --minor) - VERSION=$(php $CLI/version_read.php --path . 2>/dev/null) - TODAY=$(date +%Y-%m-%d) - echo "Stable bump: ${BUMP_OUT}" - - # Set platform-specific version (Joomla XML, Dolibarr mod*.class.php) - php $CLI/version_set_platform.php --path . --version "$VERSION" --stability stable --branch main - - # Promote [Unreleased] in CHANGELOG.md - php $CLI/changelog_promote.php --path . --version "$VERSION" --date "$TODAY" 2>/dev/null || true - - # Commit and push - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" - git add -A - git diff --cached --quiet || { - git commit -m "chore(version): bump ${CURRENT} → ${VERSION} [skip ci]" - git push origin HEAD:main 2>&1 - } - - # Override version output for rest of pipeline - MAJOR=$(echo "$VERSION" | cut -d. -f1) - echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - echo "major=${MAJOR}" >> "$GITHUB_OUTPUT" - - - name: Check if already released - if: steps.version.outputs.skip != 'true' - id: check - run: | - TAG="${{ steps.version.outputs.release_tag }}" - BRANCH="${{ steps.version.outputs.branch }}" - - TAG_EXISTS=false - BRANCH_EXISTS=false - - git rev-parse "$TAG" >/dev/null 2>&1 && TAG_EXISTS=true - git ls-remote --heads origin "$BRANCH" 2>/dev/null | grep -q "$BRANCH" && BRANCH_EXISTS=true - - echo "tag_exists=$TAG_EXISTS" >> "$GITHUB_OUTPUT" - echo "branch_exists=$BRANCH_EXISTS" >> "$GITHUB_OUTPUT" - - # Tag and branch may persist across patch releases — never skip - echo "already_released=false" >> "$GITHUB_OUTPUT" - - # -- SANITY CHECKS ------------------------------------------------------- - - name: "Sanity: Pre-release validation" - if: >- - steps.version.outputs.skip != 'true' && - steps.check.outputs.already_released != 'true' - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - ERRORS=0 - - PLATFORM="${{ steps.platform.outputs.platform }}" - MANIFEST="${{ steps.platform.outputs.manifest }}" - MOD_FILE="${{ steps.platform.outputs.mod_file }}" - echo "## Pre-Release Sanity Checks (${PLATFORM})" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # -- Version drift check (must pass before release) -------- - README_VER=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1) - if [ "$README_VER" != "$VERSION" ]; then - echo "- Version drift: README says \`${README_VER}\` but releasing \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS+1)) - else - echo "- Version consistent: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY - fi - - # Check CHANGELOG version matches - CL_VER=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' CHANGELOG.md 2>/dev/null | head -1) - if [ -n "$CL_VER" ] && [ "$CL_VER" != "$VERSION" ]; then - echo "- CHANGELOG drift: \`${CL_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS+1)) - fi - - # Check composer.json version if present - if [ -f "composer.json" ]; then - COMP_VER=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' composer.json 2>/dev/null | head -1) - if [ -n "$COMP_VER" ] && [ "$COMP_VER" != "$VERSION" ]; then - echo "- composer.json drift: \`${COMP_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS+1)) - fi - fi - - # Common checks - if [ ! -f "LICENSE" ]; then - echo "- Missing LICENSE file" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS+1)) - else - echo "- LICENSE present" >> $GITHUB_STEP_SUMMARY - fi - - if [ ! -d "src" ] && [ ! -d "htdocs" ]; then - echo "- Warning: No src/ or htdocs/ directory" >> $GITHUB_STEP_SUMMARY - else - echo "- Source directory present" >> $GITHUB_STEP_SUMMARY - fi - - # -- Platform-specific checks -------- - case "$PLATFORM" in - joomla) - if [ -n "$MANIFEST" ]; then - XML_VER=$(sed -n 's/.*<version>\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1) - if [ -n "$XML_VER" ] && [ "$XML_VER" != "$VERSION" ]; then - echo "- Manifest drift: \`${XML_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS+1)) - else - echo "- Manifest version: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY - fi - TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" 2>/dev/null) - echo "- Extension type: ${TYPE:-unknown}" >> $GITHUB_STEP_SUMMARY - else - echo "- No Joomla XML manifest (WaaS site)" >> $GITHUB_STEP_SUMMARY - fi ;; - dolibarr) - if [ -n "$MOD_FILE" ]; then - MOD_VER=$(sed -n "s/.*\\\$this->version = '\([^']*\)'.*/\1/p" "$MOD_FILE" 2>/dev/null | head -1) - if [ -n "$MOD_VER" ] && [ "$MOD_VER" != "$VERSION" ]; then - echo "- Module drift: \`${MOD_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS+1)) - else - echo "- Module version: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY - fi - else - echo "- No mod*.class.php found" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS+1)) - fi - if [ ! -f "update.txt" ]; then - echo "- Missing update.txt" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS+1)) - fi ;; - *) echo "- Generic platform � no manifest checks" >> $GITHUB_STEP_SUMMARY ;; - esac - - echo "" >> $GITHUB_STEP_SUMMARY - if [ "$ERRORS" -gt 0 ]; then - echo "**${ERRORS} error(s) — release may be incomplete**" >> $GITHUB_STEP_SUMMARY - else - echo "**All sanity checks passed**" >> $GITHUB_STEP_SUMMARY - fi - - # -- STEP 2: Create or update version/XX.YY archive branch --------------- - # Always runs — every version change on main archives to version/XX.YY - - name: "Step 2: Version archive branch" - if: steps.check.outputs.already_released != 'true' - run: | - BRANCH="${{ steps.version.outputs.branch }}" - IS_MINOR="${{ steps.version.outputs.is_minor }}" - PATCH="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - PATCH_NUM=$(echo "$PATCH" | awk -F. '{print $3}') - - # Check if branch exists - if git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then - git push origin HEAD:"$BRANCH" --force - echo "Updated archive branch: ${BRANCH} (patch ${PATCH_NUM})" >> $GITHUB_STEP_SUMMARY - else - git checkout -b "$BRANCH" 2>/dev/null || git checkout "$BRANCH" - git push origin "$BRANCH" --force - echo "Created archive branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY - fi - - # -- STEP 3: Set platform version ---------------------------------------- - - name: "Step 3: Set platform version" - if: >- - steps.version.outputs.skip != 'true' && - steps.check.outputs.already_released != 'true' - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - php /tmp/moko-platform-api/cli/version_set_platform.php \ - --path . --version "$VERSION" --branch main - - # -- STEP 4: Update version badges ---------------------------------------- - - name: "Step 4: Update version badges" - if: >- - steps.version.outputs.skip != 'true' && - steps.check.outputs.already_released != 'true' - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - php /tmp/moko-platform-api/cli/badge_update.php --path . --version "$VERSION" - - # -- STEP 5: Write updates.xml (Joomla update server) --------------------- - - name: "Step 5: Write update stream" - id: updates - if: >- - steps.version.outputs.skip != 'true' && - steps.check.outputs.already_released != 'true' - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - CLI="/tmp/moko-platform-api/cli" - - # Generate updates.xml with all stability channels + suffixed versions - # Also exports ext_element, ext_name, ext_type, ext_folder to GITHUB_OUTPUT - php $CLI/updates_xml_build.php \ - --path . \ - --version "$VERSION" \ - --stability stable \ - --gitea-url "${GITEA_URL}" \ - --org "${GITEA_ORG}" \ - --repo "${GITEA_REPO}" \ - --github-output - - echo "updates.xml: ${VERSION} (all channels updated to stable)" >> $GITHUB_STEP_SUMMARY - - # -- Commit all changes --------------------------------------------------- - - name: Commit release changes - if: >- - steps.version.outputs.skip != 'true' && - steps.check.outputs.already_released != 'true' - run: | - if git diff --quiet && git diff --cached --quiet; then - echo "No changes to commit" - exit 0 - fi - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - # Set push URL with token for branch-protected repos - git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" - git add -A - git commit -m "chore(release): build ${VERSION} [skip ci]" \ - --author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" - git push -u origin HEAD - - # -- STEP 6: Create tag --------------------------------------------------- - - name: "Step 6: Create git tag" - if: >- - steps.version.outputs.skip != 'true' && - steps.check.outputs.tag_exists != 'true' && - steps.version.outputs.is_minor == 'true' - run: | - RELEASE_TAG="${{ steps.version.outputs.release_tag }}" - # Only create the major release tag if it doesn't exist yet - if ! git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then - git tag "$RELEASE_TAG" - git push origin "$RELEASE_TAG" - echo "Tag created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY - else - echo "Tag ${RELEASE_TAG} already exists" >> $GITHUB_STEP_SUMMARY - fi - echo "Tag: ${TAG}" >> $GITHUB_STEP_SUMMARY - - # -- STEP 7: Create or update Gitea Release -------------------------------- - - name: "Step 7: Gitea Release" - if: >- - steps.version.outputs.skip != 'true' - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - RELEASE_TAG="${{ steps.version.outputs.release_tag }}" - BRANCH="${{ steps.version.outputs.branch }}" - CLI="/tmp/moko-platform-api/cli" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - - # Reuse metadata from Step 5 - EXT_NAME="${{ steps.updates.outputs.ext_name }}" - TYPE_PREFIX="${{ steps.updates.outputs.type_prefix }}" - EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}" - [ -z "$EXT_NAME" ] && EXT_NAME="${GITEA_REPO}" - [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') - - RELEASE_NAME="${EXT_NAME} ${VERSION} (${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION})" - NOTES=$(php $CLI/release_notes.php --path . --version "$VERSION" 2>/dev/null) - [ -z "$NOTES" ] && NOTES="Release ${VERSION}" - - php $CLI/release_manage.php \ - --action create \ - --tag "$RELEASE_TAG" \ - --name "$RELEASE_NAME" \ - --body "## ${VERSION} ($(date +%Y-%m-%d))\n${NOTES}" \ - --target "$BRANCH" \ - --token "${{ secrets.GA_TOKEN }}" \ - --api-base "$API_BASE" - - echo "Release created: ${RELEASE_NAME}" >> $GITHUB_STEP_SUMMARY - - # -- STEP 8: Build package, upload, and update checksums ------------------- - - name: "Step 8: Build package and upload" - if: >- - steps.version.outputs.skip != 'true' - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - RELEASE_TAG="${{ steps.version.outputs.release_tag }}" - CLI="/tmp/moko-platform-api/cli" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - - # Build ZIP + tar.gz via CLI (handles single and multi-extension packages) - php $CLI/package_build.php --path . --version "$VERSION" --output-dir /tmp --github-output - - # Read outputs from package_build - ZIP_NAME="${{ steps.updates.outputs.type_prefix }}${{ steps.updates.outputs.ext_element }}-${VERSION}.zip" - TAR_NAME="${{ steps.updates.outputs.type_prefix }}${{ steps.updates.outputs.ext_element }}-${VERSION}.tar.gz" - - # Upload assets to release (handles dedup automatically) - php $CLI/release_manage.php \ - --action upload \ - --tag "$RELEASE_TAG" \ - --files "/tmp/${ZIP_NAME},/tmp/${TAR_NAME}" \ - --token "${{ secrets.GA_TOKEN }}" \ - --api-base "$API_BASE" - - # Regenerate updates.xml with SHA-256 from built package - SHA256_ZIP=$(sha256sum "/tmp/${ZIP_NAME}" | cut -d' ' -f1) - php $CLI/updates_xml_build.php \ - --path . \ - --version "$VERSION" \ - --stability stable \ - --sha "$SHA256_ZIP" \ - --gitea-url "${GITEA_URL}" \ - --org "${GITEA_ORG}" \ - --repo "${GITEA_REPO}" - - # Commit updated updates.xml - git add updates.xml - git commit -m "chore(release): ZIP + tar.gz for ${VERSION} [skip ci]" \ - --author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" || true - git push || true - - # Sync updates.xml to main via API (may be on version/XX branch) - GA_TOKEN="${{ secrets.GA_TOKEN }}" - API="${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}" - FILE_SHA=$(curl -sf -H "Authorization: token ${GA_TOKEN}" \ - "${API}/contents/updates.xml?ref=main" | jq -r '.sha // empty') - if [ -n "$FILE_SHA" ]; then - CONTENT=$(base64 -w0 updates.xml) - curl -sf -X PUT -H "Authorization: token ${GA_TOKEN}" \ - -H "Content-Type: application/json" \ - "${API}/contents/updates.xml" \ - -d "$(jq -n \ - --arg content "$CONTENT" \ - --arg sha "$FILE_SHA" \ - --arg msg "chore: sync updates.xml ${VERSION} [skip ci]" \ - --arg branch "main" \ - '{content: $content, sha: $sha, message: $msg, branch: $branch}' - )" > /dev/null 2>&1 \ - && echo "updates.xml synced to main via API" \ - || echo "WARNING: failed to sync updates.xml to main" - fi - - # Build release body with changelog + SHA - NOTES=$(php $CLI/release_notes.php --path . --version "$VERSION" 2>/dev/null) - SHA256_TAR="" - [ -f "/tmp/${TAR_NAME}" ] && SHA256_TAR=$(sha256sum "/tmp/${TAR_NAME}" | cut -d' ' -f1) - - BODY="## ${VERSION} ($(date +%Y-%m-%d))\n\n${NOTES}\n\n---\n\n### Checksums\n\n" - BODY="${BODY}| File | SHA-256 |\n|------|--------|\n" - BODY="${BODY}| \`${ZIP_NAME}\` | \`${SHA256_ZIP}\` |\n" - [ -n "$SHA256_TAR" ] && BODY="${BODY}| \`${TAR_NAME}\` | \`${SHA256_TAR}\` |\n" - - printf '%b' "$BODY" > /tmp/release_body.md - php $CLI/release_manage.php \ - --action update-body \ - --tag "$RELEASE_TAG" \ - --body-file /tmp/release_body.md \ - --token "${{ secrets.GA_TOKEN }}" \ - --api-base "$API_BASE" - - echo "### Packages" >> $GITHUB_STEP_SUMMARY - echo "| Package | SHA-256 |" >> $GITHUB_STEP_SUMMARY - echo "|---------|---------|" >> $GITHUB_STEP_SUMMARY - echo "| \`${ZIP_NAME}\` | \`${SHA256_ZIP}\` |" >> $GITHUB_STEP_SUMMARY - echo "| \`${TAR_NAME}\` | \`${SHA256_TAR}\` |" >> $GITHUB_STEP_SUMMARY - - # -- STEP 9: Mirror to GitHub (stable only) -------------------------------- - - name: "Step 9: Mirror release to GitHub" - if: >- - steps.version.outputs.skip != 'true' && - steps.version.outputs.stability == 'stable' && - secrets.GH_TOKEN != '' - continue-on-error: true - env: - GH_TOKEN: ${{ secrets.GH_TOKEN }} - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - RELEASE_TAG="${{ steps.version.outputs.release_tag }}" - MAJOR="${{ steps.version.outputs.major }}" - BRANCH="${{ steps.version.outputs.branch }}" - GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" - - NOTES=$(php /tmp/moko-platform-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null || true) - [ -z "$NOTES" ] && NOTES="Release ${VERSION}" - echo "$NOTES" > /tmp/release_notes.md - - EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/tags/$RELEASE_TAG" 2>/dev/null | jq -r ".tag_name // empty" || true) - - if [ -z "$EXISTING" ]; then - gh release create "$RELEASE_TAG" \ - --repo "$GH_REPO" \ - --title "v${MAJOR} (latest: ${VERSION})" \ - --notes-file /tmp/release_notes.md \ - --target "$BRANCH" || true - else - gh release edit "$RELEASE_TAG" \ - --repo "$GH_REPO" \ - --title "v${MAJOR} (latest: ${VERSION})" || true - fi - - # Upload assets to GitHub mirror - for PKG in /tmp/${EXT_ELEMENT:-pkg}-${VERSION}.*; do - if [ -f "$PKG" ]; then - _RELID=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/tags/$RELEASE_TAG" 2>/dev/null | jq -r ".id // empty") - [ -n "$_RELID" ] && curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" -H "Content-Type: application/octet-stream" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/${_RELID}/assets?name=$(basename $PKG)" --data-binary "@$PKG" > /dev/null 2>&1 || true - fi - done - echo "GitHub mirror updated: ${GH_REPO} ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY - - # -- STEP 10: Sync main branch to GitHub mirror ---------------------------- - - name: "Step 10: Push main to GitHub mirror" - if: >- - steps.version.outputs.skip != 'true' && - secrets.GH_TOKEN != '' - continue-on-error: true - run: | - GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" - GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1) - GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2) - git remote add github "https://x-access-token:${{ secrets.GH_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \ - git remote set-url github "https://x-access-token:${{ secrets.GH_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" - git fetch origin main --depth=1 - git push github origin/main:refs/heads/main --force 2>/dev/null \ - && echo "main branch pushed to GitHub mirror" \ - || echo "WARNING: GitHub mirror push failed" - - # -- Clean up lesser pre-releases (cascade) --------------------------------- - - name: "Delete lesser pre-release channels" - if: steps.version.outputs.skip != 'true' - continue-on-error: true - run: | - php /tmp/moko-platform-api/cli/release_cascade.php \ - --stability stable \ - --token "${{ secrets.GA_TOKEN }}" \ - --api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - - # -- STEP 11: Reset dev branch from main ------------------------------------ - - name: "Step 11: Delete and recreate dev branch from main" - if: steps.version.outputs.skip != 'true' - continue-on-error: true - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.GA_TOKEN }}" - - # Delete dev branch - curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ - "${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch" - - # Recreate dev from main (now includes version bump + changelog promotion) - curl -sf -X POST -H "Authorization: token ${TOKEN}" \ - -H "Content-Type: application/json" \ - "${API_BASE}/branches" \ - -d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main" - - echo "Dev branch reset from main (keeps dev ahead after release)" >> $GITHUB_STEP_SUMMARY - - - # -- Dolibarr post-release: Reset dev version ----------------------------- - - name: "Dolibarr: Reset dev version" - if: >- - steps.version.outputs.skip != 'true' && - steps.platform.outputs.platform == 'dolibarr' && - steps.platform.outputs.mod_file != '' - continue-on-error: true - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.GA_TOKEN }}" - MOD_FILE="${{ steps.platform.outputs.mod_file }}" - ENCODED_PATH=$(echo "$MOD_FILE" | sed 's|^\./||' | python3 -c "import sys,urllib.parse; print(urllib.parse.quote(sys.stdin.read().strip()))") - FILE_RESP=$(curl -sf -H "Authorization: token ${TOKEN}" "${API_BASE}/contents/${ENCODED_PATH}?ref=dev" 2>/dev/null || true) - FILE_SHA=$(echo "$FILE_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true) - FILE_CONTENT=$(echo "$FILE_RESP" | python3 -c "import sys,json,base64; print(base64.b64decode(json.load(sys.stdin).get('content','')).decode())" 2>/dev/null || true) - if [ -n "$FILE_SHA" ] && [ -n "$FILE_CONTENT" ]; then - UPDATED=$(echo "$FILE_CONTENT" | sed "s/\$this->version = '[^']*'/\$this->version = 'development'/") - ENCODED=$(echo "$UPDATED" | base64 -w0) - curl -sf -X PUT -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/contents/${ENCODED_PATH}" \ - -d "$(jq -n --arg content \"$ENCODED\" --arg sha \"$FILE_SHA\" --arg msg \"chore(version): reset dev version [skip ci]\" --arg branch \"dev\" '{content:$content,sha:$sha,message:$msg,branch:$branch}')" > /dev/null 2>&1 || true - fi - - # -- Summary -------------------------------------------------------------- - - name: Pipeline Summary - if: always() - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - PLATFORM="${{ steps.platform.outputs.platform }}" - if [ "${{ steps.version.outputs.skip }}" = "true" ]; then - echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY - echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY - elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then - echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY - else - echo "" >> $GITHUB_STEP_SUMMARY - echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY - echo "|------|--------|" >> $GITHUB_STEP_SUMMARY - echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY - fi +# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: moko-platform.Release +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform +# PATH: /templates/workflows/universal/auto-release.yml.template +# VERSION: 05.00.00 +# BRIEF: Universal build & release � detects platform from manifest.xml +# +# +========================================================================+ +# | UNIVERSAL BUILD & RELEASE PIPELINE | +# +========================================================================+ +# | | +# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. | +# | | +# | Platform-specific: | +# | joomla: XML manifest, updates.xml, type-prefixed packages | +# | dolibarr: mod*.class.php, update.txt, dev version reset | +# | generic: README-only, no update stream | +# | | +# +========================================================================+ + +name: "Universal: Build & Release" + +on: + pull_request: + types: [opened, closed] + branches: + - main + paths: + - 'src/**' + - 'htdocs/**' + workflow_dispatch: + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + +permissions: + contents: write + +jobs: + # ── Draft PR → Promote highest pre-release to RC ───────────────────────────── + promote-rc: + name: Promote Pre-Release to RC + runs-on: release + if: >- + github.event.action == 'opened' && + github.event.pull_request.draft == true + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.GA_TOKEN }} + fetch-depth: 1 + + - name: Setup moko-platform tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + run: | + if ! command -v composer &> /dev/null; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ + /tmp/moko-platform-api + cd /tmp/moko-platform-api + composer install --no-dev --no-interaction --quiet + + - name: Promote to release-candidate + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php /tmp/moko-platform-api/cli/release_promote.php \ + --from auto --to release-candidate \ + --token "${{ secrets.GA_TOKEN }}" \ + --api-base "${API_BASE}" \ + --branch "${{ github.event.pull_request.head.ref }}" + + - name: Cascade lesser channels + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php /tmp/moko-platform-api/cli/release_cascade.php \ + --stability release-candidate \ + --token "${{ secrets.GA_TOKEN }}" \ + --api-base "${API_BASE}" + + - name: Summary + if: always() + run: | + echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY + echo "Draft PR opened — promoted highest pre-release to RC" >> $GITHUB_STEP_SUMMARY + + # ── Merged PR → Build & Release (or promote RC to stable) ──────────────────── + release: + name: Build & Release Pipeline + runs-on: release + if: >- + github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.GA_TOKEN }} + fetch-depth: 0 + + - name: Setup moko-platform tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN }}"}}' + run: | + # Ensure PHP + Composer are available + if ! command -v composer &> /dev/null; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ + /tmp/moko-platform-api + cd /tmp/moko-platform-api + composer install --no-dev --no-interaction --quiet + + + # -- PLATFORM DETECTION --------------------------------------------------- + - name: Detect platform + id: platform + run: | + php /tmp/moko-platform-api/cli/manifest_read.php --path . --github-output + MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true) + MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1 || true) + echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT" + echo "mod_file=${MOD_FILE}" >> "$GITHUB_OUTPUT" + + - name: "Step 1: Read version" + id: version + run: | + VERSION=$(php /tmp/moko-platform-api/cli/version_read.php --path .) + if [ -z "$VERSION" ]; then + echo "::error::No VERSION in README.md" + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + MAJOR=$(echo "$VERSION" | cut -d. -f1) + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "release_tag=stable" >> "$GITHUB_OUTPUT" + echo "skip=false" >> "$GITHUB_OUTPUT" + echo "branch=main" >> "$GITHUB_OUTPUT" + + # -- CHECK FOR RC PROMOTION ------------------------------------------------ + - name: "Check for RC release" + id: rc + if: steps.version.outputs.skip != 'true' + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + RC_JSON=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/releases/tags/release-candidate" 2>/dev/null || echo "{}") + RC_ID=$(echo "$RC_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('id',''))" 2>/dev/null || true) + + if [ -n "$RC_ID" ] && [ "$RC_ID" != "None" ] && [ "$RC_ID" != "" ]; then + echo "promote=true" >> "$GITHUB_OUTPUT" + echo "release_id=${RC_ID}" >> "$GITHUB_OUTPUT" + echo "::notice::RC release found (id: ${RC_ID}) — will promote to stable" + else + echo "promote=false" >> "$GITHUB_OUTPUT" + echo "::notice::No RC release — full build pipeline" + fi + + - name: "Step 1b: Bump version" + id: bump + if: >- + steps.version.outputs.skip != 'true' && + steps.rc.outputs.promote != 'true' + run: | + MOKO_API="/tmp/moko-platform-api/cli" + BUMP=$(php ${MOKO_API}/version_bump.php --path . --minor) + VERSION=$(echo "$BUMP" | grep -oP '\d{2}\.\d{2}\.\d{2}$' || true) + [ -z "$VERSION" ] && VERSION=$(php ${MOKO_API}/version_read.php --path .) + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "Bumped to: ${VERSION}" + + - name: Check if already released + if: steps.version.outputs.skip != 'true' + id: check + run: | + TAG="${{ steps.version.outputs.release_tag }}" + BRANCH="${{ steps.version.outputs.branch }}" + + TAG_EXISTS=false + BRANCH_EXISTS=false + + git rev-parse "$TAG" >/dev/null 2>&1 && TAG_EXISTS=true + git ls-remote --heads origin "$BRANCH" 2>/dev/null | grep -q "$BRANCH" && BRANCH_EXISTS=true + + echo "tag_exists=$TAG_EXISTS" >> "$GITHUB_OUTPUT" + echo "branch_exists=$BRANCH_EXISTS" >> "$GITHUB_OUTPUT" + + # Tag and branch may persist across patch releases — never skip + echo "already_released=false" >> "$GITHUB_OUTPUT" + + # -- SANITY CHECKS ------------------------------------------------------- + - name: "Sanity: Pre-release validation" + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.already_released != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + ERRORS=0 + + PLATFORM="${{ steps.platform.outputs.platform }}" + MANIFEST="${{ steps.platform.outputs.manifest }}" + MOD_FILE="${{ steps.platform.outputs.mod_file }}" + echo "## Pre-Release Sanity Checks (${PLATFORM})" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # -- Version drift check (must pass before release) -------- + README_VER=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1) + if [ "$README_VER" != "$VERSION" ]; then + echo "- Version drift: README says \`${README_VER}\` but releasing \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + else + echo "- Version consistent: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + fi + + # Check CHANGELOG version matches + CL_VER=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' CHANGELOG.md 2>/dev/null | head -1) + if [ -n "$CL_VER" ] && [ "$CL_VER" != "$VERSION" ]; then + echo "- CHANGELOG drift: \`${CL_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + fi + + # Check composer.json version if present + if [ -f "composer.json" ]; then + COMP_VER=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' composer.json 2>/dev/null | head -1) + if [ -n "$COMP_VER" ] && [ "$COMP_VER" != "$VERSION" ]; then + echo "- composer.json drift: \`${COMP_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + fi + fi + + # Common checks + if [ ! -f "LICENSE" ]; then + echo "- Missing LICENSE file" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + else + echo "- LICENSE present" >> $GITHUB_STEP_SUMMARY + fi + + if [ ! -d "src" ] && [ ! -d "htdocs" ]; then + echo "- Warning: No src/ or htdocs/ directory" >> $GITHUB_STEP_SUMMARY + else + echo "- Source directory present" >> $GITHUB_STEP_SUMMARY + fi + + # -- Platform-specific checks -------- + case "$PLATFORM" in + joomla) + if [ -n "$MANIFEST" ]; then + XML_VER=$(sed -n 's/.*<version>\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1) + if [ -n "$XML_VER" ] && [ "$XML_VER" != "$VERSION" ]; then + echo "- Manifest drift: \`${XML_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + else + echo "- Manifest version: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + fi + TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" 2>/dev/null) + echo "- Extension type: ${TYPE:-unknown}" >> $GITHUB_STEP_SUMMARY + else + echo "- No Joomla XML manifest (WaaS site)" >> $GITHUB_STEP_SUMMARY + fi ;; + dolibarr) + if [ -n "$MOD_FILE" ]; then + MOD_VER=$(sed -n "s/.*\\\$this->version = '\([^']*\)'.*/\1/p" "$MOD_FILE" 2>/dev/null | head -1) + if [ -n "$MOD_VER" ] && [ "$MOD_VER" != "$VERSION" ]; then + echo "- Module drift: \`${MOD_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + else + echo "- Module version: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + fi + else + echo "- No mod*.class.php found" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + fi + if [ ! -f "update.txt" ]; then + echo "- Missing update.txt" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + fi ;; + *) echo "- Generic platform � no manifest checks" >> $GITHUB_STEP_SUMMARY ;; + esac + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "$ERRORS" -gt 0 ]; then + echo "**${ERRORS} error(s) — release may be incomplete**" >> $GITHUB_STEP_SUMMARY + else + echo "**All sanity checks passed**" >> $GITHUB_STEP_SUMMARY + fi + + # -- STEP 2: Create or update version/XX.YY archive branch --------------- + # Always runs — every version change on main archives to version/XX.YY + - name: "Step 2: Version archive branch" + if: steps.check.outputs.already_released != 'true' + run: | + BRANCH="${{ steps.version.outputs.branch }}" + IS_MINOR="${{ steps.version.outputs.is_minor }}" + PATCH="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + PATCH_NUM=$(echo "$PATCH" | awk -F. '{print $3}') + + # Check if branch exists + if git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then + git push origin HEAD:"$BRANCH" --force + echo "Updated archive branch: ${BRANCH} (patch ${PATCH_NUM})" >> $GITHUB_STEP_SUMMARY + else + git checkout -b "$BRANCH" 2>/dev/null || git checkout "$BRANCH" + git push origin "$BRANCH" --force + echo "Created archive branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY + fi + + # -- STEP 3: Set platform version ---------------------------------------- + - name: "Step 3: Set platform version" + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.already_released != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + php /tmp/moko-platform-api/cli/version_set_platform.php \ + --path . --version "$VERSION" --branch main + + # -- STEP 4: Update version badges ---------------------------------------- + - name: "Step 4: Update version badges" + if: steps.version.outputs.skip != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + php /tmp/moko-platform-api/cli/badge_update.php --path . --version "${VERSION}" 2>/dev/null || true + php /tmp/moko-platform-api/cli/version_check.php --path . --fix 2>/dev/null || true + + - name: "Step 5: Write update stream" + if: >- + steps.version.outputs.skip != 'true' && + steps.platform.outputs.platform == 'joomla' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + + # Fetch latest updates.xml from main so preserve logic has all channels + GA_TOKEN="${{ secrets.GA_TOKEN }}" + API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" + curl -sf -H "Authorization: token ${GA_TOKEN}" \ + "${API}/contents/updates.xml?ref=main" 2>/dev/null | \ + python3 -c "import sys,json,base64; print(base64.b64decode(json.load(sys.stdin)['content']).decode())" \ + > updates.xml 2>/dev/null || true + + php /tmp/moko-platform-api/cli/updates_xml_build.php \ + --path . --version "${VERSION}" --stability stable \ + --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \ + --github-output + + - name: Commit release changes + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.already_released != 'true' + run: | + if git diff --quiet && git diff --cached --quiet; then + echo "No changes to commit" + exit 0 + fi + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + # Set push URL with token for branch-protected repos + git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + git add -A + git commit -m "chore(release): build ${VERSION} [skip ci]" \ + --author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" + git push -u origin HEAD + + # -- STEP 6: Create tag --------------------------------------------------- + - name: "Step 6: Create git tag" + if: >- + steps.version.outputs.skip != 'true' + run: | + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + # Only create the major release tag if it doesn't exist yet + if ! git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then + git tag "$RELEASE_TAG" + git push origin "$RELEASE_TAG" + echo "Tag created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY + else + echo "Tag ${RELEASE_TAG} already exists" >> $GITHUB_STEP_SUMMARY + fi + echo "Tag: ${TAG}" >> $GITHUB_STEP_SUMMARY + + # -- STEP 7a: Promote RC to stable (skip build) ---------------------------- + - name: "Step 7a: Promote RC to stable" + if: >- + steps.version.outputs.skip != 'true' && + steps.rc.outputs.promote == 'true' + run: | + VERSION="${{ steps.version.outputs.version }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php /tmp/moko-platform-api/cli/release_promote.php \ + --from release-candidate --to stable \ + --token "${{ secrets.GA_TOKEN }}" \ + --api-base "${API_BASE}" \ + --path . --branch main + echo "Promoted RC → stable (${VERSION})" >> $GITHUB_STEP_SUMMARY + + # -- STEP 7b: Create or update Gitea Release (full build path) ------------- + - name: "Step 7b: Gitea Release" + if: >- + steps.version.outputs.skip != 'true' && + steps.rc.outputs.promote != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + BRANCH="${{ steps.version.outputs.branch }}" + MAJOR="${{ steps.version.outputs.major }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + + # Reuse metadata from Step 5 (single source of truth) + EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}" + EXT_NAME="${{ steps.updates.outputs.ext_name }}" + EXT_TYPE="${{ steps.updates.outputs.ext_type }}" + EXT_FOLDER="${{ steps.updates.outputs.ext_folder }}" + + # Fallbacks if Step 5 was skipped + if [ -z "$EXT_ELEMENT" ]; then + EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') + fi + [ -z "$EXT_NAME" ] && EXT_NAME="${GITEA_REPO}" + + NOTES=$(php /tmp/moko-platform-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null) + [ -z "$NOTES" ] && NOTES="Release ${VERSION}" + + # Build release name: "Pretty Name VERSION (type_element-VERSION)" + # Strip existing type prefix to prevent duplication + EXT_ELEMENT=$(echo "$EXT_ELEMENT" | sed -E 's/^(pkg_|com_|mod_|plg_[a-z]+_|tpl_|lib_)//') + TYPE_PREFIX="" + case "${EXT_TYPE}" in + plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;; + module) TYPE_PREFIX="mod_" ;; + component) TYPE_PREFIX="com_" ;; + template) TYPE_PREFIX="tpl_" ;; + library) TYPE_PREFIX="lib_" ;; + package) TYPE_PREFIX="pkg_" ;; + esac + RELEASE_NAME="${EXT_NAME} ${VERSION} (${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION})" + + # Delete existing release if present (overwrite, not append) + EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true) + EXISTING_ID=$(echo "$EXISTING" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('id',''))" 2>/dev/null || true) + + if [ -n "$EXISTING_ID" ]; then + curl -sS -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/releases/${EXISTING_ID}" 2>/dev/null || true + curl -sS -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/tags/${RELEASE_TAG}" 2>/dev/null || true + echo "Deleted previous stable release (id: ${EXISTING_ID})" + fi + + # Create fresh release + curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + -H "Content-Type: application/json" \ + "${API_BASE}/releases" \ + -d "$(python3 -c "import json; print(json.dumps({ + 'tag_name': '${RELEASE_TAG}', + 'name': '${RELEASE_NAME}', + 'body': '''## ${VERSION} ($(date +%Y-%m-%d))\n${NOTES}''', + 'target_commitish': '${BRANCH}' + }))")" + echo "Release created: ${RELEASE_NAME}" >> $GITHUB_STEP_SUMMARY + + # -- STEP 8: Build Joomla install ZIP + SHA-256 checksum ------------------ + - name: "Step 8: Build package and update checksum" + if: >- + steps.version.outputs.skip != 'true' && + steps.rc.outputs.promote != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + REPO="${{ github.repository }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + + # All ZIPs upload to the major release tag (vXX) + RELEASE_JSON=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true) + RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) + if [ -z "$RELEASE_ID" ]; then + echo "No release ${RELEASE_TAG} found — skipping ZIP upload" + exit 0 + fi + + # Find extension element name from manifest + MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true) + [ -z "$MANIFEST" ] && exit 0 + + # Reuse element from Step 5, with same fallback chain + EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}" + if [ -z "$EXT_ELEMENT" ]; then + EXT_ELEMENT=$(sed -n 's/.*<element>\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1) + [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(sed -n 's/.*plugin="\([^"]*\)".*/\1/p' "$MANIFEST" 2>/dev/null | head -1) + [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]') + [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') + fi + # ZIP name: type_folder_element-VERSION (e.g. plg_system_mokojgdpc-01.01.00.zip) + EXT_TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + EXT_FOLDER=$(sed -n 's/.*<extension[^>]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + # For packages, prefer <packagename> over filename-derived element + if [ "$EXT_TYPE" = "package" ]; then + PKG_NAME=$(sed -n 's/.*<packagename>\([^<]*\)<\/packagename>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1) + [ -n "$PKG_NAME" ] && EXT_ELEMENT="$PKG_NAME" + fi + # Strip existing type prefix to prevent duplication (e.g. pkg_mokowaas → mokowaas) + EXT_ELEMENT=$(echo "$EXT_ELEMENT" | sed -E 's/^(pkg_|com_|mod_|plg_[a-z]+_|tpl_|lib_)//') + TYPE_PREFIX="" + case "${EXT_TYPE}" in + plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;; + module) TYPE_PREFIX="mod_" ;; + component) TYPE_PREFIX="com_" ;; + template) TYPE_PREFIX="tpl_" ;; + library) TYPE_PREFIX="lib_" ;; + package) TYPE_PREFIX="pkg_" ;; + esac + ZIP_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip" + TAR_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.tar.gz" + + # -- Build install packages from src/ ---------------------------- + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" + [ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/"; exit 0; } + + # ZIP package (type-aware via moko-platform PHP API) + php /tmp/moko-platform-api/cli/joomla_build.php --path . --version "${VERSION}" --output /tmp + # Match the expected ZIP_NAME for upload + BUILT_ZIP=$(ls /tmp/${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip 2>/dev/null | head -1 || true) + if [ -n "$BUILT_ZIP" ] && [ "$BUILT_ZIP" != "/tmp/${ZIP_NAME}" ]; then + mv "$BUILT_ZIP" "/tmp/${ZIP_NAME}" + fi + + # tar.gz package (flat source archive) + tar -czf "/tmp/${TAR_NAME}" -C "$SOURCE_DIR" --exclude='.ftpignore' --exclude='sftp-config*' --exclude='*.ppk' --exclude='*.pem' --exclude='*.key' --exclude='.env*' . + + ZIP_SIZE=$(stat -c%s "/tmp/${ZIP_NAME}" 2>/dev/null || stat -f%z "/tmp/${ZIP_NAME}" 2>/dev/null || echo "unknown") + TAR_SIZE=$(stat -c%s "/tmp/${TAR_NAME}" 2>/dev/null || stat -f%z "/tmp/${TAR_NAME}" 2>/dev/null || echo "unknown") + + # -- Calculate SHA-256 for both ---------------------------------- + SHA256_ZIP=$(sha256sum "/tmp/${ZIP_NAME}" | cut -d' ' -f1) + SHA256_TAR=$(sha256sum "/tmp/${TAR_NAME}" | cut -d' ' -f1) + + # -- Get existing assets for cleanup -------------------------------- + ASSETS=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/releases/${RELEASE_ID}/assets" 2>/dev/null || echo "[]") + + # -- Create per-file .sha256 checksum files ------------------------- + echo "${SHA256_ZIP} ${ZIP_NAME}" > "/tmp/${ZIP_NAME}.sha256" + echo "${SHA256_TAR} ${TAR_NAME}" > "/tmp/${TAR_NAME}.sha256" + + # -- Upload packages + checksums to release tag -------------------- + for ASSET in "${ZIP_NAME}" "${TAR_NAME}" "${ZIP_NAME}.sha256" "${TAR_NAME}.sha256"; do + [ ! -f "/tmp/${ASSET}" ] && continue + # Delete existing asset with same name + ASSET_ID=$(echo "$ASSETS" | python3 -c " + import sys,json + assets = json.load(sys.stdin) + for a in assets: + if a['name'] == '${ASSET}': + print(a['id']); break + " 2>/dev/null || true) + [ -n "$ASSET_ID" ] && curl -sf -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/releases/${RELEASE_ID}/assets/${ASSET_ID}" 2>/dev/null || true + # Upload + curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + -H "Content-Type: application/octet-stream" \ + --data-binary @"/tmp/${ASSET}" \ + "${API_BASE}/releases/${RELEASE_ID}/assets?name=${ASSET}" > /dev/null 2>&1 || true + done + + # updates.xml already handled by Step 5 (updates_xml_build.php with preserve logic) + + echo "### Packages" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Package | Size | SHA-256 |" >> $GITHUB_STEP_SUMMARY + echo "|---------|------|---------|" >> $GITHUB_STEP_SUMMARY + echo "| \`${ZIP_NAME}\` | ${ZIP_SIZE} | \`${SHA256_ZIP}\` |" >> $GITHUB_STEP_SUMMARY + echo "| \`${TAR_NAME}\` | ${TAR_SIZE} | \`${SHA256_TAR}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Release | \`${RELEASE_TAG}\` | |" >> $GITHUB_STEP_SUMMARY + echo "| Download | [${ZIP_NAME}](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${ZIP_NAME}) |" >> $GITHUB_STEP_SUMMARY + + # -- STEP 8b: Update release description with changelog ---------------------- + - name: "Step 8b: Update release body" + if: steps.version.outputs.skip != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + MOKO_CLI="/tmp/moko-platform-api/cli" + + php ${MOKO_CLI}/release_body_update.php \ + --path . --version "${VERSION}" --tag "${RELEASE_TAG}" \ + --token "${{ secrets.GA_TOKEN }}" \ + --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \ + 2>/dev/null || { + # Fallback: simple body update if CLI not available + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null | \ + python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) + if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "None" ]; then + BODY="## ${VERSION} ($(date +%Y-%m-%d))\n\nChecksum files attached as \`*.sha256\` assets." + curl -sf -X PATCH -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + -H "Content-Type: application/json" \ + "${API_BASE}/releases/${RELEASE_ID}" \ + -d "{\"body\":\"${BODY}\"}" > /dev/null 2>&1 + fi + } + echo "Release body updated" >> $GITHUB_STEP_SUMMARY + + # -- STEP 9: Mirror to GitHub (stable only) -------------------------------- + - name: "Step 9: Mirror release to GitHub" + if: >- + steps.version.outputs.skip != 'true' && + steps.version.outputs.stability == 'stable' && + secrets.GH_TOKEN != '' + continue-on-error: true + env: + GH_TOKEN: ${{ secrets.GH_TOKEN }} + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + MAJOR="${{ steps.version.outputs.major }}" + BRANCH="${{ steps.version.outputs.branch }}" + GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" + + NOTES=$(php /tmp/moko-platform-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null || true) + [ -z "$NOTES" ] && NOTES="Release ${VERSION}" + echo "$NOTES" > /tmp/release_notes.md + + EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/tags/$RELEASE_TAG" 2>/dev/null | jq -r ".tag_name // empty" || true) + + if [ -z "$EXISTING" ]; then + gh release create "$RELEASE_TAG" \ + --repo "$GH_REPO" \ + --title "v${MAJOR} (latest: ${VERSION})" \ + --notes-file /tmp/release_notes.md \ + --target "$BRANCH" || true + else + gh release edit "$RELEASE_TAG" \ + --repo "$GH_REPO" \ + --title "v${MAJOR} (latest: ${VERSION})" || true + fi + + # Upload assets to GitHub mirror + for PKG in /tmp/${EXT_ELEMENT:-pkg}-${VERSION}.*; do + if [ -f "$PKG" ]; then + _RELID=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/tags/$RELEASE_TAG" 2>/dev/null | jq -r ".id // empty") + [ -n "$_RELID" ] && curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" -H "Content-Type: application/octet-stream" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/${_RELID}/assets?name=$(basename $PKG)" --data-binary "@$PKG" > /dev/null 2>&1 || true + fi + done + echo "GitHub mirror updated: ${GH_REPO} ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY + + # -- STEP 10: Sync main branch to GitHub mirror ---------------------------- + - name: "Step 10: Push main to GitHub mirror" + if: >- + steps.version.outputs.skip != 'true' && + secrets.GH_TOKEN != '' + continue-on-error: true + run: | + GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" + GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1) + GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2) + git remote add github "https://x-access-token:${{ secrets.GH_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \ + git remote set-url github "https://x-access-token:${{ secrets.GH_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" + git fetch origin main --depth=1 + git push github origin/main:refs/heads/main --force 2>/dev/null \ + && echo "main branch pushed to GitHub mirror" \ + || echo "WARNING: GitHub mirror push failed" + + # -- Clean up lesser pre-releases (cascade) --------------------------------- + # stable → deletes all | rc → beta,alpha,dev | beta → alpha,dev | alpha → dev + - name: "Delete lesser pre-release channels" + continue-on-error: true + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php /tmp/moko-platform-api/cli/release_cascade.php \ + --stability stable \ + --version "${VERSION}" \ + --token "${{ secrets.GA_TOKEN }}" \ + --api-base "${API_BASE}" 2>/dev/null || true + + - name: "Step 11: Delete and recreate dev branch from main" + if: steps.version.outputs.skip != 'true' + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.GA_TOKEN }}" + + # Delete dev branch + curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch" + + # Recreate dev from main (now includes version bump + changelog promotion) + curl -sf -X POST -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/json" \ + "${API_BASE}/branches" \ + -d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main" + + echo "Dev branch reset from main (keeps dev ahead after release)" >> $GITHUB_STEP_SUMMARY + + + # -- Dolibarr post-release: Reset dev version ----------------------------- + - name: "Dolibarr: Reset dev version" + if: >- + steps.version.outputs.skip != 'true' && + steps.platform.outputs.platform == 'dolibarr' && + steps.platform.outputs.mod_file != '' + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.GA_TOKEN }}" + MOD_FILE="${{ steps.platform.outputs.mod_file }}" + ENCODED_PATH=$(echo "$MOD_FILE" | sed 's|^\./||' | python3 -c "import sys,urllib.parse; print(urllib.parse.quote(sys.stdin.read().strip()))") + FILE_RESP=$(curl -sf -H "Authorization: token ${TOKEN}" "${API_BASE}/contents/${ENCODED_PATH}?ref=dev" 2>/dev/null || true) + FILE_SHA=$(echo "$FILE_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true) + FILE_CONTENT=$(echo "$FILE_RESP" | python3 -c "import sys,json,base64; print(base64.b64decode(json.load(sys.stdin).get('content','')).decode())" 2>/dev/null || true) + if [ -n "$FILE_SHA" ] && [ -n "$FILE_CONTENT" ]; then + UPDATED=$(echo "$FILE_CONTENT" | sed "s/\$this->version = '[^']*'/\$this->version = 'development'/") + ENCODED=$(echo "$UPDATED" | base64 -w0) + curl -sf -X PUT -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/contents/${ENCODED_PATH}" \ + -d "$(jq -n --arg content \"$ENCODED\" --arg sha \"$FILE_SHA\" --arg msg \"chore(version): reset dev version [skip ci]\" --arg branch \"dev\" '{content:$content,sha:$sha,message:$msg,branch:$branch}')" > /dev/null 2>&1 || true + fi + + # -- Summary -------------------------------------------------------------- + - name: Pipeline Summary + if: always() + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + PLATFORM="${{ steps.platform.outputs.platform }}" + if [ "${{ steps.version.outputs.skip }}" = "true" ]; then + echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY + echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY + elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then + echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY + echo "|------|--------|" >> $GITHUB_STEP_SUMMARY + echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY + fi From bbda9318d8a07657915c869c4775b84ae597d5c0 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 26 May 2026 19:36:22 +0000 Subject: [PATCH 036/132] chore(ci): update auto-release.yml from moko-platform [skip ci] --- .mokogitea/workflows/auto-release.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml index 9eb98c8..eca25d8 100644 --- a/.mokogitea/workflows/auto-release.yml +++ b/.mokogitea/workflows/auto-release.yml @@ -30,9 +30,6 @@ on: types: [opened, closed] branches: - main - paths: - - 'src/**' - - 'htdocs/**' workflow_dispatch: env: From 23de6dcb07e9a7b309613e2914a412219f4f992f Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 26 May 2026 19:36:22 +0000 Subject: [PATCH 037/132] chore(ci): update pre-release.yml from moko-platform [skip ci] --- .mokogitea/workflows/pre-release.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml index b90d2c8..698251d 100644 --- a/.mokogitea/workflows/pre-release.yml +++ b/.mokogitea/workflows/pre-release.yml @@ -17,9 +17,6 @@ on: types: [closed] branches: - dev - paths: - - 'src/**' - - 'htdocs/**' workflow_dispatch: inputs: stability: From 105d2820774eefbd141018bab7acd0c83b665d98 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 26 May 2026 19:54:38 +0000 Subject: [PATCH 038/132] fix(ci): use release_package.php for Joomla package builds [skip ci] --- .mokogitea/workflows/update-server.yml | 1061 +++++++++++++----------- 1 file changed, 597 insertions(+), 464 deletions(-) diff --git a/.mokogitea/workflows/update-server.yml b/.mokogitea/workflows/update-server.yml index 6e617f6..fd6407f 100644 --- a/.mokogitea/workflows/update-server.yml +++ b/.mokogitea/workflows/update-server.yml @@ -1,464 +1,597 @@ -# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: MokoStandards.Joomla -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API -# PATH: /templates/workflows/joomla/update-server.yml.template -# VERSION: 04.06.00 -# BRIEF: Update Joomla update server XML feed with stable/rc/dev entries -# -# Writes updates.xml with multiple <update> entries: -# - <tag>stable</tag> on push to main (from auto-release) -# - <tag>rc</tag> on push to rc/** -# - <tag>development</tag> on push to dev or dev/** -# -# Joomla filters by user's "Minimum Stability" setting. - -name: "Joomla: Update Server" - -on: - push: - branches: - - 'dev' - - 'dev/**' - - 'alpha/**' - - 'beta/**' - - 'rc/**' - paths: - - 'src/**' - - 'htdocs/**' - pull_request: - types: [closed] - branches: - - 'dev' - - 'dev/**' - - 'alpha/**' - - 'beta/**' - - 'rc/**' - paths: - - 'src/**' - - 'htdocs/**' - workflow_dispatch: - inputs: - stability: - description: 'Stability tag' - required: true - default: 'development' - type: choice - options: - - development - - alpha - - beta - - rc - - stable - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} - GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} - -permissions: - contents: write - -jobs: - update-xml: - name: Update updates.xml - runs-on: release - if: >- - github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || github.event_name == 'push' - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - token: ${{ secrets.GA_TOKEN }} - fetch-depth: 0 - - - name: Setup MokoStandards tools - env: - MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }} - MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting - COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.GA_TOKEN }}"}}}' - run: | - if ! command -v composer &> /dev/null; then - sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 - fi - git clone --depth 1 --branch main --quiet \ - "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \ - /tmp/mokostandards-api 2>/dev/null || true - if [ -d "/tmp/mokostandards-api" ] && [ -f "/tmp/mokostandards-api/composer.json" ]; then - cd /tmp/mokostandards-api && composer install --no-dev --no-interaction --quiet 2>/dev/null || true - fi - - - name: Generate updates.xml entry - id: update - run: | - BRANCH="${{ github.ref_name }}" - REPO="${{ github.repository }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - VERSION=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null || echo "0.0.0") - - # Auto-bump patch on all branches (dev, alpha, beta, rc) - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - BUMPED=$(php /tmp/mokostandards-api/cli/version_bump.php --path . 2>/dev/null || true) - if [ -n "$BUMPED" ]; then - VERSION=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null || echo "$VERSION") - git add -A - git commit -m "chore(version): auto-bump patch ${VERSION} [skip ci]" \ - --author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" 2>/dev/null || true - git push 2>/dev/null || true - fi - - # Determine stability from branch or input - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - STABILITY="${{ inputs.stability }}" - elif [[ "$BRANCH" == rc/* ]]; then - STABILITY="rc" - elif [[ "$BRANCH" == beta/* ]]; then - STABILITY="beta" - elif [[ "$BRANCH" == alpha/* ]]; then - STABILITY="alpha" - elif [[ "$BRANCH" == dev/* ]] || [[ "$BRANCH" == "dev" ]]; then - STABILITY="development" - else - STABILITY="stable" - fi - - echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT" - - # Parse manifest (portable — no grep -P) - MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "./build/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1) - if [ -z "$MANIFEST" ]; then - echo "No Joomla manifest found — skipping" - exit 0 - fi - - # Extract fields using sed (works on all runners) - EXT_NAME=$(sed -n 's/.*<name>\([^<]*\)<\/name>.*/\1/p' "$MANIFEST" | head -1) - EXT_TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) - EXT_ELEMENT=$(sed -n 's/.*<element>\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" | head -1) - EXT_CLIENT=$(sed -n 's/.*<extension[^>]*client="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) - EXT_FOLDER=$(sed -n 's/.*<extension[^>]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) - EXT_VERSION=$(sed -n 's/.*<version>\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1) - TARGET_PLATFORM=$(sed -n 's/.*\(<targetplatform[^/]*\/>\).*/\1/p' "$MANIFEST" | head -1) - PHP_MINIMUM=$(sed -n 's/.*<php_minimum>\([^<]*\)<\/php_minimum>.*/\1/p' "$MANIFEST" | head -1) - - # Fallbacks - [ -z "$EXT_NAME" ] && EXT_NAME="${{ github.event.repository.name }}" - [ -z "$EXT_TYPE" ] && EXT_TYPE="component" - - # Derive element if not in manifest: try XML filename, then repo name - if [ -z "$EXT_ELEMENT" ]; then - EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]') - case "$EXT_ELEMENT" in - templatedetails|manifest|*.xml) EXT_ELEMENT=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;; - esac - fi - - # Use manifest version if README version is empty - [ "$VERSION" = "0.0.0" ] && [ -n "$EXT_VERSION" ] && VERSION="$EXT_VERSION" - - [ -z "$TARGET_PLATFORM" ] && TARGET_PLATFORM=$(printf '<targetplatform name="joomla" version="((5.[0-9])|(6.[0-9]))" %s>' "/") - - CLIENT_TAG="" - [ -n "$EXT_CLIENT" ] && CLIENT_TAG="<client>${EXT_CLIENT}</client>" - [ -z "$CLIENT_TAG" ] && ([ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]) && CLIENT_TAG="<client>site</client>" - - FOLDER_TAG="" - [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ] && FOLDER_TAG="<folder>${EXT_FOLDER}</folder>" - - PHP_TAG="" - [ -n "$PHP_MINIMUM" ] && PHP_TAG="<php_minimum>${PHP_MINIMUM}</php_minimum>" - - # Version suffix for non-stable - DISPLAY_VERSION="$VERSION" - case "$STABILITY" in - development) DISPLAY_VERSION="${VERSION}-dev" ;; - alpha) DISPLAY_VERSION="${VERSION}-alpha" ;; - beta) DISPLAY_VERSION="${VERSION}-beta" ;; - rc) DISPLAY_VERSION="${VERSION}-rc" ;; - esac - - MAJOR=$(echo "$VERSION" | awk -F. '{print $1}') - - # Each stability level has its own release tag - case "$STABILITY" in - development) RELEASE_TAG="development" ;; - alpha) RELEASE_TAG="alpha" ;; - beta) RELEASE_TAG="beta" ;; - rc) RELEASE_TAG="release-candidate" ;; - *) RELEASE_TAG="v${MAJOR}" ;; - esac - - PACKAGE_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.zip" - DOWNLOAD_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${PACKAGE_NAME}" - INFO_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}" - - # -- Build install packages (ZIP + tar.gz) -------------------- - SOURCE_DIR="src" - [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" - if [ -d "$SOURCE_DIR" ]; then - EXCLUDES=".ftpignore sftp-config* *.ppk *.pem *.key .env*" - TAR_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.tar.gz" - - cd "$SOURCE_DIR" - zip -r "/tmp/${PACKAGE_NAME}" . -x $EXCLUDES - cd .. - tar -czf "/tmp/${TAR_NAME}" -C "$SOURCE_DIR" \ - --exclude='.ftpignore' --exclude='sftp-config*' \ - --exclude='*.ppk' --exclude='*.pem' --exclude='*.key' --exclude='.env*' . - - SHA256=$(sha256sum "/tmp/${PACKAGE_NAME}" | cut -d' ' -f1) - - # Ensure release exists on Gitea - RELEASE_JSON=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ - "${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true) - RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) - - if [ -z "$RELEASE_ID" ]; then - # Create release - RELEASE_JSON=$(curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ - -H "Content-Type: application/json" \ - "${API_BASE}/releases" \ - -d "$(python3 -c "import json; print(json.dumps({ - 'tag_name': '${RELEASE_TAG}', - 'name': '${RELEASE_TAG} (${DISPLAY_VERSION})', - 'body': '${STABILITY} release', - 'prerelease': True, - 'target_commitish': 'main' - }))")" 2>/dev/null || true) - RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) - fi - - if [ -n "$RELEASE_ID" ]; then - # Delete existing assets with same name before uploading - ASSETS=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ - "${API_BASE}/releases/${RELEASE_ID}/assets" 2>/dev/null || echo "[]") - for ASSET_FILE in "$PACKAGE_NAME" "$TAR_NAME"; do - ASSET_ID=$(echo "$ASSETS" | python3 -c " - import sys,json - assets = json.load(sys.stdin) - for a in assets: - if a['name'] == '${ASSET_FILE}': - print(a['id']); break - " 2>/dev/null || true) - if [ -n "$ASSET_ID" ]; then - curl -sf -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ - "${API_BASE}/releases/${RELEASE_ID}/assets/${ASSET_ID}" 2>/dev/null || true - fi - done - - # Upload both formats - curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ - -H "Content-Type: application/octet-stream" \ - --data-binary @"/tmp/${PACKAGE_NAME}" \ - "${API_BASE}/releases/${RELEASE_ID}/assets?name=${PACKAGE_NAME}" > /dev/null 2>&1 || true - - curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ - -H "Content-Type: application/octet-stream" \ - --data-binary @"/tmp/${TAR_NAME}" \ - "${API_BASE}/releases/${RELEASE_ID}/assets?name=${TAR_NAME}" > /dev/null 2>&1 || true - fi - - echo "Packages: ${PACKAGE_NAME} + ${TAR_NAME} (SHA: ${SHA256})" >> $GITHUB_STEP_SUMMARY - else - SHA256="" - fi - - # -- Build the new entry (canonical format matching release.yml) -- - NEW_ENTRY="" - NEW_ENTRY="${NEW_ENTRY} <update>\n" - NEW_ENTRY="${NEW_ENTRY} <name>${EXT_NAME}</name>\n" - NEW_ENTRY="${NEW_ENTRY} <description>${EXT_NAME} ${STABILITY} build.</description>\n" - NEW_ENTRY="${NEW_ENTRY} <element>${EXT_ELEMENT}</element>\n" - NEW_ENTRY="${NEW_ENTRY} <type>${EXT_TYPE}</type>\n" - [ -n "$CLIENT_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${CLIENT_TAG}\n" - [ -n "$FOLDER_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${FOLDER_TAG}\n" - NEW_ENTRY="${NEW_ENTRY} <version>${VERSION}</version>\n" - NEW_ENTRY="${NEW_ENTRY} <creationDate>$(date +%Y-%m-%d)</creationDate>\n" - NEW_ENTRY="${NEW_ENTRY} <infourl title='${EXT_NAME}'>https://git.mokoconsulting.tech/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${RELEASE_TAG}</infourl>\n" - NEW_ENTRY="${NEW_ENTRY} <downloads>\n" - NEW_ENTRY="${NEW_ENTRY} <downloadurl type='full' format='zip'>${DOWNLOAD_URL}</downloadurl>\n" - NEW_ENTRY="${NEW_ENTRY} </downloads>\n" - [ -n "$SHA256" ] && NEW_ENTRY="${NEW_ENTRY} <sha256>${SHA256}</sha256>\n" - NEW_ENTRY="${NEW_ENTRY} <tags><tag>${STABILITY}</tag></tags>\n" - NEW_ENTRY="${NEW_ENTRY} <maintainer>Moko Consulting</maintainer>\n" - NEW_ENTRY="${NEW_ENTRY} <maintainerurl>https://mokoconsulting.tech</maintainerurl>\n" - NEW_ENTRY="${NEW_ENTRY} <targetplatform name='joomla' version='(5|6).*'/>\n" - [ -n "$PHP_MINIMUM" ] && NEW_ENTRY="${NEW_ENTRY} <php_minimum>${PHP_MINIMUM}</php_minimum>\n" - NEW_ENTRY="${NEW_ENTRY} </update>" - - # -- Write new entry to temp file -------------------------------- - printf '%b' "$NEW_ENTRY" > /tmp/new_entry.xml - - # -- Merge into updates.xml ---------------------------------------- - # Cascade: stable→all | rc→rc+lower | beta→beta+lower | alpha→alpha+dev | dev→dev - CASCADE_MAP="stable:development,alpha,beta,rc,stable rc:development,alpha,beta,rc beta:development,alpha,beta alpha:development,alpha development:development" - TARGETS="" - for entry in $CASCADE_MAP; do - key="${entry%%:*}" - vals="${entry#*:}" - if [ "$key" = "${STABILITY}" ]; then - TARGETS="$vals" - break - fi - done - [ -z "$TARGETS" ] && TARGETS="${STABILITY}" - - echo "Cascade: ${STABILITY} → ${TARGETS}" - - # Create updates.xml if missing - if [ ! -f "updates.xml" ]; then - printf '%s\n' "<?xml version='1.0' encoding='UTF-8'?>" > updates.xml - printf '%s\n' "<!-- Copyright (C) $(date +%Y) Moko Consulting -->" >> updates.xml - printf '%s\n' "<updates>" >> updates.xml - printf '%s\n' "</updates>" >> updates.xml - fi - - # Update existing blocks or create missing ones - export PY_TARGETS="$TARGETS" PY_VERSION="$VERSION" PY_DATE="$(date +%Y-%m-%d)" - python3 << 'PYEOF' - import re, os - - targets = os.environ["PY_TARGETS"].split(",") - version = os.environ["PY_VERSION"] - date = os.environ["PY_DATE"] - - with open("updates.xml") as f: - content = f.read() - with open("/tmp/new_entry.xml") as f: - new_entry_template = f.read() - - for tag in targets: - tag = tag.strip() - # Build entry with this tag's name - new_entry = re.sub(r"<tag>[^<]*</tag>", f"<tag>{tag}</tag>", new_entry_template) - - # Try to find existing block (handles both single-line and multi-line <tags>) - block_pattern = r"(<update>(?:(?!</update>).)*?<tag>" + re.escape(tag) + r"</tag>.*?</update>)" - match = re.search(block_pattern, content, re.DOTALL) - - if match: - # Update in place — replace entire block - content = content.replace(match.group(1), new_entry.strip()) - print(f" UPDATED: <tag>{tag}</tag> → {version}") - else: - # Create — insert before </updates> - content = content.replace("</updates>", "\n" + new_entry.strip() + "\n\n</updates>") - print(f" CREATED: <tag>{tag}</tag> → {version}") - - # Clean up excessive blank lines - content = re.sub(r"\n{3,}", "\n\n", content) - - with open("updates.xml", "w") as f: - f.write(content) - PYEOF - - # Commit - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - git add updates.xml - git diff --cached --quiet || { - git commit -m "chore: update updates.xml (${STABILITY}: ${DISPLAY_VERSION}) [skip ci]" \ - --author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" - git push - } - - # -- Sync updates.xml to main (for non-main branches) ---------------------- - - name: Sync updates.xml to main - if: github.ref_name != 'main' - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - GA_TOKEN="${{ secrets.GA_TOKEN }}" - - FILE_SHA=$(curl -sf -H "Authorization: token ${GA_TOKEN}" \ - "${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true) - - if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then - CONTENT=$(base64 -w0 updates.xml) - curl -sf -X PUT -H "Authorization: token ${GA_TOKEN}" \ - -H "Content-Type: application/json" \ - "${API_BASE}/contents/updates.xml" \ - -d "$(python3 -c "import json; print(json.dumps({ - 'content': '${CONTENT}', - 'sha': '${FILE_SHA}', - 'message': 'chore: sync updates.xml from ${STABILITY} [skip ci]', - 'branch': 'main' - }))")" > /dev/null 2>&1 \ - && echo "updates.xml synced to main (${STABILITY})" >> $GITHUB_STEP_SUMMARY \ - || echo "WARNING: failed to sync updates.xml to main" >> $GITHUB_STEP_SUMMARY - else - echo "WARNING: could not get updates.xml SHA from main" >> $GITHUB_STEP_SUMMARY - fi - - - name: SFTP deploy to dev server - if: contains(github.ref, 'dev/') || github.ref == 'refs/heads/dev' - env: - DEV_HOST: ${{ vars.DEV_FTP_HOST }} - DEV_PATH: ${{ vars.DEV_FTP_PATH }} - DEV_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }} - DEV_USER: ${{ vars.DEV_FTP_USERNAME }} - DEV_PORT: ${{ vars.DEV_FTP_PORT }} - DEV_KEY: ${{ secrets.DEV_FTP_KEY }} - DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }} - run: | - # -- Permission check: admin or maintain role required -------- - ACTOR="${{ github.actor }}" - REPO="${{ github.repository }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - - PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ - "${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \ - python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read") - case "$PERMISSION" in - admin|maintain|write) ;; - *) - echo "Deploy denied: ${ACTOR} has '${PERMISSION}' — requires admin, maintain, or write" - exit 0 - ;; - esac - - [ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured — skipping SFTP"; exit 0; } - - SOURCE_DIR="src" - [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" - [ ! -d "$SOURCE_DIR" ] && exit 0 - - PORT="${DEV_PORT:-22}" - REMOTE="${DEV_PATH%/}" - [ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}" - - printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \ - "$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json - if [ -n "$DEV_KEY" ]; then - echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key - printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json - else - printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json - fi - - PLATFORM=$(php /tmp/mokostandards-api/cli/platform_detect.php --path . 2>/dev/null || true) - if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards-api/deploy/deploy-joomla.php" ]; then - php /tmp/mokostandards-api/deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json - elif [ -f "/tmp/mokostandards-api/deploy/deploy-sftp.php" ]; then - php /tmp/mokostandards-api/deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json - fi - rm -f /tmp/deploy_key /tmp/sftp-config.json - echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY - - - name: Summary - if: always() - run: | - echo "## Joomla Update Server" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY - echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY - echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Version | \`${DISPLAY_VERSION}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Element | \`${EXT_ELEMENT}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Download | [ZIP](${DOWNLOAD_URL}) |" >> $GITHUB_STEP_SUMMARY +# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: MokoStandards.Universal +# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform +# PATH: /templates/workflows/update-server.yml +# VERSION: 04.07.00 +# BRIEF: Update server XML feed with stable/rc/beta/alpha/dev entries (universal) +# +# Writes updates.xml with multiple <update> entries: +# - <tag>stable</tag> on push to main (from auto-release) +# - <tag>rc</tag> on push to rc/** +# - <tag>development</tag> on push to dev or dev/** +# +# Joomla filters by user's "Minimum Stability" setting. + +name: "Update Server" + +on: + push: + branches: + - 'dev' + - 'dev/**' + - 'alpha/**' + - 'beta/**' + - 'rc/**' + paths: + - 'src/**' + - 'htdocs/**' + pull_request: + types: [closed] + branches: + - 'dev' + - 'dev/**' + - 'alpha/**' + - 'beta/**' + - 'rc/**' + paths: + - 'src/**' + - 'htdocs/**' + workflow_dispatch: + inputs: + stability: + description: 'Stability tag' + required: true + default: 'development' + type: choice + options: + - development + - alpha + - beta + - rc + - stable + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + +permissions: + contents: write + +jobs: + update-xml: + name: Update updates.xml + runs-on: release + if: >- + github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || github.event_name == 'push' + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.GA_TOKEN }} + fetch-depth: 0 + + - name: Setup moko-platform tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.GA_TOKEN }}"}}}' + run: | + if ! command -v composer &> /dev/null; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + if [ -d "/tmp/moko-platform" ]; then + echo "moko-platform already available — skipping clone" + else + 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 + fi + 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 + + - name: Generate updates.xml entry + id: update + run: | + BRANCH="${{ github.ref_name }}" + REPO="${{ github.repository }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + VERSION=$(php /tmp/moko-platform/cli/version_read.php --path . 2>/dev/null || echo "0.0.0") + + # Auto-bump patch on all branches (dev, alpha, beta, rc) + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + BUMPED=$(php /tmp/moko-platform/cli/version_bump.php --path . 2>/dev/null || true) + if [ -n "$BUMPED" ]; then + VERSION=$(php /tmp/moko-platform/cli/version_read.php --path . 2>/dev/null || echo "$VERSION") + git add -A + git commit -m "chore(version): auto-bump patch ${VERSION} [skip ci]" \ + --author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" 2>/dev/null || true + git push 2>/dev/null || true + fi + + # Determine stability from branch or input + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + STABILITY="${{ inputs.stability }}" + elif [[ "$BRANCH" == rc/* ]]; then + STABILITY="rc" + elif [[ "$BRANCH" == beta/* ]]; then + STABILITY="beta" + elif [[ "$BRANCH" == alpha/* ]]; then + STABILITY="alpha" + elif [[ "$BRANCH" == dev/* ]] || [[ "$BRANCH" == "dev" ]]; then + STABILITY="development" + else + STABILITY="stable" + fi + + echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT" + + # Parse manifest (portable — no grep -P) + MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "./build/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1) + if [ -z "$MANIFEST" ]; then + echo "No Joomla manifest found — skipping" + exit 0 + fi + + # Extract fields using sed (works on all runners) + EXT_NAME=$(sed -n 's/.*<name>\([^<]*\)<\/name>.*/\1/p' "$MANIFEST" | head -1) + EXT_TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + EXT_ELEMENT=$(sed -n 's/.*<element>\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" | head -1) + EXT_CLIENT=$(sed -n 's/.*<extension[^>]*client="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + EXT_FOLDER=$(sed -n 's/.*<extension[^>]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + EXT_VERSION=$(sed -n 's/.*<version>\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1) + TARGET_PLATFORM=$(sed -n 's/.*\(<targetplatform[^/]*\/>\).*/\1/p' "$MANIFEST" | head -1) + PHP_MINIMUM=$(sed -n 's/.*<php_minimum>\([^<]*\)<\/php_minimum>.*/\1/p' "$MANIFEST" | head -1) + + # Fallbacks + [ -z "$EXT_NAME" ] && EXT_NAME="${{ github.event.repository.name }}" + [ -z "$EXT_TYPE" ] && EXT_TYPE="component" + + # Derive element if not in manifest: try XML filename, then repo name + if [ -z "$EXT_ELEMENT" ]; then + EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]') + case "$EXT_ELEMENT" in + templatedetails|manifest|*.xml) EXT_ELEMENT=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;; + esac + fi + + # Use manifest version if README version is empty + [ "$VERSION" = "0.0.0" ] && [ -n "$EXT_VERSION" ] && VERSION="$EXT_VERSION" + + [ -z "$TARGET_PLATFORM" ] && TARGET_PLATFORM=$(printf '<targetplatform name="joomla" version="((5.[0-9])|(6.[0-9]))" %s>' "/") + + # Joomla requires <client> on ALL extension types for update matching + if [ -n "$EXT_CLIENT" ]; then + CLIENT_TAG="<client>${EXT_CLIENT}</client>" + else + CLIENT_TAG="<client>site</client>" + fi + + FOLDER_TAG="" + [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ] && FOLDER_TAG="<folder>${EXT_FOLDER}</folder>" + + PHP_TAG="" + [ -n "$PHP_MINIMUM" ] && PHP_TAG="<php_minimum>${PHP_MINIMUM}</php_minimum>" + + # Version suffix for non-stable + DISPLAY_VERSION="$VERSION" + case "$STABILITY" in + development) DISPLAY_VERSION="${VERSION}-dev" ;; + alpha) DISPLAY_VERSION="${VERSION}-alpha" ;; + beta) DISPLAY_VERSION="${VERSION}-beta" ;; + rc) DISPLAY_VERSION="${VERSION}-rc" ;; + esac + + MAJOR=$(echo "$VERSION" | awk -F. '{print $1}') + + # Each stability level has its own release tag + case "$STABILITY" in + development) RELEASE_TAG="development" ;; + alpha) RELEASE_TAG="alpha" ;; + beta) RELEASE_TAG="beta" ;; + rc) RELEASE_TAG="release-candidate" ;; + *) RELEASE_TAG="v${MAJOR}" ;; + esac + + PACKAGE_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.zip" + DOWNLOAD_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${PACKAGE_NAME}" + INFO_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}" + + # -- Build install packages and upload to release -------------------- + php /tmp/moko-platform/cli/release_package.php \ + --path . --version "${DISPLAY_VERSION}" --tag "${RELEASE_TAG}" \ + --token "${{ secrets.GA_TOKEN }}" --api-base "${API_BASE}" \ + --repo "${GITEA_REPO}" --output /tmp 2>&1 || true + + echo "Package built and uploaded for ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY + + # -- Build the new entry (canonical format matching release.yml) -- + NEW_ENTRY="" + NEW_ENTRY="${NEW_ENTRY} <update>\n" + NEW_ENTRY="${NEW_ENTRY} <name>${EXT_NAME}</name>\n" + NEW_ENTRY="${NEW_ENTRY} <description>${EXT_NAME} ${STABILITY} build.</description>\n" + NEW_ENTRY="${NEW_ENTRY} <element>${EXT_ELEMENT}</element>\n" + NEW_ENTRY="${NEW_ENTRY} <type>${EXT_TYPE}</type>\n" + [ -n "$CLIENT_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${CLIENT_TAG}\n" + [ -n "$FOLDER_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${FOLDER_TAG}\n" + NEW_ENTRY="${NEW_ENTRY} <version>${VERSION}</version>\n" + NEW_ENTRY="${NEW_ENTRY} <creationDate>$(date +%Y-%m-%d)</creationDate>\n" + NEW_ENTRY="${NEW_ENTRY} <infourl title='${EXT_NAME}'>https://git.mokoconsulting.tech/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${RELEASE_TAG}</infourl>\n" + NEW_ENTRY="${NEW_ENTRY} <downloads>\n" + NEW_ENTRY="${NEW_ENTRY} <downloadurl type='full' format='zip'>${DOWNLOAD_URL}</downloadurl>\n" + NEW_ENTRY="${NEW_ENTRY} </downloads>\n" + [ -n "$SHA256" ] && NEW_ENTRY="${NEW_ENTRY} <sha256>${SHA256}</sha256>\n" + NEW_ENTRY="${NEW_ENTRY} <tags><tag>${STABILITY}</tag></tags>\n" + NEW_ENTRY="${NEW_ENTRY} <maintainer>Moko Consulting</maintainer>\n" + NEW_ENTRY="${NEW_ENTRY} <maintainerurl>https://mokoconsulting.tech</maintainerurl>\n" + NEW_ENTRY="${NEW_ENTRY} <targetplatform name='joomla' version='(5|6).*'/>\n" + [ -n "$PHP_MINIMUM" ] && NEW_ENTRY="${NEW_ENTRY} <php_minimum>${PHP_MINIMUM}</php_minimum>\n" + NEW_ENTRY="${NEW_ENTRY} </update>" + + # -- Write new entry to temp file -------------------------------- + printf '%b' "$NEW_ENTRY" > /tmp/new_entry.xml + + # -- Merge into updates.xml ---------------------------------------- + # Cascade: stable→all | rc→rc+lower | beta→beta+lower | alpha→alpha+dev | dev→dev + CASCADE_MAP="stable:development,alpha,beta,rc,stable rc:development,alpha,beta,rc beta:development,alpha,beta alpha:development,alpha development:development" + TARGETS="" + for entry in $CASCADE_MAP; do + key="${entry%%:*}" + vals="${entry#*:}" + if [ "$key" = "${STABILITY}" ]; then + TARGETS="$vals" + break + fi + done + [ -z "$TARGETS" ] && TARGETS="${STABILITY}" + + echo "Cascade: ${STABILITY} → ${TARGETS}" + + # Create updates.xml if missing + if [ ! -f "updates.xml" ]; then + printf '%s\n' "<?xml version='1.0' encoding='UTF-8'?>" > updates.xml + printf '%s\n' "<!-- Copyright (C) $(date +%Y) Moko Consulting -->" >> updates.xml + printf '%s\n' "<updates>" >> updates.xml + printf '%s\n' "</updates>" >> updates.xml + fi + + # Update existing blocks or create missing ones + export PY_TARGETS="$TARGETS" PY_VERSION="$VERSION" PY_DATE="$(date +%Y-%m-%d)" + python3 << 'PYEOF' + import re, os + + targets = os.environ["PY_TARGETS"].split(",") + version = os.environ["PY_VERSION"] + date = os.environ["PY_DATE"] + + with open("updates.xml") as f: + content = f.read() + with open("/tmp/new_entry.xml") as f: + new_entry_template = f.read() + + for tag in targets: + tag = tag.strip() + # Build entry with this tag's name + new_entry = re.sub(r"<tag>[^<]*</tag>", f"<tag>{tag}</tag>", new_entry_template) + + # Try to find existing block (handles both single-line and multi-line <tags>) + block_pattern = r"(<update>(?:(?!</update>).)*?<tag>" + re.escape(tag) + r"</tag>.*?</update>)" + match = re.search(block_pattern, content, re.DOTALL) + + if match: + # Update in place — replace entire block + content = content.replace(match.group(1), new_entry.strip()) + print(f" UPDATED: <tag>{tag}</tag> → {version}") + else: + # Create — insert before </updates> + content = content.replace("</updates>", "\n" + new_entry.strip() + "\n\n</updates>") + print(f" CREATED: <tag>{tag}</tag> → {version}") + + # Clean up excessive blank lines + content = re.sub(r"\n{3,}", "\n\n", content) + + with open("updates.xml", "w") as f: + f.write(content) + PYEOF + + # Commit + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git add updates.xml + git diff --cached --quiet || { + git commit -m "chore: update updates.xml (${STABILITY}: ${DISPLAY_VERSION}) [skip ci]" \ + --author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" + git push + } + + # -- Sync updates.xml to main (for non-main branches) ---------------------- + - name: Sync updates.xml to main + if: github.ref_name != 'main' + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + GA_TOKEN="${{ secrets.GA_TOKEN }}" + + FILE_SHA=$(curl -sf -H "Authorization: token ${GA_TOKEN}" \ + "${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true) + + if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then + 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 ${STABILITY} [skip ci]', + 'branch': 'main' + }).encode() + req = urllib.request.Request( + '${API_BASE}/contents/updates.xml', + data=payload, method='PUT', + headers={ + 'Authorization': 'token ${GA_TOKEN}', + 'Content-Type': 'application/json' + }) + try: + urllib.request.urlopen(req) + print('updates.xml synced to main') + except Exception as e: + print(f'ERROR: failed to sync updates.xml to main: {e}', file=sys.stderr) + sys.exit(1) + " \ + && echo "updates.xml synced to main (${STABILITY})" >> $GITHUB_STEP_SUMMARY \ + || echo "::error::failed to sync updates.xml to main" >> $GITHUB_STEP_SUMMARY + else + echo "::error::could not get updates.xml SHA from main — file may not exist on main yet" >> $GITHUB_STEP_SUMMARY + fi + + - name: SFTP deploy to dev server + if: contains(github.ref, 'dev/') || github.ref == 'refs/heads/dev' + env: + DEV_HOST: ${{ vars.DEV_FTP_HOST }} + DEV_PATH: ${{ vars.DEV_FTP_PATH }} + DEV_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }} + DEV_USER: ${{ vars.DEV_FTP_USERNAME }} + DEV_PORT: ${{ vars.DEV_FTP_PORT }} + DEV_KEY: ${{ secrets.DEV_FTP_KEY }} + DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }} + run: | + # -- Permission check: admin or maintain role required -------- + ACTOR="${{ github.actor }}" + REPO="${{ github.repository }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + + PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \ + python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read") + case "$PERMISSION" in + admin|maintain|write) ;; + *) + echo "Deploy denied: ${ACTOR} has '${PERMISSION}' — requires admin, maintain, or write" + exit 0 + ;; + esac + + [ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured — skipping SFTP"; exit 0; } + + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" + [ ! -d "$SOURCE_DIR" ] && exit 0 + + PORT="${DEV_PORT:-22}" + REMOTE="${DEV_PATH%/}" + [ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}" + + printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \ + "$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json + if [ -n "$DEV_KEY" ]; then + echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key + printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json + else + printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json + fi + + PLATFORM=$(php /tmp/moko-platform/cli/platform_detect.php --path . 2>/dev/null || true) + if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/moko-platform/deploy/deploy-joomla.php" ]; then + php /tmp/moko-platform/deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json + elif [ -f "/tmp/moko-platform/deploy/deploy-sftp.php" ]; then + php /tmp/moko-platform/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: Validate updates.xml integrity + run: | + ERRORS=0 + + if [ ! -f "updates.xml" ]; then + echo "::error::updates.xml not found" + exit 1 + fi + + # Well-formed XML + if ! python3 -c "import xml.etree.ElementTree as ET; ET.parse('updates.xml')" 2>/dev/null; then + echo "::error::updates.xml is not valid XML" + ERRORS=$((ERRORS+1)) + fi + + python3 << 'PYEOF' + import xml.etree.ElementTree as ET, sys, re, os + + tree = ET.parse("updates.xml") + root = tree.getroot() + updates = root.findall("update") + errors = 0 + warnings = 0 + seen_tags = set() + + # All 5 channels MUST be present + REQUIRED_CHANNELS = {"stable", "rc", "beta", "alpha", "dev"} + VALID_TAGS = REQUIRED_CHANNELS | {"development"} # accept legacy alias + REPO = os.environ.get("GITEA_REPO", "") + ORG = os.environ.get("GITEA_ORG", "MokoConsulting") + REPO_BASE = f"https://git.mokoconsulting.tech/{ORG}/" + + # Gitea release tag names per channel (Moko standard) + RELEASE_TAG_MAP = { + "stable": "stable", + "rc": "release-candidate", + "beta": "beta", + "alpha": "alpha", + "dev": "development", + "development": "development", + } + + # Joomla update XML required fields per + # https://docs.joomla.org/Deploying_an_Update_Server + REQUIRED_FIELDS = ["name", "element", "type", "version", "infourl"] + + for i, u in enumerate(updates): + tag_el = u.find("tags/tag") + tag = tag_el.text.strip() if tag_el is not None and tag_el.text else None + label = f"Entry {i+1} (<tag>{tag or '?'}</tag>)" + + # -- Required Joomla fields -- + for field in REQUIRED_FIELDS: + el = u.find(field) + if el is None or not (el.text or "").strip(): + print(f"::error::{label}: missing required <{field}>") + errors += 1 + + # -- <downloads><downloadurl> -- + dl = u.find("downloads/downloadurl") + if dl is None or not (dl.text or "").strip(): + print(f"::error::{label}: missing <downloads><downloadurl>") + errors += 1 + else: + dl_url = dl.text.strip() + # Must point to org repo + if REPO_BASE not in dl_url: + print(f"::error::{label}: download URL not under {REPO_BASE}: {dl_url}") + errors += 1 + # Must end in .zip + if not dl_url.endswith(".zip"): + print(f"::error::{label}: download URL must end in .zip: {dl_url}") + errors += 1 + # Must use correct Gitea release tag in path + if tag and tag in RELEASE_TAG_MAP: + expected_tag = RELEASE_TAG_MAP[tag] + if f"/download/{expected_tag}/" not in dl_url: + print(f"::error::{label}: download URL should contain /download/{expected_tag}/ but got: {dl_url}") + errors += 1 + + # -- <client> (required for Joomla to match update) -- + client = u.find("client") + if client is None or not (client.text or "").strip(): + print(f"::error::{label}: missing <client> (required for Joomla update matching)") + errors += 1 + + # -- <targetplatform> -- + tp = u.find("targetplatform") + if tp is None: + print(f"::error::{label}: missing <targetplatform>") + errors += 1 + else: + tp_name = tp.get("name", "") + tp_ver = tp.get("version", "") + if tp_name != "joomla": + print(f"::error::{label}: targetplatform name should be 'joomla', got '{tp_name}'") + errors += 1 + if not tp_ver: + print(f"::error::{label}: targetplatform missing version regex") + errors += 1 + elif "5" not in tp_ver or "6" not in tp_ver: + print(f"::warning::{label}: targetplatform version may not cover Joomla 5+6: {tp_ver}") + warnings += 1 + + # -- <type> must be valid Joomla type -- + type_el = u.find("type") + if type_el is not None and type_el.text: + valid_types = {"component", "module", "plugin", "template", "library", "package", "file"} + if type_el.text.strip() not in valid_types: + print(f"::error::{label}: invalid type '{type_el.text}' (expected: {valid_types})") + errors += 1 + + # -- <version> format (XX.YY.ZZ with optional suffix) -- + ver_el = u.find("version") + if ver_el is not None and ver_el.text: + if not re.match(r"^\d{2}\.\d{2}\.\d{2}(-\w+)?$", ver_el.text.strip()): + print(f"::warning::{label}: version '{ver_el.text}' does not match XX.YY.ZZ format") + warnings += 1 + + # -- <maintainer> and <maintainerurl> -- + for field in ["maintainer", "maintainerurl"]: + el = u.find(field) + if el is None or not (el.text or "").strip(): + print(f"::warning::{label}: missing <{field}>") + warnings += 1 + + # -- Valid stability tag -- + if tag is None: + print(f"::error::{label}: missing <tags><tag>") + errors += 1 + elif tag not in VALID_TAGS: + print(f"::error::{label}: invalid tag '{tag}' (expected: {VALID_TAGS})") + errors += 1 + + # -- Duplicate tag check -- + norm_tag = "dev" if tag == "development" else tag + if norm_tag in seen_tags: + print(f"::error::{label}: duplicate channel '{tag}'") + errors += 1 + if norm_tag: + seen_tags.add(norm_tag) + + # -- All 5 channels must exist -- + missing = REQUIRED_CHANNELS - seen_tags + if missing: + print(f"::error::Missing required update channels: {', '.join(sorted(missing))}") + errors += 1 + + # -- Version ordering: higher stability must not exceed dev version -- + channel_versions = {} + for u in updates: + tag_el = u.find("tags/tag") + ver_el = u.find("version") + if tag_el is not None and ver_el is not None and tag_el.text and ver_el.text: + norm = "dev" if tag_el.text.strip() == "development" else tag_el.text.strip() + # Strip suffix for comparison (01.00.18-dev -> 01.00.18) + base_ver = re.sub(r"-\w+$", "", ver_el.text.strip()) + channel_versions[norm] = base_ver + + # Cascade check: dev >= alpha >= beta >= rc >= stable + ORDER = ["dev", "alpha", "beta", "rc", "stable"] + for j in range(1, len(ORDER)): + current = ORDER[j] + previous = ORDER[j - 1] + if current in channel_versions and previous in channel_versions: + if channel_versions[current] > channel_versions[previous]: + print(f"::error::{current} version ({channel_versions[current]}) is ahead of {previous} ({channel_versions[previous]})") + errors += 1 + + # -- Summary -- + print(f"\nupdates.xml validation: {len(updates)} entries, {errors} error(s), {warnings} warning(s)") + if errors > 0: + sys.exit(1) + PYEOF + + - name: Summary + if: always() + run: | + echo "## Joomla Update Server" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Version | \`${DISPLAY_VERSION}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Element | \`${EXT_ELEMENT}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Download | [ZIP](${DOWNLOAD_URL}) |" >> $GITHUB_STEP_SUMMARY From 72e0f85e4bc1b67908de1557d9dc744e1937b3fb Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 26 May 2026 22:12:39 +0000 Subject: [PATCH 039/132] chore(ci): add auto-bump.yml from moko-platform [skip ci] --- .mokogitea/workflows/auto-bump.yml | 82 ++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 .mokogitea/workflows/auto-bump.yml diff --git a/.mokogitea/workflows/auto-bump.yml b/.mokogitea/workflows/auto-bump.yml new file mode 100644 index 0000000..ef0aedc --- /dev/null +++ b/.mokogitea/workflows/auto-bump.yml @@ -0,0 +1,82 @@ +# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: moko-platform.Release +# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform +# PATH: /.mokogitea/workflows/auto-bump.yml +# VERSION: 09.02.00 +# BRIEF: Auto patch-bump version on every push to dev + +name: "Universal: Auto Version Bump" + +on: + push: + branches: + - dev + +env: + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + +permissions: + contents: write + +jobs: + bump: + name: Patch Bump + runs-on: release + if: >- + !contains(github.event.head_commit.message, '[skip ci]') && + !contains(github.event.head_commit.message, '[skip bump]') + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.GA_TOKEN }} + fetch-depth: 1 + + - name: Setup moko-platform tools + run: | + if ! command -v composer &> /dev/null; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + if [ -d "/opt/moko-platform/cli" ]; then + echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV" + else + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \ + /tmp/moko-platform-api + cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet + echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV" + fi + + - name: Patch bump version + run: | + BUMP=$(php ${MOKO_CLI}/version_bump.php --path . 2>&1) || true + echo "$BUMP" + + VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null) || true + [ -z "$VERSION" ] && { echo "No version found — skipping"; exit 0; } + + # Propagate to platform manifests + php ${MOKO_CLI}/version_set_platform.php \ + --path . --version "$VERSION" --branch dev 2>/dev/null || true + php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true + + # Commit if anything changed + if git diff --quiet && git diff --cached --quiet; then + echo "No version changes to commit" + exit 0 + fi + + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + git add -A + git commit -m "chore(version): patch bump to ${VERSION} [skip ci]" \ + --author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" + git push origin dev + echo "Bumped to ${VERSION}" >> $GITHUB_STEP_SUMMARY From 2b5b42567b668c6f463df5b1031ee3b8c9547a0c Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 26 May 2026 22:13:52 +0000 Subject: [PATCH 040/132] chore(ci): update pre-release.yml from moko-platform [skip ci] --- .mokogitea/workflows/pre-release.yml | 104 +++++---------------------- 1 file changed, 16 insertions(+), 88 deletions(-) diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml index 698251d..83443c7 100644 --- a/.mokogitea/workflows/pre-release.yml +++ b/.mokogitea/workflows/pre-release.yml @@ -78,15 +78,7 @@ jobs: - name: Detect platform id: platform run: | - PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1 | tr -d '[:space:]') - [ -z "$PLATFORM" ] && PLATFORM="generic" - echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT" - MANIFEST=$(find ./src -maxdepth 1 -name "pkg_*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1) - [ -z "$MANIFEST" ] && MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "*/packages/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1) - [ -z "$MANIFEST" ] && MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1) - MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1) - echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT" - echo "mod_file=${MOD_FILE}" >> "$GITHUB_OUTPUT" + php ${MOKO_CLI}/manifest_read.php --path . --github-output - name: Resolve metadata and bump version id: meta @@ -100,16 +92,9 @@ jobs: release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;; esac - # Patch bump via CLI tool - php ${MOKO_CLI}/version_bump.php --path . + # Read current version (bump already handled by push workflow) VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null) [ -z "$VERSION" ] && VERSION="00.00.01" - TODAY=$(date +%Y-%m-%d) - - # Update platform-specific manifest - PLATFORM="${{ steps.platform.outputs.platform }}" - MANIFEST="${{ steps.platform.outputs.manifest }}" - MOD_FILE="${{ steps.platform.outputs.mod_file }}" php ${MOKO_CLI}/version_set_platform.php \ --path . --version "$VERSION" --branch "${{ github.ref_name }}" 2>/dev/null || true @@ -127,36 +112,16 @@ jobs: git push origin HEAD 2>&1 } - # Auto-detect element (platform-aware) - EXT_ELEMENT="" - case "$PLATFORM" in - joomla) - if [ -n "$MANIFEST" ]; then - EXT_ELEMENT=$(sed -n 's/.*<element>\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1) - if [ -z "$EXT_ELEMENT" ]; then - EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]') - case "$EXT_ELEMENT" in - templatedetails|manifest) EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;; - esac - fi - else - EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') - fi - ;; - dolibarr) - if [ -n "$MOD_FILE" ]; then - MOD_BASENAME=$(basename "$MOD_FILE" .class.php) - EXT_ELEMENT=$(echo "$MOD_BASENAME" | sed 's/^mod//' | tr '[:upper:]' '[:lower:]') - else - EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') - fi - ;; - *) - EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') - ;; - esac + # Auto-detect element via manifest_element.php + php ${MOKO_CLI}/manifest_element.php \ + --path . --version "$VERSION" --stability "$STABILITY" \ + --repo "${GITEA_REPO}" --github-output - ZIP_NAME="${EXT_ELEMENT}-${VERSION}${SUFFIX}.zip" + # Read back element outputs + EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) + ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) + [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') + [ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}${SUFFIX}.zip" echo "version=${VERSION}" >> "$GITHUB_OUTPUT" echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT" @@ -278,54 +243,17 @@ jobs: - name: Update updates.xml if: steps.platform.outputs.platform == 'joomla' run: | - STABILITY="${{ steps.meta.outputs.stability }}" VERSION="${{ steps.meta.outputs.version }}" - SHA256="${{ steps.zip.outputs.sha256 }}" - ZIP_NAME="${{ steps.meta.outputs.zip_name }}" - TAG="${{ steps.meta.outputs.tag }}" + STABILITY="${{ steps.meta.outputs.stability }}" if [ ! -f "updates.xml" ]; then echo "No updates.xml -- skipping" exit 0 fi - # Map stability to XML tag name - case "$STABILITY" in - development) XML_TAG="development" ;; - alpha) XML_TAG="alpha" ;; - beta) XML_TAG="beta" ;; - release-candidate) XML_TAG="rc" ;; - *) XML_TAG="$STABILITY" ;; - esac - - DOWNLOAD_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${TAG}/${ZIP_NAME}" - - # Use PHP to update the channel in updates.xml - php -r ' - $xml_tag = $argv[1]; - $version = $argv[2]; - $sha256 = $argv[3]; - $url = $argv[4]; - $date = date("Y-m-d"); - - $content = file_get_contents("updates.xml"); - $pattern = "/(<update>(?:(?!<\/update>).)*?<tag>" . preg_quote($xml_tag) . "<\/tag>.*?<\/update>)/s"; - - $content = preg_replace_callback($pattern, function($m) use ($version, $sha256, $url, $date) { - $block = $m[0]; - $block = preg_replace("/<version>[^<]*<\/version>/", "<version>{$version}</version>", $block); - if (strpos($block, "<sha256>") !== false) { - $block = preg_replace("/<sha256>[^<]*<\/sha256>/", "<sha256>{$sha256}</sha256>", $block); - } else { - $block = str_replace("</downloads>", "</downloads>\n <sha256>{$sha256}</sha256>", $block); - } - $block = preg_replace("/(<downloadurl[^>]*>)[^<]*(<\/downloadurl>)/", "\${1}{$url}\${2}", $block); - return $block; - }, $content); - - file_put_contents("updates.xml", $content); - echo "Updated {$xml_tag} channel: version={$version}\n"; - ' "$XML_TAG" "$VERSION" "$SHA256" "$DOWNLOAD_URL" + php ${MOKO_CLI}/updates_xml_build.php \ + --path . --version "${VERSION}" --stability "${STABILITY}" \ + --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" # Commit and push if ! git diff --quiet updates.xml 2>/dev/null; then @@ -347,7 +275,7 @@ jobs: [ "$BRANCH" = "$CURRENT_BRANCH" ] && continue echo "Syncing updates.xml -> ${BRANCH}" git fetch origin "${BRANCH}" 2>/dev/null || continue - git checkout "origin/${BRANCH}" -- . 2>/dev/null || continue + git checkout "origin/${BRANCH}" -- updates.xml 2>/dev/null || continue git checkout "${CURRENT_BRANCH}" -- updates.xml if ! git diff --quiet updates.xml 2>/dev/null; then git add updates.xml From fa732f44c8679233d2ce7490b500f9aac1d5f1fd Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 26 May 2026 22:24:31 +0000 Subject: [PATCH 041/132] chore(ci): update auto-release.yml from moko-platform [skip ci] --- .mokogitea/workflows/auto-release.yml | 380 ++++---------------------- 1 file changed, 51 insertions(+), 329 deletions(-) diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml index eca25d8..d665ce7 100644 --- a/.mokogitea/workflows/auto-release.yml +++ b/.mokogitea/workflows/auto-release.yml @@ -31,6 +31,15 @@ on: branches: - main workflow_dispatch: + inputs: + action: + description: 'Action to perform' + required: false + type: choice + default: release + options: + - release + - promote-rc env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true @@ -47,8 +56,8 @@ jobs: name: Promote Pre-Release to RC runs-on: release if: >- - github.event.action == 'opened' && - github.event.pull_request.draft == true + (github.event.action == 'opened' && github.event.pull_request.draft == true) || + (github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc') steps: - name: Checkout repository @@ -100,7 +109,8 @@ jobs: name: Build & Release Pipeline runs-on: release if: >- - github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' + github.event.pull_request.merged == true || + (github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc') steps: - name: Checkout repository @@ -170,18 +180,7 @@ jobs: echo "::notice::No RC release — full build pipeline" fi - - name: "Step 1b: Bump version" - id: bump - if: >- - steps.version.outputs.skip != 'true' && - steps.rc.outputs.promote != 'true' - run: | - MOKO_API="/tmp/moko-platform-api/cli" - BUMP=$(php ${MOKO_API}/version_bump.php --path . --minor) - VERSION=$(echo "$BUMP" | grep -oP '\d{2}\.\d{2}\.\d{2}$' || true) - [ -z "$VERSION" ] && VERSION=$(php ${MOKO_API}/version_read.php --path .) - echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - echo "Bumped to: ${VERSION}" + # Version bump handled by auto-bump.yml (minor on main, patch on dev) - name: Check if already released if: steps.version.outputs.skip != 'true' @@ -208,96 +207,9 @@ jobs: steps.version.outputs.skip != 'true' && steps.check.outputs.already_released != 'true' run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - ERRORS=0 - - PLATFORM="${{ steps.platform.outputs.platform }}" - MANIFEST="${{ steps.platform.outputs.manifest }}" - MOD_FILE="${{ steps.platform.outputs.mod_file }}" - echo "## Pre-Release Sanity Checks (${PLATFORM})" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # -- Version drift check (must pass before release) -------- - README_VER=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1) - if [ "$README_VER" != "$VERSION" ]; then - echo "- Version drift: README says \`${README_VER}\` but releasing \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS+1)) - else - echo "- Version consistent: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY - fi - - # Check CHANGELOG version matches - CL_VER=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' CHANGELOG.md 2>/dev/null | head -1) - if [ -n "$CL_VER" ] && [ "$CL_VER" != "$VERSION" ]; then - echo "- CHANGELOG drift: \`${CL_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS+1)) - fi - - # Check composer.json version if present - if [ -f "composer.json" ]; then - COMP_VER=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' composer.json 2>/dev/null | head -1) - if [ -n "$COMP_VER" ] && [ "$COMP_VER" != "$VERSION" ]; then - echo "- composer.json drift: \`${COMP_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS+1)) - fi - fi - - # Common checks - if [ ! -f "LICENSE" ]; then - echo "- Missing LICENSE file" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS+1)) - else - echo "- LICENSE present" >> $GITHUB_STEP_SUMMARY - fi - - if [ ! -d "src" ] && [ ! -d "htdocs" ]; then - echo "- Warning: No src/ or htdocs/ directory" >> $GITHUB_STEP_SUMMARY - else - echo "- Source directory present" >> $GITHUB_STEP_SUMMARY - fi - - # -- Platform-specific checks -------- - case "$PLATFORM" in - joomla) - if [ -n "$MANIFEST" ]; then - XML_VER=$(sed -n 's/.*<version>\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1) - if [ -n "$XML_VER" ] && [ "$XML_VER" != "$VERSION" ]; then - echo "- Manifest drift: \`${XML_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS+1)) - else - echo "- Manifest version: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY - fi - TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" 2>/dev/null) - echo "- Extension type: ${TYPE:-unknown}" >> $GITHUB_STEP_SUMMARY - else - echo "- No Joomla XML manifest (WaaS site)" >> $GITHUB_STEP_SUMMARY - fi ;; - dolibarr) - if [ -n "$MOD_FILE" ]; then - MOD_VER=$(sed -n "s/.*\\\$this->version = '\([^']*\)'.*/\1/p" "$MOD_FILE" 2>/dev/null | head -1) - if [ -n "$MOD_VER" ] && [ "$MOD_VER" != "$VERSION" ]; then - echo "- Module drift: \`${MOD_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS+1)) - else - echo "- Module version: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY - fi - else - echo "- No mod*.class.php found" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS+1)) - fi - if [ ! -f "update.txt" ]; then - echo "- Missing update.txt" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS+1)) - fi ;; - *) echo "- Generic platform � no manifest checks" >> $GITHUB_STEP_SUMMARY ;; - esac - - echo "" >> $GITHUB_STEP_SUMMARY - if [ "$ERRORS" -gt 0 ]; then - echo "**${ERRORS} error(s) — release may be incomplete**" >> $GITHUB_STEP_SUMMARY - else - echo "**All sanity checks passed**" >> $GITHUB_STEP_SUMMARY - fi + VERSION="${{ steps.version.outputs.version }}" + php /tmp/moko-platform-api/cli/release_validate.php \ + --path . --version "$VERSION" --output-summary --github-output || true # -- STEP 2: Create or update version/XX.YY archive branch --------------- # Always runs — every version change on main archives to version/XX.YY @@ -306,7 +218,7 @@ jobs: run: | BRANCH="${{ steps.version.outputs.branch }}" IS_MINOR="${{ steps.version.outputs.is_minor }}" - PATCH="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + PATCH="${{ steps.version.outputs.version }}" PATCH_NUM=$(echo "$PATCH" | awk -F. '{print $3}') # Check if branch exists @@ -325,7 +237,7 @@ jobs: steps.version.outputs.skip != 'true' && steps.check.outputs.already_released != 'true' run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + VERSION="${{ steps.version.outputs.version }}" php /tmp/moko-platform-api/cli/version_set_platform.php \ --path . --version "$VERSION" --branch main @@ -333,7 +245,7 @@ jobs: - name: "Step 4: Update version badges" if: steps.version.outputs.skip != 'true' run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + VERSION="${{ steps.version.outputs.version }}" php /tmp/moko-platform-api/cli/badge_update.php --path . --version "${VERSION}" 2>/dev/null || true php /tmp/moko-platform-api/cli/version_check.php --path . --fix 2>/dev/null || true @@ -342,7 +254,7 @@ jobs: steps.version.outputs.skip != 'true' && steps.platform.outputs.platform == 'joomla' run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + VERSION="${{ steps.version.outputs.version }}" # Fetch latest updates.xml from main so preserve logic has all channels GA_TOKEN="${{ secrets.GA_TOKEN }}" @@ -366,7 +278,7 @@ jobs: echo "No changes to commit" exit 0 fi - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + VERSION="${{ steps.version.outputs.version }}" git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" git config --local user.name "gitea-actions[bot]" # Set push URL with token for branch-protected repos @@ -413,187 +325,34 @@ jobs: steps.version.outputs.skip != 'true' && steps.rc.outputs.promote != 'true' run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + VERSION="${{ steps.version.outputs.version }}" RELEASE_TAG="${{ steps.version.outputs.release_tag }}" - BRANCH="${{ steps.version.outputs.branch }}" - MAJOR="${{ steps.version.outputs.major }}" API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php /tmp/moko-platform-api/cli/release_create.php \ + --path . --version "$VERSION" --tag "$RELEASE_TAG" \ + --token "${{ secrets.GA_TOKEN }}" --api-base "$API_BASE" \ + --repo "${GITEA_REPO}" --branch main + echo "Release created: ${VERSION}" >> $GITHUB_STEP_SUMMARY - # Reuse metadata from Step 5 (single source of truth) - EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}" - EXT_NAME="${{ steps.updates.outputs.ext_name }}" - EXT_TYPE="${{ steps.updates.outputs.ext_type }}" - EXT_FOLDER="${{ steps.updates.outputs.ext_folder }}" - - # Fallbacks if Step 5 was skipped - if [ -z "$EXT_ELEMENT" ]; then - EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') - fi - [ -z "$EXT_NAME" ] && EXT_NAME="${GITEA_REPO}" - - NOTES=$(php /tmp/moko-platform-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null) - [ -z "$NOTES" ] && NOTES="Release ${VERSION}" - - # Build release name: "Pretty Name VERSION (type_element-VERSION)" - # Strip existing type prefix to prevent duplication - EXT_ELEMENT=$(echo "$EXT_ELEMENT" | sed -E 's/^(pkg_|com_|mod_|plg_[a-z]+_|tpl_|lib_)//') - TYPE_PREFIX="" - case "${EXT_TYPE}" in - plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;; - module) TYPE_PREFIX="mod_" ;; - component) TYPE_PREFIX="com_" ;; - template) TYPE_PREFIX="tpl_" ;; - library) TYPE_PREFIX="lib_" ;; - package) TYPE_PREFIX="pkg_" ;; - esac - RELEASE_NAME="${EXT_NAME} ${VERSION} (${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION})" - - # Delete existing release if present (overwrite, not append) - EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ - "${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true) - EXISTING_ID=$(echo "$EXISTING" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('id',''))" 2>/dev/null || true) - - if [ -n "$EXISTING_ID" ]; then - curl -sS -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ - "${API_BASE}/releases/${EXISTING_ID}" 2>/dev/null || true - curl -sS -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ - "${API_BASE}/tags/${RELEASE_TAG}" 2>/dev/null || true - echo "Deleted previous stable release (id: ${EXISTING_ID})" - fi - - # Create fresh release - curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ - -H "Content-Type: application/json" \ - "${API_BASE}/releases" \ - -d "$(python3 -c "import json; print(json.dumps({ - 'tag_name': '${RELEASE_TAG}', - 'name': '${RELEASE_NAME}', - 'body': '''## ${VERSION} ($(date +%Y-%m-%d))\n${NOTES}''', - 'target_commitish': '${BRANCH}' - }))")" - echo "Release created: ${RELEASE_NAME}" >> $GITHUB_STEP_SUMMARY - - # -- STEP 8: Build Joomla install ZIP + SHA-256 checksum ------------------ - - name: "Step 8: Build package and update checksum" + # -- STEP 8: Build packages and upload to release ---------------------------- + - name: "Step 8: Build package and upload" if: >- steps.version.outputs.skip != 'true' && steps.rc.outputs.promote != 'true' run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + VERSION="${{ steps.version.outputs.version }}" RELEASE_TAG="${{ steps.version.outputs.release_tag }}" - REPO="${{ github.repository }}" API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - - # All ZIPs upload to the major release tag (vXX) - RELEASE_JSON=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ - "${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true) - RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) - if [ -z "$RELEASE_ID" ]; then - echo "No release ${RELEASE_TAG} found — skipping ZIP upload" - exit 0 - fi - - # Find extension element name from manifest - MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true) - [ -z "$MANIFEST" ] && exit 0 - - # Reuse element from Step 5, with same fallback chain - EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}" - if [ -z "$EXT_ELEMENT" ]; then - EXT_ELEMENT=$(sed -n 's/.*<element>\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1) - [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(sed -n 's/.*plugin="\([^"]*\)".*/\1/p' "$MANIFEST" 2>/dev/null | head -1) - [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]') - [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') - fi - # ZIP name: type_folder_element-VERSION (e.g. plg_system_mokojgdpc-01.01.00.zip) - EXT_TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) - EXT_FOLDER=$(sed -n 's/.*<extension[^>]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) - # For packages, prefer <packagename> over filename-derived element - if [ "$EXT_TYPE" = "package" ]; then - PKG_NAME=$(sed -n 's/.*<packagename>\([^<]*\)<\/packagename>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1) - [ -n "$PKG_NAME" ] && EXT_ELEMENT="$PKG_NAME" - fi - # Strip existing type prefix to prevent duplication (e.g. pkg_mokowaas → mokowaas) - EXT_ELEMENT=$(echo "$EXT_ELEMENT" | sed -E 's/^(pkg_|com_|mod_|plg_[a-z]+_|tpl_|lib_)//') - TYPE_PREFIX="" - case "${EXT_TYPE}" in - plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;; - module) TYPE_PREFIX="mod_" ;; - component) TYPE_PREFIX="com_" ;; - template) TYPE_PREFIX="tpl_" ;; - library) TYPE_PREFIX="lib_" ;; - package) TYPE_PREFIX="pkg_" ;; - esac - ZIP_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip" - TAR_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.tar.gz" - - # -- Build install packages from src/ ---------------------------- - SOURCE_DIR="src" - [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" - [ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/"; exit 0; } - - # ZIP package (type-aware via moko-platform PHP API) - php /tmp/moko-platform-api/cli/joomla_build.php --path . --version "${VERSION}" --output /tmp - # Match the expected ZIP_NAME for upload - BUILT_ZIP=$(ls /tmp/${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip 2>/dev/null | head -1 || true) - if [ -n "$BUILT_ZIP" ] && [ "$BUILT_ZIP" != "/tmp/${ZIP_NAME}" ]; then - mv "$BUILT_ZIP" "/tmp/${ZIP_NAME}" - fi - - # tar.gz package (flat source archive) - tar -czf "/tmp/${TAR_NAME}" -C "$SOURCE_DIR" --exclude='.ftpignore' --exclude='sftp-config*' --exclude='*.ppk' --exclude='*.pem' --exclude='*.key' --exclude='.env*' . - - ZIP_SIZE=$(stat -c%s "/tmp/${ZIP_NAME}" 2>/dev/null || stat -f%z "/tmp/${ZIP_NAME}" 2>/dev/null || echo "unknown") - TAR_SIZE=$(stat -c%s "/tmp/${TAR_NAME}" 2>/dev/null || stat -f%z "/tmp/${TAR_NAME}" 2>/dev/null || echo "unknown") - - # -- Calculate SHA-256 for both ---------------------------------- - SHA256_ZIP=$(sha256sum "/tmp/${ZIP_NAME}" | cut -d' ' -f1) - SHA256_TAR=$(sha256sum "/tmp/${TAR_NAME}" | cut -d' ' -f1) - - # -- Get existing assets for cleanup -------------------------------- - ASSETS=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ - "${API_BASE}/releases/${RELEASE_ID}/assets" 2>/dev/null || echo "[]") - - # -- Create per-file .sha256 checksum files ------------------------- - echo "${SHA256_ZIP} ${ZIP_NAME}" > "/tmp/${ZIP_NAME}.sha256" - echo "${SHA256_TAR} ${TAR_NAME}" > "/tmp/${TAR_NAME}.sha256" - - # -- Upload packages + checksums to release tag -------------------- - for ASSET in "${ZIP_NAME}" "${TAR_NAME}" "${ZIP_NAME}.sha256" "${TAR_NAME}.sha256"; do - [ ! -f "/tmp/${ASSET}" ] && continue - # Delete existing asset with same name - ASSET_ID=$(echo "$ASSETS" | python3 -c " - import sys,json - assets = json.load(sys.stdin) - for a in assets: - if a['name'] == '${ASSET}': - print(a['id']); break - " 2>/dev/null || true) - [ -n "$ASSET_ID" ] && curl -sf -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ - "${API_BASE}/releases/${RELEASE_ID}/assets/${ASSET_ID}" 2>/dev/null || true - # Upload - curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ - -H "Content-Type: application/octet-stream" \ - --data-binary @"/tmp/${ASSET}" \ - "${API_BASE}/releases/${RELEASE_ID}/assets?name=${ASSET}" > /dev/null 2>&1 || true - done - - # updates.xml already handled by Step 5 (updates_xml_build.php with preserve logic) - - echo "### Packages" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Package | Size | SHA-256 |" >> $GITHUB_STEP_SUMMARY - echo "|---------|------|---------|" >> $GITHUB_STEP_SUMMARY - echo "| \`${ZIP_NAME}\` | ${ZIP_SIZE} | \`${SHA256_ZIP}\` |" >> $GITHUB_STEP_SUMMARY - echo "| \`${TAR_NAME}\` | ${TAR_SIZE} | \`${SHA256_TAR}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Release | \`${RELEASE_TAG}\` | |" >> $GITHUB_STEP_SUMMARY - echo "| Download | [${ZIP_NAME}](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${ZIP_NAME}) |" >> $GITHUB_STEP_SUMMARY + php /tmp/moko-platform-api/cli/release_package.php \ + --path . --version "$VERSION" --tag "$RELEASE_TAG" \ + --token "${{ secrets.GA_TOKEN }}" --api-base "$API_BASE" \ + --repo "${GITEA_REPO}" --output /tmp || true # -- STEP 8b: Update release description with changelog ---------------------- - name: "Step 8b: Update release body" if: steps.version.outputs.skip != 'true' run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + VERSION="${{ steps.version.outputs.version }}" RELEASE_TAG="${{ steps.version.outputs.release_tag }}" MOKO_CLI="/tmp/moko-platform-api/cli" @@ -621,44 +380,19 @@ jobs: - name: "Step 9: Mirror release to GitHub" if: >- steps.version.outputs.skip != 'true' && - steps.version.outputs.stability == 'stable' && secrets.GH_TOKEN != '' continue-on-error: true - env: - GH_TOKEN: ${{ secrets.GH_TOKEN }} run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + VERSION="${{ steps.version.outputs.version }}" RELEASE_TAG="${{ steps.version.outputs.release_tag }}" - MAJOR="${{ steps.version.outputs.major }}" - BRANCH="${{ steps.version.outputs.branch }}" GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" - - NOTES=$(php /tmp/moko-platform-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null || true) - [ -z "$NOTES" ] && NOTES="Release ${VERSION}" - echo "$NOTES" > /tmp/release_notes.md - - EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/tags/$RELEASE_TAG" 2>/dev/null | jq -r ".tag_name // empty" || true) - - if [ -z "$EXISTING" ]; then - gh release create "$RELEASE_TAG" \ - --repo "$GH_REPO" \ - --title "v${MAJOR} (latest: ${VERSION})" \ - --notes-file /tmp/release_notes.md \ - --target "$BRANCH" || true - else - gh release edit "$RELEASE_TAG" \ - --repo "$GH_REPO" \ - --title "v${MAJOR} (latest: ${VERSION})" || true - fi - - # Upload assets to GitHub mirror - for PKG in /tmp/${EXT_ELEMENT:-pkg}-${VERSION}.*; do - if [ -f "$PKG" ]; then - _RELID=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/tags/$RELEASE_TAG" 2>/dev/null | jq -r ".id // empty") - [ -n "$_RELID" ] && curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" -H "Content-Type: application/octet-stream" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/${_RELID}/assets?name=$(basename $PKG)" --data-binary "@$PKG" > /dev/null 2>&1 || true - fi - done - echo "GitHub mirror updated: ${GH_REPO} ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php /tmp/moko-platform-api/cli/release_mirror.php \ + --version "$VERSION" --tag "$RELEASE_TAG" \ + --token "${{ secrets.GA_TOKEN }}" --api-base "$API_BASE" \ + --gh-token "${{ secrets.GH_TOKEN }}" --gh-repo "$GH_REPO" \ + --branch main 2>&1 || true + echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY # -- STEP 10: Sync main branch to GitHub mirror ---------------------------- - name: "Step 10: Push main to GitHub mirror" @@ -682,7 +416,7 @@ jobs: - name: "Delete lesser pre-release channels" continue-on-error: true run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + VERSION="${{ steps.version.outputs.version }}" API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" php /tmp/moko-platform-api/cli/release_cascade.php \ --stability stable \ @@ -711,32 +445,20 @@ jobs: # -- Dolibarr post-release: Reset dev version ----------------------------- - - name: "Dolibarr: Reset dev version" - if: >- - steps.version.outputs.skip != 'true' && - steps.platform.outputs.platform == 'dolibarr' && - steps.platform.outputs.mod_file != '' + - name: "Post-release: Reset dev version" + if: steps.version.outputs.skip != 'true' continue-on-error: true run: | API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.GA_TOKEN }}" - MOD_FILE="${{ steps.platform.outputs.mod_file }}" - ENCODED_PATH=$(echo "$MOD_FILE" | sed 's|^\./||' | python3 -c "import sys,urllib.parse; print(urllib.parse.quote(sys.stdin.read().strip()))") - FILE_RESP=$(curl -sf -H "Authorization: token ${TOKEN}" "${API_BASE}/contents/${ENCODED_PATH}?ref=dev" 2>/dev/null || true) - FILE_SHA=$(echo "$FILE_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true) - FILE_CONTENT=$(echo "$FILE_RESP" | python3 -c "import sys,json,base64; print(base64.b64decode(json.load(sys.stdin).get('content','')).decode())" 2>/dev/null || true) - if [ -n "$FILE_SHA" ] && [ -n "$FILE_CONTENT" ]; then - UPDATED=$(echo "$FILE_CONTENT" | sed "s/\$this->version = '[^']*'/\$this->version = 'development'/") - ENCODED=$(echo "$UPDATED" | base64 -w0) - curl -sf -X PUT -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/contents/${ENCODED_PATH}" \ - -d "$(jq -n --arg content \"$ENCODED\" --arg sha \"$FILE_SHA\" --arg msg \"chore(version): reset dev version [skip ci]\" --arg branch \"dev\" '{content:$content,sha:$sha,message:$msg,branch:$branch}')" > /dev/null 2>&1 || true - fi + php /tmp/moko-platform-api/cli/version_reset_dev.php \ + --token "${{ secrets.GA_TOKEN }}" --api-base "${API_BASE}" \ + --branch dev --path . 2>&1 || true # -- Summary -------------------------------------------------------------- - name: Pipeline Summary if: always() run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + VERSION="${{ steps.version.outputs.version }}" PLATFORM="${{ steps.platform.outputs.platform }}" if [ "${{ steps.version.outputs.skip }}" = "true" ]; then echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY From 1954b36720e94077304cbb6b12e254f7df507c20 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 26 May 2026 22:25:46 +0000 Subject: [PATCH 042/132] chore(ci): update auto-bump.yml from moko-platform [skip ci] --- .mokogitea/workflows/auto-bump.yml | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/.mokogitea/workflows/auto-bump.yml b/.mokogitea/workflows/auto-bump.yml index ef0aedc..d1dedf1 100644 --- a/.mokogitea/workflows/auto-bump.yml +++ b/.mokogitea/workflows/auto-bump.yml @@ -16,6 +16,7 @@ on: push: branches: - dev + - main env: GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} @@ -53,9 +54,20 @@ jobs: echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV" fi - - name: Patch bump version + - name: Bump version run: | - BUMP=$(php ${MOKO_CLI}/version_bump.php --path . 2>&1) || true + BRANCH="${{ github.ref_name }}" + + # main = minor bump, dev = patch bump + if [ "$BRANCH" = "main" ]; then + BUMP_TYPE="--minor" + BUMP_LABEL="minor" + else + BUMP_TYPE="" + BUMP_LABEL="patch" + fi + + BUMP=$(php ${MOKO_CLI}/version_bump.php --path . $BUMP_TYPE 2>&1) || true echo "$BUMP" VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null) || true @@ -63,7 +75,7 @@ jobs: # Propagate to platform manifests php ${MOKO_CLI}/version_set_platform.php \ - --path . --version "$VERSION" --branch dev 2>/dev/null || true + --path . --version "$VERSION" --branch "$BRANCH" 2>/dev/null || true php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true # Commit if anything changed @@ -76,7 +88,7 @@ jobs: git config --local user.name "gitea-actions[bot]" git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" git add -A - git commit -m "chore(version): patch bump to ${VERSION} [skip ci]" \ + git commit -m "chore(version): ${BUMP_LABEL} bump to ${VERSION} [skip ci]" \ --author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" - git push origin dev - echo "Bumped to ${VERSION}" >> $GITHUB_STEP_SUMMARY + git push origin "$BRANCH" + echo "Bumped to ${VERSION} (${BUMP_LABEL})" >> $GITHUB_STEP_SUMMARY From 943b5077b35287db744c78a35f1350bbe1cac67a Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 26 May 2026 22:36:08 +0000 Subject: [PATCH 043/132] chore(ci): update auto-release.yml from moko-platform [skip ci] --- .mokogitea/workflows/auto-release.yml | 55 ++++++++++++++++++--------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml index d665ce7..e555c14 100644 --- a/.mokogitea/workflows/auto-release.yml +++ b/.mokogitea/workflows/auto-release.yml @@ -249,25 +249,7 @@ jobs: php /tmp/moko-platform-api/cli/badge_update.php --path . --version "${VERSION}" 2>/dev/null || true php /tmp/moko-platform-api/cli/version_check.php --path . --fix 2>/dev/null || true - - name: "Step 5: Write update stream" - if: >- - steps.version.outputs.skip != 'true' && - steps.platform.outputs.platform == 'joomla' - run: | - VERSION="${{ steps.version.outputs.version }}" - - # Fetch latest updates.xml from main so preserve logic has all channels - GA_TOKEN="${{ secrets.GA_TOKEN }}" - API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" - curl -sf -H "Authorization: token ${GA_TOKEN}" \ - "${API}/contents/updates.xml?ref=main" 2>/dev/null | \ - python3 -c "import sys,json,base64; print(base64.b64decode(json.load(sys.stdin)['content']).decode())" \ - > updates.xml 2>/dev/null || true - - php /tmp/moko-platform-api/cli/updates_xml_build.php \ - --path . --version "${VERSION}" --stability stable \ - --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \ - --github-output + # Step 5 (updates.xml) moved after Step 8 to include SHA-256 checksum - name: Commit release changes if: >- @@ -336,6 +318,7 @@ jobs: # -- STEP 8: Build packages and upload to release ---------------------------- - name: "Step 8: Build package and upload" + id: package if: >- steps.version.outputs.skip != 'true' && steps.rc.outputs.promote != 'true' @@ -348,6 +331,40 @@ jobs: --token "${{ secrets.GA_TOKEN }}" --api-base "$API_BASE" \ --repo "${GITEA_REPO}" --output /tmp || true + # -- STEP 5: Write update stream (after build so SHA-256 is available) ----- + - name: "Step 5: Write update stream" + if: steps.version.outputs.skip != 'true' + run: | + VERSION="${{ steps.version.outputs.version }}" + SHA256="${{ steps.package.outputs.sha256_zip }}" + + # Fetch latest updates.xml from main so preserve logic has all channels + GA_TOKEN="${{ secrets.GA_TOKEN }}" + API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" + curl -sf -H "Authorization: token ${GA_TOKEN}" \ + "${API}/contents/updates.xml?ref=main" 2>/dev/null | \ + python3 -c "import sys,json,base64; print(base64.b64decode(json.load(sys.stdin)['content']).decode())" \ + > updates.xml 2>/dev/null || true + + SHA_FLAG="" + [ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}" + + php /tmp/moko-platform-api/cli/updates_xml_build.php \ + --path . --version "${VERSION}" --stability stable \ + --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \ + ${SHA_FLAG} --github-output + + # Commit updates.xml if changed + if ! git diff --quiet updates.xml 2>/dev/null; then + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + git add updates.xml + git commit -m "chore: update stable channel ${VERSION} [skip ci]" \ + --author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" + git push origin HEAD 2>&1 || true + fi + # -- STEP 8b: Update release description with changelog ---------------------- - name: "Step 8b: Update release body" if: steps.version.outputs.skip != 'true' From f86d1bca41fddee3a456ce0cb4792dd901804ec0 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 26 May 2026 22:37:35 +0000 Subject: [PATCH 044/132] chore(ci): update pre-release.yml from moko-platform [skip ci] --- .mokogitea/workflows/pre-release.yml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml index 83443c7..a84e469 100644 --- a/.mokogitea/workflows/pre-release.yml +++ b/.mokogitea/workflows/pre-release.yml @@ -162,9 +162,18 @@ jobs: zip -r "../../build/package/${EXT_NAME}.zip" . -x $EXCLUDES cd "$OLDPWD" done + # Copy top-level files (manifest XML, script PHP, etc.) for f in "${SOURCE_DIR}"/*.xml "${SOURCE_DIR}"/*.php; do [ -f "$f" ] && cp "$f" build/package/ done + # Copy top-level directories (language/, media/, etc.) — exclude packages/ + for d in "${SOURCE_DIR}"/*/; do + [ ! -d "$d" ] && continue + DIRNAME=$(basename "$d") + [ "$DIRNAME" = "packages" ] && continue + cp -r "$d" "build/package/${DIRNAME}" + echo " Included dir: ${DIRNAME}/" + done else echo "=== Building standard extension ===" rsync -a \ @@ -245,15 +254,20 @@ jobs: run: | VERSION="${{ steps.meta.outputs.version }}" STABILITY="${{ steps.meta.outputs.stability }}" + SHA256="${{ steps.zip.outputs.sha256 }}" 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}" + --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \ + ${SHA_FLAG} # Commit and push if ! git diff --quiet updates.xml 2>/dev/null; then From 2c90881a5bf5f49aaed56838050b227f4a0024d7 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 26 May 2026 22:49:02 +0000 Subject: [PATCH 045/132] chore(ci): update auto-release.yml from moko-platform [skip ci] --- .mokogitea/workflows/auto-release.yml | 60 +++++++++++++-------------- 1 file changed, 28 insertions(+), 32 deletions(-) diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml index e555c14..80908e4 100644 --- a/.mokogitea/workflows/auto-release.yml +++ b/.mokogitea/workflows/auto-release.yml @@ -87,7 +87,7 @@ jobs: --from auto --to release-candidate \ --token "${{ secrets.GA_TOKEN }}" \ --api-base "${API_BASE}" \ - --branch "${{ github.event.pull_request.head.ref }}" + --branch "${{ github.event.pull_request.head.ref || 'dev' }}" - name: Cascade lesser channels continue-on-error: true @@ -180,7 +180,17 @@ jobs: echo "::notice::No RC release — full build pipeline" fi - # Version bump handled by auto-bump.yml (minor on main, patch on dev) + - name: "Step 1b: Minor bump version" + id: bump + if: >- + steps.version.outputs.skip != 'true' && + steps.rc.outputs.promote != 'true' + run: | + MOKO_API="/tmp/moko-platform-api/cli" + php ${MOKO_API}/version_bump.php --path . --minor 2>&1 || true + VERSION=$(php ${MOKO_API}/version_read.php --path .) + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "Bumped to: ${VERSION}" - name: Check if already released if: steps.version.outputs.skip != 'true' @@ -207,7 +217,7 @@ jobs: steps.version.outputs.skip != 'true' && steps.check.outputs.already_released != 'true' run: | - VERSION="${{ steps.version.outputs.version }}" + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" php /tmp/moko-platform-api/cli/release_validate.php \ --path . --version "$VERSION" --output-summary --github-output || true @@ -218,7 +228,7 @@ jobs: run: | BRANCH="${{ steps.version.outputs.branch }}" IS_MINOR="${{ steps.version.outputs.is_minor }}" - PATCH="${{ steps.version.outputs.version }}" + PATCH="${{ steps.bump.outputs.version || steps.version.outputs.version }}" PATCH_NUM=$(echo "$PATCH" | awk -F. '{print $3}') # Check if branch exists @@ -237,7 +247,7 @@ jobs: steps.version.outputs.skip != 'true' && steps.check.outputs.already_released != 'true' run: | - VERSION="${{ steps.version.outputs.version }}" + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" php /tmp/moko-platform-api/cli/version_set_platform.php \ --path . --version "$VERSION" --branch main @@ -245,7 +255,7 @@ jobs: - name: "Step 4: Update version badges" if: steps.version.outputs.skip != 'true' run: | - VERSION="${{ steps.version.outputs.version }}" + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" php /tmp/moko-platform-api/cli/badge_update.php --path . --version "${VERSION}" 2>/dev/null || true php /tmp/moko-platform-api/cli/version_check.php --path . --fix 2>/dev/null || true @@ -260,7 +270,7 @@ jobs: echo "No changes to commit" exit 0 fi - VERSION="${{ steps.version.outputs.version }}" + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" git config --local user.name "gitea-actions[bot]" # Set push URL with token for branch-protected repos @@ -292,7 +302,7 @@ jobs: steps.version.outputs.skip != 'true' && steps.rc.outputs.promote == 'true' run: | - VERSION="${{ steps.version.outputs.version }}" + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" php /tmp/moko-platform-api/cli/release_promote.php \ --from release-candidate --to stable \ @@ -307,7 +317,7 @@ jobs: steps.version.outputs.skip != 'true' && steps.rc.outputs.promote != 'true' run: | - VERSION="${{ steps.version.outputs.version }}" + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" RELEASE_TAG="${{ steps.version.outputs.release_tag }}" API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" php /tmp/moko-platform-api/cli/release_create.php \ @@ -323,7 +333,7 @@ jobs: steps.version.outputs.skip != 'true' && steps.rc.outputs.promote != 'true' run: | - VERSION="${{ steps.version.outputs.version }}" + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" RELEASE_TAG="${{ steps.version.outputs.release_tag }}" API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" php /tmp/moko-platform-api/cli/release_package.php \ @@ -335,7 +345,7 @@ jobs: - name: "Step 5: Write update stream" if: steps.version.outputs.skip != 'true' run: | - VERSION="${{ steps.version.outputs.version }}" + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" SHA256="${{ steps.package.outputs.sha256_zip }}" # Fetch latest updates.xml from main so preserve logic has all channels @@ -368,29 +378,15 @@ jobs: # -- STEP 8b: Update release description with changelog ---------------------- - name: "Step 8b: Update release body" if: steps.version.outputs.skip != 'true' + continue-on-error: true run: | - VERSION="${{ steps.version.outputs.version }}" + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" RELEASE_TAG="${{ steps.version.outputs.release_tag }}" - MOKO_CLI="/tmp/moko-platform-api/cli" - - php ${MOKO_CLI}/release_body_update.php \ + php /tmp/moko-platform-api/cli/release_body_update.php \ --path . --version "${VERSION}" --tag "${RELEASE_TAG}" \ --token "${{ secrets.GA_TOKEN }}" \ --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \ - 2>/dev/null || { - # Fallback: simple body update if CLI not available - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ - "${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null | \ - python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) - if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "None" ]; then - BODY="## ${VERSION} ($(date +%Y-%m-%d))\n\nChecksum files attached as \`*.sha256\` assets." - curl -sf -X PATCH -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ - -H "Content-Type: application/json" \ - "${API_BASE}/releases/${RELEASE_ID}" \ - -d "{\"body\":\"${BODY}\"}" > /dev/null 2>&1 - fi - } + 2>&1 || true echo "Release body updated" >> $GITHUB_STEP_SUMMARY # -- STEP 9: Mirror to GitHub (stable only) -------------------------------- @@ -400,7 +396,7 @@ jobs: secrets.GH_TOKEN != '' continue-on-error: true run: | - VERSION="${{ steps.version.outputs.version }}" + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" RELEASE_TAG="${{ steps.version.outputs.release_tag }}" GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" @@ -433,7 +429,7 @@ jobs: - name: "Delete lesser pre-release channels" continue-on-error: true run: | - VERSION="${{ steps.version.outputs.version }}" + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" php /tmp/moko-platform-api/cli/release_cascade.php \ --stability stable \ @@ -475,7 +471,7 @@ jobs: - name: Pipeline Summary if: always() run: | - VERSION="${{ steps.version.outputs.version }}" + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" PLATFORM="${{ steps.platform.outputs.platform }}" if [ "${{ steps.version.outputs.skip }}" = "true" ]; then echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY From e1c826a46a199170697e68ce95c3a4e2a0ac50c6 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 26 May 2026 22:50:14 +0000 Subject: [PATCH 046/132] chore(ci): update auto-bump.yml from moko-platform [skip ci] --- .mokogitea/workflows/auto-bump.yml | 30 ++++++++++-------------------- 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/.mokogitea/workflows/auto-bump.yml b/.mokogitea/workflows/auto-bump.yml index d1dedf1..10a7e51 100644 --- a/.mokogitea/workflows/auto-bump.yml +++ b/.mokogitea/workflows/auto-bump.yml @@ -8,7 +8,7 @@ # REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform # PATH: /.mokogitea/workflows/auto-bump.yml # VERSION: 09.02.00 -# BRIEF: Auto patch-bump version on every push to dev +# BRIEF: Auto patch-bump version on every push to dev (skips merge commits) name: "Universal: Auto Version Bump" @@ -16,9 +16,9 @@ on: push: branches: - dev - - main env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} permissions: @@ -26,11 +26,12 @@ permissions: jobs: bump: - name: Patch Bump + name: Version Bump runs-on: release if: >- !contains(github.event.head_commit.message, '[skip ci]') && - !contains(github.event.head_commit.message, '[skip bump]') + !contains(github.event.head_commit.message, '[skip bump]') && + !startsWith(github.event.head_commit.message, 'Merge pull request') steps: - name: Checkout @@ -56,18 +57,7 @@ jobs: - name: Bump version run: | - BRANCH="${{ github.ref_name }}" - - # main = minor bump, dev = patch bump - if [ "$BRANCH" = "main" ]; then - BUMP_TYPE="--minor" - BUMP_LABEL="minor" - else - BUMP_TYPE="" - BUMP_LABEL="patch" - fi - - BUMP=$(php ${MOKO_CLI}/version_bump.php --path . $BUMP_TYPE 2>&1) || true + BUMP=$(php ${MOKO_CLI}/version_bump.php --path . 2>&1) || true echo "$BUMP" VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null) || true @@ -75,7 +65,7 @@ jobs: # Propagate to platform manifests php ${MOKO_CLI}/version_set_platform.php \ - --path . --version "$VERSION" --branch "$BRANCH" 2>/dev/null || true + --path . --version "$VERSION" --branch dev 2>/dev/null || true php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true # Commit if anything changed @@ -88,7 +78,7 @@ jobs: git config --local user.name "gitea-actions[bot]" git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" git add -A - git commit -m "chore(version): ${BUMP_LABEL} bump to ${VERSION} [skip ci]" \ + git commit -m "chore(version): patch bump to ${VERSION} [skip ci]" \ --author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" - git push origin "$BRANCH" - echo "Bumped to ${VERSION} (${BUMP_LABEL})" >> $GITHUB_STEP_SUMMARY + git push origin dev + echo "Bumped to ${VERSION}" >> $GITHUB_STEP_SUMMARY From 129a9d547b801f41cf04e21c62844ccba03499af Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 26 May 2026 22:51:25 +0000 Subject: [PATCH 047/132] chore(ci): update pre-release.yml from moko-platform [skip ci] --- .mokogitea/workflows/pre-release.yml | 162 +++++---------------------- 1 file changed, 29 insertions(+), 133 deletions(-) diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml index a84e469..f0b1d88 100644 --- a/.mokogitea/workflows/pre-release.yml +++ b/.mokogitea/workflows/pre-release.yml @@ -52,28 +52,19 @@ jobs: fetch-depth: 0 token: ${{ secrets.GA_TOKEN }} - - name: Setup tools + - name: Setup moko-platform tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting run: | - # Update moko-platform CLI tools if available; install PHP if missing - if command -v moko-platform-update &> /dev/null; then - moko-platform-update - elif [ -d "/opt/moko-platform" ]; then - cd /opt/moko-platform && git pull origin main --quiet 2>/dev/null || true - else - if ! command -v php &> /dev/null; then - sudo apt-get update -qq - sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl >/dev/null 2>&1 - fi - git clone --depth 1 --branch main --quiet \ - "https://x-access-token:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \ - /tmp/moko-platform-api - fi - # Set MOKO_CLI to whichever path exists - if [ -d "/opt/moko-platform/cli" ]; then - echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV" - else - echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV" + if ! command -v composer &> /dev/null; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 fi + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ + /tmp/moko-platform-api + cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet + echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV" - name: Detect platform id: platform @@ -129,132 +120,37 @@ jobs: echo "tag=${TAG}" >> "$GITHUB_OUTPUT" echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT" echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT" - echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT" echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ===" - - name: Build package - run: | - SOURCE_DIR="src" - [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" - if [ ! -d "$SOURCE_DIR" ]; then - echo "::error::No src/ or htdocs/ directory" - exit 1 - fi - - MANIFEST="${{ steps.meta.outputs.manifest }}" - EXT_TYPE="" - if [ -n "$MANIFEST" ]; then - EXT_TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) - fi - - EXCLUDES="sftp-config* .ftpignore *.ppk *.pem *.key .env* *.local .build-trigger" - - mkdir -p build/package - - if [ "$EXT_TYPE" = "package" ] && [ -d "${SOURCE_DIR}/packages" ]; then - echo "=== Building Joomla PACKAGE (multi-extension) ===" - for ext_dir in "${SOURCE_DIR}"/packages/*/; do - [ ! -d "$ext_dir" ] && continue - EXT_NAME=$(basename "$ext_dir") - echo " Packaging sub-extension: ${EXT_NAME}" - cd "$ext_dir" - zip -r "../../build/package/${EXT_NAME}.zip" . -x $EXCLUDES - cd "$OLDPWD" - done - # Copy top-level files (manifest XML, script PHP, etc.) - for f in "${SOURCE_DIR}"/*.xml "${SOURCE_DIR}"/*.php; do - [ -f "$f" ] && cp "$f" build/package/ - done - # Copy top-level directories (language/, media/, etc.) — exclude packages/ - for d in "${SOURCE_DIR}"/*/; do - [ ! -d "$d" ] && continue - DIRNAME=$(basename "$d") - [ "$DIRNAME" = "packages" ] && continue - cp -r "$d" "build/package/${DIRNAME}" - echo " Included dir: ${DIRNAME}/" - done - else - echo "=== Building standard extension ===" - rsync -a \ - --exclude='sftp-config*' \ - --exclude='.ftpignore' \ - --exclude='*.ppk' \ - --exclude='*.pem' \ - --exclude='*.key' \ - --exclude='.env*' \ - --exclude='*.local' \ - --exclude='.build-trigger' \ - "${SOURCE_DIR}/" build/package/ - fi - - - name: Create ZIP - id: zip - run: | - ZIP_NAME="${{ steps.meta.outputs.zip_name }}" - cd build/package - zip -r "../${ZIP_NAME}" . - cd .. - - SHA256=$(sha256sum "${ZIP_NAME}" | cut -d' ' -f1) - echo "sha256=${SHA256}" >> "$GITHUB_OUTPUT" - echo "ZIP: ${ZIP_NAME} (SHA: ${SHA256:0:16}...)" - - - name: Create or replace Gitea release + - name: Create release id: release run: | TAG="${{ steps.meta.outputs.tag }}" VERSION="${{ steps.meta.outputs.version }}" - STABILITY="${{ steps.meta.outputs.stability }}" - SHA256="${{ steps.zip.outputs.sha256 }}" - ZIP_NAME="${{ steps.meta.outputs.zip_name }}" - EXT_ELEMENT="${{ steps.meta.outputs.ext_element }}" - TOKEN="${{ secrets.GA_TOKEN }}" - API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - BRANCH=$(git branch --show-current) + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php ${MOKO_CLI}/release_create.php \ + --path . --version "$VERSION" --tag "$TAG" \ + --token "${{ secrets.GA_TOKEN }}" --api-base "$API_BASE" \ + --repo "${GITEA_REPO}" --branch dev --prerelease - BODY="## ${VERSION} ($(date +%Y-%m-%d)) - **Channel:** ${STABILITY} - **SHA-256:** \`${SHA256}\`" - - # Delete existing release - EXISTING_ID=$(curl -sS -H "Authorization: token ${TOKEN}" \ - "${API}/releases/tags/${TAG}" | jq -r '.id // empty' 2>/dev/null) - if [ -n "$EXISTING_ID" ]; then - curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \ - "${API}/releases/${EXISTING_ID}" 2>/dev/null || true - curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \ - "${API}/tags/${TAG}" 2>/dev/null || true - fi - - # Create release - RELEASE_ID=$(curl -sS -X POST -H "Authorization: token ${TOKEN}" \ - -H "Content-Type: application/json" \ - "${API}/releases" \ - -d "$(jq -n \ - --arg tag "$TAG" \ - --arg target "$BRANCH" \ - --arg name "${EXT_ELEMENT} ${VERSION} (${STABILITY})" \ - --arg body "$BODY" \ - '{tag_name: $tag, target_commitish: $target, name: $name, body: $body, prerelease: true}' - )" | jq -r '.id') - - echo "release_id=${RELEASE_ID}" >> "$GITHUB_OUTPUT" - - # Upload ZIP - curl -sS -X POST -H "Authorization: token ${TOKEN}" \ - -H "Content-Type: application/octet-stream" \ - "${API}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" \ - --data-binary "@build/${ZIP_NAME}" - - echo "Released: ${EXT_ELEMENT} ${VERSION} (${STABILITY})" + - name: Build package and upload + id: package + run: | + VERSION="${{ steps.meta.outputs.version }}" + TAG="${{ steps.meta.outputs.tag }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php ${MOKO_CLI}/release_package.php \ + --path . --version "$VERSION" --tag "$TAG" \ + --token "${{ secrets.GA_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.zip.outputs.sha256 }}" + SHA256="${{ steps.package.outputs.sha256_zip }}" if [ ! -f "updates.xml" ]; then echo "No updates.xml -- skipping" @@ -316,7 +212,7 @@ jobs: VERSION="${{ steps.meta.outputs.version }}" STABILITY="${{ steps.meta.outputs.stability }}" ZIP_NAME="${{ steps.meta.outputs.zip_name }}" - SHA256="${{ steps.zip.outputs.sha256 }}" + SHA256="${{ steps.package.outputs.sha256_zip }}" echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY From 000d9a8fb6d790e1ea5ea4b5c2c00cb846cf9bf4 Mon Sep 17 00:00:00 2001 From: Moko Consulting <hello@mokoconsulting.tech> Date: Thu, 28 May 2026 13:42:28 -0500 Subject: [PATCH 048/132] feat(workflows): append stability suffix to manifest versions [skip bump] --- .mokogitea/workflows/pre-release.yml | 235 ++++++++++++++++++ .mokogitea/workflows/update-server.yml | 316 +++++++++++++++++++++++++ 2 files changed, 551 insertions(+) diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml index f0b1d88..45e19d2 100644 --- a/.mokogitea/workflows/pre-release.yml +++ b/.mokogitea/workflows/pre-release.yml @@ -1,3 +1,4 @@ +<<<<<<< Updated upstream # Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> # # SPDX-License-Identifier: GPL-3.0-or-later @@ -221,3 +222,237 @@ jobs: echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY +======= +# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: moko-platform.Release +# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform +# PATH: /templates/workflows/universal/pre-release.yml.template +# VERSION: 05.01.00 +# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch + +name: "Universal: Pre-Release" + +on: + pull_request: + types: [closed] + branches: + - dev + workflow_dispatch: + inputs: + stability: + description: 'Pre-release channel' + required: true + type: choice + options: + - development + - alpha + - beta + - release-candidate + +permissions: + contents: write + +env: + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + +jobs: + build: + name: "Build Pre-Release (${{ inputs.stability || 'development' }})" + runs-on: release + if: >- + github.event_name == 'workflow_dispatch' || + (github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev') + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GA_TOKEN }} + + - name: Setup moko-platform tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + run: | + if ! command -v composer &> /dev/null; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ + /tmp/moko-platform-api + cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet + echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV" + + - name: Detect platform + id: platform + run: | + php ${MOKO_CLI}/manifest_read.php --path . --github-output + + - name: Resolve metadata and bump version + id: meta + run: | + STABILITY="${{ inputs.stability || 'development' }}" + + case "$STABILITY" in + development) SUFFIX="-dev"; TAG="development" ;; + alpha) SUFFIX="-alpha"; TAG="alpha" ;; + beta) SUFFIX="-beta"; TAG="beta" ;; + release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;; + esac + + # Read current version (bump already handled by push workflow) + VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null) + [ -z "$VERSION" ] && VERSION="00.00.01" + + php ${MOKO_CLI}/version_set_platform.php \ + --path . --version "$VERSION" --branch "${{ github.ref_name }}" 2>/dev/null || true + + # Verify version consistency across all files + php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true + + # Append suffix to all manifest <version> tags + if [ -n "$SUFFIX" ]; then + find . -maxdepth 4 -name "*.xml" ! -path "./.git/*" ! -path "./build/*" \ + -exec grep -l "<version>${VERSION}</version>" {} \; 2>/dev/null | while read f; do + sed -i "s|<version>${VERSION}</version>|<version>${VERSION}${SUFFIX}</version>|g" "$f" + done + VERSION="${VERSION}${SUFFIX}" + fi + + # Commit version bump + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + git add -A + git diff --cached --quiet || { + git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]" + git push origin HEAD 2>&1 + } + + # Auto-detect element via manifest_element.php + php ${MOKO_CLI}/manifest_element.php \ + --path . --version "$VERSION" --stability "$STABILITY" \ + --repo "${GITEA_REPO}" --github-output + + # Read back element outputs + EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) + ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) + [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') + [ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip" + + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT" + echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT" + echo "tag=${TAG}" >> "$GITHUB_OUTPUT" + echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT" + echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT" + + echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ===" + + - name: Create release + id: release + run: | + TAG="${{ steps.meta.outputs.tag }}" + VERSION="${{ steps.meta.outputs.version }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php ${MOKO_CLI}/release_create.php \ + --path . --version "$VERSION" --tag "$TAG" \ + --token "${{ secrets.GA_TOKEN }}" --api-base "$API_BASE" \ + --repo "${GITEA_REPO}" --branch dev --prerelease + + - name: Build package and upload + id: package + run: | + VERSION="${{ steps.meta.outputs.version }}" + TAG="${{ steps.meta.outputs.tag }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php ${MOKO_CLI}/release_package.php \ + --path . --version "$VERSION" --tag "$TAG" \ + --token "${{ secrets.GA_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 + if ! git diff --quiet updates.xml 2>/dev/null; then + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git add updates.xml + git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]" + git push origin HEAD 2>&1 || echo "WARNING: push failed" + fi + + - name: "Sync updates.xml to all branches" + if: steps.platform.outputs.platform == 'joomla' + run: | + CURRENT_BRANCH="${{ github.ref_name }}" + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + + for BRANCH in main dev; do + [ "$BRANCH" = "$CURRENT_BRANCH" ] && continue + echo "Syncing updates.xml -> ${BRANCH}" + git fetch origin "${BRANCH}" 2>/dev/null || continue + git checkout "origin/${BRANCH}" -- updates.xml 2>/dev/null || continue + git checkout "${CURRENT_BRANCH}" -- updates.xml + if ! git diff --quiet updates.xml 2>/dev/null; then + git add updates.xml + git commit -m "chore: sync updates.xml from ${CURRENT_BRANCH} [skip ci]" + git push origin HEAD:refs/heads/${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed" + fi + git checkout "${CURRENT_BRANCH}" 2>/dev/null + done + + - name: "Delete lesser pre-release channels (cascade)" + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.GA_TOKEN }}" + + php ${MOKO_CLI}/release_cascade.php \ + --stability "${{ steps.meta.outputs.stability }}" \ + --token "${TOKEN}" \ + --api-base "${API_BASE}" + + - name: Summary + if: always() + run: | + VERSION="${{ steps.meta.outputs.version }}" + STABILITY="${{ steps.meta.outputs.stability }}" + ZIP_NAME="${{ steps.meta.outputs.zip_name }}" + SHA256="${{ steps.package.outputs.sha256_zip }}" + echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY + echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY + echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY +>>>>>>> Stashed changes diff --git a/.mokogitea/workflows/update-server.yml b/.mokogitea/workflows/update-server.yml index fd6407f..008e188 100644 --- a/.mokogitea/workflows/update-server.yml +++ b/.mokogitea/workflows/update-server.yml @@ -1,3 +1,4 @@ +<<<<<<< Updated upstream # Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> # # SPDX-License-Identifier: GPL-3.0-or-later @@ -595,3 +596,318 @@ jobs: echo "| Version | \`${DISPLAY_VERSION}\` |" >> $GITHUB_STEP_SUMMARY echo "| Element | \`${EXT_ELEMENT}\` |" >> $GITHUB_STEP_SUMMARY echo "| Download | [ZIP](${DOWNLOAD_URL}) |" >> $GITHUB_STEP_SUMMARY +======= +# 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.GA_TOKEN }} + fetch-depth: 0 + + - name: Setup moko-platform tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.GA_TOKEN }}"}}}' + run: | + if ! command -v composer &> /dev/null; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + if [ -d "/tmp/moko-platform" ]; then + echo "moko-platform already available — skipping clone" + else + 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 + fi + 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 }}" + + # Auto-bump patch version + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + 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") + + # Propagate version to all manifest files + php ${MOKO_CLI}/version_set_platform.php --path . --version "$VERSION" --branch "$BRANCH" 2>/dev/null || true + php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true + + # 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 + + # Append suffix to all manifest <version> tags (non-stable only) + if [ -n "$SUFFIX" ]; then + find . -maxdepth 4 -name "*.xml" ! -path "./.git/*" ! -path "./build/*" \ + -exec grep -l "<version>${VERSION}</version>" {} \; 2>/dev/null | while read f; do + sed -i "s|<version>${VERSION}</version>|<version>${VERSION}${SUFFIX}</version>|g" "$f" + done + 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.GA_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.GA_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 config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git add updates.xml + git diff --cached --quiet || { + git commit -m "chore: update ${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}" + GA_TOKEN="${{ secrets.GA_TOKEN }}" + + FILE_SHA=$(curl -sf -H "Authorization: token ${GA_TOKEN}" \ + "${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true) + + if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then + 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 ${GA_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.GA_TOKEN }}" \ + "${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \ + python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read") + case "$PERMISSION" in + admin|maintain|write) ;; + *) + echo "Deploy denied: ${ACTOR} has '${PERMISSION}' — requires admin, maintain, or write" + exit 0 + ;; + esac + + [ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured — skipping SFTP"; exit 0; } + + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" + [ ! -d "$SOURCE_DIR" ] && exit 0 + + PORT="${DEV_PORT:-22}" + REMOTE="${DEV_PATH%/}" + [ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}" + + printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \ + "$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json + if [ -n "$DEV_KEY" ]; then + echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key + printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json + else + printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json + fi + + PLATFORM=$(php ${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 +>>>>>>> Stashed changes From 72639c1155048722c6904e3d830ad479871e044b Mon Sep 17 00:00:00 2001 From: Moko Consulting <hello@mokoconsulting.tech> Date: Thu, 28 May 2026 14:16:05 -0500 Subject: [PATCH 049/132] =?UTF-8?q?fix(workflows):=20proper=20suffix=20han?= =?UTF-8?q?dling=20=E2=80=94=20use=20version=5Fset=5Fplatform=20instead=20?= =?UTF-8?q?of=20sed=20[skip=20bump]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .mokogitea/workflows/auto-bump.yml | 7 +- .mokogitea/workflows/auto-release.yml | 33 + .mokogitea/workflows/pre-release.yml | 247 +------- .mokogitea/workflows/update-server.yml | 821 ++++--------------------- 4 files changed, 156 insertions(+), 952 deletions(-) diff --git a/.mokogitea/workflows/auto-bump.yml b/.mokogitea/workflows/auto-bump.yml index 10a7e51..4d0ffc0 100644 --- a/.mokogitea/workflows/auto-bump.yml +++ b/.mokogitea/workflows/auto-bump.yml @@ -63,10 +63,11 @@ jobs: VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null) || true [ -z "$VERSION" ] && { echo "No version found — skipping"; exit 0; } - # Propagate to platform manifests + # Propagate to platform manifests with -dev suffix php ${MOKO_CLI}/version_set_platform.php \ - --path . --version "$VERSION" --branch dev 2>/dev/null || true + --path . --version "$VERSION" --branch dev --stability dev 2>/dev/null || true php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true + VERSION="${VERSION}-dev" # Commit if anything changed if git diff --quiet && git diff --cached --quiet; then @@ -78,7 +79,7 @@ jobs: git config --local user.name "gitea-actions[bot]" git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" git add -A - git commit -m "chore(version): patch bump to ${VERSION} [skip ci]" \ + git commit -m "chore(version): auto-bump patch ${VERSION} [skip ci]" \ --author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" git push origin dev echo "Bumped to ${VERSION}" >> $GITHUB_STEP_SUMMARY diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml index 80908e4..8cdf410 100644 --- a/.mokogitea/workflows/auto-release.yml +++ b/.mokogitea/workflows/auto-release.yml @@ -155,6 +155,8 @@ jobs: echo "skip=true" >> "$GITHUB_OUTPUT" exit 0 fi + # Strip any pre-release suffix merged from dev (e.g. 01.02.20-dev → 01.02.20) + VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//') MAJOR=$(echo "$VERSION" | cut -d. -f1) echo "version=${VERSION}" >> "$GITHUB_OUTPUT" echo "release_tag=stable" >> "$GITHUB_OUTPUT" @@ -261,6 +263,18 @@ jobs: # Step 5 (updates.xml) moved after Step 8 to include SHA-256 checksum + - name: "Step 4b: Promote and prune CHANGELOG" + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.already_released != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + MOKO_API="/tmp/moko-platform-api/cli" + if [ -f "CHANGELOG.md" ]; then + php ${MOKO_API}/changelog_promote.php --path . --version "$VERSION" 2>&1 || true + php ${MOKO_API}/changelog_prune.php --path . --keep 5 2>&1 || true + fi + - name: Commit release changes if: >- steps.version.outputs.skip != 'true' && @@ -456,6 +470,25 @@ jobs: echo "Dev branch reset from main (keeps dev ahead after release)" >> $GITHUB_STEP_SUMMARY + - name: "Step 12: Create version branch from main" + if: steps.version.outputs.skip != 'true' + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.GA_TOKEN }}" + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + BRANCH_NAME="version/${VERSION}" + MAIN_SHA=$(git rev-parse HEAD) + + # Delete old version branch if it exists (same version re-release) + curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}" + + # Create version/XX.YY.ZZ from main + curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed" + + echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY + + # -- Dolibarr post-release: Reset dev version ----------------------------- - name: "Post-release: Reset dev version" diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml index 45e19d2..edbe60e 100644 --- a/.mokogitea/workflows/pre-release.yml +++ b/.mokogitea/workflows/pre-release.yml @@ -1,4 +1,3 @@ -<<<<<<< Updated upstream # Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> # # SPDX-License-Identifier: GPL-3.0-or-later @@ -88,12 +87,20 @@ jobs: VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null) [ -z "$VERSION" ] && VERSION="00.00.01" + # Strip any existing suffix from version before applying stability + VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//') + php ${MOKO_CLI}/version_set_platform.php \ - --path . --version "$VERSION" --branch "${{ github.ref_name }}" 2>/dev/null || true + --path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true # Verify version consistency across all files php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true + # Update VERSION variable with suffix + if [ -n "$SUFFIX" ]; then + VERSION="${VERSION}${SUFFIX}" + fi + # Commit version bump git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" git config --local user.name "gitea-actions[bot]" @@ -113,7 +120,7 @@ jobs: EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') - [ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}${SUFFIX}.zip" + [ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip" echo "version=${VERSION}" >> "$GITHUB_OUTPUT" echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT" @@ -222,237 +229,3 @@ jobs: echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY -======= -# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: moko-platform.Release -# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform -# PATH: /templates/workflows/universal/pre-release.yml.template -# VERSION: 05.01.00 -# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch - -name: "Universal: Pre-Release" - -on: - pull_request: - types: [closed] - branches: - - dev - workflow_dispatch: - inputs: - stability: - description: 'Pre-release channel' - required: true - type: choice - options: - - development - - alpha - - beta - - release-candidate - -permissions: - contents: write - -env: - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} - GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} - -jobs: - build: - name: "Build Pre-Release (${{ inputs.stability || 'development' }})" - runs-on: release - if: >- - github.event_name == 'workflow_dispatch' || - (github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev') - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - token: ${{ secrets.GA_TOKEN }} - - - name: Setup moko-platform tools - env: - MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }} - MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting - run: | - if ! command -v composer &> /dev/null; then - sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 - fi - git clone --depth 1 --branch main --quiet \ - "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ - /tmp/moko-platform-api - cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet - echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV" - - - name: Detect platform - id: platform - run: | - php ${MOKO_CLI}/manifest_read.php --path . --github-output - - - name: Resolve metadata and bump version - id: meta - run: | - STABILITY="${{ inputs.stability || 'development' }}" - - case "$STABILITY" in - development) SUFFIX="-dev"; TAG="development" ;; - alpha) SUFFIX="-alpha"; TAG="alpha" ;; - beta) SUFFIX="-beta"; TAG="beta" ;; - release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;; - esac - - # Read current version (bump already handled by push workflow) - VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null) - [ -z "$VERSION" ] && VERSION="00.00.01" - - php ${MOKO_CLI}/version_set_platform.php \ - --path . --version "$VERSION" --branch "${{ github.ref_name }}" 2>/dev/null || true - - # Verify version consistency across all files - php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true - - # Append suffix to all manifest <version> tags - if [ -n "$SUFFIX" ]; then - find . -maxdepth 4 -name "*.xml" ! -path "./.git/*" ! -path "./build/*" \ - -exec grep -l "<version>${VERSION}</version>" {} \; 2>/dev/null | while read f; do - sed -i "s|<version>${VERSION}</version>|<version>${VERSION}${SUFFIX}</version>|g" "$f" - done - VERSION="${VERSION}${SUFFIX}" - fi - - # Commit version bump - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" - git add -A - git diff --cached --quiet || { - git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]" - git push origin HEAD 2>&1 - } - - # Auto-detect element via manifest_element.php - php ${MOKO_CLI}/manifest_element.php \ - --path . --version "$VERSION" --stability "$STABILITY" \ - --repo "${GITEA_REPO}" --github-output - - # Read back element outputs - EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) - ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) - [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') - [ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip" - - echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT" - echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT" - echo "tag=${TAG}" >> "$GITHUB_OUTPUT" - echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT" - echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT" - - echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ===" - - - name: Create release - id: release - run: | - TAG="${{ steps.meta.outputs.tag }}" - VERSION="${{ steps.meta.outputs.version }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php ${MOKO_CLI}/release_create.php \ - --path . --version "$VERSION" --tag "$TAG" \ - --token "${{ secrets.GA_TOKEN }}" --api-base "$API_BASE" \ - --repo "${GITEA_REPO}" --branch dev --prerelease - - - name: Build package and upload - id: package - run: | - VERSION="${{ steps.meta.outputs.version }}" - TAG="${{ steps.meta.outputs.tag }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php ${MOKO_CLI}/release_package.php \ - --path . --version "$VERSION" --tag "$TAG" \ - --token "${{ secrets.GA_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 - if ! git diff --quiet updates.xml 2>/dev/null; then - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - git add updates.xml - git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]" - git push origin HEAD 2>&1 || echo "WARNING: push failed" - fi - - - name: "Sync updates.xml to all branches" - if: steps.platform.outputs.platform == 'joomla' - run: | - CURRENT_BRANCH="${{ github.ref_name }}" - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - - for BRANCH in main dev; do - [ "$BRANCH" = "$CURRENT_BRANCH" ] && continue - echo "Syncing updates.xml -> ${BRANCH}" - git fetch origin "${BRANCH}" 2>/dev/null || continue - git checkout "origin/${BRANCH}" -- updates.xml 2>/dev/null || continue - git checkout "${CURRENT_BRANCH}" -- updates.xml - if ! git diff --quiet updates.xml 2>/dev/null; then - git add updates.xml - git commit -m "chore: sync updates.xml from ${CURRENT_BRANCH} [skip ci]" - git push origin HEAD:refs/heads/${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed" - fi - git checkout "${CURRENT_BRANCH}" 2>/dev/null - done - - - name: "Delete lesser pre-release channels (cascade)" - continue-on-error: true - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.GA_TOKEN }}" - - php ${MOKO_CLI}/release_cascade.php \ - --stability "${{ steps.meta.outputs.stability }}" \ - --token "${TOKEN}" \ - --api-base "${API_BASE}" - - - name: Summary - if: always() - run: | - VERSION="${{ steps.meta.outputs.version }}" - STABILITY="${{ steps.meta.outputs.stability }}" - ZIP_NAME="${{ steps.meta.outputs.zip_name }}" - SHA256="${{ steps.package.outputs.sha256_zip }}" - echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY - echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY - echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY - echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY - echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY ->>>>>>> Stashed changes diff --git a/.mokogitea/workflows/update-server.yml b/.mokogitea/workflows/update-server.yml index 008e188..b99a5f2 100644 --- a/.mokogitea/workflows/update-server.yml +++ b/.mokogitea/workflows/update-server.yml @@ -1,22 +1,19 @@ -<<<<<<< Updated upstream # Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> # # SPDX-License-Identifier: GPL-3.0-or-later # # FILE INFORMATION # DEFGROUP: Gitea.Workflow -# INGROUP: MokoStandards.Universal +# INGROUP: moko-platform.Universal # REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform # PATH: /templates/workflows/update-server.yml -# VERSION: 04.07.00 -# BRIEF: Update server XML feed with stable/rc/beta/alpha/dev entries (universal) +# VERSION: 05.00.00 +# BRIEF: Pre-release build + update server XML for dev/alpha/beta/rc branches # -# Writes updates.xml with multiple <update> entries: -# - <tag>stable</tag> on push to main (from auto-release) -# - <tag>rc</tag> on push to rc/** -# - <tag>development</tag> on push to dev or dev/** +# Thin wrapper around moko-platform CLI tools. +# Builds packages, updates updates.xml, and optionally deploys via SFTP. # -# Joomla filters by user's "Minimum Stability" setting. +# Joomla filters update entries by the user's "Minimum Stability" setting. name: "Update Server" @@ -67,7 +64,7 @@ permissions: jobs: update-xml: - name: Update updates.xml + name: Update Server runs-on: release if: >- github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || github.event_name == 'push' @@ -98,28 +95,25 @@ jobs: 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: Generate updates.xml entry - id: update + - 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 }}" - REPO="${{ github.repository }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - VERSION=$(php /tmp/moko-platform/cli/version_read.php --path . 2>/dev/null || echo "0.0.0") - # Auto-bump patch on all branches (dev, alpha, beta, rc) + # Auto-bump patch version git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" git config --local user.name "gitea-actions[bot]" - BUMPED=$(php /tmp/moko-platform/cli/version_bump.php --path . 2>/dev/null || true) - if [ -n "$BUMPED" ]; then - VERSION=$(php /tmp/moko-platform/cli/version_read.php --path . 2>/dev/null || echo "$VERSION") - git add -A - git commit -m "chore(version): auto-bump patch ${VERSION} [skip ci]" \ - --author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" 2>/dev/null || true - git push 2>/dev/null || true - fi + php ${MOKO_CLI}/version_bump.php --path . 2>/dev/null || true - # Determine stability from branch or input + VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "0.0.0") + + # Determine stability from branch or manual input if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then STABILITY="${{ inputs.stability }}" elif [[ "$BRANCH" == rc/* ]]; then @@ -128,195 +122,93 @@ jobs: STABILITY="beta" elif [[ "$BRANCH" == alpha/* ]]; then STABILITY="alpha" - elif [[ "$BRANCH" == dev/* ]] || [[ "$BRANCH" == "dev" ]]; then + else STABILITY="development" - else - STABILITY="stable" 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" - # Parse manifest (portable — no grep -P) - MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "./build/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1) - if [ -z "$MANIFEST" ]; then - echo "No Joomla manifest found — skipping" - exit 0 - fi - - # Extract fields using sed (works on all runners) - EXT_NAME=$(sed -n 's/.*<name>\([^<]*\)<\/name>.*/\1/p' "$MANIFEST" | head -1) - EXT_TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) - EXT_ELEMENT=$(sed -n 's/.*<element>\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" | head -1) - EXT_CLIENT=$(sed -n 's/.*<extension[^>]*client="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) - EXT_FOLDER=$(sed -n 's/.*<extension[^>]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) - EXT_VERSION=$(sed -n 's/.*<version>\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1) - TARGET_PLATFORM=$(sed -n 's/.*\(<targetplatform[^/]*\/>\).*/\1/p' "$MANIFEST" | head -1) - PHP_MINIMUM=$(sed -n 's/.*<php_minimum>\([^<]*\)<\/php_minimum>.*/\1/p' "$MANIFEST" | head -1) - - # Fallbacks - [ -z "$EXT_NAME" ] && EXT_NAME="${{ github.event.repository.name }}" - [ -z "$EXT_TYPE" ] && EXT_TYPE="component" - - # Derive element if not in manifest: try XML filename, then repo name - if [ -z "$EXT_ELEMENT" ]; then - EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]') - case "$EXT_ELEMENT" in - templatedetails|manifest|*.xml) EXT_ELEMENT=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;; - esac - fi - - # Use manifest version if README version is empty - [ "$VERSION" = "0.0.0" ] && [ -n "$EXT_VERSION" ] && VERSION="$EXT_VERSION" - - [ -z "$TARGET_PLATFORM" ] && TARGET_PLATFORM=$(printf '<targetplatform name="joomla" version="((5.[0-9])|(6.[0-9]))" %s>' "/") - - # Joomla requires <client> on ALL extension types for update matching - if [ -n "$EXT_CLIENT" ]; then - CLIENT_TAG="<client>${EXT_CLIENT}</client>" - else - CLIENT_TAG="<client>site</client>" - fi - - FOLDER_TAG="" - [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ] && FOLDER_TAG="<folder>${EXT_FOLDER}</folder>" - - PHP_TAG="" - [ -n "$PHP_MINIMUM" ] && PHP_TAG="<php_minimum>${PHP_MINIMUM}</php_minimum>" - - # Version suffix for non-stable - DISPLAY_VERSION="$VERSION" - case "$STABILITY" in - development) DISPLAY_VERSION="${VERSION}-dev" ;; - alpha) DISPLAY_VERSION="${VERSION}-alpha" ;; - beta) DISPLAY_VERSION="${VERSION}-beta" ;; - rc) DISPLAY_VERSION="${VERSION}-rc" ;; - esac - - MAJOR=$(echo "$VERSION" | awk -F. '{print $1}') - - # Each stability level has its own release tag - case "$STABILITY" in - development) RELEASE_TAG="development" ;; - alpha) RELEASE_TAG="alpha" ;; - beta) RELEASE_TAG="beta" ;; - rc) RELEASE_TAG="release-candidate" ;; - *) RELEASE_TAG="v${MAJOR}" ;; - esac - - PACKAGE_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.zip" - DOWNLOAD_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${PACKAGE_NAME}" - INFO_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}" - - # -- Build install packages and upload to release -------------------- - php /tmp/moko-platform/cli/release_package.php \ - --path . --version "${DISPLAY_VERSION}" --tag "${RELEASE_TAG}" \ - --token "${{ secrets.GA_TOKEN }}" --api-base "${API_BASE}" \ - --repo "${GITEA_REPO}" --output /tmp 2>&1 || true - - echo "Package built and uploaded for ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY - - # -- Build the new entry (canonical format matching release.yml) -- - NEW_ENTRY="" - NEW_ENTRY="${NEW_ENTRY} <update>\n" - NEW_ENTRY="${NEW_ENTRY} <name>${EXT_NAME}</name>\n" - NEW_ENTRY="${NEW_ENTRY} <description>${EXT_NAME} ${STABILITY} build.</description>\n" - NEW_ENTRY="${NEW_ENTRY} <element>${EXT_ELEMENT}</element>\n" - NEW_ENTRY="${NEW_ENTRY} <type>${EXT_TYPE}</type>\n" - [ -n "$CLIENT_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${CLIENT_TAG}\n" - [ -n "$FOLDER_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${FOLDER_TAG}\n" - NEW_ENTRY="${NEW_ENTRY} <version>${VERSION}</version>\n" - NEW_ENTRY="${NEW_ENTRY} <creationDate>$(date +%Y-%m-%d)</creationDate>\n" - NEW_ENTRY="${NEW_ENTRY} <infourl title='${EXT_NAME}'>https://git.mokoconsulting.tech/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${RELEASE_TAG}</infourl>\n" - NEW_ENTRY="${NEW_ENTRY} <downloads>\n" - NEW_ENTRY="${NEW_ENTRY} <downloadurl type='full' format='zip'>${DOWNLOAD_URL}</downloadurl>\n" - NEW_ENTRY="${NEW_ENTRY} </downloads>\n" - [ -n "$SHA256" ] && NEW_ENTRY="${NEW_ENTRY} <sha256>${SHA256}</sha256>\n" - NEW_ENTRY="${NEW_ENTRY} <tags><tag>${STABILITY}</tag></tags>\n" - NEW_ENTRY="${NEW_ENTRY} <maintainer>Moko Consulting</maintainer>\n" - NEW_ENTRY="${NEW_ENTRY} <maintainerurl>https://mokoconsulting.tech</maintainerurl>\n" - NEW_ENTRY="${NEW_ENTRY} <targetplatform name='joomla' version='(5|6).*'/>\n" - [ -n "$PHP_MINIMUM" ] && NEW_ENTRY="${NEW_ENTRY} <php_minimum>${PHP_MINIMUM}</php_minimum>\n" - NEW_ENTRY="${NEW_ENTRY} </update>" - - # -- Write new entry to temp file -------------------------------- - printf '%b' "$NEW_ENTRY" > /tmp/new_entry.xml - - # -- Merge into updates.xml ---------------------------------------- - # Cascade: stable→all | rc→rc+lower | beta→beta+lower | alpha→alpha+dev | dev→dev - CASCADE_MAP="stable:development,alpha,beta,rc,stable rc:development,alpha,beta,rc beta:development,alpha,beta alpha:development,alpha development:development" - TARGETS="" - for entry in $CASCADE_MAP; do - key="${entry%%:*}" - vals="${entry#*:}" - if [ "$key" = "${STABILITY}" ]; then - TARGETS="$vals" - break - fi - done - [ -z "$TARGETS" ] && TARGETS="${STABILITY}" - - echo "Cascade: ${STABILITY} → ${TARGETS}" - - # Create updates.xml if missing - if [ ! -f "updates.xml" ]; then - printf '%s\n' "<?xml version='1.0' encoding='UTF-8'?>" > updates.xml - printf '%s\n' "<!-- Copyright (C) $(date +%Y) Moko Consulting -->" >> updates.xml - printf '%s\n' "<updates>" >> updates.xml - printf '%s\n' "</updates>" >> updates.xml - fi - - # Update existing blocks or create missing ones - export PY_TARGETS="$TARGETS" PY_VERSION="$VERSION" PY_DATE="$(date +%Y-%m-%d)" - python3 << 'PYEOF' - import re, os - - targets = os.environ["PY_TARGETS"].split(",") - version = os.environ["PY_VERSION"] - date = os.environ["PY_DATE"] - - with open("updates.xml") as f: - content = f.read() - with open("/tmp/new_entry.xml") as f: - new_entry_template = f.read() - - for tag in targets: - tag = tag.strip() - # Build entry with this tag's name - new_entry = re.sub(r"<tag>[^<]*</tag>", f"<tag>{tag}</tag>", new_entry_template) - - # Try to find existing block (handles both single-line and multi-line <tags>) - block_pattern = r"(<update>(?:(?!</update>).)*?<tag>" + re.escape(tag) + r"</tag>.*?</update>)" - match = re.search(block_pattern, content, re.DOTALL) - - if match: - # Update in place — replace entire block - content = content.replace(match.group(1), new_entry.strip()) - print(f" UPDATED: <tag>{tag}</tag> → {version}") - else: - # Create — insert before </updates> - content = content.replace("</updates>", "\n" + new_entry.strip() + "\n\n</updates>") - print(f" CREATED: <tag>{tag}</tag> → {version}") - - # Clean up excessive blank lines - content = re.sub(r"\n{3,}", "\n\n", content) - - with open("updates.xml", "w") as f: - f.write(content) - PYEOF - - # Commit - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - git add updates.xml + # Commit version bump if changed + git add -A git diff --cached --quiet || { - git commit -m "chore: update updates.xml (${STABILITY}: ${DISPLAY_VERSION}) [skip ci]" \ + git commit -m "chore(version): auto-bump ${VERSION} [skip ci]" \ --author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" git push } - # -- Sync updates.xml to main (for non-main branches) ---------------------- + - name: Create release and upload package + id: package + run: | + VERSION="${{ steps.meta.outputs.version }}" + TAG="${{ steps.meta.outputs.tag }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + + # Create or update Gitea release + php ${MOKO_CLI}/release_create.php \ + --path . --version "$VERSION" --tag "$TAG" \ + --token "${{ secrets.GA_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.GA_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 config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git add updates.xml + git diff --cached --quiet || { + git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]" + git push + } + - name: Sync updates.xml to main - if: github.ref_name != 'main' + if: github.ref_name != 'main' && steps.platform.outputs.platform == 'joomla' run: | API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" GA_TOKEN="${{ secrets.GA_TOKEN }}" @@ -332,7 +224,7 @@ jobs: payload = json.dumps({ 'content': content, 'sha': '${FILE_SHA}', - 'message': 'chore: sync updates.xml from ${STABILITY} [skip ci]', + 'message': 'chore: sync updates.xml from ${{ steps.meta.outputs.stability }} [skip ci]', 'branch': 'main' }).encode() req = urllib.request.Request( @@ -346,13 +238,8 @@ jobs: urllib.request.urlopen(req) print('updates.xml synced to main') except Exception as e: - print(f'ERROR: failed to sync updates.xml to main: {e}', file=sys.stderr) - sys.exit(1) - " \ - && echo "updates.xml synced to main (${STABILITY})" >> $GITHUB_STEP_SUMMARY \ - || echo "::error::failed to sync updates.xml to main" >> $GITHUB_STEP_SUMMARY - else - echo "::error::could not get updates.xml SHA from main — file may not exist on main yet" >> $GITHUB_STEP_SUMMARY + print(f'WARNING: sync to main failed: {e}', file=sys.stderr) + " fi - name: SFTP deploy to dev server @@ -366,9 +253,8 @@ jobs: DEV_KEY: ${{ secrets.DEV_FTP_KEY }} DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }} run: | - # -- Permission check: admin or maintain role required -------- + # Permission check: admin or maintain role required ACTOR="${{ github.actor }}" - REPO="${{ github.repository }}" API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ @@ -401,513 +287,24 @@ jobs: printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json fi - PLATFORM=$(php /tmp/moko-platform/cli/platform_detect.php --path . 2>/dev/null || true) - if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/moko-platform/deploy/deploy-joomla.php" ]; then - php /tmp/moko-platform/deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json - elif [ -f "/tmp/moko-platform/deploy/deploy-sftp.php" ]; then - php /tmp/moko-platform/deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json + 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: Validate updates.xml integrity - run: | - ERRORS=0 - - if [ ! -f "updates.xml" ]; then - echo "::error::updates.xml not found" - exit 1 - fi - - # Well-formed XML - if ! python3 -c "import xml.etree.ElementTree as ET; ET.parse('updates.xml')" 2>/dev/null; then - echo "::error::updates.xml is not valid XML" - ERRORS=$((ERRORS+1)) - fi - - python3 << 'PYEOF' - import xml.etree.ElementTree as ET, sys, re, os - - tree = ET.parse("updates.xml") - root = tree.getroot() - updates = root.findall("update") - errors = 0 - warnings = 0 - seen_tags = set() - - # All 5 channels MUST be present - REQUIRED_CHANNELS = {"stable", "rc", "beta", "alpha", "dev"} - VALID_TAGS = REQUIRED_CHANNELS | {"development"} # accept legacy alias - REPO = os.environ.get("GITEA_REPO", "") - ORG = os.environ.get("GITEA_ORG", "MokoConsulting") - REPO_BASE = f"https://git.mokoconsulting.tech/{ORG}/" - - # Gitea release tag names per channel (Moko standard) - RELEASE_TAG_MAP = { - "stable": "stable", - "rc": "release-candidate", - "beta": "beta", - "alpha": "alpha", - "dev": "development", - "development": "development", - } - - # Joomla update XML required fields per - # https://docs.joomla.org/Deploying_an_Update_Server - REQUIRED_FIELDS = ["name", "element", "type", "version", "infourl"] - - for i, u in enumerate(updates): - tag_el = u.find("tags/tag") - tag = tag_el.text.strip() if tag_el is not None and tag_el.text else None - label = f"Entry {i+1} (<tag>{tag or '?'}</tag>)" - - # -- Required Joomla fields -- - for field in REQUIRED_FIELDS: - el = u.find(field) - if el is None or not (el.text or "").strip(): - print(f"::error::{label}: missing required <{field}>") - errors += 1 - - # -- <downloads><downloadurl> -- - dl = u.find("downloads/downloadurl") - if dl is None or not (dl.text or "").strip(): - print(f"::error::{label}: missing <downloads><downloadurl>") - errors += 1 - else: - dl_url = dl.text.strip() - # Must point to org repo - if REPO_BASE not in dl_url: - print(f"::error::{label}: download URL not under {REPO_BASE}: {dl_url}") - errors += 1 - # Must end in .zip - if not dl_url.endswith(".zip"): - print(f"::error::{label}: download URL must end in .zip: {dl_url}") - errors += 1 - # Must use correct Gitea release tag in path - if tag and tag in RELEASE_TAG_MAP: - expected_tag = RELEASE_TAG_MAP[tag] - if f"/download/{expected_tag}/" not in dl_url: - print(f"::error::{label}: download URL should contain /download/{expected_tag}/ but got: {dl_url}") - errors += 1 - - # -- <client> (required for Joomla to match update) -- - client = u.find("client") - if client is None or not (client.text or "").strip(): - print(f"::error::{label}: missing <client> (required for Joomla update matching)") - errors += 1 - - # -- <targetplatform> -- - tp = u.find("targetplatform") - if tp is None: - print(f"::error::{label}: missing <targetplatform>") - errors += 1 - else: - tp_name = tp.get("name", "") - tp_ver = tp.get("version", "") - if tp_name != "joomla": - print(f"::error::{label}: targetplatform name should be 'joomla', got '{tp_name}'") - errors += 1 - if not tp_ver: - print(f"::error::{label}: targetplatform missing version regex") - errors += 1 - elif "5" not in tp_ver or "6" not in tp_ver: - print(f"::warning::{label}: targetplatform version may not cover Joomla 5+6: {tp_ver}") - warnings += 1 - - # -- <type> must be valid Joomla type -- - type_el = u.find("type") - if type_el is not None and type_el.text: - valid_types = {"component", "module", "plugin", "template", "library", "package", "file"} - if type_el.text.strip() not in valid_types: - print(f"::error::{label}: invalid type '{type_el.text}' (expected: {valid_types})") - errors += 1 - - # -- <version> format (XX.YY.ZZ with optional suffix) -- - ver_el = u.find("version") - if ver_el is not None and ver_el.text: - if not re.match(r"^\d{2}\.\d{2}\.\d{2}(-\w+)?$", ver_el.text.strip()): - print(f"::warning::{label}: version '{ver_el.text}' does not match XX.YY.ZZ format") - warnings += 1 - - # -- <maintainer> and <maintainerurl> -- - for field in ["maintainer", "maintainerurl"]: - el = u.find(field) - if el is None or not (el.text or "").strip(): - print(f"::warning::{label}: missing <{field}>") - warnings += 1 - - # -- Valid stability tag -- - if tag is None: - print(f"::error::{label}: missing <tags><tag>") - errors += 1 - elif tag not in VALID_TAGS: - print(f"::error::{label}: invalid tag '{tag}' (expected: {VALID_TAGS})") - errors += 1 - - # -- Duplicate tag check -- - norm_tag = "dev" if tag == "development" else tag - if norm_tag in seen_tags: - print(f"::error::{label}: duplicate channel '{tag}'") - errors += 1 - if norm_tag: - seen_tags.add(norm_tag) - - # -- All 5 channels must exist -- - missing = REQUIRED_CHANNELS - seen_tags - if missing: - print(f"::error::Missing required update channels: {', '.join(sorted(missing))}") - errors += 1 - - # -- Version ordering: higher stability must not exceed dev version -- - channel_versions = {} - for u in updates: - tag_el = u.find("tags/tag") - ver_el = u.find("version") - if tag_el is not None and ver_el is not None and tag_el.text and ver_el.text: - norm = "dev" if tag_el.text.strip() == "development" else tag_el.text.strip() - # Strip suffix for comparison (01.00.18-dev -> 01.00.18) - base_ver = re.sub(r"-\w+$", "", ver_el.text.strip()) - channel_versions[norm] = base_ver - - # Cascade check: dev >= alpha >= beta >= rc >= stable - ORDER = ["dev", "alpha", "beta", "rc", "stable"] - for j in range(1, len(ORDER)): - current = ORDER[j] - previous = ORDER[j - 1] - if current in channel_versions and previous in channel_versions: - if channel_versions[current] > channel_versions[previous]: - print(f"::error::{current} version ({channel_versions[current]}) is ahead of {previous} ({channel_versions[previous]})") - errors += 1 - - # -- Summary -- - print(f"\nupdates.xml validation: {len(updates)} entries, {errors} error(s), {warnings} warning(s)") - if errors > 0: - sys.exit(1) - PYEOF - - name: Summary if: always() run: | - echo "## Joomla Update Server" >> $GITHUB_STEP_SUMMARY + VERSION="${{ steps.meta.outputs.version }}" + STABILITY="${{ steps.meta.outputs.stability }}" + DISPLAY="${{ steps.meta.outputs.display_version }}" + echo "## Update Server" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Version | \`${DISPLAY_VERSION}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Element | \`${EXT_ELEMENT}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Download | [ZIP](${DOWNLOAD_URL}) |" >> $GITHUB_STEP_SUMMARY -======= -# 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.GA_TOKEN }} - fetch-depth: 0 - - - name: Setup moko-platform tools - env: - MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }} - MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting - COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.GA_TOKEN }}"}}}' - run: | - if ! command -v composer &> /dev/null; then - sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 - fi - if [ -d "/tmp/moko-platform" ]; then - echo "moko-platform already available — skipping clone" - else - 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 - fi - 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 }}" - - # Auto-bump patch version - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - 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") - - # Propagate version to all manifest files - php ${MOKO_CLI}/version_set_platform.php --path . --version "$VERSION" --branch "$BRANCH" 2>/dev/null || true - php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true - - # 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 - - # Append suffix to all manifest <version> tags (non-stable only) - if [ -n "$SUFFIX" ]; then - find . -maxdepth 4 -name "*.xml" ! -path "./.git/*" ! -path "./build/*" \ - -exec grep -l "<version>${VERSION}</version>" {} \; 2>/dev/null | while read f; do - sed -i "s|<version>${VERSION}</version>|<version>${VERSION}${SUFFIX}</version>|g" "$f" - done - 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.GA_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.GA_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 config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - git add updates.xml - git diff --cached --quiet || { - git commit -m "chore: update ${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}" - GA_TOKEN="${{ secrets.GA_TOKEN }}" - - FILE_SHA=$(curl -sf -H "Authorization: token ${GA_TOKEN}" \ - "${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true) - - if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then - 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 ${GA_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.GA_TOKEN }}" \ - "${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \ - python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read") - case "$PERMISSION" in - admin|maintain|write) ;; - *) - echo "Deploy denied: ${ACTOR} has '${PERMISSION}' — requires admin, maintain, or write" - exit 0 - ;; - esac - - [ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured — skipping SFTP"; exit 0; } - - SOURCE_DIR="src" - [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" - [ ! -d "$SOURCE_DIR" ] && exit 0 - - PORT="${DEV_PORT:-22}" - REMOTE="${DEV_PATH%/}" - [ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}" - - printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \ - "$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json - if [ -n "$DEV_KEY" ]; then - echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key - printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json - else - printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json - fi - - PLATFORM=$(php ${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 ->>>>>>> Stashed changes + echo "| Version | \`${DISPLAY}\` |" >> $GITHUB_STEP_SUMMARY From f3a76c93be0c68fb0f2512d37cc61bec2e1bf27f Mon Sep 17 00:00:00 2001 From: Moko Consulting <hello@mokoconsulting.tech> Date: Thu, 28 May 2026 14:24:11 -0500 Subject: [PATCH 050/132] refactor(workflows): rename secrets MOKOGITEA_TOKEN/GITHUB_TOKEN, use x-access-token [skip bump] --- .mokogitea/workflows/auto-bump.yml | 6 +-- .mokogitea/workflows/auto-release.yml | 61 +++++++++++++------------- .mokogitea/workflows/cascade-dev.yml | 20 ++++----- .mokogitea/workflows/cleanup.yml | 18 ++++---- .mokogitea/workflows/pr-check.yml | 26 +++++++++-- .mokogitea/workflows/pre-release.yml | 12 ++--- .mokogitea/workflows/update-server.yml | 25 ++++++----- 7 files changed, 94 insertions(+), 74 deletions(-) diff --git a/.mokogitea/workflows/auto-bump.yml b/.mokogitea/workflows/auto-bump.yml index 4d0ffc0..572facd 100644 --- a/.mokogitea/workflows/auto-bump.yml +++ b/.mokogitea/workflows/auto-bump.yml @@ -37,7 +37,7 @@ jobs: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: - token: ${{ secrets.GA_TOKEN }} + token: ${{ secrets.MOKOGITEA_TOKEN }} fetch-depth: 1 - name: Setup moko-platform tools @@ -49,7 +49,7 @@ jobs: echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV" else git clone --depth 1 --branch main --quiet \ - "https://x-access-token:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \ + "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \ /tmp/moko-platform-api cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV" @@ -77,7 +77,7 @@ jobs: git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" git config --local user.name "gitea-actions[bot]" - git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" git add -A git commit -m "chore(version): auto-bump patch ${VERSION} [skip ci]" \ --author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml index 8cdf410..8be9b71 100644 --- a/.mokogitea/workflows/auto-release.yml +++ b/.mokogitea/workflows/auto-release.yml @@ -63,12 +63,12 @@ jobs: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: - token: ${{ secrets.GA_TOKEN }} + token: ${{ secrets.MOKOGITEA_TOKEN }} fetch-depth: 1 - name: Setup moko-platform tools env: - MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }} + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting run: | if ! command -v composer &> /dev/null; then @@ -85,7 +85,7 @@ jobs: API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" php /tmp/moko-platform-api/cli/release_promote.php \ --from auto --to release-candidate \ - --token "${{ secrets.GA_TOKEN }}" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" \ --api-base "${API_BASE}" \ --branch "${{ github.event.pull_request.head.ref || 'dev' }}" @@ -95,7 +95,7 @@ jobs: API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" php /tmp/moko-platform-api/cli/release_cascade.php \ --stability release-candidate \ - --token "${{ secrets.GA_TOKEN }}" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" \ --api-base "${API_BASE}" - name: Summary @@ -116,14 +116,20 @@ jobs: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: - token: ${{ secrets.GA_TOKEN }} + token: ${{ secrets.MOKOGITEA_TOKEN }} fetch-depth: 0 + - name: Configure git for bot pushes + run: | + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + - name: Setup moko-platform tools env: - MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }} + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN }}"}}' + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GITHUB_TOKEN }}"}}' run: | # Ensure PHP + Composer are available if ! command -v composer &> /dev/null; then @@ -169,7 +175,7 @@ jobs: if: steps.version.outputs.skip != 'true' run: | API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - RC_JSON=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + RC_JSON=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \ "${API_BASE}/releases/tags/release-candidate" 2>/dev/null || echo "{}") RC_ID=$(echo "$RC_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('id',''))" 2>/dev/null || true) @@ -285,10 +291,6 @@ jobs: exit 0 fi VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - # Set push URL with token for branch-protected repos - git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" git add -A git commit -m "chore(release): build ${VERSION} [skip ci]" \ --author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" @@ -320,7 +322,7 @@ jobs: API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" php /tmp/moko-platform-api/cli/release_promote.php \ --from release-candidate --to stable \ - --token "${{ secrets.GA_TOKEN }}" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" \ --api-base "${API_BASE}" \ --path . --branch main echo "Promoted RC → stable (${VERSION})" >> $GITHUB_STEP_SUMMARY @@ -336,7 +338,7 @@ jobs: API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" php /tmp/moko-platform-api/cli/release_create.php \ --path . --version "$VERSION" --tag "$RELEASE_TAG" \ - --token "${{ secrets.GA_TOKEN }}" --api-base "$API_BASE" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ --repo "${GITEA_REPO}" --branch main echo "Release created: ${VERSION}" >> $GITHUB_STEP_SUMMARY @@ -352,7 +354,7 @@ jobs: API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" php /tmp/moko-platform-api/cli/release_package.php \ --path . --version "$VERSION" --tag "$RELEASE_TAG" \ - --token "${{ secrets.GA_TOKEN }}" --api-base "$API_BASE" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ --repo "${GITEA_REPO}" --output /tmp || true # -- STEP 5: Write update stream (after build so SHA-256 is available) ----- @@ -363,9 +365,9 @@ jobs: SHA256="${{ steps.package.outputs.sha256_zip }}" # Fetch latest updates.xml from main so preserve logic has all channels - GA_TOKEN="${{ secrets.GA_TOKEN }}" + GITEA_TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" - curl -sf -H "Authorization: token ${GA_TOKEN}" \ + curl -sf -H "Authorization: token ${GITEA_TOKEN}" \ "${API}/contents/updates.xml?ref=main" 2>/dev/null | \ python3 -c "import sys,json,base64; print(base64.b64decode(json.load(sys.stdin)['content']).decode())" \ > updates.xml 2>/dev/null || true @@ -380,9 +382,6 @@ jobs: # Commit updates.xml if changed if ! git diff --quiet updates.xml 2>/dev/null; then - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" git add updates.xml git commit -m "chore: update stable channel ${VERSION} [skip ci]" \ --author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" @@ -398,7 +397,7 @@ jobs: RELEASE_TAG="${{ steps.version.outputs.release_tag }}" php /tmp/moko-platform-api/cli/release_body_update.php \ --path . --version "${VERSION}" --tag "${RELEASE_TAG}" \ - --token "${{ secrets.GA_TOKEN }}" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" \ --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \ 2>&1 || true echo "Release body updated" >> $GITHUB_STEP_SUMMARY @@ -407,7 +406,7 @@ jobs: - name: "Step 9: Mirror release to GitHub" if: >- steps.version.outputs.skip != 'true' && - secrets.GH_TOKEN != '' + secrets.GITHUB_TOKEN != '' continue-on-error: true run: | VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" @@ -416,8 +415,8 @@ jobs: API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" php /tmp/moko-platform-api/cli/release_mirror.php \ --version "$VERSION" --tag "$RELEASE_TAG" \ - --token "${{ secrets.GA_TOKEN }}" --api-base "$API_BASE" \ - --gh-token "${{ secrets.GH_TOKEN }}" --gh-repo "$GH_REPO" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ + --gh-token "${{ secrets.GITHUB_TOKEN }}" --gh-repo "$GH_REPO" \ --branch main 2>&1 || true echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY @@ -425,14 +424,14 @@ jobs: - name: "Step 10: Push main to GitHub mirror" if: >- steps.version.outputs.skip != 'true' && - secrets.GH_TOKEN != '' + secrets.GITHUB_TOKEN != '' continue-on-error: true run: | GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1) GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2) - git remote add github "https://x-access-token:${{ secrets.GH_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \ - git remote set-url github "https://x-access-token:${{ secrets.GH_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" + git remote add github "https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \ + git remote set-url github "https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" git fetch origin main --depth=1 git push github origin/main:refs/heads/main --force 2>/dev/null \ && echo "main branch pushed to GitHub mirror" \ @@ -448,7 +447,7 @@ jobs: php /tmp/moko-platform-api/cli/release_cascade.php \ --stability stable \ --version "${VERSION}" \ - --token "${{ secrets.GA_TOKEN }}" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" \ --api-base "${API_BASE}" 2>/dev/null || true - name: "Step 11: Delete and recreate dev branch from main" @@ -456,7 +455,7 @@ jobs: continue-on-error: true run: | API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.GA_TOKEN }}" + TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" # Delete dev branch curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ @@ -475,7 +474,7 @@ jobs: continue-on-error: true run: | API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.GA_TOKEN }}" + TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" BRANCH_NAME="version/${VERSION}" MAIN_SHA=$(git rev-parse HEAD) @@ -497,7 +496,7 @@ jobs: run: | API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" php /tmp/moko-platform-api/cli/version_reset_dev.php \ - --token "${{ secrets.GA_TOKEN }}" --api-base "${API_BASE}" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \ --branch dev --path . 2>&1 || true # -- Summary -------------------------------------------------------------- diff --git a/.mokogitea/workflows/cascade-dev.yml b/.mokogitea/workflows/cascade-dev.yml index 4dbb135..f7f0b3c 100644 --- a/.mokogitea/workflows/cascade-dev.yml +++ b/.mokogitea/workflows/cascade-dev.yml @@ -4,8 +4,8 @@ # # FILE INFORMATION # DEFGROUP: Gitea.Workflow -# INGROUP: MokoStandards.Maintenance -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API +# INGROUP: moko-platform.Maintenance +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform # PATH: /templates/workflows/cascade-dev.yml.template # VERSION: 02.00.00 # BRIEF: Forward-merge main → all open branches after every push to main @@ -52,7 +52,7 @@ jobs: - name: Discover target branches id: branches env: - GA_TOKEN: ${{ secrets.GA_TOKEN }} + GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} run: | API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" @@ -61,7 +61,7 @@ jobs: ALL_BRANCHES="" while true; do BATCH=$(curl -sS \ - -H "Authorization: token ${GA_TOKEN}" \ + -H "Authorization: token ${GITEA_TOKEN}" \ "${API}/branches?page=${PAGE}&limit=50" \ | jq -r '.[].name // empty') [ -z "$BATCH" ] && break @@ -93,7 +93,7 @@ jobs: - name: Cascade to all target branches if: steps.branches.outputs.targets != '' env: - GA_TOKEN: ${{ secrets.GA_TOKEN }} + GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} run: | API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" SHORT_SHA="${GITHUB_SHA:0:7}" @@ -111,7 +111,7 @@ jobs: # Check if branch is already up to date ENCODED_BRANCH=$(echo "$BRANCH" | sed 's|/|%2F|g') RESPONSE=$(curl -sS \ - -H "Authorization: token ${GA_TOKEN}" \ + -H "Authorization: token ${GITEA_TOKEN}" \ "${API}/compare/${ENCODED_BRANCH}...main") AHEAD=$(echo "$RESPONSE" | jq '.total_commits // 0') @@ -126,7 +126,7 @@ jobs: # Check for existing cascade PR EXISTING=$(curl -sS \ - -H "Authorization: token ${GA_TOKEN}" \ + -H "Authorization: token ${GITEA_TOKEN}" \ "${API}/pulls?state=open&head=${GITEA_ORG}:main&base=${ENCODED_BRANCH}&limit=1") EXISTING_COUNT=$(echo "$EXISTING" | jq 'length') @@ -139,7 +139,7 @@ jobs: # Create cascade PR PR_RESPONSE=$(curl -sS -w "\n%{http_code}" \ -X POST \ - -H "Authorization: token ${GA_TOKEN}" \ + -H "Authorization: token ${GITEA_TOKEN}" \ -H "Content-Type: application/json" \ -d "{ \"title\": \"chore: cascade main → ${BRANCH} (${SHORT_SHA}) [skip ci]\", @@ -165,7 +165,7 @@ jobs: # Try auto-merge PR_DATA=$(curl -sS \ - -H "Authorization: token ${GA_TOKEN}" \ + -H "Authorization: token ${GITEA_TOKEN}" \ "${API}/pulls/${PR_NUMBER}") MERGEABLE=$(echo "$PR_DATA" | jq -r '.mergeable // false') @@ -178,7 +178,7 @@ jobs: MERGE_RESPONSE=$(curl -sS -w "\n%{http_code}" \ -X POST \ - -H "Authorization: token ${GA_TOKEN}" \ + -H "Authorization: token ${GITEA_TOKEN}" \ -H "Content-Type: application/json" \ -d "{ \"Do\": \"merge\", diff --git a/.mokogitea/workflows/cleanup.yml b/.mokogitea/workflows/cleanup.yml index 3a81856..29ca4d4 100644 --- a/.mokogitea/workflows/cleanup.yml +++ b/.mokogitea/workflows/cleanup.yml @@ -4,8 +4,8 @@ # # FILE INFORMATION # DEFGROUP: Gitea.Workflow -# INGROUP: MokoStandards.Maintenance -# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards +# INGROUP: moko-platform.Maintenance +# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform # 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.GA_TOKEN }} + token: ${{ secrets.MOKOGITEA_TOKEN }} - name: Delete merged branches env: - GA_TOKEN: ${{ secrets.GA_TOKEN }} + GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} run: | echo "=== Merged Branch Cleanup ===" API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" # List branches via API - BRANCHES=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \ + BRANCHES=$(curl -sS -H "Authorization: token ${GITEA_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 ${GA_TOKEN}" \ + curl -sS -X DELETE -H "Authorization: token ${GITEA_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.GA_TOKEN }} + GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} run: | echo "=== Workflow Run Cleanup ===" API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ) # Get old completed runs - RUNS=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \ + RUNS=$(curl -sS -H "Authorization: token ${GITEA_TOKEN}" \ "${API}/actions/runs?status=completed&limit=50" | \ jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null) DELETED=0 for RUN_ID in $RUNS; do - curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \ + curl -sS -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \ "${API}/actions/runs/${RUN_ID}" 2>/dev/null || true DELETED=$((DELETED + 1)) done diff --git a/.mokogitea/workflows/pr-check.yml b/.mokogitea/workflows/pr-check.yml index 014f6b0..df06523 100644 --- a/.mokogitea/workflows/pr-check.yml +++ b/.mokogitea/workflows/pr-check.yml @@ -4,8 +4,8 @@ # # FILE INFORMATION # DEFGROUP: Gitea.Workflow -# INGROUP: MokoStandards.CI -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API +# INGROUP: moko-platform.CI +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform # PATH: /templates/workflows/universal/pr-check.yml.template # VERSION: 05.00.00 # BRIEF: PR gate — branch policy + code validation before merge @@ -108,7 +108,9 @@ jobs: - name: Detect platform id: platform run: | - PLATFORM=$(cat .mokogitea/.moko-platform 2>/dev/null | tr -d '[:space:]') + # Read platform from XML manifest (<platform> tag) or plain text fallback + PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1) + [ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]') [ -z "$PLATFORM" ] && PLATFORM="generic" echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT" @@ -192,3 +194,21 @@ jobs: FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l) echo "Source: ${FILE_COUNT} files" [ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; } + + # ── Pre-Release RC Build ───────────────────────────────────────────────── + pre-release: + name: Build RC Package + runs-on: ubuntu-latest + needs: [branch-policy, validate] + + steps: + - name: Trigger RC pre-release + env: + GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + REPO: ${{ github.repository }} + BRANCH: ${{ github.head_ref }} + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + run: | + curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}" + echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY + echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml index edbe60e..7920f53 100644 --- a/.mokogitea/workflows/pre-release.yml +++ b/.mokogitea/workflows/pre-release.yml @@ -50,11 +50,11 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 - token: ${{ secrets.GA_TOKEN }} + token: ${{ secrets.MOKOGITEA_TOKEN }} - name: Setup moko-platform tools env: - MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }} + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting run: | if ! command -v composer &> /dev/null; then @@ -104,7 +104,7 @@ jobs: # Commit version bump git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" git config --local user.name "gitea-actions[bot]" - git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" git add -A git diff --cached --quiet || { git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]" @@ -139,7 +139,7 @@ jobs: API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" php ${MOKO_CLI}/release_create.php \ --path . --version "$VERSION" --tag "$TAG" \ - --token "${{ secrets.GA_TOKEN }}" --api-base "$API_BASE" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ --repo "${GITEA_REPO}" --branch dev --prerelease - name: Build package and upload @@ -150,7 +150,7 @@ jobs: API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" php ${MOKO_CLI}/release_package.php \ --path . --version "$VERSION" --tag "$TAG" \ - --token "${{ secrets.GA_TOKEN }}" --api-base "$API_BASE" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ --repo "${GITEA_REPO}" --output /tmp || true - name: Update updates.xml @@ -207,7 +207,7 @@ jobs: continue-on-error: true run: | API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.GA_TOKEN }}" + TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" php ${MOKO_CLI}/release_cascade.php \ --stability "${{ steps.meta.outputs.stability }}" \ diff --git a/.mokogitea/workflows/update-server.yml b/.mokogitea/workflows/update-server.yml index b99a5f2..cd2eff0 100644 --- a/.mokogitea/workflows/update-server.yml +++ b/.mokogitea/workflows/update-server.yml @@ -73,14 +73,14 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 with: - token: ${{ secrets.GA_TOKEN }} + token: ${{ secrets.MOKOGITEA_TOKEN }} fetch-depth: 0 - name: Setup moko-platform tools env: - MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }} + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting - COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.GA_TOKEN }}"}}}' + COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.MOKOGITEA_TOKEN }}"}}}' run: | 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 @@ -106,9 +106,12 @@ jobs: run: | BRANCH="${{ github.ref_name }}" - # Auto-bump patch version + # 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") @@ -169,13 +172,13 @@ jobs: # Create or update Gitea release php ${MOKO_CLI}/release_create.php \ --path . --version "$VERSION" --tag "$TAG" \ - --token "${{ secrets.GA_TOKEN }}" --api-base "$API_BASE" \ + --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.GA_TOKEN }}" --api-base "$API_BASE" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ --repo "${GITEA_REPO}" --output /tmp || true - name: Update updates.xml @@ -199,8 +202,6 @@ jobs: ${SHA_FLAG} # Commit and push updates.xml - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" git add updates.xml git diff --cached --quiet || { git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]" @@ -211,9 +212,9 @@ jobs: if: github.ref_name != 'main' && steps.platform.outputs.platform == 'joomla' run: | API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - GA_TOKEN="${{ secrets.GA_TOKEN }}" + GITEA_TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" - FILE_SHA=$(curl -sf -H "Authorization: token ${GA_TOKEN}" \ + FILE_SHA=$(curl -sf -H "Authorization: token ${GITEA_TOKEN}" \ "${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true) if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then @@ -231,7 +232,7 @@ jobs: '${API_BASE}/contents/updates.xml', data=payload, method='PUT', headers={ - 'Authorization': 'token ${GA_TOKEN}', + 'Authorization': 'token ${GITEA_TOKEN}', 'Content-Type': 'application/json' }) try: @@ -257,7 +258,7 @@ jobs: ACTOR="${{ github.actor }}" API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \ "${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \ python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read") case "$PERMISSION" in From a1d5953015527d044ef0e3b025bae8384a928174 Mon Sep 17 00:00:00 2001 From: Moko Consulting <hello@mokoconsulting.tech> Date: Thu, 28 May 2026 14:25:41 -0500 Subject: [PATCH 051/132] chore(workflows): sync all universal workflows from moko-platform [skip bump] --- .mokogitea/workflows/gitleaks.yml | 4 +- .mokogitea/workflows/notify.yml | 5 +- .mokogitea/workflows/repo-health.yml | 171 ++++++++++++------------ .mokogitea/workflows/security-audit.yml | 20 ++- 4 files changed, 109 insertions(+), 91 deletions(-) diff --git a/.mokogitea/workflows/gitleaks.yml b/.mokogitea/workflows/gitleaks.yml index 0c07612..e0fdd1d 100644 --- a/.mokogitea/workflows/gitleaks.yml +++ b/.mokogitea/workflows/gitleaks.yml @@ -4,8 +4,8 @@ # # FILE INFORMATION # DEFGROUP: Gitea.Workflow -# INGROUP: MokoStandards.Security -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API +# INGROUP: moko-platform.Security +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform # PATH: /templates/workflows/gitleaks.yml.template # VERSION: 01.00.00 # BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens diff --git a/.mokogitea/workflows/notify.yml b/.mokogitea/workflows/notify.yml index 463a900..cde4541 100644 --- a/.mokogitea/workflows/notify.yml +++ b/.mokogitea/workflows/notify.yml @@ -4,8 +4,8 @@ # # FILE INFORMATION # DEFGROUP: Gitea.Workflow -# INGROUP: MokoStandards.Notifications -# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards +# INGROUP: moko-platform.Notifications +# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform # PATH: /.gitea/workflows/notify.yml # VERSION: 01.00.00 # BRIEF: Push notifications via ntfy on release success or workflow failure @@ -18,7 +18,6 @@ on: - "Joomla Build & Release" - "Joomla Extension CI" - "Deploy" - - "Cascade Main → Dev" types: - completed diff --git a/.mokogitea/workflows/repo-health.yml b/.mokogitea/workflows/repo-health.yml index e5e1c73..be52e37 100644 --- a/.mokogitea/workflows/repo-health.yml +++ b/.mokogitea/workflows/repo-health.yml @@ -7,18 +7,14 @@ # # FILE INFORMATION # DEFGROUP: Gitea.Workflow -# INGROUP: MokoStandards.Validation -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API +# INGROUP: moko-platform.Validation +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform # PATH: /templates/workflows/joomla/repo_health.yml.template # VERSION: 04.06.00 # BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts. # ============================================================================ -name: "Joomla: Repo Health" - -concurrency: - group: repo-health-${{ github.repository }}-${{ github.ref }} - cancel-in-progress: true +name: "Generic: Repo Health" defaults: run: @@ -53,7 +49,7 @@ env: SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate # Repo health policy - REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.gitea/workflows/ + REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.mokogitea/workflows/ REPO_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore,README.md,docs/ REPO_DISALLOWED_DIRS: REPO_DISALLOWED_FILES: TODO.md,todo.md @@ -64,7 +60,7 @@ env: # File / directory variables DOCS_INDEX: docs/docs-index.md SCRIPT_DIR: scripts - WORKFLOWS_DIR: .gitea/workflows + WORKFLOWS_DIR: .mokogitea/workflows SHELLCHECK_PATTERN: '*.sh' SPDX_FILE_GLOBS: '*.sh,*.php,*.js,*.ts,*.css,*.xml,*.yml,*.yaml' FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true @@ -85,7 +81,7 @@ jobs: - name: Check actor permission (admin only) id: perm env: - TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }} + TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }} REPO: ${{ github.repository }} ACTOR: ${{ github.actor }} run: | @@ -288,7 +284,7 @@ jobs: exit 0 fi - IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}" + if [ -n "${SCRIPTS_REQUIRED_DIRS:-}" ]; then IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}"; else required_dirs=(); fi IFS=',' read -r -a allowed_dirs <<< "${SCRIPTS_ALLOWED_DIRS}" missing_dirs=() @@ -392,23 +388,27 @@ jobs: exit 0 fi - # Source directory: src/ or htdocs/ (either is valid) + IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}" + IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}" + if [ -n "${REPO_DISALLOWED_DIRS:-}" ]; then IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}"; else disallowed_dirs=(); fi + IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES:-}" + + missing_required=() + missing_optional=() + + # Source directory: src/ or htdocs/ (either is valid for extension repos) + SOURCE_DIR="" if [ -d "src" ]; then SOURCE_DIR="src" elif [ -d "htdocs" ]; then SOURCE_DIR="htdocs" + elif [ -d "deploy" ] || [ -d "cli" ] || [ -d "monitoring" ]; then + # Platform/tooling repos don't need src/ + SOURCE_DIR="" else missing_required+=("src/ or htdocs/ (source directory required)") fi - IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}" - IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}" - IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}" - IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES}" - - missing_required=() - missing_optional=() - for item in "${required_artifacts[@]}"; do if printf '%s' "${item}" | grep -q '/$'; then d="${item%/}" @@ -450,12 +450,8 @@ jobs: fi done < <(git branch -r --list 'origin/dev*' | sed 's/^ *//') - if [ "${#dev_paths[@]}" -eq 0 ]; then - missing_required+=("dev/* branch (e.g. dev/01.00.00)") - fi - - if [ "${#dev_branches[@]}" -gt 0 ]; then - missing_required+=("invalid branch dev (must be dev/<version>)") + if [ "${#dev_paths[@]}" -eq 0 ] && [ "${#dev_branches[@]}" -eq 0 ]; then + missing_required+=("dev or dev/* branch") fi content_warnings=() @@ -481,26 +477,7 @@ jobs: export MISSING_OPTIONAL="$(printf '%s\n' "${missing_optional[@]:-}")" export CONTENT_WARNINGS="$(printf '%s\n' "${content_warnings[@]:-}")" - report_json="$(python3 - <<'PY' - import json - import os - - profile = os.environ.get('PROFILE_RAW') or 'all' - - missing_required = os.environ.get('MISSING_REQUIRED', '').splitlines() if os.environ.get('MISSING_REQUIRED') else [] - missing_optional = os.environ.get('MISSING_OPTIONAL', '').splitlines() if os.environ.get('MISSING_OPTIONAL') else [] - content_warnings = os.environ.get('CONTENT_WARNINGS', '').splitlines() if os.environ.get('CONTENT_WARNINGS') else [] - - out = { - 'profile': profile, - 'missing_required': [x for x in missing_required if x], - 'missing_optional': [x for x in missing_optional if x], - 'content_warnings': [x for x in content_warnings if x], - } - - print(json.dumps(out, indent=2)) - PY - )" + report_json=$(printf '{"profile":"%s","missing_required":%d,"missing_optional":%d,"content_warnings":%d}' "$profile" "${#missing_required[@]}" "${#missing_optional[@]}" "${#content_warnings[@]}") { printf '%s\n' '### Repository health' @@ -578,12 +555,14 @@ jobs: joomla_findings+=("updates.xml missing in root (required for Joomla update server)") fi - INDEX_DIRS=("${SOURCE_DIR}" "${SOURCE_DIR}/admin" "${SOURCE_DIR}/site") - for dir in "${INDEX_DIRS[@]}"; do - if [ -d "${dir}" ] && [ ! -f "${dir}/index.html" ]; then - joomla_findings+=("${dir}/index.html missing (directory listing protection)") - fi - done + if [ -n "${SOURCE_DIR}" ]; then + INDEX_DIRS=("${SOURCE_DIR}" "${SOURCE_DIR}/admin" "${SOURCE_DIR}/site") + for dir in "${INDEX_DIRS[@]}"; do + if [ -d "${dir}" ] && [ ! -f "${dir}/index.html" ]; then + joomla_findings+=("${dir}/index.html missing (directory listing protection)") + fi + done + fi if [ "${#joomla_findings[@]}" -gt 0 ]; then { @@ -629,43 +608,29 @@ jobs: fi if [ -f "${DOCS_INDEX}" ]; then - missing_links="$(python3 - <<'PY' - import os - import re - - idx = os.environ.get('DOCS_INDEX', 'docs/docs-index.md') - base = os.getcwd() - - bad = [] - pat = re.compile(r'\[[^\]]+\]\(([^)]+)\)') - - with open(idx, 'r', encoding='utf-8') as f: - for line in f: - for m in pat.findall(line): - link = m.strip() - if link.startswith('http://') or link.startswith('https://') or link.startswith('#') or link.startswith('mailto:'): - continue - if link.startswith('/'): - rel = link.lstrip('/') - else: - rel = os.path.normpath(os.path.join(os.path.dirname(idx), link)) - rel = rel.split('#', 1)[0] - rel = rel.split('?', 1)[0] - if not rel: - continue - p = os.path.join(base, rel) - if not os.path.exists(p): - bad.append(rel) - - print('\n'.join(sorted(set(bad)))) - PY - )" + missing_links="" + while IFS= read -r docline; do + for link in $(echo "$docline" | grep -oE '\]\([^)]+\)' | sed 's/\](//' | sed 's/)$//' || true); do + case "$link" in http://*|https://*|"#"*|mailto:*) continue ;; esac + linkpath="${link%%#*}" + linkpath="${linkpath%%\?*}" + [ -z "$linkpath" ] && continue + if [ "${linkpath:0:1}" = "/" ]; then + testpath="${linkpath#/}" + else + testpath="$(dirname "${DOCS_INDEX}")/${linkpath}" + fi + [ ! -e "$testpath" ] && missing_links="${missing_links}${testpath} " + done + done < "${DOCS_INDEX}" if [ -n "${missing_links}" ]; then extended_findings+=("docs/docs-index.md contains broken relative links") { printf '%s\n' '### Docs index link integrity' printf '%s\n' 'Broken relative links:' - while IFS= read -r l; do [ -n "${l}" ] && printf '%s\n' "- ${l}"; done <<< "${missing_links}" + for bl in ${missing_links}; do + printf '%s\n' "- ${bl}" + done printf '\n' } >> "${GITHUB_STEP_SUMMARY}" fi @@ -764,3 +729,41 @@ jobs: fi printf '%s\n' 'Repository health guardrails passed.' >> "${GITHUB_STEP_SUMMARY}" + + + site-health: + name: Site Health + runs-on: ubuntu-latest + if: github.event_name == 'workflow_dispatch' + steps: + - uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + + - name: Uptime check + if: env.URLS != '' + run: | + echo "$URLS" > /tmp/urls.txt + php monitoring/uptime-probe.php --urls /tmp/urls.txt --timeout 15 || echo "::warning::Some sites are down" + rm -f /tmp/urls.txt + env: + URLS: ${{ vars.MONITORED_URLS }} + + - name: SSL certificate check + if: env.DOMAINS != '' + run: | + echo "$DOMAINS" > /tmp/domains.txt + php monitoring/ssl-check.php --domains /tmp/domains.txt --warn-days 30 || echo "::warning::SSL certificates expiring soon" + rm -f /tmp/domains.txt + env: + DOMAINS: ${{ vars.MONITORED_DOMAINS }} + + - name: Summary + if: always() + run: | + echo "### Site Health" >> $GITHUB_STEP_SUMMARY + echo "Uptime and SSL checks completed." >> $GITHUB_STEP_SUMMARY + diff --git a/.mokogitea/workflows/security-audit.yml b/.mokogitea/workflows/security-audit.yml index 789325a..714d407 100644 --- a/.mokogitea/workflows/security-audit.yml +++ b/.mokogitea/workflows/security-audit.yml @@ -4,8 +4,8 @@ # # FILE INFORMATION # DEFGROUP: Gitea.Workflow -# INGROUP: MokoStandards.Security -# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards +# INGROUP: moko-platform.Security +# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform # PATH: /.gitea/workflows/security-audit.yml # VERSION: 01.00.00 # BRIEF: Dependency vulnerability scanning for composer and npm packages @@ -80,3 +80,19 @@ 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 }} + From 90bcabd2fa4e16b6fe79adef4ea3e15e3c5ed662 Mon Sep 17 00:00:00 2001 From: Moko Consulting <hello@mokoconsulting.tech> Date: Thu, 28 May 2026 14:37:54 -0500 Subject: [PATCH 052/132] =?UTF-8?q?fix(workflows):=20GITHUB=5FTOKEN?= =?UTF-8?q?=E2=86=92GH=5FMIRROR=5FTOKEN=20(reserved=20name)=20[skip=20bump?= =?UTF-8?q?]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .mokogitea/workflows/auto-bump.yml | 170 ++-- .mokogitea/workflows/auto-release.yml | 1048 ++++++++++++------------ .mokogitea/workflows/pre-release.yml | 462 +++++------ .mokogitea/workflows/update-server.yml | 622 +++++++------- 4 files changed, 1151 insertions(+), 1151 deletions(-) diff --git a/.mokogitea/workflows/auto-bump.yml b/.mokogitea/workflows/auto-bump.yml index 572facd..dc76039 100644 --- a/.mokogitea/workflows/auto-bump.yml +++ b/.mokogitea/workflows/auto-bump.yml @@ -1,85 +1,85 @@ -# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: moko-platform.Release -# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform -# PATH: /.mokogitea/workflows/auto-bump.yml -# VERSION: 09.02.00 -# BRIEF: Auto patch-bump version on every push to dev (skips merge commits) - -name: "Universal: Auto Version Bump" - -on: - push: - branches: - - dev - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - -permissions: - contents: write - -jobs: - bump: - name: Version Bump - runs-on: release - if: >- - !contains(github.event.head_commit.message, '[skip ci]') && - !contains(github.event.head_commit.message, '[skip bump]') && - !startsWith(github.event.head_commit.message, 'Merge pull request') - - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - token: ${{ secrets.MOKOGITEA_TOKEN }} - fetch-depth: 1 - - - name: Setup moko-platform tools - run: | - if ! command -v composer &> /dev/null; then - sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 - fi - if [ -d "/opt/moko-platform/cli" ]; then - echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV" - else - git clone --depth 1 --branch main --quiet \ - "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \ - /tmp/moko-platform-api - cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet - echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV" - fi - - - name: Bump version - run: | - BUMP=$(php ${MOKO_CLI}/version_bump.php --path . 2>&1) || true - echo "$BUMP" - - VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null) || true - [ -z "$VERSION" ] && { echo "No version found — skipping"; exit 0; } - - # Propagate to platform manifests with -dev suffix - php ${MOKO_CLI}/version_set_platform.php \ - --path . --version "$VERSION" --branch dev --stability dev 2>/dev/null || true - php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true - VERSION="${VERSION}-dev" - - # Commit if anything changed - if git diff --quiet && git diff --cached --quiet; then - echo "No version changes to commit" - exit 0 - fi - - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" - git add -A - git commit -m "chore(version): auto-bump patch ${VERSION} [skip ci]" \ - --author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" - git push origin dev - echo "Bumped to ${VERSION}" >> $GITHUB_STEP_SUMMARY +# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: moko-platform.Release +# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform +# PATH: /.mokogitea/workflows/auto-bump.yml +# VERSION: 09.02.00 +# BRIEF: Auto patch-bump version on every push to dev (skips merge commits) + +name: "Universal: Auto Version Bump" + +on: + push: + branches: + - dev + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + +permissions: + contents: write + +jobs: + bump: + name: Version Bump + runs-on: release + if: >- + !contains(github.event.head_commit.message, '[skip ci]') && + !contains(github.event.head_commit.message, '[skip bump]') && + !startsWith(github.event.head_commit.message, 'Merge pull request') + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.MOKOGITEA_TOKEN }} + fetch-depth: 1 + + - name: Setup moko-platform tools + run: | + if ! command -v composer &> /dev/null; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + if [ -d "/opt/moko-platform/cli" ]; then + echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV" + else + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \ + /tmp/moko-platform-api + cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet + echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV" + fi + + - name: Bump version + run: | + BUMP=$(php ${MOKO_CLI}/version_bump.php --path . 2>&1) || true + echo "$BUMP" + + VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null) || true + [ -z "$VERSION" ] && { echo "No version found — skipping"; exit 0; } + + # Propagate to platform manifests with -dev suffix + php ${MOKO_CLI}/version_set_platform.php \ + --path . --version "$VERSION" --branch dev --stability dev 2>/dev/null || true + php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true + VERSION="${VERSION}-dev" + + # Commit if anything changed + if git diff --quiet && git diff --cached --quiet; then + echo "No version changes to commit" + exit 0 + fi + + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + git add -A + git commit -m "chore(version): auto-bump patch ${VERSION} [skip ci]" \ + --author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" + git push origin dev + echo "Bumped to ${VERSION}" >> $GITHUB_STEP_SUMMARY diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml index 8be9b71..8d0ac29 100644 --- a/.mokogitea/workflows/auto-release.yml +++ b/.mokogitea/workflows/auto-release.yml @@ -1,524 +1,524 @@ -# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: moko-platform.Release -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform -# PATH: /templates/workflows/universal/auto-release.yml.template -# VERSION: 05.00.00 -# BRIEF: Universal build & release � detects platform from manifest.xml -# -# +========================================================================+ -# | UNIVERSAL BUILD & RELEASE PIPELINE | -# +========================================================================+ -# | | -# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. | -# | | -# | Platform-specific: | -# | joomla: XML manifest, updates.xml, type-prefixed packages | -# | dolibarr: mod*.class.php, update.txt, dev version reset | -# | generic: README-only, no update stream | -# | | -# +========================================================================+ - -name: "Universal: Build & Release" - -on: - pull_request: - types: [opened, closed] - branches: - - main - workflow_dispatch: - inputs: - action: - description: 'Action to perform' - required: false - type: choice - default: release - options: - - release - - promote-rc - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} - GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} - -permissions: - contents: write - -jobs: - # ── Draft PR → Promote highest pre-release to RC ───────────────────────────── - promote-rc: - name: Promote Pre-Release to RC - runs-on: release - if: >- - (github.event.action == 'opened' && github.event.pull_request.draft == true) || - (github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc') - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - token: ${{ secrets.MOKOGITEA_TOKEN }} - fetch-depth: 1 - - - name: Setup moko-platform tools - env: - MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting - run: | - if ! command -v composer &> /dev/null; then - sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 - fi - git clone --depth 1 --branch main --quiet \ - "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ - /tmp/moko-platform-api - cd /tmp/moko-platform-api - composer install --no-dev --no-interaction --quiet - - - name: Promote to release-candidate - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php /tmp/moko-platform-api/cli/release_promote.php \ - --from auto --to release-candidate \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" \ - --api-base "${API_BASE}" \ - --branch "${{ github.event.pull_request.head.ref || 'dev' }}" - - - name: Cascade lesser channels - continue-on-error: true - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php /tmp/moko-platform-api/cli/release_cascade.php \ - --stability release-candidate \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" \ - --api-base "${API_BASE}" - - - name: Summary - if: always() - run: | - echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY - echo "Draft PR opened — promoted highest pre-release to RC" >> $GITHUB_STEP_SUMMARY - - # ── Merged PR → Build & Release (or promote RC to stable) ──────────────────── - release: - name: Build & Release Pipeline - runs-on: release - if: >- - github.event.pull_request.merged == true || - (github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc') - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - token: ${{ secrets.MOKOGITEA_TOKEN }} - fetch-depth: 0 - - - name: Configure git for bot pushes - run: | - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" - - - name: Setup moko-platform tools - env: - MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GITHUB_TOKEN }}"}}' - run: | - # Ensure PHP + Composer are available - if ! command -v composer &> /dev/null; then - sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 - fi - git clone --depth 1 --branch main --quiet \ - "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ - /tmp/moko-platform-api - cd /tmp/moko-platform-api - composer install --no-dev --no-interaction --quiet - - - # -- PLATFORM DETECTION --------------------------------------------------- - - name: Detect platform - id: platform - run: | - php /tmp/moko-platform-api/cli/manifest_read.php --path . --github-output - MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true) - MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1 || true) - echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT" - echo "mod_file=${MOD_FILE}" >> "$GITHUB_OUTPUT" - - - name: "Step 1: Read version" - id: version - run: | - VERSION=$(php /tmp/moko-platform-api/cli/version_read.php --path .) - if [ -z "$VERSION" ]; then - echo "::error::No VERSION in README.md" - echo "skip=true" >> "$GITHUB_OUTPUT" - exit 0 - fi - # Strip any pre-release suffix merged from dev (e.g. 01.02.20-dev → 01.02.20) - VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//') - MAJOR=$(echo "$VERSION" | cut -d. -f1) - echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - echo "release_tag=stable" >> "$GITHUB_OUTPUT" - echo "skip=false" >> "$GITHUB_OUTPUT" - echo "branch=main" >> "$GITHUB_OUTPUT" - - # -- CHECK FOR RC PROMOTION ------------------------------------------------ - - name: "Check for RC release" - id: rc - if: steps.version.outputs.skip != 'true' - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - RC_JSON=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \ - "${API_BASE}/releases/tags/release-candidate" 2>/dev/null || echo "{}") - RC_ID=$(echo "$RC_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('id',''))" 2>/dev/null || true) - - if [ -n "$RC_ID" ] && [ "$RC_ID" != "None" ] && [ "$RC_ID" != "" ]; then - echo "promote=true" >> "$GITHUB_OUTPUT" - echo "release_id=${RC_ID}" >> "$GITHUB_OUTPUT" - echo "::notice::RC release found (id: ${RC_ID}) — will promote to stable" - else - echo "promote=false" >> "$GITHUB_OUTPUT" - echo "::notice::No RC release — full build pipeline" - fi - - - name: "Step 1b: Minor bump version" - id: bump - if: >- - steps.version.outputs.skip != 'true' && - steps.rc.outputs.promote != 'true' - run: | - MOKO_API="/tmp/moko-platform-api/cli" - php ${MOKO_API}/version_bump.php --path . --minor 2>&1 || true - VERSION=$(php ${MOKO_API}/version_read.php --path .) - echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - echo "Bumped to: ${VERSION}" - - - name: Check if already released - if: steps.version.outputs.skip != 'true' - id: check - run: | - TAG="${{ steps.version.outputs.release_tag }}" - BRANCH="${{ steps.version.outputs.branch }}" - - TAG_EXISTS=false - BRANCH_EXISTS=false - - git rev-parse "$TAG" >/dev/null 2>&1 && TAG_EXISTS=true - git ls-remote --heads origin "$BRANCH" 2>/dev/null | grep -q "$BRANCH" && BRANCH_EXISTS=true - - echo "tag_exists=$TAG_EXISTS" >> "$GITHUB_OUTPUT" - echo "branch_exists=$BRANCH_EXISTS" >> "$GITHUB_OUTPUT" - - # Tag and branch may persist across patch releases — never skip - echo "already_released=false" >> "$GITHUB_OUTPUT" - - # -- SANITY CHECKS ------------------------------------------------------- - - name: "Sanity: Pre-release validation" - if: >- - steps.version.outputs.skip != 'true' && - steps.check.outputs.already_released != 'true' - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - php /tmp/moko-platform-api/cli/release_validate.php \ - --path . --version "$VERSION" --output-summary --github-output || true - - # -- STEP 2: Create or update version/XX.YY archive branch --------------- - # Always runs — every version change on main archives to version/XX.YY - - name: "Step 2: Version archive branch" - if: steps.check.outputs.already_released != 'true' - run: | - BRANCH="${{ steps.version.outputs.branch }}" - IS_MINOR="${{ steps.version.outputs.is_minor }}" - PATCH="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - PATCH_NUM=$(echo "$PATCH" | awk -F. '{print $3}') - - # Check if branch exists - if git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then - git push origin HEAD:"$BRANCH" --force - echo "Updated archive branch: ${BRANCH} (patch ${PATCH_NUM})" >> $GITHUB_STEP_SUMMARY - else - git checkout -b "$BRANCH" 2>/dev/null || git checkout "$BRANCH" - git push origin "$BRANCH" --force - echo "Created archive branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY - fi - - # -- STEP 3: Set platform version ---------------------------------------- - - name: "Step 3: Set platform version" - if: >- - steps.version.outputs.skip != 'true' && - steps.check.outputs.already_released != 'true' - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - php /tmp/moko-platform-api/cli/version_set_platform.php \ - --path . --version "$VERSION" --branch main - - # -- STEP 4: Update version badges ---------------------------------------- - - name: "Step 4: Update version badges" - if: steps.version.outputs.skip != 'true' - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - php /tmp/moko-platform-api/cli/badge_update.php --path . --version "${VERSION}" 2>/dev/null || true - php /tmp/moko-platform-api/cli/version_check.php --path . --fix 2>/dev/null || true - - # Step 5 (updates.xml) moved after Step 8 to include SHA-256 checksum - - - name: "Step 4b: Promote and prune CHANGELOG" - if: >- - steps.version.outputs.skip != 'true' && - steps.check.outputs.already_released != 'true' - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - MOKO_API="/tmp/moko-platform-api/cli" - if [ -f "CHANGELOG.md" ]; then - php ${MOKO_API}/changelog_promote.php --path . --version "$VERSION" 2>&1 || true - php ${MOKO_API}/changelog_prune.php --path . --keep 5 2>&1 || true - fi - - - name: Commit release changes - if: >- - steps.version.outputs.skip != 'true' && - steps.check.outputs.already_released != 'true' - run: | - if git diff --quiet && git diff --cached --quiet; then - echo "No changes to commit" - exit 0 - fi - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - git add -A - git commit -m "chore(release): build ${VERSION} [skip ci]" \ - --author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" - git push -u origin HEAD - - # -- STEP 6: Create tag --------------------------------------------------- - - name: "Step 6: Create git tag" - if: >- - steps.version.outputs.skip != 'true' - run: | - RELEASE_TAG="${{ steps.version.outputs.release_tag }}" - # Only create the major release tag if it doesn't exist yet - if ! git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then - git tag "$RELEASE_TAG" - git push origin "$RELEASE_TAG" - echo "Tag created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY - else - echo "Tag ${RELEASE_TAG} already exists" >> $GITHUB_STEP_SUMMARY - fi - echo "Tag: ${TAG}" >> $GITHUB_STEP_SUMMARY - - # -- STEP 7a: Promote RC to stable (skip build) ---------------------------- - - name: "Step 7a: Promote RC to stable" - if: >- - steps.version.outputs.skip != 'true' && - steps.rc.outputs.promote == 'true' - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php /tmp/moko-platform-api/cli/release_promote.php \ - --from release-candidate --to stable \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" \ - --api-base "${API_BASE}" \ - --path . --branch main - echo "Promoted RC → stable (${VERSION})" >> $GITHUB_STEP_SUMMARY - - # -- STEP 7b: Create or update Gitea Release (full build path) ------------- - - name: "Step 7b: Gitea Release" - if: >- - steps.version.outputs.skip != 'true' && - steps.rc.outputs.promote != 'true' - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - RELEASE_TAG="${{ steps.version.outputs.release_tag }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php /tmp/moko-platform-api/cli/release_create.php \ - --path . --version "$VERSION" --tag "$RELEASE_TAG" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ - --repo "${GITEA_REPO}" --branch main - echo "Release created: ${VERSION}" >> $GITHUB_STEP_SUMMARY - - # -- STEP 8: Build packages and upload to release ---------------------------- - - name: "Step 8: Build package and upload" - id: package - if: >- - steps.version.outputs.skip != 'true' && - steps.rc.outputs.promote != 'true' - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - RELEASE_TAG="${{ steps.version.outputs.release_tag }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php /tmp/moko-platform-api/cli/release_package.php \ - --path . --version "$VERSION" --tag "$RELEASE_TAG" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ - --repo "${GITEA_REPO}" --output /tmp || true - - # -- STEP 5: Write update stream (after build so SHA-256 is available) ----- - - name: "Step 5: Write update stream" - if: steps.version.outputs.skip != 'true' - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - SHA256="${{ steps.package.outputs.sha256_zip }}" - - # Fetch latest updates.xml from main so preserve logic has all channels - GITEA_TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" - API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" - curl -sf -H "Authorization: token ${GITEA_TOKEN}" \ - "${API}/contents/updates.xml?ref=main" 2>/dev/null | \ - python3 -c "import sys,json,base64; print(base64.b64decode(json.load(sys.stdin)['content']).decode())" \ - > updates.xml 2>/dev/null || true - - SHA_FLAG="" - [ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}" - - php /tmp/moko-platform-api/cli/updates_xml_build.php \ - --path . --version "${VERSION}" --stability stable \ - --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \ - ${SHA_FLAG} --github-output - - # Commit updates.xml if changed - if ! git diff --quiet updates.xml 2>/dev/null; then - git add updates.xml - git commit -m "chore: update stable channel ${VERSION} [skip ci]" \ - --author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" - git push origin HEAD 2>&1 || true - fi - - # -- STEP 8b: Update release description with changelog ---------------------- - - name: "Step 8b: Update release body" - if: steps.version.outputs.skip != 'true' - continue-on-error: true - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - RELEASE_TAG="${{ steps.version.outputs.release_tag }}" - php /tmp/moko-platform-api/cli/release_body_update.php \ - --path . --version "${VERSION}" --tag "${RELEASE_TAG}" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" \ - --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \ - 2>&1 || true - echo "Release body updated" >> $GITHUB_STEP_SUMMARY - - # -- STEP 9: Mirror to GitHub (stable only) -------------------------------- - - name: "Step 9: Mirror release to GitHub" - if: >- - steps.version.outputs.skip != 'true' && - secrets.GITHUB_TOKEN != '' - continue-on-error: true - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - RELEASE_TAG="${{ steps.version.outputs.release_tag }}" - GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php /tmp/moko-platform-api/cli/release_mirror.php \ - --version "$VERSION" --tag "$RELEASE_TAG" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ - --gh-token "${{ secrets.GITHUB_TOKEN }}" --gh-repo "$GH_REPO" \ - --branch main 2>&1 || true - echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY - - # -- STEP 10: Sync main branch to GitHub mirror ---------------------------- - - name: "Step 10: Push main to GitHub mirror" - if: >- - steps.version.outputs.skip != 'true' && - secrets.GITHUB_TOKEN != '' - continue-on-error: true - run: | - GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" - GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1) - GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2) - git remote add github "https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \ - git remote set-url github "https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" - git fetch origin main --depth=1 - git push github origin/main:refs/heads/main --force 2>/dev/null \ - && echo "main branch pushed to GitHub mirror" \ - || echo "WARNING: GitHub mirror push failed" - - # -- Clean up lesser pre-releases (cascade) --------------------------------- - # stable → deletes all | rc → beta,alpha,dev | beta → alpha,dev | alpha → dev - - name: "Delete lesser pre-release channels" - continue-on-error: true - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php /tmp/moko-platform-api/cli/release_cascade.php \ - --stability stable \ - --version "${VERSION}" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" \ - --api-base "${API_BASE}" 2>/dev/null || true - - - name: "Step 11: Delete and recreate dev branch from main" - if: steps.version.outputs.skip != 'true' - continue-on-error: true - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" - - # Delete dev branch - curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ - "${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch" - - # Recreate dev from main (now includes version bump + changelog promotion) - curl -sf -X POST -H "Authorization: token ${TOKEN}" \ - -H "Content-Type: application/json" \ - "${API_BASE}/branches" \ - -d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main" - - echo "Dev branch reset from main (keeps dev ahead after release)" >> $GITHUB_STEP_SUMMARY - - - name: "Step 12: Create version branch from main" - if: steps.version.outputs.skip != 'true' - continue-on-error: true - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - BRANCH_NAME="version/${VERSION}" - MAIN_SHA=$(git rev-parse HEAD) - - # Delete old version branch if it exists (same version re-release) - curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}" - - # Create version/XX.YY.ZZ from main - curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed" - - echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY - - - - # -- Dolibarr post-release: Reset dev version ----------------------------- - - name: "Post-release: Reset dev version" - if: steps.version.outputs.skip != 'true' - continue-on-error: true - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php /tmp/moko-platform-api/cli/version_reset_dev.php \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \ - --branch dev --path . 2>&1 || true - - # -- Summary -------------------------------------------------------------- - - name: Pipeline Summary - if: always() - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - PLATFORM="${{ steps.platform.outputs.platform }}" - if [ "${{ steps.version.outputs.skip }}" = "true" ]; then - echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY - echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY - elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then - echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY - else - echo "" >> $GITHUB_STEP_SUMMARY - echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY - echo "|------|--------|" >> $GITHUB_STEP_SUMMARY - echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY - fi +# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: moko-platform.Release +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform +# PATH: /templates/workflows/universal/auto-release.yml.template +# VERSION: 05.00.00 +# BRIEF: Universal build & release � detects platform from manifest.xml +# +# +========================================================================+ +# | UNIVERSAL BUILD & RELEASE PIPELINE | +# +========================================================================+ +# | | +# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. | +# | | +# | Platform-specific: | +# | joomla: XML manifest, updates.xml, type-prefixed packages | +# | dolibarr: mod*.class.php, update.txt, dev version reset | +# | generic: README-only, no update stream | +# | | +# +========================================================================+ + +name: "Universal: Build & Release" + +on: + pull_request: + types: [opened, closed] + branches: + - main + workflow_dispatch: + inputs: + action: + description: 'Action to perform' + required: false + type: choice + default: release + options: + - release + - promote-rc + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + +permissions: + contents: write + +jobs: + # ── Draft PR → Promote highest pre-release to RC ───────────────────────────── + promote-rc: + name: Promote Pre-Release to RC + runs-on: release + if: >- + (github.event.action == 'opened' && github.event.pull_request.draft == true) || + (github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc') + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.MOKOGITEA_TOKEN }} + fetch-depth: 1 + + - name: Setup moko-platform tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + run: | + if ! command -v composer &> /dev/null; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ + /tmp/moko-platform-api + cd /tmp/moko-platform-api + composer install --no-dev --no-interaction --quiet + + - name: Promote to release-candidate + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php /tmp/moko-platform-api/cli/release_promote.php \ + --from auto --to release-candidate \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" \ + --api-base "${API_BASE}" \ + --branch "${{ github.event.pull_request.head.ref || 'dev' }}" + + - name: Cascade lesser channels + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php /tmp/moko-platform-api/cli/release_cascade.php \ + --stability release-candidate \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" \ + --api-base "${API_BASE}" + + - name: Summary + if: always() + run: | + echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY + echo "Draft PR opened — promoted highest pre-release to RC" >> $GITHUB_STEP_SUMMARY + + # ── Merged PR → Build & Release (or promote RC to stable) ──────────────────── + release: + name: Build & Release Pipeline + runs-on: release + if: >- + github.event.pull_request.merged == true || + (github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc') + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.MOKOGITEA_TOKEN }} + fetch-depth: 0 + + - name: Configure git for bot pushes + run: | + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + + - name: Setup moko-platform tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}' + run: | + # Ensure PHP + Composer are available + if ! command -v composer &> /dev/null; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ + /tmp/moko-platform-api + cd /tmp/moko-platform-api + composer install --no-dev --no-interaction --quiet + + + # -- PLATFORM DETECTION --------------------------------------------------- + - name: Detect platform + id: platform + run: | + php /tmp/moko-platform-api/cli/manifest_read.php --path . --github-output + MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true) + MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1 || true) + echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT" + echo "mod_file=${MOD_FILE}" >> "$GITHUB_OUTPUT" + + - name: "Step 1: Read version" + id: version + run: | + VERSION=$(php /tmp/moko-platform-api/cli/version_read.php --path .) + if [ -z "$VERSION" ]; then + echo "::error::No VERSION in README.md" + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + # Strip any pre-release suffix merged from dev (e.g. 01.02.20-dev → 01.02.20) + VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//') + MAJOR=$(echo "$VERSION" | cut -d. -f1) + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "release_tag=stable" >> "$GITHUB_OUTPUT" + echo "skip=false" >> "$GITHUB_OUTPUT" + echo "branch=main" >> "$GITHUB_OUTPUT" + + # -- CHECK FOR RC PROMOTION ------------------------------------------------ + - name: "Check for RC release" + id: rc + if: steps.version.outputs.skip != 'true' + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + RC_JSON=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \ + "${API_BASE}/releases/tags/release-candidate" 2>/dev/null || echo "{}") + RC_ID=$(echo "$RC_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('id',''))" 2>/dev/null || true) + + if [ -n "$RC_ID" ] && [ "$RC_ID" != "None" ] && [ "$RC_ID" != "" ]; then + echo "promote=true" >> "$GITHUB_OUTPUT" + echo "release_id=${RC_ID}" >> "$GITHUB_OUTPUT" + echo "::notice::RC release found (id: ${RC_ID}) — will promote to stable" + else + echo "promote=false" >> "$GITHUB_OUTPUT" + echo "::notice::No RC release — full build pipeline" + fi + + - name: "Step 1b: Minor bump version" + id: bump + if: >- + steps.version.outputs.skip != 'true' && + steps.rc.outputs.promote != 'true' + run: | + MOKO_API="/tmp/moko-platform-api/cli" + php ${MOKO_API}/version_bump.php --path . --minor 2>&1 || true + VERSION=$(php ${MOKO_API}/version_read.php --path .) + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "Bumped to: ${VERSION}" + + - name: Check if already released + if: steps.version.outputs.skip != 'true' + id: check + run: | + TAG="${{ steps.version.outputs.release_tag }}" + BRANCH="${{ steps.version.outputs.branch }}" + + TAG_EXISTS=false + BRANCH_EXISTS=false + + git rev-parse "$TAG" >/dev/null 2>&1 && TAG_EXISTS=true + git ls-remote --heads origin "$BRANCH" 2>/dev/null | grep -q "$BRANCH" && BRANCH_EXISTS=true + + echo "tag_exists=$TAG_EXISTS" >> "$GITHUB_OUTPUT" + echo "branch_exists=$BRANCH_EXISTS" >> "$GITHUB_OUTPUT" + + # Tag and branch may persist across patch releases — never skip + echo "already_released=false" >> "$GITHUB_OUTPUT" + + # -- SANITY CHECKS ------------------------------------------------------- + - name: "Sanity: Pre-release validation" + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.already_released != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + php /tmp/moko-platform-api/cli/release_validate.php \ + --path . --version "$VERSION" --output-summary --github-output || true + + # -- STEP 2: Create or update version/XX.YY archive branch --------------- + # Always runs — every version change on main archives to version/XX.YY + - name: "Step 2: Version archive branch" + if: steps.check.outputs.already_released != 'true' + run: | + BRANCH="${{ steps.version.outputs.branch }}" + IS_MINOR="${{ steps.version.outputs.is_minor }}" + PATCH="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + PATCH_NUM=$(echo "$PATCH" | awk -F. '{print $3}') + + # Check if branch exists + if git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then + git push origin HEAD:"$BRANCH" --force + echo "Updated archive branch: ${BRANCH} (patch ${PATCH_NUM})" >> $GITHUB_STEP_SUMMARY + else + git checkout -b "$BRANCH" 2>/dev/null || git checkout "$BRANCH" + git push origin "$BRANCH" --force + echo "Created archive branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY + fi + + # -- STEP 3: Set platform version ---------------------------------------- + - name: "Step 3: Set platform version" + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.already_released != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + php /tmp/moko-platform-api/cli/version_set_platform.php \ + --path . --version "$VERSION" --branch main + + # -- STEP 4: Update version badges ---------------------------------------- + - name: "Step 4: Update version badges" + if: steps.version.outputs.skip != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + php /tmp/moko-platform-api/cli/badge_update.php --path . --version "${VERSION}" 2>/dev/null || true + php /tmp/moko-platform-api/cli/version_check.php --path . --fix 2>/dev/null || true + + # Step 5 (updates.xml) moved after Step 8 to include SHA-256 checksum + + - name: "Step 4b: Promote and prune CHANGELOG" + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.already_released != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + MOKO_API="/tmp/moko-platform-api/cli" + if [ -f "CHANGELOG.md" ]; then + php ${MOKO_API}/changelog_promote.php --path . --version "$VERSION" 2>&1 || true + php ${MOKO_API}/changelog_prune.php --path . --keep 5 2>&1 || true + fi + + - name: Commit release changes + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.already_released != 'true' + run: | + if git diff --quiet && git diff --cached --quiet; then + echo "No changes to commit" + exit 0 + fi + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + git add -A + git commit -m "chore(release): build ${VERSION} [skip ci]" \ + --author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" + git push -u origin HEAD + + # -- STEP 6: Create tag --------------------------------------------------- + - name: "Step 6: Create git tag" + if: >- + steps.version.outputs.skip != 'true' + run: | + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + # Only create the major release tag if it doesn't exist yet + if ! git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then + git tag "$RELEASE_TAG" + git push origin "$RELEASE_TAG" + echo "Tag created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY + else + echo "Tag ${RELEASE_TAG} already exists" >> $GITHUB_STEP_SUMMARY + fi + echo "Tag: ${TAG}" >> $GITHUB_STEP_SUMMARY + + # -- STEP 7a: Promote RC to stable (skip build) ---------------------------- + - name: "Step 7a: Promote RC to stable" + if: >- + steps.version.outputs.skip != 'true' && + steps.rc.outputs.promote == 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php /tmp/moko-platform-api/cli/release_promote.php \ + --from release-candidate --to stable \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" \ + --api-base "${API_BASE}" \ + --path . --branch main + echo "Promoted RC → stable (${VERSION})" >> $GITHUB_STEP_SUMMARY + + # -- STEP 7b: Create or update Gitea Release (full build path) ------------- + - name: "Step 7b: Gitea Release" + if: >- + steps.version.outputs.skip != 'true' && + steps.rc.outputs.promote != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php /tmp/moko-platform-api/cli/release_create.php \ + --path . --version "$VERSION" --tag "$RELEASE_TAG" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ + --repo "${GITEA_REPO}" --branch main + echo "Release created: ${VERSION}" >> $GITHUB_STEP_SUMMARY + + # -- STEP 8: Build packages and upload to release ---------------------------- + - name: "Step 8: Build package and upload" + id: package + if: >- + steps.version.outputs.skip != 'true' && + steps.rc.outputs.promote != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php /tmp/moko-platform-api/cli/release_package.php \ + --path . --version "$VERSION" --tag "$RELEASE_TAG" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ + --repo "${GITEA_REPO}" --output /tmp || true + + # -- STEP 5: Write update stream (after build so SHA-256 is available) ----- + - name: "Step 5: Write update stream" + if: steps.version.outputs.skip != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + SHA256="${{ steps.package.outputs.sha256_zip }}" + + # Fetch latest updates.xml from main so preserve logic has all channels + GITEA_TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" + API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" + curl -sf -H "Authorization: token ${GITEA_TOKEN}" \ + "${API}/contents/updates.xml?ref=main" 2>/dev/null | \ + python3 -c "import sys,json,base64; print(base64.b64decode(json.load(sys.stdin)['content']).decode())" \ + > updates.xml 2>/dev/null || true + + SHA_FLAG="" + [ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}" + + php /tmp/moko-platform-api/cli/updates_xml_build.php \ + --path . --version "${VERSION}" --stability stable \ + --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \ + ${SHA_FLAG} --github-output + + # Commit updates.xml if changed + if ! git diff --quiet updates.xml 2>/dev/null; then + git add updates.xml + git commit -m "chore: update stable channel ${VERSION} [skip ci]" \ + --author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" + git push origin HEAD 2>&1 || true + fi + + # -- STEP 8b: Update release description with changelog ---------------------- + - name: "Step 8b: Update release body" + if: steps.version.outputs.skip != 'true' + continue-on-error: true + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + php /tmp/moko-platform-api/cli/release_body_update.php \ + --path . --version "${VERSION}" --tag "${RELEASE_TAG}" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" \ + --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \ + 2>&1 || true + echo "Release body updated" >> $GITHUB_STEP_SUMMARY + + # -- STEP 9: Mirror to GitHub (stable only) -------------------------------- + - name: "Step 9: Mirror release to GitHub" + if: >- + steps.version.outputs.skip != 'true' && + secrets.GH_MIRROR_TOKEN != '' + continue-on-error: true + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php /tmp/moko-platform-api/cli/release_mirror.php \ + --version "$VERSION" --tag "$RELEASE_TAG" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ + --gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \ + --branch main 2>&1 || true + echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY + + # -- STEP 10: Sync main branch to GitHub mirror ---------------------------- + - name: "Step 10: Push main to GitHub mirror" + if: >- + steps.version.outputs.skip != 'true' && + secrets.GH_MIRROR_TOKEN != '' + continue-on-error: true + run: | + GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" + GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1) + GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2) + git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \ + git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" + git fetch origin main --depth=1 + git push github origin/main:refs/heads/main --force 2>/dev/null \ + && echo "main branch pushed to GitHub mirror" \ + || echo "WARNING: GitHub mirror push failed" + + # -- Clean up lesser pre-releases (cascade) --------------------------------- + # stable → deletes all | rc → beta,alpha,dev | beta → alpha,dev | alpha → dev + - name: "Delete lesser pre-release channels" + continue-on-error: true + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php /tmp/moko-platform-api/cli/release_cascade.php \ + --stability stable \ + --version "${VERSION}" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" \ + --api-base "${API_BASE}" 2>/dev/null || true + + - name: "Step 11: Delete and recreate dev branch from main" + if: steps.version.outputs.skip != 'true' + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" + + # Delete dev branch + curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch" + + # Recreate dev from main (now includes version bump + changelog promotion) + curl -sf -X POST -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/json" \ + "${API_BASE}/branches" \ + -d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main" + + echo "Dev branch reset from main (keeps dev ahead after release)" >> $GITHUB_STEP_SUMMARY + + - name: "Step 12: Create version branch from main" + if: steps.version.outputs.skip != 'true' + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + BRANCH_NAME="version/${VERSION}" + MAIN_SHA=$(git rev-parse HEAD) + + # Delete old version branch if it exists (same version re-release) + curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}" + + # Create version/XX.YY.ZZ from main + curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed" + + echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY + + + + # -- Dolibarr post-release: Reset dev version ----------------------------- + - name: "Post-release: Reset dev version" + if: steps.version.outputs.skip != 'true' + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php /tmp/moko-platform-api/cli/version_reset_dev.php \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \ + --branch dev --path . 2>&1 || true + + # -- Summary -------------------------------------------------------------- + - name: Pipeline Summary + if: always() + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + PLATFORM="${{ steps.platform.outputs.platform }}" + if [ "${{ steps.version.outputs.skip }}" = "true" ]; then + echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY + echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY + elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then + echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY + echo "|------|--------|" >> $GITHUB_STEP_SUMMARY + echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY + fi diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml index 7920f53..0f0d000 100644 --- a/.mokogitea/workflows/pre-release.yml +++ b/.mokogitea/workflows/pre-release.yml @@ -1,231 +1,231 @@ -# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: moko-platform.Release -# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform -# PATH: /templates/workflows/universal/pre-release.yml.template -# VERSION: 05.01.00 -# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch - -name: "Universal: Pre-Release" - -on: - pull_request: - types: [closed] - branches: - - dev - workflow_dispatch: - inputs: - stability: - description: 'Pre-release channel' - required: true - type: choice - options: - - development - - alpha - - beta - - release-candidate - -permissions: - contents: write - -env: - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} - GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} - -jobs: - build: - name: "Build Pre-Release (${{ inputs.stability || 'development' }})" - runs-on: release - if: >- - github.event_name == 'workflow_dispatch' || - (github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev') - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - token: ${{ secrets.MOKOGITEA_TOKEN }} - - - name: Setup moko-platform tools - env: - MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting - run: | - if ! command -v composer &> /dev/null; then - sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 - fi - git clone --depth 1 --branch main --quiet \ - "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ - /tmp/moko-platform-api - cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet - echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV" - - - name: Detect platform - id: platform - run: | - php ${MOKO_CLI}/manifest_read.php --path . --github-output - - - name: Resolve metadata and bump version - id: meta - run: | - STABILITY="${{ inputs.stability || 'development' }}" - - case "$STABILITY" in - development) SUFFIX="-dev"; TAG="development" ;; - alpha) SUFFIX="-alpha"; TAG="alpha" ;; - beta) SUFFIX="-beta"; TAG="beta" ;; - release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;; - esac - - # Read current version (bump already handled by push workflow) - VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null) - [ -z "$VERSION" ] && VERSION="00.00.01" - - # Strip any existing suffix from version before applying stability - VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//') - - php ${MOKO_CLI}/version_set_platform.php \ - --path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true - - # Verify version consistency across all files - php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true - - # Update VERSION variable with suffix - if [ -n "$SUFFIX" ]; then - VERSION="${VERSION}${SUFFIX}" - fi - - # Commit version bump - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" - git add -A - git diff --cached --quiet || { - git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]" - git push origin HEAD 2>&1 - } - - # Auto-detect element via manifest_element.php - php ${MOKO_CLI}/manifest_element.php \ - --path . --version "$VERSION" --stability "$STABILITY" \ - --repo "${GITEA_REPO}" --github-output - - # Read back element outputs - EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) - ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) - [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') - [ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip" - - echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT" - echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT" - echo "tag=${TAG}" >> "$GITHUB_OUTPUT" - echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT" - echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT" - - echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ===" - - - name: Create release - id: release - run: | - TAG="${{ steps.meta.outputs.tag }}" - VERSION="${{ steps.meta.outputs.version }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php ${MOKO_CLI}/release_create.php \ - --path . --version "$VERSION" --tag "$TAG" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ - --repo "${GITEA_REPO}" --branch dev --prerelease - - - name: Build package and upload - id: package - run: | - VERSION="${{ steps.meta.outputs.version }}" - TAG="${{ steps.meta.outputs.tag }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php ${MOKO_CLI}/release_package.php \ - --path . --version "$VERSION" --tag "$TAG" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ - --repo "${GITEA_REPO}" --output /tmp || true - - - 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 - if ! git diff --quiet updates.xml 2>/dev/null; then - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - git add updates.xml - git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]" - git push origin HEAD 2>&1 || echo "WARNING: push failed" - fi - - - name: "Sync updates.xml to all branches" - if: steps.platform.outputs.platform == 'joomla' - run: | - CURRENT_BRANCH="${{ github.ref_name }}" - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - - for BRANCH in main dev; do - [ "$BRANCH" = "$CURRENT_BRANCH" ] && continue - echo "Syncing updates.xml -> ${BRANCH}" - git fetch origin "${BRANCH}" 2>/dev/null || continue - git checkout "origin/${BRANCH}" -- updates.xml 2>/dev/null || continue - git checkout "${CURRENT_BRANCH}" -- updates.xml - if ! git diff --quiet updates.xml 2>/dev/null; then - git add updates.xml - git commit -m "chore: sync updates.xml from ${CURRENT_BRANCH} [skip ci]" - git push origin HEAD:refs/heads/${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed" - fi - git checkout "${CURRENT_BRANCH}" 2>/dev/null - done - - - name: "Delete lesser pre-release channels (cascade)" - continue-on-error: true - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" - - php ${MOKO_CLI}/release_cascade.php \ - --stability "${{ steps.meta.outputs.stability }}" \ - --token "${TOKEN}" \ - --api-base "${API_BASE}" - - - name: Summary - if: always() - run: | - VERSION="${{ steps.meta.outputs.version }}" - STABILITY="${{ steps.meta.outputs.stability }}" - ZIP_NAME="${{ steps.meta.outputs.zip_name }}" - SHA256="${{ steps.package.outputs.sha256_zip }}" - echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY - echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY - echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY - echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY - echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY +# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: moko-platform.Release +# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform +# PATH: /templates/workflows/universal/pre-release.yml.template +# VERSION: 05.01.00 +# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch + +name: "Universal: Pre-Release" + +on: + pull_request: + types: [closed] + branches: + - dev + workflow_dispatch: + inputs: + stability: + description: 'Pre-release channel' + required: true + type: choice + options: + - development + - alpha + - beta + - release-candidate + +permissions: + contents: write + +env: + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + +jobs: + build: + name: "Build Pre-Release (${{ inputs.stability || 'development' }})" + runs-on: release + if: >- + github.event_name == 'workflow_dispatch' || + (github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev') + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.MOKOGITEA_TOKEN }} + + - name: Setup moko-platform tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + run: | + if ! command -v composer &> /dev/null; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ + /tmp/moko-platform-api + cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet + echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV" + + - name: Detect platform + id: platform + run: | + php ${MOKO_CLI}/manifest_read.php --path . --github-output + + - name: Resolve metadata and bump version + id: meta + run: | + STABILITY="${{ inputs.stability || 'development' }}" + + case "$STABILITY" in + development) SUFFIX="-dev"; TAG="development" ;; + alpha) SUFFIX="-alpha"; TAG="alpha" ;; + beta) SUFFIX="-beta"; TAG="beta" ;; + release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;; + esac + + # Read current version (bump already handled by push workflow) + VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null) + [ -z "$VERSION" ] && VERSION="00.00.01" + + # Strip any existing suffix from version before applying stability + VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//') + + php ${MOKO_CLI}/version_set_platform.php \ + --path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true + + # Verify version consistency across all files + php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true + + # Update VERSION variable with suffix + if [ -n "$SUFFIX" ]; then + VERSION="${VERSION}${SUFFIX}" + fi + + # Commit version bump + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + git add -A + git diff --cached --quiet || { + git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]" + git push origin HEAD 2>&1 + } + + # Auto-detect element via manifest_element.php + php ${MOKO_CLI}/manifest_element.php \ + --path . --version "$VERSION" --stability "$STABILITY" \ + --repo "${GITEA_REPO}" --github-output + + # Read back element outputs + EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) + ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) + [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') + [ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip" + + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT" + echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT" + echo "tag=${TAG}" >> "$GITHUB_OUTPUT" + echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT" + echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT" + + echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ===" + + - name: Create release + id: release + run: | + TAG="${{ steps.meta.outputs.tag }}" + VERSION="${{ steps.meta.outputs.version }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php ${MOKO_CLI}/release_create.php \ + --path . --version "$VERSION" --tag "$TAG" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ + --repo "${GITEA_REPO}" --branch dev --prerelease + + - name: Build package and upload + id: package + run: | + VERSION="${{ steps.meta.outputs.version }}" + TAG="${{ steps.meta.outputs.tag }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php ${MOKO_CLI}/release_package.php \ + --path . --version "$VERSION" --tag "$TAG" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ + --repo "${GITEA_REPO}" --output /tmp || true + + - 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 + if ! git diff --quiet updates.xml 2>/dev/null; then + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git add updates.xml + git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]" + git push origin HEAD 2>&1 || echo "WARNING: push failed" + fi + + - name: "Sync updates.xml to all branches" + if: steps.platform.outputs.platform == 'joomla' + run: | + CURRENT_BRANCH="${{ github.ref_name }}" + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + + for BRANCH in main dev; do + [ "$BRANCH" = "$CURRENT_BRANCH" ] && continue + echo "Syncing updates.xml -> ${BRANCH}" + git fetch origin "${BRANCH}" 2>/dev/null || continue + git checkout "origin/${BRANCH}" -- updates.xml 2>/dev/null || continue + git checkout "${CURRENT_BRANCH}" -- updates.xml + if ! git diff --quiet updates.xml 2>/dev/null; then + git add updates.xml + git commit -m "chore: sync updates.xml from ${CURRENT_BRANCH} [skip ci]" + git push origin HEAD:refs/heads/${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed" + fi + git checkout "${CURRENT_BRANCH}" 2>/dev/null + done + + - name: "Delete lesser pre-release channels (cascade)" + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" + + php ${MOKO_CLI}/release_cascade.php \ + --stability "${{ steps.meta.outputs.stability }}" \ + --token "${TOKEN}" \ + --api-base "${API_BASE}" + + - name: Summary + if: always() + run: | + VERSION="${{ steps.meta.outputs.version }}" + STABILITY="${{ steps.meta.outputs.stability }}" + ZIP_NAME="${{ steps.meta.outputs.zip_name }}" + SHA256="${{ steps.package.outputs.sha256_zip }}" + echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY + echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY + echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY diff --git a/.mokogitea/workflows/update-server.yml b/.mokogitea/workflows/update-server.yml index cd2eff0..061476f 100644 --- a/.mokogitea/workflows/update-server.yml +++ b/.mokogitea/workflows/update-server.yml @@ -1,311 +1,311 @@ -# 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 - if [ -d "/tmp/moko-platform" ]; then - echo "moko-platform already available — skipping clone" - else - 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 - fi - 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") - - # 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 +# 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 + if [ -d "/tmp/moko-platform" ]; then + echo "moko-platform already available — skipping clone" + else + 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 + fi + 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") + + # 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 From 5d34cbced4e486cfd4e6d5fef8cd20f472391620 Mon Sep 17 00:00:00 2001 From: Moko Consulting <hello@mokoconsulting.tech> Date: Thu, 28 May 2026 14:50:17 -0500 Subject: [PATCH 053/132] fix(workflows): rename remaining old secrets in repo-specific workflows [skip bump] --- .mokogitea/workflows/ci-joomla.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.mokogitea/workflows/ci-joomla.yml b/.mokogitea/workflows/ci-joomla.yml index 5c66f14..9c3d8e6 100644 --- a/.mokogitea/workflows/ci-joomla.yml +++ b/.mokogitea/workflows/ci-joomla.yml @@ -43,9 +43,9 @@ jobs: - name: Clone MokoStandards env: - GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }} - MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }} - MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }} + GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }} + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }} + MOKO_CLONE_HOST: ${{ secrets.MOKOGITEA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }} run: | git clone --depth 1 --branch main --quiet \ "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \ @@ -53,7 +53,7 @@ jobs: - name: Install dependencies env: - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}' + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}' run: | if [ -f "composer.json" ]; then composer install \ @@ -346,7 +346,7 @@ jobs: - name: Install dependencies env: - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}' + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}' run: | if [ -f "composer.json" ]; then composer install \ @@ -391,7 +391,7 @@ jobs: - name: Install dependencies env: - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}' + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}' run: | if [ -f "composer.json" ]; then composer install --no-interaction --prefer-dist --optimize-autoloader From 4fef3cd528a8b986a55bb98e50adee213bc80323 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Thu, 28 May 2026 20:03:23 +0000 Subject: [PATCH 054/132] chore: sync .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] --- .mokogitea/workflows/auto-release.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml index 8d0ac29..a05d0f4 100644 --- a/.mokogitea/workflows/auto-release.yml +++ b/.mokogitea/workflows/auto-release.yml @@ -74,6 +74,8 @@ jobs: if ! command -v composer &> /dev/null; then sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 fi + # Always fetch latest CLI tools — never use stale cache from previous runs + rm -rf /tmp/moko-platform-api git clone --depth 1 --branch main --quiet \ "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ /tmp/moko-platform-api @@ -135,6 +137,8 @@ jobs: if ! command -v composer &> /dev/null; then sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 fi + # Always fetch latest CLI tools — never use stale cache from previous runs + rm -rf /tmp/moko-platform-api git clone --depth 1 --branch main --quiet \ "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ /tmp/moko-platform-api From afb431a62c333172f86b861cd56fe586425dbfe5 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Thu, 28 May 2026 20:06:39 +0000 Subject: [PATCH 055/132] chore: sync .mokogitea/workflows/update-server.yml from moko-platform [skip ci] --- .mokogitea/workflows/update-server.yml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/.mokogitea/workflows/update-server.yml b/.mokogitea/workflows/update-server.yml index 061476f..0e0a8e5 100644 --- a/.mokogitea/workflows/update-server.yml +++ b/.mokogitea/workflows/update-server.yml @@ -85,13 +85,11 @@ jobs: 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 "/tmp/moko-platform" ]; then - echo "moko-platform already available — skipping clone" - else - 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 - 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 From 29917529cab7ac1ae238cdde149f6361f57b6e39 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Thu, 28 May 2026 20:09:45 +0000 Subject: [PATCH 056/132] chore: sync .mokogitea/workflows/pre-release.yml from moko-platform [skip ci] --- .mokogitea/workflows/pre-release.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml index 0f0d000..162b08f 100644 --- a/.mokogitea/workflows/pre-release.yml +++ b/.mokogitea/workflows/pre-release.yml @@ -60,6 +60,8 @@ jobs: if ! command -v composer &> /dev/null; then sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 fi + # Always fetch latest CLI tools — never use stale cache from previous runs + rm -rf /tmp/moko-platform-api git clone --depth 1 --branch main --quiet \ "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ /tmp/moko-platform-api From 9a88daec9da748fdb3eb24caea054c7b0c4842e2 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Thu, 28 May 2026 20:28:58 +0000 Subject: [PATCH 057/132] chore: sync .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] --- .mokogitea/workflows/auto-release.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml index a05d0f4..757bfb4 100644 --- a/.mokogitea/workflows/auto-release.yml +++ b/.mokogitea/workflows/auto-release.yml @@ -298,7 +298,8 @@ jobs: git add -A git commit -m "chore(release): build ${VERSION} [skip ci]" \ --author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" - git push -u origin HEAD + # Detached HEAD on PR merge — push explicitly to main + git push origin HEAD:refs/heads/main # -- STEP 6: Create tag --------------------------------------------------- - name: "Step 6: Create git tag" @@ -389,7 +390,7 @@ jobs: git add updates.xml git commit -m "chore: update stable channel ${VERSION} [skip ci]" \ --author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" - git push origin HEAD 2>&1 || true + git push origin HEAD:refs/heads/main 2>&1 || true fi # -- STEP 8b: Update release description with changelog ---------------------- From 7e4b3910045026599cebcbd9ae960b094a065d07 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Thu, 28 May 2026 20:46:53 +0000 Subject: [PATCH 058/132] chore: sync .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] --- .mokogitea/workflows/auto-release.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml index 757bfb4..72ce95a 100644 --- a/.mokogitea/workflows/auto-release.yml +++ b/.mokogitea/workflows/auto-release.yml @@ -201,6 +201,8 @@ jobs: MOKO_API="/tmp/moko-platform-api/cli" php ${MOKO_API}/version_bump.php --path . --minor 2>&1 || true VERSION=$(php ${MOKO_API}/version_read.php --path .) + # Strip any pre-release suffix — stable releases have no suffix + VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//') echo "version=${VERSION}" >> "$GITHUB_OUTPUT" echo "Bumped to: ${VERSION}" From efd86ee77e84c95ea8b2a183bc0c629268b51090 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Thu, 28 May 2026 20:51:44 +0000 Subject: [PATCH 059/132] chore: sync .mokogitea/workflows/update-server.yml from moko-platform [skip ci] --- .mokogitea/workflows/update-server.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.mokogitea/workflows/update-server.yml b/.mokogitea/workflows/update-server.yml index 0e0a8e5..339d3f5 100644 --- a/.mokogitea/workflows/update-server.yml +++ b/.mokogitea/workflows/update-server.yml @@ -114,6 +114,9 @@ jobs: 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 }}" From 56d7d8784b684edd6eda4af5ca18287dc17c2c51 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Fri, 29 May 2026 10:32:33 +0000 Subject: [PATCH 060/132] chore: sync .mokogitea/workflows/auto-bump.yml from moko-platform [skip ci] --- .mokogitea/workflows/auto-bump.yml | 152 +++++++++++++---------------- 1 file changed, 67 insertions(+), 85 deletions(-) diff --git a/.mokogitea/workflows/auto-bump.yml b/.mokogitea/workflows/auto-bump.yml index dc76039..a397a9e 100644 --- a/.mokogitea/workflows/auto-bump.yml +++ b/.mokogitea/workflows/auto-bump.yml @@ -1,85 +1,67 @@ -# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: moko-platform.Release -# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform -# PATH: /.mokogitea/workflows/auto-bump.yml -# VERSION: 09.02.00 -# BRIEF: Auto patch-bump version on every push to dev (skips merge commits) - -name: "Universal: Auto Version Bump" - -on: - push: - branches: - - dev - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - -permissions: - contents: write - -jobs: - bump: - name: Version Bump - runs-on: release - if: >- - !contains(github.event.head_commit.message, '[skip ci]') && - !contains(github.event.head_commit.message, '[skip bump]') && - !startsWith(github.event.head_commit.message, 'Merge pull request') - - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - token: ${{ secrets.MOKOGITEA_TOKEN }} - fetch-depth: 1 - - - name: Setup moko-platform tools - run: | - if ! command -v composer &> /dev/null; then - sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 - fi - if [ -d "/opt/moko-platform/cli" ]; then - echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV" - else - git clone --depth 1 --branch main --quiet \ - "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \ - /tmp/moko-platform-api - cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet - echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV" - fi - - - name: Bump version - run: | - BUMP=$(php ${MOKO_CLI}/version_bump.php --path . 2>&1) || true - echo "$BUMP" - - VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null) || true - [ -z "$VERSION" ] && { echo "No version found — skipping"; exit 0; } - - # Propagate to platform manifests with -dev suffix - php ${MOKO_CLI}/version_set_platform.php \ - --path . --version "$VERSION" --branch dev --stability dev 2>/dev/null || true - php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true - VERSION="${VERSION}-dev" - - # Commit if anything changed - if git diff --quiet && git diff --cached --quiet; then - echo "No version changes to commit" - exit 0 - fi - - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" - git add -A - git commit -m "chore(version): auto-bump patch ${VERSION} [skip ci]" \ - --author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" - git push origin dev - echo "Bumped to ${VERSION}" >> $GITHUB_STEP_SUMMARY +# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: moko-platform.Release +# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform +# PATH: /.mokogitea/workflows/auto-bump.yml +# VERSION: 09.02.00 +# BRIEF: Auto patch-bump version on every push to dev (skips merge commits) + +name: "Universal: Auto Version Bump" + +on: + push: + branches: + - dev + - alpha + - beta + - rc + - 'feature/**' + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + +permissions: + contents: write + +jobs: + bump: + name: Version Bump + runs-on: release + if: >- + !contains(github.event.head_commit.message, '[skip ci]') && + !contains(github.event.head_commit.message, '[skip bump]') && + !startsWith(github.event.head_commit.message, 'Merge pull request') + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.MOKOGITEA_TOKEN }} + fetch-depth: 1 + + - name: Setup moko-platform tools + run: | + if ! command -v composer &> /dev/null; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + if [ -d "/opt/moko-platform/cli" ]; then + echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV" + else + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \ + /tmp/moko-platform-api + cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet + echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV" + fi + + - name: Bump version + run: | + 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" From 182e8caa90e6a264036b8af934072926e8f59596 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sat, 30 May 2026 01:17:07 +0000 Subject: [PATCH 061/132] chore: sync .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] --- .mokogitea/workflows/auto-release.yml | 1110 +++++++++++++------------ 1 file changed, 579 insertions(+), 531 deletions(-) diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml index 72ce95a..04ec817 100644 --- a/.mokogitea/workflows/auto-release.yml +++ b/.mokogitea/workflows/auto-release.yml @@ -1,531 +1,579 @@ -# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: moko-platform.Release -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform -# PATH: /templates/workflows/universal/auto-release.yml.template -# VERSION: 05.00.00 -# BRIEF: Universal build & release � detects platform from manifest.xml -# -# +========================================================================+ -# | UNIVERSAL BUILD & RELEASE PIPELINE | -# +========================================================================+ -# | | -# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. | -# | | -# | Platform-specific: | -# | joomla: XML manifest, updates.xml, type-prefixed packages | -# | dolibarr: mod*.class.php, update.txt, dev version reset | -# | generic: README-only, no update stream | -# | | -# +========================================================================+ - -name: "Universal: Build & Release" - -on: - pull_request: - types: [opened, closed] - branches: - - main - workflow_dispatch: - inputs: - action: - description: 'Action to perform' - required: false - type: choice - default: release - options: - - release - - promote-rc - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} - GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} - -permissions: - contents: write - -jobs: - # ── Draft PR → Promote highest pre-release to RC ───────────────────────────── - promote-rc: - name: Promote Pre-Release to RC - runs-on: release - if: >- - (github.event.action == 'opened' && github.event.pull_request.draft == true) || - (github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc') - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - token: ${{ secrets.MOKOGITEA_TOKEN }} - fetch-depth: 1 - - - name: Setup moko-platform tools - env: - MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting - run: | - if ! command -v composer &> /dev/null; then - sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 - fi - # Always fetch latest CLI tools — never use stale cache from previous runs - rm -rf /tmp/moko-platform-api - git clone --depth 1 --branch main --quiet \ - "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ - /tmp/moko-platform-api - cd /tmp/moko-platform-api - composer install --no-dev --no-interaction --quiet - - - name: Promote to release-candidate - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php /tmp/moko-platform-api/cli/release_promote.php \ - --from auto --to release-candidate \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" \ - --api-base "${API_BASE}" \ - --branch "${{ github.event.pull_request.head.ref || 'dev' }}" - - - name: Cascade lesser channels - continue-on-error: true - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php /tmp/moko-platform-api/cli/release_cascade.php \ - --stability release-candidate \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" \ - --api-base "${API_BASE}" - - - name: Summary - if: always() - run: | - echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY - echo "Draft PR opened — promoted highest pre-release to RC" >> $GITHUB_STEP_SUMMARY - - # ── Merged PR → Build & Release (or promote RC to stable) ──────────────────── - release: - name: Build & Release Pipeline - runs-on: release - if: >- - github.event.pull_request.merged == true || - (github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc') - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - token: ${{ secrets.MOKOGITEA_TOKEN }} - fetch-depth: 0 - - - name: Configure git for bot pushes - run: | - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" - - - name: Setup moko-platform tools - env: - MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}' - run: | - # Ensure PHP + Composer are available - if ! command -v composer &> /dev/null; then - sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 - fi - # Always fetch latest CLI tools — never use stale cache from previous runs - rm -rf /tmp/moko-platform-api - git clone --depth 1 --branch main --quiet \ - "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ - /tmp/moko-platform-api - cd /tmp/moko-platform-api - composer install --no-dev --no-interaction --quiet - - - # -- PLATFORM DETECTION --------------------------------------------------- - - name: Detect platform - id: platform - run: | - php /tmp/moko-platform-api/cli/manifest_read.php --path . --github-output - MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true) - MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1 || true) - echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT" - echo "mod_file=${MOD_FILE}" >> "$GITHUB_OUTPUT" - - - name: "Step 1: Read version" - id: version - run: | - VERSION=$(php /tmp/moko-platform-api/cli/version_read.php --path .) - if [ -z "$VERSION" ]; then - echo "::error::No VERSION in README.md" - echo "skip=true" >> "$GITHUB_OUTPUT" - exit 0 - fi - # Strip any pre-release suffix merged from dev (e.g. 01.02.20-dev → 01.02.20) - VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//') - MAJOR=$(echo "$VERSION" | cut -d. -f1) - echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - echo "release_tag=stable" >> "$GITHUB_OUTPUT" - echo "skip=false" >> "$GITHUB_OUTPUT" - echo "branch=main" >> "$GITHUB_OUTPUT" - - # -- CHECK FOR RC PROMOTION ------------------------------------------------ - - name: "Check for RC release" - id: rc - if: steps.version.outputs.skip != 'true' - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - RC_JSON=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \ - "${API_BASE}/releases/tags/release-candidate" 2>/dev/null || echo "{}") - RC_ID=$(echo "$RC_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('id',''))" 2>/dev/null || true) - - if [ -n "$RC_ID" ] && [ "$RC_ID" != "None" ] && [ "$RC_ID" != "" ]; then - echo "promote=true" >> "$GITHUB_OUTPUT" - echo "release_id=${RC_ID}" >> "$GITHUB_OUTPUT" - echo "::notice::RC release found (id: ${RC_ID}) — will promote to stable" - else - echo "promote=false" >> "$GITHUB_OUTPUT" - echo "::notice::No RC release — full build pipeline" - fi - - - name: "Step 1b: Minor bump version" - id: bump - if: >- - steps.version.outputs.skip != 'true' && - steps.rc.outputs.promote != 'true' - run: | - MOKO_API="/tmp/moko-platform-api/cli" - php ${MOKO_API}/version_bump.php --path . --minor 2>&1 || true - VERSION=$(php ${MOKO_API}/version_read.php --path .) - # Strip any pre-release suffix — stable releases have no suffix - VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//') - echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - echo "Bumped to: ${VERSION}" - - - name: Check if already released - if: steps.version.outputs.skip != 'true' - id: check - run: | - TAG="${{ steps.version.outputs.release_tag }}" - BRANCH="${{ steps.version.outputs.branch }}" - - TAG_EXISTS=false - BRANCH_EXISTS=false - - git rev-parse "$TAG" >/dev/null 2>&1 && TAG_EXISTS=true - git ls-remote --heads origin "$BRANCH" 2>/dev/null | grep -q "$BRANCH" && BRANCH_EXISTS=true - - echo "tag_exists=$TAG_EXISTS" >> "$GITHUB_OUTPUT" - echo "branch_exists=$BRANCH_EXISTS" >> "$GITHUB_OUTPUT" - - # Tag and branch may persist across patch releases — never skip - echo "already_released=false" >> "$GITHUB_OUTPUT" - - # -- SANITY CHECKS ------------------------------------------------------- - - name: "Sanity: Pre-release validation" - if: >- - steps.version.outputs.skip != 'true' && - steps.check.outputs.already_released != 'true' - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - php /tmp/moko-platform-api/cli/release_validate.php \ - --path . --version "$VERSION" --output-summary --github-output || true - - # -- STEP 2: Create or update version/XX.YY archive branch --------------- - # Always runs — every version change on main archives to version/XX.YY - - name: "Step 2: Version archive branch" - if: steps.check.outputs.already_released != 'true' - run: | - BRANCH="${{ steps.version.outputs.branch }}" - IS_MINOR="${{ steps.version.outputs.is_minor }}" - PATCH="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - PATCH_NUM=$(echo "$PATCH" | awk -F. '{print $3}') - - # Check if branch exists - if git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then - git push origin HEAD:"$BRANCH" --force - echo "Updated archive branch: ${BRANCH} (patch ${PATCH_NUM})" >> $GITHUB_STEP_SUMMARY - else - git checkout -b "$BRANCH" 2>/dev/null || git checkout "$BRANCH" - git push origin "$BRANCH" --force - echo "Created archive branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY - fi - - # -- STEP 3: Set platform version ---------------------------------------- - - name: "Step 3: Set platform version" - if: >- - steps.version.outputs.skip != 'true' && - steps.check.outputs.already_released != 'true' - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - php /tmp/moko-platform-api/cli/version_set_platform.php \ - --path . --version "$VERSION" --branch main - - # -- STEP 4: Update version badges ---------------------------------------- - - name: "Step 4: Update version badges" - if: steps.version.outputs.skip != 'true' - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - php /tmp/moko-platform-api/cli/badge_update.php --path . --version "${VERSION}" 2>/dev/null || true - php /tmp/moko-platform-api/cli/version_check.php --path . --fix 2>/dev/null || true - - # Step 5 (updates.xml) moved after Step 8 to include SHA-256 checksum - - - name: "Step 4b: Promote and prune CHANGELOG" - if: >- - steps.version.outputs.skip != 'true' && - steps.check.outputs.already_released != 'true' - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - MOKO_API="/tmp/moko-platform-api/cli" - if [ -f "CHANGELOG.md" ]; then - php ${MOKO_API}/changelog_promote.php --path . --version "$VERSION" 2>&1 || true - php ${MOKO_API}/changelog_prune.php --path . --keep 5 2>&1 || true - fi - - - name: Commit release changes - if: >- - steps.version.outputs.skip != 'true' && - steps.check.outputs.already_released != 'true' - run: | - if git diff --quiet && git diff --cached --quiet; then - echo "No changes to commit" - exit 0 - fi - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - git add -A - git commit -m "chore(release): build ${VERSION} [skip ci]" \ - --author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" - # Detached HEAD on PR merge — push explicitly to main - git push origin HEAD:refs/heads/main - - # -- STEP 6: Create tag --------------------------------------------------- - - name: "Step 6: Create git tag" - if: >- - steps.version.outputs.skip != 'true' - run: | - RELEASE_TAG="${{ steps.version.outputs.release_tag }}" - # Only create the major release tag if it doesn't exist yet - if ! git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then - git tag "$RELEASE_TAG" - git push origin "$RELEASE_TAG" - echo "Tag created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY - else - echo "Tag ${RELEASE_TAG} already exists" >> $GITHUB_STEP_SUMMARY - fi - echo "Tag: ${TAG}" >> $GITHUB_STEP_SUMMARY - - # -- STEP 7a: Promote RC to stable (skip build) ---------------------------- - - name: "Step 7a: Promote RC to stable" - if: >- - steps.version.outputs.skip != 'true' && - steps.rc.outputs.promote == 'true' - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php /tmp/moko-platform-api/cli/release_promote.php \ - --from release-candidate --to stable \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" \ - --api-base "${API_BASE}" \ - --path . --branch main - echo "Promoted RC → stable (${VERSION})" >> $GITHUB_STEP_SUMMARY - - # -- STEP 7b: Create or update Gitea Release (full build path) ------------- - - name: "Step 7b: Gitea Release" - if: >- - steps.version.outputs.skip != 'true' && - steps.rc.outputs.promote != 'true' - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - RELEASE_TAG="${{ steps.version.outputs.release_tag }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php /tmp/moko-platform-api/cli/release_create.php \ - --path . --version "$VERSION" --tag "$RELEASE_TAG" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ - --repo "${GITEA_REPO}" --branch main - echo "Release created: ${VERSION}" >> $GITHUB_STEP_SUMMARY - - # -- STEP 8: Build packages and upload to release ---------------------------- - - name: "Step 8: Build package and upload" - id: package - if: >- - steps.version.outputs.skip != 'true' && - steps.rc.outputs.promote != 'true' - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - RELEASE_TAG="${{ steps.version.outputs.release_tag }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php /tmp/moko-platform-api/cli/release_package.php \ - --path . --version "$VERSION" --tag "$RELEASE_TAG" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ - --repo "${GITEA_REPO}" --output /tmp || true - - # -- STEP 5: Write update stream (after build so SHA-256 is available) ----- - - name: "Step 5: Write update stream" - if: steps.version.outputs.skip != 'true' - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - SHA256="${{ steps.package.outputs.sha256_zip }}" - - # Fetch latest updates.xml from main so preserve logic has all channels - GITEA_TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" - API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" - curl -sf -H "Authorization: token ${GITEA_TOKEN}" \ - "${API}/contents/updates.xml?ref=main" 2>/dev/null | \ - python3 -c "import sys,json,base64; print(base64.b64decode(json.load(sys.stdin)['content']).decode())" \ - > updates.xml 2>/dev/null || true - - SHA_FLAG="" - [ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}" - - php /tmp/moko-platform-api/cli/updates_xml_build.php \ - --path . --version "${VERSION}" --stability stable \ - --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \ - ${SHA_FLAG} --github-output - - # Commit updates.xml if changed - if ! git diff --quiet updates.xml 2>/dev/null; then - git add updates.xml - git commit -m "chore: update stable channel ${VERSION} [skip ci]" \ - --author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" - git push origin HEAD:refs/heads/main 2>&1 || true - fi - - # -- STEP 8b: Update release description with changelog ---------------------- - - name: "Step 8b: Update release body" - if: steps.version.outputs.skip != 'true' - continue-on-error: true - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - RELEASE_TAG="${{ steps.version.outputs.release_tag }}" - php /tmp/moko-platform-api/cli/release_body_update.php \ - --path . --version "${VERSION}" --tag "${RELEASE_TAG}" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" \ - --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \ - 2>&1 || true - echo "Release body updated" >> $GITHUB_STEP_SUMMARY - - # -- STEP 9: Mirror to GitHub (stable only) -------------------------------- - - name: "Step 9: Mirror release to GitHub" - if: >- - steps.version.outputs.skip != 'true' && - secrets.GH_MIRROR_TOKEN != '' - continue-on-error: true - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - RELEASE_TAG="${{ steps.version.outputs.release_tag }}" - GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php /tmp/moko-platform-api/cli/release_mirror.php \ - --version "$VERSION" --tag "$RELEASE_TAG" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ - --gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \ - --branch main 2>&1 || true - echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY - - # -- STEP 10: Sync main branch to GitHub mirror ---------------------------- - - name: "Step 10: Push main to GitHub mirror" - if: >- - steps.version.outputs.skip != 'true' && - secrets.GH_MIRROR_TOKEN != '' - continue-on-error: true - run: | - GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" - GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1) - GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2) - git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \ - git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" - git fetch origin main --depth=1 - git push github origin/main:refs/heads/main --force 2>/dev/null \ - && echo "main branch pushed to GitHub mirror" \ - || echo "WARNING: GitHub mirror push failed" - - # -- Clean up lesser pre-releases (cascade) --------------------------------- - # stable → deletes all | rc → beta,alpha,dev | beta → alpha,dev | alpha → dev - - name: "Delete lesser pre-release channels" - continue-on-error: true - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php /tmp/moko-platform-api/cli/release_cascade.php \ - --stability stable \ - --version "${VERSION}" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" \ - --api-base "${API_BASE}" 2>/dev/null || true - - - name: "Step 11: Delete and recreate dev branch from main" - if: steps.version.outputs.skip != 'true' - continue-on-error: true - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" - - # Delete dev branch - curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ - "${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch" - - # Recreate dev from main (now includes version bump + changelog promotion) - curl -sf -X POST -H "Authorization: token ${TOKEN}" \ - -H "Content-Type: application/json" \ - "${API_BASE}/branches" \ - -d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main" - - echo "Dev branch reset from main (keeps dev ahead after release)" >> $GITHUB_STEP_SUMMARY - - - name: "Step 12: Create version branch from main" - if: steps.version.outputs.skip != 'true' - continue-on-error: true - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - BRANCH_NAME="version/${VERSION}" - MAIN_SHA=$(git rev-parse HEAD) - - # Delete old version branch if it exists (same version re-release) - curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}" - - # Create version/XX.YY.ZZ from main - curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed" - - echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY - - - - # -- Dolibarr post-release: Reset dev version ----------------------------- - - name: "Post-release: Reset dev version" - if: steps.version.outputs.skip != 'true' - continue-on-error: true - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php /tmp/moko-platform-api/cli/version_reset_dev.php \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \ - --branch dev --path . 2>&1 || true - - # -- Summary -------------------------------------------------------------- - - name: Pipeline Summary - if: always() - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - PLATFORM="${{ steps.platform.outputs.platform }}" - if [ "${{ steps.version.outputs.skip }}" = "true" ]; then - echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY - echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY - elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then - echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY - else - echo "" >> $GITHUB_STEP_SUMMARY - echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY - echo "|------|--------|" >> $GITHUB_STEP_SUMMARY - echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY - fi +# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: moko-platform.Release +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform +# PATH: /templates/workflows/universal/auto-release.yml.template +# VERSION: 05.00.00 +# BRIEF: Universal build & release � detects platform from manifest.xml +# +# +========================================================================+ +# | UNIVERSAL BUILD & RELEASE PIPELINE | +# +========================================================================+ +# | | +# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. | +# | | +# | Platform-specific: | +# | joomla: XML manifest, updates.xml, type-prefixed packages | +# | dolibarr: mod*.class.php, update.txt, dev version reset | +# | generic: README-only, no update stream | +# | | +# +========================================================================+ + +name: "Universal: Build & Release" + +on: + pull_request: + types: [opened, closed] + branches: + - main + workflow_dispatch: + inputs: + action: + description: 'Action to perform' + required: false + type: choice + default: release + options: + - release + - promote-rc + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + +permissions: + contents: write + +jobs: + # ── PR Opened → Rename branch to RC and build RC release ───────────────────── + promote-rc: + name: Promote to RC + runs-on: release + if: >- + (github.event.action == 'opened' && github.event.pull_request.merged != true) || + (github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc') + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.MOKOGITEA_TOKEN }} + fetch-depth: 1 + + - name: Setup moko-platform tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + run: | + if ! command -v composer &> /dev/null; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + # Always fetch latest CLI tools — never use stale cache from previous runs + rm -rf /tmp/moko-platform-api + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ + /tmp/moko-platform-api + cd /tmp/moko-platform-api + composer install --no-dev --no-interaction --quiet + + - name: Rename source branch to rc + run: | + SOURCE_BRANCH="${{ github.event.pull_request.head.ref || 'dev' }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + PR_NUM="${{ github.event.pull_request.number }}" + php /tmp/moko-platform-api/cli/branch_rename.php \ + --from "$SOURCE_BRANCH" --to rc \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" \ + --api-base "${API_BASE}" \ + --pr "$PR_NUM" + + - name: Set RC version on renamed branch + run: | + # Checkout the new rc branch + git fetch origin rc + git checkout rc + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + MOKO_CLI="/tmp/moko-platform-api/cli" + + VERSION=$(php ${MOKO_CLI}/version_read.php --path .) || true + [ -z "$VERSION" ] && { echo "No version — skipping"; exit 0; } + + php ${MOKO_CLI}/version_set_platform.php \ + --path . --version "$VERSION" --branch rc --stability rc 2>/dev/null || true + php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true + + if ! git diff --quiet || ! git diff --cached --quiet; then + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git add -A + git commit -m "chore(version): set RC stability suffix [skip ci]" \ + --author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" + git push origin rc + fi + + - name: Build RC release + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + MOKO_CLI="/tmp/moko-platform-api/cli" + VERSION=$(php ${MOKO_CLI}/version_read.php --path .) || true + + php ${MOKO_CLI}/release_create.php \ + --path . --version "$VERSION" --tag "release-candidate" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ + --repo "${GITEA_REPO}" --branch rc 2>&1 || true + + php ${MOKO_CLI}/release_package.php \ + --path . --version "$VERSION" --tag "release-candidate" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ + --repo "${GITEA_REPO}" --output /tmp 2>&1 || true + + - name: Cascade lesser channels + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php /tmp/moko-platform-api/cli/release_cascade.php \ + --stability release-candidate \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" \ + --api-base "${API_BASE}" + + - name: Summary + if: always() + run: | + echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY + echo "Draft PR opened — branch renamed to rc, RC release built" >> $GITHUB_STEP_SUMMARY + + # ── Merged PR → Build & Release (or promote RC to stable) ──────────────────── + release: + name: Build & Release Pipeline + runs-on: release + if: >- + github.event.pull_request.merged == true || + (github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc') + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.MOKOGITEA_TOKEN }} + fetch-depth: 0 + + - name: Configure git for bot pushes + run: | + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + + - name: Setup moko-platform tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}' + run: | + # Ensure PHP + Composer are available + if ! command -v composer &> /dev/null; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + # Always fetch latest CLI tools — never use stale cache from previous runs + rm -rf /tmp/moko-platform-api + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ + /tmp/moko-platform-api + cd /tmp/moko-platform-api + composer install --no-dev --no-interaction --quiet + + + # -- PLATFORM DETECTION --------------------------------------------------- + - name: Detect platform + id: platform + run: | + php /tmp/moko-platform-api/cli/manifest_read.php --path . --github-output + MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true) + MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1 || true) + echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT" + echo "mod_file=${MOD_FILE}" >> "$GITHUB_OUTPUT" + + - name: "Step 1: Read version" + id: version + run: | + VERSION=$(php /tmp/moko-platform-api/cli/version_read.php --path .) + if [ -z "$VERSION" ]; then + echo "::error::No VERSION in README.md" + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + # version_set_platform strips suffixes internally when --stability stable + MAJOR=$(echo "$VERSION" | cut -d. -f1 | sed 's/-.*//') + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "release_tag=stable" >> "$GITHUB_OUTPUT" + echo "skip=false" >> "$GITHUB_OUTPUT" + echo "branch=main" >> "$GITHUB_OUTPUT" + + # -- CHECK FOR RC PROMOTION ------------------------------------------------ + - name: "Check for RC release" + id: rc + if: steps.version.outputs.skip != 'true' + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + RC_JSON=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \ + "${API_BASE}/releases/tags/release-candidate" 2>/dev/null || echo "{}") + RC_ID=$(echo "$RC_JSON" | php -r "\$d=json_decode(file_get_contents('php://stdin'),true); echo \$d['id'] ?? '';" 2>/dev/null || true) + + if [ -n "$RC_ID" ] && [ "$RC_ID" != "None" ] && [ "$RC_ID" != "" ]; then + echo "promote=true" >> "$GITHUB_OUTPUT" + echo "release_id=${RC_ID}" >> "$GITHUB_OUTPUT" + echo "::notice::RC release found (id: ${RC_ID}) — will promote to stable" + else + echo "promote=false" >> "$GITHUB_OUTPUT" + echo "::notice::No RC release — full build pipeline" + fi + + - name: "Step 1b: Minor bump version" + id: bump + if: >- + steps.version.outputs.skip != 'true' && + steps.rc.outputs.promote != 'true' + run: | + MOKO_API="/tmp/moko-platform-api/cli" + php ${MOKO_API}/version_bump.php --path . --minor 2>&1 || true + VERSION=$(php ${MOKO_API}/version_read.php --path .) + # version_set_platform handles suffix stripping — just pass clean base version + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "Bumped to: ${VERSION}" + + - name: Check if already released + if: steps.version.outputs.skip != 'true' + id: check + run: | + TAG="${{ steps.version.outputs.release_tag }}" + BRANCH="${{ steps.version.outputs.branch }}" + + TAG_EXISTS=false + BRANCH_EXISTS=false + + git rev-parse "$TAG" >/dev/null 2>&1 && TAG_EXISTS=true + git ls-remote --heads origin "$BRANCH" 2>/dev/null | grep -q "$BRANCH" && BRANCH_EXISTS=true + + echo "tag_exists=$TAG_EXISTS" >> "$GITHUB_OUTPUT" + echo "branch_exists=$BRANCH_EXISTS" >> "$GITHUB_OUTPUT" + + # Tag and branch may persist across patch releases — never skip + echo "already_released=false" >> "$GITHUB_OUTPUT" + + # -- SANITY CHECKS ------------------------------------------------------- + - name: "Sanity: Pre-release validation" + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.already_released != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + php /tmp/moko-platform-api/cli/release_validate.php \ + --path . --version "$VERSION" --output-summary --github-output || true + + # -- STEP 2: Create or update version/XX.YY archive branch --------------- + # Always runs — every version change on main archives to version/XX.YY + - name: "Step 2: Version archive branch" + if: steps.check.outputs.already_released != 'true' + run: | + BRANCH="${{ steps.version.outputs.branch }}" + IS_MINOR="${{ steps.version.outputs.is_minor }}" + PATCH="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + PATCH_NUM=$(echo "$PATCH" | awk -F. '{print $3}') + + # Check if branch exists + if git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then + git push origin HEAD:"$BRANCH" --force + echo "Updated archive branch: ${BRANCH} (patch ${PATCH_NUM})" >> $GITHUB_STEP_SUMMARY + else + git checkout -b "$BRANCH" 2>/dev/null || git checkout "$BRANCH" + git push origin "$BRANCH" --force + echo "Created archive branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY + fi + + # -- STEP 3: Set platform version ---------------------------------------- + - name: "Step 3: Set platform version" + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.already_released != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + php /tmp/moko-platform-api/cli/version_set_platform.php \ + --path . --version "$VERSION" --branch main + + # -- STEP 4: Update version badges ---------------------------------------- + - name: "Step 4: Update version badges" + if: steps.version.outputs.skip != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + php /tmp/moko-platform-api/cli/badge_update.php --path . --version "${VERSION}" 2>/dev/null || true + php /tmp/moko-platform-api/cli/version_check.php --path . --fix 2>/dev/null || true + + # Step 5 (updates.xml) moved after Step 8 to include SHA-256 checksum + + - name: "Step 4b: Promote and prune CHANGELOG" + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.already_released != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + MOKO_API="/tmp/moko-platform-api/cli" + if [ -f "CHANGELOG.md" ]; then + php ${MOKO_API}/changelog_promote.php --path . --version "$VERSION" 2>&1 || true + php ${MOKO_API}/changelog_prune.php --path . --keep 5 2>&1 || true + fi + + - name: Commit release changes + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.already_released != 'true' + run: | + if git diff --quiet && git diff --cached --quiet; then + echo "No changes to commit" + exit 0 + fi + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + git add -A + git commit -m "chore(release): build ${VERSION} [skip ci]" \ + --author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" + # Detached HEAD on PR merge — push explicitly to main + git push origin HEAD:refs/heads/main + + # -- STEP 6: Create tag --------------------------------------------------- + - name: "Step 6: Create git tag" + if: >- + steps.version.outputs.skip != 'true' + run: | + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + # Only create the major release tag if it doesn't exist yet + if ! git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then + git tag "$RELEASE_TAG" + git push origin "$RELEASE_TAG" + echo "Tag created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY + else + echo "Tag ${RELEASE_TAG} already exists" >> $GITHUB_STEP_SUMMARY + fi + echo "Tag: ${TAG}" >> $GITHUB_STEP_SUMMARY + + # -- STEP 7a: Promote RC to stable (skip build) ---------------------------- + - name: "Step 7a: Promote RC to stable" + if: >- + steps.version.outputs.skip != 'true' && + steps.rc.outputs.promote == 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php /tmp/moko-platform-api/cli/release_promote.php \ + --from release-candidate --to stable \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" \ + --api-base "${API_BASE}" \ + --path . --branch main + echo "Promoted RC → stable (${VERSION})" >> $GITHUB_STEP_SUMMARY + + # -- STEP 7b: Create or update Gitea Release (full build path) ------------- + - name: "Step 7b: Gitea Release" + if: >- + steps.version.outputs.skip != 'true' && + steps.rc.outputs.promote != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php /tmp/moko-platform-api/cli/release_create.php \ + --path . --version "$VERSION" --tag "$RELEASE_TAG" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ + --repo "${GITEA_REPO}" --branch main + echo "Release created: ${VERSION}" >> $GITHUB_STEP_SUMMARY + + # -- STEP 8: Build packages and upload to release ---------------------------- + - name: "Step 8: Build package and upload" + id: package + if: >- + steps.version.outputs.skip != 'true' && + steps.rc.outputs.promote != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php /tmp/moko-platform-api/cli/release_package.php \ + --path . --version "$VERSION" --tag "$RELEASE_TAG" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ + --repo "${GITEA_REPO}" --output /tmp || true + + # -- STEP 5: Write update stream (after build so SHA-256 is available) ----- + - name: "Step 5: Write update stream" + if: steps.version.outputs.skip != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + SHA256="${{ steps.package.outputs.sha256_zip }}" + + # Fetch latest updates.xml from main so preserve logic has all channels + GITEA_TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" + API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" + curl -sf -H "Authorization: token ${GITEA_TOKEN}" \ + "${API}/contents/updates.xml?ref=main" 2>/dev/null | \ + php -r "\$d=json_decode(file_get_contents('php://stdin'),true); echo base64_decode(\$d['content'] ?? '');" \ + > updates.xml 2>/dev/null || true + + SHA_FLAG="" + [ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}" + + php /tmp/moko-platform-api/cli/updates_xml_build.php \ + --path . --version "${VERSION}" --stability stable \ + --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \ + ${SHA_FLAG} --github-output + + # Commit updates.xml if changed + if ! git diff --quiet updates.xml 2>/dev/null; then + git add updates.xml + git commit -m "chore: update stable channel ${VERSION} [skip ci]" \ + --author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" + git push origin HEAD:refs/heads/main 2>&1 || true + fi + + # -- STEP 8b: Update release description with changelog ---------------------- + - name: "Step 8b: Update release body" + if: steps.version.outputs.skip != 'true' + continue-on-error: true + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + php /tmp/moko-platform-api/cli/release_body_update.php \ + --path . --version "${VERSION}" --tag "${RELEASE_TAG}" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" \ + --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \ + 2>&1 || true + echo "Release body updated" >> $GITHUB_STEP_SUMMARY + + # -- STEP 9: Mirror to GitHub (stable only) -------------------------------- + - name: "Step 9: Mirror release to GitHub" + if: >- + steps.version.outputs.skip != 'true' && + secrets.GH_MIRROR_TOKEN != '' + continue-on-error: true + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php /tmp/moko-platform-api/cli/release_mirror.php \ + --version "$VERSION" --tag "$RELEASE_TAG" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ + --gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \ + --branch main 2>&1 || true + echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY + + # -- STEP 10: Sync main branch to GitHub mirror ---------------------------- + - name: "Step 10: Push main to GitHub mirror" + if: >- + steps.version.outputs.skip != 'true' && + secrets.GH_MIRROR_TOKEN != '' + continue-on-error: true + run: | + GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" + GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1) + GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2) + git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \ + git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" + git fetch origin main --depth=1 + git push github origin/main:refs/heads/main --force 2>/dev/null \ + && echo "main branch pushed to GitHub mirror" \ + || echo "WARNING: GitHub mirror push failed" + + # -- Clean up lesser pre-releases (cascade) --------------------------------- + # stable → deletes all | rc → beta,alpha,dev | beta → alpha,dev | alpha → dev + - name: "Delete lesser pre-release channels" + continue-on-error: true + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php /tmp/moko-platform-api/cli/release_cascade.php \ + --stability stable \ + --version "${VERSION}" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" \ + --api-base "${API_BASE}" 2>/dev/null || true + + - name: "Step 11: Clean up pre-release branches and recreate dev from main" + if: steps.version.outputs.skip != 'true' + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" + + # Delete ephemeral pre-release branches (rc, alpha, beta) + for EPHEMERAL in rc alpha beta; do + curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/branches/${EPHEMERAL}" 2>/dev/null \ + && echo "Deleted ${EPHEMERAL} branch" \ + || echo "${EPHEMERAL} branch not found" + done + + # Delete dev branch + curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch" + + # Recreate dev from main (now includes version bump + changelog promotion) + curl -sf -X POST -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/json" \ + "${API_BASE}/branches" \ + -d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main" + + echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY + + - name: "Step 12: Create version branch from main" + if: steps.version.outputs.skip != 'true' + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + BRANCH_NAME="version/${VERSION}" + MAIN_SHA=$(git rev-parse HEAD) + + # Delete old version branch if it exists (same version re-release) + curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}" + + # Create version/XX.YY.ZZ from main + curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed" + + echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY + + + + # -- Dolibarr post-release: Reset dev version ----------------------------- + - name: "Post-release: Reset dev version" + if: steps.version.outputs.skip != 'true' + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php /tmp/moko-platform-api/cli/version_reset_dev.php \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \ + --branch dev --path . 2>&1 || true + + # -- Summary -------------------------------------------------------------- + - name: Pipeline Summary + if: always() + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + PLATFORM="${{ steps.platform.outputs.platform }}" + if [ "${{ steps.version.outputs.skip }}" = "true" ]; then + echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY + echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY + elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then + echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY + echo "|------|--------|" >> $GITHUB_STEP_SUMMARY + echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY + fi From 5c175ebc21a6d2dec2f105f14f5eae42cf3df7ee Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sat, 30 May 2026 15:02:21 +0000 Subject: [PATCH 062/132] chore: sync .mokogitea/workflows/auto-bump.yml from moko-platform [skip ci] --- .mokogitea/workflows/auto-bump.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.mokogitea/workflows/auto-bump.yml b/.mokogitea/workflows/auto-bump.yml index a397a9e..fb9dc82 100644 --- a/.mokogitea/workflows/auto-bump.yml +++ b/.mokogitea/workflows/auto-bump.yml @@ -16,10 +16,9 @@ on: push: branches: - dev - - alpha - - beta - rc - 'feature/**' + - 'patch/**' env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true From 0ff6798918f91c35bac5c178d8a58d2ab39630ac Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sat, 30 May 2026 15:04:57 +0000 Subject: [PATCH 063/132] chore: sync .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] --- .mokogitea/workflows/auto-release.yml | 353 ++------------------------ 1 file changed, 22 insertions(+), 331 deletions(-) diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml index 04ec817..1227ff8 100644 --- a/.mokogitea/workflows/auto-release.yml +++ b/.mokogitea/workflows/auto-release.yml @@ -82,71 +82,33 @@ jobs: cd /tmp/moko-platform-api composer install --no-dev --no-interaction --quiet - - name: Rename source branch to rc + - name: Rename branch to rc run: | - SOURCE_BRANCH="${{ github.event.pull_request.head.ref || 'dev' }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - PR_NUM="${{ github.event.pull_request.number }}" php /tmp/moko-platform-api/cli/branch_rename.php \ - --from "$SOURCE_BRANCH" --to rc \ + --from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \ --token "${{ secrets.MOKOGITEA_TOKEN }}" \ - --api-base "${API_BASE}" \ - --pr "$PR_NUM" + --api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \ + --pr "${{ github.event.pull_request.number }}" - - name: Set RC version on renamed branch + - name: Checkout rc and configure git run: | - # Checkout the new rc branch git fetch origin rc git checkout rc - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - MOKO_CLI="/tmp/moko-platform-api/cli" + 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" - VERSION=$(php ${MOKO_CLI}/version_read.php --path .) || true - [ -z "$VERSION" ] && { echo "No version — skipping"; exit 0; } - - php ${MOKO_CLI}/version_set_platform.php \ - --path . --version "$VERSION" --branch rc --stability rc 2>/dev/null || true - php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true - - if ! git diff --quiet || ! git diff --cached --quiet; then - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - git add -A - git commit -m "chore(version): set RC stability suffix [skip ci]" \ - --author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" - git push origin rc - fi - - - name: Build RC release + - name: Publish RC release run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - MOKO_CLI="/tmp/moko-platform-api/cli" - VERSION=$(php ${MOKO_CLI}/version_read.php --path .) || true - - php ${MOKO_CLI}/release_create.php \ - --path . --version "$VERSION" --tag "release-candidate" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ - --repo "${GITEA_REPO}" --branch rc 2>&1 || true - - php ${MOKO_CLI}/release_package.php \ - --path . --version "$VERSION" --tag "release-candidate" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ - --repo "${GITEA_REPO}" --output /tmp 2>&1 || true - - - name: Cascade lesser channels - continue-on-error: true - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php /tmp/moko-platform-api/cli/release_cascade.php \ - --stability release-candidate \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" \ - --api-base "${API_BASE}" + php /tmp/moko-platform-api/cli/release_publish.php \ + --path . --stability rc --bump minor --branch rc \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" - name: Summary if: always() run: | echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY - echo "Draft PR opened — branch renamed to rc, RC release built" >> $GITHUB_STEP_SUMMARY + echo "Branch renamed to rc, minor bump, RC + lesser stream releases built, updates.xml synced" >> $GITHUB_STEP_SUMMARY # ── Merged PR → Build & Release (or promote RC to stable) ──────────────────── release: @@ -188,266 +150,11 @@ jobs: composer install --no-dev --no-interaction --quiet - # -- PLATFORM DETECTION --------------------------------------------------- - - name: Detect platform - id: platform + - name: "Publish stable release" run: | - php /tmp/moko-platform-api/cli/manifest_read.php --path . --github-output - MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true) - MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1 || true) - echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT" - echo "mod_file=${MOD_FILE}" >> "$GITHUB_OUTPUT" - - - name: "Step 1: Read version" - id: version - run: | - VERSION=$(php /tmp/moko-platform-api/cli/version_read.php --path .) - if [ -z "$VERSION" ]; then - echo "::error::No VERSION in README.md" - echo "skip=true" >> "$GITHUB_OUTPUT" - exit 0 - fi - # version_set_platform strips suffixes internally when --stability stable - MAJOR=$(echo "$VERSION" | cut -d. -f1 | sed 's/-.*//') - echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - echo "release_tag=stable" >> "$GITHUB_OUTPUT" - echo "skip=false" >> "$GITHUB_OUTPUT" - echo "branch=main" >> "$GITHUB_OUTPUT" - - # -- CHECK FOR RC PROMOTION ------------------------------------------------ - - name: "Check for RC release" - id: rc - if: steps.version.outputs.skip != 'true' - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - RC_JSON=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \ - "${API_BASE}/releases/tags/release-candidate" 2>/dev/null || echo "{}") - RC_ID=$(echo "$RC_JSON" | php -r "\$d=json_decode(file_get_contents('php://stdin'),true); echo \$d['id'] ?? '';" 2>/dev/null || true) - - if [ -n "$RC_ID" ] && [ "$RC_ID" != "None" ] && [ "$RC_ID" != "" ]; then - echo "promote=true" >> "$GITHUB_OUTPUT" - echo "release_id=${RC_ID}" >> "$GITHUB_OUTPUT" - echo "::notice::RC release found (id: ${RC_ID}) — will promote to stable" - else - echo "promote=false" >> "$GITHUB_OUTPUT" - echo "::notice::No RC release — full build pipeline" - fi - - - name: "Step 1b: Minor bump version" - id: bump - if: >- - steps.version.outputs.skip != 'true' && - steps.rc.outputs.promote != 'true' - run: | - MOKO_API="/tmp/moko-platform-api/cli" - php ${MOKO_API}/version_bump.php --path . --minor 2>&1 || true - VERSION=$(php ${MOKO_API}/version_read.php --path .) - # version_set_platform handles suffix stripping — just pass clean base version - echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - echo "Bumped to: ${VERSION}" - - - name: Check if already released - if: steps.version.outputs.skip != 'true' - id: check - run: | - TAG="${{ steps.version.outputs.release_tag }}" - BRANCH="${{ steps.version.outputs.branch }}" - - TAG_EXISTS=false - BRANCH_EXISTS=false - - git rev-parse "$TAG" >/dev/null 2>&1 && TAG_EXISTS=true - git ls-remote --heads origin "$BRANCH" 2>/dev/null | grep -q "$BRANCH" && BRANCH_EXISTS=true - - echo "tag_exists=$TAG_EXISTS" >> "$GITHUB_OUTPUT" - echo "branch_exists=$BRANCH_EXISTS" >> "$GITHUB_OUTPUT" - - # Tag and branch may persist across patch releases — never skip - echo "already_released=false" >> "$GITHUB_OUTPUT" - - # -- SANITY CHECKS ------------------------------------------------------- - - name: "Sanity: Pre-release validation" - if: >- - steps.version.outputs.skip != 'true' && - steps.check.outputs.already_released != 'true' - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - php /tmp/moko-platform-api/cli/release_validate.php \ - --path . --version "$VERSION" --output-summary --github-output || true - - # -- STEP 2: Create or update version/XX.YY archive branch --------------- - # Always runs — every version change on main archives to version/XX.YY - - name: "Step 2: Version archive branch" - if: steps.check.outputs.already_released != 'true' - run: | - BRANCH="${{ steps.version.outputs.branch }}" - IS_MINOR="${{ steps.version.outputs.is_minor }}" - PATCH="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - PATCH_NUM=$(echo "$PATCH" | awk -F. '{print $3}') - - # Check if branch exists - if git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then - git push origin HEAD:"$BRANCH" --force - echo "Updated archive branch: ${BRANCH} (patch ${PATCH_NUM})" >> $GITHUB_STEP_SUMMARY - else - git checkout -b "$BRANCH" 2>/dev/null || git checkout "$BRANCH" - git push origin "$BRANCH" --force - echo "Created archive branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY - fi - - # -- STEP 3: Set platform version ---------------------------------------- - - name: "Step 3: Set platform version" - if: >- - steps.version.outputs.skip != 'true' && - steps.check.outputs.already_released != 'true' - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - php /tmp/moko-platform-api/cli/version_set_platform.php \ - --path . --version "$VERSION" --branch main - - # -- STEP 4: Update version badges ---------------------------------------- - - name: "Step 4: Update version badges" - if: steps.version.outputs.skip != 'true' - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - php /tmp/moko-platform-api/cli/badge_update.php --path . --version "${VERSION}" 2>/dev/null || true - php /tmp/moko-platform-api/cli/version_check.php --path . --fix 2>/dev/null || true - - # Step 5 (updates.xml) moved after Step 8 to include SHA-256 checksum - - - name: "Step 4b: Promote and prune CHANGELOG" - if: >- - steps.version.outputs.skip != 'true' && - steps.check.outputs.already_released != 'true' - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - MOKO_API="/tmp/moko-platform-api/cli" - if [ -f "CHANGELOG.md" ]; then - php ${MOKO_API}/changelog_promote.php --path . --version "$VERSION" 2>&1 || true - php ${MOKO_API}/changelog_prune.php --path . --keep 5 2>&1 || true - fi - - - name: Commit release changes - if: >- - steps.version.outputs.skip != 'true' && - steps.check.outputs.already_released != 'true' - run: | - if git diff --quiet && git diff --cached --quiet; then - echo "No changes to commit" - exit 0 - fi - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - git add -A - git commit -m "chore(release): build ${VERSION} [skip ci]" \ - --author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" - # Detached HEAD on PR merge — push explicitly to main - git push origin HEAD:refs/heads/main - - # -- STEP 6: Create tag --------------------------------------------------- - - name: "Step 6: Create git tag" - if: >- - steps.version.outputs.skip != 'true' - run: | - RELEASE_TAG="${{ steps.version.outputs.release_tag }}" - # Only create the major release tag if it doesn't exist yet - if ! git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then - git tag "$RELEASE_TAG" - git push origin "$RELEASE_TAG" - echo "Tag created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY - else - echo "Tag ${RELEASE_TAG} already exists" >> $GITHUB_STEP_SUMMARY - fi - echo "Tag: ${TAG}" >> $GITHUB_STEP_SUMMARY - - # -- STEP 7a: Promote RC to stable (skip build) ---------------------------- - - name: "Step 7a: Promote RC to stable" - if: >- - steps.version.outputs.skip != 'true' && - steps.rc.outputs.promote == 'true' - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php /tmp/moko-platform-api/cli/release_promote.php \ - --from release-candidate --to stable \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" \ - --api-base "${API_BASE}" \ - --path . --branch main - echo "Promoted RC → stable (${VERSION})" >> $GITHUB_STEP_SUMMARY - - # -- STEP 7b: Create or update Gitea Release (full build path) ------------- - - name: "Step 7b: Gitea Release" - if: >- - steps.version.outputs.skip != 'true' && - steps.rc.outputs.promote != 'true' - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - RELEASE_TAG="${{ steps.version.outputs.release_tag }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php /tmp/moko-platform-api/cli/release_create.php \ - --path . --version "$VERSION" --tag "$RELEASE_TAG" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ - --repo "${GITEA_REPO}" --branch main - echo "Release created: ${VERSION}" >> $GITHUB_STEP_SUMMARY - - # -- STEP 8: Build packages and upload to release ---------------------------- - - name: "Step 8: Build package and upload" - id: package - if: >- - steps.version.outputs.skip != 'true' && - steps.rc.outputs.promote != 'true' - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - RELEASE_TAG="${{ steps.version.outputs.release_tag }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php /tmp/moko-platform-api/cli/release_package.php \ - --path . --version "$VERSION" --tag "$RELEASE_TAG" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ - --repo "${GITEA_REPO}" --output /tmp || true - - # -- STEP 5: Write update stream (after build so SHA-256 is available) ----- - - name: "Step 5: Write update stream" - if: steps.version.outputs.skip != 'true' - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - SHA256="${{ steps.package.outputs.sha256_zip }}" - - # Fetch latest updates.xml from main so preserve logic has all channels - GITEA_TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" - API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" - curl -sf -H "Authorization: token ${GITEA_TOKEN}" \ - "${API}/contents/updates.xml?ref=main" 2>/dev/null | \ - php -r "\$d=json_decode(file_get_contents('php://stdin'),true); echo base64_decode(\$d['content'] ?? '');" \ - > updates.xml 2>/dev/null || true - - SHA_FLAG="" - [ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}" - - php /tmp/moko-platform-api/cli/updates_xml_build.php \ - --path . --version "${VERSION}" --stability stable \ - --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \ - ${SHA_FLAG} --github-output - - # Commit updates.xml if changed - if ! git diff --quiet updates.xml 2>/dev/null; then - git add updates.xml - git commit -m "chore: update stable channel ${VERSION} [skip ci]" \ - --author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" - git push origin HEAD:refs/heads/main 2>&1 || true - fi - - # -- STEP 8b: Update release description with changelog ---------------------- - - name: "Step 8b: Update release body" - if: steps.version.outputs.skip != 'true' - continue-on-error: true - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - RELEASE_TAG="${{ steps.version.outputs.release_tag }}" - php /tmp/moko-platform-api/cli/release_body_update.php \ - --path . --version "${VERSION}" --tag "${RELEASE_TAG}" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" \ - --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \ - 2>&1 || true - echo "Release body updated" >> $GITHUB_STEP_SUMMARY + php /tmp/moko-platform-api/cli/release_publish.php \ + --path . --stability stable --bump minor --branch main \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" # -- STEP 9: Mirror to GitHub (stable only) -------------------------------- - name: "Step 9: Mirror release to GitHub" @@ -484,33 +191,17 @@ jobs: && echo "main branch pushed to GitHub mirror" \ || echo "WARNING: GitHub mirror push failed" - # -- Clean up lesser pre-releases (cascade) --------------------------------- - # stable → deletes all | rc → beta,alpha,dev | beta → alpha,dev | alpha → dev - - name: "Delete lesser pre-release channels" - continue-on-error: true - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php /tmp/moko-platform-api/cli/release_cascade.php \ - --stability stable \ - --version "${VERSION}" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" \ - --api-base "${API_BASE}" 2>/dev/null || true - - - name: "Step 11: Clean up pre-release branches and recreate dev from main" + - name: "Step 11: Delete rc branch and recreate dev from main" if: steps.version.outputs.skip != 'true' continue-on-error: true run: | API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" - # Delete ephemeral pre-release branches (rc, alpha, beta) - for EPHEMERAL in rc alpha beta; do - curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ - "${API_BASE}/branches/${EPHEMERAL}" 2>/dev/null \ - && echo "Deleted ${EPHEMERAL} branch" \ - || echo "${EPHEMERAL} branch not found" - done + # Delete rc branch (ephemeral — created by promote-rc) + curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/branches/rc" 2>/dev/null \ + && echo "Deleted rc branch" || echo "rc branch not found" # Delete dev branch curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ From d46f726a787ad4a5896c49937055d581831670ec Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sat, 30 May 2026 16:03:52 +0000 Subject: [PATCH 064/132] chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] --- .mokogitea/workflows/pr-check.yml | 38 ++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/.mokogitea/workflows/pr-check.yml b/.mokogitea/workflows/pr-check.yml index df06523..ce64a27 100644 --- a/.mokogitea/workflows/pr-check.yml +++ b/.mokogitea/workflows/pr-check.yml @@ -52,22 +52,22 @@ jobs: REASON="Fix branches must target 'dev', not '${BASE}'" fi ;; + patch/*) + if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then + ALLOWED=false + REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'" + fi + ;; hotfix/*) if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then ALLOWED=false REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'" fi ;; - alpha/*|beta/*) - if [ "$BASE" != "dev" ]; then - ALLOWED=false - REASON="Pre-release branches must target 'dev', not '${BASE}'" - fi - ;; - rc/*) + rc) if [ "$BASE" != "main" ]; then ALLOWED=false - REASON="Release candidate branches must target 'main', not '${BASE}'" + REASON="RC branch can only merge into 'main', not '${BASE}'" fi ;; dev) @@ -183,6 +183,28 @@ jobs: ;; esac + - name: Check changelog has unreleased entry + run: | + if [ ! -f "CHANGELOG.md" ]; then + echo "::warning::No CHANGELOG.md found" + exit 0 + fi + # Check for content under [Unreleased] section + if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then + echo "::error::CHANGELOG.md missing [Unreleased] section" + exit 1 + fi + # Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased + UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true) + if [ "$UNRELEASED_CONTENT" -eq 0 ]; then + echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes." + echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY + echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY + echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]" + - name: Verify package source run: | SOURCE_DIR="src" From a5b14048f4491fd2ade8b760c127fc8b69cc0b4f Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sat, 30 May 2026 19:03:10 -0500 Subject: [PATCH 065/132] =?UTF-8?q?refactor:=20rename=20MokoOpenGraph=20?= =?UTF-8?q?=E2=86=92=20MokoJoomOpenGraph,=20add=20moko-platform=20standard?= =?UTF-8?q?s=20compliance?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename project display name across 69 files (PHP, XML, INI, SQL, CSS, JS, JSON, MD) - Add <display-name> to .mokogitea/manifest.xml per moko-platform schema - Update pkg_mokoog.xml <name> to "Package - MokoJoomOpenGraph" (Joomla convention) - Update all update server URLs to new repo path - Add CONTRIBUTING.md and CODE_OF_CONDUCT.md (required by repo-health workflow) - Add .gitattributes for line-ending normalization and export-ignore rules - Add .gitignore Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- .gitattributes | 62 ++++++ .gitignore | 204 ++++++++++++++++++ .mokogitea/manifest.xml | 3 +- CHANGELOG.md | 2 +- CLAUDE.md | 4 +- CODE_OF_CONDUCT.md | 28 +++ CONTRIBUTING.md | 34 +++ Makefile | 2 +- README.md | 8 +- src/language/en-GB/pkg_mokoog.sys.ini | 6 +- src/language/en-US/pkg_mokoog.sys.ini | 6 +- .../api/src/Controller/TagsController.php | 2 +- .../api/src/View/Tags/JsonapiView.php | 2 +- src/packages/com_mokoog/forms/filter_tags.xml | 2 +- src/packages/com_mokoog/forms/tag.xml | 2 +- .../com_mokoog/language/en-GB/com_mokoog.ini | 6 +- .../language/en-GB/com_mokoog.sys.ini | 4 +- .../com_mokoog/language/en-US/com_mokoog.ini | 6 +- .../language/en-US/com_mokoog.sys.ini | 4 +- src/packages/com_mokoog/mokoog.xml | 2 +- src/packages/com_mokoog/script.php | 6 +- src/packages/com_mokoog/services/provider.php | 2 +- src/packages/com_mokoog/sql/install.mysql.sql | 2 +- .../com_mokoog/sql/uninstall.mysql.sql | 2 +- .../com_mokoog/sql/updates/mysql/01.01.00.sql | 2 +- .../com_mokoog/sql/updates/mysql/01.02.00.sql | 2 +- .../src/ContentType/ContentTypeInterface.php | 2 +- .../src/ContentType/HikaShopAdapter.php | 2 +- .../com_mokoog/src/ContentType/K2Adapter.php | 2 +- .../src/ContentType/VirtueMartAdapter.php | 2 +- .../src/Controller/BatchController.php | 2 +- .../src/Controller/DisplayController.php | 2 +- .../src/Controller/ImportExportController.php | 2 +- .../src/Extension/MokoOGComponent.php | 2 +- .../com_mokoog/src/Model/TagModel.php | 2 +- .../com_mokoog/src/Model/TagsModel.php | 2 +- .../com_mokoog/src/Table/TagTable.php | 2 +- .../com_mokoog/src/View/Tags/HtmlView.php | 2 +- src/packages/com_mokoog/tmpl/tags/default.php | 2 +- .../plg_content_mokoog/forms/mokoog.xml | 2 +- .../language/en-GB/plg_content_mokoog.ini | 2 +- .../language/en-GB/plg_content_mokoog.sys.ini | 4 +- .../language/en-US/plg_content_mokoog.ini | 2 +- .../language/en-US/plg_content_mokoog.sys.ini | 4 +- .../plg_content_mokoog/media/css/preview.css | 2 +- .../media/joomla.asset.json | 2 +- .../plg_content_mokoog/media/js/preview.js | 2 +- src/packages/plg_content_mokoog/mokoog.php | 2 +- src/packages/plg_content_mokoog/mokoog.xml | 4 +- .../plg_content_mokoog/services/provider.php | 2 +- .../src/Extension/MokoOGContent.php | 2 +- .../language/en-GB/plg_system_mokoog.ini | 2 +- .../language/en-GB/plg_system_mokoog.sys.ini | 4 +- .../language/en-US/plg_system_mokoog.ini | 2 +- .../language/en-US/plg_system_mokoog.sys.ini | 4 +- src/packages/plg_system_mokoog/mokoog.php | 2 +- src/packages/plg_system_mokoog/mokoog.xml | 4 +- .../plg_system_mokoog/services/provider.php | 2 +- .../src/Extension/MokoOG.php | 2 +- .../src/Helper/ImageGenerator.php | 2 +- .../src/Helper/ImageHelper.php | 2 +- .../src/Helper/JsonLdBuilder.php | 2 +- .../language/en-GB/plg_webservices_mokoog.ini | 4 +- .../en-GB/plg_webservices_mokoog.sys.ini | 6 +- .../language/en-US/plg_webservices_mokoog.ini | 4 +- .../en-US/plg_webservices_mokoog.sys.ini | 6 +- .../plg_webservices_mokoog/mokoog.php | 2 +- .../plg_webservices_mokoog/mokoog.xml | 4 +- .../services/provider.php | 2 +- .../src/Extension/MokoOGWebServices.php | 4 +- src/pkg_mokoog.xml | 6 +- src/script.php | 2 +- updates.xml | 8 +- 73 files changed, 433 insertions(+), 104 deletions(-) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..998448a --- /dev/null +++ b/.gitattributes @@ -0,0 +1,62 @@ +# Auto detect text files and perform LF normalization +* text=auto + +# PHP files +*.php text eol=lf + +# XML manifests +*.xml text eol=lf + +# Language files +*.ini text eol=lf + +# SQL files +*.sql text eol=lf + +# Shell scripts +*.sh text eol=lf + +# Markdown +*.md text eol=lf + +# YAML +*.yml text eol=lf +*.yaml text eol=lf + +# CSS/JS +*.css text eol=lf +*.js text eol=lf + +# JSON +*.json text eol=lf + +# Windows scripts +*.bat text eol=crlf +*.cmd text eol=crlf +*.ps1 text eol=crlf + +# Binary files +*.zip binary +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.ico binary +*.webp binary +*.woff binary +*.woff2 binary +*.ttf binary +*.eot binary + +# Export ignore (not included in archives) +.mokogitea/ export-ignore +.editorconfig export-ignore +.gitattributes export-ignore +.gitignore export-ignore +.gitmessage export-ignore +CLAUDE.md export-ignore +CONTRIBUTING.md export-ignore +CODE_OF_CONDUCT.md export-ignore +Makefile export-ignore +composer.json export-ignore +phpstan.neon export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..726a684 --- /dev/null +++ b/.gitignore @@ -0,0 +1,204 @@ +# ============================================================ +# Local task tracking (not version controlled) +# ============================================================ +TODO.md + +# ============================================================ +# Environment and secrets +# ============================================================ +.env +.env.local +.env.*.local +*.local.php +*.secret.php +configuration.php +configuration.*.php +configuration.local.php +conf/conf.php +conf/conf*.php +secrets/ +*.secrets.* + +# ============================================================ +# Logs, dumps and databases +# ============================================================ +*.db +*.db-journal +*.dump +*.log +*.pid +*.seed + + +# ============================================================ +# OS / Editor / IDE cruft +# ============================================================ +.DS_Store +Thumbs.db +desktop.ini +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db +$RECYCLE.BIN/ +System Volume Information/ +*.lnk +Icon? +.idea/ +.settings/ +.claude/ +.vscode/* +!.vscode/tasks.json +!.vscode/settings.json.example +!.vscode/extensions.json +*.code-workspace +*.sublime* +.project +.buildpath +.classpath +*.bak +*.swp +*.swo +*.tmp +*.old +*.orig + +# ============================================================ +# Dev scripts and scratch +# ============================================================ +TODO.md +todo* +*ffs* + +# ============================================================ +# SFTP / sync tools +# ============================================================ +sftp-config*.json +sftp-config.json.template +sftp-settings.json + +# ============================================================ +# Sublime SFTP / FTP sync +# ============================================================ +*.sublime-project +*.sublime-workspace +*.sublime-settings +.libsass.json +*.ffs* + +# ============================================================ +# Replit / cloud IDE +# ============================================================ +.replit +replit.md + +# ============================================================ +# Archives / release artifacts +# ============================================================ +*.7z +*.rar +*.tar +*.tar.gz +*.tgz +*.zip +artifacts/ +release/ +releases/ + +# ============================================================ +# Build outputs and site generators +# ============================================================ +.mkdocs-build/ +.cache/ +.parcel-cache/ +build/ +dist/ +out/ +site/ +*.map +*.css.map +*.js.map +*.tsbuildinfo + +# ============================================================ +# CI / test artifacts +# ============================================================ +.coverage +.coverage.* +coverage/ +coverage.xml +htmlcov/ +junit.xml +reports/ +test-results/ +tests/_output/ +.github/local/ +.github/workflows/*.log + +# ============================================================ +# Node / JavaScript +# ============================================================ +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +.pnpm-store/ +.yarn/ +.npmrc +.eslintcache +package-lock.json + +# ============================================================ +# PHP / Composer tooling +# ============================================================ +vendor/ +!src/media/vendor/ +composer.lock +*.phar +codeception.phar +.phpunit.result.cache +.php_cs.cache +.php-cs-fixer.cache +.phpstan.cache +.phplint-cache +phpmd-cache/ +.psalm/ +.rector/ + +# ============================================================ +# Python +# ============================================================ +__pycache__/ +*.py[cod] +*.pyc +*$py.class +*.so +.Python +.eggs/ +*.egg +*.egg-info/ +.installed.cfg +MANIFEST +develop-eggs/ +downloads/ +eggs/ +parts/ +sdist/ +var/ +wheels/ +ENV/ +env/ +.venv/ +venv/ +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.pyright/ +.tox/ +.nox/ +*.cover +*.coverage +hypothesis/ + +profile.ps1 +.mcp.json diff --git a/.mokogitea/manifest.xml b/.mokogitea/manifest.xml index 7404636..533e05a 100644 --- a/.mokogitea/manifest.xml +++ b/.mokogitea/manifest.xml @@ -5,7 +5,8 @@ --> <moko-platform xmlns="https://standards.mokoconsulting.tech/moko-platform/1.0" schema-version="1.0"> <identity> - <name>MokoOpenGraph</name> + <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> <license spdx="GPL-3.0-or-later">GNU General Public License v3</license> diff --git a/CHANGELOG.md b/CHANGELOG.md index 926a08c..472e290 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ <!-- VERSION: 01.00.00 --> -All notable changes to MokoOpenGraph will be documented in this file. +All notable changes to MokoJoomOpenGraph will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). diff --git a/CLAUDE.md b/CLAUDE.md index 1d6447f..cdaa011 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code when working with this repository. ## Project Overview -**MokoOpenGraph** -- Open Graph, Twitter Card, and social sharing meta tag management for Joomla +**MokoJoomOpenGraph** -- Open Graph, Twitter Card, and social sharing meta tag management for Joomla | Field | Value | |---|---| @@ -12,7 +12,7 @@ This file provides guidance to Claude Code when working with this repository. | **Language** | PHP | | **Default branch** | main | | **License** | GPL-3.0-or-later | -| **Wiki** | [MokoOpenGraph Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoOpenGraph/wiki) | +| **Wiki** | [MokoJoomOpenGraph Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/wiki) | | **Standards** | [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home) | ## Common Commands diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..2d63a38 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,28 @@ +# Code of Conduct + +## Our Pledge + +We pledge to make participation in our project a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to a positive environment: + +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community + +Examples of unacceptable behavior: + +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information without explicit permission + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the project team at hello@mokoconsulting.tech. All complaints will be reviewed and investigated. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), version 2.1. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..e566bdf --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,34 @@ +# Contributing to MokoJoomOpenGraph + +Thank you for your interest in contributing to MokoJoomOpenGraph. + +## Getting Started + +1. Fork the repository on Gitea +2. Create a feature branch from `dev` (`feature/your-feature`) +3. Make your changes following the coding standards below +4. Submit a pull request targeting `dev` + +## Branch Strategy + +- `main` — stable releases only +- `dev` — active development +- `feature/*` — new features (target `dev`) +- `fix/*` — bug fixes (target `dev`) +- `hotfix/*` — urgent fixes (target `dev` or `main`) + +## Coding Standards + +- PHP 8.1+ required +- Follow Joomla coding standards +- SPDX license headers on all PHP files +- Use `SubscriberInterface` for event subscription +- Use `bind() -> check() -> store()` for Table operations + +## Reporting Issues + +Report bugs and feature requests via [Issues](https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/issues). + +## License + +By contributing, you agree that your contributions will be licensed under GPL-3.0-or-later. diff --git a/Makefile b/Makefile index 67abb20..43340a6 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ # Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> # SPDX-License-Identifier: GPL-3.0-or-later # -# MokoOpenGraph — Open Graph & social sharing meta tag management +# MokoJoomOpenGraph — Open Graph & social sharing meta tag management # ============================================================================== # CONFIGURATION - Customize these for your extension diff --git a/README.md b/README.md index 89b9bda..85f5e7e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# MokoOpenGraph +# MokoJoomOpenGraph <!-- VERSION: 01.00.00 --> @@ -6,7 +6,7 @@ Open Graph, Twitter Card, and social sharing meta tag management for Joomla 4/5/ ## Overview -MokoOpenGraph gives you full control over how your Joomla content appears when shared on Facebook, Twitter/X, LinkedIn, WhatsApp, and other social platforms. Set custom titles, descriptions, and images per article and menu item — or let the extension auto-generate them from your existing content. +MokoJoomOpenGraph gives you full control over how your Joomla content appears when shared on Facebook, Twitter/X, LinkedIn, WhatsApp, and other social platforms. Set custom titles, descriptions, and images per article and menu item — or let the extension auto-generate them from your existing content. ## Features @@ -22,13 +22,13 @@ MokoOpenGraph gives you full control over how your Joomla content appears when s ## Installation -1. Download the latest `pkg_mokoog-*.zip` from [Releases](https://git.mokoconsulting.tech/MokoConsulting/MokoOpenGraph/releases) +1. Download the latest `pkg_mokoog-*.zip` from [Releases](https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/releases) 2. In Joomla Administrator → Extensions → Install → Upload Package File 3. The system plugin is enabled automatically on install ## Configuration -Navigate to **Extensions → Plugins → System - MokoOpenGraph** to configure: +Navigate to **Extensions → Plugins → System - MokoJoomOpenGraph** to configure: - Site name override - Default fallback image - Twitter Card type and @username diff --git a/src/language/en-GB/pkg_mokoog.sys.ini b/src/language/en-GB/pkg_mokoog.sys.ini index 76e8e3d..47aa587 100644 --- a/src/language/en-GB/pkg_mokoog.sys.ini +++ b/src/language/en-GB/pkg_mokoog.sys.ini @@ -1,7 +1,7 @@ -; MokoOpenGraph - Package System Language File +; MokoJoomOpenGraph - Package System Language File ; Copyright (C) 2026 Moko Consulting. All rights reserved. ; License: GPL-3.0-or-later -PKG_MOKOOG="MokoOpenGraph" +PKG_MOKOOG="MokoJoomOpenGraph" PKG_MOKOOG_DESCRIPTION="Complete Open Graph, Twitter Card, and social sharing meta tag management for Joomla. Control how every page appears when shared on Facebook, Twitter/X, LinkedIn, WhatsApp, and more." -PKG_MOKOOG_PHP_VERSION_ERROR="MokoOpenGraph requires PHP %s or later." +PKG_MOKOOG_PHP_VERSION_ERROR="MokoJoomOpenGraph requires PHP %s or later." diff --git a/src/language/en-US/pkg_mokoog.sys.ini b/src/language/en-US/pkg_mokoog.sys.ini index 76e8e3d..47aa587 100644 --- a/src/language/en-US/pkg_mokoog.sys.ini +++ b/src/language/en-US/pkg_mokoog.sys.ini @@ -1,7 +1,7 @@ -; MokoOpenGraph - Package System Language File +; MokoJoomOpenGraph - Package System Language File ; Copyright (C) 2026 Moko Consulting. All rights reserved. ; License: GPL-3.0-or-later -PKG_MOKOOG="MokoOpenGraph" +PKG_MOKOOG="MokoJoomOpenGraph" PKG_MOKOOG_DESCRIPTION="Complete Open Graph, Twitter Card, and social sharing meta tag management for Joomla. Control how every page appears when shared on Facebook, Twitter/X, LinkedIn, WhatsApp, and more." -PKG_MOKOOG_PHP_VERSION_ERROR="MokoOpenGraph requires PHP %s or later." +PKG_MOKOOG_PHP_VERSION_ERROR="MokoJoomOpenGraph requires PHP %s or later." diff --git a/src/packages/com_mokoog/api/src/Controller/TagsController.php b/src/packages/com_mokoog/api/src/Controller/TagsController.php index 28457f4..9fbe168 100644 --- a/src/packages/com_mokoog/api/src/Controller/TagsController.php +++ b/src/packages/com_mokoog/api/src/Controller/TagsController.php @@ -1,7 +1,7 @@ <?php /** - * @package MokoOpenGraph + * @package MokoJoomOpenGraph * @subpackage com_mokoog.api * @author Moko Consulting <hello@mokoconsulting.tech> * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. diff --git a/src/packages/com_mokoog/api/src/View/Tags/JsonapiView.php b/src/packages/com_mokoog/api/src/View/Tags/JsonapiView.php index c103df2..97a0aa2 100644 --- a/src/packages/com_mokoog/api/src/View/Tags/JsonapiView.php +++ b/src/packages/com_mokoog/api/src/View/Tags/JsonapiView.php @@ -1,7 +1,7 @@ <?php /** - * @package MokoOpenGraph + * @package MokoJoomOpenGraph * @subpackage com_mokoog.api * @author Moko Consulting <hello@mokoconsulting.tech> * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. diff --git a/src/packages/com_mokoog/forms/filter_tags.xml b/src/packages/com_mokoog/forms/filter_tags.xml index d7e87b4..b4d4015 100644 --- a/src/packages/com_mokoog/forms/filter_tags.xml +++ b/src/packages/com_mokoog/forms/filter_tags.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?> <!-- - * @package MokoOpenGraph + * @package MokoJoomOpenGraph * @subpackage com_mokoog * @author Moko Consulting <hello@mokoconsulting.tech> * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. diff --git a/src/packages/com_mokoog/forms/tag.xml b/src/packages/com_mokoog/forms/tag.xml index 3c1ab5c..87d8068 100644 --- a/src/packages/com_mokoog/forms/tag.xml +++ b/src/packages/com_mokoog/forms/tag.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?> <!-- - * @package MokoOpenGraph + * @package MokoJoomOpenGraph * @subpackage com_mokoog * @author Moko Consulting <hello@mokoconsulting.tech> * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. diff --git a/src/packages/com_mokoog/language/en-GB/com_mokoog.ini b/src/packages/com_mokoog/language/en-GB/com_mokoog.ini index c7dca09..4a5fef3 100644 --- a/src/packages/com_mokoog/language/en-GB/com_mokoog.ini +++ b/src/packages/com_mokoog/language/en-GB/com_mokoog.ini @@ -1,9 +1,9 @@ -; MokoOpenGraph - Component Language File +; MokoJoomOpenGraph - Component Language File ; Copyright (C) 2026 Moko Consulting. All rights reserved. ; License: GPL-3.0-or-later -COM_MOKOOG="MokoOpenGraph" -COM_MOKOOG_TAGS_TITLE="MokoOpenGraph - Tag Manager" +COM_MOKOOG="MokoJoomOpenGraph" +COM_MOKOOG_TAGS_TITLE="MokoJoomOpenGraph - Tag Manager" COM_MOKOOG_SUBMENU_TAGS="Tags" COM_MOKOOG_NO_TAGS="No Open Graph tags have been created yet. Tags are created automatically when you edit articles or menu items." COM_MOKOOG_TABLE_CAPTION="Table of Open Graph tags" diff --git a/src/packages/com_mokoog/language/en-GB/com_mokoog.sys.ini b/src/packages/com_mokoog/language/en-GB/com_mokoog.sys.ini index be6c090..0aacbc4 100644 --- a/src/packages/com_mokoog/language/en-GB/com_mokoog.sys.ini +++ b/src/packages/com_mokoog/language/en-GB/com_mokoog.sys.ini @@ -1,6 +1,6 @@ -; MokoOpenGraph - Component System Language File +; MokoJoomOpenGraph - Component System Language File ; Copyright (C) 2026 Moko Consulting. All rights reserved. ; License: GPL-3.0-or-later -COM_MOKOOG="MokoOpenGraph" +COM_MOKOOG="MokoJoomOpenGraph" COM_MOKOOG_DESCRIPTION="Manage Open Graph and social sharing tags for all your content. View, edit, and batch-process OG metadata." diff --git a/src/packages/com_mokoog/language/en-US/com_mokoog.ini b/src/packages/com_mokoog/language/en-US/com_mokoog.ini index c7dca09..4a5fef3 100644 --- a/src/packages/com_mokoog/language/en-US/com_mokoog.ini +++ b/src/packages/com_mokoog/language/en-US/com_mokoog.ini @@ -1,9 +1,9 @@ -; MokoOpenGraph - Component Language File +; MokoJoomOpenGraph - Component Language File ; Copyright (C) 2026 Moko Consulting. All rights reserved. ; License: GPL-3.0-or-later -COM_MOKOOG="MokoOpenGraph" -COM_MOKOOG_TAGS_TITLE="MokoOpenGraph - Tag Manager" +COM_MOKOOG="MokoJoomOpenGraph" +COM_MOKOOG_TAGS_TITLE="MokoJoomOpenGraph - Tag Manager" COM_MOKOOG_SUBMENU_TAGS="Tags" COM_MOKOOG_NO_TAGS="No Open Graph tags have been created yet. Tags are created automatically when you edit articles or menu items." COM_MOKOOG_TABLE_CAPTION="Table of Open Graph tags" diff --git a/src/packages/com_mokoog/language/en-US/com_mokoog.sys.ini b/src/packages/com_mokoog/language/en-US/com_mokoog.sys.ini index be6c090..0aacbc4 100644 --- a/src/packages/com_mokoog/language/en-US/com_mokoog.sys.ini +++ b/src/packages/com_mokoog/language/en-US/com_mokoog.sys.ini @@ -1,6 +1,6 @@ -; MokoOpenGraph - Component System Language File +; MokoJoomOpenGraph - Component System Language File ; Copyright (C) 2026 Moko Consulting. All rights reserved. ; License: GPL-3.0-or-later -COM_MOKOOG="MokoOpenGraph" +COM_MOKOOG="MokoJoomOpenGraph" COM_MOKOOG_DESCRIPTION="Manage Open Graph and social sharing tags for all your content. View, edit, and batch-process OG metadata." diff --git a/src/packages/com_mokoog/mokoog.xml b/src/packages/com_mokoog/mokoog.xml index 619dcaf..507be58 100644 --- a/src/packages/com_mokoog/mokoog.xml +++ b/src/packages/com_mokoog/mokoog.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?> <!-- - * @package MokoOpenGraph + * @package MokoJoomOpenGraph * @subpackage com_mokoog * @author Moko Consulting <hello@mokoconsulting.tech> * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. diff --git a/src/packages/com_mokoog/script.php b/src/packages/com_mokoog/script.php index 1dc0a47..5073df5 100644 --- a/src/packages/com_mokoog/script.php +++ b/src/packages/com_mokoog/script.php @@ -1,7 +1,7 @@ <?php /** - * @package MokoOpenGraph + * @package MokoJoomOpenGraph * @subpackage com_mokoog * @author Moko Consulting <hello@mokoconsulting.tech> * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. @@ -23,7 +23,7 @@ class Com_MokoOGInstallerScript */ public function install(InstallerAdapter $parent): void { - echo '<p>MokoOpenGraph component installed successfully.</p>'; + echo '<p>MokoJoomOpenGraph component installed successfully.</p>'; } /** @@ -35,6 +35,6 @@ class Com_MokoOGInstallerScript */ public function update(InstallerAdapter $parent): void { - echo '<p>MokoOpenGraph component updated successfully.</p>'; + echo '<p>MokoJoomOpenGraph component updated successfully.</p>'; } } diff --git a/src/packages/com_mokoog/services/provider.php b/src/packages/com_mokoog/services/provider.php index 249c8a1..27c3354 100644 --- a/src/packages/com_mokoog/services/provider.php +++ b/src/packages/com_mokoog/services/provider.php @@ -1,7 +1,7 @@ <?php /** - * @package MokoOpenGraph + * @package MokoJoomOpenGraph * @subpackage com_mokoog * @author Moko Consulting <hello@mokoconsulting.tech> * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. diff --git a/src/packages/com_mokoog/sql/install.mysql.sql b/src/packages/com_mokoog/sql/install.mysql.sql index cd91ceb..cc3558a 100644 --- a/src/packages/com_mokoog/sql/install.mysql.sql +++ b/src/packages/com_mokoog/sql/install.mysql.sql @@ -1,5 +1,5 @@ -- --- MokoOpenGraph - Database Schema +-- MokoJoomOpenGraph - Database Schema -- Copyright (C) 2026 Moko Consulting. All rights reserved. -- License: GPL-3.0-or-later -- diff --git a/src/packages/com_mokoog/sql/uninstall.mysql.sql b/src/packages/com_mokoog/sql/uninstall.mysql.sql index ab50f15..8652ac6 100644 --- a/src/packages/com_mokoog/sql/uninstall.mysql.sql +++ b/src/packages/com_mokoog/sql/uninstall.mysql.sql @@ -1,5 +1,5 @@ -- --- MokoOpenGraph - Uninstall +-- MokoJoomOpenGraph - Uninstall -- DROP TABLE IF EXISTS `#__mokoog_tags`; diff --git a/src/packages/com_mokoog/sql/updates/mysql/01.01.00.sql b/src/packages/com_mokoog/sql/updates/mysql/01.01.00.sql index 4f4c433..5bac763 100644 --- a/src/packages/com_mokoog/sql/updates/mysql/01.01.00.sql +++ b/src/packages/com_mokoog/sql/updates/mysql/01.01.00.sql @@ -1,5 +1,5 @@ -- --- MokoOpenGraph 01.01.00 — Add SEO meta management columns +-- MokoJoomOpenGraph 01.01.00 — Add SEO meta management columns -- ALTER TABLE `#__mokoog_tags` diff --git a/src/packages/com_mokoog/sql/updates/mysql/01.02.00.sql b/src/packages/com_mokoog/sql/updates/mysql/01.02.00.sql index 2655270..ca87208 100644 --- a/src/packages/com_mokoog/sql/updates/mysql/01.02.00.sql +++ b/src/packages/com_mokoog/sql/updates/mysql/01.02.00.sql @@ -1,5 +1,5 @@ -- --- MokoOpenGraph 01.02.00 — Add multilingual OG tag support +-- MokoJoomOpenGraph 01.02.00 — Add multilingual OG tag support -- ALTER TABLE `#__mokoog_tags` diff --git a/src/packages/com_mokoog/src/ContentType/ContentTypeInterface.php b/src/packages/com_mokoog/src/ContentType/ContentTypeInterface.php index c596ca0..6689aad 100644 --- a/src/packages/com_mokoog/src/ContentType/ContentTypeInterface.php +++ b/src/packages/com_mokoog/src/ContentType/ContentTypeInterface.php @@ -1,7 +1,7 @@ <?php /** - * @package MokoOpenGraph + * @package MokoJoomOpenGraph * @subpackage com_mokoog * @author Moko Consulting <hello@mokoconsulting.tech> * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. diff --git a/src/packages/com_mokoog/src/ContentType/HikaShopAdapter.php b/src/packages/com_mokoog/src/ContentType/HikaShopAdapter.php index 595f6e4..07150b3 100644 --- a/src/packages/com_mokoog/src/ContentType/HikaShopAdapter.php +++ b/src/packages/com_mokoog/src/ContentType/HikaShopAdapter.php @@ -1,7 +1,7 @@ <?php /** - * @package MokoOpenGraph + * @package MokoJoomOpenGraph * @subpackage com_mokoog * @author Moko Consulting <hello@mokoconsulting.tech> * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. diff --git a/src/packages/com_mokoog/src/ContentType/K2Adapter.php b/src/packages/com_mokoog/src/ContentType/K2Adapter.php index f8dcd5d..ec3f017 100644 --- a/src/packages/com_mokoog/src/ContentType/K2Adapter.php +++ b/src/packages/com_mokoog/src/ContentType/K2Adapter.php @@ -1,7 +1,7 @@ <?php /** - * @package MokoOpenGraph + * @package MokoJoomOpenGraph * @subpackage com_mokoog * @author Moko Consulting <hello@mokoconsulting.tech> * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. diff --git a/src/packages/com_mokoog/src/ContentType/VirtueMartAdapter.php b/src/packages/com_mokoog/src/ContentType/VirtueMartAdapter.php index 60822ec..d68a03d 100644 --- a/src/packages/com_mokoog/src/ContentType/VirtueMartAdapter.php +++ b/src/packages/com_mokoog/src/ContentType/VirtueMartAdapter.php @@ -1,7 +1,7 @@ <?php /** - * @package MokoOpenGraph + * @package MokoJoomOpenGraph * @subpackage com_mokoog * @author Moko Consulting <hello@mokoconsulting.tech> * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. diff --git a/src/packages/com_mokoog/src/Controller/BatchController.php b/src/packages/com_mokoog/src/Controller/BatchController.php index 909ef78..42cf3b4 100644 --- a/src/packages/com_mokoog/src/Controller/BatchController.php +++ b/src/packages/com_mokoog/src/Controller/BatchController.php @@ -1,7 +1,7 @@ <?php /** - * @package MokoOpenGraph + * @package MokoJoomOpenGraph * @subpackage com_mokoog * @author Moko Consulting <hello@mokoconsulting.tech> * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. diff --git a/src/packages/com_mokoog/src/Controller/DisplayController.php b/src/packages/com_mokoog/src/Controller/DisplayController.php index d65ffaf..28b73c9 100644 --- a/src/packages/com_mokoog/src/Controller/DisplayController.php +++ b/src/packages/com_mokoog/src/Controller/DisplayController.php @@ -1,7 +1,7 @@ <?php /** - * @package MokoOpenGraph + * @package MokoJoomOpenGraph * @subpackage com_mokoog * @author Moko Consulting <hello@mokoconsulting.tech> * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. diff --git a/src/packages/com_mokoog/src/Controller/ImportExportController.php b/src/packages/com_mokoog/src/Controller/ImportExportController.php index aec4584..95a8143 100644 --- a/src/packages/com_mokoog/src/Controller/ImportExportController.php +++ b/src/packages/com_mokoog/src/Controller/ImportExportController.php @@ -1,7 +1,7 @@ <?php /** - * @package MokoOpenGraph + * @package MokoJoomOpenGraph * @subpackage com_mokoog * @author Moko Consulting <hello@mokoconsulting.tech> * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. diff --git a/src/packages/com_mokoog/src/Extension/MokoOGComponent.php b/src/packages/com_mokoog/src/Extension/MokoOGComponent.php index 65307b3..f8e3a9e 100644 --- a/src/packages/com_mokoog/src/Extension/MokoOGComponent.php +++ b/src/packages/com_mokoog/src/Extension/MokoOGComponent.php @@ -1,7 +1,7 @@ <?php /** - * @package MokoOpenGraph + * @package MokoJoomOpenGraph * @subpackage com_mokoog * @author Moko Consulting <hello@mokoconsulting.tech> * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. diff --git a/src/packages/com_mokoog/src/Model/TagModel.php b/src/packages/com_mokoog/src/Model/TagModel.php index 62d14b9..c56b682 100644 --- a/src/packages/com_mokoog/src/Model/TagModel.php +++ b/src/packages/com_mokoog/src/Model/TagModel.php @@ -1,7 +1,7 @@ <?php /** - * @package MokoOpenGraph + * @package MokoJoomOpenGraph * @subpackage com_mokoog * @author Moko Consulting <hello@mokoconsulting.tech> * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. diff --git a/src/packages/com_mokoog/src/Model/TagsModel.php b/src/packages/com_mokoog/src/Model/TagsModel.php index 6294af5..4171c21 100644 --- a/src/packages/com_mokoog/src/Model/TagsModel.php +++ b/src/packages/com_mokoog/src/Model/TagsModel.php @@ -1,7 +1,7 @@ <?php /** - * @package MokoOpenGraph + * @package MokoJoomOpenGraph * @subpackage com_mokoog * @author Moko Consulting <hello@mokoconsulting.tech> * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. diff --git a/src/packages/com_mokoog/src/Table/TagTable.php b/src/packages/com_mokoog/src/Table/TagTable.php index 32817a2..29d68e4 100644 --- a/src/packages/com_mokoog/src/Table/TagTable.php +++ b/src/packages/com_mokoog/src/Table/TagTable.php @@ -1,7 +1,7 @@ <?php /** - * @package MokoOpenGraph + * @package MokoJoomOpenGraph * @subpackage com_mokoog * @author Moko Consulting <hello@mokoconsulting.tech> * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. diff --git a/src/packages/com_mokoog/src/View/Tags/HtmlView.php b/src/packages/com_mokoog/src/View/Tags/HtmlView.php index cf93aff..eca4100 100644 --- a/src/packages/com_mokoog/src/View/Tags/HtmlView.php +++ b/src/packages/com_mokoog/src/View/Tags/HtmlView.php @@ -1,7 +1,7 @@ <?php /** - * @package MokoOpenGraph + * @package MokoJoomOpenGraph * @subpackage com_mokoog * @author Moko Consulting <hello@mokoconsulting.tech> * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. diff --git a/src/packages/com_mokoog/tmpl/tags/default.php b/src/packages/com_mokoog/tmpl/tags/default.php index 094de26..0543271 100644 --- a/src/packages/com_mokoog/tmpl/tags/default.php +++ b/src/packages/com_mokoog/tmpl/tags/default.php @@ -1,7 +1,7 @@ <?php /** - * @package MokoOpenGraph + * @package MokoJoomOpenGraph * @subpackage com_mokoog * @author Moko Consulting <hello@mokoconsulting.tech> * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. diff --git a/src/packages/plg_content_mokoog/forms/mokoog.xml b/src/packages/plg_content_mokoog/forms/mokoog.xml index 9e5632e..90be310 100644 --- a/src/packages/plg_content_mokoog/forms/mokoog.xml +++ b/src/packages/plg_content_mokoog/forms/mokoog.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?> <!-- - * @package MokoOpenGraph + * @package MokoJoomOpenGraph * @subpackage plg_content_mokoog * @author Moko Consulting <hello@mokoconsulting.tech> * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. diff --git a/src/packages/plg_content_mokoog/language/en-GB/plg_content_mokoog.ini b/src/packages/plg_content_mokoog/language/en-GB/plg_content_mokoog.ini index 167fe7e..1da4c1e 100644 --- a/src/packages/plg_content_mokoog/language/en-GB/plg_content_mokoog.ini +++ b/src/packages/plg_content_mokoog/language/en-GB/plg_content_mokoog.ini @@ -1,4 +1,4 @@ -; MokoOpenGraph - Content Plugin Language File +; MokoJoomOpenGraph - Content Plugin Language File ; Copyright (C) 2026 Moko Consulting. All rights reserved. ; License: GPL-3.0-or-later diff --git a/src/packages/plg_content_mokoog/language/en-GB/plg_content_mokoog.sys.ini b/src/packages/plg_content_mokoog/language/en-GB/plg_content_mokoog.sys.ini index 418df7f..5d16a1b 100644 --- a/src/packages/plg_content_mokoog/language/en-GB/plg_content_mokoog.sys.ini +++ b/src/packages/plg_content_mokoog/language/en-GB/plg_content_mokoog.sys.ini @@ -1,6 +1,6 @@ -; MokoOpenGraph - Content Plugin System Language File +; MokoJoomOpenGraph - Content Plugin System Language File ; Copyright (C) 2026 Moko Consulting. All rights reserved. ; License: GPL-3.0-or-later -PLG_CONTENT_MOKOOG="Content - MokoOpenGraph" +PLG_CONTENT_MOKOOG="Content - MokoJoomOpenGraph" PLG_CONTENT_MOKOOG_DESCRIPTION="Adds Open Graph fields to article and menu item edit forms for per-page social sharing control." diff --git a/src/packages/plg_content_mokoog/language/en-US/plg_content_mokoog.ini b/src/packages/plg_content_mokoog/language/en-US/plg_content_mokoog.ini index 167fe7e..1da4c1e 100644 --- a/src/packages/plg_content_mokoog/language/en-US/plg_content_mokoog.ini +++ b/src/packages/plg_content_mokoog/language/en-US/plg_content_mokoog.ini @@ -1,4 +1,4 @@ -; MokoOpenGraph - Content Plugin Language File +; MokoJoomOpenGraph - Content Plugin Language File ; Copyright (C) 2026 Moko Consulting. All rights reserved. ; License: GPL-3.0-or-later diff --git a/src/packages/plg_content_mokoog/language/en-US/plg_content_mokoog.sys.ini b/src/packages/plg_content_mokoog/language/en-US/plg_content_mokoog.sys.ini index 418df7f..5d16a1b 100644 --- a/src/packages/plg_content_mokoog/language/en-US/plg_content_mokoog.sys.ini +++ b/src/packages/plg_content_mokoog/language/en-US/plg_content_mokoog.sys.ini @@ -1,6 +1,6 @@ -; MokoOpenGraph - Content Plugin System Language File +; MokoJoomOpenGraph - Content Plugin System Language File ; Copyright (C) 2026 Moko Consulting. All rights reserved. ; License: GPL-3.0-or-later -PLG_CONTENT_MOKOOG="Content - MokoOpenGraph" +PLG_CONTENT_MOKOOG="Content - MokoJoomOpenGraph" PLG_CONTENT_MOKOOG_DESCRIPTION="Adds Open Graph fields to article and menu item edit forms for per-page social sharing control." diff --git a/src/packages/plg_content_mokoog/media/css/preview.css b/src/packages/plg_content_mokoog/media/css/preview.css index b5d74a9..0ad41bb 100644 --- a/src/packages/plg_content_mokoog/media/css/preview.css +++ b/src/packages/plg_content_mokoog/media/css/preview.css @@ -1,5 +1,5 @@ /** - * @package MokoOpenGraph + * @package MokoJoomOpenGraph * @subpackage plg_content_mokoog * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GPL-3.0-or-later diff --git a/src/packages/plg_content_mokoog/media/joomla.asset.json b/src/packages/plg_content_mokoog/media/joomla.asset.json index 340aa7a..626a0fc 100644 --- a/src/packages/plg_content_mokoog/media/joomla.asset.json +++ b/src/packages/plg_content_mokoog/media/joomla.asset.json @@ -2,7 +2,7 @@ "$schema": "https://developer.joomla.org/schemas/json-schema/web_assets.json", "name": "plg_content_mokoog", "version": "01.00.00", - "description": "MokoOpenGraph Content Plugin Assets", + "description": "MokoJoomOpenGraph Content Plugin Assets", "license": "GPL-3.0-or-later", "assets": [ { diff --git a/src/packages/plg_content_mokoog/media/js/preview.js b/src/packages/plg_content_mokoog/media/js/preview.js index 62e8925..a10c3f1 100644 --- a/src/packages/plg_content_mokoog/media/js/preview.js +++ b/src/packages/plg_content_mokoog/media/js/preview.js @@ -1,5 +1,5 @@ /** - * @package MokoOpenGraph + * @package MokoJoomOpenGraph * @subpackage plg_content_mokoog * @author Moko Consulting <hello@mokoconsulting.tech> * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. diff --git a/src/packages/plg_content_mokoog/mokoog.php b/src/packages/plg_content_mokoog/mokoog.php index 4800146..0f81e14 100644 --- a/src/packages/plg_content_mokoog/mokoog.php +++ b/src/packages/plg_content_mokoog/mokoog.php @@ -1,7 +1,7 @@ <?php /** - * @package MokoOpenGraph + * @package MokoJoomOpenGraph * @subpackage plg_content_mokoog * @author Moko Consulting <hello@mokoconsulting.tech> * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. diff --git a/src/packages/plg_content_mokoog/mokoog.xml b/src/packages/plg_content_mokoog/mokoog.xml index 3fc2e68..a958a75 100644 --- a/src/packages/plg_content_mokoog/mokoog.xml +++ b/src/packages/plg_content_mokoog/mokoog.xml @@ -1,13 +1,13 @@ <?xml version="1.0" encoding="UTF-8"?> <!-- - * @package MokoOpenGraph + * @package MokoJoomOpenGraph * @subpackage plg_content_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 --> <extension type="plugin" group="content" method="upgrade"> - <name>Content - MokoOpenGraph</name> + <name>Content - MokoJoomOpenGraph</name> <version>01.00.00-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> diff --git a/src/packages/plg_content_mokoog/services/provider.php b/src/packages/plg_content_mokoog/services/provider.php index d6f7e90..aca7a7d 100644 --- a/src/packages/plg_content_mokoog/services/provider.php +++ b/src/packages/plg_content_mokoog/services/provider.php @@ -1,7 +1,7 @@ <?php /** - * @package MokoOpenGraph + * @package MokoJoomOpenGraph * @subpackage plg_content_mokoog * @author Moko Consulting <hello@mokoconsulting.tech> * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. diff --git a/src/packages/plg_content_mokoog/src/Extension/MokoOGContent.php b/src/packages/plg_content_mokoog/src/Extension/MokoOGContent.php index d89afb4..7a485f3 100644 --- a/src/packages/plg_content_mokoog/src/Extension/MokoOGContent.php +++ b/src/packages/plg_content_mokoog/src/Extension/MokoOGContent.php @@ -1,7 +1,7 @@ <?php /** - * @package MokoOpenGraph + * @package MokoJoomOpenGraph * @subpackage plg_content_mokoog * @author Moko Consulting <hello@mokoconsulting.tech> * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. diff --git a/src/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.ini b/src/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.ini index 987bbc1..34f17fd 100644 --- a/src/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.ini +++ b/src/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.ini @@ -1,4 +1,4 @@ -; MokoOpenGraph - System Plugin Language File +; MokoJoomOpenGraph - System Plugin Language File ; Copyright (C) 2026 Moko Consulting. All rights reserved. ; License: GPL-3.0-or-later diff --git a/src/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.sys.ini b/src/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.sys.ini index 51efd5f..2a356e2 100644 --- a/src/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.sys.ini +++ b/src/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.sys.ini @@ -1,6 +1,6 @@ -; MokoOpenGraph - System Plugin System Language File +; MokoJoomOpenGraph - System Plugin System Language File ; Copyright (C) 2026 Moko Consulting. All rights reserved. ; License: GPL-3.0-or-later -PLG_SYSTEM_MOKOOG="System - MokoOpenGraph" +PLG_SYSTEM_MOKOOG="System - MokoJoomOpenGraph" PLG_SYSTEM_MOKOOG_DESCRIPTION="Injects Open Graph and Twitter Card meta tags into every page for optimal social media sharing previews." diff --git a/src/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.ini b/src/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.ini index 987bbc1..34f17fd 100644 --- a/src/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.ini +++ b/src/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.ini @@ -1,4 +1,4 @@ -; MokoOpenGraph - System Plugin Language File +; MokoJoomOpenGraph - System Plugin Language File ; Copyright (C) 2026 Moko Consulting. All rights reserved. ; License: GPL-3.0-or-later diff --git a/src/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.sys.ini b/src/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.sys.ini index 51efd5f..2a356e2 100644 --- a/src/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.sys.ini +++ b/src/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.sys.ini @@ -1,6 +1,6 @@ -; MokoOpenGraph - System Plugin System Language File +; MokoJoomOpenGraph - System Plugin System Language File ; Copyright (C) 2026 Moko Consulting. All rights reserved. ; License: GPL-3.0-or-later -PLG_SYSTEM_MOKOOG="System - MokoOpenGraph" +PLG_SYSTEM_MOKOOG="System - MokoJoomOpenGraph" PLG_SYSTEM_MOKOOG_DESCRIPTION="Injects Open Graph and Twitter Card meta tags into every page for optimal social media sharing previews." diff --git a/src/packages/plg_system_mokoog/mokoog.php b/src/packages/plg_system_mokoog/mokoog.php index 47d238c..bfc5577 100644 --- a/src/packages/plg_system_mokoog/mokoog.php +++ b/src/packages/plg_system_mokoog/mokoog.php @@ -1,7 +1,7 @@ <?php /** - * @package MokoOpenGraph + * @package MokoJoomOpenGraph * @subpackage plg_system_mokoog * @author Moko Consulting <hello@mokoconsulting.tech> * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. diff --git a/src/packages/plg_system_mokoog/mokoog.xml b/src/packages/plg_system_mokoog/mokoog.xml index e40429c..c5a0e17 100644 --- a/src/packages/plg_system_mokoog/mokoog.xml +++ b/src/packages/plg_system_mokoog/mokoog.xml @@ -1,13 +1,13 @@ <?xml version="1.0" encoding="UTF-8"?> <!-- - * @package MokoOpenGraph + * @package MokoJoomOpenGraph * @subpackage plg_system_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 --> <extension type="plugin" group="system" method="upgrade"> - <name>System - MokoOpenGraph</name> + <name>System - MokoJoomOpenGraph</name> <version>01.00.00-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> diff --git a/src/packages/plg_system_mokoog/services/provider.php b/src/packages/plg_system_mokoog/services/provider.php index 2650be2..390b1d3 100644 --- a/src/packages/plg_system_mokoog/services/provider.php +++ b/src/packages/plg_system_mokoog/services/provider.php @@ -1,7 +1,7 @@ <?php /** - * @package MokoOpenGraph + * @package MokoJoomOpenGraph * @subpackage plg_system_mokoog * @author Moko Consulting <hello@mokoconsulting.tech> * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. diff --git a/src/packages/plg_system_mokoog/src/Extension/MokoOG.php b/src/packages/plg_system_mokoog/src/Extension/MokoOG.php index 756fbdd..bf9268e 100644 --- a/src/packages/plg_system_mokoog/src/Extension/MokoOG.php +++ b/src/packages/plg_system_mokoog/src/Extension/MokoOG.php @@ -1,7 +1,7 @@ <?php /** - * @package MokoOpenGraph + * @package MokoJoomOpenGraph * @subpackage plg_system_mokoog * @author Moko Consulting <hello@mokoconsulting.tech> * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. diff --git a/src/packages/plg_system_mokoog/src/Helper/ImageGenerator.php b/src/packages/plg_system_mokoog/src/Helper/ImageGenerator.php index 2837cdc..ece760e 100644 --- a/src/packages/plg_system_mokoog/src/Helper/ImageGenerator.php +++ b/src/packages/plg_system_mokoog/src/Helper/ImageGenerator.php @@ -1,7 +1,7 @@ <?php /** - * @package MokoOpenGraph + * @package MokoJoomOpenGraph * @subpackage plg_system_mokoog * @author Moko Consulting <hello@mokoconsulting.tech> * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. diff --git a/src/packages/plg_system_mokoog/src/Helper/ImageHelper.php b/src/packages/plg_system_mokoog/src/Helper/ImageHelper.php index 8364f18..3ea0325 100644 --- a/src/packages/plg_system_mokoog/src/Helper/ImageHelper.php +++ b/src/packages/plg_system_mokoog/src/Helper/ImageHelper.php @@ -1,7 +1,7 @@ <?php /** - * @package MokoOpenGraph + * @package MokoJoomOpenGraph * @subpackage plg_system_mokoog * @author Moko Consulting <hello@mokoconsulting.tech> * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. diff --git a/src/packages/plg_system_mokoog/src/Helper/JsonLdBuilder.php b/src/packages/plg_system_mokoog/src/Helper/JsonLdBuilder.php index d56f029..98f06ec 100644 --- a/src/packages/plg_system_mokoog/src/Helper/JsonLdBuilder.php +++ b/src/packages/plg_system_mokoog/src/Helper/JsonLdBuilder.php @@ -1,7 +1,7 @@ <?php /** - * @package MokoOpenGraph + * @package MokoJoomOpenGraph * @subpackage plg_system_mokoog * @author Moko Consulting <hello@mokoconsulting.tech> * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. diff --git a/src/packages/plg_webservices_mokoog/language/en-GB/plg_webservices_mokoog.ini b/src/packages/plg_webservices_mokoog/language/en-GB/plg_webservices_mokoog.ini index e397923..97f99e3 100644 --- a/src/packages/plg_webservices_mokoog/language/en-GB/plg_webservices_mokoog.ini +++ b/src/packages/plg_webservices_mokoog/language/en-GB/plg_webservices_mokoog.ini @@ -1,5 +1,5 @@ -; MokoOpenGraph - Web Services Plugin Language File +; MokoJoomOpenGraph - Web Services Plugin Language File ; Copyright (C) 2026 Moko Consulting. All rights reserved. ; License: GPL-3.0-or-later -PLG_WEBSERVICES_MOKOOG="Web Services - MokoOpenGraph" +PLG_WEBSERVICES_MOKOOG="Web Services - MokoJoomOpenGraph" diff --git a/src/packages/plg_webservices_mokoog/language/en-GB/plg_webservices_mokoog.sys.ini b/src/packages/plg_webservices_mokoog/language/en-GB/plg_webservices_mokoog.sys.ini index 903c909..086ffe8 100644 --- a/src/packages/plg_webservices_mokoog/language/en-GB/plg_webservices_mokoog.sys.ini +++ b/src/packages/plg_webservices_mokoog/language/en-GB/plg_webservices_mokoog.sys.ini @@ -1,6 +1,6 @@ -; MokoOpenGraph - Web Services Plugin System Language File +; MokoJoomOpenGraph - Web Services Plugin System Language File ; Copyright (C) 2026 Moko Consulting. All rights reserved. ; License: GPL-3.0-or-later -PLG_WEBSERVICES_MOKOOG="Web Services - MokoOpenGraph" -PLG_WEBSERVICES_MOKOOG_DESCRIPTION="Exposes MokoOpenGraph OG tag data via Joomla's REST API at /api/index.php/v1/mokoog/tags." +PLG_WEBSERVICES_MOKOOG="Web Services - MokoJoomOpenGraph" +PLG_WEBSERVICES_MOKOOG_DESCRIPTION="Exposes MokoJoomOpenGraph OG tag data via Joomla's REST API at /api/index.php/v1/mokoog/tags." diff --git a/src/packages/plg_webservices_mokoog/language/en-US/plg_webservices_mokoog.ini b/src/packages/plg_webservices_mokoog/language/en-US/plg_webservices_mokoog.ini index e397923..97f99e3 100644 --- a/src/packages/plg_webservices_mokoog/language/en-US/plg_webservices_mokoog.ini +++ b/src/packages/plg_webservices_mokoog/language/en-US/plg_webservices_mokoog.ini @@ -1,5 +1,5 @@ -; MokoOpenGraph - Web Services Plugin Language File +; MokoJoomOpenGraph - Web Services Plugin Language File ; Copyright (C) 2026 Moko Consulting. All rights reserved. ; License: GPL-3.0-or-later -PLG_WEBSERVICES_MOKOOG="Web Services - MokoOpenGraph" +PLG_WEBSERVICES_MOKOOG="Web Services - MokoJoomOpenGraph" diff --git a/src/packages/plg_webservices_mokoog/language/en-US/plg_webservices_mokoog.sys.ini b/src/packages/plg_webservices_mokoog/language/en-US/plg_webservices_mokoog.sys.ini index 903c909..086ffe8 100644 --- a/src/packages/plg_webservices_mokoog/language/en-US/plg_webservices_mokoog.sys.ini +++ b/src/packages/plg_webservices_mokoog/language/en-US/plg_webservices_mokoog.sys.ini @@ -1,6 +1,6 @@ -; MokoOpenGraph - Web Services Plugin System Language File +; MokoJoomOpenGraph - Web Services Plugin System Language File ; Copyright (C) 2026 Moko Consulting. All rights reserved. ; License: GPL-3.0-or-later -PLG_WEBSERVICES_MOKOOG="Web Services - MokoOpenGraph" -PLG_WEBSERVICES_MOKOOG_DESCRIPTION="Exposes MokoOpenGraph OG tag data via Joomla's REST API at /api/index.php/v1/mokoog/tags." +PLG_WEBSERVICES_MOKOOG="Web Services - MokoJoomOpenGraph" +PLG_WEBSERVICES_MOKOOG_DESCRIPTION="Exposes MokoJoomOpenGraph OG tag data via Joomla's REST API at /api/index.php/v1/mokoog/tags." diff --git a/src/packages/plg_webservices_mokoog/mokoog.php b/src/packages/plg_webservices_mokoog/mokoog.php index 899de00..945c592 100644 --- a/src/packages/plg_webservices_mokoog/mokoog.php +++ b/src/packages/plg_webservices_mokoog/mokoog.php @@ -1,7 +1,7 @@ <?php /** - * @package MokoOpenGraph + * @package MokoJoomOpenGraph * @subpackage plg_webservices_mokoog * @author Moko Consulting <hello@mokoconsulting.tech> * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. diff --git a/src/packages/plg_webservices_mokoog/mokoog.xml b/src/packages/plg_webservices_mokoog/mokoog.xml index af192f8..be15f80 100644 --- a/src/packages/plg_webservices_mokoog/mokoog.xml +++ b/src/packages/plg_webservices_mokoog/mokoog.xml @@ -1,13 +1,13 @@ <?xml version="1.0" encoding="UTF-8"?> <!-- - * @package MokoOpenGraph + * @package MokoJoomOpenGraph * @subpackage plg_webservices_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 --> <extension type="plugin" group="webservices" method="upgrade"> - <name>Web Services - MokoOpenGraph</name> + <name>Web Services - MokoJoomOpenGraph</name> <version>01.00.00-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> diff --git a/src/packages/plg_webservices_mokoog/services/provider.php b/src/packages/plg_webservices_mokoog/services/provider.php index 2aa760d..be36a42 100644 --- a/src/packages/plg_webservices_mokoog/services/provider.php +++ b/src/packages/plg_webservices_mokoog/services/provider.php @@ -1,7 +1,7 @@ <?php /** - * @package MokoOpenGraph + * @package MokoJoomOpenGraph * @subpackage plg_webservices_mokoog * @author Moko Consulting <hello@mokoconsulting.tech> * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. diff --git a/src/packages/plg_webservices_mokoog/src/Extension/MokoOGWebServices.php b/src/packages/plg_webservices_mokoog/src/Extension/MokoOGWebServices.php index 887e3b8..b5cc48f 100644 --- a/src/packages/plg_webservices_mokoog/src/Extension/MokoOGWebServices.php +++ b/src/packages/plg_webservices_mokoog/src/Extension/MokoOGWebServices.php @@ -1,7 +1,7 @@ <?php /** - * @package MokoOpenGraph + * @package MokoJoomOpenGraph * @subpackage plg_webservices_mokoog * @author Moko Consulting <hello@mokoconsulting.tech> * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. @@ -38,7 +38,7 @@ final class MokoOGWebServices extends CMSPlugin implements SubscriberInterface } /** - * Register API routes for MokoOpenGraph. + * Register API routes for MokoJoomOpenGraph. * * Endpoints: * GET /api/index.php/v1/mokoog/tags - List all OG tags diff --git a/src/pkg_mokoog.xml b/src/pkg_mokoog.xml index b45955d..757a56b 100644 --- a/src/pkg_mokoog.xml +++ b/src/pkg_mokoog.xml @@ -1,12 +1,12 @@ <?xml version="1.0" encoding="UTF-8"?> <!-- - * @package MokoOpenGraph + * @package MokoJoomOpenGraph * @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>MokoOpenGraph</name> + <name>Package - MokoJoomOpenGraph</name> <packagename>mokoog</packagename> <version>01.00.00-dev</version> <creationDate>2026-05-23</creationDate> @@ -31,6 +31,6 @@ </languages> <updateservers> - <server type="extension" name="MokoOpenGraph Updates">https://git.mokoconsulting.tech/MokoConsulting/MokoOpenGraph/raw/branch/main/updates.xml</server> + <server type="extension" name="MokoJoomOpenGraph Updates">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/raw/branch/main/updates.xml</server> </updateservers> </extension> diff --git a/src/script.php b/src/script.php index 3f76f33..d515031 100644 --- a/src/script.php +++ b/src/script.php @@ -1,7 +1,7 @@ <?php /** - * @package MokoOpenGraph + * @package MokoJoomOpenGraph * @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 diff --git a/updates.xml b/updates.xml index a7aa193..57e320d 100644 --- a/updates.xml +++ b/updates.xml @@ -6,15 +6,15 @@ <updates> <update> - <name>MokoOpenGraph</name> - <description>MokoOpenGraph update</description> + <name>MokoJoomOpenGraph</name> + <description>MokoJoomOpenGraph update</description> <element>pkg_mokoog</element> <type>package</type> <version>01.00.00-dev</version> <tags><tag>development</tag></tags> - <infourl title="MokoOpenGraph">https://git.mokoconsulting.tech/MokoConsulting/MokoOpenGraph/releases/tag/development</infourl> + <infourl title="MokoJoomOpenGraph">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/releases/tag/development</infourl> <downloads> - <downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoOpenGraph/releases/download/development/pkg_pkg_mokoog-01.00.00-dev.zip</downloadurl> + <downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/releases/download/development/pkg_pkg_mokoog-01.00.00-dev.zip</downloadurl> </downloads> <targetplatform name="joomla" version="((5.[0-9])|(6.[0-9]))" /> <maintainer>Moko Consulting</maintainer> From de9f7eeb58bad994af2cbf348942af9da3ab352d Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sat, 30 May 2026 20:37:50 -0500 Subject: [PATCH 066/132] fix(security): harden controllers, add site defaults, platform-specific OG tags Security fixes: - Fix JSON-LD XSS via </script> injection in content data (#34) - Add ACL permission checks to Batch and ImportExport controllers (#37) - Add CSV import file type, MIME, and size validation (#35) - Fix multilingual bug in content plugin load/save OG data (#41) Enhancements: - Add site-wide default OG title and description plugin parameters - Add Discord embed color (theme-color) plugin parameter - Add og:image:width/height for faster social previews - Add article:published_time, article:modified_time, article:author for LinkedIn - Add onMokoOGAfterRender event for third-party plugin extensibility - Add content_type regex validation on CSV import rows Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- .../com_mokoog/language/en-GB/com_mokoog.ini | 2 + .../com_mokoog/language/en-US/com_mokoog.ini | 2 + .../src/Controller/BatchController.php | 8 ++ .../src/Controller/ImportExportController.php | 59 +++++++++++- .../src/Extension/MokoOGContent.php | 47 ++++++++-- .../language/en-GB/plg_system_mokoog.ini | 6 ++ .../language/en-US/plg_system_mokoog.ini | 6 ++ src/packages/plg_system_mokoog/mokoog.xml | 24 +++++ .../src/Extension/MokoOG.php | 89 ++++++++++++++++++- .../src/Helper/JsonLdBuilder.php | 3 + 10 files changed, 233 insertions(+), 13 deletions(-) diff --git a/src/packages/com_mokoog/language/en-GB/com_mokoog.ini b/src/packages/com_mokoog/language/en-GB/com_mokoog.ini index 4a5fef3..f970f01 100644 --- a/src/packages/com_mokoog/language/en-GB/com_mokoog.ini +++ b/src/packages/com_mokoog/language/en-GB/com_mokoog.ini @@ -53,5 +53,7 @@ COM_MOKOOG_BATCH_ERROR="Error:" COM_MOKOOG_TOOLBAR_EXPORT="Export CSV" COM_MOKOOG_TOOLBAR_IMPORT="Import CSV" COM_MOKOOG_IMPORT_NO_FILE="No CSV file was uploaded." +COM_MOKOOG_IMPORT_INVALID_TYPE="Invalid file type. Please upload a .csv file." +COM_MOKOOG_IMPORT_FILE_TOO_LARGE="File is too large. Maximum allowed size is %s." COM_MOKOOG_IMPORT_READ_ERROR="Could not read the uploaded CSV file." COM_MOKOOG_IMPORT_RESULT="Import complete: %d created, %d updated, %d skipped." diff --git a/src/packages/com_mokoog/language/en-US/com_mokoog.ini b/src/packages/com_mokoog/language/en-US/com_mokoog.ini index 4a5fef3..f970f01 100644 --- a/src/packages/com_mokoog/language/en-US/com_mokoog.ini +++ b/src/packages/com_mokoog/language/en-US/com_mokoog.ini @@ -53,5 +53,7 @@ COM_MOKOOG_BATCH_ERROR="Error:" COM_MOKOOG_TOOLBAR_EXPORT="Export CSV" COM_MOKOOG_TOOLBAR_IMPORT="Import CSV" COM_MOKOOG_IMPORT_NO_FILE="No CSV file was uploaded." +COM_MOKOOG_IMPORT_INVALID_TYPE="Invalid file type. Please upload a .csv file." +COM_MOKOOG_IMPORT_FILE_TOO_LARGE="File is too large. Maximum allowed size is %s." COM_MOKOOG_IMPORT_READ_ERROR="Could not read the uploaded CSV file." COM_MOKOOG_IMPORT_RESULT="Import complete: %d created, %d updated, %d skipped." diff --git a/src/packages/com_mokoog/src/Controller/BatchController.php b/src/packages/com_mokoog/src/Controller/BatchController.php index 42cf3b4..ca9aaeb 100644 --- a/src/packages/com_mokoog/src/Controller/BatchController.php +++ b/src/packages/com_mokoog/src/Controller/BatchController.php @@ -29,6 +29,10 @@ class BatchController extends BaseController { Session::checkToken('get') || jexit(Text::_('JINVALID_TOKEN')); + if (!Factory::getApplication()->getIdentity()->authorise('core.create', 'com_mokoog')) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); + } + $db = Factory::getDbo(); $query = $db->getQuery(true) ->select('COUNT(*)') @@ -58,6 +62,10 @@ class BatchController extends BaseController { Session::checkToken('get') || jexit(Text::_('JINVALID_TOKEN')); + if (!Factory::getApplication()->getIdentity()->authorise('core.create', 'com_mokoog')) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); + } + $app = Factory::getApplication(); $offset = $app->getInput()->getInt('offset', 0); $limit = $app->getInput()->getInt('limit', 50); diff --git a/src/packages/com_mokoog/src/Controller/ImportExportController.php b/src/packages/com_mokoog/src/Controller/ImportExportController.php index 95a8143..b135575 100644 --- a/src/packages/com_mokoog/src/Controller/ImportExportController.php +++ b/src/packages/com_mokoog/src/Controller/ImportExportController.php @@ -19,6 +19,16 @@ use Joomla\CMS\Session\Session; class ImportExportController extends BaseController { + /** + * Maximum upload file size in bytes (2 MB). + */ + private const MAX_FILE_SIZE = 2 * 1024 * 1024; + + /** + * Allowed content_type patterns for import. + */ + private const CONTENT_TYPE_PATTERN = '/^[a-z][a-z0-9_.]*$/'; + /** * Export all OG tags as CSV. * @@ -28,6 +38,10 @@ class ImportExportController extends BaseController { Session::checkToken('get') || jexit(Text::_('JINVALID_TOKEN')); + if (!Factory::getApplication()->getIdentity()->authorise('core.manage', 'com_mokoog')) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); + } + $app = Factory::getApplication(); $db = Factory::getDbo(); @@ -88,6 +102,12 @@ class ImportExportController extends BaseController { Session::checkToken() || jexit(Text::_('JINVALID_TOKEN')); + $identity = Factory::getApplication()->getIdentity(); + + if (!$identity->authorise('core.create', 'com_mokoog') || !$identity->authorise('core.edit', 'com_mokoog')) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); + } + $app = Factory::getApplication(); $input = $app->getInput(); $files = $input->files->get('jform', [], 'array'); @@ -99,7 +119,37 @@ class ImportExportController extends BaseController return; } - $tmpFile = $files['csv_file']['tmp_name']; + $csvFile = $files['csv_file']; + + // Validate file extension + $ext = strtolower(pathinfo($csvFile['name'] ?? '', PATHINFO_EXTENSION)); + + if ($ext !== 'csv') { + $app->enqueueMessage(Text::_('COM_MOKOOG_IMPORT_INVALID_TYPE'), 'error'); + $app->redirect('index.php?option=com_mokoog&view=tags'); + + return; + } + + // Validate MIME type + $allowedMimes = ['text/csv', 'text/plain', 'application/csv', 'application/vnd.ms-excel']; + + if (!empty($csvFile['type']) && !\in_array($csvFile['type'], $allowedMimes, true)) { + $app->enqueueMessage(Text::_('COM_MOKOOG_IMPORT_INVALID_TYPE'), 'error'); + $app->redirect('index.php?option=com_mokoog&view=tags'); + + return; + } + + // Validate file size + if (($csvFile['size'] ?? 0) > self::MAX_FILE_SIZE) { + $app->enqueueMessage(Text::sprintf('COM_MOKOOG_IMPORT_FILE_TOO_LARGE', '2 MB'), 'error'); + $app->redirect('index.php?option=com_mokoog&view=tags'); + + return; + } + + $tmpFile = $csvFile['tmp_name']; $handle = fopen($tmpFile, 'r'); if (!$handle) { @@ -141,6 +191,13 @@ class ImportExportController extends BaseController continue; } + // Validate content_type against allowed pattern + if (!preg_match(self::CONTENT_TYPE_PATTERN, $contentType)) { + $skipped++; + + continue; + } + // Check for existing record $query = $db->getQuery(true) ->select($db->quoteName('id')) diff --git a/src/packages/plg_content_mokoog/src/Extension/MokoOGContent.php b/src/packages/plg_content_mokoog/src/Extension/MokoOGContent.php index 7a485f3..7bd1b9b 100644 --- a/src/packages/plg_content_mokoog/src/Extension/MokoOGContent.php +++ b/src/packages/plg_content_mokoog/src/Extension/MokoOGContent.php @@ -94,7 +94,8 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface 'com_categories.categorycom_content' => 'com_content.category', ]; $contentType = $formTypeMap[$formName] ?? 'com_content'; - $ogData = $this->loadOgData($contentType, $id); + $language = $this->getContentLanguage($data); + $ogData = $this->loadOgData($contentType, $id, $language); if ($ogData) { $form->bind(['mokoog' => (array) $ogData]); @@ -132,6 +133,7 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface $contentType = $supportedContexts[$context]; $contentId = (int) $article->id; + $language = $this->getContentLanguage($article); $input = $app->getInput(); $jform = $input->get('jform', [], 'array'); @@ -141,7 +143,7 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface return; } - $this->saveOgData($contentType, $contentId, $ogData); + $this->saveOgData($contentType, $contentId, $ogData, $language); } /** @@ -179,14 +181,15 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface } /** - * Load existing OG data for a content item. + * Load existing OG data for a content item, filtered by language. * * @param string $contentType Content type identifier * @param int $contentId Content ID + * @param string $language Language tag (e.g. 'en-GB') or '*' for all * * @return object|null */ - private function loadOgData(string $contentType, int $contentId): ?object + private function loadOgData(string $contentType, int $contentId, string $language = '*'): ?object { $db = Factory::getDbo(); $query = $db->getQuery(true) @@ -196,9 +199,12 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface ])) ->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) + . ' 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(); } @@ -209,19 +215,21 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface * @param string $contentType Content type identifier * @param int $contentId Content ID * @param array $ogData OG field values + * @param string $language Language tag (e.g. 'en-GB') or '*' for all * * @return void */ - private function saveOgData(string $contentType, int $contentId, array $ogData): void + private function saveOgData(string $contentType, int $contentId, array $ogData, string $language = '*'): void { $db = Factory::getDbo(); - // Check if record exists + // Check if record exists for this content + language $query = $db->getQuery(true) ->select('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(); @@ -236,6 +244,7 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface $record = (object) [ 'content_type' => $contentType, 'content_id' => $contentId, + 'language' => $language, 'og_title' => trim($ogData['og_title'] ?? ''), 'og_description' => trim($ogData['og_description'] ?? ''), 'og_image' => trim($ogData['og_image'] ?? ''), @@ -256,4 +265,24 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface $db->insertObject('#__mokoog_tags', $record); } } + + /** + * Extract the language tag from content data. + * + * @param object|array $data Content data from form or article object + * + * @return string Language tag (e.g. 'en-GB') or '*' for all languages + */ + private function getContentLanguage($data): string + { + $language = '*'; + + if (\is_object($data) && isset($data->language)) { + $language = $data->language; + } elseif (\is_array($data) && isset($data['language'])) { + $language = $data['language']; + } + + return !empty($language) ? $language : '*'; + } } diff --git a/src/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.ini b/src/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.ini index 34f17fd..3d1c0f6 100644 --- a/src/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.ini +++ b/src/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.ini @@ -7,6 +7,10 @@ PLG_SYSTEM_MOKOOG_FIELDSET_ADVANCED="Advanced Settings" PLG_SYSTEM_MOKOOG_FIELD_SITE_NAME="Site Name" PLG_SYSTEM_MOKOOG_FIELD_SITE_NAME_DESC="The og:site_name value. Leave blank to use the Joomla site name." +PLG_SYSTEM_MOKOOG_FIELD_DEFAULT_OG_TITLE="Default OG Title" +PLG_SYSTEM_MOKOOG_FIELD_DEFAULT_OG_TITLE_DESC="Site-wide fallback title for social sharing. Used when a page has no custom OG title. Leave blank to use the page title." +PLG_SYSTEM_MOKOOG_FIELD_DEFAULT_OG_DESCRIPTION="Default OG Description" +PLG_SYSTEM_MOKOOG_FIELD_DEFAULT_OG_DESCRIPTION_DESC="Site-wide fallback description for social sharing. Used when a page has no custom OG description and no meta description. Leave blank to auto-generate from page content." PLG_SYSTEM_MOKOOG_FIELD_DEFAULT_IMAGE="Default Image" PLG_SYSTEM_MOKOOG_FIELD_DEFAULT_IMAGE_DESC="Fallback image used when no article or page image is found." PLG_SYSTEM_MOKOOG_FIELD_TWITTER_CARD="Twitter Card Type" @@ -17,6 +21,8 @@ PLG_SYSTEM_MOKOOG_FIELD_TWITTER_SITE="Twitter @username" PLG_SYSTEM_MOKOOG_FIELD_TWITTER_SITE_DESC="Your site's Twitter handle (e.g. @mokoconsulting)." PLG_SYSTEM_MOKOOG_FIELD_FB_APP_ID="Facebook App ID" PLG_SYSTEM_MOKOOG_FIELD_FB_APP_ID_DESC="Your Facebook App ID for fb:app_id meta tag." +PLG_SYSTEM_MOKOOG_FIELD_DISCORD_COLOR="Discord Embed Color" +PLG_SYSTEM_MOKOOG_FIELD_DISCORD_COLOR_DESC="The color of the embed sidebar when shared on Discord. Sets the theme-color meta tag. Leave blank to use Discord defaults." PLG_SYSTEM_MOKOOG_FIELD_AUTO_GENERATE="Auto-generate Tags" PLG_SYSTEM_MOKOOG_FIELD_AUTO_GENERATE_DESC="Automatically generate OG tags from article content when no custom tags are set." PLG_SYSTEM_MOKOOG_FIELD_STRIP_HTML="Strip HTML from Description" diff --git a/src/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.ini b/src/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.ini index 34f17fd..3d1c0f6 100644 --- a/src/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.ini +++ b/src/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.ini @@ -7,6 +7,10 @@ PLG_SYSTEM_MOKOOG_FIELDSET_ADVANCED="Advanced Settings" PLG_SYSTEM_MOKOOG_FIELD_SITE_NAME="Site Name" PLG_SYSTEM_MOKOOG_FIELD_SITE_NAME_DESC="The og:site_name value. Leave blank to use the Joomla site name." +PLG_SYSTEM_MOKOOG_FIELD_DEFAULT_OG_TITLE="Default OG Title" +PLG_SYSTEM_MOKOOG_FIELD_DEFAULT_OG_TITLE_DESC="Site-wide fallback title for social sharing. Used when a page has no custom OG title. Leave blank to use the page title." +PLG_SYSTEM_MOKOOG_FIELD_DEFAULT_OG_DESCRIPTION="Default OG Description" +PLG_SYSTEM_MOKOOG_FIELD_DEFAULT_OG_DESCRIPTION_DESC="Site-wide fallback description for social sharing. Used when a page has no custom OG description and no meta description. Leave blank to auto-generate from page content." PLG_SYSTEM_MOKOOG_FIELD_DEFAULT_IMAGE="Default Image" PLG_SYSTEM_MOKOOG_FIELD_DEFAULT_IMAGE_DESC="Fallback image used when no article or page image is found." PLG_SYSTEM_MOKOOG_FIELD_TWITTER_CARD="Twitter Card Type" @@ -17,6 +21,8 @@ PLG_SYSTEM_MOKOOG_FIELD_TWITTER_SITE="Twitter @username" PLG_SYSTEM_MOKOOG_FIELD_TWITTER_SITE_DESC="Your site's Twitter handle (e.g. @mokoconsulting)." PLG_SYSTEM_MOKOOG_FIELD_FB_APP_ID="Facebook App ID" PLG_SYSTEM_MOKOOG_FIELD_FB_APP_ID_DESC="Your Facebook App ID for fb:app_id meta tag." +PLG_SYSTEM_MOKOOG_FIELD_DISCORD_COLOR="Discord Embed Color" +PLG_SYSTEM_MOKOOG_FIELD_DISCORD_COLOR_DESC="The color of the embed sidebar when shared on Discord. Sets the theme-color meta tag. Leave blank to use Discord defaults." PLG_SYSTEM_MOKOOG_FIELD_AUTO_GENERATE="Auto-generate Tags" PLG_SYSTEM_MOKOOG_FIELD_AUTO_GENERATE_DESC="Automatically generate OG tags from article content when no custom tags are set." PLG_SYSTEM_MOKOOG_FIELD_STRIP_HTML="Strip HTML from Description" diff --git a/src/packages/plg_system_mokoog/mokoog.xml b/src/packages/plg_system_mokoog/mokoog.xml index c5a0e17..231c803 100644 --- a/src/packages/plg_system_mokoog/mokoog.xml +++ b/src/packages/plg_system_mokoog/mokoog.xml @@ -41,6 +41,23 @@ description="PLG_SYSTEM_MOKOOG_FIELD_SITE_NAME_DESC" default="" /> + <field + name="default_og_title" + type="text" + label="PLG_SYSTEM_MOKOOG_FIELD_DEFAULT_OG_TITLE" + description="PLG_SYSTEM_MOKOOG_FIELD_DEFAULT_OG_TITLE_DESC" + default="" + filter="string" + /> + <field + name="default_og_description" + type="textarea" + label="PLG_SYSTEM_MOKOOG_FIELD_DEFAULT_OG_DESCRIPTION" + description="PLG_SYSTEM_MOKOOG_FIELD_DEFAULT_OG_DESCRIPTION_DESC" + default="" + filter="string" + rows="3" + /> <field name="default_image" type="media" @@ -82,6 +99,13 @@ default="" filter="string" /> + <field + name="discord_color" + type="color" + label="PLG_SYSTEM_MOKOOG_FIELD_DISCORD_COLOR" + description="PLG_SYSTEM_MOKOOG_FIELD_DISCORD_COLOR_DESC" + default="" + /> </fieldset> <fieldset name="advanced" label="PLG_SYSTEM_MOKOOG_FIELDSET_ADVANCED"> <field diff --git a/src/packages/plg_system_mokoog/src/Extension/MokoOG.php b/src/packages/plg_system_mokoog/src/Extension/MokoOG.php index bf9268e..0b68cc7 100644 --- a/src/packages/plg_system_mokoog/src/Extension/MokoOG.php +++ b/src/packages/plg_system_mokoog/src/Extension/MokoOG.php @@ -86,9 +86,12 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface // --- SEO meta tags (set first, before OG) --- $this->applySeoTags($doc, $ogData); - // Build tag values — custom overrides auto-generated - $title = $ogData->og_title ?: $doc->getTitle(); - $description = $ogData->og_description ?: $this->buildDescription($doc); + // Build tag values — custom OG data → site-wide defaults → auto-generated + $defaultTitle = $this->params->get('default_og_title', ''); + $defaultDesc = $this->params->get('default_og_description', ''); + + $title = $ogData->og_title ?: ($doc->getTitle() ?: $defaultTitle); + $description = $ogData->og_description ?: ($this->buildDescription($doc) ?: $defaultDesc); $image = $ogData->og_image ?: $this->findImage($option, $view, $id); $url = Uri::getInstance()->toString(); $siteName = $this->params->get('og_site_name', $app->get('sitename', '')); @@ -104,6 +107,10 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface if ($image) { $imageUrl = $this->resolveImageUrl($image); $doc->setMetaData('og:image', $imageUrl, 'property'); + + // Image dimensions help Facebook, LinkedIn, and Discord render previews faster + $doc->setMetaData('og:image:width', '1200', 'property'); + $doc->setMetaData('og:image:height', '630', 'property'); } // og:locale from current language @@ -141,6 +148,39 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface $doc->setMetaData('telegram:channel', $telegramChannel); } + // Discord embed color (theme-color meta tag) + $discordColor = $this->params->get('discord_color', ''); + + if ($discordColor) { + $doc->setMetaData('theme-color', $discordColor); + } + + // LinkedIn article tags + if ($option === 'com_content' && $view === 'article' && $id > 0) { + $doc->setMetaData('article:published_time', $this->getArticleDate($id, 'publish_up'), 'property'); + $doc->setMetaData('article:modified_time', $this->getArticleDate($id, 'modified'), 'property'); + + $author = $this->getArticleAuthor($id); + + if ($author) { + $doc->setMetaData('article:author', $author, 'property'); + } + } + + // Fire event so third-party plugins can add custom OG/social tags + $eventData = [ + 'subject' => $doc, + 'title' => $title, + 'description' => $description, + 'image' => $image ? $this->resolveImageUrl($image) : '', + 'url' => $url, + 'type' => $type, + 'option' => $option, + 'view' => $view, + 'id' => $id, + ]; + $app->getDispatcher()->dispatch('onMokoOGAfterRender', new Event('onMokoOGAfterRender', $eventData)); + // JSON-LD structured data if ($this->params->get('jsonld_enabled', 1)) { $imageUrl = $image ? $this->resolveImageUrl($image) : ''; @@ -404,4 +444,47 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface return rtrim(Uri::root(), '/') . '/' . ltrim($image, '/'); } + + /** + * Get a date field from an article. + * + * @param int $id Article ID + * @param string $field Date field name (publish_up, modified, created) + * + * @return string ISO 8601 date string + */ + 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); + + $db->setQuery($query); + $date = $db->loadResult(); + + return $date ?: ''; + } + + /** + * Get the author name for an article. + * + * @param int $id Article ID + * + * @return string + */ + 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); + + $db->setQuery($query); + + return $db->loadResult() ?: ''; + } } diff --git a/src/packages/plg_system_mokoog/src/Helper/JsonLdBuilder.php b/src/packages/plg_system_mokoog/src/Helper/JsonLdBuilder.php index 98f06ec..b9cf789 100644 --- a/src/packages/plg_system_mokoog/src/Helper/JsonLdBuilder.php +++ b/src/packages/plg_system_mokoog/src/Helper/JsonLdBuilder.php @@ -163,6 +163,9 @@ class JsonLdBuilder { $json = json_encode($schema, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + // Escape </ sequences to prevent XSS via </script> in content data + $json = str_replace('</', '<\/', $json); + return '<script type="application/ld+json">' . $json . '</script>'; } } From 5125fff07824fca8e881c7836d9ef5aa43d00215 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" <gitea-actions[bot]@mokoconsulting.tech> Date: Sun, 31 May 2026 01:40:48 +0000 Subject: [PATCH 067/132] chore(version): auto-bump 01.00.01-dev [skip ci] --- .mokogitea/manifest.xml | 1 + .mokogitea/workflows/auto-bump.yml | 132 ++--- .mokogitea/workflows/auto-release.yml | 540 +++++++++--------- CHANGELOG.md | 2 +- README.md | 2 +- src/packages/com_mokoog/mokoog.xml | 2 +- src/packages/plg_content_mokoog/mokoog.xml | 2 +- src/packages/plg_system_mokoog/mokoog.xml | 2 +- .../plg_webservices_mokoog/mokoog.xml | 2 +- src/pkg_mokoog.xml | 2 +- updates.xml | 2 +- 11 files changed, 345 insertions(+), 344 deletions(-) diff --git a/.mokogitea/manifest.xml b/.mokogitea/manifest.xml index 533e05a..97027dd 100644 --- a/.mokogitea/manifest.xml +++ b/.mokogitea/manifest.xml @@ -9,6 +9,7 @@ <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.01</version> <license spdx="GPL-3.0-or-later">GNU General Public License v3</license> </identity> <governance> diff --git a/.mokogitea/workflows/auto-bump.yml b/.mokogitea/workflows/auto-bump.yml index fb9dc82..34953b1 100644 --- a/.mokogitea/workflows/auto-bump.yml +++ b/.mokogitea/workflows/auto-bump.yml @@ -1,66 +1,66 @@ -# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: moko-platform.Release -# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform -# PATH: /.mokogitea/workflows/auto-bump.yml -# VERSION: 09.02.00 -# BRIEF: Auto patch-bump version on every push to dev (skips merge commits) - -name: "Universal: Auto Version Bump" - -on: - push: - branches: - - dev - - 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 moko-platform tools - run: | - if ! command -v composer &> /dev/null; then - sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 - fi - if [ -d "/opt/moko-platform/cli" ]; then - echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV" - else - git clone --depth 1 --branch main --quiet \ - "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \ - /tmp/moko-platform-api - cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet - echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV" - fi - - - name: Bump version - run: | - 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" +# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: moko-platform.Release +# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform +# PATH: /.mokogitea/workflows/auto-bump.yml +# VERSION: 09.02.00 +# BRIEF: Auto patch-bump version on every push to dev (skips merge commits) + +name: "Universal: Auto Version Bump" + +on: + push: + branches: + - dev + - 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 moko-platform tools + run: | + if ! command -v composer &> /dev/null; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + if [ -d "/opt/moko-platform/cli" ]; then + echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV" + else + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \ + /tmp/moko-platform-api + cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet + echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV" + fi + + - name: Bump version + run: | + 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" diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml index 1227ff8..c775dee 100644 --- a/.mokogitea/workflows/auto-release.yml +++ b/.mokogitea/workflows/auto-release.yml @@ -1,270 +1,270 @@ -# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: moko-platform.Release -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform -# PATH: /templates/workflows/universal/auto-release.yml.template -# VERSION: 05.00.00 -# BRIEF: Universal build & release � detects platform from manifest.xml -# -# +========================================================================+ -# | UNIVERSAL BUILD & RELEASE PIPELINE | -# +========================================================================+ -# | | -# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. | -# | | -# | Platform-specific: | -# | joomla: XML manifest, updates.xml, type-prefixed packages | -# | dolibarr: mod*.class.php, update.txt, dev version reset | -# | generic: README-only, no update stream | -# | | -# +========================================================================+ - -name: "Universal: Build & Release" - -on: - pull_request: - types: [opened, closed] - branches: - - main - workflow_dispatch: - inputs: - action: - description: 'Action to perform' - required: false - type: choice - default: release - options: - - release - - promote-rc - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} - GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} - -permissions: - contents: write - -jobs: - # ── PR Opened → Rename branch to RC and build RC release ───────────────────── - promote-rc: - name: Promote to RC - runs-on: release - if: >- - (github.event.action == 'opened' && github.event.pull_request.merged != true) || - (github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc') - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - token: ${{ secrets.MOKOGITEA_TOKEN }} - fetch-depth: 1 - - - name: Setup moko-platform tools - env: - MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting - run: | - if ! command -v composer &> /dev/null; then - sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 - fi - # Always fetch latest CLI tools — never use stale cache from previous runs - rm -rf /tmp/moko-platform-api - git clone --depth 1 --branch main --quiet \ - "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ - /tmp/moko-platform-api - cd /tmp/moko-platform-api - composer install --no-dev --no-interaction --quiet - - - name: Rename branch to rc - run: | - php /tmp/moko-platform-api/cli/branch_rename.php \ - --from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" \ - --api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \ - --pr "${{ github.event.pull_request.number }}" - - - name: Checkout rc and configure git - run: | - git fetch origin rc - git checkout rc - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" - - - name: Publish RC release - run: | - php /tmp/moko-platform-api/cli/release_publish.php \ - --path . --stability rc --bump minor --branch rc \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" - - - name: Summary - if: always() - run: | - echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY - echo "Branch renamed to rc, minor bump, RC + lesser stream releases built, updates.xml synced" >> $GITHUB_STEP_SUMMARY - - # ── Merged PR → Build & Release (or promote RC to stable) ──────────────────── - release: - name: Build & Release Pipeline - runs-on: release - if: >- - github.event.pull_request.merged == true || - (github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc') - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - token: ${{ secrets.MOKOGITEA_TOKEN }} - fetch-depth: 0 - - - name: Configure git for bot pushes - run: | - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" - - - name: Setup moko-platform tools - env: - MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}' - run: | - # Ensure PHP + Composer are available - if ! command -v composer &> /dev/null; then - sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 - fi - # Always fetch latest CLI tools — never use stale cache from previous runs - rm -rf /tmp/moko-platform-api - git clone --depth 1 --branch main --quiet \ - "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ - /tmp/moko-platform-api - cd /tmp/moko-platform-api - composer install --no-dev --no-interaction --quiet - - - - name: "Publish stable release" - run: | - php /tmp/moko-platform-api/cli/release_publish.php \ - --path . --stability stable --bump minor --branch main \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" - - # -- STEP 9: Mirror to GitHub (stable only) -------------------------------- - - name: "Step 9: Mirror release to GitHub" - if: >- - steps.version.outputs.skip != 'true' && - secrets.GH_MIRROR_TOKEN != '' - continue-on-error: true - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - RELEASE_TAG="${{ steps.version.outputs.release_tag }}" - GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php /tmp/moko-platform-api/cli/release_mirror.php \ - --version "$VERSION" --tag "$RELEASE_TAG" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ - --gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \ - --branch main 2>&1 || true - echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY - - # -- STEP 10: Sync main branch to GitHub mirror ---------------------------- - - name: "Step 10: Push main to GitHub mirror" - if: >- - steps.version.outputs.skip != 'true' && - secrets.GH_MIRROR_TOKEN != '' - continue-on-error: true - run: | - GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" - GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1) - GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2) - git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \ - git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" - git fetch origin main --depth=1 - git push github origin/main:refs/heads/main --force 2>/dev/null \ - && echo "main branch pushed to GitHub mirror" \ - || echo "WARNING: GitHub mirror push failed" - - - name: "Step 11: Delete rc branch and recreate dev from main" - if: steps.version.outputs.skip != 'true' - continue-on-error: true - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" - - # Delete rc branch (ephemeral — created by promote-rc) - curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ - "${API_BASE}/branches/rc" 2>/dev/null \ - && echo "Deleted rc branch" || echo "rc branch not found" - - # Delete dev branch - curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ - "${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch" - - # Recreate dev from main (now includes version bump + changelog promotion) - curl -sf -X POST -H "Authorization: token ${TOKEN}" \ - -H "Content-Type: application/json" \ - "${API_BASE}/branches" \ - -d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main" - - echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY - - - name: "Step 12: Create version branch from main" - if: steps.version.outputs.skip != 'true' - continue-on-error: true - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - BRANCH_NAME="version/${VERSION}" - MAIN_SHA=$(git rev-parse HEAD) - - # Delete old version branch if it exists (same version re-release) - curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}" - - # Create version/XX.YY.ZZ from main - curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed" - - echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY - - - - # -- Dolibarr post-release: Reset dev version ----------------------------- - - name: "Post-release: Reset dev version" - if: steps.version.outputs.skip != 'true' - continue-on-error: true - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php /tmp/moko-platform-api/cli/version_reset_dev.php \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \ - --branch dev --path . 2>&1 || true - - # -- Summary -------------------------------------------------------------- - - name: Pipeline Summary - if: always() - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - PLATFORM="${{ steps.platform.outputs.platform }}" - if [ "${{ steps.version.outputs.skip }}" = "true" ]; then - echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY - echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY - elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then - echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY - else - echo "" >> $GITHUB_STEP_SUMMARY - echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY - echo "|------|--------|" >> $GITHUB_STEP_SUMMARY - echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY - fi +# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: moko-platform.Release +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform +# PATH: /templates/workflows/universal/auto-release.yml.template +# VERSION: 05.00.00 +# BRIEF: Universal build & release � detects platform from manifest.xml +# +# +========================================================================+ +# | UNIVERSAL BUILD & RELEASE PIPELINE | +# +========================================================================+ +# | | +# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. | +# | | +# | Platform-specific: | +# | joomla: XML manifest, updates.xml, type-prefixed packages | +# | dolibarr: mod*.class.php, update.txt, dev version reset | +# | generic: README-only, no update stream | +# | | +# +========================================================================+ + +name: "Universal: Build & Release" + +on: + pull_request: + types: [opened, closed] + branches: + - main + workflow_dispatch: + inputs: + action: + description: 'Action to perform' + required: false + type: choice + default: release + options: + - release + - promote-rc + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + +permissions: + contents: write + +jobs: + # ── PR Opened → Rename branch to RC and build RC release ───────────────────── + promote-rc: + name: Promote to RC + runs-on: release + if: >- + (github.event.action == 'opened' && github.event.pull_request.merged != true) || + (github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc') + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.MOKOGITEA_TOKEN }} + fetch-depth: 1 + + - name: Setup moko-platform tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + run: | + if ! command -v composer &> /dev/null; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + # Always fetch latest CLI tools — never use stale cache from previous runs + rm -rf /tmp/moko-platform-api + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ + /tmp/moko-platform-api + cd /tmp/moko-platform-api + composer install --no-dev --no-interaction --quiet + + - name: Rename branch to rc + run: | + php /tmp/moko-platform-api/cli/branch_rename.php \ + --from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" \ + --api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \ + --pr "${{ github.event.pull_request.number }}" + + - name: Checkout rc and configure git + run: | + git fetch origin rc + git checkout rc + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + + - name: Publish RC release + run: | + php /tmp/moko-platform-api/cli/release_publish.php \ + --path . --stability rc --bump minor --branch rc \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" + + - name: Summary + if: always() + run: | + echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY + echo "Branch renamed to rc, minor bump, RC + lesser stream releases built, updates.xml synced" >> $GITHUB_STEP_SUMMARY + + # ── Merged PR → Build & Release (or promote RC to stable) ──────────────────── + release: + name: Build & Release Pipeline + runs-on: release + if: >- + github.event.pull_request.merged == true || + (github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc') + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.MOKOGITEA_TOKEN }} + fetch-depth: 0 + + - name: Configure git for bot pushes + run: | + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + + - name: Setup moko-platform tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}' + run: | + # Ensure PHP + Composer are available + if ! command -v composer &> /dev/null; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + # Always fetch latest CLI tools — never use stale cache from previous runs + rm -rf /tmp/moko-platform-api + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ + /tmp/moko-platform-api + cd /tmp/moko-platform-api + composer install --no-dev --no-interaction --quiet + + + - name: "Publish stable release" + run: | + php /tmp/moko-platform-api/cli/release_publish.php \ + --path . --stability stable --bump minor --branch main \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" + + # -- STEP 9: Mirror to GitHub (stable only) -------------------------------- + - name: "Step 9: Mirror release to GitHub" + if: >- + steps.version.outputs.skip != 'true' && + secrets.GH_MIRROR_TOKEN != '' + continue-on-error: true + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php /tmp/moko-platform-api/cli/release_mirror.php \ + --version "$VERSION" --tag "$RELEASE_TAG" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ + --gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \ + --branch main 2>&1 || true + echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY + + # -- STEP 10: Sync main branch to GitHub mirror ---------------------------- + - name: "Step 10: Push main to GitHub mirror" + if: >- + steps.version.outputs.skip != 'true' && + secrets.GH_MIRROR_TOKEN != '' + continue-on-error: true + run: | + GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" + GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1) + GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2) + git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \ + git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" + git fetch origin main --depth=1 + git push github origin/main:refs/heads/main --force 2>/dev/null \ + && echo "main branch pushed to GitHub mirror" \ + || echo "WARNING: GitHub mirror push failed" + + - name: "Step 11: Delete rc branch and recreate dev from main" + if: steps.version.outputs.skip != 'true' + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" + + # Delete rc branch (ephemeral — created by promote-rc) + curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/branches/rc" 2>/dev/null \ + && echo "Deleted rc branch" || echo "rc branch not found" + + # Delete dev branch + curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch" + + # Recreate dev from main (now includes version bump + changelog promotion) + curl -sf -X POST -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/json" \ + "${API_BASE}/branches" \ + -d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main" + + echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY + + - name: "Step 12: Create version branch from main" + if: steps.version.outputs.skip != 'true' + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + BRANCH_NAME="version/${VERSION}" + MAIN_SHA=$(git rev-parse HEAD) + + # Delete old version branch if it exists (same version re-release) + curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}" + + # Create version/XX.YY.ZZ from main + curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed" + + echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY + + + + # -- Dolibarr post-release: Reset dev version ----------------------------- + - name: "Post-release: Reset dev version" + if: steps.version.outputs.skip != 'true' + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php /tmp/moko-platform-api/cli/version_reset_dev.php \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \ + --branch dev --path . 2>&1 || true + + # -- Summary -------------------------------------------------------------- + - name: Pipeline Summary + if: always() + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + PLATFORM="${{ steps.platform.outputs.platform }}" + if [ "${{ steps.version.outputs.skip }}" = "true" ]; then + echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY + echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY + elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then + echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY + echo "|------|--------|" >> $GITHUB_STEP_SUMMARY + echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY + fi diff --git a/CHANGELOG.md b/CHANGELOG.md index 472e290..4d7e4c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -<!-- VERSION: 01.00.00 --> +<!-- VERSION: 01.00.01 --> All notable changes to MokoJoomOpenGraph will be documented in this file. diff --git a/README.md b/README.md index 85f5e7e..c27ce8e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MokoJoomOpenGraph -<!-- VERSION: 01.00.00 --> +<!-- VERSION: 01.00.01 --> Open Graph, Twitter Card, and social sharing meta tag management for Joomla 4/5/6. diff --git a/src/packages/com_mokoog/mokoog.xml b/src/packages/com_mokoog/mokoog.xml index 507be58..1bccaae 100644 --- a/src/packages/com_mokoog/mokoog.xml +++ b/src/packages/com_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="component" method="upgrade"> <name>com_mokoog</name> - <version>01.00.00-dev</version> + <version>01.00.01-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/src/packages/plg_content_mokoog/mokoog.xml b/src/packages/plg_content_mokoog/mokoog.xml index a958a75..1d1e2e1 100644 --- a/src/packages/plg_content_mokoog/mokoog.xml +++ b/src/packages/plg_content_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="plugin" group="content" method="upgrade"> <name>Content - MokoJoomOpenGraph</name> - <version>01.00.00-dev</version> + <version>01.00.01-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/src/packages/plg_system_mokoog/mokoog.xml b/src/packages/plg_system_mokoog/mokoog.xml index 231c803..2b1246f 100644 --- a/src/packages/plg_system_mokoog/mokoog.xml +++ b/src/packages/plg_system_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="plugin" group="system" method="upgrade"> <name>System - MokoJoomOpenGraph</name> - <version>01.00.00-dev</version> + <version>01.00.01-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/src/packages/plg_webservices_mokoog/mokoog.xml b/src/packages/plg_webservices_mokoog/mokoog.xml index be15f80..08008aa 100644 --- a/src/packages/plg_webservices_mokoog/mokoog.xml +++ b/src/packages/plg_webservices_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="plugin" group="webservices" method="upgrade"> <name>Web Services - MokoJoomOpenGraph</name> - <version>01.00.00-dev</version> + <version>01.00.01-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/src/pkg_mokoog.xml b/src/pkg_mokoog.xml index 757a56b..a68ab75 100644 --- a/src/pkg_mokoog.xml +++ b/src/pkg_mokoog.xml @@ -8,7 +8,7 @@ <extension type="package" method="upgrade"> <name>Package - MokoJoomOpenGraph</name> <packagename>mokoog</packagename> - <version>01.00.00-dev</version> + <version>01.00.01-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/updates.xml b/updates.xml index 57e320d..3c318d7 100644 --- a/updates.xml +++ b/updates.xml @@ -1,7 +1,7 @@ <?xml version='1.0' encoding='UTF-8'?> <!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> SPDX-License-Identifier: GPL-3.0-or-later - VERSION: 01.00.00-dev + VERSION: 01.00.01 --> <updates> From e1eb40994313ca80dc7e0bd304ac3ca909730308 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" <gitea-actions[bot]@mokoconsulting.tech> Date: Sun, 31 May 2026 01:40:50 +0000 Subject: [PATCH 068/132] chore: update development channel 01.00.01-dev [skip ci] --- updates.xml | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/updates.xml b/updates.xml index 3c318d7..0534d44 100644 --- a/updates.xml +++ b/updates.xml @@ -1,23 +1,27 @@ <?xml version='1.0' encoding='UTF-8'?> <!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> SPDX-License-Identifier: GPL-3.0-or-later - VERSION: 01.00.01 + VERSION: 01.00.01-dev --> <updates> <update> - <name>MokoJoomOpenGraph</name> - <description>MokoJoomOpenGraph update</description> + <name>Package - MokoJoomOpenGraph</name> + <description>Package - MokoJoomOpenGraph development build.</description> <element>pkg_mokoog</element> <type>package</type> - <version>01.00.00-dev</version> - <tags><tag>development</tag></tags> - <infourl title="MokoJoomOpenGraph">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/releases/tag/development</infourl> + <client>site</client> + <version>01.00.01-dev</version> + <creationDate>2026-05-31</creationDate> + <infourl title='Package - MokoJoomOpenGraph'>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/releases/tag/development</infourl> <downloads> - <downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/releases/download/development/pkg_pkg_mokoog-01.00.00-dev.zip</downloadurl> + <downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/releases/download/development/pkg_mokoog-01.00.01-dev.zip</downloadurl> </downloads> - <targetplatform name="joomla" version="((5.[0-9])|(6.[0-9]))" /> + <sha256>183fde7dcc8e6c00a4cf063165556d5548f4ea5c553be7c2efa7e7e073866403</sha256> + <tags><tag>dev</tag></tags> + <changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/raw/branch/main/CHANGELOG.md</changelogurl> <maintainer>Moko Consulting</maintainer> <maintainerurl>https://mokoconsulting.tech</maintainerurl> + <targetplatform name="joomla" version="(5|6)\..*" /> </update> </updates> From 5da49ce4164cdeb67ebad90adec182191e60ede2 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sun, 31 May 2026 01:46:05 +0000 Subject: [PATCH 069/132] chore: sync .mokogitea/workflows/cascade-dev.yml from moko-platform [skip ci] --- .mokogitea/workflows/cascade-dev.yml | 217 +-------------------------- 1 file changed, 7 insertions(+), 210 deletions(-) diff --git a/.mokogitea/workflows/cascade-dev.yml b/.mokogitea/workflows/cascade-dev.yml index f7f0b3c..5f7c1d7 100644 --- a/.mokogitea/workflows/cascade-dev.yml +++ b/.mokogitea/workflows/cascade-dev.yml @@ -1,213 +1,10 @@ -# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: moko-platform.Maintenance -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform -# PATH: /templates/workflows/cascade-dev.yml.template -# VERSION: 02.00.00 -# BRIEF: Forward-merge main → all open branches after every push to main -# -# +========================================================================+ -# | CASCADE MAIN → ALL BRANCHES | -# +========================================================================+ -# | | -# | Triggers on every push to main (PR merges, bot commits, etc.) | -# | | -# | 1. List all branches matching: dev, rc/*, beta/*, alpha/* | -# | 2. For each: create PR (main → branch), auto-merge if clean | -# | 3. On conflict: leave PR open for manual resolution | -# | | -# +========================================================================+ - -name: "Universal: Cascade Main → Dev" - -on: - push: - branches: - - main - workflow_dispatch: - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} - GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} - -permissions: - contents: write - pull-requests: write - +# 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: - cascade: - name: Cascade main → branches + noop: runs-on: ubuntu-latest - if: >- - !contains(github.event.head_commit.message, '[skip ci]') && - !contains(github.event.head_commit.message, '[skip cascade]') - steps: - - name: Discover target branches - id: branches - env: - GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - run: | - API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - - # Fetch all branches (paginated) - PAGE=1 - ALL_BRANCHES="" - while true; do - BATCH=$(curl -sS \ - -H "Authorization: token ${GITEA_TOKEN}" \ - "${API}/branches?page=${PAGE}&limit=50" \ - | jq -r '.[].name // empty') - [ -z "$BATCH" ] && break - ALL_BRANCHES="$ALL_BRANCHES $BATCH" - PAGE=$((PAGE + 1)) - done - - # Filter to cascade targets: dev, dev/*, rc/*, beta/*, alpha/* - TARGETS="" - for BRANCH in $ALL_BRANCHES; do - case "$BRANCH" in - dev|dev/*|rc/*|beta/*|alpha/*) - TARGETS="$TARGETS $BRANCH" - ;; - esac - done - - TARGETS=$(echo "$TARGETS" | xargs) # trim whitespace - - if [ -z "$TARGETS" ]; then - echo "targets=" >> "$GITHUB_OUTPUT" - echo "ℹ️ No cascade target branches found" - else - echo "targets=$TARGETS" >> "$GITHUB_OUTPUT" - COUNT=$(echo "$TARGETS" | wc -w) - echo "📋 Found ${COUNT} target branch(es): ${TARGETS}" - fi - - - name: Cascade to all target branches - if: steps.branches.outputs.targets != '' - env: - GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - run: | - API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - SHORT_SHA="${GITHUB_SHA:0:7}" - TARGETS="${{ steps.branches.outputs.targets }}" - - SUCCESS=0 - CONFLICTS=0 - SKIPPED=0 - FAILED=0 - - for BRANCH in $TARGETS; do - echo "" - echo "═══ main → ${BRANCH} ═══" - - # Check if branch is already up to date - ENCODED_BRANCH=$(echo "$BRANCH" | sed 's|/|%2F|g') - RESPONSE=$(curl -sS \ - -H "Authorization: token ${GITEA_TOKEN}" \ - "${API}/compare/${ENCODED_BRANCH}...main") - - AHEAD=$(echo "$RESPONSE" | jq '.total_commits // 0') - - if [ "$AHEAD" -eq 0 ]; then - echo " ✅ Already up to date" - SKIPPED=$((SKIPPED + 1)) - continue - fi - - echo " ℹ️ main is ${AHEAD} commit(s) ahead" - - # Check for existing cascade PR - EXISTING=$(curl -sS \ - -H "Authorization: token ${GITEA_TOKEN}" \ - "${API}/pulls?state=open&head=${GITEA_ORG}:main&base=${ENCODED_BRANCH}&limit=1") - - EXISTING_COUNT=$(echo "$EXISTING" | jq 'length') - PR_NUMBER="" - - if [ "$EXISTING_COUNT" -gt 0 ]; then - PR_NUMBER=$(echo "$EXISTING" | jq -r '.[0].number') - echo " ℹ️ Reusing existing PR #${PR_NUMBER}" - else - # Create cascade PR - PR_RESPONSE=$(curl -sS -w "\n%{http_code}" \ - -X POST \ - -H "Authorization: token ${GITEA_TOKEN}" \ - -H "Content-Type: application/json" \ - -d "{ - \"title\": \"chore: cascade main → ${BRANCH} (${SHORT_SHA}) [skip ci]\", - \"body\": \"## Automatic cascade\\n\\nForward-merging \`main\` (${SHORT_SHA}) into \`${BRANCH}\`.\\n\\nIf conflicts exist, resolve manually and merge.\\n\\n> Auto-created by **Cascade Main → Dev**.\", - \"head\": \"main\", - \"base\": \"${BRANCH}\" - }" \ - "${API}/pulls") - - HTTP_CODE=$(echo "$PR_RESPONSE" | tail -1) - BODY=$(echo "$PR_RESPONSE" | sed '$d') - PR_NUMBER=$(echo "$BODY" | jq -r '.number // empty') - - if [ "$HTTP_CODE" != "201" ] || [ -z "$PR_NUMBER" ]; then - MSG=$(echo "$BODY" | jq -r '.message // .' 2>/dev/null | head -1) - echo " ❌ Failed to create PR (HTTP ${HTTP_CODE}): ${MSG}" - FAILED=$((FAILED + 1)) - continue - fi - - echo " ✅ Created PR #${PR_NUMBER}" - fi - - # Try auto-merge - PR_DATA=$(curl -sS \ - -H "Authorization: token ${GITEA_TOKEN}" \ - "${API}/pulls/${PR_NUMBER}") - - MERGEABLE=$(echo "$PR_DATA" | jq -r '.mergeable // false') - - if [ "$MERGEABLE" != "true" ]; then - echo " ⚠️ Conflicts — PR #${PR_NUMBER} left open" - CONFLICTS=$((CONFLICTS + 1)) - continue - fi - - MERGE_RESPONSE=$(curl -sS -w "\n%{http_code}" \ - -X POST \ - -H "Authorization: token ${GITEA_TOKEN}" \ - -H "Content-Type: application/json" \ - -d "{ - \"Do\": \"merge\", - \"merge_message_field\": \"chore: cascade main → ${BRANCH} [skip ci]\", - \"delete_branch_after_merge\": false - }" \ - "${API}/pulls/${PR_NUMBER}/merge") - - MERGE_HTTP=$(echo "$MERGE_RESPONSE" | tail -1) - - if [ "$MERGE_HTTP" = "200" ] || [ "$MERGE_HTTP" = "204" ]; then - echo " ✅ Merged — ${BRANCH} is in sync" - SUCCESS=$((SUCCESS + 1)) - else - MERGE_BODY=$(echo "$MERGE_RESPONSE" | sed '$d') - echo " ⚠️ Merge failed (HTTP ${MERGE_HTTP}) — PR #${PR_NUMBER} left open" - CONFLICTS=$((CONFLICTS + 1)) - fi - done - - # Summary - echo "" - echo "════════════════════════════════════════" - echo " ✅ Merged: ${SUCCESS}" - echo " ⚠️ Conflicts: ${CONFLICTS}" - echo " ⏭️ Up to date: ${SKIPPED}" - echo " ❌ Failed: ${FAILED}" - echo "════════════════════════════════════════" - - if [ "$FAILED" -gt 0 ]; then - exit 1 - fi + - run: echo "Cascade disabled — auto-release handles dev recreation" From 381557e79bc9dddec0725dc29b055d6d52e4c218 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sun, 31 May 2026 01:46:47 +0000 Subject: [PATCH 070/132] chore: sync .mokogitea/workflows/auto-bump.yml from moko-platform [skip ci] --- .mokogitea/workflows/auto-bump.yml | 132 ++++++++++++++--------------- 1 file changed, 66 insertions(+), 66 deletions(-) diff --git a/.mokogitea/workflows/auto-bump.yml b/.mokogitea/workflows/auto-bump.yml index 34953b1..fb9dc82 100644 --- a/.mokogitea/workflows/auto-bump.yml +++ b/.mokogitea/workflows/auto-bump.yml @@ -1,66 +1,66 @@ -# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: moko-platform.Release -# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform -# PATH: /.mokogitea/workflows/auto-bump.yml -# VERSION: 09.02.00 -# BRIEF: Auto patch-bump version on every push to dev (skips merge commits) - -name: "Universal: Auto Version Bump" - -on: - push: - branches: - - dev - - 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 moko-platform tools - run: | - if ! command -v composer &> /dev/null; then - sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 - fi - if [ -d "/opt/moko-platform/cli" ]; then - echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV" - else - git clone --depth 1 --branch main --quiet \ - "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \ - /tmp/moko-platform-api - cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet - echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV" - fi - - - name: Bump version - run: | - 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" +# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: moko-platform.Release +# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform +# PATH: /.mokogitea/workflows/auto-bump.yml +# VERSION: 09.02.00 +# BRIEF: Auto patch-bump version on every push to dev (skips merge commits) + +name: "Universal: Auto Version Bump" + +on: + push: + branches: + - dev + - 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 moko-platform tools + run: | + if ! command -v composer &> /dev/null; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + if [ -d "/opt/moko-platform/cli" ]; then + echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV" + else + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \ + /tmp/moko-platform-api + cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet + echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV" + fi + + - name: Bump version + run: | + 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" From e1cf4cb3859b02bc90fe8c5da7273343841e7546 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sat, 30 May 2026 20:42:58 -0500 Subject: [PATCH 071/132] docs: update README and CHANGELOG for v1.0.0 release Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- CHANGELOG.md | 42 +++++++++++++++++++++++++++++++----------- README.md | 51 ++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 71 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d7e4c6..92962d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,17 +8,37 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] -### Removed -- Removed deploy-manual.yml workflow — switching to Joomla update server method for extension distribution +### Security +- Fix JSON-LD XSS vulnerability via `</script>` injection in content data (#34) +- Add ACL permission checks to Batch and ImportExport controllers (#37) +- Add CSV import file type, MIME type, size, and content_type validation (#35) +- Fix multilingual data corruption in content plugin load/save (#41) ### Added -- Initial package structure with component, system plugin, and content plugin -- Open Graph meta tag injection via system plugin (`onBeforeCompileHead`) -- Twitter/X Card meta tag support (Summary and Summary with Large Image) -- Per-article OG fields in the article editor -- Per-menu-item OG fields in the menu item editor -- Auto-generation of OG tags from article title, description, and images +- Site-wide default OG title and description plugin parameters +- Discord embed color via `theme-color` meta tag (color picker in plugin config) +- LinkedIn article tags: `article:published_time`, `article:modified_time`, `article:author` +- `og:image:width` and `og:image:height` for faster social preview rendering +- `onMokoOGAfterRender` event for third-party plugin extensibility +- Joomla Web Services API for OG tags — full CRUD at `/api/v1/mokoog/tags` (#27) +- Live social preview in article/menu editors (Facebook and Twitter/X card mockups) (#3) +- 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) +- Social platform debugger quick links (Facebook, LinkedIn, Google) (#9) +- Content type adapter architecture for K2, VirtueMart, HikaShop (#5) +- WhatsApp and Telegram link preview optimization (#10) +- Category-level OG tag support (#4) +- Batch OG tag generation for existing articles (#1) +- Auto-resize OG images to 1200x630px with center crop (#2) +- SEO meta tag management: title, description, robots, canonical URL (#8) +- Per-article and per-menu-item OG fields in the editor +- Auto-generation of OG tags from article content, title, and images - Default fallback image configuration -- Admin tag manager component for viewing all OG records -- Facebook App ID support -- Database table `#__mokoog_tags` for storing custom OG data +- Admin tag manager component with filtering, search, and pagination +- Facebook App ID and Telegram channel support +- Database table `#__mokoog_tags` with multilingual unique key + +### Removed +- Removed deploy-manual.yml workflow — using Joomla update server for distribution diff --git a/README.md b/README.md index c27ce8e..b51c8a2 100644 --- a/README.md +++ b/README.md @@ -6,35 +6,64 @@ Open Graph, Twitter Card, and social sharing meta tag management for Joomla 4/5/ ## Overview -MokoJoomOpenGraph gives you full control over how your Joomla content appears when shared on Facebook, Twitter/X, LinkedIn, WhatsApp, and other social platforms. Set custom titles, descriptions, and images per article and menu item — or let the extension auto-generate them from your existing content. +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. ## Features -- **Open Graph tags** — `og:title`, `og:description`, `og:image`, `og:url`, `og:type`, `og:site_name` +### Social Meta Tags +- **Open Graph tags** — `og:title`, `og:description`, `og:image`, `og:url`, `og:type`, `og:site_name`, `og:locale` - **Twitter/X Cards** — Summary and Summary with Large Image card types -- **Per-article control** — Custom OG fields in the article editor +- **LinkedIn** — `article:published_time`, `article:modified_time`, `article:author` +- **Discord** — Custom embed color via `theme-color` meta tag +- **Telegram** — `telegram:channel` for link previews +- **Facebook** — `fb:app_id` support, `og:image:width`/`og:image:height` for instant previews + +### Content Management +- **Per-article control** — Custom OG fields tab in the article editor - **Per-menu-item control** — Custom OG fields in the menu item editor -- **Auto-generation** — Automatically builds tags from article content, title, and images -- **Default fallback image** — Site-wide default when no article image exists -- **Admin tag manager** — View and manage all OG records from a central dashboard -- **Facebook App ID** — Optional `fb:app_id` meta tag support -- **Joomla 4/5/6** — Modern DI container architecture, Joomla coding standards +- **Per-category control** — Category-level OG tag overrides +- **Multilingual support** — Per-language OG data with language-aware fallback +- **Auto-generation** — Builds tags from article content, title, and images automatically +- **Site-wide defaults** — Default OG title, description, and image for all pages + +### SEO +- **SEO title override** — Custom `<title>` tag per page +- **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 + +### Admin Tools +- **Tag manager dashboard** — View and manage all OG records centrally +- **Batch generation** — Auto-generate OG tags for all existing articles +- **CSV import/export** — Bulk manage OG data via CSV files +- **SEO health badges** — Visual indicators for missing descriptions, long titles, noindex +- **Debug links** — Quick links to Facebook Debugger, LinkedIn Inspector, Google Rich Results +- **Live preview** — Real-time Facebook and Twitter/X card preview in the editor + +### Developer Features +- **REST API** — Full CRUD via Joomla Web Services (`/api/v1/mokoog/tags`) +- **Content type adapters** — Extensible architecture for K2, VirtueMart, HikaShop +- **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) 2. In Joomla Administrator → Extensions → Install → Upload Package File -3. The system plugin is enabled automatically on install +3. All plugins are enabled automatically on install ## Configuration Navigate to **Extensions → Plugins → System - MokoJoomOpenGraph** to configure: - Site name override +- Default OG title and description (site-wide fallback) - Default fallback image - Twitter Card type and @username - Facebook App ID -- Auto-generation behavior -- Description length limit +- Discord embed color +- Telegram channel +- Auto-generation, image resize, JSON-LD, and description length settings ## License From fe928923107bbb59ef6c2ed99f50bb87f8640add Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sat, 30 May 2026 20:54:02 -0500 Subject: [PATCH 072/132] feat(ci): trigger RC build on PR draft to main, rename branch to rc When a PR is opened or drafted targeting main, the pre-release workflow now automatically builds a release-candidate package and renames the source branch to 'rc'. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- .mokogitea/workflows/pre-release.yml | 37 ++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml index 162b08f..a85031a 100644 --- a/.mokogitea/workflows/pre-release.yml +++ b/.mokogitea/workflows/pre-release.yml @@ -14,9 +14,10 @@ name: "Universal: Pre-Release" on: pull_request: - types: [closed] + types: [opened, ready_for_review, closed] branches: - dev + - main workflow_dispatch: inputs: stability: @@ -39,11 +40,12 @@ env: jobs: build: - name: "Build Pre-Release (${{ inputs.stability || 'development' }})" + name: "Build Pre-Release (${{ inputs.stability || (github.event.pull_request.base.ref == 'main' && 'release-candidate') || 'development' }})" runs-on: release if: >- github.event_name == 'workflow_dispatch' || - (github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev') + (github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev') || + (github.event.action != 'closed' && github.event.pull_request.base.ref == 'main') steps: - name: Checkout @@ -73,10 +75,35 @@ jobs: run: | php ${MOKO_CLI}/manifest_read.php --path . --github-output + - name: Rename branch to rc on PR draft to main + if: >- + github.event.action != 'closed' && + github.event.pull_request.base.ref == 'main' + run: | + HEAD_BRANCH="${{ github.event.pull_request.head.ref }}" + if [ "$HEAD_BRANCH" != "rc" ]; then + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + git checkout -b rc + git push origin rc 2>&1 || true + # Update PR head branch via API + curl -s -X PATCH \ + -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \ + -H "Content-Type: application/json" \ + -d "{\"head\": \"rc\"}" \ + "${GITEA_URL}/api/v1/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}" || true + fi + - name: Resolve metadata and bump version id: meta run: | - STABILITY="${{ inputs.stability || 'development' }}" + # Auto-detect RC when PR targets main + if [ "${{ github.event.pull_request.base.ref }}" = "main" ] && [ "${{ github.event.action }}" != "closed" ]; then + STABILITY="release-candidate" + else + STABILITY="${{ inputs.stability || 'development' }}" + fi case "$STABILITY" in development) SUFFIX="-dev"; TAG="development" ;; @@ -142,7 +169,7 @@ jobs: php ${MOKO_CLI}/release_create.php \ --path . --version "$VERSION" --tag "$TAG" \ --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ - --repo "${GITEA_REPO}" --branch dev --prerelease + --repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease - name: Build package and upload id: package From 516e2a4a472fca7788ddd74de4ee27bd94fc48ed Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" <gitea-actions[bot]@mokoconsulting.tech> Date: Sun, 31 May 2026 01:54:51 +0000 Subject: [PATCH 073/132] chore(version): pre-release bump to 01.00.01-rc [skip ci] --- .mokogitea/workflows/auto-bump.yml | 132 +++++++++--------- src/packages/com_mokoog/mokoog.xml | 2 +- src/packages/plg_content_mokoog/mokoog.xml | 2 +- src/packages/plg_system_mokoog/mokoog.xml | 2 +- .../plg_webservices_mokoog/mokoog.xml | 2 +- src/pkg_mokoog.xml | 2 +- 6 files changed, 71 insertions(+), 71 deletions(-) diff --git a/.mokogitea/workflows/auto-bump.yml b/.mokogitea/workflows/auto-bump.yml index fb9dc82..34953b1 100644 --- a/.mokogitea/workflows/auto-bump.yml +++ b/.mokogitea/workflows/auto-bump.yml @@ -1,66 +1,66 @@ -# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: moko-platform.Release -# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform -# PATH: /.mokogitea/workflows/auto-bump.yml -# VERSION: 09.02.00 -# BRIEF: Auto patch-bump version on every push to dev (skips merge commits) - -name: "Universal: Auto Version Bump" - -on: - push: - branches: - - dev - - 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 moko-platform tools - run: | - if ! command -v composer &> /dev/null; then - sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 - fi - if [ -d "/opt/moko-platform/cli" ]; then - echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV" - else - git clone --depth 1 --branch main --quiet \ - "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \ - /tmp/moko-platform-api - cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet - echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV" - fi - - - name: Bump version - run: | - 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" +# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: moko-platform.Release +# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform +# PATH: /.mokogitea/workflows/auto-bump.yml +# VERSION: 09.02.00 +# BRIEF: Auto patch-bump version on every push to dev (skips merge commits) + +name: "Universal: Auto Version Bump" + +on: + push: + branches: + - dev + - 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 moko-platform tools + run: | + if ! command -v composer &> /dev/null; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + if [ -d "/opt/moko-platform/cli" ]; then + echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV" + else + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \ + /tmp/moko-platform-api + cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet + echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV" + fi + + - name: Bump version + run: | + 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" diff --git a/src/packages/com_mokoog/mokoog.xml b/src/packages/com_mokoog/mokoog.xml index 1bccaae..6ede7db 100644 --- a/src/packages/com_mokoog/mokoog.xml +++ b/src/packages/com_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="component" method="upgrade"> <name>com_mokoog</name> - <version>01.00.01-dev</version> + <version>01.00.01-rc</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/src/packages/plg_content_mokoog/mokoog.xml b/src/packages/plg_content_mokoog/mokoog.xml index 1d1e2e1..458438c 100644 --- a/src/packages/plg_content_mokoog/mokoog.xml +++ b/src/packages/plg_content_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="plugin" group="content" method="upgrade"> <name>Content - MokoJoomOpenGraph</name> - <version>01.00.01-dev</version> + <version>01.00.01-rc</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/src/packages/plg_system_mokoog/mokoog.xml b/src/packages/plg_system_mokoog/mokoog.xml index 2b1246f..25c8ff8 100644 --- a/src/packages/plg_system_mokoog/mokoog.xml +++ b/src/packages/plg_system_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="plugin" group="system" method="upgrade"> <name>System - MokoJoomOpenGraph</name> - <version>01.00.01-dev</version> + <version>01.00.01-rc</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/src/packages/plg_webservices_mokoog/mokoog.xml b/src/packages/plg_webservices_mokoog/mokoog.xml index 08008aa..8b42f6c 100644 --- a/src/packages/plg_webservices_mokoog/mokoog.xml +++ b/src/packages/plg_webservices_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="plugin" group="webservices" method="upgrade"> <name>Web Services - MokoJoomOpenGraph</name> - <version>01.00.01-dev</version> + <version>01.00.01-rc</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/src/pkg_mokoog.xml b/src/pkg_mokoog.xml index a68ab75..a0bee4c 100644 --- a/src/pkg_mokoog.xml +++ b/src/pkg_mokoog.xml @@ -8,7 +8,7 @@ <extension type="package" method="upgrade"> <name>Package - MokoJoomOpenGraph</name> <packagename>mokoog</packagename> - <version>01.00.01-dev</version> + <version>01.00.01-rc</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> From 5a6f315403af3378eb4c6f4f6b78f39a20065481 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" <gitea-actions[bot]@mokoconsulting.tech> Date: Sun, 31 May 2026 01:54:54 +0000 Subject: [PATCH 074/132] chore: update release-candidate channel 01.00.01-rc [skip ci] --- updates.xml | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/updates.xml b/updates.xml index 0534d44..fdd7eb4 100644 --- a/updates.xml +++ b/updates.xml @@ -1,7 +1,7 @@ <?xml version='1.0' encoding='UTF-8'?> <!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> SPDX-License-Identifier: GPL-3.0-or-later - VERSION: 01.00.01-dev + VERSION: 01.00.01 --> <updates> @@ -13,15 +13,34 @@ <client>site</client> <version>01.00.01-dev</version> <creationDate>2026-05-31</creationDate> - <infourl title='Package - MokoJoomOpenGraph'>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/releases/tag/development</infourl> + <infourl title="Package - MokoJoomOpenGraph">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/releases/tag/development</infourl> <downloads> - <downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/releases/download/development/pkg_mokoog-01.00.01-dev.zip</downloadurl> + <downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/releases/download/development/pkg_mokoog-01.00.01-dev.zip</downloadurl> </downloads> <sha256>183fde7dcc8e6c00a4cf063165556d5548f4ea5c553be7c2efa7e7e073866403</sha256> <tags><tag>dev</tag></tags> <changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/raw/branch/main/CHANGELOG.md</changelogurl> <maintainer>Moko Consulting</maintainer> <maintainerurl>https://mokoconsulting.tech</maintainerurl> + <targetplatform name="joomla" version="(5|6)\..*"/> + </update> + <update> + <name>Package - MokoJoomOpenGraph</name> + <description>Package - MokoJoomOpenGraph release-candidate build.</description> + <element>pkg_mokoog</element> + <type>package</type> + <client>site</client> + <version>01.00.01</version> + <creationDate>2026-05-31</creationDate> + <infourl title='Package - MokoJoomOpenGraph'>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/releases/tag/release-candidate</infourl> + <downloads> + <downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/releases/download/release-candidate/pkg_mokoog-01.00.01.zip</downloadurl> + </downloads> + <sha256>6fe8c8da51782b4f1a57d0acb9529f3e7b22471445119bf61a8de7d682be8585</sha256> + <tags><tag>release-candidate</tag></tags> + <changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/raw/branch/main/CHANGELOG.md</changelogurl> + <maintainer>Moko Consulting</maintainer> + <maintainerurl>https://mokoconsulting.tech</maintainerurl> <targetplatform name="joomla" version="(5|6)\..*" /> </update> </updates> From 073e24bf4e787130417fe8109e75bf5dc023d56d Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sat, 30 May 2026 20:57:29 -0500 Subject: [PATCH 075/132] revert(ci): restore pre-release.yml to upstream template RC-on-PR-to-main is already handled by auto-release.yml (promote-rc job). pre-release.yml only needs to handle dev releases (PR merged to dev) and manual dispatch. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- .mokogitea/workflows/pre-release.yml | 37 ++++------------------------ 1 file changed, 5 insertions(+), 32 deletions(-) diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml index a85031a..162b08f 100644 --- a/.mokogitea/workflows/pre-release.yml +++ b/.mokogitea/workflows/pre-release.yml @@ -14,10 +14,9 @@ name: "Universal: Pre-Release" on: pull_request: - types: [opened, ready_for_review, closed] + types: [closed] branches: - dev - - main workflow_dispatch: inputs: stability: @@ -40,12 +39,11 @@ env: jobs: build: - name: "Build Pre-Release (${{ inputs.stability || (github.event.pull_request.base.ref == 'main' && 'release-candidate') || 'development' }})" + name: "Build Pre-Release (${{ inputs.stability || 'development' }})" runs-on: release if: >- github.event_name == 'workflow_dispatch' || - (github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev') || - (github.event.action != 'closed' && github.event.pull_request.base.ref == 'main') + (github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev') steps: - name: Checkout @@ -75,35 +73,10 @@ jobs: run: | php ${MOKO_CLI}/manifest_read.php --path . --github-output - - name: Rename branch to rc on PR draft to main - if: >- - github.event.action != 'closed' && - github.event.pull_request.base.ref == 'main' - run: | - HEAD_BRANCH="${{ github.event.pull_request.head.ref }}" - if [ "$HEAD_BRANCH" != "rc" ]; then - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" - git checkout -b rc - git push origin rc 2>&1 || true - # Update PR head branch via API - curl -s -X PATCH \ - -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \ - -H "Content-Type: application/json" \ - -d "{\"head\": \"rc\"}" \ - "${GITEA_URL}/api/v1/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}" || true - fi - - name: Resolve metadata and bump version id: meta run: | - # Auto-detect RC when PR targets main - if [ "${{ github.event.pull_request.base.ref }}" = "main" ] && [ "${{ github.event.action }}" != "closed" ]; then - STABILITY="release-candidate" - else - STABILITY="${{ inputs.stability || 'development' }}" - fi + STABILITY="${{ inputs.stability || 'development' }}" case "$STABILITY" in development) SUFFIX="-dev"; TAG="development" ;; @@ -169,7 +142,7 @@ jobs: php ${MOKO_CLI}/release_create.php \ --path . --version "$VERSION" --tag "$TAG" \ --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ - --repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease + --repo "${GITEA_REPO}" --branch dev --prerelease - name: Build package and upload id: package From 5cbd0b64d4af9ef804b50e5a4331cab2da23edaa Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sat, 30 May 2026 21:05:26 -0500 Subject: [PATCH 076/132] =?UTF-8?q?refactor(ci):=20simplify=20workflows=20?= =?UTF-8?q?=E2=80=94=20merge=20update-server=20into=20pre-release,=20remov?= =?UTF-8?q?e=20redundancy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changes: - Delete cascade-dev.yml (disabled; auto-release Step 11 handles dev recreation) - Delete update-server.yml (merged into pre-release.yml) - Consolidate pre-release.yml: now handles push triggers on dev/alpha/beta/rc branches, PR merged to dev, manual dispatch, SFTP deploy, and updates.xml sync - Remove pre-release RC trigger from pr-check.yml (auto-release.yml handles RC via promote-rc job on PR opened to main) - Restrict repo-health.yml to manual dispatch only (was noisy on every push/PR) Workflow count: 12 → 10 Before: pre-release.yml — PR merged to dev + manual update-server.yml — push to dev + PR merged to dev + manual + SFTP deploy pr-check.yml — branch policy + validate + trigger pre-release RC cascade-dev.yml — disabled repo-health.yml — push + PR + manual After: pre-release.yml — push to dev + PR merged to dev + manual + SFTP deploy pr-check.yml — branch policy + validate (no RC trigger) repo-health.yml — manual only Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- .mokogitea/workflows/cascade-dev.yml | 10 - .mokogitea/workflows/pr-check.yml | 17 -- .mokogitea/workflows/pre-release.yml | 142 ++++++++--- .mokogitea/workflows/repo-health.yml | 2 - .mokogitea/workflows/update-server.yml | 312 ------------------------- 5 files changed, 109 insertions(+), 374 deletions(-) delete mode 100644 .mokogitea/workflows/cascade-dev.yml delete mode 100644 .mokogitea/workflows/update-server.yml diff --git a/.mokogitea/workflows/cascade-dev.yml b/.mokogitea/workflows/cascade-dev.yml deleted file mode 100644 index 5f7c1d7..0000000 --- a/.mokogitea/workflows/cascade-dev.yml +++ /dev/null @@ -1,10 +0,0 @@ -# DISABLED — auto-release Step 11 recreates dev from main after every release. -# Cascade-dev is redundant and causes version conflicts when both main and dev -# have different version numbers in templateDetails.xml / manifest.xml. -name: "Cascade Main → Dev (DISABLED)" -on: workflow_dispatch -jobs: - noop: - runs-on: ubuntu-latest - steps: - - run: echo "Cascade disabled — auto-release handles dev recreation" diff --git a/.mokogitea/workflows/pr-check.yml b/.mokogitea/workflows/pr-check.yml index ce64a27..39d5623 100644 --- a/.mokogitea/workflows/pr-check.yml +++ b/.mokogitea/workflows/pr-check.yml @@ -217,20 +217,3 @@ jobs: echo "Source: ${FILE_COUNT} files" [ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; } - # ── Pre-Release RC Build ───────────────────────────────────────────────── - pre-release: - name: Build RC Package - runs-on: ubuntu-latest - needs: [branch-policy, validate] - - steps: - - name: Trigger RC pre-release - env: - GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - REPO: ${{ github.repository }} - BRANCH: ${{ github.head_ref }} - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - run: | - curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}" - echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY - echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml index 162b08f..b6f7e27 100644 --- a/.mokogitea/workflows/pre-release.yml +++ b/.mokogitea/workflows/pre-release.yml @@ -7,12 +7,28 @@ # INGROUP: moko-platform.Release # REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform # PATH: /templates/workflows/universal/pre-release.yml.template -# VERSION: 05.01.00 -# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch +# VERSION: 06.00.00 +# BRIEF: Pre-release pipeline — builds dev/alpha/beta/rc packages, updates update server +# +# Consolidates the former pre-release.yml + update-server.yml into one workflow. +# Triggers: +# - Push to dev/alpha/beta/rc branches (src/ or htdocs/ changes) +# - PR merged into dev (any source branch) +# - Manual dispatch with stability choice name: "Universal: Pre-Release" on: + push: + branches: + - 'dev' + - 'dev/**' + - 'alpha/**' + - 'beta/**' + - 'rc/**' + paths: + - 'src/**' + - 'htdocs/**' pull_request: types: [closed] branches: @@ -33,6 +49,7 @@ permissions: contents: write 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 }} @@ -43,6 +60,7 @@ jobs: runs-on: release if: >- github.event_name == 'workflow_dispatch' || + github.event_name == 'push' || (github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev') steps: @@ -56,11 +74,11 @@ jobs: 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-api git clone --depth 1 --branch main --quiet \ "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ @@ -76,7 +94,25 @@ jobs: - name: Resolve metadata and bump version id: meta run: | - STABILITY="${{ inputs.stability || 'development' }}" + 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" + + # Determine stability from manual input, branch name, or default + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + STABILITY="${{ inputs.stability }}" + elif [[ "$BRANCH" == rc/* ]]; then + STABILITY="release-candidate" + elif [[ "$BRANCH" == beta/* ]]; then + STABILITY="beta" + elif [[ "$BRANCH" == alpha/* ]]; then + STABILITY="alpha" + else + STABILITY="development" + fi case "$STABILITY" in development) SUFFIX="-dev"; TAG="development" ;; @@ -85,40 +121,27 @@ jobs: release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;; esac - # Read current version (bump already handled by push workflow) + # Read current version VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null) [ -z "$VERSION" ] && VERSION="00.00.01" - # Strip any existing suffix from version before applying stability + # Strip any existing suffix before applying stability VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//') + # Propagate version with stability suffix to all manifest files php ${MOKO_CLI}/version_set_platform.php \ - --path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true - - # Verify version consistency across all files + --path . --version "$VERSION" --branch "$BRANCH" --stability "$STABILITY" 2>/dev/null || true php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true - # Update VERSION variable with suffix if [ -n "$SUFFIX" ]; then VERSION="${VERSION}${SUFFIX}" fi - # Commit version bump - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" - git add -A - git diff --cached --quiet || { - git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]" - git push origin HEAD 2>&1 - } - # Auto-detect element via manifest_element.php php ${MOKO_CLI}/manifest_element.php \ --path . --version "$VERSION" --stability "$STABILITY" \ --repo "${GITEA_REPO}" --github-output - # Read back element outputs EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') @@ -131,7 +154,14 @@ jobs: echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT" echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT" - echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ===" + # Commit version changes + git add -A + git diff --cached --quiet || { + git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]" + git push origin HEAD 2>&1 + } + + echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION} ===" - name: Create release id: release @@ -142,7 +172,7 @@ jobs: php ${MOKO_CLI}/release_create.php \ --path . --version "$VERSION" --tag "$TAG" \ --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ - --repo "${GITEA_REPO}" --branch dev --prerelease + --repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease - name: Build package and upload id: package @@ -175,21 +205,16 @@ jobs: --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \ ${SHA_FLAG} - # Commit and push if ! git diff --quiet updates.xml 2>/dev/null; then - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" git add updates.xml git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]" git push origin HEAD 2>&1 || echo "WARNING: push failed" fi - - name: "Sync updates.xml to all branches" + - name: Sync updates.xml to all branches if: steps.platform.outputs.platform == 'joomla' run: | CURRENT_BRANCH="${{ github.ref_name }}" - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" for BRANCH in main dev; do [ "$BRANCH" = "$CURRENT_BRANCH" ] && continue @@ -205,17 +230,68 @@ jobs: git checkout "${CURRENT_BRANCH}" 2>/dev/null done - - name: "Delete lesser pre-release channels (cascade)" + - name: Delete lesser pre-release channels continue-on-error: true run: | API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" - php ${MOKO_CLI}/release_cascade.php \ --stability "${{ steps.meta.outputs.stability }}" \ - --token "${TOKEN}" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" \ --api-base "${API_BASE}" + - 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: | + 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: | diff --git a/.mokogitea/workflows/repo-health.yml b/.mokogitea/workflows/repo-health.yml index be52e37..b34d35d 100644 --- a/.mokogitea/workflows/repo-health.yml +++ b/.mokogitea/workflows/repo-health.yml @@ -33,8 +33,6 @@ on: - release - scripts - repo - pull_request: - push: permissions: contents: read diff --git a/.mokogitea/workflows/update-server.yml b/.mokogitea/workflows/update-server.yml deleted file mode 100644 index 339d3f5..0000000 --- a/.mokogitea/workflows/update-server.yml +++ /dev/null @@ -1,312 +0,0 @@ -# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: moko-platform.Universal -# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform -# PATH: /templates/workflows/update-server.yml -# VERSION: 05.00.00 -# BRIEF: Pre-release build + update server XML for dev/alpha/beta/rc branches -# -# Thin wrapper around moko-platform CLI tools. -# Builds packages, updates updates.xml, and optionally deploys via SFTP. -# -# Joomla filters update entries by the user's "Minimum Stability" setting. - -name: "Update Server" - -on: - push: - branches: - - 'dev' - - 'dev/**' - - 'alpha/**' - - 'beta/**' - - 'rc/**' - paths: - - 'src/**' - - 'htdocs/**' - pull_request: - types: [closed] - branches: - - 'dev' - - 'dev/**' - - 'alpha/**' - - 'beta/**' - - 'rc/**' - paths: - - 'src/**' - - 'htdocs/**' - workflow_dispatch: - inputs: - stability: - description: 'Stability tag' - required: true - default: 'development' - type: choice - options: - - development - - alpha - - beta - - rc - - stable - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} - GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} - -permissions: - contents: write - -jobs: - update-xml: - name: Update Server - runs-on: release - if: >- - github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || github.event_name == 'push' - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - token: ${{ secrets.MOKOGITEA_TOKEN }} - fetch-depth: 0 - - - name: Setup moko-platform tools - env: - MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting - COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.MOKOGITEA_TOKEN }}"}}}' - run: | - if ! command -v composer &> /dev/null; then - sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 - fi - # Always fetch latest CLI tools — never use stale cache from previous runs - rm -rf /tmp/moko-platform - git clone --depth 1 --branch main --quiet \ - "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ - /tmp/moko-platform 2>/dev/null || true - if [ -d "/tmp/moko-platform" ] && [ -f "/tmp/moko-platform/composer.json" ]; then - cd /tmp/moko-platform && composer install --no-dev --no-interaction --quiet 2>/dev/null || true - fi - echo "MOKO_CLI=/tmp/moko-platform/cli" >> "$GITHUB_ENV" - - - name: Detect platform - id: platform - run: php ${MOKO_CLI}/manifest_read.php --path . --github-output - - - name: Resolve stability and bump version - id: meta - run: | - BRANCH="${{ github.ref_name }}" - - # Configure git for bot pushes - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" - - # Auto-bump patch version - php ${MOKO_CLI}/version_bump.php --path . 2>/dev/null || true - - VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "0.0.0") - - # Strip any existing suffix before applying stability - VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//') - - # Determine stability from branch or manual input - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - STABILITY="${{ inputs.stability }}" - elif [[ "$BRANCH" == rc/* ]]; then - STABILITY="rc" - elif [[ "$BRANCH" == beta/* ]]; then - STABILITY="beta" - elif [[ "$BRANCH" == alpha/* ]]; then - STABILITY="alpha" - else - STABILITY="development" - fi - - # Version suffix per stability stream - case "$STABILITY" in - development) SUFFIX="-dev"; TAG="development" ;; - alpha) SUFFIX="-alpha"; TAG="alpha" ;; - beta) SUFFIX="-beta"; TAG="beta" ;; - rc) SUFFIX="-rc"; TAG="release-candidate" ;; - *) SUFFIX=""; TAG="stable" ;; - esac - - # Propagate version with stability suffix to all manifest files - php ${MOKO_CLI}/version_set_platform.php \ - --path . --version "$VERSION" --branch "$BRANCH" --stability "$STABILITY" 2>/dev/null || true - php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true - - # Re-read version (now includes suffix from version_set_platform) - if [ -n "$SUFFIX" ]; then - VERSION="${VERSION}${SUFFIX}" - fi - - echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT" - echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT" - echo "tag=${TAG}" >> "$GITHUB_OUTPUT" - echo "display_version=${VERSION}" >> "$GITHUB_OUTPUT" - - # Commit version bump if changed - git add -A - git diff --cached --quiet || { - git commit -m "chore(version): auto-bump ${VERSION} [skip ci]" \ - --author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" - git push - } - - - name: Create release and upload package - id: package - run: | - VERSION="${{ steps.meta.outputs.version }}" - TAG="${{ steps.meta.outputs.tag }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - - # Create or update Gitea release - php ${MOKO_CLI}/release_create.php \ - --path . --version "$VERSION" --tag "$TAG" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ - --repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease - - # Build package and upload - php ${MOKO_CLI}/release_package.php \ - --path . --version "$VERSION" --tag "$TAG" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ - --repo "${GITEA_REPO}" --output /tmp || true - - - name: Update updates.xml - if: steps.platform.outputs.platform == 'joomla' - run: | - VERSION="${{ steps.meta.outputs.version }}" - STABILITY="${{ steps.meta.outputs.stability }}" - SHA256="${{ steps.package.outputs.sha256_zip }}" - - if [ ! -f "updates.xml" ]; then - echo "No updates.xml — skipping" - exit 0 - fi - - SHA_FLAG="" - [ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}" - - php ${MOKO_CLI}/updates_xml_build.php \ - --path . --version "${VERSION}" --stability "${STABILITY}" \ - --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \ - ${SHA_FLAG} - - # Commit and push updates.xml - git add updates.xml - git diff --cached --quiet || { - git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]" - git push - } - - - name: Sync updates.xml to main - if: github.ref_name != 'main' && steps.platform.outputs.platform == 'joomla' - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - GITEA_TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" - - FILE_SHA=$(curl -sf -H "Authorization: token ${GITEA_TOKEN}" \ - "${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true) - - if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then - python3 -c " - import base64, json, urllib.request, sys - with open('updates.xml', 'rb') as f: - content = base64.b64encode(f.read()).decode() - payload = json.dumps({ - 'content': content, - 'sha': '${FILE_SHA}', - 'message': 'chore: sync updates.xml from ${{ steps.meta.outputs.stability }} [skip ci]', - 'branch': 'main' - }).encode() - req = urllib.request.Request( - '${API_BASE}/contents/updates.xml', - data=payload, method='PUT', - headers={ - 'Authorization': 'token ${GITEA_TOKEN}', - 'Content-Type': 'application/json' - }) - try: - urllib.request.urlopen(req) - print('updates.xml synced to main') - except Exception as e: - print(f'WARNING: sync to main failed: {e}', file=sys.stderr) - " - fi - - - name: SFTP deploy to dev server - if: contains(github.ref, 'dev/') || github.ref == 'refs/heads/dev' - env: - DEV_HOST: ${{ vars.DEV_FTP_HOST }} - DEV_PATH: ${{ vars.DEV_FTP_PATH }} - DEV_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }} - DEV_USER: ${{ vars.DEV_FTP_USERNAME }} - DEV_PORT: ${{ vars.DEV_FTP_PORT }} - DEV_KEY: ${{ secrets.DEV_FTP_KEY }} - DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }} - run: | - # Permission check: admin or maintain role required - ACTOR="${{ github.actor }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - - PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \ - "${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \ - python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read") - case "$PERMISSION" in - admin|maintain|write) ;; - *) - echo "Deploy denied: ${ACTOR} has '${PERMISSION}' — requires admin, maintain, or write" - exit 0 - ;; - esac - - [ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured — skipping SFTP"; exit 0; } - - SOURCE_DIR="src" - [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" - [ ! -d "$SOURCE_DIR" ] && exit 0 - - PORT="${DEV_PORT:-22}" - REMOTE="${DEV_PATH%/}" - [ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}" - - printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \ - "$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json - if [ -n "$DEV_KEY" ]; then - echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key - printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json - else - printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json - fi - - PLATFORM=$(php ${MOKO_CLI}/platform_detect.php --path . 2>/dev/null || true) - if [ "$PLATFORM" = "waas-component" ] && [ -f "${MOKO_CLI}/../deploy/deploy-joomla.php" ]; then - php ${MOKO_CLI}/../deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json - elif [ -f "${MOKO_CLI}/../deploy/deploy-sftp.php" ]; then - php ${MOKO_CLI}/../deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json - fi - rm -f /tmp/deploy_key /tmp/sftp-config.json - echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY - - - name: Summary - if: always() - run: | - VERSION="${{ steps.meta.outputs.version }}" - STABILITY="${{ steps.meta.outputs.stability }}" - DISPLAY="${{ steps.meta.outputs.display_version }}" - echo "## Update Server" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY - echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY - echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Version | \`${DISPLAY}\` |" >> $GITHUB_STEP_SUMMARY From ad09eae874b6ef60c68cacedc5fd5a32e77e126e Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sat, 30 May 2026 21:16:20 -0500 Subject: [PATCH 077/132] fix(ci): always clone fresh moko-platform tools, fix composer constraint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - auto-bump.yml: remove stale /opt/moko-platform check, always clone fresh from git (matches pre-release.yml pattern) - composer.json: fix joomla/coding-standards ^4.0 → ^3.0 (v4 doesn't exist on Packagist) Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- .mokogitea/workflows/auto-bump.yml | 19 ++++++++++--------- composer.json | 2 +- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/.mokogitea/workflows/auto-bump.yml b/.mokogitea/workflows/auto-bump.yml index 34953b1..731bd64 100644 --- a/.mokogitea/workflows/auto-bump.yml +++ b/.mokogitea/workflows/auto-bump.yml @@ -44,19 +44,20 @@ jobs: fetch-depth: 1 - name: Setup moko-platform tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting run: | if ! command -v composer &> /dev/null; then sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 fi - if [ -d "/opt/moko-platform/cli" ]; then - echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV" - else - git clone --depth 1 --branch main --quiet \ - "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \ - /tmp/moko-platform-api - cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet - echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV" - fi + # Always fetch latest CLI tools — never use stale cache from previous runs + rm -rf /tmp/moko-platform-api + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ + /tmp/moko-platform-api + cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet + echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV" - name: Bump version run: | diff --git a/composer.json b/composer.json index 132b2d7..652d195 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "require-dev": { "squizlabs/php_codesniffer": "^3.7", "phpstan/phpstan": "^1.10", - "joomla/coding-standards": "^4.0" + "joomla/coding-standards": "^3.0" }, "config": { "sort-packages": true From e1747da3ff6192626f0361ac8932dc02f5fc7d43 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sat, 30 May 2026 21:23:40 -0500 Subject: [PATCH 078/132] fix: add minimum-stability alpha for joomla/coding-standards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit joomla/coding-standards has no stable release — only 3.0.0-alpha. Add minimum-stability: alpha with prefer-stable: true. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- composer.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/composer.json b/composer.json index 652d195..c21717f 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,8 @@ "phpstan/phpstan": "^1.10", "joomla/coding-standards": "^3.0" }, + "minimum-stability": "alpha", + "prefer-stable": true, "config": { "sort-packages": true } From 90e5f8c7176e2d0012abdd3bad71b0848bac9383 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sat, 30 May 2026 21:26:10 -0500 Subject: [PATCH 079/132] fix(ci): use GH_MIRROR_TOKEN for composer GitHub auth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MOKOGITEA_TOKEN is a Gitea token — cannot authenticate against github.com for Packagist downloads. Use GH_MIRROR_TOKEN (GitHub PAT) instead. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- .mokogitea/workflows/ci-joomla.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.mokogitea/workflows/ci-joomla.yml b/.mokogitea/workflows/ci-joomla.yml index 9c3d8e6..8cdf408 100644 --- a/.mokogitea/workflows/ci-joomla.yml +++ b/.mokogitea/workflows/ci-joomla.yml @@ -53,7 +53,7 @@ jobs: - name: Install dependencies env: - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}' + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN || github.token }}"}}' run: | if [ -f "composer.json" ]; then composer install \ @@ -346,7 +346,7 @@ jobs: - name: Install dependencies env: - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}' + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN || github.token }}"}}' run: | if [ -f "composer.json" ]; then composer install \ @@ -391,7 +391,7 @@ jobs: - name: Install dependencies env: - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}' + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN || github.token }}"}}' run: | if [ -f "composer.json" ]; then composer install --no-interaction --prefer-dist --optimize-autoloader From ba62e95e582205f3806be757cf3ac46e7f26ad87 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sat, 30 May 2026 21:41:46 -0500 Subject: [PATCH 080/132] fix(ci): skip namespace check for package manifests, secrets already set MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Package type extensions (pkg_*) don't have <namespace> tags — only component/plugin manifests do. Skip the check when type="package". Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- .mokogitea/workflows/ci-joomla.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.mokogitea/workflows/ci-joomla.yml b/.mokogitea/workflows/ci-joomla.yml index 8cdf408..2da3158 100644 --- a/.mokogitea/workflows/ci-joomla.yml +++ b/.mokogitea/workflows/ci-joomla.yml @@ -124,8 +124,13 @@ jobs: echo "Manifest is well-formed XML." >> $GITHUB_STEP_SUMMARY fi - # Check required tags: name, version, author, namespace (Joomla 5+) - for TAG in name version author namespace; do + # 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 if ! grep -q "<${TAG}>" "$MANIFEST" 2>/dev/null; then echo "Missing required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY ERRORS=$((ERRORS + 1)) From 8c0fcfe81ffff5f5e815a84f3f542ee4a97b3a9b Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sat, 30 May 2026 21:46:21 -0500 Subject: [PATCH 081/132] fix: add missing index.html to all extension directories MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Joomla security requirement — prevents directory listing on misconfigured servers. Added to all 57 directories that were missing them. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- src/index.html | 1 + src/language/en-GB/index.html | 1 + src/language/en-US/index.html | 1 + src/language/index.html | 1 + src/packages/com_mokoog/api/index.html | 1 + src/packages/com_mokoog/api/src/Controller/index.html | 1 + src/packages/com_mokoog/api/src/View/Tags/index.html | 1 + src/packages/com_mokoog/api/src/View/index.html | 1 + src/packages/com_mokoog/api/src/index.html | 1 + src/packages/com_mokoog/forms/index.html | 1 + src/packages/com_mokoog/index.html | 1 + src/packages/com_mokoog/language/en-GB/index.html | 1 + src/packages/com_mokoog/language/en-US/index.html | 1 + src/packages/com_mokoog/language/index.html | 1 + src/packages/com_mokoog/services/index.html | 1 + src/packages/com_mokoog/sql/index.html | 1 + src/packages/com_mokoog/sql/updates/index.html | 1 + src/packages/com_mokoog/sql/updates/mysql/index.html | 1 + src/packages/com_mokoog/src/ContentType/index.html | 1 + src/packages/com_mokoog/src/Controller/index.html | 1 + src/packages/com_mokoog/src/Extension/index.html | 1 + src/packages/com_mokoog/src/Field/index.html | 1 + src/packages/com_mokoog/src/Model/index.html | 1 + src/packages/com_mokoog/src/Service/index.html | 1 + src/packages/com_mokoog/src/Table/index.html | 1 + src/packages/com_mokoog/src/View/Tags/index.html | 1 + src/packages/com_mokoog/src/View/Tags/tmpl/index.html | 1 + src/packages/com_mokoog/src/View/index.html | 1 + src/packages/com_mokoog/src/index.html | 1 + src/packages/com_mokoog/tmpl/index.html | 1 + src/packages/com_mokoog/tmpl/tags/index.html | 1 + src/packages/index.html | 1 + src/packages/plg_content_mokoog/forms/index.html | 1 + src/packages/plg_content_mokoog/index.html | 1 + src/packages/plg_content_mokoog/language/en-GB/index.html | 1 + src/packages/plg_content_mokoog/language/en-US/index.html | 1 + src/packages/plg_content_mokoog/language/index.html | 1 + src/packages/plg_content_mokoog/media/css/index.html | 1 + src/packages/plg_content_mokoog/media/index.html | 1 + src/packages/plg_content_mokoog/media/js/index.html | 1 + src/packages/plg_content_mokoog/services/index.html | 1 + src/packages/plg_content_mokoog/src/Extension/index.html | 1 + src/packages/plg_content_mokoog/src/Field/index.html | 1 + src/packages/plg_content_mokoog/src/index.html | 1 + src/packages/plg_system_mokoog/index.html | 1 + src/packages/plg_system_mokoog/language/en-GB/index.html | 1 + src/packages/plg_system_mokoog/language/en-US/index.html | 1 + src/packages/plg_system_mokoog/language/index.html | 1 + src/packages/plg_system_mokoog/services/index.html | 1 + src/packages/plg_system_mokoog/src/Extension/index.html | 1 + src/packages/plg_system_mokoog/src/Helper/index.html | 1 + src/packages/plg_system_mokoog/src/index.html | 1 + src/packages/plg_webservices_mokoog/index.html | 1 + src/packages/plg_webservices_mokoog/language/en-GB/index.html | 1 + src/packages/plg_webservices_mokoog/language/en-US/index.html | 1 + src/packages/plg_webservices_mokoog/language/index.html | 1 + src/packages/plg_webservices_mokoog/services/index.html | 1 + src/packages/plg_webservices_mokoog/src/Extension/index.html | 1 + src/packages/plg_webservices_mokoog/src/index.html | 1 + 59 files changed, 59 insertions(+) create mode 100644 src/index.html create mode 100644 src/language/en-GB/index.html create mode 100644 src/language/en-US/index.html create mode 100644 src/language/index.html create mode 100644 src/packages/com_mokoog/api/index.html create mode 100644 src/packages/com_mokoog/api/src/Controller/index.html create mode 100644 src/packages/com_mokoog/api/src/View/Tags/index.html create mode 100644 src/packages/com_mokoog/api/src/View/index.html create mode 100644 src/packages/com_mokoog/api/src/index.html create mode 100644 src/packages/com_mokoog/forms/index.html create mode 100644 src/packages/com_mokoog/index.html create mode 100644 src/packages/com_mokoog/language/en-GB/index.html create mode 100644 src/packages/com_mokoog/language/en-US/index.html create mode 100644 src/packages/com_mokoog/language/index.html create mode 100644 src/packages/com_mokoog/services/index.html create mode 100644 src/packages/com_mokoog/sql/index.html create mode 100644 src/packages/com_mokoog/sql/updates/index.html create mode 100644 src/packages/com_mokoog/sql/updates/mysql/index.html create mode 100644 src/packages/com_mokoog/src/ContentType/index.html create mode 100644 src/packages/com_mokoog/src/Controller/index.html create mode 100644 src/packages/com_mokoog/src/Extension/index.html create mode 100644 src/packages/com_mokoog/src/Field/index.html create mode 100644 src/packages/com_mokoog/src/Model/index.html create mode 100644 src/packages/com_mokoog/src/Service/index.html create mode 100644 src/packages/com_mokoog/src/Table/index.html create mode 100644 src/packages/com_mokoog/src/View/Tags/index.html create mode 100644 src/packages/com_mokoog/src/View/Tags/tmpl/index.html create mode 100644 src/packages/com_mokoog/src/View/index.html create mode 100644 src/packages/com_mokoog/src/index.html create mode 100644 src/packages/com_mokoog/tmpl/index.html create mode 100644 src/packages/com_mokoog/tmpl/tags/index.html create mode 100644 src/packages/index.html create mode 100644 src/packages/plg_content_mokoog/forms/index.html create mode 100644 src/packages/plg_content_mokoog/index.html create mode 100644 src/packages/plg_content_mokoog/language/en-GB/index.html create mode 100644 src/packages/plg_content_mokoog/language/en-US/index.html create mode 100644 src/packages/plg_content_mokoog/language/index.html create mode 100644 src/packages/plg_content_mokoog/media/css/index.html create mode 100644 src/packages/plg_content_mokoog/media/index.html create mode 100644 src/packages/plg_content_mokoog/media/js/index.html create mode 100644 src/packages/plg_content_mokoog/services/index.html create mode 100644 src/packages/plg_content_mokoog/src/Extension/index.html create mode 100644 src/packages/plg_content_mokoog/src/Field/index.html create mode 100644 src/packages/plg_content_mokoog/src/index.html create mode 100644 src/packages/plg_system_mokoog/index.html create mode 100644 src/packages/plg_system_mokoog/language/en-GB/index.html create mode 100644 src/packages/plg_system_mokoog/language/en-US/index.html create mode 100644 src/packages/plg_system_mokoog/language/index.html create mode 100644 src/packages/plg_system_mokoog/services/index.html create mode 100644 src/packages/plg_system_mokoog/src/Extension/index.html create mode 100644 src/packages/plg_system_mokoog/src/Helper/index.html create mode 100644 src/packages/plg_system_mokoog/src/index.html create mode 100644 src/packages/plg_webservices_mokoog/index.html create mode 100644 src/packages/plg_webservices_mokoog/language/en-GB/index.html create mode 100644 src/packages/plg_webservices_mokoog/language/en-US/index.html create mode 100644 src/packages/plg_webservices_mokoog/language/index.html create mode 100644 src/packages/plg_webservices_mokoog/services/index.html create mode 100644 src/packages/plg_webservices_mokoog/src/Extension/index.html create mode 100644 src/packages/plg_webservices_mokoog/src/index.html diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/language/en-GB/index.html b/src/language/en-GB/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/language/en-GB/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/language/en-US/index.html b/src/language/en-US/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/language/en-US/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/language/index.html b/src/language/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/language/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/com_mokoog/api/index.html b/src/packages/com_mokoog/api/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/com_mokoog/api/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/com_mokoog/api/src/Controller/index.html b/src/packages/com_mokoog/api/src/Controller/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/com_mokoog/api/src/Controller/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/com_mokoog/api/src/View/Tags/index.html b/src/packages/com_mokoog/api/src/View/Tags/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/com_mokoog/api/src/View/Tags/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/com_mokoog/api/src/View/index.html b/src/packages/com_mokoog/api/src/View/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/com_mokoog/api/src/View/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/com_mokoog/api/src/index.html b/src/packages/com_mokoog/api/src/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/com_mokoog/api/src/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/com_mokoog/forms/index.html b/src/packages/com_mokoog/forms/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/com_mokoog/forms/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/com_mokoog/index.html b/src/packages/com_mokoog/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/com_mokoog/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/com_mokoog/language/en-GB/index.html b/src/packages/com_mokoog/language/en-GB/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/com_mokoog/language/en-GB/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/com_mokoog/language/en-US/index.html b/src/packages/com_mokoog/language/en-US/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/com_mokoog/language/en-US/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/com_mokoog/language/index.html b/src/packages/com_mokoog/language/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/com_mokoog/language/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/com_mokoog/services/index.html b/src/packages/com_mokoog/services/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/com_mokoog/services/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/com_mokoog/sql/index.html b/src/packages/com_mokoog/sql/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/com_mokoog/sql/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/com_mokoog/sql/updates/index.html b/src/packages/com_mokoog/sql/updates/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/com_mokoog/sql/updates/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/com_mokoog/sql/updates/mysql/index.html b/src/packages/com_mokoog/sql/updates/mysql/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/com_mokoog/sql/updates/mysql/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/com_mokoog/src/ContentType/index.html b/src/packages/com_mokoog/src/ContentType/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/com_mokoog/src/ContentType/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/com_mokoog/src/Controller/index.html b/src/packages/com_mokoog/src/Controller/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/com_mokoog/src/Controller/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/com_mokoog/src/Extension/index.html b/src/packages/com_mokoog/src/Extension/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/com_mokoog/src/Extension/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/com_mokoog/src/Field/index.html b/src/packages/com_mokoog/src/Field/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/com_mokoog/src/Field/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/com_mokoog/src/Model/index.html b/src/packages/com_mokoog/src/Model/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/com_mokoog/src/Model/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/com_mokoog/src/Service/index.html b/src/packages/com_mokoog/src/Service/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/com_mokoog/src/Service/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/com_mokoog/src/Table/index.html b/src/packages/com_mokoog/src/Table/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/com_mokoog/src/Table/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/com_mokoog/src/View/Tags/index.html b/src/packages/com_mokoog/src/View/Tags/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/com_mokoog/src/View/Tags/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/com_mokoog/src/View/Tags/tmpl/index.html b/src/packages/com_mokoog/src/View/Tags/tmpl/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/com_mokoog/src/View/Tags/tmpl/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/com_mokoog/src/View/index.html b/src/packages/com_mokoog/src/View/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/com_mokoog/src/View/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/com_mokoog/src/index.html b/src/packages/com_mokoog/src/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/com_mokoog/src/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/com_mokoog/tmpl/index.html b/src/packages/com_mokoog/tmpl/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/com_mokoog/tmpl/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/com_mokoog/tmpl/tags/index.html b/src/packages/com_mokoog/tmpl/tags/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/com_mokoog/tmpl/tags/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/index.html b/src/packages/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/plg_content_mokoog/forms/index.html b/src/packages/plg_content_mokoog/forms/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/plg_content_mokoog/forms/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/plg_content_mokoog/index.html b/src/packages/plg_content_mokoog/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/plg_content_mokoog/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/plg_content_mokoog/language/en-GB/index.html b/src/packages/plg_content_mokoog/language/en-GB/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/plg_content_mokoog/language/en-GB/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/plg_content_mokoog/language/en-US/index.html b/src/packages/plg_content_mokoog/language/en-US/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/plg_content_mokoog/language/en-US/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/plg_content_mokoog/language/index.html b/src/packages/plg_content_mokoog/language/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/plg_content_mokoog/language/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/plg_content_mokoog/media/css/index.html b/src/packages/plg_content_mokoog/media/css/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/plg_content_mokoog/media/css/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/plg_content_mokoog/media/index.html b/src/packages/plg_content_mokoog/media/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/plg_content_mokoog/media/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/plg_content_mokoog/media/js/index.html b/src/packages/plg_content_mokoog/media/js/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/plg_content_mokoog/media/js/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/plg_content_mokoog/services/index.html b/src/packages/plg_content_mokoog/services/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/plg_content_mokoog/services/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/plg_content_mokoog/src/Extension/index.html b/src/packages/plg_content_mokoog/src/Extension/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/plg_content_mokoog/src/Extension/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/plg_content_mokoog/src/Field/index.html b/src/packages/plg_content_mokoog/src/Field/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/plg_content_mokoog/src/Field/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/plg_content_mokoog/src/index.html b/src/packages/plg_content_mokoog/src/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/plg_content_mokoog/src/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/plg_system_mokoog/index.html b/src/packages/plg_system_mokoog/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/plg_system_mokoog/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/plg_system_mokoog/language/en-GB/index.html b/src/packages/plg_system_mokoog/language/en-GB/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/plg_system_mokoog/language/en-GB/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/plg_system_mokoog/language/en-US/index.html b/src/packages/plg_system_mokoog/language/en-US/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/plg_system_mokoog/language/en-US/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/plg_system_mokoog/language/index.html b/src/packages/plg_system_mokoog/language/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/plg_system_mokoog/language/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/plg_system_mokoog/services/index.html b/src/packages/plg_system_mokoog/services/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/plg_system_mokoog/services/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/plg_system_mokoog/src/Extension/index.html b/src/packages/plg_system_mokoog/src/Extension/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/plg_system_mokoog/src/Extension/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/plg_system_mokoog/src/Helper/index.html b/src/packages/plg_system_mokoog/src/Helper/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/plg_system_mokoog/src/Helper/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/plg_system_mokoog/src/index.html b/src/packages/plg_system_mokoog/src/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/plg_system_mokoog/src/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/plg_webservices_mokoog/index.html b/src/packages/plg_webservices_mokoog/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/plg_webservices_mokoog/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/plg_webservices_mokoog/language/en-GB/index.html b/src/packages/plg_webservices_mokoog/language/en-GB/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/plg_webservices_mokoog/language/en-GB/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/plg_webservices_mokoog/language/en-US/index.html b/src/packages/plg_webservices_mokoog/language/en-US/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/plg_webservices_mokoog/language/en-US/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/plg_webservices_mokoog/language/index.html b/src/packages/plg_webservices_mokoog/language/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/plg_webservices_mokoog/language/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/plg_webservices_mokoog/services/index.html b/src/packages/plg_webservices_mokoog/services/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/plg_webservices_mokoog/services/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/plg_webservices_mokoog/src/Extension/index.html b/src/packages/plg_webservices_mokoog/src/Extension/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/plg_webservices_mokoog/src/Extension/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> diff --git a/src/packages/plg_webservices_mokoog/src/index.html b/src/packages/plg_webservices_mokoog/src/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/plg_webservices_mokoog/src/index.html @@ -0,0 +1 @@ +<html><body bgcolor="#FFFFFF"></body></html> From 01055aa8442c085f6f71136546b2cfceab767f7c Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" <gitea-actions[bot]@mokoconsulting.tech> Date: Sun, 31 May 2026 02:46:34 +0000 Subject: [PATCH 082/132] chore(version): pre-release bump to 01.00.01-dev [skip ci] --- src/packages/com_mokoog/mokoog.xml | 2 +- src/packages/plg_content_mokoog/mokoog.xml | 2 +- src/packages/plg_system_mokoog/mokoog.xml | 2 +- src/packages/plg_webservices_mokoog/mokoog.xml | 2 +- src/pkg_mokoog.xml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/packages/com_mokoog/mokoog.xml b/src/packages/com_mokoog/mokoog.xml index 6ede7db..1bccaae 100644 --- a/src/packages/com_mokoog/mokoog.xml +++ b/src/packages/com_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="component" method="upgrade"> <name>com_mokoog</name> - <version>01.00.01-rc</version> + <version>01.00.01-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/src/packages/plg_content_mokoog/mokoog.xml b/src/packages/plg_content_mokoog/mokoog.xml index 458438c..1d1e2e1 100644 --- a/src/packages/plg_content_mokoog/mokoog.xml +++ b/src/packages/plg_content_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="plugin" group="content" method="upgrade"> <name>Content - MokoJoomOpenGraph</name> - <version>01.00.01-rc</version> + <version>01.00.01-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/src/packages/plg_system_mokoog/mokoog.xml b/src/packages/plg_system_mokoog/mokoog.xml index 25c8ff8..2b1246f 100644 --- a/src/packages/plg_system_mokoog/mokoog.xml +++ b/src/packages/plg_system_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="plugin" group="system" method="upgrade"> <name>System - MokoJoomOpenGraph</name> - <version>01.00.01-rc</version> + <version>01.00.01-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/src/packages/plg_webservices_mokoog/mokoog.xml b/src/packages/plg_webservices_mokoog/mokoog.xml index 8b42f6c..08008aa 100644 --- a/src/packages/plg_webservices_mokoog/mokoog.xml +++ b/src/packages/plg_webservices_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="plugin" group="webservices" method="upgrade"> <name>Web Services - MokoJoomOpenGraph</name> - <version>01.00.01-rc</version> + <version>01.00.01-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/src/pkg_mokoog.xml b/src/pkg_mokoog.xml index a0bee4c..a68ab75 100644 --- a/src/pkg_mokoog.xml +++ b/src/pkg_mokoog.xml @@ -8,7 +8,7 @@ <extension type="package" method="upgrade"> <name>Package - MokoJoomOpenGraph</name> <packagename>mokoog</packagename> - <version>01.00.01-rc</version> + <version>01.00.01-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> From 2dcc6860c2c73d4b66ebd552607a771cc34cec94 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" <gitea-actions[bot]@mokoconsulting.tech> Date: Sun, 31 May 2026 02:46:37 +0000 Subject: [PATCH 083/132] chore: update development channel 01.00.01-dev [skip ci] --- updates.xml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/updates.xml b/updates.xml index fdd7eb4..084618e 100644 --- a/updates.xml +++ b/updates.xml @@ -1,7 +1,7 @@ <?xml version='1.0' encoding='UTF-8'?> <!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> SPDX-License-Identifier: GPL-3.0-or-later - VERSION: 01.00.01 + VERSION: 01.00.01-dev --> <updates> @@ -13,16 +13,16 @@ <client>site</client> <version>01.00.01-dev</version> <creationDate>2026-05-31</creationDate> - <infourl title="Package - MokoJoomOpenGraph">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/releases/tag/development</infourl> + <infourl title='Package - MokoJoomOpenGraph'>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/releases/tag/development</infourl> <downloads> - <downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/releases/download/development/pkg_mokoog-01.00.01-dev.zip</downloadurl> + <downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/releases/download/development/pkg_mokoog-01.00.01-dev.zip</downloadurl> </downloads> - <sha256>183fde7dcc8e6c00a4cf063165556d5548f4ea5c553be7c2efa7e7e073866403</sha256> + <sha256>1dd3a62c6a4cbec7406090e5618fb8b618b2d9a2b22bb7720fbaf38a681bb340</sha256> <tags><tag>dev</tag></tags> <changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/raw/branch/main/CHANGELOG.md</changelogurl> <maintainer>Moko Consulting</maintainer> <maintainerurl>https://mokoconsulting.tech</maintainerurl> - <targetplatform name="joomla" version="(5|6)\..*"/> + <targetplatform name="joomla" version="(5|6)\..*" /> </update> <update> <name>Package - MokoJoomOpenGraph</name> @@ -32,15 +32,15 @@ <client>site</client> <version>01.00.01</version> <creationDate>2026-05-31</creationDate> - <infourl title='Package - MokoJoomOpenGraph'>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/releases/tag/release-candidate</infourl> + <infourl title="Package - MokoJoomOpenGraph">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/releases/tag/release-candidate</infourl> <downloads> - <downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/releases/download/release-candidate/pkg_mokoog-01.00.01.zip</downloadurl> + <downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/releases/download/release-candidate/pkg_mokoog-01.00.01.zip</downloadurl> </downloads> <sha256>6fe8c8da51782b4f1a57d0acb9529f3e7b22471445119bf61a8de7d682be8585</sha256> <tags><tag>release-candidate</tag></tags> <changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/raw/branch/main/CHANGELOG.md</changelogurl> <maintainer>Moko Consulting</maintainer> <maintainerurl>https://mokoconsulting.tech</maintainerurl> - <targetplatform name="joomla" version="(5|6)\..*" /> + <targetplatform name="joomla" version="(5|6)\..*"/> </update> </updates> From d001ef7c3585c5da11f753d2bceed0765650e0ad Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sat, 30 May 2026 21:49:52 -0500 Subject: [PATCH 084/132] fix(ci): support HTML comment VERSION format, add en-GB/en-US check - Release readiness: support <!-- VERSION: XX.YY.ZZ --> format in README.md (not just FILE INFORMATION block format) - Add language directory check: verify en-GB and en-US exist in all language/ directories under src/ or htdocs/ Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- .mokogitea/workflows/ci-joomla.yml | 35 ++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/.mokogitea/workflows/ci-joomla.yml b/.mokogitea/workflows/ci-joomla.yml index 2da3158..7090e40 100644 --- a/.mokogitea/workflows/ci-joomla.yml +++ b/.mokogitea/workflows/ci-joomla.yml @@ -202,6 +202,37 @@ 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 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 @@ -245,8 +276,8 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY ERRORS=0 - # Extract version from README.md - README_VERSION=$(grep -oP '^\s*VERSION:\s*\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' README.md | head -1) + # Extract version from README.md (supports both FILE INFORMATION block and HTML comment format) + README_VERSION=$(grep -oP '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)) From 23caf15df64ac1dc22dc9b43932a4cf84f011a10 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sat, 30 May 2026 21:59:55 -0500 Subject: [PATCH 085/132] fix(ci): replace PCRE grep with sed for Alpine compat Alpine Linux grep doesn't support -P (PCRE). Use sed for VERSION and manifest version extraction. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- .mokogitea/workflows/ci-joomla.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.mokogitea/workflows/ci-joomla.yml b/.mokogitea/workflows/ci-joomla.yml index 7090e40..f679e86 100644 --- a/.mokogitea/workflows/ci-joomla.yml +++ b/.mokogitea/workflows/ci-joomla.yml @@ -277,7 +277,7 @@ jobs: ERRORS=0 # Extract version from README.md (supports both FILE INFORMATION block and HTML comment format) - README_VERSION=$(grep -oP 'VERSION:\s*\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' README.md | head -1) + README_VERSION=$(sed -n 's/.*VERSION:\s*\([0-9]\{2\}\.[0-9]\{2\}\.[0-9]\{2\}\).*/\1/p' 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 +301,7 @@ jobs: echo "Manifest: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY # Check <version> matches README VERSION - MANIFEST_VERSION=$(grep -oP '<version>\K[^<]+' "$MANIFEST" | head -1) + MANIFEST_VERSION=$(sed -n 's/.*<version>\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1) if [ -z "$MANIFEST_VERSION" ]; then echo "No \`<version>\` tag in manifest." >> $GITHUB_STEP_SUMMARY ERRORS=$((ERRORS + 1)) From 87fc0930a0d2a6210018529f50d89b54f4a859f7 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sat, 30 May 2026 22:35:14 -0500 Subject: [PATCH 086/132] chore(ci): remove release workflows for update server migration Delete auto-release.yml, pre-release.yml, and auto-bump.yml in preparation for the new update server system. Remaining workflows: ci-joomla, cleanup, gitleaks, notify, pr-check, repo-health, security-audit (7 total). Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- .mokogitea/workflows/auto-bump.yml | 67 ------ .mokogitea/workflows/auto-release.yml | 270 ---------------------- .mokogitea/workflows/pre-release.yml | 309 -------------------------- 3 files changed, 646 deletions(-) delete mode 100644 .mokogitea/workflows/auto-bump.yml delete mode 100644 .mokogitea/workflows/auto-release.yml delete mode 100644 .mokogitea/workflows/pre-release.yml diff --git a/.mokogitea/workflows/auto-bump.yml b/.mokogitea/workflows/auto-bump.yml deleted file mode 100644 index 731bd64..0000000 --- a/.mokogitea/workflows/auto-bump.yml +++ /dev/null @@ -1,67 +0,0 @@ -# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: moko-platform.Release -# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform -# PATH: /.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 moko-platform tools - env: - MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting - run: | - if ! command -v composer &> /dev/null; then - sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 - fi - # Always fetch latest CLI tools — never use stale cache from previous runs - rm -rf /tmp/moko-platform-api - git clone --depth 1 --branch main --quiet \ - "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ - /tmp/moko-platform-api - cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet - echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV" - - - 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" diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml deleted file mode 100644 index c775dee..0000000 --- a/.mokogitea/workflows/auto-release.yml +++ /dev/null @@ -1,270 +0,0 @@ -# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: moko-platform.Release -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform -# PATH: /templates/workflows/universal/auto-release.yml.template -# VERSION: 05.00.00 -# BRIEF: Universal build & release � detects platform from manifest.xml -# -# +========================================================================+ -# | UNIVERSAL BUILD & RELEASE PIPELINE | -# +========================================================================+ -# | | -# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. | -# | | -# | Platform-specific: | -# | joomla: XML manifest, updates.xml, type-prefixed packages | -# | dolibarr: mod*.class.php, update.txt, dev version reset | -# | generic: README-only, no update stream | -# | | -# +========================================================================+ - -name: "Universal: Build & Release" - -on: - pull_request: - types: [opened, closed] - branches: - - main - workflow_dispatch: - inputs: - action: - description: 'Action to perform' - required: false - type: choice - default: release - options: - - release - - promote-rc - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} - GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} - -permissions: - contents: write - -jobs: - # ── PR Opened → Rename branch to RC and build RC release ───────────────────── - promote-rc: - name: Promote to RC - runs-on: release - if: >- - (github.event.action == 'opened' && github.event.pull_request.merged != true) || - (github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc') - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - token: ${{ secrets.MOKOGITEA_TOKEN }} - fetch-depth: 1 - - - name: Setup moko-platform tools - env: - MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting - run: | - if ! command -v composer &> /dev/null; then - sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 - fi - # Always fetch latest CLI tools — never use stale cache from previous runs - rm -rf /tmp/moko-platform-api - git clone --depth 1 --branch main --quiet \ - "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ - /tmp/moko-platform-api - cd /tmp/moko-platform-api - composer install --no-dev --no-interaction --quiet - - - name: Rename branch to rc - run: | - php /tmp/moko-platform-api/cli/branch_rename.php \ - --from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" \ - --api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \ - --pr "${{ github.event.pull_request.number }}" - - - name: Checkout rc and configure git - run: | - git fetch origin rc - git checkout rc - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" - - - name: Publish RC release - run: | - php /tmp/moko-platform-api/cli/release_publish.php \ - --path . --stability rc --bump minor --branch rc \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" - - - name: Summary - if: always() - run: | - echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY - echo "Branch renamed to rc, minor bump, RC + lesser stream releases built, updates.xml synced" >> $GITHUB_STEP_SUMMARY - - # ── Merged PR → Build & Release (or promote RC to stable) ──────────────────── - release: - name: Build & Release Pipeline - runs-on: release - if: >- - github.event.pull_request.merged == true || - (github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc') - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - token: ${{ secrets.MOKOGITEA_TOKEN }} - fetch-depth: 0 - - - name: Configure git for bot pushes - run: | - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" - - - name: Setup moko-platform tools - env: - MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}' - run: | - # Ensure PHP + Composer are available - if ! command -v composer &> /dev/null; then - sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 - fi - # Always fetch latest CLI tools — never use stale cache from previous runs - rm -rf /tmp/moko-platform-api - git clone --depth 1 --branch main --quiet \ - "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ - /tmp/moko-platform-api - cd /tmp/moko-platform-api - composer install --no-dev --no-interaction --quiet - - - - name: "Publish stable release" - run: | - php /tmp/moko-platform-api/cli/release_publish.php \ - --path . --stability stable --bump minor --branch main \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" - - # -- STEP 9: Mirror to GitHub (stable only) -------------------------------- - - name: "Step 9: Mirror release to GitHub" - if: >- - steps.version.outputs.skip != 'true' && - secrets.GH_MIRROR_TOKEN != '' - continue-on-error: true - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - RELEASE_TAG="${{ steps.version.outputs.release_tag }}" - GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php /tmp/moko-platform-api/cli/release_mirror.php \ - --version "$VERSION" --tag "$RELEASE_TAG" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ - --gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \ - --branch main 2>&1 || true - echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY - - # -- STEP 10: Sync main branch to GitHub mirror ---------------------------- - - name: "Step 10: Push main to GitHub mirror" - if: >- - steps.version.outputs.skip != 'true' && - secrets.GH_MIRROR_TOKEN != '' - continue-on-error: true - run: | - GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" - GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1) - GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2) - git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \ - git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" - git fetch origin main --depth=1 - git push github origin/main:refs/heads/main --force 2>/dev/null \ - && echo "main branch pushed to GitHub mirror" \ - || echo "WARNING: GitHub mirror push failed" - - - name: "Step 11: Delete rc branch and recreate dev from main" - if: steps.version.outputs.skip != 'true' - continue-on-error: true - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" - - # Delete rc branch (ephemeral — created by promote-rc) - curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ - "${API_BASE}/branches/rc" 2>/dev/null \ - && echo "Deleted rc branch" || echo "rc branch not found" - - # Delete dev branch - curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ - "${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch" - - # Recreate dev from main (now includes version bump + changelog promotion) - curl -sf -X POST -H "Authorization: token ${TOKEN}" \ - -H "Content-Type: application/json" \ - "${API_BASE}/branches" \ - -d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main" - - echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY - - - name: "Step 12: Create version branch from main" - if: steps.version.outputs.skip != 'true' - continue-on-error: true - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - BRANCH_NAME="version/${VERSION}" - MAIN_SHA=$(git rev-parse HEAD) - - # Delete old version branch if it exists (same version re-release) - curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}" - - # Create version/XX.YY.ZZ from main - curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed" - - echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY - - - - # -- Dolibarr post-release: Reset dev version ----------------------------- - - name: "Post-release: Reset dev version" - if: steps.version.outputs.skip != 'true' - continue-on-error: true - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php /tmp/moko-platform-api/cli/version_reset_dev.php \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \ - --branch dev --path . 2>&1 || true - - # -- Summary -------------------------------------------------------------- - - name: Pipeline Summary - if: always() - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - PLATFORM="${{ steps.platform.outputs.platform }}" - if [ "${{ steps.version.outputs.skip }}" = "true" ]; then - echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY - echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY - elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then - echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY - else - echo "" >> $GITHUB_STEP_SUMMARY - echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY - echo "|------|--------|" >> $GITHUB_STEP_SUMMARY - echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY - fi diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml deleted file mode 100644 index b6f7e27..0000000 --- a/.mokogitea/workflows/pre-release.yml +++ /dev/null @@ -1,309 +0,0 @@ -# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: moko-platform.Release -# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform -# PATH: /templates/workflows/universal/pre-release.yml.template -# VERSION: 06.00.00 -# BRIEF: Pre-release pipeline — builds dev/alpha/beta/rc packages, updates update server -# -# Consolidates the former pre-release.yml + update-server.yml into one workflow. -# Triggers: -# - Push to dev/alpha/beta/rc branches (src/ or htdocs/ changes) -# - PR merged into dev (any source branch) -# - Manual dispatch with stability choice - -name: "Universal: Pre-Release" - -on: - push: - branches: - - 'dev' - - 'dev/**' - - 'alpha/**' - - 'beta/**' - - 'rc/**' - paths: - - 'src/**' - - 'htdocs/**' - pull_request: - types: [closed] - branches: - - dev - workflow_dispatch: - inputs: - stability: - description: 'Pre-release channel' - required: true - type: choice - options: - - development - - alpha - - beta - - release-candidate - -permissions: - contents: write - -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 }} - -jobs: - build: - name: "Build Pre-Release (${{ inputs.stability || 'development' }})" - runs-on: release - if: >- - github.event_name == 'workflow_dispatch' || - github.event_name == 'push' || - (github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev') - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - token: ${{ secrets.MOKOGITEA_TOKEN }} - - - 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 - rm -rf /tmp/moko-platform-api - git clone --depth 1 --branch main --quiet \ - "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ - /tmp/moko-platform-api - cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet - echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV" - - - name: Detect platform - id: platform - run: | - php ${MOKO_CLI}/manifest_read.php --path . --github-output - - - name: Resolve metadata 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" - - # Determine stability from manual input, branch name, or default - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - STABILITY="${{ inputs.stability }}" - elif [[ "$BRANCH" == rc/* ]]; then - STABILITY="release-candidate" - elif [[ "$BRANCH" == beta/* ]]; then - STABILITY="beta" - elif [[ "$BRANCH" == alpha/* ]]; then - STABILITY="alpha" - else - STABILITY="development" - fi - - case "$STABILITY" in - development) SUFFIX="-dev"; TAG="development" ;; - alpha) SUFFIX="-alpha"; TAG="alpha" ;; - beta) SUFFIX="-beta"; TAG="beta" ;; - release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;; - esac - - # Read current version - VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null) - [ -z "$VERSION" ] && VERSION="00.00.01" - - # Strip any existing suffix before applying stability - VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//') - - # 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 - - if [ -n "$SUFFIX" ]; then - VERSION="${VERSION}${SUFFIX}" - fi - - # Auto-detect element via manifest_element.php - php ${MOKO_CLI}/manifest_element.php \ - --path . --version "$VERSION" --stability "$STABILITY" \ - --repo "${GITEA_REPO}" --github-output - - EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) - ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) - [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') - [ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip" - - echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT" - echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT" - echo "tag=${TAG}" >> "$GITHUB_OUTPUT" - echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT" - echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT" - - # Commit version changes - git add -A - git diff --cached --quiet || { - git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]" - git push origin HEAD 2>&1 - } - - echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION} ===" - - - name: Create release - id: release - run: | - TAG="${{ steps.meta.outputs.tag }}" - VERSION="${{ steps.meta.outputs.version }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - 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 - - - name: Build package and upload - id: package - run: | - VERSION="${{ steps.meta.outputs.version }}" - TAG="${{ steps.meta.outputs.tag }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php ${MOKO_CLI}/release_package.php \ - --path . --version "$VERSION" --tag "$TAG" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ - --repo "${GITEA_REPO}" --output /tmp || true - - - 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} - - if ! git diff --quiet updates.xml 2>/dev/null; then - git add updates.xml - git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]" - git push origin HEAD 2>&1 || echo "WARNING: push failed" - fi - - - name: Sync updates.xml to all branches - if: steps.platform.outputs.platform == 'joomla' - run: | - CURRENT_BRANCH="${{ github.ref_name }}" - - for BRANCH in main dev; do - [ "$BRANCH" = "$CURRENT_BRANCH" ] && continue - echo "Syncing updates.xml -> ${BRANCH}" - git fetch origin "${BRANCH}" 2>/dev/null || continue - git checkout "origin/${BRANCH}" -- updates.xml 2>/dev/null || continue - git checkout "${CURRENT_BRANCH}" -- updates.xml - if ! git diff --quiet updates.xml 2>/dev/null; then - git add updates.xml - git commit -m "chore: sync updates.xml from ${CURRENT_BRANCH} [skip ci]" - git push origin HEAD:refs/heads/${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed" - fi - git checkout "${CURRENT_BRANCH}" 2>/dev/null - done - - - name: Delete lesser pre-release channels - continue-on-error: true - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php ${MOKO_CLI}/release_cascade.php \ - --stability "${{ steps.meta.outputs.stability }}" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" \ - --api-base "${API_BASE}" - - - 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: | - 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 }}" - ZIP_NAME="${{ steps.meta.outputs.zip_name }}" - SHA256="${{ steps.package.outputs.sha256_zip }}" - echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY - echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY - echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY - echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY - echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY From 6a38ca957a14d209eef1c342d108e7acdd9b81ce Mon Sep 17 00:00:00 2001 From: Moko Consulting <hello@mokoconsulting.tech> Date: Tue, 2 Jun 2026 20:38:31 +0000 Subject: [PATCH 087/132] chore(ci): add CI issue reporter for auto-filing gate failures --- .mokogitea/workflows/repo-health.yml | 1584 +++++++++++++------------- 1 file changed, 817 insertions(+), 767 deletions(-) diff --git a/.mokogitea/workflows/repo-health.yml b/.mokogitea/workflows/repo-health.yml index b34d35d..b23d971 100644 --- a/.mokogitea/workflows/repo-health.yml +++ b/.mokogitea/workflows/repo-health.yml @@ -1,767 +1,817 @@ -# ============================================================================ -# Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech> -# -# This file is part of a Moko Consulting project. -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: moko-platform.Validation -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform -# PATH: /templates/workflows/joomla/repo_health.yml.template -# VERSION: 04.06.00 -# BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts. -# ============================================================================ - -name: "Generic: Repo Health" - -defaults: - run: - shell: bash - -on: - workflow_dispatch: - inputs: - profile: - description: 'Validation profile: all, release, scripts, or repo' - required: true - default: all - type: choice - options: - - all - - release - - scripts - - repo - -permissions: - contents: read - -env: - # Release policy - Repository Variables Only - RELEASE_REQUIRED_REPO_VARS: RS_FTP_PATH_SUFFIX - RELEASE_OPTIONAL_REPO_VARS: DEV_FTP_SUFFIX - - # Scripts governance policy - SCRIPTS_REQUIRED_DIRS: - SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate - - # Repo health policy - REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.mokogitea/workflows/ - REPO_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore,README.md,docs/ - REPO_DISALLOWED_DIRS: - REPO_DISALLOWED_FILES: TODO.md,todo.md - - # Extended checks toggles - EXTENDED_CHECKS: "true" - - # File / directory variables - DOCS_INDEX: docs/docs-index.md - SCRIPT_DIR: scripts - WORKFLOWS_DIR: .mokogitea/workflows - SHELLCHECK_PATTERN: '*.sh' - SPDX_FILE_GLOBS: '*.sh,*.php,*.js,*.ts,*.css,*.xml,*.yml,*.yaml' - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - access_check: - name: Access control - runs-on: ubuntu-latest - timeout-minutes: 10 - permissions: - contents: read - - outputs: - allowed: ${{ steps.perm.outputs.allowed }} - permission: ${{ steps.perm.outputs.permission }} - - steps: - - name: Check actor permission (admin only) - id: perm - env: - TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }} - REPO: ${{ github.repository }} - ACTOR: ${{ github.actor }} - run: | - set -euo pipefail - ALLOWED=false - PERMISSION=unknown - METHOD="" - - # Hardcoded authorized users — always allowed - case "$ACTOR" in - jmiller|gitea-actions[bot]) - ALLOWED=true - PERMISSION=admin - METHOD="hardcoded allowlist" - ;; - *) - # Detect platform and check permissions via API - API_BASE="${GITHUB_API_URL:-${GITEA_API_URL:-https://api.github.com}}" - RESP=$(curl -sf -H "Authorization: token ${TOKEN}" \ - "${API_BASE}/repos/${REPO}/collaborators/${ACTOR}/permission" 2>/dev/null || echo '{}') - PERMISSION=$(echo "$RESP" | grep -oP '"permission"\s*:\s*"\K[^"]+' || echo "unknown") - if [ "$PERMISSION" = "admin" ] || [ "$PERMISSION" = "maintain" ] || [ "$PERMISSION" = "owner" ]; then - ALLOWED=true - fi - METHOD="collaborator API" - ;; - esac - - echo "permission=${PERMISSION}" >> "$GITHUB_OUTPUT" - echo "allowed=${ALLOWED}" >> "$GITHUB_OUTPUT" - - { - echo "## Access Authorization" - echo "" - echo "| Field | Value |" - echo "|-------|-------|" - echo "| **Actor** | \`${ACTOR}\` |" - echo "| **Repository** | \`${REPO}\` |" - echo "| **Permission** | \`${PERMISSION}\` |" - echo "| **Method** | ${METHOD} |" - echo "| **Authorized** | ${ALLOWED} |" - echo "" - if [ "$ALLOWED" = "true" ]; then - echo "${ACTOR} authorized (${METHOD})" - else - echo "${ACTOR} is NOT authorized. Requires admin or maintain role." - fi - } >> "${GITHUB_STEP_SUMMARY}" - - - name: Deny execution when not permitted - if: ${{ steps.perm.outputs.allowed != 'true' }} - run: | - set -euo pipefail - printf '%s\n' 'ERROR: Access denied. Admin permission required.' >> "${GITHUB_STEP_SUMMARY}" - exit 1 - - release_config: - name: Release configuration - needs: access_check - if: ${{ needs.access_check.outputs.allowed == 'true' }} - runs-on: ubuntu-latest - timeout-minutes: 20 - permissions: - contents: read - - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - fetch-depth: 0 - - - name: Guardrails release vars - env: - PROFILE_RAW: ${{ github.event.inputs.profile }} - RS_FTP_PATH_SUFFIX: ${{ vars.RS_FTP_PATH_SUFFIX }} - DEV_FTP_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }} - run: | - set -euo pipefail - - profile="${PROFILE_RAW:-all}" - case "${profile}" in - all|release|scripts|repo) ;; - *) - printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" - exit 1 - ;; - esac - - if [ "${profile}" = 'scripts' ] || [ "${profile}" = 'repo' ]; then - { - printf '%s\n' '### Release configuration (Repository Variables)' - printf '%s\n' "Profile: ${profile}" - printf '%s\n' 'Status: SKIPPED' - printf '%s\n' 'Reason: profile excludes release validation' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - exit 0 - fi - - IFS=',' read -r -a required <<< "${RELEASE_REQUIRED_REPO_VARS}" - IFS=',' read -r -a optional <<< "${RELEASE_OPTIONAL_REPO_VARS}" - - missing=() - missing_optional=() - - for k in "${required[@]}"; do - v="${!k:-}" - [ -z "${v}" ] && missing+=("${k}") - done - - for k in "${optional[@]}"; do - v="${!k:-}" - [ -z "${v}" ] && missing_optional+=("${k}") - done - - { - printf '%s\n' '### Release configuration (Repository Variables)' - printf '%s\n' "Profile: ${profile}" - printf '%s\n' '| Variable | Status |' - printf '%s\n' '|---|---|' - printf '%s\n' "| RS_FTP_PATH_SUFFIX | ${RS_FTP_PATH_SUFFIX:-NOT SET} |" - printf '%s\n' "| DEV_FTP_SUFFIX | ${DEV_FTP_SUFFIX:-NOT SET} |" - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - - if [ "${#missing_optional[@]}" -gt 0 ]; then - { - printf '%s\n' '### Missing optional repository variables' - for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - - if [ "${#missing[@]}" -gt 0 ]; then - { - printf '%s\n' '### Missing required repository variables' - for m in "${missing[@]}"; do printf '%s\n' "- ${m}"; done - printf '%s\n' 'ERROR: Guardrails failed. Missing required repository variables.' - } >> "${GITHUB_STEP_SUMMARY}" - exit 1 - fi - - { - printf '%s\n' '### Repository variables validation result' - printf '%s\n' 'Status: OK' - printf '%s\n' 'All required repository variables present.' - printf '%s\n' '' - printf '%s\n' '**Note**: Organization secrets (RS_FTP_HOST, RS_FTP_USER, etc.) are validated at deployment time, not in repository health checks.' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - - scripts_governance: - name: Scripts governance - needs: access_check - if: ${{ needs.access_check.outputs.allowed == 'true' }} - runs-on: ubuntu-latest - timeout-minutes: 15 - permissions: - contents: read - - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - fetch-depth: 0 - - - name: Scripts folder checks - env: - PROFILE_RAW: ${{ github.event.inputs.profile }} - run: | - set -euo pipefail - - profile="${PROFILE_RAW:-all}" - case "${profile}" in - all|release|scripts|repo) ;; - *) - printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" - exit 1 - ;; - esac - - if [ "${profile}" = 'release' ] || [ "${profile}" = 'repo' ]; then - { - printf '%s\n' '### Scripts governance' - printf '%s\n' "Profile: ${profile}" - printf '%s\n' 'Status: SKIPPED' - printf '%s\n' 'Reason: profile excludes scripts governance' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - exit 0 - fi - - if [ ! -d "${SCRIPT_DIR}" ]; then - { - printf '%s\n' '### Scripts governance' - printf '%s\n' 'Status: OK (advisory)' - printf '%s\n' 'scripts/ directory not present. No scripts governance enforced.' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - exit 0 - fi - - if [ -n "${SCRIPTS_REQUIRED_DIRS:-}" ]; then IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}"; else required_dirs=(); fi - IFS=',' read -r -a allowed_dirs <<< "${SCRIPTS_ALLOWED_DIRS}" - - missing_dirs=() - unapproved_dirs=() - - for d in "${required_dirs[@]}"; do - req="${d%/}" - [ ! -d "${req}" ] && missing_dirs+=("${req}/") - done - - while IFS= read -r d; do - allowed=false - for a in "${allowed_dirs[@]}"; do - a_norm="${a%/}" - [ "${d%/}" = "${a_norm}" ] && allowed=true - done - [ "${allowed}" = false ] && unapproved_dirs+=("${d%/}/") - done < <(find "${SCRIPT_DIR}" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sed 's#^\./##') - - { - printf '%s\n' '### Scripts governance' - printf '%s\n' "Profile: ${profile}" - printf '%s\n' '| Area | Status | Notes |' - printf '%s\n' '|---|---|---|' - - if [ "${#missing_dirs[@]}" -gt 0 ]; then - printf '%s\n' '| Required directories | Warning | Missing required subfolders |' - else - printf '%s\n' '| Required directories | OK | All required subfolders present |' - fi - - if [ "${#unapproved_dirs[@]}" -gt 0 ]; then - printf '%s\n' '| Directory policy | Warning | Unapproved directories detected |' - else - printf '%s\n' '| Directory policy | OK | No unapproved directories |' - fi - - printf '%s\n' '| Enforcement mode | Advisory | scripts folder is optional |' - printf '\n' - - if [ "${#missing_dirs[@]}" -gt 0 ]; then - printf '%s\n' 'Missing required script directories:' - for m in "${missing_dirs[@]}"; do printf '%s\n' "- ${m}"; done - printf '\n' - else - printf '%s\n' 'Missing required script directories: none.' - printf '\n' - fi - - if [ "${#unapproved_dirs[@]}" -gt 0 ]; then - printf '%s\n' 'Unapproved script directories detected:' - for m in "${unapproved_dirs[@]}"; do printf '%s\n' "- ${m}"; done - printf '\n' - else - printf '%s\n' 'Unapproved script directories detected: none.' - printf '\n' - fi - - printf '%s\n' 'Scripts governance completed in advisory mode.' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - - repo_health: - name: Repository health - needs: access_check - if: ${{ needs.access_check.outputs.allowed == 'true' }} - runs-on: ubuntu-latest - timeout-minutes: 20 - permissions: - contents: read - - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - fetch-depth: 0 - - - name: Repository health checks - env: - PROFILE_RAW: ${{ github.event.inputs.profile }} - run: | - set -euo pipefail - - profile="${PROFILE_RAW:-all}" - case "${profile}" in - all|release|scripts|repo) ;; - *) - printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" - exit 1 - ;; - esac - - if [ "${profile}" = 'release' ] || [ "${profile}" = 'scripts' ]; then - { - printf '%s\n' '### Repository health' - printf '%s\n' "Profile: ${profile}" - printf '%s\n' 'Status: SKIPPED' - printf '%s\n' 'Reason: profile excludes repository health' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - exit 0 - fi - - IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}" - IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}" - if [ -n "${REPO_DISALLOWED_DIRS:-}" ]; then IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}"; else disallowed_dirs=(); fi - IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES:-}" - - missing_required=() - missing_optional=() - - # Source directory: src/ or htdocs/ (either is valid for extension repos) - SOURCE_DIR="" - if [ -d "src" ]; then - SOURCE_DIR="src" - elif [ -d "htdocs" ]; then - SOURCE_DIR="htdocs" - elif [ -d "deploy" ] || [ -d "cli" ] || [ -d "monitoring" ]; then - # Platform/tooling repos don't need src/ - SOURCE_DIR="" - else - missing_required+=("src/ or htdocs/ (source directory required)") - fi - - for item in "${required_artifacts[@]}"; do - if printf '%s' "${item}" | grep -q '/$'; then - d="${item%/}" - [ ! -d "${d}" ] && missing_required+=("${item}") - else - [ ! -f "${item}" ] && missing_required+=("${item}") - fi - done - - for f in "${optional_files[@]}"; do - if printf '%s' "${f}" | grep -q '/$'; then - d="${f%/}" - [ ! -d "${d}" ] && missing_optional+=("${f}") - else - [ ! -f "${f}" ] && missing_optional+=("${f}") - fi - done - - for d in "${disallowed_dirs[@]}"; do - d_norm="${d%/}" - [ -d "${d_norm}" ] && missing_required+=("${d_norm}/ (disallowed)") - done - - for f in "${disallowed_files[@]}"; do - [ -f "${f}" ] && missing_required+=("${f} (disallowed)") - done - - git fetch origin --prune - - dev_paths=() - dev_branches=() - - while IFS= read -r b; do - name="${b#origin/}" - if [ "${name}" = 'dev' ]; then - dev_branches+=("${name}") - else - dev_paths+=("${name}") - fi - done < <(git branch -r --list 'origin/dev*' | sed 's/^ *//') - - if [ "${#dev_paths[@]}" -eq 0 ] && [ "${#dev_branches[@]}" -eq 0 ]; then - missing_required+=("dev or dev/* branch") - fi - - content_warnings=() - - if [ -f 'CHANGELOG.md' ] && ! grep -Eq '^# Changelog' CHANGELOG.md; then - content_warnings+=("CHANGELOG.md missing '# Changelog' header") - fi - - if [ -f 'CHANGELOG.md' ] && grep -Eq '^[# ]*Unreleased' CHANGELOG.md; then - content_warnings+=("CHANGELOG.md contains Unreleased section (review release readiness)") - fi - - if [ -f 'LICENSE' ] && ! grep -qiE 'GNU GENERAL PUBLIC LICENSE|GPL' LICENSE; then - content_warnings+=("LICENSE does not look like a GPL text") - fi - - if [ -f 'README.md' ] && ! grep -qiE 'moko|Moko' README.md; then - content_warnings+=("README.md missing expected brand keyword") - fi - - export PROFILE_RAW="${profile}" - export MISSING_REQUIRED="$(printf '%s\n' "${missing_required[@]:-}")" - export MISSING_OPTIONAL="$(printf '%s\n' "${missing_optional[@]:-}")" - export CONTENT_WARNINGS="$(printf '%s\n' "${content_warnings[@]:-}")" - - report_json=$(printf '{"profile":"%s","missing_required":%d,"missing_optional":%d,"content_warnings":%d}' "$profile" "${#missing_required[@]}" "${#missing_optional[@]}" "${#content_warnings[@]}") - - { - printf '%s\n' '### Repository health' - printf '%s\n' "Profile: ${profile}" - printf '%s\n' '| Metric | Value |' - printf '%s\n' '|---|---|' - printf '%s\n' "| Missing required | ${#missing_required[@]} |" - printf '%s\n' "| Missing optional | ${#missing_optional[@]} |" - printf '%s\n' "| Content warnings | ${#content_warnings[@]} |" - printf '\n' - - printf '%s\n' '### Guardrails report (JSON)' - printf '%s\n' '```json' - printf '%s\n' "${report_json}" - printf '%s\n' '```' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - - if [ "${#missing_required[@]}" -gt 0 ]; then - { - printf '%s\n' '### Missing required repo artifacts' - for m in "${missing_required[@]}"; do printf '%s\n' "- ${m}"; done - printf '%s\n' 'ERROR: Guardrails failed. Missing required repository artifacts.' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - exit 1 - fi - - if [ "${#missing_optional[@]}" -gt 0 ]; then - { - printf '%s\n' '### Missing optional repo artifacts' - for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - - if [ "${#content_warnings[@]}" -gt 0 ]; then - { - printf '%s\n' '### Repo content warnings' - for m in "${content_warnings[@]}"; do printf '%s\n' "- ${m}"; done - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - - # -- Joomla-specific checks -- - joomla_findings=() - - MANIFEST="$(find . -maxdepth 2 -name '*.xml' -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true)" - if [ -z "${MANIFEST}" ]; then - joomla_findings+=("Joomla XML manifest not found (no *.xml with <extension> tag)") - else - if ! grep -qP '<version>' "${MANIFEST}"; then - joomla_findings+=("XML manifest: <version> tag missing") - fi - if ! grep -qP 'type="(component|module|plugin|library|package|template|language)"' "${MANIFEST}"; then - joomla_findings+=("XML manifest: type attribute missing or invalid") - fi - if ! grep -qP '<name>' "${MANIFEST}"; then - joomla_findings+=("XML manifest: <name> tag missing") - fi - if ! grep -qP '<author>' "${MANIFEST}"; then - joomla_findings+=("XML manifest: <author> tag missing") - fi - if ! grep -qP '<namespace' "${MANIFEST}"; then - joomla_findings+=("XML manifest: <namespace> missing (required for Joomla 5+)") - fi - fi - - INI_COUNT="$(find . -name '*.ini' -type f 2>/dev/null | wc -l)" - if [ "${INI_COUNT}" -eq 0 ]; then - joomla_findings+=("No .ini language files found") - fi - - if [ ! -f 'updates.xml' ]; then - joomla_findings+=("updates.xml missing in root (required for Joomla update server)") - fi - - if [ -n "${SOURCE_DIR}" ]; then - INDEX_DIRS=("${SOURCE_DIR}" "${SOURCE_DIR}/admin" "${SOURCE_DIR}/site") - for dir in "${INDEX_DIRS[@]}"; do - if [ -d "${dir}" ] && [ ! -f "${dir}/index.html" ]; then - joomla_findings+=("${dir}/index.html missing (directory listing protection)") - fi - done - fi - - if [ "${#joomla_findings[@]}" -gt 0 ]; then - { - printf '%s\n' '### Joomla extension checks' - printf '%s\n' '| Check | Status |' - printf '%s\n' '|---|---|' - for f in "${joomla_findings[@]}"; do - printf '%s\n' "| ${f} | Warning |" - done - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - else - { - printf '%s\n' '### Joomla extension checks' - printf '%s\n' 'All Joomla-specific checks passed.' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - - extended_enabled="${EXTENDED_CHECKS:-true}" - extended_findings=() - - if [ "${extended_enabled}" = 'true' ]; then - if [ -f '.github/CODEOWNERS' ] || [ -f 'CODEOWNERS' ] || [ -f 'docs/CODEOWNERS' ]; then - : - else - extended_findings+=("CODEOWNERS not found (.github/CODEOWNERS preferred)") - fi - - if ls "${WORKFLOWS_DIR}"/*.yml >/dev/null 2>&1 || ls "${WORKFLOWS_DIR}"/*.yaml >/dev/null 2>&1; then - bad_refs="$(grep -RIn --include='*.yml' --include='*.yaml' -E '^[[:space:]]*uses:[[:space:]]*[^#]+@(main|master)\b' "${WORKFLOWS_DIR}" 2>/dev/null || true)" - if [ -n "${bad_refs}" ]; then - extended_findings+=("Workflows reference actions @main/@master (pin versions): see log excerpt") - { - printf '%s\n' '### Workflow pinning advisory' - printf '%s\n' 'Found uses: entries pinned to main/master:' - printf '%s\n' '```' - printf '%s\n' "${bad_refs}" - printf '%s\n' '```' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - fi - - if [ -f "${DOCS_INDEX}" ]; then - missing_links="" - while IFS= read -r docline; do - for link in $(echo "$docline" | grep -oE '\]\([^)]+\)' | sed 's/\](//' | sed 's/)$//' || true); do - case "$link" in http://*|https://*|"#"*|mailto:*) continue ;; esac - linkpath="${link%%#*}" - linkpath="${linkpath%%\?*}" - [ -z "$linkpath" ] && continue - if [ "${linkpath:0:1}" = "/" ]; then - testpath="${linkpath#/}" - else - testpath="$(dirname "${DOCS_INDEX}")/${linkpath}" - fi - [ ! -e "$testpath" ] && missing_links="${missing_links}${testpath} " - done - done < "${DOCS_INDEX}" - if [ -n "${missing_links}" ]; then - extended_findings+=("docs/docs-index.md contains broken relative links") - { - printf '%s\n' '### Docs index link integrity' - printf '%s\n' 'Broken relative links:' - for bl in ${missing_links}; do - printf '%s\n' "- ${bl}" - done - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - fi - - if [ -d "${SCRIPT_DIR}" ]; then - if ! command -v shellcheck >/dev/null 2>&1; then - sudo apt-get update -qq - sudo apt-get install -y shellcheck >/dev/null - fi - - sc_out='' - while IFS= read -r shf; do - [ -z "${shf}" ] && continue - out_one="$(shellcheck -S warning -x "${shf}" 2>/dev/null || true)" - if [ -n "${out_one}" ]; then - sc_out="${sc_out}${out_one}\n" - fi - done < <(find "${SCRIPT_DIR}" -type f -name "${SHELLCHECK_PATTERN}" 2>/dev/null | sort) - - if [ -n "${sc_out}" ]; then - extended_findings+=("ShellCheck warnings detected (advisory)") - sc_head="$(printf '%s' "${sc_out}" | head -n 200)" - { - printf '%s\n' '### ShellCheck (advisory)' - printf '%s\n' '```' - printf '%s\n' "${sc_head}" - printf '%s\n' '```' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - fi - - spdx_missing=() - IFS=',' read -r -a spdx_globs <<< "${SPDX_FILE_GLOBS}" - spdx_args=() - for g in "${spdx_globs[@]}"; do spdx_args+=("${g}"); done - - while IFS= read -r f; do - [ -z "${f}" ] && continue - if ! head -n 40 "${f}" | grep -q 'SPDX-License-Identifier:'; then - spdx_missing+=("${f}") - fi - done < <(git ls-files "${spdx_args[@]}" 2>/dev/null || true) - - if [ "${#spdx_missing[@]}" -gt 0 ]; then - extended_findings+=("SPDX header missing in some tracked files (advisory)") - { - printf '%s\n' '### SPDX header advisory' - printf '%s\n' 'Files missing SPDX-License-Identifier (first 40 lines scan):' - for f in "${spdx_missing[@]}"; do printf '%s\n' "- ${f}"; done - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - - stale_cutoff_days=180 - stale_branches="$(git for-each-ref --format='%(refname:short) %(committerdate:unix)' refs/remotes/origin 2>/dev/null | awk -v now="$(date +%s)" -v days="${stale_cutoff_days}" '{if (now-$2 > days*86400) print $1}' | head -50)" - if [ -n "${stale_branches}" ]; then - extended_findings+=("Stale remote branches detected (advisory)") - { - printf '%s\n' '### Git hygiene advisory' - printf '%s\n' "Branches with last commit older than ${stale_cutoff_days} days (sample up to 50):" - while IFS= read -r b; do [ -n "${b}" ] && printf '%s\n' "- ${b}"; done <<< "${stale_branches}" - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - fi - - { - printf '%s\n' '### Guardrails coverage matrix' - printf '%s\n' '| Domain | Status | Notes |' - printf '%s\n' '|---|---|---|' - printf '%s\n' '| Access control | OK | Admin-only execution gate |' - printf '%s\n' '| Release variables | OK | Repository variables validation |' - printf '%s\n' '| Scripts governance | OK | Directory policy and advisory reporting |' - printf '%s\n' '| Repo required artifacts | OK | Required, optional, disallowed enforcement |' - printf '%s\n' '| Repo content heuristics | OK | Brand, license, changelog structure |' - if [ "${extended_enabled}" = 'true' ]; then - if [ "${#extended_findings[@]}" -gt 0 ]; then - printf '%s\n' '| Extended checks | Warning | See extended findings below |' - else - printf '%s\n' '| Extended checks | OK | No findings |' - fi - else - printf '%s\n' '| Extended checks | SKIPPED | EXTENDED_CHECKS disabled |' - fi - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - - if [ "${extended_enabled}" = 'true' ] && [ "${#extended_findings[@]}" -gt 0 ]; then - { - printf '%s\n' '### Extended findings (advisory)' - for f in "${extended_findings[@]}"; do printf '%s\n' "- ${f}"; done - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - - printf '%s\n' 'Repository health guardrails passed.' >> "${GITHUB_STEP_SUMMARY}" - - - site-health: - name: Site Health - runs-on: ubuntu-latest - if: github.event_name == 'workflow_dispatch' - steps: - - uses: actions/checkout@v4 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: '8.3' - - - name: Uptime check - if: env.URLS != '' - run: | - echo "$URLS" > /tmp/urls.txt - php monitoring/uptime-probe.php --urls /tmp/urls.txt --timeout 15 || echo "::warning::Some sites are down" - rm -f /tmp/urls.txt - env: - URLS: ${{ vars.MONITORED_URLS }} - - - name: SSL certificate check - if: env.DOMAINS != '' - run: | - echo "$DOMAINS" > /tmp/domains.txt - php monitoring/ssl-check.php --domains /tmp/domains.txt --warn-days 30 || echo "::warning::SSL certificates expiring soon" - rm -f /tmp/domains.txt - env: - DOMAINS: ${{ vars.MONITORED_DOMAINS }} - - - name: Summary - if: always() - run: | - echo "### Site Health" >> $GITHUB_STEP_SUMMARY - echo "Uptime and SSL checks completed." >> $GITHUB_STEP_SUMMARY - +# ============================================================================ +# Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech> +# +# This file is part of a Moko Consulting project. +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: moko-platform.Validation +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform +# PATH: /templates/workflows/joomla/repo_health.yml.template +# VERSION: 09.23.00 +# BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts. +# ============================================================================ + +name: "Generic: Repo Health" + +defaults: + run: + shell: bash + +on: + workflow_dispatch: + inputs: + profile: + description: 'Validation profile: all, release, scripts, or repo' + required: true + default: all + type: choice + options: + - all + - release + - scripts + - repo + pull_request: + push: + +permissions: + contents: read + +env: + # Release policy - Repository Variables Only + RELEASE_REQUIRED_REPO_VARS: RS_FTP_PATH_SUFFIX + RELEASE_OPTIONAL_REPO_VARS: DEV_FTP_SUFFIX + + # Scripts governance policy + SCRIPTS_REQUIRED_DIRS: + SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate + + # Repo health policy + REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.mokogitea/workflows/ + REPO_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore,README.md,docs/ + REPO_DISALLOWED_DIRS: + REPO_DISALLOWED_FILES: TODO.md,todo.md + + # Extended checks toggles + EXTENDED_CHECKS: "true" + + # File / directory variables + DOCS_INDEX: docs/docs-index.md + SCRIPT_DIR: scripts + WORKFLOWS_DIR: .mokogitea/workflows + SHELLCHECK_PATTERN: '*.sh' + SPDX_FILE_GLOBS: '*.sh,*.php,*.js,*.ts,*.css,*.xml,*.yml,*.yaml' + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + access_check: + name: Access control + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + + outputs: + allowed: ${{ steps.perm.outputs.allowed }} + permission: ${{ steps.perm.outputs.permission }} + + steps: + - name: Check actor permission (admin only) + id: perm + env: + TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }} + REPO: ${{ github.repository }} + ACTOR: ${{ github.actor }} + run: | + set -euo pipefail + ALLOWED=false + PERMISSION=unknown + METHOD="" + + # Hardcoded authorized users — always allowed + case "$ACTOR" in + jmiller|gitea-actions[bot]) + ALLOWED=true + PERMISSION=admin + METHOD="hardcoded allowlist" + ;; + *) + # Detect platform and check permissions via API + API_BASE="${GITHUB_API_URL:-${GITEA_API_URL:-https://api.github.com}}" + RESP=$(curl -sf -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/repos/${REPO}/collaborators/${ACTOR}/permission" 2>/dev/null || echo '{}') + PERMISSION=$(echo "$RESP" | grep -oP '"permission"\s*:\s*"\K[^"]+' || echo "unknown") + if [ "$PERMISSION" = "admin" ] || [ "$PERMISSION" = "maintain" ] || [ "$PERMISSION" = "owner" ]; then + ALLOWED=true + fi + METHOD="collaborator API" + ;; + esac + + echo "permission=${PERMISSION}" >> "$GITHUB_OUTPUT" + echo "allowed=${ALLOWED}" >> "$GITHUB_OUTPUT" + + { + echo "## Access Authorization" + echo "" + echo "| Field | Value |" + echo "|-------|-------|" + echo "| **Actor** | \`${ACTOR}\` |" + echo "| **Repository** | \`${REPO}\` |" + echo "| **Permission** | \`${PERMISSION}\` |" + echo "| **Method** | ${METHOD} |" + echo "| **Authorized** | ${ALLOWED} |" + echo "" + if [ "$ALLOWED" = "true" ]; then + echo "${ACTOR} authorized (${METHOD})" + else + echo "${ACTOR} is NOT authorized. Requires admin or maintain role." + fi + } >> "${GITHUB_STEP_SUMMARY}" + + - name: Deny execution when not permitted + if: ${{ steps.perm.outputs.allowed != 'true' }} + run: | + set -euo pipefail + printf '%s\n' 'ERROR: Access denied. Admin permission required.' >> "${GITHUB_STEP_SUMMARY}" + exit 1 + + release_config: + name: Release configuration + needs: access_check + if: ${{ needs.access_check.outputs.allowed == 'true' }} + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 0 + + - name: Guardrails release vars + env: + PROFILE_RAW: ${{ github.event.inputs.profile }} + RS_FTP_PATH_SUFFIX: ${{ vars.RS_FTP_PATH_SUFFIX }} + DEV_FTP_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }} + run: | + set -euo pipefail + + profile="${PROFILE_RAW:-all}" + case "${profile}" in + all|release|scripts|repo) ;; + *) + printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" + exit 1 + ;; + esac + + if [ "${profile}" = 'scripts' ] || [ "${profile}" = 'repo' ]; then + { + printf '%s\n' '### Release configuration (Repository Variables)' + printf '%s\n' "Profile: ${profile}" + printf '%s\n' 'Status: SKIPPED' + printf '%s\n' 'Reason: profile excludes release validation' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + exit 0 + fi + + IFS=',' read -r -a required <<< "${RELEASE_REQUIRED_REPO_VARS}" + IFS=',' read -r -a optional <<< "${RELEASE_OPTIONAL_REPO_VARS}" + + missing=() + missing_optional=() + + for k in "${required[@]}"; do + v="${!k:-}" + [ -z "${v}" ] && missing+=("${k}") + done + + for k in "${optional[@]}"; do + v="${!k:-}" + [ -z "${v}" ] && missing_optional+=("${k}") + done + + { + printf '%s\n' '### Release configuration (Repository Variables)' + printf '%s\n' "Profile: ${profile}" + printf '%s\n' '| Variable | Status |' + printf '%s\n' '|---|---|' + printf '%s\n' "| RS_FTP_PATH_SUFFIX | ${RS_FTP_PATH_SUFFIX:-NOT SET} |" + printf '%s\n' "| DEV_FTP_SUFFIX | ${DEV_FTP_SUFFIX:-NOT SET} |" + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + + if [ "${#missing_optional[@]}" -gt 0 ]; then + { + printf '%s\n' '### Missing optional repository variables' + for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + + if [ "${#missing[@]}" -gt 0 ]; then + { + printf '%s\n' '### Missing required repository variables' + for m in "${missing[@]}"; do printf '%s\n' "- ${m}"; done + printf '%s\n' 'ERROR: Guardrails failed. Missing required repository variables.' + } >> "${GITHUB_STEP_SUMMARY}" + exit 1 + fi + + { + printf '%s\n' '### Repository variables validation result' + printf '%s\n' 'Status: OK' + printf '%s\n' 'All required repository variables present.' + printf '%s\n' '' + printf '%s\n' '**Note**: Organization secrets (RS_FTP_HOST, RS_FTP_USER, etc.) are validated at deployment time, not in repository health checks.' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + + scripts_governance: + name: Scripts governance + needs: access_check + if: ${{ needs.access_check.outputs.allowed == 'true' }} + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 0 + + - name: Scripts folder checks + env: + PROFILE_RAW: ${{ github.event.inputs.profile }} + run: | + set -euo pipefail + + profile="${PROFILE_RAW:-all}" + case "${profile}" in + all|release|scripts|repo) ;; + *) + printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" + exit 1 + ;; + esac + + if [ "${profile}" = 'release' ] || [ "${profile}" = 'repo' ]; then + { + printf '%s\n' '### Scripts governance' + printf '%s\n' "Profile: ${profile}" + printf '%s\n' 'Status: SKIPPED' + printf '%s\n' 'Reason: profile excludes scripts governance' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + exit 0 + fi + + if [ ! -d "${SCRIPT_DIR}" ]; then + { + printf '%s\n' '### Scripts governance' + printf '%s\n' 'Status: OK (advisory)' + printf '%s\n' 'scripts/ directory not present. No scripts governance enforced.' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + exit 0 + fi + + if [ -n "${SCRIPTS_REQUIRED_DIRS:-}" ]; then IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}"; else required_dirs=(); fi + IFS=',' read -r -a allowed_dirs <<< "${SCRIPTS_ALLOWED_DIRS}" + + missing_dirs=() + unapproved_dirs=() + + for d in "${required_dirs[@]}"; do + req="${d%/}" + [ ! -d "${req}" ] && missing_dirs+=("${req}/") + done + + while IFS= read -r d; do + allowed=false + for a in "${allowed_dirs[@]}"; do + a_norm="${a%/}" + [ "${d%/}" = "${a_norm}" ] && allowed=true + done + [ "${allowed}" = false ] && unapproved_dirs+=("${d%/}/") + done < <(find "${SCRIPT_DIR}" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sed 's#^\./##') + + { + printf '%s\n' '### Scripts governance' + printf '%s\n' "Profile: ${profile}" + printf '%s\n' '| Area | Status | Notes |' + printf '%s\n' '|---|---|---|' + + if [ "${#missing_dirs[@]}" -gt 0 ]; then + printf '%s\n' '| Required directories | Warning | Missing required subfolders |' + else + printf '%s\n' '| Required directories | OK | All required subfolders present |' + fi + + if [ "${#unapproved_dirs[@]}" -gt 0 ]; then + printf '%s\n' '| Directory policy | Warning | Unapproved directories detected |' + else + printf '%s\n' '| Directory policy | OK | No unapproved directories |' + fi + + printf '%s\n' '| Enforcement mode | Advisory | scripts folder is optional |' + printf '\n' + + if [ "${#missing_dirs[@]}" -gt 0 ]; then + printf '%s\n' 'Missing required script directories:' + for m in "${missing_dirs[@]}"; do printf '%s\n' "- ${m}"; done + printf '\n' + else + printf '%s\n' 'Missing required script directories: none.' + printf '\n' + fi + + if [ "${#unapproved_dirs[@]}" -gt 0 ]; then + printf '%s\n' 'Unapproved script directories detected:' + for m in "${unapproved_dirs[@]}"; do printf '%s\n' "- ${m}"; done + printf '\n' + else + printf '%s\n' 'Unapproved script directories detected: none.' + printf '\n' + fi + + printf '%s\n' 'Scripts governance completed in advisory mode.' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + + repo_health: + name: Repository health + needs: access_check + if: ${{ needs.access_check.outputs.allowed == 'true' }} + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 0 + + - name: Repository health checks + env: + PROFILE_RAW: ${{ github.event.inputs.profile }} + run: | + set -euo pipefail + + profile="${PROFILE_RAW:-all}" + case "${profile}" in + all|release|scripts|repo) ;; + *) + printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" + exit 1 + ;; + esac + + if [ "${profile}" = 'release' ] || [ "${profile}" = 'scripts' ]; then + { + printf '%s\n' '### Repository health' + printf '%s\n' "Profile: ${profile}" + printf '%s\n' 'Status: SKIPPED' + printf '%s\n' 'Reason: profile excludes repository health' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + exit 0 + fi + + IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}" + IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}" + if [ -n "${REPO_DISALLOWED_DIRS:-}" ]; then IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}"; else disallowed_dirs=(); fi + IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES:-}" + + missing_required=() + missing_optional=() + + # Source directory: src/ or htdocs/ (either is valid for extension repos) + SOURCE_DIR="" + if [ -d "src" ]; then + SOURCE_DIR="src" + elif [ -d "htdocs" ]; then + SOURCE_DIR="htdocs" + elif [ -d "deploy" ] || [ -d "cli" ] || [ -d "monitoring" ]; then + # Platform/tooling repos don't need src/ + SOURCE_DIR="" + else + missing_required+=("src/ or htdocs/ (source directory required)") + fi + + for item in "${required_artifacts[@]}"; do + if printf '%s' "${item}" | grep -q '/$'; then + d="${item%/}" + [ ! -d "${d}" ] && missing_required+=("${item}") + else + [ ! -f "${item}" ] && missing_required+=("${item}") + fi + done + + for f in "${optional_files[@]}"; do + if printf '%s' "${f}" | grep -q '/$'; then + d="${f%/}" + [ ! -d "${d}" ] && missing_optional+=("${f}") + else + [ ! -f "${f}" ] && missing_optional+=("${f}") + fi + done + + for d in "${disallowed_dirs[@]}"; do + d_norm="${d%/}" + [ -d "${d_norm}" ] && missing_required+=("${d_norm}/ (disallowed)") + done + + for f in "${disallowed_files[@]}"; do + [ -f "${f}" ] && missing_required+=("${f} (disallowed)") + done + + git fetch origin --prune + + dev_paths=() + dev_branches=() + + while IFS= read -r b; do + name="${b#origin/}" + if [ "${name}" = 'dev' ]; then + dev_branches+=("${name}") + else + dev_paths+=("${name}") + fi + done < <(git branch -r --list 'origin/dev*' | sed 's/^ *//') + + if [ "${#dev_paths[@]}" -eq 0 ] && [ "${#dev_branches[@]}" -eq 0 ]; then + missing_required+=("dev or dev/* branch") + fi + + content_warnings=() + + if [ -f 'CHANGELOG.md' ] && ! grep -Eq '^# Changelog' CHANGELOG.md; then + content_warnings+=("CHANGELOG.md missing '# Changelog' header") + fi + + if [ -f 'CHANGELOG.md' ] && grep -Eq '^[# ]*Unreleased' CHANGELOG.md; then + content_warnings+=("CHANGELOG.md contains Unreleased section (review release readiness)") + fi + + if [ -f 'LICENSE' ] && ! grep -qiE 'GNU GENERAL PUBLIC LICENSE|GPL' LICENSE; then + content_warnings+=("LICENSE does not look like a GPL text") + fi + + if [ -f 'README.md' ] && ! grep -qiE 'moko|Moko' README.md; then + content_warnings+=("README.md missing expected brand keyword") + fi + + export PROFILE_RAW="${profile}" + export MISSING_REQUIRED="$(printf '%s\n' "${missing_required[@]:-}")" + export MISSING_OPTIONAL="$(printf '%s\n' "${missing_optional[@]:-}")" + export CONTENT_WARNINGS="$(printf '%s\n' "${content_warnings[@]:-}")" + + report_json=$(printf '{"profile":"%s","missing_required":%d,"missing_optional":%d,"content_warnings":%d}' "$profile" "${#missing_required[@]}" "${#missing_optional[@]}" "${#content_warnings[@]}") + + { + printf '%s\n' '### Repository health' + printf '%s\n' "Profile: ${profile}" + printf '%s\n' '| Metric | Value |' + printf '%s\n' '|---|---|' + printf '%s\n' "| Missing required | ${#missing_required[@]} |" + printf '%s\n' "| Missing optional | ${#missing_optional[@]} |" + printf '%s\n' "| Content warnings | ${#content_warnings[@]} |" + printf '\n' + + printf '%s\n' '### Guardrails report (JSON)' + printf '%s\n' '```json' + printf '%s\n' "${report_json}" + printf '%s\n' '```' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + + if [ "${#missing_required[@]}" -gt 0 ]; then + { + printf '%s\n' '### Missing required repo artifacts' + for m in "${missing_required[@]}"; do printf '%s\n' "- ${m}"; done + printf '%s\n' 'ERROR: Guardrails failed. Missing required repository artifacts.' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + exit 1 + fi + + if [ "${#missing_optional[@]}" -gt 0 ]; then + { + printf '%s\n' '### Missing optional repo artifacts' + for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + + if [ "${#content_warnings[@]}" -gt 0 ]; then + { + printf '%s\n' '### Repo content warnings' + for m in "${content_warnings[@]}"; do printf '%s\n' "- ${m}"; done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + + # -- Joomla-specific checks -- + joomla_findings=() + + MANIFEST="$(find . -maxdepth 2 -name '*.xml' -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true)" + if [ -z "${MANIFEST}" ]; then + joomla_findings+=("Joomla XML manifest not found (no *.xml with <extension> tag)") + else + if ! grep -qP '<version>' "${MANIFEST}"; then + joomla_findings+=("XML manifest: <version> tag missing") + fi + if ! grep -qP 'type="(component|module|plugin|library|package|template|language)"' "${MANIFEST}"; then + joomla_findings+=("XML manifest: type attribute missing or invalid") + fi + if ! grep -qP '<name>' "${MANIFEST}"; then + joomla_findings+=("XML manifest: <name> tag missing") + fi + if ! grep -qP '<author>' "${MANIFEST}"; then + joomla_findings+=("XML manifest: <author> tag missing") + fi + if ! grep -qP '<namespace' "${MANIFEST}"; then + joomla_findings+=("XML manifest: <namespace> missing (required for Joomla 5+)") + fi + fi + + INI_COUNT="$(find . -name '*.ini' -type f 2>/dev/null | wc -l)" + if [ "${INI_COUNT}" -eq 0 ]; then + joomla_findings+=("No .ini language files found") + fi + + if [ ! -f 'updates.xml' ]; then + joomla_findings+=("updates.xml missing in root (required for Joomla update server)") + fi + + if [ -n "${SOURCE_DIR}" ]; then + INDEX_DIRS=("${SOURCE_DIR}" "${SOURCE_DIR}/admin" "${SOURCE_DIR}/site") + for dir in "${INDEX_DIRS[@]}"; do + if [ -d "${dir}" ] && [ ! -f "${dir}/index.html" ]; then + joomla_findings+=("${dir}/index.html missing (directory listing protection)") + fi + done + fi + + if [ "${#joomla_findings[@]}" -gt 0 ]; then + { + printf '%s\n' '### Joomla extension checks' + printf '%s\n' '| Check | Status |' + printf '%s\n' '|---|---|' + for f in "${joomla_findings[@]}"; do + printf '%s\n' "| ${f} | Warning |" + done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + else + { + printf '%s\n' '### Joomla extension checks' + printf '%s\n' 'All Joomla-specific checks passed.' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + + extended_enabled="${EXTENDED_CHECKS:-true}" + extended_findings=() + + if [ "${extended_enabled}" = 'true' ]; then + if [ -f '.github/CODEOWNERS' ] || [ -f 'CODEOWNERS' ] || [ -f 'docs/CODEOWNERS' ]; then + : + else + extended_findings+=("CODEOWNERS not found (.github/CODEOWNERS preferred)") + fi + + if ls "${WORKFLOWS_DIR}"/*.yml >/dev/null 2>&1 || ls "${WORKFLOWS_DIR}"/*.yaml >/dev/null 2>&1; then + bad_refs="$(grep -RIn --include='*.yml' --include='*.yaml' -E '^[[:space:]]*uses:[[:space:]]*[^#]+@(main|master)\b' "${WORKFLOWS_DIR}" 2>/dev/null || true)" + if [ -n "${bad_refs}" ]; then + extended_findings+=("Workflows reference actions @main/@master (pin versions): see log excerpt") + { + printf '%s\n' '### Workflow pinning advisory' + printf '%s\n' 'Found uses: entries pinned to main/master:' + printf '%s\n' '```' + printf '%s\n' "${bad_refs}" + printf '%s\n' '```' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + fi + + if [ -f "${DOCS_INDEX}" ]; then + missing_links="" + while IFS= read -r docline; do + for link in $(echo "$docline" | grep -oE '\]\([^)]+\)' | sed 's/\](//' | sed 's/)$//' || true); do + case "$link" in http://*|https://*|"#"*|mailto:*) continue ;; esac + linkpath="${link%%#*}" + linkpath="${linkpath%%\?*}" + [ -z "$linkpath" ] && continue + if [ "${linkpath:0:1}" = "/" ]; then + testpath="${linkpath#/}" + else + testpath="$(dirname "${DOCS_INDEX}")/${linkpath}" + fi + [ ! -e "$testpath" ] && missing_links="${missing_links}${testpath} " + done + done < "${DOCS_INDEX}" + if [ -n "${missing_links}" ]; then + extended_findings+=("docs/docs-index.md contains broken relative links") + { + printf '%s\n' '### Docs index link integrity' + printf '%s\n' 'Broken relative links:' + for bl in ${missing_links}; do + printf '%s\n' "- ${bl}" + done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + fi + + if [ -d "${SCRIPT_DIR}" ]; then + if ! command -v shellcheck >/dev/null 2>&1; then + sudo apt-get update -qq + sudo apt-get install -y shellcheck >/dev/null + fi + + sc_out='' + while IFS= read -r shf; do + [ -z "${shf}" ] && continue + out_one="$(shellcheck -S warning -x "${shf}" 2>/dev/null || true)" + if [ -n "${out_one}" ]; then + sc_out="${sc_out}${out_one}\n" + fi + done < <(find "${SCRIPT_DIR}" -type f -name "${SHELLCHECK_PATTERN}" 2>/dev/null | sort) + + if [ -n "${sc_out}" ]; then + extended_findings+=("ShellCheck warnings detected (advisory)") + sc_head="$(printf '%s' "${sc_out}" | head -n 200)" + { + printf '%s\n' '### ShellCheck (advisory)' + printf '%s\n' '```' + printf '%s\n' "${sc_head}" + printf '%s\n' '```' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + fi + + spdx_missing=() + IFS=',' read -r -a spdx_globs <<< "${SPDX_FILE_GLOBS}" + spdx_args=() + for g in "${spdx_globs[@]}"; do spdx_args+=("${g}"); done + + while IFS= read -r f; do + [ -z "${f}" ] && continue + if ! head -n 40 "${f}" | grep -q 'SPDX-License-Identifier:'; then + spdx_missing+=("${f}") + fi + done < <(git ls-files "${spdx_args[@]}" 2>/dev/null || true) + + if [ "${#spdx_missing[@]}" -gt 0 ]; then + extended_findings+=("SPDX header missing in some tracked files (advisory)") + { + printf '%s\n' '### SPDX header advisory' + printf '%s\n' 'Files missing SPDX-License-Identifier (first 40 lines scan):' + for f in "${spdx_missing[@]}"; do printf '%s\n' "- ${f}"; done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + + stale_cutoff_days=180 + stale_branches="$(git for-each-ref --format='%(refname:short) %(committerdate:unix)' refs/remotes/origin 2>/dev/null | awk -v now="$(date +%s)" -v days="${stale_cutoff_days}" '{if (now-$2 > days*86400) print $1}' | head -50)" + if [ -n "${stale_branches}" ]; then + extended_findings+=("Stale remote branches detected (advisory)") + { + printf '%s\n' '### Git hygiene advisory' + printf '%s\n' "Branches with last commit older than ${stale_cutoff_days} days (sample up to 50):" + while IFS= read -r b; do [ -n "${b}" ] && printf '%s\n' "- ${b}"; done <<< "${stale_branches}" + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + fi + + { + printf '%s\n' '### Guardrails coverage matrix' + printf '%s\n' '| Domain | Status | Notes |' + printf '%s\n' '|---|---|---|' + printf '%s\n' '| Access control | OK | Admin-only execution gate |' + printf '%s\n' '| Release variables | OK | Repository variables validation |' + printf '%s\n' '| Scripts governance | OK | Directory policy and advisory reporting |' + printf '%s\n' '| Repo required artifacts | OK | Required, optional, disallowed enforcement |' + printf '%s\n' '| Repo content heuristics | OK | Brand, license, changelog structure |' + if [ "${extended_enabled}" = 'true' ]; then + if [ "${#extended_findings[@]}" -gt 0 ]; then + printf '%s\n' '| Extended checks | Warning | See extended findings below |' + else + printf '%s\n' '| Extended checks | OK | No findings |' + fi + else + printf '%s\n' '| Extended checks | SKIPPED | EXTENDED_CHECKS disabled |' + fi + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + + if [ "${extended_enabled}" = 'true' ] && [ "${#extended_findings[@]}" -gt 0 ]; then + { + printf '%s\n' '### Extended findings (advisory)' + for f in "${extended_findings[@]}"; do printf '%s\n' "- ${f}"; done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + + printf '%s\n' 'Repository health guardrails passed.' >> "${GITHUB_STEP_SUMMARY}" + + + site-health: + name: Site Health + runs-on: ubuntu-latest + if: github.event_name == 'workflow_dispatch' + steps: + - uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + + - name: Uptime check + if: env.URLS != '' + run: | + echo "$URLS" > /tmp/urls.txt + php monitoring/uptime-probe.php --urls /tmp/urls.txt --timeout 15 || echo "::warning::Some sites are down" + rm -f /tmp/urls.txt + env: + URLS: ${{ vars.MONITORED_URLS }} + + - name: SSL certificate check + if: env.DOMAINS != '' + run: | + echo "$DOMAINS" > /tmp/domains.txt + php monitoring/ssl-check.php --domains /tmp/domains.txt --warn-days 30 || echo "::warning::SSL certificates expiring soon" + rm -f /tmp/domains.txt + env: + DOMAINS: ${{ vars.MONITORED_DOMAINS }} + + - name: Summary + if: always() + run: | + echo "### Site Health" >> $GITHUB_STEP_SUMMARY + echo "Uptime and SSL checks completed." >> $GITHUB_STEP_SUMMARY + + # ═══════════════════════════════════════════════════════════════════════ + # Issue Reporter — file issues for failed gates + # ═══════════════════════════════════════════════════════════════════════ + report-issues: + name: "Report Issues" + runs-on: ubuntu-latest + needs: [access_check, release_config, scripts_governance, repo_health] + if: >- + always() && + (needs.release_config.result == 'failure' || + needs.scripts_governance.result == 'failure' || + needs.repo_health.result == 'failure') + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + sparse-checkout: automation/ci-issue-reporter.sh + sparse-checkout-cone-mode: false + + - name: "File issues for failed gates" + env: + GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + run: | + chmod +x automation/ci-issue-reporter.sh + REPORTER="./automation/ci-issue-reporter.sh" + WF="Repo Health" + + report_gate() { + local gate="$1" result="$2" details="$3" + if [ "$result" = "failure" ]; then + "$REPORTER" --gate "$gate" --details "$details" --workflow "$WF" --severity error + fi + } + + report_gate "Release Configuration" \ + "${{ needs.release_config.result }}" \ + "Required repository variables are missing (RS_FTP_PATH_SUFFIX). Check repository settings." + + report_gate "Scripts Governance" \ + "${{ needs.scripts_governance.result }}" \ + "Scripts directory policy violations detected. Review required and allowed directories." + + report_gate "Repository Health" \ + "${{ needs.repo_health.result }}" \ + "Repository health checks failed — missing required artifacts, disallowed files, or content warnings. Check the CI run summary." + From 922f74d37ad6db709a9e1dc91ce608697b22fbdf Mon Sep 17 00:00:00 2001 From: Moko Consulting <hello@mokoconsulting.tech> Date: Tue, 2 Jun 2026 20:38:32 +0000 Subject: [PATCH 088/132] chore(ci): add CI issue reporter for auto-filing gate failures --- .mokogitea/workflows/pr-check.yml | 483 ++++++++++++++++-------------- 1 file changed, 264 insertions(+), 219 deletions(-) diff --git a/.mokogitea/workflows/pr-check.yml b/.mokogitea/workflows/pr-check.yml index 39d5623..e2c82ef 100644 --- a/.mokogitea/workflows/pr-check.yml +++ b/.mokogitea/workflows/pr-check.yml @@ -1,219 +1,264 @@ -# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: moko-platform.CI -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform -# PATH: /templates/workflows/universal/pr-check.yml.template -# VERSION: 05.00.00 -# BRIEF: PR gate — branch policy + code validation before merge - -name: "Universal: PR Check" - -on: - pull_request: - types: [opened, synchronize, reopened, edited] - -permissions: - contents: read - pull-requests: write - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - # ── Branch Policy ────────────────────────────────────────────────────── - branch-policy: - name: Branch Policy - runs-on: ubuntu-latest - steps: - - name: Check branch merge target - run: | - HEAD="${{ github.head_ref }}" - BASE="${{ github.base_ref }}" - - echo "PR: ${HEAD} → ${BASE}" - - ALLOWED=true - REASON="" - - case "$HEAD" in - feature/*|feat/*) - if [ "$BASE" != "dev" ]; then - ALLOWED=false - REASON="Feature branches must target 'dev', not '${BASE}'" - fi - ;; - fix/*|bugfix/*) - if [ "$BASE" != "dev" ]; then - ALLOWED=false - REASON="Fix branches must target 'dev', not '${BASE}'" - fi - ;; - patch/*) - if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then - ALLOWED=false - REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'" - fi - ;; - hotfix/*) - if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then - ALLOWED=false - REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'" - fi - ;; - rc) - if [ "$BASE" != "main" ]; then - ALLOWED=false - REASON="RC branch can only merge into 'main', not '${BASE}'" - fi - ;; - dev) - if [ "$BASE" != "main" ]; then - ALLOWED=false - REASON="Dev branch can only merge into 'main', not '${BASE}'" - fi - ;; - esac - - if [ "$ALLOWED" = false ]; then - echo "::error::${REASON}" - echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "${REASON}" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY - echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY - echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY - echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY - echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY - echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY - exit 1 - fi - - echo "Branch policy: OK (${HEAD} → ${BASE})" - echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY - - # ── Code Validation ──────────────────────────────────────────────────── - validate: - name: Validate PR - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Detect platform - id: platform - run: | - # Read platform from XML manifest (<platform> tag) or plain text fallback - PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1) - [ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]') - [ -z "$PLATFORM" ] && PLATFORM="generic" - echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT" - - - name: Setup PHP - if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr' - run: | - if ! command -v php &> /dev/null; then - sudo apt-get update -qq - sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1 - fi - - - name: PHP syntax check - if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr' - run: | - ERRORS=0 - while IFS= read -r -d '' file; do - if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then - ERRORS=$((ERRORS + 1)) - fi - done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0) - echo "PHP lint: ${ERRORS} error(s)" - [ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; } - - - name: Validate platform manifest - run: | - PLATFORM="${{ steps.platform.outputs.platform }}" - case "$PLATFORM" in - joomla) - MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1) - if [ -z "$MANIFEST" ]; then - echo "::warning::No Joomla manifest found (WaaS site)" - exit 0 - fi - echo "Manifest: ${MANIFEST}" - if command -v php &> /dev/null; then - php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::Manifest XML is malformed"; exit 1; } - fi - for ELEMENT in name version description; do - grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; } - done - echo "Joomla manifest valid" - ;; - dolibarr) - MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1) - if [ -z "$MOD_FILE" ]; then - echo "::error::No mod*.class.php found" - exit 1 - fi - echo "Dolibarr module: ${MOD_FILE}" - ;; - *) - echo "Generic platform — no manifest validation" - ;; - esac - - - name: Check update stream format - run: | - PLATFORM="${{ steps.platform.outputs.platform }}" - case "$PLATFORM" in - joomla) - if [ -f "updates.xml" ]; then - if command -v php &> /dev/null; then - php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::updates.xml is malformed"; exit 1; } - fi - echo "updates.xml valid" - fi - ;; - dolibarr) - [ -f "update.txt" ] && echo "update.txt present" || echo "::warning::No update.txt" - ;; - esac - - - name: Check changelog has unreleased entry - run: | - if [ ! -f "CHANGELOG.md" ]; then - echo "::warning::No CHANGELOG.md found" - exit 0 - fi - # Check for content under [Unreleased] section - if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then - echo "::error::CHANGELOG.md missing [Unreleased] section" - exit 1 - fi - # Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased - UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true) - if [ "$UNRELEASED_CONTENT" -eq 0 ]; then - echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes." - echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY - echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY - echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY - exit 1 - fi - echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]" - - - name: Verify package source - run: | - SOURCE_DIR="src" - [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" - if [ ! -d "$SOURCE_DIR" ]; then - echo "::warning::No src/ or htdocs/ directory" - exit 0 - fi - FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l) - echo "Source: ${FILE_COUNT} files" - [ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; } - +# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: moko-platform.CI +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform +# PATH: /templates/workflows/universal/pr-check.yml.template +# VERSION: 09.23.00 +# BRIEF: PR gate — branch policy + code validation before merge + +name: "Universal: PR Check" + +on: + pull_request: + types: [opened, synchronize, reopened, edited] + +permissions: + contents: read + pull-requests: write + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + # ── Branch Policy ────────────────────────────────────────────────────── + branch-policy: + name: Branch Policy + runs-on: ubuntu-latest + steps: + - name: Check branch merge target + run: | + HEAD="${{ github.head_ref }}" + BASE="${{ github.base_ref }}" + + echo "PR: ${HEAD} → ${BASE}" + + ALLOWED=true + REASON="" + + case "$HEAD" in + feature/*|feat/*) + if [ "$BASE" != "dev" ]; then + ALLOWED=false + REASON="Feature branches must target 'dev', not '${BASE}'" + fi + ;; + fix/*|bugfix/*) + if [ "$BASE" != "dev" ]; then + ALLOWED=false + REASON="Fix branches must target 'dev', not '${BASE}'" + fi + ;; + patch/*) + if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then + ALLOWED=false + REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'" + fi + ;; + hotfix/*) + if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then + ALLOWED=false + REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'" + fi + ;; + rc) + if [ "$BASE" != "main" ]; then + ALLOWED=false + REASON="RC branch can only merge into 'main', not '${BASE}'" + fi + ;; + dev) + if [ "$BASE" != "main" ]; then + ALLOWED=false + REASON="Dev branch can only merge into 'main', not '${BASE}'" + fi + ;; + esac + + if [ "$ALLOWED" = false ]; then + echo "::error::${REASON}" + echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "${REASON}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY + echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY + echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY + echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY + echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY + echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + echo "Branch policy: OK (${HEAD} → ${BASE})" + echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY + + # ── Code Validation ──────────────────────────────────────────────────── + validate: + name: Validate PR + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Detect platform + id: platform + run: | + # Read platform from XML manifest (<platform> tag) or plain text fallback + PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1) + [ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]') + [ -z "$PLATFORM" ] && PLATFORM="generic" + echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT" + + - name: Setup PHP + if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr' + run: | + if ! command -v php &> /dev/null; then + sudo apt-get update -qq + sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1 + fi + + - name: PHP syntax check + if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr' + run: | + ERRORS=0 + while IFS= read -r -d '' file; do + if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then + ERRORS=$((ERRORS + 1)) + fi + done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0) + echo "PHP lint: ${ERRORS} error(s)" + [ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; } + + - name: Validate platform manifest + run: | + PLATFORM="${{ steps.platform.outputs.platform }}" + case "$PLATFORM" in + joomla) + MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1) + if [ -z "$MANIFEST" ]; then + echo "::warning::No Joomla manifest found (WaaS site)" + exit 0 + fi + echo "Manifest: ${MANIFEST}" + if command -v php &> /dev/null; then + php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::Manifest XML is malformed"; exit 1; } + fi + for ELEMENT in name version description; do + grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; } + done + echo "Joomla manifest valid" + ;; + dolibarr) + MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1) + if [ -z "$MOD_FILE" ]; then + echo "::error::No mod*.class.php found" + exit 1 + fi + echo "Dolibarr module: ${MOD_FILE}" + ;; + *) + echo "Generic platform — no manifest validation" + ;; + esac + + - name: Check update stream format + run: | + PLATFORM="${{ steps.platform.outputs.platform }}" + case "$PLATFORM" in + joomla) + if [ -f "updates.xml" ]; then + if command -v php &> /dev/null; then + php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::updates.xml is malformed"; exit 1; } + fi + echo "updates.xml valid" + fi + ;; + dolibarr) + [ -f "update.txt" ] && echo "update.txt present" || echo "::warning::No update.txt" + ;; + esac + + - name: Check changelog has unreleased entry + run: | + if [ ! -f "CHANGELOG.md" ]; then + echo "::warning::No CHANGELOG.md found" + exit 0 + fi + # Check for content under [Unreleased] section + if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then + echo "::error::CHANGELOG.md missing [Unreleased] section" + exit 1 + fi + # Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased + UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true) + if [ "$UNRELEASED_CONTENT" -eq 0 ]; then + echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes." + echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY + echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY + echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]" + + - name: Verify package source + run: | + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" + if [ ! -d "$SOURCE_DIR" ]; then + echo "::warning::No src/ or htdocs/ directory" + exit 0 + fi + FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l) + echo "Source: ${FILE_COUNT} files" + [ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; } + + # ── Pre-Release RC Build ───────────────────────────────────────────────── + pre-release: + name: Build RC Package + runs-on: ubuntu-latest + needs: [branch-policy, validate] + + steps: + - name: Trigger RC pre-release + env: + GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + REPO: ${{ github.repository }} + BRANCH: ${{ github.head_ref }} + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + run: | + curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}" + echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY + echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY + + # ── Issue Reporter ────────────────────────────────────────────────────── + report-issues: + name: Report Issues + runs-on: ubuntu-latest + needs: [branch-policy, validate] + if: >- + always() && + needs.validate.result == 'failure' + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + sparse-checkout: automation/ci-issue-reporter.sh + sparse-checkout-cone-mode: false + + - name: "File issue for PR validation failure" + env: + GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + run: | + chmod +x automation/ci-issue-reporter.sh + ./automation/ci-issue-reporter.sh \ + --gate "PR Validation" \ + --workflow "PR Check" \ + --severity error \ + --details "PR validation failed (syntax, manifest, changelog, or source checks). See the CI run for the specific check that failed." From 5d32a37258c926f33526ca2a843d29de2509110d Mon Sep 17 00:00:00 2001 From: Moko Consulting <hello@mokoconsulting.tech> Date: Tue, 2 Jun 2026 20:38:33 +0000 Subject: [PATCH 089/132] chore(ci): add CI issue reporter for auto-filing gate failures --- automation/ci-issue-reporter.sh | 237 ++++++++++++++++++++++++++++++++ 1 file changed, 237 insertions(+) create mode 100644 automation/ci-issue-reporter.sh diff --git a/automation/ci-issue-reporter.sh b/automation/ci-issue-reporter.sh new file mode 100644 index 0000000..65c47ba --- /dev/null +++ b/automation/ci-issue-reporter.sh @@ -0,0 +1,237 @@ +#!/usr/bin/env bash +# ============================================================================ +# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Automation.CI +# INGROUP: moko-platform.Automation +# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform +# PATH: /automation/ci-issue-reporter.sh +# VERSION: 09.23.00 +# BRIEF: Creates or updates a Gitea issue when a CI gate fails. +# Deduplicates by searching open issues with the "ci-auto" label +# whose title matches the gate. If a matching issue exists, a comment +# is appended instead of opening a duplicate. +# ============================================================================ + +set -euo pipefail + +# ── Defaults ──────────────────────────────────────────────────────────────── +GITEA_URL="${GITEA_URL:-https://git.mokoconsulting.tech}" +GITEA_TOKEN="${GITEA_TOKEN:-}" +REPO="${GITHUB_REPOSITORY:-}" +RUN_URL="${GITHUB_SERVER_URL:-${GITEA_URL}}/${REPO}/actions/runs/${GITHUB_RUN_ID:-0}" +LABEL_NAME="ci-auto" +LABEL_COLOR="#e11d48" + +GATE="" +DETAILS="" +SEVERITY="error" +WORKFLOW="" + +# ── Parse arguments ───────────────────────────────────────────────────────── +usage() { + cat <<EOF +Usage: ci-issue-reporter.sh --gate NAME --details TEXT [OPTIONS] + +Required: + --gate CI gate name (e.g. "Code Quality", "Self-Health") + --details Human-readable failure description + +Optional: + --severity "error" (default) or "warning" + --workflow Workflow name for the issue title + --repo owner/repo (default: \$GITHUB_REPOSITORY) + --run-url URL to the CI run (auto-detected from env) + --token Gitea API token (default: \$GITEA_TOKEN) + --url Gitea base URL (default: \$GITEA_URL) +EOF + exit 1 +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --gate) GATE="$2"; shift 2 ;; + --details) DETAILS="$2"; shift 2 ;; + --severity) SEVERITY="$2"; shift 2 ;; + --workflow) WORKFLOW="$2"; shift 2 ;; + --repo) REPO="$2"; shift 2 ;; + --run-url) RUN_URL="$2"; shift 2 ;; + --token) GITEA_TOKEN="$2"; shift 2 ;; + --url) GITEA_URL="$2"; shift 2 ;; + -h|--help) usage ;; + *) echo "Unknown option: $1"; usage ;; + esac +done + +[[ -z "$GATE" ]] && { echo "ERROR: --gate is required"; usage; } +[[ -z "$DETAILS" ]] && { echo "ERROR: --details is required"; usage; } +[[ -z "$GITEA_TOKEN" ]] && { echo "ERROR: GITEA_TOKEN not set"; exit 1; } +[[ -z "$REPO" ]] && { echo "ERROR: GITHUB_REPOSITORY not set"; exit 1; } + +API="${GITEA_URL}/api/v1/repos/${REPO}" + +# ── Build title ───────────────────────────────────────────────────────────── +if [[ -n "$WORKFLOW" ]]; then + TITLE="[CI] ${WORKFLOW}: ${GATE} failed" +else + TITLE="[CI] ${GATE} failed" +fi + +# ── Ensure label exists ───────────────────────────────────────────────────── +ensure_label() { + local exists + exists=$(curl -sf -o /dev/null -w '%{http_code}' \ + -H "Authorization: token ${GITEA_TOKEN}" \ + "${API}/labels" 2>/dev/null || echo "000") + + if [[ "$exists" == "200" ]]; then + # Check if label already exists + local found + found=$(curl -sf \ + -H "Authorization: token ${GITEA_TOKEN}" \ + "${API}/labels" 2>/dev/null \ + | grep -o "\"name\":\"${LABEL_NAME}\"" || true) + + if [[ -z "$found" ]]; then + curl -sf -X POST \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/json" \ + "${API}/labels" \ + -d "{\"name\":\"${LABEL_NAME}\",\"color\":\"${LABEL_COLOR}\",\"description\":\"Auto-created by CI issue reporter\"}" \ + > /dev/null 2>&1 || true + fi + fi +} + +# ── Search for existing open issue ────────────────────────────────────────── +find_existing_issue() { + # URL-encode the gate name for the query + local query + query=$(printf '%s' "[CI] ${GATE}" | sed 's/ /%20/g; s/\[/%5B/g; s/\]/%5D/g') + + local response + response=$(curl -sf \ + -H "Authorization: token ${GITEA_TOKEN}" \ + "${API}/issues?type=issues&state=open&labels=${LABEL_NAME}&q=${query}&limit=5" \ + 2>/dev/null || echo "[]") + + # Extract the first matching issue number + echo "$response" \ + | grep -oP '"number":\s*\K[0-9]+' \ + | head -1 +} + +# ── Build issue body ──────────────────────────────────────────────────────── +build_body() { + local severity_badge + if [[ "$SEVERITY" == "error" ]]; then + severity_badge="**Severity:** Error" + else + severity_badge="**Severity:** Warning" + fi + + cat <<BODY +## CI Gate Failure: ${GATE} + +${severity_badge} +**Workflow:** ${WORKFLOW:-unknown} +**Branch:** ${GITHUB_REF_NAME:-unknown} +**Commit:** \`${GITHUB_SHA:0:8}\` +**Run:** [View CI run](${RUN_URL}) + +### Details + +${DETAILS} + +### Resolution + +Fix the issue described above and push a new commit. This issue will be closed automatically when the gate passes, or can be closed manually. + +--- +*Auto-created by [ci-issue-reporter](${GITEA_URL}/${REPO}/src/branch/main/automation/ci-issue-reporter.sh)* +BODY +} + +# ── Build comment body (for existing issues) ──────────────────────────────── +build_comment() { + cat <<COMMENT +### CI failure recurrence + +**Branch:** ${GITHUB_REF_NAME:-unknown} +**Commit:** \`${GITHUB_SHA:0:8}\` +**Run:** [View CI run](${RUN_URL}) + +${DETAILS} +COMMENT +} + +# ── Main ──────────────────────────────────────────────────────────────────── +ensure_label + +EXISTING=$(find_existing_issue) + +if [[ -n "$EXISTING" ]]; then + # Append comment to existing issue + COMMENT_BODY=$(build_comment) + COMMENT_JSON=$(printf '%s' "$COMMENT_BODY" | python3 -c " +import sys, json +print(json.dumps({'body': sys.stdin.read()}))" 2>/dev/null) + + HTTP=$(curl -sf -o /dev/null -w '%{http_code}' -X POST \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/json" \ + "${API}/issues/${EXISTING}/comments" \ + -d "${COMMENT_JSON}" 2>/dev/null || echo "000") + + if [[ "$HTTP" == "201" ]]; then + echo "Commented on existing issue #${EXISTING}" + else + echo "WARNING: Failed to comment on issue #${EXISTING} (HTTP ${HTTP})" + fi +else + # Create new issue + ISSUE_BODY=$(build_body) + ISSUE_JSON=$(python3 -c " +import sys, json +body = sys.stdin.read() +print(json.dumps({ + 'title': sys.argv[1], + 'body': body, + 'labels': [] +}))" "$TITLE" <<< "$ISSUE_BODY" 2>/dev/null) + + # Create the issue + RESPONSE=$(curl -sf -X POST \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/json" \ + "${API}/issues" \ + -d "${ISSUE_JSON}" 2>/dev/null || echo "{}") + + ISSUE_NUM=$(echo "$RESPONSE" | grep -oP '"number":\s*\K[0-9]+' | head -1) + + if [[ -n "$ISSUE_NUM" ]]; then + # Apply label (separate call — more reliable across Gitea versions) + LABEL_ID=$(curl -sf \ + -H "Authorization: token ${GITEA_TOKEN}" \ + "${API}/labels" 2>/dev/null \ + | grep -oP "\"id\":\s*\K[0-9]+(?=[^}]*\"name\":\s*\"${LABEL_NAME}\")" \ + | head -1 || true) + + if [[ -n "$LABEL_ID" ]]; then + curl -sf -X POST \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/json" \ + "${API}/issues/${ISSUE_NUM}/labels" \ + -d "{\"labels\":[${LABEL_ID}]}" \ + > /dev/null 2>&1 || true + fi + + echo "Created issue #${ISSUE_NUM}: ${TITLE}" + else + echo "WARNING: Failed to create issue" + echo "Response: ${RESPONSE}" + fi +fi From 9d0d772dd45e10013b6d9fd8dccae4c944c3193f Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 2 Jun 2026 21:52:10 +0000 Subject: [PATCH 090/132] chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] --- .mokogitea/workflows/pr-check.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.mokogitea/workflows/pr-check.yml b/.mokogitea/workflows/pr-check.yml index e2c82ef..0ac0ef1 100644 --- a/.mokogitea/workflows/pr-check.yml +++ b/.mokogitea/workflows/pr-check.yml @@ -105,6 +105,19 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Check for merge conflict markers + run: | + CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true) + if [ -n "$CONFLICTS" ]; then + echo "::error::Merge conflict markers found in source files" + echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + exit 1 + fi + echo "No conflict markers found" + - name: Detect platform id: platform run: | From 2bb8aaf8b4a6503752a16b0d2ce22e04ded12e12 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 2 Jun 2026 23:47:55 +0000 Subject: [PATCH 091/132] chore: add .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] --- .mokogitea/workflows/auto-release.yml | 283 ++++++++++++++++++++++++++ 1 file changed, 283 insertions(+) create mode 100644 .mokogitea/workflows/auto-release.yml diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml new file mode 100644 index 0000000..2325032 --- /dev/null +++ b/.mokogitea/workflows/auto-release.yml @@ -0,0 +1,283 @@ +# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: moko-platform.Release +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform +# PATH: /templates/workflows/universal/auto-release.yml.template +# VERSION: 05.00.00 +# BRIEF: Universal build & release � detects platform from manifest.xml +# +# +========================================================================+ +# | UNIVERSAL BUILD & RELEASE PIPELINE | +# +========================================================================+ +# | | +# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. | +# | | +# | Platform-specific: | +# | joomla: XML manifest, updates.xml, type-prefixed packages | +# | dolibarr: mod*.class.php, update.txt, dev version reset | +# | generic: README-only, no update stream | +# | | +# +========================================================================+ + +name: "Universal: Build & Release" + +on: + pull_request: + types: [opened, closed] + branches: + - main + workflow_dispatch: + inputs: + action: + description: 'Action to perform' + required: false + type: choice + default: release + options: + - release + - promote-rc + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + +permissions: + contents: write + +jobs: + # ── PR Opened → Rename branch to RC and build RC release ───────────────────── + promote-rc: + name: Promote to RC + runs-on: release + if: >- + (github.event.action == 'opened' && github.event.pull_request.merged != true) || + (github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc') + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.MOKOGITEA_TOKEN }} + fetch-depth: 1 + + - name: Setup moko-platform tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + run: | + if ! command -v composer &> /dev/null; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + # Always fetch latest CLI tools — never use stale cache from previous runs + rm -rf /tmp/moko-platform-api + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ + /tmp/moko-platform-api + cd /tmp/moko-platform-api + composer install --no-dev --no-interaction --quiet + + - name: Rename branch to rc + run: | + php /tmp/moko-platform-api/cli/branch_rename.php \ + --from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" \ + --api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \ + --pr "${{ github.event.pull_request.number }}" + + - name: Checkout rc and configure git + run: | + git fetch origin rc + git checkout rc + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + + - name: Publish RC release + run: | + php /tmp/moko-platform-api/cli/release_publish.php \ + --path . --stability rc --bump minor --branch rc \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" + + - name: Summary + if: always() + run: | + echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY + echo "Branch renamed to rc, minor bump, RC + lesser stream releases built, updates.xml synced" >> $GITHUB_STEP_SUMMARY + + # ── Merged PR → Build & Release (or promote RC to stable) ──────────────────── + release: + name: Build & Release Pipeline + runs-on: release + if: >- + github.event.pull_request.merged == true || + (github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc') + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.MOKOGITEA_TOKEN }} + fetch-depth: 0 + + - name: Configure git for bot pushes + run: | + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + + - name: Check for merge conflict markers + run: | + CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true) + if [ -n "$CONFLICTS" ]; then + echo "::error::Merge conflict markers found — aborting release" + echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + exit 1 + fi + echo "No conflict markers found" + + - name: Setup moko-platform tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}' + run: | + # Ensure PHP + Composer are available + if ! command -v composer &> /dev/null; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + # Always fetch latest CLI tools — never use stale cache from previous runs + rm -rf /tmp/moko-platform-api + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ + /tmp/moko-platform-api + cd /tmp/moko-platform-api + composer install --no-dev --no-interaction --quiet + + + - name: "Publish stable release" + run: | + php /tmp/moko-platform-api/cli/release_publish.php \ + --path . --stability stable --bump minor --branch main \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" + + # -- STEP 9: Mirror to GitHub (stable only) -------------------------------- + - name: "Step 9: Mirror release to GitHub" + if: >- + steps.version.outputs.skip != 'true' && + secrets.GH_MIRROR_TOKEN != '' + continue-on-error: true + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php /tmp/moko-platform-api/cli/release_mirror.php \ + --version "$VERSION" --tag "$RELEASE_TAG" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ + --gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \ + --branch main 2>&1 || true + echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY + + # -- STEP 10: Sync main branch to GitHub mirror ---------------------------- + - name: "Step 10: Push main to GitHub mirror" + if: >- + steps.version.outputs.skip != 'true' && + secrets.GH_MIRROR_TOKEN != '' + continue-on-error: true + run: | + GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" + GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1) + GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2) + git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \ + git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" + git fetch origin main --depth=1 + git push github origin/main:refs/heads/main --force 2>/dev/null \ + && echo "main branch pushed to GitHub mirror" \ + || echo "WARNING: GitHub mirror push failed" + + - name: "Step 11: Delete rc branch and recreate dev from main" + if: steps.version.outputs.skip != 'true' + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" + + # Delete rc branch (ephemeral — created by promote-rc) + curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/branches/rc" 2>/dev/null \ + && echo "Deleted rc branch" || echo "rc branch not found" + + # Delete dev branch + curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch" + + # Recreate dev from main (now includes version bump + changelog promotion) + curl -sf -X POST -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/json" \ + "${API_BASE}/branches" \ + -d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main" + + echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY + + - name: "Step 12: Create version branch from main" + if: steps.version.outputs.skip != 'true' + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + BRANCH_NAME="version/${VERSION}" + MAIN_SHA=$(git rev-parse HEAD) + + # Delete old version branch if it exists (same version re-release) + curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}" + + # Create version/XX.YY.ZZ from main + curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed" + + echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY + + + + # -- Dolibarr post-release: Reset dev version ----------------------------- + - name: "Post-release: Reset dev version" + if: steps.version.outputs.skip != 'true' + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php /tmp/moko-platform-api/cli/version_reset_dev.php \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \ + --branch dev --path . 2>&1 || true + + # -- Summary -------------------------------------------------------------- + - name: Pipeline Summary + if: always() + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + PLATFORM="${{ steps.platform.outputs.platform }}" + if [ "${{ steps.version.outputs.skip }}" = "true" ]; then + echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY + echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY + elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then + echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY + echo "|------|--------|" >> $GITHUB_STEP_SUMMARY + echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY + fi From 26e146bcf4996f24023fb0fc705e59813660d1cf Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Wed, 3 Jun 2026 03:11:19 +0000 Subject: [PATCH 092/132] chore: sync .mokogitea/workflows/repo-health.yml from moko-platform [skip ci] --- .mokogitea/workflows/repo-health.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.mokogitea/workflows/repo-health.yml b/.mokogitea/workflows/repo-health.yml index b23d971..d7743f0 100644 --- a/.mokogitea/workflows/repo-health.yml +++ b/.mokogitea/workflows/repo-health.yml @@ -41,7 +41,8 @@ permissions: env: # Release policy - Repository Variables Only - RELEASE_REQUIRED_REPO_VARS: RS_FTP_PATH_SUFFIX + # RS_FTP_PATH_SUFFIX removed — MokoGitea handles all releases now + RELEASE_REQUIRED_REPO_VARS: RELEASE_OPTIONAL_REPO_VARS: DEV_FTP_SUFFIX # Scripts governance policy From 7091e64e2c6d642ced4ba25073de83b843733110 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Wed, 3 Jun 2026 09:37:32 +0000 Subject: [PATCH 093/132] chore: sync .mokogitea/workflows/repo-health.yml from moko-platform [skip ci] --- .mokogitea/workflows/repo-health.yml | 125 ++------------------------- 1 file changed, 9 insertions(+), 116 deletions(-) diff --git a/.mokogitea/workflows/repo-health.yml b/.mokogitea/workflows/repo-health.yml index d7743f0..8d57aaf 100644 --- a/.mokogitea/workflows/repo-health.yml +++ b/.mokogitea/workflows/repo-health.yml @@ -11,7 +11,7 @@ # REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform # PATH: /templates/workflows/joomla/repo_health.yml.template # VERSION: 09.23.00 -# BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts. +# BRIEF: Enforces repository guardrails by validating scripts governance, tooling availability, and core repository health artifacts. # ============================================================================ name: "Generic: Repo Health" @@ -24,13 +24,12 @@ on: workflow_dispatch: inputs: profile: - description: 'Validation profile: all, release, scripts, or repo' + description: 'Validation profile: all, scripts, or repo' required: true default: all type: choice options: - all - - release - scripts - repo pull_request: @@ -40,11 +39,6 @@ permissions: contents: read env: - # Release policy - Repository Variables Only - # RS_FTP_PATH_SUFFIX removed — MokoGitea handles all releases now - RELEASE_REQUIRED_REPO_VARS: - RELEASE_OPTIONAL_REPO_VARS: DEV_FTP_SUFFIX - # Scripts governance policy SCRIPTS_REQUIRED_DIRS: SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate @@ -139,101 +133,6 @@ jobs: printf '%s\n' 'ERROR: Access denied. Admin permission required.' >> "${GITHUB_STEP_SUMMARY}" exit 1 - release_config: - name: Release configuration - needs: access_check - if: ${{ needs.access_check.outputs.allowed == 'true' }} - runs-on: ubuntu-latest - timeout-minutes: 20 - permissions: - contents: read - - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - fetch-depth: 0 - - - name: Guardrails release vars - env: - PROFILE_RAW: ${{ github.event.inputs.profile }} - RS_FTP_PATH_SUFFIX: ${{ vars.RS_FTP_PATH_SUFFIX }} - DEV_FTP_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }} - run: | - set -euo pipefail - - profile="${PROFILE_RAW:-all}" - case "${profile}" in - all|release|scripts|repo) ;; - *) - printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" - exit 1 - ;; - esac - - if [ "${profile}" = 'scripts' ] || [ "${profile}" = 'repo' ]; then - { - printf '%s\n' '### Release configuration (Repository Variables)' - printf '%s\n' "Profile: ${profile}" - printf '%s\n' 'Status: SKIPPED' - printf '%s\n' 'Reason: profile excludes release validation' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - exit 0 - fi - - IFS=',' read -r -a required <<< "${RELEASE_REQUIRED_REPO_VARS}" - IFS=',' read -r -a optional <<< "${RELEASE_OPTIONAL_REPO_VARS}" - - missing=() - missing_optional=() - - for k in "${required[@]}"; do - v="${!k:-}" - [ -z "${v}" ] && missing+=("${k}") - done - - for k in "${optional[@]}"; do - v="${!k:-}" - [ -z "${v}" ] && missing_optional+=("${k}") - done - - { - printf '%s\n' '### Release configuration (Repository Variables)' - printf '%s\n' "Profile: ${profile}" - printf '%s\n' '| Variable | Status |' - printf '%s\n' '|---|---|' - printf '%s\n' "| RS_FTP_PATH_SUFFIX | ${RS_FTP_PATH_SUFFIX:-NOT SET} |" - printf '%s\n' "| DEV_FTP_SUFFIX | ${DEV_FTP_SUFFIX:-NOT SET} |" - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - - if [ "${#missing_optional[@]}" -gt 0 ]; then - { - printf '%s\n' '### Missing optional repository variables' - for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - - if [ "${#missing[@]}" -gt 0 ]; then - { - printf '%s\n' '### Missing required repository variables' - for m in "${missing[@]}"; do printf '%s\n' "- ${m}"; done - printf '%s\n' 'ERROR: Guardrails failed. Missing required repository variables.' - } >> "${GITHUB_STEP_SUMMARY}" - exit 1 - fi - - { - printf '%s\n' '### Repository variables validation result' - printf '%s\n' 'Status: OK' - printf '%s\n' 'All required repository variables present.' - printf '%s\n' '' - printf '%s\n' '**Note**: Organization secrets (RS_FTP_HOST, RS_FTP_USER, etc.) are validated at deployment time, not in repository health checks.' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - scripts_governance: name: Scripts governance needs: access_check @@ -257,14 +156,14 @@ jobs: profile="${PROFILE_RAW:-all}" case "${profile}" in - all|release|scripts|repo) ;; + all|scripts|repo) ;; *) printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" exit 1 ;; esac - if [ "${profile}" = 'release' ] || [ "${profile}" = 'repo' ]; then + if [ "${profile}" = 'repo' ]; then { printf '%s\n' '### Scripts governance' printf '%s\n' "Profile: ${profile}" @@ -371,14 +270,14 @@ jobs: profile="${PROFILE_RAW:-all}" case "${profile}" in - all|release|scripts|repo) ;; + all|scripts|repo) ;; *) printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" exit 1 ;; esac - if [ "${profile}" = 'release' ] || [ "${profile}" = 'scripts' ]; then + if [ "${profile}" = 'scripts' ]; then { printf '%s\n' '### Repository health' printf '%s\n' "Profile: ${profile}" @@ -705,7 +604,7 @@ jobs: printf '%s\n' '| Domain | Status | Notes |' printf '%s\n' '|---|---|---|' printf '%s\n' '| Access control | OK | Admin-only execution gate |' - printf '%s\n' '| Release variables | OK | Repository variables validation |' + printf '%s\n' '| Release policy | N/A | Releases handled by MokoGitea |' printf '%s\n' '| Scripts governance | OK | Directory policy and advisory reporting |' printf '%s\n' '| Repo required artifacts | OK | Required, optional, disallowed enforcement |' printf '%s\n' '| Repo content heuristics | OK | Brand, license, changelog structure |' @@ -774,11 +673,10 @@ jobs: report-issues: name: "Report Issues" runs-on: ubuntu-latest - needs: [access_check, release_config, scripts_governance, repo_health] + needs: [access_check, scripts_governance, repo_health] if: >- always() && - (needs.release_config.result == 'failure' || - needs.scripts_governance.result == 'failure' || + (needs.scripts_governance.result == 'failure' || needs.repo_health.result == 'failure') steps: @@ -804,10 +702,6 @@ jobs: fi } - report_gate "Release Configuration" \ - "${{ needs.release_config.result }}" \ - "Required repository variables are missing (RS_FTP_PATH_SUFFIX). Check repository settings." - report_gate "Scripts Governance" \ "${{ needs.scripts_governance.result }}" \ "Scripts directory policy violations detected. Review required and allowed directories." @@ -815,4 +709,3 @@ jobs: report_gate "Repository Health" \ "${{ needs.repo_health.result }}" \ "Repository health checks failed — missing required artifacts, disallowed files, or content warnings. Check the CI run summary." - From 364bba02713839a14e3a769b454ee7d0ac3e3f3f Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Thu, 4 Jun 2026 14:21:09 +0000 Subject: [PATCH 094/132] chore: sync .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] --- .mokogitea/workflows/auto-release.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml index 2325032..44a2d64 100644 --- a/.mokogitea/workflows/auto-release.yml +++ b/.mokogitea/workflows/auto-release.yml @@ -102,13 +102,14 @@ jobs: run: | php /tmp/moko-platform-api/cli/release_publish.php \ --path . --stability rc --bump minor --branch rc \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" + --token "${{ secrets.MOKOGITEA_TOKEN }}" \ + --skip-update-stream - name: Summary if: always() run: | echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY - echo "Branch renamed to rc, minor bump, RC + lesser stream releases built, updates.xml synced" >> $GITHUB_STEP_SUMMARY + echo "Branch renamed to rc, minor bump, RC release built (updates.xml managed by Gitea Pages)" >> $GITHUB_STEP_SUMMARY # ── Merged PR → Build & Release (or promote RC to stable) ──────────────────── release: @@ -167,7 +168,8 @@ jobs: run: | php /tmp/moko-platform-api/cli/release_publish.php \ --path . --stability stable --bump minor --branch main \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" + --token "${{ secrets.MOKOGITEA_TOKEN }}" \ + --skip-update-stream # -- STEP 9: Mirror to GitHub (stable only) -------------------------------- - name: "Step 9: Mirror release to GitHub" From 40de6532d63248245b190113e456d98245a51826 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Thu, 4 Jun 2026 14:33:53 +0000 Subject: [PATCH 095/132] feat(update): migrate update server URL to Gitea Pages [skip ci] --- src/pkg_mokoog.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pkg_mokoog.xml b/src/pkg_mokoog.xml index a68ab75..ad58838 100644 --- a/src/pkg_mokoog.xml +++ b/src/pkg_mokoog.xml @@ -31,6 +31,6 @@ </languages> <updateservers> - <server type="extension" name="MokoJoomOpenGraph Updates">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/raw/branch/main/updates.xml</server> + <server type="extension" name="MokoJoomOpenGraph Updates">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/updates.xml</server> </updateservers> </extension> From f960b37b33aa662d530a5360186ca9a3dceecec5 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Thu, 4 Jun 2026 15:14:19 +0000 Subject: [PATCH 096/132] chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] --- .mokogitea/workflows/pr-check.yml | 95 +++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/.mokogitea/workflows/pr-check.yml b/.mokogitea/workflows/pr-check.yml index 0ac0ef1..9d0cb35 100644 --- a/.mokogitea/workflows/pr-check.yml +++ b/.mokogitea/workflows/pr-check.yml @@ -196,6 +196,101 @@ jobs: ;; esac + - name: Validate Joomla language files + if: steps.platform.outputs.platform == 'joomla' + run: | + ERRORS=0 + WARNINGS=0 + + # Find all .ini language files + INI_FILES=$(find . -path "*/language/*/*.ini" -not -path "./.git/*" 2>/dev/null) + if [ -z "$INI_FILES" ]; then + echo "No .ini language files found — skipping" + exit 0 + fi + + echo "Found $(echo "$INI_FILES" | wc -l) language file(s)" + + for FILE in $INI_FILES; do + FNAME=$(basename "$FILE") + LINENUM=0 + SEEN_KEYS="" + + while IFS= read -r line || [ -n "$line" ]; do + LINENUM=$((LINENUM + 1)) + + # Skip empty lines and comments + [ -z "$line" ] && continue + echo "$line" | grep -qE '^\s*;' && continue + echo "$line" | grep -qE '^\s*$' && continue + + # Must match KEY="VALUE" format + if ! echo "$line" | grep -qE '^[A-Z_][A-Z0-9_]*=".*"$'; then + echo "::error file=${FILE},line=${LINENUM}::Malformed line: ${line}" + ERRORS=$((ERRORS + 1)) + continue + fi + + # Extract key and check for duplicates + KEY=$(echo "$line" | sed 's/=.*//') + if echo "$SEEN_KEYS" | grep -qx "$KEY"; then + echo "::error file=${FILE},line=${LINENUM}::Duplicate key: ${KEY}" + ERRORS=$((ERRORS + 1)) + fi + SEEN_KEYS="${SEEN_KEYS} + ${KEY}" + done < "$FILE" + + echo " ${FILE}: checked ${LINENUM} lines" + done + + # Cross-check en-GB vs en-US key consistency + GB_DIR=$(find . -path "*/language/en-GB" -type d -not -path "./.git/*" 2>/dev/null | head -1) + US_DIR=$(find . -path "*/language/en-US" -type d -not -path "./.git/*" 2>/dev/null | head -1) + + if [ -n "$GB_DIR" ] && [ -n "$US_DIR" ]; then + for GB_FILE in "$GB_DIR"/*.ini; do + [ ! -f "$GB_FILE" ] && continue + FNAME=$(basename "$GB_FILE") + US_FILE="$US_DIR/$FNAME" + [ ! -f "$US_FILE" ] && continue + + GB_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$GB_FILE" 2>/dev/null | sort) + US_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$US_FILE" 2>/dev/null | sort) + + # Keys in en-GB but not en-US + MISSING_US=$(comm -23 <(echo "$GB_KEYS") <(echo "$US_KEYS")) + if [ -n "$MISSING_US" ]; then + echo "::warning::Keys in en-GB/$FNAME but missing from en-US/$FNAME:" + echo "$MISSING_US" | while read -r k; do echo " - $k"; done + WARNINGS=$((WARNINGS + 1)) + fi + + # Keys in en-US but not en-GB + MISSING_GB=$(comm -13 <(echo "$GB_KEYS") <(echo "$US_KEYS")) + if [ -n "$MISSING_GB" ]; then + echo "::warning::Keys in en-US/$FNAME but missing from en-GB/$FNAME:" + echo "$MISSING_GB" | while read -r k; do echo " - $k"; done + WARNINGS=$((WARNINGS + 1)) + fi + done + fi + + { + echo "### Language File Validation" + echo "| Metric | Count |" + echo "|---|---|" + echo "| Files checked | $(echo "$INI_FILES" | wc -l) |" + echo "| Errors | ${ERRORS} |" + echo "| Warnings | ${WARNINGS} |" + } >> $GITHUB_STEP_SUMMARY + + if [ "$ERRORS" -gt 0 ]; then + echo "::error::Language validation failed with ${ERRORS} error(s)" + exit 1 + fi + echo "Language files: OK (${WARNINGS} warning(s))" + - name: Check changelog has unreleased entry run: | if [ ! -f "CHANGELOG.md" ]; then From daa17a627ee17cad5b459ebc5916f5ba50fb6991 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Thu, 4 Jun 2026 15:27:11 +0000 Subject: [PATCH 097/132] chore: remove updates.xml [skip ci] --- updates.xml | 46 ---------------------------------------------- 1 file changed, 46 deletions(-) delete mode 100644 updates.xml diff --git a/updates.xml b/updates.xml deleted file mode 100644 index 084618e..0000000 --- a/updates.xml +++ /dev/null @@ -1,46 +0,0 @@ -<?xml version='1.0' encoding='UTF-8'?> -<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> - SPDX-License-Identifier: GPL-3.0-or-later - VERSION: 01.00.01-dev - --> - -<updates> - <update> - <name>Package - MokoJoomOpenGraph</name> - <description>Package - MokoJoomOpenGraph development build.</description> - <element>pkg_mokoog</element> - <type>package</type> - <client>site</client> - <version>01.00.01-dev</version> - <creationDate>2026-05-31</creationDate> - <infourl title='Package - MokoJoomOpenGraph'>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/releases/tag/development</infourl> - <downloads> - <downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/releases/download/development/pkg_mokoog-01.00.01-dev.zip</downloadurl> - </downloads> - <sha256>1dd3a62c6a4cbec7406090e5618fb8b618b2d9a2b22bb7720fbaf38a681bb340</sha256> - <tags><tag>dev</tag></tags> - <changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/raw/branch/main/CHANGELOG.md</changelogurl> - <maintainer>Moko Consulting</maintainer> - <maintainerurl>https://mokoconsulting.tech</maintainerurl> - <targetplatform name="joomla" version="(5|6)\..*" /> - </update> - <update> - <name>Package - MokoJoomOpenGraph</name> - <description>Package - MokoJoomOpenGraph release-candidate build.</description> - <element>pkg_mokoog</element> - <type>package</type> - <client>site</client> - <version>01.00.01</version> - <creationDate>2026-05-31</creationDate> - <infourl title="Package - MokoJoomOpenGraph">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/releases/tag/release-candidate</infourl> - <downloads> - <downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/releases/download/release-candidate/pkg_mokoog-01.00.01.zip</downloadurl> - </downloads> - <sha256>6fe8c8da51782b4f1a57d0acb9529f3e7b22471445119bf61a8de7d682be8585</sha256> - <tags><tag>release-candidate</tag></tags> - <changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/raw/branch/main/CHANGELOG.md</changelogurl> - <maintainer>Moko Consulting</maintainer> - <maintainerurl>https://mokoconsulting.tech</maintainerurl> - <targetplatform name="joomla" version="(5|6)\..*"/> - </update> -</updates> From 8b7b84dd41df61f0fce7fe570141c90cc522851b Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Thu, 4 Jun 2026 15:30:13 +0000 Subject: [PATCH 098/132] chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] --- .mokogitea/workflows/pr-check.yml | 39 ++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/.mokogitea/workflows/pr-check.yml b/.mokogitea/workflows/pr-check.yml index 9d0cb35..473eeb2 100644 --- a/.mokogitea/workflows/pr-check.yml +++ b/.mokogitea/workflows/pr-check.yml @@ -202,10 +202,47 @@ jobs: ERRORS=0 WARNINGS=0 + # Require both en-GB and en-US language directories + LANG_ROOT=$(find . -path "*/language" -type d -not -path "./.git/*" 2>/dev/null | head -1) + if [ -z "$LANG_ROOT" ]; then + echo "No language/ directory found — skipping" + exit 0 + fi + + if [ ! -d "$LANG_ROOT/en-GB" ]; then + echo "::error::Missing en-GB language directory (${LANG_ROOT}/en-GB)" + ERRORS=$((ERRORS + 1)) + fi + if [ ! -d "$LANG_ROOT/en-US" ]; then + echo "::error::Missing en-US language directory (${LANG_ROOT}/en-US)" + ERRORS=$((ERRORS + 1)) + fi + + # Check that en-GB and en-US have matching .ini files + if [ -d "$LANG_ROOT/en-GB" ] && [ -d "$LANG_ROOT/en-US" ]; then + for GB_INI in "$LANG_ROOT/en-GB"/*.ini; do + [ ! -f "$GB_INI" ] && continue + US_INI="$LANG_ROOT/en-US/$(basename "$GB_INI")" + if [ ! -f "$US_INI" ]; then + echo "::error::$(basename "$GB_INI") exists in en-GB but missing from en-US" + ERRORS=$((ERRORS + 1)) + fi + done + for US_INI in "$LANG_ROOT/en-US"/*.ini; do + [ ! -f "$US_INI" ] && continue + GB_INI="$LANG_ROOT/en-GB/$(basename "$US_INI")" + if [ ! -f "$GB_INI" ]; then + echo "::error::$(basename "$US_INI") exists in en-US but missing from en-GB" + ERRORS=$((ERRORS + 1)) + fi + done + fi + # Find all .ini language files INI_FILES=$(find . -path "*/language/*/*.ini" -not -path "./.git/*" 2>/dev/null) if [ -z "$INI_FILES" ]; then - echo "No .ini language files found — skipping" + echo "No .ini language files found" + [ "$ERRORS" -gt 0 ] && exit 1 exit 0 fi From 7de46486e6e4bd64e949daa970cb34a81406a68a Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Thu, 4 Jun 2026 15:39:16 +0000 Subject: [PATCH 099/132] chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] --- .mokogitea/workflows/pr-check.yml | 92 +++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/.mokogitea/workflows/pr-check.yml b/.mokogitea/workflows/pr-check.yml index 473eeb2..3dd7540 100644 --- a/.mokogitea/workflows/pr-check.yml +++ b/.mokogitea/workflows/pr-check.yml @@ -147,6 +147,98 @@ jobs: echo "PHP lint: ${ERRORS} error(s)" [ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; } + - name: Joomla JEXEC guard check + if: steps.platform.outputs.platform == 'joomla' + run: | + ERRORS=0 + while IFS= read -r -d '' file; do + # Skip vendor, node_modules, and index.html stub files + case "$file" in ./vendor/*|./node_modules/*) continue ;; esac + # Check first 10 lines for JEXEC or JPATH guard + if ! head -20 "$file" | grep -qE "defined\s*\(\s*['\"](_JEXEC|JPATH_BASE|\\\\JPATH_PLATFORM)['\"]"; then + echo "::error file=${file}::Missing JEXEC guard: ${file}" + ERRORS=$((ERRORS + 1)) + fi + 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 src/ are missing the Joomla execution guard." >> $GITHUB_STEP_SUMMARY + exit 1 + fi + echo "JEXEC guard: OK" + + - name: Joomla directory listing protection + if: steps.platform.outputs.platform == 'joomla' + run: | + MISSING=0 + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && exit 0 + while IFS= read -r dir; do + if [ ! -f "${dir}/index.html" ]; then + echo "::warning::Missing index.html in ${dir} (directory listing protection)" + MISSING=$((MISSING + 1)) + fi + done < <(find "$SOURCE_DIR" -type d -not -path "./.git/*" -not -path "*/vendor/*" -not -path "*/node_modules/*") + if [ "$MISSING" -gt 0 ]; then + echo "## Directory Protection" >> $GITHUB_STEP_SUMMARY + echo "${MISSING} director(ies) missing index.html" >> $GITHUB_STEP_SUMMARY + fi + echo "Directory protection: ${MISSING} missing (advisory)" + + - name: Joomla script file and asset checks + if: steps.platform.outputs.platform == 'joomla' + run: | + ERRORS=0 + MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1) + [ -z "$MANIFEST" ] && exit 0 + MANIFEST_DIR=$(dirname "$MANIFEST") + + # Check scriptfile exists if declared + SCRIPTFILE=$(sed -n 's/.*<scriptfile>\([^<]*\)<\/scriptfile>.*/\1/p' "$MANIFEST" 2>/dev/null) + if [ -n "$SCRIPTFILE" ]; then + if [ ! -f "${MANIFEST_DIR}/${SCRIPTFILE}" ]; then + echo "::error::Manifest declares <scriptfile>${SCRIPTFILE}</scriptfile> but file not found at ${MANIFEST_DIR}/${SCRIPTFILE}" + ERRORS=$((ERRORS + 1)) + else + echo "Script file: ${MANIFEST_DIR}/${SCRIPTFILE} (OK)" + fi + fi + + # Require joomla.asset.json and validate it + ASSET_JSON=$(find "$MANIFEST_DIR" -name "joomla.asset.json" -not -path "./.git/*" 2>/dev/null | head -1) + if [ -z "$ASSET_JSON" ]; then + echo "::error::joomla.asset.json not found — Joomla asset system is required" + ERRORS=$((ERRORS + 1)) + else + if command -v php &> /dev/null; then + php -r "json_decode(file_get_contents('$ASSET_JSON')); if(json_last_error()!==JSON_ERROR_NONE){echo json_last_error_msg();exit(1);}" 2>&1 || { + echo "::error::joomla.asset.json is not valid JSON" + ERRORS=$((ERRORS + 1)) + } + fi + echo "joomla.asset.json: valid" + fi + + # 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 + if ! php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$xmlfile'); if(!\$x){foreach(libxml_get_errors() as \$e) echo trim(\$e->message) . ' in $xmlfile'; exit(1);}" 2>&1; then + XML_ERRORS=$((XML_ERRORS + 1)) + fi + done < <(find "$MANIFEST_DIR" -name "*.xml" -not -path "./.git/*" -print0) + fi + if [ "$XML_ERRORS" -gt 0 ]; then + echo "::error::${XML_ERRORS} XML file(s) are malformed" + ERRORS=$((ERRORS + 1)) + else + echo "XML well-formedness: OK" + fi + + [ "$ERRORS" -gt 0 ] && exit 1 + echo "Joomla asset checks: OK" + - name: Validate platform manifest run: | PLATFORM="${{ steps.platform.outputs.platform }}" From b2d2ae9eff9ae942344f74c60baf3795e45212db Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Thu, 4 Jun 2026 15:56:48 +0000 Subject: [PATCH 100/132] chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] --- .mokogitea/workflows/pr-check.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.mokogitea/workflows/pr-check.yml b/.mokogitea/workflows/pr-check.yml index 3dd7540..4d78d7a 100644 --- a/.mokogitea/workflows/pr-check.yml +++ b/.mokogitea/workflows/pr-check.yml @@ -256,6 +256,13 @@ jobs: for ELEMENT in name version description; do grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; } done + # Block legacy raw/branch update server URLs on MokoGitea + RAW_URLS=$(grep -n 'raw/branch' "$MANIFEST" | grep -i 'mokoconsulting\|mokogitea\|git\.mokoconsulting\.tech' || true) + if [ -n "$RAW_URLS" ]; then + echo "::error::Manifest contains legacy raw/branch update server URL on MokoGitea. Use the Gitea Pages URL instead (e.g. /{REPO}/updates.xml not /{REPO}/raw/branch/main/updates.xml)" + echo "$RAW_URLS" + exit 1 + fi echo "Joomla manifest valid" ;; dolibarr) From 8fe846928752bc87eba8e2dd1a1b01c74f4feb6b Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Thu, 4 Jun 2026 22:02:38 +0000 Subject: [PATCH 101/132] chore: add dlid and blockChildUninstall to package manifest [skip ci] --- src/pkg_mokoog.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pkg_mokoog.xml b/src/pkg_mokoog.xml index ad58838..4f7a54a 100644 --- a/src/pkg_mokoog.xml +++ b/src/pkg_mokoog.xml @@ -33,4 +33,6 @@ <updateservers> <server type="extension" name="MokoJoomOpenGraph Updates">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/updates.xml</server> </updateservers> + <dlid prefix="dlid=" suffix=""/> + <blockChildUninstall>true</blockChildUninstall> </extension> From 252d75c44f68e94731fd3641c0816db3f6c76f2f Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sat, 6 Jun 2026 07:12:40 -0500 Subject: [PATCH 102/132] fix: resolve 5 bugs found during code assessment - fix(batch): use offset=0 for self-consuming LEFT JOIN query that excludes already-processed articles, preventing chunk skips - fix(license): move session flag after DB query succeeds so a failed check retries on next page load instead of silently giving up - fix(og:image): detect actual image dimensions via getimagesize() instead of hardcoding 1200x630 which was wrong for unresized, small, or external images - fix(i18n): use mb_strlen() consistently with mb_substr() for multibyte-safe description truncation across all 4 call sites - fix(ImageGenerator): guard wrapText truncation when third line is shorter than 3 characters to prevent broken output Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- .../src/ContentType/HikaShopAdapter.php | 2 +- .../com_mokoog/src/ContentType/K2Adapter.php | 2 +- .../src/Controller/BatchController.php | 9 +- src/packages/com_mokoog/tmpl/tags/default.php | 11 +- .../src/Extension/MokoOG.php | 121 +++++++++++++++++- .../src/Helper/ImageGenerator.php | 7 +- 6 files changed, 135 insertions(+), 17 deletions(-) diff --git a/src/packages/com_mokoog/src/ContentType/HikaShopAdapter.php b/src/packages/com_mokoog/src/ContentType/HikaShopAdapter.php index 07150b3..87f9b2f 100644 --- a/src/packages/com_mokoog/src/ContentType/HikaShopAdapter.php +++ b/src/packages/com_mokoog/src/ContentType/HikaShopAdapter.php @@ -52,7 +52,7 @@ class HikaShopAdapter implements ContentTypeInterface $text = strip_tags($text); $text = trim(preg_replace('/\s+/', ' ', $text)); - if (\strlen($text) > 160) { + if (mb_strlen($text) > 160) { $text = mb_substr($text, 0, 157) . '...'; } diff --git a/src/packages/com_mokoog/src/ContentType/K2Adapter.php b/src/packages/com_mokoog/src/ContentType/K2Adapter.php index ec3f017..028d0c4 100644 --- a/src/packages/com_mokoog/src/ContentType/K2Adapter.php +++ b/src/packages/com_mokoog/src/ContentType/K2Adapter.php @@ -52,7 +52,7 @@ class K2Adapter implements ContentTypeInterface $text = strip_tags($text); $text = trim(preg_replace('/\s+/', ' ', $text)); - if (\strlen($text) > 160) { + if (mb_strlen($text) > 160) { $text = mb_substr($text, 0, 157) . '...'; } diff --git a/src/packages/com_mokoog/src/Controller/BatchController.php b/src/packages/com_mokoog/src/Controller/BatchController.php index ca9aaeb..1f86892 100644 --- a/src/packages/com_mokoog/src/Controller/BatchController.php +++ b/src/packages/com_mokoog/src/Controller/BatchController.php @@ -67,7 +67,6 @@ class BatchController extends BaseController } $app = Factory::getApplication(); - $offset = $app->getInput()->getInt('offset', 0); $limit = $app->getInput()->getInt('limit', 50); $db = Factory::getDbo(); @@ -85,7 +84,9 @@ class BatchController extends BaseController ->where($db->quoteName('t.id') . ' IS NULL') ->order($db->quoteName('c.id') . ' ASC'); - $db->setQuery($query, $offset, $limit); + // Always offset=0: processed articles now have #__mokoog_tags rows + // and are excluded by the LEFT JOIN ... IS NULL filter automatically. + $db->setQuery($query, 0, $limit); $articles = $db->loadObjectList(); $created = 0; @@ -118,8 +119,6 @@ class BatchController extends BaseController echo new JsonResponse([ 'created' => $created, - 'offset' => $offset, - 'processed' => $offset + $created, ]); $app->close(); @@ -144,7 +143,7 @@ class BatchController extends BaseController $text = strip_tags($text); $text = trim(preg_replace('/\s+/', ' ', $text)); - if (\strlen($text) > 160) { + if (mb_strlen($text) > 160) { $text = mb_substr($text, 0, 157) . '...'; } diff --git a/src/packages/com_mokoog/tmpl/tags/default.php b/src/packages/com_mokoog/tmpl/tags/default.php index 0543271..6ca4e90 100644 --- a/src/packages/com_mokoog/tmpl/tags/default.php +++ b/src/packages/com_mokoog/tmpl/tags/default.php @@ -215,22 +215,23 @@ document.addEventListener('DOMContentLoaded', function() { }); } - function processChunk(offset, total, chunkSize, token, bar, status) { - fetch('index.php?option=com_mokoog&task=batch.process&format=json&offset=' + offset + '&limit=' + chunkSize + '&' + token + '=1') + function processChunk(processed, total, chunkSize, token, bar, status) { + // Always offset=0: processed items are excluded by the IS NULL filter + fetch('index.php?option=com_mokoog&task=batch.process&format=json&limit=' + chunkSize + '&' + token + '=1') .then(function(r) { return r.json(); }) .then(function(resp) { - var processed = resp.data.processed; + processed += resp.data.created; var pct = Math.min(100, Math.round((processed / total) * 100)); bar.style.width = pct + '%'; bar.textContent = pct + '%'; status.textContent = processed + ' / ' + total + ' <?php echo Text::_('COM_MOKOOG_BATCH_PROCESSED', true); ?>'; - if (processed < total) { + if (resp.data.created > 0 && processed < total) { processChunk(processed, total, chunkSize, token, bar, status); } else { bar.classList.remove('progress-bar-animated'); bar.classList.add('bg-success'); - status.textContent = '<?php echo Text::_('COM_MOKOOG_BATCH_COMPLETE', true); ?> ' + total + ' articles.'; + status.textContent = '<?php echo Text::_('COM_MOKOOG_BATCH_COMPLETE', true); ?> ' + processed + ' articles.'; setTimeout(function() { location.reload(); }, 2000); } }) diff --git a/src/packages/plg_system_mokoog/src/Extension/MokoOG.php b/src/packages/plg_system_mokoog/src/Extension/MokoOG.php index 0b68cc7..5655dc8 100644 --- a/src/packages/plg_system_mokoog/src/Extension/MokoOG.php +++ b/src/packages/plg_system_mokoog/src/Extension/MokoOG.php @@ -35,10 +35,27 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface public static function getSubscribedEvents(): array { return [ + 'onAfterRoute' => 'onAfterRoute', 'onBeforeCompileHead' => 'onBeforeCompileHead', ]; } + /** + * Run admin-side license key check after routing. + * + * @param Event $event The event object + * + * @return void + */ + public function onAfterRoute(Event $event): void + { + $app = $this->getApplication(); + + if ($app->isClient('administrator')) { + $this->warnMissingLicenseKey(); + } + } + /** * Inject Open Graph and Twitter Card meta tags before the document head is compiled. * @@ -108,9 +125,13 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface $imageUrl = $this->resolveImageUrl($image); $doc->setMetaData('og:image', $imageUrl, 'property'); - // Image dimensions help Facebook, LinkedIn, and Discord render previews faster - $doc->setMetaData('og:image:width', '1200', 'property'); - $doc->setMetaData('og:image:height', '630', 'property'); + // Emit actual image dimensions when detectable + $imageDims = $this->getImageDimensions($image); + + if ($imageDims) { + $doc->setMetaData('og:image:width', (string) $imageDims[0], 'property'); + $doc->setMetaData('og:image:height', (string) $imageDims[1], 'property'); + } } // og:locale from current language @@ -355,7 +376,7 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface $description = trim(preg_replace('/\s+/', ' ', $description)); - if (\strlen($description) > $maxLength) { + if (mb_strlen($description) > $maxLength) { $description = mb_substr($description, 0, $maxLength - 3) . '...'; } @@ -487,4 +508,96 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface return $db->loadResult() ?: ''; } + + /** + * Warn administrators once per session when no license key is configured. + * + * @return void + */ + private function warnMissingLicenseKey(): void + { + $session = Factory::getSession(); + + if ($session->get('mokoog.license_warned', false)) { + return; + } + + $user = Factory::getUser(); + + if ($user->guest || !$user->authorise('core.manage')) { + return; + } + + try { + $db = Factory::getDbo(); + + $query = $db->getQuery(true) + ->select($db->quoteName('extra_query')) + ->from($db->quoteName('#__update_sites')) + ->where($db->quoteName('name') . ' = ' . $db->quote('MokoJoomOpenGraph Updates')) + ->setLimit(1); + $db->setQuery($query); + $extraQuery = (string) $db->loadResult(); + + // Mark as checked only after the DB query succeeds + $session->set('mokoog.license_warned', true); + + if (!empty($extraQuery)) { + parse_str($extraQuery, $parsed); + + if (!empty($parsed['dlid']) && preg_match('/^MOKO-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$/', $parsed['dlid'])) { + return; + } + } + + $this->getApplication()->enqueueMessage( + '<strong>Moko Consulting License Key Required</strong> — ' + . '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.', + 'warning' + ); + } catch (\Throwable $e) { + // Don't break admin over a license check + } + } + + /** + * Get the actual pixel dimensions of a local image. + * + * Returns [width, height] or null for external URLs or unreadable images. + * + * @param string $image Image path (relative or absolute URL) + * + * @return array{0: int, 1: int}|null + */ + private function getImageDimensions(string $image): ?array + { + // Cannot determine dimensions for external URLs + if (str_starts_with($image, 'http://') || str_starts_with($image, 'https://')) { + return null; + } + + // If auto-resize is on, the resized image lives in the generated dir + if ($this->params->get('auto_resize', 1)) { + $resolved = ImageHelper::resize($image); + } else { + $resolved = $image; + } + + $absPath = JPATH_ROOT . '/' . ltrim($resolved, '/'); + + if (!is_file($absPath)) { + return null; + } + + $info = @getimagesize($absPath); + + if (!$info) { + return null; + } + + return [$info[0], $info[1]]; + } } diff --git a/src/packages/plg_system_mokoog/src/Helper/ImageGenerator.php b/src/packages/plg_system_mokoog/src/Helper/ImageGenerator.php index ece760e..9f94980 100644 --- a/src/packages/plg_system_mokoog/src/Helper/ImageGenerator.php +++ b/src/packages/plg_system_mokoog/src/Helper/ImageGenerator.php @@ -152,7 +152,12 @@ class ImageGenerator // Limit to 3 lines, truncate last line if needed if (\count($lines) > 3) { $lines = \array_slice($lines, 0, 3); - $lines[2] = mb_substr($lines[2], 0, -3) . '...'; + + if (mb_strlen($lines[2]) > 3) { + $lines[2] = mb_substr($lines[2], 0, -3) . '...'; + } else { + $lines[2] .= '...'; + } } return implode("\n", $lines); From 3d315df905704dd781c98f57bd80593c86884a5e Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sat, 6 Jun 2026 12:32:13 +0000 Subject: [PATCH 103/132] chore: add .mokogitea/workflows/pre-release.yml from moko-platform [skip ci] --- .mokogitea/workflows/pre-release.yml | 241 +++++++++++++++++++++++++++ 1 file changed, 241 insertions(+) create mode 100644 .mokogitea/workflows/pre-release.yml diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml new file mode 100644 index 0000000..1a9eeef --- /dev/null +++ b/.mokogitea/workflows/pre-release.yml @@ -0,0 +1,241 @@ +# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: moko-platform.Release +# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform +# PATH: /templates/workflows/universal/pre-release.yml.template +# VERSION: 05.01.00 +# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch + +name: "Universal: Pre-Release" + +on: + pull_request: + types: [closed] + branches: + - dev + pull_request_target: + types: [synchronize, opened, reopened] + branches: + - main + workflow_dispatch: + inputs: + stability: + description: 'Pre-release channel' + required: true + type: choice + options: + - development + - alpha + - beta + - release-candidate + +permissions: + contents: write + +env: + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + +jobs: + build: + name: "Build Pre-Release (${{ inputs.stability || 'development' }})" + runs-on: release + if: >- + github.event_name == 'workflow_dispatch' || + (github.event_name == 'pull_request' && github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev') || + (github.event_name == 'pull_request_target' && github.event.pull_request.base.ref == 'main') + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.MOKOGITEA_TOKEN }} + ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || '' }} + + - name: Setup moko-platform 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/vendor/autoload.php” ]; then + echo “Using pre-installed /opt/moko-platform” + echo “MOKO_CLI=/opt/moko-platform/cli” >> “$GITHUB_ENV” + else + echo “Falling back to fresh clone” + if ! command -v composer &> /dev/null; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + rm -rf /tmp/moko-platform-api + git clone --depth 1 --branch main --quiet \ + “https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git” \ + /tmp/moko-platform-api + cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet + echo “MOKO_CLI=/tmp/moko-platform-api/cli” >> “$GITHUB_ENV” + fi + + - name: Detect platform + id: platform + run: | + php ${MOKO_CLI}/manifest_read.php --path . --github-output + + - name: Resolve metadata and bump version + id: meta + run: | + # Auto-detect stability: RC for PRs targeting main, else use input or default to development + if [ "${{ github.event_name }}" = "pull_request_target" ] && [ "${{ github.event.pull_request.base.ref }}" = "main" ]; then + STABILITY="release-candidate" + else + STABILITY="${{ inputs.stability || 'development' }}" + fi + + case "$STABILITY" in + development) SUFFIX="-dev"; TAG="development" ;; + alpha) SUFFIX="-alpha"; TAG="alpha" ;; + beta) SUFFIX="-beta"; TAG="beta" ;; + release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;; + esac + + # Bump version via CLI: patch for dev/alpha/beta, minor for RC + case "$STABILITY" in + release-candidate) BUMP="minor" ;; + *) BUMP="patch" ;; + esac + + php ${MOKO_CLI}/version_bump.php --path . $([ "$BUMP" = "minor" ] && echo "--minor") 2>/dev/null || true + + # Set stability suffix and verify consistency + VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "00.00.01") + VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//') + + php ${MOKO_CLI}/version_set_platform.php \ + --path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true + php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true + + # Append suffix for output + if [ -n "$SUFFIX" ]; then + VERSION="${VERSION}${SUFFIX}" + fi + + # Commit version bump + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + git add -A + git diff --cached --quiet || { + git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]" + git push origin HEAD 2>&1 + } + + # Auto-detect element via manifest_element.php + php ${MOKO_CLI}/manifest_element.php \ + --path . --version "$VERSION" --stability "$STABILITY" \ + --repo "${GITEA_REPO}" --github-output + + # Read back element outputs + EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) + ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) + [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') + [ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip" + + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT" + echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT" + echo "tag=${TAG}" >> "$GITHUB_OUTPUT" + echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT" + echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT" + + echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ===" + + - name: Create release + id: release + run: | + TAG="${{ steps.meta.outputs.tag }}" + VERSION="${{ steps.meta.outputs.version }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php ${MOKO_CLI}/release_create.php \ + --path . --version "$VERSION" --tag "$TAG" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ + --repo "${GITEA_REPO}" --branch dev --prerelease + + - name: Update release notes from CHANGELOG.md + run: | + TAG="${{ steps.meta.outputs.tag }}" + VERSION="${{ steps.meta.outputs.version }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + + # Extract [Unreleased] section from changelog (everything between [Unreleased] and next ## heading) + if [ -f "CHANGELOG.md" ]; then + NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md) + [ -z "$NOTES" ] && NOTES="Release ${VERSION}" + else + NOTES="Release ${VERSION}" + fi + + # Update release body via API + RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \ + "${API_BASE}/releases/tags/${TAG}" | 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 ${{ secrets.MOKOGITEA_TOKEN }}', + 'Content-Type': 'application/json' + }) + urllib.request.urlopen(req) + " <<< "$NOTES" + echo "Release notes updated from CHANGELOG.md" + fi + + - name: Build package and upload + id: package + run: | + VERSION="${{ steps.meta.outputs.version }}" + TAG="${{ steps.meta.outputs.tag }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php ${MOKO_CLI}/release_package.php \ + --path . --version "$VERSION" --tag "$TAG" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ + --repo "${GITEA_REPO}" --output /tmp || true + + # updates.xml is generated dynamically by MokoGitea license server + # No need to build, commit, or sync updates.xml from workflows + + - name: "Delete lesser pre-release channels (cascade)" + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" + + php ${MOKO_CLI}/release_cascade.php \ + --stability "${{ steps.meta.outputs.stability }}" \ + --token "${TOKEN}" \ + --api-base "${API_BASE}" + + - name: Summary + if: always() + run: | + VERSION="${{ steps.meta.outputs.version }}" + STABILITY="${{ steps.meta.outputs.stability }}" + ZIP_NAME="${{ steps.meta.outputs.zip_name }}" + SHA256="${{ steps.package.outputs.sha256_zip }}" + echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY + echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY + echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY From 05b2a44209b79f2ff5467fafbffa957957708c54 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sat, 6 Jun 2026 08:09:39 -0500 Subject: [PATCH 104/132] refactor: rename src/ to source/ and update all CI references - Rename project source directory from src/ to source/ - Update CI workflows (ci-joomla, pr-check, repo-health) to check source/ first, falling back to src/ and htdocs/ for compat - Update .gitignore vendor exception path - manifest.xml entry-point already updated Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- .gitignore | 2 +- .mokogitea/manifest.xml | 2 +- .mokogitea/workflows/ci-joomla.yml | 12 +- .mokogitea/workflows/pr-check.yml | 1017 ++++++------ .mokogitea/workflows/repo-health.yml | 1424 +++++++++-------- {src => source}/index.html | 0 {src => source}/language/en-GB/index.html | 0 .../language/en-GB/pkg_mokoog.sys.ini | 0 {src => source}/language/en-US/index.html | 0 .../language/en-US/pkg_mokoog.sys.ini | 0 {src => source}/language/index.html | 0 .../packages/com_mokoog/api/index.html | 0 .../api/src/Controller/TagsController.php | 0 .../com_mokoog/api/src/Controller/index.html | 0 .../api/src/View/Tags/JsonapiView.php | 0 .../com_mokoog/api/src/View/Tags/index.html | 0 .../com_mokoog/api/src/View/index.html | 0 .../packages/com_mokoog/api/src/index.html | 0 .../packages/com_mokoog/forms/filter_tags.xml | 0 .../packages/com_mokoog/forms/index.html | 0 .../packages/com_mokoog/forms/tag.xml | 0 .../packages/com_mokoog/index.html | 0 .../com_mokoog/language/en-GB/com_mokoog.ini | 0 .../language/en-GB/com_mokoog.sys.ini | 0 .../com_mokoog/language/en-GB/index.html | 0 .../com_mokoog/language/en-US/com_mokoog.ini | 0 .../language/en-US/com_mokoog.sys.ini | 0 .../com_mokoog/language/en-US/index.html | 0 .../packages/com_mokoog/language/index.html | 0 .../packages/com_mokoog/mokoog.xml | 0 .../packages/com_mokoog/script.php | 0 .../packages/com_mokoog/services/index.html | 0 .../packages/com_mokoog/services/provider.php | 0 .../packages/com_mokoog/sql/index.html | 0 .../packages/com_mokoog/sql/install.mysql.sql | 0 .../com_mokoog/sql/uninstall.mysql.sql | 0 .../com_mokoog/sql/updates/index.html | 0 .../com_mokoog/sql/updates/mysql/01.00.00.sql | 0 .../com_mokoog/sql/updates/mysql/01.01.00.sql | 0 .../com_mokoog/sql/updates/mysql/01.02.00.sql | 0 .../com_mokoog/sql/updates/mysql/index.html | 0 .../src/ContentType/ContentTypeInterface.php | 0 .../src/ContentType/HikaShopAdapter.php | 0 .../com_mokoog/src/ContentType/K2Adapter.php | 0 .../src/ContentType/VirtueMartAdapter.php | 0 .../com_mokoog/src/ContentType/index.html | 0 .../src/Controller/BatchController.php | 0 .../src/Controller/DisplayController.php | 0 .../src/Controller/ImportExportController.php | 0 .../com_mokoog/src/Controller/index.html | 0 .../src/Extension/MokoOGComponent.php | 0 .../com_mokoog/src/Extension/index.html | 0 .../packages/com_mokoog/src/Field/index.html | 0 .../com_mokoog/src/Model/TagModel.php | 0 .../com_mokoog/src/Model/TagsModel.php | 0 .../packages/com_mokoog/src/Model/index.html | 0 .../com_mokoog/src/Service/index.html | 0 .../com_mokoog/src/Table/TagTable.php | 0 .../packages/com_mokoog/src/Table/index.html | 0 .../com_mokoog/src/View/Tags/HtmlView.php | 0 .../com_mokoog/src/View/Tags/index.html | 0 .../com_mokoog/src/View/Tags/tmpl/index.html | 0 .../packages/com_mokoog/src/View/index.html | 0 .../packages/com_mokoog/src/index.html | 0 .../packages/com_mokoog/tmpl/index.html | 0 .../packages/com_mokoog/tmpl/tags/default.php | 0 .../packages/com_mokoog/tmpl/tags/index.html | 0 {src => source}/packages/index.html | 0 .../plg_content_mokoog/forms/index.html | 0 .../plg_content_mokoog/forms/mokoog.xml | 0 .../packages/plg_content_mokoog/index.html | 0 .../language/en-GB/index.html | 0 .../language/en-GB/plg_content_mokoog.ini | 0 .../language/en-GB/plg_content_mokoog.sys.ini | 0 .../language/en-US/index.html | 0 .../language/en-US/plg_content_mokoog.ini | 0 .../language/en-US/plg_content_mokoog.sys.ini | 0 .../plg_content_mokoog/language/index.html | 0 .../plg_content_mokoog/media/css/index.html | 0 .../plg_content_mokoog/media/css/preview.css | 0 .../plg_content_mokoog/media/index.html | 0 .../media/joomla.asset.json | 0 .../plg_content_mokoog/media/js/index.html | 0 .../plg_content_mokoog/media/js/preview.js | 0 .../packages/plg_content_mokoog/mokoog.php | 0 .../packages/plg_content_mokoog/mokoog.xml | 0 .../plg_content_mokoog/services/index.html | 0 .../plg_content_mokoog/services/provider.php | 0 .../src/Extension/MokoOGContent.php | 0 .../src/Extension/index.html | 0 .../plg_content_mokoog/src/Field/index.html | 0 .../plg_content_mokoog/src/index.html | 0 .../packages/plg_system_mokoog/index.html | 0 .../language/en-GB/index.html | 0 .../language/en-GB/plg_system_mokoog.ini | 0 .../language/en-GB/plg_system_mokoog.sys.ini | 0 .../language/en-US/index.html | 0 .../language/en-US/plg_system_mokoog.ini | 0 .../language/en-US/plg_system_mokoog.sys.ini | 0 .../plg_system_mokoog/language/index.html | 0 .../packages/plg_system_mokoog/mokoog.php | 0 .../packages/plg_system_mokoog/mokoog.xml | 0 .../plg_system_mokoog/services/index.html | 0 .../plg_system_mokoog/services/provider.php | 0 .../src/Extension/MokoOG.php | 0 .../src/Extension/index.html | 0 .../src/Helper/ImageGenerator.php | 0 .../src/Helper/ImageHelper.php | 0 .../src/Helper/JsonLdBuilder.php | 0 .../plg_system_mokoog/src/Helper/index.html | 0 .../packages/plg_system_mokoog/src/index.html | 0 .../plg_webservices_mokoog/index.html | 0 .../language/en-GB/index.html | 0 .../language/en-GB/plg_webservices_mokoog.ini | 0 .../en-GB/plg_webservices_mokoog.sys.ini | 0 .../language/en-US/index.html | 0 .../language/en-US/plg_webservices_mokoog.ini | 0 .../en-US/plg_webservices_mokoog.sys.ini | 0 .../language/index.html | 0 .../plg_webservices_mokoog/mokoog.php | 0 .../plg_webservices_mokoog/mokoog.xml | 0 .../services/index.html | 0 .../services/provider.php | 0 .../src/Extension/MokoOGWebServices.php | 0 .../src/Extension/index.html | 0 .../plg_webservices_mokoog/src/index.html | 0 {src => source}/pkg_mokoog.xml | 0 {src => source}/script.php | 0 128 files changed, 1230 insertions(+), 1227 deletions(-) rename {src => source}/index.html (100%) rename {src => source}/language/en-GB/index.html (100%) rename {src => source}/language/en-GB/pkg_mokoog.sys.ini (100%) rename {src => source}/language/en-US/index.html (100%) rename {src => source}/language/en-US/pkg_mokoog.sys.ini (100%) rename {src => source}/language/index.html (100%) rename {src => source}/packages/com_mokoog/api/index.html (100%) rename {src => source}/packages/com_mokoog/api/src/Controller/TagsController.php (100%) rename {src => source}/packages/com_mokoog/api/src/Controller/index.html (100%) rename {src => source}/packages/com_mokoog/api/src/View/Tags/JsonapiView.php (100%) rename {src => source}/packages/com_mokoog/api/src/View/Tags/index.html (100%) rename {src => source}/packages/com_mokoog/api/src/View/index.html (100%) rename {src => source}/packages/com_mokoog/api/src/index.html (100%) rename {src => source}/packages/com_mokoog/forms/filter_tags.xml (100%) rename {src => source}/packages/com_mokoog/forms/index.html (100%) rename {src => source}/packages/com_mokoog/forms/tag.xml (100%) rename {src => source}/packages/com_mokoog/index.html (100%) rename {src => source}/packages/com_mokoog/language/en-GB/com_mokoog.ini (100%) rename {src => source}/packages/com_mokoog/language/en-GB/com_mokoog.sys.ini (100%) rename {src => source}/packages/com_mokoog/language/en-GB/index.html (100%) rename {src => source}/packages/com_mokoog/language/en-US/com_mokoog.ini (100%) rename {src => source}/packages/com_mokoog/language/en-US/com_mokoog.sys.ini (100%) rename {src => source}/packages/com_mokoog/language/en-US/index.html (100%) rename {src => source}/packages/com_mokoog/language/index.html (100%) rename {src => source}/packages/com_mokoog/mokoog.xml (100%) rename {src => source}/packages/com_mokoog/script.php (100%) rename {src => source}/packages/com_mokoog/services/index.html (100%) rename {src => source}/packages/com_mokoog/services/provider.php (100%) rename {src => source}/packages/com_mokoog/sql/index.html (100%) rename {src => source}/packages/com_mokoog/sql/install.mysql.sql (100%) rename {src => source}/packages/com_mokoog/sql/uninstall.mysql.sql (100%) rename {src => source}/packages/com_mokoog/sql/updates/index.html (100%) rename {src => source}/packages/com_mokoog/sql/updates/mysql/01.00.00.sql (100%) rename {src => source}/packages/com_mokoog/sql/updates/mysql/01.01.00.sql (100%) rename {src => source}/packages/com_mokoog/sql/updates/mysql/01.02.00.sql (100%) rename {src => source}/packages/com_mokoog/sql/updates/mysql/index.html (100%) rename {src => source}/packages/com_mokoog/src/ContentType/ContentTypeInterface.php (100%) rename {src => source}/packages/com_mokoog/src/ContentType/HikaShopAdapter.php (100%) rename {src => source}/packages/com_mokoog/src/ContentType/K2Adapter.php (100%) rename {src => source}/packages/com_mokoog/src/ContentType/VirtueMartAdapter.php (100%) rename {src => source}/packages/com_mokoog/src/ContentType/index.html (100%) rename {src => source}/packages/com_mokoog/src/Controller/BatchController.php (100%) rename {src => source}/packages/com_mokoog/src/Controller/DisplayController.php (100%) rename {src => source}/packages/com_mokoog/src/Controller/ImportExportController.php (100%) rename {src => source}/packages/com_mokoog/src/Controller/index.html (100%) rename {src => source}/packages/com_mokoog/src/Extension/MokoOGComponent.php (100%) rename {src => source}/packages/com_mokoog/src/Extension/index.html (100%) rename {src => source}/packages/com_mokoog/src/Field/index.html (100%) rename {src => source}/packages/com_mokoog/src/Model/TagModel.php (100%) rename {src => source}/packages/com_mokoog/src/Model/TagsModel.php (100%) rename {src => source}/packages/com_mokoog/src/Model/index.html (100%) rename {src => source}/packages/com_mokoog/src/Service/index.html (100%) rename {src => source}/packages/com_mokoog/src/Table/TagTable.php (100%) rename {src => source}/packages/com_mokoog/src/Table/index.html (100%) rename {src => source}/packages/com_mokoog/src/View/Tags/HtmlView.php (100%) rename {src => source}/packages/com_mokoog/src/View/Tags/index.html (100%) rename {src => source}/packages/com_mokoog/src/View/Tags/tmpl/index.html (100%) rename {src => source}/packages/com_mokoog/src/View/index.html (100%) rename {src => source}/packages/com_mokoog/src/index.html (100%) rename {src => source}/packages/com_mokoog/tmpl/index.html (100%) rename {src => source}/packages/com_mokoog/tmpl/tags/default.php (100%) rename {src => source}/packages/com_mokoog/tmpl/tags/index.html (100%) rename {src => source}/packages/index.html (100%) rename {src => source}/packages/plg_content_mokoog/forms/index.html (100%) rename {src => source}/packages/plg_content_mokoog/forms/mokoog.xml (100%) rename {src => source}/packages/plg_content_mokoog/index.html (100%) rename {src => source}/packages/plg_content_mokoog/language/en-GB/index.html (100%) rename {src => source}/packages/plg_content_mokoog/language/en-GB/plg_content_mokoog.ini (100%) rename {src => source}/packages/plg_content_mokoog/language/en-GB/plg_content_mokoog.sys.ini (100%) rename {src => source}/packages/plg_content_mokoog/language/en-US/index.html (100%) rename {src => source}/packages/plg_content_mokoog/language/en-US/plg_content_mokoog.ini (100%) rename {src => source}/packages/plg_content_mokoog/language/en-US/plg_content_mokoog.sys.ini (100%) rename {src => source}/packages/plg_content_mokoog/language/index.html (100%) rename {src => source}/packages/plg_content_mokoog/media/css/index.html (100%) rename {src => source}/packages/plg_content_mokoog/media/css/preview.css (100%) rename {src => source}/packages/plg_content_mokoog/media/index.html (100%) rename {src => source}/packages/plg_content_mokoog/media/joomla.asset.json (100%) rename {src => source}/packages/plg_content_mokoog/media/js/index.html (100%) rename {src => source}/packages/plg_content_mokoog/media/js/preview.js (100%) rename {src => source}/packages/plg_content_mokoog/mokoog.php (100%) rename {src => source}/packages/plg_content_mokoog/mokoog.xml (100%) rename {src => source}/packages/plg_content_mokoog/services/index.html (100%) rename {src => source}/packages/plg_content_mokoog/services/provider.php (100%) rename {src => source}/packages/plg_content_mokoog/src/Extension/MokoOGContent.php (100%) rename {src => source}/packages/plg_content_mokoog/src/Extension/index.html (100%) rename {src => source}/packages/plg_content_mokoog/src/Field/index.html (100%) rename {src => source}/packages/plg_content_mokoog/src/index.html (100%) rename {src => source}/packages/plg_system_mokoog/index.html (100%) rename {src => source}/packages/plg_system_mokoog/language/en-GB/index.html (100%) rename {src => source}/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.ini (100%) rename {src => source}/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.sys.ini (100%) rename {src => source}/packages/plg_system_mokoog/language/en-US/index.html (100%) rename {src => source}/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.ini (100%) rename {src => source}/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.sys.ini (100%) rename {src => source}/packages/plg_system_mokoog/language/index.html (100%) rename {src => source}/packages/plg_system_mokoog/mokoog.php (100%) rename {src => source}/packages/plg_system_mokoog/mokoog.xml (100%) rename {src => source}/packages/plg_system_mokoog/services/index.html (100%) rename {src => source}/packages/plg_system_mokoog/services/provider.php (100%) rename {src => source}/packages/plg_system_mokoog/src/Extension/MokoOG.php (100%) rename {src => source}/packages/plg_system_mokoog/src/Extension/index.html (100%) rename {src => source}/packages/plg_system_mokoog/src/Helper/ImageGenerator.php (100%) rename {src => source}/packages/plg_system_mokoog/src/Helper/ImageHelper.php (100%) rename {src => source}/packages/plg_system_mokoog/src/Helper/JsonLdBuilder.php (100%) rename {src => source}/packages/plg_system_mokoog/src/Helper/index.html (100%) rename {src => source}/packages/plg_system_mokoog/src/index.html (100%) rename {src => source}/packages/plg_webservices_mokoog/index.html (100%) rename {src => source}/packages/plg_webservices_mokoog/language/en-GB/index.html (100%) rename {src => source}/packages/plg_webservices_mokoog/language/en-GB/plg_webservices_mokoog.ini (100%) rename {src => source}/packages/plg_webservices_mokoog/language/en-GB/plg_webservices_mokoog.sys.ini (100%) rename {src => source}/packages/plg_webservices_mokoog/language/en-US/index.html (100%) rename {src => source}/packages/plg_webservices_mokoog/language/en-US/plg_webservices_mokoog.ini (100%) rename {src => source}/packages/plg_webservices_mokoog/language/en-US/plg_webservices_mokoog.sys.ini (100%) rename {src => source}/packages/plg_webservices_mokoog/language/index.html (100%) rename {src => source}/packages/plg_webservices_mokoog/mokoog.php (100%) rename {src => source}/packages/plg_webservices_mokoog/mokoog.xml (100%) rename {src => source}/packages/plg_webservices_mokoog/services/index.html (100%) rename {src => source}/packages/plg_webservices_mokoog/services/provider.php (100%) rename {src => source}/packages/plg_webservices_mokoog/src/Extension/MokoOGWebServices.php (100%) rename {src => source}/packages/plg_webservices_mokoog/src/Extension/index.html (100%) rename {src => source}/packages/plg_webservices_mokoog/src/index.html (100%) rename {src => source}/pkg_mokoog.xml (100%) rename {src => source}/script.php (100%) diff --git a/.gitignore b/.gitignore index 726a684..26927d2 100644 --- a/.gitignore +++ b/.gitignore @@ -152,7 +152,7 @@ package-lock.json # PHP / Composer tooling # ============================================================ vendor/ -!src/media/vendor/ +!source/media/vendor/ composer.lock *.phar codeception.phar diff --git a/.mokogitea/manifest.xml b/.mokogitea/manifest.xml index 97027dd..43d13ca 100644 --- a/.mokogitea/manifest.xml +++ b/.mokogitea/manifest.xml @@ -21,6 +21,6 @@ <build> <language>PHP</language> <package-type>joomla-extension</package-type> - <entry-point>src/</entry-point> + <entry-point>source/</entry-point> </build> </moko-platform> diff --git a/.mokogitea/workflows/ci-joomla.yml b/.mokogitea/workflows/ci-joomla.yml index f679e86..e67987b 100644 --- a/.mokogitea/workflows/ci-joomla.yml +++ b/.mokogitea/workflows/ci-joomla.yml @@ -67,7 +67,7 @@ jobs: - name: PHP syntax check run: | ERRORS=0 - for DIR in src/ htdocs/; do + for DIR in source/ src/ htdocs/; do if [ -d "$DIR" ]; then FOUND=1 while IFS= read -r -d '' FILE; do @@ -207,7 +207,7 @@ jobs: echo "### Language Directory Check" >> $GITHUB_STEP_SUMMARY ERRORS=0 - for DIR in src/ htdocs/; do + for DIR in source/ src/ htdocs/; do [ -d "$DIR" ] || continue # Find all language directories while IFS= read -r -d '' LANG_DIR; do @@ -239,7 +239,7 @@ jobs: MISSING=0 CHECKED=0 - for DIR in src/ htdocs/; do + for DIR in source/ src/ htdocs/; do if [ -d "$DIR" ]; then while IFS= read -r -d '' SUBDIR; do CHECKED=$((CHECKED + 1)) @@ -252,7 +252,7 @@ jobs: done if [ "${CHECKED}" -eq 0 ]; then - echo "No src/ or htdocs/ directories found — skipping." >> $GITHUB_STEP_SUMMARY + echo "No source/, 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 @@ -450,7 +450,7 @@ jobs: # Determine source directory SRC_DIR="" - for DIR in src/ htdocs/ lib/; do + for DIR in source/ src/ htdocs/ lib/; do if [ -d "$DIR" ]; then SRC_DIR="$DIR" break @@ -458,7 +458,7 @@ jobs: done if [ -z "$SRC_DIR" ]; then - echo "No source directory found (src/, htdocs/, lib/) — skipping." >> $GITHUB_STEP_SUMMARY + echo "No source directory found (source/, src/, htdocs/, lib/) — skipping." >> $GITHUB_STEP_SUMMARY exit 0 fi diff --git a/.mokogitea/workflows/pr-check.yml b/.mokogitea/workflows/pr-check.yml index 4d78d7a..7134fb7 100644 --- a/.mokogitea/workflows/pr-check.yml +++ b/.mokogitea/workflows/pr-check.yml @@ -1,508 +1,509 @@ -# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: moko-platform.CI -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform -# PATH: /templates/workflows/universal/pr-check.yml.template -# VERSION: 09.23.00 -# BRIEF: PR gate — branch policy + code validation before merge - -name: "Universal: PR Check" - -on: - pull_request: - types: [opened, synchronize, reopened, edited] - -permissions: - contents: read - pull-requests: write - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - # ── Branch Policy ────────────────────────────────────────────────────── - branch-policy: - name: Branch Policy - runs-on: ubuntu-latest - steps: - - name: Check branch merge target - run: | - HEAD="${{ github.head_ref }}" - BASE="${{ github.base_ref }}" - - echo "PR: ${HEAD} → ${BASE}" - - ALLOWED=true - REASON="" - - case "$HEAD" in - feature/*|feat/*) - if [ "$BASE" != "dev" ]; then - ALLOWED=false - REASON="Feature branches must target 'dev', not '${BASE}'" - fi - ;; - fix/*|bugfix/*) - if [ "$BASE" != "dev" ]; then - ALLOWED=false - REASON="Fix branches must target 'dev', not '${BASE}'" - fi - ;; - patch/*) - if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then - ALLOWED=false - REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'" - fi - ;; - hotfix/*) - if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then - ALLOWED=false - REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'" - fi - ;; - rc) - if [ "$BASE" != "main" ]; then - ALLOWED=false - REASON="RC branch can only merge into 'main', not '${BASE}'" - fi - ;; - dev) - if [ "$BASE" != "main" ]; then - ALLOWED=false - REASON="Dev branch can only merge into 'main', not '${BASE}'" - fi - ;; - esac - - if [ "$ALLOWED" = false ]; then - echo "::error::${REASON}" - echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "${REASON}" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY - echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY - echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY - echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY - echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY - echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY - exit 1 - fi - - echo "Branch policy: OK (${HEAD} → ${BASE})" - echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY - - # ── Code Validation ──────────────────────────────────────────────────── - validate: - name: Validate PR - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Check for merge conflict markers - run: | - CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true) - if [ -n "$CONFLICTS" ]; then - echo "::error::Merge conflict markers found in source files" - echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - exit 1 - fi - echo "No conflict markers found" - - - name: Detect platform - id: platform - run: | - # Read platform from XML manifest (<platform> tag) or plain text fallback - PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1) - [ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]') - [ -z "$PLATFORM" ] && PLATFORM="generic" - echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT" - - - name: Setup PHP - if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr' - run: | - if ! command -v php &> /dev/null; then - sudo apt-get update -qq - sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1 - fi - - - name: PHP syntax check - if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr' - run: | - ERRORS=0 - while IFS= read -r -d '' file; do - if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then - ERRORS=$((ERRORS + 1)) - fi - done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0) - echo "PHP lint: ${ERRORS} error(s)" - [ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; } - - - name: Joomla JEXEC guard check - if: steps.platform.outputs.platform == 'joomla' - run: | - ERRORS=0 - while IFS= read -r -d '' file; do - # Skip vendor, node_modules, and index.html stub files - case "$file" in ./vendor/*|./node_modules/*) continue ;; esac - # Check first 10 lines for JEXEC or JPATH guard - if ! head -20 "$file" | grep -qE "defined\s*\(\s*['\"](_JEXEC|JPATH_BASE|\\\\JPATH_PLATFORM)['\"]"; then - echo "::error file=${file}::Missing JEXEC guard: ${file}" - ERRORS=$((ERRORS + 1)) - fi - 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 src/ are missing the Joomla execution guard." >> $GITHUB_STEP_SUMMARY - exit 1 - fi - echo "JEXEC guard: OK" - - - name: Joomla directory listing protection - if: steps.platform.outputs.platform == 'joomla' - run: | - MISSING=0 - SOURCE_DIR="src" - [ ! -d "$SOURCE_DIR" ] && exit 0 - while IFS= read -r dir; do - if [ ! -f "${dir}/index.html" ]; then - echo "::warning::Missing index.html in ${dir} (directory listing protection)" - MISSING=$((MISSING + 1)) - fi - done < <(find "$SOURCE_DIR" -type d -not -path "./.git/*" -not -path "*/vendor/*" -not -path "*/node_modules/*") - if [ "$MISSING" -gt 0 ]; then - echo "## Directory Protection" >> $GITHUB_STEP_SUMMARY - echo "${MISSING} director(ies) missing index.html" >> $GITHUB_STEP_SUMMARY - fi - echo "Directory protection: ${MISSING} missing (advisory)" - - - name: Joomla script file and asset checks - if: steps.platform.outputs.platform == 'joomla' - run: | - ERRORS=0 - MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1) - [ -z "$MANIFEST" ] && exit 0 - MANIFEST_DIR=$(dirname "$MANIFEST") - - # Check scriptfile exists if declared - SCRIPTFILE=$(sed -n 's/.*<scriptfile>\([^<]*\)<\/scriptfile>.*/\1/p' "$MANIFEST" 2>/dev/null) - if [ -n "$SCRIPTFILE" ]; then - if [ ! -f "${MANIFEST_DIR}/${SCRIPTFILE}" ]; then - echo "::error::Manifest declares <scriptfile>${SCRIPTFILE}</scriptfile> but file not found at ${MANIFEST_DIR}/${SCRIPTFILE}" - ERRORS=$((ERRORS + 1)) - else - echo "Script file: ${MANIFEST_DIR}/${SCRIPTFILE} (OK)" - fi - fi - - # Require joomla.asset.json and validate it - ASSET_JSON=$(find "$MANIFEST_DIR" -name "joomla.asset.json" -not -path "./.git/*" 2>/dev/null | head -1) - if [ -z "$ASSET_JSON" ]; then - echo "::error::joomla.asset.json not found — Joomla asset system is required" - ERRORS=$((ERRORS + 1)) - else - if command -v php &> /dev/null; then - php -r "json_decode(file_get_contents('$ASSET_JSON')); if(json_last_error()!==JSON_ERROR_NONE){echo json_last_error_msg();exit(1);}" 2>&1 || { - echo "::error::joomla.asset.json is not valid JSON" - ERRORS=$((ERRORS + 1)) - } - fi - echo "joomla.asset.json: valid" - fi - - # 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 - if ! php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$xmlfile'); if(!\$x){foreach(libxml_get_errors() as \$e) echo trim(\$e->message) . ' in $xmlfile'; exit(1);}" 2>&1; then - XML_ERRORS=$((XML_ERRORS + 1)) - fi - done < <(find "$MANIFEST_DIR" -name "*.xml" -not -path "./.git/*" -print0) - fi - if [ "$XML_ERRORS" -gt 0 ]; then - echo "::error::${XML_ERRORS} XML file(s) are malformed" - ERRORS=$((ERRORS + 1)) - else - echo "XML well-formedness: OK" - fi - - [ "$ERRORS" -gt 0 ] && exit 1 - echo "Joomla asset checks: OK" - - - name: Validate platform manifest - run: | - PLATFORM="${{ steps.platform.outputs.platform }}" - case "$PLATFORM" in - joomla) - MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1) - if [ -z "$MANIFEST" ]; then - echo "::warning::No Joomla manifest found (WaaS site)" - exit 0 - fi - echo "Manifest: ${MANIFEST}" - if command -v php &> /dev/null; then - php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::Manifest XML is malformed"; exit 1; } - fi - for ELEMENT in name version description; do - grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; } - done - # Block legacy raw/branch update server URLs on MokoGitea - RAW_URLS=$(grep -n 'raw/branch' "$MANIFEST" | grep -i 'mokoconsulting\|mokogitea\|git\.mokoconsulting\.tech' || true) - if [ -n "$RAW_URLS" ]; then - echo "::error::Manifest contains legacy raw/branch update server URL on MokoGitea. Use the Gitea Pages URL instead (e.g. /{REPO}/updates.xml not /{REPO}/raw/branch/main/updates.xml)" - echo "$RAW_URLS" - exit 1 - fi - echo "Joomla manifest valid" - ;; - dolibarr) - MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1) - if [ -z "$MOD_FILE" ]; then - echo "::error::No mod*.class.php found" - exit 1 - fi - echo "Dolibarr module: ${MOD_FILE}" - ;; - *) - echo "Generic platform — no manifest validation" - ;; - esac - - - name: Check update stream format - run: | - PLATFORM="${{ steps.platform.outputs.platform }}" - case "$PLATFORM" in - joomla) - if [ -f "updates.xml" ]; then - if command -v php &> /dev/null; then - php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::updates.xml is malformed"; exit 1; } - fi - echo "updates.xml valid" - fi - ;; - dolibarr) - [ -f "update.txt" ] && echo "update.txt present" || echo "::warning::No update.txt" - ;; - esac - - - name: Validate Joomla language files - if: steps.platform.outputs.platform == 'joomla' - run: | - ERRORS=0 - WARNINGS=0 - - # Require both en-GB and en-US language directories - LANG_ROOT=$(find . -path "*/language" -type d -not -path "./.git/*" 2>/dev/null | head -1) - if [ -z "$LANG_ROOT" ]; then - echo "No language/ directory found — skipping" - exit 0 - fi - - if [ ! -d "$LANG_ROOT/en-GB" ]; then - echo "::error::Missing en-GB language directory (${LANG_ROOT}/en-GB)" - ERRORS=$((ERRORS + 1)) - fi - if [ ! -d "$LANG_ROOT/en-US" ]; then - echo "::error::Missing en-US language directory (${LANG_ROOT}/en-US)" - ERRORS=$((ERRORS + 1)) - fi - - # Check that en-GB and en-US have matching .ini files - if [ -d "$LANG_ROOT/en-GB" ] && [ -d "$LANG_ROOT/en-US" ]; then - for GB_INI in "$LANG_ROOT/en-GB"/*.ini; do - [ ! -f "$GB_INI" ] && continue - US_INI="$LANG_ROOT/en-US/$(basename "$GB_INI")" - if [ ! -f "$US_INI" ]; then - echo "::error::$(basename "$GB_INI") exists in en-GB but missing from en-US" - ERRORS=$((ERRORS + 1)) - fi - done - for US_INI in "$LANG_ROOT/en-US"/*.ini; do - [ ! -f "$US_INI" ] && continue - GB_INI="$LANG_ROOT/en-GB/$(basename "$US_INI")" - if [ ! -f "$GB_INI" ]; then - echo "::error::$(basename "$US_INI") exists in en-US but missing from en-GB" - ERRORS=$((ERRORS + 1)) - fi - done - fi - - # Find all .ini language files - INI_FILES=$(find . -path "*/language/*/*.ini" -not -path "./.git/*" 2>/dev/null) - if [ -z "$INI_FILES" ]; then - echo "No .ini language files found" - [ "$ERRORS" -gt 0 ] && exit 1 - exit 0 - fi - - echo "Found $(echo "$INI_FILES" | wc -l) language file(s)" - - for FILE in $INI_FILES; do - FNAME=$(basename "$FILE") - LINENUM=0 - SEEN_KEYS="" - - while IFS= read -r line || [ -n "$line" ]; do - LINENUM=$((LINENUM + 1)) - - # Skip empty lines and comments - [ -z "$line" ] && continue - echo "$line" | grep -qE '^\s*;' && continue - echo "$line" | grep -qE '^\s*$' && continue - - # Must match KEY="VALUE" format - if ! echo "$line" | grep -qE '^[A-Z_][A-Z0-9_]*=".*"$'; then - echo "::error file=${FILE},line=${LINENUM}::Malformed line: ${line}" - ERRORS=$((ERRORS + 1)) - continue - fi - - # Extract key and check for duplicates - KEY=$(echo "$line" | sed 's/=.*//') - if echo "$SEEN_KEYS" | grep -qx "$KEY"; then - echo "::error file=${FILE},line=${LINENUM}::Duplicate key: ${KEY}" - ERRORS=$((ERRORS + 1)) - fi - SEEN_KEYS="${SEEN_KEYS} - ${KEY}" - done < "$FILE" - - echo " ${FILE}: checked ${LINENUM} lines" - done - - # Cross-check en-GB vs en-US key consistency - GB_DIR=$(find . -path "*/language/en-GB" -type d -not -path "./.git/*" 2>/dev/null | head -1) - US_DIR=$(find . -path "*/language/en-US" -type d -not -path "./.git/*" 2>/dev/null | head -1) - - if [ -n "$GB_DIR" ] && [ -n "$US_DIR" ]; then - for GB_FILE in "$GB_DIR"/*.ini; do - [ ! -f "$GB_FILE" ] && continue - FNAME=$(basename "$GB_FILE") - US_FILE="$US_DIR/$FNAME" - [ ! -f "$US_FILE" ] && continue - - GB_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$GB_FILE" 2>/dev/null | sort) - US_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$US_FILE" 2>/dev/null | sort) - - # Keys in en-GB but not en-US - MISSING_US=$(comm -23 <(echo "$GB_KEYS") <(echo "$US_KEYS")) - if [ -n "$MISSING_US" ]; then - echo "::warning::Keys in en-GB/$FNAME but missing from en-US/$FNAME:" - echo "$MISSING_US" | while read -r k; do echo " - $k"; done - WARNINGS=$((WARNINGS + 1)) - fi - - # Keys in en-US but not en-GB - MISSING_GB=$(comm -13 <(echo "$GB_KEYS") <(echo "$US_KEYS")) - if [ -n "$MISSING_GB" ]; then - echo "::warning::Keys in en-US/$FNAME but missing from en-GB/$FNAME:" - echo "$MISSING_GB" | while read -r k; do echo " - $k"; done - WARNINGS=$((WARNINGS + 1)) - fi - done - fi - - { - echo "### Language File Validation" - echo "| Metric | Count |" - echo "|---|---|" - echo "| Files checked | $(echo "$INI_FILES" | wc -l) |" - echo "| Errors | ${ERRORS} |" - echo "| Warnings | ${WARNINGS} |" - } >> $GITHUB_STEP_SUMMARY - - if [ "$ERRORS" -gt 0 ]; then - echo "::error::Language validation failed with ${ERRORS} error(s)" - exit 1 - fi - echo "Language files: OK (${WARNINGS} warning(s))" - - - name: Check changelog has unreleased entry - run: | - if [ ! -f "CHANGELOG.md" ]; then - echo "::warning::No CHANGELOG.md found" - exit 0 - fi - # Check for content under [Unreleased] section - if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then - echo "::error::CHANGELOG.md missing [Unreleased] section" - exit 1 - fi - # Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased - UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true) - if [ "$UNRELEASED_CONTENT" -eq 0 ]; then - echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes." - echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY - echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY - echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY - exit 1 - fi - echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]" - - - name: Verify package source - run: | - SOURCE_DIR="src" - [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" - if [ ! -d "$SOURCE_DIR" ]; then - echo "::warning::No src/ or htdocs/ directory" - exit 0 - fi - FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l) - echo "Source: ${FILE_COUNT} files" - [ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; } - - # ── Pre-Release RC Build ───────────────────────────────────────────────── - pre-release: - name: Build RC Package - runs-on: ubuntu-latest - needs: [branch-policy, validate] - - steps: - - name: Trigger RC pre-release - env: - GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - REPO: ${{ github.repository }} - BRANCH: ${{ github.head_ref }} - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - run: | - curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}" - echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY - echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY - - # ── Issue Reporter ────────────────────────────────────────────────────── - report-issues: - name: Report Issues - runs-on: ubuntu-latest - needs: [branch-policy, validate] - if: >- - always() && - needs.validate.result == 'failure' - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - sparse-checkout: automation/ci-issue-reporter.sh - sparse-checkout-cone-mode: false - - - name: "File issue for PR validation failure" - env: - GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - run: | - chmod +x automation/ci-issue-reporter.sh - ./automation/ci-issue-reporter.sh \ - --gate "PR Validation" \ - --workflow "PR Check" \ - --severity error \ - --details "PR validation failed (syntax, manifest, changelog, or source checks). See the CI run for the specific check that failed." +# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: moko-platform.CI +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform +# PATH: /templates/workflows/universal/pr-check.yml.template +# VERSION: 09.23.00 +# BRIEF: PR gate — branch policy + code validation before merge + +name: "Universal: PR Check" + +on: + pull_request: + types: [opened, synchronize, reopened, edited] + +permissions: + contents: read + pull-requests: write + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + # ── Branch Policy ────────────────────────────────────────────────────── + branch-policy: + name: Branch Policy + runs-on: ubuntu-latest + steps: + - name: Check branch merge target + run: | + HEAD="${{ github.head_ref }}" + BASE="${{ github.base_ref }}" + + echo "PR: ${HEAD} → ${BASE}" + + ALLOWED=true + REASON="" + + case "$HEAD" in + feature/*|feat/*) + if [ "$BASE" != "dev" ]; then + ALLOWED=false + REASON="Feature branches must target 'dev', not '${BASE}'" + fi + ;; + fix/*|bugfix/*) + if [ "$BASE" != "dev" ]; then + ALLOWED=false + REASON="Fix branches must target 'dev', not '${BASE}'" + fi + ;; + patch/*) + if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then + ALLOWED=false + REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'" + fi + ;; + hotfix/*) + if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then + ALLOWED=false + REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'" + fi + ;; + rc) + if [ "$BASE" != "main" ]; then + ALLOWED=false + REASON="RC branch can only merge into 'main', not '${BASE}'" + fi + ;; + dev) + if [ "$BASE" != "main" ]; then + ALLOWED=false + REASON="Dev branch can only merge into 'main', not '${BASE}'" + fi + ;; + esac + + if [ "$ALLOWED" = false ]; then + echo "::error::${REASON}" + echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "${REASON}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY + echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY + echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY + echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY + echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY + echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + echo "Branch policy: OK (${HEAD} → ${BASE})" + echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY + + # ── Code Validation ──────────────────────────────────────────────────── + validate: + name: Validate PR + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Check for merge conflict markers + run: | + CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true) + if [ -n "$CONFLICTS" ]; then + echo "::error::Merge conflict markers found in source files" + echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + exit 1 + fi + echo "No conflict markers found" + + - name: Detect platform + id: platform + run: | + # Read platform from XML manifest (<platform> tag) or plain text fallback + PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1) + [ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]') + [ -z "$PLATFORM" ] && PLATFORM="generic" + echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT" + + - name: Setup PHP + if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr' + run: | + if ! command -v php &> /dev/null; then + sudo apt-get update -qq + sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1 + fi + + - name: PHP syntax check + if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr' + run: | + ERRORS=0 + while IFS= read -r -d '' file; do + if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then + ERRORS=$((ERRORS + 1)) + fi + done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0) + echo "PHP lint: ${ERRORS} error(s)" + [ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; } + + - name: Joomla JEXEC guard check + if: steps.platform.outputs.platform == 'joomla' + run: | + ERRORS=0 + while IFS= read -r -d '' file; do + # Skip vendor, node_modules, and index.html stub files + case "$file" in ./vendor/*|./node_modules/*) continue ;; esac + # Check first 10 lines for JEXEC or JPATH guard + if ! head -20 "$file" | grep -qE "defined\s*\(\s*['\"](_JEXEC|JPATH_BASE|\\\\JPATH_PLATFORM)['\"]"; then + 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) + 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 + exit 1 + fi + echo "JEXEC guard: OK" + + - name: Joomla directory listing protection + if: steps.platform.outputs.platform == 'joomla' + run: | + MISSING=0 + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && exit 0 + while IFS= read -r dir; do + if [ ! -f "${dir}/index.html" ]; then + echo "::warning::Missing index.html in ${dir} (directory listing protection)" + MISSING=$((MISSING + 1)) + fi + done < <(find "$SOURCE_DIR" -type d -not -path "./.git/*" -not -path "*/vendor/*" -not -path "*/node_modules/*") + if [ "$MISSING" -gt 0 ]; then + echo "## Directory Protection" >> $GITHUB_STEP_SUMMARY + echo "${MISSING} director(ies) missing index.html" >> $GITHUB_STEP_SUMMARY + fi + echo "Directory protection: ${MISSING} missing (advisory)" + + - name: Joomla script file and asset checks + if: steps.platform.outputs.platform == 'joomla' + run: | + ERRORS=0 + MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1) + [ -z "$MANIFEST" ] && exit 0 + MANIFEST_DIR=$(dirname "$MANIFEST") + + # Check scriptfile exists if declared + SCRIPTFILE=$(sed -n 's/.*<scriptfile>\([^<]*\)<\/scriptfile>.*/\1/p' "$MANIFEST" 2>/dev/null) + if [ -n "$SCRIPTFILE" ]; then + if [ ! -f "${MANIFEST_DIR}/${SCRIPTFILE}" ]; then + echo "::error::Manifest declares <scriptfile>${SCRIPTFILE}</scriptfile> but file not found at ${MANIFEST_DIR}/${SCRIPTFILE}" + ERRORS=$((ERRORS + 1)) + else + echo "Script file: ${MANIFEST_DIR}/${SCRIPTFILE} (OK)" + fi + fi + + # Require joomla.asset.json and validate it + ASSET_JSON=$(find "$MANIFEST_DIR" -name "joomla.asset.json" -not -path "./.git/*" 2>/dev/null | head -1) + if [ -z "$ASSET_JSON" ]; then + echo "::error::joomla.asset.json not found — Joomla asset system is required" + ERRORS=$((ERRORS + 1)) + else + if command -v php &> /dev/null; then + php -r "json_decode(file_get_contents('$ASSET_JSON')); if(json_last_error()!==JSON_ERROR_NONE){echo json_last_error_msg();exit(1);}" 2>&1 || { + echo "::error::joomla.asset.json is not valid JSON" + ERRORS=$((ERRORS + 1)) + } + fi + echo "joomla.asset.json: valid" + fi + + # Validate all XML files in source/ are well-formed + XML_ERRORS=0 + if command -v php &> /dev/null; then + while IFS= read -r -d '' xmlfile; do + if ! php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$xmlfile'); if(!\$x){foreach(libxml_get_errors() as \$e) echo trim(\$e->message) . ' in $xmlfile'; exit(1);}" 2>&1; then + XML_ERRORS=$((XML_ERRORS + 1)) + fi + done < <(find "$MANIFEST_DIR" -name "*.xml" -not -path "./.git/*" -print0) + fi + if [ "$XML_ERRORS" -gt 0 ]; then + echo "::error::${XML_ERRORS} XML file(s) are malformed" + ERRORS=$((ERRORS + 1)) + else + echo "XML well-formedness: OK" + fi + + [ "$ERRORS" -gt 0 ] && exit 1 + echo "Joomla asset checks: OK" + + - name: Validate platform manifest + run: | + PLATFORM="${{ steps.platform.outputs.platform }}" + case "$PLATFORM" in + joomla) + MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1) + if [ -z "$MANIFEST" ]; then + echo "::warning::No Joomla manifest found (WaaS site)" + exit 0 + fi + echo "Manifest: ${MANIFEST}" + if command -v php &> /dev/null; then + php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::Manifest XML is malformed"; exit 1; } + fi + for ELEMENT in name version description; do + grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; } + done + # Block legacy raw/branch update server URLs on MokoGitea + RAW_URLS=$(grep -n 'raw/branch' "$MANIFEST" | grep -i 'mokoconsulting\|mokogitea\|git\.mokoconsulting\.tech' || true) + if [ -n "$RAW_URLS" ]; then + echo "::error::Manifest contains legacy raw/branch update server URL on MokoGitea. Use the Gitea Pages URL instead (e.g. /{REPO}/updates.xml not /{REPO}/raw/branch/main/updates.xml)" + echo "$RAW_URLS" + exit 1 + fi + echo "Joomla manifest valid" + ;; + dolibarr) + MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1) + if [ -z "$MOD_FILE" ]; then + echo "::error::No mod*.class.php found" + exit 1 + fi + echo "Dolibarr module: ${MOD_FILE}" + ;; + *) + echo "Generic platform — no manifest validation" + ;; + esac + + - name: Check update stream format + run: | + PLATFORM="${{ steps.platform.outputs.platform }}" + case "$PLATFORM" in + joomla) + if [ -f "updates.xml" ]; then + if command -v php &> /dev/null; then + php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::updates.xml is malformed"; exit 1; } + fi + echo "updates.xml valid" + fi + ;; + dolibarr) + [ -f "update.txt" ] && echo "update.txt present" || echo "::warning::No update.txt" + ;; + esac + + - name: Validate Joomla language files + if: steps.platform.outputs.platform == 'joomla' + run: | + ERRORS=0 + WARNINGS=0 + + # Require both en-GB and en-US language directories + LANG_ROOT=$(find . -path "*/language" -type d -not -path "./.git/*" 2>/dev/null | head -1) + if [ -z "$LANG_ROOT" ]; then + echo "No language/ directory found — skipping" + exit 0 + fi + + if [ ! -d "$LANG_ROOT/en-GB" ]; then + echo "::error::Missing en-GB language directory (${LANG_ROOT}/en-GB)" + ERRORS=$((ERRORS + 1)) + fi + if [ ! -d "$LANG_ROOT/en-US" ]; then + echo "::error::Missing en-US language directory (${LANG_ROOT}/en-US)" + ERRORS=$((ERRORS + 1)) + fi + + # Check that en-GB and en-US have matching .ini files + if [ -d "$LANG_ROOT/en-GB" ] && [ -d "$LANG_ROOT/en-US" ]; then + for GB_INI in "$LANG_ROOT/en-GB"/*.ini; do + [ ! -f "$GB_INI" ] && continue + US_INI="$LANG_ROOT/en-US/$(basename "$GB_INI")" + if [ ! -f "$US_INI" ]; then + echo "::error::$(basename "$GB_INI") exists in en-GB but missing from en-US" + ERRORS=$((ERRORS + 1)) + fi + done + for US_INI in "$LANG_ROOT/en-US"/*.ini; do + [ ! -f "$US_INI" ] && continue + GB_INI="$LANG_ROOT/en-GB/$(basename "$US_INI")" + if [ ! -f "$GB_INI" ]; then + echo "::error::$(basename "$US_INI") exists in en-US but missing from en-GB" + ERRORS=$((ERRORS + 1)) + fi + done + fi + + # Find all .ini language files + INI_FILES=$(find . -path "*/language/*/*.ini" -not -path "./.git/*" 2>/dev/null) + if [ -z "$INI_FILES" ]; then + echo "No .ini language files found" + [ "$ERRORS" -gt 0 ] && exit 1 + exit 0 + fi + + echo "Found $(echo "$INI_FILES" | wc -l) language file(s)" + + for FILE in $INI_FILES; do + FNAME=$(basename "$FILE") + LINENUM=0 + SEEN_KEYS="" + + while IFS= read -r line || [ -n "$line" ]; do + LINENUM=$((LINENUM + 1)) + + # Skip empty lines and comments + [ -z "$line" ] && continue + echo "$line" | grep -qE '^\s*;' && continue + echo "$line" | grep -qE '^\s*$' && continue + + # Must match KEY="VALUE" format + if ! echo "$line" | grep -qE '^[A-Z_][A-Z0-9_]*=".*"$'; then + echo "::error file=${FILE},line=${LINENUM}::Malformed line: ${line}" + ERRORS=$((ERRORS + 1)) + continue + fi + + # Extract key and check for duplicates + KEY=$(echo "$line" | sed 's/=.*//') + if echo "$SEEN_KEYS" | grep -qx "$KEY"; then + echo "::error file=${FILE},line=${LINENUM}::Duplicate key: ${KEY}" + ERRORS=$((ERRORS + 1)) + fi + SEEN_KEYS="${SEEN_KEYS} + ${KEY}" + done < "$FILE" + + echo " ${FILE}: checked ${LINENUM} lines" + done + + # Cross-check en-GB vs en-US key consistency + GB_DIR=$(find . -path "*/language/en-GB" -type d -not -path "./.git/*" 2>/dev/null | head -1) + US_DIR=$(find . -path "*/language/en-US" -type d -not -path "./.git/*" 2>/dev/null | head -1) + + if [ -n "$GB_DIR" ] && [ -n "$US_DIR" ]; then + for GB_FILE in "$GB_DIR"/*.ini; do + [ ! -f "$GB_FILE" ] && continue + FNAME=$(basename "$GB_FILE") + US_FILE="$US_DIR/$FNAME" + [ ! -f "$US_FILE" ] && continue + + GB_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$GB_FILE" 2>/dev/null | sort) + US_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$US_FILE" 2>/dev/null | sort) + + # Keys in en-GB but not en-US + MISSING_US=$(comm -23 <(echo "$GB_KEYS") <(echo "$US_KEYS")) + if [ -n "$MISSING_US" ]; then + echo "::warning::Keys in en-GB/$FNAME but missing from en-US/$FNAME:" + echo "$MISSING_US" | while read -r k; do echo " - $k"; done + WARNINGS=$((WARNINGS + 1)) + fi + + # Keys in en-US but not en-GB + MISSING_GB=$(comm -13 <(echo "$GB_KEYS") <(echo "$US_KEYS")) + if [ -n "$MISSING_GB" ]; then + echo "::warning::Keys in en-US/$FNAME but missing from en-GB/$FNAME:" + echo "$MISSING_GB" | while read -r k; do echo " - $k"; done + WARNINGS=$((WARNINGS + 1)) + fi + done + fi + + { + echo "### Language File Validation" + echo "| Metric | Count |" + echo "|---|---|" + echo "| Files checked | $(echo "$INI_FILES" | wc -l) |" + echo "| Errors | ${ERRORS} |" + echo "| Warnings | ${WARNINGS} |" + } >> $GITHUB_STEP_SUMMARY + + if [ "$ERRORS" -gt 0 ]; then + echo "::error::Language validation failed with ${ERRORS} error(s)" + exit 1 + fi + echo "Language files: OK (${WARNINGS} warning(s))" + + - name: Check changelog has unreleased entry + run: | + if [ ! -f "CHANGELOG.md" ]; then + echo "::warning::No CHANGELOG.md found" + exit 0 + fi + # Check for content under [Unreleased] section + if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then + echo "::error::CHANGELOG.md missing [Unreleased] section" + exit 1 + fi + # Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased + UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true) + if [ "$UNRELEASED_CONTENT" -eq 0 ]; then + echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes." + echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY + echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY + echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]" + + - name: Verify package source + run: | + SOURCE_DIR="source" + [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" + if [ ! -d "$SOURCE_DIR" ]; then + echo "::warning::No source/, src/, or htdocs/ directory" + exit 0 + fi + FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l) + echo "Source: ${FILE_COUNT} files" + [ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; } + + # ── Pre-Release RC Build ───────────────────────────────────────────────── + pre-release: + name: Build RC Package + runs-on: ubuntu-latest + needs: [branch-policy, validate] + + steps: + - name: Trigger RC pre-release + env: + GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + REPO: ${{ github.repository }} + BRANCH: ${{ github.head_ref }} + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + run: | + curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}" + echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY + echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY + + # ── Issue Reporter ────────────────────────────────────────────────────── + report-issues: + name: Report Issues + runs-on: ubuntu-latest + needs: [branch-policy, validate] + if: >- + always() && + needs.validate.result == 'failure' + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + sparse-checkout: automation/ci-issue-reporter.sh + sparse-checkout-cone-mode: false + + - name: "File issue for PR validation failure" + env: + GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + run: | + chmod +x automation/ci-issue-reporter.sh + ./automation/ci-issue-reporter.sh \ + --gate "PR Validation" \ + --workflow "PR Check" \ + --severity error \ + --details "PR validation failed (syntax, manifest, changelog, or source checks). See the CI run for the specific check that failed." diff --git a/.mokogitea/workflows/repo-health.yml b/.mokogitea/workflows/repo-health.yml index 8d57aaf..0c5d5bf 100644 --- a/.mokogitea/workflows/repo-health.yml +++ b/.mokogitea/workflows/repo-health.yml @@ -1,711 +1,713 @@ -# ============================================================================ -# Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech> -# -# This file is part of a Moko Consulting project. -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: moko-platform.Validation -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform -# PATH: /templates/workflows/joomla/repo_health.yml.template -# VERSION: 09.23.00 -# BRIEF: Enforces repository guardrails by validating scripts governance, tooling availability, and core repository health artifacts. -# ============================================================================ - -name: "Generic: Repo Health" - -defaults: - run: - shell: bash - -on: - workflow_dispatch: - inputs: - profile: - description: 'Validation profile: all, scripts, or repo' - required: true - default: all - type: choice - options: - - all - - scripts - - repo - pull_request: - push: - -permissions: - contents: read - -env: - # Scripts governance policy - SCRIPTS_REQUIRED_DIRS: - SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate - - # Repo health policy - REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.mokogitea/workflows/ - REPO_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore,README.md,docs/ - REPO_DISALLOWED_DIRS: - REPO_DISALLOWED_FILES: TODO.md,todo.md - - # Extended checks toggles - EXTENDED_CHECKS: "true" - - # File / directory variables - DOCS_INDEX: docs/docs-index.md - SCRIPT_DIR: scripts - WORKFLOWS_DIR: .mokogitea/workflows - SHELLCHECK_PATTERN: '*.sh' - SPDX_FILE_GLOBS: '*.sh,*.php,*.js,*.ts,*.css,*.xml,*.yml,*.yaml' - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - access_check: - name: Access control - runs-on: ubuntu-latest - timeout-minutes: 10 - permissions: - contents: read - - outputs: - allowed: ${{ steps.perm.outputs.allowed }} - permission: ${{ steps.perm.outputs.permission }} - - steps: - - name: Check actor permission (admin only) - id: perm - env: - TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }} - REPO: ${{ github.repository }} - ACTOR: ${{ github.actor }} - run: | - set -euo pipefail - ALLOWED=false - PERMISSION=unknown - METHOD="" - - # Hardcoded authorized users — always allowed - case "$ACTOR" in - jmiller|gitea-actions[bot]) - ALLOWED=true - PERMISSION=admin - METHOD="hardcoded allowlist" - ;; - *) - # Detect platform and check permissions via API - API_BASE="${GITHUB_API_URL:-${GITEA_API_URL:-https://api.github.com}}" - RESP=$(curl -sf -H "Authorization: token ${TOKEN}" \ - "${API_BASE}/repos/${REPO}/collaborators/${ACTOR}/permission" 2>/dev/null || echo '{}') - PERMISSION=$(echo "$RESP" | grep -oP '"permission"\s*:\s*"\K[^"]+' || echo "unknown") - if [ "$PERMISSION" = "admin" ] || [ "$PERMISSION" = "maintain" ] || [ "$PERMISSION" = "owner" ]; then - ALLOWED=true - fi - METHOD="collaborator API" - ;; - esac - - echo "permission=${PERMISSION}" >> "$GITHUB_OUTPUT" - echo "allowed=${ALLOWED}" >> "$GITHUB_OUTPUT" - - { - echo "## Access Authorization" - echo "" - echo "| Field | Value |" - echo "|-------|-------|" - echo "| **Actor** | \`${ACTOR}\` |" - echo "| **Repository** | \`${REPO}\` |" - echo "| **Permission** | \`${PERMISSION}\` |" - echo "| **Method** | ${METHOD} |" - echo "| **Authorized** | ${ALLOWED} |" - echo "" - if [ "$ALLOWED" = "true" ]; then - echo "${ACTOR} authorized (${METHOD})" - else - echo "${ACTOR} is NOT authorized. Requires admin or maintain role." - fi - } >> "${GITHUB_STEP_SUMMARY}" - - - name: Deny execution when not permitted - if: ${{ steps.perm.outputs.allowed != 'true' }} - run: | - set -euo pipefail - printf '%s\n' 'ERROR: Access denied. Admin permission required.' >> "${GITHUB_STEP_SUMMARY}" - exit 1 - - scripts_governance: - name: Scripts governance - needs: access_check - if: ${{ needs.access_check.outputs.allowed == 'true' }} - runs-on: ubuntu-latest - timeout-minutes: 15 - permissions: - contents: read - - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - fetch-depth: 0 - - - name: Scripts folder checks - env: - PROFILE_RAW: ${{ github.event.inputs.profile }} - run: | - set -euo pipefail - - profile="${PROFILE_RAW:-all}" - case "${profile}" in - all|scripts|repo) ;; - *) - printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" - exit 1 - ;; - esac - - if [ "${profile}" = 'repo' ]; then - { - printf '%s\n' '### Scripts governance' - printf '%s\n' "Profile: ${profile}" - printf '%s\n' 'Status: SKIPPED' - printf '%s\n' 'Reason: profile excludes scripts governance' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - exit 0 - fi - - if [ ! -d "${SCRIPT_DIR}" ]; then - { - printf '%s\n' '### Scripts governance' - printf '%s\n' 'Status: OK (advisory)' - printf '%s\n' 'scripts/ directory not present. No scripts governance enforced.' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - exit 0 - fi - - if [ -n "${SCRIPTS_REQUIRED_DIRS:-}" ]; then IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}"; else required_dirs=(); fi - IFS=',' read -r -a allowed_dirs <<< "${SCRIPTS_ALLOWED_DIRS}" - - missing_dirs=() - unapproved_dirs=() - - for d in "${required_dirs[@]}"; do - req="${d%/}" - [ ! -d "${req}" ] && missing_dirs+=("${req}/") - done - - while IFS= read -r d; do - allowed=false - for a in "${allowed_dirs[@]}"; do - a_norm="${a%/}" - [ "${d%/}" = "${a_norm}" ] && allowed=true - done - [ "${allowed}" = false ] && unapproved_dirs+=("${d%/}/") - done < <(find "${SCRIPT_DIR}" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sed 's#^\./##') - - { - printf '%s\n' '### Scripts governance' - printf '%s\n' "Profile: ${profile}" - printf '%s\n' '| Area | Status | Notes |' - printf '%s\n' '|---|---|---|' - - if [ "${#missing_dirs[@]}" -gt 0 ]; then - printf '%s\n' '| Required directories | Warning | Missing required subfolders |' - else - printf '%s\n' '| Required directories | OK | All required subfolders present |' - fi - - if [ "${#unapproved_dirs[@]}" -gt 0 ]; then - printf '%s\n' '| Directory policy | Warning | Unapproved directories detected |' - else - printf '%s\n' '| Directory policy | OK | No unapproved directories |' - fi - - printf '%s\n' '| Enforcement mode | Advisory | scripts folder is optional |' - printf '\n' - - if [ "${#missing_dirs[@]}" -gt 0 ]; then - printf '%s\n' 'Missing required script directories:' - for m in "${missing_dirs[@]}"; do printf '%s\n' "- ${m}"; done - printf '\n' - else - printf '%s\n' 'Missing required script directories: none.' - printf '\n' - fi - - if [ "${#unapproved_dirs[@]}" -gt 0 ]; then - printf '%s\n' 'Unapproved script directories detected:' - for m in "${unapproved_dirs[@]}"; do printf '%s\n' "- ${m}"; done - printf '\n' - else - printf '%s\n' 'Unapproved script directories detected: none.' - printf '\n' - fi - - printf '%s\n' 'Scripts governance completed in advisory mode.' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - - repo_health: - name: Repository health - needs: access_check - if: ${{ needs.access_check.outputs.allowed == 'true' }} - runs-on: ubuntu-latest - timeout-minutes: 20 - permissions: - contents: read - - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - fetch-depth: 0 - - - name: Repository health checks - env: - PROFILE_RAW: ${{ github.event.inputs.profile }} - run: | - set -euo pipefail - - profile="${PROFILE_RAW:-all}" - case "${profile}" in - all|scripts|repo) ;; - *) - printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" - exit 1 - ;; - esac - - if [ "${profile}" = 'scripts' ]; then - { - printf '%s\n' '### Repository health' - printf '%s\n' "Profile: ${profile}" - printf '%s\n' 'Status: SKIPPED' - printf '%s\n' 'Reason: profile excludes repository health' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - exit 0 - fi - - IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}" - IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}" - if [ -n "${REPO_DISALLOWED_DIRS:-}" ]; then IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}"; else disallowed_dirs=(); fi - IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES:-}" - - missing_required=() - missing_optional=() - - # Source directory: src/ or htdocs/ (either is valid for extension repos) - SOURCE_DIR="" - if [ -d "src" ]; then - SOURCE_DIR="src" - elif [ -d "htdocs" ]; then - SOURCE_DIR="htdocs" - elif [ -d "deploy" ] || [ -d "cli" ] || [ -d "monitoring" ]; then - # Platform/tooling repos don't need src/ - SOURCE_DIR="" - else - missing_required+=("src/ or htdocs/ (source directory required)") - fi - - for item in "${required_artifacts[@]}"; do - if printf '%s' "${item}" | grep -q '/$'; then - d="${item%/}" - [ ! -d "${d}" ] && missing_required+=("${item}") - else - [ ! -f "${item}" ] && missing_required+=("${item}") - fi - done - - for f in "${optional_files[@]}"; do - if printf '%s' "${f}" | grep -q '/$'; then - d="${f%/}" - [ ! -d "${d}" ] && missing_optional+=("${f}") - else - [ ! -f "${f}" ] && missing_optional+=("${f}") - fi - done - - for d in "${disallowed_dirs[@]}"; do - d_norm="${d%/}" - [ -d "${d_norm}" ] && missing_required+=("${d_norm}/ (disallowed)") - done - - for f in "${disallowed_files[@]}"; do - [ -f "${f}" ] && missing_required+=("${f} (disallowed)") - done - - git fetch origin --prune - - dev_paths=() - dev_branches=() - - while IFS= read -r b; do - name="${b#origin/}" - if [ "${name}" = 'dev' ]; then - dev_branches+=("${name}") - else - dev_paths+=("${name}") - fi - done < <(git branch -r --list 'origin/dev*' | sed 's/^ *//') - - if [ "${#dev_paths[@]}" -eq 0 ] && [ "${#dev_branches[@]}" -eq 0 ]; then - missing_required+=("dev or dev/* branch") - fi - - content_warnings=() - - if [ -f 'CHANGELOG.md' ] && ! grep -Eq '^# Changelog' CHANGELOG.md; then - content_warnings+=("CHANGELOG.md missing '# Changelog' header") - fi - - if [ -f 'CHANGELOG.md' ] && grep -Eq '^[# ]*Unreleased' CHANGELOG.md; then - content_warnings+=("CHANGELOG.md contains Unreleased section (review release readiness)") - fi - - if [ -f 'LICENSE' ] && ! grep -qiE 'GNU GENERAL PUBLIC LICENSE|GPL' LICENSE; then - content_warnings+=("LICENSE does not look like a GPL text") - fi - - if [ -f 'README.md' ] && ! grep -qiE 'moko|Moko' README.md; then - content_warnings+=("README.md missing expected brand keyword") - fi - - export PROFILE_RAW="${profile}" - export MISSING_REQUIRED="$(printf '%s\n' "${missing_required[@]:-}")" - export MISSING_OPTIONAL="$(printf '%s\n' "${missing_optional[@]:-}")" - export CONTENT_WARNINGS="$(printf '%s\n' "${content_warnings[@]:-}")" - - report_json=$(printf '{"profile":"%s","missing_required":%d,"missing_optional":%d,"content_warnings":%d}' "$profile" "${#missing_required[@]}" "${#missing_optional[@]}" "${#content_warnings[@]}") - - { - printf '%s\n' '### Repository health' - printf '%s\n' "Profile: ${profile}" - printf '%s\n' '| Metric | Value |' - printf '%s\n' '|---|---|' - printf '%s\n' "| Missing required | ${#missing_required[@]} |" - printf '%s\n' "| Missing optional | ${#missing_optional[@]} |" - printf '%s\n' "| Content warnings | ${#content_warnings[@]} |" - printf '\n' - - printf '%s\n' '### Guardrails report (JSON)' - printf '%s\n' '```json' - printf '%s\n' "${report_json}" - printf '%s\n' '```' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - - if [ "${#missing_required[@]}" -gt 0 ]; then - { - printf '%s\n' '### Missing required repo artifacts' - for m in "${missing_required[@]}"; do printf '%s\n' "- ${m}"; done - printf '%s\n' 'ERROR: Guardrails failed. Missing required repository artifacts.' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - exit 1 - fi - - if [ "${#missing_optional[@]}" -gt 0 ]; then - { - printf '%s\n' '### Missing optional repo artifacts' - for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - - if [ "${#content_warnings[@]}" -gt 0 ]; then - { - printf '%s\n' '### Repo content warnings' - for m in "${content_warnings[@]}"; do printf '%s\n' "- ${m}"; done - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - - # -- Joomla-specific checks -- - joomla_findings=() - - MANIFEST="$(find . -maxdepth 2 -name '*.xml' -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true)" - if [ -z "${MANIFEST}" ]; then - joomla_findings+=("Joomla XML manifest not found (no *.xml with <extension> tag)") - else - if ! grep -qP '<version>' "${MANIFEST}"; then - joomla_findings+=("XML manifest: <version> tag missing") - fi - if ! grep -qP 'type="(component|module|plugin|library|package|template|language)"' "${MANIFEST}"; then - joomla_findings+=("XML manifest: type attribute missing or invalid") - fi - if ! grep -qP '<name>' "${MANIFEST}"; then - joomla_findings+=("XML manifest: <name> tag missing") - fi - if ! grep -qP '<author>' "${MANIFEST}"; then - joomla_findings+=("XML manifest: <author> tag missing") - fi - if ! grep -qP '<namespace' "${MANIFEST}"; then - joomla_findings+=("XML manifest: <namespace> missing (required for Joomla 5+)") - fi - fi - - INI_COUNT="$(find . -name '*.ini' -type f 2>/dev/null | wc -l)" - if [ "${INI_COUNT}" -eq 0 ]; then - joomla_findings+=("No .ini language files found") - fi - - if [ ! -f 'updates.xml' ]; then - joomla_findings+=("updates.xml missing in root (required for Joomla update server)") - fi - - if [ -n "${SOURCE_DIR}" ]; then - INDEX_DIRS=("${SOURCE_DIR}" "${SOURCE_DIR}/admin" "${SOURCE_DIR}/site") - for dir in "${INDEX_DIRS[@]}"; do - if [ -d "${dir}" ] && [ ! -f "${dir}/index.html" ]; then - joomla_findings+=("${dir}/index.html missing (directory listing protection)") - fi - done - fi - - if [ "${#joomla_findings[@]}" -gt 0 ]; then - { - printf '%s\n' '### Joomla extension checks' - printf '%s\n' '| Check | Status |' - printf '%s\n' '|---|---|' - for f in "${joomla_findings[@]}"; do - printf '%s\n' "| ${f} | Warning |" - done - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - else - { - printf '%s\n' '### Joomla extension checks' - printf '%s\n' 'All Joomla-specific checks passed.' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - - extended_enabled="${EXTENDED_CHECKS:-true}" - extended_findings=() - - if [ "${extended_enabled}" = 'true' ]; then - if [ -f '.github/CODEOWNERS' ] || [ -f 'CODEOWNERS' ] || [ -f 'docs/CODEOWNERS' ]; then - : - else - extended_findings+=("CODEOWNERS not found (.github/CODEOWNERS preferred)") - fi - - if ls "${WORKFLOWS_DIR}"/*.yml >/dev/null 2>&1 || ls "${WORKFLOWS_DIR}"/*.yaml >/dev/null 2>&1; then - bad_refs="$(grep -RIn --include='*.yml' --include='*.yaml' -E '^[[:space:]]*uses:[[:space:]]*[^#]+@(main|master)\b' "${WORKFLOWS_DIR}" 2>/dev/null || true)" - if [ -n "${bad_refs}" ]; then - extended_findings+=("Workflows reference actions @main/@master (pin versions): see log excerpt") - { - printf '%s\n' '### Workflow pinning advisory' - printf '%s\n' 'Found uses: entries pinned to main/master:' - printf '%s\n' '```' - printf '%s\n' "${bad_refs}" - printf '%s\n' '```' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - fi - - if [ -f "${DOCS_INDEX}" ]; then - missing_links="" - while IFS= read -r docline; do - for link in $(echo "$docline" | grep -oE '\]\([^)]+\)' | sed 's/\](//' | sed 's/)$//' || true); do - case "$link" in http://*|https://*|"#"*|mailto:*) continue ;; esac - linkpath="${link%%#*}" - linkpath="${linkpath%%\?*}" - [ -z "$linkpath" ] && continue - if [ "${linkpath:0:1}" = "/" ]; then - testpath="${linkpath#/}" - else - testpath="$(dirname "${DOCS_INDEX}")/${linkpath}" - fi - [ ! -e "$testpath" ] && missing_links="${missing_links}${testpath} " - done - done < "${DOCS_INDEX}" - if [ -n "${missing_links}" ]; then - extended_findings+=("docs/docs-index.md contains broken relative links") - { - printf '%s\n' '### Docs index link integrity' - printf '%s\n' 'Broken relative links:' - for bl in ${missing_links}; do - printf '%s\n' "- ${bl}" - done - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - fi - - if [ -d "${SCRIPT_DIR}" ]; then - if ! command -v shellcheck >/dev/null 2>&1; then - sudo apt-get update -qq - sudo apt-get install -y shellcheck >/dev/null - fi - - sc_out='' - while IFS= read -r shf; do - [ -z "${shf}" ] && continue - out_one="$(shellcheck -S warning -x "${shf}" 2>/dev/null || true)" - if [ -n "${out_one}" ]; then - sc_out="${sc_out}${out_one}\n" - fi - done < <(find "${SCRIPT_DIR}" -type f -name "${SHELLCHECK_PATTERN}" 2>/dev/null | sort) - - if [ -n "${sc_out}" ]; then - extended_findings+=("ShellCheck warnings detected (advisory)") - sc_head="$(printf '%s' "${sc_out}" | head -n 200)" - { - printf '%s\n' '### ShellCheck (advisory)' - printf '%s\n' '```' - printf '%s\n' "${sc_head}" - printf '%s\n' '```' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - fi - - spdx_missing=() - IFS=',' read -r -a spdx_globs <<< "${SPDX_FILE_GLOBS}" - spdx_args=() - for g in "${spdx_globs[@]}"; do spdx_args+=("${g}"); done - - while IFS= read -r f; do - [ -z "${f}" ] && continue - if ! head -n 40 "${f}" | grep -q 'SPDX-License-Identifier:'; then - spdx_missing+=("${f}") - fi - done < <(git ls-files "${spdx_args[@]}" 2>/dev/null || true) - - if [ "${#spdx_missing[@]}" -gt 0 ]; then - extended_findings+=("SPDX header missing in some tracked files (advisory)") - { - printf '%s\n' '### SPDX header advisory' - printf '%s\n' 'Files missing SPDX-License-Identifier (first 40 lines scan):' - for f in "${spdx_missing[@]}"; do printf '%s\n' "- ${f}"; done - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - - stale_cutoff_days=180 - stale_branches="$(git for-each-ref --format='%(refname:short) %(committerdate:unix)' refs/remotes/origin 2>/dev/null | awk -v now="$(date +%s)" -v days="${stale_cutoff_days}" '{if (now-$2 > days*86400) print $1}' | head -50)" - if [ -n "${stale_branches}" ]; then - extended_findings+=("Stale remote branches detected (advisory)") - { - printf '%s\n' '### Git hygiene advisory' - printf '%s\n' "Branches with last commit older than ${stale_cutoff_days} days (sample up to 50):" - while IFS= read -r b; do [ -n "${b}" ] && printf '%s\n' "- ${b}"; done <<< "${stale_branches}" - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - fi - - { - printf '%s\n' '### Guardrails coverage matrix' - printf '%s\n' '| Domain | Status | Notes |' - printf '%s\n' '|---|---|---|' - printf '%s\n' '| Access control | OK | Admin-only execution gate |' - printf '%s\n' '| Release policy | N/A | Releases handled by MokoGitea |' - printf '%s\n' '| Scripts governance | OK | Directory policy and advisory reporting |' - printf '%s\n' '| Repo required artifacts | OK | Required, optional, disallowed enforcement |' - printf '%s\n' '| Repo content heuristics | OK | Brand, license, changelog structure |' - if [ "${extended_enabled}" = 'true' ]; then - if [ "${#extended_findings[@]}" -gt 0 ]; then - printf '%s\n' '| Extended checks | Warning | See extended findings below |' - else - printf '%s\n' '| Extended checks | OK | No findings |' - fi - else - printf '%s\n' '| Extended checks | SKIPPED | EXTENDED_CHECKS disabled |' - fi - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - - if [ "${extended_enabled}" = 'true' ] && [ "${#extended_findings[@]}" -gt 0 ]; then - { - printf '%s\n' '### Extended findings (advisory)' - for f in "${extended_findings[@]}"; do printf '%s\n' "- ${f}"; done - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - - printf '%s\n' 'Repository health guardrails passed.' >> "${GITHUB_STEP_SUMMARY}" - - - site-health: - name: Site Health - runs-on: ubuntu-latest - if: github.event_name == 'workflow_dispatch' - steps: - - uses: actions/checkout@v4 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: '8.3' - - - name: Uptime check - if: env.URLS != '' - run: | - echo "$URLS" > /tmp/urls.txt - php monitoring/uptime-probe.php --urls /tmp/urls.txt --timeout 15 || echo "::warning::Some sites are down" - rm -f /tmp/urls.txt - env: - URLS: ${{ vars.MONITORED_URLS }} - - - name: SSL certificate check - if: env.DOMAINS != '' - run: | - echo "$DOMAINS" > /tmp/domains.txt - php monitoring/ssl-check.php --domains /tmp/domains.txt --warn-days 30 || echo "::warning::SSL certificates expiring soon" - rm -f /tmp/domains.txt - env: - DOMAINS: ${{ vars.MONITORED_DOMAINS }} - - - name: Summary - if: always() - run: | - echo "### Site Health" >> $GITHUB_STEP_SUMMARY - echo "Uptime and SSL checks completed." >> $GITHUB_STEP_SUMMARY - - # ═══════════════════════════════════════════════════════════════════════ - # Issue Reporter — file issues for failed gates - # ═══════════════════════════════════════════════════════════════════════ - report-issues: - name: "Report Issues" - runs-on: ubuntu-latest - needs: [access_check, scripts_governance, repo_health] - if: >- - always() && - (needs.scripts_governance.result == 'failure' || - needs.repo_health.result == 'failure') - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - sparse-checkout: automation/ci-issue-reporter.sh - sparse-checkout-cone-mode: false - - - name: "File issues for failed gates" - env: - GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - run: | - chmod +x automation/ci-issue-reporter.sh - REPORTER="./automation/ci-issue-reporter.sh" - WF="Repo Health" - - report_gate() { - local gate="$1" result="$2" details="$3" - if [ "$result" = "failure" ]; then - "$REPORTER" --gate "$gate" --details "$details" --workflow "$WF" --severity error - fi - } - - report_gate "Scripts Governance" \ - "${{ needs.scripts_governance.result }}" \ - "Scripts directory policy violations detected. Review required and allowed directories." - - report_gate "Repository Health" \ - "${{ needs.repo_health.result }}" \ - "Repository health checks failed — missing required artifacts, disallowed files, or content warnings. Check the CI run summary." +# ============================================================================ +# Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech> +# +# This file is part of a Moko Consulting project. +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: moko-platform.Validation +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform +# PATH: /templates/workflows/joomla/repo_health.yml.template +# VERSION: 09.23.00 +# BRIEF: Enforces repository guardrails by validating scripts governance, tooling availability, and core repository health artifacts. +# ============================================================================ + +name: "Generic: Repo Health" + +defaults: + run: + shell: bash + +on: + workflow_dispatch: + inputs: + profile: + description: 'Validation profile: all, scripts, or repo' + required: true + default: all + type: choice + options: + - all + - scripts + - repo + pull_request: + push: + +permissions: + contents: read + +env: + # Scripts governance policy + SCRIPTS_REQUIRED_DIRS: + SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate + + # Repo health policy + REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.mokogitea/workflows/ + REPO_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore,README.md,docs/ + REPO_DISALLOWED_DIRS: + REPO_DISALLOWED_FILES: TODO.md,todo.md + + # Extended checks toggles + EXTENDED_CHECKS: "true" + + # File / directory variables + DOCS_INDEX: docs/docs-index.md + SCRIPT_DIR: scripts + WORKFLOWS_DIR: .mokogitea/workflows + SHELLCHECK_PATTERN: '*.sh' + SPDX_FILE_GLOBS: '*.sh,*.php,*.js,*.ts,*.css,*.xml,*.yml,*.yaml' + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + access_check: + name: Access control + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + + outputs: + allowed: ${{ steps.perm.outputs.allowed }} + permission: ${{ steps.perm.outputs.permission }} + + steps: + - name: Check actor permission (admin only) + id: perm + env: + TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }} + REPO: ${{ github.repository }} + ACTOR: ${{ github.actor }} + run: | + set -euo pipefail + ALLOWED=false + PERMISSION=unknown + METHOD="" + + # Hardcoded authorized users — always allowed + case "$ACTOR" in + jmiller|gitea-actions[bot]) + ALLOWED=true + PERMISSION=admin + METHOD="hardcoded allowlist" + ;; + *) + # Detect platform and check permissions via API + API_BASE="${GITHUB_API_URL:-${GITEA_API_URL:-https://api.github.com}}" + RESP=$(curl -sf -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/repos/${REPO}/collaborators/${ACTOR}/permission" 2>/dev/null || echo '{}') + PERMISSION=$(echo "$RESP" | grep -oP '"permission"\s*:\s*"\K[^"]+' || echo "unknown") + if [ "$PERMISSION" = "admin" ] || [ "$PERMISSION" = "maintain" ] || [ "$PERMISSION" = "owner" ]; then + ALLOWED=true + fi + METHOD="collaborator API" + ;; + esac + + echo "permission=${PERMISSION}" >> "$GITHUB_OUTPUT" + echo "allowed=${ALLOWED}" >> "$GITHUB_OUTPUT" + + { + echo "## Access Authorization" + echo "" + echo "| Field | Value |" + echo "|-------|-------|" + echo "| **Actor** | \`${ACTOR}\` |" + echo "| **Repository** | \`${REPO}\` |" + echo "| **Permission** | \`${PERMISSION}\` |" + echo "| **Method** | ${METHOD} |" + echo "| **Authorized** | ${ALLOWED} |" + echo "" + if [ "$ALLOWED" = "true" ]; then + echo "${ACTOR} authorized (${METHOD})" + else + echo "${ACTOR} is NOT authorized. Requires admin or maintain role." + fi + } >> "${GITHUB_STEP_SUMMARY}" + + - name: Deny execution when not permitted + if: ${{ steps.perm.outputs.allowed != 'true' }} + run: | + set -euo pipefail + printf '%s\n' 'ERROR: Access denied. Admin permission required.' >> "${GITHUB_STEP_SUMMARY}" + exit 1 + + scripts_governance: + name: Scripts governance + needs: access_check + if: ${{ needs.access_check.outputs.allowed == 'true' }} + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 0 + + - name: Scripts folder checks + env: + PROFILE_RAW: ${{ github.event.inputs.profile }} + run: | + set -euo pipefail + + profile="${PROFILE_RAW:-all}" + case "${profile}" in + all|scripts|repo) ;; + *) + printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" + exit 1 + ;; + esac + + if [ "${profile}" = 'repo' ]; then + { + printf '%s\n' '### Scripts governance' + printf '%s\n' "Profile: ${profile}" + printf '%s\n' 'Status: SKIPPED' + printf '%s\n' 'Reason: profile excludes scripts governance' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + exit 0 + fi + + if [ ! -d "${SCRIPT_DIR}" ]; then + { + printf '%s\n' '### Scripts governance' + printf '%s\n' 'Status: OK (advisory)' + printf '%s\n' 'scripts/ directory not present. No scripts governance enforced.' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + exit 0 + fi + + if [ -n "${SCRIPTS_REQUIRED_DIRS:-}" ]; then IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}"; else required_dirs=(); fi + IFS=',' read -r -a allowed_dirs <<< "${SCRIPTS_ALLOWED_DIRS}" + + missing_dirs=() + unapproved_dirs=() + + for d in "${required_dirs[@]}"; do + req="${d%/}" + [ ! -d "${req}" ] && missing_dirs+=("${req}/") + done + + while IFS= read -r d; do + allowed=false + for a in "${allowed_dirs[@]}"; do + a_norm="${a%/}" + [ "${d%/}" = "${a_norm}" ] && allowed=true + done + [ "${allowed}" = false ] && unapproved_dirs+=("${d%/}/") + done < <(find "${SCRIPT_DIR}" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sed 's#^\./##') + + { + printf '%s\n' '### Scripts governance' + printf '%s\n' "Profile: ${profile}" + printf '%s\n' '| Area | Status | Notes |' + printf '%s\n' '|---|---|---|' + + if [ "${#missing_dirs[@]}" -gt 0 ]; then + printf '%s\n' '| Required directories | Warning | Missing required subfolders |' + else + printf '%s\n' '| Required directories | OK | All required subfolders present |' + fi + + if [ "${#unapproved_dirs[@]}" -gt 0 ]; then + printf '%s\n' '| Directory policy | Warning | Unapproved directories detected |' + else + printf '%s\n' '| Directory policy | OK | No unapproved directories |' + fi + + printf '%s\n' '| Enforcement mode | Advisory | scripts folder is optional |' + printf '\n' + + if [ "${#missing_dirs[@]}" -gt 0 ]; then + printf '%s\n' 'Missing required script directories:' + for m in "${missing_dirs[@]}"; do printf '%s\n' "- ${m}"; done + printf '\n' + else + printf '%s\n' 'Missing required script directories: none.' + printf '\n' + fi + + if [ "${#unapproved_dirs[@]}" -gt 0 ]; then + printf '%s\n' 'Unapproved script directories detected:' + for m in "${unapproved_dirs[@]}"; do printf '%s\n' "- ${m}"; done + printf '\n' + else + printf '%s\n' 'Unapproved script directories detected: none.' + printf '\n' + fi + + printf '%s\n' 'Scripts governance completed in advisory mode.' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + + repo_health: + name: Repository health + needs: access_check + if: ${{ needs.access_check.outputs.allowed == 'true' }} + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 0 + + - name: Repository health checks + env: + PROFILE_RAW: ${{ github.event.inputs.profile }} + run: | + set -euo pipefail + + profile="${PROFILE_RAW:-all}" + case "${profile}" in + all|scripts|repo) ;; + *) + printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" + exit 1 + ;; + esac + + if [ "${profile}" = 'scripts' ]; then + { + printf '%s\n' '### Repository health' + printf '%s\n' "Profile: ${profile}" + printf '%s\n' 'Status: SKIPPED' + printf '%s\n' 'Reason: profile excludes repository health' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + exit 0 + fi + + IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}" + IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}" + if [ -n "${REPO_DISALLOWED_DIRS:-}" ]; then IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}"; else disallowed_dirs=(); fi + IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES:-}" + + missing_required=() + missing_optional=() + + # Source directory: source/, src/, or htdocs/ (any is valid for extension repos) + SOURCE_DIR="" + if [ -d "source" ]; then + SOURCE_DIR="source" + elif [ -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/ + SOURCE_DIR="" + else + missing_required+=("source/, src/, or htdocs/ (source directory required)") + fi + + for item in "${required_artifacts[@]}"; do + if printf '%s' "${item}" | grep -q '/$'; then + d="${item%/}" + [ ! -d "${d}" ] && missing_required+=("${item}") + else + [ ! -f "${item}" ] && missing_required+=("${item}") + fi + done + + for f in "${optional_files[@]}"; do + if printf '%s' "${f}" | grep -q '/$'; then + d="${f%/}" + [ ! -d "${d}" ] && missing_optional+=("${f}") + else + [ ! -f "${f}" ] && missing_optional+=("${f}") + fi + done + + for d in "${disallowed_dirs[@]}"; do + d_norm="${d%/}" + [ -d "${d_norm}" ] && missing_required+=("${d_norm}/ (disallowed)") + done + + for f in "${disallowed_files[@]}"; do + [ -f "${f}" ] && missing_required+=("${f} (disallowed)") + done + + git fetch origin --prune + + dev_paths=() + dev_branches=() + + while IFS= read -r b; do + name="${b#origin/}" + if [ "${name}" = 'dev' ]; then + dev_branches+=("${name}") + else + dev_paths+=("${name}") + fi + done < <(git branch -r --list 'origin/dev*' | sed 's/^ *//') + + if [ "${#dev_paths[@]}" -eq 0 ] && [ "${#dev_branches[@]}" -eq 0 ]; then + missing_required+=("dev or dev/* branch") + fi + + content_warnings=() + + if [ -f 'CHANGELOG.md' ] && ! grep -Eq '^# Changelog' CHANGELOG.md; then + content_warnings+=("CHANGELOG.md missing '# Changelog' header") + fi + + if [ -f 'CHANGELOG.md' ] && grep -Eq '^[# ]*Unreleased' CHANGELOG.md; then + content_warnings+=("CHANGELOG.md contains Unreleased section (review release readiness)") + fi + + if [ -f 'LICENSE' ] && ! grep -qiE 'GNU GENERAL PUBLIC LICENSE|GPL' LICENSE; then + content_warnings+=("LICENSE does not look like a GPL text") + fi + + if [ -f 'README.md' ] && ! grep -qiE 'moko|Moko' README.md; then + content_warnings+=("README.md missing expected brand keyword") + fi + + export PROFILE_RAW="${profile}" + export MISSING_REQUIRED="$(printf '%s\n' "${missing_required[@]:-}")" + export MISSING_OPTIONAL="$(printf '%s\n' "${missing_optional[@]:-}")" + export CONTENT_WARNINGS="$(printf '%s\n' "${content_warnings[@]:-}")" + + report_json=$(printf '{"profile":"%s","missing_required":%d,"missing_optional":%d,"content_warnings":%d}' "$profile" "${#missing_required[@]}" "${#missing_optional[@]}" "${#content_warnings[@]}") + + { + printf '%s\n' '### Repository health' + printf '%s\n' "Profile: ${profile}" + printf '%s\n' '| Metric | Value |' + printf '%s\n' '|---|---|' + printf '%s\n' "| Missing required | ${#missing_required[@]} |" + printf '%s\n' "| Missing optional | ${#missing_optional[@]} |" + printf '%s\n' "| Content warnings | ${#content_warnings[@]} |" + printf '\n' + + printf '%s\n' '### Guardrails report (JSON)' + printf '%s\n' '```json' + printf '%s\n' "${report_json}" + printf '%s\n' '```' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + + if [ "${#missing_required[@]}" -gt 0 ]; then + { + printf '%s\n' '### Missing required repo artifacts' + for m in "${missing_required[@]}"; do printf '%s\n' "- ${m}"; done + printf '%s\n' 'ERROR: Guardrails failed. Missing required repository artifacts.' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + exit 1 + fi + + if [ "${#missing_optional[@]}" -gt 0 ]; then + { + printf '%s\n' '### Missing optional repo artifacts' + for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + + if [ "${#content_warnings[@]}" -gt 0 ]; then + { + printf '%s\n' '### Repo content warnings' + for m in "${content_warnings[@]}"; do printf '%s\n' "- ${m}"; done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + + # -- Joomla-specific checks -- + joomla_findings=() + + MANIFEST="$(find . -maxdepth 2 -name '*.xml' -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true)" + if [ -z "${MANIFEST}" ]; then + joomla_findings+=("Joomla XML manifest not found (no *.xml with <extension> tag)") + else + if ! grep -qP '<version>' "${MANIFEST}"; then + joomla_findings+=("XML manifest: <version> tag missing") + fi + if ! grep -qP 'type="(component|module|plugin|library|package|template|language)"' "${MANIFEST}"; then + joomla_findings+=("XML manifest: type attribute missing or invalid") + fi + if ! grep -qP '<name>' "${MANIFEST}"; then + joomla_findings+=("XML manifest: <name> tag missing") + fi + if ! grep -qP '<author>' "${MANIFEST}"; then + joomla_findings+=("XML manifest: <author> tag missing") + fi + if ! grep -qP '<namespace' "${MANIFEST}"; then + joomla_findings+=("XML manifest: <namespace> missing (required for Joomla 5+)") + fi + fi + + INI_COUNT="$(find . -name '*.ini' -type f 2>/dev/null | wc -l)" + if [ "${INI_COUNT}" -eq 0 ]; then + joomla_findings+=("No .ini language files found") + fi + + if [ ! -f 'updates.xml' ]; then + joomla_findings+=("updates.xml missing in root (required for Joomla update server)") + fi + + if [ -n "${SOURCE_DIR}" ]; then + INDEX_DIRS=("${SOURCE_DIR}" "${SOURCE_DIR}/admin" "${SOURCE_DIR}/site") + for dir in "${INDEX_DIRS[@]}"; do + if [ -d "${dir}" ] && [ ! -f "${dir}/index.html" ]; then + joomla_findings+=("${dir}/index.html missing (directory listing protection)") + fi + done + fi + + if [ "${#joomla_findings[@]}" -gt 0 ]; then + { + printf '%s\n' '### Joomla extension checks' + printf '%s\n' '| Check | Status |' + printf '%s\n' '|---|---|' + for f in "${joomla_findings[@]}"; do + printf '%s\n' "| ${f} | Warning |" + done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + else + { + printf '%s\n' '### Joomla extension checks' + printf '%s\n' 'All Joomla-specific checks passed.' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + + extended_enabled="${EXTENDED_CHECKS:-true}" + extended_findings=() + + if [ "${extended_enabled}" = 'true' ]; then + if [ -f '.github/CODEOWNERS' ] || [ -f 'CODEOWNERS' ] || [ -f 'docs/CODEOWNERS' ]; then + : + else + extended_findings+=("CODEOWNERS not found (.github/CODEOWNERS preferred)") + fi + + if ls "${WORKFLOWS_DIR}"/*.yml >/dev/null 2>&1 || ls "${WORKFLOWS_DIR}"/*.yaml >/dev/null 2>&1; then + bad_refs="$(grep -RIn --include='*.yml' --include='*.yaml' -E '^[[:space:]]*uses:[[:space:]]*[^#]+@(main|master)\b' "${WORKFLOWS_DIR}" 2>/dev/null || true)" + if [ -n "${bad_refs}" ]; then + extended_findings+=("Workflows reference actions @main/@master (pin versions): see log excerpt") + { + printf '%s\n' '### Workflow pinning advisory' + printf '%s\n' 'Found uses: entries pinned to main/master:' + printf '%s\n' '```' + printf '%s\n' "${bad_refs}" + printf '%s\n' '```' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + fi + + if [ -f "${DOCS_INDEX}" ]; then + missing_links="" + while IFS= read -r docline; do + for link in $(echo "$docline" | grep -oE '\]\([^)]+\)' | sed 's/\](//' | sed 's/)$//' || true); do + case "$link" in http://*|https://*|"#"*|mailto:*) continue ;; esac + linkpath="${link%%#*}" + linkpath="${linkpath%%\?*}" + [ -z "$linkpath" ] && continue + if [ "${linkpath:0:1}" = "/" ]; then + testpath="${linkpath#/}" + else + testpath="$(dirname "${DOCS_INDEX}")/${linkpath}" + fi + [ ! -e "$testpath" ] && missing_links="${missing_links}${testpath} " + done + done < "${DOCS_INDEX}" + if [ -n "${missing_links}" ]; then + extended_findings+=("docs/docs-index.md contains broken relative links") + { + printf '%s\n' '### Docs index link integrity' + printf '%s\n' 'Broken relative links:' + for bl in ${missing_links}; do + printf '%s\n' "- ${bl}" + done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + fi + + if [ -d "${SCRIPT_DIR}" ]; then + if ! command -v shellcheck >/dev/null 2>&1; then + sudo apt-get update -qq + sudo apt-get install -y shellcheck >/dev/null + fi + + sc_out='' + while IFS= read -r shf; do + [ -z "${shf}" ] && continue + out_one="$(shellcheck -S warning -x "${shf}" 2>/dev/null || true)" + if [ -n "${out_one}" ]; then + sc_out="${sc_out}${out_one}\n" + fi + done < <(find "${SCRIPT_DIR}" -type f -name "${SHELLCHECK_PATTERN}" 2>/dev/null | sort) + + if [ -n "${sc_out}" ]; then + extended_findings+=("ShellCheck warnings detected (advisory)") + sc_head="$(printf '%s' "${sc_out}" | head -n 200)" + { + printf '%s\n' '### ShellCheck (advisory)' + printf '%s\n' '```' + printf '%s\n' "${sc_head}" + printf '%s\n' '```' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + fi + + spdx_missing=() + IFS=',' read -r -a spdx_globs <<< "${SPDX_FILE_GLOBS}" + spdx_args=() + for g in "${spdx_globs[@]}"; do spdx_args+=("${g}"); done + + while IFS= read -r f; do + [ -z "${f}" ] && continue + if ! head -n 40 "${f}" | grep -q 'SPDX-License-Identifier:'; then + spdx_missing+=("${f}") + fi + done < <(git ls-files "${spdx_args[@]}" 2>/dev/null || true) + + if [ "${#spdx_missing[@]}" -gt 0 ]; then + extended_findings+=("SPDX header missing in some tracked files (advisory)") + { + printf '%s\n' '### SPDX header advisory' + printf '%s\n' 'Files missing SPDX-License-Identifier (first 40 lines scan):' + for f in "${spdx_missing[@]}"; do printf '%s\n' "- ${f}"; done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + + stale_cutoff_days=180 + stale_branches="$(git for-each-ref --format='%(refname:short) %(committerdate:unix)' refs/remotes/origin 2>/dev/null | awk -v now="$(date +%s)" -v days="${stale_cutoff_days}" '{if (now-$2 > days*86400) print $1}' | head -50)" + if [ -n "${stale_branches}" ]; then + extended_findings+=("Stale remote branches detected (advisory)") + { + printf '%s\n' '### Git hygiene advisory' + printf '%s\n' "Branches with last commit older than ${stale_cutoff_days} days (sample up to 50):" + while IFS= read -r b; do [ -n "${b}" ] && printf '%s\n' "- ${b}"; done <<< "${stale_branches}" + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + fi + + { + printf '%s\n' '### Guardrails coverage matrix' + printf '%s\n' '| Domain | Status | Notes |' + printf '%s\n' '|---|---|---|' + printf '%s\n' '| Access control | OK | Admin-only execution gate |' + printf '%s\n' '| Release policy | N/A | Releases handled by MokoGitea |' + printf '%s\n' '| Scripts governance | OK | Directory policy and advisory reporting |' + printf '%s\n' '| Repo required artifacts | OK | Required, optional, disallowed enforcement |' + printf '%s\n' '| Repo content heuristics | OK | Brand, license, changelog structure |' + if [ "${extended_enabled}" = 'true' ]; then + if [ "${#extended_findings[@]}" -gt 0 ]; then + printf '%s\n' '| Extended checks | Warning | See extended findings below |' + else + printf '%s\n' '| Extended checks | OK | No findings |' + fi + else + printf '%s\n' '| Extended checks | SKIPPED | EXTENDED_CHECKS disabled |' + fi + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + + if [ "${extended_enabled}" = 'true' ] && [ "${#extended_findings[@]}" -gt 0 ]; then + { + printf '%s\n' '### Extended findings (advisory)' + for f in "${extended_findings[@]}"; do printf '%s\n' "- ${f}"; done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + + printf '%s\n' 'Repository health guardrails passed.' >> "${GITHUB_STEP_SUMMARY}" + + + site-health: + name: Site Health + runs-on: ubuntu-latest + if: github.event_name == 'workflow_dispatch' + steps: + - uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + + - name: Uptime check + if: env.URLS != '' + run: | + echo "$URLS" > /tmp/urls.txt + php monitoring/uptime-probe.php --urls /tmp/urls.txt --timeout 15 || echo "::warning::Some sites are down" + rm -f /tmp/urls.txt + env: + URLS: ${{ vars.MONITORED_URLS }} + + - name: SSL certificate check + if: env.DOMAINS != '' + run: | + echo "$DOMAINS" > /tmp/domains.txt + php monitoring/ssl-check.php --domains /tmp/domains.txt --warn-days 30 || echo "::warning::SSL certificates expiring soon" + rm -f /tmp/domains.txt + env: + DOMAINS: ${{ vars.MONITORED_DOMAINS }} + + - name: Summary + if: always() + run: | + echo "### Site Health" >> $GITHUB_STEP_SUMMARY + echo "Uptime and SSL checks completed." >> $GITHUB_STEP_SUMMARY + + # ═══════════════════════════════════════════════════════════════════════ + # Issue Reporter — file issues for failed gates + # ═══════════════════════════════════════════════════════════════════════ + report-issues: + name: "Report Issues" + runs-on: ubuntu-latest + needs: [access_check, scripts_governance, repo_health] + if: >- + always() && + (needs.scripts_governance.result == 'failure' || + needs.repo_health.result == 'failure') + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + sparse-checkout: automation/ci-issue-reporter.sh + sparse-checkout-cone-mode: false + + - name: "File issues for failed gates" + env: + GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + run: | + chmod +x automation/ci-issue-reporter.sh + REPORTER="./automation/ci-issue-reporter.sh" + WF="Repo Health" + + report_gate() { + local gate="$1" result="$2" details="$3" + if [ "$result" = "failure" ]; then + "$REPORTER" --gate "$gate" --details "$details" --workflow "$WF" --severity error + fi + } + + report_gate "Scripts Governance" \ + "${{ needs.scripts_governance.result }}" \ + "Scripts directory policy violations detected. Review required and allowed directories." + + report_gate "Repository Health" \ + "${{ needs.repo_health.result }}" \ + "Repository health checks failed — missing required artifacts, disallowed files, or content warnings. Check the CI run summary." diff --git a/src/index.html b/source/index.html similarity index 100% rename from src/index.html rename to source/index.html diff --git a/src/language/en-GB/index.html b/source/language/en-GB/index.html similarity index 100% rename from src/language/en-GB/index.html rename to source/language/en-GB/index.html diff --git a/src/language/en-GB/pkg_mokoog.sys.ini b/source/language/en-GB/pkg_mokoog.sys.ini similarity index 100% rename from src/language/en-GB/pkg_mokoog.sys.ini rename to source/language/en-GB/pkg_mokoog.sys.ini diff --git a/src/language/en-US/index.html b/source/language/en-US/index.html similarity index 100% rename from src/language/en-US/index.html rename to source/language/en-US/index.html diff --git a/src/language/en-US/pkg_mokoog.sys.ini b/source/language/en-US/pkg_mokoog.sys.ini similarity index 100% rename from src/language/en-US/pkg_mokoog.sys.ini rename to source/language/en-US/pkg_mokoog.sys.ini diff --git a/src/language/index.html b/source/language/index.html similarity index 100% rename from src/language/index.html rename to source/language/index.html diff --git a/src/packages/com_mokoog/api/index.html b/source/packages/com_mokoog/api/index.html similarity index 100% rename from src/packages/com_mokoog/api/index.html rename to source/packages/com_mokoog/api/index.html diff --git a/src/packages/com_mokoog/api/src/Controller/TagsController.php b/source/packages/com_mokoog/api/src/Controller/TagsController.php similarity index 100% rename from src/packages/com_mokoog/api/src/Controller/TagsController.php rename to source/packages/com_mokoog/api/src/Controller/TagsController.php diff --git a/src/packages/com_mokoog/api/src/Controller/index.html b/source/packages/com_mokoog/api/src/Controller/index.html similarity index 100% rename from src/packages/com_mokoog/api/src/Controller/index.html rename to source/packages/com_mokoog/api/src/Controller/index.html diff --git a/src/packages/com_mokoog/api/src/View/Tags/JsonapiView.php b/source/packages/com_mokoog/api/src/View/Tags/JsonapiView.php similarity index 100% rename from src/packages/com_mokoog/api/src/View/Tags/JsonapiView.php rename to source/packages/com_mokoog/api/src/View/Tags/JsonapiView.php diff --git a/src/packages/com_mokoog/api/src/View/Tags/index.html b/source/packages/com_mokoog/api/src/View/Tags/index.html similarity index 100% rename from src/packages/com_mokoog/api/src/View/Tags/index.html rename to source/packages/com_mokoog/api/src/View/Tags/index.html diff --git a/src/packages/com_mokoog/api/src/View/index.html b/source/packages/com_mokoog/api/src/View/index.html similarity index 100% rename from src/packages/com_mokoog/api/src/View/index.html rename to source/packages/com_mokoog/api/src/View/index.html diff --git a/src/packages/com_mokoog/api/src/index.html b/source/packages/com_mokoog/api/src/index.html similarity index 100% rename from src/packages/com_mokoog/api/src/index.html rename to source/packages/com_mokoog/api/src/index.html diff --git a/src/packages/com_mokoog/forms/filter_tags.xml b/source/packages/com_mokoog/forms/filter_tags.xml similarity index 100% rename from src/packages/com_mokoog/forms/filter_tags.xml rename to source/packages/com_mokoog/forms/filter_tags.xml diff --git a/src/packages/com_mokoog/forms/index.html b/source/packages/com_mokoog/forms/index.html similarity index 100% rename from src/packages/com_mokoog/forms/index.html rename to source/packages/com_mokoog/forms/index.html diff --git a/src/packages/com_mokoog/forms/tag.xml b/source/packages/com_mokoog/forms/tag.xml similarity index 100% rename from src/packages/com_mokoog/forms/tag.xml rename to source/packages/com_mokoog/forms/tag.xml diff --git a/src/packages/com_mokoog/index.html b/source/packages/com_mokoog/index.html similarity index 100% rename from src/packages/com_mokoog/index.html rename to source/packages/com_mokoog/index.html diff --git a/src/packages/com_mokoog/language/en-GB/com_mokoog.ini b/source/packages/com_mokoog/language/en-GB/com_mokoog.ini similarity index 100% rename from src/packages/com_mokoog/language/en-GB/com_mokoog.ini rename to source/packages/com_mokoog/language/en-GB/com_mokoog.ini diff --git a/src/packages/com_mokoog/language/en-GB/com_mokoog.sys.ini b/source/packages/com_mokoog/language/en-GB/com_mokoog.sys.ini similarity index 100% rename from src/packages/com_mokoog/language/en-GB/com_mokoog.sys.ini rename to source/packages/com_mokoog/language/en-GB/com_mokoog.sys.ini diff --git a/src/packages/com_mokoog/language/en-GB/index.html b/source/packages/com_mokoog/language/en-GB/index.html similarity index 100% rename from src/packages/com_mokoog/language/en-GB/index.html rename to source/packages/com_mokoog/language/en-GB/index.html diff --git a/src/packages/com_mokoog/language/en-US/com_mokoog.ini b/source/packages/com_mokoog/language/en-US/com_mokoog.ini similarity index 100% rename from src/packages/com_mokoog/language/en-US/com_mokoog.ini rename to source/packages/com_mokoog/language/en-US/com_mokoog.ini diff --git a/src/packages/com_mokoog/language/en-US/com_mokoog.sys.ini b/source/packages/com_mokoog/language/en-US/com_mokoog.sys.ini similarity index 100% rename from src/packages/com_mokoog/language/en-US/com_mokoog.sys.ini rename to source/packages/com_mokoog/language/en-US/com_mokoog.sys.ini diff --git a/src/packages/com_mokoog/language/en-US/index.html b/source/packages/com_mokoog/language/en-US/index.html similarity index 100% rename from src/packages/com_mokoog/language/en-US/index.html rename to source/packages/com_mokoog/language/en-US/index.html diff --git a/src/packages/com_mokoog/language/index.html b/source/packages/com_mokoog/language/index.html similarity index 100% rename from src/packages/com_mokoog/language/index.html rename to source/packages/com_mokoog/language/index.html diff --git a/src/packages/com_mokoog/mokoog.xml b/source/packages/com_mokoog/mokoog.xml similarity index 100% rename from src/packages/com_mokoog/mokoog.xml rename to source/packages/com_mokoog/mokoog.xml diff --git a/src/packages/com_mokoog/script.php b/source/packages/com_mokoog/script.php similarity index 100% rename from src/packages/com_mokoog/script.php rename to source/packages/com_mokoog/script.php diff --git a/src/packages/com_mokoog/services/index.html b/source/packages/com_mokoog/services/index.html similarity index 100% rename from src/packages/com_mokoog/services/index.html rename to source/packages/com_mokoog/services/index.html diff --git a/src/packages/com_mokoog/services/provider.php b/source/packages/com_mokoog/services/provider.php similarity index 100% rename from src/packages/com_mokoog/services/provider.php rename to source/packages/com_mokoog/services/provider.php diff --git a/src/packages/com_mokoog/sql/index.html b/source/packages/com_mokoog/sql/index.html similarity index 100% rename from src/packages/com_mokoog/sql/index.html rename to source/packages/com_mokoog/sql/index.html diff --git a/src/packages/com_mokoog/sql/install.mysql.sql b/source/packages/com_mokoog/sql/install.mysql.sql similarity index 100% rename from src/packages/com_mokoog/sql/install.mysql.sql rename to source/packages/com_mokoog/sql/install.mysql.sql diff --git a/src/packages/com_mokoog/sql/uninstall.mysql.sql b/source/packages/com_mokoog/sql/uninstall.mysql.sql similarity index 100% rename from src/packages/com_mokoog/sql/uninstall.mysql.sql rename to source/packages/com_mokoog/sql/uninstall.mysql.sql diff --git a/src/packages/com_mokoog/sql/updates/index.html b/source/packages/com_mokoog/sql/updates/index.html similarity index 100% rename from src/packages/com_mokoog/sql/updates/index.html rename to source/packages/com_mokoog/sql/updates/index.html diff --git a/src/packages/com_mokoog/sql/updates/mysql/01.00.00.sql b/source/packages/com_mokoog/sql/updates/mysql/01.00.00.sql similarity index 100% rename from src/packages/com_mokoog/sql/updates/mysql/01.00.00.sql rename to source/packages/com_mokoog/sql/updates/mysql/01.00.00.sql diff --git a/src/packages/com_mokoog/sql/updates/mysql/01.01.00.sql b/source/packages/com_mokoog/sql/updates/mysql/01.01.00.sql similarity index 100% rename from src/packages/com_mokoog/sql/updates/mysql/01.01.00.sql rename to source/packages/com_mokoog/sql/updates/mysql/01.01.00.sql diff --git a/src/packages/com_mokoog/sql/updates/mysql/01.02.00.sql b/source/packages/com_mokoog/sql/updates/mysql/01.02.00.sql similarity index 100% rename from src/packages/com_mokoog/sql/updates/mysql/01.02.00.sql rename to source/packages/com_mokoog/sql/updates/mysql/01.02.00.sql diff --git a/src/packages/com_mokoog/sql/updates/mysql/index.html b/source/packages/com_mokoog/sql/updates/mysql/index.html similarity index 100% rename from src/packages/com_mokoog/sql/updates/mysql/index.html rename to source/packages/com_mokoog/sql/updates/mysql/index.html diff --git a/src/packages/com_mokoog/src/ContentType/ContentTypeInterface.php b/source/packages/com_mokoog/src/ContentType/ContentTypeInterface.php similarity index 100% rename from src/packages/com_mokoog/src/ContentType/ContentTypeInterface.php rename to source/packages/com_mokoog/src/ContentType/ContentTypeInterface.php diff --git a/src/packages/com_mokoog/src/ContentType/HikaShopAdapter.php b/source/packages/com_mokoog/src/ContentType/HikaShopAdapter.php similarity index 100% rename from src/packages/com_mokoog/src/ContentType/HikaShopAdapter.php rename to source/packages/com_mokoog/src/ContentType/HikaShopAdapter.php diff --git a/src/packages/com_mokoog/src/ContentType/K2Adapter.php b/source/packages/com_mokoog/src/ContentType/K2Adapter.php similarity index 100% rename from src/packages/com_mokoog/src/ContentType/K2Adapter.php rename to source/packages/com_mokoog/src/ContentType/K2Adapter.php diff --git a/src/packages/com_mokoog/src/ContentType/VirtueMartAdapter.php b/source/packages/com_mokoog/src/ContentType/VirtueMartAdapter.php similarity index 100% rename from src/packages/com_mokoog/src/ContentType/VirtueMartAdapter.php rename to source/packages/com_mokoog/src/ContentType/VirtueMartAdapter.php diff --git a/src/packages/com_mokoog/src/ContentType/index.html b/source/packages/com_mokoog/src/ContentType/index.html similarity index 100% rename from src/packages/com_mokoog/src/ContentType/index.html rename to source/packages/com_mokoog/src/ContentType/index.html diff --git a/src/packages/com_mokoog/src/Controller/BatchController.php b/source/packages/com_mokoog/src/Controller/BatchController.php similarity index 100% rename from src/packages/com_mokoog/src/Controller/BatchController.php rename to source/packages/com_mokoog/src/Controller/BatchController.php diff --git a/src/packages/com_mokoog/src/Controller/DisplayController.php b/source/packages/com_mokoog/src/Controller/DisplayController.php similarity index 100% rename from src/packages/com_mokoog/src/Controller/DisplayController.php rename to source/packages/com_mokoog/src/Controller/DisplayController.php diff --git a/src/packages/com_mokoog/src/Controller/ImportExportController.php b/source/packages/com_mokoog/src/Controller/ImportExportController.php similarity index 100% rename from src/packages/com_mokoog/src/Controller/ImportExportController.php rename to source/packages/com_mokoog/src/Controller/ImportExportController.php diff --git a/src/packages/com_mokoog/src/Controller/index.html b/source/packages/com_mokoog/src/Controller/index.html similarity index 100% rename from src/packages/com_mokoog/src/Controller/index.html rename to source/packages/com_mokoog/src/Controller/index.html diff --git a/src/packages/com_mokoog/src/Extension/MokoOGComponent.php b/source/packages/com_mokoog/src/Extension/MokoOGComponent.php similarity index 100% rename from src/packages/com_mokoog/src/Extension/MokoOGComponent.php rename to source/packages/com_mokoog/src/Extension/MokoOGComponent.php diff --git a/src/packages/com_mokoog/src/Extension/index.html b/source/packages/com_mokoog/src/Extension/index.html similarity index 100% rename from src/packages/com_mokoog/src/Extension/index.html rename to source/packages/com_mokoog/src/Extension/index.html diff --git a/src/packages/com_mokoog/src/Field/index.html b/source/packages/com_mokoog/src/Field/index.html similarity index 100% rename from src/packages/com_mokoog/src/Field/index.html rename to source/packages/com_mokoog/src/Field/index.html diff --git a/src/packages/com_mokoog/src/Model/TagModel.php b/source/packages/com_mokoog/src/Model/TagModel.php similarity index 100% rename from src/packages/com_mokoog/src/Model/TagModel.php rename to source/packages/com_mokoog/src/Model/TagModel.php diff --git a/src/packages/com_mokoog/src/Model/TagsModel.php b/source/packages/com_mokoog/src/Model/TagsModel.php similarity index 100% rename from src/packages/com_mokoog/src/Model/TagsModel.php rename to source/packages/com_mokoog/src/Model/TagsModel.php diff --git a/src/packages/com_mokoog/src/Model/index.html b/source/packages/com_mokoog/src/Model/index.html similarity index 100% rename from src/packages/com_mokoog/src/Model/index.html rename to source/packages/com_mokoog/src/Model/index.html diff --git a/src/packages/com_mokoog/src/Service/index.html b/source/packages/com_mokoog/src/Service/index.html similarity index 100% rename from src/packages/com_mokoog/src/Service/index.html rename to source/packages/com_mokoog/src/Service/index.html diff --git a/src/packages/com_mokoog/src/Table/TagTable.php b/source/packages/com_mokoog/src/Table/TagTable.php similarity index 100% rename from src/packages/com_mokoog/src/Table/TagTable.php rename to source/packages/com_mokoog/src/Table/TagTable.php diff --git a/src/packages/com_mokoog/src/Table/index.html b/source/packages/com_mokoog/src/Table/index.html similarity index 100% rename from src/packages/com_mokoog/src/Table/index.html rename to source/packages/com_mokoog/src/Table/index.html diff --git a/src/packages/com_mokoog/src/View/Tags/HtmlView.php b/source/packages/com_mokoog/src/View/Tags/HtmlView.php similarity index 100% rename from src/packages/com_mokoog/src/View/Tags/HtmlView.php rename to source/packages/com_mokoog/src/View/Tags/HtmlView.php diff --git a/src/packages/com_mokoog/src/View/Tags/index.html b/source/packages/com_mokoog/src/View/Tags/index.html similarity index 100% rename from src/packages/com_mokoog/src/View/Tags/index.html rename to source/packages/com_mokoog/src/View/Tags/index.html diff --git a/src/packages/com_mokoog/src/View/Tags/tmpl/index.html b/source/packages/com_mokoog/src/View/Tags/tmpl/index.html similarity index 100% rename from src/packages/com_mokoog/src/View/Tags/tmpl/index.html rename to source/packages/com_mokoog/src/View/Tags/tmpl/index.html diff --git a/src/packages/com_mokoog/src/View/index.html b/source/packages/com_mokoog/src/View/index.html similarity index 100% rename from src/packages/com_mokoog/src/View/index.html rename to source/packages/com_mokoog/src/View/index.html diff --git a/src/packages/com_mokoog/src/index.html b/source/packages/com_mokoog/src/index.html similarity index 100% rename from src/packages/com_mokoog/src/index.html rename to source/packages/com_mokoog/src/index.html diff --git a/src/packages/com_mokoog/tmpl/index.html b/source/packages/com_mokoog/tmpl/index.html similarity index 100% rename from src/packages/com_mokoog/tmpl/index.html rename to source/packages/com_mokoog/tmpl/index.html diff --git a/src/packages/com_mokoog/tmpl/tags/default.php b/source/packages/com_mokoog/tmpl/tags/default.php similarity index 100% rename from src/packages/com_mokoog/tmpl/tags/default.php rename to source/packages/com_mokoog/tmpl/tags/default.php diff --git a/src/packages/com_mokoog/tmpl/tags/index.html b/source/packages/com_mokoog/tmpl/tags/index.html similarity index 100% rename from src/packages/com_mokoog/tmpl/tags/index.html rename to source/packages/com_mokoog/tmpl/tags/index.html diff --git a/src/packages/index.html b/source/packages/index.html similarity index 100% rename from src/packages/index.html rename to source/packages/index.html diff --git a/src/packages/plg_content_mokoog/forms/index.html b/source/packages/plg_content_mokoog/forms/index.html similarity index 100% rename from src/packages/plg_content_mokoog/forms/index.html rename to source/packages/plg_content_mokoog/forms/index.html diff --git a/src/packages/plg_content_mokoog/forms/mokoog.xml b/source/packages/plg_content_mokoog/forms/mokoog.xml similarity index 100% rename from src/packages/plg_content_mokoog/forms/mokoog.xml rename to source/packages/plg_content_mokoog/forms/mokoog.xml diff --git a/src/packages/plg_content_mokoog/index.html b/source/packages/plg_content_mokoog/index.html similarity index 100% rename from src/packages/plg_content_mokoog/index.html rename to source/packages/plg_content_mokoog/index.html diff --git a/src/packages/plg_content_mokoog/language/en-GB/index.html b/source/packages/plg_content_mokoog/language/en-GB/index.html similarity index 100% rename from src/packages/plg_content_mokoog/language/en-GB/index.html rename to source/packages/plg_content_mokoog/language/en-GB/index.html diff --git a/src/packages/plg_content_mokoog/language/en-GB/plg_content_mokoog.ini b/source/packages/plg_content_mokoog/language/en-GB/plg_content_mokoog.ini similarity index 100% rename from src/packages/plg_content_mokoog/language/en-GB/plg_content_mokoog.ini rename to source/packages/plg_content_mokoog/language/en-GB/plg_content_mokoog.ini diff --git a/src/packages/plg_content_mokoog/language/en-GB/plg_content_mokoog.sys.ini b/source/packages/plg_content_mokoog/language/en-GB/plg_content_mokoog.sys.ini similarity index 100% rename from src/packages/plg_content_mokoog/language/en-GB/plg_content_mokoog.sys.ini rename to source/packages/plg_content_mokoog/language/en-GB/plg_content_mokoog.sys.ini diff --git a/src/packages/plg_content_mokoog/language/en-US/index.html b/source/packages/plg_content_mokoog/language/en-US/index.html similarity index 100% rename from src/packages/plg_content_mokoog/language/en-US/index.html rename to source/packages/plg_content_mokoog/language/en-US/index.html diff --git a/src/packages/plg_content_mokoog/language/en-US/plg_content_mokoog.ini b/source/packages/plg_content_mokoog/language/en-US/plg_content_mokoog.ini similarity index 100% rename from src/packages/plg_content_mokoog/language/en-US/plg_content_mokoog.ini rename to source/packages/plg_content_mokoog/language/en-US/plg_content_mokoog.ini diff --git a/src/packages/plg_content_mokoog/language/en-US/plg_content_mokoog.sys.ini b/source/packages/plg_content_mokoog/language/en-US/plg_content_mokoog.sys.ini similarity index 100% rename from src/packages/plg_content_mokoog/language/en-US/plg_content_mokoog.sys.ini rename to source/packages/plg_content_mokoog/language/en-US/plg_content_mokoog.sys.ini diff --git a/src/packages/plg_content_mokoog/language/index.html b/source/packages/plg_content_mokoog/language/index.html similarity index 100% rename from src/packages/plg_content_mokoog/language/index.html rename to source/packages/plg_content_mokoog/language/index.html diff --git a/src/packages/plg_content_mokoog/media/css/index.html b/source/packages/plg_content_mokoog/media/css/index.html similarity index 100% rename from src/packages/plg_content_mokoog/media/css/index.html rename to source/packages/plg_content_mokoog/media/css/index.html diff --git a/src/packages/plg_content_mokoog/media/css/preview.css b/source/packages/plg_content_mokoog/media/css/preview.css similarity index 100% rename from src/packages/plg_content_mokoog/media/css/preview.css rename to source/packages/plg_content_mokoog/media/css/preview.css diff --git a/src/packages/plg_content_mokoog/media/index.html b/source/packages/plg_content_mokoog/media/index.html similarity index 100% rename from src/packages/plg_content_mokoog/media/index.html rename to source/packages/plg_content_mokoog/media/index.html diff --git a/src/packages/plg_content_mokoog/media/joomla.asset.json b/source/packages/plg_content_mokoog/media/joomla.asset.json similarity index 100% rename from src/packages/plg_content_mokoog/media/joomla.asset.json rename to source/packages/plg_content_mokoog/media/joomla.asset.json diff --git a/src/packages/plg_content_mokoog/media/js/index.html b/source/packages/plg_content_mokoog/media/js/index.html similarity index 100% rename from src/packages/plg_content_mokoog/media/js/index.html rename to source/packages/plg_content_mokoog/media/js/index.html diff --git a/src/packages/plg_content_mokoog/media/js/preview.js b/source/packages/plg_content_mokoog/media/js/preview.js similarity index 100% rename from src/packages/plg_content_mokoog/media/js/preview.js rename to source/packages/plg_content_mokoog/media/js/preview.js diff --git a/src/packages/plg_content_mokoog/mokoog.php b/source/packages/plg_content_mokoog/mokoog.php similarity index 100% rename from src/packages/plg_content_mokoog/mokoog.php rename to source/packages/plg_content_mokoog/mokoog.php diff --git a/src/packages/plg_content_mokoog/mokoog.xml b/source/packages/plg_content_mokoog/mokoog.xml similarity index 100% rename from src/packages/plg_content_mokoog/mokoog.xml rename to source/packages/plg_content_mokoog/mokoog.xml diff --git a/src/packages/plg_content_mokoog/services/index.html b/source/packages/plg_content_mokoog/services/index.html similarity index 100% rename from src/packages/plg_content_mokoog/services/index.html rename to source/packages/plg_content_mokoog/services/index.html diff --git a/src/packages/plg_content_mokoog/services/provider.php b/source/packages/plg_content_mokoog/services/provider.php similarity index 100% rename from src/packages/plg_content_mokoog/services/provider.php rename to source/packages/plg_content_mokoog/services/provider.php diff --git a/src/packages/plg_content_mokoog/src/Extension/MokoOGContent.php b/source/packages/plg_content_mokoog/src/Extension/MokoOGContent.php similarity index 100% rename from src/packages/plg_content_mokoog/src/Extension/MokoOGContent.php rename to source/packages/plg_content_mokoog/src/Extension/MokoOGContent.php diff --git a/src/packages/plg_content_mokoog/src/Extension/index.html b/source/packages/plg_content_mokoog/src/Extension/index.html similarity index 100% rename from src/packages/plg_content_mokoog/src/Extension/index.html rename to source/packages/plg_content_mokoog/src/Extension/index.html diff --git a/src/packages/plg_content_mokoog/src/Field/index.html b/source/packages/plg_content_mokoog/src/Field/index.html similarity index 100% rename from src/packages/plg_content_mokoog/src/Field/index.html rename to source/packages/plg_content_mokoog/src/Field/index.html diff --git a/src/packages/plg_content_mokoog/src/index.html b/source/packages/plg_content_mokoog/src/index.html similarity index 100% rename from src/packages/plg_content_mokoog/src/index.html rename to source/packages/plg_content_mokoog/src/index.html diff --git a/src/packages/plg_system_mokoog/index.html b/source/packages/plg_system_mokoog/index.html similarity index 100% rename from src/packages/plg_system_mokoog/index.html rename to source/packages/plg_system_mokoog/index.html diff --git a/src/packages/plg_system_mokoog/language/en-GB/index.html b/source/packages/plg_system_mokoog/language/en-GB/index.html similarity index 100% rename from src/packages/plg_system_mokoog/language/en-GB/index.html rename to source/packages/plg_system_mokoog/language/en-GB/index.html diff --git a/src/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.ini b/source/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.ini similarity index 100% rename from src/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.ini rename to source/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.ini diff --git a/src/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.sys.ini b/source/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.sys.ini similarity index 100% rename from src/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.sys.ini rename to source/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.sys.ini diff --git a/src/packages/plg_system_mokoog/language/en-US/index.html b/source/packages/plg_system_mokoog/language/en-US/index.html similarity index 100% rename from src/packages/plg_system_mokoog/language/en-US/index.html rename to source/packages/plg_system_mokoog/language/en-US/index.html diff --git a/src/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.ini b/source/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.ini similarity index 100% rename from src/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.ini rename to source/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.ini diff --git a/src/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.sys.ini b/source/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.sys.ini similarity index 100% rename from src/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.sys.ini rename to source/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.sys.ini diff --git a/src/packages/plg_system_mokoog/language/index.html b/source/packages/plg_system_mokoog/language/index.html similarity index 100% rename from src/packages/plg_system_mokoog/language/index.html rename to source/packages/plg_system_mokoog/language/index.html diff --git a/src/packages/plg_system_mokoog/mokoog.php b/source/packages/plg_system_mokoog/mokoog.php similarity index 100% rename from src/packages/plg_system_mokoog/mokoog.php rename to source/packages/plg_system_mokoog/mokoog.php diff --git a/src/packages/plg_system_mokoog/mokoog.xml b/source/packages/plg_system_mokoog/mokoog.xml similarity index 100% rename from src/packages/plg_system_mokoog/mokoog.xml rename to source/packages/plg_system_mokoog/mokoog.xml diff --git a/src/packages/plg_system_mokoog/services/index.html b/source/packages/plg_system_mokoog/services/index.html similarity index 100% rename from src/packages/plg_system_mokoog/services/index.html rename to source/packages/plg_system_mokoog/services/index.html diff --git a/src/packages/plg_system_mokoog/services/provider.php b/source/packages/plg_system_mokoog/services/provider.php similarity index 100% rename from src/packages/plg_system_mokoog/services/provider.php rename to source/packages/plg_system_mokoog/services/provider.php diff --git a/src/packages/plg_system_mokoog/src/Extension/MokoOG.php b/source/packages/plg_system_mokoog/src/Extension/MokoOG.php similarity index 100% rename from src/packages/plg_system_mokoog/src/Extension/MokoOG.php rename to source/packages/plg_system_mokoog/src/Extension/MokoOG.php diff --git a/src/packages/plg_system_mokoog/src/Extension/index.html b/source/packages/plg_system_mokoog/src/Extension/index.html similarity index 100% rename from src/packages/plg_system_mokoog/src/Extension/index.html rename to source/packages/plg_system_mokoog/src/Extension/index.html diff --git a/src/packages/plg_system_mokoog/src/Helper/ImageGenerator.php b/source/packages/plg_system_mokoog/src/Helper/ImageGenerator.php similarity index 100% rename from src/packages/plg_system_mokoog/src/Helper/ImageGenerator.php rename to source/packages/plg_system_mokoog/src/Helper/ImageGenerator.php diff --git a/src/packages/plg_system_mokoog/src/Helper/ImageHelper.php b/source/packages/plg_system_mokoog/src/Helper/ImageHelper.php similarity index 100% rename from src/packages/plg_system_mokoog/src/Helper/ImageHelper.php rename to source/packages/plg_system_mokoog/src/Helper/ImageHelper.php diff --git a/src/packages/plg_system_mokoog/src/Helper/JsonLdBuilder.php b/source/packages/plg_system_mokoog/src/Helper/JsonLdBuilder.php similarity index 100% rename from src/packages/plg_system_mokoog/src/Helper/JsonLdBuilder.php rename to source/packages/plg_system_mokoog/src/Helper/JsonLdBuilder.php diff --git a/src/packages/plg_system_mokoog/src/Helper/index.html b/source/packages/plg_system_mokoog/src/Helper/index.html similarity index 100% rename from src/packages/plg_system_mokoog/src/Helper/index.html rename to source/packages/plg_system_mokoog/src/Helper/index.html diff --git a/src/packages/plg_system_mokoog/src/index.html b/source/packages/plg_system_mokoog/src/index.html similarity index 100% rename from src/packages/plg_system_mokoog/src/index.html rename to source/packages/plg_system_mokoog/src/index.html diff --git a/src/packages/plg_webservices_mokoog/index.html b/source/packages/plg_webservices_mokoog/index.html similarity index 100% rename from src/packages/plg_webservices_mokoog/index.html rename to source/packages/plg_webservices_mokoog/index.html diff --git a/src/packages/plg_webservices_mokoog/language/en-GB/index.html b/source/packages/plg_webservices_mokoog/language/en-GB/index.html similarity index 100% rename from src/packages/plg_webservices_mokoog/language/en-GB/index.html rename to source/packages/plg_webservices_mokoog/language/en-GB/index.html diff --git a/src/packages/plg_webservices_mokoog/language/en-GB/plg_webservices_mokoog.ini b/source/packages/plg_webservices_mokoog/language/en-GB/plg_webservices_mokoog.ini similarity index 100% rename from src/packages/plg_webservices_mokoog/language/en-GB/plg_webservices_mokoog.ini rename to source/packages/plg_webservices_mokoog/language/en-GB/plg_webservices_mokoog.ini diff --git a/src/packages/plg_webservices_mokoog/language/en-GB/plg_webservices_mokoog.sys.ini b/source/packages/plg_webservices_mokoog/language/en-GB/plg_webservices_mokoog.sys.ini similarity index 100% rename from src/packages/plg_webservices_mokoog/language/en-GB/plg_webservices_mokoog.sys.ini rename to source/packages/plg_webservices_mokoog/language/en-GB/plg_webservices_mokoog.sys.ini diff --git a/src/packages/plg_webservices_mokoog/language/en-US/index.html b/source/packages/plg_webservices_mokoog/language/en-US/index.html similarity index 100% rename from src/packages/plg_webservices_mokoog/language/en-US/index.html rename to source/packages/plg_webservices_mokoog/language/en-US/index.html diff --git a/src/packages/plg_webservices_mokoog/language/en-US/plg_webservices_mokoog.ini b/source/packages/plg_webservices_mokoog/language/en-US/plg_webservices_mokoog.ini similarity index 100% rename from src/packages/plg_webservices_mokoog/language/en-US/plg_webservices_mokoog.ini rename to source/packages/plg_webservices_mokoog/language/en-US/plg_webservices_mokoog.ini diff --git a/src/packages/plg_webservices_mokoog/language/en-US/plg_webservices_mokoog.sys.ini b/source/packages/plg_webservices_mokoog/language/en-US/plg_webservices_mokoog.sys.ini similarity index 100% rename from src/packages/plg_webservices_mokoog/language/en-US/plg_webservices_mokoog.sys.ini rename to source/packages/plg_webservices_mokoog/language/en-US/plg_webservices_mokoog.sys.ini diff --git a/src/packages/plg_webservices_mokoog/language/index.html b/source/packages/plg_webservices_mokoog/language/index.html similarity index 100% rename from src/packages/plg_webservices_mokoog/language/index.html rename to source/packages/plg_webservices_mokoog/language/index.html diff --git a/src/packages/plg_webservices_mokoog/mokoog.php b/source/packages/plg_webservices_mokoog/mokoog.php similarity index 100% rename from src/packages/plg_webservices_mokoog/mokoog.php rename to source/packages/plg_webservices_mokoog/mokoog.php diff --git a/src/packages/plg_webservices_mokoog/mokoog.xml b/source/packages/plg_webservices_mokoog/mokoog.xml similarity index 100% rename from src/packages/plg_webservices_mokoog/mokoog.xml rename to source/packages/plg_webservices_mokoog/mokoog.xml diff --git a/src/packages/plg_webservices_mokoog/services/index.html b/source/packages/plg_webservices_mokoog/services/index.html similarity index 100% rename from src/packages/plg_webservices_mokoog/services/index.html rename to source/packages/plg_webservices_mokoog/services/index.html diff --git a/src/packages/plg_webservices_mokoog/services/provider.php b/source/packages/plg_webservices_mokoog/services/provider.php similarity index 100% rename from src/packages/plg_webservices_mokoog/services/provider.php rename to source/packages/plg_webservices_mokoog/services/provider.php diff --git a/src/packages/plg_webservices_mokoog/src/Extension/MokoOGWebServices.php b/source/packages/plg_webservices_mokoog/src/Extension/MokoOGWebServices.php similarity index 100% rename from src/packages/plg_webservices_mokoog/src/Extension/MokoOGWebServices.php rename to source/packages/plg_webservices_mokoog/src/Extension/MokoOGWebServices.php diff --git a/src/packages/plg_webservices_mokoog/src/Extension/index.html b/source/packages/plg_webservices_mokoog/src/Extension/index.html similarity index 100% rename from src/packages/plg_webservices_mokoog/src/Extension/index.html rename to source/packages/plg_webservices_mokoog/src/Extension/index.html diff --git a/src/packages/plg_webservices_mokoog/src/index.html b/source/packages/plg_webservices_mokoog/src/index.html similarity index 100% rename from src/packages/plg_webservices_mokoog/src/index.html rename to source/packages/plg_webservices_mokoog/src/index.html diff --git a/src/pkg_mokoog.xml b/source/pkg_mokoog.xml similarity index 100% rename from src/pkg_mokoog.xml rename to source/pkg_mokoog.xml diff --git a/src/script.php b/source/script.php similarity index 100% rename from src/script.php rename to source/script.php From 493fcb6dd5f8ecb01d6af60acbd67af516824aac Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sat, 6 Jun 2026 08:24:21 -0500 Subject: [PATCH 105/132] chore: normalize line endings in auto-release.yml Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- .mokogitea/workflows/auto-release.yml | 570 +++++++++++++------------- 1 file changed, 285 insertions(+), 285 deletions(-) diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml index 44a2d64..141fdcc 100644 --- a/.mokogitea/workflows/auto-release.yml +++ b/.mokogitea/workflows/auto-release.yml @@ -1,285 +1,285 @@ -# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: moko-platform.Release -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform -# PATH: /templates/workflows/universal/auto-release.yml.template -# VERSION: 05.00.00 -# BRIEF: Universal build & release � detects platform from manifest.xml -# -# +========================================================================+ -# | UNIVERSAL BUILD & RELEASE PIPELINE | -# +========================================================================+ -# | | -# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. | -# | | -# | Platform-specific: | -# | joomla: XML manifest, updates.xml, type-prefixed packages | -# | dolibarr: mod*.class.php, update.txt, dev version reset | -# | generic: README-only, no update stream | -# | | -# +========================================================================+ - -name: "Universal: Build & Release" - -on: - pull_request: - types: [opened, closed] - branches: - - main - workflow_dispatch: - inputs: - action: - description: 'Action to perform' - required: false - type: choice - default: release - options: - - release - - promote-rc - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} - GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} - -permissions: - contents: write - -jobs: - # ── PR Opened → Rename branch to RC and build RC release ───────────────────── - promote-rc: - name: Promote to RC - runs-on: release - if: >- - (github.event.action == 'opened' && github.event.pull_request.merged != true) || - (github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc') - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - token: ${{ secrets.MOKOGITEA_TOKEN }} - fetch-depth: 1 - - - name: Setup moko-platform tools - env: - MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting - run: | - if ! command -v composer &> /dev/null; then - sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 - fi - # Always fetch latest CLI tools — never use stale cache from previous runs - rm -rf /tmp/moko-platform-api - git clone --depth 1 --branch main --quiet \ - "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ - /tmp/moko-platform-api - cd /tmp/moko-platform-api - composer install --no-dev --no-interaction --quiet - - - name: Rename branch to rc - run: | - php /tmp/moko-platform-api/cli/branch_rename.php \ - --from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" \ - --api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \ - --pr "${{ github.event.pull_request.number }}" - - - name: Checkout rc and configure git - run: | - git fetch origin rc - git checkout rc - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" - - - name: Publish RC release - run: | - php /tmp/moko-platform-api/cli/release_publish.php \ - --path . --stability rc --bump minor --branch rc \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" \ - --skip-update-stream - - - name: Summary - if: always() - run: | - echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY - echo "Branch renamed to rc, minor bump, RC release built (updates.xml managed by Gitea Pages)" >> $GITHUB_STEP_SUMMARY - - # ── Merged PR → Build & Release (or promote RC to stable) ──────────────────── - release: - name: Build & Release Pipeline - runs-on: release - if: >- - github.event.pull_request.merged == true || - (github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc') - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - token: ${{ secrets.MOKOGITEA_TOKEN }} - fetch-depth: 0 - - - name: Configure git for bot pushes - run: | - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" - - - name: Check for merge conflict markers - run: | - CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true) - if [ -n "$CONFLICTS" ]; then - echo "::error::Merge conflict markers found — aborting release" - echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - exit 1 - fi - echo "No conflict markers found" - - - name: Setup moko-platform tools - env: - MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}' - run: | - # Ensure PHP + Composer are available - if ! command -v composer &> /dev/null; then - sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 - fi - # Always fetch latest CLI tools — never use stale cache from previous runs - rm -rf /tmp/moko-platform-api - git clone --depth 1 --branch main --quiet \ - "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ - /tmp/moko-platform-api - cd /tmp/moko-platform-api - composer install --no-dev --no-interaction --quiet - - - - name: "Publish stable release" - run: | - php /tmp/moko-platform-api/cli/release_publish.php \ - --path . --stability stable --bump minor --branch main \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" \ - --skip-update-stream - - # -- STEP 9: Mirror to GitHub (stable only) -------------------------------- - - name: "Step 9: Mirror release to GitHub" - if: >- - steps.version.outputs.skip != 'true' && - secrets.GH_MIRROR_TOKEN != '' - continue-on-error: true - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - RELEASE_TAG="${{ steps.version.outputs.release_tag }}" - GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php /tmp/moko-platform-api/cli/release_mirror.php \ - --version "$VERSION" --tag "$RELEASE_TAG" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ - --gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \ - --branch main 2>&1 || true - echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY - - # -- STEP 10: Sync main branch to GitHub mirror ---------------------------- - - name: "Step 10: Push main to GitHub mirror" - if: >- - steps.version.outputs.skip != 'true' && - secrets.GH_MIRROR_TOKEN != '' - continue-on-error: true - run: | - GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" - GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1) - GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2) - git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \ - git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" - git fetch origin main --depth=1 - git push github origin/main:refs/heads/main --force 2>/dev/null \ - && echo "main branch pushed to GitHub mirror" \ - || echo "WARNING: GitHub mirror push failed" - - - name: "Step 11: Delete rc branch and recreate dev from main" - if: steps.version.outputs.skip != 'true' - continue-on-error: true - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" - - # Delete rc branch (ephemeral — created by promote-rc) - curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ - "${API_BASE}/branches/rc" 2>/dev/null \ - && echo "Deleted rc branch" || echo "rc branch not found" - - # Delete dev branch - curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ - "${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch" - - # Recreate dev from main (now includes version bump + changelog promotion) - curl -sf -X POST -H "Authorization: token ${TOKEN}" \ - -H "Content-Type: application/json" \ - "${API_BASE}/branches" \ - -d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main" - - echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY - - - name: "Step 12: Create version branch from main" - if: steps.version.outputs.skip != 'true' - continue-on-error: true - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - BRANCH_NAME="version/${VERSION}" - MAIN_SHA=$(git rev-parse HEAD) - - # Delete old version branch if it exists (same version re-release) - curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}" - - # Create version/XX.YY.ZZ from main - curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed" - - echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY - - - - # -- Dolibarr post-release: Reset dev version ----------------------------- - - name: "Post-release: Reset dev version" - if: steps.version.outputs.skip != 'true' - continue-on-error: true - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php /tmp/moko-platform-api/cli/version_reset_dev.php \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \ - --branch dev --path . 2>&1 || true - - # -- Summary -------------------------------------------------------------- - - name: Pipeline Summary - if: always() - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - PLATFORM="${{ steps.platform.outputs.platform }}" - if [ "${{ steps.version.outputs.skip }}" = "true" ]; then - echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY - echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY - elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then - echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY - else - echo "" >> $GITHUB_STEP_SUMMARY - echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY - echo "|------|--------|" >> $GITHUB_STEP_SUMMARY - echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY - fi +# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: moko-platform.Release +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform +# PATH: /templates/workflows/universal/auto-release.yml.template +# VERSION: 05.00.00 +# BRIEF: Universal build & release � detects platform from manifest.xml +# +# +========================================================================+ +# | UNIVERSAL BUILD & RELEASE PIPELINE | +# +========================================================================+ +# | | +# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. | +# | | +# | Platform-specific: | +# | joomla: XML manifest, updates.xml, type-prefixed packages | +# | dolibarr: mod*.class.php, update.txt, dev version reset | +# | generic: README-only, no update stream | +# | | +# +========================================================================+ + +name: "Universal: Build & Release" + +on: + pull_request: + types: [opened, closed] + branches: + - main + workflow_dispatch: + inputs: + action: + description: 'Action to perform' + required: false + type: choice + default: release + options: + - release + - promote-rc + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + +permissions: + contents: write + +jobs: + # ── PR Opened → Rename branch to RC and build RC release ───────────────────── + promote-rc: + name: Promote to RC + runs-on: release + if: >- + (github.event.action == 'opened' && github.event.pull_request.merged != true) || + (github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc') + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.MOKOGITEA_TOKEN }} + fetch-depth: 1 + + - name: Setup moko-platform tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + run: | + if ! command -v composer &> /dev/null; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + # Always fetch latest CLI tools — never use stale cache from previous runs + rm -rf /tmp/moko-platform-api + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ + /tmp/moko-platform-api + cd /tmp/moko-platform-api + composer install --no-dev --no-interaction --quiet + + - name: Rename branch to rc + run: | + php /tmp/moko-platform-api/cli/branch_rename.php \ + --from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" \ + --api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \ + --pr "${{ github.event.pull_request.number }}" + + - name: Checkout rc and configure git + run: | + git fetch origin rc + git checkout rc + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + + - name: Publish RC release + run: | + php /tmp/moko-platform-api/cli/release_publish.php \ + --path . --stability rc --bump minor --branch rc \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" \ + --skip-update-stream + + - name: Summary + if: always() + run: | + echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY + echo "Branch renamed to rc, minor bump, RC release built (updates.xml managed by Gitea Pages)" >> $GITHUB_STEP_SUMMARY + + # ── Merged PR → Build & Release (or promote RC to stable) ──────────────────── + release: + name: Build & Release Pipeline + runs-on: release + if: >- + github.event.pull_request.merged == true || + (github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc') + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.MOKOGITEA_TOKEN }} + fetch-depth: 0 + + - name: Configure git for bot pushes + run: | + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + + - name: Check for merge conflict markers + run: | + CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true) + if [ -n "$CONFLICTS" ]; then + echo "::error::Merge conflict markers found — aborting release" + echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + exit 1 + fi + echo "No conflict markers found" + + - name: Setup moko-platform tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}' + run: | + # Ensure PHP + Composer are available + if ! command -v composer &> /dev/null; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + # Always fetch latest CLI tools — never use stale cache from previous runs + rm -rf /tmp/moko-platform-api + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ + /tmp/moko-platform-api + cd /tmp/moko-platform-api + composer install --no-dev --no-interaction --quiet + + + - name: "Publish stable release" + run: | + php /tmp/moko-platform-api/cli/release_publish.php \ + --path . --stability stable --bump minor --branch main \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" \ + --skip-update-stream + + # -- STEP 9: Mirror to GitHub (stable only) -------------------------------- + - name: "Step 9: Mirror release to GitHub" + if: >- + steps.version.outputs.skip != 'true' && + secrets.GH_MIRROR_TOKEN != '' + continue-on-error: true + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php /tmp/moko-platform-api/cli/release_mirror.php \ + --version "$VERSION" --tag "$RELEASE_TAG" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ + --gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \ + --branch main 2>&1 || true + echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY + + # -- STEP 10: Sync main branch to GitHub mirror ---------------------------- + - name: "Step 10: Push main to GitHub mirror" + if: >- + steps.version.outputs.skip != 'true' && + secrets.GH_MIRROR_TOKEN != '' + continue-on-error: true + run: | + GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" + GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1) + GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2) + git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \ + git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" + git fetch origin main --depth=1 + git push github origin/main:refs/heads/main --force 2>/dev/null \ + && echo "main branch pushed to GitHub mirror" \ + || echo "WARNING: GitHub mirror push failed" + + - name: "Step 11: Delete rc branch and recreate dev from main" + if: steps.version.outputs.skip != 'true' + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" + + # Delete rc branch (ephemeral — created by promote-rc) + curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/branches/rc" 2>/dev/null \ + && echo "Deleted rc branch" || echo "rc branch not found" + + # Delete dev branch + curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch" + + # Recreate dev from main (now includes version bump + changelog promotion) + curl -sf -X POST -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/json" \ + "${API_BASE}/branches" \ + -d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main" + + echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY + + - name: "Step 12: Create version branch from main" + if: steps.version.outputs.skip != 'true' + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + BRANCH_NAME="version/${VERSION}" + MAIN_SHA=$(git rev-parse HEAD) + + # Delete old version branch if it exists (same version re-release) + curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}" + + # Create version/XX.YY.ZZ from main + curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed" + + echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY + + + + # -- Dolibarr post-release: Reset dev version ----------------------------- + - name: "Post-release: Reset dev version" + if: steps.version.outputs.skip != 'true' + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php /tmp/moko-platform-api/cli/version_reset_dev.php \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \ + --branch dev --path . 2>&1 || true + + # -- Summary -------------------------------------------------------------- + - name: Pipeline Summary + if: always() + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + PLATFORM="${{ steps.platform.outputs.platform }}" + if [ "${{ steps.version.outputs.skip }}" = "true" ]; then + echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY + echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY + elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then + echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY + echo "|------|--------|" >> $GITHUB_STEP_SUMMARY + echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY + fi From 426af7a879675bf5fd30c280c4db0f4f91dec260 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sat, 6 Jun 2026 09:30:53 -0500 Subject: [PATCH 106/132] chore: move CLAUDE.md to .mokogitea/ directory Relocate CLAUDE.md from repo root to .mokogitea/ per project convention. Content updated with focused, repo-specific architecture and rules. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- .mokogitea/CLAUDE.md | 67 +++++++++++++++++++++++++++++++++++++ CLAUDE.md | 78 -------------------------------------------- 2 files changed, 67 insertions(+), 78 deletions(-) create mode 100644 .mokogitea/CLAUDE.md delete mode 100644 CLAUDE.md diff --git a/.mokogitea/CLAUDE.md b/.mokogitea/CLAUDE.md new file mode 100644 index 0000000..8aefbbf --- /dev/null +++ b/.mokogitea/CLAUDE.md @@ -0,0 +1,67 @@ +# MokoJoomOpenGraph + +Open Graph, Twitter Card, and social sharing meta tag management for Joomla. Per-article SEO with auto-generation fallback. + +## Quick Reference + +| Field | Value | +|---|---| +| **Package** | `pkg_mokoog` | +| **Language** | PHP 8.1+ | +| **Branch** | develop on `dev`, merge to `main` (protected) | +| **Wiki** | [MokoJoomOpenGraph Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/wiki) | + +## Commands + +```bash +make build # Build package ZIP +make lint # Run linters +make validate # Validate structure +make release # Full release pipeline +make clean # Clean build artifacts +composer install # Install PHP dependencies +``` + +## Architecture + +Joomla **package** with three sub-extensions: + +### com_mokoog (Component) +- Admin backend for viewing/managing all OG tag records +- Joomla 4/5 MVC: `Controller/DisplayController`, `Model/TagsModel`, `View/Tags/HtmlView`, `Table/TagTable` +- Namespace: `Joomla\Component\MokoOG\Administrator` + +### plg_system_mokoog (System Plugin) +- Hooks `onBeforeCompileHead` to inject `<meta property="og:*">` and `<meta name="twitter:*">` +- Auto-generates tags from article title, description, images when no custom tags exist +- Supports articles (`com_content`), menu items, extensible content types + +### plg_content_mokoog (Content Plugin) +- Hooks `onContentPrepareForm` to add OG fields tab to article/menu editors +- Hooks `onContentAfterSave`/`onContentAfterDelete` to persist/clean OG data + +### Database Schema + +Single table `#__mokoog_tags`: +- `content_type` + `content_id` = unique key for any content item +- `og_title`, `og_description`, `og_image`, `og_type` = custom OG overrides +- `published` flag for per-item enable/disable + +## Rules + +- **Never commit** `.claude/`, `.mcp.json`, `TODO.md`, `*.min.css`/`*.min.js` +- **Attribution**: `Authored-by: Moko Consulting` +- **Workflow directory**: `.mokogitea/` (not `.gitea/` or `.github/`) +- **Minification**: handled at build time (CI) +- **Wiki**: documentation lives in the Gitea wiki, not `docs/` files +- **Standards**: [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home) + +## Coding Standards + +- PHP 8.1+ minimum +- Joomla 4/5 DI container pattern: `services/provider.php` → Extension class +- Legacy stub `.php` file required for plugin loader but empty +- `SubscriberInterface` for event subscription (not `on*` method naming) +- `bind() → check() → store()` for Table operations (not `save()`) +- Language file placement: site (no `folder`) vs admin (`folder="administrator"`) +- SPDX license headers on all PHP files diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index cdaa011..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,78 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code when working with this repository. - -## Project Overview - -**MokoJoomOpenGraph** -- Open Graph, Twitter Card, and social sharing meta tag management for Joomla - -| Field | Value | -|---|---| -| **Platform** | joomla | -| **Language** | PHP | -| **Default branch** | main | -| **License** | GPL-3.0-or-later | -| **Wiki** | [MokoJoomOpenGraph Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/wiki) | -| **Standards** | [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home) | - -## Common Commands - -```bash -make build # Build the project -make lint # Run linters -make validate # Validate structure -make release # Full release pipeline -make minify # Minify CSS/JS assets -make clean # Clean build artifacts -``` - -```bash -composer install # Install PHP dependencies -``` - -## Architecture - -This is a Joomla **package** extension (`pkg_mokoog`) containing three sub-extensions: - -### com_mokoog (Component) -- Admin backend for viewing and managing all OG tag records -- Joomla 4/5 MVC: `Controller/DisplayController`, `Model/TagsModel`, `View/Tags/HtmlView`, `Table/TagTable` -- Namespace: `Joomla\Component\MokoOG\Administrator` -- Database table: `#__mokoog_tags` — stores custom OG data per content item - -### plg_system_mokoog (System Plugin) -- Hooks `onBeforeCompileHead` to inject `<meta property="og:*">` and `<meta name="twitter:*">` tags -- Auto-generates tags from article title, description, and images when no custom tags exist -- Supports articles (`com_content`), menu items, and extensible content types -- Namespace: `Joomla\Plugin\System\MokoOG` - -### plg_content_mokoog (Content Plugin) -- Hooks `onContentPrepareForm` to add OG fields tab to article and menu item editors -- Hooks `onContentAfterSave` / `onContentAfterDelete` to persist/clean OG data -- Namespace: `Joomla\Plugin\Content\MokoOG` - -### Database Schema - -Single table `#__mokoog_tags`: -- `content_type` + `content_id` = unique key identifying any content item -- `og_title`, `og_description`, `og_image`, `og_type` = custom OG overrides -- `published` flag for enabling/disabling per-item - -## Rules - -- **Never commit** `.claude/`, `.mcp.json`, `TODO.md`, or `*.min.css`/`*.min.js` -- **Attribution**: use `Authored-by: Moko Consulting` in commits -- **Branch strategy**: develop on `dev`, merge to `main` for release -- **Minification**: handled at build time (CI) -- **Wiki**: documentation lives in the Gitea wiki, not in `docs/` files -- **Standards**: this repo follows [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home) - -## Coding Standards - -- PHP 8.1+ minimum -- Joomla 4/5 DI container pattern: `services/provider.php` → Extension class -- Legacy stub `.php` file required for plugin loader but empty -- `SubscriberInterface` for event subscription (not `on*` method naming) -- `bind() → check() → store()` for Table operations (not `save()`) -- Language file placement: site (no `folder`) vs admin (`folder="administrator"`) -- SPDX license headers on all PHP files From b108d083b2431cd30aca077b5f531d78df6ac509 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sat, 6 Jun 2026 19:49:05 +0000 Subject: [PATCH 107/132] chore: sync .mokogitea/workflows/pre-release.yml from moko-platform [skip ci] --- .mokogitea/workflows/pre-release.yml | 484 ++++++++++++++------------- 1 file changed, 243 insertions(+), 241 deletions(-) diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml index 1a9eeef..9615a4e 100644 --- a/.mokogitea/workflows/pre-release.yml +++ b/.mokogitea/workflows/pre-release.yml @@ -1,241 +1,243 @@ -# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: moko-platform.Release -# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform -# PATH: /templates/workflows/universal/pre-release.yml.template -# VERSION: 05.01.00 -# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch - -name: "Universal: Pre-Release" - -on: - pull_request: - types: [closed] - branches: - - dev - pull_request_target: - types: [synchronize, opened, reopened] - branches: - - main - workflow_dispatch: - inputs: - stability: - description: 'Pre-release channel' - required: true - type: choice - options: - - development - - alpha - - beta - - release-candidate - -permissions: - contents: write - -env: - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} - GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} - -jobs: - build: - name: "Build Pre-Release (${{ inputs.stability || 'development' }})" - runs-on: release - if: >- - github.event_name == 'workflow_dispatch' || - (github.event_name == 'pull_request' && github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev') || - (github.event_name == 'pull_request_target' && github.event.pull_request.base.ref == 'main') - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - token: ${{ secrets.MOKOGITEA_TOKEN }} - ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || '' }} - - - name: Setup moko-platform 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/vendor/autoload.php” ]; then - echo “Using pre-installed /opt/moko-platform” - echo “MOKO_CLI=/opt/moko-platform/cli” >> “$GITHUB_ENV” - else - echo “Falling back to fresh clone” - if ! command -v composer &> /dev/null; then - sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 - fi - rm -rf /tmp/moko-platform-api - git clone --depth 1 --branch main --quiet \ - “https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git” \ - /tmp/moko-platform-api - cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet - echo “MOKO_CLI=/tmp/moko-platform-api/cli” >> “$GITHUB_ENV” - fi - - - name: Detect platform - id: platform - run: | - php ${MOKO_CLI}/manifest_read.php --path . --github-output - - - name: Resolve metadata and bump version - id: meta - run: | - # Auto-detect stability: RC for PRs targeting main, else use input or default to development - if [ "${{ github.event_name }}" = "pull_request_target" ] && [ "${{ github.event.pull_request.base.ref }}" = "main" ]; then - STABILITY="release-candidate" - else - STABILITY="${{ inputs.stability || 'development' }}" - fi - - case "$STABILITY" in - development) SUFFIX="-dev"; TAG="development" ;; - alpha) SUFFIX="-alpha"; TAG="alpha" ;; - beta) SUFFIX="-beta"; TAG="beta" ;; - release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;; - esac - - # Bump version via CLI: patch for dev/alpha/beta, minor for RC - case "$STABILITY" in - release-candidate) BUMP="minor" ;; - *) BUMP="patch" ;; - esac - - php ${MOKO_CLI}/version_bump.php --path . $([ "$BUMP" = "minor" ] && echo "--minor") 2>/dev/null || true - - # Set stability suffix and verify consistency - VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "00.00.01") - VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//') - - php ${MOKO_CLI}/version_set_platform.php \ - --path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true - php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true - - # Append suffix for output - if [ -n "$SUFFIX" ]; then - VERSION="${VERSION}${SUFFIX}" - fi - - # Commit version bump - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" - git add -A - git diff --cached --quiet || { - git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]" - git push origin HEAD 2>&1 - } - - # Auto-detect element via manifest_element.php - php ${MOKO_CLI}/manifest_element.php \ - --path . --version "$VERSION" --stability "$STABILITY" \ - --repo "${GITEA_REPO}" --github-output - - # Read back element outputs - EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) - ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) - [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') - [ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip" - - echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT" - echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT" - echo "tag=${TAG}" >> "$GITHUB_OUTPUT" - echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT" - echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT" - - echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ===" - - - name: Create release - id: release - run: | - TAG="${{ steps.meta.outputs.tag }}" - VERSION="${{ steps.meta.outputs.version }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php ${MOKO_CLI}/release_create.php \ - --path . --version "$VERSION" --tag "$TAG" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ - --repo "${GITEA_REPO}" --branch dev --prerelease - - - name: Update release notes from CHANGELOG.md - run: | - TAG="${{ steps.meta.outputs.tag }}" - VERSION="${{ steps.meta.outputs.version }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - - # Extract [Unreleased] section from changelog (everything between [Unreleased] and next ## heading) - if [ -f "CHANGELOG.md" ]; then - NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md) - [ -z "$NOTES" ] && NOTES="Release ${VERSION}" - else - NOTES="Release ${VERSION}" - fi - - # Update release body via API - RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \ - "${API_BASE}/releases/tags/${TAG}" | 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 ${{ secrets.MOKOGITEA_TOKEN }}', - 'Content-Type': 'application/json' - }) - urllib.request.urlopen(req) - " <<< "$NOTES" - echo "Release notes updated from CHANGELOG.md" - fi - - - name: Build package and upload - id: package - run: | - VERSION="${{ steps.meta.outputs.version }}" - TAG="${{ steps.meta.outputs.tag }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php ${MOKO_CLI}/release_package.php \ - --path . --version "$VERSION" --tag "$TAG" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ - --repo "${GITEA_REPO}" --output /tmp || true - - # updates.xml is generated dynamically by MokoGitea license server - # No need to build, commit, or sync updates.xml from workflows - - - name: "Delete lesser pre-release channels (cascade)" - continue-on-error: true - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" - - php ${MOKO_CLI}/release_cascade.php \ - --stability "${{ steps.meta.outputs.stability }}" \ - --token "${TOKEN}" \ - --api-base "${API_BASE}" - - - name: Summary - if: always() - run: | - VERSION="${{ steps.meta.outputs.version }}" - STABILITY="${{ steps.meta.outputs.stability }}" - ZIP_NAME="${{ steps.meta.outputs.zip_name }}" - SHA256="${{ steps.package.outputs.sha256_zip }}" - echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY - echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY - echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY - echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY - echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY +# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: moko-platform.Release +# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform +# PATH: /templates/workflows/universal/pre-release.yml.template +# VERSION: 05.01.00 +# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch + +name: "Universal: Pre-Release" + +on: + pull_request: + types: [closed] + branches: + - dev + pull_request_target: + types: [synchronize, opened, reopened] + branches: + - main + workflow_dispatch: + inputs: + stability: + description: 'Pre-release channel' + required: true + type: choice + options: + - development + - alpha + - beta + - release-candidate + +permissions: + contents: write + +env: + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + +jobs: + build: + name: "Build Pre-Release (${{ inputs.stability || 'development' }})" + runs-on: release + if: >- + github.event_name == 'workflow_dispatch' || + (github.event_name == 'pull_request' && github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev') || + (github.event_name == 'pull_request_target' && github.event.pull_request.base.ref == 'main') + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.MOKOGITEA_TOKEN }} + ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || '' }} + + - name: Setup moko-platform 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 + 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 + fi + + - name: Detect platform + id: platform + run: | + php ${MOKO_CLI}/manifest_read.php --path . --github-output + + - name: Resolve metadata and bump version + id: meta + run: | + # Auto-detect stability: RC for PRs targeting main, else use input or default to development + if [ "${{ github.event_name }}" = "pull_request_target" ] && [ "${{ github.event.pull_request.base.ref }}" = "main" ]; then + STABILITY="release-candidate" + else + STABILITY="${{ inputs.stability || 'development' }}" + fi + + case "$STABILITY" in + development) SUFFIX="-dev"; TAG="development" ;; + alpha) SUFFIX="-alpha"; TAG="alpha" ;; + beta) SUFFIX="-beta"; TAG="beta" ;; + release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;; + esac + + # Bump version via CLI: patch for dev/alpha/beta, minor for RC + case "$STABILITY" in + release-candidate) BUMP="minor" ;; + *) BUMP="patch" ;; + esac + + php ${MOKO_CLI}/version_bump.php --path . $([ "$BUMP" = "minor" ] && echo "--minor") 2>/dev/null || true + + # Set stability suffix and verify consistency + VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "00.00.01") + VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//') + + php ${MOKO_CLI}/version_set_platform.php \ + --path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true + php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true + + # Ensure licensing tags (updateservers, dlid) if enabled in manifest.xml + php ${MOKO_CLI}/manifest_licensing.php --path . --fix 2>/dev/null || true + + # Append suffix for output + if [ -n "$SUFFIX" ]; then + VERSION="${VERSION}${SUFFIX}" + fi + + # Commit version bump + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + git add -A + git diff --cached --quiet || { + git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]" + git push origin HEAD 2>&1 + } + + # Auto-detect element via manifest_element.php + php ${MOKO_CLI}/manifest_element.php \ + --path . --version "$VERSION" --stability "$STABILITY" \ + --repo "${GITEA_REPO}" --github-output + + # Read back element outputs + EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) + ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) + [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') + [ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip" + + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT" + echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT" + echo "tag=${TAG}" >> "$GITHUB_OUTPUT" + echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT" + echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT" + + echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ===" + + - name: Create release + id: release + run: | + TAG="${{ steps.meta.outputs.tag }}" + VERSION="${{ steps.meta.outputs.version }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php ${MOKO_CLI}/release_create.php \ + --path . --version "$VERSION" --tag "$TAG" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ + --repo "${GITEA_REPO}" --branch dev --prerelease + + - name: Update release notes from CHANGELOG.md + run: | + TAG="${{ steps.meta.outputs.tag }}" + VERSION="${{ steps.meta.outputs.version }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + + # Extract [Unreleased] section from changelog (everything between [Unreleased] and next ## heading) + if [ -f "CHANGELOG.md" ]; then + NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md) + [ -z "$NOTES" ] && NOTES="Release ${VERSION}" + else + NOTES="Release ${VERSION}" + fi + + # Update release body via API + RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \ + "${API_BASE}/releases/tags/${TAG}" | 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 ${{ secrets.MOKOGITEA_TOKEN }}', + 'Content-Type': 'application/json' + }) + urllib.request.urlopen(req) + " <<< "$NOTES" + echo "Release notes updated from CHANGELOG.md" + fi + + - name: Build package and upload + id: package + run: | + VERSION="${{ steps.meta.outputs.version }}" + TAG="${{ steps.meta.outputs.tag }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php ${MOKO_CLI}/release_package.php \ + --path . --version "$VERSION" --tag "$TAG" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ + --repo "${GITEA_REPO}" --output /tmp || true + + # updates.xml is generated dynamically by MokoGitea license server + # No need to build, commit, or sync updates.xml from workflows + + - name: "Delete lesser pre-release channels (cascade)" + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" + + php ${MOKO_CLI}/release_cascade.php \ + --stability "${{ steps.meta.outputs.stability }}" \ + --token "${TOKEN}" \ + --api-base "${API_BASE}" + + - name: Summary + if: always() + run: | + VERSION="${{ steps.meta.outputs.version }}" + STABILITY="${{ steps.meta.outputs.stability }}" + ZIP_NAME="${{ steps.meta.outputs.zip_name }}" + SHA256="${{ steps.package.outputs.sha256_zip }}" + echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY + echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY + echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY From 19335867680afd4f69fbcc063b16a7d4152810ac Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sat, 6 Jun 2026 19:51:37 +0000 Subject: [PATCH 108/132] chore: sync .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] --- .mokogitea/workflows/auto-release.yml | 609 ++++++++++++++------------ 1 file changed, 324 insertions(+), 285 deletions(-) diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml index 141fdcc..ca40435 100644 --- a/.mokogitea/workflows/auto-release.yml +++ b/.mokogitea/workflows/auto-release.yml @@ -1,285 +1,324 @@ -# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: moko-platform.Release -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform -# PATH: /templates/workflows/universal/auto-release.yml.template -# VERSION: 05.00.00 -# BRIEF: Universal build & release � detects platform from manifest.xml -# -# +========================================================================+ -# | UNIVERSAL BUILD & RELEASE PIPELINE | -# +========================================================================+ -# | | -# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. | -# | | -# | Platform-specific: | -# | joomla: XML manifest, updates.xml, type-prefixed packages | -# | dolibarr: mod*.class.php, update.txt, dev version reset | -# | generic: README-only, no update stream | -# | | -# +========================================================================+ - -name: "Universal: Build & Release" - -on: - pull_request: - types: [opened, closed] - branches: - - main - workflow_dispatch: - inputs: - action: - description: 'Action to perform' - required: false - type: choice - default: release - options: - - release - - promote-rc - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} - GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} - -permissions: - contents: write - -jobs: - # ── PR Opened → Rename branch to RC and build RC release ───────────────────── - promote-rc: - name: Promote to RC - runs-on: release - if: >- - (github.event.action == 'opened' && github.event.pull_request.merged != true) || - (github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc') - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - token: ${{ secrets.MOKOGITEA_TOKEN }} - fetch-depth: 1 - - - name: Setup moko-platform tools - env: - MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting - run: | - if ! command -v composer &> /dev/null; then - sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 - fi - # Always fetch latest CLI tools — never use stale cache from previous runs - rm -rf /tmp/moko-platform-api - git clone --depth 1 --branch main --quiet \ - "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ - /tmp/moko-platform-api - cd /tmp/moko-platform-api - composer install --no-dev --no-interaction --quiet - - - name: Rename branch to rc - run: | - php /tmp/moko-platform-api/cli/branch_rename.php \ - --from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" \ - --api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \ - --pr "${{ github.event.pull_request.number }}" - - - name: Checkout rc and configure git - run: | - git fetch origin rc - git checkout rc - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" - - - name: Publish RC release - run: | - php /tmp/moko-platform-api/cli/release_publish.php \ - --path . --stability rc --bump minor --branch rc \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" \ - --skip-update-stream - - - name: Summary - if: always() - run: | - echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY - echo "Branch renamed to rc, minor bump, RC release built (updates.xml managed by Gitea Pages)" >> $GITHUB_STEP_SUMMARY - - # ── Merged PR → Build & Release (or promote RC to stable) ──────────────────── - release: - name: Build & Release Pipeline - runs-on: release - if: >- - github.event.pull_request.merged == true || - (github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc') - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - token: ${{ secrets.MOKOGITEA_TOKEN }} - fetch-depth: 0 - - - name: Configure git for bot pushes - run: | - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" - - - name: Check for merge conflict markers - run: | - CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true) - if [ -n "$CONFLICTS" ]; then - echo "::error::Merge conflict markers found — aborting release" - echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - exit 1 - fi - echo "No conflict markers found" - - - name: Setup moko-platform tools - env: - MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}' - run: | - # Ensure PHP + Composer are available - if ! command -v composer &> /dev/null; then - sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 - fi - # Always fetch latest CLI tools — never use stale cache from previous runs - rm -rf /tmp/moko-platform-api - git clone --depth 1 --branch main --quiet \ - "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ - /tmp/moko-platform-api - cd /tmp/moko-platform-api - composer install --no-dev --no-interaction --quiet - - - - name: "Publish stable release" - run: | - php /tmp/moko-platform-api/cli/release_publish.php \ - --path . --stability stable --bump minor --branch main \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" \ - --skip-update-stream - - # -- STEP 9: Mirror to GitHub (stable only) -------------------------------- - - name: "Step 9: Mirror release to GitHub" - if: >- - steps.version.outputs.skip != 'true' && - secrets.GH_MIRROR_TOKEN != '' - continue-on-error: true - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - RELEASE_TAG="${{ steps.version.outputs.release_tag }}" - GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php /tmp/moko-platform-api/cli/release_mirror.php \ - --version "$VERSION" --tag "$RELEASE_TAG" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ - --gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \ - --branch main 2>&1 || true - echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY - - # -- STEP 10: Sync main branch to GitHub mirror ---------------------------- - - name: "Step 10: Push main to GitHub mirror" - if: >- - steps.version.outputs.skip != 'true' && - secrets.GH_MIRROR_TOKEN != '' - continue-on-error: true - run: | - GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" - GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1) - GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2) - git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \ - git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" - git fetch origin main --depth=1 - git push github origin/main:refs/heads/main --force 2>/dev/null \ - && echo "main branch pushed to GitHub mirror" \ - || echo "WARNING: GitHub mirror push failed" - - - name: "Step 11: Delete rc branch and recreate dev from main" - if: steps.version.outputs.skip != 'true' - continue-on-error: true - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" - - # Delete rc branch (ephemeral — created by promote-rc) - curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ - "${API_BASE}/branches/rc" 2>/dev/null \ - && echo "Deleted rc branch" || echo "rc branch not found" - - # Delete dev branch - curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ - "${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch" - - # Recreate dev from main (now includes version bump + changelog promotion) - curl -sf -X POST -H "Authorization: token ${TOKEN}" \ - -H "Content-Type: application/json" \ - "${API_BASE}/branches" \ - -d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main" - - echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY - - - name: "Step 12: Create version branch from main" - if: steps.version.outputs.skip != 'true' - continue-on-error: true - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - BRANCH_NAME="version/${VERSION}" - MAIN_SHA=$(git rev-parse HEAD) - - # Delete old version branch if it exists (same version re-release) - curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}" - - # Create version/XX.YY.ZZ from main - curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed" - - echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY - - - - # -- Dolibarr post-release: Reset dev version ----------------------------- - - name: "Post-release: Reset dev version" - if: steps.version.outputs.skip != 'true' - continue-on-error: true - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php /tmp/moko-platform-api/cli/version_reset_dev.php \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \ - --branch dev --path . 2>&1 || true - - # -- Summary -------------------------------------------------------------- - - name: Pipeline Summary - if: always() - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - PLATFORM="${{ steps.platform.outputs.platform }}" - if [ "${{ steps.version.outputs.skip }}" = "true" ]; then - echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY - echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY - elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then - echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY - else - echo "" >> $GITHUB_STEP_SUMMARY - echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY - echo "|------|--------|" >> $GITHUB_STEP_SUMMARY - echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY - fi +# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: moko-platform.Release +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform +# PATH: /templates/workflows/universal/auto-release.yml.template +# VERSION: 05.00.00 +# BRIEF: Universal build & release � detects platform from manifest.xml +# +# +========================================================================+ +# | UNIVERSAL BUILD & RELEASE PIPELINE | +# +========================================================================+ +# | | +# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. | +# | | +# | Platform-specific: | +# | joomla: XML manifest, type-prefixed packages | +# | dolibarr: mod*.class.php, update.txt, dev version reset | +# | generic: README-only, no update stream | +# | | +# +========================================================================+ + +name: "Universal: Build & Release" + +on: + pull_request: + types: [opened, closed] + branches: + - main + workflow_dispatch: + inputs: + action: + description: 'Action to perform' + required: false + type: choice + default: release + options: + - release + - promote-rc + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + +permissions: + contents: write + +jobs: + # ── PR Opened → Rename branch to RC and build RC release ───────────────────── + promote-rc: + name: Promote to RC + runs-on: release + if: >- + (github.event.action == 'opened' && github.event.pull_request.merged != true) || + (github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc') + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.MOKOGITEA_TOKEN }} + fetch-depth: 1 + + - name: Setup moko-platform tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + run: | + if [ -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 + 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 + fi + + - name: Rename branch to rc + run: | + php ${MOKO_CLI}/branch_rename.php \ + --from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" \ + --api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \ + --pr "${{ github.event.pull_request.number }}" + + - name: Checkout rc and configure git + run: | + git fetch origin rc + git checkout rc + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + + - name: Publish RC release + run: | + php ${MOKO_CLI}/release_publish.php \ + --path . --stability rc --bump minor --branch rc \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" + + - name: Summary + if: always() + run: | + echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY + echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY + + # ── Merged PR → Build & Release (or promote RC to stable) ──────────────────── + release: + name: Build & Release Pipeline + runs-on: release + if: >- + github.event.pull_request.merged == true || + (github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc') + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.MOKOGITEA_TOKEN }} + fetch-depth: 0 + + - name: Configure git for bot pushes + run: | + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + + - name: Check for merge conflict markers + run: | + CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true) + if [ -n "$CONFLICTS" ]; then + echo "::error::Merge conflict markers found — aborting release" + echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + exit 1 + fi + echo "No conflict markers found" + + - name: Setup moko-platform tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}' + run: | + 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 + 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 + fi + + - name: "Publish stable release" + run: | + php ${MOKO_CLI}/release_publish.php \ + --path . --stability stable --bump minor --branch main \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" + + - name: Update release notes from CHANGELOG.md + 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" + else + NOTES="Stable release" + 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) + + 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 ${{ secrets.MOKOGITEA_TOKEN }}', + 'Content-Type': 'application/json' + }) + urllib.request.urlopen(req) + " <<< "$NOTES" + echo "Release notes updated from CHANGELOG.md" + fi + + # -- STEP 9: Mirror to GitHub (stable only) -------------------------------- + - name: "Step 9: Mirror release to GitHub" + if: >- + steps.version.outputs.skip != 'true' && + secrets.GH_MIRROR_TOKEN != '' + continue-on-error: true + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php ${MOKO_CLI}/release_mirror.php \ + --version "$VERSION" --tag "$RELEASE_TAG" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ + --gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \ + --branch main 2>&1 || true + echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY + + # -- STEP 10: Sync main branch to GitHub mirror ---------------------------- + - name: "Step 10: Push main to GitHub mirror" + if: >- + steps.version.outputs.skip != 'true' && + secrets.GH_MIRROR_TOKEN != '' + continue-on-error: true + run: | + GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" + GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1) + GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2) + git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \ + git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" + git fetch origin main --depth=1 + git push github origin/main:refs/heads/main --force 2>/dev/null \ + && echo "main branch pushed to GitHub mirror" \ + || echo "WARNING: GitHub mirror push failed" + + - name: "Step 11: Delete rc branch and recreate dev from main" + if: steps.version.outputs.skip != 'true' + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" + + # Delete rc branch (ephemeral — created by promote-rc) + curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/branches/rc" 2>/dev/null \ + && echo "Deleted rc branch" || echo "rc branch not found" + + # Delete dev branch + curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch" + + # Recreate dev from main (now includes version bump + changelog promotion) + curl -sf -X POST -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/json" \ + "${API_BASE}/branches" \ + -d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main" + + echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY + + - name: "Step 12: Create version branch from main" + if: steps.version.outputs.skip != 'true' + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + BRANCH_NAME="version/${VERSION}" + MAIN_SHA=$(git rev-parse HEAD) + + # Delete old version branch if it exists (same version re-release) + curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}" + + # Create version/XX.YY.ZZ from main + curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed" + + echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY + + + + # -- Dolibarr post-release: Reset dev version ----------------------------- + - name: "Post-release: Reset dev version" + if: steps.version.outputs.skip != 'true' + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php ${MOKO_CLI}/version_reset_dev.php \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \ + --branch dev --path . 2>&1 || true + + # -- Summary -------------------------------------------------------------- + - name: Pipeline Summary + if: always() + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + PLATFORM="${{ steps.platform.outputs.platform }}" + if [ "${{ steps.version.outputs.skip }}" = "true" ]; then + echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY + echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY + elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then + echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY + echo "|------|--------|" >> $GITHUB_STEP_SUMMARY + echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY + fi From a6e350d3b465f5ce96208819c3c6013d2b4ec00d Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sat, 6 Jun 2026 17:13:18 -0500 Subject: [PATCH 109/132] feat: download key preservation + license key warning (MokoWaaS pattern) --- source/script.php | 206 ++++++++++++++++++++++++++++++---------------- 1 file changed, 134 insertions(+), 72 deletions(-) diff --git a/source/script.php b/source/script.php index d515031..674566c 100644 --- a/source/script.php +++ b/source/script.php @@ -15,86 +15,148 @@ use Joomla\CMS\Language\Text; class Pkg_MokoOGInstallerScript { - /** - * Minimum Joomla version required - * - * @var string - */ - protected $minimumJoomla = '4.0.0'; + protected $minimumJoomla = '4.0.0'; + protected $minimumPhp = '8.1.0'; - /** - * Minimum PHP version required - * - * @var string - */ - protected $minimumPhp = '8.1.0'; + /** @var array Download keys saved before Joomla wipes update sites */ + private array $savedDownloadKeys = []; - /** - * Called before any install/update/uninstall action. - * - * @param string $type Action type (install, update, uninstall) - * @param InstallerAdapter $parent Installer adapter - * - * @return bool - */ - public function preflight(string $type, InstallerAdapter $parent): bool - { - if (version_compare(PHP_VERSION, $this->minimumPhp, '<')) { - Factory::getApplication()->enqueueMessage( - Text::sprintf('PKG_MOKOOG_PHP_VERSION_ERROR', $this->minimumPhp), - 'error' - ); + public function preflight(string $type, InstallerAdapter $parent): bool + { + if (version_compare(PHP_VERSION, $this->minimumPhp, '<')) + { + Factory::getApplication()->enqueueMessage( + Text::sprintf('PKG_MOKOOG_PHP_VERSION_ERROR', $this->minimumPhp), + 'error' + ); - return false; - } + return false; + } - return true; - } + $this->savedDownloadKeys = $this->backupDownloadKeys(); - /** - * Called after install/update. - * - * @param string $type Action type - * @param InstallerAdapter $parent Installer adapter - * - * @return void - */ - public function postflight(string $type, InstallerAdapter $parent): void - { - if ($type === 'install') { - // Enable the system plugin automatically on fresh install - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->update($db->quoteName('#__extensions')) - ->set($db->quoteName('enabled') . ' = 1') - ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) - ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) - ->where($db->quoteName('element') . ' = ' . $db->quote('mokoog')); + return true; + } - $db->setQuery($query); - $db->execute(); + public function postflight(string $type, InstallerAdapter $parent): void + { + $this->restoreDownloadKeys($this->savedDownloadKeys); + $this->warnMissingLicenseKey(); - // Enable the content plugin automatically - $query = $db->getQuery(true) - ->update($db->quoteName('#__extensions')) - ->set($db->quoteName('enabled') . ' = 1') - ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) - ->where($db->quoteName('folder') . ' = ' . $db->quote('content')) - ->where($db->quoteName('element') . ' = ' . $db->quote('mokoog')); + if ($type === 'install') + { + $db = Factory::getDbo(); - $db->setQuery($query); - $db->execute(); + foreach (['system', 'content', 'webservices'] as $folder) + { + $db->setQuery( + $db->getQuery(true) + ->update($db->quoteName('#__extensions')) + ->set($db->quoteName('enabled') . ' = 1') + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('folder') . ' = ' . $db->quote($folder)) + ->where($db->quoteName('element') . ' = ' . $db->quote('mokoog')) + )->execute(); + } + } + } - // Enable the webservices plugin automatically - $query = $db->getQuery(true) - ->update($db->quoteName('#__extensions')) - ->set($db->quoteName('enabled') . ' = 1') - ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) - ->where($db->quoteName('folder') . ' = ' . $db->quote('webservices')) - ->where($db->quoteName('element') . ' = ' . $db->quote('mokoog')); + private function backupDownloadKeys(): array + { + $keys = []; - $db->setQuery($query); - $db->execute(); - } - } + try + { + $db = Factory::getDbo(); + $db->setQuery( + $db->getQuery(true) + ->select(['us.' . $db->quoteName('update_site_id'), 'us.' . $db->quoteName('extra_query'), 'us.' . $db->quoteName('location'), 'e.' . $db->quoteName('element')]) + ->from($db->quoteName('#__update_sites', 'us')) + ->join('INNER', $db->quoteName('#__update_sites_extensions', 'use') . ' ON us.update_site_id = use.update_site_id') + ->join('INNER', $db->quoteName('#__extensions', 'e') . ' ON e.extension_id = use.extension_id') + ->where($db->quoteName('us.extra_query') . ' != ' . $db->quote('')) + ); + + foreach ($db->loadObjectList() ?: [] as $row) + { + $keys['elem_' . $row->element] = $row->extra_query; + $keys[$row->location] = $row->extra_query; + $keys['id_' . $row->update_site_id] = $row->extra_query; + } + } + catch (\Throwable $e) {} + + return $keys; + } + + private function restoreDownloadKeys(array $savedKeys): void + { + if (empty($savedKeys)) { return; } + + try + { + $db = Factory::getDbo(); + $db->setQuery( + $db->getQuery(true) + ->select(['us.' . $db->quoteName('update_site_id'), 'us.' . $db->quoteName('extra_query'), 'us.' . $db->quoteName('location'), 'e.' . $db->quoteName('element')]) + ->from($db->quoteName('#__update_sites', 'us')) + ->join('LEFT', $db->quoteName('#__update_sites_extensions', 'use') . ' ON us.update_site_id = use.update_site_id') + ->join('LEFT', $db->quoteName('#__extensions', 'e') . ' ON e.extension_id = use.extension_id') + ->where('(' . $db->quoteName('us.extra_query') . ' = ' . $db->quote('') . ' OR ' . $db->quoteName('us.extra_query') . ' NOT LIKE ' . $db->quote('%dlid=%') . ')') + ); + + $restored = 0; + + foreach ($db->loadObjectList() ?: [] as $site) + { + $element = (string) ($site->element ?? ''); + $key = ($element !== '') ? ($savedKeys['elem_' . $element] ?? '') : ''; + if (empty($key)) { $key = $savedKeys[$site->location] ?? $savedKeys['id_' . $site->update_site_id] ?? ''; } + + if (!empty($key)) + { + $db->setQuery($db->getQuery(true)->update($db->quoteName('#__update_sites'))->set($db->quoteName('extra_query') . ' = ' . $db->quote($key))->where($db->quoteName('update_site_id') . ' = ' . (int) $site->update_site_id))->execute(); + $restored++; + } + } + + if ($restored > 0) { Factory::getApplication()->enqueueMessage(sprintf('Restored %d download key(s).', $restored), 'message'); } + } + catch (\Throwable $e) {} + } + + private function warnMissingLicenseKey(): void + { + try + { + $db = Factory::getDbo(); + $db->setQuery( + $db->getQuery(true) + ->select([$db->quoteName('update_site_id'), $db->quoteName('extra_query')]) + ->from($db->quoteName('#__update_sites')) + ->where('(' . $db->quoteName('name') . ' LIKE ' . $db->quote('%MokoJoomOpenGraph%') . ' OR ' . $db->quoteName('location') . ' LIKE ' . $db->quote('%MokoJoomOpenGraph%') . ')') + ->setLimit(1) + ); + $site = $db->loadObject(); + + if ($site) + { + $eq = (string) ($site->extra_query ?? ''); + if (!empty($eq) && strpos($eq, 'dlid=') !== false) { parse_str($eq, $p); if (!empty($p['dlid'])) { return; } } + $editUrl = 'index.php?option=com_installer&task=updatesite.edit&update_site_id=' . (int) $site->update_site_id; + } + else + { + $editUrl = 'index.php?option=com_installer&view=updatesites'; + } + + Factory::getApplication()->enqueueMessage( + '<strong>Moko Consulting License Key Required</strong> — ' + . 'No download key is configured. Updates will not be available until a valid license key is entered. ' + . '<a href="' . $editUrl . '" class="btn btn-sm btn-warning ms-2">Enter License Key</a>', + 'warning' + ); + } + catch (\Throwable $e) {} + } } From d55fb0d38f79a7671921c0dfbba2cd988d8be060 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sat, 6 Jun 2026 17:31:07 -0500 Subject: [PATCH 110/132] fix: use single-key pattern instead of universal backup --- source/script.php | 78 +++++++++++++++++++++-------------------------- 1 file changed, 34 insertions(+), 44 deletions(-) diff --git a/source/script.php b/source/script.php index 674566c..b50d096 100644 --- a/source/script.php +++ b/source/script.php @@ -18,8 +18,8 @@ class Pkg_MokoOGInstallerScript protected $minimumJoomla = '4.0.0'; protected $minimumPhp = '8.1.0'; - /** @var array Download keys saved before Joomla wipes update sites */ - private array $savedDownloadKeys = []; + + public function preflight(string $type, InstallerAdapter $parent): bool { @@ -33,14 +33,14 @@ class Pkg_MokoOGInstallerScript return false; } - $this->savedDownloadKeys = $this->backupDownloadKeys(); + $this->saveDownloadKey(); return true; } public function postflight(string $type, InstallerAdapter $parent): void { - $this->restoreDownloadKeys($this->savedDownloadKeys); + $this->restoreDownloadKey(); $this->warnMissingLicenseKey(); if ($type === 'install') @@ -61,66 +61,56 @@ class Pkg_MokoOGInstallerScript } } - private function backupDownloadKeys(): array - { - $keys = []; + + private ?string $savedDownloadKey = null; + + private function saveDownloadKey(): void + { try { - $db = Factory::getDbo(); + $db = \Joomla\CMS\Factory::getDbo(); $db->setQuery( $db->getQuery(true) - ->select(['us.' . $db->quoteName('update_site_id'), 'us.' . $db->quoteName('extra_query'), 'us.' . $db->quoteName('location'), 'e.' . $db->quoteName('element')]) + ->select($db->quoteName('us.extra_query')) ->from($db->quoteName('#__update_sites', 'us')) - ->join('INNER', $db->quoteName('#__update_sites_extensions', 'use') . ' ON us.update_site_id = use.update_site_id') + ->join('INNER', $db->quoteName('#__update_sites_extensions', 'use') . ' ON use.update_site_id = us.update_site_id') ->join('INNER', $db->quoteName('#__extensions', 'e') . ' ON e.extension_id = use.extension_id') - ->where($db->quoteName('us.extra_query') . ' != ' . $db->quote('')) + ->where($db->quoteName('e.element') . ' = ' . $db->quote('pkg_mokoog')) + ->setLimit(1) ); - - foreach ($db->loadObjectList() ?: [] as $row) - { - $keys['elem_' . $row->element] = $row->extra_query; - $keys[$row->location] = $row->extra_query; - $keys['id_' . $row->update_site_id] = $row->extra_query; - } + $key = $db->loadResult(); + if (!empty($key)) { $this->savedDownloadKey = $key; } } catch (\Throwable $e) {} - - return $keys; } - private function restoreDownloadKeys(array $savedKeys): void + private function restoreDownloadKey(): void { - if (empty($savedKeys)) { return; } + if ($this->savedDownloadKey === null) { return; } try { - $db = Factory::getDbo(); + $db = \Joomla\CMS\Factory::getDbo(); $db->setQuery( $db->getQuery(true) - ->select(['us.' . $db->quoteName('update_site_id'), 'us.' . $db->quoteName('extra_query'), 'us.' . $db->quoteName('location'), 'e.' . $db->quoteName('element')]) + ->select($db->quoteName('us.update_site_id')) ->from($db->quoteName('#__update_sites', 'us')) - ->join('LEFT', $db->quoteName('#__update_sites_extensions', 'use') . ' ON us.update_site_id = use.update_site_id') - ->join('LEFT', $db->quoteName('#__extensions', 'e') . ' ON e.extension_id = use.extension_id') - ->where('(' . $db->quoteName('us.extra_query') . ' = ' . $db->quote('') . ' OR ' . $db->quoteName('us.extra_query') . ' NOT LIKE ' . $db->quote('%dlid=%') . ')') + ->join('INNER', $db->quoteName('#__update_sites_extensions', 'use') . ' ON use.update_site_id = us.update_site_id') + ->join('INNER', $db->quoteName('#__extensions', 'e') . ' ON e.extension_id = use.extension_id') + ->where($db->quoteName('e.element') . ' = ' . $db->quote('pkg_mokoog')) + ->setLimit(1) ); - - $restored = 0; - - foreach ($db->loadObjectList() ?: [] as $site) + $siteId = (int) $db->loadResult(); + if ($siteId > 0) { - $element = (string) ($site->element ?? ''); - $key = ($element !== '') ? ($savedKeys['elem_' . $element] ?? '') : ''; - if (empty($key)) { $key = $savedKeys[$site->location] ?? $savedKeys['id_' . $site->update_site_id] ?? ''; } - - if (!empty($key)) - { - $db->setQuery($db->getQuery(true)->update($db->quoteName('#__update_sites'))->set($db->quoteName('extra_query') . ' = ' . $db->quote($key))->where($db->quoteName('update_site_id') . ' = ' . (int) $site->update_site_id))->execute(); - $restored++; - } + $db->setQuery( + $db->getQuery(true) + ->update($db->quoteName('#__update_sites')) + ->set($db->quoteName('extra_query') . ' = ' . $db->quote($this->savedDownloadKey)) + ->where($db->quoteName('update_site_id') . ' = ' . $siteId) + )->execute(); } - - if ($restored > 0) { Factory::getApplication()->enqueueMessage(sprintf('Restored %d download key(s).', $restored), 'message'); } } catch (\Throwable $e) {} } @@ -129,7 +119,7 @@ class Pkg_MokoOGInstallerScript { try { - $db = Factory::getDbo(); + $db = \Joomla\CMS\Factory::getDbo(); $db->setQuery( $db->getQuery(true) ->select([$db->quoteName('update_site_id'), $db->quoteName('extra_query')]) @@ -150,7 +140,7 @@ class Pkg_MokoOGInstallerScript $editUrl = 'index.php?option=com_installer&view=updatesites'; } - Factory::getApplication()->enqueueMessage( + \Joomla\CMS\Factory::getApplication()->enqueueMessage( '<strong>Moko Consulting License Key Required</strong> — ' . 'No download key is configured. Updates will not be available until a valid license key is entered. ' . '<a href="' . $editUrl . '" class="btn btn-sm btn-warning ms-2">Enter License Key</a>', From 1593f28f1916ce3d78bac6c8ecf633425cfe5d84 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" <gitea-actions[bot]@mokoconsulting.tech> Date: Sun, 7 Jun 2026 01:10:13 +0000 Subject: [PATCH 111/132] chore(version): pre-release bump to 01.00.02-dev [skip ci] --- .mokogitea/manifest.xml | 2 +- .mokogitea/workflows/auto-release.yml | 648 +++++++++--------- .mokogitea/workflows/pre-release.yml | 486 ++++++------- CHANGELOG.md | 2 +- README.md | 2 +- source/packages/com_mokoog/mokoog.xml | 2 +- source/packages/plg_content_mokoog/mokoog.xml | 2 +- source/packages/plg_system_mokoog/mokoog.xml | 2 +- .../plg_webservices_mokoog/mokoog.xml | 2 +- source/pkg_mokoog.xml | 2 +- 10 files changed, 575 insertions(+), 575 deletions(-) diff --git a/.mokogitea/manifest.xml b/.mokogitea/manifest.xml index 43d13ca..58032f2 100644 --- a/.mokogitea/manifest.xml +++ b/.mokogitea/manifest.xml @@ -9,7 +9,7 @@ <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.01</version> + <version>01.00.02</version> <license spdx="GPL-3.0-or-later">GNU General Public License v3</license> </identity> <governance> diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml index ca40435..b657b98 100644 --- a/.mokogitea/workflows/auto-release.yml +++ b/.mokogitea/workflows/auto-release.yml @@ -1,324 +1,324 @@ -# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: moko-platform.Release -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform -# PATH: /templates/workflows/universal/auto-release.yml.template -# VERSION: 05.00.00 -# BRIEF: Universal build & release � detects platform from manifest.xml -# -# +========================================================================+ -# | UNIVERSAL BUILD & RELEASE PIPELINE | -# +========================================================================+ -# | | -# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. | -# | | -# | Platform-specific: | -# | joomla: XML manifest, type-prefixed packages | -# | dolibarr: mod*.class.php, update.txt, dev version reset | -# | generic: README-only, no update stream | -# | | -# +========================================================================+ - -name: "Universal: Build & Release" - -on: - pull_request: - types: [opened, closed] - branches: - - main - workflow_dispatch: - inputs: - action: - description: 'Action to perform' - required: false - type: choice - default: release - options: - - release - - promote-rc - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} - GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} - -permissions: - contents: write - -jobs: - # ── PR Opened → Rename branch to RC and build RC release ───────────────────── - promote-rc: - name: Promote to RC - runs-on: release - if: >- - (github.event.action == 'opened' && github.event.pull_request.merged != true) || - (github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc') - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - token: ${{ secrets.MOKOGITEA_TOKEN }} - fetch-depth: 1 - - - name: Setup moko-platform tools - env: - MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting - run: | - if [ -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 - 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 - fi - - - name: Rename branch to rc - run: | - php ${MOKO_CLI}/branch_rename.php \ - --from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" \ - --api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \ - --pr "${{ github.event.pull_request.number }}" - - - name: Checkout rc and configure git - run: | - git fetch origin rc - git checkout rc - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" - - - name: Publish RC release - run: | - php ${MOKO_CLI}/release_publish.php \ - --path . --stability rc --bump minor --branch rc \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" - - - name: Summary - if: always() - run: | - echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY - echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY - - # ── Merged PR → Build & Release (or promote RC to stable) ──────────────────── - release: - name: Build & Release Pipeline - runs-on: release - if: >- - github.event.pull_request.merged == true || - (github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc') - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - token: ${{ secrets.MOKOGITEA_TOKEN }} - fetch-depth: 0 - - - name: Configure git for bot pushes - run: | - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" - - - name: Check for merge conflict markers - run: | - CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true) - if [ -n "$CONFLICTS" ]; then - echo "::error::Merge conflict markers found — aborting release" - echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - exit 1 - fi - echo "No conflict markers found" - - - name: Setup moko-platform tools - env: - MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}' - run: | - 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 - 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 - fi - - - name: "Publish stable release" - run: | - php ${MOKO_CLI}/release_publish.php \ - --path . --stability stable --bump minor --branch main \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" - - - name: Update release notes from CHANGELOG.md - 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" - else - NOTES="Stable release" - 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) - - 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 ${{ secrets.MOKOGITEA_TOKEN }}', - 'Content-Type': 'application/json' - }) - urllib.request.urlopen(req) - " <<< "$NOTES" - echo "Release notes updated from CHANGELOG.md" - fi - - # -- STEP 9: Mirror to GitHub (stable only) -------------------------------- - - name: "Step 9: Mirror release to GitHub" - if: >- - steps.version.outputs.skip != 'true' && - secrets.GH_MIRROR_TOKEN != '' - continue-on-error: true - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - RELEASE_TAG="${{ steps.version.outputs.release_tag }}" - GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php ${MOKO_CLI}/release_mirror.php \ - --version "$VERSION" --tag "$RELEASE_TAG" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ - --gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \ - --branch main 2>&1 || true - echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY - - # -- STEP 10: Sync main branch to GitHub mirror ---------------------------- - - name: "Step 10: Push main to GitHub mirror" - if: >- - steps.version.outputs.skip != 'true' && - secrets.GH_MIRROR_TOKEN != '' - continue-on-error: true - run: | - GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" - GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1) - GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2) - git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \ - git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" - git fetch origin main --depth=1 - git push github origin/main:refs/heads/main --force 2>/dev/null \ - && echo "main branch pushed to GitHub mirror" \ - || echo "WARNING: GitHub mirror push failed" - - - name: "Step 11: Delete rc branch and recreate dev from main" - if: steps.version.outputs.skip != 'true' - continue-on-error: true - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" - - # Delete rc branch (ephemeral — created by promote-rc) - curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ - "${API_BASE}/branches/rc" 2>/dev/null \ - && echo "Deleted rc branch" || echo "rc branch not found" - - # Delete dev branch - curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ - "${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch" - - # Recreate dev from main (now includes version bump + changelog promotion) - curl -sf -X POST -H "Authorization: token ${TOKEN}" \ - -H "Content-Type: application/json" \ - "${API_BASE}/branches" \ - -d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main" - - echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY - - - name: "Step 12: Create version branch from main" - if: steps.version.outputs.skip != 'true' - continue-on-error: true - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - BRANCH_NAME="version/${VERSION}" - MAIN_SHA=$(git rev-parse HEAD) - - # Delete old version branch if it exists (same version re-release) - curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}" - - # Create version/XX.YY.ZZ from main - curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed" - - echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY - - - - # -- Dolibarr post-release: Reset dev version ----------------------------- - - name: "Post-release: Reset dev version" - if: steps.version.outputs.skip != 'true' - continue-on-error: true - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php ${MOKO_CLI}/version_reset_dev.php \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \ - --branch dev --path . 2>&1 || true - - # -- Summary -------------------------------------------------------------- - - name: Pipeline Summary - if: always() - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - PLATFORM="${{ steps.platform.outputs.platform }}" - if [ "${{ steps.version.outputs.skip }}" = "true" ]; then - echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY - echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY - elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then - echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY - else - echo "" >> $GITHUB_STEP_SUMMARY - echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY - echo "|------|--------|" >> $GITHUB_STEP_SUMMARY - echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY - fi +# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: moko-platform.Release +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform +# PATH: /templates/workflows/universal/auto-release.yml.template +# VERSION: 05.00.00 +# BRIEF: Universal build & release � detects platform from manifest.xml +# +# +========================================================================+ +# | UNIVERSAL BUILD & RELEASE PIPELINE | +# +========================================================================+ +# | | +# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. | +# | | +# | Platform-specific: | +# | joomla: XML manifest, type-prefixed packages | +# | dolibarr: mod*.class.php, update.txt, dev version reset | +# | generic: README-only, no update stream | +# | | +# +========================================================================+ + +name: "Universal: Build & Release" + +on: + pull_request: + types: [opened, closed] + branches: + - main + workflow_dispatch: + inputs: + action: + description: 'Action to perform' + required: false + type: choice + default: release + options: + - release + - promote-rc + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + +permissions: + contents: write + +jobs: + # ── PR Opened → Rename branch to RC and build RC release ───────────────────── + promote-rc: + name: Promote to RC + runs-on: release + if: >- + (github.event.action == 'opened' && github.event.pull_request.merged != true) || + (github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc') + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.MOKOGITEA_TOKEN }} + fetch-depth: 1 + + - name: Setup moko-platform tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + run: | + if [ -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 + 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 + fi + + - name: Rename branch to rc + run: | + php ${MOKO_CLI}/branch_rename.php \ + --from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" \ + --api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \ + --pr "${{ github.event.pull_request.number }}" + + - name: Checkout rc and configure git + run: | + git fetch origin rc + git checkout rc + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + + - name: Publish RC release + run: | + php ${MOKO_CLI}/release_publish.php \ + --path . --stability rc --bump minor --branch rc \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" + + - name: Summary + if: always() + run: | + echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY + echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY + + # ── Merged PR → Build & Release (or promote RC to stable) ──────────────────── + release: + name: Build & Release Pipeline + runs-on: release + if: >- + github.event.pull_request.merged == true || + (github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc') + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.MOKOGITEA_TOKEN }} + fetch-depth: 0 + + - name: Configure git for bot pushes + run: | + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + + - name: Check for merge conflict markers + run: | + CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true) + if [ -n "$CONFLICTS" ]; then + echo "::error::Merge conflict markers found — aborting release" + echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + exit 1 + fi + echo "No conflict markers found" + + - name: Setup moko-platform tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}' + run: | + 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 + 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 + fi + + - name: "Publish stable release" + run: | + php ${MOKO_CLI}/release_publish.php \ + --path . --stability stable --bump minor --branch main \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" + + - name: Update release notes from CHANGELOG.md + 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" + else + NOTES="Stable release" + 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) + + 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 ${{ secrets.MOKOGITEA_TOKEN }}', + 'Content-Type': 'application/json' + }) + urllib.request.urlopen(req) + " <<< "$NOTES" + echo "Release notes updated from CHANGELOG.md" + fi + + # -- STEP 9: Mirror to GitHub (stable only) -------------------------------- + - name: "Step 9: Mirror release to GitHub" + if: >- + steps.version.outputs.skip != 'true' && + secrets.GH_MIRROR_TOKEN != '' + continue-on-error: true + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php ${MOKO_CLI}/release_mirror.php \ + --version "$VERSION" --tag "$RELEASE_TAG" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ + --gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \ + --branch main 2>&1 || true + echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY + + # -- STEP 10: Sync main branch to GitHub mirror ---------------------------- + - name: "Step 10: Push main to GitHub mirror" + if: >- + steps.version.outputs.skip != 'true' && + secrets.GH_MIRROR_TOKEN != '' + continue-on-error: true + run: | + GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" + GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1) + GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2) + git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \ + git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" + git fetch origin main --depth=1 + git push github origin/main:refs/heads/main --force 2>/dev/null \ + && echo "main branch pushed to GitHub mirror" \ + || echo "WARNING: GitHub mirror push failed" + + - name: "Step 11: Delete rc branch and recreate dev from main" + if: steps.version.outputs.skip != 'true' + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" + + # Delete rc branch (ephemeral — created by promote-rc) + curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/branches/rc" 2>/dev/null \ + && echo "Deleted rc branch" || echo "rc branch not found" + + # Delete dev branch + curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch" + + # Recreate dev from main (now includes version bump + changelog promotion) + curl -sf -X POST -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/json" \ + "${API_BASE}/branches" \ + -d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main" + + echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY + + - name: "Step 12: Create version branch from main" + if: steps.version.outputs.skip != 'true' + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + BRANCH_NAME="version/${VERSION}" + MAIN_SHA=$(git rev-parse HEAD) + + # Delete old version branch if it exists (same version re-release) + curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}" + + # Create version/XX.YY.ZZ from main + curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed" + + echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY + + + + # -- Dolibarr post-release: Reset dev version ----------------------------- + - name: "Post-release: Reset dev version" + if: steps.version.outputs.skip != 'true' + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php ${MOKO_CLI}/version_reset_dev.php \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \ + --branch dev --path . 2>&1 || true + + # -- Summary -------------------------------------------------------------- + - name: Pipeline Summary + if: always() + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + PLATFORM="${{ steps.platform.outputs.platform }}" + if [ "${{ steps.version.outputs.skip }}" = "true" ]; then + echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY + echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY + elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then + echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY + echo "|------|--------|" >> $GITHUB_STEP_SUMMARY + echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY + fi diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml index 9615a4e..86908c2 100644 --- a/.mokogitea/workflows/pre-release.yml +++ b/.mokogitea/workflows/pre-release.yml @@ -1,243 +1,243 @@ -# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: moko-platform.Release -# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform -# PATH: /templates/workflows/universal/pre-release.yml.template -# VERSION: 05.01.00 -# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch - -name: "Universal: Pre-Release" - -on: - pull_request: - types: [closed] - branches: - - dev - pull_request_target: - types: [synchronize, opened, reopened] - branches: - - main - workflow_dispatch: - inputs: - stability: - description: 'Pre-release channel' - required: true - type: choice - options: - - development - - alpha - - beta - - release-candidate - -permissions: - contents: write - -env: - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} - GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} - -jobs: - build: - name: "Build Pre-Release (${{ inputs.stability || 'development' }})" - runs-on: release - if: >- - github.event_name == 'workflow_dispatch' || - (github.event_name == 'pull_request' && github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev') || - (github.event_name == 'pull_request_target' && github.event.pull_request.base.ref == 'main') - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - token: ${{ secrets.MOKOGITEA_TOKEN }} - ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || '' }} - - - name: Setup moko-platform 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 - 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 - fi - - - name: Detect platform - id: platform - run: | - php ${MOKO_CLI}/manifest_read.php --path . --github-output - - - name: Resolve metadata and bump version - id: meta - run: | - # Auto-detect stability: RC for PRs targeting main, else use input or default to development - if [ "${{ github.event_name }}" = "pull_request_target" ] && [ "${{ github.event.pull_request.base.ref }}" = "main" ]; then - STABILITY="release-candidate" - else - STABILITY="${{ inputs.stability || 'development' }}" - fi - - case "$STABILITY" in - development) SUFFIX="-dev"; TAG="development" ;; - alpha) SUFFIX="-alpha"; TAG="alpha" ;; - beta) SUFFIX="-beta"; TAG="beta" ;; - release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;; - esac - - # Bump version via CLI: patch for dev/alpha/beta, minor for RC - case "$STABILITY" in - release-candidate) BUMP="minor" ;; - *) BUMP="patch" ;; - esac - - php ${MOKO_CLI}/version_bump.php --path . $([ "$BUMP" = "minor" ] && echo "--minor") 2>/dev/null || true - - # Set stability suffix and verify consistency - VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "00.00.01") - VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//') - - php ${MOKO_CLI}/version_set_platform.php \ - --path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true - php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true - - # Ensure licensing tags (updateservers, dlid) if enabled in manifest.xml - php ${MOKO_CLI}/manifest_licensing.php --path . --fix 2>/dev/null || true - - # Append suffix for output - if [ -n "$SUFFIX" ]; then - VERSION="${VERSION}${SUFFIX}" - fi - - # Commit version bump - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" - git add -A - git diff --cached --quiet || { - git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]" - git push origin HEAD 2>&1 - } - - # Auto-detect element via manifest_element.php - php ${MOKO_CLI}/manifest_element.php \ - --path . --version "$VERSION" --stability "$STABILITY" \ - --repo "${GITEA_REPO}" --github-output - - # Read back element outputs - EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) - ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) - [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') - [ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip" - - echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT" - echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT" - echo "tag=${TAG}" >> "$GITHUB_OUTPUT" - echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT" - echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT" - - echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ===" - - - name: Create release - id: release - run: | - TAG="${{ steps.meta.outputs.tag }}" - VERSION="${{ steps.meta.outputs.version }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php ${MOKO_CLI}/release_create.php \ - --path . --version "$VERSION" --tag "$TAG" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ - --repo "${GITEA_REPO}" --branch dev --prerelease - - - name: Update release notes from CHANGELOG.md - run: | - TAG="${{ steps.meta.outputs.tag }}" - VERSION="${{ steps.meta.outputs.version }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - - # Extract [Unreleased] section from changelog (everything between [Unreleased] and next ## heading) - if [ -f "CHANGELOG.md" ]; then - NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md) - [ -z "$NOTES" ] && NOTES="Release ${VERSION}" - else - NOTES="Release ${VERSION}" - fi - - # Update release body via API - RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \ - "${API_BASE}/releases/tags/${TAG}" | 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 ${{ secrets.MOKOGITEA_TOKEN }}', - 'Content-Type': 'application/json' - }) - urllib.request.urlopen(req) - " <<< "$NOTES" - echo "Release notes updated from CHANGELOG.md" - fi - - - name: Build package and upload - id: package - run: | - VERSION="${{ steps.meta.outputs.version }}" - TAG="${{ steps.meta.outputs.tag }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php ${MOKO_CLI}/release_package.php \ - --path . --version "$VERSION" --tag "$TAG" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ - --repo "${GITEA_REPO}" --output /tmp || true - - # updates.xml is generated dynamically by MokoGitea license server - # No need to build, commit, or sync updates.xml from workflows - - - name: "Delete lesser pre-release channels (cascade)" - continue-on-error: true - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" - - php ${MOKO_CLI}/release_cascade.php \ - --stability "${{ steps.meta.outputs.stability }}" \ - --token "${TOKEN}" \ - --api-base "${API_BASE}" - - - name: Summary - if: always() - run: | - VERSION="${{ steps.meta.outputs.version }}" - STABILITY="${{ steps.meta.outputs.stability }}" - ZIP_NAME="${{ steps.meta.outputs.zip_name }}" - SHA256="${{ steps.package.outputs.sha256_zip }}" - echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY - echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY - echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY - echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY - echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY +# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: moko-platform.Release +# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform +# PATH: /templates/workflows/universal/pre-release.yml.template +# VERSION: 05.01.00 +# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch + +name: "Universal: Pre-Release" + +on: + pull_request: + types: [closed] + branches: + - dev + pull_request_target: + types: [synchronize, opened, reopened] + branches: + - main + workflow_dispatch: + inputs: + stability: + description: 'Pre-release channel' + required: true + type: choice + options: + - development + - alpha + - beta + - release-candidate + +permissions: + contents: write + +env: + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + +jobs: + build: + name: "Build Pre-Release (${{ inputs.stability || 'development' }})" + runs-on: release + if: >- + github.event_name == 'workflow_dispatch' || + (github.event_name == 'pull_request' && github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev') || + (github.event_name == 'pull_request_target' && github.event.pull_request.base.ref == 'main') + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.MOKOGITEA_TOKEN }} + ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || '' }} + + - name: Setup moko-platform 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 + 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 + fi + + - name: Detect platform + id: platform + run: | + php ${MOKO_CLI}/manifest_read.php --path . --github-output + + - name: Resolve metadata and bump version + id: meta + run: | + # Auto-detect stability: RC for PRs targeting main, else use input or default to development + if [ "${{ github.event_name }}" = "pull_request_target" ] && [ "${{ github.event.pull_request.base.ref }}" = "main" ]; then + STABILITY="release-candidate" + else + STABILITY="${{ inputs.stability || 'development' }}" + fi + + case "$STABILITY" in + development) SUFFIX="-dev"; TAG="development" ;; + alpha) SUFFIX="-alpha"; TAG="alpha" ;; + beta) SUFFIX="-beta"; TAG="beta" ;; + release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;; + esac + + # Bump version via CLI: patch for dev/alpha/beta, minor for RC + case "$STABILITY" in + release-candidate) BUMP="minor" ;; + *) BUMP="patch" ;; + esac + + php ${MOKO_CLI}/version_bump.php --path . $([ "$BUMP" = "minor" ] && echo "--minor") 2>/dev/null || true + + # Set stability suffix and verify consistency + VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "00.00.01") + VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//') + + php ${MOKO_CLI}/version_set_platform.php \ + --path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true + php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true + + # Ensure licensing tags (updateservers, dlid) if enabled in manifest.xml + php ${MOKO_CLI}/manifest_licensing.php --path . --fix 2>/dev/null || true + + # Append suffix for output + if [ -n "$SUFFIX" ]; then + VERSION="${VERSION}${SUFFIX}" + fi + + # Commit version bump + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + git add -A + git diff --cached --quiet || { + git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]" + git push origin HEAD 2>&1 + } + + # Auto-detect element via manifest_element.php + php ${MOKO_CLI}/manifest_element.php \ + --path . --version "$VERSION" --stability "$STABILITY" \ + --repo "${GITEA_REPO}" --github-output + + # Read back element outputs + EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) + ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) + [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') + [ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip" + + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT" + echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT" + echo "tag=${TAG}" >> "$GITHUB_OUTPUT" + echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT" + echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT" + + echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ===" + + - name: Create release + id: release + run: | + TAG="${{ steps.meta.outputs.tag }}" + VERSION="${{ steps.meta.outputs.version }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php ${MOKO_CLI}/release_create.php \ + --path . --version "$VERSION" --tag "$TAG" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ + --repo "${GITEA_REPO}" --branch dev --prerelease + + - name: Update release notes from CHANGELOG.md + run: | + TAG="${{ steps.meta.outputs.tag }}" + VERSION="${{ steps.meta.outputs.version }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + + # Extract [Unreleased] section from changelog (everything between [Unreleased] and next ## heading) + if [ -f "CHANGELOG.md" ]; then + NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md) + [ -z "$NOTES" ] && NOTES="Release ${VERSION}" + else + NOTES="Release ${VERSION}" + fi + + # Update release body via API + RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \ + "${API_BASE}/releases/tags/${TAG}" | 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 ${{ secrets.MOKOGITEA_TOKEN }}', + 'Content-Type': 'application/json' + }) + urllib.request.urlopen(req) + " <<< "$NOTES" + echo "Release notes updated from CHANGELOG.md" + fi + + - name: Build package and upload + id: package + run: | + VERSION="${{ steps.meta.outputs.version }}" + TAG="${{ steps.meta.outputs.tag }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php ${MOKO_CLI}/release_package.php \ + --path . --version "$VERSION" --tag "$TAG" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ + --repo "${GITEA_REPO}" --output /tmp || true + + # updates.xml is generated dynamically by MokoGitea license server + # No need to build, commit, or sync updates.xml from workflows + + - name: "Delete lesser pre-release channels (cascade)" + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" + + php ${MOKO_CLI}/release_cascade.php \ + --stability "${{ steps.meta.outputs.stability }}" \ + --token "${TOKEN}" \ + --api-base "${API_BASE}" + + - name: Summary + if: always() + run: | + VERSION="${{ steps.meta.outputs.version }}" + STABILITY="${{ steps.meta.outputs.stability }}" + ZIP_NAME="${{ steps.meta.outputs.zip_name }}" + SHA256="${{ steps.package.outputs.sha256_zip }}" + echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY + echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY + echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY diff --git a/CHANGELOG.md b/CHANGELOG.md index 92962d7..83e555b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -<!-- VERSION: 01.00.01 --> +<!-- VERSION: 01.00.02 --> All notable changes to MokoJoomOpenGraph will be documented in this file. diff --git a/README.md b/README.md index b51c8a2..c42cd09 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MokoJoomOpenGraph -<!-- VERSION: 01.00.01 --> +<!-- VERSION: 01.00.02 --> Open Graph, Twitter Card, and social sharing meta tag management for Joomla 4/5/6. diff --git a/source/packages/com_mokoog/mokoog.xml b/source/packages/com_mokoog/mokoog.xml index 1bccaae..d3353b5 100644 --- a/source/packages/com_mokoog/mokoog.xml +++ b/source/packages/com_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="component" method="upgrade"> <name>com_mokoog</name> - <version>01.00.01-dev</version> + <version>01.00.02-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/source/packages/plg_content_mokoog/mokoog.xml b/source/packages/plg_content_mokoog/mokoog.xml index 1d1e2e1..0352b91 100644 --- a/source/packages/plg_content_mokoog/mokoog.xml +++ b/source/packages/plg_content_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="plugin" group="content" method="upgrade"> <name>Content - MokoJoomOpenGraph</name> - <version>01.00.01-dev</version> + <version>01.00.02-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/source/packages/plg_system_mokoog/mokoog.xml b/source/packages/plg_system_mokoog/mokoog.xml index 2b1246f..256f02d 100644 --- a/source/packages/plg_system_mokoog/mokoog.xml +++ b/source/packages/plg_system_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="plugin" group="system" method="upgrade"> <name>System - MokoJoomOpenGraph</name> - <version>01.00.01-dev</version> + <version>01.00.02-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/source/packages/plg_webservices_mokoog/mokoog.xml b/source/packages/plg_webservices_mokoog/mokoog.xml index 08008aa..86d7c1b 100644 --- a/source/packages/plg_webservices_mokoog/mokoog.xml +++ b/source/packages/plg_webservices_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="plugin" group="webservices" method="upgrade"> <name>Web Services - MokoJoomOpenGraph</name> - <version>01.00.01-dev</version> + <version>01.00.02-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/source/pkg_mokoog.xml b/source/pkg_mokoog.xml index 4f7a54a..c25f1dd 100644 --- a/source/pkg_mokoog.xml +++ b/source/pkg_mokoog.xml @@ -8,7 +8,7 @@ <extension type="package" method="upgrade"> <name>Package - MokoJoomOpenGraph</name> <packagename>mokoog</packagename> - <version>01.00.01-dev</version> + <version>01.00.02-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> From a67cd6da7634f0ca64602a25d9a05d95bb8ac7a8 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sat, 6 Jun 2026 22:20:08 -0500 Subject: [PATCH 112/132] fix: gitignore site/ should be /site/ to avoid matching tmpl/site/ --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 26927d2..8affbb6 100644 --- a/.gitignore +++ b/.gitignore @@ -113,7 +113,7 @@ releases/ build/ dist/ out/ -site/ +/site/ *.map *.css.map *.js.map From 64ac7796b789398676ba3f14063e28561b8971a6 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Thu, 11 Jun 2026 20:31:30 +0000 Subject: [PATCH 113/132] ci(pre-release): sync universal v05 workflow with chore/** branch trigger --- .mokogitea/workflows/pre-release.yml | 39 +++++++++++++++++----------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml index 86908c2..24c47e0 100644 --- a/.mokogitea/workflows/pre-release.yml +++ b/.mokogitea/workflows/pre-release.yml @@ -8,19 +8,22 @@ # REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform # PATH: /templates/workflows/universal/pre-release.yml.template # VERSION: 05.01.00 -# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch +# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches name: "Universal: Pre-Release" on: - pull_request: - types: [closed] + push: branches: - dev - pull_request_target: - types: [synchronize, opened, reopened] - branches: - - main + - 'fix/**' + - 'patch/**' + - 'hotfix/**' + - 'bugfix/**' + - 'chore/**' + - alpha + - beta + - rc workflow_dispatch: inputs: stability: @@ -43,12 +46,11 @@ env: jobs: build: - name: "Build Pre-Release (${{ inputs.stability || 'development' }})" + name: "Build Pre-Release (${{ inputs.stability || github.ref_name }})" runs-on: release if: >- github.event_name == 'workflow_dispatch' || - (github.event_name == 'pull_request' && github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev') || - (github.event_name == 'pull_request_target' && github.event.pull_request.base.ref == 'main') + github.event_name == 'push' steps: - name: Checkout @@ -56,7 +58,7 @@ jobs: with: fetch-depth: 0 token: ${{ secrets.MOKOGITEA_TOKEN }} - ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || '' }} + ref: ${{ github.ref_name }} - name: Setup moko-platform tools env: @@ -82,14 +84,21 @@ jobs: - name: Detect platform id: platform run: | + # Auto-detect and update platform if not set in manifest + php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true php ${MOKO_CLI}/manifest_read.php --path . --github-output - name: Resolve metadata and bump version id: meta run: | - # Auto-detect stability: RC for PRs targeting main, else use input or default to development - if [ "${{ github.event_name }}" = "pull_request_target" ] && [ "${{ github.event.pull_request.base.ref }}" = "main" ]; then - STABILITY="release-candidate" + # Auto-detect stability from branch name on push, or use input on dispatch + if [ "${{ github.event_name }}" = "push" ]; then + case "${{ github.ref_name }}" in + rc) STABILITY="release-candidate" ;; + alpha) STABILITY="alpha" ;; + beta) STABILITY="beta" ;; + *) STABILITY="development" ;; + esac else STABILITY="${{ inputs.stability || 'development' }}" fi @@ -164,7 +173,7 @@ jobs: php ${MOKO_CLI}/release_create.php \ --path . --version "$VERSION" --tag "$TAG" \ --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ - --repo "${GITEA_REPO}" --branch dev --prerelease + --repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease - name: Update release notes from CHANGELOG.md run: | From 2bf63f70ab5fe77d37981ea1fa301a27e2b23ece Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" <gitea-actions[bot]@mokoconsulting.tech> Date: Thu, 11 Jun 2026 20:32:40 +0000 Subject: [PATCH 114/132] chore(version): pre-release bump to 01.00.03-dev [skip ci] --- .mokogitea/manifest.xml | 2 +- CHANGELOG.md | 2 +- README.md | 2 +- source/packages/com_mokoog/mokoog.xml | 2 +- source/packages/plg_content_mokoog/mokoog.xml | 2 +- source/packages/plg_system_mokoog/mokoog.xml | 2 +- source/packages/plg_webservices_mokoog/mokoog.xml | 2 +- source/pkg_mokoog.xml | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.mokogitea/manifest.xml b/.mokogitea/manifest.xml index 58032f2..1066fff 100644 --- a/.mokogitea/manifest.xml +++ b/.mokogitea/manifest.xml @@ -9,7 +9,7 @@ <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.02</version> + <version>01.00.03</version> <license spdx="GPL-3.0-or-later">GNU General Public License v3</license> </identity> <governance> diff --git a/CHANGELOG.md b/CHANGELOG.md index 83e555b..1896f50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -<!-- VERSION: 01.00.02 --> +<!-- VERSION: 01.00.03 --> All notable changes to MokoJoomOpenGraph will be documented in this file. diff --git a/README.md b/README.md index c42cd09..0e0e9ba 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MokoJoomOpenGraph -<!-- VERSION: 01.00.02 --> +<!-- VERSION: 01.00.03 --> Open Graph, Twitter Card, and social sharing meta tag management for Joomla 4/5/6. diff --git a/source/packages/com_mokoog/mokoog.xml b/source/packages/com_mokoog/mokoog.xml index d3353b5..b7ed00d 100644 --- a/source/packages/com_mokoog/mokoog.xml +++ b/source/packages/com_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="component" method="upgrade"> <name>com_mokoog</name> - <version>01.00.02-dev</version> + <version>01.00.03-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/source/packages/plg_content_mokoog/mokoog.xml b/source/packages/plg_content_mokoog/mokoog.xml index 0352b91..69efc0d 100644 --- a/source/packages/plg_content_mokoog/mokoog.xml +++ b/source/packages/plg_content_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="plugin" group="content" method="upgrade"> <name>Content - MokoJoomOpenGraph</name> - <version>01.00.02-dev</version> + <version>01.00.03-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/source/packages/plg_system_mokoog/mokoog.xml b/source/packages/plg_system_mokoog/mokoog.xml index 256f02d..7f4b1bd 100644 --- a/source/packages/plg_system_mokoog/mokoog.xml +++ b/source/packages/plg_system_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="plugin" group="system" method="upgrade"> <name>System - MokoJoomOpenGraph</name> - <version>01.00.02-dev</version> + <version>01.00.03-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/source/packages/plg_webservices_mokoog/mokoog.xml b/source/packages/plg_webservices_mokoog/mokoog.xml index 86d7c1b..05db0a8 100644 --- a/source/packages/plg_webservices_mokoog/mokoog.xml +++ b/source/packages/plg_webservices_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="plugin" group="webservices" method="upgrade"> <name>Web Services - MokoJoomOpenGraph</name> - <version>01.00.02-dev</version> + <version>01.00.03-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/source/pkg_mokoog.xml b/source/pkg_mokoog.xml index c25f1dd..9ded126 100644 --- a/source/pkg_mokoog.xml +++ b/source/pkg_mokoog.xml @@ -8,7 +8,7 @@ <extension type="package" method="upgrade"> <name>Package - MokoJoomOpenGraph</name> <packagename>mokoog</packagename> - <version>01.00.02-dev</version> + <version>01.00.03-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> From 433ecfea71a4764610abf80aa8ec4bacff3eaded Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sun, 21 Jun 2026 10:02:03 -0500 Subject: [PATCH 115/132] 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 --- ISSUES.md | 318 ++++++++++++++++++ .../src/Controller/TagsController.php | 33 ++ .../src/Extension/MokoOG.php | 42 ++- 3 files changed, 380 insertions(+), 13 deletions(-) create mode 100644 ISSUES.md create mode 100644 source/packages/com_mokoog/src/Controller/TagsController.php diff --git a/ISSUES.md b/ISSUES.md new file mode 100644 index 0000000..a4d3679 --- /dev/null +++ b/ISSUES.md @@ -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 diff --git a/source/packages/com_mokoog/src/Controller/TagsController.php b/source/packages/com_mokoog/src/Controller/TagsController.php new file mode 100644 index 0000000..00d2000 --- /dev/null +++ b/source/packages/com_mokoog/src/Controller/TagsController.php @@ -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); + } +} diff --git a/source/packages/plg_system_mokoog/src/Extension/MokoOG.php b/source/packages/plg_system_mokoog/src/Extension/MokoOG.php index 5655dc8..d18c81d 100644 --- a/source/packages/plg_system_mokoog/src/Extension/MokoOG.php +++ b/source/packages/plg_system_mokoog/src/Extension/MokoOG.php @@ -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. @@ -253,11 +253,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 +329,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 +356,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(); } @@ -534,7 +550,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,7 +571,7 @@ 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) { From 0afc8b135a6a62cc63865591dca37ad13b0ac299 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sun, 21 Jun 2026 10:12:00 -0500 Subject: [PATCH 116/132] fix: replace GD error suppression with logging, remove dead adapters (#49, #36) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- .../src/ContentType/ContentTypeInterface.php | 60 -------------- .../src/ContentType/HikaShopAdapter.php | 76 ----------------- .../com_mokoog/src/ContentType/K2Adapter.php | 73 ----------------- .../src/ContentType/VirtueMartAdapter.php | 82 ------------------- .../src/Helper/ImageGenerator.php | 23 +++++- .../src/Helper/ImageHelper.php | 19 +++-- 6 files changed, 32 insertions(+), 301 deletions(-) delete mode 100644 source/packages/com_mokoog/src/ContentType/ContentTypeInterface.php delete mode 100644 source/packages/com_mokoog/src/ContentType/HikaShopAdapter.php delete mode 100644 source/packages/com_mokoog/src/ContentType/K2Adapter.php delete mode 100644 source/packages/com_mokoog/src/ContentType/VirtueMartAdapter.php diff --git a/source/packages/com_mokoog/src/ContentType/ContentTypeInterface.php b/source/packages/com_mokoog/src/ContentType/ContentTypeInterface.php deleted file mode 100644 index 6689aad..0000000 --- a/source/packages/com_mokoog/src/ContentType/ContentTypeInterface.php +++ /dev/null @@ -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; -} diff --git a/source/packages/com_mokoog/src/ContentType/HikaShopAdapter.php b/source/packages/com_mokoog/src/ContentType/HikaShopAdapter.php deleted file mode 100644 index 87f9b2f..0000000 --- a/source/packages/com_mokoog/src/ContentType/HikaShopAdapter.php +++ /dev/null @@ -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() ?: ''; - } -} diff --git a/source/packages/com_mokoog/src/ContentType/K2Adapter.php b/source/packages/com_mokoog/src/ContentType/K2Adapter.php deleted file mode 100644 index 028d0c4..0000000 --- a/source/packages/com_mokoog/src/ContentType/K2Adapter.php +++ /dev/null @@ -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 ''; - } -} diff --git a/source/packages/com_mokoog/src/ContentType/VirtueMartAdapter.php b/source/packages/com_mokoog/src/ContentType/VirtueMartAdapter.php deleted file mode 100644 index d68a03d..0000000 --- a/source/packages/com_mokoog/src/ContentType/VirtueMartAdapter.php +++ /dev/null @@ -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)); - } -} diff --git a/source/packages/plg_system_mokoog/src/Helper/ImageGenerator.php b/source/packages/plg_system_mokoog/src/Helper/ImageGenerator.php index 9f94980..4b35818 100644 --- a/source/packages/plg_system_mokoog/src/Helper/ImageGenerator.php +++ b/source/packages/plg_system_mokoog/src/Helper/ImageGenerator.php @@ -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,13 +41,23 @@ 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 ''; } @@ -70,17 +81,21 @@ class ImageGenerator $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 ''; } diff --git a/source/packages/plg_system_mokoog/src/Helper/ImageHelper.php b/source/packages/plg_system_mokoog/src/Helper/ImageHelper.php index 3ea0325..a5f1386 100644 --- a/source/packages/plg_system_mokoog/src/Helper/ImageHelper.php +++ b/source/packages/plg_system_mokoog/src/Helper/ImageHelper.php @@ -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 { @@ -211,12 +212,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; } } From 8793e6b3f4b494573541d6468f1cd89676ecb951 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sun, 21 Jun 2026 10:20:38 -0500 Subject: [PATCH 117/132] 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 --- .../src/Extension/MokoOG.php | 66 ++++++++++++++- .../src/Helper/JsonLdBuilder.php | 82 ++++++++++++++++++- 2 files changed, 145 insertions(+), 3 deletions(-) diff --git a/source/packages/plg_system_mokoog/src/Extension/MokoOG.php b/source/packages/plg_system_mokoog/src/Extension/MokoOG.php index d18c81d..37a3321 100644 --- a/source/packages/plg_system_mokoog/src/Extension/MokoOG.php +++ b/source/packages/plg_system_mokoog/src/Extension/MokoOG.php @@ -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,7 +217,9 @@ 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) { + if ($option === 'com_mokoshop' && $view === 'product' && $id > 0) { + $schema = JsonLdBuilder::buildProduct($id, $title, $description, $imageUrl); + } elseif ($option === 'com_content' && $view === 'article' && $id > 0) { $schema = JsonLdBuilder::buildArticle($id, $title, $description, $imageUrl); } else { $schema = JsonLdBuilder::buildWebPage($title, $description); @@ -414,6 +427,25 @@ 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(); @@ -588,6 +620,36 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface * * @return array{0: int, 1: int}|null */ + /** + * Load MokoSuiteShop product data by product ID. + * + * @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]; + } + + $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(); + + return $cache[$productId]; + } + private function getImageDimensions(string $image): ?array { // Cannot determine dimensions for external URLs diff --git a/source/packages/plg_system_mokoog/src/Helper/JsonLdBuilder.php b/source/packages/plg_system_mokoog/src/Helper/JsonLdBuilder.php index b9cf789..5d1b448 100644 --- a/source/packages/plg_system_mokoog/src/Helper/JsonLdBuilder.php +++ b/source/packages/plg_system_mokoog/src/Helper/JsonLdBuilder.php @@ -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. @@ -152,6 +152,86 @@ 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) + * + * @return array|null + */ + public static function buildProduct(int $productId, string $title, string $description, string $image): ?array + { + if ($productId <= 0) { + return null; + } + + $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 + $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, + ]; + } + + return $schema; + } + /** * Encode a schema array to a JSON-LD script tag string. * From f4846753002a94498e2b44cb1c3f3c0678d914af Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sun, 21 Jun 2026 10:57:38 -0500 Subject: [PATCH 118/132] 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 --- .../src/Controller/BatchController.php | 4 +- .../src/Controller/ImportExportController.php | 16 ++++- .../com_mokoog/src/Table/TagTable.php | 58 ++++++++++++++++++- 3 files changed, 72 insertions(+), 6 deletions(-) diff --git a/source/packages/com_mokoog/src/Controller/BatchController.php b/source/packages/com_mokoog/src/Controller/BatchController.php index 1f86892..468984b 100644 --- a/source/packages/com_mokoog/src/Controller/BatchController.php +++ b/source/packages/com_mokoog/src/Controller/BatchController.php @@ -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) diff --git a/source/packages/com_mokoog/src/Controller/ImportExportController.php b/source/packages/com_mokoog/src/Controller/ImportExportController.php index b135575..5fcb49f 100644 --- a/source/packages/com_mokoog/src/Controller/ImportExportController.php +++ b/source/packages/com_mokoog/src/Controller/ImportExportController.php @@ -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, ]; diff --git a/source/packages/com_mokoog/src/Table/TagTable.php b/source/packages/com_mokoog/src/Table/TagTable.php index 29d68e4..af85bac 100644 --- a/source/packages/com_mokoog/src/Table/TagTable.php +++ b/source/packages/com_mokoog/src/Table/TagTable.php @@ -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; } } From 7a7041c7f3f241e34c9d892848ee4981e247c6f2 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sun, 21 Jun 2026 11:01:32 -0500 Subject: [PATCH 119/132] fix: remove updateservers from package manifest (#44) Update server is managed externally, not via static updates.xml. --- source/pkg_mokoog.xml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/source/pkg_mokoog.xml b/source/pkg_mokoog.xml index 4f7a54a..78c5668 100644 --- a/source/pkg_mokoog.xml +++ b/source/pkg_mokoog.xml @@ -1,12 +1,12 @@ <?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.01-dev</version> <creationDate>2026-05-23</creationDate> @@ -30,9 +30,6 @@ <language tag="en-GB">language/en-GB/pkg_mokoog.sys.ini</language> </languages> - <updateservers> - <server type="extension" name="MokoJoomOpenGraph Updates">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/updates.xml</server> - </updateservers> <dlid prefix="dlid=" suffix=""/> <blockChildUninstall>true</blockChildUninstall> </extension> From ca06c863282059a5e98ca76422fc0fcf2ce6dbdc Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sun, 21 Jun 2026 11:09:52 -0500 Subject: [PATCH 120/132] 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 --- .../src/Extension/MokoOG.php | 69 +++++++++++-------- .../src/Helper/JsonLdBuilder.php | 43 +++++++----- 2 files changed, 65 insertions(+), 47 deletions(-) diff --git a/source/packages/plg_system_mokoog/src/Extension/MokoOG.php b/source/packages/plg_system_mokoog/src/Extension/MokoOG.php index 37a3321..5418449 100644 --- a/source/packages/plg_system_mokoog/src/Extension/MokoOG.php +++ b/source/packages/plg_system_mokoog/src/Extension/MokoOG.php @@ -220,7 +220,7 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface if ($option === 'com_mokoshop' && $view === 'product' && $id > 0) { $schema = JsonLdBuilder::buildProduct($id, $title, $description, $imageUrl); } elseif ($option === 'com_content' && $view === 'article' && $id > 0) { - $schema = JsonLdBuilder::buildArticle($id, $title, $description, $imageUrl); + $schema = JsonLdBuilder::buildArticle($id, $title, $description, $imageUrl, $this->loadArticle($id)); } else { $schema = JsonLdBuilder::buildWebPage($title, $description); } @@ -448,17 +448,10 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface // 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']; @@ -514,6 +507,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. * @@ -524,16 +549,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 ?? ''; } /** @@ -545,16 +563,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 ?? ''; } /** diff --git a/source/packages/plg_system_mokoog/src/Helper/JsonLdBuilder.php b/source/packages/plg_system_mokoog/src/Helper/JsonLdBuilder.php index 5d1b448..9c92d85 100644 --- a/source/packages/plg_system_mokoog/src/Helper/JsonLdBuilder.php +++ b/source/packages/plg_system_mokoog/src/Helper/JsonLdBuilder.php @@ -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, ]; } From 7fd716f3a49f5adf86a98142b821612fa8ab51b5 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sun, 21 Jun 2026 15:23:30 -0500 Subject: [PATCH 121/132] chore: normalize workflow line endings for merge --- .mokogitea/workflows/auto-release.yml | 648 +++++++++++++------------- .mokogitea/workflows/pre-release.yml | 486 +++++++++---------- 2 files changed, 567 insertions(+), 567 deletions(-) diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml index ca40435..b657b98 100644 --- a/.mokogitea/workflows/auto-release.yml +++ b/.mokogitea/workflows/auto-release.yml @@ -1,324 +1,324 @@ -# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: moko-platform.Release -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform -# PATH: /templates/workflows/universal/auto-release.yml.template -# VERSION: 05.00.00 -# BRIEF: Universal build & release � detects platform from manifest.xml -# -# +========================================================================+ -# | UNIVERSAL BUILD & RELEASE PIPELINE | -# +========================================================================+ -# | | -# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. | -# | | -# | Platform-specific: | -# | joomla: XML manifest, type-prefixed packages | -# | dolibarr: mod*.class.php, update.txt, dev version reset | -# | generic: README-only, no update stream | -# | | -# +========================================================================+ - -name: "Universal: Build & Release" - -on: - pull_request: - types: [opened, closed] - branches: - - main - workflow_dispatch: - inputs: - action: - description: 'Action to perform' - required: false - type: choice - default: release - options: - - release - - promote-rc - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} - GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} - -permissions: - contents: write - -jobs: - # ── PR Opened → Rename branch to RC and build RC release ───────────────────── - promote-rc: - name: Promote to RC - runs-on: release - if: >- - (github.event.action == 'opened' && github.event.pull_request.merged != true) || - (github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc') - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - token: ${{ secrets.MOKOGITEA_TOKEN }} - fetch-depth: 1 - - - name: Setup moko-platform tools - env: - MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting - run: | - if [ -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 - 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 - fi - - - name: Rename branch to rc - run: | - php ${MOKO_CLI}/branch_rename.php \ - --from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" \ - --api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \ - --pr "${{ github.event.pull_request.number }}" - - - name: Checkout rc and configure git - run: | - git fetch origin rc - git checkout rc - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" - - - name: Publish RC release - run: | - php ${MOKO_CLI}/release_publish.php \ - --path . --stability rc --bump minor --branch rc \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" - - - name: Summary - if: always() - run: | - echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY - echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY - - # ── Merged PR → Build & Release (or promote RC to stable) ──────────────────── - release: - name: Build & Release Pipeline - runs-on: release - if: >- - github.event.pull_request.merged == true || - (github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc') - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - token: ${{ secrets.MOKOGITEA_TOKEN }} - fetch-depth: 0 - - - name: Configure git for bot pushes - run: | - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" - - - name: Check for merge conflict markers - run: | - CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true) - if [ -n "$CONFLICTS" ]; then - echo "::error::Merge conflict markers found — aborting release" - echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - exit 1 - fi - echo "No conflict markers found" - - - name: Setup moko-platform tools - env: - MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}' - run: | - 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 - 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 - fi - - - name: "Publish stable release" - run: | - php ${MOKO_CLI}/release_publish.php \ - --path . --stability stable --bump minor --branch main \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" - - - name: Update release notes from CHANGELOG.md - 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" - else - NOTES="Stable release" - 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) - - 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 ${{ secrets.MOKOGITEA_TOKEN }}', - 'Content-Type': 'application/json' - }) - urllib.request.urlopen(req) - " <<< "$NOTES" - echo "Release notes updated from CHANGELOG.md" - fi - - # -- STEP 9: Mirror to GitHub (stable only) -------------------------------- - - name: "Step 9: Mirror release to GitHub" - if: >- - steps.version.outputs.skip != 'true' && - secrets.GH_MIRROR_TOKEN != '' - continue-on-error: true - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - RELEASE_TAG="${{ steps.version.outputs.release_tag }}" - GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php ${MOKO_CLI}/release_mirror.php \ - --version "$VERSION" --tag "$RELEASE_TAG" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ - --gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \ - --branch main 2>&1 || true - echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY - - # -- STEP 10: Sync main branch to GitHub mirror ---------------------------- - - name: "Step 10: Push main to GitHub mirror" - if: >- - steps.version.outputs.skip != 'true' && - secrets.GH_MIRROR_TOKEN != '' - continue-on-error: true - run: | - GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" - GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1) - GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2) - git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \ - git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" - git fetch origin main --depth=1 - git push github origin/main:refs/heads/main --force 2>/dev/null \ - && echo "main branch pushed to GitHub mirror" \ - || echo "WARNING: GitHub mirror push failed" - - - name: "Step 11: Delete rc branch and recreate dev from main" - if: steps.version.outputs.skip != 'true' - continue-on-error: true - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" - - # Delete rc branch (ephemeral — created by promote-rc) - curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ - "${API_BASE}/branches/rc" 2>/dev/null \ - && echo "Deleted rc branch" || echo "rc branch not found" - - # Delete dev branch - curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ - "${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch" - - # Recreate dev from main (now includes version bump + changelog promotion) - curl -sf -X POST -H "Authorization: token ${TOKEN}" \ - -H "Content-Type: application/json" \ - "${API_BASE}/branches" \ - -d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main" - - echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY - - - name: "Step 12: Create version branch from main" - if: steps.version.outputs.skip != 'true' - continue-on-error: true - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - BRANCH_NAME="version/${VERSION}" - MAIN_SHA=$(git rev-parse HEAD) - - # Delete old version branch if it exists (same version re-release) - curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}" - - # Create version/XX.YY.ZZ from main - curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed" - - echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY - - - - # -- Dolibarr post-release: Reset dev version ----------------------------- - - name: "Post-release: Reset dev version" - if: steps.version.outputs.skip != 'true' - continue-on-error: true - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php ${MOKO_CLI}/version_reset_dev.php \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \ - --branch dev --path . 2>&1 || true - - # -- Summary -------------------------------------------------------------- - - name: Pipeline Summary - if: always() - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - PLATFORM="${{ steps.platform.outputs.platform }}" - if [ "${{ steps.version.outputs.skip }}" = "true" ]; then - echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY - echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY - elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then - echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY - else - echo "" >> $GITHUB_STEP_SUMMARY - echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY - echo "|------|--------|" >> $GITHUB_STEP_SUMMARY - echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY - fi +# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: moko-platform.Release +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform +# PATH: /templates/workflows/universal/auto-release.yml.template +# VERSION: 05.00.00 +# BRIEF: Universal build & release � detects platform from manifest.xml +# +# +========================================================================+ +# | UNIVERSAL BUILD & RELEASE PIPELINE | +# +========================================================================+ +# | | +# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. | +# | | +# | Platform-specific: | +# | joomla: XML manifest, type-prefixed packages | +# | dolibarr: mod*.class.php, update.txt, dev version reset | +# | generic: README-only, no update stream | +# | | +# +========================================================================+ + +name: "Universal: Build & Release" + +on: + pull_request: + types: [opened, closed] + branches: + - main + workflow_dispatch: + inputs: + action: + description: 'Action to perform' + required: false + type: choice + default: release + options: + - release + - promote-rc + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + +permissions: + contents: write + +jobs: + # ── PR Opened → Rename branch to RC and build RC release ───────────────────── + promote-rc: + name: Promote to RC + runs-on: release + if: >- + (github.event.action == 'opened' && github.event.pull_request.merged != true) || + (github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc') + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.MOKOGITEA_TOKEN }} + fetch-depth: 1 + + - name: Setup moko-platform tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + run: | + if [ -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 + 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 + fi + + - name: Rename branch to rc + run: | + php ${MOKO_CLI}/branch_rename.php \ + --from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" \ + --api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \ + --pr "${{ github.event.pull_request.number }}" + + - name: Checkout rc and configure git + run: | + git fetch origin rc + git checkout rc + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + + - name: Publish RC release + run: | + php ${MOKO_CLI}/release_publish.php \ + --path . --stability rc --bump minor --branch rc \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" + + - name: Summary + if: always() + run: | + echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY + echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY + + # ── Merged PR → Build & Release (or promote RC to stable) ──────────────────── + release: + name: Build & Release Pipeline + runs-on: release + if: >- + github.event.pull_request.merged == true || + (github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc') + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.MOKOGITEA_TOKEN }} + fetch-depth: 0 + + - name: Configure git for bot pushes + run: | + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + + - name: Check for merge conflict markers + run: | + CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true) + if [ -n "$CONFLICTS" ]; then + echo "::error::Merge conflict markers found — aborting release" + echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + exit 1 + fi + echo "No conflict markers found" + + - name: Setup moko-platform tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}' + run: | + 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 + 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 + fi + + - name: "Publish stable release" + run: | + php ${MOKO_CLI}/release_publish.php \ + --path . --stability stable --bump minor --branch main \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" + + - name: Update release notes from CHANGELOG.md + 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" + else + NOTES="Stable release" + 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) + + 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 ${{ secrets.MOKOGITEA_TOKEN }}', + 'Content-Type': 'application/json' + }) + urllib.request.urlopen(req) + " <<< "$NOTES" + echo "Release notes updated from CHANGELOG.md" + fi + + # -- STEP 9: Mirror to GitHub (stable only) -------------------------------- + - name: "Step 9: Mirror release to GitHub" + if: >- + steps.version.outputs.skip != 'true' && + secrets.GH_MIRROR_TOKEN != '' + continue-on-error: true + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php ${MOKO_CLI}/release_mirror.php \ + --version "$VERSION" --tag "$RELEASE_TAG" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ + --gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \ + --branch main 2>&1 || true + echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY + + # -- STEP 10: Sync main branch to GitHub mirror ---------------------------- + - name: "Step 10: Push main to GitHub mirror" + if: >- + steps.version.outputs.skip != 'true' && + secrets.GH_MIRROR_TOKEN != '' + continue-on-error: true + run: | + GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" + GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1) + GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2) + git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \ + git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" + git fetch origin main --depth=1 + git push github origin/main:refs/heads/main --force 2>/dev/null \ + && echo "main branch pushed to GitHub mirror" \ + || echo "WARNING: GitHub mirror push failed" + + - name: "Step 11: Delete rc branch and recreate dev from main" + if: steps.version.outputs.skip != 'true' + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" + + # Delete rc branch (ephemeral — created by promote-rc) + curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/branches/rc" 2>/dev/null \ + && echo "Deleted rc branch" || echo "rc branch not found" + + # Delete dev branch + curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch" + + # Recreate dev from main (now includes version bump + changelog promotion) + curl -sf -X POST -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/json" \ + "${API_BASE}/branches" \ + -d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main" + + echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY + + - name: "Step 12: Create version branch from main" + if: steps.version.outputs.skip != 'true' + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + BRANCH_NAME="version/${VERSION}" + MAIN_SHA=$(git rev-parse HEAD) + + # Delete old version branch if it exists (same version re-release) + curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}" + + # Create version/XX.YY.ZZ from main + curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed" + + echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY + + + + # -- Dolibarr post-release: Reset dev version ----------------------------- + - name: "Post-release: Reset dev version" + if: steps.version.outputs.skip != 'true' + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php ${MOKO_CLI}/version_reset_dev.php \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \ + --branch dev --path . 2>&1 || true + + # -- Summary -------------------------------------------------------------- + - name: Pipeline Summary + if: always() + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + PLATFORM="${{ steps.platform.outputs.platform }}" + if [ "${{ steps.version.outputs.skip }}" = "true" ]; then + echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY + echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY + elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then + echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY + echo "|------|--------|" >> $GITHUB_STEP_SUMMARY + echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY + fi diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml index 9615a4e..86908c2 100644 --- a/.mokogitea/workflows/pre-release.yml +++ b/.mokogitea/workflows/pre-release.yml @@ -1,243 +1,243 @@ -# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: moko-platform.Release -# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform -# PATH: /templates/workflows/universal/pre-release.yml.template -# VERSION: 05.01.00 -# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch - -name: "Universal: Pre-Release" - -on: - pull_request: - types: [closed] - branches: - - dev - pull_request_target: - types: [synchronize, opened, reopened] - branches: - - main - workflow_dispatch: - inputs: - stability: - description: 'Pre-release channel' - required: true - type: choice - options: - - development - - alpha - - beta - - release-candidate - -permissions: - contents: write - -env: - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} - GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} - -jobs: - build: - name: "Build Pre-Release (${{ inputs.stability || 'development' }})" - runs-on: release - if: >- - github.event_name == 'workflow_dispatch' || - (github.event_name == 'pull_request' && github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev') || - (github.event_name == 'pull_request_target' && github.event.pull_request.base.ref == 'main') - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - token: ${{ secrets.MOKOGITEA_TOKEN }} - ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || '' }} - - - name: Setup moko-platform 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 - 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 - fi - - - name: Detect platform - id: platform - run: | - php ${MOKO_CLI}/manifest_read.php --path . --github-output - - - name: Resolve metadata and bump version - id: meta - run: | - # Auto-detect stability: RC for PRs targeting main, else use input or default to development - if [ "${{ github.event_name }}" = "pull_request_target" ] && [ "${{ github.event.pull_request.base.ref }}" = "main" ]; then - STABILITY="release-candidate" - else - STABILITY="${{ inputs.stability || 'development' }}" - fi - - case "$STABILITY" in - development) SUFFIX="-dev"; TAG="development" ;; - alpha) SUFFIX="-alpha"; TAG="alpha" ;; - beta) SUFFIX="-beta"; TAG="beta" ;; - release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;; - esac - - # Bump version via CLI: patch for dev/alpha/beta, minor for RC - case "$STABILITY" in - release-candidate) BUMP="minor" ;; - *) BUMP="patch" ;; - esac - - php ${MOKO_CLI}/version_bump.php --path . $([ "$BUMP" = "minor" ] && echo "--minor") 2>/dev/null || true - - # Set stability suffix and verify consistency - VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "00.00.01") - VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//') - - php ${MOKO_CLI}/version_set_platform.php \ - --path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true - php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true - - # Ensure licensing tags (updateservers, dlid) if enabled in manifest.xml - php ${MOKO_CLI}/manifest_licensing.php --path . --fix 2>/dev/null || true - - # Append suffix for output - if [ -n "$SUFFIX" ]; then - VERSION="${VERSION}${SUFFIX}" - fi - - # Commit version bump - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" - git add -A - git diff --cached --quiet || { - git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]" - git push origin HEAD 2>&1 - } - - # Auto-detect element via manifest_element.php - php ${MOKO_CLI}/manifest_element.php \ - --path . --version "$VERSION" --stability "$STABILITY" \ - --repo "${GITEA_REPO}" --github-output - - # Read back element outputs - EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) - ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) - [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') - [ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip" - - echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT" - echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT" - echo "tag=${TAG}" >> "$GITHUB_OUTPUT" - echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT" - echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT" - - echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ===" - - - name: Create release - id: release - run: | - TAG="${{ steps.meta.outputs.tag }}" - VERSION="${{ steps.meta.outputs.version }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php ${MOKO_CLI}/release_create.php \ - --path . --version "$VERSION" --tag "$TAG" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ - --repo "${GITEA_REPO}" --branch dev --prerelease - - - name: Update release notes from CHANGELOG.md - run: | - TAG="${{ steps.meta.outputs.tag }}" - VERSION="${{ steps.meta.outputs.version }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - - # Extract [Unreleased] section from changelog (everything between [Unreleased] and next ## heading) - if [ -f "CHANGELOG.md" ]; then - NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md) - [ -z "$NOTES" ] && NOTES="Release ${VERSION}" - else - NOTES="Release ${VERSION}" - fi - - # Update release body via API - RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \ - "${API_BASE}/releases/tags/${TAG}" | 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 ${{ secrets.MOKOGITEA_TOKEN }}', - 'Content-Type': 'application/json' - }) - urllib.request.urlopen(req) - " <<< "$NOTES" - echo "Release notes updated from CHANGELOG.md" - fi - - - name: Build package and upload - id: package - run: | - VERSION="${{ steps.meta.outputs.version }}" - TAG="${{ steps.meta.outputs.tag }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php ${MOKO_CLI}/release_package.php \ - --path . --version "$VERSION" --tag "$TAG" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ - --repo "${GITEA_REPO}" --output /tmp || true - - # updates.xml is generated dynamically by MokoGitea license server - # No need to build, commit, or sync updates.xml from workflows - - - name: "Delete lesser pre-release channels (cascade)" - continue-on-error: true - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" - - php ${MOKO_CLI}/release_cascade.php \ - --stability "${{ steps.meta.outputs.stability }}" \ - --token "${TOKEN}" \ - --api-base "${API_BASE}" - - - name: Summary - if: always() - run: | - VERSION="${{ steps.meta.outputs.version }}" - STABILITY="${{ steps.meta.outputs.stability }}" - ZIP_NAME="${{ steps.meta.outputs.zip_name }}" - SHA256="${{ steps.package.outputs.sha256_zip }}" - echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY - echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY - echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY - echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY - echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY +# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: moko-platform.Release +# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform +# PATH: /templates/workflows/universal/pre-release.yml.template +# VERSION: 05.01.00 +# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch + +name: "Universal: Pre-Release" + +on: + pull_request: + types: [closed] + branches: + - dev + pull_request_target: + types: [synchronize, opened, reopened] + branches: + - main + workflow_dispatch: + inputs: + stability: + description: 'Pre-release channel' + required: true + type: choice + options: + - development + - alpha + - beta + - release-candidate + +permissions: + contents: write + +env: + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + +jobs: + build: + name: "Build Pre-Release (${{ inputs.stability || 'development' }})" + runs-on: release + if: >- + github.event_name == 'workflow_dispatch' || + (github.event_name == 'pull_request' && github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev') || + (github.event_name == 'pull_request_target' && github.event.pull_request.base.ref == 'main') + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.MOKOGITEA_TOKEN }} + ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || '' }} + + - name: Setup moko-platform 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 + 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 + fi + + - name: Detect platform + id: platform + run: | + php ${MOKO_CLI}/manifest_read.php --path . --github-output + + - name: Resolve metadata and bump version + id: meta + run: | + # Auto-detect stability: RC for PRs targeting main, else use input or default to development + if [ "${{ github.event_name }}" = "pull_request_target" ] && [ "${{ github.event.pull_request.base.ref }}" = "main" ]; then + STABILITY="release-candidate" + else + STABILITY="${{ inputs.stability || 'development' }}" + fi + + case "$STABILITY" in + development) SUFFIX="-dev"; TAG="development" ;; + alpha) SUFFIX="-alpha"; TAG="alpha" ;; + beta) SUFFIX="-beta"; TAG="beta" ;; + release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;; + esac + + # Bump version via CLI: patch for dev/alpha/beta, minor for RC + case "$STABILITY" in + release-candidate) BUMP="minor" ;; + *) BUMP="patch" ;; + esac + + php ${MOKO_CLI}/version_bump.php --path . $([ "$BUMP" = "minor" ] && echo "--minor") 2>/dev/null || true + + # Set stability suffix and verify consistency + VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "00.00.01") + VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//') + + php ${MOKO_CLI}/version_set_platform.php \ + --path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true + php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true + + # Ensure licensing tags (updateservers, dlid) if enabled in manifest.xml + php ${MOKO_CLI}/manifest_licensing.php --path . --fix 2>/dev/null || true + + # Append suffix for output + if [ -n "$SUFFIX" ]; then + VERSION="${VERSION}${SUFFIX}" + fi + + # Commit version bump + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + git add -A + git diff --cached --quiet || { + git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]" + git push origin HEAD 2>&1 + } + + # Auto-detect element via manifest_element.php + php ${MOKO_CLI}/manifest_element.php \ + --path . --version "$VERSION" --stability "$STABILITY" \ + --repo "${GITEA_REPO}" --github-output + + # Read back element outputs + EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) + ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) + [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') + [ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip" + + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT" + echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT" + echo "tag=${TAG}" >> "$GITHUB_OUTPUT" + echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT" + echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT" + + echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ===" + + - name: Create release + id: release + run: | + TAG="${{ steps.meta.outputs.tag }}" + VERSION="${{ steps.meta.outputs.version }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php ${MOKO_CLI}/release_create.php \ + --path . --version "$VERSION" --tag "$TAG" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ + --repo "${GITEA_REPO}" --branch dev --prerelease + + - name: Update release notes from CHANGELOG.md + run: | + TAG="${{ steps.meta.outputs.tag }}" + VERSION="${{ steps.meta.outputs.version }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + + # Extract [Unreleased] section from changelog (everything between [Unreleased] and next ## heading) + if [ -f "CHANGELOG.md" ]; then + NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md) + [ -z "$NOTES" ] && NOTES="Release ${VERSION}" + else + NOTES="Release ${VERSION}" + fi + + # Update release body via API + RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \ + "${API_BASE}/releases/tags/${TAG}" | 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 ${{ secrets.MOKOGITEA_TOKEN }}', + 'Content-Type': 'application/json' + }) + urllib.request.urlopen(req) + " <<< "$NOTES" + echo "Release notes updated from CHANGELOG.md" + fi + + - name: Build package and upload + id: package + run: | + VERSION="${{ steps.meta.outputs.version }}" + TAG="${{ steps.meta.outputs.tag }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php ${MOKO_CLI}/release_package.php \ + --path . --version "$VERSION" --tag "$TAG" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ + --repo "${GITEA_REPO}" --output /tmp || true + + # updates.xml is generated dynamically by MokoGitea license server + # No need to build, commit, or sync updates.xml from workflows + + - name: "Delete lesser pre-release channels (cascade)" + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" + + php ${MOKO_CLI}/release_cascade.php \ + --stability "${{ steps.meta.outputs.stability }}" \ + --token "${TOKEN}" \ + --api-base "${API_BASE}" + + - name: Summary + if: always() + run: | + VERSION="${{ steps.meta.outputs.version }}" + STABILITY="${{ steps.meta.outputs.stability }}" + ZIP_NAME="${{ steps.meta.outputs.zip_name }}" + SHA256="${{ steps.package.outputs.sha256_zip }}" + echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY + echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY + echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY From 0e6137b0643fc8c9adde5c28ea761fbde6d6f196 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" <gitea-actions[bot]@mokoconsulting.tech> Date: Sun, 21 Jun 2026 20:27:36 +0000 Subject: [PATCH 122/132] chore(version): pre-release bump to 01.00.04-dev [skip ci] --- .mokogitea/manifest.xml | 2 +- CHANGELOG.md | 2 +- README.md | 2 +- source/packages/com_mokoog/mokoog.xml | 2 +- source/packages/plg_content_mokoog/mokoog.xml | 2 +- source/packages/plg_system_mokoog/mokoog.xml | 2 +- source/packages/plg_webservices_mokoog/mokoog.xml | 2 +- source/pkg_mokoog.xml | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.mokogitea/manifest.xml b/.mokogitea/manifest.xml index 1066fff..92e303c 100644 --- a/.mokogitea/manifest.xml +++ b/.mokogitea/manifest.xml @@ -9,7 +9,7 @@ <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> + <version>01.00.04</version> <license spdx="GPL-3.0-or-later">GNU General Public License v3</license> </identity> <governance> diff --git a/CHANGELOG.md b/CHANGELOG.md index 1896f50..7494401 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -<!-- VERSION: 01.00.03 --> +<!-- VERSION: 01.00.04 --> All notable changes to MokoJoomOpenGraph will be documented in this file. diff --git a/README.md b/README.md index 0e0e9ba..ee39954 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MokoJoomOpenGraph -<!-- VERSION: 01.00.03 --> +<!-- VERSION: 01.00.04 --> Open Graph, Twitter Card, and social sharing meta tag management for Joomla 4/5/6. diff --git a/source/packages/com_mokoog/mokoog.xml b/source/packages/com_mokoog/mokoog.xml index b7ed00d..43a6095 100644 --- a/source/packages/com_mokoog/mokoog.xml +++ b/source/packages/com_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="component" method="upgrade"> <name>com_mokoog</name> - <version>01.00.03-dev</version> + <version>01.00.04-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/source/packages/plg_content_mokoog/mokoog.xml b/source/packages/plg_content_mokoog/mokoog.xml index 69efc0d..1df7e8c 100644 --- a/source/packages/plg_content_mokoog/mokoog.xml +++ b/source/packages/plg_content_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="plugin" group="content" method="upgrade"> <name>Content - MokoJoomOpenGraph</name> - <version>01.00.03-dev</version> + <version>01.00.04-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/source/packages/plg_system_mokoog/mokoog.xml b/source/packages/plg_system_mokoog/mokoog.xml index 7f4b1bd..3a5e0e6 100644 --- a/source/packages/plg_system_mokoog/mokoog.xml +++ b/source/packages/plg_system_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="plugin" group="system" method="upgrade"> <name>System - MokoJoomOpenGraph</name> - <version>01.00.03-dev</version> + <version>01.00.04-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/source/packages/plg_webservices_mokoog/mokoog.xml b/source/packages/plg_webservices_mokoog/mokoog.xml index 05db0a8..2ae002a 100644 --- a/source/packages/plg_webservices_mokoog/mokoog.xml +++ b/source/packages/plg_webservices_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="plugin" group="webservices" method="upgrade"> <name>Web Services - MokoJoomOpenGraph</name> - <version>01.00.03-dev</version> + <version>01.00.04-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/source/pkg_mokoog.xml b/source/pkg_mokoog.xml index 10987d5..82de8e8 100644 --- a/source/pkg_mokoog.xml +++ b/source/pkg_mokoog.xml @@ -8,7 +8,7 @@ <extension type="package" method="upgrade"> <name>Package - MokoSuiteOpenGraph</name> <packagename>mokoog</packagename> - <version>01.00.03-dev</version> + <version>01.00.04-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> From 1375c5820eb49c78b6a933448a031689b3195c1a Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sun, 21 Jun 2026 15:31:18 -0500 Subject: [PATCH 123/132] 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 --- CHANGELOG.md | 20 ++++++++++++++++---- README.md | 12 ++++++------ 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1896f50..fa77da0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ <!-- VERSION: 01.00.03 --> -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 diff --git a/README.md b/README.md index 0e0e9ba..12e3480 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# MokoJoomOpenGraph +# MokoSuiteOpenGraph <!-- VERSION: 01.00.03 --> @@ -6,7 +6,7 @@ Open Graph, Twitter Card, and social sharing meta tag management for Joomla 4/5/ ## 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 From 3d2d91ace54541c654a1ac8088ebd9345164de18 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" <gitea-actions[bot]@mokoconsulting.tech> Date: Sun, 21 Jun 2026 20:32:13 +0000 Subject: [PATCH 124/132] chore(version): pre-release bump to 01.00.05-dev [skip ci] --- .mokogitea/manifest.xml | 2 +- CHANGELOG.md | 2 +- README.md | 2 +- source/packages/com_mokoog/mokoog.xml | 2 +- source/packages/plg_content_mokoog/mokoog.xml | 2 +- source/packages/plg_system_mokoog/mokoog.xml | 2 +- source/packages/plg_webservices_mokoog/mokoog.xml | 2 +- source/pkg_mokoog.xml | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.mokogitea/manifest.xml b/.mokogitea/manifest.xml index 92e303c..74a6c50 100644 --- a/.mokogitea/manifest.xml +++ b/.mokogitea/manifest.xml @@ -9,7 +9,7 @@ <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.04</version> + <version>01.00.05</version> <license spdx="GPL-3.0-or-later">GNU General Public License v3</license> </identity> <governance> diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f11c48..43acb51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -<!-- VERSION: 01.00.04 --> +<!-- VERSION: 01.00.05 --> All notable changes to MokoSuiteOpenGraph will be documented in this file. diff --git a/README.md b/README.md index 30ead38..ed3b6f2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MokoSuiteOpenGraph -<!-- VERSION: 01.00.04 --> +<!-- VERSION: 01.00.05 --> Open Graph, Twitter Card, and social sharing meta tag management for Joomla 4/5/6. diff --git a/source/packages/com_mokoog/mokoog.xml b/source/packages/com_mokoog/mokoog.xml index 43a6095..0b05952 100644 --- a/source/packages/com_mokoog/mokoog.xml +++ b/source/packages/com_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="component" method="upgrade"> <name>com_mokoog</name> - <version>01.00.04-dev</version> + <version>01.00.05-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/source/packages/plg_content_mokoog/mokoog.xml b/source/packages/plg_content_mokoog/mokoog.xml index 1df7e8c..4462c7e 100644 --- a/source/packages/plg_content_mokoog/mokoog.xml +++ b/source/packages/plg_content_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="plugin" group="content" method="upgrade"> <name>Content - MokoJoomOpenGraph</name> - <version>01.00.04-dev</version> + <version>01.00.05-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/source/packages/plg_system_mokoog/mokoog.xml b/source/packages/plg_system_mokoog/mokoog.xml index 3a5e0e6..1931d8d 100644 --- a/source/packages/plg_system_mokoog/mokoog.xml +++ b/source/packages/plg_system_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="plugin" group="system" method="upgrade"> <name>System - MokoJoomOpenGraph</name> - <version>01.00.04-dev</version> + <version>01.00.05-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/source/packages/plg_webservices_mokoog/mokoog.xml b/source/packages/plg_webservices_mokoog/mokoog.xml index 2ae002a..6fdcf94 100644 --- a/source/packages/plg_webservices_mokoog/mokoog.xml +++ b/source/packages/plg_webservices_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="plugin" group="webservices" method="upgrade"> <name>Web Services - MokoJoomOpenGraph</name> - <version>01.00.04-dev</version> + <version>01.00.05-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/source/pkg_mokoog.xml b/source/pkg_mokoog.xml index 82de8e8..bf99e2e 100644 --- a/source/pkg_mokoog.xml +++ b/source/pkg_mokoog.xml @@ -8,7 +8,7 @@ <extension type="package" method="upgrade"> <name>Package - MokoSuiteOpenGraph</name> <packagename>mokoog</packagename> - <version>01.00.04-dev</version> + <version>01.00.05-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> From 28d44d6884b2b80aad6e457b7a003fe15f4b7e2d Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sun, 21 Jun 2026 15:40:01 -0500 Subject: [PATCH 125/132] fix: undefined $db in findImage(), pass cached product to buildProduct() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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() --- .../src/Extension/MokoOG.php | 19 ++++++------ .../src/Helper/JsonLdBuilder.php | 31 +++++++++++-------- 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/source/packages/plg_system_mokoog/src/Extension/MokoOG.php b/source/packages/plg_system_mokoog/src/Extension/MokoOG.php index 5418449..312d96a 100644 --- a/source/packages/plg_system_mokoog/src/Extension/MokoOG.php +++ b/source/packages/plg_system_mokoog/src/Extension/MokoOG.php @@ -218,7 +218,7 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface $imageUrl = $image ? $this->resolveImageUrl($image) : ''; if ($option === 'com_mokoshop' && $view === 'product' && $id > 0) { - $schema = JsonLdBuilder::buildProduct($id, $title, $description, $imageUrl); + $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 { @@ -464,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')) @@ -622,15 +623,6 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface } } - /** - * Get the actual pixel dimensions of a local image. - * - * Returns [width, height] or null for external URLs or unreadable images. - * - * @param string $image Image path (relative or absolute URL) - * - * @return array{0: int, 1: int}|null - */ /** * Load MokoSuiteShop product data by product ID. * @@ -661,6 +653,13 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface return $cache[$productId]; } + /** + * Get the actual pixel dimensions of a local image. + * + * @param string $image Image path (relative or absolute URL) + * + * @return array{0: int, 1: int}|null + */ private function getImageDimensions(string $image): ?array { // Cannot determine dimensions for external URLs diff --git a/source/packages/plg_system_mokoog/src/Helper/JsonLdBuilder.php b/source/packages/plg_system_mokoog/src/Helper/JsonLdBuilder.php index 9c92d85..9f7f975 100644 --- a/source/packages/plg_system_mokoog/src/Helper/JsonLdBuilder.php +++ b/source/packages/plg_system_mokoog/src/Helper/JsonLdBuilder.php @@ -162,28 +162,33 @@ 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 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): ?array + public static function buildProduct(int $productId, string $title, string $description, string $image, ?object $cachedProduct = null): ?array { if ($productId <= 0) { return null; } - $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'); + $product = $cachedProduct; - $db->setQuery($query); - $product = $db->loadObject(); + 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; From 77148d24012438eeaed4af14a1b7b8ace04de526 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" <gitea-actions[bot]@mokoconsulting.tech> Date: Sun, 21 Jun 2026 20:40:44 +0000 Subject: [PATCH 126/132] chore(version): pre-release bump to 01.00.06-dev [skip ci] --- .mokogitea/manifest.xml | 2 +- CHANGELOG.md | 2 +- README.md | 2 +- source/packages/com_mokoog/mokoog.xml | 2 +- source/packages/plg_content_mokoog/mokoog.xml | 2 +- source/packages/plg_system_mokoog/mokoog.xml | 2 +- source/packages/plg_webservices_mokoog/mokoog.xml | 2 +- source/pkg_mokoog.xml | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.mokogitea/manifest.xml b/.mokogitea/manifest.xml index 74a6c50..4b9d580 100644 --- a/.mokogitea/manifest.xml +++ b/.mokogitea/manifest.xml @@ -9,7 +9,7 @@ <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.05</version> + <version>01.00.06</version> <license spdx="GPL-3.0-or-later">GNU General Public License v3</license> </identity> <governance> diff --git a/CHANGELOG.md b/CHANGELOG.md index 43acb51..c7cf0b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -<!-- VERSION: 01.00.05 --> +<!-- VERSION: 01.00.06 --> All notable changes to MokoSuiteOpenGraph will be documented in this file. diff --git a/README.md b/README.md index ed3b6f2..3c53f8f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MokoSuiteOpenGraph -<!-- VERSION: 01.00.05 --> +<!-- VERSION: 01.00.06 --> Open Graph, Twitter Card, and social sharing meta tag management for Joomla 4/5/6. diff --git a/source/packages/com_mokoog/mokoog.xml b/source/packages/com_mokoog/mokoog.xml index 0b05952..777a4a4 100644 --- a/source/packages/com_mokoog/mokoog.xml +++ b/source/packages/com_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="component" method="upgrade"> <name>com_mokoog</name> - <version>01.00.05-dev</version> + <version>01.00.06-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/source/packages/plg_content_mokoog/mokoog.xml b/source/packages/plg_content_mokoog/mokoog.xml index 4462c7e..aa0655b 100644 --- a/source/packages/plg_content_mokoog/mokoog.xml +++ b/source/packages/plg_content_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="plugin" group="content" method="upgrade"> <name>Content - MokoJoomOpenGraph</name> - <version>01.00.05-dev</version> + <version>01.00.06-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/source/packages/plg_system_mokoog/mokoog.xml b/source/packages/plg_system_mokoog/mokoog.xml index 1931d8d..669c81c 100644 --- a/source/packages/plg_system_mokoog/mokoog.xml +++ b/source/packages/plg_system_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="plugin" group="system" method="upgrade"> <name>System - MokoJoomOpenGraph</name> - <version>01.00.05-dev</version> + <version>01.00.06-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/source/packages/plg_webservices_mokoog/mokoog.xml b/source/packages/plg_webservices_mokoog/mokoog.xml index 6fdcf94..05f7eb8 100644 --- a/source/packages/plg_webservices_mokoog/mokoog.xml +++ b/source/packages/plg_webservices_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="plugin" group="webservices" method="upgrade"> <name>Web Services - MokoJoomOpenGraph</name> - <version>01.00.05-dev</version> + <version>01.00.06-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/source/pkg_mokoog.xml b/source/pkg_mokoog.xml index bf99e2e..47dbd07 100644 --- a/source/pkg_mokoog.xml +++ b/source/pkg_mokoog.xml @@ -8,7 +8,7 @@ <extension type="package" method="upgrade"> <name>Package - MokoSuiteOpenGraph</name> <packagename>mokoog</packagename> - <version>01.00.05-dev</version> + <version>01.00.06-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> From 46e30c950b619ccd95ad8a4960b404da55fa345f Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sun, 21 Jun 2026 16:26:13 -0500 Subject: [PATCH 127/132] =?UTF-8?q?fix:=20address=20PR=20review=20findings?= =?UTF-8?q?=20=E2=80=94=20error=20handling=20and=20data=20integrity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../src/Controller/BatchController.php | 34 +++++++++++-------- .../src/Extension/MokoOG.php | 28 +++++++++------ .../src/Helper/JsonLdBuilder.php | 30 +++++++++------- source/script.php | 12 +++++-- 4 files changed, 63 insertions(+), 41 deletions(-) diff --git a/source/packages/com_mokoog/src/Controller/BatchController.php b/source/packages/com_mokoog/src/Controller/BatchController.php index 468984b..68141f4 100644 --- a/source/packages/com_mokoog/src/Controller/BatchController.php +++ b/source/packages/com_mokoog/src/Controller/BatchController.php @@ -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([ diff --git a/source/packages/plg_system_mokoog/src/Extension/MokoOG.php b/source/packages/plg_system_mokoog/src/Extension/MokoOG.php index 312d96a..37e826e 100644 --- a/source/packages/plg_system_mokoog/src/Extension/MokoOG.php +++ b/source/packages/plg_system_mokoog/src/Extension/MokoOG.php @@ -619,7 +619,8 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface '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'); } } @@ -638,17 +639,22 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface return $cache[$productId]; } - $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'); + 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(); + $db->setQuery($query); + $cache[$productId] = $db->loadObject(); + } catch (\RuntimeException $e) { + // MokoSuiteShop tables may not exist + $cache[$productId] = null; + } return $cache[$productId]; } diff --git a/source/packages/plg_system_mokoog/src/Helper/JsonLdBuilder.php b/source/packages/plg_system_mokoog/src/Helper/JsonLdBuilder.php index 9f7f975..7f0598a 100644 --- a/source/packages/plg_system_mokoog/src/Helper/JsonLdBuilder.php +++ b/source/packages/plg_system_mokoog/src/Helper/JsonLdBuilder.php @@ -224,21 +224,25 @@ class JsonLdBuilder ]; // Aggregate rating from reviews if available - $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')); + 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(); + $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, - ]; + 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; diff --git a/source/script.php b/source/script.php index b50d096..315a788 100644 --- a/source/script.php +++ b/source/script.php @@ -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'); + } } } From 6a928f856f2ccc0cf648393a34c246afaab27d83 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" <gitea-actions[bot]@mokoconsulting.tech> Date: Sun, 21 Jun 2026 21:26:34 +0000 Subject: [PATCH 128/132] chore(version): pre-release bump to 01.00.07-dev [skip ci] --- .mokogitea/manifest.xml | 2 +- CHANGELOG.md | 2 +- README.md | 2 +- source/packages/com_mokoog/mokoog.xml | 2 +- source/packages/plg_content_mokoog/mokoog.xml | 2 +- source/packages/plg_system_mokoog/mokoog.xml | 2 +- source/packages/plg_webservices_mokoog/mokoog.xml | 2 +- source/pkg_mokoog.xml | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.mokogitea/manifest.xml b/.mokogitea/manifest.xml index 4b9d580..7435739 100644 --- a/.mokogitea/manifest.xml +++ b/.mokogitea/manifest.xml @@ -9,7 +9,7 @@ <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.06</version> + <version>01.00.07</version> <license spdx="GPL-3.0-or-later">GNU General Public License v3</license> </identity> <governance> diff --git a/CHANGELOG.md b/CHANGELOG.md index c7cf0b7..ca28bdb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -<!-- VERSION: 01.00.06 --> +<!-- VERSION: 01.00.07 --> All notable changes to MokoSuiteOpenGraph will be documented in this file. diff --git a/README.md b/README.md index 3c53f8f..f18fc3a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MokoSuiteOpenGraph -<!-- VERSION: 01.00.06 --> +<!-- VERSION: 01.00.07 --> Open Graph, Twitter Card, and social sharing meta tag management for Joomla 4/5/6. diff --git a/source/packages/com_mokoog/mokoog.xml b/source/packages/com_mokoog/mokoog.xml index 777a4a4..11ed4c4 100644 --- a/source/packages/com_mokoog/mokoog.xml +++ b/source/packages/com_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="component" method="upgrade"> <name>com_mokoog</name> - <version>01.00.06-dev</version> + <version>01.00.07-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/source/packages/plg_content_mokoog/mokoog.xml b/source/packages/plg_content_mokoog/mokoog.xml index aa0655b..e565c7c 100644 --- a/source/packages/plg_content_mokoog/mokoog.xml +++ b/source/packages/plg_content_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="plugin" group="content" method="upgrade"> <name>Content - MokoJoomOpenGraph</name> - <version>01.00.06-dev</version> + <version>01.00.07-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/source/packages/plg_system_mokoog/mokoog.xml b/source/packages/plg_system_mokoog/mokoog.xml index 669c81c..fbf84b1 100644 --- a/source/packages/plg_system_mokoog/mokoog.xml +++ b/source/packages/plg_system_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="plugin" group="system" method="upgrade"> <name>System - MokoJoomOpenGraph</name> - <version>01.00.06-dev</version> + <version>01.00.07-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/source/packages/plg_webservices_mokoog/mokoog.xml b/source/packages/plg_webservices_mokoog/mokoog.xml index 05f7eb8..03d268d 100644 --- a/source/packages/plg_webservices_mokoog/mokoog.xml +++ b/source/packages/plg_webservices_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="plugin" group="webservices" method="upgrade"> <name>Web Services - MokoJoomOpenGraph</name> - <version>01.00.06-dev</version> + <version>01.00.07-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/source/pkg_mokoog.xml b/source/pkg_mokoog.xml index 47dbd07..c6394f8 100644 --- a/source/pkg_mokoog.xml +++ b/source/pkg_mokoog.xml @@ -8,7 +8,7 @@ <extension type="package" method="upgrade"> <name>Package - MokoSuiteOpenGraph</name> <packagename>mokoog</packagename> - <version>01.00.06-dev</version> + <version>01.00.07-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> From 77da0c5517c40428fc8b8cefd34869355475ac87 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <jmiller-moko@noreply.git.mokoconsulting.tech> Date: Sun, 21 Jun 2026 16:42:54 -0500 Subject: [PATCH 129/132] fix: remove remaining @ suppression, check Folder::create() returns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../plg_system_mokoog/src/Extension/MokoOG.php | 2 +- .../plg_system_mokoog/src/Helper/ImageGenerator.php | 8 +++++--- .../plg_system_mokoog/src/Helper/ImageHelper.php | 12 ++++++++---- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/source/packages/plg_system_mokoog/src/Extension/MokoOG.php b/source/packages/plg_system_mokoog/src/Extension/MokoOG.php index 37e826e..cd7d710 100644 --- a/source/packages/plg_system_mokoog/src/Extension/MokoOG.php +++ b/source/packages/plg_system_mokoog/src/Extension/MokoOG.php @@ -686,7 +686,7 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface return null; } - $info = @getimagesize($absPath); + $info = getimagesize($absPath); if (!$info) { return null; diff --git a/source/packages/plg_system_mokoog/src/Helper/ImageGenerator.php b/source/packages/plg_system_mokoog/src/Helper/ImageGenerator.php index 4b35818..7a16305 100644 --- a/source/packages/plg_system_mokoog/src/Helper/ImageGenerator.php +++ b/source/packages/plg_system_mokoog/src/Helper/ImageGenerator.php @@ -63,8 +63,10 @@ class ImageGenerator $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); @@ -78,7 +80,7 @@ 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'); diff --git a/source/packages/plg_system_mokoog/src/Helper/ImageHelper.php b/source/packages/plg_system_mokoog/src/Helper/ImageHelper.php index a5f1386..6bdca69 100644 --- a/source/packages/plg_system_mokoog/src/Helper/ImageHelper.php +++ b/source/packages/plg_system_mokoog/src/Helper/ImageHelper.php @@ -64,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; } @@ -80,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 @@ -180,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']; From dfd0fef3b81b8d9bf4dd145fe611d7d6b01609e6 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" <gitea-actions[bot]@mokoconsulting.tech> Date: Sun, 21 Jun 2026 21:43:08 +0000 Subject: [PATCH 130/132] chore(version): pre-release bump to 01.00.08-dev [skip ci] --- .mokogitea/manifest.xml | 2 +- CHANGELOG.md | 2 +- README.md | 2 +- source/packages/com_mokoog/mokoog.xml | 2 +- source/packages/plg_content_mokoog/mokoog.xml | 2 +- source/packages/plg_system_mokoog/mokoog.xml | 2 +- source/packages/plg_webservices_mokoog/mokoog.xml | 2 +- source/pkg_mokoog.xml | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.mokogitea/manifest.xml b/.mokogitea/manifest.xml index 7435739..f88a3c5 100644 --- a/.mokogitea/manifest.xml +++ b/.mokogitea/manifest.xml @@ -9,7 +9,7 @@ <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.07</version> + <version>01.00.08</version> <license spdx="GPL-3.0-or-later">GNU General Public License v3</license> </identity> <governance> diff --git a/CHANGELOG.md b/CHANGELOG.md index ca28bdb..9cac8b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -<!-- VERSION: 01.00.07 --> +<!-- VERSION: 01.00.08 --> All notable changes to MokoSuiteOpenGraph will be documented in this file. diff --git a/README.md b/README.md index f18fc3a..1ea72f7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MokoSuiteOpenGraph -<!-- VERSION: 01.00.07 --> +<!-- VERSION: 01.00.08 --> Open Graph, Twitter Card, and social sharing meta tag management for Joomla 4/5/6. diff --git a/source/packages/com_mokoog/mokoog.xml b/source/packages/com_mokoog/mokoog.xml index 11ed4c4..44ace1f 100644 --- a/source/packages/com_mokoog/mokoog.xml +++ b/source/packages/com_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="component" method="upgrade"> <name>com_mokoog</name> - <version>01.00.07-dev</version> + <version>01.00.08-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/source/packages/plg_content_mokoog/mokoog.xml b/source/packages/plg_content_mokoog/mokoog.xml index e565c7c..b4aeb53 100644 --- a/source/packages/plg_content_mokoog/mokoog.xml +++ b/source/packages/plg_content_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="plugin" group="content" method="upgrade"> <name>Content - MokoJoomOpenGraph</name> - <version>01.00.07-dev</version> + <version>01.00.08-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/source/packages/plg_system_mokoog/mokoog.xml b/source/packages/plg_system_mokoog/mokoog.xml index fbf84b1..63c6914 100644 --- a/source/packages/plg_system_mokoog/mokoog.xml +++ b/source/packages/plg_system_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="plugin" group="system" method="upgrade"> <name>System - MokoJoomOpenGraph</name> - <version>01.00.07-dev</version> + <version>01.00.08-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/source/packages/plg_webservices_mokoog/mokoog.xml b/source/packages/plg_webservices_mokoog/mokoog.xml index 03d268d..9c6a2cc 100644 --- a/source/packages/plg_webservices_mokoog/mokoog.xml +++ b/source/packages/plg_webservices_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="plugin" group="webservices" method="upgrade"> <name>Web Services - MokoJoomOpenGraph</name> - <version>01.00.07-dev</version> + <version>01.00.08-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/source/pkg_mokoog.xml b/source/pkg_mokoog.xml index c6394f8..c2f34d2 100644 --- a/source/pkg_mokoog.xml +++ b/source/pkg_mokoog.xml @@ -8,7 +8,7 @@ <extension type="package" method="upgrade"> <name>Package - MokoSuiteOpenGraph</name> <packagename>mokoog</packagename> - <version>01.00.07-dev</version> + <version>01.00.08-dev</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> From 19e177f1a4412c9ef6ca5b4fa0678b5f1dddd23a Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" <gitea-actions[bot]@mokoconsulting.tech> Date: Sun, 21 Jun 2026 22:00:38 +0000 Subject: [PATCH 131/132] chore(version): auto-bump patch 01.00.09-dev [skip ci] --- .mokogitea/workflows/issue-branch.yml | 2 +- CHANGELOG.md | 2 +- README.md | 2 +- source/packages/com_mokoog/mokoog.xml | 2 +- source/packages/plg_content_mokoog/mokoog.xml | 2 +- source/packages/plg_system_mokoog/mokoog.xml | 2 +- source/packages/plg_webservices_mokoog/mokoog.xml | 2 +- source/pkg_mokoog.xml | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 75a6963..c646a01 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokocli.Automation -# VERSION: 01.00.00 +# VERSION: 01.00.09 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cac8b3..291d2e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -<!-- VERSION: 01.00.08 --> +<!-- VERSION: 01.00.09 --> All notable changes to MokoSuiteOpenGraph will be documented in this file. diff --git a/README.md b/README.md index 1ea72f7..a305361 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MokoSuiteOpenGraph -<!-- VERSION: 01.00.08 --> +<!-- VERSION: 01.00.09 --> Open Graph, Twitter Card, and social sharing meta tag management for Joomla 4/5/6. diff --git a/source/packages/com_mokoog/mokoog.xml b/source/packages/com_mokoog/mokoog.xml index 44ace1f..7fd19df 100644 --- a/source/packages/com_mokoog/mokoog.xml +++ b/source/packages/com_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="component" method="upgrade"> <name>com_mokoog</name> - <version>01.00.08-dev</version> + <version>01.00.09</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/source/packages/plg_content_mokoog/mokoog.xml b/source/packages/plg_content_mokoog/mokoog.xml index b4aeb53..a9eff0c 100644 --- a/source/packages/plg_content_mokoog/mokoog.xml +++ b/source/packages/plg_content_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="plugin" group="content" method="upgrade"> <name>Content - MokoJoomOpenGraph</name> - <version>01.00.08-dev</version> + <version>01.00.09</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/source/packages/plg_system_mokoog/mokoog.xml b/source/packages/plg_system_mokoog/mokoog.xml index 63c6914..20e5cc7 100644 --- a/source/packages/plg_system_mokoog/mokoog.xml +++ b/source/packages/plg_system_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="plugin" group="system" method="upgrade"> <name>System - MokoJoomOpenGraph</name> - <version>01.00.08-dev</version> + <version>01.00.09</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/source/packages/plg_webservices_mokoog/mokoog.xml b/source/packages/plg_webservices_mokoog/mokoog.xml index 9c6a2cc..c05b83b 100644 --- a/source/packages/plg_webservices_mokoog/mokoog.xml +++ b/source/packages/plg_webservices_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="plugin" group="webservices" method="upgrade"> <name>Web Services - MokoJoomOpenGraph</name> - <version>01.00.08-dev</version> + <version>01.00.09</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/source/pkg_mokoog.xml b/source/pkg_mokoog.xml index c2f34d2..28fb26d 100644 --- a/source/pkg_mokoog.xml +++ b/source/pkg_mokoog.xml @@ -8,7 +8,7 @@ <extension type="package" method="upgrade"> <name>Package - MokoSuiteOpenGraph</name> <packagename>mokoog</packagename> - <version>01.00.08-dev</version> + <version>01.00.09</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> From 549a3b55991c50d26d5efe2fb0556708812f5daa Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" <gitea-actions[bot]@mokoconsulting.tech> Date: Sun, 21 Jun 2026 22:00:50 +0000 Subject: [PATCH 132/132] chore(version): pre-release bump to 01.01.01-dev [skip ci] --- .mokogitea/workflows/issue-branch.yml | 2 +- CHANGELOG.md | 2 +- README.md | 2 +- source/packages/com_mokoog/mokoog.xml | 2 +- source/packages/plg_content_mokoog/mokoog.xml | 2 +- source/packages/plg_system_mokoog/mokoog.xml | 2 +- source/packages/plg_webservices_mokoog/mokoog.xml | 2 +- source/pkg_mokoog.xml | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index c646a01..3bb20cf 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokocli.Automation -# VERSION: 01.00.09 +# VERSION: 01.01.01 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/CHANGELOG.md b/CHANGELOG.md index 291d2e7..8a55abc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -<!-- VERSION: 01.00.09 --> +<!-- VERSION: 01.01.01 --> All notable changes to MokoSuiteOpenGraph will be documented in this file. diff --git a/README.md b/README.md index a305361..3224b68 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MokoSuiteOpenGraph -<!-- VERSION: 01.00.09 --> +<!-- VERSION: 01.01.01 --> Open Graph, Twitter Card, and social sharing meta tag management for Joomla 4/5/6. diff --git a/source/packages/com_mokoog/mokoog.xml b/source/packages/com_mokoog/mokoog.xml index 7fd19df..c4d796a 100644 --- a/source/packages/com_mokoog/mokoog.xml +++ b/source/packages/com_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="component" method="upgrade"> <name>com_mokoog</name> - <version>01.00.09</version> + <version>01.01.01</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/source/packages/plg_content_mokoog/mokoog.xml b/source/packages/plg_content_mokoog/mokoog.xml index a9eff0c..00d3991 100644 --- a/source/packages/plg_content_mokoog/mokoog.xml +++ b/source/packages/plg_content_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="plugin" group="content" method="upgrade"> <name>Content - MokoJoomOpenGraph</name> - <version>01.00.09</version> + <version>01.01.01</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/source/packages/plg_system_mokoog/mokoog.xml b/source/packages/plg_system_mokoog/mokoog.xml index 20e5cc7..864a622 100644 --- a/source/packages/plg_system_mokoog/mokoog.xml +++ b/source/packages/plg_system_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="plugin" group="system" method="upgrade"> <name>System - MokoJoomOpenGraph</name> - <version>01.00.09</version> + <version>01.01.01</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/source/packages/plg_webservices_mokoog/mokoog.xml b/source/packages/plg_webservices_mokoog/mokoog.xml index c05b83b..e9e7bc6 100644 --- a/source/packages/plg_webservices_mokoog/mokoog.xml +++ b/source/packages/plg_webservices_mokoog/mokoog.xml @@ -8,7 +8,7 @@ --> <extension type="plugin" group="webservices" method="upgrade"> <name>Web Services - MokoJoomOpenGraph</name> - <version>01.00.09</version> + <version>01.01.01</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail> diff --git a/source/pkg_mokoog.xml b/source/pkg_mokoog.xml index 28fb26d..6db9309 100644 --- a/source/pkg_mokoog.xml +++ b/source/pkg_mokoog.xml @@ -8,7 +8,7 @@ <extension type="package" method="upgrade"> <name>Package - MokoSuiteOpenGraph</name> <packagename>mokoog</packagename> - <version>01.00.09</version> + <version>01.01.01</version> <creationDate>2026-05-23</creationDate> <author>Moko Consulting</author> <authorEmail>hello@mokoconsulting.tech</authorEmail>