From 1efa71d87c70ebac8b55c8634f29b30cabba9237 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Thu, 9 Apr 2026 10:51:01 -0500
Subject: [PATCH 01/40] chore: update LICENSE from MokoStandards
--
2.49.1
From 0fe620fcf7a1c19dba9ed250b34bcefeb06bd706 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Thu, 9 Apr 2026 10:51:02 -0500
Subject: [PATCH 02/40] chore: update SECURITY.md from MokoStandards
---
SECURITY.md | 287 +++++++++++++++++++++++++++++++---------------------
1 file changed, 171 insertions(+), 116 deletions(-)
diff --git a/SECURITY.md b/SECURITY.md
index 778309e..d5855cf 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] |
--
2.49.1
From 7a0691ac1494f87de948f29cb4fc429d0c91e703 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Thu, 9 Apr 2026 10:51:03 -0500
Subject: [PATCH 03/40] chore: update CODE_OF_CONDUCT.md from MokoStandards
---
CODE_OF_CONDUCT.md | 131 +++++++++++++++++++++------------------------
1 file changed, 60 insertions(+), 71 deletions(-)
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
index 964953f..474f823 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.
--
2.49.1
From 6bb18410869caba9c324dd831065ffd477e13989 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Thu, 9 Apr 2026 10:51:03 -0500
Subject: [PATCH 04/40] chore: update CONTRIBUTING.md from MokoStandards
---
CONTRIBUTING.md | 189 ++++++++++++++++++++++--------------------------
1 file changed, 86 insertions(+), 103 deletions(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 4e0fd34..eb95e62 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,145 +1,128 @@
+ VERSION: 04.05.13
+ 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` | 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` | 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` 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.*
--
2.49.1
From 36709b1f0a6cfc710a4826cfe94fbf5bfa5a2ae0 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Thu, 9 Apr 2026 10:51:04 -0500
Subject: [PATCH 05/40] chore: add update.xml from MokoStandards
---
update.xml | 34 ++++++++++++++++++++++++++++++++++
1 file changed, 34 insertions(+)
create mode 100644 update.xml
diff --git a/update.xml b/update.xml
new file mode 100644
index 0000000..6c4bb15
--- /dev/null
+++ b/update.xml
@@ -0,0 +1,34 @@
+
+
+
+ {{EXTENSION_NAME}}
+ MokoCassiopeia — Moko Consulting Joomla extension
+ {{EXTENSION_ELEMENT}}
+ {{EXTENSION_TYPE}}
+ {{VERSION}}
+ https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/tag/{{VERSION}}
+
+ {{DOWNLOAD_URL}}
+
+
+ 7.4
+ Moko Consulting
+ {{MAINTAINER_URL}}
+
+
\ No newline at end of file
--
2.49.1
From a0f9859771441672c8bdded1f71b94f9e3670e8c Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Thu, 9 Apr 2026 10:51:05 -0500
Subject: [PATCH 06/40] chore: update phpstan.neon from MokoStandards
--
2.49.1
From 9df13aa63749a6d89b6c93ad26c0eef3c154c7b3 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Thu, 9 Apr 2026 10:51:06 -0500
Subject: [PATCH 07/40] chore: update Makefile from MokoStandards
--
2.49.1
From 084f239a4655db3eee11e7fae324ae138d0b612d Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Thu, 9 Apr 2026 10:51:06 -0500
Subject: [PATCH 08/40] chore: add .mokostandards from MokoStandards
---
.mokostandards | 20 ++++++++++++++++++++
1 file changed, 20 insertions(+)
create mode 100644 .mokostandards
diff --git a/.mokostandards b/.mokostandards
new file mode 100644
index 0000000..e97bc67
--- /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.05.13
+# BRIEF: Governance attachment template — synced to .mokostandards in every governed repository
+# NOTE: Tokens replaced at sync time: mokoconsulting-tech, MokoCassiopeia, waas-component, 04.05.13
+#
+# 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.05.13"
+platform: "waas-component"
+governed_repo: "mokoconsulting-tech/MokoCassiopeia"
--
2.49.1
From 02fda9064e5ef14a326eeade9ffa915260758b92 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Thu, 9 Apr 2026 10:51:07 -0500
Subject: [PATCH 09/40] chore: add docs/update-server.md from MokoStandards
---
docs/update-server.md | 119 ++++++++++++++++++++++++++++++++++++++++++
1 file changed, 119 insertions(+)
create mode 100644 docs/update-server.md
diff --git a/docs/update-server.md b/docs/update-server.md
new file mode 100644
index 0000000..0f55573
--- /dev/null
+++ b/docs/update-server.md
@@ -0,0 +1,119 @@
+
+
+# Joomla Update Server
+
+[](https://github.com/mokoconsulting-tech/MokoStandards)
+
+This document explains how `updates.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 `updates.xml` on the `main` branch:
+
+```xml
+
+ My Extension
+ com_myextension
+
+
+
+ https://raw.githubusercontent.com/mokoconsulting-tech/MokoCassiopeia/main/updates.xml
+
+
+
+```
+
+### Branch Lifecycle
+
+```
+dev/XX.YY.ZZ → rc/XX.YY.ZZ → main → version/XX
+(development) (rc) (stable) (frozen snapshot)
+```
+
+1. **Development** (`dev/**`): `updates.xml` with `development`, download points to branch archive
+2. **Release Candidate** (`rc/**`): `updates.xml` with `rc`, version set to `XX.YY.ZZ-rc`
+3. **Stable Release** (merge to `main`): `updates.xml` with `stable`, download points to GitHub Release asset
+4. **Frozen Snapshot** (`version/XX`): immutable, never force-pushed
+
+### Health Checks
+
+The `repo_health.yml` workflow verifies on every commit:
+
+- `updates.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.*
--
2.49.1
From bb5c7fd8e6988485897f271be2ec0a4ee5ded68c Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Thu, 9 Apr 2026 10:51:08 -0500
Subject: [PATCH 10/40] chore: update .github/copilot.yml from MokoStandards
--
2.49.1
From 21de772b2e00004c84b09c6ef42a3340916ecd0e Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Thu, 9 Apr 2026 10:51:09 -0500
Subject: [PATCH 11/40] chore: update .github/copilot-instructions.md from
MokoStandards
---
.github/copilot-instructions.md | 45 ++++++++++-----------------------
1 file changed, 13 insertions(+), 32 deletions(-)
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index 4e7b705..3968dc4 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -1,22 +1,3 @@
-
-
> [!IMPORTANT]
> **🔧 AI Self-Update Required on First Use**
>
@@ -122,13 +103,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 `updates.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 `update.xml`. The `make release` command / release workflow updates all three automatically.
```xml
01.02.04
-
@@ -154,7 +135,7 @@ The version in `README.md` **must always match** the `` tag in `manifes
```
MokoCassiopeia/
├── manifest.xml # Joomla installer manifest (root — required)
-├── updates.xml # Update server manifest (root — required, see below)
+├── update.xml # Update server manifest (root — required, see below)
├── site/ # Frontend (site) code
│ ├── controller.php
│ ├── controllers/
@@ -183,22 +164,22 @@ MokoCassiopeia/
---
-## updates.xml — Required in Repo Root
+## update.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.
+`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.
The `manifest.xml` must reference it via:
```xml
- https://github.com/mokoconsulting-tech/MokoCassiopeia/raw/main/updates.xml
+ https://github.com/mokoconsulting-tech/MokoCassiopeia/raw/main/update.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`.
+- 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`.
- 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.
@@ -207,8 +188,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 `updates.xml`.
-- Must include `` block pointing to this repo's `updates.xml`.
+- `` tag must be kept in sync with `README.md` version and `update.xml`.
+- Must include `` block pointing to this repo's `update.xml`.
- Must include `` and `` sections.
- Joomla 4.x requires `Moko\{{EXTENSION_NAME}}` for namespaced extensions.
@@ -286,8 +267,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 `updates.xml` version; bump README.md version |
-| New release | Prepend `` block to `updates.xml`; update CHANGELOG.md; bump README.md version |
+| 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 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 |
@@ -301,4 +282,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, `updates.xml` version, and `README.md` version go out of sync
+- Never let `manifest.xml` version, `update.xml` version, and `README.md` version go out of sync
\ No newline at end of file
--
2.49.1
From f678508f8aba7d776d0c1f8d20c0f02bfec30309 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Thu, 9 Apr 2026 10:51:10 -0500
Subject: [PATCH 12/40] chore: update .github/CLAUDE.md from MokoStandards
---
.github/CLAUDE.md | 372 +++++++++++++++++++++-------------------------
1 file changed, 171 insertions(+), 201 deletions(-)
diff --git a/.github/CLAUDE.md b/.github/CLAUDE.md
index 4e7b705..df7d501 100644
--- a/.github/CLAUDE.md
+++ b/.github/CLAUDE.md
@@ -1,22 +1,3 @@
-
-
> [!IMPORTANT]
> **🔧 AI Self-Update Required on First Use**
>
@@ -38,34 +19,135 @@ NOTE: Synced to .github/copilot-instructions.md in all Joomla/WaaS repos via bul
> |---|---|
> | `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. MokoCassiopeia 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`) |
>
> ---
-# MokoCassiopeia — GitHub Copilot Custom Instructions
+# What This Repo Is
-## What This Repo Is
+**MokoCassiopeia** is a Moko Consulting **MokoWaaS** (Joomla) extension repository.
-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.
+A modern enhancement layer for Joomla’s Cassiopeia template. MokoCassiopeia 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.
-Repository URL: https://github.com/mokoconsulting-tech/MokoCassiopeia
Extension name: **{{EXTENSION_NAME}}**
Extension type: **{{EXTENSION_TYPE}}** (`{{EXTENSION_ELEMENT}}`)
-Platform: **Joomla 4.x / MokoWaaS**
+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.
---
-## Primary Language
+# Repo Structure
-**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`.
+```
+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
+```
---
-## File Header — Always Required on New Files
+# Primary Language
-Every new file needs a copyright header as its first content.
+**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.
**PHP:**
```php
@@ -80,141 +162,47 @@ Every new file needs a copyright header as its first content.
* DEFGROUP: MokoCassiopeia.{{EXTENSION_TYPE}}
* INGROUP: MokoCassiopeia
* REPO: https://github.com/mokoconsulting-tech/MokoCassiopeia
- * PATH: /path/to/file.php
+ * PATH: /site/controllers/item.php
* VERSION: XX.YY.ZZ
- * BRIEF: One-line description of purpose
+ * BRIEF: One-line description of file purpose
*/
defined('_JEXEC') or die;
```
-**Markdown:**
-```markdown
-
-```
-
-**YAML / Shell / XML:** Use the appropriate comment syntax with the same fields. JSON files are exempt.
+**Markdown / YAML / Shell / XML:** Use the appropriate comment syntax with the same fields.
---
-## Version Management
+# Coding Standards
-**`README.md` is the single source of truth for the repository version.**
+## Naming Conventions
-- **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.
+| 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` |
-### Joomla Version Alignment
+## Commit Messages
-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.
+Format: `(): ` — imperative, lower-case subject, no trailing period.
-```xml
-
-01.02.04
+Valid types: `feat` · `fix` · `docs` · `chore` · `ci` · `refactor` · `style` · `test` · `perf` · `revert` · `build`
-
-
-
- {{EXTENSION_NAME}}
- 01.02.04
-
-
- https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/01.02.04/{{EXTENSION_ELEMENT}}-01.02.04.zip
-
-
-
-
-
-
-```
+## Branch Naming
+
+Format: `/[/description]`
+
+Approved prefixes: `dev/` · `rc/` · `version/` · `patch/` · `copilot/` · `dependabot/`
---
-## 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
+# GitHub Actions — Token Usage
Every workflow must use **`secrets.GH_TOKEN`** (the org-level Personal Access Token).
@@ -229,76 +217,58 @@ env:
```
```yaml
-# ❌ Wrong — never use these in workflows
+# ❌ Wrong — never use these
token: ${{ github.token }}
token: ${{ secrets.GITHUB_TOKEN }}
```
---
-## MokoStandards Reference
-
-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 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 |
-
----
-
-## 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
+# 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 `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 |
---
-## Key Constraints
+# What NOT to Do
-- 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
+- **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)
+
+| 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 |
+| [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
--
2.49.1
From 43835441b725b482fcd354a347166a7cb951ceda Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Thu, 9 Apr 2026 10:51:10 -0500
Subject: [PATCH 13/40] chore: update .github/workflows/ci-joomla.yml from
MokoStandards
---
.github/workflows/ci-joomla.yml | 13 ++++++++++---
1 file changed, 10 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/ci-joomla.yml b/.github/workflows/ci-joomla.yml
index 7329a62..e2c637f 100644
--- a/.github/workflows/ci-joomla.yml
+++ b/.github/workflows/ci-joomla.yml
@@ -8,18 +8,25 @@
# 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
+# PATH: /templates/workflows/joomla/ci-joomla.yml
+# VERSION: 04.05.13
# 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/**'
+ - dev/**
+ - rc/**
workflow_dispatch:
permissions:
--
2.49.1
From d2a3aca37497ea31fe18ad46eb733c3b4bea4cc2 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Thu, 9 Apr 2026 10:51:11 -0500
Subject: [PATCH 14/40] chore: update .github/workflows/codeql-analysis.yml
from MokoStandards
---
.github/workflows/codeql-analysis.yml | 13 ++++++++++---
1 file changed, 10 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 72cacae..6cbeb71 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -8,8 +8,8 @@
# DEFGROUP: GitHub.Workflow.Template
# INGROUP: MokoStandards.Security
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
-# PATH: /templates/workflows/generic/codeql-analysis.yml.template
-# VERSION: 03.09.03
+# PATH: /templates/workflows/generic/codeql-analysis.yml
+# VERSION: 04.05.00
# 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.
@@ -21,7 +21,14 @@ on:
push:
branches:
- main
- - version/*
+ - dev/**
+ - rc/**
+ - version/**
+ pull_request:
+ branches:
+ - main
+ - dev/**
+ - rc/**
schedule:
# Weekly on Monday at 06:00 UTC
- cron: '0 6 * * 1'
--
2.49.1
From c514033a7204c707c1d5d4a93fe066f37cf400f1 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Thu, 9 Apr 2026 10:51:12 -0500
Subject: [PATCH 15/40] chore: update
.github/workflows/standards-compliance.yml from MokoStandards
---
.github/workflows/standards-compliance.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/standards-compliance.yml b/.github/workflows/standards-compliance.yml
index 79aaedd..773279d 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.06.00
+# VERSION: 04.05.00
# BRIEF: MokoStandards compliance validation workflow
# NOTE: Validates repository structure, documentation, and coding standards
--
2.49.1
From 1267bd9b1cb92bfc47d47ace16b0007ec6ba4762 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Thu, 9 Apr 2026 10:51:12 -0500
Subject: [PATCH 16/40] chore: update
.github/workflows/enterprise-firewall-setup.yml from MokoStandards
---
.github/workflows/enterprise-firewall-setup.yml | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/enterprise-firewall-setup.yml b/.github/workflows/enterprise-firewall-setup.yml
index 1a533fb..3abe9ed 100644
--- a/.github/workflows/enterprise-firewall-setup.yml
+++ b/.github/workflows/enterprise-firewall-setup.yml
@@ -21,8 +21,8 @@
# DEFGROUP: GitHub.Workflow
# INGROUP: MokoStandards.Firewall
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
-# PATH: /templates/workflows/shared/enterprise-firewall-setup.yml.template
-# VERSION: 04.06.00
+# PATH: /templates/workflows/shared/enterprise-firewall-setup.yml
+# VERSION: 04.05.13
# 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.
@@ -90,7 +90,7 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
--
2.49.1
From 8e93f3b2f5d4693b4889cab12dadf4d043feff4a Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Thu, 9 Apr 2026 10:51:13 -0500
Subject: [PATCH 17/40] chore: add .github/workflows/deploy-dev.yml from
MokoStandards
---
.github/workflows/deploy-dev.yml | 700 +++++++++++++++++++++++++++++++
1 file changed, 700 insertions(+)
create mode 100644 .github/workflows/deploy-dev.yml
diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml
new file mode 100644
index 0000000..34178de
--- /dev/null
+++ b/.github/workflows/deploy-dev.yml
@@ -0,0 +1,700 @@
+# Copyright (C) 2026 Moko Consulting
+#
+# This file is part of a Moko Consulting project.
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+# FILE INFORMATION
+# DEFGROUP: GitHub.Workflow
+# INGROUP: MokoStandards.Deploy
+# REPO: https://github.com/mokoconsulting-tech/MokoStandards
+# PATH: /templates/workflows/shared/deploy-dev.yml
+# VERSION: 04.05.13
+# 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.
+
+name: Deploy to Dev Server (SFTP)
+
+# Deploys the contents of the src/ directory to the development server via SFTP.
+# Triggers on every pull_request to development branches (so the dev server always
+# reflects the latest PR state) and on push/merge to main branches.
+#
+# Required org-level variables: DEV_FTP_HOST, DEV_FTP_PATH, DEV_FTP_USERNAME
+# 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 .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.
+
+on:
+ push:
+ branches:
+ - 'dev/**'
+ - 'rc/**'
+ - develop
+ - development
+ paths:
+ - 'src/**'
+ - 'htdocs/**'
+ pull_request:
+ types: [opened, synchronize, reopened, closed]
+ branches:
+ - 'dev/**'
+ - 'rc/**'
+ - develop
+ - development
+ paths:
+ - 'src/**'
+ - 'htdocs/**'
+ workflow_dispatch:
+ inputs:
+ clear_remote:
+ description: 'Delete all files inside the remote destination folder before uploading'
+ required: false
+ default: false
+ type: boolean
+
+permissions:
+ contents: read
+ pull-requests: write
+
+env:
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
+
+jobs:
+ check-permission:
+ name: Verify Deployment Permission
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check actor permission
+ env:
+ # Prefer the org-scoped GH_TOKEN secret (needed for the org membership
+ # fallback). Falls back to the built-in github.token so the collaborator
+ # endpoint still works even if GH_TOKEN is not configured.
+ GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
+ run: |
+ ACTOR="${{ github.actor }}"
+ REPO="${{ github.repository }}"
+ ORG="${{ github.repository_owner }}"
+
+ METHOD=""
+ AUTHORIZED="false"
+
+ # Hardcoded authorized users — always allowed to deploy
+ AUTHORIZED_USERS="jmiller-moko github-actions[bot]"
+ for user in $AUTHORIZED_USERS; do
+ if [ "$ACTOR" = "$user" ]; then
+ AUTHORIZED="true"
+ METHOD="hardcoded allowlist"
+ PERMISSION="admin"
+ break
+ fi
+ done
+
+ # For other actors, check repo/org permissions via API
+ if [ "$AUTHORIZED" != "true" ]; then
+ PERMISSION=$(gh api "repos/${REPO}/collaborators/${ACTOR}/permission" \
+ --jq '.permission' 2>/dev/null)
+ METHOD="repo collaborator API"
+
+ if [ -z "$PERMISSION" ]; then
+ ORG_ROLE=$(gh api "orgs/${ORG}/memberships/${ACTOR}" \
+ --jq '.role' 2>/dev/null)
+ METHOD="org membership API"
+ if [ "$ORG_ROLE" = "owner" ]; then
+ PERMISSION="admin"
+ else
+ PERMISSION="none"
+ fi
+ fi
+
+ case "$PERMISSION" in
+ admin|maintain) AUTHORIZED="true" ;;
+ esac
+ fi
+
+ # Write detailed summary
+ {
+ echo "## 🔐 Deploy Authorization"
+ echo ""
+ echo "| Field | Value |"
+ echo "|-------|-------|"
+ echo "| **Actor** | \`${ACTOR}\` |"
+ echo "| **Repository** | \`${REPO}\` |"
+ echo "| **Permission** | \`${PERMISSION}\` |"
+ echo "| **Method** | ${METHOD} |"
+ echo "| **Authorized** | ${AUTHORIZED} |"
+ echo "| **Trigger** | \`${{ github.event_name }}\` |"
+ echo "| **Branch** | \`${{ github.ref_name }}\` |"
+ echo ""
+ } >> "$GITHUB_STEP_SUMMARY"
+
+ if [ "$AUTHORIZED" = "true" ]; then
+ echo "✅ ${ACTOR} authorized to deploy (${METHOD})" >> "$GITHUB_STEP_SUMMARY"
+ else
+ echo "❌ ${ACTOR} is NOT authorized to deploy." >> "$GITHUB_STEP_SUMMARY"
+ echo "" >> "$GITHUB_STEP_SUMMARY"
+ echo "Deployment requires one of:" >> "$GITHUB_STEP_SUMMARY"
+ echo "- Being in the hardcoded allowlist" >> "$GITHUB_STEP_SUMMARY"
+ echo "- Having \`admin\` or \`maintain\` role on the repository" >> "$GITHUB_STEP_SUMMARY"
+ exit 1
+ fi
+
+ deploy:
+ name: SFTP Deploy → Dev
+ runs-on: ubuntu-latest
+ needs: [check-permission]
+ if: >-
+ !startsWith(github.head_ref || github.ref_name, 'chore/') &&
+ (github.event_name == 'workflow_dispatch' ||
+ github.event_name == 'push' ||
+ (github.event_name == 'pull_request' &&
+ (github.event.action == 'opened' ||
+ github.event.action == 'synchronize' ||
+ github.event.action == 'reopened' ||
+ github.event.pull_request.merged == true)))
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+
+ - name: Resolve source directory
+ id: source
+ run: |
+ # Resolve source directory: src/ preferred, htdocs/ as fallback
+ if [ -d "src" ]; then
+ SRC="src"
+ elif [ -d "htdocs" ]; then
+ SRC="htdocs"
+ else
+ echo "⚠️ No src/ or htdocs/ directory found — skipping deployment"
+ echo "skip=true" >> "$GITHUB_OUTPUT"
+ exit 0
+ fi
+ COUNT=$(find "$SRC" -type f | wc -l)
+ echo "✅ Source: ${SRC}/ (${COUNT} file(s))"
+ echo "skip=false" >> "$GITHUB_OUTPUT"
+ echo "dir=${SRC}" >> "$GITHUB_OUTPUT"
+
+ - name: Preview files to deploy
+ if: steps.source.outputs.skip == 'false'
+ env:
+ SOURCE_DIR: ${{ steps.source.outputs.dir }}
+ run: |
+ # ── Convert a ftpignore-style glob line to an ERE pattern ──────────────
+ ftpignore_to_regex() {
+ local line="$1"
+ local anchored=false
+ # Strip inline comments and whitespace
+ line=$(printf '%s' "$line" | sed 's/[[:space:]]*#.*$//' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
+ [ -z "$line" ] && return
+ # Skip negation patterns (not supported)
+ [[ "$line" == !* ]] && return
+ # Trailing slash = directory marker; strip it
+ line="${line%/}"
+ # Leading slash = anchored to root; strip it
+ if [[ "$line" == /* ]]; then
+ anchored=true
+ line="${line#/}"
+ fi
+ # Escape ERE special chars, then restore glob semantics
+ local regex
+ regex=$(printf '%s' "$line" \
+ | sed 's/[.+^${}()|[\\]/\\&/g' \
+ | sed 's/\\\*\\\*/\x01/g' \
+ | sed 's/\\\*/[^\/]*/g' \
+ | sed 's/\x01/.*/g' \
+ | sed 's/\\\?/[^\/]/g')
+ if $anchored; then
+ printf '^%s(/|$)' "$regex"
+ else
+ printf '(^|/)%s(/|$)' "$regex"
+ fi
+ }
+
+ # ── Read .ftpignore (ftpignore-style globs) ─────────────────────────
+ IGNORE_PATTERNS=()
+ IGNORE_SOURCES=()
+ if [ -f ".ftpignore" ]; then
+ while IFS= read -r line; do
+ [[ "$line" =~ ^[[:space:]]*$ || "$line" =~ ^[[:space:]]*# ]] && continue
+ regex=$(ftpignore_to_regex "$line")
+ [ -n "$regex" ] && IGNORE_PATTERNS+=("$regex") && IGNORE_SOURCES+=("$line")
+ done < ".ftpignore"
+ fi
+
+ # ── Walk src/ and classify every file ────────────────────────────────
+ WILL_UPLOAD=()
+ IGNORED_FILES=()
+ while IFS= read -r -d '' file; do
+ rel="${file#${SOURCE_DIR}/}"
+ SKIP=false
+ for i in "${!IGNORE_PATTERNS[@]}"; do
+ if echo "$rel" | grep -qE "${IGNORE_PATTERNS[$i]}" 2>/dev/null; then
+ IGNORED_FILES+=("$rel | .ftpignore \`${IGNORE_SOURCES[$i]}\`")
+ SKIP=true; break
+ fi
+ done
+ $SKIP && continue
+ WILL_UPLOAD+=("$rel")
+ done < <(find "$SOURCE_DIR" -type f -print0 | sort -z)
+
+ UPLOAD_COUNT="${#WILL_UPLOAD[@]}"
+ IGNORE_COUNT="${#IGNORED_FILES[@]}"
+
+ echo "ℹ️ ${UPLOAD_COUNT} file(s) will be uploaded, ${IGNORE_COUNT} ignored"
+
+ # ── Write deployment preview to step summary ──────────────────────────
+ {
+ echo "## 📋 Deployment Preview"
+ echo ""
+ echo "| Field | Value |"
+ echo "|---|---|"
+ echo "| Source | \`${SOURCE_DIR}/\` |"
+ echo "| Files to upload | **${UPLOAD_COUNT}** |"
+ echo "| Files ignored | **${IGNORE_COUNT}** |"
+ echo ""
+ if [ "${UPLOAD_COUNT}" -gt 0 ]; then
+ echo "### 📂 Files that will be uploaded"
+ echo '```'
+ printf '%s\n' "${WILL_UPLOAD[@]}"
+ echo '```'
+ echo ""
+ fi
+ if [ "${IGNORE_COUNT}" -gt 0 ]; then
+ echo "### ⏭️ Files excluded"
+ echo "| File | Reason |"
+ echo "|---|---|"
+ for entry in "${IGNORED_FILES[@]}"; do
+ f="${entry% | *}"; r="${entry##* | }"
+ echo "| \`${f}\` | ${r} |"
+ done
+ echo ""
+ fi
+ } >> "$GITHUB_STEP_SUMMARY"
+
+ - name: Resolve SFTP host and port
+ if: steps.source.outputs.skip == 'false'
+ id: conn
+ env:
+ HOST_RAW: ${{ vars.DEV_FTP_HOST }}
+ PORT_VAR: ${{ vars.DEV_FTP_PORT }}
+ run: |
+ HOST="$HOST_RAW"
+ PORT="$PORT_VAR"
+
+ # Priority 1 — explicit DEV_FTP_PORT variable
+ if [ -n "$PORT" ]; then
+ echo "ℹ️ Using explicit DEV_FTP_PORT=${PORT}"
+
+ # Priority 2 — port embedded in DEV_FTP_HOST (host:port)
+ elif [[ "$HOST" == *:* ]]; then
+ PORT="${HOST##*:}"
+ HOST="${HOST%:*}"
+ echo "ℹ️ Extracted port ${PORT} from DEV_FTP_HOST"
+
+ # Priority 3 — SFTP default
+ else
+ PORT="22"
+ echo "ℹ️ No port specified — defaulting to SFTP port 22"
+ fi
+
+ echo "host=${HOST}" >> "$GITHUB_OUTPUT"
+ echo "port=${PORT}" >> "$GITHUB_OUTPUT"
+ echo "SFTP target: ${HOST}:${PORT}"
+
+ - name: Build remote path
+ if: steps.source.outputs.skip == 'false'
+ id: remote
+ env:
+ DEV_FTP_PATH: ${{ vars.DEV_FTP_PATH }}
+ DEV_FTP_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
+ run: |
+ BASE="$DEV_FTP_PATH"
+
+ if [ -z "$BASE" ]; then
+ echo "❌ DEV_FTP_PATH is not set."
+ echo " Configure it as an org-level variable (Settings → Variables) and"
+ echo " ensure this repository has been granted access to it."
+ exit 1
+ fi
+
+ # DEV_FTP_SUFFIX is required — it identifies the remote subdirectory for this repo.
+ # Without it we cannot safely determine the deployment target.
+ if [ -z "$DEV_FTP_SUFFIX" ]; then
+ echo "⏭️ DEV_FTP_SUFFIX variable is not set — skipping deployment."
+ echo " Set DEV_FTP_SUFFIX as a repo or org variable to enable deploy-dev."
+ echo "skip=true" >> "$GITHUB_OUTPUT"
+ echo "path=" >> "$GITHUB_OUTPUT"
+ exit 0
+ fi
+
+ REMOTE="${BASE%/}/${DEV_FTP_SUFFIX#/}"
+
+ # ── Platform-specific path safety guards ──────────────────────────────
+ PLATFORM=""
+ MOKO_FILE=".github/.mokostandards"; [ ! -f "$MOKO_FILE" ] && MOKO_FILE=".mokostandards"; if [ -f "$MOKO_FILE" ]; then
+ PLATFORM=$(grep -oP '^platform:.*' "$MOKO_FILE" 2>/dev/null || true)
+ fi
+
+ if [ "$PLATFORM" = "crm-module" ]; then
+ # Dolibarr modules must deploy under htdocs/custom/ — guard against
+ # accidentally overwriting server root or unrelated directories.
+ if [[ "$REMOTE" != *custom* ]]; then
+ echo "❌ Safety check failed: Dolibarr (crm-module) remote path must contain 'custom'."
+ echo " Current path: ${REMOTE}"
+ echo " Set DEV_FTP_SUFFIX to the module's htdocs/custom/ subdirectory."
+ exit 1
+ fi
+ fi
+
+ if [ "$PLATFORM" = "waas-component" ]; then
+ # Joomla extensions may only deploy to the server's tmp/ directory.
+ if [[ "$REMOTE" != *tmp* ]]; then
+ echo "❌ Safety check failed: Joomla (waas-component) remote path must contain 'tmp'."
+ echo " Current path: ${REMOTE}"
+ echo " Set DEV_FTP_SUFFIX to a path under the server tmp/ directory."
+ exit 1
+ fi
+ fi
+
+ echo "ℹ️ Remote path: ${REMOTE}"
+ echo "path=${REMOTE}" >> "$GITHUB_OUTPUT"
+
+ - name: Detect SFTP authentication method
+ if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true'
+ id: auth
+ env:
+ HAS_KEY: ${{ secrets.DEV_FTP_KEY }}
+ HAS_PASSWORD: ${{ secrets.DEV_FTP_PASSWORD }}
+ run: |
+ if [ -n "$HAS_KEY" ] && [ -n "$HAS_PASSWORD" ]; then
+ # Both set: key auth with password as passphrase; falls back to password-only if key fails
+ echo "method=key" >> "$GITHUB_OUTPUT"
+ echo "use_passphrase=true" >> "$GITHUB_OUTPUT"
+ echo "has_password=true" >> "$GITHUB_OUTPUT"
+ echo "ℹ️ Primary: SSH key + passphrase (DEV_FTP_KEY / DEV_FTP_PASSWORD)"
+ echo "ℹ️ Fallback: password-only auth if key authentication fails"
+ elif [ -n "$HAS_KEY" ]; then
+ # Key only: no passphrase, no password fallback
+ echo "method=key" >> "$GITHUB_OUTPUT"
+ echo "use_passphrase=false" >> "$GITHUB_OUTPUT"
+ echo "has_password=false" >> "$GITHUB_OUTPUT"
+ echo "ℹ️ Using SSH key authentication (DEV_FTP_KEY, no passphrase, no fallback)"
+ elif [ -n "$HAS_PASSWORD" ]; then
+ # Password only: direct SFTP password auth
+ echo "method=password" >> "$GITHUB_OUTPUT"
+ echo "use_passphrase=false" >> "$GITHUB_OUTPUT"
+ echo "has_password=true" >> "$GITHUB_OUTPUT"
+ echo "ℹ️ Using password authentication (DEV_FTP_PASSWORD)"
+ else
+ echo "❌ No SFTP credentials configured."
+ echo " Set DEV_FTP_KEY (preferred) or DEV_FTP_PASSWORD as an org-level secret."
+ exit 1
+ fi
+
+ - name: Setup PHP
+ if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true'
+ uses: shivammathur/setup-php@fcafdd6392932010c2bd5094439b8e33be2a8a09 # v2.37.0
+ with:
+ php-version: '8.1'
+ tools: composer
+
+ - name: Setup MokoStandards deploy tools
+ if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true'
+ 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
+ cd /tmp/mokostandards
+ composer install --no-dev --no-interaction --quiet
+
+ - name: Clear remote destination folder (manual only)
+ if: >-
+ steps.source.outputs.skip == 'false' &&
+ steps.remote.outputs.skip != 'true' &&
+ inputs.clear_remote == true
+ env:
+ SFTP_HOST: ${{ steps.conn.outputs.host }}
+ SFTP_PORT: ${{ steps.conn.outputs.port }}
+ SFTP_USER: ${{ vars.DEV_FTP_USERNAME }}
+ SFTP_KEY: ${{ secrets.DEV_FTP_KEY }}
+ SFTP_PASSWORD: ${{ secrets.DEV_FTP_PASSWORD }}
+ AUTH_METHOD: ${{ steps.auth.outputs.method }}
+ USE_PASSPHRASE: ${{ steps.auth.outputs.use_passphrase }}
+ HAS_PASSWORD: ${{ steps.auth.outputs.has_password }}
+ REMOTE_PATH: ${{ steps.remote.outputs.path }}
+ run: |
+ cat > /tmp/moko_clear.php << 'PHPEOF'
+ login($username, $key)) {
+ if ($password !== '') {
+ echo "⚠️ Key auth failed — falling back to password\n";
+ if (!$sftp->login($username, $password)) {
+ fwrite(STDERR, "❌ Both key and password authentication failed\n");
+ exit(1);
+ }
+ echo "✅ Connected via password authentication (key fallback)\n";
+ } else {
+ fwrite(STDERR, "❌ Key authentication failed and no password fallback is available\n");
+ exit(1);
+ }
+ } else {
+ echo "✅ Connected via SSH key authentication\n";
+ }
+ } else {
+ if (!$sftp->login($username, (string) getenv('SFTP_PASSWORD'))) {
+ fwrite(STDERR, "❌ Password authentication failed\n");
+ exit(1);
+ }
+ echo "✅ Connected via password authentication\n";
+ }
+
+ // ── Recursive delete ────────────────────────────────────────────
+ function rmrf(SFTP $sftp, string $path): void
+ {
+ $entries = $sftp->nlist($path);
+ if ($entries === false) {
+ return; // path does not exist — nothing to clear
+ }
+ foreach ($entries as $name) {
+ if ($name === '.' || $name === '..') {
+ continue;
+ }
+ $entry = "{$path}/{$name}";
+ if ($sftp->is_dir($entry)) {
+ rmrf($sftp, $entry);
+ $sftp->rmdir($entry);
+ echo " 🗑️ Removed dir: {$entry}\n";
+ } else {
+ $sftp->delete($entry);
+ echo " 🗑️ Removed file: {$entry}\n";
+ }
+ }
+ }
+
+ // ── Create remote directory tree ────────────────────────────────
+ function sftpMakedirs(SFTP $sftp, string $path): void
+ {
+ $parts = array_values(array_filter(explode('/', $path), fn(string $p) => $p !== ''));
+ $current = str_starts_with($path, '/') ? '' : '';
+ foreach ($parts as $part) {
+ $current .= '/' . $part;
+ $sftp->mkdir($current); // silently returns false if already exists
+ }
+ }
+
+ rmrf($sftp, $remotePath);
+ sftpMakedirs($sftp, $remotePath);
+ echo "✅ Remote folder ready: {$remotePath}\n";
+ PHPEOF
+ php /tmp/moko_clear.php
+
+ - name: Deploy via SFTP
+ if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true'
+ env:
+ SFTP_HOST: ${{ steps.conn.outputs.host }}
+ SFTP_PORT: ${{ steps.conn.outputs.port }}
+ SFTP_USER: ${{ vars.DEV_FTP_USERNAME }}
+ SFTP_KEY: ${{ secrets.DEV_FTP_KEY }}
+ SFTP_PASSWORD: ${{ secrets.DEV_FTP_PASSWORD }}
+ AUTH_METHOD: ${{ steps.auth.outputs.method }}
+ USE_PASSPHRASE: ${{ steps.auth.outputs.use_passphrase }}
+ REMOTE_PATH: ${{ steps.remote.outputs.path }}
+ SOURCE_DIR: ${{ steps.source.outputs.dir }}
+ run: |
+ # ── Write SSH key to temp file (key auth only) ────────────────────────
+ if [ "$AUTH_METHOD" = "key" ]; then
+ printf '%s' "$SFTP_KEY" > /tmp/deploy_key
+ chmod 600 /tmp/deploy_key
+ fi
+
+ # ── Generate sftp-config.json safely via jq ───────────────────────────
+ if [ "$AUTH_METHOD" = "key" ]; then
+ jq -n \
+ --arg host "$SFTP_HOST" \
+ --argjson port "${SFTP_PORT:-22}" \
+ --arg user "$SFTP_USER" \
+ --arg path "$REMOTE_PATH" \
+ --arg key "/tmp/deploy_key" \
+ '{host:$host, port:$port, user:$user, remote_path:$path, ssh_key_file:$key}' \
+ > /tmp/sftp-config.json
+ else
+ jq -n \
+ --arg host "$SFTP_HOST" \
+ --argjson port "${SFTP_PORT:-22}" \
+ --arg user "$SFTP_USER" \
+ --arg path "$REMOTE_PATH" \
+ --arg pass "$SFTP_PASSWORD" \
+ '{host:$host, port:$port, user:$user, remote_path:$path, password:$pass}' \
+ > /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
+ DEPLOY_ARGS+=(--key-passphrase "$SFTP_PASSWORD")
+ fi
+
+ # Set platform version to "development" before deploy (Dolibarr + Joomla)
+ php /tmp/mokostandards/api/cli/version_set_platform.php --path . --version development
+
+ # Write update files — dev/** = development, rc/** = rc
+ PLATFORM=$(php /tmp/mokostandards/api/cli/platform_detect.php --path . 2>/dev/null || true)
+ REPO="${{ github.repository }}"
+ BRANCH="${{ github.ref_name }}"
+
+ # Determine stability tag from branch prefix
+ STABILITY="development"
+ VERSION_LABEL="development"
+ if [[ "$BRANCH" == rc/* ]]; then
+ STABILITY="rc"
+ VERSION_LABEL=$(php /tmp/mokostandards/api/cli/version_read.php --path . 2>/dev/null || echo "${BRANCH#rc/}")-rc
+ fi
+
+ if [ "$PLATFORM" = "crm-module" ]; then
+ printf '%s' "$VERSION_LABEL" > update.txt
+ fi
+
+ if [ "$PLATFORM" = "waas-component" ]; then
+ MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1 || true)
+ if [ -n "$MANIFEST" ]; then
+ 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 '/dev/null | head -1 || true)
+ [ -n "$TARGET_PLATFORM" ] && TARGET_PLATFORM="${TARGET_PLATFORM}>"
+ [ -z "$TARGET_PLATFORM" ] && TARGET_PLATFORM=$(printf '' "/")
+
+ CLIENT_TAG=""
+ if [ -n "$EXT_CLIENT" ]; then
+ CLIENT_TAG="${EXT_CLIENT}"
+ elif [ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]; then
+ CLIENT_TAG="site"
+ fi
+
+ FOLDER_TAG=""
+ if [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ]; then
+ FOLDER_TAG="${EXT_FOLDER}"
+ fi
+
+ DOWNLOAD_URL="https://github.com/${REPO}/archive/refs/heads/${BRANCH}.zip"
+
+ {
+ printf '%s\n' ''
+ printf '%s\n' ''
+ printf '%s\n' ' '
+ printf '%s\n' " ${EXT_NAME}"
+ printf '%s\n' " ${EXT_NAME} ${STABILITY} build"
+ printf '%s\n' " ${EXT_ELEMENT}"
+ printf '%s\n' " ${EXT_TYPE}"
+ printf '%s\n' " ${VERSION_LABEL}"
+ [ -n "$CLIENT_TAG" ] && printf '%s\n' " ${CLIENT_TAG}"
+ [ -n "$FOLDER_TAG" ] && printf '%s\n' " ${FOLDER_TAG}"
+ printf '%s\n' ' '
+ printf '%s\n' " ${STABILITY}"
+ printf '%s\n' ' '
+ printf '%s\n' " https://github.com/${REPO}/tree/${BRANCH}"
+ printf '%s\n' ' '
+ printf '%s\n' " ${DOWNLOAD_URL}"
+ printf '%s\n' ' '
+ printf '%s\n' " ${TARGET_PLATFORM}"
+ printf '%s\n' ' Moko Consulting'
+ printf '%s\n' ' https://mokoconsulting.tech'
+ printf '%s\n' ' '
+ printf '%s\n' ''
+ } > updates.xml
+ sed -i '/^[[:space:]]*$/d' updates.xml
+ fi
+ fi
+
+ # 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
+
+ # Dev deploys fail silently — no issue creation.
+ # Demo and RS deploys create failure issues (production-facing).
+
+ - name: Deployment summary
+ if: always()
+ run: |
+ if [ "${{ steps.source.outputs.skip }}" == "true" ]; then
+ echo "### ⏭️ Deployment Skipped" >> "$GITHUB_STEP_SUMMARY"
+ echo "" >> "$GITHUB_STEP_SUMMARY"
+ echo "No \`src/\` directory found in this repository." >> "$GITHUB_STEP_SUMMARY"
+ elif [ "${{ job.status }}" == "success" ]; then
+ echo "" >> "$GITHUB_STEP_SUMMARY"
+ echo "### ✅ Dev Deployment Successful" >> "$GITHUB_STEP_SUMMARY"
+ echo "" >> "$GITHUB_STEP_SUMMARY"
+ echo "| Field | Value |" >> "$GITHUB_STEP_SUMMARY"
+ echo "|-------|-------|" >> "$GITHUB_STEP_SUMMARY"
+ echo "| Host | \`${{ steps.conn.outputs.host }}:${{ steps.conn.outputs.port }}\` |" >> "$GITHUB_STEP_SUMMARY"
+ echo "| Remote path | \`${{ steps.remote.outputs.path }}\` |" >> "$GITHUB_STEP_SUMMARY"
+ echo "| Source | \`src/\` |" >> "$GITHUB_STEP_SUMMARY"
+ echo "| Trigger | ${{ github.event_name }} |" >> "$GITHUB_STEP_SUMMARY"
+ echo "| Auth | ${{ steps.auth.outputs.method }} |" >> "$GITHUB_STEP_SUMMARY"
+ echo "| Clear remote | ${{ inputs.clear_remote || 'false' }} |" >> "$GITHUB_STEP_SUMMARY"
+ else
+ echo "### ❌ Dev Deployment Failed" >> "$GITHUB_STEP_SUMMARY"
+ echo "" >> "$GITHUB_STEP_SUMMARY"
+ echo "Check the job log above for error details." >> "$GITHUB_STEP_SUMMARY"
+ fi
--
2.49.1
From 9f9749352572db5c2e17c75acbfb22b9b1aff0cd Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Thu, 9 Apr 2026 10:51:14 -0500
Subject: [PATCH 18/40] chore: add .github/workflows/deploy-demo.yml from
MokoStandards
---
.github/workflows/deploy-demo.yml | 734 ++++++++++++++++++++++++++++++
1 file changed, 734 insertions(+)
create mode 100644 .github/workflows/deploy-demo.yml
diff --git a/.github/workflows/deploy-demo.yml b/.github/workflows/deploy-demo.yml
new file mode 100644
index 0000000..db2227d
--- /dev/null
+++ b/.github/workflows/deploy-demo.yml
@@ -0,0 +1,734 @@
+# Copyright (C) 2026 Moko Consulting
+#
+# This file is part of a Moko Consulting project.
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+# FILE INFORMATION
+# DEFGROUP: GitHub.Workflow
+# INGROUP: MokoStandards.Deploy
+# REPO: https://github.com/mokoconsulting-tech/MokoStandards
+# PATH: /templates/workflows/shared/deploy-demo.yml
+# VERSION: 04.05.13
+# 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.
+
+name: Deploy to Demo Server (SFTP)
+
+# Deploys the contents of the src/ directory to the demo server via SFTP.
+# Triggers on push/merge to main — deploys the production-ready build to the demo server.
+#
+# Required org-level variables: DEMO_FTP_HOST, DEMO_FTP_PATH, DEMO_FTP_USERNAME
+# 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 .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.
+
+on:
+ push:
+ branches:
+ - main
+ - master
+ paths:
+ - 'src/**'
+ - 'htdocs/**'
+ pull_request:
+ types: [opened, synchronize, reopened, closed]
+ branches:
+ - main
+ - master
+ paths:
+ - 'src/**'
+ - 'htdocs/**'
+ workflow_dispatch:
+ inputs:
+ clear_remote:
+ description: 'Delete all files inside the remote destination folder before uploading'
+ required: false
+ default: false
+ type: boolean
+
+permissions:
+ contents: read
+ pull-requests: write
+
+env:
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
+
+jobs:
+ check-permission:
+ name: Verify Deployment Permission
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check actor permission
+ env:
+ # Prefer the org-scoped GH_TOKEN secret (needed for the org membership
+ # fallback). Falls back to the built-in github.token so the collaborator
+ # endpoint still works even if GH_TOKEN is not configured.
+ GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
+ run: |
+ ACTOR="${{ github.actor }}"
+ REPO="${{ github.repository }}"
+ ORG="${{ github.repository_owner }}"
+
+ METHOD=""
+ AUTHORIZED="false"
+
+ # Hardcoded authorized users — always allowed to deploy
+ AUTHORIZED_USERS="jmiller-moko github-actions[bot]"
+ for user in $AUTHORIZED_USERS; do
+ if [ "$ACTOR" = "$user" ]; then
+ AUTHORIZED="true"
+ METHOD="hardcoded allowlist"
+ PERMISSION="admin"
+ break
+ fi
+ done
+
+ # For other actors, check repo/org permissions via API
+ if [ "$AUTHORIZED" != "true" ]; then
+ PERMISSION=$(gh api "repos/${REPO}/collaborators/${ACTOR}/permission" \
+ --jq '.permission' 2>/dev/null)
+ METHOD="repo collaborator API"
+
+ if [ -z "$PERMISSION" ]; then
+ ORG_ROLE=$(gh api "orgs/${ORG}/memberships/${ACTOR}" \
+ --jq '.role' 2>/dev/null)
+ METHOD="org membership API"
+ if [ "$ORG_ROLE" = "owner" ]; then
+ PERMISSION="admin"
+ else
+ PERMISSION="none"
+ fi
+ fi
+
+ case "$PERMISSION" in
+ admin|maintain) AUTHORIZED="true" ;;
+ esac
+ fi
+
+ # Write detailed summary
+ {
+ echo "## 🔐 Deploy Authorization"
+ echo ""
+ echo "| Field | Value |"
+ echo "|-------|-------|"
+ echo "| **Actor** | \`${ACTOR}\` |"
+ echo "| **Repository** | \`${REPO}\` |"
+ echo "| **Permission** | \`${PERMISSION}\` |"
+ echo "| **Method** | ${METHOD} |"
+ echo "| **Authorized** | ${AUTHORIZED} |"
+ echo "| **Trigger** | \`${{ github.event_name }}\` |"
+ echo "| **Branch** | \`${{ github.ref_name }}\` |"
+ echo ""
+ } >> "$GITHUB_STEP_SUMMARY"
+
+ if [ "$AUTHORIZED" = "true" ]; then
+ echo "✅ ${ACTOR} authorized to deploy (${METHOD})" >> "$GITHUB_STEP_SUMMARY"
+ else
+ echo "❌ ${ACTOR} is NOT authorized to deploy." >> "$GITHUB_STEP_SUMMARY"
+ echo "" >> "$GITHUB_STEP_SUMMARY"
+ echo "Deployment requires one of:" >> "$GITHUB_STEP_SUMMARY"
+ echo "- Being in the hardcoded allowlist" >> "$GITHUB_STEP_SUMMARY"
+ echo "- Having \`admin\` or \`maintain\` role on the repository" >> "$GITHUB_STEP_SUMMARY"
+ exit 1
+ fi
+
+ deploy:
+ name: SFTP Deploy → Demo
+ runs-on: ubuntu-latest
+ needs: [check-permission]
+ if: >-
+ !startsWith(github.head_ref || github.ref_name, 'chore/') &&
+ (github.event_name == 'workflow_dispatch' ||
+ github.event_name == 'push' ||
+ (github.event_name == 'pull_request' &&
+ (github.event.action == 'opened' ||
+ github.event.action == 'synchronize' ||
+ github.event.action == 'reopened' ||
+ github.event.pull_request.merged == true)))
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+
+ - name: Resolve source directory
+ id: source
+ run: |
+ # Resolve source directory: src/ preferred, htdocs/ as fallback
+ if [ -d "src" ]; then
+ SRC="src"
+ elif [ -d "htdocs" ]; then
+ SRC="htdocs"
+ else
+ echo "⚠️ No src/ or htdocs/ directory found — skipping deployment"
+ echo "skip=true" >> "$GITHUB_OUTPUT"
+ exit 0
+ fi
+ COUNT=$(find "$SRC" -type f | wc -l)
+ echo "✅ Source: ${SRC}/ (${COUNT} file(s))"
+ echo "skip=false" >> "$GITHUB_OUTPUT"
+ echo "dir=${SRC}" >> "$GITHUB_OUTPUT"
+
+ - name: Preview files to deploy
+ if: steps.source.outputs.skip == 'false'
+ env:
+ SOURCE_DIR: ${{ steps.source.outputs.dir }}
+ run: |
+ # ── Convert a ftpignore-style glob line to an ERE pattern ──────────────
+ ftpignore_to_regex() {
+ local line="$1"
+ local anchored=false
+ # Strip inline comments and whitespace
+ line=$(printf '%s' "$line" | sed 's/[[:space:]]*#.*$//' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
+ [ -z "$line" ] && return
+ # Skip negation patterns (not supported)
+ [[ "$line" == !* ]] && return
+ # Trailing slash = directory marker; strip it
+ line="${line%/}"
+ # Leading slash = anchored to root; strip it
+ if [[ "$line" == /* ]]; then
+ anchored=true
+ line="${line#/}"
+ fi
+ # Escape ERE special chars, then restore glob semantics
+ local regex
+ regex=$(printf '%s' "$line" \
+ | sed 's/[.+^${}()|[\\]/\\&/g' \
+ | sed 's/\\\*\\\*/\x01/g' \
+ | sed 's/\\\*/[^\/]*/g' \
+ | sed 's/\x01/.*/g' \
+ | sed 's/\\\?/[^\/]/g')
+ if $anchored; then
+ printf '^%s(/|$)' "$regex"
+ else
+ printf '(^|/)%s(/|$)' "$regex"
+ fi
+ }
+
+ # ── Read .ftpignore (ftpignore-style globs) ─────────────────────────
+ IGNORE_PATTERNS=()
+ IGNORE_SOURCES=()
+ if [ -f ".ftpignore" ]; then
+ while IFS= read -r line; do
+ [[ "$line" =~ ^[[:space:]]*$ || "$line" =~ ^[[:space:]]*# ]] && continue
+ regex=$(ftpignore_to_regex "$line")
+ [ -n "$regex" ] && IGNORE_PATTERNS+=("$regex") && IGNORE_SOURCES+=("$line")
+ done < ".ftpignore"
+ fi
+
+ # ── Walk src/ and classify every file ────────────────────────────────
+ WILL_UPLOAD=()
+ IGNORED_FILES=()
+ while IFS= read -r -d '' file; do
+ rel="${file#${SOURCE_DIR}/}"
+ SKIP=false
+ for i in "${!IGNORE_PATTERNS[@]}"; do
+ if echo "$rel" | grep -qE "${IGNORE_PATTERNS[$i]}" 2>/dev/null; then
+ IGNORED_FILES+=("$rel | .ftpignore \`${IGNORE_SOURCES[$i]}\`")
+ SKIP=true; break
+ fi
+ done
+ $SKIP && continue
+ WILL_UPLOAD+=("$rel")
+ done < <(find "$SOURCE_DIR" -type f -print0 | sort -z)
+
+ UPLOAD_COUNT="${#WILL_UPLOAD[@]}"
+ IGNORE_COUNT="${#IGNORED_FILES[@]}"
+
+ echo "ℹ️ ${UPLOAD_COUNT} file(s) will be uploaded, ${IGNORE_COUNT} ignored"
+
+ # ── Write deployment preview to step summary ──────────────────────────
+ {
+ echo "## 📋 Deployment Preview"
+ echo ""
+ echo "| Field | Value |"
+ echo "|---|---|"
+ echo "| Source | \`${SOURCE_DIR}/\` |"
+ echo "| Files to upload | **${UPLOAD_COUNT}** |"
+ echo "| Files ignored | **${IGNORE_COUNT}** |"
+ echo ""
+ if [ "${UPLOAD_COUNT}" -gt 0 ]; then
+ echo "### 📂 Files that will be uploaded"
+ echo '```'
+ printf '%s\n' "${WILL_UPLOAD[@]}"
+ echo '```'
+ echo ""
+ fi
+ if [ "${IGNORE_COUNT}" -gt 0 ]; then
+ echo "### ⏭️ Files excluded"
+ echo "| File | Reason |"
+ echo "|---|---|"
+ for entry in "${IGNORED_FILES[@]}"; do
+ f="${entry% | *}"; r="${entry##* | }"
+ echo "| \`${f}\` | ${r} |"
+ done
+ echo ""
+ fi
+ } >> "$GITHUB_STEP_SUMMARY"
+
+ - name: Resolve SFTP host and port
+ if: steps.source.outputs.skip == 'false'
+ id: conn
+ env:
+ HOST_RAW: ${{ vars.DEMO_FTP_HOST }}
+ PORT_VAR: ${{ vars.DEMO_FTP_PORT }}
+ run: |
+ HOST="$HOST_RAW"
+ PORT="$PORT_VAR"
+
+ if [ -z "$HOST" ]; then
+ echo "⏭️ DEMO_FTP_HOST not configured — skipping demo deployment."
+ echo "skip=true" >> "$GITHUB_OUTPUT"
+ exit 0
+ fi
+
+ # Priority 1 — explicit DEMO_FTP_PORT variable
+ if [ -n "$PORT" ]; then
+ echo "ℹ️ Using explicit DEMO_FTP_PORT=${PORT}"
+
+ # Priority 2 — port embedded in DEMO_FTP_HOST (host:port)
+ elif [[ "$HOST" == *:* ]]; then
+ PORT="${HOST##*:}"
+ HOST="${HOST%:*}"
+ echo "ℹ️ Extracted port ${PORT} from DEMO_FTP_HOST"
+
+ # Priority 3 — SFTP default
+ else
+ PORT="22"
+ echo "ℹ️ No port specified — defaulting to SFTP port 22"
+ fi
+
+ echo "host=${HOST}" >> "$GITHUB_OUTPUT"
+ echo "port=${PORT}" >> "$GITHUB_OUTPUT"
+ echo "SFTP target: ${HOST}:${PORT}"
+
+ - name: Build remote path
+ if: steps.source.outputs.skip == 'false' && steps.conn.outputs.skip != 'true'
+ id: remote
+ env:
+ DEMO_FTP_PATH: ${{ vars.DEMO_FTP_PATH }}
+ DEMO_FTP_SUFFIX: ${{ vars.DEMO_FTP_SUFFIX }}
+ run: |
+ BASE="$DEMO_FTP_PATH"
+
+ if [ -z "$BASE" ]; then
+ echo "⏭️ DEMO_FTP_PATH not configured — skipping demo deployment."
+ echo "skip=true" >> "$GITHUB_OUTPUT"
+ exit 0
+ fi
+
+ # DEMO_FTP_SUFFIX is required — it identifies the remote subdirectory for this repo.
+ # Without it we cannot safely determine the deployment target.
+ if [ -z "$DEMO_FTP_SUFFIX" ]; then
+ echo "⏭️ DEMO_FTP_SUFFIX variable is not set — skipping deployment."
+ echo " Set DEMO_FTP_SUFFIX as a repo or org variable to enable deploy-demo."
+ echo "skip=true" >> "$GITHUB_OUTPUT"
+ echo "path=" >> "$GITHUB_OUTPUT"
+ exit 0
+ fi
+
+ REMOTE="${BASE%/}/${DEMO_FTP_SUFFIX#/}"
+
+ # ── Platform-specific path safety guards ──────────────────────────────
+ PLATFORM=""
+ MOKO_FILE=".github/.mokostandards"; [ ! -f "$MOKO_FILE" ] && MOKO_FILE=".mokostandards"; if [ -f "$MOKO_FILE" ]; then
+ PLATFORM=$(grep -E '^platform:' "$MOKO_FILE" | sed 's/.*:[[:space:]]*//' | tr -d '"')
+ fi
+
+ if [ "$PLATFORM" = "crm-module" ]; then
+ # Dolibarr modules must deploy under htdocs/custom/ — guard against
+ # accidentally overwriting server root or unrelated directories.
+ if [[ "$REMOTE" != *custom* ]]; then
+ echo "❌ Safety check failed: Dolibarr (crm-module) remote path must contain 'custom'."
+ echo " Current path: ${REMOTE}"
+ echo " Set DEMO_FTP_SUFFIX to the module's htdocs/custom/ subdirectory."
+ exit 1
+ fi
+ fi
+
+ if [ "$PLATFORM" = "waas-component" ]; then
+ # Joomla extensions may only deploy to the server's tmp/ directory.
+ if [[ "$REMOTE" != *tmp* ]]; then
+ echo "❌ Safety check failed: Joomla (waas-component) remote path must contain 'tmp'."
+ echo " Current path: ${REMOTE}"
+ echo " Set DEMO_FTP_SUFFIX to a path under the server tmp/ directory."
+ exit 1
+ fi
+ fi
+
+ echo "ℹ️ Remote path: ${REMOTE}"
+ echo "path=${REMOTE}" >> "$GITHUB_OUTPUT"
+
+ - name: Detect SFTP authentication method
+ if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true'
+ id: auth
+ env:
+ HAS_KEY: ${{ secrets.DEMO_FTP_KEY }}
+ HAS_PASSWORD: ${{ secrets.DEMO_FTP_PASSWORD }}
+ run: |
+ if [ -n "$HAS_KEY" ] && [ -n "$HAS_PASSWORD" ]; then
+ # Both set: key auth with password as passphrase; falls back to password-only if key fails
+ echo "method=key" >> "$GITHUB_OUTPUT"
+ echo "use_passphrase=true" >> "$GITHUB_OUTPUT"
+ echo "has_password=true" >> "$GITHUB_OUTPUT"
+ echo "ℹ️ Primary: SSH key + passphrase (DEMO_FTP_KEY / DEMO_FTP_PASSWORD)"
+ echo "ℹ️ Fallback: password-only auth if key authentication fails"
+ elif [ -n "$HAS_KEY" ]; then
+ # Key only: no passphrase, no password fallback
+ echo "method=key" >> "$GITHUB_OUTPUT"
+ echo "use_passphrase=false" >> "$GITHUB_OUTPUT"
+ echo "has_password=false" >> "$GITHUB_OUTPUT"
+ echo "ℹ️ Using SSH key authentication (DEMO_FTP_KEY, no passphrase, no fallback)"
+ elif [ -n "$HAS_PASSWORD" ]; then
+ # Password only: direct SFTP password auth
+ echo "method=password" >> "$GITHUB_OUTPUT"
+ echo "use_passphrase=false" >> "$GITHUB_OUTPUT"
+ echo "has_password=true" >> "$GITHUB_OUTPUT"
+ echo "ℹ️ Using password authentication (DEMO_FTP_PASSWORD)"
+ else
+ echo "❌ No SFTP credentials configured."
+ echo " Set DEMO_FTP_KEY (preferred) or DEMO_FTP_PASSWORD as an org-level secret."
+ exit 1
+ fi
+
+ - name: Setup PHP
+ if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true'
+ uses: shivammathur/setup-php@fcafdd6392932010c2bd5094439b8e33be2a8a09 # v2.37.0
+ with:
+ php-version: '8.1'
+ tools: composer
+
+ - name: Setup MokoStandards deploy tools
+ if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true'
+ 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
+ cd /tmp/mokostandards
+ composer install --no-dev --no-interaction --quiet
+
+ - name: Clear remote destination folder (manual only)
+ if: >-
+ steps.source.outputs.skip == 'false' &&
+ steps.remote.outputs.skip != 'true' &&
+ inputs.clear_remote == true
+ env:
+ SFTP_HOST: ${{ steps.conn.outputs.host }}
+ SFTP_PORT: ${{ steps.conn.outputs.port }}
+ SFTP_USER: ${{ vars.DEMO_FTP_USERNAME }}
+ SFTP_KEY: ${{ secrets.DEMO_FTP_KEY }}
+ SFTP_PASSWORD: ${{ secrets.DEMO_FTP_PASSWORD }}
+ AUTH_METHOD: ${{ steps.auth.outputs.method }}
+ USE_PASSPHRASE: ${{ steps.auth.outputs.use_passphrase }}
+ HAS_PASSWORD: ${{ steps.auth.outputs.has_password }}
+ REMOTE_PATH: ${{ steps.remote.outputs.path }}
+ run: |
+ cat > /tmp/moko_clear.php << 'PHPEOF'
+ login($username, $key)) {
+ if ($password !== '') {
+ echo "⚠️ Key auth failed — falling back to password\n";
+ if (!$sftp->login($username, $password)) {
+ fwrite(STDERR, "❌ Both key and password authentication failed\n");
+ exit(1);
+ }
+ echo "✅ Connected via password authentication (key fallback)\n";
+ } else {
+ fwrite(STDERR, "❌ Key authentication failed and no password fallback is available\n");
+ exit(1);
+ }
+ } else {
+ echo "✅ Connected via SSH key authentication\n";
+ }
+ } else {
+ if (!$sftp->login($username, (string) getenv('SFTP_PASSWORD'))) {
+ fwrite(STDERR, "❌ Password authentication failed\n");
+ exit(1);
+ }
+ echo "✅ Connected via password authentication\n";
+ }
+
+ // ── Recursive delete ────────────────────────────────────────────
+ function rmrf(SFTP $sftp, string $path): void
+ {
+ $entries = $sftp->nlist($path);
+ if ($entries === false) {
+ return; // path does not exist — nothing to clear
+ }
+ foreach ($entries as $name) {
+ if ($name === '.' || $name === '..') {
+ continue;
+ }
+ $entry = "{$path}/{$name}";
+ if ($sftp->is_dir($entry)) {
+ rmrf($sftp, $entry);
+ $sftp->rmdir($entry);
+ echo " 🗑️ Removed dir: {$entry}\n";
+ } else {
+ $sftp->delete($entry);
+ echo " 🗑️ Removed file: {$entry}\n";
+ }
+ }
+ }
+
+ // ── Create remote directory tree ────────────────────────────────
+ function sftpMakedirs(SFTP $sftp, string $path): void
+ {
+ $parts = array_values(array_filter(explode('/', $path), fn(string $p) => $p !== ''));
+ $current = str_starts_with($path, '/') ? '' : '';
+ foreach ($parts as $part) {
+ $current .= '/' . $part;
+ $sftp->mkdir($current); // silently returns false if already exists
+ }
+ }
+
+ rmrf($sftp, $remotePath);
+ sftpMakedirs($sftp, $remotePath);
+ echo "✅ Remote folder ready: {$remotePath}\n";
+ PHPEOF
+ php /tmp/moko_clear.php
+
+ - name: Deploy via SFTP
+ if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true'
+ env:
+ SFTP_HOST: ${{ steps.conn.outputs.host }}
+ SFTP_PORT: ${{ steps.conn.outputs.port }}
+ SFTP_USER: ${{ vars.DEMO_FTP_USERNAME }}
+ SFTP_KEY: ${{ secrets.DEMO_FTP_KEY }}
+ SFTP_PASSWORD: ${{ secrets.DEMO_FTP_PASSWORD }}
+ AUTH_METHOD: ${{ steps.auth.outputs.method }}
+ USE_PASSPHRASE: ${{ steps.auth.outputs.use_passphrase }}
+ REMOTE_PATH: ${{ steps.remote.outputs.path }}
+ SOURCE_DIR: ${{ steps.source.outputs.dir }}
+ run: |
+ # ── Write SSH key to temp file (key auth only) ────────────────────────
+ if [ "$AUTH_METHOD" = "key" ]; then
+ printf '%s' "$SFTP_KEY" > /tmp/deploy_key
+ chmod 600 /tmp/deploy_key
+ fi
+
+ # ── Generate sftp-config.json safely via jq ───────────────────────────
+ if [ "$AUTH_METHOD" = "key" ]; then
+ jq -n \
+ --arg host "$SFTP_HOST" \
+ --argjson port "${SFTP_PORT:-22}" \
+ --arg user "$SFTP_USER" \
+ --arg path "$REMOTE_PATH" \
+ --arg key "/tmp/deploy_key" \
+ '{host:$host, port:$port, user:$user, remote_path:$path, ssh_key_file:$key}' \
+ > /tmp/sftp-config.json
+ else
+ jq -n \
+ --arg host "$SFTP_HOST" \
+ --argjson port "${SFTP_PORT:-22}" \
+ --arg user "$SFTP_USER" \
+ --arg path "$REMOTE_PATH" \
+ --arg pass "$SFTP_PASSWORD" \
+ '{host:$host, port:$port, user:$user, remote_path:$path, password:$pass}' \
+ > /tmp/sftp-config.json
+ fi
+
+ # ── Write update files (demo = stable) ─────────────────────────────
+ PLATFORM=$(php /tmp/mokostandards/api/cli/platform_detect.php --path . 2>/dev/null || true)
+ VERSION=$(php /tmp/mokostandards/api/cli/version_read.php --path . 2>/dev/null || echo "unknown")
+ REPO="${{ github.repository }}"
+
+ if [ "$PLATFORM" = "crm-module" ]; then
+ printf '%s' "$VERSION" > update.txt
+ fi
+
+ if [ "$PLATFORM" = "waas-component" ]; then
+ MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1 || true)
+ if [ -n "$MANIFEST" ]; then
+ 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 '/dev/null | head -1 || true)
+ [ -n "$TARGET_PLATFORM" ] && TARGET_PLATFORM="${TARGET_PLATFORM}>"
+ [ -z "$TARGET_PLATFORM" ] && TARGET_PLATFORM=$(printf '' "/")
+
+ CLIENT_TAG=""
+ if [ -n "$EXT_CLIENT" ]; then CLIENT_TAG="${EXT_CLIENT}"; elif [ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]; then CLIENT_TAG="site"; fi
+ FOLDER_TAG=""
+ if [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ]; then FOLDER_TAG="${EXT_FOLDER}"; fi
+
+ DOWNLOAD_URL="https://github.com/${REPO}/releases/download/v${VERSION}/${EXT_ELEMENT}-${VERSION}.zip"
+ {
+ 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' " https://github.com/${REPO}"
+ printf '%s\n' ' '
+ printf '%s\n' " ${DOWNLOAD_URL}"
+ printf '%s\n' ' '
+ printf '%s\n' " ${TARGET_PLATFORM}"
+ printf '%s\n' ' Moko Consulting'
+ printf '%s\n' ' https://mokoconsulting.tech'
+ printf '%s\n' ' '
+ printf '%s\n' ''
+ } > updates.xml
+ fi
+ fi
+
+ # ── Run deploy-sftp.php from MokoStandards ────────────────────────────
+ DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json)
+ if [ "$USE_PASSPHRASE" = "true" ]; then
+ DEPLOY_ARGS+=(--key-passphrase "$SFTP_PASSWORD")
+ 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 "${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
+
+ - name: Create or update failure issue
+ if: failure() && steps.remote.outputs.skip != 'true' && steps.conn.outputs.skip != 'true'
+ 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: Demo deployment failed — ${REPO}"
+ BODY="## Demo Deployment Failed
+
+ A deployment to the demo 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 Demo Server → Run workflow**.
+
+ ---
+ *Auto-created by deploy-demo.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
+
+ - name: Deployment summary
+ if: always()
+ run: |
+ if [ "${{ steps.source.outputs.skip }}" == "true" ]; then
+ echo "### ⏭️ Deployment Skipped" >> "$GITHUB_STEP_SUMMARY"
+ echo "" >> "$GITHUB_STEP_SUMMARY"
+ echo "No \`src/\` directory found in this repository." >> "$GITHUB_STEP_SUMMARY"
+ elif [ "${{ job.status }}" == "success" ]; then
+ echo "" >> "$GITHUB_STEP_SUMMARY"
+ echo "### ✅ Demo Deployment Successful" >> "$GITHUB_STEP_SUMMARY"
+ echo "" >> "$GITHUB_STEP_SUMMARY"
+ echo "| Field | Value |" >> "$GITHUB_STEP_SUMMARY"
+ echo "|-------|-------|" >> "$GITHUB_STEP_SUMMARY"
+ echo "| Host | \`${{ steps.conn.outputs.host }}:${{ steps.conn.outputs.port }}\` |" >> "$GITHUB_STEP_SUMMARY"
+ echo "| Remote path | \`${{ steps.remote.outputs.path }}\` |" >> "$GITHUB_STEP_SUMMARY"
+ echo "| Source | \`src/\` |" >> "$GITHUB_STEP_SUMMARY"
+ echo "| Trigger | ${{ github.event_name }} |" >> "$GITHUB_STEP_SUMMARY"
+ echo "| Auth | ${{ steps.auth.outputs.method }} |" >> "$GITHUB_STEP_SUMMARY"
+ echo "| Clear remote | ${{ inputs.clear_remote || 'false' }} |" >> "$GITHUB_STEP_SUMMARY"
+ else
+ echo "### ❌ Demo Deployment Failed" >> "$GITHUB_STEP_SUMMARY"
+ echo "" >> "$GITHUB_STEP_SUMMARY"
+ echo "Check the job log above for error details." >> "$GITHUB_STEP_SUMMARY"
+ fi
--
2.49.1
From b608b00125d0cbcb17e6911bb29e9f48b0dd141d Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Thu, 9 Apr 2026 10:51:15 -0500
Subject: [PATCH 19/40] chore: add .github/workflows/deploy-rs.yml from
MokoStandards
---
.github/workflows/deploy-rs.yml | 661 ++++++++++++++++++++++++++++++++
1 file changed, 661 insertions(+)
create mode 100644 .github/workflows/deploy-rs.yml
diff --git a/.github/workflows/deploy-rs.yml b/.github/workflows/deploy-rs.yml
new file mode 100644
index 0000000..233303d
--- /dev/null
+++ b/.github/workflows/deploy-rs.yml
@@ -0,0 +1,661 @@
+# Copyright (C) 2026 Moko Consulting
+#
+# This file is part of a Moko Consulting project.
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+# FILE INFORMATION
+# DEFGROUP: GitHub.Workflow
+# INGROUP: MokoStandards.Deploy
+# REPO: https://github.com/mokoconsulting-tech/MokoStandards
+# PATH: /templates/workflows/shared/deploy-rs.yml
+# VERSION: 04.05.13
+# 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.
+
+name: Deploy to RS Server (SFTP)
+
+# Deploys the contents of the src/ directory to the release staging server via SFTP.
+# Triggers on push/merge to main — deploys the production-ready build to the release staging server.
+#
+# Required org-level variables: RS_FTP_HOST, RS_FTP_PATH, RS_FTP_USERNAME
+# 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 .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.
+
+on:
+ push:
+ branches:
+ - main
+ - master
+ paths:
+ - 'src/**'
+ - 'htdocs/**'
+ pull_request:
+ types: [opened, synchronize, reopened, closed]
+ branches:
+ - main
+ - master
+ paths:
+ - 'src/**'
+ - 'htdocs/**'
+ workflow_dispatch:
+ inputs:
+ clear_remote:
+ description: 'Delete all files inside the remote destination folder before uploading'
+ required: false
+ default: false
+ type: boolean
+
+permissions:
+ contents: read
+ pull-requests: write
+
+env:
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
+
+jobs:
+ check-permission:
+ name: Verify Deployment Permission
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check actor permission
+ env:
+ # Prefer the org-scoped GH_TOKEN secret (needed for the org membership
+ # fallback). Falls back to the built-in github.token so the collaborator
+ # endpoint still works even if GH_TOKEN is not configured.
+ GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
+ run: |
+ ACTOR="${{ github.actor }}"
+ REPO="${{ github.repository }}"
+ ORG="${{ github.repository_owner }}"
+
+ METHOD=""
+ AUTHORIZED="false"
+
+ # Hardcoded authorized users — always allowed to deploy
+ AUTHORIZED_USERS="jmiller-moko github-actions[bot]"
+ for user in $AUTHORIZED_USERS; do
+ if [ "$ACTOR" = "$user" ]; then
+ AUTHORIZED="true"
+ METHOD="hardcoded allowlist"
+ PERMISSION="admin"
+ break
+ fi
+ done
+
+ # For other actors, check repo/org permissions via API
+ if [ "$AUTHORIZED" != "true" ]; then
+ PERMISSION=$(gh api "repos/${REPO}/collaborators/${ACTOR}/permission" \
+ --jq '.permission' 2>/dev/null)
+ METHOD="repo collaborator API"
+
+ if [ -z "$PERMISSION" ]; then
+ ORG_ROLE=$(gh api "orgs/${ORG}/memberships/${ACTOR}" \
+ --jq '.role' 2>/dev/null)
+ METHOD="org membership API"
+ if [ "$ORG_ROLE" = "owner" ]; then
+ PERMISSION="admin"
+ else
+ PERMISSION="none"
+ fi
+ fi
+
+ case "$PERMISSION" in
+ admin|maintain) AUTHORIZED="true" ;;
+ esac
+ fi
+
+ # Write detailed summary
+ {
+ echo "## 🔐 Deploy Authorization"
+ echo ""
+ echo "| Field | Value |"
+ echo "|-------|-------|"
+ echo "| **Actor** | \`${ACTOR}\` |"
+ echo "| **Repository** | \`${REPO}\` |"
+ echo "| **Permission** | \`${PERMISSION}\` |"
+ echo "| **Method** | ${METHOD} |"
+ echo "| **Authorized** | ${AUTHORIZED} |"
+ echo "| **Trigger** | \`${{ github.event_name }}\` |"
+ echo "| **Branch** | \`${{ github.ref_name }}\` |"
+ echo ""
+ } >> "$GITHUB_STEP_SUMMARY"
+
+ if [ "$AUTHORIZED" = "true" ]; then
+ echo "✅ ${ACTOR} authorized to deploy (${METHOD})" >> "$GITHUB_STEP_SUMMARY"
+ else
+ echo "❌ ${ACTOR} is NOT authorized to deploy." >> "$GITHUB_STEP_SUMMARY"
+ echo "" >> "$GITHUB_STEP_SUMMARY"
+ echo "Deployment requires one of:" >> "$GITHUB_STEP_SUMMARY"
+ echo "- Being in the hardcoded allowlist" >> "$GITHUB_STEP_SUMMARY"
+ echo "- Having \`admin\` or \`maintain\` role on the repository" >> "$GITHUB_STEP_SUMMARY"
+ exit 1
+ fi
+
+ deploy:
+ name: SFTP Deploy → RS
+ runs-on: ubuntu-latest
+ needs: [check-permission]
+ if: >-
+ !startsWith(github.head_ref || github.ref_name, 'chore/') &&
+ (github.event_name == 'workflow_dispatch' ||
+ github.event_name == 'push' ||
+ (github.event_name == 'pull_request' &&
+ (github.event.action == 'opened' ||
+ github.event.action == 'synchronize' ||
+ github.event.action == 'reopened' ||
+ github.event.pull_request.merged == true)))
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+
+ - name: Resolve source directory
+ id: source
+ run: |
+ # Resolve source directory: src/ preferred, htdocs/ as fallback
+ if [ -d "src" ]; then
+ SRC="src"
+ elif [ -d "htdocs" ]; then
+ SRC="htdocs"
+ else
+ echo "⚠️ No src/ or htdocs/ directory found — skipping deployment"
+ echo "skip=true" >> "$GITHUB_OUTPUT"
+ exit 0
+ fi
+ COUNT=$(find "$SRC" -type f | wc -l)
+ echo "✅ Source: ${SRC}/ (${COUNT} file(s))"
+ echo "skip=false" >> "$GITHUB_OUTPUT"
+ echo "dir=${SRC}" >> "$GITHUB_OUTPUT"
+
+ - name: Preview files to deploy
+ if: steps.source.outputs.skip == 'false'
+ env:
+ SOURCE_DIR: ${{ steps.source.outputs.dir }}
+ run: |
+ # ── Convert a ftpignore-style glob line to an ERE pattern ──────────────
+ ftpignore_to_regex() {
+ local line="$1"
+ local anchored=false
+ # Strip inline comments and whitespace
+ line=$(printf '%s' "$line" | sed 's/[[:space:]]*#.*$//' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
+ [ -z "$line" ] && return
+ # Skip negation patterns (not supported)
+ [[ "$line" == !* ]] && return
+ # Trailing slash = directory marker; strip it
+ line="${line%/}"
+ # Leading slash = anchored to root; strip it
+ if [[ "$line" == /* ]]; then
+ anchored=true
+ line="${line#/}"
+ fi
+ # Escape ERE special chars, then restore glob semantics
+ local regex
+ regex=$(printf '%s' "$line" \
+ | sed 's/[.+^${}()|[\\]/\\&/g' \
+ | sed 's/\\\*\\\*/\x01/g' \
+ | sed 's/\\\*/[^\/]*/g' \
+ | sed 's/\x01/.*/g' \
+ | sed 's/\\\?/[^\/]/g')
+ if $anchored; then
+ printf '^%s(/|$)' "$regex"
+ else
+ printf '(^|/)%s(/|$)' "$regex"
+ fi
+ }
+
+ # ── Read .ftpignore (ftpignore-style globs) ─────────────────────────
+ IGNORE_PATTERNS=()
+ IGNORE_SOURCES=()
+ if [ -f ".ftpignore" ]; then
+ while IFS= read -r line; do
+ [[ "$line" =~ ^[[:space:]]*$ || "$line" =~ ^[[:space:]]*# ]] && continue
+ regex=$(ftpignore_to_regex "$line")
+ [ -n "$regex" ] && IGNORE_PATTERNS+=("$regex") && IGNORE_SOURCES+=("$line")
+ done < ".ftpignore"
+ fi
+
+ # ── Walk src/ and classify every file ────────────────────────────────
+ WILL_UPLOAD=()
+ IGNORED_FILES=()
+ while IFS= read -r -d '' file; do
+ rel="${file#${SOURCE_DIR}/}"
+ SKIP=false
+ for i in "${!IGNORE_PATTERNS[@]}"; do
+ if echo "$rel" | grep -qE "${IGNORE_PATTERNS[$i]}" 2>/dev/null; then
+ IGNORED_FILES+=("$rel | .ftpignore \`${IGNORE_SOURCES[$i]}\`")
+ SKIP=true; break
+ fi
+ done
+ $SKIP && continue
+ WILL_UPLOAD+=("$rel")
+ done < <(find "$SOURCE_DIR" -type f -print0 | sort -z)
+
+ UPLOAD_COUNT="${#WILL_UPLOAD[@]}"
+ IGNORE_COUNT="${#IGNORED_FILES[@]}"
+
+ echo "ℹ️ ${UPLOAD_COUNT} file(s) will be uploaded, ${IGNORE_COUNT} ignored"
+
+ # ── Write deployment preview to step summary ──────────────────────────
+ {
+ echo "## 📋 Deployment Preview"
+ echo ""
+ echo "| Field | Value |"
+ echo "|---|---|"
+ echo "| Source | \`${SOURCE_DIR}/\` |"
+ echo "| Files to upload | **${UPLOAD_COUNT}** |"
+ echo "| Files ignored | **${IGNORE_COUNT}** |"
+ echo ""
+ if [ "${UPLOAD_COUNT}" -gt 0 ]; then
+ echo "### 📂 Files that will be uploaded"
+ echo '```'
+ printf '%s\n' "${WILL_UPLOAD[@]}"
+ echo '```'
+ echo ""
+ fi
+ if [ "${IGNORE_COUNT}" -gt 0 ]; then
+ echo "### ⏭️ Files excluded"
+ echo "| File | Reason |"
+ echo "|---|---|"
+ for entry in "${IGNORED_FILES[@]}"; do
+ f="${entry% | *}"; r="${entry##* | }"
+ echo "| \`${f}\` | ${r} |"
+ done
+ echo ""
+ fi
+ } >> "$GITHUB_STEP_SUMMARY"
+
+ - name: Resolve SFTP host and port
+ if: steps.source.outputs.skip == 'false'
+ id: conn
+ env:
+ HOST_RAW: ${{ vars.RS_FTP_HOST }}
+ PORT_VAR: ${{ vars.RS_FTP_PORT }}
+ run: |
+ HOST="$HOST_RAW"
+ PORT="$PORT_VAR"
+
+ if [ -z "$HOST" ]; then
+ echo "⏭️ RS_FTP_HOST not configured — skipping RS deployment."
+ echo "skip=true" >> "$GITHUB_OUTPUT"
+ exit 0
+ fi
+
+ # Priority 1 — explicit RS_FTP_PORT variable
+ if [ -n "$PORT" ]; then
+ echo "ℹ️ Using explicit RS_FTP_PORT=${PORT}"
+
+ # Priority 2 — port embedded in RS_FTP_HOST (host:port)
+ elif [[ "$HOST" == *:* ]]; then
+ PORT="${HOST##*:}"
+ HOST="${HOST%:*}"
+ echo "ℹ️ Extracted port ${PORT} from RS_FTP_HOST"
+
+ # Priority 3 — SFTP default
+ else
+ PORT="22"
+ echo "ℹ️ No port specified — defaulting to SFTP port 22"
+ fi
+
+ echo "host=${HOST}" >> "$GITHUB_OUTPUT"
+ echo "port=${PORT}" >> "$GITHUB_OUTPUT"
+ echo "SFTP target: ${HOST}:${PORT}"
+
+ - name: Build remote path
+ if: steps.source.outputs.skip == 'false' && steps.conn.outputs.skip != 'true'
+ id: remote
+ env:
+ RS_FTP_PATH: ${{ vars.RS_FTP_PATH }}
+ RS_FTP_SUFFIX: ${{ vars.RS_FTP_SUFFIX }}
+ run: |
+ BASE="$RS_FTP_PATH"
+
+ if [ -z "$BASE" ]; then
+ echo "⏭️ RS_FTP_PATH not configured — skipping RS deployment."
+ echo "skip=true" >> "$GITHUB_OUTPUT"
+ exit 0
+ fi
+
+ # RS_FTP_SUFFIX is required — it identifies the remote subdirectory for this repo.
+ # Without it we cannot safely determine the deployment target.
+ if [ -z "$RS_FTP_SUFFIX" ]; then
+ echo "⏭️ RS_FTP_SUFFIX variable is not set — skipping deployment."
+ echo " Set RS_FTP_SUFFIX as a repo or org variable to enable deploy-rs."
+ echo "skip=true" >> "$GITHUB_OUTPUT"
+ echo "path=" >> "$GITHUB_OUTPUT"
+ exit 0
+ fi
+
+ REMOTE="${BASE%/}/${RS_FTP_SUFFIX#/}"
+
+ # ── Platform-specific path safety guards ──────────────────────────────
+ PLATFORM=""
+ MOKO_FILE=".github/.mokostandards"; [ ! -f "$MOKO_FILE" ] && MOKO_FILE=".mokostandards"; if [ -f "$MOKO_FILE" ]; then
+ PLATFORM=$(grep -E '^platform:' "$MOKO_FILE" | sed 's/.*:[[:space:]]*//' | tr -d '"')
+ fi
+
+ # RS deployment: no path restrictions for any platform
+
+ echo "ℹ️ Remote path: ${REMOTE}"
+ echo "path=${REMOTE}" >> "$GITHUB_OUTPUT"
+
+ - name: Detect SFTP authentication method
+ if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true'
+ id: auth
+ env:
+ HAS_KEY: ${{ secrets.RS_FTP_KEY }}
+ HAS_PASSWORD: ${{ secrets.RS_FTP_PASSWORD }}
+ run: |
+ if [ -n "$HAS_KEY" ] && [ -n "$HAS_PASSWORD" ]; then
+ # Both set: key auth with password as passphrase; falls back to password-only if key fails
+ echo "method=key" >> "$GITHUB_OUTPUT"
+ echo "use_passphrase=true" >> "$GITHUB_OUTPUT"
+ echo "has_password=true" >> "$GITHUB_OUTPUT"
+ echo "ℹ️ Primary: SSH key + passphrase (RS_FTP_KEY / RS_FTP_PASSWORD)"
+ echo "ℹ️ Fallback: password-only auth if key authentication fails"
+ elif [ -n "$HAS_KEY" ]; then
+ # Key only: no passphrase, no password fallback
+ echo "method=key" >> "$GITHUB_OUTPUT"
+ echo "use_passphrase=false" >> "$GITHUB_OUTPUT"
+ echo "has_password=false" >> "$GITHUB_OUTPUT"
+ echo "ℹ️ Using SSH key authentication (RS_FTP_KEY, no passphrase, no fallback)"
+ elif [ -n "$HAS_PASSWORD" ]; then
+ # Password only: direct SFTP password auth
+ echo "method=password" >> "$GITHUB_OUTPUT"
+ echo "use_passphrase=false" >> "$GITHUB_OUTPUT"
+ echo "has_password=true" >> "$GITHUB_OUTPUT"
+ echo "ℹ️ Using password authentication (RS_FTP_PASSWORD)"
+ else
+ echo "❌ No SFTP credentials configured."
+ echo " Set RS_FTP_KEY (preferred) or RS_FTP_PASSWORD as an org-level secret."
+ exit 1
+ fi
+
+ - name: Setup PHP
+ if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true'
+ uses: shivammathur/setup-php@fcafdd6392932010c2bd5094439b8e33be2a8a09 # v2.37.0
+ with:
+ php-version: '8.1'
+ tools: composer
+
+ - name: Setup MokoStandards deploy tools
+ if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true'
+ 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
+ cd /tmp/mokostandards
+ composer install --no-dev --no-interaction --quiet
+
+ - name: Clear remote destination folder (manual only)
+ if: >-
+ steps.source.outputs.skip == 'false' &&
+ steps.remote.outputs.skip != 'true' &&
+ inputs.clear_remote == true
+ env:
+ SFTP_HOST: ${{ steps.conn.outputs.host }}
+ SFTP_PORT: ${{ steps.conn.outputs.port }}
+ SFTP_USER: ${{ vars.RS_FTP_USERNAME }}
+ SFTP_KEY: ${{ secrets.RS_FTP_KEY }}
+ SFTP_PASSWORD: ${{ secrets.RS_FTP_PASSWORD }}
+ AUTH_METHOD: ${{ steps.auth.outputs.method }}
+ USE_PASSPHRASE: ${{ steps.auth.outputs.use_passphrase }}
+ HAS_PASSWORD: ${{ steps.auth.outputs.has_password }}
+ REMOTE_PATH: ${{ steps.remote.outputs.path }}
+ run: |
+ cat > /tmp/moko_clear.php << 'PHPEOF'
+ login($username, $key)) {
+ if ($password !== '') {
+ echo "⚠️ Key auth failed — falling back to password\n";
+ if (!$sftp->login($username, $password)) {
+ fwrite(STDERR, "❌ Both key and password authentication failed\n");
+ exit(1);
+ }
+ echo "✅ Connected via password authentication (key fallback)\n";
+ } else {
+ fwrite(STDERR, "❌ Key authentication failed and no password fallback is available\n");
+ exit(1);
+ }
+ } else {
+ echo "✅ Connected via SSH key authentication\n";
+ }
+ } else {
+ if (!$sftp->login($username, (string) getenv('SFTP_PASSWORD'))) {
+ fwrite(STDERR, "❌ Password authentication failed\n");
+ exit(1);
+ }
+ echo "✅ Connected via password authentication\n";
+ }
+
+ // ── Recursive delete ────────────────────────────────────────────
+ function rmrf(SFTP $sftp, string $path): void
+ {
+ $entries = $sftp->nlist($path);
+ if ($entries === false) {
+ return; // path does not exist — nothing to clear
+ }
+ foreach ($entries as $name) {
+ if ($name === '.' || $name === '..') {
+ continue;
+ }
+ $entry = "{$path}/{$name}";
+ if ($sftp->is_dir($entry)) {
+ rmrf($sftp, $entry);
+ $sftp->rmdir($entry);
+ echo " 🗑️ Removed dir: {$entry}\n";
+ } else {
+ $sftp->delete($entry);
+ echo " 🗑️ Removed file: {$entry}\n";
+ }
+ }
+ }
+
+ // ── Create remote directory tree ────────────────────────────────
+ function sftpMakedirs(SFTP $sftp, string $path): void
+ {
+ $parts = array_values(array_filter(explode('/', $path), fn(string $p) => $p !== ''));
+ $current = str_starts_with($path, '/') ? '' : '';
+ foreach ($parts as $part) {
+ $current .= '/' . $part;
+ $sftp->mkdir($current); // silently returns false if already exists
+ }
+ }
+
+ rmrf($sftp, $remotePath);
+ sftpMakedirs($sftp, $remotePath);
+ echo "✅ Remote folder ready: {$remotePath}\n";
+ PHPEOF
+ php /tmp/moko_clear.php
+
+ - name: Deploy via SFTP
+ if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true'
+ env:
+ SFTP_HOST: ${{ steps.conn.outputs.host }}
+ SFTP_PORT: ${{ steps.conn.outputs.port }}
+ SFTP_USER: ${{ vars.RS_FTP_USERNAME }}
+ SFTP_KEY: ${{ secrets.RS_FTP_KEY }}
+ SFTP_PASSWORD: ${{ secrets.RS_FTP_PASSWORD }}
+ AUTH_METHOD: ${{ steps.auth.outputs.method }}
+ USE_PASSPHRASE: ${{ steps.auth.outputs.use_passphrase }}
+ REMOTE_PATH: ${{ steps.remote.outputs.path }}
+ SOURCE_DIR: ${{ steps.source.outputs.dir }}
+ run: |
+ # ── Write SSH key to temp file (key auth only) ────────────────────────
+ if [ "$AUTH_METHOD" = "key" ]; then
+ printf '%s' "$SFTP_KEY" > /tmp/deploy_key
+ chmod 600 /tmp/deploy_key
+ fi
+
+ # ── Generate sftp-config.json safely via jq ───────────────────────────
+ if [ "$AUTH_METHOD" = "key" ]; then
+ jq -n \
+ --arg host "$SFTP_HOST" \
+ --argjson port "${SFTP_PORT:-22}" \
+ --arg user "$SFTP_USER" \
+ --arg path "$REMOTE_PATH" \
+ --arg key "/tmp/deploy_key" \
+ '{host:$host, port:$port, user:$user, remote_path:$path, ssh_key_file:$key}' \
+ > /tmp/sftp-config.json
+ else
+ jq -n \
+ --arg host "$SFTP_HOST" \
+ --argjson port "${SFTP_PORT:-22}" \
+ --arg user "$SFTP_USER" \
+ --arg path "$REMOTE_PATH" \
+ --arg pass "$SFTP_PASSWORD" \
+ '{host:$host, port:$port, user:$user, remote_path:$path, password:$pass}' \
+ > /tmp/sftp-config.json
+ fi
+
+ # ── Run deploy-sftp.php from MokoStandards ────────────────────────────
+ DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json)
+ if [ "$USE_PASSPHRASE" = "true" ]; then
+ DEPLOY_ARGS+=(--key-passphrase "$SFTP_PASSWORD")
+ 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 "${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
+
+ - name: Create or update failure issue
+ if: failure() && steps.remote.outputs.skip != 'true' && steps.conn.outputs.skip != 'true'
+ 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: RS deployment failed — ${REPO}"
+ BODY="## RS Deployment Failed
+
+ A deployment to the RS 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 RS Server → Run workflow**.
+
+ ---
+ *Auto-created by deploy-rs.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 deploy-failure issue (any state — reopen if closed)
+ 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
+
+ - name: Deployment summary
+ if: always()
+ run: |
+ if [ "${{ steps.source.outputs.skip }}" == "true" ]; then
+ echo "### ⏭️ Deployment Skipped" >> "$GITHUB_STEP_SUMMARY"
+ echo "" >> "$GITHUB_STEP_SUMMARY"
+ echo "No \`src/\` directory found in this repository." >> "$GITHUB_STEP_SUMMARY"
+ elif [ "${{ job.status }}" == "success" ]; then
+ echo "" >> "$GITHUB_STEP_SUMMARY"
+ echo "### ✅ RS Deployment Successful" >> "$GITHUB_STEP_SUMMARY"
+ echo "" >> "$GITHUB_STEP_SUMMARY"
+ echo "| Field | Value |" >> "$GITHUB_STEP_SUMMARY"
+ echo "|-------|-------|" >> "$GITHUB_STEP_SUMMARY"
+ echo "| Host | \`${{ steps.conn.outputs.host }}:${{ steps.conn.outputs.port }}\` |" >> "$GITHUB_STEP_SUMMARY"
+ echo "| Remote path | \`${{ steps.remote.outputs.path }}\` |" >> "$GITHUB_STEP_SUMMARY"
+ echo "| Source | \`src/\` |" >> "$GITHUB_STEP_SUMMARY"
+ echo "| Trigger | ${{ github.event_name }} |" >> "$GITHUB_STEP_SUMMARY"
+ echo "| Auth | ${{ steps.auth.outputs.method }} |" >> "$GITHUB_STEP_SUMMARY"
+ echo "| Clear remote | ${{ inputs.clear_remote || 'false' }} |" >> "$GITHUB_STEP_SUMMARY"
+ else
+ echo "### ❌ RS Deployment Failed" >> "$GITHUB_STEP_SUMMARY"
+ echo "" >> "$GITHUB_STEP_SUMMARY"
+ echo "Check the job log above for error details." >> "$GITHUB_STEP_SUMMARY"
+ fi
--
2.49.1
From 4883345a79a18f8241de3a73eac3100fb2de2c90 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Thu, 9 Apr 2026 10:51:15 -0500
Subject: [PATCH 20/40] chore: update
.github/workflows/sync-version-on-merge.yml from MokoStandards
---
.github/workflows/sync-version-on-merge.yml | 12 +++++-------
1 file changed, 5 insertions(+), 7 deletions(-)
diff --git a/.github/workflows/sync-version-on-merge.yml b/.github/workflows/sync-version-on-merge.yml
index 4761168..668c608 100644
--- a/.github/workflows/sync-version-on-merge.yml
+++ b/.github/workflows/sync-version-on-merge.yml
@@ -8,8 +8,8 @@
# DEFGROUP: GitHub.Workflow
# INGROUP: MokoStandards.Automation
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
-# PATH: /templates/workflows/shared/sync-version-on-merge.yml.template
-# VERSION: 04.06.00
+# PATH: /templates/workflows/shared/sync-version-on-merge.yml
+# VERSION: 04.05.13
# 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.
@@ -17,10 +17,10 @@
name: Sync Version from README
on:
- pull_request:
- types: [closed]
+ push:
branches:
- main
+ - master
workflow_dispatch:
inputs:
dry_run:
@@ -39,8 +39,6 @@ jobs:
sync-version:
name: Propagate README version
runs-on: ubuntu-latest
- if: >-
- github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch'
steps:
- name: Checkout repository
@@ -67,7 +65,7 @@ jobs:
composer install --no-dev --no-interaction --quiet
- name: Auto-bump patch version
- if: ${{ github.event_name != 'workflow_dispatch' && github.actor != 'github-actions[bot]' }}
+ if: ${{ github.event_name == 'push' && github.actor != 'github-actions[bot]' }}
run: |
if git diff --name-only HEAD~1 HEAD 2>/dev/null | grep -q '^README\.md$'; then
echo "README.md changed in this push — skipping auto-bump"
--
2.49.1
From 4ce29b2e4a7c38429e55da0a0acc9574ed9b9832 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Thu, 9 Apr 2026 10:51:16 -0500
Subject: [PATCH 21/40] chore: update .github/workflows/auto-release.yml from
MokoStandards
---
.github/workflows/auto-release.yml | 255 ++---------------------------
1 file changed, 16 insertions(+), 239 deletions(-)
diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml
index 62eff0f..7098b62 100644
--- a/.github/workflows/auto-release.yml
+++ b/.github/workflows/auto-release.yml
@@ -6,24 +6,22 @@
# DEFGROUP: GitHub.Workflow
# INGROUP: MokoStandards.Release
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
-# PATH: /templates/workflows/joomla/auto-release.yml.template
-# VERSION: 04.06.00
-# BRIEF: Joomla build & release — ZIP package, updates.xml, SHA-256 checksum
+# PATH: /templates/workflows/shared/auto-release.yml
+# VERSION: 04.05.13
+# BRIEF: Generic build & release pipeline — version branch, platform version, badges, tag, release
#
# +========================================================================+
-# | BUILD & RELEASE PIPELINE (JOOMLA) |
+# | 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 (Joomla ) |
+# | 3. Set platform version |
# | 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. |
@@ -35,14 +33,13 @@
name: Build & Release
on:
- pull_request:
- types: [closed]
+ push:
branches:
- main
+ - master
paths:
- 'src/**'
- 'htdocs/**'
- workflow_dispatch:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
@@ -55,7 +52,8 @@ jobs:
name: Build & Release Pipeline
runs-on: ubuntu-latest
if: >-
- github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch'
+ !contains(github.event.head_commit.message, '[skip ci]') &&
+ github.actor != 'github-actions[bot]'
steps:
- name: Checkout repository
@@ -143,11 +141,11 @@ jobs:
VERSION="${{ steps.version.outputs.version }}"
ERRORS=0
- echo "## Pre-Release Sanity Checks (Joomla)" >> $GITHUB_STEP_SUMMARY
+ echo "## Pre-Release Sanity Checks" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# -- Version drift check (must pass before release) --------
- README_VER=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1)
+ 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))
@@ -156,7 +154,7 @@ jobs:
fi
# Check CHANGELOG version matches
- CL_VER=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' CHANGELOG.md 2>/dev/null | head -1)
+ 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))
@@ -164,7 +162,7 @@ jobs:
# Check composer.json version if present
if [ -f "composer.json" ]; then
- COMP_VER=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' composer.json 2>/dev/null | head -1)
+ 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))
@@ -185,30 +183,6 @@ jobs:
echo "- Source directory present" >> $GITHUB_STEP_SUMMARY
fi
- # -- Joomla: manifest version drift --------
- MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1)
- if [ -n "$MANIFEST" ]; then
- XML_VER=$(sed -n 's/.*\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
- if [ -n "$XML_VER" ] && [ "$XML_VER" != "$VERSION" ]; then
- echo "- Manifest drift: \`${XML_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
- ERRORS=$((ERRORS+1))
- else
- echo "- Manifest version: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
- fi
- fi
-
- # -- 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
-
- # -- Joomla: extension type check --------
- TYPE=$(sed -n 's/.*]*type="\([^"]*\)".*/\1/p' "$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
@@ -259,120 +233,6 @@ jobs:
fi
done
- # -- 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: |
- VERSION="${{ steps.version.outputs.version }}"
- REPO="${{ github.repository }}"
-
- # -- 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
-
- # Extract fields using sed (portable — no grep -P)
- EXT_NAME=$(sed -n 's/.*\([^<]*\)<\/name>.*/\1/p' "$MANIFEST" | head -1)
- EXT_TYPE=$(sed -n 's/.*]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
- EXT_ELEMENT=$(sed -n 's/.*\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" | head -1)
- EXT_CLIENT=$(sed -n 's/.*]*client="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
- EXT_FOLDER=$(sed -n 's/.*]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
- TARGET_PLATFORM=$(sed -n 's/.*\(\).*/\1/p' "$MANIFEST" | head -1)
- PHP_MINIMUM=$(sed -n 's/.*\([^<]*\)<\/php_minimum>.*/\1/p' "$MANIFEST" | head -1)
-
- # Fallbacks
- [ -z "$EXT_NAME" ] && EXT_NAME="${{ github.event.repository.name }}"
- [ -z "$EXT_TYPE" ] && EXT_TYPE="component"
-
- # Templates/modules don't have — derive from (lowercased)
- if [ -z "$EXT_ELEMENT" ]; then
- EXT_ELEMENT=$(echo "$EXT_NAME" | tr '[:upper:]' '[:lower:]' | tr -d ' ')
- 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 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 ──────────────
- # Extract existing entries for other stability levels
- # Order reflects release workflow: development → alpha → beta → rc → stable
- if [ -f "updates.xml" ]; then
- printf 'import re, sys\n' > /tmp/extract.py
- printf 'with open("updates.xml") as f: c = f.read()\n' >> /tmp/extract.py
- printf '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
- fi
- DEV_ENTRY=$(python3 /tmp/extract.py development 2>/dev/null || true)
- ALPHA_ENTRY=$(python3 /tmp/extract.py alpha 2>/dev/null || true)
- BETA_ENTRY=$(python3 /tmp/extract.py beta 2>/dev/null || true)
- RC_ENTRY=$(python3 /tmp/extract.py rc 2>/dev/null || true)
-
- {
- printf '%s\n' ''
- printf '%s\n' ''
- [ -n "$DEV_ENTRY" ] && echo "$DEV_ENTRY"
- [ -n "$ALPHA_ENTRY" ] && echo "$ALPHA_ENTRY"
- [ -n "$BETA_ENTRY" ] && echo "$BETA_ENTRY"
- [ -n "$RC_ENTRY" ] && echo "$RC_ENTRY"
- cat /tmp/stable_entry.xml
- printf '%s\n' ''
- } > updates.xml
-
- echo "updates.xml: ${VERSION} (stable + rc/dev preserved)" >> $GITHUB_STEP_SUMMARY
-
# -- Commit all changes ---------------------------------------------------
- name: Commit release changes
if: >-
@@ -430,14 +290,14 @@ jobs:
EXISTING=$(gh release view "$RELEASE_TAG" --json tagName -q .tagName 2>/dev/null || true)
if [ -z "$EXISTING" ]; then
- # First release for this major
+ # First release for this major: create GitHub Release
gh release create "$RELEASE_TAG" \
--title "v${MAJOR} (latest: ${VERSION})" \
--notes-file /tmp/release_notes.md \
--target "$BRANCH"
echo "Release created: ${RELEASE_TAG} (${VERSION})" >> $GITHUB_STEP_SUMMARY
else
- # Append version notes to existing major release
+ # Update existing major release with new version info
CURRENT_NOTES=$(gh release view "$RELEASE_TAG" --json body -q .body 2>/dev/null || true)
{
echo "$CURRENT_NOTES"
@@ -454,89 +314,6 @@ jobs:
echo "Release updated: ${RELEASE_TAG} -> ${VERSION}" >> $GITHUB_STEP_SUMMARY
fi
- # -- 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=$(sed -n 's/.*\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1 || basename "$MANIFEST" .xml)
- ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip"
- TAR_NAME="${EXT_ELEMENT}-${VERSION}.tar.gz"
-
- # -- Build install packages from src/ ----------------------------
- SOURCE_DIR="src"
- [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
- [ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ — skipping package"; exit 0; }
-
- EXCLUDES=".ftpignore sftp-config* *.ppk *.pem *.key .env*"
-
- # ZIP package
- cd "$SOURCE_DIR"
- zip -r "/tmp/${ZIP_NAME}" . -x $EXCLUDES
- cd ..
-
- # tar.gz package
- tar -czf "/tmp/${TAR_NAME}" -C "$SOURCE_DIR" \
- --exclude='.ftpignore' --exclude='sftp-config*' \
- --exclude='*.ppk' --exclude='*.pem' --exclude='*.key' --exclude='.env*' .
-
- ZIP_SIZE=$(stat -c%s "/tmp/${ZIP_NAME}" 2>/dev/null || stat -f%z "/tmp/${ZIP_NAME}" 2>/dev/null || echo "unknown")
- TAR_SIZE=$(stat -c%s "/tmp/${TAR_NAME}" 2>/dev/null || stat -f%z "/tmp/${TAR_NAME}" 2>/dev/null || echo "unknown")
-
- # -- Calculate SHA-256 for both ----------------------------------
- SHA256_ZIP=$(sha256sum "/tmp/${ZIP_NAME}" | cut -d' ' -f1)
- SHA256_TAR=$(sha256sum "/tmp/${TAR_NAME}" | cut -d' ' -f1)
-
- # -- Upload both to release tag ----------------------------------
- gh release upload "$RELEASE_TAG" "/tmp/${ZIP_NAME}" --clobber 2>/dev/null || true
- gh release upload "$RELEASE_TAG" "/tmp/${TAR_NAME}" --clobber 2>/dev/null || true
-
- # -- Update updates.xml with both download formats ---------------
- if [ -f "updates.xml" ]; then
- ZIP_URL="https://github.com/${REPO}/releases/download/${RELEASE_TAG}/${ZIP_NAME}"
- TAR_URL="https://github.com/${REPO}/releases/download/${RELEASE_TAG}/${TAR_NAME}"
-
- # Replace downloads block with both formats + SHA
- sed -i "s|.*|\n ${ZIP_URL}\n ${TAR_URL}\n |" updates.xml 2>/dev/null || true
- if grep -q '' updates.xml; then
- sed -i "s|.*|sha256:${SHA256_ZIP}|" updates.xml
- else
- sed -i "s||\n sha256:${SHA256_ZIP}|" updates.xml
- fi
-
- git add updates.xml
- git commit -m "chore(release): ZIP + tar.gz for ${VERSION} [skip ci]" \
- --author="github-actions[bot] " || true
- git push || true
- fi
-
- echo "### Joomla Packages" >> $GITHUB_STEP_SUMMARY
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "| Package | Size | SHA-256 |" >> $GITHUB_STEP_SUMMARY
- echo "|---------|------|---------|" >> $GITHUB_STEP_SUMMARY
- echo "| \`${ZIP_NAME}\` | ${ZIP_SIZE} | \`${SHA256_ZIP}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| \`${TAR_NAME}\` | ${TAR_SIZE} | \`${SHA256_TAR}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| Release | \`${RELEASE_TAG}\` | |" >> $GITHUB_STEP_SUMMARY
- echo "| Download | [${PACKAGE_NAME}](https://github.com/${REPO}/releases/download/${RELEASE_TAG}/${PACKAGE_NAME}) |" >> $GITHUB_STEP_SUMMARY
-
# -- Summary --------------------------------------------------------------
- name: Pipeline Summary
if: always()
@@ -549,7 +326,7 @@ jobs:
echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
else
echo "" >> $GITHUB_STEP_SUMMARY
- echo "## Build & Release Complete (Joomla)" >> $GITHUB_STEP_SUMMARY
+ echo "## Build & Release Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
--
2.49.1
From a9a29a11c99a1178585543f398674b9f7f67bd81 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Thu, 9 Apr 2026 10:51:17 -0500
Subject: [PATCH 22/40] chore: update .github/workflows/repository-cleanup.yml
from MokoStandards
---
.github/workflows/repository-cleanup.yml | 8 ++------
1 file changed, 2 insertions(+), 6 deletions(-)
diff --git a/.github/workflows/repository-cleanup.yml b/.github/workflows/repository-cleanup.yml
index ea9219d..f564b93 100644
--- a/.github/workflows/repository-cleanup.yml
+++ b/.github/workflows/repository-cleanup.yml
@@ -8,8 +8,8 @@
# DEFGROUP: GitHub.Workflow
# INGROUP: MokoStandards.Maintenance
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
-# PATH: /templates/workflows/shared/repository-cleanup.yml.template
-# VERSION: 04.06.00
+# PATH: /templates/workflows/shared/repository-cleanup.yml
+# VERSION: 04.05.13
# 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.
@@ -154,10 +154,6 @@ jobs:
".github/workflows/auto-version-branch.yml"
".github/workflows/publish-to-mokodolibarr.yml"
".github/workflows/ci.yml"
- ".github/workflows/deploy-rs.yml"
- "sftp-config.json"
- "sftp-config.json.template"
- "scripts/sftp-config"
)
DELETED=0
--
2.49.1
From 2edf20b6a6aea0fb28e8752b754a5ceb8818ab4f Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Thu, 9 Apr 2026 10:51:18 -0500
Subject: [PATCH 23/40] chore: update .github/workflows/auto-dev-issue.yml from
MokoStandards
---
.github/workflows/auto-dev-issue.yml | 67 +++++++++++-----------------
1 file changed, 26 insertions(+), 41 deletions(-)
diff --git a/.github/workflows/auto-dev-issue.yml b/.github/workflows/auto-dev-issue.yml
index 9b5fbe2..160fa47 100644
--- a/.github/workflows/auto-dev-issue.yml
+++ b/.github/workflows/auto-dev-issue.yml
@@ -8,8 +8,8 @@
# DEFGROUP: GitHub.Workflow
# INGROUP: MokoStandards.Automation
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
-# PATH: /templates/workflows/shared/auto-dev-issue.yml.template
-# VERSION: 04.06.00
+# PATH: /templates/workflows/shared/auto-dev-issue.yml
+# VERSION: 04.05.13
# 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.
@@ -39,10 +39,7 @@ jobs:
runs-on: ubuntu-latest
if: >-
(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/')))
+ (github.event.ref_type == 'branch' && startsWith(github.event.ref, 'rc/'))
steps:
- name: Create tracking issue and sub-issues
@@ -65,16 +62,6 @@ 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"
@@ -93,20 +80,14 @@ jobs:
exit 0
fi
- # ── Define sub-issues for the workflow ─────────────────────────
+ # ── Define sub-issues for the dev 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"
+ "Regression Testing|Run full regression suite before merge to main|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"
+ "Merge to Main|Create PR from rc branch to main|type: release,needs-review"
)
else
SUB_ISSUES=(
@@ -175,26 +156,30 @@ jobs:
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
+ # ── RC: Create or update draft release ────────────────────────────
+ if [[ "$BRANCH" == rc/* ]]; then
+ MAJOR=$(echo "$VERSION" | awk -F. '{print $1}')
+ RELEASE_TAG="v${MAJOR}"
+ DRAFT_EXISTS=$(gh release view "$RELEASE_TAG" --json isDraft -q .isDraft 2>/dev/null || true)
- EXISTING=$(gh release view "$RELEASE_TAG" --json tagName -q .tagName 2>/dev/null || true)
- if [ -z "$EXISTING" ]; then
+ if [ -z "$DRAFT_EXISTS" ]; then
+ # No release exists — create draft
gh release create "$RELEASE_TAG" \
- --title "${RELEASE_TAG} (${VERSION})" \
- --notes "## ${BRANCH_TYPE} ${VERSION}\n\nBranch: \`${BRANCH}\`\nTracking issue: ${PARENT_URL}" \
- --prerelease \
+ --title "v${MAJOR} (RC: ${VERSION})" \
+ --notes "## Release Candidate ${VERSION}\n\nRC branch: \`${BRANCH}\`\nTracking issue: ${PARENT_URL}" \
+ --draft \
--target main 2>/dev/null || true
- echo "${BRANCH_TYPE} release created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
- else
+ echo "Draft release created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
+ elif [ "$DRAFT_EXISTS" = "true" ]; then
+ # Draft exists — update title
gh release edit "$RELEASE_TAG" \
- --title "${RELEASE_TAG} (${VERSION})" --prerelease 2>/dev/null || true
- echo "${BRANCH_TYPE} release updated: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
+ --title "v${MAJOR} (RC: ${VERSION})" --draft 2>/dev/null || true
+ echo "Draft release updated: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
+ else
+ # Release exists and is published — set back to draft for RC
+ gh release edit "$RELEASE_TAG" \
+ --title "v${MAJOR} (RC: ${VERSION})" --draft 2>/dev/null || true
+ echo "Release ${RELEASE_TAG} set to draft for RC" >> $GITHUB_STEP_SUMMARY
fi
fi
--
2.49.1
From a18e42da2abaf9ab27ff081f77c6bfaa5101baae Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Thu, 9 Apr 2026 10:51:19 -0500
Subject: [PATCH 24/40] chore: update .github/workflows/repo_health.yml from
MokoStandards
---
.github/workflows/repo_health.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/repo_health.yml b/.github/workflows/repo_health.yml
index 73308be..885203a 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.06.00
+# VERSION: 04.05.00
# BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts.
# NOTE: Field is user-managed.
# ============================================================================
--
2.49.1
From c664d2983cf25fe7ff42534ffd0cfd66020ff99d Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Thu, 9 Apr 2026 10:51:19 -0500
Subject: [PATCH 25/40] chore: update .github/ISSUE_TEMPLATE/config.yml from
MokoStandards
--
2.49.1
From 316af332d61be0f3cf8d5599e845bf921dfdc686 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Thu, 9 Apr 2026 10:51:20 -0500
Subject: [PATCH 26/40] chore: update .github/ISSUE_TEMPLATE/adr.md from
MokoStandards
--
2.49.1
From a2599ecb5f09cfdcfbf4abb0c8e2371d5a37ab76 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Thu, 9 Apr 2026 10:51:21 -0500
Subject: [PATCH 27/40] chore: update .github/ISSUE_TEMPLATE/bug_report.md from
MokoStandards
--
2.49.1
From c54d2f76bfb336474ea781138e940828ce3cf5d4 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Thu, 9 Apr 2026 10:51:21 -0500
Subject: [PATCH 28/40] chore: update .github/ISSUE_TEMPLATE/documentation.md
from MokoStandards
--
2.49.1
From fc3e090ab64819d2a9c3f97c98adb8e1fc249445 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Thu, 9 Apr 2026 10:51:22 -0500
Subject: [PATCH 29/40] chore: update
.github/ISSUE_TEMPLATE/enterprise_support.md from MokoStandards
--
2.49.1
From bf1d8dc5776f0647887e98445b42fe993f87a210 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Thu, 9 Apr 2026 10:51:23 -0500
Subject: [PATCH 30/40] chore: update .github/ISSUE_TEMPLATE/feature_request.md
from MokoStandards
--
2.49.1
From 5a26d1b3c810d95c3db9b653b1b7d3773eb994e2 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Thu, 9 Apr 2026 10:51:24 -0500
Subject: [PATCH 31/40] chore: update
.github/ISSUE_TEMPLATE/firewall-request.md from MokoStandards
---
.github/ISSUE_TEMPLATE/firewall-request.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/ISSUE_TEMPLATE/firewall-request.md b/.github/ISSUE_TEMPLATE/firewall-request.md
index 4a43395..38be866 100644
--- a/.github/ISSUE_TEMPLATE/firewall-request.md
+++ b/.github/ISSUE_TEMPLATE/firewall-request.md
@@ -3,7 +3,7 @@ name: Firewall Request
about: Request firewall rule changes or access to external resources
title: '[FIREWALL] [Resource Name] - [Brief Description]'
labels: ['firewall-request', 'infrastructure', 'security']
-assignees: []
+assignees: ['jmiller-moko']
---
--
2.49.1
From c07677486702abfb8fd5b512cf3133f3f5ba059e Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Thu, 9 Apr 2026 10:51:24 -0500
Subject: [PATCH 32/40] chore: update .github/ISSUE_TEMPLATE/question.md from
MokoStandards
---
.github/ISSUE_TEMPLATE/question.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md
index e17850b..74df7a0 100644
--- a/.github/ISSUE_TEMPLATE/question.md
+++ b/.github/ISSUE_TEMPLATE/question.md
@@ -3,7 +3,7 @@ name: Question
about: Ask a question about usage, features, or best practices
title: '[QUESTION] '
labels: ['question']
-assignees: []
+assignees: ['jmiller-moko']
---
--
2.49.1
From 93877cc1fb66b650d5877b9e06cefe9dfa0ac067 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Thu, 9 Apr 2026 10:51:25 -0500
Subject: [PATCH 33/40] chore: update .github/ISSUE_TEMPLATE/request-license.md
from MokoStandards
---
.github/ISSUE_TEMPLATE/request-license.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/ISSUE_TEMPLATE/request-license.md b/.github/ISSUE_TEMPLATE/request-license.md
index 52c3b74..a9c87a7 100644
--- a/.github/ISSUE_TEMPLATE/request-license.md
+++ b/.github/ISSUE_TEMPLATE/request-license.md
@@ -3,7 +3,7 @@ name: License Request
about: Request an organization license for Sublime Text
title: '[LICENSE REQUEST] Sublime Text - [Your Name]'
labels: ['license-request', 'admin']
-assignees: []
+assignees: ['jmiller-moko']
---
--
2.49.1
From 8f9316b1876ef255a61f9730631f2d06d35cc743 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Thu, 9 Apr 2026 10:51:26 -0500
Subject: [PATCH 34/40] chore: update .github/ISSUE_TEMPLATE/rfc.md from
MokoStandards
--
2.49.1
From 3adc863aba3707bcdd30f9caa3dc8275059afa80 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Thu, 9 Apr 2026 10:51:26 -0500
Subject: [PATCH 35/40] chore: update .github/ISSUE_TEMPLATE/security.md from
MokoStandards
--
2.49.1
From dbe3363e5d4fdc4954d69e9e31a3fc398686e62d Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Thu, 9 Apr 2026 10:51:27 -0500
Subject: [PATCH 36/40] chore: update .github/ISSUE_TEMPLATE/joomla_issue.md
from MokoStandards
--
2.49.1
From 3faf10f8fdde720b8f821bec52a5bb1bd57d9dd9 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Thu, 9 Apr 2026 10:51:28 -0500
Subject: [PATCH 37/40] chore: update .github/workflows/auto-assign.yml from
MokoStandards
---
.github/workflows/auto-assign.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/auto-assign.yml b/.github/workflows/auto-assign.yml
index d0b70f6..3752b66 100644
--- a/.github/workflows/auto-assign.yml
+++ b/.github/workflows/auto-assign.yml
@@ -6,7 +6,7 @@
# INGROUP: MokoStandards.Workflows.Shared
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /.github/workflows/auto-assign.yml
-# VERSION: 04.06.00
+# VERSION: 04.05.11
# BRIEF: Auto-assign jmiller-moko to unassigned issues and PRs every 15 minutes
name: Auto-Assign Issues & PRs
--
2.49.1
From dca8c56ec4c090ba351ce362ad6d7ad8da6c9f26 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Thu, 9 Apr 2026 10:51:28 -0500
Subject: [PATCH 38/40] chore: update
.github/workflows/changelog-validation.yml from MokoStandards
---
.github/workflows/changelog-validation.yml | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/changelog-validation.yml b/.github/workflows/changelog-validation.yml
index 5521195..d38c0ba 100644
--- a/.github/workflows/changelog-validation.yml
+++ b/.github/workflows/changelog-validation.yml
@@ -8,18 +8,20 @@
# 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
+# PATH: /templates/workflows/shared/changelog-validation.yml
+# VERSION: 04.05.13
# 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
- - 'dev/**'
workflow_dispatch:
permissions:
--
2.49.1
From 3aa6ab2a58ed291c8bd71de90d19d501ac481fad Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Thu, 9 Apr 2026 10:51:29 -0500
Subject: [PATCH 39/40] chore: update .github/workflows/update-server.yml from
MokoStandards
---
.github/workflows/update-server.yml | 290 ++++++++++------------------
1 file changed, 102 insertions(+), 188 deletions(-)
diff --git a/.github/workflows/update-server.yml b/.github/workflows/update-server.yml
index 83c8e0d..c217389 100644
--- a/.github/workflows/update-server.yml
+++ b/.github/workflows/update-server.yml
@@ -6,8 +6,8 @@
# 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
+# PATH: /templates/workflows/joomla/update-server.yml
+# VERSION: 04.05.13
# BRIEF: Update Joomla update server XML feed with stable/rc/dev entries
#
# Writes updates.xml with multiple entries:
@@ -20,12 +20,9 @@
name: Update Joomla Update Server XML Feed
on:
- pull_request:
- types: [closed]
+ push:
branches:
- 'dev/**'
- - 'alpha/**'
- - 'beta/**'
- 'rc/**'
paths:
- 'src/**'
@@ -33,14 +30,12 @@ on:
workflow_dispatch:
inputs:
stability:
- description: 'Stability tag'
+ description: 'Stability tag (development, rc, stable)'
required: true
default: 'development'
type: choice
options:
- development
- - alpha
- - beta
- rc
- stable
@@ -54,8 +49,6 @@ jobs:
update-xml:
name: Update updates.xml
runs-on: ubuntu-latest
- if: >-
- github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch'
steps:
- name: Checkout repository
@@ -82,64 +75,33 @@ jobs:
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 (portable — no grep -P)
+ # 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
- # Extract fields using sed (works on all runners)
- EXT_NAME=$(sed -n 's/.*\([^<]*\)<\/name>.*/\1/p' "$MANIFEST" | head -1)
- EXT_TYPE=$(sed -n 's/.*]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
- EXT_ELEMENT=$(sed -n 's/.*\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" | head -1)
- EXT_CLIENT=$(sed -n 's/.*]*client="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
- EXT_FOLDER=$(sed -n 's/.*]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
- EXT_VERSION=$(sed -n 's/.*\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1)
- TARGET_PLATFORM=$(sed -n 's/.*\(\).*/\1/p' "$MANIFEST" | head -1)
- PHP_MINIMUM=$(sed -n 's/.*\([^<]*\)<\/php_minimum>.*/\1/p' "$MANIFEST" | head -1)
-
- # Fallbacks
- [ -z "$EXT_NAME" ] && EXT_NAME="${{ github.event.repository.name }}"
- [ -z "$EXT_TYPE" ] && EXT_TYPE="component"
-
- # Templates and modules don't have — derive from
- if [ -z "$EXT_ELEMENT" ]; then
- EXT_ELEMENT=$(echo "$EXT_NAME" | tr '[:upper:]' '[:lower:]' | tr -d ' ')
- fi
-
- # Use manifest version if README version is empty
- [ "$VERSION" = "0.0.0" ] && [ -n "$EXT_VERSION" ] && VERSION="$EXT_VERSION"
+ 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=""
@@ -154,117 +116,127 @@ jobs:
# 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
+ [ "$STABILITY" = "rc" ] && DISPLAY_VERSION="${VERSION}-rc"
+ [ "$STABILITY" = "development" ] && DISPLAY_VERSION="${VERSION}-dev"
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
-
+ RELEASE_TAG="v${MAJOR}"
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 packages (ZIP + tar.gz) ───────────────────
+ # ── Build install-ready ZIP ─────────────────────────────────
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
if [ -d "$SOURCE_DIR" ]; then
- EXCLUDES=".ftpignore sftp-config* *.ppk *.pem *.key .env*"
- TAR_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.tar.gz"
-
cd "$SOURCE_DIR"
- zip -r "/tmp/${PACKAGE_NAME}" . -x $EXCLUDES
+ zip -r "/tmp/${PACKAGE_NAME}" .
cd ..
- tar -czf "/tmp/${TAR_NAME}" -C "$SOURCE_DIR" \
- --exclude='.ftpignore' --exclude='sftp-config*' \
- --exclude='*.ppk' --exclude='*.pem' --exclude='*.key' --exclude='.env*' .
SHA256=$(sha256sum "/tmp/${PACKAGE_NAME}" | cut -d' ' -f1)
- # Ensure release exists
+ # 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
+ gh release create "$RELEASE_TAG" --title "v${MAJOR}" --notes "Development release" --draft --target main 2>/dev/null || true
- # Upload both formats
+ # Upload ZIP to the major release
gh release upload "$RELEASE_TAG" "/tmp/${PACKAGE_NAME}" --clobber 2>/dev/null || true
- gh release upload "$RELEASE_TAG" "/tmp/${TAR_NAME}" --clobber 2>/dev/null || true
- echo "Packages: ${PACKAGE_NAME} + ${TAR_NAME} (SHA: ${SHA256})" >> $GITHUB_STEP_SUMMARY
+ 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"
- TAR_URL="https://github.com/${REPO}/releases/download/${RELEASE_TAG}/${EXT_ELEMENT}-${DISPLAY_VERSION}.tar.gz"
- NEW_ENTRY="${NEW_ENTRY} ${DOWNLOAD_URL}\n"
- NEW_ENTRY="${NEW_ENTRY} ${TAR_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
+ NEW_ENTRY=$(cat <
+ ${EXT_NAME}
+ ${EXT_NAME} (${STABILITY})
+ ${EXT_ELEMENT}
+ ${EXT_TYPE}
+ ${DISPLAY_VERSION}
+ $([ -n "$CLIENT_TAG" ] && echo " ${CLIENT_TAG}")
+ $([ -n "$FOLDER_TAG" ] && echo " ${FOLDER_TAG}")
+
+ ${STABILITY}
+
+ ${INFO_URL}
+
+ ${DOWNLOAD_URL}
+
+ $([ -n "$SHA256" ] && echo " sha256:${SHA256}")
+ ${TARGET_PLATFORM}
+ $([ -n "$PHP_TAG" ] && echo " ${PHP_TAG}")
+ Moko Consulting
+ https://mokoconsulting.tech
+
+XMLEOF
+)
# ── Merge into updates.xml ─────────────────────────────────────
if [ ! -f "updates.xml" ]; then
+ # Create fresh
printf '%s\n' '' > updates.xml
printf '%s\n' '' >> updates.xml
- cat /tmp/new_entry.xml >> updates.xml
- printf '\n%s\n' '' >> updates.xml
+ echo "$NEW_ENTRY" >> updates.xml
+ printf '%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
+ # Remove existing entry for this stability, add new one
+ # Use python for reliable XML manipulation
+ python3 -c "
+import re, sys
+
+with open('updates.xml', 'r') as f:
+ content = f.read()
+
+# Remove existing entry with this stability tag
+pattern = r' .*?${STABILITY}.*?\n?'
+content = re.sub(pattern, '', content, flags=re.DOTALL)
+
+# Insert new entry before
+new_entry = '''${NEW_ENTRY}'''
+content = content.replace('', new_entry + '\n')
+
+# Clean up empty lines
+content = re.sub(r'\n{3,}', '\n\n', content)
+
+with open('updates.xml', 'w') as f:
+ f.write(content)
+" 2>/dev/null || {
+ # Fallback: just rewrite the whole file if python fails
+ # Keep existing stable entry if present
+ STABLE_ENTRY=""
+ if [ "$STABILITY" != "stable" ] && grep -q 'stable' updates.xml; then
+ STABLE_ENTRY=$(sed -n '//,/<\/update>/{ /stable<\/tag>/,/<\/update>/p; //,/stable<\/tag>/p }' updates.xml | sort -u)
+ fi
+ RC_ENTRY=""
+ if [ "$STABILITY" != "rc" ] && grep -q 'rc' updates.xml; then
+ RC_ENTRY=$(python3 -c "
+import re
+with open('updates.xml') as f: c = f.read()
+m = re.search(r'(.*?rc.*?)', c, re.DOTALL)
+if m: print(m.group(1))
+" 2>/dev/null || true)
+ fi
+ DEV_ENTRY=""
+ if [ "$STABILITY" != "development" ] && grep -q 'development' updates.xml; then
+ DEV_ENTRY=$(python3 -c "
+import re
+with open('updates.xml') as f: c = f.read()
+m = re.search(r'(.*?development.*?)', c, re.DOTALL)
+if m: print(m.group(1))
+" 2>/dev/null || true)
+ fi
+
{
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
+ [ -n "$STABLE_ENTRY" ] && echo "$STABLE_ENTRY"
+ [ -n "$RC_ENTRY" ] && echo "$RC_ENTRY"
+ [ -n "$DEV_ENTRY" ] && echo "$DEV_ENTRY"
+ echo "$NEW_ENTRY"
+ printf '%s\n' ''
+ } > updates.xml
}
fi
@@ -278,64 +250,6 @@ jobs:
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
--
2.49.1
From a45c03c44f17fae72ecacf8e69c694ee8f1ee87f Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Thu, 9 Apr 2026 10:51:30 -0500
Subject: [PATCH 40/40] chore: update .github/CODEOWNERS from MokoStandards
---
.github/CODEOWNERS | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index e7e6e80..2402dee 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -11,7 +11,7 @@
# ── 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-manual.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
@@ -23,11 +23,9 @@
/.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
-/.github/workflows/branch-freeze.yml @jmiller-moko
# Custom workflows in .github/workflows/ not listed above are repo-owned.
# ── GitHub configuration ─────────────────────────────────────────────────
--
2.49.1