From 0f7eac971fd06cad828a6558b14c5a2c710014b2 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 14 Apr 2026 21:11:20 -0500 Subject: [PATCH] feat: add templates, cli, docs, and Gitea-first platform updates - Add cli/, build/, release/ directories (previously in MokoStandards/api/) - Add templates/ with workflow templates, configs, github/gitea issue templates - Add docs/api/, docs/workflows/, docs/automation/ documentation - Update all REPO headers to MokoStandards-API - Update all api/definitions/ paths to definitions/ (no api/ prefix) - Set default platform to gitea, token to GA_TOKEN - Add syncUpdatesBetweenPlatforms() to PlatformAdapterFactory - Update RepositorySynchronizer for new template paths - Convert workflow template DEFGROUP to Gitea.Workflow - Update README with full repo description and platform config Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 30 +- automation/bulk_joomla_template.php | 2 +- automation/bulk_sync.php | 4 +- automation/migrate_to_gitea.php | 4 +- automation/push_files.php | 4 +- automation/repo_cleanup.php | 4 +- build/index.md | 19 + build/moko-make | 112 ++ cli/archive_repo.php | 158 ++ cli/create_project.php | 477 +++++ cli/create_repo.php | 255 +++ cli/joomla_release.php | 394 ++++ cli/platform_detect.php | 41 + cli/release.php | 172 ++ cli/release_notes.php | 67 + cli/sync_rulesets.php | 178 ++ cli/version_bump.php | 63 + cli/version_read.php | 38 + cli/version_set_platform.php | 119 ++ deploy/deploy-joomla.php | 4 +- deploy/deploy-sftp.php | 4 +- docs/ARCHITECTURE.md | 2 +- docs/api/analysis/index.md | 23 + docs/api/automation/index.md | 138 ++ docs/api/definitions/default/index.md | 212 +++ docs/api/definitions/sync/index.md | 224 +++ docs/api/deploy/index.md | 162 ++ docs/api/fix/index.md | 114 ++ docs/api/index.md | 322 ++++ docs/api/lib/index.md | 20 + docs/api/maintenance/index.md | 180 ++ docs/api/plugin/index.md | 176 ++ docs/api/tests/index.md | 62 + docs/api/tests/sample/index.md | 56 + docs/api/validate/index.md | 276 +++ docs/api/wrappers/index.md | 20 + docs/automation/README.md | 136 ++ docs/automation/branch-version-automation.md | 337 ++++ docs/automation/push-files.md | 57 + docs/automation/repo-cleanup.md | 114 ++ docs/workflows/README.md | 598 +++++++ docs/workflows/auto-release.md | 103 ++ docs/workflows/build-release.md | 87 + docs/workflows/bulk-repo-sync.md | 775 ++++++++ docs/workflows/changelog-management.md | 338 ++++ docs/workflows/demo-deployment.md | 75 + docs/workflows/dev-branch-tracking.md | 390 ++++ docs/workflows/dev-deployment.md | 280 +++ docs/workflows/index.md | 71 + docs/workflows/release-system.md | 211 +++ docs/workflows/reserve-dolibarr-module-id.md | 693 ++++++++ docs/workflows/reusable-workflows.md | 567 ++++++ docs/workflows/rs-deployment.md | 43 + docs/workflows/shared-workflows.md | 239 +++ docs/workflows/standards-compliance.md | 588 ++++++ docs/workflows/sub-issue-management.md | 219 +++ docs/workflows/update-server.md | 333 ++++ docs/workflows/workflow-architecture.md | 559 ++++++ docs/workflows/workflow-inventory.md | 1577 +++++++++++++++++ fix/fix_line_endings.php | 4 +- fix/fix_permissions.php | 4 +- fix/fix_tabs.php | 4 +- fix/fix_trailing_spaces.php | 4 +- lib/CliBase.php | 4 +- lib/Common.php | 4 +- lib/Enterprise/CliFramework.php | 4 +- lib/Enterprise/Config.php | 6 +- lib/Enterprise/DefinitionParser.php | 8 +- .../EnterpriseReadinessValidator.php | 4 +- lib/Enterprise/FileFixUtility.php | 4 +- lib/Enterprise/GitHubAdapter.php | 4 +- lib/Enterprise/GitPlatformAdapter.php | 4 +- lib/Enterprise/GiteaAdapter.php | 4 +- lib/Enterprise/PackageBuilder.php | 4 +- lib/Enterprise/PlatformAdapterFactory.php | 70 +- lib/Enterprise/Plugins/ApiPlugin.php | 4 +- .../Plugins/DocumentationPlugin.php | 4 +- lib/Enterprise/Plugins/DolibarrPlugin.php | 4 +- lib/Enterprise/Plugins/GenericPlugin.php | 4 +- lib/Enterprise/Plugins/JoomlaPlugin.php | 4 +- lib/Enterprise/Plugins/MobilePlugin.php | 4 +- lib/Enterprise/Plugins/NodeJsPlugin.php | 4 +- lib/Enterprise/Plugins/PythonPlugin.php | 4 +- lib/Enterprise/Plugins/TerraformPlugin.php | 4 +- lib/Enterprise/Plugins/WordPressPlugin.php | 4 +- lib/Enterprise/ProjectConfigValidator.php | 4 +- lib/Enterprise/ProjectMetricsCollector.php | 4 +- lib/Enterprise/ProjectTypeDetector.php | 4 +- lib/Enterprise/RepositoryHealthChecker.php | 4 +- lib/Enterprise/RepositorySynchronizer.php | 18 +- lib/Enterprise/SynchronizationException.php | 4 +- lib/plugins/Joomla/UpdateXmlGenerator.php | 4 +- maintenance/pin_action_shas.php | 4 +- maintenance/repo_inventory.php | 4 +- maintenance/rotate_secrets.php | 4 +- maintenance/setup_labels.php | 4 +- maintenance/sync_dolibarr_readmes.php | 4 +- maintenance/update_repo_inventory.php | 4 +- maintenance/update_sha_hashes.php | 4 +- maintenance/update_version_from_readme.php | 4 +- plugin_health_check.php | 4 +- plugin_list.php | 4 +- plugin_metrics.php | 4 +- plugin_readiness.php | 4 +- plugin_validate.php | 4 +- release/.gitkeep | 0 release/generate_dolibarr_version_txt.php | 355 ++++ release/generate_joomla_update_xml.php | 577 ++++++ release/index.md | 20 + templates/configs/.eslintrc.json | 28 + templates/configs/.gitignore.joomla | 215 +++ templates/configs/.htmlhintrc | 22 + templates/configs/.prettierrc.json | 11 + templates/configs/.pylintrc | 92 + templates/configs/README.md | 307 ++++ templates/configs/composer.dolibarr.json | 55 + templates/configs/composer.generic.json | 51 + templates/configs/composer.joomla.json | 55 + templates/configs/ftpignore | 47 + templates/configs/gitignore | 202 +++ templates/configs/gitignore.dolibarr | 220 +++ templates/configs/index.md | 38 + templates/configs/mokostandards.yml.template | 20 + templates/configs/pa11yci.json | 22 + templates/configs/phpcs.xml | 54 + templates/configs/phpstan.dolibarr.neon | 39 + templates/configs/phpstan.joomla.neon | 32 + templates/configs/phpstan.neon | 35 + templates/configs/psalm.xml | 34 + templates/configs/pyproject.toml | 85 + templates/gitea/ISSUE_TEMPLATE/adr.md | 110 ++ templates/gitea/ISSUE_TEMPLATE/bug_report.md | 48 + .../gitea/ISSUE_TEMPLATE/documentation.md | 52 + .../gitea/ISSUE_TEMPLATE/dolibarr_issue.md | 92 + .../dolibarr_module_id_request.md | 189 ++ .../ISSUE_TEMPLATE/enterprise_support.md | 85 + .../gitea/ISSUE_TEMPLATE/feature_request.md | 51 + .../gitea/ISSUE_TEMPLATE/firewall-request.md | 190 ++ .../gitea/ISSUE_TEMPLATE/joomla_issue.md | 87 + templates/gitea/ISSUE_TEMPLATE/question.md | 82 + .../gitea/ISSUE_TEMPLATE/request-license.md | 107 ++ templates/gitea/ISSUE_TEMPLATE/rfc.md | 126 ++ templates/gitea/ISSUE_TEMPLATE/security.md | 51 + templates/gitea/ISSUE_TEMPLATE/version.md | 24 + templates/gitea/PULL_REQUEST_TEMPLATE.md | 86 + templates/gitea/renovate.json | 38 + templates/github/CLAUDE.dolibarr.md.template | 296 ++++ templates/github/CLAUDE.joomla.md.template | 296 ++++ templates/github/CLAUDE.md.template | 366 ++++ templates/github/CODEOWNERS | 55 + templates/github/CODEOWNERS.template | 51 + templates/github/ISSUE_TEMPLATE/adr.md | 110 ++ templates/github/ISSUE_TEMPLATE/bug_report.md | 48 + templates/github/ISSUE_TEMPLATE/config.yml | 18 + .../github/ISSUE_TEMPLATE/documentation.md | 52 + .../github/ISSUE_TEMPLATE/dolibarr_issue.md | 92 + .../dolibarr_module_id_request.md | 189 ++ .../ISSUE_TEMPLATE/enterprise_support.md | 85 + .../github/ISSUE_TEMPLATE/feature_request.md | 51 + .../github/ISSUE_TEMPLATE/firewall-request.md | 190 ++ .../github/ISSUE_TEMPLATE/joomla_issue.md | 87 + templates/github/ISSUE_TEMPLATE/question.md | 82 + .../github/ISSUE_TEMPLATE/request-license.md | 107 ++ templates/github/ISSUE_TEMPLATE/rfc.md | 126 ++ templates/github/ISSUE_TEMPLATE/security.md | 51 + templates/github/ISSUE_TEMPLATE/version.md | 24 + templates/github/PULL_REQUEST_TEMPLATE.md | 86 + .../github/PULL_REQUEST_TEMPLATE.md.backup | 85 + templates/github/README.md | 386 ++++ .../copilot-instructions.dolibarr.md.template | 335 ++++ .../copilot-instructions.joomla.md.template | 307 ++++ .../github/copilot-instructions.md.template | 328 ++++ templates/github/dependabot.yml.template | 151 ++ templates/github/override.tf.template | 114 ++ templates/joomla/index.html | 1 + templates/joomla/updates.xml.template | 59 + templates/scripts/README.md | 129 ++ templates/scripts/common/CliBase.template.php | 80 + .../deploy/sftp-config.dev.json.example | 48 + .../deploy/sftp-config.rs.json.example | 48 + templates/scripts/fix/index.md | 16 + templates/scripts/index.md | 27 + templates/scripts/release/index.md | 16 + .../scripts/release/package_dolibarr.php | 210 +++ templates/scripts/release/package_joomla.php | 217 +++ templates/scripts/sftp-config/README.md | 99 ++ .../scripts/validate/dolibarr_module.php | 178 ++ templates/scripts/validate/index.md | 16 + .../scripts/validate/validate_manifest.php | 193 ++ .../scripts/validate/validate_structure.php | 162 ++ templates/stubs/dolibarr.php | 195 ++ templates/stubs/joomla.php | 312 ++++ templates/workflows/README.md | 708 ++++++++ templates/workflows/audit-log-archival.yml | 155 ++ templates/workflows/auto-update-changelog.md | 296 ++++ .../dolibarr/auto-release.yml.template | 370 ++++ .../dolibarr/ci-dolibarr.yml.template | 305 ++++ templates/workflows/dolibarr/index.md | 21 + .../publish-to-mokodolimods.yml.template | 259 +++ templates/workflows/dolibarr/release-guide.md | 379 ++++ .../dolibarr/repo_health.yml.template | 783 ++++++++ templates/workflows/generic/ci.yml.template | 39 + .../generic/code-quality.yml.template | 325 ++++ .../generic/codeql-analysis.yml.template | 115 ++ .../generic/dependency-review.yml.template | 275 +++ .../workflows/generic/deploy.yml.template | 281 +++ templates/workflows/generic/index.md | 24 + templates/workflows/generic/test.yml.template | 292 +++ templates/workflows/health-check.yml | 323 ++++ templates/workflows/index.md | 26 + templates/workflows/integration-tests.yml | 372 ++++ .../joomla/auto-release.yml.template | 560 ++++++ .../workflows/joomla/ci-joomla.yml.template | 384 ++++ .../joomla/deploy-manual.yml.template | 132 ++ templates/workflows/joomla/index.md | 24 + .../workflows/joomla/repo_health.yml.template | 795 +++++++++ .../joomla/update-server.yml.template | 346 ++++ templates/workflows/metrics-collection.yml | 245 +++ templates/workflows/security-scan.yml | 310 ++++ .../workflows/shared/auto-assign.yml.template | 76 + .../shared/auto-dev-issue.yml.template | 207 +++ .../shared/auto-release.yml.template | 337 ++++ .../shared/branch-freeze.yml.template | 114 ++ .../shared/changelog-validation.yml.template | 99 ++ .../workflows/shared/deploy-demo.yml.template | 719 ++++++++ .../workflows/shared/deploy-dev.yml.template | 684 +++++++ .../workflows/shared/deploy-rs.yml.template | 661 +++++++ .../enterprise-firewall-setup.yml.template | 758 ++++++++ .../shared/repository-cleanup.yml.template | 525 ++++++ .../shared/sync-version-on-merge.yml.template | 135 ++ templates/workflows/terraform/ci.yml.template | 207 +++ .../workflows/terraform/deploy.yml.template | 210 +++ .../terraform/drift-detection.yml.template | 231 +++ templates/workflows/terraform/index.md | 290 +++ .../manage-repo-templates.yml.template | 368 ++++ templates/workflows/validate-api-project.yml | 104 ++ .../validate-documentation-project.yml | 104 ++ .../workflows/validate-dolibarr-project.yml | 91 + .../workflows/validate-generic-project.yml | 91 + .../workflows/validate-joomla-project.yml | 162 ++ .../workflows/validate-mobile-project.yml | 91 + .../workflows/validate-nodejs-project.yml | 147 ++ .../workflows/validate-python-project.yml | 111 ++ .../workflows/validate-terraform-project.yml | 106 ++ .../workflows/validate-wordpress-project.yml | 103 ++ tests/Enterprise/GitPlatformAdapterTest.php | 4 +- validate/auto_detect_platform.php | 6 +- validate/check_changelog.php | 4 +- validate/check_composer_deps.php | 4 +- validate/check_dolibarr_module.php | 4 +- validate/check_enterprise_readiness.php | 4 +- validate/check_joomla_manifest.php | 4 +- validate/check_language_structure.php | 4 +- validate/check_license_headers.php | 4 +- validate/check_no_secrets.php | 4 +- validate/check_paths.php | 4 +- validate/check_php_syntax.php | 4 +- validate/check_repo_health.php | 4 +- validate/check_structure.php | 4 +- validate/check_tabs.php | 4 +- validate/check_version_consistency.php | 6 +- validate/check_xml_wellformed.php | 4 +- validate/scan_drift.php | 4 +- wrappers/auto_detect_platform.php | 4 +- wrappers/bulk_sync.php | 4 +- wrappers/check_changelog.php | 4 +- wrappers/check_dolibarr_module.php | 4 +- wrappers/check_enterprise_readiness.php | 4 +- wrappers/check_joomla_manifest.php | 4 +- wrappers/check_language_structure.php | 4 +- wrappers/check_license_headers.php | 4 +- wrappers/check_no_secrets.php | 4 +- wrappers/check_paths.php | 4 +- wrappers/check_php_syntax.php | 4 +- wrappers/check_repo_health.php | 4 +- wrappers/check_structure.php | 4 +- wrappers/check_tabs.php | 4 +- wrappers/check_version_consistency.php | 4 +- wrappers/check_xml_wellformed.php | 4 +- wrappers/deploy_sftp.php | 4 +- wrappers/fix_line_endings.php | 4 +- wrappers/fix_permissions.php | 4 +- wrappers/fix_tabs.php | 4 +- wrappers/fix_trailing_spaces.php | 4 +- wrappers/gen_wrappers.php | 8 +- wrappers/index.md | 4 +- wrappers/pin_action_shas.php | 4 +- wrappers/plugin_health_check.php | 4 +- wrappers/plugin_list.php | 4 +- wrappers/plugin_metrics.php | 4 +- wrappers/plugin_readiness.php | 4 +- wrappers/plugin_validate.php | 4 +- wrappers/scan_drift.php | 4 +- wrappers/setup_labels.php | 4 +- wrappers/sync_dolibarr_readmes.php | 4 +- wrappers/update_sha_hashes.php | 4 +- wrappers/update_version_from_readme.php | 4 +- 297 files changed, 37868 insertions(+), 227 deletions(-) create mode 100644 build/index.md create mode 100644 build/moko-make create mode 100644 cli/archive_repo.php create mode 100644 cli/create_project.php create mode 100644 cli/create_repo.php create mode 100644 cli/joomla_release.php create mode 100644 cli/platform_detect.php create mode 100644 cli/release.php create mode 100644 cli/release_notes.php create mode 100644 cli/sync_rulesets.php create mode 100644 cli/version_bump.php create mode 100644 cli/version_read.php create mode 100644 cli/version_set_platform.php create mode 100644 docs/api/analysis/index.md create mode 100644 docs/api/automation/index.md create mode 100644 docs/api/definitions/default/index.md create mode 100644 docs/api/definitions/sync/index.md create mode 100644 docs/api/deploy/index.md create mode 100644 docs/api/fix/index.md create mode 100644 docs/api/index.md create mode 100644 docs/api/lib/index.md create mode 100644 docs/api/maintenance/index.md create mode 100644 docs/api/plugin/index.md create mode 100644 docs/api/tests/index.md create mode 100644 docs/api/tests/sample/index.md create mode 100644 docs/api/validate/index.md create mode 100644 docs/api/wrappers/index.md create mode 100644 docs/automation/README.md create mode 100644 docs/automation/branch-version-automation.md create mode 100644 docs/automation/push-files.md create mode 100644 docs/automation/repo-cleanup.md create mode 100644 docs/workflows/README.md create mode 100644 docs/workflows/auto-release.md create mode 100644 docs/workflows/build-release.md create mode 100644 docs/workflows/bulk-repo-sync.md create mode 100644 docs/workflows/changelog-management.md create mode 100644 docs/workflows/demo-deployment.md create mode 100644 docs/workflows/dev-branch-tracking.md create mode 100644 docs/workflows/dev-deployment.md create mode 100644 docs/workflows/index.md create mode 100644 docs/workflows/release-system.md create mode 100644 docs/workflows/reserve-dolibarr-module-id.md create mode 100644 docs/workflows/reusable-workflows.md create mode 100644 docs/workflows/rs-deployment.md create mode 100644 docs/workflows/shared-workflows.md create mode 100644 docs/workflows/standards-compliance.md create mode 100644 docs/workflows/sub-issue-management.md create mode 100644 docs/workflows/update-server.md create mode 100644 docs/workflows/workflow-architecture.md create mode 100644 docs/workflows/workflow-inventory.md create mode 100644 release/.gitkeep create mode 100644 release/generate_dolibarr_version_txt.php create mode 100644 release/generate_joomla_update_xml.php create mode 100644 release/index.md create mode 100644 templates/configs/.eslintrc.json create mode 100644 templates/configs/.gitignore.joomla create mode 100644 templates/configs/.htmlhintrc create mode 100644 templates/configs/.prettierrc.json create mode 100644 templates/configs/.pylintrc create mode 100644 templates/configs/README.md create mode 100644 templates/configs/composer.dolibarr.json create mode 100644 templates/configs/composer.generic.json create mode 100644 templates/configs/composer.joomla.json create mode 100644 templates/configs/ftpignore create mode 100644 templates/configs/gitignore create mode 100644 templates/configs/gitignore.dolibarr create mode 100644 templates/configs/index.md create mode 100644 templates/configs/mokostandards.yml.template create mode 100644 templates/configs/pa11yci.json create mode 100644 templates/configs/phpcs.xml create mode 100644 templates/configs/phpstan.dolibarr.neon create mode 100644 templates/configs/phpstan.joomla.neon create mode 100644 templates/configs/phpstan.neon create mode 100644 templates/configs/psalm.xml create mode 100644 templates/configs/pyproject.toml create mode 100644 templates/gitea/ISSUE_TEMPLATE/adr.md create mode 100644 templates/gitea/ISSUE_TEMPLATE/bug_report.md create mode 100644 templates/gitea/ISSUE_TEMPLATE/documentation.md create mode 100644 templates/gitea/ISSUE_TEMPLATE/dolibarr_issue.md create mode 100644 templates/gitea/ISSUE_TEMPLATE/dolibarr_module_id_request.md create mode 100644 templates/gitea/ISSUE_TEMPLATE/enterprise_support.md create mode 100644 templates/gitea/ISSUE_TEMPLATE/feature_request.md create mode 100644 templates/gitea/ISSUE_TEMPLATE/firewall-request.md create mode 100644 templates/gitea/ISSUE_TEMPLATE/joomla_issue.md create mode 100644 templates/gitea/ISSUE_TEMPLATE/question.md create mode 100644 templates/gitea/ISSUE_TEMPLATE/request-license.md create mode 100644 templates/gitea/ISSUE_TEMPLATE/rfc.md create mode 100644 templates/gitea/ISSUE_TEMPLATE/security.md create mode 100644 templates/gitea/ISSUE_TEMPLATE/version.md create mode 100644 templates/gitea/PULL_REQUEST_TEMPLATE.md create mode 100644 templates/gitea/renovate.json create mode 100644 templates/github/CLAUDE.dolibarr.md.template create mode 100644 templates/github/CLAUDE.joomla.md.template create mode 100644 templates/github/CLAUDE.md.template create mode 100644 templates/github/CODEOWNERS create mode 100644 templates/github/CODEOWNERS.template create mode 100644 templates/github/ISSUE_TEMPLATE/adr.md create mode 100644 templates/github/ISSUE_TEMPLATE/bug_report.md create mode 100644 templates/github/ISSUE_TEMPLATE/config.yml create mode 100644 templates/github/ISSUE_TEMPLATE/documentation.md create mode 100644 templates/github/ISSUE_TEMPLATE/dolibarr_issue.md create mode 100644 templates/github/ISSUE_TEMPLATE/dolibarr_module_id_request.md create mode 100644 templates/github/ISSUE_TEMPLATE/enterprise_support.md create mode 100644 templates/github/ISSUE_TEMPLATE/feature_request.md create mode 100644 templates/github/ISSUE_TEMPLATE/firewall-request.md create mode 100644 templates/github/ISSUE_TEMPLATE/joomla_issue.md create mode 100644 templates/github/ISSUE_TEMPLATE/question.md create mode 100644 templates/github/ISSUE_TEMPLATE/request-license.md create mode 100644 templates/github/ISSUE_TEMPLATE/rfc.md create mode 100644 templates/github/ISSUE_TEMPLATE/security.md create mode 100644 templates/github/ISSUE_TEMPLATE/version.md create mode 100644 templates/github/PULL_REQUEST_TEMPLATE.md create mode 100644 templates/github/PULL_REQUEST_TEMPLATE.md.backup create mode 100644 templates/github/README.md create mode 100644 templates/github/copilot-instructions.dolibarr.md.template create mode 100644 templates/github/copilot-instructions.joomla.md.template create mode 100644 templates/github/copilot-instructions.md.template create mode 100644 templates/github/dependabot.yml.template create mode 100644 templates/github/override.tf.template create mode 100644 templates/joomla/index.html create mode 100644 templates/joomla/updates.xml.template create mode 100644 templates/scripts/README.md create mode 100644 templates/scripts/common/CliBase.template.php create mode 100644 templates/scripts/deploy/sftp-config.dev.json.example create mode 100644 templates/scripts/deploy/sftp-config.rs.json.example create mode 100644 templates/scripts/fix/index.md create mode 100644 templates/scripts/index.md create mode 100644 templates/scripts/release/index.md create mode 100644 templates/scripts/release/package_dolibarr.php create mode 100644 templates/scripts/release/package_joomla.php create mode 100644 templates/scripts/sftp-config/README.md create mode 100644 templates/scripts/validate/dolibarr_module.php create mode 100644 templates/scripts/validate/index.md create mode 100644 templates/scripts/validate/validate_manifest.php create mode 100644 templates/scripts/validate/validate_structure.php create mode 100644 templates/stubs/dolibarr.php create mode 100644 templates/stubs/joomla.php create mode 100644 templates/workflows/README.md create mode 100644 templates/workflows/audit-log-archival.yml create mode 100644 templates/workflows/auto-update-changelog.md create mode 100644 templates/workflows/dolibarr/auto-release.yml.template create mode 100644 templates/workflows/dolibarr/ci-dolibarr.yml.template create mode 100644 templates/workflows/dolibarr/index.md create mode 100644 templates/workflows/dolibarr/publish-to-mokodolimods.yml.template create mode 100644 templates/workflows/dolibarr/release-guide.md create mode 100644 templates/workflows/dolibarr/repo_health.yml.template create mode 100644 templates/workflows/generic/ci.yml.template create mode 100644 templates/workflows/generic/code-quality.yml.template create mode 100644 templates/workflows/generic/codeql-analysis.yml.template create mode 100644 templates/workflows/generic/dependency-review.yml.template create mode 100644 templates/workflows/generic/deploy.yml.template create mode 100644 templates/workflows/generic/index.md create mode 100644 templates/workflows/generic/test.yml.template create mode 100644 templates/workflows/health-check.yml create mode 100644 templates/workflows/index.md create mode 100644 templates/workflows/integration-tests.yml create mode 100644 templates/workflows/joomla/auto-release.yml.template create mode 100644 templates/workflows/joomla/ci-joomla.yml.template create mode 100644 templates/workflows/joomla/deploy-manual.yml.template create mode 100644 templates/workflows/joomla/index.md create mode 100644 templates/workflows/joomla/repo_health.yml.template create mode 100644 templates/workflows/joomla/update-server.yml.template create mode 100644 templates/workflows/metrics-collection.yml create mode 100644 templates/workflows/security-scan.yml create mode 100644 templates/workflows/shared/auto-assign.yml.template create mode 100644 templates/workflows/shared/auto-dev-issue.yml.template create mode 100644 templates/workflows/shared/auto-release.yml.template create mode 100644 templates/workflows/shared/branch-freeze.yml.template create mode 100644 templates/workflows/shared/changelog-validation.yml.template create mode 100644 templates/workflows/shared/deploy-demo.yml.template create mode 100644 templates/workflows/shared/deploy-dev.yml.template create mode 100644 templates/workflows/shared/deploy-rs.yml.template create mode 100644 templates/workflows/shared/enterprise-firewall-setup.yml.template create mode 100644 templates/workflows/shared/repository-cleanup.yml.template create mode 100644 templates/workflows/shared/sync-version-on-merge.yml.template create mode 100644 templates/workflows/terraform/ci.yml.template create mode 100644 templates/workflows/terraform/deploy.yml.template create mode 100644 templates/workflows/terraform/drift-detection.yml.template create mode 100644 templates/workflows/terraform/index.md create mode 100644 templates/workflows/terraform/manage-repo-templates.yml.template create mode 100644 templates/workflows/validate-api-project.yml create mode 100644 templates/workflows/validate-documentation-project.yml create mode 100644 templates/workflows/validate-dolibarr-project.yml create mode 100644 templates/workflows/validate-generic-project.yml create mode 100644 templates/workflows/validate-joomla-project.yml create mode 100644 templates/workflows/validate-mobile-project.yml create mode 100644 templates/workflows/validate-nodejs-project.yml create mode 100644 templates/workflows/validate-python-project.yml create mode 100644 templates/workflows/validate-terraform-project.yml create mode 100644 templates/workflows/validate-wordpress-project.yml diff --git a/README.md b/README.md index 68b1da6..fe65cd9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,24 @@ # MokoStandards Enterprise API -PHP implementation of MokoStandards — enterprise standards and automation framework. +PHP implementation of MokoStandards — enterprise standards, automation framework, workflow templates, and bulk sync tooling. + +> **Primary platform**: [Gitea — git.mokoconsulting.tech](https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API) +> **Backup mirror**: [GitHub](https://github.com/mokoconsulting-tech/MokoStandards-API) *(read-only mirror)* + +## What Lives Here + +| Directory | Purpose | +|-----------|---------| +| `lib/Enterprise/` | 38 PHP enterprise library classes (platform adapters, sync, validation, plugins) | +| `cli/` | CLI scripts (archive, create, release, sync rulesets, version management) | +| `automation/` | Bulk sync, push files, repo cleanup, Gitea migration | +| `validate/` | 18 validation scripts (health, structure, secrets, syntax, drift) | +| `templates/` | **Workflow templates** and config templates synced to governed repos | +| `definitions/` | Repository structure definitions (`.tf` format) | +| `deploy/` | Deployment scripts (SFTP, Joomla) | +| `maintenance/` | Labels, inventory, SHA pinning, version sync | +| `docs/` | API documentation, workflow guides, automation docs | +| `tests/` | PHPUnit test suite | ## Installation @@ -36,6 +54,16 @@ vendor/bin/moko sync vendor/bin/moko inventory -- --path . ``` +## Platform Configuration + +| Variable | Purpose | +|----------|---------| +| `GIT_PLATFORM` | `gitea` (default) or `github` | +| `GA_TOKEN` | Gitea API / Gitea Actions token | +| `GH_TOKEN` | GitHub API token (for mirror sync) | +| `GITEA_URL` | Gitea instance URL (default: `https://git.mokoconsulting.tech`) | +| `GITEA_ORG` | Gitea organization (default: `mokoconsulting-tech`) | + ## License GPL-3.0-or-later — See [LICENSE.md](LICENSE.md) diff --git a/automation/bulk_joomla_template.php b/automation/bulk_joomla_template.php index c6853b8..b35f08c 100644 --- a/automation/bulk_joomla_template.php +++ b/automation/bulk_joomla_template.php @@ -10,7 +10,7 @@ * FILE INFORMATION * DEFGROUP: MokoStandards.Automation * INGROUP: MokoStandards.Scripts - * PATH: /api/automation/bulk_joomla_template.php + * PATH: /automation/bulk_joomla_template.php * VERSION: 04.06.10 * BRIEF: Bulk scaffold and sync Joomla template repositories * diff --git a/automation/bulk_sync.php b/automation/bulk_sync.php index 0a41765..b5e49ff 100755 --- a/automation/bulk_sync.php +++ b/automation/bulk_sync.php @@ -10,8 +10,8 @@ * FILE INFORMATION * DEFGROUP: MokoStandards.Automation * INGROUP: MokoStandards.Scripts - * REPO: https://github.com/mokoconsulting-tech/MokoStandards - * PATH: /api/automation/bulk_sync.php + * REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API + * PATH: /automation/bulk_sync.php * VERSION: 04.06.00 * BRIEF: Enterprise-grade bulk repository synchronization */ diff --git a/automation/migrate_to_gitea.php b/automation/migrate_to_gitea.php index 7dd855c..5ca538c 100644 --- a/automation/migrate_to_gitea.php +++ b/automation/migrate_to_gitea.php @@ -9,8 +9,8 @@ * FILE INFORMATION * DEFGROUP: MokoStandards.Automation * INGROUP: MokoStandards - * REPO: https://github.com/mokoconsulting-tech/MokoStandards - * PATH: /api/automation/migrate_to_gitea.php + * REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API + * PATH: /automation/migrate_to_gitea.php * VERSION: 04.06.10 * BRIEF: Migrate repositories from GitHub to self-hosted Gitea instance * diff --git a/automation/push_files.php b/automation/push_files.php index bcfcfb3..e310d65 100644 --- a/automation/push_files.php +++ b/automation/push_files.php @@ -10,8 +10,8 @@ * FILE INFORMATION * DEFGROUP: MokoStandards.Automation * INGROUP: MokoStandards.Scripts - * REPO: https://github.com/mokoconsulting-tech/MokoStandards - * PATH: /api/automation/push_files.php + * REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API + * PATH: /automation/push_files.php * VERSION: 04.06.00 * BRIEF: Push one or more specific files to one or more remote repositories */ diff --git a/automation/repo_cleanup.php b/automation/repo_cleanup.php index c6d990c..e975132 100644 --- a/automation/repo_cleanup.php +++ b/automation/repo_cleanup.php @@ -10,8 +10,8 @@ * FILE INFORMATION * DEFGROUP: MokoStandards.Automation * INGROUP: MokoStandards.Scripts - * REPO: https://github.com/mokoconsulting-tech/MokoStandards - * PATH: /api/automation/repo_cleanup.php + * REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API + * PATH: /automation/repo_cleanup.php * VERSION: 04.06.00 * BRIEF: Enterprise repository cleanup — branches, PRs, issues, workflows, labels, logs */ diff --git a/build/index.md b/build/index.md new file mode 100644 index 0000000..46be81f --- /dev/null +++ b/build/index.md @@ -0,0 +1,19 @@ +# Build Index: /api/build + +## Purpose + +This folder contains build system management and compilation scripts. + +## Quick Links + +- [README](./README.md) - Build scripts documentation + +## Scripts + +- [moko-make](./moko-make) - Build system wrapper +- [resolve_makefile.py](./resolve_makefile.py) - Makefile resolution + +## Metadata + +- **Document Type:** index +- **Auto-generated:** This file is manually maintained for ignored directory diff --git a/build/moko-make b/build/moko-make new file mode 100644 index 0000000..ff51dc8 --- /dev/null +++ b/build/moko-make @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +# Copyright (C) 2026 Moko Consulting +# SPDX-License-Identifier: GPL-3.0-or-later +# +# Moko Build Wrapper +# Automatically finds and uses appropriate Makefile from MokoStandards + +set -e + +# Colors +COLOR_RESET="\033[0m" +COLOR_GREEN="\033[32m" +COLOR_BLUE="\033[34m" +COLOR_RED="\033[31m" + +# Find MokoStandards root +find_mokostandards() { + # Check environment variable + if [ -n "$MOKOSTANDARDS_ROOT" ] && [ -d "$MOKOSTANDARDS_ROOT/templates/build" ]; then + echo "$MOKOSTANDARDS_ROOT" + return 0 + fi + + # Check adjacent directories + if [ -d "../MokoStandards/templates/build" ]; then + echo "$(cd ../MokoStandards && pwd)" + return 0 + fi + + if [ -d "../../MokoStandards/templates/build" ]; then + echo "$(cd ../../MokoStandards && pwd)" + return 0 + fi + + # Check home directory + if [ -d "$HOME/.mokostandards/templates/build" ]; then + echo "$HOME/.mokostandards" + return 0 + fi + + # Check system location + if [ -d "/opt/mokostandards/templates/build" ]; then + echo "/opt/mokostandards" + return 0 + fi + + return 1 +} + +# Find appropriate Makefile +find_makefile() { + # Check for local Makefile + if [ -f "Makefile" ]; then + echo "Makefile" + return 0 + fi + + # Check for .moko/Makefile + if [ -f ".moko/Makefile" ]; then + echo ".moko/Makefile" + return 0 + fi + + # Find MokoStandards + MOKO_ROOT=$(find_mokostandards) + if [ $? -ne 0 ]; then + echo -e "${COLOR_RED}✗${COLOR_RESET} MokoStandards repository not found" >&2 + echo -e "${COLOR_BLUE}Hint:${COLOR_RESET} Set MOKOSTANDARDS_ROOT or clone adjacent" >&2 + return 1 + fi + + # Detect project type + if [ -d "core/modules" ] && ls core/modules/mod*.class.php >/dev/null 2>&1; then + echo "$MOKO_ROOT/templates/build/dolibarr/Makefile" + return 0 + fi + + # Check for Joomla XML files + shopt -s nullglob # Prevent glob expansion if no matches + for xml in *.xml; do + if [ -f "$xml" ]; then + if grep -q 'type="component"' "$xml" 2>/dev/null; then + echo "$MOKO_ROOT/templates/build/joomla/Makefile.component" + return 0 + elif grep -q 'type="module"' "$xml" 2>/dev/null; then + echo "$MOKO_ROOT/templates/build/joomla/Makefile.module" + return 0 + elif grep -q 'type="plugin"' "$xml" 2>/dev/null; then + echo "$MOKO_ROOT/templates/build/joomla/Makefile.plugin" + return 0 + fi + fi + done + shopt -u nullglob + + echo -e "${COLOR_RED}✗${COLOR_RESET} Could not detect project type" >&2 + return 1 +} + +# Main execution +MAKEFILE=$(find_makefile) +if [ $? -ne 0 ]; then + exit 1 +fi + +# Show which Makefile we're using +if [[ "$MAKEFILE" == *"MokoStandards"* ]] || [[ "$MAKEFILE" == *".mokostandards"* ]]; then + echo -e "${COLOR_BLUE}ℹ${COLOR_RESET} Using MokoStandards template" +fi + +# Run make with the found Makefile +exec make -f "$MAKEFILE" "$@" diff --git a/cli/archive_repo.php b/cli/archive_repo.php new file mode 100644 index 0000000..80c863b --- /dev/null +++ b/cli/archive_repo.php @@ -0,0 +1,158 @@ +#!/usr/bin/env php + + * + * This file is part of a Moko Consulting project. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * FILE INFORMATION + * DEFGROUP: MokoStandards.CLI + * INGROUP: MokoStandards + * REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API + * PATH: /cli/archive_repo.php + * VERSION: 04.06.10 + * BRIEF: Gracefully retire a governed repository — archive, close issues/PRs, remove sync def + * + * USAGE + * php api/cli/archive_repo.php --repo MokoOldModule + * php api/cli/archive_repo.php --repo MokoOldModule --dry-run + * php api/cli/archive_repo.php --repo MokoOldModule --skip-close # Archive only, keep issues open + */ + +declare(strict_types=1); + +require_once __DIR__ . '/../../vendor/autoload.php'; + +use MokoEnterprise\Config; +use MokoEnterprise\PlatformAdapterFactory; + +$dryRun = in_array('--dry-run', $argv); +$skipClose = in_array('--skip-close', $argv); + +$repoName = null; + +foreach ($argv as $i => $arg) { + if ($arg === '--repo' && isset($argv[$i + 1])) { $repoName = $argv[$i + 1]; } +} + +if (!$repoName) { + fwrite(STDERR, "Usage: php archive_repo.php --repo [--skip-close] [--dry-run]\n"); + exit(2); +} + +$config = Config::load(); +$adapter = PlatformAdapterFactory::create($config); +$org = $config->getString( + $adapter->getPlatformName() . '.organization', + 'mokoconsulting-tech' +); + +$repoRoot = dirname(__DIR__, 2); +$platformName = $adapter->getPlatformName(); + +echo "Archiving repository: {$org}/{$repoName} (on {$platformName})\n\n"; + +// ── Step 1: Verify repo exists ────────────────────────────────────────── +echo "Step 1: Verifying repository...\n"; +try { + $repoData = $adapter->getRepo($org, $repoName); +} catch (\Exception $e) { + fwrite(STDERR, " Repository {$org}/{$repoName} not found: " . $e->getMessage() . "\n"); + exit(1); +} +if ($repoData['archived'] ?? false) { + echo " Already archived — nothing to do\n"; + exit(0); +} +echo " Found: " . ($repoData['html_url'] ?? "{$org}/{$repoName}") . "\n"; + +// ── Step 2: Close all open PRs ────────────────────────────────────────── +if (!$skipClose) { + echo "Step 2: Closing open pull requests...\n"; + $prs = $adapter->listPullRequests($org, $repoName, ['state' => 'open']); + $prCount = count($prs); + echo " Found {$prCount} open PRs\n"; + + foreach ($prs as $pr) { + $num = $pr['number']; + if (!$dryRun) { + $adapter->updatePullRequest($org, $repoName, $num, ['state' => 'closed']); + $adapter->addIssueComment($org, $repoName, $num, + "Closed as part of repository archival. This repository is being retired.\n\n*Auto-closed by `archive_repo.php`*" + ); + } + echo " Closed PR #{$num}: {$pr['title']}\n"; + } + + // ── Step 3: Close all open issues ─────────────────────────────────── + echo "Step 3: Closing open issues...\n"; + $issues = $adapter->listIssues($org, $repoName, ['state' => 'open']); + $issues = array_filter($issues, fn($i) => !isset($i['pull_request'])); + $issueCount = count($issues); + echo " Found {$issueCount} open issues\n"; + + foreach ($issues as $issue) { + $num = $issue['number']; + if (!$dryRun) { + $adapter->closeIssue($org, $repoName, $num); + $adapter->addIssueComment($org, $repoName, $num, + "Closed as part of repository archival.\n\n*Auto-closed by `archive_repo.php`*" + ); + } + echo " Closed issue #{$num}: {$issue['title']}\n"; + } +} else { + echo "Step 2-3: Skipping issue/PR closure (--skip-close)\n"; +} + +// ── Step 4: Archive the repository ────────────────────────────────────── +echo "Step 4: Archiving repository...\n"; +if (!$dryRun) { + try { + $adapter->archiveRepo($org, $repoName); + echo " Repository archived\n"; + } catch (\Exception $e) { + echo " Failed to archive: " . $e->getMessage() . "\n"; + } +} else { + echo " (dry-run) would archive {$org}/{$repoName}\n"; +} + +// ── Step 5: Remove sync definition ────────────────────────────────────── +echo "Step 5: Removing sync definition...\n"; +$defFile = "{$repoRoot}/definitions/sync/{$repoName}.def.tf"; +if (file_exists($defFile)) { + if (!$dryRun) { + unlink($defFile); + echo " Removed: {$defFile}\n"; + } else { + echo " (dry-run) would remove {$defFile}\n"; + } +} else { + echo " No sync definition found\n"; +} + +// ── Step 6: Create archival record ────────────────────────────────────── +echo "Step 6: Creating archival record...\n"; +if (!$dryRun) { + $now = gmdate('Y-m-d H:i:s') . ' UTC'; + try { + $issue = $adapter->createIssue($org, 'MokoStandards', + "chore: archived repository {$repoName}", + "## Repository Archived\n\n**Repository:** `{$org}/{$repoName}`\n**Archived:** {$now}\n**Platform:** {$platformName}\n**Sync definition removed:** yes\n\n---\n*Auto-created by `archive_repo.php`*\n", + [ + 'labels' => ['type: chore', 'automation', 'archived'], + 'assignees' => ['jmiller-moko'], + ] + ); + if (isset($issue['number'])) { echo " Archival record: MokoStandards#{$issue['number']}\n"; } + } catch (\Exception $e) { + echo " Warning: could not create archival record: " . $e->getMessage() . "\n"; + } +} else { + echo " (dry-run) would create archival record issue\n"; +} + +echo "\n" . str_repeat('-', 50) . "\n"; +echo "Repository {$org}/{$repoName} archived successfully\n"; diff --git a/cli/create_project.php b/cli/create_project.php new file mode 100644 index 0000000..4a70a4b --- /dev/null +++ b/cli/create_project.php @@ -0,0 +1,477 @@ +#!/usr/bin/env php + + * + * This file is part of a Moko Consulting project. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * FILE INFORMATION + * DEFGROUP: MokoStandards.CLI + * INGROUP: MokoStandards + * REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API + * PATH: /cli/create_project.php + * VERSION: 04.06.00 + * BRIEF: Create baseline GitHub Projects for repositories with standard fields and views + * + * USAGE + * php api/cli/create_project.php --repo MokoCRM # Auto-detect type, create project + * php api/cli/create_project.php --repo MokoCRM --type dolibarr # Force type + * php api/cli/create_project.php --org mokoconsulting-tech --all # All repos without projects + * php api/cli/create_project.php --repo MokoCRM --dry-run # Preview without changes + */ + +declare(strict_types=1); + +$dryRun = in_array('--dry-run', $argv); +$allMode = in_array('--all', $argv); + +$org = 'mokoconsulting-tech'; +$repoName = null; +$typeOverride = null; + +foreach ($argv as $i => $arg) { + if ($arg === '--repo' && isset($argv[$i + 1])) { + $repoName = $argv[$i + 1]; + } + if ($arg === '--org' && isset($argv[$i + 1])) { + $org = $argv[$i + 1]; + } + if ($arg === '--type' && isset($argv[$i + 1])) { + $typeOverride = $argv[$i + 1]; + } +} + +if (!$repoName && !$allMode) { + fwrite(STDERR, "Usage: php create_project.php --repo [--type ] [--dry-run]\n"); + fwrite(STDERR, " php create_project.php --all [--org ] [--dry-run]\n"); + fwrite(STDERR, "\nTypes: generic, dolibarr, joomla, nodejs, terraform, python, wordpress, mobile-app, api, documentation\n"); + exit(2); +} + +$token = getenv('GH_TOKEN') ?: getenv('GITHUB_TOKEN'); +if (empty($token)) { + fwrite(STDERR, "GH_TOKEN or GITHUB_TOKEN not set\n"); + exit(1); +} + +$repoRoot = dirname(__DIR__, 2); +$templatesDir = "{$repoRoot}/templates/projects"; + +// ── Always-exclude list (no project needed) ───────────────────────────── +$ALWAYS_EXCLUDE = ['MokoStandards', '.github-private']; + +// ── Platform type map ─────────────────────────────────────────────────── +$PLATFORM_TO_TYPE = [ + 'crm-module' => 'dolibarr', + 'crm-platform' => 'dolibarr', + 'waas-component' => 'joomla', + 'waas-library' => 'joomla', + 'waas-plugin' => 'joomla', + 'waas-package' => 'joomla', + 'nodejs' => 'nodejs', + 'terraform' => 'terraform', + 'python' => 'python', + 'wordpress' => 'wordpress', + 'mobile' => 'mobile-app', + 'api' => 'api', + 'documentation' => 'documentation', +]; + +// ── Template file map ─────────────────────────────────────────────────── +$TYPE_TO_TEMPLATE = [ + 'generic' => 'generic-project-definition.tf', + 'dolibarr' => 'dolibarr-project-definition.tf', + 'joomla' => 'joomla-project-definition.tf', + 'nodejs' => 'nodejs-project-definition.tf', + 'terraform' => 'terraform-project-definition.tf', + 'python' => 'python-project-definition.tf', + 'wordpress' => 'wordpress-project-definition.tf', + 'mobile-app' => 'mobile-app-project-definition.tf', + 'api' => 'api-project-definition.tf', + 'documentation' => 'documentation-project-definition.tf', +]; + +/** + * Execute a GitHub GraphQL query. + * + * @return array + */ +function graphql(string $query, array $variables, string $token): array +{ + $payload = json_encode(['query' => $query, 'variables' => $variables]); + $ch = curl_init('https://api.github.com/graphql'); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $payload, + CURLOPT_HTTPHEADER => [ + 'Authorization: bearer ' . $token, + 'Content-Type: application/json', + 'User-Agent: MokoStandards-CreateProject', + ], + ]); + $body = (string) curl_exec($ch); + $status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($status !== 200) { + fwrite(STDERR, "GraphQL request failed (HTTP {$status}): {$body}\n"); + return []; + } + + $data = json_decode($body, true) ?? []; + if (!empty($data['errors'])) { + foreach ($data['errors'] as $err) { + fwrite(STDERR, " GraphQL error: " . ($err['message'] ?? 'unknown') . "\n"); + } + } + + return $data['data'] ?? []; +} + +/** + * Execute a GitHub REST API GET call. + * + * @return array + */ +function restGet(string $path, string $token): array +{ + $ch = curl_init("https://api.github.com/{$path}"); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => [ + 'Authorization: token ' . $token, + 'User-Agent: MokoStandards-CreateProject', + 'Accept: application/vnd.github.v3+json', + ], + ]); + $body = (string) curl_exec($ch); + $status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + return $status === 200 ? (json_decode($body, true) ?? []) : []; +} + +/** + * Detect platform type from .mokostandards file in the repo. + */ +function detectPlatform(string $org, string $repo, string $token): string +{ + // Try .github/.mokostandards first, then root + foreach (['.github/.mokostandards', '.mokostandards'] as $path) { + $data = restGet("repos/{$org}/{$repo}/contents/{$path}", $token); + if (!empty($data['content'])) { + $content = base64_decode($data['content']); + if (preg_match('/^platform:\s*(.+)/m', $content, $m)) { + return trim($m[1], " \t\n\r\"'"); + } + } + } + return ''; +} + +/** + * Get the GitHub node ID for a repository. + */ +function getRepoNodeId(string $org, string $repo, string $token): string +{ + $data = graphql( + 'query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { id } }', + ['owner' => $org, 'name' => $repo], + $token + ); + return $data['repository']['id'] ?? ''; +} + +/** + * Get the GitHub node ID for the organization owner. + */ +function getOrgNodeId(string $org, string $token): string +{ + $data = graphql( + 'query($login: String!) { organization(login: $login) { id } }', + ['login' => $org], + $token + ); + return $data['organization']['id'] ?? ''; +} + +/** + * Check if a repo already has a GitHub Project linked. + * + * @return array{bool, string} [hasProject, projectTitle] + */ +function repoHasProject(string $org, string $repo, string $token): array +{ + $data = graphql( + 'query($owner: String!, $name: String!) { + repository(owner: $owner, name: $name) { + projectsV2(first: 1) { nodes { id title } totalCount } + } + }', + ['owner' => $org, 'name' => $repo], + $token + ); + + $count = $data['repository']['projectsV2']['totalCount'] ?? 0; + $title = $data['repository']['projectsV2']['nodes'][0]['title'] ?? ''; + return [$count > 0, $title]; +} + +/** + * Parse a .tf template file to extract custom fields. + * + * @return array{name: string, fields: array, views: array} + */ +function parseTemplate(string $filePath): array +{ + if (!file_exists($filePath)) { + return ['name' => 'Development Board', 'fields' => [], 'views' => []]; + } + + $content = file_get_contents($filePath); + $result = ['name' => 'Development Board', 'fields' => [], 'views' => []]; + + // Extract project name + if (preg_match('/name\s*=\s*"([^"]+)"/', $content, $m)) { + $result['name'] = $m[1]; + } + + // Extract custom fields + if (preg_match_all('/\{\s*name\s*=\s*"([^"]+)"\s*type\s*=\s*"([^"]+)"\s*description\s*=\s*"([^"]+)"(?:\s*options\s*=\s*\[([^\]]*)\])?\s*\}/s', $content, $matches, PREG_SET_ORDER)) { + foreach ($matches as $match) { + $field = [ + 'name' => $match[1], + 'type' => $match[2], + 'description' => $match[3], + ]; + if (!empty($match[4])) { + $field['options'] = array_map( + fn($o) => trim($o, " \t\n\r\"'"), + explode(',', $match[4]) + ); + $field['options'] = array_filter($field['options']); + } + $result['fields'][] = $field; + } + } + + return $result; +} + +/** + * Create a GitHub Project V2 for a repository. + */ +function createProject( + string $org, + string $repo, + string $ownerId, + string $repoId, + array $template, + string $token, + bool $dryRun +): bool { + $title = "{$repo} — {$template['name']}"; + + if ($dryRun) { + echo " (dry-run) would create project: {$title}\n"; + echo " (dry-run) fields: " . count($template['fields']) . "\n"; + return true; + } + + // Step 1: Create the project + echo " Creating project: {$title}\n"; + $data = graphql( + 'mutation($ownerId: ID!, $title: String!) { + createProjectV2(input: { ownerId: $ownerId, title: $title }) { + projectV2 { id number url } + } + }', + ['ownerId' => $ownerId, 'title' => $title], + $token + ); + + $projectId = $data['createProjectV2']['projectV2']['id'] ?? ''; + $projectUrl = $data['createProjectV2']['projectV2']['url'] ?? ''; + + if (empty($projectId)) { + fwrite(STDERR, " Failed to create project for {$repo}\n"); + return false; + } + + echo " Project created: {$projectUrl}\n"; + + // Step 2: Link the project to the repository + graphql( + 'mutation($projectId: ID!, $repositoryId: ID!) { + linkProjectV2ToRepository(input: { projectId: $projectId, repositoryId: $repositoryId }) { + repository { id } + } + }', + ['projectId' => $projectId, 'repositoryId' => $repoId], + $token + ); + echo " Linked to {$org}/{$repo}\n"; + + // Step 3: Create custom fields + $fieldCount = 0; + foreach ($template['fields'] as $field) { + $fieldType = match ($field['type']) { + 'single_select' => 'SINGLE_SELECT', + 'text' => 'TEXT', + 'number' => 'NUMBER', + 'date' => 'DATE', + 'iteration' => 'ITERATION', + default => 'TEXT', + }; + + $vars = [ + 'projectId' => $projectId, + 'name' => $field['name'], + 'dataType' => $fieldType, + ]; + + // Single select fields need options created with the field + if ($fieldType === 'SINGLE_SELECT' && !empty($field['options'])) { + $optionInputs = array_map( + fn($o) => ['name' => $o, 'description' => '', 'color' => 'GRAY'], + $field['options'] + ); + $vars['singleSelectOptions'] = $optionInputs; + + graphql( + 'mutation($projectId: ID!, $name: String!, $dataType: ProjectV2CustomFieldType!, $singleSelectOptions: [ProjectV2SingleSelectFieldOptionInput!]) { + createProjectV2Field(input: { + projectId: $projectId, + dataType: $dataType, + name: $name, + singleSelectOptions: $singleSelectOptions + }) { + projectV2Field { ... on ProjectV2SingleSelectField { id name } } + } + }', + $vars, + $token + ); + } else { + graphql( + 'mutation($projectId: ID!, $name: String!, $dataType: ProjectV2CustomFieldType!) { + createProjectV2Field(input: { + projectId: $projectId, + dataType: $dataType, + name: $name + }) { + projectV2Field { ... on ProjectV2Field { id name } } + } + }', + $vars, + $token + ); + } + + $fieldCount++; + } + + echo " Created {$fieldCount} custom fields\n"; + + // Step 4: Update project description and README + graphql( + 'mutation($projectId: ID!, $shortDescription: String!) { + updateProjectV2(input: { + projectId: $projectId, + shortDescription: $shortDescription, + readme: "Managed by MokoStandards. Run `php api/cli/create_project.php` to regenerate." + }) { + projectV2 { id } + } + }', + [ + 'projectId' => $projectId, + 'shortDescription' => "Standard project board for {$repo}. Auto-created by MokoStandards.", + ], + $token + ); + + echo " Project setup complete\n"; + return true; +} + +// ── Main ──────────────────────────────────────────────────────────────── + +$repos = []; + +if ($allMode) { + echo "Fetching repositories from {$org}...\n"; + $page = 1; + do { + $batch = restGet("orgs/{$org}/repos?per_page=100&page={$page}&type=all", $token); + foreach ($batch as $r) { + if (!$r['archived'] && !in_array($r['name'], $ALWAYS_EXCLUDE, true)) { + $repos[] = $r['name']; + } + } + $page++; + } while (count($batch) === 100); + + sort($repos); + echo "Found " . count($repos) . " repositories\n\n"; +} else { + $repos = [$repoName]; +} + +$ownerId = getOrgNodeId($org, $token); +if (empty($ownerId)) { + fwrite(STDERR, "Could not resolve org node ID for {$org}\n"); + exit(1); +} + +$created = 0; +$skipped = 0; +$failed = 0; + +foreach ($repos as $repo) { + echo "Processing {$repo}...\n"; + + // Check if project already exists + [$hasProject, $existingTitle] = repoHasProject($org, $repo, $token); + if ($hasProject) { + echo " Already has project: {$existingTitle} — skipping\n"; + $skipped++; + continue; + } + + // Detect project type + $type = $typeOverride; + if (!$type) { + $platform = detectPlatform($org, $repo, $token); + $type = $PLATFORM_TO_TYPE[$platform] ?? 'generic'; + echo " Platform: {$platform} → type: {$type}\n"; + } + + // Load template + $templateFile = $TYPE_TO_TEMPLATE[$type] ?? $TYPE_TO_TEMPLATE['generic']; + $template = parseTemplate("{$templatesDir}/{$templateFile}"); + + // Get repo node ID + $repoId = getRepoNodeId($org, $repo, $token); + if (empty($repoId)) { + fwrite(STDERR, " Could not resolve repo node ID for {$repo}\n"); + $failed++; + continue; + } + + // Create the project + $ok = createProject($org, $repo, $ownerId, $repoId, $template, $token, $dryRun); + if ($ok) { + $created++; + } else { + $failed++; + } + + echo "\n"; +} + +echo str_repeat('-', 50) . "\n"; +echo "Done: {$created} created, {$skipped} skipped, {$failed} failed\n"; +exit($failed > 0 ? 1 : 0); diff --git a/cli/create_repo.php b/cli/create_repo.php new file mode 100644 index 0000000..ca83f09 --- /dev/null +++ b/cli/create_repo.php @@ -0,0 +1,255 @@ +#!/usr/bin/env php + + * + * This file is part of a Moko Consulting project. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * FILE INFORMATION + * DEFGROUP: MokoStandards.CLI + * INGROUP: MokoStandards + * REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API + * PATH: /cli/create_repo.php + * VERSION: 04.06.10 + * BRIEF: Scaffold a new governed repository with full MokoStandards baseline + * + * USAGE + * php api/cli/create_repo.php --name MokoNewModule --type dolibarr --description "My new module" + * php api/cli/create_repo.php --name MokoNewModule --type joomla --private + * php api/cli/create_repo.php --name MokoNewModule --type generic --dry-run + */ + +declare(strict_types=1); + +require_once __DIR__ . '/../../vendor/autoload.php'; + +use MokoEnterprise\Config; +use MokoEnterprise\PlatformAdapterFactory; + +$dryRun = in_array('--dry-run', $argv); +$private = in_array('--private', $argv); + +$name = null; +$type = null; +$description = ''; + +foreach ($argv as $i => $arg) { + if ($arg === '--name' && isset($argv[$i + 1])) { $name = $argv[$i + 1]; } + if ($arg === '--type' && isset($argv[$i + 1])) { $type = $argv[$i + 1]; } + if ($arg === '--description' && isset($argv[$i + 1])) { $description = $argv[$i + 1]; } +} + +if (!$name || !$type) { + fwrite(STDERR, "Usage: php create_repo.php --name --type [--description \"...\"] [--private] [--dry-run]\n"); + fwrite(STDERR, "\nTypes: generic, dolibarr, dolibarr-platform, joomla, nodejs, terraform, python, wordpress\n"); + exit(2); +} + +$config = Config::load(); +$adapter = PlatformAdapterFactory::create($config); +$org = $config->getString( + $adapter->getPlatformName() . '.organization', + 'mokoconsulting-tech' +); + +$repoRoot = dirname(__DIR__, 2); + +$TYPE_TO_PLATFORM = [ + 'dolibarr' => 'crm-module', + 'dolibarr-platform' => 'crm-platform', + 'joomla' => 'waas-component', + 'nodejs' => 'nodejs', + 'terraform' => 'terraform', + 'python' => 'python', + 'wordpress' => 'wordpress', + 'generic' => 'generic', +]; + +$TYPE_TO_TOPICS = [ + 'dolibarr' => ['dolibarr', 'erp', 'crm', 'php', 'mokostandards'], + 'joomla' => ['joomla', 'cms', 'php', 'mokostandards'], + 'nodejs' => ['nodejs', 'javascript', 'typescript', 'mokostandards'], + 'terraform' => ['terraform', 'infrastructure', 'iac', 'mokostandards'], + 'python' => ['python', 'mokostandards'], + 'wordpress' => ['wordpress', 'php', 'cms', 'mokostandards'], + 'generic' => ['mokostandards'], +]; + +$platform = $TYPE_TO_PLATFORM[$type] ?? 'generic'; +$topics = $TYPE_TO_TOPICS[$type] ?? ['mokostandards']; +$platformName = $adapter->getPlatformName(); + +echo "Scaffolding new repository: {$org}/{$name} (on {$platformName})\n"; +echo " Type: {$type} (platform: {$platform})\n"; +echo " Visibility: " . ($private ? 'private' : 'public') . "\n"; +if ($description) { echo " Description: {$description}\n"; } +echo "\n"; + +// ── Step 1: Create the repository ─────────────────────────────────────── +echo "Step 1: Creating repository...\n"; +if (!$dryRun) { + try { + $data = $adapter->createOrgRepo($org, $name, [ + 'description' => $description ?: "Managed by MokoStandards ({$type})", + 'private' => $private, + 'has_issues' => true, + 'has_projects' => true, + 'has_wiki' => false, + 'auto_init' => true, + 'delete_branch_on_merge' => true, + 'allow_squash_merge' => true, + 'allow_merge_commit' => false, + 'allow_rebase_merge' => false, + ]); + echo " Created: " . ($data['html_url'] ?? "{$org}/{$name}") . "\n"; + } catch (\Exception $e) { + if (str_contains($e->getMessage(), '422') || str_contains($e->getMessage(), 'already exists')) { + echo " Repository already exists — continuing with setup\n"; + } else { + fwrite(STDERR, " Failed to create repo: " . $e->getMessage() . "\n"); + exit(1); + } + } +} else { + echo " (dry-run) would create {$org}/{$name}\n"; +} + +// ── Step 2: Set topics ────────────────────────────────────────────────── +echo "Step 2: Setting topics...\n"; +if (!$dryRun) { + $adapter->setRepoTopics($org, $name, $topics); + echo " Topics: " . implode(', ', $topics) . "\n"; +} else { + echo " (dry-run) would set topics: " . implode(', ', $topics) . "\n"; +} + +// ── Step 3: Create .mokostandards file ────────────────────────────────── +echo "Step 3: Creating .github/.mokostandards...\n"; +$mokoContent = "platform: {$platform}\nversion: 04.02.30\nmanaged: true\n"; +if (!$dryRun) { + try { + $adapter->createOrUpdateFile( + $org, $name, '.github/.mokostandards', $mokoContent, + 'chore: add .mokostandards platform config [skip ci]' + ); + echo " .mokostandards created\n"; + } catch (\Exception $e) { + echo " Warning: " . $e->getMessage() . "\n"; + } +} else { + echo " (dry-run) would create .github/.mokostandards\n"; +} + +// ── Step 4: Create initial README.md ──────────────────────────────────── +echo "Step 4: Creating README.md...\n"; + +// Determine the repo base URL based on platform +$baseUrl = $platformName === 'gitea' + ? $config->getString('gitea.url', 'https://git.mokoconsulting.tech') + : 'https://github.com'; +$repoUrl = "{$baseUrl}/{$org}/{$name}"; +$standardsUrl = "{$baseUrl}/{$org}/MokoStandards"; + +$readmeContent = << + +SPDX-License-Identifier: GPL-3.0-or-later + +# FILE INFORMATION +DEFGROUP: {$name} +INGROUP: MokoStandards +REPO: {$repoUrl} +PATH: /README.md +VERSION: 01.00.00 +BRIEF: {$description} +--> + +# {$name} + +[![MokoStandards](https://img.shields.io/badge/MokoStandards-04.06.00-blue)]({$standardsUrl}) +[![Version](https://img.shields.io/badge/version-01.00.00-green)]({$repoUrl}) + +{$description} + +## Getting Started + +This repository is governed by [MokoStandards]({$standardsUrl}). + +## License + +This project is licensed under the GPL-3.0-or-later license. See [LICENSE](LICENSE) for details. + +--- + +*This file is part of the Moko Consulting ecosystem. All rights reserved.* +*This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.* +MD; + +if (!$dryRun) { + // Get existing README sha (auto_init creates one) + $sha = null; + try { + $existing = $adapter->getFileContents($org, $name, 'README.md'); + $sha = $existing['sha'] ?? null; + } catch (\Exception $e) { + $adapter->getApiClient()->resetCircuitBreaker(); + } + + $adapter->createOrUpdateFile( + $org, $name, 'README.md', $readmeContent, + 'docs: initialize README with MokoStandards header [skip ci]', + $sha + ); + echo " README.md created\n"; +} else { + echo " (dry-run) would create README.md\n"; +} + +// ── Step 5: Provision labels ──────────────────────────────────────────── +echo "Step 5: Provisioning labels...\n"; +if (!$dryRun) { + $labelScript = "{$repoRoot}/api/maintenance/setup_labels.php"; + if (file_exists($labelScript)) { + $exitCode = 0; + passthru("php " . escapeshellarg($labelScript) . " --org " . escapeshellarg($org) . " --repo " . escapeshellarg($name), $exitCode); + echo $exitCode === 0 ? " Labels provisioned\n" : " Label provisioning had issues\n"; + } else { + echo " Labels will be provisioned on next sync\n"; + } +} else { + echo " (dry-run) would provision standard labels\n"; +} + +// ── Step 6: Run first sync ────────────────────────────────────────────── +echo "Step 6: Running initial sync...\n"; +if (!$dryRun) { + $syncScript = "{$repoRoot}/api/automation/bulk_sync.php"; + if (file_exists($syncScript)) { + passthru("php " . escapeshellarg($syncScript) . " --repos " . escapeshellarg($name) . " --force --yes"); + } else { + echo " Run manually: php api/automation/bulk_sync.php --repos {$name} --force --yes\n"; + } +} else { + echo " (dry-run) would run initial sync\n"; +} + +// ── Step 7: Create Project ────────────────────────────────────────────── +echo "Step 7: Creating Project...\n"; +if (!$dryRun) { + $projectScript = "{$repoRoot}/api/cli/create_project.php"; + if (file_exists($projectScript)) { + passthru("php " . escapeshellarg($projectScript) . " --repo " . escapeshellarg($name) . " --type " . escapeshellarg($type)); + } else { + echo " Run manually: php api/cli/create_project.php --repo {$name} --type {$type}\n"; + } +} else { + echo " (dry-run) would create Project\n"; +} + +echo "\n" . str_repeat('-', 50) . "\n"; +echo "Repository {$org}/{$name} scaffolded successfully\n"; +echo " URL: {$repoUrl}\n"; +echo " Platform: {$platform} ({$platformName})\n"; +echo " Next: verify the sync and merge any PRs\n"; diff --git a/cli/joomla_release.php b/cli/joomla_release.php new file mode 100644 index 0000000..a2821c6 --- /dev/null +++ b/cli/joomla_release.php @@ -0,0 +1,394 @@ +#!/usr/bin/env php + + * + * This file is part of a Moko Consulting project. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * FILE INFORMATION + * DEFGROUP: MokoStandards.CLI + * INGROUP: MokoStandards + * REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API + * PATH: /cli/joomla_release.php + * VERSION: 04.06.00 + * BRIEF: Joomla release pipeline — build ZIP+tar.gz, upload to GitHub Release, update updates.xml + * + * USAGE + * php api/cli/joomla_release.php --repo MokoCassiopeia --stability stable + * php api/cli/joomla_release.php --repo MokoCassiopeia --stability development + * php api/cli/joomla_release.php --repo MokoCassiopeia --stability rc --dry-run + * php api/cli/joomla_release.php --path /local/repo --stability stable + */ + +declare(strict_types=1); + +require_once __DIR__ . '/../../vendor/autoload.php'; + +use MokoEnterprise\{ApiClient, AuditLogger, CLIApp}; + +class JoomlaRelease extends CLIApp +{ + private const VERSION = '04.06.00'; + private const ORG = 'mokoconsulting-tech'; + + private const STABILITY_TAGS = [ + 'development' => 'development', + 'alpha' => 'alpha', + 'beta' => 'beta', + 'rc' => 'release-candidate', + 'stable' => null, + ]; + + private const SUFFIXES = [ + 'development' => '-dev', + 'alpha' => '-alpha', + 'beta' => '-beta', + 'rc' => '-rc', + 'stable' => '', + ]; + + private ApiClient $api; + private AuditLogger $logger; + + protected function configure(): void + { + $this->setDescription('Joomla release pipeline — build packages, upload, update updates.xml'); + $this->addArgument('--repo', 'Repository name (e.g., MokoCassiopeia)', ''); + $this->addArgument('--path', 'Local repo path (alternative to --repo)', '.'); + $this->addArgument('--stability', 'Stability level: development|alpha|beta|rc|stable', 'stable'); + $this->addArgument('--dry-run', 'Preview without making changes', false); + $this->addArgument('--verbose', 'Show detailed output', false); + } + + protected function run(): int + { + $repo = (string) $this->getArgument('--repo'); + $path = (string) $this->getArgument('--path'); + $stability = (string) $this->getArgument('--stability'); + $dryRun = (bool) $this->getArgument('--dry-run'); + + if (!isset(self::STABILITY_TAGS[$stability])) { + $this->log('ERROR', "Invalid stability: {$stability}. Use: " . implode(', ', array_keys(self::STABILITY_TAGS))); + return 1; + } + + $this->api = new ApiClient(); + $this->logger = new AuditLogger('joomla_release'); + + if ($repo !== '') { + $path = $this->cloneRepo($repo); + if ($path === null) { return 1; } + } + $path = rtrim($path, '/\\'); + + $this->log('INFO', "Joomla Release Pipeline v" . self::VERSION); + $this->log('INFO', "Path: {$path} | Stability: {$stability} | Dry run: " . ($dryRun ? 'yes' : 'no')); + + // ── Step 1: Parse manifest ──────────────────────────────────── + $manifest = $this->findManifest($path); + if ($manifest === null) { + $this->log('ERROR', 'No Joomla XML manifest found'); + return 1; + } + + $meta = $this->parseManifest($manifest); + $this->log('INFO', "Extension: {$meta['name']} ({$meta['type']}) — element: {$meta['element']}"); + + // ── Step 2: Read version ────────────────────────────────────── + $version = $this->readVersion($path) ?? $meta['version']; + if ($version === '') { + $this->log('ERROR', 'No version found in README.md or manifest'); + return 1; + } + + $suffix = self::SUFFIXES[$stability]; + $displayVersion = $version . $suffix; + $major = explode('.', $version)[0]; + $releaseTag = self::STABILITY_TAGS[$stability] ?? "v{$major}"; + + $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); + if ($srcDir === null) { + $this->log('ERROR', 'No src/ or htdocs/ directory'); + return 1; + } + + $zipName = "{$meta['element']}-{$displayVersion}.zip"; + $tarName = "{$meta['element']}-{$displayVersion}.tar.gz"; + $zipPath = sys_get_temp_dir() . "/{$zipName}"; + $tarPath = sys_get_temp_dir() . "/{$tarName}"; + + $sha256 = 'dry-run'; + if (!$dryRun) { + $this->buildZip($srcDir, $zipPath); + $this->buildTarGz($srcDir, $tarPath); + $sha256 = hash_file('sha256', $zipPath); + $this->log('SUCCESS', "ZIP: {$zipName} (" . filesize($zipPath) . " bytes)"); + $this->log('SUCCESS', "tar.gz: {$tarName} (" . filesize($tarPath) . " bytes)"); + $this->log('SUCCESS', "SHA-256: {$sha256}"); + } else { + $this->log('INFO', "[DRY-RUN] Would build: {$zipName} + {$tarName}"); + } + + // ── Step 4: Upload to GitHub Release ────────────────────────── + $repoFullName = self::ORG . '/' . ($repo ?: basename($path)); + + if (!$dryRun) { + $this->ensureRelease($repoFullName, $releaseTag, $displayVersion, $stability); + $this->uploadAsset($repoFullName, $releaseTag, $zipPath, $zipName); + $this->uploadAsset($repoFullName, $releaseTag, $tarPath, $tarName); + $this->log('SUCCESS', "Uploaded to release: {$releaseTag}"); + } else { + $this->log('INFO', "[DRY-RUN] Would upload to {$releaseTag}"); + } + + // ── Step 5: Update updates.xml ──────────────────────────────── + $updatesXml = "{$path}/updates.xml"; + $zipUrl = "https://github.com/{$repoFullName}/releases/download/{$releaseTag}/{$zipName}"; + $tarUrl = "https://github.com/{$repoFullName}/releases/download/{$releaseTag}/{$tarName}"; + + $entry = $this->buildUpdateEntry($meta, $displayVersion, $stability, $zipUrl, $tarUrl, $sha256); + + if (!$dryRun) { + $this->mergeUpdateEntry($updatesXml, $stability, $entry); + $this->log('SUCCESS', "updates.xml updated ({$stability}: {$displayVersion})"); + } else { + $this->log('INFO', "[DRY-RUN] Would update updates.xml"); + } + + echo "\n"; + $this->log('SUCCESS', "Release complete: {$displayVersion} → {$releaseTag}"); + + if (!$dryRun) { + @unlink($zipPath); + @unlink($tarPath); + } + + return 0; + } + + // ── Manifest ───────────────────────────────────────────────────── + + private function findManifest(string $path): ?string + { + foreach ([$path, "{$path}/src", "{$path}/htdocs"] as $dir) { + if (!is_dir($dir)) { continue; } + foreach (glob("{$dir}/*.xml") as $file) { + if (str_contains((string) file_get_contents($file), 'name ?? ''); + $type = (string) ($xml->attributes()->type ?? 'component'); + $element = (string) ($xml->element ?? ''); + $client = (string) ($xml->attributes()->client ?? ''); + $group = (string) ($xml->attributes()->group ?? ''); + $version = (string) ($xml->version ?? ''); + $phpMin = (string) ($xml->php_minimum ?? ''); + + // Templates don't have — derive from + if ($element === '') { + $element = strtolower(str_replace(' ', '', $name)); + } + + $tp = ''; + if (isset($xml->targetplatform)) { + $tpNode = $xml->targetplatform; + $tp = ''; + } + if ($tp === '') { + $tp = ''; + } + + return compact('name', 'type', 'element', 'client', 'group', 'version', 'tp', 'phpMin'); + } + + private function readVersion(string $path): ?string + { + $readme = "{$path}/README.md"; + if (!is_file($readme)) { return null; } + if (preg_match('/VERSION:\s*(\d{2}\.\d{2}\.\d{2})/', file_get_contents($readme), $m)) { + return $m[1]; + } + return null; + } + + // ── Package building ───────────────────────────────────────────── + + private function buildZip(string $srcDir, string $outPath): void + { + $zip = new \ZipArchive(); + $zip->open($outPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE); + $iter = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($srcDir, \FilesystemIterator::SKIP_DOTS), + \RecursiveIteratorIterator::SELF_FIRST + ); + foreach ($iter as $file) { + $local = str_replace('\\', '/', str_replace($srcDir . DIRECTORY_SEPARATOR, '', $file->getPathname())); + if ($this->isExcluded(basename($local))) { continue; } + $file->isDir() ? $zip->addEmptyDir($local) : $zip->addFile($file->getPathname(), $local); + } + $zip->close(); + } + + private function buildTarGz(string $srcDir, string $outPath): void + { + $tarPath = preg_replace('/\.gz$/', '', $outPath); + $phar = new \PharData($tarPath); + $phar->buildFromDirectory($srcDir); + $phar->compress(\Phar::GZ); + @unlink($tarPath); + } + + private function isExcluded(string $name): bool + { + if ($name === '.ftpignore') { return true; } + if (str_starts_with($name, 'sftp-config')) { return true; } + if (str_starts_with($name, '.env')) { return true; } + $ext = pathinfo($name, PATHINFO_EXTENSION); + return in_array($ext, ['ppk', 'pem', 'key'], true); + } + + // ── GitHub Release ─────────────────────────────────────────────── + + private function ensureRelease(string $repo, string $tag, string $version, string $stability): void + { + try { + $this->api->get("/repos/{$repo}/releases/tags/{$tag}"); + } catch (\Exception $e) { + $this->api->post("/repos/{$repo}/releases", [ + 'tag_name' => $tag, + 'name' => ($stability === 'stable') ? "v" . explode('.', $version)[0] . " (latest: {$version})" : "{$tag} ({$version})", + 'body' => "## {$version}\n\nCreated by MokoStandards release pipeline.", + 'prerelease' => ($stability !== 'stable'), + ]); + } + } + + private function uploadAsset(string $repo, string $tag, string $filePath, string $fileName): void + { + $release = $this->api->get("/repos/{$repo}/releases/tags/{$tag}"); + $uploadUrl = str_replace('{?name,label}', '', $release['upload_url']); + + foreach ($release['assets'] ?? [] as $asset) { + if ($asset['name'] === $fileName) { + $this->api->delete("/repos/{$repo}/releases/assets/{$asset['id']}"); + } + } + + $token = getenv('GH_TOKEN') ?: getenv('GITHUB_TOKEN'); + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => "{$uploadUrl}?name=" . urlencode($fileName), + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => file_get_contents($filePath), + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => [ + "Authorization: token {$token}", + 'Content-Type: application/octet-stream', + 'Accept: application/vnd.github+json', + ], + ]); + curl_exec($ch); + curl_close($ch); + } + + // ── updates.xml ────────────────────────────────────────────────── + + private function buildUpdateEntry(array $meta, string $version, string $stability, string $zipUrl, string $tarUrl, string $sha256): string + { + $lines = [' ']; + $lines[] = " {$meta['name']}"; + $lines[] = " {$meta['name']} ({$stability})"; + $lines[] = " {$meta['element']}"; + $lines[] = " {$meta['type']}"; + $lines[] = " {$version}"; + + if ($meta['client'] !== '') { + $lines[] = " {$meta['client']}"; + } elseif (in_array($meta['type'], ['module', 'plugin'])) { + $lines[] = ' site'; + } + if ($meta['group'] !== '' && $meta['type'] === 'plugin') { + $lines[] = " {$meta['group']}"; + } + + $lines[] = ' '; + $lines[] = " {$stability}"; + $lines[] = ' '; + $lines[] = " https://github.com/" . self::ORG . ""; + $lines[] = ' '; + $lines[] = " {$zipUrl}"; + $lines[] = " {$tarUrl}"; + $lines[] = ' '; + + if ($sha256 !== '' && $sha256 !== 'dry-run') { + $lines[] = " sha256:{$sha256}"; + } + + $lines[] = " {$meta['tp']}"; + if ($meta['phpMin'] !== '') { + $lines[] = " {$meta['phpMin']}"; + } + $lines[] = ' Moko Consulting'; + $lines[] = ' https://mokoconsulting.tech'; + $lines[] = ' '; + + return implode("\n", $lines); + } + + private function mergeUpdateEntry(string $xmlPath, string $stability, string $newEntry): void + { + if (!is_file($xmlPath)) { + file_put_contents($xmlPath, "\n\n{$newEntry}\n\n"); + return; + } + + $content = file_get_contents($xmlPath); + $pattern = '#\s*.*?' . preg_quote($stability, '#') . '.*?#s'; + $content = preg_replace($pattern, '', $content); + $content = str_replace('', "{$newEntry}\n", $content); + $content = preg_replace('/\n{3,}/', "\n\n", $content); + file_put_contents($xmlPath, $content); + } + + private function cloneRepo(string $repo): ?string + { + $tmpDir = sys_get_temp_dir() . "/joomla_release_{$repo}"; + if (is_dir($tmpDir)) { + $this->rmdir($tmpDir); + } + $token = getenv('GH_TOKEN') ?: getenv('GITHUB_TOKEN'); + $url = "https://x-access-token:{$token}@github.com/" . self::ORG . "/{$repo}.git"; + $cmd = ['git', 'clone', '--depth', '1', '--quiet', $url, $tmpDir]; + $proc = proc_open($cmd, [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes); + proc_close($proc); + return is_dir($tmpDir) ? $tmpDir : null; + } + + private function rmdir(string $dir): void + { + $iter = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS), + \RecursiveIteratorIterator::CHILD_FIRST + ); + foreach ($iter as $file) { + $file->isDir() ? rmdir($file->getPathname()) : unlink($file->getPathname()); + } + rmdir($dir); + } +} + +$script = new JoomlaRelease('joomla_release', 'Joomla release pipeline'); +exit($script->execute()); diff --git a/cli/platform_detect.php b/cli/platform_detect.php new file mode 100644 index 0000000..0c37d09 --- /dev/null +++ b/cli/platform_detect.php @@ -0,0 +1,41 @@ +#!/usr/bin/env php + + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * FILE INFORMATION + * DEFGROUP: MokoStandards.CLI + * INGROUP: MokoStandards + * REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API + * PATH: /cli/platform_detect.php + * VERSION: 04.06.00 + * BRIEF: Detect platform from .mokostandards file — outputs platform string + */ + +declare(strict_types=1); + +$path = '.'; +foreach ($argv as $i => $arg) { + if ($arg === '--path' && isset($argv[$i + 1])) $path = $argv[$i + 1]; +} + +$root = realpath($path) ?: $path; +// Check .github/.mokostandards first, fallback to root +$file = "{$root}/.github/.mokostandards"; +if (!file_exists($file)) { + $file = "{$root}/.mokostandards"; +} +if (!file_exists($file)) { + echo "unknown\n"; + exit(0); +} + +$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"; +} + +exit(0); diff --git a/cli/release.php b/cli/release.php new file mode 100644 index 0000000..5bc4e5d --- /dev/null +++ b/cli/release.php @@ -0,0 +1,172 @@ +#!/usr/bin/env php + + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * FILE INFORMATION + * DEFGROUP: MokoStandards.CLI + * INGROUP: MokoStandards + * REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API + * PATH: /cli/release.php + * VERSION: 04.06.00 + * BRIEF: Automate the MokoStandards version branch release flow + * + * USAGE + * php api/cli/release.php # Release current version + * php api/cli/release.php --bump minor # Bump minor, then release + * php api/cli/release.php --bump major # Bump major, then release + * php api/cli/release.php --dry-run # Preview without changes + */ + +declare(strict_types=1); + +$dryRun = in_array('--dry-run', $argv); +$bumpType = null; +foreach ($argv as $i => $arg) { + if ($arg === '--bump' && isset($argv[$i + 1])) { + $bumpType = $argv[$i + 1]; // patch | minor | major + } +} + +$repoRoot = dirname(__DIR__, 2); +$syncFile = "{$repoRoot}/api/lib/Enterprise/RepositorySynchronizer.php"; +// Check both workflow directories for the bulk-repo-sync workflow +$bulkSyncFile = file_exists("{$repoRoot}/.gitea/workflows/bulk-repo-sync.yml") + ? "{$repoRoot}/.gitea/workflows/bulk-repo-sync.yml" + : "{$repoRoot}/.github/workflows/bulk-repo-sync.yml"; +$cleanupFile = "{$repoRoot}/templates/workflows/shared/repository-cleanup.yml.template"; + +// ── Step 1: Read current version ──────────────────────────────────────── +$readme = "{$repoRoot}/README.md"; +$content = file_get_contents($readme); +if (!preg_match('/^\s*VERSION:\s*(\d{2})\.(\d{2})\.(\d{2})/m', $content, $m)) { + fwrite(STDERR, "No VERSION found in README.md\n"); + exit(1); +} + +$major = (int)$m[1]; +$minor = (int)$m[2]; +$patch = (int)$m[3]; +$currentVersion = sprintf('%02d.%02d.%02d', $major, $minor, $patch); + +// ── Step 2: Bump version if requested ─────────────────────────────────── +if ($bumpType) { + switch ($bumpType) { + case 'major': $major++; $minor = 0; $patch = 0; break; + case 'minor': $minor++; $patch = 0; break; + case 'patch': $patch++; break; + default: + fwrite(STDERR, "Invalid bump type: {$bumpType} (use patch/minor/major)\n"); + exit(1); + } + $newVersion = sprintf('%02d.%02d.%02d', $major, $minor, $patch); + echo "Bumping: {$currentVersion} → {$newVersion}\n"; + + if (!$dryRun) { + // Update README.md + $content = preg_replace( + '/^(\s*VERSION:\s*)\d{2}\.\d{2}\.\d{2}/m', + '${1}' . $newVersion, + $content, + 1 + ); + file_put_contents($readme, $content); + + // Propagate to all files + echo "Propagating version to all files...\n"; + passthru("php {$repoRoot}/api/maintenance/update_version_from_readme.php --path {$repoRoot}"); + } + $currentVersion = $newVersion; +} else { + echo "Version: {$currentVersion}\n"; +} + +// Derive major.minor for branch naming (patches update existing branch) +$versionParts = explode('.', $currentVersion); +$minorVersion = $versionParts[0] . '.' . $versionParts[1]; +$branch = "version/{$minorVersion}"; + +// ── Step 3: Update STANDARDS_VERSION + STANDARDS_MINOR constants ──────── +echo "Updating STANDARDS_VERSION → {$currentVersion}\n"; +echo "Updating STANDARDS_MINOR → {$minorVersion}\n"; +if (!$dryRun) { + $syncContent = file_get_contents($syncFile); + $syncContent = preg_replace( + "/STANDARDS_VERSION\s*=\s*'[^']+'/", + "STANDARDS_VERSION = '{$currentVersion}'", + $syncContent + ); + $syncContent = preg_replace( + "/STANDARDS_MINOR\s*=\s*'[^']+'/", + "STANDARDS_MINOR = '{$minorVersion}'", + $syncContent + ); + file_put_contents($syncFile, $syncContent); +} + +// ── Step 4: Update bulk-repo-sync.yml checkout ref ────────────────────── +echo "Updating bulk-repo-sync.yml → {$branch}\n"; +if (!$dryRun) { + $bulkContent = file_get_contents($bulkSyncFile); + $bulkContent = preg_replace( + '/ref:\s*version\/[\d.]+/', + "ref: {$branch}", + $bulkContent + ); + file_put_contents($bulkSyncFile, $bulkContent); +} + +// ── Step 5: Update repository-cleanup.yml current branch ──────────────── +echo "Updating repository-cleanup.yml → chore/sync-mokostandards-v{$minorVersion}\n"; +if (!$dryRun) { + $cleanupContent = file_get_contents($cleanupFile); + $cleanupContent = preg_replace( + '/CURRENT="chore\/sync-mokostandards-v[^"]*"/', + "CURRENT=\"chore/sync-mokostandards-v{$minorVersion}\"", + $cleanupContent + ); + file_put_contents($cleanupFile, $cleanupContent); +} + +// ── Step 6: Commit changes ────────────────────────────────────────────── +if (!$dryRun) { + echo "Committing...\n"; + passthru("cd {$repoRoot} && git add -A && git commit -m \"chore(release): prepare {$currentVersion} release [skip ci]\""); + passthru("cd {$repoRoot} && git pull --rebase 2>/dev/null; git push"); +} + +// ── Step 7: Create or update version branch ───────────────────────────── +$isPatch = ($versionParts[2] ?? '00') !== '00'; +if ($isPatch) { + echo "Updating version branch: {$branch} (patch update)\n"; + if (!$dryRun) { + passthru("cd " . escapeshellarg($repoRoot) . " && git push origin main:{$branch} --force 2>&1"); + } +} else { + echo "Creating version branch: {$branch} (minor release)\n"; + if (!$dryRun) { + $exitCode = 0; + passthru("cd " . escapeshellarg($repoRoot) . " && git push origin main:{$branch} 2>&1", $exitCode); + if ($exitCode !== 0) { + echo "Branch {$branch} already exists — force updating\n"; + passthru("cd " . escapeshellarg($repoRoot) . " && git push origin main:{$branch} --force 2>&1"); + } + } +} + +// ── Step 8: Create git tag (never overwrite existing) ─────────────────── +$tag = "v{$currentVersion}"; +echo "Creating tag {$tag}\n"; +if (!$dryRun) { + $exitCode = 0; + passthru("cd {$repoRoot} && git tag {$tag} 2>/dev/null && git push origin {$tag} 2>/dev/null", $exitCode); + if ($exitCode !== 0) { + echo "⚠️ Tag {$tag} already exists — skipping\n"; + } +} + +echo "\n✅ Release {$currentVersion} complete\n"; +echo " Branch: {$branch}\n"; +echo " Tag: {$tag}\n"; +echo " Next: run bulk sync to push to all repos\n"; diff --git a/cli/release_notes.php b/cli/release_notes.php new file mode 100644 index 0000000..a7815a7 --- /dev/null +++ b/cli/release_notes.php @@ -0,0 +1,67 @@ +#!/usr/bin/env php + + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * FILE INFORMATION + * DEFGROUP: MokoStandards.CLI + * INGROUP: MokoStandards + * REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API + * PATH: /cli/release_notes.php + * VERSION: 04.06.00 + * BRIEF: Extract release notes from CHANGELOG.md for a given version + */ + +declare(strict_types=1); + +$path = '.'; +$version = null; +foreach ($argv as $i => $arg) { + if ($arg === '--path' && isset($argv[$i + 1])) $path = $argv[$i + 1]; + if ($arg === '--version' && isset($argv[$i + 1])) $version = $argv[$i + 1]; +} + +if ($version === null) { + // Read from README.md + $readme = realpath($path) . '/README.md'; + if (file_exists($readme)) { + $content = file_get_contents($readme); + if (preg_match('/^\s*VERSION:\s*(\d{2}\.\d{2}\.\d{2})/m', $content, $m)) { + $version = $m[1]; + } + } +} + +if ($version === null) { + fwrite(STDERR, "Usage: release_notes.php --path . --version XX.YY.ZZ\n"); + exit(1); +} + +$changelog = realpath($path) . '/CHANGELOG.md'; +if (!file_exists($changelog)) { + echo "Release {$version}\n"; + exit(0); +} + +$lines = file($changelog, FILE_IGNORE_NEW_LINES); +$notes = []; +$capturing = false; + +foreach ($lines as $line) { + if (preg_match('/^##\s.*' . preg_quote($version, '/') . '/', $line)) { + $capturing = true; + continue; + } + if ($capturing && preg_match('/^## /', $line)) { + break; // Next version heading — stop + } + if ($capturing) { + $notes[] = $line; + } +} + +$result = trim(implode("\n", $notes)); +echo $result ?: "Release {$version}"; +echo "\n"; +exit(0); diff --git a/cli/sync_rulesets.php b/cli/sync_rulesets.php new file mode 100644 index 0000000..d00588e --- /dev/null +++ b/cli/sync_rulesets.php @@ -0,0 +1,178 @@ +#!/usr/bin/env php + + * + * This file is part of a Moko Consulting project. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * FILE INFORMATION + * DEFGROUP: MokoStandards.CLI + * INGROUP: MokoStandards + * REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API + * PATH: /cli/sync_rulesets.php + * VERSION: 04.06.10 + * BRIEF: Apply branch protection rules to all repos via platform adapter + * + * USAGE + * php api/cli/sync_rulesets.php # Apply to all repos + * php api/cli/sync_rulesets.php --repo MokoCRM # Single repo + * php api/cli/sync_rulesets.php --dry-run # Preview only + * php api/cli/sync_rulesets.php --delete # Remove then re-apply + * + * NOTE: On GitHub, this creates rulesets via the rulesets API. + * On Gitea, this creates branch_protections via the branch protection API. + */ + +declare(strict_types=1); + +require_once __DIR__ . '/../../vendor/autoload.php'; + +use MokoEnterprise\Config; +use MokoEnterprise\PlatformAdapterFactory; + +$dryRun = in_array('--dry-run', $argv); +$deleteOld = in_array('--delete', $argv); + +$repoName = null; + +foreach ($argv as $i => $arg) { + if ($arg === '--repo' && isset($argv[$i + 1])) { $repoName = $argv[$i + 1]; } +} + +$config = Config::load(); +$adapter = PlatformAdapterFactory::create($config); +$org = $config->getString( + $adapter->getPlatformName() . '.organization', + 'mokoconsulting-tech' +); + +$platformName = $adapter->getPlatformName(); +$ALWAYS_EXCLUDE = ['MokoStandards', '.github-private']; + +// ── Protection rules (platform-agnostic format) ───────────────────────── +// On GitHub → rulesets API. On Gitea → branch_protections API. +$PROTECTIONS = [ + [ + 'name' => 'MAIN — protect default branch', + 'branch' => 'main', + 'rules' => [ + 'required_reviews' => 1, + 'dismiss_stale' => true, + 'enforce_admins' => true, + 'block_on_rejected' => true, + ], + ], + [ + 'name' => 'VERSION — immutable snapshots', + 'branch' => 'version/*', + 'rules' => [ + 'required_reviews' => 0, + 'enforce_admins' => true, + ], + ], + [ + 'name' => 'DEV — prevent branch deletion', + 'branch' => 'dev/*', + 'rules' => [ + 'required_reviews' => 0, + 'enforce_admins' => true, + ], + ], + [ + 'name' => 'RC — prevent branch deletion', + 'branch' => 'rc/*', + 'rules' => [ + 'required_reviews' => 0, + 'enforce_admins' => true, + ], + ], +]; + +// ── Build repo list ───────────────────────────────────────────────────── +$repos = []; +if ($repoName) { + $repos = [$repoName]; +} else { + echo "Fetching repositories from {$org} ({$platformName})...\n"; + $allRepos = $adapter->listOrgRepos($org, true); // skip archived + foreach ($allRepos as $r) { + if (!in_array($r['name'], $ALWAYS_EXCLUDE, true)) { + $repos[] = $r['name']; + } + } + sort($repos); + echo "Found " . count($repos) . " repositories\n\n"; +} + +$created = 0; +$skipped = 0; +$failed = 0; + +foreach ($repos as $repo) { + echo "Processing {$repo}...\n"; + + // Check existing protections + $existing = $adapter->listBranchProtections($org, $repo); + $existingNames = []; + if (is_array($existing)) { + foreach ($existing as $bp) { + $bpName = $bp['name'] ?? $bp['branch_name'] ?? $bp['rule_name'] ?? ''; + $bpId = $bp['id'] ?? null; + if ($bpName !== '') { + $existingNames[$bpName] = $bpId; + } + } + } + + foreach ($PROTECTIONS as $protection) { + $pName = $protection['name']; + + if ($deleteOld && isset($existingNames[$pName])) { + if (!$dryRun) { + try { + // Platform-specific deletion via raw API + $adapter->getApiClient()->delete( + "/repos/{$org}/{$repo}/" . + ($platformName === 'github' ? 'rulesets' : 'branch_protections') . + "/{$existingNames[$pName]}" + ); + } catch (\Exception $e) { /* ignore delete errors */ } + } + echo " Deleted: {$pName}\n"; + unset($existingNames[$pName]); + } + + if (isset($existingNames[$pName])) { + echo " Exists: {$pName}\n"; + $skipped++; + continue; + } + + if ($dryRun) { + echo " (dry-run) would create: {$pName}\n"; + $created++; + continue; + } + + try { + $adapter->setBranchProtection($org, $repo, $protection['branch'], $protection['rules']); + echo " Created: {$pName}\n"; + $created++; + } catch (\Exception $e) { + $msg = $e->getMessage(); + if (str_contains($msg, '403')) { + echo " Skipped (needs Pro/paid plan): {$pName}\n"; + $skipped++; + } else { + echo " Failed: {$pName} — {$msg}\n"; + $failed++; + } + } + } + echo "\n"; +} + +echo str_repeat('-', 50) . "\n"; +echo "Done: {$created} created, {$skipped} skipped, {$failed} failed\n"; +exit($failed > 0 ? 1 : 0); diff --git a/cli/version_bump.php b/cli/version_bump.php new file mode 100644 index 0000000..124e590 --- /dev/null +++ b/cli/version_bump.php @@ -0,0 +1,63 @@ +#!/usr/bin/env php + + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * FILE INFORMATION + * DEFGROUP: MokoStandards.CLI + * INGROUP: MokoStandards + * REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API + * PATH: /cli/version_bump.php + * VERSION: 04.06.00 + * BRIEF: Auto-increment patch version in README.md — outputs old → new + */ + +declare(strict_types=1); + +$path = '.'; +$type = 'patch'; // patch | minor | major +foreach ($argv as $i => $arg) { + if ($arg === '--path' && isset($argv[$i + 1])) $path = $argv[$i + 1]; + if ($arg === '--minor') $type = 'minor'; + if ($arg === '--major') $type = 'major'; +} + +$readme = realpath($path) . '/README.md'; +if (!file_exists($readme)) { + fwrite(STDERR, "No README.md found at {$path}\n"); + exit(1); +} + +$content = file_get_contents($readme); +if (!preg_match('/^(\s*VERSION:\s*)(\d{2})\.(\d{2})\.(\d{2})/m', $content, $m)) { + fwrite(STDERR, "No VERSION field found in README.md\n"); + exit(1); +} + +$major = (int)$m[2]; +$minor = (int)$m[3]; +$patch = (int)$m[4]; +$old = sprintf('%02d.%02d.%02d', $major, $minor, $patch); + +switch ($type) { + case 'major': $major++; $minor = 0; $patch = 0; break; + case 'minor': $minor++; $patch = 0; break; + default: + $patch++; + if ($patch > 99) { $minor++; $patch = 0; } + if ($minor > 99) { $major++; $minor = 0; } + break; +} + +$new = sprintf('%02d.%02d.%02d', $major, $minor, $patch); +$updated = preg_replace( + '/^(\s*VERSION:\s*)\d{2}\.\d{2}\.\d{2}/m', + '${1}' . $new, + $content, + 1 +); + +file_put_contents($readme, $updated); +echo "{$old} → {$new}\n"; +exit(0); diff --git a/cli/version_read.php b/cli/version_read.php new file mode 100644 index 0000000..e27c5d4 --- /dev/null +++ b/cli/version_read.php @@ -0,0 +1,38 @@ +#!/usr/bin/env php + + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * FILE INFORMATION + * DEFGROUP: MokoStandards.CLI + * INGROUP: MokoStandards + * REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API + * PATH: /cli/version_read.php + * VERSION: 04.06.00 + * BRIEF: Read VERSION from README.md — outputs just the version string + */ + +declare(strict_types=1); + +$path = '.'; +foreach ($argv as $i => $arg) { + if ($arg === '--path' && isset($argv[$i + 1])) { + $path = $argv[$i + 1]; + } +} + +$readme = realpath($path) . '/README.md'; +if (!file_exists($readme)) { + fwrite(STDERR, "No README.md found at {$path}\n"); + exit(1); +} + +$content = file_get_contents($readme); +if (preg_match('/^\s*VERSION:\s*(\d{2}\.\d{2}\.\d{2})/m', $content, $m)) { + echo $m[1] . "\n"; + exit(0); +} + +fwrite(STDERR, "No VERSION field found in README.md\n"); +exit(1); diff --git a/cli/version_set_platform.php b/cli/version_set_platform.php new file mode 100644 index 0000000..de6e4be --- /dev/null +++ b/cli/version_set_platform.php @@ -0,0 +1,119 @@ +#!/usr/bin/env php + + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * FILE INFORMATION + * DEFGROUP: MokoStandards.CLI + * INGROUP: MokoStandards + * REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API + * PATH: /cli/version_set_platform.php + * VERSION: 04.06.00 + * BRIEF: Set version in platform-specific files (Dolibarr $this->version, Joomla ) + */ + +declare(strict_types=1); + +$path = '.'; +$version = null; +$branch = null; +foreach ($argv as $i => $arg) { + if ($arg === '--path' && isset($argv[$i + 1])) $path = $argv[$i + 1]; + if ($arg === '--version' && isset($argv[$i + 1])) $version = $argv[$i + 1]; + if ($arg === '--branch' && isset($argv[$i + 1])) $branch = $argv[$i + 1]; +} + +// Auto-detect branch from git or GitHub env +if ($branch === null) { + $branch = trim((string) @shell_exec('git rev-parse --abbrev-ref HEAD 2>/dev/null')); + if (empty($branch) || $branch === 'HEAD') { + $branch = getenv('GITHUB_REF_NAME') ?: 'main'; + } +} + +if ($version === null) { + fwrite(STDERR, "Usage: version_set_platform.php --path . --version development\n"); + exit(1); +} + +$root = realpath($path) ?: $path; + +// Detect platform +$platform = ''; +$mokoStandards = "{$root}/.github/.mokostandards"; +if (!file_exists($mokoStandards)) { + $mokoStandards = "{$root}/.mokostandards"; +} +if (file_exists($mokoStandards)) { + $content = file_get_contents($mokoStandards); + if (preg_match('/^platform:\s*(.+)/m', $content, $m)) { + $platform = trim($m[1], " \t\n\r\"'"); + } +} + +$changed = 0; + +// Dolibarr: $this->version + $this->url_last_version in mod*.class.php +if ($platform === 'crm-module') { + $pattern = "{$root}/src/core/modules/mod*.class.php"; + foreach (glob($pattern) ?: [] as $file) { + $content = file_get_contents($file); + + // Set $this->version + $updated = preg_replace( + '/(\$this->version\s*=\s*)[\'"][^\'"]*[\'"]/', + "\${1}'{$version}'", + $content + ); + + // Rewrite $this->url_last_version to point to current branch + if (preg_match('/\$this->url_last_version\s*=\s*[\'"]([^\'"]+)[\'"]/', $updated, $urlMatch)) { + $oldUrl = $urlMatch[1]; + // Replace the branch segment: .../BRANCH/update.txt + $newUrl = preg_replace( + '#(raw\.githubusercontent\.com/[^/]+/[^/]+/)[^/]+(/update\.json)#', + "\${1}{$branch}\${2}", + $oldUrl + ); + if ($newUrl !== $oldUrl) { + $updated = str_replace($oldUrl, $newUrl, $updated); + echo "Dolibarr: url_last_version → {$branch}/update.txt\n"; + } + } + + if ($updated !== $content) { + file_put_contents($file, $updated); + echo "Dolibarr: " . basename($file) . " → version={$version}, branch={$branch}\n"; + $changed++; + } + } +} + +// Joomla: in XML manifests +if ($platform === 'waas-component') { + foreach (glob("{$root}/src/*.xml") ?: glob("{$root}/*.xml") ?: [] as $file) { + $content = file_get_contents($file); + if (!str_contains($content, '[^<]*|', + "{$version}", + $content + ); + if ($updated !== $content) { + file_put_contents($file, $updated); + echo "Joomla: " . basename($file) . " → {$version}\n"; + $changed++; + } + } +} + +if ($changed === 0) { + if (empty($platform)) { + echo "No .mokostandards file — skipping platform version set\n"; + } else { + echo "No platform-specific version files found for {$platform}\n"; + } +} + +exit(0); diff --git a/deploy/deploy-joomla.php b/deploy/deploy-joomla.php index 1f3b6b2..c2a81f8 100644 --- a/deploy/deploy-joomla.php +++ b/deploy/deploy-joomla.php @@ -9,8 +9,8 @@ * FILE INFORMATION * DEFGROUP: MokoStandards.Scripts.Deploy * INGROUP: MokoStandards - * REPO: https://github.com/mokoconsulting-tech/MokoStandards - * PATH: /api/deploy/deploy-joomla.php + * REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API + * PATH: /deploy/deploy-joomla.php * VERSION: 04.06.00 * BRIEF: Smart Joomla deploy — routes files to correct Joomla directories based on XML manifest * diff --git a/deploy/deploy-sftp.php b/deploy/deploy-sftp.php index 54aff34..379e5c3 100644 --- a/deploy/deploy-sftp.php +++ b/deploy/deploy-sftp.php @@ -9,8 +9,8 @@ * FILE INFORMATION * DEFGROUP: MokoStandards.Scripts.Deploy * INGROUP: MokoStandards - * REPO: https://github.com/mokoconsulting-tech/MokoStandards - * PATH: /api/deploy/deploy-sftp.php + * REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API + * PATH: /deploy/deploy-sftp.php * VERSION: 04.06.00 * BRIEF: Deploy a repository src/ directory to a remote web server via SFTP */ diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 449c5f7..a9048ea 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -204,7 +204,7 @@ All files must include: # FILE INFORMATION # DEFGROUP: [Group] # INGROUP: [Parent Group] -# REPO: https://github.com/mokoconsulting-tech/MokoStandards +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API # PATH: [Relative path from repo root] # VERSION: [X.Y.Z] # Version is dynamically read from README.md title line # BRIEF: [One-line description] diff --git a/docs/api/analysis/index.md b/docs/api/analysis/index.md new file mode 100644 index 0000000..4bc444c --- /dev/null +++ b/docs/api/analysis/index.md @@ -0,0 +1,23 @@ +# Analysis Tools + +## Overview + +This directory contains documentation for analysis tools in `/api/analysis/`. + +Analysis tools provide code quality, dependency analysis, and other analytical capabilities. + +## Contents + +_To be documented_ + +## Related Documentation + +- [API Overview](../index.md) +- [Validation Tools](../validate/index.md) + +--- + +**Location**: `docs/api/analysis/` +**Mirrors**: `/api/analysis/` +**Last Updated**: 2026-03-03 +**Maintained By**: MokoStandards Team diff --git a/docs/api/automation/index.md b/docs/api/automation/index.md new file mode 100644 index 0000000..08ee2e5 --- /dev/null +++ b/docs/api/automation/index.md @@ -0,0 +1,138 @@ + + +# Automation Scripts + +Scripts in `api/automation/` orchestrate large-scale operations across multiple +repositories. They require a GitHub PAT (`GH_TOKEN`) with appropriate scopes. + +--- + +## bulk_sync.php + +Enterprise-grade bulk synchronization. Reads repository definitions from +`api/definitions/sync/`, applies template files to governed repositories, +and opens Pull Requests with the changes. + +**Base class:** `CLIApp` + +```bash +# Dry-run sync for the entire org +php api/automation/bulk_sync.php --org mokoconsulting-tech --dry-run + +# Sync specific repositories +php api/automation/bulk_sync.php --org mokoconsulting-tech --repos "repo-a,repo-b" + +# Exclude repos and skip archived +php api/automation/bulk_sync.php --org mokoconsulting-tech --exclude "legacy-repo" --skip-archived + +# Auto-approve PRs (non-interactive) +php api/automation/bulk_sync.php --org mokoconsulting-tech --yes +``` + +| Option | Description | +|--------|-------------| +| `--org ` | GitHub organization to sync | +| `--repos ` | Comma-separated list of specific repositories | +| `--exclude ` | Repositories to skip | +| `--skip-archived` | Skip archived repositories | +| `--yes` | Approve all PRs without prompting | +| `--dry-run` | Preview changes without creating PRs | +| `--verbose` / `-v` | Verbose output | +| `--help` / `-h` | Show help and exit | + +**Related:** See [Bulk Repo Sync](../../bulk-repo-sync-override-files.md) for +override file conventions. + +--- + +## push_files.php + +Targeted file push tool. Pushes one or more specific files from MokoStandards +templates to remote repositories without running a full sync. Supports both +definition-based lookup and arbitrary `source:destination` pairs. + +```bash +# Push a single file to one repo +php api/automation/push_files.php --files=.github/ISSUE_TEMPLATE/config.yml --repos=MokoCRM + +# Push multiple files to multiple repos +php api/automation/push_files.php --files=".github/workflows/ci.yml,.github/workflows/codeql-analysis.yml" --repos=MokoCRM,WaasComponent + +# Push directly to a branch (no PR) +php api/automation/push_files.php --files=templates/foo.txt:docs/foo.txt --repos=MyRepo --direct --branch=main +``` + +| Option | Description | +|--------|-------------| +| `--org ` | GitHub organization (default: mokoconsulting-tech) | +| `--repos ` | Target repositories — comma-separated (required) | +| `--files ` | Files to push — destination paths or `source:dest` pairs (required) | +| `--message ` | Custom commit message | +| `--branch ` | Target branch for `--direct` pushes | +| `--direct` | Push directly instead of creating a PR | +| `--yes` | Auto-confirm without prompting | +| `--no-issue` | Skip creating a tracking issue | + +--- + +## repo_cleanup.php + +Enterprise repository cleanup. Comprehensive maintenance for governed repos: +delete stale sync branches, close superseded PRs, close resolved tracking +issues, delete retired workflows, clean workflow logs, verify labels, and +detect version drift. + +```bash +# Preview all cleanup operations +php api/automation/repo_cleanup.php --all --dry-run + +# Run all operations on specific repos +php api/automation/repo_cleanup.php --repos "MokoCRM MokoWaas" --all --yes + +# Delete retired workflow files only +php api/automation/repo_cleanup.php --delete-retired --yes + +# Close resolved issues and clean old logs +php api/automation/repo_cleanup.php --close-issues --clean-logs --log-days=14 --yes + +# Check version drift across all repos +php api/automation/repo_cleanup.php --check-drift --json +``` + +| Option | Description | +|--------|-------------| +| `--org ` | GitHub organization | +| `--repos ` | Specific repositories (space-separated) | +| `--skip-archived` | Skip archived repositories | +| `--close-issues` | Close resolved tracking issues | +| `--lock-old-issues` | Lock issues closed >30 days | +| `--clean-workflows` | Delete cancelled/stale workflow runs | +| `--clean-logs` | Delete workflow run logs older than `--log-days` | +| `--log-days ` | Days to keep logs (default: 30) | +| `--delete-retired` | Delete retired workflow files from repos | +| `--check-labels` | Verify mokostandards label exists | +| `--check-drift` | Check for version drift against README.md | +| `--all` | Run all cleanup operations | +| `--yes` | Auto-confirm prompts | +| `--dry-run` | Preview changes without making them | +| `--json` | Output results as JSON | + +--- + +**Location:** `docs/api/automation/` +**Mirrors:** `api/automation/` +**Last Updated:** 2026-04-04 diff --git a/docs/api/definitions/default/index.md b/docs/api/definitions/default/index.md new file mode 100644 index 0000000..7725187 --- /dev/null +++ b/docs/api/definitions/default/index.md @@ -0,0 +1,212 @@ +# Default Repository Definitions + +## Overview + +This directory contains base platform-specific definition files that serve as templates for repository validation and structure. These definitions are used as the foundation for generating repository-specific definitions during bulk sync operations. + +## Definition Files + +| File | Platform | Description | +|------|----------|-------------| +| **crm-module.tf** | Dolibarr | Structure for Dolibarr/CRM modules with core/modules structure, language files, SQL tables | +| **default-repository.json** | Generic | JSON format of default repository structure (legacy format) | +| **default-repository.tf** | Generic | Standard structure for generic repositories, libraries, and multi-language projects | +| **generic-repository.tf** | Generic | Alternative generic repository structure with minimal requirements | +| **standards-repository.tf** | Standards | Structure for MokoStandards organizational repository with api/, templates/, docs/, logs/ | +| **waas-component.tf** | Joomla | Structure for Joomla/WaaS components, modules, plugins with manifest validation | + +## File Format + +All definition files use Terraform HCL (HashiCorp Configuration Language) format with `.tf` extension: + +```hcl +locals { + repository_structure = { + metadata = { + name = "Repository Type Name" + description = "Detailed description" + repository_type = "type-identifier" + platform = "platform-name" + last_updated = "ISO8601-timestamp" + maintainer = "Maintainer Name" + version = "1.0" + schema_version = "1.0" + } + + root_files = [ + { + name = "filename.ext" + extension = "ext" + description = "File description" + required = true + audience = "general" + } + ] + + directories = [ + { + name = "dirname" + path = "path/to/dir" + description = "Directory description" + required = true + purpose = "Purpose description" + + files = [ /* nested files */ ] + subdirectories = [ /* nested directories */ ] + } + ] + + repository_requirements = { + secrets = [ /* required secrets */ ] + variables = [ /* required variables */ ] + branch_protections = { /* protection rules */ } + repository_settings = { /* settings */ } + } + } +} +``` + +## Usage + +### Platform Detection + +These definitions are automatically selected during platform detection: + +```bash +# Auto-detect platform and load appropriate definition +php api/validate/auto_detect_platform.php \ + --repo-path /path/to/repository \ + --schema-dir api/definitions/default +``` + +The detection script will: +1. Analyze repository structure and content +2. Score each platform based on indicators +3. Select the best-matching definition +4. Validate repository against the definition + +### Manual Selection + +You can manually specify which definition to use: + +```bash +# Use specific definition +php api/validate/auto_detect_platform.php \ + --repo-path /path/to/repository \ + --platform dolibarr +``` + +This will load `api/definitions/default/crm-module.tf`. + +### During Bulk Sync + +When bulk sync runs, it: +1. Detects the repository platform +2. Loads the corresponding base definition from this directory +3. Customizes it with repository-specific metadata +4. Saves to `api/definitions/sync/{repo}.def.tf` + +## Platform Mapping + +The auto-detection script maps platforms to definition files: + +| Platform | Definition File | +|----------|----------------| +| `joomla` | waas-component.tf | +| `dolibarr` | crm-module.tf | +| `nodejs` | nodejs-repository.tf (future) | +| `python` | python-repository.tf (future) | +| `terraform` | terraform-repository.tf (future) | +| `standards` | standards-repository.tf | +| `generic` | default-repository.tf | + +## Creating New Definitions + +To create a new platform definition: + +1. **Copy an existing definition as template:** + ```bash + cp api/definitions/default/default-repository.tf \ + api/definitions/default/myplatform-repository.tf + ``` + +2. **Update metadata:** + - Change `name`, `description`, `repository_type`, `platform` + - Update `last_updated` timestamp + - Set appropriate `version` and `schema_version` + +3. **Define structure:** + - Update `root_files` array with platform-specific files + - Define `directories` with nested structure + - Add platform-specific validation rules + - Specify repository requirements (secrets, variables, etc.) + +4. **Update platform detection:** + Add mapping in `api/validate/auto_detect_platform.php`: + ```php + 'myplatform' => 'myplatform-repository.tf', + ``` + +5. **Add detection logic:** + Update detection scoring to recognize your platform based on: + - File patterns (e.g., `composer.json`, `package.json`) + - Directory structure + - Repository topics + - Naming conventions + +6. **Validate syntax:** + ```bash + # Using Terraform (if installed) + terraform fmt -check api/definitions/default/myplatform-repository.tf + ``` + +7. **Test definition:** + ```bash + php api/validate/auto_detect_platform.php \ + --repo-path /test/repository \ + --platform myplatform + ``` + +## Validation Levels + +Definitions support multiple requirement levels: + +| Level | Meaning | Impact | +|-------|---------|--------| +| **required** | MUST be present | Blocks deployment if missing | +| **suggested** | SHOULD be present | Warning if missing, reduces health score | +| **optional** | MAY be present | No validation, informational only | +| **not-allowed** | MUST NOT be present | Error if present (e.g., node_modules) | + +## Maintenance + +### Updating Definitions + +1. Edit the definition file +2. Update `last_updated` timestamp +3. Increment `version` if breaking changes +4. Test with validation script +5. Update CHANGELOG.md +6. Commit changes + +### Version Control + +- Definition files are versioned through git +- Breaking changes require major version bump +- Schema version tracked in definition metadata +- Maintain backward compatibility when possible + +## Related Documentation + +- [Synced Definitions](../sync/index.md) - Auto-generated repository definitions +- [Schema Guide](../../../docs/schemas/repohealth/schema-guide.md) - Complete schema specification +- [Validation Guide](../../../docs/guide/validation/auto-detection.md) - Platform detection and validation +- [Main Definitions README](../README.md) - Overview of definitions structure + +--- + +**Location**: `api/definitions/default/` +**Purpose**: Base platform-specific repository definitions +**Used By**: Auto-detection, validation, bulk sync +**Last Updated**: 2026-03-03 +**Maintained By**: MokoStandards Team diff --git a/docs/api/definitions/sync/index.md b/docs/api/definitions/sync/index.md new file mode 100644 index 0000000..6e69dab --- /dev/null +++ b/docs/api/definitions/sync/index.md @@ -0,0 +1,224 @@ +# Synced Repository Definitions + +## Overview + +This directory contains auto-generated repository structure definitions created during bulk synchronization operations. Each synced repository gets its own definition file that captures its detected platform, structure, and configuration. + +## Purpose + +When the bulk sync process runs, it: + +1. **Detects the repository platform** (Joomla, Dolibarr, Node.js, etc.) +2. **Generates a repository-specific definition** based on the detected platform +3. **Saves the definition** in this directory as `{repo}.def.tf` +4. **Uses the definition** for validation and health checks + +This replaces the previous approach of creating `.github/override.tf` files in remote repositories. + +## File Naming Convention + +Files are named using the pattern: `{repository}.def.tf` + +The `.def.tf` extension follows Terraform conventions where: +- `.tf` indicates Terraform HCL format +- `.def` indicates this is a definition/configuration file +- Follows Terraform protocol standards for module and configuration naming + +**Examples:** +- `MokoDoliCGAdClaude.def.tf` - Dolibarr CRM module +- `joomla-component-example.def.tf` - Joomla component +- `nodejs-api.def.tf` - Node.js API project + +## File Format + +Each definition file uses Terraform HCL format (`.tf` extension) with the same structure as files in `api/definitions/default/`: + +```hcl +/** + * Repository Definition: {org}/{repo} + * Auto-generated during bulk sync on {date} + * Platform: {platform} + * Repository Type: {type} + */ + +locals { + repository_structure = { + metadata = { + name = "{Repository Name}" + description = "Repository structure for {org}/{repo}" + repository_type = "{type}" + platform = "{platform}" + last_updated = "{ISO8601 timestamp}" + maintainer = "{org}" + version = "1.0" + schema_version = "1.0" + + # Sync metadata + sync_generated = true + sync_date = "{ISO8601 timestamp}" + source_repo = "{org}/{repo}" + detected_platform = "{platform}" + } + + # Root files, directories, etc. inherited from platform definition + # with repository-specific customizations + ... + } +} +``` + +## Generation Process + +During bulk sync: + +1. **Platform Detection**: Auto-detect repository platform using `auto_detect_platform.php` +2. **Load Base Definition**: Load the appropriate base definition from `api/definitions/default/` +3. **Customize Metadata**: Add repository-specific metadata (org, repo name, sync date) +4. **Save Definition**: Write to `api/definitions/sync/{org}-{repo}.tf` +5. **Skip Remote Override**: Do NOT create `.github/override.tf` in the remote repository + +## Benefits + +### Centralized Management +- All repository definitions stored in MokoStandards repository +- Easy to track changes and history through git +- No scattered override files across multiple repositories + +### Audit Trail +- Complete history of when repositories were synced +- Platform detection results preserved +- Changes to repository structure tracked in version control + +### Validation & Health Checks +- Health check scripts can reference these definitions +- Validation scripts can compare actual vs. expected structure +- Automated compliance monitoring across all repositories + +### Clean Remote Repositories +- No `.github/override.tf` files cluttering remote repos +- Remote repositories only receive templates and workflows +- Cleaner git history in remote repositories + +## Usage + +### Validation Scripts + +Validation scripts automatically check both locations: + +```bash +# Auto-detect platform and validate using synced definition if available +php api/validate/auto_detect_platform.php \ + --repo-path /path/to/repository \ + --repo repository-name +``` + +The script will: +1. Check for synced definition in `api/definitions/sync/{repo}.def.tf` +2. Fall back to platform-based definition in `api/definitions/default/` if not found +3. Validate repository structure against the definition + +### Health Checks + +```bash +# Run health check using synced definition +php api/validate/check_repo_health.php \ + --repo-path /path/to/repository \ + --repo repository-name +``` + +### Manual Review + +To review a synced repository's definition: + +```bash +# View the definition +cat api/definitions/sync/MyRepo.def.tf + +# Compare with base definition +diff api/definitions/default/default-repository.tf \ + api/definitions/sync/MyRepo.def.tf +``` + +## Maintenance + +### Cleaning Stale Definitions + +Periodically review and remove definitions for repositories that: +- Have been deleted +- Have been archived +- Are no longer being synced + +```bash +# Find definitions for archived repositories +php api/maintenance/clean_stale_definitions.php --dry-run +``` + +### Updating Definitions + +Definitions are automatically regenerated on each bulk sync. To manually regenerate: + +```bash +# Regenerate definition for a specific repository +php api/automation/bulk_sync.php \ + --repos mokoconsulting-tech/MyRepo \ + --regenerate-definitions +``` + +## Git Tracking + +All files in this directory are **tracked in git** for complete audit trail and version control: + +``` +api/definitions/sync/ +├── .gitignore # Git configuration +├── README.md # This documentation +└── *.def.tf # All synced repository definitions (TRACKED) +``` + +This approach: +- ✅ Preserves complete history of all synced repositories +- ✅ Provides audit trail of definition changes over time +- ✅ Enables diff/review of repository structure changes +- ✅ Allows rollback to previous definition versions +- ✅ Documents which repositories are actively synced + +## Migration Notes + +### From Previous Approach + +Previously, bulk sync created `.github/override.tf` files in each remote repository. This has been replaced with: + +**Old Approach:** +``` +Remote Repo: .github/override.tf (created by bulk sync) +MokoStandards: templates/github/override.tf.template +``` + +**New Approach:** +``` +Remote Repo: (no override file) +MokoStandards: api/definitions/sync/{repo}.def.tf (auto-generated) +``` + +### Existing Override Files + +For repositories that already have `.github/override.tf` files: +- They will continue to work (not deleted) +- New syncs will not update them +- Health checks will prefer synced definitions in MokoStandards +- Manual cleanup of old override files can be done separately + +## Related Documentation + +- [Default Definitions](../default/README.md) - Base platform definitions +- [Schema Guide](../../../docs/schemas/repohealth/schema-guide.md) - Definition schema specification +- [Bulk Sync Documentation](../../../docs/automation/bulk-repo-sync.md) - Bulk sync process +- [Validation Guide](../../../docs/guide/validation/auto-detection.md) - Platform detection and validation + +--- + +**Location**: `api/definitions/sync/` +**Purpose**: Auto-generated synced repository definitions +**Generated By**: Bulk sync automation +**Last Updated**: 2026-03-03 +**Maintained By**: MokoStandards automation (do not edit manually) diff --git a/docs/api/deploy/index.md b/docs/api/deploy/index.md new file mode 100644 index 0000000..50c26c8 --- /dev/null +++ b/docs/api/deploy/index.md @@ -0,0 +1,162 @@ + + +# Deploy Scripts + +Scripts in `api/deploy/` upload repository source files to remote web servers via SFTP. + +--- + +## deploy-sftp.php + +**Path:** `api/deploy/deploy-sftp.php` +**Base class:** `MokoEnterprise\CliFramework` + +Reads connection details from a `sftp-config.json` file and recursively uploads +a repository's `src/` directory to the configured remote path. +Supports PuTTY `.ppk` keys and OpenSSH PEM keys via phpseclib. Strips `//` +line comments from the config file so the Sublime Text SFTP plugin format works +without modification. + +### Usage + +```bash +php api/deploy/deploy-sftp.php [OPTIONS] +``` + +| Option | Default | Description | +|--------|---------|-------------| +| `--path ` | `.` | Repository root to deploy | +| `--src-dir ` | `src` | Sub-directory inside the repo to upload | +| `--env ` | — | Target environment; selects named config file (see below) | +| `--config ` | — | Explicit config path — overrides `--env` and auto-lookup | +| `--key-passphrase ` | _(none)_ | Passphrase for encrypted SSH key | +| `--dry-run` | off | Preview uploads without connecting | +| `--verbose` / `-v` | off | Show per-file transfer details | +| `--quiet` / `-q` | off | Suppress all output except errors | +| `--help` / `-h` | — | Show help and exit | + +### Config File Resolution + +`--env` controls which config file is loaded from `{path}/scripts/sftp-config/`: + +| `--env` | Config file | +|---------|-------------| +| `dev` | `scripts/sftp-config/sftp-config.dev.json` | +| `demo` | `scripts/sftp-config/sftp-config.demo.json` | +| _(none)_ | `scripts/sftp-config/sftp-config.json` (generic fallback) | + +> **Note:** The `rs` environment has been retired. RS deployment is via the release pipeline only. + +`--config ` always takes precedence over `--env`. + +### Directory Layout + +Both directories are **gitignored** — create them locally and never commit their contents: + +``` +{repo_root}/ + scripts/ + sftp-config/ ← gitignored; copy templates from templates/scripts/deploy/ + sftp-config.dev.json ← copy of sftp-config.dev.json.example, filled in + sftp-config.demo.json ← copy of sftp-config.demo.json.example, filled in + keys/ ← gitignored; place your .ppk / PEM key file here +``` + +> **IMPORTANT:** `sftp-config.json` files are for local development only. In CI/CD, all credentials must come from GitHub variables and secrets. Never commit sftp-config files. + +See `templates/scripts/sftp-config/README.md` for step-by-step setup instructions. + +### Key Resolution + +`ssh_key_file` in `sftp-config.json` may be an absolute path or a bare filename. +When not absolute, the script looks for the key under `{path}/scripts/keys/` first, +then falls back to the value as a path relative to CWD. + +### Examples + +```bash +# Preview what would be uploaded (no connection) +php api/deploy/deploy-sftp.php --env dev --dry-run --verbose + +# Deploy src/ to dev server +php api/deploy/deploy-sftp.php --path /repos/mymodule --env dev + +# Deploy src/ to demo server +php api/deploy/deploy-sftp.php --path /repos/mymodule --env demo + +# Use a different source directory +php api/deploy/deploy-sftp.php --path /repos/mymodule --env dev --src-dir htdocs + +# Deploy with explicit config and encrypted key +php api/deploy/deploy-sftp.php \ + --path /repos/mymodule \ + --config /repos/mymodule/scripts/sftp-config/sftp-config.demo.json \ + --key-passphrase "my passphrase" +``` + +### Config Format + +Copy a template from `templates/scripts/deploy/` to `scripts/sftp-config/` and fill in your values: + +```json +{ + "type": "sftp", + "host": "iad1-shared-b7-01.dreamhost.com", + "user": "mokoconsulting_dev", + "ssh_key_file": "jmiller_private.ppk", + "port": "22", + "remote_path": "/home/mokoconsulting_dev/crm.dev.mokoconsulting.tech/htdocs/custom/mymodule/", + "ignore_regexes": [ + "\\.git*", + "sftp-config(-alt\\d?)?\\.json", + "\\.DS_Store", + "Thumbs\\.db" + ] +} +``` + +`ssh_key_file` may be a bare filename (resolved from `scripts/keys/`) or an +absolute path (e.g. `J:/My Drive/Keys/jmiller_private.ppk`). + +### Exit Codes + +| Code | Meaning | +|------|---------| +| `0` | All files uploaded successfully | +| `1` | Connection failed or one or more files could not be uploaded | +| `2` | Invalid arguments or config file error | + +### Called by Workflows + +| Workflow | Trigger | Target | Secrets prefix | +|----------|---------|--------|----------------| +| `deploy-dev.yml` | `workflow_call`, `workflow_dispatch` | Dev server | `DEV_FTP_` | +| `deploy-demo.yml` | `workflow_call`, `workflow_dispatch` | Demo server | `DEMO_FTP_` | + +> **Note:** The former `deploy-rs.yml` workflow (RS server, `RS_FTP_` prefix) has been retired. RS deployment is now handled via the release pipeline (`auto-release.yml`). + +See [Deploy Workflows](../../workflows/dev-deployment.md) for workflow usage. + +### GitHub Secrets and Variables Reference + +When called from CI, the script reads credentials from environment variables set by the workflow. See the [SFTP Deployment Guide](../../deployment/sftp.md#github-secrets-and-variables) for the full secrets/variables tables for `DEV_FTP_*` and `DEMO_FTP_*` environments, including types (Secret vs. Variable) and scopes (Org vs. Repo). + +--- + +**Location:** `docs/api/deploy/` +**Mirrors:** `api/deploy/` +**Last Updated:** 2026-04-07 diff --git a/docs/api/fix/index.md b/docs/api/fix/index.md new file mode 100644 index 0000000..a284f31 --- /dev/null +++ b/docs/api/fix/index.md @@ -0,0 +1,114 @@ + + +# Fix Scripts + +Scripts in `api/fix/` make automated corrections to source files. All scripts: + +- Extend `CliBase` +- Support `--dry-run` (preview changes without writing) +- Support `--help` for usage information +- Operate on tracked files only (respects `.gitignore`) + +Always run with `--dry-run` first to review changes before applying them. + +```bash +php api/fix/