Compare commits
213 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4cfde99e7f | |||
| 1e105d6c7b | |||
| 2140c9e07f | |||
| 5cb6dd8008 | |||
| 2264b00828 | |||
| f87086bd0f | |||
| fc23c771c0 | |||
| 96299a6b9a | |||
| 1961585e83 | |||
| 14f5407820 | |||
| 407b30a437 | |||
| 49041565eb | |||
| 1d1026f7e7 | |||
| 1f7329272d | |||
| 4c855ac7c8 | |||
| a350d02d08 | |||
| a860d414bd | |||
| e03c86f2c6 | |||
| 1b0025e55f | |||
| ffe599ee92 | |||
| ac56b3a776 | |||
| 95badba96e | |||
| de66983cda | |||
| 89a59f8a8e | |||
| 34367ae93c | |||
| b2d4071193 | |||
| 3bc5678768 | |||
| ca0cfd9a6d | |||
| 9cc4b90b78 | |||
| 4ebb9e30d6 | |||
| 0f164b607c | |||
| 6762764006 | |||
| efdcaa712f | |||
| d3581564cf | |||
| a29d8f4e12 | |||
| 109ca703ef | |||
| 794746e20d | |||
| 85848c2d6c | |||
| 86d4681fcd | |||
| 0a14a29ac6 | |||
| df07b4b672 | |||
| 7bd151ad62 | |||
| ddc867ad06 | |||
| a111f5b5e9 | |||
| 1897805483 | |||
| 8919db6fc3 | |||
| d69b26af51 | |||
| a8dae85f42 | |||
| d3bc62f810 | |||
| 13683adfba | |||
| e183b62aba | |||
| ce9d72b50d | |||
| 92358a673b | |||
| 99308cd7a4 | |||
| 561ba24090 | |||
| 3e1cb9a500 | |||
| 5ae8e3e001 | |||
| faea3637e0 | |||
| 79eaa5217d | |||
| 0e0891f1a8 | |||
| 33aaf666ae | |||
| a634938799 | |||
| 14ff4ab2f1 | |||
| b3de21e7d1 | |||
| 72a373b17c | |||
| bc290f3bed | |||
| a4704ad267 | |||
| d1762ad5df | |||
| df1467c518 | |||
| 7cdd97ca59 | |||
| 5b36d10b04 | |||
| 56699fdd4d | |||
| fcf1cc41c8 | |||
| b8640ccb1d | |||
| 4b51e2dd9a | |||
| e068e14004 | |||
| 941fd4c6cd | |||
| f2021d478e | |||
| 900ceb2bb5 | |||
| 9b498e6786 | |||
| ca06298e64 | |||
| dc8d7d59d4 | |||
| 55d2123c33 | |||
| 274c1f34af | |||
| 75c878507e | |||
| f1ea8ead74 | |||
| 23de84610e | |||
| 0cb24b4759 | |||
| 7fa97231a1 | |||
| 2291db32c5 | |||
| 491bd3b858 | |||
| 064d5e3ab1 | |||
| c512829cd4 | |||
| 69c728cd5a | |||
| fca6d4f25f | |||
| b668e1d4ed | |||
| 5b760a1b74 | |||
| 1f8dccf898 | |||
| b9d8eb3950 | |||
| 053fe2d52c | |||
| b3846fa633 | |||
| a8ef4cfb77 | |||
| 58317cd205 | |||
| 642e2bffe7 | |||
| d6d423b946 | |||
| e92a963088 | |||
| f3a8246e34 | |||
| f65d261598 | |||
| c9f50e452b | |||
| c820d015e7 | |||
| 78cbd1f370 | |||
| 70d2bab52d | |||
| 166a6366f8 | |||
| ac8a64c4c1 | |||
| 2ee8a5e286 | |||
| 9d2620faea | |||
| c79e8bed73 | |||
| 4ddf02c7af | |||
| 0fb95ced3f | |||
| 62a8e9bd99 | |||
| de0b588be0 | |||
| 4650f9ba46 | |||
| be58716391 | |||
| 03870dce33 | |||
| 5fee5d7810 | |||
| c13c2a372e | |||
| 09074e3c00 | |||
| 9bfbf36090 | |||
| 7e5ff12d03 | |||
| 42f7a09bb3 | |||
| 6ad536c0ef | |||
| eb1b112a93 | |||
| 4918879eec | |||
| add973771b | |||
| 5753c307c6 | |||
| bfe4432c78 | |||
| 3c1f3a2421 | |||
| 9017b06c7d | |||
| 1cb5c77bec | |||
| c2b88e9a94 | |||
| 845ed4b53d | |||
| dc5feaa9aa | |||
| 7281cd1500 | |||
| a1b2bf40ce | |||
| 854dbc6350 | |||
| 1bcbe800e9 | |||
| c8918df03e | |||
| 54236c0d73 | |||
| 33ce9784cc | |||
| 582a16e132 | |||
| e97388c119 | |||
| 954cdaa2ae | |||
| c60be2bf3c | |||
| cec436f90e | |||
| a4e39df6ed | |||
| 70dbb65173 | |||
| 6a00d7ddf9 | |||
| 78c7b99c6a | |||
| 6d56949452 | |||
| 137b2556ac | |||
| 9b711d2309 | |||
| 122c7b630a | |||
| 8ab62abf29 | |||
| 27505f7501 | |||
| 65bba1f561 | |||
| 28db9a67b6 | |||
| b9b0c88ad5 | |||
| 370fa86f59 | |||
| b6bed1e6df | |||
| acf599b25e | |||
| a1dd54db72 | |||
| 3403785e1f | |||
| 7d1f30aaaa | |||
| 282fe5fce1 | |||
| 1430b18583 | |||
| 54bcd044be | |||
| 1b719a6216 | |||
| c825e800e0 | |||
| 81103615a4 | |||
| 93c3c5b214 | |||
| c0d5a884a4 | |||
| f5eed45566 | |||
| 6633e38d8f | |||
| 8b88c1f368 | |||
| b5705ffffe | |||
| 311178278a | |||
| c1732e6932 | |||
| 4444b116d1 | |||
| bfb2b9f925 | |||
| 88b3d0df0f | |||
| b97b76eb0d | |||
| 7571b26969 | |||
| 4eba3d2be4 | |||
| 238dc29535 | |||
| 6765c2406e | |||
| da67260991 | |||
| b485cc6fb5 | |||
| 4d6f76acde | |||
| e106b6d4be | |||
| 566b6c2e6e | |||
| 625e7d1337 | |||
| 14f3f4a17c | |||
| a5066645d8 | |||
| 47678a892c | |||
| f07806d3dc | |||
| 99fd758900 | |||
| ff4cdf3c93 | |||
| f1e7f0dd18 | |||
| 86427f9b44 | |||
| 4003f53acc | |||
| 4bec6c4cfd | |||
| 5cdc8f533d | |||
| a40dfa7e69 |
@@ -0,0 +1,41 @@
|
|||||||
|
# EditorConfig helps maintain consistent coding styles across different editors and IDEs
|
||||||
|
# https://editorconfig.org/
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
# Default settings — Tabs preferred, width = 2 spaces
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
indent_style = tab
|
||||||
|
tab_width = 2
|
||||||
|
|
||||||
|
# PowerShell scripts — tabs, 2-space visual width
|
||||||
|
[*.ps1]
|
||||||
|
indent_style = tab
|
||||||
|
tab_width = 2
|
||||||
|
end_of_line = crlf
|
||||||
|
|
||||||
|
# Markdown files — keep trailing whitespace for line breaks
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
# JSON / YAML files — tabs, 2-space visual width
|
||||||
|
[*.{json,yml,yaml}]
|
||||||
|
indent_style = tab
|
||||||
|
tab_width = 2
|
||||||
|
|
||||||
|
# Makefiles — always tabs, default width
|
||||||
|
[Makefile]
|
||||||
|
indent_style = tab
|
||||||
|
tab_width = 2
|
||||||
|
|
||||||
|
# Windows batch scripts — keep CRLF endings
|
||||||
|
[*.{bat,cmd}]
|
||||||
|
end_of_line = crlf
|
||||||
|
|
||||||
|
# Shell scripts — ensure LF endings
|
||||||
|
[*.sh]
|
||||||
|
end_of_line = lf
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
---
|
||||||
|
name: Architecture Decision Record (ADR)
|
||||||
|
about: Propose or document an architectural decision
|
||||||
|
title: '[ADR] '
|
||||||
|
labels: 'architecture, decision'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## ADR Number
|
||||||
|
ADR-XXXX
|
||||||
|
|
||||||
|
## Status
|
||||||
|
- [ ] Proposed
|
||||||
|
- [ ] Accepted
|
||||||
|
- [ ] Deprecated
|
||||||
|
- [ ] Superseded by ADR-XXXX
|
||||||
|
|
||||||
|
## Context
|
||||||
|
Describe the issue or problem that motivates this decision.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
State the architecture decision and provide rationale.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
### Positive
|
||||||
|
- List positive consequences
|
||||||
|
|
||||||
|
### Negative
|
||||||
|
- List negative consequences or trade-offs
|
||||||
|
|
||||||
|
### Neutral
|
||||||
|
- List neutral aspects
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
### Alternative 1
|
||||||
|
- Description
|
||||||
|
- Pros
|
||||||
|
- Cons
|
||||||
|
- Why not chosen
|
||||||
|
|
||||||
|
### Alternative 2
|
||||||
|
- Description
|
||||||
|
- Pros
|
||||||
|
- Cons
|
||||||
|
- Why not chosen
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
1. Step 1
|
||||||
|
2. Step 2
|
||||||
|
3. Step 3
|
||||||
|
|
||||||
|
## Stakeholders
|
||||||
|
- **Decision Makers**: @user1, @user2
|
||||||
|
- **Consulted**: @user3, @user4
|
||||||
|
- **Informed**: team-name
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
### Architecture Diagram
|
||||||
|
```
|
||||||
|
[Add diagram or link]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
- Dependency 1
|
||||||
|
- Dependency 2
|
||||||
|
|
||||||
|
### Impact Analysis
|
||||||
|
- **Performance**: [Impact description]
|
||||||
|
- **Security**: [Impact description]
|
||||||
|
- **Scalability**: [Impact description]
|
||||||
|
- **Maintainability**: [Impact description]
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
- [ ] Unit tests
|
||||||
|
- [ ] Integration tests
|
||||||
|
- [ ] Performance tests
|
||||||
|
- [ ] Security tests
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
- [ ] Architecture documentation updated
|
||||||
|
- [ ] API documentation updated
|
||||||
|
- [ ] Developer guide updated
|
||||||
|
- [ ] Runbook created
|
||||||
|
|
||||||
|
## Migration Path
|
||||||
|
Describe how to migrate from current state to new architecture.
|
||||||
|
|
||||||
|
## Rollback Plan
|
||||||
|
Describe how to rollback if issues occur.
|
||||||
|
|
||||||
|
## Timeline
|
||||||
|
- **Proposal Date**:
|
||||||
|
- **Decision Date**:
|
||||||
|
- **Implementation Start**:
|
||||||
|
- **Expected Completion**:
|
||||||
|
|
||||||
|
## References
|
||||||
|
- Related ADRs:
|
||||||
|
- External resources:
|
||||||
|
- RFCs:
|
||||||
|
|
||||||
|
## Review Checklist
|
||||||
|
- [ ] Aligns with enterprise architecture principles
|
||||||
|
- [ ] Security implications reviewed
|
||||||
|
- [ ] Performance implications reviewed
|
||||||
|
- [ ] Cost implications reviewed
|
||||||
|
- [ ] Compliance requirements met
|
||||||
|
- [ ] Team consensus achieved
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
---
|
||||||
|
name: Bug Report
|
||||||
|
about: Report a bug or issue with the project
|
||||||
|
title: '[BUG] '
|
||||||
|
labels: 'bug'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## Bug Description
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
## Steps to Reproduce
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '...'
|
||||||
|
3. Scroll down to '...'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
## Actual Behavior
|
||||||
|
A clear and concise description of what actually happened.
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
- **Project**: [e.g., MokoDoliTools, moko-cassiopeia]
|
||||||
|
- **Version**: [e.g., 1.2.3]
|
||||||
|
- **Platform**: [e.g., Dolibarr 18.0, Joomla 5.0]
|
||||||
|
- **PHP Version**: [e.g., 8.1]
|
||||||
|
- **Database**: [e.g., MySQL 8.0, PostgreSQL 14]
|
||||||
|
- **Browser** (if applicable): [e.g., Chrome 120, Firefox 121]
|
||||||
|
- **OS**: [e.g., Ubuntu 22.04, Windows 11]
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
Add any other context about the problem here.
|
||||||
|
|
||||||
|
## Possible Solution
|
||||||
|
If you have suggestions on how to fix the issue, please describe them here.
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
- [ ] I have searched for similar issues before creating this one
|
||||||
|
- [ ] I have provided all the requested information
|
||||||
|
- [ ] I have tested this on the latest stable version
|
||||||
|
- [ ] I have checked the documentation and couldn't find a solution
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
blank_issues_enabled: true
|
||||||
|
contact_links:
|
||||||
|
- name: 💼 Enterprise Support
|
||||||
|
url: https://mokoconsulting.tech/enterprise
|
||||||
|
about: Enterprise-level support and consultation services
|
||||||
|
- name: 💬 Ask a Question
|
||||||
|
url: https://mokoconsulting.tech/
|
||||||
|
about: Get help or ask questions through our website
|
||||||
|
- name: 📚 MokoStandards Documentation
|
||||||
|
url: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
|
about: View our coding standards and best practices
|
||||||
|
- name: 🔒 Report a Security Vulnerability
|
||||||
|
url: https://git.mokoconsulting.tech/mokoconsulting-tech/.github-private/security/advisories/new
|
||||||
|
about: Report security vulnerabilities privately (for critical issues)
|
||||||
|
- name: 💡 Community Discussions
|
||||||
|
url: https://github.com/orgs/mokoconsulting-tech/discussions
|
||||||
|
about: Join community discussions and Q&A
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
---
|
||||||
|
name: Documentation Issue
|
||||||
|
about: Report an issue with documentation
|
||||||
|
title: '[DOCS] '
|
||||||
|
labels: 'documentation'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## Documentation Issue
|
||||||
|
|
||||||
|
**Location**:
|
||||||
|
<!-- Specify the file, page, or section with the issue -->
|
||||||
|
|
||||||
|
## Issue Type
|
||||||
|
<!-- Mark the relevant option with an "x" -->
|
||||||
|
- [ ] Typo or grammar error
|
||||||
|
- [ ] Outdated information
|
||||||
|
- [ ] Missing documentation
|
||||||
|
- [ ] Unclear explanation
|
||||||
|
- [ ] Broken links
|
||||||
|
- [ ] Missing examples
|
||||||
|
- [ ] Other (specify below)
|
||||||
|
|
||||||
|
## Description
|
||||||
|
<!-- Clearly describe the documentation issue -->
|
||||||
|
|
||||||
|
## Current Content
|
||||||
|
<!-- Quote or describe the current documentation (if applicable) -->
|
||||||
|
```
|
||||||
|
Current text here
|
||||||
|
```
|
||||||
|
|
||||||
|
## Suggested Improvement
|
||||||
|
<!-- Provide your suggestion for how to improve the documentation -->
|
||||||
|
```
|
||||||
|
Suggested text here
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
<!-- Add any other context, screenshots, or references -->
|
||||||
|
|
||||||
|
## Standards Alignment
|
||||||
|
- [ ] Follows MokoStandards documentation guidelines
|
||||||
|
- [ ] Uses en_US/en_GB localization
|
||||||
|
- [ ] Includes proper SPDX headers where applicable
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
- [ ] I have searched for similar documentation issues
|
||||||
|
- [ ] I have provided a clear description
|
||||||
|
- [ ] I have suggested an improvement (if applicable)
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
name: Feature Request
|
||||||
|
about: Suggest a new feature or enhancement
|
||||||
|
title: '[FEATURE] '
|
||||||
|
labels: 'enhancement'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## Feature Description
|
||||||
|
A clear and concise description of the feature you'd like to see.
|
||||||
|
|
||||||
|
## Problem or Use Case
|
||||||
|
Describe the problem this feature would solve or the use case it addresses.
|
||||||
|
Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
## Proposed Solution
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
## Alternative Solutions
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
Describe how this feature would benefit users:
|
||||||
|
- Who would use this feature?
|
||||||
|
- What problems does it solve?
|
||||||
|
- What value does it add?
|
||||||
|
|
||||||
|
## Implementation Details (Optional)
|
||||||
|
If you have ideas about how this could be implemented, share them here:
|
||||||
|
- Technical approach
|
||||||
|
- Files/components that might need changes
|
||||||
|
- Any concerns or challenges you foresee
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
Add any other context, mockups, or screenshots about the feature request here.
|
||||||
|
|
||||||
|
## Relevant Standards
|
||||||
|
Does this relate to any standards in [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards)?
|
||||||
|
- [ ] Accessibility (WCAG 2.1 AA)
|
||||||
|
- [ ] Localization (en_US/en_GB)
|
||||||
|
- [ ] Security best practices
|
||||||
|
- [ ] Code quality standards
|
||||||
|
- [ ] Other: [specify]
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
- [ ] I have searched for similar feature requests before creating this one
|
||||||
|
- [ ] I have clearly described the use case and benefits
|
||||||
|
- [ ] I have considered alternative solutions
|
||||||
|
- [ ] This feature aligns with the project's goals and scope
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
---
|
||||||
|
name: Joomla Extension Issue
|
||||||
|
about: Report an issue with a Joomla extension
|
||||||
|
title: '[JOOMLA] '
|
||||||
|
labels: 'joomla'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## Issue Type
|
||||||
|
- [ ] Component Issue
|
||||||
|
- [ ] Module Issue
|
||||||
|
- [ ] Plugin Issue
|
||||||
|
- [ ] Template Issue
|
||||||
|
|
||||||
|
## Extension Details
|
||||||
|
- **Extension Name**: [e.g., moko-cassiopeia]
|
||||||
|
- **Extension Version**: [e.g., 1.2.3]
|
||||||
|
- **Extension Type**: [Component / Module / Plugin / Template]
|
||||||
|
|
||||||
|
## Joomla Environment
|
||||||
|
- **Joomla Version**: [e.g., 4.4.0, 5.0.0]
|
||||||
|
- **PHP Version**: [e.g., 8.1.0]
|
||||||
|
- **Database**: [MySQL / PostgreSQL / MariaDB]
|
||||||
|
- **Database Version**: [e.g., 8.0]
|
||||||
|
- **Server**: [Apache / Nginx / IIS]
|
||||||
|
- **Hosting**: [Shared / VPS / Dedicated / Cloud]
|
||||||
|
|
||||||
|
## Issue Description
|
||||||
|
Provide a clear and detailed description of the issue.
|
||||||
|
|
||||||
|
## Steps to Reproduce
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '...'
|
||||||
|
3. Configure '...'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
What you expected to happen.
|
||||||
|
|
||||||
|
## Actual Behavior
|
||||||
|
What actually happened.
|
||||||
|
|
||||||
|
## Error Messages
|
||||||
|
```
|
||||||
|
# Paste any error messages from Joomla error logs
|
||||||
|
# Location: administrator/logs/error.php
|
||||||
|
```
|
||||||
|
|
||||||
|
## Browser Console Errors
|
||||||
|
```javascript
|
||||||
|
// Paste any JavaScript console errors (F12 in browser)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
Add screenshots to help explain the issue.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
```ini
|
||||||
|
# Paste extension configuration (sanitize sensitive data)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installed Extensions
|
||||||
|
List other installed extensions that might conflict:
|
||||||
|
- Extension 1 (version)
|
||||||
|
- Extension 2 (version)
|
||||||
|
|
||||||
|
## Template Overrides
|
||||||
|
- [ ] Using template overrides
|
||||||
|
- [ ] Custom CSS
|
||||||
|
- [ ] Custom JavaScript
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
- **Multilingual Site**: [Yes / No]
|
||||||
|
- **Cache Enabled**: [Yes / No]
|
||||||
|
- **Debug Mode**: [Yes / No]
|
||||||
|
- **SEF URLs**: [Yes / No]
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
- [ ] I have cleared Joomla cache
|
||||||
|
- [ ] I have disabled other extensions to test for conflicts
|
||||||
|
- [ ] I have checked Joomla error logs
|
||||||
|
- [ ] I have tested with a default Joomla template
|
||||||
|
- [ ] I have checked browser console for JavaScript errors
|
||||||
|
- [ ] I have searched for similar issues
|
||||||
|
- [ ] I am using a supported Joomla version
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
---
|
||||||
|
name: Question
|
||||||
|
about: Ask a question about usage, features, or best practices
|
||||||
|
title: '[QUESTION] '
|
||||||
|
labels: ['question']
|
||||||
|
assignees: ['jmiller']
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## Question
|
||||||
|
|
||||||
|
**Your question:**
|
||||||
|
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
**What are you trying to accomplish?**
|
||||||
|
|
||||||
|
|
||||||
|
**What have you already tried?**
|
||||||
|
|
||||||
|
|
||||||
|
**Category**:
|
||||||
|
- [ ] Script usage
|
||||||
|
- [ ] Configuration
|
||||||
|
- [ ] Workflow setup
|
||||||
|
- [ ] Documentation interpretation
|
||||||
|
- [ ] Best practices
|
||||||
|
- [ ] Integration
|
||||||
|
- [ ] Other: __________
|
||||||
|
|
||||||
|
## Environment (if relevant)
|
||||||
|
|
||||||
|
**Your setup**:
|
||||||
|
- Operating System:
|
||||||
|
- Version:
|
||||||
|
|
||||||
|
## What You've Researched
|
||||||
|
|
||||||
|
**Documentation reviewed**:
|
||||||
|
- [ ] README.md
|
||||||
|
- [ ] Project documentation
|
||||||
|
- [ ] Other (specify): __________
|
||||||
|
|
||||||
|
**Similar issues/questions found**:
|
||||||
|
- #
|
||||||
|
- #
|
||||||
|
|
||||||
|
## Expected Outcome
|
||||||
|
|
||||||
|
**What result are you hoping for?**
|
||||||
|
|
||||||
|
|
||||||
|
## Code/Configuration Samples
|
||||||
|
|
||||||
|
**Relevant code or configuration** (if applicable):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Your code here
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
|
||||||
|
**Any other relevant information:**
|
||||||
|
|
||||||
|
|
||||||
|
**Screenshots** (if helpful):
|
||||||
|
|
||||||
|
|
||||||
|
## Urgency
|
||||||
|
|
||||||
|
- [ ] Urgent (blocking work)
|
||||||
|
- [ ] Normal (can work on other things meanwhile)
|
||||||
|
- [ ] Low priority (just curious)
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
- [ ] I have searched existing issues and discussions
|
||||||
|
- [ ] I have reviewed relevant documentation
|
||||||
|
- [ ] I have provided sufficient context
|
||||||
|
- [ ] I have included code/configuration samples if relevant
|
||||||
|
- [ ] This is a genuine question (not a bug report or feature request)
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
---
|
||||||
|
name: Request for Comments (RFC)
|
||||||
|
about: Propose a significant change for community discussion
|
||||||
|
title: '[RFC] '
|
||||||
|
labels: 'rfc, discussion'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## RFC Summary
|
||||||
|
One-paragraph summary of the proposal.
|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
Why are we doing this? What use cases does it support? What is the expected outcome?
|
||||||
|
|
||||||
|
## Detailed Design
|
||||||
|
### Overview
|
||||||
|
Provide a detailed explanation of the proposed change.
|
||||||
|
|
||||||
|
### API Changes (if applicable)
|
||||||
|
```php
|
||||||
|
// Before
|
||||||
|
function oldApi($param1) { }
|
||||||
|
|
||||||
|
// After
|
||||||
|
function newApi($param1, $param2) { }
|
||||||
|
```
|
||||||
|
|
||||||
|
### User Experience Changes
|
||||||
|
Describe how users will interact with this change.
|
||||||
|
|
||||||
|
### Implementation Approach
|
||||||
|
High-level implementation strategy.
|
||||||
|
|
||||||
|
## Drawbacks
|
||||||
|
Why should we *not* do this?
|
||||||
|
|
||||||
|
## Alternatives
|
||||||
|
What other designs have been considered? What is the impact of not doing this?
|
||||||
|
|
||||||
|
### Alternative 1
|
||||||
|
- Description
|
||||||
|
- Trade-offs
|
||||||
|
|
||||||
|
### Alternative 2
|
||||||
|
- Description
|
||||||
|
- Trade-offs
|
||||||
|
|
||||||
|
## Adoption Strategy
|
||||||
|
How will existing users adopt this? Is this a breaking change?
|
||||||
|
|
||||||
|
### Migration Guide
|
||||||
|
```bash
|
||||||
|
# Steps to migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deprecation Timeline
|
||||||
|
- **Announcement**:
|
||||||
|
- **Deprecation**:
|
||||||
|
- **Removal**:
|
||||||
|
|
||||||
|
## Unresolved Questions
|
||||||
|
- Question 1
|
||||||
|
- Question 2
|
||||||
|
|
||||||
|
## Future Possibilities
|
||||||
|
What future work does this enable?
|
||||||
|
|
||||||
|
## Impact Assessment
|
||||||
|
### Performance
|
||||||
|
Expected performance impact.
|
||||||
|
|
||||||
|
### Security
|
||||||
|
Security considerations and implications.
|
||||||
|
|
||||||
|
### Compatibility
|
||||||
|
- **Backward Compatible**: [Yes / No]
|
||||||
|
- **Breaking Changes**: [List]
|
||||||
|
|
||||||
|
### Maintenance
|
||||||
|
Long-term maintenance considerations.
|
||||||
|
|
||||||
|
## Community Input
|
||||||
|
### Stakeholders
|
||||||
|
- [ ] Core team
|
||||||
|
- [ ] Module developers
|
||||||
|
- [ ] End users
|
||||||
|
- [ ] Enterprise customers
|
||||||
|
|
||||||
|
### Feedback Period
|
||||||
|
**Duration**: [e.g., 2 weeks]
|
||||||
|
**Deadline**: [date]
|
||||||
|
|
||||||
|
## Implementation Timeline
|
||||||
|
### Phase 1: Design
|
||||||
|
- [ ] RFC discussion
|
||||||
|
- [ ] Design finalization
|
||||||
|
- [ ] Approval
|
||||||
|
|
||||||
|
### Phase 2: Implementation
|
||||||
|
- [ ] Core implementation
|
||||||
|
- [ ] Tests
|
||||||
|
- [ ] Documentation
|
||||||
|
|
||||||
|
### Phase 3: Release
|
||||||
|
- [ ] Beta release
|
||||||
|
- [ ] Feedback collection
|
||||||
|
- [ ] Stable release
|
||||||
|
|
||||||
|
## Success Metrics
|
||||||
|
How will we measure success?
|
||||||
|
- Metric 1
|
||||||
|
- Metric 2
|
||||||
|
|
||||||
|
## References
|
||||||
|
- Related RFCs:
|
||||||
|
- External documentation:
|
||||||
|
- Prior art:
|
||||||
|
|
||||||
|
## Open Questions for Community
|
||||||
|
1. Question 1?
|
||||||
|
2. Question 2?
|
||||||
|
|
||||||
|
---
|
||||||
|
**Note**: This RFC is open for community discussion. Please provide feedback in the comments below.
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
name: Security Vulnerability Report
|
||||||
|
about: Report a security vulnerability (use only for non-critical issues)
|
||||||
|
title: '[SECURITY] '
|
||||||
|
labels: 'security'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## ⚠️ IMPORTANT: Private Disclosure Required
|
||||||
|
|
||||||
|
**For critical security vulnerabilities, DO NOT use this template.**
|
||||||
|
Follow the process in [SECURITY.md](../SECURITY.md) for responsible disclosure.
|
||||||
|
|
||||||
|
Use this template only for:
|
||||||
|
- Security improvements
|
||||||
|
- Non-critical security suggestions
|
||||||
|
- Security documentation updates
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security Issue
|
||||||
|
|
||||||
|
**Severity**:
|
||||||
|
<!-- Low, Medium, or informational only -->
|
||||||
|
|
||||||
|
## Description
|
||||||
|
<!-- Describe the security concern or improvement suggestion -->
|
||||||
|
|
||||||
|
## Affected Components
|
||||||
|
<!-- List the affected files, features, or components -->
|
||||||
|
|
||||||
|
## Suggested Mitigation
|
||||||
|
<!-- Describe how this could be addressed -->
|
||||||
|
|
||||||
|
## Standards Reference
|
||||||
|
Does this relate to security standards in [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards)?
|
||||||
|
- [ ] SPDX license identifiers
|
||||||
|
- [ ] Secret management
|
||||||
|
- [ ] Dependency security
|
||||||
|
- [ ] Access control
|
||||||
|
- [ ] Other: [specify]
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
<!-- Add any other context about the security concern -->
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
- [ ] This is NOT a critical vulnerability requiring private disclosure
|
||||||
|
- [ ] I have reviewed the SECURITY.md policy
|
||||||
|
- [ ] I have provided sufficient detail for evaluation
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
name: Version Bump
|
||||||
|
about: Request or track a version change
|
||||||
|
title: '[VERSION] '
|
||||||
|
labels: 'version, type: version'
|
||||||
|
assignees: 'jmiller'
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version Change
|
||||||
|
|
||||||
|
**Current version**: <!-- e.g., 01.02.03 -->
|
||||||
|
**Requested version**: <!-- e.g., 01.03.00 -->
|
||||||
|
**Change type**: <!-- patch / minor / major -->
|
||||||
|
|
||||||
|
## Reason
|
||||||
|
|
||||||
|
<!-- Why is this version bump needed? -->
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
- [ ] README.md `VERSION:` field updated
|
||||||
|
- [ ] CHANGELOG.md entry added
|
||||||
|
- [ ] Module descriptor version updated (Dolibarr: `$this->version`, Joomla: `<version>`)
|
||||||
|
- [ ] All file headers will be auto-propagated by `sync-version-on-merge` workflow
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<mokoplatform xmlns="https://standards.mokoconsulting.tech/mokoplatform/1.0" schema-version="1.0">
|
|
||||||
<identity>
|
|
||||||
<name>MokoSuiteCross</name>
|
|
||||||
<display-name>Package - MokoSuiteCross</display-name>
|
|
||||||
<org>MokoConsulting</org>
|
|
||||||
<description>Cross-posting Joomla content to social media, email marketing, and chat platforms</description>
|
|
||||||
<version>01.02.00</version>
|
|
||||||
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
|
|
||||||
</identity>
|
|
||||||
<governance>
|
|
||||||
<platform>joomla</platform>
|
|
||||||
<standards-version>05.00.00</standards-version>
|
|
||||||
<standards-source>https://git.mokoconsulting.tech/MokoConsulting/mokoplatform</standards-source>
|
|
||||||
</governance>
|
|
||||||
<build>
|
|
||||||
<language>PHP</language>
|
|
||||||
<package-type>joomla-extension</package-type>
|
|
||||||
<entry-point>source/</entry-point>
|
|
||||||
</build>
|
|
||||||
<licensing>
|
|
||||||
<enabled>true</enabled>
|
|
||||||
<dlid>true</dlid>
|
|
||||||
<update-server>https://git.mokoconsulting.tech/{org}/{repo}/updates.xml</update-server>
|
|
||||||
</licensing>
|
|
||||||
</mokoplatform>
|
|
||||||
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: moko-platform.Release
|
# INGROUP: mokocli.Release
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
# PATH: /.mokogitea/workflows/auto-bump.yml
|
# PATH: /.mokogitea/workflows/auto-bump.yml
|
||||||
# VERSION: 09.02.00
|
# VERSION: 09.02.00
|
||||||
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
|
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
|
||||||
@@ -43,19 +43,19 @@ jobs:
|
|||||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
- name: Setup moko-platform tools
|
- name: Setup mokocli tools
|
||||||
run: |
|
run: |
|
||||||
if ! command -v composer &> /dev/null; then
|
if ! command -v composer &> /dev/null; then
|
||||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||||
fi
|
fi
|
||||||
if [ -d "/opt/moko-platform/cli" ]; then
|
if [ -d "/opt/mokocli/cli" ]; then
|
||||||
echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV"
|
echo "MOKO_CLI=/opt/mokocli/cli" >> "$GITHUB_ENV"
|
||||||
else
|
else
|
||||||
git clone --depth 1 --branch main --quiet \
|
git clone --depth 1 --branch main --quiet \
|
||||||
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \
|
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/mokocli.git" \
|
||||||
/tmp/moko-platform-api
|
/tmp/mokocli
|
||||||
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
|
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
|
||||||
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
|
echo "MOKO_CLI=/tmp/mokocli/cli" >> "$GITHUB_ENV"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Bump version
|
- name: Bump version
|
||||||
|
|||||||
@@ -4,15 +4,15 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: mokoplatform.Release
|
# INGROUP: mokocli.Release
|
||||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokoplatform
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli
|
||||||
# PATH: /templates/workflows/universal/auto-release.yml.template
|
# PATH: /templates/workflows/universal/auto-release.yml.template
|
||||||
# VERSION: 05.00.00
|
# VERSION: 05.00.00
|
||||||
# BRIEF: Universal build & release � detects platform from manifest.xml
|
# BRIEF: Universal build & release � detects platform from manifest.xml
|
||||||
#
|
#
|
||||||
# +========================================================================+
|
# +=======================================================================+
|
||||||
# | UNIVERSAL BUILD & RELEASE PIPELINE |
|
# | UNIVERSAL BUILD & RELEASE PIPELINE |
|
||||||
# +========================================================================+
|
# +=======================================================================+
|
||||||
# | |
|
# | |
|
||||||
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
|
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
|
||||||
# | |
|
# | |
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
# | dolibarr: mod*.class.php, update.txt, dev version reset |
|
# | dolibarr: mod*.class.php, update.txt, dev version reset |
|
||||||
# | generic: README-only, no update stream |
|
# | generic: README-only, no update stream |
|
||||||
# | |
|
# | |
|
||||||
# +========================================================================+
|
# +=======================================================================+
|
||||||
|
|
||||||
name: "Universal: Build & Release"
|
name: "Universal: Build & Release"
|
||||||
|
|
||||||
@@ -30,6 +30,15 @@ on:
|
|||||||
types: [opened, closed]
|
types: [opened, closed]
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
paths-ignore:
|
||||||
|
- '.mokogitea/workflows/**'
|
||||||
|
- '*.md'
|
||||||
|
- 'wiki/**'
|
||||||
|
- '.editorconfig'
|
||||||
|
- '.gitignore'
|
||||||
|
- '.gitattributes'
|
||||||
|
- '.gitmessage'
|
||||||
|
- 'LICENSE'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
action:
|
action:
|
||||||
@@ -51,7 +60,7 @@ permissions:
|
|||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# ── PR Opened → Rename branch to RC and build RC release ─────────────────────
|
# ── PR Opened → Rename branch to RC and build RC release ─────────────────────────
|
||||||
promote-rc:
|
promote-rc:
|
||||||
name: Promote to RC
|
name: Promote to RC
|
||||||
runs-on: release
|
runs-on: release
|
||||||
@@ -66,25 +75,25 @@ jobs:
|
|||||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
- name: Setup mokoplatform tools
|
- name: Setup mokocli tools
|
||||||
env:
|
env:
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
run: |
|
run: |
|
||||||
if [ -f /opt/mokoplatform/cli/version_bump.php ] && [ -f /opt/mokoplatform/vendor/autoload.php ]; then
|
if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then
|
||||||
echo Using pre-installed /opt/mokoplatform
|
echo Using pre-installed /opt/mokocli
|
||||||
echo MOKO_CLI=/opt/mokoplatform/cli >> $GITHUB_ENV
|
echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV
|
||||||
else
|
else
|
||||||
echo Falling back to fresh clone
|
echo Falling back to fresh clone
|
||||||
if ! command -v composer > /dev/null 2>&1; then
|
if ! command -v composer > /dev/null 2>&1; then
|
||||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
|
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
|
||||||
fi
|
fi
|
||||||
rm -rf /tmp/mokoplatform-api
|
rm -rf /tmp/mokocli
|
||||||
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokoplatform.git
|
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git
|
||||||
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokoplatform-api
|
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
|
||||||
cd /tmp/mokoplatform-api
|
cd /tmp/mokocli
|
||||||
composer install --no-dev --no-interaction --quiet
|
composer install --no-dev --no-interaction --quiet
|
||||||
echo MOKO_CLI=/tmp/mokoplatform-api/cli >> $GITHUB_ENV
|
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Rename branch to rc
|
- name: Rename branch to rc
|
||||||
@@ -109,13 +118,47 @@ jobs:
|
|||||||
--path . --stability rc --bump minor --branch rc \
|
--path . --stability rc --bump minor --branch rc \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
|
- name: Update RC release notes from CHANGELOG.md
|
||||||
|
run: |
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
|
# Extract [Unreleased] section from changelog
|
||||||
|
NOTES=""
|
||||||
|
if [ -f "CHANGELOG.md" ]; then
|
||||||
|
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
|
||||||
|
fi
|
||||||
|
[ -z "$NOTES" ] && NOTES="Release candidate"
|
||||||
|
|
||||||
|
# Find the RC release and update its body
|
||||||
|
RELEASE_ID=$(curl -sf -H "Authorization: token ${TOKEN}" \
|
||||||
|
"${API_BASE}/releases/tags/release-candidate" \
|
||||||
|
| python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [ -n "$RELEASE_ID" ]; then
|
||||||
|
python3 -c "
|
||||||
|
import json, urllib.request
|
||||||
|
body = open('/dev/stdin').read()
|
||||||
|
payload = json.dumps({'body': body}).encode()
|
||||||
|
req = urllib.request.Request(
|
||||||
|
'${API_BASE}/releases/${RELEASE_ID}',
|
||||||
|
data=payload, method='PATCH',
|
||||||
|
headers={
|
||||||
|
'Authorization': 'token ${TOKEN}',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
})
|
||||||
|
urllib.request.urlopen(req)
|
||||||
|
" <<< "$NOTES"
|
||||||
|
echo "RC release notes updated from CHANGELOG.md"
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Summary
|
- name: Summary
|
||||||
if: always()
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
|
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY
|
echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
# ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
|
# ── Merged PR → Build & Release (or promote RC to stable) ─────────────────────────
|
||||||
release:
|
release:
|
||||||
name: Build & Release Pipeline
|
name: Build & Release Pipeline
|
||||||
runs-on: release
|
runs-on: release
|
||||||
@@ -149,50 +192,131 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
echo "No conflict markers found"
|
echo "No conflict markers found"
|
||||||
|
|
||||||
- name: Setup mokoplatform tools
|
- name: Setup mokocli tools
|
||||||
env:
|
env:
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
|
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
|
||||||
run: |
|
run: |
|
||||||
if [ -f /opt/mokoplatform/cli/version_bump.php ] && [ -f /opt/mokoplatform/vendor/autoload.php ]; then
|
if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then
|
||||||
echo Using pre-installed /opt/mokoplatform
|
echo Using pre-installed /opt/mokocli
|
||||||
echo MOKO_CLI=/opt/mokoplatform/cli >> $GITHUB_ENV
|
echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV
|
||||||
else
|
else
|
||||||
echo Falling back to fresh clone
|
echo Falling back to fresh clone
|
||||||
if ! command -v composer > /dev/null 2>&1; then
|
if ! command -v composer > /dev/null 2>&1; then
|
||||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
|
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
|
||||||
fi
|
fi
|
||||||
rm -rf /tmp/mokoplatform-api
|
rm -rf /tmp/mokocli
|
||||||
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokoplatform.git
|
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git
|
||||||
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokoplatform-api
|
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
|
||||||
cd /tmp/mokoplatform-api
|
cd /tmp/mokocli
|
||||||
composer install --no-dev --no-interaction --quiet
|
composer install --no-dev --no-interaction --quiet
|
||||||
echo MOKO_CLI=/tmp/mokoplatform-api/cli >> $GITHUB_ENV
|
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
- name: "Detect platform"
|
||||||
|
id: platform
|
||||||
|
run: |
|
||||||
|
php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true
|
||||||
|
php ${MOKO_CLI}/manifest_read.php --path . --github-output 2>/dev/null || true
|
||||||
|
|
||||||
|
- name: "Determine version bump level"
|
||||||
|
id: bump
|
||||||
|
run: |
|
||||||
|
# Fix/patch branches: version was already bumped by pre-release, just strip suffix
|
||||||
|
# Feature/dev branches: bump minor for the new stable release
|
||||||
|
HEAD_REF="${{ github.event.pull_request.head.ref || 'dev' }}"
|
||||||
|
case "$HEAD_REF" in
|
||||||
|
fix/*|patch/*|hotfix/*|bugfix/*) BUMP="none" ;;
|
||||||
|
*) BUMP="minor" ;;
|
||||||
|
esac
|
||||||
|
echo "level=${BUMP}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Bump level: ${BUMP} (from branch: ${HEAD_REF})"
|
||||||
|
|
||||||
- name: "Publish stable release"
|
- name: "Publish stable release"
|
||||||
run: |
|
run: |
|
||||||
|
BUMP_FLAG=""
|
||||||
|
if [ "${{ steps.bump.outputs.level }}" != "none" ]; then
|
||||||
|
BUMP_FLAG="--bump ${{ steps.bump.outputs.level }}"
|
||||||
|
fi
|
||||||
php ${MOKO_CLI}/release_publish.php \
|
php ${MOKO_CLI}/release_publish.php \
|
||||||
--path . --stability stable --bump minor --branch main \
|
--path . --stability stable ${BUMP_FLAG} --branch main \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
- name: Update release notes from CHANGELOG.md
|
- name: "Read published version"
|
||||||
|
id: version
|
||||||
run: |
|
run: |
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "")
|
||||||
|
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
||||||
# Extract [Unreleased] section from changelog
|
[ -z "$VERSION" ] && VERSION="00.00.00" && echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||||
if [ -f "CHANGELOG.md" ]; then
|
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
|
PLATFORM="${{ steps.platform.outputs.platform }}"
|
||||||
[ -z "$NOTES" ] && NOTES="Stable release"
|
if [[ "$PLATFORM" == joomla* ]]; then
|
||||||
|
echo "tag=stable" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "release_tag=stable" >> "$GITHUB_OUTPUT"
|
||||||
else
|
else
|
||||||
NOTES="Stable release"
|
echo "tag=v${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "release_tag=v${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
echo "branch=main" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Published version: ${VERSION}"
|
||||||
|
|
||||||
|
- name: "Create semver tag for non-Joomla repos"
|
||||||
|
id: semver
|
||||||
|
if: |
|
||||||
|
steps.version.outputs.skip != 'true' &&
|
||||||
|
!startsWith(steps.platform.outputs.platform, 'joomla')
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
SEMVER_TAG="v${VERSION}"
|
||||||
|
|
||||||
|
echo "Creating semver tag: ${SEMVER_TAG}"
|
||||||
|
|
||||||
|
# Create the git tag via API
|
||||||
|
HTTP_CODE=$(curl -sf -o /dev/null -w "%{http_code}" \
|
||||||
|
-X POST -H "Authorization: token ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API_BASE}/tags" \
|
||||||
|
-d "{\"tag_name\":\"${SEMVER_TAG}\",\"target\":\"main\",\"message\":\"Release ${VERSION}\"}" 2>/dev/null || echo "000")
|
||||||
|
|
||||||
|
if [ "$HTTP_CODE" = "201" ] || [ "$HTTP_CODE" = "200" ]; then
|
||||||
|
echo "Created semver tag: ${SEMVER_TAG}"
|
||||||
|
elif [ "$HTTP_CODE" = "409" ]; then
|
||||||
|
echo "Semver tag ${SEMVER_TAG} already exists (skipped)"
|
||||||
|
else
|
||||||
|
echo "::warning::Failed to create semver tag ${SEMVER_TAG} (HTTP ${HTTP_CODE})"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Update release body via API
|
echo "semver_tag=${SEMVER_TAG}" >> "$GITHUB_OUTPUT"
|
||||||
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
|
||||||
"${API_BASE}/releases/tags/stable" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
|
||||||
|
|
||||||
|
- name: Update release notes and promote changelog
|
||||||
|
run: |
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
|
# Get the stable release info (version and ID)
|
||||||
|
RELEASE_JSON=$(curl -sf -H "Authorization: token ${TOKEN}" \
|
||||||
|
"${API_BASE}/releases/tags/stable" 2>/dev/null || echo '{}')
|
||||||
|
RELEASE_ID=$(python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" <<< "$RELEASE_JSON" 2>/dev/null || true)
|
||||||
|
# Extract version from release name (e.g. "06.17.00" or "v06.17.00")
|
||||||
|
VERSION=$(python3 -c "
|
||||||
|
import json, sys, re
|
||||||
|
r = json.load(sys.stdin)
|
||||||
|
name = r.get('name', '')
|
||||||
|
m = re.search(r'(\d+\.\d+\.\d+)', name)
|
||||||
|
print(m.group(1) if m else '')
|
||||||
|
" <<< "$RELEASE_JSON" 2>/dev/null || true)
|
||||||
|
|
||||||
|
# Extract [Unreleased] section from changelog
|
||||||
|
NOTES=""
|
||||||
|
if [ -f "CHANGELOG.md" ]; then
|
||||||
|
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
|
||||||
|
fi
|
||||||
|
[ -z "$NOTES" ] && NOTES="Stable release"
|
||||||
|
|
||||||
|
# Update release body via API
|
||||||
if [ -n "$RELEASE_ID" ]; then
|
if [ -n "$RELEASE_ID" ]; then
|
||||||
python3 -c "
|
python3 -c "
|
||||||
import json, urllib.request
|
import json, urllib.request
|
||||||
@@ -202,7 +326,7 @@ jobs:
|
|||||||
'${API_BASE}/releases/${RELEASE_ID}',
|
'${API_BASE}/releases/${RELEASE_ID}',
|
||||||
data=payload, method='PATCH',
|
data=payload, method='PATCH',
|
||||||
headers={
|
headers={
|
||||||
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
|
'Authorization': 'token ${TOKEN}',
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
})
|
})
|
||||||
urllib.request.urlopen(req)
|
urllib.request.urlopen(req)
|
||||||
@@ -210,6 +334,24 @@ jobs:
|
|||||||
echo "Release notes updated from CHANGELOG.md"
|
echo "Release notes updated from CHANGELOG.md"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Promote [Unreleased] → [version] in CHANGELOG.md and reset
|
||||||
|
if [ -n "$VERSION" ] && [ -f "CHANGELOG.md" ]; then
|
||||||
|
DATE=$(date +%Y-%m-%d)
|
||||||
|
python3 -c "
|
||||||
|
import sys
|
||||||
|
version, date = sys.argv[1], sys.argv[2]
|
||||||
|
content = open('CHANGELOG.md').read()
|
||||||
|
old = '## [Unreleased]'
|
||||||
|
new = f'## [Unreleased]\n\n## [{version}] --- {date}'
|
||||||
|
content = content.replace(old, new, 1)
|
||||||
|
open('CHANGELOG.md', 'w').write(content)
|
||||||
|
" "$VERSION" "$DATE"
|
||||||
|
git add CHANGELOG.md
|
||||||
|
git commit -m "chore: promote changelog [Unreleased] → [${VERSION}]" || true
|
||||||
|
git push origin main || true
|
||||||
|
echo "Changelog promoted: [Unreleased] → [${VERSION}]"
|
||||||
|
fi
|
||||||
|
|
||||||
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
|
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
|
||||||
- name: "Step 9: Mirror release to GitHub"
|
- name: "Step 9: Mirror release to GitHub"
|
||||||
if: >-
|
if: >-
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: MokoStandards.Universal
|
# INGROUP: MokoStandards.Universal
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
# PATH: /.mokogitea/workflows/branch-cleanup.yml
|
# PATH: /.mokogitea/workflows/branch-cleanup.yml
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Delete feature branches after PR merge
|
# BRIEF: Delete feature branches after PR merge
|
||||||
|
|||||||
@@ -13,19 +13,6 @@
|
|||||||
name: "Generic: Project CI"
|
name: "Generic: Project CI"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- dev
|
|
||||||
- dev/**
|
|
||||||
- rc/**
|
|
||||||
- version/**
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- dev
|
|
||||||
- dev/**
|
|
||||||
- rc/**
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
IyBDb3B5cmlnaHQgKEMpIDIwMjYgTW9rbyBDb25zdWx0aW5nIDxoZWxsb0Btb2tvY29uc3VsdGluZy50ZWNoPgojCiMgU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IEdQTC0zLjAtb3ItbGF0ZXIKIwojIEZJTEUgSU5GT1JNQVRJT04KIyBERUZHUk9VUDogR2l0ZWEuV29ya2Zsb3cKIyBJTkdST1VQOiBtb2tvY2xpLlVuaXZlcnNhbAojIFJFUE86IGh0dHBzOi8vZ2l0Lm1va29jb25zdWx0aW5nLnRlY2gvTW9rb0NvbnN1bHRpbmcvbW9rb2NsaQojIFBBVEg6IC8ubW9rb2dpdGVhL3dvcmtmbG93cy9jaS1pc3N1ZS1yZXBvcnRlci55bWwKIyBWRVJTSU9OOiAwMS4wMC4wMAojIEJSSUVGOiBSZXVzYWJsZSB3b3JrZmxvdyDigJQgY3JlYXRlcy91cGRhdGVzIGEgR2l0ZWEgaXNzdWUgd2hlbiBhIENJIGdhdGUgZmFpbHMuCiMgICAgICAgIENsb25lcyBNb2tvQ0xJIGFuZCBydW5zIGNsaS9jaV9pc3N1ZV9yZXBvcnRlci5zaC4KCm5hbWU6ICJVbml2ZXJzYWw6IENJIElzc3VlIFJlcG9ydGVyIgoKb246CiAgd29ya2Zsb3dfY2FsbDoKICAgIGlucHV0czoKICAgICAgZ2F0ZToKICAgICAgICBkZXNjcmlwdGlvbjogIkNJIGdhdGUgbmFtZSAoZS5nLiBQUiBWYWxpZGF0aW9uLCBSZXBvc2l0b3J5IEhlYWx0aCkiCiAgICAgICAgcmVxdWlyZWQ6IHRydWUKICAgICAgICB0eXBlOiBzdHJpbmcKICAgICAgZGV0YWlsczoKICAgICAgICBkZXNjcmlwdGlvbjogIkh1bWFuLXJlYWRhYmxlIGZhaWx1cmUgZGVzY3JpcHRpb24iCiAgICAgICAgcmVxdWlyZWQ6IHRydWUKICAgICAgICB0eXBlOiBzdHJpbmcKICAgICAgc2V2ZXJpdHk6CiAgICAgICAgZGVzY3JpcHRpb246ICJlcnJvciBvciB3YXJuaW5nIgogICAgICAgIHJlcXVpcmVkOiBmYWxzZQogICAgICAgIHR5cGU6IHN0cmluZwogICAgICAgIGRlZmF1bHQ6ICJlcnJvciIKICAgICAgd29ya2Zsb3c6CiAgICAgICAgZGVzY3JpcHRpb246ICJXb3JrZmxvdyBuYW1lIGZvciB0aGUgaXNzdWUgdGl0bGUiCiAgICAgICAgcmVxdWlyZWQ6IGZhbHNlCiAgICAgICAgdHlwZTogc3RyaW5nCiAgICAgICAgZGVmYXVsdDogIiIKICAgIHNlY3JldHM6CiAgICAgIE1PS09HSVRFQV9UT0tFTjoKICAgICAgICByZXF1aXJlZDogdHJ1ZQoKZW52OgogIEZPUkNFX0pBVkFTQ1JJUFRfQUNUSU9OU19UT19OT0RFMjQ6IHRydWUKCmpvYnM6CiAgcmVwb3J0OgogICAgbmFtZTogIlJlcG9ydDogJHt7IGlucHV0cy5nYXRlIH19IgogICAgcnVucy1vbjogdWJ1bnR1LWxhdGVzdAoKICAgIHN0ZXBzOgogICAgICAtIG5hbWU6IENsb25lIE1va29DTEkKICAgICAgICBlbnY6CiAgICAgICAgICBNT0tPR0lURUFfVE9LRU46ICR7eyBzZWNyZXRzLk1PS09HSVRFQV9UT0tFTiB9fQogICAgICAgIHJ1bjogfAogICAgICAgICAgTU9LT0dJVEVBX1VSTD0iJHt7IHZhcnMuR0lURUFfVVJMIHx8ICdodHRwczovL2dpdC5tb2tvY29uc3VsdGluZy50ZWNoJyB9fSIKICAgICAgICAgIGdpdCBjbG9uZSAtLWRlcHRoIDEgLS1maWx0ZXI9YmxvYjpub25lIC0tc3BhcnNlICIke01PS09HSVRFQV9VUkx9L01va29Db25zdWx0aW5nL01va29DTEkuZ2l0IiAvdG1wL21va29jbGkKICAgICAgICAgIGNkIC90bXAvbW9rb2NsaSAmJiBnaXQgc3BhcnNlLWNoZWNrb3V0IHNldCBjbGkvY2lfaXNzdWVfcmVwb3J0ZXIuc2gKCiAgICAgIC0gbmFtZTogUmVwb3J0IENJIGZhaWx1cmUKICAgICAgICBlbnY6CiAgICAgICAgICBNT0tPR0lURUFfVE9LRU46ICR7eyBzZWNyZXRzLk1PS09HSVRFQV9UT0tFTiB9fQogICAgICAgICAgTU9LT0dJVEVBX1VSTDogJHt7IHZhcnMuR0lURUFfVVJMIHx8ICdodHRwczovL2dpdC5tb2tvY29uc3VsdGluZy50ZWNoJyB9fQogICAgICAgIHJ1bjogfAogICAgICAgICAgY2htb2QgK3ggL3RtcC9tb2tvY2xpL2NsaS9jaV9pc3N1ZV9yZXBvcnRlci5zaAogICAgICAgICAgL3RtcC9tb2tvY2xpL2NsaS9jaV9pc3N1ZV9yZXBvcnRlci5zaCBcCiAgICAgICAgICAgIC0tZ2F0ZSAiJHt7IGlucHV0cy5nYXRlIH19IiBcCiAgICAgICAgICAgIC0tZGV0YWlscyAiJHt7IGlucHV0cy5kZXRhaWxzIH19IiBcCiAgICAgICAgICAgIC0tc2V2ZXJpdHkgIiR7eyBpbnB1dHMuc2V2ZXJpdHkgfX0iIFwKICAgICAgICAgICAgLS13b3JrZmxvdyAiJHt7IGlucHV0cy53b3JrZmxvdyB9fSIK
|
||||||
@@ -45,17 +45,17 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
php -v && composer --version
|
php -v && composer --version
|
||||||
|
|
||||||
- name: Setup moko-platform tools
|
- name: Setup mokocli tools
|
||||||
env:
|
env:
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || github.token }}
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.GA_TOKEN || github.token }}
|
||||||
MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
|
MOKO_CLONE_HOST: ${{ secrets.MOKOGITEA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
|
||||||
run: |
|
run: |
|
||||||
if [ -d "/tmp/moko-platform" ] || [ -d "/opt/moko-platform" ]; then
|
if [ -d "/opt/mokocli" ] || [ -d "/tmp/mokocli" ]; then
|
||||||
echo "moko-platform already available on runner — skipping clone"
|
echo "mokocli already available on runner — skipping clone"
|
||||||
else
|
else
|
||||||
git clone --depth 1 --branch main --quiet \
|
git clone --depth 1 --branch main --quiet \
|
||||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git" \
|
||||||
/tmp/moko-platform 2>/dev/null || echo "moko-platform clone skipped — continuing without it"
|
/tmp/mokocli 2>/dev/null || echo "mokocli clone skipped — continuing without it"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
@@ -164,6 +164,75 @@ jobs:
|
|||||||
echo "**Manifest validation passed.**" >> $GITHUB_STEP_SUMMARY
|
echo "**Manifest validation passed.**" >> $GITHUB_STEP_SUMMARY
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
- name: Update server & packaging checks
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
echo "### Update Server & Packaging" >> $GITHUB_STEP_SUMMARY
|
||||||
|
WARNINGS=0
|
||||||
|
|
||||||
|
# Find the extension manifest
|
||||||
|
MANIFEST=""
|
||||||
|
for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
|
||||||
|
if grep -q "<extension" "$XML_FILE" 2>/dev/null; then
|
||||||
|
MANIFEST="$XML_FILE"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$MANIFEST" ]; then
|
||||||
|
echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
EXT_TYPE=$(grep -oP '<extension[^>]*\btype="\K[^"]+' "$MANIFEST" | head -1)
|
||||||
|
|
||||||
|
# 1. Check <updateservers> exists and uses MokoGitea update server
|
||||||
|
if ! grep -q '<updateservers>' "$MANIFEST" 2>/dev/null; then
|
||||||
|
echo "::warning file=${MANIFEST}::Missing \`<updateservers>\` tag — extension will not receive OTA updates"
|
||||||
|
echo "- **Missing** \`<updateservers>\` — extension will not receive OTA updates" >> $GITHUB_STEP_SUMMARY
|
||||||
|
WARNINGS=$((WARNINGS + 1))
|
||||||
|
else
|
||||||
|
SERVER_URL=$(grep -oP '<server[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1)
|
||||||
|
if [ -z "$SERVER_URL" ]; then
|
||||||
|
echo "::warning file=${MANIFEST}::\`<updateservers>\` is empty — no server URL defined"
|
||||||
|
echo "- **Empty** \`<updateservers>\` — no server URL defined" >> $GITHUB_STEP_SUMMARY
|
||||||
|
WARNINGS=$((WARNINGS + 1))
|
||||||
|
elif ! echo "$SERVER_URL" | grep -q 'git\.mokoconsulting\.tech'; then
|
||||||
|
echo "::warning file=${MANIFEST}::Update server does not use MokoGitea engine: ${SERVER_URL}"
|
||||||
|
echo "- **Non-MokoGitea update server:** \`${SERVER_URL}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo " Expected: \`https://git.mokoconsulting.tech/{org}/{repo}/updates.xml\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
WARNINGS=$((WARNINGS + 1))
|
||||||
|
else
|
||||||
|
echo "- \`<updateservers>\`: MokoGitea engine ✓" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. Check <dlid> tag exists
|
||||||
|
if ! grep -q '<dlid' "$MANIFEST" 2>/dev/null; then
|
||||||
|
echo "::warning file=${MANIFEST}::Missing \`<dlid>\` tag — download ID authentication is not configured"
|
||||||
|
echo "- **Missing** \`<dlid>\` — download ID authentication not configured" >> $GITHUB_STEP_SUMMARY
|
||||||
|
WARNINGS=$((WARNINGS + 1))
|
||||||
|
else
|
||||||
|
echo "- \`<dlid>\`: present ✓" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3. For packages: check <childuninstall> tag
|
||||||
|
if [ "$EXT_TYPE" = "package" ]; then
|
||||||
|
if ! grep -q '<childuninstall>' "$MANIFEST" 2>/dev/null; then
|
||||||
|
echo "::warning file=${MANIFEST}::Package is missing \`<childuninstall>\` — child extensions will not be removed on uninstall"
|
||||||
|
echo "- **Missing** \`<childuninstall>\` — child extensions will remain when package is uninstalled" >> $GITHUB_STEP_SUMMARY
|
||||||
|
WARNINGS=$((WARNINGS + 1))
|
||||||
|
else
|
||||||
|
echo "- \`<childuninstall>\`: present ✓" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
if [ "$WARNINGS" -gt 0 ]; then
|
||||||
|
echo "**${WARNINGS} packaging warning(s).** These won't block CI but should be addressed." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "**Update server & packaging checks passed.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Check language files referenced in manifest
|
- name: Check language files referenced in manifest
|
||||||
run: |
|
run: |
|
||||||
echo "### Language File Check" >> $GITHUB_STEP_SUMMARY
|
echo "### Language File Check" >> $GITHUB_STEP_SUMMARY
|
||||||
@@ -245,10 +314,675 @@ jobs:
|
|||||||
echo "All ${CHECKED} directories contain index.html." >> $GITHUB_STEP_SUMMARY
|
echo "All ${CHECKED} directories contain index.html." >> $GITHUB_STEP_SUMMARY
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
- name: Check config.xml and access.xml for components
|
||||||
|
run: |
|
||||||
|
echo "### Component Config & ACL Check" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=0
|
||||||
|
|
||||||
|
# Find all component manifests (XML with type="component")
|
||||||
|
COMP_MANIFESTS=$(find . -maxdepth 4 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*" -exec grep -l '<extension[^>]*type="component"' {} ; 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [ -z "$COMP_MANIFESTS" ]; then
|
||||||
|
echo "No component extensions found — skipping." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
for MANIFEST in $COMP_MANIFESTS; do
|
||||||
|
COMP_DIR=$(dirname "$MANIFEST")
|
||||||
|
COMP_NAME=$(basename "$COMP_DIR")
|
||||||
|
echo "Component: `${COMP_NAME}` (manifest: `${MANIFEST}`)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# Check access.xml exists
|
||||||
|
ACCESS_FILE=$(find "$COMP_DIR" -name "access.xml" -not -path "./.git/*" 2>/dev/null | head -1)
|
||||||
|
if [ -z "$ACCESS_FILE" ]; then
|
||||||
|
echo "- Missing `access.xml` — ACL permissions will not work." >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
if command -v php &> /dev/null; then
|
||||||
|
if ! php -r "@simplexml_load_file('$ACCESS_FILE') ?: exit(1);" 2>/dev/null; then
|
||||||
|
echo "- `access.xml` is not well-formed XML." >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
for ACTION in core.admin core.manage; do
|
||||||
|
if ! grep -q "name=\"${ACTION}\"" "$ACCESS_FILE" 2>/dev/null; then
|
||||||
|
echo "- `access.xml` missing required action: `${ACTION}`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "- `access.xml`: valid" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check config.xml exists
|
||||||
|
CONFIG_FILE=$(find "$COMP_DIR" -name "config.xml" -not -path "./.git/*" 2>/dev/null | head -1)
|
||||||
|
if [ -z "$CONFIG_FILE" ]; then
|
||||||
|
echo "- Missing `config.xml` — component Options page will be empty." >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
if command -v php &> /dev/null; then
|
||||||
|
if ! php -r "@simplexml_load_file('$CONFIG_FILE') ?: exit(1);" 2>/dev/null; then
|
||||||
|
echo "- `config.xml` is not well-formed XML." >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "- `config.xml`: valid" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
if [ "${ERRORS}" -gt 0 ]; then
|
||||||
|
echo "**${ERRORS} config/ACL issue(s) found.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "**Component config & ACL check passed.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: SQL schema validation
|
||||||
|
run: |
|
||||||
|
echo "### SQL Schema Validation" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=0
|
||||||
|
|
||||||
|
# Find SQL files in source/htdocs
|
||||||
|
SQL_FILES=$(find . -name "*.sql" -path "*/sql/*" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null)
|
||||||
|
if [ -z "$SQL_FILES" ]; then
|
||||||
|
echo "No SQL files found — skipping." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "Found $(echo "$SQL_FILES" | wc -l) SQL file(s)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
for FILE in $SQL_FILES; do
|
||||||
|
# Basic syntax check: balanced parentheses, no empty files
|
||||||
|
SIZE=$(wc -c < "$FILE" | tr -d ' ')
|
||||||
|
if [ "$SIZE" -eq 0 ]; then
|
||||||
|
echo "- Empty SQL file: \`${FILE}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for common SQL errors
|
||||||
|
if grep -qP '^\s*$' "$FILE" && [ "$SIZE" -lt 5 ]; then
|
||||||
|
echo "- Whitespace-only SQL file: \`${FILE}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "- \`${FILE}\`: ${SIZE} bytes" >> $GITHUB_STEP_SUMMARY
|
||||||
|
done
|
||||||
|
|
||||||
|
# Check update SQL files follow version numbering pattern
|
||||||
|
UPDATE_DIR=$(find . -path "*/sql/updates/mysql" -type d -not -path "./.git/*" 2>/dev/null | head -1)
|
||||||
|
if [ -n "$UPDATE_DIR" ]; then
|
||||||
|
BAD_NAMES=0
|
||||||
|
for UFILE in "$UPDATE_DIR"/*.sql; do
|
||||||
|
[ ! -f "$UFILE" ] && continue
|
||||||
|
BASENAME=$(basename "$UFILE" .sql)
|
||||||
|
if ! echo "$BASENAME" | grep -qP '^\d+\.\d+\.\d+'; then
|
||||||
|
echo "- Update file \`${UFILE}\` does not follow version naming (expected X.Y.Z.sql)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
BAD_NAMES=$((BAD_NAMES + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ "$BAD_NAMES" -gt 0 ]; then
|
||||||
|
ERRORS=$((ERRORS + BAD_NAMES))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
if [ "${ERRORS}" -gt 0 ]; then
|
||||||
|
echo "**${ERRORS} SQL issue(s) found.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "**SQL schema validation passed.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Manifest file references check
|
||||||
|
run: |
|
||||||
|
echo "### Manifest File References" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=0
|
||||||
|
|
||||||
|
MANIFEST=""
|
||||||
|
for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
|
||||||
|
if grep -q "<extension" "$XML_FILE" 2>/dev/null; then
|
||||||
|
MANIFEST="$XML_FILE"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$MANIFEST" ]; then
|
||||||
|
echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
MANIFEST_DIR=$(dirname "$MANIFEST")
|
||||||
|
|
||||||
|
# Check <filename> references
|
||||||
|
FILENAMES=$(grep -oP '<filename[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null || true)
|
||||||
|
for F in $FILENAMES; do
|
||||||
|
if [ ! -f "${MANIFEST_DIR}/${F}" ] && [ ! -d "${MANIFEST_DIR}/${F}" ]; then
|
||||||
|
echo "- Missing: \`${F}\` (referenced in manifest)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Check <folder> references
|
||||||
|
FOLDERS=$(grep -oP '<folder[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null || true)
|
||||||
|
for F in $FOLDERS; do
|
||||||
|
if [ ! -d "${MANIFEST_DIR}/${F}" ]; then
|
||||||
|
echo "- Missing folder: \`${F}\` (referenced in manifest)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Check <file> references in package manifests (ZIP files won't exist in source)
|
||||||
|
EXT_TYPE=$(grep -oP '<extension[^>]*\btype="\K[^"]+' "$MANIFEST" | head -1)
|
||||||
|
if [ "$EXT_TYPE" != "package" ]; then
|
||||||
|
FILES=$(grep -oP '<file[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null || true)
|
||||||
|
for F in $FILES; do
|
||||||
|
if [ ! -f "${MANIFEST_DIR}/${F}" ]; then
|
||||||
|
echo "- Missing file: \`${F}\` (referenced in manifest)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
if [ "${ERRORS}" -gt 0 ]; then
|
||||||
|
echo "**${ERRORS} missing file reference(s).**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "**Manifest file references check passed.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Form XML validation
|
||||||
|
run: |
|
||||||
|
echo "### Form XML Validation" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=0
|
||||||
|
|
||||||
|
FORM_FILES=$(find . -name "*.xml" -path "*/forms/*" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null)
|
||||||
|
if [ -z "$FORM_FILES" ]; then
|
||||||
|
echo "No form XML files found — skipping." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "Found $(echo "$FORM_FILES" | wc -l) form file(s)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
for FILE in $FORM_FILES; do
|
||||||
|
if command -v php &> /dev/null; then
|
||||||
|
if ! php -r "@simplexml_load_file('$FILE') ?: exit(1);" 2>/dev/null; then
|
||||||
|
echo "- \`${FILE}\`: malformed XML" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
# Check for valid Joomla form structure
|
||||||
|
if ! grep -qE '<form|<field|<fieldset' "$FILE" 2>/dev/null; then
|
||||||
|
echo "- \`${FILE}\`: no \`<form>\`, \`<field>\`, or \`<fieldset>\` elements found" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "- \`${FILE}\`: valid" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
if [ "${ERRORS}" -gt 0 ]; then
|
||||||
|
echo "**${ERRORS} form XML issue(s).**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "**Form XML validation passed.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Deprecated Joomla API check
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
echo "### Deprecated Joomla API Check" >> $GITHUB_STEP_SUMMARY
|
||||||
|
WARNINGS=0
|
||||||
|
|
||||||
|
SRC_DIR=""
|
||||||
|
for DIR in source/ src/ htdocs/; do
|
||||||
|
[ -d "$DIR" ] && SRC_DIR="$DIR" && break
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$SRC_DIR" ]; then
|
||||||
|
echo "No source directory found — skipping." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
# Joomla 3/4 deprecated patterns that break in Joomla 6
|
||||||
|
PATTERNS=(
|
||||||
|
'JFactory::'
|
||||||
|
'JText::'
|
||||||
|
'JHtml::'
|
||||||
|
'JRoute::'
|
||||||
|
'JUri::'
|
||||||
|
'JLog::'
|
||||||
|
'JTable::'
|
||||||
|
'JInput'
|
||||||
|
'CMSFactory::\$application'
|
||||||
|
'JApplicationCms'
|
||||||
|
)
|
||||||
|
|
||||||
|
for PATTERN in "${PATTERNS[@]}"; do
|
||||||
|
HITS=$(grep -rnl "$PATTERN" "$SRC_DIR" --include="*.php" 2>/dev/null || true)
|
||||||
|
if [ -n "$HITS" ]; then
|
||||||
|
COUNT=$(echo "$HITS" | wc -l)
|
||||||
|
echo "- \`${PATTERN}\` found in ${COUNT} file(s)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
WARNINGS=$((WARNINGS + COUNT))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
if [ "$WARNINGS" -gt 0 ]; then
|
||||||
|
echo "**${WARNINGS} deprecated API usage(s) found.** These will break in Joomla 6." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "**No deprecated APIs found.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Template output escaping check
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
echo "### Template Output Escaping" >> $GITHUB_STEP_SUMMARY
|
||||||
|
WARNINGS=0
|
||||||
|
|
||||||
|
TMPL_FILES=$(find . -name "*.php" -path "*/tmpl/*" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null)
|
||||||
|
if [ -z "$TMPL_FILES" ]; then
|
||||||
|
echo "No template files found — skipping." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "Found $(echo "$TMPL_FILES" | wc -l) template file(s)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
for FILE in $TMPL_FILES; do
|
||||||
|
# Check for unescaped output: <?= $var ?> or echo $var without escape()
|
||||||
|
UNESCAPED=$(grep -nP '<\?=\s*\$(?!this->escape)' "$FILE" 2>/dev/null || true)
|
||||||
|
if [ -n "$UNESCAPED" ]; then
|
||||||
|
HITS=$(echo "$UNESCAPED" | wc -l)
|
||||||
|
echo "- \`${FILE}\`: ${HITS} unescaped \`<?= \$var ?>\` output(s) — use \`<?= \$this->escape(\$var) ?>\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
WARNINGS=$((WARNINGS + HITS))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for echo without escaping in template context
|
||||||
|
RAW_ECHO=$(grep -nP '^\s*echo\s+\$(?!this->escape)' "$FILE" 2>/dev/null || true)
|
||||||
|
if [ -n "$RAW_ECHO" ]; then
|
||||||
|
HITS=$(echo "$RAW_ECHO" | wc -l)
|
||||||
|
echo "- \`${FILE}\`: ${HITS} raw \`echo \$var\` — consider \`echo \$this->escape(\$var)\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
WARNINGS=$((WARNINGS + HITS))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
if [ "$WARNINGS" -gt 0 ]; then
|
||||||
|
echo "**${WARNINGS} potential XSS risk(s) in templates.** Review unescaped output." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "**All template output appears properly escaped.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Namespace consistency check
|
||||||
|
run: |
|
||||||
|
echo "### Namespace Consistency" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=0
|
||||||
|
|
||||||
|
# Find component/plugin manifests with <namespace> tags
|
||||||
|
MANIFESTS=$(find . -maxdepth 4 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*" -exec grep -l '<namespace' {} \; 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [ -z "$MANIFESTS" ]; then
|
||||||
|
echo "No manifests with \`<namespace>\` found — skipping." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
for MANIFEST in $MANIFESTS; do
|
||||||
|
NS_PATH=$(grep -oP '<namespace[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1)
|
||||||
|
[ -z "$NS_PATH" ] && continue
|
||||||
|
MANIFEST_DIR=$(dirname "$MANIFEST")
|
||||||
|
|
||||||
|
echo "Manifest: \`${MANIFEST}\` → namespace \`${NS_PATH}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# Check PHP files have matching namespace
|
||||||
|
while IFS= read -r -d '' PHP_FILE; do
|
||||||
|
FILE_NS=$(grep -oP '^\s*namespace\s+\K[^;]+' "$PHP_FILE" 2>/dev/null | head -1)
|
||||||
|
[ -z "$FILE_NS" ] && continue
|
||||||
|
|
||||||
|
# Namespace should start with the manifest namespace path
|
||||||
|
if ! echo "$FILE_NS" | grep -qF "${NS_PATH}"; then
|
||||||
|
echo "- \`${PHP_FILE}\`: namespace \`${FILE_NS}\` doesn't match manifest \`${NS_PATH}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
done < <(find "$MANIFEST_DIR" -name "*.php" -path "*/src/*" -not -path "./vendor/*" -print0 2>/dev/null)
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
if [ "${ERRORS}" -gt 0 ]; then
|
||||||
|
echo "**${ERRORS} namespace mismatch(es).**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "**Namespace consistency check passed.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: SPDX license header check
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
echo "### SPDX License Headers" >> $GITHUB_STEP_SUMMARY
|
||||||
|
MISSING=0
|
||||||
|
|
||||||
|
SRC_DIR=""
|
||||||
|
for DIR in source/ src/ htdocs/; do
|
||||||
|
[ -d "$DIR" ] && SRC_DIR="$DIR" && break
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$SRC_DIR" ]; then
|
||||||
|
echo "No source directory found — skipping." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
TOTAL=0
|
||||||
|
while IFS= read -r -d '' FILE; do
|
||||||
|
TOTAL=$((TOTAL + 1))
|
||||||
|
if ! head -10 "$FILE" | grep -qi "SPDX"; then
|
||||||
|
echo "- Missing SPDX header: \`${FILE}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
MISSING=$((MISSING + 1))
|
||||||
|
fi
|
||||||
|
done < <(find "$SRC_DIR" -name "*.php" -not -path "./vendor/*" -print0)
|
||||||
|
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
if [ "$MISSING" -gt 0 ]; then
|
||||||
|
echo "**${MISSING}/${TOTAL} PHP file(s) missing SPDX license header.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "**All ${TOTAL} PHP files have SPDX headers.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Service provider check
|
||||||
|
run: |
|
||||||
|
echo "### Service Provider Check" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=0
|
||||||
|
|
||||||
|
PROVIDERS=$(find . -name "provider.php" -path "*/services/*" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null)
|
||||||
|
if [ -z "$PROVIDERS" ]; then
|
||||||
|
echo "No service providers found — skipping." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
for FILE in $PROVIDERS; do
|
||||||
|
# Must return a ServiceProviderInterface
|
||||||
|
if ! grep -qP 'ServiceProviderInterface|ComponentInterface|MVCFactoryInterface|DispatcherInterface' "$FILE" 2>/dev/null; then
|
||||||
|
echo "- \`${FILE}\`: does not reference ServiceProviderInterface or component interfaces" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "- \`${FILE}\`: valid service provider" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Must have return statement
|
||||||
|
if ! grep -qP '^\s*return\s+new\s+' "$FILE" 2>/dev/null; then
|
||||||
|
echo "- \`${FILE}\`: missing \`return new ...\` statement" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
if [ "${ERRORS}" -gt 0 ]; then
|
||||||
|
echo "**${ERRORS} service provider issue(s).**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "**Service provider check passed.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Script file reference check
|
||||||
|
run: |
|
||||||
|
echo "### Script File Reference" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=0
|
||||||
|
|
||||||
|
MANIFEST=""
|
||||||
|
for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
|
||||||
|
if grep -q "<extension" "$XML_FILE" 2>/dev/null; then
|
||||||
|
MANIFEST="$XML_FILE"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$MANIFEST" ]; then
|
||||||
|
echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
MANIFEST_DIR=$(dirname "$MANIFEST")
|
||||||
|
SCRIPT_FILE=$(grep -oP '<scriptfile>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1)
|
||||||
|
if [ -z "$SCRIPT_FILE" ]; then
|
||||||
|
echo "No \`<scriptfile>\` referenced — skipping." >> $GITHUB_STEP_SUMMARY
|
||||||
|
elif [ ! -f "${MANIFEST_DIR}/${SCRIPT_FILE}" ]; then
|
||||||
|
echo "::error file=${MANIFEST}::Manifest references \`<scriptfile>${SCRIPT_FILE}</scriptfile>\` but file does not exist"
|
||||||
|
echo "- **Missing** \`${SCRIPT_FILE}\` — referenced in \`<scriptfile>\` but not found" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "- \`${SCRIPT_FILE}\`: present ✓" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
if [ "${ERRORS}" -gt 0 ]; then
|
||||||
|
echo "**${ERRORS} script file issue(s).**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "**Script file reference check passed.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Media folder validation
|
||||||
|
run: |
|
||||||
|
echo "### Media Folder Validation" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=0
|
||||||
|
|
||||||
|
MANIFEST=""
|
||||||
|
for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
|
||||||
|
if grep -q "<extension" "$XML_FILE" 2>/dev/null; then
|
||||||
|
MANIFEST="$XML_FILE"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$MANIFEST" ]; then
|
||||||
|
echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
MANIFEST_DIR=$(dirname "$MANIFEST")
|
||||||
|
|
||||||
|
# Check <media> tag and its folder/filename children
|
||||||
|
MEDIA_DEST=$(grep -oP '<media[^>]*\bdestination="\K[^"]+' "$MANIFEST" 2>/dev/null | head -1)
|
||||||
|
MEDIA_FOLDER=$(grep -oP '<media[^>]*\bfolder="\K[^"]+' "$MANIFEST" 2>/dev/null | head -1)
|
||||||
|
|
||||||
|
if [ -z "$MEDIA_DEST" ] && [ -z "$MEDIA_FOLDER" ]; then
|
||||||
|
echo "No \`<media>\` tag found — skipping." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
if [ -n "$MEDIA_FOLDER" ] && [ ! -d "${MANIFEST_DIR}/${MEDIA_FOLDER}" ]; then
|
||||||
|
echo "::error file=${MANIFEST}::\`<media folder=\"${MEDIA_FOLDER}\">\` references missing directory"
|
||||||
|
echo "- **Missing** media folder \`${MEDIA_FOLDER}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "- Media folder \`${MEDIA_FOLDER:-(inline)}\`: present ✓" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# Check child references inside <media> block
|
||||||
|
if [ -n "$MEDIA_FOLDER" ]; then
|
||||||
|
MEDIA_FOLDERS=$(sed -n '/<media /,/<\/media>/p' "$MANIFEST" | grep -oP '<folder>\K[^<]+' 2>/dev/null || true)
|
||||||
|
for F in $MEDIA_FOLDERS; do
|
||||||
|
if [ ! -d "${MANIFEST_DIR}/${MEDIA_FOLDER}/${F}" ]; then
|
||||||
|
echo "- **Missing** media subfolder \`${MEDIA_FOLDER}/${F}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
MEDIA_FILES=$(sed -n '/<media /,/<\/media>/p' "$MANIFEST" | grep -oP '<filename>\K[^<]+' 2>/dev/null || true)
|
||||||
|
for F in $MEDIA_FILES; do
|
||||||
|
if [ ! -f "${MANIFEST_DIR}/${MEDIA_FOLDER}/${F}" ]; then
|
||||||
|
echo "- **Missing** media file \`${MEDIA_FOLDER}/${F}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
if [ "${ERRORS}" -gt 0 ]; then
|
||||||
|
echo "**${ERRORS} media reference issue(s).**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "**Media folder validation passed.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Target platform check
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
echo "### Target Platform Check" >> $GITHUB_STEP_SUMMARY
|
||||||
|
WARNINGS=0
|
||||||
|
|
||||||
|
MANIFEST=""
|
||||||
|
for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
|
||||||
|
if grep -q "<extension" "$XML_FILE" 2>/dev/null; then
|
||||||
|
MANIFEST="$XML_FILE"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$MANIFEST" ]; then
|
||||||
|
echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
# Check updates.xml for targetplatform if it exists
|
||||||
|
if [ -f "updates.xml" ]; then
|
||||||
|
if ! grep -q '<targetplatform' "updates.xml" 2>/dev/null; then
|
||||||
|
echo "::warning file=updates.xml::No \`<targetplatform>\` found — Joomla updater cannot filter by compatible version"
|
||||||
|
echo "- **Missing** \`<targetplatform>\` in updates.xml" >> $GITHUB_STEP_SUMMARY
|
||||||
|
WARNINGS=$((WARNINGS + 1))
|
||||||
|
else
|
||||||
|
echo "- \`<targetplatform>\` in updates.xml: present ✓" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check manifest for minimum PHP/Joomla version hints
|
||||||
|
if ! grep -qP '<php_minimum>|targetplatform|joomla.*version' "$MANIFEST" 2>/dev/null; then
|
||||||
|
echo "::warning file=${MANIFEST}::No minimum Joomla or PHP version constraint found in manifest"
|
||||||
|
echo "- **Missing** version constraints (\`<php_minimum>\` or \`<targetplatform>\`)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
WARNINGS=$((WARNINGS + 1))
|
||||||
|
else
|
||||||
|
echo "- Version constraints in manifest: present ✓" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
if [ "$WARNINGS" -gt 0 ]; then
|
||||||
|
echo "**${WARNINGS} target platform warning(s).**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "**Target platform check passed.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Changelog URL check
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
echo "### Changelog URL Check" >> $GITHUB_STEP_SUMMARY
|
||||||
|
WARNINGS=0
|
||||||
|
|
||||||
|
MANIFEST=""
|
||||||
|
for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
|
||||||
|
if grep -q "<extension" "$XML_FILE" 2>/dev/null; then
|
||||||
|
MANIFEST="$XML_FILE"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$MANIFEST" ]; then
|
||||||
|
echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
if ! grep -q '<changelogurl>' "$MANIFEST" 2>/dev/null; then
|
||||||
|
echo "::warning file=${MANIFEST}::Missing \`<changelogurl>\` — Joomla updater will not display changelogs"
|
||||||
|
echo "- **Missing** \`<changelogurl>\` — Joomla 4+ shows changelogs in the update manager when this is set" >> $GITHUB_STEP_SUMMARY
|
||||||
|
WARNINGS=$((WARNINGS + 1))
|
||||||
|
else
|
||||||
|
CHANGELOG_URL=$(grep -oP '<changelogurl>\K[^<]+' "$MANIFEST" | head -1)
|
||||||
|
echo "- \`<changelogurl>\`: \`${CHANGELOG_URL}\` ✓" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
if [ "$WARNINGS" -gt 0 ]; then
|
||||||
|
echo "**${WARNINGS} changelog URL warning(s).**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "**Changelog URL check passed.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Duplicate file references check
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
echo "### Duplicate File References" >> $GITHUB_STEP_SUMMARY
|
||||||
|
WARNINGS=0
|
||||||
|
|
||||||
|
MANIFEST=""
|
||||||
|
for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
|
||||||
|
if grep -q "<extension" "$XML_FILE" 2>/dev/null; then
|
||||||
|
MANIFEST="$XML_FILE"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$MANIFEST" ]; then
|
||||||
|
echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
# Extract all <filename> and <folder> references
|
||||||
|
ALL_REFS=$(grep -oP '<(filename|folder)[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null | sort || true)
|
||||||
|
if [ -z "$ALL_REFS" ]; then
|
||||||
|
echo "No file/folder references found — skipping." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
DUPES=$(echo "$ALL_REFS" | uniq -d)
|
||||||
|
if [ -n "$DUPES" ]; then
|
||||||
|
while IFS= read -r DUP; do
|
||||||
|
COUNT=$(echo "$ALL_REFS" | grep -cx "$DUP")
|
||||||
|
echo "::warning file=${MANIFEST}::Duplicate reference: \`${DUP}\` appears ${COUNT} times (may be valid if in different sections)"
|
||||||
|
echo "- **Duplicate:** \`${DUP}\` (${COUNT}x) — check if cross-section" >> $GITHUB_STEP_SUMMARY
|
||||||
|
WARNINGS=$((WARNINGS + 1))
|
||||||
|
done <<< "$DUPES"
|
||||||
|
else
|
||||||
|
TOTAL=$(echo "$ALL_REFS" | wc -l)
|
||||||
|
echo "All ${TOTAL} file/folder references are unique." >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
if [ "$WARNINGS" -gt 0 ]; then
|
||||||
|
echo "**${WARNINGS} duplicate reference(s) found.** Review for cross-section validity." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "**Duplicate file references check passed.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Empty language keys check
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
echo "### Empty Language Keys" >> $GITHUB_STEP_SUMMARY
|
||||||
|
WARNINGS=0
|
||||||
|
|
||||||
|
LANG_FILES=$(find . -name "*.ini" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null)
|
||||||
|
if [ -z "$LANG_FILES" ]; then
|
||||||
|
echo "No .ini language files found — skipping." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
TOTAL_FILES=0
|
||||||
|
for FILE in $LANG_FILES; do
|
||||||
|
TOTAL_FILES=$((TOTAL_FILES + 1))
|
||||||
|
# Find lines with KEY= but no value (empty or whitespace-only after =)
|
||||||
|
EMPTY_KEYS=$(grep -nP '^[A-Z_]+=\s*$' "$FILE" 2>/dev/null || true)
|
||||||
|
if [ -n "$EMPTY_KEYS" ]; then
|
||||||
|
COUNT=$(echo "$EMPTY_KEYS" | wc -l)
|
||||||
|
echo "::warning file=${FILE}::${COUNT} empty language key(s)"
|
||||||
|
echo "- \`${FILE}\`: ${COUNT} empty key(s)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
while IFS= read -r LINE; do
|
||||||
|
LINE_NUM=$(echo "$LINE" | cut -d: -f1)
|
||||||
|
KEY=$(echo "$LINE" | cut -d: -f2 | cut -d= -f1)
|
||||||
|
echo " - Line ${LINE_NUM}: \`${KEY}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
done <<< "$EMPTY_KEYS"
|
||||||
|
WARNINGS=$((WARNINGS + COUNT))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$WARNINGS" -eq 0 ]; then
|
||||||
|
echo "All ${TOTAL_FILES} language file(s) have populated keys." >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
if [ "$WARNINGS" -gt 0 ]; then
|
||||||
|
echo "**${WARNINGS} empty language key(s) across ${TOTAL_FILES} file(s).**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "**Empty language keys check passed.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
release-readiness:
|
release-readiness:
|
||||||
name: Release Readiness Check
|
name: Release Readiness Check
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.event_name == 'pull_request' && github.base_ref == 'main'
|
if: github.event_name == 'pull_request' && github.base_ref == 'main'
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
|
|||||||
@@ -1,126 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: Gitea.Workflow
|
|
||||||
# INGROUP: MokoStandards.Deploy
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
|
|
||||||
# PATH: /templates/workflows/joomla/deploy-manual.yml.template
|
|
||||||
# VERSION: 04.07.00
|
|
||||||
# BRIEF: Manual SFTP deploy to dev server for Joomla repos
|
|
||||||
|
|
||||||
name: "Universal: Deploy to Dev (Manual)"
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
clear_remote:
|
|
||||||
description: 'Delete all remote files before uploading'
|
|
||||||
required: false
|
|
||||||
default: 'false'
|
|
||||||
type: boolean
|
|
||||||
|
|
||||||
env:
|
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
name: SFTP Deploy to Dev
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
||||||
|
|
||||||
- name: Setup PHP
|
|
||||||
run: |
|
|
||||||
php -v && composer --version
|
|
||||||
|
|
||||||
- name: Setup MokoStandards tools
|
|
||||||
env:
|
|
||||||
GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
|
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
|
|
||||||
MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
|
|
||||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
|
|
||||||
run: |
|
|
||||||
git clone --depth 1 --branch main --quiet \
|
|
||||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
|
|
||||||
/tmp/mokostandards-api 2>/dev/null || true
|
|
||||||
if [ -d "/tmp/mokostandards-api" ] && [ -f "/tmp/mokostandards-api/composer.json" ]; then
|
|
||||||
cd /tmp/mokostandards-api && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Check FTP configuration
|
|
||||||
id: check
|
|
||||||
env:
|
|
||||||
HOST: ${{ vars.DEV_FTP_HOST }}
|
|
||||||
PATH_VAR: ${{ vars.DEV_FTP_PATH }}
|
|
||||||
PORT: ${{ vars.DEV_FTP_PORT }}
|
|
||||||
run: |
|
|
||||||
if [ -z "$HOST" ] || [ -z "$PATH_VAR" ]; then
|
|
||||||
echo "DEV_FTP_HOST or DEV_FTP_PATH not configured -- cannot deploy"
|
|
||||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
echo "skip=false" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "host=$HOST" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
REMOTE="${PATH_VAR%/}"
|
|
||||||
echo "remote=$REMOTE" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
[ -z "$PORT" ] && PORT="22"
|
|
||||||
echo "port=$PORT" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
- name: Deploy via SFTP
|
|
||||||
if: steps.check.outputs.skip != 'true'
|
|
||||||
env:
|
|
||||||
SFTP_KEY: ${{ secrets.DEV_FTP_KEY }}
|
|
||||||
SFTP_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
|
|
||||||
SFTP_USER: ${{ vars.DEV_FTP_USERNAME }}
|
|
||||||
run: |
|
|
||||||
SOURCE_DIR="src"
|
|
||||||
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
|
||||||
[ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ -- nothing to deploy"; exit 0; }
|
|
||||||
|
|
||||||
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
|
|
||||||
"${{ steps.check.outputs.host }}" "${{ steps.check.outputs.port }}" "$SFTP_USER" "${{ steps.check.outputs.remote }}" \
|
|
||||||
> /tmp/sftp-config.json
|
|
||||||
|
|
||||||
if [ -n "$SFTP_KEY" ]; then
|
|
||||||
echo "$SFTP_KEY" > /tmp/deploy_key
|
|
||||||
chmod 600 /tmp/deploy_key
|
|
||||||
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
|
|
||||||
else
|
|
||||||
printf ',"password":"%s"}' "$SFTP_PASS" >> /tmp/sftp-config.json
|
|
||||||
fi
|
|
||||||
|
|
||||||
DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json)
|
|
||||||
[ "${{ inputs.clear_remote }}" = "true" ] && DEPLOY_ARGS+=(--clear-remote)
|
|
||||||
|
|
||||||
PLATFORM=$(php /tmp/mokostandards-api/cli/platform_detect.php --path . 2>/dev/null || true)
|
|
||||||
if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards-api/deploy/deploy-joomla.php" ]; then
|
|
||||||
php /tmp/mokostandards-api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}"
|
|
||||||
else
|
|
||||||
php /tmp/mokostandards-api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
rm -f /tmp/deploy_key /tmp/sftp-config.json
|
|
||||||
|
|
||||||
- name: Summary
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
if [ "${{ steps.check.outputs.skip }}" = "true" ]; then
|
|
||||||
echo "### Deploy Skipped -- FTP not configured" >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "### Manual Dev Deploy Complete" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Host | \`${{ steps.check.outputs.host }}\` |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Remote | \`${{ steps.check.outputs.remote }}\` |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Clear | ${{ inputs.clear_remote }} |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
@@ -25,10 +25,6 @@
|
|||||||
name: "Universal: Secret Scanning"
|
name: "Universal: Secret Scanning"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- 'dev/**'
|
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 5 * * 1' # Weekly Monday 05:00 UTC
|
- cron: '0 5 * * 1' # Weekly Monday 05:00 UTC
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: mokoplatform.Automation
|
# INGROUP: mokocli.Automation
|
||||||
# VERSION: 01.02.00
|
# VERSION: 01.08.37
|
||||||
# BRIEF: Auto-create feature branch when an issue is opened
|
# BRIEF: Auto-create feature branch when an issue is opened
|
||||||
|
|
||||||
name: "Universal: Issue Branch"
|
name: "Universal: Issue Branch"
|
||||||
@@ -28,7 +28,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Create branch and comment
|
- name: Create branch and comment
|
||||||
run: |
|
run: |
|
||||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
TOKEN="${{ secrets.GA_TOKEN }}"
|
||||||
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||||
ISSUE_NUM="${{ github.event.issue.number }}"
|
ISSUE_NUM="${{ github.event.issue.number }}"
|
||||||
ISSUE_TITLE="${{ github.event.issue.title }}"
|
ISSUE_TITLE="${{ github.event.issue.title }}"
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: mokoplatform.CI
|
# INGROUP: moko-platform.CI
|
||||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokoplatform
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
||||||
# PATH: /templates/workflows/universal/pr-check.yml.template
|
# PATH: /templates/workflows/universal/pr-check.yml.template
|
||||||
# VERSION: 09.23.00
|
# VERSION: 09.23.00
|
||||||
# BRIEF: PR gate — branch policy + code validation before merge
|
# BRIEF: PR gate — branch policy + code validation before merge
|
||||||
@@ -96,6 +96,32 @@ jobs:
|
|||||||
echo "Branch policy: OK (${HEAD} → ${BASE})"
|
echo "Branch policy: OK (${HEAD} → ${BASE})"
|
||||||
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
|
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# ── Secret Scanning ──────────────────────────────────────────────────
|
||||||
|
gitleaks:
|
||||||
|
name: Secret Scan
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Install Gitleaks
|
||||||
|
run: |
|
||||||
|
GITLEAKS_VERSION="8.21.2"
|
||||||
|
curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \
|
||||||
|
| tar -xz -C /usr/local/bin gitleaks
|
||||||
|
|
||||||
|
- name: Scan PR commits for secrets
|
||||||
|
run: |
|
||||||
|
if gitleaks detect --source . --verbose \
|
||||||
|
--log-opts=${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }} 2>&1; then
|
||||||
|
echo "**No secrets detected.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "::error::Potential secrets detected in PR commits"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# ── Code Validation ────────────────────────────────────────────────────
|
# ── Code Validation ────────────────────────────────────────────────────
|
||||||
validate:
|
validate:
|
||||||
name: Validate PR
|
name: Validate PR
|
||||||
@@ -159,11 +185,11 @@ jobs:
|
|||||||
echo "::error file=${file}::Missing JEXEC guard: ${file}"
|
echo "::error file=${file}::Missing JEXEC guard: ${file}"
|
||||||
ERRORS=$((ERRORS + 1))
|
ERRORS=$((ERRORS + 1))
|
||||||
fi
|
fi
|
||||||
done < <(find . -name "*.php" \( -path "*/source/*" -o -path "*/src/*" \) -not -path "./.git/*" -not -path "./vendor/*" -print0)
|
done < <(find . -name "*.php" -path "*/src/*" -not -path "./.git/*" -not -path "./vendor/*" -print0)
|
||||||
if [ "$ERRORS" -gt 0 ]; then
|
if [ "$ERRORS" -gt 0 ]; then
|
||||||
echo "::error::${ERRORS} PHP file(s) missing defined('_JEXEC') or die guard"
|
echo "::error::${ERRORS} PHP file(s) missing defined('_JEXEC') or die guard"
|
||||||
echo "## JEXEC Guard Check: Failed" >> $GITHUB_STEP_SUMMARY
|
echo "## JEXEC Guard Check: Failed" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "${ERRORS} file(s) are missing the Joomla execution guard." >> $GITHUB_STEP_SUMMARY
|
echo "${ERRORS} file(s) in src/ are missing the Joomla execution guard." >> $GITHUB_STEP_SUMMARY
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "JEXEC guard: OK"
|
echo "JEXEC guard: OK"
|
||||||
@@ -172,8 +198,7 @@ jobs:
|
|||||||
if: steps.platform.outputs.platform == 'joomla'
|
if: steps.platform.outputs.platform == 'joomla'
|
||||||
run: |
|
run: |
|
||||||
MISSING=0
|
MISSING=0
|
||||||
SOURCE_DIR="source"
|
SOURCE_DIR="src"
|
||||||
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="src"
|
|
||||||
[ ! -d "$SOURCE_DIR" ] && exit 0
|
[ ! -d "$SOURCE_DIR" ] && exit 0
|
||||||
while IFS= read -r dir; do
|
while IFS= read -r dir; do
|
||||||
if [ ! -f "${dir}/index.html" ]; then
|
if [ ! -f "${dir}/index.html" ]; then
|
||||||
@@ -221,7 +246,7 @@ jobs:
|
|||||||
echo "joomla.asset.json: valid"
|
echo "joomla.asset.json: valid"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Validate all XML files in source/src are well-formed
|
# Validate all XML files in src/ are well-formed
|
||||||
XML_ERRORS=0
|
XML_ERRORS=0
|
||||||
if command -v php &> /dev/null; then
|
if command -v php &> /dev/null; then
|
||||||
while IFS= read -r -d '' xmlfile; do
|
while IFS= read -r -d '' xmlfile; do
|
||||||
@@ -450,38 +475,12 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]"
|
echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]"
|
||||||
|
|
||||||
- name: Require README and CHANGELOG in PR diff
|
|
||||||
run: |
|
|
||||||
BASE="${{ github.base_ref }}"
|
|
||||||
HEAD="${{ github.head_ref }}"
|
|
||||||
CHANGED=$(git diff --name-only "origin/${BASE}...HEAD" 2>/dev/null || git diff --name-only HEAD~1 2>/dev/null || echo "")
|
|
||||||
SOURCE_CHANGED=$(echo "$CHANGED" | grep -E '^source/|^src/' || true)
|
|
||||||
if [ -z "$SOURCE_CHANGED" ]; then
|
|
||||||
echo "No source changes — skipping README/CHANGELOG diff check"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
ERRORS=0
|
|
||||||
if ! echo "$CHANGED" | grep -q '^CHANGELOG.md$'; then
|
|
||||||
echo "::error::Source code was modified but CHANGELOG.md was not updated."
|
|
||||||
ERRORS=$((ERRORS + 1))
|
|
||||||
fi
|
|
||||||
if ! echo "$CHANGED" | grep -q '^README.md$'; then
|
|
||||||
echo "::warning::Source code was modified but README.md was not updated."
|
|
||||||
fi
|
|
||||||
if [ "$ERRORS" -gt 0 ]; then
|
|
||||||
echo "## Documentation Check: Failed" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "Source code was modified but CHANGELOG.md was not updated." >> $GITHUB_STEP_SUMMARY
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "Documentation diff check: OK"
|
|
||||||
|
|
||||||
- name: Verify package source
|
- name: Verify package source
|
||||||
run: |
|
run: |
|
||||||
SOURCE_DIR="source"
|
SOURCE_DIR="src"
|
||||||
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="src"
|
|
||||||
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
||||||
if [ ! -d "$SOURCE_DIR" ]; then
|
if [ ! -d "$SOURCE_DIR" ]; then
|
||||||
echo "::warning::No source/, src/, or htdocs/ directory"
|
echo "::warning::No src/ or htdocs/ directory"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
|
FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: mokocli.Validation
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
|
# PATH: /templates/workflows/joomla/pr-metadata-check.yml.template
|
||||||
|
# VERSION: 01.00.00
|
||||||
|
# BRIEF: Validate MokoGitea metadata matches Joomla extension manifest on PRs
|
||||||
|
|
||||||
|
name: "Joomla: Metadata Validation"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize, reopened, converted_to_draft, ready_for_review]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
env:
|
||||||
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||||||
|
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
validate-metadata:
|
||||||
|
name: "Validate Joomla Metadata"
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup mokocli tools
|
||||||
|
env:
|
||||||
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
|
run: |
|
||||||
|
if [ -f /opt/mokocli/cli/joomla_metadata_validate.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then
|
||||||
|
echo Using pre-installed /opt/mokocli
|
||||||
|
echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo Falling back to fresh clone
|
||||||
|
if ! command -v composer > /dev/null 2>&1; then
|
||||||
|
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
|
||||||
|
fi
|
||||||
|
rm -rf /tmp/mokocli
|
||||||
|
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git
|
||||||
|
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
|
||||||
|
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
|
||||||
|
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Validate metadata against Joomla manifest
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
run: |
|
||||||
|
php ${MOKO_CLI}/joomla_metadata_validate.php \
|
||||||
|
--path . \
|
||||||
|
--token "${GITEA_TOKEN}" \
|
||||||
|
--org "${GITEA_ORG}" \
|
||||||
|
--repo "${GITEA_REPO}" \
|
||||||
|
--api-base "${GITEA_URL}/api/v1" \
|
||||||
|
--ci
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "::error::Joomla metadata mismatch — update delivery will fail. Run 'php cli/joomla_metadata_validate.php' locally to see details."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -4,23 +4,26 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: mokoplatform.Release
|
# INGROUP: mokocli.Release
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
# PATH: /templates/workflows/universal/pre-release.yml.template
|
# PATH: /templates/workflows/universal/pre-release.yml.template
|
||||||
# VERSION: 05.01.00
|
# VERSION: 05.01.00
|
||||||
# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch
|
# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches
|
||||||
|
|
||||||
name: "Universal: Pre-Release"
|
name: "Universal: Pre-Release"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
push:
|
||||||
types: [closed]
|
|
||||||
branches:
|
branches:
|
||||||
- dev
|
- dev
|
||||||
pull_request_target:
|
- 'fix/**'
|
||||||
types: [synchronize, opened, reopened]
|
- 'patch/**'
|
||||||
branches:
|
- 'hotfix/**'
|
||||||
- main
|
- 'bugfix/**'
|
||||||
|
- 'chore/**'
|
||||||
|
- alpha
|
||||||
|
- beta
|
||||||
|
- rc
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
stability:
|
stability:
|
||||||
@@ -43,12 +46,11 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: "Build Pre-Release (${{ inputs.stability || 'development' }})"
|
name: "Build Pre-Release (${{ inputs.stability || github.ref_name }})"
|
||||||
runs-on: release
|
runs-on: release
|
||||||
if: >-
|
if: >-
|
||||||
github.event_name == 'workflow_dispatch' ||
|
github.event_name == 'workflow_dispatch' ||
|
||||||
(github.event_name == 'pull_request' && github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev') ||
|
github.event_name == 'push'
|
||||||
(github.event_name == 'pull_request_target' && github.event.pull_request.base.ref == 'main')
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -56,40 +58,59 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || '' }}
|
ref: ${{ github.ref_name }}
|
||||||
|
|
||||||
- name: Setup mokoplatform tools
|
- name: Setup mokocli tools
|
||||||
env:
|
env:
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
run: |
|
run: |
|
||||||
# Use pre-installed /opt/mokoplatform if available (updated by cron every 6h)
|
# Use pre-installed /opt/mokocli if available (updated by cron every 6h)
|
||||||
if [ -f /opt/mokoplatform/cli/version_bump.php ] && [ -f /opt/mokoplatform/cli/manifest_element.php ] && [ -f /opt/mokoplatform/vendor/autoload.php ]; then
|
if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/cli/manifest_element.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then
|
||||||
echo Using pre-installed /opt/mokoplatform
|
echo Using pre-installed /opt/mokocli
|
||||||
echo MOKO_CLI=/opt/mokoplatform/cli >> $GITHUB_ENV
|
echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV
|
||||||
else
|
else
|
||||||
echo Falling back to fresh clone
|
echo Falling back to fresh clone
|
||||||
if ! command -v composer > /dev/null 2>&1; then
|
if ! command -v composer > /dev/null 2>&1; then
|
||||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
|
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
|
||||||
fi
|
fi
|
||||||
rm -rf /tmp/mokoplatform-api
|
rm -rf /tmp/mokocli
|
||||||
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokoplatform.git
|
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git
|
||||||
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokoplatform-api
|
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
|
||||||
cd /tmp/mokoplatform-api && composer install --no-dev --no-interaction --quiet
|
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
|
||||||
echo MOKO_CLI=/tmp/mokoplatform-api/cli >> $GITHUB_ENV
|
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Detect platform
|
- name: Detect platform
|
||||||
id: platform
|
id: platform
|
||||||
run: |
|
run: |
|
||||||
|
# Auto-detect and update platform if not set in manifest
|
||||||
|
php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true
|
||||||
php ${MOKO_CLI}/manifest_read.php --path . --github-output
|
php ${MOKO_CLI}/manifest_read.php --path . --github-output
|
||||||
|
|
||||||
|
- name: Check platform eligibility (Joomla only)
|
||||||
|
id: eligibility
|
||||||
|
run: |
|
||||||
|
PLATFORM="${{ steps.platform.outputs.platform }}"
|
||||||
|
if [[ "$PLATFORM" == joomla* ]] || [[ "$PLATFORM" == "joomla" ]]; then
|
||||||
|
echo "proceed=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "proceed=false" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "::notice::Platform '$PLATFORM' — non-Joomla, skipping pre-release auto-bump"
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Resolve metadata and bump version
|
- name: Resolve metadata and bump version
|
||||||
id: meta
|
id: meta
|
||||||
|
if: steps.eligibility.outputs.proceed == 'true'
|
||||||
run: |
|
run: |
|
||||||
# Auto-detect stability: RC for PRs targeting main, else use input or default to development
|
# Auto-detect stability from branch name on push, or use input on dispatch
|
||||||
if [ "${{ github.event_name }}" = "pull_request_target" ] && [ "${{ github.event.pull_request.base.ref }}" = "main" ]; then
|
if [ "${{ github.event_name }}" = "push" ]; then
|
||||||
STABILITY="release-candidate"
|
case "${{ github.ref_name }}" in
|
||||||
|
rc) STABILITY="release-candidate" ;;
|
||||||
|
alpha) STABILITY="alpha" ;;
|
||||||
|
beta) STABILITY="beta" ;;
|
||||||
|
*) STABILITY="development" ;;
|
||||||
|
esac
|
||||||
else
|
else
|
||||||
STABILITY="${{ inputs.stability || 'development' }}"
|
STABILITY="${{ inputs.stability || 'development' }}"
|
||||||
fi
|
fi
|
||||||
@@ -157,6 +178,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Create release
|
- name: Create release
|
||||||
id: release
|
id: release
|
||||||
|
if: steps.eligibility.outputs.proceed == 'true'
|
||||||
run: |
|
run: |
|
||||||
TAG="${{ steps.meta.outputs.tag }}"
|
TAG="${{ steps.meta.outputs.tag }}"
|
||||||
VERSION="${{ steps.meta.outputs.version }}"
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
@@ -164,9 +186,10 @@ jobs:
|
|||||||
php ${MOKO_CLI}/release_create.php \
|
php ${MOKO_CLI}/release_create.php \
|
||||||
--path . --version "$VERSION" --tag "$TAG" \
|
--path . --version "$VERSION" --tag "$TAG" \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
--repo "${GITEA_REPO}" --branch dev --prerelease
|
--repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease
|
||||||
|
|
||||||
- name: Update release notes from CHANGELOG.md
|
- name: Update release notes from CHANGELOG.md
|
||||||
|
if: steps.eligibility.outputs.proceed == 'true'
|
||||||
run: |
|
run: |
|
||||||
TAG="${{ steps.meta.outputs.tag }}"
|
TAG="${{ steps.meta.outputs.tag }}"
|
||||||
VERSION="${{ steps.meta.outputs.version }}"
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
@@ -203,6 +226,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build package and upload
|
- name: Build package and upload
|
||||||
id: package
|
id: package
|
||||||
|
if: steps.eligibility.outputs.proceed == 'true'
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ steps.meta.outputs.version }}"
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
TAG="${{ steps.meta.outputs.tag }}"
|
TAG="${{ steps.meta.outputs.tag }}"
|
||||||
@@ -216,6 +240,7 @@ jobs:
|
|||||||
# No need to build, commit, or sync updates.xml from workflows
|
# No need to build, commit, or sync updates.xml from workflows
|
||||||
|
|
||||||
- name: "Delete lesser pre-release channels (cascade)"
|
- name: "Delete lesser pre-release channels (cascade)"
|
||||||
|
if: steps.eligibility.outputs.proceed == 'true'
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: mokocli.Universal
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
|
# PATH: /.mokogitea/workflows/rc-revert.yml
|
||||||
|
# VERSION: 09.23.00
|
||||||
|
# BRIEF: Rename rc/ branch back to dev/ when PR is closed without merge
|
||||||
|
|
||||||
|
name: "RC Revert"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [closed]
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
revert:
|
||||||
|
name: Rename rc/ back to dev/
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: >-
|
||||||
|
github.event.pull_request.merged == false &&
|
||||||
|
startsWith(github.event.pull_request.head.ref, 'rc/')
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Rename branch
|
||||||
|
run: |
|
||||||
|
BRANCH="${{ github.event.pull_request.head.ref }}"
|
||||||
|
SUFFIX="${BRANCH#rc/}"
|
||||||
|
DEV_BRANCH="dev/${SUFFIX}"
|
||||||
|
API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches"
|
||||||
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
|
# Create dev/ branch from rc/ branch
|
||||||
|
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X POST \
|
||||||
|
-H "Authorization: token ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"new_branch_name\": \"${DEV_BRANCH}\", \"old_branch_name\": \"${BRANCH}\"}" \
|
||||||
|
"${API}" 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [ "$STATUS" = "201" ]; then
|
||||||
|
echo "Created branch: ${DEV_BRANCH}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "::error::Failed to create ${DEV_BRANCH} from ${BRANCH} (HTTP ${STATUS})"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Delete rc/ branch
|
||||||
|
ENCODED=$(php -r "echo rawurlencode('${BRANCH}');")
|
||||||
|
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \
|
||||||
|
-H "Authorization: token ${TOKEN}" \
|
||||||
|
"${API}/${ENCODED}" 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [ "$STATUS" = "204" ]; then
|
||||||
|
echo "Deleted branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "::warning::Failed to delete ${BRANCH} (HTTP ${STATUS})"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "### RC Reverted" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "${BRANCH} → ${DEV_BRANCH}" >> $GITHUB_STEP_SUMMARY
|
||||||
@@ -7,8 +7,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: mokoplatform.Validation
|
# INGROUP: mokocli.Validation
|
||||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokoplatform
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli
|
||||||
# PATH: /templates/workflows/joomla/repo_health.yml.template
|
# PATH: /templates/workflows/joomla/repo_health.yml.template
|
||||||
# VERSION: 09.23.00
|
# VERSION: 09.23.00
|
||||||
# BRIEF: Enforces repository guardrails by validating scripts governance, tooling availability, and core repository health artifacts.
|
# BRIEF: Enforces repository guardrails by validating scripts governance, tooling availability, and core repository health artifacts.
|
||||||
@@ -33,7 +33,8 @@ on:
|
|||||||
- scripts
|
- scripts
|
||||||
- repo
|
- repo
|
||||||
pull_request:
|
pull_request:
|
||||||
push:
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -296,19 +297,17 @@ jobs:
|
|||||||
missing_required=()
|
missing_required=()
|
||||||
missing_optional=()
|
missing_optional=()
|
||||||
|
|
||||||
# Source directory: source/, src/, or htdocs/ (any is valid for extension repos)
|
# Source directory: src/ or htdocs/ (either is valid for extension repos)
|
||||||
SOURCE_DIR=""
|
SOURCE_DIR=""
|
||||||
if [ -d "source" ]; then
|
if [ -d "src" ]; then
|
||||||
SOURCE_DIR="source"
|
|
||||||
elif [ -d "src" ]; then
|
|
||||||
SOURCE_DIR="src"
|
SOURCE_DIR="src"
|
||||||
elif [ -d "htdocs" ]; then
|
elif [ -d "htdocs" ]; then
|
||||||
SOURCE_DIR="htdocs"
|
SOURCE_DIR="htdocs"
|
||||||
elif [ -d "deploy" ] || [ -d "cli" ] || [ -d "monitoring" ]; then
|
elif [ -d "deploy" ] || [ -d "cli" ] || [ -d "monitoring" ]; then
|
||||||
# Platform/tooling repos don't need source/
|
# Platform/tooling repos don't need src/
|
||||||
SOURCE_DIR=""
|
SOURCE_DIR=""
|
||||||
else
|
else
|
||||||
missing_required+=("source/ or src/ or htdocs/ (source directory required)")
|
missing_required+=("src/ or htdocs/ (source directory required)")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
for item in "${required_artifacts[@]}"; do
|
for item in "${required_artifacts[@]}"; do
|
||||||
|
|||||||
@@ -1,82 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: Gitea.Workflow
|
|
||||||
# INGROUP: MokoStandards.Security
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
|
||||||
# PATH: /.gitea/workflows/security-audit.yml
|
|
||||||
# VERSION: 01.00.00
|
|
||||||
# BRIEF: Dependency vulnerability scanning for composer and npm packages
|
|
||||||
|
|
||||||
name: "Universal: Security Audit"
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: '0 6 * * 1' # Weekly on Monday at 06:00 UTC
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- 'composer.json'
|
|
||||||
- 'composer.lock'
|
|
||||||
- 'package.json'
|
|
||||||
- 'package-lock.json'
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
env:
|
|
||||||
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
|
|
||||||
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
audit:
|
|
||||||
name: Dependency Audit
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Composer audit
|
|
||||||
if: hashFiles('composer.lock') != ''
|
|
||||||
run: |
|
|
||||||
echo "=== Composer Security Audit ==="
|
|
||||||
if ! command -v composer &> /dev/null; then
|
|
||||||
sudo apt-get update -qq
|
|
||||||
sudo apt-get install -y -qq php-cli composer >/dev/null 2>&1
|
|
||||||
fi
|
|
||||||
composer audit --format=plain 2>&1 | tee /tmp/composer-audit.txt
|
|
||||||
RESULT=$?
|
|
||||||
if [ $RESULT -ne 0 ]; then
|
|
||||||
echo "::warning::Composer vulnerabilities found"
|
|
||||||
echo "composer_vulnerable=true" >> "$GITHUB_ENV"
|
|
||||||
else
|
|
||||||
echo "No known vulnerabilities in composer dependencies"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: NPM audit
|
|
||||||
if: hashFiles('package-lock.json') != ''
|
|
||||||
run: |
|
|
||||||
echo "=== NPM Security Audit ==="
|
|
||||||
npm audit --production 2>&1 | tee /tmp/npm-audit.txt || true
|
|
||||||
if npm audit --production 2>&1 | grep -q "found 0 vulnerabilities"; then
|
|
||||||
echo "No known vulnerabilities in npm dependencies"
|
|
||||||
else
|
|
||||||
echo "::warning::NPM vulnerabilities found"
|
|
||||||
echo "npm_vulnerable=true" >> "$GITHUB_ENV"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Notify on vulnerabilities
|
|
||||||
if: env.composer_vulnerable == 'true' || env.npm_vulnerable == 'true'
|
|
||||||
run: |
|
|
||||||
REPO="${{ github.event.repository.name }}"
|
|
||||||
curl -sS \
|
|
||||||
-H "Title: ${REPO} has vulnerable dependencies" \
|
|
||||||
-H "Tags: lock,warning" \
|
|
||||||
-H "Priority: high" \
|
|
||||||
-d "Security audit found vulnerabilities. Review dependency updates." \
|
|
||||||
"${NTFY_URL}/${NTFY_TOPIC}" || true
|
|
||||||
@@ -1,312 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: Gitea.Workflow
|
|
||||||
# INGROUP: moko-platform.Universal
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
|
||||||
# PATH: /templates/workflows/update-server.yml
|
|
||||||
# VERSION: 05.00.00
|
|
||||||
# BRIEF: Pre-release build + update server XML for dev/alpha/beta/rc branches
|
|
||||||
#
|
|
||||||
# Thin wrapper around moko-platform CLI tools.
|
|
||||||
# Builds packages, updates updates.xml, and optionally deploys via SFTP.
|
|
||||||
#
|
|
||||||
# Joomla filters update entries by the user's "Minimum Stability" setting.
|
|
||||||
|
|
||||||
name: "Update Server"
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- 'dev'
|
|
||||||
- 'dev/**'
|
|
||||||
- 'alpha/**'
|
|
||||||
- 'beta/**'
|
|
||||||
- 'rc/**'
|
|
||||||
paths:
|
|
||||||
- 'src/**'
|
|
||||||
- 'htdocs/**'
|
|
||||||
pull_request:
|
|
||||||
types: [closed]
|
|
||||||
branches:
|
|
||||||
- 'dev'
|
|
||||||
- 'dev/**'
|
|
||||||
- 'alpha/**'
|
|
||||||
- 'beta/**'
|
|
||||||
- 'rc/**'
|
|
||||||
paths:
|
|
||||||
- 'src/**'
|
|
||||||
- 'htdocs/**'
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
stability:
|
|
||||||
description: 'Stability tag'
|
|
||||||
required: true
|
|
||||||
default: 'development'
|
|
||||||
type: choice
|
|
||||||
options:
|
|
||||||
- development
|
|
||||||
- alpha
|
|
||||||
- beta
|
|
||||||
- rc
|
|
||||||
- stable
|
|
||||||
|
|
||||||
env:
|
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
||||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
|
||||||
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
|
||||||
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
update-xml:
|
|
||||||
name: Update Server
|
|
||||||
runs-on: release
|
|
||||||
if: >-
|
|
||||||
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || github.event_name == 'push'
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Setup moko-platform tools
|
|
||||||
env:
|
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
|
||||||
COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.MOKOGITEA_TOKEN }}"}}}'
|
|
||||||
run: |
|
|
||||||
if ! command -v composer &> /dev/null; then
|
|
||||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
|
||||||
fi
|
|
||||||
# Always fetch latest CLI tools — never use stale cache from previous runs
|
|
||||||
rm -rf /tmp/moko-platform
|
|
||||||
git clone --depth 1 --branch main --quiet \
|
|
||||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
|
||||||
/tmp/moko-platform 2>/dev/null || true
|
|
||||||
if [ -d "/tmp/moko-platform" ] && [ -f "/tmp/moko-platform/composer.json" ]; then
|
|
||||||
cd /tmp/moko-platform && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
echo "MOKO_CLI=/tmp/moko-platform/cli" >> "$GITHUB_ENV"
|
|
||||||
|
|
||||||
- name: Detect platform
|
|
||||||
id: platform
|
|
||||||
run: php ${MOKO_CLI}/manifest_read.php --path . --github-output
|
|
||||||
|
|
||||||
- name: Resolve stability and bump version
|
|
||||||
id: meta
|
|
||||||
run: |
|
|
||||||
BRANCH="${{ github.ref_name }}"
|
|
||||||
|
|
||||||
# Configure git for bot pushes
|
|
||||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
|
||||||
git config --local user.name "gitea-actions[bot]"
|
|
||||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
|
||||||
|
|
||||||
# Auto-bump patch version
|
|
||||||
php ${MOKO_CLI}/version_bump.php --path . 2>/dev/null || true
|
|
||||||
|
|
||||||
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "0.0.0")
|
|
||||||
|
|
||||||
# Strip any existing suffix before applying stability
|
|
||||||
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
|
||||||
|
|
||||||
# Determine stability from branch or manual input
|
|
||||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
|
||||||
STABILITY="${{ inputs.stability }}"
|
|
||||||
elif [[ "$BRANCH" == rc/* ]]; then
|
|
||||||
STABILITY="rc"
|
|
||||||
elif [[ "$BRANCH" == beta/* ]]; then
|
|
||||||
STABILITY="beta"
|
|
||||||
elif [[ "$BRANCH" == alpha/* ]]; then
|
|
||||||
STABILITY="alpha"
|
|
||||||
else
|
|
||||||
STABILITY="development"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Version suffix per stability stream
|
|
||||||
case "$STABILITY" in
|
|
||||||
development) SUFFIX="-dev"; TAG="development" ;;
|
|
||||||
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
|
|
||||||
beta) SUFFIX="-beta"; TAG="beta" ;;
|
|
||||||
rc) SUFFIX="-rc"; TAG="release-candidate" ;;
|
|
||||||
*) SUFFIX=""; TAG="stable" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# Propagate version with stability suffix to all manifest files
|
|
||||||
php ${MOKO_CLI}/version_set_platform.php \
|
|
||||||
--path . --version "$VERSION" --branch "$BRANCH" --stability "$STABILITY" 2>/dev/null || true
|
|
||||||
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
|
|
||||||
|
|
||||||
# Re-read version (now includes suffix from version_set_platform)
|
|
||||||
if [ -n "$SUFFIX" ]; then
|
|
||||||
VERSION="${VERSION}${SUFFIX}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "display_version=${VERSION}" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
# Commit version bump if changed
|
|
||||||
git add -A
|
|
||||||
git diff --cached --quiet || {
|
|
||||||
git commit -m "chore(version): auto-bump ${VERSION} [skip ci]" \
|
|
||||||
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
|
|
||||||
git push
|
|
||||||
}
|
|
||||||
|
|
||||||
- name: Create release and upload package
|
|
||||||
id: package
|
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.meta.outputs.version }}"
|
|
||||||
TAG="${{ steps.meta.outputs.tag }}"
|
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
|
||||||
|
|
||||||
# Create or update Gitea release
|
|
||||||
php ${MOKO_CLI}/release_create.php \
|
|
||||||
--path . --version "$VERSION" --tag "$TAG" \
|
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
|
||||||
--repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease
|
|
||||||
|
|
||||||
# Build package and upload
|
|
||||||
php ${MOKO_CLI}/release_package.php \
|
|
||||||
--path . --version "$VERSION" --tag "$TAG" \
|
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
|
||||||
--repo "${GITEA_REPO}" --output /tmp || true
|
|
||||||
|
|
||||||
- name: Update updates.xml
|
|
||||||
if: steps.platform.outputs.platform == 'joomla'
|
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.meta.outputs.version }}"
|
|
||||||
STABILITY="${{ steps.meta.outputs.stability }}"
|
|
||||||
SHA256="${{ steps.package.outputs.sha256_zip }}"
|
|
||||||
|
|
||||||
if [ ! -f "updates.xml" ]; then
|
|
||||||
echo "No updates.xml — skipping"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
SHA_FLAG=""
|
|
||||||
[ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
|
|
||||||
|
|
||||||
php ${MOKO_CLI}/updates_xml_build.php \
|
|
||||||
--path . --version "${VERSION}" --stability "${STABILITY}" \
|
|
||||||
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
|
|
||||||
${SHA_FLAG}
|
|
||||||
|
|
||||||
# Commit and push updates.xml
|
|
||||||
git add updates.xml
|
|
||||||
git diff --cached --quiet || {
|
|
||||||
git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
|
|
||||||
git push
|
|
||||||
}
|
|
||||||
|
|
||||||
- name: Sync updates.xml to main
|
|
||||||
if: github.ref_name != 'main' && steps.platform.outputs.platform == 'joomla'
|
|
||||||
run: |
|
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
|
||||||
GITEA_TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
|
||||||
|
|
||||||
FILE_SHA=$(curl -sf -H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
"${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
|
|
||||||
|
|
||||||
if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then
|
|
||||||
python3 -c "
|
|
||||||
import base64, json, urllib.request, sys
|
|
||||||
with open('updates.xml', 'rb') as f:
|
|
||||||
content = base64.b64encode(f.read()).decode()
|
|
||||||
payload = json.dumps({
|
|
||||||
'content': content,
|
|
||||||
'sha': '${FILE_SHA}',
|
|
||||||
'message': 'chore: sync updates.xml from ${{ steps.meta.outputs.stability }} [skip ci]',
|
|
||||||
'branch': 'main'
|
|
||||||
}).encode()
|
|
||||||
req = urllib.request.Request(
|
|
||||||
'${API_BASE}/contents/updates.xml',
|
|
||||||
data=payload, method='PUT',
|
|
||||||
headers={
|
|
||||||
'Authorization': 'token ${GITEA_TOKEN}',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
})
|
|
||||||
try:
|
|
||||||
urllib.request.urlopen(req)
|
|
||||||
print('updates.xml synced to main')
|
|
||||||
except Exception as e:
|
|
||||||
print(f'WARNING: sync to main failed: {e}', file=sys.stderr)
|
|
||||||
"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: SFTP deploy to dev server
|
|
||||||
if: contains(github.ref, 'dev/') || github.ref == 'refs/heads/dev'
|
|
||||||
env:
|
|
||||||
DEV_HOST: ${{ vars.DEV_FTP_HOST }}
|
|
||||||
DEV_PATH: ${{ vars.DEV_FTP_PATH }}
|
|
||||||
DEV_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
|
|
||||||
DEV_USER: ${{ vars.DEV_FTP_USERNAME }}
|
|
||||||
DEV_PORT: ${{ vars.DEV_FTP_PORT }}
|
|
||||||
DEV_KEY: ${{ secrets.DEV_FTP_KEY }}
|
|
||||||
DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
|
|
||||||
run: |
|
|
||||||
# Permission check: admin or maintain role required
|
|
||||||
ACTOR="${{ github.actor }}"
|
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
|
||||||
|
|
||||||
PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
|
||||||
"${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \
|
|
||||||
python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read")
|
|
||||||
case "$PERMISSION" in
|
|
||||||
admin|maintain|write) ;;
|
|
||||||
*)
|
|
||||||
echo "Deploy denied: ${ACTOR} has '${PERMISSION}' — requires admin, maintain, or write"
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
[ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured — skipping SFTP"; exit 0; }
|
|
||||||
|
|
||||||
SOURCE_DIR="src"
|
|
||||||
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
|
||||||
[ ! -d "$SOURCE_DIR" ] && exit 0
|
|
||||||
|
|
||||||
PORT="${DEV_PORT:-22}"
|
|
||||||
REMOTE="${DEV_PATH%/}"
|
|
||||||
[ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}"
|
|
||||||
|
|
||||||
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
|
|
||||||
"$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json
|
|
||||||
if [ -n "$DEV_KEY" ]; then
|
|
||||||
echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key
|
|
||||||
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
|
|
||||||
else
|
|
||||||
printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json
|
|
||||||
fi
|
|
||||||
|
|
||||||
PLATFORM=$(php ${MOKO_CLI}/platform_detect.php --path . 2>/dev/null || true)
|
|
||||||
if [ "$PLATFORM" = "waas-component" ] && [ -f "${MOKO_CLI}/../deploy/deploy-joomla.php" ]; then
|
|
||||||
php ${MOKO_CLI}/../deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
|
|
||||||
elif [ -f "${MOKO_CLI}/../deploy/deploy-sftp.php" ]; then
|
|
||||||
php ${MOKO_CLI}/../deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
|
|
||||||
fi
|
|
||||||
rm -f /tmp/deploy_key /tmp/sftp-config.json
|
|
||||||
echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
- name: Summary
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.meta.outputs.version }}"
|
|
||||||
STABILITY="${{ steps.meta.outputs.stability }}"
|
|
||||||
DISPLAY="${{ steps.meta.outputs.display_version }}"
|
|
||||||
echo "## Update Server" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Version | \`${DISPLAY}\` |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow.Template
|
||||||
|
# INGROUP: MokoStandards.CI
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla
|
||||||
|
# PATH: /.mokogitea/workflows/version-set.yml
|
||||||
|
# VERSION: 01.00.00
|
||||||
|
# BRIEF: Set or reset the extension version across all version-bearing files
|
||||||
|
|
||||||
|
name: "Joomla: Set Version"
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: "Version number (e.g. 01.00.00)"
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
branch:
|
||||||
|
description: "Branch to update (default: current)"
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
set-version:
|
||||||
|
name: Set Version to ${{ inputs.version }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Validate version format
|
||||||
|
run: |
|
||||||
|
VERSION="${{ inputs.version }}"
|
||||||
|
if ! echo "$VERSION" | grep -qP '^\d{2}\.\d{2}\.\d{2}$'; then
|
||||||
|
echo "::error::Invalid version format '${VERSION}' — expected XX.YY.ZZ (e.g. 01.00.00)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "VERSION=${VERSION}" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.MOKOGITEA_TOKEN || secrets.GA_TOKEN || github.token }}
|
||||||
|
ref: ${{ inputs.branch || github.ref }}
|
||||||
|
fetch-depth: 1
|
||||||
|
|
||||||
|
- name: Update manifest version
|
||||||
|
run: |
|
||||||
|
MANIFEST=""
|
||||||
|
for XML_FILE in $(find . -maxdepth 3 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
|
||||||
|
if grep -q "<extension" "$XML_FILE" 2>/dev/null; then
|
||||||
|
MANIFEST="$XML_FILE"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$MANIFEST" ]; then
|
||||||
|
echo "::warning::No Joomla extension manifest found — skipping manifest update"
|
||||||
|
else
|
||||||
|
OLD_VER=$(grep -oP '<version>\K[^<]+' "$MANIFEST" | head -1)
|
||||||
|
sed -i "s|<version>${OLD_VER}</version>|<version>${VERSION}</version>|" "$MANIFEST"
|
||||||
|
echo "Manifest: ${OLD_VER} → ${VERSION} (${MANIFEST})"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Update README.md version
|
||||||
|
run: |
|
||||||
|
if [ -f "README.md" ]; then
|
||||||
|
if grep -qP '^\s*VERSION:\s*\d' README.md; then
|
||||||
|
sed -i -E "s/(VERSION:\s*)[0-9]{2}\.[0-9]{2}\.[0-9]{2}/\1${VERSION}/" README.md
|
||||||
|
echo "README.md version updated to ${VERSION}"
|
||||||
|
else
|
||||||
|
echo "::warning::No VERSION line found in README.md — skipping"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Update CHANGELOG.md
|
||||||
|
run: |
|
||||||
|
if [ -f "CHANGELOG.md" ]; then
|
||||||
|
DATE=$(date +%Y-%m-%d)
|
||||||
|
# Check if this version already has an entry
|
||||||
|
if grep -q "^\#\# \[${VERSION}\]" CHANGELOG.md; then
|
||||||
|
echo "CHANGELOG.md already has entry for ${VERSION} — skipping"
|
||||||
|
else
|
||||||
|
# Insert new version entry after [Unreleased] or at the top after header
|
||||||
|
if grep -q '^\#\# \[Unreleased\]' CHANGELOG.md; then
|
||||||
|
sed -i "/^\#\# \[Unreleased\]/a\\\\n## [${VERSION}] --- ${DATE}" CHANGELOG.md
|
||||||
|
else
|
||||||
|
sed -i "/^\# Changelog/a\\\\n## [Unreleased]\n\n## [${VERSION}] --- ${DATE}" CHANGELOG.md
|
||||||
|
fi
|
||||||
|
echo "CHANGELOG.md: added entry for ${VERSION}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "::warning::No CHANGELOG.md found — skipping"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Update FILE INFORMATION blocks
|
||||||
|
run: |
|
||||||
|
# Update VERSION in file header blocks (# VERSION: XX.YY.ZZ)
|
||||||
|
find . -maxdepth 1 -type f \( -name "*.yml" -o -name "*.yaml" -o -name "*.php" -o -name "*.md" \) \
|
||||||
|
-not -path "./.git/*" -not -path "./vendor/*" -print0 2>/dev/null | \
|
||||||
|
while IFS= read -r -d '' FILE; do
|
||||||
|
if head -20 "$FILE" | grep -qP '^\s*#?\s*VERSION:\s*\d{2}\.\d{2}\.\d{2}'; then
|
||||||
|
sed -i -E "s/(#?\s*VERSION:\s*)[0-9]{2}\.[0-9]{2}\.[0-9]{2}/\1${VERSION}/" "$FILE"
|
||||||
|
echo "Updated FILE INFORMATION VERSION in ${FILE}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Commit and push
|
||||||
|
run: |
|
||||||
|
git config user.name "Moko Consulting [bot]"
|
||||||
|
git config user.email "hello@mokoconsulting.tech"
|
||||||
|
git add -A
|
||||||
|
if git diff --cached --quiet; then
|
||||||
|
echo "No version changes detected — nothing to commit"
|
||||||
|
else
|
||||||
|
git commit -m "chore: set version to ${VERSION} [skip bump]
|
||||||
|
|
||||||
|
Authored-by: Moko Consulting"
|
||||||
|
git push
|
||||||
|
echo "### Version Set" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Version updated to \`${VERSION}\` on branch \`${GITHUB_REF_NAME}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: mokocli.Universal
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
|
# PATH: /.mokogitea/workflows/workflow-sync-trigger.yml
|
||||||
|
# VERSION: 01.01.00
|
||||||
|
# BRIEF: Trigger workflow sync to live repos when a PR is merged to main
|
||||||
|
|
||||||
|
name: "Universal: Workflow Sync Trigger"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [closed]
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sync:
|
||||||
|
name: Sync workflows to live repos
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: >-
|
||||||
|
github.event.pull_request.merged == true &&
|
||||||
|
!contains(github.event.pull_request.title, '[skip sync]')
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Determine platform from repo name
|
||||||
|
id: platform
|
||||||
|
run: |
|
||||||
|
REPO="${{ github.event.repository.name }}"
|
||||||
|
case "$REPO" in
|
||||||
|
Template-Joomla) PLATFORM="joomla" ;;
|
||||||
|
Template-Dolibarr) PLATFORM="dolibarr" ;;
|
||||||
|
Template-Go) PLATFORM="go" ;;
|
||||||
|
Template-MCP) PLATFORM="mcp" ;;
|
||||||
|
Template-Generic) PLATFORM="" ;;
|
||||||
|
*) PLATFORM="" ;;
|
||||||
|
esac
|
||||||
|
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Platform: ${PLATFORM:-all}"
|
||||||
|
|
||||||
|
- name: Clone mokocli
|
||||||
|
env:
|
||||||
|
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
run: |
|
||||||
|
GITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}"
|
||||||
|
git clone --depth 1 "${GITEA_URL}/MokoConsulting/mokocli.git" /tmp/mokocli
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
cd /tmp/mokocli
|
||||||
|
composer install --no-dev --no-interaction --quiet 2>/dev/null || true
|
||||||
|
|
||||||
|
- name: Run workflow sync
|
||||||
|
env:
|
||||||
|
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
run: |
|
||||||
|
ARGS="--token ${MOKOGITEA_TOKEN}"
|
||||||
|
ARGS="${ARGS} --org ${{ vars.GITEA_ORG || github.repository_owner }}"
|
||||||
|
ARGS="${ARGS} --phase repos"
|
||||||
|
|
||||||
|
PLATFORM="${{ steps.platform.outputs.platform }}"
|
||||||
|
if [ -n "$PLATFORM" ]; then
|
||||||
|
ARGS="${ARGS} --platform-filter ${PLATFORM}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
php /tmp/mokocli/cli/workflow_sync.php ${ARGS}
|
||||||
+81
-228
@@ -1,236 +1,89 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Instagram carousel**: Multi-image/video posts via Meta carousel container flow (up to 10 items)
|
||||||
|
- **Instagram Reels**: Short-form video publishing via REELS media type
|
||||||
|
- **Instagram Stories**: Image and video story publishing via STORIES media type
|
||||||
|
- **Instagram alt text**: Alt text support for image containers
|
||||||
|
- **Nostr plugin**: Full NIP-01 WebSocket relay publishing with BIP-340 Schnorr signatures (pure PHP, requires ext-gmp)
|
||||||
|
- **Nostr**: Publishes kind-1 text note events to multiple relays with automatic failover
|
||||||
|
- **Nostr**: Raw WebSocket client using stream_socket_client (no external dependencies)
|
||||||
|
- **Nostr**: Public key derivation and event signing via secp256k1 elliptic curve math
|
||||||
|
- **Threads carousel**: Support up to 20-item carousel posts via Threads API multi-container flow
|
||||||
|
- **Threads polls**: Poll creation support via poll_options parameter (2-4 options)
|
||||||
|
- **Threads spoiler tags**: Content warning / spoiler flag support for Threads posts
|
||||||
|
- **Threads text-only optimization**: Simplified single-step flow for text-only posts without media
|
||||||
|
|
||||||
<!-- VERSION: 01.02.00 -->
|
### Fixed
|
||||||
|
- Webservices plugin Joomla 6 compatibility — `onBeforeApiRoute` receives `BeforeApiRouteEvent` object, extract router via `$event->getRouter()`
|
||||||
|
|
||||||
|
## [01.07.00] --- 2026-06-23
|
||||||
|
|
||||||
|
## [01.07.00] --- 2026-06-23
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Full ACL system**: 12 granular permissions in access.xml with permissions fieldset in config.xml
|
||||||
|
- **ACL enforcement**: All controllers and views check permissions before allowing actions
|
||||||
|
- **MokoSuiteCrossHelper::getActions()**: Centralized ACL helper for toolbar and view logic
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **License warning**: Removed duplicate from system plugin (install script already shows it)
|
||||||
|
- **Content plugin**: Fixed func_get_arg crash when non-article content is saved (e.g. update sites, installer)
|
||||||
|
|
||||||
|
## [01.05.00] --- 2026-06-23
|
||||||
|
|
||||||
|
## [01.05.00] --- 2026-06-23
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Instagram plugin**: Cross-post to Instagram via Meta Content Publishing API (2-step container flow)
|
||||||
|
- **YouTube plugin**: Cross-post to YouTube via Data API v3 channel bulletins
|
||||||
|
- **Share Content panel**: Per-article editor panel with platform-specific share text fields
|
||||||
|
- **New placeholders**: {social}, {short}, {chat}, {email_subject}, {email_body} for platform-optimized templates
|
||||||
|
- **Share image control**: Choose intro image, fulltext image, custom image, or no image per article
|
||||||
|
- **Mailchimp templates**: Support Mailchimp saved templates with section injection, plus responsive email wrapper fallback
|
||||||
|
- **Delete from platforms**: New MokoSuiteCrossDeleteInterface for removing cross-posted content from remote platforms
|
||||||
|
- **Delete support**: Twitter, Mastodon, Bluesky, Facebook, LinkedIn, Telegram, Discord (7 of 38 plugins)
|
||||||
|
- **Auto-delete on unpublish**: Component config option to delete from platforms when articles are unpublished or trashed
|
||||||
|
- **UTM auto-tagging**: Append utm_source, utm_medium, utm_campaign to shared URLs with {platform} token support
|
||||||
|
- **Caption rotation**: {random:opt1|opt2|opt3} placeholder picks a random option per post
|
||||||
|
- **{url_raw} placeholder**: Clean article URL without UTM parameters
|
||||||
|
- **Mastodon enhancements**: Visibility levels, content warnings, scheduled posts, polls, language tags
|
||||||
|
- **Bluesky threads**: Auto-split long messages into reply chains at sentence boundaries
|
||||||
|
- **Bluesky link cards**: External link card embeds with article title and description
|
||||||
|
- **Ntfy default server**: Default server changed to ntfy.mokoconsulting.tech with configurable plugin params
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **Default templates**: Updated to use platform-specific placeholders (social/short/chat/email) with graceful fallback
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Mailchimp**: Fixed broken namespace placeholder in XML manifest
|
||||||
|
- **ConvertKit**: Removed duplicate curl_setopt_array with undefined $token
|
||||||
|
- **Brevo**: Removed duplicate curl_setopt_array with undefined $token and wrong auth header
|
||||||
|
- **Constant Contact**: Removed duplicate curl_setopt_array
|
||||||
|
- **Mailchimp**: Fixed campaign creation checking HTTP 200 instead of 2xx range
|
||||||
|
- **Medium**: Fixed getUserId() returning array instead of string on error
|
||||||
|
- **Bluesky**: Replaced md5() with hash('sha256', ...) for cache key
|
||||||
|
- **ServiceController**: Exception details no longer exposed to client
|
||||||
|
- **License warning**: Removed duplicate from system plugin -- install script already shows it with direct edit link
|
||||||
|
|
||||||
|
## [01.04.01] --- 2026-06-21
|
||||||
|
|
||||||
|
|
||||||
|
## [01.04.01] --- 2026-06-21
|
||||||
|
|
||||||
|
|
||||||
|
## [01.04.00] --- 2026-06-21
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Package manifest**: Added missing `plg_system_mokosuitecross_events` and `plg_system_mokosuitecross_gallery` to `pkg_mokosuitecross.xml` — these system plugins were not installed with the package
|
||||||
|
- **Cleanup**: Removed old `src/` directory (pre-rename cruft with `mokojoomcross` files)
|
||||||
|
|
||||||
|
## [01.03.00] --- 2026-06-21
|
||||||
|
|
||||||
|
|
||||||
|
<!-- VERSION: 01.08.37 -->
|
||||||
|
|
||||||
All notable changes to MokoSuiteCross will be documented in this file.
|
All notable changes to MokoSuiteCross will be documented in this file.
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||||
|
|
||||||
## [01.02.00] --- 2026-06-21
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- **Rebrand complete**: All 1,151 language key references renamed from `MOKOJOOMCROSS` to `MOKOSUITECROSS` across .ini, .xml, and .php files
|
|
||||||
- **Event names**: All Joomla events renamed from `onMokoJoomCross*` to `onMokoSuiteCross*`
|
|
||||||
- **Telegram default bot**: Updated from @MokoWaaSBot to @mokosuite_bot with obfuscated embedded token
|
|
||||||
- **Branding**: All `MokoWaaS` references updated to `MokoSuite` across codebase, wiki, and docs
|
|
||||||
- **Wiki**: Reorganized into folder structure (getting-started/, user-guide/, services/, developer/)
|
|
||||||
- **README**: Updated with all 36 implemented service plugins and current feature list
|
|
||||||
- **PR workflow**: Added README/CHANGELOG diff check — blocks PRs that modify source without updating CHANGELOG
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- **SendGrid**: Removed duplicate `curl_setopt_array` with undefined `$token` variable in `publish()`
|
|
||||||
- **Reddit**: Removed duplicate `curl_setopt_array` with undefined `$token` variable in `publish()`
|
|
||||||
- **TikTok**: Removed duplicate `curl_setopt_array` in `publish()`
|
|
||||||
- **Pinterest**: Removed duplicate `curl_setopt_array` in `publish()`
|
|
||||||
- **Telegram**: Added missing `<config>` section to plugin XML for parse_mode and disable_preview settings
|
|
||||||
|
|
||||||
### Fixed (previous)
|
|
||||||
- **C-1 OauthController**: Added CSRF nonce validation to OAuth callback — session-based nonce is generated during `authorize()`, embedded in the state parameter, and verified in `callback()` to prevent CSRF attacks
|
|
||||||
- **C-2 DispatchController**: Added POST method enforcement — rejects non-POST requests with 405 status
|
|
||||||
- **C-5 ServiceModel**: Credential form fields (`cred_*`) are now collected into the `credentials` JSON column on save, and expanded back into individual fields on load — previously these fields were silently discarded
|
|
||||||
- **H-1 Event pattern**: Fixed Joomla 5 SubscriberInterface incompatibility where `onMokoSuiteCrossGetServices` by-reference pattern silently lost all service plugins — dispatchers now read plugin instances from Event ArrayAccess indices after dispatch
|
|
||||||
- **H-4 ServiceTable**: Added `check()` method with alias generation, required field validation (title, service_type), timestamp management, and JSON defaults for credentials/params
|
|
||||||
- **H-9 WebhookService**: Fixed credential key mismatch — `publish()` and `validateCredentials()` now use keys matching the service.xml form fields (`url`, `method`, `auth_type`, `bearer_token`, `basic_username`, `basic_password`, `content_type`) and properly apply Bearer/Basic auth headers
|
|
||||||
- **M-4 ServiceIconHelper**: Escaped `$extraClass` parameter in `renderIcon()` with `htmlspecialchars()` to prevent XSS
|
|
||||||
- **M-5 Content plugin**: Fixed double-escaped HTML in cross-post history panel — uses `setFieldAttribute()` to inject history HTML into the note field description after XML load, avoiding XML attribute encoding
|
|
||||||
|
|
||||||
- **Content plugin**: Fixed `onContentBeforeDisplay` signature for Joomla 5/6 — now accepts `BeforeDisplayEvent` object instead of individual parameters
|
|
||||||
|
|
||||||
- **QueueProcessor**: Replaced read-then-write DB lock with MySQL advisory locks (`GET_LOCK`/`RELEASE_LOCK`) to eliminate race condition
|
|
||||||
- **Twitter/X**: Replaced Bearer token auth with OAuth 1.0a (HMAC-SHA1) — Bearer tokens are app-only and cannot create tweets
|
|
||||||
- **service.xml**: Fixed missing closing `</field>` tag on webhook method field
|
|
||||||
- **Views**: Added missing `Toolbar` and `Route` imports in Logs, Posts, Services, Template, Templates HtmlView files
|
|
||||||
- **13 service plugins**: Fixed broken `publish()` methods that had literal placeholder URLs instead of using credential values — ActivityPub, Blogger, Ghost, Google Business, Hashnode, Matrix, Medium, Nostr, RSS Feed, Threads, Tumblr, WhatsApp, WordPress
|
|
||||||
- **Ghost**: Proper JWT auth from `{id}:{secret}` admin API key format
|
|
||||||
- **WordPress**: Correct Basic Auth (not Bearer) with Application Passwords
|
|
||||||
- **Medium**: 2-step flow — fetch user ID via /v1/me, then post
|
|
||||||
- **Matrix**: PUT with transaction ID for idempotent message sending
|
|
||||||
- **Hashnode**: GraphQL mutation with proper query structure
|
|
||||||
- **Threads**: 2-step container creation + publish flow
|
|
||||||
- **WhatsApp**: Meta Cloud API with messaging_product payload
|
|
||||||
- **Nostr**: Stub with clear "not yet implemented" message (requires WebSocket)
|
|
||||||
- **RSS Feed**: Local service — no external API, always succeeds
|
|
||||||
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- **ServiceIconHelper**: Centralised icon mapping for all 34 service types — replaces per-template icon arrays with `ServiceIconHelper::getIcon()` / `::renderIcon()`
|
|
||||||
- **Service Stats drill-down**: New `servicestats` view with per-service analytics — post counts, success rate, daily trend chart, recent posts table, and top articles list
|
|
||||||
- **Dashboard service links**: Service breakdown table rows now link to the per-service stats view with service type icons
|
|
||||||
- **Posts list icons**: Service type column in the posts list now shows the service icon
|
|
||||||
- **Category routing rules**: New `#__mokosuitecross_category_rules` table to whitelist services per Joomla category — if rules exist for a category, only those services receive posts; no rules = all services (backward compatible)
|
|
||||||
- **CrossPostDispatcher**: Category rule filtering integrated before per-article service filter in the dispatch loop
|
|
||||||
- **Template editor**: Live character counter below template body textarea with platform-aware limits (green/yellow/red badges)
|
|
||||||
- **Template editor**: Added `{tags}`, `{hashtags}`, and `{field:xxx}` rows to the placeholder reference table
|
|
||||||
- **Content plugin**: Cross-post history panel in article editor showing last 10 posts with status badges, service names, timestamps, and error messages
|
|
||||||
- **Config**: New "Category Rules" fieldset with explanatory note about the feature
|
|
||||||
|
|
||||||
- **CrossPostDispatcher**: New static helper (`com_mokosuitecross/Helper/CrossPostDispatcher`) centralising dispatch logic for reuse by all source plugins
|
|
||||||
- **Content plugin**: Added `onContentAfterSave` and `onContentChangeState` handlers with Joomla 5/6 event compatibility, dispatching via `CrossPostDispatcher`
|
|
||||||
- **plg_system_mokosuitecross_events**: New source plugin for MokoSuiteCalendar — cross-posts calendar events when published
|
|
||||||
- **plg_system_mokosuitecross_gallery**: New source plugin for MokoSuiteGallery — cross-posts galleries and images when published
|
|
||||||
|
|
||||||
- **Credential fields**: Added fields for 19 previously missing services (Pinterest, Tumblr, TikTok, Nostr, ActivityPub, Brevo, ConvertKit, Constant Contact, Hashnode, Blogger, Google Business, RSS Feed config)
|
|
||||||
- **Twitter**: Access Token and Access Token Secret fields for OAuth 1.0a
|
|
||||||
- **LinkedIn**: Refresh token field for automatic token renewal
|
|
||||||
- **Bluesky**: PDS URL field for self-hosted instances
|
|
||||||
- **Discord**: Username and avatar URL override fields
|
|
||||||
- **Mailchimp**: From name and from email fields
|
|
||||||
- **SendGrid**: From email and from name fields
|
|
||||||
- **Reddit**: Account password field for script-type OAuth
|
|
||||||
- **WordPress**: Default post status selector (draft/publish)
|
|
||||||
- **Dev.to**: Organization ID field
|
|
||||||
- **Ghost**: Default post status selector (draft/published)
|
|
||||||
- **Webhook**: Auth type selector (none/bearer/basic), auth token field, content type selector (JSON/form)
|
|
||||||
- **RSS Feed**: Feed title and max items config fields
|
|
||||||
- **OAuth services**: Added Pinterest, Tumblr, TikTok, Constant Contact, Blogger, Google Business to OAuth authorize flow
|
|
||||||
- **Developer Guide**: Comprehensive wiki page for building new service plugins
|
|
||||||
- **Help articles**: 42 KB articles on mokoconsulting.tech (overview, installation, 34 per-service guides, templates, queue, troubleshooting)
|
|
||||||
- **Service help link**: Per-service "Setup Guide" button in service edit sidebar links to the matching KB article
|
|
||||||
- **Evergreen re-sharing**: Articles can be marked as evergreen for automatic recurring cross-posts on a configurable interval (default 30 days)
|
|
||||||
- **Post edit form**: Full CRUD for queue posts — edit message, reschedule, change status, re-queue failed posts
|
|
||||||
- **Manual post creator**: New button in Post Queue toolbar to create manual cross-posts with article/service selection, custom message, and optional scheduling
|
|
||||||
- **Scheduled posts**: Calendar picker for scheduling posts to specific date/time; scheduled_at shown in queue list
|
|
||||||
- **Dashboard trend chart**: Chart.js line chart showing daily posted vs failed counts between stat cards and service breakdown
|
|
||||||
- **Dashboard date range filter**: Period selector (7/30/90 days, all time) filters service breakdown, top articles, and trend chart
|
|
||||||
- **Hashtag placeholders**: `{tags}` (comma-separated) and `{hashtags}` (#-prefixed space-separated) template placeholders from article tags
|
|
||||||
- **Posts service filter**: SQL-driven service dropdown filter in posts list, plus search filter by article title or message content
|
|
||||||
- **CSV export**: "Export CSV" toolbar button on posts list to download filtered post data as CSV
|
|
||||||
- **WordPress canonical URL**: WordPress cross-posts now include an "Originally published at" source link appended to content with the Joomla article URL
|
|
||||||
- **REST API dispatch endpoint**: `POST /api/v1/mokosuitecross/dispatch` — trigger cross-posts for an article via API with optional service filtering, duplicate guard, and template rendering
|
|
||||||
|
|
||||||
|
|
||||||
### Added (original)
|
|
||||||
|
|
||||||
#### Core Engine
|
|
||||||
- Cross-posting engine dispatches articles to service plugins on publish
|
|
||||||
- System plugin hooks `onContentAfterSave` and `onContentChangeState`
|
|
||||||
- Duplicate guard prevents re-posting to services that already received an article
|
|
||||||
- Message template rendering with 8 placeholders: `{title}`, `{url}`, `{introtext}`, `{fulltext}`, `{image}`, `{category}`, `{author}`, `{date}`
|
|
||||||
- Custom `mokosuitecross` plugin group for extensible service architecture
|
|
||||||
- `MokoSuiteCrossServiceInterface` contract for all service plugins
|
|
||||||
|
|
||||||
#### Admin Component (5 views)
|
|
||||||
- **Dashboard** — summary cards, posts-by-service analytics with success rates, top cross-posted articles, recent activity feed, PP Pro migration banner, page-load processing warning
|
|
||||||
- **Post Queue** — list with color-coded status badges, error messages, retry counts, platform post IDs, article/service columns, date filters
|
|
||||||
- **Services** — CRUD with service type selector (34 platforms organized by category), default/custom mode badges, publish toggle, credential editor
|
|
||||||
- **Templates** — CRUD for message templates, per-platform assignment, placeholder reference panel, template body preview
|
|
||||||
- **Activity Logs** — list with level badges (info/warning/error), service column, context data, level and search filters
|
|
||||||
|
|
||||||
#### Queue Processing (3 methods)
|
|
||||||
- Joomla Scheduled Task plugin (`plg_task_mokosuitecross`) — preferred, processes 20 posts per run
|
|
||||||
- Page-load fallback via system plugin `onAfterRender` — configurable throttle interval, backend/frontend/both
|
|
||||||
- Shared `QueueProcessor` helper with DB lock to prevent concurrent execution
|
|
||||||
- Failed post retry with configurable max retries and exponential delay
|
|
||||||
- Scheduled post support (`scheduled_at` column)
|
|
||||||
- Automatic log cleanup based on configurable retention period
|
|
||||||
|
|
||||||
#### Per-Article Controls
|
|
||||||
- "Cross-Posting" fieldset injected into article editor via `onContentPrepareForm`
|
|
||||||
- Skip cross-posting toggle per article
|
|
||||||
- Service selection checkboxes (unchecked = post to all enabled services)
|
|
||||||
|
|
||||||
#### OAuth 2.0
|
|
||||||
- `OAuthHelper` with authorization URL generation, code-to-token exchange, token storage
|
|
||||||
- Twitter PKCE flow support
|
|
||||||
- `OauthController` with authorize and callback endpoints
|
|
||||||
- Reads client ID/secret from service plugin params
|
|
||||||
|
|
||||||
#### Perfect Publisher Pro Migration
|
|
||||||
- Reads `#__autotweet_channels` table with per-platform credential mapping
|
|
||||||
- Fallback extraction from component params when channel table missing
|
|
||||||
- Maps Facebook, Twitter, LinkedIn, Telegram, Discord, Slack, Mastodon
|
|
||||||
- Creates services in disabled state for manual verification
|
|
||||||
- One-click migration from dashboard
|
|
||||||
|
|
||||||
#### Service Plugins (34 platforms)
|
|
||||||
|
|
||||||
**Social Media (12)**
|
|
||||||
- Facebook / Meta — Graph API v19.0, default MokoSuite app mode, page feed posting
|
|
||||||
- X / Twitter — API v2, OAuth 2.0 Bearer Token, 280 char limit
|
|
||||||
- LinkedIn — Share API v2, organization + personal profile, 3000 char limit
|
|
||||||
- Mastodon — API v1, multi-instance, hashtags, 500 char limit
|
|
||||||
- Bluesky — AT Protocol, session auth, app passwords, 300 char limit
|
|
||||||
- Threads (Meta) — Threads Publishing API, default app mode, 500 char limit
|
|
||||||
- Pinterest — Pins API v5, board selection, image-focused
|
|
||||||
- Reddit — OAuth2 link submission, subreddit selection
|
|
||||||
- Tumblr — API v2, link/text posts, OAuth 1.0a
|
|
||||||
- TikTok — Content Posting API, photo slideshows
|
|
||||||
- Nostr — NIP-01 event publishing, configurable relays
|
|
||||||
- ActivityPub — generic Fediverse (Pleroma, Akkoma, Misskey, Pixelfed)
|
|
||||||
|
|
||||||
**Chat / Messaging (8)**
|
|
||||||
- Telegram — Bot API, default @mokosuite_bot + custom bot, HTML/Markdown, 4096 chars
|
|
||||||
- Discord — Webhooks, default MokoSuite webhook mode, embeds, 2000 chars
|
|
||||||
- Slack — Incoming Webhooks, default MokoSuite webhook mode, Block Kit
|
|
||||||
- Microsoft Teams — Incoming Webhooks, default mode, Adaptive Cards
|
|
||||||
- Google Chat — Webhook API, card formatting
|
|
||||||
- WhatsApp Business — Meta Cloud API, template + free-form messages
|
|
||||||
- Matrix / Element — Client-Server API, self-hosted homeserver support
|
|
||||||
- Ntfy — Push notifications, priority levels, action buttons
|
|
||||||
|
|
||||||
**Email / Newsletter (5)**
|
|
||||||
- Mailchimp — Campaigns API, audience selection, send/draft modes
|
|
||||||
- SendGrid — Marketing Campaigns API v3, Single Send creation
|
|
||||||
- Brevo (Sendinblue) — API v3, campaign creation
|
|
||||||
- ConvertKit — API v3, broadcast creation
|
|
||||||
- Constant Contact — API v3, campaign creation
|
|
||||||
|
|
||||||
**Publishing / Blogging (6)**
|
|
||||||
- Medium — Publishing API, full HTML, canonical URL, tags
|
|
||||||
- WordPress — REST API v2, Application Passwords, category mapping
|
|
||||||
- Dev.to — Forem API, markdown, series support
|
|
||||||
- Ghost — Admin API v5, JWT auth, full HTML
|
|
||||||
- Hashnode — GraphQL API, cover image, tags
|
|
||||||
- Google Blogger — Blogger API v3, labels from categories
|
|
||||||
|
|
||||||
**Business (1)**
|
|
||||||
- Google Business Profile — API v1, local posts (UPDATE/EVENT/OFFER)
|
|
||||||
|
|
||||||
**Universal (2)**
|
|
||||||
- Generic Webhook — POST/PUT to any URL, JSON/form body, custom headers (IFTTT, Zapier, n8n, Make)
|
|
||||||
- RSS Feed — dedicated cross-post feed generation
|
|
||||||
|
|
||||||
#### Plugin Configuration
|
|
||||||
- Telegram: default bot token, parse mode, link preview toggle
|
|
||||||
- Facebook: default page access token, default page ID
|
|
||||||
- Discord: default webhook URL, embed color
|
|
||||||
- Slack: default webhook URL
|
|
||||||
- LinkedIn: OAuth client ID/secret, redirect URI
|
|
||||||
- Mastodon: default instance URL, visibility, hashtags
|
|
||||||
- Bluesky: default PDS URL, auto link cards
|
|
||||||
- Mailchimp: default sender name/email, auto-send toggle
|
|
||||||
- Microsoft Teams: default webhook URL
|
|
||||||
- Threads: default webhook URL
|
|
||||||
|
|
||||||
#### Infrastructure
|
|
||||||
- 7 CI/CD workflows: CI, auto-release, pre-release, auto-bump, update-server, cascade-dev, issue-branch
|
|
||||||
- Joomla update server (`updates.xml`) with development channel
|
|
||||||
- WebServices REST API plugin with CRUD routes for posts and services
|
|
||||||
- Database: 4 tables (services, posts, templates, logs) with default templates
|
|
||||||
- Package installer with auto-enable for core + task + service plugins
|
|
||||||
- 9 wiki documentation pages
|
|
||||||
- Windows Terminal profile in Joomla dropdown
|
|
||||||
|
|
||||||
|
|
||||||
## [01.01.00] - 2026-06-19
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- Initial package structure with component, system plugin, content plugin, and webservices plugin
|
|
||||||
- Admin component with dashboard, post queue, services management, and activity logs
|
|
||||||
- System plugin triggering cross-post on article publish via `onContentAfterSave`
|
|
||||||
- Content plugin adding cross-post controls to article editor
|
|
||||||
- WebServices API plugin with REST endpoints for posts and services
|
|
||||||
- Custom `mokosuitecross` plugin group for extensible service architecture
|
|
||||||
- Service plugins: Facebook, X/Twitter, LinkedIn, Mastodon, Bluesky, Mailchimp, Telegram, Discord, Slack
|
|
||||||
- Database tables: services, posts, templates, logs
|
|
||||||
- Perfect Publisher Pro migration tool in installer script
|
|
||||||
- Message template system with per-platform placeholders
|
|
||||||
- Post queue with scheduled posting, retry logic, and delivery tracking
|
|
||||||
|
|
||||||
## [01.00] - 2026-05-28
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- Initial release
|
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
|
||||||
|
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 (./LICENSE).
|
||||||
|
|
||||||
|
# FILE INFORMATION
|
||||||
|
DEFGROUP: Template-Joomla
|
||||||
|
INGROUP: Template-Joomla.Documentation
|
||||||
|
REPO: https://github.com/mokoconsulting-tech/Template-Joomla/
|
||||||
|
VERSION: 01.08.37
|
||||||
|
PATH: ./CODE_OF_CONDUCT.md
|
||||||
|
BRIEF: Community expectations and enforcement guidelines
|
||||||
|
NOTE: Adapted with attribution from the Contributor Covenant v2.1
|
||||||
|
-->
|
||||||
|
|
||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
- Be empathetic and kind
|
||||||
|
- Be respectful of differing opinions
|
||||||
|
- Accept constructive feedback
|
||||||
|
- Own mistakes and learn from them
|
||||||
|
|
||||||
|
Unacceptable behavior includes sexualized language/imagery, trolling, harassment, doxing, and other inappropriate conduct.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
Report incidents to **hello@mokoconsulting.tech** or through GitHub Discussions if you prefer a community-visible approach. Private complaints will be reviewed promptly and fairly.
|
||||||
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
1. **Correction** — Private warning
|
||||||
|
2. **Warning** — Formal warning and limited interaction
|
||||||
|
3. **Temporary Ban** — Time-boxed exclusion
|
||||||
|
4. **Permanent Ban** — Removal from the community
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
Adapted from the Contributor Covenant v2.1.
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,203 +0,0 @@
|
|||||||
# Makefile for Joomla Extensions
|
|
||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# MokoSuiteCross — Cross-posting Joomla content to social media, email marketing, and chat platforms
|
|
||||||
|
|
||||||
# ==============================================================================
|
|
||||||
# CONFIGURATION - Customize these for your extension
|
|
||||||
# ==============================================================================
|
|
||||||
|
|
||||||
# Extension Configuration
|
|
||||||
EXTENSION_NAME := mokosuitecross
|
|
||||||
EXTENSION_TYPE := package
|
|
||||||
# Options: module, plugin, component, package, template
|
|
||||||
EXTENSION_VERSION := 1.0.0
|
|
||||||
|
|
||||||
# Module Configuration (for modules only)
|
|
||||||
MODULE_TYPE := site
|
|
||||||
# Options: site, admin
|
|
||||||
|
|
||||||
# Plugin Configuration (for plugins only)
|
|
||||||
PLUGIN_GROUP := system
|
|
||||||
# Options: system, content, user, authentication, etc.
|
|
||||||
|
|
||||||
# Directories
|
|
||||||
SRC_DIR := src
|
|
||||||
BUILD_DIR := build
|
|
||||||
DIST_DIR := dist
|
|
||||||
DOCS_DIR := docs
|
|
||||||
|
|
||||||
# Joomla Installation (for local testing - customize paths)
|
|
||||||
JOOMLA_ROOT := /var/www/html/joomla
|
|
||||||
JOOMLA_VERSION := 4
|
|
||||||
|
|
||||||
# Tools
|
|
||||||
PHP := php
|
|
||||||
COMPOSER := composer
|
|
||||||
NPM := npm
|
|
||||||
PHPCS := vendor/bin/phpcs
|
|
||||||
PHPCBF := vendor/bin/phpcbf
|
|
||||||
PHPUNIT := vendor/bin/phpunit
|
|
||||||
ZIP := zip
|
|
||||||
|
|
||||||
# Coding Standards
|
|
||||||
PHPCS_STANDARD := Joomla
|
|
||||||
|
|
||||||
# Colors for output
|
|
||||||
COLOR_RESET := \033[0m
|
|
||||||
COLOR_GREEN := \033[32m
|
|
||||||
COLOR_YELLOW := \033[33m
|
|
||||||
COLOR_BLUE := \033[34m
|
|
||||||
COLOR_RED := \033[31m
|
|
||||||
|
|
||||||
# ==============================================================================
|
|
||||||
# TARGETS
|
|
||||||
# ==============================================================================
|
|
||||||
|
|
||||||
.PHONY: help
|
|
||||||
help: ## Show this help message
|
|
||||||
@echo "$(COLOR_BLUE)╔════════════════════════════════════════════════════════════╗$(COLOR_RESET)"
|
|
||||||
@echo "$(COLOR_BLUE)║ Joomla Extension Makefile ║$(COLOR_RESET)"
|
|
||||||
@echo "$(COLOR_BLUE)╚════════════════════════════════════════════════════════════╝$(COLOR_RESET)"
|
|
||||||
@echo ""
|
|
||||||
@echo "Extension: $(EXTENSION_NAME) ($(EXTENSION_TYPE)) v$(EXTENSION_VERSION)"
|
|
||||||
@echo ""
|
|
||||||
@echo "$(COLOR_GREEN)Available targets:$(COLOR_RESET)"
|
|
||||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " $(COLOR_BLUE)%-20s$(COLOR_RESET) %s\n", $$1, $$2}'
|
|
||||||
@echo ""
|
|
||||||
|
|
||||||
.PHONY: install-deps
|
|
||||||
install-deps: ## Install all dependencies (Composer + npm)
|
|
||||||
@echo "$(COLOR_BLUE)Installing dependencies...$(COLOR_RESET)"
|
|
||||||
@if [ -f "composer.json" ]; then \
|
|
||||||
$(COMPOSER) install; \
|
|
||||||
echo "$(COLOR_GREEN)✓ Composer dependencies installed$(COLOR_RESET)"; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
.PHONY: lint
|
|
||||||
lint: ## Run PHP linter (syntax check)
|
|
||||||
@echo "$(COLOR_BLUE)Running PHP linter...$(COLOR_RESET)"
|
|
||||||
@find . -name "*.php" ! -path "./vendor/*" ! -path "./node_modules/*" ! -path "./$(BUILD_DIR)/*" \
|
|
||||||
-exec $(PHP) -l {} \; | grep -v "No syntax errors" || true
|
|
||||||
@echo "$(COLOR_GREEN)✓ PHP linting complete$(COLOR_RESET)"
|
|
||||||
|
|
||||||
.PHONY: phpcs
|
|
||||||
phpcs: ## Run PHP CodeSniffer (Joomla standards)
|
|
||||||
@echo "$(COLOR_BLUE)Running PHP CodeSniffer...$(COLOR_RESET)"
|
|
||||||
@if [ -f "$(PHPCS)" ]; then \
|
|
||||||
$(PHPCS) --standard=$(PHPCS_STANDARD) --extensions=php --ignore=vendor,node_modules,$(BUILD_DIR) .; \
|
|
||||||
else \
|
|
||||||
echo "$(COLOR_YELLOW)⚠ PHP CodeSniffer not installed. Run: make install-deps$(COLOR_RESET)"; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
.PHONY: validate
|
|
||||||
validate: lint phpcs ## Run all validation checks
|
|
||||||
@echo "$(COLOR_GREEN)✓ All validation checks passed$(COLOR_RESET)"
|
|
||||||
|
|
||||||
.PHONY: clean
|
|
||||||
clean: ## Clean build artifacts
|
|
||||||
@echo "$(COLOR_BLUE)Cleaning build artifacts...$(COLOR_RESET)"
|
|
||||||
@rm -rf $(BUILD_DIR) $(DIST_DIR)
|
|
||||||
@echo "$(COLOR_GREEN)✓ Build artifacts cleaned$(COLOR_RESET)"
|
|
||||||
|
|
||||||
MOKO_PLATFORM ?= $(or $(wildcard ../moko-platform),$(wildcard $(HOME)/moko-platform),$(wildcard /opt/moko-platform))
|
|
||||||
MINIFY_SCRIPT := $(MOKO_PLATFORM)/build/minify.js
|
|
||||||
|
|
||||||
.PHONY: minify
|
|
||||||
minify: ## Minify CSS/JS assets
|
|
||||||
@echo "Minifying assets..."
|
|
||||||
@if [ -f "$(MINIFY_SCRIPT)" ]; then \
|
|
||||||
node "$(MINIFY_SCRIPT)" $(SRC_DIR); \
|
|
||||||
elif [ -f "scripts/minify.js" ]; then \
|
|
||||||
node scripts/minify.js; \
|
|
||||||
else \
|
|
||||||
echo "No minify script found"; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
.PHONY: build
|
|
||||||
build: clean validate minify ## Build extension package
|
|
||||||
@echo "$(COLOR_BLUE)Building Joomla extension package...$(COLOR_RESET)"
|
|
||||||
@mkdir -p $(DIST_DIR) $(BUILD_DIR)
|
|
||||||
|
|
||||||
# Determine package prefix based on extension type
|
|
||||||
@case "$(EXTENSION_TYPE)" in \
|
|
||||||
module) \
|
|
||||||
PACKAGE_PREFIX="mod_$(EXTENSION_NAME)"; \
|
|
||||||
BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \
|
|
||||||
;; \
|
|
||||||
plugin) \
|
|
||||||
PACKAGE_PREFIX="plg_$(PLUGIN_GROUP)_$(EXTENSION_NAME)"; \
|
|
||||||
BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \
|
|
||||||
;; \
|
|
||||||
component) \
|
|
||||||
PACKAGE_PREFIX="com_$(EXTENSION_NAME)"; \
|
|
||||||
BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \
|
|
||||||
;; \
|
|
||||||
package) \
|
|
||||||
PACKAGE_PREFIX="pkg_$(EXTENSION_NAME)"; \
|
|
||||||
BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \
|
|
||||||
;; \
|
|
||||||
template) \
|
|
||||||
PACKAGE_PREFIX="tpl_$(EXTENSION_NAME)"; \
|
|
||||||
BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \
|
|
||||||
;; \
|
|
||||||
*) \
|
|
||||||
echo "$(COLOR_RED)✗ Unknown extension type: $(EXTENSION_TYPE)$(COLOR_RESET)"; \
|
|
||||||
exit 1; \
|
|
||||||
;; \
|
|
||||||
esac; \
|
|
||||||
\
|
|
||||||
mkdir -p "$$BUILD_TARGET"; \
|
|
||||||
\
|
|
||||||
echo "Building $$PACKAGE_PREFIX..."; \
|
|
||||||
\
|
|
||||||
rsync -av --progress \
|
|
||||||
--exclude='$(BUILD_DIR)' \
|
|
||||||
--exclude='$(DIST_DIR)' \
|
|
||||||
--exclude='.git*' \
|
|
||||||
--exclude='vendor/' \
|
|
||||||
--exclude='node_modules/' \
|
|
||||||
--exclude='tests/' \
|
|
||||||
--exclude='Makefile' \
|
|
||||||
--exclude='composer.json' \
|
|
||||||
--exclude='composer.lock' \
|
|
||||||
--exclude='package.json' \
|
|
||||||
--exclude='package-lock.json' \
|
|
||||||
--exclude='phpunit.xml' \
|
|
||||||
--exclude='*.md' \
|
|
||||||
--exclude='.editorconfig' \
|
|
||||||
. "$$BUILD_TARGET/"; \
|
|
||||||
\
|
|
||||||
cd $(BUILD_DIR) && $(ZIP) -r "../$(DIST_DIR)/$${PACKAGE_PREFIX}-$(EXTENSION_VERSION).zip" "$${PACKAGE_PREFIX}"; \
|
|
||||||
\
|
|
||||||
echo "$(COLOR_GREEN)✓ Package created: $(DIST_DIR)/$${PACKAGE_PREFIX}-$(EXTENSION_VERSION).zip$(COLOR_RESET)"
|
|
||||||
|
|
||||||
.PHONY: package
|
|
||||||
package: build ## Alias for build
|
|
||||||
@echo "$(COLOR_GREEN)✓ Package ready for distribution$(COLOR_RESET)"
|
|
||||||
|
|
||||||
.PHONY: release
|
|
||||||
release: validate build ## Create a release (validate + build)
|
|
||||||
@echo "$(COLOR_GREEN)✓ Release package ready$(COLOR_RESET)"
|
|
||||||
|
|
||||||
.PHONY: version
|
|
||||||
version: ## Display version information
|
|
||||||
@echo "$(COLOR_BLUE)Extension Information:$(COLOR_RESET)"
|
|
||||||
@echo " Name: $(EXTENSION_NAME)"
|
|
||||||
@echo " Type: $(EXTENSION_TYPE)"
|
|
||||||
@echo " Version: $(EXTENSION_VERSION)"
|
|
||||||
|
|
||||||
.PHONY: security-check
|
|
||||||
security-check: ## Run security checks on dependencies
|
|
||||||
@echo "$(COLOR_BLUE)Running security checks...$(COLOR_RESET)"
|
|
||||||
@if [ -f "composer.json" ]; then \
|
|
||||||
$(COMPOSER) audit || echo "$(COLOR_YELLOW)⚠ Vulnerabilities found$(COLOR_RESET)"; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
.PHONY: all
|
|
||||||
all: install-deps validate build ## Run complete build pipeline
|
|
||||||
@echo "$(COLOR_GREEN)✓ Complete build pipeline finished$(COLOR_RESET)"
|
|
||||||
|
|
||||||
# Default target
|
|
||||||
.DEFAULT_GOAL := help
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# MokoSuiteCross
|
# MokoSuiteCross
|
||||||
|
|
||||||
<!-- VERSION: 01.02.00 -->
|
<!-- VERSION: 01.08.37 -->
|
||||||
|
|
||||||
Cross-posting Joomla content to social media, email marketing, and chat platforms for Joomla 5/6.
|
Cross-posting Joomla content to social media, email marketing, and chat platforms for Joomla 5/6.
|
||||||
|
|
||||||
@@ -14,20 +14,27 @@ MokoSuiteCross automatically publishes your Joomla articles to multiple platform
|
|||||||
- **Plugin-based services** — Each platform is a separate plugin; install only what you need
|
- **Plugin-based services** — Each platform is a separate plugin; install only what you need
|
||||||
- **Default bot mode** — Pre-configured bots for Telegram (@mokosuite_bot), Discord, and Slack — just add your channel
|
- **Default bot mode** — Pre-configured bots for Telegram (@mokosuite_bot), Discord, and Slack — just add your channel
|
||||||
- **Post queue** — Scheduled posting, retry on failure, detailed delivery logs
|
- **Post queue** — Scheduled posting, retry on failure, detailed delivery logs
|
||||||
- **Message templates** — Customize post format per platform with placeholders ({title}, {url}, {intro}, {image}, {tags}, {field:xxx})
|
- **Message templates** — Customize post format per platform with placeholders ({title}, {url}, {social}, {short}, {chat}, {email_subject}, {email_body}, {field:xxx})
|
||||||
|
- **Share Content panel** — Per-article fields for platform-optimized text (social, short, chat, email) with image picker
|
||||||
|
- **Caption rotation** — {random:opt1|opt2|opt3} placeholder for varying evergreen re-shares
|
||||||
|
- **UTM tracking** — Auto-append UTM parameters to shared links with {platform} token
|
||||||
|
- **Delete from platforms** — Remove cross-posted content when articles are unpublished/trashed (7 platforms)
|
||||||
- **Post history** — Track what was posted where, with platform response data
|
- **Post history** — Track what was posted where, with platform response data
|
||||||
- **Evergreen re-sharing** — Automatically re-share articles on a configurable interval
|
- **Evergreen re-sharing** — Automatically re-share articles on a configurable interval
|
||||||
- **Category routing** — Route articles to specific services by Joomla category
|
- **Category routing** — Route articles to specific services by Joomla category
|
||||||
|
- **Mailchimp templates** — Use saved Mailchimp templates with section injection, or built-in responsive email wrapper
|
||||||
- **Migration** — Import settings from Perfect Publisher Pro
|
- **Migration** — Import settings from Perfect Publisher Pro
|
||||||
- **REST API** — WebServices plugin for headless/external integration
|
- **REST API** — WebServices plugin for headless/external integration
|
||||||
|
|
||||||
### Supported Platforms (36)
|
### Supported Platforms (38)
|
||||||
|
|
||||||
#### Social Media
|
#### Social Media
|
||||||
| Platform | Plugin | Status |
|
| Platform | Plugin | Status |
|
||||||
|----------|--------|--------|
|
|----------|--------|--------|
|
||||||
| Facebook / Meta | `plg_mokosuitecross_facebook` | Implemented |
|
| Facebook / Meta | `plg_mokosuitecross_facebook` | Implemented |
|
||||||
| X / Twitter | `plg_mokosuitecross_twitter` | Implemented |
|
| X / Twitter | `plg_mokosuitecross_twitter` | Implemented |
|
||||||
|
| Instagram | `plg_mokosuitecross_instagram` | Implemented |
|
||||||
|
| YouTube | `plg_mokosuitecross_youtube` | Implemented |
|
||||||
| LinkedIn | `plg_mokosuitecross_linkedin` | Implemented |
|
| LinkedIn | `plg_mokosuitecross_linkedin` | Implemented |
|
||||||
| Mastodon | `plg_mokosuitecross_mastodon` | Implemented |
|
| Mastodon | `plg_mokosuitecross_mastodon` | Implemented |
|
||||||
| Bluesky | `plg_mokosuitecross_bluesky` | Implemented |
|
| Bluesky | `plg_mokosuitecross_bluesky` | Implemented |
|
||||||
|
|||||||
+241
@@ -0,0 +1,241 @@
|
|||||||
|
<!--
|
||||||
|
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
|
||||||
|
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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# FILE INFORMATION
|
||||||
|
DEFGROUP: Template-Joomla
|
||||||
|
INGROUP: Template-Joomla.Documentation
|
||||||
|
REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla
|
||||||
|
PATH: /SECURITY.md
|
||||||
|
VERSION: 01.08.37
|
||||||
|
BRIEF: Security vulnerability reporting and handling policy
|
||||||
|
-->
|
||||||
|
|
||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Purpose and Scope
|
||||||
|
|
||||||
|
This document defines the security vulnerability reporting, response, and disclosure policy for this Joomla Plugin template repository. It establishes the authoritative process for responsible disclosure, assessment, remediation, and communication of security issues.
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
Security updates are provided for the following versions:
|
||||||
|
|
||||||
|
| Version | Supported |
|
||||||
|
| ------- | ------------------ |
|
||||||
|
| 01.x.x | :white_check_mark: |
|
||||||
|
| < 01.0 | :x: |
|
||||||
|
|
||||||
|
Only the current major version receives security updates. Users should upgrade to the latest supported version to receive security patches.
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
### Where to Report
|
||||||
|
|
||||||
|
**DO NOT** create public GitHub issues for security vulnerabilities.
|
||||||
|
|
||||||
|
Report security vulnerabilities privately to:
|
||||||
|
|
||||||
|
**Email**: `security@mokoconsulting.tech`
|
||||||
|
|
||||||
|
**Subject Line**: `[SECURITY] Template-Joomla - Brief Description`
|
||||||
|
|
||||||
|
### What to Include
|
||||||
|
|
||||||
|
A complete vulnerability report should include:
|
||||||
|
|
||||||
|
1. **Description**: Clear explanation of the vulnerability
|
||||||
|
2. **Impact**: Potential security impact and severity assessment
|
||||||
|
3. **Affected Versions**: Which versions are vulnerable
|
||||||
|
4. **Reproduction Steps**: Detailed steps to reproduce the issue
|
||||||
|
5. **Proof of Concept**: Code, configuration, or demonstration (if applicable)
|
||||||
|
6. **Suggested Fix**: Proposed remediation (if known)
|
||||||
|
7. **Disclosure Timeline**: Your expectations for public disclosure
|
||||||
|
|
||||||
|
### Response Timeline
|
||||||
|
|
||||||
|
* **Initial Response**: Within 3 business days
|
||||||
|
* **Assessment Complete**: Within 7 business days
|
||||||
|
* **Fix Timeline**: Depends on severity (see below)
|
||||||
|
* **Disclosure**: Coordinated with reporter
|
||||||
|
|
||||||
|
## Severity Classification
|
||||||
|
|
||||||
|
Vulnerabilities are classified using the following severity levels:
|
||||||
|
|
||||||
|
### Critical
|
||||||
|
* Remote code execution
|
||||||
|
* Authentication bypass
|
||||||
|
* Data breach or exposure of sensitive information
|
||||||
|
* **Fix Timeline**: 7 days
|
||||||
|
|
||||||
|
### High
|
||||||
|
* Privilege escalation
|
||||||
|
* SQL injection or command injection
|
||||||
|
* Cross-site scripting (XSS) with significant impact
|
||||||
|
* **Fix Timeline**: 14 days
|
||||||
|
|
||||||
|
### Medium
|
||||||
|
* Information disclosure (limited scope)
|
||||||
|
* Denial of service
|
||||||
|
* Security misconfigurations with moderate impact
|
||||||
|
* **Fix Timeline**: 30 days
|
||||||
|
|
||||||
|
### Low
|
||||||
|
* Security best practice violations
|
||||||
|
* Minor information leaks
|
||||||
|
* Issues requiring user interaction or complex preconditions
|
||||||
|
* **Fix Timeline**: 60 days or next release
|
||||||
|
|
||||||
|
## Remediation Process
|
||||||
|
|
||||||
|
1. **Acknowledgment**: Security team confirms receipt and begins investigation
|
||||||
|
2. **Assessment**: Vulnerability is validated, severity assigned, and impact analyzed
|
||||||
|
3. **Development**: Security patch is developed and tested
|
||||||
|
4. **Review**: Patch undergoes security review and validation
|
||||||
|
5. **Release**: Fixed version is released with security advisory
|
||||||
|
6. **Disclosure**: Public disclosure follows coordinated timeline
|
||||||
|
|
||||||
|
## Security Advisories
|
||||||
|
|
||||||
|
Security advisories are published via:
|
||||||
|
|
||||||
|
* GitHub Security Advisories
|
||||||
|
* Release notes and CHANGELOG.md
|
||||||
|
* Email notification to project users (if mailing list is established)
|
||||||
|
|
||||||
|
Advisories include:
|
||||||
|
|
||||||
|
* CVE identifier (if applicable)
|
||||||
|
* Severity rating
|
||||||
|
* Affected versions
|
||||||
|
* Fixed versions
|
||||||
|
* Mitigation steps
|
||||||
|
* Attribution (with reporter consent)
|
||||||
|
|
||||||
|
## Security Best Practices
|
||||||
|
|
||||||
|
For projects using this template:
|
||||||
|
|
||||||
|
### Required Controls
|
||||||
|
|
||||||
|
* Enable GitHub security features (Dependabot, code scanning)
|
||||||
|
* Implement branch protection on `main`
|
||||||
|
* Require code review for all changes
|
||||||
|
* Enforce signed commits (recommended)
|
||||||
|
* Use secrets management (never commit credentials)
|
||||||
|
* Maintain security documentation
|
||||||
|
* Follow secure coding standards defined in MokoStandards
|
||||||
|
|
||||||
|
### Joomla Plugin Security
|
||||||
|
|
||||||
|
* Follow Joomla security best practices
|
||||||
|
* Validate and sanitize all user input
|
||||||
|
* Use Joomla's database API to prevent SQL injection
|
||||||
|
* Properly escape output to prevent XSS
|
||||||
|
* Implement proper access control checks
|
||||||
|
* Use Joomla's session and authentication APIs
|
||||||
|
* Keep Joomla and dependencies up to date
|
||||||
|
|
||||||
|
### CI/CD Security
|
||||||
|
|
||||||
|
* Validate all inputs
|
||||||
|
* Sanitize outputs
|
||||||
|
* Use least privilege access
|
||||||
|
* Pin dependencies with hash verification
|
||||||
|
* Scan for vulnerabilities in dependencies
|
||||||
|
* Audit third-party actions and tools
|
||||||
|
|
||||||
|
#### Automated Security Scanning
|
||||||
|
|
||||||
|
All repositories SHOULD implement:
|
||||||
|
|
||||||
|
**CodeQL Analysis**:
|
||||||
|
* Enabled for PHP and other supported languages
|
||||||
|
* Runs on: push to main, pull requests, weekly schedule
|
||||||
|
* Query sets: `security-extended` and `security-and-quality`
|
||||||
|
* Configuration: `.github/workflows/codeql-analysis.yml`
|
||||||
|
|
||||||
|
**Dependabot Security Updates**:
|
||||||
|
* Weekly scans for vulnerable dependencies
|
||||||
|
* Automated pull requests for security patches
|
||||||
|
* Configuration: `.github/dependabot.yml`
|
||||||
|
|
||||||
|
**Secret Scanning**:
|
||||||
|
* Enabled by default with push protection
|
||||||
|
* Prevents accidental credential commits
|
||||||
|
|
||||||
|
### Dependency Management
|
||||||
|
|
||||||
|
* Keep dependencies up to date
|
||||||
|
* Monitor security advisories for dependencies
|
||||||
|
* Remove unused dependencies
|
||||||
|
* Audit new dependencies before adoption
|
||||||
|
* Document security-critical dependencies
|
||||||
|
|
||||||
|
## Compliance and Governance
|
||||||
|
|
||||||
|
This security policy is aligned with MokoStandards. Deviations require documented justification.
|
||||||
|
|
||||||
|
Security policies are reviewed and updated at least annually or following significant security incidents.
|
||||||
|
|
||||||
|
## Attribution and Recognition
|
||||||
|
|
||||||
|
We acknowledge and appreciate responsible disclosure. With your permission, we will:
|
||||||
|
|
||||||
|
* Credit you in security advisories
|
||||||
|
* List you in CHANGELOG.md for the fix release
|
||||||
|
* Recognize your contribution publicly (if desired)
|
||||||
|
|
||||||
|
## Contact and Escalation
|
||||||
|
|
||||||
|
* **Security Team**: security@mokoconsulting.tech
|
||||||
|
* **Primary Contact**: hello@mokoconsulting.tech
|
||||||
|
* **Escalation**: For urgent matters requiring immediate attention, contact the maintainer directly via GitHub
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
|
||||||
|
The following are explicitly out of scope:
|
||||||
|
|
||||||
|
* Issues in third-party dependencies (report directly to maintainers)
|
||||||
|
* Social engineering attacks
|
||||||
|
* Physical security issues
|
||||||
|
* Denial of service via resource exhaustion without amplification
|
||||||
|
* Issues requiring physical access to systems
|
||||||
|
* Theoretical vulnerabilities without proof of exploitability
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
| ------------ | ------------------------------------------------------------------------------------------------------------ |
|
||||||
|
| Document | Security Policy |
|
||||||
|
| Path | /SECURITY.md |
|
||||||
|
| Repository | [https://github.com/mokoconsulting-tech/Template-Joomla](https://github.com/mokoconsulting-tech/Template-Joomla) |
|
||||||
|
| Owner | Moko Consulting |
|
||||||
|
| Scope | Security vulnerability handling |
|
||||||
|
| Status | Active |
|
||||||
|
| Effective | 2026-01-16 |
|
||||||
|
|
||||||
|
## Revision History
|
||||||
|
|
||||||
|
| Date | Change Description | Author |
|
||||||
|
| ---------- | ------------------------------------------------- | --------------- |
|
||||||
|
| 2026-01-16 | Initial creation for template repository | Moko Consulting |
|
||||||
@@ -1,237 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# ============================================================================
|
|
||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: Automation.CI
|
|
||||||
# INGROUP: moko-platform.Automation
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
|
||||||
# PATH: /automation/ci-issue-reporter.sh
|
|
||||||
# VERSION: 09.23.00
|
|
||||||
# BRIEF: Creates or updates a Gitea issue when a CI gate fails.
|
|
||||||
# Deduplicates by searching open issues with the "ci-auto" label
|
|
||||||
# whose title matches the gate. If a matching issue exists, a comment
|
|
||||||
# is appended instead of opening a duplicate.
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# ── Defaults ────────────────────────────────────────────────────────────────
|
|
||||||
GITEA_URL="${GITEA_URL:-https://git.mokoconsulting.tech}"
|
|
||||||
GITEA_TOKEN="${GITEA_TOKEN:-}"
|
|
||||||
REPO="${GITHUB_REPOSITORY:-}"
|
|
||||||
RUN_URL="${GITHUB_SERVER_URL:-${GITEA_URL}}/${REPO}/actions/runs/${GITHUB_RUN_ID:-0}"
|
|
||||||
LABEL_NAME="ci-auto"
|
|
||||||
LABEL_COLOR="#e11d48"
|
|
||||||
|
|
||||||
GATE=""
|
|
||||||
DETAILS=""
|
|
||||||
SEVERITY="error"
|
|
||||||
WORKFLOW=""
|
|
||||||
|
|
||||||
# ── Parse arguments ─────────────────────────────────────────────────────────
|
|
||||||
usage() {
|
|
||||||
cat <<EOF
|
|
||||||
Usage: ci-issue-reporter.sh --gate NAME --details TEXT [OPTIONS]
|
|
||||||
|
|
||||||
Required:
|
|
||||||
--gate CI gate name (e.g. "Code Quality", "Self-Health")
|
|
||||||
--details Human-readable failure description
|
|
||||||
|
|
||||||
Optional:
|
|
||||||
--severity "error" (default) or "warning"
|
|
||||||
--workflow Workflow name for the issue title
|
|
||||||
--repo owner/repo (default: \$GITHUB_REPOSITORY)
|
|
||||||
--run-url URL to the CI run (auto-detected from env)
|
|
||||||
--token Gitea API token (default: \$GITEA_TOKEN)
|
|
||||||
--url Gitea base URL (default: \$GITEA_URL)
|
|
||||||
EOF
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--gate) GATE="$2"; shift 2 ;;
|
|
||||||
--details) DETAILS="$2"; shift 2 ;;
|
|
||||||
--severity) SEVERITY="$2"; shift 2 ;;
|
|
||||||
--workflow) WORKFLOW="$2"; shift 2 ;;
|
|
||||||
--repo) REPO="$2"; shift 2 ;;
|
|
||||||
--run-url) RUN_URL="$2"; shift 2 ;;
|
|
||||||
--token) GITEA_TOKEN="$2"; shift 2 ;;
|
|
||||||
--url) GITEA_URL="$2"; shift 2 ;;
|
|
||||||
-h|--help) usage ;;
|
|
||||||
*) echo "Unknown option: $1"; usage ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
[[ -z "$GATE" ]] && { echo "ERROR: --gate is required"; usage; }
|
|
||||||
[[ -z "$DETAILS" ]] && { echo "ERROR: --details is required"; usage; }
|
|
||||||
[[ -z "$GITEA_TOKEN" ]] && { echo "ERROR: GITEA_TOKEN not set"; exit 1; }
|
|
||||||
[[ -z "$REPO" ]] && { echo "ERROR: GITHUB_REPOSITORY not set"; exit 1; }
|
|
||||||
|
|
||||||
API="${GITEA_URL}/api/v1/repos/${REPO}"
|
|
||||||
|
|
||||||
# ── Build title ─────────────────────────────────────────────────────────────
|
|
||||||
if [[ -n "$WORKFLOW" ]]; then
|
|
||||||
TITLE="[CI] ${WORKFLOW}: ${GATE} failed"
|
|
||||||
else
|
|
||||||
TITLE="[CI] ${GATE} failed"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Ensure label exists ─────────────────────────────────────────────────────
|
|
||||||
ensure_label() {
|
|
||||||
local exists
|
|
||||||
exists=$(curl -sf -o /dev/null -w '%{http_code}' \
|
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
"${API}/labels" 2>/dev/null || echo "000")
|
|
||||||
|
|
||||||
if [[ "$exists" == "200" ]]; then
|
|
||||||
# Check if label already exists
|
|
||||||
local found
|
|
||||||
found=$(curl -sf \
|
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
"${API}/labels" 2>/dev/null \
|
|
||||||
| grep -o "\"name\":\"${LABEL_NAME}\"" || true)
|
|
||||||
|
|
||||||
if [[ -z "$found" ]]; then
|
|
||||||
curl -sf -X POST \
|
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"${API}/labels" \
|
|
||||||
-d "{\"name\":\"${LABEL_NAME}\",\"color\":\"${LABEL_COLOR}\",\"description\":\"Auto-created by CI issue reporter\"}" \
|
|
||||||
> /dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Search for existing open issue ──────────────────────────────────────────
|
|
||||||
find_existing_issue() {
|
|
||||||
# URL-encode the gate name for the query
|
|
||||||
local query
|
|
||||||
query=$(printf '%s' "[CI] ${GATE}" | sed 's/ /%20/g; s/\[/%5B/g; s/\]/%5D/g')
|
|
||||||
|
|
||||||
local response
|
|
||||||
response=$(curl -sf \
|
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
"${API}/issues?type=issues&state=open&labels=${LABEL_NAME}&q=${query}&limit=5" \
|
|
||||||
2>/dev/null || echo "[]")
|
|
||||||
|
|
||||||
# Extract the first matching issue number
|
|
||||||
echo "$response" \
|
|
||||||
| grep -oP '"number":\s*\K[0-9]+' \
|
|
||||||
| head -1
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Build issue body ────────────────────────────────────────────────────────
|
|
||||||
build_body() {
|
|
||||||
local severity_badge
|
|
||||||
if [[ "$SEVERITY" == "error" ]]; then
|
|
||||||
severity_badge="**Severity:** Error"
|
|
||||||
else
|
|
||||||
severity_badge="**Severity:** Warning"
|
|
||||||
fi
|
|
||||||
|
|
||||||
cat <<BODY
|
|
||||||
## CI Gate Failure: ${GATE}
|
|
||||||
|
|
||||||
${severity_badge}
|
|
||||||
**Workflow:** ${WORKFLOW:-unknown}
|
|
||||||
**Branch:** ${GITHUB_REF_NAME:-unknown}
|
|
||||||
**Commit:** \`${GITHUB_SHA:0:8}\`
|
|
||||||
**Run:** [View CI run](${RUN_URL})
|
|
||||||
|
|
||||||
### Details
|
|
||||||
|
|
||||||
${DETAILS}
|
|
||||||
|
|
||||||
### Resolution
|
|
||||||
|
|
||||||
Fix the issue described above and push a new commit. This issue will be closed automatically when the gate passes, or can be closed manually.
|
|
||||||
|
|
||||||
---
|
|
||||||
*Auto-created by [ci-issue-reporter](${GITEA_URL}/${REPO}/src/branch/main/automation/ci-issue-reporter.sh)*
|
|
||||||
BODY
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Build comment body (for existing issues) ────────────────────────────────
|
|
||||||
build_comment() {
|
|
||||||
cat <<COMMENT
|
|
||||||
### CI failure recurrence
|
|
||||||
|
|
||||||
**Branch:** ${GITHUB_REF_NAME:-unknown}
|
|
||||||
**Commit:** \`${GITHUB_SHA:0:8}\`
|
|
||||||
**Run:** [View CI run](${RUN_URL})
|
|
||||||
|
|
||||||
${DETAILS}
|
|
||||||
COMMENT
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Main ────────────────────────────────────────────────────────────────────
|
|
||||||
ensure_label
|
|
||||||
|
|
||||||
EXISTING=$(find_existing_issue)
|
|
||||||
|
|
||||||
if [[ -n "$EXISTING" ]]; then
|
|
||||||
# Append comment to existing issue
|
|
||||||
COMMENT_BODY=$(build_comment)
|
|
||||||
COMMENT_JSON=$(printf '%s' "$COMMENT_BODY" | python3 -c "
|
|
||||||
import sys, json
|
|
||||||
print(json.dumps({'body': sys.stdin.read()}))" 2>/dev/null)
|
|
||||||
|
|
||||||
HTTP=$(curl -sf -o /dev/null -w '%{http_code}' -X POST \
|
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"${API}/issues/${EXISTING}/comments" \
|
|
||||||
-d "${COMMENT_JSON}" 2>/dev/null || echo "000")
|
|
||||||
|
|
||||||
if [[ "$HTTP" == "201" ]]; then
|
|
||||||
echo "Commented on existing issue #${EXISTING}"
|
|
||||||
else
|
|
||||||
echo "WARNING: Failed to comment on issue #${EXISTING} (HTTP ${HTTP})"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
# Create new issue
|
|
||||||
ISSUE_BODY=$(build_body)
|
|
||||||
ISSUE_JSON=$(python3 -c "
|
|
||||||
import sys, json
|
|
||||||
body = sys.stdin.read()
|
|
||||||
print(json.dumps({
|
|
||||||
'title': sys.argv[1],
|
|
||||||
'body': body,
|
|
||||||
'labels': []
|
|
||||||
}))" "$TITLE" <<< "$ISSUE_BODY" 2>/dev/null)
|
|
||||||
|
|
||||||
# Create the issue
|
|
||||||
RESPONSE=$(curl -sf -X POST \
|
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"${API}/issues" \
|
|
||||||
-d "${ISSUE_JSON}" 2>/dev/null || echo "{}")
|
|
||||||
|
|
||||||
ISSUE_NUM=$(echo "$RESPONSE" | grep -oP '"number":\s*\K[0-9]+' | head -1)
|
|
||||||
|
|
||||||
if [[ -n "$ISSUE_NUM" ]]; then
|
|
||||||
# Apply label (separate call — more reliable across Gitea versions)
|
|
||||||
LABEL_ID=$(curl -sf \
|
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
"${API}/labels" 2>/dev/null \
|
|
||||||
| grep -oP "\"id\":\s*\K[0-9]+(?=[^}]*\"name\":\s*\"${LABEL_NAME}\")" \
|
|
||||||
| head -1 || true)
|
|
||||||
|
|
||||||
if [[ -n "$LABEL_ID" ]]; then
|
|
||||||
curl -sf -X POST \
|
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"${API}/issues/${ISSUE_NUM}/labels" \
|
|
||||||
-d "{\"labels\":[${LABEL_ID}]}" \
|
|
||||||
> /dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Created issue #${ISSUE_NUM}: ${TITLE}"
|
|
||||||
else
|
|
||||||
echo "WARNING: Failed to create issue"
|
|
||||||
echo "Response: ${RESPONSE}"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# PHPStan configuration for Joomla extension repositories.
|
||||||
|
# Extends the base MokoStandards config and adds Joomla framework class stubs
|
||||||
|
# so PHPStan can resolve Factory, CMSApplication, User, Table, etc.
|
||||||
|
# without requiring a full Joomla installation.
|
||||||
|
|
||||||
|
parameters:
|
||||||
|
level: 5
|
||||||
|
|
||||||
|
paths:
|
||||||
|
- src
|
||||||
|
|
||||||
|
excludePaths:
|
||||||
|
- vendor
|
||||||
|
- node_modules
|
||||||
|
|
||||||
|
# Joomla framework stubs — resolved via the enterprise package from vendor/
|
||||||
|
stubFiles:
|
||||||
|
- vendor/mokoconsulting-tech/enterprise/templates/stubs/joomla.php
|
||||||
|
|
||||||
|
# Suppress errors that are structural in Joomla's service-container architecture
|
||||||
|
ignoreErrors:
|
||||||
|
# Joomla's service-based dependency injection returns mixed from getApplication()
|
||||||
|
- '#Cannot call method .+ on Joomla\\CMS\\Application\\CMSApplication\|null#'
|
||||||
|
# Factory::getX() patterns are safe at runtime even when nullable in stubs
|
||||||
|
- '#Call to static method [a-zA-Z]+\(\) on an interface#'
|
||||||
|
|
||||||
|
reportUnmatchedIgnoredErrors: false
|
||||||
|
checkMissingIterableValueType: false
|
||||||
|
checkGenericClassInNonGenericObjectType: false
|
||||||
@@ -3,6 +3,6 @@
|
|||||||
; License: GPL-3.0-or-later
|
; License: GPL-3.0-or-later
|
||||||
|
|
||||||
PKG_MOKOSUITECROSS="MokoSuiteCross"
|
PKG_MOKOSUITECROSS="MokoSuiteCross"
|
||||||
PKG_MOKOSUITECROSS_DESCRIPTION="Cross-posting Joomla content to social media, email marketing, and chat platforms. Automatically publish articles to Facebook, X/Twitter, LinkedIn, Mastodon, Bluesky, Mailchimp, Telegram, Discord, and Slack."
|
PKG_MOKOSUITECROSS_DESCRIPTION="Cross-post Joomla articles to 38 platforms including Facebook, Instagram, X/Twitter, LinkedIn, Threads, Mastodon, Bluesky, Nostr, TikTok, YouTube, Pinterest, Reddit, Medium, Telegram, Discord, Slack, Teams, Mailchimp, SendGrid, Brevo, and more. Features scheduled posting, template placeholders, UTM tagging, link shortening, caption rotation, and per-article service selection."
|
||||||
PKG_MOKOSUITECROSS_PHP_VERSION_ERROR="MokoSuiteCross requires PHP %s or later."
|
PKG_MOKOSUITECROSS_PHP_VERSION_ERROR="MokoSuiteCross requires PHP %s or later."
|
||||||
PKG_MOKOSUITECROSS_MIGRATION_DETECTED="Perfect Publisher Pro detected! Navigate to Components → MokoSuiteCross → Dashboard to migrate your settings."
|
PKG_MOKOSUITECROSS_MIGRATION_DETECTED="Perfect Publisher Pro detected! Navigate to Components → MokoSuiteCross → Dashboard to migrate your settings."
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<access component="com_mokosuitecross">
|
<access component="com_mokosuitecross">
|
||||||
<section name="component">
|
<section name="component">
|
||||||
|
<!-- Joomla core actions -->
|
||||||
<action name="core.admin" title="JACTION_ADMIN" />
|
<action name="core.admin" title="JACTION_ADMIN" />
|
||||||
<action name="core.options" title="JACTION_OPTIONS" />
|
<action name="core.options" title="JACTION_OPTIONS" />
|
||||||
<action name="core.manage" title="JACTION_MANAGE" />
|
<action name="core.manage" title="JACTION_MANAGE" />
|
||||||
@@ -8,7 +9,18 @@
|
|||||||
<action name="core.delete" title="JACTION_DELETE" />
|
<action name="core.delete" title="JACTION_DELETE" />
|
||||||
<action name="core.edit" title="JACTION_EDIT" />
|
<action name="core.edit" title="JACTION_EDIT" />
|
||||||
<action name="core.edit.state" title="JACTION_EDITSTATE" />
|
<action name="core.edit.state" title="JACTION_EDITSTATE" />
|
||||||
|
<!-- Component-specific actions -->
|
||||||
<action name="mokosuitecross.crosspost" title="COM_MOKOSUITECROSS_ACTION_CROSSPOST" />
|
<action name="mokosuitecross.crosspost" title="COM_MOKOSUITECROSS_ACTION_CROSSPOST" />
|
||||||
|
<action name="mokosuitecross.crosspost.manual" title="COM_MOKOSUITECROSS_ACTION_CROSSPOST_MANUAL" />
|
||||||
|
<action name="mokosuitecross.delete.remote" title="COM_MOKOSUITECROSS_ACTION_DELETE_REMOTE" />
|
||||||
|
<action name="mokosuitecross.services.manage" title="COM_MOKOSUITECROSS_ACTION_SERVICES_MANAGE" />
|
||||||
|
<action name="mokosuitecross.services.credentials" title="COM_MOKOSUITECROSS_ACTION_SERVICES_CREDENTIALS" />
|
||||||
|
<action name="mokosuitecross.templates.manage" title="COM_MOKOSUITECROSS_ACTION_TEMPLATES_MANAGE" />
|
||||||
|
<action name="mokosuitecross.logs.view" title="COM_MOKOSUITECROSS_ACTION_LOGS_VIEW" />
|
||||||
|
<action name="mokosuitecross.logs.purge" title="COM_MOKOSUITECROSS_ACTION_LOGS_PURGE" />
|
||||||
|
<action name="mokosuitecross.queue.manage" title="COM_MOKOSUITECROSS_ACTION_QUEUE_MANAGE" />
|
||||||
|
<action name="mokosuitecross.queue.export" title="COM_MOKOSUITECROSS_ACTION_QUEUE_EXPORT" />
|
||||||
|
<action name="mokosuitecross.dispatch" title="COM_MOKOSUITECROSS_ACTION_DISPATCH" />
|
||||||
<action name="mokosuitecross.migrate" title="COM_MOKOSUITECROSS_ACTION_MIGRATE" />
|
<action name="mokosuitecross.migrate" title="COM_MOKOSUITECROSS_ACTION_MIGRATE" />
|
||||||
</section>
|
</section>
|
||||||
</access>
|
</access>
|
||||||
|
|||||||
@@ -24,6 +24,17 @@
|
|||||||
<option value="0">JNO</option>
|
<option value="0">JNO</option>
|
||||||
</field>
|
</field>
|
||||||
|
|
||||||
|
<field
|
||||||
|
name="delete_on_unpublish"
|
||||||
|
type="radio"
|
||||||
|
label="COM_MOKOSUITECROSS_CONFIG_DELETE_ON_UNPUBLISH"
|
||||||
|
description="COM_MOKOSUITECROSS_CONFIG_DELETE_ON_UNPUBLISH_DESC"
|
||||||
|
default="0"
|
||||||
|
class="btn-group">
|
||||||
|
<option value="1">JYES</option>
|
||||||
|
<option value="0">JNO</option>
|
||||||
|
</field>
|
||||||
|
|
||||||
<field
|
<field
|
||||||
name="retry_max"
|
name="retry_max"
|
||||||
type="number"
|
type="number"
|
||||||
@@ -64,6 +75,51 @@
|
|||||||
/>
|
/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset name="utm" label="COM_MOKOSUITECROSS_CONFIG_UTM">
|
||||||
|
<field
|
||||||
|
name="utm_enabled"
|
||||||
|
type="radio"
|
||||||
|
label="COM_MOKOSUITECROSS_CONFIG_UTM_ENABLED"
|
||||||
|
description="COM_MOKOSUITECROSS_CONFIG_UTM_ENABLED_DESC"
|
||||||
|
default="0"
|
||||||
|
class="btn-group">
|
||||||
|
<option value="1">JYES</option>
|
||||||
|
<option value="0">JNO</option>
|
||||||
|
</field>
|
||||||
|
<field
|
||||||
|
name="utm_source"
|
||||||
|
type="text"
|
||||||
|
label="COM_MOKOSUITECROSS_CONFIG_UTM_SOURCE"
|
||||||
|
description="COM_MOKOSUITECROSS_CONFIG_UTM_SOURCE_DESC"
|
||||||
|
default="{platform}"
|
||||||
|
showon="utm_enabled:1"
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="utm_medium"
|
||||||
|
type="text"
|
||||||
|
label="COM_MOKOSUITECROSS_CONFIG_UTM_MEDIUM"
|
||||||
|
description="COM_MOKOSUITECROSS_CONFIG_UTM_MEDIUM_DESC"
|
||||||
|
default="social"
|
||||||
|
showon="utm_enabled:1"
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="utm_campaign"
|
||||||
|
type="text"
|
||||||
|
label="COM_MOKOSUITECROSS_CONFIG_UTM_CAMPAIGN"
|
||||||
|
description="COM_MOKOSUITECROSS_CONFIG_UTM_CAMPAIGN_DESC"
|
||||||
|
default="mokosuitecross"
|
||||||
|
showon="utm_enabled:1"
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="utm_content"
|
||||||
|
type="text"
|
||||||
|
label="COM_MOKOSUITECROSS_CONFIG_UTM_CONTENT"
|
||||||
|
description="COM_MOKOSUITECROSS_CONFIG_UTM_CONTENT_DESC"
|
||||||
|
hint="Optional"
|
||||||
|
showon="utm_enabled:1"
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
<fieldset name="evergreen" label="COM_MOKOSUITECROSS_CONFIG_EVERGREEN">
|
<fieldset name="evergreen" label="COM_MOKOSUITECROSS_CONFIG_EVERGREEN">
|
||||||
<field
|
<field
|
||||||
name="evergreen_enabled"
|
name="evergreen_enabled"
|
||||||
@@ -143,4 +199,19 @@
|
|||||||
description="COM_MOKOSUITECROSS_CONFIG_CATEGORY_RULES_NOTE_DESC"
|
description="COM_MOKOSUITECROSS_CONFIG_CATEGORY_RULES_NOTE_DESC"
|
||||||
/>
|
/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset
|
||||||
|
name="permissions"
|
||||||
|
label="JCONFIG_PERMISSIONS_LABEL"
|
||||||
|
description="JCONFIG_PERMISSIONS_DESC">
|
||||||
|
<field
|
||||||
|
name="rules"
|
||||||
|
type="rules"
|
||||||
|
label="JCONFIG_PERMISSIONS_LABEL"
|
||||||
|
component="com_mokosuitecross"
|
||||||
|
filter="rules"
|
||||||
|
validate="rules"
|
||||||
|
section="component"
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
</config>
|
</config>
|
||||||
|
|||||||
@@ -5,6 +5,20 @@
|
|||||||
COM_MOKOSUITECROSS="MokoSuiteCross"
|
COM_MOKOSUITECROSS="MokoSuiteCross"
|
||||||
COM_MOKOSUITECROSS_DESCRIPTION="Cross-posting Joomla content to social media, email marketing, and chat platforms"
|
COM_MOKOSUITECROSS_DESCRIPTION="Cross-posting Joomla content to social media, email marketing, and chat platforms"
|
||||||
|
|
||||||
|
; ACL Actions
|
||||||
|
COM_MOKOSUITECROSS_ACTION_CROSSPOST="Cross-Post Articles"
|
||||||
|
COM_MOKOSUITECROSS_ACTION_CROSSPOST_MANUAL="Manually Create Posts"
|
||||||
|
COM_MOKOSUITECROSS_ACTION_DELETE_REMOTE="Delete from Remote Platforms"
|
||||||
|
COM_MOKOSUITECROSS_ACTION_SERVICES_MANAGE="Manage Services"
|
||||||
|
COM_MOKOSUITECROSS_ACTION_SERVICES_CREDENTIALS="View Service Credentials"
|
||||||
|
COM_MOKOSUITECROSS_ACTION_TEMPLATES_MANAGE="Manage Templates"
|
||||||
|
COM_MOKOSUITECROSS_ACTION_LOGS_VIEW="View Activity Logs"
|
||||||
|
COM_MOKOSUITECROSS_ACTION_LOGS_PURGE="Purge Activity Logs"
|
||||||
|
COM_MOKOSUITECROSS_ACTION_QUEUE_MANAGE="Manage Post Queue"
|
||||||
|
COM_MOKOSUITECROSS_ACTION_QUEUE_EXPORT="Export Post Queue"
|
||||||
|
COM_MOKOSUITECROSS_ACTION_DISPATCH="Trigger API Dispatch"
|
||||||
|
COM_MOKOSUITECROSS_ACTION_MIGRATE="Run Migration"
|
||||||
|
|
||||||
; Submenu
|
; Submenu
|
||||||
COM_MOKOSUITECROSS_SUBMENU_DASHBOARD="Dashboard"
|
COM_MOKOSUITECROSS_SUBMENU_DASHBOARD="Dashboard"
|
||||||
COM_MOKOSUITECROSS_SUBMENU_POSTS="Post Queue"
|
COM_MOKOSUITECROSS_SUBMENU_POSTS="Post Queue"
|
||||||
@@ -476,6 +490,19 @@ COM_MOKOSUITECROSS_DASHBOARD_QUEUE_DEPTH_WARNING_TITLE="Large queue backlog"
|
|||||||
COM_MOKOSUITECROSS_DASHBOARD_QUEUE_DEPTH_WARNING="There are %d posts waiting in the queue. Please verify that the Joomla Task Scheduler is running and the MokoSuiteCross scheduled task is enabled in System → Scheduled Tasks."
|
COM_MOKOSUITECROSS_DASHBOARD_QUEUE_DEPTH_WARNING="There are %d posts waiting in the queue. Please verify that the Joomla Task Scheduler is running and the MokoSuiteCross scheduled task is enabled in System → Scheduled Tasks."
|
||||||
|
|
||||||
; First-Publish-Only
|
; First-Publish-Only
|
||||||
|
COM_MOKOSUITECROSS_CONFIG_DELETE_ON_UNPUBLISH="Delete from Platforms on Unpublish"
|
||||||
|
COM_MOKOSUITECROSS_CONFIG_DELETE_ON_UNPUBLISH_DESC="When an article is unpublished or trashed, automatically delete the cross-posted content from remote platforms (where supported)."
|
||||||
|
COM_MOKOSUITECROSS_CONFIG_UTM="UTM Tracking"
|
||||||
|
COM_MOKOSUITECROSS_CONFIG_UTM_ENABLED="Enable UTM Parameters"
|
||||||
|
COM_MOKOSUITECROSS_CONFIG_UTM_ENABLED_DESC="Append UTM tracking parameters to article URLs in cross-posted content for Google Analytics tracking."
|
||||||
|
COM_MOKOSUITECROSS_CONFIG_UTM_SOURCE="UTM Source"
|
||||||
|
COM_MOKOSUITECROSS_CONFIG_UTM_SOURCE_DESC="Value for utm_source. Use {platform} to auto-insert the service type (e.g. facebook, twitter, telegram)."
|
||||||
|
COM_MOKOSUITECROSS_CONFIG_UTM_MEDIUM="UTM Medium"
|
||||||
|
COM_MOKOSUITECROSS_CONFIG_UTM_MEDIUM_DESC="Value for utm_medium. Default: social."
|
||||||
|
COM_MOKOSUITECROSS_CONFIG_UTM_CAMPAIGN="UTM Campaign"
|
||||||
|
COM_MOKOSUITECROSS_CONFIG_UTM_CAMPAIGN_DESC="Value for utm_campaign. Default: mokosuitecross."
|
||||||
|
COM_MOKOSUITECROSS_CONFIG_UTM_CONTENT="UTM Content"
|
||||||
|
COM_MOKOSUITECROSS_CONFIG_UTM_CONTENT_DESC="Optional value for utm_content. Leave empty to omit."
|
||||||
COM_MOKOSUITECROSS_CONFIG_FIRST_PUBLISH_ONLY="First Publish Only"
|
COM_MOKOSUITECROSS_CONFIG_FIRST_PUBLISH_ONLY="First Publish Only"
|
||||||
COM_MOKOSUITECROSS_CONFIG_FIRST_PUBLISH_ONLY_DESC="When enabled, articles are only cross-posted on their first save as published. Subsequent edits to already-published articles will not trigger new cross-posts."
|
COM_MOKOSUITECROSS_CONFIG_FIRST_PUBLISH_ONLY_DESC="When enabled, articles are only cross-posted on their first save as published. Subsequent edits to already-published articles will not trigger new cross-posts."
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<extension type="component" method="upgrade">
|
<extension type="component" method="upgrade">
|
||||||
<name>com_mokosuitecross</name>
|
<name>com_mokosuitecross</name>
|
||||||
<version>01.02.00-rc</version>
|
<version>01.08.37</version>
|
||||||
<creationDate>2026-05-28</creationDate>
|
<creationDate>2026-05-28</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ CREATE TABLE IF NOT EXISTS `#__mokosuitecross_posts` (
|
|||||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
`article_id` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'FK to #__content.id',
|
`article_id` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'FK to #__content.id',
|
||||||
`service_id` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'FK to #__mokosuitecross_services.id',
|
`service_id` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'FK to #__mokosuitecross_services.id',
|
||||||
`status` varchar(20) NOT NULL DEFAULT 'queued' COMMENT 'queued, posting, posted, failed, scheduled',
|
`status` varchar(20) NOT NULL DEFAULT 'queued' COMMENT 'queued, posting, posted, failed, scheduled, deleted',
|
||||||
`message` text NOT NULL COMMENT 'Rendered message sent to platform',
|
`message` text NOT NULL COMMENT 'Rendered message sent to platform',
|
||||||
`platform_post_id` varchar(255) NOT NULL DEFAULT '' COMMENT 'Post ID returned by platform',
|
`platform_post_id` varchar(255) NOT NULL DEFAULT '' COMMENT 'Post ID returned by platform',
|
||||||
`platform_response` text NOT NULL COMMENT 'JSON — full API response from platform',
|
`platform_response` text NOT NULL COMMENT 'JSON — full API response from platform',
|
||||||
@@ -74,25 +74,27 @@ CREATE TABLE IF NOT EXISTS `#__mokosuitecross_logs` (
|
|||||||
-- Insert default templates
|
-- Insert default templates
|
||||||
INSERT INTO `#__mokosuitecross_templates` (`service_type`, `title`, `template_body`, `published`, `ordering`, `created`) VALUES
|
INSERT INTO `#__mokosuitecross_templates` (`service_type`, `title`, `template_body`, `published`, `ordering`, `created`) VALUES
|
||||||
('default', 'Default Template', '{title}\n\n{introtext}\n\n{url}', 1, 1, NOW()),
|
('default', 'Default Template', '{title}\n\n{introtext}\n\n{url}', 1, 1, NOW()),
|
||||||
('twitter', 'Twitter/X Default', '{title}\n\n{url}', 1, 2, NOW()),
|
('twitter', 'Twitter/X Default', '{short}\n\n{url}', 1, 2, NOW()),
|
||||||
('mastodon', 'Mastodon Default', '{title}\n\n{introtext}\n\n{url}\n\n#Joomla', 1, 3, NOW()),
|
('mastodon', 'Mastodon Default', '{social}\n\n{url}\n\n{hashtags}', 1, 3, NOW()),
|
||||||
('mailchimp', 'Mailchimp Default', '<h1>{title}</h1>\n<p>{introtext}</p>\n<p><a href=\"{url}\">Read more</a></p>', 1, 4, NOW()),
|
('mailchimp', 'Mailchimp Default', '<h1>{email_subject}</h1>\n{email_body}\n<p><a href=\"{url}\">Read more</a></p>', 1, 4, NOW()),
|
||||||
('telegram', 'Telegram Default', '<b>{title}</b>\n\n{introtext}\n\n<a href=\"{url}\">Read more</a>', 1, 5, NOW()),
|
('telegram', 'Telegram Default', '<b>{title}</b>\n\n{chat}\n\n<a href=\"{url}\">Read more</a>', 1, 5, NOW()),
|
||||||
('discord', 'Discord Default', '**{title}**\n\n{introtext}\n\n{url}', 1, 6, NOW()),
|
('discord', 'Discord Default', '**{title}**\n\n{chat}\n\n{url}', 1, 6, NOW()),
|
||||||
('slack', 'Slack Default', '*{title}*\n\n{introtext}\n\n{url}', 1, 7, NOW()),
|
('slack', 'Slack Default', '*{title}*\n\n{chat}\n\n{url}', 1, 7, NOW()),
|
||||||
('facebook', 'Facebook Default', '{title}\n\n{introtext}\n\n{url}', 1, 8, NOW()),
|
('facebook', 'Facebook Default', '{social}\n\n{url}', 1, 8, NOW()),
|
||||||
('linkedin', 'LinkedIn Default', '{title}\n\n{introtext}\n\n{url}', 1, 9, NOW()),
|
('linkedin', 'LinkedIn Default', '{social}\n\n{url}\n\n{hashtags}', 1, 9, NOW()),
|
||||||
('bluesky', 'Bluesky Default', '{title}\n\n{url}', 1, 10, NOW()),
|
('bluesky', 'Bluesky Default', '{short}\n\n{url}', 1, 10, NOW()),
|
||||||
('threads', 'Threads Default', '{title}\n\n{introtext}\n\n{url}', 1, 11, NOW()),
|
('threads', 'Threads Default', '{social}\n\n{url}', 1, 11, NOW()),
|
||||||
('teams', 'Teams Default', '**{title}**\n\n{introtext}\n\n[Read more]({url})', 1, 12, NOW()),
|
('teams', 'Teams Default', '**{title}**\n\n{chat}\n\n[Read more]({url})', 1, 12, NOW()),
|
||||||
('medium', 'Medium Default', '{title}\n\n{introtext}\n\n{url}', 1, 13, NOW()),
|
('medium', 'Medium Default', '{title}\n\n{social}\n\n{url}', 1, 13, NOW()),
|
||||||
('wordpress', 'WordPress Default', '{title}\n\n{introtext}\n\n{url}', 1, 14, NOW()),
|
('wordpress', 'WordPress Default', '{title}\n\n{introtext}\n\n{url}', 1, 14, NOW()),
|
||||||
('webhook', 'Webhook Default', '{title}\n\n{introtext}\n\n{url}', 1, 15, NOW()),
|
('webhook', 'Webhook Default', '{title}\n\n{introtext}\n\n{url}', 1, 15, NOW()),
|
||||||
('sendgrid', 'SendGrid Default', '<h1>{title}</h1>\n<p>{introtext}</p>\n<p><a href=\"{url}\">Read more</a></p>', 1, 16, NOW()),
|
('sendgrid', 'SendGrid Default', '<h1>{email_subject}</h1>\n{email_body}\n<p><a href=\"{url}\">Read more</a></p>', 1, 16, NOW()),
|
||||||
('brevo', 'Brevo Default', '<h1>{title}</h1>\n<p>{introtext}</p>\n<p><a href=\"{url}\">Read more</a></p>', 1, 17, NOW()),
|
('brevo', 'Brevo Default', '<h1>{email_subject}</h1>\n{email_body}\n<p><a href=\"{url}\">Read more</a></p>', 1, 17, NOW()),
|
||||||
('ntfy', 'Ntfy Default', '{title}: {introtext}', 1, 18, NOW()),
|
('ntfy', 'Ntfy Default', '{title}: {short}', 1, 18, NOW()),
|
||||||
('reddit', 'Reddit Default', '{title}', 1, 19, NOW()),
|
('reddit', 'Reddit Default', '{title}', 1, 19, NOW()),
|
||||||
('pinterest', 'Pinterest Default', '{title} - {introtext}', 1, 20, NOW());
|
('pinterest', 'Pinterest Default', '{title} - {social}', 1, 20, NOW()),
|
||||||
|
('instagram', 'Instagram Default', '{social}\n\n{hashtags}', 1, 21, NOW()),
|
||||||
|
('youtube', 'YouTube Default', '{social}\n\n{url}', 1, 22, NOW());
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS `#__mokosuitecross_category_rules` (
|
CREATE TABLE IF NOT EXISTS `#__mokosuitecross_category_rules` (
|
||||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.08.05 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.08.07 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.08.08 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.08.09 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.08.10 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.08.11 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.08.12 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.08.13 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.08.14 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.08.15 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.08.16 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.08.17 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.08.19 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.08.20 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.08.21 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.08.22 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.08.23 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.08.24 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.08.25 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.08.26 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.08.27 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.08.28 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.08.29 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.08.30 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.08.31 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.08.32 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.08.33 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.08.34 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.08.35 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.08.36 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.08.37 — no schema changes */
|
||||||
@@ -56,7 +56,7 @@ class DispatchController extends BaseController
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ACL check — require core.manage on the component
|
// ACL check — require core.manage on the component
|
||||||
if (!Factory::getApplication()->getIdentity()->authorise('core.manage', 'com_mokosuitecross')) {
|
if (!Factory::getApplication()->getIdentity()->authorise('mokosuitecross.dispatch', 'com_mokosuitecross')) {
|
||||||
$this->sendJsonResponse(['error' => 'Forbidden'], 403);
|
$this->sendJsonResponse(['error' => 'Forbidden'], 403);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ namespace Joomla\Component\MokoSuiteCross\Administrator\Controller;
|
|||||||
|
|
||||||
defined('_JEXEC') or die;
|
defined('_JEXEC') or die;
|
||||||
|
|
||||||
|
use Joomla\CMS\Language\Text;
|
||||||
use Joomla\CMS\MVC\Controller\BaseController;
|
use Joomla\CMS\MVC\Controller\BaseController;
|
||||||
|
|
||||||
class DisplayController extends BaseController
|
class DisplayController extends BaseController
|
||||||
@@ -23,4 +24,13 @@ class DisplayController extends BaseController
|
|||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
protected $default_view = 'dashboard';
|
protected $default_view = 'dashboard';
|
||||||
|
|
||||||
|
public function display($cachable = false, $urlparams = [])
|
||||||
|
{
|
||||||
|
if (!$this->app->getIdentity()->authorise('core.manage', 'com_mokosuitecross')) {
|
||||||
|
throw new \Joomla\CMS\Access\Exception\NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent::display($cachable, $urlparams);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ class PostsController extends AdminController
|
|||||||
{
|
{
|
||||||
$this->checkToken('get');
|
$this->checkToken('get');
|
||||||
|
|
||||||
if (!$this->app->getIdentity()->authorise('core.manage', 'com_mokosuitecross')) {
|
if (!$this->app->getIdentity()->authorise('mokosuitecross.queue.export', 'com_mokosuitecross')) {
|
||||||
throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403);
|
throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class ServiceController extends FormController
|
|||||||
{
|
{
|
||||||
$this->checkToken();
|
$this->checkToken();
|
||||||
|
|
||||||
if (!$this->app->getIdentity()->authorise('core.manage', 'com_mokosuitecross')) {
|
if (!$this->app->getIdentity()->authorise('mokosuitecross.services.manage', 'com_mokosuitecross')) {
|
||||||
throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403);
|
throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +96,7 @@ class ServiceController extends FormController
|
|||||||
$app->mimeType = 'application/json';
|
$app->mimeType = 'application/json';
|
||||||
$app->setHeader('Content-Type', 'application/json; charset=utf-8');
|
$app->setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||||
|
|
||||||
echo new JsonResponse($e);
|
echo new JsonResponse(['error' => $e->getMessage()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$app->close();
|
$app->close();
|
||||||
|
|||||||
@@ -21,4 +21,22 @@ class ServicesController extends AdminController
|
|||||||
{
|
{
|
||||||
return parent::getModel($name, $prefix, $config);
|
return parent::getModel($name, $prefix, $config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function publish(): void
|
||||||
|
{
|
||||||
|
if (!$this->app->getIdentity()->authorise('core.edit.state', 'com_mokosuitecross')) {
|
||||||
|
throw new \Joomla\CMS\Access\Exception\NotAllowed(\Joomla\CMS\Language\Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
parent::publish();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(): void
|
||||||
|
{
|
||||||
|
if (!$this->app->getIdentity()->authorise('core.delete', 'com_mokosuitecross')) {
|
||||||
|
throw new \Joomla\CMS\Access\Exception\NotAllowed(\Joomla\CMS\Language\Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
parent::delete();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,4 +21,22 @@ class TemplatesController extends AdminController
|
|||||||
{
|
{
|
||||||
return parent::getModel($name, $prefix, $config);
|
return parent::getModel($name, $prefix, $config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function publish(): void
|
||||||
|
{
|
||||||
|
if (!$this->app->getIdentity()->authorise('mokosuitecross.templates.manage', 'com_mokosuitecross')) {
|
||||||
|
throw new \Joomla\CMS\Access\Exception\NotAllowed(\Joomla\CMS\Language\Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
parent::publish();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(): void
|
||||||
|
{
|
||||||
|
if (!$this->app->getIdentity()->authorise('mokosuitecross.templates.manage', 'com_mokosuitecross')) {
|
||||||
|
throw new \Joomla\CMS\Access\Exception\NotAllowed(\Joomla\CMS\Language\Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
parent::delete();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ use Joomla\CMS\Component\ComponentHelper;
|
|||||||
use Joomla\CMS\Factory;
|
use Joomla\CMS\Factory;
|
||||||
use Joomla\CMS\Plugin\PluginHelper;
|
use Joomla\CMS\Plugin\PluginHelper;
|
||||||
use Joomla\CMS\Uri\Uri;
|
use Joomla\CMS\Uri\Uri;
|
||||||
|
use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossDeleteInterface;
|
||||||
use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface;
|
use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -243,7 +244,21 @@ class CrossPostDispatcher
|
|||||||
$params = json_decode($service->params ?: '{}', true) ?: [];
|
$params = json_decode($service->params ?: '{}', true) ?: [];
|
||||||
|
|
||||||
if (!empty($articleUrl)) {
|
if (!empty($articleUrl)) {
|
||||||
$params['_article_url'] = $articleUrl;
|
$params['article_url'] = $articleUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass article title for platforms that need it (e.g. Bluesky link cards)
|
||||||
|
$db2 = Factory::getDbo();
|
||||||
|
$postRow = $db2->setQuery(
|
||||||
|
$db2->getQuery(true)->select('article_id')->from('#__mokosuitecross_posts')->where('id = ' . $postId)
|
||||||
|
)->loadObject();
|
||||||
|
if ($postRow && $postRow->article_id) {
|
||||||
|
$articleTitle = $db2->setQuery(
|
||||||
|
$db2->getQuery(true)->select('title')->from('#__content')->where('id = ' . (int) $postRow->article_id)
|
||||||
|
)->loadResult();
|
||||||
|
if ($articleTitle) {
|
||||||
|
$params['article_title'] = $articleTitle;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lifecycle event: before post
|
// Lifecycle event: before post
|
||||||
@@ -383,12 +398,34 @@ class CrossPostDispatcher
|
|||||||
$authorName = $db->loadResult() ?: '';
|
$authorName = $db->loadResult() ?: '';
|
||||||
}
|
}
|
||||||
|
|
||||||
$introImage = '';
|
// Resolve share image from article attribs
|
||||||
|
$attribs = json_decode($article->attribs ?? '{}', true) ?: [];
|
||||||
|
$imageMode = $attribs['mokosuitecross_share_image'] ?? 'intro';
|
||||||
$images = json_decode($article->images ?? '{}');
|
$images = json_decode($article->images ?? '{}');
|
||||||
|
$introImage = '';
|
||||||
|
|
||||||
|
switch ($imageMode) {
|
||||||
|
case 'fulltext':
|
||||||
|
if (!empty($images->image_fulltext)) {
|
||||||
|
$introImage = Uri::root() . ltrim($images->image_fulltext, '/');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'custom':
|
||||||
|
$customImg = $attribs['mokosuitecross_custom_image'] ?? '';
|
||||||
|
if (!empty($customImg)) {
|
||||||
|
$introImage = Uri::root() . ltrim($customImg, '/');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'none':
|
||||||
|
$introImage = '';
|
||||||
|
break;
|
||||||
|
case 'intro':
|
||||||
|
default:
|
||||||
if (!empty($images->image_intro)) {
|
if (!empty($images->image_intro)) {
|
||||||
$introImage = Uri::root() . ltrim($images->image_intro, '/');
|
$introImage = Uri::root() . ltrim($images->image_intro, '/');
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
$tagNames = [];
|
$tagNames = [];
|
||||||
|
|
||||||
@@ -410,17 +447,54 @@ class CrossPostDispatcher
|
|||||||
return '#' . preg_replace('/\s+/', '', $tag);
|
return '#' . preg_replace('/\s+/', '', $tag);
|
||||||
}, $tagNames));
|
}, $tagNames));
|
||||||
|
|
||||||
|
// Per-article share text (from article editor Share Content panel)
|
||||||
|
$socialText = $attribs['mokosuitecross_social_text'] ?? '';
|
||||||
|
$shortText = $attribs['mokosuitecross_short_text'] ?? '';
|
||||||
|
$chatText = $attribs['mokosuitecross_chat_text'] ?? '';
|
||||||
|
$emailSubject = $attribs['mokosuitecross_email_subject'] ?? '';
|
||||||
|
$emailBody = $attribs['mokosuitecross_email_body'] ?? '';
|
||||||
|
|
||||||
|
$introStripped = strip_tags(mb_substr($article->introtext ?? '', 0, 280));
|
||||||
|
$titleText = $article->title ?? '';
|
||||||
|
|
||||||
|
// UTM auto-tagging (#154)
|
||||||
|
$componentParams = ComponentHelper::getParams('com_mokosuitecross');
|
||||||
|
$urlRaw = $url;
|
||||||
|
|
||||||
|
if ($componentParams->get('utm_enabled', 0)) {
|
||||||
|
$utmParams = [
|
||||||
|
'utm_source' => $componentParams->get('utm_source', '{platform}'),
|
||||||
|
'utm_medium' => $componentParams->get('utm_medium', 'social'),
|
||||||
|
'utm_campaign' => $componentParams->get('utm_campaign', 'mokosuitecross'),
|
||||||
|
];
|
||||||
|
$utmContent = $componentParams->get('utm_content', '');
|
||||||
|
|
||||||
|
if (!empty($utmContent)) {
|
||||||
|
$utmParams['utm_content'] = $utmContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
$separator = (strpos($url, '?') !== false) ? '&' : '?';
|
||||||
|
$url = $url . $separator . http_build_query($utmParams);
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'{title}' => $article->title ?? '',
|
'{title}' => $titleText,
|
||||||
'{introtext}' => strip_tags(mb_substr($article->introtext ?? '', 0, 280)),
|
'{introtext}' => $introStripped,
|
||||||
'{fulltext}' => strip_tags(mb_substr($article->fulltext ?? '', 0, 500)),
|
'{fulltext}' => strip_tags(mb_substr($article->fulltext ?? '', 0, 500)),
|
||||||
'{url}' => $url,
|
'{url}' => $url,
|
||||||
|
'{url_raw}' => $urlRaw,
|
||||||
'{image}' => $introImage,
|
'{image}' => $introImage,
|
||||||
'{category}' => $categoryName,
|
'{category}' => $categoryName,
|
||||||
'{author}' => $authorName,
|
'{author}' => $authorName,
|
||||||
'{date}' => Factory::getDate($article->publish_up ?? 'now')->format('Y-m-d'),
|
'{date}' => Factory::getDate($article->publish_up ?? 'now')->format('Y-m-d'),
|
||||||
'{tags}' => $tagsComma,
|
'{tags}' => $tagsComma,
|
||||||
'{hashtags}' => $hashtags,
|
'{hashtags}' => $hashtags,
|
||||||
|
// Platform-specific share content (falls back to introtext/title if empty)
|
||||||
|
'{social}' => !empty($socialText) ? $socialText : $introStripped,
|
||||||
|
'{short}' => !empty($shortText) ? $shortText : mb_substr($titleText, 0, 250),
|
||||||
|
'{chat}' => !empty($chatText) ? $chatText : $introStripped,
|
||||||
|
'{email_subject}' => !empty($emailSubject) ? $emailSubject : $titleText,
|
||||||
|
'{email_body}' => !empty($emailBody) ? $emailBody : ($article->fulltext ?? $article->introtext ?? ''),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -459,6 +533,15 @@ class CrossPostDispatcher
|
|||||||
|
|
||||||
$message = str_replace(array_keys($replacements), array_values($replacements), $template);
|
$message = str_replace(array_keys($replacements), array_values($replacements), $template);
|
||||||
|
|
||||||
|
// Resolve {platform} token in UTM params (replaced with service_type)
|
||||||
|
$message = str_replace('{platform}', $service->service_type, $message);
|
||||||
|
|
||||||
|
// Resolve caption rotation: {random:option1|option2|option3} (#155)
|
||||||
|
$message = preg_replace_callback('/\{random:([^}]+)\}/', function ($matches) {
|
||||||
|
$options = explode('|', $matches[1]);
|
||||||
|
return $options[array_rand($options)];
|
||||||
|
}, $message);
|
||||||
|
|
||||||
// Resolve custom field placeholders: {field:field_name}
|
// Resolve custom field placeholders: {field:field_name}
|
||||||
$message = preg_replace_callback('/\{field:([a-zA-Z0-9_-]+)\}/', function ($matches) use ($db, $article) {
|
$message = preg_replace_callback('/\{field:([a-zA-Z0-9_-]+)\}/', function ($matches) use ($db, $article) {
|
||||||
$fieldName = $matches[1];
|
$fieldName = $matches[1];
|
||||||
@@ -478,6 +561,82 @@ class CrossPostDispatcher
|
|||||||
/**
|
/**
|
||||||
* Write an entry to the activity log.
|
* Write an entry to the activity log.
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Delete cross-posted content from remote platforms for a given article.
|
||||||
|
*
|
||||||
|
* Finds all posts with status 'posted' for this article, resolves the
|
||||||
|
* service plugin, and calls deletePost() if the plugin supports it.
|
||||||
|
*
|
||||||
|
* @param int $articleId The Joomla article ID
|
||||||
|
*/
|
||||||
|
public static function deleteFromPlatforms(int $articleId): void
|
||||||
|
{
|
||||||
|
$db = Factory::getDbo();
|
||||||
|
|
||||||
|
// Find all successfully posted entries for this article
|
||||||
|
$query = $db->getQuery(true)
|
||||||
|
->select('p.*, s.service_type, s.credentials')
|
||||||
|
->from($db->quoteName('#__mokosuitecross_posts', 'p'))
|
||||||
|
->join('INNER', $db->quoteName('#__mokosuitecross_services', 's')
|
||||||
|
. ' ON ' . $db->quoteName('s.id') . ' = ' . $db->quoteName('p.service_id'))
|
||||||
|
->where($db->quoteName('p.article_id') . ' = ' . $articleId)
|
||||||
|
->where($db->quoteName('p.status') . ' = ' . $db->quote('posted'))
|
||||||
|
->where($db->quoteName('p.platform_post_id') . ' != ' . $db->quote(''));
|
||||||
|
|
||||||
|
$db->setQuery($query);
|
||||||
|
$posts = $db->loadObjectList();
|
||||||
|
|
||||||
|
if (empty($posts)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load service plugins
|
||||||
|
PluginHelper::importPlugin('mokosuitecross');
|
||||||
|
$plugins = [];
|
||||||
|
Factory::getApplication()->triggerEvent('onMokoSuiteCrossGetServices', [&$plugins]);
|
||||||
|
|
||||||
|
$pluginMap = [];
|
||||||
|
foreach ($plugins as $plugin) {
|
||||||
|
$pluginMap[$plugin->getServiceType()] = $plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($posts as $post) {
|
||||||
|
$plugin = $pluginMap[$post->service_type] ?? null;
|
||||||
|
|
||||||
|
if (!$plugin instanceof MokoSuiteCrossDeleteInterface) {
|
||||||
|
self::log($db, $post->id, $post->service_id, 'info',
|
||||||
|
'Delete not supported for ' . $post->service_type);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$credentials = json_decode($post->credentials, true) ?: [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$result = $plugin->deletePost($post->platform_post_id, $credentials);
|
||||||
|
|
||||||
|
if (!empty($result['success'])) {
|
||||||
|
// Mark as deleted
|
||||||
|
$db->setQuery(
|
||||||
|
$db->getQuery(true)
|
||||||
|
->update($db->quoteName('#__mokosuitecross_posts'))
|
||||||
|
->set($db->quoteName('status') . ' = ' . $db->quote('deleted'))
|
||||||
|
->where($db->quoteName('id') . ' = ' . (int) $post->id)
|
||||||
|
);
|
||||||
|
$db->execute();
|
||||||
|
|
||||||
|
self::log($db, $post->id, $post->service_id, 'info',
|
||||||
|
'Deleted from ' . $post->service_type . ': ' . ($result['message'] ?? 'OK'));
|
||||||
|
} else {
|
||||||
|
self::log($db, $post->id, $post->service_id, 'warning',
|
||||||
|
'Delete failed on ' . $post->service_type . ': ' . ($result['message'] ?? 'Unknown error'));
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
self::log($db, $post->id, $post->service_id, 'error',
|
||||||
|
'Delete exception on ' . $post->service_type . ': ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static function log($db, ?int $postId, ?int $serviceId, string $level, string $message): void
|
private static function log($db, ?int $postId, ?int $serviceId, string $level, string $message): void
|
||||||
{
|
{
|
||||||
$log = (object) [
|
$log = (object) [
|
||||||
|
|||||||
@@ -71,4 +71,26 @@ class MokoSuiteCrossHelper
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a list of ACL actions for the component.
|
||||||
|
*
|
||||||
|
* @return \Joomla\CMS\Object\CMSObject
|
||||||
|
*/
|
||||||
|
public static function getActions(): \Joomla\CMS\Object\CMSObject
|
||||||
|
{
|
||||||
|
$user = \Joomla\CMS\Factory::getApplication()->getIdentity();
|
||||||
|
$result = new \Joomla\CMS\Object\CMSObject();
|
||||||
|
|
||||||
|
$actions = \Joomla\CMS\Access\Access::getActionsFromFile(
|
||||||
|
JPATH_ADMINISTRATOR . '/components/com_mokosuitecross/access.xml',
|
||||||
|
'/access/section[@name="component"]/'
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($actions as $action) {
|
||||||
|
$result->set($action->name, $user->authorise($action->name, 'com_mokosuitecross'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package MokoSuiteCross
|
||||||
|
* @subpackage com_mokosuitecross
|
||||||
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
|
* @license GNU General Public License version 3 or later; see LICENSE
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Joomla\Component\MokoSuiteCross\Administrator\Service;
|
||||||
|
|
||||||
|
defined('_JEXEC') or die;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional interface for service plugins that support deleting posts
|
||||||
|
* from the remote platform.
|
||||||
|
*
|
||||||
|
* Plugins that implement this can be invoked when a Joomla article
|
||||||
|
* is unpublished or trashed, or when a user manually requests deletion
|
||||||
|
* from the Post Queue view.
|
||||||
|
*/
|
||||||
|
interface MokoSuiteCrossDeleteInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Delete a previously published post from the remote platform.
|
||||||
|
*
|
||||||
|
* @param string $platformPostId The platform-specific post ID
|
||||||
|
* @param array $credentials Decrypted credentials for this service
|
||||||
|
*
|
||||||
|
* @return array ['success' => bool, 'message' => string]
|
||||||
|
*/
|
||||||
|
public function deletePost(string $platformPostId, array $credentials): array;
|
||||||
|
}
|
||||||
@@ -65,7 +65,11 @@ class HtmlView extends BaseHtmlView
|
|||||||
|
|
||||||
protected function addToolbar(): void
|
protected function addToolbar(): void
|
||||||
{
|
{
|
||||||
|
$canDo = MokoSuiteCrossHelper::getActions();
|
||||||
|
|
||||||
ToolbarHelper::title('MokoSuiteCross — Dashboard', 'share-alt');
|
ToolbarHelper::title('MokoSuiteCross — Dashboard', 'share-alt');
|
||||||
|
if ($canDo->get('core.admin')) {
|
||||||
ToolbarHelper::preferences('com_mokosuitecross');
|
ToolbarHelper::preferences('com_mokosuitecross');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ class HtmlView extends BaseHtmlView
|
|||||||
|
|
||||||
protected function addToolbar(): void
|
protected function addToolbar(): void
|
||||||
{
|
{
|
||||||
|
$canDo = MokoSuiteCrossHelper::getActions();
|
||||||
|
|
||||||
ToolbarHelper::title('MokoSuiteCross — Activity Logs', 'share-alt');
|
ToolbarHelper::title('MokoSuiteCross — Activity Logs', 'share-alt');
|
||||||
ToolbarHelper::deleteList('JGLOBAL_CONFIRM_DELETE', 'logs.delete', 'JTOOLBAR_DELETE');
|
ToolbarHelper::deleteList('JGLOBAL_CONFIRM_DELETE', 'logs.delete', 'JTOOLBAR_DELETE');
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
|||||||
use Joomla\CMS\Router\Route;
|
use Joomla\CMS\Router\Route;
|
||||||
use Joomla\CMS\Toolbar\Toolbar;
|
use Joomla\CMS\Toolbar\Toolbar;
|
||||||
use Joomla\CMS\Toolbar\ToolbarHelper;
|
use Joomla\CMS\Toolbar\ToolbarHelper;
|
||||||
|
use Joomla\Component\MokoSuiteCross\Administrator\Helper\MokoSuiteCrossHelper;
|
||||||
|
|
||||||
class HtmlView extends BaseHtmlView
|
class HtmlView extends BaseHtmlView
|
||||||
{
|
{
|
||||||
@@ -37,14 +38,17 @@ class HtmlView extends BaseHtmlView
|
|||||||
protected function addToolbar(): void
|
protected function addToolbar(): void
|
||||||
{
|
{
|
||||||
$isNew = empty($this->item->id);
|
$isNew = empty($this->item->id);
|
||||||
|
$canDo = MokoSuiteCrossHelper::getActions();
|
||||||
|
|
||||||
ToolbarHelper::title(
|
ToolbarHelper::title(
|
||||||
'MokoSuiteCross — ' . ($isNew ? Text::_('COM_MOKOSUITECROSS_NEW_POST') : Text::_('COM_MOKOSUITECROSS_EDIT_POST')),
|
'MokoSuiteCross — ' . ($isNew ? Text::_('COM_MOKOSUITECROSS_NEW_POST') : Text::_('COM_MOKOSUITECROSS_EDIT_POST')),
|
||||||
'share-alt'
|
'share-alt'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if ($canDo->get('mokosuitecross.queue.manage')) {
|
||||||
ToolbarHelper::apply('post.apply');
|
ToolbarHelper::apply('post.apply');
|
||||||
ToolbarHelper::save('post.save');
|
ToolbarHelper::save('post.save');
|
||||||
|
}
|
||||||
|
|
||||||
$toolbar = Toolbar::getInstance('toolbar');
|
$toolbar = Toolbar::getInstance('toolbar');
|
||||||
$toolbar->appendButton(
|
$toolbar->appendButton(
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ class HtmlView extends BaseHtmlView
|
|||||||
|
|
||||||
protected function addToolbar(): void
|
protected function addToolbar(): void
|
||||||
{
|
{
|
||||||
|
$canDo = MokoSuiteCrossHelper::getActions();
|
||||||
|
|
||||||
ToolbarHelper::title('MokoSuiteCross — Post Queue', 'share-alt');
|
ToolbarHelper::title('MokoSuiteCross — Post Queue', 'share-alt');
|
||||||
ToolbarHelper::addNew('post.add');
|
ToolbarHelper::addNew('post.add');
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
|||||||
use Joomla\CMS\Router\Route;
|
use Joomla\CMS\Router\Route;
|
||||||
use Joomla\CMS\Toolbar\Toolbar;
|
use Joomla\CMS\Toolbar\Toolbar;
|
||||||
use Joomla\CMS\Toolbar\ToolbarHelper;
|
use Joomla\CMS\Toolbar\ToolbarHelper;
|
||||||
|
use Joomla\Component\MokoSuiteCross\Administrator\Helper\MokoSuiteCrossHelper;
|
||||||
|
|
||||||
class HtmlView extends BaseHtmlView
|
class HtmlView extends BaseHtmlView
|
||||||
{
|
{
|
||||||
@@ -38,14 +39,17 @@ class HtmlView extends BaseHtmlView
|
|||||||
protected function addToolbar(): void
|
protected function addToolbar(): void
|
||||||
{
|
{
|
||||||
$isNew = empty($this->item->id);
|
$isNew = empty($this->item->id);
|
||||||
|
$canDo = MokoSuiteCrossHelper::getActions();
|
||||||
|
|
||||||
ToolbarHelper::title(
|
ToolbarHelper::title(
|
||||||
'MokoSuiteCross — ' . ($isNew ? Text::_('COM_MOKOSUITECROSS_NEW_SERVICE') : Text::_('COM_MOKOSUITECROSS_EDIT_SERVICE')),
|
'MokoSuiteCross — ' . ($isNew ? Text::_('COM_MOKOSUITECROSS_NEW_SERVICE') : Text::_('COM_MOKOSUITECROSS_EDIT_SERVICE')),
|
||||||
'share-alt'
|
'share-alt'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if ($canDo->get('mokosuitecross.services.manage')) {
|
||||||
ToolbarHelper::apply('service.apply');
|
ToolbarHelper::apply('service.apply');
|
||||||
ToolbarHelper::save('service.save');
|
ToolbarHelper::save('service.save');
|
||||||
|
}
|
||||||
|
|
||||||
// Dashboard button in toolbar
|
// Dashboard button in toolbar
|
||||||
$toolbar = Toolbar::getInstance('toolbar');
|
$toolbar = Toolbar::getInstance('toolbar');
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ class HtmlView extends BaseHtmlView
|
|||||||
|
|
||||||
protected function addToolbar(): void
|
protected function addToolbar(): void
|
||||||
{
|
{
|
||||||
|
$canDo = MokoSuiteCrossHelper::getActions();
|
||||||
|
|
||||||
ToolbarHelper::title('MokoSuiteCross — Services', 'share-alt');
|
ToolbarHelper::title('MokoSuiteCross — Services', 'share-alt');
|
||||||
ToolbarHelper::addNew('service.add');
|
ToolbarHelper::addNew('service.add');
|
||||||
ToolbarHelper::editList('service.edit');
|
ToolbarHelper::editList('service.edit');
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
|||||||
use Joomla\CMS\Router\Route;
|
use Joomla\CMS\Router\Route;
|
||||||
use Joomla\CMS\Toolbar\Toolbar;
|
use Joomla\CMS\Toolbar\Toolbar;
|
||||||
use Joomla\CMS\Toolbar\ToolbarHelper;
|
use Joomla\CMS\Toolbar\ToolbarHelper;
|
||||||
|
use Joomla\Component\MokoSuiteCross\Administrator\Helper\MokoSuiteCrossHelper;
|
||||||
|
|
||||||
class HtmlView extends BaseHtmlView
|
class HtmlView extends BaseHtmlView
|
||||||
{
|
{
|
||||||
@@ -36,13 +37,17 @@ class HtmlView extends BaseHtmlView
|
|||||||
protected function addToolbar(): void
|
protected function addToolbar(): void
|
||||||
{
|
{
|
||||||
$isNew = empty($this->item->id);
|
$isNew = empty($this->item->id);
|
||||||
|
$canDo = MokoSuiteCrossHelper::getActions();
|
||||||
|
|
||||||
ToolbarHelper::title(
|
ToolbarHelper::title(
|
||||||
'MokoSuiteCross — ' . ($isNew ? 'New Template' : 'Edit Template'),
|
'MokoSuiteCross — ' . ($isNew ? 'New Template' : 'Edit Template'),
|
||||||
'share-alt'
|
'share-alt'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if ($canDo->get('mokosuitecross.templates.manage')) {
|
||||||
ToolbarHelper::apply('template.apply');
|
ToolbarHelper::apply('template.apply');
|
||||||
ToolbarHelper::save('template.save');
|
ToolbarHelper::save('template.save');
|
||||||
|
}
|
||||||
ToolbarHelper::cancel('template.cancel');
|
ToolbarHelper::cancel('template.cancel');
|
||||||
|
|
||||||
// Dashboard link in toolbar
|
// Dashboard link in toolbar
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ class HtmlView extends BaseHtmlView
|
|||||||
|
|
||||||
protected function addToolbar(): void
|
protected function addToolbar(): void
|
||||||
{
|
{
|
||||||
|
$canDo = MokoSuiteCrossHelper::getActions();
|
||||||
|
|
||||||
ToolbarHelper::title('MokoSuiteCross — Message Templates', 'share-alt');
|
ToolbarHelper::title('MokoSuiteCross — Message Templates', 'share-alt');
|
||||||
ToolbarHelper::addNew('template.add');
|
ToolbarHelper::addNew('template.add');
|
||||||
ToolbarHelper::editList('template.edit');
|
ToolbarHelper::editList('template.edit');
|
||||||
|
|||||||
+20
@@ -11,3 +11,23 @@ PLG_CONTENT_MOKOSUITECROSS_EVERGREEN_DESC="Automatically re-share this article o
|
|||||||
PLG_CONTENT_MOKOSUITECROSS_EVERGREEN_INTERVAL="Re-share Interval (days)"
|
PLG_CONTENT_MOKOSUITECROSS_EVERGREEN_INTERVAL="Re-share Interval (days)"
|
||||||
PLG_CONTENT_MOKOSUITECROSS_EVERGREEN_INTERVAL_DESC="How many days to wait between automatic re-shares. Default: 30 days."
|
PLG_CONTENT_MOKOSUITECROSS_EVERGREEN_INTERVAL_DESC="How many days to wait between automatic re-shares. Default: 30 days."
|
||||||
PLG_CONTENT_MOKOSUITECROSS_HISTORY="Cross-Post History"
|
PLG_CONTENT_MOKOSUITECROSS_HISTORY="Cross-Post History"
|
||||||
|
|
||||||
|
PLG_CONTENT_MOKOSUITECROSS_FIELDSET_SHARE="Share Content"
|
||||||
|
PLG_CONTENT_MOKOSUITECROSS_SOCIAL_TEXT="Social Media Text"
|
||||||
|
PLG_CONTENT_MOKOSUITECROSS_SOCIAL_TEXT_DESC="Custom text for Facebook, LinkedIn, Threads. Use {social} placeholder in templates. Falls back to intro text if empty."
|
||||||
|
PLG_CONTENT_MOKOSUITECROSS_SHORT_TEXT="Short Text (Twitter/Bluesky)"
|
||||||
|
PLG_CONTENT_MOKOSUITECROSS_SHORT_TEXT_DESC="Optimized text for character-limited platforms (Twitter 280, Bluesky 300). Use {short} placeholder. Falls back to truncated title."
|
||||||
|
PLG_CONTENT_MOKOSUITECROSS_CHAT_TEXT="Chat Text"
|
||||||
|
PLG_CONTENT_MOKOSUITECROSS_CHAT_TEXT_DESC="Custom text for Telegram, Discord, Slack, Teams. Use {chat} placeholder. Falls back to intro text."
|
||||||
|
PLG_CONTENT_MOKOSUITECROSS_EMAIL_SUBJECT="Email Subject"
|
||||||
|
PLG_CONTENT_MOKOSUITECROSS_EMAIL_SUBJECT_DESC="Subject line for Mailchimp, SendGrid, Brevo campaigns. Use {email_subject} placeholder. Falls back to article title."
|
||||||
|
PLG_CONTENT_MOKOSUITECROSS_EMAIL_BODY="Email Body"
|
||||||
|
PLG_CONTENT_MOKOSUITECROSS_EMAIL_BODY_DESC="HTML content for email campaigns. Use {email_body} placeholder. Falls back to full article text."
|
||||||
|
PLG_CONTENT_MOKOSUITECROSS_SHARE_IMAGE="Share Image"
|
||||||
|
PLG_CONTENT_MOKOSUITECROSS_SHARE_IMAGE_DESC="Which image to use when cross-posting this article."
|
||||||
|
PLG_CONTENT_MOKOSUITECROSS_SHARE_IMAGE_INTRO="Intro Image"
|
||||||
|
PLG_CONTENT_MOKOSUITECROSS_SHARE_IMAGE_FULLTEXT="Full Text Image"
|
||||||
|
PLG_CONTENT_MOKOSUITECROSS_SHARE_IMAGE_CUSTOM="Custom Image"
|
||||||
|
PLG_CONTENT_MOKOSUITECROSS_SHARE_IMAGE_NONE="No Image"
|
||||||
|
PLG_CONTENT_MOKOSUITECROSS_CUSTOM_IMAGE="Custom Share Image"
|
||||||
|
PLG_CONTENT_MOKOSUITECROSS_CUSTOM_IMAGE_DESC="Select an image from the media manager to use for cross-posting."
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<extension type="plugin" group="content" method="upgrade">
|
<extension type="plugin" group="content" method="upgrade">
|
||||||
<name>Content - MokoSuiteCross</name>
|
<name>Content - MokoSuiteCross</name>
|
||||||
<version>01.02.00-rc</version>
|
<version>01.08.37</version>
|
||||||
<creationDate>2026-05-28</creationDate>
|
<creationDate>2026-05-28</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
|||||||
+91
-39
@@ -140,6 +140,71 @@ class MokoSuiteCrossContent extends CMSPlugin implements SubscriberInterface
|
|||||||
showon="mokosuitecross_skip:0[AND]mokosuitecross_evergreen:1"
|
showon="mokosuitecross_skip:0[AND]mokosuitecross_evergreen:1"
|
||||||
/>
|
/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
<fieldset name="mokosuitecross_share" label="PLG_CONTENT_MOKOSUITECROSS_FIELDSET_SHARE">
|
||||||
|
<field
|
||||||
|
name="mokosuitecross_social_text"
|
||||||
|
type="textarea"
|
||||||
|
label="PLG_CONTENT_MOKOSUITECROSS_SOCIAL_TEXT"
|
||||||
|
description="PLG_CONTENT_MOKOSUITECROSS_SOCIAL_TEXT_DESC"
|
||||||
|
rows="3"
|
||||||
|
hint="Optimized for Facebook, LinkedIn, Threads. Leave empty to use intro text."
|
||||||
|
filter="string"
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="mokosuitecross_short_text"
|
||||||
|
type="textarea"
|
||||||
|
label="PLG_CONTENT_MOKOSUITECROSS_SHORT_TEXT"
|
||||||
|
description="PLG_CONTENT_MOKOSUITECROSS_SHORT_TEXT_DESC"
|
||||||
|
rows="2"
|
||||||
|
hint="For Twitter (280), Bluesky (300). Leave empty for auto-truncated title."
|
||||||
|
filter="string"
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="mokosuitecross_chat_text"
|
||||||
|
type="textarea"
|
||||||
|
label="PLG_CONTENT_MOKOSUITECROSS_CHAT_TEXT"
|
||||||
|
description="PLG_CONTENT_MOKOSUITECROSS_CHAT_TEXT_DESC"
|
||||||
|
rows="3"
|
||||||
|
hint="For Telegram, Discord, Slack, Teams. Leave empty to use intro text."
|
||||||
|
filter="string"
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="mokosuitecross_email_subject"
|
||||||
|
type="text"
|
||||||
|
label="PLG_CONTENT_MOKOSUITECROSS_EMAIL_SUBJECT"
|
||||||
|
description="PLG_CONTENT_MOKOSUITECROSS_EMAIL_SUBJECT_DESC"
|
||||||
|
hint="For Mailchimp, SendGrid, Brevo. Leave empty to use article title."
|
||||||
|
filter="string"
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="mokosuitecross_email_body"
|
||||||
|
type="editor"
|
||||||
|
label="PLG_CONTENT_MOKOSUITECROSS_EMAIL_BODY"
|
||||||
|
description="PLG_CONTENT_MOKOSUITECROSS_EMAIL_BODY_DESC"
|
||||||
|
filter="safehtml"
|
||||||
|
buttons="true"
|
||||||
|
hide="readmore,pagebreak"
|
||||||
|
height="200"
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="mokosuitecross_share_image"
|
||||||
|
type="list"
|
||||||
|
label="PLG_CONTENT_MOKOSUITECROSS_SHARE_IMAGE"
|
||||||
|
description="PLG_CONTENT_MOKOSUITECROSS_SHARE_IMAGE_DESC"
|
||||||
|
default="intro">
|
||||||
|
<option value="intro">PLG_CONTENT_MOKOSUITECROSS_SHARE_IMAGE_INTRO</option>
|
||||||
|
<option value="fulltext">PLG_CONTENT_MOKOSUITECROSS_SHARE_IMAGE_FULLTEXT</option>
|
||||||
|
<option value="custom">PLG_CONTENT_MOKOSUITECROSS_SHARE_IMAGE_CUSTOM</option>
|
||||||
|
<option value="none">PLG_CONTENT_MOKOSUITECROSS_SHARE_IMAGE_NONE</option>
|
||||||
|
</field>
|
||||||
|
<field
|
||||||
|
name="mokosuitecross_custom_image"
|
||||||
|
type="media"
|
||||||
|
label="PLG_CONTENT_MOKOSUITECROSS_CUSTOM_IMAGE"
|
||||||
|
description="PLG_CONTENT_MOKOSUITECROSS_CUSTOM_IMAGE_DESC"
|
||||||
|
showon="mokosuitecross_share_image:custom"
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
</fields>
|
</fields>
|
||||||
</form>
|
</form>
|
||||||
XML;
|
XML;
|
||||||
@@ -205,25 +270,11 @@ XML;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Add cross-post status badges before article content in admin.
|
* Add cross-post status badges before article content in admin.
|
||||||
*
|
|
||||||
* Joomla 5/6 compatible — accepts both BeforeDisplayEvent and legacy parameters.
|
|
||||||
*/
|
*/
|
||||||
public function onContentBeforeDisplay($event): string
|
public function onContentBeforeDisplay(\Joomla\CMS\Event\Content\BeforeDisplayEvent $event): string
|
||||||
{
|
{
|
||||||
// Joomla 5/6 compatibility
|
|
||||||
if ($event instanceof \Joomla\CMS\Event\Content\BeforeDisplayEvent) {
|
|
||||||
$context = $event->getContext();
|
$context = $event->getContext();
|
||||||
$article = $event->getItem();
|
$article = $event->getItem();
|
||||||
} elseif (is_string($event)) {
|
|
||||||
$context = $event;
|
|
||||||
$article = func_get_arg(1);
|
|
||||||
} else {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($context !== 'com_content.article') {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
$app = $this->getApplication();
|
$app = $this->getApplication();
|
||||||
|
|
||||||
@@ -265,26 +316,18 @@ XML;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Dispatch cross-post when an article is saved and published.
|
* Dispatch cross-post when an article is saved and published.
|
||||||
*
|
|
||||||
* Joomla 5/6 compatible — accepts both AfterSaveEvent and legacy parameters.
|
|
||||||
*/
|
*/
|
||||||
public function onContentAfterSave($event): void
|
public function onContentAfterSave(\Joomla\CMS\Event\Content\AfterSaveEvent $event): void
|
||||||
{
|
{
|
||||||
// Joomla 5/6 compatibility
|
|
||||||
if ($event instanceof \Joomla\CMS\Event\Content\AfterSaveEvent) {
|
|
||||||
$context = $event->getContext();
|
$context = $event->getContext();
|
||||||
$article = $event->getItem();
|
|
||||||
$isNew = $event->getIsNew();
|
|
||||||
} else {
|
|
||||||
$context = $event;
|
|
||||||
$article = func_get_arg(1);
|
|
||||||
$isNew = func_get_arg(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($context !== 'com_content.article') {
|
if ($context !== 'com_content.article') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$article = $event->getItem();
|
||||||
|
$isNew = $event->getIsNew();
|
||||||
|
|
||||||
if ((int) ($article->state ?? 0) !== 1) {
|
if ((int) ($article->state ?? 0) !== 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -310,27 +353,36 @@ XML;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Dispatch cross-post when article state changes to published.
|
* Dispatch cross-post when article state changes to published.
|
||||||
*
|
|
||||||
* Joomla 5/6 compatible — accepts both ContentChangeStateEvent and legacy parameters.
|
|
||||||
*/
|
*/
|
||||||
public function onContentChangeState($event): void
|
public function onContentChangeState(\Joomla\CMS\Event\Content\ContentChangeStateEvent $event): void
|
||||||
{
|
{
|
||||||
if ($event instanceof \Joomla\CMS\Event\Content\ContentChangeStateEvent) {
|
|
||||||
$context = $event->getContext();
|
$context = $event->getContext();
|
||||||
$pks = $event->getPks();
|
|
||||||
$value = $event->getValue();
|
|
||||||
} else {
|
|
||||||
$context = $event;
|
|
||||||
$pks = func_get_arg(1);
|
|
||||||
$value = func_get_arg(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($context !== 'com_content.article' || $value !== 1) {
|
if ($context !== 'com_content.article') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$pks = $event->getPks();
|
||||||
|
$value = $event->getValue();
|
||||||
|
|
||||||
$params = ComponentHelper::getParams('com_mokosuitecross');
|
$params = ComponentHelper::getParams('com_mokosuitecross');
|
||||||
|
|
||||||
|
// Unpublish/trash: delete from platforms if configured
|
||||||
|
if ($value === 0 || $value === -2) {
|
||||||
|
if ($params->get('delete_on_unpublish', 0)) {
|
||||||
|
foreach ($pks as $pk) {
|
||||||
|
CrossPostDispatcher::deleteFromPlatforms((int) $pk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Publish: auto-post if configured
|
||||||
|
if ($value !== 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!$params->get('auto_post_on_publish', 1)) {
|
if (!$params->get('auto_post_on_publish', 1)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<extension type="plugin" group="mokosuitecross" method="upgrade">
|
<extension type="plugin" group="mokosuitecross" method="upgrade">
|
||||||
<name>MokoSuiteCross - ActivityPub (Fediverse)</name>
|
<name>MokoSuiteCross - ActivityPub (Fediverse)</name>
|
||||||
<version>01.02.00-rc</version>
|
<version>01.08.37</version>
|
||||||
<creationDate>2026-05-28</creationDate>
|
<creationDate>2026-05-28</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<extension type="plugin" group="mokosuitecross" method="upgrade">
|
<extension type="plugin" group="mokosuitecross" method="upgrade">
|
||||||
<name>MokoSuiteCross - Google Blogger</name>
|
<name>MokoSuiteCross - Google Blogger</name>
|
||||||
<version>01.02.00-rc</version>
|
<version>01.08.37</version>
|
||||||
<creationDate>2026-05-28</creationDate>
|
<creationDate>2026-05-28</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<extension type="plugin" group="mokosuitecross" method="upgrade">
|
<extension type="plugin" group="mokosuitecross" method="upgrade">
|
||||||
<name>MokoSuiteCross - Bluesky</name>
|
<name>MokoSuiteCross - Bluesky</name>
|
||||||
<version>01.02.00-rc</version>
|
<version>01.08.37</version>
|
||||||
<creationDate>2026-05-28</creationDate>
|
<creationDate>2026-05-28</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
<license>GPL-3.0-or-later</license>
|
<license>GPL-3.0-or-later</license>
|
||||||
<description>PLG_MOKOSUITECROSS_BLUESKY_DESCRIPTION</description>
|
<description>PLG_MOKOSUITECROSS_BLUESKY_DESCRIPTION</description>
|
||||||
|
|
||||||
<namespace path="src">Joomla\Plugin\MokoSuiteCross${CLASS_NAME}</namespace>
|
<namespace path="src">Joomla\Plugin\MokoSuiteCross\Bluesky</namespace>
|
||||||
|
|
||||||
<files>
|
<files>
|
||||||
<filename plugin="bluesky">bluesky.php</filename>
|
<filename plugin="bluesky">bluesky.php</filename>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ namespace Joomla\Plugin\MokoSuiteCross\Bluesky\Extension;
|
|||||||
defined('_JEXEC') or die;
|
defined('_JEXEC') or die;
|
||||||
|
|
||||||
use Joomla\CMS\Plugin\CMSPlugin;
|
use Joomla\CMS\Plugin\CMSPlugin;
|
||||||
|
use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossDeleteInterface;
|
||||||
use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface;
|
use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface;
|
||||||
use Joomla\Event\SubscriberInterface;
|
use Joomla\Event\SubscriberInterface;
|
||||||
|
|
||||||
@@ -29,7 +30,7 @@ use Joomla\Event\SubscriberInterface;
|
|||||||
* "pds_url": "https://bsky.social" // Optional, defaults to bsky.social
|
* "pds_url": "https://bsky.social" // Optional, defaults to bsky.social
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
class BlueskyService extends CMSPlugin implements SubscriberInterface, MokoSuiteCrossServiceInterface
|
class BlueskyService extends CMSPlugin implements SubscriberInterface, MokoSuiteCrossServiceInterface, MokoSuiteCrossDeleteInterface
|
||||||
{
|
{
|
||||||
public static function getSubscribedEvents(): array
|
public static function getSubscribedEvents(): array
|
||||||
{
|
{
|
||||||
@@ -65,15 +66,59 @@ class BlueskyService extends CMSPlugin implements SubscriberInterface, MokoSuite
|
|||||||
return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Authentication failed']];
|
return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Authentication failed']];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create post
|
// Build external link card embed if URL is in params
|
||||||
|
$embed = null;
|
||||||
|
$articleUrl = $params['article_url'] ?? '';
|
||||||
|
if (!empty($articleUrl)) {
|
||||||
|
$embed = [
|
||||||
|
'$type' => 'app.bsky.embed.external',
|
||||||
|
'external' => [
|
||||||
|
'uri' => $articleUrl,
|
||||||
|
'title' => $params['article_title'] ?? '',
|
||||||
|
'description' => mb_substr(strip_tags($message), 0, 200),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-thread: split long messages at sentence boundaries
|
||||||
|
$chunks = $this->splitIntoThread($message, 300);
|
||||||
|
|
||||||
|
if (count($chunks) === 1) {
|
||||||
|
// Single post
|
||||||
|
return $this->createPost($pds, $authData, $chunks[0], $embed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thread: post each chunk as a reply to the previous
|
||||||
|
$rootUri = null;
|
||||||
|
$rootCid = null;
|
||||||
|
$parentUri = null;
|
||||||
|
$parentCid = null;
|
||||||
|
$lastResult = [];
|
||||||
|
|
||||||
|
foreach ($chunks as $i => $chunk) {
|
||||||
|
$record = [
|
||||||
|
'$type' => 'app.bsky.feed.post',
|
||||||
|
'text' => $chunk,
|
||||||
|
'createdAt' => gmdate('Y-m-d\TH:i:s\Z'),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Add reply reference for thread posts after the first
|
||||||
|
if ($rootUri !== null) {
|
||||||
|
$record['reply'] = [
|
||||||
|
'root' => ['uri' => $rootUri, 'cid' => $rootCid],
|
||||||
|
'parent' => ['uri' => $parentUri, 'cid' => $parentCid],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach link card embed to last post only
|
||||||
|
if ($embed !== null && $i === count($chunks) - 1) {
|
||||||
|
$record['embed'] = $embed;
|
||||||
|
}
|
||||||
|
|
||||||
$postData = json_encode([
|
$postData = json_encode([
|
||||||
'repo' => $authData['did'],
|
'repo' => $authData['did'],
|
||||||
'collection' => 'app.bsky.feed.post',
|
'collection' => 'app.bsky.feed.post',
|
||||||
'record' => [
|
'record' => $record,
|
||||||
'$type' => 'app.bsky.feed.post',
|
|
||||||
'text' => mb_substr($message, 0, 300),
|
|
||||||
'createdAt' => gmdate('Y-m-d\TH:i:s\Z'),
|
|
||||||
],
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$ch = curl_init($pds . '/xrpc/com.atproto.repo.createRecord');
|
$ch = curl_init($pds . '/xrpc/com.atproto.repo.createRecord');
|
||||||
@@ -83,31 +128,33 @@ class BlueskyService extends CMSPlugin implements SubscriberInterface, MokoSuite
|
|||||||
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $authData['accessJwt'], 'Content-Type: application/json'],
|
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $authData['accessJwt'], 'Content-Type: application/json'],
|
||||||
CURLOPT_RETURNTRANSFER => true,
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
CURLOPT_TIMEOUT => 30,
|
CURLOPT_TIMEOUT => 30,
|
||||||
CURLOPT_PROTOCOLS => CURLPROTO_HTTPS | CURLPROTO_HTTP,
|
|
||||||
CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTPS | CURLPROTO_HTTP,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response = curl_exec($ch);
|
$response = curl_exec($ch);
|
||||||
|
|
||||||
if ($response === false) {
|
if ($response === false) {
|
||||||
|
|
||||||
$curlError = curl_error($ch);
|
$curlError = curl_error($ch);
|
||||||
|
|
||||||
curl_close($ch);
|
curl_close($ch);
|
||||||
|
return ['success' => false, 'platform_post_id' => $rootUri ?? '', 'response' => ['error' => 'Thread error at post ' . ($i + 1) . ': ' . $curlError]];
|
||||||
return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Connection error: ' . $curlError]];
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
curl_close($ch);
|
curl_close($ch);
|
||||||
|
|
||||||
$data = json_decode($response, true) ?: [];
|
$data = json_decode($response, true) ?: [];
|
||||||
|
|
||||||
if ($httpCode === 200 && !empty($data['uri'])) {
|
if ($httpCode !== 200 || empty($data['uri'])) {
|
||||||
return ['success' => true, 'platform_post_id' => $data['uri'], 'response' => $data];
|
return ['success' => false, 'platform_post_id' => $rootUri ?? '', 'response' => $data];
|
||||||
}
|
}
|
||||||
|
|
||||||
return ['success' => false, 'platform_post_id' => '', 'response' => $data];
|
if ($rootUri === null) {
|
||||||
|
$rootUri = $data['uri'];
|
||||||
|
$rootCid = $data['cid'];
|
||||||
|
}
|
||||||
|
$parentUri = $data['uri'];
|
||||||
|
$parentCid = $data['cid'];
|
||||||
|
$lastResult = $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['success' => true, 'platform_post_id' => $rootUri, 'response' => array_merge($lastResult, ['thread_count' => count($chunks)])];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function validateCredentials(array $credentials): array
|
public function validateCredentials(array $credentials): array
|
||||||
@@ -127,7 +174,7 @@ class BlueskyService extends CMSPlugin implements SubscriberInterface, MokoSuite
|
|||||||
|
|
||||||
private function authenticateWithCache(string $pds, string $handle, string $appPwd): array
|
private function authenticateWithCache(string $pds, string $handle, string $appPwd): array
|
||||||
{
|
{
|
||||||
$cacheKey = md5($pds . $handle);
|
$cacheKey = hash('sha256', $pds . $handle);
|
||||||
|
|
||||||
if (isset(self::$sessionCache[$cacheKey])) {
|
if (isset(self::$sessionCache[$cacheKey])) {
|
||||||
$cached = self::$sessionCache[$cacheKey];
|
$cached = self::$sessionCache[$cacheKey];
|
||||||
@@ -175,6 +222,157 @@ class BlueskyService extends CMSPlugin implements SubscriberInterface, MokoSuite
|
|||||||
return json_decode($response, true) ?: [];
|
return json_decode($response, true) ?: [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a single Bluesky post (used for non-threaded messages).
|
||||||
|
*/
|
||||||
|
private function createPost(string $pds, array $authData, string $text, ?array $embed = null): array
|
||||||
|
{
|
||||||
|
$record = [
|
||||||
|
'$type' => 'app.bsky.feed.post',
|
||||||
|
'text' => $text,
|
||||||
|
'createdAt' => gmdate('Y-m-d\TH:i:s\Z'),
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($embed !== null) {
|
||||||
|
$record['embed'] = $embed;
|
||||||
|
}
|
||||||
|
|
||||||
|
$postData = json_encode([
|
||||||
|
'repo' => $authData['did'],
|
||||||
|
'collection' => 'app.bsky.feed.post',
|
||||||
|
'record' => $record,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$ch = curl_init($pds . '/xrpc/com.atproto.repo.createRecord');
|
||||||
|
curl_setopt_array($ch, [
|
||||||
|
CURLOPT_POST => true,
|
||||||
|
CURLOPT_POSTFIELDS => $postData,
|
||||||
|
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $authData['accessJwt'], 'Content-Type: application/json'],
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_TIMEOUT => 30,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
if ($response === false) {
|
||||||
|
$curlError = curl_error($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Connection error: ' . $curlError]];
|
||||||
|
}
|
||||||
|
|
||||||
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
$data = json_decode($response, true) ?: [];
|
||||||
|
|
||||||
|
if ($httpCode === 200 && !empty($data['uri'])) {
|
||||||
|
return ['success' => true, 'platform_post_id' => $data['uri'], 'response' => $data];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['success' => false, 'platform_post_id' => '', 'response' => $data];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split a long message into thread-sized chunks at sentence boundaries.
|
||||||
|
*/
|
||||||
|
private function splitIntoThread(string $message, int $maxLength): array
|
||||||
|
{
|
||||||
|
if (mb_strlen($message) <= $maxLength) {
|
||||||
|
return [$message];
|
||||||
|
}
|
||||||
|
|
||||||
|
$chunks = [];
|
||||||
|
$remaining = $message;
|
||||||
|
|
||||||
|
while (mb_strlen($remaining) > $maxLength) {
|
||||||
|
$segment = mb_substr($remaining, 0, $maxLength);
|
||||||
|
|
||||||
|
// Try to break at last sentence boundary (. ! ? followed by space)
|
||||||
|
$breakPos = max(
|
||||||
|
mb_strrpos($segment, '. ') ?: 0,
|
||||||
|
mb_strrpos($segment, '! ') ?: 0,
|
||||||
|
mb_strrpos($segment, '? ') ?: 0
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($breakPos < $maxLength * 0.3) {
|
||||||
|
// No good sentence break; try last space
|
||||||
|
$breakPos = mb_strrpos($segment, ' ') ?: $maxLength;
|
||||||
|
} else {
|
||||||
|
$breakPos += 1; // Include the punctuation
|
||||||
|
}
|
||||||
|
|
||||||
|
$chunks[] = trim(mb_substr($remaining, 0, $breakPos));
|
||||||
|
$remaining = trim(mb_substr($remaining, $breakPos));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($remaining)) {
|
||||||
|
$chunks[] = $remaining;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $chunks;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deletePost(string $platformPostId, array $credentials): array
|
||||||
|
{
|
||||||
|
$pds = rtrim($credentials['pds_url'] ?? 'https://bsky.social', '/');
|
||||||
|
$handle = $credentials['handle'] ?? '';
|
||||||
|
$appPwd = $credentials['app_password'] ?? '';
|
||||||
|
|
||||||
|
if (empty($handle) || empty($appPwd)) {
|
||||||
|
return ['success' => false, 'message' => 'Missing credentials.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse AT URI: at://did:plc:xxx/app.bsky.feed.post/rkey
|
||||||
|
$parts = explode('/', $platformPostId);
|
||||||
|
$rkey = end($parts);
|
||||||
|
|
||||||
|
if (empty($rkey)) {
|
||||||
|
return ['success' => false, 'message' => 'Invalid AT URI -- could not extract rkey.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticate (uses cached session if still valid)
|
||||||
|
$authData = $this->authenticateWithCache($pds, $handle, $appPwd);
|
||||||
|
|
||||||
|
if (empty($authData['accessJwt'])) {
|
||||||
|
return ['success' => false, 'message' => 'Authentication failed.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$postData = json_encode([
|
||||||
|
'repo' => $authData['did'],
|
||||||
|
'collection' => 'app.bsky.feed.post',
|
||||||
|
'rkey' => $rkey,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$ch = curl_init($pds . '/xrpc/com.atproto.repo.deleteRecord');
|
||||||
|
curl_setopt_array($ch, [
|
||||||
|
CURLOPT_POST => true,
|
||||||
|
CURLOPT_POSTFIELDS => $postData,
|
||||||
|
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $authData['accessJwt'], 'Content-Type: application/json'],
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_TIMEOUT => 30,
|
||||||
|
CURLOPT_PROTOCOLS => CURLPROTO_HTTPS | CURLPROTO_HTTP,
|
||||||
|
CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTPS | CURLPROTO_HTTP,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
|
||||||
|
if ($response === false) {
|
||||||
|
$curlError = curl_error($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
return ['success' => false, 'message' => 'Connection error: ' . $curlError];
|
||||||
|
}
|
||||||
|
|
||||||
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($httpCode === 200) {
|
||||||
|
return ['success' => true, 'message' => 'Post deleted successfully.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode($response, true) ?: [];
|
||||||
|
|
||||||
|
return ['success' => false, 'message' => $data['message'] ?? 'Delete failed with HTTP ' . $httpCode];
|
||||||
|
}
|
||||||
|
|
||||||
public function getSupportedMediaTypes(): array
|
public function getSupportedMediaTypes(): array
|
||||||
{
|
{
|
||||||
return ['image'];
|
return ['image'];
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<extension type="plugin" group="mokosuitecross" method="upgrade">
|
<extension type="plugin" group="mokosuitecross" method="upgrade">
|
||||||
<name>MokoSuiteCross - Brevo (Sendinblue)</name>
|
<name>MokoSuiteCross - Brevo (Sendinblue)</name>
|
||||||
<version>01.02.00-rc</version>
|
<version>01.08.37</version>
|
||||||
<creationDate>2026-05-28</creationDate>
|
<creationDate>2026-05-28</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user