From 62df05e483799792d181aa105a45411aaa7a144c Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Fri, 10 Jul 2026 16:26:37 -0500 Subject: [PATCH] Revert "Merge branch 'dev' into main" This reverts commit 4ed45dc56a81a8ec37b440900891986de3048d13, reversing changes made to d6c3351ddeed4b001667fb90a54b17aacd104530. --- .editorconfig | 42 +- .gitignore | 12 - .mokogitea/ISSUE_TEMPLATE/adr.md | 1 - .mokogitea/ISSUE_TEMPLATE/bug_report.md | 1 - .mokogitea/ISSUE_TEMPLATE/config.yml | 1 - .mokogitea/ISSUE_TEMPLATE/documentation.md | 1 - .mokogitea/ISSUE_TEMPLATE/feature_request.md | 1 - .mokogitea/ISSUE_TEMPLATE/joomla_issue.md | 1 - .mokogitea/ISSUE_TEMPLATE/question.md | 1 - .mokogitea/ISSUE_TEMPLATE/rfc.md | 1 - .mokogitea/ISSUE_TEMPLATE/security.md | 1 - .mokogitea/ISSUE_TEMPLATE/version.md | 1 - .mokogitea/branch-protection.yml | 1 - .mokogitea/workflows/auto-bump.yml | 68 +- .mokogitea/workflows/auto-release.yml | 498 ++++++- .mokogitea/workflows/branch-cleanup.yml | 50 +- .mokogitea/workflows/cascade-dev.yml | 107 +- .mokogitea/workflows/ci-generic.yml | 204 ++- .mokogitea/workflows/ci-issue-reporter.yml | 69 +- .mokogitea/workflows/ci-joomla.yml | 1235 ----------------- .mokogitea/workflows/cleanup.yml | 88 +- .mokogitea/workflows/gitleaks.yml | 93 +- .mokogitea/workflows/issue-branch.yml | 74 +- .mokogitea/workflows/notify.yml | 71 +- .mokogitea/workflows/pr-check.yml | 603 +++++++- .mokogitea/workflows/pr-metadata-check.yml | 72 +- .mokogitea/workflows/pre-release.yml | 284 +++- .mokogitea/workflows/rc-revert.yml | 73 +- .mokogitea/workflows/repo-health.yml | 701 +++++++++- .mokogitea/workflows/version-set.yml | 132 +- .../workflows/workflow-sync-trigger.yml | 83 +- CHANGELOG.md | 19 - CLAUDE.md | 32 - CODE_OF_CONDUCT.md | 47 +- CONTRIBUTING.md | 162 ++- GOVERNANCE.md | 120 +- README.md | 45 +- SECURITY.md | 242 +++- composer.json | 28 +- phpstan.neon | 33 +- .../com_mokosuitetaxi/admin/access.xml | 26 - .../com_mokosuitetaxi/admin/config.xml | 51 - .../admin/services/provider.php | 26 - .../src/Controller/DisplayController.php | 18 - .../admin/src/Model/TaxiDashboardModel.php | 17 - .../admin/src/View/TaxiDashboard/HtmlView.php | 23 - .../admin/src/View/TaxiDrivers/HtmlView.php | 23 - .../admin/src/View/TaxiFares/HtmlView.php | 23 - .../admin/src/View/TaxiRides/HtmlView.php | 23 - .../admin/src/View/TaxiVehicles/HtmlView.php | 23 - .../admin/src/View/TaxiZones/HtmlView.php | 23 - .../admin/tmpl/taxidashboard/default.php | 1 - .../admin/tmpl/taxidrivers/default.php | 1 - .../admin/tmpl/taxifares/default.php | 1 - .../admin/tmpl/taxirides/default.php | 1 - .../admin/tmpl/taxivehicles/default.php | 1 - .../admin/tmpl/taxizones/default.php | 1 - .../com_mokosuitetaxi/mokosuitetaxi.xml | 14 - .../en-GB/plg_system_mokosuitetaxi.ini | 2 - .../en-GB/plg_system_mokosuitetaxi.sys.ini | 2 - .../mokosuitetaxi.xml | 67 - .../services/provider.php | 33 - .../sql/install.mysql.sql | 216 --- .../sql/uninstall.mysql.sql | 12 - .../src/Extension/Taxi.php | 24 - .../src/Helper/DispatchHelper.php | 177 --- .../src/Helper/FareHelper.php | 126 -- .../src/Helper/RideHelper.php | 211 --- .../src/Helper/TaxiHelper.php | 104 -- .../src/Helper/VehicleHelper.php | 97 -- .../src/Helper/ZoneHelper.php | 104 -- .../mokosuitetaxi.xml | 10 - .../services/provider.php | 24 - .../src/Extension/MokoSuiteTaxi.php | 36 - source/pkg_mokosuitetaxi.xml | 22 - 75 files changed, 3922 insertions(+), 2939 deletions(-) delete mode 100644 .gitignore delete mode 100644 .mokogitea/ISSUE_TEMPLATE/adr.md delete mode 100644 .mokogitea/ISSUE_TEMPLATE/bug_report.md delete mode 100644 .mokogitea/ISSUE_TEMPLATE/config.yml delete mode 100644 .mokogitea/ISSUE_TEMPLATE/documentation.md delete mode 100644 .mokogitea/ISSUE_TEMPLATE/feature_request.md delete mode 100644 .mokogitea/ISSUE_TEMPLATE/joomla_issue.md delete mode 100644 .mokogitea/ISSUE_TEMPLATE/question.md delete mode 100644 .mokogitea/ISSUE_TEMPLATE/rfc.md delete mode 100644 .mokogitea/ISSUE_TEMPLATE/security.md delete mode 100644 .mokogitea/ISSUE_TEMPLATE/version.md delete mode 100644 .mokogitea/branch-protection.yml delete mode 100644 .mokogitea/workflows/ci-joomla.yml delete mode 100644 CHANGELOG.md delete mode 100644 CLAUDE.md delete mode 100644 source/packages/com_mokosuitetaxi/admin/access.xml delete mode 100644 source/packages/com_mokosuitetaxi/admin/config.xml delete mode 100644 source/packages/com_mokosuitetaxi/admin/services/provider.php delete mode 100644 source/packages/com_mokosuitetaxi/admin/src/Controller/DisplayController.php delete mode 100644 source/packages/com_mokosuitetaxi/admin/src/Model/TaxiDashboardModel.php delete mode 100644 source/packages/com_mokosuitetaxi/admin/src/View/TaxiDashboard/HtmlView.php delete mode 100644 source/packages/com_mokosuitetaxi/admin/src/View/TaxiDrivers/HtmlView.php delete mode 100644 source/packages/com_mokosuitetaxi/admin/src/View/TaxiFares/HtmlView.php delete mode 100644 source/packages/com_mokosuitetaxi/admin/src/View/TaxiRides/HtmlView.php delete mode 100644 source/packages/com_mokosuitetaxi/admin/src/View/TaxiVehicles/HtmlView.php delete mode 100644 source/packages/com_mokosuitetaxi/admin/src/View/TaxiZones/HtmlView.php delete mode 100644 source/packages/com_mokosuitetaxi/admin/tmpl/taxidashboard/default.php delete mode 100644 source/packages/com_mokosuitetaxi/admin/tmpl/taxidrivers/default.php delete mode 100644 source/packages/com_mokosuitetaxi/admin/tmpl/taxifares/default.php delete mode 100644 source/packages/com_mokosuitetaxi/admin/tmpl/taxirides/default.php delete mode 100644 source/packages/com_mokosuitetaxi/admin/tmpl/taxivehicles/default.php delete mode 100644 source/packages/com_mokosuitetaxi/admin/tmpl/taxizones/default.php delete mode 100644 source/packages/com_mokosuitetaxi/mokosuitetaxi.xml delete mode 100644 source/packages/plg_system_mokosuitetaxi/language/en-GB/plg_system_mokosuitetaxi.ini delete mode 100644 source/packages/plg_system_mokosuitetaxi/language/en-GB/plg_system_mokosuitetaxi.sys.ini delete mode 100644 source/packages/plg_system_mokosuitetaxi/mokosuitetaxi.xml delete mode 100644 source/packages/plg_system_mokosuitetaxi/services/provider.php delete mode 100644 source/packages/plg_system_mokosuitetaxi/sql/install.mysql.sql delete mode 100644 source/packages/plg_system_mokosuitetaxi/sql/uninstall.mysql.sql delete mode 100644 source/packages/plg_system_mokosuitetaxi/src/Extension/Taxi.php delete mode 100644 source/packages/plg_system_mokosuitetaxi/src/Helper/DispatchHelper.php delete mode 100644 source/packages/plg_system_mokosuitetaxi/src/Helper/FareHelper.php delete mode 100644 source/packages/plg_system_mokosuitetaxi/src/Helper/RideHelper.php delete mode 100644 source/packages/plg_system_mokosuitetaxi/src/Helper/TaxiHelper.php delete mode 100644 source/packages/plg_system_mokosuitetaxi/src/Helper/VehicleHelper.php delete mode 100644 source/packages/plg_system_mokosuitetaxi/src/Helper/ZoneHelper.php delete mode 100644 source/packages/plg_webservices_mokosuitetaxi/mokosuitetaxi.xml delete mode 100644 source/packages/plg_webservices_mokosuitetaxi/services/provider.php delete mode 100644 source/packages/plg_webservices_mokosuitetaxi/src/Extension/MokoSuiteTaxi.php delete mode 100644 source/pkg_mokosuitetaxi.xml diff --git a/.editorconfig b/.editorconfig index c96f15f..e868be9 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1 +1,41 @@ -IyBFZGl0b3JDb25maWcgaGVscHMgbWFpbnRhaW4gY29uc2lzdGVudCBjb2Rpbmcgc3R5bGVzIGFjcm9zcyBkaWZmZXJlbnQgZWRpdG9ycyBhbmQgSURFcwojIGh0dHBzOi8vZWRpdG9yY29uZmlnLm9yZy8KCnJvb3QgPSB0cnVlCgojIERlZmF1bHQgc2V0dGluZ3Mg4oCUIFRhYnMgcHJlZmVycmVkLCB3aWR0aCA9IDIgc3BhY2VzClsqXQpjaGFyc2V0ID0gdXRmLTgKZW5kX29mX2xpbmUgPSBsZgppbnNlcnRfZmluYWxfbmV3bGluZSA9IHRydWUKdHJpbV90cmFpbGluZ193aGl0ZXNwYWNlID0gdHJ1ZQppbmRlbnRfc3R5bGUgPSB0YWIKdGFiX3dpZHRoID0gMgoKIyBQb3dlclNoZWxsIHNjcmlwdHMg4oCUIHRhYnMsIDItc3BhY2UgdmlzdWFsIHdpZHRoClsqLnBzMV0KaW5kZW50X3N0eWxlID0gdGFiCnRhYl93aWR0aCA9IDIKZW5kX29mX2xpbmUgPSBjcmxmCgojIE1hcmtkb3duIGZpbGVzIOKAlCBrZWVwIHRyYWlsaW5nIHdoaXRlc3BhY2UgZm9yIGxpbmUgYnJlYWtzClsqLm1kXQp0cmltX3RyYWlsaW5nX3doaXRlc3BhY2UgPSBmYWxzZQoKIyBKU09OIC8gWUFNTCBmaWxlcyDigJQgdGFicywgMi1zcGFjZSB2aXN1YWwgd2lkdGgKWyoue2pzb24seW1sLHlhbWx9XQppbmRlbnRfc3R5bGUgPSB0YWIKdGFiX3dpZHRoID0gMgoKIyBNYWtlZmlsZXMg4oCUIGFsd2F5cyB0YWJzLCBkZWZhdWx0IHdpZHRoCltNYWtlZmlsZV0KaW5kZW50X3N0eWxlID0gdGFiCnRhYl93aWR0aCA9IDIKCiMgV2luZG93cyBiYXRjaCBzY3JpcHRzIOKAlCBrZWVwIENSTEYgZW5kaW5ncwpbKi57YmF0LGNtZH1dCmVuZF9vZl9saW5lID0gY3JsZgoKIyBTaGVsbCBzY3JpcHRzIOKAlCBlbnN1cmUgTEYgZW5kaW5ncwpbKi5zaF0KZW5kX29mX2xpbmUgPSBsZgo= \ No newline at end of file +# EditorConfig helps maintain consistent coding styles across different editors and IDEs +# https://editorconfig.org/ + +root = true + +# Default settings — Tabs preferred, width = 2 spaces +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = tab +tab_width = 2 + +# PowerShell scripts — tabs, 2-space visual width +[*.ps1] +indent_style = tab +tab_width = 2 +end_of_line = crlf + +# Markdown files — keep trailing whitespace for line breaks +[*.md] +trim_trailing_whitespace = false + +# JSON / YAML files — tabs, 2-space visual width +[*.{json,yml,yaml}] +indent_style = tab +tab_width = 2 + +# Makefiles — always tabs, default width +[Makefile] +indent_style = tab +tab_width = 2 + +# Windows batch scripts — keep CRLF endings +[*.{bat,cmd}] +end_of_line = crlf + +# Shell scripts — ensure LF endings +[*.sh] +end_of_line = lf diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 0388e51..0000000 --- a/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -# MokoSuite standard ignores -.claude/ -.mcp.json -TODO.md -vendor/ -node_modules/ -.env -*.min.css -*.min.js -.DS_Store -Thumbs.db -*.ppk diff --git a/.mokogitea/ISSUE_TEMPLATE/adr.md b/.mokogitea/ISSUE_TEMPLATE/adr.md deleted file mode 100644 index 643c295..0000000 --- a/.mokogitea/ISSUE_TEMPLATE/adr.md +++ /dev/null @@ -1 +0,0 @@ -LS0tCm5hbWU6IEFyY2hpdGVjdHVyZSBEZWNpc2lvbiBSZWNvcmQgKEFEUikKYWJvdXQ6IFByb3Bvc2Ugb3IgZG9jdW1lbnQgYW4gYXJjaGl0ZWN0dXJhbCBkZWNpc2lvbgp0aXRsZTogJ1tBRFJdICcKbGFiZWxzOiAnYXJjaGl0ZWN0dXJlLCBkZWNpc2lvbicKYXNzaWduZWVzOiAnJwoKLS0tCgoKIyMgQURSIE51bWJlcgpBRFItWFhYWAoKIyMgU3RhdHVzCi0gWyBdIFByb3Bvc2VkCi0gWyBdIEFjY2VwdGVkCi0gWyBdIERlcHJlY2F0ZWQKLSBbIF0gU3VwZXJzZWRlZCBieSBBRFItWFhYWAoKIyMgQ29udGV4dApEZXNjcmliZSB0aGUgaXNzdWUgb3IgcHJvYmxlbSB0aGF0IG1vdGl2YXRlcyB0aGlzIGRlY2lzaW9uLgoKIyMgRGVjaXNpb24KU3RhdGUgdGhlIGFyY2hpdGVjdHVyZSBkZWNpc2lvbiBhbmQgcHJvdmlkZSByYXRpb25hbGUuCgojIyBDb25zZXF1ZW5jZXMKIyMjIFBvc2l0aXZlCi0gTGlzdCBwb3NpdGl2ZSBjb25zZXF1ZW5jZXMKCiMjIyBOZWdhdGl2ZQotIExpc3QgbmVnYXRpdmUgY29uc2VxdWVuY2VzIG9yIHRyYWRlLW9mZnMKCiMjIyBOZXV0cmFsCi0gTGlzdCBuZXV0cmFsIGFzcGVjdHMKCiMjIEFsdGVybmF0aXZlcyBDb25zaWRlcmVkCiMjIyBBbHRlcm5hdGl2ZSAxCi0gRGVzY3JpcHRpb24KLSBQcm9zCi0gQ29ucwotIFdoeSBub3QgY2hvc2VuCgojIyMgQWx0ZXJuYXRpdmUgMgotIERlc2NyaXB0aW9uCi0gUHJvcwotIENvbnMKLSBXaHkgbm90IGNob3NlbgoKIyMgSW1wbGVtZW50YXRpb24gUGxhbgoxLiBTdGVwIDEKMi4gU3RlcCAyCjMuIFN0ZXAgMwoKIyMgU3Rha2Vob2xkZXJzCi0gKipEZWNpc2lvbiBNYWtlcnMqKjogQHVzZXIxLCBAdXNlcjIKLSAqKkNvbnN1bHRlZCoqOiBAdXNlcjMsIEB1c2VyNAotICoqSW5mb3JtZWQqKjogdGVhbS1uYW1lCgojIyBUZWNobmljYWwgRGV0YWlscwojIyMgQXJjaGl0ZWN0dXJlIERpYWdyYW0KYGBgCltBZGQgZGlhZ3JhbSBvciBsaW5rXQpgYGAKCiMjIyBEZXBlbmRlbmNpZXMKLSBEZXBlbmRlbmN5IDEKLSBEZXBlbmRlbmN5IDIKCiMjIyBJbXBhY3QgQW5hbHlzaXMKLSAqKlBlcmZvcm1hbmNlKio6IFtJbXBhY3QgZGVzY3JpcHRpb25dCi0gKipTZWN1cml0eSoqOiBbSW1wYWN0IGRlc2NyaXB0aW9uXQotICoqU2NhbGFiaWxpdHkqKjogW0ltcGFjdCBkZXNjcmlwdGlvbl0KLSAqKk1haW50YWluYWJpbGl0eSoqOiBbSW1wYWN0IGRlc2NyaXB0aW9uXQoKIyMgVGVzdGluZyBTdHJhdGVneQotIFsgXSBVbml0IHRlc3RzCi0gWyBdIEludGVncmF0aW9uIHRlc3RzCi0gWyBdIFBlcmZvcm1hbmNlIHRlc3RzCi0gWyBdIFNlY3VyaXR5IHRlc3RzCgojIyBEb2N1bWVudGF0aW9uCi0gWyBdIEFyY2hpdGVjdHVyZSBkb2N1bWVudGF0aW9uIHVwZGF0ZWQKLSBbIF0gQVBJIGRvY3VtZW50YXRpb24gdXBkYXRlZAotIFsgXSBEZXZlbG9wZXIgZ3VpZGUgdXBkYXRlZAotIFsgXSBSdW5ib29rIGNyZWF0ZWQKCiMjIE1pZ3JhdGlvbiBQYXRoCkRlc2NyaWJlIGhvdyB0byBtaWdyYXRlIGZyb20gY3VycmVudCBzdGF0ZSB0byBuZXcgYXJjaGl0ZWN0dXJlLgoKIyMgUm9sbGJhY2sgUGxhbgpEZXNjcmliZSBob3cgdG8gcm9sbGJhY2sgaWYgaXNzdWVzIG9jY3VyLgoKIyMgVGltZWxpbmUKLSAqKlByb3Bvc2FsIERhdGUqKjoKLSAqKkRlY2lzaW9uIERhdGUqKjoKLSAqKkltcGxlbWVudGF0aW9uIFN0YXJ0Kio6Ci0gKipFeHBlY3RlZCBDb21wbGV0aW9uKio6CgojIyBSZWZlcmVuY2VzCi0gUmVsYXRlZCBBRFJzOgotIEV4dGVybmFsIHJlc291cmNlczoKLSBSRkNzOgoKIyMgUmV2aWV3IENoZWNrbGlzdAotIFsgXSBBbGlnbnMgd2l0aCBlbnRlcnByaXNlIGFyY2hpdGVjdHVyZSBwcmluY2lwbGVzCi0gWyBdIFNlY3VyaXR5IGltcGxpY2F0aW9ucyByZXZpZXdlZAotIFsgXSBQZXJmb3JtYW5jZSBpbXBsaWNhdGlvbnMgcmV2aWV3ZWQKLSBbIF0gQ29zdCBpbXBsaWNhdGlvbnMgcmV2aWV3ZWQKLSBbIF0gQ29tcGxpYW5jZSByZXF1aXJlbWVudHMgbWV0Ci0gWyBdIFRlYW0gY29uc2Vuc3VzIGFjaGlldmVkCg== \ No newline at end of file diff --git a/.mokogitea/ISSUE_TEMPLATE/bug_report.md b/.mokogitea/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index f5cf6b4..0000000 --- a/.mokogitea/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1 +0,0 @@ -LS0tCm5hbWU6IEJ1ZyBSZXBvcnQKYWJvdXQ6IFJlcG9ydCBhIGJ1ZyBvciBpc3N1ZSB3aXRoIHRoZSBwcm9qZWN0CnRpdGxlOiAnW0JVR10gJwpsYWJlbHM6ICdidWcnCmFzc2lnbmVlczogJycKCi0tLQoKCiMjIEJ1ZyBEZXNjcmlwdGlvbgpBIGNsZWFyIGFuZCBjb25jaXNlIGRlc2NyaXB0aW9uIG9mIHdoYXQgdGhlIGJ1ZyBpcy4KCiMjIFN0ZXBzIHRvIFJlcHJvZHVjZQoxLiBHbyB0byAnLi4uJwoyLiBDbGljayBvbiAnLi4uJwozLiBTY3JvbGwgZG93biB0byAnLi4uJwo0LiBTZWUgZXJyb3IKCiMjIEV4cGVjdGVkIEJlaGF2aW9yCkEgY2xlYXIgYW5kIGNvbmNpc2UgZGVzY3JpcHRpb24gb2Ygd2hhdCB5b3UgZXhwZWN0ZWQgdG8gaGFwcGVuLgoKIyMgQWN0dWFsIEJlaGF2aW9yCkEgY2xlYXIgYW5kIGNvbmNpc2UgZGVzY3JpcHRpb24gb2Ygd2hhdCBhY3R1YWxseSBoYXBwZW5lZC4KCiMjIFNjcmVlbnNob3RzCklmIGFwcGxpY2FibGUsIGFkZCBzY3JlZW5zaG90cyB0byBoZWxwIGV4cGxhaW4geW91ciBwcm9ibGVtLgoKIyMgRW52aXJvbm1lbnQKLSAqKlByb2plY3QqKjogW2UuZy4sIE1va29Eb2xpVG9vbHMsIG1va28tY2Fzc2lvcGVpYV0KLSAqKlZlcnNpb24qKjogW2UuZy4sIDEuMi4zXQotICoqUGxhdGZvcm0qKjogW2UuZy4sIERvbGliYXJyIDE4LjAsIEpvb21sYSA1LjBdCi0gKipQSFAgVmVyc2lvbioqOiBbZS5nLiwgOC4xXQotICoqRGF0YWJhc2UqKjogW2UuZy4sIE15U1FMIDguMCwgUG9zdGdyZVNRTCAxNF0KLSAqKkJyb3dzZXIqKiAoaWYgYXBwbGljYWJsZSk6IFtlLmcuLCBDaHJvbWUgMTIwLCBGaXJlZm94IDEyMV0KLSAqKk9TKio6IFtlLmcuLCBVYnVudHUgMjIuMDQsIFdpbmRvd3MgMTFdCgojIyBBZGRpdGlvbmFsIENvbnRleHQKQWRkIGFueSBvdGhlciBjb250ZXh0IGFib3V0IHRoZSBwcm9ibGVtIGhlcmUuCgojIyBQb3NzaWJsZSBTb2x1dGlvbgpJZiB5b3UgaGF2ZSBzdWdnZXN0aW9ucyBvbiBob3cgdG8gZml4IHRoZSBpc3N1ZSwgcGxlYXNlIGRlc2NyaWJlIHRoZW0gaGVyZS4KCiMjIENoZWNrbGlzdAotIFsgXSBJIGhhdmUgc2VhcmNoZWQgZm9yIHNpbWlsYXIgaXNzdWVzIGJlZm9yZSBjcmVhdGluZyB0aGlzIG9uZQotIFsgXSBJIGhhdmUgcHJvdmlkZWQgYWxsIHRoZSByZXF1ZXN0ZWQgaW5mb3JtYXRpb24KLSBbIF0gSSBoYXZlIHRlc3RlZCB0aGlzIG9uIHRoZSBsYXRlc3Qgc3RhYmxlIHZlcnNpb24KLSBbIF0gSSBoYXZlIGNoZWNrZWQgdGhlIGRvY3VtZW50YXRpb24gYW5kIGNvdWxkbid0IGZpbmQgYSBzb2x1dGlvbgo= \ No newline at end of file diff --git a/.mokogitea/ISSUE_TEMPLATE/config.yml b/.mokogitea/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index 9acaf67..0000000 --- a/.mokogitea/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1 +0,0 @@ -LS0tCmJsYW5rX2lzc3Vlc19lbmFibGVkOiB0cnVlCmNvbnRhY3RfbGlua3M6CiAgLSBuYW1lOiDwn5K8IEVudGVycHJpc2UgU3VwcG9ydAogICAgdXJsOiBodHRwczovL21va29jb25zdWx0aW5nLnRlY2gvZW50ZXJwcmlzZQogICAgYWJvdXQ6IEVudGVycHJpc2UtbGV2ZWwgc3VwcG9ydCBhbmQgY29uc3VsdGF0aW9uIHNlcnZpY2VzCiAgLSBuYW1lOiDwn5KsIEFzayBhIFF1ZXN0aW9uCiAgICB1cmw6IGh0dHBzOi8vbW9rb2NvbnN1bHRpbmcudGVjaC8KICAgIGFib3V0OiBHZXQgaGVscCBvciBhc2sgcXVlc3Rpb25zIHRocm91Z2ggb3VyIHdlYnNpdGUKICAtIG5hbWU6IPCfk5ogTW9rb1N0YW5kYXJkcyBEb2N1bWVudGF0aW9uCiAgICB1cmw6IGh0dHBzOi8vZ2l0Lm1va29jb25zdWx0aW5nLnRlY2gvTW9rb0NvbnN1bHRpbmcvbW9rby1wbGF0Zm9ybQogICAgYWJvdXQ6IFZpZXcgb3VyIGNvZGluZyBzdGFuZGFyZHMgYW5kIGJlc3QgcHJhY3RpY2VzCiAgLSBuYW1lOiDwn5SSIFJlcG9ydCBhIFNlY3VyaXR5IFZ1bG5lcmFiaWxpdHkKICAgIHVybDogaHR0cHM6Ly9naXQubW9rb2NvbnN1bHRpbmcudGVjaC9tb2tvY29uc3VsdGluZy10ZWNoLy5naXRodWItcHJpdmF0ZS9zZWN1cml0eS9hZHZpc29yaWVzL25ldwogICAgYWJvdXQ6IFJlcG9ydCBzZWN1cml0eSB2dWxuZXJhYmlsaXRpZXMgcHJpdmF0ZWx5IChmb3IgY3JpdGljYWwgaXNzdWVzKQogIC0gbmFtZTog8J+SoSBDb21tdW5pdHkgRGlzY3Vzc2lvbnMKICAgIHVybDogaHR0cHM6Ly9naXRodWIuY29tL29yZ3MvbW9rb2NvbnN1bHRpbmctdGVjaC9kaXNjdXNzaW9ucwogICAgYWJvdXQ6IEpvaW4gY29tbXVuaXR5IGRpc2N1c3Npb25zIGFuZCBRJkEK \ No newline at end of file diff --git a/.mokogitea/ISSUE_TEMPLATE/documentation.md b/.mokogitea/ISSUE_TEMPLATE/documentation.md deleted file mode 100644 index 68588a7..0000000 --- a/.mokogitea/ISSUE_TEMPLATE/documentation.md +++ /dev/null @@ -1 +0,0 @@ -LS0tCm5hbWU6IERvY3VtZW50YXRpb24gSXNzdWUKYWJvdXQ6IFJlcG9ydCBhbiBpc3N1ZSB3aXRoIGRvY3VtZW50YXRpb24KdGl0bGU6ICdbRE9DU10gJwpsYWJlbHM6ICdkb2N1bWVudGF0aW9uJwphc3NpZ25lZXM6ICcnCgotLS0KCgojIyBEb2N1bWVudGF0aW9uIElzc3VlCgoqKkxvY2F0aW9uKio6IAo8IS0tIFNwZWNpZnkgdGhlIGZpbGUsIHBhZ2UsIG9yIHNlY3Rpb24gd2l0aCB0aGUgaXNzdWUgLS0+CgojIyBJc3N1ZSBUeXBlCjwhLS0gTWFyayB0aGUgcmVsZXZhbnQgb3B0aW9uIHdpdGggYW4gIngiIC0tPgotIFsgXSBUeXBvIG9yIGdyYW1tYXIgZXJyb3IKLSBbIF0gT3V0ZGF0ZWQgaW5mb3JtYXRpb24KLSBbIF0gTWlzc2luZyBkb2N1bWVudGF0aW9uCi0gWyBdIFVuY2xlYXIgZXhwbGFuYXRpb24KLSBbIF0gQnJva2VuIGxpbmtzCi0gWyBdIE1pc3NpbmcgZXhhbXBsZXMKLSBbIF0gT3RoZXIgKHNwZWNpZnkgYmVsb3cpCgojIyBEZXNjcmlwdGlvbgo8IS0tIENsZWFybHkgZGVzY3JpYmUgdGhlIGRvY3VtZW50YXRpb24gaXNzdWUgLS0+CgojIyBDdXJyZW50IENvbnRlbnQKPCEtLSBRdW90ZSBvciBkZXNjcmliZSB0aGUgY3VycmVudCBkb2N1bWVudGF0aW9uIChpZiBhcHBsaWNhYmxlKSAtLT4KYGBgCkN1cnJlbnQgdGV4dCBoZXJlCmBgYAoKIyMgU3VnZ2VzdGVkIEltcHJvdmVtZW50CjwhLS0gUHJvdmlkZSB5b3VyIHN1Z2dlc3Rpb24gZm9yIGhvdyB0byBpbXByb3ZlIHRoZSBkb2N1bWVudGF0aW9uIC0tPgpgYGAKU3VnZ2VzdGVkIHRleHQgaGVyZQpgYGAKCiMjIEFkZGl0aW9uYWwgQ29udGV4dAo8IS0tIEFkZCBhbnkgb3RoZXIgY29udGV4dCwgc2NyZWVuc2hvdHMsIG9yIHJlZmVyZW5jZXMgLS0+CgojIyBTdGFuZGFyZHMgQWxpZ25tZW50Ci0gWyBdIEZvbGxvd3MgTW9rb1N0YW5kYXJkcyBkb2N1bWVudGF0aW9uIGd1aWRlbGluZXMKLSBbIF0gVXNlcyBlbl9VUy9lbl9HQiBsb2NhbGl6YXRpb24KLSBbIF0gSW5jbHVkZXMgcHJvcGVyIFNQRFggaGVhZGVycyB3aGVyZSBhcHBsaWNhYmxlCgojIyBDaGVja2xpc3QKLSBbIF0gSSBoYXZlIHNlYXJjaGVkIGZvciBzaW1pbGFyIGRvY3VtZW50YXRpb24gaXNzdWVzCi0gWyBdIEkgaGF2ZSBwcm92aWRlZCBhIGNsZWFyIGRlc2NyaXB0aW9uCi0gWyBdIEkgaGF2ZSBzdWdnZXN0ZWQgYW4gaW1wcm92ZW1lbnQgKGlmIGFwcGxpY2FibGUpCg== \ No newline at end of file diff --git a/.mokogitea/ISSUE_TEMPLATE/feature_request.md b/.mokogitea/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 1bc6523..0000000 --- a/.mokogitea/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1 +0,0 @@ -LS0tCm5hbWU6IEZlYXR1cmUgUmVxdWVzdAphYm91dDogU3VnZ2VzdCBhIG5ldyBmZWF0dXJlIG9yIGVuaGFuY2VtZW50CnRpdGxlOiAnW0ZFQVRVUkVdICcKbGFiZWxzOiAnZW5oYW5jZW1lbnQnCmFzc2lnbmVlczogJycKCi0tLQoKCiMjIEZlYXR1cmUgRGVzY3JpcHRpb24KQSBjbGVhciBhbmQgY29uY2lzZSBkZXNjcmlwdGlvbiBvZiB0aGUgZmVhdHVyZSB5b3UnZCBsaWtlIHRvIHNlZS4KCiMjIFByb2JsZW0gb3IgVXNlIENhc2UKRGVzY3JpYmUgdGhlIHByb2JsZW0gdGhpcyBmZWF0dXJlIHdvdWxkIHNvbHZlIG9yIHRoZSB1c2UgY2FzZSBpdCBhZGRyZXNzZXMuCkV4LiBJJ20gYWx3YXlzIGZydXN0cmF0ZWQgd2hlbiBbLi4uXQoKIyMgUHJvcG9zZWQgU29sdXRpb24KQSBjbGVhciBhbmQgY29uY2lzZSBkZXNjcmlwdGlvbiBvZiB3aGF0IHlvdSB3YW50IHRvIGhhcHBlbi4KCiMjIEFsdGVybmF0aXZlIFNvbHV0aW9ucwpBIGNsZWFyIGFuZCBjb25jaXNlIGRlc2NyaXB0aW9uIG9mIGFueSBhbHRlcm5hdGl2ZSBzb2x1dGlvbnMgb3IgZmVhdHVyZXMgeW91J3ZlIGNvbnNpZGVyZWQuCgojIyBCZW5lZml0cwpEZXNjcmliZSBob3cgdGhpcyBmZWF0dXJlIHdvdWxkIGJlbmVmaXQgdXNlcnM6Ci0gV2hvIHdvdWxkIHVzZSB0aGlzIGZlYXR1cmU/Ci0gV2hhdCBwcm9ibGVtcyBkb2VzIGl0IHNvbHZlPwotIFdoYXQgdmFsdWUgZG9lcyBpdCBhZGQ/CgojIyBJbXBsZW1lbnRhdGlvbiBEZXRhaWxzIChPcHRpb25hbCkKSWYgeW91IGhhdmUgaWRlYXMgYWJvdXQgaG93IHRoaXMgY291bGQgYmUgaW1wbGVtZW50ZWQsIHNoYXJlIHRoZW0gaGVyZToKLSBUZWNobmljYWwgYXBwcm9hY2gKLSBGaWxlcy9jb21wb25lbnRzIHRoYXQgbWlnaHQgbmVlZCBjaGFuZ2VzCi0gQW55IGNvbmNlcm5zIG9yIGNoYWxsZW5nZXMgeW91IGZvcmVzZWUKCiMjIEFkZGl0aW9uYWwgQ29udGV4dApBZGQgYW55IG90aGVyIGNvbnRleHQsIG1vY2t1cHMsIG9yIHNjcmVlbnNob3RzIGFib3V0IHRoZSBmZWF0dXJlIHJlcXVlc3QgaGVyZS4KCiMjIFJlbGV2YW50IFN0YW5kYXJkcwpEb2VzIHRoaXMgcmVsYXRlIHRvIGFueSBzdGFuZGFyZHMgaW4gW01va29TdGFuZGFyZHNdKGh0dHBzOi8vZ2l0Lm1va29jb25zdWx0aW5nLnRlY2gvTW9rb0NvbnN1bHRpbmcvTW9rb1N0YW5kYXJkcyk/Ci0gWyBdIEFjY2Vzc2liaWxpdHkgKFdDQUcgMi4xIEFBKQotIFsgXSBMb2NhbGl6YXRpb24gKGVuX1VTL2VuX0dCKQotIFsgXSBTZWN1cml0eSBiZXN0IHByYWN0aWNlcwotIFsgXSBDb2RlIHF1YWxpdHkgc3RhbmRhcmRzCi0gWyBdIE90aGVyOiBbc3BlY2lmeV0KCiMjIENoZWNrbGlzdAotIFsgXSBJIGhhdmUgc2VhcmNoZWQgZm9yIHNpbWlsYXIgZmVhdHVyZSByZXF1ZXN0cyBiZWZvcmUgY3JlYXRpbmcgdGhpcyBvbmUKLSBbIF0gSSBoYXZlIGNsZWFybHkgZGVzY3JpYmVkIHRoZSB1c2UgY2FzZSBhbmQgYmVuZWZpdHMKLSBbIF0gSSBoYXZlIGNvbnNpZGVyZWQgYWx0ZXJuYXRpdmUgc29sdXRpb25zCi0gWyBdIFRoaXMgZmVhdHVyZSBhbGlnbnMgd2l0aCB0aGUgcHJvamVjdCdzIGdvYWxzIGFuZCBzY29wZQo= \ No newline at end of file diff --git a/.mokogitea/ISSUE_TEMPLATE/joomla_issue.md b/.mokogitea/ISSUE_TEMPLATE/joomla_issue.md deleted file mode 100644 index 8bc2535..0000000 --- a/.mokogitea/ISSUE_TEMPLATE/joomla_issue.md +++ /dev/null @@ -1 +0,0 @@ -LS0tCm5hbWU6IEpvb21sYSBFeHRlbnNpb24gSXNzdWUKYWJvdXQ6IFJlcG9ydCBhbiBpc3N1ZSB3aXRoIGEgSm9vbWxhIGV4dGVuc2lvbgp0aXRsZTogJ1tKT09NTEFdICcKbGFiZWxzOiAnam9vbWxhJwphc3NpZ25lZXM6ICcnCgotLS0KCgojIyBJc3N1ZSBUeXBlCi0gWyBdIENvbXBvbmVudCBJc3N1ZQotIFsgXSBNb2R1bGUgSXNzdWUKLSBbIF0gUGx1Z2luIElzc3VlCi0gWyBdIFRlbXBsYXRlIElzc3VlCgojIyBFeHRlbnNpb24gRGV0YWlscwotICoqRXh0ZW5zaW9uIE5hbWUqKjogW2UuZy4sIG1va28tY2Fzc2lvcGVpYV0KLSAqKkV4dGVuc2lvbiBWZXJzaW9uKio6IFtlLmcuLCAxLjIuM10KLSAqKkV4dGVuc2lvbiBUeXBlKio6IFtDb21wb25lbnQgLyBNb2R1bGUgLyBQbHVnaW4gLyBUZW1wbGF0ZV0KCiMjIEpvb21sYSBFbnZpcm9ubWVudAotICoqSm9vbWxhIFZlcnNpb24qKjogW2UuZy4sIDQuNC4wLCA1LjAuMF0KLSAqKlBIUCBWZXJzaW9uKio6IFtlLmcuLCA4LjEuMF0KLSAqKkRhdGFiYXNlKio6IFtNeVNRTCAvIFBvc3RncmVTUUwgLyBNYXJpYURCXQotICoqRGF0YWJhc2UgVmVyc2lvbioqOiBbZS5nLiwgOC4wXQotICoqU2VydmVyKio6IFtBcGFjaGUgLyBOZ2lueCAvIElJU10KLSAqKkhvc3RpbmcqKjogW1NoYXJlZCAvIFZQUyAvIERlZGljYXRlZCAvIENsb3VkXQoKIyMgSXNzdWUgRGVzY3JpcHRpb24KUHJvdmlkZSBhIGNsZWFyIGFuZCBkZXRhaWxlZCBkZXNjcmlwdGlvbiBvZiB0aGUgaXNzdWUuCgojIyBTdGVwcyB0byBSZXByb2R1Y2UKMS4gR28gdG8gJy4uLicKMi4gQ2xpY2sgb24gJy4uLicKMy4gQ29uZmlndXJlICcuLi4nCjQuIFNlZSBlcnJvcgoKIyMgRXhwZWN0ZWQgQmVoYXZpb3IKV2hhdCB5b3UgZXhwZWN0ZWQgdG8gaGFwcGVuLgoKIyMgQWN0dWFsIEJlaGF2aW9yCldoYXQgYWN0dWFsbHkgaGFwcGVuZWQuCgojIyBFcnJvciBNZXNzYWdlcwpgYGAKIyBQYXN0ZSBhbnkgZXJyb3IgbWVzc2FnZXMgZnJvbSBKb29tbGEgZXJyb3IgbG9ncwojIExvY2F0aW9uOiBhZG1pbmlzdHJhdG9yL2xvZ3MvZXJyb3IucGhwCmBgYAoKIyMgQnJvd3NlciBDb25zb2xlIEVycm9ycwpgYGBqYXZhc2NyaXB0Ci8vIFBhc3RlIGFueSBKYXZhU2NyaXB0IGNvbnNvbGUgZXJyb3JzIChGMTIgaW4gYnJvd3NlcikKYGBgCgojIyBTY3JlZW5zaG90cwpBZGQgc2NyZWVuc2hvdHMgdG8gaGVscCBleHBsYWluIHRoZSBpc3N1ZS4KCiMjIENvbmZpZ3VyYXRpb24KYGBgaW5pCiMgUGFzdGUgZXh0ZW5zaW9uIGNvbmZpZ3VyYXRpb24gKHNhbml0aXplIHNlbnNpdGl2ZSBkYXRhKQpgYGAKCiMjIEluc3RhbGxlZCBFeHRlbnNpb25zCkxpc3Qgb3RoZXIgaW5zdGFsbGVkIGV4dGVuc2lvbnMgdGhhdCBtaWdodCBjb25mbGljdDoKLSBFeHRlbnNpb24gMSAodmVyc2lvbikKLSBFeHRlbnNpb24gMiAodmVyc2lvbikKCiMjIFRlbXBsYXRlIE92ZXJyaWRlcwotIFsgXSBVc2luZyB0ZW1wbGF0ZSBvdmVycmlkZXMKLSBbIF0gQ3VzdG9tIENTUwotIFsgXSBDdXN0b20gSmF2YVNjcmlwdAoKIyMgQWRkaXRpb25hbCBDb250ZXh0Ci0gKipNdWx0aWxpbmd1YWwgU2l0ZSoqOiBbWWVzIC8gTm9dCi0gKipDYWNoZSBFbmFibGVkKio6IFtZZXMgLyBOb10KLSAqKkRlYnVnIE1vZGUqKjogW1llcyAvIE5vXQotICoqU0VGIFVSTHMqKjogW1llcyAvIE5vXQoKIyMgQ2hlY2tsaXN0Ci0gWyBdIEkgaGF2ZSBjbGVhcmVkIEpvb21sYSBjYWNoZQotIFsgXSBJIGhhdmUgZGlzYWJsZWQgb3RoZXIgZXh0ZW5zaW9ucyB0byB0ZXN0IGZvciBjb25mbGljdHMKLSBbIF0gSSBoYXZlIGNoZWNrZWQgSm9vbWxhIGVycm9yIGxvZ3MKLSBbIF0gSSBoYXZlIHRlc3RlZCB3aXRoIGEgZGVmYXVsdCBKb29tbGEgdGVtcGxhdGUKLSBbIF0gSSBoYXZlIGNoZWNrZWQgYnJvd3NlciBjb25zb2xlIGZvciBKYXZhU2NyaXB0IGVycm9ycwotIFsgXSBJIGhhdmUgc2VhcmNoZWQgZm9yIHNpbWlsYXIgaXNzdWVzCi0gWyBdIEkgYW0gdXNpbmcgYSBzdXBwb3J0ZWQgSm9vbWxhIHZlcnNpb24K \ No newline at end of file diff --git a/.mokogitea/ISSUE_TEMPLATE/question.md b/.mokogitea/ISSUE_TEMPLATE/question.md deleted file mode 100644 index 3a096ac..0000000 --- a/.mokogitea/ISSUE_TEMPLATE/question.md +++ /dev/null @@ -1 +0,0 @@ -LS0tCm5hbWU6IFF1ZXN0aW9uCmFib3V0OiBBc2sgYSBxdWVzdGlvbiBhYm91dCB1c2FnZSwgZmVhdHVyZXMsIG9yIGJlc3QgcHJhY3RpY2VzCnRpdGxlOiAnW1FVRVNUSU9OXSAnCmxhYmVsczogWydxdWVzdGlvbiddCmFzc2lnbmVlczogWydqbWlsbGVyJ10KLS0tCgoKIyMgUXVlc3Rpb24KCioqWW91ciBxdWVzdGlvbjoqKgoKCiMjIENvbnRleHQKCioqV2hhdCBhcmUgeW91IHRyeWluZyB0byBhY2NvbXBsaXNoPyoqCgoKKipXaGF0IGhhdmUgeW91IGFscmVhZHkgdHJpZWQ/KioKCgoqKkNhdGVnb3J5Kio6Ci0gWyBdIFNjcmlwdCB1c2FnZQotIFsgXSBDb25maWd1cmF0aW9uCi0gWyBdIFdvcmtmbG93IHNldHVwCi0gWyBdIERvY3VtZW50YXRpb24gaW50ZXJwcmV0YXRpb24KLSBbIF0gQmVzdCBwcmFjdGljZXMKLSBbIF0gSW50ZWdyYXRpb24KLSBbIF0gT3RoZXI6IF9fX19fX19fX18KCiMjIEVudmlyb25tZW50IChpZiByZWxldmFudCkKCioqWW91ciBzZXR1cCoqOgotIE9wZXJhdGluZyBTeXN0ZW06Ci0gVmVyc2lvbjoKCiMjIFdoYXQgWW91J3ZlIFJlc2VhcmNoZWQKCioqRG9jdW1lbnRhdGlvbiByZXZpZXdlZCoqOgotIFsgXSBSRUFETUUubWQKLSBbIF0gUHJvamVjdCBkb2N1bWVudGF0aW9uCi0gWyBdIE90aGVyIChzcGVjaWZ5KTogX19fX19fX19fXwoKKipTaW1pbGFyIGlzc3Vlcy9xdWVzdGlvbnMgZm91bmQqKjoKLSAjCi0gIwoKIyMgRXhwZWN0ZWQgT3V0Y29tZQoKKipXaGF0IHJlc3VsdCBhcmUgeW91IGhvcGluZyBmb3I/KioKCgojIyBDb2RlL0NvbmZpZ3VyYXRpb24gU2FtcGxlcwoKKipSZWxldmFudCBjb2RlIG9yIGNvbmZpZ3VyYXRpb24qKiAoaWYgYXBwbGljYWJsZSk6CgpgYGBiYXNoCiMgWW91ciBjb2RlIGhlcmUKYGBgCgojIyBBZGRpdGlvbmFsIENvbnRleHQKCioqQW55IG90aGVyIHJlbGV2YW50IGluZm9ybWF0aW9uOioqCgoKKipTY3JlZW5zaG90cyoqIChpZiBoZWxwZnVsKToKCgojIyBVcmdlbmN5CgotIFsgXSBVcmdlbnQgKGJsb2NraW5nIHdvcmspCi0gWyBdIE5vcm1hbCAoY2FuIHdvcmsgb24gb3RoZXIgdGhpbmdzIG1lYW53aGlsZSkKLSBbIF0gTG93IHByaW9yaXR5IChqdXN0IGN1cmlvdXMpCgojIyBDaGVja2xpc3QKCi0gWyBdIEkgaGF2ZSBzZWFyY2hlZCBleGlzdGluZyBpc3N1ZXMgYW5kIGRpc2N1c3Npb25zCi0gWyBdIEkgaGF2ZSByZXZpZXdlZCByZWxldmFudCBkb2N1bWVudGF0aW9uCi0gWyBdIEkgaGF2ZSBwcm92aWRlZCBzdWZmaWNpZW50IGNvbnRleHQKLSBbIF0gSSBoYXZlIGluY2x1ZGVkIGNvZGUvY29uZmlndXJhdGlvbiBzYW1wbGVzIGlmIHJlbGV2YW50Ci0gWyBdIFRoaXMgaXMgYSBnZW51aW5lIHF1ZXN0aW9uIChub3QgYSBidWcgcmVwb3J0IG9yIGZlYXR1cmUgcmVxdWVzdCkK \ No newline at end of file diff --git a/.mokogitea/ISSUE_TEMPLATE/rfc.md b/.mokogitea/ISSUE_TEMPLATE/rfc.md deleted file mode 100644 index 494ee30..0000000 --- a/.mokogitea/ISSUE_TEMPLATE/rfc.md +++ /dev/null @@ -1 +0,0 @@ -LS0tCm5hbWU6IFJlcXVlc3QgZm9yIENvbW1lbnRzIChSRkMpCmFib3V0OiBQcm9wb3NlIGEgc2lnbmlmaWNhbnQgY2hhbmdlIGZvciBjb21tdW5pdHkgZGlzY3Vzc2lvbgp0aXRsZTogJ1tSRkNdICcKbGFiZWxzOiAncmZjLCBkaXNjdXNzaW9uJwphc3NpZ25lZXM6ICcnCgotLS0KCgojIyBSRkMgU3VtbWFyeQpPbmUtcGFyYWdyYXBoIHN1bW1hcnkgb2YgdGhlIHByb3Bvc2FsLgoKIyMgTW90aXZhdGlvbgpXaHkgYXJlIHdlIGRvaW5nIHRoaXM/IFdoYXQgdXNlIGNhc2VzIGRvZXMgaXQgc3VwcG9ydD8gV2hhdCBpcyB0aGUgZXhwZWN0ZWQgb3V0Y29tZT8KCiMjIERldGFpbGVkIERlc2lnbgojIyMgT3ZlcnZpZXcKUHJvdmlkZSBhIGRldGFpbGVkIGV4cGxhbmF0aW9uIG9mIHRoZSBwcm9wb3NlZCBjaGFuZ2UuCgojIyMgQVBJIENoYW5nZXMgKGlmIGFwcGxpY2FibGUpCmBgYHBocAovLyBCZWZvcmUKZnVuY3Rpb24gb2xkQXBpKCRwYXJhbTEpIHsgfQoKLy8gQWZ0ZXIKZnVuY3Rpb24gbmV3QXBpKCRwYXJhbTEsICRwYXJhbTIpIHsgfQpgYGAKCiMjIyBVc2VyIEV4cGVyaWVuY2UgQ2hhbmdlcwpEZXNjcmliZSBob3cgdXNlcnMgd2lsbCBpbnRlcmFjdCB3aXRoIHRoaXMgY2hhbmdlLgoKIyMjIEltcGxlbWVudGF0aW9uIEFwcHJvYWNoCkhpZ2gtbGV2ZWwgaW1wbGVtZW50YXRpb24gc3RyYXRlZ3kuCgojIyBEcmF3YmFja3MKV2h5IHNob3VsZCB3ZSAqbm90KiBkbyB0aGlzPwoKIyMgQWx0ZXJuYXRpdmVzCldoYXQgb3RoZXIgZGVzaWducyBoYXZlIGJlZW4gY29uc2lkZXJlZD8gV2hhdCBpcyB0aGUgaW1wYWN0IG9mIG5vdCBkb2luZyB0aGlzPwoKIyMjIEFsdGVybmF0aXZlIDEKLSBEZXNjcmlwdGlvbgotIFRyYWRlLW9mZnMKCiMjIyBBbHRlcm5hdGl2ZSAyCi0gRGVzY3JpcHRpb24KLSBUcmFkZS1vZmZzCgojIyBBZG9wdGlvbiBTdHJhdGVneQpIb3cgd2lsbCBleGlzdGluZyB1c2VycyBhZG9wdCB0aGlzPyBJcyB0aGlzIGEgYnJlYWtpbmcgY2hhbmdlPwoKIyMjIE1pZ3JhdGlvbiBHdWlkZQpgYGBiYXNoCiMgU3RlcHMgdG8gbWlncmF0ZQpgYGAKCiMjIyBEZXByZWNhdGlvbiBUaW1lbGluZQotICoqQW5ub3VuY2VtZW50Kio6Ci0gKipEZXByZWNhdGlvbioqOgotICoqUmVtb3ZhbCoqOgoKIyMgVW5yZXNvbHZlZCBRdWVzdGlvbnMKLSBRdWVzdGlvbiAxCi0gUXVlc3Rpb24gMgoKIyMgRnV0dXJlIFBvc3NpYmlsaXRpZXMKV2hhdCBmdXR1cmUgd29yayBkb2VzIHRoaXMgZW5hYmxlPwoKIyMgSW1wYWN0IEFzc2Vzc21lbnQKIyMjIFBlcmZvcm1hbmNlCkV4cGVjdGVkIHBlcmZvcm1hbmNlIGltcGFjdC4KCiMjIyBTZWN1cml0eQpTZWN1cml0eSBjb25zaWRlcmF0aW9ucyBhbmQgaW1wbGljYXRpb25zLgoKIyMjIENvbXBhdGliaWxpdHkKLSAqKkJhY2t3YXJkIENvbXBhdGlibGUqKjogW1llcyAvIE5vXQotICoqQnJlYWtpbmcgQ2hhbmdlcyoqOiBbTGlzdF0KCiMjIyBNYWludGVuYW5jZQpMb25nLXRlcm0gbWFpbnRlbmFuY2UgY29uc2lkZXJhdGlvbnMuCgojIyBDb21tdW5pdHkgSW5wdXQKIyMjIFN0YWtlaG9sZGVycwotIFsgXSBDb3JlIHRlYW0KLSBbIF0gTW9kdWxlIGRldmVsb3BlcnMKLSBbIF0gRW5kIHVzZXJzCi0gWyBdIEVudGVycHJpc2UgY3VzdG9tZXJzCgojIyMgRmVlZGJhY2sgUGVyaW9kCioqRHVyYXRpb24qKjogW2UuZy4sIDIgd2Vla3NdCioqRGVhZGxpbmUqKjogW2RhdGVdCgojIyBJbXBsZW1lbnRhdGlvbiBUaW1lbGluZQojIyMgUGhhc2UgMTogRGVzaWduCi0gWyBdIFJGQyBkaXNjdXNzaW9uCi0gWyBdIERlc2lnbiBmaW5hbGl6YXRpb24KLSBbIF0gQXBwcm92YWwKCiMjIyBQaGFzZSAyOiBJbXBsZW1lbnRhdGlvbgotIFsgXSBDb3JlIGltcGxlbWVudGF0aW9uCi0gWyBdIFRlc3RzCi0gWyBdIERvY3VtZW50YXRpb24KCiMjIyBQaGFzZSAzOiBSZWxlYXNlCi0gWyBdIEJldGEgcmVsZWFzZQotIFsgXSBGZWVkYmFjayBjb2xsZWN0aW9uCi0gWyBdIFN0YWJsZSByZWxlYXNlCgojIyBTdWNjZXNzIE1ldHJpY3MKSG93IHdpbGwgd2UgbWVhc3VyZSBzdWNjZXNzPwotIE1ldHJpYyAxCi0gTWV0cmljIDIKCiMjIFJlZmVyZW5jZXMKLSBSZWxhdGVkIFJGQ3M6Ci0gRXh0ZXJuYWwgZG9jdW1lbnRhdGlvbjoKLSBQcmlvciBhcnQ6CgojIyBPcGVuIFF1ZXN0aW9ucyBmb3IgQ29tbXVuaXR5CjEuIFF1ZXN0aW9uIDE/CjIuIFF1ZXN0aW9uIDI/CgotLS0KKipOb3RlKio6IFRoaXMgUkZDIGlzIG9wZW4gZm9yIGNvbW11bml0eSBkaXNjdXNzaW9uLiBQbGVhc2UgcHJvdmlkZSBmZWVkYmFjayBpbiB0aGUgY29tbWVudHMgYmVsb3cuCg== \ No newline at end of file diff --git a/.mokogitea/ISSUE_TEMPLATE/security.md b/.mokogitea/ISSUE_TEMPLATE/security.md deleted file mode 100644 index 51f7766..0000000 --- a/.mokogitea/ISSUE_TEMPLATE/security.md +++ /dev/null @@ -1 +0,0 @@ -LS0tCm5hbWU6IFNlY3VyaXR5IFZ1bG5lcmFiaWxpdHkgUmVwb3J0CmFib3V0OiBSZXBvcnQgYSBzZWN1cml0eSB2dWxuZXJhYmlsaXR5ICh1c2Ugb25seSBmb3Igbm9uLWNyaXRpY2FsIGlzc3VlcykKdGl0bGU6ICdbU0VDVVJJVFldICcKbGFiZWxzOiAnc2VjdXJpdHknCmFzc2lnbmVlczogJycKCi0tLQoKCiMjIOKaoO+4jyBJTVBPUlRBTlQ6IFByaXZhdGUgRGlzY2xvc3VyZSBSZXF1aXJlZAoKKipGb3IgY3JpdGljYWwgc2VjdXJpdHkgdnVsbmVyYWJpbGl0aWVzLCBETyBOT1QgdXNlIHRoaXMgdGVtcGxhdGUuKioKRm9sbG93IHRoZSBwcm9jZXNzIGluIFtTRUNVUklUWS5tZF0oLi4vU0VDVVJJVFkubWQpIGZvciByZXNwb25zaWJsZSBkaXNjbG9zdXJlLgoKVXNlIHRoaXMgdGVtcGxhdGUgb25seSBmb3I6Ci0gU2VjdXJpdHkgaW1wcm92ZW1lbnRzCi0gTm9uLWNyaXRpY2FsIHNlY3VyaXR5IHN1Z2dlc3Rpb25zCi0gU2VjdXJpdHkgZG9jdW1lbnRhdGlvbiB1cGRhdGVzCgotLS0KCiMjIFNlY3VyaXR5IElzc3VlCgoqKlNldmVyaXR5Kio6IAo8IS0tIExvdywgTWVkaXVtLCBvciBpbmZvcm1hdGlvbmFsIG9ubHkgLS0+CgojIyBEZXNjcmlwdGlvbgo8IS0tIERlc2NyaWJlIHRoZSBzZWN1cml0eSBjb25jZXJuIG9yIGltcHJvdmVtZW50IHN1Z2dlc3Rpb24gLS0+CgojIyBBZmZlY3RlZCBDb21wb25lbnRzCjwhLS0gTGlzdCB0aGUgYWZmZWN0ZWQgZmlsZXMsIGZlYXR1cmVzLCBvciBjb21wb25lbnRzIC0tPgoKIyMgU3VnZ2VzdGVkIE1pdGlnYXRpb24KPCEtLSBEZXNjcmliZSBob3cgdGhpcyBjb3VsZCBiZSBhZGRyZXNzZWQgLS0+CgojIyBTdGFuZGFyZHMgUmVmZXJlbmNlCkRvZXMgdGhpcyByZWxhdGUgdG8gc2VjdXJpdHkgc3RhbmRhcmRzIGluIFtNb2tvU3RhbmRhcmRzXShodHRwczovL2dpdC5tb2tvY29uc3VsdGluZy50ZWNoL01va29Db25zdWx0aW5nL01va29TdGFuZGFyZHMpPwotIFsgXSBTUERYIGxpY2Vuc2UgaWRlbnRpZmllcnMKLSBbIF0gU2VjcmV0IG1hbmFnZW1lbnQKLSBbIF0gRGVwZW5kZW5jeSBzZWN1cml0eQotIFsgXSBBY2Nlc3MgY29udHJvbAotIFsgXSBPdGhlcjogW3NwZWNpZnldCgojIyBBZGRpdGlvbmFsIENvbnRleHQKPCEtLSBBZGQgYW55IG90aGVyIGNvbnRleHQgYWJvdXQgdGhlIHNlY3VyaXR5IGNvbmNlcm4gLS0+CgojIyBDaGVja2xpc3QKLSBbIF0gVGhpcyBpcyBOT1QgYSBjcml0aWNhbCB2dWxuZXJhYmlsaXR5IHJlcXVpcmluZyBwcml2YXRlIGRpc2Nsb3N1cmUKLSBbIF0gSSBoYXZlIHJldmlld2VkIHRoZSBTRUNVUklUWS5tZCBwb2xpY3kKLSBbIF0gSSBoYXZlIHByb3ZpZGVkIHN1ZmZpY2llbnQgZGV0YWlsIGZvciBldmFsdWF0aW9uCg== \ No newline at end of file diff --git a/.mokogitea/ISSUE_TEMPLATE/version.md b/.mokogitea/ISSUE_TEMPLATE/version.md deleted file mode 100644 index a943689..0000000 --- a/.mokogitea/ISSUE_TEMPLATE/version.md +++ /dev/null @@ -1 +0,0 @@ -LS0tCm5hbWU6IFZlcnNpb24gQnVtcAphYm91dDogUmVxdWVzdCBvciB0cmFjayBhIHZlcnNpb24gY2hhbmdlCnRpdGxlOiAnW1ZFUlNJT05dICcKbGFiZWxzOiAndmVyc2lvbiwgdHlwZTogdmVyc2lvbicKYXNzaWduZWVzOiAnam1pbGxlcicKLS0tCgojIyBWZXJzaW9uIENoYW5nZQoKKipDdXJyZW50IHZlcnNpb24qKjogPCEtLSBlLmcuLCAwMS4wMi4wMyAtLT4KKipSZXF1ZXN0ZWQgdmVyc2lvbioqOiA8IS0tIGUuZy4sIDAxLjAzLjAwIC0tPgoqKkNoYW5nZSB0eXBlKio6IDwhLS0gcGF0Y2ggLyBtaW5vciAvIG1ham9yIC0tPgoKIyMgUmVhc29uCgo8IS0tIFdoeSBpcyB0aGlzIHZlcnNpb24gYnVtcCBuZWVkZWQ/IC0tPgoKIyMgQ2hlY2tsaXN0CgotIFsgXSBSRUFETUUubWQgYFZFUlNJT046YCBmaWVsZCB1cGRhdGVkCi0gWyBdIENIQU5HRUxPRy5tZCBlbnRyeSBhZGRlZAotIFsgXSBNb2R1bGUgZGVzY3JpcHRvciB2ZXJzaW9uIHVwZGF0ZWQgKERvbGliYXJyOiBgJHRoaXMtPnZlcnNpb25gLCBKb29tbGE6IGA8dmVyc2lvbj5gKQotIFsgXSBBbGwgZmlsZSBoZWFkZXJzIHdpbGwgYmUgYXV0by1wcm9wYWdhdGVkIGJ5IGBzeW5jLXZlcnNpb24tb24tbWVyZ2VgIHdvcmtmbG93Cg== \ No newline at end of file diff --git a/.mokogitea/branch-protection.yml b/.mokogitea/branch-protection.yml deleted file mode 100644 index a3468d0..0000000 --- a/.mokogitea/branch-protection.yml +++ /dev/null @@ -1 +0,0 @@ -IyBDb3B5cmlnaHQgKEMpIDIwMjYgTW9rbyBDb25zdWx0aW5nIDxoZWxsb0Btb2tvY29uc3VsdGluZy50ZWNoPgojIFNQRFgtTGljZW5zZS1JZGVudGlmaWVyOiBHUEwtMy4wLW9yLWxhdGVyCiMgRklMRSBJTkZPUk1BVElPTgojIERFRkdST1VQOiBHaXRlYS5Xb3JrZmxvdwojIElOR1JPVVA6IG1va28tcGxhdGZvcm0uQXV0b21hdGlvbgojIFJFUE86IGh0dHBzOi8vZ2l0Lm1va29jb25zdWx0aW5nLnRlY2gvTW9rb0NvbnN1bHRpbmcvbW9rby1wbGF0Zm9ybQojIFBBVEg6IC8uZ2l0ZWEvd29ya2Zsb3dzL2JyYW5jaC1wcm90ZWN0aW9uLnltbAojIEJSSUVGOiBBcHBseSBzdGFuZGFyZGlzZWQgYnJhbmNoIHByb3RlY3Rpb24gcnVsZXMgdG8gYWxsIGdvdmVybmVkIHJlcG9zaXRvcmllcwojCiMgKz09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PSsKIyB8ICAgICAgICAgICAgICAgICAgIEJSQU5DSCBQUk9URUNUSU9OIFNFVFVQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfAojICs9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0rCiMgfCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHwKIyB8ICBBcHBsaWVzIHByb3RlY3Rpb24gcnVsZXMgZm9yOiBtYWluLCBkZXYsIHJjLCBiZXRhLCBhbHBoYSAgICAgICB8CiMgfCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHwKIyB8ICBtYWluICDigJQgUmVxdWlyZSBQUiwgYmxvY2sgcmVqZWN0ZWQgcmV2aWV3cywgbm8gZm9yY2UgcHVzaCAgICAgICAgICAgICB8CiMgfCAgZGV2ICAg4oCUIEFsbG93IHB1c2gsIG5vIGZvcmNlIHB1c2gsIG5vIGRlbGV0ZSAgICAgICAgICAgICAgICAgICAgICAgICAgfAojIHwgIHJjICDigJQgQWxsb3cgcHVzaCwgbm8gZm9yY2UgcHVzaCwgbm8gZGVsZXRlICAgICAgICAgICAgICAgICAgICAgICAgICB8CiMgfCAgYmV0YSDigJQgQWxsb3cgcHVzaCwgbm8gZm9yY2UgcHVzaCwgbm8gZGVsZXRlICAgICAgICAgICAgICAgICAgICAgICAgIHwKIyB8ICBhbHBoYSDigJQgQWxsb3cgcHVzaCwgbm8gZm9yY2UgcHVzaCwgbm8gZGVsZXRlICAgICAgICAgICAgICAgICAgICAgICAgfAojIHwgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB8CiMgfCAgam1pbGxlciBoYXMgb3ZlcnJpZGUgYXV0aG9yaXR5IG9uIGFsbCBicmFuY2hlcy4gICAgICAgICAgICAgICAgICAgICAgIHwKIyB8ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfAojICs9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0rCgpuYW1lOiBCcmFuY2ggUHJvdGVjdGlvbiBTZXR1cAoKb246CiAgc2NoZWR1bGU6CiAgICAtIGNyb246ICcwIDIgKiAqIDEnICAjIFdlZWtseSBNb25kYXkgMDI6MDAgVVRDCiAgd29ya2Zsb3dfZGlzcGF0Y2g6CiAgICBpbnB1dHM6CiAgICAgIGRyeV9ydW46CiAgICAgICAgZGVzY3JpcHRpb246ICdQcmV2aWV3IG1vZGUgKG5vIGNoYW5nZXMpJwogICAgICAgIHJlcXVpcmVkOiBmYWxzZQogICAgICAgIHR5cGU6IGJvb2xlYW4KICAgICAgICBkZWZhdWx0OiBmYWxzZQogICAgICByZXBvczoKICAgICAgICBkZXNjcmlwdGlvbjogJ0NvbW1hLXNlcGFyYXRlZCByZXBvIG5hbWVzIChlbXB0eSA9IGFsbCBnb3Zlcm5lZCByZXBvcyknCiAgICAgICAgcmVxdWlyZWQ6IGZhbHNlCiAgICAgICAgdHlwZTogc3RyaW5nCiAgICAgICAgZGVmYXVsdDogJycKCmVudjoKICBHSVRFQV9VUkw6IGh0dHBzOi8vZ2l0Lm1va29jb25zdWx0aW5nLnRlY2gKICBHSVRFQV9PUkc6IE1va29Db25zdWx0aW5nCgpwZXJtaXNzaW9uczoKICBjb250ZW50czogcmVhZAoKam9iczoKICBwcm90ZWN0OgogICAgbmFtZTogQXBwbHkgQnJhbmNoIFByb3RlY3Rpb24gUnVsZXMKICAgIHJ1bnMtb246IHVidW50dS1sYXRlc3QKCiAgICBzdGVwczoKICAgICAgLSBuYW1lOiBEZXRlcm1pbmUgdGFyZ2V0IHJlcG9zCiAgICAgICAgaWQ6IHJlcG9zCiAgICAgICAgZW52OgogICAgICAgICAgR0FfVE9LRU46ICR7eyBzZWNyZXRzLkdBX1RPS0VOIH19CiAgICAgICAgcnVuOiB8CiAgICAgICAgICBBUEk9IiR7R0lURUFfVVJMfS9hcGkvdjEiCgogICAgICAgICAgIyBQbGF0Zm9ybS9zdGFuZGFyZHMvaW5mcmEgcmVwb3MgdG8gZXhjbHVkZQogICAgICAgICAgRVhDTFVERT0iZ2l0ZWEtb3JnLWNvbmZpZyBvcmctcHJvZmlsZSBnaXRlYS1wcml2YXRlIC5tb2tvZ2l0ZWEtcHJpdmF0ZSBNb2tvU3RhbmRhcmRzIG1va28tcGxhdGZvcm0gTW9rb1Rlc3RpbmciCiAgICAgICAgICBFWENMVURFPSIkRVhDTFVERSBNb2tvU3RhbmRhcmRzLVRlbXBsYXRlLUNsaWVudCBNb2tvU3RhbmRhcmRzLVRlbXBsYXRlLURvbGliYXJyIE1va29TdGFuZGFyZHMtVGVtcGxhdGUtR2VuZXJpYyBNb2tvU3RhbmRhcmRzLVRlbXBsYXRlLUpvb21sYSBNb2tvRG9saVByb2pUZW1wbGF0ZSIKCiAgICAgICAgICBpZiBbIC1uICIke3sgaW5wdXRzLnJlcG9zIH19IiBdOyB0aGVuCiAgICAgICAgICAgICMgVXNlci1zcGVjaWZpZWQgcmVwb3MKICAgICAgICAgICAgUkVQT1M9JChlY2hvICIke3sgaW5wdXRzLnJlcG9zIH19IiB8IHRyICcsJyAnICcpCiAgICAgICAgICBlbHNlCiAgICAgICAgICAgICMgRmV0Y2ggYWxsIG9yZyByZXBvcwogICAgICAgICAgICBQQUdFPTEKICAgICAgICAgICAgUkVQT1M9IiIKICAgICAgICAgICAgd2hpbGUgdHJ1ZTsgZG8KICAgICAgICAgICAgICBCQVRDSD0kKGN1cmwgLXNTIFwKICAgICAgICAgICAgICAgIC1IICJBdXRob3JpemF0aW9uOiB0b2tlbiAke0dBX1RPS0VOfSIgXAogICAgICAgICAgICAgICAgIiR7QVBJfS9vcmdzLyR7R0lURUFfT1JHfS9yZXBvcz9wYWdlPSR7UEFHRX0mbGltaXQ9NTAiIFwKICAgICAgICAgICAgICAgIHwganEgLXIgJy5bXS5uYW1lIC8vIGVtcHR5JykKICAgICAgICAgICAgICBbIC16ICIkQkFUQ0giIF0gJiYgYnJlYWsKICAgICAgICAgICAgICBSRVBPUz0iJFJFUE9TICRCQVRDSCIKICAgICAgICAgICAgICBQQUdFPSQoKFBBR0UgKyAxKSkKICAgICAgICAgICAgZG9uZQoKICAgICAgICAgICAgIyBGaWx0ZXIgb3V0IGV4Y2x1ZGVkIHJlcG9zCiAgICAgICAgICAgIEZJTFRFUkVEPSIiCiAgICAgICAgICAgIGZvciBSRVBPIGluICRSRVBPUzsgZG8KICAgICAgICAgICAgICBTS0lQPWZhbHNlCiAgICAgICAgICAgICAgZm9yIEVYIGluICRFWENMVURFOyBkbwogICAgICAgICAgICAgICAgaWYgWyAiJFJFUE8iID0gIiRFWCIgXTsgdGhlbgogICAgICAgICAgICAgICAgICBTS0lQPXRydWUKICAgICAgICAgICAgICAgICAgYnJlYWsKICAgICAgICAgICAgICAgIGZpCiAgICAgICAgICAgICAgZG9uZQogICAgICAgICAgICAgIGlmIFsgIiRTS0lQIiA9ICJmYWxzZSIgXTsgdGhlbgogICAgICAgICAgICAgICAgRklMVEVSRUQ9IiRGSUxURVJFRCAkUkVQTyIKICAgICAgICAgICAgICBmaQogICAgICAgICAgICBkb25lCiAgICAgICAgICAgIFJFUE9TPSIkRklMVEVSRUQiCiAgICAgICAgICBmaQoKICAgICAgICAgIGVjaG8gInJlcG9zPSRSRVBPUyIgPj4gIiRHSVRIVUJfT1VUUFVUIgogICAgICAgICAgQ09VTlQ9JChlY2hvICIkUkVQT1MiIHwgd2MgLXcpCiAgICAgICAgICBlY2hvICLwn5OLIFRhcmdldCByZXBvcyAoJHtDT1VOVH0pOiAkUkVQT1MiCgogICAgICAtIG5hbWU6IEFwcGx5IHByb3RlY3Rpb24gcnVsZXMKICAgICAgICBlbnY6CiAgICAgICAgICBHQV9UT0tFTjogJHt7IHNlY3JldHMuR0FfVE9LRU4gfX0KICAgICAgICAgIERSWV9SVU46ICR7eyBpbnB1dHMuZHJ5X3J1biB8fCAnZmFsc2UnIH19CiAgICAgICAgcnVuOiB8CiAgICAgICAgICBBUEk9IiR7R0lURUFfVVJMfS9hcGkvdjEiCiAgICAgICAgICBSRVBPUz0iJHt7IHN0ZXBzLnJlcG9zLm91dHB1dHMucmVwb3MgfX0iCgogICAgICAgICAgU1VDQ0VTUz0wCiAgICAgICAgICBGQUlMRUQ9MAogICAgICAgICAgU0tJUFBFRD0wCgogICAgICAgICAgIyDilIDilIAgUnVsZSBkZWZpbml0aW9ucyDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKICAgICAgICAgICMgT25seSB0aGUgQ0kgYm90IChqbWlsbGVyIHRva2VuKSBjYW4gcHVzaCBkaXJlY3RseS4KICAgICAgICAgICMgQWxsIGh1bWFuIGNvbnRyaWJ1dG9ycyBtdXN0IHVzZSBQUnMuCiAgICAgICAgICAjIEZvcmNlIHB1c2ggZGlzYWJsZWQgb24gYWxsIGJyYW5jaGVzLgoKICAgICAgICAgIFJVTEVfTUFJTj0newogICAgICAgICAgICAicnVsZV9uYW1lIjogIm1haW4iLAogICAgICAgICAgICAiZW5hYmxlX3B1c2giOiB0cnVlLAogICAgICAgICAgICAiZW5hYmxlX3B1c2hfd2hpdGVsaXN0IjogdHJ1ZSwKICAgICAgICAgICAgInB1c2hfd2hpdGVsaXN0X3VzZXJuYW1lcyI6IFsiam1pbGxlciJdLAogICAgICAgICAgICAiZW5hYmxlX2ZvcmNlX3B1c2giOiBmYWxzZSwKICAgICAgICAgICAgImVuYWJsZV9mb3JjZV9wdXNoX2FsbG93bGlzdCI6IGZhbHNlLAogICAgICAgICAgICAiZm9yY2VfcHVzaF9hbGxvd2xpc3RfdXNlcm5hbWVzIjogW10sCiAgICAgICAgICAgICJlbmFibGVfbWVyZ2Vfd2hpdGVsaXN0IjogZmFsc2UsCiAgICAgICAgICAgICJyZXF1aXJlZF9hcHByb3ZhbHMiOiAwLAogICAgICAgICAgICAiZGlzbWlzc19zdGFsZV9hcHByb3ZhbHMiOiB0cnVlLAogICAgICAgICAgICAiYmxvY2tfb25fcmVqZWN0ZWRfcmV2aWV3cyI6IHRydWUsCiAgICAgICAgICAgICJibG9ja19vbl9vdXRkYXRlZF9icmFuY2giOiBmYWxzZSwKICAgICAgICAgICAgInByaW9yaXR5IjogMQogICAgICAgICAgfScKCiAgICAgICAgICBSVUxFX0RFVj0newogICAgICAgICAgICAicnVsZV9uYW1lIjogImRldiIsCiAgICAgICAgICAgICJlbmFibGVfcHVzaCI6IHRydWUsCiAgICAgICAgICAgICJlbmFibGVfcHVzaF93aGl0ZWxpc3QiOiB0cnVlLAogICAgICAgICAgICAicHVzaF93aGl0ZWxpc3RfdXNlcm5hbWVzIjogWyJqbWlsbGVyIl0sCiAgICAgICAgICAgICJlbmFibGVfZm9yY2VfcHVzaCI6IGZhbHNlLAogICAgICAgICAgICAiZW5hYmxlX2ZvcmNlX3B1c2hfYWxsb3dsaXN0IjogZmFsc2UsCiAgICAgICAgICAgICJmb3JjZV9wdXNoX2FsbG93bGlzdF91c2VybmFtZXMiOiBbXSwKICAgICAgICAgICAgImVuYWJsZV9tZXJnZV93aGl0ZWxpc3QiOiBmYWxzZSwKICAgICAgICAgICAgInJlcXVpcmVkX2FwcHJvdmFscyI6IDAsCiAgICAgICAgICAgICJibG9ja19vbl9yZWplY3RlZF9yZXZpZXdzIjogZmFsc2UsCiAgICAgICAgICAgICJwcmlvcml0eSI6IDIKICAgICAgICAgIH0nCgogICAgICAgICAgUlVMRV9SQz0newogICAgICAgICAgICAicnVsZV9uYW1lIjogInJjIiwKICAgICAgICAgICAgImVuYWJsZV9wdXNoIjogdHJ1ZSwKICAgICAgICAgICAgImVuYWJsZV9wdXNoX3doaXRlbGlzdCI6IHRydWUsCiAgICAgICAgICAgICJwdXNoX3doaXRlbGlzdF91c2VybmFtZXMiOiBbImptaWxsZXIiXSwKICAgICAgICAgICAgImVuYWJsZV9mb3JjZV9wdXNoIjogZmFsc2UsCiAgICAgICAgICAgICJlbmFibGVfZm9yY2VfcHVzaF9hbGxvd2xpc3QiOiBmYWxzZSwKICAgICAgICAgICAgImZvcmNlX3B1c2hfYWxsb3dsaXN0X3VzZXJuYW1lcyI6IFtdLAogICAgICAgICAgICAiZW5hYmxlX21lcmdlX3doaXRlbGlzdCI6IGZhbHNlLAogICAgICAgICAgICAicmVxdWlyZWRfYXBwcm92YWxzIjogMCwKICAgICAgICAgICAgImJsb2NrX29uX3JlamVjdGVkX3Jldmlld3MiOiBmYWxzZSwKICAgICAgICAgICAgInByaW9yaXR5IjogMwogICAgICAgICAgfScKCiAgICAgICAgICBSVUxFX0JFVEE9J3sKICAgICAgICAgICAgInJ1bGVfbmFtZSI6ICJiZXRhIiwKICAgICAgICAgICAgImVuYWJsZV9wdXNoIjogdHJ1ZSwKICAgICAgICAgICAgImVuYWJsZV9wdXNoX3doaXRlbGlzdCI6IHRydWUsCiAgICAgICAgICAgICJwdXNoX3doaXRlbGlzdF91c2VybmFtZXMiOiBbImptaWxsZXIiXSwKICAgICAgICAgICAgImVuYWJsZV9mb3JjZV9wdXNoIjogZmFsc2UsCiAgICAgICAgICAgICJlbmFibGVfZm9yY2VfcHVzaF9hbGxvd2xpc3QiOiBmYWxzZSwKICAgICAgICAgICAgImZvcmNlX3B1c2hfYWxsb3dsaXN0X3VzZXJuYW1lcyI6IFtdLAogICAgICAgICAgICAiZW5hYmxlX21lcmdlX3doaXRlbGlzdCI6IGZhbHNlLAogICAgICAgICAgICAicmVxdWlyZWRfYXBwcm92YWxzIjogMCwKICAgICAgICAgICAgImJsb2NrX29uX3JlamVjdGVkX3Jldmlld3MiOiBmYWxzZSwKICAgICAgICAgICAgInByaW9yaXR5IjogNAogICAgICAgICAgfScKCiAgICAgICAgICBSVUxFX0FMUEhBPSd7CiAgICAgICAgICAgICJydWxlX25hbWUiOiAiYWxwaGEiLAogICAgICAgICAgICAiZW5hYmxlX3B1c2giOiB0cnVlLAogICAgICAgICAgICAiZW5hYmxlX3B1c2hfd2hpdGVsaXN0IjogdHJ1ZSwKICAgICAgICAgICAgInB1c2hfd2hpdGVsaXN0X3VzZXJuYW1lcyI6IFsiam1pbGxlciJdLAogICAgICAgICAgICAiZW5hYmxlX2ZvcmNlX3B1c2giOiBmYWxzZSwKICAgICAgICAgICAgImVuYWJsZV9mb3JjZV9wdXNoX2FsbG93bGlzdCI6IGZhbHNlLAogICAgICAgICAgICAiZm9yY2VfcHVzaF9hbGxvd2xpc3RfdXNlcm5hbWVzIjogW10sCiAgICAgICAgICAgICJlbmFibGVfbWVyZ2Vfd2hpdGVsaXN0IjogZmFsc2UsCiAgICAgICAgICAgICJyZXF1aXJlZF9hcHByb3ZhbHMiOiAwLAogICAgICAgICAgICAiYmxvY2tfb25fcmVqZWN0ZWRfcmV2aWV3cyI6IGZhbHNlLAogICAgICAgICAgICAicHJpb3JpdHkiOiA1CiAgICAgICAgICB9JwoKICAgICAgICAgIFJVTEVTPSgiJFJVTEVfTUFJTiIgIiRSVUxFX0RFViIgIiRSVUxFX1JDIiAiJFJVTEVfQkVUQSIgIiRSVUxFX0FMUEhBIikKICAgICAgICAgIFJVTEVfTkFNRVM9KCJtYWluIiAiZGV2IiAicmMiICJiZXRhIiAiYWxwaGEiKQoKICAgICAgICAgICMg4pSA4pSAIEFwcGx5IHJ1bGVzIHRvIGVhY2ggcmVwbyDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKICAgICAgICAgIGZvciBSRVBPIGluICRSRVBPUzsgZG8KICAgICAgICAgICAgZWNobyAiIgogICAgICAgICAgICBlY2hvICLilZDilZDilZAgJHtSRVBPfSDilZDilZDilZAiCgogICAgICAgICAgICBmb3IgaSBpbiAiJHshUlVMRVNbQF19IjsgZG8KICAgICAgICAgICAgICBSVUxFPSIke1JVTEVTWyRpXX0iCiAgICAgICAgICAgICAgTkFNRT0iJHtSVUxFX05BTUVTWyRpXX0iCgogICAgICAgICAgICAgIGlmIFsgIiREUllfUlVOIiA9ICJ0cnVlIiBdOyB0aGVuCiAgICAgICAgICAgICAgICBlY2hvICIgIFtEUlkgUlVOXSBXb3VsZCBhcHBseSBydWxlOiAke05BTUV9IgogICAgICAgICAgICAgICAgU0tJUFBFRD0kKChTS0lQUEVEICsgMSkpCiAgICAgICAgICAgICAgICBjb250aW51ZQogICAgICAgICAgICAgIGZpCgogICAgICAgICAgICAgICMgRGVsZXRlIGV4aXN0aW5nIHJ1bGUgaWYgcHJlc2VudCAoaWRlbXBvdGVudCByZWNyZWF0ZSkKICAgICAgICAgICAgICBFTkNPREVEX05BTUU9JChlY2hvICIkTkFNRSIgfCBzZWQgJ3N8L3wlMkZ8ZycpCiAgICAgICAgICAgICAgY3VybCAtc1MgLW8gL2Rldi9udWxsIC13ICIiIFwKICAgICAgICAgICAgICAgIC1YIERFTEVURSBcCiAgICAgICAgICAgICAgICAtSCAiQXV0aG9yaXphdGlvbjogdG9rZW4gJHtHQV9UT0tFTn0iIFwKICAgICAgICAgICAgICAgICIke0FQSX0vcmVwb3MvJHtHSVRFQV9PUkd9LyR7UkVQT30vYnJhbmNoX3Byb3RlY3Rpb25zLyR7RU5DT0RFRF9OQU1FfSIgMj4vZGV2L251bGwgfHwgdHJ1ZQoKICAgICAgICAgICAgICAjIENyZWF0ZSBydWxlCiAgICAgICAgICAgICAgUkVTUE9OU0U9JChjdXJsIC1zUyAtdyAiXG4le2h0dHBfY29kZX0iIFwKICAgICAgICAgICAgICAgIC1YIFBPU1QgXAogICAgICAgICAgICAgICAgLUggIkF1dGhvcml6YXRpb246IHRva2VuICR7R0FfVE9LRU59IiBcCiAgICAgICAgICAgICAgICAtSCAiQ29udGVudC1UeXBlOiBhcHBsaWNhdGlvbi9qc29uIiBcCiAgICAgICAgICAgICAgICAtZCAiJFJVTEUiIFwKICAgICAgICAgICAgICAgICIke0FQSX0vcmVwb3MvJHtHSVRFQV9PUkd9LyR7UkVQT30vYnJhbmNoX3Byb3RlY3Rpb25zIikKCiAgICAgICAgICAgICAgSFRUUD0kKGVjaG8gIiRSRVNQT05TRSIgfCB0YWlsIC0xKQogICAgICAgICAgICAgIEJPRFk9JChlY2hvICIkUkVTUE9OU0UiIHwgc2VkICckZCcpCgogICAgICAgICAgICAgIGlmIFsgIiRIVFRQIiA9ICIyMDEiIF07IHRoZW4KICAgICAgICAgICAgICAgIGVjaG8gIiAg4pyFICR7TkFNRX0iCiAgICAgICAgICAgICAgICBTVUNDRVNTPSQoKFNVQ0NFU1MgKyAxKSkKICAgICAgICAgICAgICBlbHNlCiAgICAgICAgICAgICAgICBlY2hvICIgIOKdjCAke05BTUV9IChIVFRQICR7SFRUUH0pOiAkKGVjaG8gIiRCT0RZIiB8IGpxIC1yICcubWVzc2FnZSAvLyAuJyAyPi9kZXYvbnVsbCB8IGhlYWQgLTEpIgogICAgICAgICAgICAgICAgRkFJTEVEPSQoKEZBSUxFRCArIDEpKQogICAgICAgICAgICAgIGZpCiAgICAgICAgICAgIGRvbmUKICAgICAgICAgIGRvbmUKCiAgICAgICAgICAjIOKUgOKUgCBTdW1tYXJ5IOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgAogICAgICAgICAgZWNobyAiIgogICAgICAgICAgZWNobyAi4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQIgogICAgICAgICAgZWNobyAiICDinIUgU3VjY2VzczogJHtTVUNDRVNTfSIKICAgICAgICAgIGVjaG8gIiAg4p2MIEZhaWxlZDogICR7RkFJTEVEfSIKICAgICAgICAgIGVjaG8gIiAg4o+t77iPICBTa2lwcGVkOiAke1NLSVBQRUR9IgogICAgICAgICAgZWNobyAi4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQIgoKICAgICAgICAgIGlmIFsgIiRGQUlMRUQiIC1ndCAwIF07IHRoZW4KICAgICAgICAgICAgZWNobyAiOjp3YXJuaW5nOjoke0ZBSUxFRH0gcnVsZShzKSBmYWlsZWQgdG8gYXBwbHkiCiAgICAgICAgICBmaQo= \ No newline at end of file diff --git a/.mokogitea/workflows/auto-bump.yml b/.mokogitea/workflows/auto-bump.yml index 3bfdbe8..91e16f7 100644 --- a/.mokogitea/workflows/auto-bump.yml +++ b/.mokogitea/workflows/auto-bump.yml @@ -1 +1,67 @@ -IyBDb3B5cmlnaHQgKEMpIDIwMjYgTW9rbyBDb25zdWx0aW5nIDxoZWxsb0Btb2tvY29uc3VsdGluZy50ZWNoPgojCiMgU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IEdQTC0zLjAtb3ItbGF0ZXIKIwojIEZJTEUgSU5GT1JNQVRJT04KIyBERUZHUk9VUDogR2l0ZWEuV29ya2Zsb3cKIyBJTkdST1VQOiBtb2tvY2xpLlJlbGVhc2UKIyBSRVBPOiBodHRwczovL2dpdC5tb2tvY29uc3VsdGluZy50ZWNoL01va29Db25zdWx0aW5nL21va29jbGkKIyBQQVRIOiAvLm1va29naXRlYS93b3JrZmxvd3MvYXV0by1idW1wLnltbAojIFZFUlNJT046IDA5LjAyLjAwCiMgQlJJRUY6IEF1dG8gcGF0Y2gtYnVtcCB2ZXJzaW9uIG9uIGV2ZXJ5IHB1c2ggdG8gZGV2IChza2lwcyBtZXJnZSBjb21taXRzKQoKbmFtZTogIlVuaXZlcnNhbDogQXV0byBWZXJzaW9uIEJ1bXAiCgpvbjoKICBwdXNoOgogICAgYnJhbmNoZXM6CiAgICAgIC0gZGV2CiAgICAgIC0gcmMKICAgICAgLSAnZmVhdHVyZS8qKicKICAgICAgLSAncGF0Y2gvKionCgplbnY6CiAgRk9SQ0VfSkFWQVNDUklQVF9BQ1RJT05TX1RPX05PREUyNDogdHJ1ZQogIE1PS09HSVRFQV9VUkw6ICR7eyB2YXJzLkdJVEVBX1VSTCB8fCAnaHR0cHM6Ly9naXQubW9rb2NvbnN1bHRpbmcudGVjaCcgfX0KCnBlcm1pc3Npb25zOgogIGNvbnRlbnRzOiB3cml0ZQoKam9iczoKICBidW1wOgogICAgbmFtZTogVmVyc2lvbiBCdW1wCiAgICBydW5zLW9uOiByZWxlYXNlCiAgICBpZjogPi0KICAgICAgIWNvbnRhaW5zKGdpdGh1Yi5ldmVudC5oZWFkX2NvbW1pdC5tZXNzYWdlLCAnW3NraXAgY2ldJykgJiYKICAgICAgIWNvbnRhaW5zKGdpdGh1Yi5ldmVudC5oZWFkX2NvbW1pdC5tZXNzYWdlLCAnW3NraXAgYnVtcF0nKSAmJgogICAgICAhc3RhcnRzV2l0aChnaXRodWIuZXZlbnQuaGVhZF9jb21taXQubWVzc2FnZSwgJ01lcmdlIHB1bGwgcmVxdWVzdCcpCgogICAgc3RlcHM6CiAgICAgIC0gbmFtZTogQ2hlY2tvdXQKICAgICAgICB1c2VzOiBhY3Rpb25zL2NoZWNrb3V0QGRlMGZhYzJlNDUwMGRhYmUwMDA5ZTY3MjE0ZmY1ZjU0NDdjZTgzZGQgIyB2NgogICAgICAgIHdpdGg6CiAgICAgICAgICB0b2tlbjogJHt7IHNlY3JldHMuTU9LT0dJVEVBX1RPS0VOIH19CiAgICAgICAgICBmZXRjaC1kZXB0aDogMQoKICAgICAgLSBuYW1lOiBTZXR1cCBtb2tvY2xpIHRvb2xzCiAgICAgICAgcnVuOiB8CiAgICAgICAgICBpZiAhIGNvbW1hbmQgLXYgY29tcG9zZXIgJj4gL2Rldi9udWxsOyB0aGVuCiAgICAgICAgICAgIHN1ZG8gYXB0LWdldCB1cGRhdGUgLXFxICYmIHN1ZG8gYXB0LWdldCBpbnN0YWxsIC15IC1xcSBwaHAtY2xpIHBocC1tYnN0cmluZyBwaHAteG1sIHBocC16aXAgcGhwLWN1cmwgY29tcG9zZXIgPi9kZXYvbnVsbCAyPiYxCiAgICAgICAgICBmaQogICAgICAgICAgaWYgWyAtZCAiL29wdC9tb2tvY2xpL2NsaSIgXTsgdGhlbgogICAgICAgICAgICBlY2hvICJNT0tPX0NMST0vb3B0L21va29jbGkvY2xpIiA+PiAiJEdJVEhVQl9FTlYiCiAgICAgICAgICBlbHNlCiAgICAgICAgICAgIGdpdCBjbG9uZSAtLWRlcHRoIDEgLS1icmFuY2ggbWFpbiAtLXF1aWV0IFwKICAgICAgICAgICAgICAiaHR0cHM6Ly94LWFjY2Vzcy10b2tlbjoke3sgc2VjcmV0cy5NT0tPR0lURUFfVE9LRU4gfX1AZ2l0Lm1va29jb25zdWx0aW5nLnRlY2gvTW9rb0NvbnN1bHRpbmcvbW9rb2NsaS5naXQiIFwKICAgICAgICAgICAgICAvdG1wL21va29jbGkKICAgICAgICAgICAgY2QgL3RtcC9tb2tvY2xpICYmIGNvbXBvc2VyIGluc3RhbGwgLS1uby1kZXYgLS1uby1pbnRlcmFjdGlvbiAtLXF1aWV0CiAgICAgICAgICAgIGVjaG8gIk1PS09fQ0xJPS90bXAvbW9rb2NsaS9jbGkiID4+ICIkR0lUSFVCX0VOViIKICAgICAgICAgIGZpCgogICAgICAtIG5hbWU6IEJ1bXAgdmVyc2lvbgogICAgICAgIHJ1bjogfAogICAgICAgICAgcGhwICR7TU9LT19DTEl9L3ZlcnNpb25fYXV0b19idW1wLnBocCBcCiAgICAgICAgICAgIC0tcGF0aCAuIC0tYnJhbmNoICIke0dJVEhVQl9SRUZfTkFNRX0iIFwKICAgICAgICAgICAgLS10b2tlbiAiJHt7IHNlY3JldHMuTU9LT0dJVEVBX1RPS0VOIH19IiBcCiAgICAgICAgICAgIC0tcmVwby11cmwgImh0dHBzOi8veC1hY2Nlc3MtdG9rZW46JHt7IHNlY3JldHMuTU9LT0dJVEVBX1RPS0VOIH19QGdpdC5tb2tvY29uc3VsdGluZy50ZWNoLyR7eyBnaXRodWIucmVwb3NpdG9yeSB9fS5naXQiCg== \ No newline at end of file +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: MokoGitea.Workflow +# INGROUP: mokocli.Release +# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic +# 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 + MOKOGITEA_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') && + !startsWith(github.event.repository.name, 'Template-') + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.MOKOGITEA_TOKEN }} + fetch-depth: 1 + + - name: Setup mokocli tools + run: | + if ! command -v composer &> /dev/null; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + if [ -d "/opt/mokocli/cli" ]; then + echo "MOKO_CLI=/opt/mokocli/cli" >> "$GITHUB_ENV" + else + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/MokoCLI.git" \ + /tmp/mokocli + cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet + echo "MOKO_CLI=/tmp/mokocli/cli" >> "$GITHUB_ENV" + fi + + - name: Bump version + run: | + php ${MOKO_CLI}/version_auto_bump.php \ + --path . --branch "${GITHUB_REF_NAME}" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" \ + --repo-url "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml index 2383115..a7563e0 100644 --- a/.mokogitea/workflows/auto-release.yml +++ b/.mokogitea/workflows/auto-release.yml @@ -1 +1,497 @@ -IyBDb3B5cmlnaHQgKEMpIDIwMjYgTW9rbyBDb25zdWx0aW5nIDxoZWxsb0Btb2tvY29uc3VsdGluZy50ZWNoPgojCiMgU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IEdQTC0zLjAtb3ItbGF0ZXIKIwojIEZJTEUgSU5GT1JNQVRJT04KIyBERUZHUk9VUDogR2l0ZWEuV29ya2Zsb3cKIyBJTkdST1VQOiBtb2tvY2xpLlJlbGVhc2UKIyBSRVBPOiBodHRwczovL2dpdC5tb2tvY29uc3VsdGluZy50ZWNoL21va29jb25zdWx0aW5nLXRlY2gvbW9rb2NsaQojIFBBVEg6IC90ZW1wbGF0ZXMvd29ya2Zsb3dzL3VuaXZlcnNhbC9hdXRvLXJlbGVhc2UueW1sLnRlbXBsYXRlCiMgVkVSU0lPTjogMDUuMDAuMDAKIyBCUklFRjogVW5pdmVyc2FsIGJ1aWxkICYgcmVsZWFzZSDvv70gZGV0ZWN0cyBwbGF0Zm9ybSBmcm9tIG1hbmlmZXN0LnhtbAojCiMgKz09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09KwojIHwgICAgICAgICAgICAgIFVOSVZFUlNBTCBCVUlMRCAmIFJFTEVBU0UgUElQRUxJTkUgICAgICAgICAgICAgICAgICAgICAgICB8CiMgKz09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09KwojIHwgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB8CiMgfCAgUmVhZHMgbWFuaWZlc3QueG1sIChqb29tbGF8ZG9saWJhcnJ8Z2VuZXJpYykgdG8gYnJhbmNoIGxvZ2ljLiAgICAgICB8CiMgfCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHwKIyB8ICBQbGF0Zm9ybS1zcGVjaWZpYzogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfAojIHwgICAgam9vbWxhOiAgIFhNTCBtYW5pZmVzdCwgdHlwZS1wcmVmaXhlZCBwYWNrYWdlcyAgICAgICAgICAgICAgICAgICAgICB8CiMgfCAgICBkb2xpYmFycjogbW9kKi5jbGFzcy5waHAsIHVwZGF0ZS50eHQsIGRldiB2ZXJzaW9uIHJlc2V0ICAgICAgICAgICAgIHwKIyB8ICAgIGdlbmVyaWM6ICBSRUFETUUtb25seSwgbm8gdXBkYXRlIHN0cmVhbSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfAojIHwgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB8CiMgKz09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09KwoKbmFtZTogIlVuaXZlcnNhbDogQnVpbGQgJiBSZWxlYXNlIgoKb246CiAgcHVsbF9yZXF1ZXN0OgogICAgdHlwZXM6IFtvcGVuZWQsIHN5bmNocm9uaXplLCBjbG9zZWRdCiAgICBicmFuY2hlczoKICAgICAgLSBtYWluCiAgICBwYXRocy1pZ25vcmU6CiAgICAgIC0gJy5tb2tvZ2l0ZWEvd29ya2Zsb3dzLyoqJwogICAgICAtICcqLm1kJwogICAgICAtICd3aWtpLyoqJwogICAgICAtICcuZWRpdG9yY29uZmlnJwogICAgICAtICcuZ2l0aWdub3JlJwogICAgICAtICcuZ2l0YXR0cmlidXRlcycKICAgICAgLSAnLmdpdG1lc3NhZ2UnCiAgICAgIC0gJ0xJQ0VOU0UnIAogIHdvcmtmbG93X2Rpc3BhdGNoOgogICAgaW5wdXRzOgogICAgICBhY3Rpb246CiAgICAgICAgZGVzY3JpcHRpb246ICdBY3Rpb24gdG8gcGVyZm9ybScKICAgICAgICByZXF1aXJlZDogZmFsc2UKICAgICAgICB0eXBlOiBjaG9pY2UKICAgICAgICBkZWZhdWx0OiByZWxlYXNlCiAgICAgICAgb3B0aW9uczoKICAgICAgICAgIC0gcmVsZWFzZQogICAgICAgICAgLSBwcm9tb3RlLXJjCgplbnY6CiAgRk9SQ0VfSkFWQVNDUklQVF9BQ1RJT05TX1RPX05PREUyNDogdHJ1ZQogIE1PS09HSVRFQV9VUkw6ICR7eyB2YXJzLkdJVEVBX1VSTCB8fCAnaHR0cHM6Ly9naXQubW9rb2NvbnN1bHRpbmcudGVjaCcgfX0KICBHSVRFQV9PUkc6ICR7eyB2YXJzLkdJVEVBX09SRyB8fCBnaXRodWIucmVwb3NpdG9yeV9vd25lciB9fQogIEdJVEVBX1JFUE86ICR7eyB2YXJzLkdJVEVBX1JFUE8gfHwgZ2l0aHViLmV2ZW50LnJlcG9zaXRvcnkubmFtZSB9fQoKcGVybWlzc2lvbnM6CiAgY29udGVudHM6IHdyaXRlCgpqb2JzOgogICMg4pSA4pSAIFBSIE9wZW5lZCDihpIgUmVuYW1lIGJyYW5jaCB0byBSQyBhbmQgYnVpbGQgUkMgcmVsZWFzZSDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKICBwcm9tb3RlLXJjOgogICAgbmFtZTogUHJvbW90ZSB0byBSQwogICAgcnVucy1vbjogcmVsZWFzZQogICAgaWY6ID4tCiAgICAgIChnaXRodWIuZXZlbnQuYWN0aW9uID09ICdvcGVuZWQnICYmIGdpdGh1Yi5ldmVudC5wdWxsX3JlcXVlc3QubWVyZ2VkICE9IHRydWUpIHx8CiAgICAgIChnaXRodWIuZXZlbnQuYWN0aW9uID09ICdzeW5jaHJvbml6ZScgJiYgZ2l0aHViLmV2ZW50LnB1bGxfcmVxdWVzdC5tZXJnZWQgIT0gdHJ1ZSkgfHwKICAgICAgKGdpdGh1Yi5ldmVudF9uYW1lID09ICd3b3JrZmxvd19kaXNwYXRjaCcgJiYgaW5wdXRzLmFjdGlvbiA9PSAncHJvbW90ZS1yYycpCgogICAgc3RlcHM6CiAgICAgIC0gbmFtZTogQ2hlY2tvdXQgcmVwb3NpdG9yeQogICAgICAgIHVzZXM6IGFjdGlvbnMvY2hlY2tvdXRAZGUwZmFjMmU0NTAwZGFiZTAwMDllNjcyMTRmZjVmNTQ0N2NlODNkZCAjIHY2CiAgICAgICAgd2l0aDoKICAgICAgICAgIHRva2VuOiAke3sgc2VjcmV0cy5NT0tPR0lURUFfVE9LRU4gfX0KICAgICAgICAgIGZldGNoLWRlcHRoOiAxCgogICAgICAtIG5hbWU6IFNldHVwIG1va29jbGkgdG9vbHMKICAgICAgICBlbnY6CiAgICAgICAgICBNT0tPX0NMT05FX1RPS0VOOiAke3sgc2VjcmV0cy5NT0tPR0lURUFfVE9LRU4gfX0KICAgICAgICAgIE1PS09fQ0xPTkVfSE9TVDogZ2l0Lm1va29jb25zdWx0aW5nLnRlY2gvTW9rb0NvbnN1bHRpbmcKICAgICAgICBydW46IHwKICAgICAgICAgIGlmIFsgLWYgL29wdC9tb2tvY2xpL2NsaS92ZXJzaW9uX2J1bXAucGhwIF0gJiYgWyAtZiAvb3B0L21va29jbGkvdmVuZG9yL2F1dG9sb2FkLnBocCBdOyB0aGVuCiAgICAgICAgICAgIGVjaG8gVXNpbmcgcHJlLWluc3RhbGxlZCAvb3B0L21va29jbGkKICAgICAgICAgICAgZWNobyBNT0tPX0NMST0vb3B0L21va29jbGkvY2xpID4+ICRHSVRIVUJfRU5WCiAgICAgICAgICBlbHNlCiAgICAgICAgICAgIGVjaG8gRmFsbGluZyBiYWNrIHRvIGZyZXNoIGNsb25lCiAgICAgICAgICAgIGlmICEgY29tbWFuZCAtdiBjb21wb3NlciA+IC9kZXYvbnVsbCAyPiYxOyB0aGVuCiAgICAgICAgICAgICAgc3VkbyBhcHQtZ2V0IHVwZGF0ZSAtcXEgJiYgc3VkbyBhcHQtZ2V0IGluc3RhbGwgLXkgLXFxIHBocC1jbGkgcGhwLW1ic3RyaW5nIHBocC14bWwgcGhwLXppcCBwaHAtY3VybCBjb21wb3NlciA+IC9kZXYvbnVsbCAyPiYxCiAgICAgICAgICAgIGZpCiAgICAgICAgICAgIHJtIC1yZiAvdG1wL21va29jbGkKICAgICAgICAgICAgQ0xPTkVfVVJMPWh0dHBzOi8veC1hY2Nlc3MtdG9rZW46JHtNT0tPX0NMT05FX1RPS0VOfUAke01PS09fQ0xPTkVfSE9TVH0vbW9rb2NsaS5naXQKICAgICAgICAgICAgZ2l0IGNsb25lIC0tZGVwdGggMSAtLWJyYW5jaCBtYWluIC0tcXVpZXQgJENMT05FX1VSTCAvdG1wL21va29jbGkKICAgICAgICAgICAgY2QgL3RtcC9tb2tvY2xpCiAgICAgICAgICAgIGNvbXBvc2VyIGluc3RhbGwgLS1uby1kZXYgLS1uby1pbnRlcmFjdGlvbiAtLXF1aWV0CiAgICAgICAgICAgIGVjaG8gTU9LT19DTEk9L3RtcC9tb2tvY2xpL2NsaSA+PiAkR0lUSFVCX0VOVgogICAgICAgICAgZmkKCiAgICAgIC0gbmFtZTogUmVuYW1lIGJyYW5jaCB0byByYwogICAgICAgIHJ1bjogfAogICAgICAgICAgcGhwICR7TU9LT19DTEl9L2JyYW5jaF9yZW5hbWUucGhwIFwKICAgICAgICAgICAgLS1mcm9tICIke3sgZ2l0aHViLmV2ZW50LnB1bGxfcmVxdWVzdC5oZWFkLnJlZiB8fCAnZGV2JyB9fSIgLS10byByYyBcCiAgICAgICAgICAgIC0tdG9rZW4gIiR7eyBzZWNyZXRzLk1PS09HSVRFQV9UT0tFTiB9fSIgXAogICAgICAgICAgICAtLWFwaS1iYXNlICIke01PS09HSVRFQV9VUkx9L2FwaS92MS9yZXBvcy8ke0dJVEVBX09SR30vJHtHSVRFQV9SRVBPfSIgXAogICAgICAgICAgICAtLXByICIke3sgZ2l0aHViLmV2ZW50LnB1bGxfcmVxdWVzdC5udW1iZXIgfX0iCgogICAgICAtIG5hbWU6IENoZWNrb3V0IHJjIGFuZCBjb25maWd1cmUgZ2l0CiAgICAgICAgcnVuOiB8CiAgICAgICAgICBnaXQgZmV0Y2ggb3JpZ2luIHJjCiAgICAgICAgICBnaXQgY2hlY2tvdXQgcmMKICAgICAgICAgIGdpdCBjb25maWcgLS1sb2NhbCB1c2VyLmVtYWlsICJnaXRlYS1hY3Rpb25zW2JvdF1AbW9rb2NvbnN1bHRpbmcudGVjaCIKICAgICAgICAgIGdpdCBjb25maWcgLS1sb2NhbCB1c2VyLm5hbWUgImdpdGVhLWFjdGlvbnNbYm90XSIKICAgICAgICAgIGdpdCByZW1vdGUgc2V0LXVybCBvcmlnaW4gImh0dHBzOi8veC1hY2Nlc3MtdG9rZW46JHt7IHNlY3JldHMuTU9LT0dJVEVBX1RPS0VOIH19QGdpdC5tb2tvY29uc3VsdGluZy50ZWNoLyR7eyBnaXRodWIucmVwb3NpdG9yeSB9fS5naXQiCgogICAgICAtIG5hbWU6IFB1Ymxpc2ggUkMgcmVsZWFzZQogICAgICAgIHJ1bjogfAogICAgICAgICAgcGhwICR7TU9LT19DTEl9L3JlbGVhc2VfcHVibGlzaC5waHAgXAogICAgICAgICAgICAtLXBhdGggLiAtLXN0YWJpbGl0eSByYyAtLWJ1bXAgbWlub3IgLS1icmFuY2ggcmMgXAogICAgICAgICAgICAtLXRva2VuICIke3sgc2VjcmV0cy5NT0tPR0lURUFfVE9LRU4gfX0iCgogICAgICAtIG5hbWU6IFVwZGF0ZSBSQyByZWxlYXNlIG5vdGVzIGZyb20gQ0hBTkdFTE9HLm1kCiAgICAgICAgcnVuOiB8CiAgICAgICAgICBBUElfQkFTRT0iJHtNT0tPR0lURUFfVVJMfS9hcGkvdjEvcmVwb3MvJHtHSVRFQV9PUkd9LyR7R0lURUFfUkVQT30iCiAgICAgICAgICBUT0tFTj0iJHt7IHNlY3JldHMuTU9LT0dJVEVBX1RPS0VOIH19IgoKICAgICAgICAgICMgRXh0cmFjdCBbVW5yZWxlYXNlZF0gc2VjdGlvbiBmcm9tIGNoYW5nZWxvZwogICAgICAgICAgTk9URVM9IiIKICAgICAgICAgIGlmIFsgLWYgIkNIQU5HRUxPRy5tZCIgXTsgdGhlbgogICAgICAgICAgICBOT1RFUz0kKGF3ayAnL14jIyBcW1VucmVsZWFzZWRcXS97Zm91bmQ9MTsgbmV4dH0gL14jIyBcWy97aWYoZm91bmQpIGV4aXR9IGZvdW5ke3ByaW50fScgQ0hBTkdFTE9HLm1kKQogICAgICAgICAgZmkKICAgICAgICAgIFsgLXogIiROT1RFUyIgXSAmJiBOT1RFUz0iUmVsZWFzZSBjYW5kaWRhdGUiCgogICAgICAgICAgIyBGaW5kIHRoZSBSQyByZWxlYXNlIGFuZCB1cGRhdGUgaXRzIGJvZHkKICAgICAgICAgIFJFTEVBU0VfSUQ9JChjdXJsIC1zZiAtSCAiQXV0aG9yaXphdGlvbjogdG9rZW4gJHtUT0tFTn0iIFwKICAgICAgICAgICAgIiR7QVBJX0JBU0V9L3JlbGVhc2VzL3RhZ3MvcmVsZWFzZS1jYW5kaWRhdGUiIFwKICAgICAgICAgICAgfCBweXRob24zIC1jICJpbXBvcnQganNvbixzeXM7IHByaW50KGpzb24ubG9hZChzeXMuc3RkaW4pLmdldCgnaWQnLCcnKSkiIDI+L2Rldi9udWxsIHx8IHRydWUpCgogICAgICAgICAgaWYgWyAtbiAiJFJFTEVBU0VfSUQiIF07IHRoZW4KICAgICAgICAgICAgcHl0aG9uMyAtYyAiCiAgICAgICAgICBpbXBvcnQganNvbiwgdXJsbGliLnJlcXVlc3QKICAgICAgICAgIGJvZHkgPSBvcGVuKCcvZGV2L3N0ZGluJykucmVhZCgpCiAgICAgICAgICBwYXlsb2FkID0ganNvbi5kdW1wcyh7J2JvZHknOiBib2R5fSkuZW5jb2RlKCkKICAgICAgICAgIHJlcSA9IHVybGxpYi5yZXF1ZXN0LlJlcXVlc3QoCiAgICAgICAgICAgICAgJyR7QVBJX0JBU0V9L3JlbGVhc2VzLyR7UkVMRUFTRV9JRH0nLAogICAgICAgICAgICAgIGRhdGE9cGF5bG9hZCwgbWV0aG9kPSdQQVRDSCcsCiAgICAgICAgICAgICAgaGVhZGVycz17CiAgICAgICAgICAgICAgICAgICdBdXRob3JpemF0aW9uJzogJ3Rva2VuICR7VE9LRU59JywKICAgICAgICAgICAgICAgICAgJ0NvbnRlbnQtVHlwZSc6ICdhcHBsaWNhdGlvbi9qc29uJwogICAgICAgICAgICAgIH0pCiAgICAgICAgICB1cmxsaWIucmVxdWVzdC51cmxvcGVuKHJlcSkKICAgICAgICAgICIgPDw8ICIkTk9URVMiCiAgICAgICAgICAgIGVjaG8gIlJDIHJlbGVhc2Ugbm90ZXMgdXBkYXRlZCBmcm9tIENIQU5HRUxPRy5tZCIKICAgICAgICAgIGZpCgogICAgICAtIG5hbWU6IFN1bW1hcnkKICAgICAgICBpZjogYWx3YXlzKCkKICAgICAgICBydW46IHwKICAgICAgICAgIGVjaG8gIiMjIFByb21vdGVkIHRvIFJlbGVhc2UgQ2FuZGlkYXRlIiA+PiAkR0lUSFVCX1NURVBfU1VNTUFSWQogICAgICAgICAgZWNobyAiQnJhbmNoIHJlbmFtZWQgdG8gcmMsIG1pbm9yIGJ1bXAsIFJDIHJlbGVhc2UgYnVpbHQiID4+ICRHSVRIVUJfU1RFUF9TVU1NQVJZCgogICMg4pSA4pSAIE1lcmdlZCBQUiDihpIgQnVpbGQgJiBSZWxlYXNlIChvciBwcm9tb3RlIFJDIHRvIHN0YWJsZSkg4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSACiAgcmVsZWFzZToKICAgIG5hbWU6IEJ1aWxkICYgUmVsZWFzZSBQaXBlbGluZQogICAgcnVucy1vbjogcmVsZWFzZQogICAgaWY6ID4tCiAgICAgIGdpdGh1Yi5ldmVudC5wdWxsX3JlcXVlc3QubWVyZ2VkID09IHRydWUgfHwKICAgICAgKGdpdGh1Yi5ldmVudF9uYW1lID09ICd3b3JrZmxvd19kaXNwYXRjaCcgJiYgaW5wdXRzLmFjdGlvbiAhPSAncHJvbW90ZS1yYycpCgogICAgc3RlcHM6CiAgICAgIC0gbmFtZTogQ2hlY2tvdXQgcmVwb3NpdG9yeQogICAgICAgIHVzZXM6IGFjdGlvbnMvY2hlY2tvdXRAZGUwZmFjMmU0NTAwZGFiZTAwMDllNjcyMTRmZjVmNTQ0N2NlODNkZCAjIHY2CiAgICAgICAgd2l0aDoKICAgICAgICAgIHRva2VuOiAke3sgc2VjcmV0cy5NT0tPR0lURUFfVE9LRU4gfX0KICAgICAgICAgIGZldGNoLWRlcHRoOiAwCgogICAgICAtIG5hbWU6IENvbmZpZ3VyZSBnaXQgZm9yIGJvdCBwdXNoZXMKICAgICAgICBydW46IHwKICAgICAgICAgIGdpdCBjb25maWcgLS1sb2NhbCB1c2VyLmVtYWlsICJnaXRlYS1hY3Rpb25zW2JvdF1AbW9rb2NvbnN1bHRpbmcudGVjaCIKICAgICAgICAgIGdpdCBjb25maWcgLS1sb2NhbCB1c2VyLm5hbWUgImdpdGVhLWFjdGlvbnNbYm90XSIKICAgICAgICAgIGdpdCByZW1vdGUgc2V0LXVybCBvcmlnaW4gImh0dHBzOi8veC1hY2Nlc3MtdG9rZW46JHt7IHNlY3JldHMuTU9LT0dJVEVBX1RPS0VOIH19QGdpdC5tb2tvY29uc3VsdGluZy50ZWNoLyR7eyBnaXRodWIucmVwb3NpdG9yeSB9fS5naXQiCgogICAgICAtIG5hbWU6IENoZWNrIGZvciBtZXJnZSBjb25mbGljdCBtYXJrZXJzCiAgICAgICAgcnVuOiB8CiAgICAgICAgICBDT05GTElDVFM9JChncmVwIC1ybiAnPDw8PDw8PCBcfD4+Pj4+Pj4gXHxePT09PT09PSQnIC0taW5jbHVkZT0nKi5waHAnIC0taW5jbHVkZT0nKi54bWwnIC0taW5jbHVkZT0nKi5jc3MnIC0taW5jbHVkZT0nKi5qcycgLS1pbmNsdWRlPScqLmpzb24nIC0taW5jbHVkZT0nKi5tZCcgLS1pbmNsdWRlPScqLnltbCcgLS1pbmNsdWRlPScqLnlhbWwnIC0taW5jbHVkZT0nKi5pbmknIC0taW5jbHVkZT0nKi50eHQnIC4gMj4vZGV2L251bGwgfCBncmVwIC12ICcuZ2l0LycgfHwgdHJ1ZSkKICAgICAgICAgIGlmIFsgLW4gIiRDT05GTElDVFMiIF07IHRoZW4KICAgICAgICAgICAgZWNobyAiOjplcnJvcjo6TWVyZ2UgY29uZmxpY3QgbWFya2VycyBmb3VuZCDigJQgYWJvcnRpbmcgcmVsZWFzZSIKICAgICAgICAgICAgZWNobyAiIyMgUmVsZWFzZSBCbG9ja2VkOiBDb25mbGljdCBNYXJrZXJzIiA+PiAkR0lUSFVCX1NURVBfU1VNTUFSWQogICAgICAgICAgICBlY2hvICdgYGAnID4+ICRHSVRIVUJfU1RFUF9TVU1NQVJZCiAgICAgICAgICAgIGVjaG8gIiRDT05GTElDVFMiID4+ICRHSVRIVUJfU1RFUF9TVU1NQVJZCiAgICAgICAgICAgIGVjaG8gJ2BgYCcgPj4gJEdJVEhVQl9TVEVQX1NVTU1BUlkKICAgICAgICAgICAgZXhpdCAxCiAgICAgICAgICBmaQogICAgICAgICAgZWNobyAiTm8gY29uZmxpY3QgbWFya2VycyBmb3VuZCIKCiAgICAgIC0gbmFtZTogU2V0dXAgbW9rb2NsaSB0b29scwogICAgICAgIGVudjoKICAgICAgICAgIE1PS09fQ0xPTkVfVE9LRU46ICR7eyBzZWNyZXRzLk1PS09HSVRFQV9UT0tFTiB9fQogICAgICAgICAgTU9LT19DTE9ORV9IT1NUOiBnaXQubW9rb2NvbnN1bHRpbmcudGVjaC9Nb2tvQ29uc3VsdGluZwogICAgICAgICAgQ09NUE9TRVJfQVVUSDogJ3siZ2l0aHViLW9hdXRoIjp7ImdpdGh1Yi5jb20iOiIke3sgc2VjcmV0cy5HSF9NSVJST1JfVE9LRU4gfX0ifX0nCiAgICAgICAgcnVuOiB8CiAgICAgICAgICBpZiBbIC1mIC9vcHQvbW9rb2NsaS9jbGkvdmVyc2lvbl9idW1wLnBocCBdICYmIFsgLWYgL29wdC9tb2tvY2xpL3ZlbmRvci9hdXRvbG9hZC5waHAgXTsgdGhlbgogICAgICAgICAgICBlY2hvIFVzaW5nIHByZS1pbnN0YWxsZWQgL29wdC9tb2tvY2xpCiAgICAgICAgICAgIGVjaG8gTU9LT19DTEk9L29wdC9tb2tvY2xpL2NsaSA+PiAkR0lUSFVCX0VOVgogICAgICAgICAgZWxzZQogICAgICAgICAgICBlY2hvIEZhbGxpbmcgYmFjayB0byBmcmVzaCBjbG9uZQogICAgICAgICAgICBpZiAhIGNvbW1hbmQgLXYgY29tcG9zZXIgPiAvZGV2L251bGwgMj4mMTsgdGhlbgogICAgICAgICAgICAgIHN1ZG8gYXB0LWdldCB1cGRhdGUgLXFxICYmIHN1ZG8gYXB0LWdldCBpbnN0YWxsIC15IC1xcSBwaHAtY2xpIHBocC1tYnN0cmluZyBwaHAteG1sIHBocC16aXAgcGhwLWN1cmwgY29tcG9zZXIgPiAvZGV2L251bGwgMj4mMQogICAgICAgICAgICBmaQogICAgICAgICAgICBybSAtcmYgL3RtcC9tb2tvY2xpCiAgICAgICAgICAgIENMT05FX1VSTD1odHRwczovL3gtYWNjZXNzLXRva2VuOiR7TU9LT19DTE9ORV9UT0tFTn1AJHtNT0tPX0NMT05FX0hPU1R9L21va29jbGkuZ2l0CiAgICAgICAgICAgIGdpdCBjbG9uZSAtLWRlcHRoIDEgLS1icmFuY2ggbWFpbiAtLXF1aWV0ICRDTE9ORV9VUkwgL3RtcC9tb2tvY2xpCiAgICAgICAgICAgIGNkIC90bXAvbW9rb2NsaQogICAgICAgICAgICBjb21wb3NlciBpbnN0YWxsIC0tbm8tZGV2IC0tbm8taW50ZXJhY3Rpb24gLS1xdWlldAogICAgICAgICAgICBlY2hvIE1PS09fQ0xJPS90bXAvbW9rb2NsaS9jbGkgPj4gJEdJVEhVQl9FTlYKICAgICAgICAgIGZpCgogICAgICAtIG5hbWU6ICJEZXRlY3QgcGxhdGZvcm0iCiAgICAgICAgaWQ6IHBsYXRmb3JtCiAgICAgICAgcnVuOiB8CiAgICAgICAgICBwaHAgJHtNT0tPX0NMSX0vcGxhdGZvcm1fZGV0ZWN0LnBocCAtLXBhdGggLiAtLWdpdGh1Yi1vdXRwdXQgMj4vZGV2L251bGwgfHwgdHJ1ZQogICAgICAgICAgcGhwICR7TU9LT19DTEl9L21hbmlmZXN0X3JlYWQucGhwIC0tcGF0aCAuIC0tZ2l0aHViLW91dHB1dCAyPi9kZXYvbnVsbCB8fCB0cnVlCgogICAgICAtIG5hbWU6ICJEZXRlcm1pbmUgdmVyc2lvbiBidW1wIGxldmVsIgogICAgICAgIGlkOiBidW1wCiAgICAgICAgcnVuOiB8CiAgICAgICAgICAjIEZpeC9wYXRjaCBicmFuY2hlczogdmVyc2lvbiB3YXMgYWxyZWFkeSBidW1wZWQgYnkgcHJlLXJlbGVhc2UsIGp1c3Qgc3RyaXAgc3VmZml4CiAgICAgICAgICAjIEZlYXR1cmUvZGV2IGJyYW5jaGVzOiBidW1wIG1pbm9yIGZvciB0aGUgbmV3IHN0YWJsZSByZWxlYXNlCiAgICAgICAgICBIRUFEX1JFRj0iJHt7IGdpdGh1Yi5ldmVudC5wdWxsX3JlcXVlc3QuaGVhZC5yZWYgfHwgJ2RldicgfX0iCiAgICAgICAgICBjYXNlICIkSEVBRF9SRUYiIGluCiAgICAgICAgICAgIGZpeC8qfHBhdGNoLyp8aG90Zml4Lyp8YnVnZml4LyopIEJVTVA9Im5vbmUiIDs7CiAgICAgICAgICAgICopICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIEJVTVA9Im1pbm9yIiA7OwogICAgICAgICAgZXNhYwogICAgICAgICAgZWNobyAibGV2ZWw9JHtCVU1QfSIgPj4gIiRHSVRIVUJfT1VUUFVUIgogICAgICAgICAgZWNobyAiQnVtcCBsZXZlbDogJHtCVU1QfSAoZnJvbSBicmFuY2g6ICR7SEVBRF9SRUZ9KSIKCiAgICAgIC0gbmFtZTogIlB1Ymxpc2ggc3RhYmxlIHJlbGVhc2UiCiAgICAgICAgcnVuOiB8CiAgICAgICAgICBCVU1QX0ZMQUc9IiIKICAgICAgICAgIGlmIFsgIiR7eyBzdGVwcy5idW1wLm91dHB1dHMubGV2ZWwgfX0iICE9ICJub25lIiBdOyB0aGVuCiAgICAgICAgICAgIEJVTVBfRkxBRz0iLS1idW1wICR7eyBzdGVwcy5idW1wLm91dHB1dHMubGV2ZWwgfX0iCiAgICAgICAgICBmaQogICAgICAgICAgcGhwICR7TU9LT19DTEl9L3JlbGVhc2VfcHVibGlzaC5waHAgXAogICAgICAgICAgICAtLXBhdGggLiAtLXN0YWJpbGl0eSBzdGFibGUgJHtCVU1QX0ZMQUd9IC0tYnJhbmNoIG1haW4gXAogICAgICAgICAgICAtLXRva2VuICIke3sgc2VjcmV0cy5NT0tPR0lURUFfVE9LRU4gfX0iCgogICAgICAtIG5hbWU6ICJSZWFkIHB1Ymxpc2hlZCB2ZXJzaW9uIgogICAgICAgIGlkOiB2ZXJzaW9uCiAgICAgICAgcnVuOiB8CiAgICAgICAgICBWRVJTSU9OPSQocGhwICR7TU9LT19DTEl9L3ZlcnNpb25fcmVhZC5waHAgLS1wYXRoIC4gMj4vZGV2L251bGwgfHwgZWNobyAiIikKICAgICAgICAgIFZFUlNJT049JChlY2hvICIkVkVSU0lPTiIgfCBzZWQgJ3MvLVwoZGV2XHxhbHBoYVx8YmV0YVx8cmNcKSQvLycpCiAgICAgICAgICBbIC16ICIkVkVSU0lPTiIgXSAmJiBWRVJTSU9OPSIwMC4wMC4wMCIgJiYgZWNobyAic2tpcD10cnVlIiA+PiAiJEdJVEhVQl9PVVRQVVQiCiAgICAgICAgICBlY2hvICJ2ZXJzaW9uPSR7VkVSU0lPTn0iID4+ICIkR0lUSFVCX09VVFBVVCIKICAgICAgICAgIFBMQVRGT1JNPSIke3sgc3RlcHMucGxhdGZvcm0ub3V0cHV0cy5wbGF0Zm9ybSB9fSIKICAgICAgICAgIGlmIFtbICIkUExBVEZPUk0iID09IGpvb21sYSogXV07IHRoZW4KICAgICAgICAgICAgZWNobyAidGFnPXN0YWJsZSIgPj4gIiRHSVRIVUJfT1VUUFVUIgogICAgICAgICAgICBlY2hvICJyZWxlYXNlX3RhZz1zdGFibGUiID4+ICIkR0lUSFVCX09VVFBVVCIKICAgICAgICAgIGVsc2UKICAgICAgICAgICAgZWNobyAidGFnPXYke1ZFUlNJT059IiA+PiAiJEdJVEhVQl9PVVRQVVQiCiAgICAgICAgICAgIGVjaG8gInJlbGVhc2VfdGFnPXYke1ZFUlNJT059IiA+PiAiJEdJVEhVQl9PVVRQVVQiCiAgICAgICAgICBmaQogICAgICAgICAgZWNobyAiYnJhbmNoPW1haW4iID4+ICIkR0lUSFVCX09VVFBVVCIKICAgICAgICAgIGVjaG8gIlB1Ymxpc2hlZCB2ZXJzaW9uOiAke1ZFUlNJT059IgoKICAgICAgLSBuYW1lOiAiQ3JlYXRlIHNlbXZlciB0YWcgZm9yIG5vbi1Kb29tbGEgcmVwb3MiCiAgICAgICAgaWQ6IHNlbXZlcgogICAgICAgIGlmOiB8CiAgICAgICAgICBzdGVwcy52ZXJzaW9uLm91dHB1dHMuc2tpcCAhPSAndHJ1ZScgJiYKICAgICAgICAgICFzdGFydHNXaXRoKHN0ZXBzLnBsYXRmb3JtLm91dHB1dHMucGxhdGZvcm0sICdqb29tbGEnKQogICAgICAgIHJ1bjogfAogICAgICAgICAgVkVSU0lPTj0iJHt7IHN0ZXBzLnZlcnNpb24ub3V0cHV0cy52ZXJzaW9uIH19IgogICAgICAgICAgQVBJX0JBU0U9IiR7TU9LT0dJVEVBX1VSTH0vYXBpL3YxL3JlcG9zLyR7R0lURUFfT1JHfS8ke0dJVEVBX1JFUE99IgogICAgICAgICAgVE9LRU49IiR7eyBzZWNyZXRzLk1PS09HSVRFQV9UT0tFTiB9fSIKICAgICAgICAgIFNFTVZFUl9UQUc9InYke1ZFUlNJT059IgoKICAgICAgICAgIGVjaG8gIkNyZWF0aW5nIHNlbXZlciB0YWc6ICR7U0VNVkVSX1RBR30iCgogICAgICAgICAgIyBDcmVhdGUgdGhlIGdpdCB0YWcgdmlhIEFQSQogICAgICAgICAgSFRUUF9DT0RFPSQoY3VybCAtc2YgLW8gL2Rldi9udWxsIC13ICIle2h0dHBfY29kZX0iIFwKICAgICAgICAgICAgLVggUE9TVCAtSCAiQXV0aG9yaXphdGlvbjogdG9rZW4gJHtUT0tFTn0iIFwKICAgICAgICAgICAgLUggIkNvbnRlbnQtVHlwZTogYXBwbGljYXRpb24vanNvbiIgXAogICAgICAgICAgICAiJHtBUElfQkFTRX0vdGFncyIgXAogICAgICAgICAgICAtZCAie1widGFnX25hbWVcIjpcIiR7U0VNVkVSX1RBR31cIixcInRhcmdldFwiOlwibWFpblwiLFwibWVzc2FnZVwiOlwiUmVsZWFzZSAke1ZFUlNJT059XCJ9IiAyPi9kZXYvbnVsbCB8fCBlY2hvICIwMDAiKQoKICAgICAgICAgIGlmIFsgIiRIVFRQX0NPREUiID0gIjIwMSIgXSB8fCBbICIkSFRUUF9DT0RFIiA9ICIyMDAiIF07IHRoZW4KICAgICAgICAgICAgZWNobyAiQ3JlYXRlZCBzZW12ZXIgdGFnOiAke1NFTVZFUl9UQUd9IgogICAgICAgICAgZWxpZiBbICIkSFRUUF9DT0RFIiA9ICI0MDkiIF07IHRoZW4KICAgICAgICAgICAgZWNobyAiU2VtdmVyIHRhZyAke1NFTVZFUl9UQUd9IGFscmVhZHkgZXhpc3RzIChza2lwcGVkKSIKICAgICAgICAgIGVsc2UKICAgICAgICAgICAgZWNobyAiOjp3YXJuaW5nOjpGYWlsZWQgdG8gY3JlYXRlIHNlbXZlciB0YWcgJHtTRU1WRVJfVEFHfSAoSFRUUCAke0hUVFBfQ09ERX0pIgogICAgICAgICAgZmkKCiAgICAgICAgICBlY2hvICJzZW12ZXJfdGFnPSR7U0VNVkVSX1RBR30iID4+ICIkR0lUSFVCX09VVFBVVCIKCiAgICAgIC0gbmFtZTogVXBkYXRlIHJlbGVhc2Ugbm90ZXMgYW5kIHByb21vdGUgY2hhbmdlbG9nCiAgICAgICAgcnVuOiB8CiAgICAgICAgICBBUElfQkFTRT0iJHtNT0tPR0lURUFfVVJMfS9hcGkvdjEvcmVwb3MvJHtHSVRFQV9PUkd9LyR7R0lURUFfUkVQT30iCiAgICAgICAgICBUT0tFTj0iJHt7IHNlY3JldHMuTU9LT0dJVEVBX1RPS0VOIH19IgoKICAgICAgICAgICMgR2V0IHRoZSBzdGFibGUgcmVsZWFzZSBpbmZvICh2ZXJzaW9uIGFuZCBJRCkKICAgICAgICAgIFJFTEVBU0VfSlNPTj0kKGN1cmwgLXNmIC1IICJBdXRob3JpemF0aW9uOiB0b2tlbiAke1RPS0VOfSIgXAogICAgICAgICAgICAiJHtBUElfQkFTRX0vcmVsZWFzZXMvdGFncy9zdGFibGUiIDI+L2Rldi9udWxsIHx8IGVjaG8gJ3t9JykKICAgICAgICAgIFJFTEVBU0VfSUQ9JChweXRob24zIC1jICJpbXBvcnQganNvbixzeXM7IHByaW50KGpzb24ubG9hZChzeXMuc3RkaW4pLmdldCgnaWQnLCcnKSkiIDw8PCAiJFJFTEVBU0VfSlNPTiIgMj4vZGV2L251bGwgfHwgdHJ1ZSkKICAgICAgICAgICMgRXh0cmFjdCB2ZXJzaW9uIGZyb20gcmVsZWFzZSBuYW1lIChlLmcuICIwNi4xNy4wMCIgb3IgInYwNi4xNy4wMCIpCiAgICAgICAgICBWRVJTSU9OPSQocHl0aG9uMyAtYyAiCiAgICAgICAgICBpbXBvcnQganNvbiwgc3lzLCByZQogICAgICAgICAgciA9IGpzb24ubG9hZChzeXMuc3RkaW4pCiAgICAgICAgICBuYW1lID0gci5nZXQoJ25hbWUnLCAnJykKICAgICAgICAgIG0gPSByZS5zZWFyY2gocicoXGQrXC5cZCtcLlxkKyknLCBuYW1lKQogICAgICAgICAgcHJpbnQobS5ncm91cCgxKSBpZiBtIGVsc2UgJycpCiAgICAgICAgICAiIDw8PCAiJFJFTEVBU0VfSlNPTiIgMj4vZGV2L251bGwgfHwgdHJ1ZSkKCiAgICAgICAgICAjIEV4dHJhY3QgW1VucmVsZWFzZWRdIHNlY3Rpb24gZnJvbSBjaGFuZ2Vsb2cKICAgICAgICAgIE5PVEVTPSIiCiAgICAgICAgICBpZiBbIC1mICJDSEFOR0VMT0cubWQiIF07IHRoZW4KICAgICAgICAgICAgTk9URVM9JChhd2sgJy9eIyMgXFtVbnJlbGVhc2VkXF0ve2ZvdW5kPTE7IG5leHR9IC9eIyMgXFsve2lmKGZvdW5kKSBleGl0fSBmb3VuZHtwcmludH0nIENIQU5HRUxPRy5tZCkKICAgICAgICAgIGZpCiAgICAgICAgICBbIC16ICIkTk9URVMiIF0gJiYgTk9URVM9IlN0YWJsZSByZWxlYXNlIgoKICAgICAgICAgICMgVXBkYXRlIHJlbGVhc2UgYm9keSB2aWEgQVBJCiAgICAgICAgICBpZiBbIC1uICIkUkVMRUFTRV9JRCIgXTsgdGhlbgogICAgICAgICAgICBweXRob24zIC1jICIKICAgICAgICAgIGltcG9ydCBqc29uLCB1cmxsaWIucmVxdWVzdAogICAgICAgICAgYm9keSA9IG9wZW4oJy9kZXYvc3RkaW4nKS5yZWFkKCkKICAgICAgICAgIHBheWxvYWQgPSBqc29uLmR1bXBzKHsnYm9keSc6IGJvZHl9KS5lbmNvZGUoKQogICAgICAgICAgcmVxID0gdXJsbGliLnJlcXVlc3QuUmVxdWVzdCgKICAgICAgICAgICAgICAnJHtBUElfQkFTRX0vcmVsZWFzZXMvJHtSRUxFQVNFX0lEfScsCiAgICAgICAgICAgICAgZGF0YT1wYXlsb2FkLCBtZXRob2Q9J1BBVENIJywKICAgICAgICAgICAgICBoZWFkZXJzPXsKICAgICAgICAgICAgICAgICAgJ0F1dGhvcml6YXRpb24nOiAndG9rZW4gJHtUT0tFTn0nLAogICAgICAgICAgICAgICAgICAnQ29udGVudC1UeXBlJzogJ2FwcGxpY2F0aW9uL2pzb24nCiAgICAgICAgICAgICAgfSkKICAgICAgICAgIHVybGxpYi5yZXF1ZXN0LnVybG9wZW4ocmVxKQogICAgICAgICAgIiA8PDwgIiROT1RFUyIKICAgICAgICAgICAgZWNobyAiUmVsZWFzZSBub3RlcyB1cGRhdGVkIGZyb20gQ0hBTkdFTE9HLm1kIgogICAgICAgICAgZmkKCiAgICAgICAgICAjIFByb21vdGUgW1VucmVsZWFzZWRdIOKGkiBbdmVyc2lvbl0gaW4gQ0hBTkdFTE9HLm1kIGFuZCByZXNldAogICAgICAgICAgaWYgWyAtbiAiJFZFUlNJT04iIF0gJiYgWyAtZiAiQ0hBTkdFTE9HLm1kIiBdOyB0aGVuCiAgICAgICAgICAgIERBVEU9JChkYXRlICslWS0lbS0lZCkKICAgICAgICAgICAgcHl0aG9uMyAtYyAiCiAgICAgICAgICBpbXBvcnQgc3lzCiAgICAgICAgICB2ZXJzaW9uLCBkYXRlID0gc3lzLmFyZ3ZbMV0sIHN5cy5hcmd2WzJdCiAgICAgICAgICBjb250ZW50ID0gb3BlbignQ0hBTkdFTE9HLm1kJykucmVhZCgpCiAgICAgICAgICBvbGQgPSAnIyMgW1VucmVsZWFzZWRdJwogICAgICAgICAgbmV3ID0gZicjIyBbVW5yZWxlYXNlZF1cblxuIyMgW3t2ZXJzaW9ufV0gLS0tIHtkYXRlfScKICAgICAgICAgIGNvbnRlbnQgPSBjb250ZW50LnJlcGxhY2Uob2xkLCBuZXcsIDEpCiAgICAgICAgICBvcGVuKCdDSEFOR0VMT0cubWQnLCAndycpLndyaXRlKGNvbnRlbnQpCiAgICAgICAgICAiICIkVkVSU0lPTiIgIiREQVRFIgogICAgICAgICAgICBnaXQgYWRkIENIQU5HRUxPRy5tZAogICAgICAgICAgICBnaXQgY29tbWl0IC1tICJjaG9yZTogcHJvbW90ZSBjaGFuZ2Vsb2cgW1VucmVsZWFzZWRdIOKGkiBbJHtWRVJTSU9OfV0iIHx8IHRydWUKICAgICAgICAgICAgZ2l0IHB1c2ggb3JpZ2luIG1haW4gfHwgdHJ1ZQogICAgICAgICAgICBlY2hvICJDaGFuZ2Vsb2cgcHJvbW90ZWQ6IFtVbnJlbGVhc2VkXSDihpIgWyR7VkVSU0lPTn1dIgogICAgICAgICAgZmkKCiAgICAgICMgLS0gU1RFUCA5OiBNaXJyb3IgdG8gR2l0SHViIChzdGFibGUgb25seSkgLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KICAgICAgLSBuYW1lOiAiU3RlcCA5OiBNaXJyb3IgcmVsZWFzZSB0byBHaXRIdWIiCiAgICAgICAgaWY6ID4tCiAgICAgICAgICBzdGVwcy52ZXJzaW9uLm91dHB1dHMuc2tpcCAhPSAndHJ1ZScgJiYKICAgICAgICAgIHNlY3JldHMuR0hfTUlSUk9SX1RPS0VOICE9ICcnCiAgICAgICAgY29udGludWUtb24tZXJyb3I6IHRydWUKICAgICAgICBydW46IHwKICAgICAgICAgIFZFUlNJT049IiR7eyBzdGVwcy5idW1wLm91dHB1dHMudmVyc2lvbiB8fCBzdGVwcy52ZXJzaW9uLm91dHB1dHMudmVyc2lvbiB9fSIKICAgICAgICAgIFJFTEVBU0VfVEFHPSIke3sgc3RlcHMudmVyc2lvbi5vdXRwdXRzLnJlbGVhc2VfdGFnIH19IgogICAgICAgICAgR0hfUkVQTz0iJHt7IHZhcnMuR0hfTUlSUk9SX1JFUE8gfHwgZ2l0aHViLnJlcG9zaXRvcnkgfX0iCiAgICAgICAgICBBUElfQkFTRT0iJHtNT0tPR0lURUFfVVJMfS9hcGkvdjEvcmVwb3MvJHtHSVRFQV9PUkd9LyR7R0lURUFfUkVQT30iCiAgICAgICAgICBwaHAgJHtNT0tPX0NMSX0vcmVsZWFzZV9taXJyb3IucGhwIFwKICAgICAgICAgICAgLS12ZXJzaW9uICIkVkVSU0lPTiIgLS10YWcgIiRSRUxFQVNFX1RBRyIgXAogICAgICAgICAgICAtLXRva2VuICIke3sgc2VjcmV0cy5NT0tPR0lURUFfVE9LRU4gfX0iIC0tYXBpLWJhc2UgIiRBUElfQkFTRSIgXAogICAgICAgICAgICAtLWdoLXRva2VuICIke3sgc2VjcmV0cy5HSF9NSVJST1JfVE9LRU4gfX0iIC0tZ2gtcmVwbyAiJEdIX1JFUE8iIFwKICAgICAgICAgICAgLS1icmFuY2ggbWFpbiAyPiYxIHx8IHRydWUKICAgICAgICAgIGVjaG8gIkdpdEh1YiBtaXJyb3IgdXBkYXRlZCIgPj4gJEdJVEhVQl9TVEVQX1NVTU1BUlkKCiAgICAgICMgLS0gU1RFUCAxMDogU3luYyBtYWluIGJyYW5jaCB0byBHaXRIdWIgbWlycm9yIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KICAgICAgLSBuYW1lOiAiU3RlcCAxMDogUHVzaCBtYWluIHRvIEdpdEh1YiBtaXJyb3IiCiAgICAgICAgaWY6ID4tCiAgICAgICAgICBzdGVwcy52ZXJzaW9uLm91dHB1dHMuc2tpcCAhPSAndHJ1ZScgJiYKICAgICAgICAgIHNlY3JldHMuR0hfTUlSUk9SX1RPS0VOICE9ICcnCiAgICAgICAgY29udGludWUtb24tZXJyb3I6IHRydWUKICAgICAgICBydW46IHwKICAgICAgICAgIEdIX1JFUE89IiR7eyB2YXJzLkdIX01JUlJPUl9SRVBPIHx8IGdpdGh1Yi5yZXBvc2l0b3J5IH19IgogICAgICAgICAgR0hfT1JHPSQoZWNobyAiJEdIX1JFUE8iIHwgY3V0IC1kLyAtZjEpCiAgICAgICAgICBHSF9OQU1FPSQoZWNobyAiJEdIX1JFUE8iIHwgY3V0IC1kLyAtZjIpCiAgICAgICAgICBnaXQgcmVtb3RlIGFkZCBnaXRodWIgImh0dHBzOi8veC1hY2Nlc3MtdG9rZW46JHt7IHNlY3JldHMuR0hfTUlSUk9SX1RPS0VOIH19QGdpdGh1Yi5jb20vJHtHSF9PUkd9LyR7R0hfTkFNRX0uZ2l0IiAyPi9kZXYvbnVsbCB8fCBcCiAgICAgICAgICAgIGdpdCByZW1vdGUgc2V0LXVybCBnaXRodWIgImh0dHBzOi8veC1hY2Nlc3MtdG9rZW46JHt7IHNlY3JldHMuR0hfTUlSUk9SX1RPS0VOIH19QGdpdGh1Yi5jb20vJHtHSF9PUkd9LyR7R0hfTkFNRX0uZ2l0IgogICAgICAgICAgZ2l0IGZldGNoIG9yaWdpbiBtYWluIC0tZGVwdGg9MQogICAgICAgICAgZ2l0IHB1c2ggZ2l0aHViIG9yaWdpbi9tYWluOnJlZnMvaGVhZHMvbWFpbiAtLWZvcmNlIDI+L2Rldi9udWxsIFwKICAgICAgICAgICAgJiYgZWNobyAibWFpbiBicmFuY2ggcHVzaGVkIHRvIEdpdEh1YiBtaXJyb3IiIFwKICAgICAgICAgICAgfHwgZWNobyAiV0FSTklORzogR2l0SHViIG1pcnJvciBwdXNoIGZhaWxlZCIKCiAgICAgIC0gbmFtZTogIlN0ZXAgMTE6IERlbGV0ZSByYyBicmFuY2ggYW5kIHJlY3JlYXRlIGRldiBmcm9tIG1haW4iCiAgICAgICAgaWY6IHN0ZXBzLnZlcnNpb24ub3V0cHV0cy5za2lwICE9ICd0cnVlJwogICAgICAgIGNvbnRpbnVlLW9uLWVycm9yOiB0cnVlCiAgICAgICAgcnVuOiB8CiAgICAgICAgICBBUElfQkFTRT0iJHtNT0tPR0lURUFfVVJMfS9hcGkvdjEvcmVwb3MvJHtHSVRFQV9PUkd9LyR7R0lURUFfUkVQT30iCiAgICAgICAgICBUT0tFTj0iJHt7IHNlY3JldHMuTU9LT0dJVEVBX1RPS0VOIH19IgoKICAgICAgICAgICMgRGVsZXRlIHJjIGJyYW5jaCAoZXBoZW1lcmFsIOKAlCBjcmVhdGVkIGJ5IHByb21vdGUtcmMpCiAgICAgICAgICBjdXJsIC1zZiAtWCBERUxFVEUgLUggIkF1dGhvcml6YXRpb246IHRva2VuICR7VE9LRU59IiBcCiAgICAgICAgICAgICIke0FQSV9CQVNFfS9icmFuY2hlcy9yYyIgMj4vZGV2L251bGwgXAogICAgICAgICAgICAmJiBlY2hvICJEZWxldGVkIHJjIGJyYW5jaCIgfHwgZWNobyAicmMgYnJhbmNoIG5vdCBmb3VuZCIKCiAgICAgICAgICAjIERlbGV0ZSBkZXYgYnJhbmNoCiAgICAgICAgICBjdXJsIC1zZiAtWCBERUxFVEUgLUggIkF1dGhvcml6YXRpb246IHRva2VuICR7VE9LRU59IiBcCiAgICAgICAgICAgICIke0FQSV9CQVNFfS9icmFuY2hlcy9kZXYiIDI+L2Rldi9udWxsICYmIGVjaG8gIkRlbGV0ZWQgZGV2IGJyYW5jaCIKCiAgICAgICAgICAjIFJlY3JlYXRlIGRldiBmcm9tIG1haW4gKG5vdyBpbmNsdWRlcyB2ZXJzaW9uIGJ1bXAgKyBjaGFuZ2Vsb2cgcHJvbW90aW9uKQogICAgICAgICAgY3VybCAtc2YgLVggUE9TVCAtSCAiQXV0aG9yaXphdGlvbjogdG9rZW4gJHtUT0tFTn0iIFwKICAgICAgICAgICAgLUggIkNvbnRlbnQtVHlwZTogYXBwbGljYXRpb24vanNvbiIgXAogICAgICAgICAgICAiJHtBUElfQkFTRX0vYnJhbmNoZXMiIFwKICAgICAgICAgICAgLWQgJ3sibmV3X2JyYW5jaF9uYW1lIjoiZGV2Iiwib2xkX2JyYW5jaF9uYW1lIjoibWFpbiJ9JyAyPi9kZXYvbnVsbCAmJiBlY2hvICJSZWNyZWF0ZWQgZGV2IGZyb20gbWFpbiIKCiAgICAgICAgICBlY2hvICJQcmUtcmVsZWFzZSBicmFuY2hlcyBjbGVhbmVkLCBkZXYgcmVzZXQgZnJvbSBtYWluIiA+PiAkR0lUSFVCX1NURVBfU1VNTUFSWQoKICAgICAgLSBuYW1lOiAiU3RlcCAxMjogQ3JlYXRlIHZlcnNpb24gYnJhbmNoIGZyb20gbWFpbiIKICAgICAgICBpZjogc3RlcHMudmVyc2lvbi5vdXRwdXRzLnNraXAgIT0gJ3RydWUnCiAgICAgICAgY29udGludWUtb24tZXJyb3I6IHRydWUKICAgICAgICBydW46IHwKICAgICAgICAgIEFQSV9CQVNFPSIke01PS09HSVRFQV9VUkx9L2FwaS92MS9yZXBvcy8ke0dJVEVBX09SR30vJHtHSVRFQV9SRVBPfSIKICAgICAgICAgIFRPS0VOPSIke3sgc2VjcmV0cy5NT0tPR0lURUFfVE9LRU4gfX0iCiAgICAgICAgICBWRVJTSU9OPSIke3sgc3RlcHMuYnVtcC5vdXRwdXRzLnZlcnNpb24gfHwgc3RlcHMudmVyc2lvbi5vdXRwdXRzLnZlcnNpb24gfX0iCiAgICAgICAgICBCUkFOQ0hfTkFNRT0idmVyc2lvbi8ke1ZFUlNJT059IgogICAgICAgICAgTUFJTl9TSEE9JChnaXQgcmV2LXBhcnNlIEhFQUQpCgogICAgICAgICAgIyBEZWxldGUgb2xkIHZlcnNpb24gYnJhbmNoIGlmIGl0IGV4aXN0cyAoc2FtZSB2ZXJzaW9uIHJlLXJlbGVhc2UpCiAgICAgICAgICBjdXJsIC1zZiAtWCBERUxFVEUgLUggIkF1dGhvcml6YXRpb246IHRva2VuICR7VE9LRU59IiAgICAgICAgICAgICAiJHtBUElfQkFTRX0vYnJhbmNoZXMvJHtCUkFOQ0hfTkFNRX0iIDI+L2Rldi9udWxsICYmIGVjaG8gIkRlbGV0ZWQgb2xkICR7QlJBTkNIX05BTUV9IgoKICAgICAgICAgICMgQ3JlYXRlIHZlcnNpb24vWFguWVkuWlogZnJvbSBtYWluCiAgICAgICAgICBjdXJsIC1zZiAtWCBQT1NUIC1IICJBdXRob3JpemF0aW9uOiB0b2tlbiAke1RPS0VOfSIgICAgICAgICAgICAgLUggIkNvbnRlbnQtVHlwZTogYXBwbGljYXRpb24vanNvbiIgICAgICAgICAgICAgIiR7QVBJX0JBU0V9L2JyYW5jaGVzIiAgICAgICAgICAgICAtZCAie1wibmV3X2JyYW5jaF9uYW1lXCI6XCIke0JSQU5DSF9OQU1FfVwiLFwib2xkX2JyYW5jaF9uYW1lXCI6XCJtYWluXCJ9IiAyPi9kZXYvbnVsbCAgICAgICAgICAgICAmJiBlY2hvICJDcmVhdGVkICR7QlJBTkNIX05BTUV9IGZyb20gbWFpbiAoJHtNQUlOX1NIQX0pIiAgICAgICAgICAgICB8fCBlY2hvICJXQVJOSU5HOiAke0JSQU5DSF9OQU1FfSBjcmVhdGlvbiBmYWlsZWQiCgogICAgICAgICAgZWNobyAiVmVyc2lvbiBicmFuY2ggY3JlYXRlZDogJHtCUkFOQ0hfTkFNRX0gKCR7TUFJTl9TSEF9KSIgPj4gJEdJVEhVQl9TVEVQX1NVTU1BUlkKCgoKICAgICAgIyAtLSBEb2xpYmFyciBwb3N0LXJlbGVhc2U6IFJlc2V0IGRldiB2ZXJzaW9uIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCiAgICAgIC0gbmFtZTogIlBvc3QtcmVsZWFzZTogUmVzZXQgZGV2IHZlcnNpb24iCiAgICAgICAgaWY6IHN0ZXBzLnZlcnNpb24ub3V0cHV0cy5za2lwICE9ICd0cnVlJwogICAgICAgIGNvbnRpbnVlLW9uLWVycm9yOiB0cnVlCiAgICAgICAgcnVuOiB8CiAgICAgICAgICBBUElfQkFTRT0iJHtNT0tPR0lURUFfVVJMfS9hcGkvdjEvcmVwb3MvJHtHSVRFQV9PUkd9LyR7R0lURUFfUkVQT30iCiAgICAgICAgICBwaHAgJHtNT0tPX0NMSX0vdmVyc2lvbl9yZXNldF9kZXYucGhwIFwKICAgICAgICAgICAgLS10b2tlbiAiJHt7IHNlY3JldHMuTU9LT0dJVEVBX1RPS0VOIH19IiAtLWFwaS1iYXNlICIke0FQSV9CQVNFfSIgXAogICAgICAgICAgICAtLWJyYW5jaCBkZXYgLS1wYXRoIC4gMj4mMSB8fCB0cnVlCgogICAgICAjIC0tIFN1bW1hcnkgLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KICAgICAgLSBuYW1lOiBQaXBlbGluZSBTdW1tYXJ5CiAgICAgICAgaWY6IGFsd2F5cygpCiAgICAgICAgcnVuOiB8CiAgICAgICAgICBWRVJTSU9OPSIke3sgc3RlcHMuYnVtcC5vdXRwdXRzLnZlcnNpb24gfHwgc3RlcHMudmVyc2lvbi5vdXRwdXRzLnZlcnNpb24gfX0iCiAgICAgICAgICBQTEFURk9STT0iJHt7IHN0ZXBzLnBsYXRmb3JtLm91dHB1dHMucGxhdGZvcm0gfX0iCiAgICAgICAgICBpZiBbICIke3sgc3RlcHMudmVyc2lvbi5vdXRwdXRzLnNraXAgfX0iID0gInRydWUiIF07IHRoZW4KICAgICAgICAgICAgZWNobyAiIyMgUmVsZWFzZSBTa2lwcGVkIiA+PiAkR0lUSFVCX1NURVBfU1VNTUFSWQogICAgICAgICAgICBlY2hvICJObyBWRVJTSU9OIGluIFJFQURNRS5tZCIgPj4gJEdJVEhVQl9TVEVQX1NVTU1BUlkKICAgICAgICAgIGVsaWYgWyAiJHt7IHN0ZXBzLmNoZWNrLm91dHB1dHMuYWxyZWFkeV9yZWxlYXNlZCB9fSIgPSAidHJ1ZSIgXTsgdGhlbgogICAgICAgICAgICBlY2hvICIjIyBBbHJlYWR5IFJlbGVhc2VkIOKAlCAke1ZFUlNJT059IiA+PiAkR0lUSFVCX1NURVBfU1VNTUFSWQogICAgICAgICAgZWxzZQogICAgICAgICAgICBlY2hvICIiID4+ICRHSVRIVUJfU1RFUF9TVU1NQVJZCiAgICAgICAgICAgIGVjaG8gIiMjIEJ1aWxkICYgUmVsZWFzZSBDb21wbGV0ZSAoJHtQTEFURk9STX0pIiA+PiAkR0lUSFVCX1NURVBfU1VNTUFSWQogICAgICAgICAgICBlY2hvICIiID4+ICRHSVRIVUJfU1RFUF9TVU1NQVJZCiAgICAgICAgICAgIGVjaG8gInwgU3RlcCB8IFJlc3VsdCB8IiA+PiAkR0lUSFVCX1NURVBfU1VNTUFSWQogICAgICAgICAgICBlY2hvICJ8LS0tLS0tfC0tLS0tLS0tfCIgPj4gJEdJVEhVQl9TVEVQX1NVTU1BUlkKICAgICAgICAgICAgZWNobyAifCBQbGF0Zm9ybSB8IFxgJHtQTEFURk9STX1cYCB8IiA+PiAkR0lUSFVCX1NURVBfU1VNTUFSWQogICAgICAgICAgICBlY2hvICJ8IFZlcnNpb24gfCBcYCR7VkVSU0lPTn1cYCB8IiA+PiAkR0lUSFVCX1NURVBfU1VNTUFSWQogICAgICAgICAgICBlY2hvICJ8IEJyYW5jaCB8IFxgJHt7IHN0ZXBzLnZlcnNpb24ub3V0cHV0cy5icmFuY2ggfX1cYCB8IiA+PiAkR0lUSFVCX1NURVBfU1VNTUFSWQogICAgICAgICAgICBlY2hvICJ8IFRhZyB8IFxgJHt7IHN0ZXBzLnZlcnNpb24ub3V0cHV0cy50YWcgfX1cYCB8IiA+PiAkR0lUSFVCX1NURVBfU1VNTUFSWQogICAgICAgICAgICBlY2hvICJ8IFJlbGVhc2UgfCBbVmlld10oJHtNT0tPR0lURUFfVVJMfS8ke0dJVEVBX09SR30vJHtHSVRFQV9SRVBPfS9yZWxlYXNlcy90YWcvJHt7IHN0ZXBzLnZlcnNpb24ub3V0cHV0cy50YWcgfX0pIHwiID4+ICRHSVRIVUJfU1RFUF9TVU1NQVJZCiAgICAgICAgICBmaQo= \ No newline at end of file +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: MokoGitea.Workflow +# INGROUP: mokocli.Release +# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic +# PATH: /.mokogitea/workflows/auto-release.yml +# VERSION: 05.01.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, synchronize, closed] + branches: + - main + paths-ignore: + - '.mokogitea/workflows/**' + - '*.md' + - 'wiki/**' + - '.editorconfig' + - '.gitignore' + - '.gitattributes' + - '.gitmessage' + - 'LICENSE' + 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 + MOKOGITEA_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 + # Skip on template repos (Template-*) — they scaffold other repos and do not release. + if: >- + !startsWith(github.event.repository.name, 'Template-') && + ( + (github.event.action == 'opened' && github.event.pull_request.merged != true) || + (github.event.action == 'synchronize' && 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 + submodules: recursive + + - name: Setup mokocli tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + run: | + if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then + echo Using pre-installed /opt/mokocli + echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV + else + echo Falling back to fresh clone + if ! command -v composer > /dev/null 2>&1; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1 + fi + rm -rf /tmp/mokocli + CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoCLI.git + git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli + cd /tmp/mokocli + composer install --no-dev --no-interaction --quiet + echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV + fi + + - name: Rename branch to rc + run: | + API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + AUTH="Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" + FROM="${{ github.event.pull_request.head.ref || 'dev' }}" + PR="${{ github.event.pull_request.number }}" + + # Resolve the source branch HEAD commit. + SRC_JSON=$(curl -sf -H "$AUTH" "${API_BASE}/branches/${FROM}") \ + || { echo "::error::Source branch ${FROM} not found"; exit 1; } + SRC_SHA=$(printf '%s' "$SRC_JSON" | python3 -c "import sys, json; print(json.load(sys.stdin)['commit']['id'])" 2>/dev/null || true) + [ -n "$SRC_SHA" ] || { echo "::error::Could not resolve HEAD of ${FROM}"; exit 1; } + + # Point rc at the source commit. If rc already exists (a protected branch that + # cannot be deleted), force-update its ref in place instead of delete+recreate: + # deleting a protected branch fails, which then makes the recreate return HTTP 409. + if curl -sf -o /dev/null -H "$AUTH" "${API_BASE}/branches/rc"; then + echo "rc exists - force-updating to ${FROM} (${SRC_SHA})" + curl -sf -X PATCH -H "$AUTH" -H "Content-Type: application/json" \ + "${API_BASE}/git/refs/heads/rc" -d "{\"sha\":\"${SRC_SHA}\",\"force\":true}" \ + || { echo "::error::Failed to force-update rc (CI token needs force-push on the protected rc branch)"; exit 1; } + else + echo "Creating rc from ${FROM}" + curl -sf -X POST -H "$AUTH" -H "Content-Type: application/json" \ + "${API_BASE}/branches" -d "{\"new_branch_name\":\"rc\",\"old_branch_name\":\"${FROM}\"}" \ + || { echo "::error::Failed to create rc from ${FROM}"; exit 1; } + fi + + # Repoint the PR at rc, then delete the old source branch (non-fatal). + if [ -n "$PR" ]; then + curl -s -X PATCH -H "$AUTH" -H "Content-Type: application/json" \ + "${API_BASE}/pulls/${PR}" -d '{"head":"rc"}' >/dev/null || true + fi + curl -s -X DELETE -H "$AUTH" "${API_BASE}/branches/${FROM}" >/dev/null || true + echo "Renamed ${FROM} -> rc" + + - name: Checkout rc and configure git + run: | + git fetch origin rc + git checkout rc + git config --local user.email "mokogitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "mokogitea-actions[bot]" + git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + + - name: Publish RC release + run: | + php ${MOKO_CLI}/release_publish.php \ + --path . --stability rc --bump minor --branch rc \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" + + - name: Update RC release notes from CHANGELOG.md + run: | + API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" + + # Extract [Unreleased] section from changelog + NOTES="" + if [ -f "CHANGELOG.md" ]; then + NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md) + fi + [ -z "$NOTES" ] && NOTES="Release candidate" + + # Find the RC release and update its body + RELEASE_ID=$(curl -sf -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/releases/tags/release-candidate" \ + | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) + + if [ -n "$RELEASE_ID" ]; then + python3 -c " + import json, urllib.request + body = open('/dev/stdin').read() + payload = json.dumps({'body': body}).encode() + req = urllib.request.Request( + '${API_BASE}/releases/${RELEASE_ID}', + data=payload, method='PATCH', + headers={ + 'Authorization': 'token ${TOKEN}', + 'Content-Type': 'application/json' + }) + urllib.request.urlopen(req) + " <<< "$NOTES" + echo "RC release notes updated from CHANGELOG.md" + fi + + - name: Summary + if: always() + run: | + echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY + echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY + + # ── Merged PR → Build & Release (or promote RC to stable) ───────────────────────── + release: + name: Build & Release Pipeline + runs-on: release + # Skip on template repos (Template-*) — they scaffold other repos and do not release. + if: >- + !startsWith(github.event.repository.name, 'Template-') && + ( + 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 + submodules: recursive + + - name: Configure git for bot pushes + run: | + git config --local user.email "mokogitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "mokogitea-actions[bot]" + git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + + - name: Check for merge conflict markers + run: | + CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true) + if [ -n "$CONFLICTS" ]; then + echo "::error::Merge conflict markers found — aborting release" + echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + exit 1 + fi + echo "No conflict markers found" + + - name: Setup mokocli tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}' + run: | + if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then + echo Using pre-installed /opt/mokocli + echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV + else + echo Falling back to fresh clone + if ! command -v composer > /dev/null 2>&1; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1 + fi + rm -rf /tmp/mokocli + CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoCLI.git + git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli + cd /tmp/mokocli + composer install --no-dev --no-interaction --quiet + echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV + fi + + - name: "Detect platform" + id: platform + run: | + php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true + php ${MOKO_CLI}/manifest_read.php --path . --github-output 2>/dev/null || true + + - name: "Determine version bump level" + id: bump + run: | + # Fix/patch branches: version was already bumped by pre-release, just strip suffix + # Feature/dev branches: bump minor for the new stable release + HEAD_REF="${{ github.event.pull_request.head.ref || 'dev' }}" + case "$HEAD_REF" in + fix/*|patch/*|hotfix/*|bugfix/*) BUMP="none" ;; + *) BUMP="minor" ;; + esac + echo "level=${BUMP}" >> "$GITHUB_OUTPUT" + echo "Bump level: ${BUMP} (from branch: ${HEAD_REF})" + + - name: "Publish stable release" + run: | + BUMP_FLAG="" + if [ "${{ steps.bump.outputs.level }}" != "none" ]; then + BUMP_FLAG="--bump ${{ steps.bump.outputs.level }}" + fi + php ${MOKO_CLI}/release_publish.php \ + --path . --stability stable ${BUMP_FLAG} --branch main \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" + + - name: "Read published version" + id: version + run: | + VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "") + VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//') + [ -z "$VERSION" ] && VERSION="00.00.00" && echo "skip=true" >> "$GITHUB_OUTPUT" + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + PLATFORM="${{ steps.platform.outputs.platform }}" + if [[ "$PLATFORM" == joomla* ]]; then + echo "tag=stable" >> "$GITHUB_OUTPUT" + echo "release_tag=stable" >> "$GITHUB_OUTPUT" + else + echo "tag=v${VERSION}" >> "$GITHUB_OUTPUT" + echo "release_tag=v${VERSION}" >> "$GITHUB_OUTPUT" + fi + echo "branch=main" >> "$GITHUB_OUTPUT" + echo "Published version: ${VERSION}" + + - name: "Create semver tag for non-Joomla repos" + id: semver + if: | + steps.version.outputs.skip != 'true' && + !startsWith(steps.platform.outputs.platform, 'joomla') + run: | + VERSION="${{ steps.version.outputs.version }}" + API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" + SEMVER_TAG="v${VERSION}" + + echo "Creating semver tag: ${SEMVER_TAG}" + + # Create the git tag via API + HTTP_CODE=$(curl -sf -o /dev/null -w "%{http_code}" \ + -X POST -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/json" \ + "${API_BASE}/tags" \ + -d "{\"tag_name\":\"${SEMVER_TAG}\",\"target\":\"main\",\"message\":\"Release ${VERSION}\"}" 2>/dev/null || echo "000") + + if [ "$HTTP_CODE" = "201" ] || [ "$HTTP_CODE" = "200" ]; then + echo "Created semver tag: ${SEMVER_TAG}" + elif [ "$HTTP_CODE" = "409" ]; then + echo "Semver tag ${SEMVER_TAG} already exists (skipped)" + else + echo "::warning::Failed to create semver tag ${SEMVER_TAG} (HTTP ${HTTP_CODE})" + fi + + echo "semver_tag=${SEMVER_TAG}" >> "$GITHUB_OUTPUT" + + - name: Update release notes and promote changelog + run: | + API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" + + # Get the stable release info (version and ID) + RELEASE_JSON=$(curl -sf -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/releases/tags/stable" 2>/dev/null || echo '{}') + RELEASE_ID=$(python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" <<< "$RELEASE_JSON" 2>/dev/null || true) + # Extract version from release name (e.g. "06.17.00" or "v06.17.00") + VERSION=$(python3 -c " + import json, sys, re + r = json.load(sys.stdin) + name = r.get('name', '') + m = re.search(r'(\d+\.\d+\.\d+)', name) + print(m.group(1) if m else '') + " <<< "$RELEASE_JSON" 2>/dev/null || true) + + # Extract [Unreleased] section from changelog + NOTES="" + if [ -f "CHANGELOG.md" ]; then + NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md) + fi + [ -z "$NOTES" ] && NOTES="Stable release" + + # Update release body via API + if [ -n "$RELEASE_ID" ]; then + python3 -c " + import json, urllib.request + body = open('/dev/stdin').read() + payload = json.dumps({'body': body}).encode() + req = urllib.request.Request( + '${API_BASE}/releases/${RELEASE_ID}', + data=payload, method='PATCH', + headers={ + 'Authorization': 'token ${TOKEN}', + 'Content-Type': 'application/json' + }) + urllib.request.urlopen(req) + " <<< "$NOTES" + echo "Release notes updated from CHANGELOG.md" + fi + + # Promote [Unreleased] → [version] in CHANGELOG.md and reset + if [ -n "$VERSION" ] && [ -f "CHANGELOG.md" ]; then + DATE=$(date +%Y-%m-%d) + python3 -c " + import sys + version, date = sys.argv[1], sys.argv[2] + content = open('CHANGELOG.md').read() + old = '## [Unreleased]' + new = f'## [Unreleased]\n\n## [{version}] --- {date}' + content = content if ('## [' + version + ']') in content else content.replace(old, new, 1) + open('CHANGELOG.md', 'w').write(content) + " "$VERSION" "$DATE" + git add CHANGELOG.md + git commit -m "chore: promote changelog [Unreleased] → [${VERSION}]" || true + git push origin main || true + echo "Changelog promoted: [Unreleased] → [${VERSION}]" + fi + + # -- STEP 9: Mirror to GitHub (stable only) -------------------------------- + - name: "Step 9: Mirror release to GitHub" + if: >- + steps.version.outputs.skip != 'true' && + secrets.GH_MIRROR_TOKEN != '' + continue-on-error: true + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" + API_BASE="${MOKOGITEA_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 (dev reset moved to cascade-dev.yml)" + if: steps.version.outputs.skip != 'true' + continue-on-error: true + run: | + API_BASE="${MOKOGITEA_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" + + # dev is reset from main by the dedicated "Cascade Main -> Dev" workflow + # (cascade-dev.yml), which runs after this release completes. + echo "rc cleaned; dev reset handled by cascade-dev.yml" >> $GITHUB_STEP_SUMMARY + + - name: "Step 12: Create version branch from main" + if: steps.version.outputs.skip != 'true' + continue-on-error: true + run: | + API_BASE="${MOKOGITEA_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="${MOKOGITEA_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](${MOKOGITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY + fi diff --git a/.mokogitea/workflows/branch-cleanup.yml b/.mokogitea/workflows/branch-cleanup.yml index 6e2c289..25ebae1 100644 --- a/.mokogitea/workflows/branch-cleanup.yml +++ b/.mokogitea/workflows/branch-cleanup.yml @@ -1 +1,49 @@ -IyBDb3B5cmlnaHQgKEMpIDIwMjYgTW9rbyBDb25zdWx0aW5nIDxoZWxsb0Btb2tvY29uc3VsdGluZy50ZWNoPgojCiMgU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IEdQTC0zLjAtb3ItbGF0ZXIKIwojIEZJTEUgSU5GT1JNQVRJT04KIyBERUZHUk9VUDogR2l0ZWEuV29ya2Zsb3cKIyBJTkdST1VQOiBNb2tvU3RhbmRhcmRzLlVuaXZlcnNhbAojIFJFUE86IGh0dHBzOi8vZ2l0Lm1va29jb25zdWx0aW5nLnRlY2gvTW9rb0NvbnN1bHRpbmcvbW9rb2NsaQojIFBBVEg6IC8ubW9rb2dpdGVhL3dvcmtmbG93cy9icmFuY2gtY2xlYW51cC55bWwKIyBWRVJTSU9OOiAwMS4wMC4wMAojIEJSSUVGOiBEZWxldGUgZmVhdHVyZSBicmFuY2hlcyBhZnRlciBQUiBtZXJnZQoKbmFtZTogIkJyYW5jaCBDbGVhbnVwIgoKb246CiAgcHVsbF9yZXF1ZXN0OgogICAgdHlwZXM6IFtjbG9zZWRdCgplbnY6CiAgRk9SQ0VfSkFWQVNDUklQVF9BQ1RJT05TX1RPX05PREUyNDogdHJ1ZQoKam9iczoKICBjbGVhbnVwOgogICAgbmFtZTogRGVsZXRlIG1lcmdlZCBicmFuY2gKICAgIHJ1bnMtb246IHVidW50dS1sYXRlc3QKICAgIGlmOiA+LQogICAgICBnaXRodWIuZXZlbnQucHVsbF9yZXF1ZXN0Lm1lcmdlZCA9PSB0cnVlICYmCiAgICAgIGdpdGh1Yi5ldmVudC5wdWxsX3JlcXVlc3QuaGVhZC5yZWYgIT0gJ2RldicgJiYKICAgICAgZ2l0aHViLmV2ZW50LnB1bGxfcmVxdWVzdC5oZWFkLnJlZiAhPSAnbWFpbicKCiAgICBzdGVwczoKICAgICAgLSBuYW1lOiBEZWxldGUgc291cmNlIGJyYW5jaAogICAgICAgIHJ1bjogfAogICAgICAgICAgQlJBTkNIPSIke3sgZ2l0aHViLmV2ZW50LnB1bGxfcmVxdWVzdC5oZWFkLnJlZiB9fSIKICAgICAgICAgIEFQST0iJHt7IHZhcnMuR0lURUFfVVJMIHx8ICdodHRwczovL2dpdC5tb2tvY29uc3VsdGluZy50ZWNoJyB9fS9hcGkvdjEvcmVwb3MvJHt7IGdpdGh1Yi5yZXBvc2l0b3J5IH19L2JyYW5jaGVzIgogICAgICAgICAgRU5DT0RFRD0kKHBocCAtciAiZWNobyByYXd1cmxlbmNvZGUoJyR7QlJBTkNIfScpOyIpCgogICAgICAgICAgU1RBVFVTPSQoY3VybCAtc2YgLW8gL2Rldi9udWxsIC13ICIle2h0dHBfY29kZX0iIC1YIERFTEVURSBcCiAgICAgICAgICAgIC1IICJBdXRob3JpemF0aW9uOiB0b2tlbiAke3sgc2VjcmV0cy5NT0tPR0lURUFfVE9LRU4gfX0iIFwKICAgICAgICAgICAgIiR7QVBJfS8ke0VOQ09ERUR9IiAyPi9kZXYvbnVsbCB8fCB0cnVlKQoKICAgICAgICAgIGlmIFsgIiRTVEFUVVMiID0gIjIwNCIgXTsgdGhlbgogICAgICAgICAgICBlY2hvICJEZWxldGVkIGJyYW5jaDogJHtCUkFOQ0h9IiA+PiAkR0lUSFVCX1NURVBfU1VNTUFSWQogICAgICAgICAgZWxpZiBbICIkU1RBVFVTIiA9ICI0MDQiIF07IHRoZW4KICAgICAgICAgICAgZWNobyAiQnJhbmNoIGFscmVhZHkgZGVsZXRlZDogJHtCUkFOQ0h9IiA+PiAkR0lUSFVCX1NURVBfU1VNTUFSWQogICAgICAgICAgZWxzZQogICAgICAgICAgICBlY2hvICI6Ondhcm5pbmc6OkZhaWxlZCB0byBkZWxldGUgYnJhbmNoICR7QlJBTkNIfSAoSFRUUCAke1NUQVRVU30pIgogICAgICAgICAgZmkK \ No newline at end of file +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: MokoGitea.Workflow +# INGROUP: MokoStandards.Universal +# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic +# PATH: /.mokogitea/workflows/branch-cleanup.yml +# VERSION: 01.00.00 +# BRIEF: Delete feature branches after PR merge + +name: "Branch Cleanup" + +on: + pull_request: + types: [closed] + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + cleanup: + name: Delete merged branch + runs-on: ubuntu-latest + if: >- + github.event.pull_request.merged == true && + github.event.pull_request.head.ref != 'dev' && + github.event.pull_request.head.ref != 'main' + + steps: + - name: Delete source branch + run: | + BRANCH="${{ github.event.pull_request.head.ref }}" + API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches" + # URL-encode the branch name's slashes (no PHP dependency on the runner) + ENCODED=$(printf '%s' "${BRANCH}" | sed 's|/|%2F|g') + + STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \ + -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \ + "${API}/${ENCODED}" 2>/dev/null || true) + + if [ "$STATUS" = "204" ]; then + echo "Deleted branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY + elif [ "$STATUS" = "404" ]; then + echo "Branch already deleted: ${BRANCH}" >> $GITHUB_STEP_SUMMARY + else + echo "::warning::Failed to delete branch ${BRANCH} (HTTP ${STATUS})" + fi diff --git a/.mokogitea/workflows/cascade-dev.yml b/.mokogitea/workflows/cascade-dev.yml index 77032cc..b2eabe1 100644 --- a/.mokogitea/workflows/cascade-dev.yml +++ b/.mokogitea/workflows/cascade-dev.yml @@ -1 +1,106 @@ -IyBESVNBQkxFRCDigJQgYXV0by1yZWxlYXNlIFN0ZXAgMTEgcmVjcmVhdGVzIGRldiBmcm9tIG1haW4gYWZ0ZXIgZXZlcnkgcmVsZWFzZS4KIyBDYXNjYWRlLWRldiBpcyByZWR1bmRhbnQgYW5kIGNhdXNlcyB2ZXJzaW9uIGNvbmZsaWN0cyB3aGVuIGJvdGggbWFpbiBhbmQgZGV2CiMgaGF2ZSBkaWZmZXJlbnQgdmVyc2lvbiBudW1iZXJzIGluIHRlbXBsYXRlRGV0YWlscy54bWwgLyBtYW5pZmVzdC54bWwuCm5hbWU6ICJDYXNjYWRlIE1haW4g4oaSIERldiAoRElTQUJMRUQpIgpvbjogd29ya2Zsb3dfZGlzcGF0Y2gKam9iczoKICBub29wOgogICAgcnVucy1vbjogdWJ1bnR1LWxhdGVzdAogICAgc3RlcHM6CiAgICAgIC0gcnVuOiBlY2hvICJDYXNjYWRlIGRpc2FibGVkIOKAlCBhdXRvLXJlbGVhc2UgaGFuZGxlcyBkZXYgcmVjcmVhdGlvbiIK \ No newline at end of file +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: MokoGitea.Workflow +# INGROUP: MokoStandards.Cascade +# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic +# PATH: /.mokogitea/workflows/cascade-dev.yml +# VERSION: 02.00.00 +# BRIEF: Cascade main -> dev via PR; auto-merge only if conflict-free, else notify + +name: "Cascade Main -> Dev" + +on: + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +env: + MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + # ntfy destination is configured via repo or org variables (org vars are inherited). + NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }} + NTFY_TOPIC: ${{ vars.CASCADE_NTFY_TOPIC || vars.NTFY_TOPIC || 'gitea-releases' }} + +jobs: + cascade: + name: Cascade main -> dev + runs-on: ubuntu-latest + steps: + - name: Open main -> dev PR (auto-merge if clean, else notify) + env: + TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + REPO: ${{ github.repository }} + run: | + set -uo pipefail + API="${MOKOGITEA_URL}/api/v1/repos/${REPO}" + AUTH="Authorization: token ${TOKEN}" + jqnum() { python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('$1',''))" 2>/dev/null; } + + # 0. dev must exist + if ! curl -sf -H "$AUTH" "${API}/branches/dev" >/dev/null 2>&1; then + echo "No dev branch - nothing to cascade."; exit 0 + fi + + # 1. is main ahead of dev? + AHEAD=$(curl -sf -H "$AUTH" "${API}/compare/dev...main" \ + | python3 -c "import sys,json; print(json.load(sys.stdin).get('total_commits',0))" 2>/dev/null || echo 0) + if [ "${AHEAD:-0}" -eq 0 ]; then + echo "dev already up to date with main."; exit 0 + fi + echo "main is ${AHEAD} commit(s) ahead of dev." + + # 2. reuse an open main->dev PR, else create one + PR=$(curl -sf -H "$AUTH" "${API}/pulls?state=open&base=dev" \ + | python3 -c "import sys,json; d=json.load(sys.stdin); print(next((str(p['number']) for p in d if p.get('head',{}).get('ref')=='main'), ''))" 2>/dev/null || echo "") + if [ -z "$PR" ]; then + RESP=$(curl -s -H "$AUTH" -H "Content-Type: application/json" -X POST "${API}/pulls" \ + -d '{"head":"main","base":"dev","title":"chore(sync): cascade main -> dev","body":"Automated cascade of main into dev. Auto-merges only if conflict-free; otherwise left open for manual resolution."}') + PR=$(printf '%s' "$RESP" | jqnum number) + if [ -z "$PR" ]; then + echo "::warning::Could not open cascade PR: $RESP"; exit 0 + fi + echo "Opened cascade PR #${PR}" + else + echo "Reusing open cascade PR #${PR}" + fi + + # 3. wait for MokoGitea to compute mergeability (conflict detection) + MERGEABLE="" + for _ in 1 2 3 4 5 6; do + MERGEABLE=$(curl -sf -H "$AUTH" "${API}/pulls/${PR}" | jqnum mergeable) + case "$MERGEABLE" in True|False) break ;; esac + sleep 3 + done + echo "mergeable=${MERGEABLE}" + + notify() { + curl -sS \ + -H "Title: ${REPO}: dev cascade needs manual merge" \ + -H "Tags: warning,twisted_rightwards_arrows" \ + -H "Priority: high" \ + -H "Click: ${MOKOGITEA_URL}/${REPO}/pulls/${PR}" \ + -d "main -> dev cascade PR #${PR} $1 It was NOT auto-merged; resolve it manually." \ + "${NTFY_URL}/${NTFY_TOPIC}" || true + } + + # 4. auto-merge only if conflict-free; otherwise notify + if [ "$MERGEABLE" = "True" ]; then + CODE=$(curl -s -o /tmp/merge.json -w "%{http_code}" -H "$AUTH" -H "Content-Type: application/json" \ + -X POST "${API}/pulls/${PR}/merge" -d '{"Do":"merge","merge_when_checks_succeed":true}') + if [ "$CODE" -ge 200 ] && [ "$CODE" -lt 300 ]; then + echo "Cascade PR #${PR} merged (or scheduled to merge when checks pass)." + else + echo "::warning::Auto-merge returned HTTP ${CODE}: $(cat /tmp/merge.json)" + notify "could not be auto-merged (HTTP ${CODE})." + fi + else + echo "::warning::Cascade PR #${PR} has conflicts (mergeable=${MERGEABLE}); sending notification." + notify "has conflicts and cannot be merged automatically." + fi diff --git a/.mokogitea/workflows/ci-generic.yml b/.mokogitea/workflows/ci-generic.yml index af9b3c9..14f81ba 100644 --- a/.mokogitea/workflows/ci-generic.yml +++ b/.mokogitea/workflows/ci-generic.yml @@ -1 +1,203 @@ -IyBDb3B5cmlnaHQgKEMpIDIwMjYgTW9rbyBDb25zdWx0aW5nIDxoZWxsb0Btb2tvY29uc3VsdGluZy50ZWNoPgojCiMgU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IEdQTC0zLjAtb3ItbGF0ZXIKIwojIEZJTEUgSU5GT1JNQVRJT04KIyBERUZHUk9VUDogR2l0ZWEuV29ya2Zsb3cKIyBJTkdST1VQOiBNb2tvU3RhbmRhcmRzLkNJCiMgUkVQTzogaHR0cHM6Ly9naXQubW9rb2NvbnN1bHRpbmcudGVjaC9Nb2tvQ29uc3VsdGluZy9UZW1wbGF0ZS1HZW5lcmljCiMgUEFUSDogLy5naXRlYS93b3JrZmxvd3MvY2ktZ2VuZXJpYy55bWwKIyBWRVJTSU9OOiAwMS4wMC4wMAojIEJSSUVGOiBDSSBwaXBlbGluZSDigJQgbGludCwgdmFsaWRhdGUsIGFuZCB0ZXN0IGZvciBnZW5lcmljIHByb2plY3RzIChQSFAgKyBOb2RlLmpzKQoKbmFtZTogIkdlbmVyaWM6IFByb2plY3QgQ0kiCgpvbjoKICBwdWxsX3JlcXVlc3Q6CiAgICBicmFuY2hlczoKICAgICAgLSBtYWluCiAgICAgIC0gZGV2CiAgICAgIC0gZGV2LyoqCiAgICAgIC0gcmMvKioKICB3b3JrZmxvd19kaXNwYXRjaDoKCnBlcm1pc3Npb25zOgogIGNvbnRlbnRzOiByZWFkCgplbnY6CiAgRk9SQ0VfSkFWQVNDUklQVF9BQ1RJT05TX1RPX05PREUyNDogdHJ1ZQoKam9iczoKICAjIOKUgOKUgCBMaW50ICYgVmFsaWRhdGUg4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSACiAgbGludDoKICAgIG5hbWU6IExpbnQgJiBWYWxpZGF0ZQogICAgcnVucy1vbjogdWJ1bnR1LWxhdGVzdAoKICAgIHN0ZXBzOgogICAgICAtIG5hbWU6IENoZWNrb3V0CiAgICAgICAgdXNlczogYWN0aW9ucy9jaGVja291dEB2NAoKICAgICAgLSBuYW1lOiBEZXRlY3QgdG9vbGNoYWluCiAgICAgICAgaWQ6IGRldGVjdAogICAgICAgIHJ1bjogfAogICAgICAgICAgSEFTX1BIUD1mYWxzZQogICAgICAgICAgSEFTX05PREU9ZmFsc2UKICAgICAgICAgIFsgLWYgImNvbXBvc2VyLmpzb24iIF0gJiYgSEFTX1BIUD10cnVlCiAgICAgICAgICBbIC1mICJwYWNrYWdlLmpzb24iIF0gJiYgSEFTX05PREU9dHJ1ZQogICAgICAgICAgZWNobyAiaGFzX3BocD0kSEFTX1BIUCIgPj4gIiRHSVRIVUJfT1VUUFVUIgogICAgICAgICAgZWNobyAiaGFzX25vZGU9JEhBU19OT0RFIiA+PiAiJEdJVEhVQl9PVVRQVVQiCiAgICAgICAgICBlY2hvICJUb29sY2hhaW46IFBIUD0kSEFTX1BIUCBOb2RlPSRIQVNfTk9ERSIKCiAgICAgIC0gbmFtZTogU2V0dXAgUEhQCiAgICAgICAgaWY6IHN0ZXBzLmRldGVjdC5vdXRwdXRzLmhhc19waHAgPT0gJ3RydWUnCiAgICAgICAgcnVuOiB8CiAgICAgICAgICBpZiAhIGNvbW1hbmQgLXYgcGhwICY+IC9kZXYvbnVsbDsgdGhlbgogICAgICAgICAgICBzdWRvIGFwdC1nZXQgdXBkYXRlIC1xcQogICAgICAgICAgICBzdWRvIGFwdC1nZXQgaW5zdGFsbCAteSAtcXEgcGhwLWNsaSBwaHAtbWJzdHJpbmcgcGhwLXhtbCA+L2Rldi9udWxsIDI+JjEKICAgICAgICAgIGZpCiAgICAgICAgICBwaHAgLXYKCiAgICAgIC0gbmFtZTogU2V0dXAgTm9kZS5qcwogICAgICAgIGlmOiBzdGVwcy5kZXRlY3Qub3V0cHV0cy5oYXNfbm9kZSA9PSAndHJ1ZScKICAgICAgICB1c2VzOiBhY3Rpb25zL3NldHVwLW5vZGVAdjQKICAgICAgICB3aXRoOgogICAgICAgICAgbm9kZS12ZXJzaW9uOiAnMjAnCgogICAgICAtIG5hbWU6IEluc3RhbGwgUEhQIGRlcGVuZGVuY2llcwogICAgICAgIGlmOiBzdGVwcy5kZXRlY3Qub3V0cHV0cy5oYXNfcGhwID09ICd0cnVlJwogICAgICAgIHJ1bjogfAogICAgICAgICAgaWYgWyAtZiAiY29tcG9zZXIuanNvbiIgXTsgdGhlbgogICAgICAgICAgICBjb21wb3NlciBpbnN0YWxsIC0tbm8taW50ZXJhY3Rpb24gLS1wcmVmZXItZGlzdCAtLXF1aWV0IDI+L2Rldi9udWxsIHx8IHRydWUKICAgICAgICAgIGZpCgogICAgICAtIG5hbWU6IEluc3RhbGwgTm9kZS5qcyBkZXBlbmRlbmNpZXMKICAgICAgICBpZjogc3RlcHMuZGV0ZWN0Lm91dHB1dHMuaGFzX25vZGUgPT0gJ3RydWUnCiAgICAgICAgcnVuOiB8CiAgICAgICAgICBpZiBbIC1mICJwYWNrYWdlLmpzb24iIF07IHRoZW4KICAgICAgICAgICAgbnBtIGNpIC0tcXVpZXQgMj4vZGV2L251bGwgfHwgbnBtIGluc3RhbGwgLS1xdWlldCAyPi9kZXYvbnVsbCB8fCB0cnVlCiAgICAgICAgICBmaQoKICAgICAgLSBuYW1lOiBQSFAgc3ludGF4IGNoZWNrCiAgICAgICAgaWY6IHN0ZXBzLmRldGVjdC5vdXRwdXRzLmhhc19waHAgPT0gJ3RydWUnCiAgICAgICAgcnVuOiB8CiAgICAgICAgICBFUlJPUlM9MAogICAgICAgICAgd2hpbGUgSUZTPSByZWFkIC1yIC1kICcnIGZpbGU7IGRvCiAgICAgICAgICAgIGlmICEgcGhwIC1sICIkZmlsZSIgMj4mMSB8IGdyZXAgLXEgIk5vIHN5bnRheCBlcnJvcnMiOyB0aGVuCiAgICAgICAgICAgICAgZWNobyAiOjplcnJvciBmaWxlPSR7ZmlsZX06OlBIUCBzeW50YXggZXJyb3IiCiAgICAgICAgICAgICAgRVJST1JTPSQoKEVSUk9SUyArIDEpKQogICAgICAgICAgICBmaQogICAgICAgICAgZG9uZSA8IDwoZmluZCAuIC1uYW1lICIqLnBocCIgLW5vdCAtcGF0aCAiLi8uZ2l0LyoiIC1ub3QgLXBhdGggIi4vdmVuZG9yLyoiIC1ub3QgLXBhdGggIi4vbm9kZV9tb2R1bGVzLyoiIC1wcmludDApCgogICAgICAgICAgZWNobyAiIyMgUEhQIExpbnQiID4+ICRHSVRIVUJfU1RFUF9TVU1NQVJZCiAgICAgICAgICBpZiBbICIkRVJST1JTIiAtZXEgMCBdOyB0aGVuCiAgICAgICAgICAgIGVjaG8gIkFsbCBQSFAgZmlsZXMgcGFzc2VkIHN5bnRheCBjaGVjay4iID4+ICRHSVRIVUJfU1RFUF9TVU1NQVJZCiAgICAgICAgICBlbHNlCiAgICAgICAgICAgIGVjaG8gIiR7RVJST1JTfSBmaWxlKHMpIHdpdGggc3ludGF4IGVycm9ycy4iID4+ICRHSVRIVUJfU1RFUF9TVU1NQVJZCiAgICAgICAgICAgIGV4aXQgMQogICAgICAgICAgZmkKCiAgICAgIC0gbmFtZTogVHlwZVNjcmlwdC9KYXZhU2NyaXB0IGxpbnQKICAgICAgICBpZjogc3RlcHMuZGV0ZWN0Lm91dHB1dHMuaGFzX25vZGUgPT0gJ3RydWUnCiAgICAgICAgcnVuOiB8CiAgICAgICAgICBpZiBbIC1mICJub2RlX21vZHVsZXMvLmJpbi9lc2xpbnQiIF07IHRoZW4KICAgICAgICAgICAgbnB4IGVzbGludCBzcmMvIC0tcXVpZXQgMj4mMSB8fCB7IGVjaG8gIjo6ZXJyb3I6OkVTTGludCBlcnJvcnMgZm91bmQiOyBleGl0IDE7IH0KICAgICAgICAgICAgZWNobyAiIyMgRVNMaW50IiA+PiAkR0lUSFVCX1NURVBfU1VNTUFSWQogICAgICAgICAgICBlY2hvICJBbGwgZmlsZXMgcGFzc2VkIEVTTGludC4iID4+ICRHSVRIVUJfU1RFUF9TVU1NQVJZCiAgICAgICAgICBlbGlmIFsgLWYgIi5lc2xpbnRyYy5qc29uIiBdIHx8IFsgLWYgIi5lc2xpbnRyYy5qcyIgXSB8fCBbIC1mICJlc2xpbnQuY29uZmlnLmpzIiBdOyB0aGVuCiAgICAgICAgICAgIGVjaG8gIjo6d2FybmluZzo6RVNMaW50IGNvbmZpZyBmb3VuZCBidXQgZXNsaW50IG5vdCBpbnN0YWxsZWQiCiAgICAgICAgICBlbHNlCiAgICAgICAgICAgIGVjaG8gIk5vIEVTTGludCBjb25maWd1cmVkIOKAlCBza2lwcGluZyIKICAgICAgICAgIGZpCgogICAgICAtIG5hbWU6IFR5cGVTY3JpcHQgY29tcGlsZSBjaGVjawogICAgICAgIGlmOiBzdGVwcy5kZXRlY3Qub3V0cHV0cy5oYXNfbm9kZSA9PSAndHJ1ZScKICAgICAgICBydW46IHwKICAgICAgICAgIGlmIFsgLWYgInRzY29uZmlnLmpzb24iIF0gJiYgWyAtZiAibm9kZV9tb2R1bGVzLy5iaW4vdHNjIiBdOyB0aGVuCiAgICAgICAgICAgIG5weCB0c2MgLS1ub0VtaXQgMj4mMSB8fCB7IGVjaG8gIjo6ZXJyb3I6OlR5cGVTY3JpcHQgY29tcGlsYXRpb24gZXJyb3JzIjsgZXhpdCAxOyB9CiAgICAgICAgICAgIGVjaG8gIiMjIFR5cGVTY3JpcHQiID4+ICRHSVRIVUJfU1RFUF9TVU1NQVJZCiAgICAgICAgICAgIGVjaG8gIlR5cGVTY3JpcHQgY29tcGlsYXRpb24gcGFzc2VkLiIgPj4gJEdJVEhVQl9TVEVQX1NVTU1BUlkKICAgICAgICAgIGZpCgogICAgICAtIG5hbWU6IFBIUFN0YW4gc3RhdGljIGFuYWx5c2lzCiAgICAgICAgaWY6IHN0ZXBzLmRldGVjdC5vdXRwdXRzLmhhc19waHAgPT0gJ3RydWUnCiAgICAgICAgcnVuOiB8CiAgICAgICAgICBpZiBbIC1mICJwaHBzdGFuLm5lb24iIF0gJiYgWyAtZiAidmVuZG9yL2Jpbi9waHBzdGFuIiBdOyB0aGVuCiAgICAgICAgICAgIHZlbmRvci9iaW4vcGhwc3RhbiBhbmFseXNlIC0tbm8tcHJvZ3Jlc3MgMj4mMSB8fCB7IGVjaG8gIjo6d2FybmluZzo6UEhQU3RhbiBmb3VuZCBpc3N1ZXMiOyB9CiAgICAgICAgICBmaQoKICAjIOKUgOKUgCBUZXN0cyDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKICB0ZXN0OgogICAgbmFtZTogVGVzdHMKICAgIHJ1bnMtb246IHVidW50dS1sYXRlc3QKICAgIG5lZWRzOiBsaW50CgogICAgc3RlcHM6CiAgICAgIC0gbmFtZTogQ2hlY2tvdXQKICAgICAgICB1c2VzOiBhY3Rpb25zL2NoZWNrb3V0QHY0CgogICAgICAtIG5hbWU6IERldGVjdCB0b29sY2hhaW4KICAgICAgICBpZDogZGV0ZWN0CiAgICAgICAgcnVuOiB8CiAgICAgICAgICBIQVNfUEhQPWZhbHNlCiAgICAgICAgICBIQVNfTk9ERT1mYWxzZQogICAgICAgICAgWyAtZiAiY29tcG9zZXIuanNvbiIgXSAmJiBIQVNfUEhQPXRydWUKICAgICAgICAgIFsgLWYgInBhY2thZ2UuanNvbiIgXSAmJiBIQVNfTk9ERT10cnVlCiAgICAgICAgICBlY2hvICJoYXNfcGhwPSRIQVNfUEhQIiA+PiAiJEdJVEhVQl9PVVRQVVQiCiAgICAgICAgICBlY2hvICJoYXNfbm9kZT0kSEFTX05PREUiID4+ICIkR0lUSFVCX09VVFBVVCIKCiAgICAgIC0gbmFtZTogU2V0dXAgUEhQCiAgICAgICAgaWY6IHN0ZXBzLmRldGVjdC5vdXRwdXRzLmhhc19waHAgPT0gJ3RydWUnCiAgICAgICAgcnVuOiB8CiAgICAgICAgICBpZiAhIGNvbW1hbmQgLXYgcGhwICY+IC9kZXYvbnVsbDsgdGhlbgogICAgICAgICAgICBzdWRvIGFwdC1nZXQgdXBkYXRlIC1xcQogICAgICAgICAgICBzdWRvIGFwdC1nZXQgaW5zdGFsbCAteSAtcXEgcGhwLWNsaSBwaHAtbWJzdHJpbmcgcGhwLXhtbCA+L2Rldi9udWxsIDI+JjEKICAgICAgICAgIGZpCgogICAgICAtIG5hbWU6IFNldHVwIE5vZGUuanMKICAgICAgICBpZjogc3RlcHMuZGV0ZWN0Lm91dHB1dHMuaGFzX25vZGUgPT0gJ3RydWUnCiAgICAgICAgdXNlczogYWN0aW9ucy9zZXR1cC1ub2RlQHY0CiAgICAgICAgd2l0aDoKICAgICAgICAgIG5vZGUtdmVyc2lvbjogJzIwJwoKICAgICAgLSBuYW1lOiBJbnN0YWxsIGRlcGVuZGVuY2llcwogICAgICAgIHJ1bjogfAogICAgICAgICAgWyAtZiAiY29tcG9zZXIuanNvbiIgXSAmJiBjb21wb3NlciBpbnN0YWxsIC0tbm8taW50ZXJhY3Rpb24gLS1wcmVmZXItZGlzdCAtLXF1aWV0IDI+L2Rldi9udWxsIHx8IHRydWUKICAgICAgICAgIFsgLWYgInBhY2thZ2UuanNvbiIgXSAmJiB7IG5wbSBjaSAtLXF1aWV0IDI+L2Rldi9udWxsIHx8IG5wbSBpbnN0YWxsIC0tcXVpZXQgMj4vZGV2L251bGwgfHwgdHJ1ZTsgfQoKICAgICAgLSBuYW1lOiBSdW4gUEhQIHRlc3RzCiAgICAgICAgaWY6IHN0ZXBzLmRldGVjdC5vdXRwdXRzLmhhc19waHAgPT0gJ3RydWUnCiAgICAgICAgcnVuOiB8CiAgICAgICAgICBpZiBbIC1mICJ2ZW5kb3IvYmluL3BocHVuaXQiIF07IHRoZW4KICAgICAgICAgICAgdmVuZG9yL2Jpbi9waHB1bml0IC0tdGVzdGRveCAyPiYxCiAgICAgICAgICAgIGVjaG8gIiMjIFBIUFVuaXQiID4+ICRHSVRIVUJfU1RFUF9TVU1NQVJZCiAgICAgICAgICAgIGVjaG8gIlRlc3RzIHBhc3NlZC4iID4+ICRHSVRIVUJfU1RFUF9TVU1NQVJZCiAgICAgICAgICBlbGlmIFsgLWYgInBocHVuaXQueG1sIiBdIHx8IFsgLWYgInBocHVuaXQueG1sLmRpc3QiIF07IHRoZW4KICAgICAgICAgICAgZWNobyAiOjp3YXJuaW5nOjpQSFBVbml0IGNvbmZpZyBmb3VuZCBidXQgcGhwdW5pdCBub3QgaW5zdGFsbGVkIgogICAgICAgICAgZWxzZQogICAgICAgICAgICBlY2hvICJObyBQSFBVbml0IGNvbmZpZ3VyZWQg4oCUIHNraXBwaW5nIgogICAgICAgICAgZmkKCiAgICAgIC0gbmFtZTogUnVuIE5vZGUuanMgdGVzdHMKICAgICAgICBpZjogc3RlcHMuZGV0ZWN0Lm91dHB1dHMuaGFzX25vZGUgPT0gJ3RydWUnCiAgICAgICAgcnVuOiB8CiAgICAgICAgICBpZiBqcSAtZSAnLnNjcmlwdHMudGVzdCcgcGFja2FnZS5qc29uID4gL2Rldi9udWxsIDI+JjE7IHRoZW4KICAgICAgICAgICAgbnBtIHRlc3QgMj4mMQogICAgICAgICAgICBlY2hvICIjIyBOb2RlLmpzIFRlc3RzIiA+PiAkR0lUSFVCX1NURVBfU1VNTUFSWQogICAgICAgICAgICBlY2hvICJUZXN0cyBwYXNzZWQuIiA+PiAkR0lUSFVCX1NURVBfU1VNTUFSWQogICAgICAgICAgZWxzZQogICAgICAgICAgICBlY2hvICJObyB0ZXN0IHNjcmlwdCBpbiBwYWNrYWdlLmpzb24g4oCUIHNraXBwaW5nIgogICAgICAgICAgZmkKCiAgICAgIC0gbmFtZTogQnVpbGQgY2hlY2sKICAgICAgICBydW46IHwKICAgICAgICAgIGlmIFsgLWYgIk1ha2VmaWxlIiBdOyB0aGVuCiAgICAgICAgICAgIG1ha2UgYnVpbGQgMj4mMSB8fCBlY2hvICI6Ondhcm5pbmc6OkJ1aWxkIGZhaWxlZCBvciBub3QgY29uZmlndXJlZCIKICAgICAgICAgIGVsaWYgWyAtZiAicGFja2FnZS5qc29uIiBdICYmIGpxIC1lICcuc2NyaXB0cy5idWlsZCcgcGFja2FnZS5qc29uID4gL2Rldi9udWxsIDI+JjE7IHRoZW4KICAgICAgICAgICAgbnBtIHJ1biBidWlsZCAyPiYxIHx8IGVjaG8gIjo6d2FybmluZzo6QnVpbGQgZmFpbGVkIgogICAgICAgICAgZmkK \ No newline at end of file +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: MokoGitea.Workflow +# INGROUP: MokoStandards.CI +# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic +# PATH: /.mokogitea/workflows/ci-generic.yml +# VERSION: 01.00.00 +# BRIEF: CI pipeline — lint, validate, and test for generic projects (PHP + Node.js) + +name: "Generic: Project CI" + +on: + pull_request: + branches: + - main + - dev + - dev/** + - rc/** + workflow_dispatch: + +permissions: + contents: read + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + # ── Lint & Validate ─────────────────────────────────────────────────── + lint: + name: Lint & Validate + runs-on: ubuntu-latest + # Skip on template repos (Template-*) — they hold placeholder scaffolding, not buildable source. + if: ${{ !startsWith(github.event.repository.name, 'Template-') }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Detect toolchain + id: detect + run: | + HAS_PHP=false + HAS_NODE=false + [ -f "composer.json" ] && HAS_PHP=true + [ -f "package.json" ] && HAS_NODE=true + echo "has_php=$HAS_PHP" >> "$GITHUB_OUTPUT" + echo "has_node=$HAS_NODE" >> "$GITHUB_OUTPUT" + echo "Toolchain: PHP=$HAS_PHP Node=$HAS_NODE" + + - name: Setup PHP + if: steps.detect.outputs.has_php == 'true' + run: | + if ! command -v php &> /dev/null; then + sudo apt-get update -qq + sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1 + fi + php -v + + - name: Setup Node.js + if: steps.detect.outputs.has_node == 'true' + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install PHP dependencies + if: steps.detect.outputs.has_php == 'true' + run: | + if [ -f "composer.json" ]; then + composer install --no-interaction --prefer-dist --quiet 2>/dev/null || true + fi + + - name: Install Node.js dependencies + if: steps.detect.outputs.has_node == 'true' + run: | + if [ -f "package.json" ]; then + npm ci --quiet 2>/dev/null || npm install --quiet 2>/dev/null || true + fi + + - name: PHP syntax check + if: steps.detect.outputs.has_php == 'true' + run: | + ERRORS=0 + while IFS= read -r -d '' file; do + if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then + echo "::error file=${file}::PHP syntax error" + ERRORS=$((ERRORS + 1)) + fi + done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -not -path "./node_modules/*" -print0) + + echo "## PHP Lint" >> $GITHUB_STEP_SUMMARY + if [ "$ERRORS" -eq 0 ]; then + echo "All PHP files passed syntax check." >> $GITHUB_STEP_SUMMARY + else + echo "${ERRORS} file(s) with syntax errors." >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + - name: TypeScript/JavaScript lint + if: steps.detect.outputs.has_node == 'true' + run: | + if [ -f "node_modules/.bin/eslint" ]; then + npx eslint src/ --quiet 2>&1 || { echo "::error::ESLint errors found"; exit 1; } + echo "## ESLint" >> $GITHUB_STEP_SUMMARY + echo "All files passed ESLint." >> $GITHUB_STEP_SUMMARY + elif [ -f ".eslintrc.json" ] || [ -f ".eslintrc.js" ] || [ -f "eslint.config.js" ]; then + echo "::warning::ESLint config found but eslint not installed" + else + echo "No ESLint configured — skipping" + fi + + - name: TypeScript compile check + if: steps.detect.outputs.has_node == 'true' + run: | + if [ -f "tsconfig.json" ] && [ -f "node_modules/.bin/tsc" ]; then + npx tsc --noEmit 2>&1 || { echo "::error::TypeScript compilation errors"; exit 1; } + echo "## TypeScript" >> $GITHUB_STEP_SUMMARY + echo "TypeScript compilation passed." >> $GITHUB_STEP_SUMMARY + fi + + - name: PHPStan static analysis + if: steps.detect.outputs.has_php == 'true' + run: | + if [ -f "phpstan.neon" ] && [ -f "vendor/bin/phpstan" ]; then + vendor/bin/phpstan analyse --no-progress 2>&1 || { echo "::warning::PHPStan found issues"; } + fi + + # ── Tests ───────────────────────────────────────────────────────────── + test: + name: Tests + runs-on: ubuntu-latest + # Independent job (no `needs: lint`): the Gitea Actions scheduler does not + # offer the dependent 2nd job of a needs-chain to runners, so it stalls in + # "waiting" and is reaped by ABANDONED_JOB_TIMEOUT. Guard template repos + # directly (same condition lint uses) instead of gating on lint's result. + if: ${{ !startsWith(github.event.repository.name, 'Template-') }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Detect toolchain + id: detect + run: | + HAS_PHP=false + HAS_NODE=false + [ -f "composer.json" ] && HAS_PHP=true + [ -f "package.json" ] && HAS_NODE=true + echo "has_php=$HAS_PHP" >> "$GITHUB_OUTPUT" + echo "has_node=$HAS_NODE" >> "$GITHUB_OUTPUT" + + - name: Setup PHP + if: steps.detect.outputs.has_php == 'true' + run: | + if ! command -v php &> /dev/null; then + sudo apt-get update -qq + sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1 + fi + + - name: Setup Node.js + if: steps.detect.outputs.has_node == 'true' + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: | + [ -f "composer.json" ] && composer install --no-interaction --prefer-dist --quiet 2>/dev/null || true + [ -f "package.json" ] && { npm ci --quiet 2>/dev/null || npm install --quiet 2>/dev/null || true; } + + - name: Run PHP tests + if: steps.detect.outputs.has_php == 'true' + run: | + if [ -f "vendor/bin/phpunit" ]; then + vendor/bin/phpunit --testdox 2>&1 + echo "## PHPUnit" >> $GITHUB_STEP_SUMMARY + echo "Tests passed." >> $GITHUB_STEP_SUMMARY + elif [ -f "phpunit.xml" ] || [ -f "phpunit.xml.dist" ]; then + echo "::warning::PHPUnit config found but phpunit not installed" + else + echo "No PHPUnit configured — skipping" + fi + + - name: Run Node.js tests + if: steps.detect.outputs.has_node == 'true' + run: | + if jq -e '.scripts.test' package.json > /dev/null 2>&1; then + npm test 2>&1 + echo "## Node.js Tests" >> $GITHUB_STEP_SUMMARY + echo "Tests passed." >> $GITHUB_STEP_SUMMARY + else + echo "No test script in package.json — skipping" + fi + + - name: Build check + run: | + if [ -f "Makefile" ]; then + make build 2>&1 || echo "::warning::Build failed or not configured" + elif [ -f "package.json" ] && jq -e '.scripts.build' package.json > /dev/null 2>&1; then + npm run build 2>&1 || echo "::warning::Build failed" + fi diff --git a/.mokogitea/workflows/ci-issue-reporter.yml b/.mokogitea/workflows/ci-issue-reporter.yml index b396c34..0520237 100644 --- a/.mokogitea/workflows/ci-issue-reporter.yml +++ b/.mokogitea/workflows/ci-issue-reporter.yml @@ -1 +1,68 @@ -IyBDb3B5cmlnaHQgKEMpIDIwMjYgTW9rbyBDb25zdWx0aW5nIDxoZWxsb0Btb2tvY29uc3VsdGluZy50ZWNoPgojCiMgU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IEdQTC0zLjAtb3ItbGF0ZXIKIwojIEZJTEUgSU5GT1JNQVRJT04KIyBERUZHUk9VUDogR2l0ZWEuV29ya2Zsb3cKIyBJTkdST1VQOiBtb2tvY2xpLlVuaXZlcnNhbAojIFJFUE86IGh0dHBzOi8vZ2l0Lm1va29jb25zdWx0aW5nLnRlY2gvTW9rb0NvbnN1bHRpbmcvbW9rb2NsaQojIFBBVEg6IC8ubW9rb2dpdGVhL3dvcmtmbG93cy9jaS1pc3N1ZS1yZXBvcnRlci55bWwKIyBWRVJTSU9OOiAwMS4wMC4wMAojIEJSSUVGOiBSZXVzYWJsZSB3b3JrZmxvdyDigJQgY3JlYXRlcy91cGRhdGVzIGEgR2l0ZWEgaXNzdWUgd2hlbiBhIENJIGdhdGUgZmFpbHMuCiMgICAgICAgIENsb25lcyBNb2tvQ0xJIGFuZCBydW5zIGNsaS9jaV9pc3N1ZV9yZXBvcnRlci5zaC4KCm5hbWU6ICJVbml2ZXJzYWw6IENJIElzc3VlIFJlcG9ydGVyIgoKb246CiAgd29ya2Zsb3dfY2FsbDoKICAgIGlucHV0czoKICAgICAgZ2F0ZToKICAgICAgICBkZXNjcmlwdGlvbjogIkNJIGdhdGUgbmFtZSAoZS5nLiBQUiBWYWxpZGF0aW9uLCBSZXBvc2l0b3J5IEhlYWx0aCkiCiAgICAgICAgcmVxdWlyZWQ6IHRydWUKICAgICAgICB0eXBlOiBzdHJpbmcKICAgICAgZGV0YWlsczoKICAgICAgICBkZXNjcmlwdGlvbjogIkh1bWFuLXJlYWRhYmxlIGZhaWx1cmUgZGVzY3JpcHRpb24iCiAgICAgICAgcmVxdWlyZWQ6IHRydWUKICAgICAgICB0eXBlOiBzdHJpbmcKICAgICAgc2V2ZXJpdHk6CiAgICAgICAgZGVzY3JpcHRpb246ICJlcnJvciBvciB3YXJuaW5nIgogICAgICAgIHJlcXVpcmVkOiBmYWxzZQogICAgICAgIHR5cGU6IHN0cmluZwogICAgICAgIGRlZmF1bHQ6ICJlcnJvciIKICAgICAgd29ya2Zsb3c6CiAgICAgICAgZGVzY3JpcHRpb246ICJXb3JrZmxvdyBuYW1lIGZvciB0aGUgaXNzdWUgdGl0bGUiCiAgICAgICAgcmVxdWlyZWQ6IGZhbHNlCiAgICAgICAgdHlwZTogc3RyaW5nCiAgICAgICAgZGVmYXVsdDogIiIKICAgIHNlY3JldHM6CiAgICAgIE1PS09HSVRFQV9UT0tFTjoKICAgICAgICByZXF1aXJlZDogdHJ1ZQoKZW52OgogIEZPUkNFX0pBVkFTQ1JJUFRfQUNUSU9OU19UT19OT0RFMjQ6IHRydWUKCmpvYnM6CiAgcmVwb3J0OgogICAgbmFtZTogIlJlcG9ydDogJHt7IGlucHV0cy5nYXRlIH19IgogICAgcnVucy1vbjogdWJ1bnR1LWxhdGVzdAoKICAgIHN0ZXBzOgogICAgICAtIG5hbWU6IENsb25lIE1va29DTEkKICAgICAgICBlbnY6CiAgICAgICAgICBNT0tPR0lURUFfVE9LRU46ICR7eyBzZWNyZXRzLk1PS09HSVRFQV9UT0tFTiB9fQogICAgICAgIHJ1bjogfAogICAgICAgICAgTU9LT0dJVEVBX1VSTD0iJHt7IHZhcnMuR0lURUFfVVJMIHx8ICdodHRwczovL2dpdC5tb2tvY29uc3VsdGluZy50ZWNoJyB9fSIKICAgICAgICAgIGdpdCBjbG9uZSAtLWRlcHRoIDEgLS1maWx0ZXI9YmxvYjpub25lIC0tc3BhcnNlICIke01PS09HSVRFQV9VUkx9L01va29Db25zdWx0aW5nL01va29DTEkuZ2l0IiAvdG1wL21va29jbGkKICAgICAgICAgIGNkIC90bXAvbW9rb2NsaSAmJiBnaXQgc3BhcnNlLWNoZWNrb3V0IHNldCBjbGkvY2lfaXNzdWVfcmVwb3J0ZXIuc2gKCiAgICAgIC0gbmFtZTogUmVwb3J0IENJIGZhaWx1cmUKICAgICAgICBlbnY6CiAgICAgICAgICBNT0tPR0lURUFfVE9LRU46ICR7eyBzZWNyZXRzLk1PS09HSVRFQV9UT0tFTiB9fQogICAgICAgICAgTU9LT0dJVEVBX1VSTDogJHt7IHZhcnMuR0lURUFfVVJMIHx8ICdodHRwczovL2dpdC5tb2tvY29uc3VsdGluZy50ZWNoJyB9fQogICAgICAgIHJ1bjogfAogICAgICAgICAgY2htb2QgK3ggL3RtcC9tb2tvY2xpL2NsaS9jaV9pc3N1ZV9yZXBvcnRlci5zaAogICAgICAgICAgL3RtcC9tb2tvY2xpL2NsaS9jaV9pc3N1ZV9yZXBvcnRlci5zaCBcCiAgICAgICAgICAgIC0tZ2F0ZSAiJHt7IGlucHV0cy5nYXRlIH19IiBcCiAgICAgICAgICAgIC0tZGV0YWlscyAiJHt7IGlucHV0cy5kZXRhaWxzIH19IiBcCiAgICAgICAgICAgIC0tc2V2ZXJpdHkgIiR7eyBpbnB1dHMuc2V2ZXJpdHkgfX0iIFwKICAgICAgICAgICAgLS13b3JrZmxvdyAiJHt7IGlucHV0cy53b3JrZmxvdyB9fSIK \ No newline at end of file +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: MokoGitea.Workflow +# INGROUP: mokocli.Universal +# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic +# PATH: /.mokogitea/workflows/ci-issue-reporter.yml +# VERSION: 01.00.00 +# BRIEF: Reusable workflow — creates/updates a MokoGitea issue when a CI gate fails. +# Clones MokoCLI and runs cli/ci_issue_reporter.sh. + +name: "Universal: CI Issue Reporter" + +on: + workflow_call: + inputs: + gate: + description: "CI gate name (e.g. PR Validation, Repository Health)" + required: true + type: string + details: + description: "Human-readable failure description" + required: true + type: string + severity: + description: "error or warning" + required: false + type: string + default: "error" + workflow: + description: "Workflow name for the issue title" + required: false + type: string + default: "" + secrets: + MOKOGITEA_TOKEN: + required: true + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + report: + name: "Report: ${{ inputs.gate }}" + runs-on: ubuntu-latest + + steps: + - name: Clone MokoCLI + env: + MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + run: | + MOKOGITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}" + git clone --depth 1 --filter=blob:none --sparse "${MOKOGITEA_URL}/MokoConsulting/MokoCLI.git" /tmp/mokocli + cd /tmp/mokocli && git sparse-checkout set cli/ci_issue_reporter.sh + + - name: Report CI failure + env: + MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + run: | + chmod +x /tmp/mokocli/cli/ci_issue_reporter.sh + /tmp/mokocli/cli/ci_issue_reporter.sh \ + --gate "${{ inputs.gate }}" \ + --details "${{ inputs.details }}" \ + --severity "${{ inputs.severity }}" \ + --workflow "${{ inputs.workflow }}" diff --git a/.mokogitea/workflows/ci-joomla.yml b/.mokogitea/workflows/ci-joomla.yml deleted file mode 100644 index 9d5a1c9..0000000 --- a/.mokogitea/workflows/ci-joomla.yml +++ /dev/null @@ -1,1235 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# This file is part of a Moko Consulting project. -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow.Template -# INGROUP: MokoStandards.CI -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API -# PATH: /templates/workflows/joomla/ci-joomla.yml.template -# VERSION: 04.06.00 -# BRIEF: CI workflow for Joomla extensions — lint, validate, test - -name: "Joomla: Extension CI" - -on: - pull_request: - branches: - - main - - 'dev/**' - workflow_dispatch: - -permissions: - contents: read - pull-requests: write - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - lint-and-validate: - name: Lint & Validate - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup PHP - run: | - if ! command -v php &> /dev/null; then - sudo apt-get update -qq - sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 - fi - php -v && composer --version - - - name: Setup mokocli tools - env: - MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || github.token }} - MOKO_CLONE_HOST: ${{ secrets.MOKOGITEA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }} - run: | - if [ -d "/opt/mokocli" ] || [ -d "/tmp/mokocli" ]; then - echo "mokocli already available on runner — skipping clone" - else - git clone --depth 1 --branch main --quiet \ - "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git" \ - /tmp/mokocli 2>/dev/null || echo "mokocli clone skipped — continuing without it" - fi - - - name: Install dependencies - env: - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}"}}' - run: | - if [ -f "composer.json" ]; then - composer install \ - --no-interaction \ - --prefer-dist \ - --optimize-autoloader - else - echo "No composer.json found — skipping dependency install" - fi - - - name: PHP syntax check - run: | - ERRORS=0 - for DIR in src/ htdocs/; do - if [ -d "$DIR" ]; then - FOUND=1 - while IFS= read -r -d '' FILE; do - OUTPUT=$(php -l "$FILE" 2>&1) - if echo "$OUTPUT" | grep -q "Parse error"; then - echo "::error file=${FILE}::${OUTPUT}" - ERRORS=$((ERRORS + 1)) - fi - done < <(find "$DIR" -name "*.php" -print0) - fi - done - echo "### PHP Syntax Check" >> $GITHUB_STEP_SUMMARY - if [ "${ERRORS}" -gt 0 ]; then - echo "**${ERRORS} syntax error(s) found.**" >> $GITHUB_STEP_SUMMARY - exit 1 - else - echo "All PHP files passed syntax check." >> $GITHUB_STEP_SUMMARY - fi - - - name: XML manifest validation - run: | - echo "### XML Manifest Validation" >> $GITHUB_STEP_SUMMARY - ERRORS=0 - - # Find the extension manifest (XML with /dev/null; then - MANIFEST="$XML_FILE" - break - fi - done - - if [ -z "$MANIFEST" ]; then - echo "No Joomla extension manifest found (XML file with \`> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - else - echo "Manifest found: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY - - # Validate well-formed XML - php -r " - \$xml = @simplexml_load_file('$MANIFEST'); - if (\$xml === false) { - echo 'INVALID'; - exit(1); - } - echo 'VALID'; - " > /tmp/xml_result 2>&1 - XML_RESULT=$(cat /tmp/xml_result) - if [ "$XML_RESULT" != "VALID" ]; then - echo "Manifest is not well-formed XML." >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - else - echo "Manifest is well-formed XML." >> $GITHUB_STEP_SUMMARY - fi - - # Check required tags: name, version, author - for TAG in name version author; do - if ! grep -q "<${TAG}>" "$MANIFEST" 2>/dev/null; then - echo "Missing required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - else - echo "Found required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY - fi - done - - # Namespace is required for components/plugins but not packages - EXT_TYPE=$(grep -oP ']*\btype="\K[^"]+' "$MANIFEST" | head -1) - if [ "$EXT_TYPE" != "package" ]; then - if ! grep -q "/dev/null; then - echo "Missing required tag: \`\` (required for Joomla 5+ ${EXT_TYPE} extensions)" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - else - echo "Found required tag: \`\`" >> $GITHUB_STEP_SUMMARY - fi - else - echo "Package extension — \`\` not required." >> $GITHUB_STEP_SUMMARY - fi - fi - - if [ "${ERRORS}" -gt 0 ]; then - echo "" >> $GITHUB_STEP_SUMMARY - echo "**${ERRORS} manifest issue(s) found.**" >> $GITHUB_STEP_SUMMARY - exit 1 - else - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Manifest validation passed.**" >> $GITHUB_STEP_SUMMARY - fi - - - name: Update server & packaging checks - continue-on-error: true - run: | - echo "### Update Server & Packaging" >> $GITHUB_STEP_SUMMARY - WARNINGS=0 - - # Find the extension manifest - MANIFEST="" - for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do - if grep -q "/dev/null; then - MANIFEST="$XML_FILE" - break - fi - done - - if [ -z "$MANIFEST" ]; then - echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY - else - EXT_TYPE=$(grep -oP ']*\btype="\K[^"]+' "$MANIFEST" | head -1) - - # 1. Check exists and uses MokoGitea update server - if ! grep -q '' "$MANIFEST" 2>/dev/null; then - echo "::warning file=${MANIFEST}::Missing \`\` tag — extension will not receive OTA updates" - echo "- **Missing** \`\` — extension will not receive OTA updates" >> $GITHUB_STEP_SUMMARY - WARNINGS=$((WARNINGS + 1)) - else - SERVER_URL=$(grep -oP ']*>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1) - if [ -z "$SERVER_URL" ]; then - echo "::warning file=${MANIFEST}::\`\` is empty — no server URL defined" - echo "- **Empty** \`\` — no server URL defined" >> $GITHUB_STEP_SUMMARY - WARNINGS=$((WARNINGS + 1)) - elif ! echo "$SERVER_URL" | grep -q 'git\.mokoconsulting\.tech'; then - echo "::warning file=${MANIFEST}::Update server does not use MokoGitea engine: ${SERVER_URL}" - echo "- **Non-MokoGitea update server:** \`${SERVER_URL}\`" >> $GITHUB_STEP_SUMMARY - echo " Expected: \`https://git.mokoconsulting.tech/{org}/{repo}/updates.xml\`" >> $GITHUB_STEP_SUMMARY - WARNINGS=$((WARNINGS + 1)) - else - echo "- \`\`: MokoGitea engine ✓" >> $GITHUB_STEP_SUMMARY - fi - fi - - # 2. Check tag exists - if ! grep -q '/dev/null; then - echo "::warning file=${MANIFEST}::Missing \`\` tag — download ID authentication is not configured" - echo "- **Missing** \`\` — download ID authentication not configured" >> $GITHUB_STEP_SUMMARY - WARNINGS=$((WARNINGS + 1)) - else - echo "- \`\`: present ✓" >> $GITHUB_STEP_SUMMARY - fi - - # 3. For packages: check tag - if [ "$EXT_TYPE" = "package" ]; then - if ! grep -q '' "$MANIFEST" 2>/dev/null; then - echo "::warning file=${MANIFEST}::Package is missing \`\` — child extensions will not be removed on uninstall" - echo "- **Missing** \`\` — child extensions will remain when package is uninstalled" >> $GITHUB_STEP_SUMMARY - WARNINGS=$((WARNINGS + 1)) - else - echo "- \`\`: present ✓" >> $GITHUB_STEP_SUMMARY - fi - fi - fi - - echo "" >> $GITHUB_STEP_SUMMARY - if [ "$WARNINGS" -gt 0 ]; then - echo "**${WARNINGS} packaging warning(s).** These won't block CI but should be addressed." >> $GITHUB_STEP_SUMMARY - else - echo "**Update server & packaging checks passed.**" >> $GITHUB_STEP_SUMMARY - fi - - - name: Check language files referenced in manifest - run: | - echo "### Language File Check" >> $GITHUB_STEP_SUMMARY - ERRORS=0 - - MANIFEST="" - for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do - if grep -q "/dev/null; then - MANIFEST="$XML_FILE" - break - fi - done - - if [ -n "$MANIFEST" ]; then - # Extract language file references from manifest - LANG_FILES=$(grep -oP 'language\s+tag="[^"]*"[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null || true) - if [ -z "$LANG_FILES" ]; then - echo "No language file references found in manifest — skipping." >> $GITHUB_STEP_SUMMARY - else - while IFS= read -r LANG_FILE; do - LANG_FILE=$(echo "$LANG_FILE" | xargs) - if [ -z "$LANG_FILE" ]; then - continue - fi - # Check in common locations - FOUND=0 - for BASE in "." "src" "htdocs"; do - if [ -f "${BASE}/${LANG_FILE}" ]; then - FOUND=1 - break - fi - done - if [ "$FOUND" -eq 0 ]; then - echo "Missing language file: \`${LANG_FILE}\`" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - else - echo "Language file present: \`${LANG_FILE}\`" >> $GITHUB_STEP_SUMMARY - fi - done <<< "$LANG_FILES" - fi - else - echo "No manifest found — skipping language check." >> $GITHUB_STEP_SUMMARY - fi - - if [ "${ERRORS}" -gt 0 ]; then - echo "" >> $GITHUB_STEP_SUMMARY - echo "**${ERRORS} missing language file(s).**" >> $GITHUB_STEP_SUMMARY - exit 1 - else - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Language file check passed.**" >> $GITHUB_STEP_SUMMARY - fi - - - name: Check index.html files in directories - run: | - echo "### Index.html Check" >> $GITHUB_STEP_SUMMARY - MISSING=0 - CHECKED=0 - - for DIR in src/ htdocs/; do - if [ -d "$DIR" ]; then - while IFS= read -r -d '' SUBDIR; do - CHECKED=$((CHECKED + 1)) - if [ ! -f "${SUBDIR}/index.html" ]; then - echo "Missing index.html in: \`${SUBDIR}\`" >> $GITHUB_STEP_SUMMARY - MISSING=$((MISSING + 1)) - fi - done < <(find "$DIR" -type d -print0) - fi - done - - if [ "${CHECKED}" -eq 0 ]; then - echo "No src/ or htdocs/ directories found — skipping." >> $GITHUB_STEP_SUMMARY - elif [ "${MISSING}" -gt 0 ]; then - echo "" >> $GITHUB_STEP_SUMMARY - echo "**${MISSING} director(ies) missing index.html out of ${CHECKED} checked.**" >> $GITHUB_STEP_SUMMARY - exit 1 - else - echo "All ${CHECKED} directories contain index.html." >> $GITHUB_STEP_SUMMARY - fi - - - name: Check config.xml and access.xml for components - run: | - echo "### Component Config & ACL Check" >> $GITHUB_STEP_SUMMARY - ERRORS=0 - - # Find all component manifests (XML with type="component") - # Uses maxdepth 10 to reach into nested package repos (packages/*/source/packages/com_*/...) - COMP_MANIFESTS=$(find . -maxdepth 10 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*" -not -path "./.claude/*" -exec grep -l ']*type="component"' {} \; 2>/dev/null || true) - - if [ -z "$COMP_MANIFESTS" ]; then - echo "No component extensions found — skipping." >> $GITHUB_STEP_SUMMARY - else - for MANIFEST in $COMP_MANIFESTS; do - COMP_DIR=$(dirname "$MANIFEST") - COMP_NAME=$(basename "$COMP_DIR") - echo "Component: `${COMP_NAME}` (manifest: `${MANIFEST}`)" >> $GITHUB_STEP_SUMMARY - - # Check access.xml exists - ACCESS_FILE=$(find "$COMP_DIR" -name "access.xml" -not -path "./.git/*" 2>/dev/null | head -1) - if [ -z "$ACCESS_FILE" ]; then - echo "- Missing `access.xml` — ACL permissions will not work." >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - else - if command -v php &> /dev/null; then - if ! php -r "@simplexml_load_file('$ACCESS_FILE') ?: exit(1);" 2>/dev/null; then - echo "- `access.xml` is not well-formed XML." >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - else - for ACTION in core.admin core.manage; do - if ! grep -q "name=\"${ACTION}\"" "$ACCESS_FILE" 2>/dev/null; then - echo "- `access.xml` missing required action: `${ACTION}`" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - fi - done - echo "- `access.xml`: valid" >> $GITHUB_STEP_SUMMARY - fi - fi - fi - - # Check config.xml exists - CONFIG_FILE=$(find "$COMP_DIR" -name "config.xml" -not -path "./.git/*" 2>/dev/null | head -1) - if [ -z "$CONFIG_FILE" ]; then - echo "- Missing `config.xml` — component Options page will be empty." >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - else - if command -v php &> /dev/null; then - if ! php -r "@simplexml_load_file('$CONFIG_FILE') ?: exit(1);" 2>/dev/null; then - echo "- `config.xml` is not well-formed XML." >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - else - echo "- `config.xml`: valid" >> $GITHUB_STEP_SUMMARY - fi - fi - fi - done - fi - - echo "" >> $GITHUB_STEP_SUMMARY - if [ "${ERRORS}" -gt 0 ]; then - echo "**${ERRORS} config/ACL issue(s) found.**" >> $GITHUB_STEP_SUMMARY - exit 1 - else - echo "**Component config & ACL check passed.**" >> $GITHUB_STEP_SUMMARY - fi - - - name: SQL schema validation - run: | - echo "### SQL Schema Validation" >> $GITHUB_STEP_SUMMARY - ERRORS=0 - - # Find SQL files in source/htdocs - SQL_FILES=$(find . -name "*.sql" -path "*/sql/*" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null) - if [ -z "$SQL_FILES" ]; then - echo "No SQL files found — skipping." >> $GITHUB_STEP_SUMMARY - else - echo "Found $(echo "$SQL_FILES" | wc -l) SQL file(s)" >> $GITHUB_STEP_SUMMARY - - for FILE in $SQL_FILES; do - # Basic syntax check: balanced parentheses, no empty files - SIZE=$(wc -c < "$FILE" | tr -d ' ') - if [ "$SIZE" -eq 0 ]; then - echo "- Empty SQL file: \`${FILE}\`" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - continue - fi - - # Check for common SQL errors - if grep -qP '^\s*$' "$FILE" && [ "$SIZE" -lt 5 ]; then - echo "- Whitespace-only SQL file: \`${FILE}\`" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - continue - fi - - echo "- \`${FILE}\`: ${SIZE} bytes" >> $GITHUB_STEP_SUMMARY - done - - # Check update SQL files follow version numbering pattern - UPDATE_DIR=$(find . -path "*/sql/updates/mysql" -type d -not -path "./.git/*" 2>/dev/null | head -1) - if [ -n "$UPDATE_DIR" ]; then - BAD_NAMES=0 - for UFILE in "$UPDATE_DIR"/*.sql; do - [ ! -f "$UFILE" ] && continue - BASENAME=$(basename "$UFILE" .sql) - if ! echo "$BASENAME" | grep -qP '^\d+\.\d+\.\d+'; then - echo "- Update file \`${UFILE}\` does not follow version naming (expected X.Y.Z.sql)" >> $GITHUB_STEP_SUMMARY - BAD_NAMES=$((BAD_NAMES + 1)) - fi - done - if [ "$BAD_NAMES" -gt 0 ]; then - ERRORS=$((ERRORS + BAD_NAMES)) - fi - fi - fi - - echo "" >> $GITHUB_STEP_SUMMARY - if [ "${ERRORS}" -gt 0 ]; then - echo "**${ERRORS} SQL issue(s) found.**" >> $GITHUB_STEP_SUMMARY - exit 1 - else - echo "**SQL schema validation passed.**" >> $GITHUB_STEP_SUMMARY - fi - - - name: Manifest file references check - run: | - echo "### Manifest File References" >> $GITHUB_STEP_SUMMARY - ERRORS=0 - - MANIFEST="" - for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do - if grep -q "/dev/null; then - MANIFEST="$XML_FILE" - break - fi - done - - if [ -z "$MANIFEST" ]; then - echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY - else - MANIFEST_DIR=$(dirname "$MANIFEST") - - # Check references - FILENAMES=$(grep -oP ']*>\K[^<]+' "$MANIFEST" 2>/dev/null || true) - for F in $FILENAMES; do - if [ ! -f "${MANIFEST_DIR}/${F}" ] && [ ! -d "${MANIFEST_DIR}/${F}" ]; then - echo "- Missing: \`${F}\` (referenced in manifest)" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - fi - done - - # Check references - FOLDERS=$(grep -oP ']*>\K[^<]+' "$MANIFEST" 2>/dev/null || true) - for F in $FOLDERS; do - if [ ! -d "${MANIFEST_DIR}/${F}" ]; then - echo "- Missing folder: \`${F}\` (referenced in manifest)" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - fi - done - - # Check references in package manifests (ZIP files won't exist in source) - EXT_TYPE=$(grep -oP ']*\btype="\K[^"]+' "$MANIFEST" | head -1) - if [ "$EXT_TYPE" != "package" ]; then - FILES=$(grep -oP ']*>\K[^<]+' "$MANIFEST" 2>/dev/null || true) - for F in $FILES; do - if [ ! -f "${MANIFEST_DIR}/${F}" ]; then - echo "- Missing file: \`${F}\` (referenced in manifest)" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - fi - done - fi - fi - - echo "" >> $GITHUB_STEP_SUMMARY - if [ "${ERRORS}" -gt 0 ]; then - echo "**${ERRORS} missing file reference(s).**" >> $GITHUB_STEP_SUMMARY - exit 1 - else - echo "**Manifest file references check passed.**" >> $GITHUB_STEP_SUMMARY - fi - - - name: Form XML validation - run: | - echo "### Form XML Validation" >> $GITHUB_STEP_SUMMARY - ERRORS=0 - - FORM_FILES=$(find . -name "*.xml" -path "*/forms/*" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null) - if [ -z "$FORM_FILES" ]; then - echo "No form XML files found — skipping." >> $GITHUB_STEP_SUMMARY - else - echo "Found $(echo "$FORM_FILES" | wc -l) form file(s)" >> $GITHUB_STEP_SUMMARY - for FILE in $FORM_FILES; do - if command -v php &> /dev/null; then - if ! php -r "@simplexml_load_file('$FILE') ?: exit(1);" 2>/dev/null; then - echo "- \`${FILE}\`: malformed XML" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - else - # Check for valid Joomla form structure - if ! grep -qE '/dev/null; then - echo "- \`${FILE}\`: no \`
\`, \`\`, or \`
\` elements found" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - else - echo "- \`${FILE}\`: valid" >> $GITHUB_STEP_SUMMARY - fi - fi - fi - done - fi - - echo "" >> $GITHUB_STEP_SUMMARY - if [ "${ERRORS}" -gt 0 ]; then - echo "**${ERRORS} form XML issue(s).**" >> $GITHUB_STEP_SUMMARY - exit 1 - else - echo "**Form XML validation passed.**" >> $GITHUB_STEP_SUMMARY - fi - - - name: Deprecated Joomla API check - continue-on-error: true - run: | - echo "### Deprecated Joomla API Check" >> $GITHUB_STEP_SUMMARY - WARNINGS=0 - - SRC_DIR="" - for DIR in source/ src/ htdocs/; do - [ -d "$DIR" ] && SRC_DIR="$DIR" && break - done - - if [ -z "$SRC_DIR" ]; then - echo "No source directory found — skipping." >> $GITHUB_STEP_SUMMARY - else - # Joomla 3/4 deprecated patterns that break in Joomla 6 - PATTERNS=( - 'JFactory::' - 'JText::' - 'JHtml::' - 'JRoute::' - 'JUri::' - 'JLog::' - 'JTable::' - 'JInput' - 'CMSFactory::\$application' - 'JApplicationCms' - ) - - for PATTERN in "${PATTERNS[@]}"; do - HITS=$(grep -rnl "$PATTERN" "$SRC_DIR" --include="*.php" 2>/dev/null || true) - if [ -n "$HITS" ]; then - COUNT=$(echo "$HITS" | wc -l) - echo "- \`${PATTERN}\` found in ${COUNT} file(s)" >> $GITHUB_STEP_SUMMARY - WARNINGS=$((WARNINGS + COUNT)) - fi - done - - echo "" >> $GITHUB_STEP_SUMMARY - if [ "$WARNINGS" -gt 0 ]; then - echo "**${WARNINGS} deprecated API usage(s) found.** These will break in Joomla 6." >> $GITHUB_STEP_SUMMARY - else - echo "**No deprecated APIs found.**" >> $GITHUB_STEP_SUMMARY - fi - fi - - - name: Template output escaping check - continue-on-error: true - run: | - echo "### Template Output Escaping" >> $GITHUB_STEP_SUMMARY - WARNINGS=0 - - TMPL_FILES=$(find . -name "*.php" -path "*/tmpl/*" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null) - if [ -z "$TMPL_FILES" ]; then - echo "No template files found — skipping." >> $GITHUB_STEP_SUMMARY - else - echo "Found $(echo "$TMPL_FILES" | wc -l) template file(s)" >> $GITHUB_STEP_SUMMARY - - for FILE in $TMPL_FILES; do - # Check for unescaped output: or echo $var without escape() - UNESCAPED=$(grep -nP '<\?=\s*\$(?!this->escape)' "$FILE" 2>/dev/null || true) - if [ -n "$UNESCAPED" ]; then - HITS=$(echo "$UNESCAPED" | wc -l) - echo "- \`${FILE}\`: ${HITS} unescaped \`\` output(s) — use \`escape(\$var) ?>\`" >> $GITHUB_STEP_SUMMARY - WARNINGS=$((WARNINGS + HITS)) - fi - - # Check for echo without escaping in template context - RAW_ECHO=$(grep -nP '^\s*echo\s+\$(?!this->escape)' "$FILE" 2>/dev/null || true) - if [ -n "$RAW_ECHO" ]; then - HITS=$(echo "$RAW_ECHO" | wc -l) - echo "- \`${FILE}\`: ${HITS} raw \`echo \$var\` — consider \`echo \$this->escape(\$var)\`" >> $GITHUB_STEP_SUMMARY - WARNINGS=$((WARNINGS + HITS)) - fi - done - - echo "" >> $GITHUB_STEP_SUMMARY - if [ "$WARNINGS" -gt 0 ]; then - echo "**${WARNINGS} potential XSS risk(s) in templates.** Review unescaped output." >> $GITHUB_STEP_SUMMARY - else - echo "**All template output appears properly escaped.**" >> $GITHUB_STEP_SUMMARY - fi - fi - - - name: Namespace consistency check - run: | - echo "### Namespace Consistency" >> $GITHUB_STEP_SUMMARY - ERRORS=0 - - # Find component/plugin manifests with tags - MANIFESTS=$(find . -maxdepth 4 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*" -exec grep -l '/dev/null || true) - - if [ -z "$MANIFESTS" ]; then - echo "No manifests with \`\` found — skipping." >> $GITHUB_STEP_SUMMARY - else - for MANIFEST in $MANIFESTS; do - NS_PATH=$(grep -oP ']*>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1) - [ -z "$NS_PATH" ] && continue - MANIFEST_DIR=$(dirname "$MANIFEST") - - echo "Manifest: \`${MANIFEST}\` → namespace \`${NS_PATH}\`" >> $GITHUB_STEP_SUMMARY - - # Check PHP files have matching namespace - while IFS= read -r -d '' PHP_FILE; do - FILE_NS=$(grep -oP '^\s*namespace\s+\K[^;]+' "$PHP_FILE" 2>/dev/null | head -1) - [ -z "$FILE_NS" ] && continue - - # Namespace should start with the manifest namespace path - if ! echo "$FILE_NS" | grep -qF "${NS_PATH}"; then - echo "- \`${PHP_FILE}\`: namespace \`${FILE_NS}\` doesn't match manifest \`${NS_PATH}\`" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - fi - done < <(find "$MANIFEST_DIR" -name "*.php" -path "*/src/*" -not -path "./vendor/*" -print0 2>/dev/null) - done - fi - - echo "" >> $GITHUB_STEP_SUMMARY - if [ "${ERRORS}" -gt 0 ]; then - echo "**${ERRORS} namespace mismatch(es).**" >> $GITHUB_STEP_SUMMARY - exit 1 - else - echo "**Namespace consistency check passed.**" >> $GITHUB_STEP_SUMMARY - fi - - - name: SPDX license header check - continue-on-error: true - run: | - echo "### SPDX License Headers" >> $GITHUB_STEP_SUMMARY - MISSING=0 - - SRC_DIR="" - for DIR in source/ src/ htdocs/; do - [ -d "$DIR" ] && SRC_DIR="$DIR" && break - done - - if [ -z "$SRC_DIR" ]; then - echo "No source directory found — skipping." >> $GITHUB_STEP_SUMMARY - else - TOTAL=0 - while IFS= read -r -d '' FILE; do - TOTAL=$((TOTAL + 1)) - if ! head -10 "$FILE" | grep -qi "SPDX"; then - echo "- Missing SPDX header: \`${FILE}\`" >> $GITHUB_STEP_SUMMARY - MISSING=$((MISSING + 1)) - fi - done < <(find "$SRC_DIR" -name "*.php" -not -path "./vendor/*" -print0) - - echo "" >> $GITHUB_STEP_SUMMARY - if [ "$MISSING" -gt 0 ]; then - echo "**${MISSING}/${TOTAL} PHP file(s) missing SPDX license header.**" >> $GITHUB_STEP_SUMMARY - else - echo "**All ${TOTAL} PHP files have SPDX headers.**" >> $GITHUB_STEP_SUMMARY - fi - fi - - - name: Service provider check - run: | - echo "### Service Provider Check" >> $GITHUB_STEP_SUMMARY - ERRORS=0 - - PROVIDERS=$(find . -name "provider.php" -path "*/services/*" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null) - if [ -z "$PROVIDERS" ]; then - echo "No service providers found — skipping." >> $GITHUB_STEP_SUMMARY - else - for FILE in $PROVIDERS; do - # Must return a ServiceProviderInterface - if ! grep -qP 'ServiceProviderInterface|ComponentInterface|MVCFactoryInterface|DispatcherInterface' "$FILE" 2>/dev/null; then - echo "- \`${FILE}\`: does not reference ServiceProviderInterface or component interfaces" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - else - echo "- \`${FILE}\`: valid service provider" >> $GITHUB_STEP_SUMMARY - fi - - # Must have return statement - if ! grep -qP '^\s*return\s+new\s+' "$FILE" 2>/dev/null; then - echo "- \`${FILE}\`: missing \`return new ...\` statement" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - fi - done - fi - - echo "" >> $GITHUB_STEP_SUMMARY - if [ "${ERRORS}" -gt 0 ]; then - echo "**${ERRORS} service provider issue(s).**" >> $GITHUB_STEP_SUMMARY - exit 1 - else - echo "**Service provider check passed.**" >> $GITHUB_STEP_SUMMARY - fi - - - name: Script file reference check - run: | - echo "### Script File Reference" >> $GITHUB_STEP_SUMMARY - ERRORS=0 - - MANIFEST="" - for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do - if grep -q "/dev/null; then - MANIFEST="$XML_FILE" - break - fi - done - - if [ -z "$MANIFEST" ]; then - echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY - else - MANIFEST_DIR=$(dirname "$MANIFEST") - SCRIPT_FILE=$(grep -oP '\K[^<]+' "$MANIFEST" 2>/dev/null | head -1) - if [ -z "$SCRIPT_FILE" ]; then - echo "No \`\` referenced — skipping." >> $GITHUB_STEP_SUMMARY - elif [ ! -f "${MANIFEST_DIR}/${SCRIPT_FILE}" ]; then - echo "::error file=${MANIFEST}::Manifest references \`${SCRIPT_FILE}\` but file does not exist" - echo "- **Missing** \`${SCRIPT_FILE}\` — referenced in \`\` but not found" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - else - echo "- \`${SCRIPT_FILE}\`: present ✓" >> $GITHUB_STEP_SUMMARY - fi - fi - - echo "" >> $GITHUB_STEP_SUMMARY - if [ "${ERRORS}" -gt 0 ]; then - echo "**${ERRORS} script file issue(s).**" >> $GITHUB_STEP_SUMMARY - exit 1 - else - echo "**Script file reference check passed.**" >> $GITHUB_STEP_SUMMARY - fi - - - name: Media folder validation - run: | - echo "### Media Folder Validation" >> $GITHUB_STEP_SUMMARY - ERRORS=0 - - MANIFEST="" - for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do - if grep -q "/dev/null; then - MANIFEST="$XML_FILE" - break - fi - done - - if [ -z "$MANIFEST" ]; then - echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY - else - MANIFEST_DIR=$(dirname "$MANIFEST") - - # Check tag and its folder/filename children - MEDIA_DEST=$(grep -oP ']*\bdestination="\K[^"]+' "$MANIFEST" 2>/dev/null | head -1) - MEDIA_FOLDER=$(grep -oP ']*\bfolder="\K[^"]+' "$MANIFEST" 2>/dev/null | head -1) - - if [ -z "$MEDIA_DEST" ] && [ -z "$MEDIA_FOLDER" ]; then - echo "No \`\` tag found — skipping." >> $GITHUB_STEP_SUMMARY - else - if [ -n "$MEDIA_FOLDER" ] && [ ! -d "${MANIFEST_DIR}/${MEDIA_FOLDER}" ]; then - echo "::error file=${MANIFEST}::\`\` references missing directory" - echo "- **Missing** media folder \`${MEDIA_FOLDER}\`" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - else - echo "- Media folder \`${MEDIA_FOLDER:-(inline)}\`: present ✓" >> $GITHUB_STEP_SUMMARY - - # Check child references inside block - if [ -n "$MEDIA_FOLDER" ]; then - MEDIA_FOLDERS=$(sed -n '//p' "$MANIFEST" | grep -oP '\K[^<]+' 2>/dev/null || true) - for F in $MEDIA_FOLDERS; do - if [ ! -d "${MANIFEST_DIR}/${MEDIA_FOLDER}/${F}" ]; then - echo "- **Missing** media subfolder \`${MEDIA_FOLDER}/${F}\`" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - fi - done - - MEDIA_FILES=$(sed -n '//p' "$MANIFEST" | grep -oP '\K[^<]+' 2>/dev/null || true) - for F in $MEDIA_FILES; do - if [ ! -f "${MANIFEST_DIR}/${MEDIA_FOLDER}/${F}" ]; then - echo "- **Missing** media file \`${MEDIA_FOLDER}/${F}\`" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - fi - done - fi - fi - fi - fi - - echo "" >> $GITHUB_STEP_SUMMARY - if [ "${ERRORS}" -gt 0 ]; then - echo "**${ERRORS} media reference issue(s).**" >> $GITHUB_STEP_SUMMARY - exit 1 - else - echo "**Media folder validation passed.**" >> $GITHUB_STEP_SUMMARY - fi - - - name: Target platform check - continue-on-error: true - run: | - echo "### Target Platform Check" >> $GITHUB_STEP_SUMMARY - WARNINGS=0 - - MANIFEST="" - for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do - if grep -q "/dev/null; then - MANIFEST="$XML_FILE" - break - fi - done - - if [ -z "$MANIFEST" ]; then - echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY - else - # Check updates.xml for targetplatform if it exists - if [ -f "updates.xml" ]; then - if ! grep -q '/dev/null; then - echo "::warning file=updates.xml::No \`\` found — Joomla updater cannot filter by compatible version" - echo "- **Missing** \`\` in updates.xml" >> $GITHUB_STEP_SUMMARY - WARNINGS=$((WARNINGS + 1)) - else - echo "- \`\` in updates.xml: present ✓" >> $GITHUB_STEP_SUMMARY - fi - fi - - # Check manifest for minimum PHP/Joomla version hints - if ! grep -qP '|targetplatform|joomla.*version' "$MANIFEST" 2>/dev/null; then - echo "::warning file=${MANIFEST}::No minimum Joomla or PHP version constraint found in manifest" - echo "- **Missing** version constraints (\`\` or \`\`)" >> $GITHUB_STEP_SUMMARY - WARNINGS=$((WARNINGS + 1)) - else - echo "- Version constraints in manifest: present ✓" >> $GITHUB_STEP_SUMMARY - fi - fi - - echo "" >> $GITHUB_STEP_SUMMARY - if [ "$WARNINGS" -gt 0 ]; then - echo "**${WARNINGS} target platform warning(s).**" >> $GITHUB_STEP_SUMMARY - else - echo "**Target platform check passed.**" >> $GITHUB_STEP_SUMMARY - fi - - - name: Changelog URL check - continue-on-error: true - run: | - echo "### Changelog URL Check" >> $GITHUB_STEP_SUMMARY - WARNINGS=0 - - MANIFEST="" - for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do - if grep -q "/dev/null; then - MANIFEST="$XML_FILE" - break - fi - done - - if [ -z "$MANIFEST" ]; then - echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY - else - if ! grep -q '' "$MANIFEST" 2>/dev/null; then - echo "::warning file=${MANIFEST}::Missing \`\` — Joomla updater will not display changelogs" - echo "- **Missing** \`\` — Joomla 4+ shows changelogs in the update manager when this is set" >> $GITHUB_STEP_SUMMARY - WARNINGS=$((WARNINGS + 1)) - else - CHANGELOG_URL=$(grep -oP '\K[^<]+' "$MANIFEST" | head -1) - echo "- \`\`: \`${CHANGELOG_URL}\` ✓" >> $GITHUB_STEP_SUMMARY - fi - fi - - echo "" >> $GITHUB_STEP_SUMMARY - if [ "$WARNINGS" -gt 0 ]; then - echo "**${WARNINGS} changelog URL warning(s).**" >> $GITHUB_STEP_SUMMARY - else - echo "**Changelog URL check passed.**" >> $GITHUB_STEP_SUMMARY - fi - - - name: Duplicate file references check - continue-on-error: true - run: | - echo "### Duplicate File References" >> $GITHUB_STEP_SUMMARY - WARNINGS=0 - - MANIFEST="" - for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do - if grep -q "/dev/null; then - MANIFEST="$XML_FILE" - break - fi - done - - if [ -z "$MANIFEST" ]; then - echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY - else - # Extract all and references - ALL_REFS=$(grep -oP '<(filename|folder)[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null | sort || true) - if [ -z "$ALL_REFS" ]; then - echo "No file/folder references found — skipping." >> $GITHUB_STEP_SUMMARY - else - DUPES=$(echo "$ALL_REFS" | uniq -d) - if [ -n "$DUPES" ]; then - while IFS= read -r DUP; do - COUNT=$(echo "$ALL_REFS" | grep -cx "$DUP") - echo "::warning file=${MANIFEST}::Duplicate reference: \`${DUP}\` appears ${COUNT} times (may be valid if in different sections)" - echo "- **Duplicate:** \`${DUP}\` (${COUNT}x) — check if cross-section" >> $GITHUB_STEP_SUMMARY - WARNINGS=$((WARNINGS + 1)) - done <<< "$DUPES" - else - TOTAL=$(echo "$ALL_REFS" | wc -l) - echo "All ${TOTAL} file/folder references are unique." >> $GITHUB_STEP_SUMMARY - fi - fi - fi - - echo "" >> $GITHUB_STEP_SUMMARY - if [ "$WARNINGS" -gt 0 ]; then - echo "**${WARNINGS} duplicate reference(s) found.** Review for cross-section validity." >> $GITHUB_STEP_SUMMARY - else - echo "**Duplicate file references check passed.**" >> $GITHUB_STEP_SUMMARY - fi - - - name: Empty language keys check - continue-on-error: true - run: | - echo "### Empty Language Keys" >> $GITHUB_STEP_SUMMARY - WARNINGS=0 - - LANG_FILES=$(find . -name "*.ini" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null) - if [ -z "$LANG_FILES" ]; then - echo "No .ini language files found — skipping." >> $GITHUB_STEP_SUMMARY - else - TOTAL_FILES=0 - for FILE in $LANG_FILES; do - TOTAL_FILES=$((TOTAL_FILES + 1)) - # Find lines with KEY= but no value (empty or whitespace-only after =) - EMPTY_KEYS=$(grep -nP '^[A-Z_]+=\s*$' "$FILE" 2>/dev/null || true) - if [ -n "$EMPTY_KEYS" ]; then - COUNT=$(echo "$EMPTY_KEYS" | wc -l) - echo "::warning file=${FILE}::${COUNT} empty language key(s)" - echo "- \`${FILE}\`: ${COUNT} empty key(s)" >> $GITHUB_STEP_SUMMARY - while IFS= read -r LINE; do - LINE_NUM=$(echo "$LINE" | cut -d: -f1) - KEY=$(echo "$LINE" | cut -d: -f2 | cut -d= -f1) - echo " - Line ${LINE_NUM}: \`${KEY}\`" >> $GITHUB_STEP_SUMMARY - done <<< "$EMPTY_KEYS" - WARNINGS=$((WARNINGS + COUNT)) - fi - done - - if [ "$WARNINGS" -eq 0 ]; then - echo "All ${TOTAL_FILES} language file(s) have populated keys." >> $GITHUB_STEP_SUMMARY - fi - fi - - echo "" >> $GITHUB_STEP_SUMMARY - if [ "$WARNINGS" -gt 0 ]; then - echo "**${WARNINGS} empty language key(s) across ${TOTAL_FILES} file(s).**" >> $GITHUB_STEP_SUMMARY - else - echo "**Empty language keys check passed.**" >> $GITHUB_STEP_SUMMARY - fi - - release-readiness: - name: Release Readiness Check - runs-on: ubuntu-latest - if: github.event_name == 'pull_request' && github.base_ref == 'main' - continue-on-error: true - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Validate release readiness - run: | - echo "## Release Readiness" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - ERRORS=0 - - # Extract version from README.md - README_VERSION=$(grep -oP '^\s*VERSION:\s*\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' README.md | head -1) - if [ -z "$README_VERSION" ]; then - echo "No VERSION found in README.md FILE INFORMATION block." >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - else - echo "README version: \`${README_VERSION}\`" >> $GITHUB_STEP_SUMMARY - fi - - # Find the extension manifest - MANIFEST="" - for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do - if grep -q "/dev/null; then - MANIFEST="$XML_FILE" - break - fi - done - - if [ -z "$MANIFEST" ]; then - echo "No Joomla extension manifest found." >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - else - echo "Manifest: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY - - # Check matches README VERSION - MANIFEST_VERSION=$(grep -oP '\K[^<]+' "$MANIFEST" | head -1) - if [ -z "$MANIFEST_VERSION" ]; then - echo "No \`\` tag in manifest." >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - elif [ -n "$README_VERSION" ] && [ "$MANIFEST_VERSION" != "$README_VERSION" ]; then - echo "Manifest version \`${MANIFEST_VERSION}\` does not match README \`${README_VERSION}\`." >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - else - echo "Manifest version: \`${MANIFEST_VERSION}\`" >> $GITHUB_STEP_SUMMARY - fi - - # Check extension type, element, client attributes - EXT_TYPE=$(grep -oP ']*\btype="\K[^"]+' "$MANIFEST" | head -1) - if [ -z "$EXT_TYPE" ]; then - echo "Missing \`type\` attribute on \`\` tag." >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - else - echo "Extension type: \`${EXT_TYPE}\`" >> $GITHUB_STEP_SUMMARY - fi - - # Element check (component/module/plugin name) - HAS_ELEMENT=$(grep -cP '<(element|name)>' "$MANIFEST" 2>/dev/null || echo "0") - if [ "$HAS_ELEMENT" -eq 0 ]; then - echo "Missing \`\` or \`\` in manifest." >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - fi - - # Client attribute for site/admin modules and plugins - if echo "$EXT_TYPE" | grep -qP "^(module|plugin)$"; then - HAS_CLIENT=$(grep -cP ']*\bclient=' "$MANIFEST" 2>/dev/null || echo "0") - if [ "$HAS_CLIENT" -eq 0 ]; then - echo "Missing \`client\` attribute for ${EXT_TYPE} extension." >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - fi - fi - fi - - # Check updates.xml exists - if [ -f "updates.xml" ] || [ -f "updates.xml" ]; then - echo "Update XML present." >> $GITHUB_STEP_SUMMARY - else - echo "No updates.xml found." >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - fi - - # Check CHANGELOG.md exists - if [ -f "CHANGELOG.md" ]; then - echo "CHANGELOG.md present." >> $GITHUB_STEP_SUMMARY - else - echo "No CHANGELOG.md found." >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - fi - - echo "" >> $GITHUB_STEP_SUMMARY - if [ $ERRORS -gt 0 ]; then - echo "**${ERRORS} issue(s) must be resolved before release.**" >> $GITHUB_STEP_SUMMARY - exit 1 - else - echo "**Extension is ready for release.**" >> $GITHUB_STEP_SUMMARY - fi - - test: - name: Tests (PHP ${{ matrix.php }}) - runs-on: ubuntu-latest - needs: lint-and-validate - - strategy: - fail-fast: false - matrix: - php: ['8.2', '8.3'] - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup PHP ${{ matrix.php }} - run: | - if ! command -v php &> /dev/null; then - sudo apt-get update -qq - sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 - fi - php -v && composer --version - - - name: Install dependencies - env: - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}"}}' - run: | - if [ -f "composer.json" ]; then - composer install \ - --no-interaction \ - --prefer-dist \ - --optimize-autoloader - else - echo "No composer.json found — skipping dependency install" - fi - - - name: Run tests - run: | - echo "### Test Results (PHP ${{ matrix.php }})" >> $GITHUB_STEP_SUMMARY - if [ -f "phpunit.xml" ] || [ -f "phpunit.xml.dist" ]; then - vendor/bin/phpunit --testdox 2>&1 | tee /tmp/test-output.log - EXIT=${PIPESTATUS[0]} - if [ $EXIT -eq 0 ]; then - echo "All tests passed." >> $GITHUB_STEP_SUMMARY - else - echo "Test failures detected — see log." >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - cat /tmp/test-output.log >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - fi - exit $EXIT - else - echo "No phpunit.xml found — skipping tests." >> $GITHUB_STEP_SUMMARY - fi - - static-analysis: - name: PHPStan Analysis - runs-on: ubuntu-latest - needs: lint-and-validate - continue-on-error: true - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup PHP - run: | - if ! command -v php &> /dev/null; then - sudo apt-get update -qq - sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 - fi - php -v && composer --version - - - name: Install dependencies - env: - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}"}}' - run: | - if [ -f "composer.json" ]; then - composer install --no-interaction --prefer-dist --optimize-autoloader - fi - - - name: Install PHPStan - run: | - if ! command -v vendor/bin/phpstan &> /dev/null; then - composer require --dev phpstan/phpstan --no-interaction 2>/dev/null || \ - composer global require phpstan/phpstan --no-interaction - fi - - - name: Run PHPStan - run: | - echo "### PHPStan Static Analysis" >> $GITHUB_STEP_SUMMARY - PHPSTAN="vendor/bin/phpstan" - if [ ! -f "$PHPSTAN" ]; then - PHPSTAN=$(composer global config bin-dir --absolute 2>/dev/null)/phpstan - fi - - # Determine source directory - SRC_DIR="" - for DIR in src/ htdocs/ lib/; do - if [ -d "$DIR" ]; then - SRC_DIR="$DIR" - break - fi - done - - if [ -z "$SRC_DIR" ]; then - echo "No source directory found (src/, htdocs/, lib/) — skipping." >> $GITHUB_STEP_SUMMARY - exit 0 - fi - - # Use repo phpstan.neon if present, otherwise use baseline config - ARGS="analyse ${SRC_DIR} --memory-limit=512M --no-progress --error-format=table" - if [ -f "phpstan.neon" ] || [ -f "phpstan.neon.dist" ]; then - echo "Using project PHPStan config." >> $GITHUB_STEP_SUMMARY - else - ARGS="$ARGS --level=3" - echo "No phpstan.neon found — using level 3 (type inference)." >> $GITHUB_STEP_SUMMARY - fi - - $PHPSTAN $ARGS 2>&1 | tee /tmp/phpstan-output.txt - EXIT=${PIPESTATUS[0]} - - if [ $EXIT -eq 0 ]; then - echo "**No errors found.**" >> $GITHUB_STEP_SUMMARY - else - ERRORS=$(grep -c "ERROR" /tmp/phpstan-output.txt 2>/dev/null || echo "some") - echo "**${ERRORS} error(s) found.** Review output above." >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - tail -30 /tmp/phpstan-output.txt >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - fi - exit $EXIT - - pre-release: - name: Build RC Pre-Release - runs-on: ubuntu-latest - needs: [lint-and-validate, test] - if: github.event_name == 'pull_request' - - steps: - - name: Trigger pre-release build - env: - MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - REPO: ${{ github.repository }} - BRANCH: ${{ github.head_ref }} - run: | - curl -s -X POST \ - "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" \ - -H "Authorization: token ${MOKOGITEA_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/cleanup.yml b/.mokogitea/workflows/cleanup.yml index cef49d4..b4db32b 100644 --- a/.mokogitea/workflows/cleanup.yml +++ b/.mokogitea/workflows/cleanup.yml @@ -1 +1,87 @@ -IyBDb3B5cmlnaHQgKEMpIDIwMjYgTW9rbyBDb25zdWx0aW5nIDxoZWxsb0Btb2tvY29uc3VsdGluZy50ZWNoPgojCiMgU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IEdQTC0zLjAtb3ItbGF0ZXIKIwojIEZJTEUgSU5GT1JNQVRJT04KIyBERUZHUk9VUDogR2l0ZWEuV29ya2Zsb3cKIyBJTkdST1VQOiBNb2tvU3RhbmRhcmRzLk1haW50ZW5hbmNlCiMgUkVQTzogaHR0cHM6Ly9naXQubW9rb2NvbnN1bHRpbmcudGVjaC9Nb2tvQ29uc3VsdGluZy9Nb2tvU3RhbmRhcmRzCiMgUEFUSDogLy5naXRlYS93b3JrZmxvd3MvY2xlYW51cC55bWwKIyBWRVJTSU9OOiAwMS4wMC4wMAojIEJSSUVGOiBTY2hlZHVsZWQgY2xlYW51cCDigJQgZGVsZXRlIG1lcmdlZCBicmFuY2hlcyBhbmQgb2xkIHdvcmtmbG93IHJ1bnMKCm5hbWU6ICJVbml2ZXJzYWw6IFJlcG9zaXRvcnkgQ2xlYW51cCIKCm9uOgogIHNjaGVkdWxlOgogICAgLSBjcm9uOiAnMCAzICogKiAwJyAgIyBXZWVrbHkgb24gU3VuZGF5IGF0IDAzOjAwIFVUQwogIHdvcmtmbG93X2Rpc3BhdGNoOgoKcGVybWlzc2lvbnM6CiAgY29udGVudHM6IHdyaXRlCgplbnY6CiAgTU9LT0dJVEVBX1VSTDogJHt7IHZhcnMuR0lURUFfVVJMIHx8ICdodHRwczovL2dpdC5tb2tvY29uc3VsdGluZy50ZWNoJyB9fQoKam9iczoKICBjbGVhbnVwOgogICAgbmFtZTogQ2xlYW4gTWVyZ2VkIEJyYW5jaGVzCiAgICBydW5zLW9uOiB1YnVudHUtbGF0ZXN0CgogICAgc3RlcHM6CiAgICAgIC0gbmFtZTogQ2hlY2tvdXQKICAgICAgICB1c2VzOiBhY3Rpb25zL2NoZWNrb3V0QHY0CiAgICAgICAgd2l0aDoKICAgICAgICAgIGZldGNoLWRlcHRoOiAwCiAgICAgICAgICB0b2tlbjogJHt7IHNlY3JldHMuTU9LT0dJVEVBX1RPS0VOIH19CgogICAgICAtIG5hbWU6IERlbGV0ZSBtZXJnZWQgYnJhbmNoZXMKICAgICAgICBlbnY6CiAgICAgICAgICBNT0tPR0lURUFfVE9LRU46ICR7eyBzZWNyZXRzLk1PS09HSVRFQV9UT0tFTiB9fQogICAgICAgIHJ1bjogfAogICAgICAgICAgZWNobyAiPT09IE1lcmdlZCBCcmFuY2ggQ2xlYW51cCA9PT0iCiAgICAgICAgICBBUEk9IiR7TU9LT0dJVEVBX1VSTH0vYXBpL3YxL3JlcG9zLyR7eyBnaXRodWIucmVwb3NpdG9yeSB9fSIKCiAgICAgICAgICAjIExpc3QgYnJhbmNoZXMgdmlhIEFQSQogICAgICAgICAgQlJBTkNIRVM9JChjdXJsIC1zUyAtSCAiQXV0aG9yaXphdGlvbjogdG9rZW4gJHtNT0tPR0lURUFfVE9LRU59IiBcCiAgICAgICAgICAgICIke0FQSX0vYnJhbmNoZXM/bGltaXQ9NTAiIHwganEgLXIgJy5bXS5uYW1lJykKCiAgICAgICAgICBERUxFVEVEPTAKICAgICAgICAgIGZvciBCUkFOQ0ggaW4gJEJSQU5DSEVTOyBkbwogICAgICAgICAgICAjIFNraXAgcHJvdGVjdGVkIGJyYW5jaGVzCiAgICAgICAgICAgIGNhc2UgIiRCUkFOQ0giIGluCiAgICAgICAgICAgICAgbWFpbnxtYXN0ZXJ8ZGV2ZWxvcHxyZWxlYXNlLyp8aG90Zml4LyopIGNvbnRpbnVlIDs7CiAgICAgICAgICAgIGVzYWMKCiAgICAgICAgICAgICMgQ2hlY2sgaWYgYnJhbmNoIGlzIG1lcmdlZCBpbnRvIG1haW4KICAgICAgICAgICAgaWYgZ2l0IG1lcmdlLWJhc2UgLS1pcy1hbmNlc3RvciAib3JpZ2luLyR7QlJBTkNIfSIgb3JpZ2luL21haW4gMj4vZGV2L251bGw7IHRoZW4KICAgICAgICAgICAgICBlY2hvICIgIERlbGV0aW5nIG1lcmdlZCBicmFuY2g6ICR7QlJBTkNIfSIKICAgICAgICAgICAgICBjdXJsIC1zUyAtWCBERUxFVEUgLUggIkF1dGhvcml6YXRpb246IHRva2VuICR7TU9LT0dJVEVBX1RPS0VOfSIgXAogICAgICAgICAgICAgICAgIiR7QVBJfS9icmFuY2hlcy8ke0JSQU5DSH0iIDI+L2Rldi9udWxsIHx8IHRydWUKICAgICAgICAgICAgICBERUxFVEVEPSQoKERFTEVURUQgKyAxKSkKICAgICAgICAgICAgZmkKICAgICAgICAgIGRvbmUKCiAgICAgICAgICBlY2hvICJEZWxldGVkICR7REVMRVRFRH0gbWVyZ2VkIGJyYW5jaChlcykiCgogICAgICAtIG5hbWU6IENsZWFuIG9sZCB3b3JrZmxvdyBydW5zCiAgICAgICAgZW52OgogICAgICAgICAgTU9LT0dJVEVBX1RPS0VOOiAke3sgc2VjcmV0cy5NT0tPR0lURUFfVE9LRU4gfX0KICAgICAgICBydW46IHwKICAgICAgICAgIGVjaG8gIj09PSBXb3JrZmxvdyBSdW4gQ2xlYW51cCA9PT0iCiAgICAgICAgICBBUEk9IiR7TU9LT0dJVEVBX1VSTH0vYXBpL3YxL3JlcG9zLyR7eyBnaXRodWIucmVwb3NpdG9yeSB9fSIKICAgICAgICAgIENVVE9GRj0kKGRhdGUgLWQgIjMwIGRheXMgYWdvIiArJVktJW0tJWRUJUg6JU06JVNaIDI+L2Rldi9udWxsIHx8IGRhdGUgLXYtMzBkICslWS0lbS0lZFQlSDolTTolU1opCgogICAgICAgICAgIyBHZXQgb2xkIGNvbXBsZXRlZCBydW5zCiAgICAgICAgICBSVU5TPSQoY3VybCAtc1MgLUggIkF1dGhvcml6YXRpb246IHRva2VuICR7TU9LT0dJVEVBX1RPS0VOfSIgXAogICAgICAgICAgICAiJHtBUEl9L2FjdGlvbnMvcnVucz9zdGF0dXM9Y29tcGxldGVkJmxpbWl0PTUwIiB8IFwKICAgICAgICAgICAganEgLXIgIi53b3JrZmxvd19ydW5zW10gfCBzZWxlY3QoLmNyZWF0ZWRfYXQgPCBcIiR7Q1VUT0ZGfVwiKSB8IC5pZCIgMj4vZGV2L251bGwpCgogICAgICAgICAgREVMRVRFRD0wCiAgICAgICAgICBmb3IgUlVOX0lEIGluICRSVU5TOyBkbwogICAgICAgICAgICBjdXJsIC1zUyAtWCBERUxFVEUgLUggIkF1dGhvcml6YXRpb246IHRva2VuICR7TU9LT0dJVEVBX1RPS0VOfSIgXAogICAgICAgICAgICAgICIke0FQSX0vYWN0aW9ucy9ydW5zLyR7UlVOX0lEfSIgMj4vZGV2L251bGwgfHwgdHJ1ZQogICAgICAgICAgICBERUxFVEVEPSQoKERFTEVURUQgKyAxKSkKICAgICAgICAgIGRvbmUKCiAgICAgICAgICBlY2hvICJEZWxldGVkICR7REVMRVRFRH0gb2xkIHdvcmtmbG93IHJ1bihzKSIK \ No newline at end of file +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: MokoGitea.Workflow +# INGROUP: MokoStandards.Maintenance +# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards +# PATH: /.mokogitea/workflows/cleanup.yml +# VERSION: 01.00.00 +# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs + +name: "Universal: Repository Cleanup" + +on: + schedule: + - cron: '0 3 * * 0' # Weekly on Sunday at 03:00 UTC + workflow_dispatch: + +permissions: + contents: write + +env: + MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + +jobs: + cleanup: + name: Clean Merged Branches + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.MOKOGITEA_TOKEN }} + + - name: Delete merged branches + env: + MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + run: | + echo "=== Merged Branch Cleanup ===" + API="${MOKOGITEA_URL}/api/v1/repos/${{ github.repository }}" + + # List branches via API + BRANCHES=$(curl -sS -H "Authorization: token ${MOKOGITEA_TOKEN}" \ + "${API}/branches?limit=50" | jq -r '.[].name') + + DELETED=0 + for BRANCH in $BRANCHES; do + # Skip protected branches + case "$BRANCH" in + main|master|dev|develop|rc|beta|alpha|release|release/*|production|stable|staging|hotfix/*|version/*) continue ;; + esac + + # Check if branch is merged into main + if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then + echo " Deleting merged branch: ${BRANCH}" + curl -sS -X DELETE -H "Authorization: token ${MOKOGITEA_TOKEN}" \ + "${API}/branches/${BRANCH}" 2>/dev/null || true + DELETED=$((DELETED + 1)) + fi + done + + echo "Deleted ${DELETED} merged branch(es)" + + - name: Clean old workflow runs + env: + MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + run: | + echo "=== Workflow Run Cleanup ===" + API="${MOKOGITEA_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 ${MOKOGITEA_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 ${MOKOGITEA_TOKEN}" \ + "${API}/actions/runs/${RUN_ID}" 2>/dev/null || true + DELETED=$((DELETED + 1)) + done + + echo "Deleted ${DELETED} old workflow run(s)" diff --git a/.mokogitea/workflows/gitleaks.yml b/.mokogitea/workflows/gitleaks.yml index f3b8eef..7312838 100644 --- a/.mokogitea/workflows/gitleaks.yml +++ b/.mokogitea/workflows/gitleaks.yml @@ -1 +1,92 @@ -IyBDb3B5cmlnaHQgKEMpIDIwMjYgTW9rbyBDb25zdWx0aW5nIDxoZWxsb0Btb2tvY29uc3VsdGluZy50ZWNoPgojCiMgU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IEdQTC0zLjAtb3ItbGF0ZXIKIwojIEZJTEUgSU5GT1JNQVRJT04KIyBERUZHUk9VUDogR2l0ZWEuV29ya2Zsb3cKIyBJTkdST1VQOiBNb2tvU3RhbmRhcmRzLlNlY3VyaXR5CiMgUkVQTzogaHR0cHM6Ly9naXQubW9rb2NvbnN1bHRpbmcudGVjaC9tb2tvY29uc3VsdGluZy10ZWNoL01va29TdGFuZGFyZHMtQVBJCiMgUEFUSDogL3RlbXBsYXRlcy93b3JrZmxvd3MvZ2l0bGVha3MueW1sLnRlbXBsYXRlCiMgVkVSU0lPTjogMDEuMDAuMDAKIyBCUklFRjogU2VjcmV0IHNjYW5uaW5nIOKAlCBkZXRlY3QgbGVha2VkIGNyZWRlbnRpYWxzLCBBUEkga2V5cywgYW5kIHRva2VucwojCiMgKz09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PSsKIyB8ICAgICAgICAgICAgICAgICAgICAgICAgU0VDUkVUIFNDQU5OSU5HICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfAojICs9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0rCiMgfCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHwKIyB8ICBTY2FucyBjb21taXRzIGZvciBsZWFrZWQgc2VjcmV0cyB1c2luZyBHaXRsZWFrcy4gICAgICAgICAgICAgICAgICAgICAgfAojIHwgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB8CiMgfCAgLSBQUiBzY2FuOiBvbmx5IG5ldyBjb21taXRzIGluIHRoZSBQUiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHwKIyB8ICAtIFNjaGVkdWxlZDogZnVsbCByZXBvIHNjYW4gd2Vla2x5ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfAojIHwgIC0gQWxlcnRzIHZpYSBudGZ5IG9uIGZpbmRpbmdzICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB8CiMgfCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHwKIyArPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09KwoKbmFtZTogIlVuaXZlcnNhbDogU2VjcmV0IFNjYW5uaW5nIgoKb246CiAgc2NoZWR1bGU6CiAgICAtIGNyb246ICcwIDUgKiAqIDEnICAjIFdlZWtseSBNb25kYXkgMDU6MDAgVVRDCiAgd29ya2Zsb3dfZGlzcGF0Y2g6CgpwZXJtaXNzaW9uczoKICBjb250ZW50czogcmVhZAoKZW52OgogIE5URllfVVJMOiAke3sgdmFycy5OVEZZX1VSTCB8fCAnaHR0cHM6Ly9udGZ5Lm1va29jb25zdWx0aW5nLnRlY2gnIH19CiAgTlRGWV9UT1BJQzogJHt7IHZhcnMuTlRGWV9UT1BJQyB8fCAnZ2l0ZWEtc2VjdXJpdHknIH19Cgpqb2JzOgogIGdpdGxlYWtzOgogICAgbmFtZTogR2l0bGVha3MgU2VjcmV0IFNjYW4KICAgIHJ1bnMtb246IHVidW50dS1sYXRlc3QKCiAgICBzdGVwczoKICAgICAgLSBuYW1lOiBDaGVja291dAogICAgICAgIHVzZXM6IGFjdGlvbnMvY2hlY2tvdXRAdjQKICAgICAgICB3aXRoOgogICAgICAgICAgZmV0Y2gtZGVwdGg6IDAKCiAgICAgIC0gbmFtZTogSW5zdGFsbCBHaXRsZWFrcwogICAgICAgIHJ1bjogfAogICAgICAgICAgR0lUTEVBS1NfVkVSU0lPTj0iOC4yMS4yIgogICAgICAgICAgY3VybCAtc1NMICJodHRwczovL2dpdGh1Yi5jb20vZ2l0bGVha3MvZ2l0bGVha3MvcmVsZWFzZXMvZG93bmxvYWQvdiR7R0lUTEVBS1NfVkVSU0lPTn0vZ2l0bGVha3NfJHtHSVRMRUFLU19WRVJTSU9OfV9saW51eF94NjQudGFyLmd6IiBcCiAgICAgICAgICAgIHwgdGFyIC14eiAtQyAvdXNyL2xvY2FsL2JpbiBnaXRsZWFrcwogICAgICAgICAgZ2l0bGVha3MgdmVyc2lvbgoKICAgICAgLSBuYW1lOiBTY2FuIGZvciBzZWNyZXRzCiAgICAgICAgaWQ6IHNjYW4KICAgICAgICBydW46IHwKICAgICAgICAgIGVjaG8gIiMjIyBTZWNyZXQgU2Nhbm5pbmciID4+ICRHSVRIVUJfU1RFUF9TVU1NQVJZCiAgICAgICAgICBBUkdTPSItLXNvdXJjZSAuIC0tdmVyYm9zZSAtLXJlcG9ydC1mb3JtYXQganNvbiAtLXJlcG9ydC1wYXRoIC90bXAvZ2l0bGVha3MtcmVwb3J0Lmpzb24iCgogICAgICAgICAgaWYgWyAiJHt7IGdpdGh1Yi5ldmVudF9uYW1lIH19IiA9ICJwdWxsX3JlcXVlc3QiIF07IHRoZW4KICAgICAgICAgICAgIyBTY2FuIG9ubHkgUFIgY29tbWl0cwogICAgICAgICAgICBBUkdTPSIkQVJHUyAtLWxvZy1vcHRzPSR7eyBnaXRodWIuZXZlbnQucHVsbF9yZXF1ZXN0LmJhc2Uuc2hhIH19Li4ke3sgZ2l0aHViLmV2ZW50LnB1bGxfcmVxdWVzdC5oZWFkLnNoYSB9fSIKICAgICAgICAgICAgZWNobyAiU2Nhbm5pbmcgUFIgY29tbWl0cyBvbmx5IiA+PiAkR0lUSFVCX1NURVBfU1VNTUFSWQogICAgICAgICAgZWxzZQogICAgICAgICAgICBlY2hvICJGdWxsIHJlcG9zaXRvcnkgc2NhbiIgPj4gJEdJVEhVQl9TVEVQX1NVTU1BUlkKICAgICAgICAgIGZpCgogICAgICAgICAgaWYgZ2l0bGVha3MgZGV0ZWN0ICRBUkdTIDI+JjE7IHRoZW4KICAgICAgICAgICAgZWNobyAicmVzdWx0PWNsZWFuIiA+PiAiJEdJVEhVQl9PVVRQVVQiCiAgICAgICAgICAgIGVjaG8gIioqTm8gc2VjcmV0cyBkZXRlY3RlZC4qKiIgPj4gJEdJVEhVQl9TVEVQX1NVTU1BUlkKICAgICAgICAgIGVsc2UKICAgICAgICAgICAgZWNobyAicmVzdWx0PWZvdW5kIiA+PiAiJEdJVEhVQl9PVVRQVVQiCiAgICAgICAgICAgIEZJTkRJTkdTPSQoanEgbGVuZ3RoIC90bXAvZ2l0bGVha3MtcmVwb3J0Lmpzb24gMj4vZGV2L251bGwgfHwgZWNobyAidW5rbm93biIpCiAgICAgICAgICAgIGVjaG8gIioqJHtGSU5ESU5HU30gcG90ZW50aWFsIHNlY3JldChzKSBkZXRlY3RlZC4qKiIgPj4gJEdJVEhVQl9TVEVQX1NVTU1BUlkKICAgICAgICAgICAgZWNobyAiIiA+PiAkR0lUSFVCX1NURVBfU1VNTUFSWQogICAgICAgICAgICBlY2hvICJSZXZpZXcgdGhlIGZpbmRpbmdzIGFuZCByb3RhdGUgYW55IGV4cG9zZWQgY3JlZGVudGlhbHMgaW1tZWRpYXRlbHkuIiA+PiAkR0lUSFVCX1NURVBfU1VNTUFSWQogICAgICAgICAgICBleGl0IDEKICAgICAgICAgIGZpCgogICAgICAtIG5hbWU6IE5vdGlmeSBvbiBmaW5kaW5ncwogICAgICAgIGlmOiBmYWlsdXJlKCkgJiYgc3RlcHMuc2Nhbi5vdXRwdXRzLnJlc3VsdCA9PSAnZm91bmQnCiAgICAgICAgcnVuOiB8CiAgICAgICAgICBSRVBPPSIke3sgZ2l0aHViLmV2ZW50LnJlcG9zaXRvcnkubmFtZSB9fSIKICAgICAgICAgIGN1cmwgLXNTIFwKICAgICAgICAgICAgLUggIlRpdGxlOiAke1JFUE99IOKAlCBzZWNyZXRzIGRldGVjdGVkIGluIGNvZGUiIFwKICAgICAgICAgICAgLUggIlRhZ3M6IHJvdGF0aW5nX2xpZ2h0LGtleSIgXAogICAgICAgICAgICAtSCAiUHJpb3JpdHk6IHVyZ2VudCIgXAogICAgICAgICAgICAtZCAiR2l0bGVha3MgZm91bmQgcG90ZW50aWFsIHNlY3JldHMuIFJldmlldyBhbmQgcm90YXRlIGNyZWRlbnRpYWxzIGltbWVkaWF0ZWx5LiIgXAogICAgICAgICAgICAiJHtOVEZZX1VSTH0vJHtOVEZZX1RPUElDfSIgfHwgdHJ1ZQo= \ No newline at end of file +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: MokoGitea.Workflow +# INGROUP: MokoStandards.Security +# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API +# PATH: /.mokogitea/workflows/gitleaks.yml +# VERSION: 01.00.00 +# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens +# +# +========================================================================+ +# | SECRET SCANNING | +# +========================================================================+ +# | | +# | Scans commits for leaked secrets using Gitleaks. | +# | | +# | - PR scan: only new commits in the PR | +# | - Scheduled: full repo scan weekly | +# | - Alerts via ntfy on findings | +# | | +# +========================================================================+ + +name: "Universal: Secret Scanning" + +on: + schedule: + - cron: '0 5 * * 1' # Weekly Monday 05:00 UTC + workflow_dispatch: + +permissions: + contents: read + +env: + NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }} + NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }} + +jobs: + gitleaks: + name: Gitleaks Secret Scan + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install Gitleaks + run: | + GITLEAKS_VERSION="8.21.2" + curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \ + | tar -xz -C /usr/local/bin gitleaks + gitleaks version + + - name: Scan for secrets + id: scan + run: | + echo "### Secret Scanning" >> $GITHUB_STEP_SUMMARY + ARGS="--source . --verbose --report-format json --report-path /tmp/gitleaks-report.json" + + if [ "${{ github.event_name }}" = "pull_request" ]; then + # Scan only PR commits + ARGS="$ARGS --log-opts=${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}" + echo "Scanning PR commits only" >> $GITHUB_STEP_SUMMARY + else + echo "Full repository scan" >> $GITHUB_STEP_SUMMARY + fi + + if gitleaks detect $ARGS 2>&1; then + echo "result=clean" >> "$GITHUB_OUTPUT" + echo "**No secrets detected.**" >> $GITHUB_STEP_SUMMARY + else + echo "result=found" >> "$GITHUB_OUTPUT" + FINDINGS=$(jq length /tmp/gitleaks-report.json 2>/dev/null || echo "unknown") + echo "**${FINDINGS} potential secret(s) detected.**" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Review the findings and rotate any exposed credentials immediately." >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + - name: Notify on findings + if: failure() && steps.scan.outputs.result == 'found' + run: | + REPO="${{ github.event.repository.name }}" + curl -sS \ + -H "Title: ${REPO} — secrets detected in code" \ + -H "Tags: rotating_light,key" \ + -H "Priority: urgent" \ + -d "Gitleaks found potential secrets. Review and rotate credentials immediately." \ + "${NTFY_URL}/${NTFY_TOPIC}" || true diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 37b08d7..eb67f8f 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -1 +1,73 @@ -IyBDb3B5cmlnaHQgKEMpIDIwMjYgTW9rbyBDb25zdWx0aW5nIDxoZWxsb0Btb2tvY29uc3VsdGluZy50ZWNoPgojCiMgU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IEdQTC0zLjAtb3ItbGF0ZXIKIwojIEZJTEUgSU5GT1JNQVRJT04KIyBERUZHUk9VUDogR2l0ZWEuV29ya2Zsb3cKIyBJTkdST1VQOiBtb2tvY2xpLkF1dG9tYXRpb24KIyBWRVJTSU9OOiAwMS4wMC4wMAojIEJSSUVGOiBBdXRvLWNyZWF0ZSBmZWF0dXJlIGJyYW5jaCB3aGVuIGFuIGlzc3VlIGlzIG9wZW5lZAoKbmFtZTogIlVuaXZlcnNhbDogSXNzdWUgQnJhbmNoIgoKb246CiAgaXNzdWVzOgogICAgdHlwZXM6IFtvcGVuZWRdCgpwZXJtaXNzaW9uczoKICBjb250ZW50czogd3JpdGUKICBpc3N1ZXM6IHdyaXRlCgplbnY6CiAgTU9LT0dJVEVBX1VSTDogJHt7IHZhcnMuR0lURUFfVVJMIHx8ICdodHRwczovL2dpdC5tb2tvY29uc3VsdGluZy50ZWNoJyB9fQoKam9iczoKICBjcmVhdGUtYnJhbmNoOgogICAgbmFtZTogQ3JlYXRlIGZlYXR1cmUgYnJhbmNoCiAgICBydW5zLW9uOiB1YnVudHUtbGF0ZXN0CiAgICBzdGVwczoKICAgICAgLSBuYW1lOiBDcmVhdGUgYnJhbmNoIGFuZCBjb21tZW50CiAgICAgICAgcnVuOiB8CiAgICAgICAgICBUT0tFTj0iJHt7IHNlY3JldHMuTU9LT0dJVEVBX1RPS0VOIH19IgogICAgICAgICAgQVBJPSIke01PS09HSVRFQV9VUkx9L2FwaS92MS9yZXBvcy8ke3sgZ2l0aHViLnJlcG9zaXRvcnkgfX0iCiAgICAgICAgICBJU1NVRV9OVU09IiR7eyBnaXRodWIuZXZlbnQuaXNzdWUubnVtYmVyIH19IgogICAgICAgICAgSVNTVUVfVElUTEU9IiR7eyBnaXRodWIuZXZlbnQuaXNzdWUudGl0bGUgfX0iCgogICAgICAgICAgIyBCdWlsZCBzbHVnIGZyb20gdGl0bGU6IGxvd2VyY2FzZSwgcmVwbGFjZSBub24tYWxudW0gd2l0aCBkYXNoLCB0cmltCiAgICAgICAgICBTTFVHPSQoZWNobyAiJHtJU1NVRV9USVRMRX0iIHwgdHIgJ1s6dXBwZXI6XScgJ1s6bG93ZXI6XScgfCBzZWQgJ3MvW15hLXowLTldLy0vZycgfCBzZWQgJ3MvLS0qLy0vZycgfCBzZWQgJ3MvXi0vLztzLy0kLy8nIHwgY3V0IC1jMS00MCkKICAgICAgICAgIEJSQU5DSD0iZmVhdHVyZS8ke0lTU1VFX05VTX0tJHtTTFVHfSIKCiAgICAgICAgICAjIENoZWNrIGRldiBicmFuY2ggZXhpc3RzCiAgICAgICAgICBERVZfRVhJU1RTPSQoY3VybCAtc2YgLW8gL2Rldi9udWxsIC13ICcle2h0dHBfY29kZX0nIFwKICAgICAgICAgICAgLUggIkF1dGhvcml6YXRpb246IHRva2VuICR7VE9LRU59IiBcCiAgICAgICAgICAgICIke0FQSX0vYnJhbmNoZXMvZGV2IiAyPi9kZXYvbnVsbCB8fCBlY2hvICIwMDAiKQoKICAgICAgICAgIGlmIFsgIiR7REVWX0VYSVNUU30iICE9ICIyMDAiIF07IHRoZW4KICAgICAgICAgICAgZWNobyAiTm8gZGV2IGJyYW5jaCAtLSBza2lwcGluZyIKICAgICAgICAgICAgZXhpdCAwCiAgICAgICAgICBmaQoKICAgICAgICAgICMgQ3JlYXRlIGJyYW5jaCBmcm9tIGRldgogICAgICAgICAgSFRUUD0kKGN1cmwgLXNmIC1vIC9kZXYvbnVsbCAtdyAnJXtodHRwX2NvZGV9JyAtWCBQT1NUIFwKICAgICAgICAgICAgLUggIkF1dGhvcml6YXRpb246IHRva2VuICR7VE9LRU59IiBcCiAgICAgICAgICAgIC1IICJDb250ZW50LVR5cGU6IGFwcGxpY2F0aW9uL2pzb24iIFwKICAgICAgICAgICAgIiR7QVBJfS9icmFuY2hlcyIgXAogICAgICAgICAgICAtZCAie1wibmV3X2JyYW5jaF9uYW1lXCI6XCIke0JSQU5DSH1cIixcIm9sZF9icmFuY2hfbmFtZVwiOlwiZGV2XCJ9IiAyPi9kZXYvbnVsbCB8fCBlY2hvICIwMDAiKQoKICAgICAgICAgIGlmIFsgIiR7SFRUUH0iID0gIjIwMSIgXTsgdGhlbgogICAgICAgICAgICBlY2hvICJDcmVhdGVkIGJyYW5jaDogJHtCUkFOQ0h9IgoKICAgICAgICAgICAgIyBDb21tZW50IG9uIGlzc3VlIHdpdGggYnJhbmNoIGxpbmsKICAgICAgICAgICAgUkVQT19VUkw9IiR7TU9LT0dJVEVBX1VSTH0vJHt7IGdpdGh1Yi5yZXBvc2l0b3J5IH19IgogICAgICAgICAgICBCT0RZPSJCcmFuY2ggY3JlYXRlZDogW1xgJHtCUkFOQ0h9XGBdKCR7UkVQT19VUkx9L3NyYy9icmFuY2gvJHtCUkFOQ0h9KVxuXG5cYFxgXGBiYXNoXG5naXQgZmV0Y2ggb3JpZ2luXG5naXQgY2hlY2tvdXQgJHtCUkFOQ0h9XG5cYFxgXGAiCgogICAgICAgICAgICBjdXJsIC1zZiAtWCBQT1NUIFwKICAgICAgICAgICAgICAtSCAiQXV0aG9yaXphdGlvbjogdG9rZW4gJHtUT0tFTn0iIFwKICAgICAgICAgICAgICAtSCAiQ29udGVudC1UeXBlOiBhcHBsaWNhdGlvbi9qc29uIiBcCiAgICAgICAgICAgICAgIiR7QVBJfS9pc3N1ZXMvJHtJU1NVRV9OVU19L2NvbW1lbnRzIiBcCiAgICAgICAgICAgICAgLWQgIntcImJvZHlcIjpcIiR7Qk9EWX1cIn0iID4gL2Rldi9udWxsIDI+JjEKCiAgICAgICAgICAgIGVjaG8gIkNvbW1lbnRlZCBvbiBpc3N1ZSAjJHtJU1NVRV9OVU19IgogICAgICAgICAgZWxzZQogICAgICAgICAgICBlY2hvICJGYWlsZWQgdG8gY3JlYXRlIGJyYW5jaCAoSFRUUCAke0hUVFB9KSAtLSBtYXkgYWxyZWFkeSBleGlzdCIKICAgICAgICAgIGZpCg== \ No newline at end of file +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: MokoGitea.Workflow +# INGROUP: mokocli.Automation +# VERSION: 01.00.00 +# BRIEF: Auto-create feature branch when an issue is opened + +name: "Universal: Issue Branch" + +on: + issues: + types: [opened] + +permissions: + contents: write + issues: write + +env: + MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + +jobs: + create-branch: + name: Create feature branch + runs-on: ubuntu-latest + steps: + - name: Create branch and comment + run: | + TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" + API="${MOKOGITEA_URL}/api/v1/repos/${{ github.repository }}" + ISSUE_NUM="${{ github.event.issue.number }}" + ISSUE_TITLE="${{ github.event.issue.title }}" + + # Build slug from title: lowercase, replace non-alnum with dash, trim + SLUG=$(echo "${ISSUE_TITLE}" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//' | cut -c1-40) + BRANCH="feature/${ISSUE_NUM}-${SLUG}" + + # Check dev branch exists + DEV_EXISTS=$(curl -sf -o /dev/null -w '%{http_code}' \ + -H "Authorization: token ${TOKEN}" \ + "${API}/branches/dev" 2>/dev/null || echo "000") + + if [ "${DEV_EXISTS}" != "200" ]; then + echo "No dev branch -- skipping" + exit 0 + fi + + # Create branch from dev + HTTP=$(curl -sf -o /dev/null -w '%{http_code}' -X POST \ + -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/json" \ + "${API}/branches" \ + -d "{\"new_branch_name\":\"${BRANCH}\",\"old_branch_name\":\"dev\"}" 2>/dev/null || echo "000") + + if [ "${HTTP}" = "201" ]; then + echo "Created branch: ${BRANCH}" + + # Comment on issue with branch link + REPO_URL="${MOKOGITEA_URL}/${{ github.repository }}" + BODY="Branch created: [\`${BRANCH}\`](${REPO_URL}/src/branch/${BRANCH})\n\n\`\`\`bash\ngit fetch origin\ngit checkout ${BRANCH}\n\`\`\`" + + curl -sf -X POST \ + -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/json" \ + "${API}/issues/${ISSUE_NUM}/comments" \ + -d "{\"body\":\"${BODY}\"}" > /dev/null 2>&1 + + echo "Commented on issue #${ISSUE_NUM}" + else + echo "Failed to create branch (HTTP ${HTTP}) -- may already exist" + fi diff --git a/.mokogitea/workflows/notify.yml b/.mokogitea/workflows/notify.yml index 05cfcc7..5fead53 100644 --- a/.mokogitea/workflows/notify.yml +++ b/.mokogitea/workflows/notify.yml @@ -1 +1,70 @@ -IyBDb3B5cmlnaHQgKEMpIDIwMjYgTW9rbyBDb25zdWx0aW5nIDxoZWxsb0Btb2tvY29uc3VsdGluZy50ZWNoPgojCiMgU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IEdQTC0zLjAtb3ItbGF0ZXIKIwojIEZJTEUgSU5GT1JNQVRJT04KIyBERUZHUk9VUDogR2l0ZWEuV29ya2Zsb3cKIyBJTkdST1VQOiBNb2tvU3RhbmRhcmRzLk5vdGlmaWNhdGlvbnMKIyBSRVBPOiBodHRwczovL2dpdC5tb2tvY29uc3VsdGluZy50ZWNoL01va29Db25zdWx0aW5nL01va29TdGFuZGFyZHMKIyBQQVRIOiAvLmdpdGVhL3dvcmtmbG93cy9ub3RpZnkueW1sCiMgVkVSU0lPTjogMDEuMDAuMDAKIyBCUklFRjogUHVzaCBub3RpZmljYXRpb25zIHZpYSBudGZ5IG9uIHJlbGVhc2Ugc3VjY2VzcyBvciB3b3JrZmxvdyBmYWlsdXJlCgpuYW1lOiAiVW5pdmVyc2FsOiBOb3RpZmljYXRpb25zIgoKb246CiAgd29ya2Zsb3dfcnVuOgogICAgd29ya2Zsb3dzOgogICAgICAtICJKb29tbGEgQnVpbGQgJiBSZWxlYXNlIgogICAgICAtICJKb29tbGEgRXh0ZW5zaW9uIENJIgogICAgICAtICJEZXBsb3kiCiAgICB0eXBlczoKICAgICAgLSBjb21wbGV0ZWQKCnBlcm1pc3Npb25zOgogIGNvbnRlbnRzOiByZWFkCgplbnY6CiAgTlRGWV9VUkw6ICR7eyB2YXJzLk5URllfVVJMIHx8ICdodHRwczovL250ZnkubW9rb2NvbnN1bHRpbmcudGVjaCcgfX0KICBOVEZZX1RPUElDOiAke3sgdmFycy5OVEZZX1RPUElDIHx8ICdnaXRlYS1yZWxlYXNlcycgfX0KCmpvYnM6CiAgbm90aWZ5OgogICAgbmFtZTogU2VuZCBOb3RpZmljYXRpb24KICAgIHJ1bnMtb246IHVidW50dS1sYXRlc3QKICAgIGlmOiA+LQogICAgICBnaXRodWIuZXZlbnQud29ya2Zsb3dfcnVuLmNvbmNsdXNpb24gPT0gJ3N1Y2Nlc3MnIHx8CiAgICAgIGdpdGh1Yi5ldmVudC53b3JrZmxvd19ydW4uY29uY2x1c2lvbiA9PSAnZmFpbHVyZScKCiAgICBzdGVwczoKICAgICAgLSBuYW1lOiBOb3RpZnkgb24gc3VjY2VzcyAocmVsZWFzZXMgb25seSkKICAgICAgICBpZjogPi0KICAgICAgICAgIGdpdGh1Yi5ldmVudC53b3JrZmxvd19ydW4uY29uY2x1c2lvbiA9PSAnc3VjY2VzcycgJiYKICAgICAgICAgIGNvbnRhaW5zKGdpdGh1Yi5ldmVudC53b3JrZmxvd19ydW4ubmFtZSwgJ1JlbGVhc2UnKQogICAgICAgIHJ1bjogfAogICAgICAgICAgUkVQTz0iJHt7IGdpdGh1Yi5ldmVudC5yZXBvc2l0b3J5Lm5hbWUgfX0iCiAgICAgICAgICBXT1JLRkxPVz0iJHt7IGdpdGh1Yi5ldmVudC53b3JrZmxvd19ydW4ubmFtZSB9fSIKICAgICAgICAgIFVSTD0iJHt7IGdpdGh1Yi5ldmVudC53b3JrZmxvd19ydW4uaHRtbF91cmwgfX0iCgogICAgICAgICAgY3VybCAtc1MgXAogICAgICAgICAgICAtSCAiVGl0bGU6ICR7UkVQT30gcmVsZWFzZWQiIFwKICAgICAgICAgICAgLUggIlRhZ3M6IHdoaXRlX2NoZWNrX21hcmsscGFja2FnZSIgXAogICAgICAgICAgICAtSCAiUHJpb3JpdHk6IGRlZmF1bHQiIFwKICAgICAgICAgICAgLUggIkNsaWNrOiAke1VSTH0iIFwKICAgICAgICAgICAgLWQgIiR7V09SS0ZMT1d9IGNvbXBsZXRlZCBzdWNjZXNzZnVsbHkuIiBcCiAgICAgICAgICAgICIke05URllfVVJMfS8ke05URllfVE9QSUN9IgoKICAgICAgLSBuYW1lOiBOb3RpZnkgb24gZmFpbHVyZQogICAgICAgIGlmOiBnaXRodWIuZXZlbnQud29ya2Zsb3dfcnVuLmNvbmNsdXNpb24gPT0gJ2ZhaWx1cmUnCiAgICAgICAgcnVuOiB8CiAgICAgICAgICBSRVBPPSIke3sgZ2l0aHViLmV2ZW50LnJlcG9zaXRvcnkubmFtZSB9fSIKICAgICAgICAgIFdPUktGTE9XPSIke3sgZ2l0aHViLmV2ZW50LndvcmtmbG93X3J1bi5uYW1lIH19IgogICAgICAgICAgVVJMPSIke3sgZ2l0aHViLmV2ZW50LndvcmtmbG93X3J1bi5odG1sX3VybCB9fSIKCiAgICAgICAgICBjdXJsIC1zUyBcCiAgICAgICAgICAgIC1IICJUaXRsZTogJHtSRVBPfSB3b3JrZmxvdyBmYWlsZWQiIFwKICAgICAgICAgICAgLUggIlRhZ3M6IHgsd2FybmluZyIgXAogICAgICAgICAgICAtSCAiUHJpb3JpdHk6IGhpZ2giIFwKICAgICAgICAgICAgLUggIkNsaWNrOiAke1VSTH0iIFwKICAgICAgICAgICAgLWQgIiR7V09SS0ZMT1d9IGZhaWxlZC4gQ2hlY2sgdGhlIHJ1biBmb3IgZGV0YWlscy4iIFwKICAgICAgICAgICAgIiR7TlRGWV9VUkx9LyR7TlRGWV9UT1BJQ30iCg== \ No newline at end of file +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: MokoGitea.Workflow +# INGROUP: MokoStandards.Notifications +# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards +# PATH: /.mokogitea/workflows/notify.yml +# VERSION: 01.00.00 +# BRIEF: Push notifications via ntfy on release success or workflow failure + +name: "Universal: Notifications" + +on: + workflow_run: + workflows: + - "Universal: Build & Release" + - "Joomla: Extension CI" + - "Generic: Project CI" + types: + - completed + +permissions: + contents: read + +env: + NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }} + NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-releases' }} + +jobs: + notify: + name: Send Notification + runs-on: ubuntu-latest + if: >- + github.event.workflow_run.conclusion == 'success' || + github.event.workflow_run.conclusion == 'failure' + + steps: + - name: Notify on success (releases only) + if: >- + github.event.workflow_run.conclusion == 'success' && + contains(github.event.workflow_run.name, 'Release') + run: | + REPO="${{ github.event.repository.name }}" + WORKFLOW="${{ github.event.workflow_run.name }}" + URL="${{ github.event.workflow_run.html_url }}" + + curl -sS \ + -H "Title: ${REPO} released" \ + -H "Tags: white_check_mark,package" \ + -H "Priority: default" \ + -H "Click: ${URL}" \ + -d "${WORKFLOW} completed successfully." \ + "${NTFY_URL}/${NTFY_TOPIC}" + + - name: Notify on failure + if: github.event.workflow_run.conclusion == 'failure' + run: | + REPO="${{ github.event.repository.name }}" + WORKFLOW="${{ github.event.workflow_run.name }}" + URL="${{ github.event.workflow_run.html_url }}" + + curl -sS \ + -H "Title: ${REPO} workflow failed" \ + -H "Tags: x,warning" \ + -H "Priority: high" \ + -H "Click: ${URL}" \ + -d "${WORKFLOW} failed. Check the run for details." \ + "${NTFY_URL}/${NTFY_TOPIC}" diff --git a/.mokogitea/workflows/pr-check.yml b/.mokogitea/workflows/pr-check.yml index a3749cf..c7c2e8f 100644 --- a/.mokogitea/workflows/pr-check.yml +++ b/.mokogitea/workflows/pr-check.yml @@ -1 +1,602 @@ -IyBDb3B5cmlnaHQgKEMpIDIwMjYgTW9rbyBDb25zdWx0aW5nIDxoZWxsb0Btb2tvY29uc3VsdGluZy50ZWNoPgojCiMgU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IEdQTC0zLjAtb3ItbGF0ZXIKIwojIEZJTEUgSU5GT1JNQVRJT04KIyBERUZHUk9VUDogR2l0ZWEuV29ya2Zsb3cKIyBJTkdST1VQOiBtb2tvLXBsYXRmb3JtLkNJCiMgUkVQTzogaHR0cHM6Ly9naXQubW9rb2NvbnN1bHRpbmcudGVjaC9tb2tvY29uc3VsdGluZy10ZWNoL21va28tcGxhdGZvcm0KIyBQQVRIOiAvdGVtcGxhdGVzL3dvcmtmbG93cy91bml2ZXJzYWwvcHItY2hlY2sueW1sLnRlbXBsYXRlCiMgVkVSU0lPTjogMDkuMjMuMDAKIyBCUklFRjogUFIgZ2F0ZSDigJQgYnJhbmNoIHBvbGljeSArIGNvZGUgdmFsaWRhdGlvbiBiZWZvcmUgbWVyZ2UKCm5hbWU6ICJVbml2ZXJzYWw6IFBSIENoZWNrIgoKb246CiAgcHVsbF9yZXF1ZXN0OgogICAgdHlwZXM6IFtvcGVuZWQsIHN5bmNocm9uaXplLCByZW9wZW5lZCwgZWRpdGVkXQoKcGVybWlzc2lvbnM6CiAgY29udGVudHM6IHJlYWQKICBwdWxsLXJlcXVlc3RzOiB3cml0ZQoKZW52OgogIEZPUkNFX0pBVkFTQ1JJUFRfQUNUSU9OU19UT19OT0RFMjQ6IHRydWUKCmpvYnM6CiAgIyDilIDilIAgQnJhbmNoIFBvbGljeSDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKICBicmFuY2gtcG9saWN5OgogICAgbmFtZTogQnJhbmNoIFBvbGljeQogICAgcnVucy1vbjogdWJ1bnR1LWxhdGVzdAogICAgc3RlcHM6CiAgICAgIC0gbmFtZTogQ2hlY2sgYnJhbmNoIG1lcmdlIHRhcmdldAogICAgICAgIHJ1bjogfAogICAgICAgICAgSEVBRD0iJHt7IGdpdGh1Yi5oZWFkX3JlZiB9fSIKICAgICAgICAgIEJBU0U9IiR7eyBnaXRodWIuYmFzZV9yZWYgfX0iCgogICAgICAgICAgZWNobyAiUFI6ICR7SEVBRH0g4oaSICR7QkFTRX0iCgogICAgICAgICAgQUxMT1dFRD10cnVlCiAgICAgICAgICBSRUFTT049IiIKCiAgICAgICAgICBjYXNlICIkSEVBRCIgaW4KICAgICAgICAgICAgZmVhdHVyZS8qfGZlYXQvKikKICAgICAgICAgICAgICBpZiBbICIkQkFTRSIgIT0gImRldiIgXTsgdGhlbgogICAgICAgICAgICAgICAgQUxMT1dFRD1mYWxzZQogICAgICAgICAgICAgICAgUkVBU09OPSJGZWF0dXJlIGJyYW5jaGVzIG11c3QgdGFyZ2V0ICdkZXYnLCBub3QgJyR7QkFTRX0nIgogICAgICAgICAgICAgIGZpCiAgICAgICAgICAgICAgOzsKICAgICAgICAgICAgZml4Lyp8YnVnZml4LyopCiAgICAgICAgICAgICAgaWYgWyAiJEJBU0UiICE9ICJkZXYiIF07IHRoZW4KICAgICAgICAgICAgICAgIEFMTE9XRUQ9ZmFsc2UKICAgICAgICAgICAgICAgIFJFQVNPTj0iRml4IGJyYW5jaGVzIG11c3QgdGFyZ2V0ICdkZXYnLCBub3QgJyR7QkFTRX0nIgogICAgICAgICAgICAgIGZpCiAgICAgICAgICAgICAgOzsKICAgICAgICAgICAgcGF0Y2gvKikKICAgICAgICAgICAgICBpZiBbICIkQkFTRSIgIT0gImRldiIgXSAmJiBbICIkQkFTRSIgIT0gInJjIiBdOyB0aGVuCiAgICAgICAgICAgICAgICBBTExPV0VEPWZhbHNlCiAgICAgICAgICAgICAgICBSRUFTT049IlBhdGNoIGJyYW5jaGVzIG11c3QgdGFyZ2V0ICdkZXYnIG9yICdyYycsIG5vdCAnJHtCQVNFfSciCiAgICAgICAgICAgICAgZmkKICAgICAgICAgICAgICA7OwogICAgICAgICAgICBob3RmaXgvKikKICAgICAgICAgICAgICBpZiBbICIkQkFTRSIgIT0gImRldiIgXSAmJiBbICIkQkFTRSIgIT0gIm1haW4iIF07IHRoZW4KICAgICAgICAgICAgICAgIEFMTE9XRUQ9ZmFsc2UKICAgICAgICAgICAgICAgIFJFQVNPTj0iSG90Zml4IGJyYW5jaGVzIGNhbiBvbmx5IHRhcmdldCAnZGV2JyBvciAnbWFpbicsIG5vdCAnJHtCQVNFfSciCiAgICAgICAgICAgICAgZmkKICAgICAgICAgICAgICA7OwogICAgICAgICAgICByYykKICAgICAgICAgICAgICBpZiBbICIkQkFTRSIgIT0gIm1haW4iIF07IHRoZW4KICAgICAgICAgICAgICAgIEFMTE9XRUQ9ZmFsc2UKICAgICAgICAgICAgICAgIFJFQVNPTj0iUkMgYnJhbmNoIGNhbiBvbmx5IG1lcmdlIGludG8gJ21haW4nLCBub3QgJyR7QkFTRX0nIgogICAgICAgICAgICAgIGZpCiAgICAgICAgICAgICAgOzsKICAgICAgICAgICAgZGV2KQogICAgICAgICAgICAgIGlmIFsgIiRCQVNFIiAhPSAibWFpbiIgXTsgdGhlbgogICAgICAgICAgICAgICAgQUxMT1dFRD1mYWxzZQogICAgICAgICAgICAgICAgUkVBU09OPSJEZXYgYnJhbmNoIGNhbiBvbmx5IG1lcmdlIGludG8gJ21haW4nLCBub3QgJyR7QkFTRX0nIgogICAgICAgICAgICAgIGZpCiAgICAgICAgICAgICAgOzsKICAgICAgICAgIGVzYWMKCiAgICAgICAgICBpZiBbICIkQUxMT1dFRCIgPSBmYWxzZSBdOyB0aGVuCiAgICAgICAgICAgIGVjaG8gIjo6ZXJyb3I6OiR7UkVBU09OfSIKICAgICAgICAgICAgZWNobyAiIyMgQnJhbmNoIFBvbGljeSBWaW9sYXRpb24iID4+ICRHSVRIVUJfU1RFUF9TVU1NQVJZCiAgICAgICAgICAgIGVjaG8gIiIgPj4gJEdJVEhVQl9TVEVQX1NVTU1BUlkKICAgICAgICAgICAgZWNobyAiJHtSRUFTT059IiA+PiAkR0lUSFVCX1NURVBfU1VNTUFSWQogICAgICAgICAgICBlY2hvICIiID4+ICRHSVRIVUJfU1RFUF9TVU1NQVJZCiAgICAgICAgICAgIGVjaG8gIiMjIyBBbGxvd2VkIG1lcmdlIHBhdGhzOiIgPj4gJEdJVEhVQl9TVEVQX1NVTU1BUlkKICAgICAgICAgICAgZWNobyAiLSBcYGZlYXR1cmUvKlxgIOKGkiBcYGRldlxgIiA+PiAkR0lUSFVCX1NURVBfU1VNTUFSWQogICAgICAgICAgICBlY2hvICItIFxgZml4LypcYCDihpIgXGBkZXZcYCIgPj4gJEdJVEhVQl9TVEVQX1NVTU1BUlkKICAgICAgICAgICAgZWNobyAiLSBcYGhvdGZpeC8qXGAg4oaSIFxgZGV2XGAgb3IgXGBtYWluXGAiID4+ICRHSVRIVUJfU1RFUF9TVU1NQVJZCiAgICAgICAgICAgIGVjaG8gIi0gXGBkZXZcYCDihpIgXGBtYWluXGAiID4+ICRHSVRIVUJfU1RFUF9TVU1NQVJZCiAgICAgICAgICAgIGVjaG8gIi0gXGByYy8qXGAg4oaSIFxgbWFpblxgIiA+PiAkR0lUSFVCX1NURVBfU1VNTUFSWQogICAgICAgICAgICBleGl0IDEKICAgICAgICAgIGZpCgogICAgICAgICAgZWNobyAiQnJhbmNoIHBvbGljeTogT0sgKCR7SEVBRH0g4oaSICR7QkFTRX0pIgogICAgICAgICAgZWNobyAiIyMgQnJhbmNoIFBvbGljeTogUGFzc2VkIiA+PiAkR0lUSFVCX1NURVBfU1VNTUFSWQoKICAjIOKUgOKUgCBTZWNyZXQgU2Nhbm5pbmcg4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSACiAgZ2l0bGVha3M6CiAgICBuYW1lOiBTZWNyZXQgU2NhbgogICAgcnVucy1vbjogdWJ1bnR1LWxhdGVzdAogICAgc3RlcHM6CiAgICAgIC0gbmFtZTogQ2hlY2tvdXQKICAgICAgICB1c2VzOiBhY3Rpb25zL2NoZWNrb3V0QHY0CiAgICAgICAgd2l0aDoKICAgICAgICAgIGZldGNoLWRlcHRoOiAwCgogICAgICAtIG5hbWU6IEluc3RhbGwgR2l0bGVha3MKICAgICAgICBydW46IHwKICAgICAgICAgIEdJVExFQUtTX1ZFUlNJT049IjguMjEuMiIKICAgICAgICAgIGN1cmwgLXNTTCAiaHR0cHM6Ly9naXRodWIuY29tL2dpdGxlYWtzL2dpdGxlYWtzL3JlbGVhc2VzL2Rvd25sb2FkL3Yke0dJVExFQUtTX1ZFUlNJT059L2dpdGxlYWtzXyR7R0lUTEVBS1NfVkVSU0lPTn1fbGludXhfeDY0LnRhci5neiIgXAogICAgICAgICAgICB8IHRhciAteHogLUMgL3Vzci9sb2NhbC9iaW4gZ2l0bGVha3MKCiAgICAgIC0gbmFtZTogU2NhbiBQUiBjb21taXRzIGZvciBzZWNyZXRzCiAgICAgICAgcnVuOiB8CiAgICAgICAgICBpZiBnaXRsZWFrcyBkZXRlY3QgLS1zb3VyY2UgLiAtLXZlcmJvc2UgXAogICAgICAgICAgICAtLWxvZy1vcHRzPSR7eyBnaXRodWIuZXZlbnQucHVsbF9yZXF1ZXN0LmJhc2Uuc2hhIH19Li4ke3sgZ2l0aHViLmV2ZW50LnB1bGxfcmVxdWVzdC5oZWFkLnNoYSB9fSAyPiYxOyB0aGVuCiAgICAgICAgICAgIGVjaG8gIioqTm8gc2VjcmV0cyBkZXRlY3RlZC4qKiIgPj4gJEdJVEhVQl9TVEVQX1NVTU1BUlkKICAgICAgICAgIGVsc2UKICAgICAgICAgICAgZWNobyAiOjplcnJvcjo6UG90ZW50aWFsIHNlY3JldHMgZGV0ZWN0ZWQgaW4gUFIgY29tbWl0cyIKICAgICAgICAgICAgZXhpdCAxCiAgICAgICAgICBmaQoKICAjIOKUgOKUgCBDb2RlIFZhbGlkYXRpb24g4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSACiAgdmFsaWRhdGU6CiAgICBuYW1lOiBWYWxpZGF0ZSBQUgogICAgcnVucy1vbjogdWJ1bnR1LWxhdGVzdAoKICAgIHN0ZXBzOgogICAgICAtIG5hbWU6IENoZWNrb3V0CiAgICAgICAgdXNlczogYWN0aW9ucy9jaGVja291dEB2NAoKICAgICAgLSBuYW1lOiBDaGVjayBmb3IgbWVyZ2UgY29uZmxpY3QgbWFya2VycwogICAgICAgIHJ1bjogfAogICAgICAgICAgQ09ORkxJQ1RTPSQoZ3JlcCAtcm4gJzw8PDw8PDwgXHw+Pj4+Pj4+IFx8Xj09PT09PT0kJyAtLWluY2x1ZGU9JyoucGhwJyAtLWluY2x1ZGU9JyoueG1sJyAtLWluY2x1ZGU9JyouY3NzJyAtLWluY2x1ZGU9JyouanMnIC0taW5jbHVkZT0nKi5qc29uJyAtLWluY2x1ZGU9JyoubWQnIC0taW5jbHVkZT0nKi55bWwnIC0taW5jbHVkZT0nKi55YW1sJyAtLWluY2x1ZGU9JyouaW5pJyAtLWluY2x1ZGU9JyoudHh0JyAuIDI+L2Rldi9udWxsIHwgZ3JlcCAtdiAnLmdpdC8nIHx8IHRydWUpCiAgICAgICAgICBpZiBbIC1uICIkQ09ORkxJQ1RTIiBdOyB0aGVuCiAgICAgICAgICAgIGVjaG8gIjo6ZXJyb3I6Ok1lcmdlIGNvbmZsaWN0IG1hcmtlcnMgZm91bmQgaW4gc291cmNlIGZpbGVzIgogICAgICAgICAgICBlY2hvICIjIyBDb25mbGljdCBNYXJrZXJzIEZvdW5kIiA+PiAkR0lUSFVCX1NURVBfU1VNTUFSWQogICAgICAgICAgICBlY2hvICdgYGAnID4+ICRHSVRIVUJfU1RFUF9TVU1NQVJZCiAgICAgICAgICAgIGVjaG8gIiRDT05GTElDVFMiID4+ICRHSVRIVUJfU1RFUF9TVU1NQVJZCiAgICAgICAgICAgIGVjaG8gJ2BgYCcgPj4gJEdJVEhVQl9TVEVQX1NVTU1BUlkKICAgICAgICAgICAgZXhpdCAxCiAgICAgICAgICBmaQogICAgICAgICAgZWNobyAiTm8gY29uZmxpY3QgbWFya2VycyBmb3VuZCIKCiAgICAgIC0gbmFtZTogRGV0ZWN0IHBsYXRmb3JtCiAgICAgICAgaWQ6IHBsYXRmb3JtCiAgICAgICAgcnVuOiB8CiAgICAgICAgICAjIFJlYWQgcGxhdGZvcm0gZnJvbSBYTUwgbWFuaWZlc3QgKDxwbGF0Zm9ybT4gdGFnKSBvciBwbGFpbiB0ZXh0IGZhbGxiYWNrCiAgICAgICAgICBQTEFURk9STT0kKHNlZCAtbiAncy8uKjxwbGF0Zm9ybT5cKFtePF0qXCk8XC9wbGF0Zm9ybT4uKi9cMS9wJyAubW9rb2dpdGVhL21hbmlmZXN0LnhtbCAyPi9kZXYvbnVsbCB8IGhlYWQgLTEpCiAgICAgICAgICBbIC16ICIkUExBVEZPUk0iIF0gJiYgUExBVEZPUk09JChjYXQgLm1va29naXRlYS9tYW5pZmVzdC54bWwgMj4vZGV2L251bGwgfCB0ciAtZCAnWzpzcGFjZTpdJykKICAgICAgICAgIFsgLXogIiRQTEFURk9STSIgXSAmJiBQTEFURk9STT0iZ2VuZXJpYyIKICAgICAgICAgIGVjaG8gInBsYXRmb3JtPSRQTEFURk9STSIgPj4gIiRHSVRIVUJfT1VUUFVUIgoKICAgICAgLSBuYW1lOiBTZXR1cCBQSFAKICAgICAgICBpZjogc3RlcHMucGxhdGZvcm0ub3V0cHV0cy5wbGF0Zm9ybSA9PSAnam9vbWxhJyB8fCBzdGVwcy5wbGF0Zm9ybS5vdXRwdXRzLnBsYXRmb3JtID09ICdkb2xpYmFycicKICAgICAgICBydW46IHwKICAgICAgICAgIGlmICEgY29tbWFuZCAtdiBwaHAgJj4gL2Rldi9udWxsOyB0aGVuCiAgICAgICAgICAgIHN1ZG8gYXB0LWdldCB1cGRhdGUgLXFxCiAgICAgICAgICAgIHN1ZG8gYXB0LWdldCBpbnN0YWxsIC15IC1xcSBwaHAtY2xpIHBocC1tYnN0cmluZyBwaHAteG1sID4vZGV2L251bGwgMj4mMQogICAgICAgICAgZmkKCiAgICAgIC0gbmFtZTogUEhQIHN5bnRheCBjaGVjawogICAgICAgIGlmOiBzdGVwcy5wbGF0Zm9ybS5vdXRwdXRzLnBsYXRmb3JtID09ICdqb29tbGEnIHx8IHN0ZXBzLnBsYXRmb3JtLm91dHB1dHMucGxhdGZvcm0gPT0gJ2RvbGliYXJyJwogICAgICAgIHJ1bjogfAogICAgICAgICAgRVJST1JTPTAKICAgICAgICAgIHdoaWxlIElGUz0gcmVhZCAtciAtZCAnJyBmaWxlOyBkbwogICAgICAgICAgICBpZiAhIHBocCAtbCAiJGZpbGUiIDI+JjEgfCBncmVwIC1xICJObyBzeW50YXggZXJyb3JzIjsgdGhlbgogICAgICAgICAgICAgIEVSUk9SUz0kKChFUlJPUlMgKyAxKSkKICAgICAgICAgICAgZmkKICAgICAgICAgIGRvbmUgPCA8KGZpbmQgLiAtbmFtZSAiKi5waHAiIC1ub3QgLXBhdGggIi4vLmdpdC8qIiAtbm90IC1wYXRoICIuL3ZlbmRvci8qIiAtcHJpbnQwKQogICAgICAgICAgZWNobyAiUEhQIGxpbnQ6ICR7RVJST1JTfSBlcnJvcihzKSIKICAgICAgICAgIFsgIiRFUlJPUlMiIC1lcSAwIF0gfHwgeyBlY2hvICI6OmVycm9yOjpQSFAgc3ludGF4IGVycm9ycyBmb3VuZCI7IGV4aXQgMTsgfQoKICAgICAgLSBuYW1lOiBKb29tbGEgSkVYRUMgZ3VhcmQgY2hlY2sKICAgICAgICBpZjogc3RlcHMucGxhdGZvcm0ub3V0cHV0cy5wbGF0Zm9ybSA9PSAnam9vbWxhJwogICAgICAgIHJ1bjogfAogICAgICAgICAgRVJST1JTPTAKICAgICAgICAgIHdoaWxlIElGUz0gcmVhZCAtciAtZCAnJyBmaWxlOyBkbwogICAgICAgICAgICAjIFNraXAgdmVuZG9yLCBub2RlX21vZHVsZXMsIGFuZCBpbmRleC5odG1sIHN0dWIgZmlsZXMKICAgICAgICAgICAgY2FzZSAiJGZpbGUiIGluIC4vdmVuZG9yLyp8Li9ub2RlX21vZHVsZXMvKikgY29udGludWUgOzsgZXNhYwogICAgICAgICAgICAjIENoZWNrIGZpcnN0IDEwIGxpbmVzIGZvciBKRVhFQyBvciBKUEFUSCBndWFyZAogICAgICAgICAgICBpZiAhIGhlYWQgLTIwICIkZmlsZSIgfCBncmVwIC1xRSAiZGVmaW5lZFxzKlwoXHMqWydcIl0oX0pFWEVDfEpQQVRIX0JBU0V8XFxcXEpQQVRIX1BMQVRGT1JNKVsnXCJdIjsgdGhlbgogICAgICAgICAgICAgIGVjaG8gIjo6ZXJyb3IgZmlsZT0ke2ZpbGV9OjpNaXNzaW5nIEpFWEVDIGd1YXJkOiAke2ZpbGV9IgogICAgICAgICAgICAgIEVSUk9SUz0kKChFUlJPUlMgKyAxKSkKICAgICAgICAgICAgZmkKICAgICAgICAgIGRvbmUgPCA8KGZpbmQgLiAtbmFtZSAiKi5waHAiIC1wYXRoICIqL3NyYy8qIiAtbm90IC1wYXRoICIuLy5naXQvKiIgLW5vdCAtcGF0aCAiLi92ZW5kb3IvKiIgLXByaW50MCkKICAgICAgICAgIGlmIFsgIiRFUlJPUlMiIC1ndCAwIF07IHRoZW4KICAgICAgICAgICAgZWNobyAiOjplcnJvcjo6JHtFUlJPUlN9IFBIUCBmaWxlKHMpIG1pc3NpbmcgZGVmaW5lZCgnX0pFWEVDJykgb3IgZGllIGd1YXJkIgogICAgICAgICAgICBlY2hvICIjIyBKRVhFQyBHdWFyZCBDaGVjazogRmFpbGVkIiA+PiAkR0lUSFVCX1NURVBfU1VNTUFSWQogICAgICAgICAgICBlY2hvICIke0VSUk9SU30gZmlsZShzKSBpbiBzcmMvIGFyZSBtaXNzaW5nIHRoZSBKb29tbGEgZXhlY3V0aW9uIGd1YXJkLiIgPj4gJEdJVEhVQl9TVEVQX1NVTU1BUlkKICAgICAgICAgICAgZXhpdCAxCiAgICAgICAgICBmaQogICAgICAgICAgZWNobyAiSkVYRUMgZ3VhcmQ6IE9LIgoKICAgICAgLSBuYW1lOiBKb29tbGEgZGlyZWN0b3J5IGxpc3RpbmcgcHJvdGVjdGlvbgogICAgICAgIGlmOiBzdGVwcy5wbGF0Zm9ybS5vdXRwdXRzLnBsYXRmb3JtID09ICdqb29tbGEnCiAgICAgICAgcnVuOiB8CiAgICAgICAgICBNSVNTSU5HPTAKICAgICAgICAgIFNPVVJDRV9ESVI9InNyYyIKICAgICAgICAgIFsgISAtZCAiJFNPVVJDRV9ESVIiIF0gJiYgZXhpdCAwCiAgICAgICAgICB3aGlsZSBJRlM9IHJlYWQgLXIgZGlyOyBkbwogICAgICAgICAgICBpZiBbICEgLWYgIiR7ZGlyfS9pbmRleC5odG1sIiBdOyB0aGVuCiAgICAgICAgICAgICAgZWNobyAiOjp3YXJuaW5nOjpNaXNzaW5nIGluZGV4Lmh0bWwgaW4gJHtkaXJ9IChkaXJlY3RvcnkgbGlzdGluZyBwcm90ZWN0aW9uKSIKICAgICAgICAgICAgICBNSVNTSU5HPSQoKE1JU1NJTkcgKyAxKSkKICAgICAgICAgICAgZmkKICAgICAgICAgIGRvbmUgPCA8KGZpbmQgIiRTT1VSQ0VfRElSIiAtdHlwZSBkIC1ub3QgLXBhdGggIi4vLmdpdC8qIiAtbm90IC1wYXRoICIqL3ZlbmRvci8qIiAtbm90IC1wYXRoICIqL25vZGVfbW9kdWxlcy8qIikKICAgICAgICAgIGlmIFsgIiRNSVNTSU5HIiAtZ3QgMCBdOyB0aGVuCiAgICAgICAgICAgIGVjaG8gIiMjIERpcmVjdG9yeSBQcm90ZWN0aW9uIiA+PiAkR0lUSFVCX1NURVBfU1VNTUFSWQogICAgICAgICAgICBlY2hvICIke01JU1NJTkd9IGRpcmVjdG9yKGllcykgbWlzc2luZyBpbmRleC5odG1sIiA+PiAkR0lUSFVCX1NURVBfU1VNTUFSWQogICAgICAgICAgZmkKICAgICAgICAgIGVjaG8gIkRpcmVjdG9yeSBwcm90ZWN0aW9uOiAke01JU1NJTkd9IG1pc3NpbmcgKGFkdmlzb3J5KSIKCiAgICAgIC0gbmFtZTogSm9vbWxhIHNjcmlwdCBmaWxlIGFuZCBhc3NldCBjaGVja3MKICAgICAgICBpZjogc3RlcHMucGxhdGZvcm0ub3V0cHV0cy5wbGF0Zm9ybSA9PSAnam9vbWxhJwogICAgICAgIHJ1bjogfAogICAgICAgICAgRVJST1JTPTAKICAgICAgICAgIE1BTklGRVNUPSQoZmluZCAuIC1tYXhkZXB0aCAzIC1uYW1lICIqLnhtbCIgISAtcGF0aCAiLi8uZ2l0LyoiIC1leGVjIGdyZXAgLWwgJzxleHRlbnNpb24nIHt9IFw7IDI+L2Rldi9udWxsIHwgaGVhZCAtMSkKICAgICAgICAgIFsgLXogIiRNQU5JRkVTVCIgXSAmJiBleGl0IDAKICAgICAgICAgIE1BTklGRVNUX0RJUj0kKGRpcm5hbWUgIiRNQU5JRkVTVCIpCgogICAgICAgICAgIyBDaGVjayBzY3JpcHRmaWxlIGV4aXN0cyBpZiBkZWNsYXJlZAogICAgICAgICAgU0NSSVBURklMRT0kKHNlZCAtbiAncy8uKjxzY3JpcHRmaWxlPlwoW148XSpcKTxcL3NjcmlwdGZpbGU+LiovXDEvcCcgIiRNQU5JRkVTVCIgMj4vZGV2L251bGwpCiAgICAgICAgICBpZiBbIC1uICIkU0NSSVBURklMRSIgXTsgdGhlbgogICAgICAgICAgICBpZiBbICEgLWYgIiR7TUFOSUZFU1RfRElSfS8ke1NDUklQVEZJTEV9IiBdOyB0aGVuCiAgICAgICAgICAgICAgZWNobyAiOjplcnJvcjo6TWFuaWZlc3QgZGVjbGFyZXMgPHNjcmlwdGZpbGU+JHtTQ1JJUFRGSUxFfTwvc2NyaXB0ZmlsZT4gYnV0IGZpbGUgbm90IGZvdW5kIGF0ICR7TUFOSUZFU1RfRElSfS8ke1NDUklQVEZJTEV9IgogICAgICAgICAgICAgIEVSUk9SUz0kKChFUlJPUlMgKyAxKSkKICAgICAgICAgICAgZWxzZQogICAgICAgICAgICAgIGVjaG8gIlNjcmlwdCBmaWxlOiAke01BTklGRVNUX0RJUn0vJHtTQ1JJUFRGSUxFfSAoT0spIgogICAgICAgICAgICBmaQogICAgICAgICAgZmkKCiAgICAgICAgICAjIFJlcXVpcmUgam9vbWxhLmFzc2V0Lmpzb24gYW5kIHZhbGlkYXRlIGl0CiAgICAgICAgICBBU1NFVF9KU09OPSQoZmluZCAiJE1BTklGRVNUX0RJUiIgLW5hbWUgImpvb21sYS5hc3NldC5qc29uIiAtbm90IC1wYXRoICIuLy5naXQvKiIgMj4vZGV2L251bGwgfCBoZWFkIC0xKQogICAgICAgICAgaWYgWyAteiAiJEFTU0VUX0pTT04iIF07IHRoZW4KICAgICAgICAgICAgZWNobyAiOjplcnJvcjo6am9vbWxhLmFzc2V0Lmpzb24gbm90IGZvdW5kIOKAlCBKb29tbGEgYXNzZXQgc3lzdGVtIGlzIHJlcXVpcmVkIgogICAgICAgICAgICBFUlJPUlM9JCgoRVJST1JTICsgMSkpCiAgICAgICAgICBlbHNlCiAgICAgICAgICAgIGlmIGNvbW1hbmQgLXYgcGhwICY+IC9kZXYvbnVsbDsgdGhlbgogICAgICAgICAgICAgIHBocCAtciAianNvbl9kZWNvZGUoZmlsZV9nZXRfY29udGVudHMoJyRBU1NFVF9KU09OJykpOyBpZihqc29uX2xhc3RfZXJyb3IoKSE9PUpTT05fRVJST1JfTk9ORSl7ZWNobyBqc29uX2xhc3RfZXJyb3JfbXNnKCk7ZXhpdCgxKTt9IiAyPiYxIHx8IHsKICAgICAgICAgICAgICAgIGVjaG8gIjo6ZXJyb3I6Ompvb21sYS5hc3NldC5qc29uIGlzIG5vdCB2YWxpZCBKU09OIgogICAgICAgICAgICAgICAgRVJST1JTPSQoKEVSUk9SUyArIDEpKQogICAgICAgICAgICAgIH0KICAgICAgICAgICAgZmkKICAgICAgICAgICAgZWNobyAiam9vbWxhLmFzc2V0Lmpzb246IHZhbGlkIgogICAgICAgICAgZmkKCiAgICAgICAgICAjIFZhbGlkYXRlIGFsbCBYTUwgZmlsZXMgaW4gc3JjLyBhcmUgd2VsbC1mb3JtZWQKICAgICAgICAgIFhNTF9FUlJPUlM9MAogICAgICAgICAgaWYgY29tbWFuZCAtdiBwaHAgJj4gL2Rldi9udWxsOyB0aGVuCiAgICAgICAgICAgIHdoaWxlIElGUz0gcmVhZCAtciAtZCAnJyB4bWxmaWxlOyBkbwogICAgICAgICAgICAgIGlmICEgcGhwIC1yICJsaWJ4bWxfdXNlX2ludGVybmFsX2Vycm9ycyh0cnVlKTsgXCR4ID0gc2ltcGxleG1sX2xvYWRfZmlsZSgnJHhtbGZpbGUnKTsgaWYoIVwkeCl7Zm9yZWFjaChsaWJ4bWxfZ2V0X2Vycm9ycygpIGFzIFwkZSkgZWNobyB0cmltKFwkZS0+bWVzc2FnZSkgLiAnIGluICR4bWxmaWxlJzsgZXhpdCgxKTt9IiAyPiYxOyB0aGVuCiAgICAgICAgICAgICAgICBYTUxfRVJST1JTPSQoKFhNTF9FUlJPUlMgKyAxKSkKICAgICAgICAgICAgICBmaQogICAgICAgICAgICBkb25lIDwgPChmaW5kICIkTUFOSUZFU1RfRElSIiAtbmFtZSAiKi54bWwiIC1ub3QgLXBhdGggIi4vLmdpdC8qIiAtcHJpbnQwKQogICAgICAgICAgZmkKICAgICAgICAgIGlmIFsgIiRYTUxfRVJST1JTIiAtZ3QgMCBdOyB0aGVuCiAgICAgICAgICAgIGVjaG8gIjo6ZXJyb3I6OiR7WE1MX0VSUk9SU30gWE1MIGZpbGUocykgYXJlIG1hbGZvcm1lZCIKICAgICAgICAgICAgRVJST1JTPSQoKEVSUk9SUyArIDEpKQogICAgICAgICAgZWxzZQogICAgICAgICAgICBlY2hvICJYTUwgd2VsbC1mb3JtZWRuZXNzOiBPSyIKICAgICAgICAgIGZpCgogICAgICAgICAgWyAiJEVSUk9SUyIgLWd0IDAgXSAmJiBleGl0IDEKICAgICAgICAgIGVjaG8gIkpvb21sYSBhc3NldCBjaGVja3M6IE9LIgoKICAgICAgLSBuYW1lOiBWYWxpZGF0ZSBwbGF0Zm9ybSBtYW5pZmVzdAogICAgICAgIHJ1bjogfAogICAgICAgICAgUExBVEZPUk09IiR7eyBzdGVwcy5wbGF0Zm9ybS5vdXRwdXRzLnBsYXRmb3JtIH19IgogICAgICAgICAgY2FzZSAiJFBMQVRGT1JNIiBpbgogICAgICAgICAgICBqb29tbGEpCiAgICAgICAgICAgICAgTUFOSUZFU1Q9JChmaW5kIC4gLW1heGRlcHRoIDMgLW5hbWUgIioueG1sIiAhIC1wYXRoICIuLy5naXQvKiIgLWV4ZWMgZ3JlcCAtbCAnPGV4dGVuc2lvbicge30gXDsgMj4vZGV2L251bGwgfCBoZWFkIC0xKQogICAgICAgICAgICAgIGlmIFsgLXogIiRNQU5JRkVTVCIgXTsgdGhlbgogICAgICAgICAgICAgICAgZWNobyAiOjp3YXJuaW5nOjpObyBKb29tbGEgbWFuaWZlc3QgZm91bmQgKFdhYVMgc2l0ZSkiCiAgICAgICAgICAgICAgICBleGl0IDAKICAgICAgICAgICAgICBmaQogICAgICAgICAgICAgIGVjaG8gIk1hbmlmZXN0OiAke01BTklGRVNUfSIKICAgICAgICAgICAgICBpZiBjb21tYW5kIC12IHBocCAmPiAvZGV2L251bGw7IHRoZW4KICAgICAgICAgICAgICAgIHBocCAtciAibGlieG1sX3VzZV9pbnRlcm5hbF9lcnJvcnModHJ1ZSk7IFwkeCA9IHNpbXBsZXhtbF9sb2FkX2ZpbGUoJyRNQU5JRkVTVCcpOyBpZighXCR4KXtmb3JlYWNoKGxpYnhtbF9nZXRfZXJyb3JzKCkgYXMgXCRlKSBlY2hvIFwkZS0+bWVzc2FnZTsgZXhpdCgxKTt9IiB8fCB7IGVjaG8gIjo6ZXJyb3I6Ok1hbmlmZXN0IFhNTCBpcyBtYWxmb3JtZWQiOyBleGl0IDE7IH0KICAgICAgICAgICAgICBmaQogICAgICAgICAgICAgIGZvciBFTEVNRU5UIGluIG5hbWUgdmVyc2lvbiBkZXNjcmlwdGlvbjsgZG8KICAgICAgICAgICAgICAgIGdyZXAgLXEgIjwke0VMRU1FTlR9PiIgIiRNQU5JRkVTVCIgfHwgeyBlY2hvICI6OmVycm9yOjpNaXNzaW5nIDwke0VMRU1FTlR9PiBpbiBtYW5pZmVzdCI7IGV4aXQgMTsgfQogICAgICAgICAgICAgIGRvbmUKICAgICAgICAgICAgICAjIEJsb2NrIGxlZ2FjeSByYXcvYnJhbmNoIHVwZGF0ZSBzZXJ2ZXIgVVJMcyBvbiBNb2tvR2l0ZWEKICAgICAgICAgICAgICBSQVdfVVJMUz0kKGdyZXAgLW4gJ3Jhdy9icmFuY2gnICIkTUFOSUZFU1QiIHwgZ3JlcCAtaSAnbW9rb2NvbnN1bHRpbmdcfG1va29naXRlYVx8Z2l0XC5tb2tvY29uc3VsdGluZ1wudGVjaCcgfHwgdHJ1ZSkKICAgICAgICAgICAgICBpZiBbIC1uICIkUkFXX1VSTFMiIF07IHRoZW4KICAgICAgICAgICAgICAgIGVjaG8gIjo6ZXJyb3I6Ok1hbmlmZXN0IGNvbnRhaW5zIGxlZ2FjeSByYXcvYnJhbmNoIHVwZGF0ZSBzZXJ2ZXIgVVJMIG9uIE1va29HaXRlYS4gVXNlIHRoZSBHaXRlYSBQYWdlcyBVUkwgaW5zdGVhZCAoZS5nLiAve1JFUE99L3VwZGF0ZXMueG1sIG5vdCAve1JFUE99L3Jhdy9icmFuY2gvbWFpbi91cGRhdGVzLnhtbCkiCiAgICAgICAgICAgICAgICBlY2hvICIkUkFXX1VSTFMiCiAgICAgICAgICAgICAgICBleGl0IDEKICAgICAgICAgICAgICBmaQogICAgICAgICAgICAgIGVjaG8gIkpvb21sYSBtYW5pZmVzdCB2YWxpZCIKICAgICAgICAgICAgICA7OwogICAgICAgICAgICBkb2xpYmFycikKICAgICAgICAgICAgICBNT0RfRklMRT0kKGZpbmQgLiAtbWF4ZGVwdGggNCAtbmFtZSAibW9kKi5jbGFzcy5waHAiICEgLXBhdGggIi4vLmdpdC8qIiAtZXhlYyBncmVwIC1sICdleHRlbmRzIERvbGliYXJyTW9kdWxlcycge30gXDsgMj4vZGV2L251bGwgfCBoZWFkIC0xKQogICAgICAgICAgICAgIGlmIFsgLXogIiRNT0RfRklMRSIgXTsgdGhlbgogICAgICAgICAgICAgICAgZWNobyAiOjplcnJvcjo6Tm8gbW9kKi5jbGFzcy5waHAgZm91bmQiCiAgICAgICAgICAgICAgICBleGl0IDEKICAgICAgICAgICAgICBmaQogICAgICAgICAgICAgIGVjaG8gIkRvbGliYXJyIG1vZHVsZTogJHtNT0RfRklMRX0iCiAgICAgICAgICAgICAgOzsKICAgICAgICAgICAgKikKICAgICAgICAgICAgICBlY2hvICJHZW5lcmljIHBsYXRmb3JtIOKAlCBubyBtYW5pZmVzdCB2YWxpZGF0aW9uIgogICAgICAgICAgICAgIDs7CiAgICAgICAgICBlc2FjCgogICAgICAtIG5hbWU6IENoZWNrIHVwZGF0ZSBzdHJlYW0gZm9ybWF0CiAgICAgICAgcnVuOiB8CiAgICAgICAgICBQTEFURk9STT0iJHt7IHN0ZXBzLnBsYXRmb3JtLm91dHB1dHMucGxhdGZvcm0gfX0iCiAgICAgICAgICBjYXNlICIkUExBVEZPUk0iIGluCiAgICAgICAgICAgIGpvb21sYSkKICAgICAgICAgICAgICBpZiBbIC1mICJ1cGRhdGVzLnhtbCIgXTsgdGhlbgogICAgICAgICAgICAgICAgaWYgY29tbWFuZCAtdiBwaHAgJj4gL2Rldi9udWxsOyB0aGVuCiAgICAgICAgICAgICAgICAgIHBocCAtciAibGlieG1sX3VzZV9pbnRlcm5hbF9lcnJvcnModHJ1ZSk7IFwkeCA9IHNpbXBsZXhtbF9sb2FkX2ZpbGUoJ3VwZGF0ZXMueG1sJyk7IGlmKCFcJHgpe2ZvcmVhY2gobGlieG1sX2dldF9lcnJvcnMoKSBhcyBcJGUpIGVjaG8gXCRlLT5tZXNzYWdlOyBleGl0KDEpO30iIHx8IHsgZWNobyAiOjplcnJvcjo6dXBkYXRlcy54bWwgaXMgbWFsZm9ybWVkIjsgZXhpdCAxOyB9CiAgICAgICAgICAgICAgICBmaQogICAgICAgICAgICAgICAgZWNobyAidXBkYXRlcy54bWwgdmFsaWQiCiAgICAgICAgICAgICAgZmkKICAgICAgICAgICAgICA7OwogICAgICAgICAgICBkb2xpYmFycikKICAgICAgICAgICAgICBbIC1mICJ1cGRhdGUudHh0IiBdICYmIGVjaG8gInVwZGF0ZS50eHQgcHJlc2VudCIgfHwgZWNobyAiOjp3YXJuaW5nOjpObyB1cGRhdGUudHh0IgogICAgICAgICAgICAgIDs7CiAgICAgICAgICBlc2FjCgogICAgICAtIG5hbWU6IFZhbGlkYXRlIEpvb21sYSBsYW5ndWFnZSBmaWxlcwogICAgICAgIGlmOiBzdGVwcy5wbGF0Zm9ybS5vdXRwdXRzLnBsYXRmb3JtID09ICdqb29tbGEnCiAgICAgICAgcnVuOiB8CiAgICAgICAgICBFUlJPUlM9MAogICAgICAgICAgV0FSTklOR1M9MAoKICAgICAgICAgICMgUmVxdWlyZSBib3RoIGVuLUdCIGFuZCBlbi1VUyBsYW5ndWFnZSBkaXJlY3RvcmllcwogICAgICAgICAgTEFOR19ST09UPSQoZmluZCAuIC1wYXRoICIqL2xhbmd1YWdlIiAtdHlwZSBkIC1ub3QgLXBhdGggIi4vLmdpdC8qIiAyPi9kZXYvbnVsbCB8IGhlYWQgLTEpCiAgICAgICAgICBpZiBbIC16ICIkTEFOR19ST09UIiBdOyB0aGVuCiAgICAgICAgICAgIGVjaG8gIk5vIGxhbmd1YWdlLyBkaXJlY3RvcnkgZm91bmQg4oCUIHNraXBwaW5nIgogICAgICAgICAgICBleGl0IDAKICAgICAgICAgIGZpCgogICAgICAgICAgaWYgWyAhIC1kICIkTEFOR19ST09UL2VuLUdCIiBdOyB0aGVuCiAgICAgICAgICAgIGVjaG8gIjo6ZXJyb3I6Ok1pc3NpbmcgZW4tR0IgbGFuZ3VhZ2UgZGlyZWN0b3J5ICgke0xBTkdfUk9PVH0vZW4tR0IpIgogICAgICAgICAgICBFUlJPUlM9JCgoRVJST1JTICsgMSkpCiAgICAgICAgICBmaQogICAgICAgICAgaWYgWyAhIC1kICIkTEFOR19ST09UL2VuLVVTIiBdOyB0aGVuCiAgICAgICAgICAgIGVjaG8gIjo6ZXJyb3I6Ok1pc3NpbmcgZW4tVVMgbGFuZ3VhZ2UgZGlyZWN0b3J5ICgke0xBTkdfUk9PVH0vZW4tVVMpIgogICAgICAgICAgICBFUlJPUlM9JCgoRVJST1JTICsgMSkpCiAgICAgICAgICBmaQoKICAgICAgICAgICMgQ2hlY2sgdGhhdCBlbi1HQiBhbmQgZW4tVVMgaGF2ZSBtYXRjaGluZyAuaW5pIGZpbGVzCiAgICAgICAgICBpZiBbIC1kICIkTEFOR19ST09UL2VuLUdCIiBdICYmIFsgLWQgIiRMQU5HX1JPT1QvZW4tVVMiIF07IHRoZW4KICAgICAgICAgICAgZm9yIEdCX0lOSSBpbiAiJExBTkdfUk9PVC9lbi1HQiIvKi5pbmk7IGRvCiAgICAgICAgICAgICAgWyAhIC1mICIkR0JfSU5JIiBdICYmIGNvbnRpbnVlCiAgICAgICAgICAgICAgVVNfSU5JPSIkTEFOR19ST09UL2VuLVVTLyQoYmFzZW5hbWUgIiRHQl9JTkkiKSIKICAgICAgICAgICAgICBpZiBbICEgLWYgIiRVU19JTkkiIF07IHRoZW4KICAgICAgICAgICAgICAgIGVjaG8gIjo6ZXJyb3I6OiQoYmFzZW5hbWUgIiRHQl9JTkkiKSBleGlzdHMgaW4gZW4tR0IgYnV0IG1pc3NpbmcgZnJvbSBlbi1VUyIKICAgICAgICAgICAgICAgIEVSUk9SUz0kKChFUlJPUlMgKyAxKSkKICAgICAgICAgICAgICBmaQogICAgICAgICAgICBkb25lCiAgICAgICAgICAgIGZvciBVU19JTkkgaW4gIiRMQU5HX1JPT1QvZW4tVVMiLyouaW5pOyBkbwogICAgICAgICAgICAgIFsgISAtZiAiJFVTX0lOSSIgXSAmJiBjb250aW51ZQogICAgICAgICAgICAgIEdCX0lOST0iJExBTkdfUk9PVC9lbi1HQi8kKGJhc2VuYW1lICIkVVNfSU5JIikiCiAgICAgICAgICAgICAgaWYgWyAhIC1mICIkR0JfSU5JIiBdOyB0aGVuCiAgICAgICAgICAgICAgICBlY2hvICI6OmVycm9yOjokKGJhc2VuYW1lICIkVVNfSU5JIikgZXhpc3RzIGluIGVuLVVTIGJ1dCBtaXNzaW5nIGZyb20gZW4tR0IiCiAgICAgICAgICAgICAgICBFUlJPUlM9JCgoRVJST1JTICsgMSkpCiAgICAgICAgICAgICAgZmkKICAgICAgICAgICAgZG9uZQogICAgICAgICAgZmkKCiAgICAgICAgICAjIEZpbmQgYWxsIC5pbmkgbGFuZ3VhZ2UgZmlsZXMKICAgICAgICAgIElOSV9GSUxFUz0kKGZpbmQgLiAtcGF0aCAiKi9sYW5ndWFnZS8qLyouaW5pIiAtbm90IC1wYXRoICIuLy5naXQvKiIgMj4vZGV2L251bGwpCiAgICAgICAgICBpZiBbIC16ICIkSU5JX0ZJTEVTIiBdOyB0aGVuCiAgICAgICAgICAgIGVjaG8gIk5vIC5pbmkgbGFuZ3VhZ2UgZmlsZXMgZm91bmQiCiAgICAgICAgICAgIFsgIiRFUlJPUlMiIC1ndCAwIF0gJiYgZXhpdCAxCiAgICAgICAgICAgIGV4aXQgMAogICAgICAgICAgZmkKCiAgICAgICAgICBlY2hvICJGb3VuZCAkKGVjaG8gIiRJTklfRklMRVMiIHwgd2MgLWwpIGxhbmd1YWdlIGZpbGUocykiCgogICAgICAgICAgZm9yIEZJTEUgaW4gJElOSV9GSUxFUzsgZG8KICAgICAgICAgICAgRk5BTUU9JChiYXNlbmFtZSAiJEZJTEUiKQogICAgICAgICAgICBMSU5FTlVNPTAKICAgICAgICAgICAgU0VFTl9LRVlTPSIiCgogICAgICAgICAgICB3aGlsZSBJRlM9IHJlYWQgLXIgbGluZSB8fCBbIC1uICIkbGluZSIgXTsgZG8KICAgICAgICAgICAgICBMSU5FTlVNPSQoKExJTkVOVU0gKyAxKSkKCiAgICAgICAgICAgICAgIyBTa2lwIGVtcHR5IGxpbmVzIGFuZCBjb21tZW50cwogICAgICAgICAgICAgIFsgLXogIiRsaW5lIiBdICYmIGNvbnRpbnVlCiAgICAgICAgICAgICAgZWNobyAiJGxpbmUiIHwgZ3JlcCAtcUUgJ15ccyo7JyAmJiBjb250aW51ZQogICAgICAgICAgICAgIGVjaG8gIiRsaW5lIiB8IGdyZXAgLXFFICdeXHMqJCcgJiYgY29udGludWUKCiAgICAgICAgICAgICAgIyBNdXN0IG1hdGNoIEtFWT0iVkFMVUUiIGZvcm1hdAogICAgICAgICAgICAgIGlmICEgZWNobyAiJGxpbmUiIHwgZ3JlcCAtcUUgJ15bQS1aX11bQS1aMC05X10qPSIuKiIkJzsgdGhlbgogICAgICAgICAgICAgICAgZWNobyAiOjplcnJvciBmaWxlPSR7RklMRX0sbGluZT0ke0xJTkVOVU19OjpNYWxmb3JtZWQgbGluZTogJHtsaW5lfSIKICAgICAgICAgICAgICAgIEVSUk9SUz0kKChFUlJPUlMgKyAxKSkKICAgICAgICAgICAgICAgIGNvbnRpbnVlCiAgICAgICAgICAgICAgZmkKCiAgICAgICAgICAgICAgIyBFeHRyYWN0IGtleSBhbmQgY2hlY2sgZm9yIGR1cGxpY2F0ZXMKICAgICAgICAgICAgICBLRVk9JChlY2hvICIkbGluZSIgfCBzZWQgJ3MvPS4qLy8nKQogICAgICAgICAgICAgIGlmIGVjaG8gIiRTRUVOX0tFWVMiIHwgZ3JlcCAtcXggIiRLRVkiOyB0aGVuCiAgICAgICAgICAgICAgICBlY2hvICI6OmVycm9yIGZpbGU9JHtGSUxFfSxsaW5lPSR7TElORU5VTX06OkR1cGxpY2F0ZSBrZXk6ICR7S0VZfSIKICAgICAgICAgICAgICAgIEVSUk9SUz0kKChFUlJPUlMgKyAxKSkKICAgICAgICAgICAgICBmaQogICAgICAgICAgICAgIFNFRU5fS0VZUz0iJHtTRUVOX0tFWVN9CiAgICAgICAgICAke0tFWX0iCiAgICAgICAgICAgIGRvbmUgPCAiJEZJTEUiCgogICAgICAgICAgICBlY2hvICIgICR7RklMRX06IGNoZWNrZWQgJHtMSU5FTlVNfSBsaW5lcyIKICAgICAgICAgIGRvbmUKCiAgICAgICAgICAjIENyb3NzLWNoZWNrIGVuLUdCIHZzIGVuLVVTIGtleSBjb25zaXN0ZW5jeQogICAgICAgICAgR0JfRElSPSQoZmluZCAuIC1wYXRoICIqL2xhbmd1YWdlL2VuLUdCIiAtdHlwZSBkIC1ub3QgLXBhdGggIi4vLmdpdC8qIiAyPi9kZXYvbnVsbCB8IGhlYWQgLTEpCiAgICAgICAgICBVU19ESVI9JChmaW5kIC4gLXBhdGggIiovbGFuZ3VhZ2UvZW4tVVMiIC10eXBlIGQgLW5vdCAtcGF0aCAiLi8uZ2l0LyoiIDI+L2Rldi9udWxsIHwgaGVhZCAtMSkKCiAgICAgICAgICBpZiBbIC1uICIkR0JfRElSIiBdICYmIFsgLW4gIiRVU19ESVIiIF07IHRoZW4KICAgICAgICAgICAgZm9yIEdCX0ZJTEUgaW4gIiRHQl9ESVIiLyouaW5pOyBkbwogICAgICAgICAgICAgIFsgISAtZiAiJEdCX0ZJTEUiIF0gJiYgY29udGludWUKICAgICAgICAgICAgICBGTkFNRT0kKGJhc2VuYW1lICIkR0JfRklMRSIpCiAgICAgICAgICAgICAgVVNfRklMRT0iJFVTX0RJUi8kRk5BTUUiCiAgICAgICAgICAgICAgWyAhIC1mICIkVVNfRklMRSIgXSAmJiBjb250aW51ZQoKICAgICAgICAgICAgICBHQl9LRVlTPSQoZ3JlcCAtb1AgJ15bQS1aX11bQS1aMC05X10qKD89PSknICIkR0JfRklMRSIgMj4vZGV2L251bGwgfCBzb3J0KQogICAgICAgICAgICAgIFVTX0tFWVM9JChncmVwIC1vUCAnXltBLVpfXVtBLVowLTlfXSooPz09KScgIiRVU19GSUxFIiAyPi9kZXYvbnVsbCB8IHNvcnQpCgogICAgICAgICAgICAgICMgS2V5cyBpbiBlbi1HQiBidXQgbm90IGVuLVVTCiAgICAgICAgICAgICAgTUlTU0lOR19VUz0kKGNvbW0gLTIzIDwoZWNobyAiJEdCX0tFWVMiKSA8KGVjaG8gIiRVU19LRVlTIikpCiAgICAgICAgICAgICAgaWYgWyAtbiAiJE1JU1NJTkdfVVMiIF07IHRoZW4KICAgICAgICAgICAgICAgIGVjaG8gIjo6d2FybmluZzo6S2V5cyBpbiBlbi1HQi8kRk5BTUUgYnV0IG1pc3NpbmcgZnJvbSBlbi1VUy8kRk5BTUU6IgogICAgICAgICAgICAgICAgZWNobyAiJE1JU1NJTkdfVVMiIHwgd2hpbGUgcmVhZCAtciBrOyBkbyBlY2hvICIgIC0gJGsiOyBkb25lCiAgICAgICAgICAgICAgICBXQVJOSU5HUz0kKChXQVJOSU5HUyArIDEpKQogICAgICAgICAgICAgIGZpCgogICAgICAgICAgICAgICMgS2V5cyBpbiBlbi1VUyBidXQgbm90IGVuLUdCCiAgICAgICAgICAgICAgTUlTU0lOR19HQj0kKGNvbW0gLTEzIDwoZWNobyAiJEdCX0tFWVMiKSA8KGVjaG8gIiRVU19LRVlTIikpCiAgICAgICAgICAgICAgaWYgWyAtbiAiJE1JU1NJTkdfR0IiIF07IHRoZW4KICAgICAgICAgICAgICAgIGVjaG8gIjo6d2FybmluZzo6S2V5cyBpbiBlbi1VUy8kRk5BTUUgYnV0IG1pc3NpbmcgZnJvbSBlbi1HQi8kRk5BTUU6IgogICAgICAgICAgICAgICAgZWNobyAiJE1JU1NJTkdfR0IiIHwgd2hpbGUgcmVhZCAtciBrOyBkbyBlY2hvICIgIC0gJGsiOyBkb25lCiAgICAgICAgICAgICAgICBXQVJOSU5HUz0kKChXQVJOSU5HUyArIDEpKQogICAgICAgICAgICAgIGZpCiAgICAgICAgICAgIGRvbmUKICAgICAgICAgIGZpCgogICAgICAgICAgewogICAgICAgICAgICBlY2hvICIjIyMgTGFuZ3VhZ2UgRmlsZSBWYWxpZGF0aW9uIgogICAgICAgICAgICBlY2hvICJ8IE1ldHJpYyB8IENvdW50IHwiCiAgICAgICAgICAgIGVjaG8gInwtLS18LS0tfCIKICAgICAgICAgICAgZWNobyAifCBGaWxlcyBjaGVja2VkIHwgJChlY2hvICIkSU5JX0ZJTEVTIiB8IHdjIC1sKSB8IgogICAgICAgICAgICBlY2hvICJ8IEVycm9ycyB8ICR7RVJST1JTfSB8IgogICAgICAgICAgICBlY2hvICJ8IFdhcm5pbmdzIHwgJHtXQVJOSU5HU30gfCIKICAgICAgICAgIH0gPj4gJEdJVEhVQl9TVEVQX1NVTU1BUlkKCiAgICAgICAgICBpZiBbICIkRVJST1JTIiAtZ3QgMCBdOyB0aGVuCiAgICAgICAgICAgIGVjaG8gIjo6ZXJyb3I6Okxhbmd1YWdlIHZhbGlkYXRpb24gZmFpbGVkIHdpdGggJHtFUlJPUlN9IGVycm9yKHMpIgogICAgICAgICAgICBleGl0IDEKICAgICAgICAgIGZpCiAgICAgICAgICBlY2hvICJMYW5ndWFnZSBmaWxlczogT0sgKCR7V0FSTklOR1N9IHdhcm5pbmcocykpIgoKICAgICAgLSBuYW1lOiBDaGVjayBjaGFuZ2Vsb2cgaGFzIHVucmVsZWFzZWQgZW50cnkKICAgICAgICBydW46IHwKICAgICAgICAgIGlmIFsgISAtZiAiQ0hBTkdFTE9HLm1kIiBdOyB0aGVuCiAgICAgICAgICAgIGVjaG8gIjo6d2FybmluZzo6Tm8gQ0hBTkdFTE9HLm1kIGZvdW5kIgogICAgICAgICAgICBleGl0IDAKICAgICAgICAgIGZpCiAgICAgICAgICAjIENoZWNrIGZvciBjb250ZW50IHVuZGVyIFtVbnJlbGVhc2VkXSBzZWN0aW9uCiAgICAgICAgICBpZiAhIGdyZXAgLXEgIiMjIFxbVW5yZWxlYXNlZFxdIiBDSEFOR0VMT0cubWQ7IHRoZW4KICAgICAgICAgICAgZWNobyAiOjplcnJvcjo6Q0hBTkdFTE9HLm1kIG1pc3NpbmcgW1VucmVsZWFzZWRdIHNlY3Rpb24iCiAgICAgICAgICAgIGV4aXQgMQogICAgICAgICAgZmkKICAgICAgICAgICMgQ2hlY2sgdGhlcmUncyBhdCBsZWFzdCBvbmUgZW50cnkgKEFkZGVkL0NoYW5nZWQvRml4ZWQvUmVtb3ZlZCkgdW5kZXIgVW5yZWxlYXNlZAogICAgICAgICAgVU5SRUxFQVNFRF9DT05URU5UPSQoc2VkIC1uICcvIyMgXFtVbnJlbGVhc2VkXF0vLC8jIyBcWy9wJyBDSEFOR0VMT0cubWQgfCBncmVwIC1jRSAnXlxzKi1ccycgfHwgdHJ1ZSkKICAgICAgICAgIGlmIFsgIiRVTlJFTEVBU0VEX0NPTlRFTlQiIC1lcSAwIF07IHRoZW4KICAgICAgICAgICAgZWNobyAiOjplcnJvcjo6Q0hBTkdFTE9HLm1kIFtVbnJlbGVhc2VkXSBzZWN0aW9uIGhhcyBubyBlbnRyaWVzLiBBZGQgYSBjaGFuZ2Vsb2cgZW50cnkgZGVzY3JpYmluZyB5b3VyIGNoYW5nZXMuIgogICAgICAgICAgICBlY2hvICIjIyBDaGFuZ2Vsb2cgQ2hlY2s6IEZhaWxlZCIgPj4gJEdJVEhVQl9TVEVQX1NVTU1BUlkKICAgICAgICAgICAgZWNobyAiVGhlIFxgW1VucmVsZWFzZWRdXGAgc2VjdGlvbiBpbiBDSEFOR0VMT0cubWQgaGFzIG5vIGVudHJpZXMuIiA+PiAkR0lUSFVCX1NURVBfU1VNTUFSWQogICAgICAgICAgICBlY2hvICJBZGQgYSBsaW5lIGxpa2UgXGAtIERlc2NyaXB0aW9uIG9mIHlvdXIgY2hhbmdlXGAgdW5kZXIgYSBoZWFkaW5nIChcYCMjIyBBZGRlZFxgLCBcYCMjIyBDaGFuZ2VkXGAsIFxgIyMjIEZpeGVkXGAsIGV0Yy4pIiA+PiAkR0lUSFVCX1NURVBfU1VNTUFSWQogICAgICAgICAgICBleGl0IDEKICAgICAgICAgIGZpCiAgICAgICAgICBlY2hvICJDaGFuZ2Vsb2c6ICR7VU5SRUxFQVNFRF9DT05URU5UfSBlbnRyeS9lbnRyaWVzIGluIFtVbnJlbGVhc2VkXSIKCiAgICAgIC0gbmFtZTogVmVyaWZ5IHBhY2thZ2Ugc291cmNlCiAgICAgICAgcnVuOiB8CiAgICAgICAgICBTT1VSQ0VfRElSPSJzcmMiCiAgICAgICAgICBbICEgLWQgIiRTT1VSQ0VfRElSIiBdICYmIFNPVVJDRV9ESVI9Imh0ZG9jcyIKICAgICAgICAgIGlmIFsgISAtZCAiJFNPVVJDRV9ESVIiIF07IHRoZW4KICAgICAgICAgICAgZWNobyAiOjp3YXJuaW5nOjpObyBzcmMvIG9yIGh0ZG9jcy8gZGlyZWN0b3J5IgogICAgICAgICAgICBleGl0IDAKICAgICAgICAgIGZpCiAgICAgICAgICBGSUxFX0NPVU5UPSQoZmluZCAiJFNPVVJDRV9ESVIiIC10eXBlIGYgfCB3YyAtbCkKICAgICAgICAgIGVjaG8gIlNvdXJjZTogJHtGSUxFX0NPVU5UfSBmaWxlcyIKICAgICAgICAgIFsgIiRGSUxFX0NPVU5UIiAtZ3QgMCBdIHx8IHsgZWNobyAiOjplcnJvcjo6U291cmNlIGRpcmVjdG9yeSBpcyBlbXB0eSI7IGV4aXQgMTsgfQoKICAjIOKUgOKUgCBQcmUtUmVsZWFzZSBSQyBCdWlsZCDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKICBwcmUtcmVsZWFzZToKICAgIG5hbWU6IEJ1aWxkIFJDIFBhY2thZ2UKICAgIHJ1bnMtb246IHVidW50dS1sYXRlc3QKICAgIG5lZWRzOiBbYnJhbmNoLXBvbGljeSwgdmFsaWRhdGVdCgogICAgc3RlcHM6CiAgICAgIC0gbmFtZTogVHJpZ2dlciBSQyBwcmUtcmVsZWFzZQogICAgICAgIGVudjoKICAgICAgICAgIE1PS09HSVRFQV9UT0tFTjogJHt7IHNlY3JldHMuTU9LT0dJVEVBX1RPS0VOIH19CiAgICAgICAgICBSRVBPOiAke3sgZ2l0aHViLnJlcG9zaXRvcnkgfX0KICAgICAgICAgIEJSQU5DSDogJHt7IGdpdGh1Yi5oZWFkX3JlZiB9fQogICAgICAgICAgTU9LT0dJVEVBX1VSTDogJHt7IHZhcnMuR0lURUFfVVJMIHx8ICdodHRwczovL2dpdC5tb2tvY29uc3VsdGluZy50ZWNoJyB9fQogICAgICAgIHJ1bjogfAogICAgICAgICAgY3VybCAtcyAtWCBQT1NUICAgICAgICAgICAgICIke01PS09HSVRFQV9VUkx9L2FwaS92MS9yZXBvcy8ke1JFUE99L2FjdGlvbnMvd29ya2Zsb3dzL3ByZS1yZWxlYXNlLnltbC9kaXNwYXRjaGVzIiAgICAgICAgICAgICAtSCAiQXV0aG9yaXphdGlvbjogdG9rZW4gJHtNT0tPR0lURUFfVE9LRU59IiAgICAgICAgICAgICAtSCAiQ29udGVudC1UeXBlOiBhcHBsaWNhdGlvbi9qc29uIiAgICAgICAgICAgICAtZCAie1wicmVmXCI6XCIke0JSQU5DSH1cIixcImlucHV0c1wiOntcInN0YWJpbGl0eVwiOlwicmVsZWFzZS1jYW5kaWRhdGVcIn19IgogICAgICAgICAgZWNobyAiIyMjIFByZS1SZWxlYXNlIiA+PiAkR0lUSFVCX1NURVBfU1VNTUFSWQogICAgICAgICAgZWNobyAiVHJpZ2dlcmVkIFJDIGJ1aWxkIG9uIGJyYW5jaCBcYCR7QlJBTkNIfVxgIiA+PiAkR0lUSFVCX1NURVBfU1VNTUFSWQoKICAjIOKUgOKUgCBJc3N1ZSBSZXBvcnRlciDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKICByZXBvcnQtaXNzdWVzOgogICAgbmFtZTogUmVwb3J0IElzc3VlcwogICAgbmVlZHM6IFticmFuY2gtcG9saWN5LCB2YWxpZGF0ZV0KICAgIGlmOiA+LQogICAgICBhbHdheXMoKSAmJgogICAgICBuZWVkcy52YWxpZGF0ZS5yZXN1bHQgPT0gJ2ZhaWx1cmUnCiAgICB1c2VzOiAuLy5tb2tvZ2l0ZWEvd29ya2Zsb3dzL2NpLWlzc3VlLXJlcG9ydGVyLnltbAogICAgd2l0aDoKICAgICAgZ2F0ZTogIlBSIFZhbGlkYXRpb24iCiAgICAgIHdvcmtmbG93OiAiUFIgQ2hlY2siCiAgICAgIHNldmVyaXR5OiBlcnJvcgogICAgICBkZXRhaWxzOiAiUFIgdmFsaWRhdGlvbiBmYWlsZWQgKHN5bnRheCwgbWFuaWZlc3QsIGNoYW5nZWxvZywgb3Igc291cmNlIGNoZWNrcykuIFNlZSB0aGUgQ0kgcnVuIGZvciB0aGUgc3BlY2lmaWMgY2hlY2sgdGhhdCBmYWlsZWQuIgogICAgc2VjcmV0czogaW5oZXJpdAo= \ No newline at end of file +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: MokoGitea.Workflow +# INGROUP: mokocli.CI +# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic +# PATH: /.mokogitea/workflows/pr-check.yml +# 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" ] && [ "$BASE" != "main" ]; then + ALLOWED=false + REASON="Fix branches must target 'dev' or 'main', not '${BASE}'" + fi + ;; + patch/*) + if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ] && [ "$BASE" != "main" ]; then + ALLOWED=false + REASON="Patch branches must target 'dev', 'rc', or 'main', 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\` or \`main\`" >> $GITHUB_STEP_SUMMARY + echo "- \`patch/*\` → \`dev\`, \`rc\`, or \`main\`" >> $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 + + # ── Docs Update Gate (main PRs) ───────────────────────────────────────── + require-docs: + name: Require Docs Update + runs-on: ubuntu-latest + # Enforce only on PRs merging into main: README.md + CHANGELOG.md must both be updated. + if: ${{ github.base_ref == 'main' }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Require README.md and CHANGELOG.md in the PR diff + run: | + BASE="${{ github.event.pull_request.base.sha }}" + HEAD="${{ github.event.pull_request.head.sha }}" + CHANGED="$(git diff --name-only "$BASE" "$HEAD" 2>/dev/null || true)" + if [ -z "$CHANGED" ]; then + git fetch -q origin "${{ github.base_ref }}" 2>/dev/null || true + CHANGED="$(git diff --name-only "origin/${{ github.base_ref }}...HEAD" 2>/dev/null || true)" + fi + echo "Changed files in PR:" + echo "$CHANGED" + MISSING="" + echo "$CHANGED" | grep -qxE 'README\.md' || MISSING="README.md" + echo "$CHANGED" | grep -qxE 'CHANGELOG\.md' || MISSING="${MISSING:+$MISSING, }CHANGELOG.md" + if [ -n "$MISSING" ]; then + echo "::error::PRs into main must update: ${MISSING}" + { + echo "## Docs Update Required" + echo "" + echo "PRs merging into \`main\` must update both **README.md** and **CHANGELOG.md**." + echo "" + echo "Not updated in this PR: **${MISSING}**" + } >> "$GITHUB_STEP_SUMMARY" + exit 1 + fi + echo "Docs update present (README.md + CHANGELOG.md)" + echo "## Docs Update: Passed" >> "$GITHUB_STEP_SUMMARY" + + # ── Wiki Update Reminder (main PRs, non-blocking) ─────────────────────── + wiki-reminder: + name: Wiki Update Reminder + runs-on: ubuntu-latest + if: ${{ github.base_ref == 'main' }} + steps: + - name: Remind to update the wiki + env: + TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + SERVER: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + REPO: ${{ github.repository }} + PR: ${{ github.event.pull_request.number }} + run: | + set -uo pipefail + { + echo "## Wiki Update Reminder" + echo "" + echo "Docs are **wiki-first** at MokoConsulting. If this change affects behavior, usage, configuration, or standards, update the repo wiki:" + echo "" + echo "- ${SERVER}/${REPO}/wiki" + echo "" + echo "_Non-blocking reminder._" + } >> "$GITHUB_STEP_SUMMARY" + # Post a single PR comment (idempotent via hidden marker); best-effort, never fails. + API="${SERVER}/api/v1/repos/${REPO}/issues/${PR}/comments" + if [ -n "${TOKEN:-}" ] && [ -n "${PR:-}" ]; then + existing="$(curl -sf -H "Authorization: token ${TOKEN}" "$API" 2>/dev/null | grep -c 'wiki-reminder' || true)" + if [ "${existing:-0}" -eq 0 ]; then + curl -sf -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" -X POST "$API" \ + -d '{"body":"\n\n**Wiki reminder:** docs are wiki-first -- if this PR changes behavior, usage, config, or standards, please update the repo wiki before/after merge. _(non-blocking)_"}' >/dev/null 2>&1 || true + fi + fi + echo "Wiki reminder emitted (non-blocking)." + + # ── Secret Scanning ────────────────────────────────────────────────── + gitleaks: + name: Secret Scan + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install Gitleaks + run: | + GITLEAKS_VERSION="8.21.2" + curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \ + | tar -xz -C /usr/local/bin gitleaks + + - name: Scan PR commits for secrets + run: | + if gitleaks detect --source . --verbose \ + --log-opts=${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }} 2>&1; then + echo "**No secrets detected.**" >> $GITHUB_STEP_SUMMARY + else + echo "::error::Potential secrets detected in PR commits" + exit 1 + fi + + # ── Code Validation ──────────────────────────────────────────────────── + validate: + name: Validate PR + runs-on: ubuntu-latest + # Skip on template repos (Template-*) — no real manifest/source/changelog to validate. + if: ${{ !startsWith(github.event.repository.name, 'Template-') }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Check for merge conflict markers + run: | + CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --exclude-dir='.git' --exclude-dir='.mokogitea' --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: | + # Platform comes from the MokoGitea metadata API (public GET); manifest.xml is no longer used. + API="${GITHUB_SERVER_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${GITHUB_REPOSITORY}/metadata" + PLATFORM="$(curl -sf "$API" 2>/dev/null | python3 -c "import sys, json; print(json.load(sys.stdin).get('platform') or '')" 2>/dev/null || true)" + [ -z "$PLATFORM" ] && PLATFORM="generic" + echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT" + echo "Detected platform: $PLATFORM" + + - 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 '/dev/null | head -1) + [ -z "$MANIFEST" ] && exit 0 + MANIFEST_DIR=$(dirname "$MANIFEST") + + # Check scriptfile exists if declared + SCRIPTFILE=$(sed -n 's/.*\([^<]*\)<\/scriptfile>.*/\1/p' "$MANIFEST" 2>/dev/null) + if [ -n "$SCRIPTFILE" ]; then + if [ ! -f "${MANIFEST_DIR}/${SCRIPTFILE}" ]; then + echo "::error::Manifest declares ${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 '/dev/null | head -1) + if [ -z "$MANIFEST" ]; then + echo "::warning::No Joomla manifest found (MokoSuite 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 MokoGitea 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] + # Run only when both gates succeeded; always() forces evaluation so a skipped + # validate (e.g. template repos) skips this job cleanly instead of hanging. + if: ${{ always() && needs.branch-policy.result == 'success' && needs.validate.result == 'success' }} + + steps: + - name: Trigger RC pre-release + env: + MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + REPO: ${{ github.repository }} + BRANCH: ${{ github.head_ref }} + MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + run: | + curl -s -X POST "${MOKOGITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${MOKOGITEA_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 + needs: [branch-policy, validate] + if: >- + always() && + needs.validate.result == 'failure' + uses: ./.mokogitea/workflows/ci-issue-reporter.yml + with: + 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." + secrets: inherit diff --git a/.mokogitea/workflows/pr-metadata-check.yml b/.mokogitea/workflows/pr-metadata-check.yml index 4c96414..b4c9cbd 100644 --- a/.mokogitea/workflows/pr-metadata-check.yml +++ b/.mokogitea/workflows/pr-metadata-check.yml @@ -1 +1,71 @@ -IyBDb3B5cmlnaHQgKEMpIDIwMjYgTW9rbyBDb25zdWx0aW5nIDxoZWxsb0Btb2tvY29uc3VsdGluZy50ZWNoPgojCiMgU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IEdQTC0zLjAtb3ItbGF0ZXIKIwojIEZJTEUgSU5GT1JNQVRJT04KIyBERUZHUk9VUDogR2l0ZWEuV29ya2Zsb3cKIyBJTkdST1VQOiBtb2tvY2xpLlZhbGlkYXRpb24KIyBSRVBPOiBodHRwczovL2dpdC5tb2tvY29uc3VsdGluZy50ZWNoL01va29Db25zdWx0aW5nL21va29jbGkKIyBQQVRIOiAvdGVtcGxhdGVzL3dvcmtmbG93cy9qb29tbGEvcHItbWV0YWRhdGEtY2hlY2sueW1sLnRlbXBsYXRlCiMgVkVSU0lPTjogMDEuMDAuMDAKIyBCUklFRjogVmFsaWRhdGUgTW9rb0dpdGVhIG1ldGFkYXRhIG1hdGNoZXMgSm9vbWxhIGV4dGVuc2lvbiBtYW5pZmVzdCBvbiBQUnMKCm5hbWU6ICJKb29tbGE6IE1ldGFkYXRhIFZhbGlkYXRpb24iCgpvbjoKICBwdWxsX3JlcXVlc3Q6CiAgICB0eXBlczogW29wZW5lZCwgc3luY2hyb25pemUsIHJlb3BlbmVkLCBjb252ZXJ0ZWRfdG9fZHJhZnQsIHJlYWR5X2Zvcl9yZXZpZXddCgpwZXJtaXNzaW9uczoKICBjb250ZW50czogcmVhZAoKZW52OgogIE1PS09HSVRFQV9VUkw6ICR7eyB2YXJzLkdJVEVBX1VSTCB8fCAnaHR0cHM6Ly9naXQubW9rb2NvbnN1bHRpbmcudGVjaCcgfX0KICBHSVRFQV9PUkc6ICR7eyB2YXJzLkdJVEVBX09SRyB8fCBnaXRodWIucmVwb3NpdG9yeV9vd25lciB9fQogIEdJVEVBX1JFUE86ICR7eyB2YXJzLkdJVEVBX1JFUE8gfHwgZ2l0aHViLmV2ZW50LnJlcG9zaXRvcnkubmFtZSB9fQoKam9iczoKICB2YWxpZGF0ZS1tZXRhZGF0YToKICAgIG5hbWU6ICJWYWxpZGF0ZSBKb29tbGEgTWV0YWRhdGEiCiAgICBydW5zLW9uOiB1YnVudHUtbGF0ZXN0CgogICAgc3RlcHM6CiAgICAgIC0gbmFtZTogQ2hlY2tvdXQKICAgICAgICB1c2VzOiBhY3Rpb25zL2NoZWNrb3V0QHY0CgogICAgICAtIG5hbWU6IFNldHVwIG1va29jbGkgdG9vbHMKICAgICAgICBlbnY6CiAgICAgICAgICBNT0tPX0NMT05FX1RPS0VOOiAke3sgc2VjcmV0cy5NT0tPR0lURUFfVE9LRU4gfX0KICAgICAgICAgIE1PS09fQ0xPTkVfSE9TVDogZ2l0Lm1va29jb25zdWx0aW5nLnRlY2gvTW9rb0NvbnN1bHRpbmcKICAgICAgICBydW46IHwKICAgICAgICAgIGlmIFsgLWYgL29wdC9tb2tvY2xpL2NsaS9qb29tbGFfbWV0YWRhdGFfdmFsaWRhdGUucGhwIF0gJiYgWyAtZiAvb3B0L21va29jbGkvdmVuZG9yL2F1dG9sb2FkLnBocCBdOyB0aGVuCiAgICAgICAgICAgIGVjaG8gVXNpbmcgcHJlLWluc3RhbGxlZCAvb3B0L21va29jbGkKICAgICAgICAgICAgZWNobyBNT0tPX0NMST0vb3B0L21va29jbGkvY2xpID4+ICRHSVRIVUJfRU5WCiAgICAgICAgICBlbHNlCiAgICAgICAgICAgIGVjaG8gRmFsbGluZyBiYWNrIHRvIGZyZXNoIGNsb25lCiAgICAgICAgICAgIGlmICEgY29tbWFuZCAtdiBjb21wb3NlciA+IC9kZXYvbnVsbCAyPiYxOyB0aGVuCiAgICAgICAgICAgICAgc3VkbyBhcHQtZ2V0IHVwZGF0ZSAtcXEgJiYgc3VkbyBhcHQtZ2V0IGluc3RhbGwgLXkgLXFxIHBocC1jbGkgcGhwLW1ic3RyaW5nIHBocC14bWwgcGhwLXppcCBwaHAtY3VybCBjb21wb3NlciA+IC9kZXYvbnVsbCAyPiYxCiAgICAgICAgICAgIGZpCiAgICAgICAgICAgIHJtIC1yZiAvdG1wL21va29jbGkKICAgICAgICAgICAgQ0xPTkVfVVJMPWh0dHBzOi8veC1hY2Nlc3MtdG9rZW46JHtNT0tPX0NMT05FX1RPS0VOfUAke01PS09fQ0xPTkVfSE9TVH0vbW9rb2NsaS5naXQKICAgICAgICAgICAgZ2l0IGNsb25lIC0tZGVwdGggMSAtLWJyYW5jaCBtYWluIC0tcXVpZXQgJENMT05FX1VSTCAvdG1wL21va29jbGkKICAgICAgICAgICAgY2QgL3RtcC9tb2tvY2xpICYmIGNvbXBvc2VyIGluc3RhbGwgLS1uby1kZXYgLS1uby1pbnRlcmFjdGlvbiAtLXF1aWV0CiAgICAgICAgICAgIGVjaG8gTU9LT19DTEk9L3RtcC9tb2tvY2xpL2NsaSA+PiAkR0lUSFVCX0VOVgogICAgICAgICAgZmkKCiAgICAgIC0gbmFtZTogVmFsaWRhdGUgbWV0YWRhdGEgYWdhaW5zdCBKb29tbGEgbWFuaWZlc3QKICAgICAgICBlbnY6CiAgICAgICAgICBNT0tPR0lURUFfVE9LRU46ICR7eyBzZWNyZXRzLk1PS09HSVRFQV9UT0tFTiB9fQogICAgICAgIHJ1bjogfAogICAgICAgICAgcGhwICR7TU9LT19DTEl9L2pvb21sYV9tZXRhZGF0YV92YWxpZGF0ZS5waHAgXAogICAgICAgICAgICAtLXBhdGggLiBcCiAgICAgICAgICAgIC0tdG9rZW4gIiR7TU9LT0dJVEVBX1RPS0VOfSIgXAogICAgICAgICAgICAtLW9yZyAiJHtHSVRFQV9PUkd9IiBcCiAgICAgICAgICAgIC0tcmVwbyAiJHtHSVRFQV9SRVBPfSIgXAogICAgICAgICAgICAtLWFwaS1iYXNlICIke01PS09HSVRFQV9VUkx9L2FwaS92MSIgXAogICAgICAgICAgICAtLWNpCgogICAgICAgICAgaWYgWyAkPyAtbmUgMCBdOyB0aGVuCiAgICAgICAgICAgIGVjaG8gIjo6ZXJyb3I6Okpvb21sYSBtZXRhZGF0YSBtaXNtYXRjaCDigJQgdXBkYXRlIGRlbGl2ZXJ5IHdpbGwgZmFpbC4gUnVuICdwaHAgY2xpL2pvb21sYV9tZXRhZGF0YV92YWxpZGF0ZS5waHAnIGxvY2FsbHkgdG8gc2VlIGRldGFpbHMuIgogICAgICAgICAgICBleGl0IDEKICAgICAgICAgIGZpCg== \ No newline at end of file +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: mokocli.Validation +# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli +# PATH: /templates/workflows/joomla/pr-metadata-check.yml.template +# VERSION: 01.00.00 +# BRIEF: Validate MokoGitea metadata matches Joomla extension manifest on PRs + +name: "Joomla: Metadata Validation" + +on: + pull_request: + types: [opened, synchronize, reopened, converted_to_draft, ready_for_review] + +permissions: + contents: read + +env: + MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + +jobs: + validate-metadata: + name: "Validate Joomla Metadata" + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup mokocli tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + run: | + if [ -f /opt/mokocli/cli/joomla_metadata_validate.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then + echo Using pre-installed /opt/mokocli + echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV + else + echo Falling back to fresh clone + if ! command -v composer > /dev/null 2>&1; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1 + fi + rm -rf /tmp/mokocli + CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git + git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli + cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet + echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV + fi + + - name: Validate metadata against Joomla manifest + env: + MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + run: | + php ${MOKO_CLI}/joomla_metadata_validate.php \ + --path . \ + --token "${MOKOGITEA_TOKEN}" \ + --org "${GITEA_ORG}" \ + --repo "${GITEA_REPO}" \ + --api-base "${MOKOGITEA_URL}/api/v1" \ + --ci + + if [ $? -ne 0 ]; then + echo "::error::Joomla metadata mismatch — update delivery will fail. Run 'php cli/joomla_metadata_validate.php' locally to see details." + exit 1 + fi diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml index c764066..b212772 100644 --- a/.mokogitea/workflows/pre-release.yml +++ b/.mokogitea/workflows/pre-release.yml @@ -1 +1,283 @@ -IyBDb3B5cmlnaHQgKEMpIDIwMjYgTW9rbyBDb25zdWx0aW5nIDxoZWxsb0Btb2tvY29uc3VsdGluZy50ZWNoPgojCiMgU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IEdQTC0zLjAtb3ItbGF0ZXIKIwojIEZJTEUgSU5GT1JNQVRJT04KIyBERUZHUk9VUDogR2l0ZWEuV29ya2Zsb3cKIyBJTkdST1VQOiBtb2tvY2xpLlJlbGVhc2UKIyBSRVBPOiBodHRwczovL2dpdC5tb2tvY29uc3VsdGluZy50ZWNoL01va29Db25zdWx0aW5nL21va29jbGkKIyBQQVRIOiAvdGVtcGxhdGVzL3dvcmtmbG93cy91bml2ZXJzYWwvcHJlLXJlbGVhc2UueW1sLnRlbXBsYXRlCiMgVkVSU0lPTjogMDUuMDIuMDAKIyBCUklFRjogQXV0byBwcmUtcmVsZWFzZSBvbiBwdXNoIHRvIGRldi9hbHBoYS9iZXRhL3JjIGJyYW5jaGVzCgpuYW1lOiAiVW5pdmVyc2FsOiBQcmUtUmVsZWFzZSIKCm9uOgogIHB1c2g6CiAgICBicmFuY2hlczoKICAgICAgLSBkZXYKICAgICAgLSAnZml4LyoqJwogICAgICAtICdwYXRjaC8qKicKICAgICAgLSAnaG90Zml4LyoqJwogICAgICAtICdidWdmaXgvKionCiAgICAgIC0gJ2Nob3JlLyoqJwogICAgICAtIGFscGhhCiAgICAgIC0gYmV0YQogICAgICAtIHJjCiAgd29ya2Zsb3dfZGlzcGF0Y2g6CiAgICBpbnB1dHM6CiAgICAgIHN0YWJpbGl0eToKICAgICAgICBkZXNjcmlwdGlvbjogJ1ByZS1yZWxlYXNlIGNoYW5uZWwnCiAgICAgICAgcmVxdWlyZWQ6IHRydWUKICAgICAgICB0eXBlOiBjaG9pY2UKICAgICAgICBvcHRpb25zOgogICAgICAgICAgLSBkZXZlbG9wbWVudAogICAgICAgICAgLSBhbHBoYQogICAgICAgICAgLSBiZXRhCiAgICAgICAgICAtIHJlbGVhc2UtY2FuZGlkYXRlCgpwZXJtaXNzaW9uczoKICBjb250ZW50czogd3JpdGUKCmVudjoKICBHSVRFQV9VUkw6ICR7eyB2YXJzLkdJVEVBX1VSTCB8fCAnaHR0cHM6Ly9naXQubW9rb2NvbnN1bHRpbmcudGVjaCcgfX0KICBHSVRFQV9PUkc6ICR7eyB2YXJzLkdJVEVBX09SRyB8fCBnaXRodWIucmVwb3NpdG9yeV9vd25lciB9fQogIEdJVEVBX1JFUE86ICR7eyB2YXJzLkdJVEVBX1JFUE8gfHwgZ2l0aHViLmV2ZW50LnJlcG9zaXRvcnkubmFtZSB9fQoKam9iczoKICBidWlsZDoKICAgIG5hbWU6ICJCdWlsZCBQcmUtUmVsZWFzZSAoJHt7IGlucHV0cy5zdGFiaWxpdHkgfHwgZ2l0aHViLnJlZl9uYW1lIH19KSIKICAgIHJ1bnMtb246IHJlbGVhc2UKICAgIGlmOiA+LQogICAgICBnaXRodWIuZXZlbnRfbmFtZSA9PSAnd29ya2Zsb3dfZGlzcGF0Y2gnIHx8CiAgICAgIGdpdGh1Yi5ldmVudF9uYW1lID09ICdwdXNoJwoKICAgIHN0ZXBzOgogICAgICAtIG5hbWU6IENoZWNrb3V0CiAgICAgICAgdXNlczogYWN0aW9ucy9jaGVja291dEB2NAogICAgICAgIHdpdGg6CiAgICAgICAgICBmZXRjaC1kZXB0aDogMAogICAgICAgICAgdG9rZW46ICR7eyBzZWNyZXRzLk1PS09HSVRFQV9UT0tFTiB9fQogICAgICAgICAgcmVmOiAke3sgZ2l0aHViLnJlZl9uYW1lIH19CiAgICAgICAgICBzdWJtb2R1bGVzOiByZWN1cnNpdmUKCiAgICAgIC0gbmFtZTogVXBkYXRlIHN1Ym1vZHVsZXMgdG8gbWFpbgogICAgICAgIHJ1bjogfAogICAgICAgICAgZ2l0IHN1Ym1vZHVsZSBmb3JlYWNoIC0tcXVpZXQgJ2dpdCBjaGVja291dCBtYWluICYmIGdpdCBwdWxsIC0tcXVpZXQgb3JpZ2luIG1haW4nIDI+L2Rldi9udWxsIHx8IHRydWUKCiAgICAgIC0gbmFtZTogU2V0dXAgbW9rb2NsaSB0b29scwogICAgICAgIGVudjoKICAgICAgICAgIE1PS09fQ0xPTkVfVE9LRU46ICR7eyBzZWNyZXRzLk1PS09HSVRFQV9UT0tFTiB9fQogICAgICAgICAgTU9LT19DTE9ORV9IT1NUOiBnaXQubW9rb2NvbnN1bHRpbmcudGVjaC9Nb2tvQ29uc3VsdGluZwogICAgICAgIHJ1bjogfAogICAgICAgICAgIyBVc2UgcHJlLWluc3RhbGxlZCAvb3B0L21va29jbGkgaWYgYXZhaWxhYmxlICh1cGRhdGVkIGJ5IGNyb24gZXZlcnkgNmgpCiAgICAgICAgICBpZiBbIC1mIC9vcHQvbW9rb2NsaS9jbGkvdmVyc2lvbl9idW1wLnBocCBdICYmIFsgLWYgL29wdC9tb2tvY2xpL2NsaS9tYW5pZmVzdF9lbGVtZW50LnBocCBdICYmIFsgLWYgL29wdC9tb2tvY2xpL3ZlbmRvci9hdXRvbG9hZC5waHAgXTsgdGhlbgogICAgICAgICAgICBlY2hvIFVzaW5nIHByZS1pbnN0YWxsZWQgL29wdC9tb2tvY2xpCiAgICAgICAgICAgIGVjaG8gTU9LT19DTEk9L29wdC9tb2tvY2xpL2NsaSA+PiAkR0lUSFVCX0VOVgogICAgICAgICAgZWxzZQogICAgICAgICAgICBlY2hvIEZhbGxpbmcgYmFjayB0byBmcmVzaCBjbG9uZQogICAgICAgICAgICBpZiAhIGNvbW1hbmQgLXYgY29tcG9zZXIgPiAvZGV2L251bGwgMj4mMTsgdGhlbgogICAgICAgICAgICAgIHN1ZG8gYXB0LWdldCB1cGRhdGUgLXFxICYmIHN1ZG8gYXB0LWdldCBpbnN0YWxsIC15IC1xcSBwaHAtY2xpIHBocC1tYnN0cmluZyBwaHAteG1sIHBocC16aXAgcGhwLWN1cmwgY29tcG9zZXIgPiAvZGV2L251bGwgMj4mMQogICAgICAgICAgICBmaQogICAgICAgICAgICBybSAtcmYgL3RtcC9tb2tvY2xpCiAgICAgICAgICAgIENMT05FX1VSTD1odHRwczovL3gtYWNjZXNzLXRva2VuOiR7TU9LT19DTE9ORV9UT0tFTn1AJHtNT0tPX0NMT05FX0hPU1R9L21va29jbGkuZ2l0CiAgICAgICAgICAgIGdpdCBjbG9uZSAtLWRlcHRoIDEgLS1icmFuY2ggbWFpbiAtLXF1aWV0ICRDTE9ORV9VUkwgL3RtcC9tb2tvY2xpCiAgICAgICAgICAgIGNkIC90bXAvbW9rb2NsaSAmJiBjb21wb3NlciBpbnN0YWxsIC0tbm8tZGV2IC0tbm8taW50ZXJhY3Rpb24gLS1xdWlldAogICAgICAgICAgICBlY2hvIE1PS09fQ0xJPS90bXAvbW9rb2NsaS9jbGkgPj4gJEdJVEhVQl9FTlYKICAgICAgICAgIGZpCgogICAgICAtIG5hbWU6IERldGVjdCBwbGF0Zm9ybQogICAgICAgIGlkOiBwbGF0Zm9ybQogICAgICAgIHJ1bjogfAogICAgICAgICAgIyBBdXRvLWRldGVjdCBhbmQgdXBkYXRlIHBsYXRmb3JtIGlmIG5vdCBzZXQgaW4gbWFuaWZlc3QKICAgICAgICAgIHBocCAke01PS09fQ0xJfS9wbGF0Zm9ybV9kZXRlY3QucGhwIC0tcGF0aCAuIC0tZ2l0aHViLW91dHB1dCAyPi9kZXYvbnVsbCB8fCB0cnVlCiAgICAgICAgICBwaHAgJHtNT0tPX0NMSX0vbWFuaWZlc3RfcmVhZC5waHAgLS1wYXRoIC4gLS1naXRodWItb3V0cHV0CgogICAgICAtIG5hbWU6IENoZWNrIHBsYXRmb3JtIGVsaWdpYmlsaXR5IChKb29tbGEgb25seSkKICAgICAgICBpZDogZWxpZ2liaWxpdHkKICAgICAgICBydW46IHwKICAgICAgICAgIFBMQVRGT1JNPSIke3sgc3RlcHMucGxhdGZvcm0ub3V0cHV0cy5wbGF0Zm9ybSB9fSIKICAgICAgICAgIGlmIFtbICIkUExBVEZPUk0iID09IGpvb21sYSogXV0gfHwgW1sgIiRQTEFURk9STSIgPT0gImpvb21sYSIgXV07IHRoZW4KICAgICAgICAgICAgZWNobyAicHJvY2VlZD10cnVlIiA+PiAiJEdJVEhVQl9PVVRQVVQiCiAgICAgICAgICBlbHNlCiAgICAgICAgICAgIGVjaG8gInByb2NlZWQ9ZmFsc2UiID4+ICIkR0lUSFVCX09VVFBVVCIKICAgICAgICAgICAgZWNobyAiOjpub3RpY2U6OlBsYXRmb3JtICckUExBVEZPUk0nIOKAlCBub24tSm9vbWxhLCBza2lwcGluZyBwcmUtcmVsZWFzZSBhdXRvLWJ1bXAiCiAgICAgICAgICBmaQoKICAgICAgLSBuYW1lOiBSZXNvbHZlIG1ldGFkYXRhIGFuZCBidW1wIHZlcnNpb24KICAgICAgICBpZDogbWV0YQogICAgICAgIGlmOiBzdGVwcy5lbGlnaWJpbGl0eS5vdXRwdXRzLnByb2NlZWQgPT0gJ3RydWUnCiAgICAgICAgcnVuOiB8CiAgICAgICAgICAjIEF1dG8tZGV0ZWN0IHN0YWJpbGl0eSBmcm9tIGJyYW5jaCBuYW1lIG9uIHB1c2gsIG9yIHVzZSBpbnB1dCBvbiBkaXNwYXRjaAogICAgICAgICAgaWYgWyAiJHt7IGdpdGh1Yi5ldmVudF9uYW1lIH19IiA9ICJwdXNoIiBdOyB0aGVuCiAgICAgICAgICAgIGNhc2UgIiR7eyBnaXRodWIucmVmX25hbWUgfX0iIGluCiAgICAgICAgICAgICAgcmMpICAgIFNUQUJJTElUWT0icmVsZWFzZS1jYW5kaWRhdGUiIDs7CiAgICAgICAgICAgICAgYWxwaGEpIFNUQUJJTElUWT0iYWxwaGEiIDs7CiAgICAgICAgICAgICAgYmV0YSkgIFNUQUJJTElUWT0iYmV0YSIgOzsKICAgICAgICAgICAgICAqKSAgICAgU1RBQklMSVRZPSJkZXZlbG9wbWVudCIgOzsKICAgICAgICAgICAgZXNhYwogICAgICAgICAgZWxzZQogICAgICAgICAgICBTVEFCSUxJVFk9IiR7eyBpbnB1dHMuc3RhYmlsaXR5IHx8ICdkZXZlbG9wbWVudCcgfX0iCiAgICAgICAgICBmaQoKICAgICAgICAgIGNhc2UgIiRTVEFCSUxJVFkiIGluCiAgICAgICAgICAgIGRldmVsb3BtZW50KSAgICAgICAgU1VGRklYPSItZGV2IjsgICBUQUc9ImRldmVsb3BtZW50IiA7OwogICAgICAgICAgICBhbHBoYSkgICAgICAgICAgICAgIFNVRkZJWD0iLWFscGhhIjsgVEFHPSJhbHBoYSIgOzsKICAgICAgICAgICAgYmV0YSkgICAgICAgICAgICAgICBTVUZGSVg9Ii1iZXRhIjsgIFRBRz0iYmV0YSIgOzsKICAgICAgICAgICAgcmVsZWFzZS1jYW5kaWRhdGUpICBTVUZGSVg9Ii1yYyI7ICAgIFRBRz0icmVsZWFzZS1jYW5kaWRhdGUiIDs7CiAgICAgICAgICBlc2FjCgogICAgICAgICAgIyBCdW1wIHZlcnNpb24gdmlhIENMSTogcGF0Y2ggZm9yIGRldi9hbHBoYS9iZXRhLCBtaW5vciBmb3IgUkMKICAgICAgICAgIGNhc2UgIiRTVEFCSUxJVFkiIGluCiAgICAgICAgICAgIHJlbGVhc2UtY2FuZGlkYXRlKSBCVU1QPSJtaW5vciIgOzsKICAgICAgICAgICAgKikgICAgICAgICAgICAgICAgIEJVTVA9InBhdGNoIiA7OwogICAgICAgICAgZXNhYwoKICAgICAgICAgIHBocCAke01PS09fQ0xJfS92ZXJzaW9uX2J1bXAucGhwIC0tcGF0aCAuICQoWyAiJEJVTVAiID0gIm1pbm9yIiBdICYmIGVjaG8gIi0tbWlub3IiKSAyPi9kZXYvbnVsbCB8fCB0cnVlCgogICAgICAgICAgIyBTZXQgc3RhYmlsaXR5IHN1ZmZpeCBhbmQgdmVyaWZ5IGNvbnNpc3RlbmN5CiAgICAgICAgICBWRVJTSU9OPSQocGhwICR7TU9LT19DTEl9L3ZlcnNpb25fcmVhZC5waHAgLS1wYXRoIC4gMj4vZGV2L251bGwgfHwgZWNobyAiMDAuMDAuMDEiKQogICAgICAgICAgVkVSU0lPTj0kKGVjaG8gIiRWRVJTSU9OIiB8IHNlZCAncy8tXChkZXZcfGFscGhhXHxiZXRhXHxyY1wpJC8vJykKCiAgICAgICAgICBwaHAgJHtNT0tPX0NMSX0vdmVyc2lvbl9zZXRfcGxhdGZvcm0ucGhwIFwKICAgICAgICAgICAgLS1wYXRoIC4gLS12ZXJzaW9uICIkVkVSU0lPTiIgLS1icmFuY2ggIiR7eyBnaXRodWIucmVmX25hbWUgfX0iIC0tc3RhYmlsaXR5ICIkU1RBQklMSVRZIiAyPi9kZXYvbnVsbCB8fCB0cnVlCiAgICAgICAgICBwaHAgJHtNT0tPX0NMSX0vdmVyc2lvbl9jaGVjay5waHAgLS1wYXRoIC4gLS1maXggMj4vZGV2L251bGwgfHwgdHJ1ZQoKICAgICAgICAgICMgRW5zdXJlIGxpY2Vuc2luZyB0YWdzICh1cGRhdGVzZXJ2ZXJzLCBkbGlkKSBpZiBlbmFibGVkIGluIG1hbmlmZXN0LnhtbAogICAgICAgICAgcGhwICR7TU9LT19DTEl9L21hbmlmZXN0X2xpY2Vuc2luZy5waHAgLS1wYXRoIC4gLS1maXggMj4vZGV2L251bGwgfHwgdHJ1ZQoKICAgICAgICAgICMgQXBwZW5kIHN1ZmZpeCBmb3Igb3V0cHV0CiAgICAgICAgICBpZiBbIC1uICIkU1VGRklYIiBdOyB0aGVuCiAgICAgICAgICAgIFZFUlNJT049IiR7VkVSU0lPTn0ke1NVRkZJWH0iCiAgICAgICAgICBmaQoKICAgICAgICAgICMgQ29tbWl0IHZlcnNpb24gYnVtcAogICAgICAgICAgZ2l0IGNvbmZpZyAtLWxvY2FsIHVzZXIuZW1haWwgImdpdGVhLWFjdGlvbnNbYm90XUBtb2tvY29uc3VsdGluZy50ZWNoIgogICAgICAgICAgZ2l0IGNvbmZpZyAtLWxvY2FsIHVzZXIubmFtZSAiZ2l0ZWEtYWN0aW9uc1tib3RdIgogICAgICAgICAgZ2l0IHJlbW90ZSBzZXQtdXJsIG9yaWdpbiAiaHR0cHM6Ly94LWFjY2Vzcy10b2tlbjoke3sgc2VjcmV0cy5NT0tPR0lURUFfVE9LRU4gfX1AZ2l0Lm1va29jb25zdWx0aW5nLnRlY2gvJHt7IGdpdGh1Yi5yZXBvc2l0b3J5IH19LmdpdCIKICAgICAgICAgIGdpdCBhZGQgLUEKICAgICAgICAgIGdpdCBkaWZmIC0tY2FjaGVkIC0tcXVpZXQgfHwgewogICAgICAgICAgICBnaXQgY29tbWl0IC1tICJjaG9yZSh2ZXJzaW9uKTogcHJlLXJlbGVhc2UgYnVtcCB0byAke1ZFUlNJT059IFtza2lwIGNpXSIKICAgICAgICAgICAgZ2l0IHB1c2ggb3JpZ2luIEhFQUQgMj4mMQogICAgICAgICAgfQoKICAgICAgICAgICMgQXV0by1kZXRlY3QgZWxlbWVudCB2aWEgbWFuaWZlc3RfZWxlbWVudC5waHAKICAgICAgICAgIHBocCAke01PS09fQ0xJfS9tYW5pZmVzdF9lbGVtZW50LnBocCBcCiAgICAgICAgICAgIC0tcGF0aCAuIC0tdmVyc2lvbiAiJFZFUlNJT04iIC0tc3RhYmlsaXR5ICIkU1RBQklMSVRZIiBcCiAgICAgICAgICAgIC0tcmVwbyAiJHtHSVRFQV9SRVBPfSIgLS1naXRodWItb3V0cHV0CgogICAgICAgICAgIyBSZWFkIGJhY2sgZWxlbWVudCBvdXRwdXRzCiAgICAgICAgICBFWFRfRUxFTUVOVD0kKGdyZXAgJ15leHRfZWxlbWVudD0nICIkR0lUSFVCX09VVFBVVCIgfCB0YWlsIC0xIHwgY3V0IC1kPSAtZjIpCiAgICAgICAgICBaSVBfTkFNRT0kKGdyZXAgJ156aXBfbmFtZT0nICIkR0lUSFVCX09VVFBVVCIgfCB0YWlsIC0xIHwgY3V0IC1kPSAtZjIpCiAgICAgICAgICBbIC16ICIkRVhUX0VMRU1FTlQiIF0gJiYgRVhUX0VMRU1FTlQ9JChlY2hvICIke0dJVEVBX1JFUE99IiB8IHRyICdbOnVwcGVyOl0nICdbOmxvd2VyOl0nIHwgdHIgLWQgJyAtJykKICAgICAgICAgIFsgLXogIiRaSVBfTkFNRSIgXSAmJiBaSVBfTkFNRT0iJHtFWFRfRUxFTUVOVH0tJHtWRVJTSU9OfS56aXAiCgogICAgICAgICAgZWNobyAidmVyc2lvbj0ke1ZFUlNJT059IiA+PiAiJEdJVEhVQl9PVVRQVVQiCiAgICAgICAgICBlY2hvICJzdGFiaWxpdHk9JHtTVEFCSUxJVFl9IiA+PiAiJEdJVEhVQl9PVVRQVVQiCiAgICAgICAgICBlY2hvICJzdWZmaXg9JHtTVUZGSVh9IiA+PiAiJEdJVEhVQl9PVVRQVVQiCiAgICAgICAgICBlY2hvICJ0YWc9JHtUQUd9IiA+PiAiJEdJVEhVQl9PVVRQVVQiCiAgICAgICAgICBlY2hvICJ6aXBfbmFtZT0ke1pJUF9OQU1FfSIgPj4gIiRHSVRIVUJfT1VUUFVUIgogICAgICAgICAgZWNobyAiZXh0X2VsZW1lbnQ9JHtFWFRfRUxFTUVOVH0iID4+ICIkR0lUSFVCX09VVFBVVCIKCiAgICAgICAgICBlY2hvICI9PT0gUHJlLVJlbGVhc2U6ICR7RVhUX0VMRU1FTlR9ICR7VkVSU0lPTn0ke1NVRkZJWH0gPT09IgoKICAgICAgLSBuYW1lOiBDcmVhdGUgcmVsZWFzZQogICAgICAgIGlkOiByZWxlYXNlCiAgICAgICAgaWY6IHN0ZXBzLmVsaWdpYmlsaXR5Lm91dHB1dHMucHJvY2VlZCA9PSAndHJ1ZScKICAgICAgICBydW46IHwKICAgICAgICAgIFRBRz0iJHt7IHN0ZXBzLm1ldGEub3V0cHV0cy50YWcgfX0iCiAgICAgICAgICBWRVJTSU9OPSIke3sgc3RlcHMubWV0YS5vdXRwdXRzLnZlcnNpb24gfX0iCiAgICAgICAgICBBUElfQkFTRT0iJHtHSVRFQV9VUkx9L2FwaS92MS9yZXBvcy8ke0dJVEVBX09SR30vJHtHSVRFQV9SRVBPfSIKICAgICAgICAgIHBocCAke01PS09fQ0xJfS9yZWxlYXNlX2NyZWF0ZS5waHAgXAogICAgICAgICAgICAtLXBhdGggLiAtLXZlcnNpb24gIiRWRVJTSU9OIiAtLXRhZyAiJFRBRyIgXAogICAgICAgICAgICAtLXRva2VuICIke3sgc2VjcmV0cy5NT0tPR0lURUFfVE9LRU4gfX0iIC0tYXBpLWJhc2UgIiRBUElfQkFTRSIgXAogICAgICAgICAgICAtLXJlcG8gIiR7R0lURUFfUkVQT30iIC0tYnJhbmNoICIke3sgZ2l0aHViLnJlZl9uYW1lIH19IiAtLXByZXJlbGVhc2UKCiAgICAgIC0gbmFtZTogVXBkYXRlIHJlbGVhc2Ugbm90ZXMgZnJvbSBDSEFOR0VMT0cubWQKICAgICAgICBpZjogc3RlcHMuZWxpZ2liaWxpdHkub3V0cHV0cy5wcm9jZWVkID09ICd0cnVlJwogICAgICAgIHJ1bjogfAogICAgICAgICAgVEFHPSIke3sgc3RlcHMubWV0YS5vdXRwdXRzLnRhZyB9fSIKICAgICAgICAgIFZFUlNJT049IiR7eyBzdGVwcy5tZXRhLm91dHB1dHMudmVyc2lvbiB9fSIKICAgICAgICAgIEFQSV9CQVNFPSIke0dJVEVBX1VSTH0vYXBpL3YxL3JlcG9zLyR7R0lURUFfT1JHfS8ke0dJVEVBX1JFUE99IgoKICAgICAgICAgICMgRXh0cmFjdCBbVW5yZWxlYXNlZF0gc2VjdGlvbiBmcm9tIGNoYW5nZWxvZyAoZXZlcnl0aGluZyBiZXR3ZWVuIFtVbnJlbGVhc2VkXSBhbmQgbmV4dCAjIyBoZWFkaW5nKQogICAgICAgICAgaWYgWyAtZiAiQ0hBTkdFTE9HLm1kIiBdOyB0aGVuCiAgICAgICAgICAgIE5PVEVTPSQoYXdrICcvXiMjIFxbVW5yZWxlYXNlZFxdL3tmb3VuZD0xOyBuZXh0fSAvXiMjIFxbL3tpZihmb3VuZCkgZXhpdH0gZm91bmR7cHJpbnR9JyBDSEFOR0VMT0cubWQpCiAgICAgICAgICAgIFsgLXogIiROT1RFUyIgXSAmJiBOT1RFUz0iUmVsZWFzZSAke1ZFUlNJT059IgogICAgICAgICAgZWxzZQogICAgICAgICAgICBOT1RFUz0iUmVsZWFzZSAke1ZFUlNJT059IgogICAgICAgICAgZmkKCiAgICAgICAgICAjIFVwZGF0ZSByZWxlYXNlIGJvZHkgdmlhIEFQSQogICAgICAgICAgUkVMRUFTRV9JRD0kKGN1cmwgLXNmIC1IICJBdXRob3JpemF0aW9uOiB0b2tlbiAke3sgc2VjcmV0cy5NT0tPR0lURUFfVE9LRU4gfX0iIFwKICAgICAgICAgICAgIiR7QVBJX0JBU0V9L3JlbGVhc2VzL3RhZ3MvJHtUQUd9IiB8IHB5dGhvbjMgLWMgImltcG9ydCBqc29uLHN5czsgcHJpbnQoanNvbi5sb2FkKHN5cy5zdGRpbikuZ2V0KCdpZCcsJycpKSIgMj4vZGV2L251bGwgfHwgdHJ1ZSkKCiAgICAgICAgICBpZiBbIC1uICIkUkVMRUFTRV9JRCIgXTsgdGhlbgogICAgICAgICAgICBweXRob24zIC1jICIKICAgICAgICAgIGltcG9ydCBqc29uLCB1cmxsaWIucmVxdWVzdAogICAgICAgICAgYm9keSA9IG9wZW4oJy9kZXYvc3RkaW4nKS5yZWFkKCkKICAgICAgICAgIHBheWxvYWQgPSBqc29uLmR1bXBzKHsnYm9keSc6IGJvZHl9KS5lbmNvZGUoKQogICAgICAgICAgcmVxID0gdXJsbGliLnJlcXVlc3QuUmVxdWVzdCgKICAgICAgICAgICAgICAnJHtBUElfQkFTRX0vcmVsZWFzZXMvJHtSRUxFQVNFX0lEfScsCiAgICAgICAgICAgICAgZGF0YT1wYXlsb2FkLCBtZXRob2Q9J1BBVENIJywKICAgICAgICAgICAgICBoZWFkZXJzPXsKICAgICAgICAgICAgICAgICAgJ0F1dGhvcml6YXRpb24nOiAndG9rZW4gJHt7IHNlY3JldHMuTU9LT0dJVEVBX1RPS0VOIH19JywKICAgICAgICAgICAgICAgICAgJ0NvbnRlbnQtVHlwZSc6ICdhcHBsaWNhdGlvbi9qc29uJwogICAgICAgICAgICAgIH0pCiAgICAgICAgICB1cmxsaWIucmVxdWVzdC51cmxvcGVuKHJlcSkKICAgICAgICAgICIgPDw8ICIkTk9URVMiCiAgICAgICAgICAgIGVjaG8gIlJlbGVhc2Ugbm90ZXMgdXBkYXRlZCBmcm9tIENIQU5HRUxPRy5tZCIKICAgICAgICAgIGZpCgogICAgICAtIG5hbWU6IEJ1aWxkIHBhY2thZ2UgYW5kIHVwbG9hZAogICAgICAgIGlkOiBwYWNrYWdlCiAgICAgICAgaWY6IHN0ZXBzLmVsaWdpYmlsaXR5Lm91dHB1dHMucHJvY2VlZCA9PSAndHJ1ZScKICAgICAgICBydW46IHwKICAgICAgICAgIFZFUlNJT049IiR7eyBzdGVwcy5tZXRhLm91dHB1dHMudmVyc2lvbiB9fSIKICAgICAgICAgIFRBRz0iJHt7IHN0ZXBzLm1ldGEub3V0cHV0cy50YWcgfX0iCiAgICAgICAgICBBUElfQkFTRT0iJHtHSVRFQV9VUkx9L2FwaS92MS9yZXBvcy8ke0dJVEVBX09SR30vJHtHSVRFQV9SRVBPfSIKICAgICAgICAgIHBocCAke01PS09fQ0xJfS9yZWxlYXNlX3BhY2thZ2UucGhwIFwKICAgICAgICAgICAgLS1wYXRoIC4gLS12ZXJzaW9uICIkVkVSU0lPTiIgLS10YWcgIiRUQUciIFwKICAgICAgICAgICAgLS10b2tlbiAiJHt7IHNlY3JldHMuTU9LT0dJVEVBX1RPS0VOIH19IiAtLWFwaS1iYXNlICIkQVBJX0JBU0UiIFwKICAgICAgICAgICAgLS1yZXBvICIke0dJVEVBX1JFUE99IiAtLW91dHB1dCAvdG1wIHx8IHRydWUKCiAgICAgICMgdXBkYXRlcy54bWwgaXMgZ2VuZXJhdGVkIGR5bmFtaWNhbGx5IGJ5IE1va29HaXRlYSBsaWNlbnNlIHNlcnZlcgogICAgICAjIE5vIG5lZWQgdG8gYnVpbGQsIGNvbW1pdCwgb3Igc3luYyB1cGRhdGVzLnhtbCBmcm9tIHdvcmtmbG93cwoKICAgICAgLSBuYW1lOiAiRGVsZXRlIGxlc3NlciBwcmUtcmVsZWFzZSBjaGFubmVscyAoY2FzY2FkZSkiCiAgICAgICAgaWY6IHN0ZXBzLmVsaWdpYmlsaXR5Lm91dHB1dHMucHJvY2VlZCA9PSAndHJ1ZScKICAgICAgICBjb250aW51ZS1vbi1lcnJvcjogdHJ1ZQogICAgICAgIHJ1bjogfAogICAgICAgICAgQVBJX0JBU0U9IiR7R0lURUFfVVJMfS9hcGkvdjEvcmVwb3MvJHtHSVRFQV9PUkd9LyR7R0lURUFfUkVQT30iCiAgICAgICAgICBUT0tFTj0iJHt7IHNlY3JldHMuTU9LT0dJVEVBX1RPS0VOIH19IgoKICAgICAgICAgIHBocCAke01PS09fQ0xJfS9yZWxlYXNlX2Nhc2NhZGUucGhwIFwKICAgICAgICAgICAgLS1zdGFiaWxpdHkgIiR7eyBzdGVwcy5tZXRhLm91dHB1dHMuc3RhYmlsaXR5IH19IiBcCiAgICAgICAgICAgIC0tdG9rZW4gIiR7VE9LRU59IiBcCiAgICAgICAgICAgIC0tYXBpLWJhc2UgIiR7QVBJX0JBU0V9IgoKICAgICAgLSBuYW1lOiBTdW1tYXJ5CiAgICAgICAgaWY6IGFsd2F5cygpCiAgICAgICAgcnVuOiB8CiAgICAgICAgICBWRVJTSU9OPSIke3sgc3RlcHMubWV0YS5vdXRwdXRzLnZlcnNpb24gfX0iCiAgICAgICAgICBTVEFCSUxJVFk9IiR7eyBzdGVwcy5tZXRhLm91dHB1dHMuc3RhYmlsaXR5IH19IgogICAgICAgICAgWklQX05BTUU9IiR7eyBzdGVwcy5tZXRhLm91dHB1dHMuemlwX25hbWUgfX0iCiAgICAgICAgICBTSEEyNTY9IiR7eyBzdGVwcy5wYWNrYWdlLm91dHB1dHMuc2hhMjU2X3ppcCB9fSIKICAgICAgICAgIGVjaG8gIiMjIFByZS1SZWxlYXNlIENvbXBsZXRlIiA+PiAkR0lUSFVCX1NURVBfU1VNTUFSWQogICAgICAgICAgZWNobyAiIiA+PiAkR0lUSFVCX1NURVBfU1VNTUFSWQogICAgICAgICAgZWNobyAifCBGaWVsZCB8IFZhbHVlIHwiID4+ICRHSVRIVUJfU1RFUF9TVU1NQVJZCiAgICAgICAgICBlY2hvICJ8LS0tLS0tLXwtLS0tLS0tfCIgPj4gJEdJVEhVQl9TVEVQX1NVTU1BUlkKICAgICAgICAgIGVjaG8gInwgVmVyc2lvbiB8IFxgJHtWRVJTSU9OfVxgIHwiID4+ICRHSVRIVUJfU1RFUF9TVU1NQVJZCiAgICAgICAgICBlY2hvICJ8IENoYW5uZWwgfCAke1NUQUJJTElUWX0gfCIgPj4gJEdJVEhVQl9TVEVQX1NVTU1BUlkKICAgICAgICAgIGVjaG8gInwgUGFja2FnZSB8IFxgJHtaSVBfTkFNRX1cYCB8IiA+PiAkR0lUSFVCX1NURVBfU1VNTUFSWQogICAgICAgICAgZWNobyAifCBTSEEtMjU2IHwgXGAke1NIQTI1Njotbi9hfVxgIHwiID4+ICRHSVRIVUJfU1RFUF9TVU1NQVJZCg== \ No newline at end of file +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: MokoGitea.Workflow +# INGROUP: mokocli.Release +# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic +# PATH: /.mokogitea/workflows/pre-release.yml +# VERSION: 05.02.01 +# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches + +name: "Universal: Pre-Release" + +on: + push: + branches: + - dev + - 'fix/**' + - 'patch/**' + - 'hotfix/**' + - 'bugfix/**' + - 'chore/**' + - alpha + - beta + - rc + 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 || github.ref_name }})" + runs-on: release + # Skip on template repos (Template-*) — they scaffold other repos and do not release. + if: >- + !startsWith(github.event.repository.name, 'Template-') && + ( + github.event_name == 'workflow_dispatch' || + github.event_name == 'push' + ) + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.MOKOGITEA_TOKEN }} + ref: ${{ github.ref_name }} + submodules: recursive + + - name: Update submodules to main + run: | + git submodule foreach --quiet 'git checkout main && git pull --quiet origin main' 2>/dev/null || true + + - name: Setup mokocli tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + run: | + # Use pre-installed /opt/mokocli if available (updated by cron every 6h) + if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/cli/manifest_element.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then + echo Using pre-installed /opt/mokocli + echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV + else + echo Falling back to fresh clone + if ! command -v composer > /dev/null 2>&1; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1 + fi + rm -rf /tmp/mokocli + CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoCLI.git + git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli + cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet + echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV + fi + + - 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: Check platform eligibility (Joomla only) + id: eligibility + run: | + PLATFORM="${{ steps.platform.outputs.platform }}" + if [[ "$PLATFORM" == joomla* ]] || [[ "$PLATFORM" == "joomla" ]]; then + echo "proceed=true" >> "$GITHUB_OUTPUT" + else + echo "proceed=false" >> "$GITHUB_OUTPUT" + echo "::notice::Platform '$PLATFORM' — non-Joomla, skipping pre-release auto-bump" + fi + + - name: Resolve metadata and bump version + id: meta + if: steps.eligibility.outputs.proceed == 'true' + run: | + # Auto-detect stability from branch name on push, or use input on dispatch + if [ "${{ github.event_name }}" = "push" ]; then + case "${{ github.ref_name }}" in + rc) STABILITY="release-candidate" ;; + alpha) STABILITY="alpha" ;; + beta) STABILITY="beta" ;; + *) STABILITY="development" ;; + esac + 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 "mokogitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "mokogitea-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]" + # Push the bump commit, but do NOT fail the release if the target branch + # is protected and the release identity is not on the push allowlist. + # The build proceeds from the in-tree bumped version regardless; if the + # push is rejected, the next run simply re-bumps from the same base. + if ! git push origin HEAD 2>&1; then + echo "::warning::Version-bump commit could not be pushed (protected branch?). Building from in-tree version ${VERSION} anyway." + fi + } + + # 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 + if: steps.eligibility.outputs.proceed == 'true' + 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: Update release notes from CHANGELOG.md + if: steps.eligibility.outputs.proceed == 'true' + 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 + if: steps.eligibility.outputs.proceed == 'true' + 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)" + if: steps.eligibility.outputs.proceed == 'true' + 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 \ No newline at end of file diff --git a/.mokogitea/workflows/rc-revert.yml b/.mokogitea/workflows/rc-revert.yml index 93985f2..57934ea 100644 --- a/.mokogitea/workflows/rc-revert.yml +++ b/.mokogitea/workflows/rc-revert.yml @@ -1 +1,72 @@ -IyBDb3B5cmlnaHQgKEMpIDIwMjYgTW9rbyBDb25zdWx0aW5nIDxoZWxsb0Btb2tvY29uc3VsdGluZy50ZWNoPgojCiMgU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IEdQTC0zLjAtb3ItbGF0ZXIKIwojIEZJTEUgSU5GT1JNQVRJT04KIyBERUZHUk9VUDogR2l0ZWEuV29ya2Zsb3cKIyBJTkdST1VQOiBtb2tvY2xpLlVuaXZlcnNhbAojIFJFUE86IGh0dHBzOi8vZ2l0Lm1va29jb25zdWx0aW5nLnRlY2gvTW9rb0NvbnN1bHRpbmcvbW9rb2NsaQojIFBBVEg6IC8ubW9rb2dpdGVhL3dvcmtmbG93cy9yYy1yZXZlcnQueW1sCiMgVkVSU0lPTjogMDkuMjMuMDAKIyBCUklFRjogUmVuYW1lIHJjLyBicmFuY2ggYmFjayB0byBkZXYvIHdoZW4gUFIgaXMgY2xvc2VkIHdpdGhvdXQgbWVyZ2UKCm5hbWU6ICJSQyBSZXZlcnQiCgpvbjoKICBwdWxsX3JlcXVlc3Q6CiAgICB0eXBlczogW2Nsb3NlZF0KCmVudjoKICBGT1JDRV9KQVZBU0NSSVBUX0FDVElPTlNfVE9fTk9ERTI0OiB0cnVlCgpqb2JzOgogIHJldmVydDoKICAgIG5hbWU6IFJlbmFtZSByYy8gYmFjayB0byBkZXYvCiAgICBydW5zLW9uOiB1YnVudHUtbGF0ZXN0CiAgICBpZjogPi0KICAgICAgZ2l0aHViLmV2ZW50LnB1bGxfcmVxdWVzdC5tZXJnZWQgPT0gZmFsc2UgJiYKICAgICAgc3RhcnRzV2l0aChnaXRodWIuZXZlbnQucHVsbF9yZXF1ZXN0LmhlYWQucmVmLCAncmMvJykKCiAgICBzdGVwczoKICAgICAgLSBuYW1lOiBSZW5hbWUgYnJhbmNoCiAgICAgICAgZW52OgogICAgICAgICAgQlJBTkNIOiAke3sgZ2l0aHViLmV2ZW50LnB1bGxfcmVxdWVzdC5oZWFkLnJlZiB9fQogICAgICAgICAgUkVQTzogJHt7IGdpdGh1Yi5yZXBvc2l0b3J5IH19CiAgICAgICAgICBHSVRFQV9VUkw6ICR7eyB2YXJzLkdJVEVBX1VSTCB8fCAnaHR0cHM6Ly9naXQubW9rb2NvbnN1bHRpbmcudGVjaCcgfX0KICAgICAgICAgIFRPS0VOOiAke3sgc2VjcmV0cy5NT0tPR0lURUFfVE9LRU4gfX0KICAgICAgICBydW46IHwKICAgICAgICAgIHNldCAtZXVvIHBpcGVmYWlsCiAgICAgICAgICAjIEJSQU5DSCBpcyBhdHRhY2tlci1jb250cm9sbGVkIChQUiBoZWFkIHJlZikuIFN0cmljdCBhbGxvd2xpc3QgYmVmb3JlIEFOWSB1c2UuCiAgICAgICAgICBpZiAhIHByaW50ZiAnJXMnICIkQlJBTkNIIiB8IGdyZXAgLUVxICdecmMvW0EtWmEtejAtOS5fLy1dKyQnOyB0aGVuCiAgICAgICAgICAgIGVjaG8gIjo6ZXJyb3I6OlJlZnVzaW5nIHVuc2FmZSBicmFuY2ggbmFtZTogJEJSQU5DSCI7IGV4aXQgMQogICAgICAgICAgZmkKICAgICAgICAgIFNVRkZJWD0iJHtCUkFOQ0gjcmMvfSIKICAgICAgICAgIERFVl9CUkFOQ0g9ImRldi8ke1NVRkZJWH0iCiAgICAgICAgICBBUEk9IiR7R0lURUFfVVJMfS9hcGkvdjEvcmVwb3MvJHtSRVBPfS9icmFuY2hlcyIKCiAgICAgICAgICAjIENyZWF0ZSBkZXYvIGJyYW5jaCBmcm9tIHJjLyBicmFuY2gKICAgICAgICAgIFNUQVRVUz0kKGN1cmwgLXNmIC1vIC9kZXYvbnVsbCAtdyAiJXtodHRwX2NvZGV9IiAtWCBQT1NUIFwKICAgICAgICAgICAgLUggIkF1dGhvcml6YXRpb246IHRva2VuICR7VE9LRU59IiBcCiAgICAgICAgICAgIC1IICJDb250ZW50LVR5cGU6IGFwcGxpY2F0aW9uL2pzb24iIFwKICAgICAgICAgICAgLWQgIntcIm5ld19icmFuY2hfbmFtZVwiOiBcIiR7REVWX0JSQU5DSH1cIiwgXCJvbGRfYnJhbmNoX25hbWVcIjogXCIke0JSQU5DSH1cIn0iIFwKICAgICAgICAgICAgIiR7QVBJfSIgMj4vZGV2L251bGwgfHwgdHJ1ZSkKICAgICAgICAgIGlmIFsgIiRTVEFUVVMiID0gIjIwMSIgXTsgdGhlbgogICAgICAgICAgICBlY2hvICJDcmVhdGVkIGJyYW5jaDogJHtERVZfQlJBTkNIfSIgPj4gIiRHSVRIVUJfU1RFUF9TVU1NQVJZIgogICAgICAgICAgZWxzZQogICAgICAgICAgICBlY2hvICI6OmVycm9yOjpGYWlsZWQgdG8gY3JlYXRlICR7REVWX0JSQU5DSH0gZnJvbSAke0JSQU5DSH0gKEhUVFAgJHtTVEFUVVN9KSI7IGV4aXQgMQogICAgICAgICAgZmkKCiAgICAgICAgICAjIFJlYWQgQlJBTkNIIGZyb20gdGhlIGVudmlyb25tZW50IGluc2lkZSBQSFAgKGdldGVudiwgbm8gc3RyaW5nIGludGVycG9sYXRpb24gLT4gbm8gUEhQIGluamVjdGlvbikKICAgICAgICAgIEVOQ09ERUQ9JChwaHAgLXIgJ2VjaG8gcmF3dXJsZW5jb2RlKGdldGVudigiQlJBTkNIIikpOycpCiAgICAgICAgICBTVEFUVVM9JChjdXJsIC1zZiAtbyAvZGV2L251bGwgLXcgIiV7aHR0cF9jb2RlfSIgLVggREVMRVRFIFwKICAgICAgICAgICAgLUggIkF1dGhvcml6YXRpb246IHRva2VuICR7VE9LRU59IiBcCiAgICAgICAgICAgICIke0FQSX0vJHtFTkNPREVEfSIgMj4vZGV2L251bGwgfHwgdHJ1ZSkKICAgICAgICAgIGlmIFsgIiRTVEFUVVMiID0gIjIwNCIgXTsgdGhlbgogICAgICAgICAgICBlY2hvICJEZWxldGVkIGJyYW5jaDogJHtCUkFOQ0h9IiA+PiAiJEdJVEhVQl9TVEVQX1NVTU1BUlkiCiAgICAgICAgICBlbHNlCiAgICAgICAgICAgIGVjaG8gIjo6d2FybmluZzo6RmFpbGVkIHRvIGRlbGV0ZSAke0JSQU5DSH0gKEhUVFAgJHtTVEFUVVN9KSIKICAgICAgICAgIGZpCgogICAgICAgICAgZWNobyAiIyMjIFJDIFJldmVydGVkIiA+PiAiJEdJVEhVQl9TVEVQX1NVTU1BUlkiCiAgICAgICAgICBlY2hvICIke0JSQU5DSH0g4oaSICR7REVWX0JSQU5DSH0iID4+ICIkR0lUSFVCX1NURVBfU1VNTUFSWSIK \ No newline at end of file +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: MokoGitea.Workflow +# INGROUP: mokocli.Universal +# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic +# PATH: /.mokogitea/workflows/rc-revert.yml +# VERSION: 09.23.00 +# BRIEF: Rename rc/ branch back to dev/ when PR is closed without merge + +name: "RC Revert" + +on: + pull_request: + types: [closed] + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + revert: + name: Rename rc/ back to dev/ + runs-on: ubuntu-latest + if: >- + github.event.pull_request.merged == false && + startsWith(github.event.pull_request.head.ref, 'rc/') && + !startsWith(github.event.repository.name, 'Template-') + + steps: + - name: Rename branch + env: + BRANCH: ${{ github.event.pull_request.head.ref }} + REPO: ${{ github.repository }} + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + run: | + set -euo pipefail + # BRANCH is attacker-controlled (PR head ref). Strict allowlist before ANY use. + if ! printf '%s' "$BRANCH" | grep -Eq '^rc/[A-Za-z0-9._/-]+$'; then + echo "::error::Refusing unsafe branch name: $BRANCH"; exit 1 + fi + SUFFIX="${BRANCH#rc/}" + DEV_BRANCH="dev/${SUFFIX}" + API="${GITEA_URL}/api/v1/repos/${REPO}/branches" + + # Create dev/ branch from rc/ branch + STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X POST \ + -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{\"new_branch_name\": \"${DEV_BRANCH}\", \"old_branch_name\": \"${BRANCH}\"}" \ + "${API}" 2>/dev/null || true) + if [ "$STATUS" = "201" ]; then + echo "Created branch: ${DEV_BRANCH}" >> "$GITHUB_STEP_SUMMARY" + else + echo "::error::Failed to create ${DEV_BRANCH} from ${BRANCH} (HTTP ${STATUS})"; exit 1 + fi + + # Read BRANCH from the environment inside PHP (getenv, no string interpolation -> no PHP injection) + ENCODED=$(php -r 'echo rawurlencode(getenv("BRANCH"));') + STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \ + -H "Authorization: token ${TOKEN}" \ + "${API}/${ENCODED}" 2>/dev/null || true) + if [ "$STATUS" = "204" ]; then + echo "Deleted branch: ${BRANCH}" >> "$GITHUB_STEP_SUMMARY" + else + echo "::warning::Failed to delete ${BRANCH} (HTTP ${STATUS})" + fi + + echo "### RC Reverted" >> "$GITHUB_STEP_SUMMARY" + echo "${BRANCH} → ${DEV_BRANCH}" >> "$GITHUB_STEP_SUMMARY" diff --git a/.mokogitea/workflows/repo-health.yml b/.mokogitea/workflows/repo-health.yml index d45e9ac..54d7cf9 100644 --- a/.mokogitea/workflows/repo-health.yml +++ b/.mokogitea/workflows/repo-health.yml @@ -1 +1,700 @@ -IyA9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09CiMgQ29weXJpZ2h0IChDKSAyMDI1IE1va28gQ29uc3VsdGluZyA8aGVsbG9AbW9rb2NvbnN1bHRpbmcudGVjaD4KIwojIFRoaXMgZmlsZSBpcyBwYXJ0IG9mIGEgTW9rbyBDb25zdWx0aW5nIHByb2plY3QuCiMKIyBTUERYLUxpY2Vuc2UtSWRlbnRpZmllcjogR1BMLTMuMC1vci1sYXRlcgojCiMgRklMRSBJTkZPUk1BVElPTgojIERFRkdST1VQOiBHaXRlYS5Xb3JrZmxvdwojIElOR1JPVVA6IG1va29jbGkuVmFsaWRhdGlvbgojIFJFUE86IGh0dHBzOi8vZ2l0Lm1va29jb25zdWx0aW5nLnRlY2gvbW9rb2NvbnN1bHRpbmctdGVjaC9tb2tvY2xpCiMgUEFUSDogL3RlbXBsYXRlcy93b3JrZmxvd3Mvam9vbWxhL3JlcG9faGVhbHRoLnltbC50ZW1wbGF0ZQojIFZFUlNJT046IDA5LjIzLjAwCiMgQlJJRUY6IEVuZm9yY2VzIHJlcG9zaXRvcnkgZ3VhcmRyYWlscyBieSB2YWxpZGF0aW5nIHNjcmlwdHMgZ292ZXJuYW5jZSwgdG9vbGluZyBhdmFpbGFiaWxpdHksIGFuZCBjb3JlIHJlcG9zaXRvcnkgaGVhbHRoIGFydGlmYWN0cy4KIyA9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09CgpuYW1lOiAiR2VuZXJpYzogUmVwbyBIZWFsdGgiCgpkZWZhdWx0czoKICBydW46CiAgICBzaGVsbDogYmFzaAoKb246CiAgd29ya2Zsb3dfZGlzcGF0Y2g6CiAgICBpbnB1dHM6CiAgICAgIHByb2ZpbGU6CiAgICAgICAgZGVzY3JpcHRpb246ICdWYWxpZGF0aW9uIHByb2ZpbGU6IGFsbCwgc2NyaXB0cywgb3IgcmVwbycKICAgICAgICByZXF1aXJlZDogdHJ1ZQogICAgICAgIGRlZmF1bHQ6IGFsbAogICAgICAgIHR5cGU6IGNob2ljZQogICAgICAgIG9wdGlvbnM6CiAgICAgICAgICAtIGFsbAogICAgICAgICAgLSBzY3JpcHRzCiAgICAgICAgICAtIHJlcG8KICBwdWxsX3JlcXVlc3Q6CiAgICBicmFuY2hlczoKICAgICAgLSBtYWluCgpwZXJtaXNzaW9uczoKICBjb250ZW50czogcmVhZAoKZW52OgogICMgU2NyaXB0cyBnb3Zlcm5hbmNlIHBvbGljeQogIFNDUklQVFNfUkVRVUlSRURfRElSUzoKICBTQ1JJUFRTX0FMTE9XRURfRElSUzogc2NyaXB0cyxzY3JpcHRzL2ZpeCxzY3JpcHRzL2xpYixzY3JpcHRzL3JlbGVhc2Usc2NyaXB0cy9ydW4sc2NyaXB0cy92YWxpZGF0ZQoKICAjIFJlcG8gaGVhbHRoIHBvbGljeQogIFJFUE9fUkVRVUlSRURfQVJUSUZBQ1RTOiBSRUFETUUubWQsTElDRU5TRSxDSEFOR0VMT0cubWQsQ09OVFJJQlVUSU5HLm1kLENPREVfT0ZfQ09ORFVDVC5tZCwubW9rb2dpdGVhL3dvcmtmbG93cy8KICBSRVBPX09QVElPTkFMX0ZJTEVTOiBTRUNVUklUWS5tZCxHT1ZFUk5BTkNFLm1kLC5lZGl0b3Jjb25maWcsLmdpdGF0dHJpYnV0ZXMsLmdpdGlnbm9yZSxSRUFETUUubWQsZG9jcy8KICBSRVBPX0RJU0FMTE9XRURfRElSUzoKICBSRVBPX0RJU0FMTE9XRURfRklMRVM6IFRPRE8ubWQsdG9kby5tZAoKICAjIEV4dGVuZGVkIGNoZWNrcyB0b2dnbGVzCiAgRVhURU5ERURfQ0hFQ0tTOiAidHJ1ZSIKCiAgIyBGaWxlIC8gZGlyZWN0b3J5IHZhcmlhYmxlcwogIERPQ1NfSU5ERVg6IGRvY3MvZG9jcy1pbmRleC5tZAogIFNDUklQVF9ESVI6IHNjcmlwdHMKICBXT1JLRkxPV1NfRElSOiAubW9rb2dpdGVhL3dvcmtmbG93cwogIFNIRUxMQ0hFQ0tfUEFUVEVSTjogJyouc2gnCiAgU1BEWF9GSUxFX0dMT0JTOiAnKi5zaCwqLnBocCwqLmpzLCoudHMsKi5jc3MsKi54bWwsKi55bWwsKi55YW1sJwogIEZPUkNFX0pBVkFTQ1JJUFRfQUNUSU9OU19UT19OT0RFMjQ6IHRydWUKCmpvYnM6CiAgYWNjZXNzX2NoZWNrOgogICAgbmFtZTogQWNjZXNzIGNvbnRyb2wKICAgIHJ1bnMtb246IHVidW50dS1sYXRlc3QKICAgIHRpbWVvdXQtbWludXRlczogMTAKICAgIHBlcm1pc3Npb25zOgogICAgICBjb250ZW50czogcmVhZAoKICAgIG91dHB1dHM6CiAgICAgIGFsbG93ZWQ6ICR7eyBzdGVwcy5wZXJtLm91dHB1dHMuYWxsb3dlZCB9fQogICAgICBwZXJtaXNzaW9uOiAke3sgc3RlcHMucGVybS5vdXRwdXRzLnBlcm1pc3Npb24gfX0KCiAgICBzdGVwczoKICAgICAgLSBuYW1lOiBDaGVjayBhY3RvciBwZXJtaXNzaW9uIChhZG1pbiBvbmx5KQogICAgICAgIGlkOiBwZXJtCiAgICAgICAgZW52OgogICAgICAgICAgVE9LRU46ICR7eyBzZWNyZXRzLk1PS09HSVRFQV9UT0tFTiB8fCBnaXRodWIudG9rZW4gfX0KICAgICAgICAgIFJFUE86ICR7eyBnaXRodWIucmVwb3NpdG9yeSB9fQogICAgICAgICAgQUNUT1I6ICR7eyBnaXRodWIuYWN0b3IgfX0KICAgICAgICBydW46IHwKICAgICAgICAgIHNldCAtZXVvIHBpcGVmYWlsCiAgICAgICAgICBBTExPV0VEPWZhbHNlCiAgICAgICAgICBQRVJNSVNTSU9OPXVua25vd24KICAgICAgICAgIE1FVEhPRD0iIgoKICAgICAgICAgICMgSGFyZGNvZGVkIGF1dGhvcml6ZWQgdXNlcnMg4oCUIGFsd2F5cyBhbGxvd2VkCiAgICAgICAgICBjYXNlICIkQUNUT1IiIGluCiAgICAgICAgICAgIGptaWxsZXJ8Z2l0ZWEtYWN0aW9uc1tib3RdKQogICAgICAgICAgICAgIEFMTE9XRUQ9dHJ1ZQogICAgICAgICAgICAgIFBFUk1JU1NJT049YWRtaW4KICAgICAgICAgICAgICBNRVRIT0Q9ImhhcmRjb2RlZCBhbGxvd2xpc3QiCiAgICAgICAgICAgICAgOzsKICAgICAgICAgICAgKikKICAgICAgICAgICAgICAjIERldGVjdCBwbGF0Zm9ybSBhbmQgY2hlY2sgcGVybWlzc2lvbnMgdmlhIEFQSQogICAgICAgICAgICAgIEFQSV9CQVNFPSIke0dJVEhVQl9BUElfVVJMOi0ke0dJVEVBX0FQSV9VUkw6LWh0dHBzOi8vYXBpLmdpdGh1Yi5jb219fSIKICAgICAgICAgICAgICBSRVNQPSQoY3VybCAtc2YgLUggIkF1dGhvcml6YXRpb246IHRva2VuICR7VE9LRU59IiBcCiAgICAgICAgICAgICAgICAiJHtBUElfQkFTRX0vcmVwb3MvJHtSRVBPfS9jb2xsYWJvcmF0b3JzLyR7QUNUT1J9L3Blcm1pc3Npb24iIDI+L2Rldi9udWxsIHx8IGVjaG8gJ3t9JykKICAgICAgICAgICAgICBQRVJNSVNTSU9OPSQoZWNobyAiJFJFU1AiIHwgZ3JlcCAtb1AgJyJwZXJtaXNzaW9uIlxzKjpccyoiXEtbXiJdKycgfHwgZWNobyAidW5rbm93biIpCiAgICAgICAgICAgICAgaWYgWyAiJFBFUk1JU1NJT04iID0gImFkbWluIiBdIHx8IFsgIiRQRVJNSVNTSU9OIiA9ICJtYWludGFpbiIgXSB8fCBbICIkUEVSTUlTU0lPTiIgPSAib3duZXIiIF07IHRoZW4KICAgICAgICAgICAgICAgIEFMTE9XRUQ9dHJ1ZQogICAgICAgICAgICAgIGZpCiAgICAgICAgICAgICAgTUVUSE9EPSJjb2xsYWJvcmF0b3IgQVBJIgogICAgICAgICAgICAgIDs7CiAgICAgICAgICBlc2FjCgogICAgICAgICAgZWNobyAicGVybWlzc2lvbj0ke1BFUk1JU1NJT059IiA+PiAiJEdJVEhVQl9PVVRQVVQiCiAgICAgICAgICBlY2hvICJhbGxvd2VkPSR7QUxMT1dFRH0iID4+ICIkR0lUSFVCX09VVFBVVCIKCiAgICAgICAgICB7CiAgICAgICAgICAgIGVjaG8gIiMjIEFjY2VzcyBBdXRob3JpemF0aW9uIgogICAgICAgICAgICBlY2hvICIiCiAgICAgICAgICAgIGVjaG8gInwgRmllbGQgfCBWYWx1ZSB8IgogICAgICAgICAgICBlY2hvICJ8LS0tLS0tLXwtLS0tLS0tfCIKICAgICAgICAgICAgZWNobyAifCAqKkFjdG9yKiogfCBcYCR7QUNUT1J9XGAgfCIKICAgICAgICAgICAgZWNobyAifCAqKlJlcG9zaXRvcnkqKiB8IFxgJHtSRVBPfVxgIHwiCiAgICAgICAgICAgIGVjaG8gInwgKipQZXJtaXNzaW9uKiogfCBcYCR7UEVSTUlTU0lPTn1cYCB8IgogICAgICAgICAgICBlY2hvICJ8ICoqTWV0aG9kKiogfCAke01FVEhPRH0gfCIKICAgICAgICAgICAgZWNobyAifCAqKkF1dGhvcml6ZWQqKiB8ICR7QUxMT1dFRH0gfCIKICAgICAgICAgICAgZWNobyAiIgogICAgICAgICAgICBpZiBbICIkQUxMT1dFRCIgPSAidHJ1ZSIgXTsgdGhlbgogICAgICAgICAgICAgIGVjaG8gIiR7QUNUT1J9IGF1dGhvcml6ZWQgKCR7TUVUSE9EfSkiCiAgICAgICAgICAgIGVsc2UKICAgICAgICAgICAgICBlY2hvICIke0FDVE9SfSBpcyBOT1QgYXV0aG9yaXplZC4gUmVxdWlyZXMgYWRtaW4gb3IgbWFpbnRhaW4gcm9sZS4iCiAgICAgICAgICAgIGZpCiAgICAgICAgICB9ID4+ICIke0dJVEhVQl9TVEVQX1NVTU1BUll9IgoKICAgICAgLSBuYW1lOiBEZW55IGV4ZWN1dGlvbiB3aGVuIG5vdCBwZXJtaXR0ZWQKICAgICAgICBpZjogJHt7IHN0ZXBzLnBlcm0ub3V0cHV0cy5hbGxvd2VkICE9ICd0cnVlJyB9fQogICAgICAgIHJ1bjogfAogICAgICAgICAgc2V0IC1ldW8gcGlwZWZhaWwKICAgICAgICAgIHByaW50ZiAnJXNcbicgJ0VSUk9SOiBBY2Nlc3MgZGVuaWVkLiBBZG1pbiBwZXJtaXNzaW9uIHJlcXVpcmVkLicgPj4gIiR7R0lUSFVCX1NURVBfU1VNTUFSWX0iCiAgICAgICAgICBleGl0IDEKCiAgc2NyaXB0c19nb3Zlcm5hbmNlOgogICAgbmFtZTogU2NyaXB0cyBnb3Zlcm5hbmNlCiAgICBuZWVkczogYWNjZXNzX2NoZWNrCiAgICBpZjogJHt7IG5lZWRzLmFjY2Vzc19jaGVjay5vdXRwdXRzLmFsbG93ZWQgPT0gJ3RydWUnIH19CiAgICBydW5zLW9uOiB1YnVudHUtbGF0ZXN0CiAgICB0aW1lb3V0LW1pbnV0ZXM6IDE1CiAgICBwZXJtaXNzaW9uczoKICAgICAgY29udGVudHM6IHJlYWQKCiAgICBzdGVwczoKICAgICAgLSBuYW1lOiBDaGVja291dAogICAgICAgIHVzZXM6IGFjdGlvbnMvY2hlY2tvdXRAZGUwZmFjMmU0NTAwZGFiZTAwMDllNjcyMTRmZjVmNTQ0N2NlODNkZCAjIHY2CiAgICAgICAgd2l0aDoKICAgICAgICAgIGZldGNoLWRlcHRoOiAwCgogICAgICAtIG5hbWU6IFNjcmlwdHMgZm9sZGVyIGNoZWNrcwogICAgICAgIGVudjoKICAgICAgICAgIFBST0ZJTEVfUkFXOiAke3sgZ2l0aHViLmV2ZW50LmlucHV0cy5wcm9maWxlIH19CiAgICAgICAgcnVuOiB8CiAgICAgICAgICBzZXQgLWV1byBwaXBlZmFpbAoKICAgICAgICAgIHByb2ZpbGU9IiR7UFJPRklMRV9SQVc6LWFsbH0iCiAgICAgICAgICBjYXNlICIke3Byb2ZpbGV9IiBpbgogICAgICAgICAgICBhbGx8c2NyaXB0c3xyZXBvKSA7OwogICAgICAgICAgICAqKQogICAgICAgICAgICAgIHByaW50ZiAnJXNcbicgIkVSUk9SOiBVbmtub3duIHByb2ZpbGU6ICR7cHJvZmlsZX0iID4+ICIke0dJVEhVQl9TVEVQX1NVTU1BUll9IgogICAgICAgICAgICAgIGV4aXQgMQogICAgICAgICAgICAgIDs7CiAgICAgICAgICBlc2FjCgogICAgICAgICAgaWYgWyAiJHtwcm9maWxlfSIgPSAncmVwbycgXTsgdGhlbgogICAgICAgICAgICB7CiAgICAgICAgICAgICAgcHJpbnRmICclc1xuJyAnIyMjIFNjcmlwdHMgZ292ZXJuYW5jZScKICAgICAgICAgICAgICBwcmludGYgJyVzXG4nICJQcm9maWxlOiAke3Byb2ZpbGV9IgogICAgICAgICAgICAgIHByaW50ZiAnJXNcbicgJ1N0YXR1czogU0tJUFBFRCcKICAgICAgICAgICAgICBwcmludGYgJyVzXG4nICdSZWFzb246IHByb2ZpbGUgZXhjbHVkZXMgc2NyaXB0cyBnb3Zlcm5hbmNlJwogICAgICAgICAgICAgIHByaW50ZiAnXG4nCiAgICAgICAgICAgIH0gPj4gIiR7R0lUSFVCX1NURVBfU1VNTUFSWX0iCiAgICAgICAgICAgIGV4aXQgMAogICAgICAgICAgZmkKCiAgICAgICAgICBpZiBbICEgLWQgIiR7U0NSSVBUX0RJUn0iIF07IHRoZW4KICAgICAgICAgICAgewogICAgICAgICAgICAgIHByaW50ZiAnJXNcbicgJyMjIyBTY3JpcHRzIGdvdmVybmFuY2UnCiAgICAgICAgICAgICAgcHJpbnRmICclc1xuJyAnU3RhdHVzOiBPSyAoYWR2aXNvcnkpJwogICAgICAgICAgICAgIHByaW50ZiAnJXNcbicgJ3NjcmlwdHMvIGRpcmVjdG9yeSBub3QgcHJlc2VudC4gTm8gc2NyaXB0cyBnb3Zlcm5hbmNlIGVuZm9yY2VkLicKICAgICAgICAgICAgICBwcmludGYgJ1xuJwogICAgICAgICAgICB9ID4+ICIke0dJVEhVQl9TVEVQX1NVTU1BUll9IgogICAgICAgICAgICBleGl0IDAKICAgICAgICAgIGZpCgogICAgICAgICAgaWYgWyAtbiAiJHtTQ1JJUFRTX1JFUVVJUkVEX0RJUlM6LX0iIF07IHRoZW4gSUZTPScsJyByZWFkIC1yIC1hIHJlcXVpcmVkX2RpcnMgPDw8ICIke1NDUklQVFNfUkVRVUlSRURfRElSU30iOyBlbHNlIHJlcXVpcmVkX2RpcnM9KCk7IGZpCiAgICAgICAgICBJRlM9JywnIHJlYWQgLXIgLWEgYWxsb3dlZF9kaXJzIDw8PCAiJHtTQ1JJUFRTX0FMTE9XRURfRElSU30iCgogICAgICAgICAgbWlzc2luZ19kaXJzPSgpCiAgICAgICAgICB1bmFwcHJvdmVkX2RpcnM9KCkKCiAgICAgICAgICBmb3IgZCBpbiAiJHtyZXF1aXJlZF9kaXJzW0BdfSI7IGRvCiAgICAgICAgICAgIHJlcT0iJHtkJS99IgogICAgICAgICAgICBbICEgLWQgIiR7cmVxfSIgXSAmJiBtaXNzaW5nX2RpcnMrPSgiJHtyZXF9LyIpCiAgICAgICAgICBkb25lCgogICAgICAgICAgd2hpbGUgSUZTPSByZWFkIC1yIGQ7IGRvCiAgICAgICAgICAgIGFsbG93ZWQ9ZmFsc2UKICAgICAgICAgICAgZm9yIGEgaW4gIiR7YWxsb3dlZF9kaXJzW0BdfSI7IGRvCiAgICAgICAgICAgICAgYV9ub3JtPSIke2ElL30iCiAgICAgICAgICAgICAgWyAiJHtkJS99IiA9ICIke2Ffbm9ybX0iIF0gJiYgYWxsb3dlZD10cnVlCiAgICAgICAgICAgIGRvbmUKICAgICAgICAgICAgWyAiJHthbGxvd2VkfSIgPSBmYWxzZSBdICYmIHVuYXBwcm92ZWRfZGlycys9KCIke2QlL30vIikKICAgICAgICAgIGRvbmUgPCA8KGZpbmQgIiR7U0NSSVBUX0RJUn0iIC1tYXhkZXB0aCAxIC1taW5kZXB0aCAxIC10eXBlIGQgMj4vZGV2L251bGwgfCBzZWQgJ3MjXlwuLyMjJykKCiAgICAgICAgICB7CiAgICAgICAgICAgIHByaW50ZiAnJXNcbicgJyMjIyBTY3JpcHRzIGdvdmVybmFuY2UnCiAgICAgICAgICAgIHByaW50ZiAnJXNcbicgIlByb2ZpbGU6ICR7cHJvZmlsZX0iCiAgICAgICAgICAgIHByaW50ZiAnJXNcbicgJ3wgQXJlYSB8IFN0YXR1cyB8IE5vdGVzIHwnCiAgICAgICAgICAgIHByaW50ZiAnJXNcbicgJ3wtLS18LS0tfC0tLXwnCgogICAgICAgICAgICBpZiBbICIkeyNtaXNzaW5nX2RpcnNbQF19IiAtZ3QgMCBdOyB0aGVuCiAgICAgICAgICAgICAgcHJpbnRmICclc1xuJyAnfCBSZXF1aXJlZCBkaXJlY3RvcmllcyB8IFdhcm5pbmcgfCBNaXNzaW5nIHJlcXVpcmVkIHN1YmZvbGRlcnMgfCcKICAgICAgICAgICAgZWxzZQogICAgICAgICAgICAgIHByaW50ZiAnJXNcbicgJ3wgUmVxdWlyZWQgZGlyZWN0b3JpZXMgfCBPSyB8IEFsbCByZXF1aXJlZCBzdWJmb2xkZXJzIHByZXNlbnQgfCcKICAgICAgICAgICAgZmkKCiAgICAgICAgICAgIGlmIFsgIiR7I3VuYXBwcm92ZWRfZGlyc1tAXX0iIC1ndCAwIF07IHRoZW4KICAgICAgICAgICAgICBwcmludGYgJyVzXG4nICd8IERpcmVjdG9yeSBwb2xpY3kgfCBXYXJuaW5nIHwgVW5hcHByb3ZlZCBkaXJlY3RvcmllcyBkZXRlY3RlZCB8JwogICAgICAgICAgICBlbHNlCiAgICAgICAgICAgICAgcHJpbnRmICclc1xuJyAnfCBEaXJlY3RvcnkgcG9saWN5IHwgT0sgfCBObyB1bmFwcHJvdmVkIGRpcmVjdG9yaWVzIHwnCiAgICAgICAgICAgIGZpCgogICAgICAgICAgICBwcmludGYgJyVzXG4nICd8IEVuZm9yY2VtZW50IG1vZGUgfCBBZHZpc29yeSB8IHNjcmlwdHMgZm9sZGVyIGlzIG9wdGlvbmFsIHwnCiAgICAgICAgICAgIHByaW50ZiAnXG4nCgogICAgICAgICAgICBpZiBbICIkeyNtaXNzaW5nX2RpcnNbQF19IiAtZ3QgMCBdOyB0aGVuCiAgICAgICAgICAgICAgcHJpbnRmICclc1xuJyAnTWlzc2luZyByZXF1aXJlZCBzY3JpcHQgZGlyZWN0b3JpZXM6JwogICAgICAgICAgICAgIGZvciBtIGluICIke21pc3NpbmdfZGlyc1tAXX0iOyBkbyBwcmludGYgJyVzXG4nICItICR7bX0iOyBkb25lCiAgICAgICAgICAgICAgcHJpbnRmICdcbicKICAgICAgICAgICAgZWxzZQogICAgICAgICAgICAgIHByaW50ZiAnJXNcbicgJ01pc3NpbmcgcmVxdWlyZWQgc2NyaXB0IGRpcmVjdG9yaWVzOiBub25lLicKICAgICAgICAgICAgICBwcmludGYgJ1xuJwogICAgICAgICAgICBmaQoKICAgICAgICAgICAgaWYgWyAiJHsjdW5hcHByb3ZlZF9kaXJzW0BdfSIgLWd0IDAgXTsgdGhlbgogICAgICAgICAgICAgIHByaW50ZiAnJXNcbicgJ1VuYXBwcm92ZWQgc2NyaXB0IGRpcmVjdG9yaWVzIGRldGVjdGVkOicKICAgICAgICAgICAgICBmb3IgbSBpbiAiJHt1bmFwcHJvdmVkX2RpcnNbQF19IjsgZG8gcHJpbnRmICclc1xuJyAiLSAke219IjsgZG9uZQogICAgICAgICAgICAgIHByaW50ZiAnXG4nCiAgICAgICAgICAgIGVsc2UKICAgICAgICAgICAgICBwcmludGYgJyVzXG4nICdVbmFwcHJvdmVkIHNjcmlwdCBkaXJlY3RvcmllcyBkZXRlY3RlZDogbm9uZS4nCiAgICAgICAgICAgICAgcHJpbnRmICdcbicKICAgICAgICAgICAgZmkKCiAgICAgICAgICAgIHByaW50ZiAnJXNcbicgJ1NjcmlwdHMgZ292ZXJuYW5jZSBjb21wbGV0ZWQgaW4gYWR2aXNvcnkgbW9kZS4nCiAgICAgICAgICAgIHByaW50ZiAnXG4nCiAgICAgICAgICB9ID4+ICIke0dJVEhVQl9TVEVQX1NVTU1BUll9IgoKICByZXBvX2hlYWx0aDoKICAgIG5hbWU6IFJlcG9zaXRvcnkgaGVhbHRoCiAgICBuZWVkczogYWNjZXNzX2NoZWNrCiAgICBpZjogJHt7IG5lZWRzLmFjY2Vzc19jaGVjay5vdXRwdXRzLmFsbG93ZWQgPT0gJ3RydWUnIH19CiAgICBydW5zLW9uOiB1YnVudHUtbGF0ZXN0CiAgICB0aW1lb3V0LW1pbnV0ZXM6IDIwCiAgICBwZXJtaXNzaW9uczoKICAgICAgY29udGVudHM6IHJlYWQKCiAgICBzdGVwczoKICAgICAgLSBuYW1lOiBDaGVja291dAogICAgICAgIHVzZXM6IGFjdGlvbnMvY2hlY2tvdXRAZGUwZmFjMmU0NTAwZGFiZTAwMDllNjcyMTRmZjVmNTQ0N2NlODNkZCAjIHY2CiAgICAgICAgd2l0aDoKICAgICAgICAgIGZldGNoLWRlcHRoOiAwCgogICAgICAtIG5hbWU6IFJlcG9zaXRvcnkgaGVhbHRoIGNoZWNrcwogICAgICAgIGVudjoKICAgICAgICAgIFBST0ZJTEVfUkFXOiAke3sgZ2l0aHViLmV2ZW50LmlucHV0cy5wcm9maWxlIH19CiAgICAgICAgcnVuOiB8CiAgICAgICAgICBzZXQgLWV1byBwaXBlZmFpbAoKICAgICAgICAgIHByb2ZpbGU9IiR7UFJPRklMRV9SQVc6LWFsbH0iCiAgICAgICAgICBjYXNlICIke3Byb2ZpbGV9IiBpbgogICAgICAgICAgICBhbGx8c2NyaXB0c3xyZXBvKSA7OwogICAgICAgICAgICAqKQogICAgICAgICAgICAgIHByaW50ZiAnJXNcbicgIkVSUk9SOiBVbmtub3duIHByb2ZpbGU6ICR7cHJvZmlsZX0iID4+ICIke0dJVEhVQl9TVEVQX1NVTU1BUll9IgogICAgICAgICAgICAgIGV4aXQgMQogICAgICAgICAgICAgIDs7CiAgICAgICAgICBlc2FjCgogICAgICAgICAgaWYgWyAiJHtwcm9maWxlfSIgPSAnc2NyaXB0cycgXTsgdGhlbgogICAgICAgICAgICB7CiAgICAgICAgICAgICAgcHJpbnRmICclc1xuJyAnIyMjIFJlcG9zaXRvcnkgaGVhbHRoJwogICAgICAgICAgICAgIHByaW50ZiAnJXNcbicgIlByb2ZpbGU6ICR7cHJvZmlsZX0iCiAgICAgICAgICAgICAgcHJpbnRmICclc1xuJyAnU3RhdHVzOiBTS0lQUEVEJwogICAgICAgICAgICAgIHByaW50ZiAnJXNcbicgJ1JlYXNvbjogcHJvZmlsZSBleGNsdWRlcyByZXBvc2l0b3J5IGhlYWx0aCcKICAgICAgICAgICAgICBwcmludGYgJ1xuJwogICAgICAgICAgICB9ID4+ICIke0dJVEhVQl9TVEVQX1NVTU1BUll9IgogICAgICAgICAgICBleGl0IDAKICAgICAgICAgIGZpCgogICAgICAgICAgSUZTPScsJyByZWFkIC1yIC1hIHJlcXVpcmVkX2FydGlmYWN0cyA8PDwgIiR7UkVQT19SRVFVSVJFRF9BUlRJRkFDVFN9IgogICAgICAgICAgSUZTPScsJyByZWFkIC1yIC1hIG9wdGlvbmFsX2ZpbGVzIDw8PCAiJHtSRVBPX09QVElPTkFMX0ZJTEVTfSIKICAgICAgICAgIGlmIFsgLW4gIiR7UkVQT19ESVNBTExPV0VEX0RJUlM6LX0iIF07IHRoZW4gSUZTPScsJyByZWFkIC1yIC1hIGRpc2FsbG93ZWRfZGlycyA8PDwgIiR7UkVQT19ESVNBTExPV0VEX0RJUlN9IjsgZWxzZSBkaXNhbGxvd2VkX2RpcnM9KCk7IGZpCiAgICAgICAgICBJRlM9JywnIHJlYWQgLXIgLWEgZGlzYWxsb3dlZF9maWxlcyA8PDwgIiR7UkVQT19ESVNBTExPV0VEX0ZJTEVTOi19IgoKICAgICAgICAgIG1pc3NpbmdfcmVxdWlyZWQ9KCkKICAgICAgICAgIG1pc3Npbmdfb3B0aW9uYWw9KCkKCiAgICAgICAgICAjIFNvdXJjZSBkaXJlY3Rvcnk6IHNyYy8gb3IgaHRkb2NzLyAoZWl0aGVyIGlzIHZhbGlkIGZvciBleHRlbnNpb24gcmVwb3MpCiAgICAgICAgICBTT1VSQ0VfRElSPSIiCiAgICAgICAgICBpZiBbIC1kICJzcmMiIF07IHRoZW4KICAgICAgICAgICAgU09VUkNFX0RJUj0ic3JjIgogICAgICAgICAgZWxpZiBbIC1kICJodGRvY3MiIF07IHRoZW4KICAgICAgICAgICAgU09VUkNFX0RJUj0iaHRkb2NzIgogICAgICAgICAgZWxpZiBbIC1kICJkZXBsb3kiIF0gfHwgWyAtZCAiY2xpIiBdIHx8IFsgLWQgIm1vbml0b3JpbmciIF07IHRoZW4KICAgICAgICAgICAgIyBQbGF0Zm9ybS90b29saW5nIHJlcG9zIGRvbid0IG5lZWQgc3JjLwogICAgICAgICAgICBTT1VSQ0VfRElSPSIiCiAgICAgICAgICBlbHNlCiAgICAgICAgICAgIG1pc3NpbmdfcmVxdWlyZWQrPSgic3JjLyBvciBodGRvY3MvIChzb3VyY2UgZGlyZWN0b3J5IHJlcXVpcmVkKSIpCiAgICAgICAgICBmaQoKICAgICAgICAgIGZvciBpdGVtIGluICIke3JlcXVpcmVkX2FydGlmYWN0c1tAXX0iOyBkbwogICAgICAgICAgICBpZiBwcmludGYgJyVzJyAiJHtpdGVtfSIgfCBncmVwIC1xICcvJCc7IHRoZW4KICAgICAgICAgICAgICBkPSIke2l0ZW0lL30iCiAgICAgICAgICAgICAgWyAhIC1kICIke2R9IiBdICYmIG1pc3NpbmdfcmVxdWlyZWQrPSgiJHtpdGVtfSIpCiAgICAgICAgICAgIGVsc2UKICAgICAgICAgICAgICBbICEgLWYgIiR7aXRlbX0iIF0gJiYgbWlzc2luZ19yZXF1aXJlZCs9KCIke2l0ZW19IikKICAgICAgICAgICAgZmkKICAgICAgICAgIGRvbmUKCiAgICAgICAgICBmb3IgZiBpbiAiJHtvcHRpb25hbF9maWxlc1tAXX0iOyBkbwogICAgICAgICAgICBpZiBwcmludGYgJyVzJyAiJHtmfSIgfCBncmVwIC1xICcvJCc7IHRoZW4KICAgICAgICAgICAgICBkPSIke2YlL30iCiAgICAgICAgICAgICAgWyAhIC1kICIke2R9IiBdICYmIG1pc3Npbmdfb3B0aW9uYWwrPSgiJHtmfSIpCiAgICAgICAgICAgIGVsc2UKICAgICAgICAgICAgICBbICEgLWYgIiR7Zn0iIF0gJiYgbWlzc2luZ19vcHRpb25hbCs9KCIke2Z9IikKICAgICAgICAgICAgZmkKICAgICAgICAgIGRvbmUKCiAgICAgICAgICBmb3IgZCBpbiAiJHtkaXNhbGxvd2VkX2RpcnNbQF19IjsgZG8KICAgICAgICAgICAgZF9ub3JtPSIke2QlL30iCiAgICAgICAgICAgIFsgLWQgIiR7ZF9ub3JtfSIgXSAmJiBtaXNzaW5nX3JlcXVpcmVkKz0oIiR7ZF9ub3JtfS8gKGRpc2FsbG93ZWQpIikKICAgICAgICAgIGRvbmUKCiAgICAgICAgICBmb3IgZiBpbiAiJHtkaXNhbGxvd2VkX2ZpbGVzW0BdfSI7IGRvCiAgICAgICAgICAgIFsgLWYgIiR7Zn0iIF0gJiYgbWlzc2luZ19yZXF1aXJlZCs9KCIke2Z9IChkaXNhbGxvd2VkKSIpCiAgICAgICAgICBkb25lCgogICAgICAgICAgZ2l0IGZldGNoIG9yaWdpbiAtLXBydW5lCgogICAgICAgICAgZGV2X3BhdGhzPSgpCiAgICAgICAgICBkZXZfYnJhbmNoZXM9KCkKCiAgICAgICAgICB3aGlsZSBJRlM9IHJlYWQgLXIgYjsgZG8KICAgICAgICAgICAgbmFtZT0iJHtiI29yaWdpbi99IgogICAgICAgICAgICBpZiBbICIke25hbWV9IiA9ICdkZXYnIF07IHRoZW4KICAgICAgICAgICAgICBkZXZfYnJhbmNoZXMrPSgiJHtuYW1lfSIpCiAgICAgICAgICAgIGVsc2UKICAgICAgICAgICAgICBkZXZfcGF0aHMrPSgiJHtuYW1lfSIpCiAgICAgICAgICAgIGZpCiAgICAgICAgICBkb25lIDwgPChnaXQgYnJhbmNoIC1yIC0tbGlzdCAnb3JpZ2luL2RldionIHwgc2VkICdzL14gKi8vJykKCiAgICAgICAgICBpZiBbICIkeyNkZXZfcGF0aHNbQF19IiAtZXEgMCBdICYmIFsgIiR7I2Rldl9icmFuY2hlc1tAXX0iIC1lcSAwIF07IHRoZW4KICAgICAgICAgICAgbWlzc2luZ19yZXF1aXJlZCs9KCJkZXYgb3IgZGV2LyogYnJhbmNoIikKICAgICAgICAgIGZpCgogICAgICAgICAgY29udGVudF93YXJuaW5ncz0oKQoKICAgICAgICAgIGlmIFsgLWYgJ0NIQU5HRUxPRy5tZCcgXSAmJiAhIGdyZXAgLUVxICdeIyBDaGFuZ2Vsb2cnIENIQU5HRUxPRy5tZDsgdGhlbgogICAgICAgICAgICBjb250ZW50X3dhcm5pbmdzKz0oIkNIQU5HRUxPRy5tZCBtaXNzaW5nICcjIENoYW5nZWxvZycgaGVhZGVyIikKICAgICAgICAgIGZpCgogICAgICAgICAgaWYgWyAtZiAnQ0hBTkdFTE9HLm1kJyBdICYmIGdyZXAgLUVxICdeWyMgXSpVbnJlbGVhc2VkJyBDSEFOR0VMT0cubWQ7IHRoZW4KICAgICAgICAgICAgY29udGVudF93YXJuaW5ncys9KCJDSEFOR0VMT0cubWQgY29udGFpbnMgVW5yZWxlYXNlZCBzZWN0aW9uIChyZXZpZXcgcmVsZWFzZSByZWFkaW5lc3MpIikKICAgICAgICAgIGZpCgogICAgICAgICAgaWYgWyAtZiAnTElDRU5TRScgXSAmJiAhIGdyZXAgLXFpRSAnR05VIEdFTkVSQUwgUFVCTElDIExJQ0VOU0V8R1BMJyBMSUNFTlNFOyB0aGVuCiAgICAgICAgICAgIGNvbnRlbnRfd2FybmluZ3MrPSgiTElDRU5TRSBkb2VzIG5vdCBsb29rIGxpa2UgYSBHUEwgdGV4dCIpCiAgICAgICAgICBmaQoKICAgICAgICAgIGlmIFsgLWYgJ1JFQURNRS5tZCcgXSAmJiAhIGdyZXAgLXFpRSAnbW9rb3xNb2tvJyBSRUFETUUubWQ7IHRoZW4KICAgICAgICAgICAgY29udGVudF93YXJuaW5ncys9KCJSRUFETUUubWQgbWlzc2luZyBleHBlY3RlZCBicmFuZCBrZXl3b3JkIikKICAgICAgICAgIGZpCgogICAgICAgICAgZXhwb3J0IFBST0ZJTEVfUkFXPSIke3Byb2ZpbGV9IgogICAgICAgICAgZXhwb3J0IE1JU1NJTkdfUkVRVUlSRUQ9IiQocHJpbnRmICclc1xuJyAiJHttaXNzaW5nX3JlcXVpcmVkW0BdOi19IikiCiAgICAgICAgICBleHBvcnQgTUlTU0lOR19PUFRJT05BTD0iJChwcmludGYgJyVzXG4nICIke21pc3Npbmdfb3B0aW9uYWxbQF06LX0iKSIKICAgICAgICAgIGV4cG9ydCBDT05URU5UX1dBUk5JTkdTPSIkKHByaW50ZiAnJXNcbicgIiR7Y29udGVudF93YXJuaW5nc1tAXTotfSIpIgoKICAgICAgICAgIHJlcG9ydF9qc29uPSQocHJpbnRmICd7InByb2ZpbGUiOiIlcyIsIm1pc3NpbmdfcmVxdWlyZWQiOiVkLCJtaXNzaW5nX29wdGlvbmFsIjolZCwiY29udGVudF93YXJuaW5ncyI6JWR9JyAgICAgICAgICAgICAiJHByb2ZpbGUiICIkeyNtaXNzaW5nX3JlcXVpcmVkW0BdfSIgIiR7I21pc3Npbmdfb3B0aW9uYWxbQF19IiAiJHsjY29udGVudF93YXJuaW5nc1tAXX0iKQoKICAgICAgICAgIHsKICAgICAgICAgICAgcHJpbnRmICclc1xuJyAnIyMjIFJlcG9zaXRvcnkgaGVhbHRoJwogICAgICAgICAgICBwcmludGYgJyVzXG4nICJQcm9maWxlOiAke3Byb2ZpbGV9IgogICAgICAgICAgICBwcmludGYgJyVzXG4nICd8IE1ldHJpYyB8IFZhbHVlIHwnCiAgICAgICAgICAgIHByaW50ZiAnJXNcbicgJ3wtLS18LS0tfCcKICAgICAgICAgICAgcHJpbnRmICclc1xuJyAifCBNaXNzaW5nIHJlcXVpcmVkIHwgJHsjbWlzc2luZ19yZXF1aXJlZFtAXX0gfCIKICAgICAgICAgICAgcHJpbnRmICclc1xuJyAifCBNaXNzaW5nIG9wdGlvbmFsIHwgJHsjbWlzc2luZ19vcHRpb25hbFtAXX0gfCIKICAgICAgICAgICAgcHJpbnRmICclc1xuJyAifCBDb250ZW50IHdhcm5pbmdzIHwgJHsjY29udGVudF93YXJuaW5nc1tAXX0gfCIKICAgICAgICAgICAgcHJpbnRmICdcbicKCiAgICAgICAgICAgIHByaW50ZiAnJXNcbicgJyMjIyBHdWFyZHJhaWxzIHJlcG9ydCAoSlNPTiknCiAgICAgICAgICAgIHByaW50ZiAnJXNcbicgJ2BgYGpzb24nCiAgICAgICAgICAgIHByaW50ZiAnJXNcbicgIiR7cmVwb3J0X2pzb259IgogICAgICAgICAgICBwcmludGYgJyVzXG4nICdgYGAnCiAgICAgICAgICAgIHByaW50ZiAnXG4nCiAgICAgICAgICB9ID4+ICIke0dJVEhVQl9TVEVQX1NVTU1BUll9IgoKICAgICAgICAgIGlmIFsgIiR7I21pc3NpbmdfcmVxdWlyZWRbQF19IiAtZ3QgMCBdOyB0aGVuCiAgICAgICAgICAgIHsKICAgICAgICAgICAgICBwcmludGYgJyVzXG4nICcjIyMgTWlzc2luZyByZXF1aXJlZCByZXBvIGFydGlmYWN0cycKICAgICAgICAgICAgICBmb3IgbSBpbiAiJHttaXNzaW5nX3JlcXVpcmVkW0BdfSI7IGRvIHByaW50ZiAnJXNcbicgIi0gJHttfSI7IGRvbmUKICAgICAgICAgICAgICBwcmludGYgJyVzXG4nICdFUlJPUjogR3VhcmRyYWlscyBmYWlsZWQuIE1pc3NpbmcgcmVxdWlyZWQgcmVwb3NpdG9yeSBhcnRpZmFjdHMuJwogICAgICAgICAgICAgIHByaW50ZiAnXG4nCiAgICAgICAgICAgIH0gPj4gIiR7R0lUSFVCX1NURVBfU1VNTUFSWX0iCiAgICAgICAgICAgIGV4aXQgMQogICAgICAgICAgZmkKCiAgICAgICAgICBpZiBbICIkeyNtaXNzaW5nX29wdGlvbmFsW0BdfSIgLWd0IDAgXTsgdGhlbgogICAgICAgICAgICB7CiAgICAgICAgICAgICAgcHJpbnRmICclc1xuJyAnIyMjIE1pc3Npbmcgb3B0aW9uYWwgcmVwbyBhcnRpZmFjdHMnCiAgICAgICAgICAgICAgZm9yIG0gaW4gIiR7bWlzc2luZ19vcHRpb25hbFtAXX0iOyBkbyBwcmludGYgJyVzXG4nICItICR7bX0iOyBkb25lCiAgICAgICAgICAgICAgcHJpbnRmICdcbicKICAgICAgICAgICAgfSA+PiAiJHtHSVRIVUJfU1RFUF9TVU1NQVJZfSIKICAgICAgICAgIGZpCgogICAgICAgICAgaWYgWyAiJHsjY29udGVudF93YXJuaW5nc1tAXX0iIC1ndCAwIF07IHRoZW4KICAgICAgICAgICAgewogICAgICAgICAgICAgIHByaW50ZiAnJXNcbicgJyMjIyBSZXBvIGNvbnRlbnQgd2FybmluZ3MnCiAgICAgICAgICAgICAgZm9yIG0gaW4gIiR7Y29udGVudF93YXJuaW5nc1tAXX0iOyBkbyBwcmludGYgJyVzXG4nICItICR7bX0iOyBkb25lCiAgICAgICAgICAgICAgcHJpbnRmICdcbicKICAgICAgICAgICAgfSA+PiAiJHtHSVRIVUJfU1RFUF9TVU1NQVJZfSIKICAgICAgICAgIGZpCgogICAgICAgICAgIyAtLSBKb29tbGEtc3BlY2lmaWMgY2hlY2tzIC0tCiAgICAgICAgICBqb29tbGFfZmluZGluZ3M9KCkKCiAgICAgICAgICBNQU5JRkVTVD0iJChmaW5kIC4gLW1heGRlcHRoIDIgLW5hbWUgJyoueG1sJyAtZXhlYyBncmVwIC1sICc8ZXh0ZW5zaW9uJyB7fSBcOyAyPi9kZXYvbnVsbCB8IGhlYWQgLTEgfHwgdHJ1ZSkiCiAgICAgICAgICBpZiBbIC16ICIke01BTklGRVNUfSIgXTsgdGhlbgogICAgICAgICAgICBqb29tbGFfZmluZGluZ3MrPSgiSm9vbWxhIFhNTCBtYW5pZmVzdCBub3QgZm91bmQgKG5vICoueG1sIHdpdGggPGV4dGVuc2lvbj4gdGFnKSIpCiAgICAgICAgICBlbHNlCiAgICAgICAgICAgIGlmICEgZ3JlcCAtcVAgJzx2ZXJzaW9uPicgIiR7TUFOSUZFU1R9IjsgdGhlbgogICAgICAgICAgICAgIGpvb21sYV9maW5kaW5ncys9KCJYTUwgbWFuaWZlc3Q6IDx2ZXJzaW9uPiB0YWcgbWlzc2luZyIpCiAgICAgICAgICAgIGZpCiAgICAgICAgICAgIGlmICEgZ3JlcCAtcVAgJ3R5cGU9Iihjb21wb25lbnR8bW9kdWxlfHBsdWdpbnxsaWJyYXJ5fHBhY2thZ2V8dGVtcGxhdGV8bGFuZ3VhZ2UpIicgIiR7TUFOSUZFU1R9IjsgdGhlbgogICAgICAgICAgICAgIGpvb21sYV9maW5kaW5ncys9KCJYTUwgbWFuaWZlc3Q6IHR5cGUgYXR0cmlidXRlIG1pc3Npbmcgb3IgaW52YWxpZCIpCiAgICAgICAgICAgIGZpCiAgICAgICAgICAgIGlmICEgZ3JlcCAtcVAgJzxuYW1lPicgIiR7TUFOSUZFU1R9IjsgdGhlbgogICAgICAgICAgICAgIGpvb21sYV9maW5kaW5ncys9KCJYTUwgbWFuaWZlc3Q6IDxuYW1lPiB0YWcgbWlzc2luZyIpCiAgICAgICAgICAgIGZpCiAgICAgICAgICAgIGlmICEgZ3JlcCAtcVAgJzxhdXRob3I+JyAiJHtNQU5JRkVTVH0iOyB0aGVuCiAgICAgICAgICAgICAgam9vbWxhX2ZpbmRpbmdzKz0oIlhNTCBtYW5pZmVzdDogPGF1dGhvcj4gdGFnIG1pc3NpbmciKQogICAgICAgICAgICBmaQogICAgICAgICAgICBpZiAhIGdyZXAgLXFQICc8bmFtZXNwYWNlJyAiJHtNQU5JRkVTVH0iOyB0aGVuCiAgICAgICAgICAgICAgam9vbWxhX2ZpbmRpbmdzKz0oIlhNTCBtYW5pZmVzdDogPG5hbWVzcGFjZT4gbWlzc2luZyAocmVxdWlyZWQgZm9yIEpvb21sYSA1KykiKQogICAgICAgICAgICBmaQogICAgICAgICAgZmkKCiAgICAgICAgICBJTklfQ09VTlQ9IiQoZmluZCAuIC1uYW1lICcqLmluaScgLXR5cGUgZiAyPi9kZXYvbnVsbCB8IHdjIC1sKSIKICAgICAgICAgIGlmIFsgIiR7SU5JX0NPVU5UfSIgLWVxIDAgXTsgdGhlbgogICAgICAgICAgICBqb29tbGFfZmluZGluZ3MrPSgiTm8gLmluaSBsYW5ndWFnZSBmaWxlcyBmb3VuZCIpCiAgICAgICAgICBmaQoKICAgICAgICAgIGlmIFsgISAtZiAndXBkYXRlcy54bWwnIF07IHRoZW4KICAgICAgICAgICAgam9vbWxhX2ZpbmRpbmdzKz0oInVwZGF0ZXMueG1sIG1pc3NpbmcgaW4gcm9vdCAocmVxdWlyZWQgZm9yIEpvb21sYSB1cGRhdGUgc2VydmVyKSIpCiAgICAgICAgICBmaQoKICAgICAgICAgIGlmIFsgLW4gIiR7U09VUkNFX0RJUn0iIF07IHRoZW4KICAgICAgICAgICAgSU5ERVhfRElSUz0oIiR7U09VUkNFX0RJUn0iICIke1NPVVJDRV9ESVJ9L2FkbWluIiAiJHtTT1VSQ0VfRElSfS9zaXRlIikKICAgICAgICAgICAgZm9yIGRpciBpbiAiJHtJTkRFWF9ESVJTW0BdfSI7IGRvCiAgICAgICAgICAgICAgaWYgWyAtZCAiJHtkaXJ9IiBdICYmIFsgISAtZiAiJHtkaXJ9L2luZGV4Lmh0bWwiIF07IHRoZW4KICAgICAgICAgICAgICAgIGpvb21sYV9maW5kaW5ncys9KCIke2Rpcn0vaW5kZXguaHRtbCBtaXNzaW5nIChkaXJlY3RvcnkgbGlzdGluZyBwcm90ZWN0aW9uKSIpCiAgICAgICAgICAgICAgZmkKICAgICAgICAgICAgZG9uZQogICAgICAgICAgZmkKCiAgICAgICAgICBpZiBbICIkeyNqb29tbGFfZmluZGluZ3NbQF19IiAtZ3QgMCBdOyB0aGVuCiAgICAgICAgICAgIHsKICAgICAgICAgICAgICBwcmludGYgJyVzXG4nICcjIyMgSm9vbWxhIGV4dGVuc2lvbiBjaGVja3MnCiAgICAgICAgICAgICAgcHJpbnRmICclc1xuJyAnfCBDaGVjayB8IFN0YXR1cyB8JwogICAgICAgICAgICAgIHByaW50ZiAnJXNcbicgJ3wtLS18LS0tfCcKICAgICAgICAgICAgICBmb3IgZiBpbiAiJHtqb29tbGFfZmluZGluZ3NbQF19IjsgZG8KICAgICAgICAgICAgICAgIHByaW50ZiAnJXNcbicgInwgJHtmfSB8IFdhcm5pbmcgfCIKICAgICAgICAgICAgICBkb25lCiAgICAgICAgICAgICAgcHJpbnRmICdcbicKICAgICAgICAgICAgfSA+PiAiJHtHSVRIVUJfU1RFUF9TVU1NQVJZfSIKICAgICAgICAgIGVsc2UKICAgICAgICAgICAgewogICAgICAgICAgICAgIHByaW50ZiAnJXNcbicgJyMjIyBKb29tbGEgZXh0ZW5zaW9uIGNoZWNrcycKICAgICAgICAgICAgICBwcmludGYgJyVzXG4nICdBbGwgSm9vbWxhLXNwZWNpZmljIGNoZWNrcyBwYXNzZWQuJwogICAgICAgICAgICAgIHByaW50ZiAnXG4nCiAgICAgICAgICAgIH0gPj4gIiR7R0lUSFVCX1NURVBfU1VNTUFSWX0iCiAgICAgICAgICBmaQoKICAgICAgICAgIGV4dGVuZGVkX2VuYWJsZWQ9IiR7RVhURU5ERURfQ0hFQ0tTOi10cnVlfSIKICAgICAgICAgIGV4dGVuZGVkX2ZpbmRpbmdzPSgpCgogICAgICAgICAgaWYgWyAiJHtleHRlbmRlZF9lbmFibGVkfSIgPSAndHJ1ZScgXTsgdGhlbgogICAgICAgICAgICBpZiBbIC1mICcuZ2l0aHViL0NPREVPV05FUlMnIF0gfHwgWyAtZiAnQ09ERU9XTkVSUycgXSB8fCBbIC1mICdkb2NzL0NPREVPV05FUlMnIF07IHRoZW4KICAgICAgICAgICAgICA6CiAgICAgICAgICAgIGVsc2UKICAgICAgICAgICAgICBleHRlbmRlZF9maW5kaW5ncys9KCJDT0RFT1dORVJTIG5vdCBmb3VuZCAoLmdpdGh1Yi9DT0RFT1dORVJTIHByZWZlcnJlZCkiKQogICAgICAgICAgICBmaQoKICAgICAgICAgICAgaWYgbHMgIiR7V09SS0ZMT1dTX0RJUn0iLyoueW1sID4vZGV2L251bGwgMj4mMSB8fCBscyAiJHtXT1JLRkxPV1NfRElSfSIvKi55YW1sID4vZGV2L251bGwgMj4mMTsgdGhlbgogICAgICAgICAgICAgIGJhZF9yZWZzPSIkKGdyZXAgLVJJbiAtLWluY2x1ZGU9JyoueW1sJyAtLWluY2x1ZGU9JyoueWFtbCcgLUUgJ15bWzpzcGFjZTpdXSp1c2VzOltbOnNwYWNlOl1dKlteI10rQChtYWlufG1hc3RlcilcYicgIiR7V09SS0ZMT1dTX0RJUn0iIDI+L2Rldi9udWxsIHx8IHRydWUpIgogICAgICAgICAgICAgIGlmIFsgLW4gIiR7YmFkX3JlZnN9IiBdOyB0aGVuCiAgICAgICAgICAgICAgICBleHRlbmRlZF9maW5kaW5ncys9KCJXb3JrZmxvd3MgcmVmZXJlbmNlIGFjdGlvbnMgQG1haW4vQG1hc3RlciAocGluIHZlcnNpb25zKTogc2VlIGxvZyBleGNlcnB0IikKICAgICAgICAgICAgICAgIHsKICAgICAgICAgICAgICAgICAgcHJpbnRmICclc1xuJyAnIyMjIFdvcmtmbG93IHBpbm5pbmcgYWR2aXNvcnknCiAgICAgICAgICAgICAgICAgIHByaW50ZiAnJXNcbicgJ0ZvdW5kIHVzZXM6IGVudHJpZXMgcGlubmVkIHRvIG1haW4vbWFzdGVyOicKICAgICAgICAgICAgICAgICAgcHJpbnRmICclc1xuJyAnYGBgJwogICAgICAgICAgICAgICAgICBwcmludGYgJyVzXG4nICIke2JhZF9yZWZzfSIKICAgICAgICAgICAgICAgICAgcHJpbnRmICclc1xuJyAnYGBgJwogICAgICAgICAgICAgICAgICBwcmludGYgJ1xuJwogICAgICAgICAgICAgICAgfSA+PiAiJHtHSVRIVUJfU1RFUF9TVU1NQVJZfSIKICAgICAgICAgICAgICBmaQogICAgICAgICAgICBmaQoKICAgICAgICAgICAgaWYgWyAtZiAiJHtET0NTX0lOREVYfSIgXTsgdGhlbgogICAgICAgICAgICAgIG1pc3NpbmdfbGlua3M9IiIKICAgICAgICAgICAgICB3aGlsZSBJRlM9IHJlYWQgLXIgZG9jbGluZTsgZG8KICAgICAgICAgICAgICAgIGZvciBsaW5rIGluICQoZWNobyAiJGRvY2xpbmUiIHwgZ3JlcCAtb0UgJ1xdXChbXildK1wpJyB8IHNlZCAncy9cXSgvLycgfCBzZWQgJ3MvKSQvLycgfHwgdHJ1ZSk7IGRvCiAgICAgICAgICAgICAgICAgIGNhc2UgIiRsaW5rIiBpbiBodHRwOi8vKnxodHRwczovLyp8IiMiKnxtYWlsdG86KikgY29udGludWUgOzsgZXNhYwogICAgICAgICAgICAgICAgICBsaW5rcGF0aD0iJHtsaW5rJSUjKn0iCiAgICAgICAgICAgICAgICAgIGxpbmtwYXRoPSIke2xpbmtwYXRoJSVcPyp9IgogICAgICAgICAgICAgICAgICBbIC16ICIkbGlua3BhdGgiIF0gJiYgY29udGludWUKICAgICAgICAgICAgICAgICAgaWYgWyAiJHtsaW5rcGF0aDowOjF9IiA9ICIvIiBdOyB0aGVuCiAgICAgICAgICAgICAgICAgICAgdGVzdHBhdGg9IiR7bGlua3BhdGgjL30iCiAgICAgICAgICAgICAgICAgIGVsc2UKICAgICAgICAgICAgICAgICAgICB0ZXN0cGF0aD0iJChkaXJuYW1lICIke0RPQ1NfSU5ERVh9IikvJHtsaW5rcGF0aH0iCiAgICAgICAgICAgICAgICAgIGZpCiAgICAgICAgICAgICAgICAgIFsgISAtZSAiJHRlc3RwYXRoIiBdICYmIG1pc3NpbmdfbGlua3M9IiR7bWlzc2luZ19saW5rc30ke3Rlc3RwYXRofSAiCiAgICAgICAgICAgICAgICBkb25lCiAgICAgICAgICAgICAgZG9uZSA8ICIke0RPQ1NfSU5ERVh9IgogICAgICAgICAgICAgIGlmIFsgLW4gIiR7bWlzc2luZ19saW5rc30iIF07IHRoZW4KICAgICAgICAgICAgICAgIGV4dGVuZGVkX2ZpbmRpbmdzKz0oImRvY3MvZG9jcy1pbmRleC5tZCBjb250YWlucyBicm9rZW4gcmVsYXRpdmUgbGlua3MiKQogICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICBwcmludGYgJyVzXG4nICcjIyMgRG9jcyBpbmRleCBsaW5rIGludGVncml0eScKICAgICAgICAgICAgICAgICAgcHJpbnRmICclc1xuJyAnQnJva2VuIHJlbGF0aXZlIGxpbmtzOicKICAgICAgICAgICAgICAgICAgZm9yIGJsIGluICR7bWlzc2luZ19saW5rc307IGRvCiAgICAgICAgICAgICAgICAgICAgcHJpbnRmICclc1xuJyAiLSAke2JsfSIKICAgICAgICAgICAgICAgICAgZG9uZQogICAgICAgICAgICAgICAgICBwcmludGYgJ1xuJwogICAgICAgICAgICAgICAgfSA+PiAiJHtHSVRIVUJfU1RFUF9TVU1NQVJZfSIKICAgICAgICAgICAgICBmaQogICAgICAgICAgICBmaQoKICAgICAgICAgICAgaWYgWyAtZCAiJHtTQ1JJUFRfRElSfSIgXTsgdGhlbgogICAgICAgICAgICAgIGlmICEgY29tbWFuZCAtdiBzaGVsbGNoZWNrID4vZGV2L251bGwgMj4mMTsgdGhlbgogICAgICAgICAgICAgICAgc3VkbyBhcHQtZ2V0IHVwZGF0ZSAtcXEKICAgICAgICAgICAgICAgIHN1ZG8gYXB0LWdldCBpbnN0YWxsIC15IHNoZWxsY2hlY2sgPi9kZXYvbnVsbAogICAgICAgICAgICAgIGZpCgogICAgICAgICAgICAgIHNjX291dD0nJwogICAgICAgICAgICAgIHdoaWxlIElGUz0gcmVhZCAtciBzaGY7IGRvCiAgICAgICAgICAgICAgICBbIC16ICIke3NoZn0iIF0gJiYgY29udGludWUKICAgICAgICAgICAgICAgIG91dF9vbmU9IiQoc2hlbGxjaGVjayAtUyB3YXJuaW5nIC14ICIke3NoZn0iIDI+L2Rldi9udWxsIHx8IHRydWUpIgogICAgICAgICAgICAgICAgaWYgWyAtbiAiJHtvdXRfb25lfSIgXTsgdGhlbgogICAgICAgICAgICAgICAgICBzY19vdXQ9IiR7c2Nfb3V0fSR7b3V0X29uZX1cbiIKICAgICAgICAgICAgICAgIGZpCiAgICAgICAgICAgICAgZG9uZSA8IDwoZmluZCAiJHtTQ1JJUFRfRElSfSIgLXR5cGUgZiAtbmFtZSAiJHtTSEVMTENIRUNLX1BBVFRFUk59IiAyPi9kZXYvbnVsbCB8IHNvcnQpCgogICAgICAgICAgICAgIGlmIFsgLW4gIiR7c2Nfb3V0fSIgXTsgdGhlbgogICAgICAgICAgICAgICAgZXh0ZW5kZWRfZmluZGluZ3MrPSgiU2hlbGxDaGVjayB3YXJuaW5ncyBkZXRlY3RlZCAoYWR2aXNvcnkpIikKICAgICAgICAgICAgICAgIHNjX2hlYWQ9IiQocHJpbnRmICclcycgIiR7c2Nfb3V0fSIgfCBoZWFkIC1uIDIwMCkiCiAgICAgICAgICAgICAgICB7CiAgICAgICAgICAgICAgICAgIHByaW50ZiAnJXNcbicgJyMjIyBTaGVsbENoZWNrIChhZHZpc29yeSknCiAgICAgICAgICAgICAgICAgIHByaW50ZiAnJXNcbicgJ2BgYCcKICAgICAgICAgICAgICAgICAgcHJpbnRmICclc1xuJyAiJHtzY19oZWFkfSIKICAgICAgICAgICAgICAgICAgcHJpbnRmICclc1xuJyAnYGBgJwogICAgICAgICAgICAgICAgICBwcmludGYgJ1xuJwogICAgICAgICAgICAgICAgfSA+PiAiJHtHSVRIVUJfU1RFUF9TVU1NQVJZfSIKICAgICAgICAgICAgICBmaQogICAgICAgICAgICBmaQoKICAgICAgICAgICAgc3BkeF9taXNzaW5nPSgpCiAgICAgICAgICAgIElGUz0nLCcgcmVhZCAtciAtYSBzcGR4X2dsb2JzIDw8PCAiJHtTUERYX0ZJTEVfR0xPQlN9IgogICAgICAgICAgICBzcGR4X2FyZ3M9KCkKICAgICAgICAgICAgZm9yIGcgaW4gIiR7c3BkeF9nbG9ic1tAXX0iOyBkbyBzcGR4X2FyZ3MrPSgiJHtnfSIpOyBkb25lCgogICAgICAgICAgICB3aGlsZSBJRlM9IHJlYWQgLXIgZjsgZG8KICAgICAgICAgICAgICBbIC16ICIke2Z9IiBdICYmIGNvbnRpbnVlCiAgICAgICAgICAgICAgaWYgISBoZWFkIC1uIDQwICIke2Z9IiB8IGdyZXAgLXEgJ1NQRFgtTGljZW5zZS1JZGVudGlmaWVyOic7IHRoZW4KICAgICAgICAgICAgICAgIHNwZHhfbWlzc2luZys9KCIke2Z9IikKICAgICAgICAgICAgICBmaQogICAgICAgICAgICBkb25lIDwgPChnaXQgbHMtZmlsZXMgIiR7c3BkeF9hcmdzW0BdfSIgMj4vZGV2L251bGwgfHwgdHJ1ZSkKCiAgICAgICAgICAgIGlmIFsgIiR7I3NwZHhfbWlzc2luZ1tAXX0iIC1ndCAwIF07IHRoZW4KICAgICAgICAgICAgICBleHRlbmRlZF9maW5kaW5ncys9KCJTUERYIGhlYWRlciBtaXNzaW5nIGluIHNvbWUgdHJhY2tlZCBmaWxlcyAoYWR2aXNvcnkpIikKICAgICAgICAgICAgICB7CiAgICAgICAgICAgICAgICBwcmludGYgJyVzXG4nICcjIyMgU1BEWCBoZWFkZXIgYWR2aXNvcnknCiAgICAgICAgICAgICAgICBwcmludGYgJyVzXG4nICdGaWxlcyBtaXNzaW5nIFNQRFgtTGljZW5zZS1JZGVudGlmaWVyIChmaXJzdCA0MCBsaW5lcyBzY2FuKTonCiAgICAgICAgICAgICAgICBmb3IgZiBpbiAiJHtzcGR4X21pc3NpbmdbQF19IjsgZG8gcHJpbnRmICclc1xuJyAiLSAke2Z9IjsgZG9uZQogICAgICAgICAgICAgICAgcHJpbnRmICdcbicKICAgICAgICAgICAgICB9ID4+ICIke0dJVEhVQl9TVEVQX1NVTU1BUll9IgogICAgICAgICAgICBmaQoKICAgICAgICAgICAgc3RhbGVfY3V0b2ZmX2RheXM9MTgwCiAgICAgICAgICAgIHN0YWxlX2JyYW5jaGVzPSIkKGdpdCBmb3ItZWFjaC1yZWYgLS1mb3JtYXQ9JyUocmVmbmFtZTpzaG9ydCkgJShjb21taXR0ZXJkYXRlOnVuaXgpJyByZWZzL3JlbW90ZXMvb3JpZ2luIDI+L2Rldi9udWxsIHwgYXdrIC12IG5vdz0iJChkYXRlICslcykiIC12IGRheXM9IiR7c3RhbGVfY3V0b2ZmX2RheXN9IiAne2lmIChub3ctJDIgPiBkYXlzKjg2NDAwKSBwcmludCAkMX0nIHwgaGVhZCAtNTApIgogICAgICAgICAgICBpZiBbIC1uICIke3N0YWxlX2JyYW5jaGVzfSIgXTsgdGhlbgogICAgICAgICAgICAgIGV4dGVuZGVkX2ZpbmRpbmdzKz0oIlN0YWxlIHJlbW90ZSBicmFuY2hlcyBkZXRlY3RlZCAoYWR2aXNvcnkpIikKICAgICAgICAgICAgICB7CiAgICAgICAgICAgICAgICBwcmludGYgJyVzXG4nICcjIyMgR2l0IGh5Z2llbmUgYWR2aXNvcnknCiAgICAgICAgICAgICAgICBwcmludGYgJyVzXG4nICJCcmFuY2hlcyB3aXRoIGxhc3QgY29tbWl0IG9sZGVyIHRoYW4gJHtzdGFsZV9jdXRvZmZfZGF5c30gZGF5cyAoc2FtcGxlIHVwIHRvIDUwKToiCiAgICAgICAgICAgICAgICB3aGlsZSBJRlM9IHJlYWQgLXIgYjsgZG8gWyAtbiAiJHtifSIgXSAmJiBwcmludGYgJyVzXG4nICItICR7Yn0iOyBkb25lIDw8PCAiJHtzdGFsZV9icmFuY2hlc30iCiAgICAgICAgICAgICAgICBwcmludGYgJ1xuJwogICAgICAgICAgICAgIH0gPj4gIiR7R0lUSFVCX1NURVBfU1VNTUFSWX0iCiAgICAgICAgICAgIGZpCiAgICAgICAgICBmaQoKICAgICAgICAgIHsKICAgICAgICAgICAgcHJpbnRmICclc1xuJyAnIyMjIEd1YXJkcmFpbHMgY292ZXJhZ2UgbWF0cml4JwogICAgICAgICAgICBwcmludGYgJyVzXG4nICd8IERvbWFpbiB8IFN0YXR1cyB8IE5vdGVzIHwnCiAgICAgICAgICAgIHByaW50ZiAnJXNcbicgJ3wtLS18LS0tfC0tLXwnCiAgICAgICAgICAgIHByaW50ZiAnJXNcbicgJ3wgQWNjZXNzIGNvbnRyb2wgfCBPSyB8IEFkbWluLW9ubHkgZXhlY3V0aW9uIGdhdGUgfCcKICAgICAgICAgICAgcHJpbnRmICclc1xuJyAnfCBSZWxlYXNlIHBvbGljeSB8IE4vQSB8IFJlbGVhc2VzIGhhbmRsZWQgYnkgTW9rb0dpdGVhIHwnCiAgICAgICAgICAgIHByaW50ZiAnJXNcbicgJ3wgU2NyaXB0cyBnb3Zlcm5hbmNlIHwgT0sgfCBEaXJlY3RvcnkgcG9saWN5IGFuZCBhZHZpc29yeSByZXBvcnRpbmcgfCcKICAgICAgICAgICAgcHJpbnRmICclc1xuJyAnfCBSZXBvIHJlcXVpcmVkIGFydGlmYWN0cyB8IE9LIHwgUmVxdWlyZWQsIG9wdGlvbmFsLCBkaXNhbGxvd2VkIGVuZm9yY2VtZW50IHwnCiAgICAgICAgICAgIHByaW50ZiAnJXNcbicgJ3wgUmVwbyBjb250ZW50IGhldXJpc3RpY3MgfCBPSyB8IEJyYW5kLCBsaWNlbnNlLCBjaGFuZ2Vsb2cgc3RydWN0dXJlIHwnCiAgICAgICAgICAgIGlmIFsgIiR7ZXh0ZW5kZWRfZW5hYmxlZH0iID0gJ3RydWUnIF07IHRoZW4KICAgICAgICAgICAgICBpZiBbICIkeyNleHRlbmRlZF9maW5kaW5nc1tAXX0iIC1ndCAwIF07IHRoZW4KICAgICAgICAgICAgICAgIHByaW50ZiAnJXNcbicgJ3wgRXh0ZW5kZWQgY2hlY2tzIHwgV2FybmluZyB8IFNlZSBleHRlbmRlZCBmaW5kaW5ncyBiZWxvdyB8JwogICAgICAgICAgICAgIGVsc2UKICAgICAgICAgICAgICAgIHByaW50ZiAnJXNcbicgJ3wgRXh0ZW5kZWQgY2hlY2tzIHwgT0sgfCBObyBmaW5kaW5ncyB8JwogICAgICAgICAgICAgIGZpCiAgICAgICAgICAgIGVsc2UKICAgICAgICAgICAgICBwcmludGYgJyVzXG4nICd8IEV4dGVuZGVkIGNoZWNrcyB8IFNLSVBQRUQgfCBFWFRFTkRFRF9DSEVDS1MgZGlzYWJsZWQgfCcKICAgICAgICAgICAgZmkKICAgICAgICAgICAgcHJpbnRmICdcbicKICAgICAgICAgIH0gPj4gIiR7R0lUSFVCX1NURVBfU1VNTUFSWX0iCgogICAgICAgICAgaWYgWyAiJHtleHRlbmRlZF9lbmFibGVkfSIgPSAndHJ1ZScgXSAmJiBbICIkeyNleHRlbmRlZF9maW5kaW5nc1tAXX0iIC1ndCAwIF07IHRoZW4KICAgICAgICAgICAgewogICAgICAgICAgICAgIHByaW50ZiAnJXNcbicgJyMjIyBFeHRlbmRlZCBmaW5kaW5ncyAoYWR2aXNvcnkpJwogICAgICAgICAgICAgIGZvciBmIGluICIke2V4dGVuZGVkX2ZpbmRpbmdzW0BdfSI7IGRvIHByaW50ZiAnJXNcbicgIi0gJHtmfSI7IGRvbmUKICAgICAgICAgICAgICBwcmludGYgJ1xuJwogICAgICAgICAgICB9ID4+ICIke0dJVEhVQl9TVEVQX1NVTU1BUll9IgogICAgICAgICAgZmkKCiAgICAgICAgICBwcmludGYgJyVzXG4nICdSZXBvc2l0b3J5IGhlYWx0aCBndWFyZHJhaWxzIHBhc3NlZC4nID4+ICIke0dJVEhVQl9TVEVQX1NVTU1BUll9IgoKCiAgc2l0ZS1oZWFsdGg6CiAgICBuYW1lOiBTaXRlIEhlYWx0aAogICAgcnVucy1vbjogdWJ1bnR1LWxhdGVzdAogICAgaWY6IGdpdGh1Yi5ldmVudF9uYW1lID09ICd3b3JrZmxvd19kaXNwYXRjaCcKICAgIHN0ZXBzOgogICAgICAtIHVzZXM6IGFjdGlvbnMvY2hlY2tvdXRAdjQKCiAgICAgIC0gbmFtZTogU2V0dXAgUEhQCiAgICAgICAgdXNlczogc2hpdmFtbWF0aHVyL3NldHVwLXBocEB2MgogICAgICAgIHdpdGg6CiAgICAgICAgICBwaHAtdmVyc2lvbjogJzguMycKCiAgICAgIC0gbmFtZTogVXB0aW1lIGNoZWNrCiAgICAgICAgaWY6IGVudi5VUkxTICE9ICcnCiAgICAgICAgcnVuOiB8CiAgICAgICAgICBlY2hvICIkVVJMUyIgPiAvdG1wL3VybHMudHh0CiAgICAgICAgICBwaHAgbW9uaXRvcmluZy91cHRpbWUtcHJvYmUucGhwIC0tdXJscyAvdG1wL3VybHMudHh0IC0tdGltZW91dCAxNSB8fCBlY2hvICI6Ondhcm5pbmc6OlNvbWUgc2l0ZXMgYXJlIGRvd24iCiAgICAgICAgICBybSAtZiAvdG1wL3VybHMudHh0CiAgICAgICAgZW52OgogICAgICAgICAgVVJMUzogJHt7IHZhcnMuTU9OSVRPUkVEX1VSTFMgfX0KCiAgICAgIC0gbmFtZTogU1NMIGNlcnRpZmljYXRlIGNoZWNrCiAgICAgICAgaWY6IGVudi5ET01BSU5TICE9ICcnCiAgICAgICAgcnVuOiB8CiAgICAgICAgICBlY2hvICIkRE9NQUlOUyIgPiAvdG1wL2RvbWFpbnMudHh0CiAgICAgICAgICBwaHAgbW9uaXRvcmluZy9zc2wtY2hlY2sucGhwIC0tZG9tYWlucyAvdG1wL2RvbWFpbnMudHh0IC0td2Fybi1kYXlzIDMwIHx8IGVjaG8gIjo6d2FybmluZzo6U1NMIGNlcnRpZmljYXRlcyBleHBpcmluZyBzb29uIgogICAgICAgICAgcm0gLWYgL3RtcC9kb21haW5zLnR4dAogICAgICAgIGVudjoKICAgICAgICAgIERPTUFJTlM6ICR7eyB2YXJzLk1PTklUT1JFRF9ET01BSU5TIH19CgogICAgICAtIG5hbWU6IFN1bW1hcnkKICAgICAgICBpZjogYWx3YXlzKCkKICAgICAgICBydW46IHwKICAgICAgICAgIGVjaG8gIiMjIyBTaXRlIEhlYWx0aCIgPj4gJEdJVEhVQl9TVEVQX1NVTU1BUlkKICAgICAgICAgIGVjaG8gIlVwdGltZSBhbmQgU1NMIGNoZWNrcyBjb21wbGV0ZWQuIiA+PiAkR0lUSFVCX1NURVBfU1VNTUFSWQoKICAjIOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkAogICMgSXNzdWUgUmVwb3J0ZXIg4oCUIGZpbGUgaXNzdWVzIGZvciBmYWlsZWQgZ2F0ZXMKICAjIOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkAogIHJlcG9ydC1zY3JpcHRzOgogICAgbmFtZTogIlJlcG9ydDogU2NyaXB0cyBHb3Zlcm5hbmNlIgogICAgbmVlZHM6IFthY2Nlc3NfY2hlY2ssIHNjcmlwdHNfZ292ZXJuYW5jZV0KICAgIGlmOiA+LQogICAgICBhbHdheXMoKSAmJgogICAgICBuZWVkcy5zY3JpcHRzX2dvdmVybmFuY2UucmVzdWx0ID09ICdmYWlsdXJlJwogICAgdXNlczogLi8ubW9rb2dpdGVhL3dvcmtmbG93cy9jaS1pc3N1ZS1yZXBvcnRlci55bWwKICAgIHdpdGg6CiAgICAgIGdhdGU6ICJTY3JpcHRzIEdvdmVybmFuY2UiCiAgICAgIHdvcmtmbG93OiAiUmVwbyBIZWFsdGgiCiAgICAgIHNldmVyaXR5OiBlcnJvcgogICAgICBkZXRhaWxzOiAiU2NyaXB0cyBkaXJlY3RvcnkgcG9saWN5IHZpb2xhdGlvbnMgZGV0ZWN0ZWQuIFJldmlldyByZXF1aXJlZCBhbmQgYWxsb3dlZCBkaXJlY3Rvcmllcy4iCiAgICBzZWNyZXRzOiBpbmhlcml0CgogIHJlcG9ydC1oZWFsdGg6CiAgICBuYW1lOiAiUmVwb3J0OiBSZXBvc2l0b3J5IEhlYWx0aCIKICAgIG5lZWRzOiBbYWNjZXNzX2NoZWNrLCByZXBvX2hlYWx0aF0KICAgIGlmOiA+LQogICAgICBhbHdheXMoKSAmJgogICAgICBuZWVkcy5yZXBvX2hlYWx0aC5yZXN1bHQgPT0gJ2ZhaWx1cmUnCiAgICB1c2VzOiAuLy5tb2tvZ2l0ZWEvd29ya2Zsb3dzL2NpLWlzc3VlLXJlcG9ydGVyLnltbAogICAgd2l0aDoKICAgICAgZ2F0ZTogIlJlcG9zaXRvcnkgSGVhbHRoIgogICAgICB3b3JrZmxvdzogIlJlcG8gSGVhbHRoIgogICAgICBzZXZlcml0eTogZXJyb3IKICAgICAgZGV0YWlsczogIlJlcG9zaXRvcnkgaGVhbHRoIGNoZWNrcyBmYWlsZWQg4oCUIG1pc3NpbmcgcmVxdWlyZWQgYXJ0aWZhY3RzLCBkaXNhbGxvd2VkIGZpbGVzLCBvciBjb250ZW50IHdhcm5pbmdzLiBDaGVjayB0aGUgQ0kgcnVuIHN1bW1hcnkuIgogICAgc2VjcmV0czogaW5oZXJpdAo= \ No newline at end of file +# ============================================================================ +# Copyright (C) 2025 Moko Consulting +# +# This file is part of a Moko Consulting project. +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: MokoGitea.Workflow +# INGROUP: mokocli.Validation +# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic +# PATH: /.mokogitea/workflows/repo-health.yml +# 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: + branches: + - main + +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 || 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|mokogitea-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 '/dev/null | head -1 || true)" + if [ -z "${MANIFEST}" ]; then + joomla_findings+=("Joomla XML manifest not found (no *.xml with tag)") + else + if ! grep -qP '' "${MANIFEST}"; then + joomla_findings+=("XML manifest: 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 '' "${MANIFEST}"; then + joomla_findings+=("XML manifest: tag missing") + fi + if ! grep -qP '' "${MANIFEST}"; then + joomla_findings+=("XML manifest: tag missing") + fi + if ! grep -qP ' 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-scripts: + name: "Report: Scripts Governance" + needs: [access_check, scripts_governance] + if: >- + always() && + needs.scripts_governance.result == 'failure' + uses: ./.mokogitea/workflows/ci-issue-reporter.yml + with: + gate: "Scripts Governance" + workflow: "Repo Health" + severity: error + details: "Scripts directory policy violations detected. Review required and allowed directories." + secrets: inherit + + report-health: + name: "Report: Repository Health" + needs: [access_check, repo_health] + if: >- + always() && + needs.repo_health.result == 'failure' + uses: ./.mokogitea/workflows/ci-issue-reporter.yml + with: + gate: "Repository Health" + workflow: "Repo Health" + severity: error + details: "Repository health checks failed — missing required artifacts, disallowed files, or content warnings. Check the CI run summary." + secrets: inherit diff --git a/.mokogitea/workflows/version-set.yml b/.mokogitea/workflows/version-set.yml index 20afb6d..eed4192 100644 --- a/.mokogitea/workflows/version-set.yml +++ b/.mokogitea/workflows/version-set.yml @@ -1 +1,131 @@ -IyBDb3B5cmlnaHQgKEMpIDIwMjYgTW9rbyBDb25zdWx0aW5nIDxoZWxsb0Btb2tvY29uc3VsdGluZy50ZWNoPgojCiMgU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IEdQTC0zLjAtb3ItbGF0ZXIKIwojIEZJTEUgSU5GT1JNQVRJT04KIyBERUZHUk9VUDogR2l0ZWEuV29ya2Zsb3cuVGVtcGxhdGUKIyBJTkdST1VQOiBNb2tvU3RhbmRhcmRzLkNJCiMgUkVQTzogaHR0cHM6Ly9naXQubW9rb2NvbnN1bHRpbmcudGVjaC9Nb2tvQ29uc3VsdGluZy9UZW1wbGF0ZS1Kb29tbGEKIyBQQVRIOiAvLm1va29naXRlYS93b3JrZmxvd3MvdmVyc2lvbi1zZXQueW1sCiMgVkVSU0lPTjogMDEuMDAuMDAKIyBCUklFRjogU2V0IG9yIHJlc2V0IHRoZSBleHRlbnNpb24gdmVyc2lvbiBhY3Jvc3MgYWxsIHZlcnNpb24tYmVhcmluZyBmaWxlcwoKbmFtZTogIkpvb21sYTogU2V0IFZlcnNpb24iCgpvbjoKICB3b3JrZmxvd19kaXNwYXRjaDoKICAgIGlucHV0czoKICAgICAgdmVyc2lvbjoKICAgICAgICBkZXNjcmlwdGlvbjogIlZlcnNpb24gbnVtYmVyIChlLmcuIDAxLjAwLjAwKSIKICAgICAgICByZXF1aXJlZDogdHJ1ZQogICAgICAgIHR5cGU6IHN0cmluZwogICAgICBicmFuY2g6CiAgICAgICAgZGVzY3JpcHRpb246ICJCcmFuY2ggdG8gdXBkYXRlIChkZWZhdWx0OiBjdXJyZW50KSIKICAgICAgICByZXF1aXJlZDogZmFsc2UKICAgICAgICB0eXBlOiBzdHJpbmcKCnBlcm1pc3Npb25zOgogIGNvbnRlbnRzOiB3cml0ZQoKZW52OgogIEZPUkNFX0pBVkFTQ1JJUFRfQUNUSU9OU19UT19OT0RFMjQ6IHRydWUKCmpvYnM6CiAgc2V0LXZlcnNpb246CiAgICBuYW1lOiBTZXQgVmVyc2lvbiB0byAke3sgaW5wdXRzLnZlcnNpb24gfX0KICAgIHJ1bnMtb246IHVidW50dS1sYXRlc3QKCiAgICBzdGVwczoKICAgICAgLSBuYW1lOiBWYWxpZGF0ZSB2ZXJzaW9uIGZvcm1hdAogICAgICAgIHJ1bjogfAogICAgICAgICAgVkVSU0lPTj0iJHt7IGlucHV0cy52ZXJzaW9uIH19IgogICAgICAgICAgaWYgISBlY2hvICIkVkVSU0lPTiIgfCBncmVwIC1xUCAnXlxkezJ9XC5cZHsyfVwuXGR7Mn0kJzsgdGhlbgogICAgICAgICAgICBlY2hvICI6OmVycm9yOjpJbnZhbGlkIHZlcnNpb24gZm9ybWF0ICcke1ZFUlNJT059JyDigJQgZXhwZWN0ZWQgWFguWVkuWlogKGUuZy4gMDEuMDAuMDApIgogICAgICAgICAgICBleGl0IDEKICAgICAgICAgIGZpCiAgICAgICAgICBlY2hvICJWRVJTSU9OPSR7VkVSU0lPTn0iID4+ICIkR0lUSFVCX0VOViIKCiAgICAgIC0gbmFtZTogQ2hlY2tvdXQKICAgICAgICB1c2VzOiBhY3Rpb25zL2NoZWNrb3V0QHY0CiAgICAgICAgd2l0aDoKICAgICAgICAgIHRva2VuOiAke3sgc2VjcmV0cy5NT0tPR0lURUFfVE9LRU4gfHwgZ2l0aHViLnRva2VuIH19CiAgICAgICAgICByZWY6ICR7eyBpbnB1dHMuYnJhbmNoIHx8IGdpdGh1Yi5yZWYgfX0KICAgICAgICAgIGZldGNoLWRlcHRoOiAxCgogICAgICAtIG5hbWU6IFVwZGF0ZSBtYW5pZmVzdCB2ZXJzaW9uCiAgICAgICAgcnVuOiB8CiAgICAgICAgICBNQU5JRkVTVD0iIgogICAgICAgICAgZm9yIFhNTF9GSUxFIGluICQoZmluZCAuIC1tYXhkZXB0aCAzIC1uYW1lICIqLnhtbCIgLW5vdCAtcGF0aCAiLi8uZ2l0LyoiIC1ub3QgLXBhdGggIi4vdmVuZG9yLyoiKTsgZG8KICAgICAgICAgICAgaWYgZ3JlcCAtcSAiPGV4dGVuc2lvbiIgIiRYTUxfRklMRSIgMj4vZGV2L251bGw7IHRoZW4KICAgICAgICAgICAgICBNQU5JRkVTVD0iJFhNTF9GSUxFIgogICAgICAgICAgICAgIGJyZWFrCiAgICAgICAgICAgIGZpCiAgICAgICAgICBkb25lCgogICAgICAgICAgaWYgWyAteiAiJE1BTklGRVNUIiBdOyB0aGVuCiAgICAgICAgICAgIGVjaG8gIjo6d2FybmluZzo6Tm8gSm9vbWxhIGV4dGVuc2lvbiBtYW5pZmVzdCBmb3VuZCDigJQgc2tpcHBpbmcgbWFuaWZlc3QgdXBkYXRlIgogICAgICAgICAgZWxzZQogICAgICAgICAgICBPTERfVkVSPSQoZ3JlcCAtb1AgJzx2ZXJzaW9uPlxLW148XSsnICIkTUFOSUZFU1QiIHwgaGVhZCAtMSkKICAgICAgICAgICAgc2VkIC1pICJzfDx2ZXJzaW9uPiR7T0xEX1ZFUn08L3ZlcnNpb24+fDx2ZXJzaW9uPiR7VkVSU0lPTn08L3ZlcnNpb24+fCIgIiRNQU5JRkVTVCIKICAgICAgICAgICAgZWNobyAiTWFuaWZlc3Q6ICR7T0xEX1ZFUn0g4oaSICR7VkVSU0lPTn0gKCR7TUFOSUZFU1R9KSIKICAgICAgICAgIGZpCgogICAgICAtIG5hbWU6IFVwZGF0ZSBSRUFETUUubWQgdmVyc2lvbgogICAgICAgIHJ1bjogfAogICAgICAgICAgaWYgWyAtZiAiUkVBRE1FLm1kIiBdOyB0aGVuCiAgICAgICAgICAgIGlmIGdyZXAgLXFQICdeXHMqVkVSU0lPTjpccypcZCcgUkVBRE1FLm1kOyB0aGVuCiAgICAgICAgICAgICAgc2VkIC1pIC1FICJzLyhWRVJTSU9OOlxzKilbMC05XXsyfVwuWzAtOV17Mn1cLlswLTldezJ9L1wxJHtWRVJTSU9OfS8iIFJFQURNRS5tZAogICAgICAgICAgICAgIGVjaG8gIlJFQURNRS5tZCB2ZXJzaW9uIHVwZGF0ZWQgdG8gJHtWRVJTSU9OfSIKICAgICAgICAgICAgZWxzZQogICAgICAgICAgICAgIGVjaG8gIjo6d2FybmluZzo6Tm8gVkVSU0lPTiBsaW5lIGZvdW5kIGluIFJFQURNRS5tZCDigJQgc2tpcHBpbmciCiAgICAgICAgICAgIGZpCiAgICAgICAgICBmaQoKICAgICAgLSBuYW1lOiBVcGRhdGUgQ0hBTkdFTE9HLm1kCiAgICAgICAgcnVuOiB8CiAgICAgICAgICBpZiBbIC1mICJDSEFOR0VMT0cubWQiIF07IHRoZW4KICAgICAgICAgICAgREFURT0kKGRhdGUgKyVZLSVtLSVkKQogICAgICAgICAgICAjIENoZWNrIGlmIHRoaXMgdmVyc2lvbiBhbHJlYWR5IGhhcyBhbiBlbnRyeQogICAgICAgICAgICBpZiBncmVwIC1xICJeXCNcIyBcWyR7VkVSU0lPTn1cXSIgQ0hBTkdFTE9HLm1kOyB0aGVuCiAgICAgICAgICAgICAgZWNobyAiQ0hBTkdFTE9HLm1kIGFscmVhZHkgaGFzIGVudHJ5IGZvciAke1ZFUlNJT059IOKAlCBza2lwcGluZyIKICAgICAgICAgICAgZWxzZQogICAgICAgICAgICAgICMgSW5zZXJ0IG5ldyB2ZXJzaW9uIGVudHJ5IGFmdGVyIFtVbnJlbGVhc2VkXSBvciBhdCB0aGUgdG9wIGFmdGVyIGhlYWRlcgogICAgICAgICAgICAgIGlmIGdyZXAgLXEgJ15cI1wjIFxbVW5yZWxlYXNlZFxdJyBDSEFOR0VMT0cubWQ7IHRoZW4KICAgICAgICAgICAgICAgIHNlZCAtaSAiL15cI1wjIFxbVW5yZWxlYXNlZFxdL2FcXFxcbiMjIFske1ZFUlNJT059XSAtLS0gJHtEQVRFfSIgQ0hBTkdFTE9HLm1kCiAgICAgICAgICAgICAgZWxzZQogICAgICAgICAgICAgICAgc2VkIC1pICIvXlwjIENoYW5nZWxvZy9hXFxcXG4jIyBbVW5yZWxlYXNlZF1cblxuIyMgWyR7VkVSU0lPTn1dIC0tLSAke0RBVEV9IiBDSEFOR0VMT0cubWQKICAgICAgICAgICAgICBmaQogICAgICAgICAgICAgIGVjaG8gIkNIQU5HRUxPRy5tZDogYWRkZWQgZW50cnkgZm9yICR7VkVSU0lPTn0iCiAgICAgICAgICAgIGZpCiAgICAgICAgICBlbHNlCiAgICAgICAgICAgIGVjaG8gIjo6d2FybmluZzo6Tm8gQ0hBTkdFTE9HLm1kIGZvdW5kIOKAlCBza2lwcGluZyIKICAgICAgICAgIGZpCgogICAgICAtIG5hbWU6IFVwZGF0ZSBGSUxFIElORk9STUFUSU9OIGJsb2NrcwogICAgICAgIHJ1bjogfAogICAgICAgICAgIyBVcGRhdGUgVkVSU0lPTiBpbiBmaWxlIGhlYWRlciBibG9ja3MgKCMgVkVSU0lPTjogWFguWVkuWlopCiAgICAgICAgICBmaW5kIC4gLW1heGRlcHRoIDEgLXR5cGUgZiBcKCAtbmFtZSAiKi55bWwiIC1vIC1uYW1lICIqLnlhbWwiIC1vIC1uYW1lICIqLnBocCIgLW8gLW5hbWUgIioubWQiIFwpIFwKICAgICAgICAgICAgLW5vdCAtcGF0aCAiLi8uZ2l0LyoiIC1ub3QgLXBhdGggIi4vdmVuZG9yLyoiIC1wcmludDAgMj4vZGV2L251bGwgfCBcCiAgICAgICAgICB3aGlsZSBJRlM9IHJlYWQgLXIgLWQgJycgRklMRTsgZG8KICAgICAgICAgICAgaWYgaGVhZCAtMjAgIiRGSUxFIiB8IGdyZXAgLXFQICdeXHMqIz9ccypWRVJTSU9OOlxzKlxkezJ9XC5cZHsyfVwuXGR7Mn0nOyB0aGVuCiAgICAgICAgICAgICAgc2VkIC1pIC1FICJzLygjP1xzKlZFUlNJT046XHMqKVswLTldezJ9XC5bMC05XXsyfVwuWzAtOV17Mn0vXDEke1ZFUlNJT059LyIgIiRGSUxFIgogICAgICAgICAgICAgIGVjaG8gIlVwZGF0ZWQgRklMRSBJTkZPUk1BVElPTiBWRVJTSU9OIGluICR7RklMRX0iCiAgICAgICAgICAgIGZpCiAgICAgICAgICBkb25lCgogICAgICAtIG5hbWU6IENvbW1pdCBhbmQgcHVzaAogICAgICAgIHJ1bjogfAogICAgICAgICAgZ2l0IGNvbmZpZyB1c2VyLm5hbWUgIk1va28gQ29uc3VsdGluZyBbYm90XSIKICAgICAgICAgIGdpdCBjb25maWcgdXNlci5lbWFpbCAiaGVsbG9AbW9rb2NvbnN1bHRpbmcudGVjaCIKICAgICAgICAgIGdpdCBhZGQgLUEKICAgICAgICAgIGlmIGdpdCBkaWZmIC0tY2FjaGVkIC0tcXVpZXQ7IHRoZW4KICAgICAgICAgICAgZWNobyAiTm8gdmVyc2lvbiBjaGFuZ2VzIGRldGVjdGVkIOKAlCBub3RoaW5nIHRvIGNvbW1pdCIKICAgICAgICAgIGVsc2UKICAgICAgICAgICAgZ2l0IGNvbW1pdCAtbSAiY2hvcmU6IHNldCB2ZXJzaW9uIHRvICR7VkVSU0lPTn0gW3NraXAgYnVtcF0KCkF1dGhvcmVkLWJ5OiBNb2tvIENvbnN1bHRpbmciCiAgICAgICAgICAgIGdpdCBwdXNoCiAgICAgICAgICAgIGVjaG8gIiMjIyBWZXJzaW9uIFNldCIgPj4gJEdJVEhVQl9TVEVQX1NVTU1BUlkKICAgICAgICAgICAgZWNobyAiVmVyc2lvbiB1cGRhdGVkIHRvIFxgJHtWRVJTSU9OfVxgIG9uIGJyYW5jaCBcYCR7R0lUSFVCX1JFRl9OQU1FfVxgIiA+PiAkR0lUSFVCX1NURVBfU1VNTUFSWQogICAgICAgICAgZmkK \ No newline at end of file +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: MokoGitea.Workflow.Template +# INGROUP: MokoStandards.CI +# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla +# PATH: /.mokogitea/workflows/version-set.yml +# VERSION: 01.00.00 +# BRIEF: Set or reset the extension version across all version-bearing files + +name: "Joomla: Set Version" + +on: + workflow_dispatch: + inputs: + version: + description: "Version number (e.g. 01.00.00)" + required: true + type: string + branch: + description: "Branch to update (default: current)" + required: false + type: string + +permissions: + contents: write + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + set-version: + name: Set Version to ${{ inputs.version }} + runs-on: ubuntu-latest + if: ${{ !startsWith(github.event.repository.name, 'Template-') }} + + steps: + - name: Validate version format + run: | + VERSION="${{ inputs.version }}" + if ! echo "$VERSION" | grep -qP '^\d{2}\.\d{2}\.\d{2}$'; then + echo "::error::Invalid version format '${VERSION}' — expected XX.YY.ZZ (e.g. 01.00.00)" + exit 1 + fi + echo "VERSION=${VERSION}" >> "$GITHUB_ENV" + + - name: Checkout + uses: actions/checkout@v4 + with: + token: ${{ secrets.MOKOGITEA_TOKEN || github.token }} + ref: ${{ inputs.branch || github.ref }} + fetch-depth: 1 + + - name: Update manifest version + run: | + MANIFEST="" + for XML_FILE in $(find . -maxdepth 3 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do + if grep -q "/dev/null; then + MANIFEST="$XML_FILE" + break + fi + done + + if [ -z "$MANIFEST" ]; then + echo "::warning::No Joomla extension manifest found — skipping manifest update" + else + OLD_VER=$(grep -oP '\K[^<]+' "$MANIFEST" | head -1) + sed -i "s|${OLD_VER}|${VERSION}|" "$MANIFEST" + echo "Manifest: ${OLD_VER} → ${VERSION} (${MANIFEST})" + fi + + - name: Update README.md version + run: | + if [ -f "README.md" ]; then + if grep -qP '^\s*VERSION:\s*\d' README.md; then + sed -i -E "s/(VERSION:\s*)[0-9]{2}\.[0-9]{2}\.[0-9]{2}/\1${VERSION}/" README.md + echo "README.md version updated to ${VERSION}" + else + echo "::warning::No VERSION line found in README.md — skipping" + fi + fi + + - name: Update CHANGELOG.md + run: | + if [ -f "CHANGELOG.md" ]; then + DATE=$(date +%Y-%m-%d) + # Check if this version already has an entry + if grep -q "^\#\# \[${VERSION}\]" CHANGELOG.md; then + echo "CHANGELOG.md already has entry for ${VERSION} — skipping" + else + # Insert new version entry after [Unreleased] or at the top after header + if grep -q '^\#\# \[Unreleased\]' CHANGELOG.md; then + sed -i "/^\#\# \[Unreleased\]/a\\\\n## [${VERSION}] --- ${DATE}" CHANGELOG.md + else + sed -i "/^\# Changelog/a\\\\n## [Unreleased]\n\n## [${VERSION}] --- ${DATE}" CHANGELOG.md + fi + echo "CHANGELOG.md: added entry for ${VERSION}" + fi + else + echo "::warning::No CHANGELOG.md found — skipping" + fi + + - name: Update FILE INFORMATION blocks + run: | + # Update VERSION in file header blocks (# VERSION: XX.YY.ZZ) + find . -maxdepth 1 -type f \( -name "*.yml" -o -name "*.yaml" -o -name "*.php" -o -name "*.md" \) \ + -not -path "./.git/*" -not -path "./vendor/*" -print0 2>/dev/null | \ + while IFS= read -r -d '' FILE; do + if head -20 "$FILE" | grep -qP '^\s*#?\s*VERSION:\s*\d{2}\.\d{2}\.\d{2}'; then + sed -i -E "s/(#?\s*VERSION:\s*)[0-9]{2}\.[0-9]{2}\.[0-9]{2}/\1${VERSION}/" "$FILE" + echo "Updated FILE INFORMATION VERSION in ${FILE}" + fi + done + + - name: Commit and push + run: | + git config user.name "Moko Consulting [bot]" + git config user.email "hello@mokoconsulting.tech" + git add -A + if git diff --cached --quiet; then + echo "No version changes detected — nothing to commit" + else + git commit -m "chore: set version to ${VERSION} [skip bump] + +Authored-by: Moko Consulting" + git push + echo "### Version Set" >> $GITHUB_STEP_SUMMARY + echo "Version updated to \`${VERSION}\` on branch \`${GITHUB_REF_NAME}\`" >> $GITHUB_STEP_SUMMARY + fi diff --git a/.mokogitea/workflows/workflow-sync-trigger.yml b/.mokogitea/workflows/workflow-sync-trigger.yml index bc873e3..69b00c4 100644 --- a/.mokogitea/workflows/workflow-sync-trigger.yml +++ b/.mokogitea/workflows/workflow-sync-trigger.yml @@ -1 +1,82 @@ -IyBDb3B5cmlnaHQgKEMpIDIwMjYgTW9rbyBDb25zdWx0aW5nIDxoZWxsb0Btb2tvY29uc3VsdGluZy50ZWNoPgojCiMgU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IEdQTC0zLjAtb3ItbGF0ZXIKIwojIEZJTEUgSU5GT1JNQVRJT04KIyBERUZHUk9VUDogR2l0ZWEuV29ya2Zsb3cKIyBJTkdST1VQOiBtb2tvY2xpLlVuaXZlcnNhbAojIFJFUE86IGh0dHBzOi8vZ2l0Lm1va29jb25zdWx0aW5nLnRlY2gvTW9rb0NvbnN1bHRpbmcvbW9rb2NsaQojIFBBVEg6IC8ubW9rb2dpdGVhL3dvcmtmbG93cy93b3JrZmxvdy1zeW5jLXRyaWdnZXIueW1sCiMgVkVSU0lPTjogMDEuMDEuMDAKIyBCUklFRjogVHJpZ2dlciB3b3JrZmxvdyBzeW5jIHRvIGxpdmUgcmVwb3Mgd2hlbiBhIFBSIGlzIG1lcmdlZCB0byBtYWluCgpuYW1lOiAiVW5pdmVyc2FsOiBXb3JrZmxvdyBTeW5jIFRyaWdnZXIiCgpvbjoKICB3b3JrZmxvd19kaXNwYXRjaDoKICBwdWxsX3JlcXVlc3Q6CiAgICB0eXBlczogW2Nsb3NlZF0KICAgIGJyYW5jaGVzOgogICAgICAtIG1haW4KCmVudjoKICBGT1JDRV9KQVZBU0NSSVBUX0FDVElPTlNfVE9fTk9ERTI0OiB0cnVlCgpqb2JzOgogIHN5bmM6CiAgICBuYW1lOiBTeW5jIHdvcmtmbG93cyB0byBsaXZlIHJlcG9zCiAgICBydW5zLW9uOiB1YnVudHUtbGF0ZXN0CiAgICBpZjogPi0KICAgICAgZ2l0aHViLmV2ZW50X25hbWUgPT0gJ3dvcmtmbG93X2Rpc3BhdGNoJyB8fAogICAgICAoZ2l0aHViLmV2ZW50LnB1bGxfcmVxdWVzdC5tZXJnZWQgPT0gdHJ1ZSAmJgogICAgICAhY29udGFpbnMoZ2l0aHViLmV2ZW50LnB1bGxfcmVxdWVzdC50aXRsZSwgJ1tza2lwIHN5bmNdJykpCgogICAgc3RlcHM6CiAgICAgIC0gbmFtZTogRGV0ZXJtaW5lIHBsYXRmb3JtIGZyb20gcmVwbyBuYW1lCiAgICAgICAgaWQ6IHBsYXRmb3JtCiAgICAgICAgcnVuOiB8CiAgICAgICAgICBSRVBPPSIke3sgZ2l0aHViLmV2ZW50LnJlcG9zaXRvcnkubmFtZSB9fSIKICAgICAgICAgIGNhc2UgIiRSRVBPIiBpbgogICAgICAgICAgICBUZW1wbGF0ZS1Kb29tbGEpICAgUExBVEZPUk09Impvb21sYSIgOzsKICAgICAgICAgICAgVGVtcGxhdGUtRG9saWJhcnIpIFBMQVRGT1JNPSJkb2xpYmFyciIgOzsKICAgICAgICAgICAgVGVtcGxhdGUtR28pICAgICAgIFBMQVRGT1JNPSJnbyIgOzsKICAgICAgICAgICAgVGVtcGxhdGUtTUNQKSAgICAgIFBMQVRGT1JNPSJtY3AiIDs7CiAgICAgICAgICAgIFRlbXBsYXRlLUdlbmVyaWMpICBQTEFURk9STT0iIiA7OwogICAgICAgICAgICAqKSAgICAgICAgICAgICAgICAgUExBVEZPUk09IiIgOzsKICAgICAgICAgIGVzYWMKICAgICAgICAgIGVjaG8gInBsYXRmb3JtPSRQTEFURk9STSIgPj4gIiRHSVRIVUJfT1VUUFVUIgogICAgICAgICAgZWNobyAiUGxhdGZvcm06ICR7UExBVEZPUk06LWFsbH0iCgogICAgICAtIG5hbWU6IENsb25lIG1va29jbGkKICAgICAgICBlbnY6CiAgICAgICAgICBNT0tPR0lURUFfVE9LRU46ICR7eyBzZWNyZXRzLk1PS09HSVRFQV9UT0tFTiB9fQogICAgICAgIHJ1bjogfAogICAgICAgICAgTU9LT0dJVEVBX1VSTD0iJHt7IHZhcnMuR0lURUFfVVJMIHx8ICdodHRwczovL2dpdC5tb2tvY29uc3VsdGluZy50ZWNoJyB9fSIKICAgICAgICAgIGdpdCBjbG9uZSAtLWRlcHRoIDEgIiR7TU9LT0dJVEVBX1VSTH0vTW9rb0NvbnN1bHRpbmcvbW9rb2NsaS5naXQiIC90bXAvbW9rb2NsaQoKICAgICAgLSBuYW1lOiBJbnN0YWxsIFBIUAogICAgICAgIHJ1bjogfAogICAgICAgICAgaWYgISBjb21tYW5kIC12IHBocCAmPiAvZGV2L251bGw7IHRoZW4KICAgICAgICAgICAgYXB0LWdldCB1cGRhdGUgLXFxICYmIGFwdC1nZXQgaW5zdGFsbCAteSAtcXEgcGhwLWNsaSBwaHAtanNvbiBwaHAtY3VybCA+IC9kZXYvbnVsbCAyPiYxCiAgICAgICAgICBmaQoKICAgICAgLSBuYW1lOiBJbnN0YWxsIGRlcGVuZGVuY2llcwogICAgICAgIHJ1bjogfAogICAgICAgICAgY2QgL3RtcC9tb2tvY2xpCiAgICAgICAgICBjb21wb3NlciBpbnN0YWxsIC0tbm8tZGV2IC0tbm8taW50ZXJhY3Rpb24gLS1xdWlldCAyPi9kZXYvbnVsbCB8fCB0cnVlCgogICAgICAtIG5hbWU6IFJ1biB3b3JrZmxvdyBzeW5jCiAgICAgICAgZW52OgogICAgICAgICAgTU9LT0dJVEVBX1RPS0VOOiAke3sgc2VjcmV0cy5NT0tPR0lURUFfVE9LRU4gfX0KICAgICAgICBydW46IHwKICAgICAgICAgIEFSR1M9Ii0tdG9rZW4gJHtNT0tPR0lURUFfVE9LRU59IgogICAgICAgICAgQVJHUz0iJHtBUkdTfSAtLW9yZyAke3sgdmFycy5HSVRFQV9PUkcgfHwgZ2l0aHViLnJlcG9zaXRvcnlfb3duZXIgfX0iCiAgICAgICAgICBBUkdTPSIke0FSR1N9IC0tcGhhc2UgcmVwb3MiCgogICAgICAgICAgUExBVEZPUk09IiR7eyBzdGVwcy5wbGF0Zm9ybS5vdXRwdXRzLnBsYXRmb3JtIH19IgogICAgICAgICAgaWYgWyAtbiAiJFBMQVRGT1JNIiBdOyB0aGVuCiAgICAgICAgICAgIEFSR1M9IiR7QVJHU30gLS1wbGF0Zm9ybS1maWx0ZXIgJHtQTEFURk9STX0iCiAgICAgICAgICBmaQoKICAgICAgICAgIHBocCAvdG1wL21va29jbGkvY2xpL3dvcmtmbG93X3N5bmMucGhwICR7QVJHU30K \ No newline at end of file +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: MokoGitea.Workflow +# INGROUP: mokocli.Universal +# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic +# PATH: /.mokogitea/workflows/workflow-sync-trigger.yml +# VERSION: 01.01.00 +# BRIEF: Trigger workflow sync to live repos when a PR is merged to main + +name: "Universal: Workflow Sync Trigger" + +on: + workflow_dispatch: + pull_request: + types: [closed] + branches: + - main + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + sync: + name: Sync workflows to live repos + runs-on: ubuntu-latest + if: >- + startsWith(github.event.repository.name, 'Template-') && + (github.event_name == 'workflow_dispatch' || + (github.event.pull_request.merged == true && + !contains(github.event.pull_request.title, '[skip sync]'))) + + steps: + - name: Determine platform from repo name + id: platform + run: | + REPO="${{ github.event.repository.name }}" + case "$REPO" in + Template-Joomla) PLATFORM="joomla" ;; + Template-Dolibarr) PLATFORM="dolibarr" ;; + Template-Go) PLATFORM="go" ;; + Template-MCP) PLATFORM="mcp" ;; + Template-Generic) PLATFORM="" ;; + *) PLATFORM="" ;; + esac + echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT" + echo "Platform: ${PLATFORM:-all}" + + - name: Clone mokocli + env: + MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + run: | + MOKOGITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}" + git clone --depth 1 "${MOKOGITEA_URL}/MokoConsulting/MokoCLI.git" /tmp/mokocli + + - name: Install PHP + run: | + if ! command -v php &> /dev/null; then + apt-get update -qq && apt-get install -y -qq php-cli php-json php-curl > /dev/null 2>&1 + fi + + - name: Install dependencies + run: | + cd /tmp/mokocli + composer install --no-dev --no-interaction --quiet 2>/dev/null || true + + - name: Run workflow sync + env: + MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + run: | + ARGS="--token ${MOKOGITEA_TOKEN}" + ARGS="${ARGS} --org ${{ vars.GITEA_ORG || github.repository_owner }}" + ARGS="${ARGS} --phase repos" + + PLATFORM="${{ steps.platform.outputs.platform }}" + if [ -n "$PLATFORM" ]; then + ARGS="${ARGS} --platform-filter ${PLATFORM}" + fi + + php /tmp/mokocli/cli/workflow_sync.php ${ARGS} diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index d37e676..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,19 +0,0 @@ - - -# Changelog - -## [Unreleased] - -### Added -- **Repository** -- initial repo creation with scaffolding -- **System Plugin** -- Extension class, service provider, 6 helpers -- **SQL Schema** -- 8 tables: vehicles, drivers, zones, fares, rides, ratings, dispatch, shifts -- **Admin Component** -- 6 views: dashboard, rides, vehicles, drivers, zones, fares -- **Webservices Plugin** -- 7 API routes: rides, vehicles, drivers, zones, fares, dispatch, ratings -- **Configuration** -- 16 settings across general, dispatch, fares, drivers, booking -- **Access Control** -- 21 permissions for granular role-based access diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 713e6a2..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,32 +0,0 @@ -# MokoSuiteTaxi - -Ride-hailing, dispatch, fleet management, fare zones, and driver scheduling for Joomla 6. - -## Quick Reference - -| Field | Value | -|---|---| -| **Package** | `pkg_mokosuitetaxi` | -| **Layer** | 2 (requires: Client, CRM) | -| **Language** | PHP 8.3+ | -| **Branch** | develop on `dev`, merge to `main` (protected) | -| **Wiki** | [MokoSuiteTaxi Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteTaxi/wiki) | - -## Architecture - -Joomla **package** -- Layer 2 add-on. CRM contacts as riders/drivers, zone-based dispatch with surge pricing. - -## Rules - -- **Never commit** `.claude/`, `.mcp.json`, `TODO.md`, `*.min.css`/`*.min.js` -- **Attribution**: `Authored-by: Moko Consulting` -- **Workflow directory**: `.mokogitea/` -- **Standards**: [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoCLI/wiki) -- **Changelog**: `[Unreleased]` only -- release system assigns versions - -## Coding Standards - -- PHP 8.3+ / Joomla 6 patterns -- `$this->getDatabase()` in models, `Factory::getContainer()->get(DatabaseInterface::class)` in helpers -- `Factory::getApplication()->getIdentity()` for user -- FOR UPDATE locking on ride acceptance/dispatch to prevent race conditions diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index da43ee5..809e983 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1 +1,46 @@ -PCEtLQlDb3B5cmlnaHQgKEMpIDIwMjYgTW9rbyBDb25zdWx0aW5nIDxoZWxsb0Btb2tvY29uc3VsdGluZy50ZWNoPgoKCVRoaXMgZmlsZSBpcyBwYXJ0IG9mIGEgTW9rbyBDb25zdWx0aW5nIHByb2plY3QuCgoJU1BEWC1MSUNFTlNFLUlERU5USUZJRVI6IEdQTC0zLjAtb3ItbGF0ZXIKCglUaGlzIHByb2dyYW0gaXMgZnJlZSBzb2Z0d2FyZTsgeW91IGNhbiByZWRpc3RyaWJ1dGUgaXQgYW5kL29yIG1vZGlmeSBpdCB1bmRlciB0aGUgdGVybXMgb2YgdGhlIEdOVSBHZW5lcmFsIFB1YmxpYyBMaWNlbnNlIGFzIHB1Ymxpc2hlZCBieSB0aGUgRnJlZSBTb2Z0d2FyZSBGb3VuZGF0aW9uOyBlaXRoZXIgdmVyc2lvbiAzIG9mIHRoZSBMaWNlbnNlLCBvciAoYXQgeW91ciBvcHRpb24pIGFueSBsYXRlciB2ZXJzaW9uLgoKCVRoaXMgcHJvZ3JhbSBpcyBkaXN0cmlidXRlZCBpbiB0aGUgaG9wZSB0aGF0IGl0IHdpbGwgYmUgdXNlZnVsLCBidXQgV0lUSE9VVCBBTlkgV0FSUkFOVFk7IHdpdGhvdXQgZXZlbiB0aGUgaW1wbGllZCB3YXJyYW50eSBvZiBNRVJDSEFOVEFCSUxJVFkgb3IgRklUTkVTUyBGT1IgQSBQQVJUSUNVTEFSIFBVUlBPU0UuIFNlZSB0aGUgR05VIEdlbmVyYWwgUHVibGljIExpY2Vuc2UgZm9yIG1vcmUgZGV0YWlscy4KCglZb3Ugc2hvdWxkIGhhdmUgcmVjZWl2ZWQgYSBjb3B5IG9mIHRoZSBHTlUgR2VuZXJhbCBQdWJsaWMgTGljZW5zZSAoLi9MSUNFTlNFKS4KCgkjIEZJTEUgSU5GT1JNQVRJT04KCURFRkdST1VQOiBUZW1wbGF0ZS1Kb29tbGEKCUlOR1JPVVA6IFRlbXBsYXRlLUpvb21sYS5Eb2N1bWVudGF0aW9uCglSRVBPOiBodHRwczovL2dpdGh1Yi5jb20vbW9rb2NvbnN1bHRpbmctdGVjaC9UZW1wbGF0ZS1Kb29tbGEvCiAgVkVSU0lPTjogMDEuMDEuMDAKICBQQVRIOiAuL0NPREVfT0ZfQ09ORFVDVC5tZAogIEJSSUVGOiBDb21tdW5pdHkgZXhwZWN0YXRpb25zIGFuZCBlbmZvcmNlbWVudCBndWlkZWxpbmVzCiAgTk9URTogQWRhcHRlZCB3aXRoIGF0dHJpYnV0aW9uIGZyb20gdGhlIENvbnRyaWJ1dG9yIENvdmVuYW50IHYyLjEKLS0+CgojIENvbnRyaWJ1dG9yIENvdmVuYW50IENvZGUgb2YgQ29uZHVjdAoKIyMgT3VyIFBsZWRnZQpXZSBhcyBtZW1iZXJzLCBjb250cmlidXRvcnMsIGFuZCBsZWFkZXJzIHBsZWRnZSB0byBtYWtlIHBhcnRpY2lwYXRpb24gaW4gb3VyIGNvbW11bml0eSBhIGhhcmFzc21lbnQtZnJlZSBleHBlcmllbmNlIGZvciBldmVyeW9uZS4KCiMjIE91ciBTdGFuZGFyZHMKLSBCZSBlbXBhdGhldGljIGFuZCBraW5kCi0gQmUgcmVzcGVjdGZ1bCBvZiBkaWZmZXJpbmcgb3BpbmlvbnMKLSBBY2NlcHQgY29uc3RydWN0aXZlIGZlZWRiYWNrCi0gT3duIG1pc3Rha2VzIGFuZCBsZWFybiBmcm9tIHRoZW0KClVuYWNjZXB0YWJsZSBiZWhhdmlvciBpbmNsdWRlcyBzZXh1YWxpemVkIGxhbmd1YWdlL2ltYWdlcnksIHRyb2xsaW5nLCBoYXJhc3NtZW50LCBkb3hpbmcsIGFuZCBvdGhlciBpbmFwcHJvcHJpYXRlIGNvbmR1Y3QuCgojIyBFbmZvcmNlbWVudApSZXBvcnQgaW5jaWRlbnRzIHRvICoqaGVsbG9AbW9rb2NvbnN1bHRpbmcudGVjaCoqIG9yIHRocm91Z2ggR2l0SHViIERpc2N1c3Npb25zIGlmIHlvdSBwcmVmZXIgYSBjb21tdW5pdHktdmlzaWJsZSBhcHByb2FjaC4gUHJpdmF0ZSBjb21wbGFpbnRzIHdpbGwgYmUgcmV2aWV3ZWQgcHJvbXB0bHkgYW5kIGZhaXJseS4KCiMjIEVuZm9yY2VtZW50IEd1aWRlbGluZXMKMS4gKipDb3JyZWN0aW9uKiog4oCUIFByaXZhdGUgd2FybmluZyAgCjIuICoqV2FybmluZyoqIOKAlCBGb3JtYWwgd2FybmluZyBhbmQgbGltaXRlZCBpbnRlcmFjdGlvbiAgCjMuICoqVGVtcG9yYXJ5IEJhbioqIOKAlCBUaW1lLWJveGVkIGV4Y2x1c2lvbiAgCjQuICoqUGVybWFuZW50IEJhbioqIOKAlCBSZW1vdmFsIGZyb20gdGhlIGNvbW11bml0eQoKIyMgQXR0cmlidXRpb24KQWRhcHRlZCBmcm9tIHRoZSBDb250cmlidXRvciBDb3ZlbmFudCB2Mi4xLgo= \ No newline at end of file + + +# Contributor Covenant Code of Conduct + +## Our Pledge +We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone. + +## Our Standards +- Be empathetic and kind +- Be respectful of differing opinions +- Accept constructive feedback +- Own mistakes and learn from them + +Unacceptable behavior includes sexualized language/imagery, trolling, harassment, doxing, and other inappropriate conduct. + +## Enforcement +Report incidents to **hello@mokoconsulting.tech** or through GitHub Discussions if you prefer a community-visible approach. Private complaints will be reviewed promptly and fairly. + +## Enforcement Guidelines +1. **Correction** — Private warning +2. **Warning** — Formal warning and limited interaction +3. **Temporary Ban** — Time-boxed exclusion +4. **Permanent Ban** — Removal from the community + +## Attribution +Adapted from the Contributor Covenant v2.1. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1ef721a..bf60cc5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1 +1,161 @@ -IyBDb250cmlidXRpbmcgdG8gTW9rbyBDb25zdWx0aW5nIFByb2plY3RzDQoNClRoYW5rIHlvdSBmb3IgeW91ciBpbnRlcmVzdCBpbiBjb250cmlidXRpbmcuIEFsbCBNb2tvIENvbnN1bHRpbmcgcmVwb3NpdG9yaWVzIGZvbGxvdyB0aGlzIHVuaXZlcnNhbCB3b3JrZmxvdyBhbmQgdmVyc2lvbiBwb2xpY3kuDQoNCiMjIEJyYW5jaGluZyBXb3JrZmxvdw0KDQpgYGANCmZlYXR1cmUvKiDilIDilIBQUuKUgOKUgD4gZGV2IOKUgOKUgGRyYWZ0IFBS4pSA4pSAPiAocmVuYW1lZCB0byByYykg4pSA4pSAbWVyZ2XilIDilIA+IG1haW4NCmBgYA0KDQojIyMgU3RlcCBieSBzdGVwDQoNCjEuICoqQ3JlYXRlIGEgZmVhdHVyZSBicmFuY2gqKiBmcm9tIGBkZXZgOg0KICAgYGBgYmFzaA0KICAgZ2l0IGNoZWNrb3V0IGRldiAmJiBnaXQgcHVsbA0KICAgZ2l0IGNoZWNrb3V0IC1iIGZlYXR1cmUvbXktY2hhbmdlDQogICBgYGANCg0KMi4gKipXb3JrIGFuZCBjb21taXQqKiBvbiB5b3VyIGZlYXR1cmUgYnJhbmNoLiBQdXNoIHRvIG9yaWdpbi4NCg0KMy4gKipPcGVuIGEgUFIqKjogYGZlYXR1cmUvbXktY2hhbmdlYCDihpIgYGRldmAuIEFmdGVyIHJldmlldyBhbmQgY2hlY2tzLCBtZXJnZSBpdC4NCg0KNC4gKipXaGVuIHJlYWR5IGZvciByZWxlYXNlKiosIG9wZW4gYSAqKmRyYWZ0IFBSKio6IGBkZXZgIOKGkiBgbWFpbmAuDQogICAtIFRoaXMgYXV0b21hdGljYWxseSByZW5hbWVzIHRoZSBzb3VyY2UgYnJhbmNoIHRvIGByY2AgKHJlbGVhc2UgY2FuZGlkYXRlKQ0KICAgLSBBbiBSQyBwcmUtcmVsZWFzZSBpcyBidWlsdCBhbmQgdXBsb2FkZWQNCg0KNS4gKipBbHBoYSBhbmQgYmV0YSBicmFuY2hlcyoqIGFyZSBjcmVhdGVkIGJ5IG1hbnVhbGx5IHJlbmFtaW5nIHRoZSBicmFuY2ggYmVmb3JlIHRoZSBSQyBzdGFnZToNCiAgIC0gUmVuYW1lIGBkZXZgIHRvIGBhbHBoYWAgZm9yIGVhcmx5IHRlc3Rpbmcg4oaSIGFscGhhIHByZS1yZWxlYXNlIGlzIGJ1aWx0DQogICAtIFJlbmFtZSBgYWxwaGFgIHRvIGBiZXRhYCBmb3IgZmVhdHVyZS1jb21wbGV0ZSB0ZXN0aW5nIOKGkiBiZXRhIHByZS1yZWxlYXNlIGlzIGJ1aWx0DQogICAtIFdoZW4gdGhlIGRyYWZ0IFBSIGlzIGNyZWF0ZWQsIHRoZSBicmFuY2ggaXMgcmVuYW1lZCB0byBgcmNgDQoNCjYuICoqT25jZSBQUiBjaGVja3MgcGFzcyoqIG9uIHRoZSBgcmNgIGJyYW5jaCwgbWFyayB0aGUgUFIgYXMgcmVhZHkgYW5kIG1lcmdlIHRvIGBtYWluYC4NCg0KNy4gKipNZXJnaW5nIHRvIG1haW4qKiB0cmlnZ2VycyB0aGUgc3RhYmxlIHJlbGVhc2UgcGlwZWxpbmU6DQogICAtIE1pbm9yIHZlcnNpb24gYnVtcCAoZS5nLiwgYDAyLjA5Lnh4YCDihpIgYDAyLjEwLjAwYCkNCiAgIC0gU3RhYmlsaXR5IHN1ZmZpeCBzdHJpcHBlZCAoY2xlYW4gdmVyc2lvbikNCiAgIC0gR2l0ZWEgcmVsZWFzZSBjcmVhdGVkIHdpdGggWklQL3Rhci5neiBwYWNrYWdlcw0KICAgLSBgdXBkYXRlcy54bWxgIHVwZGF0ZWQgKEpvb21sYSBleHRlbnNpb25zKQ0KICAgLSBgZGV2YCBicmFuY2ggcmVjcmVhdGVkIGZyb20gYG1haW5gDQoNCiMjIyBCcmFuY2ggc3VtbWFyeQ0KDQp8IEJyYW5jaCB8IFB1cnBvc2UgfCBDcmVhdGVkIGJ5IHwNCnwtLS0tLS0tLXwtLS0tLS0tLS18LS0tLS0tLS0tLS18DQp8IGBmZWF0dXJlLypgIHwgTmV3IGZlYXR1cmVzIGFuZCBmaXhlcyB8IERldmVsb3BlciB8DQp8IGBkZXZgIHwgSW50ZWdyYXRpb24gYnJhbmNoIHwgQXV0by1yZWNyZWF0ZWQgYWZ0ZXIgcmVsZWFzZSB8DQp8IGBhbHBoYWAgfCBBbHBoYSBwcmUtcmVsZWFzZSB0ZXN0aW5nIHwgTWFudWFsIHJlbmFtZSBmcm9tIGBkZXZgIHwNCnwgYGJldGFgIHwgQmV0YSBwcmUtcmVsZWFzZSB0ZXN0aW5nIHwgTWFudWFsIHJlbmFtZSBmcm9tIGBhbHBoYWAgfA0KfCBgcmNgIHwgUmVsZWFzZSBjYW5kaWRhdGUgfCBBdXRvLXJlbmFtZWQgb24gZHJhZnQgUFIgdG8gbWFpbiB8DQp8IGBtYWluYCB8IFN0YWJsZSByZWxlYXNlcyB8IFByb3RlY3RlZCwgbWVyZ2Ugb25seSB8DQp8IGB2ZXJzaW9uL1hYLllZLlpaYCB8IEFyY2hpdmVkIHJlbGVhc2Ugc25hcHNob3RzIHwgQXV0by1jcmVhdGVkIGJ5IENJIHwNCg0KIyMjIFByb3RlY3RlZCBicmFuY2hlcw0KDQp8IEJyYW5jaCB8IERpcmVjdCBwdXNoIHwgTWVyZ2UgdmlhIHwNCnwtLS0tLS0tLXwtLS0tLS0tLS0tLS18LS0tLS0tLS0tLS18DQp8IGBtYWluYCB8IEJsb2NrZWQgKENJIGJvdCB3aGl0ZWxpc3RlZCkgfCBQUiBtZXJnZSBvbmx5IHwNCnwgYGRldmAgfCBCbG9ja2VkIChDSSBib3Qgd2hpdGVsaXN0ZWQpIHwgUFIgbWVyZ2UgZnJvbSBmZWF0dXJlLyogfA0KfCBgcmNgIHwgQmxvY2tlZCAoQ0kgYm90IHdoaXRlbGlzdGVkKSB8IEF1dG8tY3JlYXRlZCBvbiBkcmFmdCBQUiB8DQp8IGBhbHBoYWAgfCBCbG9ja2VkIChDSSBib3Qgd2hpdGVsaXN0ZWQpIHwgTWFudWFsIHJlbmFtZSB8DQp8IGBiZXRhYCB8IEJsb2NrZWQgKENJIGJvdCB3aGl0ZWxpc3RlZCkgfCBNYW51YWwgcmVuYW1lIHwNCnwgYGZlYXR1cmUvKmAgfCBPcGVuIHwgTi9BIChzb3VyY2UgYnJhbmNoKSB8DQoNCiMjIFZlcnNpb24gUG9saWN5DQoNCiMjIyBGb3JtYXQNCg0KQWxsIHZlcnNpb25zIHVzZSBgWFguWVkuWlpgIOKAlCB0aHJlZSB0d28tZGlnaXQgc2VnbWVudHMsIHplcm8tcGFkZGVkOg0KDQotICoqWFgqKiDigJQgTWFqb3IgdmVyc2lvbiAoYnJlYWtpbmcgY2hhbmdlcykNCi0gKipZWSoqIOKAlCBNaW5vciB2ZXJzaW9uIChuZXcgZmVhdHVyZXMsIGJ1bXBlZCBvbiByZWxlYXNlIHRvIG1haW4pDQotICoqWloqKiDigJQgUGF0Y2ggdmVyc2lvbiAoYXV0by1pbmNyZW1lbnRlZCBvbiBldmVyeSBwdXNoIHRvIGRldi9mZWF0dXJlIGJyYW5jaGVzKQ0KDQpSb2xsb3ZlcjogcGF0Y2ggYDk5YCDihpIgYDAwYCBpbmNyZW1lbnRzIG1pbm9yOyBtaW5vciBgOTlgIOKGkiBgMDBgIGluY3JlbWVudHMgbWFqb3IuDQoNCiMjIyBTdGFiaWxpdHkgc3VmZml4ZXMNCg0KRWFjaCBicmFuY2ggYXBwZW5kcyBhIHN1ZmZpeCB0byBpbmRpY2F0ZSBzdGFiaWxpdHk6DQoNCnwgQnJhbmNoIHwgU3VmZml4IHwgRXhhbXBsZSB8DQp8LS0tLS0tLS18LS0tLS0tLS18LS0tLS0tLS0tfA0KfCBgbWFpbmAgfCAobm9uZSkgfCBgMDIuMDkuMDBgIHwNCnwgYGRldmAgfCBgLWRldmAgfCBgMDIuMDkuMDEtZGV2YCB8DQp8IGBmZWF0dXJlLypgIHwgYC1kZXZgIHwgYDAyLjA5LjAxLWRldmAgfA0KfCBgYWxwaGFgIHwgYC1hbHBoYWAgfCBgMDIuMDkuMDEtYWxwaGFgIHwNCnwgYGJldGFgIHwgYC1iZXRhYCB8IGAwMi4wOS4wMS1iZXRhYCB8DQp8IGByY2AgfCBgLXJjYCB8IGAwMi4wOS4wMS1yY2AgfA0KDQojIyMgQXV0byB2ZXJzaW9uIGJ1bXANCg0KT24gZXZlcnkgcHVzaCB0byBgZGV2YCwgYGZlYXR1cmUvKmAsIG9yIGBwYXRjaC8qYDoNCg0KMS4gUGF0Y2ggdmVyc2lvbiBpbmNyZW1lbnRlZA0KMi4gU3RhYmlsaXR5IHN1ZmZpeCBgLWRldmAgYXBwbGllZA0KMy4gQWxsIHZlcnNpb24tYmVhcmluZyBmaWxlcyB1cGRhdGVkIChtYW5pZmVzdHMsIENIQU5HRUxPRywgUEhQIGhlYWRlcnMsIGV0Yy4pDQo0LiBDb21taXQgY3JlYXRlZCB3aXRoIGBbc2tpcCBjaV1gIHRvIGF2b2lkIGxvb3BzDQoNCiMjIyBSZWxlYXNlIHZlcnNpb24gZmxvdw0KDQpWZXJzaW9uIGJ1bXBzIGhhcHBlbiBhdCBzcGVjaWZpYyByZWxlYXNlIGV2ZW50czoNCg0KfCBFdmVudCB8IEJ1bXAgfCBFeGFtcGxlIHwNCnwtLS0tLS0tfC0tLS0tLXwtLS0tLS0tLS18DQp8IEZlYXR1cmUgbWVyZ2VkIHRvIGRldiB8IFBhdGNoIGJ1bXAgYWZ0ZXIgZGV2IHJlbGVhc2UgfCBgMDIuMDkuMDEtZGV2YCDihpIgcmVsZWFzZSDihpIgYDAyLjA5LjAyLWRldmAgfA0KfCBEZXYgcHJvbW90ZWQgdG8gUkMgfCBNaW5vciBidW1wIHwgYDAyLjA5LjAyLWRldmAg4oaSIGAwMi4xMC4wMC1yY2AgfA0KfCBSQyBtZXJnZWQgdG8gbWFpbiB8IE1pbm9yIGJ1bXAgfCBgMDIuMTAuMDAtcmNgIOKGkiBgMDIuMTEuMDBgIChzdGFibGUpIHwNCnwgRGV2IHJlY3JlYXRlZCBmcm9tIG1haW4gfCBQYXRjaCBidW1wIHwgYDAyLjExLjAwYCDihpIgYDAyLjExLjAxLWRldmAgfA0KDQojIyMgUmVsZWFzZSBzdHJlYW0gY29waWVzDQoNCldoZW4gYSBoaWdoZXItc3RhYmlsaXR5IHJlbGVhc2UgaXMgcHVibGlzaGVkLCBjb3BpZXMgYXJlIGNyZWF0ZWQgZm9yIGFsbCBsZXNzZXIgc3RyZWFtcyB3aXRoIHRoZSBzYW1lIGJhc2UgdmVyc2lvbjoNCg0KLSAqKlJDIGAwMi4xMC4wMC1yY2AqKiBhbHNvIGNyZWF0ZXM6IGAwMi4xMC4wMC1kZXZgLCBgMDIuMTAuMDAtYWxwaGFgLCBgMDIuMTAuMDAtYmV0YWANCi0gKipTdGFibGUgYDAyLjExLjAwYCoqIGFsc28gY3JlYXRlczogYDAyLjExLjAwLWRldmAsIGAwMi4xMS4wMC1hbHBoYWAsIGAwMi4xMS4wMC1iZXRhYCwgYDAyLjExLjAwLXJjYA0KDQpUaGlzIGVuc3VyZXMgSm9vbWxhIHNpdGVzIG9uIEFOWSBzdGFiaWxpdHkgY2hhbm5lbCBzZWUgdGhlIHVwZGF0ZSAoSm9vbWxhIG9ubHkgc2hvd3MgdmVyc2lvbnMgaGlnaGVyIHRoYW4gd2hhdCdzIGluc3RhbGxlZCkuDQoNCiMjIyBWZXJzaW9uIGZpbGVzDQoNClRoZSB2ZXJzaW9uIHRvb2xzIHVwZGF0ZSBhbGwgZmlsZXMgY29udGFpbmluZyB2ZXJzaW9uIHN0YW1wczoNCg0KLSBgLm1va29naXRlYS9tYW5pZmVzdC54bWxgIChjYW5vbmljYWwgc291cmNlKQ0KLSBKb29tbGEgWE1MIG1hbmlmZXN0cyAoYDx2ZXJzaW9uPmAgdGFnKQ0KLSBgUkVBRE1FLm1kYCwgYENIQU5HRUxPRy5tZGAgKGBWRVJTSU9OOmAgcGF0dGVybikNCi0gYHBhY2thZ2UuanNvbmAsIGBweXByb2plY3QudG9tbGANCi0gQW55IHRleHQgZmlsZSB3aXRoIGEgYFZFUlNJT046IFhYLllZLlpaYCBsYWJlbA0KDQpGaWxlcyBzeW5jZWQgZnJvbSBvdGhlciByZXBvcyAod2l0aCBhIGAjIFJFUE86YCBoZWFkZXIpIGFyZSBub3QgdG91Y2hlZC4NCg0KIyMgQ29kZSBTdGFuZGFyZHMNCg0KLSAqKlBIUCoqOiBQU1ItMTIsIHRhYnMgZm9yIGluZGVudGF0aW9uDQotICoqQ29weXJpZ2h0Kio6IGFsbCBmaWxlcyBtdXN0IGluY2x1ZGUgdGhlIE1va28gQ29uc3VsdGluZyBjb3B5cmlnaHQgaGVhZGVyDQotICoqTGljZW5zZSoqOiBTUERYIGlkZW50aWZpZXIgYEdQTC0zLjAtb3ItbGF0ZXJgIChvciBhcyBzcGVjaWZpZWQgcGVyIHJlcG8pDQotICoqQXR0cmlidXRpb24qKjogdXNlIGBBdXRob3JlZC1ieTogTW9rbyBDb25zdWx0aW5nYCBpbiBjb21taXRzLCBub3QgaW5kaXZpZHVhbCBuYW1lcw0KDQojIyBDb21taXQgTWVzc2FnZXMNCg0KVXNlIGNvbnZlbnRpb25hbCBjb21taXQgZm9ybWF0Og0KDQpgYGANCnR5cGUoc2NvcGUpOiBzaG9ydCBkZXNjcmlwdGlvbg0KDQpPcHRpb25hbCBib2R5IHdpdGggY29udGV4dC4NCg0KQXV0aG9yZWQtYnk6IE1va28gQ29uc3VsdGluZw0KYGBgDQoNClR5cGVzOiBgZmVhdGAsIGBmaXhgLCBgY2hvcmVgLCBgZG9jc2AsIGBzdHlsZWAsIGByZWZhY3RvcmAsIGB0ZXN0YCwgYGNpYA0KDQpTcGVjaWFsIGZsYWdzIGluIGNvbW1pdCBtZXNzYWdlczoNCi0gYFtza2lwIGNpXWAg4oCUIHNraXAgYWxsIENJIHdvcmtmbG93cw0KLSBgW3NraXAgYnVtcF1gIOKAlCBza2lwIGF1dG8gdmVyc2lvbiBidW1wIG9ubHkNCg0KIyMgUmVwb3J0aW5nIElzc3Vlcw0KDQpVc2UgdGhlIHJlcG9zaXRvcnkncyBpc3N1ZSB0cmFja2VyIHdpdGggdGhlIGFwcHJvcHJpYXRlIHRlbXBsYXRlLg0KDQotLS0NCg0KKk1va28gQ29uc3VsdGluZyA8aGVsbG9AbW9rb2NvbnN1bHRpbmcudGVjaD4qDQo= \ No newline at end of file +# Contributing to Moko Consulting Projects + +Thank you for your interest in contributing. All Moko Consulting repositories follow this universal workflow and version policy. + +## Branching Workflow + +``` +feature/* ──PR──> dev ──draft PR──> (renamed to rc) ──merge──> main +``` + +### Step by step + +1. **Create a feature branch** from `dev`: + ```bash + git checkout dev && git pull + git checkout -b feature/my-change + ``` + +2. **Work and commit** on your feature branch. Push to origin. + +3. **Open a PR**: `feature/my-change` → `dev`. After review and checks, merge it. + +4. **When ready for release**, open a **draft PR**: `dev` → `main`. + - This automatically renames the source branch to `rc` (release candidate) + - An RC pre-release is built and uploaded + +5. **Alpha and beta branches** are created by manually renaming the branch before the RC stage: + - Rename `dev` to `alpha` for early testing → alpha pre-release is built + - Rename `alpha` to `beta` for feature-complete testing → beta pre-release is built + - When the draft PR is created, the branch is renamed to `rc` + +6. **Once PR checks pass** on the `rc` branch, mark the PR as ready and merge to `main`. + +7. **Merging to main** triggers the stable release pipeline: + - Minor version bump (e.g., `02.09.xx` → `02.10.00`) + - Stability suffix stripped (clean version) + - Gitea release created with ZIP/tar.gz packages + - `updates.xml` updated (Joomla extensions) + - `dev` branch recreated from `main` + +### Branch summary + +| Branch | Purpose | Created by | +|--------|---------|-----------| +| `feature/*` | New features and fixes | Developer | +| `dev` | Integration branch | Auto-recreated after release | +| `alpha` | Alpha pre-release testing | Manual rename from `dev` | +| `beta` | Beta pre-release testing | Manual rename from `alpha` | +| `rc` | Release candidate | Auto-renamed on draft PR to main | +| `main` | Stable releases | Protected, merge only | +| `version/XX.YY.ZZ` | Archived release snapshots | Auto-created by CI | + +### Protected branches + +| Branch | Direct push | Merge via | +|--------|------------|-----------| +| `main` | Blocked (CI bot whitelisted) | PR merge only | +| `dev` | Blocked (CI bot whitelisted) | PR merge from feature/* | +| `rc` | Blocked (CI bot whitelisted) | Auto-created on draft PR | +| `alpha` | Blocked (CI bot whitelisted) | Manual rename | +| `beta` | Blocked (CI bot whitelisted) | Manual rename | +| `feature/*` | Open | N/A (source branch) | + +## Version Policy + +### Format + +All versions use `XX.YY.ZZ` — three two-digit segments, zero-padded: + +- **XX** — Major version (breaking changes) +- **YY** — Minor version (new features, bumped on release to main) +- **ZZ** — Patch version (auto-incremented on every push to dev/feature branches) + +Rollover: patch `99` → `00` increments minor; minor `99` → `00` increments major. + +### Stability suffixes + +Each branch appends a suffix to indicate stability: + +| Branch | Suffix | Example | +|--------|--------|---------| +| `main` | (none) | `02.09.00` | +| `dev` | `-dev` | `02.09.01-dev` | +| `feature/*` | `-dev` | `02.09.01-dev` | +| `alpha` | `-alpha` | `02.09.01-alpha` | +| `beta` | `-beta` | `02.09.01-beta` | +| `rc` | `-rc` | `02.09.01-rc` | + +### Auto version bump + +On every push to `dev`, `feature/*`, or `patch/*`: + +1. Patch version incremented +2. Stability suffix `-dev` applied +3. All version-bearing files updated (manifests, CHANGELOG, PHP headers, etc.) +4. Commit created with `[skip ci]` to avoid loops + +### Release version flow + +Version bumps happen at specific release events: + +| Event | Bump | Example | +|-------|------|---------| +| Feature merged to dev | Patch bump after dev release | `02.09.01-dev` → release → `02.09.02-dev` | +| Dev promoted to RC | Minor bump | `02.09.02-dev` → `02.10.00-rc` | +| RC merged to main | Minor bump | `02.10.00-rc` → `02.11.00` (stable) | +| Dev recreated from main | Patch bump | `02.11.00` → `02.11.01-dev` | + +### Release stream copies + +When a higher-stability release is published, copies are created for all lesser streams with the same base version: + +- **RC `02.10.00-rc`** also creates: `02.10.00-dev`, `02.10.00-alpha`, `02.10.00-beta` +- **Stable `02.11.00`** also creates: `02.11.00-dev`, `02.11.00-alpha`, `02.11.00-beta`, `02.11.00-rc` + +This ensures Joomla sites on ANY stability channel see the update (Joomla only shows versions higher than what's installed). + +### Version files + +The version tools update all files containing version stamps: + +- `.mokogitea/manifest.xml` (canonical source) +- Joomla XML manifests (`` tag) +- `README.md`, `CHANGELOG.md` (`VERSION:` pattern) +- `package.json`, `pyproject.toml` +- Any text file with a `VERSION: XX.YY.ZZ` label + +Files synced from other repos (with a `# REPO:` header) are not touched. + +## Code Standards + +- **PHP**: PSR-12, tabs for indentation +- **Copyright**: all files must include the Moko Consulting copyright header +- **License**: SPDX identifier `GPL-3.0-or-later` (or as specified per repo) +- **Attribution**: use `Authored-by: Moko Consulting` in commits, not individual names + +## Commit Messages + +Use conventional commit format: + +``` +type(scope): short description + +Optional body with context. + +Authored-by: Moko Consulting +``` + +Types: `feat`, `fix`, `chore`, `docs`, `style`, `refactor`, `test`, `ci` + +Special flags in commit messages: +- `[skip ci]` — skip all CI workflows +- `[skip bump]` — skip auto version bump only + +## Reporting Issues + +Use the repository's issue tracker with the appropriate template. + +--- + +*Moko Consulting * diff --git a/GOVERNANCE.md b/GOVERNANCE.md index bad8ed0..47fa254 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -1 +1,119 @@ -PCEtLQogQ29weXJpZ2h0IChDKSAyMDI2IE1va28gQ29uc3VsdGluZyA8aGVsbG9AbW9rb2NvbnN1bHRpbmcudGVjaD4KCiBUaGlzIGZpbGUgaXMgcGFydCBvZiBhIE1va28gQ29uc3VsdGluZyBwcm9qZWN0LgoKIFNQRFgtTGljZW5zZS1JZGVudGlmaWVyOiBHUEwtMy4wLW9yLWxhdGVyCgogVGhpcyBwcm9ncmFtIGlzIGZyZWUgc29mdHdhcmU7IHlvdSBjYW4gcmVkaXN0cmlidXRlIGl0IGFuZC9vciBtb2RpZnkgaXQgdW5kZXIgdGhlIHRlcm1zIG9mCiB0aGUgR05VIEdlbmVyYWwgUHVibGljIExpY2Vuc2UgYXMgcHVibGlzaGVkIGJ5IHRoZSBGcmVlIFNvZnR3YXJlIEZvdW5kYXRpb247IGVpdGhlciB2ZXJzaW9uIDMKIG9mIHRoZSBMaWNlbnNlLCBvciAoYXQgeW91ciBvcHRpb24pIGFueSBsYXRlciB2ZXJzaW9uLgoKIFRoaXMgcHJvZ3JhbSBpcyBkaXN0cmlidXRlZCBpbiB0aGUgaG9wZSB0aGF0IGl0IHdpbGwgYmUgdXNlZnVsLCBidXQgV0lUSE9VVCBBTlkgV0FSUkFOVFk7CiB3aXRob3V0IGV2ZW4gdGhlIGltcGxpZWQgd2FycmFudHkgb2YgTUVSQ0hBTlRBQklMSVRZIG9yIEZJVE5FU1MgRk9SIEEgUEFSVElDVUxBUiBQVVJQT1NFLgogU2VlIHRoZSBHTlUgR2VuZXJhbCBQdWJsaWMgTGljZW5zZSBmb3IgbW9yZSBkZXRhaWxzLgoKIFlvdSBzaG91bGQgaGF2ZSByZWNlaXZlZCBhIGNvcHkgb2YgdGhlIEdOVSBHZW5lcmFsIFB1YmxpYyBMaWNlbnNlICguL0xJQ0VOU0UpLgoKIEZJTEUgSU5GT1JNQVRJT04KIERFRkdST1VQOiBtb2tvY29uc3VsdGluZy10ZWNoLlRlbXBsYXRlLUpvb21sYQogSU5HUk9VUDogTW9rb1N0YW5kYXJkcy5Hb3Zlcm5hbmNlCiBSRVBPOiBodHRwczovL2dpdGh1Yi5jb20vbW9rb2NvbnN1bHRpbmctdGVjaC9UZW1wbGF0ZS1Kb29tbGEKIFZFUlNJT046IDAxLjAxLjAwCiBQQVRIOiAvR09WRVJOQU5DRS5tZAogQlJJRUY6IFByb2plY3QgZ292ZXJuYW5jZSBydWxlcywgcm9sZXMsIGFuZCBkZWNpc2lvbiBwcm9jZXNzIGZvciBUZW1wbGF0ZS1Kb29tbGEKLS0+CgpbIVtNb2tvU3RhbmRhcmRzXShodHRwczovL2ltZy5zaGllbGRzLmlvL2JhZGdlL01va29TdGFuZGFyZHMtMDQuMDAuMDQtYmx1ZSldKGh0dHBzOi8vZ2l0aHViLmNvbS9tb2tvY29uc3VsdGluZy10ZWNoL01va29TdGFuZGFyZHMpCgojIFByb2plY3QgR292ZXJuYW5jZQoKIyMgT3ZlcnZpZXcKClRoaXMgZG9jdW1lbnQgZGVmaW5lcyB0aGUgZ292ZXJuYW5jZSBtb2RlbCBmb3IgdGhlIGBUZW1wbGF0ZS1Kb29tbGFgIHJlcG9zaXRvcnkgd2l0aGluIHRoZQpgbW9rb2NvbnN1bHRpbmctdGVjaGAgb3JnYW5pemF0aW9uLiBJdCBpcyBhdXRvbWF0aWNhbGx5IG1haW50YWluZWQgYnkKW01va29TdGFuZGFyZHNdKGh0dHBzOi8vZ2l0aHViLmNvbS9tb2tvY29uc3VsdGluZy10ZWNoL01va29TdGFuZGFyZHMpIHYwNC4wMC4wNC4KCkZ1bGwgZ292ZXJuYW5jZSBwb2xpY3kgaXMgZGVmaW5lZCBpbiB0aGUgTW9rb1N0YW5kYXJkcyBzb3VyY2UgcmVwb3NpdG9yeToKW2RvY3MvcG9saWN5L0dPVkVSTkFOQ0UubWRdKGh0dHBzOi8vZ2l0aHViLmNvbS9tb2tvY29uc3VsdGluZy10ZWNoL01va29TdGFuZGFyZHMvYmxvYi9tYWluL2RvY3MvcG9saWN5L0dPVkVSTkFOQ0UubWQpCgotLS0KCiMjIFJvbGVzIGFuZCBSZXNwb25zaWJpbGl0aWVzCgojIyMgTWFpbnRhaW5lcgoKKipHaXRIdWIqKjogQG1va29jb25zdWx0aW5nLXRlY2gKCioqQXV0aG9yaXR5Kio6IEZpbmFsIGRlY2lzaW9uLW1ha2luZyBhdXRob3JpdHkgb24gYWxsIG1hdHRlcnMgZm9yIHRoaXMgcmVwb3NpdG9yeS4KCioqUmVzcG9uc2liaWxpdGllcyoqOgotIFJldmlldyBhbmQgbWVyZ2UgcHVsbCByZXF1ZXN0cwotIE1haW50YWluIGNvZGUgcXVhbGl0eSBhbmQgc3RhbmRhcmRzIGNvbXBsaWFuY2UKLSBNYW5hZ2UgcmVsZWFzZXMgYW5kIHZlcnNpb25pbmcKLSBSZXNwb25kIHRvIGlzc3VlcyBhbmQgc2VjdXJpdHkgcmVwb3J0cwoKIyMjIENvbnRyaWJ1dG9ycwoKKipBdXRob3JpdHkqKjogU3VibWl0IGNoYW5nZXMgdmlhIHB1bGwgcmVxdWVzdHMuCgoqKlJlcXVpcmVtZW50cyoqOgotIFJlYWQgYW5kIGFjY2VwdCBgQ09ERV9PRl9DT05EVUNULm1kYAotIEZvbGxvdyBgQ09OVFJJQlVUSU5HLm1kYCBndWlkZWxpbmVzCgotLS0KCiMjIERlY2lzaW9uLU1ha2luZwoKQWxsIGNoYW5nZXMgbXVzdCBiZSBzdWJtaXR0ZWQgYXMgcHVsbCByZXF1ZXN0cy4gVGhlIG1haW50YWluZXIgKEBtb2tvY29uc3VsdGluZy10ZWNoKQpyZXZpZXdzIGFuZCBhcHByb3ZlcyBhbGwgY2hhbmdlcyBiZWZvcmUgdGhleSBhcmUgbWVyZ2VkLgoKIyMjIFNvbGUgT3BlcmF0b3IgUG9saWN5CgpUaGlzIG9yZ2FuaXphdGlvbiBvcGVyYXRlcyB1bmRlciBhICoqc29sZSBvcGVyYXRvcioqIG1vZGVsLiBUaGUgbWFpbnRhaW5lciAoQG1va29jb25zdWx0aW5nLXRlY2gpCmlzIHRoZSBzb2xlIGVtcGxveWVlIGFuZCBvd25lciBhbmQgbWF5IHNlbGYtYXBwcm92ZSBwdWxsIHJlcXVlc3RzIHdoZW4gbm8gc2Vjb25kIHJldmlld2VyIGlzCmF2YWlsYWJsZS4gVGhlIGZvbGxvd2luZyByZXF1aXJlbWVudHMgcmVtYWluIG1hbmRhdG9yeSByZWdhcmRsZXNzOgoKMS4gKipQdWxsIFJlcXVlc3RzIFJlcXVpcmVkKiog4oCUIGFsbCBjaGFuZ2VzIHRvIHByb3RlY3RlZCBicmFuY2hlcyBnbyB0aHJvdWdoIGEgUFIuCjIuICoqQXV0b21hdGVkIENoZWNrcyoqIOKAlCBhbGwgQ0kgY2hlY2tzIG11c3QgcGFzcyBiZWZvcmUgbWVyZ2luZy4KMy4gKipBdWRpdCBUcmFpbCoqIOKAlCBpc3N1ZXMsIHB1bGwgcmVxdWVzdHMsIGFuZCBjb21taXQgaGlzdG9yeSBhcmUgcHJlc2VydmVkLgo0LiAqKkRvY3VtZW50YXRpb24qKiDigJQgY2hhbmdlcyBhcmUgZG9jdW1lbnRlZCBpbiBgQ0hBTkdFTE9HLm1kYC4KClNlZSB0aGUgZnVsbCBwb2xpY3k6CltTb2xlIE9wZXJhdG9yIFBvbGljeV0oaHR0cHM6Ly9naXRodWIuY29tL21va29jb25zdWx0aW5nLXRlY2gvTW9rb1N0YW5kYXJkcy9ibG9iL21haW4vZG9jcy9wb2xpY3kvR09WRVJOQU5DRS5tZCNzb2xlLW9wZXJhdG9yLXBvbGljeSkKCi0tLQoKIyMgQ2hhbmdlIE1hbmFnZW1lbnQKCnwgQ2hhbmdlIFR5cGUgfCBBcHByb3ZhbCB8IFByb2Nlc3MgfAp8LS0tLS0tLS0tLS0tLXwtLS0tLS0tLS0tfC0tLS0tLS0tLXwKfCBSb3V0aW5lIChkb2NzLCBidWcgZml4ZXMpIHwgTWFpbnRhaW5lciB8IFBSIOKGkiBDSSBwYXNzIOKGkiBtZXJnZSB8CnwgU2lnbmlmaWNhbnQgKG5ldyBmZWF0dXJlcykgfCBNYWludGFpbmVyIHwgUFIgd2l0aCBkZXNjcmlwdGlvbiDihpIgQ0kgcGFzcyDihpIgbWVyZ2UgfAp8IE1ham9yIChicmVha2luZywgYXJjaGl0ZWN0dXJlKSB8IE1haW50YWluZXIgfCBJc3N1ZSBkaXNjdXNzaW9uIOKGkiBQUiDihpIgQ0kgcGFzcyDihpIgbWVyZ2UgfAp8IEVtZXJnZW5jeSAoc2VjdXJpdHkpIHwgTWFpbnRhaW5lciB8IExhYmVsbGVkIGBFTUVSR0VOQ1lgIOKGkiBpbW1lZGlhdGUgbWVyZ2Ug4oaSIHBvc3QtbW9ydGVtIHwKCi0tLQoKIyMgUmVwb3J0aW5nIElzc3VlcwoKLSAqKkJ1Z3MgLyBGZWF0dXJlcyoqOiBPcGVuIGEgW0dpdEh1YiBJc3N1ZV0oaHR0cHM6Ly9naXRodWIuY29tL21va29jb25zdWx0aW5nLXRlY2gvVGVtcGxhdGUtSm9vbWxhL2lzc3VlcykKLSAqKlNlY3VyaXR5IHZ1bG5lcmFiaWxpdGllcyoqOiBTZWUgW1NFQ1VSSVRZLm1kXSguL1NFQ1VSSVRZLm1kKQotICoqQ29kZSBvZiBDb25kdWN0Kio6IFNlZSBbQ09ERV9PRl9DT05EVUNULm1kXSguL0NPREVfT0ZfQ09ORFVDVC5tZCkKLSAqKkNvbnRhY3QqKjogZGV2QG1va29jb25zdWx0aW5nLnRlY2gKCi0tLQoKIyMgTWV0YWRhdGEKCnwgRmllbGQgICAgICAgICB8IFZhbHVlICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHwKfCAtLS0tLS0tLS0tLS0tIHwgLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0gfAp8IERvY3VtZW50IFR5cGUgfCBQb2xpY3kgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB8CnwgRG9tYWluICAgICAgICB8IEdvdmVybmFuY2UgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHwKfCBBcHBsaWVzIFRvICAgIHwgbW9rb2NvbnN1bHRpbmctdGVjaC9UZW1wbGF0ZS1Kb29tbGEgICAgICAgICAgICAgICAgICAgICAgICAgICB8CnwgSnVyaXNkaWN0aW9uICB8IFRlbm5lc3NlZSwgVVNBICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHwKfCBNYWludGFpbmVyICAgIHwgQG1va29jb25zdWx0aW5nLXRlY2ggICAgICAgICAgICAgICAgICAgICAgICAgICAgfAp8IFN0YW5kYXJkcyAgICAgfCBNb2tvU3RhbmRhcmRzIHYwNC4wMC4wNCAgICAgICAgICAgIHwKfCBSZXBvICAgICAgICAgIHwgaHR0cHM6Ly9naXRodWIuY29tL21va29jb25zdWx0aW5nLXRlY2gvVGVtcGxhdGUtSm9vbWxhICAgICAgICB8CnwgUGF0aCAgICAgICAgICB8IC9HT1ZFUk5BTkNFLm1kICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHwKfCBTdGF0dXMgICAgICAgIHwgQWN0aXZlIOKAlCBhdXRvLW1haW50YWluZWQgYnkgTW9rb1N0YW5kYXJkcyAgICAgICB8Cg== \ No newline at end of file + + +[![MokoStandards](https://img.shields.io/badge/MokoStandards-04.00.04-blue)](https://github.com/mokoconsulting-tech/MokoStandards) + +# Project Governance + +## Overview + +This document defines the governance model for the `Template-Joomla` repository within the +`mokoconsulting-tech` organization. It is automatically maintained by +[MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards) v04.00.04. + +Full governance policy is defined in the MokoStandards source repository: +[docs/policy/GOVERNANCE.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/GOVERNANCE.md) + +--- + +## Roles and Responsibilities + +### Maintainer + +**GitHub**: @mokoconsulting-tech + +**Authority**: Final decision-making authority on all matters for this repository. + +**Responsibilities**: +- Review and merge pull requests +- Maintain code quality and standards compliance +- Manage releases and versioning +- Respond to issues and security reports + +### Contributors + +**Authority**: Submit changes via pull requests. + +**Requirements**: +- Read and accept `CODE_OF_CONDUCT.md` +- Follow `CONTRIBUTING.md` guidelines + +--- + +## Decision-Making + +All changes must be submitted as pull requests. The maintainer (@mokoconsulting-tech) +reviews and approves all changes before they are merged. + +### Sole Operator Policy + +This organization operates under a **sole operator** model. The maintainer (@mokoconsulting-tech) +is the sole employee and owner and may self-approve pull requests when no second reviewer is +available. The following requirements remain mandatory regardless: + +1. **Pull Requests Required** — all changes to protected branches go through a PR. +2. **Automated Checks** — all CI checks must pass before merging. +3. **Audit Trail** — issues, pull requests, and commit history are preserved. +4. **Documentation** — changes are documented in `CHANGELOG.md`. + +See the full policy: +[Sole Operator Policy](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/GOVERNANCE.md#sole-operator-policy) + +--- + +## Change Management + +| Change Type | Approval | Process | +|-------------|----------|---------| +| Routine (docs, bug fixes) | Maintainer | PR → CI pass → merge | +| Significant (new features) | Maintainer | PR with description → CI pass → merge | +| Major (breaking, architecture) | Maintainer | Issue discussion → PR → CI pass → merge | +| Emergency (security) | Maintainer | Labelled `EMERGENCY` → immediate merge → post-mortem | + +--- + +## Reporting Issues + +- **Bugs / Features**: Open a [GitHub Issue](https://github.com/mokoconsulting-tech/Template-Joomla/issues) +- **Security vulnerabilities**: See [SECURITY.md](./SECURITY.md) +- **Code of Conduct**: See [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md) +- **Contact**: dev@mokoconsulting.tech + +--- + +## Metadata + +| Field | Value | +| ------------- | ----------------------------------------------- | +| Document Type | Policy | +| Domain | Governance | +| Applies To | mokoconsulting-tech/Template-Joomla | +| Jurisdiction | Tennessee, USA | +| Maintainer | @mokoconsulting-tech | +| Standards | MokoStandards v04.00.04 | +| Repo | https://github.com/mokoconsulting-tech/Template-Joomla | +| Path | /GOVERNANCE.md | +| Status | Active — auto-maintained by MokoStandards | diff --git a/README.md b/README.md index 21a8ded..eeff7b8 100644 --- a/README.md +++ b/README.md @@ -1,44 +1,3 @@ - +# MokoSuiteTaxi -# MokoSuite Taxi - -Ride-hailing, dispatch, fleet management, fare zones, and driver scheduling module for MokoSuite on Joomla 6. - -## Overview - -MokoSuiteTaxi is a Layer 2 module in the MokoSuite platform, building on MokoSuiteClient (Layer 0) and MokoSuiteCRM (Layer 1) to provide complete taxi and ride-hailing operations management. - -## Features - -- **Ride Management** -- on-demand, scheduled, airport, and corporate ride types -- **Dispatch Engine** -- auto-dispatch with zone-aware driver matching and FOR UPDATE locking -- **Fleet Management** -- vehicle tracking, insurance/inspection expiry alerts, maintenance status -- **Driver Management** -- profiles linked to CRM contacts, approval workflow, background checks -- **Fare Zones** -- GeoJSON polygon boundaries with radius fallback, zone-based pricing -- **Dynamic Pricing** -- surge multiplier based on demand/supply ratio, night surcharges, peak hours -- **Ratings** -- bidirectional rider/driver ratings with feedback tags -- **Shift Scheduling** -- driver shift management with earnings tracking - -## Requirements - -- Joomla 6.x -- PHP 8.3+ -- MokoSuiteClient (Layer 0) -- MokoSuiteCRM (Layer 1) - -## Installation - -Install via Joomla Extension Manager using the package file `pkg_mokosuitetaxi.zip`. - -## License - -GNU General Public License v3.0 or later -- see [LICENSE](LICENSE). - -## Links - -- [Documentation](https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteTaxi/wiki) -- [Issues](https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteTaxi/issues) -- [MokoSuite Platform](https://mokoconsulting.tech) +MokoSuite Taxi — ride-hailing, dispatch, fleet management, fare zones, driver scheduling for Joomla 6 \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md index c7e8476..86b35ed 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1 +1,241 @@ -PCEtLQpDb3B5cmlnaHQgKEMpIDIwMjYgTW9rbyBDb25zdWx0aW5nIDxoZWxsb0Btb2tvY29uc3VsdGluZy50ZWNoPgoKVGhpcyBmaWxlIGlzIHBhcnQgb2YgYSBNb2tvIENvbnN1bHRpbmcgcHJvamVjdC4KClNQRFgtTGljZW5zZS1JZGVudGlmaWVyOiBHUEwtMy4wLW9yLWxhdGVyCgpUaGlzIHByb2dyYW0gaXMgZnJlZSBzb2Z0d2FyZTsgeW91IGNhbiByZWRpc3RyaWJ1dGUgaXQgYW5kL29yIG1vZGlmeSBpdCB1bmRlciB0aGUgdGVybXMgb2YKdGhlIEdOVSBHZW5lcmFsIFB1YmxpYyBMaWNlbnNlIGFzIHB1Ymxpc2hlZCBieSB0aGUgRnJlZSBTb2Z0d2FyZSBGb3VuZGF0aW9uOyBlaXRoZXIgdmVyc2lvbiAzCm9mIHRoZSBMaWNlbnNlLCBvciAoYXQgeW91ciBvcHRpb24pIGFueSBsYXRlciB2ZXJzaW9uLgoKVGhpcyBwcm9ncmFtIGlzIGRpc3RyaWJ1dGVkIGluIHRoZSBob3BlIHRoYXQgaXQgd2lsbCBiZSB1c2VmdWwsIGJ1dCBXSVRIT1VUIEFOWSBXQVJSQU5UWTsKd2l0aG91dCBldmVuIHRoZSBpbXBsaWVkIHdhcnJhbnR5IG9mIE1FUkNIQU5UQUJJTElUWSBvciBGSVRORVNTIEZPUiBBIFBBUlRJQ1VMQVIgUFVSUE9TRS4KU2VlIHRoZSBHTlUgR2VuZXJhbCBQdWJsaWMgTGljZW5zZSBmb3IgbW9yZSBkZXRhaWxzLgoKWW91IHNob3VsZCBoYXZlIHJlY2VpdmVkIGEgY29weSBvZiB0aGUgR05VIEdlbmVyYWwgUHVibGljIExpY2Vuc2UgKC4vTElDRU5TRSkuCgpGSUxFIElORk9STUFUSU9OCkRFRkdST1VQOiBtb2tvY29uc3VsdGluZy10ZWNoLlRlbXBsYXRlLUpvb21sYQpJTkdST1VQOiBNb2tvU3RhbmRhcmRzLlNlY3VyaXR5ClJFUE86IGh0dHBzOi8vZ2l0aHViLmNvbS9tb2tvY29uc3VsdGluZy10ZWNoL1RlbXBsYXRlLUpvb21sYQpWRVJTSU9OOiAwMS4wMS4wMApQQVRIOiAvU0VDVVJJVFkubWQKQlJJRUY6IFNlY3VyaXR5IHBvbGljeSwgdnVsbmVyYWJpbGl0eSByZXBvcnRpbmcgcHJvY2VkdXJlLCBhbmQgcmVzcG9uc2UgdGltZWxpbmVzCi0tPgoKWyFbTW9rb1N0YW5kYXJkc10oaHR0cHM6Ly9pbWcuc2hpZWxkcy5pby9iYWRnZS9Nb2tvU3RhbmRhcmRzLTA0LjAwLjA0LWJsdWUpXShodHRwczovL2dpdGh1Yi5jb20vbW9rb2NvbnN1bHRpbmctdGVjaC9Nb2tvU3RhbmRhcmRzKQoKIyBTZWN1cml0eSBQb2xpY3kKCiMjIFN1cHBvcnRlZCBWZXJzaW9ucwoKfCBWZXJzaW9uIHwgU3VwcG9ydGVkIHwKfCAtLS0tLS0tIHwgLS0tLS0tLS0tIHwKfCBMYXRlc3Qgc3RhYmxlIHJlbGVhc2UgfCDinIUgRnVsbCBzdXBwb3J0IHwKfCBQcmV2aW91cyBtYWpvciByZWxlYXNlIHwg4pyFIFNlY3VyaXR5IHBhdGNoZXMgb25seSB8CnwgT2xkZXIgcmVsZWFzZXMgfCDinIggTm8gc3VwcG9ydCB8CnwgYGRldmAgLyBgcmNgIGJyYW5jaGVzIHwg4p2MIE5vdCBmb3IgcHJvZHVjdGlvbiB8CgojIyBSZXBvcnRpbmcgYSBWdWxuZXJhYmlsaXR5CgoqKkRvIG5vdCBvcGVuIGEgcHVibGljIGlzc3VlIGZvciBzZWN1cml0eSB2dWxuZXJhYmlsaXRpZXMuKioKClBsZWFzZSByZXBvcnQgc2VjdXJpdHkgaXNzdWVzIHRvOgoKPiAqKnNlY3VyaXR5QG1va29jb25zdWx0aW5nLnRlY2gqKgoKSW5jbHVkZSBpbiB5b3VyIHJlcG9ydDoKCjEuICoqRGVzY3JpcHRpb24qKiDigJQgd2hhdCB0aGUgdnVsbmVyYWJpbGl0eSBpcwoyLiAqKlJlcHJvZHVjdGlvbioqIOKAlCBzdGVwLWJ5LXN0ZXAgaW5zdHJ1Y3Rpb25zIHRvIHJlcHJvZHVjZQozLiAqKkltcGFjdCoqIOKAlCB3aGF0IGFuIGF0dGFja2VyIGNvdWxkIGFjaGlldmUKNC4gKipWZXJzaW9uKiog4oCUIHdoaWNoIHZlcnNpb24ocykgYXJlIGFmZmVjdGVkCjUuICoqRW52aXJvbm1lbnQqKiDigJQgSm9vbWxhIHZlcnNpb24sIFBIUCB2ZXJzaW9uLCBob3N0aW5nIGVudmlyb25tZW50CjYuICoqUHJvb2Ygb2YgY29uY2VwdCoqIOKAlCBjb2RlIG9yIHNjcmVlbnNob3RzIGlmIGF2YWlsYWJsZQoKIyMjIFBHUCBFbmNyeXB0aW9uCgpJZiB5b3UgbmVlZCB0byBzZW5kIHNlbnNpdGl2ZSBpbmZvcm1hdGlvbiwgcmVxdWVzdCBvdXIgUEdQIGtleSBieSBlbWFpbGluZyBzZWN1cml0eUBtb2tvY29uc3VsdGluZy50ZWNoIHdpdGggdGhlIHN1YmplY3QgbGluZSAiUEdQIEtleSBSZXF1ZXN0Ii4KCiMjIFJlc3BvbnNlIFRpbWVsaW5lCgp8IFN0YWdlIHwgVGltZWxpbmUgfCBEZXNjcmlwdGlvbiB8CnwgLS0tLS0gfCAtLS0tLS0tLSB8IC0tLS0tLS0tLS0tIHwKfCAqKkFja25vd2xlZGdlbWVudCoqIHwgV2l0aGluIDQ4IGhvdXJzIHwgV2UgY29uZmlybSByZWNlaXB0IG9mIHlvdXIgcmVwb3J0IHwKfCAqKkFzc2Vzc21lbnQqKiB8IFdpdGhpbiA3IGRheXMgfCBXZSBldmFsdWF0ZSBzZXZlcml0eSBhbmQgaW1wYWN0IHwKfCAqKlJlbWVkaWF0aW9uKiogfCBXaXRoaW4gMzAgZGF5cyB8IFdlIGRldmVsb3AgYW5kIHRlc3QgYSBmaXggfAp8ICoqRGlzY2xvc3VyZSoqIHwgQWZ0ZXIgZml4IHJlbGVhc2UgfCBDb29yZGluYXRlZCBwdWJsaWMgZGlzY2xvc3VyZSB8CgojIyMgU2V2ZXJpdHkgTGV2ZWxzCgp8IExldmVsIHwgRGVzY3JpcHRpb24gfCBSZXNwb25zZSB8CnwgLS0tLS0gfCAtLS0tLS0tLS0tLSB8IC0tLS0tLS0tIHwKfCAqKkNyaXRpY2FsKiogfCBSZW1vdGUgY29kZSBleGVjdXRpb24sIGRhdGEgYnJlYWNoIHwgSW1tZWRpYXRlIHBhdGNoLCBvdXQtb2YtYmFuZCByZWxlYXNlIHwKfCAqKkhpZ2gqKiB8IFByaXZpbGVnZSBlc2NhbGF0aW9uLCBTUUwgaW5qZWN0aW9uIHwgUGF0Y2ggaW4gbmV4dCByZWxlYXNlICg8IDcgZGF5cykgfAp8ICoqTWVkaXVtKiogfCBYU1MsIENTUkYsIGluZm9ybWF0aW9uIGRpc2Nsb3N1cmUgfCBQYXRjaCBpbiBuZXh0IHNjaGVkdWxlZCByZWxlYXNlIHwKfCAqKkxvdyoqIHwgTWlub3IgaW5mb3JtYXRpb24gbGVhaywgYmVzdC1wcmFjdGljZSB2aW9sYXRpb24gfCBUcmFja2VkIGFuZCBmaXhlZCBpbiByb2FkbWFwIHwKCiMjIFNlY3VyaXR5IFByYWN0aWNlcwoKIyMjIENvZGUgU2VjdXJpdHkKLSBBbGwgY29kZSBpcyBzY2FubmVkIHdpdGggW2dpdGxlYWtzXShodHRwczovL2dpdGh1Yi5jb20vZ2l0bGVha3MvZ2l0bGVha3MpIG9uIGV2ZXJ5IHB1c2gKLSBQSFBTdGFuIHN0YXRpYyBhbmFseXNpcyBydW5zIGF0IGxldmVsIDUKLSBBbGwgU1FMIHVzZXMgcGFyYW1ldGVyaXNlZCBxdWVyaWVzIHZpYSBKb29tbGEncyBEYXRhYmFzZURyaXZlcgotIElucHV0IHZhbGlkYXRpb24gdXNlcyBKb29tbGEncyBJbnB1dEZpbHRlcgotIE91dHB1dCBlc2NhcGluZyB1c2VzIEpvb21sYSdzIGBIdG1sSGVscGVyYCBhbmQgdGVtcGxhdGUgZXNjYXBpbmcKLSBDU1JGIHByb3RlY3Rpb24gdmlhIEpvb21sYSdzIGZvcm0gdG9rZW5zCi0gQWNjZXNzIGNvbnRyb2wgdmlhIEpvb21sYSdzIEFDTCBzeXN0ZW0KCiMjIyBSZWxlYXNlIFNlY3VyaXR5Ci0gQWxsIHJlbGVhc2UgcGFja2FnZXMgYXJlIGJ1aWx0IGF1dG9tYXRpY2FsbHkgYnkgQ0kKLSBSZWxlYXNlIGFzc2V0cyBhcmUgY2hlY2tzdW1tZWQgKFNIQS0yNTYpCi0gVXBkYXRlIHNlcnZlciBVUkxzIHVzZSBIVFRQUwotIERvd25sb2FkIElEIChgZGxpZGApIGdhdGluZyBmb3IgYXV0aG9yaXNlZCBhY2Nlc3MKCiMjIyBJbmZyYXN0cnVjdHVyZQotIEdpdCByZXBvc2l0b3J5IGhvc3RlZCBvbiBzZWxmLWhvc3RlZCBHaXRlYSBpbnN0YW5jZQotIEJyYW5jaCBwcm90ZWN0aW9uIG9uIGBtYWluYCwgYGRldmAsIGByY2AsIGBiZXRhYCwgYGFscGhhYAotIEZvcmNlIHB1c2ggZGlzYWJsZWQgb24gYWxsIHByb3RlY3RlZCBicmFuY2hlcwotIFNlY3JldHMgbWFuYWdlZCB2aWEgR2l0ZWEgQWN0aW9ucyBzZWNyZXRzIChub3QgaW4gY29kZSkKCiMjIERpc2Nsb3N1cmUgUG9saWN5CgpXZSBmb2xsb3cgYSAqKmNvb3JkaW5hdGVkIGRpc2Nsb3N1cmUqKiBwb2xpY3k6CgoxLiBSZXBvcnRlciBzdWJtaXRzIHZ1bG5lcmFiaWxpdHkgdG8gc2VjdXJpdHlAbW9rb2NvbnN1bHRpbmcudGVjaAoyLiBXZSBhY2tub3dsZWRnZSBhbmQgYXNzZXNzCjMuIFdlIGRldmVsb3AgYSBmaXggYW5kIGNvb3JkaW5hdGUgd2l0aCB0aGUgcmVwb3J0ZXIKNS4gRml4IGlzIHJlbGVhc2VkCjYuIFB1YmxpYyBhZHZpc29yeSBpcyBwdWJsaXNoZWQKCiMjIyBDcmVkaXQKCldlIGNyZWRpdCBzZWN1cml0eSByZXNlYXJjaGVycyB3aG8gcmVzcG9uc2libHkgZGlzY2xvc2UgdnVsbmVyYWJpbGl0aWVzIChvcHQtb3V0IGF2YWlsYWJsZSkuCgojIyBTY29wZQoKVGhpcyBwb2xpY3kgY292ZXJzOgotIEFsbCBjb2RlIGluIHRoaXMgcmVwb3NpdG9yeQotIEFsbCBvZmZpY2lhbCByZWxlYXNlIHBhY2thZ2VzCi0gVGhlIHVwZGF0ZSBzZXJ2ZXIgZW5kcG9pbnQKClRoaXMgcG9saWN5IGRvZXMgKipub3QqKiBjb3ZlcjoKLSBUaGlyZC1wYXJ0eSBKb29tbGEgZXh0ZW5zaW9ucwotIFRoZSBKb29tbGEgZnJhbWV3b3JrIGl0c2VsZgotIEhvc3RpbmcgZW52aXJvbm1lbnRzCgojIyBDb250YWN0CgotICoqU2VjdXJpdHkgcmVwb3J0cyoqOiBzZWN1cml0eUBtb2tvY29uc3VsdGluZy50ZWNoCi0gKipHZW5lcmFsIGlucXVpcmllcyoqOiBoZWxsb0Btb2tvY29uc3VsdGluZy50ZWNoCi0gKipEZXZlbG9wZXIgY29udGFjdCoqOiBkZXZAbW9rb2NvbnN1bHRpbmcudGVjaAoKLS0tCgojIyBNZXRhZGF0YQoKfCBGaWVsZCB8IFZhbHVlIHwKfCAtLS0tLSB8IC0tLS0tIHwKfCBEb2N1bWVudCBUeXBlIHwgUG9saWN5IHwKfCBEb21haW4gfCBTZWN1cml0eSB8CnwgQXBwbGllcyBUbyB8IG1va29jb25zdWx0aW5nLXRlY2gvVGVtcGxhdGUtSm9vbWxhIHwKfCBNYWludGFpbmVyIHwgQG1va29jb25zdWx0aW5nLXRlY2ggfAp8IFN0YW5kYXJkcyB8IE1va29TdGFuZGFyZHMgdjA0LjAwLjA0IHwKfCBSZXBvIHwgaHR0cHM6Ly9naXRodWIuY29tL21va29jb25zdWx0aW5nLXRlY2gvVGVtcGxhdGUtSm9vbWxhIHwKfCBQYXRoIHwgL1NFQ1VSSVRZLm1kIHwKfCBTdGF0dXMgfCBBY3RpdmUg4oCUIGF1dG8tbWFpbnRhaW5lZCBieSBNb2tvU3RhbmRhcmRzIHwK \ No newline at end of file + + +# Security Policy + +## Purpose and Scope + +This document defines the security vulnerability reporting, response, and disclosure policy for this Joomla Plugin template repository. It establishes the authoritative process for responsible disclosure, assessment, remediation, and communication of security issues. + +## Supported Versions + +Security updates are provided for the following versions: + +| Version | Supported | +| ------- | ------------------ | +| 01.x.x | :white_check_mark: | +| < 01.0 | :x: | + +Only the current major version receives security updates. Users should upgrade to the latest supported version to receive security patches. + +## Reporting a Vulnerability + +### Where to Report + +**DO NOT** create public GitHub issues for security vulnerabilities. + +Report security vulnerabilities privately to: + +**Email**: `security@mokoconsulting.tech` + +**Subject Line**: `[SECURITY] Template-Joomla - Brief Description` + +### What to Include + +A complete vulnerability report should include: + +1. **Description**: Clear explanation of the vulnerability +2. **Impact**: Potential security impact and severity assessment +3. **Affected Versions**: Which versions are vulnerable +4. **Reproduction Steps**: Detailed steps to reproduce the issue +5. **Proof of Concept**: Code, configuration, or demonstration (if applicable) +6. **Suggested Fix**: Proposed remediation (if known) +7. **Disclosure Timeline**: Your expectations for public disclosure + +### Response Timeline + +* **Initial Response**: Within 3 business days +* **Assessment Complete**: Within 7 business days +* **Fix Timeline**: Depends on severity (see below) +* **Disclosure**: Coordinated with reporter + +## Severity Classification + +Vulnerabilities are classified using the following severity levels: + +### Critical +* Remote code execution +* Authentication bypass +* Data breach or exposure of sensitive information +* **Fix Timeline**: 7 days + +### High +* Privilege escalation +* SQL injection or command injection +* Cross-site scripting (XSS) with significant impact +* **Fix Timeline**: 14 days + +### Medium +* Information disclosure (limited scope) +* Denial of service +* Security misconfigurations with moderate impact +* **Fix Timeline**: 30 days + +### Low +* Security best practice violations +* Minor information leaks +* Issues requiring user interaction or complex preconditions +* **Fix Timeline**: 60 days or next release + +## Remediation Process + +1. **Acknowledgment**: Security team confirms receipt and begins investigation +2. **Assessment**: Vulnerability is validated, severity assigned, and impact analyzed +3. **Development**: Security patch is developed and tested +4. **Review**: Patch undergoes security review and validation +5. **Release**: Fixed version is released with security advisory +6. **Disclosure**: Public disclosure follows coordinated timeline + +## Security Advisories + +Security advisories are published via: + +* GitHub Security Advisories +* Release notes and CHANGELOG.md +* Email notification to project users (if mailing list is established) + +Advisories include: + +* CVE identifier (if applicable) +* Severity rating +* Affected versions +* Fixed versions +* Mitigation steps +* Attribution (with reporter consent) + +## Security Best Practices + +For projects using this template: + +### Required Controls + +* Enable GitHub security features (Dependabot, code scanning) +* Implement branch protection on `main` +* Require code review for all changes +* Enforce signed commits (recommended) +* Use secrets management (never commit credentials) +* Maintain security documentation +* Follow secure coding standards defined in MokoStandards + +### Joomla Plugin Security + +* Follow Joomla security best practices +* Validate and sanitize all user input +* Use Joomla's database API to prevent SQL injection +* Properly escape output to prevent XSS +* Implement proper access control checks +* Use Joomla's session and authentication APIs +* Keep Joomla and dependencies up to date + +### CI/CD Security + +* Validate all inputs +* Sanitize outputs +* Use least privilege access +* Pin dependencies with hash verification +* Scan for vulnerabilities in dependencies +* Audit third-party actions and tools + +#### Automated Security Scanning + +All repositories SHOULD implement: + +**CodeQL Analysis**: +* Enabled for PHP and other supported languages +* Runs on: push to main, pull requests, weekly schedule +* Query sets: `security-extended` and `security-and-quality` +* Configuration: `.github/workflows/codeql-analysis.yml` + +**Dependabot Security Updates**: +* Weekly scans for vulnerable dependencies +* Automated pull requests for security patches +* Configuration: `.github/dependabot.yml` + +**Secret Scanning**: +* Enabled by default with push protection +* Prevents accidental credential commits + +### Dependency Management + +* Keep dependencies up to date +* Monitor security advisories for dependencies +* Remove unused dependencies +* Audit new dependencies before adoption +* Document security-critical dependencies + +## Compliance and Governance + +This security policy is aligned with MokoStandards. Deviations require documented justification. + +Security policies are reviewed and updated at least annually or following significant security incidents. + +## Attribution and Recognition + +We acknowledge and appreciate responsible disclosure. With your permission, we will: + +* Credit you in security advisories +* List you in CHANGELOG.md for the fix release +* Recognize your contribution publicly (if desired) + +## Contact and Escalation + +* **Security Team**: security@mokoconsulting.tech +* **Primary Contact**: hello@mokoconsulting.tech +* **Escalation**: For urgent matters requiring immediate attention, contact the maintainer directly via GitHub + +## Out of Scope + +The following are explicitly out of scope: + +* Issues in third-party dependencies (report directly to maintainers) +* Social engineering attacks +* Physical security issues +* Denial of service via resource exhaustion without amplification +* Issues requiring physical access to systems +* Theoretical vulnerabilities without proof of exploitability + +--- + +## Metadata + +| Field | Value | +| ------------ | ------------------------------------------------------------------------------------------------------------ | +| Document | Security Policy | +| Path | /SECURITY.md | +| Repository | [https://github.com/mokoconsulting-tech/Template-Joomla](https://github.com/mokoconsulting-tech/Template-Joomla) | +| Owner | Moko Consulting | +| Scope | Security vulnerability handling | +| Status | Active | +| Effective | 2026-01-16 | + +## Revision History + +| Date | Change Description | Author | +| ---------- | ------------------------------------------------- | --------------- | +| 2026-01-16 | Initial creation for template repository | Moko Consulting | diff --git a/composer.json b/composer.json index 3b451c5..10afe3a 100644 --- a/composer.json +++ b/composer.json @@ -1 +1,27 @@ -ewogICAgIm5hbWUiOiAibW9rb0NvbnN1bHRpbmcvbW9rb3N1aXRldGF4aSIsCiAgICAiZGVzY3JpcHRpb24iOiAiUmlkZS1oYWlsaW5nLCBkaXNwYXRjaCwgZmxlZXQgbWFuYWdlbWVudCwgZmFyZSB6b25lcywgYW5kIGRyaXZlciBzY2hlZHVsaW5nIGZvciBKb29tbGEgNiIsCiAgICAidHlwZSI6ICJqb29tbGEtcGFja2FnZSIsCiAgICAidmVyc2lvbiI6ICIwNi4wMC4wMCIsCiAgICAibGljZW5zZSI6ICJHUEwtMy4wLW9yLWxhdGVyIiwKICAgICJhdXRob3JzIjogWwogICAgICAgIHsKICAgICAgICAgICAgIm5hbWUiOiAiTW9rbyBDb25zdWx0aW5nIiwKICAgICAgICAgICAgImVtYWlsIjogImhlbGxvQG1va29jb25zdWx0aW5nLnRlY2giLAogICAgICAgICAgICAiaG9tZXBhZ2UiOiAiaHR0cHM6Ly9tb2tvY29uc3VsdGluZy50ZWNoIgogICAgICAgIH0KICAgIF0sCiAgICAicmVxdWlyZSI6IHsKICAgICAgICAicGhwIjogIj49OC4zIgogICAgfSwKICAgICJyZXF1aXJlLWRldiI6IHsKICAgICAgICAic3F1aXpsYWJzL3BocF9jb2Rlc25pZmZlciI6ICJeMy43IiwKICAgICAgICAicGhwc3Rhbi9waHBzdGFuIjogIl4xLjEwIiwKICAgICAgICAiam9vbWxhL2NvZGluZy1zdGFuZGFyZHMiOiAiMy4wLngtZGV2IgogICAgfSwKICAgICJtaW5pbXVtLXN0YWJpbGl0eSI6ICJkZXYiLAogICAgInByZWZlci1zdGFibGUiOiB0cnVlLAogICAgImNvbmZpZyI6IHsKICAgICAgICAic29ydC1wYWNrYWdlcyI6IHRydWUKICAgIH0KfQ== \ No newline at end of file +{ + "name": "mokoconsulting/mokojoomgallery", + "description": "Photo gallery management for Joomla — galleries, images, thumbnails, lightbox, and frontend display", + "type": "joomla-package", + "version": "01.00.00", + "license": "GPL-3.0-or-later", + "authors": [ + { + "name": "Moko Consulting", + "email": "hello@mokoconsulting.tech", + "homepage": "https://mokoconsulting.tech" + } + ], + "require": { + "php": ">=8.1" + }, + "require-dev": { + "squizlabs/php_codesniffer": "^3.7", + "phpstan/phpstan": "^1.10", + "joomla/coding-standards": "3.0.x-dev" + }, + "minimum-stability": "dev", + "prefer-stable": true, + "config": { + "sort-packages": true + } +} diff --git a/phpstan.neon b/phpstan.neon index 9c9a802..3d4adca 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1 +1,32 @@ -IyBDb3B5cmlnaHQgKEMpIDIwMjYgTW9rbyBDb25zdWx0aW5nIDxoZWxsb0Btb2tvY29uc3VsdGluZy50ZWNoPgojIFNQRFgtTGljZW5zZS1JZGVudGlmaWVyOiBHUEwtMy4wLW9yLWxhdGVyCiMKIyBQSFBTdGFuIGNvbmZpZ3VyYXRpb24gZm9yIEpvb21sYSBleHRlbnNpb24gcmVwb3NpdG9yaWVzLgojIEV4dGVuZHMgdGhlIGJhc2UgTW9rb1N0YW5kYXJkcyBjb25maWcgYW5kIGFkZHMgSm9vbWxhIGZyYW1ld29yayBjbGFzcyBzdHVicwojIHNvIFBIUFN0YW4gY2FuIHJlc29sdmUgRmFjdG9yeSwgQ01TQXBwbGljYXRpb24sIFVzZXIsIFRhYmxlLCBldGMuCiMgd2l0aG91dCByZXF1aXJpbmcgYSBmdWxsIEpvb21sYSBpbnN0YWxsYXRpb24uCgpwYXJhbWV0ZXJzOgogICAgbGV2ZWw6IDUKCiAgICBwYXRoczoKICAgICAgICAtIHNyYwoKICAgIGV4Y2x1ZGVQYXRoczoKICAgICAgICAtIHZlbmRvcgogICAgICAgIC0gbm9kZV9tb2R1bGVzCgogICAgIyBKb29tbGEgZnJhbWV3b3JrIHN0dWJzIOKAlCByZXNvbHZlZCB2aWEgdGhlIGVudGVycHJpc2UgcGFja2FnZSBmcm9tIHZlbmRvci8KICAgIHN0dWJGaWxlczoKICAgICAgICAtIHZlbmRvci9tb2tvY29uc3VsdGluZy10ZWNoL2VudGVycHJpc2UvdGVtcGxhdGVzL3N0dWJzL2pvb21sYS5waHAKCiAgICAjIFN1cHByZXNzIGVycm9ycyB0aGF0IGFyZSBzdHJ1Y3R1cmFsIGluIEpvb21sYSdzIHNlcnZpY2UtY29udGFpbmVyIGFyY2hpdGVjdHVyZQogICAgaWdub3JlRXJyb3JzOgogICAgICAgICMgSm9vbWxhJ3Mgc2VydmljZS1iYXNlZCBkZXBlbmRlbmN5IGluamVjdGlvbiByZXR1cm5zIG1peGVkIGZyb20gZ2V0QXBwbGljYXRpb24oKQogICAgICAgIC0gJyNDYW5ub3QgY2FsbCBtZXRob2QgLisgb24gSm9vbWxhXFxDTVNcXEFwcGxpY2F0aW9uXFxDTVNBcHBsaWNhdGlvblx8bnVsbCMnCiAgICAgICAgIyBGYWN0b3J5OjpnZXRYKCkgcGF0dGVybnMgYXJlIHNhZmUgYXQgcnVudGltZSBldmVuIHdoZW4gbnVsbGFibGUgaW4gc3R1YnMKICAgICAgICAtICcjQ2FsbCB0byBzdGF0aWMgbWV0aG9kIFthLXpBLVpdK1woXCkgb24gYW4gaW50ZXJmYWNlIycKCiAgICByZXBvcnRVbm1hdGNoZWRJZ25vcmVkRXJyb3JzOiBmYWxzZQogICAgY2hlY2tNaXNzaW5nSXRlcmFibGVWYWx1ZVR5cGU6IGZhbHNlCiAgICBjaGVja0dlbmVyaWNDbGFzc0luTm9uR2VuZXJpY09iamVjdFR5cGU6IGZhbHNlCg== \ No newline at end of file +# Copyright (C) 2026 Moko Consulting +# SPDX-License-Identifier: GPL-3.0-or-later +# +# PHPStan configuration for Joomla extension repositories. +# Extends the base MokoStandards config and adds Joomla framework class stubs +# so PHPStan can resolve Factory, CMSApplication, User, Table, etc. +# without requiring a full Joomla installation. + +parameters: + level: 5 + + paths: + - src + + excludePaths: + - vendor + - node_modules + + # Joomla framework stubs — resolved via the enterprise package from vendor/ + stubFiles: + - vendor/mokoconsulting-tech/enterprise/templates/stubs/joomla.php + + # Suppress errors that are structural in Joomla's service-container architecture + ignoreErrors: + # Joomla's service-based dependency injection returns mixed from getApplication() + - '#Cannot call method .+ on Joomla\\CMS\\Application\\CMSApplication\|null#' + # Factory::getX() patterns are safe at runtime even when nullable in stubs + - '#Call to static method [a-zA-Z]+\(\) on an interface#' + + reportUnmatchedIgnoredErrors: false + checkMissingIterableValueType: false + checkGenericClassInNonGenericObjectType: false diff --git a/source/packages/com_mokosuitetaxi/admin/access.xml b/source/packages/com_mokosuitetaxi/admin/access.xml deleted file mode 100644 index 5ae7eda..0000000 --- a/source/packages/com_mokosuitetaxi/admin/access.xml +++ /dev/null @@ -1,26 +0,0 @@ - - -
- - - - - - - - - - - - - - - - - - - - - -
-
diff --git a/source/packages/com_mokosuitetaxi/admin/config.xml b/source/packages/com_mokosuitetaxi/admin/config.xml deleted file mode 100644 index 860a4d8..0000000 --- a/source/packages/com_mokosuitetaxi/admin/config.xml +++ /dev/null @@ -1,51 +0,0 @@ - - -
- - - - - - -
-
- - - - - - - - -
-
- - - - - - - - - -
-
- - - - - - -
-
- - - - - - - - - -
-
diff --git a/source/packages/com_mokosuitetaxi/admin/services/provider.php b/source/packages/com_mokosuitetaxi/admin/services/provider.php deleted file mode 100644 index 6c33848..0000000 --- a/source/packages/com_mokosuitetaxi/admin/services/provider.php +++ /dev/null @@ -1,26 +0,0 @@ - - * SPDX-License-Identifier: GPL-3.0-or-later - * Authored-by: Moko Consulting - */ - -defined('_JEXEC') or die; - -use Joomla\CMS\Extension\ComponentInterface; -use Joomla\CMS\Extension\MVCComponent; -use Joomla\CMS\Dispatcher\ComponentDispatcherFactoryInterface; -use Joomla\CMS\MVC\Factory\MVCFactoryInterface; -use Joomla\DI\Container; -use Joomla\DI\ServiceProviderInterface; - -return new class implements ServiceProviderInterface { - public function register(Container $container): void { - $container->set(ComponentInterface::class, function (Container $container) { - $c = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $c->setMVCFactory($container->get(MVCFactoryInterface::class)); - return $c; - }); - } -}; diff --git a/source/packages/com_mokosuitetaxi/admin/src/Controller/DisplayController.php b/source/packages/com_mokosuitetaxi/admin/src/Controller/DisplayController.php deleted file mode 100644 index dc995f1..0000000 --- a/source/packages/com_mokosuitetaxi/admin/src/Controller/DisplayController.php +++ /dev/null @@ -1,18 +0,0 @@ - - * SPDX-License-Identifier: GPL-3.0-or-later - * Authored-by: Moko Consulting - */ - -namespace Moko\Component\MokoSuiteTaxi\Administrator\Controller; - -defined('_JEXEC') or die; - -use Joomla\CMS\MVC\Controller\BaseController; - -class DisplayController extends BaseController -{ - protected $default_view = 'taxidashboard'; -} diff --git a/source/packages/com_mokosuitetaxi/admin/src/Model/TaxiDashboardModel.php b/source/packages/com_mokosuitetaxi/admin/src/Model/TaxiDashboardModel.php deleted file mode 100644 index 04e69b5..0000000 --- a/source/packages/com_mokosuitetaxi/admin/src/Model/TaxiDashboardModel.php +++ /dev/null @@ -1,17 +0,0 @@ - - * SPDX-License-Identifier: GPL-3.0-or-later - * Authored-by: Moko Consulting - */ - -namespace Moko\Component\MokoSuiteTaxi\Administrator\Model; - -defined('_JEXEC') or die; - -use Joomla\CMS\MVC\Model\BaseDatabaseModel; - -class TaxiDashboardModel extends BaseDatabaseModel -{ -} diff --git a/source/packages/com_mokosuitetaxi/admin/src/View/TaxiDashboard/HtmlView.php b/source/packages/com_mokosuitetaxi/admin/src/View/TaxiDashboard/HtmlView.php deleted file mode 100644 index 4c7c3c9..0000000 --- a/source/packages/com_mokosuitetaxi/admin/src/View/TaxiDashboard/HtmlView.php +++ /dev/null @@ -1,23 +0,0 @@ - - * SPDX-License-Identifier: GPL-3.0-or-later - * Authored-by: Moko Consulting - */ - -namespace Moko\Component\MokoSuiteTaxi\Administrator\View\TaxiDashboard; - -defined('_JEXEC') or die; - -use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; -use Joomla\CMS\Toolbar\ToolbarHelper; - -class HtmlView extends BaseHtmlView -{ - public function display($tpl = null): void - { - ToolbarHelper::title('MokoSuite Taxi - Dashboard'); - parent::display($tpl); - } -} diff --git a/source/packages/com_mokosuitetaxi/admin/src/View/TaxiDrivers/HtmlView.php b/source/packages/com_mokosuitetaxi/admin/src/View/TaxiDrivers/HtmlView.php deleted file mode 100644 index a0f6d70..0000000 --- a/source/packages/com_mokosuitetaxi/admin/src/View/TaxiDrivers/HtmlView.php +++ /dev/null @@ -1,23 +0,0 @@ - - * SPDX-License-Identifier: GPL-3.0-or-later - * Authored-by: Moko Consulting - */ - -namespace Moko\Component\MokoSuiteTaxi\Administrator\View\TaxiDrivers; - -defined('_JEXEC') or die; - -use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; -use Joomla\CMS\Toolbar\ToolbarHelper; - -class HtmlView extends BaseHtmlView -{ - public function display($tpl = null): void - { - ToolbarHelper::title('MokoSuite Taxi - Drivers'); - parent::display($tpl); - } -} diff --git a/source/packages/com_mokosuitetaxi/admin/src/View/TaxiFares/HtmlView.php b/source/packages/com_mokosuitetaxi/admin/src/View/TaxiFares/HtmlView.php deleted file mode 100644 index 0914bda..0000000 --- a/source/packages/com_mokosuitetaxi/admin/src/View/TaxiFares/HtmlView.php +++ /dev/null @@ -1,23 +0,0 @@ - - * SPDX-License-Identifier: GPL-3.0-or-later - * Authored-by: Moko Consulting - */ - -namespace Moko\Component\MokoSuiteTaxi\Administrator\View\TaxiFares; - -defined('_JEXEC') or die; - -use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; -use Joomla\CMS\Toolbar\ToolbarHelper; - -class HtmlView extends BaseHtmlView -{ - public function display($tpl = null): void - { - ToolbarHelper::title('MokoSuite Taxi - Fare Rules'); - parent::display($tpl); - } -} diff --git a/source/packages/com_mokosuitetaxi/admin/src/View/TaxiRides/HtmlView.php b/source/packages/com_mokosuitetaxi/admin/src/View/TaxiRides/HtmlView.php deleted file mode 100644 index 127b07c..0000000 --- a/source/packages/com_mokosuitetaxi/admin/src/View/TaxiRides/HtmlView.php +++ /dev/null @@ -1,23 +0,0 @@ - - * SPDX-License-Identifier: GPL-3.0-or-later - * Authored-by: Moko Consulting - */ - -namespace Moko\Component\MokoSuiteTaxi\Administrator\View\TaxiRides; - -defined('_JEXEC') or die; - -use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; -use Joomla\CMS\Toolbar\ToolbarHelper; - -class HtmlView extends BaseHtmlView -{ - public function display($tpl = null): void - { - ToolbarHelper::title('MokoSuite Taxi - Rides'); - parent::display($tpl); - } -} diff --git a/source/packages/com_mokosuitetaxi/admin/src/View/TaxiVehicles/HtmlView.php b/source/packages/com_mokosuitetaxi/admin/src/View/TaxiVehicles/HtmlView.php deleted file mode 100644 index 054e4b1..0000000 --- a/source/packages/com_mokosuitetaxi/admin/src/View/TaxiVehicles/HtmlView.php +++ /dev/null @@ -1,23 +0,0 @@ - - * SPDX-License-Identifier: GPL-3.0-or-later - * Authored-by: Moko Consulting - */ - -namespace Moko\Component\MokoSuiteTaxi\Administrator\View\TaxiVehicles; - -defined('_JEXEC') or die; - -use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; -use Joomla\CMS\Toolbar\ToolbarHelper; - -class HtmlView extends BaseHtmlView -{ - public function display($tpl = null): void - { - ToolbarHelper::title('MokoSuite Taxi - Vehicles'); - parent::display($tpl); - } -} diff --git a/source/packages/com_mokosuitetaxi/admin/src/View/TaxiZones/HtmlView.php b/source/packages/com_mokosuitetaxi/admin/src/View/TaxiZones/HtmlView.php deleted file mode 100644 index 3ad6d82..0000000 --- a/source/packages/com_mokosuitetaxi/admin/src/View/TaxiZones/HtmlView.php +++ /dev/null @@ -1,23 +0,0 @@ - - * SPDX-License-Identifier: GPL-3.0-or-later - * Authored-by: Moko Consulting - */ - -namespace Moko\Component\MokoSuiteTaxi\Administrator\View\TaxiZones; - -defined('_JEXEC') or die; - -use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; -use Joomla\CMS\Toolbar\ToolbarHelper; - -class HtmlView extends BaseHtmlView -{ - public function display($tpl = null): void - { - ToolbarHelper::title('MokoSuite Taxi - Zones'); - parent::display($tpl); - } -} diff --git a/source/packages/com_mokosuitetaxi/admin/tmpl/taxidashboard/default.php b/source/packages/com_mokosuitetaxi/admin/tmpl/taxidashboard/default.php deleted file mode 100644 index e80e602..0000000 --- a/source/packages/com_mokosuitetaxi/admin/tmpl/taxidashboard/default.php +++ /dev/null @@ -1 +0,0 @@ -

Dashboard

Coming soon.

diff --git a/source/packages/com_mokosuitetaxi/admin/tmpl/taxidrivers/default.php b/source/packages/com_mokosuitetaxi/admin/tmpl/taxidrivers/default.php deleted file mode 100644 index 9668d20..0000000 --- a/source/packages/com_mokosuitetaxi/admin/tmpl/taxidrivers/default.php +++ /dev/null @@ -1 +0,0 @@ -

Drivers

Coming soon.

diff --git a/source/packages/com_mokosuitetaxi/admin/tmpl/taxifares/default.php b/source/packages/com_mokosuitetaxi/admin/tmpl/taxifares/default.php deleted file mode 100644 index c6883f7..0000000 --- a/source/packages/com_mokosuitetaxi/admin/tmpl/taxifares/default.php +++ /dev/null @@ -1 +0,0 @@ -

Fares

Coming soon.

diff --git a/source/packages/com_mokosuitetaxi/admin/tmpl/taxirides/default.php b/source/packages/com_mokosuitetaxi/admin/tmpl/taxirides/default.php deleted file mode 100644 index 018e753..0000000 --- a/source/packages/com_mokosuitetaxi/admin/tmpl/taxirides/default.php +++ /dev/null @@ -1 +0,0 @@ -

Rides

Coming soon.

diff --git a/source/packages/com_mokosuitetaxi/admin/tmpl/taxivehicles/default.php b/source/packages/com_mokosuitetaxi/admin/tmpl/taxivehicles/default.php deleted file mode 100644 index 70f3d47..0000000 --- a/source/packages/com_mokosuitetaxi/admin/tmpl/taxivehicles/default.php +++ /dev/null @@ -1 +0,0 @@ -

Vehicles

Coming soon.

diff --git a/source/packages/com_mokosuitetaxi/admin/tmpl/taxizones/default.php b/source/packages/com_mokosuitetaxi/admin/tmpl/taxizones/default.php deleted file mode 100644 index 597a753..0000000 --- a/source/packages/com_mokosuitetaxi/admin/tmpl/taxizones/default.php +++ /dev/null @@ -1 +0,0 @@ -

Zones

Coming soon.

diff --git a/source/packages/com_mokosuitetaxi/mokosuitetaxi.xml b/source/packages/com_mokosuitetaxi/mokosuitetaxi.xml deleted file mode 100644 index bd0bfd6..0000000 --- a/source/packages/com_mokosuitetaxi/mokosuitetaxi.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - MokoSuite Taxi - Moko Consulting - 2026-06-27 - Copyright (C) 2026 Moko Consulting. - GPL-3.0-or-later - 06.00.00 - Moko\Component\MokoSuiteTaxi - - servicessrctmpl - COM_MOKOSUITETAXI - - diff --git a/source/packages/plg_system_mokosuitetaxi/language/en-GB/plg_system_mokosuitetaxi.ini b/source/packages/plg_system_mokosuitetaxi/language/en-GB/plg_system_mokosuitetaxi.ini deleted file mode 100644 index 6c013ee..0000000 --- a/source/packages/plg_system_mokosuitetaxi/language/en-GB/plg_system_mokosuitetaxi.ini +++ /dev/null @@ -1,2 +0,0 @@ -PLG_SYSTEM_MOKOSUITETAXI="System - MokoSuite Taxi" -PLG_SYSTEM_MOKOSUITETAXI_DESC="Ride-hailing, dispatch, fleet management, fare zones, and driver scheduling" diff --git a/source/packages/plg_system_mokosuitetaxi/language/en-GB/plg_system_mokosuitetaxi.sys.ini b/source/packages/plg_system_mokosuitetaxi/language/en-GB/plg_system_mokosuitetaxi.sys.ini deleted file mode 100644 index 6c013ee..0000000 --- a/source/packages/plg_system_mokosuitetaxi/language/en-GB/plg_system_mokosuitetaxi.sys.ini +++ /dev/null @@ -1,2 +0,0 @@ -PLG_SYSTEM_MOKOSUITETAXI="System - MokoSuite Taxi" -PLG_SYSTEM_MOKOSUITETAXI_DESC="Ride-hailing, dispatch, fleet management, fare zones, and driver scheduling" diff --git a/source/packages/plg_system_mokosuitetaxi/mokosuitetaxi.xml b/source/packages/plg_system_mokosuitetaxi/mokosuitetaxi.xml deleted file mode 100644 index bc6ab3f..0000000 --- a/source/packages/plg_system_mokosuitetaxi/mokosuitetaxi.xml +++ /dev/null @@ -1,67 +0,0 @@ - - - System - MokoSuite Taxi - mokosuitetaxi - Moko Consulting - 2026-06-27 - Copyright (C) 2026 Moko Consulting. All rights reserved. - GPL-3.0-or-later - hello@mokoconsulting.tech - https://mokoconsulting.tech - 06.00.00 - 8.3 - PLG_SYSTEM_MOKOSUITETAXI_DESC - Moko\Plugin\System\MokoSuiteTaxi - - src - services - language - sql - - - en-GB/plg_system_mokosuitetaxi.ini - en-GB/plg_system_mokosuitetaxi.sys.ini - - sql/install.mysql.sql - sql/uninstall.mysql.sql - - -
- - - - - - - - - - -
-
- - - - - - - - - -
-
- - - - -
-
- - - - - -
-
-
-
diff --git a/source/packages/plg_system_mokosuitetaxi/services/provider.php b/source/packages/plg_system_mokosuitetaxi/services/provider.php deleted file mode 100644 index 130e71f..0000000 --- a/source/packages/plg_system_mokosuitetaxi/services/provider.php +++ /dev/null @@ -1,33 +0,0 @@ - - * SPDX-License-Identifier: GPL-3.0-or-later - * Authored-by: Moko Consulting - */ - -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 Moko\Plugin\System\MokoSuiteTaxi\Extension\Taxi; - -return new class implements ServiceProviderInterface -{ - public function register(Container $container): void - { - $container->set( - PluginInterface::class, - function (Container $container) { - $dispatcher = $container->get(DispatcherInterface::class); - $plugin = new Taxi($dispatcher, (array) PluginHelper::getPlugin('system', 'mokosuitetaxi')); - $plugin->setApplication(Factory::getApplication()); - return $plugin; - } - ); - } -}; diff --git a/source/packages/plg_system_mokosuitetaxi/sql/install.mysql.sql b/source/packages/plg_system_mokosuitetaxi/sql/install.mysql.sql deleted file mode 100644 index d3c40b2..0000000 --- a/source/packages/plg_system_mokosuitetaxi/sql/install.mysql.sql +++ /dev/null @@ -1,216 +0,0 @@ --- --- MokoSuite Taxi Tables --- - -CREATE TABLE IF NOT EXISTS `#__mokosuitetaxi_vehicles` ( - `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, - `plate_number` VARCHAR(20) NOT NULL, - `vin` VARCHAR(17) NOT NULL DEFAULT '', - `make` VARCHAR(100) NOT NULL, - `model` VARCHAR(100) NOT NULL, - `year` SMALLINT UNSIGNED NOT NULL, - `color` VARCHAR(50) NOT NULL DEFAULT '', - `vehicle_type` ENUM('sedan','suv','van','luxury','minibus','motorcycle') NOT NULL DEFAULT 'sedan', - `capacity` TINYINT UNSIGNED NOT NULL DEFAULT 4, - `fuel_type` ENUM('gasoline','diesel','electric','hybrid') NOT NULL DEFAULT 'gasoline', - `status` ENUM('available','in_service','maintenance','retired') NOT NULL DEFAULT 'available', - `insurance_expiry` DATE DEFAULT NULL, - `inspection_expiry` DATE DEFAULT NULL, - `odometer_km` INT UNSIGNED NOT NULL DEFAULT 0, - `photo` VARCHAR(500) NOT NULL DEFAULT '', - `notes` TEXT, - `published` TINYINT NOT NULL DEFAULT 1, - `created` DATETIME NOT NULL, - `modified` DATETIME DEFAULT NULL, - `created_by` INT NOT NULL DEFAULT 0, - PRIMARY KEY (`id`), - UNIQUE KEY `idx_plate` (`plate_number`), - KEY `idx_status` (`status`), - KEY `idx_type` (`vehicle_type`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - -CREATE TABLE IF NOT EXISTS `#__mokosuitetaxi_drivers` ( - `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, - `contact_id` INT DEFAULT NULL COMMENT 'FK to #__contact_details', - `name` VARCHAR(255) NOT NULL, - `phone` VARCHAR(50) NOT NULL DEFAULT '', - `email` VARCHAR(255) NOT NULL DEFAULT '', - `license_number` VARCHAR(50) NOT NULL, - `license_expiry` DATE DEFAULT NULL, - `license_class` VARCHAR(10) NOT NULL DEFAULT '', - `vehicle_id` INT UNSIGNED DEFAULT NULL, - `status` ENUM('active','inactive','suspended','pending_approval') NOT NULL DEFAULT 'pending_approval', - `rating` DECIMAL(3,2) NOT NULL DEFAULT 5.00, - `total_rides` INT UNSIGNED NOT NULL DEFAULT 0, - `total_earnings` DECIMAL(12,2) NOT NULL DEFAULT 0.00, - `commission_rate` DECIMAL(5,2) DEFAULT NULL COMMENT 'Override platform default', - `photo` VARCHAR(500) NOT NULL DEFAULT '', - `background_check_date` DATE DEFAULT NULL, - `published` TINYINT NOT NULL DEFAULT 1, - `created` DATETIME NOT NULL, - `modified` DATETIME DEFAULT NULL, - `created_by` INT NOT NULL DEFAULT 0, - PRIMARY KEY (`id`), - UNIQUE KEY `idx_license` (`license_number`), - KEY `idx_contact` (`contact_id`), - KEY `idx_vehicle` (`vehicle_id`), - KEY `idx_status` (`status`), - KEY `idx_rating` (`rating`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - -CREATE TABLE IF NOT EXISTS `#__mokosuitetaxi_zones` ( - `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, - `name` VARCHAR(255) NOT NULL, - `description` TEXT, - `zone_type` ENUM('pickup','dropoff','service','surge','restricted') NOT NULL DEFAULT 'service', - `boundary_geojson` JSON COMMENT 'GeoJSON polygon defining zone boundary', - `center_lat` DECIMAL(10,7) DEFAULT NULL, - `center_lng` DECIMAL(10,7) DEFAULT NULL, - `radius_km` DECIMAL(8,2) DEFAULT NULL COMMENT 'Circular zone fallback if no polygon', - `fare_multiplier` DECIMAL(4,2) NOT NULL DEFAULT 1.00, - `active_hours_start` TIME DEFAULT NULL, - `active_hours_end` TIME DEFAULT NULL, - `published` TINYINT NOT NULL DEFAULT 1, - `ordering` INT NOT NULL DEFAULT 0, - `created` DATETIME NOT NULL, - PRIMARY KEY (`id`), - KEY `idx_type` (`zone_type`), - KEY `idx_center` (`center_lat`, `center_lng`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - -CREATE TABLE IF NOT EXISTS `#__mokosuitetaxi_fares` ( - `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, - `title` VARCHAR(255) NOT NULL, - `vehicle_type` ENUM('sedan','suv','van','luxury','minibus','motorcycle','all') NOT NULL DEFAULT 'all', - `zone_id` INT UNSIGNED DEFAULT NULL COMMENT 'Zone-specific pricing, NULL = default', - `base_fare` DECIMAL(10,2) NOT NULL DEFAULT 2.50, - `per_km` DECIMAL(10,2) NOT NULL DEFAULT 1.50, - `per_minute` DECIMAL(10,2) NOT NULL DEFAULT 0.25, - `minimum_fare` DECIMAL(10,2) NOT NULL DEFAULT 5.00, - `booking_fee` DECIMAL(10,2) NOT NULL DEFAULT 0.00, - `peak_multiplier` DECIMAL(4,2) NOT NULL DEFAULT 1.00, - `peak_hours_start` TIME DEFAULT NULL, - `peak_hours_end` TIME DEFAULT NULL, - `night_surcharge` DECIMAL(10,2) NOT NULL DEFAULT 0.00, - `airport_surcharge` DECIMAL(10,2) NOT NULL DEFAULT 0.00, - `effective_from` DATE DEFAULT NULL, - `effective_to` DATE DEFAULT NULL, - `published` TINYINT NOT NULL DEFAULT 1, - `created` DATETIME NOT NULL, - PRIMARY KEY (`id`), - KEY `idx_vehicle_type` (`vehicle_type`), - KEY `idx_zone` (`zone_id`), - KEY `idx_effective` (`effective_from`, `effective_to`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - -CREATE TABLE IF NOT EXISTS `#__mokosuitetaxi_rides` ( - `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, - `ride_ref` VARCHAR(20) NOT NULL, - `rider_contact_id` INT DEFAULT NULL COMMENT 'FK to #__contact_details', - `rider_name` VARCHAR(255) NOT NULL, - `rider_phone` VARCHAR(50) NOT NULL DEFAULT '', - `driver_id` INT UNSIGNED DEFAULT NULL, - `vehicle_id` INT UNSIGNED DEFAULT NULL, - `status` ENUM('requested','dispatched','accepted','arriving','in_progress','completed','cancelled','no_driver') NOT NULL DEFAULT 'requested', - `ride_type` ENUM('on_demand','scheduled','airport','corporate') NOT NULL DEFAULT 'on_demand', - `vehicle_type_requested` ENUM('sedan','suv','van','luxury','minibus','motorcycle','any') NOT NULL DEFAULT 'any', - `pickup_address` VARCHAR(500) NOT NULL, - `pickup_lat` DECIMAL(10,7) DEFAULT NULL, - `pickup_lng` DECIMAL(10,7) DEFAULT NULL, - `dropoff_address` VARCHAR(500) NOT NULL DEFAULT '', - `dropoff_lat` DECIMAL(10,7) DEFAULT NULL, - `dropoff_lng` DECIMAL(10,7) DEFAULT NULL, - `pickup_zone_id` INT UNSIGNED DEFAULT NULL, - `dropoff_zone_id` INT UNSIGNED DEFAULT NULL, - `scheduled_at` DATETIME DEFAULT NULL, - `dispatched_at` DATETIME DEFAULT NULL, - `accepted_at` DATETIME DEFAULT NULL, - `arrived_at` DATETIME DEFAULT NULL, - `started_at` DATETIME DEFAULT NULL, - `completed_at` DATETIME DEFAULT NULL, - `cancelled_at` DATETIME DEFAULT NULL, - `cancelled_by` ENUM('rider','driver','system') DEFAULT NULL, - `cancel_reason` VARCHAR(500) NOT NULL DEFAULT '', - `distance_km` DECIMAL(10,2) DEFAULT NULL, - `duration_minutes` DECIMAL(10,2) DEFAULT NULL, - `fare_id` INT UNSIGNED DEFAULT NULL, - `base_fare` DECIMAL(10,2) NOT NULL DEFAULT 0.00, - `distance_charge` DECIMAL(10,2) NOT NULL DEFAULT 0.00, - `time_charge` DECIMAL(10,2) NOT NULL DEFAULT 0.00, - `surge_multiplier` DECIMAL(4,2) NOT NULL DEFAULT 1.00, - `surcharges` DECIMAL(10,2) NOT NULL DEFAULT 0.00, - `discount` DECIMAL(10,2) NOT NULL DEFAULT 0.00, - `total_fare` DECIMAL(10,2) NOT NULL DEFAULT 0.00, - `driver_payout` DECIMAL(10,2) NOT NULL DEFAULT 0.00, - `platform_fee` DECIMAL(10,2) NOT NULL DEFAULT 0.00, - `payment_method` ENUM('cash','card','wallet','corporate') NOT NULL DEFAULT 'cash', - `payment_status` ENUM('pending','paid','refunded') NOT NULL DEFAULT 'pending', - `passenger_count` TINYINT UNSIGNED NOT NULL DEFAULT 1, - `notes` TEXT, - `route_polyline` TEXT COMMENT 'Encoded polyline of actual route', - `created` DATETIME NOT NULL, - `created_by` INT NOT NULL DEFAULT 0, - PRIMARY KEY (`id`), - UNIQUE KEY `idx_ref` (`ride_ref`), - KEY `idx_rider` (`rider_contact_id`), - KEY `idx_driver` (`driver_id`), - KEY `idx_vehicle` (`vehicle_id`), - KEY `idx_status` (`status`), - KEY `idx_type` (`ride_type`), - KEY `idx_created` (`created`), - KEY `idx_scheduled` (`scheduled_at`), - KEY `idx_pickup_zone` (`pickup_zone_id`), - KEY `idx_dropoff_zone` (`dropoff_zone_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - -CREATE TABLE IF NOT EXISTS `#__mokosuitetaxi_ratings` ( - `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, - `ride_id` INT UNSIGNED NOT NULL, - `rated_by` ENUM('rider','driver') NOT NULL, - `rating` TINYINT UNSIGNED NOT NULL COMMENT '1-5 stars', - `comment` TEXT, - `tags` VARCHAR(500) NOT NULL DEFAULT '' COMMENT 'Comma-separated feedback tags', - `created` DATETIME NOT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `idx_ride_rater` (`ride_id`, `rated_by`), - KEY `idx_rating` (`rating`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - -CREATE TABLE IF NOT EXISTS `#__mokosuitetaxi_dispatch` ( - `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, - `ride_id` INT UNSIGNED NOT NULL, - `driver_id` INT UNSIGNED NOT NULL, - `status` ENUM('offered','accepted','rejected','expired') NOT NULL DEFAULT 'offered', - `offered_at` DATETIME NOT NULL, - `responded_at` DATETIME DEFAULT NULL, - `distance_to_pickup_km` DECIMAL(10,2) DEFAULT NULL, - `eta_minutes` DECIMAL(10,2) DEFAULT NULL, - `attempt_number` TINYINT UNSIGNED NOT NULL DEFAULT 1, - PRIMARY KEY (`id`), - KEY `idx_ride` (`ride_id`), - KEY `idx_driver` (`driver_id`), - KEY `idx_status` (`status`), - KEY `idx_offered` (`offered_at`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - -CREATE TABLE IF NOT EXISTS `#__mokosuitetaxi_shifts` ( - `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, - `driver_id` INT UNSIGNED NOT NULL, - `vehicle_id` INT UNSIGNED DEFAULT NULL, - `start_time` DATETIME NOT NULL, - `end_time` DATETIME DEFAULT NULL, - `status` ENUM('scheduled','active','completed','cancelled') NOT NULL DEFAULT 'scheduled', - `start_lat` DECIMAL(10,7) DEFAULT NULL, - `start_lng` DECIMAL(10,7) DEFAULT NULL, - `end_lat` DECIMAL(10,7) DEFAULT NULL, - `end_lng` DECIMAL(10,7) DEFAULT NULL, - `total_rides` INT UNSIGNED NOT NULL DEFAULT 0, - `total_distance_km` DECIMAL(10,2) NOT NULL DEFAULT 0.00, - `total_earnings` DECIMAL(12,2) NOT NULL DEFAULT 0.00, - `created` DATETIME NOT NULL, - PRIMARY KEY (`id`), - KEY `idx_driver` (`driver_id`), - KEY `idx_vehicle` (`vehicle_id`), - KEY `idx_times` (`start_time`, `end_time`), - KEY `idx_status` (`status`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/source/packages/plg_system_mokosuitetaxi/sql/uninstall.mysql.sql b/source/packages/plg_system_mokosuitetaxi/sql/uninstall.mysql.sql deleted file mode 100644 index 6e0b8c6..0000000 --- a/source/packages/plg_system_mokosuitetaxi/sql/uninstall.mysql.sql +++ /dev/null @@ -1,12 +0,0 @@ --- --- MokoSuite Taxi — Uninstall --- - -DROP TABLE IF EXISTS `#__mokosuitetaxi_shifts`; -DROP TABLE IF EXISTS `#__mokosuitetaxi_dispatch`; -DROP TABLE IF EXISTS `#__mokosuitetaxi_ratings`; -DROP TABLE IF EXISTS `#__mokosuitetaxi_rides`; -DROP TABLE IF EXISTS `#__mokosuitetaxi_fares`; -DROP TABLE IF EXISTS `#__mokosuitetaxi_zones`; -DROP TABLE IF EXISTS `#__mokosuitetaxi_drivers`; -DROP TABLE IF EXISTS `#__mokosuitetaxi_vehicles`; diff --git a/source/packages/plg_system_mokosuitetaxi/src/Extension/Taxi.php b/source/packages/plg_system_mokosuitetaxi/src/Extension/Taxi.php deleted file mode 100644 index 55bf0e8..0000000 --- a/source/packages/plg_system_mokosuitetaxi/src/Extension/Taxi.php +++ /dev/null @@ -1,24 +0,0 @@ - - * SPDX-License-Identifier: GPL-3.0-or-later - * Authored-by: Moko Consulting - */ - -namespace Moko\Plugin\System\MokoSuiteTaxi\Extension; - -defined('_JEXEC') or die; - -use Joomla\CMS\Plugin\CMSPlugin; -use Joomla\Event\SubscriberInterface; - -class Taxi extends CMSPlugin implements SubscriberInterface -{ - protected $autoloadLanguage = true; - - public static function getSubscribedEvents(): array - { - return []; - } -} diff --git a/source/packages/plg_system_mokosuitetaxi/src/Helper/DispatchHelper.php b/source/packages/plg_system_mokosuitetaxi/src/Helper/DispatchHelper.php deleted file mode 100644 index 0312d68..0000000 --- a/source/packages/plg_system_mokosuitetaxi/src/Helper/DispatchHelper.php +++ /dev/null @@ -1,177 +0,0 @@ - - * SPDX-License-Identifier: GPL-3.0-or-later - * Authored-by: Moko Consulting - */ - -declare(strict_types=1); - -namespace Moko\Plugin\System\MokoSuiteTaxi\Helper; - -defined('_JEXEC') or die; - -use Joomla\CMS\Factory; -use Joomla\Database\DatabaseInterface; - -class DispatchHelper -{ - public static function findNearbyDrivers( - float $lat, - float $lng, - float $radiusKm = 10.0, - string $vehicleType = 'any', - int $limit = 10 - ): array { - $db = Factory::getContainer()->get(DatabaseInterface::class); - - $query = $db->getQuery(true) - ->select([ - 'd.id', 'd.name', 'd.rating', 'd.vehicle_id', - 'v.plate_number', 'v.vehicle_type', 'v.capacity', - 's.start_lat AS lat', 's.start_lng AS lng', - ]) - ->from($db->quoteName('#__mokosuitetaxi_drivers', 'd')) - ->join('INNER', $db->quoteName('#__mokosuitetaxi_shifts', 's') - . ' ON s.driver_id = d.id AND s.status = ' . $db->quote('active')) - ->join('LEFT', $db->quoteName('#__mokosuitetaxi_vehicles', 'v') - . ' ON v.id = d.vehicle_id') - ->where($db->quoteName('d.status') . ' = ' . $db->quote('active')) - ->where($db->quoteName('d.published') . ' = 1') - ->where('s.start_lat IS NOT NULL') - ->where('s.start_lng IS NOT NULL') - ->order('d.rating DESC') - ->setLimit($limit); - - if ($vehicleType !== 'any') { - $query->where($db->quoteName('v.vehicle_type') . ' = ' . $db->quote($vehicleType)); - } - - $db->setQuery($query); - $drivers = $db->loadObjectList() ?: []; - - $nearby = []; - - foreach ($drivers as $driver) { - $distance = TaxiHelper::haversineDistance($lat, $lng, (float) $driver->lat, (float) $driver->lng); - - if ($distance <= $radiusKm) { - $driver->distance_km = round($distance, 2); - $driver->eta_minutes = round($distance * 2.5, 1); - $nearby[] = $driver; - } - } - - usort($nearby, fn($a, $b) => $a->distance_km <=> $b->distance_km); - - return $nearby; - } - - public static function dispatchRide(int $rideId, int $driverId): object - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - $now = Factory::getDate()->toSql(); - - // FOR UPDATE lock to prevent race condition on ride acceptance - $db->transactionStart(); - - try { - $db->setQuery('SELECT id, status FROM ' . $db->quoteName('#__mokosuitetaxi_rides') - . ' WHERE id = ' . (int) $rideId . ' FOR UPDATE'); - $ride = $db->loadObject(); - - if (!$ride || !in_array($ride->status, ['requested', 'dispatched'], true)) { - $db->transactionRollback(); - throw new \RuntimeException('Ride is no longer available for dispatch'); - } - - $attempt = 1; - $db->setQuery($db->getQuery(true) - ->select('COALESCE(MAX(attempt_number), 0)') - ->from('#__mokosuitetaxi_dispatch') - ->where('ride_id = ' . (int) $rideId)); - $attempt = (int) $db->loadResult() + 1; - - $dispatch = (object) [ - 'ride_id' => $rideId, - 'driver_id' => $driverId, - 'status' => 'offered', - 'offered_at' => $now, - 'attempt_number' => $attempt, - ]; - - $db->insertObject('#__mokosuitetaxi_dispatch', $dispatch, 'id'); - - $update = (object) [ - 'id' => $rideId, - 'status' => 'dispatched', - 'driver_id' => $driverId, - 'dispatched_at' => $now, - ]; - $db->updateObject('#__mokosuitetaxi_rides', $update, 'id'); - - $db->transactionCommit(); - - return $dispatch; - } catch (\Throwable $e) { - $db->transactionRollback(); - throw $e; - } - } - - public static function acceptRide(int $rideId, int $driverId): bool - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - $now = Factory::getDate()->toSql(); - - $db->transactionStart(); - - try { - $db->setQuery('SELECT id, status, driver_id FROM ' . $db->quoteName('#__mokosuitetaxi_rides') - . ' WHERE id = ' . (int) $rideId . ' FOR UPDATE'); - $ride = $db->loadObject(); - - if (!$ride || $ride->status !== 'dispatched' || (int) $ride->driver_id !== $driverId) { - $db->transactionRollback(); - return false; - } - - $db->updateObject('#__mokosuitetaxi_rides', (object) [ - 'id' => $rideId, - 'status' => 'accepted', - 'accepted_at' => $now, - ], 'id'); - - $query = $db->getQuery(true) - ->update('#__mokosuitetaxi_dispatch') - ->set($db->quoteName('status') . ' = ' . $db->quote('accepted')) - ->set($db->quoteName('responded_at') . ' = ' . $db->quote($now)) - ->where('ride_id = ' . (int) $rideId) - ->where('driver_id = ' . (int) $driverId) - ->where($db->quoteName('status') . ' = ' . $db->quote('offered')); - $db->setQuery($query)->execute(); - - $db->transactionCommit(); - - return true; - } catch (\Throwable $e) { - $db->transactionRollback(); - throw $e; - } - } - - public static function getActiveRidesForDriver(int $driverId): array - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - - $db->setQuery($db->getQuery(true) - ->select('*') - ->from('#__mokosuitetaxi_rides') - ->where('driver_id = ' . (int) $driverId) - ->where($db->quoteName('status') . ' IN (' . implode(',', array_map([$db, 'quote'], ['dispatched', 'accepted', 'arriving', 'in_progress'])) . ')') - ->order('created DESC')); - - return $db->loadObjectList() ?: []; - } -} diff --git a/source/packages/plg_system_mokosuitetaxi/src/Helper/FareHelper.php b/source/packages/plg_system_mokosuitetaxi/src/Helper/FareHelper.php deleted file mode 100644 index 87913b7..0000000 --- a/source/packages/plg_system_mokosuitetaxi/src/Helper/FareHelper.php +++ /dev/null @@ -1,126 +0,0 @@ - - * SPDX-License-Identifier: GPL-3.0-or-later - * Authored-by: Moko Consulting - */ - -declare(strict_types=1); - -namespace Moko\Plugin\System\MokoSuiteTaxi\Helper; - -defined('_JEXEC') or die; - -use Joomla\CMS\Factory; -use Joomla\Database\DatabaseInterface; - -class FareHelper -{ - public static function calculateFare( - float $distanceKm, - float $durationMinutes, - string $vehicleType = 'sedan', - ?int $zoneId = null, - float $surgeMultiplier = 1.0 - ): object { - $rule = self::getApplicableRule($vehicleType, $zoneId); - - $baseFare = (float) $rule->base_fare; - $distanceCharge = $distanceKm * (float) $rule->per_km; - $timeCharge = $durationMinutes * (float) $rule->per_minute; - - $subtotal = ($baseFare + $distanceCharge + $timeCharge) * $surgeMultiplier; - $subtotal += (float) $rule->booking_fee; - - $hour = (int) date('H'); - if ($hour >= 22 || $hour < 6) { - $subtotal += (float) $rule->night_surcharge; - } - - $subtotal *= (float) ($rule->peak_multiplier ?? 1.0); - $total = max($subtotal, (float) $rule->minimum_fare); - - return (object) [ - 'fare_id' => (int) $rule->id, - 'base_fare' => $baseFare, - 'distance_charge' => round($distanceCharge, 2), - 'time_charge' => round($timeCharge, 2), - 'surge_multiplier' => $surgeMultiplier, - 'surcharges' => round((float) $rule->night_surcharge + (float) $rule->booking_fee, 2), - 'total_fare' => round($total, 2), - ]; - } - - public static function estimateFare( - float $pickupLat, - float $pickupLng, - float $dropoffLat, - float $dropoffLng, - string $vehicleType = 'sedan' - ): object { - $distanceKm = TaxiHelper::haversineDistance($pickupLat, $pickupLng, $dropoffLat, $dropoffLng); - $distanceKm *= 1.3; // road distance approximation - $durationMinutes = $distanceKm * 2.0; - - $estimate = self::calculateFare($distanceKm, $durationMinutes, $vehicleType); - $estimate->estimated_distance_km = round($distanceKm, 2); - $estimate->estimated_duration_minutes = round($durationMinutes, 1); - - return $estimate; - } - - public static function getApplicableRule(string $vehicleType = 'sedan', ?int $zoneId = null): object - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - $now = Factory::getDate()->format('Y-m-d'); - - $query = $db->getQuery(true) - ->select('*') - ->from('#__mokosuitetaxi_fares') - ->where($db->quoteName('published') . ' = 1') - ->where('(' . $db->quoteName('effective_from') . ' IS NULL OR ' . $db->quoteName('effective_from') . ' <= ' . $db->quote($now) . ')') - ->where('(' . $db->quoteName('effective_to') . ' IS NULL OR ' . $db->quoteName('effective_to') . ' >= ' . $db->quote($now) . ')') - ->order('zone_id DESC, vehicle_type DESC') - ->setLimit(1); - - if ($zoneId) { - $query->where('(' . $db->quoteName('zone_id') . ' = ' . (int) $zoneId . ' OR ' . $db->quoteName('zone_id') . ' IS NULL)'); - } else { - $query->where($db->quoteName('zone_id') . ' IS NULL'); - } - - $query->where('(' . $db->quoteName('vehicle_type') . ' = ' . $db->quote($vehicleType) . ' OR ' . $db->quoteName('vehicle_type') . ' = ' . $db->quote('all') . ')'); - - $db->setQuery($query); - $rule = $db->loadObject(); - - if (!$rule) { - return (object) [ - 'id' => 0, 'base_fare' => 2.50, 'per_km' => 1.50, 'per_minute' => 0.25, - 'minimum_fare' => 5.00, 'booking_fee' => 0.00, 'peak_multiplier' => 1.00, - 'night_surcharge' => 0.00, 'airport_surcharge' => 0.00, - ]; - } - - return $rule; - } - - public static function calculateDriverPayout(float $totalFare, ?float $driverCommissionRate = null): object - { - if ($driverCommissionRate === null) { - $params = Factory::getApplication()->bootPlugin('mokosuitetaxi', 'system')->params; - $driverCommissionRate = (float) ($params->get('commission_rate', 20)); - } - - $platformFee = round($totalFare * ($driverCommissionRate / 100), 2); - $driverPayout = round($totalFare - $platformFee, 2); - - return (object) [ - 'total_fare' => $totalFare, - 'commission_rate' => $driverCommissionRate, - 'platform_fee' => $platformFee, - 'driver_payout' => $driverPayout, - ]; - } -} diff --git a/source/packages/plg_system_mokosuitetaxi/src/Helper/RideHelper.php b/source/packages/plg_system_mokosuitetaxi/src/Helper/RideHelper.php deleted file mode 100644 index d68fba0..0000000 --- a/source/packages/plg_system_mokosuitetaxi/src/Helper/RideHelper.php +++ /dev/null @@ -1,211 +0,0 @@ - - * SPDX-License-Identifier: GPL-3.0-or-later - * Authored-by: Moko Consulting - */ - -declare(strict_types=1); - -namespace Moko\Plugin\System\MokoSuiteTaxi\Helper; - -defined('_JEXEC') or die; - -use Joomla\CMS\Factory; -use Joomla\Database\DatabaseInterface; - -class RideHelper -{ - private const STATUS_FLOW = [ - 'requested' => ['dispatched', 'cancelled', 'no_driver'], - 'dispatched' => ['accepted', 'cancelled', 'no_driver'], - 'accepted' => ['arriving', 'cancelled'], - 'arriving' => ['in_progress', 'cancelled'], - 'in_progress' => ['completed', 'cancelled'], - 'completed' => [], - 'cancelled' => [], - 'no_driver' => ['requested'], - ]; - - public static function requestRide(array $data): object - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - $now = Factory::getDate()->toSql(); - $user = Factory::getApplication()->getIdentity(); - - $ride = (object) [ - 'ride_ref' => TaxiHelper::generateRideRef(), - 'rider_contact_id' => isset($data['rider_contact_id']) ? (int) $data['rider_contact_id'] : null, - 'rider_name' => $data['rider_name'], - 'rider_phone' => $data['rider_phone'] ?? '', - 'status' => 'requested', - 'ride_type' => $data['ride_type'] ?? 'on_demand', - 'vehicle_type_requested' => $data['vehicle_type'] ?? 'any', - 'pickup_address' => $data['pickup_address'], - 'pickup_lat' => $data['pickup_lat'] ?? null, - 'pickup_lng' => $data['pickup_lng'] ?? null, - 'dropoff_address' => $data['dropoff_address'] ?? '', - 'dropoff_lat' => $data['dropoff_lat'] ?? null, - 'dropoff_lng' => $data['dropoff_lng'] ?? null, - 'scheduled_at' => $data['scheduled_at'] ?? null, - 'passenger_count' => (int) ($data['passenger_count'] ?? 1), - 'payment_method' => $data['payment_method'] ?? 'cash', - 'notes' => $data['notes'] ?? '', - 'created' => $now, - 'created_by' => $user ? (int) $user->id : 0, - ]; - - if ($ride->pickup_lat && $ride->pickup_lng && $ride->dropoff_lat && $ride->dropoff_lng) { - $estimate = FareHelper::estimateFare( - (float) $ride->pickup_lat, (float) $ride->pickup_lng, - (float) $ride->dropoff_lat, (float) $ride->dropoff_lng, - $ride->vehicle_type_requested === 'any' ? 'sedan' : $ride->vehicle_type_requested - ); - $ride->fare_id = $estimate->fare_id; - $ride->base_fare = $estimate->base_fare; - $ride->distance_charge = $estimate->distance_charge; - $ride->time_charge = $estimate->time_charge; - $ride->surcharges = $estimate->surcharges; - $ride->total_fare = $estimate->total_fare; - } - - $db->insertObject('#__mokosuitetaxi_rides', $ride, 'id'); - - return $ride; - } - - public static function updateStatus(int $rideId, string $newStatus): bool - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - $now = Factory::getDate()->toSql(); - - $db->setQuery($db->getQuery(true) - ->select('id, status, driver_id') - ->from('#__mokosuitetaxi_rides') - ->where('id = ' . (int) $rideId)); - $ride = $db->loadObject(); - - if (!$ride) { - return false; - } - - $allowed = self::STATUS_FLOW[$ride->status] ?? []; - - if (!in_array($newStatus, $allowed, true)) { - throw new \InvalidArgumentException( - "Cannot transition from '{$ride->status}' to '{$newStatus}'" - ); - } - - $update = (object) ['id' => $rideId, 'status' => $newStatus]; - - $timestampMap = [ - 'arriving' => 'arrived_at', - 'in_progress' => 'started_at', - 'completed' => 'completed_at', - 'cancelled' => 'cancelled_at', - ]; - - if (isset($timestampMap[$newStatus])) { - $field = $timestampMap[$newStatus]; - $update->$field = $now; - } - - $db->updateObject('#__mokosuitetaxi_rides', $update, 'id'); - - if ($newStatus === 'completed' && $ride->driver_id) { - self::updateDriverStats((int) $ride->driver_id); - } - - return true; - } - - public static function completeRide(int $rideId, float $distanceKm, float $durationMinutes): object - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - $now = Factory::getDate()->toSql(); - - $db->setQuery($db->getQuery(true)->select('*')->from('#__mokosuitetaxi_rides')->where('id = ' . (int) $rideId)); - $ride = $db->loadObject(); - - if (!$ride || $ride->status !== 'in_progress') { - throw new \RuntimeException('Ride is not in progress'); - } - - $vehicleType = $ride->vehicle_type_requested === 'any' ? 'sedan' : $ride->vehicle_type_requested; - $fare = FareHelper::calculateFare($distanceKm, $durationMinutes, $vehicleType, null, (float) $ride->surge_multiplier); - $payout = FareHelper::calculateDriverPayout($fare->total_fare); - - $update = (object) [ - 'id' => $rideId, - 'status' => 'completed', - 'completed_at' => $now, - 'distance_km' => $distanceKm, - 'duration_minutes' => $durationMinutes, - 'base_fare' => $fare->base_fare, - 'distance_charge' => $fare->distance_charge, - 'time_charge' => $fare->time_charge, - 'surcharges' => $fare->surcharges, - 'total_fare' => $fare->total_fare, - 'driver_payout' => $payout->driver_payout, - 'platform_fee' => $payout->platform_fee, - ]; - - $db->updateObject('#__mokosuitetaxi_rides', $update, 'id'); - - if ($ride->driver_id) { - self::updateDriverStats((int) $ride->driver_id); - } - - return $update; - } - - public static function getById(int $rideId): ?object - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - - $query = $db->getQuery(true) - ->select([ - 'r.*', - 'd.name AS driver_name', 'd.phone AS driver_phone', 'd.rating AS driver_rating', - 'v.plate_number', 'v.make AS vehicle_make', 'v.model AS vehicle_model', 'v.color AS vehicle_color', - ]) - ->from($db->quoteName('#__mokosuitetaxi_rides', 'r')) - ->join('LEFT', $db->quoteName('#__mokosuitetaxi_drivers', 'd') . ' ON d.id = r.driver_id') - ->join('LEFT', $db->quoteName('#__mokosuitetaxi_vehicles', 'v') . ' ON v.id = r.vehicle_id') - ->where('r.id = ' . (int) $rideId); - - $db->setQuery($query); - - return $db->loadObject() ?: null; - } - - private static function updateDriverStats(int $driverId): void - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - - $db->setQuery($db->getQuery(true) - ->select(['COUNT(*) AS total_rides', 'COALESCE(SUM(driver_payout), 0) AS total_earnings']) - ->from('#__mokosuitetaxi_rides') - ->where('driver_id = ' . (int) $driverId) - ->where($db->quoteName('status') . ' = ' . $db->quote('completed'))); - $stats = $db->loadObject(); - - $db->setQuery($db->getQuery(true) - ->select('COALESCE(AVG(rating), 5.00)') - ->from('#__mokosuitetaxi_ratings') - ->join('INNER', '#__mokosuitetaxi_rides AS r ON r.id = ride_id') - ->where('r.driver_id = ' . (int) $driverId) - ->where($db->quoteName('rated_by') . ' = ' . $db->quote('rider'))); - $avgRating = (float) $db->loadResult(); - - $db->updateObject('#__mokosuitetaxi_drivers', (object) [ - 'id' => $driverId, - 'total_rides' => (int) $stats->total_rides, - 'total_earnings' => (float) $stats->total_earnings, - 'rating' => round($avgRating, 2), - 'modified' => Factory::getDate()->toSql(), - ], 'id'); - } -} diff --git a/source/packages/plg_system_mokosuitetaxi/src/Helper/TaxiHelper.php b/source/packages/plg_system_mokosuitetaxi/src/Helper/TaxiHelper.php deleted file mode 100644 index 22a9b56..0000000 --- a/source/packages/plg_system_mokosuitetaxi/src/Helper/TaxiHelper.php +++ /dev/null @@ -1,104 +0,0 @@ - - * SPDX-License-Identifier: GPL-3.0-or-later - * Authored-by: Moko Consulting - */ - -declare(strict_types=1); - -namespace Moko\Plugin\System\MokoSuiteTaxi\Helper; - -defined('_JEXEC') or die; - -use Joomla\CMS\Factory; -use Joomla\Database\DatabaseInterface; - -class TaxiHelper -{ - public static function getDashboard(): object - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - $now = Factory::getDate()->toSql(); - - $db->setQuery($db->getQuery(true) - ->select('COUNT(*)') - ->from('#__mokosuitetaxi_drivers') - ->where($db->quoteName('status') . ' = ' . $db->quote('active')) - ->where($db->quoteName('published') . ' = 1')); - $activeDrivers = (int) $db->loadResult(); - - $db->setQuery($db->getQuery(true) - ->select('COUNT(*)') - ->from('#__mokosuitetaxi_rides') - ->where($db->quoteName('status') . ' IN (' . implode(',', array_map([$db, 'quote'], ['requested', 'dispatched', 'accepted', 'arriving', 'in_progress'])) . ')')); - $activeRides = (int) $db->loadResult(); - - $db->setQuery($db->getQuery(true) - ->select('COUNT(*)') - ->from('#__mokosuitetaxi_vehicles') - ->where($db->quoteName('status') . ' = ' . $db->quote('available')) - ->where($db->quoteName('published') . ' = 1')); - $availableVehicles = (int) $db->loadResult(); - - $db->setQuery($db->getQuery(true) - ->select('COALESCE(SUM(' . $db->quoteName('total_fare') . '), 0)') - ->from('#__mokosuitetaxi_rides') - ->where($db->quoteName('status') . ' = ' . $db->quote('completed')) - ->where($db->quoteName('payment_status') . ' = ' . $db->quote('paid')) - ->where('DATE(' . $db->quoteName('completed_at') . ') = CURDATE()')); - $todayRevenue = (float) $db->loadResult(); - - $db->setQuery($db->getQuery(true) - ->select('COUNT(*)') - ->from('#__mokosuitetaxi_rides') - ->where($db->quoteName('status') . ' = ' . $db->quote('completed')) - ->where('DATE(' . $db->quoteName('completed_at') . ') = CURDATE()')); - $todayCompleted = (int) $db->loadResult(); - - $db->setQuery($db->getQuery(true) - ->select('COALESCE(AVG(' . $db->quoteName('r.rating') . '), 0)') - ->from($db->quoteName('#__mokosuitetaxi_ratings', 'r')) - ->where($db->quoteName('r.rated_by') . ' = ' . $db->quote('rider')) - ->where('DATE(' . $db->quoteName('r.created') . ') >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)')); - $avgRating = round((float) $db->loadResult(), 2); - - return (object) [ - 'active_drivers' => $activeDrivers, - 'active_rides' => $activeRides, - 'available_vehicles' => $availableVehicles, - 'today_revenue' => $todayRevenue, - 'today_completed' => $todayCompleted, - 'avg_rating_30d' => $avgRating, - ]; - } - - public static function generateRideRef(): string - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - $prefix = 'TX'; - - do { - $ref = $prefix . strtoupper(bin2hex(random_bytes(4))); - $db->setQuery($db->getQuery(true) - ->select('COUNT(*)') - ->from('#__mokosuitetaxi_rides') - ->where($db->quoteName('ride_ref') . ' = ' . $db->quote($ref))); - } while ((int) $db->loadResult() > 0); - - return $ref; - } - - public static function haversineDistance(float $lat1, float $lng1, float $lat2, float $lng2): float - { - $earthRadius = 6371.0; - $dLat = deg2rad($lat2 - $lat1); - $dLng = deg2rad($lng2 - $lng1); - $a = sin($dLat / 2) * sin($dLat / 2) + - cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * - sin($dLng / 2) * sin($dLng / 2); - - return $earthRadius * 2 * atan2(sqrt($a), sqrt(1 - $a)); - } -} diff --git a/source/packages/plg_system_mokosuitetaxi/src/Helper/VehicleHelper.php b/source/packages/plg_system_mokosuitetaxi/src/Helper/VehicleHelper.php deleted file mode 100644 index 9566dee..0000000 --- a/source/packages/plg_system_mokosuitetaxi/src/Helper/VehicleHelper.php +++ /dev/null @@ -1,97 +0,0 @@ - - * SPDX-License-Identifier: GPL-3.0-or-later - * Authored-by: Moko Consulting - */ - -declare(strict_types=1); - -namespace Moko\Plugin\System\MokoSuiteTaxi\Helper; - -defined('_JEXEC') or die; - -use Joomla\CMS\Factory; -use Joomla\Database\DatabaseInterface; - -class VehicleHelper -{ - public static function getById(int $vehicleId): ?object - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - - $query = $db->getQuery(true) - ->select([ - 'v.*', - 'd.name AS assigned_driver', - 'd.id AS assigned_driver_id', - ]) - ->from($db->quoteName('#__mokosuitetaxi_vehicles', 'v')) - ->join('LEFT', $db->quoteName('#__mokosuitetaxi_drivers', 'd') - . ' ON d.vehicle_id = v.id AND d.status = ' . $db->quote('active')) - ->where('v.id = ' . (int) $vehicleId); - - $db->setQuery($query); - - return $db->loadObject() ?: null; - } - - public static function getFleetSummary(): object - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - - $db->setQuery($db->getQuery(true) - ->select([ - $db->quoteName('status'), - 'COUNT(*) AS count', - ]) - ->from('#__mokosuitetaxi_vehicles') - ->where($db->quoteName('published') . ' = 1') - ->group('status')); - - $rows = $db->loadObjectList('status') ?: []; - - $db->setQuery($db->getQuery(true) - ->select([ - $db->quoteName('vehicle_type'), - 'COUNT(*) AS count', - ]) - ->from('#__mokosuitetaxi_vehicles') - ->where($db->quoteName('published') . ' = 1') - ->group('vehicle_type')); - - $byType = $db->loadObjectList('vehicle_type') ?: []; - - $db->setQuery($db->getQuery(true) - ->select('COUNT(*)') - ->from('#__mokosuitetaxi_vehicles') - ->where($db->quoteName('published') . ' = 1') - ->where('(' . $db->quoteName('insurance_expiry') . ' <= DATE_ADD(CURDATE(), INTERVAL 30 DAY)' - . ' OR ' . $db->quoteName('inspection_expiry') . ' <= DATE_ADD(CURDATE(), INTERVAL 30 DAY))')); - $expiringCount = (int) $db->loadResult(); - - return (object) [ - 'by_status' => $rows, - 'by_type' => $byType, - 'expiring_soon' => $expiringCount, - 'total' => array_sum(array_map(fn($r) => (int) $r->count, (array) $rows)), - ]; - } - - public static function getExpiringDocuments(int $days = 30): array - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - $deadline = Factory::getDate('+' . $days . ' days')->format('Y-m-d'); - - $db->setQuery($db->getQuery(true) - ->select('id, plate_number, make, model, insurance_expiry, inspection_expiry') - ->from('#__mokosuitetaxi_vehicles') - ->where($db->quoteName('published') . ' = 1') - ->where('(' . $db->quoteName('insurance_expiry') . ' <= ' . $db->quote($deadline) - . ' OR ' . $db->quoteName('inspection_expiry') . ' <= ' . $db->quote($deadline) . ')') - ->order('LEAST(COALESCE(insurance_expiry, ' . $db->quote('9999-12-31') . '), COALESCE(inspection_expiry, ' . $db->quote('9999-12-31') . ')) ASC')); - - return $db->loadObjectList() ?: []; - } -} diff --git a/source/packages/plg_system_mokosuitetaxi/src/Helper/ZoneHelper.php b/source/packages/plg_system_mokosuitetaxi/src/Helper/ZoneHelper.php deleted file mode 100644 index 50a46aa..0000000 --- a/source/packages/plg_system_mokosuitetaxi/src/Helper/ZoneHelper.php +++ /dev/null @@ -1,104 +0,0 @@ - - * SPDX-License-Identifier: GPL-3.0-or-later - * Authored-by: Moko Consulting - */ - -declare(strict_types=1); - -namespace Moko\Plugin\System\MokoSuiteTaxi\Helper; - -defined('_JEXEC') or die; - -use Joomla\CMS\Factory; -use Joomla\Database\DatabaseInterface; - -class ZoneHelper -{ - public static function detectZone(float $lat, float $lng): ?object - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - - $db->setQuery($db->getQuery(true) - ->select('*') - ->from('#__mokosuitetaxi_zones') - ->where($db->quoteName('published') . ' = 1') - ->where('center_lat IS NOT NULL') - ->where('center_lng IS NOT NULL') - ->order('ordering ASC')); - - $zones = $db->loadObjectList() ?: []; - - foreach ($zones as $zone) { - $radius = (float) ($zone->radius_km ?: 50.0); - $distance = TaxiHelper::haversineDistance($lat, $lng, (float) $zone->center_lat, (float) $zone->center_lng); - - if ($distance <= $radius) { - $zone->distance_from_center = round($distance, 2); - return $zone; - } - } - - return null; - } - - public static function getSurgeMultiplier(int $zoneId): float - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - - $db->setQuery($db->getQuery(true) - ->select('COUNT(*)') - ->from('#__mokosuitetaxi_rides') - ->where($db->quoteName('pickup_zone_id') . ' = ' . (int) $zoneId) - ->where($db->quoteName('status') . ' IN (' . implode(',', array_map([$db, 'quote'], ['requested', 'dispatched'])) . ')') - ->where($db->quoteName('created') . ' >= DATE_SUB(NOW(), INTERVAL 15 MINUTE)')); - $demandCount = (int) $db->loadResult(); - - $db->setQuery($db->getQuery(true) - ->select('COUNT(DISTINCT d.id)') - ->from($db->quoteName('#__mokosuitetaxi_drivers', 'd')) - ->join('INNER', $db->quoteName('#__mokosuitetaxi_shifts', 's') . ' ON s.driver_id = d.id AND s.status = ' . $db->quote('active')) - ->where($db->quoteName('d.status') . ' = ' . $db->quote('active'))); - $supplyCount = max((int) $db->loadResult(), 1); - - $ratio = $demandCount / $supplyCount; - - if ($ratio <= 0.5) { - return 1.0; - } - - if ($ratio <= 1.0) { - return 1.25; - } - - if ($ratio <= 2.0) { - return 1.5; - } - - $params = Factory::getApplication()->bootPlugin('mokosuitetaxi', 'system')->params; - $maxSurge = (float) $params->get('max_surge_multiplier', 3.0); - - return min(1.5 + ($ratio - 2.0) * 0.5, $maxSurge); - } - - public static function listZones(string $type = ''): array - { - $db = Factory::getContainer()->get(DatabaseInterface::class); - - $query = $db->getQuery(true) - ->select('*') - ->from('#__mokosuitetaxi_zones') - ->where($db->quoteName('published') . ' = 1') - ->order('ordering ASC'); - - if ($type) { - $query->where($db->quoteName('zone_type') . ' = ' . $db->quote($type)); - } - - $db->setQuery($query); - - return $db->loadObjectList() ?: []; - } -} diff --git a/source/packages/plg_webservices_mokosuitetaxi/mokosuitetaxi.xml b/source/packages/plg_webservices_mokosuitetaxi/mokosuitetaxi.xml deleted file mode 100644 index d8b2406..0000000 --- a/source/packages/plg_webservices_mokosuitetaxi/mokosuitetaxi.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - Web Services - MokoSuite Taxi - mokosuitetaxi - Moko Consulting - 06.00.00 - GPL-3.0-or-later - Moko\Plugin\WebServices\MokoSuiteTaxi - srcservices - diff --git a/source/packages/plg_webservices_mokosuitetaxi/services/provider.php b/source/packages/plg_webservices_mokosuitetaxi/services/provider.php deleted file mode 100644 index 39d09c9..0000000 --- a/source/packages/plg_webservices_mokosuitetaxi/services/provider.php +++ /dev/null @@ -1,24 +0,0 @@ - - * SPDX-License-Identifier: GPL-3.0-or-later - * Authored-by: Moko Consulting - */ - -defined('_JEXEC') or die; - -use Joomla\CMS\Extension\PluginInterface; -use Joomla\CMS\Plugin\PluginHelper; -use Joomla\DI\Container; -use Joomla\DI\ServiceProviderInterface; -use Joomla\Event\DispatcherInterface; -use Moko\Plugin\WebServices\MokoSuiteTaxi\Extension\MokoSuiteTaxi; - -return new class implements ServiceProviderInterface { - public function register(Container $container): void { - $container->set(PluginInterface::class, function (Container $container) { - return new MokoSuiteTaxi($container->get(DispatcherInterface::class), (array) PluginHelper::getPlugin('webservices', 'mokosuitetaxi')); - }); - } -}; diff --git a/source/packages/plg_webservices_mokosuitetaxi/src/Extension/MokoSuiteTaxi.php b/source/packages/plg_webservices_mokosuitetaxi/src/Extension/MokoSuiteTaxi.php deleted file mode 100644 index 47cc288..0000000 --- a/source/packages/plg_webservices_mokosuitetaxi/src/Extension/MokoSuiteTaxi.php +++ /dev/null @@ -1,36 +0,0 @@ - - * SPDX-License-Identifier: GPL-3.0-or-later - * Authored-by: Moko Consulting - */ - -namespace Moko\Plugin\WebServices\MokoSuiteTaxi\Extension; - -defined('_JEXEC') or die; - -use Joomla\CMS\Plugin\CMSPlugin; -use Joomla\Event\SubscriberInterface; - -class MokoSuiteTaxi extends CMSPlugin implements SubscriberInterface -{ - public static function getSubscribedEvents(): array - { - return ['onBeforeApiRoute' => 'onBeforeApiRoute']; - } - - public function onBeforeApiRoute(&$event): void - { - $router = $event->getArgument('router'); - $opts = ['component' => 'com_mokosuitetaxi']; - - $router->createCRUDRoutes('v1/mokosuitetaxi/rides', 'rides', $opts); - $router->createCRUDRoutes('v1/mokosuitetaxi/vehicles', 'vehicles', $opts); - $router->createCRUDRoutes('v1/mokosuitetaxi/drivers', 'drivers', $opts); - $router->createCRUDRoutes('v1/mokosuitetaxi/zones', 'zones', $opts); - $router->createCRUDRoutes('v1/mokosuitetaxi/fares', 'fares', $opts); - $router->createCRUDRoutes('v1/mokosuitetaxi/dispatch', 'dispatch', $opts); - $router->createCRUDRoutes('v1/mokosuitetaxi/ratings', 'ratings', $opts); - } -} diff --git a/source/pkg_mokosuitetaxi.xml b/source/pkg_mokosuitetaxi.xml deleted file mode 100644 index 2c1dfb5..0000000 --- a/source/pkg_mokosuitetaxi.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - Package - MokoSuite Taxi - mokosuitetaxi - 06.00.00 - 2026-06-27 - Moko Consulting - hello@mokoconsulting.tech - https://mokoconsulting.tech - Copyright (C) 2026 Moko Consulting. All rights reserved. - GNU General Public License version 3 or later; see LICENSE - Ride-hailing, dispatch, fleet management, fare zones, and driver scheduling - 8.3 - - true - - plg_system_mokosuitetaxi.zip - - - https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteTaxi/updates.xml - -