Public Access
Compare commits
201 Commits
rc
...
development
| Author | SHA1 | Date | |
|---|---|---|---|
| 8fef6a3ce7 | |||
| e678f31817 | |||
| 3ef651e34d | |||
| 193fa8be53 | |||
| 8a5209bf19 | |||
| 95880d3e44 | |||
| 2904f1ce0e | |||
| e17d61e366 | |||
| 24fbc08bce | |||
| 2ae287a25b | |||
| 66d4c0651d | |||
| 32c500109f | |||
| 11371cf2d0 | |||
| 5948d599f2 | |||
| 7275c8c646 | |||
| cb7340ce21 | |||
| a67bf83467 | |||
| 632d8486b8 | |||
| 558cd6043d | |||
| 1cf076f088 | |||
| 00f0e44c78 | |||
| c976f400f4 | |||
| ebf37423f2 | |||
| e4d836067f | |||
| 8900b12f81 | |||
| 4fc3d0a4a9 | |||
| 19aa0111f0 | |||
| 46e33a9383 | |||
| 2f43bac247 | |||
| 817e9caee8 | |||
| 6216803590 | |||
| caad8ee7d0 | |||
| 558c0a0edf | |||
| cb1053274e | |||
| 743da9c4c2 | |||
| 4b6fcb5fa4 | |||
| e7b2c1fba2 | |||
| 2a45dd873b | |||
| e0f1ec1372 | |||
| f325de91d4 | |||
| 109493ab4a | |||
| 0d3b14d55c | |||
| 35075aa743 | |||
| 7be017ae30 | |||
| e8a3414ff4 | |||
| e8697c2d0e | |||
| 7d369628f0 | |||
| e834b8a3ea | |||
| 1655e2a0ae | |||
| 4aef631244 | |||
| 6b2cf099f7 | |||
| fefa44965f | |||
| 2b7e38b711 | |||
| 3ed575906f | |||
| c775cb9447 | |||
| 89542f6018 | |||
| bd97b6f79c | |||
| c461cc5520 | |||
| ae4191bad0 | |||
| 73f3d0e734 | |||
| 0e7ca2f19a | |||
| 685089f60c | |||
| 4d3aef3cc5 | |||
| d6d05a1075 | |||
| 1c8bf4e76e | |||
| 6e99d0fac6 | |||
| 5161072b0e | |||
| 7a4358454d | |||
| a0cc0953c7 | |||
| 79c853354b | |||
| 033e948c79 | |||
| 016fda394b | |||
| 8b18bd73e5 | |||
| ffba748762 | |||
| d67ff279a1 | |||
| fbbc170333 | |||
| b574799ca1 | |||
| 79866a15e5 | |||
| a3c18a0248 | |||
| 57392c254e | |||
| acdc0e903c | |||
| 55af5b0feb | |||
| 47ab0de146 | |||
| cfdb0094ba | |||
| 5e8092a1c9 | |||
| 3699edbf62 | |||
| c5860d4074 | |||
| 5d8fa2b1d3 | |||
| 9f3dd6f790 | |||
| 4ccc850edf | |||
| 957460db03 | |||
| 8b014ad0a9 | |||
| f942615a12 | |||
| 116d94dd8c | |||
| a918cb38b2 | |||
| dc7f6c9eeb | |||
| 0b1f39a75c | |||
| db21aca7d0 | |||
| 463ae43e64 | |||
| 93c524b655 | |||
| 2e0f3554db | |||
| 5521a7f81e | |||
| 3331442381 | |||
| 62b19d516f | |||
| 8d2ac56f0c | |||
| 12a16b178a | |||
| f8e21dce5a | |||
| 1e4d3d5b3f | |||
| 78fa142342 | |||
| 7ac196e4a8 | |||
| 2a3e733d8c | |||
| 3abd08da44 | |||
| 7269800891 | |||
| b038a49279 | |||
| 9b05cd5fc5 | |||
| 2846c361e0 | |||
| 7ccf2585dd | |||
| 5c31771037 | |||
| 8de72a386d | |||
| 05aafbac61 | |||
| 3c57e87066 | |||
| e37443774b | |||
| e6b9a3b4f6 | |||
| d0200c28f0 | |||
| 1f946c0b75 | |||
| d4e2d36301 | |||
| a2c1a61759 | |||
| 0d12bca60e | |||
| 7cabc6e5ef | |||
| b73c1eba25 | |||
| e5ab1a4cd5 | |||
| f81873cf37 | |||
| d9e15b4f4a | |||
| 31b4bbca2a | |||
| a7f07b77a2 | |||
| 3e84bc9a18 | |||
| 7e8c96b629 | |||
| 2bf88d1d17 | |||
| 2160cd1ce7 | |||
| 6b61d9a211 | |||
| 1fbffbcb57 | |||
| f910a8ad12 | |||
| 5439df3876 | |||
| ab0c209897 | |||
| e4d9bce5d0 | |||
| e933e7b651 | |||
| 157e87279e | |||
| 7850721f86 | |||
| 8949f69699 | |||
| af2313d936 | |||
| 2e5446ff5e | |||
| 17a50851fb | |||
| ef3614d249 | |||
| ab05bb7008 | |||
| 3d5d581883 | |||
| 6bd26698c4 | |||
| 19b504526b | |||
| e7bdf7cbc7 | |||
| ff5794d0cc | |||
| bd5f676e0a | |||
| bfba45e8b5 | |||
| 78ea05233b | |||
| ae0d54310d | |||
| 9df59836bf | |||
| 6e40707223 | |||
| ca55e5d2d2 | |||
| 9526d006c4 | |||
| c90a5671bd | |||
| 048a7d71d1 | |||
| 847263dd86 | |||
| 6e540f64c4 | |||
| c57b5724ac | |||
| 78affd37ff | |||
| b3062c6559 | |||
| cf02738930 | |||
| 455d4c8a19 | |||
| 8286d493b9 | |||
| b740152d67 | |||
| 9dab9f1ef6 | |||
| c61d32709c | |||
| a59091e348 | |||
| ee3d58c20f | |||
| 2b137f9041 | |||
| 54a27c0a8f | |||
| 5754fae5a8 | |||
| ab3c0a3a8d | |||
| eb3689cff6 | |||
| 7338a3da2e | |||
| 0a0e1f11e0 | |||
| c3a3ab3f62 | |||
| 556ac85a63 | |||
| c1a145480c | |||
| ab7b6cfba1 | |||
| 2d6155d655 | |||
| 65215cdc4c | |||
| 8c87cf1e74 | |||
| 59d3524615 | |||
| 8058baef95 | |||
| df2efa4838 | |||
| 76bc91a383 | |||
| b53846f6f4 |
+12
@@ -0,0 +1,12 @@
|
|||||||
|
[submodule "templates/repos/Template-Client"]
|
||||||
|
path = templates/repos/Template-Client
|
||||||
|
url = https://git.mokoconsulting.tech/MokoConsulting/Template-Client.git
|
||||||
|
[submodule "templates/repos/Template-Generic"]
|
||||||
|
path = templates/repos/Template-Generic
|
||||||
|
url = https://git.mokoconsulting.tech/MokoConsulting/Template-Generic.git
|
||||||
|
[submodule "templates/repos/Template-Joomla"]
|
||||||
|
path = templates/repos/Template-Joomla
|
||||||
|
url = https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla.git
|
||||||
|
[submodule "templates/repos/Template-MCP"]
|
||||||
|
path = templates/repos/Template-MCP
|
||||||
|
url = https://git.mokoconsulting.tech/MokoConsulting/Template-MCP.git
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# mokocli
|
||||||
|
|
||||||
|
Enterprise automation, validation, sync, and governance engine for all Moko Consulting repositories.
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Language** | PHP 8.1+ |
|
||||||
|
| **Version** | 09.01.00 |
|
||||||
|
| **Branch** | develop on `dev`, merge to `main` (protected) |
|
||||||
|
| **Wiki** | [mokocli Wiki](https://git.mokoconsulting.tech/MokoConsulting/mokocli/wiki) |
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer install # Install PHP dependencies
|
||||||
|
php bin/moko health --path . # Repo health check
|
||||||
|
php bin/moko check:syntax --path . # PHP syntax check
|
||||||
|
php bin/moko drift --org MokoConsulting # Scan for standards drift
|
||||||
|
php bin/moko dashboard --token $TOKEN -o dashboard.html # Client dashboard
|
||||||
|
|
||||||
|
# Code quality
|
||||||
|
php vendor/bin/phpcs --standard=phpcs.xml -n lib/ validate/ automation/ cli/
|
||||||
|
php vendor/bin/phpcbf --standard=phpcs.xml lib/ validate/ automation/ cli/
|
||||||
|
php vendor/bin/phpstan analyse -c phpstan.neon --memory-limit=512M
|
||||||
|
composer check # Run all checks
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
| Directory | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `cli/` | 32 standalone CLI tools (version, release, build, repo management) |
|
||||||
|
| `validate/` | 20 validation scripts (syntax, structure, manifests, drift) |
|
||||||
|
| `automation/` | 7 bulk operations (sync, push files, templates, cleanup) |
|
||||||
|
| `lib/Enterprise/` | Core library — CliFramework, ApiClient, adapters, validators |
|
||||||
|
| `lib/Enterprise/Plugins/` | 11 platform plugins (Joomla, Dolibarr, Node.js, Python, etc.) |
|
||||||
|
| `deploy/` | SFTP deployment scripts (Joomla, Dolibarr, health checks) |
|
||||||
|
| `templates/` | Universal templates, configs, governance schema |
|
||||||
|
| `.mokogitea/workflows/` | CI/CD workflows (Gitea Actions) |
|
||||||
|
| `bin/moko` | Unified CLI dispatcher — `php bin/moko <command>` |
|
||||||
|
| `monitoring/sites.json` | Sites list for mcp_mokomonitor |
|
||||||
|
|
||||||
|
### CLI Framework
|
||||||
|
|
||||||
|
All CLI tools extend `MokoCli\CliFramework` (`lib/Enterprise/CliFramework.php`).
|
||||||
|
Built-in flags: `--help`, `--verbose`, `--quiet`, `--dry-run`.
|
||||||
|
After adding a CLI tool, register it in `bin/moko` COMMAND_MAP.
|
||||||
|
|
||||||
|
### Platform Adapters
|
||||||
|
|
||||||
|
- `MokoGiteaAdapter` — git.mokoconsulting.tech (primary)
|
||||||
|
- `GitHubAdapter` — github.com mirrors
|
||||||
|
|
||||||
|
### Plugin System
|
||||||
|
|
||||||
|
Platform-specific logic in `lib/Enterprise/Plugins/`. Each implements `ProjectPluginInterface` with health checks, validation, build commands, config schemas.
|
||||||
|
|
||||||
|
## Code Quality
|
||||||
|
|
||||||
|
| Tool | Level | Config |
|
||||||
|
|---|---|---|
|
||||||
|
| PHPCS | PSR-12 (errors only) | `phpcs.xml` |
|
||||||
|
| PHPStan | Level 2 (advisory) | `phpstan.neon` |
|
||||||
|
|
||||||
|
PHPStan runs with `--memory-limit=512M`. CI enforces PHPCS errors; PHPStan is `continue-on-error`.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- **Never commit** `.claude/`, `.mcp.json`, `TODO.md`, `*.min.css`/`*.min.js`
|
||||||
|
- **Attribution**: `Authored-by: Moko Consulting`
|
||||||
|
- **Workflow directory**: `.mokogitea/` (not `.gitea/` or `.github/`)
|
||||||
|
- **Wiki**: documentation lives in the Gitea wiki, not `docs/` files
|
||||||
|
- **New CLI tools**: extend `CliFramework`, not `CLIApp` (legacy)
|
||||||
|
- **Standards**: [MokoCli](https://git.mokoconsulting.tech/MokoConsulting/mokocli/wiki/Home)
|
||||||
@@ -7,8 +7,8 @@ contact_links:
|
|||||||
- name: 💬 Ask a Question
|
- name: 💬 Ask a Question
|
||||||
url: https://mokoconsulting.tech/
|
url: https://mokoconsulting.tech/
|
||||||
about: Get help or ask questions through our website
|
about: Get help or ask questions through our website
|
||||||
- name: 📚 moko-platform Documentation
|
- name: 📚 mokocli Documentation
|
||||||
url: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
url: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
about: View our coding standards and best practices
|
about: View our coding standards and best practices
|
||||||
- name: 🔒 Report a Security Vulnerability
|
- name: 🔒 Report a Security Vulnerability
|
||||||
url: https://git.mokoconsulting.tech/mokoconsulting-tech/.github-private/security/advisories/new
|
url: https://git.mokoconsulting.tech/mokoconsulting-tech/.github-private/security/advisories/new
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ Suggested text here
|
|||||||
<!-- Add any other context, screenshots, or references -->
|
<!-- Add any other context, screenshots, or references -->
|
||||||
|
|
||||||
## Standards Alignment
|
## Standards Alignment
|
||||||
- [ ] Follows moko-platform documentation guidelines
|
- [ ] Follows mokocli documentation guidelines
|
||||||
- [ ] Uses en_US/en_GB localization
|
- [ ] Uses en_US/en_GB localization
|
||||||
- [ ] Includes proper SPDX headers where applicable
|
- [ ] Includes proper SPDX headers where applicable
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ If you have ideas about how this could be implemented, share them here:
|
|||||||
Add any other context, mockups, or screenshots about the feature request here.
|
Add any other context, mockups, or screenshots about the feature request here.
|
||||||
|
|
||||||
## Relevant Standards
|
## Relevant Standards
|
||||||
Does this relate to any standards in [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform)?
|
Does this relate to any standards in [mokocli](https://git.mokoconsulting.tech/MokoConsulting/mokocli)?
|
||||||
- [ ] Accessibility (WCAG 2.1 AA)
|
- [ ] Accessibility (WCAG 2.1 AA)
|
||||||
- [ ] Localization (en_US/en_GB)
|
- [ ] Localization (en_US/en_GB)
|
||||||
- [ ] Security best practices
|
- [ ] Security best practices
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ Use this template only for:
|
|||||||
<!-- Describe how this could be addressed -->
|
<!-- Describe how this could be addressed -->
|
||||||
|
|
||||||
## Standards Reference
|
## Standards Reference
|
||||||
Does this relate to security standards in [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform)?
|
Does this relate to security standards in [mokocli](https://git.mokoconsulting.tech/MokoConsulting/mokocli)?
|
||||||
- [ ] SPDX license identifiers
|
- [ ] SPDX license identifiers
|
||||||
- [ ] Secret management
|
- [ ] Secret management
|
||||||
- [ ] Dependency security
|
- [ ] Dependency security
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: moko-platform.Automation
|
# INGROUP: mokocli.Automation
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
# PATH: /.gitea/workflows/branch-protection.yml
|
# PATH: /.gitea/workflows/branch-protection.yml
|
||||||
# BRIEF: Apply standardised branch protection rules to all governed repositories
|
# BRIEF: Apply standardised branch protection rules to all governed repositories
|
||||||
#
|
#
|
||||||
@@ -57,13 +57,13 @@ jobs:
|
|||||||
- name: Determine target repos
|
- name: Determine target repos
|
||||||
id: repos
|
id: repos
|
||||||
env:
|
env:
|
||||||
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
API="${GITEA_URL}/api/v1"
|
API="${GITEA_URL}/api/v1"
|
||||||
|
|
||||||
# Platform/standards/infra repos to exclude
|
# Platform/standards/infra repos to exclude
|
||||||
EXCLUDE="gitea-org-config org-profile gitea-private .mokogitea-private moko-platform MokoTesting"
|
EXCLUDE="gitea-org-config org-profile gitea-private .mokogitea-private mokocli MokoTesting"
|
||||||
EXCLUDE="$EXCLUDE MokoStandards-Template-Client MokoStandards-Template-Dolibarr MokoStandards-Template-Generic MokoStandards-Template-Joomla MokoDoliProjTemplate"
|
EXCLUDE="$EXCLUDE MokoCli-Template-Client MokoCli-Template-Dolibarr MokoCli-Template-Generic MokoCli-Template-Joomla MokoDoliProjTemplate"
|
||||||
|
|
||||||
if [ -n "${{ inputs.repos }}" ]; then
|
if [ -n "${{ inputs.repos }}" ]; then
|
||||||
# User-specified repos
|
# User-specified repos
|
||||||
@@ -105,7 +105,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Apply protection rules
|
- name: Apply protection rules
|
||||||
env:
|
env:
|
||||||
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
DRY_RUN: ${{ inputs.dry_run || 'false' }}
|
DRY_RUN: ${{ inputs.dry_run || 'false' }}
|
||||||
run: |
|
run: |
|
||||||
API="${GITEA_URL}/api/v1"
|
API="${GITEA_URL}/api/v1"
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: moko-platform.Automation
|
# INGROUP: mokocli.Automation
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
# PATH: /.gitea/workflows/bulk-repo-sync.yml
|
# PATH: /.gitea/workflows/bulk-repo-sync.yml
|
||||||
# BRIEF: Bulk repo sync — runs from API repo, syncs standards to all governed repos
|
# BRIEF: Bulk repo sync — runs from API repo, syncs standards to all governed repos
|
||||||
|
|
||||||
@@ -84,8 +84,8 @@ jobs:
|
|||||||
echo "Running: php automation/bulk_sync.php ${{ steps.args.outputs.args }}"
|
echo "Running: php automation/bulk_sync.php ${{ steps.args.outputs.args }}"
|
||||||
php automation/bulk_sync.php ${{ steps.args.outputs.args }} 2>&1 | tee /tmp/bulk_sync.log
|
php automation/bulk_sync.php ${{ steps.args.outputs.args }} 2>&1 | tee /tmp/bulk_sync.log
|
||||||
env:
|
env:
|
||||||
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
GH_TOKEN: ${{ secrets.GH_PAT }}
|
||||||
GIT_PLATFORM: gitea
|
GIT_PLATFORM: gitea
|
||||||
GITEA_URL: https://git.mokoconsulting.tech
|
GITEA_URL: https://git.mokoconsulting.tech
|
||||||
GITEA_ORG: MokoConsulting
|
GITEA_ORG: MokoConsulting
|
||||||
@@ -112,7 +112,7 @@ jobs:
|
|||||||
bash automation/enforce_tags.sh || echo "Tag enforcement had errors (non-fatal)"
|
bash automation/enforce_tags.sh || echo "Tag enforcement had errors (non-fatal)"
|
||||||
fi
|
fi
|
||||||
env:
|
env:
|
||||||
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
GITEA_URL: https://git.mokoconsulting.tech
|
GITEA_URL: https://git.mokoconsulting.tech
|
||||||
GITEA_ORG: MokoConsulting
|
GITEA_ORG: MokoConsulting
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: moko-platform.CI
|
# DEFGROUP: mokocli.CI
|
||||||
# INGROUP: moko-platform
|
# INGROUP: mokocli
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
# PATH: /.gitea/workflows/pr-branch-check.yml
|
# PATH: /.gitea/workflows/pr-branch-check.yml
|
||||||
# BRIEF: PR branch merge policy enforcement
|
# BRIEF: PR branch merge policy enforcement
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: moko-platform.Automation
|
# INGROUP: mokocli.Automation
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
# PATH: /.gitea/workflows/renovate.yml
|
# PATH: /.gitea/workflows/renovate.yml
|
||||||
# BRIEF: Run Renovate Bot across all governed repos for dependency updates
|
# BRIEF: Run Renovate Bot across all governed repos for dependency updates
|
||||||
#
|
#
|
||||||
@@ -57,12 +57,12 @@ jobs:
|
|||||||
- name: Determine target repos
|
- name: Determine target repos
|
||||||
id: repos
|
id: repos
|
||||||
env:
|
env:
|
||||||
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
API="${GITEA_URL}/api/v1"
|
API="${GITEA_URL}/api/v1"
|
||||||
|
|
||||||
EXCLUDE="gitea-org-config org-profile gitea-private .mokogitea-private moko-platform MokoTesting"
|
EXCLUDE="gitea-org-config org-profile gitea-private .mokogitea-private mokocli MokoTesting"
|
||||||
EXCLUDE="$EXCLUDE MokoStandards-Template-Client MokoStandards-Template-Dolibarr MokoStandards-Template-Generic MokoStandards-Template-Joomla MokoDoliProjTemplate"
|
EXCLUDE="$EXCLUDE MokoCli-Template-Client MokoCli-Template-Dolibarr MokoCli-Template-Generic MokoCli-Template-Joomla MokoDoliProjTemplate"
|
||||||
|
|
||||||
if [ -n "${{ inputs.repos }}" ]; then
|
if [ -n "${{ inputs.repos }}" ]; then
|
||||||
REPOS=$(echo "${{ inputs.repos }}" | tr ',' ' ')
|
REPOS=$(echo "${{ inputs.repos }}" | tr ',' ' ')
|
||||||
@@ -107,7 +107,7 @@ jobs:
|
|||||||
- name: Run Renovate
|
- name: Run Renovate
|
||||||
if: steps.repos.outputs.repo_list != ''
|
if: steps.repos.outputs.repo_list != ''
|
||||||
env:
|
env:
|
||||||
RENOVATE_TOKEN: ${{ secrets.GA_TOKEN }}
|
RENOVATE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
RENOVATE_PLATFORM: gitea
|
RENOVATE_PLATFORM: gitea
|
||||||
RENOVATE_ENDPOINT: ${{ env.GITEA_URL }}/api/v1
|
RENOVATE_ENDPOINT: ${{ env.GITEA_URL }}/api/v1
|
||||||
RENOVATE_GIT_AUTHOR: 'Renovate Bot <renovate@mokoconsulting.tech>'
|
RENOVATE_GIT_AUTHOR: 'Renovate Bot <renovate@mokoconsulting.tech>'
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: moko-platform.Maintenance
|
# INGROUP: mokocli.Maintenance
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
# PATH: /.gitea/workflows/sync-wikis.yml
|
# PATH: /.gitea/workflows/sync-wikis.yml
|
||||||
# BRIEF: Daily sync of all Gitea wikis to consolidated GitHub wiki repo
|
# BRIEF: Daily sync of all Gitea wikis to consolidated GitHub wiki repo
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Sync all wikis
|
- name: Sync all wikis
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
GH_TOKEN: ${{ secrets.GH_PAT }}
|
||||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
run: |
|
run: |
|
||||||
if [ -z "$GH_TOKEN" ]; then
|
if [ -z "$GH_TOKEN" ]; then
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
#
|
#
|
||||||
# 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.23.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)
|
||||||
|
|
||||||
name: "Universal: Auto Version Bump"
|
name: "Universal: Auto Version Bump"
|
||||||
@@ -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,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-tech/moko-platform
|
# 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
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
|
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
|
||||||
# | |
|
# | |
|
||||||
# | Platform-specific: |
|
# | Platform-specific: |
|
||||||
# | joomla: XML manifest, updates.xml, type-prefixed packages |
|
# | joomla: XML manifest, type-prefixed packages |
|
||||||
# | 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 |
|
||||||
# | |
|
# | |
|
||||||
@@ -66,25 +66,30 @@ jobs:
|
|||||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
- name: Setup moko-platform 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 ! command -v composer &> /dev/null; then
|
if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; 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
|
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
|
fi
|
||||||
# Always fetch latest CLI tools — never use stale cache from previous runs
|
rm -rf /tmp/mokocli
|
||||||
rm -rf /tmp/moko-platform-api
|
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git
|
||||||
git clone --depth 1 --branch main --quiet \
|
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
|
||||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
cd /tmp/mokocli
|
||||||
/tmp/moko-platform-api
|
|
||||||
cd /tmp/moko-platform-api
|
|
||||||
composer install --no-dev --no-interaction --quiet
|
composer install --no-dev --no-interaction --quiet
|
||||||
|
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Rename branch to rc
|
- name: Rename branch to rc
|
||||||
run: |
|
run: |
|
||||||
php /tmp/moko-platform-api/cli/branch_rename.php \
|
php ${MOKO_CLI}/branch_rename.php \
|
||||||
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
|
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
|
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
|
||||||
@@ -100,16 +105,49 @@ jobs:
|
|||||||
|
|
||||||
- name: Publish RC release
|
- name: Publish RC release
|
||||||
run: |
|
run: |
|
||||||
php /tmp/moko-platform-api/cli/release_publish.php \
|
php ${MOKO_CLI}/release_publish.php \
|
||||||
--path . --stability rc --bump minor --branch rc \
|
--path . --stability rc --bump minor --branch rc \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
--skip-update-stream
|
|
||||||
|
- 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 (updates.xml managed by Gitea Pages)" >> $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:
|
||||||
@@ -145,31 +183,129 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
echo "No conflict markers found"
|
echo "No conflict markers found"
|
||||||
|
|
||||||
- name: Setup moko-platform 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: |
|
||||||
# Ensure PHP + Composer are available
|
if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then
|
||||||
if ! command -v composer &> /dev/null; then
|
echo Using pre-installed /opt/mokocli
|
||||||
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
|
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
|
fi
|
||||||
# Always fetch latest CLI tools — never use stale cache from previous runs
|
rm -rf /tmp/mokocli
|
||||||
rm -rf /tmp/moko-platform-api
|
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git
|
||||||
git clone --depth 1 --branch main --quiet \
|
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
|
||||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
cd /tmp/mokocli
|
||||||
/tmp/moko-platform-api
|
|
||||||
cd /tmp/moko-platform-api
|
|
||||||
composer install --no-dev --no-interaction --quiet
|
composer install --no-dev --no-interaction --quiet
|
||||||
|
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: "Detect platform"
|
||||||
|
id: platform
|
||||||
|
run: |
|
||||||
|
php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true
|
||||||
|
php ${MOKO_CLI}/manifest_read.php --path . --github-output 2>/dev/null || true
|
||||||
|
|
||||||
|
- name: "Determine version bump level"
|
||||||
|
id: bump
|
||||||
|
run: |
|
||||||
|
# Fix/patch branches: version was already bumped by pre-release, just strip suffix
|
||||||
|
# Feature/dev branches: bump minor for the new stable release
|
||||||
|
HEAD_REF="${{ github.event.pull_request.head.ref || 'dev' }}"
|
||||||
|
case "$HEAD_REF" in
|
||||||
|
fix/*|patch/*|hotfix/*|bugfix/*) BUMP="none" ;;
|
||||||
|
*) BUMP="minor" ;;
|
||||||
|
esac
|
||||||
|
echo "level=${BUMP}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Bump level: ${BUMP} (from branch: ${HEAD_REF})"
|
||||||
|
|
||||||
- name: "Publish stable release"
|
- name: "Publish stable release"
|
||||||
run: |
|
run: |
|
||||||
php /tmp/moko-platform-api/cli/release_publish.php \
|
BUMP_FLAG=""
|
||||||
--path . --stability stable --bump minor --branch main \
|
if [ "${{ steps.bump.outputs.level }}" != "none" ]; then
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
BUMP_FLAG="--bump ${{ steps.bump.outputs.level }}"
|
||||||
--skip-update-stream
|
fi
|
||||||
|
php ${MOKO_CLI}/release_publish.php \
|
||||||
|
--path . --stability stable ${BUMP_FLAG} --branch main \
|
||||||
|
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
|
- name: "Read published version"
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "")
|
||||||
|
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
||||||
|
[ -z "$VERSION" ] && VERSION="00.00.00" && echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "tag=stable" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "release_tag=stable" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "branch=main" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Published version: ${VERSION}"
|
||||||
|
|
||||||
|
- 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
|
||||||
|
python3 -c "
|
||||||
|
import json, urllib.request
|
||||||
|
body = open('/dev/stdin').read()
|
||||||
|
payload = json.dumps({'body': body}).encode()
|
||||||
|
req = urllib.request.Request(
|
||||||
|
'${API_BASE}/releases/${RELEASE_ID}',
|
||||||
|
data=payload, method='PATCH',
|
||||||
|
headers={
|
||||||
|
'Authorization': 'token ${TOKEN}',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
})
|
||||||
|
urllib.request.urlopen(req)
|
||||||
|
" <<< "$NOTES"
|
||||||
|
echo "Release notes updated from CHANGELOG.md"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Promote [Unreleased] → [version] in CHANGELOG.md and reset
|
||||||
|
if [ -n "$VERSION" ] && [ -f "CHANGELOG.md" ]; then
|
||||||
|
DATE=$(date +%Y-%m-%d)
|
||||||
|
python3 -c "
|
||||||
|
import sys
|
||||||
|
version, date = sys.argv[1], sys.argv[2]
|
||||||
|
content = open('CHANGELOG.md').read()
|
||||||
|
old = '## [Unreleased]'
|
||||||
|
new = f'## [Unreleased]\n\n## [{version}] --- {date}'
|
||||||
|
content = content.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"
|
||||||
@@ -182,7 +318,7 @@ jobs:
|
|||||||
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
||||||
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
php /tmp/moko-platform-api/cli/release_mirror.php \
|
php ${MOKO_CLI}/release_mirror.php \
|
||||||
--version "$VERSION" --tag "$RELEASE_TAG" \
|
--version "$VERSION" --tag "$RELEASE_TAG" \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
|
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
|
||||||
@@ -256,7 +392,7 @@ jobs:
|
|||||||
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}"
|
||||||
php /tmp/moko-platform-api/cli/version_reset_dev.php \
|
php ${MOKO_CLI}/version_reset_dev.php \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
|
||||||
--branch dev --path . 2>&1 || true
|
--branch dev --path . 2>&1 || true
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: MokoPlatform.Universal
|
# INGROUP: MokoCli.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: 09.23.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Delete feature branches after PR merge
|
# BRIEF: Delete feature branches after PR merge
|
||||||
|
|
||||||
name: "Branch Cleanup"
|
name: "Branch Cleanup"
|
||||||
|
|||||||
@@ -0,0 +1,191 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: MokoCli.CI
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
||||||
|
# PATH: /.gitea/workflows/ci-generic.yml
|
||||||
|
# VERSION: 01.00.00
|
||||||
|
# BRIEF: CI pipeline — lint, validate, and test for generic projects (PHP + Node.js)
|
||||||
|
|
||||||
|
name: "Generic: Project CI"
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# ── Lint & Validate ───────────────────────────────────────────────────
|
||||||
|
lint:
|
||||||
|
name: Lint & Validate
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Detect toolchain
|
||||||
|
id: detect
|
||||||
|
run: |
|
||||||
|
HAS_PHP=false
|
||||||
|
HAS_NODE=false
|
||||||
|
[ -f "composer.json" ] && HAS_PHP=true
|
||||||
|
[ -f "package.json" ] && HAS_NODE=true
|
||||||
|
echo "has_php=$HAS_PHP" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "has_node=$HAS_NODE" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Toolchain: PHP=$HAS_PHP Node=$HAS_NODE"
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
if: steps.detect.outputs.has_php == 'true'
|
||||||
|
run: |
|
||||||
|
if ! command -v php &> /dev/null; then
|
||||||
|
sudo apt-get update -qq
|
||||||
|
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
php -v
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
if: steps.detect.outputs.has_node == 'true'
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: Install PHP dependencies
|
||||||
|
if: steps.detect.outputs.has_php == 'true'
|
||||||
|
run: |
|
||||||
|
if [ -f "composer.json" ]; then
|
||||||
|
composer install --no-interaction --prefer-dist --quiet 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Install Node.js dependencies
|
||||||
|
if: steps.detect.outputs.has_node == 'true'
|
||||||
|
run: |
|
||||||
|
if [ -f "package.json" ]; then
|
||||||
|
npm ci --quiet 2>/dev/null || npm install --quiet 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: PHP syntax check
|
||||||
|
if: steps.detect.outputs.has_php == 'true'
|
||||||
|
run: |
|
||||||
|
ERRORS=0
|
||||||
|
while IFS= read -r -d '' file; do
|
||||||
|
if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
|
||||||
|
echo "::error file=${file}::PHP syntax error"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -not -path "./node_modules/*" -print0)
|
||||||
|
|
||||||
|
echo "## PHP Lint" >> $GITHUB_STEP_SUMMARY
|
||||||
|
if [ "$ERRORS" -eq 0 ]; then
|
||||||
|
echo "All PHP files passed syntax check." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "${ERRORS} file(s) with syntax errors." >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: TypeScript/JavaScript lint
|
||||||
|
if: steps.detect.outputs.has_node == 'true'
|
||||||
|
run: |
|
||||||
|
if [ -f "node_modules/.bin/eslint" ]; then
|
||||||
|
npx eslint src/ --quiet 2>&1 || { echo "::error::ESLint errors found"; exit 1; }
|
||||||
|
echo "## ESLint" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "All files passed ESLint." >> $GITHUB_STEP_SUMMARY
|
||||||
|
elif [ -f ".eslintrc.json" ] || [ -f ".eslintrc.js" ] || [ -f "eslint.config.js" ]; then
|
||||||
|
echo "::warning::ESLint config found but eslint not installed"
|
||||||
|
else
|
||||||
|
echo "No ESLint configured — skipping"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: TypeScript compile check
|
||||||
|
if: steps.detect.outputs.has_node == 'true'
|
||||||
|
run: |
|
||||||
|
if [ -f "tsconfig.json" ] && [ -f "node_modules/.bin/tsc" ]; then
|
||||||
|
npx tsc --noEmit 2>&1 || { echo "::error::TypeScript compilation errors"; exit 1; }
|
||||||
|
echo "## TypeScript" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "TypeScript compilation passed." >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: PHPStan static analysis
|
||||||
|
if: steps.detect.outputs.has_php == 'true'
|
||||||
|
run: |
|
||||||
|
if [ -f "phpstan.neon" ] && [ -f "vendor/bin/phpstan" ]; then
|
||||||
|
vendor/bin/phpstan analyse --no-progress 2>&1 || { echo "::warning::PHPStan found issues"; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Tests ─────────────────────────────────────────────────────────────
|
||||||
|
test:
|
||||||
|
name: Tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: lint
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Detect toolchain
|
||||||
|
id: detect
|
||||||
|
run: |
|
||||||
|
HAS_PHP=false
|
||||||
|
HAS_NODE=false
|
||||||
|
[ -f "composer.json" ] && HAS_PHP=true
|
||||||
|
[ -f "package.json" ] && HAS_NODE=true
|
||||||
|
echo "has_php=$HAS_PHP" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "has_node=$HAS_NODE" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
if: steps.detect.outputs.has_php == 'true'
|
||||||
|
run: |
|
||||||
|
if ! command -v php &> /dev/null; then
|
||||||
|
sudo apt-get update -qq
|
||||||
|
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
if: steps.detect.outputs.has_node == 'true'
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
[ -f "composer.json" ] && composer install --no-interaction --prefer-dist --quiet 2>/dev/null || true
|
||||||
|
[ -f "package.json" ] && { npm ci --quiet 2>/dev/null || npm install --quiet 2>/dev/null || true; }
|
||||||
|
|
||||||
|
- name: Run PHP tests
|
||||||
|
if: steps.detect.outputs.has_php == 'true'
|
||||||
|
run: |
|
||||||
|
if [ -f "vendor/bin/phpunit" ]; then
|
||||||
|
vendor/bin/phpunit --testdox 2>&1
|
||||||
|
echo "## PHPUnit" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Tests passed." >> $GITHUB_STEP_SUMMARY
|
||||||
|
elif [ -f "phpunit.xml" ] || [ -f "phpunit.xml.dist" ]; then
|
||||||
|
echo "::warning::PHPUnit config found but phpunit not installed"
|
||||||
|
else
|
||||||
|
echo "No PHPUnit configured — skipping"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Run Node.js tests
|
||||||
|
if: steps.detect.outputs.has_node == 'true'
|
||||||
|
run: |
|
||||||
|
if jq -e '.scripts.test' package.json > /dev/null 2>&1; then
|
||||||
|
npm test 2>&1
|
||||||
|
echo "## Node.js Tests" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Tests passed." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "No test script in package.json — skipping"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Build check
|
||||||
|
run: |
|
||||||
|
if [ -f "Makefile" ]; then
|
||||||
|
make build 2>&1 || echo "::warning::Build failed or not configured"
|
||||||
|
elif [ -f "package.json" ] && jq -e '.scripts.build' package.json > /dev/null 2>&1; then
|
||||||
|
npm run build 2>&1 || echo "::warning::Build failed"
|
||||||
|
fi
|
||||||
@@ -4,18 +4,18 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: moko-platform.CI
|
# INGROUP: mokocli.CI
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
# PATH: /.gitea/workflows/ci-platform.yml
|
# PATH: /.mokogitea/workflows/ci-platform.yml
|
||||||
# VERSION: 09.23.00
|
# VERSION: 09.23.00
|
||||||
# BRIEF: moko-platform CI — the standards engine validates itself
|
# BRIEF: mokocli CI — the standards engine validates itself
|
||||||
#
|
#
|
||||||
# +========================================================================+
|
# +========================================================================+
|
||||||
# | MOKO-PLATFORM CI |
|
# | MOKO-PLATFORM CI |
|
||||||
# +========================================================================+
|
# +========================================================================+
|
||||||
# | |
|
# | |
|
||||||
# | This is NOT a generic CI workflow. This is the self-validation |
|
# | This is NOT a generic CI workflow. This is the self-validation |
|
||||||
# | pipeline for the central moko-platform enterprise engine. |
|
# | pipeline for the central mokocli enterprise engine. |
|
||||||
# | |
|
# | |
|
||||||
# | It dogfoods every tool the platform ships to governed repos: |
|
# | It dogfoods every tool the platform ships to governed repos: |
|
||||||
# | |
|
# | |
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
# | |
|
# | |
|
||||||
# +========================================================================+
|
# +========================================================================+
|
||||||
|
|
||||||
name: "Platform: moko-platform CI"
|
name: "Platform: mokocli CI"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -41,7 +41,7 @@ on:
|
|||||||
paths-ignore:
|
paths-ignore:
|
||||||
- '**.md'
|
- '**.md'
|
||||||
- 'wiki/**'
|
- 'wiki/**'
|
||||||
- '.gitea/ISSUE_TEMPLATE/**'
|
- '.mokogitea/ISSUE_TEMPLATE/**'
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
@@ -104,7 +104,7 @@ jobs:
|
|||||||
echo "::error file=${file}::PHP syntax error"
|
echo "::error file=${file}::PHP syntax error"
|
||||||
ERRORS=$((ERRORS + 1))
|
ERRORS=$((ERRORS + 1))
|
||||||
fi
|
fi
|
||||||
done < <(find lib/ validate/ automation/ cli/ src/ deploy/ -name "*.php" -print0 2>/dev/null)
|
done < <(find lib/ validate/ automation/ cli/ source/ src/ deploy/ -name "*.php" -print0 2>/dev/null)
|
||||||
|
|
||||||
{
|
{
|
||||||
echo "### PHP Syntax"
|
echo "### PHP Syntax"
|
||||||
@@ -270,7 +270,7 @@ jobs:
|
|||||||
echo "::warning file=${file}::Missing SPDX header"
|
echo "::warning file=${file}::Missing SPDX header"
|
||||||
MISSING=$((MISSING + 1))
|
MISSING=$((MISSING + 1))
|
||||||
fi
|
fi
|
||||||
done < <(find lib/ validate/ cli/ src/ automation/ deploy/ -name "*.php" -print0 2>/dev/null)
|
done < <(find lib/ validate/ cli/ source/ src/ automation/ deploy/ -name "*.php" -print0 2>/dev/null)
|
||||||
|
|
||||||
{
|
{
|
||||||
echo "### License Headers"
|
echo "### License Headers"
|
||||||
@@ -289,7 +289,7 @@ jobs:
|
|||||||
echo "::error file=${file}::Potential hardcoded secret detected"
|
echo "::error file=${file}::Potential hardcoded secret detected"
|
||||||
FOUND=$((FOUND + 1))
|
FOUND=$((FOUND + 1))
|
||||||
fi
|
fi
|
||||||
done < <(find lib/ validate/ cli/ src/ automation/ deploy/ -name "*.php" -print0 2>/dev/null)
|
done < <(find lib/ validate/ cli/ source/ src/ automation/ deploy/ -name "*.php" -print0 2>/dev/null)
|
||||||
|
|
||||||
{
|
{
|
||||||
echo "### Secret Detection"
|
echo "### Secret Detection"
|
||||||
@@ -412,10 +412,16 @@ jobs:
|
|||||||
if: always()
|
if: always()
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
sparse-checkout: automation/ci-issue-reporter.sh
|
||||||
|
sparse-checkout-cone-mode: false
|
||||||
|
|
||||||
- name: Check gate results
|
- name: Check gate results
|
||||||
run: |
|
run: |
|
||||||
{
|
{
|
||||||
echo "# moko-platform CI"
|
echo "# mokocli CI"
|
||||||
echo ""
|
echo ""
|
||||||
echo "| Gate | Job | Status |"
|
echo "| Gate | Job | Status |"
|
||||||
echo "|---|---|---|"
|
echo "|---|---|---|"
|
||||||
@@ -437,3 +443,46 @@ jobs:
|
|||||||
echo "::error::One or more CI gates failed"
|
echo "::error::One or more CI gates failed"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
- name: "File issues for failed gates"
|
||||||
|
if: >-
|
||||||
|
always() &&
|
||||||
|
(needs.code-quality.result == 'failure' ||
|
||||||
|
needs.tests.result == 'failure' ||
|
||||||
|
needs.self-health.result == 'failure' ||
|
||||||
|
needs.governance.result == 'failure' ||
|
||||||
|
needs.templates.result == 'failure')
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
run: |
|
||||||
|
chmod +x automation/ci-issue-reporter.sh
|
||||||
|
REPORTER="./automation/ci-issue-reporter.sh"
|
||||||
|
WF="Platform CI"
|
||||||
|
|
||||||
|
report_gate() {
|
||||||
|
local gate="$1" result="$2" details="$3"
|
||||||
|
if [ "$result" = "failure" ]; then
|
||||||
|
"$REPORTER" --gate "$gate" --details "$details" --workflow "$WF" --severity error
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
report_gate "Code Quality" \
|
||||||
|
"${{ needs.code-quality.result }}" \
|
||||||
|
"PHPCS (PSR-12), PHPStan, or PHP syntax checks failed. Run \`composer check\` locally to reproduce."
|
||||||
|
|
||||||
|
report_gate "Unit Tests" \
|
||||||
|
"${{ needs.tests.result }}" \
|
||||||
|
"PHPUnit tests failed on one or more PHP versions (8.1, 8.2, 8.3). Run \`vendor/bin/phpunit --testdox\` locally."
|
||||||
|
|
||||||
|
report_gate "Self-Health" \
|
||||||
|
"${{ needs.self-health.result }}" \
|
||||||
|
"Self-health score fell below the 80% threshold. Run \`php bin/moko health -- --path .\` locally."
|
||||||
|
|
||||||
|
report_gate "Governance" \
|
||||||
|
"${{ needs.governance.result }}" \
|
||||||
|
"Governance checks failed (license headers, secrets, or version consistency). Check the CI run summary for specifics."
|
||||||
|
|
||||||
|
report_gate "Template Integrity" \
|
||||||
|
"${{ needs.templates.result }}" \
|
||||||
|
"Workflow or gitignore templates failed YAML validation or are missing required entries."
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: moko-platform.Maintenance
|
# INGROUP: MokoCli.Maintenance
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoCli
|
||||||
# PATH: /.gitea/workflows/cleanup.yml
|
# PATH: /.gitea/workflows/cleanup.yml
|
||||||
# VERSION: 09.23.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
|
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
|
||||||
|
|
||||||
name: "Universal: Repository Cleanup"
|
name: "Universal: Repository Cleanup"
|
||||||
@@ -33,17 +33,17 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
token: ${{ secrets.GA_TOKEN }}
|
||||||
|
|
||||||
- name: Delete merged branches
|
- name: Delete merged branches
|
||||||
env:
|
env:
|
||||||
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
echo "=== Merged Branch Cleanup ==="
|
echo "=== Merged Branch Cleanup ==="
|
||||||
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||||
|
|
||||||
# List branches via API
|
# List branches via API
|
||||||
BRANCHES=$(curl -sS -H "Authorization: token ${GITEA_TOKEN}" \
|
BRANCHES=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
|
||||||
"${API}/branches?limit=50" | jq -r '.[].name')
|
"${API}/branches?limit=50" | jq -r '.[].name')
|
||||||
|
|
||||||
DELETED=0
|
DELETED=0
|
||||||
@@ -56,7 +56,7 @@ jobs:
|
|||||||
# Check if branch is merged into main
|
# Check if branch is merged into main
|
||||||
if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then
|
if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then
|
||||||
echo " Deleting merged branch: ${BRANCH}"
|
echo " Deleting merged branch: ${BRANCH}"
|
||||||
curl -sS -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
|
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
|
||||||
"${API}/branches/${BRANCH}" 2>/dev/null || true
|
"${API}/branches/${BRANCH}" 2>/dev/null || true
|
||||||
DELETED=$((DELETED + 1))
|
DELETED=$((DELETED + 1))
|
||||||
fi
|
fi
|
||||||
@@ -66,20 +66,20 @@ jobs:
|
|||||||
|
|
||||||
- name: Clean old workflow runs
|
- name: Clean old workflow runs
|
||||||
env:
|
env:
|
||||||
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
echo "=== Workflow Run Cleanup ==="
|
echo "=== Workflow Run Cleanup ==="
|
||||||
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||||
CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ)
|
CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ)
|
||||||
|
|
||||||
# Get old completed runs
|
# Get old completed runs
|
||||||
RUNS=$(curl -sS -H "Authorization: token ${GITEA_TOKEN}" \
|
RUNS=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
|
||||||
"${API}/actions/runs?status=completed&limit=50" | \
|
"${API}/actions/runs?status=completed&limit=50" | \
|
||||||
jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null)
|
jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null)
|
||||||
|
|
||||||
DELETED=0
|
DELETED=0
|
||||||
for RUN_ID in $RUNS; do
|
for RUN_ID in $RUNS; do
|
||||||
curl -sS -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
|
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
|
||||||
"${API}/actions/runs/${RUN_ID}" 2>/dev/null || true
|
"${API}/actions/runs/${RUN_ID}" 2>/dev/null || true
|
||||||
DELETED=$((DELETED + 1))
|
DELETED=$((DELETED + 1))
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
name: "Publish to Composer"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
- '[0-9]*.[0-9]*.[0-9]*'
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
name: Publish Package
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: >-
|
||||||
|
!contains(github.event.head_commit.message, '[skip ci]') &&
|
||||||
|
!contains(github.event.head_commit.message, '[skip publish]')
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
run: |
|
||||||
|
if ! command -v php &> /dev/null; then
|
||||||
|
sudo apt-get update -qq
|
||||||
|
sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: composer install --no-dev --no-interaction --prefer-dist --quiet
|
||||||
|
|
||||||
|
- name: Determine version
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
VERSION=$(php -r "echo json_decode(file_get_contents('composer.json'))->version;")
|
||||||
|
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Package version: ${VERSION}"
|
||||||
|
|
||||||
|
# Gitea Composer Registry — auto-publishes from tags
|
||||||
|
# The tag push itself registers the package at:
|
||||||
|
# https://git.mokoconsulting.tech/api/packages/MokoConsulting/composer
|
||||||
|
- name: Verify Gitea registry
|
||||||
|
run: |
|
||||||
|
echo "Gitea Composer registry auto-publishes from tags."
|
||||||
|
echo "Package available at: ${GITEA_URL}/api/packages/MokoConsulting/composer"
|
||||||
|
echo "Install: composer require mokoconsulting/mokocli"
|
||||||
|
|
||||||
|
# Packagist — notify of new version
|
||||||
|
- name: Notify Packagist
|
||||||
|
if: secrets.PACKAGIST_TOKEN != ''
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
|
echo "Notifying Packagist of version ${VERSION}..."
|
||||||
|
curl -sf -X POST \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"repository":{"url":"https://git.mokoconsulting.tech/MokoConsulting/mokocli"}}' \
|
||||||
|
"https://packagist.org/api/update-package?username=mokoconsulting&apiToken=${{ secrets.PACKAGIST_TOKEN }}" \
|
||||||
|
&& echo "Packagist notified" \
|
||||||
|
|| echo "::warning::Packagist notification failed (package may not be registered yet)"
|
||||||
|
|
||||||
|
- name: Summary
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
|
echo "## Composer Package Published" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Registry | Status |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "|----------|--------|" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Gitea | \`composer require mokoconsulting/mokocli:${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Packagist | \`composer require mokoconsulting/mokocli\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: MokoCli.Deploy
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
|
# 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 MokoCli 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}/mokocli.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
|
||||||
@@ -4,10 +4,10 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: moko-platform.Security
|
# INGROUP: MokoCli.Security
|
||||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli
|
||||||
# PATH: /templates/workflows/gitleaks.yml.template
|
# PATH: /templates/workflows/gitleaks.yml.template
|
||||||
# VERSION: 09.23.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
|
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
|
||||||
#
|
#
|
||||||
# +========================================================================+
|
# +========================================================================+
|
||||||
@@ -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: moko-platform.Automation
|
# INGROUP: mokocli.Automation
|
||||||
# VERSION: 09.23.00
|
# VERSION: 09.38.01
|
||||||
# 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,10 +4,10 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: moko-platform.Notifications
|
# INGROUP: MokoCli.Notifications
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoCli
|
||||||
# PATH: /.gitea/workflows/notify.yml
|
# PATH: /.gitea/workflows/notify.yml
|
||||||
# VERSION: 09.23.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Push notifications via ntfy on release success or workflow failure
|
# BRIEF: Push notifications via ntfy on release success or workflow failure
|
||||||
|
|
||||||
name: "Universal: Notifications"
|
name: "Universal: Notifications"
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: moko-platform.CI
|
# INGROUP: mokocli.CI
|
||||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli
|
||||||
# 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
|
||||||
|
|||||||
@@ -4,19 +4,26 @@
|
|||||||
#
|
#
|
||||||
# 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: /templates/workflows/universal/pre-release.yml.template
|
# PATH: /templates/workflows/universal/pre-release.yml.template
|
||||||
# VERSION: 09.23.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
|
||||||
|
- 'fix/**'
|
||||||
|
- 'patch/**'
|
||||||
|
- 'hotfix/**'
|
||||||
|
- 'bugfix/**'
|
||||||
|
- 'chore/**'
|
||||||
|
- alpha
|
||||||
|
- beta
|
||||||
|
- rc
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
stability:
|
stability:
|
||||||
@@ -39,11 +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.pull_request.merged == true && github.event.pull_request.base.ref == 'dev')
|
github.event_name == 'push'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -51,56 +58,81 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
ref: ${{ github.ref_name }}
|
||||||
|
|
||||||
- name: Setup moko-platform 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 ! command -v composer &> /dev/null; then
|
# Use pre-installed /opt/mokocli if available (updated by cron every 6h)
|
||||||
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
|
if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/cli/manifest_element.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then
|
||||||
|
echo Using pre-installed /opt/mokocli
|
||||||
|
echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo Falling back to fresh clone
|
||||||
|
if ! command -v composer > /dev/null 2>&1; then
|
||||||
|
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
|
||||||
|
fi
|
||||||
|
rm -rf /tmp/mokocli
|
||||||
|
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git
|
||||||
|
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
|
||||||
|
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
|
||||||
|
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
|
||||||
fi
|
fi
|
||||||
# Always fetch latest CLI tools — never use stale cache from previous runs
|
|
||||||
rm -rf /tmp/moko-platform-api
|
|
||||||
git clone --depth 1 --branch main --quiet \
|
|
||||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
|
||||||
/tmp/moko-platform-api
|
|
||||||
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
|
|
||||||
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
|
|
||||||
|
|
||||||
- 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: Resolve metadata and bump version
|
- name: Resolve metadata and bump version
|
||||||
id: meta
|
id: meta
|
||||||
run: |
|
run: |
|
||||||
|
# Auto-detect stability from branch name on push, or use input on dispatch
|
||||||
|
if [ "${{ github.event_name }}" = "push" ]; then
|
||||||
|
case "${{ github.ref_name }}" in
|
||||||
|
rc) STABILITY="release-candidate" ;;
|
||||||
|
alpha) STABILITY="alpha" ;;
|
||||||
|
beta) STABILITY="beta" ;;
|
||||||
|
*) STABILITY="development" ;;
|
||||||
|
esac
|
||||||
|
else
|
||||||
STABILITY="${{ inputs.stability || 'development' }}"
|
STABILITY="${{ inputs.stability || 'development' }}"
|
||||||
|
fi
|
||||||
|
|
||||||
case "$STABILITY" in
|
case "$STABILITY" in
|
||||||
development) TAG="development" ;;
|
development) SUFFIX="-dev"; TAG="development" ;;
|
||||||
alpha) TAG="alpha" ;;
|
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
|
||||||
beta) TAG="beta" ;;
|
beta) SUFFIX="-beta"; TAG="beta" ;;
|
||||||
release-candidate) TAG="release-candidate" ;;
|
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# Bump version: patch for dev/alpha/beta, minor for RC
|
# Bump version via CLI: patch for dev/alpha/beta, minor for RC
|
||||||
case "$STABILITY" in
|
case "$STABILITY" in
|
||||||
release-candidate) php ${MOKO_CLI}/version_bump.php --path . --minor 2>/dev/null || true ;;
|
release-candidate) BUMP="minor" ;;
|
||||||
*) php ${MOKO_CLI}/version_bump.php --path . 2>/dev/null || true ;;
|
*) BUMP="patch" ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# Set stability suffix and fix consistency
|
php ${MOKO_CLI}/version_bump.php --path . $([ "$BUMP" = "minor" ] && echo "--minor") 2>/dev/null || true
|
||||||
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo '00.00.01')
|
|
||||||
|
# Set stability suffix and verify consistency
|
||||||
|
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "00.00.01")
|
||||||
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
||||||
|
|
||||||
php ${MOKO_CLI}/version_set_platform.php \
|
php ${MOKO_CLI}/version_set_platform.php \
|
||||||
--path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true
|
--path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true
|
||||||
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
|
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
|
||||||
|
|
||||||
# Read final version with suffix
|
# Ensure licensing tags (updateservers, dlid) if enabled in manifest.xml
|
||||||
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null)
|
php ${MOKO_CLI}/manifest_licensing.php --path . --fix 2>/dev/null || true
|
||||||
[ -z "$VERSION" ] && VERSION="00.00.01"
|
|
||||||
|
# Append suffix for output
|
||||||
|
if [ -n "$SUFFIX" ]; then
|
||||||
|
VERSION="${VERSION}${SUFFIX}"
|
||||||
|
fi
|
||||||
|
|
||||||
# Commit version bump
|
# Commit version bump
|
||||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
@@ -125,11 +157,12 @@ jobs:
|
|||||||
|
|
||||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
|
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
|
||||||
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||||
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
|
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
|
||||||
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
|
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION} ==="
|
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
|
||||||
|
|
||||||
- name: Create release
|
- name: Create release
|
||||||
id: release
|
id: release
|
||||||
@@ -140,7 +173,42 @@ 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
|
||||||
|
run: |
|
||||||
|
TAG="${{ steps.meta.outputs.tag }}"
|
||||||
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
|
||||||
|
# Extract [Unreleased] section from changelog (everything between [Unreleased] and next ## heading)
|
||||||
|
if [ -f "CHANGELOG.md" ]; then
|
||||||
|
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
|
||||||
|
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
|
||||||
|
else
|
||||||
|
NOTES="Release ${VERSION}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Update release body via API
|
||||||
|
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
|
"${API_BASE}/releases/tags/${TAG}" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [ -n "$RELEASE_ID" ]; then
|
||||||
|
python3 -c "
|
||||||
|
import json, urllib.request
|
||||||
|
body = open('/dev/stdin').read()
|
||||||
|
payload = json.dumps({'body': body}).encode()
|
||||||
|
req = urllib.request.Request(
|
||||||
|
'${API_BASE}/releases/${RELEASE_ID}',
|
||||||
|
data=payload, method='PATCH',
|
||||||
|
headers={
|
||||||
|
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
})
|
||||||
|
urllib.request.urlopen(req)
|
||||||
|
" <<< "$NOTES"
|
||||||
|
echo "Release notes updated from CHANGELOG.md"
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Build package and upload
|
- name: Build package and upload
|
||||||
id: package
|
id: package
|
||||||
|
|||||||
@@ -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: moko-platform.Validation
|
# INGROUP: mokocli.Validation
|
||||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
# 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
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: moko-platform.Security
|
# INGROUP: MokoCli.Security
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoCli
|
||||||
# PATH: /.gitea/workflows/security-audit.yml
|
# PATH: /.gitea/workflows/security-audit.yml
|
||||||
# VERSION: 09.23.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Dependency vulnerability scanning for composer and npm packages
|
# BRIEF: Dependency vulnerability scanning for composer and npm packages
|
||||||
|
|
||||||
name: "Universal: Security Audit"
|
name: "Universal: Security Audit"
|
||||||
@@ -80,19 +80,3 @@ jobs:
|
|||||||
-H "Priority: high" \
|
-H "Priority: high" \
|
||||||
-d "Security audit found vulnerabilities. Review dependency updates." \
|
-d "Security audit found vulnerabilities. Review dependency updates." \
|
||||||
"${NTFY_URL}/${NTFY_TOPIC}" || true
|
"${NTFY_URL}/${NTFY_TOPIC}" || true
|
||||||
|
|
||||||
|
|
||||||
- name: Joomla version audit
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
if [ -f "monitoring/joomla-version-audit.php" ] && [ -n "$JOOMLA_SITES" ]; then
|
|
||||||
echo "$JOOMLA_SITES" > /tmp/sites.json
|
|
||||||
php monitoring/joomla-version-audit.php --sites /tmp/sites.json || true
|
|
||||||
echo "### Joomla Version Audit" >> $GITHUB_STEP_SUMMARY
|
|
||||||
rm -f /tmp/sites.json
|
|
||||||
else
|
|
||||||
echo "Joomla audit skipped (no script or JOOMLA_SITES_JSON not configured)"
|
|
||||||
fi
|
|
||||||
env:
|
|
||||||
JOOMLA_SITES: ${{ vars.JOOMLA_SITES_JSON }}
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: mokocli.Automation
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
|
# PATH: /.mokogitea/workflows/sync-feature-versions.yml
|
||||||
|
# VERSION: 01.00.00
|
||||||
|
# BRIEF: Merge dev into open feature branches after version bumps
|
||||||
|
|
||||||
|
name: "Universal: Sync Feature Branch Versions"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
env:
|
||||||
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sync:
|
||||||
|
name: Sync feature branches with dev
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: >-
|
||||||
|
github.event_name == 'workflow_dispatch' ||
|
||||||
|
contains(github.event.head_commit.message, 'chore(version)')
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout dev
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
ref: dev
|
||||||
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
|
||||||
|
- name: Configure git
|
||||||
|
run: |
|
||||||
|
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"
|
||||||
|
|
||||||
|
- name: Merge dev into feature branches
|
||||||
|
run: |
|
||||||
|
echo "=== Syncing feature branches with dev ==="
|
||||||
|
|
||||||
|
# Fetch all remote branches
|
||||||
|
git fetch origin
|
||||||
|
|
||||||
|
# Find feature branches (feature/*, fix/*, patch/*, hotfix/*, bugfix/*, chore/*)
|
||||||
|
BRANCHES=$(git branch -r --list 'origin/feature/*' 'origin/fix/*' 'origin/patch/*' 'origin/hotfix/*' 'origin/bugfix/*' 'origin/chore/*' | sed 's|origin/||; s/^[[:space:]]*//')
|
||||||
|
|
||||||
|
if [ -z "$BRANCHES" ]; then
|
||||||
|
echo "No feature branches found — nothing to sync"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
SYNCED=0
|
||||||
|
SKIPPED=0
|
||||||
|
FAILED=0
|
||||||
|
|
||||||
|
for BRANCH in $BRANCHES; do
|
||||||
|
echo ""
|
||||||
|
echo "--- ${BRANCH} ---"
|
||||||
|
|
||||||
|
# Skip branches that are already up to date with dev
|
||||||
|
if git merge-base --is-ancestor dev "origin/${BRANCH}" 2>/dev/null; then
|
||||||
|
echo "Already up to date"
|
||||||
|
SKIPPED=$((SKIPPED + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Try to merge dev into the branch
|
||||||
|
git checkout "origin/${BRANCH}" -B "$BRANCH" 2>/dev/null
|
||||||
|
if git merge dev --no-edit -m "chore: merge dev into ${BRANCH} (version sync) [skip ci]" 2>/dev/null; then
|
||||||
|
git push origin "$BRANCH" 2>/dev/null
|
||||||
|
echo "Synced successfully"
|
||||||
|
SYNCED=$((SYNCED + 1))
|
||||||
|
else
|
||||||
|
git merge --abort 2>/dev/null || true
|
||||||
|
echo "Merge conflict — skipping (manual rebase needed)"
|
||||||
|
FAILED=$((FAILED + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Return to dev
|
||||||
|
git checkout dev 2>/dev/null || true
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Summary ==="
|
||||||
|
echo "Synced: ${SYNCED}"
|
||||||
|
echo "Already current: ${SKIPPED}"
|
||||||
|
echo "Conflicts (skipped): ${FAILED}"
|
||||||
|
|
||||||
|
if [ "$FAILED" -gt 0 ]; then
|
||||||
|
echo "::warning::${FAILED} branch(es) had merge conflicts and need manual rebase"
|
||||||
|
fi
|
||||||
@@ -1,302 +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: 09.23.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"
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
# Gitea release tag per stability
|
|
||||||
case "$STABILITY" in
|
|
||||||
development) TAG="development" ;;
|
|
||||||
alpha) TAG="alpha" ;;
|
|
||||||
beta) TAG="beta" ;;
|
|
||||||
rc) TAG="release-candidate" ;;
|
|
||||||
*) TAG="stable" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# Bump patch, set platform suffix, fix consistency — version_bump preserves suffix
|
|
||||||
php ${MOKO_CLI}/version_set_platform.php \
|
|
||||||
--path . --version "$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo '00.00.01')" \
|
|
||||||
--branch "$BRANCH" --stability "$STABILITY" 2>/dev/null || true
|
|
||||||
php ${MOKO_CLI}/version_bump.php --path . 2>/dev/null || true
|
|
||||||
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
|
|
||||||
|
|
||||||
# Read final version (includes suffix, e.g. 01.02.15-dev)
|
|
||||||
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "00.00.01")
|
|
||||||
|
|
||||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "tag=${TAG}" >> "$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="${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,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}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"generated_at": "2026-03-10T19:51:42.238134Z",
|
"generated_at": "2026-03-10T19:51:42.238134Z",
|
||||||
"repository": "MokoConsulting/moko-platform",
|
"repository": "MokoConsulting/mokocli",
|
||||||
"version": "1.0.0"
|
"version": "1.0.0"
|
||||||
},
|
},
|
||||||
"scripts": [
|
"scripts": [
|
||||||
|
|||||||
+9
-22
@@ -2,37 +2,24 @@
|
|||||||
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
SPDX-License-Identifier: GPL-3.0-or-later
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
FILE INFORMATION
|
FILE INFORMATION
|
||||||
DEFGROUP: MokoStandards.Root
|
DEFGROUP: MokoCli.Root
|
||||||
INGROUP: MokoStandards
|
INGROUP: MokoCli
|
||||||
REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
PATH: /CHANGELOG.md
|
PATH: /CHANGELOG.md
|
||||||
BRIEF: Release changelog
|
BRIEF: Release changelog
|
||||||
-->
|
-->
|
||||||
|
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
## [09.23] --- 2026-05-31
|
## [09.37.00] --- 2026-06-21
|
||||||
|
|
||||||
## [09.22] --- 2026-05-31
|
## [09.37.00] --- 2026-06-21
|
||||||
|
|
||||||
### Changed
|
## [09.36.00] --- 2026-06-21
|
||||||
- **refactor(cli):** migrate 64 legacy scripts to CliFramework (#235) — all tools in cli/, automation/, maintenance/, deploy/, release/ now extend CliFramework with free --help, --verbose, --quiet, --dry-run, --json, banners, and coloured logging
|
|
||||||
|
|
||||||
### Fixed
|
## [09.36.00] --- 2026-06-21
|
||||||
- fix: auto-detect org/repo in updates_xml_build from manifest and git remote
|
|
||||||
- fix: restore hyphen in version suffixes
|
|
||||||
- fix: release names use standardized format
|
|
||||||
- fix: remove lesser stream copies, each stream updates independently
|
|
||||||
- fix: sort updates.xml entries dev first, stable last
|
|
||||||
|
|
||||||
## [09.21] --- 2026-05-30
|
## [09.35.00] --- 2026-06-21
|
||||||
|
|
||||||
## [09.20] --- 2026-05-30
|
## [09.35.00] --- 2026-06-21
|
||||||
|
|
||||||
## [09.19] --- 2026-05-30
|
|
||||||
|
|
||||||
## [09.18] --- 2026-05-30
|
|
||||||
|
|
||||||
## [09.17] --- 2026-05-30
|
|
||||||
|
|||||||
@@ -1,102 +0,0 @@
|
|||||||
# CLAUDE.md
|
|
||||||
|
|
||||||
This file provides guidance to Claude Code when working with this repository.
|
|
||||||
|
|
||||||
## Project Overview
|
|
||||||
|
|
||||||
**moko-platform** — Enterprise automation, validation, sync, and governance engine for all Moko Consulting repositories
|
|
||||||
|
|
||||||
| Field | Value |
|
|
||||||
|---|---|
|
|
||||||
| **Language** | PHP 8.1+ |
|
|
||||||
| **Default branch** | main |
|
|
||||||
| **License** | GPL-3.0-or-later |
|
|
||||||
| **Version** | 09.01.00 |
|
|
||||||
| **Wiki** | [moko-platform Wiki](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki) |
|
|
||||||
|
|
||||||
## Common Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
composer install # Install PHP dependencies
|
|
||||||
php bin/moko health --path . # Run repo health check
|
|
||||||
php bin/moko check:syntax --path . # PHP syntax check
|
|
||||||
php bin/moko drift --org MokoConsulting # Scan for standards drift
|
|
||||||
php bin/moko dashboard --token $TOKEN -o dashboard.html # Generate client dashboard
|
|
||||||
|
|
||||||
# Code quality
|
|
||||||
php vendor/bin/phpcs --standard=phpcs.xml -n lib/ validate/ automation/ cli/
|
|
||||||
php vendor/bin/phpcbf --standard=phpcs.xml lib/ validate/ automation/ cli/
|
|
||||||
php vendor/bin/phpstan analyse -c phpstan.neon --memory-limit=512M
|
|
||||||
|
|
||||||
# Run all checks
|
|
||||||
composer check
|
|
||||||
```
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
### Directory Layout
|
|
||||||
|
|
||||||
| Directory | Purpose |
|
|
||||||
|-----------|---------|
|
|
||||||
| `cli/` | 32 standalone CLI tools (version, release, build, repo management) |
|
|
||||||
| `validate/` | 20 validation scripts (syntax, structure, manifests, drift) |
|
|
||||||
| `automation/` | 7 bulk operations (sync, push files, templates, cleanup) |
|
|
||||||
| `lib/Enterprise/` | Core library — CliFramework, ApiClient, adapters, validators |
|
|
||||||
| `lib/Enterprise/Plugins/` | 11 platform plugins (Joomla, Dolibarr, Node.js, Python, etc.) |
|
|
||||||
| `deploy/` | SFTP deployment scripts (Joomla, Dolibarr, health checks) |
|
|
||||||
| `templates/` | Universal templates, configs, governance schema |
|
|
||||||
| `.mokogitea/workflows/` | CI/CD workflows (Gitea Actions) |
|
|
||||||
| `bin/moko` | Unified CLI dispatcher — runs any tool via `php bin/moko <command>` |
|
|
||||||
|
|
||||||
### CLI Framework
|
|
||||||
|
|
||||||
All CLI tools extend `MokoEnterprise\CliFramework` (defined in `lib/Enterprise/CliFramework.php`).
|
|
||||||
|
|
||||||
Pattern for new tools:
|
|
||||||
```php
|
|
||||||
class MyTool extends CliFramework {
|
|
||||||
protected function configure(): void {
|
|
||||||
$this->setDescription('What this tool does');
|
|
||||||
$this->addArgument('--name', 'Description', 'default');
|
|
||||||
}
|
|
||||||
protected function run(): int {
|
|
||||||
$name = $this->getArgument('--name');
|
|
||||||
// ... business logic ...
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$app = new MyTool();
|
|
||||||
exit($app->execute());
|
|
||||||
```
|
|
||||||
|
|
||||||
Built-in flags: `--help`, `--verbose`, `--quiet`, `--dry-run`
|
|
||||||
|
|
||||||
### Platform Adapters
|
|
||||||
|
|
||||||
Git operations are abstracted via `GitPlatformAdapter` interface:
|
|
||||||
- `MokoGiteaAdapter` — for git.mokoconsulting.tech (primary)
|
|
||||||
- `GitHubAdapter` — for github.com mirrors
|
|
||||||
|
|
||||||
### Plugin System
|
|
||||||
|
|
||||||
Platform-specific logic lives in `lib/Enterprise/Plugins/`. Each plugin implements `ProjectPluginInterface` with methods for health checks, validation, build commands, and config schemas.
|
|
||||||
|
|
||||||
## Code Quality
|
|
||||||
|
|
||||||
| Tool | Level | Config |
|
|
||||||
|------|-------|--------|
|
|
||||||
| PHPCS | PSR-12 (errors only) | `phpcs.xml` |
|
|
||||||
| PHPStan | Level 2 | `phpstan.neon` |
|
|
||||||
|
|
||||||
PHPStan runs with `--memory-limit=512M` due to large codebase. CI enforces PHPCS errors; PHPStan is advisory (`continue-on-error`).
|
|
||||||
|
|
||||||
## Rules
|
|
||||||
|
|
||||||
- **Workflow directory**: `.mokogitea/` (not `.gitea/` or `.github/`)
|
|
||||||
- **Never commit** `.claude/`, `.mcp.json`, `TODO.md`, or `*.min.css`/`*.min.js`
|
|
||||||
- **Attribution**: use `Authored-by: Moko Consulting` in commits
|
|
||||||
- **Branch strategy**: develop on `dev`, merge to `main` for release
|
|
||||||
- **Minification**: handled at build time (CI) and runtime (MokoMinifyHelper for Joomla templates)
|
|
||||||
- **Wiki**: documentation lives in the Gitea wiki, not in `docs/` files
|
|
||||||
- **New CLI tools**: extend `CliFramework`, not `CLIApp` (legacy)
|
|
||||||
- **After adding a CLI tool**: register it in `bin/moko` COMMAND_MAP
|
|
||||||
+2
-2
@@ -4,14 +4,14 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
FILE INFORMATION
|
FILE INFORMATION
|
||||||
DEFGROUP: MokoPlatform.Root
|
DEFGROUP: MokoPlatform.Root
|
||||||
INGROUP: MokoPlatform
|
INGROUP: MokoPlatform
|
||||||
REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
PATH: /PLUGIN_SCRIPTS.md
|
PATH: /PLUGIN_SCRIPTS.md
|
||||||
BRIEF: Plugin system CLI documentation
|
BRIEF: Plugin system CLI documentation
|
||||||
-->
|
-->
|
||||||
|
|
||||||
# Plugin System CLI Scripts
|
# Plugin System CLI Scripts
|
||||||
|
|
||||||
Command-line scripts for validating, health checking, and managing projects using the moko-platform plugin system.
|
Command-line scripts for validating, health checking, and managing projects using the mokocli plugin system.
|
||||||
|
|
||||||
## Available Scripts
|
## Available Scripts
|
||||||
|
|
||||||
|
|||||||
@@ -4,20 +4,20 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
FILE INFORMATION
|
FILE INFORMATION
|
||||||
DEFGROUP: MokoPlatform.Root
|
DEFGROUP: MokoPlatform.Root
|
||||||
INGROUP: MokoPlatform
|
INGROUP: MokoPlatform
|
||||||
REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
PATH: /README.md
|
PATH: /README.md
|
||||||
VERSION: 09.23.00
|
VERSION: 09.38.01
|
||||||
BRIEF: Project overview and documentation
|
BRIEF: Project overview and documentation
|
||||||
-->
|
-->
|
||||||
|
|
||||||
# moko-platform Enterprise API
|
# mokocli Enterprise API
|
||||||
|
|
||||||
  
|
  
|
||||||
|
|
||||||
PHP implementation of moko-platform — enterprise standards, automation framework, workflow templates, and bulk sync tooling.
|
PHP implementation of mokocli — enterprise standards, automation framework, workflow templates, and bulk sync tooling.
|
||||||
|
|
||||||
> **Primary platform**: [Gitea — git.mokoconsulting.tech](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API)
|
> **Primary platform**: [Gitea — git.mokoconsulting.tech](https://git.mokoconsulting.tech/MokoConsulting/MokoCli-API)
|
||||||
> **Backup mirror**: [GitHub](https://github.com/MokoConsulting/MokoStandards-API) *(read-only mirror)*
|
> **Backup mirror**: [GitHub](https://github.com/MokoConsulting/MokoCli-API) *(read-only mirror)*
|
||||||
|
|
||||||
## What Lives Here
|
## What Lives Here
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
FILE INFORMATION
|
FILE INFORMATION
|
||||||
DEFGROUP: MokoPlatform.Index
|
DEFGROUP: MokoPlatform.Index
|
||||||
INGROUP: MokoPlatform.Analysis
|
INGROUP: MokoPlatform.Analysis
|
||||||
REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
PATH: /analysis/index.md
|
PATH: /analysis/index.md
|
||||||
BRIEF: Analysis directory index
|
BRIEF: Analysis directory index
|
||||||
-->
|
-->
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: MokoPlatform.Automation
|
* DEFGROUP: MokoPlatform.Automation
|
||||||
* INGROUP: MokoPlatform.Scripts
|
* INGROUP: MokoPlatform.Scripts
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /automation/bulk_joomla_template.php
|
* PATH: /automation/bulk_joomla_template.php
|
||||||
* BRIEF: Bulk scaffold and sync Joomla template repositories
|
* BRIEF: Bulk scaffold and sync Joomla template repositories
|
||||||
*
|
*
|
||||||
@@ -28,7 +28,7 @@ declare(strict_types=1);
|
|||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\{
|
use MokoCli\{
|
||||||
AuditLogger,
|
AuditLogger,
|
||||||
CliFramework,
|
CliFramework,
|
||||||
Config,
|
Config,
|
||||||
@@ -42,7 +42,7 @@ use MokoEnterprise\{
|
|||||||
*
|
*
|
||||||
* Provides three operations for Joomla template projects:
|
* Provides three operations for Joomla template projects:
|
||||||
* --scaffold: Create a new template repository with the full directory structure
|
* --scaffold: Create a new template repository with the full directory structure
|
||||||
* --sync: Push moko-platform files to existing template repositories
|
* --sync: Push mokocli files to existing template repositories
|
||||||
* --list: List all repositories tagged as joomla-template
|
* --list: List all repositories tagged as joomla-template
|
||||||
*
|
*
|
||||||
* Works with both GitHub and Gitea via the PlatformAdapterFactory.
|
* Works with both GitHub and Gitea via the PlatformAdapterFactory.
|
||||||
@@ -318,7 +318,7 @@ class BulkJoomlaTemplate extends CliFramework
|
|||||||
$name,
|
$name,
|
||||||
$path,
|
$path,
|
||||||
$content,
|
$content,
|
||||||
"chore: update {$path} from moko-platform",
|
"chore: update {$path} from mokocli",
|
||||||
$existingSha,
|
$existingSha,
|
||||||
$branch
|
$branch
|
||||||
);
|
);
|
||||||
|
|||||||
+41
-41
@@ -11,7 +11,7 @@
|
|||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: MokoPlatform.Automation
|
* DEFGROUP: MokoPlatform.Automation
|
||||||
* INGROUP: MokoPlatform.Scripts
|
* INGROUP: MokoPlatform.Scripts
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /automation/bulk_sync.php
|
* PATH: /automation/bulk_sync.php
|
||||||
* BRIEF: Enterprise-grade bulk repository synchronization
|
* BRIEF: Enterprise-grade bulk repository synchronization
|
||||||
*/
|
*/
|
||||||
@@ -21,7 +21,7 @@ declare(strict_types=1);
|
|||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\{
|
use MokoCli\{
|
||||||
ApiClient,
|
ApiClient,
|
||||||
AuditLogger,
|
AuditLogger,
|
||||||
CheckpointManager,
|
CheckpointManager,
|
||||||
@@ -42,7 +42,7 @@ use MokoEnterprise\{
|
|||||||
/**
|
/**
|
||||||
* Bulk Repository Synchronization Tool
|
* Bulk Repository Synchronization Tool
|
||||||
*
|
*
|
||||||
* Synchronizes moko-platform files across multiple repositories using
|
* Synchronizes mokocli files across multiple repositories using
|
||||||
* the Enterprise library for robust, audited operations.
|
* the Enterprise library for robust, audited operations.
|
||||||
*/
|
*/
|
||||||
class BulkSync extends CliFramework
|
class BulkSync extends CliFramework
|
||||||
@@ -95,7 +95,7 @@ class BulkSync extends CliFramework
|
|||||||
*/
|
*/
|
||||||
protected function run(): int
|
protected function run(): int
|
||||||
{
|
{
|
||||||
$this->log("🚀 moko-platform Bulk Synchronization v" . self::VERSION, 'INFO');
|
$this->log("🚀 mokocli Bulk Synchronization v" . self::VERSION, 'INFO');
|
||||||
|
|
||||||
// Initialize enterprise components
|
// Initialize enterprise components
|
||||||
if (!$this->initializeComponents()) {
|
if (!$this->initializeComponents()) {
|
||||||
@@ -180,7 +180,7 @@ class BulkSync extends CliFramework
|
|||||||
$results['health'] = $this->runHealthChecksAll($org, $repositories);
|
$results['health'] = $this->runHealthChecksAll($org, $repositories);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create/update tracking issue in moko-platform
|
// Create/update tracking issue in mokocli
|
||||||
$this->createSyncIssue($org, $results);
|
$this->createSyncIssue($org, $results);
|
||||||
|
|
||||||
// Create/update a failure issue when any repos failed
|
// Create/update a failure issue when any repos failed
|
||||||
@@ -244,7 +244,7 @@ class BulkSync extends CliFramework
|
|||||||
* Filter repositories based on include/exclude lists
|
* Filter repositories based on include/exclude lists
|
||||||
*/
|
*/
|
||||||
/** Repositories that are permanently excluded from bulk sync. */
|
/** Repositories that are permanently excluded from bulk sync. */
|
||||||
private const ALWAYS_EXCLUDE = ['moko-platform', '.github-private'];
|
private const ALWAYS_EXCLUDE = ['mokocli', '.github-private'];
|
||||||
|
|
||||||
private function filterRepositories(array $repositories, array $include, array $exclude): array
|
private function filterRepositories(array $repositories, array $include, array $exclude): array
|
||||||
{
|
{
|
||||||
@@ -426,7 +426,7 @@ class BulkSync extends CliFramework
|
|||||||
$this->log("", 'ERROR');
|
$this->log("", 'ERROR');
|
||||||
$this->log("Required Implementation:", 'ERROR');
|
$this->log("Required Implementation:", 'ERROR');
|
||||||
$this->log(" 1. Clone/fetch target repository", 'ERROR');
|
$this->log(" 1. Clone/fetch target repository", 'ERROR');
|
||||||
$this->log(" 2. Apply file updates based on moko-platform configuration", 'ERROR');
|
$this->log(" 2. Apply file updates based on mokocli configuration", 'ERROR');
|
||||||
$this->log(" 3. Create pull request with changes", 'ERROR');
|
$this->log(" 3. Create pull request with changes", 'ERROR');
|
||||||
$this->log(" 4. Handle merge conflicts and validation", 'ERROR');
|
$this->log(" 4. Handle merge conflicts and validation", 'ERROR');
|
||||||
$this->log("", 'ERROR');
|
$this->log("", 'ERROR');
|
||||||
@@ -837,7 +837,7 @@ class BulkSync extends CliFramework
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure all standard moko-platform labels exist on a target repository.
|
* Ensure all standard mokocli labels exist on a target repository.
|
||||||
*
|
*
|
||||||
* Fetches existing labels first (GET) and only POSTs the ones that are
|
* Fetches existing labels first (GET) and only POSTs the ones that are
|
||||||
* missing. This avoids the 422 "already exists" responses that would
|
* missing. This avoids the 422 "already exists" responses that would
|
||||||
@@ -872,7 +872,7 @@ class BulkSync extends CliFramework
|
|||||||
|
|
||||||
// Workflow / Process
|
// Workflow / Process
|
||||||
['automation', '8B4513', 'Automated processes or scripts'],
|
['automation', '8B4513', 'Automated processes or scripts'],
|
||||||
['moko-platform', 'B60205', 'moko-platform compliance'],
|
['mokocli', 'B60205', 'mokocli compliance'],
|
||||||
['needs-review', 'FBCA04', 'Awaiting code review'],
|
['needs-review', 'FBCA04', 'Awaiting code review'],
|
||||||
['work-in-progress', 'D93F0B', 'Work in progress, not ready for merge'],
|
['work-in-progress', 'D93F0B', 'Work in progress, not ready for merge'],
|
||||||
['breaking-change', 'D73A4A', 'Breaking API or functionality change'],
|
['breaking-change', 'D73A4A', 'Breaking API or functionality change'],
|
||||||
@@ -912,8 +912,8 @@ class BulkSync extends CliFramework
|
|||||||
['health: poor', 'FF6B6B', 'Health score below 50'],
|
['health: poor', 'FF6B6B', 'Health score below 50'],
|
||||||
|
|
||||||
// Sync / Automation (used by bulk_sync, scan_drift, check_repo_health)
|
// Sync / Automation (used by bulk_sync, scan_drift, check_repo_health)
|
||||||
['standards-update', 'B60205', 'moko-platform sync update'],
|
['standards-update', 'B60205', 'mokocli sync update'],
|
||||||
['standards-drift', 'FBCA04', 'Repository drifted from moko-platform'],
|
['standards-drift', 'FBCA04', 'Repository drifted from mokocli'],
|
||||||
['sync-report', '0075CA', 'Bulk sync run report'],
|
['sync-report', '0075CA', 'Bulk sync run report'],
|
||||||
['sync-failure', 'D73A4A', 'Bulk sync failure requiring attention'],
|
['sync-failure', 'D73A4A', 'Bulk sync failure requiring attention'],
|
||||||
['push-failure', 'D73A4A', 'File push failure requiring attention'],
|
['push-failure', 'D73A4A', 'File push failure requiring attention'],
|
||||||
@@ -925,10 +925,10 @@ class BulkSync extends CliFramework
|
|||||||
['type: version', '0E8A16', 'Version-related change'],
|
['type: version', '0E8A16', 'Version-related change'],
|
||||||
];
|
];
|
||||||
|
|
||||||
// Quick check: if the repo already has the 'moko-platform' label, it was
|
// Quick check: if the repo already has the 'mokocli' label, it was
|
||||||
// provisioned previously — skip the expensive full label provisioning.
|
// provisioned previously — skip the expensive full label provisioning.
|
||||||
try {
|
try {
|
||||||
$probe = $this->api->get("/repos/{$org}/{$repo}/labels/moko-platform");
|
$probe = $this->api->get("/repos/{$org}/{$repo}/labels/mokocli");
|
||||||
if (!empty($probe['name'])) {
|
if (!empty($probe['name'])) {
|
||||||
return; // already provisioned
|
return; // already provisioned
|
||||||
}
|
}
|
||||||
@@ -1024,7 +1024,7 @@ class BulkSync extends CliFramework
|
|||||||
*/
|
*/
|
||||||
private function updateOpenBranches(string $org, string $repo): void
|
private function updateOpenBranches(string $org, string $repo): void
|
||||||
{
|
{
|
||||||
$syncBranchPrefix = 'chore/sync-moko-platform-';
|
$syncBranchPrefix = 'chore/sync-mokocli-';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$defaultBranch = 'main';
|
$defaultBranch = 'main';
|
||||||
@@ -1055,7 +1055,7 @@ class BulkSync extends CliFramework
|
|||||||
$this->api->post("/repos/{$org}/{$repo}/merges", [
|
$this->api->post("/repos/{$org}/{$repo}/merges", [
|
||||||
'base' => $branch,
|
'base' => $branch,
|
||||||
'head' => $defaultBranch,
|
'head' => $defaultBranch,
|
||||||
'commit_message' => "chore: merge {$defaultBranch} into {$branch} (moko-platform sync)",
|
'commit_message' => "chore: merge {$defaultBranch} into {$branch} (mokocli sync)",
|
||||||
]);
|
]);
|
||||||
$this->log(" 🔀 Merged {$defaultBranch} → {$branch} (PR #{$prNum})", 'INFO');
|
$this->log(" 🔀 Merged {$defaultBranch} → {$branch} (PR #{$prNum})", 'INFO');
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
@@ -1076,7 +1076,7 @@ class BulkSync extends CliFramework
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Records which sync run touched the repo, the PR number, and the
|
* Records which sync run touched the repo, the PR number, and the
|
||||||
* moko-platform version that was applied — giving each repo a clear audit
|
* mokocli version that was applied — giving each repo a clear audit
|
||||||
* trail of what was changed and why.
|
* trail of what was changed and why.
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
@@ -1119,16 +1119,16 @@ class BulkSync extends CliFramework
|
|||||||
$minor = self::VERSION_MINOR;
|
$minor = self::VERSION_MINOR;
|
||||||
$force = isset($this->options['force']) ? ' *(--force)*' : '';
|
$force = isset($this->options['force']) ? ' *(--force)*' : '';
|
||||||
$prLink = $this->adapter->getPullRequestWebUrl($org, $repo, $prNumber);
|
$prLink = $this->adapter->getPullRequestWebUrl($org, $repo, $prNumber);
|
||||||
$source = $this->adapter->getRepoWebUrl($org, 'moko-platform');
|
$source = $this->adapter->getRepoWebUrl($org, 'mokocli');
|
||||||
$branchName = 'chore/sync-moko-platform-v' . $minor;
|
$branchName = 'chore/sync-mokocli-v' . $minor;
|
||||||
$branchLink = $this->adapter->getBranchWebUrl($org, $repo, $branchName);
|
$branchLink = $this->adapter->getBranchWebUrl($org, $repo, $branchName);
|
||||||
|
|
||||||
$title = "chore: moko-platform v{$minor} sync tracking";
|
$title = "chore: mokocli v{$minor} sync tracking";
|
||||||
|
|
||||||
$body = <<<MD
|
$body = <<<MD
|
||||||
## moko-platform Sync Applied
|
## mokocli Sync Applied
|
||||||
|
|
||||||
A moko-platform bulk sync run has updated files in this repository.
|
A mokocli bulk sync run has updated files in this repository.
|
||||||
|
|
||||||
| Field | Value |
|
| Field | Value |
|
||||||
|-------|-------|
|
|-------|-------|
|
||||||
@@ -1144,13 +1144,13 @@ class BulkSync extends CliFramework
|
|||||||
Protected files (README, CHANGELOG, GOVERNANCE, etc.) were not overwritten.
|
Protected files (README, CHANGELOG, GOVERNANCE, etc.) were not overwritten.
|
||||||
|
|
||||||
---
|
---
|
||||||
*Updated automatically by [moko-platform]({$source}) `bulk_sync.php`*
|
*Updated automatically by [mokocli]({$source}) `bulk_sync.php`*
|
||||||
MD;
|
MD;
|
||||||
|
|
||||||
// Dedent heredoc
|
// Dedent heredoc
|
||||||
$body = preg_replace('/^ /m', '', $body);
|
$body = preg_replace('/^ /m', '', $body);
|
||||||
|
|
||||||
$labelNames = ['standards-update', 'moko-platform', 'type: chore', 'automation'];
|
$labelNames = ['standards-update', 'mokocli', 'type: chore', 'automation'];
|
||||||
$labels = $this->resolveLabelIds($org, $repo, $labelNames);
|
$labels = $this->resolveLabelIds($org, $repo, $labelNames);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -1213,7 +1213,7 @@ class BulkSync extends CliFramework
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a tracking issue in moko-platform for this sync run.
|
* Create a tracking issue in mokocli for this sync run.
|
||||||
*/
|
*/
|
||||||
private function createSyncIssue(string $org, array $results): void
|
private function createSyncIssue(string $org, array $results): void
|
||||||
{
|
{
|
||||||
@@ -1232,7 +1232,7 @@ class BulkSync extends CliFramework
|
|||||||
$issues = $results['issues'] ?? [];
|
$issues = $results['issues'] ?? [];
|
||||||
|
|
||||||
// Stable title — no timestamp so repeated runs update a single issue
|
// Stable title — no timestamp so repeated runs update a single issue
|
||||||
$title = "sync: moko-platform v" . self::VERSION_MINOR . " bulk sync report";
|
$title = "sync: mokocli v" . self::VERSION_MINOR . " bulk sync report";
|
||||||
|
|
||||||
$protection = $results['protection'] ?? [];
|
$protection = $results['protection'] ?? [];
|
||||||
$hasProtect = !empty($protection);
|
$hasProtect = !empty($protection);
|
||||||
@@ -1281,7 +1281,7 @@ class BulkSync extends CliFramework
|
|||||||
: "|---|---|---|---|";
|
: "|---|---|---|---|";
|
||||||
|
|
||||||
$body = <<<MD
|
$body = <<<MD
|
||||||
## moko-platform Bulk Sync Report
|
## mokocli Bulk Sync Report
|
||||||
|
|
||||||
**Organisation:** `{$org}`
|
**Organisation:** `{$org}`
|
||||||
**Triggered:** {$now}{$force}
|
**Triggered:** {$now}{$force}
|
||||||
@@ -1301,7 +1301,7 @@ class BulkSync extends CliFramework
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Search for existing issue by label — any state so we can reopen closed ones
|
// Search for existing issue by label — any state so we can reopen closed ones
|
||||||
$existing = $this->api->get("/repos/{$org}/moko-platform/issues", [
|
$existing = $this->api->get("/repos/{$org}/mokocli/issues", [
|
||||||
'labels' => 'sync-report',
|
'labels' => 'sync-report',
|
||||||
'state' => 'all',
|
'state' => 'all',
|
||||||
'per_page' => 1,
|
'per_page' => 1,
|
||||||
@@ -1309,8 +1309,8 @@ class BulkSync extends CliFramework
|
|||||||
'direction' => 'desc',
|
'direction' => 'desc',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$labelNames = ['sync-report', 'moko-platform', 'type: chore', 'automation'];
|
$labelNames = ['sync-report', 'mokocli', 'type: chore', 'automation'];
|
||||||
$labels = $this->resolveLabelIds($org, 'moko-platform', $labelNames);
|
$labels = $this->resolveLabelIds($org, 'mokocli', $labelNames);
|
||||||
$existing = array_values($existing);
|
$existing = array_values($existing);
|
||||||
|
|
||||||
if (!empty($existing) && isset($existing[0]['number'])) {
|
if (!empty($existing) && isset($existing[0]['number'])) {
|
||||||
@@ -1319,22 +1319,22 @@ class BulkSync extends CliFramework
|
|||||||
if (($existing[0]['state'] ?? 'open') === 'closed') {
|
if (($existing[0]['state'] ?? 'open') === 'closed') {
|
||||||
$patch['state'] = 'open';
|
$patch['state'] = 'open';
|
||||||
}
|
}
|
||||||
$this->api->patch("/repos/{$org}/moko-platform/issues/{$issueNumber}", $patch);
|
$this->api->patch("/repos/{$org}/mokocli/issues/{$issueNumber}", $patch);
|
||||||
try {
|
try {
|
||||||
$this->api->post("/repos/{$org}/moko-platform/issues/{$issueNumber}/labels", ['labels' => $labels]);
|
$this->api->post("/repos/{$org}/mokocli/issues/{$issueNumber}/labels", ['labels' => $labels]);
|
||||||
} catch (\Exception $le) {
|
} catch (\Exception $le) {
|
||||||
/* non-fatal */
|
/* non-fatal */
|
||||||
}
|
}
|
||||||
$this->log("📋 Sync report issue updated: {$org}/moko-platform#{$issueNumber}", 'INFO');
|
$this->log("📋 Sync report issue updated: {$org}/mokocli#{$issueNumber}", 'INFO');
|
||||||
} else {
|
} else {
|
||||||
$issue = $this->api->post("/repos/{$org}/moko-platform/issues", [
|
$issue = $this->api->post("/repos/{$org}/mokocli/issues", [
|
||||||
'title' => $title,
|
'title' => $title,
|
||||||
'body' => $body,
|
'body' => $body,
|
||||||
'labels' => $labels,
|
'labels' => $labels,
|
||||||
'assignees' => ['jmiller'],
|
'assignees' => ['jmiller'],
|
||||||
]);
|
]);
|
||||||
$issueNumber = $issue['number'] ?? '?';
|
$issueNumber = $issue['number'] ?? '?';
|
||||||
$this->log("📋 Sync report issue created: {$org}/moko-platform#{$issueNumber}", 'INFO');
|
$this->log("📋 Sync report issue created: {$org}/mokocli#{$issueNumber}", 'INFO');
|
||||||
}
|
}
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$this->log("⚠️ Failed to create/update sync report issue: " . $e->getMessage(), 'WARN');
|
$this->log("⚠️ Failed to create/update sync report issue: " . $e->getMessage(), 'WARN');
|
||||||
@@ -1342,7 +1342,7 @@ class BulkSync extends CliFramework
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create or update a failure issue in moko-platform when repos fail to sync.
|
* Create or update a failure issue in mokocli when repos fail to sync.
|
||||||
* Uses the 'sync-failure' label so it is distinct from the run-report issue.
|
* Uses the 'sync-failure' label so it is distinct from the run-report issue.
|
||||||
* Reopens a closed issue rather than creating a duplicate.
|
* Reopens a closed issue rather than creating a duplicate.
|
||||||
*/
|
*/
|
||||||
@@ -1388,7 +1388,7 @@ class BulkSync extends CliFramework
|
|||||||
$body = preg_replace('/^ /m', '', $body);
|
$body = preg_replace('/^ /m', '', $body);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$existing = $this->api->get("/repos/{$org}/moko-platform/issues", [
|
$existing = $this->api->get("/repos/{$org}/mokocli/issues", [
|
||||||
'labels' => 'sync-failure',
|
'labels' => 'sync-failure',
|
||||||
'state' => 'all',
|
'state' => 'all',
|
||||||
'per_page' => 1,
|
'per_page' => 1,
|
||||||
@@ -1403,17 +1403,17 @@ class BulkSync extends CliFramework
|
|||||||
if (($existing[0]['state'] ?? 'open') === 'closed') {
|
if (($existing[0]['state'] ?? 'open') === 'closed') {
|
||||||
$patch['state'] = 'open';
|
$patch['state'] = 'open';
|
||||||
}
|
}
|
||||||
$this->api->patch("/repos/{$org}/moko-platform/issues/{$num}", $patch);
|
$this->api->patch("/repos/{$org}/mokocli/issues/{$num}", $patch);
|
||||||
$this->log("🚨 Failure issue #{$num} updated: {$org}/moko-platform#{$num}", 'WARN');
|
$this->log("🚨 Failure issue #{$num} updated: {$org}/mokocli#{$num}", 'WARN');
|
||||||
} else {
|
} else {
|
||||||
$issue = $this->api->post("/repos/{$org}/moko-platform/issues", [
|
$issue = $this->api->post("/repos/{$org}/mokocli/issues", [
|
||||||
'title' => $title,
|
'title' => $title,
|
||||||
'body' => $body,
|
'body' => $body,
|
||||||
'labels' => $this->resolveLabelIds($org, 'moko-platform', ['sync-failure']),
|
'labels' => $this->resolveLabelIds($org, 'mokocli', ['sync-failure']),
|
||||||
'assignees' => ['jmiller'],
|
'assignees' => ['jmiller'],
|
||||||
]);
|
]);
|
||||||
$num = $issue['number'] ?? '?';
|
$num = $issue['number'] ?? '?';
|
||||||
$this->log("🚨 Failure issue created: {$org}/moko-platform#{$num}", 'WARN');
|
$this->log("🚨 Failure issue created: {$org}/mokocli#{$num}", 'WARN');
|
||||||
}
|
}
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$this->log("⚠️ Could not create/update failure issue: " . $e->getMessage(), 'WARN');
|
$this->log("⚠️ Could not create/update failure issue: " . $e->getMessage(), 'WARN');
|
||||||
|
|||||||
@@ -0,0 +1,237 @@
|
|||||||
|
#!/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: mokocli.Automation
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
|
# 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
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: MokoPlatform.Automation
|
* DEFGROUP: MokoPlatform.Automation
|
||||||
* INGROUP: MokoPlatform
|
* INGROUP: MokoPlatform
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /automation/enrich_manifest_xml.php
|
* PATH: /automation/enrich_manifest_xml.php
|
||||||
* BRIEF: Enrich XML manifests with repo-specific build and deploy details
|
* BRIEF: Enrich XML manifests with repo-specific build and deploy details
|
||||||
*
|
*
|
||||||
@@ -21,8 +21,8 @@ declare(strict_types=1);
|
|||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoCli\CliFramework;
|
||||||
use MokoEnterprise\MokoStandardsParser;
|
use MokoCli\ManifestParser;
|
||||||
|
|
||||||
class EnrichManifestXmlCli extends CliFramework
|
class EnrichManifestXmlCli extends CliFramework
|
||||||
{
|
{
|
||||||
@@ -43,10 +43,10 @@ class EnrichManifestXmlCli extends CliFramework
|
|||||||
$skipStr = $this->getArgument('--skip');
|
$skipStr = $this->getArgument('--skip');
|
||||||
$skipRepos = $skipStr !== '' ? array_map('trim', explode(',', $skipStr)) : [];
|
$skipRepos = $skipStr !== '' ? array_map('trim', explode(',', $skipStr)) : [];
|
||||||
|
|
||||||
$parser = new MokoStandardsParser();
|
$parser = new ManifestParser();
|
||||||
$tmpBase = sys_get_temp_dir() . '/moko-enrich-' . getmypid();
|
$tmpBase = sys_get_temp_dir() . '/moko-enrich-' . getmypid();
|
||||||
|
|
||||||
echo "=== moko-platform XML Manifest Enrichment ===\n";
|
echo "=== mokocli XML Manifest Enrichment ===\n";
|
||||||
echo "Mode: " . ($this->dryRun ? "DRY RUN" : "LIVE") . "\n";
|
echo "Mode: " . ($this->dryRun ? "DRY RUN" : "LIVE") . "\n";
|
||||||
if (!empty($skipRepos)) {
|
if (!empty($skipRepos)) {
|
||||||
echo "Skipping: " . implode(', ', $skipRepos) . "\n";
|
echo "Skipping: " . implode(', ', $skipRepos) . "\n";
|
||||||
@@ -97,7 +97,7 @@ class EnrichManifestXmlCli extends CliFramework
|
|||||||
}
|
}
|
||||||
|
|
||||||
$manifestPath = "{$workDir}/.mokogitea/manifest.xml";
|
$manifestPath = "{$workDir}/.mokogitea/manifest.xml";
|
||||||
if (!file_exists($manifestPath) || !str_contains(file_get_contents($manifestPath), '<moko-platform')) {
|
if (!file_exists($manifestPath) || !str_contains(file_get_contents($manifestPath), '<mokocli')) {
|
||||||
echo "SKIP (no XML manifest)\n";
|
echo "SKIP (no XML manifest)\n";
|
||||||
$stats['skipped']++;
|
$stats['skipped']++;
|
||||||
$this->rmTree($workDir);
|
$this->rmTree($workDir);
|
||||||
@@ -113,8 +113,8 @@ class EnrichManifestXmlCli extends CliFramework
|
|||||||
}
|
}
|
||||||
$enrichment['build']['language'] = $enrichment['build']['language']
|
$enrichment['build']['language'] = $enrichment['build']['language']
|
||||||
?? $repo['language']
|
?? $repo['language']
|
||||||
?? MokoStandardsParser::platformLanguage($platform);
|
?? ManifestParser::platformLanguage($platform);
|
||||||
$enrichment['build']['package_type'] = $enrichment['build']['package_type'] ?? MokoStandardsParser::platformPackageType($platform);
|
$enrichment['build']['package_type'] = $enrichment['build']['package_type'] ?? ManifestParser::platformPackageType($platform);
|
||||||
|
|
||||||
$enrichedXml = $this->enrichManifestXml($existingXml, $enrichment);
|
$enrichedXml = $this->enrichManifestXml($existingXml, $enrichment);
|
||||||
$dc = count($enrichment['deploy'] ?? []);
|
$dc = count($enrichment['deploy'] ?? []);
|
||||||
@@ -312,7 +312,7 @@ class EnrichManifestXmlCli extends CliFramework
|
|||||||
return $xml;
|
return $xml;
|
||||||
}
|
}
|
||||||
|
|
||||||
$ns = MokoStandardsParser::NAMESPACE_URI;
|
$ns = ManifestParser::NAMESPACE_URI;
|
||||||
$root = $dom->documentElement;
|
$root = $dom->documentElement;
|
||||||
|
|
||||||
foreach (['build', 'deploy', 'scripts'] as $tag) {
|
foreach (['build', 'deploy', 'scripts'] as $tag) {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: MokoPlatform.Automation
|
* DEFGROUP: MokoPlatform.Automation
|
||||||
* INGROUP: MokoPlatform
|
* INGROUP: MokoPlatform
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /automation/enrich_mokostandards_xml.php
|
* PATH: /automation/enrich_mokostandards_xml.php
|
||||||
* BRIEF: Enrich XML manifests with repo-specific build and deploy details
|
* BRIEF: Enrich XML manifests with repo-specific build and deploy details
|
||||||
*
|
*
|
||||||
@@ -21,8 +21,8 @@ declare(strict_types=1);
|
|||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoCli\CliFramework;
|
||||||
use MokoEnterprise\MokoStandardsParser;
|
use MokoCli\ManifestParser;
|
||||||
|
|
||||||
class EnrichMokostandardsXmlCli extends CliFramework
|
class EnrichMokostandardsXmlCli extends CliFramework
|
||||||
{
|
{
|
||||||
@@ -43,10 +43,10 @@ class EnrichMokostandardsXmlCli extends CliFramework
|
|||||||
$skipStr = $this->getArgument('--skip');
|
$skipStr = $this->getArgument('--skip');
|
||||||
$skipRepos = $skipStr !== '' ? array_map('trim', explode(',', $skipStr)) : [];
|
$skipRepos = $skipStr !== '' ? array_map('trim', explode(',', $skipStr)) : [];
|
||||||
|
|
||||||
$parser = new MokoStandardsParser();
|
$parser = new ManifestParser();
|
||||||
$tmpBase = sys_get_temp_dir() . '/moko-enrich-' . getmypid();
|
$tmpBase = sys_get_temp_dir() . '/moko-enrich-' . getmypid();
|
||||||
|
|
||||||
echo "=== moko-platform XML Manifest Enrichment ===\n";
|
echo "=== mokocli XML Manifest Enrichment ===\n";
|
||||||
echo "Mode: " . ($this->dryRun ? "DRY RUN" : "LIVE") . "\n";
|
echo "Mode: " . ($this->dryRun ? "DRY RUN" : "LIVE") . "\n";
|
||||||
if (!empty($skipRepos)) {
|
if (!empty($skipRepos)) {
|
||||||
echo "Skipping: " . implode(', ', $skipRepos) . "\n";
|
echo "Skipping: " . implode(', ', $skipRepos) . "\n";
|
||||||
@@ -97,7 +97,7 @@ class EnrichMokostandardsXmlCli extends CliFramework
|
|||||||
}
|
}
|
||||||
|
|
||||||
$manifestPath = "{$workDir}/.mokogitea/manifest.xml";
|
$manifestPath = "{$workDir}/.mokogitea/manifest.xml";
|
||||||
if (!file_exists($manifestPath) || !str_contains(file_get_contents($manifestPath), '<moko-platform')) {
|
if (!file_exists($manifestPath) || !str_contains(file_get_contents($manifestPath), '<mokocli')) {
|
||||||
echo "SKIP (no XML manifest)\n";
|
echo "SKIP (no XML manifest)\n";
|
||||||
$stats['skipped']++;
|
$stats['skipped']++;
|
||||||
$this->rmTree($workDir);
|
$this->rmTree($workDir);
|
||||||
@@ -113,8 +113,8 @@ class EnrichMokostandardsXmlCli extends CliFramework
|
|||||||
}
|
}
|
||||||
$enrichment['build']['language'] = $enrichment['build']['language']
|
$enrichment['build']['language'] = $enrichment['build']['language']
|
||||||
?? $repo['language']
|
?? $repo['language']
|
||||||
?? MokoStandardsParser::platformLanguage($platform);
|
?? ManifestParser::platformLanguage($platform);
|
||||||
$enrichment['build']['package_type'] = $enrichment['build']['package_type'] ?? MokoStandardsParser::platformPackageType($platform);
|
$enrichment['build']['package_type'] = $enrichment['build']['package_type'] ?? ManifestParser::platformPackageType($platform);
|
||||||
|
|
||||||
$enrichedXml = $this->enrichManifestXml($existingXml, $enrichment);
|
$enrichedXml = $this->enrichManifestXml($existingXml, $enrichment);
|
||||||
$dc = count($enrichment['deploy'] ?? []);
|
$dc = count($enrichment['deploy'] ?? []);
|
||||||
@@ -315,7 +315,7 @@ class EnrichMokostandardsXmlCli extends CliFramework
|
|||||||
return $xml;
|
return $xml;
|
||||||
}
|
}
|
||||||
|
|
||||||
$ns = MokoStandardsParser::NAMESPACE_URI;
|
$ns = ManifestParser::NAMESPACE_URI;
|
||||||
$root = $dom->documentElement;
|
$root = $dom->documentElement;
|
||||||
|
|
||||||
foreach (['build', 'deploy', 'scripts'] as $tag) {
|
foreach (['build', 'deploy', 'scripts'] as $tag) {
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
FILE INFORMATION
|
FILE INFORMATION
|
||||||
DEFGROUP: MokoPlatform.Index
|
DEFGROUP: MokoPlatform.Index
|
||||||
INGROUP: MokoPlatform.Automation
|
INGROUP: MokoPlatform.Automation
|
||||||
REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
PATH: /automation/index.md
|
PATH: /automation/index.md
|
||||||
BRIEF: Automation directory index
|
BRIEF: Automation directory index
|
||||||
-->
|
-->
|
||||||
|
|||||||
@@ -10,14 +10,14 @@
|
|||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: MokoPlatform.Automation
|
* DEFGROUP: MokoPlatform.Automation
|
||||||
* INGROUP: MokoPlatform
|
* INGROUP: MokoPlatform
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /automation/migrate_to_gitea.php
|
* PATH: /automation/migrate_to_gitea.php
|
||||||
* BRIEF: Migrate repositories from GitHub to self-hosted Gitea instance
|
* BRIEF: Migrate repositories from GitHub to self-hosted Gitea instance
|
||||||
*
|
*
|
||||||
* USAGE
|
* USAGE
|
||||||
* php automation/migrate_to_gitea.php --dry-run
|
* php automation/migrate_to_gitea.php --dry-run
|
||||||
* php automation/migrate_to_gitea.php --repos MokoCRM MokoDoliMods
|
* php automation/migrate_to_gitea.php --repos MokoCRM MokoDoliMods
|
||||||
* php automation/migrate_to_gitea.php --exclude moko-platform --skip-archived
|
* php automation/migrate_to_gitea.php --exclude mokocli --skip-archived
|
||||||
* php automation/migrate_to_gitea.php --resume
|
* php automation/migrate_to_gitea.php --resume
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -25,12 +25,12 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
use MokoEnterprise\CheckpointManager;
|
use MokoCli\CheckpointManager;
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoCli\CliFramework;
|
||||||
use MokoEnterprise\Config;
|
use MokoCli\Config;
|
||||||
use MokoEnterprise\PlatformAdapterFactory;
|
use MokoCli\PlatformAdapterFactory;
|
||||||
use MokoEnterprise\GitHubAdapter;
|
use MokoCli\GitHubAdapter;
|
||||||
use MokoEnterprise\MokoGiteaAdapter;
|
use MokoCli\MokoGiteaAdapter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gitea Migration Script
|
* Gitea Migration Script
|
||||||
@@ -278,7 +278,7 @@ class MigrateToGitea extends CliFramework
|
|||||||
try {
|
try {
|
||||||
$this->gitea->createIssue(
|
$this->gitea->createIssue(
|
||||||
$giteaOrg,
|
$giteaOrg,
|
||||||
'moko-platform',
|
'mokocli',
|
||||||
'chore: GitHub → Gitea migration report — ' . count($results['migrated']) . ' repos migrated',
|
'chore: GitHub → Gitea migration report — ' . count($results['migrated']) . ' repos migrated',
|
||||||
$report,
|
$report,
|
||||||
['labels' => ['automation', 'type: chore']]
|
['labels' => ['automation', 'type: chore']]
|
||||||
|
|||||||
+23
-22
@@ -11,7 +11,7 @@
|
|||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: MokoPlatform.Automation
|
* DEFGROUP: MokoPlatform.Automation
|
||||||
* INGROUP: MokoPlatform.Scripts
|
* INGROUP: MokoPlatform.Scripts
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /automation/push_files.php
|
* PATH: /automation/push_files.php
|
||||||
* BRIEF: Push one or more specific files to one or more remote repositories
|
* BRIEF: Push one or more specific files to one or more remote repositories
|
||||||
*/
|
*/
|
||||||
@@ -21,7 +21,7 @@ declare(strict_types=1);
|
|||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\{
|
use MokoCli\{
|
||||||
ApiClient,
|
ApiClient,
|
||||||
AuditLogger,
|
AuditLogger,
|
||||||
CliFramework,
|
CliFramework,
|
||||||
@@ -35,7 +35,7 @@ use MokoEnterprise\{
|
|||||||
/**
|
/**
|
||||||
* Targeted File Push Tool
|
* Targeted File Push Tool
|
||||||
*
|
*
|
||||||
* Pushes one or more specific files from moko-platform templates to one or
|
* Pushes one or more specific files from mokocli templates to one or
|
||||||
* more remote repositories — without running a full sync.
|
* more remote repositories — without running a full sync.
|
||||||
*
|
*
|
||||||
* Files are specified by their destination path as they appear in the target
|
* Files are specified by their destination path as they appear in the target
|
||||||
@@ -81,7 +81,7 @@ class PushFiles extends CliFramework
|
|||||||
*/
|
*/
|
||||||
protected function run(): int
|
protected function run(): int
|
||||||
{
|
{
|
||||||
$this->log('📦 moko-platform File Push v' . self::VERSION, 'INFO');
|
$this->log('📦 mokocli File Push v' . self::VERSION, 'INFO');
|
||||||
|
|
||||||
if (!$this->initializeComponents()) {
|
if (!$this->initializeComponents()) {
|
||||||
return 1;
|
return 1;
|
||||||
@@ -230,7 +230,8 @@ class PushFiles extends CliFramework
|
|||||||
{
|
{
|
||||||
// Read platform from repo's .mokogitea/manifest.xml via API
|
// Read platform from repo's .mokogitea/manifest.xml via API
|
||||||
try {
|
try {
|
||||||
$manifestData = $this->adapter->getFileContent($org, $repo, '.mokogitea/manifest.xml', 'main');
|
$fileInfo = $this->adapter->getFileContents($org, $repo, '.mokogitea/manifest.xml', 'main');
|
||||||
|
$manifestData = isset($fileInfo['content']) ? base64_decode($fileInfo['content']) : '';
|
||||||
if (!empty($manifestData)) {
|
if (!empty($manifestData)) {
|
||||||
$xml = @simplexml_load_string($manifestData);
|
$xml = @simplexml_load_string($manifestData);
|
||||||
if ($xml !== false) {
|
if ($xml !== false) {
|
||||||
@@ -336,7 +337,7 @@ class PushFiles extends CliFramework
|
|||||||
|
|
||||||
$prNumber = null;
|
$prNumber = null;
|
||||||
if (!$direct) {
|
if (!$direct) {
|
||||||
$prTitle = "chore: push " . count($entries) . " file(s) from moko-platform";
|
$prTitle = "chore: push " . count($entries) . " file(s) from mokocli";
|
||||||
$prBody = $this->buildPRBody($entries);
|
$prBody = $this->buildPRBody($entries);
|
||||||
$pr = $this->adapter->createPullRequest(
|
$pr = $this->adapter->createPullRequest(
|
||||||
$org,
|
$org,
|
||||||
@@ -413,7 +414,7 @@ class PushFiles extends CliFramework
|
|||||||
|
|
||||||
$message = !empty($customMessage)
|
$message = !empty($customMessage)
|
||||||
? $customMessage
|
? $customMessage
|
||||||
: "chore: update {$destPath} from moko-platform";
|
: "chore: update {$destPath} from mokocli";
|
||||||
|
|
||||||
// Fetch existing file SHA (needed for updates)
|
// Fetch existing file SHA (needed for updates)
|
||||||
$existingSha = null;
|
$existingSha = null;
|
||||||
@@ -456,9 +457,9 @@ class PushFiles extends CliFramework
|
|||||||
): void {
|
): void {
|
||||||
$now = gmdate('Y-m-d H:i:s') . ' UTC';
|
$now = gmdate('Y-m-d H:i:s') . ' UTC';
|
||||||
$version = self::VERSION;
|
$version = self::VERSION;
|
||||||
$source = $this->adapter->getRepoWebUrl($org, 'moko-platform');
|
$source = $this->adapter->getRepoWebUrl($org, 'mokocli');
|
||||||
|
|
||||||
$title = "chore: moko-platform file push tracking";
|
$title = "chore: mokocli file push tracking";
|
||||||
|
|
||||||
$deliveryLine = $prNumber !== null
|
$deliveryLine = $prNumber !== null
|
||||||
? "| **Pull request** | [#{$prNumber}](" . $this->adapter->getPullRequestWebUrl($org, $repo, $prNumber) . ") |"
|
? "| **Pull request** | [#{$prNumber}](" . $this->adapter->getPullRequestWebUrl($org, $repo, $prNumber) . ") |"
|
||||||
@@ -470,9 +471,9 @@ class PushFiles extends CliFramework
|
|||||||
));
|
));
|
||||||
|
|
||||||
$body = <<<MD
|
$body = <<<MD
|
||||||
## moko-platform File Push
|
## mokocli File Push
|
||||||
|
|
||||||
One or more files were pushed to this repository from moko-platform.
|
One or more files were pushed to this repository from mokocli.
|
||||||
|
|
||||||
| Field | Value |
|
| Field | Value |
|
||||||
|-------|-------|
|
|-------|-------|
|
||||||
@@ -486,12 +487,12 @@ class PushFiles extends CliFramework
|
|||||||
{$fileRows}
|
{$fileRows}
|
||||||
|
|
||||||
---
|
---
|
||||||
*Generated automatically by [moko-platform]({$source}) `push_files.php`*
|
*Generated automatically by [mokocli]({$source}) `push_files.php`*
|
||||||
MD;
|
MD;
|
||||||
|
|
||||||
$body = preg_replace('/^ /m', '', $body);
|
$body = preg_replace('/^ /m', '', $body);
|
||||||
|
|
||||||
$labels = ['standards-update', 'moko-platform', 'type: chore', 'automation'];
|
$labels = ['standards-update', 'mokocli', 'type: chore', 'automation'];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$existing = $this->api->get("/repos/{$org}/{$repo}/issues", [
|
$existing = $this->api->get("/repos/{$org}/{$repo}/issues", [
|
||||||
@@ -549,7 +550,7 @@ class PushFiles extends CliFramework
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create or update a failure issue in moko-platform when repos fail to receive files.
|
* Create or update a failure issue in mokocli when repos fail to receive files.
|
||||||
* Uses the 'push-failure' label. Reopens a closed issue rather than creating a duplicate.
|
* Uses the 'push-failure' label. Reopens a closed issue rather than creating a duplicate.
|
||||||
*/
|
*/
|
||||||
private function createFailureIssue(string $org, array $results): void
|
private function createFailureIssue(string $org, array $results): void
|
||||||
@@ -597,7 +598,7 @@ class PushFiles extends CliFramework
|
|||||||
$body = preg_replace('/^ /m', '', $body);
|
$body = preg_replace('/^ /m', '', $body);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$existing = $this->api->get("/repos/{$org}/moko-platform/issues", [
|
$existing = $this->api->get("/repos/{$org}/mokocli/issues", [
|
||||||
'labels' => 'push-failure',
|
'labels' => 'push-failure',
|
||||||
'state' => 'all',
|
'state' => 'all',
|
||||||
'per_page' => 1,
|
'per_page' => 1,
|
||||||
@@ -612,17 +613,17 @@ class PushFiles extends CliFramework
|
|||||||
if (($existing[0]['state'] ?? 'open') === 'closed') {
|
if (($existing[0]['state'] ?? 'open') === 'closed') {
|
||||||
$patch['state'] = 'open';
|
$patch['state'] = 'open';
|
||||||
}
|
}
|
||||||
$this->api->patch("/repos/{$org}/moko-platform/issues/{$num}", $patch);
|
$this->api->patch("/repos/{$org}/mokocli/issues/{$num}", $patch);
|
||||||
$this->log("🚨 Failure issue #{$num} updated: {$org}/moko-platform#{$num}", 'WARN');
|
$this->log("🚨 Failure issue #{$num} updated: {$org}/mokocli#{$num}", 'WARN');
|
||||||
} else {
|
} else {
|
||||||
$issue = $this->api->post("/repos/{$org}/moko-platform/issues", [
|
$issue = $this->api->post("/repos/{$org}/mokocli/issues", [
|
||||||
'title' => $title,
|
'title' => $title,
|
||||||
'body' => $body,
|
'body' => $body,
|
||||||
'labels' => ['push-failure'],
|
'labels' => ['push-failure'],
|
||||||
'assignees' => ['jmiller'],
|
'assignees' => ['jmiller'],
|
||||||
]);
|
]);
|
||||||
$num = $issue['number'] ?? '?';
|
$num = $issue['number'] ?? '?';
|
||||||
$this->log("🚨 Failure issue created: {$org}/moko-platform#{$num}", 'WARN');
|
$this->log("🚨 Failure issue created: {$org}/mokocli#{$num}", 'WARN');
|
||||||
}
|
}
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$this->log("⚠️ Could not create/update failure issue: " . $e->getMessage(), 'WARN');
|
$this->log("⚠️ Could not create/update failure issue: " . $e->getMessage(), 'WARN');
|
||||||
@@ -637,14 +638,14 @@ class PushFiles extends CliFramework
|
|||||||
private function buildPRBody(array $entries): string
|
private function buildPRBody(array $entries): string
|
||||||
{
|
{
|
||||||
$now = gmdate('Y-m-d H:i:s') . ' UTC';
|
$now = gmdate('Y-m-d H:i:s') . ' UTC';
|
||||||
$lines = ["## moko-platform File Push\n", "**Pushed:** {$now}\n", '### Files\n'];
|
$lines = ["## mokocli File Push\n", "**Pushed:** {$now}\n", '### Files\n'];
|
||||||
|
|
||||||
foreach ($entries as $entry) {
|
foreach ($entries as $entry) {
|
||||||
$lines[] = "- `{$entry['destination']}`";
|
$lines[] = "- `{$entry['destination']}`";
|
||||||
}
|
}
|
||||||
|
|
||||||
$sourceUrl = $this->adapter->getRepoWebUrl(self::DEFAULT_ORG, 'moko-platform');
|
$sourceUrl = $this->adapter->getRepoWebUrl(self::DEFAULT_ORG, 'mokocli');
|
||||||
$lines[] = "\n---\n*Generated by [moko-platform]({$sourceUrl}) `push_files.php`*";
|
$lines[] = "\n---\n*Generated by [mokocli]({$sourceUrl}) `push_files.php`*";
|
||||||
|
|
||||||
return implode("\n", $lines);
|
return implode("\n", $lines);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: MokoPlatform.Automation
|
* DEFGROUP: MokoPlatform.Automation
|
||||||
* INGROUP: MokoPlatform
|
* INGROUP: MokoPlatform
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /automation/push_manifest_xml.php
|
* PATH: /automation/push_manifest_xml.php
|
||||||
* BRIEF: Push XML manifests to all governed repositories
|
* BRIEF: Push XML manifests to all governed repositories
|
||||||
*/
|
*/
|
||||||
@@ -18,8 +18,8 @@ declare(strict_types=1);
|
|||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoCli\CliFramework;
|
||||||
use MokoEnterprise\MokoStandardsParser;
|
use MokoCli\ManifestParser;
|
||||||
|
|
||||||
class PushManifestXmlCli extends CliFramework
|
class PushManifestXmlCli extends CliFramework
|
||||||
{
|
{
|
||||||
@@ -44,10 +44,10 @@ class PushManifestXmlCli extends CliFramework
|
|||||||
$skipStr = $this->getArgument('--skip');
|
$skipStr = $this->getArgument('--skip');
|
||||||
$skipRepos = $skipStr !== '' ? array_map('trim', explode(',', $skipStr)) : [];
|
$skipRepos = $skipStr !== '' ? array_map('trim', explode(',', $skipStr)) : [];
|
||||||
|
|
||||||
$parser = new MokoStandardsParser();
|
$parser = new ManifestParser();
|
||||||
$tmpBase = sys_get_temp_dir() . '/moko-manifest-push-' . getmypid();
|
$tmpBase = sys_get_temp_dir() . '/moko-manifest-push-' . getmypid();
|
||||||
|
|
||||||
echo "=== moko-platform XML Manifest Push ===\n";
|
echo "=== mokocli XML Manifest Push ===\n";
|
||||||
echo "Org: {$giteaOrg}\n";
|
echo "Org: {$giteaOrg}\n";
|
||||||
echo "Mode: " . ($this->dryRun ? "DRY RUN" : "LIVE") . "\n";
|
echo "Mode: " . ($this->dryRun ? "DRY RUN" : "LIVE") . "\n";
|
||||||
if ($repoFilter) {
|
if ($repoFilter) {
|
||||||
@@ -97,8 +97,8 @@ class PushManifestXmlCli extends CliFramework
|
|||||||
'description' => $repo['description'] ?? '',
|
'description' => $repo['description'] ?? '',
|
||||||
'license' => 'GPL-3.0-or-later',
|
'license' => 'GPL-3.0-or-later',
|
||||||
'topics' => $repo['topics'] ?? [],
|
'topics' => $repo['topics'] ?? [],
|
||||||
'language' => $repo['language'] ?? MokoStandardsParser::platformLanguage($platform),
|
'language' => $repo['language'] ?? ManifestParser::platformLanguage($platform),
|
||||||
'package_type' => MokoStandardsParser::platformPackageType($platform),
|
'package_type' => ManifestParser::platformPackageType($platform),
|
||||||
'last_synced' => date('c'),
|
'last_synced' => date('c'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -125,7 +125,7 @@ class PushManifestXmlCli extends CliFramework
|
|||||||
|
|
||||||
// Check if already XML and up-to-date
|
// Check if already XML and up-to-date
|
||||||
$manifestPath = "{$workDir}/.mokogitea/manifest.xml";
|
$manifestPath = "{$workDir}/.mokogitea/manifest.xml";
|
||||||
$existingIsXml = file_exists($manifestPath) && str_contains(file_get_contents($manifestPath), '<moko-platform');
|
$existingIsXml = file_exists($manifestPath) && str_contains(file_get_contents($manifestPath), '<mokocli');
|
||||||
if ($existingIsXml && !$force) {
|
if ($existingIsXml && !$force) {
|
||||||
$existingPlatform = $parser->extractPlatform(file_get_contents($manifestPath));
|
$existingPlatform = $parser->extractPlatform(file_get_contents($manifestPath));
|
||||||
if ($existingPlatform === $platform) {
|
if ($existingPlatform === $platform) {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: MokoPlatform.Automation
|
* DEFGROUP: MokoPlatform.Automation
|
||||||
* INGROUP: MokoPlatform
|
* INGROUP: MokoPlatform
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /automation/push_mokostandards_xml.php
|
* PATH: /automation/push_mokostandards_xml.php
|
||||||
* BRIEF: Push XML manifests to all governed repositories
|
* BRIEF: Push XML manifests to all governed repositories
|
||||||
*/
|
*/
|
||||||
@@ -18,8 +18,8 @@ declare(strict_types=1);
|
|||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoCli\CliFramework;
|
||||||
use MokoEnterprise\MokoStandardsParser;
|
use MokoCli\ManifestParser;
|
||||||
|
|
||||||
class PushMokostandardsXmlCli extends CliFramework
|
class PushMokostandardsXmlCli extends CliFramework
|
||||||
{
|
{
|
||||||
@@ -44,10 +44,10 @@ class PushMokostandardsXmlCli extends CliFramework
|
|||||||
$skipStr = $this->getArgument('--skip');
|
$skipStr = $this->getArgument('--skip');
|
||||||
$skipRepos = $skipStr !== '' ? array_map('trim', explode(',', $skipStr)) : [];
|
$skipRepos = $skipStr !== '' ? array_map('trim', explode(',', $skipStr)) : [];
|
||||||
|
|
||||||
$parser = new MokoStandardsParser();
|
$parser = new ManifestParser();
|
||||||
$tmpBase = sys_get_temp_dir() . '/moko-manifest-push-' . getmypid();
|
$tmpBase = sys_get_temp_dir() . '/moko-manifest-push-' . getmypid();
|
||||||
|
|
||||||
echo "=== moko-platform XML Manifest Push ===\n";
|
echo "=== mokocli XML Manifest Push ===\n";
|
||||||
echo "Org: {$giteaOrg}\n";
|
echo "Org: {$giteaOrg}\n";
|
||||||
echo "Mode: " . ($this->dryRun ? "DRY RUN" : "LIVE") . "\n";
|
echo "Mode: " . ($this->dryRun ? "DRY RUN" : "LIVE") . "\n";
|
||||||
if ($repoFilter) {
|
if ($repoFilter) {
|
||||||
@@ -97,8 +97,8 @@ class PushMokostandardsXmlCli extends CliFramework
|
|||||||
'description' => $repo['description'] ?? '',
|
'description' => $repo['description'] ?? '',
|
||||||
'license' => 'GPL-3.0-or-later',
|
'license' => 'GPL-3.0-or-later',
|
||||||
'topics' => $repo['topics'] ?? [],
|
'topics' => $repo['topics'] ?? [],
|
||||||
'language' => $repo['language'] ?? MokoStandardsParser::platformLanguage($platform),
|
'language' => $repo['language'] ?? ManifestParser::platformLanguage($platform),
|
||||||
'package_type' => MokoStandardsParser::platformPackageType($platform),
|
'package_type' => ManifestParser::platformPackageType($platform),
|
||||||
'last_synced' => date('c'),
|
'last_synced' => date('c'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -125,7 +125,7 @@ class PushMokostandardsXmlCli extends CliFramework
|
|||||||
|
|
||||||
// Check if already XML and up-to-date
|
// Check if already XML and up-to-date
|
||||||
$manifestPath = "{$workDir}/.mokogitea/manifest.xml";
|
$manifestPath = "{$workDir}/.mokogitea/manifest.xml";
|
||||||
$existingIsXml = file_exists($manifestPath) && str_contains(file_get_contents($manifestPath), '<moko-platform');
|
$existingIsXml = file_exists($manifestPath) && str_contains(file_get_contents($manifestPath), '<mokocli');
|
||||||
if ($existingIsXml && !$force) {
|
if ($existingIsXml && !$force) {
|
||||||
$existingPlatform = $parser->extractPlatform(file_get_contents($manifestPath));
|
$existingPlatform = $parser->extractPlatform(file_get_contents($manifestPath));
|
||||||
if ($existingPlatform === $platform) {
|
if ($existingPlatform === $platform) {
|
||||||
|
|||||||
+10
-10
@@ -11,7 +11,7 @@
|
|||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: MokoPlatform.Automation
|
* DEFGROUP: MokoPlatform.Automation
|
||||||
* INGROUP: MokoPlatform.Scripts
|
* INGROUP: MokoPlatform.Scripts
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /automation/repo_cleanup.php
|
* PATH: /automation/repo_cleanup.php
|
||||||
* BRIEF: Enterprise repository cleanup — branches, PRs, issues, workflows, labels, logs
|
* BRIEF: Enterprise repository cleanup — branches, PRs, issues, workflows, labels, logs
|
||||||
*/
|
*/
|
||||||
@@ -21,7 +21,7 @@ declare(strict_types=1);
|
|||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\{ApiClient, AuditLogger, CliFramework, Config, GitPlatformAdapter, MetricsCollector, PlatformAdapterFactory};
|
use MokoCli\{ApiClient, AuditLogger, CliFramework, Config, GitPlatformAdapter, MetricsCollector, PlatformAdapterFactory};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enterprise Repository Cleanup
|
* Enterprise Repository Cleanup
|
||||||
@@ -39,14 +39,14 @@ use MokoEnterprise\{ApiClient, AuditLogger, CliFramework, Config, GitPlatformAda
|
|||||||
class RepoCleanup extends CliFramework
|
class RepoCleanup extends CliFramework
|
||||||
{
|
{
|
||||||
private const VERSION = '09.23.00';
|
private const VERSION = '09.23.00';
|
||||||
private const SYNC_PREFIX = 'chore/sync-moko-platform-';
|
private const SYNC_PREFIX = 'chore/sync-mokocli-';
|
||||||
private const CURRENT_BRANCH = 'chore/sync-moko-platform-v04.02.00';
|
private const CURRENT_BRANCH = 'chore/sync-mokocli-v04.02.00';
|
||||||
|
|
||||||
/** Workflow files that have been retired and should be deleted from governed repos. */
|
/** Workflow files that have been retired and should be deleted from governed repos. */
|
||||||
private const RETIRED_WORKFLOWS = [
|
private const RETIRED_WORKFLOWS = [
|
||||||
'build.yml', 'code-quality.yml', 'release-cycle.yml', 'release-pipeline.yml',
|
'build.yml', 'code-quality.yml', 'release-cycle.yml', 'release-pipeline.yml',
|
||||||
'branch-cleanup.yml', 'auto-update-changelog.yml', 'enterprise-issue-manager.yml',
|
'branch-cleanup.yml', 'auto-update-changelog.yml', 'enterprise-issue-manager.yml',
|
||||||
'flush-actions-cache.yml', 'moko-platform-script-runner.yml', 'unified-ci.yml',
|
'flush-actions-cache.yml', 'mokocli-script-runner.yml', 'unified-ci.yml',
|
||||||
'unified-platform-testing.yml', 'reusable-build.yml', 'reusable-ci-validation.yml',
|
'unified-platform-testing.yml', 'reusable-build.yml', 'reusable-ci-validation.yml',
|
||||||
'reusable-deploy.yml', 'reusable-php-quality.yml', 'reusable-platform-testing.yml',
|
'reusable-deploy.yml', 'reusable-php-quality.yml', 'reusable-platform-testing.yml',
|
||||||
'reusable-project-detector.yml', 'reusable-release.yml', 'reusable-script-executor.yml',
|
'reusable-project-detector.yml', 'reusable-release.yml', 'reusable-script-executor.yml',
|
||||||
@@ -98,7 +98,7 @@ class RepoCleanup extends CliFramework
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
$this->logMsg("🧹 moko-platform Repository Cleanup v" . self::VERSION);
|
$this->logMsg("🧹 mokocli Repository Cleanup v" . self::VERSION);
|
||||||
$this->logMsg("Organization: {$org}");
|
$this->logMsg("Organization: {$org}");
|
||||||
$this->logMsg("Current sync branch: " . self::CURRENT_BRANCH);
|
$this->logMsg("Current sync branch: " . self::CURRENT_BRANCH);
|
||||||
if ($this->dryRun) {
|
if ($this->dryRun) {
|
||||||
@@ -225,7 +225,7 @@ class RepoCleanup extends CliFramework
|
|||||||
}
|
}
|
||||||
|
|
||||||
$allRepos = $this->adapter->listOrgRepos($org, $skipArchived);
|
$allRepos = $this->adapter->listOrgRepos($org, $skipArchived);
|
||||||
return array_filter($allRepos, fn($r) => !in_array($r['name'], ['moko-platform', '.github-private'], true));
|
return array_filter($allRepos, fn($r) => !in_array($r['name'], ['mokocli', '.github-private'], true));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Cleanup operations ──────────────────────────────────────────────
|
// ─── Cleanup operations ──────────────────────────────────────────────
|
||||||
@@ -463,9 +463,9 @@ class RepoCleanup extends CliFramework
|
|||||||
private function checkLabels(string $org, string $repo, array &$results): void
|
private function checkLabels(string $org, string $repo, array &$results): void
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$this->api->get("/repos/{$org}/{$repo}/labels/moko-platform");
|
$this->api->get("/repos/{$org}/{$repo}/labels/mokocli");
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$this->logMsg(" ⚠️ Missing 'moko-platform' label");
|
$this->logMsg(" ⚠️ Missing 'mokocli' label");
|
||||||
$results['labels_missing']++;
|
$results['labels_missing']++;
|
||||||
$this->api->resetCircuitBreaker();
|
$this->api->resetCircuitBreaker();
|
||||||
}
|
}
|
||||||
@@ -479,7 +479,7 @@ class RepoCleanup extends CliFramework
|
|||||||
if (preg_match('/^\s*VERSION:\s*(\d{2}\.\d{2}\.\d{2})/m', $content, $m)) {
|
if (preg_match('/^\s*VERSION:\s*(\d{2}\.\d{2}\.\d{2})/m', $content, $m)) {
|
||||||
$version = $m[1];
|
$version = $m[1];
|
||||||
|
|
||||||
// Check manifest.xml for the tracked moko-platform version
|
// Check manifest.xml for the tracked mokocli version
|
||||||
try {
|
try {
|
||||||
$mokoFile = $this->api->get("/repos/{$org}/{$repo}/contents/.mokogitea/manifest.xml");
|
$mokoFile = $this->api->get("/repos/{$org}/{$repo}/contents/.mokogitea/manifest.xml");
|
||||||
$mokoContent = base64_decode($mokoFile['content'] ?? '');
|
$mokoContent = base64_decode($mokoFile['content'] ?? '');
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
#
|
#
|
||||||
# DEFGROUP: MokoPlatform.Automation.ServerAutoheal
|
# DEFGROUP: MokoPlatform.Automation.ServerAutoheal
|
||||||
# INGROUP: MokoPlatform.Automation
|
# INGROUP: MokoPlatform.Automation
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
# PATH: /automation/server-autoheal.sh
|
# PATH: /automation/server-autoheal.sh
|
||||||
# BRIEF: Server auto-heal on unclean restart + split system/content backups
|
# BRIEF: Server auto-heal on unclean restart + split system/content backups
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -0,0 +1,633 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* FILE INFORMATION
|
||||||
|
* DEFGROUP: MokoPlatform.Automation
|
||||||
|
* INGROUP: MokoPlatform.Scripts
|
||||||
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
|
* PATH: /automation/update_dependencies.php
|
||||||
|
* VERSION: 09.38.01
|
||||||
|
* BRIEF: Cross-repo dependency update automation — scan, update, PR, auto-merge
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
use MokoCli\{
|
||||||
|
ApiClient,
|
||||||
|
AuditLogger,
|
||||||
|
CheckpointManager,
|
||||||
|
CircuitBreakerOpen,
|
||||||
|
CliFramework,
|
||||||
|
Config,
|
||||||
|
GitPlatformAdapter,
|
||||||
|
PlatformAdapterFactory,
|
||||||
|
RateLimitExceeded
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cross-Repo Dependency Update Automation
|
||||||
|
*
|
||||||
|
* Scans org repos for outdated Composer/npm dependencies, creates PRs with
|
||||||
|
* changelogs, and optionally auto-merges safe patch updates.
|
||||||
|
*
|
||||||
|
* @see https://git.mokoconsulting.tech/MokoConsulting/mokocli/issues/149
|
||||||
|
*/
|
||||||
|
class UpdateDependencies extends CliFramework
|
||||||
|
{
|
||||||
|
public const VERSION = '01.00.00';
|
||||||
|
|
||||||
|
private const BRANCH_PREFIX = 'chore/deps-update';
|
||||||
|
|
||||||
|
private ApiClient $api;
|
||||||
|
private GitPlatformAdapter $adapter;
|
||||||
|
private AuditLogger $logger;
|
||||||
|
private CheckpointManager $checkpoints;
|
||||||
|
|
||||||
|
/** Summary counters. */
|
||||||
|
private int $reposScanned = 0;
|
||||||
|
private int $reposUpdated = 0;
|
||||||
|
private int $prsCreated = 0;
|
||||||
|
private int $autoMerged = 0;
|
||||||
|
private int $reposFailed = 0;
|
||||||
|
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this->setDescription('Cross-repo dependency update automation');
|
||||||
|
$this->addArgument('--org', 'Organization to scan', 'MokoConsulting');
|
||||||
|
$this->addArgument('--repos', 'Comma-separated list of specific repos', '');
|
||||||
|
$this->addArgument('--exclude', 'Comma-separated list of repos to exclude', '');
|
||||||
|
$this->addArgument('--skip-archived', 'Skip archived repositories', true);
|
||||||
|
$this->addArgument('--type', 'Dependency type: composer, npm, or all', 'all');
|
||||||
|
$this->addArgument('--patch-only', 'Only update patch versions (safe updates)', false);
|
||||||
|
$this->addArgument('--auto-merge', 'Auto-merge PRs with only patch updates', false);
|
||||||
|
$this->addArgument('--resume', 'Resume from checkpoint', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function run(): int
|
||||||
|
{
|
||||||
|
$this->log("Dependency Update Automation v" . self::VERSION, 'INFO');
|
||||||
|
|
||||||
|
if (!$this->initComponents()) {
|
||||||
|
return self::EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$org = $this->getArgument('--org', 'MokoConsulting');
|
||||||
|
$depType = strtolower($this->getArgument('--type', 'all'));
|
||||||
|
$patchOnly = $this->getArgument('--patch-only', false);
|
||||||
|
$autoMerge = $this->getArgument('--auto-merge', false);
|
||||||
|
|
||||||
|
// ── Gather repos ─────────────────────────────────────────────────
|
||||||
|
$repos = $this->gatherRepos($org);
|
||||||
|
if ($repos === null) {
|
||||||
|
return self::EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$total = count($repos);
|
||||||
|
$this->log("Found {$total} repositories to scan", 'INFO');
|
||||||
|
|
||||||
|
// ── Resume support ───────────────────────────────────────────────
|
||||||
|
$completed = [];
|
||||||
|
if ($this->getArgument('--resume', false)) {
|
||||||
|
$checkpoint = $this->checkpoints->load('deps_update');
|
||||||
|
if ($checkpoint) {
|
||||||
|
$completed = $checkpoint['completed'] ?? [];
|
||||||
|
$this->log("Resuming — skipping " . count($completed) . " already-processed repos", 'INFO');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Process each repo ────────────────────────────────────────────
|
||||||
|
$this->section('Scanning repositories for outdated dependencies');
|
||||||
|
|
||||||
|
foreach ($repos as $i => $repo) {
|
||||||
|
$repoName = $repo['name'];
|
||||||
|
$this->progress($i + 1, $total, $repoName);
|
||||||
|
|
||||||
|
if (in_array($repoName, $completed, true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->processRepo($org, $repoName, $depType, $patchOnly, $autoMerge);
|
||||||
|
$completed[] = $repoName;
|
||||||
|
|
||||||
|
$this->checkpoints->save('deps_update', ['completed' => $completed]);
|
||||||
|
} catch (RateLimitExceeded $e) {
|
||||||
|
$this->log("Rate limit hit — checkpoint saved", 'WARNING');
|
||||||
|
break;
|
||||||
|
} catch (CircuitBreakerOpen $e) {
|
||||||
|
$this->log("Circuit breaker open — checkpoint saved", 'WARNING');
|
||||||
|
break;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->log("Failed {$repoName}: {$e->getMessage()}", 'ERROR');
|
||||||
|
$this->reposFailed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->progress($total, $total, '', true);
|
||||||
|
|
||||||
|
// ── Summary ──────────────────────────────────────────────────────
|
||||||
|
$this->section('Summary');
|
||||||
|
$this->printSummary(
|
||||||
|
$this->reposScanned - $this->reposFailed,
|
||||||
|
$this->reposFailed,
|
||||||
|
$this->elapsed()
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->log("Repos scanned: {$this->reposScanned}", 'INFO');
|
||||||
|
$this->log("Repos updated: {$this->reposUpdated}", 'INFO');
|
||||||
|
$this->log("PRs created: {$this->prsCreated}", 'INFO');
|
||||||
|
if ($autoMerge) {
|
||||||
|
$this->log("Auto-merged: {$this->autoMerged}", 'INFO');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($completed) === $total) {
|
||||||
|
$this->checkpoints->clear('deps_update');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->reposFailed > 0 ? self::EXIT_FAILURE : self::EXIT_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Component init ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private function initComponents(): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$config = new Config();
|
||||||
|
$this->api = new ApiClient($config);
|
||||||
|
$this->adapter = PlatformAdapterFactory::create($this->api, $config);
|
||||||
|
$this->logger = new AuditLogger();
|
||||||
|
$this->checkpoints = new CheckpointManager();
|
||||||
|
return true;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->log("Failed to initialise: {$e->getMessage()}", 'ERROR');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Repo gathering ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private function gatherRepos(string $org): ?array
|
||||||
|
{
|
||||||
|
$specificRepos = array_filter(explode(',', $this->getArgument('--repos', '')));
|
||||||
|
$excludeRepos = array_filter(explode(',', $this->getArgument('--exclude', '')));
|
||||||
|
$skipArchived = $this->getArgument('--skip-archived', true);
|
||||||
|
|
||||||
|
// Default exclusions
|
||||||
|
$excludeRepos = array_merge($excludeRepos, [
|
||||||
|
'mokocli', '.mokogitea-private', 'org-profile',
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$repos = $this->adapter->listOrgRepos($org, $skipArchived);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->log("Failed to list repos: {$e->getMessage()}", 'ERROR');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($specificRepos)) {
|
||||||
|
$repos = array_filter($repos, fn($r) => in_array($r['name'], $specificRepos, true));
|
||||||
|
}
|
||||||
|
if (!empty($excludeRepos)) {
|
||||||
|
$repos = array_filter($repos, fn($r) => !in_array($r['name'], $excludeRepos, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values($repos);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Per-repo processing ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
private function processRepo(
|
||||||
|
string $org,
|
||||||
|
string $repoName,
|
||||||
|
string $depType,
|
||||||
|
bool $patchOnly,
|
||||||
|
bool $autoMerge
|
||||||
|
): void {
|
||||||
|
$this->reposScanned++;
|
||||||
|
|
||||||
|
$hasComposer = ($depType === 'all' || $depType === 'composer');
|
||||||
|
$hasNpm = ($depType === 'all' || $depType === 'npm');
|
||||||
|
|
||||||
|
$outdated = [];
|
||||||
|
|
||||||
|
// ── Composer ─────────────────────────────────────────────────
|
||||||
|
if ($hasComposer) {
|
||||||
|
$composerOutdated = $this->scanComposer($org, $repoName, $patchOnly);
|
||||||
|
if ($composerOutdated !== null) {
|
||||||
|
$outdated['composer'] = $composerOutdated;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── npm ──────────────────────────────────────────────────────
|
||||||
|
if ($hasNpm) {
|
||||||
|
$npmOutdated = $this->scanNpm($org, $repoName, $patchOnly);
|
||||||
|
if ($npmOutdated !== null) {
|
||||||
|
$outdated['npm'] = $npmOutdated;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($outdated)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there's already an open deps PR
|
||||||
|
if ($this->hasExistingDepsPR($org, $repoName)) {
|
||||||
|
$this->log(" {$repoName}: existing deps PR found — skipping", 'INFO');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->reposUpdated++;
|
||||||
|
|
||||||
|
// ── Create PR ────────────────────────────────────────────────
|
||||||
|
$totalUpdates = 0;
|
||||||
|
$allPatchOnly = true;
|
||||||
|
|
||||||
|
foreach ($outdated as $type => $packages) {
|
||||||
|
$totalUpdates += count($packages);
|
||||||
|
foreach ($packages as $pkg) {
|
||||||
|
if (!$this->isPatchUpdate($pkg['current'] ?? '', $pkg['latest'] ?? '')) {
|
||||||
|
$allPatchOnly = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$title = "chore(deps): update {$totalUpdates} " . ($totalUpdates === 1 ? 'dependency' : 'dependencies');
|
||||||
|
$body = $this->buildPrBody($repoName, $outdated);
|
||||||
|
$branch = self::BRANCH_PREFIX . '-' . date('Y-m-d');
|
||||||
|
|
||||||
|
if ($this->dryRun) {
|
||||||
|
$this->log("[dry-run] Would create PR in {$repoName}: {$title}", 'INFO');
|
||||||
|
foreach ($outdated as $type => $packages) {
|
||||||
|
foreach ($packages as $pkg) {
|
||||||
|
$this->log(" [{$type}] {$pkg['name']}: {$pkg['current']} → {$pkg['latest']}", 'INFO');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Clone repo, run updates, push branch
|
||||||
|
$prNumber = $this->cloneUpdateAndPR($org, $repoName, $branch, $title, $body, $outdated);
|
||||||
|
|
||||||
|
if ($prNumber > 0) {
|
||||||
|
$this->prsCreated++;
|
||||||
|
$this->log(" {$repoName}: PR #{$prNumber} created", 'INFO');
|
||||||
|
|
||||||
|
// Auto-merge if all updates are patch-level
|
||||||
|
if ($autoMerge && $allPatchOnly && $prNumber > 0) {
|
||||||
|
$this->tryAutoMerge($org, $repoName, $prNumber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->log(" {$repoName}: PR creation failed — {$e->getMessage()}", 'ERROR');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Composer scanning ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private function scanComposer(string $org, string $repoName, bool $patchOnly): ?array
|
||||||
|
{
|
||||||
|
// Check if repo has composer.json
|
||||||
|
try {
|
||||||
|
$this->adapter->getFileContents($org, $repoName, 'composer.json');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if repo has composer.lock
|
||||||
|
try {
|
||||||
|
$this->adapter->getFileContents($org, $repoName, 'composer.lock');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone to temp dir and run composer outdated
|
||||||
|
$tmpDir = sys_get_temp_dir() . '/moko_deps_' . $repoName . '_' . getmypid();
|
||||||
|
@mkdir($tmpDir, 0700, true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$cloneUrl = $this->adapter->getCloneUrl($org, $repoName);
|
||||||
|
$cmd = sprintf(
|
||||||
|
'git clone --depth 1 --quiet %s %s 2>/dev/null',
|
||||||
|
escapeshellarg($cloneUrl),
|
||||||
|
escapeshellarg($tmpDir)
|
||||||
|
);
|
||||||
|
exec($cmd, $output, $exitCode);
|
||||||
|
if ($exitCode !== 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run composer outdated
|
||||||
|
$flags = $patchOnly ? '--minor-only' : '';
|
||||||
|
$cmd = sprintf(
|
||||||
|
'composer outdated --format=json --no-interaction %s --working-dir=%s 2>/dev/null',
|
||||||
|
$flags,
|
||||||
|
escapeshellarg($tmpDir)
|
||||||
|
);
|
||||||
|
$json = shell_exec($cmd);
|
||||||
|
if ($json === null || $json === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode($json, true);
|
||||||
|
$installed = $data['installed'] ?? [];
|
||||||
|
|
||||||
|
if (empty($installed)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$outdated = [];
|
||||||
|
foreach ($installed as $pkg) {
|
||||||
|
// Skip abandoned/dev packages
|
||||||
|
if (($pkg['abandoned'] ?? false) || str_starts_with($pkg['version'] ?? '', 'dev-')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$outdated[] = [
|
||||||
|
'name' => $pkg['name'] ?? '',
|
||||||
|
'current' => $pkg['version'] ?? '',
|
||||||
|
'latest' => $pkg['latest'] ?? '',
|
||||||
|
'status' => $pkg['latest-status'] ?? 'unknown',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return empty($outdated) ? null : $outdated;
|
||||||
|
} finally {
|
||||||
|
// Cleanup
|
||||||
|
if (is_dir($tmpDir)) {
|
||||||
|
exec(sprintf('rm -rf %s', escapeshellarg($tmpDir)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── npm scanning ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private function scanNpm(string $org, string $repoName, bool $patchOnly): ?array
|
||||||
|
{
|
||||||
|
// Check if repo has package.json
|
||||||
|
try {
|
||||||
|
$this->adapter->getFileContents($org, $repoName, 'package.json');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for lock file
|
||||||
|
$hasLock = false;
|
||||||
|
foreach (['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml'] as $lockFile) {
|
||||||
|
try {
|
||||||
|
$this->adapter->getFileContents($org, $repoName, $lockFile);
|
||||||
|
$hasLock = true;
|
||||||
|
break;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$hasLock) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tmpDir = sys_get_temp_dir() . '/moko_deps_npm_' . $repoName . '_' . getmypid();
|
||||||
|
@mkdir($tmpDir, 0700, true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$cloneUrl = $this->adapter->getCloneUrl($org, $repoName);
|
||||||
|
exec(sprintf('git clone --depth 1 --quiet %s %s 2>/dev/null',
|
||||||
|
escapeshellarg($cloneUrl), escapeshellarg($tmpDir)));
|
||||||
|
|
||||||
|
if (!file_exists("{$tmpDir}/package.json")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install deps first (needed for npm outdated)
|
||||||
|
exec(sprintf('cd %s && npm install --silent 2>/dev/null', escapeshellarg($tmpDir)));
|
||||||
|
|
||||||
|
$json = shell_exec(sprintf('cd %s && npm outdated --json 2>/dev/null', escapeshellarg($tmpDir)));
|
||||||
|
if ($json === null || $json === '' || $json === '{}') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode($json, true);
|
||||||
|
if (!is_array($data) || empty($data)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$outdated = [];
|
||||||
|
foreach ($data as $name => $info) {
|
||||||
|
$current = $info['current'] ?? '';
|
||||||
|
$wanted = $info['wanted'] ?? '';
|
||||||
|
$latest = $info['latest'] ?? '';
|
||||||
|
$target = $patchOnly ? $wanted : $latest;
|
||||||
|
|
||||||
|
if ($current === $target || $target === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$outdated[] = [
|
||||||
|
'name' => $name,
|
||||||
|
'current' => $current,
|
||||||
|
'latest' => $target,
|
||||||
|
'status' => ($current === $wanted) ? 'up-to-date' : 'outdated',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return empty($outdated) ? null : $outdated;
|
||||||
|
} finally {
|
||||||
|
if (is_dir($tmpDir)) {
|
||||||
|
exec(sprintf('rm -rf %s', escapeshellarg($tmpDir)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── PR creation ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private function cloneUpdateAndPR(
|
||||||
|
string $org,
|
||||||
|
string $repoName,
|
||||||
|
string $branch,
|
||||||
|
string $title,
|
||||||
|
string $body,
|
||||||
|
array $outdated
|
||||||
|
): int {
|
||||||
|
$tmpDir = sys_get_temp_dir() . '/moko_deps_pr_' . $repoName . '_' . getmypid();
|
||||||
|
@mkdir($tmpDir, 0700, true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$cloneUrl = $this->adapter->getCloneUrl($org, $repoName);
|
||||||
|
exec(sprintf('git clone --quiet %s %s 2>/dev/null',
|
||||||
|
escapeshellarg($cloneUrl), escapeshellarg($tmpDir)));
|
||||||
|
|
||||||
|
// Create branch
|
||||||
|
exec(sprintf('git -C %s checkout -b %s 2>/dev/null',
|
||||||
|
escapeshellarg($tmpDir), escapeshellarg($branch)));
|
||||||
|
|
||||||
|
$updated = false;
|
||||||
|
|
||||||
|
// Run composer update if needed
|
||||||
|
if (isset($outdated['composer'])) {
|
||||||
|
$packages = array_column($outdated['composer'], 'name');
|
||||||
|
$cmd = sprintf(
|
||||||
|
'cd %s && composer update %s --no-interaction --quiet 2>/dev/null',
|
||||||
|
escapeshellarg($tmpDir),
|
||||||
|
implode(' ', array_map('escapeshellarg', $packages))
|
||||||
|
);
|
||||||
|
exec($cmd, $output, $exitCode);
|
||||||
|
if ($exitCode === 0) {
|
||||||
|
$updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run npm update if needed
|
||||||
|
if (isset($outdated['npm'])) {
|
||||||
|
$packages = array_column($outdated['npm'], 'name');
|
||||||
|
$cmd = sprintf(
|
||||||
|
'cd %s && npm update %s --save 2>/dev/null',
|
||||||
|
escapeshellarg($tmpDir),
|
||||||
|
implode(' ', array_map('escapeshellarg', $packages))
|
||||||
|
);
|
||||||
|
exec($cmd, $output, $exitCode);
|
||||||
|
if ($exitCode === 0) {
|
||||||
|
$updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$updated) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit and push
|
||||||
|
exec(sprintf('git -C %s config user.email "gitea-actions[bot]@mokoconsulting.tech"', escapeshellarg($tmpDir)));
|
||||||
|
exec(sprintf('git -C %s config user.name "gitea-actions[bot]"', escapeshellarg($tmpDir)));
|
||||||
|
exec(sprintf('git -C %s add -A', escapeshellarg($tmpDir)));
|
||||||
|
|
||||||
|
// Check if there are actual changes
|
||||||
|
exec(sprintf('git -C %s diff --cached --quiet', escapeshellarg($tmpDir)), $output, $diffExit);
|
||||||
|
if ($diffExit === 0) {
|
||||||
|
return 0; // No changes
|
||||||
|
}
|
||||||
|
|
||||||
|
exec(sprintf('git -C %s commit -m %s',
|
||||||
|
escapeshellarg($tmpDir),
|
||||||
|
escapeshellarg($title . " [skip ci]")));
|
||||||
|
exec(sprintf('git -C %s push origin %s 2>/dev/null',
|
||||||
|
escapeshellarg($tmpDir), escapeshellarg($branch)), $output, $pushExit);
|
||||||
|
|
||||||
|
if ($pushExit !== 0) {
|
||||||
|
$this->log(" {$repoName}: push failed", 'ERROR');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create PR via API
|
||||||
|
$defaultBranch = $this->getDefaultBranch($org, $repoName);
|
||||||
|
$pr = $this->adapter->createPullRequest(
|
||||||
|
$org, $repoName, $title, $branch, $defaultBranch, $body, [
|
||||||
|
'labels' => ['dependencies'],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (int) ($pr['number'] ?? 0);
|
||||||
|
} finally {
|
||||||
|
if (is_dir($tmpDir)) {
|
||||||
|
exec(sprintf('rm -rf %s', escapeshellarg($tmpDir)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Auto-merge ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private function tryAutoMerge(string $org, string $repoName, int $prNumber): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->api->put(
|
||||||
|
"/repos/{$org}/{$repoName}/pulls/{$prNumber}/merge",
|
||||||
|
['Do' => 'squash', 'merge_message_field' => 'chore(deps): auto-merge patch updates']
|
||||||
|
);
|
||||||
|
$this->autoMerged++;
|
||||||
|
$this->log(" {$repoName}: PR #{$prNumber} auto-merged", 'INFO');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->log(" {$repoName}: auto-merge failed — {$e->getMessage()}", 'WARNING');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Helpers ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private function hasExistingDepsPR(string $org, string $repoName): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$prs = $this->adapter->listPullRequests($org, $repoName, ['state' => 'open']);
|
||||||
|
foreach ($prs as $pr) {
|
||||||
|
if (str_starts_with($pr['head']['ref'] ?? '', self::BRANCH_PREFIX)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Ignore — proceed with creating PR
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getDefaultBranch(string $org, string $repoName): string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$repo = $this->api->get("/repos/{$org}/{$repoName}");
|
||||||
|
return $repo['default_branch'] ?? 'main';
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return 'main';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isPatchUpdate(string $current, string $latest): bool
|
||||||
|
{
|
||||||
|
$cur = explode('.', ltrim($current, 'v'));
|
||||||
|
$lat = explode('.', ltrim($latest, 'v'));
|
||||||
|
|
||||||
|
if (count($cur) < 3 || count($lat) < 3) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same major and minor, only patch differs
|
||||||
|
return $cur[0] === $lat[0] && $cur[1] === $lat[1] && $cur[2] !== $lat[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildPrBody(string $repoName, array $outdated): string
|
||||||
|
{
|
||||||
|
$lines = [
|
||||||
|
"## Dependency Updates",
|
||||||
|
"",
|
||||||
|
"**Repository**: `{$repoName}`",
|
||||||
|
"**Scanned**: " . date('Y-m-d H:i:s'),
|
||||||
|
"",
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($outdated as $type => $packages) {
|
||||||
|
$lines[] = "### " . ucfirst($type);
|
||||||
|
$lines[] = "";
|
||||||
|
$lines[] = "| Package | Current | Latest | Type |";
|
||||||
|
$lines[] = "|---------|---------|--------|------|";
|
||||||
|
|
||||||
|
foreach ($packages as $pkg) {
|
||||||
|
$updateType = $this->isPatchUpdate($pkg['current'], $pkg['latest']) ? 'patch' : 'minor/major';
|
||||||
|
$lines[] = "| `{$pkg['name']}` | {$pkg['current']} | {$pkg['latest']} | {$updateType} |";
|
||||||
|
}
|
||||||
|
|
||||||
|
$lines[] = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
$lines[] = "---";
|
||||||
|
$lines[] = "*Auto-generated by `moko deps:update`*";
|
||||||
|
|
||||||
|
return implode("\n", $lines);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$script = new UpdateDependencies('update_dependencies', 'Cross-repo dependency update automation');
|
||||||
|
exit($script->execute());
|
||||||
@@ -9,11 +9,11 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: MokoStandards.CLI
|
* DEFGROUP: MokoCli.CLI
|
||||||
* INGROUP: MokoStandards
|
* INGROUP: MokoCli
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /bin/moko
|
* PATH: /bin/moko
|
||||||
* BRIEF: Unified CLI dispatcher — run any MokoStandards script without needing GitHub Actions
|
* BRIEF: Unified CLI dispatcher — run any MokoCli script without needing GitHub Actions
|
||||||
*
|
*
|
||||||
* USAGE
|
* USAGE
|
||||||
* php bin/moko <command> [options] (all platforms)
|
* php bin/moko <command> [options] (all platforms)
|
||||||
@@ -89,6 +89,7 @@ const COMMAND_MAP = [
|
|||||||
|
|
||||||
// Automation
|
// Automation
|
||||||
'sync' => 'automation/bulk_sync.php',
|
'sync' => 'automation/bulk_sync.php',
|
||||||
|
'deps:update' => 'automation/update_dependencies.php',
|
||||||
'automation:cleanup' => 'automation/repo_cleanup.php',
|
'automation:cleanup' => 'automation/repo_cleanup.php',
|
||||||
'automation:migrate-gitea' => 'automation/migrate_to_gitea.php',
|
'automation:migrate-gitea' => 'automation/migrate_to_gitea.php',
|
||||||
|
|
||||||
@@ -177,6 +178,7 @@ const COMMAND_MAP = [
|
|||||||
'repo:archive' => 'cli/archive_repo.php',
|
'repo:archive' => 'cli/archive_repo.php',
|
||||||
'repo:scaffold-client' => 'cli/scaffold_client.php',
|
'repo:scaffold-client' => 'cli/scaffold_client.php',
|
||||||
'repo:provision' => 'cli/client_provision.php',
|
'repo:provision' => 'cli/client_provision.php',
|
||||||
|
'repo:wizard' => 'cli/repo_wizard.php',
|
||||||
'repo:rename-branch' => 'cli/branch_rename.php',
|
'repo:rename-branch' => 'cli/branch_rename.php',
|
||||||
'repo:reset-dev' => 'cli/dev_branch_reset.php',
|
'repo:reset-dev' => 'cli/dev_branch_reset.php',
|
||||||
|
|
||||||
@@ -198,6 +200,7 @@ const COMMAND_MAP = [
|
|||||||
'deploy:sftp' => 'deploy/deploy-sftp.php',
|
'deploy:sftp' => 'deploy/deploy-sftp.php',
|
||||||
'deploy:backup' => 'deploy/backup-before-deploy.php',
|
'deploy:backup' => 'deploy/backup-before-deploy.php',
|
||||||
'deploy:health-check' => 'deploy/health-check.php',
|
'deploy:health-check' => 'deploy/health-check.php',
|
||||||
|
'deploy:verify' => 'deploy/deploy-and-verify.php',
|
||||||
'deploy:rollback' => 'deploy/rollback-joomla.php',
|
'deploy:rollback' => 'deploy/rollback-joomla.php',
|
||||||
'deploy:sync' => 'deploy/sync-joomla.php',
|
'deploy:sync' => 'deploy/sync-joomla.php',
|
||||||
|
|
||||||
@@ -220,6 +223,9 @@ const COMMAND_MAP = [
|
|||||||
// Licensing
|
// Licensing
|
||||||
'license' => 'cli/license_manage.php',
|
'license' => 'cli/license_manage.php',
|
||||||
|
|
||||||
|
// Security
|
||||||
|
'security:advisories' => 'security/advisory_scan.php',
|
||||||
|
|
||||||
// Shell completion
|
// Shell completion
|
||||||
'completion' => 'cli/completion.php',
|
'completion' => 'cli/completion.php',
|
||||||
|
|
||||||
@@ -292,10 +298,10 @@ function printHelp(): void
|
|||||||
{
|
{
|
||||||
echo <<<'HELP'
|
echo <<<'HELP'
|
||||||
╔══════════════════════════════════════════════════════════╗
|
╔══════════════════════════════════════════════════════════╗
|
||||||
║ MokoStandards CLI (bin/moko) ║
|
║ MokoCli CLI (bin/moko) ║
|
||||||
╚══════════════════════════════════════════════════════════╝
|
╚══════════════════════════════════════════════════════════╝
|
||||||
|
|
||||||
Run any MokoStandards script locally without GitHub Actions.
|
Run any MokoCli script locally without GitHub Actions.
|
||||||
|
|
||||||
USAGE
|
USAGE
|
||||||
php bin/moko <command> [options] (all platforms)
|
php bin/moko <command> [options] (all platforms)
|
||||||
@@ -397,7 +403,7 @@ function loadPluginCommands(): array
|
|||||||
$commands = [];
|
$commands = [];
|
||||||
|
|
||||||
foreach (glob("{$pluginDir}/*Plugin.php") as $file) {
|
foreach (glob("{$pluginDir}/*Plugin.php") as $file) {
|
||||||
$className = 'MokoEnterprise\\Plugins\\'
|
$className = 'MokoCli\\Plugins\\'
|
||||||
. pathinfo($file, PATHINFO_FILENAME);
|
. pathinfo($file, PATHINFO_FILENAME);
|
||||||
|
|
||||||
if (!class_exists($className)) {
|
if (!class_exists($className)) {
|
||||||
|
|||||||
@@ -8,9 +8,9 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: moko-platform.CLI
|
* DEFGROUP: mokocli.CLI
|
||||||
* INGROUP: moko-platform
|
* INGROUP: mokocli
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /cli/archive_repo.php
|
* PATH: /cli/archive_repo.php
|
||||||
* BRIEF: Gracefully retire a governed repository — archive, close issues/PRs, remove sync def
|
* BRIEF: Gracefully retire a governed repository — archive, close issues/PRs, remove sync def
|
||||||
*/
|
*/
|
||||||
@@ -20,9 +20,9 @@ declare(strict_types=1);
|
|||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoCli\CliFramework;
|
||||||
use MokoEnterprise\Config;
|
use MokoCli\Config;
|
||||||
use MokoEnterprise\PlatformAdapterFactory;
|
use MokoCli\PlatformAdapterFactory;
|
||||||
|
|
||||||
class ArchiveRepoCli extends CliFramework
|
class ArchiveRepoCli extends CliFramework
|
||||||
{
|
{
|
||||||
@@ -135,7 +135,7 @@ class ArchiveRepoCli extends CliFramework
|
|||||||
try {
|
try {
|
||||||
$issue = $adapter->createIssue(
|
$issue = $adapter->createIssue(
|
||||||
$org,
|
$org,
|
||||||
'moko-platform',
|
'mokocli',
|
||||||
"chore: archived repository {$repoName}",
|
"chore: archived repository {$repoName}",
|
||||||
"## Repository Archived\n\n"
|
"## Repository Archived\n\n"
|
||||||
. "**Repository:** `{$org}/{$repoName}`\n"
|
. "**Repository:** `{$org}/{$repoName}`\n"
|
||||||
@@ -150,7 +150,7 @@ class ArchiveRepoCli extends CliFramework
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
if (isset($issue['number'])) {
|
if (isset($issue['number'])) {
|
||||||
echo " Archival record: moko-platform#{$issue['number']}\n";
|
echo " Archival record: mokocli#{$issue['number']}\n";
|
||||||
}
|
}
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
echo " Warning: could not create archival record: " . $e->getMessage() . "\n";
|
echo " Warning: could not create archival record: " . $e->getMessage() . "\n";
|
||||||
|
|||||||
+2
-2
@@ -16,7 +16,7 @@
|
|||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: MokoPlatform.Enterprise.CLI
|
* DEFGROUP: MokoPlatform.Enterprise.CLI
|
||||||
* INGROUP: MokoPlatform.Enterprise
|
* INGROUP: MokoPlatform.Enterprise
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /cli/audit_query.php
|
* PATH: /cli/audit_query.php
|
||||||
* BRIEF: Search, filter, and export audit logs
|
* BRIEF: Search, filter, and export audit logs
|
||||||
*/
|
*/
|
||||||
@@ -25,7 +25,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoCli\CliFramework;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CLI tool to search, filter, and export audit logs.
|
* CLI tool to search, filter, and export audit logs.
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: moko-platform.CLI
|
* DEFGROUP: mokocli.CLI
|
||||||
* INGROUP: moko-platform
|
* INGROUP: mokocli
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /cli/badge_update.php
|
* PATH: /cli/badge_update.php
|
||||||
* BRIEF: Update [VERSION: XX.XX.XX] badges in all markdown files
|
* BRIEF: Update [VERSION: XX.XX.XX] badges in all markdown files
|
||||||
*/
|
*/
|
||||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoCli\CliFramework;
|
||||||
|
|
||||||
class BadgeUpdateCli extends CliFramework
|
class BadgeUpdateCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,11 +6,11 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: moko-platform.CLI
|
* DEFGROUP: mokocli.CLI
|
||||||
* INGROUP: moko-platform
|
* INGROUP: mokocli
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /cli/branch_rename.php
|
* PATH: /cli/branch_rename.php
|
||||||
* VERSION: 09.23.00
|
* VERSION: 09.38.01
|
||||||
* BRIEF: Rename a git branch via Gitea API (create new, update PR, delete old)
|
* BRIEF: Rename a git branch via Gitea API (create new, update PR, delete old)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoCli\CliFramework;
|
||||||
|
|
||||||
class BranchRenameCli extends CliFramework
|
class BranchRenameCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,11 +8,11 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: moko-platform.CLI
|
* DEFGROUP: mokocli.CLI
|
||||||
* INGROUP: moko-platform
|
* INGROUP: mokocli
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /cli/bulk_workflow_push.php
|
* PATH: /cli/bulk_workflow_push.php
|
||||||
* VERSION: 09.23.00
|
* VERSION: 09.38.01
|
||||||
* BRIEF: Push a workflow file to all governed repos via the Gitea Contents API
|
* BRIEF: Push a workflow file to all governed repos via the Gitea Contents API
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoCli\CliFramework;
|
||||||
|
|
||||||
class BulkWorkflowPushCli extends CliFramework
|
class BulkWorkflowPushCli extends CliFramework
|
||||||
{
|
{
|
||||||
@@ -154,7 +154,7 @@ class BulkWorkflowPushCli extends CliFramework
|
|||||||
'content' => $encodedContent,
|
'content' => $encodedContent,
|
||||||
'sha' => $remoteSha,
|
'sha' => $remoteSha,
|
||||||
'message' => "chore: sync {$destPath} "
|
'message' => "chore: sync {$destPath} "
|
||||||
. "from moko-platform [skip ci]",
|
. "from mokocli [skip ci]",
|
||||||
'branch' => $branch,
|
'branch' => $branch,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -184,7 +184,7 @@ class BulkWorkflowPushCli extends CliFramework
|
|||||||
$payload = json_encode([
|
$payload = json_encode([
|
||||||
'content' => $encodedContent,
|
'content' => $encodedContent,
|
||||||
'message' => "chore: add {$destPath} "
|
'message' => "chore: add {$destPath} "
|
||||||
. "from moko-platform [skip ci]",
|
. "from mokocli [skip ci]",
|
||||||
'branch' => $branch,
|
'branch' => $branch,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -8,11 +8,11 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: moko-platform.CLI
|
* DEFGROUP: mokocli.CLI
|
||||||
* INGROUP: moko-platform
|
* INGROUP: mokocli
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /cli/bulk_workflow_trigger.php
|
* PATH: /cli/bulk_workflow_trigger.php
|
||||||
* VERSION: 09.23.00
|
* VERSION: 09.38.01
|
||||||
* BRIEF: Trigger a workflow across multiple repos at once
|
* BRIEF: Trigger a workflow across multiple repos at once
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoCli\CliFramework;
|
||||||
|
|
||||||
class BulkWorkflowTriggerCli extends CliFramework
|
class BulkWorkflowTriggerCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: moko-platform.CLI
|
* DEFGROUP: mokocli.CLI
|
||||||
* INGROUP: moko-platform
|
* INGROUP: mokocli
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /cli/changelog_promote.php
|
* PATH: /cli/changelog_promote.php
|
||||||
* BRIEF: Promote [Unreleased] section in CHANGELOG.md to a versioned entry
|
* BRIEF: Promote [Unreleased] section in CHANGELOG.md to a versioned entry
|
||||||
*/
|
*/
|
||||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoCli\CliFramework;
|
||||||
|
|
||||||
class ChangelogPromoteCli extends CliFramework
|
class ChangelogPromoteCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: moko-platform.CLI
|
* DEFGROUP: mokocli.CLI
|
||||||
* INGROUP: moko-platform
|
* INGROUP: mokocli
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /cli/changelog_prune.php
|
* PATH: /cli/changelog_prune.php
|
||||||
* BRIEF: Prune old CHANGELOG.md entries — keeps [Unreleased] + last N releases
|
* BRIEF: Prune old CHANGELOG.md entries — keeps [Unreleased] + last N releases
|
||||||
*/
|
*/
|
||||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoCli\CliFramework;
|
||||||
|
|
||||||
class ChangelogPruneCli extends CliFramework
|
class ChangelogPruneCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,11 +8,11 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: moko-platform.CLI
|
* DEFGROUP: mokocli.CLI
|
||||||
* INGROUP: moko-platform
|
* INGROUP: mokocli
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /cli/client_dashboard.php
|
* PATH: /cli/client_dashboard.php
|
||||||
* VERSION: 09.23.00
|
* VERSION: 09.38.01
|
||||||
* BRIEF: Generate unified client dashboard HTML
|
* BRIEF: Generate unified client dashboard HTML
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoCli\CliFramework;
|
||||||
|
|
||||||
class ClientDashboardCli extends CliFramework
|
class ClientDashboardCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: moko-platform.CLI
|
* DEFGROUP: mokocli.CLI
|
||||||
* INGROUP: moko-platform
|
* INGROUP: mokocli
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /cli/client_health_check.php
|
* PATH: /cli/client_health_check.php
|
||||||
* BRIEF: Verify a client site's update server, installed version, and release availability
|
* BRIEF: Verify a client site's update server, installed version, and release availability
|
||||||
*/
|
*/
|
||||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoCli\CliFramework;
|
||||||
|
|
||||||
class ClientHealthCheckCli extends CliFramework
|
class ClientHealthCheckCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,11 +8,11 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: moko-platform.CLI
|
* DEFGROUP: mokocli.CLI
|
||||||
* INGROUP: moko-platform
|
* INGROUP: mokocli
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /cli/client_inventory.php
|
* PATH: /cli/client_inventory.php
|
||||||
* VERSION: 09.23.00
|
* VERSION: 09.38.01
|
||||||
* BRIEF: Discover and list all client-waas repos with their server configuration status
|
* BRIEF: Discover and list all client-waas repos with their server configuration status
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoCli\CliFramework;
|
||||||
|
|
||||||
class ClientInventoryCli extends CliFramework
|
class ClientInventoryCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,11 +8,11 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: moko-platform.CLI
|
* DEFGROUP: mokocli.CLI
|
||||||
* INGROUP: moko-platform
|
* INGROUP: mokocli
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /cli/client_provision.php
|
* PATH: /cli/client_provision.php
|
||||||
* VERSION: 09.23.00
|
* VERSION: 09.38.01
|
||||||
* BRIEF: Provision a new client environment end-to-end
|
* BRIEF: Provision a new client environment end-to-end
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoCli\CliFramework;
|
||||||
|
|
||||||
class ClientProvisionCli extends CliFramework
|
class ClientProvisionCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
+4
-4
@@ -6,9 +6,9 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: moko-platform.CLI
|
* DEFGROUP: mokocli.CLI
|
||||||
* INGROUP: moko-platform
|
* INGROUP: mokocli
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /cli/completion.php
|
* PATH: /cli/completion.php
|
||||||
* BRIEF: Generate bash/zsh tab completion scripts for bin/moko
|
* BRIEF: Generate bash/zsh tab completion scripts for bin/moko
|
||||||
*/
|
*/
|
||||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoCli\CliFramework;
|
||||||
|
|
||||||
class CompletionCli extends CliFramework
|
class CompletionCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
+12
-12
@@ -8,9 +8,9 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: moko-platform.CLI
|
* DEFGROUP: mokocli.CLI
|
||||||
* INGROUP: moko-platform
|
* INGROUP: mokocli
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /cli/create_project.php
|
* PATH: /cli/create_project.php
|
||||||
* BRIEF: Create baseline GitHub Projects for repositories with standard fields and views
|
* BRIEF: Create baseline GitHub Projects for repositories with standard fields and views
|
||||||
*/
|
*/
|
||||||
@@ -19,12 +19,12 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoCli\CliFramework;
|
||||||
|
|
||||||
class CreateProjectCli extends CliFramework
|
class CreateProjectCli extends CliFramework
|
||||||
{
|
{
|
||||||
/** @var string[] */
|
/** @var string[] */
|
||||||
private array $ALWAYS_EXCLUDE = ['moko-platform', '.github-private'];
|
private array $ALWAYS_EXCLUDE = ['mokocli', '.github-private'];
|
||||||
|
|
||||||
/** @var array<string, string> */
|
/** @var array<string, string> */
|
||||||
private array $PLATFORM_TO_TYPE = [
|
private array $PLATFORM_TO_TYPE = [
|
||||||
@@ -80,10 +80,10 @@ class CreateProjectCli extends CliFramework
|
|||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
$config = \MokoEnterprise\Config::load();
|
$config = \MokoCli\Config::load();
|
||||||
$platformName = $config->getString('platform', 'gitea');
|
$platformName = $config->getString('platform', 'gitea');
|
||||||
try {
|
try {
|
||||||
$adapter = \MokoEnterprise\PlatformAdapterFactory::create($config);
|
$adapter = \MokoCli\PlatformAdapterFactory::create($config);
|
||||||
$api = $adapter->getApiClient();
|
$api = $adapter->getApiClient();
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$this->log('ERROR', "Platform initialization failed: " . $e->getMessage());
|
$this->log('ERROR', "Platform initialization failed: " . $e->getMessage());
|
||||||
@@ -183,7 +183,7 @@ class CreateProjectCli extends CliFramework
|
|||||||
CURLOPT_HTTPHEADER => [
|
CURLOPT_HTTPHEADER => [
|
||||||
'Authorization: bearer ' . $token,
|
'Authorization: bearer ' . $token,
|
||||||
'Content-Type: application/json',
|
'Content-Type: application/json',
|
||||||
'User-Agent: moko-platform-CreateProject',
|
'User-Agent: mokocli-CreateProject',
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
$body = (string) curl_exec($ch);
|
$body = (string) curl_exec($ch);
|
||||||
@@ -205,7 +205,7 @@ class CreateProjectCli extends CliFramework
|
|||||||
return $data['data'] ?? [];
|
return $data['data'] ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function restGet(string $path, string $token, ?\MokoEnterprise\ApiClient $apiClient = null): array
|
private function restGet(string $path, string $token, ?\MokoCli\ApiClient $apiClient = null): array
|
||||||
{
|
{
|
||||||
if ($apiClient !== null) {
|
if ($apiClient !== null) {
|
||||||
try {
|
try {
|
||||||
@@ -217,7 +217,7 @@ class CreateProjectCli extends CliFramework
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function detectRepoPlatform(string $org, string $repo, string $token, ?\MokoEnterprise\ApiClient $apiClient = null): string
|
private function detectRepoPlatform(string $org, string $repo, string $token, ?\MokoCli\ApiClient $apiClient = null): string
|
||||||
{
|
{
|
||||||
foreach (['.github/.mokostandards', '.mokogitea/.mokostandards', '.mokostandards'] as $path) {
|
foreach (['.github/.mokostandards', '.mokogitea/.mokostandards', '.mokostandards'] as $path) {
|
||||||
$data = $this->restGet("repos/{$org}/{$repo}/contents/{$path}", $token, $apiClient);
|
$data = $this->restGet("repos/{$org}/{$repo}/contents/{$path}", $token, $apiClient);
|
||||||
@@ -422,14 +422,14 @@ class CreateProjectCli extends CliFramework
|
|||||||
updateProjectV2(input: {
|
updateProjectV2(input: {
|
||||||
projectId: $projectId,
|
projectId: $projectId,
|
||||||
shortDescription: $shortDescription,
|
shortDescription: $shortDescription,
|
||||||
readme: "Managed by moko-platform. Run `php cli/create_project.php` to regenerate."
|
readme: "Managed by mokocli. Run `php cli/create_project.php` to regenerate."
|
||||||
}) {
|
}) {
|
||||||
projectV2 { id }
|
projectV2 { id }
|
||||||
}
|
}
|
||||||
}',
|
}',
|
||||||
[
|
[
|
||||||
'projectId' => $projectId,
|
'projectId' => $projectId,
|
||||||
'shortDescription' => "Standard project board for {$repo}. Auto-created by moko-platform.",
|
'shortDescription' => "Standard project board for {$repo}. Auto-created by mokocli.",
|
||||||
],
|
],
|
||||||
$token
|
$token
|
||||||
);
|
);
|
||||||
|
|||||||
+21
-21
@@ -8,11 +8,11 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: moko-platform.CLI
|
* DEFGROUP: mokocli.CLI
|
||||||
* INGROUP: moko-platform
|
* INGROUP: mokocli
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /cli/create_repo.php
|
* PATH: /cli/create_repo.php
|
||||||
* BRIEF: Scaffold a new governed repository with full moko-platform baseline
|
* BRIEF: Scaffold a new governed repository with full mokocli baseline
|
||||||
*/
|
*/
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
@@ -20,15 +20,15 @@ declare(strict_types=1);
|
|||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoCli\CliFramework;
|
||||||
use MokoEnterprise\Config;
|
use MokoCli\Config;
|
||||||
use MokoEnterprise\PlatformAdapterFactory;
|
use MokoCli\PlatformAdapterFactory;
|
||||||
|
|
||||||
class CreateRepoCli extends CliFramework
|
class CreateRepoCli extends CliFramework
|
||||||
{
|
{
|
||||||
protected function configure(): void
|
protected function configure(): void
|
||||||
{
|
{
|
||||||
$this->setDescription('Scaffold a new governed repository with full moko-platform baseline');
|
$this->setDescription('Scaffold a new governed repository with full mokocli baseline');
|
||||||
$this->addArgument('--name', 'Repository name', null);
|
$this->addArgument('--name', 'Repository name', null);
|
||||||
$this->addArgument('--type', 'Project type', null);
|
$this->addArgument('--type', 'Project type', null);
|
||||||
$this->addArgument('--description', 'Repository description', '');
|
$this->addArgument('--description', 'Repository description', '');
|
||||||
@@ -60,16 +60,16 @@ class CreateRepoCli extends CliFramework
|
|||||||
'generic' => 'generic',
|
'generic' => 'generic',
|
||||||
];
|
];
|
||||||
$TYPE_TO_TOPICS = [
|
$TYPE_TO_TOPICS = [
|
||||||
'dolibarr' => ['dolibarr', 'erp', 'crm', 'php', 'moko-platform'],
|
'dolibarr' => ['dolibarr', 'erp', 'crm', 'php', 'mokocli'],
|
||||||
'joomla' => ['joomla', 'cms', 'php', 'moko-platform'],
|
'joomla' => ['joomla', 'cms', 'php', 'mokocli'],
|
||||||
'nodejs' => ['nodejs', 'javascript', 'typescript', 'moko-platform'],
|
'nodejs' => ['nodejs', 'javascript', 'typescript', 'mokocli'],
|
||||||
'terraform' => ['terraform', 'infrastructure', 'iac', 'moko-platform'],
|
'terraform' => ['terraform', 'infrastructure', 'iac', 'mokocli'],
|
||||||
'python' => ['python', 'moko-platform'],
|
'python' => ['python', 'mokocli'],
|
||||||
'wordpress' => ['wordpress', 'php', 'cms', 'moko-platform'],
|
'wordpress' => ['wordpress', 'php', 'cms', 'mokocli'],
|
||||||
'generic' => ['moko-platform'],
|
'generic' => ['mokocli'],
|
||||||
];
|
];
|
||||||
$platform = $TYPE_TO_PLATFORM[$type] ?? 'generic';
|
$platform = $TYPE_TO_PLATFORM[$type] ?? 'generic';
|
||||||
$topics = $TYPE_TO_TOPICS[$type] ?? ['moko-platform'];
|
$topics = $TYPE_TO_TOPICS[$type] ?? ['mokocli'];
|
||||||
$platformName = $adapter->getPlatformName();
|
$platformName = $adapter->getPlatformName();
|
||||||
$vis = $private ? 'private' : 'public';
|
$vis = $private ? 'private' : 'public';
|
||||||
echo "Scaffolding new repository: {$org}/{$name}"
|
echo "Scaffolding new repository: {$org}/{$name}"
|
||||||
@@ -84,7 +84,7 @@ class CreateRepoCli extends CliFramework
|
|||||||
if (!$this->dryRun) {
|
if (!$this->dryRun) {
|
||||||
try {
|
try {
|
||||||
$data = $adapter->createOrgRepo($org, $name, [
|
$data = $adapter->createOrgRepo($org, $name, [
|
||||||
'description' => $description ?: "Managed by moko-platform ({$type})",
|
'description' => $description ?: "Managed by mokocli ({$type})",
|
||||||
'private' => $private,
|
'private' => $private,
|
||||||
'has_issues' => true,
|
'has_issues' => true,
|
||||||
'has_projects' => true,
|
'has_projects' => true,
|
||||||
@@ -138,12 +138,12 @@ class CreateRepoCli extends CliFramework
|
|||||||
echo "Step 4: Creating README.md...\n";
|
echo "Step 4: Creating README.md...\n";
|
||||||
$baseUrl = $platformName === 'gitea' ? $config->getString('gitea.url', 'https://git.mokoconsulting.tech') : 'https://github.com';
|
$baseUrl = $platformName === 'gitea' ? $config->getString('gitea.url', 'https://git.mokoconsulting.tech') : 'https://github.com';
|
||||||
$repoUrl = "{$baseUrl}/{$org}/{$name}";
|
$repoUrl = "{$baseUrl}/{$org}/{$name}";
|
||||||
$standardsUrl = "{$baseUrl}/{$org}/MokoStandards";
|
$standardsUrl = "{$baseUrl}/{$org}/MokoCli";
|
||||||
$readmeContent = "<!--\n"
|
$readmeContent = "<!--\n"
|
||||||
. "Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>\n"
|
. "Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>\n"
|
||||||
. "SPDX-License-Identifier: GPL-3.0-or-later\n"
|
. "SPDX-License-Identifier: GPL-3.0-or-later\n"
|
||||||
. "DEFGROUP: {$name}\n"
|
. "DEFGROUP: {$name}\n"
|
||||||
. "INGROUP: moko-platform\n"
|
. "INGROUP: mokocli\n"
|
||||||
. "REPO: {$repoUrl}\n"
|
. "REPO: {$repoUrl}\n"
|
||||||
. "PATH: /README.md\n"
|
. "PATH: /README.md\n"
|
||||||
. "BRIEF: {$description}\n"
|
. "BRIEF: {$description}\n"
|
||||||
@@ -152,7 +152,7 @@ class CreateRepoCli extends CliFramework
|
|||||||
. "{$description}\n\n"
|
. "{$description}\n\n"
|
||||||
. "## Getting Started\n\n"
|
. "## Getting Started\n\n"
|
||||||
. "This repository is governed by"
|
. "This repository is governed by"
|
||||||
. " [moko-platform]({$standardsUrl}).\n\n"
|
. " [mokocli]({$standardsUrl}).\n\n"
|
||||||
. "## License\n\n"
|
. "## License\n\n"
|
||||||
. "GPL-3.0-or-later. See [LICENSE](LICENSE)"
|
. "GPL-3.0-or-later. See [LICENSE](LICENSE)"
|
||||||
. " for details.\n";
|
. " for details.\n";
|
||||||
@@ -169,7 +169,7 @@ class CreateRepoCli extends CliFramework
|
|||||||
$name,
|
$name,
|
||||||
'README.md',
|
'README.md',
|
||||||
$readmeContent,
|
$readmeContent,
|
||||||
'docs: initialize README with moko-platform header [skip ci]',
|
'docs: initialize README with mokocli header [skip ci]',
|
||||||
$sha
|
$sha
|
||||||
);
|
);
|
||||||
echo " README.md created\n";
|
echo " README.md created\n";
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: MokoPlatform.CLI
|
* DEFGROUP: MokoPlatform.CLI
|
||||||
* INGROUP: MokoPlatform
|
* INGROUP: MokoPlatform
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /cli/deploy_joomla.php
|
* PATH: /cli/deploy_joomla.php
|
||||||
* BRIEF: Smart Joomla deploy — routes files to correct server directories by extension type
|
* BRIEF: Smart Joomla deploy — routes files to correct server directories by extension type
|
||||||
*
|
*
|
||||||
@@ -31,7 +31,7 @@ require_once __DIR__ . '/../vendor/autoload.php';
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoCli\{CliFramework, SourceResolver};
|
||||||
use phpseclib3\Net\SFTP;
|
use phpseclib3\Net\SFTP;
|
||||||
use phpseclib3\Crypt\PublicKeyLoader;
|
use phpseclib3\Crypt\PublicKeyLoader;
|
||||||
|
|
||||||
@@ -866,11 +866,11 @@ class DeployJoomla extends CliFramework
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3-5. Fallback chain
|
// 3-5. Fallback chain (source/ → src/ → htdocs/)
|
||||||
foreach (['src', 'htdocs'] as $candidate) {
|
$resolved = SourceResolver::resolveAbsolute($repoPath);
|
||||||
if (is_dir("{$repoPath}/{$candidate}")) {
|
if ($resolved !== null) {
|
||||||
return "{$repoPath}/{$candidate}";
|
SourceResolver::warnIfLegacy($repoPath);
|
||||||
}
|
return $resolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Last resort: repo root itself
|
// Last resort: repo root itself
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: moko-platform.CLI
|
* DEFGROUP: mokocli.CLI
|
||||||
* INGROUP: moko-platform
|
* INGROUP: mokocli
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /cli/dev_branch_reset.php
|
* PATH: /cli/dev_branch_reset.php
|
||||||
* BRIEF: Delete and recreate dev branch from main via Gitea API
|
* BRIEF: Delete and recreate dev branch from main via Gitea API
|
||||||
*/
|
*/
|
||||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoCli\CliFramework;
|
||||||
|
|
||||||
class DevBranchResetCli extends CliFramework
|
class DevBranchResetCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,11 +8,11 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: moko-platform.CLI
|
* DEFGROUP: mokocli.CLI
|
||||||
* INGROUP: moko-platform
|
* INGROUP: mokocli
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /cli/grafana_dashboard.php
|
* PATH: /cli/grafana_dashboard.php
|
||||||
* VERSION: 09.23.00
|
* VERSION: 09.38.01
|
||||||
* BRIEF: Manage Grafana dashboards via API
|
* BRIEF: Manage Grafana dashboards via API
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoCli\CliFramework;
|
||||||
|
|
||||||
class GrafanaDashboardCli extends CliFramework
|
class GrafanaDashboardCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
+8
-13
@@ -6,11 +6,11 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: moko-platform.CLI
|
* DEFGROUP: mokocli.CLI
|
||||||
* INGROUP: moko-platform
|
* INGROUP: mokocli
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /cli/joomla_build.php
|
* PATH: /cli/joomla_build.php
|
||||||
* VERSION: 09.23.00
|
* VERSION: 09.38.01
|
||||||
* BRIEF: Build a Joomla extension ZIP from manifest — all types supported
|
* BRIEF: Build a Joomla extension ZIP from manifest — all types supported
|
||||||
* NOTE: Called by pre-release and auto-release workflows.
|
* NOTE: Called by pre-release and auto-release workflows.
|
||||||
*/
|
*/
|
||||||
@@ -19,7 +19,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoCli\{CliFramework, SourceResolver};
|
||||||
|
|
||||||
class JoomlaBuildCli extends CliFramework
|
class JoomlaBuildCli extends CliFramework
|
||||||
{
|
{
|
||||||
@@ -49,17 +49,12 @@ class JoomlaBuildCli extends CliFramework
|
|||||||
$path = realpath($path) ?: $path;
|
$path = realpath($path) ?: $path;
|
||||||
|
|
||||||
// ── Find source directory ──────────────────────────────────────────────
|
// ── Find source directory ──────────────────────────────────────────────
|
||||||
$srcDir = null;
|
$srcDir = SourceResolver::resolveAbsolute($path);
|
||||||
foreach (['src', 'htdocs'] as $d) {
|
|
||||||
if (is_dir("{$path}/{$d}")) {
|
|
||||||
$srcDir = "{$path}/{$d}";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($srcDir === null) {
|
if ($srcDir === null) {
|
||||||
$this->log('ERROR', "::error::No src/ or htdocs/ directory in {$path}");
|
$this->log('ERROR', "::error::No source/ or src/ directory in {$path}");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
SourceResolver::warnIfLegacy($path);
|
||||||
|
|
||||||
// ── Find manifest ──────────────────────────────────────────────────────
|
// ── Find manifest ──────────────────────────────────────────────────────
|
||||||
$manifest = $this->findManifest($srcDir);
|
$manifest = $this->findManifest($srcDir);
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: moko-platform.CLI
|
* DEFGROUP: mokocli.CLI
|
||||||
* INGROUP: moko-platform
|
* INGROUP: mokocli
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /cli/joomla_compat_check.php
|
* PATH: /cli/joomla_compat_check.php
|
||||||
* BRIEF: Check if extension targetplatform regex matches the latest Joomla version
|
* BRIEF: Check if extension targetplatform regex matches the latest Joomla version
|
||||||
*/
|
*/
|
||||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoCli\CliFramework;
|
||||||
|
|
||||||
class JoomlaCompatCheckCli extends CliFramework
|
class JoomlaCompatCheckCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,507 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*
|
||||||
|
* FILE INFORMATION
|
||||||
|
* DEFGROUP: mokocli.CLI
|
||||||
|
* INGROUP: mokocli
|
||||||
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
|
* PATH: /cli/joomla_metadata_validate.php
|
||||||
|
* VERSION: 09.38.01
|
||||||
|
* BRIEF: Validate MokoGitea repo metadata against Joomla extension manifest XML
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
|
use MokoCli\CliFramework;
|
||||||
|
|
||||||
|
class JoomlaMetadataValidateCli extends CliFramework
|
||||||
|
{
|
||||||
|
/** Joomla element prefix map — must match MokoGitea's cleanJoomlaElement() */
|
||||||
|
private const JOOMLA_PREFIX = [
|
||||||
|
'package' => 'pkg_',
|
||||||
|
'component' => 'com_',
|
||||||
|
'module' => 'mod_',
|
||||||
|
'template' => 'tpl_',
|
||||||
|
'library' => 'lib_',
|
||||||
|
'file' => 'file_',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this->setDescription('Validate MokoGitea repo metadata against Joomla extension manifest XML');
|
||||||
|
$this->addArgument('--path', 'Repo root path (default: current directory)', '.');
|
||||||
|
$this->addArgument('--token', 'Gitea API token (or GITEA_TOKEN env)', '');
|
||||||
|
$this->addArgument('--org', 'Gitea org', 'MokoConsulting');
|
||||||
|
$this->addArgument('--repo', 'Repo name (auto-detected from git if empty)', '');
|
||||||
|
$this->addArgument('--api-base', 'Gitea API base URL', 'https://git.mokoconsulting.tech/api/v1');
|
||||||
|
$this->addArgument('--ci', 'CI mode: exit 1 on any error', false);
|
||||||
|
$this->addArgument('--json', 'Output as JSON', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function run(): int
|
||||||
|
{
|
||||||
|
$path = realpath($this->getArgument('--path')) ?: $this->getArgument('--path');
|
||||||
|
$token = $this->getArgument('--token') ?: getenv('GITEA_TOKEN') ?: '';
|
||||||
|
$org = $this->getArgument('--org');
|
||||||
|
$repoName = $this->getArgument('--repo');
|
||||||
|
$apiBase = rtrim($this->getArgument('--api-base'), '/');
|
||||||
|
$ciMode = (bool) $this->getArgument('--ci');
|
||||||
|
$jsonMode = (bool) $this->getArgument('--json');
|
||||||
|
|
||||||
|
if (!is_dir($path)) {
|
||||||
|
$this->log('ERROR', "Path does not exist: {$path}");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($repoName === '') {
|
||||||
|
$repoName = $this->detectRepoName($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Step 1: Find the Joomla extension manifest XML ──────────
|
||||||
|
$joomlaXml = $this->findJoomlaManifest($path);
|
||||||
|
|
||||||
|
if ($joomlaXml === null) {
|
||||||
|
$this->log('ERROR', 'No Joomla extension manifest XML found');
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log('INFO', "Joomla manifest: {$joomlaXml['path']}");
|
||||||
|
|
||||||
|
// ── Step 2: Load MokoGitea metadata ─────────────────────────
|
||||||
|
$metadata = $this->loadMetadata($path, $org, $repoName, $token, $apiBase);
|
||||||
|
|
||||||
|
if ($metadata === null) {
|
||||||
|
$this->log('ERROR', 'Could not load MokoGitea metadata');
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Step 3: Compare ─────────────────────────────────────────
|
||||||
|
$results = $this->compare($metadata, $joomlaXml, $path);
|
||||||
|
|
||||||
|
// ── Step 4: Output ──────────────────────────────────────────
|
||||||
|
if ($jsonMode) {
|
||||||
|
echo json_encode([
|
||||||
|
'repo' => $repoName,
|
||||||
|
'results' => $results,
|
||||||
|
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";
|
||||||
|
} else {
|
||||||
|
$this->printResults($repoName, $results);
|
||||||
|
}
|
||||||
|
|
||||||
|
$errors = count(array_filter($results, fn($r) => $r['status'] === 'error'));
|
||||||
|
|
||||||
|
return ($ciMode && $errors > 0) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// Find Joomla manifest XML
|
||||||
|
// =================================================================
|
||||||
|
|
||||||
|
private function findJoomlaManifest(string $root): ?array
|
||||||
|
{
|
||||||
|
// Search common locations for a Joomla extension manifest
|
||||||
|
$candidates = [];
|
||||||
|
|
||||||
|
// Package manifest: source/pkg_*.xml
|
||||||
|
foreach (glob("{$root}/source/pkg_*.xml") as $file) {
|
||||||
|
$candidates[] = $file;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Component manifest: source/packages/com_*/[name].xml
|
||||||
|
foreach (glob("{$root}/source/packages/com_*/*.xml") as $file) {
|
||||||
|
$basename = basename($file);
|
||||||
|
// Skip access.xml, config.xml, etc.
|
||||||
|
if (in_array($basename, ['access.xml', 'config.xml'], true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$candidates[] = $file;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Direct source/*.xml
|
||||||
|
foreach (glob("{$root}/source/*.xml") as $file) {
|
||||||
|
if (basename($file) !== 'pkg_mokosuitebackup.xml') {
|
||||||
|
// Already caught above
|
||||||
|
}
|
||||||
|
$candidates[] = $file;
|
||||||
|
}
|
||||||
|
|
||||||
|
// src/ fallback
|
||||||
|
foreach (glob("{$root}/src/pkg_*.xml") as $file) {
|
||||||
|
$candidates[] = $file;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the first one that has <extension type="...">
|
||||||
|
foreach (array_unique($candidates) as $file) {
|
||||||
|
$content = file_get_contents($file);
|
||||||
|
if ($content === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match('/<extension\s[^>]*type=["\']([^"\']+)["\']/', $content, $typeMatch)) {
|
||||||
|
$xml = @simplexml_load_string($content);
|
||||||
|
if ($xml === false) {
|
||||||
|
$relPath = str_replace($root . '/', '', $file);
|
||||||
|
$relPath = str_replace($root . '\\', '', $relPath);
|
||||||
|
$this->log('WARN', "Skipping {$relPath}: malformed XML");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$type = strtolower($typeMatch[1]);
|
||||||
|
$relPath = str_replace($root . '/', '', $file);
|
||||||
|
$relPath = str_replace($root . '\\', '', $relPath);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'path' => $relPath,
|
||||||
|
'type' => $type,
|
||||||
|
'xml' => $xml,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// Load metadata (from API)
|
||||||
|
// =================================================================
|
||||||
|
|
||||||
|
private function loadMetadata(string $root, string $org, string $repoName, string $token, string $apiBase): ?array
|
||||||
|
{
|
||||||
|
if ($token === '') {
|
||||||
|
$this->log('ERROR', 'No API token provided (use --token or set GITEA_TOKEN env var)');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = "{$apiBase}/repos/{$org}/{$repoName}/metadata";
|
||||||
|
$ctx = stream_context_create([
|
||||||
|
'http' => [
|
||||||
|
'header' => "Authorization: token {$token}\r\nAccept: application/json\r\n",
|
||||||
|
'timeout' => 10,
|
||||||
|
'ignore_errors' => true,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$body = file_get_contents($url, false, $ctx);
|
||||||
|
|
||||||
|
// Extract HTTP status from response headers
|
||||||
|
$httpCode = 0;
|
||||||
|
if (isset($http_response_header[0]) && preg_match('/\d{3}/', $http_response_header[0], $m)) {
|
||||||
|
$httpCode = (int) $m[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($body === false) {
|
||||||
|
$this->log('ERROR', "Failed to connect to {$url} — check network or TLS configuration");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($httpCode === 404) {
|
||||||
|
$this->log('ERROR', "API endpoint not found: {$url}");
|
||||||
|
$this->log('ERROR', 'Server may need MokoGitea-Fork >= #650 (metadata endpoint rename)');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($httpCode === 401 || $httpCode === 403) {
|
||||||
|
$this->log('ERROR', "Authentication failed (HTTP {$httpCode}) — check your API token");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($httpCode >= 400) {
|
||||||
|
$this->log('ERROR', "API returned HTTP {$httpCode}: " . substr($body, 0, 200));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode($body, true);
|
||||||
|
if (!is_array($data)) {
|
||||||
|
$this->log('ERROR', "API returned invalid JSON from {$url}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data['source'] = 'api';
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// Compare metadata against Joomla manifest
|
||||||
|
// =================================================================
|
||||||
|
|
||||||
|
private function compare(array $metadata, array $joomlaXml, string $root): array
|
||||||
|
{
|
||||||
|
$results = [];
|
||||||
|
$xml = $joomlaXml['xml'];
|
||||||
|
$type = $joomlaXml['type'];
|
||||||
|
|
||||||
|
// 1. Extension type
|
||||||
|
$metaType = $this->normalizeExtensionType(
|
||||||
|
$metadata['extension_type'] ?? $metadata['package_type'] ?? ''
|
||||||
|
);
|
||||||
|
$results[] = [
|
||||||
|
'field' => 'extension_type',
|
||||||
|
'metadata' => $metaType,
|
||||||
|
'joomla' => $type,
|
||||||
|
'status' => ($metaType === $type) ? 'ok' : 'error',
|
||||||
|
'message' => ($metaType === $type)
|
||||||
|
? "matches <extension type=\"{$type}\">"
|
||||||
|
: "metadata has \"{$metaType}\" but Joomla manifest has \"{$type}\"",
|
||||||
|
];
|
||||||
|
|
||||||
|
// 2. Element name
|
||||||
|
$metaName = strtolower($metadata['name'] ?? '');
|
||||||
|
$metaElement = $this->deriveElement($metaType, $metaName);
|
||||||
|
$joomlaElement = $this->extractJoomlaElement($xml, $type);
|
||||||
|
|
||||||
|
$elementMatch = ($metaElement === $joomlaElement);
|
||||||
|
$results[] = [
|
||||||
|
'field' => 'element',
|
||||||
|
'metadata' => $metaElement,
|
||||||
|
'joomla' => $joomlaElement,
|
||||||
|
'status' => $elementMatch ? 'ok' : 'error',
|
||||||
|
'message' => $elementMatch
|
||||||
|
? "derived correctly"
|
||||||
|
: "metadata derives \"{$metaElement}\" but Joomla uses \"{$joomlaElement}\"",
|
||||||
|
];
|
||||||
|
|
||||||
|
// 3. Version
|
||||||
|
$metaVersion = $metadata['version'] ?? '';
|
||||||
|
$joomlaVersion = (string) ($xml->version ?? '');
|
||||||
|
|
||||||
|
if ($metaVersion !== '' && $joomlaVersion !== '') {
|
||||||
|
// Strip dev/rc suffixes for comparison (CI bumps these)
|
||||||
|
$metaBase = preg_replace('/-(dev|rc|alpha|beta)\d*$/', '', $metaVersion);
|
||||||
|
$joomlaBase = preg_replace('/-(dev|rc|alpha|beta)\d*$/', '', $joomlaVersion);
|
||||||
|
$versionMatch = ($metaBase === $joomlaBase);
|
||||||
|
|
||||||
|
$results[] = [
|
||||||
|
'field' => 'version',
|
||||||
|
'metadata' => $metaVersion,
|
||||||
|
'joomla' => $joomlaVersion,
|
||||||
|
'status' => $versionMatch ? 'ok' : 'warn',
|
||||||
|
'message' => $versionMatch
|
||||||
|
? 'matches (base version)'
|
||||||
|
: "metadata has \"{$metaVersion}\" but Joomla has \"{$joomlaVersion}\"",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. PHP minimum (from composer.json)
|
||||||
|
$composerPhp = $this->readComposerPhpRequirement($root);
|
||||||
|
$metaPhp = $metadata['php_minimum'] ?? '';
|
||||||
|
|
||||||
|
if ($composerPhp !== '' && $metaPhp !== '') {
|
||||||
|
$phpMatch = ($metaPhp === $composerPhp);
|
||||||
|
$results[] = [
|
||||||
|
'field' => 'php_minimum',
|
||||||
|
'metadata' => $metaPhp,
|
||||||
|
'joomla' => $composerPhp . ' (composer.json)',
|
||||||
|
'status' => $phpMatch ? 'ok' : 'warn',
|
||||||
|
'message' => $phpMatch
|
||||||
|
? 'matches composer.json'
|
||||||
|
: "metadata has \"{$metaPhp}\" but composer.json requires \"{$composerPhp}\"",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Description
|
||||||
|
$metaDesc = $metadata['description'] ?? '';
|
||||||
|
$joomlaDesc = (string) ($xml->description ?? '');
|
||||||
|
|
||||||
|
// Joomla descriptions are often language keys, skip those
|
||||||
|
if ($metaDesc !== '' && $joomlaDesc !== '' && !str_starts_with($joomlaDesc, 'COM_') && !str_starts_with($joomlaDesc, 'PKG_')) {
|
||||||
|
$descMatch = ($metaDesc === $joomlaDesc);
|
||||||
|
$results[] = [
|
||||||
|
'field' => 'description',
|
||||||
|
'metadata' => substr($metaDesc, 0, 60) . (strlen($metaDesc) > 60 ? '...' : ''),
|
||||||
|
'joomla' => substr($joomlaDesc, 0, 60) . (strlen($joomlaDesc) > 60 ? '...' : ''),
|
||||||
|
'status' => $descMatch ? 'ok' : 'info',
|
||||||
|
'message' => $descMatch ? 'matches' : 'descriptions differ (informational)',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// Helpers
|
||||||
|
// =================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize extension_type — map MokoGitea types to Joomla types.
|
||||||
|
*/
|
||||||
|
private function normalizeExtensionType(string $type): string
|
||||||
|
{
|
||||||
|
return match (strtolower($type)) {
|
||||||
|
'joomla-extension' => 'package', // legacy mapping
|
||||||
|
default => strtolower($type),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derive the Joomla element name from type + name.
|
||||||
|
* Replicates MokoGitea's cleanJoomlaElement() + prefix logic.
|
||||||
|
*/
|
||||||
|
private function deriveElement(string $type, string $name): string
|
||||||
|
{
|
||||||
|
// Clean: lowercase, strip non-alphanumeric except . _ -
|
||||||
|
$clean = strtolower($name);
|
||||||
|
$clean = preg_replace('/[^a-z0-9._-]/', '', $clean);
|
||||||
|
|
||||||
|
$prefix = self::JOOMLA_PREFIX[$type] ?? '';
|
||||||
|
|
||||||
|
return $prefix . $clean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the element name from a Joomla manifest XML.
|
||||||
|
* Follows the same logic as Joomla's InstallerAdapter::getElement().
|
||||||
|
*/
|
||||||
|
private function extractJoomlaElement(\SimpleXMLElement $xml, string $type): string
|
||||||
|
{
|
||||||
|
switch ($type) {
|
||||||
|
case 'package':
|
||||||
|
$packagename = (string) ($xml->packagename ?? '');
|
||||||
|
if ($packagename !== '') {
|
||||||
|
return 'pkg_' . strtolower(preg_replace('/[^a-zA-Z0-9._-]/', '', $packagename));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'component':
|
||||||
|
$element = (string) ($xml->element ?? '');
|
||||||
|
if ($element !== '') {
|
||||||
|
$element = strtolower($element);
|
||||||
|
return str_starts_with($element, 'com_') ? $element : 'com_' . $element;
|
||||||
|
}
|
||||||
|
$name = (string) ($xml->name ?? '');
|
||||||
|
$name = strtolower(preg_replace('/[^a-zA-Z0-9._-]/', '', $name));
|
||||||
|
return str_starts_with($name, 'com_') ? $name : 'com_' . $name;
|
||||||
|
|
||||||
|
case 'module':
|
||||||
|
$element = (string) ($xml->element ?? '');
|
||||||
|
if ($element !== '') {
|
||||||
|
return strtolower($element);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'plugin':
|
||||||
|
// Plugins derive element from the file attribute
|
||||||
|
if (isset($xml->files)) {
|
||||||
|
foreach ($xml->files->children() as $file) {
|
||||||
|
$plugin = (string) ($file->attributes()->plugin ?? '');
|
||||||
|
if ($plugin !== '') {
|
||||||
|
return strtolower($plugin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'library':
|
||||||
|
$libname = (string) ($xml->libraryname ?? '');
|
||||||
|
if ($libname !== '') {
|
||||||
|
return strtolower($libname);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: use <name> tag
|
||||||
|
$name = (string) ($xml->name ?? '');
|
||||||
|
return strtolower(preg_replace('/[^a-zA-Z0-9._-]/', '', $name));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read PHP version requirement from composer.json.
|
||||||
|
*/
|
||||||
|
private function readComposerPhpRequirement(string $root): string
|
||||||
|
{
|
||||||
|
$composerFile = "{$root}/composer.json";
|
||||||
|
|
||||||
|
if (!is_file($composerFile)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode(file_get_contents($composerFile), true);
|
||||||
|
|
||||||
|
if (!is_array($data)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$phpReq = $data['require']['php'] ?? '';
|
||||||
|
|
||||||
|
// Extract version number from constraint like ">=8.1"
|
||||||
|
if (preg_match('/(\d+\.\d+)/', $phpReq, $m)) {
|
||||||
|
return $m[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function detectRepoName(string $root): string
|
||||||
|
{
|
||||||
|
$gitConfig = "{$root}/.git/config";
|
||||||
|
|
||||||
|
if (!file_exists($gitConfig)) {
|
||||||
|
return basename($root);
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = file_get_contents($gitConfig);
|
||||||
|
|
||||||
|
if (preg_match('/url\s*=\s*.*\/([^\/\s]+?)(?:\.git)?\s*$/m', $content, $m)) {
|
||||||
|
return $m[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return basename($root);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// Output
|
||||||
|
// =================================================================
|
||||||
|
|
||||||
|
private function printResults(string $repoName, array $results): void
|
||||||
|
{
|
||||||
|
$errors = count(array_filter($results, fn($r) => $r['status'] === 'error'));
|
||||||
|
$warns = count(array_filter($results, fn($r) => $r['status'] === 'warn'));
|
||||||
|
$oks = count(array_filter($results, fn($r) => $r['status'] === 'ok'));
|
||||||
|
|
||||||
|
$this->log('INFO', "Validating {$repoName} Joomla metadata...\n");
|
||||||
|
|
||||||
|
foreach ($results as $r) {
|
||||||
|
$icon = match ($r['status']) {
|
||||||
|
'ok' => "\xE2\x9C\x93", // ✓
|
||||||
|
'error' => "\xE2\x9C\x97", // ✗
|
||||||
|
'warn' => "\xE2\x9A\xA0", // ⚠
|
||||||
|
default => "\xE2\x84\xB9", // ℹ
|
||||||
|
};
|
||||||
|
|
||||||
|
$line = sprintf(
|
||||||
|
" %s %-16s %s",
|
||||||
|
$icon,
|
||||||
|
$r['field'],
|
||||||
|
$r['message']
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->log(
|
||||||
|
match ($r['status']) {
|
||||||
|
'error' => 'ERROR',
|
||||||
|
'warn' => 'WARN',
|
||||||
|
'ok' => 'OK',
|
||||||
|
default => 'INFO',
|
||||||
|
},
|
||||||
|
$line
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n";
|
||||||
|
|
||||||
|
if ($errors > 0) {
|
||||||
|
$this->log('ERROR', "{$errors} error(s) — update delivery will fail");
|
||||||
|
} elseif ($warns > 0) {
|
||||||
|
$this->log('WARN', "All critical checks passed, {$warns} warning(s)");
|
||||||
|
} else {
|
||||||
|
$this->log('OK', "All {$oks} checks passed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$app = new JoomlaMetadataValidateCli();
|
||||||
|
exit($app->execute());
|
||||||
@@ -8,9 +8,9 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: moko-platform.CLI
|
* DEFGROUP: mokocli.CLI
|
||||||
* INGROUP: moko-platform
|
* INGROUP: mokocli
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /cli/joomla_release.php
|
* PATH: /cli/joomla_release.php
|
||||||
* BRIEF: Joomla release pipeline — build ZIP+tar.gz, upload to GitHub Release, update updates.xml
|
* BRIEF: Joomla release pipeline — build ZIP+tar.gz, upload to GitHub Release, update updates.xml
|
||||||
*
|
*
|
||||||
@@ -25,7 +25,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
use MokoEnterprise\{ApiClient, AuditLogger, CliFramework, Config, PlatformAdapterFactory};
|
use MokoCli\{ApiClient, AuditLogger, CliFramework, Config, PlatformAdapterFactory, SourceResolver};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Joomla Release Manager
|
* Joomla Release Manager
|
||||||
@@ -57,7 +57,7 @@ class JoomlaRelease extends CliFramework
|
|||||||
];
|
];
|
||||||
|
|
||||||
private ApiClient $api;
|
private ApiClient $api;
|
||||||
private \MokoEnterprise\GitPlatformAdapter $adapter;
|
private \MokoCli\GitPlatformAdapter $adapter;
|
||||||
|
|
||||||
protected function configure(): void
|
protected function configure(): void
|
||||||
{
|
{
|
||||||
@@ -121,11 +121,12 @@ class JoomlaRelease extends CliFramework
|
|||||||
$this->log('INFO', "Version: {$displayVersion} | Release tag: {$releaseTag}");
|
$this->log('INFO', "Version: {$displayVersion} | Release tag: {$releaseTag}");
|
||||||
|
|
||||||
// ── Step 3: Build packages ────────────────────────────────────
|
// ── Step 3: Build packages ────────────────────────────────────
|
||||||
$srcDir = is_dir("{$path}/src") ? "{$path}/src" : (is_dir("{$path}/htdocs") ? "{$path}/htdocs" : null);
|
$srcDir = SourceResolver::resolveAbsolute($path);
|
||||||
if ($srcDir === null) {
|
if ($srcDir === null) {
|
||||||
$this->log('ERROR', 'No src/ or htdocs/ directory');
|
$this->log('ERROR', 'No source/ or src/ directory');
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
SourceResolver::warnIfLegacy($path);
|
||||||
|
|
||||||
$prefix = $this->typePrefix($meta);
|
$prefix = $this->typePrefix($meta);
|
||||||
$zipName = "{$prefix}{$meta['element']}-{$displayVersion}.zip";
|
$zipName = "{$prefix}{$meta['element']}-{$displayVersion}.zip";
|
||||||
@@ -406,7 +407,7 @@ class JoomlaRelease extends CliFramework
|
|||||||
$this->api->post("/repos/{$repo}/releases", [
|
$this->api->post("/repos/{$repo}/releases", [
|
||||||
'tag_name' => $tag,
|
'tag_name' => $tag,
|
||||||
'name' => $releaseName,
|
'name' => $releaseName,
|
||||||
'body' => "## {$version}\n\nCreated by moko-platform release pipeline.",
|
'body' => "## {$version}\n\nCreated by mokocli release pipeline.",
|
||||||
'prerelease' => ($stability !== 'stable'),
|
'prerelease' => ($stability !== 'stable'),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: moko-platform.CLI
|
* DEFGROUP: mokocli.CLI
|
||||||
* INGROUP: moko-platform
|
* INGROUP: mokocli
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /cli/license_manage.php
|
* PATH: /cli/license_manage.php
|
||||||
* BRIEF: Manage license packages and keys via MokoGitea licensing API
|
* BRIEF: Manage license packages and keys via MokoGitea licensing API
|
||||||
*
|
*
|
||||||
@@ -28,7 +28,7 @@ declare(strict_types=1);
|
|||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoCli\CliFramework;
|
||||||
|
|
||||||
class LicenseManage extends CliFramework
|
class LicenseManage extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,749 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*
|
||||||
|
* FILE INFORMATION
|
||||||
|
* DEFGROUP: mokocli.CLI
|
||||||
|
* INGROUP: mokocli
|
||||||
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
|
* PATH: /cli/manifest_detect.php
|
||||||
|
* VERSION: 09.38.01
|
||||||
|
* BRIEF: Auto-detect manifest fields from source files and optionally push to API
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
|
use MokoCli\{CliFramework, SourceResolver};
|
||||||
|
|
||||||
|
class ManifestDetectCli extends CliFramework
|
||||||
|
{
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this->setDescription('Auto-detect manifest fields from source files');
|
||||||
|
$this->addArgument('--path', 'Repository root path', '.');
|
||||||
|
$this->addArgument('--json', 'Output as JSON', false);
|
||||||
|
$this->addArgument('--diff', 'Show diff against current manifest API values', false);
|
||||||
|
$this->addArgument('--update', 'Push detected fields to manifest API', false);
|
||||||
|
$this->addArgument('--token', 'Gitea API token (or GITEA_TOKEN env)', '');
|
||||||
|
$this->addArgument('--api-base', 'Gitea API base URL', 'https://git.mokoconsulting.tech/api/v1');
|
||||||
|
$this->addArgument('--org', 'Gitea org', 'MokoConsulting');
|
||||||
|
$this->addArgument('--repo', 'Gitea repo name (auto-detected from remote if empty)', '');
|
||||||
|
$this->addArgument('--github-output', 'Append fields to $GITHUB_OUTPUT', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function run(): int
|
||||||
|
{
|
||||||
|
$path = $this->getArgument('--path');
|
||||||
|
$jsonMode = (bool) $this->getArgument('--json');
|
||||||
|
$diffMode = (bool) $this->getArgument('--diff');
|
||||||
|
$updateMode = (bool) $this->getArgument('--update');
|
||||||
|
$ghOutput = (bool) $this->getArgument('--github-output');
|
||||||
|
$token = $this->getArgument('--token') ?: getenv('GITEA_TOKEN') ?: '';
|
||||||
|
$apiBase = rtrim($this->getArgument('--api-base'), '/');
|
||||||
|
$org = $this->getArgument('--org');
|
||||||
|
$repoName = $this->getArgument('--repo');
|
||||||
|
|
||||||
|
$root = realpath($path) ?: $path;
|
||||||
|
|
||||||
|
if (!is_dir($root)) {
|
||||||
|
$this->log('ERROR', "Path does not exist: {$path}");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-detect repo name from git remote
|
||||||
|
if ($repoName === '') {
|
||||||
|
$repoName = $this->detectRepoName($root);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Detect all fields ───────────────────────────────────────
|
||||||
|
$detected = $this->detectAll($root, $repoName);
|
||||||
|
|
||||||
|
// ── Warn about missing fields ────────────────────────────────
|
||||||
|
$expected = ['platform', 'name', 'version', 'package_type', 'language', 'entry_point'];
|
||||||
|
foreach ($expected as $field) {
|
||||||
|
if (!isset($detected[$field]) || $detected[$field] === '') {
|
||||||
|
$this->log('WARN', "Could not detect: {$field}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Output ──────────────────────────────────────────────────
|
||||||
|
if ($diffMode || $updateMode) {
|
||||||
|
if ($token === '') {
|
||||||
|
$this->log('ERROR', 'API token required for --diff/--update (use --token or GITEA_TOKEN env)');
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if ($repoName === '') {
|
||||||
|
$this->log('ERROR', 'Could not determine repo name (use --repo)');
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$current = $this->fetchManifest($apiBase, $org, $repoName, $token);
|
||||||
|
if ($current === null) {
|
||||||
|
$this->log('ERROR', 'Failed to fetch current manifest from API');
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$changes = $this->computeDiff($current, $detected);
|
||||||
|
|
||||||
|
if ($diffMode) {
|
||||||
|
if (empty($changes)) {
|
||||||
|
$this->log('INFO', 'No differences — manifest matches source');
|
||||||
|
} else {
|
||||||
|
$this->sectionHeader('Manifest Drift');
|
||||||
|
foreach ($changes as $field => $info) {
|
||||||
|
$this->log('WARN', sprintf(
|
||||||
|
'%-20s API: %-30s Detected: %s',
|
||||||
|
$field,
|
||||||
|
$info['current'] === '' ? '(empty)' : $info['current'],
|
||||||
|
$info['detected']
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($updateMode) {
|
||||||
|
if (empty($changes)) {
|
||||||
|
$this->log('INFO', 'Nothing to update');
|
||||||
|
} else {
|
||||||
|
$update = array_map(fn($i) => $i['detected'], $changes);
|
||||||
|
$ok = $this->pushManifest($apiBase, $org, $repoName, $token, $current, $update);
|
||||||
|
if ($ok) {
|
||||||
|
$this->log('OK', 'Updated ' . count($update) . ' field(s): ' . implode(', ', array_keys($update)));
|
||||||
|
} else {
|
||||||
|
$this->log('ERROR', 'Failed to push manifest update');
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($ghOutput) {
|
||||||
|
$outputFile = getenv('GITHUB_OUTPUT');
|
||||||
|
$lines = [];
|
||||||
|
foreach ($detected as $k => $v) {
|
||||||
|
$envKey = str_replace('-', '_', $k);
|
||||||
|
$lines[] = "{$envKey}={$v}";
|
||||||
|
}
|
||||||
|
if ($outputFile !== false && $outputFile !== '') {
|
||||||
|
file_put_contents($outputFile, implode("\n", $lines) . "\n", FILE_APPEND);
|
||||||
|
$this->log('INFO', 'Wrote ' . count($detected) . ' fields to GITHUB_OUTPUT');
|
||||||
|
} else {
|
||||||
|
$this->log('WARN', 'GITHUB_OUTPUT not set — printing to stdout instead');
|
||||||
|
echo implode("\n", $lines) . "\n";
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($jsonMode) {
|
||||||
|
echo json_encode($detected, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";
|
||||||
|
} else {
|
||||||
|
foreach ($detected as $k => $v) {
|
||||||
|
echo "{$k}={$v}\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Detection engine
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
private function detectAll(string $root, string $repoName): array
|
||||||
|
{
|
||||||
|
$platform = $this->detectPlatform($root);
|
||||||
|
|
||||||
|
$fields = [
|
||||||
|
'platform' => $platform,
|
||||||
|
'name' => '',
|
||||||
|
'description' => '',
|
||||||
|
'version' => '',
|
||||||
|
'element_name' => '',
|
||||||
|
'package_type' => '',
|
||||||
|
'language' => '',
|
||||||
|
'entry_point' => '',
|
||||||
|
'license_spdx' => '',
|
||||||
|
'display_name' => '',
|
||||||
|
'target_version' => '',
|
||||||
|
'php_minimum' => '',
|
||||||
|
];
|
||||||
|
|
||||||
|
switch ($platform) {
|
||||||
|
case 'joomla':
|
||||||
|
$this->detectJoomla($root, $repoName, $fields);
|
||||||
|
break;
|
||||||
|
case 'dolibarr':
|
||||||
|
$this->detectDolibarr($root, $repoName, $fields);
|
||||||
|
break;
|
||||||
|
case 'go':
|
||||||
|
$this->detectGo($root, $repoName, $fields);
|
||||||
|
break;
|
||||||
|
case 'mcp':
|
||||||
|
$this->detectNode($root, $repoName, $fields);
|
||||||
|
break;
|
||||||
|
case 'node':
|
||||||
|
$this->detectNode($root, $repoName, $fields);
|
||||||
|
$fields['platform'] = 'node';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$this->detectGeneric($root, $repoName, $fields);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallbacks
|
||||||
|
if ($fields['name'] === '') {
|
||||||
|
$fields['name'] = $repoName ?: basename($root);
|
||||||
|
}
|
||||||
|
if ($fields['entry_point'] === '') {
|
||||||
|
$fields['entry_point'] = $this->detectEntryPoint($root);
|
||||||
|
}
|
||||||
|
if ($fields['license_spdx'] === '') {
|
||||||
|
$fields['license_spdx'] = $this->detectLicense($root);
|
||||||
|
}
|
||||||
|
// description: only from platform-specific source, never guessed
|
||||||
|
|
||||||
|
// Strip empty values
|
||||||
|
return array_filter($fields, fn($v) => $v !== '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Platform detection ──────────────────────────────────────────
|
||||||
|
|
||||||
|
private function detectPlatform(string $root): string
|
||||||
|
{
|
||||||
|
// Joomla: look for pkg_*.xml or extension XML in source dirs
|
||||||
|
$joomlaXmls = array_merge(
|
||||||
|
SourceResolver::globSource($root, 'pkg_*.xml'),
|
||||||
|
glob("{$root}/pkg_*.xml") ?: []
|
||||||
|
);
|
||||||
|
if (!empty($joomlaXmls)) {
|
||||||
|
return 'joomla';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check source dirs for any Joomla extension XML
|
||||||
|
foreach (SourceResolver::globSource($root, '*.xml') as $xmlFile) {
|
||||||
|
$content = file_get_contents($xmlFile);
|
||||||
|
if (strpos($content, '<extension') !== false) {
|
||||||
|
return 'joomla';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dolibarr: mod*.class.php with DolibarrModules
|
||||||
|
$modFiles = array_merge(
|
||||||
|
SourceResolver::globSource($root, 'core/modules/mod*.class.php'),
|
||||||
|
glob("{$root}/core/modules/mod*.class.php") ?: []
|
||||||
|
);
|
||||||
|
foreach ($modFiles as $file) {
|
||||||
|
if (strpos(file_get_contents($file), 'DolibarrModules') !== false) {
|
||||||
|
return 'dolibarr';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go
|
||||||
|
if (file_exists("{$root}/go.mod")) {
|
||||||
|
return 'go';
|
||||||
|
}
|
||||||
|
|
||||||
|
// MCP: package.json with mcp-related content
|
||||||
|
if (file_exists("{$root}/package.json")) {
|
||||||
|
$pkg = json_decode(file_get_contents("{$root}/package.json"), true) ?? [];
|
||||||
|
$deps = array_merge(
|
||||||
|
array_keys($pkg['dependencies'] ?? []),
|
||||||
|
array_keys($pkg['devDependencies'] ?? [])
|
||||||
|
);
|
||||||
|
foreach ($deps as $dep) {
|
||||||
|
if (strpos($dep, '@modelcontextprotocol/') === 0 || $dep === '@anthropic/mcp-sdk') {
|
||||||
|
return 'mcp';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 'node';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Python
|
||||||
|
if (file_exists("{$root}/pyproject.toml") || file_exists("{$root}/setup.py")) {
|
||||||
|
return 'python';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'generic';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Joomla ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private function detectJoomla(string $root, string $repoName, array &$fields): void
|
||||||
|
{
|
||||||
|
$fields['language'] = 'PHP';
|
||||||
|
|
||||||
|
// Find the primary extension manifest XML
|
||||||
|
$extManifest = $this->findJoomlaManifest($root);
|
||||||
|
if ($extManifest === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$xml = file_get_contents($extManifest);
|
||||||
|
|
||||||
|
// Type
|
||||||
|
$extType = '';
|
||||||
|
if (preg_match('/type="([^"]*)"/', $xml, $m)) {
|
||||||
|
$extType = $m[1];
|
||||||
|
}
|
||||||
|
$fields['package_type'] = $extType;
|
||||||
|
|
||||||
|
// Element name
|
||||||
|
$element = '';
|
||||||
|
if (preg_match('/<element>([^<]+)<\/element>/', $xml, $m)) {
|
||||||
|
$element = $m[1];
|
||||||
|
}
|
||||||
|
if ($element === '' && preg_match('/module="([^"]*)"/', $xml, $m)) {
|
||||||
|
$element = $m[1];
|
||||||
|
}
|
||||||
|
if ($element === '' && preg_match('/plugin="([^"]*)"/', $xml, $m)) {
|
||||||
|
$element = $m[1];
|
||||||
|
}
|
||||||
|
if ($extType === 'package' && preg_match('/<packagename>([^<]+)<\/packagename>/', $xml, $m)) {
|
||||||
|
$element = $m[1];
|
||||||
|
}
|
||||||
|
if ($element === '') {
|
||||||
|
$element = strtolower(basename($extManifest, '.xml'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure element has type prefix (API stores full element_name like pkg_mokosuite)
|
||||||
|
$prefixMap = [
|
||||||
|
'package' => 'pkg_', 'component' => 'com_', 'module' => 'mod_',
|
||||||
|
'template' => 'tpl_', 'library' => 'lib_', 'file' => 'file_',
|
||||||
|
];
|
||||||
|
if (isset($prefixMap[$extType])) {
|
||||||
|
$prefix = $prefixMap[$extType];
|
||||||
|
// Only add prefix if not already present (check all known prefixes)
|
||||||
|
$hasPrefix = false;
|
||||||
|
foreach ($prefixMap as $p) {
|
||||||
|
if (strpos($element, $p) === 0) { $hasPrefix = true; break; }
|
||||||
|
}
|
||||||
|
if (strpos($element, 'plg_') === 0) { $hasPrefix = true; }
|
||||||
|
if (!$hasPrefix) {
|
||||||
|
$element = $prefix . $element;
|
||||||
|
}
|
||||||
|
} elseif ($extType === 'plugin') {
|
||||||
|
$folder = '';
|
||||||
|
if (preg_match('/group="([^"]*)"/', $xml, $gm)) {
|
||||||
|
$folder = $gm[1];
|
||||||
|
}
|
||||||
|
if ($folder !== '' && strpos($element, 'plg_') !== 0) {
|
||||||
|
$element = "plg_{$folder}_" . $element;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$fields['element_name'] = $element;
|
||||||
|
|
||||||
|
// Name
|
||||||
|
if (preg_match('/<name>([^<]+)<\/name>/', $xml, $m)) {
|
||||||
|
$fields['name'] = trim($m[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version
|
||||||
|
if (preg_match('/<version>([^<]+)<\/version>/', $xml, $m)) {
|
||||||
|
$fields['version'] = trim($m[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description
|
||||||
|
if (preg_match('/<description>([^<]+)<\/description>/', $xml, $m)) {
|
||||||
|
$desc = trim($m[1]);
|
||||||
|
// Skip language string keys like COM_MOKOSUITE_DESCRIPTION
|
||||||
|
if (strpos($desc, '_') === false || strlen($desc) > 60) {
|
||||||
|
$fields['description'] = $desc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display name for update feeds
|
||||||
|
if (!empty($fields['name'])) {
|
||||||
|
$name = $fields['name'];
|
||||||
|
// If name already has "Type - " prefix, use as-is
|
||||||
|
if (preg_match('/^(Package|Component|Module|Plugin|Template|Library)\s*-\s*/i', $name)) {
|
||||||
|
$fields['display_name'] = $name;
|
||||||
|
} elseif (!empty($extType)) {
|
||||||
|
$fields['display_name'] = ucfirst($extType) . ' - ' . $name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Target Joomla version
|
||||||
|
if (preg_match('/<targetplatform\s[^>]*version="([^"]+)"/', $xml, $m)) {
|
||||||
|
$fields['target_version'] = trim($m[1]);
|
||||||
|
} else {
|
||||||
|
// Default for Joomla 5/6
|
||||||
|
$fields['target_version'] = '(5|6)\..*';
|
||||||
|
}
|
||||||
|
|
||||||
|
// PHP minimum
|
||||||
|
if (preg_match('/<php_minimum>([^<]+)<\/php_minimum>/', $xml, $m)) {
|
||||||
|
$fields['php_minimum'] = trim($m[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// License
|
||||||
|
if (preg_match('/<license>([^<]+)<\/license>/', $xml, $m)) {
|
||||||
|
$fields['license_spdx'] = $this->normalizeLicense(trim($m[1]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function findJoomlaManifest(string $root): ?string
|
||||||
|
{
|
||||||
|
// Priority: pkg_*.xml (package manifest)
|
||||||
|
$pkgXmls = array_merge(
|
||||||
|
SourceResolver::globSource($root, 'pkg_*.xml'),
|
||||||
|
glob("{$root}/pkg_*.xml") ?: []
|
||||||
|
);
|
||||||
|
if (!empty($pkgXmls)) {
|
||||||
|
return $pkgXmls[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Any extension XML in source dir
|
||||||
|
foreach (SourceResolver::globSource($root, '*.xml') as $file) {
|
||||||
|
$content = file_get_contents($file);
|
||||||
|
if (strpos($content, '<extension') !== false) {
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Root level
|
||||||
|
foreach (glob("{$root}/*.xml") ?: [] as $file) {
|
||||||
|
$content = file_get_contents($file);
|
||||||
|
if (strpos($content, '<extension') !== false) {
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Dolibarr ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private function detectDolibarr(string $root, string $repoName, array &$fields): void
|
||||||
|
{
|
||||||
|
$fields['language'] = 'PHP';
|
||||||
|
$fields['package_type'] = 'dolibarr-module';
|
||||||
|
|
||||||
|
$modFile = $this->findDolibarrModule($root);
|
||||||
|
if ($modFile === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = file_get_contents($modFile);
|
||||||
|
|
||||||
|
// Element name from class file
|
||||||
|
$modBasename = basename($modFile, '.class.php');
|
||||||
|
$fields['element_name'] = strtolower(preg_replace('/^mod/', '', $modBasename));
|
||||||
|
|
||||||
|
// Name
|
||||||
|
if (preg_match('/\$this->name\s*=\s*[\'"]([^\'"]+)[\'"]/', $content, $m)) {
|
||||||
|
$fields['name'] = $m[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version
|
||||||
|
if (preg_match('/\$this->version\s*=\s*[\'"]([^\'"]+)[\'"]/', $content, $m)) {
|
||||||
|
$fields['version'] = $m[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description
|
||||||
|
if (preg_match('/\$this->description\s*=\s*[\'"]([^\'"]+)[\'"]/', $content, $m)) {
|
||||||
|
$desc = $m[1];
|
||||||
|
if (strpos($desc, '$') === false) {
|
||||||
|
$fields['description'] = $desc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// License
|
||||||
|
if (preg_match('/SPDX-License-Identifier:\s*(\S+)/', $content, $m)) {
|
||||||
|
$fields['license_spdx'] = $m[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function findDolibarrModule(string $root): ?string
|
||||||
|
{
|
||||||
|
$candidates = array_merge(
|
||||||
|
SourceResolver::globSource($root, 'core/modules/mod*.class.php'),
|
||||||
|
glob("{$root}/core/modules/mod*.class.php") ?: []
|
||||||
|
);
|
||||||
|
foreach ($candidates as $file) {
|
||||||
|
if (strpos(file_get_contents($file), 'DolibarrModules') !== false) {
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Go ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private function detectGo(string $root, string $repoName, array &$fields): void
|
||||||
|
{
|
||||||
|
$fields['language'] = 'Go';
|
||||||
|
$fields['package_type'] = 'application';
|
||||||
|
$fields['entry_point'] = './';
|
||||||
|
|
||||||
|
$goMod = "{$root}/go.mod";
|
||||||
|
if (!file_exists($goMod)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = file_get_contents($goMod);
|
||||||
|
|
||||||
|
// Module path → name
|
||||||
|
if (preg_match('/^module\s+(\S+)/m', $content, $m)) {
|
||||||
|
$modulePath = $m[1];
|
||||||
|
$parts = explode('/', $modulePath);
|
||||||
|
$fields['name'] = end($parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go version
|
||||||
|
if (preg_match('/^go\s+(\S+)/m', $content, $m)) {
|
||||||
|
// This is Go language version, not the project version
|
||||||
|
// Project version comes from git tags or source files
|
||||||
|
}
|
||||||
|
|
||||||
|
// License
|
||||||
|
$fields['license_spdx'] = $this->detectLicense($root);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Node / MCP ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private function detectNode(string $root, string $repoName, array &$fields): void
|
||||||
|
{
|
||||||
|
$pkgFile = "{$root}/package.json";
|
||||||
|
if (!file_exists($pkgFile)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pkg = json_decode(file_get_contents($pkgFile), true) ?? [];
|
||||||
|
|
||||||
|
$fields['name'] = $pkg['name'] ?? '';
|
||||||
|
// Strip npm scope
|
||||||
|
if (strpos($fields['name'], '/') !== false) {
|
||||||
|
$fields['name'] = explode('/', $fields['name'])[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
$fields['version'] = $pkg['version'] ?? '';
|
||||||
|
$fields['description'] = $pkg['description'] ?? '';
|
||||||
|
$fields['license_spdx'] = $pkg['license'] ?? '';
|
||||||
|
|
||||||
|
// Language detection
|
||||||
|
if (file_exists("{$root}/tsconfig.json")) {
|
||||||
|
$fields['language'] = 'TypeScript';
|
||||||
|
} else {
|
||||||
|
$fields['language'] = 'JavaScript';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Package type
|
||||||
|
$deps = array_merge(
|
||||||
|
array_keys($pkg['dependencies'] ?? []),
|
||||||
|
array_keys($pkg['devDependencies'] ?? [])
|
||||||
|
);
|
||||||
|
$isMcp = false;
|
||||||
|
foreach ($deps as $dep) {
|
||||||
|
if (strpos($dep, '@modelcontextprotocol/') === 0 || $dep === '@anthropic/mcp-sdk') {
|
||||||
|
$isMcp = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$fields['package_type'] = $isMcp ? 'mcp-server' : 'application';
|
||||||
|
|
||||||
|
// Entry point
|
||||||
|
if (file_exists("{$root}/dist")) {
|
||||||
|
$fields['entry_point'] = 'dist/';
|
||||||
|
} elseif (file_exists("{$root}/src")) {
|
||||||
|
$fields['entry_point'] = 'src/';
|
||||||
|
} else {
|
||||||
|
$fields['entry_point'] = './';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Generic ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private function detectGeneric(string $root, string $repoName, array &$fields): void
|
||||||
|
{
|
||||||
|
$fields['package_type'] = 'generic';
|
||||||
|
|
||||||
|
// Try to detect language from file extensions
|
||||||
|
$fields['language'] = $this->detectLanguageFromFiles($root);
|
||||||
|
$fields['license_spdx'] = $this->detectLicense($root);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Shared detection helpers
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
private function detectEntryPoint(string $root): string
|
||||||
|
{
|
||||||
|
$abs = SourceResolver::resolveAbsolute($root);
|
||||||
|
if ($abs !== null) {
|
||||||
|
return basename($abs) . '/';
|
||||||
|
}
|
||||||
|
if (is_dir("{$root}/dist")) return 'dist/';
|
||||||
|
if (is_dir("{$root}/src")) return 'src/';
|
||||||
|
return './';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function detectLicense(string $root): string
|
||||||
|
{
|
||||||
|
// Check LICENSE file
|
||||||
|
foreach (['LICENSE', 'LICENSE.md', 'LICENSE.txt', 'COPYING'] as $name) {
|
||||||
|
$file = "{$root}/{$name}";
|
||||||
|
if (!file_exists($file)) continue;
|
||||||
|
$content = file_get_contents($file);
|
||||||
|
|
||||||
|
// SPDX header
|
||||||
|
if (preg_match('/SPDX-License-Identifier:\s*(\S+)/', $content, $m)) {
|
||||||
|
return $m[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common license patterns
|
||||||
|
if (strpos($content, 'GNU GENERAL PUBLIC LICENSE') !== false) {
|
||||||
|
if (strpos($content, 'Version 3') !== false) return 'GPL-3.0-or-later';
|
||||||
|
if (strpos($content, 'Version 2') !== false) return 'GPL-2.0-or-later';
|
||||||
|
}
|
||||||
|
if (strpos($content, 'MIT License') !== false) return 'MIT';
|
||||||
|
if (strpos($content, 'Apache License') !== false && strpos($content, 'Version 2.0') !== false) return 'Apache-2.0';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private function detectLanguageFromFiles(string $root): string
|
||||||
|
{
|
||||||
|
$counts = ['PHP' => 0, 'Go' => 0, 'TypeScript' => 0, 'JavaScript' => 0, 'Python' => 0, 'Shell' => 0];
|
||||||
|
|
||||||
|
$extensions = [
|
||||||
|
'php' => 'PHP', 'go' => 'Go', 'ts' => 'TypeScript',
|
||||||
|
'js' => 'JavaScript', 'py' => 'Python', 'sh' => 'Shell',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Quick scan: only check top two levels
|
||||||
|
foreach (glob("{$root}/*") ?: [] as $item) {
|
||||||
|
$ext = pathinfo($item, PATHINFO_EXTENSION);
|
||||||
|
if (isset($extensions[$ext])) {
|
||||||
|
$counts[$extensions[$ext]]++;
|
||||||
|
}
|
||||||
|
if (is_dir($item) && basename($item)[0] !== '.') {
|
||||||
|
foreach (glob("{$item}/*") ?: [] as $subItem) {
|
||||||
|
$ext = pathinfo($subItem, PATHINFO_EXTENSION);
|
||||||
|
if (isset($extensions[$ext])) {
|
||||||
|
$counts[$extensions[$ext]]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
arsort($counts);
|
||||||
|
$top = key($counts);
|
||||||
|
return $counts[$top] > 0 ? $top : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeLicense(string $license): string
|
||||||
|
{
|
||||||
|
$lower = strtolower($license);
|
||||||
|
$isGpl = strpos($lower, 'gpl') !== false || strpos($lower, 'general public license') !== false;
|
||||||
|
if ($isGpl && strpos($lower, '3') !== false) return 'GPL-3.0-or-later';
|
||||||
|
if ($isGpl && strpos($lower, '2') !== false) return 'GPL-2.0-or-later';
|
||||||
|
if ($lower === 'mit' || strpos($lower, 'mit license') !== false) return 'MIT';
|
||||||
|
if (strpos($lower, 'apache') !== false) return 'Apache-2.0';
|
||||||
|
return $license;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function detectRepoName(string $root): string
|
||||||
|
{
|
||||||
|
$gitConfig = "{$root}/.git/config";
|
||||||
|
if (!file_exists($gitConfig)) {
|
||||||
|
return basename($root);
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = file_get_contents($gitConfig);
|
||||||
|
if (preg_match('/url\s*=\s*.*\/([^\/\s]+?)(?:\.git)?\s*$/m', $content, $m)) {
|
||||||
|
return $m[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return basename($root);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// API interaction
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
private function fetchManifest(string $apiBase, string $org, string $repo, string $token): ?array
|
||||||
|
{
|
||||||
|
$url = "{$apiBase}/repos/{$org}/{$repo}/manifest";
|
||||||
|
$ctx = stream_context_create([
|
||||||
|
'http' => [
|
||||||
|
'header' => "Authorization: token {$token}\r\nAccept: application/json\r\n",
|
||||||
|
'timeout' => 10,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$body = @file_get_contents($url, false, $ctx);
|
||||||
|
if ($body === false) return null;
|
||||||
|
|
||||||
|
return json_decode($body, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function computeDiff(array $current, array $detected): array
|
||||||
|
{
|
||||||
|
// Map detected keys to API keys (underscores match)
|
||||||
|
$changes = [];
|
||||||
|
|
||||||
|
foreach ($detected as $key => $value) {
|
||||||
|
$apiKey = $key;
|
||||||
|
$currentVal = $current[$apiKey] ?? '';
|
||||||
|
|
||||||
|
// Only flag as changed if detected value is non-empty and differs
|
||||||
|
if ($value !== '' && $value !== $currentVal) {
|
||||||
|
// Don't overwrite a non-empty API value with a detected value
|
||||||
|
// unless the API value is actually empty
|
||||||
|
if ($currentVal === '' || $this->shouldOverride($key, $currentVal, $value)) {
|
||||||
|
$changes[$key] = [
|
||||||
|
'current' => $currentVal,
|
||||||
|
'detected' => $value,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function shouldOverride(string $field, string $current, string $detected): bool
|
||||||
|
{
|
||||||
|
// Version: detected from source is authoritative
|
||||||
|
if ($field === 'version') return true;
|
||||||
|
|
||||||
|
// These fields: source files are authoritative
|
||||||
|
if (in_array($field, ['element_name', 'package_type', 'language', 'entry_point'], true)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For other fields, only fill empty — don't overwrite manual edits
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function pushManifest(string $apiBase, string $org, string $repo, string $token, array $current, array $update): bool
|
||||||
|
{
|
||||||
|
$merged = array_merge($current, $update);
|
||||||
|
$url = "{$apiBase}/repos/{$org}/{$repo}/manifest";
|
||||||
|
$payload = json_encode($merged);
|
||||||
|
|
||||||
|
$ctx = stream_context_create([
|
||||||
|
'http' => [
|
||||||
|
'method' => 'PUT',
|
||||||
|
'header' => "Authorization: token {$token}\r\nContent-Type: application/json\r\nAccept: application/json\r\n",
|
||||||
|
'content' => $payload,
|
||||||
|
'timeout' => 10,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$body = @file_get_contents($url, false, $ctx);
|
||||||
|
return $body !== false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$app = new ManifestDetectCli();
|
||||||
|
exit($app->execute());
|
||||||
@@ -6,9 +6,9 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: moko-platform.CLI
|
* DEFGROUP: mokocli.CLI
|
||||||
* INGROUP: moko-platform
|
* INGROUP: mokocli
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /cli/manifest_element.php
|
* PATH: /cli/manifest_element.php
|
||||||
* BRIEF: Extract element name, type, type prefix, and ZIP name from manifest
|
* BRIEF: Extract element name, type, type prefix, and ZIP name from manifest
|
||||||
*/
|
*/
|
||||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoCli\{CliFramework, SourceResolver};
|
||||||
|
|
||||||
class ManifestElementCli extends CliFramework
|
class ManifestElementCli extends CliFramework
|
||||||
{
|
{
|
||||||
@@ -48,7 +48,7 @@ class ManifestElementCli extends CliFramework
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
$extManifest = null;
|
$extManifest = null;
|
||||||
$manifestFiles = array_merge(glob("{$root}/src/pkg_*.xml") ?: [], glob("{$root}/src/*.xml") ?: [], glob("{$root}/*.xml") ?: []);
|
$manifestFiles = array_merge(SourceResolver::globSource($root, 'pkg_*.xml'), SourceResolver::globSource($root, '*.xml'), glob("{$root}/*.xml") ?: []);
|
||||||
foreach ($manifestFiles as $file) {
|
foreach ($manifestFiles as $file) {
|
||||||
$c = file_get_contents($file);
|
$c = file_get_contents($file);
|
||||||
if (strpos($c, '<extension') !== false) {
|
if (strpos($c, '<extension') !== false) {
|
||||||
@@ -58,8 +58,7 @@ class ManifestElementCli extends CliFramework
|
|||||||
}
|
}
|
||||||
$modFile = null;
|
$modFile = null;
|
||||||
$modFiles = array_merge(
|
$modFiles = array_merge(
|
||||||
glob("{$root}/src/core/modules/mod*.class.php") ?: [],
|
SourceResolver::globSource($root, 'core/modules/mod*.class.php'),
|
||||||
glob("{$root}/htdocs/core/modules/mod*.class.php") ?: [],
|
|
||||||
glob("{$root}/core/modules/mod*.class.php") ?: []
|
glob("{$root}/core/modules/mod*.class.php") ?: []
|
||||||
);
|
);
|
||||||
foreach ($modFiles as $file) {
|
foreach ($modFiles as $file) {
|
||||||
|
|||||||
@@ -0,0 +1,564 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*
|
||||||
|
* FILE INFORMATION
|
||||||
|
* DEFGROUP: mokocli.CLI
|
||||||
|
* INGROUP: mokocli
|
||||||
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
|
* PATH: /cli/manifest_integrity.php
|
||||||
|
* VERSION: 09.38.01
|
||||||
|
* BRIEF: Cross-check manifest API fields against repo contents across the org
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
|
use MokoCli\CliFramework;
|
||||||
|
|
||||||
|
class ManifestIntegrityCli extends CliFramework
|
||||||
|
{
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this->setDescription('Cross-check manifest fields against repo contents across the org');
|
||||||
|
$this->addArgument('--path', 'Single repo path (local mode)', '');
|
||||||
|
$this->addArgument('--org', 'Gitea org (bulk mode)', 'MokoConsulting');
|
||||||
|
$this->addArgument('--repo', 'Single repo name (remote mode)', '');
|
||||||
|
$this->addArgument('--token', 'Gitea API token (or GITEA_TOKEN env)', '');
|
||||||
|
$this->addArgument('--api-base', 'Gitea API base URL', 'https://git.mokoconsulting.tech/api/v1');
|
||||||
|
$this->addArgument('--fix', 'Push fixes for detected drift', false);
|
||||||
|
$this->addArgument('--json', 'Output as JSON', false);
|
||||||
|
$this->addArgument('--quiet', 'Only show repos with issues', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function run(): int
|
||||||
|
{
|
||||||
|
$path = $this->getArgument('--path');
|
||||||
|
$org = $this->getArgument('--org');
|
||||||
|
$repoName = $this->getArgument('--repo');
|
||||||
|
$token = $this->getArgument('--token') ?: getenv('GITEA_TOKEN') ?: '';
|
||||||
|
$apiBase = rtrim($this->getArgument('--api-base'), '/');
|
||||||
|
$fixMode = (bool) $this->getArgument('--fix');
|
||||||
|
$jsonMode = (bool) $this->getArgument('--json');
|
||||||
|
$quiet = (bool) $this->getArgument('--quiet');
|
||||||
|
|
||||||
|
if ($token === '') {
|
||||||
|
$this->log('ERROR', 'API token required (use --token or GITEA_TOKEN env)');
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Mode selection ──────────────────────────────────────────
|
||||||
|
if ($path !== '') {
|
||||||
|
// Local mode: detect from source + compare to API
|
||||||
|
return $this->checkLocal($path, $org, $repoName, $token, $apiBase, $fixMode, $jsonMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($repoName !== '') {
|
||||||
|
// Single remote repo
|
||||||
|
return $this->checkRemoteRepo($org, $repoName, $token, $apiBase, $fixMode, $jsonMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bulk mode: all repos in org
|
||||||
|
return $this->checkOrg($org, $token, $apiBase, $fixMode, $jsonMode, $quiet);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Local mode — detect from source, compare to API
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
private function checkLocal(string $path, string $org, string $repoName, string $token, string $apiBase, bool $fix, bool $json): int
|
||||||
|
{
|
||||||
|
$root = realpath($path) ?: $path;
|
||||||
|
if (!is_dir($root)) {
|
||||||
|
$this->log('ERROR', "Path does not exist: {$path}");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($repoName === '') {
|
||||||
|
$repoName = $this->detectRepoName($root);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run manifest_detect logic
|
||||||
|
$detected = $this->runDetect($root, $repoName);
|
||||||
|
$current = $this->fetchManifest($apiBase, $org, $repoName, $token);
|
||||||
|
|
||||||
|
if ($current === null) {
|
||||||
|
$this->log('ERROR', "Failed to fetch manifest for {$org}/{$repoName}");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$issues = $this->validate($current, $detected, $repoName);
|
||||||
|
|
||||||
|
if ($json) {
|
||||||
|
echo json_encode(['repo' => $repoName, 'issues' => $issues], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";
|
||||||
|
} else {
|
||||||
|
$this->printIssues($repoName, $issues);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($fix && !empty($issues)) {
|
||||||
|
return $this->applyFixes($apiBase, $org, $repoName, $token, $current, $issues);
|
||||||
|
}
|
||||||
|
|
||||||
|
return empty($issues) ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Remote single repo mode — fetch source files via API
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
private function checkRemoteRepo(string $org, string $repoName, string $token, string $apiBase, bool $fix, bool $json): int
|
||||||
|
{
|
||||||
|
$current = $this->fetchManifest($apiBase, $org, $repoName, $token);
|
||||||
|
if ($current === null) {
|
||||||
|
$this->log('ERROR', "Failed to fetch manifest for {$org}/{$repoName}");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$issues = $this->validateManifestOnly($current, $repoName);
|
||||||
|
|
||||||
|
if ($json) {
|
||||||
|
echo json_encode(['repo' => $repoName, 'issues' => $issues], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";
|
||||||
|
} else {
|
||||||
|
$this->printIssues($repoName, $issues);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($fix && !empty($issues)) {
|
||||||
|
return $this->applyFixes($apiBase, $org, $repoName, $token, $current, $issues);
|
||||||
|
}
|
||||||
|
|
||||||
|
return empty($issues) ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Bulk org mode — check all repos
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
private function checkOrg(string $org, string $token, string $apiBase, bool $fix, bool $json, bool $quiet): int
|
||||||
|
{
|
||||||
|
$repos = $this->fetchOrgRepos($apiBase, $org, $token);
|
||||||
|
if ($repos === null) {
|
||||||
|
$this->log('ERROR', "Failed to fetch repos for org {$org}");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log('INFO', "Manifest Integrity Check — {$org} (" . count($repos) . " repos)");
|
||||||
|
|
||||||
|
$allResults = [];
|
||||||
|
$totalIssues = 0;
|
||||||
|
$reposWithIssues = 0;
|
||||||
|
|
||||||
|
foreach ($repos as $repo) {
|
||||||
|
$name = $repo['name'];
|
||||||
|
$manifest = $this->fetchManifest($apiBase, $org, $name, $token);
|
||||||
|
|
||||||
|
if ($manifest === null) {
|
||||||
|
if (!$quiet) {
|
||||||
|
$this->log('WARN', "{$name}: no manifest");
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$issues = $this->validateManifestOnly($manifest, $name);
|
||||||
|
|
||||||
|
if (!empty($issues)) {
|
||||||
|
$reposWithIssues++;
|
||||||
|
$totalIssues += count($issues);
|
||||||
|
|
||||||
|
if ($json) {
|
||||||
|
$allResults[] = ['repo' => $name, 'issues' => $issues];
|
||||||
|
} else {
|
||||||
|
$this->printIssues($name, $issues);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($fix) {
|
||||||
|
$this->applyFixes($apiBase, $org, $name, $token, $manifest, $issues);
|
||||||
|
}
|
||||||
|
} elseif (!$quiet && !$json) {
|
||||||
|
$this->log('OK', "{$name}: clean");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($json) {
|
||||||
|
echo json_encode($allResults, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";
|
||||||
|
} else {
|
||||||
|
echo "\n";
|
||||||
|
$level = $reposWithIssues > 0 ? 'WARN' : 'OK';
|
||||||
|
$this->log($level, sprintf(
|
||||||
|
'Summary: %d repos checked, %d with issues (%d total issues)',
|
||||||
|
count($repos),
|
||||||
|
$reposWithIssues,
|
||||||
|
$totalIssues
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $reposWithIssues > 0 ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Validation rules
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full validation: compare API manifest against locally-detected fields.
|
||||||
|
*/
|
||||||
|
private function validate(array $current, array $detected, string $repoName): array
|
||||||
|
{
|
||||||
|
$issues = [];
|
||||||
|
|
||||||
|
// Required fields that should never be empty
|
||||||
|
$required = ['platform', 'name', 'version', 'package_type', 'language', 'entry_point'];
|
||||||
|
foreach ($required as $field) {
|
||||||
|
if (empty($current[$field])) {
|
||||||
|
$fix = $detected[$field] ?? null;
|
||||||
|
$issues[] = [
|
||||||
|
'field' => $field,
|
||||||
|
'severity' => 'error',
|
||||||
|
'message' => 'Missing required field',
|
||||||
|
'current' => '',
|
||||||
|
'fix' => $fix,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drift detection: detected value differs from API
|
||||||
|
foreach ($detected as $field => $detectedValue) {
|
||||||
|
$currentValue = $current[$field] ?? '';
|
||||||
|
if ($detectedValue !== '' && $currentValue !== '' && $detectedValue !== $currentValue) {
|
||||||
|
// Version drift is expected on dev branches (suffix)
|
||||||
|
if ($field === 'version' && strpos($detectedValue, $currentValue) === 0) {
|
||||||
|
continue; // e.g., detected "02.34.50-dev" vs API "02.34.50"
|
||||||
|
}
|
||||||
|
if ($field === 'version' && strpos($currentValue, $detectedValue) === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$issues[] = [
|
||||||
|
'field' => $field,
|
||||||
|
'severity' => 'warn',
|
||||||
|
'message' => 'Drift: source differs from manifest',
|
||||||
|
'current' => $currentValue,
|
||||||
|
'fix' => $detectedValue,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Platform-specific structure validation
|
||||||
|
$platform = $current['platform'] ?? '';
|
||||||
|
$issues = array_merge($issues, $this->validatePlatformStructure($platform, $current, $repoName));
|
||||||
|
|
||||||
|
return $issues;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API-only validation: check manifest fields for completeness and consistency
|
||||||
|
* without access to source files.
|
||||||
|
*/
|
||||||
|
private function validateManifestOnly(array $manifest, string $repoName): array
|
||||||
|
{
|
||||||
|
$issues = [];
|
||||||
|
|
||||||
|
// Required fields
|
||||||
|
$required = ['platform', 'name', 'version', 'language'];
|
||||||
|
foreach ($required as $field) {
|
||||||
|
if (empty($manifest[$field])) {
|
||||||
|
$issues[] = [
|
||||||
|
'field' => $field,
|
||||||
|
'severity' => 'error',
|
||||||
|
'message' => 'Missing required field',
|
||||||
|
'current' => '',
|
||||||
|
'fix' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recommended fields
|
||||||
|
$recommended = ['package_type', 'entry_point', 'license_spdx', 'description'];
|
||||||
|
foreach ($recommended as $field) {
|
||||||
|
if (empty($manifest[$field])) {
|
||||||
|
$issues[] = [
|
||||||
|
'field' => $field,
|
||||||
|
'severity' => 'info',
|
||||||
|
'message' => 'Recommended field is empty',
|
||||||
|
'current' => '',
|
||||||
|
'fix' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Platform-specific checks
|
||||||
|
$platform = $manifest['platform'] ?? '';
|
||||||
|
$issues = array_merge($issues, $this->validatePlatformStructure($platform, $manifest, $repoName));
|
||||||
|
|
||||||
|
return $issues;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Platform-specific validation rules.
|
||||||
|
*/
|
||||||
|
private function validatePlatformStructure(string $platform, array $manifest, string $repoName): array
|
||||||
|
{
|
||||||
|
$issues = [];
|
||||||
|
|
||||||
|
switch ($platform) {
|
||||||
|
case 'joomla':
|
||||||
|
case 'waas-component':
|
||||||
|
// Joomla repos must have element_name
|
||||||
|
if (empty($manifest['element_name'])) {
|
||||||
|
$issues[] = [
|
||||||
|
'field' => 'element_name',
|
||||||
|
'severity' => 'error',
|
||||||
|
'message' => 'Joomla repos require element_name',
|
||||||
|
'current' => '',
|
||||||
|
'fix' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
// Language should be PHP
|
||||||
|
if (!empty($manifest['language']) && $manifest['language'] !== 'PHP') {
|
||||||
|
$issues[] = [
|
||||||
|
'field' => 'language',
|
||||||
|
'severity' => 'warn',
|
||||||
|
'message' => 'Joomla repos should have language=PHP',
|
||||||
|
'current' => $manifest['language'],
|
||||||
|
'fix' => 'PHP',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'dolibarr':
|
||||||
|
case 'crm-module':
|
||||||
|
if (!empty($manifest['language']) && $manifest['language'] !== 'PHP') {
|
||||||
|
$issues[] = [
|
||||||
|
'field' => 'language',
|
||||||
|
'severity' => 'warn',
|
||||||
|
'message' => 'Dolibarr repos should have language=PHP',
|
||||||
|
'current' => $manifest['language'],
|
||||||
|
'fix' => 'PHP',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'go':
|
||||||
|
if (!empty($manifest['language']) && $manifest['language'] !== 'Go') {
|
||||||
|
$issues[] = [
|
||||||
|
'field' => 'language',
|
||||||
|
'severity' => 'warn',
|
||||||
|
'message' => 'Go repos should have language=Go',
|
||||||
|
'current' => $manifest['language'],
|
||||||
|
'fix' => 'Go',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'mcp':
|
||||||
|
if (!empty($manifest['language']) && !in_array($manifest['language'], ['TypeScript', 'JavaScript'], true)) {
|
||||||
|
$issues[] = [
|
||||||
|
'field' => 'language',
|
||||||
|
'severity' => 'warn',
|
||||||
|
'message' => 'MCP repos should have language=TypeScript or JavaScript',
|
||||||
|
'current' => $manifest['language'],
|
||||||
|
'fix' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version format check: should be XX.YY.ZZ
|
||||||
|
$version = $manifest['version'] ?? '';
|
||||||
|
if ($version !== '' && !preg_match('/^\d{2}\.\d{2}\.\d{2}/', $version)) {
|
||||||
|
// Allow semver for node/go repos
|
||||||
|
if (!in_array($platform, ['mcp', 'node', 'go'], true)) {
|
||||||
|
$issues[] = [
|
||||||
|
'field' => 'version',
|
||||||
|
'severity' => 'info',
|
||||||
|
'message' => 'Version does not match XX.YY.ZZ format',
|
||||||
|
'current' => $version,
|
||||||
|
'fix' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $issues;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Output
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
private function printIssues(string $repoName, array $issues): void
|
||||||
|
{
|
||||||
|
if (empty($issues)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$errors = count(array_filter($issues, fn($i) => $i['severity'] === 'error'));
|
||||||
|
$warns = count(array_filter($issues, fn($i) => $i['severity'] === 'warn'));
|
||||||
|
$infos = count($issues) - $errors - $warns;
|
||||||
|
|
||||||
|
echo "\n";
|
||||||
|
$summary = [];
|
||||||
|
if ($errors > 0) $summary[] = "{$errors} error(s)";
|
||||||
|
if ($warns > 0) $summary[] = "{$warns} warning(s)";
|
||||||
|
if ($infos > 0) $summary[] = "{$infos} info";
|
||||||
|
$this->log($errors > 0 ? 'ERROR' : 'WARN', "{$repoName} — " . implode(', ', $summary));
|
||||||
|
|
||||||
|
foreach ($issues as $issue) {
|
||||||
|
$icon = match ($issue['severity']) {
|
||||||
|
'error' => 'ERROR',
|
||||||
|
'warn' => 'WARN',
|
||||||
|
default => 'INFO',
|
||||||
|
};
|
||||||
|
$msg = sprintf(' %-18s %s', $issue['field'], $issue['message']);
|
||||||
|
if ($issue['current'] !== '') {
|
||||||
|
$msg .= " (current: {$issue['current']})";
|
||||||
|
}
|
||||||
|
if ($issue['fix'] !== null) {
|
||||||
|
$msg .= " → fix: {$issue['fix']}";
|
||||||
|
}
|
||||||
|
$this->log($icon, $msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Fix application
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
private function applyFixes(string $apiBase, string $org, string $repo, string $token, array $current, array $issues): int
|
||||||
|
{
|
||||||
|
$fixes = [];
|
||||||
|
foreach ($issues as $issue) {
|
||||||
|
if ($issue['fix'] !== null && $issue['fix'] !== '') {
|
||||||
|
$fixes[$issue['field']] = $issue['fix'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($fixes)) {
|
||||||
|
$this->log('INFO', "{$repo}: no auto-fixable issues");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$merged = array_merge($current, $fixes);
|
||||||
|
$url = "{$apiBase}/repos/{$org}/{$repo}/manifest";
|
||||||
|
$payload = json_encode($merged);
|
||||||
|
|
||||||
|
$ctx = stream_context_create([
|
||||||
|
'http' => [
|
||||||
|
'method' => 'PUT',
|
||||||
|
'header' => "Authorization: token {$token}\r\nContent-Type: application/json\r\nAccept: application/json\r\n",
|
||||||
|
'content' => $payload,
|
||||||
|
'timeout' => 10,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$body = @file_get_contents($url, false, $ctx);
|
||||||
|
if ($body === false) {
|
||||||
|
$this->log('ERROR', "{$repo}: failed to push fixes");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log('OK', "{$repo}: fixed " . implode(', ', array_keys($fixes)));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// API helpers
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
private function fetchManifest(string $apiBase, string $org, string $repo, string $token): ?array
|
||||||
|
{
|
||||||
|
$url = "{$apiBase}/repos/{$org}/{$repo}/manifest";
|
||||||
|
$ctx = stream_context_create([
|
||||||
|
'http' => [
|
||||||
|
'header' => "Authorization: token {$token}\r\nAccept: application/json\r\n",
|
||||||
|
'timeout' => 10,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$body = @file_get_contents($url, false, $ctx);
|
||||||
|
if ($body === false) return null;
|
||||||
|
|
||||||
|
$data = json_decode($body, true);
|
||||||
|
return is_array($data) ? $data : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function fetchOrgRepos(string $apiBase, string $org, string $token): ?array
|
||||||
|
{
|
||||||
|
$allRepos = [];
|
||||||
|
$page = 1;
|
||||||
|
$limit = 50;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
$url = "{$apiBase}/orgs/{$org}/repos?page={$page}&limit={$limit}";
|
||||||
|
$ctx = stream_context_create([
|
||||||
|
'http' => [
|
||||||
|
'header' => "Authorization: token {$token}\r\nAccept: application/json\r\n",
|
||||||
|
'timeout' => 15,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$body = @file_get_contents($url, false, $ctx);
|
||||||
|
if ($body === false) return null;
|
||||||
|
|
||||||
|
$repos = json_decode($body, true);
|
||||||
|
if (!is_array($repos) || empty($repos)) break;
|
||||||
|
|
||||||
|
$allRepos = array_merge($allRepos, $repos);
|
||||||
|
|
||||||
|
if (count($repos) < $limit) break;
|
||||||
|
$page++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out archived and empty repos
|
||||||
|
return array_filter($allRepos, fn($r) => !($r['archived'] ?? false) && !($r['empty'] ?? false));
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Detection (delegates to manifest_detect logic)
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
private function runDetect(string $root, string $repoName): array
|
||||||
|
{
|
||||||
|
$script = __DIR__ . '/manifest_detect.php';
|
||||||
|
$redirect = PHP_OS_FAMILY === 'Windows' ? '2>NUL' : '2>/dev/null';
|
||||||
|
$cmd = sprintf(
|
||||||
|
'php %s --path %s --repo %s --json --quiet %s',
|
||||||
|
escapeshellarg($script),
|
||||||
|
escapeshellarg($root),
|
||||||
|
escapeshellarg($repoName),
|
||||||
|
$redirect
|
||||||
|
);
|
||||||
|
|
||||||
|
$output = shell_exec($cmd) ?? '';
|
||||||
|
|
||||||
|
// Extract JSON object from output (skip banner/log lines)
|
||||||
|
if (preg_match('/\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/s', $output, $m)) {
|
||||||
|
$data = json_decode($m[0], true);
|
||||||
|
if (is_array($data)) {
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function detectRepoName(string $root): string
|
||||||
|
{
|
||||||
|
$gitConfig = "{$root}/.git/config";
|
||||||
|
if (!file_exists($gitConfig)) {
|
||||||
|
return basename($root);
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = file_get_contents($gitConfig);
|
||||||
|
if (preg_match('/url\s*=\s*.*\/([^\/\s]+?)(?:\.git)?\s*$/m', $content, $m)) {
|
||||||
|
return $m[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return basename($root);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$app = new ManifestIntegrityCli();
|
||||||
|
exit($app->execute());
|
||||||
@@ -0,0 +1,280 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*
|
||||||
|
* FILE INFORMATION
|
||||||
|
* DEFGROUP: mokocli.CLI
|
||||||
|
* INGROUP: mokocli
|
||||||
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
|
* PATH: /cli/manifest_licensing.php
|
||||||
|
* VERSION: 09.38.01
|
||||||
|
* BRIEF: Ensure licensing tags (updateservers, dlid) in Joomla extension manifests
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
|
use MokoCli\{CliFramework, SourceResolver};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the <licensing> block from .mokogitea/manifest.xml and ensures that the
|
||||||
|
* Joomla extension manifest contains the correct <updateservers> and <dlid> tags.
|
||||||
|
*
|
||||||
|
* manifest.xml licensing block example:
|
||||||
|
*
|
||||||
|
* <licensing>
|
||||||
|
* <enabled>true</enabled>
|
||||||
|
* <dlid>true</dlid>
|
||||||
|
* <update-server>https://git.mokoconsulting.tech/{org}/{repo}/updates.xml</update-server>
|
||||||
|
* <update-server-name>MyExtension Updates</update-server-name>
|
||||||
|
* </licensing>
|
||||||
|
*
|
||||||
|
* Supports {org} and {repo} placeholders in update-server URL, resolved from
|
||||||
|
* the manifest's <identity> block or git remote.
|
||||||
|
*/
|
||||||
|
class ManifestLicensingCli extends CliFramework
|
||||||
|
{
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this->setDescription('Ensure licensing tags (updateservers, dlid) in Joomla extension manifests');
|
||||||
|
$this->addArgument('--path', 'Repository root path', '.');
|
||||||
|
$this->addArgument('--fix', 'Apply fixes (default: dry-run check only)', false);
|
||||||
|
$this->addArgument('--github-output', 'Write results to $GITHUB_OUTPUT', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function run(): int
|
||||||
|
{
|
||||||
|
$root = realpath($this->getArgument('--path')) ?: $this->getArgument('--path');
|
||||||
|
$fix = (bool) $this->getArgument('--fix');
|
||||||
|
$ghOutput = (bool) $this->getArgument('--github-output');
|
||||||
|
|
||||||
|
// ── 1. Read manifest.xml ──────────────────────────────────────────
|
||||||
|
$manifestFile = "{$root}/.mokogitea/manifest.xml";
|
||||||
|
|
||||||
|
if (!file_exists($manifestFile)) {
|
||||||
|
$this->log('WARN', "No manifest.xml found at {$manifestFile}");
|
||||||
|
$this->outputResult($ghOutput, 'skipped', 'No manifest.xml');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$xml = @simplexml_load_file($manifestFile);
|
||||||
|
|
||||||
|
if ($xml === false) {
|
||||||
|
$this->log('ERROR', "Failed to parse {$manifestFile}");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 2. Check if licensing is enabled ──────────────────────────────
|
||||||
|
if (!isset($xml->licensing) || (string) ($xml->licensing->enabled ?? '') !== 'true') {
|
||||||
|
$this->log('INFO', 'Licensing not enabled in manifest.xml — skipping');
|
||||||
|
$this->outputResult($ghOutput, 'skipped', 'Licensing not enabled');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$licensingNode = $xml->licensing;
|
||||||
|
$dlidEnabled = ((string) ($licensingNode->dlid ?? 'true')) === 'true';
|
||||||
|
$updateServerUrl = (string) ($licensingNode->{'update-server'} ?? '');
|
||||||
|
$updateServerName = (string) ($licensingNode->{'update-server-name'} ?? '');
|
||||||
|
|
||||||
|
// ── 3. Resolve placeholders ───────────────────────────────────────
|
||||||
|
$org = (string) ($xml->identity->org ?? '');
|
||||||
|
$repo = (string) ($xml->identity->name ?? '');
|
||||||
|
|
||||||
|
// Fallback to git remote if manifest doesn't have org/name
|
||||||
|
if (empty($org) || empty($repo)) {
|
||||||
|
$remote = trim((string) @shell_exec("cd " . escapeshellarg($root) . " && git remote get-url origin 2>/dev/null"));
|
||||||
|
|
||||||
|
if (preg_match('#[/:]([^/]+)/([^/.]+?)(?:\.git)?$#', $remote, $m)) {
|
||||||
|
if (empty($org)) {
|
||||||
|
$org = $m[1];
|
||||||
|
}
|
||||||
|
if (empty($repo)) {
|
||||||
|
$repo = $m[2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default update server URL if not specified
|
||||||
|
if (empty($updateServerUrl) && !empty($org) && !empty($repo)) {
|
||||||
|
$updateServerUrl = "https://git.mokoconsulting.tech/{$org}/{$repo}/updates.xml";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve {org} and {repo} placeholders
|
||||||
|
$updateServerUrl = str_replace(['{org}', '{repo}'], [$org, $repo], $updateServerUrl);
|
||||||
|
|
||||||
|
// Default server name from display-name or repo name
|
||||||
|
if (empty($updateServerName)) {
|
||||||
|
$displayName = (string) ($xml->identity->{'display-name'} ?? $repo);
|
||||||
|
$updateServerName = $displayName . ' Updates';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($updateServerUrl)) {
|
||||||
|
$this->log('ERROR', 'Cannot determine update server URL — set <update-server> in manifest.xml or ensure org/repo are available');
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log('INFO', "Licensing enabled — org={$org}, repo={$repo}");
|
||||||
|
$this->log('INFO', "Update server: {$updateServerUrl}");
|
||||||
|
$this->log('INFO', "DLID required: " . ($dlidEnabled ? 'yes' : 'no'));
|
||||||
|
|
||||||
|
// ── 4. Find Joomla extension manifests ────────────────────────────
|
||||||
|
$xmlFiles = array_merge(
|
||||||
|
SourceResolver::globSource($root, '*.xml'),
|
||||||
|
SourceResolver::globSource($root, 'packages/*/*.xml'),
|
||||||
|
glob("{$root}/*.xml") ?: []
|
||||||
|
);
|
||||||
|
|
||||||
|
$packageManifest = null;
|
||||||
|
|
||||||
|
foreach ($xmlFiles as $file) {
|
||||||
|
$content = file_get_contents($file);
|
||||||
|
|
||||||
|
if (!str_contains($content, '<extension')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the package manifest (type="package") or the main extension manifest
|
||||||
|
if (str_contains($content, 'type="package"')) {
|
||||||
|
$packageManifest = $file;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: first extension manifest found
|
||||||
|
if ($packageManifest === null) {
|
||||||
|
$packageManifest = $file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($packageManifest === null) {
|
||||||
|
$this->log('WARN', 'No Joomla extension manifest found');
|
||||||
|
$this->outputResult($ghOutput, 'skipped', 'No extension manifest');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$relPath = str_replace($root . '/', '', str_replace('\\', '/', $packageManifest));
|
||||||
|
$this->log('INFO', "Package manifest: {$relPath}");
|
||||||
|
|
||||||
|
// ── 5. Check and fix the manifest ─────────────────────────────────
|
||||||
|
$content = file_get_contents($packageManifest);
|
||||||
|
$original = $content;
|
||||||
|
$changes = [];
|
||||||
|
|
||||||
|
// --- 5a. Ensure <updateservers> block with correct URL ---
|
||||||
|
if (preg_match('#<updateservers>\s*</updateservers>#s', $content)) {
|
||||||
|
// Empty updateservers block — inject the server
|
||||||
|
$replacement = "<updateservers>\n"
|
||||||
|
. " <server type=\"extension\" name=\"{$updateServerName}\">{$updateServerUrl}</server>\n"
|
||||||
|
. " </updateservers>";
|
||||||
|
$content = preg_replace('#<updateservers>\s*</updateservers>#s', $replacement, $content);
|
||||||
|
$changes[] = 'Added update server URL to empty <updateservers>';
|
||||||
|
} elseif (!str_contains($content, '<updateservers>')) {
|
||||||
|
// No updateservers at all — add before </extension>
|
||||||
|
$serverBlock = "\n <updateservers>\n"
|
||||||
|
. " <server type=\"extension\" name=\"{$updateServerName}\">{$updateServerUrl}</server>\n"
|
||||||
|
. " </updateservers>\n";
|
||||||
|
$content = str_replace('</extension>', $serverBlock . '</extension>', $content);
|
||||||
|
$changes[] = 'Added <updateservers> block';
|
||||||
|
} else {
|
||||||
|
// updateservers exists — verify URL is correct
|
||||||
|
if (preg_match('#<server[^>]*>([^<]+)</server>#', $content, $m)) {
|
||||||
|
if ($m[1] !== $updateServerUrl) {
|
||||||
|
$content = preg_replace(
|
||||||
|
'#(<server[^>]*>)[^<]+(</server>)#',
|
||||||
|
"\${1}{$updateServerUrl}\${2}",
|
||||||
|
$content
|
||||||
|
);
|
||||||
|
$changes[] = "Updated server URL: {$m[1]} → {$updateServerUrl}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 5b. Ensure <dlid> tag if required ---
|
||||||
|
if ($dlidEnabled) {
|
||||||
|
if (!str_contains($content, '<dlid')) {
|
||||||
|
// Add before <updateservers> if present, otherwise before </extension>
|
||||||
|
$dlidTag = ' <dlid prefix="dlid=" suffix=""/>' . "\n";
|
||||||
|
|
||||||
|
if (str_contains($content, '<updateservers>')) {
|
||||||
|
$content = str_replace('<updateservers>', $dlidTag . "\n <updateservers>", $content);
|
||||||
|
} else {
|
||||||
|
$content = str_replace('</extension>', $dlidTag . '</extension>', $content);
|
||||||
|
}
|
||||||
|
|
||||||
|
$changes[] = 'Added <dlid> tag';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 5c. Ensure <blockChildUninstall> for packages ---
|
||||||
|
if (str_contains($content, 'type="package"') && !str_contains($content, '<blockChildUninstall>')) {
|
||||||
|
$blockTag = ' <blockChildUninstall>true</blockChildUninstall>' . "\n";
|
||||||
|
|
||||||
|
if (str_contains($content, '<dlid')) {
|
||||||
|
// Add after <dlid>
|
||||||
|
$content = preg_replace(
|
||||||
|
'#(<dlid[^/]*/>\s*\n)#',
|
||||||
|
"\${1}{$blockTag}",
|
||||||
|
$content
|
||||||
|
);
|
||||||
|
} elseif (str_contains($content, '<updateservers>')) {
|
||||||
|
$content = str_replace('<updateservers>', $blockTag . "\n <updateservers>", $content);
|
||||||
|
} else {
|
||||||
|
$content = str_replace('</extension>', $blockTag . '</extension>', $content);
|
||||||
|
}
|
||||||
|
|
||||||
|
$changes[] = 'Added <blockChildUninstall>true</blockChildUninstall>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 6. Report and apply ───────────────────────────────────────────
|
||||||
|
if (empty($changes)) {
|
||||||
|
$this->log('INFO', 'All licensing tags are correct — no changes needed');
|
||||||
|
$this->outputResult($ghOutput, 'ok', 'No changes needed');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($changes as $change) {
|
||||||
|
$this->log($fix ? 'INFO' : 'WARN', ($fix ? 'Fixed: ' : 'Needs fix: ') . $change);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($fix) {
|
||||||
|
file_put_contents($packageManifest, $content);
|
||||||
|
$this->log('INFO', "Wrote {$relPath} with " . count($changes) . " change(s)");
|
||||||
|
$this->outputResult($ghOutput, 'fixed', implode('; ', $changes));
|
||||||
|
} else {
|
||||||
|
$this->log('WARN', 'Run with --fix to apply changes');
|
||||||
|
$this->outputResult($ghOutput, 'needs-fix', implode('; ', $changes));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write result to $GITHUB_OUTPUT if requested.
|
||||||
|
*/
|
||||||
|
private function outputResult(bool $ghOutput, string $status, string $detail): void
|
||||||
|
{
|
||||||
|
if (!$ghOutput) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$outputFile = getenv('GITHUB_OUTPUT');
|
||||||
|
|
||||||
|
if ($outputFile === false || $outputFile === '') {
|
||||||
|
echo "licensing_status={$status}\n";
|
||||||
|
echo "licensing_detail={$detail}\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fh = fopen($outputFile, 'a');
|
||||||
|
fwrite($fh, "licensing_status={$status}\n");
|
||||||
|
fwrite($fh, "licensing_detail={$detail}\n");
|
||||||
|
fclose($fh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$app = new ManifestLicensingCli();
|
||||||
|
exit($app->execute());
|
||||||
+387
-83
@@ -6,25 +6,28 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: moko-platform.CLI
|
* DEFGROUP: mokocli.CLI
|
||||||
* INGROUP: moko-platform
|
* INGROUP: mokocli
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /cli/manifest_read.php
|
* PATH: /cli/manifest_read.php
|
||||||
* VERSION: 09.23.00
|
* VERSION: 09.38.01
|
||||||
* BRIEF: Parse .manifest.xml and output requested field(s) for CI consumption
|
* BRIEF: Read repo metadata from Gitea manifest API, auto-detect the rest
|
||||||
*/
|
*/
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoCli\CliFramework;
|
||||||
|
|
||||||
class ManifestReadCli extends CliFramework
|
class ManifestReadCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
/** Joomla extension XML element names searched in root and source/ dirs. */
|
||||||
|
private const JOOMLA_XML_ROOTS = ['extension', 'install'];
|
||||||
|
|
||||||
protected function configure(): void
|
protected function configure(): void
|
||||||
{
|
{
|
||||||
$this->setDescription('Parse manifest.xml and output requested field(s) for CI consumption');
|
$this->setDescription('Read repo metadata from Gitea API with auto-detection fallback');
|
||||||
$this->addArgument('--path', 'Repository root path', '.');
|
$this->addArgument('--path', 'Repository root path', '.');
|
||||||
$this->addArgument('--field', 'Single field name to output', '');
|
$this->addArgument('--field', 'Single field name to output', '');
|
||||||
$this->addArgument('--all', 'Print all fields as KEY=VALUE lines', false);
|
$this->addArgument('--all', 'Print all fields as KEY=VALUE lines', false);
|
||||||
@@ -37,95 +40,397 @@ class ManifestReadCli extends CliFramework
|
|||||||
$path = $this->getArgument('--path');
|
$path = $this->getArgument('--path');
|
||||||
$field = $this->getArgument('--field');
|
$field = $this->getArgument('--field');
|
||||||
$showAll = $this->getArgument('--all');
|
$showAll = $this->getArgument('--all');
|
||||||
$ghOutput = $this->getArgument('--github-output');
|
$ghOut = $this->getArgument('--github-output');
|
||||||
$jsonMode = $this->getArgument('--json');
|
$jsonMode = $this->getArgument('--json');
|
||||||
|
|
||||||
// Determine mode
|
$mode = match (true) {
|
||||||
if ($ghOutput) {
|
(bool) $ghOut => 'github-output',
|
||||||
$mode = 'github-output';
|
(bool) $showAll => 'all',
|
||||||
} elseif ($showAll) {
|
(bool) $jsonMode => 'json',
|
||||||
$mode = 'all';
|
default => 'field',
|
||||||
} elseif ($jsonMode) {
|
};
|
||||||
$mode = 'json';
|
|
||||||
} else {
|
$root = realpath($path) ?: $path;
|
||||||
$mode = 'field';
|
|
||||||
|
// ── 1. Resolve org/repo ──────────────────────────────────────────
|
||||||
|
[$org, $repo] = $this->resolveOrgRepo($root);
|
||||||
|
|
||||||
|
// ── 2. Primary: Gitea manifest API ───────────────────────────────
|
||||||
|
$fields = null;
|
||||||
|
if ($org !== '' && $repo !== '') {
|
||||||
|
$fields = $this->fetchFromApi($org, $repo);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Locate manifest --
|
// ── 3. Fallback: auto-detect from source tree ────────────────────
|
||||||
$root = realpath($path) ?: $path;
|
if ($fields === null) {
|
||||||
$manifestFile = null;
|
$this->log('INFO', 'API unavailable — falling back to source-tree detection');
|
||||||
|
$fields = $this->autoDetect($root, $repo);
|
||||||
|
}
|
||||||
|
|
||||||
// Priority: manifest.xml (current standard)
|
if (empty($fields)) {
|
||||||
$candidates = [
|
$this->log('ERROR', "Could not resolve metadata for {$root}");
|
||||||
"{$root}/.mokogitea/manifest.xml",
|
return 1;
|
||||||
"{$root}/.mokogitea/.manifest.xml", // legacy (dot-prefixed)
|
}
|
||||||
"{$root}/.mokogitea/.moko-platform", // legacy v4
|
|
||||||
|
// Provide backward-compatible aliases (hyphenated → underscore)
|
||||||
|
$fields = $this->addAliases($fields);
|
||||||
|
|
||||||
|
// Strip empty values
|
||||||
|
$fields = array_filter($fields, fn($v) => $v !== '' && $v !== null);
|
||||||
|
|
||||||
|
// ── 4. Output ────────────────────────────────────────────────────
|
||||||
|
return $this->outputFields($fields, $mode, $field);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Gitea manifest API ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
private function fetchFromApi(string $org, string $repo): ?array
|
||||||
|
{
|
||||||
|
$token = getenv('GA_TOKEN') ?: getenv('GITEA_TOKEN') ?: '';
|
||||||
|
$baseUrl = getenv('GITEA_URL') ?: 'https://git.mokoconsulting.tech';
|
||||||
|
$baseUrl = rtrim($baseUrl, '/');
|
||||||
|
|
||||||
|
if ($token === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = "{$baseUrl}/api/v1/repos/{$org}/{$repo}/manifest";
|
||||||
|
$ctx = stream_context_create([
|
||||||
|
'http' => [
|
||||||
|
'header' => "Authorization: token {$token}\r\nAccept: application/json\r\n",
|
||||||
|
'timeout' => 10,
|
||||||
|
'ignore_errors' => true,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$body = @file_get_contents($url, false, $ctx);
|
||||||
|
if ($body === false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check HTTP status from response headers
|
||||||
|
$status = 0;
|
||||||
|
if (isset($http_response_header[0])) {
|
||||||
|
preg_match('/\d{3}/', $http_response_header[0], $m);
|
||||||
|
$status = (int) ($m[0] ?? 0);
|
||||||
|
}
|
||||||
|
if ($status < 200 || $status >= 300) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode($body, true);
|
||||||
|
if (!is_array($data) || empty($data)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log('INFO', "Loaded metadata from Gitea manifest API ({$org}/{$repo})");
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Auto-detection fallback ──────────────────────────────────────────
|
||||||
|
|
||||||
|
private function autoDetect(string $root, string $repoName): array
|
||||||
|
{
|
||||||
|
$fields = [
|
||||||
|
'name' => $repoName ?: basename($root),
|
||||||
|
'org' => 'MokoConsulting',
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($candidates as $candidate) {
|
// Resolve source directory (source/ or src/)
|
||||||
if (file_exists($candidate)) {
|
$srcDir = null;
|
||||||
$manifestFile = $candidate;
|
foreach (['source', 'src'] as $candidate) {
|
||||||
|
if (is_dir("{$root}/{$candidate}")) {
|
||||||
|
$srcDir = $candidate;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($manifestFile === null) {
|
// ── Try Joomla detection ─────────────────────────────────────
|
||||||
$this->log('ERROR', "No manifest found in {$root}");
|
$joomlaResult = $this->detectJoomla($root, $srcDir);
|
||||||
return 1;
|
if ($joomlaResult !== null) {
|
||||||
|
$fields = array_merge($fields, $joomlaResult);
|
||||||
|
$this->log('INFO', "Auto-detected platform: joomla ({$fields['extension_type']} — {$fields['element_name']})");
|
||||||
|
return $fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Parse XML --
|
// ── Try Dolibarr detection ───────────────────────────────────
|
||||||
$xml = @simplexml_load_file($manifestFile);
|
$dolibarrResult = $this->detectDolibarr($root);
|
||||||
|
if ($dolibarrResult !== null) {
|
||||||
|
$fields = array_merge($fields, $dolibarrResult);
|
||||||
|
$this->log('INFO', "Auto-detected platform: dolibarr");
|
||||||
|
return $fields;
|
||||||
|
}
|
||||||
|
|
||||||
if ($xml === false) {
|
// ── Generic fallback ─────────────────────────────────────────
|
||||||
// Fallback: try YAML format (.mokostandards legacy)
|
$fields['platform'] = $this->detectGenericPlatform($root);
|
||||||
$content = file_get_contents($manifestFile);
|
$fields['element_name'] = strtolower($fields['name']);
|
||||||
$fields = [];
|
$fields['extension_type'] = 'application';
|
||||||
if (preg_match('/^platform:\s*(.+)/m', $content, $m)) {
|
$fields['language'] = $this->detectLanguage($root);
|
||||||
$fields['platform'] = trim($m[1], " \t\n\r\"'");
|
if ($srcDir !== null) {
|
||||||
|
$fields['entry_point'] = "{$srcDir}/";
|
||||||
}
|
}
|
||||||
if (preg_match('/^standards_version:\s*(.+)/m', $content, $m)) {
|
|
||||||
$fields['standards-version'] = trim($m[1], " \t\n\r\"'");
|
$this->log('INFO', "Auto-detected platform: {$fields['platform']}");
|
||||||
|
return $fields;
|
||||||
}
|
}
|
||||||
if (preg_match('/^governed_repo:\s*(.+)/m', $content, $m)) {
|
|
||||||
$fields['name'] = trim($m[1], " \t\n\r\"'");
|
/**
|
||||||
|
* Detect Joomla platform by scanning for extension XML manifests.
|
||||||
|
*
|
||||||
|
* Searches root and source/ dirs for XML files containing <extension type="...">.
|
||||||
|
* Extracts element name from the filename (pkg_*, com_*, mod_*, plg_*, tpl_*) or
|
||||||
|
* from the <element> tag inside the manifest.
|
||||||
|
*/
|
||||||
|
private function detectJoomla(string $root, ?string $srcDir): ?array
|
||||||
|
{
|
||||||
|
$searchDirs = [$root];
|
||||||
|
if ($srcDir !== null) {
|
||||||
|
$searchDirs[] = "{$root}/{$srcDir}";
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($searchDirs as $dir) {
|
||||||
|
$xmlFiles = glob("{$dir}/*.xml") ?: [];
|
||||||
|
foreach ($xmlFiles as $xmlFile) {
|
||||||
|
$content = @file_get_contents($xmlFile);
|
||||||
|
if ($content === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match <extension type="component|module|plugin|package|template|file|library">
|
||||||
|
if (!preg_match('/<extension\s+[^>]*type="([^"]+)"/', $content, $typeMatch)) {
|
||||||
|
// Also try legacy <install type="...">
|
||||||
|
if (!preg_match('/<install\s+[^>]*type="([^"]+)"/', $content, $typeMatch)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$extType = strtolower($typeMatch[1]);
|
||||||
|
$basename = pathinfo($xmlFile, PATHINFO_FILENAME);
|
||||||
|
|
||||||
|
// Try to extract element name from XML <element> tag
|
||||||
|
$xml = @simplexml_load_string($content);
|
||||||
|
$element = '';
|
||||||
|
if ($xml !== false) {
|
||||||
|
// Package manifests have <files><file ...>element</file></files>
|
||||||
|
// Component/module manifests have <element> or use filename
|
||||||
|
$element = (string) ($xml->element ?? '');
|
||||||
|
if ($element === '') {
|
||||||
|
$element = strtolower($basename);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Register namespace for XPath (optional, simple path works without)
|
$element = strtolower($basename);
|
||||||
$fields = [
|
}
|
||||||
'name' => (string)($xml->identity->name ?? ''),
|
|
||||||
'display-name' => (string)($xml->identity->{"display-name"} ?? ''),
|
// Derive display name
|
||||||
'org' => (string)($xml->identity->org ?? ''),
|
$displayName = (string) ($xml->name ?? ucfirst(str_replace('_', ' ', $basename)));
|
||||||
'description' => (string)($xml->identity->description ?? ''),
|
|
||||||
'license' => (string)($xml->identity->license ?? ''),
|
return [
|
||||||
'license-spdx' => (string)($xml->identity->license['spdx'] ?? ''),
|
'platform' => 'joomla',
|
||||||
'platform' => (string)($xml->governance->platform ?? ''),
|
'extension_type' => $extType,
|
||||||
'standards-version' => (string)($xml->governance->{"standards-version"} ?? ''),
|
'element_name' => $element,
|
||||||
'standards-source' => (string)($xml->governance->{"standards-source"} ?? ''),
|
'display_name' => $displayName,
|
||||||
'language' => (string)($xml->build->language ?? ''),
|
'language' => 'PHP',
|
||||||
'package-type' => (string)($xml->build->{"package-type"} ?? ''),
|
'entry_point' => ($srcDir ?? '.') . '/',
|
||||||
'entry-point' => (string)($xml->build->{"entry-point"} ?? ''),
|
|
||||||
'version' => (string)($xml->identity->version ?? ''),
|
|
||||||
'source-dir' => (string)($xml->deploy->{"source-dir"} ?? ''),
|
|
||||||
'remote-subdir' => (string)($xml->deploy->{"remote-subdir"} ?? ''),
|
|
||||||
'excludes' => (string)($xml->deploy->excludes ?? ''),
|
|
||||||
'dev-host' => (string)($xml->deploy->{"dev-host"} ?? ''),
|
|
||||||
'demo-host' => (string)($xml->deploy->{"demo-host"} ?? ''),
|
|
||||||
'manifest-file' => $manifestFile,
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strip empty values for cleaner output
|
// Also check for pkg_*.xml pattern specifically
|
||||||
$fields = array_filter($fields, fn($v) => $v !== '');
|
$pkgFiles = glob("{$dir}/pkg_*.xml") ?: [];
|
||||||
|
if (!empty($pkgFiles)) {
|
||||||
|
$basename = pathinfo($pkgFiles[0], PATHINFO_FILENAME);
|
||||||
|
return [
|
||||||
|
'platform' => 'joomla',
|
||||||
|
'extension_type' => 'package',
|
||||||
|
'element_name' => strtolower($basename),
|
||||||
|
'display_name' => ucfirst(str_replace('_', ' ', $basename)),
|
||||||
|
'language' => 'PHP',
|
||||||
|
'entry_point' => ($srcDir ?? '.') . '/',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -- Output --
|
// Check for com_*/manifest.xml pattern (component subdirectory)
|
||||||
|
$comDirs = glob("{$root}/com_*", GLOB_ONLYDIR) ?: [];
|
||||||
|
foreach ($comDirs as $comDir) {
|
||||||
|
$comManifest = glob("{$comDir}/*.xml") ?: [];
|
||||||
|
foreach ($comManifest as $xmlFile) {
|
||||||
|
$content = @file_get_contents($xmlFile);
|
||||||
|
if ($content && preg_match('/<extension\s+[^>]*type="component"/', $content)) {
|
||||||
|
return [
|
||||||
|
'platform' => 'joomla',
|
||||||
|
'extension_type' => 'component',
|
||||||
|
'element_name' => strtolower(basename($comDir)),
|
||||||
|
'display_name' => ucfirst(str_replace('com_', '', basename($comDir))),
|
||||||
|
'language' => 'PHP',
|
||||||
|
'entry_point' => ($srcDir ?? '.') . '/',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect Dolibarr platform by scanning for module descriptor files.
|
||||||
|
*/
|
||||||
|
private function detectDolibarr(string $root): ?array
|
||||||
|
{
|
||||||
|
// Look for mod*.class.php containing DolibarrModules
|
||||||
|
$searchPaths = [
|
||||||
|
"{$root}/core/modules/mod*.class.php",
|
||||||
|
"{$root}/*/core/modules/mod*.class.php",
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($searchPaths as $pattern) {
|
||||||
|
$files = glob($pattern) ?: [];
|
||||||
|
foreach ($files as $file) {
|
||||||
|
$content = @file_get_contents($file);
|
||||||
|
if ($content && str_contains($content, 'DolibarrModules')) {
|
||||||
|
$modName = pathinfo($file, PATHINFO_FILENAME);
|
||||||
|
// modMyModule.class → mymodule
|
||||||
|
$element = strtolower(preg_replace('/^mod/', '', str_replace('.class', '', $modName)));
|
||||||
|
|
||||||
|
return [
|
||||||
|
'platform' => 'dolibarr',
|
||||||
|
'extension_type' => 'module',
|
||||||
|
'element_name' => $element,
|
||||||
|
'display_name' => ucfirst($element),
|
||||||
|
'language' => 'PHP',
|
||||||
|
'entry_point' => './',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Secondary: check for update.txt (Dolibarr marker)
|
||||||
|
if (file_exists("{$root}/update.txt")) {
|
||||||
|
return [
|
||||||
|
'platform' => 'dolibarr',
|
||||||
|
'extension_type' => 'module',
|
||||||
|
'element_name' => strtolower(basename($root)),
|
||||||
|
'display_name' => basename($root),
|
||||||
|
'language' => 'PHP',
|
||||||
|
'entry_point' => './',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect generic platform type (php, nodejs, python, etc.) from project files.
|
||||||
|
*/
|
||||||
|
private function detectGenericPlatform(string $root): string
|
||||||
|
{
|
||||||
|
if (file_exists("{$root}/composer.json")) {
|
||||||
|
return 'php';
|
||||||
|
}
|
||||||
|
if (file_exists("{$root}/package.json")) {
|
||||||
|
return 'nodejs';
|
||||||
|
}
|
||||||
|
if (file_exists("{$root}/pyproject.toml") || file_exists("{$root}/setup.py")) {
|
||||||
|
return 'python';
|
||||||
|
}
|
||||||
|
if (file_exists("{$root}/go.mod")) {
|
||||||
|
return 'go';
|
||||||
|
}
|
||||||
|
if (file_exists("{$root}/Cargo.toml")) {
|
||||||
|
return 'rust';
|
||||||
|
}
|
||||||
|
return 'generic';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect primary language from project files.
|
||||||
|
*/
|
||||||
|
private function detectLanguage(string $root): string
|
||||||
|
{
|
||||||
|
if (file_exists("{$root}/composer.json")) {
|
||||||
|
return 'PHP';
|
||||||
|
}
|
||||||
|
if (file_exists("{$root}/tsconfig.json")) {
|
||||||
|
return 'TypeScript';
|
||||||
|
}
|
||||||
|
if (file_exists("{$root}/package.json")) {
|
||||||
|
return 'JavaScript';
|
||||||
|
}
|
||||||
|
if (file_exists("{$root}/pyproject.toml") || file_exists("{$root}/setup.py")) {
|
||||||
|
return 'Python';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Org/repo resolution ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve org and repo name from environment or git remote.
|
||||||
|
*
|
||||||
|
* @return array{0: string, 1: string} [org, repo]
|
||||||
|
*/
|
||||||
|
private function resolveOrgRepo(string $root): array
|
||||||
|
{
|
||||||
|
// 1. GITHUB_REPOSITORY env (set in Gitea Actions / GitHub Actions)
|
||||||
|
$envRepo = getenv('GITHUB_REPOSITORY') ?: '';
|
||||||
|
if ($envRepo !== '' && str_contains($envRepo, '/')) {
|
||||||
|
return explode('/', $envRepo, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Parse git remote origin URL
|
||||||
|
$remoteUrl = trim((string) shell_exec(
|
||||||
|
'git -C ' . escapeshellarg($root) . ' remote get-url origin 2>/dev/null'
|
||||||
|
));
|
||||||
|
|
||||||
|
if ($remoteUrl !== '') {
|
||||||
|
// SSH: git@host:Org/Repo.git or HTTPS: https://host/Org/Repo.git
|
||||||
|
if (preg_match('#[/:]([^/]+)/([^/]+?)(?:\.git)?$#', $remoteUrl, $m)) {
|
||||||
|
return [$m[1], $m[2]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['', basename($root)];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Backward-compatible aliases ──────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add hyphenated aliases for underscore fields (backward compat with old manifest.xml consumers).
|
||||||
|
* Also map old field names to new ones.
|
||||||
|
*/
|
||||||
|
private function addAliases(array $fields): array
|
||||||
|
{
|
||||||
|
// Map API field names → old manifest.xml hyphenated names
|
||||||
|
$aliases = [
|
||||||
|
'display_name' => 'display-name',
|
||||||
|
'license_spdx' => 'license-spdx',
|
||||||
|
'license_name' => 'license',
|
||||||
|
'standards_version' => 'standards-version',
|
||||||
|
'standards_source' => 'standards-source',
|
||||||
|
'extension_type' => 'package-type',
|
||||||
|
'entry_point' => 'entry-point',
|
||||||
|
'element_name' => 'name',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($aliases as $newKey => $oldKey) {
|
||||||
|
if (isset($fields[$newKey]) && !isset($fields[$oldKey])) {
|
||||||
|
$fields[$oldKey] = $fields[$newKey];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Output ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private function outputFields(array $fields, string $mode, string $field): int
|
||||||
|
{
|
||||||
switch ($mode) {
|
switch ($mode) {
|
||||||
case 'field':
|
case 'field':
|
||||||
if ($field === '') {
|
if ($field === '') {
|
||||||
$this->log('ERROR', "Usage: manifest_read.php --path <dir> --field <name>");
|
$this->log('ERROR', "Usage: manifest:read --path <dir> --field <name>");
|
||||||
$this->log('ERROR', " manifest_read.php --path <dir> --all");
|
$this->log('ERROR', " manifest:read --path <dir> --all");
|
||||||
$this->log('ERROR', " manifest_read.php --path <dir> --json");
|
$this->log('ERROR', " manifest:read --path <dir> --json");
|
||||||
$this->log('ERROR', " manifest_read.php --path <dir> --github-output");
|
$this->log('ERROR', " manifest:read --path <dir> --github-output");
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
echo ($fields[$field] ?? '') . "\n";
|
echo ($fields[$field] ?? '') . "\n";
|
||||||
@@ -142,22 +447,21 @@ class ManifestReadCli extends CliFramework
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'github-output':
|
case 'github-output':
|
||||||
$outputFile = getenv('GITHUB_OUTPUT');
|
$outputFile = getenv('GITHUB_OUTPUT') ?: getenv('GITEA_OUTPUT') ?: '';
|
||||||
if ($outputFile === false || $outputFile === '') {
|
$lines = [];
|
||||||
$this->log('ERROR', 'GITHUB_OUTPUT not set — printing to stdout instead');
|
|
||||||
foreach ($fields as $k => $v) {
|
foreach ($fields as $k => $v) {
|
||||||
// Convert field-name to FIELD_NAME for env var style
|
|
||||||
$envKey = str_replace('-', '_', $k);
|
$envKey = str_replace('-', '_', $k);
|
||||||
echo "{$envKey}={$v}\n";
|
$lines[$envKey] = "{$envKey}={$v}\n";
|
||||||
}
|
}
|
||||||
|
// Deduplicate (aliases may collide after underscore conversion)
|
||||||
|
$output = implode('', $lines);
|
||||||
|
|
||||||
|
if ($outputFile === '') {
|
||||||
|
$this->log('WARNING', 'GITHUB_OUTPUT not set — printing to stdout');
|
||||||
|
echo $output;
|
||||||
} else {
|
} else {
|
||||||
$fh = fopen($outputFile, 'a');
|
file_put_contents($outputFile, $output, FILE_APPEND);
|
||||||
foreach ($fields as $k => $v) {
|
$this->log('INFO', "Wrote " . count($lines) . " fields to GITHUB_OUTPUT");
|
||||||
$envKey = str_replace('-', '_', $k);
|
|
||||||
fwrite($fh, "{$envKey}={$v}\n");
|
|
||||||
}
|
|
||||||
fclose($fh);
|
|
||||||
$this->log('INFO', "Wrote " . count($fields) . " fields to GITHUB_OUTPUT");
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-12
@@ -6,9 +6,9 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: moko-platform.CLI
|
* DEFGROUP: mokocli.CLI
|
||||||
* INGROUP: moko-platform
|
* INGROUP: mokocli
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /cli/package_build.php
|
* PATH: /cli/package_build.php
|
||||||
* BRIEF: Build ZIP and tar.gz install packages for Joomla/Dolibarr/generic projects
|
* BRIEF: Build ZIP and tar.gz install packages for Joomla/Dolibarr/generic projects
|
||||||
*
|
*
|
||||||
@@ -19,7 +19,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoCli\{CliFramework, SourceResolver};
|
||||||
|
|
||||||
class PackageBuildCli extends CliFramework
|
class PackageBuildCli extends CliFramework
|
||||||
{
|
{
|
||||||
@@ -56,18 +56,13 @@ class PackageBuildCli extends CliFramework
|
|||||||
}
|
}
|
||||||
|
|
||||||
// -- Determine source directory -----------------------------------------------
|
// -- Determine source directory -----------------------------------------------
|
||||||
$sourceDir = null;
|
$sourceDir = SourceResolver::resolveAbsolute($root);
|
||||||
foreach (['src', 'htdocs'] as $candidate) {
|
|
||||||
if (is_dir("{$root}/{$candidate}")) {
|
|
||||||
$sourceDir = "{$root}/{$candidate}";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($sourceDir === null) {
|
if ($sourceDir === null) {
|
||||||
$this->log('ERROR', "No src/ or htdocs/ directory found in {$root}");
|
$this->log('ERROR', "No source/ or src/ directory found in {$root}");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
SourceResolver::warnIfLegacy($root);
|
||||||
|
|
||||||
// -- Determine element and type prefix from manifest --------------------------
|
// -- Determine element and type prefix from manifest --------------------------
|
||||||
$extElement = $elementOverride;
|
$extElement = $elementOverride;
|
||||||
|
|||||||
+163
-20
@@ -6,25 +6,32 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: moko-platform.CLI
|
* DEFGROUP: mokocli.CLI
|
||||||
* INGROUP: moko-platform
|
* INGROUP: mokocli
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /cli/platform_detect.php
|
* PATH: /cli/platform_detect.php
|
||||||
* BRIEF: Detect platform from manifest.xml file — outputs platform string
|
* VERSION: 09.38.01
|
||||||
|
* BRIEF: Auto-detect repository platform type and optionally update manifest
|
||||||
*/
|
*/
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoCli\CliFramework;
|
||||||
|
|
||||||
class PlatformDetectCli extends CliFramework
|
class PlatformDetectCli extends CliFramework
|
||||||
{
|
{
|
||||||
protected function configure(): void
|
protected function configure(): void
|
||||||
{
|
{
|
||||||
$this->setDescription('Detect platform from manifest.xml file');
|
$this->setDescription('Auto-detect repository platform type and optionally update manifest');
|
||||||
$this->addArgument('--path', 'Repository root path', '.');
|
$this->addArgument('--path', 'Local repo path to scan (default: .)', '.');
|
||||||
|
$this->addArgument('--token', 'Gitea API token for updating manifest', '');
|
||||||
|
$this->addArgument('--gitea-url', 'Gitea URL (default: https://git.mokoconsulting.tech)', 'https://git.mokoconsulting.tech');
|
||||||
|
$this->addArgument('--owner', 'Repo owner for API update', '');
|
||||||
|
$this->addArgument('--repo', 'Repo name for API update', '');
|
||||||
|
$this->addArgument('--update', 'Update manifest.platform via API (flag)', 'false');
|
||||||
|
$this->addArgument('--github-output', 'Append platform=xxx to $GITHUB_OUTPUT (flag)', 'false');
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function run(): int
|
protected function run(): int
|
||||||
@@ -32,25 +39,161 @@ class PlatformDetectCli extends CliFramework
|
|||||||
$path = $this->getArgument('--path');
|
$path = $this->getArgument('--path');
|
||||||
$root = realpath($path) ?: $path;
|
$root = realpath($path) ?: $path;
|
||||||
|
|
||||||
// Check .mokogitea/manifest.xml first, fallback to root
|
$token = $this->getArgument('--token');
|
||||||
$file = "{$root}/.mokogitea/manifest.xml";
|
$giteaUrl = rtrim($this->getArgument('--gitea-url'), '/');
|
||||||
if (!file_exists($file)) {
|
$owner = $this->getArgument('--owner');
|
||||||
$file = "{$root}/.mokostandards";
|
$repo = $this->getArgument('--repo');
|
||||||
}
|
$doUpdate = $this->isFlagSet('--update');
|
||||||
if (!file_exists($file)) {
|
$githubOutput = $this->isFlagSet('--github-output');
|
||||||
echo "unknown\n";
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
$content = file_get_contents($file);
|
$platform = $this->detectPlatform($root);
|
||||||
if (preg_match('/^platform:\s*(.+)/m', $content, $m)) {
|
|
||||||
echo trim($m[1], " \t\n\r\"'") . "\n";
|
$this->log('INFO', "Detected platform: {$platform}");
|
||||||
|
echo $platform . "\n";
|
||||||
|
|
||||||
|
// Append to $GITHUB_OUTPUT if requested
|
||||||
|
if ($githubOutput) {
|
||||||
|
$outputFile = getenv('GITHUB_OUTPUT');
|
||||||
|
|
||||||
|
if ($outputFile !== false && $outputFile !== '') {
|
||||||
|
file_put_contents($outputFile, "platform={$platform}\n", FILE_APPEND);
|
||||||
|
$this->log('INFO', "Appended platform={$platform} to \$GITHUB_OUTPUT");
|
||||||
} else {
|
} else {
|
||||||
echo "unknown\n";
|
$this->log('WARN', '$GITHUB_OUTPUT is not set; skipping output append.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update manifest via API if requested
|
||||||
|
if ($doUpdate) {
|
||||||
|
if ($token === '' || $owner === '' || $repo === '') {
|
||||||
|
$this->log('ERROR', '--update requires --token, --owner, and --repo.');
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->dryRun) {
|
||||||
|
$this->log('INFO', "[DRY RUN] Would update manifest.platform to \"{$platform}\" "
|
||||||
|
. "for {$owner}/{$repo}.");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log('INFO', "Updating manifest.platform for {$owner}/{$repo} to \"{$platform}\"...");
|
||||||
|
|
||||||
|
$response = $this->apiRequest(
|
||||||
|
$giteaUrl,
|
||||||
|
$token,
|
||||||
|
'PATCH',
|
||||||
|
"/api/v1/repos/{$owner}/{$repo}/metadata",
|
||||||
|
json_encode(['platform' => $platform])
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($response['code'] >= 200 && $response['code'] < 300) {
|
||||||
|
$this->log('INFO', "Manifest updated successfully (HTTP {$response['code']}).");
|
||||||
|
} else {
|
||||||
|
$this->log('ERROR', "Failed to update manifest (HTTP {$response['code']}): "
|
||||||
|
. $response['body']);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function detectPlatform(string $root): string
|
||||||
|
{
|
||||||
|
// 1. Joomla — has pkg_*.xml or Joomla-style extension manifest
|
||||||
|
$joomlaIndicators = array_merge(
|
||||||
|
glob("{$root}/source/pkg_*.xml") ?: [],
|
||||||
|
glob("{$root}/pkg_*.xml") ?: [],
|
||||||
|
glob("{$root}/source/packages/*/services/provider.php") ?: [],
|
||||||
|
glob("{$root}/**/templateDetails.xml") ?: [],
|
||||||
|
);
|
||||||
|
if (!empty($joomlaIndicators)) {
|
||||||
|
return 'joomla';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Dolibarr — has mod*.class.php or dolibarr module descriptor
|
||||||
|
$doliIndicators = array_merge(
|
||||||
|
glob("{$root}/core/modules/mod*.class.php") ?: [],
|
||||||
|
glob("{$root}/class/*.class.php") ?: [],
|
||||||
|
);
|
||||||
|
if (!empty($doliIndicators) && file_exists("{$root}/langs")) {
|
||||||
|
return 'dolibarr';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Go — has go.mod
|
||||||
|
if (file_exists("{$root}/go.mod")) {
|
||||||
|
return 'go';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. MCP — has package.json with mcp-related content or dist/index.js pattern
|
||||||
|
if (file_exists("{$root}/package.json")) {
|
||||||
|
$pkg = json_decode(file_get_contents("{$root}/package.json"), true);
|
||||||
|
$name = $pkg['name'] ?? '';
|
||||||
|
if (str_contains($name, 'mcp') || isset($pkg['dependencies']['@modelcontextprotocol/sdk'])) {
|
||||||
|
return 'mcp';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Platform — is mokocli itself or org-config
|
||||||
|
$repoName = basename($root);
|
||||||
|
if (in_array($repoName, ['mokocli', 'mokogitea-org-config'])) {
|
||||||
|
return 'platform';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Default
|
||||||
|
return 'generic';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isFlagSet(string $flag): bool
|
||||||
|
{
|
||||||
|
$value = $this->getArgument($flag);
|
||||||
|
|
||||||
|
return $value === 'true' || $value === '1' || $value === 'yes';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function apiRequest(
|
||||||
|
string $giteaUrl,
|
||||||
|
string $token,
|
||||||
|
string $method,
|
||||||
|
string $endpoint,
|
||||||
|
?string $body = null
|
||||||
|
): array {
|
||||||
|
$url = $giteaUrl . $endpoint;
|
||||||
|
|
||||||
|
$ch = curl_init();
|
||||||
|
curl_setopt($ch, CURLOPT_URL, $url);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||||
|
'Content-Type: application/json',
|
||||||
|
'Accept: application/json',
|
||||||
|
"Authorization: token {$token}",
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($body !== null) {
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
|
||||||
|
}
|
||||||
|
|
||||||
|
$responseBody = curl_exec($ch);
|
||||||
|
$httpCode = (int) curl_getinfo(
|
||||||
|
$ch,
|
||||||
|
CURLINFO_HTTP_CODE
|
||||||
|
);
|
||||||
|
|
||||||
|
if (curl_errno($ch)) {
|
||||||
|
$error = curl_error($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'code' => 0,
|
||||||
|
'body' => "cURL error: {$error}",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
return ['code' => $httpCode, 'body' => $responseBody];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$app = new PlatformDetectCli();
|
$app = new PlatformDetectCli();
|
||||||
|
|||||||
+6
-6
@@ -6,24 +6,24 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: moko-platform.CLI
|
* DEFGROUP: mokocli.CLI
|
||||||
* INGROUP: moko-platform
|
* INGROUP: mokocli
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /cli/release.php
|
* PATH: /cli/release.php
|
||||||
* BRIEF: Automate the moko-platform version branch release flow
|
* BRIEF: Automate the mokocli version branch release flow
|
||||||
*/
|
*/
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoCli\CliFramework;
|
||||||
|
|
||||||
class ReleaseCli extends CliFramework
|
class ReleaseCli extends CliFramework
|
||||||
{
|
{
|
||||||
protected function configure(): void
|
protected function configure(): void
|
||||||
{
|
{
|
||||||
$this->setDescription('Automate the moko-platform version branch release flow');
|
$this->setDescription('Automate the mokocli version branch release flow');
|
||||||
$this->addArgument('--bump', 'Bump type: patch, minor, or major', '');
|
$this->addArgument('--bump', 'Bump type: patch, minor, or major', '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: moko-platform.CLI
|
* DEFGROUP: mokocli.CLI
|
||||||
* INGROUP: moko-platform
|
* INGROUP: mokocli
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /cli/release_body_update.php
|
* PATH: /cli/release_body_update.php
|
||||||
* BRIEF: Update Gitea release body with changelog extract and checksums
|
* BRIEF: Update Gitea release body with changelog extract and checksums
|
||||||
*/
|
*/
|
||||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoCli\CliFramework;
|
||||||
|
|
||||||
class ReleaseBodyUpdateCli extends CliFramework
|
class ReleaseBodyUpdateCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
+313
-8
@@ -6,32 +6,337 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: moko-platform.CLI
|
* DEFGROUP: mokocli.CLI
|
||||||
* INGROUP: moko-platform
|
* INGROUP: mokocli
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /cli/release_cascade.php
|
* PATH: /cli/release_cascade.php
|
||||||
* VERSION: 09.23.00
|
* VERSION: 09.38.01
|
||||||
* BRIEF: DEPRECATED — cascade behavior removed. Each release stream is independent.
|
* BRIEF: Cascade release zip to all lower stability channels
|
||||||
*/
|
*/
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoCli\CliFramework;
|
||||||
|
|
||||||
class ReleaseCascadeCli extends CliFramework
|
class ReleaseCascadeCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
/** Channel hierarchy: highest stability first. */
|
||||||
|
private const CHANNELS = ['stable', 'release-candidate', 'beta', 'alpha', 'development'];
|
||||||
|
|
||||||
|
/** Map stability input names to canonical tag names. */
|
||||||
|
private const TAG_MAP = [
|
||||||
|
'stable' => 'stable',
|
||||||
|
'release-candidate' => 'release-candidate',
|
||||||
|
'rc' => 'release-candidate',
|
||||||
|
'beta' => 'beta',
|
||||||
|
'alpha' => 'alpha',
|
||||||
|
'development' => 'development',
|
||||||
|
'dev' => 'development',
|
||||||
|
];
|
||||||
|
|
||||||
protected function configure(): void
|
protected function configure(): void
|
||||||
{
|
{
|
||||||
$this->setDescription('DEPRECATED — cascade behavior removed');
|
$this->setDescription('Cascade release zip to all lower stability channels');
|
||||||
|
$this->addArgument('--stability', 'Source stability channel (required)', '');
|
||||||
|
$this->addArgument('--token', 'Gitea API token (required)', '');
|
||||||
|
$this->addArgument('--api-base', 'Gitea API base URL for the repo (required)', '');
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function run(): int
|
protected function run(): int
|
||||||
{
|
{
|
||||||
$this->log('INFO', 'No-op (cascade behavior removed — each stream is independent)');
|
$stability = strtolower($this->getArgument('--stability'));
|
||||||
|
$token = $this->getArgument('--token');
|
||||||
|
$apiBase = rtrim($this->getArgument('--api-base'), '/');
|
||||||
|
|
||||||
|
if ($token === '') {
|
||||||
|
$envToken = getenv('MOKOGITEA_TOKEN');
|
||||||
|
if ($envToken === false || $envToken === '') {
|
||||||
|
$envToken = getenv('GITEA_TOKEN');
|
||||||
|
}
|
||||||
|
if ($envToken !== false && $envToken !== '') {
|
||||||
|
$token = $envToken;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($stability === '' || $token === '' || $apiBase === '') {
|
||||||
|
$this->log('ERROR', 'Usage: release_cascade.php --stability CHANNEL --token TOKEN --api-base URL');
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sourceTag = self::TAG_MAP[$stability] ?? null;
|
||||||
|
if ($sourceTag === null) {
|
||||||
|
$this->log('ERROR', "Unknown stability: {$stability}");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find lower channels to cascade to
|
||||||
|
$lowerChannels = $this->getLowerChannels($sourceTag);
|
||||||
|
if (count($lowerChannels) === 0) {
|
||||||
|
$this->log('INFO', "No lower channels for '{$stability}' — nothing to cascade.");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->log('INFO', "Cascading from '{$sourceTag}' to: " . implode(', ', $lowerChannels));
|
||||||
|
|
||||||
|
if ($this->dryRun) {
|
||||||
|
$this->log('INFO', '[DRY RUN] No changes will be made.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Get source release
|
||||||
|
$sourceRelease = $this->giteaApi("{$apiBase}/releases/tags/{$sourceTag}", $token);
|
||||||
|
if ($sourceRelease === null) {
|
||||||
|
$this->log('WARN', "No release found at tag '{$sourceTag}' — nothing to cascade.");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sourceVersion = $sourceRelease['name'] ?? $sourceTag;
|
||||||
|
$sourceBody = $sourceRelease['body'] ?? '';
|
||||||
|
$sourceAssets = $sourceRelease['assets'] ?? [];
|
||||||
|
|
||||||
|
// Find zip assets (exclude .sha256 sidecars)
|
||||||
|
$zipAssets = array_filter($sourceAssets, function (array $asset): bool {
|
||||||
|
$name = strtolower($asset['name'] ?? '');
|
||||||
|
return str_ends_with($name, '.zip') && !str_ends_with($name, '.sha256');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also grab sha256 sidecars
|
||||||
|
$sha256Assets = array_filter($sourceAssets, function (array $asset): bool {
|
||||||
|
return str_ends_with(strtolower($asset['name'] ?? ''), '.zip.sha256');
|
||||||
|
});
|
||||||
|
|
||||||
|
if (count($zipAssets) === 0) {
|
||||||
|
$this->log('WARN', "Source release '{$sourceTag}' has no zip assets — nothing to cascade.");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log('INFO', "Source: {$sourceVersion} — " . count($zipAssets) . " zip(s)");
|
||||||
|
echo "\n";
|
||||||
|
|
||||||
|
// 2. Download source assets to temp files
|
||||||
|
$downloads = [];
|
||||||
|
foreach (array_merge($zipAssets, $sha256Assets) as $asset) {
|
||||||
|
$url = $asset['browser_download_url'] ?? '';
|
||||||
|
if ($url === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$tmpFile = tempnam(sys_get_temp_dir(), 'cascade_');
|
||||||
|
if ($this->downloadFile($url, $token, $tmpFile)) {
|
||||||
|
$downloads[] = ['name' => $asset['name'], 'path' => $tmpFile];
|
||||||
|
$this->log('INFO', "Downloaded: {$asset['name']}");
|
||||||
|
} else {
|
||||||
|
$this->log('ERROR', "Failed to download: {$asset['name']}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($downloads) === 0) {
|
||||||
|
$this->log('ERROR', 'Could not download any source assets.');
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Cascade to each lower channel
|
||||||
|
$errors = 0;
|
||||||
|
foreach ($lowerChannels as $targetTag) {
|
||||||
|
echo "\n";
|
||||||
|
$result = $this->cascadeToChannel(
|
||||||
|
$apiBase, $token, $targetTag,
|
||||||
|
$sourceVersion, $sourceBody, $downloads
|
||||||
|
);
|
||||||
|
if (!$result) {
|
||||||
|
$errors++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Cleanup temp files
|
||||||
|
foreach ($downloads as $dl) {
|
||||||
|
@unlink($dl['path']);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n";
|
||||||
|
$this->log('INFO', "Cascade complete. " . (count($lowerChannels) - $errors)
|
||||||
|
. "/" . count($lowerChannels) . " channels updated.");
|
||||||
|
|
||||||
|
return $errors > 0 ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cascade assets to a single target channel.
|
||||||
|
*/
|
||||||
|
private function cascadeToChannel(
|
||||||
|
string $apiBase,
|
||||||
|
string $token,
|
||||||
|
string $targetTag,
|
||||||
|
string $sourceVersion,
|
||||||
|
string $sourceBody,
|
||||||
|
array $downloads
|
||||||
|
): bool {
|
||||||
|
$this->log('INFO', "→ {$targetTag}");
|
||||||
|
|
||||||
|
if ($this->dryRun) {
|
||||||
|
$this->log('INFO', " [DRY RUN] Would cascade to {$targetTag}");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find existing release at target tag
|
||||||
|
$existing = $this->giteaApi("{$apiBase}/releases/tags/{$targetTag}", $token);
|
||||||
|
|
||||||
|
if ($existing !== null && !empty($existing['id'])) {
|
||||||
|
$releaseId = (int) $existing['id'];
|
||||||
|
|
||||||
|
// Delete existing assets
|
||||||
|
$existingAssets = $existing['assets'] ?? [];
|
||||||
|
foreach ($existingAssets as $asset) {
|
||||||
|
$assetId = $asset['id'] ?? 0;
|
||||||
|
if ($assetId > 0) {
|
||||||
|
$this->giteaApi(
|
||||||
|
"{$apiBase}/releases/{$releaseId}/assets/{$assetId}",
|
||||||
|
$token, 'DELETE'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update release metadata
|
||||||
|
$updatePayload = json_encode([
|
||||||
|
'name' => $sourceVersion,
|
||||||
|
'body' => $sourceBody,
|
||||||
|
]);
|
||||||
|
$this->giteaApi(
|
||||||
|
"{$apiBase}/releases/{$releaseId}",
|
||||||
|
$token, 'PATCH', $updatePayload
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->log('INFO', " Updated release metadata (id: {$releaseId})");
|
||||||
|
} else {
|
||||||
|
// Create new release at target tag
|
||||||
|
// Use the source release's target commitish so the tag points to the same commit
|
||||||
|
$createPayload = json_encode([
|
||||||
|
'tag_name' => $targetTag,
|
||||||
|
'target_commitish' => 'main',
|
||||||
|
'name' => $sourceVersion,
|
||||||
|
'body' => $sourceBody,
|
||||||
|
'prerelease' => ($targetTag !== 'stable'),
|
||||||
|
]);
|
||||||
|
$newRelease = $this->giteaApi("{$apiBase}/releases", $token, 'POST', $createPayload);
|
||||||
|
if ($newRelease === null || empty($newRelease['id'])) {
|
||||||
|
$this->log('ERROR', " Failed to create release at tag '{$targetTag}'");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$releaseId = (int) $newRelease['id'];
|
||||||
|
$this->log('INFO', " Created release (id: {$releaseId})");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload assets
|
||||||
|
foreach ($downloads as $dl) {
|
||||||
|
$uploadUrl = "{$apiBase}/releases/{$releaseId}/assets?name=" . rawurlencode($dl['name']);
|
||||||
|
$success = $this->uploadAsset($uploadUrl, $token, $dl['path'], $dl['name']);
|
||||||
|
if ($success) {
|
||||||
|
$this->log('INFO', " Uploaded: {$dl['name']}");
|
||||||
|
} else {
|
||||||
|
$this->log('ERROR', " Failed to upload: {$dl['name']}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all channels below the given source channel.
|
||||||
|
*/
|
||||||
|
private function getLowerChannels(string $sourceTag): array
|
||||||
|
{
|
||||||
|
$idx = array_search($sourceTag, self::CHANNELS, true);
|
||||||
|
if ($idx === false) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return array_slice(self::CHANNELS, $idx + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download a file via HTTP.
|
||||||
|
*/
|
||||||
|
private function downloadFile(string $url, string $token, string $destPath): bool
|
||||||
|
{
|
||||||
|
$ch = curl_init($url);
|
||||||
|
if ($ch === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$fp = fopen($destPath, 'wb');
|
||||||
|
if ($fp === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
curl_setopt_array($ch, [
|
||||||
|
CURLOPT_FOLLOWLOCATION => true,
|
||||||
|
CURLOPT_FILE => $fp,
|
||||||
|
CURLOPT_HTTPHEADER => ["Authorization: token {$token}"],
|
||||||
|
CURLOPT_TIMEOUT => 120,
|
||||||
|
]);
|
||||||
|
curl_exec($ch);
|
||||||
|
$code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
fclose($fp);
|
||||||
|
return $code >= 200 && $code < 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload a file as a release asset via multipart form.
|
||||||
|
*/
|
||||||
|
private function uploadAsset(string $url, string $token, string $filePath, string $fileName): bool
|
||||||
|
{
|
||||||
|
$ch = curl_init($url);
|
||||||
|
if ($ch === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$cfile = new CURLFile($filePath, 'application/octet-stream', $fileName);
|
||||||
|
curl_setopt_array($ch, [
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_POST => true,
|
||||||
|
CURLOPT_POSTFIELDS => ['attachment' => $cfile],
|
||||||
|
CURLOPT_HTTPHEADER => ["Authorization: token {$token}"],
|
||||||
|
CURLOPT_TIMEOUT => 120,
|
||||||
|
]);
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
return $code >= 200 && $code < 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make an HTTP request to the Gitea API.
|
||||||
|
*/
|
||||||
|
private function giteaApi(
|
||||||
|
string $url,
|
||||||
|
string $token,
|
||||||
|
string $method = 'GET',
|
||||||
|
?string $body = null
|
||||||
|
): ?array {
|
||||||
|
$ch = curl_init($url);
|
||||||
|
if ($ch === false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
curl_setopt_array($ch, [
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_HTTPHEADER => [
|
||||||
|
"Authorization: token {$token}",
|
||||||
|
'Content-Type: application/json',
|
||||||
|
],
|
||||||
|
CURLOPT_TIMEOUT => 30,
|
||||||
|
CURLOPT_CUSTOMREQUEST => $method,
|
||||||
|
]);
|
||||||
|
if ($body !== null) {
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
|
||||||
|
}
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($httpCode < 200 || $httpCode >= 300 || empty($response) || !is_string($response)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$decoded = json_decode($response, true);
|
||||||
|
return is_array($decoded) ? $decoded : null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$app = new ReleaseCascadeCli();
|
$app = new ReleaseCascadeCli();
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: moko-platform.CLI
|
* DEFGROUP: mokocli.CLI
|
||||||
* INGROUP: moko-platform
|
* INGROUP: mokocli
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /cli/release_create.php
|
* PATH: /cli/release_create.php
|
||||||
* BRIEF: Create or overwrite a Gitea release with proper naming
|
* BRIEF: Create or overwrite a Gitea release with proper naming
|
||||||
*/
|
*/
|
||||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoCli\{CliFramework, SourceResolver};
|
||||||
|
|
||||||
class ReleaseCreateCli extends CliFramework
|
class ReleaseCreateCli extends CliFramework
|
||||||
{
|
{
|
||||||
@@ -97,8 +97,8 @@ class ReleaseCreateCli extends CliFramework
|
|||||||
// Find extension manifest (Joomla XML)
|
// Find extension manifest (Joomla XML)
|
||||||
$extManifest = null;
|
$extManifest = null;
|
||||||
$manifestFiles = array_merge(
|
$manifestFiles = array_merge(
|
||||||
glob("{$root}/src/pkg_*.xml") ?: [],
|
SourceResolver::globSource($root, 'pkg_*.xml'),
|
||||||
glob("{$root}/src/*.xml") ?: [],
|
SourceResolver::globSource($root, '*.xml'),
|
||||||
glob("{$root}/*.xml") ?: []
|
glob("{$root}/*.xml") ?: []
|
||||||
);
|
);
|
||||||
foreach ($manifestFiles as $file) {
|
foreach ($manifestFiles as $file) {
|
||||||
@@ -112,8 +112,7 @@ class ReleaseCreateCli extends CliFramework
|
|||||||
// Find Dolibarr module file
|
// Find Dolibarr module file
|
||||||
$modFile = null;
|
$modFile = null;
|
||||||
$modFiles = array_merge(
|
$modFiles = array_merge(
|
||||||
glob("{$root}/src/core/modules/mod*.class.php") ?: [],
|
SourceResolver::globSource($root, 'core/modules/mod*.class.php'),
|
||||||
glob("{$root}/htdocs/core/modules/mod*.class.php") ?: [],
|
|
||||||
glob("{$root}/core/modules/mod*.class.php") ?: []
|
glob("{$root}/core/modules/mod*.class.php") ?: []
|
||||||
);
|
);
|
||||||
foreach ($modFiles as $file) {
|
foreach ($modFiles as $file) {
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: moko-platform.CLI
|
* DEFGROUP: mokocli.CLI
|
||||||
* INGROUP: moko-platform
|
* INGROUP: mokocli
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /cli/release_manage.php
|
* PATH: /cli/release_manage.php
|
||||||
* BRIEF: Create/update Gitea releases, upload assets, update release body
|
* BRIEF: Create/update Gitea releases, upload assets, update release body
|
||||||
*/
|
*/
|
||||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoCli\CliFramework;
|
||||||
|
|
||||||
class ReleaseManageCli extends CliFramework
|
class ReleaseManageCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: moko-platform.CLI
|
* DEFGROUP: mokocli.CLI
|
||||||
* INGROUP: moko-platform
|
* INGROUP: mokocli
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /cli/release_mirror.php
|
* PATH: /cli/release_mirror.php
|
||||||
* BRIEF: Mirror a Gitea release (with assets) to a GitHub repository
|
* BRIEF: Mirror a Gitea release (with assets) to a GitHub repository
|
||||||
*/
|
*/
|
||||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoCli\CliFramework;
|
||||||
|
|
||||||
class ReleaseMirrorCli extends CliFramework
|
class ReleaseMirrorCli extends CliFramework
|
||||||
{
|
{
|
||||||
@@ -201,7 +201,7 @@ class ReleaseMirrorCli extends CliFramework
|
|||||||
CURLOPT_HTTPHEADER => [
|
CURLOPT_HTTPHEADER => [
|
||||||
"Authorization: token {$token}",
|
"Authorization: token {$token}",
|
||||||
'Accept: application/vnd.github+json',
|
'Accept: application/vnd.github+json',
|
||||||
'User-Agent: moko-platform',
|
'User-Agent: mokocli',
|
||||||
'Content-Type: application/json',
|
'Content-Type: application/json',
|
||||||
],
|
],
|
||||||
CURLOPT_TIMEOUT => 30,
|
CURLOPT_TIMEOUT => 30,
|
||||||
@@ -229,7 +229,7 @@ class ReleaseMirrorCli extends CliFramework
|
|||||||
CURLOPT_HTTPHEADER => [
|
CURLOPT_HTTPHEADER => [
|
||||||
"Authorization: token {$token}",
|
"Authorization: token {$token}",
|
||||||
'Accept: application/vnd.github+json',
|
'Accept: application/vnd.github+json',
|
||||||
'User-Agent: moko-platform',
|
'User-Agent: mokocli',
|
||||||
'Content-Type: application/octet-stream',
|
'Content-Type: application/octet-stream',
|
||||||
],
|
],
|
||||||
CURLOPT_POSTFIELDS => file_get_contents($filePath),
|
CURLOPT_POSTFIELDS => file_get_contents($filePath),
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: moko-platform.CLI
|
* DEFGROUP: mokocli.CLI
|
||||||
* INGROUP: moko-platform
|
* INGROUP: mokocli
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /cli/release_notes.php
|
* PATH: /cli/release_notes.php
|
||||||
* BRIEF: Extract release notes from CHANGELOG.md for a given version
|
* BRIEF: Extract release notes from CHANGELOG.md for a given version
|
||||||
*/
|
*/
|
||||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoCli\CliFramework;
|
||||||
|
|
||||||
class ReleaseNotesCli extends CliFramework
|
class ReleaseNotesCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
+52
-12
@@ -6,9 +6,9 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: moko-platform.CLI
|
* DEFGROUP: mokocli.CLI
|
||||||
* INGROUP: moko-platform
|
* INGROUP: mokocli
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /cli/release_package.php
|
* PATH: /cli/release_package.php
|
||||||
* BRIEF: Build packages (ZIP + tar.gz) with SHA-256 and upload to Gitea release
|
* BRIEF: Build packages (ZIP + tar.gz) with SHA-256 and upload to Gitea release
|
||||||
*/
|
*/
|
||||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoCli\{CliFramework, SourceResolver};
|
||||||
|
|
||||||
class ReleasePackageCli extends CliFramework
|
class ReleasePackageCli extends CliFramework
|
||||||
{
|
{
|
||||||
@@ -99,9 +99,10 @@ class ReleasePackageCli extends CliFramework
|
|||||||
$extFolder = '';
|
$extFolder = '';
|
||||||
$typePrefix = '';
|
$typePrefix = '';
|
||||||
|
|
||||||
|
SourceResolver::warnIfLegacy($root);
|
||||||
$manifestFiles = array_merge(
|
$manifestFiles = array_merge(
|
||||||
glob("{$root}/src/pkg_*.xml") ?: [],
|
SourceResolver::globSource($root, 'pkg_*.xml'),
|
||||||
glob("{$root}/src/*.xml") ?: [],
|
SourceResolver::globSource($root, '*.xml'),
|
||||||
glob("{$root}/*.xml") ?: []
|
glob("{$root}/*.xml") ?: []
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -200,14 +201,12 @@ class ReleasePackageCli extends CliFramework
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($sourceDir === null && is_dir("{$root}/src")) {
|
if ($sourceDir === null) {
|
||||||
$sourceDir = "{$root}/src";
|
$sourceDir = SourceResolver::resolveAbsolute($root);
|
||||||
} elseif ($sourceDir === null && is_dir("{$root}/htdocs")) {
|
|
||||||
$sourceDir = "{$root}/htdocs";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($sourceDir === null) {
|
if ($sourceDir === null) {
|
||||||
echo "No src/ or htdocs/ directory found — skipping package build\n";
|
echo "No source/ or src/ directory found — skipping package build\n";
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,17 +224,58 @@ class ReleasePackageCli extends CliFramework
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only zip sub-extensions listed in the package manifest
|
||||||
|
$manifestedSubs = [];
|
||||||
|
$pkgManifestFiles = glob("{$sourceDir}/pkg_*.xml") ?: [];
|
||||||
|
foreach ($pkgManifestFiles as $pmf) {
|
||||||
|
$pmXml = @simplexml_load_file($pmf);
|
||||||
|
if ($pmXml && isset($pmXml->files)) {
|
||||||
|
foreach ($pmXml->files->file as $fileNode) {
|
||||||
|
$zipName = pathinfo((string) $fileNode, PATHINFO_FILENAME);
|
||||||
|
if (!empty($zipName)) {
|
||||||
|
$manifestedSubs[$zipName] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$packageDirs = glob("{$sourceDir}/packages/*", GLOB_ONLYDIR) ?: [];
|
$packageDirs = glob("{$sourceDir}/packages/*", GLOB_ONLYDIR) ?: [];
|
||||||
foreach ($packageDirs as $pkgDir) {
|
foreach ($packageDirs as $pkgDir) {
|
||||||
$subName = basename($pkgDir);
|
$subName = basename($pkgDir);
|
||||||
|
|
||||||
|
// Skip directories not listed in the package manifest
|
||||||
|
if (!empty($manifestedSubs) && !isset($manifestedSubs[$subName])) {
|
||||||
|
echo " Skipping {$subName} (not in package manifest)\n";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
$subZipPath = "{$outputDir}/{$subName}.zip";
|
$subZipPath = "{$outputDir}/{$subName}.zip";
|
||||||
|
|
||||||
|
// If sub-package is a full repo checkout (e.g. git submodule),
|
||||||
|
// look for a source/ or src/ subdirectory containing a Joomla manifest XML
|
||||||
|
// and zip that instead of the repo root.
|
||||||
|
$subSourceDir = $pkgDir;
|
||||||
|
$subSrcAbs = SourceResolver::resolveAbsolute($pkgDir);
|
||||||
|
if ($subSrcAbs !== null) {
|
||||||
|
$srcManifests = array_merge(
|
||||||
|
glob("{$subSrcAbs}/*.xml") ?: [],
|
||||||
|
glob("{$subSrcAbs}/pkg_*.xml") ?: []
|
||||||
|
);
|
||||||
|
foreach ($srcManifests as $mf) {
|
||||||
|
if (strpos(file_get_contents($mf) ?: '', '<extension') !== false) {
|
||||||
|
$subSourceDir = $subSrcAbs;
|
||||||
|
$subSrcName = SourceResolver::resolve($pkgDir);
|
||||||
|
echo " Sub-package {$subName}: using {$subSrcName}/ entry-point\n";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$subZip = new \ZipArchive();
|
$subZip = new \ZipArchive();
|
||||||
if ($subZip->open($subZipPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) {
|
if ($subZip->open($subZipPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) {
|
||||||
$this->log('ERROR', "Failed to create sub-package ZIP: {$subZipPath}");
|
$this->log('ERROR', "Failed to create sub-package ZIP: {$subZipPath}");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$this->addDirToZip($subZip, $pkgDir, '', $this->excludePatterns);
|
$this->addDirToZip($subZip, $subSourceDir, '', $this->excludePatterns);
|
||||||
$subZip->close();
|
$subZip->close();
|
||||||
|
|
||||||
$zip->addFile($subZipPath, "packages/{$subName}.zip");
|
$zip->addFile($subZipPath, "packages/{$subName}.zip");
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: moko-platform.CLI
|
* DEFGROUP: mokocli.CLI
|
||||||
* INGROUP: moko-platform
|
* INGROUP: mokocli
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /cli/release_promote.php
|
* PATH: /cli/release_promote.php
|
||||||
* BRIEF: Promote a Gitea release from one channel to another (rename release, tag, assets)
|
* BRIEF: Promote a Gitea release from one channel to another (rename release, tag, assets)
|
||||||
*/
|
*/
|
||||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoCli\{CliFramework, SourceResolver};
|
||||||
|
|
||||||
class ReleasePromoteCli extends CliFramework
|
class ReleasePromoteCli extends CliFramework
|
||||||
{
|
{
|
||||||
@@ -109,8 +109,8 @@ class ReleasePromoteCli extends CliFramework
|
|||||||
if ($to === 'stable') {
|
if ($to === 'stable') {
|
||||||
$root = realpath($path) ?: $path;
|
$root = realpath($path) ?: $path;
|
||||||
$manifestFiles = array_merge(
|
$manifestFiles = array_merge(
|
||||||
glob("{$root}/src/pkg_*.xml") ?: [],
|
SourceResolver::globSource($root, 'pkg_*.xml'),
|
||||||
glob("{$root}/src/*.xml") ?: [],
|
SourceResolver::globSource($root, '*.xml'),
|
||||||
glob("{$root}/*.xml") ?: []
|
glob("{$root}/*.xml") ?: []
|
||||||
);
|
);
|
||||||
foreach ($manifestFiles as $xmlFile) {
|
foreach ($manifestFiles as $xmlFile) {
|
||||||
|
|||||||
@@ -6,11 +6,11 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: moko-platform.CLI
|
* DEFGROUP: mokocli.CLI
|
||||||
* INGROUP: moko-platform
|
* INGROUP: mokocli
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /cli/release_publish.php
|
* PATH: /cli/release_publish.php
|
||||||
* VERSION: 09.23.00
|
* VERSION: 09.38.01
|
||||||
* BRIEF: Publish a release and create copies for all lesser stability streams.
|
* BRIEF: Publish a release and create copies for all lesser stability streams.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoCli\CliFramework;
|
||||||
|
|
||||||
class ReleasePublishCli extends CliFramework
|
class ReleasePublishCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: moko-platform.CLI
|
* DEFGROUP: mokocli.CLI
|
||||||
* INGROUP: moko-platform
|
* INGROUP: mokocli
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /cli/release_validate.php
|
* PATH: /cli/release_validate.php
|
||||||
* BRIEF: Pre-release validation -- version consistency, required files, manifest checks
|
* BRIEF: Pre-release validation -- version consistency, required files, manifest checks
|
||||||
*/
|
*/
|
||||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoCli\{CliFramework, SourceResolver};
|
||||||
|
|
||||||
class ReleaseValidateCli extends CliFramework
|
class ReleaseValidateCli extends CliFramework
|
||||||
{
|
{
|
||||||
@@ -66,8 +66,10 @@ class ReleaseValidateCli extends CliFramework
|
|||||||
$platform = 'generic';
|
$platform = 'generic';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$hasSource = is_dir("{$root}/src") || is_dir("{$root}/htdocs");
|
$hasSource = SourceResolver::resolveAbsolute($root) !== null;
|
||||||
$this->addVResult('Source directory', $hasSource ? 'PASS' : 'WARN', $hasSource ? 'src/ or htdocs/ found' : 'No src/ or htdocs/ directory');
|
SourceResolver::warnIfLegacy($root);
|
||||||
|
$srcDirName = SourceResolver::resolve($root);
|
||||||
|
$this->addVResult('Source directory', $hasSource ? 'PASS' : 'WARN', $hasSource ? "{$srcDirName}/ found" : 'No source/ or src/ directory');
|
||||||
if (!file_exists("{$root}/README.md")) {
|
if (!file_exists("{$root}/README.md")) {
|
||||||
$this->addVResult('README.md', 'FAIL', 'Not found');
|
$this->addVResult('README.md', 'FAIL', 'Not found');
|
||||||
} else {
|
} else {
|
||||||
@@ -109,7 +111,8 @@ class ReleaseValidateCli extends CliFramework
|
|||||||
$this->addVResult('LICENSE', $licenseFound ? 'PASS' : 'FAIL', $licenseFound ? 'Found' : 'Not found');
|
$this->addVResult('LICENSE', $licenseFound ? 'PASS' : 'FAIL', $licenseFound ? 'Found' : 'Not found');
|
||||||
if ($platform === 'joomla') {
|
if ($platform === 'joomla') {
|
||||||
$manifest = null;
|
$manifest = null;
|
||||||
foreach (["{$root}/src", $root] as $dir) {
|
$srcAbs = SourceResolver::resolveAbsolute($root);
|
||||||
|
foreach (array_filter([$srcAbs, $root]) as $dir) {
|
||||||
if (!is_dir($dir)) {
|
if (!is_dir($dir)) {
|
||||||
continue;
|
continue;
|
||||||
} foreach (glob("{$dir}/*.xml") as $xmlFile) {
|
} foreach (glob("{$dir}/*.xml") as $xmlFile) {
|
||||||
@@ -156,7 +159,7 @@ class ReleaseValidateCli extends CliFramework
|
|||||||
}
|
}
|
||||||
} elseif ($platform === 'dolibarr') {
|
} elseif ($platform === 'dolibarr') {
|
||||||
$modFile = null;
|
$modFile = null;
|
||||||
foreach (['src', 'htdocs'] as $sd) {
|
foreach (SourceResolver::getCandidates() as $sd) {
|
||||||
$matches = glob("{$root}/{$sd}/mod*.class.php");
|
$matches = glob("{$root}/{$sd}/mod*.class.php");
|
||||||
if (!empty($matches)) {
|
if (!empty($matches)) {
|
||||||
$modFile = $matches[0];
|
$modFile = $matches[0];
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: moko-platform.CLI
|
* DEFGROUP: mokocli.CLI
|
||||||
* INGROUP: moko-platform
|
* INGROUP: mokocli
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /cli/release_verify.php
|
* PATH: /cli/release_verify.php
|
||||||
* BRIEF: Verify a built release artifact — version, SHA256, disallowed files
|
* BRIEF: Verify a built release artifact — version, SHA256, disallowed files
|
||||||
*/
|
*/
|
||||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoCli\CliFramework;
|
||||||
|
|
||||||
class ReleaseVerifyCli extends CliFramework
|
class ReleaseVerifyCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,429 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
* FILE INFORMATION
|
||||||
|
* DEFGROUP: mokocli.CLI
|
||||||
|
* INGROUP: mokocli
|
||||||
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
|
* PATH: /cli/repo_wizard.php
|
||||||
|
* BRIEF: Interactive configuration wizard for new repositories
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
|
use MokoCli\{CliFramework, Config, PlatformAdapterFactory};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interactive repo setup wizard.
|
||||||
|
*
|
||||||
|
* Walks through platform selection, generates config files, workflows,
|
||||||
|
* and optionally creates the repo on Gitea via API.
|
||||||
|
*
|
||||||
|
* @see https://git.mokoconsulting.tech/MokoConsulting/mokocli/issues/145
|
||||||
|
*/
|
||||||
|
class RepoWizard extends CliFramework
|
||||||
|
{
|
||||||
|
private const PLATFORMS = [
|
||||||
|
'joomla' => 'Joomla extension (component, module, plugin, package)',
|
||||||
|
'dolibarr' => 'Dolibarr ERP module',
|
||||||
|
'nodejs' => 'Node.js / TypeScript project',
|
||||||
|
'python' => 'Python project',
|
||||||
|
'mcp-server' => 'MCP server (Model Context Protocol)',
|
||||||
|
'generic' => 'Generic PHP or multi-language project',
|
||||||
|
];
|
||||||
|
|
||||||
|
private const LICENSES = [
|
||||||
|
'GPL-3.0-or-later' => 'GNU General Public License v3',
|
||||||
|
'MIT' => 'MIT License',
|
||||||
|
'Apache-2.0' => 'Apache License 2.0',
|
||||||
|
'proprietary' => 'Proprietary / All rights reserved',
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Collected wizard answers. */
|
||||||
|
private array $answers = [];
|
||||||
|
|
||||||
|
/** When true, skip all interactive prompts and use defaults. */
|
||||||
|
private bool $nonInteractive = false;
|
||||||
|
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this->setDescription('Interactive configuration wizard for new repositories');
|
||||||
|
$this->addArgument('--path', 'Directory to generate files in', '.');
|
||||||
|
$this->addArgument('--create-remote', 'Create repo on Gitea via API', false);
|
||||||
|
$this->addArgument('--non-interactive', 'Use defaults (no prompts)', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function run(): int
|
||||||
|
{
|
||||||
|
$rawPath = $this->getArgument('--path', '.');
|
||||||
|
$targetPath = realpath($rawPath) ?: $rawPath;
|
||||||
|
$this->nonInteractive = (bool) $this->getArgument('--non-interactive', false);
|
||||||
|
|
||||||
|
// Validate target path
|
||||||
|
if (!is_dir($targetPath) && !@mkdir($targetPath, 0755, true)) {
|
||||||
|
$this->log('ERROR', "Target path does not exist and cannot be created: {$targetPath}");
|
||||||
|
return self::EXIT_USAGE;
|
||||||
|
}
|
||||||
|
$targetPath = realpath($targetPath) ?: $targetPath;
|
||||||
|
|
||||||
|
$this->section('MokoCli Repository Wizard');
|
||||||
|
|
||||||
|
// ── Gather info ──────────────────────────────────────────────
|
||||||
|
$this->answers['name'] = $this->ask('Repository name', basename($targetPath));
|
||||||
|
$this->answers['platform'] = $this->choose('Platform type', self::PLATFORMS, 'generic');
|
||||||
|
$this->answers['org'] = $this->ask('Organization', 'MokoConsulting');
|
||||||
|
$this->answers['description'] = $this->ask('Description', '');
|
||||||
|
$this->answers['license'] = $this->choose('License', self::LICENSES, 'GPL-3.0-or-later');
|
||||||
|
|
||||||
|
// ── Confirm ──────────────────────────────────────────────────
|
||||||
|
$this->section('Configuration Summary');
|
||||||
|
foreach ($this->answers as $key => $value) {
|
||||||
|
$this->log('INFO', sprintf(' %-12s %s', $key . ':', $value));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->confirm('Proceed with these settings?', true)) {
|
||||||
|
$this->log('INFO', 'Wizard cancelled');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Generate files ───────────────────────────────────────────
|
||||||
|
$this->section('Generating files');
|
||||||
|
$generated = $this->generateFiles($targetPath);
|
||||||
|
|
||||||
|
foreach ($generated as $file) {
|
||||||
|
$this->status(true, $file);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Create remote repo ───────────────────────────────────────
|
||||||
|
if ($this->getArgument('--create-remote', false)) {
|
||||||
|
$this->section('Creating remote repository');
|
||||||
|
$this->createRemoteRepo();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log('INFO', '');
|
||||||
|
$this->log('INFO', 'Generated ' . count($generated) . " files in {$targetPath}");
|
||||||
|
$this->log('INFO', 'Next: git init && git add -A && git commit -m "chore: initial scaffold"');
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── File generation ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
private function generateFiles(string $path): array
|
||||||
|
{
|
||||||
|
$platform = $this->answers['platform'];
|
||||||
|
$name = $this->answers['name'];
|
||||||
|
$generated = [];
|
||||||
|
|
||||||
|
// .editorconfig
|
||||||
|
$generated[] = $this->writeFile($path, '.editorconfig', $this->editorconfig());
|
||||||
|
|
||||||
|
// README.md
|
||||||
|
$generated[] = $this->writeFile($path, 'README.md', $this->readme());
|
||||||
|
|
||||||
|
// CHANGELOG.md
|
||||||
|
$generated[] = $this->writeFile($path, 'CHANGELOG.md', $this->changelog());
|
||||||
|
|
||||||
|
// LICENSE
|
||||||
|
if ($this->answers['license'] !== 'proprietary') {
|
||||||
|
$generated[] = $this->writeFile($path, 'LICENSE', "See SPDX: {$this->answers['license']}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Platform-specific configs
|
||||||
|
switch ($platform) {
|
||||||
|
case 'joomla':
|
||||||
|
case 'dolibarr':
|
||||||
|
case 'generic':
|
||||||
|
$generated[] = $this->writeFile($path, 'phpcs.xml', $this->phpcsXml());
|
||||||
|
$generated[] = $this->writeFile($path, 'phpstan.neon', $this->phpstanNeon());
|
||||||
|
$generated[] = $this->writeFile($path, 'composer.json', $this->composerJson());
|
||||||
|
break;
|
||||||
|
case 'nodejs':
|
||||||
|
case 'mcp-server':
|
||||||
|
$generated[] = $this->writeFile($path, 'package.json', $this->packageJson());
|
||||||
|
$generated[] = $this->writeFile($path, 'tsconfig.json', $this->tsconfigJson());
|
||||||
|
$generated[] = $this->writeFile($path, '.eslintrc.json', $this->eslintrc());
|
||||||
|
break;
|
||||||
|
case 'python':
|
||||||
|
$generated[] = $this->writeFile($path, 'pyproject.toml', $this->pyprojectToml());
|
||||||
|
$generated[] = $this->writeFile($path, 'requirements.txt', '');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// .mokogitea/workflows
|
||||||
|
$generated[] = $this->writeFile($path, '.mokogitea/workflows/pr-check.yml',
|
||||||
|
"# PR check workflow — synced from mokocli templates\n# Run: moko sync to update\n");
|
||||||
|
|
||||||
|
// .gitignore
|
||||||
|
$generated[] = $this->writeFile($path, '.gitignore', $this->gitignore($platform));
|
||||||
|
|
||||||
|
// Source directory
|
||||||
|
$srcDir = in_array($platform, ['joomla', 'dolibarr', 'generic']) ? 'source' : 'src';
|
||||||
|
if (!is_dir("{$path}/{$srcDir}")) {
|
||||||
|
@mkdir("{$path}/{$srcDir}", 0755, true);
|
||||||
|
$generated[] = "{$srcDir}/";
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_filter($generated);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function writeFile(string $basePath, string $relativePath, string $content): ?string
|
||||||
|
{
|
||||||
|
$fullPath = $basePath . '/' . $relativePath;
|
||||||
|
$dir = dirname($fullPath);
|
||||||
|
|
||||||
|
if (file_exists($fullPath)) {
|
||||||
|
$this->log('DEBUG', " SKIP {$relativePath} (already exists)");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->dryRun) {
|
||||||
|
$this->log('INFO', "[dry-run] Would create {$relativePath}");
|
||||||
|
return $relativePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_dir($dir)) {
|
||||||
|
@mkdir($dir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
file_put_contents($fullPath, $content);
|
||||||
|
return $relativePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Remote repo creation ─────────────────────────────────────────
|
||||||
|
|
||||||
|
private function createRemoteRepo(): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$config = Config::load();
|
||||||
|
$adapter = PlatformAdapterFactory::create($config);
|
||||||
|
$org = $this->answers['org'];
|
||||||
|
|
||||||
|
if ($this->dryRun) {
|
||||||
|
$this->log('INFO', "[dry-run] Would create {$org}/{$this->answers['name']} on Gitea");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $adapter->createRepository($org, $this->answers['name'], [
|
||||||
|
'description' => $this->answers['description'],
|
||||||
|
'private' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$url = $result['html_url'] ?? "{$org}/{$this->answers['name']}";
|
||||||
|
$this->log('INFO', "Created: {$url}");
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->log('ERROR', "Failed to create remote repo: {$e->getMessage()}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Interactive helpers (respect --non-interactive) ─────────────
|
||||||
|
|
||||||
|
private function ask(string $prompt, string $default): string
|
||||||
|
{
|
||||||
|
if ($this->nonInteractive) {
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
return $this->input($prompt, $default);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function choose(string $prompt, array $options, string $default): string
|
||||||
|
{
|
||||||
|
if ($this->nonInteractive) {
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
$keys = array_keys($options);
|
||||||
|
$labels = [];
|
||||||
|
foreach ($options as $key => $desc) {
|
||||||
|
$labels[] = "{$key} — {$desc}";
|
||||||
|
}
|
||||||
|
$chosen = $this->select($prompt, $labels);
|
||||||
|
// Extract the key from "key — description"
|
||||||
|
$chosenKey = explode(' — ', $chosen, 2)[0] ?? $default;
|
||||||
|
return in_array($chosenKey, $keys, true) ? $chosenKey : $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Template content ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
private function editorconfig(): string
|
||||||
|
{
|
||||||
|
return <<<'CONF'
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = tab
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.{yml,yaml}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
CONF;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function readme(): string
|
||||||
|
{
|
||||||
|
$name = $this->answers['name'];
|
||||||
|
$desc = $this->answers['description'] ?: 'A Moko Consulting project.';
|
||||||
|
$license = $this->answers['license'];
|
||||||
|
|
||||||
|
return <<<MD
|
||||||
|
# {$name}
|
||||||
|
|
||||||
|
{$desc}
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
{$license}
|
||||||
|
MD;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function changelog(): string
|
||||||
|
{
|
||||||
|
return <<<MD
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Initial project scaffold
|
||||||
|
MD;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function composerJson(): string
|
||||||
|
{
|
||||||
|
$data = [
|
||||||
|
'name' => 'mokoconsulting/' . strtolower($this->answers['name']),
|
||||||
|
'description' => $this->answers['description'] ?: $this->answers['name'],
|
||||||
|
'type' => 'library',
|
||||||
|
'license' => $this->answers['license'],
|
||||||
|
'require' => ['php' => '>=8.1'],
|
||||||
|
'autoload' => ['psr-4' => new \stdClass()],
|
||||||
|
];
|
||||||
|
return json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
private function phpcsXml(): string
|
||||||
|
{
|
||||||
|
return <<<'XML'
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<ruleset name="MokoCli">
|
||||||
|
<rule ref="PSR12"/>
|
||||||
|
<file>source/</file>
|
||||||
|
<exclude-pattern>vendor/*</exclude-pattern>
|
||||||
|
</ruleset>
|
||||||
|
XML;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function phpstanNeon(): string
|
||||||
|
{
|
||||||
|
return <<<'NEON'
|
||||||
|
parameters:
|
||||||
|
level: 6
|
||||||
|
paths:
|
||||||
|
- source/
|
||||||
|
NEON;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function packageJson(): string
|
||||||
|
{
|
||||||
|
$data = [
|
||||||
|
'name' => '@mokoconsulting/' . strtolower($this->answers['name']),
|
||||||
|
'version' => '0.1.0',
|
||||||
|
'description' => $this->answers['description'] ?: $this->answers['name'],
|
||||||
|
'type' => 'module',
|
||||||
|
'scripts' => ['build' => 'tsc', 'start' => 'node dist/index.js'],
|
||||||
|
'devDependencies' => ['typescript' => '^5.0'],
|
||||||
|
];
|
||||||
|
return json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
private function tsconfigJson(): string
|
||||||
|
{
|
||||||
|
return <<<'JSON'
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"declaration": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"]
|
||||||
|
}
|
||||||
|
JSON;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function eslintrc(): string
|
||||||
|
{
|
||||||
|
return <<<'JSON'
|
||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"plugins": ["@typescript-eslint"],
|
||||||
|
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"]
|
||||||
|
}
|
||||||
|
JSON;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function pyprojectToml(): string
|
||||||
|
{
|
||||||
|
$name = strtolower($this->answers['name']);
|
||||||
|
$desc = str_replace(['\\', '"'], ['\\\\', '\\"'], $this->answers['description'] ?: $this->answers['name']);
|
||||||
|
|
||||||
|
return <<<TOML
|
||||||
|
[project]
|
||||||
|
name = "{$name}"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "{$desc}"
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=68.0"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
TOML;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function gitignore(string $platform): string
|
||||||
|
{
|
||||||
|
$common = <<<'GI'
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
desktop.ini
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
GI;
|
||||||
|
|
||||||
|
$extra = match ($platform) {
|
||||||
|
'joomla', 'dolibarr', 'generic' => "\n# PHP\nvendor/\n.phpunit.result.cache\n",
|
||||||
|
'nodejs', 'mcp-server' => "\n# Node\nnode_modules/\ndist/\n*.tsbuildinfo\n",
|
||||||
|
'python' => "\n# Python\n__pycache__/\n*.pyc\n.venv/\n*.egg-info/\n",
|
||||||
|
default => '',
|
||||||
|
};
|
||||||
|
|
||||||
|
return $common . $extra;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$app = new RepoWizard('repo_wizard');
|
||||||
|
exit($app->execute());
|
||||||
@@ -8,11 +8,11 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: moko-platform.CLI
|
* DEFGROUP: mokocli.CLI
|
||||||
* INGROUP: moko-platform
|
* INGROUP: mokocli
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /cli/scaffold_client.php
|
* PATH: /cli/scaffold_client.php
|
||||||
* VERSION: 09.23.00
|
* VERSION: 09.38.01
|
||||||
* BRIEF: Scaffold a new client-waas repo from Template-Client-WaaS with pre-configured settings
|
* BRIEF: Scaffold a new client-waas repo from Template-Client-WaaS with pre-configured settings
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoCli\CliFramework;
|
||||||
|
|
||||||
class ScaffoldClientCli extends CliFramework
|
class ScaffoldClientCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,9 +8,9 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: moko-platform.CLI
|
* DEFGROUP: mokocli.CLI
|
||||||
* INGROUP: moko-platform
|
* INGROUP: mokocli
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /cli/sync_rulesets.php
|
* PATH: /cli/sync_rulesets.php
|
||||||
* BRIEF: Apply branch protection rules to all repos via platform adapter
|
* BRIEF: Apply branch protection rules to all repos via platform adapter
|
||||||
*/
|
*/
|
||||||
@@ -20,9 +20,9 @@ declare(strict_types=1);
|
|||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoCli\CliFramework;
|
||||||
use MokoEnterprise\Config;
|
use MokoCli\Config;
|
||||||
use MokoEnterprise\PlatformAdapterFactory;
|
use MokoCli\PlatformAdapterFactory;
|
||||||
|
|
||||||
class SyncRulesetsCli extends CliFramework
|
class SyncRulesetsCli extends CliFramework
|
||||||
{
|
{
|
||||||
@@ -46,7 +46,7 @@ class SyncRulesetsCli extends CliFramework
|
|||||||
);
|
);
|
||||||
|
|
||||||
$platformName = $adapter->getPlatformName();
|
$platformName = $adapter->getPlatformName();
|
||||||
$ALWAYS_EXCLUDE = ['moko-platform', '.github-private'];
|
$ALWAYS_EXCLUDE = ['mokocli', '.github-private'];
|
||||||
|
|
||||||
// -- Protection rules (platform-agnostic format) --
|
// -- Protection rules (platform-agnostic format) --
|
||||||
$PROTECTIONS = [
|
$PROTECTIONS = [
|
||||||
|
|||||||
+7
-12
@@ -6,9 +6,9 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: moko-platform.CLI
|
* DEFGROUP: mokocli.CLI
|
||||||
* INGROUP: moko-platform
|
* INGROUP: mokocli
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /cli/theme_lint.php
|
* PATH: /cli/theme_lint.php
|
||||||
* BRIEF: Lint theme files -- CSS syntax, image sizes, hardcoded URLs
|
* BRIEF: Lint theme files -- CSS syntax, image sizes, hardcoded URLs
|
||||||
*/
|
*/
|
||||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoCli\{CliFramework, SourceResolver};
|
||||||
|
|
||||||
class ThemeLintCli extends CliFramework
|
class ThemeLintCli extends CliFramework
|
||||||
{
|
{
|
||||||
@@ -41,17 +41,12 @@ class ThemeLintCli extends CliFramework
|
|||||||
$errors = 0;
|
$errors = 0;
|
||||||
$warnings = 0;
|
$warnings = 0;
|
||||||
|
|
||||||
$srcDir = null;
|
$srcDir = SourceResolver::resolveAbsolute($root);
|
||||||
foreach (['src', 'htdocs'] as $d) {
|
|
||||||
if (is_dir("{$root}/{$d}")) {
|
|
||||||
$srcDir = "{$root}/{$d}";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($srcDir === null) {
|
if ($srcDir === null) {
|
||||||
$this->log('ERROR', "No src/ or htdocs/ directory in {$root}");
|
$this->log('ERROR', "No source/ or src/ directory in {$root}");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
SourceResolver::warnIfLegacy($root);
|
||||||
|
|
||||||
echo "Theme Lint: {$srcDir}\n\n";
|
echo "Theme Lint: {$srcDir}\n\n";
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: moko-platform.CLI
|
* DEFGROUP: mokocli.CLI
|
||||||
* INGROUP: moko-platform
|
* INGROUP: mokocli
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /cli/updates_xml_build.php
|
* PATH: /cli/updates_xml_build.php
|
||||||
* BRIEF: Generate Joomla updates.xml from extension manifest metadata
|
* BRIEF: Generate Joomla updates.xml from extension manifest metadata
|
||||||
*/
|
*/
|
||||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoCli\{CliFramework, SourceResolver};
|
||||||
|
|
||||||
class UpdatesXmlBuildCli extends CliFramework
|
class UpdatesXmlBuildCli extends CliFramework
|
||||||
{
|
{
|
||||||
@@ -109,7 +109,7 @@ class UpdatesXmlBuildCli extends CliFramework
|
|||||||
// -- Locate Joomla manifest ---------------------------------------------------
|
// -- Locate Joomla manifest ---------------------------------------------------
|
||||||
$manifest = null;
|
$manifest = null;
|
||||||
|
|
||||||
$candidates = glob("{$root}/src/pkg_*.xml") ?: [];
|
$candidates = SourceResolver::globSource($root, 'pkg_*.xml');
|
||||||
foreach ($candidates as $f) {
|
foreach ($candidates as $f) {
|
||||||
if (strpos(file_get_contents($f), '<extension') !== false) {
|
if (strpos(file_get_contents($f), '<extension') !== false) {
|
||||||
$manifest = $f;
|
$manifest = $f;
|
||||||
|
|||||||
@@ -6,11 +6,11 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: moko-platform.CLI
|
* DEFGROUP: mokocli.CLI
|
||||||
* INGROUP: moko-platform
|
* INGROUP: mokocli
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /cli/updates_xml_sync.php
|
* PATH: /cli/updates_xml_sync.php
|
||||||
* VERSION: 09.23.00
|
* VERSION: 09.38.01
|
||||||
* BRIEF: Sync updates.xml to target branches via Gitea API
|
* BRIEF: Sync updates.xml to target branches via Gitea API
|
||||||
* NOTE: Called by pre-release and auto-release workflows after updates.xml
|
* NOTE: Called by pre-release and auto-release workflows after updates.xml
|
||||||
* is modified on the current branch. Pushes the file to other branches
|
* is modified on the current branch. Pushes the file to other branches
|
||||||
@@ -21,7 +21,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoCli\CliFramework;
|
||||||
|
|
||||||
class UpdatesXmlSyncCli extends CliFramework
|
class UpdatesXmlSyncCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,11 +6,11 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: moko-platform.CLI
|
* DEFGROUP: mokocli.CLI
|
||||||
* INGROUP: moko-platform
|
* INGROUP: mokocli
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /cli/version_auto_bump.php
|
* PATH: /cli/version_auto_bump.php
|
||||||
* VERSION: 09.23.00
|
* VERSION: 09.38.01
|
||||||
* BRIEF: Auto patch-bump, set stability suffix, and commit — single CLI replacing inline workflow bash
|
* BRIEF: Auto patch-bump, set stability suffix, and commit — single CLI replacing inline workflow bash
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoCli\CliFramework;
|
||||||
|
|
||||||
class VersionAutoBumpCli extends CliFramework
|
class VersionAutoBumpCli extends CliFramework
|
||||||
{
|
{
|
||||||
@@ -109,10 +109,18 @@ class VersionAutoBumpCli extends CliFramework
|
|||||||
echo "{$line}\n";
|
echo "{$line}\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: Read version
|
// Step 2: Read version (--quiet suppresses banner so only the version is output)
|
||||||
$versionOutput = [];
|
$versionOutput = [];
|
||||||
exec("{$php} {$cli}/version_read.php --path " . escapeshellarg($path) . " 2>&1", $versionOutput, $versionRc);
|
exec("{$php} {$cli}/version_read.php --path " . escapeshellarg($path) . " --quiet 2>&1", $versionOutput, $versionRc);
|
||||||
$version = trim($versionOutput[0] ?? '');
|
// Take the last non-empty line — the version is always the final output
|
||||||
|
$version = '';
|
||||||
|
foreach (array_reverse($versionOutput) as $line) {
|
||||||
|
$line = trim($line);
|
||||||
|
if (preg_match('/^\d{2}\.\d{2}\.\d{2}/', $line)) {
|
||||||
|
$version = $line;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (empty($version)) {
|
if (empty($version)) {
|
||||||
echo "No version found — skipping\n";
|
echo "No version found — skipping\n";
|
||||||
|
|||||||
+173
-12
@@ -6,9 +6,9 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: moko-platform.CLI
|
* DEFGROUP: mokocli.CLI
|
||||||
* INGROUP: moko-platform
|
* INGROUP: mokocli
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
* PATH: /cli/version_bump.php
|
* PATH: /cli/version_bump.php
|
||||||
* BRIEF: Auto-increment version -- manifest.xml is canonical, cascades to all XML and MD files
|
* BRIEF: Auto-increment version -- manifest.xml is canonical, cascades to all XML and MD files
|
||||||
*/
|
*/
|
||||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoEnterprise\CliFramework;
|
use MokoCli\{CliFramework, SourceResolver};
|
||||||
|
|
||||||
class VersionBumpCli extends CliFramework
|
class VersionBumpCli extends CliFramework
|
||||||
{
|
{
|
||||||
@@ -27,6 +27,7 @@ class VersionBumpCli extends CliFramework
|
|||||||
$this->addArgument('--path', 'Repository root', '.');
|
$this->addArgument('--path', 'Repository root', '.');
|
||||||
$this->addArgument('--minor', 'Bump minor version', false);
|
$this->addArgument('--minor', 'Bump minor version', false);
|
||||||
$this->addArgument('--major', 'Bump major version', false);
|
$this->addArgument('--major', 'Bump major version', false);
|
||||||
|
$this->addArgument('--min-version', 'Minimum base version (ensures bump is above this)', '');
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function run(): int
|
protected function run(): int
|
||||||
@@ -42,6 +43,7 @@ class VersionBumpCli extends CliFramework
|
|||||||
$root = realpath($path) ?: $path;
|
$root = realpath($path) ?: $path;
|
||||||
$mokoVersion = null;
|
$mokoVersion = null;
|
||||||
$existingSuffix = '';
|
$existingSuffix = '';
|
||||||
|
$versionPrefix = '';
|
||||||
$mokoManifest = "{$root}/.mokogitea/manifest.xml";
|
$mokoManifest = "{$root}/.mokogitea/manifest.xml";
|
||||||
$mokoContent = '';
|
$mokoContent = '';
|
||||||
if (file_exists($mokoManifest)) {
|
if (file_exists($mokoManifest)) {
|
||||||
@@ -50,29 +52,58 @@ class VersionBumpCli extends CliFramework
|
|||||||
$mokoVersion = $m[1];
|
$mokoVersion = $m[1];
|
||||||
$existingSuffix = $m[2] ?? '';
|
$existingSuffix = $m[2] ?? '';
|
||||||
}
|
}
|
||||||
|
// Read version_prefix from manifest.xml (supports nested and flat structure)
|
||||||
|
$xml = @simplexml_load_file($mokoManifest);
|
||||||
|
if ($xml !== false) {
|
||||||
|
$prefix = (string)($xml->identity->version_prefix ?? '');
|
||||||
|
if ($prefix === '') {
|
||||||
|
$prefix = (string)($xml->version_prefix ?? '');
|
||||||
|
}
|
||||||
|
$versionPrefix = $prefix;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
$readmeVersion = null;
|
$readmeVersion = null;
|
||||||
$readme = "{$root}/README.md";
|
$readme = "{$root}/README.md";
|
||||||
$readmeContent = '';
|
$readmeContent = '';
|
||||||
if (file_exists($readme)) {
|
if (file_exists($readme)) {
|
||||||
$readmeContent = file_get_contents($readme);
|
$readmeContent = file_get_contents($readme);
|
||||||
if (preg_match('/VERSION:\s*(\d{2}\.\d{2}\.\d{2})/m', $readmeContent, $m)) {
|
if (!empty($versionPrefix)) {
|
||||||
|
// Prefix-aware README scan
|
||||||
|
$prefixPattern = preg_quote($versionPrefix, '/');
|
||||||
|
if (preg_match('/' . $prefixPattern . '(\d{2}\.\d{2}\.\d{2})/m', $readmeContent, $m)) {
|
||||||
|
$readmeVersion = $m[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($readmeVersion === null && preg_match('/VERSION:\s*(\d{2}\.\d{2}\.\d{2})/m', $readmeContent, $m)) {
|
||||||
$readmeVersion = $m[1];
|
$readmeVersion = $m[1];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$manifestVersion = null;
|
$manifestVersion = null;
|
||||||
|
SourceResolver::warnIfLegacy($root);
|
||||||
$manifestFiles = array_merge(
|
$manifestFiles = array_merge(
|
||||||
glob("{$root}/src/pkg_*.xml") ?: [],
|
SourceResolver::globSource($root, 'pkg_*.xml'),
|
||||||
glob("{$root}/src/*.xml") ?: [],
|
SourceResolver::globSource($root, '*.xml'),
|
||||||
glob("{$root}/src/packages/*/mokowaas.xml") ?: [],
|
SourceResolver::globSource($root, 'packages/*/mokowaas.xml'),
|
||||||
glob("{$root}/src/packages/*/*.xml") ?: [],
|
SourceResolver::globSource($root, 'packages/*/*.xml'),
|
||||||
glob("{$root}/*.xml") ?: []
|
glob("{$root}/*.xml") ?: []
|
||||||
);
|
);
|
||||||
foreach ($manifestFiles as $xmlFile) {
|
foreach ($manifestFiles as $xmlFile) {
|
||||||
$xmlContent = file_get_contents($xmlFile);
|
$xmlContent = file_get_contents($xmlFile);
|
||||||
if (strpos($xmlContent, '<extension') === false && strpos($xmlContent, '<version>') === false) {
|
if (strpos($xmlContent, '<extension') === false && strpos($xmlContent, '<version>') === false) {
|
||||||
continue;
|
continue;
|
||||||
} if (preg_match('#<version>(\d{2}\.\d{2}\.\d{2})((?:-(?:dev|alpha|beta|rc))+)?</version>#', $xmlContent, $xm)) {
|
}
|
||||||
|
if (!empty($versionPrefix)) {
|
||||||
|
// Prefix-aware: look for <version>prefix + XX.YY.ZZ</version>
|
||||||
|
$prefixPattern = preg_quote($versionPrefix, '#');
|
||||||
|
if (preg_match('#<version>' . $prefixPattern . '(\d{2}\.\d{2}\.\d{2})</version>#', $xmlContent, $xm)) {
|
||||||
|
$candidate = $xm[1];
|
||||||
|
if ($manifestVersion === null || version_compare($candidate, $manifestVersion, '>')) {
|
||||||
|
$manifestVersion = $candidate;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (preg_match('#<version>(\d{2}\.\d{2}\.\d{2})((?:-(?:dev|alpha|beta|rc))+)?</version>#', $xmlContent, $xm)) {
|
||||||
$candidate = $xm[1];
|
$candidate = $xm[1];
|
||||||
if ($manifestVersion === null || version_compare($candidate, $manifestVersion, '>')) {
|
if ($manifestVersion === null || version_compare($candidate, $manifestVersion, '>')) {
|
||||||
$manifestVersion = $candidate;
|
$manifestVersion = $candidate;
|
||||||
@@ -86,6 +117,26 @@ class VersionBumpCli extends CliFramework
|
|||||||
$baseVersion = $v;
|
$baseVersion = $v;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check --min-version: ensures dev never falls behind stable
|
||||||
|
$minVersion = $this->getArgument('--min-version');
|
||||||
|
if (!empty($minVersion)) {
|
||||||
|
$minVersion = preg_replace('/-(?:dev|alpha|beta|rc)$/', '', $minVersion);
|
||||||
|
if (preg_match('/^\d{2}\.\d{2}\.\d{2}$/', $minVersion)) {
|
||||||
|
if ($baseVersion === null || version_compare($minVersion, $baseVersion, '>')) {
|
||||||
|
$this->log('INFO', "Using --min-version {$minVersion} (higher than manifest " . ($baseVersion ?? '(none)') . ")");
|
||||||
|
$baseVersion = $minVersion;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-detect: scan git tags for higher versions from other channels
|
||||||
|
$gitTagVersion = $this->getHighestGitTagVersion($root);
|
||||||
|
if ($gitTagVersion !== null && ($baseVersion === null || version_compare($gitTagVersion, $baseVersion, '>'))) {
|
||||||
|
$this->log('INFO', "Git tag version {$gitTagVersion} is higher than manifest " . ($baseVersion ?? '(none)') . " — using as base");
|
||||||
|
$baseVersion = $gitTagVersion;
|
||||||
|
}
|
||||||
|
|
||||||
if ($baseVersion === null) {
|
if ($baseVersion === null) {
|
||||||
$this->log('ERROR', "No version found in manifest.xml, README.md, or Joomla XML");
|
$this->log('ERROR', "No version found in manifest.xml, README.md, or Joomla XML");
|
||||||
return 1;
|
return 1;
|
||||||
@@ -135,18 +186,35 @@ class VersionBumpCli extends CliFramework
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (file_exists($readme) && !empty($readmeContent)) {
|
if (file_exists($readme) && !empty($readmeContent)) {
|
||||||
|
if (!empty($versionPrefix)) {
|
||||||
|
// Prefix-aware README replacement: preserve prefix, replace only version part
|
||||||
|
$prefixPattern = preg_quote($versionPrefix, '/');
|
||||||
|
$updated = preg_replace('/(' . $prefixPattern . ')\d{2}\.\d{2}\.\d{2}/m', '${1}' . $newBase, $readmeContent, 1);
|
||||||
|
} else {
|
||||||
$updated = preg_replace('/(VERSION:\s*)\d{2}\.\d{2}\.\d{2}(?:(?:-(?:dev|alpha|beta|rc))+)?/m', '${1}' . $newBase, $readmeContent, 1);
|
$updated = preg_replace('/(VERSION:\s*)\d{2}\.\d{2}\.\d{2}(?:(?:-(?:dev|alpha|beta|rc))+)?/m', '${1}' . $newBase, $readmeContent, 1);
|
||||||
|
}
|
||||||
if ($updated !== null) {
|
if ($updated !== null) {
|
||||||
file_put_contents($readme, $updated);
|
file_put_contents($readme, $updated);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$updatedFiles = [];
|
$updatedFiles = [];
|
||||||
foreach (["{$root}/src/pkg_*.xml", "{$root}/src/*.xml", "{$root}/src/packages/*/*.xml", "{$root}/*.xml"] as $pattern) {
|
$srcName = SourceResolver::resolve($root);
|
||||||
|
foreach (["{$root}/{$srcName}/pkg_*.xml", "{$root}/{$srcName}/*.xml", "{$root}/{$srcName}/packages/*/*.xml", "{$root}/*.xml"] as $pattern) {
|
||||||
foreach (glob($pattern) ?: [] as $xmlFile) {
|
foreach (glob($pattern) ?: [] as $xmlFile) {
|
||||||
$content = file_get_contents($xmlFile);
|
$content = file_get_contents($xmlFile);
|
||||||
if (strpos($content, '<extension') === false) {
|
if (strpos($content, '<extension') === false) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (!empty($versionPrefix)) {
|
||||||
|
// Prefix-aware: preserve prefix, replace only the Moko version part
|
||||||
|
$prefixPattern = preg_quote($versionPrefix, '#');
|
||||||
|
$xmlPattern = '#(<version>' . $prefixPattern . ')\d{2}\.\d{2}\.\d{2}</version>#';
|
||||||
|
$newContent = preg_replace(
|
||||||
|
$xmlPattern,
|
||||||
|
'${1}' . $newBase . '</version>',
|
||||||
|
$content
|
||||||
|
);
|
||||||
|
} else {
|
||||||
$xmlPattern = '#<version>\d{2}\.\d{2}\.\d{2}'
|
$xmlPattern = '#<version>\d{2}\.\d{2}\.\d{2}'
|
||||||
. '(?:(?:-(?:dev|alpha|beta|rc))+)?</version>#';
|
. '(?:(?:-(?:dev|alpha|beta|rc))+)?</version>#';
|
||||||
$newContent = preg_replace(
|
$newContent = preg_replace(
|
||||||
@@ -154,6 +222,7 @@ class VersionBumpCli extends CliFramework
|
|||||||
"<version>{$newFull}</version>",
|
"<version>{$newFull}</version>",
|
||||||
$content
|
$content
|
||||||
);
|
);
|
||||||
|
}
|
||||||
if ($newContent !== null && $newContent !== $content) {
|
if ($newContent !== null && $newContent !== $content) {
|
||||||
file_put_contents($xmlFile, $newContent);
|
file_put_contents($xmlFile, $newContent);
|
||||||
$updatedFiles[] = substr($xmlFile, strlen($root) + 1);
|
$updatedFiles[] = substr($xmlFile, strlen($root) + 1);
|
||||||
@@ -166,6 +235,16 @@ class VersionBumpCli extends CliFramework
|
|||||||
$packageJsonFile = "{$root}/package.json";
|
$packageJsonFile = "{$root}/package.json";
|
||||||
if (file_exists($packageJsonFile)) {
|
if (file_exists($packageJsonFile)) {
|
||||||
$pkgContent = file_get_contents($packageJsonFile);
|
$pkgContent = file_get_contents($packageJsonFile);
|
||||||
|
if (!empty($versionPrefix)) {
|
||||||
|
// Prefix-aware package.json replacement
|
||||||
|
$prefixPattern = preg_quote($versionPrefix, '/');
|
||||||
|
$pkgPattern = '/("version"\s*:\s*")' . $prefixPattern . '\d{2}\.\d{2}\.\d{2}(")/m';
|
||||||
|
$updatedPkg = preg_replace(
|
||||||
|
$pkgPattern,
|
||||||
|
'${1}' . $versionPrefix . $newBase . '${2}',
|
||||||
|
$pkgContent
|
||||||
|
);
|
||||||
|
} else {
|
||||||
$pkgPattern = '/("version"\s*:\s*")\d{2}\.\d{2}\.\d{2}'
|
$pkgPattern = '/("version"\s*:\s*")\d{2}\.\d{2}\.\d{2}'
|
||||||
. '(?:(?:-(?:dev|alpha|beta|rc))+)?(")/m';
|
. '(?:(?:-(?:dev|alpha|beta|rc))+)?(")/m';
|
||||||
$updatedPkg = preg_replace(
|
$updatedPkg = preg_replace(
|
||||||
@@ -173,14 +252,41 @@ class VersionBumpCli extends CliFramework
|
|||||||
'${1}' . $newFull . '${2}',
|
'${1}' . $newFull . '${2}',
|
||||||
$pkgContent
|
$pkgContent
|
||||||
);
|
);
|
||||||
if ($updatedPkg !== $pkgContent) {
|
}
|
||||||
|
if ($updatedPkg !== $pkgContent && $updatedPkg !== null) {
|
||||||
file_put_contents($packageJsonFile, $updatedPkg);
|
file_put_contents($packageJsonFile, $updatedPkg);
|
||||||
fwrite(STDERR, "Updated package.json\n");
|
fwrite(STDERR, "Updated package.json\n");
|
||||||
|
} elseif (preg_match('/("version"\s*:\s*")(\d+)\.(\d+)\.(\d+)(")/m', $pkgContent, $semM)) {
|
||||||
|
// Semver fallback: bump standard x.y.z version when XX.YY.ZZ pattern didn't match
|
||||||
|
$sMajor = (int)$semM[2];
|
||||||
|
$sMinor = (int)$semM[3];
|
||||||
|
$sPatch = (int)$semM[4];
|
||||||
|
switch ($type) {
|
||||||
|
case 'major': $sMajor++; $sMinor = 0; $sPatch = 0; break;
|
||||||
|
case 'minor': $sMinor++; $sPatch = 0; break;
|
||||||
|
default: $sPatch++; break;
|
||||||
|
}
|
||||||
|
$semNew = "{$sMajor}.{$sMinor}.{$sPatch}";
|
||||||
|
$semUpdated = preg_replace('/("version"\s*:\s*")\d+\.\d+\.\d+(")/m', '${1}' . $semNew . '${2}', $pkgContent);
|
||||||
|
if ($semUpdated !== $pkgContent) {
|
||||||
|
file_put_contents($packageJsonFile, $semUpdated);
|
||||||
|
fwrite(STDERR, "Updated package.json (semver: {$semM[2]}.{$semM[3]}.{$semM[4]} -> $semNew)\n");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$pyprojectFile = "{$root}/pyproject.toml";
|
$pyprojectFile = "{$root}/pyproject.toml";
|
||||||
if (file_exists($pyprojectFile)) {
|
if (file_exists($pyprojectFile)) {
|
||||||
$pyContent = file_get_contents($pyprojectFile);
|
$pyContent = file_get_contents($pyprojectFile);
|
||||||
|
if (!empty($versionPrefix)) {
|
||||||
|
// Prefix-aware pyproject.toml replacement
|
||||||
|
$prefixPattern = preg_quote($versionPrefix, '/');
|
||||||
|
$pyPattern = '/^(version\s*=\s*")' . $prefixPattern . '\d{2}\.\d{2}\.\d{2}(")/m';
|
||||||
|
$updatedPy = preg_replace(
|
||||||
|
$pyPattern,
|
||||||
|
'${1}' . $versionPrefix . $newBase . '${2}',
|
||||||
|
$pyContent
|
||||||
|
);
|
||||||
|
} else {
|
||||||
$pyPattern = '/^(version\s*=\s*")\d{2}\.\d{2}\.\d{2}'
|
$pyPattern = '/^(version\s*=\s*")\d{2}\.\d{2}\.\d{2}'
|
||||||
. '(?:(?:-(?:dev|alpha|beta|rc))+)?(")/m';
|
. '(?:(?:-(?:dev|alpha|beta|rc))+)?(")/m';
|
||||||
$updatedPy = preg_replace(
|
$updatedPy = preg_replace(
|
||||||
@@ -188,6 +294,7 @@ class VersionBumpCli extends CliFramework
|
|||||||
'${1}' . $newFull . '${2}',
|
'${1}' . $newFull . '${2}',
|
||||||
$pyContent
|
$pyContent
|
||||||
);
|
);
|
||||||
|
}
|
||||||
if ($updatedPy !== $pyContent) {
|
if ($updatedPy !== $pyContent) {
|
||||||
file_put_contents($pyprojectFile, $updatedPy);
|
file_put_contents($pyprojectFile, $updatedPy);
|
||||||
fwrite(STDERR, "Updated pyproject.toml\n");
|
fwrite(STDERR, "Updated pyproject.toml\n");
|
||||||
@@ -204,7 +311,13 @@ class VersionBumpCli extends CliFramework
|
|||||||
}
|
}
|
||||||
$scanExtensions = ['php', 'yml', 'yaml', 'md', 'txt', 'xml', 'sh', 'toml', 'ini', 'css', 'js'];
|
$scanExtensions = ['php', 'yml', 'yaml', 'md', 'txt', 'xml', 'sh', 'toml', 'ini', 'css', 'js'];
|
||||||
$excludeDirs = ['.git', 'vendor', 'node_modules', 'build', 'dist', '.claude'];
|
$excludeDirs = ['.git', 'vendor', 'node_modules', 'build', 'dist', '.claude'];
|
||||||
|
// Build the generic VERSION: pattern — prefix-aware if configured
|
||||||
|
if (!empty($versionPrefix)) {
|
||||||
|
$prefixPatternGeneric = preg_quote($versionPrefix, '/');
|
||||||
|
$versionPattern = '/(' . $prefixPatternGeneric . ')\d{2}\.\d{2}\.\d{2}/m';
|
||||||
|
} else {
|
||||||
$versionPattern = '/(VERSION:\s*)\d{2}\.\d{2}\.\d{2}/m';
|
$versionPattern = '/(VERSION:\s*)\d{2}\.\d{2}\.\d{2}/m';
|
||||||
|
}
|
||||||
$directory = new RecursiveDirectoryIterator($root, RecursiveDirectoryIterator::SKIP_DOTS);
|
$directory = new RecursiveDirectoryIterator($root, RecursiveDirectoryIterator::SKIP_DOTS);
|
||||||
$filter = new RecursiveCallbackFilterIterator($directory, function ($current, $key, $iterator) use ($excludeDirs) {
|
$filter = new RecursiveCallbackFilterIterator($directory, function ($current, $key, $iterator) use ($excludeDirs) {
|
||||||
if ($current->isDir() && in_array($current->getFilename(), $excludeDirs, true)) {
|
if ($current->isDir() && in_array($current->getFilename(), $excludeDirs, true)) {
|
||||||
@@ -251,6 +364,54 @@ class VersionBumpCli extends CliFramework
|
|||||||
echo "{$old} -> {$newFull}\n";
|
echo "{$old} -> {$newFull}\n";
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scan git release tags for the highest version across all channels.
|
||||||
|
*
|
||||||
|
* Checks release names like "MokoSuiteClient (VERSION: 09.38.01)" in
|
||||||
|
* git tags (stable, release-candidate, development, etc.) to find the
|
||||||
|
* highest version that has been released on any channel.
|
||||||
|
*/
|
||||||
|
private function getHighestGitTagVersion(string $root): ?string
|
||||||
|
{
|
||||||
|
$highest = null;
|
||||||
|
|
||||||
|
// Method 1: Parse version from git tag annotations / release commit messages
|
||||||
|
$output = [];
|
||||||
|
exec("cd " . escapeshellarg($root) . " && git log --all --oneline --grep='chore(version)' --grep='chore(release)' --format='%s' -20 2>/dev/null", $output);
|
||||||
|
|
||||||
|
foreach ($output as $line) {
|
||||||
|
if (preg_match('/(\d{2}\.\d{2}\.\d{2})/', $line, $m)) {
|
||||||
|
$v = preg_replace('/-(?:dev|alpha|beta|rc)$/', '', $m[1]);
|
||||||
|
if ($highest === null || version_compare($v, $highest, '>')) {
|
||||||
|
$highest = $v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method 2: Check version in remote branches' manifest files
|
||||||
|
$branches = ['origin/main', 'origin/rc', 'origin/dev'];
|
||||||
|
$manifestPaths = ['source/pkg_*.xml', 'pkg_*.xml'];
|
||||||
|
|
||||||
|
foreach ($branches as $branch) {
|
||||||
|
foreach ($manifestPaths as $pattern) {
|
||||||
|
$files = [];
|
||||||
|
exec("cd " . escapeshellarg($root) . " && git ls-tree --name-only {$branch} -- '{$pattern}' 2>/dev/null", $files);
|
||||||
|
|
||||||
|
foreach ($files as $file) {
|
||||||
|
$content = shell_exec("cd " . escapeshellarg($root) . " && git show {$branch}:{$file} 2>/dev/null");
|
||||||
|
if ($content && preg_match('#<version>(\d{2}\.\d{2}\.\d{2})(?:-(?:dev|alpha|beta|rc))?</version>#', $content, $m)) {
|
||||||
|
$v = $m[1];
|
||||||
|
if ($highest === null || version_compare($v, $highest, '>')) {
|
||||||
|
$highest = $v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $highest;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$app = new VersionBumpCli();
|
$app = new VersionBumpCli();
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user