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 + +[![MokoStandards](https://img.shields.io/badge/MokoStandards-04.05.13-blue)](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