218 Commits

Author SHA1 Message Date
gitea-actions[bot] 6d8e09827d chore: promote changelog [Unreleased] → [09.38.00] 2026-06-21 23:22:28 +00:00
gitea-actions[bot] 6190be6ed2 chore(release): build 09.38.00 [skip ci]
Publish to Composer / Publish Package (release) Waiting to run
2026-06-21 23:22:24 +00:00
jmiller 58590c4f7f Merge pull request 'chore: remove automation directory' (#309) from fix/remove-automation into main
Platform: mokocli CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: mokocli CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: mokocli CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: mokocli CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: mokocli CI / Gate 4: Governance (push) Blocked by required conditions
Platform: mokocli CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: mokocli CI / CI Summary (push) Blocked by required conditions
Platform: mokocli CI / Gate 1: Code Quality (push) Failing after 1m15s
2026-06-21 23:10:01 +00:00
Jonathan Miller b74e3f0f83 chore: remove automation directory
Platform: mokocli CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: mokocli CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: mokocli CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: mokocli CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: mokocli CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: mokocli CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: mokocli CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Universal: PR Check / Branch Policy (pull_request) Failing after 2s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Validate PR (pull_request) Failing after 9s
Universal: PR Check / Secret Scan (pull_request) Successful in 18s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 19s
Platform: mokocli CI / Gate 1: Code Quality (pull_request) Failing after 1m16s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 25s
Branch Cleanup / Delete merged branch (pull_request) Failing after 2s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Failing after 8s
2026-06-21 18:03:06 -05:00
jmiller 7904a17355 chore: sync auto-release.yml from Template-Generic [skip ci] 2026-06-21 22:02:21 +00:00
jmiller 65ade5dc57 chore: sync pre-release.yml from Template-Generic [skip ci] 2026-06-21 16:05:09 +00:00
jmiller 173e2c4c35 chore: sync security-audit.yml from Template-Generic [skip ci] 2026-06-21 06:34:41 +00:00
jmiller 798094bab9 chore: sync repo-health.yml from Template-Generic [skip ci] 2026-06-21 06:34:40 +00:00
jmiller 665a4fec63 chore: sync pr-check.yml from Template-Generic [skip ci] 2026-06-21 06:34:39 +00:00
jmiller eda5875718 chore: sync notify.yml from Template-Generic [skip ci] 2026-06-21 06:34:38 +00:00
jmiller cd02979860 chore: sync issue-branch.yml from Template-Generic [skip ci] 2026-06-21 06:34:37 +00:00
jmiller 62428b1c9a chore: sync gitleaks.yml from Template-Generic [skip ci] 2026-06-21 06:34:36 +00:00
jmiller 47be9257d3 chore: sync deploy-manual.yml from Template-Generic [skip ci] 2026-06-21 06:34:34 +00:00
jmiller f12347594b chore: sync cleanup.yml from Template-Generic [skip ci] 2026-06-21 06:34:33 +00:00
jmiller a4c1364eb2 chore: sync ci-generic.yml from Template-Generic [skip ci] 2026-06-21 06:34:32 +00:00
jmiller 1cd3aff4ce chore: sync branch-cleanup.yml from Template-Generic [skip ci] 2026-06-21 06:34:31 +00:00
jmiller fccc9313e3 chore: sync auto-bump.yml from Template-Generic [skip ci] 2026-06-21 06:34:30 +00:00
gitea-actions[bot] f19fb7fb0d chore: promote changelog [Unreleased] → [09.38.00] 2026-06-21 06:19:19 +00:00
gitea-actions[bot] 733de92d19 chore(release): build 09.38.00 [skip ci]
Platform: mokocli CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: mokocli CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: mokocli CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: mokocli CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: mokocli CI / Gate 4: Governance (push) Blocked by required conditions
Platform: mokocli CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: mokocli CI / CI Summary (push) Blocked by required conditions
Publish to Composer / Publish Package (release) Successful in 34s
Platform: mokocli CI / Gate 1: Code Quality (push) Failing after 42s
2026-06-21 06:19:16 +00:00
jmiller fa0862b224 Merge pull request 'fix(package): only zip sub-extensions listed in package manifest (#300)' (#305) from dev into main
Platform: mokocli CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: mokocli CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: mokocli CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: mokocli CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: mokocli CI / Gate 4: Governance (push) Blocked by required conditions
Platform: mokocli CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: mokocli CI / CI Summary (push) Blocked by required conditions
Platform: mokocli CI / Gate 1: Code Quality (push) Failing after 49s
2026-06-21 06:18:59 +00:00
gitea-actions[bot] 193fa8be53 chore(version): pre-release bump to 09.37.07-dev [skip ci]
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Failing after 5s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 23s
Publish to Composer / Publish Package (release) Successful in 41s
2026-06-21 06:18:56 +00:00
jmiller 8a5209bf19 Merge pull request 'chore: complete namespace cleanup across all files' (#306) from fix/namespace-cleanup into dev
Platform: mokocli CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: mokocli CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: mokocli CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: mokocli CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: mokocli CI / Gate 4: Governance (push) Blocked by required conditions
Platform: mokocli CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: mokocli CI / CI Summary (push) Blocked by required conditions
Platform: mokocli CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: mokocli CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: mokocli CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: mokocli CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: mokocli CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: mokocli CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: mokocli CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Universal: Sync Feature Branch Versions / Sync feature branches with dev (push) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Universal: PR Check / Secret Scan (pull_request) Successful in 9s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 15s
Platform: mokocli CI / Gate 1: Code Quality (pull_request) Failing after 55s
Platform: mokocli CI / Gate 1: Code Quality (push) Failing after 57s
2026-06-21 06:18:37 +00:00
Jonathan Miller 95880d3e44 chore: complete namespace cleanup — remove all mokoplatform/MokoStandards/MokoEnterprise refs
Platform: mokocli CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: mokocli CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: mokocli CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: mokocli CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: mokocli CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: mokocli CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: mokocli CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Universal: PR Check / Secret Scan (pull_request) Successful in 7s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Failing after 1s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 14s
Platform: mokocli CI / Gate 1: Code Quality (pull_request) Failing after 53s
390 files: templates, workflows, MCP servers, CLI tools, lib, deploy,
validate, wrappers, configs, docs. Pure find-and-replace.
2026-06-21 01:18:13 -05:00
gitea-actions[bot] 2904f1ce0e chore(version): pre-release bump to 09.37.04-dev [skip ci]
Publish to Composer / Publish Package (release) Successful in 9s
2026-06-21 06:14:47 +00:00
gitea-actions[bot] e17d61e366 chore(version): auto-bump patch 09.37.03-dev [skip ci] 2026-06-21 06:14:35 +00:00
Jonathan Miller 24fbc08bce fix(package): only zip sub-extensions listed in package manifest (#300)
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: mokoplatform CI / CI Summary (push) Blocked by required conditions
Universal: Sync Feature Branch Versions / Sync feature branches with dev (push) Has been skipped
Universal: Auto Version Bump / Version Bump (push) Successful in 12s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 13s
Platform: mokoplatform CI / Gate 1: Code Quality (push) Failing after 46s
release_package.php was globbing ALL directories under packages/,
including ones removed from the manifest (backup, license, dbip).
Now parses pkg_*.xml to build allowlist and skips unlisted dirs.
2026-06-21 01:14:20 -05:00
gitea-actions[bot] 2ae287a25b chore(version): pre-release bump to 09.37.02-dev [skip ci]
Publish to Composer / Publish Package (release) Successful in 8s
2026-06-21 06:10:16 +00:00
gitea-actions[bot] 66d4c0651d chore(version): auto-bump patch 09.37.01-dev [skip ci] 2026-06-21 06:10:06 +00:00
jmiller 32c500109f fix: remove version field from composer.json for Packagist compatibility
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: mokoplatform CI / CI Summary (push) Blocked by required conditions
Universal: Sync Feature Branch Versions / Sync feature branches with dev (push) Has been skipped
Universal: Auto Version Bump / Version Bump (push) Successful in 9s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 12s
Platform: mokoplatform CI / Gate 1: Code Quality (push) Failing after 42s
2026-06-21 05:59:37 +00:00
jmiller 11371cf2d0 feat: add Composer publish workflow for Gitea registry + Packagist
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: mokoplatform CI / CI Summary (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 1: Code Quality (push) Failing after 46s
2026-06-21 05:45:12 +00:00
jmiller 5948d599f2 chore: rename package to mokoconsulting/mokocli, update description
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: mokoplatform CI / CI Summary (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 1: Code Quality (push) Failing after 49s
2026-06-21 05:44:48 +00:00
gitea-actions[bot] 7275c8c646 chore: promote changelog [Unreleased] → [09.37.00] 2026-06-21 05:28:23 +00:00
gitea-actions[bot] cb7340ce21 chore(release): build 09.37.00 [skip ci]
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: mokoplatform CI / CI Summary (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 1: Code Quality (push) Failing after 1m2s
2026-06-21 05:28:19 +00:00
jmiller a67bf83467 Merge pull request 'feat: interactive repo configuration wizard (#145)' (#294) from feature/145-repo-wizard into main
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: mokoplatform CI / CI Summary (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 1: Code Quality (push) Failing after 50s
2026-06-21 05:25:42 +00:00
gitea-actions[bot] 632d8486b8 chore(version): auto-bump patch 09.36.01-dev [skip ci]
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 22s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Failing after 1s
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Failing after 3s
2026-06-21 05:25:14 +00:00
Jonathan Miller 558cd6043d fix: address review findings in repo_wizard.php
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: mokoplatform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: mokoplatform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: mokoplatform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: mokoplatform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Universal: PR Check / Branch Policy (pull_request) Failing after 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Universal: PR Check / Secret Scan (pull_request) Successful in 6s
Universal: Auto Version Bump / Version Bump (push) Successful in 12s
Platform: mokoplatform CI / Gate 1: Code Quality (pull_request) Failing after 1m2s
- Fix #1: replace nonexistent menu() with choose() using select()
- Fix #2: constructor — pass name only, not description as version
- Fix #3: respect --non-interactive flag (skip prompts, use defaults)
- Fix #4: use json_encode for composer/package.json (prevent injection)
- Fix #5: remove pointless count() wrapper
- Fix #6: validate --path exists or can be created before proceeding
- Fix TOML description escaping
2026-06-21 00:24:50 -05:00
Jonathan Miller 1cf076f088 feat: interactive repo configuration wizard (#145)
Add `repo:wizard` command — walks through platform selection, generates
config files (phpcs, phpstan, eslint, tsconfig, composer/package.json,
.editorconfig, README, CHANGELOG, .gitignore, workflows), and optionally
creates the repo on Gitea via API.

Supports --dry-run, --non-interactive, and --create-remote flags.
2026-06-21 00:24:49 -05:00
jmiller 00f0e44c78 chore: sync issue-branch.yml from Template-Generic [skip ci] 2026-06-21 04:46:56 +00:00
gitea-actions[bot] c976f400f4 chore: promote changelog [Unreleased] → [09.36.00] 2026-06-21 04:46:41 +00:00
gitea-actions[bot] ebf37423f2 chore(release): build 09.36.00 [skip ci]
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: mokoplatform CI / CI Summary (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 1: Code Quality (push) Failing after 42s
2026-06-21 04:46:38 +00:00
jmiller e4d836067f Merge pull request 'feat: deploy:verify — deploy with auto health check and rollback (#147)' (#293) from feature/147-deploy-verify into main
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: mokoplatform CI / CI Summary (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 1: Code Quality (push) Failing after 49s
2026-06-21 04:46:21 +00:00
gitea-actions[bot] 8900b12f81 chore(version): auto-bump patch 09.35.02-dev [skip ci]
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 20s
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Failing after 1m28s
2026-06-21 04:42:00 +00:00
Jonathan Miller 4fc3d0a4a9 fix: address review findings in deploy-and-verify.php
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: mokoplatform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: mokoplatform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: mokoplatform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: mokoplatform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Universal: PR Check / Branch Policy (pull_request) Failing after 2s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Validate PR (pull_request) Failing after 9s
Universal: PR Check / Secret Scan (pull_request) Successful in 10s
Universal: Auto Version Bump / Version Bump (push) Successful in 11s
Platform: mokoplatform CI / Gate 1: Code Quality (pull_request) Failing after 56s
- Fix #1: replace rm -rf with cross-platform PHP removeDirectory()
- Fix #2: sanitize URL in audit log (log hostname only)
- Fix #3: remove unused buildHealthArgs() and $healthArgs
- Fix #4: add random suffix to snapshot dir name for uniqueness
- Fix #5: fix constructor to match CliFramework pattern (no args)
- Fix #6: trigger rollback on deploy failure (partial deploy risk)
2026-06-20 23:41:09 -05:00
gitea-actions[bot] 19aa0111f0 chore(version): auto-bump patch 09.35.01-dev [skip ci] 2026-06-21 04:31:30 +00:00
Jonathan Miller 46e33a9383 feat: deploy:verify — deploy with auto health check and rollback (#147)
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
Orchestrates backup → deploy → health-check → rollback-if-failed:
- Pre-deploy snapshot via backup-before-deploy.php
- Deploy via deploy-sftp.php subprocess
- Inline health check with configurable retries and delay
- Auto-rollback via rollback-joomla.php if health check fails
- Post-rollback verification
- Full audit trail via AuditLogger
2026-06-20 23:31:08 -05:00
jmiller 2f43bac247 chore: sync issue-branch.yml from Template-Generic [skip ci] 2026-06-21 04:10:46 +00:00
gitea-actions[bot] 817e9caee8 chore: promote changelog [Unreleased] → [09.35.00] 2026-06-21 03:22:59 +00:00
gitea-actions[bot] 6216803590 chore(release): build 09.35.00 [skip ci]
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: mokoplatform CI / CI Summary (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 1: Code Quality (push) Failing after 40s
2026-06-21 03:22:57 +00:00
jmiller caad8ee7d0 Merge pull request 'feat: smart error recovery suggestions in validators (#146)' (#292) from feature/146-recovery-suggestions into main
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: mokoplatform CI / CI Summary (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 1: Code Quality (push) Failing after 40s
2026-06-21 03:22:42 +00:00
gitea-actions[bot] 558c0a0edf chore(version): auto-bump patch 09.34.01-dev [skip ci]
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Failing after 2s
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Failing after 3s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 16s
2026-06-21 03:20:33 +00:00
Jonathan Miller cb1053274e feat: smart error recovery suggestions in validators (#146)
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: mokoplatform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: mokoplatform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: mokoplatform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: mokoplatform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Failing after 1s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Universal: PR Check / Secret Scan (pull_request) Successful in 8s
Universal: Auto Version Bump / Version Bump (push) Successful in 10s
Platform: mokoplatform CI / Gate 1: Code Quality (pull_request) Failing after 48s
- Add RecoverySuggestion class with methods for common fix patterns
  (missing files, directories, XML elements, version mismatches, commands)
- Add suggest() method to CliFramework (yellow lightbulb-prefixed output)
- Integrate into check_structure.php (missing file/dir suggestions)
- Integrate into check_version_consistency.php (version mismatch fixes)
2026-06-20 22:20:16 -05:00
jmiller 743da9c4c2 chore: sync issue-branch.yml from Template-Generic [skip ci] 2026-06-21 02:50:31 +00:00
jmiller 4b6fcb5fa4 Merge pull request 'fix(version-bump): prevent dev from falling behind stable (#289)' (#291) from feature/289-cherry-pick-version-bump into main
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: mokoplatform CI / CI Summary (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 1: Code Quality (push) Failing after 1m2s
2026-06-21 02:49:47 +00:00
Jonathan Miller e7b2c1fba2 fix(version-bump): prevent dev from falling behind stable (#289)
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: mokoplatform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: mokoplatform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: mokoplatform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: mokoplatform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Universal: PR Check / Branch Policy (pull_request) Failing after 1s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Universal: PR Check / Secret Scan (pull_request) Successful in 8s
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Platform: mokoplatform CI / Gate 1: Code Quality (pull_request) Failing after 1m6s
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Failing after 2m2s
Universal: Auto Version Bump / Version Bump (push) Successful in 11s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 28s
Cherry-pick from dev (d5fa609). version_bump.php now checks:
1. --min-version argument from workflow
2. Auto-detect from git — scans origin/main and origin/rc for highest
   released version and uses it as the bump base

Closes #289
2026-06-20 21:49:17 -05:00
gitea-actions[bot] 2a45dd873b chore: promote changelog [Unreleased] → [09.34.00] 2026-06-21 02:47:06 +00:00
gitea-actions[bot] e0f1ec1372 chore(release): build 09.34.00 [skip ci]
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: mokoplatform CI / CI Summary (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 1: Code Quality (push) Failing after 1m6s
2026-06-21 02:47:03 +00:00
jmiller f325de91d4 Merge pull request 'feat: cross-repo dependency update automation (#149)' (#290) from feature/149-dependency-update-automation into main
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: mokoplatform CI / CI Summary (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 1: Code Quality (push) Failing after 1m3s
2026-06-21 02:46:47 +00:00
gitea-actions[bot] 109493ab4a chore(version): auto-bump patch 09.33.01-dev [skip ci]
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Failing after 3s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 19s
2026-06-21 02:42:15 +00:00
Jonathan Miller 0d3b14d55c feat: cross-repo dependency update automation (#149)
Universal: Auto Version Bump / Version Bump (push) Successful in 9s
Add `deps:update` command that scans org repos for outdated Composer/npm
dependencies, creates PRs with changelogs, and optionally auto-merges
safe patch updates.

- Composer: runs `composer outdated --format=json`, updates targeted packages
- npm: runs `npm outdated --json`, updates targeted packages
- Skips repos with existing deps PRs (no duplicates)
- Checkpoint-based resumability with --resume
- --patch-only for safe updates, --auto-merge for patch PRs
- Supports --repos and --exclude filters
2026-06-20 21:41:43 -05:00
jmiller 35075aa743 chore: sync issue-branch.yml from Template-Generic [skip ci] 2026-06-21 02:23:11 +00:00
gitea-actions[bot] 7be017ae30 chore: promote changelog [Unreleased] → [09.33.00] 2026-06-21 02:22:56 +00:00
gitea-actions[bot] e8a3414ff4 chore(release): build 09.33.00 [skip ci]
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: mokoplatform CI / CI Summary (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 1: Code Quality (push) Failing after 1m25s
2026-06-21 02:22:53 +00:00
jmiller e8697c2d0e Merge pull request 'feat: deploy-sftp.php --env demo/live + multi-instance (#184)' (#287) from feature/184-deploy-env-demo-live into main
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: mokoplatform CI / CI Summary (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 1: Code Quality (push) Failing after 1m23s
2026-06-21 02:22:39 +00:00
gitea-actions[bot] 7d369628f0 chore(version): auto-bump patch 09.32.01-dev [skip ci]
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 16s
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Failing after 1m57s
2026-06-21 02:21:55 +00:00
Jonathan Miller e834b8a3ea feat: deploy-sftp.php supports --env demo and --env live with multi-instance (#184)
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
- Add demo and live to ENV_CONFIG_MAP
- Add multi-target deploy via LIVE_TARGETS env var (JSON array of targets)
- Add sftp-config.demo.json.example and sftp-config.live.json.example templates
- Failed targets logged but don't block remaining deploys
2026-06-20 21:21:42 -05:00
gitea-actions[bot] 1655e2a0ae chore: promote changelog [Unreleased] → [09.32.00] 2026-06-21 02:04:14 +00:00
gitea-actions[bot] 4aef631244 chore(release): build 09.32.00 [skip ci]
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: mokoplatform CI / CI Summary (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 1: Code Quality (push) Failing after 1m3s
2026-06-21 02:04:11 +00:00
jmiller 6b2cf099f7 Merge pull request 'feat: SourceResolver queries Gitea Manifest API for entry_point (#249)' (#286) from feature/249-source-resolver-api into main
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: mokoplatform CI / CI Summary (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 1: Code Quality (push) Failing after 1m1s
2026-06-21 02:04:01 +00:00
gitea-actions[bot] fefa44965f chore(version): auto-bump patch 09.31.01-dev [skip ci]
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Failing after 1s
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Failing after 3s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 14s
2026-06-21 02:02:20 +00:00
Jonathan Miller 2b7e38b711 feat: SourceResolver queries Gitea Manifest API for entry_point (#249)
Universal: Auto Version Bump / Version Bump (push) Successful in 12s
SourceResolver::resolve() now checks the Gitea Manifest API first when
GA_TOKEN/GITEA_TOKEN and GITHUB_REPOSITORY are available (CI environments).
Falls back to filesystem detection (source/, src/, htdocs/) when offline.

- API results cached per org/repo for process lifetime
- 5s timeout to avoid blocking local dev
- resolveFromApi() also available as standalone method
- org/repo derived from GITHUB_REPOSITORY env or git remote URL
2026-06-20 21:02:04 -05:00
jmiller 3ed575906f chore: sync issue-branch.yml from Template-Generic [skip ci] 2026-06-21 01:40:36 +00:00
gitea-actions[bot] c775cb9447 chore: promote changelog [Unreleased] → [09.31.00] 2026-06-21 01:40:16 +00:00
gitea-actions[bot] 89542f6018 chore(release): build 09.31.00 [skip ci]
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: mokoplatform CI / CI Summary (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 1: Code Quality (push) Failing after 1m25s
2026-06-21 01:40:13 +00:00
jmiller bd97b6f79c Merge pull request 'feat: sync feature branch versions with dev after bumps (#250)' (#285) from feature/250-sync-feature-branch-versions into main
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: mokoplatform CI / CI Summary (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 1: Code Quality (push) Failing after 1m28s
2026-06-21 01:39:59 +00:00
Jonathan Miller c461cc5520 feat: sync feature branch versions with dev after bumps (#250)
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: mokoplatform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: mokoplatform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: mokoplatform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: mokoplatform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Universal: PR Check / Branch Policy (pull_request) Failing after 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Universal: PR Check / Secret Scan (pull_request) Successful in 6s
Universal: Auto Version Bump / Version Bump (push) Successful in 11s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 18s
Platform: mokoplatform CI / Gate 1: Code Quality (pull_request) Failing after 1m20s
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Failing after 2m10s
Add workflow that merges dev into all open feature/fix/patch/hotfix/bugfix/chore
branches when dev gets a version bump commit. Branches with merge conflicts are
skipped with a warning. Can also be triggered manually via workflow_dispatch.
2026-06-20 20:39:38 -05:00
jmiller ae4191bad0 chore: sync workflow-sync-trigger.yml from Template-Generic [skip ci] 2026-06-21 01:27:28 +00:00
jmiller 73f3d0e734 chore: sync repo-health.yml from Template-Generic [skip ci] 2026-06-21 01:27:27 +00:00
jmiller 0e7ca2f19a chore: sync rc-revert.yml from Template-Generic [skip ci] 2026-06-21 01:27:26 +00:00
jmiller 685089f60c chore: sync pr-check.yml from Template-Generic [skip ci] 2026-06-21 01:27:24 +00:00
jmiller 4d3aef3cc5 chore: sync issue-branch.yml from Template-Generic [skip ci] 2026-06-21 01:27:23 +00:00
jmiller d6d05a1075 chore: sync deploy-manual.yml from Template-Generic [skip ci] 2026-06-21 01:27:22 +00:00
jmiller 1c8bf4e76e chore: sync ci-generic.yml from Template-Generic [skip ci] 2026-06-21 01:27:21 +00:00
jmiller 6e99d0fac6 chore: sync auto-release.yml from Template-Generic [skip ci] 2026-06-21 01:27:19 +00:00
gitea-actions[bot] 5161072b0e chore: promote changelog [Unreleased] → [09.30.00]
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
2026-06-21 01:26:33 +00:00
gitea-actions[bot] 7a4358454d chore(release): build 09.30.00 [skip ci]
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: mokoplatform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Platform: mokoplatform CI / Gate 1: Code Quality (push) Failing after 1m29s
2026-06-21 01:26:29 +00:00
jmiller a0cc0953c7 Merge pull request 'feat: security advisory aggregator, manifest API rewrite, namespace rename' (#284) from feature/150-security-advisory-aggregator into main
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: mokoplatform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Platform: mokoplatform CI / Gate 1: Code Quality (push) Failing after 1m33s
2026-06-21 01:26:10 +00:00
gitea-actions[bot] 79c853354b chore(version): auto-bump patch 09.29.02-dev [skip ci]
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 21s
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Failing after 4m3s
2026-06-21 01:25:35 +00:00
Jonathan Miller 033e948c79 feat: security advisory aggregator, manifest API rewrite, namespace rename (#150, #283)
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: mokoplatform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: mokoplatform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: mokoplatform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: mokoplatform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Failing after 2s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Universal: Security Audit / Dependency Audit (pull_request) Successful in 7s
Universal: Auto Version Bump / Version Bump (push) Successful in 14s
Platform: mokoplatform CI / Gate 1: Code Quality (pull_request) Failing after 1m8s
- Add `security:advisories` command — cross-repo CVE scanner via composer audit
  with checkpoint resumability, severity filtering, and auto-issue creation
- Rewrite `manifest:read` to use Gitea manifest API as primary source with
  auto-detection fallback from source tree (no more manifest.xml dependency)
- Rename MokoStandards namespace → MokoCli across all files
- Rename MokoEnterprise namespace → MokoCli across all files
- Rename MokoStandardsParser class → ManifestParser
- Fix composer.json autoload paths: src/ → source/
2026-06-20 20:24:58 -05:00
jmiller 016fda394b chore: delete manifest.xml (no longer used)
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: mokoplatform CI / CI Summary (push) Blocked by required conditions
Platform: mokoplatform CI / Gate 1: Code Quality (push) Failing after 1m10s
2026-06-21 00:19:36 +00:00
jmiller 8b18bd73e5 chore: sync pr-check.yml from Template-Generic [skip ci] 2026-06-20 23:45:51 +00:00
jmiller ffba748762 chore: sync gitleaks.yml from Template-Generic [skip ci] 2026-06-20 23:45:50 +00:00
jmiller d67ff279a1 chore: sync ci-generic.yml from Template-Generic [skip ci] 2026-06-20 23:45:50 +00:00
jmiller fbbc170333 chore: sync workflow-sync-trigger.yml from Template-Generic [skip ci] 2026-06-20 20:50:56 +00:00
jmiller b574799ca1 chore: sync security-audit.yml from Template-Generic [skip ci] 2026-06-20 20:50:55 +00:00
jmiller 79866a15e5 chore: sync repo-health.yml from Template-Generic [skip ci] 2026-06-20 20:50:54 +00:00
jmiller a3c18a0248 chore: sync pre-release.yml from Template-Generic [skip ci] 2026-06-20 20:50:53 +00:00
jmiller 57392c254e chore: sync pr-check.yml from Template-Generic [skip ci] 2026-06-20 20:50:52 +00:00
jmiller acdc0e903c chore: sync notify.yml from Template-Generic [skip ci] 2026-06-20 20:50:51 +00:00
jmiller 55af5b0feb chore: sync issue-branch.yml from Template-Generic [skip ci] 2026-06-20 20:50:50 +00:00
jmiller 47ab0de146 chore: sync gitleaks.yml from Template-Generic [skip ci] 2026-06-20 20:50:49 +00:00
jmiller cfdb0094ba chore: sync deploy-manual.yml from Template-Generic [skip ci] 2026-06-20 20:50:48 +00:00
jmiller 5e8092a1c9 chore: sync cleanup.yml from Template-Generic [skip ci] 2026-06-20 20:50:47 +00:00
jmiller 3699edbf62 chore: sync ci-generic.yml from Template-Generic [skip ci] 2026-06-20 20:50:46 +00:00
jmiller c5860d4074 chore: sync branch-cleanup.yml from Template-Generic [skip ci] 2026-06-20 20:50:45 +00:00
jmiller 5d8fa2b1d3 chore: sync auto-release.yml from Template-Generic [skip ci] 2026-06-20 20:50:44 +00:00
jmiller 9f3dd6f790 chore: sync auto-bump.yml from Template-Generic [skip ci] 2026-06-20 20:50:43 +00:00
jmiller 4ccc850edf Merge pull request 'fix: add missing src/functions.php referenced by composer autoload' (#282) from fix/missing-functions-php into main
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Project CI / Lint & Validate (push) Failing after 38s
Platform: mokoplatform CI / Gate 1: Code Quality (push) Failing after 1m19s
Generic: Project CI / Tests (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: mokoplatform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: mokoplatform CI / Gate 4: Governance (push) Has been cancelled
Platform: mokoplatform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: mokoplatform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-20 19:03:55 +00:00
Moko Consulting 957460db03 fix: add missing src/functions.php referenced by composer autoload
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Failing after 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 1s
Branch Cleanup / Delete merged branch (pull_request) Failing after 2s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 7s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request_target) Failing after 11s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Generic: Project CI / Lint & Validate (pull_request) Failing after 23s
Platform: mokoplatform CI / Gate 1: Code Quality (pull_request) Failing after 1m13s
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Project CI / Tests (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 3: Self-Health Check (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 4: Governance (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 5: Template Integrity (pull_request) Has been cancelled
Platform: mokoplatform CI / CI Summary (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled
2026-06-20 18:48:40 +00:00
jmiller 8b014ad0a9 feat: implement release zip cascade to lower stability channels (#280)
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Project CI / Lint & Validate (push) Failing after 57s
Platform: mokoplatform CI / Gate 1: Code Quality (push) Failing after 1m16s
Generic: Project CI / Tests (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: mokoplatform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: mokoplatform CI / Gate 4: Governance (push) Has been cancelled
Platform: mokoplatform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: mokoplatform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-20 17:40:03 +00:00
jmiller f942615a12 fix: add branch+PR fallback for protected repos, rename moko-platform to mokocli
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Project CI / Lint & Validate (push) Failing after 39s
Platform: mokoplatform CI / Gate 1: Code Quality (push) Failing after 44s
Generic: Project CI / Tests (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: mokoplatform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: mokoplatform CI / Gate 4: Governance (push) Has been cancelled
Platform: mokoplatform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: mokoplatform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-20 17:30:34 +00:00
jmiller 116d94dd8c fix: add branch+PR fallback for protected repos, rename moko-platform to mokocli
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Project CI / Lint & Validate (push) Failing after 44s
Platform: mokoplatform CI / Gate 1: Code Quality (push) Failing after 47s
Generic: Project CI / Tests (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: mokoplatform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: mokoplatform CI / Gate 4: Governance (push) Has been cancelled
Platform: mokoplatform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: mokoplatform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-20 17:30:33 +00:00
jmiller a918cb38b2 Merge pull request 'fix: rename package_type to extension_type, remove display_name (#259)' (#281) from fix/259-cherry-pick into main
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Project CI / Lint & Validate (push) Failing after 32s
Platform: mokoplatform CI / Gate 1: Code Quality (push) Failing after 46s
Generic: Project CI / Tests (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: mokoplatform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: mokoplatform CI / Gate 4: Governance (push) Has been cancelled
Platform: mokoplatform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: mokoplatform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-20 17:15:50 +00:00
Jonathan Miller dc7f6c9eeb fix: improve error handling, add extension_type fallback (#259)
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request_target) Failing after 14s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Failing after 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 10s
Generic: Project CI / Lint & Validate (pull_request) Failing after 44s
Platform: mokoplatform CI / Gate 1: Code Quality (pull_request) Failing after 1m17s
Branch Cleanup / Delete merged branch (pull_request) Failing after 2s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Project CI / Tests (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 3: Self-Health Check (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 4: Governance (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 5: Template Integrity (pull_request) Has been cancelled
Platform: mokoplatform CI / CI Summary (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled
- Surface HTTP errors instead of suppressing with @file_get_contents
- Add specific messages for 401/403/404 and missing token
- Fall back to package_type if extension_type not in API response
- Log warnings for malformed XML candidates
- Fix platform_detect.php endpoint from /manifest to /metadata
2026-06-20 12:14:08 -05:00
Jonathan Miller 0b1f39a75c fix: rename package_type to extension_type, remove display_name validation (#259)
- API endpoint updated from /manifest to /metadata
- Removed dead .mokogitea/manifest.xml local file fallback
- display_name is now server-computed, no longer validated
- package_type renamed to extension_type throughout
2026-06-20 12:14:07 -05:00
jmiller db21aca7d0 Merge pull request 'fix: rename Template-Client-Waas submodule to Template-Client' (#275) from fix/rename-client-waas-submodule into main
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Project CI / Lint & Validate (push) Failing after 49s
Platform: mokoplatform CI / Gate 1: Code Quality (push) Failing after 58s
Generic: Project CI / Tests (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: mokoplatform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: mokoplatform CI / Gate 4: Governance (push) Has been cancelled
Platform: mokoplatform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: mokoplatform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-20 16:00:42 +00:00
Jonathan Miller 463ae43e64 fix: rename Template-Client-Waas submodule to Template-Client
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Failing after 1s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 9s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Generic: Project CI / Lint & Validate (pull_request) Failing after 44s
Platform: mokoplatform CI / Gate 1: Code Quality (pull_request) Failing after 1m41s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request_target) Failing after 7s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Project CI / Tests (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 3: Self-Health Check (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 4: Governance (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 5: Template Integrity (pull_request) Has been cancelled
Platform: mokoplatform CI / CI Summary (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled
2026-06-20 11:00:15 -05:00
jmiller 93c524b655 Merge pull request 'refactor: rename src/ to source/' (#274) from fix/rename-src-to-source-v2 into main
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Project CI / Lint & Validate (push) Failing after 30s
Platform: mokoplatform CI / Gate 1: Code Quality (push) Failing after 1m1s
Generic: Project CI / Tests (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: mokoplatform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: mokoplatform CI / Gate 4: Governance (push) Has been cancelled
Platform: mokoplatform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: mokoplatform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-20 16:00:11 +00:00
jmiller 2e0f3554db Merge pull request 'refactor: replace template dirs with git submodules' (#273) from fix/templates-to-submodules into main
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Project CI / Lint & Validate (push) Failing after 1m1s
Platform: mokoplatform CI / Gate 1: Code Quality (push) Failing after 1m41s
Generic: Project CI / Tests (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: mokoplatform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: mokoplatform CI / Gate 4: Governance (push) Has been cancelled
Platform: mokoplatform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: mokoplatform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-20 15:58:25 +00:00
Jonathan Miller 5521a7f81e refactor: replace template dirs with git submodules
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Failing after 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 7s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Generic: Project CI / Lint & Validate (pull_request) Failing after 24s
Branch Cleanup / Delete merged branch (pull_request) Failing after 1s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Platform: mokoplatform CI / Gate 1: Code Quality (pull_request) Failing after 1m46s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Project CI / Tests (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 3: Self-Health Check (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 4: Governance (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 5: Template Integrity (pull_request) Has been cancelled
Platform: mokoplatform CI / CI Summary (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled
Remove embedded template copies from templates/repos/ and replace
with submodules pointing to the canonical template repos:

- Template-Client-Waas -> Template-Client
- Template-Generic -> Template-Generic
- Template-Joomla -> Template-Joomla
- Template-MCP -> Template-MCP

Dolibarr template removed (no longer needed).
Submodule paths match repo names for consistency.

Use `git submodule update --init` to populate after clone.
2026-06-20 10:56:40 -05:00
Jonathan Miller 3331442381 refactor: rename src/ to source/ for consistency
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Project CI / Lint & Validate (pull_request) Failing after 31s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 9s
Universal: PR Check / Branch Policy (pull_request) Failing after 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 9s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
Platform: mokoplatform CI / Gate 1: Code Quality (pull_request) Failing after 1m12s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request_target) Failing after 7s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Project CI / Tests (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 3: Self-Health Check (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 4: Governance (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 5: Template Integrity (pull_request) Has been cancelled
Platform: mokoplatform CI / CI Summary (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled
2026-06-20 10:56:35 -05:00
Jonathan Miller 62b19d516f refactor: replace template dirs with git submodules
Remove embedded template copies from templates/repos/ and replace
with submodules pointing to the canonical template repos:

- client-waas -> Template-Client
- generic -> Template-Generic
- joomla -> Template-Joomla
- mcp -> Template-MCP

Dolibarr template removed (no longer needed).

Templates are now maintained in their own repos. Use
`git submodule update --init` to populate after clone.
2026-06-20 10:52:08 -05:00
jmiller 8d2ac56f0c Merge pull request 'chore: rename GA_TOKEN/GH_TOKEN secret references' (#272) from fix/rename-secret-refs into main
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Project CI / Lint & Validate (push) Failing after 31s
Platform: mokoplatform CI / Gate 1: Code Quality (push) Failing after 1m17s
Generic: Project CI / Tests (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: mokoplatform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: mokoplatform CI / Gate 4: Governance (push) Has been cancelled
Platform: mokoplatform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: mokoplatform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-20 15:30:20 +00:00
Jonathan Miller 12a16b178a chore: rename secret references for readability
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Failing after 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 9s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 10s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Failing after 1s
Generic: Project CI / Lint & Validate (pull_request) Failing after 27s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request_target) Failing after 12s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Platform: mokoplatform CI / Gate 1: Code Quality (pull_request) Failing after 1m22s
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Project CI / Tests (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 3: Self-Health Check (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 4: Governance (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 5: Template Integrity (pull_request) Has been cancelled
Platform: mokoplatform CI / CI Summary (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled
GA_TOKEN -> MOKOGITEA_TOKEN
GH_TOKEN -> GH_PAT

Applied across all workflows, templates, and embedded MCP servers.
Secrets need to be updated in repo settings to match.
2026-06-20 10:29:48 -05:00
jmiller f8e21dce5a fix(ci): detect rebuild by branch name not version suffix [skip ci] 2026-06-19 01:59:20 +00:00
jmiller 1e4d3d5b3f ci: patch bump on same-branch rebuilds, minor only on elevation [skip ci] 2026-06-19 01:06:59 +00:00
jmiller 78fa142342 Merge pull request 'feat: joomla_metadata_validate CLI command (#257)' (#258) from feature/257-joomla-metadata-validate into main
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Project CI / Lint & Validate (push) Failing after 18s
Platform: mokoplatform CI / Gate 1: Code Quality (push) Failing after 50s
Generic: Project CI / Tests (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: mokoplatform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: mokoplatform CI / Gate 4: Governance (push) Has been cancelled
Platform: mokoplatform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: mokoplatform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-18 20:55:12 +00:00
gitea-actions[bot] 7ac196e4a8 chore(version): auto-bump patch 09.29.01-dev [skip ci]
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Failing after 1s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
2026-06-18 20:53:54 +00:00
Jonathan Miller 2a3e733d8c feat: add joomla_metadata_validate CLI command (#257)
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Site Health (push) Has been skipped
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Validates MokoGitea repo metadata against the actual Joomla extension
manifest XML to catch update delivery mismatches before production.

Checks:
- package_type matches <extension type>
- Element name derived correctly (prefix + lowercase + clean)
- Display name matches <name> tag
- Version consistency (ignoring -dev/-rc suffixes)
- PHP minimum matches composer.json
- Description match (informational)

Supports:
- Local mode: reads .mokogitea/manifest.xml + Joomla XML from disk
- API mode: fetches metadata via Gitea API (--token)
- CI mode: --ci flag exits 1 on errors
- JSON output: --json for workflow integration

Handles all Joomla types: package, component, module, plugin,
template, library, file. Replicates Joomla's InputFilter::clean('cmd')
for element name derivation.

Refs mokoplatform #257
2026-06-18 15:53:43 -05:00
jmiller 3abd08da44 ci: add changelog extraction to promote-rc job [skip ci] 2026-06-18 16:41:04 +00:00
Jonathan Miller 7269800891 fix: restore full workflow_sync.php with PLATFORM_EXCLUDES
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Project CI / Lint & Validate (push) Failing after 17s
Platform: mokoplatform CI / Gate 1: Code Quality (push) Failing after 52s
Generic: Project CI / Tests (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: mokoplatform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: mokoplatform CI / Gate 4: Governance (push) Has been cancelled
Platform: mokoplatform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: mokoplatform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Previous API pushes truncated the file content. This commit restores
the complete file with all changes: PLATFORM_EXCLUDES constant,
Phase 1 and Phase 2 exclusion checks, version bump to 09.30.00.
2026-06-18 11:34:59 -05:00
jmiller b038a49279 fix: push full workflow_sync.php with PLATFORM_EXCLUDES (replace truncated content)
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Project CI / Lint & Validate (push) Failing after 25s
Platform: mokoplatform CI / Gate 1: Code Quality (push) Failing after 38s
Generic: Project CI / Tests (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: mokoplatform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: mokoplatform CI / Gate 4: Governance (push) Has been cancelled
Platform: mokoplatform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: mokoplatform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-18 16:19:28 +00:00
jmiller 9b05cd5fc5 fix: push actual workflow_sync.php with PLATFORM_EXCLUDES (previous commit had placeholder content)
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Project CI / Lint & Validate (push) Failing after 19s
Platform: mokoplatform CI / Gate 1: Code Quality (push) Failing after 54s
Generic: Project CI / Tests (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: mokoplatform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: mokoplatform CI / Gate 4: Governance (push) Has been cancelled
Platform: mokoplatform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: mokoplatform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-18 16:15:29 +00:00
jmiller 2846c361e0 feat: add PLATFORM_EXCLUDES to workflow_sync - skip deploy-manual.yml for Joomla
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Project CI / Lint & Validate (push) Failing after 22s
Platform: mokoplatform CI / Gate 1: Code Quality (push) Failing after 1m4s
Generic: Project CI / Tests (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: mokoplatform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: mokoplatform CI / Gate 4: Governance (push) Has been cancelled
Platform: mokoplatform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: mokoplatform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-18 16:13:40 +00:00
jmiller 7ccf2585dd ci: disable auto-bump on push to dev [skip ci] 2026-06-16 19:40:38 +00:00
jmiller 5c31771037 feat(cli): add semver fallback for package.json version bump
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Project CI / Lint & Validate (push) Failing after 29s
Platform: mokoplatform CI / Gate 1: Code Quality (push) Failing after 1m27s
Generic: Project CI / Tests (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: mokoplatform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: mokoplatform CI / Gate 4: Governance (push) Has been cancelled
Platform: mokoplatform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: mokoplatform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
When package.json uses standard x.y.z semver instead of
MokoPlatform XX.YY.ZZ format, auto-bump using the same
bump type (major/minor/patch). Respects --minor and --major flags.
2026-06-11 07:29:20 +00:00
gitea-actions[bot] 8de72a386d chore(release): build 09.29.00 [skip ci] 2026-06-09 19:40:34 +00:00
jmiller 05aafbac61 Merge pull request 'feat: detect display_name, target_version, php_minimum in manifest_detect' (#254) from feat/manifest-detect-update-fields into main 2026-06-09 19:40:08 +00:00
Jonathan Miller 3c57e87066 feat: detect display_name, target_version, php_minimum in manifest_detect
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 3s
Universal: PR Check / Branch Policy (pull_request) Failing after 3s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 4s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: PR Check / Validate PR (pull_request) Failing after 9s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 12s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 5s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request_target) Failing after 21s
Generic: Project CI / Lint & Validate (pull_request) Failing after 34s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 33s
Platform: mokoplatform CI / Gate 1: Code Quality (pull_request) Failing after 1m37s
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Project CI / Tests (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 3: Self-Health Check (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 4: Governance (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 5: Template Integrity (pull_request) Has been cancelled
Platform: mokoplatform CI / CI Summary (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled
These fields are now required by MokoGitea's update feed generator
which reads them from the manifest API (#592).

- display_name: from XML <name> with type prefix detection
- target_version: from <targetplatform version=""> or default (5|6)
- php_minimum: from <php_minimum> tag
2026-06-09 14:39:54 -05:00
gitea-actions[bot] e37443774b chore(release): build 09.28.00 [skip ci] 2026-06-07 22:30:41 +00:00
jmiller e6b9a3b4f6 Merge pull request 'chore: replace MokoJoom references with MokoSuite' (#253) from feat/manifest-integrity into main
Generic: Project CI / Tests (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: mokoplatform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: mokoplatform CI / Gate 4: Governance (push) Has been cancelled
Platform: mokoplatform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: mokoplatform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Generic: Project CI / Lint & Validate (push) Has been cancelled
Platform: mokoplatform CI / Gate 1: Code Quality (push) Has been cancelled
2026-06-07 22:30:31 +00:00
Jonathan Miller d0200c28f0 chore: replace MokoJoom references with MokoSuite in templates
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Project CI / Tests (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 3: Self-Health Check (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 4: Governance (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 5: Template Integrity (pull_request) Has been cancelled
Platform: mokoplatform CI / CI Summary (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Universal: PR Check / Branch Policy (pull_request) Has been cancelled
Generic: Repo Health / Access control (pull_request) Has been cancelled
Generic: Repo Health / Site Health (pull_request) Has been cancelled
Universal: PR Check / Validate PR (pull_request) Has been cancelled
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Has been cancelled
RC Revert / Rename rc/ back to dev/ (pull_request) Has been cancelled
Branch Cleanup / Delete merged branch (pull_request) Has been cancelled
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request_target) Has been cancelled
Generic: Project CI / Lint & Validate (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 1: Code Quality (pull_request) Has been cancelled
Universal: Build & Release / Promote to RC (pull_request) Has been cancelled
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been cancelled
- Makefile: use $(EXTENSION_NAME) variable instead of hardcoded
  mokojoomgallery, update example name to mokosuite
- composer.json: update template package name and description
- mokobackup: rename MokoJoomBackup to MokoSuite Backup in docs/code
2026-06-07 17:26:58 -05:00
gitea-actions[bot] 1f946c0b75 chore(release): build 09.27.00 [skip ci] 2026-06-07 21:30:44 +00:00
jmiller d4e2d36301 Merge pull request 'feat: manifest_integrity.php — org-wide manifest validation' (#252) from feat/manifest-integrity into main
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: mokoplatform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: mokoplatform CI / Gate 4: Governance (push) Has been cancelled
Platform: mokoplatform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: mokoplatform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Platform: mokoplatform CI / Gate 1: Code Quality (push) Has been cancelled
2026-06-07 21:30:32 +00:00
Jonathan Miller a2c1a61759 feat: add manifest_integrity.php for org-wide manifest validation
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Project CI / Tests (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 3: Self-Health Check (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 4: Governance (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 5: Template Integrity (pull_request) Has been cancelled
Platform: mokoplatform CI / CI Summary (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: PR Check / Branch Policy (pull_request) Has been cancelled
Generic: Repo Health / Access control (pull_request) Has been cancelled
Generic: Repo Health / Site Health (pull_request) Has been cancelled
Universal: PR Check / Validate PR (pull_request) Has been cancelled
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Has been cancelled
RC Revert / Rename rc/ back to dev/ (pull_request) Has been cancelled
Branch Cleanup / Delete merged branch (pull_request) Has been cancelled
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request_target) Has been cancelled
Generic: Project CI / Lint & Validate (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 1: Code Quality (pull_request) Has been cancelled
Universal: Build & Release / Promote to RC (pull_request) Has been cancelled
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been cancelled
Cross-checks manifest API fields against repo contents.
Three modes: local (--path), single remote (--repo), bulk org.
Validates required fields, platform-specific rules, and version format.
Supports --fix to auto-push corrections, --json for CI consumption.
2026-06-07 16:25:49 -05:00
gitea-actions[bot] 0d12bca60e chore(release): build 09.26.00 [skip ci] 2026-06-07 20:38:11 +00:00
jmiller 7cabc6e5ef Merge pull request 'feat: manifest_detect.php + dev sync + workflow updates' (#251) from feat/manifest-detect into main
Generic: Project CI / Tests (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: mokoplatform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: mokoplatform CI / Gate 4: Governance (push) Has been cancelled
Platform: mokoplatform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: mokoplatform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Generic: Project CI / Lint & Validate (push) Has been cancelled
Platform: mokoplatform CI / Gate 1: Code Quality (push) Has been cancelled
2026-06-07 20:37:54 +00:00
Jonathan Miller b73c1eba25 feat: add manifest_detect.php CLI tool for auto-detecting manifest fields
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Project CI / Tests (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 3: Self-Health Check (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 4: Governance (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 5: Template Integrity (pull_request) Has been cancelled
Platform: mokoplatform CI / CI Summary (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Generic: Repo Health / Site Health (pull_request) Has been cancelled
Universal: PR Check / Branch Policy (pull_request) Has been cancelled
Generic: Repo Health / Access control (pull_request) Has been cancelled
Universal: Build & Release / Promote to RC (pull_request) Has been cancelled
RC Revert / Rename rc/ back to dev/ (pull_request) Has been cancelled
Universal: Security Audit / Dependency Audit (pull_request) Has been cancelled
Branch Cleanup / Delete merged branch (pull_request) Has been cancelled
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Has been cancelled
Universal: PR Check / Validate PR (pull_request) Has been cancelled
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been cancelled
Generic: Project CI / Lint & Validate (pull_request) Has been cancelled
Platform: mokoplatform CI / Gate 1: Code Quality (pull_request) Has been cancelled
Scans source files to detect platform, name, version, element_name,
package_type, language, entry_point, description, and license_spdx.
Supports Joomla, Dolibarr, Go, MCP/Node, and generic platforms.

Includes --diff and --update modes for comparing against and pushing
to the Gitea manifest API. Warns on missing core fields.

Also removes deprecated mcp/servers/mokowaas_api (consolidated to
separate repo) and syncs dev branch changes.
2026-06-07 15:37:24 -05:00
jmiller e5ab1a4cd5 chore: sync security-audit.yml from Template-Generic [skip ci] 2026-06-07 17:55:25 +00:00
jmiller f81873cf37 chore: sync repo-health.yml from Template-Generic [skip ci] 2026-06-07 17:55:24 +00:00
jmiller d9e15b4f4a chore: sync pre-release.yml from Template-Generic [skip ci] 2026-06-07 17:55:23 +00:00
jmiller 31b4bbca2a chore: sync pr-check.yml from Template-Generic [skip ci] 2026-06-07 17:55:23 +00:00
jmiller a7f07b77a2 chore: sync notify.yml from Template-Generic [skip ci] 2026-06-07 17:55:22 +00:00
jmiller 3e84bc9a18 chore: sync issue-branch.yml from Template-Generic [skip ci] 2026-06-07 17:55:21 +00:00
jmiller 7e8c96b629 chore: sync gitleaks.yml from Template-Generic [skip ci] 2026-06-07 17:55:21 +00:00
jmiller 2bf88d1d17 chore: sync deploy-manual.yml from Template-Generic [skip ci] 2026-06-07 17:55:20 +00:00
jmiller 2160cd1ce7 chore: sync cleanup.yml from Template-Generic [skip ci] 2026-06-07 17:55:19 +00:00
jmiller 6b61d9a211 chore: sync ci-generic.yml from Template-Generic [skip ci] 2026-06-07 17:55:19 +00:00
jmiller 1fbffbcb57 chore: sync branch-cleanup.yml from Template-Generic [skip ci] 2026-06-07 17:55:18 +00:00
jmiller f910a8ad12 chore: sync auto-release.yml from Template-Generic [skip ci] 2026-06-07 17:55:17 +00:00
jmiller 5439df3876 chore: sync auto-bump.yml from Template-Generic [skip ci] 2026-06-07 17:55:17 +00:00
Jonathan Miller ab0c209897 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
2026-06-07 12:44:31 -05:00
jmiller e4d9bce5d0 docs: update changelog with workflow_sync, platform_detect, and version_prefix
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Successful in 5s
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-07 17:35:58 +00:00
gitea-actions[bot] e933e7b651 chore(version): auto-bump patch 09.25.02-dev [skip ci] 2026-06-07 17:35:06 +00:00
jmiller 157e87279e feat: add version_prefix support to version_bump — prefix-aware find/replace in source files
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
2026-06-07 17:35:01 +00:00
gitea-actions[bot] 7850721f86 chore(version): auto-bump patch 09.25.01-dev [skip ci] 2026-06-07 17:34:03 +00:00
jmiller 8949f69699 feat: add version_prefix support — prefix-aware version read and bump
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
2026-06-07 17:33:55 +00:00
jmiller af2313d936 feat: add platform_detect.php — auto-detect repo platform and update manifest
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
2026-06-07 17:30:52 +00:00
jmiller 2e5446ff5e feat: add workflow_sync.php — cascading sync based on manifest.platform
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
2026-06-07 17:27:49 +00:00
jmiller 17a50851fb fix: gitignore site/ should be /site/ to avoid matching tmpl/site/
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
2026-06-07 03:18:38 +00:00
jmiller ef3614d249 fix: gitignore site/ should be /site/ to avoid matching tmpl/site/
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
2026-06-07 03:17:58 +00:00
jmiller ab05bb7008 chore: sync .mokogitea/workflows/pre-release.yml from moko-platform [skip ci] 2026-06-06 19:48:11 +00:00
Jonathan Miller 3d5d581883 Merge branch 'dev'
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
2026-06-06 13:00:10 -05:00
Jonathan Miller 6bd26698c4 fix: check for manifest_element.php in pre-installed tools validation
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Runner image has stale /opt/moko-platform missing manifest_element.php.
Adding it to the existence check forces a fresh clone until the image
is rebuilt.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 12:53:27 -05:00
Jonathan Miller 19b504526b fix: remove double quotes from shell commands in workflow YAML
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
act_runner passes run: | blocks through a shell that treats double
quotes as literal characters in some contexts. Removed all double
quotes from echo, test, and git clone commands. Git clone URL is
now built in a variable to avoid quoting issues with the token.

Fixes pre-release and auto-release workflows failing with:
  fatal: protocol '"https' is not supported

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 12:44:44 -05:00
Jonathan Miller e7bdf7cbc7 fix: load Composer autoloader in CliFramework constructor (#248)
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
CLI tools failed with "Class MokoEnterprise\SourceResolver not found"
because the Composer autoloader was never loaded. The require_once
for CliFramework.php loaded the framework but not the PSR-4 autoloader
that maps MokoEnterprise\ to lib/Enterprise/.

Adding require_once for vendor/autoload.php in the constructor ensures
all Enterprise classes (SourceResolver, etc.) are available to every
CLI tool that extends CliFramework.

Closes #248

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 12:34:33 -05:00
Jonathan Miller ff5794d0cc fix: remove dead definitionParser reference in RepositorySynchronizer
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
The definitionParser property was never initialized or implemented.
synchronizeRepository() crashed with "Call to a member function
parseForPlatform() on null". Replaced with direct use of
getSharedWorkflows() which provides all files to sync.
2026-06-06 12:09:14 -05:00
Jonathan Miller bd5f676e0a Merge remote-tracking branch 'origin/dev'
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
# Conflicts:
#	cli/manifest_licensing.php
2026-06-06 11:49:30 -05:00
Jonathan Miller bfba45e8b5 chore: remove deprecated updates.xml build/sync from workflows
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Has been cancelled
Platform: moko-platform CI / CI Summary (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
Generic: Repo Health / Site Health (pull_request) Has been cancelled
Universal: PR Check / Branch Policy (pull_request) Has been cancelled
Generic: Repo Health / Access control (pull_request) Has been cancelled
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Has been cancelled
Universal: PR Check / Validate PR (pull_request) Has been cancelled
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request_target) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Has been cancelled
Branch Cleanup / Delete merged branch (pull_request) Has been cancelled
Universal: Build & Release / Promote to RC (pull_request) Has been cancelled
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been cancelled
MokoGitea generates update feeds dynamically from releases.
Static updates.xml is no longer needed.
2026-06-06 11:24:03 -05:00
Jonathan Miller 78ea05233b fix: update workflow path comments and token references
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
- Replace .gitea/ with .mokogitea/ in PATH comments
- Standardize token names to MOKOGITEA_TOKEN
- Remove github.token fallback patterns
2026-06-06 11:11:10 -05:00
Jonathan Miller ae0d54310d fix: replace smart quotes with ASCII in pre-release.yml (#245)
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
Unicode smart quotes (U+201C/U+201D) in the Setup moko-platform tools
step caused `fatal: protocol '"https' is not supported` during git clone.

Closes #245

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 10:23:42 -05:00
Jonathan Miller 9df59836bf chore: update CLAUDE.md template with first-run setup and focused format
Replace verbose boilerplate with platform-specific scaffold including
first-run setup checklist and placeholder tokens for new repos.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 10:23:42 -05:00
Jonathan Miller 6e40707223 chore: move CLAUDE.md to .mokogitea/ directory
Relocate CLAUDE.md from repo root to .mokogitea/ per project convention.
Content updated with focused, repo-specific architecture and rules.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 10:23:41 -05:00
Jonathan Miller ca55e5d2d2 feat(core): add SourceResolver for backwards-compatible src/ → source/ migration
Introduces SourceResolver utility class with source/ → src/ → htdocs/
fallback chain, replacing hardcoded src/ references across 28 files.
This enables renaming root-level src/ to source/ in all repos while
maintaining backwards compatibility during the transition.

Phase 1: New lib/Enterprise/SourceResolver.php with resolve(),
resolveAbsolute(), globSource(), findUnderSource(), warnIfLegacy()
Phase 2: Updated 19 CLI/deploy tools to use SourceResolver
Phase 3: Updated 7 validator/lib files (McpServerPlugin,
PackageBuilder, RepositorySynchronizer, auto_detect_platform,
check_dolibarr_module, check_client_theme, check_structure)

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 10:23:41 -05:00
jmiller 9526d006c4 feat(ci): add manifest_licensing step to pre-release workflow
Ensures updateservers, dlid, and blockChildUninstall tags are
present in Joomla extension manifests when licensing is enabled.

Authored-by: Moko Consulting
2026-06-06 10:23:41 -05:00
Jonathan Miller c90a5671bd feat(cli): add manifest_licensing.php for update server and dlid management
New CLI tool that reads <licensing> from manifest.xml and ensures
Joomla extension manifests have correct updateservers, dlid, and
blockChildUninstall tags. Supports dry-run and --fix modes.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 10:23:40 -05:00
gitea-actions[bot] 048a7d71d1 chore(release): build 09.25.00 [skip ci] 2026-06-06 10:23:40 -05:00
jmiller 847263dd86 feat(ci): add manifest_licensing step to pre-release workflow
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
Ensures updateservers, dlid, and blockChildUninstall tags are
present in Joomla extension manifests when licensing is enabled.

Authored-by: Moko Consulting
2026-06-06 12:27:19 +00:00
Jonathan Miller 6e540f64c4 feat(cli): add manifest_licensing.php for update server and dlid management
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
New CLI tool that reads <licensing> from manifest.xml and ensures
Joomla extension manifests have correct updateservers, dlid, and
blockChildUninstall tags. Supports dry-run and --fix modes.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 07:19:19 -05:00
jmiller c57b5724ac chore: remove update-server docs [skip ci] 2026-06-05 00:55:13 +00:00
jmiller 78affd37ff chore: remove update-server docs [skip ci] 2026-06-05 00:55:12 +00:00
jmiller b3062c6559 chore: remove update-server docs [skip ci] 2026-06-05 00:55:11 +00:00
jmiller cf02738930 chore: remove update-server docs [skip ci] 2026-06-05 00:55:10 +00:00
jmiller 455d4c8a19 chore: remove update-server docs [skip ci] 2026-06-05 00:55:09 +00:00
jmiller 8286d493b9 chore: remove update-server docs [skip ci] 2026-06-05 00:55:08 +00:00
jmiller b740152d67 Merge pull request 'ci: pre-installed /opt/moko-platform on runner' (#244) from dev into main
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-04 23:43:57 +00:00
Jonathan Miller 9dab9f1ef6 ci: use pre-installed /opt/moko-platform on runner, fallback to clone
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Universal: PR Check / Branch Policy (pull_request) Has been cancelled
Generic: Repo Health / Access control (pull_request) Has been cancelled
Generic: Repo Health / Site Health (pull_request) Has been cancelled
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Has been cancelled
Universal: PR Check / Validate PR (pull_request) Has been cancelled
Branch Cleanup / Delete merged branch (pull_request) Has been cancelled
Universal: Build & Release / Promote to RC (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Has been cancelled
Platform: moko-platform CI / CI Summary (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled
All workflows check for /opt/moko-platform first (updated by cron
every 6h). Falls back to fresh clone if not available.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 18:43:03 -05:00
Jonathan Miller c61d32709c chore: remove deprecated update-server.yml workflow [skip ci]
Authored-by: Moko Consulting
2026-06-04 18:42:33 -05:00
gitea-actions[bot] a59091e348 chore(release): build 09.25.00 [skip ci] 2026-06-04 23:03:39 +00:00
jmiller ee3d58c20f Merge pull request 'ci: remove updates.xml workflow steps, use CLI version_bump for pre-release' (#243) from dev into main
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-04 23:03:26 +00:00
Jonathan Miller 2b137f9041 Merge remote-tracking branch 'origin/main' into dev
Generic: Repo Health / Site Health (pull_request) Has been cancelled
Universal: PR Check / Branch Policy (pull_request) Has been cancelled
Generic: Repo Health / Access control (pull_request) Has been cancelled
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Universal: PR Check / Validate PR (pull_request) Has been cancelled
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request_target) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Has been cancelled
Branch Cleanup / Delete merged branch (pull_request) Has been cancelled
Universal: Build & Release / Promote to RC (pull_request) Has been cancelled
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Has been cancelled
Platform: moko-platform CI / CI Summary (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
# Conflicts:
#	.mokogitea/workflows/pre-release.yml
#	CHANGELOG.md
2026-06-04 18:02:23 -05:00
gitea-actions[bot] 54a27c0a8f chore(release): build 09.24.00 [skip ci] 2026-06-04 16:29:58 +00:00
jmiller 5754fae5a8 Merge pull request 'feat: --skip-update-stream flag + CI workflow updates' (#227) from dev into main
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
feat: --skip-update-stream flag + CI workflow updates
2026-06-04 16:29:47 +00:00
gitea-actions[bot] ab3c0a3a8d fix: ensure all pre-releases marked prerelease=true [skip ci] 2026-06-04 16:10:52 +00:00
jmiller eb3689cff6 chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] 2026-06-04 15:58:05 +00:00
jmiller 7338a3da2e chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] 2026-06-04 15:40:31 +00:00
jmiller 0a0e1f11e0 docs: add Dolibarr Update Server standards [skip ci] 2026-06-04 15:39:23 +00:00
jmiller c3a3ab3f62 docs: add Joomla Update Server standards [skip ci] 2026-06-04 15:38:29 +00:00
jmiller 556ac85a63 chore: remove static updates.xml.template - now served dynamically [skip ci] 2026-06-04 15:36:21 +00:00
jmiller c1a145480c chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] 2026-06-04 15:31:53 +00:00
jmiller ab7b6cfba1 chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] 2026-06-04 15:18:43 +00:00
gitea-actions[bot] 2d6155d655 chore: sync pre-release workflow — auto RC on PR to main [skip ci] 2026-06-04 14:45:48 +00:00
jmiller 65215cdc4c chore: sync .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] 2026-06-04 14:23:09 +00:00
jmiller 8c87cf1e74 chore: sync .mokogitea/workflows/repo-health.yml from moko-platform [skip ci] 2026-06-04 13:46:56 +00:00
Jonathan Miller 59d3524615 feat(ci): auto-file Gitea issues when CI gates fail
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Add ci-issue-reporter.sh shared script that creates or updates issues
via the Gitea API with dedup (searches open issues by ci-auto label).
Integrated into ci-platform, repo-health, and pr-check workflows.

Also fix push_files.php: getFileContent() → getFileContents() with
base64 decode to match the MokoGiteaAdapter interface.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 15:42:04 -05:00
Jonathan Miller 8058baef95 feat(cli): detect src/ entry-point in Joomla package sub-extensions
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
When building a Joomla package, sub-package directories that contain a
src/ folder with a Joomla manifest XML (e.g. git submodules of full
repos) now zip src/ instead of the repo root. This avoids bundling
README, CI config, and other repo-level files into extension packages.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 10:10:40 -05:00
Jonathan Miller df2efa4838 Merge fix/cli-banner-stderr: route all decorative CLI output to stderr
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Properly fixes the root cause that b53846f worked around — instead of
adding --quiet flags to individual callers, all CliFramework display
methods now write to stderr via display(), keeping stdout clean for
machine-readable data in all CLI scripts.

Authored-by: Moko Consulting
2026-06-02 10:07:34 -05:00
Jonathan Miller 76bc91a383 fix: route all decorative CLI output to stderr
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
All display methods (banner, progress, section, status, log, table,
summary box, divider, step) now write to stderr via a new display()
helper. This ensures stdout is reserved for machine-readable data,
fixing CI pipelines that capture CLI output with $() or redirect to
$GITHUB_OUTPUT.

Root cause: version_read.php banner was written to stdout, so
  VERSION=$(php version_read.php --path .)
captured the box-drawing characters along with the version string,
corrupting $GITHUB_OUTPUT and breaking downstream release steps.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 10:01:16 -05:00
Jonathan Miller b53846f6f4 fix: prevent version_read banner from corrupting XML manifests
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
- Add --quiet flag to version_read.php call in version_auto_bump.php
  so the CliFramework banner doesn't pollute stdout
- Parse version output by matching XX.YY.ZZ pattern instead of
  blindly taking the first line
- Add version format validation in version_set_platform.php to reject
  non-XX.YY.ZZ values before writing to XML files

Root cause: exec() captured the decorative banner output from
version_read.php and version_set_platform.php's regex replacement
injected it into <version> tags across all Joomla manifests.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 09:01:19 -05:00
736 changed files with 80169 additions and 10012 deletions
+12
View File
@@ -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
+76
View File
@@ -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)
+2 -2
View File
@@ -7,8 +7,8 @@ contact_links:
- name: 💬 Ask a Question
url: https://mokoconsulting.tech/
about: Get help or ask questions through our website
- name: 📚 moko-platform Documentation
url: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
- name: 📚 mokocli Documentation
url: https://git.mokoconsulting.tech/MokoConsulting/mokocli
about: View our coding standards and best practices
- name: 🔒 Report a Security Vulnerability
url: https://git.mokoconsulting.tech/mokoconsulting-tech/.github-private/security/advisories/new
+1 -1
View File
@@ -42,7 +42,7 @@ Suggested text here
<!-- Add any other context, screenshots, or references -->
## Standards Alignment
- [ ] Follows moko-platform documentation guidelines
- [ ] Follows mokocli documentation guidelines
- [ ] Uses en_US/en_GB localization
- [ ] Includes proper SPDX headers where applicable
+1 -1
View File
@@ -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.
## 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)
- [ ] Localization (en_US/en_GB)
- [ ] Security best practices
+1 -1
View File
@@ -35,7 +35,7 @@ Use this template only for:
<!-- Describe how this could be addressed -->
## 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
- [ ] Secret management
- [ ] Dependency security
+6 -6
View File
@@ -2,8 +2,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Automation
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# INGROUP: mokocli.Automation
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /.gitea/workflows/branch-protection.yml
# BRIEF: Apply standardised branch protection rules to all governed repositories
#
@@ -57,13 +57,13 @@ jobs:
- name: Determine target repos
id: repos
env:
GA_TOKEN: ${{ secrets.GA_TOKEN }}
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
run: |
API="${GITEA_URL}/api/v1"
# Platform/standards/infra repos to exclude
EXCLUDE="gitea-org-config org-profile gitea-private .mokogitea-private moko-platform MokoTesting"
EXCLUDE="$EXCLUDE MokoStandards-Template-Client MokoStandards-Template-Dolibarr MokoStandards-Template-Generic MokoStandards-Template-Joomla MokoDoliProjTemplate"
EXCLUDE="gitea-org-config org-profile gitea-private .mokogitea-private mokocli MokoTesting"
EXCLUDE="$EXCLUDE MokoCli-Template-Client MokoCli-Template-Dolibarr MokoCli-Template-Generic MokoCli-Template-Joomla MokoDoliProjTemplate"
if [ -n "${{ inputs.repos }}" ]; then
# User-specified repos
@@ -105,7 +105,7 @@ jobs:
- name: Apply protection rules
env:
GA_TOKEN: ${{ secrets.GA_TOKEN }}
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
DRY_RUN: ${{ inputs.dry_run || 'false' }}
run: |
API="${GITEA_URL}/api/v1"
+5 -5
View File
@@ -2,8 +2,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Automation
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# INGROUP: mokocli.Automation
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /.gitea/workflows/bulk-repo-sync.yml
# 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 }}"
php automation/bulk_sync.php ${{ steps.args.outputs.args }} 2>&1 | tee /tmp/bulk_sync.log
env:
GA_TOKEN: ${{ secrets.GA_TOKEN }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
GH_TOKEN: ${{ secrets.GH_PAT }}
GIT_PLATFORM: gitea
GITEA_URL: https://git.mokoconsulting.tech
GITEA_ORG: MokoConsulting
@@ -112,7 +112,7 @@ jobs:
bash automation/enforce_tags.sh || echo "Tag enforcement had errors (non-fatal)"
fi
env:
GA_TOKEN: ${{ secrets.GA_TOKEN }}
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
GITEA_URL: https://git.mokoconsulting.tech
GITEA_ORG: MokoConsulting
+3 -3
View File
@@ -2,9 +2,9 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: moko-platform.CI
# INGROUP: moko-platform
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# DEFGROUP: mokocli.CI
# INGROUP: mokocli
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /.gitea/workflows/pr-branch-check.yml
# BRIEF: PR branch merge policy enforcement
#
+6 -6
View File
@@ -4,8 +4,8 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Automation
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# INGROUP: mokocli.Automation
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /.gitea/workflows/renovate.yml
# BRIEF: Run Renovate Bot across all governed repos for dependency updates
#
@@ -57,12 +57,12 @@ jobs:
- name: Determine target repos
id: repos
env:
GA_TOKEN: ${{ secrets.GA_TOKEN }}
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
run: |
API="${GITEA_URL}/api/v1"
EXCLUDE="gitea-org-config org-profile gitea-private .mokogitea-private moko-platform MokoTesting"
EXCLUDE="$EXCLUDE MokoStandards-Template-Client MokoStandards-Template-Dolibarr MokoStandards-Template-Generic MokoStandards-Template-Joomla MokoDoliProjTemplate"
EXCLUDE="gitea-org-config org-profile gitea-private .mokogitea-private mokocli MokoTesting"
EXCLUDE="$EXCLUDE MokoCli-Template-Client MokoCli-Template-Dolibarr MokoCli-Template-Generic MokoCli-Template-Joomla MokoDoliProjTemplate"
if [ -n "${{ inputs.repos }}" ]; then
REPOS=$(echo "${{ inputs.repos }}" | tr ',' ' ')
@@ -107,7 +107,7 @@ jobs:
- name: Run Renovate
if: steps.repos.outputs.repo_list != ''
env:
RENOVATE_TOKEN: ${{ secrets.GA_TOKEN }}
RENOVATE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
RENOVATE_PLATFORM: gitea
RENOVATE_ENDPOINT: ${{ env.GITEA_URL }}/api/v1
RENOVATE_GIT_AUTHOR: 'Renovate Bot <renovate@mokoconsulting.tech>'
+3 -3
View File
@@ -4,8 +4,8 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Maintenance
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# INGROUP: mokocli.Maintenance
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /.gitea/workflows/sync-wikis.yml
# BRIEF: Daily sync of all Gitea wikis to consolidated GitHub wiki repo
@@ -31,7 +31,7 @@ jobs:
- name: Sync all wikis
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
GH_TOKEN: ${{ secrets.GH_PAT }}
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
run: |
if [ -z "$GH_TOKEN" ]; then
+10 -10
View File
@@ -4,10 +4,10 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# INGROUP: mokocli.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# 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)
name: "Universal: Auto Version Bump"
@@ -43,19 +43,19 @@ jobs:
token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 1
- name: Setup moko-platform tools
- name: Setup mokocli tools
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
if [ -d "/opt/moko-platform/cli" ]; then
echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV"
if [ -d "/opt/mokocli/cli" ]; then
echo "MOKO_CLI=/opt/mokocli/cli" >> "$GITHUB_ENV"
else
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/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"
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/mokocli.git" \
/tmp/mokocli
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
echo "MOKO_CLI=/tmp/mokocli/cli" >> "$GITHUB_ENV"
fi
- name: Bump version
+457 -285
View File
@@ -1,285 +1,457 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Release
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# PATH: /templates/workflows/universal/auto-release.yml.template
# VERSION: 05.00.00
# BRIEF: Universal build & release detects platform from manifest.xml
#
# +========================================================================+
# | UNIVERSAL BUILD & RELEASE PIPELINE |
# +========================================================================+
# | |
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
# | |
# | Platform-specific: |
# | joomla: XML manifest, updates.xml, type-prefixed packages |
# | dolibarr: mod*.class.php, update.txt, dev version reset |
# | generic: README-only, no update stream |
# | |
# +========================================================================+
name: "Universal: Build & Release"
on:
pull_request:
types: [opened, closed]
branches:
- main
workflow_dispatch:
inputs:
action:
description: 'Action to perform'
required: false
type: choice
default: release
options:
- release
- promote-rc
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:
# ── PR Opened → Rename branch to RC and build RC release ─────────────────────
promote-rc:
name: Promote to RC
runs-on: release
if: >-
(github.event.action == 'opened' && github.event.pull_request.merged != true) ||
(github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 1
- name: Setup moko-platform tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
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-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
- name: Rename branch to rc
run: |
php /tmp/moko-platform-api/cli/branch_rename.php \
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
--pr "${{ github.event.pull_request.number }}"
- name: Checkout rc and configure git
run: |
git fetch origin rc
git checkout rc
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: Publish RC release
run: |
php /tmp/moko-platform-api/cli/release_publish.php \
--path . --stability rc --bump minor --branch rc \
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
--skip-update-stream
- name: Summary
if: always()
run: |
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
# ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
release:
name: Build & Release Pipeline
runs-on: release
if: >-
github.event.pull_request.merged == true ||
(github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 0
- name: Configure git for bot pushes
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: Check for merge conflict markers
run: |
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
if [ -n "$CONFLICTS" ]; then
echo "::error::Merge conflict markers found — aborting release"
echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "No conflict markers found"
- name: Setup moko-platform tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
run: |
# Ensure PHP + Composer are available
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-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
- name: "Publish stable release"
run: |
php /tmp/moko-platform-api/cli/release_publish.php \
--path . --stability stable --bump minor --branch main \
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
--skip-update-stream
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
- name: "Step 9: Mirror release to GitHub"
if: >-
steps.version.outputs.skip != 'true' &&
secrets.GH_MIRROR_TOKEN != ''
continue-on-error: true
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php /tmp/moko-platform-api/cli/release_mirror.php \
--version "$VERSION" --tag "$RELEASE_TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
--branch main 2>&1 || true
echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY
# -- STEP 10: Sync main branch to GitHub mirror ----------------------------
- name: "Step 10: Push main to GitHub mirror"
if: >-
steps.version.outputs.skip != 'true' &&
secrets.GH_MIRROR_TOKEN != ''
continue-on-error: true
run: |
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
git fetch origin main --depth=1
git push github origin/main:refs/heads/main --force 2>/dev/null \
&& echo "main branch pushed to GitHub mirror" \
|| echo "WARNING: GitHub mirror push failed"
- name: "Step 11: Delete rc branch and recreate dev from main"
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
# Delete rc branch (ephemeral — created by promote-rc)
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
"${API_BASE}/branches/rc" 2>/dev/null \
&& echo "Deleted rc branch" || echo "rc branch not found"
# Delete dev branch
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
"${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
# Recreate dev from main (now includes version bump + changelog promotion)
curl -sf -X POST -H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
"${API_BASE}/branches" \
-d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY
- name: "Step 12: Create version branch from main"
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
BRANCH_NAME="version/${VERSION}"
MAIN_SHA=$(git rev-parse HEAD)
# Delete old version branch if it exists (same version re-release)
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}"
# Create version/XX.YY.ZZ from main
curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed"
echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY
# -- Dolibarr post-release: Reset dev version -----------------------------
- name: "Post-release: Reset dev version"
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php /tmp/moko-platform-api/cli/version_reset_dev.php \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
--branch dev --path . 2>&1 || true
# -- Summary --------------------------------------------------------------
- name: Pipeline Summary
if: always()
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
PLATFORM="${{ steps.platform.outputs.platform }}"
if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
else
echo "" >> $GITHUB_STEP_SUMMARY
echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
fi
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Release
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli
# PATH: /templates/workflows/universal/auto-release.yml.template
# VERSION: 05.00.00
# BRIEF: Universal build & release detects platform from manifest.xml
#
# +=======================================================================+
# | UNIVERSAL BUILD & RELEASE PIPELINE |
# +=======================================================================+
# | |
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
# | |
# | Platform-specific: |
# | joomla: XML manifest, type-prefixed packages |
# | dolibarr: mod*.class.php, update.txt, dev version reset |
# | generic: README-only, no update stream |
# | |
# +=======================================================================+
name: "Universal: Build & Release"
on:
pull_request:
types: [opened, closed]
branches:
- main
workflow_dispatch:
inputs:
action:
description: 'Action to perform'
required: false
type: choice
default: release
options:
- release
- promote-rc
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:
# ── PR Opened → Rename branch to RC and build RC release ─────────────────────────
promote-rc:
name: Promote to RC
runs-on: release
if: >-
(github.event.action == 'opened' && github.event.pull_request.merged != true) ||
(github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 1
- name: Setup mokocli tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
run: |
if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then
echo Using pre-installed /opt/mokocli
echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV
else
echo Falling back to fresh clone
if ! command -v composer > /dev/null 2>&1; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
fi
rm -rf /tmp/mokocli
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
cd /tmp/mokocli
composer install --no-dev --no-interaction --quiet
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
fi
- name: Rename branch to rc
run: |
php ${MOKO_CLI}/branch_rename.php \
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
--pr "${{ github.event.pull_request.number }}"
- name: Checkout rc and configure git
run: |
git fetch origin rc
git checkout rc
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: Publish RC release
run: |
php ${MOKO_CLI}/release_publish.php \
--path . --stability rc --bump minor --branch rc \
--token "${{ secrets.MOKOGITEA_TOKEN }}"
- name: Update RC release notes from CHANGELOG.md
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
# Extract [Unreleased] section from changelog
NOTES=""
if [ -f "CHANGELOG.md" ]; then
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
fi
[ -z "$NOTES" ] && NOTES="Release candidate"
# Find the RC release and update its body
RELEASE_ID=$(curl -sf -H "Authorization: token ${TOKEN}" \
"${API_BASE}/releases/tags/release-candidate" \
| python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
if [ -n "$RELEASE_ID" ]; then
python3 -c "
import json, urllib.request
body = open('/dev/stdin').read()
payload = json.dumps({'body': body}).encode()
req = urllib.request.Request(
'${API_BASE}/releases/${RELEASE_ID}',
data=payload, method='PATCH',
headers={
'Authorization': 'token ${TOKEN}',
'Content-Type': 'application/json'
})
urllib.request.urlopen(req)
" <<< "$NOTES"
echo "RC release notes updated from CHANGELOG.md"
fi
- name: Summary
if: always()
run: |
echo "## Promoted to Release Candidate" >> $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) ─────────────────────────
release:
name: Build & Release Pipeline
runs-on: release
if: >-
github.event.pull_request.merged == true ||
(github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 0
- name: Configure git for bot pushes
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: Check for merge conflict markers
run: |
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
if [ -n "$CONFLICTS" ]; then
echo "::error::Merge conflict markers found — aborting release"
echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "No conflict markers found"
- name: Setup mokocli tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
run: |
if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then
echo Using pre-installed /opt/mokocli
echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV
else
echo Falling back to fresh clone
if ! command -v composer > /dev/null 2>&1; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
fi
rm -rf /tmp/mokocli
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
cd /tmp/mokocli
composer install --no-dev --no-interaction --quiet
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
fi
- name: "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"
run: |
BUMP_FLAG=""
if [ "${{ steps.bump.outputs.level }}" != "none" ]; then
BUMP_FLAG="--bump ${{ steps.bump.outputs.level }}"
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"
PLATFORM="${{ steps.platform.outputs.platform }}"
if [[ "$PLATFORM" == joomla* ]]; then
echo "tag=stable" >> "$GITHUB_OUTPUT"
echo "release_tag=stable" >> "$GITHUB_OUTPUT"
else
echo "tag=v${VERSION}" >> "$GITHUB_OUTPUT"
echo "release_tag=v${VERSION}" >> "$GITHUB_OUTPUT"
fi
echo "branch=main" >> "$GITHUB_OUTPUT"
echo "Published version: ${VERSION}"
- name: "Create semver tag for non-Joomla repos"
id: semver
if: |
steps.version.outputs.skip != 'true' &&
!startsWith(steps.platform.outputs.platform, 'joomla')
run: |
VERSION="${{ steps.version.outputs.version }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
SEMVER_TAG="v${VERSION}"
echo "Creating semver tag: ${SEMVER_TAG}"
# Create the git tag via API
HTTP_CODE=$(curl -sf -o /dev/null -w "%{http_code}" \
-X POST -H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
"${API_BASE}/tags" \
-d "{\"tag_name\":\"${SEMVER_TAG}\",\"target\":\"main\",\"message\":\"Release ${VERSION}\"}" 2>/dev/null || echo "000")
if [ "$HTTP_CODE" = "201" ] || [ "$HTTP_CODE" = "200" ]; then
echo "Created semver tag: ${SEMVER_TAG}"
elif [ "$HTTP_CODE" = "409" ]; then
echo "Semver tag ${SEMVER_TAG} already exists (skipped)"
else
echo "::warning::Failed to create semver tag ${SEMVER_TAG} (HTTP ${HTTP_CODE})"
fi
echo "semver_tag=${SEMVER_TAG}" >> "$GITHUB_OUTPUT"
- 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) --------------------------------
- name: "Step 9: Mirror release to GitHub"
if: >-
steps.version.outputs.skip != 'true' &&
secrets.GH_MIRROR_TOKEN != ''
continue-on-error: true
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php ${MOKO_CLI}/release_mirror.php \
--version "$VERSION" --tag "$RELEASE_TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
--branch main 2>&1 || true
echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY
# -- STEP 10: Sync main branch to GitHub mirror ----------------------------
- name: "Step 10: Push main to GitHub mirror"
if: >-
steps.version.outputs.skip != 'true' &&
secrets.GH_MIRROR_TOKEN != ''
continue-on-error: true
run: |
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
git fetch origin main --depth=1
git push github origin/main:refs/heads/main --force 2>/dev/null \
&& echo "main branch pushed to GitHub mirror" \
|| echo "WARNING: GitHub mirror push failed"
- name: "Step 11: Delete rc branch and recreate dev from main"
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
# Delete rc branch (ephemeral — created by promote-rc)
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
"${API_BASE}/branches/rc" 2>/dev/null \
&& echo "Deleted rc branch" || echo "rc branch not found"
# Delete dev branch
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
"${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
# Recreate dev from main (now includes version bump + changelog promotion)
curl -sf -X POST -H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
"${API_BASE}/branches" \
-d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY
- name: "Step 12: Create version branch from main"
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
BRANCH_NAME="version/${VERSION}"
MAIN_SHA=$(git rev-parse HEAD)
# Delete old version branch if it exists (same version re-release)
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}"
# Create version/XX.YY.ZZ from main
curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed"
echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY
# -- Dolibarr post-release: Reset dev version -----------------------------
- name: "Post-release: Reset dev version"
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php ${MOKO_CLI}/version_reset_dev.php \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
--branch dev --path . 2>&1 || true
# -- Summary --------------------------------------------------------------
- name: Pipeline Summary
if: always()
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
PLATFORM="${{ steps.platform.outputs.platform }}"
if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
else
echo "" >> $GITHUB_STEP_SUMMARY
echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
fi
+3 -3
View File
@@ -4,10 +4,10 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoPlatform.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# INGROUP: MokoStandards.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /.mokogitea/workflows/branch-cleanup.yml
# VERSION: 09.23.00
# VERSION: 01.00.00
# BRIEF: Delete feature branches after PR merge
name: "Branch Cleanup"
+191
View File
@@ -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: MokoStandards.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
+60 -11
View File
@@ -4,18 +4,18 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.CI
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /.gitea/workflows/ci-platform.yml
# INGROUP: mokocli.CI
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /.mokogitea/workflows/ci-platform.yml
# VERSION: 09.23.00
# BRIEF: moko-platform CI — the standards engine validates itself
# BRIEF: mokocli CI — the standards engine validates itself
#
# +========================================================================+
# | MOKO-PLATFORM CI |
# +========================================================================+
# | |
# | 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: |
# | |
@@ -29,7 +29,7 @@
# | |
# +========================================================================+
name: "Platform: moko-platform CI"
name: "Platform: mokocli CI"
on:
push:
@@ -41,7 +41,7 @@ on:
paths-ignore:
- '**.md'
- 'wiki/**'
- '.gitea/ISSUE_TEMPLATE/**'
- '.mokogitea/ISSUE_TEMPLATE/**'
pull_request:
branches:
- main
@@ -104,7 +104,7 @@ jobs:
echo "::error file=${file}::PHP syntax error"
ERRORS=$((ERRORS + 1))
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"
@@ -270,7 +270,7 @@ jobs:
echo "::warning file=${file}::Missing SPDX header"
MISSING=$((MISSING + 1))
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"
@@ -289,7 +289,7 @@ jobs:
echo "::error file=${file}::Potential hardcoded secret detected"
FOUND=$((FOUND + 1))
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"
@@ -412,10 +412,16 @@ jobs:
if: always()
steps:
- name: Checkout
uses: actions/checkout@v4
with:
sparse-checkout: automation/ci-issue-reporter.sh
sparse-checkout-cone-mode: false
- name: Check gate results
run: |
{
echo "# moko-platform CI"
echo "# mokocli CI"
echo ""
echo "| Gate | Job | Status |"
echo "|---|---|---|"
@@ -437,3 +443,46 @@ jobs:
echo "::error::One or more CI gates failed"
exit 1
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."
+10 -10
View File
@@ -4,10 +4,10 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Maintenance
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# INGROUP: MokoStandards.Maintenance
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /.gitea/workflows/cleanup.yml
# VERSION: 09.23.00
# VERSION: 01.00.00
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
name: "Universal: Repository Cleanup"
@@ -33,17 +33,17 @@ jobs:
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.MOKOGITEA_TOKEN }}
token: ${{ secrets.GA_TOKEN }}
- name: Delete merged branches
env:
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
GA_TOKEN: ${{ secrets.GA_TOKEN }}
run: |
echo "=== Merged Branch Cleanup ==="
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
# 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')
DELETED=0
@@ -56,7 +56,7 @@ jobs:
# Check if branch is merged into main
if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then
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
DELETED=$((DELETED + 1))
fi
@@ -66,20 +66,20 @@ jobs:
- name: Clean old workflow runs
env:
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
GA_TOKEN: ${{ secrets.GA_TOKEN }}
run: |
echo "=== Workflow Run Cleanup ==="
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)
# 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" | \
jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null)
DELETED=0
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
DELETED=$((DELETED + 1))
done
+76
View File
@@ -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
+126
View File
@@ -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: MokoStandards.Deploy
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
# PATH: /templates/workflows/joomla/deploy-manual.yml.template
# VERSION: 04.07.00
# BRIEF: Manual SFTP deploy to dev server for Joomla repos
name: "Universal: Deploy to Dev (Manual)"
on:
workflow_dispatch:
inputs:
clear_remote:
description: 'Delete all remote files before uploading'
required: false
default: 'false'
type: boolean
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
permissions:
contents: read
jobs:
deploy:
name: SFTP Deploy to Dev
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup PHP
run: |
php -v && composer --version
- name: Setup MokoStandards tools
env:
GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
run: |
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
/tmp/mokostandards-api 2>/dev/null || true
if [ -d "/tmp/mokostandards-api" ] && [ -f "/tmp/mokostandards-api/composer.json" ]; then
cd /tmp/mokostandards-api && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
fi
- name: Check FTP configuration
id: check
env:
HOST: ${{ vars.DEV_FTP_HOST }}
PATH_VAR: ${{ vars.DEV_FTP_PATH }}
PORT: ${{ vars.DEV_FTP_PORT }}
run: |
if [ -z "$HOST" ] || [ -z "$PATH_VAR" ]; then
echo "DEV_FTP_HOST or DEV_FTP_PATH not configured -- cannot deploy"
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "skip=false" >> "$GITHUB_OUTPUT"
echo "host=$HOST" >> "$GITHUB_OUTPUT"
REMOTE="${PATH_VAR%/}"
echo "remote=$REMOTE" >> "$GITHUB_OUTPUT"
[ -z "$PORT" ] && PORT="22"
echo "port=$PORT" >> "$GITHUB_OUTPUT"
- name: Deploy via SFTP
if: steps.check.outputs.skip != 'true'
env:
SFTP_KEY: ${{ secrets.DEV_FTP_KEY }}
SFTP_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
SFTP_USER: ${{ vars.DEV_FTP_USERNAME }}
run: |
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
[ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ -- nothing to deploy"; exit 0; }
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
"${{ steps.check.outputs.host }}" "${{ steps.check.outputs.port }}" "$SFTP_USER" "${{ steps.check.outputs.remote }}" \
> /tmp/sftp-config.json
if [ -n "$SFTP_KEY" ]; then
echo "$SFTP_KEY" > /tmp/deploy_key
chmod 600 /tmp/deploy_key
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
else
printf ',"password":"%s"}' "$SFTP_PASS" >> /tmp/sftp-config.json
fi
DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json)
[ "${{ inputs.clear_remote }}" = "true" ] && DEPLOY_ARGS+=(--clear-remote)
PLATFORM=$(php /tmp/mokostandards-api/cli/platform_detect.php --path . 2>/dev/null || true)
if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards-api/deploy/deploy-joomla.php" ]; then
php /tmp/mokostandards-api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}"
else
php /tmp/mokostandards-api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}"
fi
rm -f /tmp/deploy_key /tmp/sftp-config.json
- name: Summary
if: always()
run: |
if [ "${{ steps.check.outputs.skip }}" = "true" ]; then
echo "### Deploy Skipped -- FTP not configured" >> $GITHUB_STEP_SUMMARY
else
echo "### Manual Dev Deploy Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Host | \`${{ steps.check.outputs.host }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Remote | \`${{ steps.check.outputs.remote }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Clear | ${{ inputs.clear_remote }} |" >> $GITHUB_STEP_SUMMARY
fi
+3 -7
View File
@@ -4,10 +4,10 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Security
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# INGROUP: MokoStandards.Security
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
# PATH: /templates/workflows/gitleaks.yml.template
# VERSION: 09.23.00
# VERSION: 01.00.00
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
#
# +========================================================================+
@@ -25,10 +25,6 @@
name: "Universal: Secret Scanning"
on:
pull_request:
branches:
- main
- 'dev/**'
schedule:
- cron: '0 5 * * 1' # Weekly Monday 05:00 UTC
workflow_dispatch:
+3 -3
View File
@@ -4,8 +4,8 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Automation
# VERSION: 09.23.00
# INGROUP: mokocli.Automation
# VERSION: 01.00.00
# BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch"
@@ -28,7 +28,7 @@ jobs:
steps:
- name: Create branch and comment
run: |
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
TOKEN="${{ secrets.GA_TOKEN }}"
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
ISSUE_NUM="${{ github.event.issue.number }}"
ISSUE_TITLE="${{ github.event.issue.title }}"
+3 -3
View File
@@ -4,10 +4,10 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Notifications
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# INGROUP: MokoStandards.Notifications
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /.gitea/workflows/notify.yml
# VERSION: 09.23.00
# VERSION: 01.00.00
# BRIEF: Push notifications via ntfy on release success or workflow failure
name: "Universal: Notifications"
+26
View File
@@ -96,6 +96,32 @@ jobs:
echo "Branch policy: OK (${HEAD} → ${BASE})"
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 ────────────────────────────────────────────────────
validate:
name: Validate PR
+117 -33
View File
@@ -4,19 +4,26 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# INGROUP: mokocli.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /templates/workflows/universal/pre-release.yml.template
# VERSION: 09.23.00
# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch
# VERSION: 05.01.00
# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches
name: "Universal: Pre-Release"
on:
pull_request:
types: [closed]
push:
branches:
- dev
- 'fix/**'
- 'patch/**'
- 'hotfix/**'
- 'bugfix/**'
- 'chore/**'
- alpha
- beta
- rc
workflow_dispatch:
inputs:
stability:
@@ -39,11 +46,11 @@ env:
jobs:
build:
name: "Build Pre-Release (${{ inputs.stability || 'development' }})"
name: "Build Pre-Release (${{ inputs.stability || github.ref_name }})"
runs-on: release
if: >-
github.event_name == 'workflow_dispatch' ||
(github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev')
github.event_name == 'push'
steps:
- name: Checkout
@@ -51,56 +58,93 @@ jobs:
with:
fetch-depth: 0
token: ${{ secrets.MOKOGITEA_TOKEN }}
ref: ${{ github.ref_name }}
- name: Setup moko-platform tools
- name: Setup mokocli tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
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
# Use pre-installed /opt/mokocli if available (updated by cron every 6h)
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
# 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
id: platform
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
- name: Check platform eligibility (Joomla only)
id: eligibility
run: |
PLATFORM="${{ steps.platform.outputs.platform }}"
if [[ "$PLATFORM" == joomla* ]] || [[ "$PLATFORM" == "joomla" ]]; then
echo "proceed=true" >> "$GITHUB_OUTPUT"
else
echo "proceed=false" >> "$GITHUB_OUTPUT"
echo "::notice::Platform '$PLATFORM' — non-Joomla, skipping pre-release auto-bump"
fi
- name: Resolve metadata and bump version
id: meta
if: steps.eligibility.outputs.proceed == 'true'
run: |
STABILITY="${{ inputs.stability || 'development' }}"
# 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' }}"
fi
case "$STABILITY" in
development) TAG="development" ;;
alpha) TAG="alpha" ;;
beta) TAG="beta" ;;
release-candidate) TAG="release-candidate" ;;
development) SUFFIX="-dev"; TAG="development" ;;
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
beta) SUFFIX="-beta"; TAG="beta" ;;
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
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
release-candidate) php ${MOKO_CLI}/version_bump.php --path . --minor 2>/dev/null || true ;;
*) php ${MOKO_CLI}/version_bump.php --path . 2>/dev/null || true ;;
release-candidate) BUMP="minor" ;;
*) BUMP="patch" ;;
esac
# Set stability suffix and fix consistency
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo '00.00.01')
php ${MOKO_CLI}/version_bump.php --path . $([ "$BUMP" = "minor" ] && echo "--minor") 2>/dev/null || true
# 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\)$//')
php ${MOKO_CLI}/version_set_platform.php \
--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
# Read final version with suffix
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null)
[ -z "$VERSION" ] && VERSION="00.00.01"
# Ensure licensing tags (updateservers, dlid) if enabled in manifest.xml
php ${MOKO_CLI}/manifest_licensing.php --path . --fix 2>/dev/null || true
# Append suffix for output
if [ -n "$SUFFIX" ]; then
VERSION="${VERSION}${SUFFIX}"
fi
# Commit version bump
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
@@ -125,14 +169,16 @@ jobs:
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "zip_name=${ZIP_NAME}" >> "$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
id: release
if: steps.eligibility.outputs.proceed == 'true'
run: |
TAG="${{ steps.meta.outputs.tag }}"
VERSION="${{ steps.meta.outputs.version }}"
@@ -140,10 +186,47 @@ jobs:
php ${MOKO_CLI}/release_create.php \
--path . --version "$VERSION" --tag "$TAG" \
--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
if: steps.eligibility.outputs.proceed == 'true'
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
id: package
if: steps.eligibility.outputs.proceed == 'true'
run: |
VERSION="${{ steps.meta.outputs.version }}"
TAG="${{ steps.meta.outputs.tag }}"
@@ -157,6 +240,7 @@ jobs:
# No need to build, commit, or sync updates.xml from workflows
- name: "Delete lesser pre-release channels (cascade)"
if: steps.eligibility.outputs.proceed == 'true'
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
+66
View File
@@ -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
+4 -3
View File
@@ -7,8 +7,8 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Validation
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# INGROUP: mokocli.Validation
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli
# PATH: /templates/workflows/joomla/repo_health.yml.template
# VERSION: 09.23.00
# BRIEF: Enforces repository guardrails by validating scripts governance, tooling availability, and core repository health artifacts.
@@ -33,7 +33,8 @@ on:
- scripts
- repo
pull_request:
push:
branches:
- main
permissions:
contents: read
+3 -19
View File
@@ -4,10 +4,10 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Security
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# INGROUP: MokoStandards.Security
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /.gitea/workflows/security-audit.yml
# VERSION: 09.23.00
# VERSION: 01.00.00
# BRIEF: Dependency vulnerability scanning for composer and npm packages
name: "Universal: Security Audit"
@@ -80,19 +80,3 @@ jobs:
-H "Priority: high" \
-d "Security audit found vulnerabilities. Review dependency updates." \
"${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
-302
View File
@@ -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 -1
View File
@@ -1,7 +1,7 @@
{
"metadata": {
"generated_at": "2026-03-10T19:51:42.238134Z",
"repository": "MokoConsulting/moko-platform",
"repository": "MokoConsulting/mokocli",
"version": "1.0.0"
},
"scripts": [
+9 -22
View File
@@ -2,37 +2,24 @@
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
SPDX-License-Identifier: GPL-3.0-or-later
FILE INFORMATION
DEFGROUP: MokoStandards.Root
INGROUP: MokoStandards
REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
DEFGROUP: MokoCli.Root
INGROUP: MokoCli
REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
PATH: /CHANGELOG.md
BRIEF: Release changelog
-->
# Changelog
## [Unreleased]
## [09.23] --- 2026-05-31
## [09.38.00] --- 2026-06-21
## [09.22] --- 2026-05-31
## [09.38.00] --- 2026-06-21
### Changed
- **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
## [09.38.00] --- 2026-06-21
### Fixed
- 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.38.00] --- 2026-06-21
## [09.21] --- 2026-05-30
## [09.37.00] --- 2026-06-21
## [09.20] --- 2026-05-30
## [09.19] --- 2026-05-30
## [09.18] --- 2026-05-30
## [09.17] --- 2026-05-30
## [09.37.00] --- 2026-06-21
-102
View File
@@ -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
+161 -161
View File
@@ -1,161 +1,161 @@
# Contributing to Moko Consulting Projects
Thank you for your interest in contributing. All Moko Consulting repositories follow this universal workflow and version policy.
## Branching Workflow
```
feature/* ──PR──> dev ──draft PR──> (renamed to rc) ──merge──> main
```
### Step by step
1. **Create a feature branch** from `dev`:
```bash
git checkout dev && git pull
git checkout -b feature/my-change
```
2. **Work and commit** on your feature branch. Push to origin.
3. **Open a PR**: `feature/my-change` → `dev`. After review and checks, merge it.
4. **When ready for release**, open a **draft PR**: `dev` → `main`.
- This automatically renames the source branch to `rc` (release candidate)
- An RC pre-release is built and uploaded
5. **Alpha and beta branches** are created by manually renaming the branch before the RC stage:
- Rename `dev` to `alpha` for early testing → alpha pre-release is built
- Rename `alpha` to `beta` for feature-complete testing → beta pre-release is built
- When the draft PR is created, the branch is renamed to `rc`
6. **Once PR checks pass** on the `rc` branch, mark the PR as ready and merge to `main`.
7. **Merging to main** triggers the stable release pipeline:
- Minor version bump (e.g., `02.09.xx` → `02.10.00`)
- Stability suffix stripped (clean version)
- Gitea release created with ZIP/tar.gz packages
- `updates.xml` updated (Joomla extensions)
- `dev` branch recreated from `main`
### Branch summary
| Branch | Purpose | Created by |
|--------|---------|-----------|
| `feature/*` | New features and fixes | Developer |
| `dev` | Integration branch | Auto-recreated after release |
| `alpha` | Alpha pre-release testing | Manual rename from `dev` |
| `beta` | Beta pre-release testing | Manual rename from `alpha` |
| `rc` | Release candidate | Auto-renamed on draft PR to main |
| `main` | Stable releases | Protected, merge only |
| `version/XX.YY.ZZ` | Archived release snapshots | Auto-created by CI |
### Protected branches
| Branch | Direct push | Merge via |
|--------|------------|-----------|
| `main` | Blocked (CI bot whitelisted) | PR merge only |
| `dev` | Blocked (CI bot whitelisted) | PR merge from feature/* |
| `rc` | Blocked (CI bot whitelisted) | Auto-created on draft PR |
| `alpha` | Blocked (CI bot whitelisted) | Manual rename |
| `beta` | Blocked (CI bot whitelisted) | Manual rename |
| `feature/*` | Open | N/A (source branch) |
## Version Policy
### Format
All versions use `XX.YY.ZZ` — three two-digit segments, zero-padded:
- **XX** — Major version (breaking changes)
- **YY** — Minor version (new features, bumped on release to main)
- **ZZ** — Patch version (auto-incremented on every push to dev/feature branches)
Rollover: patch `99` → `00` increments minor; minor `99` → `00` increments major.
### Stability suffixes
Each branch appends a suffix to indicate stability:
| Branch | Suffix | Example |
|--------|--------|---------|
| `main` | (none) | `02.09.00` |
| `dev` | `-dev` | `02.09.01-dev` |
| `feature/*` | `-dev` | `02.09.01-dev` |
| `alpha` | `-alpha` | `02.09.01-alpha` |
| `beta` | `-beta` | `02.09.01-beta` |
| `rc` | `-rc` | `02.09.01-rc` |
### Auto version bump
On every push to `dev`, `feature/*`, or `patch/*`:
1. Patch version incremented
2. Stability suffix `-dev` applied
3. All version-bearing files updated (manifests, CHANGELOG, PHP headers, etc.)
4. Commit created with `[skip ci]` to avoid loops
### Release version flow
Version bumps happen at specific release events:
| Event | Bump | Example |
|-------|------|---------|
| Feature merged to dev | Patch bump after dev release | `02.09.01-dev` → release → `02.09.02-dev` |
| Dev promoted to RC | Minor bump | `02.09.02-dev` → `02.10.00-rc` |
| RC merged to main | Minor bump | `02.10.00-rc` → `02.11.00` (stable) |
| Dev recreated from main | Patch bump | `02.11.00` → `02.11.01-dev` |
### Release stream copies
When a higher-stability release is published, copies are created for all lesser streams with the same base version:
- **RC `02.10.00-rc`** also creates: `02.10.00-dev`, `02.10.00-alpha`, `02.10.00-beta`
- **Stable `02.11.00`** also creates: `02.11.00-dev`, `02.11.00-alpha`, `02.11.00-beta`, `02.11.00-rc`
This ensures Joomla sites on ANY stability channel see the update (Joomla only shows versions higher than what's installed).
### Version files
The version tools update all files containing version stamps:
- `.mokogitea/manifest.xml` (canonical source)
- Joomla XML manifests (`<version>` tag)
- `README.md`, `CHANGELOG.md` (`VERSION:` pattern)
- `package.json`, `pyproject.toml`
- Any text file with a `VERSION: XX.YY.ZZ` label
Files synced from other repos (with a `# REPO:` header) are not touched.
## Code Standards
- **PHP**: PSR-12, tabs for indentation
- **Copyright**: all files must include the Moko Consulting copyright header
- **License**: SPDX identifier `GPL-3.0-or-later` (or as specified per repo)
- **Attribution**: use `Authored-by: Moko Consulting` in commits, not individual names
## Commit Messages
Use conventional commit format:
```
type(scope): short description
Optional body with context.
Authored-by: Moko Consulting
```
Types: `feat`, `fix`, `chore`, `docs`, `style`, `refactor`, `test`, `ci`
Special flags in commit messages:
- `[skip ci]` — skip all CI workflows
- `[skip bump]` — skip auto version bump only
## Reporting Issues
Use the repository's issue tracker with the appropriate template.
---
*Moko Consulting <hello@mokoconsulting.tech>*
# Contributing to Moko Consulting Projects
Thank you for your interest in contributing. All Moko Consulting repositories follow this universal workflow and version policy.
## Branching Workflow
```
feature/* ──PR──> dev ──draft PR──> (renamed to rc) ──merge──> main
```
### Step by step
1. **Create a feature branch** from `dev`:
```bash
git checkout dev && git pull
git checkout -b feature/my-change
```
2. **Work and commit** on your feature branch. Push to origin.
3. **Open a PR**: `feature/my-change` → `dev`. After review and checks, merge it.
4. **When ready for release**, open a **draft PR**: `dev` → `main`.
- This automatically renames the source branch to `rc` (release candidate)
- An RC pre-release is built and uploaded
5. **Alpha and beta branches** are created by manually renaming the branch before the RC stage:
- Rename `dev` to `alpha` for early testing → alpha pre-release is built
- Rename `alpha` to `beta` for feature-complete testing → beta pre-release is built
- When the draft PR is created, the branch is renamed to `rc`
6. **Once PR checks pass** on the `rc` branch, mark the PR as ready and merge to `main`.
7. **Merging to main** triggers the stable release pipeline:
- Minor version bump (e.g., `02.09.xx` → `02.10.00`)
- Stability suffix stripped (clean version)
- Gitea release created with ZIP/tar.gz packages
- `updates.xml` updated (Joomla extensions)
- `dev` branch recreated from `main`
### Branch summary
| Branch | Purpose | Created by |
|--------|---------|-----------|
| `feature/*` | New features and fixes | Developer |
| `dev` | Integration branch | Auto-recreated after release |
| `alpha` | Alpha pre-release testing | Manual rename from `dev` |
| `beta` | Beta pre-release testing | Manual rename from `alpha` |
| `rc` | Release candidate | Auto-renamed on draft PR to main |
| `main` | Stable releases | Protected, merge only |
| `version/XX.YY.ZZ` | Archived release snapshots | Auto-created by CI |
### Protected branches
| Branch | Direct push | Merge via |
|--------|------------|-----------|
| `main` | Blocked (CI bot whitelisted) | PR merge only |
| `dev` | Blocked (CI bot whitelisted) | PR merge from feature/* |
| `rc` | Blocked (CI bot whitelisted) | Auto-created on draft PR |
| `alpha` | Blocked (CI bot whitelisted) | Manual rename |
| `beta` | Blocked (CI bot whitelisted) | Manual rename |
| `feature/*` | Open | N/A (source branch) |
## Version Policy
### Format
All versions use `XX.YY.ZZ` — three two-digit segments, zero-padded:
- **XX** — Major version (breaking changes)
- **YY** — Minor version (new features, bumped on release to main)
- **ZZ** — Patch version (auto-incremented on every push to dev/feature branches)
Rollover: patch `99` → `00` increments minor; minor `99` → `00` increments major.
### Stability suffixes
Each branch appends a suffix to indicate stability:
| Branch | Suffix | Example |
|--------|--------|---------|
| `main` | (none) | `02.09.00` |
| `dev` | `-dev` | `02.09.01-dev` |
| `feature/*` | `-dev` | `02.09.01-dev` |
| `alpha` | `-alpha` | `02.09.01-alpha` |
| `beta` | `-beta` | `02.09.01-beta` |
| `rc` | `-rc` | `02.09.01-rc` |
### Auto version bump
On every push to `dev`, `feature/*`, or `patch/*`:
1. Patch version incremented
2. Stability suffix `-dev` applied
3. All version-bearing files updated (manifests, CHANGELOG, PHP headers, etc.)
4. Commit created with `[skip ci]` to avoid loops
### Release version flow
Version bumps happen at specific release events:
| Event | Bump | Example |
|-------|------|---------|
| Feature merged to dev | Patch bump after dev release | `02.09.01-dev` → release → `02.09.02-dev` |
| Dev promoted to RC | Minor bump | `02.09.02-dev` → `02.10.00-rc` |
| RC merged to main | Minor bump | `02.10.00-rc` → `02.11.00` (stable) |
| Dev recreated from main | Patch bump | `02.11.00` → `02.11.01-dev` |
### Release stream copies
When a higher-stability release is published, copies are created for all lesser streams with the same base version:
- **RC `02.10.00-rc`** also creates: `02.10.00-dev`, `02.10.00-alpha`, `02.10.00-beta`
- **Stable `02.11.00`** also creates: `02.11.00-dev`, `02.11.00-alpha`, `02.11.00-beta`, `02.11.00-rc`
This ensures Joomla sites on ANY stability channel see the update (Joomla only shows versions higher than what's installed).
### Version files
The version tools update all files containing version stamps:
- `.mokogitea/manifest.xml` (canonical source)
- Joomla XML manifests (`<version>` tag)
- `README.md`, `CHANGELOG.md` (`VERSION:` pattern)
- `package.json`, `pyproject.toml`
- Any text file with a `VERSION: XX.YY.ZZ` label
Files synced from other repos (with a `# REPO:` header) are not touched.
## Code Standards
- **PHP**: PSR-12, tabs for indentation
- **Copyright**: all files must include the Moko Consulting copyright header
- **License**: SPDX identifier `GPL-3.0-or-later` (or as specified per repo)
- **Attribution**: use `Authored-by: Moko Consulting` in commits, not individual names
## Commit Messages
Use conventional commit format:
```
type(scope): short description
Optional body with context.
Authored-by: Moko Consulting
```
Types: `feat`, `fix`, `chore`, `docs`, `style`, `refactor`, `test`, `ci`
Special flags in commit messages:
- `[skip ci]` — skip all CI workflows
- `[skip bump]` — skip auto version bump only
## Reporting Issues
Use the repository's issue tracker with the appropriate template.
---
*Moko Consulting <hello@mokoconsulting.tech>*
+2 -2
View File
@@ -4,14 +4,14 @@ SPDX-License-Identifier: GPL-3.0-or-later
FILE INFORMATION
DEFGROUP: MokoPlatform.Root
INGROUP: MokoPlatform
REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
PATH: /PLUGIN_SCRIPTS.md
BRIEF: Plugin system CLI documentation
-->
# 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
+6 -6
View File
@@ -4,20 +4,20 @@ SPDX-License-Identifier: GPL-3.0-or-later
FILE INFORMATION
DEFGROUP: MokoPlatform.Root
INGROUP: MokoPlatform
REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
PATH: /README.md
VERSION: 09.23.00
VERSION: 09.38.00
BRIEF: Project overview and documentation
-->
# moko-platform Enterprise API
# mokocli Enterprise API
![Version](https://img.shields.io/badge/version-09.01.00-blue) ![PHP](https://img.shields.io/badge/PHP-8.1%2B-777BB4) ![License](https://img.shields.io/badge/license-GPL--3.0--or--later-green)
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)
> **Backup mirror**: [GitHub](https://github.com/MokoConsulting/MokoStandards-API) *(read-only mirror)*
> **Primary platform**: [Gitea — git.mokoconsulting.tech](https://git.mokoconsulting.tech/MokoConsulting/MokoCli-API)
> **Backup mirror**: [GitHub](https://github.com/MokoConsulting/MokoCli-API) *(read-only mirror)*
## What Lives Here
+1 -1
View File
@@ -4,7 +4,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
FILE INFORMATION
DEFGROUP: MokoPlatform.Index
INGROUP: MokoPlatform.Analysis
REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
PATH: /analysis/index.md
BRIEF: Analysis directory index
-->
-937
View File
@@ -1,937 +0,0 @@
#!/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/moko-platform
* PATH: /automation/bulk_joomla_template.php
* BRIEF: Bulk scaffold and sync Joomla template repositories
*
* USAGE
* php automation/bulk_joomla_template.php --scaffold --name=MokoTheme
* php automation/bulk_joomla_template.php --scaffold --name=MokoTheme --client=administrator
* php automation/bulk_joomla_template.php --sync --repos=MokoTheme,MokoDarkTheme
* php automation/bulk_joomla_template.php --sync --all
* php automation/bulk_joomla_template.php --list
*/
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoEnterprise\{
AuditLogger,
CliFramework,
Config,
GitPlatformAdapter,
MetricsCollector,
PlatformAdapterFactory
};
/**
* Bulk Joomla Template Manager
*
* Provides three operations for Joomla template projects:
* --scaffold: Create a new template repository with the full directory structure
* --sync: Push moko-platform files to existing template repositories
* --list: List all repositories tagged as joomla-template
*
* Works with both GitHub and Gitea via the PlatformAdapterFactory.
*/
class BulkJoomlaTemplate extends CliFramework
{
public const DEFAULT_ORG = 'MokoConsulting';
public const VERSION = '09.23.00';
private GitPlatformAdapter $adapter;
private Config $config;
protected function configure(): void
{
$this->setDescription('Bulk Joomla template management');
$this->addArgument('--org', 'Organization', self::DEFAULT_ORG);
$this->addArgument('--scaffold', 'Create new template repo', false);
$this->addArgument('--sync', 'Sync files to template repos', false);
$this->addArgument('--list', 'List template repos', false);
$this->addArgument('--name', 'Template name for scaffold', '');
$this->addArgument('--client', 'Joomla client: site or admin', 'site');
$this->addArgument('--repos', 'Target repos (comma-separated)', '');
$this->addArgument('--all', 'Sync all tagged repos', false);
$this->addArgument('--sync-updates', 'Sync updates.xml', false);
$this->addArgument('--private', 'Create as private repo', false);
$this->addArgument('--yes', 'Auto-confirm', false);
}
protected function run(): int
{
$this->log("🎨 Joomla Template Manager v" . self::VERSION, 'INFO');
$this->config = Config::load();
try {
$this->adapter = PlatformAdapterFactory::create($this->config);
} catch (\Exception $e) {
$this->log("❌ Failed to initialize: " . $e->getMessage(), 'ERROR');
return 1;
}
$org = $this->getArgument('--org', self::DEFAULT_ORG);
$platform = $this->adapter->getPlatformName();
$this->log("Platform: {$platform} | Organization: {$org}", 'INFO');
if ($this->getArgument('--list', false)) {
return $this->listTemplateRepos($org);
}
if ($this->getArgument('--scaffold', false)) {
return $this->scaffoldTemplate($org);
}
if ($this->getArgument('--sync', false)) {
return $this->syncTemplates($org);
}
if ($this->getArgument('--sync-updates', false)) {
return $this->syncUpdatesBetweenPlatforms($org);
}
$this->log("❌ Specify --scaffold, --sync, --sync-updates, or --list", 'ERROR');
return 1;
}
// ── List ─────────────────────────────────────────────────────────────
private function listTemplateRepos(string $org): int
{
$repos = $this->findTemplateRepos($org);
if (empty($repos)) {
$this->log("No joomla-template repositories found in {$org}", 'INFO');
return 0;
}
$this->log("\nJoomla template repositories in {$org}:", 'INFO');
foreach ($repos as $repo) {
$vis = ($repo['private'] ?? false) ? 'private' : 'public';
$url = $this->adapter->getRepoWebUrl($org, $repo['name']);
$this->log(" - {$repo['name']} ({$vis}) {$url}", 'INFO');
}
$this->log("\nTotal: " . count($repos), 'INFO');
return 0;
}
// ── Scaffold ─────────────────────────────────────────────────────────
private function scaffoldTemplate(string $org): int
{
$name = $this->getArgument('--name', '');
$client = $this->getArgument('--client', 'site');
$dryRun = $this->dryRun;
if (empty($name)) {
$this->log("❌ --name is required for --scaffold", 'ERROR');
$this->log(" Example: --name=MokoTheme", 'ERROR');
return 1;
}
if (!in_array($client, ['site', 'administrator'], true)) {
$this->log("❌ --client must be 'site' or 'administrator'", 'ERROR');
return 1;
}
$shortName = $this->deriveShortName($name);
$this->log("\nScaffolding Joomla template:", 'INFO');
$this->log(" Name: {$name}", 'INFO');
$this->log(" Short name: {$shortName}", 'INFO');
$this->log(" Client: {$client}", 'INFO');
$this->log(" Element: tpl_{$shortName}", 'INFO');
if ($dryRun) {
$this->log("\n[DRY RUN] Would create repository and scaffold files", 'INFO');
$this->printScaffoldPlan($shortName);
return 0;
}
// Check if repo already exists
try {
$this->adapter->getRepo($org, $name);
$this->log("❌ Repository {$org}/{$name} already exists", 'ERROR');
return 1;
} catch (\Exception $e) {
$this->adapter->getApiClient()->resetCircuitBreaker();
}
// Confirm
if (!$this->getArgument('--yes', false)) {
echo "\nCreate repository {$org}/{$name}? [y/N]: ";
$handle = fopen('php://stdin', 'r');
$line = fgets($handle);
if ($handle) {
fclose($handle);
}
if (!is_string($line) || strtolower(trim($line)) !== 'y') {
$this->log("Cancelled.", 'INFO');
return 0;
}
}
// Create repository
$this->log("\nCreating repository...", 'INFO');
try {
$isPrivate = $this->getArgument('--private', false);
$this->adapter->createOrgRepo($org, $name, [
'description' => "Joomla {$client} template — {$name}",
'private' => $isPrivate,
'auto_init' => true,
]);
$this->log(" ✓ Repository created: {$org}/{$name}", 'INFO');
} catch (\Exception $e) {
$this->log("❌ Failed to create repository: " . $e->getMessage(), 'ERROR');
return 1;
}
// Set topics
try {
$this->adapter->setRepoTopics($org, $name, [
'joomla', 'joomla-template', 'template', "joomla-{$client}",
]);
$this->log(" ✓ Topics set", 'INFO');
} catch (\Exception $e) {
$this->log(" ⚠️ Could not set topics: " . $e->getMessage(), 'WARN');
$this->adapter->getApiClient()->resetCircuitBreaker();
}
// Scaffold files
$this->log("\nScaffolding template files...", 'INFO');
$files = $this->getScaffoldFiles($name, $shortName, $client, $org);
$created = 0;
foreach ($files as $path => $content) {
try {
$this->adapter->createOrUpdateFile(
$org,
$name,
$path,
$content,
"chore: scaffold {$path}"
);
$this->log("{$path}", 'INFO');
$created++;
} catch (\Exception $e) {
$this->log("{$path}: " . $e->getMessage(), 'ERROR');
$this->adapter->getApiClient()->resetCircuitBreaker();
}
}
// Apply branch protection
try {
$this->adapter->setBranchProtection($org, $name, 'main', [
'required_reviews' => 1,
'dismiss_stale' => true,
'block_on_rejected' => true,
]);
$this->log(" ✓ Branch protection applied", 'INFO');
} catch (\Exception $e) {
$this->log(" ⚠️ Branch protection: " . $e->getMessage(), 'WARN');
$this->adapter->getApiClient()->resetCircuitBreaker();
}
$url = $this->adapter->getRepoWebUrl($org, $name);
$this->log("\n✅ Template scaffolded: {$url}", 'INFO');
$this->log(" {$created} files created", 'INFO');
return 0;
}
// ── Sync ─────────────────────────────────────────────────────────────
private function syncTemplates(string $org): int
{
$repos = [];
if ($this->getArgument('--all', false)) {
$repos = $this->findTemplateRepos($org);
} else {
$reposArg = $this->getArgument('--repos', '');
if (empty($reposArg)) {
$this->log("❌ --repos or --all required for --sync", 'ERROR');
return 1;
}
$names = array_filter(array_map('trim', explode(',', $reposArg)));
foreach ($names as $name) {
$repos[] = ['name' => $name];
}
}
if (empty($repos)) {
$this->log("No template repositories to sync", 'INFO');
return 0;
}
$this->log("\nSyncing " . count($repos) . " template repo(s)...", 'INFO');
$dryRun = $this->dryRun;
$success = 0;
$failed = 0;
foreach ($repos as $repo) {
$name = $repo['name'];
$this->log("\n[{$name}]", 'INFO');
try {
$repoData = $this->adapter->getRepo($org, $name);
$shortName = $this->deriveShortName($name);
$branch = $repoData['default_branch'] ?? 'main';
$syncFiles = $this->getSyncFiles($name, $shortName);
$updated = 0;
foreach ($syncFiles as $path => $content) {
if ($dryRun) {
$this->log(" (dry-run) {$path}", 'INFO');
$updated++;
continue;
}
// Check if file exists
$existingSha = null;
try {
$existing = $this->adapter->getFileContents($org, $name, $path, $branch);
$existingSha = $existing['sha'] ?? null;
} catch (\Exception $e) {
$this->adapter->getApiClient()->resetCircuitBreaker();
}
try {
$this->adapter->createOrUpdateFile(
$org,
$name,
$path,
$content,
"chore: update {$path} from moko-platform",
$existingSha,
$branch
);
$this->log("{$path}", 'INFO');
$updated++;
} catch (\Exception $e) {
$this->log("{$path}: " . $e->getMessage(), 'ERROR');
$this->adapter->getApiClient()->resetCircuitBreaker();
}
}
$this->log(" {$updated} file(s) synced", 'INFO');
$success++;
} catch (\Exception $e) {
$this->log("{$name}: " . $e->getMessage(), 'ERROR');
$failed++;
$this->adapter->getApiClient()->resetCircuitBreaker();
}
}
$this->log("\n" . str_repeat('=', 50), 'INFO');
$this->log("Sync complete: {$success} succeeded, {$failed} failed", 'INFO');
return $failed > 0 ? 1 : 0;
}
// ── Helpers ──────────────────────────────────────────────────────────
private function findTemplateRepos(string $org): array
{
$allRepos = $this->adapter->listOrgRepos($org, true);
$templates = [];
foreach ($allRepos as $repo) {
try {
$topics = $this->adapter->getRepoTopics($org, $repo['name']);
if (in_array('joomla-template', $topics, true)) {
$templates[] = $repo;
}
} catch (\Exception $e) {
$this->adapter->getApiClient()->resetCircuitBreaker();
}
}
return $templates;
}
private function deriveShortName(string $name): string
{
// MokoTheme → mokotheme, Moko-Dark-Theme → mokodarktheme
return strtolower(preg_replace('/[^a-zA-Z0-9]/', '', $name));
}
private function printScaffoldPlan(string $shortName): void
{
$files = [
'templateDetails.xml',
'updates.xml',
'src/index.php',
'src/error.php',
'src/offline.php',
'src/component.php',
'src/html/index.html',
'src/css/.gitkeep',
'src/js/.gitkeep',
'src/images/.gitkeep',
"src/language/en-GB/tpl_{$shortName}.ini",
"src/language/en-GB/tpl_{$shortName}.sys.ini",
'media/css/.gitkeep',
'media/js/.gitkeep',
'media/images/.gitkeep',
'media/scss/.gitkeep',
'.editorconfig',
];
$this->log("\nFiles that would be created:", 'INFO');
foreach ($files as $f) {
$this->log(" + {$f}", 'INFO');
}
}
/**
* Generate the full set of scaffold files for a new template.
*
* @return array<string, string> path => content
*/
private function getScaffoldFiles(string $name, string $shortName, string $client, string $org): array
{
$element = "tpl_{$shortName}";
$now = date('Y-m-d');
$files = [];
// templateDetails.xml
$files['templateDetails.xml'] = <<<XML
<?xml version="1.0" encoding="utf-8"?>
<extension type="template" client="{$client}" method="upgrade">
<name>{$name}</name>
<creationDate>{$now}</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
<license>GPL-3.0-or-later</license>
<version>1.0.0</version>
<description>{$name} — Joomla {$client} template by Moko Consulting</description>
<files>
<filename>index.php</filename>
<filename>component.php</filename>
<filename>error.php</filename>
<filename>offline.php</filename>
<filename>templateDetails.xml</filename>
<folder>html</folder>
<folder>css</folder>
<folder>js</folder>
<folder>images</folder>
<folder>language</folder>
</files>
<media destination="templates/{$client}/{$shortName}" folder="media">
<folder>css</folder>
<folder>js</folder>
<folder>images</folder>
<folder>scss</folder>
</media>
<positions>
<position>topbar</position>
<position>navbar</position>
<position>hero</position>
<position>breadcrumbs</position>
<position>sidebar-left</position>
<position>sidebar-right</position>
<position>main-top</position>
<position>main-bottom</position>
<position>footer</position>
<position>debug</position>
</positions>
<updateservers>
<server type="extension" priority="1" name="{$name} Update Server">
https://git.mokoconsulting.tech/{$org}/{$name}/raw/branch/main/updates.xml
</server>
<server type="extension" priority="2" name="{$name} Update Server">
https://raw.githubusercontent.com/{$org}/{$name}/main/updates.xml
</server>
</updateservers>
<config>
<fields name="params">
<fieldset name="basic">
<field name="logoFile" type="media" label="Logo" />
<field name="siteTitle" type="text" label="Site Title" default="" />
<field name="siteDescription" type="text" label="Site Description" default="" />
<field name="colorScheme" type="list" label="Color Scheme" default="light">
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="auto">Auto (system preference)</option>
</field>
</fieldset>
</fields>
</config>
</extension>
XML;
$files['templateDetails.xml'] = preg_replace('/^\t\t/m', '', $files['templateDetails.xml']);
// updates.xml — dual-platform download URLs (Gitea primary, GitHub secondary)
$files['updates.xml'] = <<<XML
<updates>
<update>
<name>{$name}</name>
<description>{$name} — Moko Consulting Joomla template</description>
<element>tpl_{$shortName}</element>
<type>template</type>
<version>1.0.0</version>
<downloads>
<downloadurl type="full" format="zip">
https://git.mokoconsulting.tech/{$org}/{$name}/releases/download/v1.0.0/{$shortName}.zip
</downloadurl>
<downloadurl type="full" format="zip">
https://github.com/{$org}/{$name}/releases/download/v1.0.0/{$shortName}.zip
</downloadurl>
</downloads>
<targetplatform name="joomla" version="[56].*"/>
<php_minimum>8.1</php_minimum>
</update>
</updates>
XML;
$files['updates.xml'] = preg_replace('/^\t\t/m', '', $files['updates.xml']);
// src/index.php
$files['src/index.php'] = <<<'PHP'
<?php
/**
* @package Joomla.Site
* @subpackage Templates.TEMPLATE_SHORT_NAME
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GPL-3.0-or-later
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
$app = Factory::getApplication();
$wa = $this->getWebAssetManager();
?>
<!DOCTYPE html>
<html lang="<?php echo $this->language; ?>" dir="<?php echo $this->direction; ?>">
<head>
<jdoc:include type="metas" />
<jdoc:include type="styles" />
<jdoc:include type="scripts" />
</head>
<body class="site <?php echo $this->direction === 'rtl' ? 'rtl' : 'ltr'; ?>">
<header>
<jdoc:include type="modules" name="topbar" style="none" />
<jdoc:include type="modules" name="navbar" style="none" />
</header>
<jdoc:include type="modules" name="hero" style="none" />
<jdoc:include type="modules" name="breadcrumbs" style="none" />
<main>
<jdoc:include type="modules" name="main-top" style="html5" />
<jdoc:include type="message" />
<jdoc:include type="component" />
<jdoc:include type="modules" name="main-bottom" style="html5" />
</main>
<footer>
<jdoc:include type="modules" name="footer" style="none" />
</footer>
<jdoc:include type="modules" name="debug" style="none" />
</body>
</html>
PHP;
$files['src/index.php'] = str_replace('TEMPLATE_SHORT_NAME', $shortName, $files['src/index.php']);
$files['src/index.php'] = preg_replace('/^\t\t/m', '', $files['src/index.php']);
// src/error.php
$files['src/error.php'] = <<<'PHP'
<?php
/**
* @package Joomla.Site
* @subpackage Templates.error
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GPL-3.0-or-later
*/
defined('_JEXEC') or die;
/** @var Joomla\CMS\Document\ErrorDocument $this */
$code = $this->error->getCode();
$message = htmlspecialchars($this->error->getMessage(), ENT_QUOTES, 'UTF-8');
?>
<!DOCTYPE html>
<html lang="<?php echo $this->language; ?>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?php echo $code; ?> — <?php echo $message; ?></title>
</head>
<body>
<div style="max-width:600px;margin:80px auto;text-align:center;font-family:sans-serif">
<h1><?php echo $code; ?></h1>
<p><?php echo $message; ?></p>
<p><a href="<?php echo $this->baseurl; ?>/">Return to homepage</a></p>
</div>
</body>
</html>
PHP;
$files['src/error.php'] = preg_replace('/^\t\t/m', '', $files['src/error.php']);
// src/offline.php
$files['src/offline.php'] = <<<'PHP'
<?php
/**
* @package Joomla.Site
* @subpackage Templates.offline
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GPL-3.0-or-later
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
$app = Factory::getApplication();
?>
<!DOCTYPE html>
<html lang="<?php echo $this->language; ?>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?php echo htmlspecialchars($app->get('sitename')); ?> — Maintenance</title>
</head>
<body>
<div style="max-width:600px;margin:80px auto;text-align:center;font-family:sans-serif">
<h1><?php echo htmlspecialchars($app->get('sitename')); ?></h1>
<p><?php echo $app->get('offline_message', 'This site is currently undergoing maintenance. Please check back soon.'); ?></p>
<jdoc:include type="message" />
<form action="<?php echo $this->baseurl; ?>/index.php" method="post">
<input type="text" name="username" placeholder="Username" />
<input type="password" name="password" placeholder="Password" />
<button type="submit">Login</button>
<input type="hidden" name="option" value="com_users" />
<input type="hidden" name="task" value="user.login" />
<?php echo \Joomla\CMS\HTML\HTMLHelper::_('form.token'); ?>
</form>
</div>
</body>
</html>
PHP;
$files['src/offline.php'] = preg_replace('/^\t\t/m', '', $files['src/offline.php']);
// src/component.php
$files['src/component.php'] = <<<'PHP'
<?php
/**
* @package Joomla.Site
* @subpackage Templates.component
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GPL-3.0-or-later
*/
defined('_JEXEC') or die;
?>
<!DOCTYPE html>
<html lang="<?php echo $this->language; ?>">
<head>
<jdoc:include type="head" />
</head>
<body class="contentpane">
<jdoc:include type="message" />
<jdoc:include type="component" />
</body>
</html>
PHP;
$files['src/component.php'] = preg_replace('/^\t\t/m', '', $files['src/component.php']);
// Directory keepfiles
$files['src/html/index.html'] = '<!DOCTYPE html><title></title>';
$files['src/css/.gitkeep'] = '';
$files['src/js/.gitkeep'] = '';
$files['src/images/.gitkeep'] = '';
$files['media/css/.gitkeep'] = '';
$files['media/js/.gitkeep'] = '';
$files['media/images/.gitkeep'] = '';
$files['media/scss/.gitkeep'] = '';
// Language files
$files["src/language/en-GB/{$element}.ini"] = "; {$name} language strings\n";
$files["src/language/en-GB/{$element}.sys.ini"] =
"; {$name} system language strings\n"
. "{$element}=\"{$name}\"\n"
. "{$element}_XML_DESCRIPTION=\"{$name} Joomla template by Moko Consulting\"\n";
// .editorconfig
$repoRoot = dirname(__DIR__, 2);
$editorConfig = "{$repoRoot}/templates/configs/.editorconfig";
if (file_exists($editorConfig)) {
$files['.editorconfig'] = file_get_contents($editorConfig) ?: '';
}
return $files;
}
/**
* Get files to sync to existing template repos (standards-only, no template code).
*
* @return array<string, string> path => content
*/
private function getSyncFiles(string $name, string $shortName): array
{
$repoRoot = dirname(__DIR__, 2);
$files = [];
// Sync standards files from templates/
$standardsFiles = [
'SECURITY.md' => 'templates/docs/required/template-SECURITY.md',
'CODE_OF_CONDUCT.md' => 'templates/docs/extra/template-CODE_OF_CONDUCT.md',
'CONTRIBUTING.md' => 'templates/docs/required/template-CONTRIBUTING.md',
'.editorconfig' => 'templates/configs/.editorconfig',
];
foreach ($standardsFiles as $dest => $source) {
$fullPath = "{$repoRoot}/{$source}";
if (file_exists($fullPath)) {
$files[$dest] = file_get_contents($fullPath) ?: '';
}
}
return $files;
}
// ── Sync updates.xml between platforms ───────────────────────────────
/**
* Sync updates.xml (or updates.xml) between Gitea and GitHub for Joomla repos.
*
* Reads the file from both platforms, compares by latest <version> tag,
* and pushes the newer one to the stale platform.
*
* Designed to be called from a CI workflow via:
* php automation/bulk_joomla_template.php --sync-updates --repos=MokoCassiopeia
*/
private function syncUpdatesBetweenPlatforms(string $org): int
{
$repos = [];
if ($this->getArgument('--all', false)) {
$repos = $this->findTemplateRepos($org);
// Also include waas-component repos
$allRepos = $this->adapter->listOrgRepos($org, true);
foreach ($allRepos as $repo) {
try {
$topics = $this->adapter->getRepoTopics($org, $repo['name']);
if (in_array('joomla', $topics, true) || in_array('joomla-extension', $topics, true)) {
$repos[] = $repo;
}
} catch (\Exception $e) {
$this->adapter->getApiClient()->resetCircuitBreaker();
}
}
// Deduplicate
$seen = [];
$repos = array_filter($repos, function ($r) use (&$seen) {
if (isset($seen[$r['name']])) {
return false;
}
$seen[$r['name']] = true;
return true;
});
} else {
$reposArg = $this->getArgument('--repos', '');
if (empty($reposArg)) {
$this->log("❌ --repos or --all required for --sync-updates", 'ERROR');
return 1;
}
$names = array_filter(array_map('trim', explode(',', $reposArg)));
foreach ($names as $name) {
$repos[] = ['name' => $name];
}
}
if (empty($repos)) {
$this->log("No Joomla repositories to sync updates for", 'INFO');
return 0;
}
// Create both platform adapters
try {
$adapters = PlatformAdapterFactory::createBoth($this->config);
} catch (\Exception $e) {
$this->log("❌ Both platform tokens required for --sync-updates: " . $e->getMessage(), 'ERROR');
return 1;
}
$gitea = $adapters['gitea'];
$github = $adapters['github'];
$dryRun = $this->dryRun;
$this->log("\nSyncing updates.xml across Gitea <-> GitHub for " . count($repos) . " repo(s)...", 'INFO');
$synced = 0;
$failed = 0;
foreach ($repos as $repo) {
$name = $repo['name'];
$this->log("\n[{$name}]", 'INFO');
// Try both updates.xml and updates.xml filenames
$updateFile = $this->resolveUpdateFile($gitea, $github, $org, $name);
if ($updateFile === null) {
$this->log(" ⊘ No update(s).xml found on either platform", 'INFO');
continue;
}
$fileName = $updateFile['name'];
$source = $updateFile['source']; // 'gitea' or 'github'
$content = $updateFile['content'];
$target = $source === 'gitea' ? 'github' : 'gitea';
$targetAdapter = $source === 'gitea' ? $github : $gitea;
$this->log(" Source: {$source} ({$fileName})", 'INFO');
if ($dryRun) {
$this->log(" (dry-run) Would push {$fileName} to {$target}", 'INFO');
$synced++;
continue;
}
// Push to the other platform
try {
$existingSha = null;
try {
$existing = $targetAdapter->getFileContents($org, $name, $fileName);
$existingSha = $existing['sha'] ?? null;
// Compare content — skip if identical
$existingContent = base64_decode($existing['content'] ?? '');
if (trim($existingContent) === trim($content)) {
$this->log(" ✓ Already in sync", 'INFO');
$synced++;
continue;
}
} catch (\Exception $e) {
$targetAdapter->getApiClient()->resetCircuitBreaker();
}
$targetAdapter->createOrUpdateFile(
$org,
$name,
$fileName,
$content,
"chore: sync {$fileName} from {$source}",
$existingSha
);
$this->log(" ✓ Pushed to {$target}", 'INFO');
$synced++;
} catch (\Exception $e) {
$this->log(" ✗ Failed to push to {$target}: " . $e->getMessage(), 'ERROR');
$targetAdapter->getApiClient()->resetCircuitBreaker();
$failed++;
}
}
$this->log("\n" . str_repeat('=', 50), 'INFO');
$this->log("Updates sync complete: {$synced} synced, {$failed} failed", 'INFO');
return $failed > 0 ? 1 : 0;
}
/**
* Find the updates file on both platforms, return the one with the higher version.
*
* Checks both `updates.xml` and `updates.xml` filenames.
* Returns the content from the platform with the newer <version>.
* Gitea wins ties (primary platform).
*
* @return array{name: string, source: string, content: string}|null
*/
private function resolveUpdateFile(
GitPlatformAdapter $gitea,
GitPlatformAdapter $github,
string $org,
string $name
): ?array {
$candidates = ['updates.xml', 'updates.xml'];
$found = []; // platform => [name, content, version]
foreach (['gitea' => $gitea, 'github' => $github] as $platform => $adapter) {
foreach ($candidates as $fileName) {
try {
$file = $adapter->getFileContents($org, $name, $fileName);
$content = base64_decode($file['content'] ?? '');
// Extract latest version from the XML
$version = '0.0.0';
if (preg_match('/<version>([^<]+)<\/version>/', $content, $m)) {
$version = trim($m[1]);
}
$found[$platform] = [
'name' => $fileName,
'content' => $content,
'version' => $version,
];
break; // Found one — stop checking other filenames for this platform
} catch (\Exception $e) {
$adapter->getApiClient()->resetCircuitBreaker();
}
}
}
if (empty($found)) {
return null;
}
// If only one platform has it, that's the source
if (count($found) === 1) {
$platform = array_key_first($found);
return [
'name' => $found[$platform]['name'],
'source' => $platform,
'content' => $found[$platform]['content'],
];
}
// Both have it — pick the one with the higher version (Gitea wins ties)
$giteaVer = $found['gitea']['version'];
$githubVer = $found['github']['version'];
$source = version_compare($githubVer, $giteaVer, '>') ? 'github' : 'gitea';
return [
'name' => $found[$source]['name'],
'source' => $source,
'content' => $found[$source]['content'],
];
}
}
// Execute if run directly
if (php_sapi_name() === 'cli' && isset($argv[0]) && realpath($argv[0]) === __FILE__) {
$app = new BulkJoomlaTemplate();
exit($app->execute());
}
File diff suppressed because it is too large Load Diff
-123
View File
@@ -1,123 +0,0 @@
#!/usr/bin/env bash
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# SPDX-License-Identifier: GPL-3.0-or-later
# BRIEF: Trigger a workflow across all client-waas repos in a Gitea org
set -euo pipefail
# ---------------------------------------------------------------------------
# Usage
# ---------------------------------------------------------------------------
usage() {
cat <<EOF
Usage: $(basename "$0") GITEA_URL TOKEN ORG WORKFLOW [REF] [INPUTS]
Arguments:
GITEA_URL Base URL of the Gitea instance (e.g. https://git.mokoconsulting.tech)
TOKEN Gitea API token with repo/action permissions
ORG Organisation or user that owns the repos
WORKFLOW Workflow filename to trigger (e.g. dependency-audit.yml)
REF Branch ref to run against (default: main)
INPUTS Optional JSON object of workflow inputs (e.g. '{"dry_run":"true"}')
Example:
$(basename "$0") https://git.mokoconsulting.tech abc123 MokoConsulting dependency-audit.yml main '{"notify":"true"}'
EOF
exit 1
}
# ---------------------------------------------------------------------------
# Argument parsing
# ---------------------------------------------------------------------------
if [ $# -lt 4 ]; then
usage
fi
GITEA_URL="${1%/}"
TOKEN="$2"
ORG="$3"
WORKFLOW="$4"
REF="${5:-main}"
INPUTS="${6:-{\}}"
# ---------------------------------------------------------------------------
# Fetch all repos in the org, paginated
# ---------------------------------------------------------------------------
echo "Fetching repos for org '${ORG}' on ${GITEA_URL} ..."
PAGE=1
LIMIT=50
ALL_REPOS=""
while true; do
RESPONSE=$(curl -s \
-H "Authorization: token ${TOKEN}" \
-H "Accept: application/json" \
"${GITEA_URL}/api/v1/orgs/${ORG}/repos?page=${PAGE}&limit=${LIMIT}")
# Break if empty array
COUNT=$(echo "$RESPONSE" | jq -r 'length')
if [ "$COUNT" -eq 0 ]; then
break
fi
NAMES=$(echo "$RESPONSE" | jq -r '.[].name')
ALL_REPOS="${ALL_REPOS}${NAMES}"$'\n'
if [ "$COUNT" -lt "$LIMIT" ]; then
break
fi
PAGE=$((PAGE + 1))
done
# ---------------------------------------------------------------------------
# Filter for client-waas repos
# ---------------------------------------------------------------------------
CLIENT_REPOS=$(echo "$ALL_REPOS" | grep 'client-waas' | sort || true)
if [ -z "$CLIENT_REPOS" ]; then
echo "No client-waas repos found in org '${ORG}'."
exit 0
fi
TOTAL=$(echo "$CLIENT_REPOS" | wc -l | tr -d ' ')
echo "Found ${TOTAL} client-waas repo(s). Triggering workflow '${WORKFLOW}' (ref: ${REF}) ..."
echo ""
# ---------------------------------------------------------------------------
# Trigger workflow for each repo
# ---------------------------------------------------------------------------
SUCCESS=0
FAIL=0
while IFS= read -r REPO; do
[ -z "$REPO" ] && continue
PAYLOAD=$(jq -n --arg ref "$REF" --argjson inputs "$INPUTS" '{ref: $ref, inputs: $inputs}')
HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' \
-X POST \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
-d "$PAYLOAD" \
"${GITEA_URL}/api/v1/repos/${ORG}/${REPO}/actions/workflows/${WORKFLOW}/dispatches")
if [ "$HTTP_CODE" -eq 204 ] || [ "$HTTP_CODE" -eq 201 ]; then
echo " [OK] ${ORG}/${REPO} (HTTP ${HTTP_CODE})"
SUCCESS=$((SUCCESS + 1))
else
echo " [FAIL] ${ORG}/${REPO} (HTTP ${HTTP_CODE})"
FAIL=$((FAIL + 1))
fi
done <<< "$CLIENT_REPOS"
# ---------------------------------------------------------------------------
# Summary
# ---------------------------------------------------------------------------
echo ""
echo "Done. Success: ${SUCCESS} | Failed: ${FAIL} | Total: ${TOTAL}"
if [ "$FAIL" -gt 0 ]; then
exit 1
fi
-108
View File
@@ -1,108 +0,0 @@
#!/usr/bin/env bash
# =============================================================================
# enforce_tags.sh — Ensure all repos have the 5 standard release channel tags
#
# Standard tags: development, alpha, beta, release-candidate, stable
# Also removes non-standard tags (keeps vXX production tags)
#
# Usage:
# GA_TOKEN=xxx ./enforce_tags.sh [--dry-run] [--repos repo1,repo2]
#
# Called by: bulk-repo-sync.yml, infrastructure-tests/mirror-check.yml
# =============================================================================
set -euo pipefail
GITEA_URL="${GITEA_URL:-https://git.mokoconsulting.tech}"
ORG="${GITEA_ORG:-MokoConsulting}"
TOKEN="${GA_TOKEN:?GA_TOKEN required}"
DRY_RUN=false
FILTER_REPOS=""
STANDARD_TAGS=("development" "alpha" "beta" "release-candidate" "stable")
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run) DRY_RUN=true; shift ;;
--repos) FILTER_REPOS="$2"; shift 2 ;;
*) shift ;;
esac
done
api() {
local method="$1" path="$2" data="${3:-}"
local args=(-sf -H "Authorization: token $TOKEN" -H "Content-Type: application/json" -X "$method")
[[ -n "$data" ]] && args+=(-d "$data")
curl "${args[@]}" "$GITEA_URL/api/v1$path" 2>/dev/null
}
# Get repos
REPOS=""
for page in 1 2 3; do
BATCH=$(api GET "/orgs/$ORG/repos?limit=50&page=$page" | python3 -c "
import sys,json
for r in json.load(sys.stdin):
if not r.get(empty) and not r.get(archived):
print(r[name])
" 2>/dev/null)
[[ -z "$BATCH" ]] && break
REPOS="$REPOS $BATCH"
done
# Filter if specified
if [[ -n "$FILTER_REPOS" ]]; then
FILTERED=""
IFS=, read -ra FILTER_ARR <<< "$FILTER_REPOS"
for repo in $REPOS; do
for f in "${FILTER_ARR[@]}"; do
[[ "$repo" == "$f" ]] && FILTERED="$FILTERED $repo"
done
done
REPOS="$FILTERED"
fi
TOTAL=$(echo $REPOS | wc -w)
ADDED=0
DELETED=0
ERRORS=0
echo "Enforcing tags on $TOTAL repos (dry_run=$DRY_RUN)"
for repo in $REPOS; do
TAGS=$(api GET "/repos/$ORG/$repo/tags?limit=50" | python3 -c "import sys,json; print( .join(t[name] for t in json.load(sys.stdin)))" 2>/dev/null)
MAIN_SHA=$(api GET "/repos/$ORG/$repo/branches/main" | python3 -c "import sys,json; print(json.load(sys.stdin)[commit][id])" 2>/dev/null)
[[ -z "$MAIN_SHA" ]] && continue
# Add missing standard tags
for st in "${STANDARD_TAGS[@]}"; do
if ! echo " $TAGS " | grep -q " $st "; then
if [[ "$DRY_RUN" == "true" ]]; then
echo " [DRY] ADD $repo: $st"
else
STATUS=$(api POST "/repos/$ORG/$repo/tags" "{\"tag_name\":\"$st\",\"target\":\"$MAIN_SHA\"}" | python3 -c "import sys,json; print(ok)" 2>/dev/null || echo "err")
[[ "$STATUS" == "ok" ]] && ADDED=$((ADDED + 1)) || ERRORS=$((ERRORS + 1))
fi
fi
done
# Remove non-standard tags
for t in $TAGS; do
IS_STD=false
for st in "${STANDARD_TAGS[@]}"; do [[ "$t" == "$st" ]] && IS_STD=true; done
# Keep vXX production tags
if [[ "$t" =~ ^v[0-9]{1,3}$ ]]; then IS_STD=true; fi
if [[ "$IS_STD" == "false" ]]; then
if [[ "$DRY_RUN" == "true" ]]; then
echo " [DRY] DEL $repo: $t"
else
# Delete release first if exists
api DELETE "/repos/$ORG/$repo/releases/tags/$t" > /dev/null 2>&1 || true
api DELETE "/repos/$ORG/$repo/tags/$t" > /dev/null 2>&1
DELETED=$((DELETED + 1))
echo " DEL $repo: $t"
fi
fi
done
done
echo "Done: $ADDED added, $DELETED deleted, $ERRORS errors (dry_run=$DRY_RUN)"
-481
View File
@@ -1,481 +0,0 @@
#!/usr/bin/env php
<?php
/**
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: MokoPlatform.Automation
* INGROUP: MokoPlatform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /automation/enrich_manifest_xml.php
* BRIEF: Enrich XML manifests with repo-specific build and deploy details
*
* Note: This script uses proc_open for shell commands. All arguments are escaped
* via escapeshellarg(). No user-supplied input reaches the shell unescaped.
*/
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoEnterprise\CliFramework;
use MokoEnterprise\MokoStandardsParser;
class EnrichManifestXmlCli extends CliFramework
{
protected function configure(): void
{
$this->setDescription('Enrich XML manifests with repo-specific build and deploy details');
$this->addArgument('--repo', 'Filter to a single repo name', '');
$this->addArgument('--skip', 'Comma-separated list of repos to skip', '');
}
protected function run(): int
{
$giteaUrl = rtrim(getenv('GITEA_URL') ?: 'https://git.mokoconsulting.tech', '/');
$giteaOrg = getenv('GITEA_ORG') ?: 'MokoConsulting';
$token = getenv('GA_TOKEN') ?: getenv('GH_TOKEN') ?: '';
$repoFilter = $this->getArgument('--repo') ?: null;
$skipStr = $this->getArgument('--skip');
$skipRepos = $skipStr !== '' ? array_map('trim', explode(',', $skipStr)) : [];
$parser = new MokoStandardsParser();
$tmpBase = sys_get_temp_dir() . '/moko-enrich-' . getmypid();
echo "=== moko-platform XML Manifest Enrichment ===\n";
echo "Mode: " . ($this->dryRun ? "DRY RUN" : "LIVE") . "\n";
if (!empty($skipRepos)) {
echo "Skipping: " . implode(', ', $skipRepos) . "\n";
}
echo "\n";
if (empty($token)) {
$this->log('ERROR', 'GA_TOKEN required');
return 1;
}
$repos = $this->fetchRepos($giteaUrl, $giteaOrg, $token);
echo "Found " . count($repos) . " repositories\n\n";
$stats = ['enriched' => 0, 'skipped' => 0, 'failed' => 0];
foreach ($repos as $repo) {
$name = $repo['name'];
if ($repoFilter && $name !== $repoFilter) {
continue;
}
if (in_array($name, $skipRepos, true)) {
echo " {$name} ... SKIP (excluded)\n";
$stats['skipped']++;
continue;
}
if ($repo['archived'] ?? false) {
$stats['skipped']++;
continue;
}
$defaultBranch = $repo['default_branch'] ?? 'main';
$httpsUrl = $repo['clone_url'] ?? "{$giteaUrl}/{$giteaOrg}/{$name}.git";
$authedUrl = preg_replace('#^https://#', "https://gitea-actions:{$token}@", $httpsUrl);
echo " {$name} ... ";
$workDir = "{$tmpBase}/{$name}";
@mkdir($workDir, 0755, true);
[$ret] = $this->safeExec(
'git clone --depth 1 --branch ' . escapeshellarg($defaultBranch)
. ' ' . escapeshellarg($authedUrl) . ' ' . escapeshellarg($workDir)
);
if ($ret !== 0) {
echo "FAIL (clone)\n";
$stats['failed']++;
continue;
}
$manifestPath = "{$workDir}/.mokogitea/manifest.xml";
if (!file_exists($manifestPath) || !str_contains(file_get_contents($manifestPath), '<moko-platform')) {
echo "SKIP (no XML manifest)\n";
$stats['skipped']++;
$this->rmTree($workDir);
continue;
}
$existingXml = file_get_contents($manifestPath);
$platform = $parser->extractPlatform($existingXml) ?? 'default-repository';
$enrichment = $this->inspectRepo($workDir, $platform);
if (!isset($enrichment['build'])) {
$enrichment['build'] = [];
}
$enrichment['build']['language'] = $enrichment['build']['language']
?? $repo['language']
?? MokoStandardsParser::platformLanguage($platform);
$enrichment['build']['package_type'] = $enrichment['build']['package_type'] ?? MokoStandardsParser::platformPackageType($platform);
$enrichedXml = $this->enrichManifestXml($existingXml, $enrichment);
$dc = count($enrichment['deploy'] ?? []);
$sc = count($enrichment['scripts'] ?? []);
$details = "deploy={$dc} scripts={$sc}";
if ($this->dryRun) {
echo "WOULD ENRICH [{$details}]\n";
$stats['enriched']++;
$this->rmTree($workDir);
continue;
}
file_put_contents($manifestPath, $enrichedXml);
$this->gitCmd($workDir, 'config', 'user.name', 'gitea-actions[bot]');
$this->gitCmd($workDir, 'config', 'user.email', 'gitea-actions[bot]@git.mokoconsulting.tech');
$this->gitCmd($workDir, 'add', '.mokogitea/manifest.xml');
[$cr, $co] = $this->gitCmd($workDir, 'commit', '-m', "chore: enrich manifest.xml with build/deploy/scripts\n\nAuto-detected: {$details}");
if ($cr !== 0) {
echo "SKIP (no diff)\n";
$stats['skipped']++;
$this->rmTree($workDir);
continue;
}
[$pr] = $this->gitCmd($workDir, 'push', 'origin', $defaultBranch);
if ($pr !== 0) {
echo "FAIL (push)\n";
$stats['failed']++;
} else {
echo "ENRICHED [{$details}]\n";
$stats['enriched']++;
}
$this->rmTree($workDir);
}
@rmdir($tmpBase);
echo "\n=== Summary ===\nEnriched: {$stats['enriched']}\nSkipped: {$stats['skipped']}\nFailed: {$stats['failed']}\n";
return 0;
}
private function inspectRepo(string $workDir, string $platform): array
{
$enrichment = [];
$build = [];
// Detect entry point
if (is_dir("{$workDir}/src")) {
foreach (glob("{$workDir}/src/*.xml") ?: [] as $xf) {
$c = file_get_contents($xf);
if (str_contains($c, '<extension') || str_contains($c, '<install')) {
$build['entry_point'] = 'src/' . basename($xf);
break;
}
}
foreach (glob("{$workDir}/src/core/modules/mod*.class.php") ?: [] as $mf) {
$build['entry_point'] = str_replace("{$workDir}/", '', $mf);
break;
}
}
// composer.json
if (file_exists("{$workDir}/composer.json")) {
$composer = json_decode(file_get_contents("{$workDir}/composer.json"), true) ?: [];
$phpReq = $composer['require']['php'] ?? null;
if ($phpReq) {
$build['runtime'] = "php:{$phpReq}";
}
$deps = [];
foreach (['joomla/cms', 'joomla/framework', 'dolibarr/dolibarr'] as $pd) {
if (isset($composer['require'][$pd])) {
$deps[] = ['name' => $pd, 'version' => $composer['require'][$pd], 'type' => 'platform'];
}
}
if (isset($composer['require']['mokoconsulting-tech/enterprise'])) {
$deps[] = [
'name' => 'mokoconsulting-tech/enterprise',
'version' => $composer['require']['mokoconsulting-tech/enterprise'],
'type' => 'composer',
];
}
if (!empty($deps)) {
$build['dependencies'] = $deps;
}
}
// Artifact from Makefile
if (file_exists("{$workDir}/Makefile")) {
$mk = file_get_contents("{$workDir}/Makefile");
if (preg_match('/\bdist\/(\S+\.zip)\b/', $mk, $m)) {
$build['artifact'] = ['format' => 'zip', 'path' => 'dist/', 'filename' => $m[1]];
}
}
if (!empty($build)) {
$enrichment['build'] = $build;
}
// Deploy targets from workflows
$targets = [];
$wfDir = is_dir("{$workDir}/.gitea/workflows") ? "{$workDir}/.gitea/workflows" : "{$workDir}/.github/workflows";
if (is_dir($wfDir)) {
foreach (['deploy-dev', 'deploy-demo', 'deploy-rs'] as $dn) {
$wf = "{$wfDir}/{$dn}.yml";
if (!file_exists($wf)) {
continue;
}
$wc = file_get_contents($wf);
$t = ['name' => str_replace('deploy-', '', $dn)];
if (str_contains($wc, 'sftp') || str_contains($wc, 'SFTP')) {
$t['method'] = 'sftp';
} elseif (str_contains($wc, 'rsync')) {
$t['method'] = 'rsync';
}
if (str_contains($wc, 'src/')) {
$t['src_dir'] = 'src/';
}
if (preg_match('/branches:\s*\n\s*-\s*["\']?([^"\'}\s]+)/', $wc, $m)) {
$t['branch'] = $m[1];
}
$targets[] = $t;
}
}
if (!empty($targets)) {
$enrichment['deploy'] = $targets;
}
// Scripts from Makefile + composer
$scripts = [];
if (file_exists("{$workDir}/Makefile")) {
$mk = file_get_contents("{$workDir}/Makefile");
$known = [
'build' => 'build', 'test' => 'test', 'lint' => 'lint',
'clean' => 'build', 'package' => 'build',
'validate' => 'validate', 'release' => 'release',
];
if (preg_match_all('/^([a-zA-Z_-]+)\s*:/m', $mk, $matches)) {
foreach ($matches[1] as $tgt) {
$tl = strtolower($tgt);
if (isset($known[$tl])) {
$scripts[] = [
'name' => $tl, 'phase' => $known[$tl],
'command' => "make {$tgt}",
'desc' => ucfirst($tl) . ' via make',
'runner' => 'make',
];
}
}
}
}
if (file_exists("{$workDir}/composer.json")) {
$composer = json_decode(file_get_contents("{$workDir}/composer.json"), true) ?: [];
$km = ['test' => 'test','lint' => 'lint','cs' => 'lint','phpcs' => 'lint','phpstan' => 'lint','validate' => 'validate'];
foreach ($composer['scripts'] ?? [] as $sn => $cmd) {
$sl = strtolower($sn);
foreach ($km as $match => $phase) {
if (str_contains($sl, $match)) {
$exists = false;
foreach ($scripts as $s) {
if ($s['name'] === $sl) {
$exists = true;
break;
}
}
if (!$exists) {
$scripts[] = [
'name' => $sn, 'phase' => $phase,
'command' => "composer run {$sn}",
'desc' => is_string($cmd) ? $cmd : "Run {$sn}",
'runner' => 'composer',
];
}
break;
}
}
}
}
if (!empty($scripts)) {
$enrichment['scripts'] = $scripts;
}
return $enrichment;
}
private function enrichManifestXml(string $xml, array $enrichment): string
{
$dom = new \DOMDocument('1.0', 'UTF-8');
$dom->preserveWhiteSpace = false;
$dom->formatOutput = true;
if (!$dom->loadXML($xml)) {
return $xml;
}
$ns = MokoStandardsParser::NAMESPACE_URI;
$root = $dom->documentElement;
foreach (['build', 'deploy', 'scripts'] as $tag) {
$toRemove = [];
$existing = $root->getElementsByTagNameNS($ns, $tag);
for ($i = 0; $i < $existing->length; $i++) {
$toRemove[] = $existing->item($i);
}
foreach ($toRemove as $node) {
$root->removeChild($node);
}
}
if (!empty($enrichment['build'])) {
$buildEl = $dom->createElementNS($ns, 'build');
$b = $enrichment['build'];
foreach (['language', 'runtime'] as $f) {
if (isset($b[$f])) {
$buildEl->appendChild($dom->createElementNS($ns, $f, htmlspecialchars($b[$f], ENT_XML1)));
}
}
if (isset($b['package_type'])) {
$buildEl->appendChild($dom->createElementNS($ns, 'package-type', htmlspecialchars($b['package_type'], ENT_XML1)));
}
if (isset($b['entry_point'])) {
$buildEl->appendChild($dom->createElementNS($ns, 'entry-point', htmlspecialchars($b['entry_point'], ENT_XML1)));
}
if (isset($b['artifact'])) {
$art = $dom->createElementNS($ns, 'artifact');
foreach (['format','path','filename'] as $af) {
if (isset($b['artifact'][$af])) {
$art->appendChild($dom->createElementNS($ns, $af, htmlspecialchars($b['artifact'][$af], ENT_XML1)));
}
}
$buildEl->appendChild($art);
}
if (isset($b['dependencies'])) {
$deps = $dom->createElementNS($ns, 'dependencies');
foreach ($b['dependencies'] as $d) {
$req = $dom->createElementNS($ns, 'requires', '');
$req->setAttribute('name', $d['name']);
if (isset($d['version'])) {
$req->setAttribute('version', $d['version']);
}
if (isset($d['type'])) {
$req->setAttribute('type', $d['type']);
}
$deps->appendChild($req);
}
$buildEl->appendChild($deps);
}
$root->appendChild($buildEl);
}
if (!empty($enrichment['deploy'])) {
$deploy = $dom->createElementNS($ns, 'deploy');
foreach ($enrichment['deploy'] as $t) {
$target = $dom->createElementNS($ns, 'target');
$target->setAttribute('name', $t['name']);
$target->appendChild($dom->createElementNS($ns, 'host', '${{ secrets.' . strtoupper($t['name']) . '_HOST }}'));
$target->appendChild($dom->createElementNS($ns, 'path', '${{ secrets.' . strtoupper($t['name']) . '_PATH }}'));
if (isset($t['method'])) {
$target->appendChild($dom->createElementNS($ns, 'method', $t['method']));
}
if (isset($t['branch'])) {
$target->appendChild($dom->createElementNS($ns, 'branch', htmlspecialchars($t['branch'], ENT_XML1)));
}
if (isset($t['src_dir'])) {
$target->appendChild($dom->createElementNS($ns, 'src-dir', htmlspecialchars($t['src_dir'], ENT_XML1)));
}
$deploy->appendChild($target);
}
$root->appendChild($deploy);
}
if (!empty($enrichment['scripts'])) {
$scriptsEl = $dom->createElementNS($ns, 'scripts');
foreach ($enrichment['scripts'] as $s) {
$script = $dom->createElementNS($ns, 'script');
$script->setAttribute('name', $s['name']);
if (isset($s['phase'])) {
$script->setAttribute('phase', $s['phase']);
}
$script->appendChild($dom->createElementNS($ns, 'command', htmlspecialchars($s['command'], ENT_XML1)));
if (isset($s['desc'])) {
$script->appendChild($dom->createElementNS($ns, 'description', htmlspecialchars($s['desc'], ENT_XML1)));
}
if (isset($s['runner'])) {
$script->appendChild($dom->createElementNS($ns, 'runner', htmlspecialchars($s['runner'], ENT_XML1)));
}
$scriptsEl->appendChild($script);
}
$root->appendChild($scriptsEl);
}
return $dom->saveXML();
}
/** @return array{int, string} */
private function safeExec(string $command, string $cwd = '.'): array
{
$proc = proc_open($command, [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes, $cwd);
if (!is_resource($proc)) {
return [1, "proc_open failed"];
}
$stdout = stream_get_contents($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
return [proc_close($proc), trim($stdout . "\n" . $stderr)];
}
private function rmTree(string $dir): void
{
if (!is_dir($dir)) {
return;
}
$it = new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS);
$files = new \RecursiveIteratorIterator($it, \RecursiveIteratorIterator::CHILD_FIRST);
foreach ($files as $file) {
if ($file->isDir()) {
@rmdir($file->getPathname());
} else {
@chmod($file->getPathname(), 0777);
@unlink($file->getPathname());
}
}
@rmdir($dir);
}
/** @return array{int, string} */
private function gitCmd(string $workDir, string ...$args): array
{
$cmd = 'git';
foreach ($args as $a) {
$cmd .= ' ' . escapeshellarg($a);
}
return $this->safeExec($cmd, $workDir);
}
private function fetchRepos(string $url, string $org, string $token): array
{
$repos = [];
$page = 1;
do {
$ch = curl_init("{$url}/api/v1/orgs/{$org}/repos?page={$page}&limit=50");
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ["Authorization: token {$token}"], CURLOPT_TIMEOUT => 30]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code !== 200) {
break;
}
$batch = json_decode($body, true);
if (empty($batch)) {
break;
}
$repos = array_merge($repos, $batch);
$page++;
} while (count($batch) >= 50);
return $repos;
}
}
$app = new EnrichManifestXmlCli();
exit($app->execute());
-484
View File
@@ -1,484 +0,0 @@
#!/usr/bin/env php
<?php
/**
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: MokoPlatform.Automation
* INGROUP: MokoPlatform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /automation/enrich_mokostandards_xml.php
* BRIEF: Enrich XML manifests with repo-specific build and deploy details
*
* Note: This script uses proc_open for shell commands. All arguments are escaped
* via escapeshellarg(). No user-supplied input reaches the shell unescaped.
*/
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoEnterprise\CliFramework;
use MokoEnterprise\MokoStandardsParser;
class EnrichMokostandardsXmlCli extends CliFramework
{
protected function configure(): void
{
$this->setDescription('Enrich XML manifests with repo-specific build and deploy details');
$this->addArgument('--repo', 'Filter to a single repo name', '');
$this->addArgument('--skip', 'Comma-separated list of repos to skip', '');
}
protected function run(): int
{
$giteaUrl = rtrim(getenv('GITEA_URL') ?: 'https://git.mokoconsulting.tech', '/');
$giteaOrg = getenv('GITEA_ORG') ?: 'MokoConsulting';
$token = getenv('GA_TOKEN') ?: getenv('GH_TOKEN') ?: '';
$repoFilter = $this->getArgument('--repo') ?: null;
$skipStr = $this->getArgument('--skip');
$skipRepos = $skipStr !== '' ? array_map('trim', explode(',', $skipStr)) : [];
$parser = new MokoStandardsParser();
$tmpBase = sys_get_temp_dir() . '/moko-enrich-' . getmypid();
echo "=== moko-platform XML Manifest Enrichment ===\n";
echo "Mode: " . ($this->dryRun ? "DRY RUN" : "LIVE") . "\n";
if (!empty($skipRepos)) {
echo "Skipping: " . implode(', ', $skipRepos) . "\n";
}
echo "\n";
if (empty($token)) {
$this->log('ERROR', 'GA_TOKEN required');
return 1;
}
$repos = $this->fetchRepos($giteaUrl, $giteaOrg, $token);
echo "Found " . count($repos) . " repositories\n\n";
$stats = ['enriched' => 0, 'skipped' => 0, 'failed' => 0];
foreach ($repos as $repo) {
$name = $repo['name'];
if ($repoFilter && $name !== $repoFilter) {
continue;
}
if (in_array($name, $skipRepos, true)) {
echo " {$name} ... SKIP (excluded)\n";
$stats['skipped']++;
continue;
}
if ($repo['archived'] ?? false) {
$stats['skipped']++;
continue;
}
$defaultBranch = $repo['default_branch'] ?? 'main';
$httpsUrl = $repo['clone_url'] ?? "{$giteaUrl}/{$giteaOrg}/{$name}.git";
$authedUrl = preg_replace('#^https://#', "https://gitea-actions:{$token}@", $httpsUrl);
echo " {$name} ... ";
$workDir = "{$tmpBase}/{$name}";
@mkdir($workDir, 0755, true);
[$ret] = $this->safeExec(
'git clone --depth 1 --branch ' . escapeshellarg($defaultBranch)
. ' ' . escapeshellarg($authedUrl) . ' ' . escapeshellarg($workDir)
);
if ($ret !== 0) {
echo "FAIL (clone)\n";
$stats['failed']++;
continue;
}
$manifestPath = "{$workDir}/.mokogitea/manifest.xml";
if (!file_exists($manifestPath) || !str_contains(file_get_contents($manifestPath), '<moko-platform')) {
echo "SKIP (no XML manifest)\n";
$stats['skipped']++;
$this->rmTree($workDir);
continue;
}
$existingXml = file_get_contents($manifestPath);
$platform = $parser->extractPlatform($existingXml) ?? 'default-repository';
$enrichment = $this->inspectRepo($workDir, $platform);
if (!isset($enrichment['build'])) {
$enrichment['build'] = [];
}
$enrichment['build']['language'] = $enrichment['build']['language']
?? $repo['language']
?? MokoStandardsParser::platformLanguage($platform);
$enrichment['build']['package_type'] = $enrichment['build']['package_type'] ?? MokoStandardsParser::platformPackageType($platform);
$enrichedXml = $this->enrichManifestXml($existingXml, $enrichment);
$dc = count($enrichment['deploy'] ?? []);
$sc = count($enrichment['scripts'] ?? []);
$details = "deploy={$dc} scripts={$sc}";
if ($this->dryRun) {
echo "WOULD ENRICH [{$details}]\n";
$stats['enriched']++;
$this->rmTree($workDir);
continue;
}
file_put_contents($manifestPath, $enrichedXml);
$this->gitCmd($workDir, 'config', 'user.name', 'gitea-actions[bot]');
$this->gitCmd($workDir, 'config', 'user.email', 'gitea-actions[bot]@git.mokoconsulting.tech');
$this->gitCmd($workDir, 'add', '.mokogitea/manifest.xml');
$commitMsg = "chore: enrich .mokostandards"
. " with build/deploy/scripts\n\n"
. "Auto-detected: {$details}";
[$cr, $co] = $this->gitCmd(
$workDir,
'commit',
'-m',
$commitMsg
);
if ($cr !== 0) {
echo "SKIP (no diff)\n";
$stats['skipped']++;
$this->rmTree($workDir);
continue;
}
[$pr] = $this->gitCmd($workDir, 'push', 'origin', $defaultBranch);
if ($pr !== 0) {
echo "FAIL (push)\n";
$stats['failed']++;
} else {
echo "ENRICHED [{$details}]\n";
$stats['enriched']++;
}
$this->rmTree($workDir);
}
@rmdir($tmpBase);
echo "\n=== Summary ===\nEnriched: {$stats['enriched']}\nSkipped: {$stats['skipped']}\nFailed: {$stats['failed']}\n";
return 0;
}
private function inspectRepo(string $workDir, string $platform): array
{
$enrichment = [];
$build = [];
if (is_dir("{$workDir}/src")) {
foreach (glob("{$workDir}/src/*.xml") ?: [] as $xf) {
$c = file_get_contents($xf);
if (str_contains($c, '<extension') || str_contains($c, '<install')) {
$build['entry_point'] = 'src/' . basename($xf);
break;
}
}
foreach (glob("{$workDir}/src/core/modules/mod*.class.php") ?: [] as $mf) {
$build['entry_point'] = str_replace("{$workDir}/", '', $mf);
break;
}
}
if (file_exists("{$workDir}/composer.json")) {
$composer = json_decode(file_get_contents("{$workDir}/composer.json"), true) ?: [];
$phpReq = $composer['require']['php'] ?? null;
if ($phpReq) {
$build['runtime'] = "php:{$phpReq}";
}
$deps = [];
foreach (['joomla/cms', 'joomla/framework', 'dolibarr/dolibarr'] as $pd) {
if (isset($composer['require'][$pd])) {
$deps[] = ['name' => $pd, 'version' => $composer['require'][$pd], 'type' => 'platform'];
}
}
if (isset($composer['require']['mokoconsulting-tech/enterprise'])) {
$deps[] = [
'name' => 'mokoconsulting-tech/enterprise',
'version' => $composer['require']['mokoconsulting-tech/enterprise'],
'type' => 'composer',
];
}
if (!empty($deps)) {
$build['dependencies'] = $deps;
}
}
if (file_exists("{$workDir}/Makefile")) {
$mk = file_get_contents("{$workDir}/Makefile");
if (preg_match('/\bdist\/(\S+\.zip)\b/', $mk, $m)) {
$build['artifact'] = ['format' => 'zip', 'path' => 'dist/', 'filename' => $m[1]];
}
}
if (!empty($build)) {
$enrichment['build'] = $build;
}
$targets = [];
$wfDir = is_dir("{$workDir}/.gitea/workflows") ? "{$workDir}/.gitea/workflows" : "{$workDir}/.github/workflows";
if (is_dir($wfDir)) {
foreach (['deploy-dev', 'deploy-demo', 'deploy-rs'] as $dn) {
$wf = "{$wfDir}/{$dn}.yml";
if (!file_exists($wf)) {
continue;
}
$wc = file_get_contents($wf);
$t = ['name' => str_replace('deploy-', '', $dn)];
if (str_contains($wc, 'sftp') || str_contains($wc, 'SFTP')) {
$t['method'] = 'sftp';
} elseif (str_contains($wc, 'rsync')) {
$t['method'] = 'rsync';
}
if (str_contains($wc, 'src/')) {
$t['src_dir'] = 'src/';
}
if (preg_match('/branches:\s*\n\s*-\s*["\']?([^"\'}\s]+)/', $wc, $m)) {
$t['branch'] = $m[1];
}
$targets[] = $t;
}
}
if (!empty($targets)) {
$enrichment['deploy'] = $targets;
}
$scripts = [];
if (file_exists("{$workDir}/Makefile")) {
$mk = file_get_contents("{$workDir}/Makefile");
$known = [
'build' => 'build', 'test' => 'test', 'lint' => 'lint',
'clean' => 'build', 'package' => 'build',
'validate' => 'validate', 'release' => 'release',
];
if (preg_match_all('/^([a-zA-Z_-]+)\s*:/m', $mk, $matches)) {
foreach ($matches[1] as $tgt) {
$tl = strtolower($tgt);
if (isset($known[$tl])) {
$scripts[] = [
'name' => $tl, 'phase' => $known[$tl],
'command' => "make {$tgt}",
'desc' => ucfirst($tl) . ' via make',
'runner' => 'make',
];
}
}
}
}
if (file_exists("{$workDir}/composer.json")) {
$composer = json_decode(file_get_contents("{$workDir}/composer.json"), true) ?: [];
$km = ['test' => 'test','lint' => 'lint','cs' => 'lint','phpcs' => 'lint','phpstan' => 'lint','validate' => 'validate'];
foreach ($composer['scripts'] ?? [] as $sn => $cmd) {
$sl = strtolower($sn);
foreach ($km as $match => $phase) {
if (str_contains($sl, $match)) {
$exists = false;
foreach ($scripts as $s) {
if ($s['name'] === $sl) {
$exists = true;
break;
}
}
if (!$exists) {
$scripts[] = [
'name' => $sn, 'phase' => $phase,
'command' => "composer run {$sn}",
'desc' => is_string($cmd) ? $cmd : "Run {$sn}",
'runner' => 'composer',
];
}
break;
}
}
}
}
if (!empty($scripts)) {
$enrichment['scripts'] = $scripts;
}
return $enrichment;
}
private function enrichManifestXml(string $xml, array $enrichment): string
{
$dom = new \DOMDocument('1.0', 'UTF-8');
$dom->preserveWhiteSpace = false;
$dom->formatOutput = true;
if (!$dom->loadXML($xml)) {
return $xml;
}
$ns = MokoStandardsParser::NAMESPACE_URI;
$root = $dom->documentElement;
foreach (['build', 'deploy', 'scripts'] as $tag) {
$toRemove = [];
$existing = $root->getElementsByTagNameNS($ns, $tag);
for ($i = 0; $i < $existing->length; $i++) {
$toRemove[] = $existing->item($i);
}
foreach ($toRemove as $node) {
$root->removeChild($node);
}
}
if (!empty($enrichment['build'])) {
$buildEl = $dom->createElementNS($ns, 'build');
$b = $enrichment['build'];
foreach (['language', 'runtime'] as $f) {
if (isset($b[$f])) {
$buildEl->appendChild($dom->createElementNS($ns, $f, htmlspecialchars($b[$f], ENT_XML1)));
}
}
if (isset($b['package_type'])) {
$buildEl->appendChild($dom->createElementNS($ns, 'package-type', htmlspecialchars($b['package_type'], ENT_XML1)));
}
if (isset($b['entry_point'])) {
$buildEl->appendChild($dom->createElementNS($ns, 'entry-point', htmlspecialchars($b['entry_point'], ENT_XML1)));
}
if (isset($b['artifact'])) {
$art = $dom->createElementNS($ns, 'artifact');
foreach (['format','path','filename'] as $af) {
if (isset($b['artifact'][$af])) {
$art->appendChild($dom->createElementNS($ns, $af, htmlspecialchars($b['artifact'][$af], ENT_XML1)));
}
}
$buildEl->appendChild($art);
}
if (isset($b['dependencies'])) {
$deps = $dom->createElementNS($ns, 'dependencies');
foreach ($b['dependencies'] as $d) {
$req = $dom->createElementNS($ns, 'requires', '');
$req->setAttribute('name', $d['name']);
if (isset($d['version'])) {
$req->setAttribute('version', $d['version']);
}
if (isset($d['type'])) {
$req->setAttribute('type', $d['type']);
}
$deps->appendChild($req);
}
$buildEl->appendChild($deps);
}
$root->appendChild($buildEl);
}
if (!empty($enrichment['deploy'])) {
$deploy = $dom->createElementNS($ns, 'deploy');
foreach ($enrichment['deploy'] as $t) {
$target = $dom->createElementNS($ns, 'target');
$target->setAttribute('name', $t['name']);
$target->appendChild($dom->createElementNS($ns, 'host', '${{ secrets.' . strtoupper($t['name']) . '_HOST }}'));
$target->appendChild($dom->createElementNS($ns, 'path', '${{ secrets.' . strtoupper($t['name']) . '_PATH }}'));
if (isset($t['method'])) {
$target->appendChild($dom->createElementNS($ns, 'method', $t['method']));
}
if (isset($t['branch'])) {
$target->appendChild($dom->createElementNS($ns, 'branch', htmlspecialchars($t['branch'], ENT_XML1)));
}
if (isset($t['src_dir'])) {
$target->appendChild($dom->createElementNS($ns, 'src-dir', htmlspecialchars($t['src_dir'], ENT_XML1)));
}
$deploy->appendChild($target);
}
$root->appendChild($deploy);
}
if (!empty($enrichment['scripts'])) {
$scriptsEl = $dom->createElementNS($ns, 'scripts');
foreach ($enrichment['scripts'] as $s) {
$script = $dom->createElementNS($ns, 'script');
$script->setAttribute('name', $s['name']);
if (isset($s['phase'])) {
$script->setAttribute('phase', $s['phase']);
}
$script->appendChild($dom->createElementNS($ns, 'command', htmlspecialchars($s['command'], ENT_XML1)));
if (isset($s['desc'])) {
$script->appendChild($dom->createElementNS($ns, 'description', htmlspecialchars($s['desc'], ENT_XML1)));
}
if (isset($s['runner'])) {
$script->appendChild($dom->createElementNS($ns, 'runner', htmlspecialchars($s['runner'], ENT_XML1)));
}
$scriptsEl->appendChild($script);
}
$root->appendChild($scriptsEl);
}
return $dom->saveXML();
}
/** @return array{int, string} */
private function safeExec(string $command, string $cwd = '.'): array
{
$proc = proc_open($command, [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes, $cwd);
if (!is_resource($proc)) {
return [1, "proc_open failed"];
}
$stdout = stream_get_contents($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
return [proc_close($proc), trim($stdout . "\n" . $stderr)];
}
private function rmTree(string $dir): void
{
if (!is_dir($dir)) {
return;
}
$it = new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS);
$files = new \RecursiveIteratorIterator($it, \RecursiveIteratorIterator::CHILD_FIRST);
foreach ($files as $file) {
if ($file->isDir()) {
@rmdir($file->getPathname());
} else {
@chmod($file->getPathname(), 0777);
@unlink($file->getPathname());
}
}
@rmdir($dir);
}
/** @return array{int, string} */
private function gitCmd(string $workDir, string ...$args): array
{
$cmd = 'git';
foreach ($args as $a) {
$cmd .= ' ' . escapeshellarg($a);
}
return $this->safeExec($cmd, $workDir);
}
private function fetchRepos(string $url, string $org, string $token): array
{
$repos = [];
$page = 1;
do {
$ch = curl_init("{$url}/api/v1/orgs/{$org}/repos?page={$page}&limit=50");
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ["Authorization: token {$token}"], CURLOPT_TIMEOUT => 30]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code !== 200) {
break;
}
$batch = json_decode($body, true);
if (empty($batch)) {
break;
}
$repos = array_merge($repos, $batch);
$page++;
} while (count($batch) >= 50);
return $repos;
}
}
$app = new EnrichMokostandardsXmlCli();
exit($app->execute());
@@ -1,12 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"description": "Example configuration file for file-distributor.ps1 v02.00.00",
"SourceFile": "C:\\path\\to\\your\\source\\file.txt",
"RootDirectory": "C:\\path\\to\\root\\directory",
"Depth": 1,
"DryRun": true,
"Overwrite": false,
"ConfirmEach": false,
"IncludeHidden": true,
"LogDirectory": "C:\\path\\to\\logs"
}
-32
View File
@@ -1,32 +0,0 @@
<!--
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
SPDX-License-Identifier: GPL-3.0-or-later
FILE INFORMATION
DEFGROUP: MokoPlatform.Index
INGROUP: MokoPlatform.Automation
REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
PATH: /automation/index.md
BRIEF: Automation directory index
-->
# Docs Index: /api/automation
## Purpose
This index provides navigation to documentation within this folder.
## Documents
- [README-file-distributor](./README-file-distributor.md)
- [README](./README.md)
## Metadata
- **Document Type:** index
- **Auto-generated:** This file is automatically generated by rebuild_indexes.py
## Revision History
| Date | Author | Change | Notes |
| ---------- | ------------------ | ----------------- | ------------------------------------------ |
| Auto | rebuild_indexes.py | Automated update | Generated by documentation index automation |
-300
View File
@@ -1,300 +0,0 @@
#!/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
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /automation/migrate_to_gitea.php
* BRIEF: Migrate repositories from GitHub to self-hosted Gitea instance
*
* USAGE
* php automation/migrate_to_gitea.php --dry-run
* 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 --resume
*/
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use MokoEnterprise\CheckpointManager;
use MokoEnterprise\CliFramework;
use MokoEnterprise\Config;
use MokoEnterprise\PlatformAdapterFactory;
use MokoEnterprise\GitHubAdapter;
use MokoEnterprise\MokoGiteaAdapter;
/**
* Gitea Migration Script
*
* Migrates repositories from GitHub to a self-hosted Gitea instance.
* Uses Gitea's built-in migration endpoint for git history, tags, releases,
* issues, and labels. Post-migration applies branch protection, topics,
* and workflow conversion.
*/
class MigrateToGitea extends CliFramework
{
private ?GitHubAdapter $github = null;
private ?MokoGiteaAdapter $gitea = null;
private ?CheckpointManager $checkpoints = null;
protected function configure(): void
{
$this->setDescription('Migrate repositories from GitHub to Gitea');
$this->addArgument('--dry-run', 'Show what would be migrated without making changes', false);
$this->addArgument('--repos', 'Specific repositories to migrate (space-separated)', '');
$this->addArgument('--exclude', 'Repositories to exclude (space-separated)', '');
$this->addArgument('--skip-archived', 'Skip archived repositories', false);
$this->addArgument('--resume', 'Resume from last checkpoint', false);
$this->addArgument('--github-token', 'GitHub token override', '');
$this->addArgument('--gitea-token', 'Gitea token override', '');
}
protected function run(): int
{
$dryRun = (bool) $this->getArgument('--dry-run');
$specificRepos = array_filter(explode(' ', (string) $this->getArgument('--repos')));
$excludeRepos = array_filter(explode(' ', (string) $this->getArgument('--exclude')));
$skipArchived = (bool) $this->getArgument('--skip-archived');
$resume = (bool) $this->getArgument('--resume');
$config = Config::load();
// Override tokens if provided
$ghToken = (string) $this->getArgument('--github-token');
$giteaToken = (string) $this->getArgument('--gitea-token');
if ($ghToken !== '') {
$config->set('github.token', $ghToken);
}
if ($giteaToken !== '') {
$config->set('gitea.token', $giteaToken);
}
// Create both adapters
try {
$adapters = PlatformAdapterFactory::createBoth($config);
$this->github = $adapters['github'];
$this->gitea = $adapters['gitea'];
} catch (\RuntimeException $e) {
$this->log('ERROR', $e->getMessage());
return 1;
}
$this->checkpoints = new CheckpointManager('.checkpoints/migration');
$org = $config->getString('github.organization', 'MokoConsulting');
$giteaOrg = $config->getString('gitea.organization', 'MokoConsulting');
echo "=== Gitea Migration Tool ===\n";
echo "Source: GitHub ({$org})\n";
echo "Destination: Gitea ({$giteaOrg}) at " . $config->getString('gitea.url') . "\n";
echo "Mode: " . ($dryRun ? 'DRY RUN' : 'LIVE') . "\n\n";
// ── Phase 1: Discovery ──────────────────────────────────────────
$this->section('Phase 1: Discovery');
$ghRepos = $this->github->listOrgRepos($org, $skipArchived);
echo "Found " . count($ghRepos) . " repositories on GitHub\n";
// Filter repos
if (!empty($specificRepos)) {
$ghRepos = array_filter($ghRepos, fn($r) => in_array($r['name'], $specificRepos, true));
}
if (!empty($excludeRepos)) {
$ghRepos = array_filter($ghRepos, fn($r) => !in_array($r['name'], $excludeRepos, true));
}
// Check which already exist on Gitea
$giteaRepos = [];
try {
$existing = $this->gitea->listOrgRepos($giteaOrg);
foreach ($existing as $r) {
$giteaRepos[$r['name']] = true;
}
} catch (\Exception $e) {
echo "Note: Could not list Gitea repos (org may not exist yet): {$e->getMessage()}\n";
}
$toMigrate = [];
$toSkip = [];
foreach ($ghRepos as $repo) {
$name = $repo['name'];
if (isset($giteaRepos[$name])) {
$toSkip[] = $name;
} else {
$toMigrate[] = $repo;
}
}
echo "\nMigration plan:\n";
echo " Migrate: " . count($toMigrate) . " repositories\n";
echo " Skip: " . count($toSkip) . " (already on Gitea)\n";
if (!empty($toSkip)) {
echo " Skipped: " . implode(', ', $toSkip) . "\n";
}
echo "\n";
if (empty($toMigrate)) {
echo "Nothing to migrate.\n";
return 0;
}
if ($dryRun) {
echo "Repositories to migrate:\n";
foreach ($toMigrate as $repo) {
$vis = $repo['private'] ? 'private' : 'public';
echo " - {$repo['name']} ({$vis})\n";
}
echo "\nDry run complete. Use without --dry-run to execute.\n";
return 0;
}
// ── Phase 2: Migrate ────────────────────────────────────────────
$this->section('Phase 2: Migration');
$ghToken = $config->getString('github.token');
$results = ['migrated' => [], 'failed' => [], 'skipped' => $toSkip];
// Resume support
$checkpoint = $resume ? $this->checkpoints->loadCheckpoint('gitea_migration') : null;
$startFrom = $checkpoint['last_completed'] ?? '';
$skipUntil = !empty($startFrom);
foreach ($toMigrate as $index => $repo) {
$name = $repo['name'];
if ($skipUntil) {
if ($name === $startFrom) {
$skipUntil = false;
}
echo " Skipping {$name} (already migrated)\n";
continue;
}
echo "\n [{$index}/{" . count($toMigrate) . "}] Migrating {$name}...\n";
try {
// Shallow migration — copy current branch state only, no past
// commit history. This gives every repo a clean start on Gitea.
$this->gitea->migrateRepository([
'clone_addr' => "https://github.com/{$org}/{$name}.git",
'repo_name' => $name,
'repo_owner' => $giteaOrg,
'service' => 'github',
'auth_token' => $ghToken,
'mirror' => false,
'private' => $repo['private'],
'issues' => false,
'labels' => true,
'milestones' => false,
'releases' => false,
'pull_requests' => false,
'wiki' => false,
]);
echo " Migrated successfully\n";
$results['migrated'][] = $name;
// Save checkpoint after each successful migration
$this->checkpoints->saveCheckpoint('gitea_migration', [
'last_completed' => $name,
'migrated' => $results['migrated'],
'failed' => $results['failed'],
]);
} catch (\Exception $e) {
echo " FAILED: " . $e->getMessage() . "\n";
$results['failed'][] = ['name' => $name, 'error' => $e->getMessage()];
$this->gitea->getApiClient()->resetCircuitBreaker();
}
}
// ── Phase 3: Post-migration ─────────────────────────────────────
$this->section('Phase 3: Post-migration');
foreach ($results['migrated'] as $name) {
echo " Post-processing {$name}...\n";
try {
// Apply topics from GitHub
$ghTopics = $this->github->getRepoTopics($org, $name);
if (!empty($ghTopics)) {
$this->gitea->setRepoTopics($giteaOrg, $name, $ghTopics);
echo " Topics applied\n";
}
// Apply branch protection
$this->gitea->setBranchProtection($giteaOrg, $name, 'main', [
'required_reviews' => 1,
'dismiss_stale' => true,
'block_on_rejected' => true,
]);
echo " Branch protection applied\n";
} catch (\Exception $e) {
echo " Warning: post-processing issue: " . $e->getMessage() . "\n";
$this->gitea->getApiClient()->resetCircuitBreaker();
}
}
// ── Phase 4: Verification ───────────────────────────────────────
$this->section('Phase 4: Verification');
$report = "## Migration Report\n\n";
$report .= "**Date:** " . gmdate('Y-m-d H:i:s') . " UTC\n";
$report .= "**Source:** GitHub ({$org})\n";
$report .= "**Destination:** Gitea ({$giteaOrg})\n\n";
$report .= "### Results\n\n";
$report .= "| Status | Count |\n|--------|-------|\n";
$report .= "| Migrated | " . count($results['migrated']) . " |\n";
$report .= "| Failed | " . count($results['failed']) . " |\n";
$report .= "| Skipped (existing) | " . count($results['skipped']) . " |\n\n";
if (!empty($results['migrated'])) {
$report .= "### Migrated Repositories\n\n";
foreach ($results['migrated'] as $name) {
$report .= "- {$name}\n";
}
$report .= "\n";
}
if (!empty($results['failed'])) {
$report .= "### Failed Repositories\n\n";
foreach ($results['failed'] as $fail) {
$report .= "- **{$fail['name']}**: {$fail['error']}\n";
}
$report .= "\n";
}
echo $report;
// Create summary issue on Gitea
try {
$this->gitea->createIssue(
$giteaOrg,
'moko-platform',
'chore: GitHub → Gitea migration report — ' . count($results['migrated']) . ' repos migrated',
$report,
['labels' => ['automation', 'type: chore']]
);
echo "Migration report issue created on Gitea.\n";
} catch (\Exception $e) {
echo "Could not create report issue: " . $e->getMessage() . "\n";
}
echo "\nMigration complete: " . count($results['migrated']) . " migrated, "
. count($results['failed']) . " failed, "
. count($results['skipped']) . " skipped\n";
return count($results['failed']) > 0 ? 1 : 0;
}
}
$script = new MigrateToGitea('migrate_to_gitea', 'Migrate repositories from GitHub to Gitea');
exit($script->execute());
-682
View File
@@ -1,682 +0,0 @@
#!/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/moko-platform
* PATH: /automation/push_files.php
* BRIEF: Push one or more specific files to one or more remote repositories
*/
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoEnterprise\{
ApiClient,
AuditLogger,
CliFramework,
Config,
GitPlatformAdapter,
MetricsCollector,
PlatformAdapterFactory,
ProjectTypeDetector
};
/**
* Targeted File Push Tool
*
* Pushes one or more specific files from moko-platform templates to one or
* more remote repositories — without running a full sync.
*
* Files are specified by their destination path as they appear in the target
* repository (e.g., ".github/ISSUE_TEMPLATE/config.yml"). The tool looks up
* the matching source template from the appropriate platform definition.
*
* Files may also be given as "source:destination" pairs to bypass definition
* lookup and push any arbitrary local file.
*
* Usage:
* php push_files.php --files=.github/ISSUE_TEMPLATE/config.yml --repos=MokoCRM
* php push_files.php --files=".github/workflows/ci.yml,.github/workflows/codeql-analysis.yml" --repos=MokoCRM,WaasComponent
* php push_files.php --files=templates/foo.txt:docs/foo.txt --repos=MyRepo --direct
*/
class PushFiles extends CliFramework
{
public const DEFAULT_ORG = 'MokoConsulting';
public const VERSION = '09.23.00';
private ApiClient $api;
private GitPlatformAdapter $adapter;
private AuditLogger $logger;
private ProjectTypeDetector $typeDetector;
/**
* Setup command-line arguments
*/
protected function configure(): void
{
$this->setDescription('Push files to remote repositories');
$this->addArgument('--org', 'GitHub organization', self::DEFAULT_ORG);
$this->addArgument('--repos', 'Target repos (comma-separated)', '');
$this->addArgument('--files', 'Files to push (comma-separated)', '');
$this->addArgument('--message', 'Custom commit message', '');
$this->addArgument('--branch', 'Target branch for direct pushes', '');
$this->addArgument('--direct', 'Push directly instead of PR', false);
$this->addArgument('--yes', 'Auto-confirm without prompting', false);
$this->addArgument('--no-issue', 'Skip creating tracking issue', false);
}
/**
* Main execution
*/
protected function run(): int
{
$this->log('📦 moko-platform File Push v' . self::VERSION, 'INFO');
if (!$this->initializeComponents()) {
return 1;
}
$org = $this->getArgument('--org', self::DEFAULT_ORG);
$reposArg = $this->getArgument('--repos', '');
$filesArg = $this->getArgument('--files', '');
$direct = $this->getArgument('--direct', false);
$autoYes = $this->getArgument('--yes', false);
// Validate required arguments
if (empty($reposArg)) {
$this->log('❌ --repos is required. Specify one or more repository names.', 'ERROR');
$this->log(' Example: --repos=MokoCRM,WaasComponent', 'ERROR');
return 1;
}
if (empty($filesArg)) {
$this->log('❌ --files is required. Specify destination paths or source:destination pairs.', 'ERROR');
$this->log(' Example: --files=.github/ISSUE_TEMPLATE/config.yml', 'ERROR');
return 1;
}
$repos = $this->parseList($reposArg);
$files = $this->parseList($filesArg);
$this->log("Organisation: {$org}", 'INFO');
$this->log('Repositories: ' . implode(', ', $repos), 'INFO');
$this->log('Files: ' . implode(', ', $files), 'INFO');
$this->log('Mode: ' . ($direct ? 'direct commit' : 'pull request'), 'INFO');
// Resolve file mappings for each repo
$this->log("\n🔍 Resolving file mappings...", 'INFO');
$repoFileMaps = $this->buildRepoFileMaps($org, $repos, $files);
if (empty($repoFileMaps)) {
$this->log('❌ No files could be resolved. Check file paths and platform definitions.', 'ERROR');
return 1;
}
// Confirm before proceeding
if (!$autoYes && !$this->confirmPush($repoFileMaps, $direct)) {
$this->log('❌ Cancelled.', 'INFO');
return 0;
}
// Execute pushes
$results = $this->executePushes($org, $repoFileMaps, $direct);
$this->displayResults($results);
if ($results['failed'] > 0 && !isset($this->options['no-issue']) && !$this->dryRun) {
$this->createFailureIssue($org, $results);
}
return $results['failed'] > 0 ? 1 : 0;
}
/**
* Initialize enterprise components
*/
private function initializeComponents(): bool
{
$config = Config::load();
try {
$this->adapter = PlatformAdapterFactory::create($config);
$this->api = $this->adapter->getApiClient();
$this->logger = new AuditLogger('push_files');
$this->typeDetector = new ProjectTypeDetector($this->logger);
$platform = $this->adapter->getPlatformName();
$this->log("✓ Components initialized for platform: {$platform}", 'INFO');
return true;
} catch (\Exception $e) {
$this->log('❌ Failed to initialize: ' . $e->getMessage(), 'ERROR');
return false;
}
}
/**
* Parse a comma- or space-separated list into a clean array
*/
private function parseList(string $input): array
{
return array_values(array_filter(
array_map('trim', preg_split('/[\s,]+/', $input)),
fn($v) => $v !== ''
));
}
/**
* Build per-repo file maps: repo → [ [source, destination], … ]
*
* Each entry in $files is either:
* - "destination/path" → looked up in the platform definition
* - "source/path:destination/path" → used as-is (raw mode)
*
* @param string[] $repos
* @param string[] $files
* @return array<string, list<array{source: string, destination: string}>>
*/
private function buildRepoFileMaps(string $org, array $repos, array $files): array
{
$repoRoot = dirname(__DIR__, 2);
$maps = [];
foreach ($repos as $repo) {
// Detect the repo's platform so we load the right definition
$platform = $this->detectRepoPlatform($org, $repo);
$this->log(" {$repo}: platform = {$platform}", 'INFO');
$resolved = [];
foreach ($files as $fileSpec) {
if (str_contains($fileSpec, ':')) {
// Raw source:destination pair
[$src, $dest] = explode(':', $fileSpec, 2);
} else {
// Same path as source and destination
$src = $fileSpec;
$dest = $fileSpec;
}
$dest = ltrim($dest, '/');
$srcAbs = rtrim($repoRoot, '/') . '/' . ltrim($src, '/');
if (!file_exists($srcAbs)) {
$this->log(" ⚠️ Source not found for {$repo}: {$src}", 'WARN');
continue;
}
$resolved[] = ['source' => $srcAbs, 'destination' => $dest];
$this->log("{$dest}", 'INFO');
}
if (!empty($resolved)) {
$maps[$repo] = $resolved;
}
}
return $maps;
}
/**
* Detect platform for a repo via manifest or live detection.
*/
private function detectRepoPlatform(string $org, string $repo): string
{
// Read platform from repo's .mokogitea/manifest.xml via API
try {
$manifestData = $this->adapter->getFileContent($org, $repo, '.mokogitea/manifest.xml', 'main');
if (!empty($manifestData)) {
$xml = @simplexml_load_string($manifestData);
if ($xml !== false) {
$platform = (string)($xml->governance->platform ?? '');
if (!empty($platform)) {
return $platform;
}
}
}
} catch (\Exception $e) {
// Fall through to local detection
}
// Fall back to live detection
try {
$result = $this->typeDetector->detect('.');
return $result['type'] ?? 'default';
} catch (\Exception $e) {
$this->log(" ⚠️ Could not detect platform for {$repo}, using 'default'", 'WARN');
return 'default';
}
}
/**
* Prompt for confirmation before pushing
*
* @param array<string, list<array{source: string, destination: string}>> $repoFileMaps
*/
private function confirmPush(array $repoFileMaps, bool $direct): bool
{
if ($this->quiet) {
return true;
}
$totalFiles = array_sum(array_map('count', $repoFileMaps));
$totalRepos = count($repoFileMaps);
$mode = $direct ? 'direct commit' : 'PR';
echo "\n";
foreach ($repoFileMaps as $repo => $entries) {
echo " {$repo}:\n";
foreach ($entries as $entry) {
echo "{$entry['destination']}\n";
}
}
echo "\n";
echo "⚠️ About to push {$totalFiles} file(s) to {$totalRepos} repo(s) via {$mode}.\n";
echo "Continue? [y/N]: ";
$handle = fopen('php://stdin', 'r');
$line = fgets($handle);
if ($handle) {
fclose($handle);
}
return is_string($line) && strtolower(trim($line)) === 'y';
}
/**
* Execute all file pushes
*
* @param array<string, list<array{source: string, destination: string}>> $repoFileMaps
* @return array{total: int, success: int, failed: int, repos: array<string, string>}
*/
private function executePushes(string $org, array $repoFileMaps, bool $direct): array
{
$results = [
'total' => count($repoFileMaps),
'success' => 0,
'failed' => 0,
'repos' => [],
];
$customMessage = $this->getArgument('--message', '');
$targetBranch = $this->getArgument('--branch', '');
foreach ($repoFileMaps as $repo => $entries) {
$this->log("\n[{$repo}] Pushing " . count($entries) . ' file(s)...', 'INFO');
try {
// Resolve the default branch
$repoData = $this->adapter->getRepo($org, $repo);
$defaultBranch = $repoData['default_branch'] ?? 'main';
$branch = $direct
? ($targetBranch ?: $defaultBranch)
: $this->createSyncBranch($org, $repo, $defaultBranch);
$pushed = 0;
foreach ($entries as $entry) {
if ($this->pushSingleFile($org, $repo, $entry['source'], $entry['destination'], $branch, $customMessage)) {
$pushed++;
$this->log("{$entry['destination']}", 'INFO');
} else {
$this->log("{$entry['destination']}", 'ERROR');
}
}
if ($pushed === 0) {
$results['failed']++;
$results['repos'][$repo] = 'failed';
continue;
}
$prNumber = null;
if (!$direct) {
$prTitle = "chore: push " . count($entries) . " file(s) from moko-platform";
$prBody = $this->buildPRBody($entries);
$pr = $this->adapter->createPullRequest(
$org,
$repo,
$prTitle,
$branch,
$defaultBranch,
$prBody,
['assignees' => ['jmiller']]
);
$prNumber = $pr['number'] ?? null;
$this->log(" 📋 PR #{$prNumber} created", 'INFO');
$results['repos'][$repo] = "pr#{$prNumber}";
} else {
$results['repos'][$repo] = 'pushed';
}
if (!isset($this->options['no-issue']) && !$this->dryRun) {
$this->createTargetRepoIssue($org, $repo, $entries, $prNumber, $direct ? $branch : null);
}
$results['success']++;
} catch (\Exception $e) {
$this->log("{$repo}: " . $e->getMessage(), 'ERROR');
$results['failed']++;
$results['repos'][$repo] = 'failed';
}
}
return $results;
}
/**
* Create a uniquely-named sync branch off the default branch
*/
private function createSyncBranch(string $org, string $repo, string $base): string
{
$branchName = 'moko/push-files-' . date('Ymd-His');
// Resolve the base branch to a commit SHA using the adapter
$sha = $this->adapter->resolveRef($org, $repo, $base);
if (empty($sha)) {
throw new \RuntimeException("Cannot resolve SHA for branch {$base} in {$repo}");
}
$this->api->post("/repos/{$org}/{$repo}/git/refs", [
'ref' => "refs/heads/{$branchName}",
'sha' => $sha,
]);
$this->log(" 🌿 Branch created: {$branchName}", 'INFO');
return $branchName;
}
/**
* Push a single file to a repository branch via the Contents API
*
* @return bool True on success
*/
private function pushSingleFile(
string $org,
string $repo,
string $sourcePath,
string $destPath,
string $branch,
string $customMessage
): bool {
$content = file_get_contents($sourcePath);
if ($content === false) {
$this->log(" ⚠️ Cannot read source: {$sourcePath}", 'WARN');
return false;
}
$message = !empty($customMessage)
? $customMessage
: "chore: update {$destPath} from moko-platform";
// Fetch existing file SHA (needed for updates)
$existingSha = null;
try {
$existing = $this->adapter->getFileContents($org, $repo, $destPath, $branch);
$existingSha = $existing['sha'] ?? null;
} catch (\Exception $e) {
// File does not exist — create it (no sha needed)
$this->adapter->getApiClient()->resetCircuitBreaker();
}
try {
$this->adapter->createOrUpdateFile(
$org,
$repo,
$destPath,
$content,
$message,
$existingSha,
$branch
);
return true;
} catch (\Exception $e) {
$this->log(" ✗ API error pushing {$destPath}: " . $e->getMessage(), 'ERROR');
return false;
}
}
/**
* Create a tracking issue in the target repository after a successful push.
*
* @param list<array{source: string, destination: string}> $entries
*/
private function createTargetRepoIssue(
string $org,
string $repo,
array $entries,
?int $prNumber,
?string $directBranch
): void {
$now = gmdate('Y-m-d H:i:s') . ' UTC';
$version = self::VERSION;
$source = $this->adapter->getRepoWebUrl($org, 'moko-platform');
$title = "chore: moko-platform file push tracking";
$deliveryLine = $prNumber !== null
? "| **Pull request** | [#{$prNumber}](" . $this->adapter->getPullRequestWebUrl($org, $repo, $prNumber) . ") |"
: "| **Delivery** | Direct commit to `{$directBranch}` |";
$fileRows = implode("\n", array_map(
fn($e) => "- `{$e['destination']}`",
$entries
));
$body = <<<MD
## moko-platform File Push
One or more files were pushed to this repository from moko-platform.
| Field | Value |
|-------|-------|
| **Pushed** | {$now} |
| **Standards version** | `{$version}` |
{$deliveryLine}
| **Source** | [{$source}]({$source}) |
### Files pushed
{$fileRows}
---
*Generated automatically by [moko-platform]({$source}) `push_files.php`*
MD;
$body = preg_replace('/^ /m', '', $body);
$labels = ['standards-update', 'moko-platform', 'type: chore', 'automation'];
try {
$existing = $this->api->get("/repos/{$org}/{$repo}/issues", [
'labels' => 'standards-update',
'state' => 'all',
'per_page' => 1,
'sort' => 'created',
'direction' => 'desc',
]);
$existing = array_values($existing);
if (!empty($existing) && isset($existing[0]['number'])) {
$num = $existing[0]['number'];
$patch = ['title' => $title, 'body' => $body, 'assignees' => ['jmiller']];
if (($existing[0]['state'] ?? 'open') === 'closed') {
$patch['state'] = 'open';
}
$this->api->patch("/repos/{$org}/{$repo}/issues/{$num}", $patch);
try {
$this->api->post("/repos/{$org}/{$repo}/issues/{$num}/labels", ['labels' => $labels]);
} catch (\Exception $le) {
/* non-fatal */
}
$this->log(" 📋 Tracking issue #{$num} updated in {$repo}", 'INFO');
} else {
$issue = $this->api->post("/repos/{$org}/{$repo}/issues", [
'title' => $title,
'body' => $body,
'labels' => $labels,
'assignees' => ['jmiller'],
]);
$num = $issue['number'] ?? null;
$this->log(" 📋 Tracking issue #{$num} created in {$repo}", 'INFO');
}
// Cross-link: patch the sync PR body to reference the tracking issue
// so GitHub shows it in the PR's Development sidebar.
if ($prNumber !== null && is_int($num)) {
try {
$pr = $this->api->get("/repos/{$org}/{$repo}/pulls/{$prNumber}");
$currentBody = $pr['body'] ?? '';
$ref = "Linked to #{$num}";
if (!str_contains($currentBody, $ref)) {
$this->api->patch("/repos/{$org}/{$repo}/pulls/{$prNumber}", [
'body' => $ref . "\n\n" . $currentBody,
]);
}
} catch (\Exception $le) {
/* non-fatal */
}
}
} catch (\Exception $e) {
$this->log(" ⚠️ Could not create/update tracking issue in {$repo}: " . $e->getMessage(), 'WARN');
}
}
/**
* Create or update a failure issue in moko-platform when repos fail to receive files.
* Uses the 'push-failure' label. Reopens a closed issue rather than creating a duplicate.
*/
private function createFailureIssue(string $org, array $results): void
{
$now = gmdate('Y-m-d H:i:s') . ' UTC';
$failed = $results['failed'];
$version = self::VERSION;
$failedRepos = array_keys(array_filter(
$results['repos'] ?? [],
fn($s) => $s === 'failed'
));
$repoList = implode("\n", array_map(fn($r) => "- `{$r}`", $failedRepos));
$fileArgs = $this->getArgument('--files', '');
$title = "fix: push_files failed for {$failed} repo(s) — action required";
$body = <<<MD
## File Push Failure
`push_files.php` v{$version} encountered failures pushing files on {$now}.
### Failed repositories
{$repoList}
### Files that were being pushed
```
{$fileArgs}
```
### Next steps
1. Check the output above for the specific error per repo.
2. Fix the underlying issue (API token, branch permissions, file path, etc.).
3. Re-run: `php automation/push_files.php --org={$org} --repos=<repo> --files=<files> --yes`
4. Close this issue once resolved.
---
*Auto-created by `push_files.php` — close once resolved.*
MD;
$body = preg_replace('/^ /m', '', $body);
try {
$existing = $this->api->get("/repos/{$org}/moko-platform/issues", [
'labels' => 'push-failure',
'state' => 'all',
'per_page' => 1,
'sort' => 'created',
'direction' => 'desc',
]);
$existing = array_values($existing);
if (!empty($existing) && isset($existing[0]['number'])) {
$num = $existing[0]['number'];
$patch = ['title' => $title, 'body' => $body, 'assignees' => ['jmiller']];
if (($existing[0]['state'] ?? 'open') === 'closed') {
$patch['state'] = 'open';
}
$this->api->patch("/repos/{$org}/moko-platform/issues/{$num}", $patch);
$this->log("🚨 Failure issue #{$num} updated: {$org}/moko-platform#{$num}", 'WARN');
} else {
$issue = $this->api->post("/repos/{$org}/moko-platform/issues", [
'title' => $title,
'body' => $body,
'labels' => ['push-failure'],
'assignees' => ['jmiller'],
]);
$num = $issue['number'] ?? '?';
$this->log("🚨 Failure issue created: {$org}/moko-platform#{$num}", 'WARN');
}
} catch (\Exception $e) {
$this->log("⚠️ Could not create/update failure issue: " . $e->getMessage(), 'WARN');
}
}
/**
* Build a markdown PR body listing every pushed file
*
* @param list<array{source: string, destination: string}> $entries
*/
private function buildPRBody(array $entries): string
{
$now = gmdate('Y-m-d H:i:s') . ' UTC';
$lines = ["## moko-platform File Push\n", "**Pushed:** {$now}\n", '### Files\n'];
foreach ($entries as $entry) {
$lines[] = "- `{$entry['destination']}`";
}
$sourceUrl = $this->adapter->getRepoWebUrl(self::DEFAULT_ORG, 'moko-platform');
$lines[] = "\n---\n*Generated by [moko-platform]({$sourceUrl}) `push_files.php`*";
return implode("\n", $lines);
}
/**
* Display final results
*
* @param array{total: int, success: int, failed: int, repos: array<string, string>} $results
*/
private function displayResults(array $results): void
{
$this->log("\n" . str_repeat('=', 60), 'INFO');
$this->log('📊 Push Complete', 'INFO');
$this->log(str_repeat('=', 60), 'INFO');
$this->log(sprintf('Total: %d repos', $results['total']), 'INFO');
$this->log(sprintf('Success: %d', $results['success']), 'INFO');
$this->log(sprintf('Failed: %d', $results['failed']), 'INFO');
if ($this->verbose) {
$this->log("\n📋 Details:", 'INFO');
foreach ($results['repos'] as $repo => $outcome) {
$icon = str_starts_with($outcome, 'pr#') || $outcome === 'pushed' ? '✓' : '✗';
$this->log(" {$icon} {$repo}: {$outcome}", 'INFO');
}
}
$this->log(str_repeat('=', 60), 'INFO');
}
}
// Execute if run directly
if (php_sapi_name() === 'cli' && isset($argv[0]) && realpath($argv[0]) === __FILE__) {
$app = new PushFiles();
exit($app->execute());
}
-345
View File
@@ -1,345 +0,0 @@
#!/usr/bin/env php
<?php
/**
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: MokoPlatform.Automation
* INGROUP: MokoPlatform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /automation/push_manifest_xml.php
* BRIEF: Push XML manifests to all governed repositories
*/
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoEnterprise\CliFramework;
use MokoEnterprise\MokoStandardsParser;
class PushManifestXmlCli extends CliFramework
{
private const CRM_PLATFORM_REPOS = ['MokoDolibarr', 'MokoDoliMods'];
protected function configure(): void
{
$this->setDescription('Push XML manifest.xml to all governed repositories');
$this->addArgument('--repo', 'Filter to a single repo name', '');
$this->addArgument('--skip', 'Comma-separated list of repos to skip', '');
$this->addArgument('--force', 'Force overwrite even if already XML', false);
}
protected function run(): int
{
$giteaUrl = rtrim(getenv('GITEA_URL') ?: 'https://git.mokoconsulting.tech', '/');
$giteaOrg = getenv('GITEA_ORG') ?: 'MokoConsulting';
$token = getenv('GA_TOKEN') ?: getenv('GH_TOKEN') ?: '';
$force = $this->getArgument('--force');
$repoFilter = $this->getArgument('--repo') ?: null;
$skipStr = $this->getArgument('--skip');
$skipRepos = $skipStr !== '' ? array_map('trim', explode(',', $skipStr)) : [];
$parser = new MokoStandardsParser();
$tmpBase = sys_get_temp_dir() . '/moko-manifest-push-' . getmypid();
echo "=== moko-platform XML Manifest Push ===\n";
echo "Org: {$giteaOrg}\n";
echo "Mode: " . ($this->dryRun ? "DRY RUN" : "LIVE") . "\n";
if ($repoFilter) {
echo "Filter: {$repoFilter}\n";
}
echo "\n";
if (empty($token)) {
$this->log('ERROR', 'GA_TOKEN or GH_TOKEN environment variable required');
return 1;
}
$repos = $this->fetchRepos($giteaUrl, $giteaOrg, $token);
echo "Found " . count($repos) . " repositories\n\n";
$stats = ['created' => 0, 'updated' => 0, 'skipped' => 0, 'failed' => 0];
foreach ($repos as $repo) {
$name = $repo['name'];
if ($repoFilter && $name !== $repoFilter) {
continue;
}
if (in_array($name, $skipRepos, true)) {
echo " SKIP {$name} (excluded)\n";
$stats['skipped']++;
continue;
}
if ($repo['archived'] ?? false) {
echo " SKIP {$name} (archived)\n";
$stats['skipped']++;
continue;
}
$platform = $this->detectPlatform($repo);
$defaultBranch = $repo['default_branch'] ?? 'main';
$httpsUrl = $repo['clone_url'] ?? "{$giteaUrl}/{$giteaOrg}/{$name}.git";
$authedUrl = preg_replace('#^https://#', "https://gitea-actions:{$token}@", $httpsUrl);
echo " {$name} [{$platform}] ... ";
// Generate XML manifest
$xmlContent = $parser->generate([
'name' => $name,
'org' => $giteaOrg,
'platform' => $platform,
'standards_version' => '04.07.00',
'description' => $repo['description'] ?? '',
'license' => 'GPL-3.0-or-later',
'topics' => $repo['topics'] ?? [],
'language' => $repo['language'] ?? MokoStandardsParser::platformLanguage($platform),
'package_type' => MokoStandardsParser::platformPackageType($platform),
'last_synced' => date('c'),
]);
if ($this->dryRun) {
echo "WOULD WRITE ({$platform})\n";
$stats['created']++;
continue;
}
// Clone shallow via HTTPS (token-authed)
$workDir = "{$tmpBase}/{$name}";
@mkdir($workDir, 0755, true);
[$ret, $out] = $this->safeExec(
'git clone --depth 1 --branch ' . escapeshellarg($defaultBranch) . ' '
. escapeshellarg($authedUrl) . ' ' . escapeshellarg($workDir)
);
if ($ret !== 0) {
echo "FAIL (clone)\n";
fprintf(STDERR, " %s\n", $out);
$stats['failed']++;
continue;
}
// Check if already XML and up-to-date
$manifestPath = "{$workDir}/.mokogitea/manifest.xml";
$existingIsXml = file_exists($manifestPath) && str_contains(file_get_contents($manifestPath), '<moko-platform');
if ($existingIsXml && !$force) {
$existingPlatform = $parser->extractPlatform(file_get_contents($manifestPath));
if ($existingPlatform === $platform) {
echo "SKIP (already XML)\n";
$stats['skipped']++;
$this->rmTree($workDir);
continue;
}
}
// Write manifest
@mkdir("{$workDir}/.gitea", 0755, true);
file_put_contents($manifestPath, $xmlContent);
// Delete legacy files if present
$legacyDeleted = [];
foreach (['.mokostandards', '.github/.mokostandards', '.gitea/.mokostandards', '.mokogitea/.mokostandards'] as $legacy) {
$legacyPath = "{$workDir}/{$legacy}";
if (file_exists($legacyPath)) {
unlink($legacyPath);
$legacyDeleted[] = $legacy;
}
}
// Commit
$isNew = !$existingIsXml;
$commitMsg = $isNew
? 'chore: add XML manifest.xml'
: 'chore: update manifest.xml';
if (!empty($legacyDeleted)) {
$commitMsg .= "\n\nRemoved legacy: " . implode(', ', $legacyDeleted);
}
$this->gitCmd($workDir, 'config', 'user.name', 'gitea-actions[bot]');
$this->gitCmd($workDir, 'config', 'user.email', 'gitea-actions[bot]@git.mokoconsulting.tech');
$this->gitCmd($workDir, 'add', '.mokogitea/manifest.xml');
foreach ($legacyDeleted as $lf) {
$this->gitCmd($workDir, 'add', $lf);
}
[$commitRet, $commitOut] = $this->gitCmd($workDir, 'commit', '-m', $commitMsg);
if ($commitRet !== 0 && str_contains($commitOut, 'nothing to commit')) {
echo "SKIP (no changes)\n";
$stats['skipped']++;
$this->rmTree($workDir);
continue;
}
if ($commitRet !== 0) {
echo "FAIL (commit)\n";
fprintf(STDERR, " %s\n", $commitOut);
$stats['failed']++;
$this->rmTree($workDir);
continue;
}
[$pushRet, $pushOut] = $this->gitCmd($workDir, 'push', 'origin', $defaultBranch);
if ($pushRet !== 0) {
echo "FAIL (push)\n";
fprintf(STDERR, " %s\n", $pushOut);
$stats['failed']++;
} else {
$action = $isNew ? 'CREATED' : 'UPDATED';
echo "{$action}\n";
$stats[$isNew ? 'created' : 'updated']++;
}
// Cleanup
$this->rmTree($workDir);
}
// Cleanup tmp base
@rmdir($tmpBase);
echo "\n=== Summary ===\n";
echo "Created: {$stats['created']}\n";
echo "Updated: {$stats['updated']}\n";
echo "Skipped: {$stats['skipped']}\n";
echo "Failed: {$stats['failed']}\n";
return 0;
}
private function detectPlatform(array $repo): string
{
$name = $repo['name'] ?? '';
$nameLower = strtolower($name);
$description = strtolower($repo['description'] ?? '');
$topics = $repo['topics'] ?? [];
if (in_array($name, self::CRM_PLATFORM_REPOS, true)) {
return 'crm-platform';
}
if (in_array('dolibarr-platform', $topics)) {
return 'crm-platform';
}
if (in_array('joomla-template', $topics)) {
return 'joomla-template';
}
if (in_array('joomla', $topics) || in_array('joomla-extension', $topics)) {
return 'waas-component';
}
if (in_array('dolibarr', $topics) || in_array('dolibarr-module', $topics)) {
return 'crm-module';
}
if (str_contains($nameLower, 'template') && (str_contains($nameLower, 'joomla') || str_contains($nameLower, 'tpl'))) {
return 'joomla-template';
}
if (str_contains($nameLower, 'joomla') || str_contains($nameLower, 'waas')) {
return 'waas-component';
}
if (str_contains($nameLower, 'doli') || str_contains($nameLower, 'crm')) {
return 'crm-module';
}
if (str_contains($description, 'joomla template')) {
return 'joomla-template';
}
if (str_contains($description, 'joomla') || str_contains($description, 'component')) {
return 'waas-component';
}
if (str_contains($description, 'dolibarr') || str_contains($description, 'module')) {
return 'crm-module';
}
if (str_contains($nameLower, 'standard')) {
return 'standards-repository';
}
return 'default-repository';
}
/**
* @return array{int, string}
*/
private function safeExec(string $command, string $cwd = '.'): array
{
$proc = proc_open(
$command,
[1 => ['pipe', 'w'], 2 => ['pipe', 'w']],
$pipes,
$cwd
);
if (!is_resource($proc)) {
return [1, "proc_open failed for: {$command}"];
}
$stdout = stream_get_contents($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
$code = proc_close($proc);
return [$code, trim($stdout . "\n" . $stderr)];
}
private function rmTree(string $dir): void
{
if (!is_dir($dir)) {
return;
}
$it = new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS);
$files = new \RecursiveIteratorIterator($it, \RecursiveIteratorIterator::CHILD_FIRST);
foreach ($files as $file) {
if ($file->isDir()) {
@rmdir($file->getPathname());
} else {
@chmod($file->getPathname(), 0777);
@unlink($file->getPathname());
}
}
@rmdir($dir);
}
/**
* @return array{int, string}
*/
private function gitCmd(string $workDir, string ...$args): array
{
$cmd = 'git';
foreach ($args as $a) {
$cmd .= ' ' . escapeshellarg($a);
}
return $this->safeExec($cmd, $workDir);
}
private function fetchRepos(string $url, string $org, string $token): array
{
$repos = [];
$page = 1;
do {
$ch = curl_init("{$url}/api/v1/orgs/{$org}/repos?page={$page}&limit=50");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: token {$token}"],
CURLOPT_TIMEOUT => 30,
]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code !== 200) {
$this->log('ERROR', "API error (HTTP {$code}) fetching repos page {$page}");
break;
}
$batch = json_decode($body, true);
if (empty($batch)) {
break;
}
$repos = array_merge($repos, $batch);
$page++;
} while (count($batch) >= 50);
return $repos;
}
}
$app = new PushManifestXmlCli();
exit($app->execute());
-345
View File
@@ -1,345 +0,0 @@
#!/usr/bin/env php
<?php
/**
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: MokoPlatform.Automation
* INGROUP: MokoPlatform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /automation/push_mokostandards_xml.php
* BRIEF: Push XML manifests to all governed repositories
*/
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoEnterprise\CliFramework;
use MokoEnterprise\MokoStandardsParser;
class PushMokostandardsXmlCli extends CliFramework
{
private const CRM_PLATFORM_REPOS = ['MokoDolibarr', 'MokoDoliMods'];
protected function configure(): void
{
$this->setDescription('Push XML manifests to all governed repositories');
$this->addArgument('--repo', 'Filter to a single repo name', '');
$this->addArgument('--skip', 'Comma-separated list of repos to skip', '');
$this->addArgument('--force', 'Force overwrite even if already XML', false);
}
protected function run(): int
{
$giteaUrl = rtrim(getenv('GITEA_URL') ?: 'https://git.mokoconsulting.tech', '/');
$giteaOrg = getenv('GITEA_ORG') ?: 'MokoConsulting';
$token = getenv('GA_TOKEN') ?: getenv('GH_TOKEN') ?: '';
$force = $this->getArgument('--force');
$repoFilter = $this->getArgument('--repo') ?: null;
$skipStr = $this->getArgument('--skip');
$skipRepos = $skipStr !== '' ? array_map('trim', explode(',', $skipStr)) : [];
$parser = new MokoStandardsParser();
$tmpBase = sys_get_temp_dir() . '/moko-manifest-push-' . getmypid();
echo "=== moko-platform XML Manifest Push ===\n";
echo "Org: {$giteaOrg}\n";
echo "Mode: " . ($this->dryRun ? "DRY RUN" : "LIVE") . "\n";
if ($repoFilter) {
echo "Filter: {$repoFilter}\n";
}
echo "\n";
if (empty($token)) {
$this->log('ERROR', 'GA_TOKEN or GH_TOKEN environment variable required');
return 1;
}
$repos = $this->fetchRepos($giteaUrl, $giteaOrg, $token);
echo "Found " . count($repos) . " repositories\n\n";
$stats = ['created' => 0, 'updated' => 0, 'skipped' => 0, 'failed' => 0];
foreach ($repos as $repo) {
$name = $repo['name'];
if ($repoFilter && $name !== $repoFilter) {
continue;
}
if (in_array($name, $skipRepos, true)) {
echo " SKIP {$name} (excluded)\n";
$stats['skipped']++;
continue;
}
if ($repo['archived'] ?? false) {
echo " SKIP {$name} (archived)\n";
$stats['skipped']++;
continue;
}
$platform = $this->detectPlatform($repo);
$defaultBranch = $repo['default_branch'] ?? 'main';
$httpsUrl = $repo['clone_url'] ?? "{$giteaUrl}/{$giteaOrg}/{$name}.git";
$authedUrl = preg_replace('#^https://#', "https://gitea-actions:{$token}@", $httpsUrl);
echo " {$name} [{$platform}] ... ";
// Generate XML manifest
$xmlContent = $parser->generate([
'name' => $name,
'org' => $giteaOrg,
'platform' => $platform,
'standards_version' => '04.07.00',
'description' => $repo['description'] ?? '',
'license' => 'GPL-3.0-or-later',
'topics' => $repo['topics'] ?? [],
'language' => $repo['language'] ?? MokoStandardsParser::platformLanguage($platform),
'package_type' => MokoStandardsParser::platformPackageType($platform),
'last_synced' => date('c'),
]);
if ($this->dryRun) {
echo "WOULD WRITE ({$platform})\n";
$stats['created']++;
continue;
}
// Clone shallow via HTTPS (token-authed)
$workDir = "{$tmpBase}/{$name}";
@mkdir($workDir, 0755, true);
[$ret, $out] = $this->safeExec(
'git clone --depth 1 --branch ' . escapeshellarg($defaultBranch) . ' '
. escapeshellarg($authedUrl) . ' ' . escapeshellarg($workDir)
);
if ($ret !== 0) {
echo "FAIL (clone)\n";
fprintf(STDERR, " %s\n", $out);
$stats['failed']++;
continue;
}
// Check if already XML and up-to-date
$manifestPath = "{$workDir}/.mokogitea/manifest.xml";
$existingIsXml = file_exists($manifestPath) && str_contains(file_get_contents($manifestPath), '<moko-platform');
if ($existingIsXml && !$force) {
$existingPlatform = $parser->extractPlatform(file_get_contents($manifestPath));
if ($existingPlatform === $platform) {
echo "SKIP (already XML)\n";
$stats['skipped']++;
$this->rmTree($workDir);
continue;
}
}
// Write manifest
@mkdir("{$workDir}/.gitea", 0755, true);
file_put_contents($manifestPath, $xmlContent);
// Delete legacy files if present
$legacyDeleted = [];
foreach (['.mokostandards', '.github/.mokostandards'] as $legacy) {
$legacyPath = "{$workDir}/{$legacy}";
if (file_exists($legacyPath)) {
unlink($legacyPath);
$legacyDeleted[] = $legacy;
}
}
// Commit
$isNew = !$existingIsXml;
$commitMsg = $isNew
? 'chore: add XML manifest.xml'
: 'chore: update .mokostandards to XML format';
if (!empty($legacyDeleted)) {
$commitMsg .= "\n\nRemoved legacy: " . implode(', ', $legacyDeleted);
}
$this->gitCmd($workDir, 'config', 'user.name', 'gitea-actions[bot]');
$this->gitCmd($workDir, 'config', 'user.email', 'gitea-actions[bot]@git.mokoconsulting.tech');
$this->gitCmd($workDir, 'add', '.mokogitea/manifest.xml');
foreach ($legacyDeleted as $lf) {
$this->gitCmd($workDir, 'add', $lf);
}
[$commitRet, $commitOut] = $this->gitCmd($workDir, 'commit', '-m', $commitMsg);
if ($commitRet !== 0 && str_contains($commitOut, 'nothing to commit')) {
echo "SKIP (no changes)\n";
$stats['skipped']++;
$this->rmTree($workDir);
continue;
}
if ($commitRet !== 0) {
echo "FAIL (commit)\n";
fprintf(STDERR, " %s\n", $commitOut);
$stats['failed']++;
$this->rmTree($workDir);
continue;
}
[$pushRet, $pushOut] = $this->gitCmd($workDir, 'push', 'origin', $defaultBranch);
if ($pushRet !== 0) {
echo "FAIL (push)\n";
fprintf(STDERR, " %s\n", $pushOut);
$stats['failed']++;
} else {
$action = $isNew ? 'CREATED' : 'UPDATED';
echo "{$action}\n";
$stats[$isNew ? 'created' : 'updated']++;
}
// Cleanup
$this->rmTree($workDir);
}
// Cleanup tmp base
@rmdir($tmpBase);
echo "\n=== Summary ===\n";
echo "Created: {$stats['created']}\n";
echo "Updated: {$stats['updated']}\n";
echo "Skipped: {$stats['skipped']}\n";
echo "Failed: {$stats['failed']}\n";
return 0;
}
private function detectPlatform(array $repo): string
{
$name = $repo['name'] ?? '';
$nameLower = strtolower($name);
$description = strtolower($repo['description'] ?? '');
$topics = $repo['topics'] ?? [];
if (in_array($name, self::CRM_PLATFORM_REPOS, true)) {
return 'crm-platform';
}
if (in_array('dolibarr-platform', $topics)) {
return 'crm-platform';
}
if (in_array('joomla-template', $topics)) {
return 'joomla-template';
}
if (in_array('joomla', $topics) || in_array('joomla-extension', $topics)) {
return 'waas-component';
}
if (in_array('dolibarr', $topics) || in_array('dolibarr-module', $topics)) {
return 'crm-module';
}
if (str_contains($nameLower, 'template') && (str_contains($nameLower, 'joomla') || str_contains($nameLower, 'tpl'))) {
return 'joomla-template';
}
if (str_contains($nameLower, 'joomla') || str_contains($nameLower, 'waas')) {
return 'waas-component';
}
if (str_contains($nameLower, 'doli') || str_contains($nameLower, 'crm')) {
return 'crm-module';
}
if (str_contains($description, 'joomla template')) {
return 'joomla-template';
}
if (str_contains($description, 'joomla') || str_contains($description, 'component')) {
return 'waas-component';
}
if (str_contains($description, 'dolibarr') || str_contains($description, 'module')) {
return 'crm-module';
}
if (str_contains($nameLower, 'standard')) {
return 'standards-repository';
}
return 'default-repository';
}
/**
* @return array{int, string}
*/
private function safeExec(string $command, string $cwd = '.'): array
{
$proc = proc_open(
$command,
[1 => ['pipe', 'w'], 2 => ['pipe', 'w']],
$pipes,
$cwd
);
if (!is_resource($proc)) {
return [1, "proc_open failed for: {$command}"];
}
$stdout = stream_get_contents($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
$code = proc_close($proc);
return [$code, trim($stdout . "\n" . $stderr)];
}
private function rmTree(string $dir): void
{
if (!is_dir($dir)) {
return;
}
$it = new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS);
$files = new \RecursiveIteratorIterator($it, \RecursiveIteratorIterator::CHILD_FIRST);
foreach ($files as $file) {
if ($file->isDir()) {
@rmdir($file->getPathname());
} else {
@chmod($file->getPathname(), 0777);
@unlink($file->getPathname());
}
}
@rmdir($dir);
}
/**
* @return array{int, string}
*/
private function gitCmd(string $workDir, string ...$args): array
{
$cmd = 'git';
foreach ($args as $a) {
$cmd .= ' ' . escapeshellarg($a);
}
return $this->safeExec($cmd, $workDir);
}
private function fetchRepos(string $url, string $org, string $token): array
{
$repos = [];
$page = 1;
do {
$ch = curl_init("{$url}/api/v1/orgs/{$org}/repos?page={$page}&limit=50");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: token {$token}"],
CURLOPT_TIMEOUT => 30,
]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code !== 200) {
$this->log('ERROR', "API error (HTTP {$code}) fetching repos page {$page}");
break;
}
$batch = json_decode($body, true);
if (empty($batch)) {
break;
}
$repos = array_merge($repos, $batch);
$page++;
} while (count($batch) >= 50);
return $repos;
}
}
$app = new PushMokostandardsXmlCli();
exit($app->execute());
-517
View File
@@ -1,517 +0,0 @@
#!/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/moko-platform
* PATH: /automation/repo_cleanup.php
* BRIEF: Enterprise repository cleanup — branches, PRs, issues, workflows, labels, logs
*/
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoEnterprise\{ApiClient, AuditLogger, CliFramework, Config, GitPlatformAdapter, MetricsCollector, PlatformAdapterFactory};
/**
* Enterprise Repository Cleanup
*
* Comprehensive maintenance tool for governed repositories:
* 1. Delete stale sync branches (keeps current versioned branch)
* 2. Close superseded PRs on deleted branches
* 3. Close/lock resolved tracking issues where linked PR is merged
* 4. Delete retired workflow files from repos
* 5. Clean cancelled/stale workflow runs
* 6. Delete workflow run logs older than N days
* 7. Verify and provision standard labels
* 8. Version drift detection
*/
class RepoCleanup extends CliFramework
{
private const VERSION = '09.23.00';
private const SYNC_PREFIX = 'chore/sync-moko-platform-';
private const CURRENT_BRANCH = 'chore/sync-moko-platform-v04.02.00';
/** Workflow files that have been retired and should be deleted from governed repos. */
private const RETIRED_WORKFLOWS = [
'build.yml', 'code-quality.yml', 'release-cycle.yml', 'release-pipeline.yml',
'branch-cleanup.yml', 'auto-update-changelog.yml', 'enterprise-issue-manager.yml',
'flush-actions-cache.yml', 'moko-platform-script-runner.yml', 'unified-ci.yml',
'unified-platform-testing.yml', 'reusable-build.yml', 'reusable-ci-validation.yml',
'reusable-deploy.yml', 'reusable-php-quality.yml', 'reusable-platform-testing.yml',
'reusable-project-detector.yml', 'reusable-release.yml', 'reusable-script-executor.yml',
'rebuild-docs-indexes.yml', 'setup-project-v2.yml', 'sync-docs-to-project.yml',
'release.yml', 'sync-changelogs.yml', 'version_branch.yml',
'publish-to-mokodolibarr.yml', 'ci.yml',
'deploy-rs.yml',
];
private ApiClient $api;
private GitPlatformAdapter $adapter;
protected bool $dryRun = false;
private float $startTime;
protected function configure(): void
{
$this->setDescription('Enterprise repository cleanup');
$this->addArgument('--org', 'GitHub organization', 'MokoConsulting');
$this->addArgument('--repos', 'Specific repos (space-separated)', '');
$this->addArgument('--skip-archived', 'Skip archived repos', false);
$this->addArgument('--close-issues', 'Close resolved tracking issues', false);
$this->addArgument('--lock-old-issues', 'Lock issues closed >30 days', false);
$this->addArgument('--clean-workflows', 'Delete stale workflow runs', false);
$this->addArgument('--clean-logs', 'Delete old workflow logs', false);
$this->addArgument('--log-days', 'Days to keep logs', '30');
$this->addArgument('--delete-retired', 'Delete retired workflows', false);
$this->addArgument('--check-labels', 'Verify labels exist', false);
$this->addArgument('--check-drift', 'Check version drift', false);
$this->addArgument('--all', 'Run all operations', false);
$this->addArgument('--yes', 'Auto-confirm', false);
$this->addArgument('--json', 'Output as JSON', false);
}
protected function run(): int
{
$this->startTime = microtime(true);
$org = $this->getArgument('--org', 'MokoConsulting');
$this->dryRun = (bool) $this->getArgument('--dry-run', false);
$runAll = (bool) $this->getArgument('--all', false);
$config = Config::load();
try {
$this->adapter = PlatformAdapterFactory::create($config);
$this->api = $this->adapter->getApiClient();
} catch (\Exception $e) {
$this->errorMsg('Failed to initialize platform adapter: ' . $e->getMessage());
return 1;
}
$this->logMsg("🧹 moko-platform Repository Cleanup v" . self::VERSION);
$this->logMsg("Organization: {$org}");
$this->logMsg("Current sync branch: " . self::CURRENT_BRANCH);
if ($this->dryRun) {
$this->logMsg("⚠️ DRY RUN — no changes will be made");
}
$this->logMsg('');
$repos = $this->fetchRepositories($org);
$this->logMsg("Found " . count($repos) . " repositories");
$this->logMsg('');
$results = [
'repos_processed' => 0,
'repos_cleaned' => 0,
'branches_deleted' => 0,
'prs_closed' => 0,
'issues_closed' => 0,
'issues_locked' => 0,
'workflows_deleted' => 0,
'runs_deleted' => 0,
'logs_deleted' => 0,
'labels_missing' => 0,
'version_drift' => 0,
'retired_files' => 0,
'errors' => 0,
];
foreach ($repos as $i => $repo) {
$name = $repo['name'];
$num = $i + 1;
$total = count($repos);
$this->logMsg("[{$num}/{$total}] {$name}");
$results['repos_processed']++;
try {
$this->api->resetCircuitBreaker();
$cleaned = false;
// Always: delete old sync branches + close their PRs
$cleaned = $this->cleanBranches($org, $name, $results) || $cleaned;
// Optional: close resolved issues
if ($runAll || $this->getArgument('--close-issues', false)) {
$cleaned = $this->closeResolvedIssues($org, $name, $results) || $cleaned;
}
// Optional: lock old closed issues
if ($runAll || $this->getArgument('--lock-old-issues', false)) {
$cleaned = $this->lockOldIssues($org, $name, $results) || $cleaned;
}
// Optional: delete retired workflow files
if ($runAll || $this->getArgument('--delete-retired', false)) {
$cleaned = $this->deleteRetiredWorkflows($org, $name, $results) || $cleaned;
}
// Optional: clean workflow runs
if ($runAll || $this->getArgument('--clean-workflows', false)) {
$cleaned = $this->cleanWorkflowRuns($org, $name, $results) || $cleaned;
}
// Optional: clean old logs
if ($runAll || $this->getArgument('--clean-logs', false)) {
$cleaned = $this->cleanOldLogs($org, $name, $results) || $cleaned;
}
// Optional: check labels
if ($runAll || $this->getArgument('--check-labels', false)) {
$this->checkLabels($org, $name, $results);
}
// Optional: check version drift
if ($runAll || $this->getArgument('--check-drift', false)) {
$this->checkVersionDrift($org, $name, $results);
}
if ($cleaned) {
$results['repos_cleaned']++;
}
} catch (\Exception $e) {
$this->errorMsg("{$name}: " . $e->getMessage());
$results['errors']++;
}
}
$duration = round(microtime(true) - $this->startTime, 1);
$this->logMsg('');
$this->logMsg('============================================================');
$this->logMsg("🧹 Cleanup Complete ({$duration}s)");
$this->logMsg('============================================================');
$this->logMsg("Repos processed: {$results['repos_processed']}");
$this->logMsg("Repos with changes: {$results['repos_cleaned']}");
$this->logMsg("Branches deleted: {$results['branches_deleted']}");
$this->logMsg("PRs closed: {$results['prs_closed']}");
$this->logMsg("Issues closed: {$results['issues_closed']}");
$this->logMsg("Issues locked: {$results['issues_locked']}");
$this->logMsg("Retired files: {$results['retired_files']}");
$this->logMsg("Workflow runs: {$results['runs_deleted']}");
$this->logMsg("Logs cleaned: {$results['logs_deleted']}");
$this->logMsg("Labels missing: {$results['labels_missing']}");
$this->logMsg("Version drift: {$results['version_drift']}");
$this->logMsg("Errors: {$results['errors']}");
$this->logMsg('============================================================');
if ($this->getArgument('--json', false)) {
$results['duration_seconds'] = $duration;
echo json_encode($results, JSON_PRETTY_PRINT) . "\n";
}
return $results['errors'] > 0 ? 1 : 0;
}
// ─── Repository fetching ─────────────────────────────────────────────
private function fetchRepositories(string $org): array
{
$specificRepos = trim((string) $this->getArgument('--repos', ''));
$skipArchived = (bool) $this->getArgument('--skip-archived', false);
if (!empty($specificRepos)) {
$names = preg_split('/[\s,]+/', $specificRepos);
return array_map(fn($n) => ['name' => trim($n), 'archived' => false], $names);
}
$allRepos = $this->adapter->listOrgRepos($org, $skipArchived);
return array_filter($allRepos, fn($r) => !in_array($r['name'], ['moko-platform', '.github-private'], true));
}
// ─── Cleanup operations ──────────────────────────────────────────────
private function cleanBranches(string $org, string $repo, array &$results): bool
{
$changed = false;
try {
$branches = $this->api->get("/repos/{$org}/{$repo}/branches", ['per_page' => 100]);
} catch (\Exception $e) {
return false;
}
foreach ($branches as $branch) {
$name = $branch['name'] ?? '';
if (!str_starts_with($name, self::SYNC_PREFIX) || $name === self::CURRENT_BRANCH) {
continue;
}
// Close open PRs on this branch
try {
$prs = $this->api->get("/repos/{$org}/{$repo}/pulls", [
'state' => 'open', 'head' => "{$org}:{$name}", 'per_page' => 10,
]);
foreach ($prs as $pr) {
if (($pr['number'] ?? 0) > 0 && !$this->dryRun) {
$this->api->patch("/repos/{$org}/{$repo}/pulls/{$pr['number']}", ['state' => 'closed']);
}
$this->logMsg(" 🔒 Closed PR #{$pr['number']} ({$name})");
$results['prs_closed']++;
$changed = true;
}
} catch (\Exception $e) {
/* non-fatal */
}
if (!$this->dryRun) {
try {
$this->api->delete("/repos/{$org}/{$repo}/git/refs/heads/{$name}");
} catch (\Exception $e) {
continue;
}
}
$this->logMsg(" 🗑️ Deleted branch: {$name}");
$results['branches_deleted']++;
$changed = true;
}
return $changed;
}
private function closeResolvedIssues(string $org, string $repo, array &$results): bool
{
$changed = false;
foreach (['standards-update', 'standards-drift'] as $label) {
try {
$issues = $this->api->get("/repos/{$org}/{$repo}/issues", [
'labels' => $label, 'state' => 'open', 'per_page' => 10,
]);
} catch (\Exception $e) {
continue;
}
foreach ($issues as $issue) {
$num = $issue['number'] ?? 0;
$body = $issue['body'] ?? '';
if (preg_match('/\[#(\d+)\]/', $body, $m)) {
$prNum = (int) $m[1];
try {
$pr = $this->api->get("/repos/{$org}/{$repo}/pulls/{$prNum}");
if (!empty($pr['merged_at'])) {
if (!$this->dryRun) {
$this->api->patch("/repos/{$org}/{$repo}/issues/{$num}", [
'state' => 'closed', 'state_reason' => 'completed',
]);
}
$this->logMsg(" ✅ Closed issue #{$num} (PR #{$prNum} merged)");
$results['issues_closed']++;
$changed = true;
}
} catch (\Exception $e) {
/* non-fatal */
}
}
}
}
return $changed;
}
private function lockOldIssues(string $org, string $repo, array &$results): bool
{
$changed = false;
$cutoff = date('Y-m-d\TH:i:s\Z', strtotime('-30 days'));
try {
$issues = $this->api->get("/repos/{$org}/{$repo}/issues", [
'state' => 'closed', 'per_page' => 50, 'sort' => 'updated', 'direction' => 'asc',
]);
} catch (\Exception $e) {
return false;
}
foreach ($issues as $issue) {
$closedAt = $issue['closed_at'] ?? '';
$locked = $issue['locked'] ?? false;
$num = $issue['number'] ?? 0;
if ($locked || $closedAt > $cutoff || $num === 0) {
continue;
}
if (!$this->dryRun) {
try {
$this->api->put("/repos/{$org}/{$repo}/issues/{$num}/lock", [
'lock_reason' => 'resolved',
]);
} catch (\Exception $e) {
continue;
}
}
$results['issues_locked']++;
$changed = true;
}
if ($results['issues_locked'] > 0) {
$this->logMsg(" 🔒 Locked {$results['issues_locked']} old closed issue(s)");
}
return $changed;
}
private function deleteRetiredWorkflows(string $org, string $repo, array &$results): bool
{
$changed = false;
$defaultBranch = 'main';
try {
$repoInfo = $this->api->get("/repos/{$org}/{$repo}");
$defaultBranch = $repoInfo['default_branch'] ?? 'main';
} catch (\Exception $e) {
/* fallback to main */
}
// Check both workflow directories for retired workflows (supports dual-platform repos)
$wfDirs = array_unique(['.github/workflows', '.mokogitea/workflows', $this->adapter->getWorkflowDir()]);
foreach (self::RETIRED_WORKFLOWS as $wf) {
foreach ($wfDirs as $wfDir) {
$path = "{$wfDir}/{$wf}";
try {
$file = $this->api->get("/repos/{$org}/{$repo}/contents/{$path}");
$sha = $file['sha'] ?? '';
if (empty($sha)) {
continue;
}
if (!$this->dryRun) {
$this->api->delete("/repos/{$org}/{$repo}/contents/{$path}", [
'message' => "chore: delete retired workflow {$wf}",
'sha' => $sha,
'branch' => $defaultBranch,
]);
}
$this->logMsg(" Deleted retired: {$wf} (from {$wfDir})");
$results['retired_files']++;
$changed = true;
} catch (\Exception $e) {
// File doesn't exist in this dir — skip
$this->api->resetCircuitBreaker();
}
}
}
return $changed;
}
private function cleanWorkflowRuns(string $org, string $repo, array &$results): bool
{
$changed = false;
foreach (['cancelled', 'stale'] as $status) {
try {
$runs = $this->api->get("/repos/{$org}/{$repo}/actions/runs", [
'status' => $status, 'per_page' => 100,
]);
foreach (($runs['workflow_runs'] ?? []) as $run) {
$id = $run['id'] ?? 0;
if ($id > 0 && !$this->dryRun) {
try {
$this->api->delete("/repos/{$org}/{$repo}/actions/runs/{$id}");
$results['runs_deleted']++;
$changed = true;
} catch (\Exception $e) {
$this->api->resetCircuitBreaker();
}
}
}
} catch (\Exception $e) {
/* non-fatal */
}
}
if ($results['runs_deleted'] > 0) {
$this->logMsg(" 🔄 Cleaned {$results['runs_deleted']} workflow run(s)");
}
return $changed;
}
private function cleanOldLogs(string $org, string $repo, array &$results): bool
{
$changed = false;
$days = (int) $this->getArgument('--log-days', '30');
$cutoff = date('Y-m-d\TH:i:s\Z', strtotime("-{$days} days"));
try {
$runs = $this->api->get("/repos/{$org}/{$repo}/actions/runs", [
'created' => "<{$cutoff}", 'per_page' => 100,
]);
foreach (($runs['workflow_runs'] ?? []) as $run) {
$id = $run['id'] ?? 0;
if ($id > 0 && !$this->dryRun) {
try {
$this->api->delete("/repos/{$org}/{$repo}/actions/runs/{$id}/logs");
$results['logs_deleted']++;
$changed = true;
} catch (\Exception $e) {
$this->api->resetCircuitBreaker();
}
}
}
} catch (\Exception $e) {
/* non-fatal */
}
if ($results['logs_deleted'] > 0) {
$this->logMsg(" 📋 Cleaned {$results['logs_deleted']} old log(s)");
}
return $changed;
}
private function checkLabels(string $org, string $repo, array &$results): void
{
try {
$this->api->get("/repos/{$org}/{$repo}/labels/moko-platform");
} catch (\Exception $e) {
$this->logMsg(" ⚠️ Missing 'moko-platform' label");
$results['labels_missing']++;
$this->api->resetCircuitBreaker();
}
}
private function checkVersionDrift(string $org, string $repo, array &$results): void
{
try {
$file = $this->api->get("/repos/{$org}/{$repo}/contents/README.md");
$content = base64_decode($file['content'] ?? '');
if (preg_match('/^\s*VERSION:\s*(\d{2}\.\d{2}\.\d{2})/m', $content, $m)) {
$version = $m[1];
// Check manifest.xml for the tracked moko-platform version
try {
$mokoFile = $this->api->get("/repos/{$org}/{$repo}/contents/.mokogitea/manifest.xml");
$mokoContent = base64_decode($mokoFile['content'] ?? '');
if (preg_match('/standards_version:\s*(\d{2}\.\d{2}\.\d{2})/m', $mokoContent, $vm)) {
if ($vm[1] !== self::VERSION) {
$this->logMsg(" ⚠️ Standards drift: {$vm[1]} (expected " . self::VERSION . ")");
$results['version_drift']++;
}
}
} catch (\Exception $e) {
$this->api->resetCircuitBreaker();
}
}
} catch (\Exception $e) {
$this->api->resetCircuitBreaker();
}
}
// ─── Helpers ─────────────────────────────────────────────────────────
private function logMsg(string $message): void
{
if (!$this->quiet) {
echo $message . "\n";
}
}
private function errorMsg(string $message): void
{
fwrite(STDERR, $message . "\n");
}
}
$app = new RepoCleanup();
exit($app->execute());
-678
View File
@@ -1,678 +0,0 @@
#!/usr/bin/env bash
# server-autoheal.sh - Auto-heal on restart + split backup management
#
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# SPDX-License-Identifier: GPL-3.0-or-later
#
# DEFGROUP: MokoPlatform.Automation.ServerAutoheal
# INGROUP: MokoPlatform.Automation
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /automation/server-autoheal.sh
# BRIEF: Server auto-heal on unclean restart + split system/content backups
#
# Usage:
# server-autoheal.sh <command> [options]
#
# Commands:
# boot-check Run at boot — auto-heals if no safe point exists
# set-safepoint Mark current state as safe (call before planned shutdown)
# backup-system Run a system backup (configs, packages, services)
# backup-content Run a content backup (site files, databases, uploads)
# cleanup Prune expired backups per retention policy
# status Show safe point and backup status
#
# Scheduling (cron):
# @reboot server-autoheal.sh boot-check
# 0 3 * * * server-autoheal.sh backup-system (daily at 3am)
# 0 */2 * * * server-autoheal.sh backup-content (every 2 hours)
# 30 */2 * * * server-autoheal.sh cleanup (30 min after content backup)
set -euo pipefail
# ──────────────────────────────────────────────
# Configuration — override via /etc/moko/autoheal.conf
# ──────────────────────────────────────────────
CONF_FILE="/etc/moko/autoheal.conf"
[[ -f "$CONF_FILE" ]] && source "$CONF_FILE"
BACKUP_ROOT="${BACKUP_ROOT:-/var/backups/moko}"
SAFEPOINT_FILE="${SAFEPOINT_FILE:-/var/run/moko/safepoint}"
LOG_FILE="${LOG_FILE:-/var/log/moko/autoheal.log}"
LOCK_DIR="${LOCK_DIR:-/var/run/moko}"
# System backup: configs, package lists, service state, cron
SYSTEM_BACKUP_DIR="${BACKUP_ROOT}/system"
SYSTEM_BACKUP_RETAIN="${SYSTEM_BACKUP_RETAIN:-7}" # keep 7 daily system backups
# Content backup: web roots, databases, uploads
CONTENT_BACKUP_DIR="${BACKUP_ROOT}/content"
CONTENT_BACKUP_RETAIN_HOURS="${CONTENT_BACKUP_RETAIN_HOURS:-24}" # 1 day of content backups
# Paths to back up — override these in /etc/moko/autoheal.conf
SYSTEM_PATHS="${SYSTEM_PATHS:-/etc/nginx /etc/php /etc/mysql /etc/cron.d /etc/systemd/system}"
CONTENT_PATHS="${CONTENT_PATHS:-/var/www}"
DB_NAMES="${DB_NAMES:-}" # space-separated list, empty = auto-detect all
# ──────────────────────────────────────────────
# Helpers
# ──────────────────────────────────────────────
log() {
local level="$1"; shift
local ts
ts=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
local msg="[$ts] [$level] $*"
echo "$msg" | tee -a "$LOG_FILE" >&2
}
ensure_dirs() {
mkdir -p "$SYSTEM_BACKUP_DIR" "$CONTENT_BACKUP_DIR" \
"$LOCK_DIR" "$(dirname "$LOG_FILE")"
}
acquire_lock() {
local lockfile="${LOCK_DIR}/autoheal-${1}.lock"
if [[ -f "$lockfile" ]]; then
local pid
pid=$(<"$lockfile")
if kill -0 "$pid" 2>/dev/null; then
log WARN "Another $1 operation is running (PID $pid), skipping"
exit 0
fi
rm -f "$lockfile"
fi
echo $$ > "$lockfile"
trap "rm -f '$lockfile'" EXIT
}
timestamp() {
date -u '+%Y%m%d_%H%M%S'
}
# ──────────────────────────────────────────────
# Safe-point management
# ──────────────────────────────────────────────
cmd_set_safepoint() {
ensure_dirs
local ts
ts=$(timestamp)
cat > "$SAFEPOINT_FILE" <<EOF
timestamp=$ts
hostname=$(hostname)
kernel=$(uname -r)
uptime=$(uptime -s 2>/dev/null || echo "unknown")
set_by=${SUDO_USER:-$(whoami)}
EOF
log INFO "Safe point set at $ts by ${SUDO_USER:-$(whoami)}"
}
cmd_clear_safepoint() {
rm -f "$SAFEPOINT_FILE"
log INFO "Safe point cleared"
}
has_safepoint() {
[[ -f "$SAFEPOINT_FILE" ]]
}
# ──────────────────────────────────────────────
# System backup (daily)
# ──────────────────────────────────────────────
cmd_backup_system() {
ensure_dirs
acquire_lock "system-backup"
local ts
ts=$(timestamp)
local archive="${SYSTEM_BACKUP_DIR}/system_${ts}.tar.gz"
local manifest="${SYSTEM_BACKUP_DIR}/system_${ts}.manifest"
log INFO "Starting system backup → $archive"
# Collect existing paths only
local existing_paths=()
for p in $SYSTEM_PATHS; do
[[ -e "$p" ]] && existing_paths+=("$p")
done
if [[ ${#existing_paths[@]} -eq 0 ]]; then
log WARN "No system paths found to back up"
return 1
fi
# Archive configs and system files
tar -czf "$archive" "${existing_paths[@]}" 2>/dev/null || true
# Capture package list and service state as manifest
{
echo "=== PACKAGES ==="
if command -v dpkg &>/dev/null; then
dpkg --get-selections
elif command -v rpm &>/dev/null; then
rpm -qa --qf '%{NAME}\t%{VERSION}\n'
fi
echo ""
echo "=== ENABLED SERVICES ==="
if command -v systemctl &>/dev/null; then
systemctl list-unit-files --state=enabled --no-pager 2>/dev/null || true
fi
echo ""
echo "=== CRONTABS ==="
for user_home in /var/spool/cron/crontabs/*; do
[[ -f "$user_home" ]] && echo "--- $(basename "$user_home") ---" && cat "$user_home"
done 2>/dev/null || true
} > "$manifest"
local size
size=$(du -sh "$archive" 2>/dev/null | cut -f1)
log INFO "System backup complete: $archive ($size)"
# Prune old system backups (keep $SYSTEM_BACKUP_RETAIN)
local count
count=$(find "$SYSTEM_BACKUP_DIR" -name 'system_*.tar.gz' | wc -l)
if [[ "$count" -gt "$SYSTEM_BACKUP_RETAIN" ]]; then
local to_remove=$((count - SYSTEM_BACKUP_RETAIN))
find "$SYSTEM_BACKUP_DIR" -name 'system_*.tar.gz' -printf '%T+ %p\n' \
| sort | head -n "$to_remove" | awk '{print $2}' \
| while read -r f; do
rm -f "$f" "${f%.tar.gz}.manifest"
log INFO "Pruned old system backup: $f"
done
fi
}
# ──────────────────────────────────────────────
# Content backup (every 2 hours)
# ──────────────────────────────────────────────
cmd_backup_content() {
ensure_dirs
acquire_lock "content-backup"
local ts
ts=$(timestamp)
local archive="${CONTENT_BACKUP_DIR}/content_${ts}.tar.gz"
local db_dump="${CONTENT_BACKUP_DIR}/content_${ts}.sql.gz"
log INFO "Starting content backup → $archive"
# Back up web content / uploads
local existing_paths=()
for p in $CONTENT_PATHS; do
[[ -e "$p" ]] && existing_paths+=("$p")
done
if [[ ${#existing_paths[@]} -gt 0 ]]; then
tar -czf "$archive" "${existing_paths[@]}" 2>/dev/null || true
local size
size=$(du -sh "$archive" 2>/dev/null | cut -f1)
log INFO "Content files archived: $archive ($size)"
else
log WARN "No content paths found to back up"
fi
# Database dump
if command -v mysqldump &>/dev/null || command -v mariadb-dump &>/dev/null; then
local dump_cmd="mysqldump"
command -v mariadb-dump &>/dev/null && dump_cmd="mariadb-dump"
local databases=()
if [[ -n "$DB_NAMES" ]]; then
read -ra databases <<< "$DB_NAMES"
else
# Auto-detect: dump all databases except system ones
databases=($(${dump_cmd%dump} -N -e \
"SELECT schema_name FROM information_schema.schemata
WHERE schema_name NOT IN ('information_schema','performance_schema','mysql','sys')" \
2>/dev/null | tr '\n' ' ')) || true
fi
if [[ ${#databases[@]} -gt 0 ]]; then
$dump_cmd --single-transaction --routines --triggers \
--databases "${databases[@]}" 2>/dev/null \
| gzip > "$db_dump"
local db_size
db_size=$(du -sh "$db_dump" 2>/dev/null | cut -f1)
log INFO "Database dump complete: $db_dump ($db_size)"
else
log WARN "No databases found to dump"
fi
fi
}
# ──────────────────────────────────────────────
# Cleanup — prune content backups older than retention
# ──────────────────────────────────────────────
cmd_cleanup() {
ensure_dirs
local before_count after_count
# Content: keep only last 24 hours (1 day)
before_count=$(find "$CONTENT_BACKUP_DIR" -name 'content_*' -type f | wc -l)
find "$CONTENT_BACKUP_DIR" -name 'content_*' -type f \
-mmin +$((CONTENT_BACKUP_RETAIN_HOURS * 60)) -delete 2>/dev/null || true
after_count=$(find "$CONTENT_BACKUP_DIR" -name 'content_*' -type f | wc -l)
local removed=$((before_count - after_count))
[[ "$removed" -gt 0 ]] && log INFO "Pruned $removed content backup(s) older than ${CONTENT_BACKUP_RETAIN_HOURS}h"
# System: keep N most recent (handled in backup-system, but double-check here)
before_count=$(find "$SYSTEM_BACKUP_DIR" -name 'system_*' -type f | wc -l)
local max_system_files=$((SYSTEM_BACKUP_RETAIN * 2)) # .tar.gz + .manifest
if [[ "$before_count" -gt "$max_system_files" ]]; then
local excess=$((before_count - max_system_files))
find "$SYSTEM_BACKUP_DIR" -name 'system_*' -type f -printf '%T+ %p\n' \
| sort | head -n "$excess" | awk '{print $2}' \
| xargs -r rm -f
log INFO "Pruned excess system backups"
fi
log INFO "Cleanup complete"
}
# ──────────────────────────────────────────────
# Boot check — the auto-heal entry point
# ──────────────────────────────────────────────
cmd_boot_check() {
ensure_dirs
acquire_lock "boot-check"
log INFO "=== Boot check started ==="
log INFO "Hostname: $(hostname), Kernel: $(uname -r)"
if has_safepoint; then
log INFO "Safe point found — server was shut down cleanly"
log INFO "Clearing safe point for next cycle"
cmd_clear_safepoint
log INFO "=== Boot check passed (clean restart) ==="
return 0
fi
log WARN "NO safe point found — server restarted without clean shutdown"
log WARN "Initiating auto-heal sequence..."
auto_heal
local rc=$?
# Set safe point after successful heal
if [[ $rc -eq 0 ]]; then
cmd_set_safepoint
log INFO "=== Boot check complete (healed successfully) ==="
else
log ERROR "=== Boot check FAILED — manual intervention required ==="
fi
return $rc
}
# ──────────────────────────────────────────────
# Auto-heal strategy
#
# TODO: This is the core decision point. Implement the recovery
# steps that match your server's architecture. See guidance below.
#
# Trade-offs to consider:
# - Restore-from-backup: safest, but content may be up to 2h stale
# - Service-restart-only: faster, keeps current data, but won't fix
# corrupted configs or broken filesystem state
# - Hybrid: restart services first, verify health, only restore if
# health checks fail — best of both worlds but more complex
#
# The function receives no arguments. Use the latest system + content
# backups to restore if needed. Return 0 on success, 1 on failure.
# ──────────────────────────────────────────────
auto_heal() {
log INFO "Phase 1: Verify and repair filesystem"
# Check for common post-crash issues
repair_filesystem
log INFO "Phase 2: Restore system configuration if corrupted"
restore_system_if_needed
log INFO "Phase 3: Restart core services"
restart_services
log INFO "Phase 4: Verify health"
if ! verify_health; then
log WARN "Health check failed after service restart — restoring from backup"
restore_from_backup
restart_services
if ! verify_health; then
log ERROR "Health check still failing after restore — giving up"
return 1
fi
fi
log INFO "Auto-heal completed successfully"
return 0
}
# ──────────────────────────────────────────────
# Heal sub-steps
# ──────────────────────────────────────────────
repair_filesystem() {
# Fix common post-crash filesystem issues
# Clear stale PID/lock/socket files that prevent services from starting
local stale_files=(
/var/run/nginx.pid
/var/run/mysqld/mysqld.pid
/var/run/php-fpm.pid
/var/lib/mysql/*.pid
)
for f in "${stale_files[@]}"; do
for expanded in $f; do
if [[ -f "$expanded" ]]; then
local pid
pid=$(<"$expanded") 2>/dev/null || true
if [[ -n "$pid" ]] && ! kill -0 "$pid" 2>/dev/null; then
rm -f "$expanded"
log INFO "Removed stale PID file: $expanded"
fi
fi
done
done
# Fix permissions on critical dirs that may get mangled
[[ -d /var/run/mysqld ]] && chown mysql:mysql /var/run/mysqld 2>/dev/null || true
[[ -d /var/lib/php/sessions ]] && chmod 1733 /var/lib/php/sessions 2>/dev/null || true
# Repair tmp/cache dirs
for d in /tmp /var/tmp; do
[[ -d "$d" ]] && chmod 1777 "$d" 2>/dev/null || true
done
}
restore_system_if_needed() {
# Find latest system backup
local latest_system
latest_system=$(find "$SYSTEM_BACKUP_DIR" -name 'system_*.tar.gz' -printf '%T+ %p\n' \
2>/dev/null | sort -r | head -1 | awk '{print $2}')
if [[ -z "$latest_system" ]]; then
log WARN "No system backup available to verify against"
return 0
fi
# Check if critical configs exist and are non-empty
local needs_restore=false
local critical_configs=("/etc/nginx/nginx.conf" "/etc/php" "/etc/mysql")
for cfg in "${critical_configs[@]}"; do
if [[ -e "$cfg" ]]; then
# Config exists — check if it's a file and non-empty, or a directory
if [[ -f "$cfg" && ! -s "$cfg" ]]; then
log WARN "Critical config is empty: $cfg"
needs_restore=true
break
fi
fi
done
if $needs_restore; then
log WARN "Restoring system config from $latest_system"
tar -xzf "$latest_system" -C / 2>/dev/null || {
log ERROR "System restore failed from $latest_system"
return 1
}
log INFO "System config restored"
else
log INFO "System configs look intact — skipping restore"
fi
}
restart_services() {
if ! command -v systemctl &>/dev/null; then
log WARN "systemctl not available — skipping service restart"
return 0
fi
local services=("mysql" "mariadb" "nginx" "apache2" "php-fpm" "php8.1-fpm" "php8.2-fpm" "php8.3-fpm")
for svc in "${services[@]}"; do
if systemctl is-enabled "$svc" &>/dev/null; then
log INFO "Restarting $svc..."
systemctl restart "$svc" 2>/dev/null && \
log INFO "$svc restarted OK" || \
log WARN "$svc restart failed"
fi
done
}
verify_health() {
local failures=0
# Check critical services are running
local services=("mysql" "mariadb" "nginx" "apache2")
for svc in "${services[@]}"; do
if systemctl is-enabled "$svc" &>/dev/null; then
if ! systemctl is-active "$svc" &>/dev/null; then
log WARN "Service not running: $svc"
((failures++))
fi
fi
done
# Check if web server responds
if command -v curl &>/dev/null; then
if ! curl -sf -o /dev/null --max-time 10 "http://localhost/" 2>/dev/null; then
log WARN "Local web server not responding"
((failures++))
fi
fi
# Check if database accepts connections
if command -v mysqladmin &>/dev/null; then
if ! mysqladmin ping --silent 2>/dev/null; then
log WARN "Database not responding to ping"
((failures++))
fi
fi
[[ $failures -eq 0 ]]
}
restore_from_backup() {
log WARN "=== Full restore from backup ==="
# Restore system config
local latest_system
latest_system=$(find "$SYSTEM_BACKUP_DIR" -name 'system_*.tar.gz' -printf '%T+ %p\n' \
2>/dev/null | sort -r | head -1 | awk '{print $2}')
if [[ -n "$latest_system" ]]; then
log INFO "Restoring system from $latest_system"
tar -xzf "$latest_system" -C / 2>/dev/null || \
log ERROR "System restore failed"
fi
# Restore content
local latest_content
latest_content=$(find "$CONTENT_BACKUP_DIR" -name 'content_*.tar.gz' -printf '%T+ %p\n' \
2>/dev/null | sort -r | head -1 | awk '{print $2}')
if [[ -n "$latest_content" ]]; then
log INFO "Restoring content from $latest_content"
tar -xzf "$latest_content" -C / 2>/dev/null || \
log ERROR "Content restore failed"
fi
# Restore database
local latest_db
latest_db=$(find "$CONTENT_BACKUP_DIR" -name 'content_*.sql.gz' -printf '%T+ %p\n' \
2>/dev/null | sort -r | head -1 | awk '{print $2}')
if [[ -n "$latest_db" ]]; then
log INFO "Restoring database from $latest_db"
local mysql_cmd="mysql"
command -v mariadb &>/dev/null && mysql_cmd="mariadb"
zcat "$latest_db" | $mysql_cmd 2>/dev/null || \
log ERROR "Database restore failed"
fi
}
# ──────────────────────────────────────────────
# Status
# ──────────────────────────────────────────────
cmd_status() {
echo "=== Moko Server Auto-Heal Status ==="
echo ""
# Safe point
if has_safepoint; then
echo "Safe point: SET"
cat "$SAFEPOINT_FILE" | sed 's/^/ /'
else
echo "Safe point: NOT SET (will auto-heal on next boot)"
fi
echo ""
# System backups
echo "System backups (${SYSTEM_BACKUP_DIR}):"
local sys_count
sys_count=$(find "$SYSTEM_BACKUP_DIR" -name 'system_*.tar.gz' 2>/dev/null | wc -l)
echo " Count: $sys_count (retain $SYSTEM_BACKUP_RETAIN)"
local latest_sys
latest_sys=$(find "$SYSTEM_BACKUP_DIR" -name 'system_*.tar.gz' -printf '%T+ %p\n' \
2>/dev/null | sort -r | head -1)
if [[ -n "$latest_sys" ]]; then
echo " Latest: $(echo "$latest_sys" | awk '{print $2}')"
echo " Timestamp: $(echo "$latest_sys" | awk '{print $1}')"
else
echo " Latest: (none)"
fi
echo ""
# Content backups
echo "Content backups (${CONTENT_BACKUP_DIR}):"
local cnt_count
cnt_count=$(find "$CONTENT_BACKUP_DIR" -name 'content_*.tar.gz' 2>/dev/null | wc -l)
echo " Count: $cnt_count (retain ${CONTENT_BACKUP_RETAIN_HOURS}h)"
local latest_cnt
latest_cnt=$(find "$CONTENT_BACKUP_DIR" -name 'content_*.tar.gz' -printf '%T+ %p\n' \
2>/dev/null | sort -r | head -1)
if [[ -n "$latest_cnt" ]]; then
echo " Latest: $(echo "$latest_cnt" | awk '{print $2}')"
echo " Timestamp: $(echo "$latest_cnt" | awk '{print $1}')"
else
echo " Latest: (none)"
fi
echo ""
# Disk usage
echo "Backup disk usage:"
du -sh "$SYSTEM_BACKUP_DIR" "$CONTENT_BACKUP_DIR" 2>/dev/null | sed 's/^/ /'
}
# ──────────────────────────────────────────────
# Install helper — sets up cron + systemd
# ──────────────────────────────────────────────
cmd_install() {
local script_path
script_path=$(readlink -f "$0")
echo "Installing Moko Auto-Heal..."
# Create config directory
mkdir -p /etc/moko "$(dirname "$LOG_FILE")" "$LOCK_DIR"
# Write example config if none exists
if [[ ! -f "$CONF_FILE" ]]; then
cat > "$CONF_FILE" <<'CONF'
# /etc/moko/autoheal.conf — Server auto-heal configuration
# Uncomment and modify as needed
# BACKUP_ROOT="/var/backups/moko"
# SAFEPOINT_FILE="/var/run/moko/safepoint"
# LOG_FILE="/var/log/moko/autoheal.log"
# System backup paths (space-separated)
# SYSTEM_PATHS="/etc/nginx /etc/php /etc/mysql /etc/cron.d /etc/systemd/system"
# Content backup paths (space-separated)
# CONTENT_PATHS="/var/www"
# Database names (space-separated, empty = auto-detect all)
# DB_NAMES=""
# Retention
# SYSTEM_BACKUP_RETAIN=7 # daily backups to keep
# CONTENT_BACKUP_RETAIN_HOURS=24 # hours of content backups to keep
CONF
echo " Created config: $CONF_FILE"
fi
# Install cron jobs
local cron_file="/etc/cron.d/moko-autoheal"
cat > "$cron_file" <<CRON
# Moko Server Auto-Heal — managed by server-autoheal.sh install
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# Boot check — auto-heal if no safe point
@reboot root ${script_path} boot-check
# System backup — daily at 3:00 AM
0 3 * * * root ${script_path} backup-system
# Content backup — every 2 hours
0 */2 * * * root ${script_path} backup-content
# Cleanup expired backups — 30 min after each content backup
30 */2 * * * root ${script_path} cleanup
CRON
echo " Installed cron: $cron_file"
# Install shutdown hook to set safe point on clean shutdown
local shutdown_hook="/etc/systemd/system/moko-safepoint.service"
cat > "$shutdown_hook" <<UNIT
[Unit]
Description=Moko Safe Point — mark clean shutdown
DefaultDependencies=no
Before=shutdown.target reboot.target halt.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/bin/true
ExecStop=${script_path} set-safepoint
[Install]
WantedBy=multi-user.target
UNIT
systemctl daemon-reload
systemctl enable moko-safepoint.service
echo " Installed systemd hook: $shutdown_hook"
echo ""
echo "Done! Edit $CONF_FILE to configure paths for your server."
echo "Run '${script_path} status' to verify."
}
# ──────────────────────────────────────────────
# Main dispatcher
# ──────────────────────────────────────────────
main() {
local cmd="${1:-help}"
case "$cmd" in
boot-check) cmd_boot_check ;;
set-safepoint) cmd_set_safepoint ;;
clear-safepoint) cmd_clear_safepoint ;;
backup-system) cmd_backup_system ;;
backup-content) cmd_backup_content ;;
cleanup) cmd_cleanup ;;
status) cmd_status ;;
install) cmd_install ;;
help|--help|-h)
sed -n '2,/^$/s/^# //p' "$0"
echo ""
echo "Commands: boot-check, set-safepoint, clear-safepoint,"
echo " backup-system, backup-content, cleanup, status, install"
;;
*)
echo "Unknown command: $cmd" >&2
echo "Run '$0 help' for usage" >&2
exit 1
;;
esac
}
main "$@"
+13 -7
View File
@@ -9,11 +9,11 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: MokoStandards.CLI
* INGROUP: MokoStandards
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* DEFGROUP: MokoCli.CLI
* INGROUP: MokoCli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* 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
* php bin/moko <command> [options] (all platforms)
@@ -89,6 +89,7 @@ const COMMAND_MAP = [
// Automation
'sync' => 'automation/bulk_sync.php',
'deps:update' => 'automation/update_dependencies.php',
'automation:cleanup' => 'automation/repo_cleanup.php',
'automation:migrate-gitea' => 'automation/migrate_to_gitea.php',
@@ -177,6 +178,7 @@ const COMMAND_MAP = [
'repo:archive' => 'cli/archive_repo.php',
'repo:scaffold-client' => 'cli/scaffold_client.php',
'repo:provision' => 'cli/client_provision.php',
'repo:wizard' => 'cli/repo_wizard.php',
'repo:rename-branch' => 'cli/branch_rename.php',
'repo:reset-dev' => 'cli/dev_branch_reset.php',
@@ -198,6 +200,7 @@ const COMMAND_MAP = [
'deploy:sftp' => 'deploy/deploy-sftp.php',
'deploy:backup' => 'deploy/backup-before-deploy.php',
'deploy:health-check' => 'deploy/health-check.php',
'deploy:verify' => 'deploy/deploy-and-verify.php',
'deploy:rollback' => 'deploy/rollback-joomla.php',
'deploy:sync' => 'deploy/sync-joomla.php',
@@ -220,6 +223,9 @@ const COMMAND_MAP = [
// Licensing
'license' => 'cli/license_manage.php',
// Security
'security:advisories' => 'security/advisory_scan.php',
// Shell completion
'completion' => 'cli/completion.php',
@@ -292,10 +298,10 @@ function printHelp(): void
{
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
php bin/moko <command> [options] (all platforms)
@@ -397,7 +403,7 @@ function loadPluginCommands(): array
$commands = [];
foreach (glob("{$pluginDir}/*Plugin.php") as $file) {
$className = 'MokoEnterprise\\Plugins\\'
$className = 'MokoCli\\Plugins\\'
. pathinfo($file, PATHINFO_FILENAME);
if (!class_exists($className)) {
+8 -8
View File
@@ -8,9 +8,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* DEFGROUP: mokocli.CLI
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/archive_repo.php
* 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__ . '/../lib/Enterprise/CliFramework.php';
use MokoEnterprise\CliFramework;
use MokoEnterprise\Config;
use MokoEnterprise\PlatformAdapterFactory;
use MokoCli\CliFramework;
use MokoCli\Config;
use MokoCli\PlatformAdapterFactory;
class ArchiveRepoCli extends CliFramework
{
@@ -135,7 +135,7 @@ class ArchiveRepoCli extends CliFramework
try {
$issue = $adapter->createIssue(
$org,
'moko-platform',
'mokocli',
"chore: archived repository {$repoName}",
"## Repository Archived\n\n"
. "**Repository:** `{$org}/{$repoName}`\n"
@@ -150,7 +150,7 @@ class ArchiveRepoCli extends CliFramework
]
);
if (isset($issue['number'])) {
echo " Archival record: moko-platform#{$issue['number']}\n";
echo " Archival record: mokocli#{$issue['number']}\n";
}
} catch (\Exception $e) {
echo " Warning: could not create archival record: " . $e->getMessage() . "\n";
+2 -2
View File
@@ -16,7 +16,7 @@
* FILE INFORMATION
* DEFGROUP: MokoPlatform.Enterprise.CLI
* INGROUP: MokoPlatform.Enterprise
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/audit_query.php
* BRIEF: Search, filter, and export audit logs
*/
@@ -25,7 +25,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoEnterprise\CliFramework;
use MokoCli\CliFramework;
/**
* CLI tool to search, filter, and export audit logs.
+4 -4
View File
@@ -6,9 +6,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* DEFGROUP: mokocli.CLI
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/badge_update.php
* 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';
use MokoEnterprise\CliFramework;
use MokoCli\CliFramework;
class BadgeUpdateCli extends CliFramework
{
+5 -5
View File
@@ -6,11 +6,11 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* DEFGROUP: mokocli.CLI
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/branch_rename.php
* VERSION: 09.23.00
* VERSION: 09.38.00
* 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';
use MokoEnterprise\CliFramework;
use MokoCli\CliFramework;
class BranchRenameCli extends CliFramework
{
+7 -7
View File
@@ -8,11 +8,11 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* DEFGROUP: mokocli.CLI
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/bulk_workflow_push.php
* VERSION: 09.23.00
* VERSION: 09.38.00
* 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';
use MokoEnterprise\CliFramework;
use MokoCli\CliFramework;
class BulkWorkflowPushCli extends CliFramework
{
@@ -154,7 +154,7 @@ class BulkWorkflowPushCli extends CliFramework
'content' => $encodedContent,
'sha' => $remoteSha,
'message' => "chore: sync {$destPath} "
. "from moko-platform [skip ci]",
. "from mokocli [skip ci]",
'branch' => $branch,
]);
@@ -184,7 +184,7 @@ class BulkWorkflowPushCli extends CliFramework
$payload = json_encode([
'content' => $encodedContent,
'message' => "chore: add {$destPath} "
. "from moko-platform [skip ci]",
. "from mokocli [skip ci]",
'branch' => $branch,
]);
+5 -5
View File
@@ -8,11 +8,11 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* DEFGROUP: mokocli.CLI
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/bulk_workflow_trigger.php
* VERSION: 09.23.00
* VERSION: 09.38.00
* BRIEF: Trigger a workflow across multiple repos at once
*/
@@ -20,7 +20,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoEnterprise\CliFramework;
use MokoCli\CliFramework;
class BulkWorkflowTriggerCli extends CliFramework
{
+4 -4
View File
@@ -6,9 +6,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* DEFGROUP: mokocli.CLI
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/changelog_promote.php
* 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';
use MokoEnterprise\CliFramework;
use MokoCli\CliFramework;
class ChangelogPromoteCli extends CliFramework
{
+4 -4
View File
@@ -6,9 +6,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* DEFGROUP: mokocli.CLI
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/changelog_prune.php
* 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';
use MokoEnterprise\CliFramework;
use MokoCli\CliFramework;
class ChangelogPruneCli extends CliFramework
{
+5 -5
View File
@@ -8,11 +8,11 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* DEFGROUP: mokocli.CLI
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/client_dashboard.php
* VERSION: 09.23.00
* VERSION: 09.38.00
* BRIEF: Generate unified client dashboard HTML
*/
@@ -20,7 +20,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoEnterprise\CliFramework;
use MokoCli\CliFramework;
class ClientDashboardCli extends CliFramework
{
+4 -4
View File
@@ -6,9 +6,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* DEFGROUP: mokocli.CLI
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/client_health_check.php
* 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';
use MokoEnterprise\CliFramework;
use MokoCli\CliFramework;
class ClientHealthCheckCli extends CliFramework
{
+5 -5
View File
@@ -8,11 +8,11 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* DEFGROUP: mokocli.CLI
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/client_inventory.php
* VERSION: 09.23.00
* VERSION: 09.38.00
* 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';
use MokoEnterprise\CliFramework;
use MokoCli\CliFramework;
class ClientInventoryCli extends CliFramework
{
+5 -5
View File
@@ -8,11 +8,11 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* DEFGROUP: mokocli.CLI
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/client_provision.php
* VERSION: 09.23.00
* VERSION: 09.38.00
* BRIEF: Provision a new client environment end-to-end
*/
@@ -20,7 +20,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoEnterprise\CliFramework;
use MokoCli\CliFramework;
class ClientProvisionCli extends CliFramework
{
+4 -4
View File
@@ -6,9 +6,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* DEFGROUP: mokocli.CLI
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/completion.php
* 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';
use MokoEnterprise\CliFramework;
use MokoCli\CliFramework;
class CompletionCli extends CliFramework
{
+12 -12
View File
@@ -8,9 +8,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* DEFGROUP: mokocli.CLI
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/create_project.php
* 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';
use MokoEnterprise\CliFramework;
use MokoCli\CliFramework;
class CreateProjectCli extends CliFramework
{
/** @var string[] */
private array $ALWAYS_EXCLUDE = ['moko-platform', '.github-private'];
private array $ALWAYS_EXCLUDE = ['mokocli', '.github-private'];
/** @var array<string, string> */
private array $PLATFORM_TO_TYPE = [
@@ -80,10 +80,10 @@ class CreateProjectCli extends CliFramework
return 2;
}
$config = \MokoEnterprise\Config::load();
$config = \MokoCli\Config::load();
$platformName = $config->getString('platform', 'gitea');
try {
$adapter = \MokoEnterprise\PlatformAdapterFactory::create($config);
$adapter = \MokoCli\PlatformAdapterFactory::create($config);
$api = $adapter->getApiClient();
} catch (\Exception $e) {
$this->log('ERROR', "Platform initialization failed: " . $e->getMessage());
@@ -183,7 +183,7 @@ class CreateProjectCli extends CliFramework
CURLOPT_HTTPHEADER => [
'Authorization: bearer ' . $token,
'Content-Type: application/json',
'User-Agent: moko-platform-CreateProject',
'User-Agent: mokocli-CreateProject',
],
]);
$body = (string) curl_exec($ch);
@@ -205,7 +205,7 @@ class CreateProjectCli extends CliFramework
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) {
try {
@@ -217,7 +217,7 @@ class CreateProjectCli extends CliFramework
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) {
$data = $this->restGet("repos/{$org}/{$repo}/contents/{$path}", $token, $apiClient);
@@ -422,14 +422,14 @@ class CreateProjectCli extends CliFramework
updateProjectV2(input: {
projectId: $projectId,
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 }
}
}',
[
'projectId' => $projectId,
'shortDescription' => "Standard project board for {$repo}. Auto-created by moko-platform.",
'shortDescription' => "Standard project board for {$repo}. Auto-created by mokocli.",
],
$token
);
+21 -21
View File
@@ -8,11 +8,11 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* DEFGROUP: mokocli.CLI
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* 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);
@@ -20,15 +20,15 @@ declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoEnterprise\CliFramework;
use MokoEnterprise\Config;
use MokoEnterprise\PlatformAdapterFactory;
use MokoCli\CliFramework;
use MokoCli\Config;
use MokoCli\PlatformAdapterFactory;
class CreateRepoCli extends CliFramework
{
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('--type', 'Project type', null);
$this->addArgument('--description', 'Repository description', '');
@@ -60,16 +60,16 @@ class CreateRepoCli extends CliFramework
'generic' => 'generic',
];
$TYPE_TO_TOPICS = [
'dolibarr' => ['dolibarr', 'erp', 'crm', 'php', 'moko-platform'],
'joomla' => ['joomla', 'cms', 'php', 'moko-platform'],
'nodejs' => ['nodejs', 'javascript', 'typescript', 'moko-platform'],
'terraform' => ['terraform', 'infrastructure', 'iac', 'moko-platform'],
'python' => ['python', 'moko-platform'],
'wordpress' => ['wordpress', 'php', 'cms', 'moko-platform'],
'generic' => ['moko-platform'],
'dolibarr' => ['dolibarr', 'erp', 'crm', 'php', 'mokocli'],
'joomla' => ['joomla', 'cms', 'php', 'mokocli'],
'nodejs' => ['nodejs', 'javascript', 'typescript', 'mokocli'],
'terraform' => ['terraform', 'infrastructure', 'iac', 'mokocli'],
'python' => ['python', 'mokocli'],
'wordpress' => ['wordpress', 'php', 'cms', 'mokocli'],
'generic' => ['mokocli'],
];
$platform = $TYPE_TO_PLATFORM[$type] ?? 'generic';
$topics = $TYPE_TO_TOPICS[$type] ?? ['moko-platform'];
$topics = $TYPE_TO_TOPICS[$type] ?? ['mokocli'];
$platformName = $adapter->getPlatformName();
$vis = $private ? 'private' : 'public';
echo "Scaffolding new repository: {$org}/{$name}"
@@ -84,7 +84,7 @@ class CreateRepoCli extends CliFramework
if (!$this->dryRun) {
try {
$data = $adapter->createOrgRepo($org, $name, [
'description' => $description ?: "Managed by moko-platform ({$type})",
'description' => $description ?: "Managed by mokocli ({$type})",
'private' => $private,
'has_issues' => true,
'has_projects' => true,
@@ -138,12 +138,12 @@ class CreateRepoCli extends CliFramework
echo "Step 4: Creating README.md...\n";
$baseUrl = $platformName === 'gitea' ? $config->getString('gitea.url', 'https://git.mokoconsulting.tech') : 'https://github.com';
$repoUrl = "{$baseUrl}/{$org}/{$name}";
$standardsUrl = "{$baseUrl}/{$org}/MokoStandards";
$standardsUrl = "{$baseUrl}/{$org}/MokoCli";
$readmeContent = "<!--\n"
. "Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>\n"
. "SPDX-License-Identifier: GPL-3.0-or-later\n"
. "DEFGROUP: {$name}\n"
. "INGROUP: moko-platform\n"
. "INGROUP: mokocli\n"
. "REPO: {$repoUrl}\n"
. "PATH: /README.md\n"
. "BRIEF: {$description}\n"
@@ -152,7 +152,7 @@ class CreateRepoCli extends CliFramework
. "{$description}\n\n"
. "## Getting Started\n\n"
. "This repository is governed by"
. " [moko-platform]({$standardsUrl}).\n\n"
. " [mokocli]({$standardsUrl}).\n\n"
. "## License\n\n"
. "GPL-3.0-or-later. See [LICENSE](LICENSE)"
. " for details.\n";
@@ -169,7 +169,7 @@ class CreateRepoCli extends CliFramework
$name,
'README.md',
$readmeContent,
'docs: initialize README with moko-platform header [skip ci]',
'docs: initialize README with mokocli header [skip ci]',
$sha
);
echo " README.md created\n";
+7 -7
View File
@@ -10,7 +10,7 @@
* FILE INFORMATION
* DEFGROUP: MokoPlatform.CLI
* INGROUP: MokoPlatform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/deploy_joomla.php
* 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';
use MokoEnterprise\CliFramework;
use MokoCli\{CliFramework, SourceResolver};
use phpseclib3\Net\SFTP;
use phpseclib3\Crypt\PublicKeyLoader;
@@ -866,11 +866,11 @@ class DeployJoomla extends CliFramework
}
}
// 3-5. Fallback chain
foreach (['src', 'htdocs'] as $candidate) {
if (is_dir("{$repoPath}/{$candidate}")) {
return "{$repoPath}/{$candidate}";
}
// 3-5. Fallback chain (source/ → src/ → htdocs/)
$resolved = SourceResolver::resolveAbsolute($repoPath);
if ($resolved !== null) {
SourceResolver::warnIfLegacy($repoPath);
return $resolved;
}
// Last resort: repo root itself
+4 -4
View File
@@ -6,9 +6,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* DEFGROUP: mokocli.CLI
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/dev_branch_reset.php
* 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';
use MokoEnterprise\CliFramework;
use MokoCli\CliFramework;
class DevBranchResetCli extends CliFramework
{
+5 -5
View File
@@ -8,11 +8,11 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* DEFGROUP: mokocli.CLI
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/grafana_dashboard.php
* VERSION: 09.23.00
* VERSION: 09.38.00
* BRIEF: Manage Grafana dashboards via API
*/
@@ -20,7 +20,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoEnterprise\CliFramework;
use MokoCli\CliFramework;
class GrafanaDashboardCli extends CliFramework
{
+8 -13
View File
@@ -6,11 +6,11 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* DEFGROUP: mokocli.CLI
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/joomla_build.php
* VERSION: 09.23.00
* VERSION: 09.38.00
* BRIEF: Build a Joomla extension ZIP from manifest all types supported
* NOTE: Called by pre-release and auto-release workflows.
*/
@@ -19,7 +19,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoEnterprise\CliFramework;
use MokoCli\{CliFramework, SourceResolver};
class JoomlaBuildCli extends CliFramework
{
@@ -49,17 +49,12 @@ class JoomlaBuildCli extends CliFramework
$path = realpath($path) ?: $path;
// ── Find source directory ──────────────────────────────────────────────
$srcDir = null;
foreach (['src', 'htdocs'] as $d) {
if (is_dir("{$path}/{$d}")) {
$srcDir = "{$path}/{$d}";
break;
}
}
$srcDir = SourceResolver::resolveAbsolute($path);
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;
}
SourceResolver::warnIfLegacy($path);
// ── Find manifest ──────────────────────────────────────────────────────
$manifest = $this->findManifest($srcDir);
+4 -4
View File
@@ -6,9 +6,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* DEFGROUP: mokocli.CLI
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/joomla_compat_check.php
* 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';
use MokoEnterprise\CliFramework;
use MokoCli\CliFramework;
class JoomlaCompatCheckCli extends CliFramework
{
+507
View File
@@ -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.00
* 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());
+9 -8
View File
@@ -8,9 +8,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* DEFGROUP: mokocli.CLI
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/joomla_release.php
* 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';
use MokoEnterprise\{ApiClient, AuditLogger, CliFramework, Config, PlatformAdapterFactory};
use MokoCli\{ApiClient, AuditLogger, CliFramework, Config, PlatformAdapterFactory, SourceResolver};
/**
* Joomla Release Manager
@@ -57,7 +57,7 @@ class JoomlaRelease extends CliFramework
];
private ApiClient $api;
private \MokoEnterprise\GitPlatformAdapter $adapter;
private \MokoCli\GitPlatformAdapter $adapter;
protected function configure(): void
{
@@ -121,11 +121,12 @@ class JoomlaRelease extends CliFramework
$this->log('INFO', "Version: {$displayVersion} | Release tag: {$releaseTag}");
// ── 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) {
$this->log('ERROR', 'No src/ or htdocs/ directory');
$this->log('ERROR', 'No source/ or src/ directory');
return 1;
}
SourceResolver::warnIfLegacy($path);
$prefix = $this->typePrefix($meta);
$zipName = "{$prefix}{$meta['element']}-{$displayVersion}.zip";
@@ -406,7 +407,7 @@ class JoomlaRelease extends CliFramework
$this->api->post("/repos/{$repo}/releases", [
'tag_name' => $tag,
'name' => $releaseName,
'body' => "## {$version}\n\nCreated by moko-platform release pipeline.",
'body' => "## {$version}\n\nCreated by mokocli release pipeline.",
'prerelease' => ($stability !== 'stable'),
]);
}
+4 -4
View File
@@ -6,9 +6,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* DEFGROUP: mokocli.CLI
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/license_manage.php
* 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__ . '/../lib/Enterprise/CliFramework.php';
use MokoEnterprise\CliFramework;
use MokoCli\CliFramework;
class LicenseManage extends CliFramework
{
+749
View File
@@ -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.00
* 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 -7
View File
@@ -6,9 +6,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* DEFGROUP: mokocli.CLI
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/manifest_element.php
* 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';
use MokoEnterprise\CliFramework;
use MokoCli\{CliFramework, SourceResolver};
class ManifestElementCli extends CliFramework
{
@@ -48,7 +48,7 @@ class ManifestElementCli extends CliFramework
}
}
$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) {
$c = file_get_contents($file);
if (strpos($c, '<extension') !== false) {
@@ -58,8 +58,7 @@ class ManifestElementCli extends CliFramework
}
$modFile = null;
$modFiles = array_merge(
glob("{$root}/src/core/modules/mod*.class.php") ?: [],
glob("{$root}/htdocs/core/modules/mod*.class.php") ?: [],
SourceResolver::globSource($root, 'core/modules/mod*.class.php'),
glob("{$root}/core/modules/mod*.class.php") ?: []
);
foreach ($modFiles as $file) {
+564
View 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.00
* 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());
+280
View File
@@ -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.00
* 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());
+398 -94
View File
@@ -6,126 +6,431 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* DEFGROUP: mokocli.CLI
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/manifest_read.php
* VERSION: 09.23.00
* BRIEF: Parse .manifest.xml and output requested field(s) for CI consumption
* VERSION: 09.38.00
* BRIEF: Read repo metadata from Gitea manifest API, auto-detect the rest
*/
declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoEnterprise\CliFramework;
use MokoCli\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
{
$this->setDescription('Parse manifest.xml and output requested field(s) for CI consumption');
$this->addArgument('--path', 'Repository root path', '.');
$this->addArgument('--field', 'Single field name to output', '');
$this->addArgument('--all', 'Print all fields as KEY=VALUE lines', false);
$this->addArgument('--github-output', 'Append all fields to $GITHUB_OUTPUT', false);
$this->addArgument('--json', 'Output all fields as JSON', false);
$this->setDescription('Read repo metadata from Gitea API with auto-detection fallback');
$this->addArgument('--path', 'Repository root path', '.');
$this->addArgument('--field', 'Single field name to output', '');
$this->addArgument('--all', 'Print all fields as KEY=VALUE lines', false);
$this->addArgument('--github-output', 'Append all fields to $GITHUB_OUTPUT', false);
$this->addArgument('--json', 'Output all fields as JSON', false);
}
protected function run(): int
{
$path = $this->getArgument('--path');
$field = $this->getArgument('--field');
$path = $this->getArgument('--path');
$field = $this->getArgument('--field');
$showAll = $this->getArgument('--all');
$ghOutput = $this->getArgument('--github-output');
$ghOut = $this->getArgument('--github-output');
$jsonMode = $this->getArgument('--json');
// Determine mode
if ($ghOutput) {
$mode = 'github-output';
} elseif ($showAll) {
$mode = 'all';
} elseif ($jsonMode) {
$mode = 'json';
} else {
$mode = 'field';
$mode = match (true) {
(bool) $ghOut => 'github-output',
(bool) $showAll => 'all',
(bool) $jsonMode => 'json',
default => 'field',
};
$root = realpath($path) ?: $path;
// ── 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 --
$root = realpath($path) ?: $path;
$manifestFile = null;
// ── 3. Fallback: auto-detect from source tree ────────────────────
if ($fields === null) {
$this->log('INFO', 'API unavailable — falling back to source-tree detection');
$fields = $this->autoDetect($root, $repo);
}
// Priority: manifest.xml (current standard)
$candidates = [
"{$root}/.mokogitea/manifest.xml",
"{$root}/.mokogitea/.manifest.xml", // legacy (dot-prefixed)
"{$root}/.mokogitea/.moko-platform", // legacy v4
if (empty($fields)) {
$this->log('ERROR', "Could not resolve metadata for {$root}");
return 1;
}
// 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) {
if (file_exists($candidate)) {
$manifestFile = $candidate;
// Resolve source directory (source/ or src/)
$srcDir = null;
foreach (['source', 'src'] as $candidate) {
if (is_dir("{$root}/{$candidate}")) {
$srcDir = $candidate;
break;
}
}
if ($manifestFile === null) {
$this->log('ERROR', "No manifest found in {$root}");
return 1;
// ── Try Joomla detection ─────────────────────────────────────
$joomlaResult = $this->detectJoomla($root, $srcDir);
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 --
$xml = @simplexml_load_file($manifestFile);
// ── Try Dolibarr detection ───────────────────────────────────
$dolibarrResult = $this->detectDolibarr($root);
if ($dolibarrResult !== null) {
$fields = array_merge($fields, $dolibarrResult);
$this->log('INFO', "Auto-detected platform: dolibarr");
return $fields;
}
if ($xml === false) {
// Fallback: try YAML format (.mokostandards legacy)
$content = file_get_contents($manifestFile);
$fields = [];
if (preg_match('/^platform:\s*(.+)/m', $content, $m)) {
$fields['platform'] = trim($m[1], " \t\n\r\"'");
// ── Generic fallback ─────────────────────────────────────────
$fields['platform'] = $this->detectGenericPlatform($root);
$fields['element_name'] = strtolower($fields['name']);
$fields['extension_type'] = 'application';
$fields['language'] = $this->detectLanguage($root);
if ($srcDir !== null) {
$fields['entry_point'] = "{$srcDir}/";
}
$this->log('INFO', "Auto-detected platform: {$fields['platform']}");
return $fields;
}
/**
* 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 {
$element = strtolower($basename);
}
// Derive display name
$displayName = (string) ($xml->name ?? ucfirst(str_replace('_', ' ', $basename)));
return [
'platform' => 'joomla',
'extension_type' => $extType,
'element_name' => $element,
'display_name' => $displayName,
'language' => 'PHP',
'entry_point' => ($srcDir ?? '.') . '/',
];
}
if (preg_match('/^standards_version:\s*(.+)/m', $content, $m)) {
$fields['standards-version'] = trim($m[1], " \t\n\r\"'");
// Also check for pkg_*.xml pattern specifically
$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 ?? '.') . '/',
];
}
if (preg_match('/^governed_repo:\s*(.+)/m', $content, $m)) {
$fields['name'] = trim($m[1], " \t\n\r\"'");
}
// 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 ?? '.') . '/',
];
}
}
} else {
// Register namespace for XPath (optional, simple path works without)
$fields = [
'name' => (string)($xml->identity->name ?? ''),
'display-name' => (string)($xml->identity->{"display-name"} ?? ''),
'org' => (string)($xml->identity->org ?? ''),
'description' => (string)($xml->identity->description ?? ''),
'license' => (string)($xml->identity->license ?? ''),
'license-spdx' => (string)($xml->identity->license['spdx'] ?? ''),
'platform' => (string)($xml->governance->platform ?? ''),
'standards-version' => (string)($xml->governance->{"standards-version"} ?? ''),
'standards-source' => (string)($xml->governance->{"standards-source"} ?? ''),
'language' => (string)($xml->build->language ?? ''),
'package-type' => (string)($xml->build->{"package-type"} ?? ''),
'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,
}
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' => './',
];
}
// Strip empty values for cleaner output
$fields = array_filter($fields, fn($v) => $v !== '');
return null;
}
// -- Output --
/**
* 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) {
case 'field':
if ($field === '') {
$this->log('ERROR', "Usage: manifest_read.php --path <dir> --field <name>");
$this->log('ERROR', " manifest_read.php --path <dir> --all");
$this->log('ERROR', " manifest_read.php --path <dir> --json");
$this->log('ERROR', " manifest_read.php --path <dir> --github-output");
$this->log('ERROR', "Usage: manifest:read --path <dir> --field <name>");
$this->log('ERROR', " manifest:read --path <dir> --all");
$this->log('ERROR', " manifest:read --path <dir> --json");
$this->log('ERROR', " manifest:read --path <dir> --github-output");
return 2;
}
echo ($fields[$field] ?? '') . "\n";
@@ -142,22 +447,21 @@ class ManifestReadCli extends CliFramework
break;
case 'github-output':
$outputFile = getenv('GITHUB_OUTPUT');
if ($outputFile === false || $outputFile === '') {
$this->log('ERROR', 'GITHUB_OUTPUT not set — printing to stdout instead');
foreach ($fields as $k => $v) {
// Convert field-name to FIELD_NAME for env var style
$envKey = str_replace('-', '_', $k);
echo "{$envKey}={$v}\n";
}
$outputFile = getenv('GITHUB_OUTPUT') ?: getenv('GITEA_OUTPUT') ?: '';
$lines = [];
foreach ($fields as $k => $v) {
$envKey = str_replace('-', '_', $k);
$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 {
$fh = fopen($outputFile, 'a');
foreach ($fields as $k => $v) {
$envKey = str_replace('-', '_', $k);
fwrite($fh, "{$envKey}={$v}\n");
}
fclose($fh);
$this->log('INFO', "Wrote " . count($fields) . " fields to GITHUB_OUTPUT");
file_put_contents($outputFile, $output, FILE_APPEND);
$this->log('INFO', "Wrote " . count($lines) . " fields to GITHUB_OUTPUT");
}
break;
}
+7 -12
View File
@@ -6,9 +6,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* DEFGROUP: mokocli.CLI
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/package_build.php
* 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';
use MokoEnterprise\CliFramework;
use MokoCli\{CliFramework, SourceResolver};
class PackageBuildCli extends CliFramework
{
@@ -56,18 +56,13 @@ class PackageBuildCli extends CliFramework
}
// -- Determine source directory -----------------------------------------------
$sourceDir = null;
foreach (['src', 'htdocs'] as $candidate) {
if (is_dir("{$root}/{$candidate}")) {
$sourceDir = "{$root}/{$candidate}";
break;
}
}
$sourceDir = SourceResolver::resolveAbsolute($root);
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;
}
SourceResolver::warnIfLegacy($root);
// -- Determine element and type prefix from manifest --------------------------
$extElement = $elementOverride;
+163 -20
View File
@@ -6,25 +6,32 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* DEFGROUP: mokocli.CLI
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/platform_detect.php
* BRIEF: Detect platform from manifest.xml file outputs platform string
* VERSION: 09.38.00
* BRIEF: Auto-detect repository platform type and optionally update manifest
*/
declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoEnterprise\CliFramework;
use MokoCli\CliFramework;
class PlatformDetectCli extends CliFramework
{
protected function configure(): void
{
$this->setDescription('Detect platform from manifest.xml file');
$this->addArgument('--path', 'Repository root path', '.');
$this->setDescription('Auto-detect repository platform type and optionally update manifest');
$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
@@ -32,25 +39,161 @@ class PlatformDetectCli extends CliFramework
$path = $this->getArgument('--path');
$root = realpath($path) ?: $path;
// Check .mokogitea/manifest.xml first, fallback to root
$file = "{$root}/.mokogitea/manifest.xml";
if (!file_exists($file)) {
$file = "{$root}/.mokostandards";
}
if (!file_exists($file)) {
echo "unknown\n";
return 0;
$token = $this->getArgument('--token');
$giteaUrl = rtrim($this->getArgument('--gitea-url'), '/');
$owner = $this->getArgument('--owner');
$repo = $this->getArgument('--repo');
$doUpdate = $this->isFlagSet('--update');
$githubOutput = $this->isFlagSet('--github-output');
$platform = $this->detectPlatform($root);
$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 {
$this->log('WARN', '$GITHUB_OUTPUT is not set; skipping output append.');
}
}
$content = file_get_contents($file);
if (preg_match('/^platform:\s*(.+)/m', $content, $m)) {
echo trim($m[1], " \t\n\r\"'") . "\n";
} else {
echo "unknown\n";
// 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;
}
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();
+6 -6
View File
@@ -6,24 +6,24 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* DEFGROUP: mokocli.CLI
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* 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);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoEnterprise\CliFramework;
use MokoCli\CliFramework;
class ReleaseCli extends CliFramework
{
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', '');
}
+4 -4
View File
@@ -6,9 +6,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* DEFGROUP: mokocli.CLI
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/release_body_update.php
* 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';
use MokoEnterprise\CliFramework;
use MokoCli\CliFramework;
class ReleaseBodyUpdateCli extends CliFramework
{
+314 -9
View File
@@ -6,31 +6,336 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* DEFGROUP: mokocli.CLI
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/release_cascade.php
* VERSION: 09.23.00
* BRIEF: DEPRECATED cascade behavior removed. Each release stream is independent.
* VERSION: 09.38.00
* BRIEF: Cascade release zip to all lower stability channels
*/
declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoEnterprise\CliFramework;
use MokoCli\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
{
$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
{
$this->log('INFO', 'No-op (cascade behavior removed — each stream is independent)');
return 0;
$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;
}
$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;
}
}
+7 -8
View File
@@ -6,9 +6,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* DEFGROUP: mokocli.CLI
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/release_create.php
* 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';
use MokoEnterprise\CliFramework;
use MokoCli\{CliFramework, SourceResolver};
class ReleaseCreateCli extends CliFramework
{
@@ -97,8 +97,8 @@ class ReleaseCreateCli extends CliFramework
// Find extension manifest (Joomla XML)
$extManifest = null;
$manifestFiles = array_merge(
glob("{$root}/src/pkg_*.xml") ?: [],
glob("{$root}/src/*.xml") ?: [],
SourceResolver::globSource($root, 'pkg_*.xml'),
SourceResolver::globSource($root, '*.xml'),
glob("{$root}/*.xml") ?: []
);
foreach ($manifestFiles as $file) {
@@ -112,8 +112,7 @@ class ReleaseCreateCli extends CliFramework
// Find Dolibarr module file
$modFile = null;
$modFiles = array_merge(
glob("{$root}/src/core/modules/mod*.class.php") ?: [],
glob("{$root}/htdocs/core/modules/mod*.class.php") ?: [],
SourceResolver::globSource($root, 'core/modules/mod*.class.php'),
glob("{$root}/core/modules/mod*.class.php") ?: []
);
foreach ($modFiles as $file) {
+4 -4
View File
@@ -6,9 +6,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* DEFGROUP: mokocli.CLI
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/release_manage.php
* 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';
use MokoEnterprise\CliFramework;
use MokoCli\CliFramework;
class ReleaseManageCli extends CliFramework
{
+6 -6
View File
@@ -6,9 +6,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* DEFGROUP: mokocli.CLI
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/release_mirror.php
* 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';
use MokoEnterprise\CliFramework;
use MokoCli\CliFramework;
class ReleaseMirrorCli extends CliFramework
{
@@ -201,7 +201,7 @@ class ReleaseMirrorCli extends CliFramework
CURLOPT_HTTPHEADER => [
"Authorization: token {$token}",
'Accept: application/vnd.github+json',
'User-Agent: moko-platform',
'User-Agent: mokocli',
'Content-Type: application/json',
],
CURLOPT_TIMEOUT => 30,
@@ -229,7 +229,7 @@ class ReleaseMirrorCli extends CliFramework
CURLOPT_HTTPHEADER => [
"Authorization: token {$token}",
'Accept: application/vnd.github+json',
'User-Agent: moko-platform',
'User-Agent: mokocli',
'Content-Type: application/octet-stream',
],
CURLOPT_POSTFIELDS => file_get_contents($filePath),
+4 -4
View File
@@ -6,9 +6,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* DEFGROUP: mokocli.CLI
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/release_notes.php
* 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';
use MokoEnterprise\CliFramework;
use MokoCli\CliFramework;
class ReleaseNotesCli extends CliFramework
{
+52 -12
View File
@@ -6,9 +6,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* DEFGROUP: mokocli.CLI
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/release_package.php
* 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';
use MokoEnterprise\CliFramework;
use MokoCli\{CliFramework, SourceResolver};
class ReleasePackageCli extends CliFramework
{
@@ -99,9 +99,10 @@ class ReleasePackageCli extends CliFramework
$extFolder = '';
$typePrefix = '';
SourceResolver::warnIfLegacy($root);
$manifestFiles = array_merge(
glob("{$root}/src/pkg_*.xml") ?: [],
glob("{$root}/src/*.xml") ?: [],
SourceResolver::globSource($root, 'pkg_*.xml'),
SourceResolver::globSource($root, '*.xml'),
glob("{$root}/*.xml") ?: []
);
@@ -200,14 +201,12 @@ class ReleasePackageCli extends CliFramework
}
}
if ($sourceDir === null && is_dir("{$root}/src")) {
$sourceDir = "{$root}/src";
} elseif ($sourceDir === null && is_dir("{$root}/htdocs")) {
$sourceDir = "{$root}/htdocs";
if ($sourceDir === null) {
$sourceDir = SourceResolver::resolveAbsolute($root);
}
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;
}
@@ -225,17 +224,58 @@ class ReleasePackageCli extends CliFramework
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) ?: [];
foreach ($packageDirs as $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";
// 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();
if ($subZip->open($subZipPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) {
$this->log('ERROR', "Failed to create sub-package ZIP: {$subZipPath}");
continue;
}
$this->addDirToZip($subZip, $pkgDir, '', $this->excludePatterns);
$this->addDirToZip($subZip, $subSourceDir, '', $this->excludePatterns);
$subZip->close();
$zip->addFile($subZipPath, "packages/{$subName}.zip");
+6 -6
View File
@@ -6,9 +6,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* DEFGROUP: mokocli.CLI
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/release_promote.php
* 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';
use MokoEnterprise\CliFramework;
use MokoCli\{CliFramework, SourceResolver};
class ReleasePromoteCli extends CliFramework
{
@@ -109,8 +109,8 @@ class ReleasePromoteCli extends CliFramework
if ($to === 'stable') {
$root = realpath($path) ?: $path;
$manifestFiles = array_merge(
glob("{$root}/src/pkg_*.xml") ?: [],
glob("{$root}/src/*.xml") ?: [],
SourceResolver::globSource($root, 'pkg_*.xml'),
SourceResolver::globSource($root, '*.xml'),
glob("{$root}/*.xml") ?: []
);
foreach ($manifestFiles as $xmlFile) {
+5 -5
View File
@@ -6,11 +6,11 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* DEFGROUP: mokocli.CLI
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/release_publish.php
* VERSION: 09.23.00
* VERSION: 09.38.00
* 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';
use MokoEnterprise\CliFramework;
use MokoCli\CliFramework;
class ReleasePublishCli extends CliFramework
{
+11 -8
View File
@@ -6,9 +6,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* DEFGROUP: mokocli.CLI
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/release_validate.php
* 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';
use MokoEnterprise\CliFramework;
use MokoCli\{CliFramework, SourceResolver};
class ReleaseValidateCli extends CliFramework
{
@@ -66,8 +66,10 @@ class ReleaseValidateCli extends CliFramework
$platform = 'generic';
}
}
$hasSource = is_dir("{$root}/src") || is_dir("{$root}/htdocs");
$this->addVResult('Source directory', $hasSource ? 'PASS' : 'WARN', $hasSource ? 'src/ or htdocs/ found' : 'No src/ or htdocs/ directory');
$hasSource = SourceResolver::resolveAbsolute($root) !== null;
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")) {
$this->addVResult('README.md', 'FAIL', 'Not found');
} else {
@@ -109,7 +111,8 @@ class ReleaseValidateCli extends CliFramework
$this->addVResult('LICENSE', $licenseFound ? 'PASS' : 'FAIL', $licenseFound ? 'Found' : 'Not found');
if ($platform === 'joomla') {
$manifest = null;
foreach (["{$root}/src", $root] as $dir) {
$srcAbs = SourceResolver::resolveAbsolute($root);
foreach (array_filter([$srcAbs, $root]) as $dir) {
if (!is_dir($dir)) {
continue;
} foreach (glob("{$dir}/*.xml") as $xmlFile) {
@@ -156,7 +159,7 @@ class ReleaseValidateCli extends CliFramework
}
} elseif ($platform === 'dolibarr') {
$modFile = null;
foreach (['src', 'htdocs'] as $sd) {
foreach (SourceResolver::getCandidates() as $sd) {
$matches = glob("{$root}/{$sd}/mod*.class.php");
if (!empty($matches)) {
$modFile = $matches[0];
+4 -4
View File
@@ -6,9 +6,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* DEFGROUP: mokocli.CLI
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/release_verify.php
* 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';
use MokoEnterprise\CliFramework;
use MokoCli\CliFramework;
class ReleaseVerifyCli extends CliFramework
{
+429
View File
@@ -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());
+5 -5
View File
@@ -8,11 +8,11 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* DEFGROUP: mokocli.CLI
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/scaffold_client.php
* VERSION: 09.23.00
* VERSION: 09.38.00
* 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';
use MokoEnterprise\CliFramework;
use MokoCli\CliFramework;
class ScaffoldClientCli extends CliFramework
{
+7 -7
View File
@@ -8,9 +8,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* DEFGROUP: mokocli.CLI
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/sync_rulesets.php
* 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__ . '/../lib/Enterprise/CliFramework.php';
use MokoEnterprise\CliFramework;
use MokoEnterprise\Config;
use MokoEnterprise\PlatformAdapterFactory;
use MokoCli\CliFramework;
use MokoCli\Config;
use MokoCli\PlatformAdapterFactory;
class SyncRulesetsCli extends CliFramework
{
@@ -46,7 +46,7 @@ class SyncRulesetsCli extends CliFramework
);
$platformName = $adapter->getPlatformName();
$ALWAYS_EXCLUDE = ['moko-platform', '.github-private'];
$ALWAYS_EXCLUDE = ['mokocli', '.github-private'];
// -- Protection rules (platform-agnostic format) --
$PROTECTIONS = [
+7 -12
View File
@@ -6,9 +6,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* DEFGROUP: mokocli.CLI
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/theme_lint.php
* 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';
use MokoEnterprise\CliFramework;
use MokoCli\{CliFramework, SourceResolver};
class ThemeLintCli extends CliFramework
{
@@ -41,17 +41,12 @@ class ThemeLintCli extends CliFramework
$errors = 0;
$warnings = 0;
$srcDir = null;
foreach (['src', 'htdocs'] as $d) {
if (is_dir("{$root}/{$d}")) {
$srcDir = "{$root}/{$d}";
break;
}
}
$srcDir = SourceResolver::resolveAbsolute($root);
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;
}
SourceResolver::warnIfLegacy($root);
echo "Theme Lint: {$srcDir}\n\n";
+5 -5
View File
@@ -6,9 +6,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* DEFGROUP: mokocli.CLI
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/updates_xml_build.php
* BRIEF: Generate Joomla updates.xml from extension manifest metadata
*/
@@ -17,7 +17,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoEnterprise\CliFramework;
use MokoCli\{CliFramework, SourceResolver};
class UpdatesXmlBuildCli extends CliFramework
{
@@ -109,7 +109,7 @@ class UpdatesXmlBuildCli extends CliFramework
// -- Locate Joomla manifest ---------------------------------------------------
$manifest = null;
$candidates = glob("{$root}/src/pkg_*.xml") ?: [];
$candidates = SourceResolver::globSource($root, 'pkg_*.xml');
foreach ($candidates as $f) {
if (strpos(file_get_contents($f), '<extension') !== false) {
$manifest = $f;
+5 -5
View File
@@ -6,11 +6,11 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* DEFGROUP: mokocli.CLI
* INGROUP: mokocli
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
* PATH: /cli/updates_xml_sync.php
* VERSION: 09.23.00
* VERSION: 09.38.00
* BRIEF: Sync updates.xml to target branches via Gitea API
* NOTE: Called by pre-release and auto-release workflows after updates.xml
* 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';
use MokoEnterprise\CliFramework;
use MokoCli\CliFramework;
class UpdatesXmlSyncCli extends CliFramework
{

Some files were not shown because too many files have changed in this diff Show More