diff --git a/.github/.mokostandards b/.github/.mokostandards new file mode 100644 index 0000000..09b7828 --- /dev/null +++ b/.github/.mokostandards @@ -0,0 +1 @@ +platform: waas-component diff --git a/.github/CLAUDE.md b/.github/CLAUDE.md index 45f7606..4e7b705 100644 --- a/.github/CLAUDE.md +++ b/.github/CLAUDE.md @@ -1,3 +1,22 @@ + + > [!IMPORTANT] > **πŸ”§ AI Self-Update Required on First Use** > @@ -19,135 +38,34 @@ > |---|---| > | `MokoCassiopeia` | The GitHub repository name (visible in the URL, `README.md` heading, or `git remote -v`) | > | `https://github.com/mokoconsulting-tech/MokoCassiopeia` | Full GitHub URL, e.g. `https://github.com/mokoconsulting-tech/` | -> | `A modern enhancement layer for Joomla’s Cassiopeia template. Moko-Cassiopeia adds Font Awesome 7, Bootstrap 5 helpers, an automatic Table of Contents (TOC) utility, and optional expansions including Google Tag Manager and Google Analytics (GA4) hooks.` | First paragraph of `README.md` body, or the GitHub repo description | > | `{{EXTENSION_NAME}}` | The `` element in `manifest.xml` at the repository root | > | `{{EXTENSION_TYPE}}` | The `type` attribute of the `` tag in `manifest.xml` (`component`, `module`, `plugin`, or `template`) | > | `{{EXTENSION_ELEMENT}}` | The `` tag in `manifest.xml`, or the filename prefix (e.g. `com_myextension`, `mod_mymodule`) | > > --- -# What This Repo Is +# MokoCassiopeia β€” GitHub Copilot Custom Instructions -**MokoCassiopeia** is a Moko Consulting **MokoWaaS** (Joomla) extension repository. +## What This Repo Is -A modern enhancement layer for Joomla’s Cassiopeia template. Moko-Cassiopeia adds Font Awesome 7, Bootstrap 5 helpers, an automatic Table of Contents (TOC) utility, and optional expansions including Google Tag Manager and Google Analytics (GA4) hooks. +This is a **Moko Consulting MokoWaaS** (Joomla) repository governed by [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards). All coding standards, workflows, and policies are defined there and enforced here via bulk sync. +Repository URL: https://github.com/mokoconsulting-tech/MokoCassiopeia Extension name: **{{EXTENSION_NAME}}** Extension type: **{{EXTENSION_TYPE}}** (`{{EXTENSION_ELEMENT}}`) -Repository URL: https://github.com/mokoconsulting-tech/MokoCassiopeia - -This repository is governed by [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards) β€” the single source of truth for coding standards, file-header policies, GitHub Actions workflows, and Terraform configuration templates across all Moko Consulting repositories. +Platform: **Joomla 4.x / MokoWaaS** --- -# Repo Structure +## Primary Language -``` -MokoCassiopeia/ -β”œβ”€β”€ manifest.xml # Joomla installer manifest (root β€” required) -β”œβ”€β”€ update.xml # Update server manifest (root β€” required) -β”œβ”€β”€ site/ # Frontend (site) code -β”‚ β”œβ”€β”€ controller.php -β”‚ β”œβ”€β”€ controllers/ -β”‚ β”œβ”€β”€ models/ -β”‚ └── views/ -β”œβ”€β”€ admin/ # Backend (admin) code -β”‚ β”œβ”€β”€ controller.php -β”‚ β”œβ”€β”€ controllers/ -β”‚ β”œβ”€β”€ models/ -β”‚ β”œβ”€β”€ views/ -β”‚ └── sql/ -β”œβ”€β”€ language/ # Language INI files -β”œβ”€β”€ media/ # CSS, JS, images -β”œβ”€β”€ docs/ # Technical documentation -β”œβ”€β”€ tests/ # Test suite -β”œβ”€β”€ .github/ -β”‚ β”œβ”€β”€ workflows/ # CI/CD workflows (synced from MokoStandards) -β”‚ β”œβ”€β”€ copilot-instructions.md -β”‚ └── CLAUDE.md # This file -β”œβ”€β”€ README.md # Version source of truth -β”œβ”€β”€ CHANGELOG.md -β”œβ”€β”€ CONTRIBUTING.md -└── LICENSE # GPL-3.0-or-later -``` +**PHP** (β‰₯ 7.4) is the primary language for this Joomla extension. JavaScript may be used for frontend enhancements. YAML uses 2-space indentation. All other text files use tabs per `.editorconfig`. --- -# Primary Language +## File Header β€” Always Required on New Files -**PHP** (β‰₯ 7.4) is the primary language for this Joomla extension. YAML uses 2-space indentation. All other text files use tabs per `.editorconfig`. - ---- - -# Version Management - -**`README.md` is the single source of truth for the repository version.** - -- **Bump the patch version on every PR** β€” increment `XX.YY.ZZ` (e.g. `01.02.03` β†’ `01.02.04`) in `README.md` before opening the PR; the `sync-version-on-merge` workflow propagates it to all `FILE INFORMATION` headers automatically on merge. -- Version format is zero-padded semver: `XX.YY.ZZ` (e.g. `01.02.03`). -- Never hardcode a version number in body text β€” use the badge or FILE INFORMATION header only. - -### Joomla Version Alignment - -Three files must **always have the same version**: - -| File | Where the version lives | -|------|------------------------| -| `README.md` | `FILE INFORMATION` block + badge | -| `manifest.xml` | `` tag | -| `update.xml` | `` in the most recent `` block | - -The `make release` command / release workflow syncs all three automatically. - ---- - -# update.xml β€” Required in Repo Root - -`update.xml` is the Joomla update server manifest. It allows Joomla installations to check for new versions of this extension via: - -```xml - - - - https://github.com/mokoconsulting-tech/MokoCassiopeia/raw/main/update.xml - - -``` - -**Rules:** -- Every release prepends a new `` block at the top β€” older entries are preserved. -- `` in `update.xml` must exactly match `` in `manifest.xml` and `README.md`. -- `` must be a publicly accessible GitHub Releases asset URL. -- `` β€” backslash is literal (Joomla regex syntax). - -Example `update.xml` entry for a new release: -```xml - - - {{EXTENSION_NAME}} - MokoCassiopeia - {{EXTENSION_ELEMENT}} - {{EXTENSION_TYPE}} - 01.02.04 - https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/tag/01.02.04 - - - https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/01.02.04/{{EXTENSION_ELEMENT}}-01.02.04.zip - - - - 7.4 - Moko Consulting - https://mokoconsulting.tech - - -``` - ---- - -# File Header Requirements - -Every new file **must** have a copyright header as its first content. JSON files, binary files, generated files, and third-party files are exempt. +Every new file needs a copyright header as its first content. **PHP:** ```php @@ -162,47 +80,141 @@ Every new file **must** have a copyright header as its first content. JSON files * DEFGROUP: MokoCassiopeia.{{EXTENSION_TYPE}} * INGROUP: MokoCassiopeia * REPO: https://github.com/mokoconsulting-tech/MokoCassiopeia - * PATH: /site/controllers/item.php + * PATH: /path/to/file.php * VERSION: XX.YY.ZZ - * BRIEF: One-line description of file purpose + * BRIEF: One-line description of purpose */ defined('_JEXEC') or die; ``` -**Markdown / YAML / Shell / XML:** Use the appropriate comment syntax with the same fields. +**Markdown:** +```markdown + +``` + +**YAML / Shell / XML:** Use the appropriate comment syntax with the same fields. JSON files are exempt. --- -# Coding Standards +## Version Management -## Naming Conventions +**`README.md` is the single source of truth for the repository version.** -| Context | Convention | Example | -|---------|-----------|---------| -| PHP class | `PascalCase` | `ItemModel` | -| PHP method / function | `camelCase` | `getItems()` | -| PHP variable | `$snake_case` | `$item_id` | -| PHP constant | `UPPER_SNAKE_CASE` | `MAX_ITEMS` | -| PHP class file | `PascalCase.php` | `ItemModel.php` | -| YAML workflow | `kebab-case.yml` | `ci-joomla.yml` | -| Markdown doc | `kebab-case.md` | `installation-guide.md` | +- **Bump the patch version on every PR** β€” increment `XX.YY.ZZ` (e.g. `01.02.03` β†’ `01.02.04`) in `README.md` before opening the PR; the `sync-version-on-merge` workflow propagates it automatically to all badges and `FILE INFORMATION` headers on merge to `main`. +- The `VERSION: XX.YY.ZZ` field in `README.md` governs all other version references. +- Version format is zero-padded semver: `XX.YY.ZZ` (e.g. `01.02.03`). +- Never hardcode a specific version in document body text β€” use the badge or FILE INFORMATION header only. -## Commit Messages +### Joomla Version Alignment -Format: `(): ` β€” imperative, lower-case subject, no trailing period. +The version in `README.md` **must always match** the `` tag in `manifest.xml` and the latest entry in `updates.xml`. The `make release` command / release workflow updates all three automatically. -Valid types: `feat` Β· `fix` Β· `docs` Β· `chore` Β· `ci` Β· `refactor` Β· `style` Β· `test` Β· `perf` Β· `revert` Β· `build` +```xml + +01.02.04 -## Branch Naming - -Format: `/[/description]` - -Approved prefixes: `dev/` Β· `rc/` Β· `version/` Β· `patch/` Β· `copilot/` Β· `dependabot/` + + + + {{EXTENSION_NAME}} + 01.02.04 + + + https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/01.02.04/{{EXTENSION_ELEMENT}}-01.02.04.zip + + + + + + +``` --- -# GitHub Actions β€” Token Usage +## Joomla Extension Structure + +``` +MokoCassiopeia/ +β”œβ”€β”€ manifest.xml # Joomla installer manifest (root β€” required) +β”œβ”€β”€ updates.xml # Update server manifest (root β€” required, see below) +β”œβ”€β”€ site/ # Frontend (site) code +β”‚ β”œβ”€β”€ controller.php +β”‚ β”œβ”€β”€ controllers/ +β”‚ β”œβ”€β”€ models/ +β”‚ └── views/ +β”œβ”€β”€ admin/ # Backend (admin) code +β”‚ β”œβ”€β”€ controller.php +β”‚ β”œβ”€β”€ controllers/ +β”‚ β”œβ”€β”€ models/ +β”‚ β”œβ”€β”€ views/ +β”‚ └── sql/ +β”œβ”€β”€ language/ # Language INI files +β”œβ”€β”€ media/ # CSS, JS, images (deployed to /media/{{EXTENSION_ELEMENT}}/) +β”œβ”€β”€ docs/ # Technical documentation +β”œβ”€β”€ tests/ # Test suite +β”œβ”€β”€ .github/ +β”‚ β”œβ”€β”€ workflows/ +β”‚ β”œβ”€β”€ copilot-instructions.md # This file +β”‚ └── CLAUDE.md +β”œβ”€β”€ README.md # Version source of truth +β”œβ”€β”€ CHANGELOG.md +β”œβ”€β”€ CONTRIBUTING.md +β”œβ”€β”€ LICENSE # GPL-3.0-or-later +└── Makefile # Build automation +``` + +--- + +## updates.xml β€” Required in Repo Root + +`updates.xml` **must exist at the repository root**. It is the Joomla update server manifest that allows Joomla installations to check for new versions of this extension. + +The `manifest.xml` must reference it via: +```xml + + + https://github.com/mokoconsulting-tech/MokoCassiopeia/raw/main/updates.xml + + +``` + +**Rules:** +- Every release must prepend a new `` block at the top of `updates.xml` β€” old entries must be preserved below. +- The `` in `updates.xml` must exactly match `` in `manifest.xml` and the version in `README.md`. +- The `` must be a publicly accessible direct download link (GitHub Releases asset URL). +- `` β€” the backslash is a **literal backslash character** in the XML attribute value; Joomla's update-server parser treats the value as a regular expression, so `\.` matches a literal dot and `[0-9]+` matches one or more digits. Do not double-escape it. + +--- + +## manifest.xml Rules + +- Lives at the repo root as `manifest.xml` (not inside `site/` or `admin/`). +- `` tag must be kept in sync with `README.md` version and `updates.xml`. +- Must include `` block pointing to this repo's `updates.xml`. +- Must include `` and `` sections. +- Joomla 4.x requires `Moko\{{EXTENSION_NAME}}` for namespaced extensions. + +--- + +## GitHub Actions β€” Token Usage Every workflow must use **`secrets.GH_TOKEN`** (the org-level Personal Access Token). @@ -217,58 +229,76 @@ env: ``` ```yaml -# ❌ Wrong β€” never use these +# ❌ Wrong β€” never use these in workflows token: ${{ github.token }} token: ${{ secrets.GITHUB_TOKEN }} ``` --- -# Keeping Documentation Current +## MokoStandards Reference -| Change type | Documentation to update | -|-------------|------------------------| -| New or renamed PHP class/method | PHPDoc block; `docs/api/` entry | -| New or changed `manifest.xml` | Sync version to `update.xml` and `README.md` | -| New release | Prepend `` to `update.xml`; update `CHANGELOG.md`; bump `README.md` | -| New or changed workflow | `docs/workflows/.md` | -| Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block | -| **Every PR** | **Bump the patch version** β€” increment `XX.YY.ZZ` in `README.md`; `sync-version-on-merge` propagates it | - ---- - -# What NOT to Do - -- **Never commit directly to `main`** β€” all changes go through a PR. -- **Never hardcode version numbers** in body text β€” update `README.md` and let automation propagate. -- **Never let `manifest.xml`, `update.xml`, and `README.md` versions diverge.** -- **Never skip the FILE INFORMATION block** on a new source file. -- **Never use bare `catch (\Throwable $e) {}`** β€” always log or re-throw. -- **Never mix tabs and spaces** within a file β€” follow `.editorconfig`. -- **Never use `github.token` or `secrets.GITHUB_TOKEN` in workflows** β€” always use `secrets.GH_TOKEN`. -- **Never remove `defined('_JEXEC') or die;`** from web-accessible PHP files. - ---- - -# PR Checklist - -Before opening a PR, verify: - -- [ ] Patch version bumped in `README.md` (e.g. `01.02.03` β†’ `01.02.04`) -- [ ] If this is a release: `manifest.xml` version updated; `update.xml` updated with new entry -- [ ] FILE INFORMATION headers updated in modified files -- [ ] CHANGELOG.md updated -- [ ] Tests pass - ---- - -# Key Policy Documents (MokoStandards) +This repository is governed by [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards). Authoritative policies: | Document | Purpose | |----------|---------| | [file-header-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/file-header-standards.md) | Copyright-header rules for every file type | | [coding-style-guide.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/coding-style-guide.md) | Naming and formatting conventions | | [branching-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/branching-strategy.md) | Branch naming, hierarchy, and release workflow | -| [merge-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/merge-strategy.md) | Squash-merge policy and PR conventions | +| [merge-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/merge-strategy.md) | Squash-merge policy and PR title/body conventions | | [changelog-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/changelog-standards.md) | How and when to update CHANGELOG.md | -| [joomla-development-guide.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/guide/waas/joomla-development-guide.md) | MokoWaaS Joomla extension development guide | \ No newline at end of file +| [joomla-development-guide.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/guide/waas/joomla-development-guide.md) | MokoWaaS Joomla extension development guide | + +--- + +## Naming Conventions + +| Context | Convention | Example | +|---------|-----------|---------| +| PHP class | `PascalCase` | `MyController` | +| PHP method / function | `camelCase` | `getItems()` | +| PHP variable | `$snake_case` | `$item_id` | +| PHP constant | `UPPER_SNAKE_CASE` | `MAX_ITEMS` | +| PHP class file | `PascalCase.php` | `ItemModel.php` | +| YAML workflow | `kebab-case.yml` | `ci-joomla.yml` | +| Markdown doc | `kebab-case.md` | `installation-guide.md` | + +--- + +## Commit Messages + +Format: `(): ` β€” imperative, lower-case subject, no trailing period. + +Valid types: `feat` Β· `fix` Β· `docs` Β· `chore` Β· `ci` Β· `refactor` Β· `style` Β· `test` Β· `perf` Β· `revert` Β· `build` + +--- + +## Branch Naming + +Format: `/[/description]` + +Approved prefixes: `dev/` Β· `rc/` Β· `version/` Β· `patch/` Β· `copilot/` Β· `dependabot/` + +--- + +## Keeping Documentation Current + +| Change type | Documentation to update | +|-------------|------------------------| +| New or renamed PHP class/method | PHPDoc block; `docs/api/` entry | +| New or changed manifest.xml | Update `updates.xml` version; bump README.md version | +| New release | Prepend `` block to `updates.xml`; update CHANGELOG.md; bump README.md version | +| New or changed workflow | `docs/workflows/.md` | +| Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block | +| **Every PR** | **Bump the patch version** β€” increment `XX.YY.ZZ` in `README.md`; `sync-version-on-merge` propagates it | + +--- + +## Key Constraints + +- Never commit directly to `main` β€” all changes go via PR, squash-merged +- Never skip the FILE INFORMATION block on a new file +- Never add `defined('_JEXEC') or die;` to CLI scripts or model tests β€” only to web-accessible PHP files +- Never hardcode version numbers in body text β€” update `README.md` and let automation propagate +- Never use `github.token` or `secrets.GITHUB_TOKEN` in workflows β€” always use `secrets.GH_TOKEN` +- Never let `manifest.xml` version, `updates.xml` version, and `README.md` version go out of sync diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 93f049a..c70eb15 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -8,8 +8,26 @@ # Combined with branch protection (require PR reviews), this prevents # unauthorized modifications to workflows, configs, and governance files. -# ── Workflows (synced from MokoStandards β€” must not be manually edited) ── -/.github/workflows/ @jmiller-moko +# ── Synced workflows (managed by MokoStandards β€” do not edit manually) ──── +/.github/workflows/deploy-dev.yml @jmiller-moko +/.github/workflows/deploy-demo.yml @jmiller-moko +/.github/workflows/deploy-rs.yml @jmiller-moko +/.github/workflows/auto-release.yml @jmiller-moko +/.github/workflows/auto-dev-issue.yml @jmiller-moko +/.github/workflows/auto-assign.yml @jmiller-moko +/.github/workflows/sync-version-on-merge.yml @jmiller-moko +/.github/workflows/enterprise-firewall-setup.yml @jmiller-moko +/.github/workflows/repository-cleanup.yml @jmiller-moko +/.github/workflows/standards-compliance.yml @jmiller-moko +/.github/workflows/codeql-analysis.yml @jmiller-moko +/.github/workflows/repo_health.yml @jmiller-moko +/.github/workflows/ci-joomla.yml @jmiller-moko +/.github/workflows/update-server.yml @jmiller-moko +/.github/workflows/deploy-manual.yml @jmiller-moko +/.github/workflows/ci-dolibarr.yml @jmiller-moko +/.github/workflows/publish-to-mokodolimods.yml @jmiller-moko +/.github/workflows/changelog-validation.yml @jmiller-moko +# Custom workflows in .github/workflows/ not listed above are repo-owned. # ── GitHub configuration ───────────────────────────────────────────────── /.github/ISSUE_TEMPLATE/ @jmiller-moko @@ -23,7 +41,7 @@ /composer.json @jmiller-moko /phpstan.neon @jmiller-moko /Makefile @jmiller-moko -/.ftp_ignore @jmiller-moko +/.ftpignore @jmiller-moko /.gitignore @jmiller-moko /.gitattributes @jmiller-moko /.editorconfig @jmiller-moko diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 3968dc4..4e7b705 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,3 +1,22 @@ + + > [!IMPORTANT] > **πŸ”§ AI Self-Update Required on First Use** > @@ -103,13 +122,13 @@ BRIEF: One-line description ### Joomla Version Alignment -The version in `README.md` **must always match** the `` tag in `manifest.xml` and the latest entry in `update.xml`. The `make release` command / release workflow updates all three automatically. +The version in `README.md` **must always match** the `` tag in `manifest.xml` and the latest entry in `updates.xml`. The `make release` command / release workflow updates all three automatically. ```xml 01.02.04 - @@ -135,7 +154,7 @@ The version in `README.md` **must always match** the `` tag in `manifes ``` MokoCassiopeia/ β”œβ”€β”€ manifest.xml # Joomla installer manifest (root β€” required) -β”œβ”€β”€ update.xml # Update server manifest (root β€” required, see below) +β”œβ”€β”€ updates.xml # Update server manifest (root β€” required, see below) β”œβ”€β”€ site/ # Frontend (site) code β”‚ β”œβ”€β”€ controller.php β”‚ β”œβ”€β”€ controllers/ @@ -164,22 +183,22 @@ MokoCassiopeia/ --- -## update.xml β€” Required in Repo Root +## updates.xml β€” Required in Repo Root -`update.xml` **must exist at the repository root**. It is the Joomla update server manifest that allows Joomla installations to check for new versions of this extension. +`updates.xml` **must exist at the repository root**. It is the Joomla update server manifest that allows Joomla installations to check for new versions of this extension. The `manifest.xml` must reference it via: ```xml - https://github.com/mokoconsulting-tech/MokoCassiopeia/raw/main/update.xml + https://github.com/mokoconsulting-tech/MokoCassiopeia/raw/main/updates.xml ``` **Rules:** -- Every release must prepend a new `` block at the top of `update.xml` β€” old entries must be preserved below. -- The `` in `update.xml` must exactly match `` in `manifest.xml` and the version in `README.md`. +- Every release must prepend a new `` block at the top of `updates.xml` β€” old entries must be preserved below. +- The `` in `updates.xml` must exactly match `` in `manifest.xml` and the version in `README.md`. - The `` must be a publicly accessible direct download link (GitHub Releases asset URL). - `` β€” the backslash is a **literal backslash character** in the XML attribute value; Joomla's update-server parser treats the value as a regular expression, so `\.` matches a literal dot and `[0-9]+` matches one or more digits. Do not double-escape it. @@ -188,8 +207,8 @@ The `manifest.xml` must reference it via: ## manifest.xml Rules - Lives at the repo root as `manifest.xml` (not inside `site/` or `admin/`). -- `` tag must be kept in sync with `README.md` version and `update.xml`. -- Must include `` block pointing to this repo's `update.xml`. +- `` tag must be kept in sync with `README.md` version and `updates.xml`. +- Must include `` block pointing to this repo's `updates.xml`. - Must include `` and `` sections. - Joomla 4.x requires `Moko\{{EXTENSION_NAME}}` for namespaced extensions. @@ -267,8 +286,8 @@ Approved prefixes: `dev/` Β· `rc/` Β· `version/` Β· `patch/` Β· `copilot/` Β· `d | Change type | Documentation to update | |-------------|------------------------| | New or renamed PHP class/method | PHPDoc block; `docs/api/` entry | -| New or changed manifest.xml | Update `update.xml` version; bump README.md version | -| New release | Prepend `` block to `update.xml`; update CHANGELOG.md; bump README.md version | +| New or changed manifest.xml | Update `updates.xml` version; bump README.md version | +| New release | Prepend `` block to `updates.xml`; update CHANGELOG.md; bump README.md version | | New or changed workflow | `docs/workflows/.md` | | Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block | | **Every PR** | **Bump the patch version** β€” increment `XX.YY.ZZ` in `README.md`; `sync-version-on-merge` propagates it | @@ -282,4 +301,4 @@ Approved prefixes: `dev/` Β· `rc/` Β· `version/` Β· `patch/` Β· `copilot/` Β· `d - Never add `defined('_JEXEC') or die;` to CLI scripts or model tests β€” only to web-accessible PHP files - Never hardcode version numbers in body text β€” update `README.md` and let automation propagate - Never use `github.token` or `secrets.GITHUB_TOKEN` in workflows β€” always use `secrets.GH_TOKEN` -- Never let `manifest.xml` version, `update.xml` version, and `README.md` version go out of sync \ No newline at end of file +- Never let `manifest.xml` version, `updates.xml` version, and `README.md` version go out of sync diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b94720f..667ae72 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,7 +5,7 @@ # INGROUP: MokoStandards.Security # REPO: https://github.com/mokoconsulting-tech/MokoStandards # PATH: /.github/dependabot.yml -# VERSION: 01.00.00 +# VERSION: 03.09.03 # BRIEF: Dependabot configuration for automated dependency updates and security patches # NOTE: Monitors GitHub Actions for vulnerabilities and keeps ecosystem secure diff --git a/.github/workflows/auto-assign.yml b/.github/workflows/auto-assign.yml new file mode 100644 index 0000000..d0b70f6 --- /dev/null +++ b/.github/workflows/auto-assign.yml @@ -0,0 +1,76 @@ +# Copyright (C) 2026 Moko Consulting +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: GitHub.Workflow +# INGROUP: MokoStandards.Workflows.Shared +# REPO: https://github.com/mokoconsulting-tech/MokoStandards +# PATH: /.github/workflows/auto-assign.yml +# VERSION: 04.06.00 +# BRIEF: Auto-assign jmiller-moko to unassigned issues and PRs every 15 minutes + +name: Auto-Assign Issues & PRs + +on: + issues: + types: [opened] + pull_request_target: + types: [opened] + schedule: + - cron: '0 */12 * * *' + workflow_dispatch: + +permissions: + issues: write + pull-requests: write + +jobs: + auto-assign: + name: Assign unassigned issues and PRs + runs-on: ubuntu-latest + + steps: + - name: Assign unassigned issues + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + run: | + REPO="${{ github.repository }}" + ASSIGNEE="jmiller-moko" + + echo "## 🏷️ Auto-Assign Report" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + ASSIGNED_ISSUES=0 + ASSIGNED_PRS=0 + + # Assign unassigned open issues + ISSUES=$(gh api "repos/$REPO/issues?state=open&per_page=100&assignee=none" --jq '.[].number' 2>/dev/null || true) + for NUM in $ISSUES; do + # Skip PRs (the issues endpoint returns PRs too) + IS_PR=$(gh api "repos/$REPO/issues/$NUM" --jq '.pull_request // empty' 2>/dev/null || true) + if [ -z "$IS_PR" ]; then + gh api "repos/$REPO/issues/$NUM/assignees" -X POST -f "assignees[]=$ASSIGNEE" --silent 2>/dev/null && { + ASSIGNED_ISSUES=$((ASSIGNED_ISSUES + 1)) + echo " Assigned issue #$NUM" + } || true + fi + done + + # Assign unassigned open PRs + PRS=$(gh api "repos/$REPO/pulls?state=open&per_page=100" --jq '.[] | select(.assignees | length == 0) | .number' 2>/dev/null || true) + for NUM in $PRS; do + gh api "repos/$REPO/issues/$NUM/assignees" -X POST -f "assignees[]=$ASSIGNEE" --silent 2>/dev/null && { + ASSIGNED_PRS=$((ASSIGNED_PRS + 1)) + echo " Assigned PR #$NUM" + } || true + done + + echo "| Type | Assigned |" >> $GITHUB_STEP_SUMMARY + echo "|------|----------|" >> $GITHUB_STEP_SUMMARY + echo "| Issues | $ASSIGNED_ISSUES |" >> $GITHUB_STEP_SUMMARY + echo "| Pull Requests | $ASSIGNED_PRS |" >> $GITHUB_STEP_SUMMARY + + if [ "$ASSIGNED_ISSUES" -eq 0 ] && [ "$ASSIGNED_PRS" -eq 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "βœ… All issues and PRs already have assignees" >> $GITHUB_STEP_SUMMARY + fi diff --git a/.github/workflows/auto-dev-issue.yml b/.github/workflows/auto-dev-issue.yml index 3364016..9b5fbe2 100644 --- a/.github/workflows/auto-dev-issue.yml +++ b/.github/workflows/auto-dev-issue.yml @@ -9,14 +9,22 @@ # INGROUP: MokoStandards.Automation # REPO: https://github.com/mokoconsulting-tech/MokoStandards # PATH: /templates/workflows/shared/auto-dev-issue.yml.template -# VERSION: 04.04.01 -# BRIEF: Auto-create tracking issue when a dev/** or rc/** branch is pushed +# VERSION: 04.06.00 +# BRIEF: Auto-create tracking issue with sub-issues for dev/rc branch workflow # NOTE: Synced via bulk-repo-sync to .github/workflows/auto-dev-issue.yml in all governed repos. -name: Auto Dev Branch Issue +name: Dev/RC Branch Issue on: + # Auto-create on RC branch creation create: + # Manual trigger for dev branches + workflow_dispatch: + inputs: + branch: + description: 'Branch name (e.g., dev/my-feature or dev/04.06)' + required: true + type: string env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true @@ -30,15 +38,23 @@ jobs: name: Create version tracking issue runs-on: ubuntu-latest if: >- - github.event.ref_type == 'branch' && - (startsWith(github.event.ref, 'dev/') || startsWith(github.event.ref, 'rc/')) + (github.event_name == 'workflow_dispatch') || + (github.event.ref_type == 'branch' && + (startsWith(github.event.ref, 'rc/') || + startsWith(github.event.ref, 'alpha/') || + startsWith(github.event.ref, 'beta/'))) steps: - - name: Create tracking issue + - name: Create tracking issue and sub-issues env: GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} run: | - BRANCH="${{ github.event.ref }}" + # For manual dispatch, use input; for auto, use event ref + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + BRANCH="${{ inputs.branch }}" + else + BRANCH="${{ github.event.ref }}" + fi REPO="${{ github.repository }}" ACTOR="${{ github.actor }}" NOW=$(date -u '+%Y-%m-%d %H:%M UTC') @@ -49,6 +65,16 @@ jobs: BRANCH_TYPE="Release Candidate" LABEL_TYPE="type: release" TITLE_PREFIX="rc" + elif [[ "$BRANCH" == beta/* ]]; then + VERSION="${BRANCH#beta/}" + BRANCH_TYPE="Beta" + LABEL_TYPE="type: release" + TITLE_PREFIX="beta" + elif [[ "$BRANCH" == alpha/* ]]; then + VERSION="${BRANCH#alpha/}" + BRANCH_TYPE="Alpha" + LABEL_TYPE="type: release" + TITLE_PREFIX="alpha" else VERSION="${BRANCH#dev/}" BRANCH_TYPE="Development" @@ -58,45 +84,124 @@ jobs: TITLE="${TITLE_PREFIX}(${VERSION}): ${BRANCH_TYPE} tracking for ${BRANCH}" - BODY="## ${BRANCH_TYPE} Branch Created - - | Field | Value | - |-------|-------| - | **Branch** | \`${BRANCH}\` | - | **Version** | \`${VERSION}\` | - | **Type** | ${BRANCH_TYPE} | - | **Created by** | @${ACTOR} | - | **Created at** | ${NOW} | - | **Repository** | \`${REPO}\` | - - ## Checklist - - - [ ] Feature development complete - - [ ] Tests passing - - [ ] README.md version bumped to \`${VERSION}\` - - [ ] CHANGELOG.md updated - - [ ] PR created targeting \`main\` - - [ ] Code reviewed and approved - - [ ] Merged to \`main\` - - --- - *Auto-created by [auto-dev-issue.yml](.github/workflows/auto-dev-issue.yml) on branch creation.*" - - # Dedent heredoc - BODY=$(echo "$BODY" | sed 's/^ //') - # Check for existing issue with same title prefix - EXISTING=$(gh api "repos/${REPO}/issues?state=open&per_page=5" \ + EXISTING=$(gh api "repos/${REPO}/issues?state=open&per_page=10" \ --jq ".[] | select(.title | startswith(\"${TITLE_PREFIX}(${VERSION})\")) | .number" 2>/dev/null | head -1) if [ -n "$EXISTING" ]; then echo "ℹ️ Issue #${EXISTING} already exists for ${VERSION}" >> $GITHUB_STEP_SUMMARY - else - ISSUE_URL=$(gh issue create \ - --repo "$REPO" \ - --title "$TITLE" \ - --body "$BODY" \ - --label "${LABEL_TYPE},version" \ - --assignee "jmiller-moko" 2>&1) - echo "βœ… Created tracking issue: ${ISSUE_URL}" >> $GITHUB_STEP_SUMMARY + exit 0 fi + + # ── Define sub-issues for the workflow ───────────────────────── + if [[ "$BRANCH" == rc/* ]]; then + SUB_ISSUES=( + "RC Testing|Verify all features work on rc branch|type: test,release-candidate" + "Regression Testing|Run full regression suite before merge|type: test,release-candidate" + "Version Bump|Bump version in README.md and all headers|type: version,release-candidate" + "Changelog Update|Update CHANGELOG.md with release notes|documentation,release-candidate" + "Merge to Version Branch|Create PR to version/XX|type: release,needs-review" + ) + elif [[ "$BRANCH" == alpha/* ]] || [[ "$BRANCH" == beta/* ]]; then + SUB_ISSUES=( + "Testing|Verify features on ${BRANCH_TYPE} branch|type: test,status: in-progress" + "Bug Fixes|Fix issues found during ${BRANCH_TYPE} testing|type: bug,status: pending" + "Promote to Next Stage|Create PR to promote to next release stage|type: release,needs-review" + ) + else + SUB_ISSUES=( + "Development|Implement feature/fix on dev branch|type: feature,status: in-progress" + "Unit Testing|Write and pass unit tests|type: test,status: pending" + "Code Review|Request and complete code review|needs-review,status: pending" + "Version Bump|Bump version in README.md and all headers|type: version,status: pending" + "Changelog Update|Update CHANGELOG.md with release notes|documentation,status: pending" + "Create RC Branch|Promote dev to rc branch for final testing|type: release,status: pending" + "Merge to Main|Create PR from rc/dev to main|type: release,needs-review,status: pending" + ) + fi + + # ── Create sub-issues first ─────────────────────────────────────── + SUB_LIST="" + SUB_NUMBERS="" + for SUB in "${SUB_ISSUES[@]}"; do + IFS='|' read -r SUB_TITLE SUB_DESC SUB_LABELS <<< "$SUB" + SUB_FULL_TITLE="${TITLE_PREFIX}(${VERSION}): ${SUB_TITLE}" + + SUB_BODY=$(printf '### %s\n\n%s\n\n| Field | Value |\n|-------|-------|\n| **Parent Branch** | `%s` |\n| **Version** | `%s` |\n\n---\n*Sub-issue of the %s tracking issue for `%s`.*' \ + "$SUB_TITLE" "$SUB_DESC" "$BRANCH" "$VERSION" "$BRANCH_TYPE" "$BRANCH") + + SUB_URL=$(gh issue create \ + --repo "$REPO" \ + --title "$SUB_FULL_TITLE" \ + --body "$SUB_BODY" \ + --label "${SUB_LABELS}" \ + --assignee "jmiller-moko" 2>&1) + + SUB_NUM=$(echo "$SUB_URL" | grep -oE '[0-9]+$') + if [ -n "$SUB_NUM" ]; then + SUB_LIST="${SUB_LIST}\n- [ ] ${SUB_TITLE} (#${SUB_NUM})" + SUB_NUMBERS="${SUB_NUMBERS} #${SUB_NUM}" + fi + sleep 0.3 + done + + # ── Create parent tracking issue ────────────────────────────────── + PARENT_BODY=$(printf '## %s Branch Created\n\n| Field | Value |\n|-------|-------|\n| **Branch** | `%s` |\n| **Version** | `%s` |\n| **Type** | %s |\n| **Created by** | @%s |\n| **Created at** | %s |\n| **Repository** | `%s` |\n\n## Workflow Sub-Issues\n\n%b\n\n---\n*Auto-created by [auto-dev-issue.yml](.github/workflows/auto-dev-issue.yml) on branch creation.*' \ + "$BRANCH_TYPE" "$BRANCH" "$VERSION" "$BRANCH_TYPE" "$ACTOR" "$NOW" "$REPO" "$SUB_LIST") + + PARENT_URL=$(gh issue create \ + --repo "$REPO" \ + --title "$TITLE" \ + --body "$PARENT_BODY" \ + --label "${LABEL_TYPE},version" \ + --assignee "jmiller-moko" 2>&1) + + PARENT_NUM=$(echo "$PARENT_URL" | grep -oE '[0-9]+$') + + # ── Link sub-issues back to parent ──────────────────────────────── + if [ -n "$PARENT_NUM" ]; then + for SUB in "${SUB_ISSUES[@]}"; do + IFS='|' read -r SUB_TITLE _ _ <<< "$SUB" + SUB_FULL_TITLE="${TITLE_PREFIX}(${VERSION}): ${SUB_TITLE}" + SUB_NUM=$(gh api "repos/${REPO}/issues?state=open&per_page=20" \ + --jq ".[] | select(.title == \"${SUB_FULL_TITLE}\") | .number" 2>/dev/null | head -1) + if [ -n "$SUB_NUM" ]; then + gh api "repos/${REPO}/issues/${SUB_NUM}" -X PATCH \ + -f body="$(gh api "repos/${REPO}/issues/${SUB_NUM}" --jq '.body' 2>/dev/null) + + > **Parent Issue:** #${PARENT_NUM}" --silent 2>/dev/null || true + fi + sleep 0.2 + done + fi + + # ── Create or update prerelease for alpha/beta/rc ──────────────── + if [[ "$BRANCH" == rc/* ]] || [[ "$BRANCH" == alpha/* ]] || [[ "$BRANCH" == beta/* ]]; then + case "$BRANCH_TYPE" in + Alpha) RELEASE_TAG="alpha" ;; + Beta) RELEASE_TAG="beta" ;; + "Release Candidate") RELEASE_TAG="release-candidate" ;; + esac + + EXISTING=$(gh release view "$RELEASE_TAG" --json tagName -q .tagName 2>/dev/null || true) + if [ -z "$EXISTING" ]; then + gh release create "$RELEASE_TAG" \ + --title "${RELEASE_TAG} (${VERSION})" \ + --notes "## ${BRANCH_TYPE} ${VERSION}\n\nBranch: \`${BRANCH}\`\nTracking issue: ${PARENT_URL}" \ + --prerelease \ + --target main 2>/dev/null || true + echo "${BRANCH_TYPE} release created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY + else + gh release edit "$RELEASE_TAG" \ + --title "${RELEASE_TAG} (${VERSION})" --prerelease 2>/dev/null || true + echo "${BRANCH_TYPE} release updated: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY + fi + fi + + # ── Summary ─────────────────────────────────────────────────────── + echo "## Dev Workflow Issues Created" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Item | Issue |" >> $GITHUB_STEP_SUMMARY + echo "|------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| **Parent** | ${PARENT_URL} |" >> $GITHUB_STEP_SUMMARY + echo "| **Sub-issues** |${SUB_NUMBERS} |" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml index 7af55e4..e5d4c49 100644 --- a/.github/workflows/auto-release.yml +++ b/.github/workflows/auto-release.yml @@ -6,29 +6,31 @@ # DEFGROUP: GitHub.Workflow # INGROUP: MokoStandards.Release # REPO: https://github.com/mokoconsulting-tech/MokoStandards -# PATH: /templates/workflows/shared/auto-release.yml.template -# VERSION: 04.04.01 -# BRIEF: Unified build & release pipeline β€” version branch, platform version, badges, tag, release +# PATH: /templates/workflows/joomla/auto-release.yml.template +# VERSION: 04.06.00 +# BRIEF: Joomla build & release β€” ZIP package, updates.xml, SHA-256 checksum # -# ╔════════════════════════════════════════════════════════════════════════╗ -# β•‘ BUILD & RELEASE PIPELINE β•‘ -# ╠════════════════════════════════════════════════════════════════════════╣ -# β•‘ β•‘ -# β•‘ Triggers on push to main (skips bot commits + [skip ci]): β•‘ -# β•‘ β•‘ -# β•‘ Every push: β•‘ -# β•‘ 1. Read version from README.md β•‘ -# β•‘ 3. Set platform version (Dolibarr $this->version, Joomla )β•‘ -# β•‘ 4. Update [VERSION: XX.YY.ZZ] badges in markdown files β•‘ -# β•‘ 5. Write update.txt / update.xml β•‘ -# β•‘ 6. Create git tag vXX.YY.ZZ β•‘ -# β•‘ 7a. Patch: update existing GitHub Release for this minor β•‘ -# β•‘ β•‘ -# β•‘ Minor releases only (patch == 00): β•‘ -# β•‘ 2. Create/update version/XX.YY branch (patches update in-place) β•‘ -# β•‘ 7b. Create new GitHub Release β•‘ -# β•‘ β•‘ -# β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β• +# +========================================================================+ +# | BUILD & RELEASE PIPELINE (JOOMLA) | +# +========================================================================+ +# | | +# | Triggers on push to main (skips bot commits + [skip ci]): | +# | | +# | Every push: | +# | 1. Read version from README.md | +# | 3. Set platform version (Joomla ) | +# | 4. Update [VERSION: XX.YY.ZZ] badges in markdown files | +# | 5. Write updates.xml (Joomla update server XML) | +# | 6. Create git tag vXX.YY.ZZ | +# | 7a. Patch: update existing GitHub Release for this minor | +# | 8. Build ZIP, upload asset, write SHA-256 to updates.xml | +# | | +# | Every version change: archives main -> version/XX.YY branch | +# | Patch 00 = development (no release). First release = patch 01. | +# | First release only (patch == 01): | +# | 7b. Create new GitHub Release | +# | | +# +========================================================================+ name: Build & Release @@ -37,6 +39,9 @@ on: branches: - main - master + paths: + - 'src/**' + - 'htdocs/**' env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true @@ -64,19 +69,19 @@ jobs: GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' run: | - git clone --depth 1 --branch version/04.04 --quiet \ + git clone --depth 1 --branch version/04 --quiet \ "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ /tmp/mokostandards cd /tmp/mokostandards composer install --no-dev --no-interaction --quiet - # ── STEP 1: Read version ─────────────────────────────────────────── + # -- STEP 1: Read version ----------------------------------------------- - name: "Step 1: Read version from README.md" id: version run: | VERSION=$(php /tmp/mokostandards/api/cli/version_read.php --path . 2>/dev/null) if [ -z "$VERSION" ]; then - echo "⏭️ No VERSION in README.md β€” skipping release" + echo "No VERSION in README.md β€” skipping release" echo "skip=true" >> "$GITHUB_OUTPUT" exit 0 fi @@ -84,24 +89,34 @@ jobs: MINOR=$(echo "$VERSION" | awk -F. '{printf "%s.%s", $1, $2}') PATCH=$(echo "$VERSION" | awk -F. '{print $3}') + MAJOR=$(echo "$VERSION" | awk -F. '{print $1}') + MINOR_NUM=$(echo "$VERSION" | awk -F. '{print $2}') + echo "version=$VERSION" >> "$GITHUB_OUTPUT" - echo "tag=v${VERSION}" >> "$GITHUB_OUTPUT" - echo "branch=version/${MINOR}" >> "$GITHUB_OUTPUT" + echo "branch=version/${MAJOR}" >> "$GITHUB_OUTPUT" echo "minor=$MINOR" >> "$GITHUB_OUTPUT" - echo "skip=false" >> "$GITHUB_OUTPUT" + echo "major=$MAJOR" >> "$GITHUB_OUTPUT" + echo "release_tag=v${MAJOR}" >> "$GITHUB_OUTPUT" if [ "$PATCH" = "00" ]; then - echo "is_minor=true" >> "$GITHUB_OUTPUT" - echo "βœ… Version: $VERSION (minor release β€” full pipeline)" - else + echo "skip=true" >> "$GITHUB_OUTPUT" echo "is_minor=false" >> "$GITHUB_OUTPUT" - echo "βœ… Version: $VERSION (patch β€” platform version + badges only)" + echo "Version: $VERSION (patch 00 = development β€” skipping release)" + else + echo "skip=false" >> "$GITHUB_OUTPUT" + if [ "$PATCH" = "01" ]; then + echo "is_minor=true" >> "$GITHUB_OUTPUT" + echo "Version: $VERSION (first release β€” full pipeline)" + else + echo "is_minor=false" >> "$GITHUB_OUTPUT" + echo "Version: $VERSION (patch β€” platform version + badges only)" + fi fi - name: Check if already released if: steps.version.outputs.skip != 'true' id: check run: | - TAG="${{ steps.version.outputs.tag }}" + TAG="${{ steps.version.outputs.release_tag }}" BRANCH="${{ steps.version.outputs.branch }}" TAG_EXISTS=false @@ -119,102 +134,109 @@ jobs: echo "already_released=false" >> "$GITHUB_OUTPUT" fi - # ── SANITY CHECKS ──────────────────────────────────────────────────── - - name: "Sanity: Platform-specific validation" + # -- SANITY CHECKS ------------------------------------------------------- + - name: "Sanity: Pre-release validation" if: >- steps.version.outputs.skip != 'true' && steps.check.outputs.already_released != 'true' run: | VERSION="${{ steps.version.outputs.version }}" - PLATFORM=$(php /tmp/mokostandards/api/cli/platform_detect.php --path . 2>/dev/null) ERRORS=0 - echo "## πŸ” Pre-Release Sanity Checks" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "Platform: \`${PLATFORM}\`" >> $GITHUB_STEP_SUMMARY + echo "## Pre-Release Sanity Checks (Joomla)" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY + # -- Version drift check (must pass before release) -------- + README_VER=$(grep -oP 'VERSION:\s*\K[\d.]+' README.md 2>/dev/null | head -1) + if [ "$README_VER" != "$VERSION" ]; then + echo "- Version drift: README says \`${README_VER}\` but releasing \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + else + echo "- Version consistent: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + fi + + # Check CHANGELOG version matches + CL_VER=$(grep -oP 'VERSION:\s*\K[\d.]+' CHANGELOG.md 2>/dev/null | head -1) + if [ -n "$CL_VER" ] && [ "$CL_VER" != "$VERSION" ]; then + echo "- CHANGELOG drift: \`${CL_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + fi + + # Check composer.json version if present + if [ -f "composer.json" ]; then + COMP_VER=$(grep -oP '"version"\s*:\s*"\K[^"]+' composer.json 2>/dev/null | head -1) + if [ -n "$COMP_VER" ] && [ "$COMP_VER" != "$VERSION" ]; then + echo "- composer.json drift: \`${COMP_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + fi + fi + # Common checks if [ ! -f "LICENSE" ]; then - echo "❌ Missing LICENSE file" >> $GITHUB_STEP_SUMMARY + echo "- Missing LICENSE file" >> $GITHUB_STEP_SUMMARY ERRORS=$((ERRORS+1)) else - echo "βœ… LICENSE" >> $GITHUB_STEP_SUMMARY + echo "- LICENSE present" >> $GITHUB_STEP_SUMMARY fi - if [ ! -d "src" ]; then - echo "⚠️ No src/ directory" >> $GITHUB_STEP_SUMMARY + if [ ! -d "src" ] && [ ! -d "htdocs" ]; then + echo "- Warning: No src/ or htdocs/ directory" >> $GITHUB_STEP_SUMMARY else - echo "βœ… src/ directory" >> $GITHUB_STEP_SUMMARY + echo "- Source directory present" >> $GITHUB_STEP_SUMMARY fi - # Dolibarr-specific checks - if [ "$PLATFORM" = "crm-module" ]; then - MOD_FILE=$(find src htdocs -path "*/core/modules/mod*.class.php" -print -quit 2>/dev/null) - if [ -z "$MOD_FILE" ]; then - echo "❌ No module descriptor (src/core/modules/mod*.class.php)" >> $GITHUB_STEP_SUMMARY + # -- Joomla: manifest version drift -------- + MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1) + if [ -n "$MANIFEST" ]; then + XML_VER=$(grep -oP '\K[^<]+' "$MANIFEST" 2>/dev/null | head -1) + if [ -n "$XML_VER" ] && [ "$XML_VER" != "$VERSION" ]; then + echo "- Manifest drift: \`${XML_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY ERRORS=$((ERRORS+1)) else - echo "βœ… Module descriptor: \`${MOD_FILE}\`" >> $GITHUB_STEP_SUMMARY - - # Check module number - NUMERO=$(grep -oP '\$this->numero\s*=\s*\K\d+' "$MOD_FILE" 2>/dev/null || echo "0") - if [ "$NUMERO" = "0" ] || [ -z "$NUMERO" ]; then - echo "❌ Module number (\$this->numero) is 0 or not set" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS+1)) - else - echo "βœ… Module number: ${NUMERO}" >> $GITHUB_STEP_SUMMARY - fi - - # Check url_last_version exists - if grep -q 'url_last_version' "$MOD_FILE" 2>/dev/null; then - echo "βœ… url_last_version is set" >> $GITHUB_STEP_SUMMARY - else - echo "⚠️ url_last_version not set β€” update checks won't work" >> $GITHUB_STEP_SUMMARY - fi + echo "- Manifest version: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY fi fi - # Joomla-specific checks - if [ "$PLATFORM" = "waas-component" ]; then - MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1) - if [ -z "$MANIFEST" ]; then - echo "❌ No Joomla XML manifest found" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS+1)) - else - echo "βœ… Manifest: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY + # -- Joomla: XML manifest existence -------- + if [ -z "$MANIFEST" ]; then + echo "- No Joomla XML manifest found" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + else + echo "- Manifest: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY - # Check extension type - TYPE=$(grep -oP ']+type="\K[^"]+' "$MANIFEST" 2>/dev/null) - echo "βœ… Extension type: ${TYPE:-unknown}" >> $GITHUB_STEP_SUMMARY - fi + # -- Joomla: extension type check -------- + TYPE=$(grep -oP ']+type="\K[^"]+' "$MANIFEST" 2>/dev/null) + echo "- Extension type: ${TYPE:-unknown}" >> $GITHUB_STEP_SUMMARY fi echo "" >> $GITHUB_STEP_SUMMARY if [ "$ERRORS" -gt 0 ]; then - echo "**❌ ${ERRORS} error(s) β€” release may be incomplete**" >> $GITHUB_STEP_SUMMARY + echo "**${ERRORS} error(s) β€” release may be incomplete**" >> $GITHUB_STEP_SUMMARY else - echo "**βœ… All sanity checks passed**" >> $GITHUB_STEP_SUMMARY + echo "**All sanity checks passed**" >> $GITHUB_STEP_SUMMARY fi - # ── STEP 2: Create or update version/XX.YY branch ────────────────── - - name: "Step 2: Version branch" - if: >- - steps.version.outputs.skip != 'true' && - steps.check.outputs.already_released != 'true' + # -- STEP 2: Create or update version/XX.YY archive branch --------------- + # Always runs β€” every version change on main archives to version/XX.YY + - name: "Step 2: Version archive branch" + if: steps.check.outputs.already_released != 'true' run: | BRANCH="${{ steps.version.outputs.branch }}" IS_MINOR="${{ steps.version.outputs.is_minor }}" - if [ "$IS_MINOR" = "true" ]; then + PATCH="${{ steps.version.outputs.version }}" + PATCH_NUM=$(echo "$PATCH" | awk -F. '{print $3}') + + # Check if branch exists + if git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then + git push origin HEAD:"$BRANCH" --force + echo "Updated archive branch: ${BRANCH} (patch ${PATCH_NUM})" >> $GITHUB_STEP_SUMMARY + else git checkout -b "$BRANCH" 2>/dev/null || git checkout "$BRANCH" git push origin "$BRANCH" --force - echo "🌿 Created branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY - else - git push origin HEAD:"$BRANCH" --force - echo "πŸ“ Updated branch: ${BRANCH} (patch)" >> $GITHUB_STEP_SUMMARY + echo "Created archive branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY fi - # ── STEP 3: Set platform version ─────────────────────────────────── + # -- STEP 3: Set platform version ---------------------------------------- - name: "Step 3: Set platform version" if: >- steps.version.outputs.skip != 'true' && @@ -224,7 +246,7 @@ jobs: php /tmp/mokostandards/api/cli/version_set_platform.php \ --path . --version "$VERSION" --branch main - # ── STEP 4: Update version badges ────────────────────────────────── + # -- STEP 4: Update version badges ---------------------------------------- - name: "Step 4: Update version badges" if: >- steps.version.outputs.skip != 'true' && @@ -237,107 +259,119 @@ jobs: fi done - # ── STEP 5: Write update files (Dolibarr: update.txt / Joomla: update.xml) - - name: "Step 5: Write update files" + # -- STEP 5: Write updates.xml (Joomla update server) --------------------- + - name: "Step 5: Write updates.xml" if: >- steps.version.outputs.skip != 'true' && steps.check.outputs.already_released != 'true' run: | - PLATFORM=$(php /tmp/mokostandards/api/cli/platform_detect.php --path . 2>/dev/null) VERSION="${{ steps.version.outputs.version }}" REPO="${{ github.repository }}" - if [ "$PLATFORM" = "crm-module" ]; then - printf '%s' "$VERSION" > update.txt - echo "πŸ“¦ update.txt: ${VERSION}" >> $GITHUB_STEP_SUMMARY + # -- Parse extension metadata from XML manifest ---------------- + MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1) + if [ -z "$MANIFEST" ]; then + echo "Warning: No Joomla XML manifest found β€” skipping updates.xml" >> $GITHUB_STEP_SUMMARY + exit 0 fi - if [ "$PLATFORM" = "waas-component" ]; then - # ── Parse extension metadata from XML manifest ────────────── - MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1) - if [ -z "$MANIFEST" ]; then - echo "⚠️ No Joomla XML manifest found β€” skipping update.xml" >> $GITHUB_STEP_SUMMARY - else - EXT_NAME=$(grep -oP '\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || echo "${{ github.event.repository.name }}") - EXT_TYPE=$(grep -oP ']+type="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "component") - EXT_ELEMENT=$(grep -oP '\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || echo "") - EXT_CLIENT=$(grep -oP ']+client="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "") - EXT_FOLDER=$(grep -oP ']+group="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "") - TARGET_PLATFORM=$(grep -oP '' "$MANIFEST" 2>/dev/null | head -1 || echo "") - PHP_MINIMUM=$(grep -oP '\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || echo "") + EXT_NAME=$(grep -oP '\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || echo "${{ github.event.repository.name }}") + EXT_TYPE=$(grep -oP ']+type="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "component") + EXT_ELEMENT=$(grep -oP '\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || echo "") + EXT_CLIENT=$(grep -oP ']+client="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "") + EXT_FOLDER=$(grep -oP ']+group="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "") + TARGET_PLATFORM=$(grep -oP '' "$MANIFEST" 2>/dev/null | head -1 || echo "") + PHP_MINIMUM=$(grep -oP '\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || echo "") - # Derive element from manifest filename if not in XML - if [ -z "$EXT_ELEMENT" ]; then - EXT_ELEMENT=$(basename "$MANIFEST" .xml) - fi - - # Build client tag: plugins and frontend modules need site - CLIENT_TAG="" - if [ -n "$EXT_CLIENT" ]; then - CLIENT_TAG="${EXT_CLIENT}" - elif [ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]; then - CLIENT_TAG="site" - fi - - # Build folder tag for plugins (required for Joomla to match the update) - FOLDER_TAG="" - if [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ]; then - FOLDER_TAG="${EXT_FOLDER}" - fi - - # Build targetplatform (fallback to Joomla 5+6 if not in manifest) - if [ -z "$TARGET_PLATFORM" ]; then - TARGET_PLATFORM=$(printf '' "/") - fi - - # Build php_minimum tag - PHP_TAG="" - if [ -n "$PHP_MINIMUM" ]; then - PHP_TAG="${PHP_MINIMUM}" - fi - - DOWNLOAD_URL="https://github.com/${REPO}/releases/download/v${VERSION}/${EXT_ELEMENT}-${VERSION}.zip" - INFO_URL="https://github.com/${REPO}/releases/tag/v${VERSION}" - - # ── Write update.xml (stable release) ─────────────────────── - { - printf '%s\n' '' - printf '%s\n' '' - printf '%s\n' ' ' - printf '%s\n' " ${EXT_NAME}" - printf '%s\n' " ${EXT_NAME} update" - printf '%s\n' " ${EXT_ELEMENT}" - printf '%s\n' " ${EXT_TYPE}" - printf '%s\n' " ${VERSION}" - [ -n "$CLIENT_TAG" ] && printf '%s\n' " ${CLIENT_TAG}" - [ -n "$FOLDER_TAG" ] && printf '%s\n' " ${FOLDER_TAG}" - printf '%s\n' ' ' - printf '%s\n' ' stable' - printf '%s\n' ' ' - printf '%s\n' " ${INFO_URL}" - printf '%s\n' ' ' - printf '%s\n' " ${DOWNLOAD_URL}" - printf '%s\n' ' ' - printf '%s\n' " ${TARGET_PLATFORM}" - [ -n "$PHP_TAG" ] && printf '%s\n' " ${PHP_TAG}" - printf '%s\n' ' Moko Consulting' - printf '%s\n' ' https://mokoconsulting.tech' - printf '%s\n' ' ' - printf '%s\n' '' - } > update.xml - - echo "πŸ“¦ update.xml: ${VERSION} (stable) β€” ${EXT_TYPE}/${EXT_ELEMENT}" >> $GITHUB_STEP_SUMMARY - fi + # Derive element from manifest filename if not in XML + if [ -z "$EXT_ELEMENT" ]; then + EXT_ELEMENT=$(basename "$MANIFEST" .xml) fi - # ── Commit all changes ───────────────────────────────────────────── + # Build client tag: plugins and frontend modules need site + CLIENT_TAG="" + if [ -n "$EXT_CLIENT" ]; then + CLIENT_TAG="${EXT_CLIENT}" + elif [ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]; then + CLIENT_TAG="site" + fi + + # Build folder tag for plugins (required for Joomla to match the update) + FOLDER_TAG="" + if [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ]; then + FOLDER_TAG="${EXT_FOLDER}" + fi + + # Build targetplatform (fallback to Joomla 5 if not in manifest) + if [ -z "$TARGET_PLATFORM" ]; then + TARGET_PLATFORM=$(printf '' "/") + fi + + # Build php_minimum tag + PHP_TAG="" + if [ -n "$PHP_MINIMUM" ]; then + PHP_TAG="${PHP_MINIMUM}" + fi + + DOWNLOAD_URL="https://github.com/${REPO}/releases/download/v${VERSION}/${EXT_ELEMENT}-${VERSION}.zip" + INFO_URL="https://github.com/${REPO}/releases/tag/v${VERSION}" + + # -- Build stable entry to temp file ───────────────────────── + { + printf '%s\n' ' ' + printf '%s\n' " ${EXT_NAME}" + printf '%s\n' " ${EXT_NAME} update" + printf '%s\n' " ${EXT_ELEMENT}" + printf '%s\n' " ${EXT_TYPE}" + printf '%s\n' " ${VERSION}" + [ -n "$CLIENT_TAG" ] && printf '%s\n' " ${CLIENT_TAG}" + [ -n "$FOLDER_TAG" ] && printf '%s\n' " ${FOLDER_TAG}" + printf '%s\n' ' ' + printf '%s\n' ' stable' + printf '%s\n' ' ' + printf '%s\n' " ${INFO_URL}" + printf '%s\n' ' ' + printf '%s\n' " ${DOWNLOAD_URL}" + printf '%s\n' ' ' + printf '%s\n' " ${TARGET_PLATFORM}" + [ -n "$PHP_TAG" ] && printf '%s\n' " ${PHP_TAG}" + printf '%s\n' ' Moko Consulting' + printf '%s\n' ' https://mokoconsulting.tech' + printf '%s\n' ' ' + } > /tmp/stable_entry.xml + + # -- Write updates.xml preserving dev/rc entries ────────────── + RC_ENTRY="" + DEV_ENTRY="" + if [ -f "updates.xml" ]; then + printf 'import re\n' > /tmp/extract.py + printf 'with open("updates.xml") as f: c = f.read()\n' >> /tmp/extract.py + printf 'import sys; tag = sys.argv[1]\n' >> /tmp/extract.py + printf 'm = re.search(r"( .*?" + re.escape(tag) + r".*?)", c, re.DOTALL)\n' >> /tmp/extract.py + printf 'if m: print(m.group(1))\n' >> /tmp/extract.py + RC_ENTRY=$(python3 /tmp/extract.py rc 2>/dev/null || true) + DEV_ENTRY=$(python3 /tmp/extract.py development 2>/dev/null || true) + fi + + { + printf '%s\n' '' + printf '%s\n' '' + cat /tmp/stable_entry.xml + [ -n "$RC_ENTRY" ] && echo "$RC_ENTRY" + [ -n "$DEV_ENTRY" ] && echo "$DEV_ENTRY" + printf '%s\n' '' + } > updates.xml + + echo "updates.xml: ${VERSION} (stable + rc/dev preserved)" >> $GITHUB_STEP_SUMMARY + + # -- Commit all changes --------------------------------------------------- - name: Commit release changes if: >- steps.version.outputs.skip != 'true' && steps.check.outputs.already_released != 'true' run: | if git diff --quiet && git diff --cached --quiet; then - echo "ℹ️ No changes to commit" + echo "No changes to commit" exit 0 fi VERSION="${{ steps.version.outputs.version }}" @@ -348,18 +382,25 @@ jobs: --author="github-actions[bot] " git push - # ── STEP 6: Create tag ───────────────────────────────────────────── + # -- STEP 6: Create tag --------------------------------------------------- - name: "Step 6: Create git tag" if: >- steps.version.outputs.skip != 'true' && - steps.check.outputs.tag_exists != 'true' + steps.check.outputs.tag_exists != 'true' && + steps.version.outputs.is_minor == 'true' run: | - TAG="${{ steps.version.outputs.tag }}" - git tag "$TAG" - git push origin "$TAG" - echo "🏷️ Tag: ${TAG}" >> $GITHUB_STEP_SUMMARY + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + # Only create the major release tag if it doesn't exist yet + if ! git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then + git tag "$RELEASE_TAG" + git push origin "$RELEASE_TAG" + echo "Tag created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY + else + echo "Tag ${RELEASE_TAG} already exists" >> $GITHUB_STEP_SUMMARY + fi + echo "Tag: ${TAG}" >> $GITHUB_STEP_SUMMARY - # ── STEP 7: Create or update GitHub Release ────────────────────────── + # -- STEP 7: Create or update GitHub Release ------------------------------ - name: "Step 7: GitHub Release" if: >- steps.version.outputs.skip != 'true' && @@ -368,67 +409,129 @@ jobs: GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} run: | VERSION="${{ steps.version.outputs.version }}" - TAG="${{ steps.version.outputs.tag }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" BRANCH="${{ steps.version.outputs.branch }}" - IS_MINOR="${{ steps.version.outputs.is_minor }}" - - # Derive the minor version base (XX.YY.00) - MINOR_BASE=$(echo "$VERSION" | sed 's/\.[0-9]*$/.00/') - MINOR_TAG="v${MINOR_BASE}" + MAJOR="${{ steps.version.outputs.major }}" NOTES=$(php /tmp/mokostandards/api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null) [ -z "$NOTES" ] && NOTES="Release ${VERSION}" echo "$NOTES" > /tmp/release_notes.md - if [ "$IS_MINOR" = "true" ]; then - # Minor release: create new GitHub Release - gh release create "$TAG" \ - --title "${VERSION}" \ + # Check if the major release already exists + EXISTING=$(gh release view "$RELEASE_TAG" --json tagName -q .tagName 2>/dev/null || true) + + if [ -z "$EXISTING" ]; then + # First release for this major + gh release create "$RELEASE_TAG" \ + --title "v${MAJOR} (latest: ${VERSION})" \ --notes-file /tmp/release_notes.md \ --target "$BRANCH" - echo "πŸš€ Release created: ${VERSION}" >> $GITHUB_STEP_SUMMARY + echo "Release created: ${RELEASE_TAG} (${VERSION})" >> $GITHUB_STEP_SUMMARY else - # Patch release: update the existing minor release with new tag - # Find the latest release for this minor version - EXISTING=$(gh release view "$MINOR_TAG" --json tagName -q .tagName 2>/dev/null || true) - if [ -n "$EXISTING" ]; then - # Update existing release body with patch info - CURRENT_NOTES=$(gh release view "$MINOR_TAG" --json body -q .body 2>/dev/null || true) - { - echo "$CURRENT_NOTES" - echo "" - echo "---" - echo "### Patch ${VERSION}" - echo "" - cat /tmp/release_notes.md - } > /tmp/updated_notes.md + # Append version notes to existing major release + CURRENT_NOTES=$(gh release view "$RELEASE_TAG" --json body -q .body 2>/dev/null || true) + { + echo "$CURRENT_NOTES" + echo "" + echo "---" + echo "### ${VERSION}" + echo "" + cat /tmp/release_notes.md + } > /tmp/updated_notes.md - gh release edit "$MINOR_TAG" \ - --title "${MINOR_BASE} (latest: ${VERSION})" \ - --notes-file /tmp/updated_notes.md - echo "πŸ“ Release updated: ${MINOR_BASE} β†’ patch ${VERSION}" >> $GITHUB_STEP_SUMMARY - else - # No existing minor release found β€” create one for this patch - gh release create "$TAG" \ - --title "${VERSION}" \ - --notes-file /tmp/release_notes.md - echo "πŸš€ Release created: ${VERSION} (no minor release found)" >> $GITHUB_STEP_SUMMARY - fi + gh release edit "$RELEASE_TAG" \ + --title "v${MAJOR} (latest: ${VERSION})" \ + --notes-file /tmp/updated_notes.md + echo "Release updated: ${RELEASE_TAG} -> ${VERSION}" >> $GITHUB_STEP_SUMMARY fi - # ── Summary ──────────────────────────────────────────────────────── + # -- STEP 8: Build Joomla install ZIP + SHA-256 checksum ------------------ + # Every patch builds an install-ready ZIP and uploads it to the minor release. + # Result: one Release per minor version with a ZIP for each patch. + - name: "Step 8: Build Joomla package and update checksum" + if: >- + steps.version.outputs.skip != 'true' + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + run: | + VERSION="${{ steps.version.outputs.version }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + REPO="${{ github.repository }}" + + # All ZIPs upload to the major release tag (vXX) + gh release view "$RELEASE_TAG" --json tagName > /dev/null 2>&1 || { + echo "No release ${RELEASE_TAG} found β€” skipping ZIP upload" + exit 0 + } + + # Find extension element name from manifest + MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1 || true) + [ -z "$MANIFEST" ] && exit 0 + + EXT_ELEMENT=$(grep -oP '\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || basename "$MANIFEST" .xml) + PACKAGE_NAME="${EXT_ELEMENT}-${VERSION}.zip" + + # -- Build install-ready ZIP from src/ ---------------------------- + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" + [ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ β€” skipping package"; exit 0; } + + cd "$SOURCE_DIR" + zip -r "/tmp/${PACKAGE_NAME}" . -x '.ftpignore' + cd .. + + FILESIZE=$(stat -c%s "/tmp/${PACKAGE_NAME}" 2>/dev/null || stat -f%z "/tmp/${PACKAGE_NAME}" 2>/dev/null || echo "unknown") + + # -- Calculate SHA-256 ------------------------------------------- + SHA256=$(sha256sum "/tmp/${PACKAGE_NAME}" | cut -d' ' -f1) + + # -- Upload ZIP to the minor release tag ------------------------- + gh release upload "$RELEASE_TAG" "/tmp/${PACKAGE_NAME}" --clobber 2>/dev/null || { + echo "Could not upload with --clobber, retrying..." + gh release upload "$RELEASE_TAG" "/tmp/${PACKAGE_NAME}" 2>/dev/null || true + } + + # -- Update updates.xml with SHA-256 for latest patch ------------- + if [ -f "updates.xml" ]; then + if grep -q '' updates.xml; then + sed -i "s|.*|sha256:${SHA256}|" updates.xml + else + sed -i "s||\n sha256:${SHA256}|" updates.xml + fi + + # Also update the download URL to point to this patch's ZIP + DOWNLOAD_URL="https://github.com/${REPO}/releases/download/${RELEASE_TAG}/${PACKAGE_NAME}" + sed -i "s|]*>[^<]*|${DOWNLOAD_URL}|" updates.xml + + git add updates.xml + git commit -m "chore(release): SHA-256 + download URL for ${VERSION} [skip ci]" \ + --author="github-actions[bot] " || true + git push || true + fi + + echo "### Joomla Package" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Package | \`${PACKAGE_NAME}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Size | ${FILESIZE} bytes |" >> $GITHUB_STEP_SUMMARY + echo "| SHA-256 | \`${SHA256}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Release | \`${RELEASE_TAG}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Download | [${PACKAGE_NAME}](https://github.com/${REPO}/releases/download/${RELEASE_TAG}/${PACKAGE_NAME}) |" >> $GITHUB_STEP_SUMMARY + + # -- Summary -------------------------------------------------------------- - name: Pipeline Summary if: always() run: | VERSION="${{ steps.version.outputs.version }}" if [ "${{ steps.version.outputs.skip }}" = "true" ]; then - echo "## ⏭️ Release Skipped" >> $GITHUB_STEP_SUMMARY + 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 + echo "## Already Released β€” ${VERSION}" >> $GITHUB_STEP_SUMMARY else echo "" >> $GITHUB_STEP_SUMMARY - echo "## βœ… Build & Release Complete" >> $GITHUB_STEP_SUMMARY + echo "## Build & Release Complete (Joomla)" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY echo "|------|--------|" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/auto-update-sha.yml b/.github/workflows/auto-update-sha.yml index 2aa5fb6..bbcfaaf 100644 --- a/.github/workflows/auto-update-sha.yml +++ b/.github/workflows/auto-update-sha.yml @@ -1,147 +1,144 @@ -# Copyright (C) 2026 Moko Consulting -# SPDX-License-Identifier: GPL-3.0-or-later -# FILE INFORMATION -# DEFGROUP: GitHub.Workflow -# INGROUP: MokoCassiopeia.Automation -# REPO: https://github.com/mokoconsulting-tech/MokoCassiopeia -# PATH: /.github/workflows/auto-update-sha.yml -# VERSION: 01.00.00 -# BRIEF: Automatically update SHA-256 hash in updates.xml after release -# NOTE: Ensures updates.xml stays synchronized with release packages - -name: Auto-Update SHA Hash - -on: - release: - types: [published] - workflow_dispatch: - inputs: - tag: - description: 'Release tag to update SHA for (e.g., 03.08.03)' - required: true - type: string - -permissions: - contents: write - -jobs: - update-sha: - name: Update SHA-256 Hash in updates.xml - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - ref: main - - - name: Get release tag - id: tag - run: | - if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then - TAG="${{ inputs.tag }}" - else - TAG="${{ github.event.release.tag_name }}" - fi - echo "tag=${TAG}" >> $GITHUB_OUTPUT - echo "Processing release: ${TAG}" - - - name: Download release package - run: | - TAG="${{ steps.tag.outputs.tag }}" - PACKAGE_NAME="mokocassiopeia-src-${TAG}.zip" - DOWNLOAD_URL="https://github.com/${{ github.repository }}/releases/download/${TAG}/${PACKAGE_NAME}" - - echo "Downloading: ${DOWNLOAD_URL}" - curl -L -o "${PACKAGE_NAME}" "${DOWNLOAD_URL}" - - if [ ! -f "${PACKAGE_NAME}" ]; then - echo "Error: Failed to download package" - exit 1 - fi - - echo "PACKAGE_NAME=${PACKAGE_NAME}" >> $GITHUB_ENV - - - name: Calculate SHA-256 hash - id: sha - run: | - SHA256_HASH=$(sha256sum "${PACKAGE_NAME}" | cut -d' ' -f1) - echo "sha256=${SHA256_HASH}" >> $GITHUB_OUTPUT - echo "SHA-256 Hash: ${SHA256_HASH}" - - - name: Update updates.xml - run: | - TAG="${{ steps.tag.outputs.tag }}" - SHA256="${{ steps.sha.outputs.sha256 }}" - DATE=$(date +%Y-%m-%d) - - # Update version - sed -i "s|.*|${TAG}|" updates.xml - - # Update creation date - sed -i "s|.*|${DATE}|" updates.xml - - # Update download URL - sed -i "s|.*|https://github.com/${{ github.repository }}/releases/download/${TAG}/mokocassiopeia-src-${TAG}.zip|" updates.xml - - # Update or add SHA-256 hash - if grep -q "" updates.xml; then - sed -i "s|.*|sha256:${SHA256}|" updates.xml - else - # Add SHA-256 after downloadurl - sed -i "/<\/downloadurl>/a\ sha256:${SHA256}<\/sha256>" updates.xml - fi - - echo "Updated updates.xml with:" - echo " Version: ${TAG}" - echo " Date: ${DATE}" - echo " SHA-256: ${SHA256}" - - - name: Check for changes - id: changes - run: | - if git diff --quiet updates.xml; then - echo "has_changes=false" >> $GITHUB_OUTPUT - echo "No changes to updates.xml" - else - echo "has_changes=true" >> $GITHUB_OUTPUT - echo "Changes detected in updates.xml" - git diff updates.xml - fi - - - name: Commit and push changes - if: steps.changes.outputs.has_changes == 'true' - run: | - TAG="${{ steps.tag.outputs.tag }}" - - git config --local user.email "github-actions[bot]@users.noreply.github.com" - git config --local user.name "github-actions[bot]" - - git add updates.xml - git commit -m "chore: Update SHA-256 hash for release ${TAG} - -Auto-generated by auto-update-sha workflow -SHA-256: ${{ steps.sha.outputs.sha256 }}" - - git push origin main - - echo "Successfully updated updates.xml with SHA-256 hash for release ${TAG}" - - - name: Summary - if: steps.changes.outputs.has_changes == 'true' - run: | - echo "### SHA-256 Hash Updated Successfully" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "- Release: ${{ steps.tag.outputs.tag }}" >> $GITHUB_STEP_SUMMARY - echo "- SHA-256: \`${{ steps.sha.outputs.sha256 }}\`" >> $GITHUB_STEP_SUMMARY - echo "- File: updates.xml" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "The Joomla update server will now provide the correct package hash." >> $GITHUB_STEP_SUMMARY - - - name: Summary (no changes) - if: steps.changes.outputs.has_changes == 'false' - run: | - echo "### No Updates Needed" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "updates.xml already contains the correct SHA-256 hash for release ${{ steps.tag.outputs.tag }}" >> $GITHUB_STEP_SUMMARY +# Copyright (C) 2026 Moko Consulting +# SPDX-License-Identifier: GPL-3.0-or-later +# FILE INFORMATION +# DEFGROUP: GitHub.Workflow +# INGROUP: MokoCassiopeia.Automation +# REPO: https://github.com/mokoconsulting-tech/MokoCassiopeia +# PATH: /.github/workflows/auto-update-sha.yml +# VERSION: 03.09.03 +# BRIEF: Automatically update SHA-256 hash in updates.xml after release +# NOTE: Ensures updates.xml stays synchronized with release packages + +name: Auto-Update SHA Hash + +on: + release: + types: [published] + workflow_dispatch: + inputs: + tag: + description: 'Release tag to update SHA for (e.g., 03.08.03)' + required: true + type: string + +permissions: + contents: write + +jobs: + update-sha: + name: Update SHA-256 Hash in updates.xml + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: main + + - name: Get release tag + id: tag + run: | + if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then + TAG="${{ inputs.tag }}" + else + TAG="${{ github.event.release.tag_name }}" + fi + echo "tag=${TAG}" >> $GITHUB_OUTPUT + echo "Processing release: ${TAG}" + + - name: Download release package + run: | + TAG="${{ steps.tag.outputs.tag }}" + PACKAGE_NAME="mokocassiopeia-src-${TAG}.zip" + DOWNLOAD_URL="https://github.com/${{ github.repository }}/releases/download/${TAG}/${PACKAGE_NAME}" + + echo "Downloading: ${DOWNLOAD_URL}" + curl -L -o "${PACKAGE_NAME}" "${DOWNLOAD_URL}" + + if [ ! -f "${PACKAGE_NAME}" ]; then + echo "Error: Failed to download package" + exit 1 + fi + + echo "PACKAGE_NAME=${PACKAGE_NAME}" >> $GITHUB_ENV + + - name: Calculate SHA-256 hash + id: sha + run: | + SHA256_HASH=$(sha256sum "${PACKAGE_NAME}" | cut -d' ' -f1) + echo "sha256=${SHA256_HASH}" >> $GITHUB_OUTPUT + echo "SHA-256 Hash: ${SHA256_HASH}" + + - name: Update updates.xml + run: | + TAG="${{ steps.tag.outputs.tag }}" + SHA256="${{ steps.sha.outputs.sha256 }}" + DATE=$(date +%Y-%m-%d) + + # Update version + sed -i "s|.*|${TAG}|" updates.xml + + # Update creation date + sed -i "s|.*|${DATE}|" updates.xml + + # Update download URL + sed -i "s|.*|https://github.com/${{ github.repository }}/releases/download/${TAG}/mokocassiopeia-src-${TAG}.zip|" updates.xml + + # Update or add SHA-256 hash + if grep -q "" updates.xml; then + sed -i "s|.*|sha256:${SHA256}|" updates.xml + else + # Add SHA-256 after downloadurl + sed -i "/<\/downloadurl>/a\ sha256:${SHA256}<\/sha256>" updates.xml + fi + + echo "Updated updates.xml with:" + echo " Version: ${TAG}" + echo " Date: ${DATE}" + echo " SHA-256: ${SHA256}" + + - name: Check for changes + id: changes + run: | + if git diff --quiet updates.xml; then + echo "has_changes=false" >> $GITHUB_OUTPUT + echo "No changes to updates.xml" + else + echo "has_changes=true" >> $GITHUB_OUTPUT + echo "Changes detected in updates.xml" + git diff updates.xml + fi + + - name: Commit and push changes + if: steps.changes.outputs.has_changes == 'true' + run: | + TAG="${{ steps.tag.outputs.tag }}" + + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + + git add updates.xml + git commit -m "chore: Update SHA-256 hash for release ${TAG} - SHA: ${{ steps.sha.outputs.sha256 }}" + + git push origin main + + echo "Successfully updated updates.xml with SHA-256 hash for release ${TAG}" + + - name: Summary + if: steps.changes.outputs.has_changes == 'true' + run: | + echo "### SHA-256 Hash Updated Successfully" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- Release: ${{ steps.tag.outputs.tag }}" >> $GITHUB_STEP_SUMMARY + echo "- SHA-256: \`${{ steps.sha.outputs.sha256 }}\`" >> $GITHUB_STEP_SUMMARY + echo "- File: updates.xml" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "The Joomla update server will now provide the correct package hash." >> $GITHUB_STEP_SUMMARY + + - name: Summary (no changes) + if: steps.changes.outputs.has_changes == 'false' + run: | + echo "### No Updates Needed" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "updates.xml already contains the correct SHA-256 hash for release ${{ steps.tag.outputs.tag }}" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/changelog-validation.yml b/.github/workflows/changelog-validation.yml new file mode 100644 index 0000000..e2ec667 --- /dev/null +++ b/.github/workflows/changelog-validation.yml @@ -0,0 +1,101 @@ +# 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: GitHub.Workflow.Template +# INGROUP: MokoStandards.CI +# REPO: https://github.com/mokoconsulting-tech/MokoStandards +# PATH: /templates/workflows/shared/changelog-validation.yml.template +# VERSION: 04.06.00 +# BRIEF: Validates CHANGELOG.md format and version consistency +# NOTE: Deployed to .github/workflows/changelog-validation.yml in governed repos. + +name: Changelog Validation + +on: + push: + branches: + - main + pull_request: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + validate-changelog: + name: Validate CHANGELOG.md + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check CHANGELOG.md exists + run: | + echo "### Changelog Validation" >> $GITHUB_STEP_SUMMARY + if [ ! -f "CHANGELOG.md" ]; then + echo "CHANGELOG.md not found in repository root." >> $GITHUB_STEP_SUMMARY + exit 1 + fi + echo "CHANGELOG.md exists." >> $GITHUB_STEP_SUMMARY + + - name: Check VERSION header matches README.md + run: | + # Extract version from README.md FILE INFORMATION block + 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 + exit 1 + fi + + # Check that CHANGELOG.md has a matching version header + CHANGELOG_VERSION=$(grep -oP '^\#\#\s*\[\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' CHANGELOG.md | head -1) + if [ -z "$CHANGELOG_VERSION" ]; then + echo "No version header found in CHANGELOG.md (expected \`## [XX.YY.ZZ] - YYYY-MM-DD\`)." >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + if [ "$CHANGELOG_VERSION" != "$README_VERSION" ]; then + echo "CHANGELOG latest version \`${CHANGELOG_VERSION}\` does not match README VERSION \`${README_VERSION}\`." >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + echo "CHANGELOG version \`${CHANGELOG_VERSION}\` matches README VERSION." >> $GITHUB_STEP_SUMMARY + + - name: Validate conventional changelog format + run: | + ERRORS=0 + + # Check that version entries follow ## [XX.YY.ZZ] - YYYY-MM-DD format + while IFS= read -r LINE; do + if ! echo "$LINE" | grep -qP '^\#\#\s*\[[0-9]{2}\.[0-9]{2}\.[0-9]{2}\]\s*-\s*[0-9]{4}-[0-9]{2}-[0-9]{2}'; then + echo "Malformed version header: \`${LINE}\`" >> $GITHUB_STEP_SUMMARY + echo " Expected format: \`## [XX.YY.ZZ] - YYYY-MM-DD\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + done < <(grep -P '^\#\#\s*\[' CHANGELOG.md) + + ENTRY_COUNT=$(grep -cP '^\#\#\s*\[' CHANGELOG.md || echo "0") + if [ "$ENTRY_COUNT" -eq 0 ]; then + echo "No version entries found in CHANGELOG.md." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Found ${ENTRY_COUNT} version entr(ies) in CHANGELOG.md." >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} format issue(s) found.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**Changelog format validation passed.**" >> $GITHUB_STEP_SUMMARY + fi diff --git a/.github/workflows/ci-joomla.yml b/.github/workflows/ci-joomla.yml new file mode 100644 index 0000000..01e1edb --- /dev/null +++ b/.github/workflows/ci-joomla.yml @@ -0,0 +1,391 @@ +# 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: GitHub.Workflow.Template +# INGROUP: MokoStandards.CI +# REPO: https://github.com/mokoconsulting-tech/MokoStandards +# PATH: /templates/workflows/joomla/ci-joomla.yml.template +# VERSION: 04.06.00 +# BRIEF: CI workflow for Joomla extensions β€” lint, validate, test +# NOTE: Deployed to .github/workflows/ci-joomla.yml in governed Joomla extension repos. + +name: Joomla Extension CI + +on: + push: + branches: + - main + - dev/** + - rc/** + - version/** + pull_request: + branches: + - main + - dev/** + - rc/** + 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Setup PHP + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 + with: + php-version: '8.2' + extensions: mbstring, xml, zip, gd, curl, json, simplexml + tools: composer:v2 + coverage: none + + - name: Clone MokoStandards + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + run: | + git clone --depth 1 --branch version/04 --quiet \ + "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ + /tmp/mokostandards + + - name: Install dependencies + env: + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_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, namespace (Joomla 5+) + for TAG in name version author namespace; do + if ! grep -q "<${TAG}>" "$MANIFEST" 2>/dev/null; then + echo "Missing required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Found required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY + fi + done + 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: 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 + + release-readiness: + name: Release Readiness Check + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' && github.base_ref == 'main' + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Setup PHP ${{ matrix.php }} + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 + with: + php-version: ${{ matrix.php }} + extensions: mbstring, xml, zip, gd, curl, json, simplexml + tools: composer:v2 + coverage: none + + - name: Install dependencies + env: + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_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 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 3f50896..1639497 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -9,7 +9,7 @@ # INGROUP: MokoStandards.Security # REPO: https://github.com/mokoconsulting-tech/MokoStandards # PATH: /templates/workflows/generic/codeql-analysis.yml.template -# VERSION: 04.04.01 +# VERSION: 03.09.03 # BRIEF: CodeQL security scanning workflow (generic β€” all repo types) # NOTE: Deployed to .github/workflows/codeql-analysis.yml in governed repos. # CodeQL does not support PHP directly; JavaScript scans JSON/YAML/shell. diff --git a/.github/workflows/deploy-demo.yml b/.github/workflows/deploy-demo.yml index 061e867..7f785bf 100644 --- a/.github/workflows/deploy-demo.yml +++ b/.github/workflows/deploy-demo.yml @@ -22,7 +22,7 @@ # INGROUP: MokoStandards.Deploy # REPO: https://github.com/mokoconsulting-tech/MokoStandards # PATH: /templates/workflows/shared/deploy-demo.yml.template -# VERSION: 04.04.01 +# VERSION: 04.05.00 # BRIEF: SFTP deployment workflow for demo server β€” synced to all governed repos # NOTE: Synced via bulk-repo-sync to .github/workflows/deploy-demo.yml in all governed repos. # Port is resolved in order: DEMO_FTP_PORT variable β†’ :port suffix in DEMO_FTP_HOST β†’ 22. @@ -36,10 +36,9 @@ name: Deploy to Demo Server (SFTP) # Optional org-level variable: DEMO_FTP_PORT (auto-detected from host or defaults to 22) # Optional org/repo variable: DEMO_FTP_SUFFIX β€” when set, appended to DEMO_FTP_PATH to form the # full remote destination: DEMO_FTP_PATH/DEMO_FTP_SUFFIX -# Ignore rules: Place a .ftp_ignore file in the repository root. Each non-empty, -# non-comment line is a regex pattern tested against the relative path -# of each file (e.g. "subdir/file.txt"). The .gitignore is also -# respected automatically. +# Ignore rules: Place a .ftpignore file in the repository root. Each non-empty, +# non-comment line is a glob pattern tested against the relative path +# of each file (e.g. "subdir/file.txt"). The .gitignore is NOT used. # Required org-level secret: DEMO_FTP_KEY (preferred) or DEMO_FTP_PASSWORD # # Access control: only users with admin or maintain role on the repository may deploy. @@ -195,8 +194,8 @@ jobs: env: SOURCE_DIR: ${{ steps.source.outputs.dir }} run: | - # ── Convert a gitignore-style glob line to an ERE pattern ────────────── - ftp_ignore_to_regex() { + # ── Convert a ftpignore-style glob line to an ERE pattern ────────────── + ftpignore_to_regex() { local line="$1" local anchored=false # Strip inline comments and whitespace @@ -226,15 +225,15 @@ jobs: fi } - # ── Read .ftp_ignore (gitignore-style globs) ───────────────────────── + # ── Read .ftpignore (ftpignore-style globs) ───────────────────────── IGNORE_PATTERNS=() IGNORE_SOURCES=() - if [ -f ".ftp_ignore" ]; then + if [ -f ".ftpignore" ]; then while IFS= read -r line; do [[ "$line" =~ ^[[:space:]]*$ || "$line" =~ ^[[:space:]]*# ]] && continue - regex=$(ftp_ignore_to_regex "$line") + regex=$(ftpignore_to_regex "$line") [ -n "$regex" ] && IGNORE_PATTERNS+=("$regex") && IGNORE_SOURCES+=("$line") - done < ".ftp_ignore" + done < ".ftpignore" fi # ── Walk src/ and classify every file ──────────────────────────────── @@ -245,17 +244,11 @@ jobs: SKIP=false for i in "${!IGNORE_PATTERNS[@]}"; do if echo "$rel" | grep -qE "${IGNORE_PATTERNS[$i]}" 2>/dev/null; then - IGNORED_FILES+=("$rel | .ftp_ignore \`${IGNORE_SOURCES[$i]}\`") + IGNORED_FILES+=("$rel | .ftpignore \`${IGNORE_SOURCES[$i]}\`") SKIP=true; break fi done $SKIP && continue - if [ -f ".gitignore" ]; then - git check-ignore -q "$rel" 2>/dev/null && { - IGNORED_FILES+=("$rel | .gitignore") - continue - } || true - fi WILL_UPLOAD+=("$rel") done < <(find "$SOURCE_DIR" -type f -print0 | sort -z) @@ -426,7 +419,7 @@ jobs: GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' run: | - git clone --depth 1 --branch version/04.04 --quiet \ + git clone --depth 1 --branch version/04.05 --quiet \ "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ /tmp/mokostandards cd /tmp/mokostandards @@ -637,8 +630,12 @@ jobs: DEPLOY_ARGS+=(--key-passphrase "$SFTP_PASSWORD") fi - php /tmp/mokostandards/api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}" - # (deploy-sftp.php handles dotfile skipping and .ftp_ignore natively) + PLATFORM=$(php /tmp/mokostandards/api/cli/platform_detect.php --path . 2>/dev/null || true) + if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards/api/deploy/deploy-joomla.php" ]; then + php /tmp/mokostandards/api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}" + else + php /tmp/mokostandards/api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}" + fi # Remove temp files that should never be left behind rm -f /tmp/deploy_key /tmp/sftp-config.json diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml index 644a38b..7019628 100644 --- a/.github/workflows/deploy-dev.yml +++ b/.github/workflows/deploy-dev.yml @@ -22,7 +22,7 @@ # INGROUP: MokoStandards.Deploy # REPO: https://github.com/mokoconsulting-tech/MokoStandards # PATH: /templates/workflows/shared/deploy-dev.yml.template -# VERSION: 04.04.01 +# VERSION: 04.05.00 # BRIEF: SFTP deployment workflow for development server β€” synced to all governed repos # NOTE: Synced via bulk-repo-sync to .github/workflows/deploy-dev.yml in all governed repos. # Port is resolved in order: DEV_FTP_PORT variable β†’ :port suffix in DEV_FTP_HOST β†’ 22. @@ -37,10 +37,9 @@ name: Deploy to Dev Server (SFTP) # Optional org-level variable: DEV_FTP_PORT (auto-detected from host or defaults to 22) # Optional org/repo variable: DEV_FTP_SUFFIX β€” when set, appended to DEV_FTP_PATH to form the # full remote destination: DEV_FTP_PATH/DEV_FTP_SUFFIX -# Ignore rules: Place a .ftp_ignore file in the repository root. Each non-empty, -# non-comment line is a regex pattern tested against the relative path -# of each file (e.g. "subdir/file.txt"). The .gitignore is also -# respected automatically. +# Ignore rules: Place a .ftpignore file in the repository root. Each non-empty, +# non-comment line is a glob pattern tested against the relative path +# of each file (e.g. "subdir/file.txt"). The .gitignore is NOT used. # Required org-level secret: DEV_FTP_KEY (preferred) or DEV_FTP_PASSWORD # # Access control: only users with admin or maintain role on the repository may deploy. @@ -200,8 +199,8 @@ jobs: env: SOURCE_DIR: ${{ steps.source.outputs.dir }} run: | - # ── Convert a gitignore-style glob line to an ERE pattern ────────────── - ftp_ignore_to_regex() { + # ── Convert a ftpignore-style glob line to an ERE pattern ────────────── + ftpignore_to_regex() { local line="$1" local anchored=false # Strip inline comments and whitespace @@ -231,15 +230,15 @@ jobs: fi } - # ── Read .ftp_ignore (gitignore-style globs) ───────────────────────── + # ── Read .ftpignore (ftpignore-style globs) ───────────────────────── IGNORE_PATTERNS=() IGNORE_SOURCES=() - if [ -f ".ftp_ignore" ]; then + if [ -f ".ftpignore" ]; then while IFS= read -r line; do [[ "$line" =~ ^[[:space:]]*$ || "$line" =~ ^[[:space:]]*# ]] && continue - regex=$(ftp_ignore_to_regex "$line") + regex=$(ftpignore_to_regex "$line") [ -n "$regex" ] && IGNORE_PATTERNS+=("$regex") && IGNORE_SOURCES+=("$line") - done < ".ftp_ignore" + done < ".ftpignore" fi # ── Walk src/ and classify every file ──────────────────────────────── @@ -250,17 +249,11 @@ jobs: SKIP=false for i in "${!IGNORE_PATTERNS[@]}"; do if echo "$rel" | grep -qE "${IGNORE_PATTERNS[$i]}" 2>/dev/null; then - IGNORED_FILES+=("$rel | .ftp_ignore \`${IGNORE_SOURCES[$i]}\`") + IGNORED_FILES+=("$rel | .ftpignore \`${IGNORE_SOURCES[$i]}\`") SKIP=true; break fi done $SKIP && continue - if [ -f ".gitignore" ]; then - git check-ignore -q "$rel" 2>/dev/null && { - IGNORED_FILES+=("$rel | .gitignore") - continue - } || true - fi WILL_UPLOAD+=("$rel") done < <(find "$SOURCE_DIR" -type f -print0 | sort -z) @@ -431,7 +424,7 @@ jobs: GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' run: | - git clone --depth 1 --branch version/04.04 --quiet \ + git clone --depth 1 --branch version/04.05 --quiet \ "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ /tmp/mokostandards cd /tmp/mokostandards @@ -582,6 +575,10 @@ jobs: > /tmp/sftp-config.json fi + # Dev deploys skip minified files β€” use unminified sources for debugging + echo "*.min.js" >> .ftpignore + echo "*.min.css" >> .ftpignore + # ── Run deploy-sftp.php from MokoStandards ──────────────────────────── DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json) if [ "$USE_PASSPHRASE" = "true" ]; then @@ -662,74 +659,20 @@ jobs: fi fi - php /tmp/mokostandards/api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}" - # (deploy-sftp.php handles dotfile skipping and .ftp_ignore natively) + # Use Joomla-aware deploy for waas-component (routes files to correct Joomla dirs) + # Use standard SFTP deploy for everything else + PLATFORM=$(php /tmp/mokostandards/api/cli/platform_detect.php --path . 2>/dev/null || true) + if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards/api/deploy/deploy-joomla.php" ]; then + php /tmp/mokostandards/api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}" + else + php /tmp/mokostandards/api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}" + fi + # (both scripts handle dotfile skipping and .ftpignore natively) # Remove temp files that should never be left behind rm -f /tmp/deploy_key /tmp/sftp-config.json - - name: Create or update failure issue - if: failure() - env: - GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} - run: | - REPO="${{ github.repository }}" - RUN_URL="${{ github.server_url }}/${REPO}/actions/runs/${{ github.run_id }}" - ACTOR="${{ github.actor }}" - BRANCH="${{ github.ref_name }}" - EVENT="${{ github.event_name }}" - NOW=$(date -u '+%Y-%m-%d %H:%M:%S UTC') - LABEL="deploy-failure" - - TITLE="fix: Dev deployment failed β€” ${REPO}" - BODY="## Dev Deployment Failed - - A deployment to the dev server failed and requires attention. - - | Field | Value | - |-------|-------| - | **Repository** | \`${REPO}\` | - | **Branch** | \`${BRANCH}\` | - | **Trigger** | ${EVENT} | - | **Actor** | @${ACTOR} | - | **Failed at** | ${NOW} | - | **Run** | [View workflow run](${RUN_URL}) | - - ### Next steps - 1. Review the [workflow run log](${RUN_URL}) for the specific error. - 2. Fix the underlying issue (credentials, SFTP connectivity, permissions). - 3. Re-trigger the deployment via **Actions β†’ Deploy to Dev Server β†’ Run workflow**. - - --- - *Auto-created by deploy-dev.yml β€” close this issue once the deployment is resolved.*" - - # Ensure the label exists (idempotent β€” no-op if already present) - gh label create "$LABEL" \ - --repo "$REPO" \ - --color "CC0000" \ - --description "Automated deploy failure tracking" \ - --force 2>/dev/null || true - - # Look for an existing open deploy-failure issue - EXISTING=$(gh api "repos/${REPO}/issues?labels=${LABEL}&state=all&per_page=1&sort=created&direction=desc" \ - --jq '.[0].number' 2>/dev/null) - - if [ -n "$EXISTING" ] && [ "$EXISTING" != "null" ]; then - gh api "repos/${REPO}/issues/${EXISTING}" \ - -X PATCH \ - -f title="$TITLE" \ - -f body="$BODY" \ - -f state="open" \ - --silent - echo "πŸ“‹ Failure issue #${EXISTING} updated/reopened: ${REPO}" >> "$GITHUB_STEP_SUMMARY" - else - gh issue create \ - --repo "$REPO" \ - --title "$TITLE" \ - --body "$BODY" \ - --label "$LABEL" \ - --assignee "jmiller-moko" \ - | tee -a "$GITHUB_STEP_SUMMARY" - fi + # Dev deploys fail silently β€” no issue creation. + # Demo and RS deploys create failure issues (production-facing). - name: Deployment summary if: always() diff --git a/.github/workflows/deploy-manual.yml b/.github/workflows/deploy-manual.yml new file mode 100644 index 0000000..e127f0e --- /dev/null +++ b/.github/workflows/deploy-manual.yml @@ -0,0 +1,132 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: GitHub.Workflow +# INGROUP: MokoStandards.Deploy +# REPO: https://github.com/mokoconsulting-tech/MokoStandards +# PATH: /templates/workflows/joomla/deploy-manual.yml.template +# VERSION: 04.06.00 +# BRIEF: Manual SFTP deploy to dev server for Joomla repos +# NOTE: Joomla repos use update.xml for distribution. This is for manual +# dev server testing only β€” triggered via workflow_dispatch. + +name: Deploy to Dev (Manual) + +on: + workflow_dispatch: + inputs: + clear_remote: + description: 'Delete all remote files before uploading' + required: false + default: 'false' + type: boolean + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +permissions: + contents: read + +jobs: + deploy: + name: SFTP Deploy to Dev + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Setup PHP + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 + with: + php-version: '8.2' + extensions: json, ssh2 + tools: composer + coverage: none + + - name: Setup MokoStandards tools + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' + run: | + git clone --depth 1 --branch version/04 --quiet \ + "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ + /tmp/mokostandards 2>/dev/null || true + if [ -d "/tmp/mokostandards" ] && [ -f "/tmp/mokostandards/composer.json" ]; then + cd /tmp/mokostandards && composer install --no-dev --no-interaction --quiet 2>/dev/null || true + fi + + - name: Check FTP configuration + id: check + env: + HOST: ${{ vars.DEV_FTP_HOST }} + PATH_VAR: ${{ vars.DEV_FTP_PATH }} + SUFFIX: ${{ vars.DEV_FTP_SUFFIX }} + PORT: ${{ vars.DEV_FTP_PORT }} + run: | + if [ -z "$HOST" ] || [ -z "$PATH_VAR" ]; then + echo "DEV_FTP_HOST or DEV_FTP_PATH not configured β€” cannot deploy" + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "skip=false" >> "$GITHUB_OUTPUT" + echo "host=$HOST" >> "$GITHUB_OUTPUT" + + REMOTE="${PATH_VAR%/}" + [ -n "$SUFFIX" ] && REMOTE="${REMOTE}/${SUFFIX#/}" + echo "remote=$REMOTE" >> "$GITHUB_OUTPUT" + + [ -z "$PORT" ] && PORT="22" + echo "port=$PORT" >> "$GITHUB_OUTPUT" + + - name: Deploy via SFTP + if: steps.check.outputs.skip != 'true' + env: + SFTP_KEY: ${{ secrets.DEV_FTP_KEY }} + SFTP_PASS: ${{ secrets.DEV_FTP_PASSWORD }} + SFTP_USER: ${{ vars.DEV_FTP_USERNAME }} + run: | + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" + [ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ β€” nothing to deploy"; exit 0; } + + printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \ + "${{ steps.check.outputs.host }}" "${{ steps.check.outputs.port }}" "$SFTP_USER" "${{ steps.check.outputs.remote }}" \ + > /tmp/sftp-config.json + + if [ -n "$SFTP_KEY" ]; then + echo "$SFTP_KEY" > /tmp/deploy_key + chmod 600 /tmp/deploy_key + printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json + else + printf ',"password":"%s"}' "$SFTP_PASS" >> /tmp/sftp-config.json + fi + + DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json) + [ "${{ inputs.clear_remote }}" = "true" ] && DEPLOY_ARGS+=(--clear-remote) + + PLATFORM=$(php /tmp/mokostandards/api/cli/platform_detect.php --path . 2>/dev/null || true) + if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards/api/deploy/deploy-joomla.php" ]; then + php /tmp/mokostandards/api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}" + else + php /tmp/mokostandards/api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}" + fi + + rm -f /tmp/deploy_key /tmp/sftp-config.json + + - name: Summary + if: always() + run: | + if [ "${{ steps.check.outputs.skip }}" = "true" ]; then + echo "### Deploy Skipped β€” FTP not configured" >> $GITHUB_STEP_SUMMARY + else + echo "### Manual Dev Deploy Complete" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Host | \`${{ steps.check.outputs.host }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Remote | \`${{ steps.check.outputs.remote }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Clear | ${{ inputs.clear_remote }} |" >> $GITHUB_STEP_SUMMARY + fi diff --git a/.github/workflows/deploy-rs.yml b/.github/workflows/deploy-rs.yml index c6c45ae..bf9a779 100644 --- a/.github/workflows/deploy-rs.yml +++ b/.github/workflows/deploy-rs.yml @@ -22,7 +22,7 @@ # INGROUP: MokoStandards.Deploy # REPO: https://github.com/mokoconsulting-tech/MokoStandards # PATH: /templates/workflows/shared/deploy-rs.yml.template -# VERSION: 04.04.01 +# VERSION: 04.05.00 # BRIEF: SFTP deployment workflow for release staging server β€” synced to all governed repos # NOTE: Synced via bulk-repo-sync to .github/workflows/deploy-rs.yml in all governed repos. # Port is resolved in order: RS_FTP_PORT variable β†’ :port suffix in RS_FTP_HOST β†’ 22. @@ -36,10 +36,9 @@ name: Deploy to RS Server (SFTP) # Optional org-level variable: RS_FTP_PORT (auto-detected from host or defaults to 22) # Optional org/repo variable: RS_FTP_SUFFIX β€” when set, appended to RS_FTP_PATH to form the # full remote destination: RS_FTP_PATH/RS_FTP_SUFFIX -# Ignore rules: Place a .ftp_ignore file in the repository root. Each non-empty, -# non-comment line is a regex pattern tested against the relative path -# of each file (e.g. "subdir/file.txt"). The .gitignore is also -# respected automatically. +# Ignore rules: Place a .ftpignore file in the repository root. Each non-empty, +# non-comment line is a glob pattern tested against the relative path +# of each file (e.g. "subdir/file.txt"). The .gitignore is NOT used. # Required org-level secret: RS_FTP_KEY (preferred) or RS_FTP_PASSWORD # # Access control: only users with admin or maintain role on the repository may deploy. @@ -195,8 +194,8 @@ jobs: env: SOURCE_DIR: ${{ steps.source.outputs.dir }} run: | - # ── Convert a gitignore-style glob line to an ERE pattern ────────────── - ftp_ignore_to_regex() { + # ── Convert a ftpignore-style glob line to an ERE pattern ────────────── + ftpignore_to_regex() { local line="$1" local anchored=false # Strip inline comments and whitespace @@ -226,15 +225,15 @@ jobs: fi } - # ── Read .ftp_ignore (gitignore-style globs) ───────────────────────── + # ── Read .ftpignore (ftpignore-style globs) ───────────────────────── IGNORE_PATTERNS=() IGNORE_SOURCES=() - if [ -f ".ftp_ignore" ]; then + if [ -f ".ftpignore" ]; then while IFS= read -r line; do [[ "$line" =~ ^[[:space:]]*$ || "$line" =~ ^[[:space:]]*# ]] && continue - regex=$(ftp_ignore_to_regex "$line") + regex=$(ftpignore_to_regex "$line") [ -n "$regex" ] && IGNORE_PATTERNS+=("$regex") && IGNORE_SOURCES+=("$line") - done < ".ftp_ignore" + done < ".ftpignore" fi # ── Walk src/ and classify every file ──────────────────────────────── @@ -245,17 +244,11 @@ jobs: SKIP=false for i in "${!IGNORE_PATTERNS[@]}"; do if echo "$rel" | grep -qE "${IGNORE_PATTERNS[$i]}" 2>/dev/null; then - IGNORED_FILES+=("$rel | .ftp_ignore \`${IGNORE_SOURCES[$i]}\`") + IGNORED_FILES+=("$rel | .ftpignore \`${IGNORE_SOURCES[$i]}\`") SKIP=true; break fi done $SKIP && continue - if [ -f ".gitignore" ]; then - git check-ignore -q "$rel" 2>/dev/null && { - IGNORED_FILES+=("$rel | .gitignore") - continue - } || true - fi WILL_UPLOAD+=("$rel") done < <(find "$SOURCE_DIR" -type f -print0 | sort -z) @@ -407,7 +400,7 @@ jobs: GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' run: | - git clone --depth 1 --branch version/04.04 --quiet \ + git clone --depth 1 --branch version/04.05 --quiet \ "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ /tmp/mokostandards cd /tmp/mokostandards @@ -564,8 +557,12 @@ jobs: DEPLOY_ARGS+=(--key-passphrase "$SFTP_PASSWORD") fi - php /tmp/mokostandards/api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}" - # (deploy-sftp.php handles dotfile skipping and .ftp_ignore natively) + PLATFORM=$(php /tmp/mokostandards/api/cli/platform_detect.php --path . 2>/dev/null || true) + if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards/api/deploy/deploy-joomla.php" ]; then + php /tmp/mokostandards/api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}" + else + php /tmp/mokostandards/api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}" + fi # Remove temp files that should never be left behind rm -f /tmp/deploy_key /tmp/sftp-config.json diff --git a/.github/workflows/enterprise-firewall-setup.yml b/.github/workflows/enterprise-firewall-setup.yml index 9465c77..46ef7d2 100644 --- a/.github/workflows/enterprise-firewall-setup.yml +++ b/.github/workflows/enterprise-firewall-setup.yml @@ -22,7 +22,7 @@ # INGROUP: MokoStandards.Firewall # REPO: https://github.com/mokoconsulting-tech/MokoStandards # PATH: /templates/workflows/shared/enterprise-firewall-setup.yml.template -# VERSION: 04.04.01 +# VERSION: 04.06.00 # BRIEF: Enterprise firewall configuration β€” generates outbound allow-rules including SFTP deployment server # NOTE: Reads DEV_FTP_HOST / DEV_FTP_PORT variables to include SFTP egress rules alongside HTTPS rules. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0ba0908..4af1d59 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,7 +22,7 @@ # INGROUP: MokoCassiopeia.Release # REPO: https://github.com/mokoconsulting-tech/MokoCassiopeia # PATH: /.github/workflows/release.yml -# VERSION: 01.00.00 +# VERSION: 03.09.03 # BRIEF: Automated release workflow for MokoCassiopeia Joomla template # NOTE: Creates release packages and publishes to GitHub Releases diff --git a/.github/workflows/repo_health.yml b/.github/workflows/repo_health.yml index d3daece..73308be 100644 --- a/.github/workflows/repo_health.yml +++ b/.github/workflows/repo_health.yml @@ -10,7 +10,7 @@ # INGROUP: MokoStandards.Validation # REPO: https://github.com/mokoconsulting-tech/MokoStandards # PATH: /.github/workflows/repo_health.yml -# VERSION: 04.01.00 +# VERSION: 04.06.00 # BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts. # NOTE: Field is user-managed. # ============================================================================ @@ -29,7 +29,7 @@ on: workflow_dispatch: inputs: profile: - description: Which configuration profile to validate. release checks SFTP variables used by release pipeline. scripts checks baseline script prerequisites. repo runs repository health only. al[...] + description: 'Validation profile: all, release, scripts, or repo' required: true default: all type: choice @@ -39,19 +39,7 @@ on: - scripts - repo pull_request: - paths: - - .github/workflows/** - - scripts/** - - docs/** - - dev/** push: - branches: - - main - paths: - - .github/workflows/** - - scripts/** - - docs/** - - dev/** permissions: contents: read @@ -68,7 +56,7 @@ env: # Repo health policy # Files are listed as-is; directories must end with a trailing slash. - REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.github/workflows/,src/ + REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.github/workflows/ REPO_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore,README.md,docs/ REPO_DISALLOWED_DIRS: REPO_DISALLOWED_FILES: TODO.md,todo.md @@ -82,6 +70,7 @@ env: WORKFLOWS_DIR: .github/workflows SHELLCHECK_PATTERN: '*.sh' SPDX_FILE_GLOBS: '*.sh,*.php,*.js,*.ts,*.css,*.xml,*.yml,*.yaml' + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true jobs: access_check: @@ -173,7 +162,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: fetch-depth: 0 @@ -268,7 +257,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: fetch-depth: 0 @@ -382,7 +371,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: fetch-depth: 0 @@ -412,6 +401,15 @@ jobs: exit 0 fi + # Source directory: src/ or htdocs/ (either is valid) + if [ -d "src" ]; then + SOURCE_DIR="src" + elif [ -d "htdocs" ]; then + SOURCE_DIR="htdocs" + else + missing_required+=("src/ or htdocs/ (source directory required)") + fi + IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}" IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}" IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}" @@ -561,6 +559,73 @@ jobs: } >> "${GITHUB_STEP_SUMMARY}" fi + # ── Joomla-specific checks ─────────────────────────────────────── + joomla_findings=() + + # XML manifest: find any XML file containing tag)") + else + # Check tag exists + if ! grep -qP '' "${MANIFEST}"; then + joomla_findings+=("XML manifest: tag missing") + fi + # Check extension type attribute + if ! grep -qP 'type="(component|module|plugin|library|package|template|language)"' "${MANIFEST}"; then + joomla_findings+=("XML manifest: type attribute missing or invalid") + fi + # Check tag + if ! grep -qP '' "${MANIFEST}"; then + joomla_findings+=("XML manifest: tag missing") + fi + # Check tag + if ! grep -qP '' "${MANIFEST}"; then + joomla_findings+=("XML manifest: tag missing") + fi + # Check for Joomla 5+ + if ! grep -qP ' missing (required for Joomla 5+)") + fi + fi + + # Language files: check for at least one .ini file + 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 + + # updates.xml must exist in root (Joomla update server) + if [ ! -f 'updates.xml' ]; then + joomla_findings+=("updates.xml missing in root (required for Joomla update server)") + fi + + # index.html files for directory listing protection + INDEX_DIRS=("${SOURCE_DIR}" "${SOURCE_DIR}/admin" "${SOURCE_DIR}/site") + for dir in "${INDEX_DIRS[@]}"; do + if [ -d "${dir}" ] && [ ! -f "${dir}/index.html" ]; then + joomla_findings+=("${dir}/index.html missing (directory listing protection)") + fi + done + + if [ "${#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=() diff --git a/.github/workflows/repository-cleanup.yml b/.github/workflows/repository-cleanup.yml index 06b2a2e..6fbf6e4 100644 --- a/.github/workflows/repository-cleanup.yml +++ b/.github/workflows/repository-cleanup.yml @@ -9,7 +9,7 @@ # INGROUP: MokoStandards.Maintenance # REPO: https://github.com/mokoconsulting-tech/MokoStandards # PATH: /templates/workflows/shared/repository-cleanup.yml.template -# VERSION: 04.04.01 +# VERSION: 04.06.00 # BRIEF: Recurring repository maintenance β€” labels, branches, workflows, logs, doc indexes # NOTE: Synced via bulk-repo-sync to .github/workflows/repository-cleanup.yml in all governed repos. # Runs on the 1st and 15th of each month at 6:00 AM UTC, and on manual dispatch. @@ -266,7 +266,7 @@ jobs: GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} run: | REPO="${{ github.repository }}" - CURRENT="chore/sync-mokostandards-v04.04" + CURRENT="chore/sync-mokostandards-v04.05" echo "## 🌿 Branch Cleanup" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/standards-compliance.yml b/.github/workflows/standards-compliance.yml index 0d85ae4..79aaedd 100644 --- a/.github/workflows/standards-compliance.yml +++ b/.github/workflows/standards-compliance.yml @@ -5,7 +5,7 @@ # INGROUP: MokoStandards.Compliance # REPO: https://github.com/mokoconsulting-tech/MokoStandards # PATH: /.github/workflows/standards-compliance.yml -# VERSION: 04.04.01 +# VERSION: 04.06.00 # BRIEF: MokoStandards compliance validation workflow # NOTE: Validates repository structure, documentation, and coding standards @@ -163,7 +163,11 @@ jobs: --include="*.php" --include="*.py" --include="*.js" --include="*.ts" \ --exclude-dir=".git" --exclude-dir="vendor" --exclude-dir="node_modules" 2>/dev/null | \ grep -v -E '(test|example|sample|getenv|getString|getArgument|config\[|/\.\*/|^\s*//|^\s*\*|CREDENTIAL_PATTERNS|SecurityValidator|SECRET_PATTERN|===|!==|ApiClient|str_contains|gen_wrappers)' | \ - grep -v "= ''" | grep -v '= ""' | grep -v '\$this->config' > /tmp/secrets1.txt 2>/dev/null || true + grep -v "= ''" | grep -v '= ""' | grep -v '\$this->config' | \ + grep -v 'type="password"' | grep -v 'type="text"' | grep -v 'name="password"' | grep -v 'name="secretkey"' | \ + grep -v '/dev/null || true + if [ -d "/tmp/mokostandards" ] && [ -f "/tmp/mokostandards/composer.json" ]; then + cd /tmp/mokostandards + composer install --no-dev --no-interaction --quiet 2>/dev/null || true + fi - name: Run Version Consistency Check id: version_check @@ -512,18 +524,15 @@ jobs: echo "## πŸ”’ Version Consistency Validation" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - # Use PHP enterprise library for version consistency check - if [ -f "vendor/bin/moko" ]; then - php vendor/bin/moko check:version -- --path . --verbose 2>&1 | tee /tmp/version-check.log - EXIT_CODE=${PIPESTATUS[0]} - elif [ -f "/tmp/mokostandards/api/validate/check_version_consistency.php" ]; then + # Use MokoStandards tools (no Composer needed on the governed repo) + if [ -f "/tmp/mokostandards/api/validate/check_version_consistency.php" ]; then php /tmp/mokostandards/api/validate/check_version_consistency.php --path . --verbose 2>&1 | tee /tmp/version-check.log EXIT_CODE=${PIPESTATUS[0]} elif [ -f "api/validate/check_version_consistency.php" ]; then php api/validate/check_version_consistency.php --path . --verbose 2>&1 | tee /tmp/version-check.log EXIT_CODE=${PIPESTATUS[0]} else - echo "⏭️ Install mokoconsulting-tech/enterprise via Composer for version checks" >> $GITHUB_STEP_SUMMARY + echo "⏭️ MokoStandards tools not available β€” skipping version check" >> $GITHUB_STEP_SUMMARY exit 0 fi @@ -1960,17 +1969,39 @@ jobs: coverage: none - name: Install API Package - run: composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader env: - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN }}"}}' + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' + run: | + if [ -f "composer.json" ]; then + composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader + else + echo "No composer.json β€” pulling MokoStandards tools" + if [ ! -d "/tmp/mokostandards" ]; then + git clone --depth 1 --branch version/04 --quiet \ + "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ + /tmp/mokostandards 2>/dev/null || true + if [ -f "/tmp/mokostandards/composer.json" ]; then + cd /tmp/mokostandards && composer install --no-dev --no-interaction --quiet 2>/dev/null || true + cd - + fi + fi + fi - name: Check Enterprise Readiness id: enterprise_check run: | echo "" >> $GITHUB_STEP_SUMMARY + SCRIPT="" if [ -f "api/validate/check_enterprise_readiness.php" ]; then - php api/validate/check_enterprise_readiness.php --verbose | tee /tmp/enterprise-check.log + SCRIPT="api/validate/check_enterprise_readiness.php" + elif [ -f "/tmp/mokostandards/api/validate/check_enterprise_readiness.php" ]; then + SCRIPT="/tmp/mokostandards/api/validate/check_enterprise_readiness.php" + fi + + if [ -n "$SCRIPT" ]; then + php "$SCRIPT" --verbose | tee /tmp/enterprise-check.log EXIT_CODE=$? echo "" >> $GITHUB_STEP_SUMMARY @@ -2010,17 +2041,39 @@ jobs: coverage: none - name: Install API Package - run: composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader env: - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN }}"}}' + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' + run: | + if [ -f "composer.json" ]; then + composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader + else + echo "No composer.json β€” pulling MokoStandards tools" + if [ ! -d "/tmp/mokostandards" ]; then + git clone --depth 1 --branch version/04 --quiet \ + "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ + /tmp/mokostandards 2>/dev/null || true + if [ -f "/tmp/mokostandards/composer.json" ]; then + cd /tmp/mokostandards && composer install --no-dev --no-interaction --quiet 2>/dev/null || true + cd - + fi + fi + fi - name: Check Repository Health id: health_check run: | echo "" >> $GITHUB_STEP_SUMMARY + SCRIPT="" if [ -f "api/validate/check_repo_health.php" ]; then - php api/validate/check_repo_health.php --verbose | tee /tmp/health-check.log + SCRIPT="api/validate/check_repo_health.php" + elif [ -f "/tmp/mokostandards/api/validate/check_repo_health.php" ]; then + SCRIPT="/tmp/mokostandards/api/validate/check_repo_health.php" + fi + + if [ -n "$SCRIPT" ]; then + php "$SCRIPT" --verbose | tee /tmp/health-check.log EXIT_CODE=$? echo "" >> $GITHUB_STEP_SUMMARY @@ -2481,8 +2534,8 @@ jobs: echo "" echo "βœ… SUCCESS: Repository is fully MokoStandards compliant" - - name: Create tracking issue for standards violations - if: failure() && github.event_name == 'push' + - name: Create or reopen tracking issue for standards violations + if: failure() env: GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} run: | diff --git a/.github/workflows/sync-version-on-merge.yml b/.github/workflows/sync-version-on-merge.yml index 81a17a0..60715f6 100644 --- a/.github/workflows/sync-version-on-merge.yml +++ b/.github/workflows/sync-version-on-merge.yml @@ -9,7 +9,7 @@ # INGROUP: MokoStandards.Automation # REPO: https://github.com/mokoconsulting-tech/MokoStandards # PATH: /templates/workflows/shared/sync-version-on-merge.yml.template -# VERSION: 04.04.01 +# VERSION: 04.06.00 # BRIEF: Auto-bump patch version on every push to main and propagate to all file headers # NOTE: Synced via bulk-repo-sync to .github/workflows/sync-version-on-merge.yml in all governed repos. # README.md is the single source of truth for the repository version. @@ -58,7 +58,7 @@ jobs: GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' run: | - git clone --depth 1 --branch version/04.04 --quiet \ + git clone --depth 1 --branch version/04 --quiet \ "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ /tmp/mokostandards cd /tmp/mokostandards diff --git a/.github/workflows/update-server.yml b/.github/workflows/update-server.yml new file mode 100644 index 0000000..f0830c2 --- /dev/null +++ b/.github/workflows/update-server.yml @@ -0,0 +1,321 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: GitHub.Workflow +# INGROUP: MokoStandards.Joomla +# REPO: https://github.com/mokoconsulting-tech/MokoStandards +# PATH: /templates/workflows/joomla/update-server.yml.template +# VERSION: 04.06.00 +# BRIEF: Update Joomla update server XML feed with stable/rc/dev entries +# +# Writes updates.xml with multiple entries: +# - stable on push to main (from auto-release) +# - rc on push to rc/** +# - development on push to dev/** +# +# Joomla filters by user's "Minimum Stability" setting. + +name: Update Joomla Update Server XML Feed + +on: + push: + branches: + - 'dev/**' + - 'alpha/**' + - 'beta/**' + - 'rc/**' + paths: + - 'src/**' + - 'htdocs/**' + workflow_dispatch: + inputs: + stability: + description: 'Stability tag' + required: true + default: 'development' + type: choice + options: + - development + - alpha + - beta + - rc + - stable + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +permissions: + contents: write + +jobs: + update-xml: + name: Update updates.xml + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.GH_TOKEN || github.token }} + fetch-depth: 0 + + - name: Setup MokoStandards tools + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' + run: | + git clone --depth 1 --branch version/04 --quiet \ + "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ + /tmp/mokostandards 2>/dev/null || true + if [ -d "/tmp/mokostandards" ] && [ -f "/tmp/mokostandards/composer.json" ]; then + cd /tmp/mokostandards && composer install --no-dev --no-interaction --quiet 2>/dev/null || true + fi + + - name: Generate updates.xml entry + run: | + BRANCH="${{ github.ref_name }}" + REPO="${{ github.repository }}" + VERSION=$(php /tmp/mokostandards/api/cli/version_read.php --path . 2>/dev/null || echo "0.0.0") + + # Auto-bump patch on alpha/beta/rc branches (not dev β€” dev bumps manually) + if [[ "$BRANCH" != dev/* ]]; then + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + BUMPED=$(php /tmp/mokostandards/api/cli/version_bump.php --path . 2>/dev/null || true) + if [ -n "$BUMPED" ]; then + VERSION=$(php /tmp/mokostandards/api/cli/version_read.php --path . 2>/dev/null || echo "$VERSION") + git add -A + git commit -m "chore(version): auto-bump patch ${VERSION} [skip ci]" \ + --author="github-actions[bot] " 2>/dev/null || true + git push 2>/dev/null || true + fi + fi + + # Determine stability from branch or input + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + STABILITY="${{ inputs.stability }}" + elif [[ "$BRANCH" == rc/* ]]; then + STABILITY="rc" + elif [[ "$BRANCH" == beta/* ]]; then + STABILITY="beta" + elif [[ "$BRANCH" == alpha/* ]]; then + STABILITY="alpha" + elif [[ "$BRANCH" == dev/* ]]; then + STABILITY="development" + else + STABILITY="stable" + fi + + # Parse manifest + MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1) + if [ -z "$MANIFEST" ]; then + echo "No Joomla manifest found β€” skipping" + exit 0 + fi + + EXT_NAME=$(grep -oP '\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || echo "${{ github.event.repository.name }}") + EXT_TYPE=$(grep -oP ']+type="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "component") + EXT_ELEMENT=$(grep -oP '\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || basename "$MANIFEST" .xml) + EXT_CLIENT=$(grep -oP ']+client="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "") + EXT_FOLDER=$(grep -oP ']+group="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "") + TARGET_PLATFORM=$(grep -oP '' "$MANIFEST" 2>/dev/null | head -1 || echo "") + PHP_MINIMUM=$(grep -oP '\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || echo "") + + [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(basename "$MANIFEST" .xml) + [ -z "$TARGET_PLATFORM" ] && TARGET_PLATFORM=$(printf '' "/") + + CLIENT_TAG="" + [ -n "$EXT_CLIENT" ] && CLIENT_TAG="${EXT_CLIENT}" + [ -z "$CLIENT_TAG" ] && ([ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]) && CLIENT_TAG="site" + + FOLDER_TAG="" + [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ] && FOLDER_TAG="${EXT_FOLDER}" + + PHP_TAG="" + [ -n "$PHP_MINIMUM" ] && PHP_TAG="${PHP_MINIMUM}" + + # Version suffix for non-stable + DISPLAY_VERSION="$VERSION" + case "$STABILITY" in + development) DISPLAY_VERSION="${VERSION}-dev" ;; + alpha) DISPLAY_VERSION="${VERSION}-alpha" ;; + beta) DISPLAY_VERSION="${VERSION}-beta" ;; + rc) DISPLAY_VERSION="${VERSION}-rc" ;; + esac + + MAJOR=$(echo "$VERSION" | awk -F. '{print $1}') + + # Each stability level has its own release tag + case "$STABILITY" in + development) RELEASE_TAG="development" ;; + alpha) RELEASE_TAG="alpha" ;; + beta) RELEASE_TAG="beta" ;; + rc) RELEASE_TAG="release-candidate" ;; + *) RELEASE_TAG="v${MAJOR}" ;; + esac + + PACKAGE_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.zip" + DOWNLOAD_URL="https://github.com/${REPO}/releases/download/${RELEASE_TAG}/${PACKAGE_NAME}" + INFO_URL="https://github.com/${REPO}" + + # ── Build install-ready ZIP ───────────────────────────────── + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" + if [ -d "$SOURCE_DIR" ]; then + cd "$SOURCE_DIR" + zip -r "/tmp/${PACKAGE_NAME}" . -x '.ftpignore' + cd .. + + SHA256=$(sha256sum "/tmp/${PACKAGE_NAME}" | cut -d' ' -f1) + + # Ensure draft release exists for this major + gh release view "$RELEASE_TAG" --json tagName > /dev/null 2>&1 || \ + gh release create "$RELEASE_TAG" --title "${RELEASE_TAG} (${DISPLAY_VERSION})" --notes "${STABILITY} release" --prerelease --target main 2>/dev/null || true + + # Upload ZIP to the major release + gh release upload "$RELEASE_TAG" "/tmp/${PACKAGE_NAME}" --clobber 2>/dev/null || true + + echo "Package: ${PACKAGE_NAME} (SHA: ${SHA256})" >> $GITHUB_STEP_SUMMARY + else + SHA256="" + fi + + # ── Build the new entry ─────────────────────────────────────── + NEW_ENTRY="" + NEW_ENTRY="${NEW_ENTRY} \n" + NEW_ENTRY="${NEW_ENTRY} ${EXT_NAME}\n" + NEW_ENTRY="${NEW_ENTRY} ${EXT_NAME} (${STABILITY})\n" + NEW_ENTRY="${NEW_ENTRY} ${EXT_ELEMENT}\n" + NEW_ENTRY="${NEW_ENTRY} ${EXT_TYPE}\n" + NEW_ENTRY="${NEW_ENTRY} ${DISPLAY_VERSION}\n" + [ -n "$CLIENT_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${CLIENT_TAG}\n" + [ -n "$FOLDER_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${FOLDER_TAG}\n" + NEW_ENTRY="${NEW_ENTRY} \n" + NEW_ENTRY="${NEW_ENTRY} ${STABILITY}\n" + NEW_ENTRY="${NEW_ENTRY} \n" + NEW_ENTRY="${NEW_ENTRY} ${INFO_URL}\n" + NEW_ENTRY="${NEW_ENTRY} \n" + NEW_ENTRY="${NEW_ENTRY} ${DOWNLOAD_URL}\n" + NEW_ENTRY="${NEW_ENTRY} \n" + [ -n "$SHA256" ] && NEW_ENTRY="${NEW_ENTRY} sha256:${SHA256}\n" + NEW_ENTRY="${NEW_ENTRY} ${TARGET_PLATFORM}\n" + [ -n "$PHP_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${PHP_TAG}\n" + NEW_ENTRY="${NEW_ENTRY} Moko Consulting\n" + NEW_ENTRY="${NEW_ENTRY} https://mokoconsulting.tech\n" + NEW_ENTRY="${NEW_ENTRY} " + + # ── Write new entry to temp file ─────────────────────────────── + printf '%b' "$NEW_ENTRY" > /tmp/new_entry.xml + + # ── Merge into updates.xml ───────────────────────────────────── + if [ ! -f "updates.xml" ]; then + printf '%s\n' '' > updates.xml + printf '%s\n' '' >> updates.xml + cat /tmp/new_entry.xml >> updates.xml + printf '\n%s\n' '' >> updates.xml + else + # Remove existing entry for this stability, insert new one + printf 'import re\nstability = "%s"\n' "${STABILITY}" > /tmp/merge_xml.py + printf 'with open("updates.xml") as f: content = f.read()\n' >> /tmp/merge_xml.py + printf 'with open("/tmp/new_entry.xml") as f: new_entry = f.read()\n' >> /tmp/merge_xml.py + printf 'pattern = r" .*?" + re.escape(stability) + r".*?\\n?"\n' >> /tmp/merge_xml.py + printf 'content = re.sub(pattern, "", content, flags=re.DOTALL)\n' >> /tmp/merge_xml.py + printf 'content = content.replace("", new_entry + "\\n")\n' >> /tmp/merge_xml.py + printf 'content = re.sub(r"\\n{3,}", "\\n\\n", content)\n' >> /tmp/merge_xml.py + printf 'with open("updates.xml", "w") as f: f.write(content)\n' >> /tmp/merge_xml.py + python3 /tmp/merge_xml.py 2>/dev/null || { + # Fallback: rebuild keeping other stability entries + { + printf '%s\n' '' + printf '%s\n' '' + for TAG in stable rc development; do + [ "$TAG" = "${STABILITY}" ] && continue + if grep -q "${TAG}" updates.xml 2>/dev/null; then + sed -n "//,/<\/update>/{ /${TAG}<\/tag>/p; }" updates.xml + fi + done + cat /tmp/new_entry.xml + printf '\n%s\n' '' + } > /tmp/updates_new.xml + mv /tmp/updates_new.xml updates.xml + } + fi + + # Commit + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git add updates.xml + git diff --cached --quiet || { + git commit -m "chore: update updates.xml (${STABILITY}: ${DISPLAY_VERSION}) [skip ci]" \ + --author="github-actions[bot] " + git push + } + + - name: SFTP deploy to dev server + if: contains(github.ref, 'dev/') + env: + DEV_HOST: ${{ vars.DEV_FTP_HOST }} + DEV_PATH: ${{ vars.DEV_FTP_PATH }} + DEV_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }} + DEV_USER: ${{ vars.DEV_FTP_USERNAME }} + DEV_PORT: ${{ vars.DEV_FTP_PORT }} + DEV_KEY: ${{ secrets.DEV_FTP_KEY }} + DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }} + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + run: | + # ── Permission check: admin or maintain role required ────── + ACTOR="${{ github.actor }}" + REPO="${{ github.repository }}" + PERMISSION=$(gh api "repos/${REPO}/collaborators/${ACTOR}/permission" \ + --jq '.permission' 2>/dev/null || \ + gh api "repos/${REPO}/collaborators/${ACTOR}" \ + --jq '.role' 2>/dev/null || echo "read") + case "$PERMISSION" in + admin|maintain|write) ;; + *) + echo "Deploy denied: ${ACTOR} has '${PERMISSION}' β€” requires admin, maintain, or write" + exit 0 + ;; + esac + + [ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured β€” skipping SFTP"; exit 0; } + + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" + [ ! -d "$SOURCE_DIR" ] && exit 0 + + PORT="${DEV_PORT:-22}" + REMOTE="${DEV_PATH%/}" + [ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}" + + printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \ + "$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json + if [ -n "$DEV_KEY" ]; then + echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key + printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json + else + printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json + fi + + PLATFORM=$(php /tmp/mokostandards/api/cli/platform_detect.php --path . 2>/dev/null || true) + if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards/api/deploy/deploy-joomla.php" ]; then + php /tmp/mokostandards/api/deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json + elif [ -f "/tmp/mokostandards/api/deploy/deploy-sftp.php" ]; then + php /tmp/mokostandards/api/deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json + fi + rm -f /tmp/deploy_key /tmp/sftp-config.json + echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY + + - name: Summary + if: always() + run: | + echo "## Joomla Update Server" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Version | \`${DISPLAY_VERSION}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Element | \`${EXT_ELEMENT}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Download | [ZIP](${DOWNLOAD_URL}) |" >> $GITHUB_STEP_SUMMARY diff --git a/.gitignore b/.gitignore index 6284ec5..01449c1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ +# ============================================================ +# Local task tracking (not version controlled) +# ============================================================ +TODO.md + # ============================================================ # Environment and secrets # ============================================================ @@ -23,10 +28,7 @@ secrets/ *.log *.pid *.seed -*.sql -*.sql.gz -*.sqlite -*.sqlite3 + # ============================================================ # OS / Editor / IDE cruft @@ -43,6 +45,7 @@ System Volume Information/ Icon? .idea/ .settings/ +.claude/ .vscode/* !.vscode/tasks.json !.vscode/settings.json.example @@ -70,6 +73,17 @@ todo* # SFTP / sync tools # ============================================================ sftp-config*.json +sftp-config.json.template +sftp-settings.json + +# ============================================================ +# Sublime SFTP / FTP sync +# ============================================================ +*.sublime-project +*.sublime-workspace +*.sublime-settings +.libsass.json +*.ffs* # ============================================================ # Replit / cloud IDE @@ -138,6 +152,7 @@ package-lock.json # PHP / Composer tooling # ============================================================ vendor/ +!src/media/vendor/ composer.lock *.phar codeception.phar @@ -185,727 +200,3 @@ venv/ *.coverage hypothesis/ -# ============================================================ -# Joomla Development -# ============================================================ -# Custom user overrides for Joomla development -user.css -user.js -colors_custom.css - -# Joomla Module Builder auto-generated files -modulebuilder.txt - -# ============================================================ -# Joomla Core -# ============================================================ -/.htaccess -/administrator/cache/* -/administrator/components/com_actionlogs/* -/administrator/components/com_admin/* -/administrator/components/com_ajax/* -/administrator/components/com_associations/* -/administrator/components/com_banners/* -/administrator/components/com_cache/* -/administrator/components/com_categories/* -/administrator/components/com_checkin/* -/administrator/components/com_config/* -/administrator/components/com_contact/* -/administrator/components/com_content/* -/administrator/components/com_contenthistory/* -/administrator/components/com_cpanel/* -/administrator/components/com_fields/* -/administrator/components/com_finder/* -/administrator/components/com_installer/* -/administrator/components/com_joomlaupdate/* -/administrator/components/com_languages/* -/administrator/components/com_login/* -/administrator/components/com_media/* -/administrator/components/com_menus/* -/administrator/components/com_messages/* -/administrator/components/com_modules/* -/administrator/components/com_newsfeeds/* -/administrator/components/com_plugins/* -/administrator/components/com_postinstall/* -/administrator/components/com_privacy/* -/administrator/components/com_redirect/* -/administrator/components/com_search/* -/administrator/components/com_tags/* -/administrator/components/com_templates/* -/administrator/components/com_users/* -/administrator/help/* -/administrator/includes/* -/administrator/index.php -/administrator/language/en-GB/en-GB.com_actionlogs.ini -/administrator/language/en-GB/en-GB.com_actionlogs.sys.ini -/administrator/language/en-GB/en-GB.com_admin.ini -/administrator/language/en-GB/en-GB.com_admin.sys.ini -/administrator/language/en-GB/en-GB.com_ajax.ini -/administrator/language/en-GB/en-GB.com_ajax.sys.ini -/administrator/language/en-GB/en-GB.com_associations.ini -/administrator/language/en-GB/en-GB.com_associations.sys.ini -/administrator/language/en-GB/en-GB.com_banners.ini -/administrator/language/en-GB/en-GB.com_banners.sys.ini -/administrator/language/en-GB/en-GB.com_cache.ini -/administrator/language/en-GB/en-GB.com_cache.sys.ini -/administrator/language/en-GB/en-GB.com_categories.ini -/administrator/language/en-GB/en-GB.com_categories.sys.ini -/administrator/language/en-GB/en-GB.com_checkin.ini -/administrator/language/en-GB/en-GB.com_checkin.sys.ini -/administrator/language/en-GB/en-GB.com_config.ini -/administrator/language/en-GB/en-GB.com_config.sys.ini -/administrator/language/en-GB/en-GB.com_contact.ini -/administrator/language/en-GB/en-GB.com_contact.sys.ini -/administrator/language/en-GB/en-GB.com_content.ini -/administrator/language/en-GB/en-GB.com_content.sys.ini -/administrator/language/en-GB/en-GB.com_contenthistory.ini -/administrator/language/en-GB/en-GB.com_contenthistory.sys.ini -/administrator/language/en-GB/en-GB.com_cpanel.ini -/administrator/language/en-GB/en-GB.com_cpanel.sys.ini -/administrator/language/en-GB/en-GB.com_fields.ini -/administrator/language/en-GB/en-GB.com_fields.sys.ini -/administrator/language/en-GB/en-GB.com_finder.ini -/administrator/language/en-GB/en-GB.com_finder.sys.ini -/administrator/language/en-GB/en-GB.com_installer.ini -/administrator/language/en-GB/en-GB.com_installer.sys.ini -/administrator/language/en-GB/en-GB.com_joomlaupdate.ini -/administrator/language/en-GB/en-GB.com_joomlaupdate.sys.ini -/administrator/language/en-GB/en-GB.com_languages.ini -/administrator/language/en-GB/en-GB.com_languages.sys.ini -/administrator/language/en-GB/en-GB.com_login.ini -/administrator/language/en-GB/en-GB.com_login.sys.ini -/administrator/language/en-GB/en-GB.com_mailto.sys.ini -/administrator/language/en-GB/en-GB.com_media.ini -/administrator/language/en-GB/en-GB.com_media.sys.ini -/administrator/language/en-GB/en-GB.com_menus.ini -/administrator/language/en-GB/en-GB.com_menus.sys.ini -/administrator/language/en-GB/en-GB.com_messages.ini -/administrator/language/en-GB/en-GB.com_messages.sys.ini -/administrator/language/en-GB/en-GB.com_modules.ini -/administrator/language/en-GB/en-GB.com_modules.sys.ini -/administrator/language/en-GB/en-GB.com_newsfeeds.ini -/administrator/language/en-GB/en-GB.com_newsfeeds.sys.ini -/administrator/language/en-GB/en-GB.com_plugins.ini -/administrator/language/en-GB/en-GB.com_plugins.sys.ini -/administrator/language/en-GB/en-GB.com_postinstall.ini -/administrator/language/en-GB/en-GB.com_postinstall.sys.ini -/administrator/language/en-GB/en-GB.com_privacy.ini -/administrator/language/en-GB/en-GB.com_privacy.sys.ini -/administrator/language/en-GB/en-GB.com_redirect.ini -/administrator/language/en-GB/en-GB.com_redirect.sys.ini -/administrator/language/en-GB/en-GB.com_search.ini -/administrator/language/en-GB/en-GB.com_search.sys.ini -/administrator/language/en-GB/en-GB.com_tags.ini -/administrator/language/en-GB/en-GB.com_tags.sys.ini -/administrator/language/en-GB/en-GB.com_templates.ini -/administrator/language/en-GB/en-GB.com_templates.sys.ini -/administrator/language/en-GB/en-GB.com_users.ini -/administrator/language/en-GB/en-GB.com_users.sys.ini -/administrator/language/en-GB/en-GB.com_weblinks.ini -/administrator/language/en-GB/en-GB.com_weblinks.sys.ini -/administrator/language/en-GB/en-GB.com_wrapper.ini -/administrator/language/en-GB/en-GB.com_wrapper.sys.ini -/administrator/language/en-GB/en-GB.ini -/administrator/language/en-GB/en-GB.lib_fof.ini -/administrator/language/en-GB/en-GB.lib_fof.sys.ini -/administrator/language/en-GB/en-GB.lib_joomla.ini -/administrator/language/en-GB/en-GB.lib_phpass.ini -/administrator/language/en-GB/en-GB.lib_phpmailer.ini -/administrator/language/en-GB/en-GB.lib_simplepie.ini -/administrator/language/en-GB/en-GB.localise.php -/administrator/language/en-GB/en-GB.mod_custom.ini -/administrator/language/en-GB/en-GB.mod_custom.sys.ini -/administrator/language/en-GB/en-GB.mod_feed.ini -/administrator/language/en-GB/en-GB.mod_feed.sys.ini -/administrator/language/en-GB/en-GB.mod_latest.ini -/administrator/language/en-GB/en-GB.mod_latest.sys.ini -/administrator/language/en-GB/en-GB.mod_latestactions.ini -/administrator/language/en-GB/en-GB.mod_latestactions.sys.ini -/administrator/language/en-GB/en-GB.mod_logged.ini -/administrator/language/en-GB/en-GB.mod_logged.sys.ini -/administrator/language/en-GB/en-GB.mod_login.ini -/administrator/language/en-GB/en-GB.mod_login.sys.ini -/administrator/language/en-GB/en-GB.mod_menu.ini -/administrator/language/en-GB/en-GB.mod_menu.sys.ini -/administrator/language/en-GB/en-GB.mod_multilangstatus.ini -/administrator/language/en-GB/en-GB.mod_multilangstatus.sys.ini -/administrator/language/en-GB/en-GB.mod_online.ini -/administrator/language/en-GB/en-GB.mod_online.sys.ini -/administrator/language/en-GB/en-GB.mod_popular.ini -/administrator/language/en-GB/en-GB.mod_popular.sys.ini -/administrator/language/en-GB/en-GB.mod_privacy_dashboard.ini -/administrator/language/en-GB/en-GB.mod_privacy_dashboard.sys.ini -/administrator/language/en-GB/en-GB.mod_quickicon.ini -/administrator/language/en-GB/en-GB.mod_quickicon.sys.ini -/administrator/language/en-GB/en-GB.mod_sampledata.ini -/administrator/language/en-GB/en-GB.mod_sampledata.sys.ini -/administrator/language/en-GB/en-GB.mod_stats_admin.ini -/administrator/language/en-GB/en-GB.mod_stats_admin.sys.ini -/administrator/language/en-GB/en-GB.mod_status.ini -/administrator/language/en-GB/en-GB.mod_status.sys.ini -/administrator/language/en-GB/en-GB.mod_submenu.ini -/administrator/language/en-GB/en-GB.mod_submenu.sys.ini -/administrator/language/en-GB/en-GB.mod_title.ini -/administrator/language/en-GB/en-GB.mod_title.sys.ini -/administrator/language/en-GB/en-GB.mod_toolbar.ini -/administrator/language/en-GB/en-GB.mod_toolbar.sys.ini -/administrator/language/en-GB/en-GB.mod_unread.ini -/administrator/language/en-GB/en-GB.mod_unread.sys.ini -/administrator/language/en-GB/en-GB.mod_version.ini -/administrator/language/en-GB/en-GB.mod_version.sys.ini -/administrator/language/en-GB/en-GB.plg_actionlog_joomla.ini -/administrator/language/en-GB/en-GB.plg_actionlog_joomla.sys.ini -/administrator/language/en-GB/en-GB.plg_authentication_cookie.ini -/administrator/language/en-GB/en-GB.plg_authentication_cookie.sys.ini -/administrator/language/en-GB/en-GB.plg_authentication_example.ini -/administrator/language/en-GB/en-GB.plg_authentication_example.sys.ini -/administrator/language/en-GB/en-GB.plg_authentication_gmail.ini -/administrator/language/en-GB/en-GB.plg_authentication_gmail.sys.ini -/administrator/language/en-GB/en-GB.plg_authentication_joomla.ini -/administrator/language/en-GB/en-GB.plg_authentication_joomla.sys.ini -/administrator/language/en-GB/en-GB.plg_authentication_ldap.ini -/administrator/language/en-GB/en-GB.plg_authentication_ldap.sys.ini -/administrator/language/en-GB/en-GB.plg_captcha_recaptcha.ini -/administrator/language/en-GB/en-GB.plg_captcha_recaptcha.sys.ini -/administrator/language/en-GB/en-GB.plg_captcha_recaptcha_invisible.ini -/administrator/language/en-GB/en-GB.plg_captcha_recaptcha_invisible.sys.ini -/administrator/language/en-GB/en-GB.plg_content_confirmconsent.ini -/administrator/language/en-GB/en-GB.plg_content_confirmconsent.sys.ini -/administrator/language/en-GB/en-GB.plg_content_contact.ini -/administrator/language/en-GB/en-GB.plg_content_contact.sys.ini -/administrator/language/en-GB/en-GB.plg_content_emailcloak.ini -/administrator/language/en-GB/en-GB.plg_content_emailcloak.sys.ini -/administrator/language/en-GB/en-GB.plg_content_example.ini -/administrator/language/en-GB/en-GB.plg_content_example.sys.ini -/administrator/language/en-GB/en-GB.plg_content_fields.ini -/administrator/language/en-GB/en-GB.plg_content_fields.sys.ini -/administrator/language/en-GB/en-GB.plg_content_finder.ini -/administrator/language/en-GB/en-GB.plg_content_finder.sys.ini -/administrator/language/en-GB/en-GB.plg_content_geshi.ini -/administrator/language/en-GB/en-GB.plg_content_geshi.sys.ini -/administrator/language/en-GB/en-GB.plg_content_joomla.ini -/administrator/language/en-GB/en-GB.plg_content_joomla.sys.ini -/administrator/language/en-GB/en-GB.plg_content_loadmodule.ini -/administrator/language/en-GB/en-GB.plg_content_loadmodule.sys.ini -/administrator/language/en-GB/en-GB.plg_content_pagebreak.ini -/administrator/language/en-GB/en-GB.plg_content_pagebreak.sys.ini -/administrator/language/en-GB/en-GB.plg_content_pagenavigation.ini -/administrator/language/en-GB/en-GB.plg_content_pagenavigation.sys.ini -/administrator/language/en-GB/en-GB.plg_content_vote.ini -/administrator/language/en-GB/en-GB.plg_content_vote.sys.ini -/administrator/language/en-GB/en-GB.plg_editors_codemirror.ini -/administrator/language/en-GB/en-GB.plg_editors_codemirror.sys.ini -/administrator/language/en-GB/en-GB.plg_editors_none.ini -/administrator/language/en-GB/en-GB.plg_editors_none.sys.ini -/administrator/language/en-GB/en-GB.plg_editors_tinymce.ini -/administrator/language/en-GB/en-GB.plg_editors_tinymce.sys.ini -/administrator/language/en-GB/en-GB.plg_editors-xtd_article.ini -/administrator/language/en-GB/en-GB.plg_editors-xtd_article.sys.ini -/administrator/language/en-GB/en-GB.plg_editors-xtd_contact.ini -/administrator/language/en-GB/en-GB.plg_editors-xtd_contact.sys.ini -/administrator/language/en-GB/en-GB.plg_editors-xtd_fields.ini -/administrator/language/en-GB/en-GB.plg_editors-xtd_fields.sys.ini -/administrator/language/en-GB/en-GB.plg_editors-xtd_image.ini -/administrator/language/en-GB/en-GB.plg_editors-xtd_image.sys.ini -/administrator/language/en-GB/en-GB.plg_editors-xtd_menu.ini -/administrator/language/en-GB/en-GB.plg_editors-xtd_menu.sys.ini -/administrator/language/en-GB/en-GB.plg_editors-xtd_module.ini -/administrator/language/en-GB/en-GB.plg_editors-xtd_module.sys.ini -/administrator/language/en-GB/en-GB.plg_editors-xtd_pagebreak.ini -/administrator/language/en-GB/en-GB.plg_editors-xtd_pagebreak.sys.ini -/administrator/language/en-GB/en-GB.plg_editors-xtd_readmore.ini -/administrator/language/en-GB/en-GB.plg_editors-xtd_readmore.sys.ini -/administrator/language/en-GB/en-GB.plg_extension_example.ini -/administrator/language/en-GB/en-GB.plg_extension_example.sys.ini -/administrator/language/en-GB/en-GB.plg_extension_joomla.ini -/administrator/language/en-GB/en-GB.plg_extension_joomla.sys.ini -/administrator/language/en-GB/en-GB.plg_fields_calendar.ini -/administrator/language/en-GB/en-GB.plg_fields_calendar.sys.ini -/administrator/language/en-GB/en-GB.plg_fields_checkboxes.ini -/administrator/language/en-GB/en-GB.plg_fields_checkboxes.sys.ini -/administrator/language/en-GB/en-GB.plg_fields_color.ini -/administrator/language/en-GB/en-GB.plg_fields_color.sys.ini -/administrator/language/en-GB/en-GB.plg_fields_editor.ini -/administrator/language/en-GB/en-GB.plg_fields_editor.sys.ini -/administrator/language/en-GB/en-GB.plg_fields_imagelist.ini -/administrator/language/en-GB/en-GB.plg_fields_imagelist.sys.ini -/administrator/language/en-GB/en-GB.plg_fields_integer.ini -/administrator/language/en-GB/en-GB.plg_fields_integer.sys.ini -/administrator/language/en-GB/en-GB.plg_fields_list.ini -/administrator/language/en-GB/en-GB.plg_fields_list.sys.ini -/administrator/language/en-GB/en-GB.plg_fields_media.ini -/administrator/language/en-GB/en-GB.plg_fields_media.sys.ini -/administrator/language/en-GB/en-GB.plg_fields_radio.ini -/administrator/language/en-GB/en-GB.plg_fields_radio.sys.ini -/administrator/language/en-GB/en-GB.plg_fields_repeatable.ini -/administrator/language/en-GB/en-GB.plg_fields_repeatable.sys.ini -/administrator/language/en-GB/en-GB.plg_fields_sql.ini -/administrator/language/en-GB/en-GB.plg_fields_sql.sys.ini -/administrator/language/en-GB/en-GB.plg_fields_text.ini -/administrator/language/en-GB/en-GB.plg_fields_text.sys.ini -/administrator/language/en-GB/en-GB.plg_fields_textarea.ini -/administrator/language/en-GB/en-GB.plg_fields_textarea.sys.ini -/administrator/language/en-GB/en-GB.plg_fields_url.ini -/administrator/language/en-GB/en-GB.plg_fields_url.sys.ini -/administrator/language/en-GB/en-GB.plg_fields_user.ini -/administrator/language/en-GB/en-GB.plg_fields_user.sys.ini -/administrator/language/en-GB/en-GB.plg_fields_usergrouplist.ini -/administrator/language/en-GB/en-GB.plg_fields_usergrouplist.sys.ini -/administrator/language/en-GB/en-GB.plg_finder_categories.ini -/administrator/language/en-GB/en-GB.plg_finder_categories.sys.ini -/administrator/language/en-GB/en-GB.plg_finder_contacts.ini -/administrator/language/en-GB/en-GB.plg_finder_contacts.sys.ini -/administrator/language/en-GB/en-GB.plg_finder_content.ini -/administrator/language/en-GB/en-GB.plg_finder_content.sys.ini -/administrator/language/en-GB/en-GB.plg_finder_newsfeeds.ini -/administrator/language/en-GB/en-GB.plg_finder_newsfeeds.sys.ini -/administrator/language/en-GB/en-GB.plg_finder_tags.ini -/administrator/language/en-GB/en-GB.plg_finder_tags.sys.ini -/administrator/language/en-GB/en-GB.plg_installer_folderinstaller.ini -/administrator/language/en-GB/en-GB.plg_installer_folderinstaller.sys.ini -/administrator/language/en-GB/en-GB.plg_installer_packageinstaller.ini -/administrator/language/en-GB/en-GB.plg_installer_packageinstaller.sys.ini -/administrator/language/en-GB/en-GB.plg_installer_urlinstaller.ini -/administrator/language/en-GB/en-GB.plg_installer_urlinstaller.sys.ini -/administrator/language/en-GB/en-GB.plg_privacy_actionlogs.ini -/administrator/language/en-GB/en-GB.plg_privacy_actionlogs.sys.ini -/administrator/language/en-GB/en-GB.plg_privacy_consents.ini -/administrator/language/en-GB/en-GB.plg_privacy_consents.sys.ini -/administrator/language/en-GB/en-GB.plg_privacy_contact.ini -/administrator/language/en-GB/en-GB.plg_privacy_contact.sys.ini -/administrator/language/en-GB/en-GB.plg_privacy_content.ini -/administrator/language/en-GB/en-GB.plg_privacy_content.sys.ini -/administrator/language/en-GB/en-GB.plg_privacy_message.ini -/administrator/language/en-GB/en-GB.plg_privacy_message.sys.ini -/administrator/language/en-GB/en-GB.plg_privacy_user.ini -/administrator/language/en-GB/en-GB.plg_privacy_user.sys.ini -/administrator/language/en-GB/en-GB.plg_quickicon_extensionupdate.ini -/administrator/language/en-GB/en-GB.plg_quickicon_extensionupdate.sys.ini -/administrator/language/en-GB/en-GB.plg_quickicon_joomlaupdate.ini -/administrator/language/en-GB/en-GB.plg_quickicon_joomlaupdate.sys.ini -/administrator/language/en-GB/en-GB.plg_quickicon_phpversioncheck.ini -/administrator/language/en-GB/en-GB.plg_quickicon_phpversioncheck.sys.ini -/administrator/language/en-GB/en-GB.plg_quickicon_privacycheck.ini -/administrator/language/en-GB/en-GB.plg_quickicon_privacycheck.sys.ini -/administrator/language/en-GB/en-GB.plg_sampledata_blog.ini -/administrator/language/en-GB/en-GB.plg_sampledata_blog.sys.ini -/administrator/language/en-GB/en-GB.plg_search_categories.ini -/administrator/language/en-GB/en-GB.plg_search_categories.sys.ini -/administrator/language/en-GB/en-GB.plg_search_contacts.ini -/administrator/language/en-GB/en-GB.plg_search_contacts.sys.ini -/administrator/language/en-GB/en-GB.plg_search_content.ini -/administrator/language/en-GB/en-GB.plg_search_content.sys.ini -/administrator/language/en-GB/en-GB.plg_search_newsfeeds.ini -/administrator/language/en-GB/en-GB.plg_search_newsfeeds.sys.ini -/administrator/language/en-GB/en-GB.plg_search_tags.ini -/administrator/language/en-GB/en-GB.plg_search_tags.sys.ini -/administrator/language/en-GB/en-GB.plg_search_weblinks.ini -/administrator/language/en-GB/en-GB.plg_search_weblinks.sys.ini -/administrator/language/en-GB/en-GB.plg_system_actionlogs.ini -/administrator/language/en-GB/en-GB.plg_system_actionlogs.sys.ini -/administrator/language/en-GB/en-GB.plg_system_cache.ini -/administrator/language/en-GB/en-GB.plg_system_cache.sys.ini -/administrator/language/en-GB/en-GB.plg_system_debug.ini -/administrator/language/en-GB/en-GB.plg_system_debug.sys.ini -/administrator/language/en-GB/en-GB.plg_system_fields.ini -/administrator/language/en-GB/en-GB.plg_system_fields.sys.ini -/administrator/language/en-GB/en-GB.plg_system_highlight.ini -/administrator/language/en-GB/en-GB.plg_system_highlight.sys.ini -/administrator/language/en-GB/en-GB.plg_system_languagecode.ini -/administrator/language/en-GB/en-GB.plg_system_languagecode.sys.ini -/administrator/language/en-GB/en-GB.plg_system_languagefilter.ini -/administrator/language/en-GB/en-GB.plg_system_languagefilter.sys.ini -/administrator/language/en-GB/en-GB.plg_system_log.ini -/administrator/language/en-GB/en-GB.plg_system_log.sys.ini -/administrator/language/en-GB/en-GB.plg_system_logout.ini -/administrator/language/en-GB/en-GB.plg_system_logout.sys.ini -/administrator/language/en-GB/en-GB.plg_system_logrotation.ini -/administrator/language/en-GB/en-GB.plg_system_logrotation.sys.ini -/administrator/language/en-GB/en-GB.plg_system_p3p.ini -/administrator/language/en-GB/en-GB.plg_system_p3p.sys.ini -/administrator/language/en-GB/en-GB.plg_system_privacyconsent.ini -/administrator/language/en-GB/en-GB.plg_system_privacyconsent.sys.ini -/administrator/language/en-GB/en-GB.plg_system_redirect.ini -/administrator/language/en-GB/en-GB.plg_system_redirect.sys.ini -/administrator/language/en-GB/en-GB.plg_system_remember.ini -/administrator/language/en-GB/en-GB.plg_system_remember.sys.ini -/administrator/language/en-GB/en-GB.plg_system_sef.ini -/administrator/language/en-GB/en-GB.plg_system_sef.sys.ini -/administrator/language/en-GB/en-GB.plg_system_sessiongc.ini -/administrator/language/en-GB/en-GB.plg_system_sessiongc.sys.ini -/administrator/language/en-GB/en-GB.plg_system_stats.ini -/administrator/language/en-GB/en-GB.plg_system_stats.sys.ini -/administrator/language/en-GB/en-GB.plg_system_updatenotification.ini -/administrator/language/en-GB/en-GB.plg_system_updatenotification.sys.ini -/administrator/language/en-GB/en-GB.plg_twofactorauth_totp.ini -/administrator/language/en-GB/en-GB.plg_twofactorauth_totp.sys.ini -/administrator/language/en-GB/en-GB.plg_twofactorauth_yubikey.ini -/administrator/language/en-GB/en-GB.plg_twofactorauth_yubikey.sys.ini -/administrator/language/en-GB/en-GB.plg_user_contactcreator.ini -/administrator/language/en-GB/en-GB.plg_user_contactcreator.sys.ini -/administrator/language/en-GB/en-GB.plg_user_example.ini -/administrator/language/en-GB/en-GB.plg_user_example.sys.ini -/administrator/language/en-GB/en-GB.plg_user_joomla.ini -/administrator/language/en-GB/en-GB.plg_user_joomla.sys.ini -/administrator/language/en-GB/en-GB.plg_user_profile.ini -/administrator/language/en-GB/en-GB.plg_user_profile.sys.ini -/administrator/language/en-GB/en-GB.plg_user_terms.ini -/administrator/language/en-GB/en-GB.plg_user_terms.sys.ini -/administrator/language/en-GB/en-GB.xml -/administrator/language/en-GB/index.html -/administrator/language/index.html -/administrator/language/overrides/* -/administrator/logs/* -/administrator/manifests/files/* -/administrator/manifests/libraries/* -/administrator/manifests/packages/* -/administrator/modules/mod_custom/* -/administrator/modules/mod_feed/* -/administrator/modules/mod_latest/* -/administrator/modules/mod_latestactions/* -/administrator/modules/mod_logged/* -/administrator/modules/mod_login/* -/administrator/modules/mod_menu/* -/administrator/modules/mod_multilangstatus/* -/administrator/modules/mod_online/* -/administrator/modules/mod_popular/* -/administrator/modules/mod_privacy_dashboard/* -/administrator/modules/mod_quickicon/* -/administrator/modules/mod_sampledata/* -/administrator/modules/mod_stats_admin/* -/administrator/modules/mod_status/* -/administrator/modules/mod_submenu/* -/administrator/modules/mod_title/* -/administrator/modules/mod_toolbar/* -/administrator/modules/mod_unread/* -/administrator/modules/mod_version/* -/administrator/templates/hathor/* -/administrator/templates/isis/* -/administrator/templates/system/* -/bin/* -/cache/* -/cli/* -/components/com_ajax/* -/components/com_banners/* -/components/com_config/* -/components/com_contact/* -/components/com_content/* -/components/com_contenthistory/* -/components/com_fields/* -/components/com_finder/* -/components/com_mailto/* -/components/com_media/* -/components/com_newsfeeds/* -/components/com_privacy/* -/components/com_search/* -/components/com_tags/* -/components/com_users/* -/components/com_weblinks/* -/components/com_wrapper/* -/images/banners/* -/images/headers/* -/images/joomla* -/images/sampledata/* -/images/index.html -/includes/* -/installation/* -/language/en-GB/en-GB.com_ajax.ini -/language/en-GB/en-GB.com_config.ini -/language/en-GB/en-GB.com_contact.ini -/language/en-GB/en-GB.com_content.ini -/language/en-GB/en-GB.com_finder.ini -/language/en-GB/en-GB.com_mailto.ini -/language/en-GB/en-GB.com_media.ini -/language/en-GB/en-GB.com_messages.ini -/language/en-GB/en-GB.com_newsfeeds.ini -/language/en-GB/en-GB.com_privacy.ini -/language/en-GB/en-GB.com_search.ini -/language/en-GB/en-GB.com_tags.ini -/language/en-GB/en-GB.com_users.ini -/language/en-GB/en-GB.com_weblinks.ini -/language/en-GB/en-GB.com_wrapper.ini -/language/en-GB/en-GB.files_joomla.sys.ini -/language/en-GB/en-GB.ini -/language/en-GB/en-GB.lib_fof.ini -/language/en-GB/en-GB.lib_fof.sys.ini -/language/en-GB/en-GB.lib_idna_convert.sys.ini -/language/en-GB/en-GB.lib_joomla.ini -/language/en-GB/en-GB.lib_joomla.sys.ini -/language/en-GB/en-GB.lib_phpmailer.sys.ini -/language/en-GB/en-GB.lib_phputf8.sys.ini -/language/en-GB/en-GB.lib_simplepie.sys.ini -/language/en-GB/en-GB.localise.php -/language/en-GB/en-GB.mod_articles_archive.ini -/language/en-GB/en-GB.mod_articles_archive.sys.ini -/language/en-GB/en-GB.mod_articles_categories.ini -/language/en-GB/en-GB.mod_articles_categories.sys.ini -/language/en-GB/en-GB.mod_articles_category.ini -/language/en-GB/en-GB.mod_articles_category.sys.ini -/language/en-GB/en-GB.mod_articles_latest.ini -/language/en-GB/en-GB.mod_articles_latest.sys.ini -/language/en-GB/en-GB.mod_articles_news.ini -/language/en-GB/en-GB.mod_articles_news.sys.ini -/language/en-GB/en-GB.mod_articles_popular.ini -/language/en-GB/en-GB.mod_articles_popular.sys.ini -/language/en-GB/en-GB.mod_banners.ini -/language/en-GB/en-GB.mod_banners.sys.ini -/language/en-GB/en-GB.mod_breadcrumbs.ini -/language/en-GB/en-GB.mod_breadcrumbs.sys.ini -/language/en-GB/en-GB.mod_custom.ini -/language/en-GB/en-GB.mod_custom.sys.ini -/language/en-GB/en-GB.mod_feed.ini -/language/en-GB/en-GB.mod_feed.sys.ini -/language/en-GB/en-GB.mod_finder.ini -/language/en-GB/en-GB.mod_finder.sys.ini -/language/en-GB/en-GB.mod_footer.ini -/language/en-GB/en-GB.mod_footer.sys.ini -/language/en-GB/en-GB.mod_languages.ini -/language/en-GB/en-GB.mod_languages.sys.ini -/language/en-GB/en-GB.mod_login.ini -/language/en-GB/en-GB.mod_login.sys.ini -/language/en-GB/en-GB.mod_menu.ini -/language/en-GB/en-GB.mod_menu.sys.ini -/language/en-GB/en-GB.mod_random_image.ini -/language/en-GB/en-GB.mod_random_image.sys.ini -/language/en-GB/en-GB.mod_related_items.ini -/language/en-GB/en-GB.mod_related_items.sys.ini -/language/en-GB/en-GB.mod_search.ini -/language/en-GB/en-GB.mod_search.sys.ini -/language/en-GB/en-GB.mod_stats.ini -/language/en-GB/en-GB.mod_stats.sys.ini -/language/en-GB/en-GB.mod_syndicate.ini -/language/en-GB/en-GB.mod_syndicate.sys.ini -/language/en-GB/en-GB.mod_tags_popular.ini -/language/en-GB/en-GB.mod_tags_popular.sys.ini -/language/en-GB/en-GB.mod_tags_similar.ini -/language/en-GB/en-GB.mod_tags_similar.sys.ini -/language/en-GB/en-GB.mod_users_latest.ini -/language/en-GB/en-GB.mod_users_latest.sys.ini -/language/en-GB/en-GB.mod_weblinks.ini -/language/en-GB/en-GB.mod_weblinks.sys.ini -/language/en-GB/en-GB.mod_whosonline.ini -/language/en-GB/en-GB.mod_whosonline.sys.ini -/language/en-GB/en-GB.mod_wrapper.ini -/language/en-GB/en-GB.mod_wrapper.sys.ini -/language/en-GB/en-GB.tpl_atomic.ini -/language/en-GB/en-GB.tpl_atomic.sys.ini -/language/en-GB/en-GB.tpl_beez3.ini -/language/en-GB/en-GB.tpl_beez3.sys.ini -/language/en-GB/en-GB.tpl_beez5.ini -/language/en-GB/en-GB.tpl_beez5.sys.ini -/language/en-GB/en-GB.tpl_beez_20.ini -/language/en-GB/en-GB.tpl_beez_20.sys.ini -/language/en-GB/en-GB.tpl_protostar.ini -/language/en-GB/en-GB.tpl_protostar.sys.ini -/language/en-GB/en-GB.xml -/language/en-GB/index.html -/language/index.html -/language/overrides/* -/layouts/joomla/* -/libraries/cms/* -/libraries/fof/* -/libraries/idna_convert/* -/libraries/joomla/* -/libraries/legacy/* -/libraries/php-encryption/* -/libraries/phpass/* -/libraries/phpmailer/* -/libraries/phputf8/* -/libraries/simplepie/* -/libraries/vendor/* -/libraries/classmap.php -/libraries/cms.php -/libraries/import.legacy.php -/libraries/import.php -/libraries/index.html -/libraries/loader.php -/media/cms/* -/media/com_associations/* -/media/com_contact/* -/media/com_content/* -/media/com_contenthistory/* -/media/com_fields/* -/media/com_finder/* -/media/com_joomlaupdate/* -/media/com_mailto/* -/media/com_media/* -/media/com_menus/* -/media/com_modules/* -/media/com_newsfeeds/* -/media/com_privacy/* -/media/com_tags/* -/media/com_weblinks/* -/media/com_wrapper/* -/media/contacts/* -/media/editors/* -/media/jui/* -/media/mailto/* -/media/media/* -/media/mod_languages/* -/media/overrider/* -/media/plg_captcha_recaptcha/* -/media/plg_captcha_recaptcha_invisible/* -/media/plg_editors-xtd_article/* -/media/plg_quickicon_extensionupdate/* -/media/plg_quickicon_joomlaupdate/* -/media/plg_quickicon_privacycheck/* -/media/plg_system_debug/* -/media/plg_system_highlight/* -/media/plg_system_stats/* -/media/plg_twofactorauth_totp/* -/media/system/* -/modules/mod_articles_archive/* -/modules/mod_articles_categories/* -/modules/mod_articles_category/* -/modules/mod_articles_latest/* -/modules/mod_articles_news/* -/modules/mod_articles_popular/* -/modules/mod_banners/* -/modules/mod_breadcrumbs/* -/modules/mod_custom/* -/modules/mod_feed/* -/modules/mod_finder/* -/modules/mod_footer/* -/modules/mod_languages/* -/modules/mod_login/* -/modules/mod_menu/* -/modules/mod_random_image/* -/modules/mod_related_items/* -/modules/mod_search/* -/modules/mod_stats/* -/modules/mod_syndicate/* -/modules/mod_tags_popular/* -/modules/mod_tags_similar/* -/modules/mod_users_latest/* -/modules/mod_weblinks/* -/modules/mod_whosonline/* -/modules/mod_wrapper/* -/plugins/actionlog/joomla/* -/plugins/authentication/cookie/* -/plugins/authentication/example/* -/plugins/authentication/gmail/* -/plugins/authentication/joomla/* -/plugins/authentication/ldap/* -/plugins/captcha/recaptcha/* -/plugins/captcha/recaptcha_invisible/* -/plugins/content/confirmconsent/* -/plugins/content/contact/* -/plugins/content/emailcloak/* -/plugins/content/example/* -/plugins/content/fields/* -/plugins/content/finder/* -/plugins/content/geshi/* -/plugins/content/joomla/* -/plugins/content/loadmodule/* -/plugins/content/pagebreak/* -/plugins/content/pagenavigation/* -/plugins/content/vote/* -/plugins/editors/codemirror/* -/plugins/editors/none/* -/plugins/editors/tinymce/* -/plugins/editors-xtd/article/* -/plugins/editors-xtd/contact/* -/plugins/editors-xtd/fields/* -/plugins/editors-xtd/image/* -/plugins/editors-xtd/menu/* -/plugins/editors-xtd/module/* -/plugins/editors-xtd/pagebreak/* -/plugins/editors-xtd/readmore/* -/plugins/extension/example/* -/plugins/extension/joomla/* -/plugins/fields/calendar/* -/plugins/fields/checkboxes/* -/plugins/fields/color/* -/plugins/fields/editor/* -/plugins/fields/imagelist/* -/plugins/fields/integer/* -/plugins/fields/list/* -/plugins/fields/media/* -/plugins/fields/radio/* -/plugins/fields/repeatable/* -/plugins/fields/sql/* -/plugins/fields/text/* -/plugins/fields/textarea/* -/plugins/fields/url/* -/plugins/fields/user/* -/plugins/fields/usergrouplist/* -/plugins/finder/categories/* -/plugins/finder/contacts/* -/plugins/finder/content/* -/plugins/finder/newsfeeds/* -/plugins/finder/tags/* -/plugins/installer/folderinstaller/* -/plugins/installer/packageinstaller/* -/plugins/installer/urlinstaller/* -/plugins/privacy/actionlogs/* -/plugins/privacy/consents/* -/plugins/privacy/contact/* -/plugins/privacy/content/* -/plugins/privacy/message/* -/plugins/privacy/user/* -/plugins/quickicon/extensionupdate/* -/plugins/quickicon/joomlaupdate/* -/plugins/quickicon/phpversioncheck/* -/plugins/quickicon/privacycheck/* -/plugins/quickicon/index.html -/plugins/sampledata/blog/* -/plugins/search/categories/* -/plugins/search/contacts/* -/plugins/search/content/* -/plugins/search/newsfeeds/* -/plugins/search/tags/* -/plugins/search/weblinks/* -/plugins/search/index.html -/plugins/system/actionlogs/* -/plugins/system/cache/* -/plugins/system/debug/* -/plugins/system/fields/* -/plugins/system/highlight/* -/plugins/system/languagecode/* -/plugins/system/languagefilter/* -/plugins/system/log/* -/plugins/system/logout/* -/plugins/system/logrotation/* -/plugins/system/p3p/* -/plugins/system/privacyconsent/* -/plugins/system/redirect/* -/plugins/system/remember/* -/plugins/system/sef/* -/plugins/system/sessiongc/* -/plugins/system/stats/* -/plugins/system/updatenotification/* -/plugins/system/index.html -/plugins/twofactorauth/totp/* -/plugins/twofactorauth/yubikey/* -/plugins/user/contactcreator/* -/plugins/user/example/* -/plugins/user/joomla/* -/plugins/user/profile/* -/plugins/user/terms/* -/plugins/user/index.html -/plugins/user/index.html -/plugins/index.html -/templates/beez3/* -/templates/protostar/* -/templates/system/* -/templates/index.html -/tmp/* -/configuration.php -/htaccess.txt -/index.php -/joomla.xml -/LICENSE.txt -/README.txt -/robots.txt.dist -/web.config.txt - -# ============================================================ -# Keep-empty folders helper -# ============================================================ -!.gitkeep - -# ── MokoStandards sync (auto-appended) ──────────────────────────────── -/.claude - -# ── MokoStandards sync (auto-appended) ──────────────────────────────── -!src/media/vendor/ diff --git a/.mokostandards b/.mokostandards new file mode 100644 index 0000000..623a1d9 --- /dev/null +++ b/.mokostandards @@ -0,0 +1,20 @@ +# Copyright (C) 2026 Moko Consulting +# SPDX-License-Identifier: GPL-3.0-or-later +# FILE INFORMATION +# DEFGROUP: MokoStandards.Templates.Config +# INGROUP: MokoStandards.Templates +# REPO: https://github.com/mokoconsulting-tech/MokoStandards +# PATH: /templates/configs/moko-standards.yml +# VERSION: 04.04.01 +# BRIEF: Governance attachment template β€” synced to .mokostandards in every governed repository +# NOTE: Tokens replaced at sync time: mokoconsulting-tech, MokoCassiopeia, waas-component, 04.04.00 +# +# This file is managed automatically by MokoStandards bulk sync. +# Do not edit manually β€” changes will be overwritten on the next sync. +# To update governance settings, open a PR in MokoStandards instead: +# https://github.com/mokoconsulting-tech/MokoStandards + +standards_source: "https://github.com/mokoconsulting-tech/MokoStandards" +standards_version: "04.04.00" +platform: "waas-component" +governed_repo: "mokoconsulting-tech/MokoCassiopeia" diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index b0b170d..9e68438 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,98 +1,87 @@ - +# Code of Conduct -## Code of Conduct +## 1. Purpose -This Code of Conduct establishes expectations for behavior within the MokoCassiopeia project community. The objective is to maintain a professional, inclusive, and respectful environment aligned with open source governance best practices. +The purpose of this Code of Conduct is to ensure a safe, inclusive, and respectful environment for all contributors and participants in Moko Consulting projects. This applies to all interactions, whether in repositories, issue trackers, documentation, meetings, or community spaces. -## Scope +## 2. Our Standards -This Code of Conduct applies to all project spaces, including: +Participants are expected to uphold behaviors that strengthen our community, including: -* GitHub repositories, issues, pull requests, discussions, and security advisories. -* Project documentation, workflows, and release processes. -* Any communication channels officially associated with the project. + Demonstrating empathy and respect toward others. + Being inclusive of diverse viewpoints and backgrounds. + Gracefully accepting constructive feedback. + Prioritizing collaboration over conflict. + Showing professionalism in all interactions. -## Our Standards +### Unacceptable behavior includes: -Participants are expected to: + Harassment, discrimination, or derogatory comments. + Threatening or violent language or actions. + Disruptive, aggressive, or intentionally harmful behavior. + Publishing others’ private information without permission. + Any behavior that violates applicable laws. -* Communicate professionally and respectfully. -* Provide constructive feedback focused on technical merit and project objectives. -* Respect differing viewpoints, experience levels, and backgrounds. -* Follow documented contribution, security, and governance policies. +## 3. Responsibilities of Maintainers -Unacceptable behavior includes: +Maintainers are responsible for: -* Harassment, discrimination, or exclusionary conduct. -* Personal attacks, insults, or inflammatory comments. -* Publishing private information without consent. -* Disruptive behavior that materially interferes with project operations. + Clarifying acceptable behavior. + Taking appropriate corrective action when unacceptable behavior occurs. + Removing, editing, or rejecting contributions that violate this Code. + Temporarily or permanently banning contributors who engage in repeated or severe violations. -## Enforcement Responsibilities +## 4. Scope -Project maintainers are responsible for: +This Code applies to: -* Clarifying standards when questions arise. -* Taking appropriate and proportionate corrective action when violations occur. -* Maintaining confidentiality to the extent practical during investigations. + All Moko Consulting repositories. + All documentation and collaboration platforms. + Public and private communication related to project activities. + Any representation of Moko Consulting in online or offline spaces. -## Reporting +## 5. Enforcement -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported through: +Instances of misconduct may be reported to: +**[hello@mokoconsulting.tech](mailto:hello@mokoconsulting.tech)** -* Email: `hello@mokoconsulting.tech` with subject `CODE OF CONDUCT: MokoCassiopeia`. +All reports will be reviewed and investigated promptly and fairly. Maintainers are obligated to maintain confidentiality where possible. -Reports should include relevant context, links, screenshots, or other supporting information. +Consequences may include: -## Enforcement Guidelines + A warning. + Required training or mediation. + Temporary or permanent bans. + Escalation to legal authorities when required. -Corrective actions may include, but are not limited to: +## 6. Acknowledgements -* Private warning or request for corrective action. -* Temporary or permanent restriction from project participation. -* Removal of content that violates this Code of Conduct. +This Code of Conduct is inspired by widely adopted community guidelines, including the Contributor Covenant and major open-source collaboration standards. -Decisions are made based on impact, severity, and pattern of behavior. +## 7. Related Documents -## No Retaliation + [Governance Guide](./docs-governance.md) + [Contributor Guide](./docs-contributing.md) + [Documentation Index](./docs-index.md) -Retaliation against individuals who report concerns in good faith is not tolerated. Any retaliatory behavior will be treated as a separate violation. - -## Jurisdiction - -This project is managed from Tennessee, USA. This statement is informational and does not constitute legal advice. - ---- - -## Metadata - -* **Document:** CODE_OF_CONDUCT.md -* **Repository:** [https://github.com/mokoconsulting-tech/MokoCassiopeia](https://github.com/mokoconsulting-tech/MokoCassiopeia) -* **Path:** /CODE_OF_CONDUCT.md -* **Owner:** Moko Consulting -* **Version:** 03.06.00 -* **Status:** Active -* **Effective Date:** 2025-12-18 -* **Last Reviewed:** 2025-12-18 - -## Revision History - -| Date | Change Summary | Author | -| ---------- | ----------------------------------------------------------------------------- | --------------- | -| 2025-12-18 | Initial publication of contributor conduct standards and enforcement process. | Moko Consulting | +This Code of Conduct is a living document and may be updated following the established Change Management process. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1b8f124..9f6464c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,145 +1,128 @@ + VERSION: 04.04.00 + PATH: ./CONTRIBUTING.md + BRIEF: How to contribute; branch strategy, commit conventions, PR workflow, and release pipeline + --> -## Contributing +# Contributing -This document defines how to contribute to the MokoCassiopeia project. The goal is to ensure changes are reviewable, auditable, and aligned with project governance and release processes. +Thank you for your interest in contributing to **MokoCassiopeia**! -## Scope +This repository is governed by **[MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards)** β€” the authoritative source of coding standards, workflows, and policies for all Moko Consulting repositories. -These guidelines apply to all contributions, including: +## Branch Strategy -* Source code changes -* Documentation updates -* Bug reports and enhancement proposals +| Branch | Purpose | Deploys To | +|--------|---------|------------| +| `main` | Bleeding edge β€” all development merges here | CI only | +| `dev/XX.YY.ZZ` | Feature development | Dev server (version: "development") | +| `version/XX.YY` | Stable frozen snapshot | Demo + RS servers | -## Prerequisites +### Development Workflow -Contributors are expected to: - -* Have a working understanding of Joomla template structure. -* Be familiar with Git and GitHub pull request workflows. -* Review repository governance documents prior to submitting changes. -* Set up the development environment using the provided tools. - -### Quick Setup - -For first-time contributors: - -```bash -# Clone the repository -git clone https://github.com/mokoconsulting-tech/MokoCassiopeia.git -cd MokoCassiopeia +``` +1. Create branch: git checkout -b dev/XX.YY.ZZ/my-feature +2. Develop + test (dev server auto-deploys on push) +3. Open PR β†’ main (squash merge only) +4. Auto-release (version branch + tag + GitHub Release created automatically) ``` -See [docs/QUICK_START.md](./docs/QUICK_START.md) for detailed setup instructions. +### Branch Naming -## Development Tools +| Prefix | Use | +|--------|-----| +| `dev/XX.YY.ZZ` | Feature development (e.g., `dev/02.00.00/add-extrafields`) | +| `version/XX.YY` | Stable release (auto-created, never manually pushed) | +| `chore/` | Automated sync branches (managed by MokoStandards) | -The repository provides several tools to streamline development: +> **Never use** `feature/`, `hotfix/`, or `release/` prefixes β€” they are not part of the MokoStandards branch strategy. -* **Pre-commit Hooks**: Automatic local validation before commits +## Commit Conventions -## Contribution Workflow +Use [conventional commits](https://www.conventionalcommits.org/): -1. Fork the repository. -2. Create a branch from the active development branch. -3. Make focused, minimal changes that address a single concern. -4. Submit a pull request with a clear description of intent and impact. +``` +feat(scope): add new extrafield for invoice tracking +fix(sql): correct column type in llx_mytable +docs(readme): update installation instructions +chore(deps): bump enterprise library to 04.02.30 +``` -Direct commits to protected branches are not permitted. +**Valid types:** `feat` | `fix` | `docs` | `chore` | `ci` | `refactor` | `style` | `test` | `perf` | `revert` | `build` -## Branching and Versioning +## Pull Request Workflow -* Development work occurs on designated development branches. -* Releases are produced from versioned branches following repository standards. -* Contributors should not bump version numbers unless explicitly requested. +1. **Branch** from `main` using `dev/XX.YY.ZZ/description` format +2. **Bump** the patch version in `README.md` before opening the PR +3. **Title** must be a valid conventional commit subject line +4. **Target** `main` β€” squash merge only (merge commits are disabled) +5. **CI checks** must pass before merge -## Coding and Formatting Standards +### What Happens on Merge -All contributions must: +When your PR is merged to `main`, these workflows run automatically: -* Follow Joomla coding standards where applicable. -* Conform to Moko Consulting repository standards for headers, metadata, and file structure. -* Avoid introducing tabs, inconsistent path separators, or non portable assumptions. +1. **sync-version-on-merge** β€” auto-bumps patch version, propagates to all file headers +2. **auto-release** β€” creates `version/XX.YY` branch, git tag, and GitHub Release +3. **deploy-demo / deploy-rs** β€” deploys to demo and RS servers (if `src/**` changed) -Automated checks may reject changes that do not meet these requirements. +## Coding Standards -## Documentation Standards +All contributions must follow [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards): -Documentation changes must: +| Standard | Reference | +|----------|-----------| +| Coding Style | [coding-style-guide.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/coding-style-guide.md) | +| File Headers | [file-header-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/file-header-standards.md) | +| Branching | [branch-release-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/branch-release-strategy.md) | +| Merge Strategy | [merge-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/merge-strategy.md) | +| Scripting | [scripting-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/scripting-standards.md) | +| Build & Release | [build-release.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/workflows/build-release.md) | -* Include required metadata and revision history sections. -* Avoid embedding version numbers in revision history tables. -* Preserve existing structure unless a structural change is explicitly proposed. +## PR Checklist -## Commit Messages +- [ ] Branch named `dev/XX.YY.ZZ/description` +- [ ] Patch version bumped in `README.md` +- [ ] Conventional commit format for PR title +- [ ] All new files have FILE INFORMATION headers +- [ ] `declare(strict_types=1)` in all PHP files +- [ ] PHPDoc on all public methods +- [ ] Tests pass +- [ ] CHANGELOG.md updated +- [ ] No secrets, tokens, or credentials committed -Commit messages should: +## Custom Workflows -* Be concise and descriptive. -* Focus on what changed and why. -* Avoid referencing internal issue trackers unless required. +Place repo-specific workflows in `.github/workflows/custom/` β€” they are **never overwritten or deleted** by MokoStandards sync: -## Reporting Issues - -Bug reports and enhancement requests should be filed as GitHub issues and include: - -* Clear reproduction steps or use cases. -* Expected versus actual behavior. -* Relevant environment details. - -Security related issues must follow the process defined in SECURITY.md and must not be reported publicly. - -## Review Process - -All pull requests are subject to review. Review criteria include: - -* Technical correctness -* Alignment with project goals -* Maintainability and clarity -* Risk introduced to release and update processes - -Maintainers may request changes prior to approval. +``` +.github/workflows/ +β”œβ”€β”€ deploy-dev.yml ← Synced from MokoStandards +β”œβ”€β”€ auto-release.yml ← Synced from MokoStandards +└── custom/ ← Your custom workflows (safe) + └── my-custom-ci.yml +``` ## License -By contributing, you agree that your contributions will be licensed under GPL-3.0-or-later, consistent with the rest of the project. - -## Code of Conduct - -Participation in this project is governed by the Code of Conduct. Unacceptable behavior may result in contribution restrictions. +By contributing, you agree that your contributions will be licensed under the [GPL-3.0-or-later](LICENSE) license. --- -## Metadata - -* **Document:** CONTRIBUTING.md -* **Repository:** [https://github.com/mokoconsulting-tech/MokoCassiopeia](https://github.com/mokoconsulting-tech/MokoCassiopeia) -* **Path:** /CONTRIBUTING.md -* **Owner:** Moko Consulting -* **Version:** 03.06.00 -* **Status:** Active -* **Effective Date:** 2025-12-18 -* **Last Reviewed:** 2025-12-18 - -## Revision History - -| Date | Change Summary | Author | -| ---------- | ------------------------------------------------------------------------- | --------------- | -| 2025-12-18 | Initial publication of contribution guidelines and workflow expectations. | Moko Consulting | +*This file is synced from [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards). Do not edit directly β€” changes will be overwritten on the next sync.* diff --git a/README.md b/README.md index 7f9b559..b60174e 100644 --- a/README.md +++ b/README.md @@ -13,13 +13,19 @@ BRIEF: Documentation for MokoCassiopeia template --> + +[![Version](https://img.shields.io/badge/version-03.06.10-blue.svg?logo=v&logoColor=white)](https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/tag/v03) +[![License](https://img.shields.io/badge/license-GPL--3.0--or--later-green.svg?logo=gnu&logoColor=white)](LICENSE) +[![Joomla](https://img.shields.io/badge/Joomla-5.x%20%7C%206.x-red.svg?logo=joomla&logoColor=white)](https://www.joomla.org) +[![PHP](https://img.shields.io/badge/PHP-8.1%2B-777BB4.svg?logo=php&logoColor=white)](https://www.php.net) + # README - MokoCassiopeia (VERSION: 03.06.03) **A Modern, Lightweight Joomla Template Based on Cassiopeia** -[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) -[![Joomla](https://img.shields.io/badge/Joomla-4.4.x%20%7C%205.x-blue.svg)](https://www.joomla.org) -[![PHP](https://img.shields.io/badge/PHP-8.0%2B-blue.svg)](https://www.php.net) +: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) +](https://img.shields.io/badge/Joomla-4.4.x%20%7C%205.x-blue.svg)](https://www.joomla.org) +](https://img.shields.io/badge/PHP-8.0%2B-blue.svg)](https://www.php.net) MokoCassiopeia is a modern, lightweight enhancement layer built on top of Joomla's Cassiopeia template. It adds **Font Awesome 7**, **Bootstrap 5** helpers, an automatic **Table of Contents (TOC)** utility, advanced **Dark Mode** theming, and optional integrations for **Google Tag Manager** and **Google Analytics (GA4)**β€”all while maintaining minimal core template overrides for maximum upgrade compatibility. diff --git a/SECURITY.md b/SECURITY.md index 9c2b528..5a7fba6 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,185 +1,240 @@ -## Security Policy +# Security Policy -This document defines how MokoCassiopeia handles vulnerability intake, triage, remediation, and disclosure. The objective is to reduce risk, protect downstream users, and preserve operational continuity with a verifiable audit trail. +## Purpose and Scope -## Scope - -This policy applies to: - -* Repository source code, workflows, scripts, and build artifacts. -* Release packaging (ZIP outputs) generated from the repository. -* Configuration and metadata used for distribution (for example manifests and update metadata). - -Out of scope: - -* Vulnerabilities in upstream Joomla core, third party extensions, or external infrastructure not controlled by this repository. -* Issues that require physical access to a host, compromised administrator credentials, or a compromised hosting provider, unless the repository materially increases impact. +This document defines the security vulnerability reporting, response, and disclosure policy for [PROJECT_NAME] and all repositories governed by these standards. It establishes the authoritative process for responsible disclosure, assessment, remediation, and communication of security issues. ## Supported Versions -Security fixes are prioritized for: +Security updates are provided for the following versions: -* The latest released version. -* The current development line when it is actively used for release engineering. +| Version | Supported | +| ------- | ------------------ | +| [X.x.x] | :white_check_mark: | +| < [X.0] | :x: | -Backports may be provided based on impact, deployment footprint, and engineering capacity. +Only the current major version receives security updates. Users should upgrade to the latest supported version to receive security patches. ## Reporting a Vulnerability -Use one of the following channels: +### Where to Report -* GitHub Security Advisories (preferred): use the repository security tab to submit a private report. -* Email: send details to `hello@mokoconsulting.tech` with subject `SECURITY: MokoCassiopeia vulnerability report`. +**DO NOT** create public GitHub issues for security vulnerabilities. -Do not file a public GitHub issue for suspected security vulnerabilities. +Report security vulnerabilities privately to: -### What to include +**Email**: `security@[DOMAIN]` -Provide enough detail to reproduce and triage: +**Subject Line**: `[SECURITY] Brief Description` -* A clear description of the vulnerability and expected impact. -* A minimal proof of concept or reproduction steps. -* Affected versions, configuration assumptions, and environment details. -* Any proposed mitigation or patch. -* Your preferred contact details for follow up. +### What to Include -## Triage and Response Targets +A complete vulnerability report should include: -The project operates with response targets aligned to practical delivery realities: +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 -* **Acknowledgement:** within 3 business days. -* **Initial triage:** within 10 business days. -* **Fix plan:** communicated once severity is confirmed. +### Response Timeline -These targets are not guarantees. Complex issues, supply chain considerations, and coordination with upstream vendors may extend timelines. +* **Initial Response**: Within 3 business days +* **Assessment Complete**: Within 7 business days +* **Fix Timeline**: Depends on severity (see below) +* **Disclosure**: Coordinated with reporter -## Severity Assessment +## Severity Classification -Issues are triaged based on business impact and technical exploitability, including: +Vulnerabilities are classified using the following severity levels: -* Remote exploitability and required privileges. -* Data confidentiality, integrity, and availability impact. -* Likelihood of exploitation in typical Joomla deployments. -* Exposure surface (public endpoints, administrator area, installation flows, and update mechanisms). +### Critical +* Remote code execution +* Authentication bypass +* Data breach or exposure of sensitive information +* **Fix Timeline**: 7 days -When appropriate, industry standard scoring such as CVSS may be used for internal prioritization. +### High +* Privilege escalation +* SQL injection or command injection +* Cross-site scripting (XSS) with significant impact +* **Fix Timeline**: 14 days -## Coordinated Disclosure +### Medium +* Information disclosure (limited scope) +* Denial of service +* Security misconfigurations with moderate impact +* **Fix Timeline**: 30 days -The project follows coordinated vulnerability disclosure: +### Low +* Security best practice violations +* Minor information leaks +* Issues requiring user interaction or complex preconditions +* **Fix Timeline**: 60 days or next release -* Reports are treated as confidential until remediation is available. -* A public advisory may be published once a fix is released. -* A reasonable embargo period is expected to enable patch distribution. +## Remediation Process -If you believe disclosure is time sensitive due to active exploitation, include that assessment and any supporting indicators. +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 Updates and Advisories +## Security Advisories -Security updates are distributed through: +Security advisories are published via: -* GitHub releases for the repository. -* GitHub Security Advisories when applicable. +* GitHub Security Advisories +* Release notes and CHANGELOG.md +* Security mailing list (when established) -Advisories may include: +Advisories include: -* Affected versions and fixed versions. -* Mitigations and workarounds when a fix is not immediately available. -* Upgrade guidance. +* CVE identifier (if applicable) +* Severity rating +* Affected versions +* Fixed versions +* Mitigation steps +* Attribution (with reporter consent) -## Dependencies and Supply Chain Controls +## Security Best Practices -The project aims to manage supply chain risk through: +For repositories adopting MokoStandards: -* Pinning and review of workflow dependencies where feasible. -* Minimizing privileged GitHub token permissions. -* Validating build inputs prior to packaging releases. +### Required Controls -If you identify a supply chain issue (for example compromised action, dependency confusion, or malicious upstream artifact), report it as a vulnerability. +* 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 `/docs/policy/` -## Secure Development and CI Expectations +### CI/CD Security -Security posture is reinforced through operational controls: +* 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 -* CI validation for packaging inputs and manifest integrity. -* Consistent path normalization and whitespace hygiene checks where required for release correctness. -* Least privilege for GitHub Actions permissions. +#### Automated Security Scanning -### Template Security Features +All repositories MUST implement: -**Custom Head Content Injection** +**CodeQL Analysis**: +* Enabled for all supported languages (Python, JavaScript, TypeScript, Java, C/C++, C#, Go, Ruby) +* Runs on: push to main, pull requests, weekly schedule +* Query sets: `security-extended` and `security-and-quality` +* Configuration: `.github/workflows/codeql-analysis.yml` -The template provides Custom Head Code fields (`custom_head_start` and `custom_head_end`) that allow administrators to inject custom HTML, CSS, and JavaScript code. This is an intentional feature for: +**Dependabot Security Updates**: +* Weekly scans for vulnerable dependencies +* Automated pull requests for security patches +* Configuration: `.github/dependabot.yml` -* Adding analytics scripts (Google Analytics, Google Tag Manager) -* Custom meta tags -* Third-party integrations -* Custom styling +**Secret Scanning**: +* Enabled by default with push protection +* Prevents accidental credential commits +* Partner patterns enabled -**Security Considerations:** +**Dependency Review**: +* Required for all pull requests +* Blocks introduction of known vulnerable dependencies +* Automatic license compliance checking -* These fields use `filter="raw"` to allow HTML/JS injection -* **Access is restricted to Joomla administrators only** via template configuration -* This is not an XSS vulnerability as it requires administrator privileges -* Administrators should only add trusted code from verified sources -* Regular security audits should review custom head content +See [Security Scanning Policy](docs/policy/security-scanning.md) for detailed requirements. -This policy does not guarantee that all vulnerabilities will be prevented. It defines how risk is managed when issues are discovered. +### Dependency Management -## Safe Harbor +* Keep dependencies up to date +* Monitor security advisories for dependencies +* Remove unused dependencies +* Audit new dependencies before adoption +* Document security-critical dependencies -The project supports good faith security research. When you: +## Compliance and Governance -* Avoid privacy violations, data destruction, and service disruption. -* Limit testing to systems you own or have explicit permission to test. -* Provide a reasonable window for coordinated disclosure. +This security policy is binding for all repositories governed by MokoStandards. Deviations require documented justification and approval from the Security Owner. -Then the project will treat your report as a constructive security contribution. +Security policies are reviewed and updated at least annually or following significant security incidents. -Jurisdiction note: this repository is managed from Tennessee, USA. This note is informational only and does not constitute legal advice. +## Attribution and Recognition -## Public Communications +We acknowledge and appreciate responsible disclosure. With your permission, we will: -Only maintainers will publish security advisories or public statements for confirmed vulnerabilities. Public communication will focus on actionable remediation and operational risk reduction. +* Credit you in security advisories +* List you in CHANGELOG.md for the fix release +* Recognize your contribution publicly (if desired) -## Acknowledgements +## Contact and Escalation -If you want credit, include the name or handle to list in an advisory. If you prefer anonymity, state that explicitly. +* **Security Team**: security@[DOMAIN] +* **Primary Contact**: [CONTACT_EMAIL] +* **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 -* **Document:** SECURITY.md -* **Repository:** [https://github.com/mokoconsulting-tech/MokoCassiopeia](https://github.com/mokoconsulting-tech/MokoCassiopeia) -* **Path:** /SECURITY.md -* **Owner:** Moko Consulting -* **Version:** 03.06.00 -* **Status:** Active -* **Effective Date:** 2025-12-18 -* **Last Reviewed:** 2025-12-18 +| Field | Value | +| ------------ | ----------------------------------------------- | +| Document | Security Policy | +| Path | /SECURITY.md | +| Repository | [REPOSITORY_URL] | +| Owner | [OWNER_NAME] | +| Scope | Security vulnerability handling | +| Applies To | All repositories governed by MokoStandards | +| Status | Active | +| Effective | [YYYY-MM-DD] | ## Revision History -| Date | Change Summary | Author | -| ---------- | ------------------------------------------------------------------------------------------------ | --------------- | -| 2026-01-30 | Added Template Security Features section documenting custom head content injection controls. | Copilot Agent | -| 2025-12-18 | Initial publication of security policy, intake channels, triage targets, and disclosure process. | Moko Consulting | +| Date | Change Description | Author | +| ---------- | ------------------------------------------------- | --------------- | +| [YYYY-MM-DD] | Initial creation | [AUTHOR_NAME] | diff --git a/composer.json b/composer.json index b60f555..83ea53f 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,7 @@ "prefer-stable": true, "require": { "php": ">=8.1", - "mokoconsulting-tech/enterprise": "^4.0" + "mokoconsulting-tech/enterprise": "dev-version/04.02.00" }, "require-dev": { "phpunit/phpunit": "^10.5", diff --git a/docs/update-server.md b/docs/update-server.md new file mode 100644 index 0000000..5e5d719 --- /dev/null +++ b/docs/update-server.md @@ -0,0 +1,119 @@ + + +# Joomla Update Server + +[![MokoStandards](https://img.shields.io/badge/MokoStandards-04.04.00-blue)](https://github.com/mokoconsulting-tech/MokoStandards) + +This document explains how `update.xml` is automatically managed for this Joomla extension following the [Joomla Update Server specification](https://docs.joomla.org/Deploying_an_Update_Server). + +## How It Works + +Joomla checks for extension updates by fetching an XML file from the URL defined in the `` tag in the extension's XML manifest. MokoStandards generates this file automatically. + +### Automatic Generation + +| Event | Workflow | `` | `` | +|-------|----------|---------|-------------| +| Merge to `main` | `auto-release.yml` | `stable` | `XX.YY.ZZ` | +| Push to `dev/**` | `deploy-dev.yml` | `development` | `development` | +| Push to `rc/**` | `deploy-dev.yml` | `rc` | `XX.YY.ZZ-rc` | + +### Generated XML Structure + +```xml + + + + Extension Name + Extension Name update + com_extensionname + component + 01.02.03 + site + system + + stable + + https://github.com/.../releases/tag/v01.02.03 + + https://github.com/.../releases/download/v01.02.03/com_ext-01.02.03.zip + + + 8.2 + Moko Consulting + https://mokoconsulting.tech + + +``` + +### Metadata Source + +All metadata is extracted from the extension's XML manifest (`src/*.xml`) at build time: + +| XML Element | Source | Notes | +|-------------|--------|-------| +| `` | `` in manifest | Extension display name | +| `` | `` in manifest | Must match installed extension identifier | +| `` | `type` attribute on `` | `component`, `module`, `plugin`, `library`, `package`, `template` | +| `` | `client` attribute on `` | `site` or `administrator` β€” **required for plugins and modules** | +| `` | `group` attribute on `` | Plugin group (e.g., `system`, `content`) β€” **required for plugins** | +| `` | `` in manifest | Falls back to Joomla 5.x / 6.x if not specified | +| `` | `` in manifest | Included only if present | + +### Extension Manifest Setup + +Your XML manifest must include an `` tag pointing to the `update.xml` on the `main` branch: + +```xml + + My Extension + com_myextension + + + + https://raw.githubusercontent.com/mokoconsulting-tech/MokoCassiopeia/main/update.xml + + + +``` + +### Branch Lifecycle + +``` +dev/XX.YY.ZZ β†’ rc/XX.YY.ZZ β†’ main β†’ version/XX.YY +(development) (rc) (stable) (frozen snapshot) +``` + +1. **Development** (`dev/**`): `update.xml` with `development`, download points to branch archive +2. **Release Candidate** (`rc/**`): `update.xml` with `rc`, version set to `XX.YY.ZZ-rc` +3. **Stable Release** (merge to `main`): `update.xml` with `stable`, download points to GitHub Release asset +4. **Frozen Snapshot** (`version/XX.YY`): immutable, never force-pushed + +### Health Checks + +The `repo_health.yml` workflow verifies on every commit: + +- `update.xml` exists in the repository root +- XML manifest exists with `` tag +- ``, ``, ``, `` tags present +- Extension `type` attribute is valid +- Language `.ini` files exist +- `index.html` directory listing protection in `src/`, `src/admin/`, `src/site/` + +--- + +*Managed by [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards). See [docs/workflows/update-server.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/workflows/update-server.md) for the full specification.*