Compare commits

...

463 Commits

Author SHA1 Message Date
Jonathan Miller 14de7dbe19 feat(cli): add 4 release pipeline CLI tools
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Release configuration (push) Successful in 4s
Generic: Repo Health / Scripts governance (push) Successful in 4s
Generic: Repo Health / Repository health (push) Failing after 5s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Universal: PR Check / Branch Policy (pull_request) Failing after 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 5s
Universal: PR Check / Validate PR (pull_request) Successful in 4s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 10s
Generic: Repo Health / Release configuration (pull_request) Successful in 4s
Generic: Repo Health / Repository health (pull_request) Failing after 4s
Generic: Repo Health / Scripts governance (pull_request) Successful in 5s
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Has been skipped
Platform: moko-platform CI / CI Summary (pull_request) Has been cancelled
- release_verify.php: Post-release artifact verification (ZIP integrity,
  manifest version, SHA256 vs updates.xml, disallowed files check)
- release_validate.php: Pre-release sanity checks (version consistency
  across README, CHANGELOG, manifest, updates.xml, composer.json)
- release_body_update.php: Update Gitea release body with changelog
  extract and checksums table via API
- dev_branch_reset.php: Delete and recreate dev branch from main via
  Gitea API

Resolves: #56, #60, #62, #64

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 22:37:37 -05:00
Jonathan Miller 492f1cbb80 fix: version_set_platform.php reads manifest.xml + supports joomla platform
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 2s
- Added .mokogitea/manifest.xml XML lookup (new format)
- Added .mokogitea/.mokostandards fallback (legacy)
- Platform 'joomla' now handled alongside 'waas-component'

This was the root cause of manifest version not updating during releases.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 15:43:42 -05:00
Jonathan Miller a4eed1c1bb fix(ci): add missing branch output to auto-release version step
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 1s
Step 2 (Version archive branch) failed because steps.version.outputs.branch
was empty. Added branch=version/MAJOR to Step 1 outputs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 14:44:44 -05:00
jmiller e618bedd2d Merge pull request 'chore: cascade main → dev (008cdeb) [skip ci]' (#51) from main into dev
chore: cascade main → dev [skip ci]
2026-05-22 03:33:45 +00:00
Jonathan Miller 008cdeb996 fix: updates_xml_build.php uses 'development' tag, not 'dev'
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 3s
The release tag is 'development' but the CLI was generating download
URLs with tag 'dev', causing Joomla update downloads to 404.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 22:33:40 -05:00
jmiller 5f3b0d9980 Merge pull request 'chore: cascade main → dev (3e20003) [skip ci]' (#50) from main into dev
chore: cascade main → dev [skip ci]
2026-05-22 03:28:34 +00:00
Jonathan Miller 3e20003d18 fix(ci): add php-curl to pre-release Setup PHP step
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 2s
updates_xml_sync.php requires cURL extension.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 22:28:26 -05:00
jmiller 32ef515d51 Merge pull request 'chore: cascade main → dev (0aa1136) [skip ci]' (#49) from main into dev
chore: cascade main → dev [skip ci]
2026-05-22 03:25:07 +00:00
Jonathan Miller 0aa113652f fix(ci): continue-on-error for release upload + CLI for updates.xml
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 2s
- Add continue-on-error to Create Release step so upload failures
  don't kill the entire job
- Replace inline Python updates.xml builder with updates_xml_build.php CLI
- Ensures Sync updates.xml step always runs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 22:24:59 -05:00
Jonathan Miller 41c5043352 fix(ci): use build step zip_name/zip_path for upload [skip ci]
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 22:18:55 -05:00
jmiller e5784c0e1d Merge pull request 'chore: cascade main → dev (ff3e4e3) [skip ci]' (#48) from main into dev
chore: cascade main → dev [skip ci]
2026-05-22 03:18:22 +00:00
Jonathan Miller ff3e4e323a fix(cli): support global Composer installs for bin/moko
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 2s
Check $GLOBALS['_composer_autoload_path'] before falling back to
project-local vendor/autoload.php, allowing the CLI to work when
installed via composer global require.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-22 03:18:17 +00:00
Jonathan Miller 05eb26c811 fix(ci): remove control char from pre-release.yml sed pattern [skip ci]
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 22:13:44 -05:00
jmiller 3f3e4e2aaa Merge pull request 'chore: cascade main → dev (02d2e55) [skip ci]' (#47) from main into dev
chore: cascade main → dev [skip ci]
2026-05-22 02:55:25 +00:00
jmiller 02d2e55954 refactor(ci): auto-release uses CLI tools (#45) (#46)
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 3s
2026-05-22 02:55:22 +00:00
Jonathan Miller 9b456da7e5 refactor(ci): auto-release uses CLI tools for 6 major steps (#45)
Replace inline bash with moko-platform CLI calls:
- manifest_read.php for platform detection
- version_read.php + version_bump.php for version management
- badge_update.php for README badges
- updates_xml_build.php for Joomla update stream
- release_cascade.php for pre-release cleanup

Reduces auto-release.yml from 1006 to 762 lines (-244).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 21:54:51 -05:00
jmiller 60e53d5e6e Merge pull request 'chore: cascade main → dev (12d27e7) [skip ci]' (#42) from main into dev
chore: cascade main → dev [skip ci]
2026-05-22 02:48:49 +00:00
jmiller 12d27e70c2 refactor(ci): pre-release uses CLI tools for detect/version/build (#41)
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 2s
2026-05-22 02:48:44 +00:00
Jonathan Miller 3c11006aae refactor(ci): pre-release uses CLI tools for detect/version/build
Replace inline bash with moko-platform CLI calls:
- manifest_read.php --github-output for platform detection
- version_bump.php for patch version increment
- version_set_platform.php for manifest version update
- joomla_build.php for type-aware package building
- updates_xml_sync.php for cross-branch sync

Reduces inline bash from ~160 lines to ~85 lines.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 21:48:12 -05:00
jmiller 4b9a682e53 Merge pull request 'chore: cascade main → dev (89007ab) [skip ci]' (#40) from main into dev
chore: cascade main → dev [skip ci]
2026-05-22 02:39:22 +00:00
jmiller 89007ab9ba feat(cli): updates_xml_sync.php replaces inline workflow sync (#39)
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 2s
2026-05-22 02:39:17 +00:00
Jonathan Miller 4dfbcf4fd2 feat(cli): add updates_xml_sync.php — replaces inline workflow sync (#34)
New CLI tool syncs updates.xml to target branches via Gitea API.
Pre-release workflow now calls the CLI instead of inline bash.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 21:38:54 -05:00
Jonathan Miller c6da98289b chore: rename MokoStandards to moko-platform in CLI headers
Update DEFGROUP and INGROUP fields across all CLI scripts
to reflect the repo rename from MokoStandards to moko-platform.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 21:35:29 -05:00
jmiller b7a07d2e08 Merge pull request 'chore: cascade main → dev (93b2e87) [skip ci]' (#38) from main into dev
chore: cascade main → dev [skip ci]
2026-05-22 02:35:05 +00:00
jmiller 93b2e87c38 fix(ci): sync updates.xml via API instead of git checkout (#34) (#37)
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 2s
2026-05-22 02:35:01 +00:00
Jonathan Miller 03801ff925 fix(ci): sync updates.xml via API instead of git checkout (#34)
The git checkout approach fails when dev and main have diverged.
Use Gitea Contents API (PUT) to update updates.xml on target branches
directly, matching the pattern already used in auto-release.yml.

Fixes #34

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 21:34:18 -05:00
jmiller 7f9c3f2ab0 Merge pull request 'chore: cascade main → dev (5b9d258) [skip ci]' (#36) from main into dev
chore: cascade main → dev [skip ci]
2026-05-22 02:28:23 +00:00
jmiller 5b9d258135 feat(cli): add release automation CLI scripts (#35)
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 2s
feat(cli): add release automation CLI scripts

Add 6 new CLI scripts replacing inline bash in CI workflows.
Update version_set_platform.php with --stability flag.
2026-05-22 02:28:17 +00:00
Jonathan Miller 3d1af376a0 feat(cli): add release automation CLI scripts
New CLI scripts to replace inline bash in CI workflows:
- changelog_promote.php: promote [Unreleased] to versioned entry
- badge_update.php: update [VERSION: XX.XX.XX] in markdown files
- updates_xml_build.php: generate Joomla updates.xml with stability suffixes
- package_build.php: build ZIP/tar.gz install packages
- release_manage.php: create/update/delete Gitea releases + upload assets
- release_cascade.php: delete lesser pre-release channels

Updated version_set_platform.php with --stability flag to append
-dev/-alpha/-beta/-rc suffixes to non-stable versions.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 21:25:35 -05:00
jmiller ab51b15dfb Merge pull request 'chore: cascade main → dev (a706a2c) [skip ci]' (#33) from main into dev
chore: cascade main → dev [skip ci]
2026-05-21 22:19:55 +00:00
jmiller a706a2cc32 refactor: MokoGiteaAdapter + manifest.xml primary lookup (#32)
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 2s
2026-05-21 22:19:49 +00:00
Jonathan Miller d37b547e87 refactor: manifest_read.php uses manifest.xml as primary lookup
Priority order: manifest.xml > .manifest.xml (legacy) > .moko-platform (v4)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 17:18:03 -05:00
jmiller c33106de1c Merge pull request 'chore: cascade main → dev (46d9af0) [skip ci]' (#31) from main into dev
chore: cascade main → dev [skip ci]
2026-05-21 22:15:57 +00:00
jmiller 46d9af0ff6 refactor: rename GiteaAdapter to MokoGiteaAdapter (#30)
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 2s
2026-05-21 22:15:50 +00:00
Jonathan Miller 6a29fbd99e refactor: rename GiteaAdapter to MokoGiteaAdapter
Rename class, file, and all references across the codebase to align
with the moko-platform naming convention.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 17:14:29 -05:00
jmiller f09a855f0b Merge pull request 'chore: cascade main → dev (8095ea6) [skip ci]' (#29) from main into dev
chore: cascade main → dev [skip ci]
2026-05-21 21:55:27 +00:00
jmiller 8095ea607b refactor: rename .gitea/ to .mokogitea/ in sync engine (#28)
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 2s
2026-05-21 21:55:24 +00:00
Jonathan Miller a09d880c0a refactor: rename .gitea/ to .mokogitea/ in all PHP code and sync engine
Update GiteaAdapter.getWorkflowDir() and getMetadataDir() to return
.mokogitea paths. All 24 PHP files referencing .gitea/ updated.
Bulk sync will now push workflows to .mokogitea/workflows/ in
governed repos.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 16:54:55 -05:00
jmiller a84683df11 Merge pull request 'chore: cascade main → dev (14763e3) [skip ci]' (#27) from main into dev
chore: cascade main → dev [skip ci]
2026-05-21 21:44:09 +00:00
jmiller 14763e3c49 feat(ci): type-aware Joomla build via PHP API (#26)
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 2s
2026-05-21 21:44:05 +00:00
Jonathan Miller e19ca4d7a9 feat(ci): type-aware Joomla build via PHP API (#20, #21)
Add cli/joomla_build.php — standalone build tool that detects all
Joomla extension types from the XML manifest and builds accordingly:
  - plugin, module, component, template, library, file: flat ZIP
  - package: nested ZIPs for each sub-extension in packages/

Update both workflows to call joomla_build.php via the moko-platform
PHP API instead of inlining bash build logic.

Also extends joomla_release.php with:
  - typePrefix() for correct naming (plg_, mod_, com_, tpl_, pkg_, lib_)
  - buildPackageZip() for multi-extension package assembly
  - copyDir() helper

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 16:43:07 -05:00
jmiller de2c17e851 Merge pull request 'chore: cascade main → dev (e73731a) [skip ci]' (#25) from main into dev
chore: cascade main → dev [skip ci]
2026-05-21 21:19:18 +00:00
jmiller e73731aab5 refactor: rename all MokoStandards-API references to moko-platform (#24)
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 2s
2026-05-21 21:19:16 +00:00
Jonathan Miller eb3e2af1ff refactor: rename all MokoStandards-API references to moko-platform
Bulk rename across all workflows, issue templates, and configs:
- Clone URL: MokoStandards-API.git → moko-platform.git
- Local path: /tmp/mokostandards-api → /tmp/moko-platform-api
- DEFGROUP/INGROUP: MokoStandards.* → moko-platform.*
- Step names and comments updated
- REPO URLs updated to MokoConsulting/moko-platform

EXCLUDE lists retain old repo names for backward compatibility
during migration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 16:18:33 -05:00
jmiller c7faf96108 Merge pull request 'chore: cascade main → dev (a27afb4) [skip ci]' (#23) from main into dev
chore: cascade main → dev [skip ci]
2026-05-21 21:07:46 +00:00
jmiller a27afb4619 fix(ci): pipefail and rsync issues in release workflows (#22)
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 2s
fix(ci): pipefail and rsync issues in release workflows (#22)

Fixes #20, Fixes #21
2026-05-21 21:07:42 +00:00
Jonathan Miller f2d1695ac3 fix(ci): pipefail and rsync issues in release workflows (#20, #21)
- Add || true to all find|grep|head pipelines to prevent grep exit-code 1
  from killing steps under bash -e -o pipefail
- Replace rsync with cp -a in pre-release Build Package step since rsync
  is not always available in runner containers (exit 127)

Fixes #20, Fixes #21

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 16:05:54 -05:00
jmiller d45cfadc0f chore: update CLAUDE.md to reference .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 20:09:18 +00:00
jmiller cb4265f07c chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:26:02 +00:00
jmiller 197fd690bf chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:26:02 +00:00
jmiller 98b9ae910b chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:26:01 +00:00
jmiller 9fdd68672f chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:26:01 +00:00
jmiller 2574fa1e8e chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:26:01 +00:00
jmiller 853247dc99 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:26:00 +00:00
jmiller 32860d7d18 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:26:00 +00:00
jmiller 8a0e3b7a97 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:26:00 +00:00
jmiller 550bbe82c7 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:25:59 +00:00
jmiller a3d56575a3 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:25:59 +00:00
jmiller 414276bcbd chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:25:59 +00:00
jmiller f79152fd72 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:25:58 +00:00
jmiller 9ade6d258a chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:25:58 +00:00
jmiller 0e00cafe48 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:25:58 +00:00
jmiller 7f7ad678c4 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:25:57 +00:00
jmiller 39bb17d053 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:25:57 +00:00
jmiller 6138510bd7 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:25:57 +00:00
jmiller 054d77dc43 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:25:56 +00:00
jmiller 851ffc224c chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:25:56 +00:00
jmiller 3ece17543c chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:25:56 +00:00
jmiller fd2c293932 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:25:55 +00:00
jmiller 0510f75680 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:25:55 +00:00
jmiller f35c423697 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:25:54 +00:00
jmiller d0b14631d6 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:25:54 +00:00
jmiller 9eba669788 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:25:54 +00:00
jmiller 9981819c30 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:25:53 +00:00
jmiller 2775e9e8ef chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:25:53 +00:00
jmiller 495ebf7d3d chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:25:53 +00:00
jmiller 2adc10a41a chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:25:52 +00:00
jmiller 2072e2e55e chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:25:52 +00:00
jmiller 1afe0c79f1 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:25:52 +00:00
jmiller 412a4066c0 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:25:51 +00:00
jmiller 64f2a2a185 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:25:51 +00:00
jmiller a90c427539 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:25:51 +00:00
jmiller 40c415404d chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:25:50 +00:00
jmiller ddecb750ec chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:25:50 +00:00
jmiller 78d41451f3 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:25:49 +00:00
jmiller 854d81eb6d chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:25:49 +00:00
jmiller 743b28b088 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:25:49 +00:00
jmiller 1a5392db71 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:25:48 +00:00
jmiller 4215ffb949 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:25:48 +00:00
jmiller 1a8b9f5bd7 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:25:48 +00:00
jmiller 952ebaf1c7 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:25:47 +00:00
jmiller d37bdb6ac3 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:25:47 +00:00
jmiller e72ba9e12f chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:25:47 +00:00
jmiller 78404e427f chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:25:46 +00:00
jmiller 33a6e47848 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:25:46 +00:00
jmiller 31b594ebb7 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:25:46 +00:00
jmiller b672b9af0e chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:25:45 +00:00
jmiller 2f573025cd chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:25:45 +00:00
jmiller bf8336cde2 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:25:45 +00:00
jmiller a33867ed3d chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:25:45 +00:00
jmiller 59a0b26272 Merge pull request 'chore: merge dev into main [skip ci]' (#19) from dev into main
chore: merge dev into main [skip ci]
2026-05-21 02:07:42 +00:00
Jonathan Miller a5d75ffd04 chore: remove monitoring infrastructure (moved to mokogitea-private)
Docker compose, Grafana dashboards, library panels, and monitoring
scripts are infrastructure config — they belong in the private repo,
not the public moko-platform.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-20 20:56:43 -05:00
Jonathan Miller fedd6726d4 Merge remote-tracking branch 'origin/main' into dev 2026-05-19 21:17:21 -05:00
jmiller 7bd4ede119 fix(ci): clean up docs-index link check (remove broken heredoc remnants)
Universal: Cascade Main → Dev / Cascade main → branches (push) Failing after 1s
Authored-by: Moko Consulting
2026-05-20 02:13:12 +00:00
jmiller 3ac3abc7d1 fix(ci): replace missing_links python heredoc with bash loop
Universal: Cascade Main → Dev / Cascade main → branches (push) Failing after 1s
Second heredoc also incompatible with act runner.

Authored-by: Moko Consulting
2026-05-20 02:09:45 +00:00
jmiller e57ad2c1ba fix(ci): replace python -c with bash printf for JSON report
Universal: Cascade Main → Dev / Cascade main → branches (push) Failing after 1s
Avoid YAML quoting issues with embedded python.

Authored-by: Moko Consulting
2026-05-20 02:04:16 +00:00
jmiller 33d5dac060 Merge pull request 'chore: cascade main → dev (fb64c17) [skip ci]' (#18) from main into dev
chore: cascade main → dev [skip ci]
2026-05-20 02:00:57 +00:00
jmiller fb64c176fe fix(ci): replace python heredoc with python -c (act runner compat)
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 3s
Indented heredoc delimiters are not supported by the act runner.

Authored-by: Moko Consulting
2026-05-20 02:00:53 +00:00
jmiller 75508e8e75 fix(ci): replace indented heredocs with python -c (act runner compat)
Universal: Cascade Main → Dev / Cascade main → branches (push) Failing after 1s
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Scripts governance (push) Successful in 3s
Generic: Repo Health / Repository health (push) Failing after 3s
Generic: Repo Health / Release configuration (push) Failing after 28s
The act runner doesn't support indented heredoc delimiters.
Replace <<'PY' ... PY with python3 -c inline.

Authored-by: Moko Consulting
2026-05-20 01:59:39 +00:00
jmiller 6b395323de fix(ci): guard empty env var reads with conditional check
Universal: Cascade Main → Dev / Cascade main → branches (push) Failing after 1s
IFS read on empty heredoc string returns exit code 1-2 in some
bash versions. Use if-else guard instead.

Authored-by: Moko Consulting
2026-05-20 01:54:29 +00:00
jmiller 5adf6e2c66 Merge pull request 'chore: cascade main → dev (3427f9f) [skip ci]' (#17) from main into dev
chore: cascade main → dev [skip ci]
2026-05-20 01:50:01 +00:00
jmiller 3427f9f989 fix(ci): remove concurrency block causing Gitea panic on dispatch
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 3s
Authored-by: Moko Consulting
2026-05-20 01:49:52 +00:00
jmiller a4e417c667 fix(ci): handle empty env vars in repo-health with set -u
Universal: Cascade Main → Dev / Cascade main → branches (push) Failing after 1s
REPO_DISALLOWED_DIRS and SCRIPTS_REQUIRED_DIRS can be empty,
which causes exit code 2 under set -u. Use ${VAR:-} pattern.

Authored-by: Moko Consulting
2026-05-20 01:46:04 +00:00
jmiller 57326e597c fix(ci): initialize missing_required before source dir check, handle empty SOURCE_DIR
Universal: Cascade Main → Dev / Cascade main → branches (push) Failing after 2s
Platform: MokoStandards CI / Gate 2: Unit Tests (8.1) (push) Has been skipped
Platform: MokoStandards CI / Gate 2: Unit Tests (8.2) (push) Has been skipped
Platform: MokoStandards CI / Gate 2: Unit Tests (8.3) (push) Has been skipped
Platform: MokoStandards CI / Gate 3: Self-Health Check (push) Has been skipped
Platform: MokoStandards CI / Gate 4: Governance (push) Has been skipped
Platform: MokoStandards CI / Gate 1: Code Quality (push) Failing after 13s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 3s
Platform: MokoStandards CI / Gate 5: Template Integrity (push) Has been skipped
Generic: Repo Health / Release configuration (push) Successful in 4s
Generic: Repo Health / Scripts governance (push) Successful in 3s
Generic: Repo Health / Repository health (push) Failing after 4s
Platform: MokoStandards CI / CI Summary (push) Failing after 1s
Authored-by: Moko Consulting
2026-05-20 01:33:43 +00:00
jmiller 11fbcf70f9 fix(ci): repo-health accepts platform repos and bare dev branch
Universal: Cascade Main → Dev / Cascade main → branches (push) Failing after 1s
Platform: MokoStandards CI / Gate 2: Unit Tests (8.1) (push) Has been skipped
Platform: MokoStandards CI / Gate 2: Unit Tests (8.2) (push) Has been skipped
Platform: MokoStandards CI / Gate 2: Unit Tests (8.3) (push) Has been skipped
Platform: MokoStandards CI / Gate 3: Self-Health Check (push) Has been skipped
Platform: MokoStandards CI / Gate 4: Governance (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 0s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Release configuration (push) Successful in 3s
Generic: Repo Health / Scripts governance (push) Successful in 3s
Generic: Repo Health / Repository health (push) Failing after 3s
Platform: MokoStandards CI / Gate 1: Code Quality (push) Failing after 35s
Platform: MokoStandards CI / Gate 5: Template Integrity (push) Has been skipped
Platform: MokoStandards CI / CI Summary (push) Failing after 1s
Platform repos (with deploy/, cli/, monitoring/) don't need src/.
Bare dev branch is now accepted alongside dev/* versioned branches.

Authored-by: Moko Consulting
2026-05-20 01:28:05 +00:00
jmiller ec18b0e52f chore: remove TODO.md (disallowed by repo health policy) [skip ci]
Authored-by: Moko Consulting
2026-05-20 01:26:40 +00:00
jmiller 66f6fd5a05 chore: add CODE_OF_CONDUCT.md
Generic: Repo Health / Access control (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Universal: Cascade Main → Dev / Cascade main → branches (push) Failing after 1s
Authored-by: Moko Consulting
2026-05-20 01:26:40 +00:00
jmiller bcde27afc6 chore: add CONTRIBUTING.md
Authored-by: Moko Consulting
2026-05-20 01:26:39 +00:00
jmiller 8d7b429b3a chore: add GPL-3.0 LICENSE file
Universal: Cascade Main → Dev / Cascade main → branches (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Scripts governance (push) Successful in 4s
Generic: Repo Health / Repository health (push) Failing after 3s
Generic: Repo Health / Release configuration (push) Failing after 30s
Platform: MokoStandards CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: MokoStandards CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: MokoStandards CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: MokoStandards CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: MokoStandards CI / Gate 4: Governance (push) Has been cancelled
Platform: MokoStandards CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: MokoStandards CI / CI Summary (push) Has been cancelled
Platform: MokoStandards CI / Gate 1: Code Quality (push) Has been cancelled
Authored-by: Moko Consulting
2026-05-20 01:26:39 +00:00
jmiller 7ee0c52aa1 Merge pull request 'chore: merge dev � ci-platform.yml and gitignore updates' (#16) from dev into main
Universal: Cascade Main → Dev / Cascade main → branches (push) Failing after 2s
Platform: MokoStandards CI / Gate 1: Code Quality (push) Failing after 8s
Platform: MokoStandards CI / Gate 2: Unit Tests (8.1) (push) Has been skipped
Platform: MokoStandards CI / Gate 2: Unit Tests (8.2) (push) Has been skipped
Platform: MokoStandards CI / Gate 2: Unit Tests (8.3) (push) Has been skipped
Platform: MokoStandards CI / Gate 3: Self-Health Check (push) Has been skipped
Platform: MokoStandards CI / Gate 4: Governance (push) Has been skipped
Platform: MokoStandards CI / Gate 5: Template Integrity (push) Has been skipped
Platform: MokoStandards CI / CI Summary (push) Failing after 1s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 0s
Generic: Repo Health / Release configuration (push) Failing after 3s
Generic: Repo Health / Scripts governance (push) Successful in 3s
Generic: Repo Health / Repository health (push) Failing after 3s
2026-05-20 00:53:34 +00:00
Jonathan Miller de639305a1 Merge remote-tracking branch 'origin/main' into dev
Platform: MokoStandards CI / Gate 1: Code Quality (push) Failing after 7s
Platform: MokoStandards CI / Gate 2: Unit Tests (8.1) (push) Has been skipped
Platform: MokoStandards CI / Gate 2: Unit Tests (8.2) (push) Has been skipped
Platform: MokoStandards CI / Gate 2: Unit Tests (8.3) (push) Has been skipped
Platform: MokoStandards CI / Gate 3: Self-Health Check (push) Has been skipped
Platform: MokoStandards CI / Gate 4: Governance (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Site Health (push) Has been skipped
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 3s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 3s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Platform: MokoStandards CI / Gate 5: Template Integrity (push) Has been skipped
Generic: Repo Health / Release configuration (push) Failing after 2s
Generic: Repo Health / Scripts governance (push) Successful in 2s
Generic: Repo Health / Repository health (push) Failing after 3s
Platform: MokoStandards CI / CI Summary (push) Failing after 1s
Generic: Repo Health / Release configuration (pull_request) Failing after 3s
Platform: MokoStandards CI / Gate 1: Code Quality (pull_request) Failing after 26s
Platform: MokoStandards CI / Gate 2: Unit Tests (8.1) (pull_request) Has been skipped
Platform: MokoStandards CI / Gate 2: Unit Tests (8.2) (pull_request) Has been skipped
Platform: MokoStandards CI / Gate 2: Unit Tests (8.3) (pull_request) Has been skipped
Platform: MokoStandards CI / Gate 3: Self-Health Check (pull_request) Has been skipped
Platform: MokoStandards CI / Gate 4: Governance (pull_request) Has been skipped
Generic: Repo Health / Repository health (pull_request) Failing after 4s
Platform: MokoStandards CI / Gate 5: Template Integrity (pull_request) Has been skipped
Generic: Repo Health / Scripts governance (pull_request) Successful in 8s
Platform: MokoStandards CI / CI Summary (pull_request) Failing after 1s
2026-05-19 19:53:04 -05:00
jmiller 219fef1691 feat(ci): add uptime and SSL checks to repo-health workflow
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 1s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Scripts governance (push) Successful in 3s
Generic: Repo Health / Repository health (push) Failing after 3s
Generic: Repo Health / Release configuration (push) Failing after 28s
Authored-by: Moko Consulting
2026-05-20 00:52:42 +00:00
jmiller a5ced62ebe feat(ci): add Joomla version audit to security-audit workflow
Generic: Repo Health / Access control (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Universal: Cascade Main → Dev / Cascade main → branches (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Authored-by: Moko Consulting
2026-05-20 00:52:41 +00:00
jmiller 755e296c1d feat(ci): add post-deploy health check to manual deploy
Universal: Cascade Main → Dev / Cascade main → branches (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Authored-by: Moko Consulting
2026-05-20 00:52:41 +00:00
Jonathan Miller 92293a0dee Merge remote-tracking branch 'origin/main' into dev
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Release configuration (push) Failing after 4s
Generic: Repo Health / Scripts governance (push) Successful in 3s
Generic: Repo Health / Repository health (push) Failing after 3s
Platform: MokoStandards CI / Gate 1: Code Quality (push) Failing after 30s
Platform: MokoStandards CI / Gate 2: Unit Tests (8.1) (push) Has been skipped
Platform: MokoStandards CI / Gate 2: Unit Tests (8.2) (push) Has been skipped
Platform: MokoStandards CI / Gate 2: Unit Tests (8.3) (push) Has been skipped
Platform: MokoStandards CI / Gate 3: Self-Health Check (push) Has been skipped
Platform: MokoStandards CI / Gate 4: Governance (push) Has been skipped
Platform: MokoStandards CI / Gate 5: Template Integrity (push) Has been skipped
Platform: MokoStandards CI / CI Summary (push) Failing after 1s
Platform: MokoStandards CI / Gate 1: Code Quality (pull_request) Failing after 8s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Platform: MokoStandards CI / Gate 2: Unit Tests (8.1) (pull_request) Has been skipped
Platform: MokoStandards CI / Gate 2: Unit Tests (8.2) (pull_request) Has been skipped
Platform: MokoStandards CI / Gate 2: Unit Tests (8.3) (pull_request) Has been skipped
Platform: MokoStandards CI / Gate 3: Self-Health Check (pull_request) Has been skipped
Platform: MokoStandards CI / Gate 4: Governance (pull_request) Has been skipped
Universal: PR Check / Validate PR (pull_request) Failing after 3s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Platform: MokoStandards CI / Gate 5: Template Integrity (pull_request) Has been skipped
Platform: MokoStandards CI / CI Summary (pull_request) Failing after 0s
Generic: Repo Health / Release configuration (pull_request) Failing after 2s
Generic: Repo Health / Scripts governance (pull_request) Successful in 3s
Generic: Repo Health / Repository health (pull_request) Failing after 3s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Failing after 41s
2026-05-19 15:49:51 -05:00
jmiller 44f21f2c3c feat: add ssl-check.php
Universal: Cascade Main → Dev / Cascade main → branches (push) Failing after 1s
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Scripts governance (push) Successful in 10s
Generic: Repo Health / Repository health (push) Failing after 9s
Generic: Repo Health / Release configuration (push) Failing after 36s
Authored-by: Moko Consulting
2026-05-19 20:47:23 +00:00
jmiller 504d463ec2 feat: add rollback-joomla.php
Universal: Cascade Main → Dev / Cascade main → branches (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Authored-by: Moko Consulting
2026-05-19 20:47:22 +00:00
jmiller 3f689dc083 feat: add backup-before-deploy.php
Universal: Cascade Main → Dev / Cascade main → branches (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Authored-by: Moko Consulting
2026-05-19 20:47:20 +00:00
jmiller 442cc2cc77 feat: add check_file_integrity.php
Universal: Cascade Main → Dev / Cascade main → branches (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Authored-by: Moko Consulting
2026-05-19 20:47:19 +00:00
jmiller 75f79fd0c8 feat: add bulk_workflow_trigger.php
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Cascade Main → Dev / Cascade main → branches (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Authored-by: Moko Consulting
2026-05-19 20:47:18 +00:00
jmiller 66939d9cc5 feat: add deploy-dolibarr.php
Universal: Cascade Main → Dev / Cascade main → branches (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Authored-by: Moko Consulting
2026-05-19 20:47:17 +00:00
jmiller 857525268a feat: add dependency-audit.yml
Universal: Cascade Main → Dev / Cascade main → branches (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Authored-by: Moko Consulting
2026-05-19 20:47:16 +00:00
jmiller cf0b2726fd feat: add joomla-version-audit.php
Universal: Cascade Main → Dev / Cascade main → branches (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Authored-by: Moko Consulting
2026-05-19 20:47:14 +00:00
jmiller 7d8c094227 feat: add client_inventory.php
Universal: Cascade Main → Dev / Cascade main → branches (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Authored-by: Moko Consulting
2026-05-19 20:47:13 +00:00
jmiller 56e53dff55 feat: add scaffold_client.php
Universal: Cascade Main → Dev / Cascade main → branches (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Authored-by: Moko Consulting
2026-05-19 20:47:11 +00:00
jmiller d788400bfd feat: add uptime-probe.php
Universal: Cascade Main → Dev / Cascade main → branches (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Authored-by: Moko Consulting
2026-05-19 20:47:10 +00:00
jmiller 96ffe19fe9 feat: add bulk_workflow_trigger.sh
Universal: Cascade Main → Dev / Cascade main → branches (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Authored-by: Moko Consulting
2026-05-19 20:47:09 +00:00
jmiller e5b243ecf0 feat: add health-check.php
Universal: Cascade Main → Dev / Cascade main → branches (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Authored-by: Moko Consulting
2026-05-19 20:47:08 +00:00
Jonathan Miller c0a16f53ad feat(ci): add ci-platform.yml — self-validating CI for the standards engine
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Release configuration (push) Failing after 2s
Generic: Repo Health / Scripts governance (push) Successful in 3s
Generic: Repo Health / Repository health (push) Failing after 3s
Platform: MokoStandards CI / Gate 1: Code Quality (push) Failing after 23s
Platform: MokoStandards CI / Gate 2: Unit Tests (8.1) (push) Has been skipped
Platform: MokoStandards CI / Gate 2: Unit Tests (8.2) (push) Has been skipped
Platform: MokoStandards CI / Gate 2: Unit Tests (8.3) (push) Has been skipped
Platform: MokoStandards CI / Gate 3: Self-Health Check (push) Has been skipped
Platform: MokoStandards CI / Gate 4: Governance (push) Has been skipped
Platform: MokoStandards CI / Gate 5: Template Integrity (push) Has been skipped
Platform: MokoStandards CI / CI Summary (push) Failing after 1s
5-gate pipeline that dogfoods the platform's own tools:

  Gate 1: Code Quality — PHPCS (PSR-12), PHPStan (L5), Psalm
  Gate 2: Unit Tests — PHPUnit across PHP 8.1/8.2/8.3 matrix
  Gate 3: Self-Health — runs bin/moko health against its own repo
  Gate 4: Governance — SPDX headers, secret detection, version consistency
  Gate 5: Template Integrity — YAML lint on workflow templates, gitignore
           validation, PHP syntax on all validate/ scripts

The standards engine must pass its own standards.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-19 15:37:24 -05:00
jmiller ac8af534f7 Merge pull request 'chore: cascade main → dev (0962252) [skip ci]' (#15) from main into dev 2026-05-19 20:22:26 +00:00
jmiller 0962252de3 Merge pull request 'chore: add wiki/ to gitignore templates and health checker' (#13) from dev into main
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 3s
Generic: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 4s
Generic: Repo Health / Release configuration (push) Failing after 3s
Generic: Repo Health / Scripts governance (push) Successful in 3s
Generic: Repo Health / Repository health (push) Failing after 3s
Generic: Repo Health / Scripts governance (pull_request) Successful in 3s
Generic: Repo Health / Repository health (pull_request) Failing after 3s
Generic: Repo Health / Release configuration (pull_request) Failing after 33s
2026-05-19 20:00:13 +00:00
jmiller e09444970a feat(deploy): add sync-joomla.php for server-to-server Joomla sync
Universal: Cascade Main → Dev / Cascade main → branches (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
New CLI script that syncs Joomla directories between two servers via
rsync over SSH relay. Always excludes configuration.php.

Authored-by: Moko Consulting
2026-05-19 20:00:05 +00:00
Jonathan Miller 79f50df907 chore: add wiki/ to gitignore templates and health checker
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 3s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 3s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Generic: Repo Health / Release configuration (push) Failing after 3s
Generic: Repo Health / Scripts governance (push) Successful in 3s
Generic: Repo Health / Repository health (push) Failing after 3s
Generic: Repo Health / Release configuration (pull_request) Failing after 4s
Generic: Repo Health / Repository health (pull_request) Failing after 3s
Generic: Repo Health / Scripts governance (pull_request) Failing after 27s
- Add wiki/ exclusion to all 3 gitignore config templates
  (generic, dolibarr, joomla)
- Add wiki/ to moko-platform's own .gitignore
- Update check_repo_health.php to require wiki/ in .gitignore
- Add wiki/ to disallowed committed directories check

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-19 14:56:51 -05:00
jmiller e3c3e3b9cf Merge pull request 'chore: sync main to dev (.mokogitea -> .gitea rename) [skip ci]' (#12) from main into dev 2026-05-16 19:00:20 +00:00
jmiller aea6ee49d4 chore: remove .mokogitea/workflows/security-audit.yml (moved to .gitea/) [skip ci] 2026-05-16 18:56:53 +00:00
jmiller 8f8f180407 chore: move .mokogitea/workflows/security-audit.yml to .gitea/workflows/security-audit.yml [skip ci] 2026-05-16 18:56:53 +00:00
jmiller 6252ebc9e1 chore: remove .mokogitea/workflows/repo-health.yml (moved to .gitea/) [skip ci] 2026-05-16 18:56:53 +00:00
jmiller 79ee7c9fb9 chore: move .mokogitea/workflows/repo-health.yml to .gitea/workflows/repo-health.yml [skip ci] 2026-05-16 18:56:52 +00:00
jmiller 98b5d6dc1a chore: remove .mokogitea/workflows/pre-release.yml (moved to .gitea/) [skip ci] 2026-05-16 18:56:52 +00:00
jmiller aeb5aeb14c chore: move .mokogitea/workflows/pre-release.yml to .gitea/workflows/pre-release.yml [skip ci] 2026-05-16 18:56:51 +00:00
jmiller 695c687d30 chore: remove .mokogitea/workflows/pr-check.yml (moved to .gitea/) [skip ci] 2026-05-16 18:56:51 +00:00
jmiller 0ccca11029 chore: move .mokogitea/workflows/pr-check.yml to .gitea/workflows/pr-check.yml [skip ci] 2026-05-16 18:56:50 +00:00
jmiller 4d2eb7f0bb chore: remove .mokogitea/workflows/notify.yml (moved to .gitea/) [skip ci] 2026-05-16 18:56:50 +00:00
jmiller 4b5c778c0c chore: move .mokogitea/workflows/notify.yml to .gitea/workflows/notify.yml [skip ci] 2026-05-16 18:56:49 +00:00
jmiller 853db948c6 chore: remove .mokogitea/workflows/gitleaks.yml (moved to .gitea/) [skip ci] 2026-05-16 18:56:49 +00:00
jmiller 4460259425 chore: move .mokogitea/workflows/gitleaks.yml to .gitea/workflows/gitleaks.yml [skip ci] 2026-05-16 18:56:48 +00:00
jmiller f743071f2f chore: remove .mokogitea/workflows/deploy-manual.yml (moved to .gitea/) [skip ci] 2026-05-16 18:56:48 +00:00
jmiller 9e1793dcc4 chore: move .mokogitea/workflows/deploy-manual.yml to .gitea/workflows/deploy-manual.yml [skip ci] 2026-05-16 18:56:47 +00:00
jmiller 62bc1231b5 chore: remove .mokogitea/workflows/cleanup.yml (moved to .gitea/) [skip ci] 2026-05-16 18:56:47 +00:00
jmiller b1cab83594 chore: move .mokogitea/workflows/cleanup.yml to .gitea/workflows/cleanup.yml [skip ci] 2026-05-16 18:56:46 +00:00
jmiller 4f8dad938c chore: remove .mokogitea/workflows/cascade-dev.yml (moved to .gitea/) [skip ci] 2026-05-16 18:56:46 +00:00
jmiller c214a11081 chore: move .mokogitea/workflows/cascade-dev.yml to .gitea/workflows/cascade-dev.yml [skip ci] 2026-05-16 18:56:45 +00:00
jmiller d97fb4304a chore: remove .mokogitea/workflows/auto-release.yml (moved to .gitea/) [skip ci] 2026-05-16 18:56:45 +00:00
jmiller 8c2c8197f7 chore: move .mokogitea/workflows/auto-release.yml to .gitea/workflows/auto-release.yml [skip ci] 2026-05-16 18:56:44 +00:00
jmiller 36423595fb chore: remove .mokogitea/sync-wikis.yml (moved to .gitea/) [skip ci] 2026-05-16 18:56:44 +00:00
jmiller 21b24e1f69 chore: move .mokogitea/sync-wikis.yml to .gitea/sync-wikis.yml [skip ci] 2026-05-16 18:56:44 +00:00
jmiller 6975111c09 chore: remove .mokogitea/renovate.yml (moved to .gitea/) [skip ci] 2026-05-16 18:56:43 +00:00
jmiller c7bf015ef7 chore: move .mokogitea/renovate.yml to .gitea/renovate.yml [skip ci] 2026-05-16 18:56:43 +00:00
jmiller 7ae8e4543a chore: remove .mokogitea/pr-branch-check.yml (moved to .gitea/) [skip ci] 2026-05-16 18:56:42 +00:00
jmiller a5dc9e7697 chore: move .mokogitea/pr-branch-check.yml to .gitea/pr-branch-check.yml [skip ci] 2026-05-16 18:56:42 +00:00
jmiller af70ec5de2 chore: remove .mokogitea/manifest.xml (moved to .gitea/) [skip ci] 2026-05-16 18:56:41 +00:00
jmiller f766f264ff chore: move .mokogitea/manifest.xml to .gitea/manifest.xml [skip ci] 2026-05-16 18:56:41 +00:00
jmiller 024fa7255b chore: remove .mokogitea/bulk-repo-sync.yml (moved to .gitea/) [skip ci] 2026-05-16 18:56:41 +00:00
jmiller c6db1aa5fc chore: move .mokogitea/bulk-repo-sync.yml to .gitea/bulk-repo-sync.yml [skip ci] 2026-05-16 18:56:40 +00:00
jmiller 63860699b5 chore: remove .mokogitea/branch-protection.yml (moved to .gitea/) [skip ci] 2026-05-16 18:56:40 +00:00
jmiller c13945b961 chore: move .mokogitea/branch-protection.yml to .gitea/branch-protection.yml [skip ci] 2026-05-16 18:56:39 +00:00
jmiller 725e16a12e chore: remove .mokogitea/ISSUE_TEMPLATE/version.md (moved to .gitea/) [skip ci] 2026-05-16 18:56:39 +00:00
jmiller 8d51cce315 chore: move .mokogitea/ISSUE_TEMPLATE/version.md to .gitea/ISSUE_TEMPLATE/version.md [skip ci] 2026-05-16 18:56:38 +00:00
jmiller e4e3b57fa2 chore: remove .mokogitea/ISSUE_TEMPLATE/security.md (moved to .gitea/) [skip ci] 2026-05-16 18:56:38 +00:00
jmiller f3276d4082 chore: move .mokogitea/ISSUE_TEMPLATE/security.md to .gitea/ISSUE_TEMPLATE/security.md [skip ci] 2026-05-16 18:56:37 +00:00
jmiller ae3ee70396 chore: remove .mokogitea/ISSUE_TEMPLATE/rfc.md (moved to .gitea/) [skip ci] 2026-05-16 18:56:37 +00:00
jmiller ffb70de26e chore: move .mokogitea/ISSUE_TEMPLATE/rfc.md to .gitea/ISSUE_TEMPLATE/rfc.md [skip ci] 2026-05-16 18:56:36 +00:00
jmiller 29c8dcc66c chore: remove .mokogitea/ISSUE_TEMPLATE/question.md (moved to .gitea/) [skip ci] 2026-05-16 18:56:36 +00:00
jmiller e26e1ba7f9 chore: move .mokogitea/ISSUE_TEMPLATE/question.md to .gitea/ISSUE_TEMPLATE/question.md [skip ci] 2026-05-16 18:56:35 +00:00
jmiller bb8c3dc0f7 chore: remove .mokogitea/ISSUE_TEMPLATE/feature_request.md (moved to .gitea/) [skip ci] 2026-05-16 18:56:35 +00:00
jmiller a5b804f796 chore: move .mokogitea/ISSUE_TEMPLATE/feature_request.md to .gitea/ISSUE_TEMPLATE/feature_request.md [skip ci] 2026-05-16 18:56:35 +00:00
jmiller e01b6945c8 chore: remove .mokogitea/ISSUE_TEMPLATE/documentation.md (moved to .gitea/) [skip ci] 2026-05-16 18:56:34 +00:00
jmiller 26eaa1e60d chore: move .mokogitea/ISSUE_TEMPLATE/documentation.md to .gitea/ISSUE_TEMPLATE/documentation.md [skip ci] 2026-05-16 18:56:34 +00:00
jmiller 4d16fbeace chore: remove .mokogitea/ISSUE_TEMPLATE/config.yml (moved to .gitea/) [skip ci] 2026-05-16 18:56:33 +00:00
jmiller cefe1ea00a chore: move .mokogitea/ISSUE_TEMPLATE/config.yml to .gitea/ISSUE_TEMPLATE/config.yml [skip ci] 2026-05-16 18:56:33 +00:00
jmiller 8d39a7e927 chore: remove .mokogitea/ISSUE_TEMPLATE/bug_report.md (moved to .gitea/) [skip ci] 2026-05-16 18:56:32 +00:00
jmiller cf63cbdeba chore: move .mokogitea/ISSUE_TEMPLATE/bug_report.md to .gitea/ISSUE_TEMPLATE/bug_report.md [skip ci] 2026-05-16 18:56:32 +00:00
jmiller fce2e79cab chore: remove .mokogitea/ISSUE_TEMPLATE/adr.md (moved to .gitea/) [skip ci] 2026-05-16 18:56:31 +00:00
jmiller ba2783e01e chore: move .mokogitea/ISSUE_TEMPLATE/adr.md to .gitea/ISSUE_TEMPLATE/adr.md [skip ci] 2026-05-16 18:56:31 +00:00
jmiller 09db381e5b Merge pull request 'chore: merge dev into main [skip ci]' (#11) from dev into main 2026-05-16 17:46:50 +00:00
Jonathan Miller 3be806d5af feat: add deploy-module workflow template for Dolibarr module deployment
Symlink-based deployment: clones module repo, pins to stable tag,
creates symlink in htdocs/custom/. Uses org-level GA_TOKEN and
DEPLOY_SSH_KEY secrets.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-16 12:44:39 -05:00
jmiller 4bc7c16e56 v05.00.00
Authored-by: Moko Consulting
2026-05-16 14:08:06 +00:00
Jonathan Miller 99f0e536a9 chore: bump to v05.00.00 — monitoring, infra consolidation, autoheal
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-16 09:07:46 -05:00
jmiller c905fa601b Release: monitoring, wiki, and infrastructure consolidation
Authored-by: Moko Consulting
2026-05-16 13:49:51 +00:00
Jonathan Miller dc2d0b027e Merge remote-tracking branch 'origin/main' into dev 2026-05-16 08:49:23 -05:00
Jonathan Miller f3cb93eb65 fix: remove v_hidden column, simplify probe queries in MokoWaaS dashboard
- Replace excludeByName hack with explicit filterFieldsByName regex
- Remove redundant `and on(site_name)` join from STATUS and API queries
- Compact Performance/Backup/Uptime panel JSON formatting

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-16 08:39:08 -05:00
Jonathan Miller b46706a7a0 fix(monitoring): add targets volume mount to Prometheus compose
The blackbox-http file_sd_configs targets directory was not mounted
into the Prometheus container, causing all probe metrics to be empty
and the MokoWaaS dashboard to show no data.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 21:27:48 -05:00
Jonathan Miller 6de15810f4 feat: add server auto-heal script with split backup management
- Boot check with safe-point detection for unclean restarts
- Split system (daily) and content (2h) backup schedules
- 4-phase auto-heal: filesystem repair, config restore, service restart, health verify
- Self-installing: creates cron jobs, systemd shutdown hook, and config
- Configurable via /etc/moko/autoheal.conf

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 20:52:27 -05:00
Jonathan Miller 4ec51e262b feat: add legend and tooltip options to Grafana library panels
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 20:52:27 -05:00
Jonathan Miller a365c9ec24 fix: remove v_hidden column, rename gitea-server-setup to .mokogitea-private
- Replace excludeByName hack with explicit filterFieldsByName regex
  in mokowaas-dashboard.json to properly hide Value #VERSION
- Update EXCLUDE lists in branch-protection.yml and renovate.yml
  to reference renamed .mokogitea-private repo

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 20:52:26 -05:00
jmiller 4246cb2938 docs: add 04.09.00 changelog — deploy section support [skip ci] 2026-05-12 20:10:51 +00:00
jmiller f2a76c0ae0 feat: manifest_read.php supports <deploy> section fields [skip ci] 2026-05-12 20:10:50 +00:00
jmiller 8d689cf2bb chore: bump standards-version to 04.09.00 [skip ci] 2026-05-12 20:10:50 +00:00
jmiller 427e058d9c docs: add 04.08.00 changelog entry [skip ci] 2026-05-12 19:49:25 +00:00
jmiller 40ff994a1a chore: bump standards-version to 04.08.00 [skip ci] 2026-05-12 19:48:57 +00:00
jmiller cdf0efa6c4 chore: remove .mokogitea/.moko-platform [skip ci] 2026-05-12 19:30:18 +00:00
jmiller ee1c7f1b4b chore: move .mokogitea/manifest.xml to .mokogitea/ [skip ci] 2026-05-12 19:30:17 +00:00
jmiller 20d4cb0434 chore: force-sync .mokogitea/ISSUE_TEMPLATE/version.md [skip ci] 2026-05-12 19:30:17 +00:00
jmiller e78125c931 chore: force-sync .mokogitea/ISSUE_TEMPLATE/security.md [skip ci] 2026-05-12 19:30:16 +00:00
jmiller 0b13722a77 chore: force-sync .mokogitea/ISSUE_TEMPLATE/rfc.md [skip ci] 2026-05-12 19:30:16 +00:00
jmiller 4fbc38c40f chore: force-sync .mokogitea/ISSUE_TEMPLATE/question.md [skip ci] 2026-05-12 19:30:16 +00:00
jmiller f24df6cd76 chore: force-sync .mokogitea/ISSUE_TEMPLATE/feature_request.md [skip ci] 2026-05-12 19:30:15 +00:00
jmiller 436e9ec872 chore: force-sync .mokogitea/ISSUE_TEMPLATE/documentation.md [skip ci] 2026-05-12 19:30:15 +00:00
jmiller 19907ee7cb chore: force-sync .mokogitea/ISSUE_TEMPLATE/config.yml [skip ci] 2026-05-12 19:30:15 +00:00
jmiller 4a0326615b chore: force-sync .mokogitea/ISSUE_TEMPLATE/bug_report.md [skip ci] 2026-05-12 19:30:14 +00:00
jmiller 149b2f9167 chore: force-sync .mokogitea/ISSUE_TEMPLATE/adr.md [skip ci] 2026-05-12 19:30:14 +00:00
jmiller e4265045eb chore: force-sync .mokogitea/workflows/security-audit.yml [skip ci] 2026-05-12 19:30:13 +00:00
jmiller 55ebff78db chore: force-sync .mokogitea/workflows/repo-health.yml [skip ci] 2026-05-12 19:30:13 +00:00
jmiller 592cbd539f chore: force-sync .mokogitea/workflows/pre-release.yml [skip ci] 2026-05-12 19:30:13 +00:00
jmiller 7a98953674 chore: force-sync .mokogitea/workflows/pr-check.yml [skip ci] 2026-05-12 19:30:12 +00:00
jmiller 0962f50b81 chore: force-sync .mokogitea/workflows/notify.yml [skip ci] 2026-05-12 19:30:12 +00:00
jmiller 77858e5cdf chore: force-sync .mokogitea/workflows/gitleaks.yml [skip ci] 2026-05-12 19:30:12 +00:00
jmiller 96d92d4733 chore: force-sync .mokogitea/workflows/deploy-manual.yml [skip ci] 2026-05-12 19:30:11 +00:00
jmiller 2677298d81 chore: force-sync .mokogitea/workflows/cleanup.yml [skip ci] 2026-05-12 19:30:11 +00:00
jmiller 9c552748dc chore: force-sync .mokogitea/workflows/cascade-dev.yml [skip ci] 2026-05-12 19:30:11 +00:00
jmiller 8e5142b674 chore: remove .mokogitea/.manifest.xml [skip ci] 2026-05-12 19:30:10 +00:00
jmiller e3ddcfc70d chore: force-sync .mokogitea/workflows/auto-release.yml [skip ci] 2026-05-12 19:30:10 +00:00
jmiller 431b673922 chore: move .mokogitea/manifest.xml to .mokogitea/ [skip ci] 2026-05-12 19:30:09 +00:00
jmiller 94f4dc482f chore: force-sync .mokogitea/ISSUE_TEMPLATE/version.md [skip ci] 2026-05-12 19:30:09 +00:00
jmiller b17e1fe1ce chore: force-sync .mokogitea/ISSUE_TEMPLATE/security.md [skip ci] 2026-05-12 19:30:08 +00:00
jmiller a5bee8d5a3 chore: force-sync .mokogitea/ISSUE_TEMPLATE/rfc.md [skip ci] 2026-05-12 19:30:08 +00:00
jmiller cb0ed9b66b chore: force-sync .mokogitea/ISSUE_TEMPLATE/question.md [skip ci] 2026-05-12 19:30:08 +00:00
jmiller d56505a478 chore: force-sync .mokogitea/ISSUE_TEMPLATE/feature_request.md [skip ci] 2026-05-12 19:30:07 +00:00
jmiller bb10ba2f91 chore: force-sync .mokogitea/ISSUE_TEMPLATE/documentation.md [skip ci] 2026-05-12 19:30:07 +00:00
jmiller 4a07d82410 chore: force-sync .mokogitea/ISSUE_TEMPLATE/config.yml [skip ci] 2026-05-12 19:30:07 +00:00
jmiller e33bceb7ee chore: force-sync .mokogitea/ISSUE_TEMPLATE/bug_report.md [skip ci] 2026-05-12 19:30:06 +00:00
jmiller f126ac66bc chore: force-sync .mokogitea/ISSUE_TEMPLATE/adr.md [skip ci] 2026-05-12 19:30:06 +00:00
jmiller 36ba47ac0b chore: force-sync .mokogitea/workflows/security-audit.yml [skip ci] 2026-05-12 19:30:05 +00:00
jmiller 0521ee54b2 chore: force-sync .mokogitea/workflows/repo-health.yml [skip ci] 2026-05-12 19:30:05 +00:00
jmiller 2db218d320 chore: force-sync .mokogitea/workflows/pre-release.yml [skip ci] 2026-05-12 19:30:05 +00:00
jmiller 8c5f74a44e chore: force-sync .mokogitea/workflows/pr-check.yml [skip ci] 2026-05-12 19:30:04 +00:00
jmiller a575604998 chore: force-sync .mokogitea/workflows/notify.yml [skip ci] 2026-05-12 19:30:04 +00:00
jmiller 7d345d7c74 chore: force-sync .mokogitea/workflows/gitleaks.yml [skip ci] 2026-05-12 19:30:03 +00:00
jmiller 62f7d798d2 chore: force-sync .mokogitea/workflows/deploy-manual.yml [skip ci] 2026-05-12 19:30:03 +00:00
jmiller fe4f5f2425 chore: force-sync .mokogitea/workflows/cleanup.yml [skip ci] 2026-05-12 19:30:03 +00:00
jmiller 0a7f558a1a chore: force-sync .mokogitea/workflows/cascade-dev.yml [skip ci] 2026-05-12 19:30:02 +00:00
jmiller 368c690f5e chore: force-sync .mokogitea/workflows/auto-release.yml [skip ci] 2026-05-12 19:30:02 +00:00
jmiller 306472f6a1 feat: use manifest_read.php for platform detection [skip ci] 2026-05-12 19:24:20 +00:00
jmiller e4a5f56ca8 feat: use manifest_read.php for platform detection [skip ci] 2026-05-12 19:24:19 +00:00
jmiller 17b09fca27 feat: use manifest_read.php for platform detection [skip ci] 2026-05-12 19:24:18 +00:00
jmiller 64de7159cf feat: add manifest_read.php � full .manifest.xml parser for CI [skip ci] 2026-05-12 19:22:28 +00:00
jmiller 9ece74e3e9 chore: backward-compatible .manifest.xml detection [skip ci] 2026-05-12 19:08:05 +00:00
jmiller 89c63f0dd0 chore: backward-compatible .manifest.xml detection [skip ci] 2026-05-12 19:08:05 +00:00
jmiller 7a1ffcc306 chore: backward-compatible .manifest.xml detection [skip ci] 2026-05-12 19:08:04 +00:00
jmiller 8cac5140d1 chore: remove legacy .moko-platform (now .manifest.xml) [skip ci] 2026-05-12 19:03:41 +00:00
jmiller 0e170b6ee5 chore: rename .moko-platform to .manifest.xml [skip ci] 2026-05-12 19:03:40 +00:00
jmiller 709340c519 chore: sync .mokogitea/ISSUE_TEMPLATE/version.md from template [skip ci] 2026-05-12 18:59:21 +00:00
jmiller dcbfb4cf0c chore: sync .mokogitea/ISSUE_TEMPLATE/security.md from template [skip ci] 2026-05-12 18:59:20 +00:00
jmiller 4aaf88c26e chore: sync .mokogitea/ISSUE_TEMPLATE/rfc.md from template [skip ci] 2026-05-12 18:59:20 +00:00
jmiller de9c36b9d1 chore: sync .mokogitea/ISSUE_TEMPLATE/question.md from template [skip ci] 2026-05-12 18:59:20 +00:00
jmiller ec6cd62e3d chore: sync .mokogitea/ISSUE_TEMPLATE/feature_request.md from template [skip ci] 2026-05-12 18:59:19 +00:00
jmiller 17c51d257e chore: sync .mokogitea/ISSUE_TEMPLATE/documentation.md from template [skip ci] 2026-05-12 18:59:19 +00:00
jmiller dca040ae6e chore: sync .mokogitea/ISSUE_TEMPLATE/config.yml from template [skip ci] 2026-05-12 18:59:19 +00:00
jmiller 2682af6a54 chore: sync .mokogitea/ISSUE_TEMPLATE/bug_report.md from template [skip ci] 2026-05-12 18:59:18 +00:00
jmiller 40d2786ccd chore: sync .mokogitea/ISSUE_TEMPLATE/adr.md from template [skip ci] 2026-05-12 18:59:18 +00:00
jmiller 1c85296c1e chore: sync .mokogitea/workflows/security-audit.yml from template [skip ci] 2026-05-12 18:59:18 +00:00
jmiller 22864806af chore: sync .mokogitea/workflows/repo-health.yml from template [skip ci] 2026-05-12 18:59:17 +00:00
jmiller 8c5e0702d0 chore: sync .mokogitea/workflows/pre-release.yml from template [skip ci] 2026-05-12 18:59:17 +00:00
jmiller 08bb7f5ef5 chore: sync .mokogitea/workflows/pr-check.yml from template [skip ci] 2026-05-12 18:59:17 +00:00
jmiller 4de1f24ab1 chore: sync .mokogitea/workflows/notify.yml from template [skip ci] 2026-05-12 18:59:16 +00:00
jmiller 574460a102 chore: sync .mokogitea/workflows/gitleaks.yml from template [skip ci] 2026-05-12 18:59:16 +00:00
jmiller e6e01fa5a8 chore: sync .mokogitea/workflows/deploy-manual.yml from template [skip ci] 2026-05-12 18:59:15 +00:00
jmiller 9f8bb4a467 chore: sync .mokogitea/workflows/cleanup.yml from template [skip ci] 2026-05-12 18:59:15 +00:00
jmiller 6b7edbe4bb chore: sync .mokogitea/workflows/cascade-dev.yml from template [skip ci] 2026-05-12 18:59:15 +00:00
jmiller db2aa274ba chore: sync .mokogitea/ISSUE_TEMPLATE/version.md from template [skip ci] 2026-05-12 18:59:14 +00:00
jmiller 1c563ac57e chore: sync .mokogitea/workflows/auto-release.yml from template [skip ci] 2026-05-12 18:59:14 +00:00
jmiller ea17e36467 chore: sync .mokogitea/ISSUE_TEMPLATE/security.md from template [skip ci] 2026-05-12 18:59:14 +00:00
jmiller ba2fecfc5d chore: sync .mokogitea/ISSUE_TEMPLATE/rfc.md from template [skip ci] 2026-05-12 18:59:13 +00:00
jmiller f27a092d94 chore: sync .mokogitea/ISSUE_TEMPLATE/question.md from template [skip ci] 2026-05-12 18:59:13 +00:00
jmiller 8d03190c26 chore: sync .mokogitea/ISSUE_TEMPLATE/feature_request.md from template [skip ci] 2026-05-12 18:59:12 +00:00
jmiller 03ac6417ca chore: sync .mokogitea/ISSUE_TEMPLATE/documentation.md from template [skip ci] 2026-05-12 18:59:12 +00:00
jmiller f8063cde8a chore: sync .mokogitea/ISSUE_TEMPLATE/config.yml from template [skip ci] 2026-05-12 18:59:12 +00:00
jmiller 9ea456e200 chore: sync .mokogitea/ISSUE_TEMPLATE/bug_report.md from template [skip ci] 2026-05-12 18:59:11 +00:00
jmiller 05ddd8c3f6 chore: sync .mokogitea/ISSUE_TEMPLATE/adr.md from template [skip ci] 2026-05-12 18:59:11 +00:00
jmiller 0efc36c01e chore: sync .mokogitea/workflows/security-audit.yml from template [skip ci] 2026-05-12 18:59:11 +00:00
jmiller ed7bb4d0cd chore: sync .mokogitea/workflows/repo-health.yml from template [skip ci] 2026-05-12 18:59:10 +00:00
jmiller 8d71491127 chore: sync .mokogitea/workflows/pre-release.yml from template [skip ci] 2026-05-12 18:59:10 +00:00
jmiller 3beb9afb6d chore: sync .mokogitea/workflows/pr-check.yml from template [skip ci] 2026-05-12 18:59:10 +00:00
jmiller f0f793102e chore: sync .mokogitea/workflows/notify.yml from template [skip ci] 2026-05-12 18:59:09 +00:00
jmiller ec5b53cc13 chore: sync .mokogitea/workflows/gitleaks.yml from template [skip ci] 2026-05-12 18:59:09 +00:00
jmiller 1fad2ea277 chore: sync .mokogitea/workflows/deploy-manual.yml from template [skip ci] 2026-05-12 18:59:09 +00:00
jmiller ccbd7e0709 chore: sync .mokogitea/workflows/cleanup.yml from template [skip ci] 2026-05-12 18:59:08 +00:00
jmiller 81b3c66a24 chore: sync .mokogitea/workflows/cascade-dev.yml from template [skip ci] 2026-05-12 18:59:08 +00:00
jmiller b086fd86e7 chore: sync .mokogitea/workflows/auto-release.yml from template [skip ci] 2026-05-12 18:59:07 +00:00
jmiller 48feb11c34 Merge pull request 'fix: rename moko-waas to mokowaas' (#8) from dev into main 2026-05-12 06:38:53 +00:00
Jonathan Miller 931ac0ee86 fix: rename moko-waas to mokowaas throughout
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-12 00:50:48 -05:00
jmiller b64bbe6c71 Merge pull request 'feat: update site_type labels and dashboard filter to mokowaas' (#7) from dev into main 2026-05-12 05:31:29 +00:00
Jonathan Miller 5c02b52c96 feat: update site_type filter from joomla.* to mokowaas.*
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-12 00:30:13 -05:00
Jonathan Miller 118bc058f6 fix: remove panopticon tag, update dashboard labels
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-12 00:23:27 -05:00
jmiller 95f1d05253 Merge pull request 'v05.00.01 � Grafana dashboard overhaul, library panels, monitoring fixes' (#6) from dev into main 2026-05-12 05:20:44 +00:00
Jonathan Miller b5d65e8aa3 chore: rename dashboard to MokoWaaS, update description
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-12 00:19:51 -05:00
Jonathan Miller 4ecb70cdc6 feat: add 14 Grafana library panels and patch bump to v05.00.01
Add reusable library panel templates exported from Grafana:
- Server: CPU, Memory, Disk, Network Traffic
- Docker: Container CPU, Container Memory
- Services: Nginx Request Rate, Nginx Connections, MySQL Queries/s, MySQL Connections
- Monitoring: SSL Certificate Expiry, Service Health, Response Time, Uptime Availability

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-12 00:17:43 -05:00
jmiller aab24bb3ee chore: remove .gitea/workflows/sync-wikis.yml (moved to .mokogitea/) [skip ci] 2026-05-12 05:14:51 +00:00
jmiller c18005da93 chore: move .gitea/workflows/sync-wikis.yml to .mokogitea/sync-wikis.yml [skip ci] 2026-05-12 05:14:51 +00:00
jmiller fac567ddc8 chore: remove .gitea/workflows/renovate.yml (moved to .mokogitea/) [skip ci] 2026-05-12 05:14:51 +00:00
jmiller 177b9a022f chore: move .gitea/workflows/renovate.yml to .mokogitea/renovate.yml [skip ci] 2026-05-12 05:14:50 +00:00
jmiller ef15c2a0a5 chore: remove .gitea/workflows/pr-branch-check.yml (moved to .mokogitea/) [skip ci] 2026-05-12 05:14:50 +00:00
jmiller 8a5c1eea55 chore: move .gitea/workflows/pr-branch-check.yml to .mokogitea/pr-branch-check.yml [skip ci] 2026-05-12 05:14:50 +00:00
jmiller deed8f2844 chore: remove .gitea/workflows/bulk-repo-sync.yml (moved to .mokogitea/) [skip ci] 2026-05-12 05:14:49 +00:00
jmiller 2602a0426c chore: move .gitea/workflows/bulk-repo-sync.yml to .mokogitea/bulk-repo-sync.yml [skip ci] 2026-05-12 05:14:49 +00:00
jmiller 77715142fc chore: remove .gitea/workflows/branch-protection.yml (moved to .mokogitea/) [skip ci] 2026-05-12 05:14:48 +00:00
jmiller d89fb2904d chore: move .gitea/workflows/branch-protection.yml to .mokogitea/branch-protection.yml [skip ci] 2026-05-12 05:14:48 +00:00
jmiller fd98406067 chore: remove .gitea/.moko-platform (moved to .mokogitea/) [skip ci] 2026-05-12 05:14:48 +00:00
jmiller d954b2373e chore: remove .gitea/workflows/sync-wikis.yml (moved to .mokogitea/) [skip ci] 2026-05-12 05:14:47 +00:00
jmiller 48943328aa chore: move .gitea/.moko-platform to .mokogitea/.moko-platform [skip ci] 2026-05-12 05:14:47 +00:00
jmiller 666e11e8dd chore: move .gitea/workflows/sync-wikis.yml to .mokogitea/sync-wikis.yml [skip ci] 2026-05-12 05:14:47 +00:00
jmiller a14002c327 chore: remove .gitea/workflows/renovate.yml (moved to .mokogitea/) [skip ci] 2026-05-12 05:14:46 +00:00
jmiller 624e64499a chore: move .gitea/workflows/renovate.yml to .mokogitea/renovate.yml [skip ci] 2026-05-12 05:14:46 +00:00
jmiller 61121252ca chore: remove .gitea/workflows/pr-branch-check.yml (moved to .mokogitea/) [skip ci] 2026-05-12 05:14:45 +00:00
jmiller 5dbb5aebcb chore: move .gitea/workflows/pr-branch-check.yml to .mokogitea/pr-branch-check.yml [skip ci] 2026-05-12 05:14:45 +00:00
jmiller 0ff2ae6f84 chore: remove .gitea/workflows/bulk-repo-sync.yml (moved to .mokogitea/) [skip ci] 2026-05-12 05:14:45 +00:00
jmiller ede5ae90a5 chore: move .gitea/workflows/bulk-repo-sync.yml to .mokogitea/bulk-repo-sync.yml [skip ci] 2026-05-12 05:14:44 +00:00
jmiller edf09ac744 chore: remove .gitea/workflows/branch-protection.yml (moved to .mokogitea/) [skip ci] 2026-05-12 05:14:44 +00:00
jmiller 63ae3bc9b0 chore: move .gitea/workflows/branch-protection.yml to .mokogitea/branch-protection.yml [skip ci] 2026-05-12 05:14:44 +00:00
jmiller ebb2f5891a chore: remove .gitea/.moko-platform (moved to .mokogitea/) [skip ci] 2026-05-12 05:14:43 +00:00
jmiller 2c7478a6f6 chore: move .gitea/.moko-platform to .mokogitea/.moko-platform [skip ci] 2026-05-12 05:14:43 +00:00
Jonathan Miller 63a21fe0c0 fix: force Site column first in Site Health table via indexByName
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-11 23:56:21 -05:00
Jonathan Miller 787d778e91 feat: rewrite dashboard tables with clean filterFieldsByName pattern
- Site Health: rewritten from scratch — site_url via label_replace on HTTP
  query (works for offline sites), filterFieldsByName to keep only needed
  fields, endpoint URL as clickable Site column
- Joomla Core & Extensions: rewritten — same pattern, Site links to /administrator/
- Backup Status: rewritten — same pattern, Site links to Akeeba Manage
- Combined SSL Days and Last Scrape into Site Health table
- Collapsed Joomla Core section by default
- Moved dashboard to Endpoints folder
- Added site-uptime-alert.sh for ntfy critical alerts on site downtime
- Split updates into System and Ext Updates columns

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-11 23:54:37 -05:00
Jonathan Miller 1c7de961c6 chore: migrate references from .gitea/ to .mokogitea/
- Update template-CONTRIBUTING.md: .github/workflows/ → .mokogitea/workflows/
- Update templates/gitea/README.md: .gitea/ → .mokogitea/
- .mokogitea/ is now the standard system folder

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-11 23:38:10 -05:00
Jonathan Miller c061338428 feat: full-width stat layout, filter non-Joomla sites, standardize noValue
- Switch all stat panels to w:24 full-width for natural 3-column wrapping
- Filter site variable to joomla.* site_type only (excludes git, grafana)
- Standardize all noValue to "—" for offline/unavailable endpoints
- Joomla Version panel uses table format with labelsToFields for xx.xx.xx display
- Remove wideLayout option (only applies to value_and_name textMode)

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-11 22:55:50 -05:00
Jonathan Miller 2e57e60335 feat: enhance WaaS Grafana dashboard with data links, backup monitoring, and layout improvements
- Add clickable links: Site Status → admin, API Reachable → endpoint, HTTP Status → Wikipedia reference, Last Backup/Records → Akeeba Manage
- Fix backup status doubling with max by() aggregation
- Show version as two-row display (site name + version string) using table format with labelsToFields
- Map 0 values to dash on Extensions, Enabled, Disabled panels
- Set minVizWidth for 3-column wrapping layout
- All links use dynamic Grafana variables, no hardcoded endpoints

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-11 22:41:41 -05:00
jmiller 87c7a7d3da release: v05.00.00 — major version bump, docs cleanup, header standardization
Sync Wikis to GitHub / Export wikis to GitHub (push) Successful in 6s
2026-05-11 22:13:29 +00:00
Jonathan Miller 0e273dae96 feat: add license header check to repo health (15 pts)
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
New 'License Headers' category in check_repo_health.php:
- Copyright headers present (>=80% of source files, 5 pts)
- SPDX-License-Identifier present (>=80%, 5 pts)
- FILE INFORMATION block present (>=70%, 5 pts)

Scans PHP, TS, CSS, and YAML files, excluding vendor/node_modules/dist.
Total health score now 155 pts (was 140).

Authored-by: Moko Consulting

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-11 17:12:47 -05:00
Jonathan Miller 1799401db5 feat: add standard file headers to all 57 files missing them
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
- Add Copyright + FILE INFORMATION headers to 11 PHP enterprise classes
- Add FILE INFORMATION blocks to 9 PHP files with incomplete headers
- Add headers to 2 test files
- Add markdown comment headers to 27 index/README files
- Add headers to 5 root markdown files
- Add FILE INFORMATION to 4 files with existing but incomplete headers

All files now conform to moko-platform file header standard.

Authored-by: Moko Consulting

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-11 17:10:19 -05:00
Jonathan Miller 1d87be7d5e fix: standardize file headers — REPO rename, SPDX case, missing fields
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
- Update REPO: from MokoStandards-API to moko-platform in 125 files
- Fix wrong org path (mokoconsulting-tech → MokoConsulting) in 10 files
- Fix SPDX-LICENSE-IDENTIFIER case in 2 template files
- Add missing REPO: field to 3 files

Authored-by: Moko Consulting

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-11 17:01:17 -05:00
Jonathan Miller 38a975ee57 chore: remove VERSION from all file header comments
Branch Policy Check / Verify merge target (pull_request) Successful in 0s
Remove VERSION: XX.YY.ZZ lines from 213 file headers across PHP,
TypeScript, TF definitions, workflows, CSS, markdown, and XML files.
Version is tracked in composer.json and CHANGELOG.md only.

Authored-by: Moko Consulting

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-11 16:51:00 -05:00
Jonathan Miller 34aace2638 refactor: remove docs/ — wiki-first, relocate non-doc files
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
- Remove 52 markdown files from docs/ (all content lives in Gitea wiki)
- Relocate legal_doc_generator.html → tools/
- Relocate mokostandards-schema.xsd → templates/schemas/
- Fix XSD path in MokoStandardsParser.php
- Update index.md and README.md to reference wiki

Authored-by: Moko Consulting

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-11 16:40:53 -05:00
Jonathan Miller 993f77d5a8 feat: major version bump 05.00.00 — CHANGELOG, Grafana 2-col layout
Branch Policy Check / Verify merge target (pull_request) Successful in 0s
- Bump platform version 04.05.00 → 05.00.00 across all 58 definition/config files
- Add CHANGELOG.md with full release history
- Add MokoWaaS Grafana dashboard template (2 columns per row)

Authored-by: Moko Consulting

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-11 16:34:10 -05:00
jmiller d318d5e854 chore: add PR branch policy check workflow [skip ci] 2026-05-11 17:16:03 +00:00
jmiller c511847fef feat: centralized MokoWaaS dashboard for all Joomla sites 2026-05-11 16:31:09 +00:00
Jonathan Miller aa1800e2f6 feat: comprehensive repo health check updates
Branch Protection Setup / Apply Branch Protection Rules (push) Successful in 24s
Sync Wikis to GitHub / Export wikis to GitHub (push) Successful in 7s
- Security: flag renovate.json as disallowed (removed from ecosystem)
- Disallowed: add .claude/, .mcp.json, renovate.json, profile.ps1
- Manifest: check .gitignore has required exclusions (.claude/, TODO.md, *.min.css/js)
- Manifest: check CLAUDE.md has project context + MokoStandards reference
- Updated scoring and header documentation

Authored-by: Moko Consulting
2026-05-10 15:07:17 -05:00
Jonathan Miller 15de3eed96 feat: add CLAUDE.md to repo health check, flag unwanted files
- CLAUDE.md now required (5 pts)
- Makefile now required (3 pts)
- Removed profile.ps1 and .mcp.json from required files
- Added negative checks: .mcp.json, TODO.md, renovate.json, .claude/
  flagged if committed

Authored-by: Moko Consulting
2026-05-10 15:03:46 -05:00
jmiller f5d35e10d9 docs: add CLAUDE.md for Claude Code context [skip ci] 2026-05-10 19:55:19 +00:00
jmiller 8245b5fd10 chore: add .moko-platform manifest [skip ci] 2026-05-10 19:51:08 +00:00
jmiller dd6eb8fc24 chore: remove deprecated .mokostandards (now .moko-platform) [skip ci] 2026-05-10 19:48:57 +00:00
Jonathan Miller 8abc30835c feat: add cleanup script to remove .claude/ and .mcp.json from repos
Scans all repos via Gitea API and deletes .claude/ directories and
.mcp.json files that were accidentally committed. These are local
workspace configs and should be gitignored.

Authored-by: Moko Consulting
2026-05-10 14:47:04 -05:00
Jonathan Miller aeb574980b feat: auto-discover all repos with wikis across all orgs
Sync Wikis to GitHub / Export wikis to GitHub (push) Successful in 8s
No more hardcoded repo list — queries Gitea API for all orgs and
repos that have wikis enabled. Falls back to hardcoded list if
no GITEA_TOKEN is set.

Authored-by: Moko Consulting
2026-05-09 19:15:02 -05:00
Jonathan Miller de1fef1de6 feat: daily wiki sync workflow — mirrors all Gitea wikis to GitHub
- sync-wikis.yml: runs daily at 5am UTC via cron, also manual dispatch
- sync-wikis-to-github.sh: exports all project wikis to consolidated
  mokoconsulting-tech/wiki repo on GitHub, organized by project folder

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-09 18:14:44 -05:00
Jonathan Miller 61d3f16675 feat: add CHANGELOG [Unreleased] section check to repo health (5 pts) 2026-05-09 18:05:11 -05:00
Jonathan Miller 5e894a2c8f feat: add wiki health check and GitHub wiki mirror sync
- validate/check_wiki_health.php — checks Home page, MokoStandards ref
- scripts/sync-wikis-to-github.sh — mirrors Gitea wikis to GitHub
- MCP tools: standards_check_wiki, standards_sync_wikis

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-09 17:58:20 -05:00
Jonathan Miller 395921282e feat: add client platform type with detection and structure definition
Client repos (WaaS) are managed Joomla sites — not extensions. They have
src/ with site structure PLUS deployment configs (sftp-config, monitoring,
sync scripts). Detection prioritizes client over joomla when deployment
markers are present.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-09 17:40:20 -05:00
Jonathan Miller 0e82802f0c feat(mcp): add mokostandards MCP server with 24 governance tools
Embeds an MCP server in mcp/ that exposes MokoStandards CLI tools as
AI assistant tools: platform detection, repo health checks, validation
(structure, headers, secrets, changelog, version consistency, enterprise
readiness, drift scan), Joomla/Dolibarr-specific checks, definitions
browser, policy/guide reader, and release notes generation.

Also adds McpServerPlugin, MCP platform detection, and MCP workflow
templates.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-07 15:44:10 -05:00
Jonathan Miller 2dc43603da chore: add cascade, gitleaks, renovate, and updated branch protections to definitions [skip ci]
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-07 15:36:04 -05:00
Jonathan Miller 38c2536c7b feat: add PHPStan, Gitleaks, and Renovate — templates, workflows, and docs [skip ci]
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-07 14:56:02 -05:00
Jonathan Miller 4e17ccf1f7 docs: update cascade docs for v2 multi-branch support [skip ci] 2026-05-07 14:32:11 -05:00
Jonathan Miller 5d6d9536a2 docs: add cascade and branch protection workflow documentation [skip ci]
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-07 14:26:48 -05:00
Jonathan Miller 5520aecc6f fix: remove gitea-actions[bot] from push whitelist (not a real user) [skip ci] 2026-05-07 14:13:32 -05:00
Jonathan Miller 671027c4f4 fix: delete-then-create branch protection rules to avoid 422 [skip ci]
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-07 14:10:44 -05:00
Jonathan Miller a6a9b8920e ci: add branch protection setup workflow [skip ci] 2026-05-07 14:04:50 -05:00
Jonathan Miller 57d05a74bd refactor: simplify platform types across definitions and sync engine
Old → New:
  crm-module → dolibarr
  crm-platform → platform
  waas-component → joomla
  joomla-template → joomla (merged)
  default-repository → generic
  standards-repository → standards
  client-site → client
  generic-repository → generic

Updated: definitions, RepositorySynchronizer.php, WORKFLOW_STANDARDS.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-05 15:29:57 -05:00
Jonathan Miller bcfd1fb029 chore: set platform to standards
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-05 15:22:56 -05:00
Jonathan Miller 89eec33c7b docs: fix stale workflow and client repo references
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-05 15:02:27 -05:00
Jonathan Miller 3f002a5996 docs: update workflow standards — minor bump policy, client changes, cascade
- Version policy: stable=minor bump (was major), pre-release=patch
- Client repos: 10 workflows (no update-server, no updates.xml)
- Cascade delete documented
- Release naming with element name documented
- Full changelog updated

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-05 14:50:37 -05:00
Jonathan Miller 46a87d2a98 fix: update mokostandards xmlns to point to MokoStandards-API repo
Schema documentation lives alongside the API implementation.
Updated namespace URI in spec docs, XSD schema, PHP parser,
XML template, and self-referencing .mokostandards manifest.

Old: https://standards.mokoconsulting.tech/mokostandards/1.0
New: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-05 14:44:17 -05:00
Jonathan Miller fe38765d03 feat: add client-site definition
Defines standard structure for client Joomla site repos:
- No updates.xml (not an installable extension)
- No update-server workflow
- Has sync-media.yml for bidirectional SFTP media sync
- 10 workflows (vs 10+update-server for extensions)
- Required per-repo variables/secrets for media sync

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-04 14:04:55 -05:00
Jonathan Miller 3740c553da chore: remove templates/github — all CI/templates now in .gitea/
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-02 18:19:56 -05:00
Jonathan Miller 06b1a36320 docs: update architecture section — sync engine clones templates at runtime
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-02 18:18:23 -05:00
Jonathan Miller 2bfbc2d89d refactor: sync engine clones template repos at runtime for workflows
No longer references local templates/workflows/ — instead clones
the canonical template repo (Joomla/Dolibarr/Generic/Client) at
sync time to get the latest workflow files directly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-02 18:17:53 -05:00
Jonathan Miller c332c3ae5c chore: remove templates/workflows — canonical source is now template repos
Workflow templates live in:
- MokoStandards-Template-Joomla/.gitea/workflows/
- MokoStandards-Template-Dolibarr/.gitea/workflows/
- MokoStandards-Template-Client/.gitea/workflows/
- MokoStandards-Template-Generic/.gitea/workflows/

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-02 18:15:49 -05:00
gitea-actions[bot] f368f271b7 chore: enrich .mokostandards with build/deploy/scripts 2026-05-02 18:13:07 -05:00
Jonathan Miller a4fbcc0f87 refactor: update sync engine to use new canonical workflow sources
- Replace old .template files with actual workflow YMLs from template repos
- Update RepositorySynchronizer to use new 10/11-workflow standard
- Remove legacy shared workflows (enterprise-firewall, auto-assign, etc.)
- Joomla workflows sourced from MokoStandards-Template-Joomla
- Dolibarr workflows sourced from MokoStandards-Template-Dolibarr

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-02 18:12:50 -05:00
gitea-actions[bot] 0119834cef chore: update .mokostandards to XML format 2026-05-02 18:05:59 -05:00
Jonathan Miller abc08fb6f2 docs: update for consolidated Joomla template repo
- Update WORKFLOW_STANDARDS.md to reference MokoStandards-Template-Joomla
- Remove 6 obsolete sync definitions for deleted individual template repos
- Update sync commands to use unified template

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-02 17:41:28 -05:00
Jonathan Miller a9c1cd3c16 docs: update workflow standards with client sync-media and full repo list
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-02 17:26:28 -05:00
Jonathan Miller 86ccfdc64f docs: add WORKFLOW_STANDARDS.md + update definitions
- Document new 10-workflow standard for Joomla, 11 for Dolibarr
- Remove deploy.yml from definitions (deploy is manual only)
- Add pre-release.yml to definitions
- Update waas-component.tf: ci-dolibarr + publish-to-mokodolimods
- Canonical source is now template repos, not API repo

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-02 17:15:07 -05:00
Jonathan Miller c9735396a9 chore: remove template workflows from API repo (canonical source is template repos)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-02 17:08:18 -05:00
Jonathan Miller 7525486710 fix: add patch version bump to pre-release workflow
Repo Health / Access control (push) Successful in 1s
Repo Health / Release configuration (push) Failing after 3s
Repo Health / Scripts governance (push) Successful in 3s
Repo Health / Repository health (push) Failing after 3s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-02 17:02:39 -05:00
Jonathan Miller 1472dcb650 chore: remove auto-deploy workflow (deploy is manual only)
Repo Health / Access control (push) Successful in 2s
Repo Health / Release configuration (push) Failing after 3s
Repo Health / Scripts governance (push) Successful in 3s
Repo Health / Repository health (push) Failing after 2s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-02 16:42:53 -05:00
Jonathan Miller 93fe181e1b feat: add pre-release workflow for manual dev/alpha/beta/rc builds
Repo Health / Access control (push) Successful in 1s
Repo Health / Release configuration (push) Failing after 3s
Repo Health / Scripts governance (push) Successful in 3s
Repo Health / Repository health (push) Failing after 3s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-02 16:40:52 -05:00
Jonathan Miller 8a864a2eb4 feat: add pr-check, security-audit, notify, cleanup to workflow definitions
Repo Health / Access control (push) Successful in 1s
Repo Health / Release configuration (push) Failing after 3s
Repo Health / Scripts governance (push) Successful in 3s
Repo Health / Repository health (push) Failing after 3s
Expands standard workflow suite from 6 to 10 in both joomla-template.tf
and waas-component.tf definitions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-02 16:32:36 -05:00
Jonathan Miller 91fdd63fe9 feat: expand workflow suite (10 workflows from MokoOnyx)
Repo Health / Access control (push) Successful in 1s
Repo Health / Release configuration (push) Failing after 3s
Repo Health / Scripts governance (push) Successful in 3s
Repo Health / Repository health (push) Failing after 3s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-02 16:31:27 -05:00
Jonathan Miller efc7180b01 feat: add .gitea/workflows definitions to Joomla structure defs
Repo Health / Access control (push) Successful in 1s
Repo Health / Release configuration (push) Failing after 3s
Repo Health / Scripts governance (push) Successful in 2s
Repo Health / Repository health (push) Failing after 3s
Replaces old .github/workflows with the standard 6-workflow set
matching MokoOnyx: auto-release, ci-joomla, deploy, deploy-manual,
repo-health, update-server.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-02 16:23:13 -05:00
Jonathan Miller f51b3a97d9 feat: add Joomla workflow templates from MokoOnyx
Repo Health / Access control (push) Successful in 1s
Repo Health / Release configuration (push) Failing after 2s
Repo Health / Scripts governance (push) Successful in 3s
Repo Health / Repository health (push) Failing after 2s
Adds auto-release, ci-joomla, deploy, deploy-manual, repo-health,
and update-server workflows as standard templates.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-02 16:17:49 -05:00
jmiller cbbb4895bb fix: always emit <client> tag in UpdateXmlGenerator, map 0→site 1→administrator
Bulk Repository Sync / Sync Standards to Repositories (push) Successful in 1m16s
2026-04-30 15:01:03 +00:00
jmiller f04d57a416 fix: rewrite updates.xml.template with 5 stability channels, client, sha256, maintainer 2026-04-30 15:00:12 +00:00
Jonathan Miller b4b7947658 chore: add .mcp.json to .gitignore and untrack 2026-04-30 09:42:38 -05:00
Jonathan Miller f12f660641 chore: chore: cleanup 2026-04-26 23:11:26 -05:00
Jonathan Miller 8758570216 fix: migrate .mokostandards from .github/ to .gitea/ on Gitea
migrateMokoStandards() now checks both root and .github/.mokostandards
as sources, migrating to .gitea/.mokostandards when running on Gitea.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-26 22:47:01 -05:00
Jonathan Miller 62394838b5 chore: replace jmiller-moko with jmiller, move .mokostandards to .gitea/ 2026-04-26 22:30:35 -05:00
Jonathan Miller 65e3c6acb6 docs: update workflow architecture — .gitea only, stream tags, cascade, auto-detect [skip ci]
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-26 19:30:30 -05:00
Jonathan Miller f71c186e26 fix: replace jmiller-moko with jmiller across all templates
GitHub username jmiller-moko replaced with Gitea username jmiller in:
- Issue templates (assignees)
- CODEOWNERS
- dependabot.yml
- Workflow authorized user lists

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-26 18:48:13 -05:00
Jonathan Miller 7800eadbd7 fix: Gitea compatibility for artifact uploads and bulk sync
- Guard upload-artifact@v4 / download-artifact@v4 with
  github.server_url == 'https://github.com' so they skip on Gitea
- Add Gitea fallbacks (checkout or log message) where artifacts are used
- Make enforce-tags step continue-on-error so sync doesn't fail on tag issues
- Replace upload-artifact in bulk-repo-sync with step summary on Gitea
- Fix escaped variable references in bulk-repo-sync.yml

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-26 18:38:52 -05:00
Jonathan Miller df81c55084 chore: add profile.ps1 to .gitignore 2026-04-26 16:02:26 -05:00
Jonathan Miller 5548eae35d chore: add debug logging to workflow sync deduplication 2026-04-26 15:54:43 -05:00
Jonathan Miller 531e462d9d fix: use API repo root for template resolution in bulk sync
syncFilesToBranch was resolving template paths against $standardsRoot
(../MokoStandards) instead of $repoRoot (the API repo where templates
actually live). This caused all template-sourced entries to silently
fail with "Source file not found", resulting in 0 files synced.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-26 15:41:22 -05:00
Jonathan Miller 78c484c6a7 chore: add .mokostandards platform definition (default-repository) 2026-04-26 13:38:30 -05:00
Jonathan Miller ff07d0a563 fix: prevent self-referencing composer dependency in enterprise package
ensureComposerEnterprise() now skips repos whose composer.json name
matches 'mokoconsulting-tech/enterprise' to avoid the package requiring
itself. Also removes the re-added self-reference from composer.json.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-26 11:55:10 -05:00
Jonathan Miller 5c0cb98082 fix: resolve label names to IDs in GiteaAdapter::createIssue
Gitea API expects label IDs (int64) not names. When string labels are
passed, resolve them via listLabels() before posting. Fixes 422
Unprocessable Entity errors that were causing tracking issue creation
to fail and repos to be marked as skipped during bulk sync.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-26 11:53:38 -05:00
jmiller 6795b72fec chore: add mokoconsulting-tech/enterprise dependency 2026-04-26 16:35:54 +00:00
jmiller c3c427df14 chore: add TODO.md from MokoStandards 2026-04-26 16:35:53 +00:00
Jonathan Miller 11dc2206b7 fix: remove self-referencing dependency in composer.json
The package mokoconsulting-tech/enterprise cannot require itself.
This was causing composer install to fail in CI, blocking bulk-repo-sync.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-26 11:34:11 -05:00
Jonathan Miller 2a84875a4e fix: repair bulk sync — array assignment bug + add missing workflow mappings
- Line 876: change $entries = to $entries[] = (was overwriting all shared
  workflow entries, causing every repo to be skipped with empty result)
- Add deploy-rs, export-mysql, pull-from-dev to shared workflows (all platforms)
- Add deploy-dev, deploy-demo, deploy.yml to waas-component platform
- This fix restores the bulk-repo-sync ability to push workflow templates

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 00:16:38 -05:00
jmiller cc9c648696 chore: add mokoconsulting-tech/enterprise dependency 2026-04-23 23:39:04 +00:00
jmiller 5db19b1201 fix: remove tag_exists gate from Step 7 — blocks patch releases [skip ci] 2026-04-23 23:01:37 +00:00
jmiller bcec65d285 fix: remove already_released skip gate — blocks patch releases [skip ci] 2026-04-23 22:43:16 +00:00
jmiller 2e97c97006 docs: update update-server.md for push triggers, bare dev support, sync-to-main, and cascade channels 2026-04-23 19:57:28 +00:00
jmiller 764451d003 fix: add updates.xml sync-to-main step for non-main branches [skip ci] 2026-04-23 19:31:17 +00:00
jmiller 4c9bb73765 ci: remove DEV_FTP_SUFFIX — path is now set per repo as full absolute path 2026-04-23 19:18:59 +00:00
jmiller 57539c7592 feat: support separate SSH hosts for dev/live deploys
DEPLOY_SSH_HOST for dev, LIVE_SSH_HOST for live (falls back to DEPLOY_SSH_HOST)
2026-04-23 19:11:24 +00:00
jmiller e7ac5f2c0b fix: support bare dev branch + push triggers in update-server [skip ci] 2026-04-23 18:03:36 +00:00
jmiller 2f4420ce8b docs: document cascade release channels and dev-release workflow [skip ci] 2026-04-23 17:41:18 +00:00
jmiller 1311cacd2c chore: add joomla-api-mcp sync definition 2026-04-23 17:36:03 +00:00
jmiller 6fce7e6569 docs: add deploy.yml.template to Joomla workflow index 2026-04-23 17:29:01 +00:00
jmiller 7f5aa2f7f4 feat: add SSH rsync auto-deploy workflow template for client repos 2026-04-23 17:28:33 +00:00
Jonathan Miller 4d5d7edee5 feat: auto-push main to GitHub mirror after release (Step 10)
Ensures updates.xml on GitHub stays in sync with Gitea.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 07:23:53 -05:00
Jonathan Miller 94da1e3a51 fix: remove tar.gz from updates.xml in update-server template
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 02:17:31 -05:00
Jonathan Miller f850377f99 fix: remove tar.gz from updates.xml — Joomla may download it instead of ZIP, causing SHA mismatch
tar.gz is still built and uploaded as release asset for manual download,
but only ZIP appears in updates.xml with matching SHA-256.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 02:04:23 -05:00
Jonathan Miller e40de18dbb fix: switch back to direct API file update for updates.xml sync
PR-based sync fails with branch protection requiring reviews.
Direct API update bypasses protection for bot commits.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-22 21:30:38 -05:00
Jonathan Miller c244790e44 fix: PR sync always runs, cleans up stale branches first
Removed conditional on CURRENT_BRANCH — workflow may be on version/XX
after archiving. Deletes stale PR branch before creating fresh one.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-22 04:29:26 -05:00
Jonathan Miller 327ffc7032 feat: release workflows run on dedicated 'release' runner
Updated auto-release and update-server templates + docs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-22 04:14:16 -05:00
jmiller d736df870a docs: add client repository standards documentation
Covers naming conventions, directory structure, privacy rules,
workflow profile, update server priority, deployment methods,
and differences from standard Joomla repos.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-22 09:01:56 +00:00
jmiller 3e15d4d3b0 chore: remove job timeout from bulk-repo-sync (no rate limit on Gitea)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-22 08:52:35 +00:00
Jonathan Miller 87ba8bc1c7 fix: install PHP+Composer if missing (works on any runner image)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-22 03:52:25 -05:00
jmiller 8c3eb17922 fix: remove duplicate mangled enforce-tags step from workflow
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-22 08:50:45 +00:00
jmiller c78cd167ea fix: repair mangled YAML in bulk-repo-sync tag enforcement step
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-22 08:46:59 +00:00
jmiller 14c4408e8d docs: codify Gitea-first update server priority policy
- docs/workflows/update-server.md: added Update Server Priority section
  explaining why Gitea must be priority 1 (source of truth, self-hosted,
  GitHub mirrors may lag)
- templates: updated CLAUDE.md and copilot-instructions templates for
  Joomla extensions with the priority rule

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-22 08:37:05 +00:00
jmiller ae0d233b93 feat: add tag enforcement to bulk-repo-sync
automation/enforce_tags.sh ensures all repos have the 5 standard
release channel tags (development, alpha, beta, release-candidate,
stable) and removes non-standard tags. Runs as part of the monthly
bulk sync and can be called standalone.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-22 08:29:03 +00:00
Jonathan Miller c3e989d150 feat: sync updates.xml to main via PR (respects branch protection)
Creates chore/update-xml-<version> branch, updates file, creates PR,
auto-merges, cleans up branch. Replaces direct API file push.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-22 03:16:21 -05:00
Jonathan Miller d146b5d51e fix: derive element from XML filename, not display name
Plugins like MokoWaaS have display name "System - MokoWaaS" but
element should be "mokowaas" (from mokowaas.xml filename).
Falls back to repo name for generic filenames like templateDetails.xml.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-22 02:45:31 -05:00
Jonathan Miller 4cf967f92b fix: stream-based tags (stable not vXX), derive element from repo name
- release_tag=stable instead of v${MAJOR}
- download URLs use /stable/ path
- Element fallback uses repo name not display name
- Updated channel-to-workflow docs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-22 02:19:03 -05:00
Jonathan Miller 4d99ab9a4e fix: git push -u origin HEAD for version branch (no upstream)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 21:33:11 -05:00
Jonathan Miller 617344c4d7 fix: GH_MIRROR_TOKEN → GH_TOKEN in all templates
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 21:12:12 -05:00
Jonathan Miller b57de90cef fix: add VERSION header to updates.xml in all workflow templates
Auto-release and update-server now write the copyright + VERSION
comment header when generating/rebuilding updates.xml.
Updated updates.xml.template scaffold to match.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 21:09:04 -05:00
Jonathan Miller dbd7ec8ae6 fix: hardcode MokoStandards-API branch to main (remove {{standards_branch}} placeholder)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 21:03:21 -05:00
Jonathan Miller f30c0dc9f9 docs: update multi-channel architecture — cascading channel updates
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 16:50:10 -05:00
Jonathan Miller dcd22dcfdc feat: cascading update channels — stable updates all, rc updates rc+below, etc
Channels cascade downward:
- stable → development, alpha, beta, rc, stable
- rc → development, alpha, beta, rc
- beta → development, alpha, beta
- alpha → development, alpha
- development → development

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 16:49:33 -05:00
Jonathan Miller adcbd2d2f4 chore: add .claude-worktree*/ to all gitignore templates
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 14:20:56 -05:00
Jonathan Miller 14b4477ff2 docs: document auto-bump on all branches in multi-channel architecture
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 12:30:21 -05:00
Jonathan Miller 032c32637f feat: auto-bump patch on all branches including dev
Previously dev branches were excluded from auto-bump. Now all
stability branches (dev, alpha, beta, rc) bump patch automatically.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 12:26:57 -05:00
Jonathan Miller 16a86a94b7 docs: add multi-channel updates.xml architecture, update Joomla template listings
- Add Multi-Channel updates.xml Architecture section to README.md
- Document auto-release.yml.template and update-server.yml.template
- Update joomla/index.md with current template inventory

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 12:17:46 -05:00
Jonathan Miller b68a23622a fix: remove patch 00 skip in auto-release template, all patches release
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 12:11:31 -05:00
Jonathan Miller 005ae12598 feat: MySQL export reads from config files, hardcode jmiller permissions
export-mysql.yml.template:
- Reads MySQL credentials from remote config files automatically:
  - Joomla: configuration.php ($user, $password, $db)
  - Dolibarr: conf/conf.php ($dolibarr_main_db_*)
- No MySQL secrets needed — credentials come from the app config
- Auto-detects platform (Joomla vs Dolibarr)
- Removed DEV_MYSQL_PASSWORD/DEMO_MYSQL_PASSWORD secret requirements

Permission hardcoding:
- Added ALLOWED_USERS="jmiller gitea-actions[bot]" to:
  deploy-demo, deploy-dev, deploy-rs, branch-freeze templates

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 17:18:48 -05:00
Jonathan Miller 3834781899 feat: add pull-from-dev and export-mysql workflow templates
pull-from-dev.yml.template:
- Downloads files from dev server via rsync/SSH into repo src/
- Configurable via DEV_SSH_HOST, DEV_SSH_USERNAME, DEV_PULL_PATH vars
- Auth via DEV_SSH_KEY secret
- Dry-run mode, branch selection, diff preview

export-mysql.yml.template:
- Exports MySQL database from dev or demo server
- Supports both Joomla and Dolibarr environments
- Sanitizes PII: passwords (bcrypt), emails, sessions, API keys, tokens
- Preserves admin/moko emails, strips everything else
- Dolibarr-specific: clears api_key, pass_crypted, ldap_pass, oauth secrets
- Saves as artifact (30d retention) or commits to sql/exports/
- Configurable per environment (dev/demo) via org or repo variables

Required variables (org or repo):
- DEV_SSH_HOST, DEV_SSH_PORT, DEV_SSH_USERNAME
- DEV_MYSQL_DATABASE, DEV_MYSQL_USER
- Secrets: DEV_SSH_KEY, DEV_MYSQL_PASSWORD

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 15:02:01 -05:00
Jonathan Miller c00a04087f Fix: protected files skip entirely before stale token check
Protected files (like updates.xml) were being overwritten because
the stale-token check ran AFTER the canOverwrite gate. Now protected
files continue (skip) immediately, even with --force.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 14:10:14 -05:00
Jonathan Miller 2b9bfb032e Protect updates.xml from bulk sync overwrite
Set protected=true, remove template reference. updates.xml is managed
by the release workflow, not bulk sync — sync was replacing it with
a stub template containing {{EXTENSION_NAME}} placeholders.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 13:07:35 -05:00
Jonathan Miller b9109c51bc docs: update release cycle — Gitea-only pre-release, dual stable downloads
- Added platform distribution table (stable=dual, pre-release=Gitea only)
- Updated all example URLs from GitHub to Gitea
- Stable gets dual <downloadurl> (Gitea + GitHub)
- RC/Beta/Alpha/Dev get single <downloadurl> (Gitea only)
- Updated targetplatform to [56].*
- Updated Dolibarr update.txt URL to Gitea
- Removed sha256/client fields from examples (not used)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 13:04:32 -05:00
Jonathan Miller 0f9f110c2d Gitea-primary: update definitions, sync lib, token guidance
- waas-component.tf: 27 lines — GitHub URLs→Gitea, GA_TOKEN guidance,
  gitea-actions[bot], jmiller username
- joomla-template.tf: same pattern
- RepositorySynchronizer.php: jmiller-moko→jmiller

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 12:45:23 -05:00
Jonathan Miller 4cf931e7a3 fix: align updates.xml template with MokoCassiopeia format
- Removed copyright/FILE INFORMATION header (not needed in synced XML)
- Hardcoded org names: MokoConsulting (Gitea), mokoconsulting-tech (GitHub)
- Download URLs formatted with line breaks matching MokoCassiopeia
- Target platform: [56].* (matches Joomla 5.x and 6.x)
- PHP minimum: 8.1 (matching live repos)
- Removed {{GITEA_ORG}}/{{GITHUB_ORG}} tokens — orgs are fixed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 01:01:26 -05:00
Jonathan Miller a7f758f888 fix: remove self-require and fix script paths in composer.json
- Removed mokoconsulting-tech/enterprise self-reference from require
  (package cannot require itself)
- Fixed phpcs/phpstan script paths: api/ → lib/ validate/ automation/

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 00:55:26 -05:00
Jonathan Miller 7b863f690d fix: remove all stale api/ path references across PHP codebase
Updated ~60 files: comments, usage docs, SCRIPT_PATH constants,
wrapper paths, require paths, error messages, and help text.
All api/validate/ → validate/, api/automation/ → automation/, etc.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 19:18:22 -05:00
Jonathan Miller bd53fe834f feat: add gitignore validation, move bulk-repo-sync workflow here
- Add REQUIRED_GITIGNORE_ENTRIES constant with mandatory patterns:
  Sublime project/workspace, sftp-config, IDE dirs, secrets, vendor, logs
- Add validateGitignoreEntries() method for checking required entries
- mergeGitConfigFile() still appends missing entries (non-destructive)
- Add .gitea/workflows/bulk-repo-sync.yml (moved from MokoStandards)
  - Runs from this repo directly (checkout self, not remote)
  - Org updated to MokoConsulting (Gitea)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 18:17:24 -05:00
Jonathan Miller 784f423973 Fix remaining --jq, --paginate, --input flags in workflow templates
branch-freeze, repository-cleanup, manage-repo-templates converted
from gh CLI flags to curl/jq equivalents.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 17:03:33 -05:00
Jonathan Miller 4742dfcbec fix: rename update.xml → updates.xml across all definitions and templates
Standardizes the Joomla update server filename to `updates.xml` (plural)
across all .tf definitions, workflow templates, and automation scripts.
The singular `update.xml` was inconsistent with the Joomla convention
and the updates.xml.template already in use.

Files fixed: 16 (definitions, templates, automation scripts)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 15:39:21 -05:00
Jonathan Miller 5dff3346f0 Fix auto-release template: use Gitea API for main sync, auth push URL
- Replace git push to main with Gitea contents API (bypasses branch protection)
- Add authenticated push URL step after checkout
- Matches MokoCassiopeia release.yml pattern

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 12:53:23 -05:00
Jonathan Miller 029033c2f6 Fix: set authenticated push URL in auto-release template for branch protection 2026-04-18 12:34:43 -05:00
Jonathan Miller 700e0abaac Fix: auto-release pushes updates.xml to main for update server
When releasing from a non-main branch, updates.xml is cherry-picked
to main so the Joomla update server always serves current data.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 12:02:21 -05:00
Jonathan Miller bbadbfd2ad Fix: jmiller-moko→jmiller, --jq→pipe jq, github-actions→gitea-actions
Remaining cleanup across 12 workflow templates:
- repo_health, auto-assign, auto-dev-issue, branch-freeze,
  deploy-*, repository-cleanup, terraform templates

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 11:39:44 -05:00
Jonathan Miller c3fe454eb6 Fix: remove sha256: prefix from update XML templates (Joomla expects raw hex) 2026-04-18 11:33:30 -05:00
467 changed files with 17059 additions and 32289 deletions
+8
View File
@@ -265,6 +265,11 @@ venv/
*.coverage
hypothesis/
# ============================================================
# Local wiki clone (not version controlled)
# ============================================================
wiki/
# ============================================================
# Dolibarr (base + runtime)
# ============================================================
@@ -682,6 +687,7 @@ modulebuilder.txt
!/bin/moko
/cache/*
/cli/*
!/cli/*.php
/components/com_ajax/*
/components/com_banners/*
/components/com_config/*
@@ -1062,3 +1068,5 @@ terraform.rc
# but can be ignored if you want flexibility across different platforms
# !.terraform.lock.hcl
logs/validation/*.md
profile.ps1
.mcp.json
@@ -7,8 +7,8 @@ contact_links:
- name: 💬 Ask a Question
url: https://mokoconsulting.tech/
about: Get help or ask questions through our website
- name: 📚 MokoStandards Documentation
url: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards
- name: 📚 moko-platform Documentation
url: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
about: View our coding standards and best practices
- name: 🔒 Report a Security Vulnerability
url: https://git.mokoconsulting.tech/mokoconsulting-tech/.github-private/security/advisories/new
@@ -37,7 +37,7 @@ If you have ideas about how this could be implemented, share them here:
Add any other context, mockups, or screenshots about the feature request here.
## Relevant Standards
Does this relate to any standards in [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards)?
Does this relate to any standards in [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform)?
- [ ] Accessibility (WCAG 2.1 AA)
- [ ] Localization (en_US/en_GB)
- [ ] Security best practices
@@ -3,7 +3,7 @@ name: Question
about: Ask a question about usage, features, or best practices
title: '[QUESTION] '
labels: ['question']
assignees: ['jmiller-moko']
assignees: ['jmiller']
---
@@ -35,7 +35,7 @@ Use this template only for:
<!-- Describe how this could be addressed -->
## Standards Reference
Does this relate to security standards in [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards)?
Does this relate to security standards in [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform)?
- [ ] SPDX license identifiers
- [ ] Secret management
- [ ] Dependency security
@@ -3,7 +3,7 @@ name: Version Bump
about: Request or track a version change
title: '[VERSION] '
labels: 'version, type: version'
assignees: 'jmiller-moko'
assignees: 'jmiller'
---
## Version Change
+246
View File
@@ -0,0 +1,246 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# SPDX-License-Identifier: GPL-3.0-or-later
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Automation
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /.gitea/workflows/branch-protection.yml
# BRIEF: Apply standardised branch protection rules to all governed repositories
#
# +========================================================================+
# | BRANCH PROTECTION SETUP |
# +========================================================================+
# | |
# | Applies protection rules for: main, dev, rc/*, beta/*, alpha/* |
# | |
# | main — Require PR, block rejected reviews, no force push |
# | dev — Allow push, no force push, no delete |
# | rc/* — Allow push, no force push, no delete |
# | beta/* — Allow push, no force push, no delete |
# | alpha/* — Allow push, no force push, no delete |
# | |
# | jmiller has override authority on all branches. |
# | |
# +========================================================================+
name: Branch Protection Setup
on:
schedule:
- cron: '0 2 * * 1' # Weekly Monday 02:00 UTC
workflow_dispatch:
inputs:
dry_run:
description: 'Preview mode (no changes)'
required: false
type: boolean
default: false
repos:
description: 'Comma-separated repo names (empty = all governed repos)'
required: false
type: string
default: ''
env:
GITEA_URL: https://git.mokoconsulting.tech
GITEA_ORG: MokoConsulting
permissions:
contents: read
jobs:
protect:
name: Apply Branch Protection Rules
runs-on: ubuntu-latest
steps:
- name: Determine target repos
id: repos
env:
GA_TOKEN: ${{ secrets.GA_TOKEN }}
run: |
API="${GITEA_URL}/api/v1"
# Platform/standards/infra repos to exclude
EXCLUDE="gitea-org-config org-profile gitea-private .mokogitea-private MokoStandards moko-platform MokoTesting"
EXCLUDE="$EXCLUDE MokoStandards-Template-Client MokoStandards-Template-Dolibarr MokoStandards-Template-Generic MokoStandards-Template-Joomla MokoDoliProjTemplate"
if [ -n "${{ inputs.repos }}" ]; then
# User-specified repos
REPOS=$(echo "${{ inputs.repos }}" | tr ',' ' ')
else
# Fetch all org repos
PAGE=1
REPOS=""
while true; do
BATCH=$(curl -sS \
-H "Authorization: token ${GA_TOKEN}" \
"${API}/orgs/${GITEA_ORG}/repos?page=${PAGE}&limit=50" \
| jq -r '.[].name // empty')
[ -z "$BATCH" ] && break
REPOS="$REPOS $BATCH"
PAGE=$((PAGE + 1))
done
# Filter out excluded repos
FILTERED=""
for REPO in $REPOS; do
SKIP=false
for EX in $EXCLUDE; do
if [ "$REPO" = "$EX" ]; then
SKIP=true
break
fi
done
if [ "$SKIP" = "false" ]; then
FILTERED="$FILTERED $REPO"
fi
done
REPOS="$FILTERED"
fi
echo "repos=$REPOS" >> "$GITHUB_OUTPUT"
COUNT=$(echo "$REPOS" | wc -w)
echo "📋 Target repos (${COUNT}): $REPOS"
- name: Apply protection rules
env:
GA_TOKEN: ${{ secrets.GA_TOKEN }}
DRY_RUN: ${{ inputs.dry_run || 'false' }}
run: |
API="${GITEA_URL}/api/v1"
REPOS="${{ steps.repos.outputs.repos }}"
SUCCESS=0
FAILED=0
SKIPPED=0
# ── Rule definitions ──────────────────────────────────────
# Each rule: NAME|JSON_BODY
# jmiller has override (force push + push whitelist) on all branches
RULE_MAIN='{
"rule_name": "main",
"enable_push": true,
"enable_push_whitelist": true,
"push_whitelist_usernames": ["jmiller"],
"enable_force_push": true,
"enable_force_push_allowlist": true,
"force_push_allowlist_usernames": ["jmiller"],
"enable_merge_whitelist": false,
"required_approvals": 0,
"dismiss_stale_approvals": true,
"block_on_rejected_reviews": true,
"block_on_outdated_branch": false,
"priority": 1
}'
RULE_DEV='{
"rule_name": "dev",
"enable_push": true,
"enable_push_whitelist": false,
"enable_force_push": true,
"enable_force_push_allowlist": true,
"force_push_allowlist_usernames": ["jmiller"],
"enable_merge_whitelist": false,
"required_approvals": 0,
"block_on_rejected_reviews": false,
"priority": 2
}'
RULE_RC='{
"rule_name": "rc/*",
"enable_push": true,
"enable_push_whitelist": false,
"enable_force_push": true,
"enable_force_push_allowlist": true,
"force_push_allowlist_usernames": ["jmiller"],
"enable_merge_whitelist": false,
"required_approvals": 0,
"block_on_rejected_reviews": false,
"priority": 3
}'
RULE_BETA='{
"rule_name": "beta/*",
"enable_push": true,
"enable_push_whitelist": false,
"enable_force_push": true,
"enable_force_push_allowlist": true,
"force_push_allowlist_usernames": ["jmiller"],
"enable_merge_whitelist": false,
"required_approvals": 0,
"block_on_rejected_reviews": false,
"priority": 4
}'
RULE_ALPHA='{
"rule_name": "alpha/*",
"enable_push": true,
"enable_push_whitelist": false,
"enable_force_push": true,
"enable_force_push_allowlist": true,
"force_push_allowlist_usernames": ["jmiller"],
"enable_merge_whitelist": false,
"required_approvals": 0,
"block_on_rejected_reviews": false,
"priority": 5
}'
RULES=("$RULE_MAIN" "$RULE_DEV" "$RULE_RC" "$RULE_BETA" "$RULE_ALPHA")
RULE_NAMES=("main" "dev" "rc/*" "beta/*" "alpha/*")
# ── Apply rules to each repo ──────────────────────────────
for REPO in $REPOS; do
echo ""
echo "═══ ${REPO} ═══"
for i in "${!RULES[@]}"; do
RULE="${RULES[$i]}"
NAME="${RULE_NAMES[$i]}"
if [ "$DRY_RUN" = "true" ]; then
echo " [DRY RUN] Would apply rule: ${NAME}"
SKIPPED=$((SKIPPED + 1))
continue
fi
# Delete existing rule if present (idempotent recreate)
ENCODED_NAME=$(echo "$NAME" | sed 's|/|%2F|g')
curl -sS -o /dev/null -w "" \
-X DELETE \
-H "Authorization: token ${GA_TOKEN}" \
"${API}/repos/${GITEA_ORG}/${REPO}/branch_protections/${ENCODED_NAME}" 2>/dev/null || true
# Create rule
RESPONSE=$(curl -sS -w "\n%{http_code}" \
-X POST \
-H "Authorization: token ${GA_TOKEN}" \
-H "Content-Type: application/json" \
-d "$RULE" \
"${API}/repos/${GITEA_ORG}/${REPO}/branch_protections")
HTTP=$(echo "$RESPONSE" | tail -1)
BODY=$(echo "$RESPONSE" | sed '$d')
if [ "$HTTP" = "201" ]; then
echo " ✅ ${NAME}"
SUCCESS=$((SUCCESS + 1))
else
echo " ❌ ${NAME} (HTTP ${HTTP}): $(echo "$BODY" | jq -r '.message // .' 2>/dev/null | head -1)"
FAILED=$((FAILED + 1))
fi
done
done
# ── Summary ───────────────────────────────────────────────
echo ""
echo "════════════════════════════════════════"
echo " ✅ Success: ${SUCCESS}"
echo " ❌ Failed: ${FAILED}"
echo " ⏭️ Skipped: ${SKIPPED}"
echo "════════════════════════════════════════"
if [ "$FAILED" -gt 0 ]; then
echo "::warning::${FAILED} rule(s) failed to apply"
fi
+135
View File
@@ -0,0 +1,135 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# SPDX-License-Identifier: GPL-3.0-or-later
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Automation
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /.gitea/workflows/bulk-repo-sync.yml
# BRIEF: Bulk repo sync — runs from API repo, syncs standards to all governed repos
name: Bulk Repository Sync
on:
schedule:
- cron: '0 0 1 * *'
workflow_dispatch:
inputs:
dry_run:
description: 'Preview mode (no changes)'
required: false
type: boolean
default: true
repos:
description: 'Comma-separated repo names (empty = all)'
required: false
type: string
default: ''
exclude:
description: 'Comma-separated repos to skip'
required: false
type: string
default: ''
force:
description: 'Force overwrite protected files'
required: false
type: boolean
default: false
permissions:
contents: write
issues: write
pull-requests: write
jobs:
bulk-sync:
name: Sync Standards to Repositories
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.1'
extensions: json, mbstring, curl
tools: composer
coverage: none
- name: Install Dependencies
run: composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader
- name: Build CLI Arguments
id: args
run: |
ARGS="--org MokoConsulting"
if [ "${{ inputs.dry_run }}" = "true" ] || [ "${{ gitea.event_name }}" = "schedule" ]; then
ARGS="$ARGS --dry-run"
fi
if [ -n "${{ inputs.repos }}" ]; then
ARGS="$ARGS --repos ${{ inputs.repos }}"
fi
if [ -n "${{ inputs.exclude }}" ]; then
ARGS="$ARGS --exclude ${{ inputs.exclude }}"
fi
if [ "${{ inputs.force }}" = "true" ]; then
ARGS="$ARGS --force"
fi
ARGS="$ARGS --yes"
echo "args=$ARGS" >> $GITHUB_OUTPUT
- name: Run Bulk Sync
run: |
echo "Running: php automation/bulk_sync.php ${{ steps.args.outputs.args }}"
php automation/bulk_sync.php ${{ steps.args.outputs.args }} 2>&1 | tee /tmp/bulk_sync.log
env:
GA_TOKEN: ${{ secrets.GA_TOKEN }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
GIT_PLATFORM: gitea
GITEA_URL: https://git.mokoconsulting.tech
GITEA_ORG: MokoConsulting
- name: Commit Updated Definitions
if: success() && inputs.dry_run != 'true'
run: |
if [ -n "$(git status --porcelain definitions/sync/)" ]; then
git config user.name "gitea-actions[bot]"
git config user.email "gitea-actions[bot]@git.mokoconsulting.tech"
git add definitions/sync/*.def.tf
git commit -m "chore: update synced repository definitions" || true
git push || true
fi
- name: Enforce Release Channel Tags
if: success()
continue-on-error: true
run: |
echo "Enforcing standard tags on all repos..."
if [ "${{ inputs.dry_run }}" = "true" ]; then
bash automation/enforce_tags.sh --dry-run || echo "Tag enforcement skipped (non-fatal)"
else
bash automation/enforce_tags.sh || echo "Tag enforcement had errors (non-fatal)"
fi
env:
GA_TOKEN: ${{ secrets.GA_TOKEN }}
GITEA_URL: https://git.mokoconsulting.tech
GITEA_ORG: MokoConsulting
- name: Upload Sync Log
if: always() && github.server_url == 'https://github.com'
uses: actions/upload-artifact@v4
with:
name: bulk-sync-log-${{ github.run_number }}
path: /tmp/bulk_sync.log
retention-days: 30
- name: Log Summary (Gitea)
if: always() && github.server_url != 'https://github.com'
run: |
if [ -f /tmp/bulk_sync.log ]; then
echo "## Sync Log (last 20 lines)" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
tail -20 /tmp/bulk_sync.log >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
fi
+25
View File
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
moko-platform Repository Manifest
Auto-generated by cleanup script.
See: https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home
-->
<moko-platform xmlns="https://standards.mokoconsulting.tech/moko-platform/1.0" schema-version="1.0">
<identity>
<name>moko-platform</name>
<org>MokoConsulting</org>
<description>Enterprise automation, validation, sync, and governance engine for all Moko Consulting repositories</description>
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
</identity>
<governance>
<platform>generic</platform>
<standards-version>05.00.00</standards-version>
<standards-source>https://git.mokoconsulting.tech/MokoConsulting/moko-platform</standards-source>
<last-synced>2026-05-10T19:51:08+00:00</last-synced>
</governance>
<build>
<language>HCL</language>
<package-type>generic</package-type>
<entry-point>src/</entry-point>
</build>
</moko-platform>
+97
View File
@@ -0,0 +1,97 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: moko-platform.CI
# INGROUP: moko-platform
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /.gitea/workflows/pr-branch-check.yml
# BRIEF: PR branch merge policy enforcement
#
# Enforces branch merge policy:
# feature/* → dev only
# fix/* → dev only
# hotfix/* → dev or main (emergency)
# dev → main only
# alpha/* → dev only
# beta/* → dev only
# rc/* → main only
name: Branch Policy Check
on:
pull_request:
types: [opened, synchronize, reopened, edited]
jobs:
check-target:
name: Verify merge target
runs-on: ubuntu-latest
steps:
- name: Check branch policy
run: |
HEAD="${{ github.head_ref }}"
BASE="${{ github.base_ref }}"
echo "PR: ${HEAD} → ${BASE}"
ALLOWED=true
REASON=""
case "$HEAD" in
feature/*|feat/*)
if [ "$BASE" != "dev" ]; then
ALLOWED=false
REASON="Feature branches must target 'dev', not '${BASE}'"
fi
;;
fix/*|bugfix/*)
if [ "$BASE" != "dev" ]; then
ALLOWED=false
REASON="Fix branches must target 'dev', not '${BASE}'"
fi
;;
hotfix/*)
if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'"
fi
;;
alpha/*|beta/*)
if [ "$BASE" != "dev" ]; then
ALLOWED=false
REASON="Pre-release branches must target 'dev', not '${BASE}'"
fi
;;
rc/*)
if [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="Release candidate branches must target 'main', not '${BASE}'"
fi
;;
dev)
if [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="Dev branch can only merge into 'main', not '${BASE}'"
fi
;;
esac
if [ "$ALLOWED" = false ]; then
echo "::error::${REASON}"
echo ""
echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "${REASON}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "Branch policy: OK (${HEAD} → ${BASE})"
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
+128
View File
@@ -0,0 +1,128 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Automation
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /.gitea/workflows/renovate.yml
# BRIEF: Run Renovate Bot across all governed repos for dependency updates
#
# +========================================================================+
# | RENOVATE DEPENDENCY UPDATES |
# +========================================================================+
# | |
# | Runs Renovate CLI against all governed repos to create PRs for |
# | outdated dependencies (composer, npm). |
# | |
# | - Scheduled: weekly Wednesday 04:00 UTC |
# | - Manual: dispatch with optional repo filter |
# | - Patch updates auto-merge, minor/major require review |
# | |
# +========================================================================+
name: Renovate Dependency Updates
on:
schedule:
- cron: '0 4 * * 3' # Weekly Wednesday 04:00 UTC
workflow_dispatch:
inputs:
repos:
description: 'Comma-separated repo names (empty = all governed)'
required: false
type: string
default: ''
dry_run:
description: 'Preview mode (log only, no PRs)'
required: false
type: boolean
default: false
env:
GITEA_URL: https://git.mokoconsulting.tech
GITEA_ORG: MokoConsulting
RENOVATE_VERSION: '39'
permissions:
contents: read
jobs:
renovate:
name: Run Renovate
runs-on: ubuntu-latest
steps:
- name: Determine target repos
id: repos
env:
GA_TOKEN: ${{ secrets.GA_TOKEN }}
run: |
API="${GITEA_URL}/api/v1"
EXCLUDE="gitea-org-config org-profile gitea-private .mokogitea-private MokoStandards moko-platform MokoTesting"
EXCLUDE="$EXCLUDE MokoStandards-Template-Client MokoStandards-Template-Dolibarr MokoStandards-Template-Generic MokoStandards-Template-Joomla MokoDoliProjTemplate"
if [ -n "${{ inputs.repos }}" ]; then
REPOS=$(echo "${{ inputs.repos }}" | tr ',' ' ')
else
PAGE=1
REPOS=""
while true; do
BATCH=$(curl -sS \
-H "Authorization: token ${GA_TOKEN}" \
"${API}/orgs/${GITEA_ORG}/repos?page=${PAGE}&limit=50" \
| jq -r '.[].name // empty')
[ -z "$BATCH" ] && break
REPOS="$REPOS $BATCH"
PAGE=$((PAGE + 1))
done
FILTERED=""
for REPO in $REPOS; do
SKIP=false
for EX in $EXCLUDE; do
[ "$REPO" = "$EX" ] && SKIP=true && break
done
[ "$SKIP" = "false" ] && FILTERED="$FILTERED $REPO"
done
REPOS="$FILTERED"
fi
# Build comma-separated list for Renovate
REPO_LIST=""
for REPO in $REPOS; do
if [ -n "$REPO_LIST" ]; then
REPO_LIST="${REPO_LIST},${GITEA_ORG}/${REPO}"
else
REPO_LIST="${GITEA_ORG}/${REPO}"
fi
done
echo "repo_list=$REPO_LIST" >> "$GITHUB_OUTPUT"
COUNT=$(echo "$REPOS" | wc -w)
echo "📋 Target repos (${COUNT})"
- name: Run Renovate
if: steps.repos.outputs.repo_list != ''
env:
RENOVATE_TOKEN: ${{ secrets.GA_TOKEN }}
RENOVATE_PLATFORM: gitea
RENOVATE_ENDPOINT: ${{ env.GITEA_URL }}/api/v1
RENOVATE_GIT_AUTHOR: 'Renovate Bot <renovate@mokoconsulting.tech>'
RENOVATE_REPOSITORIES: ${{ steps.repos.outputs.repo_list }}
RENOVATE_DRY_RUN: ${{ inputs.dry_run == 'true' && 'full' || 'null' }}
LOG_LEVEL: info
run: |
npx --yes renovate@${RENOVATE_VERSION} \
--platform=gitea \
--endpoint="${GITEA_URL}/api/v1" \
--token="${RENOVATE_TOKEN}" \
--git-author="Renovate Bot <renovate@mokoconsulting.tech>" \
--autodiscover=false \
${{ inputs.dry_run == 'true' && '--dry-run=full' || '' }} \
2>&1 | tee /tmp/renovate.log
echo "### Renovate Summary" >> $GITHUB_STEP_SUMMARY
grep -E "(INFO|WARN|ERROR)" /tmp/renovate.log | tail -30 >> $GITHUB_STEP_SUMMARY || true
+41
View File
@@ -0,0 +1,41 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Maintenance
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /.gitea/workflows/sync-wikis.yml
# BRIEF: Daily sync of all Gitea wikis to consolidated GitHub wiki repo
name: Sync Wikis to GitHub
on:
schedule:
- cron: '0 5 * * *' # Daily at 5am UTC
workflow_dispatch:
permissions:
contents: read
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
sync-wikis:
name: Export wikis to GitHub
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Sync all wikis
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
run: |
if [ -z "$GH_TOKEN" ]; then
echo "::error::GH_TOKEN secret not set"
exit 1
fi
bash scripts/sync-wikis-to-github.sh
+763
View File
@@ -0,0 +1,763 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Release
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# PATH: /templates/workflows/universal/auto-release.yml.template
# VERSION: 05.00.00
# BRIEF: Universal build & release detects platform from manifest.xml
#
# +========================================================================+
# | UNIVERSAL BUILD & RELEASE PIPELINE |
# +========================================================================+
# | |
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
# | |
# | Platform-specific: |
# | joomla: XML manifest, updates.xml, type-prefixed packages |
# | dolibarr: mod*.class.php, update.txt, dev version reset |
# | generic: README-only, no update stream |
# | |
# +========================================================================+
name: "Universal: Build & Release"
on:
pull_request:
types: [closed]
branches:
- main
paths:
- 'src/**'
- 'htdocs/**'
workflow_dispatch:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
permissions:
contents: write
jobs:
release:
name: Build & Release Pipeline
runs-on: release
if: >-
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch'
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.GA_TOKEN }}
fetch-depth: 0
- name: Setup moko-platform tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN }}"}}'
run: |
# Ensure PHP + Composer are available
if ! command -v composer &> /dev/null; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
fi
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
/tmp/moko-platform-api
cd /tmp/moko-platform-api
composer install --no-dev --no-interaction --quiet
# -- PLATFORM DETECTION ---------------------------------------------------
- name: Detect platform
id: platform
run: |
php /tmp/moko-platform-api/cli/manifest_read.php --path . --github-output
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true)
MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1 || true)
echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT"
echo "mod_file=${MOD_FILE}" >> "$GITHUB_OUTPUT"
- name: "Step 1: Read version"
id: version
run: |
VERSION=$(php /tmp/moko-platform-api/cli/version_read.php --path .)
if [ -z "$VERSION" ]; then
echo "::error::No VERSION in README.md"
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
MAJOR=$(echo "$VERSION" | cut -d. -f1)
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "release_tag=v${MAJOR}" >> "$GITHUB_OUTPUT"
echo "skip=false" >> "$GITHUB_OUTPUT"
echo "branch=version/${MAJOR}" >> "$GITHUB_OUTPUT"
- name: "Step 1b: Bump version"
id: bump
if: steps.version.outputs.skip != 'true'
run: |
MOKO_API="/tmp/moko-platform-api/cli"
BUMP=$(php ${MOKO_API}/version_bump.php --path . --minor)
VERSION=$(echo "$BUMP" | grep -oP '\d{2}\.\d{2}\.\d{2}$' || true)
[ -z "$VERSION" ] && VERSION=$(php ${MOKO_API}/version_read.php --path .)
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "Bumped to: ${VERSION}"
- name: Check if already released
if: steps.version.outputs.skip != 'true'
id: check
run: |
TAG="${{ steps.version.outputs.release_tag }}"
BRANCH="${{ steps.version.outputs.branch }}"
TAG_EXISTS=false
BRANCH_EXISTS=false
git rev-parse "$TAG" >/dev/null 2>&1 && TAG_EXISTS=true
git ls-remote --heads origin "$BRANCH" 2>/dev/null | grep -q "$BRANCH" && BRANCH_EXISTS=true
echo "tag_exists=$TAG_EXISTS" >> "$GITHUB_OUTPUT"
echo "branch_exists=$BRANCH_EXISTS" >> "$GITHUB_OUTPUT"
# Tag and branch may persist across patch releases — never skip
echo "already_released=false" >> "$GITHUB_OUTPUT"
# -- SANITY CHECKS -------------------------------------------------------
- name: "Sanity: Pre-release validation"
if: >-
steps.version.outputs.skip != 'true' &&
steps.check.outputs.already_released != 'true'
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
ERRORS=0
PLATFORM="${{ steps.platform.outputs.platform }}"
MANIFEST="${{ steps.platform.outputs.manifest }}"
MOD_FILE="${{ steps.platform.outputs.mod_file }}"
echo "## Pre-Release Sanity Checks (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# -- Version drift check (must pass before release) --------
README_VER=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1)
if [ "$README_VER" != "$VERSION" ]; then
echo "- Version drift: README says \`${README_VER}\` but releasing \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
else
echo "- Version consistent: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
fi
# Check CHANGELOG version matches
CL_VER=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' CHANGELOG.md 2>/dev/null | head -1)
if [ -n "$CL_VER" ] && [ "$CL_VER" != "$VERSION" ]; then
echo "- CHANGELOG drift: \`${CL_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
fi
# Check composer.json version if present
if [ -f "composer.json" ]; then
COMP_VER=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' composer.json 2>/dev/null | head -1)
if [ -n "$COMP_VER" ] && [ "$COMP_VER" != "$VERSION" ]; then
echo "- composer.json drift: \`${COMP_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
fi
fi
# Common checks
if [ ! -f "LICENSE" ]; then
echo "- Missing LICENSE file" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
else
echo "- LICENSE present" >> $GITHUB_STEP_SUMMARY
fi
if [ ! -d "src" ] && [ ! -d "htdocs" ]; then
echo "- Warning: No src/ or htdocs/ directory" >> $GITHUB_STEP_SUMMARY
else
echo "- Source directory present" >> $GITHUB_STEP_SUMMARY
fi
# -- Platform-specific checks --------
case "$PLATFORM" in
joomla)
if [ -n "$MANIFEST" ]; then
XML_VER=$(sed -n 's/.*<version>\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
if [ -n "$XML_VER" ] && [ "$XML_VER" != "$VERSION" ]; then
echo "- Manifest drift: \`${XML_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
else
echo "- Manifest version: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
fi
TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" 2>/dev/null)
echo "- Extension type: ${TYPE:-unknown}" >> $GITHUB_STEP_SUMMARY
else
echo "- No Joomla XML manifest (WaaS site)" >> $GITHUB_STEP_SUMMARY
fi ;;
dolibarr)
if [ -n "$MOD_FILE" ]; then
MOD_VER=$(sed -n "s/.*\\\$this->version = '\([^']*\)'.*/\1/p" "$MOD_FILE" 2>/dev/null | head -1)
if [ -n "$MOD_VER" ] && [ "$MOD_VER" != "$VERSION" ]; then
echo "- Module drift: \`${MOD_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
else
echo "- Module version: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
fi
else
echo "- No mod*.class.php found" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
fi
if [ ! -f "update.txt" ]; then
echo "- Missing update.txt" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
fi ;;
*) echo "- Generic platform no manifest checks" >> $GITHUB_STEP_SUMMARY ;;
esac
echo "" >> $GITHUB_STEP_SUMMARY
if [ "$ERRORS" -gt 0 ]; then
echo "**${ERRORS} error(s) — release may be incomplete**" >> $GITHUB_STEP_SUMMARY
else
echo "**All sanity checks passed**" >> $GITHUB_STEP_SUMMARY
fi
# -- STEP 2: Create or update version/XX.YY archive branch ---------------
# Always runs — every version change on main archives to version/XX.YY
- name: "Step 2: Version archive branch"
if: steps.check.outputs.already_released != 'true'
run: |
BRANCH="${{ steps.version.outputs.branch }}"
IS_MINOR="${{ steps.version.outputs.is_minor }}"
PATCH="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
PATCH_NUM=$(echo "$PATCH" | awk -F. '{print $3}')
# Check if branch exists
if git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then
git push origin HEAD:"$BRANCH" --force
echo "Updated archive branch: ${BRANCH} (patch ${PATCH_NUM})" >> $GITHUB_STEP_SUMMARY
else
git checkout -b "$BRANCH" 2>/dev/null || git checkout "$BRANCH"
git push origin "$BRANCH" --force
echo "Created archive branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
fi
# -- STEP 3: Set platform version ----------------------------------------
- name: "Step 3: Set platform version"
if: >-
steps.version.outputs.skip != 'true' &&
steps.check.outputs.already_released != 'true'
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
php /tmp/moko-platform-api/cli/version_set_platform.php \
--path . --version "$VERSION" --branch main
# -- STEP 4: Update version badges ----------------------------------------
- name: "Step 4: Update version badges"
if: steps.version.outputs.skip != 'true'
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
php /tmp/moko-platform-api/cli/badge_update.php --path . --version "${VERSION}" 2>/dev/null || true
- name: "Step 5: Write update stream"
if: >-
steps.version.outputs.skip != 'true' &&
steps.platform.outputs.platform == 'joomla'
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
php /tmp/moko-platform-api/cli/updates_xml_build.php \
--path . --version "${VERSION}" --stability stable \
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
--github-output
- name: Commit release changes
if: >-
steps.version.outputs.skip != 'true' &&
steps.check.outputs.already_released != 'true'
run: |
if git diff --quiet && git diff --cached --quiet; then
echo "No changes to commit"
exit 0
fi
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
# Set push URL with token for branch-protected repos
git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
git add -A
git commit -m "chore(release): build ${VERSION} [skip ci]" \
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
git push -u origin HEAD
# -- STEP 6: Create tag ---------------------------------------------------
- name: "Step 6: Create git tag"
if: >-
steps.version.outputs.skip != 'true' &&
steps.check.outputs.tag_exists != 'true' &&
steps.version.outputs.is_minor == 'true'
run: |
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
# Only create the major release tag if it doesn't exist yet
if ! git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then
git tag "$RELEASE_TAG"
git push origin "$RELEASE_TAG"
echo "Tag created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
else
echo "Tag ${RELEASE_TAG} already exists" >> $GITHUB_STEP_SUMMARY
fi
echo "Tag: ${TAG}" >> $GITHUB_STEP_SUMMARY
# -- STEP 7: Create or update Gitea Release --------------------------------
- name: "Step 7: Gitea Release"
if: >-
steps.version.outputs.skip != 'true'
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
BRANCH="${{ steps.version.outputs.branch }}"
MAJOR="${{ steps.version.outputs.major }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
# Reuse metadata from Step 5 (single source of truth)
EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}"
EXT_NAME="${{ steps.updates.outputs.ext_name }}"
EXT_TYPE="${{ steps.updates.outputs.ext_type }}"
EXT_FOLDER="${{ steps.updates.outputs.ext_folder }}"
# Fallbacks if Step 5 was skipped
if [ -z "$EXT_ELEMENT" ]; then
EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
fi
[ -z "$EXT_NAME" ] && EXT_NAME="${GITEA_REPO}"
NOTES=$(php /tmp/moko-platform-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null)
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
# Build release name: "Pretty Name VERSION (type_element-VERSION)"
TYPE_PREFIX=""
case "${EXT_TYPE}" in
plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;;
module) TYPE_PREFIX="mod_" ;;
component) TYPE_PREFIX="com_" ;;
template) TYPE_PREFIX="tpl_" ;;
library) TYPE_PREFIX="lib_" ;;
package) TYPE_PREFIX="pkg_" ;;
esac
RELEASE_NAME="${EXT_NAME} ${VERSION} (${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION})"
# Delete existing release if present (overwrite, not append)
EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
"${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true)
EXISTING_ID=$(echo "$EXISTING" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('id',''))" 2>/dev/null || true)
if [ -n "$EXISTING_ID" ]; then
curl -sS -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
"${API_BASE}/releases/${EXISTING_ID}" 2>/dev/null || true
curl -sS -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
"${API_BASE}/tags/${RELEASE_TAG}" 2>/dev/null || true
echo "Deleted previous stable release (id: ${EXISTING_ID})"
fi
# Create fresh release
curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
-H "Content-Type: application/json" \
"${API_BASE}/releases" \
-d "$(python3 -c "import json; print(json.dumps({
'tag_name': '${RELEASE_TAG}',
'name': '${RELEASE_NAME}',
'body': '''## ${VERSION} ($(date +%Y-%m-%d))\n${NOTES}''',
'target_commitish': '${BRANCH}'
}))")"
echo "Release created: ${RELEASE_NAME}" >> $GITHUB_STEP_SUMMARY
# -- STEP 8: Build Joomla install ZIP + SHA-256 checksum ------------------
- name: "Step 8: Build package and update checksum"
if: >-
steps.version.outputs.skip != 'true'
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
REPO="${{ github.repository }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
# All ZIPs upload to the major release tag (vXX)
RELEASE_JSON=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
"${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true)
RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
if [ -z "$RELEASE_ID" ]; then
echo "No release ${RELEASE_TAG} found — skipping ZIP upload"
exit 0
fi
# Find extension element name from manifest
MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true)
[ -z "$MANIFEST" ] && exit 0
# Reuse element from Step 5, with same fallback chain
EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}"
if [ -z "$EXT_ELEMENT" ]; then
EXT_ELEMENT=$(sed -n 's/.*<element>\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(sed -n 's/.*plugin="\([^"]*\)".*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]')
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
fi
# ZIP name: type_folder_element-VERSION (e.g. plg_system_mokojgdpc-01.01.00.zip)
EXT_TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
EXT_FOLDER=$(sed -n 's/.*<extension[^>]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
TYPE_PREFIX=""
case "${EXT_TYPE}" in
plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;;
module) TYPE_PREFIX="mod_" ;;
component) TYPE_PREFIX="com_" ;;
template) TYPE_PREFIX="tpl_" ;;
library) TYPE_PREFIX="lib_" ;;
package) TYPE_PREFIX="pkg_" ;;
esac
ZIP_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip"
TAR_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.tar.gz"
# -- Build install packages from src/ ----------------------------
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
[ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/"; exit 0; }
# ZIP package (type-aware via moko-platform PHP API)
php /tmp/moko-platform-api/cli/joomla_build.php --path . --version "${VERSION}" --output /tmp
# Match the expected ZIP_NAME for upload
BUILT_ZIP=$(ls /tmp/${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip 2>/dev/null | head -1 || true)
if [ -n "$BUILT_ZIP" ] && [ "$BUILT_ZIP" != "/tmp/${ZIP_NAME}" ]; then
mv "$BUILT_ZIP" "/tmp/${ZIP_NAME}"
fi
# tar.gz package (flat source archive)
tar -czf "/tmp/${TAR_NAME}" -C "$SOURCE_DIR" --exclude='.ftpignore' --exclude='sftp-config*' --exclude='*.ppk' --exclude='*.pem' --exclude='*.key' --exclude='.env*' .
ZIP_SIZE=$(stat -c%s "/tmp/${ZIP_NAME}" 2>/dev/null || stat -f%z "/tmp/${ZIP_NAME}" 2>/dev/null || echo "unknown")
TAR_SIZE=$(stat -c%s "/tmp/${TAR_NAME}" 2>/dev/null || stat -f%z "/tmp/${TAR_NAME}" 2>/dev/null || echo "unknown")
# -- Calculate SHA-256 for both ----------------------------------
SHA256_ZIP=$(sha256sum "/tmp/${ZIP_NAME}" | cut -d' ' -f1)
SHA256_TAR=$(sha256sum "/tmp/${TAR_NAME}" | cut -d' ' -f1)
# -- Delete existing assets with same name before uploading ------
ASSETS=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
"${API_BASE}/releases/${RELEASE_ID}/assets" 2>/dev/null || echo "[]")
for ASSET_NAME in "$ZIP_NAME" "$TAR_NAME"; do
ASSET_ID=$(echo "$ASSETS" | python3 -c "
import sys,json
assets = json.load(sys.stdin)
for a in assets:
if a['name'] == '${ASSET_NAME}':
print(a['id']); break
" 2>/dev/null || true)
if [ -n "$ASSET_ID" ]; then
curl -sf -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
"${API_BASE}/releases/${RELEASE_ID}/assets/${ASSET_ID}" 2>/dev/null || true
fi
done
# -- Upload both to release tag ----------------------------------
curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
-H "Content-Type: application/octet-stream" \
--data-binary @"/tmp/${ZIP_NAME}" \
"${API_BASE}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" > /dev/null 2>&1 || true
curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
-H "Content-Type: application/octet-stream" \
--data-binary @"/tmp/${TAR_NAME}" \
"${API_BASE}/releases/${RELEASE_ID}/assets?name=${TAR_NAME}" > /dev/null 2>&1 || true
# -- Update updates.xml with both download formats ---------------
if [ -f "updates.xml" ]; then
ZIP_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${ZIP_NAME}"
TAR_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${TAR_NAME}"
# Use Python to update only the stable entry's downloads + sha256
export PY_ZIP_URL="$ZIP_URL" PY_TAR_URL="$TAR_URL" PY_SHA="$SHA256_ZIP"
python3 << 'PYEOF'
import re, os
with open("updates.xml") as f:
content = f.read()
zip_url = os.environ["PY_ZIP_URL"]
tar_url = os.environ["PY_TAR_URL"]
sha = os.environ["PY_SHA"]
# Find the stable update block and replace its downloads + sha256
def replace_stable(m):
block = m.group(0)
# Replace downloads block
new_downloads = (
" <downloads>\n"
f" <downloadurl type=\"full\" format=\"zip\">{zip_url}</downloadurl>\n"
" </downloads>"
)
block = re.sub(r' <downloads>.*?</downloads>', new_downloads, block, flags=re.DOTALL)
# Add or replace sha256
if '<sha256>' in block:
block = re.sub(r' <sha256>.*?</sha256>', f' <sha256>{sha}</sha256>', block)
else:
block = block.replace('</downloads>', f'</downloads>\n <sha256>{sha}</sha256>')
return block
content = re.sub(
r' <update>.*?<tag>stable</tag>.*?</update>',
replace_stable,
content,
flags=re.DOTALL
)
with open("updates.xml", "w") as f:
f.write(content)
PYEOF
CURRENT_BRANCH="${{ github.ref_name }}"
git add updates.xml
git commit -m "chore(release): ZIP + tar.gz for ${VERSION} [skip ci]" \
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" || true
git push || true
# Sync updates.xml to main via direct API (always runs — may be on version/XX branch)
GA_TOKEN="${{ secrets.GA_TOKEN }}"
API="${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}"
FILE_SHA=$(curl -sf -H "Authorization: token ${GA_TOKEN}" \
"${API}/contents/updates.xml?ref=main" | jq -r '.sha // empty')
if [ -n "$FILE_SHA" ]; then
CONTENT=$(base64 -w0 updates.xml)
curl -sf -X PUT -H "Authorization: token ${GA_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/contents/updates.xml" \
-d "$(jq -n \
--arg content "$CONTENT" \
--arg sha "$FILE_SHA" \
--arg msg "chore: sync updates.xml ${VERSION} [skip ci]" \
--arg branch "main" \
'{content: $content, sha: $sha, message: $msg, branch: $branch}'
)" > /dev/null 2>&1 \
&& echo "updates.xml synced to main via API" \
|| echo "WARNING: failed to sync updates.xml to main"
else
echo "WARNING: could not get updates.xml SHA from main"
fi
fi
echo "### Packages" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Package | Size | SHA-256 |" >> $GITHUB_STEP_SUMMARY
echo "|---------|------|---------|" >> $GITHUB_STEP_SUMMARY
echo "| \`${ZIP_NAME}\` | ${ZIP_SIZE} | \`${SHA256_ZIP}\` |" >> $GITHUB_STEP_SUMMARY
echo "| \`${TAR_NAME}\` | ${TAR_SIZE} | \`${SHA256_TAR}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Release | \`${RELEASE_TAG}\` | |" >> $GITHUB_STEP_SUMMARY
echo "| Download | [${ZIP_NAME}](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${ZIP_NAME}) |" >> $GITHUB_STEP_SUMMARY
# -- STEP 8b: Update release description with changelog + SHA ----------------
- name: "Step 8b: Update release body with changelog and SHA"
if: steps.version.outputs.skip != 'true'
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}"
EXT_TYPE="${{ steps.updates.outputs.ext_type }}"
EXT_FOLDER="${{ steps.updates.outputs.ext_folder }}"
# Build TYPE_PREFIX to match Step 8's ZIP naming
TYPE_PREFIX=""
case "${EXT_TYPE}" in
plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;;
module) TYPE_PREFIX="mod_" ;;
component) TYPE_PREFIX="com_" ;;
template) TYPE_PREFIX="tpl_" ;;
library) TYPE_PREFIX="lib_" ;;
package) TYPE_PREFIX="pkg_" ;;
esac
ZIP_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip"
TAR_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.tar.gz"
# Get SHA from the built files
SHA256_ZIP=""
[ -f "/tmp/${ZIP_NAME}" ] && SHA256_ZIP=$(sha256sum "/tmp/${ZIP_NAME}" | cut -d' ' -f1)
SHA256_TAR=""
[ -f "/tmp/${TAR_NAME}" ] && SHA256_TAR=$(sha256sum "/tmp/${TAR_NAME}" | cut -d' ' -f1)
# Extract latest changelog entry (strip the ## header to avoid duplicate)
CHANGELOG=""
if [ -f "CHANGELOG.md" ]; then
CHANGELOG=$(sed -n "/^## \[*${VERSION}/,/^## \[*[0-9]/p" CHANGELOG.md | sed '$d' | sed '1d')
[ -z "$CHANGELOG" ] && CHANGELOG=$(sed -n '/^## /,/^## /p' CHANGELOG.md | sed '$d' | sed '1d' | head -30)
fi
# Build release body (single header, no duplicate from changelog)
BODY="## ${VERSION} ($(date +%Y-%m-%d))\n\n"
if [ -n "$CHANGELOG" ]; then
BODY="${BODY}${CHANGELOG}\n\n"
fi
BODY="${BODY}---\n\n### Checksums\n\n"
BODY="${BODY}| File | SHA-256 |\n|------|--------|\n"
[ -n "$SHA256_ZIP" ] && BODY="${BODY}| \`${ZIP_NAME}\` | \`${SHA256_ZIP}\` |\n"
[ -n "$SHA256_TAR" ] && BODY="${BODY}| \`${TAR_NAME}\` | \`${SHA256_TAR}\` |\n"
# Get release ID and update body
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
"${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null | \
python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "None" ]; then
python3 -c "
import json, urllib.request
body = '''$(printf '%b' "$BODY")'''
data = json.dumps({'body': body}).encode()
req = urllib.request.Request(
'${API_BASE}/releases/${RELEASE_ID}',
data=data,
headers={'Authorization': 'token ${{ secrets.GA_TOKEN }}', 'Content-Type': 'application/json'},
method='PATCH'
)
urllib.request.urlopen(req)
" 2>/dev/null && echo "Release body updated with changelog + SHA" >> $GITHUB_STEP_SUMMARY
fi
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
- name: "Step 9: Mirror release to GitHub"
if: >-
steps.version.outputs.skip != 'true' &&
steps.version.outputs.stability == 'stable' &&
secrets.GH_TOKEN != ''
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
MAJOR="${{ steps.version.outputs.major }}"
BRANCH="${{ steps.version.outputs.branch }}"
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
NOTES=$(php /tmp/moko-platform-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null || true)
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
echo "$NOTES" > /tmp/release_notes.md
EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/tags/$RELEASE_TAG" 2>/dev/null | jq -r ".tag_name // empty" || true)
if [ -z "$EXISTING" ]; then
gh release create "$RELEASE_TAG" \
--repo "$GH_REPO" \
--title "v${MAJOR} (latest: ${VERSION})" \
--notes-file /tmp/release_notes.md \
--target "$BRANCH" || true
else
gh release edit "$RELEASE_TAG" \
--repo "$GH_REPO" \
--title "v${MAJOR} (latest: ${VERSION})" || true
fi
# Upload assets to GitHub mirror
for PKG in /tmp/${EXT_ELEMENT:-pkg}-${VERSION}.*; do
if [ -f "$PKG" ]; then
_RELID=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/tags/$RELEASE_TAG" 2>/dev/null | jq -r ".id // empty")
[ -n "$_RELID" ] && curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" -H "Content-Type: application/octet-stream" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/${_RELID}/assets?name=$(basename $PKG)" --data-binary "@$PKG" > /dev/null 2>&1 || true
fi
done
echo "GitHub mirror updated: ${GH_REPO} ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
# -- STEP 10: Sync main branch to GitHub mirror ----------------------------
- name: "Step 10: Push main to GitHub mirror"
if: >-
steps.version.outputs.skip != 'true' &&
secrets.GH_TOKEN != ''
continue-on-error: true
run: |
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
git remote add github "https://x-access-token:${{ secrets.GH_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
git remote set-url github "https://x-access-token:${{ secrets.GH_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
git fetch origin main --depth=1
git push github origin/main:refs/heads/main --force 2>/dev/null \
&& echo "main branch pushed to GitHub mirror" \
|| echo "WARNING: GitHub mirror push failed"
# -- Clean up lesser pre-releases (cascade) ---------------------------------
# stable → deletes all | rc → beta,alpha,dev | beta → alpha,dev | alpha → dev
- name: "Delete lesser pre-release channels"
continue-on-error: true
run: |
php /tmp/moko-platform-api/cli/release_cascade.php \
--stability stable \
--token "${{ secrets.GA_TOKEN }}" \
--org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
--gitea-url "${GITEA_URL}" 2>/dev/null || true
- name: "Step 11: Delete and recreate dev branch from main"
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.GA_TOKEN }}"
# Delete dev branch
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
"${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
# Recreate dev from main (now includes version bump + changelog promotion)
curl -sf -X POST -H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
"${API_BASE}/branches" \
-d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
echo "Dev branch reset from main (keeps dev ahead after release)" >> $GITHUB_STEP_SUMMARY
# -- Dolibarr post-release: Reset dev version -----------------------------
- name: "Dolibarr: Reset dev version"
if: >-
steps.version.outputs.skip != 'true' &&
steps.platform.outputs.platform == 'dolibarr' &&
steps.platform.outputs.mod_file != ''
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.GA_TOKEN }}"
MOD_FILE="${{ steps.platform.outputs.mod_file }}"
ENCODED_PATH=$(echo "$MOD_FILE" | sed 's|^\./||' | python3 -c "import sys,urllib.parse; print(urllib.parse.quote(sys.stdin.read().strip()))")
FILE_RESP=$(curl -sf -H "Authorization: token ${TOKEN}" "${API_BASE}/contents/${ENCODED_PATH}?ref=dev" 2>/dev/null || true)
FILE_SHA=$(echo "$FILE_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
FILE_CONTENT=$(echo "$FILE_RESP" | python3 -c "import sys,json,base64; print(base64.b64decode(json.load(sys.stdin).get('content','')).decode())" 2>/dev/null || true)
if [ -n "$FILE_SHA" ] && [ -n "$FILE_CONTENT" ]; then
UPDATED=$(echo "$FILE_CONTENT" | sed "s/\$this->version = '[^']*'/\$this->version = 'development'/")
ENCODED=$(echo "$UPDATED" | base64 -w0)
curl -sf -X PUT -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/contents/${ENCODED_PATH}" \
-d "$(jq -n --arg content \"$ENCODED\" --arg sha \"$FILE_SHA\" --arg msg \"chore(version): reset dev version [skip ci]\" --arg branch \"dev\" '{content:$content,sha:$sha,message:$msg,branch:$branch}')" > /dev/null 2>&1 || true
fi
# -- Summary --------------------------------------------------------------
- name: Pipeline Summary
if: always()
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
PLATFORM="${{ steps.platform.outputs.platform }}"
if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
else
echo "" >> $GITHUB_STEP_SUMMARY
echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
fi
+213
View File
@@ -0,0 +1,213 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Maintenance
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# PATH: /templates/workflows/cascade-dev.yml.template
# VERSION: 02.00.00
# BRIEF: Forward-merge main → all open branches after every push to main
#
# +========================================================================+
# | CASCADE MAIN → ALL BRANCHES |
# +========================================================================+
# | |
# | Triggers on every push to main (PR merges, bot commits, etc.) |
# | |
# | 1. List all branches matching: dev, rc/*, beta/*, alpha/* |
# | 2. For each: create PR (main → branch), auto-merge if clean |
# | 3. On conflict: leave PR open for manual resolution |
# | |
# +========================================================================+
name: "Universal: Cascade Main → Dev"
on:
push:
branches:
- main
workflow_dispatch:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
permissions:
contents: write
pull-requests: write
jobs:
cascade:
name: Cascade main → branches
runs-on: ubuntu-latest
if: >-
!contains(github.event.head_commit.message, '[skip ci]') &&
!contains(github.event.head_commit.message, '[skip cascade]')
steps:
- name: Discover target branches
id: branches
env:
GA_TOKEN: ${{ secrets.GA_TOKEN }}
run: |
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
# Fetch all branches (paginated)
PAGE=1
ALL_BRANCHES=""
while true; do
BATCH=$(curl -sS \
-H "Authorization: token ${GA_TOKEN}" \
"${API}/branches?page=${PAGE}&limit=50" \
| jq -r '.[].name // empty')
[ -z "$BATCH" ] && break
ALL_BRANCHES="$ALL_BRANCHES $BATCH"
PAGE=$((PAGE + 1))
done
# Filter to cascade targets: dev, dev/*, rc/*, beta/*, alpha/*
TARGETS=""
for BRANCH in $ALL_BRANCHES; do
case "$BRANCH" in
dev|dev/*|rc/*|beta/*|alpha/*)
TARGETS="$TARGETS $BRANCH"
;;
esac
done
TARGETS=$(echo "$TARGETS" | xargs) # trim whitespace
if [ -z "$TARGETS" ]; then
echo "targets=" >> "$GITHUB_OUTPUT"
echo "️ No cascade target branches found"
else
echo "targets=$TARGETS" >> "$GITHUB_OUTPUT"
COUNT=$(echo "$TARGETS" | wc -w)
echo "📋 Found ${COUNT} target branch(es): ${TARGETS}"
fi
- name: Cascade to all target branches
if: steps.branches.outputs.targets != ''
env:
GA_TOKEN: ${{ secrets.GA_TOKEN }}
run: |
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
SHORT_SHA="${GITHUB_SHA:0:7}"
TARGETS="${{ steps.branches.outputs.targets }}"
SUCCESS=0
CONFLICTS=0
SKIPPED=0
FAILED=0
for BRANCH in $TARGETS; do
echo ""
echo "═══ main → ${BRANCH} ═══"
# Check if branch is already up to date
ENCODED_BRANCH=$(echo "$BRANCH" | sed 's|/|%2F|g')
RESPONSE=$(curl -sS \
-H "Authorization: token ${GA_TOKEN}" \
"${API}/compare/${ENCODED_BRANCH}...main")
AHEAD=$(echo "$RESPONSE" | jq '.total_commits // 0')
if [ "$AHEAD" -eq 0 ]; then
echo " ✅ Already up to date"
SKIPPED=$((SKIPPED + 1))
continue
fi
echo " ️ main is ${AHEAD} commit(s) ahead"
# Check for existing cascade PR
EXISTING=$(curl -sS \
-H "Authorization: token ${GA_TOKEN}" \
"${API}/pulls?state=open&head=${GITEA_ORG}:main&base=${ENCODED_BRANCH}&limit=1")
EXISTING_COUNT=$(echo "$EXISTING" | jq 'length')
PR_NUMBER=""
if [ "$EXISTING_COUNT" -gt 0 ]; then
PR_NUMBER=$(echo "$EXISTING" | jq -r '.[0].number')
echo " ️ Reusing existing PR #${PR_NUMBER}"
else
# Create cascade PR
PR_RESPONSE=$(curl -sS -w "\n%{http_code}" \
-X POST \
-H "Authorization: token ${GA_TOKEN}" \
-H "Content-Type: application/json" \
-d "{
\"title\": \"chore: cascade main → ${BRANCH} (${SHORT_SHA}) [skip ci]\",
\"body\": \"## Automatic cascade\\n\\nForward-merging \`main\` (${SHORT_SHA}) into \`${BRANCH}\`.\\n\\nIf conflicts exist, resolve manually and merge.\\n\\n> Auto-created by **Cascade Main → Dev**.\",
\"head\": \"main\",
\"base\": \"${BRANCH}\"
}" \
"${API}/pulls")
HTTP_CODE=$(echo "$PR_RESPONSE" | tail -1)
BODY=$(echo "$PR_RESPONSE" | sed '$d')
PR_NUMBER=$(echo "$BODY" | jq -r '.number // empty')
if [ "$HTTP_CODE" != "201" ] || [ -z "$PR_NUMBER" ]; then
MSG=$(echo "$BODY" | jq -r '.message // .' 2>/dev/null | head -1)
echo " ❌ Failed to create PR (HTTP ${HTTP_CODE}): ${MSG}"
FAILED=$((FAILED + 1))
continue
fi
echo " ✅ Created PR #${PR_NUMBER}"
fi
# Try auto-merge
PR_DATA=$(curl -sS \
-H "Authorization: token ${GA_TOKEN}" \
"${API}/pulls/${PR_NUMBER}")
MERGEABLE=$(echo "$PR_DATA" | jq -r '.mergeable // false')
if [ "$MERGEABLE" != "true" ]; then
echo " ⚠️ Conflicts — PR #${PR_NUMBER} left open"
CONFLICTS=$((CONFLICTS + 1))
continue
fi
MERGE_RESPONSE=$(curl -sS -w "\n%{http_code}" \
-X POST \
-H "Authorization: token ${GA_TOKEN}" \
-H "Content-Type: application/json" \
-d "{
\"Do\": \"merge\",
\"merge_message_field\": \"chore: cascade main → ${BRANCH} [skip ci]\",
\"delete_branch_after_merge\": false
}" \
"${API}/pulls/${PR_NUMBER}/merge")
MERGE_HTTP=$(echo "$MERGE_RESPONSE" | tail -1)
if [ "$MERGE_HTTP" = "200" ] || [ "$MERGE_HTTP" = "204" ]; then
echo " ✅ Merged — ${BRANCH} is in sync"
SUCCESS=$((SUCCESS + 1))
else
MERGE_BODY=$(echo "$MERGE_RESPONSE" | sed '$d')
echo " ⚠️ Merge failed (HTTP ${MERGE_HTTP}) — PR #${PR_NUMBER} left open"
CONFLICTS=$((CONFLICTS + 1))
fi
done
# Summary
echo ""
echo "════════════════════════════════════════"
echo " ✅ Merged: ${SUCCESS}"
echo " ⚠️ Conflicts: ${CONFLICTS}"
echo " ⏭️ Up to date: ${SKIPPED}"
echo " ❌ Failed: ${FAILED}"
echo "════════════════════════════════════════"
if [ "$FAILED" -gt 0 ]; then
exit 1
fi
+431
View File
@@ -0,0 +1,431 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.CI
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /.gitea/workflows/ci-platform.yml
# VERSION: 01.00.00
# BRIEF: moko-platform CI — the standards engine validates itself
#
# +========================================================================+
# | MOKOSTANDARDS PLATFORM CI |
# +========================================================================+
# | |
# | This is NOT a generic CI workflow. This is the self-validation |
# | pipeline for the central moko-platform enterprise engine. |
# | |
# | It dogfoods every tool the platform ships to governed repos: |
# | |
# | Gate 1 — Code Quality phpcs (PSR-12), phpstan (L5), psalm |
# | Gate 2 — Unit Tests phpunit with coverage threshold |
# | Gate 3 — Self-Health bin/moko health against its own repo |
# | Gate 4 — Governance Checks headers, secrets, structure, versions |
# | Gate 5 — Template Lint validate workflow templates parse clean |
# | |
# | If it doesn't pass its own checks, it can't enforce them. |
# | |
# +========================================================================+
name: "Platform: moko-platform CI"
on:
push:
branches:
- main
- dev
- dev/**
- rc/**
paths-ignore:
- '**.md'
- 'wiki/**'
- '.gitea/ISSUE_TEMPLATE/**'
pull_request:
branches:
- main
- dev
- dev/**
- rc/**
workflow_dispatch:
inputs:
full_suite:
description: 'Run full validation suite (including slow checks)'
required: false
default: 'true'
type: boolean
concurrency:
group: ci-platform-${{ github.repository }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
PHP_VERSION: '8.2'
jobs:
# ═══════════════════════════════════════════════════════════════════════
# Gate 1 — Code Quality
# ═══════════════════════════════════════════════════════════════════════
code-quality:
name: "Gate 1: Code Quality"
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup PHP ${{ env.PHP_VERSION }}
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq php${{ env.PHP_VERSION }}-cli php${{ env.PHP_VERSION }}-mbstring \
php${{ env.PHP_VERSION }}-xml php${{ env.PHP_VERSION }}-curl php${{ env.PHP_VERSION }}-zip \
php${{ env.PHP_VERSION }}-intl >/dev/null 2>&1
php -v
- name: Install Composer dependencies
run: |
composer install --no-interaction --prefer-dist
echo "Dependencies installed: $(composer show | wc -l) packages"
- name: "PHP Syntax Check"
run: |
ERRORS=0
CHECKED=0
while IFS= read -r -d '' file; do
CHECKED=$((CHECKED + 1))
if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
echo "::error file=${file}::PHP syntax error"
ERRORS=$((ERRORS + 1))
fi
done < <(find lib/ validate/ automation/ cli/ src/ deploy/ -name "*.php" -print0 2>/dev/null)
{
echo "### PHP Syntax"
echo "Checked ${CHECKED} files — ${ERRORS} error(s)"
} >> $GITHUB_STEP_SUMMARY
[ "$ERRORS" -eq 0 ] || exit 1
- name: "PHPCS (PSR-12)"
run: |
vendor/bin/phpcs --standard=phpcs.xml --report=summary lib/ validate/ automation/ 2>&1 || {
echo "::error::PHPCS found coding standard violations"
echo "### PHPCS" >> $GITHUB_STEP_SUMMARY
echo "Coding standard violations detected. Run \`composer phpcs\` locally." >> $GITHUB_STEP_SUMMARY
exit 1
}
echo "### PHPCS" >> $GITHUB_STEP_SUMMARY
echo "PSR-12 compliance: passed" >> $GITHUB_STEP_SUMMARY
- name: "PHPStan (Level 5)"
run: |
vendor/bin/phpstan analyse -c phpstan.neon --no-progress --error-format=github 2>&1 || {
echo "::error::PHPStan found type errors"
echo "### PHPStan" >> $GITHUB_STEP_SUMMARY
echo "Static analysis errors detected. Run \`composer phpstan\` locally." >> $GITHUB_STEP_SUMMARY
exit 1
}
echo "### PHPStan" >> $GITHUB_STEP_SUMMARY
echo "Static analysis (level 5): passed" >> $GITHUB_STEP_SUMMARY
- name: "Psalm"
continue-on-error: true
run: |
if [ -f "psalm.xml" ]; then
vendor/bin/psalm --config=psalm.xml --no-progress --output-format=github 2>&1 || {
echo "### Psalm" >> $GITHUB_STEP_SUMMARY
echo "Psalm found issues (advisory — not blocking)." >> $GITHUB_STEP_SUMMARY
}
fi
# ═══════════════════════════════════════════════════════════════════════
# Gate 2 — Unit Tests
# ═══════════════════════════════════════════════════════════════════════
tests:
name: "Gate 2: Unit Tests"
runs-on: ubuntu-latest
timeout-minutes: 15
needs: code-quality
strategy:
matrix:
php: ['8.1', '8.2', '8.3']
fail-fast: false
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup PHP ${{ matrix.php }}
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq php${{ matrix.php }}-cli php${{ matrix.php }}-mbstring \
php${{ matrix.php }}-xml php${{ matrix.php }}-curl php${{ matrix.php }}-zip \
php${{ matrix.php }}-intl >/dev/null 2>&1
php -v
- name: Install dependencies
run: composer install --no-interaction --prefer-dist
- name: "PHPUnit (PHP ${{ matrix.php }})"
run: |
vendor/bin/phpunit --testdox 2>&1
{
echo "### PHPUnit (PHP ${{ matrix.php }})"
echo "All tests passed."
} >> $GITHUB_STEP_SUMMARY
# ═══════════════════════════════════════════════════════════════════════
# Gate 3 — Self-Health (Dogfood)
# ═══════════════════════════════════════════════════════════════════════
self-health:
name: "Gate 3: Self-Health Check"
runs-on: ubuntu-latest
timeout-minutes: 10
needs: code-quality
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup PHP
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq php${{ env.PHP_VERSION }}-cli php${{ env.PHP_VERSION }}-mbstring \
php${{ env.PHP_VERSION }}-xml php${{ env.PHP_VERSION }}-curl php${{ env.PHP_VERSION }}-zip >/dev/null 2>&1
- name: Install dependencies
run: composer install --no-interaction --prefer-dist
- name: "Run bin/moko health against self"
run: |
php bin/moko health -- --path . --json > /tmp/health-report.json 2>&1 || true
SCORE=$(cat /tmp/health-report.json | python3 -c "import sys,json; print(json.load(sys.stdin).get('percentage', 0))" 2>/dev/null || echo "0")
LEVEL=$(cat /tmp/health-report.json | python3 -c "import sys,json; print(json.load(sys.stdin).get('level', 'unknown'))" 2>/dev/null || echo "unknown")
{
echo "### Self-Health Report"
echo ""
echo "| Metric | Value |"
echo "|---|---|"
echo "| Score | ${SCORE}% |"
echo "| Level | ${LEVEL} |"
echo ""
echo "The platform must pass its own health check to enforce it on others."
} >> $GITHUB_STEP_SUMMARY
# Platform must score at least 80%
python3 -c "exit(0 if float('${SCORE}') >= 80.0 else 1)" || {
echo "::error::Self-health score ${SCORE}% is below 80% threshold"
exit 1
}
# ═══════════════════════════════════════════════════════════════════════
# Gate 4 — Governance Checks
# ═══════════════════════════════════════════════════════════════════════
governance:
name: "Gate 4: Governance"
runs-on: ubuntu-latest
timeout-minutes: 10
needs: code-quality
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup PHP
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq php${{ env.PHP_VERSION }}-cli php${{ env.PHP_VERSION }}-mbstring \
php${{ env.PHP_VERSION }}-xml php${{ env.PHP_VERSION }}-curl >/dev/null 2>&1
- name: Install dependencies
run: composer install --no-interaction --prefer-dist
- name: "License headers (SPDX)"
run: |
MISSING=0
CHECKED=0
while IFS= read -r -d '' file; do
CHECKED=$((CHECKED + 1))
if ! head -n 20 "$file" | grep -q "SPDX-License-Identifier:"; then
echo "::warning file=${file}::Missing SPDX header"
MISSING=$((MISSING + 1))
fi
done < <(find lib/ validate/ cli/ src/ automation/ deploy/ -name "*.php" -print0 2>/dev/null)
{
echo "### License Headers"
echo "Checked ${CHECKED} files — ${MISSING} missing SPDX headers"
} >> $GITHUB_STEP_SUMMARY
# Advisory — warn but don't fail (yet)
[ "$MISSING" -eq 0 ] || echo "::warning::${MISSING} files missing SPDX license headers"
- name: "Secret detection"
run: |
FOUND=0
# Check for common secret patterns in source files
while IFS= read -r -d '' file; do
if grep -qEi '(password|secret|token|apikey|api_key)\s*[:=]\s*["\x27][^\s]{8,}' "$file" 2>/dev/null; then
echo "::error file=${file}::Potential hardcoded secret detected"
FOUND=$((FOUND + 1))
fi
done < <(find lib/ validate/ cli/ src/ automation/ deploy/ -name "*.php" -print0 2>/dev/null)
{
echo "### Secret Detection"
if [ "$FOUND" -eq 0 ]; then
echo "No hardcoded secrets detected."
else
echo "${FOUND} potential secrets found."
fi
} >> $GITHUB_STEP_SUMMARY
[ "$FOUND" -eq 0 ] || exit 1
- name: "Version consistency"
run: |
# Extract version from composer.json
COMPOSER_VER=$(python3 -c "import json; print(json.load(open('composer.json'))['version'])")
# Extract version from README.md
README_VER=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1)
{
echo "### Version Consistency"
echo "| Source | Version |"
echo "|---|---|"
echo "| composer.json | ${COMPOSER_VER} |"
echo "| README.md | ${README_VER:-not found} |"
} >> $GITHUB_STEP_SUMMARY
if [ -n "$README_VER" ] && [ "$COMPOSER_VER" != "$README_VER" ]; then
echo "::warning::Version mismatch: composer.json=${COMPOSER_VER} vs README.md=${README_VER}"
fi
# ═══════════════════════════════════════════════════════════════════════
# Gate 5 — Template Integrity
# ═══════════════════════════════════════════════════════════════════════
templates:
name: "Gate 5: Template Integrity"
runs-on: ubuntu-latest
timeout-minutes: 10
needs: code-quality
if: github.event_name != 'push' || github.event.inputs.full_suite != 'false'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: "Validate workflow templates"
run: |
ERRORS=0
CHECKED=0
# Check all YAML workflow templates parse cleanly
while IFS= read -r -d '' file; do
CHECKED=$((CHECKED + 1))
if ! python3 -c "import yaml; yaml.safe_load(open('${file}'))" 2>/dev/null; then
echo "::error file=${file}::Invalid YAML"
ERRORS=$((ERRORS + 1))
fi
done < <(find templates/workflows/ -name "*.yml" -o -name "*.yaml" 2>/dev/null | tr '\n' '\0')
# Also check the live workflows
while IFS= read -r -d '' file; do
CHECKED=$((CHECKED + 1))
if ! python3 -c "import yaml; yaml.safe_load(open('${file}'))" 2>/dev/null; then
echo "::error file=${file}::Invalid YAML"
ERRORS=$((ERRORS + 1))
fi
done < <(find .mokogitea/workflows/ -name "*.yml" -o -name "*.yaml" 2>/dev/null | tr '\n' '\0')
{
echo "### Template Integrity"
echo "Validated ${CHECKED} YAML files — ${ERRORS} parse errors"
} >> $GITHUB_STEP_SUMMARY
[ "$ERRORS" -eq 0 ] || exit 1
- name: "Validate gitignore templates"
run: |
TEMPLATES=0
for GI in templates/configs/gitignore templates/configs/gitignore.dolibarr templates/configs/.gitignore.joomla; do
if [ -f "$GI" ]; then
TEMPLATES=$((TEMPLATES + 1))
# Verify required entries
for REQUIRED in ".claude/" "TODO.md" "*.min.css" "*.min.js" "wiki/"; do
if ! grep -q "$REQUIRED" "$GI"; then
echo "::error file=${GI}::Missing required entry: ${REQUIRED}"
fi
done
fi
done
echo "### Gitignore Templates" >> $GITHUB_STEP_SUMMARY
echo "Validated ${TEMPLATES} gitignore templates." >> $GITHUB_STEP_SUMMARY
- name: "Validate PHP validation scripts"
run: |
ERRORS=0
CHECKED=0
while IFS= read -r -d '' file; do
CHECKED=$((CHECKED + 1))
if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
echo "::error file=${file}::Validation script has syntax error"
ERRORS=$((ERRORS + 1))
fi
done < <(find validate/ -name "*.php" -print0 2>/dev/null)
{
echo "### Validation Scripts"
echo "Checked ${CHECKED} scripts — ${ERRORS} syntax errors"
} >> $GITHUB_STEP_SUMMARY
[ "$ERRORS" -eq 0 ] || { echo "::error::Validation scripts must be error-free"; exit 1; }
# ═══════════════════════════════════════════════════════════════════════
# Summary
# ═══════════════════════════════════════════════════════════════════════
summary:
name: "CI Summary"
runs-on: ubuntu-latest
needs: [code-quality, tests, self-health, governance, templates]
if: always()
steps:
- name: Check gate results
run: |
{
echo "# moko-platform CI"
echo ""
echo "| Gate | Job | Status |"
echo "|---|---|---|"
echo "| 1 | Code Quality | ${{ needs.code-quality.result }} |"
echo "| 2 | Unit Tests | ${{ needs.tests.result }} |"
echo "| 3 | Self-Health | ${{ needs.self-health.result }} |"
echo "| 4 | Governance | ${{ needs.governance.result }} |"
echo "| 5 | Templates | ${{ needs.templates.result }} |"
echo ""
echo "> *The standards engine must pass its own standards.*"
} >> $GITHUB_STEP_SUMMARY
# Fail if any required gate failed
if [ "${{ needs.code-quality.result }}" = "failure" ] || \
[ "${{ needs.tests.result }}" = "failure" ] || \
[ "${{ needs.self-health.result }}" = "failure" ] || \
[ "${{ needs.governance.result }}" = "failure" ] || \
[ "${{ needs.templates.result }}" = "failure" ]; then
echo "::error::One or more CI gates failed"
exit 1
fi
+87
View File
@@ -0,0 +1,87 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Maintenance
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /.gitea/workflows/cleanup.yml
# VERSION: 01.00.00
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
name: "Universal: Repository Cleanup"
on:
schedule:
- cron: '0 3 * * 0' # Weekly on Sunday at 03:00 UTC
workflow_dispatch:
permissions:
contents: write
env:
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
jobs:
cleanup:
name: Clean Merged Branches
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GA_TOKEN }}
- name: Delete merged branches
env:
GA_TOKEN: ${{ secrets.GA_TOKEN }}
run: |
echo "=== Merged Branch Cleanup ==="
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
# List branches via API
BRANCHES=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
"${API}/branches?limit=50" | jq -r '.[].name')
DELETED=0
for BRANCH in $BRANCHES; do
# Skip protected branches
case "$BRANCH" in
main|master|develop|release/*|hotfix/*) continue ;;
esac
# Check if branch is merged into main
if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then
echo " Deleting merged branch: ${BRANCH}"
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
"${API}/branches/${BRANCH}" 2>/dev/null || true
DELETED=$((DELETED + 1))
fi
done
echo "Deleted ${DELETED} merged branch(es)"
- name: Clean old workflow runs
env:
GA_TOKEN: ${{ secrets.GA_TOKEN }}
run: |
echo "=== Workflow Run Cleanup ==="
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ)
# Get old completed runs
RUNS=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
"${API}/actions/runs?status=completed&limit=50" | \
jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null)
DELETED=0
for RUN_ID in $RUNS; do
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
"${API}/actions/runs/${RUN_ID}" 2>/dev/null || true
DELETED=$((DELETED + 1))
done
echo "Deleted ${DELETED} old workflow run(s)"
@@ -4,15 +4,13 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Deploy
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
# INGROUP: moko-platform.Deploy
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /templates/workflows/joomla/deploy-manual.yml.template
# VERSION: 04.06.00
# VERSION: 04.07.00
# BRIEF: Manual SFTP deploy to dev server for Joomla repos
# NOTE: Joomla repos use update.xml for distribution. This is for manual
# dev server testing only — triggered via workflow_dispatch.
name: Deploy to Dev (Manual)
name: "Universal: Deploy to Dev (Manual)"
on:
workflow_dispatch:
@@ -42,18 +40,18 @@ jobs:
run: |
php -v && composer --version
- name: Setup MokoStandards tools
- name: Setup moko-platform tools
env:
GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
run: |
git clone --depth 1 --branch {{standards_branch}} --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
/tmp/mokostandards-api 2>/dev/null || true
if [ -d "/tmp/mokostandards-api" ] && [ -f "/tmp/mokostandards-api/composer.json" ]; then
cd /tmp/mokostandards-api && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
/tmp/moko-platform-api 2>/dev/null || true
if [ -d "/tmp/moko-platform-api" ] && [ -f "/tmp/moko-platform-api/composer.json" ]; then
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
fi
- name: Check FTP configuration
@@ -61,11 +59,10 @@ jobs:
env:
HOST: ${{ vars.DEV_FTP_HOST }}
PATH_VAR: ${{ vars.DEV_FTP_PATH }}
SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
PORT: ${{ vars.DEV_FTP_PORT }}
run: |
if [ -z "$HOST" ] || [ -z "$PATH_VAR" ]; then
echo "DEV_FTP_HOST or DEV_FTP_PATH not configured cannot deploy"
echo "DEV_FTP_HOST or DEV_FTP_PATH not configured -- cannot deploy"
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
@@ -73,7 +70,6 @@ jobs:
echo "host=$HOST" >> "$GITHUB_OUTPUT"
REMOTE="${PATH_VAR%/}"
[ -n "$SUFFIX" ] && REMOTE="${REMOTE}/${SUFFIX#/}"
echo "remote=$REMOTE" >> "$GITHUB_OUTPUT"
[ -z "$PORT" ] && PORT="22"
@@ -88,7 +84,7 @@ jobs:
run: |
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
[ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ nothing to deploy"; exit 0; }
[ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ -- nothing to deploy"; exit 0; }
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
"${{ steps.check.outputs.host }}" "${{ steps.check.outputs.port }}" "$SFTP_USER" "${{ steps.check.outputs.remote }}" \
@@ -105,20 +101,33 @@ jobs:
DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json)
[ "${{ inputs.clear_remote }}" = "true" ] && DEPLOY_ARGS+=(--clear-remote)
PLATFORM=$(php /tmp/mokostandards-api/cli/platform_detect.php --path . 2>/dev/null || true)
if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards-api/deploy/deploy-joomla.php" ]; then
php /tmp/mokostandards-api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}"
PLATFORM=$(php /tmp/moko-platform-api/cli/platform_detect.php --path . 2>/dev/null || true)
if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/moko-platform-api/deploy/deploy-joomla.php" ]; then
php /tmp/moko-platform-api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}"
else
php /tmp/mokostandards-api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}"
php /tmp/moko-platform-api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}"
fi
rm -f /tmp/deploy_key /tmp/sftp-config.json
- name: Post-deploy health check
if: success() && steps.check.outputs.skip != 'true'
run: |
if [ -f "deploy/health-check.php" ]; then
SITE_URL="${{ vars.DEV_SITE_URL }}"
if [ -n "$SITE_URL" ]; then
php deploy/health-check.php --url "$SITE_URL" --checks http --timeout 30 || echo "::warning::Health check failed after deploy"
else
echo "DEV_SITE_URL not configured, skipping health check"
fi
fi
- name: Summary
if: always()
run: |
if [ "${{ steps.check.outputs.skip }}" = "true" ]; then
echo "### Deploy Skipped FTP not configured" >> $GITHUB_STEP_SUMMARY
echo "### Deploy Skipped -- FTP not configured" >> $GITHUB_STEP_SUMMARY
else
echo "### Manual Dev Deploy Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
+96
View File
@@ -0,0 +1,96 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Security
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# PATH: /templates/workflows/gitleaks.yml.template
# VERSION: 01.00.00
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
#
# +========================================================================+
# | SECRET SCANNING |
# +========================================================================+
# | |
# | Scans commits for leaked secrets using Gitleaks. |
# | |
# | - PR scan: only new commits in the PR |
# | - Scheduled: full repo scan weekly |
# | - Alerts via ntfy on findings |
# | |
# +========================================================================+
name: "Universal: Secret Scanning"
on:
pull_request:
branches:
- main
- 'dev/**'
schedule:
- cron: '0 5 * * 1' # Weekly Monday 05:00 UTC
workflow_dispatch:
permissions:
contents: read
env:
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }}
jobs:
gitleaks:
name: Gitleaks Secret Scan
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Gitleaks
run: |
GITLEAKS_VERSION="8.21.2"
curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \
| tar -xz -C /usr/local/bin gitleaks
gitleaks version
- name: Scan for secrets
id: scan
run: |
echo "### Secret Scanning" >> $GITHUB_STEP_SUMMARY
ARGS="--source . --verbose --report-format json --report-path /tmp/gitleaks-report.json"
if [ "${{ github.event_name }}" = "pull_request" ]; then
# Scan only PR commits
ARGS="$ARGS --log-opts=${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}"
echo "Scanning PR commits only" >> $GITHUB_STEP_SUMMARY
else
echo "Full repository scan" >> $GITHUB_STEP_SUMMARY
fi
if gitleaks detect $ARGS 2>&1; then
echo "result=clean" >> "$GITHUB_OUTPUT"
echo "**No secrets detected.**" >> $GITHUB_STEP_SUMMARY
else
echo "result=found" >> "$GITHUB_OUTPUT"
FINDINGS=$(jq length /tmp/gitleaks-report.json 2>/dev/null || echo "unknown")
echo "**${FINDINGS} potential secret(s) detected.**" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Review the findings and rotate any exposed credentials immediately." >> $GITHUB_STEP_SUMMARY
exit 1
fi
- name: Notify on findings
if: failure() && steps.scan.outputs.result == 'found'
run: |
REPO="${{ github.event.repository.name }}"
curl -sS \
-H "Title: ${REPO} — secrets detected in code" \
-H "Tags: rotating_light,key" \
-H "Priority: urgent" \
-d "Gitleaks found potential secrets. Review and rotate credentials immediately." \
"${NTFY_URL}/${NTFY_TOPIC}" || true
+70
View File
@@ -0,0 +1,70 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Notifications
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /.gitea/workflows/notify.yml
# VERSION: 01.00.00
# BRIEF: Push notifications via ntfy on release success or workflow failure
name: "Universal: Notifications"
on:
workflow_run:
workflows:
- "Joomla Build & Release"
- "Joomla Extension CI"
- "Deploy"
types:
- completed
permissions:
contents: read
env:
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-releases' }}
jobs:
notify:
name: Send Notification
runs-on: ubuntu-latest
if: >-
github.event.workflow_run.conclusion == 'success' ||
github.event.workflow_run.conclusion == 'failure'
steps:
- name: Notify on success (releases only)
if: >-
github.event.workflow_run.conclusion == 'success' &&
contains(github.event.workflow_run.name, 'Release')
run: |
REPO="${{ github.event.repository.name }}"
WORKFLOW="${{ github.event.workflow_run.name }}"
URL="${{ github.event.workflow_run.html_url }}"
curl -sS \
-H "Title: ${REPO} released" \
-H "Tags: white_check_mark,package" \
-H "Priority: default" \
-H "Click: ${URL}" \
-d "${WORKFLOW} completed successfully." \
"${NTFY_URL}/${NTFY_TOPIC}"
- name: Notify on failure
if: github.event.workflow_run.conclusion == 'failure'
run: |
REPO="${{ github.event.repository.name }}"
WORKFLOW="${{ github.event.workflow_run.name }}"
URL="${{ github.event.workflow_run.html_url }}"
curl -sS \
-H "Title: ${REPO} workflow failed" \
-H "Tags: x,warning" \
-H "Priority: high" \
-H "Click: ${URL}" \
-d "${WORKFLOW} failed. Check the run for details." \
"${NTFY_URL}/${NTFY_TOPIC}"
+196
View File
@@ -0,0 +1,196 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.CI
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# PATH: /templates/workflows/universal/pr-check.yml.template
# VERSION: 05.00.00
# BRIEF: PR gate — branch policy + code validation before merge
name: "Universal: PR Check"
on:
pull_request:
types: [opened, synchronize, reopened, edited]
permissions:
contents: read
pull-requests: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
# ── Branch Policy ──────────────────────────────────────────────────────
branch-policy:
name: Branch Policy
runs-on: ubuntu-latest
steps:
- name: Check branch merge target
run: |
HEAD="${{ github.head_ref }}"
BASE="${{ github.base_ref }}"
echo "PR: ${HEAD} → ${BASE}"
ALLOWED=true
REASON=""
case "$HEAD" in
feature/*|feat/*)
if [ "$BASE" != "dev" ]; then
ALLOWED=false
REASON="Feature branches must target 'dev', not '${BASE}'"
fi
;;
fix/*|bugfix/*)
if [ "$BASE" != "dev" ]; then
ALLOWED=false
REASON="Fix branches must target 'dev', not '${BASE}'"
fi
;;
hotfix/*)
if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'"
fi
;;
alpha/*|beta/*)
if [ "$BASE" != "dev" ]; then
ALLOWED=false
REASON="Pre-release branches must target 'dev', not '${BASE}'"
fi
;;
rc/*)
if [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="Release candidate branches must target 'main', not '${BASE}'"
fi
;;
dev)
if [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="Dev branch can only merge into 'main', not '${BASE}'"
fi
;;
esac
if [ "$ALLOWED" = false ]; then
echo "::error::${REASON}"
echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "${REASON}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "Branch policy: OK (${HEAD} → ${BASE})"
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
# ── Code Validation ────────────────────────────────────────────────────
validate:
name: Validate PR
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Detect platform
id: platform
run: |
# Read platform from XML manifest (<platform> tag) or plain text fallback
PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1)
[ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]')
[ -z "$PLATFORM" ] && PLATFORM="generic"
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
- name: Setup PHP
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
run: |
if ! command -v php &> /dev/null; then
sudo apt-get update -qq
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
fi
- name: PHP syntax check
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
run: |
ERRORS=0
while IFS= read -r -d '' file; do
if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
ERRORS=$((ERRORS + 1))
fi
done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0)
echo "PHP lint: ${ERRORS} error(s)"
[ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
- name: Validate platform manifest
run: |
PLATFORM="${{ steps.platform.outputs.platform }}"
case "$PLATFORM" in
joomla)
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
if [ -z "$MANIFEST" ]; then
echo "::warning::No Joomla manifest found (WaaS site)"
exit 0
fi
echo "Manifest: ${MANIFEST}"
if command -v php &> /dev/null; then
php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::Manifest XML is malformed"; exit 1; }
fi
for ELEMENT in name version description; do
grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; }
done
echo "Joomla manifest valid"
;;
dolibarr)
MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1)
if [ -z "$MOD_FILE" ]; then
echo "::error::No mod*.class.php found"
exit 1
fi
echo "Dolibarr module: ${MOD_FILE}"
;;
*)
echo "Generic platform — no manifest validation"
;;
esac
- name: Check update stream format
run: |
PLATFORM="${{ steps.platform.outputs.platform }}"
case "$PLATFORM" in
joomla)
if [ -f "updates.xml" ]; then
if command -v php &> /dev/null; then
php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::updates.xml is malformed"; exit 1; }
fi
echo "updates.xml valid"
fi
;;
dolibarr)
[ -f "update.txt" ] && echo "update.txt present" || echo "::warning::No update.txt"
;;
esac
- name: Verify package source
run: |
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
if [ ! -d "$SOURCE_DIR" ]; then
echo "::warning::No src/ or htdocs/ directory"
exit 0
fi
FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
echo "Source: ${FILE_COUNT} files"
[ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; }
+246
View File
@@ -0,0 +1,246 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /templates/workflows/universal/pre-release.yml.template
# VERSION: 05.00.00
# BRIEF: Manual pre-release — builds dev/alpha/beta/rc packages from any branch
name: "Universal: Pre-Release"
on:
workflow_dispatch:
inputs:
stability:
description: 'Pre-release channel'
required: true
type: choice
options:
- development
- alpha
- beta
- release-candidate
permissions:
contents: write
env:
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
jobs:
build:
name: "Build Pre-Release (${{ inputs.stability }})"
runs-on: release
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GA_TOKEN }}
- name: Setup PHP
run: |
if ! command -v php &> /dev/null; then
sudo apt-get update -qq
sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl >/dev/null 2>&1
fi
- name: Setup moko-platform tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
run: |
git clone --depth 1 --branch main --quiet "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" /tmp/moko-platform-api
- name: Detect platform
id: platform
run: |
php /tmp/moko-platform-api/cli/manifest_read.php --path . --github-output
- name: Resolve metadata
id: meta
run: |
STABILITY="${{ inputs.stability }}"
MOKO_API="/tmp/moko-platform-api/cli"
case "$STABILITY" in
development) SUFFIX="-dev"; TAG="development" ;;
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
beta) SUFFIX="-beta"; TAG="beta" ;;
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
esac
# Bump patch version
BUMP_OUTPUT=$(php ${MOKO_API}/version_bump.php --path .)
VERSION=$(echo "$BUMP_OUTPUT" | grep -oP '\d{2}\.\d{2}\.\d{2}$' || true)
[ -z "$VERSION" ] && VERSION=$(php ${MOKO_API}/version_read.php --path .)
echo "Version: ${VERSION}"
# Update platform-specific manifest
php ${MOKO_API}/version_set_platform.php --path . --version "${VERSION}"
# Commit version bump
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
git add -A
git diff --cached --quiet || {
git commit -m "chore(version): bump to ${VERSION} [skip ci]"
git push origin HEAD 2>&1
}
# Detect element from Joomla/Dolibarr manifest
PLATFORM="${{ steps.platform.outputs.platform }}"
EXT_ELEMENT=$(php ${MOKO_API}/manifest_read.php --path . --field name 2>/dev/null | tr -d ' ' | tr '[:upper:]' '[:lower:]' || true)
# For Joomla, prefer <element> tag
if [ "$PLATFORM" = "joomla" ]; then
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true)
if [ -n "$MANIFEST" ]; then
ELEM=$(grep -oP "<element>\K[^<]+" "$MANIFEST" 2>/dev/null | head -1)
[ -n "$ELEM" ] && EXT_ELEMENT="$ELEM"
fi
fi
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
ZIP_NAME="${EXT_ELEMENT}-${VERSION}${SUFFIX}.zip"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
- name: Build package
id: zip
run: |
VERSION="${{ steps.meta.outputs.version }}"
SUFFIX="${{ steps.meta.outputs.suffix }}"
PLATFORM="${{ steps.platform.outputs.platform }}"
if [ "$PLATFORM" = "joomla" ]; then
php /tmp/moko-platform-api/cli/joomla_build.php --path . --version "${VERSION}" --suffix "${SUFFIX}" --output build --github-output
else
# Generic build: zip src/ directory
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
[ ! -d "$SOURCE_DIR" ] && { echo "::error::No src/ or htdocs/"; exit 1; }
EXT_ELEMENT="${{ steps.meta.outputs.ext_element }}"
ZIP_NAME="${EXT_ELEMENT}-${VERSION}${SUFFIX}.zip"
mkdir -p build
cd "$SOURCE_DIR" && zip -r "../build/${ZIP_NAME}" . && cd ..
SHA256=$(sha256sum "build/${ZIP_NAME}" | cut -d' ' -f1)
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
echo "zip_path=build/${ZIP_NAME}" >> "$GITHUB_OUTPUT"
echo "sha256=${SHA256}" >> "$GITHUB_OUTPUT"
fi
- name: Create or replace Gitea release
id: release
continue-on-error: true
run: |
TAG="${{ steps.meta.outputs.tag }}"
VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
SHA256="${{ steps.zip.outputs.sha256 }}"
ZIP_NAME="${{ steps.zip.outputs.zip_name }}"
EXT_ELEMENT="${{ steps.meta.outputs.ext_element }}"
TOKEN="${{ secrets.GA_TOKEN }}"
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
BRANCH=$(git branch --show-current)
BODY="## ${VERSION} ($(date +%Y-%m-%d))
**Channel:** ${STABILITY}
**SHA-256:** \`${SHA256}\`"
# Delete existing release
EXISTING_ID=$(curl -sS -H "Authorization: token ${TOKEN}" \
"${API}/releases/tags/${TAG}" | jq -r '.id // empty' 2>/dev/null)
if [ -n "$EXISTING_ID" ]; then
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
"${API}/releases/${EXISTING_ID}" 2>/dev/null || true
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
"${API}/tags/${TAG}" 2>/dev/null || true
fi
# Create release
RELEASE_ID=$(curl -sS -X POST -H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
"${API}/releases" \
-d "$(jq -n \
--arg tag "$TAG" \
--arg target "$BRANCH" \
--arg name "${EXT_ELEMENT} ${VERSION} (${STABILITY})" \
--arg body "$BODY" \
'{tag_name: $tag, target_commitish: $target, name: $name, body: $body, prerelease: true}'
)" | jq -r '.id')
echo "release_id=${RELEASE_ID}" >> "$GITHUB_OUTPUT"
# Upload ZIP
curl -sS -X POST -H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/octet-stream" \
"${API}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" \
--data-binary "@${{ steps.zip.outputs.zip_path }}"
echo "Released: ${EXT_ELEMENT} ${VERSION} (${STABILITY})"
- name: "Update updates.xml"
if: steps.platform.outputs.platform == 'joomla'
run: |
VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
SHA256="${{ steps.zip.outputs.sha256 }}"
php /tmp/moko-platform-api/cli/updates_xml_build.php --path . --version "$VERSION" --stability "$STABILITY" --sha "$SHA256" --gitea-url "$GITEA_URL" --org "$GITEA_ORG" --repo "$GITEA_REPO"
if ! git diff --quiet updates.xml 2>/dev/null; then
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git add updates.xml
git commit -m "chore: update $STABILITY channel $VERSION [skip ci]"
git push origin HEAD 2>&1 || echo "WARNING: push failed"
fi
- name: "Sync updates.xml to all branches"
if: steps.platform.outputs.platform == 'joomla'
run: |
php /tmp/moko-platform-api/cli/updates_xml_sync.php --path . --current "${{ github.ref_name }}" --branches main,dev --version "${{ steps.meta.outputs.version }}" --token "${{ secrets.GA_TOKEN }}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" --gitea-url "${GITEA_URL}"
- name: "Delete lesser pre-release channels (cascade)"
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.GA_TOKEN }}"
STABILITY="${{ steps.meta.outputs.stability }}"
# Cascade: rc → beta,alpha,dev | beta → alpha,dev | alpha → dev | dev → nothing
case "$STABILITY" in
release-candidate) TAGS_TO_DELETE="beta alpha development" ;;
beta) TAGS_TO_DELETE="alpha development" ;;
alpha) TAGS_TO_DELETE="development" ;;
*) TAGS_TO_DELETE="" ;;
esac
[ -z "$TAGS_TO_DELETE" ] && exit 0
for TAG in $TAGS_TO_DELETE; do
RELEASE_ID=$(curl -sS -H "Authorization: token ${TOKEN}" \
"${API_BASE}/releases/tags/${TAG}" 2>/dev/null | \
python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "None" ]; then
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
"${API_BASE}/releases/${RELEASE_ID}" 2>/dev/null || true
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
"${API_BASE}/tags/${TAG}" 2>/dev/null || true
echo "Deleted: ${TAG} (id: ${RELEASE_ID})"
fi
done
@@ -7,19 +7,14 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Validation
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
# PATH: /.github/workflows/repo_health.yml
# INGROUP: moko-platform.Validation
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# PATH: /templates/workflows/joomla/repo_health.yml.template
# VERSION: 04.06.00
# BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts.
# NOTE: Field is user-managed.
# ============================================================================
name: Repo Health
concurrency:
group: repo-health-${{ github.repository }}-${{ github.ref }}
cancel-in-progress: true
name: "Generic: Repo Health"
defaults:
run:
@@ -50,13 +45,11 @@ env:
RELEASE_OPTIONAL_REPO_VARS: DEV_FTP_SUFFIX
# Scripts governance policy
# Note: directories listed without a trailing slash.
SCRIPTS_REQUIRED_DIRS:
SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate
# Repo health policy
# Files are listed as-is; directories must end with a trailing slash.
REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.github/workflows/
REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.gitea/workflows/
REPO_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore,README.md,docs/
REPO_DISALLOWED_DIRS:
REPO_DISALLOWED_FILES: TODO.md,todo.md
@@ -64,10 +57,10 @@ env:
# Extended checks toggles
EXTENDED_CHECKS: "true"
# File / directory variables (moved to top-level env)
# File / directory variables
DOCS_INDEX: docs/docs-index.md
SCRIPT_DIR: scripts
WORKFLOWS_DIR: .github/workflows
WORKFLOWS_DIR: .gitea/workflows
SHELLCHECK_PATTERN: '*.sh'
SPDX_FILE_GLOBS: '*.sh,*.php,*.js,*.ts,*.css,*.xml,*.yml,*.yaml'
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
@@ -99,7 +92,7 @@ jobs:
# Hardcoded authorized users — always allowed
case "$ACTOR" in
jmiller-moko|github-actions\[bot\])
jmiller|gitea-actions[bot])
ALLOWED=true
PERMISSION=admin
METHOD="hardcoded allowlist"
@@ -121,7 +114,7 @@ jobs:
echo "allowed=${ALLOWED}" >> "$GITHUB_OUTPUT"
{
echo "## 🔐 Access Authorization"
echo "## Access Authorization"
echo ""
echo "| Field | Value |"
echo "|-------|-------|"
@@ -132,9 +125,9 @@ jobs:
echo "| **Authorized** | ${ALLOWED} |"
echo ""
if [ "$ALLOWED" = "true" ]; then
echo "${ACTOR} authorized (${METHOD})"
echo "${ACTOR} authorized (${METHOD})"
else
echo "${ACTOR} is NOT authorized. Requires admin or maintain role."
echo "${ACTOR} is NOT authorized. Requires admin or maintain role."
fi
} >> "${GITHUB_STEP_SUMMARY}"
@@ -291,7 +284,7 @@ jobs:
exit 0
fi
IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}"
if [ -n "${SCRIPTS_REQUIRED_DIRS:-}" ]; then IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}"; else required_dirs=(); fi
IFS=',' read -r -a allowed_dirs <<< "${SCRIPTS_ALLOWED_DIRS}"
missing_dirs=()
@@ -395,23 +388,27 @@ jobs:
exit 0
fi
# Source directory: src/ or htdocs/ (either is valid)
IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}"
IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}"
if [ -n "${REPO_DISALLOWED_DIRS:-}" ]; then IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}"; else disallowed_dirs=(); fi
IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES:-}"
missing_required=()
missing_optional=()
# Source directory: src/ or htdocs/ (either is valid for extension repos)
SOURCE_DIR=""
if [ -d "src" ]; then
SOURCE_DIR="src"
elif [ -d "htdocs" ]; then
SOURCE_DIR="htdocs"
elif [ -d "deploy" ] || [ -d "cli" ] || [ -d "monitoring" ]; then
# Platform/tooling repos don't need src/
SOURCE_DIR=""
else
missing_required+=("src/ or htdocs/ (source directory required)")
fi
IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}"
IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}"
IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}"
IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES}"
missing_required=()
missing_optional=()
for item in "${required_artifacts[@]}"; do
if printf '%s' "${item}" | grep -q '/$'; then
d="${item%/}"
@@ -421,7 +418,6 @@ jobs:
fi
done
# Optional entries: handle files and directories (trailing slash indicates dir)
for f in "${optional_files[@]}"; do
if printf '%s' "${f}" | grep -q '/$'; then
d="${f%/}"
@@ -445,8 +441,6 @@ jobs:
dev_paths=()
dev_branches=()
# Look for remote branches matching origin/dev*.
# A plain origin/dev is considered invalid; we require dev/<something> branches.
while IFS= read -r b; do
name="${b#origin/}"
if [ "${name}" = 'dev' ]; then
@@ -456,14 +450,8 @@ jobs:
fi
done < <(git branch -r --list 'origin/dev*' | sed 's/^ *//')
# If there are no dev/* branches, fail the guardrail.
if [ "${#dev_paths[@]}" -eq 0 ]; then
missing_required+=("dev/* branch (e.g. dev/01.00.00)")
fi
# If a plain dev branch exists (origin/dev), flag it as invalid.
if [ "${#dev_branches[@]}" -gt 0 ]; then
missing_required+=("invalid branch dev (must be dev/<version>)")
if [ "${#dev_paths[@]}" -eq 0 ] && [ "${#dev_branches[@]}" -eq 0 ]; then
missing_required+=("dev or dev/* branch")
fi
content_warnings=()
@@ -489,26 +477,7 @@ jobs:
export MISSING_OPTIONAL="$(printf '%s\n' "${missing_optional[@]:-}")"
export CONTENT_WARNINGS="$(printf '%s\n' "${content_warnings[@]:-}")"
report_json="$(python3 - <<'PY'
import json
import os
profile = os.environ.get('PROFILE_RAW') or 'all'
missing_required = os.environ.get('MISSING_REQUIRED', '').splitlines() if os.environ.get('MISSING_REQUIRED') else []
missing_optional = os.environ.get('MISSING_OPTIONAL', '').splitlines() if os.environ.get('MISSING_OPTIONAL') else []
content_warnings = os.environ.get('CONTENT_WARNINGS', '').splitlines() if os.environ.get('CONTENT_WARNINGS') else []
out = {
'profile': profile,
'missing_required': [x for x in missing_required if x],
'missing_optional': [x for x in missing_optional if x],
'content_warnings': [x for x in content_warnings if x],
}
print(json.dumps(out, indent=2))
PY
)"
report_json=$(printf '{"profile":"%s","missing_required":%d,"missing_optional":%d,"content_warnings":%d}' "$profile" "${#missing_required[@]}" "${#missing_optional[@]}" "${#content_warnings[@]}")
{
printf '%s\n' '### Repository health'
@@ -553,54 +522,47 @@ jobs:
} >> "${GITHUB_STEP_SUMMARY}"
fi
# ── Joomla-specific checks ───────────────────────────────────────
# -- Joomla-specific checks --
joomla_findings=()
# XML manifest: find any XML file containing <extension
MANIFEST="$(find . -maxdepth 2 -name '*.xml' -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true)"
if [ -z "${MANIFEST}" ]; then
joomla_findings+=("Joomla XML manifest not found (no *.xml with <extension> tag)")
else
# Check <version> tag exists
if ! grep -qP '<version>' "${MANIFEST}"; then
joomla_findings+=("XML manifest: <version> tag missing")
fi
# Check extension type attribute
if ! grep -qP 'type="(component|module|plugin|library|package|template|language)"' "${MANIFEST}"; then
joomla_findings+=("XML manifest: type attribute missing or invalid")
fi
# Check <name> tag
if ! grep -qP '<name>' "${MANIFEST}"; then
joomla_findings+=("XML manifest: <name> tag missing")
fi
# Check <author> tag
if ! grep -qP '<author>' "${MANIFEST}"; then
joomla_findings+=("XML manifest: <author> tag missing")
fi
# Check <namespace> for Joomla 5+
if ! grep -qP '<namespace' "${MANIFEST}"; then
joomla_findings+=("XML manifest: <namespace> missing (required for Joomla 5+)")
fi
fi
# Language files: check for at least one .ini file
INI_COUNT="$(find . -name '*.ini' -type f 2>/dev/null | wc -l)"
if [ "${INI_COUNT}" -eq 0 ]; then
joomla_findings+=("No .ini language files found")
fi
# updates.xml must exist in root (Joomla update server)
if [ ! -f 'updates.xml' ]; then
joomla_findings+=("updates.xml missing in root (required for Joomla update server)")
fi
# index.html files for directory listing protection
INDEX_DIRS=("${SOURCE_DIR}" "${SOURCE_DIR}/admin" "${SOURCE_DIR}/site")
for dir in "${INDEX_DIRS[@]}"; do
if [ -d "${dir}" ] && [ ! -f "${dir}/index.html" ]; then
joomla_findings+=("${dir}/index.html missing (directory listing protection)")
fi
done
if [ -n "${SOURCE_DIR}" ]; then
INDEX_DIRS=("${SOURCE_DIR}" "${SOURCE_DIR}/admin" "${SOURCE_DIR}/site")
for dir in "${INDEX_DIRS[@]}"; do
if [ -d "${dir}" ] && [ ! -f "${dir}/index.html" ]; then
joomla_findings+=("${dir}/index.html missing (directory listing protection)")
fi
done
fi
if [ "${#joomla_findings[@]}" -gt 0 ]; then
{
@@ -624,14 +586,12 @@ jobs:
extended_findings=()
if [ "${extended_enabled}" = 'true' ]; then
# CODEOWNERS presence
if [ -f '.github/CODEOWNERS' ] || [ -f 'CODEOWNERS' ] || [ -f 'docs/CODEOWNERS' ]; then
:
else
extended_findings+=("CODEOWNERS not found (.github/CODEOWNERS preferred)")
fi
# Workflow pinning advisory: flag uses @main/@master
if ls "${WORKFLOWS_DIR}"/*.yml >/dev/null 2>&1 || ls "${WORKFLOWS_DIR}"/*.yaml >/dev/null 2>&1; then
bad_refs="$(grep -RIn --include='*.yml' --include='*.yaml' -E '^[[:space:]]*uses:[[:space:]]*[^#]+@(main|master)\b' "${WORKFLOWS_DIR}" 2>/dev/null || true)"
if [ -n "${bad_refs}" ]; then
@@ -647,51 +607,35 @@ jobs:
fi
fi
# Docs index link integrity (docs/docs-index.md)
if [ -f "${DOCS_INDEX}" ]; then
missing_links="$(python3 - <<'PY'
import os
import re
idx = os.environ.get('DOCS_INDEX', 'docs/docs-index.md')
base = os.getcwd()
bad = []
pat = re.compile(r'\[[^\]]+\]\(([^)]+)\)')
with open(idx, 'r', encoding='utf-8') as f:
for line in f:
for m in pat.findall(line):
link = m.strip()
if link.startswith('http://') or link.startswith('https://') or link.startswith('#') or link.startswith('mailto:'):
continue
if link.startswith('/'):
rel = link.lstrip('/')
else:
rel = os.path.normpath(os.path.join(os.path.dirname(idx), link))
rel = rel.split('#', 1)[0]
rel = rel.split('?', 1)[0]
if not rel:
continue
p = os.path.join(base, rel)
if not os.path.exists(p):
bad.append(rel)
print('\n'.join(sorted(set(bad))))
PY
)"
missing_links=""
while IFS= read -r docline; do
for link in $(echo "$docline" | grep -oE '\]\([^)]+\)' | sed 's/\](//' | sed 's/)$//' || true); do
case "$link" in http://*|https://*|"#"*|mailto:*) continue ;; esac
linkpath="${link%%#*}"
linkpath="${linkpath%%\?*}"
[ -z "$linkpath" ] && continue
if [ "${linkpath:0:1}" = "/" ]; then
testpath="${linkpath#/}"
else
testpath="$(dirname "${DOCS_INDEX}")/${linkpath}"
fi
[ ! -e "$testpath" ] && missing_links="${missing_links}${testpath} "
done
done < "${DOCS_INDEX}"
if [ -n "${missing_links}" ]; then
extended_findings+=("docs/docs-index.md contains broken relative links")
{
printf '%s\n' '### Docs index link integrity'
printf '%s\n' 'Broken relative links:'
while IFS= read -r l; do [ -n "${l}" ] && printf '%s\n' "- ${l}"; done <<< "${missing_links}"
for bl in ${missing_links}; do
printf '%s\n' "- ${bl}"
done
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
fi
# ShellCheck advisory
if [ -d "${SCRIPT_DIR}" ]; then
if ! command -v shellcheck >/dev/null 2>&1; then
sudo apt-get update -qq
@@ -720,7 +664,6 @@ jobs:
fi
fi
# SPDX header advisory for common source types
spdx_missing=()
IFS=',' read -r -a spdx_globs <<< "${SPDX_FILE_GLOBS}"
spdx_args=()
@@ -743,9 +686,8 @@ jobs:
} >> "${GITHUB_STEP_SUMMARY}"
fi
# Git hygiene advisory: branches older than 180 days (remote)
stale_cutoff_days=180
stale_branches="$(git for-each-ref --format='%(refname:short) %(committerdate:unix)' refs/remotes/origin 2>/dev/null | awk -v now="$(date +%s)" -v days="${stale_cutoff_days}" '{if (now-$2 [...]
stale_branches="$(git for-each-ref --format='%(refname:short) %(committerdate:unix)' refs/remotes/origin 2>/dev/null | awk -v now="$(date +%s)" -v days="${stale_cutoff_days}" '{if (now-$2 > days*86400) print $1}' | head -50)"
if [ -n "${stale_branches}" ]; then
extended_findings+=("Stale remote branches detected (advisory)")
{
@@ -787,3 +729,41 @@ jobs:
fi
printf '%s\n' 'Repository health guardrails passed.' >> "${GITHUB_STEP_SUMMARY}"
site-health:
name: Site Health
runs-on: ubuntu-latest
if: github.event_name == 'workflow_dispatch'
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
- name: Uptime check
if: env.URLS != ''
run: |
echo "$URLS" > /tmp/urls.txt
php monitoring/uptime-probe.php --urls /tmp/urls.txt --timeout 15 || echo "::warning::Some sites are down"
rm -f /tmp/urls.txt
env:
URLS: ${{ vars.MONITORED_URLS }}
- name: SSL certificate check
if: env.DOMAINS != ''
run: |
echo "$DOMAINS" > /tmp/domains.txt
php monitoring/ssl-check.php --domains /tmp/domains.txt --warn-days 30 || echo "::warning::SSL certificates expiring soon"
rm -f /tmp/domains.txt
env:
DOMAINS: ${{ vars.MONITORED_DOMAINS }}
- name: Summary
if: always()
run: |
echo "### Site Health" >> $GITHUB_STEP_SUMMARY
echo "Uptime and SSL checks completed." >> $GITHUB_STEP_SUMMARY
+98
View File
@@ -0,0 +1,98 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Security
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /.gitea/workflows/security-audit.yml
# VERSION: 01.00.00
# BRIEF: Dependency vulnerability scanning for composer and npm packages
name: "Universal: Security Audit"
on:
schedule:
- cron: '0 6 * * 1' # Weekly on Monday at 06:00 UTC
pull_request:
branches:
- main
paths:
- 'composer.json'
- 'composer.lock'
- 'package.json'
- 'package-lock.json'
workflow_dispatch:
permissions:
contents: read
env:
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }}
jobs:
audit:
name: Dependency Audit
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Composer audit
if: hashFiles('composer.lock') != ''
run: |
echo "=== Composer Security Audit ==="
if ! command -v composer &> /dev/null; then
sudo apt-get update -qq
sudo apt-get install -y -qq php-cli composer >/dev/null 2>&1
fi
composer audit --format=plain 2>&1 | tee /tmp/composer-audit.txt
RESULT=$?
if [ $RESULT -ne 0 ]; then
echo "::warning::Composer vulnerabilities found"
echo "composer_vulnerable=true" >> "$GITHUB_ENV"
else
echo "No known vulnerabilities in composer dependencies"
fi
- name: NPM audit
if: hashFiles('package-lock.json') != ''
run: |
echo "=== NPM Security Audit ==="
npm audit --production 2>&1 | tee /tmp/npm-audit.txt || true
if npm audit --production 2>&1 | grep -q "found 0 vulnerabilities"; then
echo "No known vulnerabilities in npm dependencies"
else
echo "::warning::NPM vulnerabilities found"
echo "npm_vulnerable=true" >> "$GITHUB_ENV"
fi
- name: Notify on vulnerabilities
if: env.composer_vulnerable == 'true' || env.npm_vulnerable == 'true'
run: |
REPO="${{ github.event.repository.name }}"
curl -sS \
-H "Title: ${REPO} has vulnerable dependencies" \
-H "Tags: lock,warning" \
-H "Priority: high" \
-d "Security audit found vulnerabilities. Review dependency updates." \
"${NTFY_URL}/${NTFY_TOPIC}" || true
- name: Joomla version audit
if: always()
run: |
if [ -f "monitoring/joomla-version-audit.php" ] && [ -n "$JOOMLA_SITES" ]; then
echo "$JOOMLA_SITES" > /tmp/sites.json
php monitoring/joomla-version-audit.php --sites /tmp/sites.json || true
echo "### Joomla Version Audit" >> $GITHUB_STEP_SUMMARY
rm -f /tmp/sites.json
else
echo "Joomla audit skipped (no script or JOOMLA_SITES_JSON not configured)"
fi
env:
JOOMLA_SITES: ${{ vars.JOOMLA_SITES_JSON }}
+126
View File
@@ -0,0 +1,126 @@
<!--
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
SPDX-License-Identifier: GPL-3.0-or-later
FILE INFORMATION
DEFGROUP: MokoStandards.Root
INGROUP: MokoStandards
REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
PATH: /CHANGELOG.md
BRIEF: Release changelog
-->
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
Version format: `XX.YY.ZZ` (zero-padded semver).
## [Unreleased]
## [05.00.00] - 2026-05-16
### Added
- `server-autoheal.sh` — boot-check, split system/content backups, self-installing with cron + systemd hook
- Grafana library panels: legend (list, right) and multi-tooltip options on all 14 panels
- Prometheus targets volume mount in monitoring Docker Compose
### Fixed
- MokoWaaS dashboard: remove `v_hidden` column — use explicit `filterFieldsByName` regex instead of broken `excludeByName`
- MokoWaaS dashboard: simplify probe queries (remove redundant `and on(site_name)` joins)
### Changed
- Rename `gitea-server-setup``.mokogitea-private` in workflow EXCLUDE lists
- Dolibarr Module ID Registry moved to MokoDolibarr wiki (moko-platform page is now a redirect)
## [04.09.00] - 2026-05-12
### Added
- `<deploy>` section support in `.manifest.xml` schema: `source-dir`, `remote-subdir`, `excludes`, `dev-host`, `demo-host`
- `manifest_read.php` now parses all deploy fields for CI consumption
### Changed
- Deploy workflows can now read deploy paths from manifest instead of guessing from directory structure
## [04.08.00] - 2026-05-12
### Added
- `cli/manifest_read.php` -- full `.manifest.xml` parser for CI consumption
- Supports `--field`, `--all`, `--json`, and `--github-output` modes
- Backward-compatible with `.moko-platform` (XML) and `.mokostandards` (YAML) formats
- Replaces inline `sed` detection blocks in workflows
### Changed
- Workflows (`auto-release`, `pre-release`, `pr-check`) now use `manifest_read.php` for platform detection
- `entry-point` field from manifest replaces `find` tree scan for mod file discovery
- Platform detection outputs all manifest fields to `GITHUB_OUTPUT` (name, org, language, package-type, etc.)
## [05.00.00] - 2026-05-11
### Added
- Centralized MokoWaaS Grafana dashboard for all Joomla sites (2-column layout)
- MokoStandards MCP server with 24 governance tools
- Wiki health check and GitHub wiki mirror sync
- Daily wiki sync workflow — mirrors all Gitea wikis to GitHub
- CHANGELOG `[Unreleased]` section check in repo health (5 pts)
- Client platform type with detection and structure definition
- PHPStan, Gitleaks, and Renovate — templates, workflows, and docs
- Cascade and branch protection workflow documentation
- Branch protection setup workflow
- Client-site definition
- Pre-release workflow for manual dev/alpha/beta/rc builds
- PR-check, security-audit, notify, cleanup workflow definitions
- Expanded workflow suite (10 workflows from MokoOnyx)
- `.gitea/workflows` definitions to Joomla structure defs
- Joomla workflow templates from MokoOnyx
- Cleanup script to remove `.claude/` and `.mcp.json` from repos
- Auto-discover all repos with wikis across all orgs
- CLAUDE.md to repo health check, flag unwanted files
- `.moko-platform` manifest (replaces `.mokostandards`)
- PR branch policy check workflow
### Changed
- Major version bump: `04.05.00``05.00.00` across all definitions, templates, and wiki
- Grafana endpoint dashboards: 2 columns per row (reduced congestion)
- Sync engine clones template repos at runtime for workflows
- Simplified platform types across definitions and sync engine
- Removed `templates/github` — all CI/templates now in `.gitea/`
- Removed `templates/workflows` — canonical source is now template repos
- Updated mokostandards xmlns to point to MokoStandards-API repo
- Comprehensive repo health check updates
### Fixed
- Remove gitea-actions[bot] from push whitelist (not a real user)
- Delete-then-create branch protection rules to avoid 422
- Patch version bump in pre-release workflow
- Always emit `<client>` tag in UpdateXmlGenerator
- Rewrite `updates.xml.template` with 5 stability channels
- Migrate `.mokostandards` from `.github/` to `.gitea/` on Gitea
## [04.05.00] - 2026-03-15
### Added
- Dual-platform support (Gitea + GitHub) and Joomla template tooling
- Templates, CLI dirs, docs, and Gitea-first platform config
- Sync to all branches, listBranches, ext-zip
- All templates from MokoStandards
### Changed
- Migrated to Gitea-only workflows and API
- Converted all gh CLI calls to Gitea API curl across workflow templates
- Gitea-primary tokens: GA_TOKEN for Gitea API, GH_TOKEN for GitHub mirror
- Updated all references to MokoConsulting org and Gitea URLs
### Fixed
- Guzzle base_uri resolution for Gitea API paths
- Replace all hardcoded GitHub API URLs with platform adapter pattern
- Split repoRoot into apiRoot + standardsRoot
- Auto-release template: use Gitea API for main sync, auth push URL
- Bulk_sync: resolve label names to IDs, fix username
- Remove sha256: prefix from update XML templates
## [04.00.00] - 2026-01-01
- Initial release: MokoStandards Enterprise API extracted from MokoStandards
+37
View File
@@ -0,0 +1,37 @@
# CLAUDE.md
This file provides guidance to Claude Code when working with this repository.
## Project Overview
**moko-platform** -- Enterprise automation, validation, sync, and governance engine for all Moko Consulting repositories
| Field | Value |
|---|---|
| **Platform** | generic |
| **Language** | HCL |
| **Default branch** | main |
| **License** | GPL-3.0-or-later |
| **Wiki** | [moko-platform Wiki](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki) |
| **Standards** | [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home) |
## Common Commands
```bash
composer install # Install PHP dependencies
```
## Architecture
See the [wiki](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki) for architecture details.
## Rules
- **Workflow directory**: `.mokogitea/` (not `.gitea/` or `.github/`)
- **Never commit** `.claude/`, `.mcp.json`, `TODO.md`, or `*.min.css`/`*.min.js`
- **Attribution**: use `Authored-by: Moko Consulting` in commits
- **Branch strategy**: develop on `dev`, merge to `main` for release
- **Minification**: handled at build time (CI) and runtime (MokoMinifyHelper for Joomla templates)
- **Wiki**: documentation lives in the Gitea wiki, not in `docs/` files
- **Standards**: this repo follows [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)
+40
View File
@@ -0,0 +1,40 @@
# Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
## Our Standards
Examples of behavior that contributes to a positive environment:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
Examples of unacceptable behavior:
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information without explicit permission
- Other conduct which could reasonably be considered inappropriate
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the project team at hello@mokoconsulting.tech.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/),
version 2.1.
---
*Moko Consulting <hello@mokoconsulting.tech>*
+30
View File
@@ -0,0 +1,30 @@
# Contributing to moko-platform
Thank you for your interest in contributing to the Moko Consulting platform.
## How to Contribute
1. **Fork** the repository
2. Create a **feature branch** from `dev` (e.g., `feature/my-feature`)
3. Make your changes following [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)
4. Submit a **Pull Request** targeting `dev`
## Branch Policy
- `feature/*`, `fix/*` branches target `dev`
- `hotfix/*` branches may target `dev` or `main`
- `dev` merges to `main` for releases
## Code Standards
- PHP: follow PSR-12, use tabs for indentation
- All files must include the Moko copyright header and SPDX identifier
- Scripts must be self-contained (no external dependencies unless via composer)
## Reporting Issues
Use the [issue tracker](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/issues) with the appropriate template.
---
*Moko Consulting <hello@mokoconsulting.tech>*
+35
View File
@@ -0,0 +1,35 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
For the full license text, see <https://www.gnu.org/licenses/gpl-3.0.txt>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
+11
View File
@@ -1,3 +1,14 @@
<!--
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
SPDX-License-Identifier: GPL-3.0-or-later
FILE INFORMATION
DEFGROUP: MokoStandards.Root
INGROUP: MokoStandards
REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
PATH: /PLUGIN_SCRIPTS.md
BRIEF: Plugin system CLI documentation
-->
# Plugin System CLI Scripts
Command-line scripts for validating, health checking, and managing projects using the MokoStandards plugin system.
+12 -1
View File
@@ -1,3 +1,14 @@
<!--
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
SPDX-License-Identifier: GPL-3.0-or-later
FILE INFORMATION
DEFGROUP: MokoStandards.Root
INGROUP: MokoStandards
REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
PATH: /README.md
BRIEF: Project overview and documentation
-->
# MokoStandards Enterprise API
PHP implementation of MokoStandards — enterprise standards, automation framework, workflow templates, and bulk sync tooling.
@@ -17,7 +28,7 @@ PHP implementation of MokoStandards — enterprise standards, automation framewo
| `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 |
| `tools/` | Standalone tools (legal doc generator) |
| `tests/` | PHPUnit test suite |
## Installation
+11
View File
@@ -1,3 +1,14 @@
<!--
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
SPDX-License-Identifier: GPL-3.0-or-later
FILE INFORMATION
DEFGROUP: MokoStandards.Index
INGROUP: MokoStandards.Analysis
REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
PATH: /analysis/index.md
BRIEF: Analysis directory index
-->
# Docs Index: /api/analysis
## Purpose
+11 -11
View File
@@ -10,16 +10,16 @@
* FILE INFORMATION
* DEFGROUP: MokoStandards.Automation
* INGROUP: MokoStandards.Scripts
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /automation/bulk_joomla_template.php
* VERSION: 04.06.10
* BRIEF: Bulk scaffold and sync Joomla template repositories
*
* USAGE
* php api/automation/bulk_joomla_template.php --scaffold --name=MokoTheme
* php api/automation/bulk_joomla_template.php --scaffold --name=MokoTheme --client=administrator
* php api/automation/bulk_joomla_template.php --sync --repos=MokoTheme,MokoDarkTheme
* php api/automation/bulk_joomla_template.php --sync --all
* php api/automation/bulk_joomla_template.php --list
* php automation/bulk_joomla_template.php --scaffold --name=MokoTheme
* php automation/bulk_joomla_template.php --scaffold --name=MokoTheme --client=administrator
* php automation/bulk_joomla_template.php --sync --repos=MokoTheme,MokoDarkTheme
* php automation/bulk_joomla_template.php --sync --all
* php automation/bulk_joomla_template.php --list
*/
declare(strict_types=1);
@@ -717,13 +717,13 @@ class BulkJoomlaTemplate extends CLIApp
// ── Sync updates.xml between platforms ───────────────────────────────
/**
* Sync updates.xml (or update.xml) between Gitea and GitHub for Joomla repos.
* Sync updates.xml (or updates.xml) between Gitea and GitHub for Joomla repos.
*
* Reads the file from both platforms, compares by latest <version> tag,
* and pushes the newer one to the stale platform.
*
* Designed to be called from a CI workflow via:
* php api/automation/bulk_joomla_template.php --sync-updates --repos=MokoCassiopeia
* php automation/bulk_joomla_template.php --sync-updates --repos=MokoCassiopeia
*/
private function syncUpdatesBetweenPlatforms(string $org): int
{
@@ -788,7 +788,7 @@ class BulkJoomlaTemplate extends CLIApp
$name = $repo['name'];
$this->log("\n[{$name}]", 'INFO');
// Try both update.xml and updates.xml filenames
// Try both updates.xml and updates.xml filenames
$updateFile = $this->resolveUpdateFile($gitea, $github, $org, $name);
if ($updateFile === null) {
$this->log(" ⊘ No update(s).xml found on either platform", 'INFO');
@@ -849,7 +849,7 @@ class BulkJoomlaTemplate extends CLIApp
/**
* Find the updates file on both platforms, return the one with the higher version.
*
* Checks both `updates.xml` and `update.xml` filenames.
* Checks both `updates.xml` and `updates.xml` filenames.
* Returns the content from the platform with the newer <version>.
* Gitea wins ties (primary platform).
*
@@ -861,7 +861,7 @@ class BulkJoomlaTemplate extends CLIApp
string $org,
string $name
): ?array {
$candidates = ['updates.xml', 'update.xml'];
$candidates = ['updates.xml', 'updates.xml'];
$found = []; // platform => [name, content, version]
foreach (['gitea' => $gitea, 'github' => $github] as $platform => $adapter) {
+4 -5
View File
@@ -10,9 +10,8 @@
* FILE INFORMATION
* DEFGROUP: MokoStandards.Automation
* INGROUP: MokoStandards.Scripts
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /automation/bulk_sync.php
* VERSION: 04.06.00
* BRIEF: Enterprise-grade bulk repository synchronization
*/
@@ -420,7 +419,7 @@ class BulkSync extends CLIApp
$this->log("The bulk repository sync is failing silently because the core", 'ERROR');
$this->log("synchronization logic has not been implemented yet.", 'ERROR');
$this->log("", 'ERROR');
$this->log("Location: api/lib/Enterprise/RepositorySynchronizer.php", 'ERROR');
$this->log("Location: lib/Enterprise/RepositorySynchronizer.php", 'ERROR');
$this->log("Method: processRepository()", 'ERROR');
$this->log("", 'ERROR');
$this->log("Required Implementation:", 'ERROR');
@@ -509,7 +508,7 @@ class BulkSync extends CLIApp
]);
$script = basename(__FILE__);
$this->log("💾 Checkpoint saved. To resume once the issue is resolved, run:", 'INFO');
$this->log(" php api/automation/{$script} --resume [same flags as before]", 'INFO');
$this->log(" php automation/{$script} --resume [same flags as before]", 'INFO');
} catch (\Exception $e) {
$this->log("⚠️ Failed to save interrupt checkpoint: " . $e->getMessage(), 'WARN');
}
@@ -1355,7 +1354,7 @@ class BulkSync extends CLIApp
1. Check the local audit log or re-run with `--repos=<repo>` to see the specific error.
2. Fix the underlying issue (API token, rate limit, branch protection, etc.).
3. Re-run: `php api/automation/bulk_sync.php --org={$org} --repos=<repo> --force --yes`
3. Re-run: `php automation/bulk_sync.php --org={$org} --repos=<repo> --force --yes`
4. Close this issue once all repos are synced successfully.
---
+123
View File
@@ -0,0 +1,123 @@
#!/usr/bin/env bash
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# SPDX-License-Identifier: GPL-3.0-or-later
# BRIEF: Trigger a workflow across all client-waas repos in a Gitea org
set -euo pipefail
# ---------------------------------------------------------------------------
# Usage
# ---------------------------------------------------------------------------
usage() {
cat <<EOF
Usage: $(basename "$0") GITEA_URL TOKEN ORG WORKFLOW [REF] [INPUTS]
Arguments:
GITEA_URL Base URL of the Gitea instance (e.g. https://git.mokoconsulting.tech)
TOKEN Gitea API token with repo/action permissions
ORG Organisation or user that owns the repos
WORKFLOW Workflow filename to trigger (e.g. dependency-audit.yml)
REF Branch ref to run against (default: main)
INPUTS Optional JSON object of workflow inputs (e.g. '{"dry_run":"true"}')
Example:
$(basename "$0") https://git.mokoconsulting.tech abc123 MokoConsulting dependency-audit.yml main '{"notify":"true"}'
EOF
exit 1
}
# ---------------------------------------------------------------------------
# Argument parsing
# ---------------------------------------------------------------------------
if [ $# -lt 4 ]; then
usage
fi
GITEA_URL="${1%/}"
TOKEN="$2"
ORG="$3"
WORKFLOW="$4"
REF="${5:-main}"
INPUTS="${6:-{\}}"
# ---------------------------------------------------------------------------
# Fetch all repos in the org, paginated
# ---------------------------------------------------------------------------
echo "Fetching repos for org '${ORG}' on ${GITEA_URL} ..."
PAGE=1
LIMIT=50
ALL_REPOS=""
while true; do
RESPONSE=$(curl -s \
-H "Authorization: token ${TOKEN}" \
-H "Accept: application/json" \
"${GITEA_URL}/api/v1/orgs/${ORG}/repos?page=${PAGE}&limit=${LIMIT}")
# Break if empty array
COUNT=$(echo "$RESPONSE" | jq -r 'length')
if [ "$COUNT" -eq 0 ]; then
break
fi
NAMES=$(echo "$RESPONSE" | jq -r '.[].name')
ALL_REPOS="${ALL_REPOS}${NAMES}"$'\n'
if [ "$COUNT" -lt "$LIMIT" ]; then
break
fi
PAGE=$((PAGE + 1))
done
# ---------------------------------------------------------------------------
# Filter for client-waas repos
# ---------------------------------------------------------------------------
CLIENT_REPOS=$(echo "$ALL_REPOS" | grep 'client-waas' | sort || true)
if [ -z "$CLIENT_REPOS" ]; then
echo "No client-waas repos found in org '${ORG}'."
exit 0
fi
TOTAL=$(echo "$CLIENT_REPOS" | wc -l | tr -d ' ')
echo "Found ${TOTAL} client-waas repo(s). Triggering workflow '${WORKFLOW}' (ref: ${REF}) ..."
echo ""
# ---------------------------------------------------------------------------
# Trigger workflow for each repo
# ---------------------------------------------------------------------------
SUCCESS=0
FAIL=0
while IFS= read -r REPO; do
[ -z "$REPO" ] && continue
PAYLOAD=$(jq -n --arg ref "$REF" --argjson inputs "$INPUTS" '{ref: $ref, inputs: $inputs}')
HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' \
-X POST \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
-d "$PAYLOAD" \
"${GITEA_URL}/api/v1/repos/${ORG}/${REPO}/actions/workflows/${WORKFLOW}/dispatches")
if [ "$HTTP_CODE" -eq 204 ] || [ "$HTTP_CODE" -eq 201 ]; then
echo " [OK] ${ORG}/${REPO} (HTTP ${HTTP_CODE})"
SUCCESS=$((SUCCESS + 1))
else
echo " [FAIL] ${ORG}/${REPO} (HTTP ${HTTP_CODE})"
FAIL=$((FAIL + 1))
fi
done <<< "$CLIENT_REPOS"
# ---------------------------------------------------------------------------
# Summary
# ---------------------------------------------------------------------------
echo ""
echo "Done. Success: ${SUCCESS} | Failed: ${FAIL} | Total: ${TOTAL}"
if [ "$FAIL" -gt 0 ]; then
exit 1
fi
+108
View File
@@ -0,0 +1,108 @@
#!/usr/bin/env bash
# =============================================================================
# enforce_tags.sh — Ensure all repos have the 5 standard release channel tags
#
# Standard tags: development, alpha, beta, release-candidate, stable
# Also removes non-standard tags (keeps vXX production tags)
#
# Usage:
# GA_TOKEN=xxx ./enforce_tags.sh [--dry-run] [--repos repo1,repo2]
#
# Called by: bulk-repo-sync.yml, infrastructure-tests/mirror-check.yml
# =============================================================================
set -euo pipefail
GITEA_URL="${GITEA_URL:-https://git.mokoconsulting.tech}"
ORG="${GITEA_ORG:-MokoConsulting}"
TOKEN="${GA_TOKEN:?GA_TOKEN required}"
DRY_RUN=false
FILTER_REPOS=""
STANDARD_TAGS=("development" "alpha" "beta" "release-candidate" "stable")
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run) DRY_RUN=true; shift ;;
--repos) FILTER_REPOS="$2"; shift 2 ;;
*) shift ;;
esac
done
api() {
local method="$1" path="$2" data="${3:-}"
local args=(-sf -H "Authorization: token $TOKEN" -H "Content-Type: application/json" -X "$method")
[[ -n "$data" ]] && args+=(-d "$data")
curl "${args[@]}" "$GITEA_URL/api/v1$path" 2>/dev/null
}
# Get repos
REPOS=""
for page in 1 2 3; do
BATCH=$(api GET "/orgs/$ORG/repos?limit=50&page=$page" | python3 -c "
import sys,json
for r in json.load(sys.stdin):
if not r.get(empty) and not r.get(archived):
print(r[name])
" 2>/dev/null)
[[ -z "$BATCH" ]] && break
REPOS="$REPOS $BATCH"
done
# Filter if specified
if [[ -n "$FILTER_REPOS" ]]; then
FILTERED=""
IFS=, read -ra FILTER_ARR <<< "$FILTER_REPOS"
for repo in $REPOS; do
for f in "${FILTER_ARR[@]}"; do
[[ "$repo" == "$f" ]] && FILTERED="$FILTERED $repo"
done
done
REPOS="$FILTERED"
fi
TOTAL=$(echo $REPOS | wc -w)
ADDED=0
DELETED=0
ERRORS=0
echo "Enforcing tags on $TOTAL repos (dry_run=$DRY_RUN)"
for repo in $REPOS; do
TAGS=$(api GET "/repos/$ORG/$repo/tags?limit=50" | python3 -c "import sys,json; print( .join(t[name] for t in json.load(sys.stdin)))" 2>/dev/null)
MAIN_SHA=$(api GET "/repos/$ORG/$repo/branches/main" | python3 -c "import sys,json; print(json.load(sys.stdin)[commit][id])" 2>/dev/null)
[[ -z "$MAIN_SHA" ]] && continue
# Add missing standard tags
for st in "${STANDARD_TAGS[@]}"; do
if ! echo " $TAGS " | grep -q " $st "; then
if [[ "$DRY_RUN" == "true" ]]; then
echo " [DRY] ADD $repo: $st"
else
STATUS=$(api POST "/repos/$ORG/$repo/tags" "{\"tag_name\":\"$st\",\"target\":\"$MAIN_SHA\"}" | python3 -c "import sys,json; print(ok)" 2>/dev/null || echo "err")
[[ "$STATUS" == "ok" ]] && ADDED=$((ADDED + 1)) || ERRORS=$((ERRORS + 1))
fi
fi
done
# Remove non-standard tags
for t in $TAGS; do
IS_STD=false
for st in "${STANDARD_TAGS[@]}"; do [[ "$t" == "$st" ]] && IS_STD=true; done
# Keep vXX production tags
if [[ "$t" =~ ^v[0-9]{1,3}$ ]]; then IS_STD=true; fi
if [[ "$IS_STD" == "false" ]]; then
if [[ "$DRY_RUN" == "true" ]]; then
echo " [DRY] DEL $repo: $t"
else
# Delete release first if exists
api DELETE "/repos/$ORG/$repo/releases/tags/$t" > /dev/null 2>&1 || true
api DELETE "/repos/$ORG/$repo/tags/$t" > /dev/null 2>&1
DELETED=$((DELETED + 1))
echo " DEL $repo: $t"
fi
fi
done
done
echo "Done: $ADDED added, $DELETED deleted, $ERRORS errors (dry_run=$DRY_RUN)"
+315
View File
@@ -0,0 +1,315 @@
#!/usr/bin/env php
<?php
/**
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: MokoStandards.Automation
* INGROUP: MokoStandards
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /automation/enrich_mokostandards_xml.php
* BRIEF: Enrich XML manifests with repo-specific build and deploy details
*
* Enrich XML .mokostandards manifests with repo-specific build, deploy, and script details.
*
* Runs AFTER push_mokostandards_xml.php. Clones each repo, inspects its contents,
* and updates the manifest with discovered build/deploy/scripts config.
*
* Usage:
* php automation/enrich_mokostandards_xml.php [--dry-run] [--repo NAME] [--skip NAME,NAME]
*
* Note: This script uses proc_open for shell commands. All arguments are escaped
* via escapeshellarg(). No user-supplied input reaches the shell unescaped.
*/
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use MokoEnterprise\MokoStandardsParser;
$giteaUrl = rtrim(getenv('GITEA_URL') ?: 'https://git.mokoconsulting.tech', '/');
$giteaOrg = getenv('GITEA_ORG') ?: 'MokoConsulting';
$token = getenv('GA_TOKEN') ?: getenv('GH_TOKEN') ?: '';
$dryRun = in_array('--dry-run', $argv, true);
$repoFilter = null;
$skipRepos = [];
foreach ($argv as $i => $arg) {
if ($arg === '--repo' && isset($argv[$i + 1])) $repoFilter = $argv[$i + 1];
if ($arg === '--skip' && isset($argv[$i + 1])) $skipRepos = array_map('trim', explode(',', $argv[$i + 1]));
}
$parser = new MokoStandardsParser();
$tmpBase = sys_get_temp_dir() . '/moko-enrich-' . getmypid();
function safeExec(string $command, string $cwd = '.'): array {
$proc = proc_open($command, [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes, $cwd);
if (!is_resource($proc)) return [1, "proc_open failed"];
$stdout = stream_get_contents($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[1]); fclose($pipes[2]);
return [proc_close($proc), trim($stdout . "\n" . $stderr)];
}
function rmTree(string $dir): void {
if (!is_dir($dir)) return;
$it = new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS);
$files = new RecursiveIteratorIterator($it, RecursiveIteratorIterator::CHILD_FIRST);
foreach ($files as $file) {
if ($file->isDir()) @rmdir($file->getPathname());
else { @chmod($file->getPathname(), 0777); @unlink($file->getPathname()); }
}
@rmdir($dir);
}
function gitCmd(string $workDir, string ...$args): array {
$cmd = 'git';
foreach ($args as $a) $cmd .= ' ' . escapeshellarg($a);
return safeExec($cmd, $workDir);
}
function fetchRepos(string $url, string $org, string $token): array {
$repos = []; $page = 1;
do {
$ch = curl_init("{$url}/api/v1/orgs/{$org}/repos?page={$page}&limit=50");
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ["Authorization: token {$token}"], CURLOPT_TIMEOUT => 30]);
$body = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch);
if ($code !== 200) break;
$batch = json_decode($body, true); if (empty($batch)) break;
$repos = array_merge($repos, $batch); $page++;
} while (count($batch) >= 50);
return $repos;
}
function inspectRepo(string $workDir, string $platform): array {
$enrichment = [];
$build = [];
// Detect entry point
if (is_dir("{$workDir}/src")) {
foreach (glob("{$workDir}/src/*.xml") ?: [] as $xf) {
$c = file_get_contents($xf);
if (str_contains($c, '<extension') || str_contains($c, '<install')) {
$build['entry_point'] = 'src/' . basename($xf); break;
}
}
foreach (glob("{$workDir}/src/core/modules/mod*.class.php") ?: [] as $mf) {
$build['entry_point'] = str_replace("{$workDir}/", '', $mf); break;
}
}
// composer.json
if (file_exists("{$workDir}/composer.json")) {
$composer = json_decode(file_get_contents("{$workDir}/composer.json"), true) ?: [];
$phpReq = $composer['require']['php'] ?? null;
if ($phpReq) $build['runtime'] = "php:{$phpReq}";
$deps = [];
foreach (['joomla/cms', 'joomla/framework', 'dolibarr/dolibarr'] as $pd) {
if (isset($composer['require'][$pd])) $deps[] = ['name' => $pd, 'version' => $composer['require'][$pd], 'type' => 'platform'];
}
if (isset($composer['require']['mokoconsulting-tech/enterprise']))
$deps[] = ['name' => 'mokoconsulting-tech/enterprise', 'version' => $composer['require']['mokoconsulting-tech/enterprise'], 'type' => 'composer'];
if (!empty($deps)) $build['dependencies'] = $deps;
}
// Artifact from Makefile
if (file_exists("{$workDir}/Makefile")) {
$mk = file_get_contents("{$workDir}/Makefile");
if (preg_match('/\bdist\/(\S+\.zip)\b/', $mk, $m)) $build['artifact'] = ['format' => 'zip', 'path' => 'dist/', 'filename' => $m[1]];
}
if (!empty($build)) $enrichment['build'] = $build;
// Deploy targets from workflows
$targets = [];
$wfDir = is_dir("{$workDir}/.gitea/workflows") ? "{$workDir}/.gitea/workflows" : "{$workDir}/.github/workflows";
if (is_dir($wfDir)) {
foreach (['deploy-dev', 'deploy-demo', 'deploy-rs'] as $dn) {
$wf = "{$wfDir}/{$dn}.yml";
if (!file_exists($wf)) continue;
$wc = file_get_contents($wf);
$t = ['name' => str_replace('deploy-', '', $dn)];
if (str_contains($wc, 'sftp') || str_contains($wc, 'SFTP')) $t['method'] = 'sftp';
elseif (str_contains($wc, 'rsync')) $t['method'] = 'rsync';
if (str_contains($wc, 'src/')) $t['src_dir'] = 'src/';
if (preg_match('/branches:\s*\n\s*-\s*["\']?([^"\'}\s]+)/', $wc, $m)) $t['branch'] = $m[1];
$targets[] = $t;
}
}
if (!empty($targets)) $enrichment['deploy'] = $targets;
// Scripts from Makefile + composer
$scripts = [];
if (file_exists("{$workDir}/Makefile")) {
$mk = file_get_contents("{$workDir}/Makefile");
$known = ['build'=>'build','test'=>'test','lint'=>'lint','clean'=>'build','package'=>'build','validate'=>'validate','release'=>'release'];
if (preg_match_all('/^([a-zA-Z_-]+)\s*:/m', $mk, $matches)) {
foreach ($matches[1] as $tgt) {
$tl = strtolower($tgt);
if (isset($known[$tl])) $scripts[] = ['name'=>$tl, 'phase'=>$known[$tl], 'command'=>"make {$tgt}", 'desc'=>ucfirst($tl).' via make', 'runner'=>'make'];
}
}
}
if (file_exists("{$workDir}/composer.json")) {
$composer = json_decode(file_get_contents("{$workDir}/composer.json"), true) ?: [];
$km = ['test'=>'test','lint'=>'lint','cs'=>'lint','phpcs'=>'lint','phpstan'=>'lint','validate'=>'validate'];
foreach ($composer['scripts'] ?? [] as $sn => $cmd) {
$sl = strtolower($sn);
foreach ($km as $match => $phase) {
if (str_contains($sl, $match)) {
$exists = false;
foreach ($scripts as $s) { if ($s['name'] === $sl) { $exists = true; break; } }
if (!$exists) $scripts[] = ['name'=>$sn, 'phase'=>$phase, 'command'=>"composer run {$sn}", 'desc'=>is_string($cmd)?$cmd:"Run {$sn}", 'runner'=>'composer'];
break;
}
}
}
}
if (!empty($scripts)) $enrichment['scripts'] = $scripts;
return $enrichment;
}
function enrichManifestXml(string $xml, array $enrichment): string {
$dom = new DOMDocument('1.0', 'UTF-8');
$dom->preserveWhiteSpace = false;
$dom->formatOutput = true;
if (!$dom->loadXML($xml)) return $xml;
$ns = MokoStandardsParser::NAMESPACE_URI;
$root = $dom->documentElement;
foreach (['build', 'deploy', 'scripts'] as $tag) {
$toRemove = [];
$existing = $root->getElementsByTagNameNS($ns, $tag);
for ($i = 0; $i < $existing->length; $i++) $toRemove[] = $existing->item($i);
foreach ($toRemove as $node) $root->removeChild($node);
}
if (!empty($enrichment['build'])) {
$build = $dom->createElementNS($ns, 'build');
$b = $enrichment['build'];
foreach (['language', 'runtime'] as $f) { if (isset($b[$f])) $build->appendChild($dom->createElementNS($ns, $f, htmlspecialchars($b[$f], ENT_XML1))); }
if (isset($b['package_type'])) $build->appendChild($dom->createElementNS($ns, 'package-type', htmlspecialchars($b['package_type'], ENT_XML1)));
if (isset($b['entry_point'])) $build->appendChild($dom->createElementNS($ns, 'entry-point', htmlspecialchars($b['entry_point'], ENT_XML1)));
if (isset($b['artifact'])) {
$art = $dom->createElementNS($ns, 'artifact');
foreach (['format','path','filename'] as $af) { if (isset($b['artifact'][$af])) $art->appendChild($dom->createElementNS($ns, $af, htmlspecialchars($b['artifact'][$af], ENT_XML1))); }
$build->appendChild($art);
}
if (isset($b['dependencies'])) {
$deps = $dom->createElementNS($ns, 'dependencies');
foreach ($b['dependencies'] as $d) {
$req = $dom->createElementNS($ns, 'requires', '');
$req->setAttribute('name', $d['name']);
if (isset($d['version'])) $req->setAttribute('version', $d['version']);
if (isset($d['type'])) $req->setAttribute('type', $d['type']);
$deps->appendChild($req);
}
$build->appendChild($deps);
}
$root->appendChild($build);
}
if (!empty($enrichment['deploy'])) {
$deploy = $dom->createElementNS($ns, 'deploy');
foreach ($enrichment['deploy'] as $t) {
$target = $dom->createElementNS($ns, 'target');
$target->setAttribute('name', $t['name']);
$target->appendChild($dom->createElementNS($ns, 'host', '${{ secrets.' . strtoupper($t['name']) . '_HOST }}'));
$target->appendChild($dom->createElementNS($ns, 'path', '${{ secrets.' . strtoupper($t['name']) . '_PATH }}'));
if (isset($t['method'])) $target->appendChild($dom->createElementNS($ns, 'method', $t['method']));
if (isset($t['branch'])) $target->appendChild($dom->createElementNS($ns, 'branch', htmlspecialchars($t['branch'], ENT_XML1)));
if (isset($t['src_dir'])) $target->appendChild($dom->createElementNS($ns, 'src-dir', htmlspecialchars($t['src_dir'], ENT_XML1)));
$deploy->appendChild($target);
}
$root->appendChild($deploy);
}
if (!empty($enrichment['scripts'])) {
$scriptsEl = $dom->createElementNS($ns, 'scripts');
foreach ($enrichment['scripts'] as $s) {
$script = $dom->createElementNS($ns, 'script');
$script->setAttribute('name', $s['name']);
if (isset($s['phase'])) $script->setAttribute('phase', $s['phase']);
$script->appendChild($dom->createElementNS($ns, 'command', htmlspecialchars($s['command'], ENT_XML1)));
if (isset($s['desc'])) $script->appendChild($dom->createElementNS($ns, 'description', htmlspecialchars($s['desc'], ENT_XML1)));
if (isset($s['runner'])) $script->appendChild($dom->createElementNS($ns, 'runner', htmlspecialchars($s['runner'], ENT_XML1)));
$scriptsEl->appendChild($script);
}
$root->appendChild($scriptsEl);
}
return $dom->saveXML();
}
// ── Main ─────────────────────────────────────────────────────────────────
echo "=== MokoStandards XML Manifest Enrichment ===\n";
echo "Mode: " . ($dryRun ? "DRY RUN" : "LIVE") . "\n";
if (!empty($skipRepos)) echo "Skipping: " . implode(', ', $skipRepos) . "\n";
echo "\n";
if (empty($token)) { fprintf(STDERR, "ERROR: GA_TOKEN required\n"); exit(1); }
$repos = fetchRepos($giteaUrl, $giteaOrg, $token);
echo "Found " . count($repos) . " repositories\n\n";
$stats = ['enriched' => 0, 'skipped' => 0, 'failed' => 0];
foreach ($repos as $repo) {
$name = $repo['name'];
if ($repoFilter && $name !== $repoFilter) continue;
if (in_array($name, $skipRepos, true)) { echo " {$name} ... SKIP (excluded)\n"; $stats['skipped']++; continue; }
if ($repo['archived'] ?? false) { $stats['skipped']++; continue; }
$defaultBranch = $repo['default_branch'] ?? 'main';
$httpsUrl = $repo['clone_url'] ?? "{$giteaUrl}/{$giteaOrg}/{$name}.git";
$authedUrl = preg_replace('#^https://#', "https://gitea-actions:{$token}@", $httpsUrl);
echo " {$name} ... ";
$workDir = "{$tmpBase}/{$name}";
@mkdir($workDir, 0755, true);
[$ret] = safeExec('git clone --depth 1 --branch ' . escapeshellarg($defaultBranch) . ' ' . escapeshellarg($authedUrl) . ' ' . escapeshellarg($workDir));
if ($ret !== 0) { echo "FAIL (clone)\n"; $stats['failed']++; continue; }
$manifestPath = "{$workDir}/.mokogitea/.mokostandards";
if (!file_exists($manifestPath) || !str_contains(file_get_contents($manifestPath), '<mokostandards')) {
echo "SKIP (no XML manifest)\n"; $stats['skipped']++; rmTree($workDir); continue;
}
$existingXml = file_get_contents($manifestPath);
$platform = $parser->extractPlatform($existingXml) ?? 'default-repository';
$enrichment = inspectRepo($workDir, $platform);
if (!isset($enrichment['build'])) $enrichment['build'] = [];
$enrichment['build']['language'] = $enrichment['build']['language'] ?? $repo['language'] ?? MokoStandardsParser::platformLanguage($platform);
$enrichment['build']['package_type'] = $enrichment['build']['package_type'] ?? MokoStandardsParser::platformPackageType($platform);
$enrichedXml = enrichManifestXml($existingXml, $enrichment);
$dc = count($enrichment['deploy'] ?? []);
$sc = count($enrichment['scripts'] ?? []);
$details = "deploy={$dc} scripts={$sc}";
if ($dryRun) { echo "WOULD ENRICH [{$details}]\n"; $stats['enriched']++; rmTree($workDir); continue; }
file_put_contents($manifestPath, $enrichedXml);
gitCmd($workDir, 'config', 'user.name', 'gitea-actions[bot]');
gitCmd($workDir, 'config', 'user.email', 'gitea-actions[bot]@git.mokoconsulting.tech');
gitCmd($workDir, 'add', '.mokogitea/.mokostandards');
[$cr, $co] = gitCmd($workDir, 'commit', '-m', "chore: enrich .mokostandards with build/deploy/scripts\n\nAuto-detected: {$details}");
if ($cr !== 0) { echo "SKIP (no diff)\n"; $stats['skipped']++; rmTree($workDir); continue; }
[$pr] = gitCmd($workDir, 'push', 'origin', $defaultBranch);
if ($pr !== 0) { echo "FAIL (push)\n"; $stats['failed']++; }
else { echo "ENRICHED [{$details}]\n"; $stats['enriched']++; }
rmTree($workDir);
}
@rmdir($tmpBase);
echo "\n=== Summary ===\nEnriched: {$stats['enriched']}\nSkipped: {$stats['skipped']}\nFailed: {$stats['failed']}\n";
+11
View File
@@ -1,3 +1,14 @@
<!--
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
SPDX-License-Identifier: GPL-3.0-or-later
FILE INFORMATION
DEFGROUP: MokoStandards.Index
INGROUP: MokoStandards.Automation
REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
PATH: /automation/index.md
BRIEF: Automation directory index
-->
# Docs Index: /api/automation
## Purpose
+7 -8
View File
@@ -9,16 +9,15 @@
* FILE INFORMATION
* DEFGROUP: MokoStandards.Automation
* INGROUP: MokoStandards
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /automation/migrate_to_gitea.php
* VERSION: 04.06.10
* BRIEF: Migrate repositories from GitHub to self-hosted Gitea instance
*
* USAGE
* php api/automation/migrate_to_gitea.php --dry-run
* php api/automation/migrate_to_gitea.php --repos MokoCRM MokoDoliMods
* php api/automation/migrate_to_gitea.php --exclude MokoStandards --skip-archived
* php api/automation/migrate_to_gitea.php --resume
* php automation/migrate_to_gitea.php --dry-run
* php automation/migrate_to_gitea.php --repos MokoCRM MokoDoliMods
* php automation/migrate_to_gitea.php --exclude MokoStandards --skip-archived
* php automation/migrate_to_gitea.php --resume
*/
declare(strict_types=1);
@@ -30,7 +29,7 @@ use MokoEnterprise\CliFramework;
use MokoEnterprise\Config;
use MokoEnterprise\PlatformAdapterFactory;
use MokoEnterprise\GitHubAdapter;
use MokoEnterprise\GiteaAdapter;
use MokoEnterprise\MokoGiteaAdapter;
/**
* Gitea Migration Script
@@ -43,7 +42,7 @@ use MokoEnterprise\GiteaAdapter;
class MigrateToGitea extends CliFramework
{
private ?GitHubAdapter $github = null;
private ?GiteaAdapter $gitea = null;
private ?MokoGiteaAdapter $gitea = null;
private ?CheckpointManager $checkpoints = null;
protected function configure(): void
+2 -3
View File
@@ -10,9 +10,8 @@
* FILE INFORMATION
* DEFGROUP: MokoStandards.Automation
* INGROUP: MokoStandards.Scripts
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /automation/push_files.php
* VERSION: 04.06.00
* BRIEF: Push one or more specific files to one or more remote repositories
*/
@@ -590,7 +589,7 @@ class PushFiles extends CLIApp
1. Check the output above for the specific error per repo.
2. Fix the underlying issue (API token, branch permissions, file path, etc.).
3. Re-run: `php api/automation/push_files.php --org={$org} --repos=<repo> --files=<files> --yes`
3. Re-run: `php automation/push_files.php --org={$org} --repos=<repo> --files=<files> --yes`
4. Close this issue once resolved.
---
+315
View File
@@ -0,0 +1,315 @@
#!/usr/bin/env php
<?php
/**
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: MokoStandards.Automation
* INGROUP: MokoStandards
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /automation/push_mokostandards_xml.php
* BRIEF: Push XML manifests to all governed repositories
*
* Push XML .mokostandards manifest to all governed repositories.
*
* Uses git SSH to bypass the Gitea reverse-proxy WAF that blocks
* API requests to paths containing ".mokogitea".
*
* Usage:
* php automation/push_mokostandards_xml.php [--dry-run] [--repo NAME] [--force]
*/
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use MokoEnterprise\MokoStandardsParser;
// ── Configuration ────────────────────────────────────────────────────────
$giteaUrl = rtrim(getenv('GITEA_URL') ?: 'https://git.mokoconsulting.tech', '/');
$giteaOrg = getenv('GITEA_ORG') ?: 'MokoConsulting';
$token = getenv('GA_TOKEN') ?: getenv('GH_TOKEN') ?: '';
$sshBase = 'ssh://gitea@git.mokoconsulting.tech:2222';
// ── CLI args ─────────────────────────────────────────────────────────────
$dryRun = in_array('--dry-run', $argv, true);
$force = in_array('--force', $argv, true);
$repoFilter = null;
$skipRepos = [];
foreach ($argv as $i => $arg) {
if ($arg === '--repo' && isset($argv[$i + 1])) {
$repoFilter = $argv[$i + 1];
}
if ($arg === '--skip' && isset($argv[$i + 1])) {
$skipRepos = array_map('trim', explode(',', $argv[$i + 1]));
}
}
$parser = new MokoStandardsParser();
$tmpBase = sys_get_temp_dir() . '/moko-manifest-push-' . getmypid();
// ── Platform detection heuristics (mirrors RepositorySynchronizer) ───────
$CRM_PLATFORM_REPOS = ['MokoDolibarr', 'MokoDoliMods'];
function detectPlatform(array $repo): string {
global $CRM_PLATFORM_REPOS;
$name = $repo['name'] ?? '';
$nameLower = strtolower($name);
$description = strtolower($repo['description'] ?? '');
$topics = $repo['topics'] ?? [];
if (in_array($name, $CRM_PLATFORM_REPOS, true)) return 'crm-platform';
if (in_array('dolibarr-platform', $topics)) return 'crm-platform';
if (in_array('joomla-template', $topics)) return 'joomla-template';
if (in_array('joomla', $topics) || in_array('joomla-extension', $topics)) return 'waas-component';
if (in_array('dolibarr', $topics) || in_array('dolibarr-module', $topics)) return 'crm-module';
if (str_contains($nameLower, 'template') && (str_contains($nameLower, 'joomla') || str_contains($nameLower, 'tpl'))) return 'joomla-template';
if (str_contains($nameLower, 'joomla') || str_contains($nameLower, 'waas')) return 'waas-component';
if (str_contains($nameLower, 'doli') || str_contains($nameLower, 'crm')) return 'crm-module';
if (str_contains($description, 'joomla template')) return 'joomla-template';
if (str_contains($description, 'joomla') || str_contains($description, 'component')) return 'waas-component';
if (str_contains($description, 'dolibarr') || str_contains($description, 'module')) return 'crm-module';
if (str_contains($nameLower, 'standard')) return 'standards-repository';
return 'default-repository';
}
/**
* Safe shell execution — uses proc_open with explicit arguments to avoid injection.
* @return array{int, string}
*/
function safeExec(string $command, string $cwd = '.'): array {
$proc = proc_open(
$command,
[1 => ['pipe', 'w'], 2 => ['pipe', 'w']],
$pipes,
$cwd
);
if (!is_resource($proc)) {
return [1, "proc_open failed for: {$command}"];
}
$stdout = stream_get_contents($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
$code = proc_close($proc);
return [$code, trim($stdout . "\n" . $stderr)];
}
/** Recursively remove a directory (cross-platform). */
function rmTree(string $dir): void {
if (!is_dir($dir)) return;
$it = new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS);
$files = new RecursiveIteratorIterator($it, RecursiveIteratorIterator::CHILD_FIRST);
foreach ($files as $file) {
if ($file->isDir()) {
@rmdir($file->getPathname());
} else {
// Clear read-only flag (git objects on Windows)
@chmod($file->getPathname(), 0777);
@unlink($file->getPathname());
}
}
@rmdir($dir);
}
/**
* Run a git command safely in a given working directory.
* @return array{int, string}
*/
function gitCmd(string $workDir, string ...$args): array {
$cmd = 'git';
foreach ($args as $a) {
$cmd .= ' ' . escapeshellarg($a);
}
return safeExec($cmd, $workDir);
}
// ── Fetch all repos via API ──────────────────────────────────────────────
function fetchRepos(string $url, string $org, string $token): array {
$repos = [];
$page = 1;
do {
$ch = curl_init("{$url}/api/v1/orgs/{$org}/repos?page={$page}&limit=50");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: token {$token}"],
CURLOPT_TIMEOUT => 30,
]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code !== 200) {
fprintf(STDERR, "API error (HTTP %d) fetching repos page %d\n", $code, $page);
break;
}
$batch = json_decode($body, true);
if (empty($batch)) break;
$repos = array_merge($repos, $batch);
$page++;
} while (count($batch) >= 50);
return $repos;
}
// ── Main ─────────────────────────────────────────────────────────────────
echo "=== MokoStandards XML Manifest Push ===\n";
echo "Org: {$giteaOrg}\n";
echo "Mode: " . ($dryRun ? "DRY RUN" : "LIVE") . "\n";
if ($repoFilter) echo "Filter: {$repoFilter}\n";
echo "\n";
if (empty($token)) {
fprintf(STDERR, "ERROR: GA_TOKEN or GH_TOKEN environment variable required\n");
exit(1);
}
$repos = fetchRepos($giteaUrl, $giteaOrg, $token);
echo "Found " . count($repos) . " repositories\n\n";
$stats = ['created' => 0, 'updated' => 0, 'skipped' => 0, 'failed' => 0];
foreach ($repos as $repo) {
$name = $repo['name'];
if ($repoFilter && $name !== $repoFilter) continue;
if (in_array($name, $skipRepos, true)) {
echo " SKIP {$name} (excluded)\n";
$stats['skipped']++;
continue;
}
if ($repo['archived'] ?? false) {
echo " SKIP {$name} (archived)\n";
$stats['skipped']++;
continue;
}
$platform = detectPlatform($repo);
$defaultBranch = $repo['default_branch'] ?? 'main';
// Prefer HTTPS with token (SSH port 2222 may be blocked); fall back to SSH
$httpsUrl = $repo['clone_url'] ?? "{$giteaUrl}/{$giteaOrg}/{$name}.git";
// Embed token in HTTPS URL for push auth
$authedUrl = preg_replace('#^https://#', "https://gitea-actions:{$token}@", $httpsUrl);
echo " {$name} [{$platform}] ... ";
// Generate XML manifest
$xmlContent = $parser->generate([
'name' => $name,
'org' => $giteaOrg,
'platform' => $platform,
'standards_version' => '04.07.00',
'description' => $repo['description'] ?? '',
'license' => 'GPL-3.0-or-later',
'topics' => $repo['topics'] ?? [],
'language' => $repo['language'] ?? MokoStandardsParser::platformLanguage($platform),
'package_type' => MokoStandardsParser::platformPackageType($platform),
'last_synced' => date('c'),
]);
if ($dryRun) {
echo "WOULD WRITE ({$platform})\n";
$stats['created']++;
continue;
}
// Clone shallow via HTTPS (token-authed)
$workDir = "{$tmpBase}/{$name}";
@mkdir($workDir, 0755, true);
[$ret, $out] = safeExec(
'git clone --depth 1 --branch ' . escapeshellarg($defaultBranch) . ' '
. escapeshellarg($authedUrl) . ' ' . escapeshellarg($workDir)
);
if ($ret !== 0) {
echo "FAIL (clone)\n";
fprintf(STDERR, " %s\n", $out);
$stats['failed']++;
continue;
}
// Check if already XML and up-to-date
$manifestPath = "{$workDir}/.mokogitea/.mokostandards";
$existingIsXml = file_exists($manifestPath) && str_contains(file_get_contents($manifestPath), '<mokostandards');
if ($existingIsXml && !$force) {
$existingPlatform = $parser->extractPlatform(file_get_contents($manifestPath));
if ($existingPlatform === $platform) {
echo "SKIP (already XML)\n";
$stats['skipped']++;
rmTree($workDir);
continue;
}
}
// Write manifest
@mkdir("{$workDir}/.gitea", 0755, true);
file_put_contents($manifestPath, $xmlContent);
// Delete legacy files if present
$legacyDeleted = [];
foreach (['.mokostandards', '.github/.mokostandards'] as $legacy) {
$legacyPath = "{$workDir}/{$legacy}";
if (file_exists($legacyPath)) {
unlink($legacyPath);
$legacyDeleted[] = $legacy;
}
}
// Commit
$isNew = !$existingIsXml;
$commitMsg = $isNew
? 'chore: add XML .mokostandards manifest'
: 'chore: update .mokostandards to XML format';
if (!empty($legacyDeleted)) {
$commitMsg .= "\n\nRemoved legacy: " . implode(', ', $legacyDeleted);
}
gitCmd($workDir, 'config', 'user.name', 'gitea-actions[bot]');
gitCmd($workDir, 'config', 'user.email', 'gitea-actions[bot]@git.mokoconsulting.tech');
gitCmd($workDir, 'add', '.mokogitea/.mokostandards');
foreach ($legacyDeleted as $lf) {
gitCmd($workDir, 'add', $lf);
}
[$commitRet, $commitOut] = gitCmd($workDir, 'commit', '-m', $commitMsg);
if ($commitRet !== 0 && str_contains($commitOut, 'nothing to commit')) {
echo "SKIP (no changes)\n";
$stats['skipped']++;
rmTree($workDir);
continue;
}
if ($commitRet !== 0) {
echo "FAIL (commit)\n";
fprintf(STDERR, " %s\n", $commitOut);
$stats['failed']++;
rmTree($workDir);
continue;
}
[$pushRet, $pushOut] = gitCmd($workDir, 'push', 'origin', $defaultBranch);
if ($pushRet !== 0) {
echo "FAIL (push)\n";
fprintf(STDERR, " %s\n", $pushOut);
$stats['failed']++;
} else {
$action = $isNew ? 'CREATED' : 'UPDATED';
echo "{$action}\n";
$stats[$isNew ? 'created' : 'updated']++;
}
// Cleanup
rmTree($workDir);
}
// Cleanup tmp base
@rmdir($tmpBase);
echo "\n=== Summary ===\n";
echo "Created: {$stats['created']}\n";
echo "Updated: {$stats['updated']}\n";
echo "Skipped: {$stats['skipped']}\n";
echo "Failed: {$stats['failed']}\n";
+2 -3
View File
@@ -10,9 +10,8 @@
* FILE INFORMATION
* DEFGROUP: MokoStandards.Automation
* INGROUP: MokoStandards.Scripts
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /automation/repo_cleanup.php
* VERSION: 04.06.00
* BRIEF: Enterprise repository cleanup — branches, PRs, issues, workflows, labels, logs
*/
@@ -362,7 +361,7 @@ class RepoCleanup extends CLIApp
} catch (\Exception $e) { /* fallback to main */ }
// Check both workflow directories for retired workflows (supports dual-platform repos)
$wfDirs = array_unique(['.github/workflows', '.gitea/workflows', $this->adapter->getWorkflowDir()]);
$wfDirs = array_unique(['.github/workflows', '.mokogitea/workflows', $this->adapter->getWorkflowDir()]);
foreach (self::RETIRED_WORKFLOWS as $wf) {
foreach ($wfDirs as $wfDir) {
$path = "{$wfDir}/{$wf}";
+678
View File
@@ -0,0 +1,678 @@
#!/usr/bin/env bash
# server-autoheal.sh - Auto-heal on restart + split backup management
#
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# SPDX-License-Identifier: GPL-3.0-or-later
#
# DEFGROUP: MokoStandards.Automation.ServerAutoheal
# INGROUP: MokoStandards.Automation
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /automation/server-autoheal.sh
# BRIEF: Server auto-heal on unclean restart + split system/content backups
#
# Usage:
# server-autoheal.sh <command> [options]
#
# Commands:
# boot-check Run at boot — auto-heals if no safe point exists
# set-safepoint Mark current state as safe (call before planned shutdown)
# backup-system Run a system backup (configs, packages, services)
# backup-content Run a content backup (site files, databases, uploads)
# cleanup Prune expired backups per retention policy
# status Show safe point and backup status
#
# Scheduling (cron):
# @reboot server-autoheal.sh boot-check
# 0 3 * * * server-autoheal.sh backup-system (daily at 3am)
# 0 */2 * * * server-autoheal.sh backup-content (every 2 hours)
# 30 */2 * * * server-autoheal.sh cleanup (30 min after content backup)
set -euo pipefail
# ──────────────────────────────────────────────
# Configuration — override via /etc/moko/autoheal.conf
# ──────────────────────────────────────────────
CONF_FILE="/etc/moko/autoheal.conf"
[[ -f "$CONF_FILE" ]] && source "$CONF_FILE"
BACKUP_ROOT="${BACKUP_ROOT:-/var/backups/moko}"
SAFEPOINT_FILE="${SAFEPOINT_FILE:-/var/run/moko/safepoint}"
LOG_FILE="${LOG_FILE:-/var/log/moko/autoheal.log}"
LOCK_DIR="${LOCK_DIR:-/var/run/moko}"
# System backup: configs, package lists, service state, cron
SYSTEM_BACKUP_DIR="${BACKUP_ROOT}/system"
SYSTEM_BACKUP_RETAIN="${SYSTEM_BACKUP_RETAIN:-7}" # keep 7 daily system backups
# Content backup: web roots, databases, uploads
CONTENT_BACKUP_DIR="${BACKUP_ROOT}/content"
CONTENT_BACKUP_RETAIN_HOURS="${CONTENT_BACKUP_RETAIN_HOURS:-24}" # 1 day of content backups
# Paths to back up — override these in /etc/moko/autoheal.conf
SYSTEM_PATHS="${SYSTEM_PATHS:-/etc/nginx /etc/php /etc/mysql /etc/cron.d /etc/systemd/system}"
CONTENT_PATHS="${CONTENT_PATHS:-/var/www}"
DB_NAMES="${DB_NAMES:-}" # space-separated list, empty = auto-detect all
# ──────────────────────────────────────────────
# Helpers
# ──────────────────────────────────────────────
log() {
local level="$1"; shift
local ts
ts=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
local msg="[$ts] [$level] $*"
echo "$msg" | tee -a "$LOG_FILE" >&2
}
ensure_dirs() {
mkdir -p "$SYSTEM_BACKUP_DIR" "$CONTENT_BACKUP_DIR" \
"$LOCK_DIR" "$(dirname "$LOG_FILE")"
}
acquire_lock() {
local lockfile="${LOCK_DIR}/autoheal-${1}.lock"
if [[ -f "$lockfile" ]]; then
local pid
pid=$(<"$lockfile")
if kill -0 "$pid" 2>/dev/null; then
log WARN "Another $1 operation is running (PID $pid), skipping"
exit 0
fi
rm -f "$lockfile"
fi
echo $$ > "$lockfile"
trap "rm -f '$lockfile'" EXIT
}
timestamp() {
date -u '+%Y%m%d_%H%M%S'
}
# ──────────────────────────────────────────────
# Safe-point management
# ──────────────────────────────────────────────
cmd_set_safepoint() {
ensure_dirs
local ts
ts=$(timestamp)
cat > "$SAFEPOINT_FILE" <<EOF
timestamp=$ts
hostname=$(hostname)
kernel=$(uname -r)
uptime=$(uptime -s 2>/dev/null || echo "unknown")
set_by=${SUDO_USER:-$(whoami)}
EOF
log INFO "Safe point set at $ts by ${SUDO_USER:-$(whoami)}"
}
cmd_clear_safepoint() {
rm -f "$SAFEPOINT_FILE"
log INFO "Safe point cleared"
}
has_safepoint() {
[[ -f "$SAFEPOINT_FILE" ]]
}
# ──────────────────────────────────────────────
# System backup (daily)
# ──────────────────────────────────────────────
cmd_backup_system() {
ensure_dirs
acquire_lock "system-backup"
local ts
ts=$(timestamp)
local archive="${SYSTEM_BACKUP_DIR}/system_${ts}.tar.gz"
local manifest="${SYSTEM_BACKUP_DIR}/system_${ts}.manifest"
log INFO "Starting system backup → $archive"
# Collect existing paths only
local existing_paths=()
for p in $SYSTEM_PATHS; do
[[ -e "$p" ]] && existing_paths+=("$p")
done
if [[ ${#existing_paths[@]} -eq 0 ]]; then
log WARN "No system paths found to back up"
return 1
fi
# Archive configs and system files
tar -czf "$archive" "${existing_paths[@]}" 2>/dev/null || true
# Capture package list and service state as manifest
{
echo "=== PACKAGES ==="
if command -v dpkg &>/dev/null; then
dpkg --get-selections
elif command -v rpm &>/dev/null; then
rpm -qa --qf '%{NAME}\t%{VERSION}\n'
fi
echo ""
echo "=== ENABLED SERVICES ==="
if command -v systemctl &>/dev/null; then
systemctl list-unit-files --state=enabled --no-pager 2>/dev/null || true
fi
echo ""
echo "=== CRONTABS ==="
for user_home in /var/spool/cron/crontabs/*; do
[[ -f "$user_home" ]] && echo "--- $(basename "$user_home") ---" && cat "$user_home"
done 2>/dev/null || true
} > "$manifest"
local size
size=$(du -sh "$archive" 2>/dev/null | cut -f1)
log INFO "System backup complete: $archive ($size)"
# Prune old system backups (keep $SYSTEM_BACKUP_RETAIN)
local count
count=$(find "$SYSTEM_BACKUP_DIR" -name 'system_*.tar.gz' | wc -l)
if [[ "$count" -gt "$SYSTEM_BACKUP_RETAIN" ]]; then
local to_remove=$((count - SYSTEM_BACKUP_RETAIN))
find "$SYSTEM_BACKUP_DIR" -name 'system_*.tar.gz' -printf '%T+ %p\n' \
| sort | head -n "$to_remove" | awk '{print $2}' \
| while read -r f; do
rm -f "$f" "${f%.tar.gz}.manifest"
log INFO "Pruned old system backup: $f"
done
fi
}
# ──────────────────────────────────────────────
# Content backup (every 2 hours)
# ──────────────────────────────────────────────
cmd_backup_content() {
ensure_dirs
acquire_lock "content-backup"
local ts
ts=$(timestamp)
local archive="${CONTENT_BACKUP_DIR}/content_${ts}.tar.gz"
local db_dump="${CONTENT_BACKUP_DIR}/content_${ts}.sql.gz"
log INFO "Starting content backup → $archive"
# Back up web content / uploads
local existing_paths=()
for p in $CONTENT_PATHS; do
[[ -e "$p" ]] && existing_paths+=("$p")
done
if [[ ${#existing_paths[@]} -gt 0 ]]; then
tar -czf "$archive" "${existing_paths[@]}" 2>/dev/null || true
local size
size=$(du -sh "$archive" 2>/dev/null | cut -f1)
log INFO "Content files archived: $archive ($size)"
else
log WARN "No content paths found to back up"
fi
# Database dump
if command -v mysqldump &>/dev/null || command -v mariadb-dump &>/dev/null; then
local dump_cmd="mysqldump"
command -v mariadb-dump &>/dev/null && dump_cmd="mariadb-dump"
local databases=()
if [[ -n "$DB_NAMES" ]]; then
read -ra databases <<< "$DB_NAMES"
else
# Auto-detect: dump all databases except system ones
databases=($(${dump_cmd%dump} -N -e \
"SELECT schema_name FROM information_schema.schemata
WHERE schema_name NOT IN ('information_schema','performance_schema','mysql','sys')" \
2>/dev/null | tr '\n' ' ')) || true
fi
if [[ ${#databases[@]} -gt 0 ]]; then
$dump_cmd --single-transaction --routines --triggers \
--databases "${databases[@]}" 2>/dev/null \
| gzip > "$db_dump"
local db_size
db_size=$(du -sh "$db_dump" 2>/dev/null | cut -f1)
log INFO "Database dump complete: $db_dump ($db_size)"
else
log WARN "No databases found to dump"
fi
fi
}
# ──────────────────────────────────────────────
# Cleanup — prune content backups older than retention
# ──────────────────────────────────────────────
cmd_cleanup() {
ensure_dirs
local before_count after_count
# Content: keep only last 24 hours (1 day)
before_count=$(find "$CONTENT_BACKUP_DIR" -name 'content_*' -type f | wc -l)
find "$CONTENT_BACKUP_DIR" -name 'content_*' -type f \
-mmin +$((CONTENT_BACKUP_RETAIN_HOURS * 60)) -delete 2>/dev/null || true
after_count=$(find "$CONTENT_BACKUP_DIR" -name 'content_*' -type f | wc -l)
local removed=$((before_count - after_count))
[[ "$removed" -gt 0 ]] && log INFO "Pruned $removed content backup(s) older than ${CONTENT_BACKUP_RETAIN_HOURS}h"
# System: keep N most recent (handled in backup-system, but double-check here)
before_count=$(find "$SYSTEM_BACKUP_DIR" -name 'system_*' -type f | wc -l)
local max_system_files=$((SYSTEM_BACKUP_RETAIN * 2)) # .tar.gz + .manifest
if [[ "$before_count" -gt "$max_system_files" ]]; then
local excess=$((before_count - max_system_files))
find "$SYSTEM_BACKUP_DIR" -name 'system_*' -type f -printf '%T+ %p\n' \
| sort | head -n "$excess" | awk '{print $2}' \
| xargs -r rm -f
log INFO "Pruned excess system backups"
fi
log INFO "Cleanup complete"
}
# ──────────────────────────────────────────────
# Boot check — the auto-heal entry point
# ──────────────────────────────────────────────
cmd_boot_check() {
ensure_dirs
acquire_lock "boot-check"
log INFO "=== Boot check started ==="
log INFO "Hostname: $(hostname), Kernel: $(uname -r)"
if has_safepoint; then
log INFO "Safe point found — server was shut down cleanly"
log INFO "Clearing safe point for next cycle"
cmd_clear_safepoint
log INFO "=== Boot check passed (clean restart) ==="
return 0
fi
log WARN "NO safe point found — server restarted without clean shutdown"
log WARN "Initiating auto-heal sequence..."
auto_heal
local rc=$?
# Set safe point after successful heal
if [[ $rc -eq 0 ]]; then
cmd_set_safepoint
log INFO "=== Boot check complete (healed successfully) ==="
else
log ERROR "=== Boot check FAILED — manual intervention required ==="
fi
return $rc
}
# ──────────────────────────────────────────────
# Auto-heal strategy
#
# TODO: This is the core decision point. Implement the recovery
# steps that match your server's architecture. See guidance below.
#
# Trade-offs to consider:
# - Restore-from-backup: safest, but content may be up to 2h stale
# - Service-restart-only: faster, keeps current data, but won't fix
# corrupted configs or broken filesystem state
# - Hybrid: restart services first, verify health, only restore if
# health checks fail — best of both worlds but more complex
#
# The function receives no arguments. Use the latest system + content
# backups to restore if needed. Return 0 on success, 1 on failure.
# ──────────────────────────────────────────────
auto_heal() {
log INFO "Phase 1: Verify and repair filesystem"
# Check for common post-crash issues
repair_filesystem
log INFO "Phase 2: Restore system configuration if corrupted"
restore_system_if_needed
log INFO "Phase 3: Restart core services"
restart_services
log INFO "Phase 4: Verify health"
if ! verify_health; then
log WARN "Health check failed after service restart — restoring from backup"
restore_from_backup
restart_services
if ! verify_health; then
log ERROR "Health check still failing after restore — giving up"
return 1
fi
fi
log INFO "Auto-heal completed successfully"
return 0
}
# ──────────────────────────────────────────────
# Heal sub-steps
# ──────────────────────────────────────────────
repair_filesystem() {
# Fix common post-crash filesystem issues
# Clear stale PID/lock/socket files that prevent services from starting
local stale_files=(
/var/run/nginx.pid
/var/run/mysqld/mysqld.pid
/var/run/php-fpm.pid
/var/lib/mysql/*.pid
)
for f in "${stale_files[@]}"; do
for expanded in $f; do
if [[ -f "$expanded" ]]; then
local pid
pid=$(<"$expanded") 2>/dev/null || true
if [[ -n "$pid" ]] && ! kill -0 "$pid" 2>/dev/null; then
rm -f "$expanded"
log INFO "Removed stale PID file: $expanded"
fi
fi
done
done
# Fix permissions on critical dirs that may get mangled
[[ -d /var/run/mysqld ]] && chown mysql:mysql /var/run/mysqld 2>/dev/null || true
[[ -d /var/lib/php/sessions ]] && chmod 1733 /var/lib/php/sessions 2>/dev/null || true
# Repair tmp/cache dirs
for d in /tmp /var/tmp; do
[[ -d "$d" ]] && chmod 1777 "$d" 2>/dev/null || true
done
}
restore_system_if_needed() {
# Find latest system backup
local latest_system
latest_system=$(find "$SYSTEM_BACKUP_DIR" -name 'system_*.tar.gz' -printf '%T+ %p\n' \
2>/dev/null | sort -r | head -1 | awk '{print $2}')
if [[ -z "$latest_system" ]]; then
log WARN "No system backup available to verify against"
return 0
fi
# Check if critical configs exist and are non-empty
local needs_restore=false
local critical_configs=("/etc/nginx/nginx.conf" "/etc/php" "/etc/mysql")
for cfg in "${critical_configs[@]}"; do
if [[ -e "$cfg" ]]; then
# Config exists — check if it's a file and non-empty, or a directory
if [[ -f "$cfg" && ! -s "$cfg" ]]; then
log WARN "Critical config is empty: $cfg"
needs_restore=true
break
fi
fi
done
if $needs_restore; then
log WARN "Restoring system config from $latest_system"
tar -xzf "$latest_system" -C / 2>/dev/null || {
log ERROR "System restore failed from $latest_system"
return 1
}
log INFO "System config restored"
else
log INFO "System configs look intact — skipping restore"
fi
}
restart_services() {
if ! command -v systemctl &>/dev/null; then
log WARN "systemctl not available — skipping service restart"
return 0
fi
local services=("mysql" "mariadb" "nginx" "apache2" "php-fpm" "php8.1-fpm" "php8.2-fpm" "php8.3-fpm")
for svc in "${services[@]}"; do
if systemctl is-enabled "$svc" &>/dev/null; then
log INFO "Restarting $svc..."
systemctl restart "$svc" 2>/dev/null && \
log INFO "$svc restarted OK" || \
log WARN "$svc restart failed"
fi
done
}
verify_health() {
local failures=0
# Check critical services are running
local services=("mysql" "mariadb" "nginx" "apache2")
for svc in "${services[@]}"; do
if systemctl is-enabled "$svc" &>/dev/null; then
if ! systemctl is-active "$svc" &>/dev/null; then
log WARN "Service not running: $svc"
((failures++))
fi
fi
done
# Check if web server responds
if command -v curl &>/dev/null; then
if ! curl -sf -o /dev/null --max-time 10 "http://localhost/" 2>/dev/null; then
log WARN "Local web server not responding"
((failures++))
fi
fi
# Check if database accepts connections
if command -v mysqladmin &>/dev/null; then
if ! mysqladmin ping --silent 2>/dev/null; then
log WARN "Database not responding to ping"
((failures++))
fi
fi
[[ $failures -eq 0 ]]
}
restore_from_backup() {
log WARN "=== Full restore from backup ==="
# Restore system config
local latest_system
latest_system=$(find "$SYSTEM_BACKUP_DIR" -name 'system_*.tar.gz' -printf '%T+ %p\n' \
2>/dev/null | sort -r | head -1 | awk '{print $2}')
if [[ -n "$latest_system" ]]; then
log INFO "Restoring system from $latest_system"
tar -xzf "$latest_system" -C / 2>/dev/null || \
log ERROR "System restore failed"
fi
# Restore content
local latest_content
latest_content=$(find "$CONTENT_BACKUP_DIR" -name 'content_*.tar.gz' -printf '%T+ %p\n' \
2>/dev/null | sort -r | head -1 | awk '{print $2}')
if [[ -n "$latest_content" ]]; then
log INFO "Restoring content from $latest_content"
tar -xzf "$latest_content" -C / 2>/dev/null || \
log ERROR "Content restore failed"
fi
# Restore database
local latest_db
latest_db=$(find "$CONTENT_BACKUP_DIR" -name 'content_*.sql.gz' -printf '%T+ %p\n' \
2>/dev/null | sort -r | head -1 | awk '{print $2}')
if [[ -n "$latest_db" ]]; then
log INFO "Restoring database from $latest_db"
local mysql_cmd="mysql"
command -v mariadb &>/dev/null && mysql_cmd="mariadb"
zcat "$latest_db" | $mysql_cmd 2>/dev/null || \
log ERROR "Database restore failed"
fi
}
# ──────────────────────────────────────────────
# Status
# ──────────────────────────────────────────────
cmd_status() {
echo "=== Moko Server Auto-Heal Status ==="
echo ""
# Safe point
if has_safepoint; then
echo "Safe point: SET"
cat "$SAFEPOINT_FILE" | sed 's/^/ /'
else
echo "Safe point: NOT SET (will auto-heal on next boot)"
fi
echo ""
# System backups
echo "System backups (${SYSTEM_BACKUP_DIR}):"
local sys_count
sys_count=$(find "$SYSTEM_BACKUP_DIR" -name 'system_*.tar.gz' 2>/dev/null | wc -l)
echo " Count: $sys_count (retain $SYSTEM_BACKUP_RETAIN)"
local latest_sys
latest_sys=$(find "$SYSTEM_BACKUP_DIR" -name 'system_*.tar.gz' -printf '%T+ %p\n' \
2>/dev/null | sort -r | head -1)
if [[ -n "$latest_sys" ]]; then
echo " Latest: $(echo "$latest_sys" | awk '{print $2}')"
echo " Timestamp: $(echo "$latest_sys" | awk '{print $1}')"
else
echo " Latest: (none)"
fi
echo ""
# Content backups
echo "Content backups (${CONTENT_BACKUP_DIR}):"
local cnt_count
cnt_count=$(find "$CONTENT_BACKUP_DIR" -name 'content_*.tar.gz' 2>/dev/null | wc -l)
echo " Count: $cnt_count (retain ${CONTENT_BACKUP_RETAIN_HOURS}h)"
local latest_cnt
latest_cnt=$(find "$CONTENT_BACKUP_DIR" -name 'content_*.tar.gz' -printf '%T+ %p\n' \
2>/dev/null | sort -r | head -1)
if [[ -n "$latest_cnt" ]]; then
echo " Latest: $(echo "$latest_cnt" | awk '{print $2}')"
echo " Timestamp: $(echo "$latest_cnt" | awk '{print $1}')"
else
echo " Latest: (none)"
fi
echo ""
# Disk usage
echo "Backup disk usage:"
du -sh "$SYSTEM_BACKUP_DIR" "$CONTENT_BACKUP_DIR" 2>/dev/null | sed 's/^/ /'
}
# ──────────────────────────────────────────────
# Install helper — sets up cron + systemd
# ──────────────────────────────────────────────
cmd_install() {
local script_path
script_path=$(readlink -f "$0")
echo "Installing Moko Auto-Heal..."
# Create config directory
mkdir -p /etc/moko "$(dirname "$LOG_FILE")" "$LOCK_DIR"
# Write example config if none exists
if [[ ! -f "$CONF_FILE" ]]; then
cat > "$CONF_FILE" <<'CONF'
# /etc/moko/autoheal.conf — Server auto-heal configuration
# Uncomment and modify as needed
# BACKUP_ROOT="/var/backups/moko"
# SAFEPOINT_FILE="/var/run/moko/safepoint"
# LOG_FILE="/var/log/moko/autoheal.log"
# System backup paths (space-separated)
# SYSTEM_PATHS="/etc/nginx /etc/php /etc/mysql /etc/cron.d /etc/systemd/system"
# Content backup paths (space-separated)
# CONTENT_PATHS="/var/www"
# Database names (space-separated, empty = auto-detect all)
# DB_NAMES=""
# Retention
# SYSTEM_BACKUP_RETAIN=7 # daily backups to keep
# CONTENT_BACKUP_RETAIN_HOURS=24 # hours of content backups to keep
CONF
echo " Created config: $CONF_FILE"
fi
# Install cron jobs
local cron_file="/etc/cron.d/moko-autoheal"
cat > "$cron_file" <<CRON
# Moko Server Auto-Heal — managed by server-autoheal.sh install
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# Boot check — auto-heal if no safe point
@reboot root ${script_path} boot-check
# System backup — daily at 3:00 AM
0 3 * * * root ${script_path} backup-system
# Content backup — every 2 hours
0 */2 * * * root ${script_path} backup-content
# Cleanup expired backups — 30 min after each content backup
30 */2 * * * root ${script_path} cleanup
CRON
echo " Installed cron: $cron_file"
# Install shutdown hook to set safe point on clean shutdown
local shutdown_hook="/etc/systemd/system/moko-safepoint.service"
cat > "$shutdown_hook" <<UNIT
[Unit]
Description=Moko Safe Point — mark clean shutdown
DefaultDependencies=no
Before=shutdown.target reboot.target halt.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/bin/true
ExecStop=${script_path} set-safepoint
[Install]
WantedBy=multi-user.target
UNIT
systemctl daemon-reload
systemctl enable moko-safepoint.service
echo " Installed systemd hook: $shutdown_hook"
echo ""
echo "Done! Edit $CONF_FILE to configure paths for your server."
echo "Run '${script_path} status' to verify."
}
# ──────────────────────────────────────────────
# Main dispatcher
# ──────────────────────────────────────────────
main() {
local cmd="${1:-help}"
case "$cmd" in
boot-check) cmd_boot_check ;;
set-safepoint) cmd_set_safepoint ;;
clear-safepoint) cmd_clear_safepoint ;;
backup-system) cmd_backup_system ;;
backup-content) cmd_backup_content ;;
cleanup) cmd_cleanup ;;
status) cmd_status ;;
install) cmd_install ;;
help|--help|-h)
sed -n '2,/^$/s/^# //p' "$0"
echo ""
echo "Commands: boot-check, set-safepoint, clear-safepoint,"
echo " backup-system, backup-content, cleanup, status, install"
;;
*)
echo "Unknown command: $cmd" >&2
echo "Run '$0 help' for usage" >&2
exit 1
;;
esac
}
main "$@"
+6 -2
View File
@@ -10,9 +10,8 @@
* FILE INFORMATION
* DEFGROUP: MokoStandards.CLI
* INGROUP: MokoStandards
* REPO: https://github.com/mokoconsulting-tech/MokoStandards
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /bin/moko
* VERSION: 04.00.15
* BRIEF: Unified CLI dispatcher — run any MokoStandards script without needing GitHub Actions
*
* USAGE
@@ -69,6 +68,11 @@ declare(strict_types=1);
$repoRoot = dirname(__DIR__);
$autoloader = $repoRoot . '/vendor/autoload.php';
// Support global Composer installs (e.g. composer global require)
if (isset($GLOBALS['_composer_autoload_path'])) {
$autoloader = $GLOBALS['_composer_autoload_path'];
}
if (!is_file($autoloader)) {
fwrite(STDERR, "Error: vendor/autoload.php not found.\nRun: composer install\n");
exit(2);
+6 -7
View File
@@ -7,17 +7,16 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: MokoStandards.CLI
* INGROUP: MokoStandards
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* 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
* php cli/archive_repo.php --repo MokoOldModule
* php cli/archive_repo.php --repo MokoOldModule --dry-run
* php cli/archive_repo.php --repo MokoOldModule --skip-close # Archive only, keep issues open
*/
declare(strict_types=1);
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env php
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/badge_update.php
* BRIEF: Update [VERSION: XX.XX.XX] badges in all markdown files
*
* Usage:
* php badge_update.php --path /repo --version 04.01.00
*/
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) {
fwrite(STDERR, "Usage: badge_update.php --path . --version XX.YY.ZZ\n");
exit(1);
}
$root = realpath($path) ?: $path;
$pattern = '/\[VERSION:\s*\d{2}\.\d{2}\.\d{2}\]/';
$replacement = "[VERSION: {$version}]";
$updated = 0;
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($root, RecursiveDirectoryIterator::SKIP_DOTS)
);
foreach ($iterator as $file) {
$filePath = $file->getPathname();
// Skip .git and vendor directories
if (preg_match('#[/\\\\](\.git|vendor)[/\\\\]#', $filePath)) {
continue;
}
// Only process markdown files
if (!preg_match('/\.md$/i', $filePath)) {
continue;
}
$content = file_get_contents($filePath);
if (preg_match($pattern, $content)) {
$newContent = preg_replace($pattern, $replacement, $content);
if ($newContent !== $content) {
file_put_contents($filePath, $newContent);
$relative = str_replace($root . DIRECTORY_SEPARATOR, '', $filePath);
echo "Updated: {$relative}\n";
$updated++;
}
}
}
echo "Updated {$updated} file(s) to {$replacement}\n";
exit(0);
+319
View File
@@ -0,0 +1,319 @@
#!/usr/bin/env php
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/bulk_workflow_trigger.php
* VERSION: 01.00.00
* BRIEF: Trigger a workflow across multiple repos at once
*/
declare(strict_types=1);
final class BulkWorkflowTrigger
{
private string $giteaUrl = 'https://git.mokoconsulting.tech';
private string $token = '';
private string $reposFile = '';
private string $org = '';
private string $workflow = '';
private string $ref = 'main';
private string $inputs = '';
private bool $dryRun = false;
public function run(): int
{
$this->parseArgs();
if ($this->token === '')
{
$this->log('ERROR: --token is required.');
$this->printUsage();
return 1;
}
if ($this->workflow === '')
{
$this->log('ERROR: --workflow is required.');
$this->printUsage();
return 1;
}
if ($this->reposFile === '' && $this->org === '')
{
$this->log('ERROR: Either --repos <file> or --org <org> is required.');
$this->printUsage();
return 1;
}
// Build repo list
$repos = $this->buildRepoList();
if ($repos === null || count($repos) === 0)
{
$this->log('ERROR: No repos found to process.');
return 1;
}
$this->log("Triggering workflow \"{$this->workflow}\" on ref \"{$this->ref}\" across " . count($repos) . " repo(s).");
$this->log("Gitea URL: {$this->giteaUrl}");
if ($this->dryRun)
{
$this->log('[DRY RUN] No requests will be sent.');
}
$this->log('');
// Parse inputs
$inputsDecoded = null;
if ($this->inputs !== '')
{
$inputsDecoded = json_decode($this->inputs, true);
if (!is_array($inputsDecoded))
{
$this->log('ERROR: --inputs must be valid JSON.');
return 1;
}
}
// Print header
$this->log(sprintf('%-40s | %s', 'Repo', 'Status'));
$this->log(str_repeat('-', 60));
$failCount = 0;
foreach ($repos as $repo)
{
$repo = trim($repo);
if ($repo === '' || strpos($repo, '/') === false)
{
continue;
}
[$owner, $repoName] = explode('/', $repo, 2);
if ($this->dryRun)
{
$this->log(sprintf('%-40s | %s', $repo, 'DRY RUN (skipped)'));
continue;
}
$payload = ['ref' => $this->ref];
if ($inputsDecoded !== null)
{
$payload['inputs'] = $inputsDecoded;
}
$response = $this->apiRequest(
'POST',
"/api/v1/repos/{$owner}/{$repoName}/actions/workflows/{$this->workflow}/dispatches",
json_encode($payload)
);
if ($response['code'] >= 200 && $response['code'] < 300)
{
$status = 'TRIGGERED';
}
elseif ($response['code'] === 404)
{
$status = 'FAILED (not found)';
$failCount++;
}
elseif ($response['code'] === 422)
{
$status = 'SKIPPED (unprocessable)';
}
else
{
$status = "FAILED (HTTP {$response['code']})";
$failCount++;
}
$this->log(sprintf('%-40s | %s', $repo, $status));
}
$this->log('');
$this->log('Done. ' . ($failCount > 0 ? "{$failCount} failure(s)." : 'All succeeded.'));
return $failCount > 0 ? 1 : 0;
}
private function parseArgs(): void
{
$args = $_SERVER['argv'] ?? [];
$count = count($args);
for ($i = 1; $i < $count; $i++)
{
switch ($args[$i])
{
case '--gitea-url':
$this->giteaUrl = rtrim($args[++$i] ?? '', '/');
break;
case '--token':
$this->token = $args[++$i] ?? '';
break;
case '--repos':
$this->reposFile = $args[++$i] ?? '';
break;
case '--org':
$this->org = $args[++$i] ?? '';
break;
case '--workflow':
$this->workflow = $args[++$i] ?? '';
break;
case '--ref':
$this->ref = $args[++$i] ?? 'main';
break;
case '--inputs':
$this->inputs = $args[++$i] ?? '';
break;
case '--dry-run':
$this->dryRun = true;
break;
case '--help':
case '-h':
$this->printUsage();
exit(0);
default:
$this->log("WARNING: Unknown argument: {$args[$i]}");
break;
}
}
}
private function printUsage(): void
{
$this->log('Usage: bulk_workflow_trigger.php --token <token> --workflow <file> [options]');
$this->log('');
$this->log('Options:');
$this->log(' --gitea-url <url> Gitea URL (default: https://git.mokoconsulting.tech)');
$this->log(' --token <token> Gitea API token');
$this->log(' --repos <file> File with newline-separated owner/repo list');
$this->log(' --org <org> Trigger on all repos in an org');
$this->log(' --workflow <filename> Workflow file (e.g., "sync-servers.yml")');
$this->log(' --ref <branch> Branch ref (default: "main")');
$this->log(' --inputs <json> Workflow inputs as JSON string');
$this->log(' --dry-run Show what would be done without triggering');
$this->log(' --help, -h Show this help');
}
private function buildRepoList(): ?array
{
if ($this->reposFile !== '')
{
if (!file_exists($this->reposFile))
{
$this->log("ERROR: Repos file not found: {$this->reposFile}");
return null;
}
$content = file_get_contents($this->reposFile);
$lines = array_filter(array_map('trim', explode("\n", $content)), function (string $line): bool {
return $line !== '' && $line[0] !== '#';
});
return array_values($lines);
}
// Fetch all repos from org
$this->log("Fetching repos from org: {$this->org}");
$page = 1;
$repos = [];
while (true)
{
$response = $this->apiRequest('GET', "/api/v1/orgs/{$this->org}/repos?limit=50&page={$page}");
if ($response['code'] < 200 || $response['code'] >= 300)
{
if ($page === 1)
{
$this->log("ERROR: Could not fetch repos for org (HTTP {$response['code']}).");
return null;
}
break;
}
$data = json_decode($response['body'], true);
if (!is_array($data) || count($data) === 0)
{
break;
}
foreach ($data as $repo)
{
$fullName = $repo['full_name'] ?? '';
if ($fullName !== '')
{
$repos[] = $fullName;
}
}
$page++;
}
$this->log('Found ' . count($repos) . " repo(s) in org \"{$this->org}\".");
return $repos;
}
private function apiRequest(string $method, string $endpoint, ?string $body = null): array
{
$url = $this->giteaUrl . $endpoint;
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Accept: application/json',
"Authorization: token {$this->token}",
]);
if ($body !== null)
{
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
}
$responseBody = curl_exec($ch);
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
if (curl_errno($ch))
{
$error = curl_error($ch);
curl_close($ch);
return ['code' => 0, 'body' => "cURL error: {$error}"];
}
curl_close($ch);
return ['code' => $httpCode, 'body' => $responseBody];
}
private function log(string $message): void
{
fwrite(STDERR, $message . PHP_EOL);
}
}
$app = new BulkWorkflowTrigger();
exit($app->run());
+82
View File
@@ -0,0 +1,82 @@
#!/usr/bin/env php
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/changelog_promote.php
* BRIEF: Promote [Unreleased] section in CHANGELOG.md to a versioned entry
*
* Usage:
* php changelog_promote.php --path /repo --version 04.01.00
* php changelog_promote.php --path /repo --version 04.01.00 --date 2026-05-21
*/
declare(strict_types=1);
$path = '.';
$version = null;
$date = date('Y-m-d');
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 === '--date' && isset($argv[$i + 1])) $date = $argv[$i + 1];
}
if ($version === null) {
fwrite(STDERR, "Usage: changelog_promote.php --path . --version XX.YY.ZZ [--date YYYY-MM-DD]\n");
exit(1);
}
$changelog = realpath($path) . '/CHANGELOG.md';
if (!file_exists($changelog)) {
fwrite(STDERR, "No CHANGELOG.md found at {$path}\n");
exit(1);
}
$content = file_get_contents($changelog);
// Check if [Unreleased] section exists
if (!preg_match('/## \[?Unreleased\]?/i', $content)) {
fwrite(STDERR, "No [Unreleased] section found in CHANGELOG.md\n");
exit(1);
}
// Replace [Unreleased] with versioned entry
$content = preg_replace(
'/## \[Unreleased\]/i',
"## [{$version}] --- {$date}",
$content,
1
);
$content = preg_replace(
'/## Unreleased/i',
"## [{$version}] --- {$date}",
$content,
1
);
// Insert new [Unreleased] section after the first heading line (# Changelog)
$lines = explode("\n", $content);
$inserted = false;
$result = [];
foreach ($lines as $line) {
$result[] = $line;
if (!$inserted && preg_match('/^# /', $line)) {
$result[] = '';
$result[] = '## [Unreleased]';
$result[] = '';
$inserted = true;
}
}
$content = implode("\n", $result);
file_put_contents($changelog, $content);
echo "CHANGELOG promoted: [Unreleased] -> [{$version}] --- {$date}\n";
exit(0);
+334
View File
@@ -0,0 +1,334 @@
#!/usr/bin/env php
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/client_inventory.php
* VERSION: 01.00.00
* BRIEF: Discover and list all client-waas repos with their server configuration status
*/
declare(strict_types=1);
final class ClientInventory
{
private string $giteaUrl = 'https://git.mokoconsulting.tech';
private string $token = '';
private bool $jsonOutput = false;
public function run(): int
{
$this->parseArgs();
if ($this->token === '')
{
$this->log('ERROR: --token is required.');
$this->printUsage();
return 1;
}
$this->log("Scanning Gitea instance: {$this->giteaUrl}");
// Step 1: List all orgs
$orgs = $this->fetchOrgs();
if ($orgs === null)
{
$this->log('ERROR: Failed to fetch organizations.');
return 1;
}
$this->log('Found ' . count($orgs) . ' organization(s).');
// Step 2 & 3: For each org, find client-waas repos
$inventory = [];
foreach ($orgs as $org)
{
$orgName = $org['username'] ?? $org['name'] ?? '';
if ($orgName === '')
{
continue;
}
$repos = $this->fetchOrgRepos($orgName);
if ($repos === null)
{
$this->log("WARNING: Could not fetch repos for org: {$orgName}");
continue;
}
foreach ($repos as $repo)
{
$repoName = $repo['name'] ?? '';
if (strpos($repoName, 'client-waas') === false)
{
continue;
}
$hasDevConfig = $this->checkVariables($orgName, $repoName, ['DEV_SYNC_HOST', 'DEV_SYNC_PATH']);
$hasLiveConfig = $this->checkVariables($orgName, $repoName, ['LIVE_SSH_HOST', 'LIVE_SYNC_PATH']);
$lastPush = $repo['updated_at'] ?? 'unknown';
if ($lastPush !== 'unknown')
{
$lastPush = substr($lastPush, 0, 19);
}
$status = 'OK';
if (!$hasDevConfig && !$hasLiveConfig)
{
$status = 'UNCONFIGURED';
}
elseif (!$hasDevConfig)
{
$status = 'NO DEV';
}
elseif (!$hasLiveConfig)
{
$status = 'NO LIVE';
}
$inventory[] = [
'org' => $orgName,
'repo' => $repoName,
'has_dev_config' => $hasDevConfig,
'has_live_config' => $hasLiveConfig,
'last_push' => $lastPush,
'status' => $status,
];
}
}
// Output results
if ($this->jsonOutput)
{
fwrite(STDOUT, json_encode($inventory, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . PHP_EOL);
return 0;
}
if (count($inventory) === 0)
{
$this->log('No client-waas repos found.');
return 0;
}
// Print table
$this->log('');
$this->log(sprintf(
'%-20s | %-35s | %-10s | %-11s | %-19s | %s',
'Org', 'Repo', 'Dev Config', 'Live Config', 'Last Push', 'Status'
));
$this->log(str_repeat('-', 120));
foreach ($inventory as $entry)
{
$this->log(sprintf(
'%-20s | %-35s | %-10s | %-11s | %-19s | %s',
$entry['org'],
$entry['repo'],
$entry['has_dev_config'] ? 'Yes' : 'No',
$entry['has_live_config'] ? 'Yes' : 'No',
$entry['last_push'],
$entry['status']
));
}
$this->log('');
$this->log('Total: ' . count($inventory) . ' client-waas repo(s).');
return 0;
}
private function parseArgs(): void
{
$args = $_SERVER['argv'] ?? [];
$count = count($args);
for ($i = 1; $i < $count; $i++)
{
switch ($args[$i])
{
case '--gitea-url':
$this->giteaUrl = rtrim($args[++$i] ?? '', '/');
break;
case '--token':
$this->token = $args[++$i] ?? '';
break;
case '--json':
$this->jsonOutput = true;
break;
case '--help':
case '-h':
$this->printUsage();
exit(0);
default:
$this->log("WARNING: Unknown argument: {$args[$i]}");
break;
}
}
}
private function printUsage(): void
{
$this->log('Usage: client_inventory.php --token <token> [options]');
$this->log('');
$this->log('Options:');
$this->log(' --gitea-url <url> Gitea URL (default: https://git.mokoconsulting.tech)');
$this->log(' --token <token> Gitea API token');
$this->log(' --json Output results as JSON');
$this->log(' --help, -h Show this help');
}
private function fetchOrgs(): ?array
{
// Try admin endpoint first, fall back to user-visible orgs
$response = $this->apiRequest('GET', '/api/v1/admin/orgs?limit=50');
if ($response['code'] >= 200 && $response['code'] < 300)
{
$data = json_decode($response['body'], true);
if (is_array($data))
{
return $data;
}
}
$this->log('Admin orgs endpoint unavailable, falling back to user orgs...');
$response = $this->apiRequest('GET', '/api/v1/user/orgs?limit=50');
if ($response['code'] >= 200 && $response['code'] < 300)
{
$data = json_decode($response['body'], true);
if (is_array($data))
{
return $data;
}
}
return null;
}
private function fetchOrgRepos(string $org): ?array
{
$page = 1;
$allRepos = [];
while (true)
{
$response = $this->apiRequest('GET', "/api/v1/orgs/{$org}/repos?limit=50&page={$page}");
if ($response['code'] < 200 || $response['code'] >= 300)
{
return $page === 1 ? null : $allRepos;
}
$data = json_decode($response['body'], true);
if (!is_array($data) || count($data) === 0)
{
break;
}
$allRepos = array_merge($allRepos, $data);
$page++;
}
return $allRepos;
}
private function checkVariables(string $org, string $repo, array $requiredVars): bool
{
$response = $this->apiRequest('GET', "/api/v1/repos/{$org}/{$repo}/actions/variables");
if ($response['code'] < 200 || $response['code'] >= 300)
{
return false;
}
$data = json_decode($response['body'], true);
if (!is_array($data))
{
return false;
}
$existingVars = [];
foreach ($data as $variable)
{
if (isset($variable['name']))
{
$existingVars[] = $variable['name'];
}
}
foreach ($requiredVars as $var)
{
if (!in_array($var, $existingVars, true))
{
return false;
}
}
return true;
}
private function apiRequest(string $method, string $endpoint, ?string $body = null): array
{
$url = $this->giteaUrl . $endpoint;
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Accept: application/json',
"Authorization: token {$this->token}",
]);
if ($body !== null)
{
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
}
$responseBody = curl_exec($ch);
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
if (curl_errno($ch))
{
$error = curl_error($ch);
curl_close($ch);
return ['code' => 0, 'body' => "cURL error: {$error}"];
}
curl_close($ch);
return ['code' => $httpCode, 'body' => $responseBody];
}
private function log(string $message): void
{
fwrite(STDERR, $message . PHP_EOL);
}
}
$app = new ClientInventory();
exit($app->run());
+9 -10
View File
@@ -7,18 +7,17 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: MokoStandards.CLI
* INGROUP: MokoStandards
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* 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
* php cli/create_project.php --repo MokoCRM # Auto-detect type, create project
* php cli/create_project.php --repo MokoCRM --type dolibarr # Force type
* php cli/create_project.php --org mokoconsulting-tech --all # All repos without projects
* php cli/create_project.php --repo MokoCRM --dry-run # Preview without changes
*/
declare(strict_types=1);
@@ -163,7 +162,7 @@ function restGet(string $path, string $token, ?\MokoEnterprise\ApiClient $apiCli
function detectPlatform(string $org, string $repo, string $token, ?\MokoEnterprise\ApiClient $apiClient = null): string
{
// Try platform metadata dir first, then root
foreach (['.github/.mokostandards', '.gitea/.mokostandards', '.mokostandards'] as $path) {
foreach (['.github/.mokostandards', '.mokogitea/.mokostandards', '.mokostandards'] as $path) {
$data = restGet("repos/{$org}/{$repo}/contents/{$path}", $token, $apiClient);
if (!empty($data['content'])) {
$content = base64_decode($data['content']);
@@ -385,7 +384,7 @@ function createProject(
updateProjectV2(input: {
projectId: $projectId,
shortDescription: $shortDescription,
readme: "Managed by MokoStandards. Run `php api/cli/create_project.php` to regenerate."
readme: "Managed by MokoStandards. Run `php cli/create_project.php` to regenerate."
}) {
projectV2 { id }
}
+9 -11
View File
@@ -7,17 +7,16 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: MokoStandards.CLI
* INGROUP: MokoStandards
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* 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
* php cli/create_repo.php --name MokoNewModule --type dolibarr --description "My new module"
* php cli/create_repo.php --name MokoNewModule --type joomla --private
* php cli/create_repo.php --name MokoNewModule --type generic --dry-run
*/
declare(strict_types=1);
@@ -159,10 +158,9 @@ SPDX-License-Identifier: GPL-3.0-or-later
# FILE INFORMATION
DEFGROUP: {$name}
INGROUP: MokoStandards
INGROUP: moko-platform
REPO: {$repoUrl}
PATH: /README.md
VERSION: 01.00.00
BRIEF: {$description}
-->
@@ -229,7 +227,7 @@ if (!$dryRun) {
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";
echo " Run manually: php automation/bulk_sync.php --repos {$name} --force --yes\n";
}
} else {
echo " (dry-run) would run initial sync\n";
@@ -242,7 +240,7 @@ if (!$dryRun) {
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";
echo " Run manually: php cli/create_project.php --repo {$name} --type {$type}\n";
}
} else {
echo " (dry-run) would create Project\n";
+97
View File
@@ -0,0 +1,97 @@
#!/usr/bin/env php
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/dev_branch_reset.php
* BRIEF: Delete and recreate dev branch from main via Gitea API
*
* Usage:
* php dev_branch_reset.php --token TOKEN --api-base URL
* php dev_branch_reset.php --token TOKEN --api-base URL --branch dev --from main
*
* Options:
* --token Gitea API token (required)
* --api-base Gitea API base URL (required)
* --branch Branch to reset (default: dev)
* --from Source branch (default: main)
* --output-summary Write to $GITHUB_STEP_SUMMARY
*/
declare(strict_types=1);
$token = null;
$apiBase = null;
$branch = 'dev';
$from = 'main';
$outputSummary = false;
foreach ($argv as $i => $arg) {
if ($arg === '--token' && isset($argv[$i + 1])) $token = $argv[$i + 1];
if ($arg === '--api-base' && isset($argv[$i + 1])) $apiBase = $argv[$i + 1];
if ($arg === '--branch' && isset($argv[$i + 1])) $branch = $argv[$i + 1];
if ($arg === '--from' && isset($argv[$i + 1])) $from = $argv[$i + 1];
if ($arg === '--output-summary') $outputSummary = true;
}
if ($token === null) $token = getenv('GA_TOKEN') ?: getenv('GITEA_TOKEN') ?: null;
if ($token === null || $apiBase === null) {
fwrite(STDERR, "Usage: dev_branch_reset.php --token TOKEN --api-base URL [--branch dev] [--from main]\n");
exit(1);
}
// Delete branch (tolerate 404)
$ch = curl_init("{$apiBase}/branches/{$branch}");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'DELETE',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: token {$token}"],
CURLOPT_TIMEOUT => 30,
]);
curl_exec($ch);
$delCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($delCode === 204 || $delCode === 200) {
echo "Deleted branch '{$branch}'\n";
} elseif ($delCode === 404) {
echo "Branch '{$branch}' did not exist (skipped delete)\n";
} else {
fwrite(STDERR, "WARNING: Delete branch returned HTTP {$delCode}\n");
}
// Create branch from source
$payload = json_encode(['new_branch_name' => $branch, 'old_branch_name' => $from]);
$ch = curl_init("{$apiBase}/branches");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: token {$token}", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $payload,
CURLOPT_TIMEOUT => 30,
]);
$response = curl_exec($ch);
$createCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($createCode === 201) {
echo "Recreated '{$branch}' from '{$from}'\n";
} else {
fwrite(STDERR, "Failed to create branch '{$branch}' from '{$from}' (HTTP {$createCode})\n");
exit(1);
}
if ($outputSummary) {
$summaryFile = getenv('GITHUB_STEP_SUMMARY');
if ($summaryFile) {
file_put_contents($summaryFile, "Dev branch reset: '{$branch}' recreated from '{$from}'\n", FILE_APPEND);
}
}
exit(0);
+295
View File
@@ -0,0 +1,295 @@
#!/usr/bin/env php
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/joomla_build.php
* VERSION: 05.00.01
* BRIEF: Build a Joomla extension ZIP from manifest — all types supported
* NOTE: Called by pre-release and auto-release workflows.
*
* USAGE
* php joomla_build.php --path . --version 02.01.24
* php joomla_build.php --path . --version 02.01.24 --suffix -dev
* php joomla_build.php --path . --version 02.01.24 --output build --github-output
*
* Supports: plugin, module, component, template, package, library, file
*/
declare(strict_types=1);
// ── Argument parsing ────────────────────────────────────────────────────
$path = '.';
$version = '';
$suffix = '';
$outputDir = 'build';
$ghOutput = false;
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 === '--suffix' && isset($argv[$i + 1])) $suffix = $argv[$i + 1];
if ($arg === '--output' && isset($argv[$i + 1])) $outputDir = $argv[$i + 1];
if ($arg === '--github-output') $ghOutput = true;
}
if ($version === '') {
fwrite(STDERR, "::error::--version is required\n");
exit(1);
}
$path = realpath($path) ?: $path;
// ── Find source directory ──────────────────────────────────────────────
$srcDir = null;
foreach (['src', 'htdocs'] as $d) {
if (is_dir("{$path}/{$d}")) { $srcDir = "{$path}/{$d}"; break; }
}
if ($srcDir === null) {
fwrite(STDERR, "::error::No src/ or htdocs/ directory in {$path}\n");
exit(1);
}
// ── Find manifest ──────────────────────────────────────────────────────
$manifest = findManifest($srcDir);
if ($manifest === null) {
fwrite(STDERR, "::error::No Joomla manifest found in {$srcDir}\n");
exit(1);
}
fwrite(STDERR, "Manifest: {$manifest}\n");
// ── Parse manifest ─────────────────────────────────────────────────────
$meta = parseManifest($manifest);
// Resolve language-key names (e.g. PLG_SYSTEM_MOKOWAAS -> "System - Moko WaaS")
if (preg_match('/^[A-Z_]+$/', $meta['name'])) {
$resolved = resolveLanguageKey($srcDir, $meta['name']);
if ($resolved !== null) { $meta['name'] = $resolved; }
}
$prefix = typePrefix($meta);
$zipName = "{$prefix}{$meta['element']}-{$version}{$suffix}.zip";
$zipPath = "{$outputDir}/{$zipName}";
fwrite(STDERR, "=== Joomla Build: {$meta['type']}{$meta['element']} {$version}{$suffix} ===\n");
fwrite(STDERR, " Type: {$meta['type']}\n");
fwrite(STDERR, " Element: {$meta['element']}\n");
fwrite(STDERR, " Group: " . ($meta['group'] ?: 'n/a') . "\n");
fwrite(STDERR, " Name: {$meta['name']}\n");
fwrite(STDERR, " Output: {$zipName}\n");
// ── Build ──────────────────────────────────────────────────────────────
if (!is_dir($outputDir)) { mkdir($outputDir, 0755, true); }
if ($meta['type'] === 'package') {
buildPackageZip($srcDir, $zipPath);
} else {
buildZip($srcDir, $zipPath);
}
$sha256 = hash_file('sha256', $zipPath);
$size = filesize($zipPath);
fwrite(STDERR, "Package: {$zipPath} ({$size} bytes, SHA: " . substr($sha256, 0, 16) . "...)\n");
// ── Output variables ───────────────────────────────────────────────────
$vars = [
'zip_name' => $zipName,
'zip_path' => $zipPath,
'sha256' => $sha256,
'ext_type' => $meta['type'],
'ext_element' => $meta['element'],
'ext_name' => $meta['name'],
'ext_group' => $meta['group'],
'type_prefix' => $prefix,
];
if ($ghOutput && ($ghFile = getenv('GITHUB_OUTPUT')) !== false && $ghFile !== '') {
$fh = fopen($ghFile, 'a');
foreach ($vars as $k => $v) { fwrite($fh, "{$k}={$v}\n"); }
fclose($fh);
fwrite(STDERR, "Wrote " . count($vars) . " outputs to GITHUB_OUTPUT\n");
} else {
foreach ($vars as $k => $v) { echo "{$k}={$v}\n"; }
}
exit(0);
// ═══════════════════════════════════════════════════════════════════════
// Functions
// ═══════════════════════════════════════════════════════════════════════
function findManifest(string $dir): ?string
{
// Priority: pkg_*.xml (packages), then any *.xml with <extension>
foreach (glob("{$dir}/pkg_*.xml") ?: [] as $f) { return $f; }
foreach (glob("{$dir}/*.xml") ?: [] as $f) {
if (str_contains((string) file_get_contents($f), '<extension')) { return $f; }
}
// Broader nested search
$iter = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iter as $item) {
if ($item->isFile() && $item->getExtension() === 'xml') {
if (str_contains((string) file_get_contents($item->getPathname()), '<extension')) {
return $item->getPathname();
}
}
}
return null;
}
function parseManifest(string $file): array
{
$xml = simplexml_load_file($file);
$name = (string) ($xml->name ?? '');
$type = (string) ($xml->attributes()->type ?? 'component');
$element = (string) ($xml->element ?? '');
$group = (string) ($xml->attributes()->group ?? '');
// Fallback element detection
if ($element === '') { $element = (string) ($xml->attributes()->plugin ?? ''); }
if ($element === '') { $element = (string) ($xml->attributes()->module ?? ''); }
if ($element === '') {
$element = strtolower(basename($file, '.xml'));
if (in_array($element, ['templatedetails', 'manifest'], true)) {
$element = strtolower(basename(dirname($file)));
}
}
if ($name === '') { $name = $element; }
return compact('name', 'type', 'element', 'group');
}
function typePrefix(array $meta): string
{
return match ($meta['type']) {
'plugin' => "plg_{$meta['group']}_",
'module' => 'mod_',
'component' => 'com_',
'template' => 'tpl_',
'package' => 'pkg_',
'library' => 'lib_',
default => '',
};
}
function resolveLanguageKey(string $srcDir, string $key): ?string
{
$iter = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($srcDir, FilesystemIterator::SKIP_DOTS)
);
foreach ($iter as $item) {
if ($item->isFile() && str_ends_with($item->getFilename(), '.sys.ini')) {
foreach (file($item->getPathname()) as $line) {
if (preg_match('/^' . preg_quote($key, '/') . '="(.+)"/', trim($line), $m)) {
return $m[1];
}
}
}
}
return null;
}
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;
if (str_starts_with($name, '.build-trigger')) return true;
$ext = pathinfo($name, PATHINFO_EXTENSION);
return in_array($ext, ['ppk', 'pem', 'key', 'local'], true);
}
function buildZip(string $srcDir, string $outPath): void
{
$zip = new ZipArchive();
if ($zip->open($outPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
fwrite(STDERR, "::error::Cannot create ZIP: {$outPath}\n");
exit(1);
}
$iter = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($srcDir, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iter as $file) {
$local = str_replace('\\', '/', substr($file->getPathname(), strlen($srcDir) + 1));
if (isExcluded(basename($local))) continue;
$file->isDir() ? $zip->addEmptyDir($local) : $zip->addFile($file->getPathname(), $local);
}
$zip->close();
}
function buildPackageZip(string $srcDir, string $outPath): void
{
fwrite(STDERR, "Building Joomla package (multi-extension)...\n");
$staging = sys_get_temp_dir() . '/moko_pkg_' . uniqid();
mkdir($staging, 0755, true);
// 1. Zip each sub-extension in packages/
$packagesDir = "{$srcDir}/packages";
if (is_dir($packagesDir)) {
foreach (glob("{$packagesDir}/*", GLOB_ONLYDIR) as $extDir) {
$subManifest = findManifest($extDir);
if ($subManifest) {
$sub = parseManifest($subManifest);
$subPrefix = typePrefix($sub);
$subZipName = "{$subPrefix}{$sub['element']}.zip";
} else {
$subZipName = basename($extDir) . '.zip';
}
fwrite(STDERR, " Sub-extension: {$subZipName}\n");
buildZip($extDir, "{$staging}/{$subZipName}");
}
}
// 2. Copy package-level files (manifest, script, language)
foreach (glob("{$srcDir}/*.xml") ?: [] as $f) copy($f, "{$staging}/" . basename($f));
foreach (glob("{$srcDir}/*.php") ?: [] as $f) copy($f, "{$staging}/" . basename($f));
foreach (['language', 'administrator'] as $d) {
if (is_dir("{$srcDir}/{$d}")) {
copyTree("{$srcDir}/{$d}", "{$staging}/{$d}");
}
}
// 3. Create outer zip
buildZip($staging, $outPath);
// Cleanup
rmTree($staging);
}
function copyTree(string $src, string $dst): void
{
if (!is_dir($dst)) mkdir($dst, 0755, true);
$iter = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($src, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iter as $item) {
$target = "{$dst}/" . $iter->getSubPathname();
$item->isDir() ? (is_dir($target) || mkdir($target, 0755, true)) : copy($item->getPathname(), $target);
}
}
function rmTree(string $dir): void
{
if (!is_dir($dir)) return;
$iter = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($iter as $item) {
$item->isDir() ? rmdir($item->getPathname()) : unlink($item->getPathname());
}
rmdir($dir);
}
+105 -11
View File
@@ -7,18 +7,17 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: MokoStandards.CLI
* INGROUP: MokoStandards
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* 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
* php cli/joomla_release.php --repo MokoCassiopeia --stability stable
* php cli/joomla_release.php --repo MokoCassiopeia --stability development
* php cli/joomla_release.php --repo MokoCassiopeia --stability rc --dry-run
* php cli/joomla_release.php --path /local/repo --stability stable
*/
declare(strict_types=1);
@@ -118,14 +117,21 @@ class JoomlaRelease extends CLIApp
return 1;
}
$zipName = "{$meta['element']}-{$displayVersion}.zip";
$tarName = "{$meta['element']}-{$displayVersion}.tar.gz";
$prefix = $this->typePrefix($meta);
$zipName = "{$prefix}{$meta['element']}-{$displayVersion}.zip";
$tarName = "{$prefix}{$meta['element']}-{$displayVersion}.tar.gz";
$zipPath = sys_get_temp_dir() . "/{$zipName}";
$tarPath = sys_get_temp_dir() . "/{$tarName}";
$this->log('INFO', "Type: {$meta['type']} | Element: {$meta['element']} | Group: {$meta['group']}");
$sha256 = 'dry-run';
if (!$dryRun) {
$this->buildZip($srcDir, $zipPath);
if ($meta['type'] === 'package') {
$this->buildPackageZip($srcDir, $zipPath);
} else {
$this->buildZip($srcDir, $zipPath);
}
$this->buildTarGz($srcDir, $tarPath);
$sha256 = hash_file('sha256', $zipPath);
$this->log('SUCCESS', "ZIP: {$zipName} (" . filesize($zipPath) . " bytes)");
@@ -228,6 +234,94 @@ class JoomlaRelease extends CLIApp
// ── Package building ─────────────────────────────────────────────
/**
* Get the Joomla type prefix for ZIP naming.
*
* @param array $meta Parsed manifest metadata
* @return string Prefix like "plg_system_", "mod_", "com_", etc.
*/
private function typePrefix(array $meta): string
{
return match ($meta['type']) {
'plugin' => "plg_{$meta['group']}_",
'module' => 'mod_',
'component' => 'com_',
'template' => 'tpl_',
'package' => 'pkg_',
'library' => 'lib_',
default => '',
};
}
/**
* Build a Joomla package ZIP (type="package") with nested sub-extension zips.
*
* @param string $srcDir Source directory containing pkg_*.xml and packages/
* @param string $outPath Output ZIP path
*/
private function buildPackageZip(string $srcDir, string $outPath): void
{
$staging = sys_get_temp_dir() . '/moko_pkg_' . uniqid();
mkdir($staging, 0755, true);
// 1. Zip each sub-extension in packages/
$packagesDir = $srcDir . '/packages';
if (is_dir($packagesDir)) {
foreach (glob("{$packagesDir}/*", GLOB_ONLYDIR) as $extDir) {
$subManifest = null;
foreach (glob("{$extDir}/*.xml") as $xml) {
if (str_contains(file_get_contents($xml), '<extension')) {
$subManifest = $xml;
break;
}
}
if ($subManifest) {
$sub = $this->parseManifest($subManifest);
$prefix = $this->typePrefix($sub);
$subZipName = "{$prefix}{$sub['element']}.zip";
} else {
$subZipName = basename($extDir) . '.zip';
}
$this->log('INFO', " Sub-extension: {$subZipName}");
$this->buildZip($extDir, "{$staging}/{$subZipName}");
}
}
// 2. Copy package-level files (manifest, script, language)
foreach (glob("{$srcDir}/*.xml") as $f) { copy($f, "{$staging}/" . basename($f)); }
foreach (glob("{$srcDir}/*.php") as $f) { copy($f, "{$staging}/" . basename($f)); }
foreach (['language', 'administrator'] as $d) {
if (is_dir("{$srcDir}/{$d}")) {
$this->copyDir("{$srcDir}/{$d}", "{$staging}/{$d}");
}
}
// 3. Create the outer zip
$this->buildZip($staging, $outPath);
// Cleanup
$this->rmdir($staging);
}
/**
* Recursively copy a directory.
*/
private function copyDir(string $src, string $dst): void
{
if (!is_dir($dst)) { mkdir($dst, 0755, true); }
$iter = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($src, \FilesystemIterator::SKIP_DOTS),
\RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iter as $item) {
$target = $dst . '/' . $iter->getSubPathname();
$item->isDir() ? (is_dir($target) || mkdir($target, 0755, true)) : copy($item->getPathname(), $target);
}
}
private function buildZip(string $srcDir, string $outPath): void
{
$zip = new \ZipArchive();
+173
View File
@@ -0,0 +1,173 @@
#!/usr/bin/env php
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/manifest_read.php
* VERSION: 04.09.00
* BRIEF: Parse .manifest.xml and output requested field(s) for CI consumption
*
* Usage:
* php manifest_read.php --path /repo --field platform
* php manifest_read.php --path /repo --field entry-point
* php manifest_read.php --path /repo --all
* php manifest_read.php --path /repo --github-output
*
* Fields: name, org, description, license, license-spdx, platform,
* standards-version, standards-source, language, package-type, entry-point,
* source-dir, remote-subdir, excludes, dev-host, demo-host
*
* --all Print all fields as KEY=VALUE lines
* --github-output Append all fields to $GITHUB_OUTPUT (for Gitea/GitHub Actions)
* --json Output all fields as JSON
* --field <name> Print a single field value (no key, just value)
*/
declare(strict_types=1);
// -- Argument parsing ---------------------------------------------------------
$path = '.';
$field = null;
$mode = 'field'; // field | all | github-output | json
foreach ($argv as $i => $arg) {
if ($arg === '--path' && isset($argv[$i + 1])) $path = $argv[$i + 1];
if ($arg === '--field' && isset($argv[$i + 1])) $field = $argv[$i + 1];
if ($arg === '--all') $mode = 'all';
if ($arg === '--github-output') $mode = 'github-output';
if ($arg === '--json') $mode = 'json';
}
// -- Locate manifest ----------------------------------------------------------
$root = realpath($path) ?: $path;
$manifestFile = null;
// Priority: manifest.xml (current standard)
$candidates = [
"{$root}/.mokogitea/manifest.xml",
"{$root}/.mokogitea/.manifest.xml", // legacy (dot-prefixed)
"{$root}/.mokogitea/.moko-platform", // legacy v4
];
foreach ($candidates as $candidate) {
if (file_exists($candidate)) {
$manifestFile = $candidate;
break;
}
}
if ($manifestFile === null) {
fwrite(STDERR, "No manifest found in {$root}
");
exit(1);
}
// -- Parse XML ----------------------------------------------------------------
$xml = @simplexml_load_file($manifestFile);
if ($xml === false) {
// Fallback: try YAML format (.mokostandards legacy)
$content = file_get_contents($manifestFile);
$fields = [];
if (preg_match('/^platform:\s*(.+)/m', $content, $m)) {
$fields['platform'] = trim($m[1], "
\"'");
}
if (preg_match('/^standards_version:\s*(.+)/m', $content, $m)) {
$fields['standards-version'] = trim($m[1], "
\"'");
}
if (preg_match('/^governed_repo:\s*(.+)/m', $content, $m)) {
$fields['name'] = trim($m[1], "
\"'");
}
} else {
// Register namespace for XPath (optional, simple path works without)
$fields = [
'name' => (string)($xml->identity->name ?? ''),
'org' => (string)($xml->identity->org ?? ''),
'description' => (string)($xml->identity->description ?? ''),
'license' => (string)($xml->identity->license ?? ''),
'license-spdx' => (string)($xml->identity->license['spdx'] ?? ''),
'platform' => (string)($xml->governance->platform ?? ''),
'standards-version' => (string)($xml->governance->{"standards-version"} ?? ''),
'standards-source' => (string)($xml->governance->{"standards-source"} ?? ''),
'language' => (string)($xml->build->language ?? ''),
'package-type' => (string)($xml->build->{"package-type"} ?? ''),
'entry-point' => (string)($xml->build->{"entry-point"} ?? ''),
'source-dir' => (string)($xml->deploy->{"source-dir"} ?? ''),
'remote-subdir' => (string)($xml->deploy->{"remote-subdir"} ?? ''),
'excludes' => (string)($xml->deploy->excludes ?? ''),
'dev-host' => (string)($xml->deploy->{"dev-host"} ?? ''),
'demo-host' => (string)($xml->deploy->{"demo-host"} ?? ''),
'manifest-file' => $manifestFile,
];
}
// Strip empty values for cleaner output
$fields = array_filter($fields, fn($v) => $v !== '');
// -- Output -------------------------------------------------------------------
switch ($mode) {
case 'field':
if ($field === null) {
fwrite(STDERR, "Usage: manifest_read.php --path <dir> --field <name>
");
fwrite(STDERR, " manifest_read.php --path <dir> --all
");
fwrite(STDERR, " manifest_read.php --path <dir> --json
");
fwrite(STDERR, " manifest_read.php --path <dir> --github-output
");
exit(2);
}
echo ($fields[$field] ?? '') . "
";
break;
case 'all':
foreach ($fields as $k => $v) {
echo "{$k}={$v}
";
}
break;
case 'json':
echo json_encode($fields, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "
";
break;
case 'github-output':
$outputFile = getenv('GITHUB_OUTPUT');
if ($outputFile === false || $outputFile === '') {
fwrite(STDERR, "GITHUB_OUTPUT not set — printing to stdout instead
");
foreach ($fields as $k => $v) {
// Convert field-name to FIELD_NAME for env var style
$envKey = str_replace('-', '_', $k);
echo "{$envKey}={$v}
";
}
} else {
$fh = fopen($outputFile, 'a');
foreach ($fields as $k => $v) {
$envKey = str_replace('-', '_', $k);
fwrite($fh, "{$envKey}={$v}
");
}
fclose($fh);
fwrite(STDERR, "Wrote " . count($fields) . " fields to GITHUB_OUTPUT
");
}
break;
}
exit(0);
+288
View File
@@ -0,0 +1,288 @@
#!/usr/bin/env php
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/package_build.php
* BRIEF: Build ZIP and tar.gz install packages for Joomla/Dolibarr/generic projects
*
* Usage:
* php package_build.php --path /repo --version 04.01.00
* php package_build.php --path /repo --version 04.01.00 --output-dir /tmp
* php package_build.php --path /repo --version 04.01.00 --github-output
*
* Options:
* --path Repository root (default: .)
* --version Version string (required)
* --output-dir Directory for built packages (default: /tmp)
* --type-prefix Override type prefix (e.g. plg_system_)
* --element Override element name
* --github-output Export zip_name, tar_name, sha256_zip, sha256_tar to $GITHUB_OUTPUT
*
* NOTE: Uses PHP exec() with escapeshellarg() for tar — all arguments are escaped.
*/
declare(strict_types=1);
$path = '.';
$version = null;
$outputDir = '/tmp';
$typePrefixOverride = null;
$elementOverride = null;
$githubOutput = false;
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 === '--output-dir' && isset($argv[$i + 1])) $outputDir = $argv[$i + 1];
if ($arg === '--type-prefix' && isset($argv[$i + 1])) $typePrefixOverride = $argv[$i + 1];
if ($arg === '--element' && isset($argv[$i + 1])) $elementOverride = $argv[$i + 1];
if ($arg === '--github-output') $githubOutput = true;
}
if ($version === null) {
fwrite(STDERR, "Usage: package_build.php --path . --version XX.YY.ZZ [--output-dir /tmp]\n");
exit(1);
}
$root = realpath($path) ?: $path;
// -- Determine source directory -----------------------------------------------
$sourceDir = null;
foreach (['src', 'htdocs'] as $candidate) {
if (is_dir("{$root}/{$candidate}")) {
$sourceDir = "{$root}/{$candidate}";
break;
}
}
if ($sourceDir === null) {
fwrite(STDERR, "No src/ or htdocs/ directory found in {$root}\n");
exit(1);
}
// -- Determine element and type prefix from manifest --------------------------
$extElement = $elementOverride;
$typePrefix = $typePrefixOverride ?? '';
$extType = '';
$isPackage = false;
if ($extElement === null || $typePrefixOverride === null) {
// Find manifest
$manifest = null;
foreach (glob("{$sourceDir}/pkg_*.xml") ?: [] as $f) {
if (strpos(file_get_contents($f), '<extension') !== false) {
$manifest = $f;
break;
}
}
if ($manifest === null) {
foreach (glob("{$sourceDir}/*.xml") ?: [] as $f) {
if (strpos(file_get_contents($f), '<extension') !== false) {
$manifest = $f;
break;
}
}
}
if ($manifest !== null) {
$xml = file_get_contents($manifest);
if ($extElement === null) {
if (preg_match('/<element>([^<]+)<\/element>/', $xml, $m)) $extElement = $m[1];
elseif (preg_match('/plugin="([^"]+)"/', $xml, $m)) $extElement = $m[1];
elseif (preg_match('/module="([^"]+)"/', $xml, $m)) $extElement = $m[1];
else $extElement = strtolower(pathinfo($manifest, PATHINFO_FILENAME));
}
if (preg_match('/<extension[^>]*type="([^"]+)"/', $xml, $m)) $extType = $m[1];
$extFolder = '';
if (preg_match('/<extension[^>]*group="([^"]+)"/', $xml, $m)) $extFolder = $m[1];
if ($typePrefixOverride === null) {
switch ($extType) {
case 'plugin': $typePrefix = "plg_{$extFolder}_"; break;
case 'module': $typePrefix = 'mod_'; break;
case 'component': $typePrefix = 'com_'; break;
case 'template': $typePrefix = 'tpl_'; break;
case 'library': $typePrefix = 'lib_'; break;
case 'package': $typePrefix = 'pkg_'; break;
}
}
$isPackage = ($extType === 'package' && is_dir("{$sourceDir}/packages"));
}
}
if ($extElement === null) {
$extElement = strtolower(basename($root));
}
$zipName = "{$typePrefix}{$extElement}-{$version}.zip";
$tarName = "{$typePrefix}{$extElement}-{$version}.tar.gz";
$zipPath = "{$outputDir}/{$zipName}";
$tarPath = "{$outputDir}/{$tarName}";
// -- Exclude patterns ---------------------------------------------------------
$excludePatterns = [
'.ftpignore',
'sftp-config*',
'*.ppk',
'*.pem',
'*.key',
'.env*',
];
// -- Build packages -----------------------------------------------------------
if ($isPackage) {
echo "=== Building Joomla PACKAGE (multi-extension) ===\n";
$stagingDir = sys_get_temp_dir() . '/moko-pkg-' . uniqid();
mkdir($stagingDir, 0755, true);
// ZIP each sub-extension
foreach (glob("{$sourceDir}/packages/*/") ?: [] as $extDir) {
$subName = basename($extDir);
echo " Packaging sub-extension: {$subName}\n";
$subZip = new ZipArchive();
$subZipPath = "{$stagingDir}/{$subName}.zip";
if ($subZip->open($subZipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
fwrite(STDERR, "Failed to create ZIP for {$subName}\n");
continue;
}
addDirectoryToZip($subZip, $extDir, '', $excludePatterns);
$subZip->close();
}
// Copy package-level files
foreach (array_merge(glob("{$sourceDir}/*.xml") ?: [], glob("{$sourceDir}/*.php") ?: []) as $f) {
copy($f, "{$stagingDir}/" . basename($f));
}
// Create ZIP from staging
$zip = new ZipArchive();
if ($zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
fwrite(STDERR, "Failed to create ZIP: {$zipPath}\n");
exit(1);
}
addDirectoryToZip($zip, $stagingDir, '', []);
$zip->close();
// Create tar.gz — all arguments are escaped via escapeshellarg()
$tarCmd = sprintf(
'tar -czf %s -C %s .',
escapeshellarg($tarPath),
escapeshellarg($stagingDir)
);
passthru($tarCmd, $tarReturn);
// Cleanup staging
$cleanCmd = sprintf('rm -rf %s', escapeshellarg($stagingDir));
passthru($cleanCmd);
} else {
echo "=== Building standard extension package ===\n";
// ZIP
$zip = new ZipArchive();
if ($zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
fwrite(STDERR, "Failed to create ZIP: {$zipPath}\n");
exit(1);
}
addDirectoryToZip($zip, $sourceDir, '', $excludePatterns);
$zip->close();
// tar.gz — all arguments are escaped via escapeshellarg()
$excludeArgs = '';
foreach ($excludePatterns as $pattern) {
$excludeArgs .= ' --exclude=' . escapeshellarg($pattern);
}
$tarCmd = sprintf(
'tar -czf %s -C %s%s .',
escapeshellarg($tarPath),
escapeshellarg($sourceDir),
$excludeArgs
);
passthru($tarCmd, $tarReturn);
}
// -- Calculate SHA-256 --------------------------------------------------------
$sha256Zip = hash_file('sha256', $zipPath);
$sha256Tar = file_exists($tarPath) ? hash_file('sha256', $tarPath) : '';
$zipSize = filesize($zipPath);
$tarSize = file_exists($tarPath) ? filesize($tarPath) : 0;
echo "\n";
echo "ZIP: {$zipName} ({$zipSize} bytes)\n";
echo " SHA-256: {$sha256Zip}\n";
if ($tarSize > 0) {
echo "TAR: {$tarName} ({$tarSize} bytes)\n";
echo " SHA-256: {$sha256Tar}\n";
}
// -- Export to GITHUB_OUTPUT --------------------------------------------------
if ($githubOutput) {
$ghOutput = getenv('GITHUB_OUTPUT');
$lines = [
"zip_name={$zipName}",
"tar_name={$tarName}",
"zip_path={$zipPath}",
"tar_path={$tarPath}",
"sha256_zip={$sha256Zip}",
"sha256_tar={$sha256Tar}",
"type_prefix={$typePrefix}",
"ext_element={$extElement}",
];
if ($ghOutput) {
file_put_contents($ghOutput, implode("\n", $lines) . "\n", FILE_APPEND);
fwrite(STDERR, "Exported " . count($lines) . " fields to GITHUB_OUTPUT\n");
} else {
foreach ($lines as $line) echo "{$line}\n";
}
}
exit(0);
// =============================================================================
// Helper: recursively add directory contents to a ZipArchive
// =============================================================================
function addDirectoryToZip(ZipArchive $zip, string $dir, string $prefix, array $excludes): void
{
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iterator as $file) {
$filePath = $file->getPathname();
$relativePath = $prefix . substr($filePath, strlen($dir) + 1);
// Check excludes
$basename = basename($filePath);
$skip = false;
foreach ($excludes as $pattern) {
if (fnmatch($pattern, $basename)) {
$skip = true;
break;
}
}
if ($skip) continue;
// Normalize path separators for ZIP
$relativePath = str_replace('\\', '/', $relativePath);
if ($file->isDir()) {
$zip->addEmptyDir($relativePath);
} else {
$zip->addFile($filePath, $relativePath);
}
}
}
+3 -4
View File
@@ -5,11 +5,10 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: MokoStandards.CLI
* INGROUP: MokoStandards
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/platform_detect.php
* VERSION: 04.06.00
* BRIEF: Detect platform from .mokostandards file — outputs platform string
*/
+10 -11
View File
@@ -5,18 +5,17 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: MokoStandards.CLI
* INGROUP: MokoStandards
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* 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
* php cli/release.php # Release current version
* php cli/release.php --bump minor # Bump minor, then release
* php cli/release.php --bump major # Bump major, then release
* php cli/release.php --dry-run # Preview without changes
*/
declare(strict_types=1);
@@ -30,10 +29,10 @@ foreach ($argv as $i => $arg) {
}
$repoRoot = dirname(__DIR__, 2);
$syncFile = "{$repoRoot}/api/lib/Enterprise/RepositorySynchronizer.php";
$syncFile = "{$repoRoot}/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"
$bulkSyncFile = file_exists("{$repoRoot}/.mokogitea/workflows/bulk-repo-sync.yml")
? "{$repoRoot}/.mokogitea/workflows/bulk-repo-sync.yml"
: "{$repoRoot}/.github/workflows/bulk-repo-sync.yml";
$cleanupFile = "{$repoRoot}/templates/workflows/shared/repository-cleanup.yml.template";
+152
View File
@@ -0,0 +1,152 @@
#!/usr/bin/env php
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/release_body_update.php
* BRIEF: Update Gitea release body with changelog extract and checksums
*
* Usage:
* php release_body_update.php --version 04.01.00 --release-tag stable --token TOKEN --api-base URL
* php release_body_update.php --version 04.01.00 --release-tag stable --token TOKEN --api-base URL --zip-name pkg.zip --zip-sha abc123
*
* Options:
* --path Repo root for CHANGELOG.md (default: .)
* --version Version string (required)
* --release-tag Gitea release tag (required)
* --token Gitea API token (required)
* --api-base Gitea API base URL (required)
* --zip-name ZIP filename for checksum table
* --tar-name tar.gz filename for checksum table
* --zip-sha SHA256 of ZIP
* --tar-sha SHA256 of tar.gz
* --output-summary Write to $GITHUB_STEP_SUMMARY
*/
declare(strict_types=1);
$path = '.';
$version = null;
$releaseTag = null;
$token = null;
$apiBase = null;
$zipName = null;
$tarName = null;
$zipSha = null;
$tarSha = null;
$outputSummary = false;
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 === '--release-tag' && isset($argv[$i + 1])) $releaseTag = $argv[$i + 1];
if ($arg === '--token' && isset($argv[$i + 1])) $token = $argv[$i + 1];
if ($arg === '--api-base' && isset($argv[$i + 1])) $apiBase = $argv[$i + 1];
if ($arg === '--zip-name' && isset($argv[$i + 1])) $zipName = $argv[$i + 1];
if ($arg === '--tar-name' && isset($argv[$i + 1])) $tarName = $argv[$i + 1];
if ($arg === '--zip-sha' && isset($argv[$i + 1])) $zipSha = $argv[$i + 1];
if ($arg === '--tar-sha' && isset($argv[$i + 1])) $tarSha = $argv[$i + 1];
if ($arg === '--output-summary') $outputSummary = true;
}
if ($token === null) $token = getenv('GA_TOKEN') ?: getenv('GITEA_TOKEN') ?: null;
if ($version === null || $releaseTag === null || $token === null || $apiBase === null) {
fwrite(STDERR, "Usage: release_body_update.php --version VER --release-tag TAG --token TOKEN --api-base URL\n");
exit(1);
}
$root = realpath($path) ?: $path;
// Extract changelog section for this version
$changelog = '';
$clFile = "{$root}/CHANGELOG.md";
if (file_exists($clFile)) {
$lines = file($clFile, FILE_IGNORE_NEW_LINES);
$capturing = false;
$clLines = [];
foreach ($lines as $line) {
if (preg_match('/^##\s.*' . preg_quote($version, '/') . '/', $line)) {
$capturing = true;
continue;
}
if ($capturing && preg_match('/^## /', $line)) break;
if ($capturing) $clLines[] = $line;
}
$changelog = trim(implode("\n", $clLines));
}
// Build release body
$body = "## {$version} (" . date('Y-m-d') . ")\n\n";
if (!empty($changelog)) {
$body .= "{$changelog}\n\n";
}
if ($zipSha !== null || $tarSha !== null) {
$body .= "---\n\n### Checksums\n\n| File | SHA-256 |\n|------|--------|\n";
if ($zipName !== null && $zipSha !== null) {
$body .= "| `{$zipName}` | `{$zipSha}` |\n";
}
if ($tarName !== null && $tarSha !== null) {
$body .= "| `{$tarName}` | `{$tarSha}` |\n";
}
}
// Get release ID by tag
$ch = curl_init("{$apiBase}/releases/tags/{$releaseTag}");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: token {$token}"],
CURLOPT_TIMEOUT => 30,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200 || empty($response)) {
fwrite(STDERR, "Failed to get release for tag '{$releaseTag}' (HTTP {$httpCode})\n");
exit(1);
}
$release = json_decode($response, true);
$releaseId = $release['id'] ?? null;
if ($releaseId === null) {
fwrite(STDERR, "No release ID found for tag '{$releaseTag}'\n");
exit(1);
}
// PATCH release body
$payload = json_encode(['body' => $body]);
$ch = curl_init("{$apiBase}/releases/{$releaseId}");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'PATCH',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: token {$token}", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $payload,
CURLOPT_TIMEOUT => 30,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
fwrite(STDERR, "Failed to update release body (HTTP {$httpCode})\n");
exit(1);
}
echo "Release body updated for {$releaseTag} (release #{$releaseId})\n";
if ($outputSummary) {
$summaryFile = getenv('GITHUB_STEP_SUMMARY');
if ($summaryFile) {
file_put_contents($summaryFile, "Release body updated with changelog + checksums\n", FILE_APPEND);
}
}
exit(0);
+116
View File
@@ -0,0 +1,116 @@
#!/usr/bin/env php
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/release_cascade.php
* BRIEF: Delete lesser pre-release channels from Gitea when promoting stability
*
* Usage:
* php release_cascade.php --stability stable --token TOKEN --api-base URL
* php release_cascade.php --stability rc --token TOKEN --api-base URL
*
* Cascade rules:
* stable -> deletes development, alpha, beta, release-candidate
* rc -> deletes development, alpha, beta
* beta -> deletes development, alpha
* alpha -> deletes development
*/
declare(strict_types=1);
$stability = null;
$token = null;
$apiBase = null;
foreach ($argv as $i => $arg) {
if ($arg === '--stability' && isset($argv[$i + 1])) $stability = $argv[$i + 1];
if ($arg === '--token' && isset($argv[$i + 1])) $token = $argv[$i + 1];
if ($arg === '--api-base' && isset($argv[$i + 1])) $apiBase = $argv[$i + 1];
}
// Allow token from environment
if ($token === null) {
$token = getenv('GA_TOKEN') ?: getenv('GITEA_TOKEN') ?: null;
}
if ($stability === null || $token === null || $apiBase === null) {
fwrite(STDERR, "Usage: release_cascade.php --stability [stable|rc|beta|alpha] --token TOKEN --api-base URL\n");
fwrite(STDERR, " --api-base: e.g. https://git.mokoconsulting.tech/api/v1/repos/Org/Repo\n");
fwrite(STDERR, " Token can also be set via GA_TOKEN or GITEA_TOKEN env var\n");
exit(1);
}
// Define cascade hierarchy
$cascadeMap = [
'stable' => ['development', 'alpha', 'beta', 'release-candidate'],
'rc' => ['development', 'alpha', 'beta'],
'beta' => ['development', 'alpha'],
'alpha' => ['development'],
];
if (!isset($cascadeMap[$stability])) {
fwrite(STDERR, "Unknown stability level: {$stability}\n");
fwrite(STDERR, "Valid options: stable, rc, beta, alpha\n");
exit(1);
}
$tagsToDelete = $cascadeMap[$stability];
$deleted = 0;
foreach ($tagsToDelete as $tag) {
// Get release by tag
$ch = curl_init("{$apiBase}/releases/tags/{$tag}");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: token {$token}"],
CURLOPT_TIMEOUT => 30,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200 || empty($response)) {
continue;
}
$data = json_decode($response, true);
$releaseId = $data['id'] ?? null;
if ($releaseId === null) {
continue;
}
// Delete release
$ch = curl_init("{$apiBase}/releases/{$releaseId}");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'DELETE',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: token {$token}"],
CURLOPT_TIMEOUT => 30,
]);
curl_exec($ch);
curl_close($ch);
// Delete tag
$ch = curl_init("{$apiBase}/tags/{$tag}");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'DELETE',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: token {$token}"],
CURLOPT_TIMEOUT => 30,
]);
curl_exec($ch);
curl_close($ch);
echo "Deleted: {$tag} (release id: {$releaseId})\n";
$deleted++;
}
echo "Cleaned up {$deleted} pre-release channel(s)\n";
exit(0);
+239
View File
@@ -0,0 +1,239 @@
#!/usr/bin/env php
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/release_manage.php
* BRIEF: Create/update Gitea releases, upload assets, update release body
*
* Usage:
* # Create a release
* php release_manage.php --action create --tag stable --name "My Plugin 04.01.00" \
* --body "Release notes" --target main --token TOKEN --api-base URL
*
* # Upload assets to a release
* php release_manage.php --action upload --tag stable --files "/tmp/pkg.zip,/tmp/pkg.tar.gz" \
* --token TOKEN --api-base URL
*
* # Update release body (e.g. add SHA checksums)
* php release_manage.php --action update-body --tag stable --body "New body" \
* --token TOKEN --api-base URL
*
* # Delete a release and its tag
* php release_manage.php --action delete --tag stable --token TOKEN --api-base URL
*
* Options:
* --action create | upload | update-body | delete (required)
* --tag Release tag name (required)
* --name Release name/title (for create)
* --body Release body/description (for create, update-body)
* --body-file Read body from file instead of --body
* --target Target branch/commitish (for create, default: main)
* --files Comma-separated file paths to upload (for upload)
* --token Gitea API token (or GA_TOKEN/GITEA_TOKEN env var)
* --api-base Gitea API base URL (e.g. https://git.mokoconsulting.tech/api/v1/repos/Org/Repo)
*
* NOTE: This script uses PHP curl for all HTTP operations (no shell calls).
*/
declare(strict_types=1);
$action = null;
$tag = null;
$name = null;
$body = null;
$bodyFile = null;
$target = 'main';
$files = [];
$token = null;
$apiBase = null;
foreach ($argv as $i => $arg) {
if ($arg === '--action' && isset($argv[$i + 1])) $action = $argv[$i + 1];
if ($arg === '--tag' && isset($argv[$i + 1])) $tag = $argv[$i + 1];
if ($arg === '--name' && isset($argv[$i + 1])) $name = $argv[$i + 1];
if ($arg === '--body' && isset($argv[$i + 1])) $body = $argv[$i + 1];
if ($arg === '--body-file' && isset($argv[$i + 1])) $bodyFile = $argv[$i + 1];
if ($arg === '--target' && isset($argv[$i + 1])) $target = $argv[$i + 1];
if ($arg === '--files' && isset($argv[$i + 1])) $files = array_filter(explode(',', $argv[$i + 1]));
if ($arg === '--token' && isset($argv[$i + 1])) $token = $argv[$i + 1];
if ($arg === '--api-base' && isset($argv[$i + 1])) $apiBase = $argv[$i + 1];
}
// Allow token from environment
if ($token === null) {
$token = getenv('GA_TOKEN') ?: getenv('GITEA_TOKEN') ?: null;
}
// Read body from file if specified
if ($bodyFile !== null && file_exists($bodyFile)) {
$body = file_get_contents($bodyFile);
}
if ($action === null || $tag === null || $token === null || $apiBase === null) {
fwrite(STDERR, "Usage: release_manage.php --action [create|upload|update-body|delete] --tag TAG --token TOKEN --api-base URL\n");
exit(1);
}
/**
* Make a Gitea API request using curl
*/
function giteaApi(string $url, string $method, string $token, ?string $jsonBody = null, ?string $filePath = null): array
{
$ch = curl_init($url);
$headers = ["Authorization: token {$token}"];
$opts = [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 60,
CURLOPT_CUSTOMREQUEST => $method,
];
if ($jsonBody !== null) {
$headers[] = 'Content-Type: application/json';
$opts[CURLOPT_POSTFIELDS] = $jsonBody;
} elseif ($filePath !== null) {
$headers[] = 'Content-Type: application/octet-stream';
$opts[CURLOPT_POSTFIELDS] = file_get_contents($filePath);
}
$opts[CURLOPT_HTTPHEADER] = $headers;
curl_setopt_array($ch, $opts);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$data = json_decode($response ?: '{}', true) ?: [];
return ['code' => $httpCode, 'data' => $data];
}
/**
* Get release by tag
*/
function getReleaseByTag(string $apiBase, string $tag, string $token): ?array
{
$result = giteaApi("{$apiBase}/releases/tags/{$tag}", 'GET', $token);
if ($result['code'] === 200 && isset($result['data']['id'])) {
return $result['data'];
}
return null;
}
// -- Action dispatch ----------------------------------------------------------
switch ($action) {
case 'create':
// Delete existing release if present
$existing = getReleaseByTag($apiBase, $tag, $token);
if ($existing !== null) {
$existingId = $existing['id'];
giteaApi("{$apiBase}/releases/{$existingId}", 'DELETE', $token);
giteaApi("{$apiBase}/tags/{$tag}", 'DELETE', $token);
echo "Deleted previous release: {$tag} (id: {$existingId})\n";
}
$payload = json_encode([
'tag_name' => $tag,
'name' => $name ?? $tag,
'body' => $body ?? '',
'target_commitish' => $target,
]);
$result = giteaApi("{$apiBase}/releases", 'POST', $token, $payload);
if ($result['code'] >= 200 && $result['code'] < 300) {
$releaseId = $result['data']['id'] ?? 'unknown';
echo "Release created: {$name} (tag: {$tag}, id: {$releaseId})\n";
} else {
fwrite(STDERR, "Failed to create release: HTTP {$result['code']}\n");
fwrite(STDERR, json_encode($result['data']) . "\n");
exit(1);
}
break;
case 'upload':
if (empty($files)) {
fwrite(STDERR, "No files specified. Use --files /path/to/file1,/path/to/file2\n");
exit(1);
}
$release = getReleaseByTag($apiBase, $tag, $token);
if ($release === null) {
fwrite(STDERR, "No release found for tag: {$tag}\n");
exit(1);
}
$releaseId = $release['id'];
// Get existing assets to avoid duplicates
$assetsResult = giteaApi("{$apiBase}/releases/{$releaseId}/assets", 'GET', $token);
$existingAssets = $assetsResult['data'] ?? [];
foreach ($files as $filePath) {
$filePath = trim($filePath);
if (!file_exists($filePath)) {
fwrite(STDERR, "File not found: {$filePath}\n");
continue;
}
$fileName = basename($filePath);
// Delete existing asset with same name
foreach ($existingAssets as $asset) {
if (($asset['name'] ?? '') === $fileName) {
giteaApi("{$apiBase}/releases/{$releaseId}/assets/{$asset['id']}", 'DELETE', $token);
echo "Deleted existing asset: {$fileName}\n";
break;
}
}
// Upload
$uploadUrl = "{$apiBase}/releases/{$releaseId}/assets?name=" . urlencode($fileName);
$result = giteaApi($uploadUrl, 'POST', $token, null, $filePath);
if ($result['code'] >= 200 && $result['code'] < 300) {
echo "Uploaded: {$fileName}\n";
} else {
fwrite(STDERR, "Failed to upload {$fileName}: HTTP {$result['code']}\n");
}
}
break;
case 'update-body':
$release = getReleaseByTag($apiBase, $tag, $token);
if ($release === null) {
fwrite(STDERR, "No release found for tag: {$tag}\n");
exit(1);
}
$releaseId = $release['id'];
$payload = json_encode(['body' => $body ?? '']);
$result = giteaApi("{$apiBase}/releases/{$releaseId}", 'PATCH', $token, $payload);
if ($result['code'] >= 200 && $result['code'] < 300) {
echo "Release body updated for tag: {$tag}\n";
} else {
fwrite(STDERR, "Failed to update body: HTTP {$result['code']}\n");
exit(1);
}
break;
case 'delete':
$existing = getReleaseByTag($apiBase, $tag, $token);
if ($existing !== null) {
giteaApi("{$apiBase}/releases/{$existing['id']}", 'DELETE', $token);
giteaApi("{$apiBase}/tags/{$tag}", 'DELETE', $token);
echo "Deleted: {$tag} (id: {$existing['id']})\n";
} else {
echo "No release found for tag: {$tag}\n";
}
break;
default:
fwrite(STDERR, "Unknown action: {$action}\n");
fwrite(STDERR, "Valid actions: create, upload, update-body, delete\n");
exit(1);
}
exit(0);
+3 -4
View File
@@ -5,11 +5,10 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: MokoStandards.CLI
* INGROUP: MokoStandards
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/release_notes.php
* VERSION: 04.06.00
* BRIEF: Extract release notes from CHANGELOG.md for a given version
*/
+178
View File
@@ -0,0 +1,178 @@
#!/usr/bin/env php
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/release_validate.php
* BRIEF: Pre-release validation — version consistency, required files, manifest checks
*
* Usage:
* php release_validate.php --path /repo --version 04.01.00
* php release_validate.php --path /repo --version 04.01.00 --platform joomla --output-summary
*
* Options:
* --path Repository root (default: .)
* --version Expected version string (required)
* --platform joomla|dolibarr|generic (default: joomla)
* --output-summary Write markdown table to $GITHUB_STEP_SUMMARY
*/
declare(strict_types=1);
$path = '.';
$version = null;
$platform = 'joomla';
$outputSummary = false;
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 === '--platform' && isset($argv[$i + 1])) $platform = $argv[$i + 1];
if ($arg === '--output-summary') $outputSummary = true;
}
if ($version === null) {
fwrite(STDERR, "Usage: release_validate.php --path . --version XX.YY.ZZ [--platform joomla]\n");
exit(1);
}
$root = realpath($path) ?: $path;
$pass = 0;
$fail = 0;
$warn = 0;
$results = [];
function addResult(string $check, string $status, string $details): void {
global $pass, $fail, $warn, $results;
$results[] = ['check' => $check, 'status' => $status, 'details' => $details];
if ($status === 'PASS') $pass++;
elseif ($status === 'FAIL') $fail++;
elseif ($status === 'WARN') $warn++;
}
// 1. README.md exists and contains VERSION
if (!file_exists("{$root}/README.md")) {
addResult('README.md', 'FAIL', 'Not found');
} else {
$readme = file_get_contents("{$root}/README.md");
if (preg_match('/VERSION:\s*' . preg_quote($version, '/') . '/', $readme) ||
strpos($readme, $version) !== false) {
addResult('README.md version', 'PASS', "`{$version}` found");
} else {
addResult('README.md version', 'FAIL', "`{$version}` not found in README.md");
}
}
// 2. CHANGELOG.md exists with matching section
if (!file_exists("{$root}/CHANGELOG.md")) {
addResult('CHANGELOG.md', 'WARN', 'Not found');
} else {
$cl = file_get_contents("{$root}/CHANGELOG.md");
if (preg_match('/^##\s.*' . preg_quote($version, '/') . '/m', $cl)) {
addResult('CHANGELOG.md version', 'PASS', "Section for `{$version}` found");
} else {
addResult('CHANGELOG.md version', 'WARN', "No section header for `{$version}`");
}
}
// 3. LICENSE file exists
$licenseFound = false;
foreach (['LICENSE', 'LICENSE.md', 'LICENSE.txt', 'COPYING'] as $lf) {
if (file_exists("{$root}/{$lf}")) { $licenseFound = true; break; }
}
addResult('LICENSE', $licenseFound ? 'PASS' : 'FAIL', $licenseFound ? 'Found' : 'Not found');
// 4. Platform-specific checks
if ($platform === 'joomla') {
// Find XML manifest
$manifest = null;
$searchDirs = ["{$root}/src", $root];
foreach ($searchDirs as $dir) {
if (!is_dir($dir)) continue;
foreach (glob("{$dir}/*.xml") as $xmlFile) {
$content = file_get_contents($xmlFile);
if (strpos($content, '<extension') !== false) {
$manifest = $xmlFile;
break 2;
}
}
}
if ($manifest === null) {
addResult('XML manifest', 'FAIL', 'No Joomla manifest found');
} else {
if (preg_match('/<version>([^<]+)<\/version>/', file_get_contents($manifest), $m)) {
$mVer = trim($m[1]);
if ($mVer === $version) {
addResult('Manifest version', 'PASS', "`{$mVer}` matches");
} else {
addResult('Manifest version', 'FAIL', "`{$mVer}` != `{$version}`");
}
} else {
addResult('Manifest version', 'FAIL', 'No <version> tag in manifest');
}
}
// updates.xml
if (!file_exists("{$root}/updates.xml")) {
addResult('updates.xml', 'WARN', 'Not found');
} else {
$ux = file_get_contents("{$root}/updates.xml");
if (preg_match('/<version>' . preg_quote($version, '/') . '<\/version>/', $ux)) {
addResult('updates.xml version', 'PASS', "`{$version}` found");
} else {
addResult('updates.xml version', 'FAIL', "`{$version}` not in updates.xml");
}
}
} elseif ($platform === 'dolibarr') {
$modFile = null;
foreach (['src', 'htdocs'] as $sd) {
$pattern = "{$root}/{$sd}/mod*.class.php";
$matches = glob($pattern);
if (!empty($matches)) { $modFile = $matches[0]; break; }
}
if ($modFile === null) {
addResult('Dolibarr mod file', 'FAIL', 'No mod*.class.php found');
} else {
$mc = file_get_contents($modFile);
if (preg_match("/\\\$this->version\s*=\s*'" . preg_quote($version, '/') . "'/", $mc)) {
addResult('Dolibarr version', 'PASS', "`{$version}` matches");
} else {
addResult('Dolibarr version', 'FAIL', "`{$version}` not found in " . basename($modFile));
}
}
}
// 5. composer.json version (if present)
if (file_exists("{$root}/composer.json")) {
$composer = json_decode(file_get_contents("{$root}/composer.json"), true);
if (isset($composer['version'])) {
if ($composer['version'] === $version) {
addResult('composer.json version', 'PASS', "`{$version}` matches");
} else {
addResult('composer.json version', 'WARN', "`{$composer['version']}` != `{$version}`");
}
}
}
// Output
$table = "| Check | Result | Details |\n|-------|--------|--------|\n";
foreach ($results as $r) {
$table .= "| {$r['check']} | {$r['status']} | {$r['details']} |\n";
}
$table .= "\n**Validation: {$pass} passed, {$fail} failed, {$warn} warnings**\n";
echo $table;
if ($outputSummary) {
$summaryFile = getenv('GITHUB_STEP_SUMMARY');
if ($summaryFile) {
file_put_contents($summaryFile, "### Pre-Release Validation\n\n{$table}\n", FILE_APPEND);
}
}
exit($fail > 0 ? 1 : 0);
+188
View File
@@ -0,0 +1,188 @@
#!/usr/bin/env php
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/release_verify.php
* BRIEF: Verify a built release artifact — version, SHA256, disallowed files
*
* Usage:
* php release_verify.php --zip-path /tmp/pkg.zip --version 04.01.00
* php release_verify.php --zip-path /tmp/pkg.zip --version 04.01.00 --updates-xml updates.xml
* php release_verify.php --zip-path /tmp/pkg.zip --version 04.01.00 --output-summary
*
* Options:
* --zip-path Path to ZIP file (required)
* --version Expected version string (required)
* --platform joomla|dolibarr|generic (default: joomla)
* --updates-xml Path to updates.xml for SHA256 comparison
* --github-output Export verify_pass, verify_fail to $GITHUB_OUTPUT
* --output-summary Write markdown table to $GITHUB_STEP_SUMMARY
*/
declare(strict_types=1);
$zipPath = null;
$version = null;
$platform = 'joomla';
$updatesXml = null;
$githubOutput = false;
$outputSummary = false;
foreach ($argv as $i => $arg) {
if ($arg === '--zip-path' && isset($argv[$i + 1])) $zipPath = $argv[$i + 1];
if ($arg === '--version' && isset($argv[$i + 1])) $version = $argv[$i + 1];
if ($arg === '--platform' && isset($argv[$i + 1])) $platform = $argv[$i + 1];
if ($arg === '--updates-xml' && isset($argv[$i + 1])) $updatesXml = $argv[$i + 1];
if ($arg === '--github-output') $githubOutput = true;
if ($arg === '--output-summary') $outputSummary = true;
}
if ($zipPath === null || $version === null) {
fwrite(STDERR, "Usage: release_verify.php --zip-path FILE --version XX.YY.ZZ [--platform joomla] [--updates-xml FILE]\n");
exit(1);
}
$pass = 0;
$fail = 0;
$warn = 0;
$results = [];
function addResult(string $check, string $status, string $details): void {
global $pass, $fail, $warn, $results;
$results[] = ['check' => $check, 'status' => $status, 'details' => $details];
if ($status === 'PASS') $pass++;
elseif ($status === 'FAIL') $fail++;
elseif ($status === 'WARN') $warn++;
}
// 1. ZIP exists and is readable
if (!file_exists($zipPath) || !is_readable($zipPath)) {
addResult('ZIP exists', 'FAIL', "Not found or not readable: {$zipPath}");
} else {
addResult('ZIP exists', 'PASS', basename($zipPath));
// 2. Extract ZIP
$tmpDir = sys_get_temp_dir() . '/release-verify-' . uniqid();
mkdir($tmpDir, 0755, true);
$zip = new ZipArchive();
if ($zip->open($zipPath) !== true) {
addResult('ZIP extract', 'FAIL', 'ZipArchive could not open file');
} else {
$zip->extractTo($tmpDir);
$zip->close();
addResult('ZIP extract', 'PASS', 'Extracted successfully');
// 3. Manifest version check (Joomla)
if ($platform === 'joomla') {
$manifest = null;
foreach (glob("{$tmpDir}/*.xml") as $xmlFile) {
$content = file_get_contents($xmlFile);
if (strpos($content, '<extension') !== false) {
$manifest = $xmlFile;
break;
}
}
if ($manifest !== null) {
if (preg_match('/<version>([^<]+)<\/version>/', file_get_contents($manifest), $m)) {
$manifestVer = trim($m[1]);
if ($manifestVer === $version) {
addResult('Manifest version', 'PASS', "`{$manifestVer}` matches release");
} else {
addResult('Manifest version', 'FAIL', "`{$manifestVer}` != `{$version}`");
}
} else {
addResult('Manifest version', 'WARN', 'No <version> tag in manifest');
}
} else {
addResult('Manifest version', 'WARN', 'No XML manifest found in ZIP');
}
}
// 4. SHA256 vs updates.xml
$zipSha = hash_file('sha256', $zipPath);
if ($updatesXml !== null && file_exists($updatesXml)) {
$uxContent = file_get_contents($updatesXml);
if (preg_match('/<sha256>([^<]+)<\/sha256>/', $uxContent, $m)) {
$expectedSha = trim($m[1]);
if ($zipSha === $expectedSha) {
addResult('SHA256 vs updates.xml', 'PASS', '`' . substr($zipSha, 0, 16) . '...`');
} else {
addResult('SHA256 vs updates.xml', 'FAIL', "ZIP=`" . substr($zipSha, 0, 16) . "...` updates.xml=`" . substr($expectedSha, 0, 16) . "...`");
}
} else {
addResult('SHA256 vs updates.xml', 'WARN', 'No <sha256> in updates.xml');
}
}
// 5. Disallowed files
$disallowed = ['.claude', '.mcp.json', 'TODO.md', 'todo.md', '.git', 'node_modules', '.env'];
$found = [];
$rit = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($tmpDir, RecursiveDirectoryIterator::SKIP_DOTS));
foreach ($rit as $file) {
$name = $file->getFilename();
if (in_array($name, $disallowed, true)) {
$found[] = $name;
}
}
if (count($found) > 0) {
addResult('Disallowed files', 'FAIL', 'Found: ' . implode(', ', array_unique($found)));
} else {
addResult('Disallowed files', 'PASS', 'None found');
}
// 6. Non-vendor .min files
$minCount = 0;
$rit = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($tmpDir, RecursiveDirectoryIterator::SKIP_DOTS));
foreach ($rit as $file) {
$rel = str_replace($tmpDir . '/', '', $file->getPathname());
if (strpos($rel, 'vendor/') !== false) continue;
if (preg_match('/\.(min\.css|min\.js)$/', $file->getFilename())) {
$minCount++;
}
}
if ($minCount > 0) {
addResult('Non-vendor .min files', 'WARN', "{$minCount} file(s) — should be generated at runtime");
} else {
addResult('Non-vendor .min files', 'PASS', 'None shipped');
}
// Clean up
$rit = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($tmpDir, RecursiveDirectoryIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST);
foreach ($rit as $file) {
$file->isDir() ? rmdir($file->getPathname()) : unlink($file->getPathname());
}
rmdir($tmpDir);
}
}
// Output
$table = "| Check | Result | Details |\n|-------|--------|--------|\n";
foreach ($results as $r) {
$table .= "| {$r['check']} | {$r['status']} | {$r['details']} |\n";
}
$table .= "\n**Verification: {$pass} passed, {$fail} failed, {$warn} warnings**\n";
echo $table;
if ($outputSummary) {
$summaryFile = getenv('GITHUB_STEP_SUMMARY');
if ($summaryFile) {
file_put_contents($summaryFile, "### Release Verification\n\n{$table}\n", FILE_APPEND);
}
}
if ($githubOutput) {
$outputFile = getenv('GITHUB_OUTPUT');
if ($outputFile) {
file_put_contents($outputFile, "verify_pass={$pass}\nverify_fail={$fail}\nverify_warn={$warn}\n", FILE_APPEND);
}
}
exit($fail > 0 ? 1 : 0);
+250
View File
@@ -0,0 +1,250 @@
#!/usr/bin/env php
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/scaffold_client.php
* VERSION: 01.00.00
* BRIEF: Scaffold a new client-waas repo from Template-Client-WaaS with pre-configured settings
*/
declare(strict_types=1);
final class ScaffoldClient
{
private string $name = '';
private string $org = '';
private string $giteaUrl = 'https://git.mokoconsulting.tech';
private string $token = '';
private bool $dryRun = false;
public function run(): int
{
$this->parseArgs();
if ($this->name === '' || $this->org === '' || $this->token === '')
{
$this->log('ERROR: --name, --org, and --token are required.');
$this->printUsage();
return 1;
}
$repoName = 'client-waas-' . $this->name;
$this->log("Scaffolding client repo: {$this->org}/{$repoName}");
$this->log("Gitea URL: {$this->giteaUrl}");
if ($this->dryRun)
{
$this->log('[DRY RUN] Would create repo from template MokoConsulting/Template-Client-WaaS');
$this->log("[DRY RUN] Repo: {$this->org}/{$repoName}");
$this->log("[DRY RUN] Description: \"{$this->name} WaaS site\"");
$this->log('[DRY RUN] Would create dev branch from main');
$this->printPostSetupInstructions($repoName);
return 0;
}
// Step 1: Create repo from template
$this->log('Step 1: Creating repo from template...');
$createPayload = json_encode([
'owner' => $this->org,
'name' => $repoName,
'description' => "{$this->name} WaaS site",
'private' => true,
'git_content' => true,
'topics' => true,
'labels' => true,
]);
$response = $this->apiRequest(
'POST',
"/api/v1/repos/MokoConsulting/Template-Client-WaaS/generate",
$createPayload
);
if ($response['code'] < 200 || $response['code'] >= 300)
{
$this->log("ERROR: Failed to create repo (HTTP {$response['code']}).");
$this->log("Response: {$response['body']}");
return 1;
}
$this->log("Repo created: {$this->org}/{$repoName}");
// Step 2: Set repo description (already set via generate, but confirm)
$this->log('Step 2: Updating repo description...');
$updatePayload = json_encode([
'description' => "{$this->name} WaaS site",
]);
$response = $this->apiRequest(
'PATCH',
"/api/v1/repos/{$this->org}/{$repoName}",
$updatePayload
);
if ($response['code'] >= 200 && $response['code'] < 300)
{
$this->log('Description updated.');
}
else
{
$this->log("WARNING: Could not update description (HTTP {$response['code']}).");
}
// Step 3: Create dev branch from main
$this->log('Step 3: Creating dev branch from main...');
$branchPayload = json_encode([
'new_branch_name' => 'dev',
'old_branch_name' => 'main',
]);
$response = $this->apiRequest(
'POST',
"/api/v1/repos/{$this->org}/{$repoName}/branches",
$branchPayload
);
if ($response['code'] >= 200 && $response['code'] < 300)
{
$this->log('Branch "dev" created from "main".');
}
else
{
$this->log("WARNING: Could not create dev branch (HTTP {$response['code']}).");
$this->log("Response: {$response['body']}");
}
// Step 4: Print post-setup instructions
$this->printPostSetupInstructions($repoName);
$this->log('Scaffold complete.');
return 0;
}
private function parseArgs(): void
{
$args = $_SERVER['argv'] ?? [];
$count = count($args);
for ($i = 1; $i < $count; $i++)
{
switch ($args[$i])
{
case '--name':
$this->name = $args[++$i] ?? '';
break;
case '--org':
$this->org = $args[++$i] ?? '';
break;
case '--gitea-url':
$this->giteaUrl = rtrim($args[++$i] ?? '', '/');
break;
case '--token':
$this->token = $args[++$i] ?? '';
break;
case '--dry-run':
$this->dryRun = true;
break;
case '--help':
case '-h':
$this->printUsage();
exit(0);
default:
$this->log("WARNING: Unknown argument: {$args[$i]}");
break;
}
}
}
private function printUsage(): void
{
$this->log('Usage: scaffold_client.php --name <client-name> --org <gitea-org> --token <token> [options]');
$this->log('');
$this->log('Options:');
$this->log(' --name <name> Client name (e.g., "clarksvillefurs")');
$this->log(' --org <org> Gitea organization (e.g., "ClarksvilleFurs")');
$this->log(' --gitea-url <url> Gitea URL (default: https://git.mokoconsulting.tech)');
$this->log(' --token <token> Gitea API token');
$this->log(' --dry-run Show what would be done without making changes');
$this->log(' --help, -h Show this help');
}
private function printPostSetupInstructions(string $repoName): void
{
$this->log('');
$this->log('=== POST-SETUP INSTRUCTIONS ===');
$this->log('');
$this->log("Navigate to: {$this->giteaUrl}/{$this->org}/{$repoName}/settings");
$this->log('');
$this->log('Set the following REPO VARIABLES (Settings > Actions > Variables):');
$this->log(' DEV_SYNC_HOST - Dev server hostname or IP');
$this->log(' DEV_SYNC_PORT - Dev server SSH port (default: 22)');
$this->log(' DEV_SYNC_USER - Dev server SSH username');
$this->log(' DEV_SYNC_PATH - Dev server deploy path');
$this->log(' LIVE_SSH_HOST - Live server hostname or IP');
$this->log(' LIVE_SSH_PORT - Live server SSH port (default: 22)');
$this->log(' LIVE_SSH_USER - Live server SSH username');
$this->log(' LIVE_SYNC_PATH - Live server deploy path');
$this->log('');
$this->log('Set the following REPO SECRETS (Settings > Actions > Secrets):');
$this->log(' DEV_SYNC_KEY - Private SSH key for dev server');
$this->log(' LIVE_SSH_KEY - Private SSH key for live server');
$this->log('');
$this->log('================================');
}
private function apiRequest(string $method, string $endpoint, ?string $body = null): array
{
$url = $this->giteaUrl . $endpoint;
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Accept: application/json',
"Authorization: token {$this->token}",
]);
if ($body !== null)
{
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
}
$responseBody = curl_exec($ch);
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
if (curl_errno($ch))
{
$error = curl_error($ch);
curl_close($ch);
return ['code' => 0, 'body' => "cURL error: {$error}"];
}
curl_close($ch);
return ['code' => $httpCode, 'body' => $responseBody];
}
private function log(string $message): void
{
fwrite(STDERR, $message . PHP_EOL);
}
}
$app = new ScaffoldClient();
exit($app->run());
+7 -8
View File
@@ -7,18 +7,17 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: MokoStandards.CLI
* INGROUP: MokoStandards
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* 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
* php cli/sync_rulesets.php # Apply to all repos
* php cli/sync_rulesets.php --repo MokoCRM # Single repo
* php cli/sync_rulesets.php --dry-run # Preview only
* php 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.
+334
View File
@@ -0,0 +1,334 @@
#!/usr/bin/env php
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/updates_xml_build.php
* BRIEF: Generate Joomla updates.xml from extension manifest metadata
*
* Usage:
* php updates_xml_build.php --path /repo --version 04.01.00 --stability stable
* php updates_xml_build.php --path /repo --version 04.01.00 --stability stable --sha SHA256
* php updates_xml_build.php --path /repo --version 04.01.00 --stability stable --github-output
*
* Options:
* --path Repository root (default: .)
* --version Version string (required)
* --stability One of: stable, rc, beta, alpha, development (default: stable)
* --sha SHA-256 hash of the ZIP package (optional)
* --gitea-url Gitea instance URL (default: env GITEA_URL or https://git.mokoconsulting.tech)
* --org Organization (default: env GITEA_ORG)
* --repo Repository name (default: env GITEA_REPO)
* --output Output file path (default: updates.xml in --path)
* --github-output Export ext_element, ext_name, ext_type, ext_folder to $GITHUB_OUTPUT
*/
declare(strict_types=1);
// -- Argument parsing ---------------------------------------------------------
$path = '.';
$version = null;
$stability = 'stable';
$sha = null;
$giteaUrl = getenv('GITEA_URL') ?: 'https://git.mokoconsulting.tech';
$org = getenv('GITEA_ORG') ?: '';
$repo = getenv('GITEA_REPO') ?: '';
$outputFile = null;
$githubOutput = false;
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 === '--stability' && isset($argv[$i + 1])) $stability = $argv[$i + 1];
if ($arg === '--sha' && isset($argv[$i + 1])) $sha = $argv[$i + 1];
if ($arg === '--gitea-url' && isset($argv[$i + 1])) $giteaUrl = $argv[$i + 1];
if ($arg === '--org' && isset($argv[$i + 1])) $org = $argv[$i + 1];
if ($arg === '--repo' && isset($argv[$i + 1])) $repo = $argv[$i + 1];
if ($arg === '--output' && isset($argv[$i + 1])) $outputFile = $argv[$i + 1];
if ($arg === '--github-output') $githubOutput = true;
}
if ($version === null) {
fwrite(STDERR, "Usage: updates_xml_build.php --path . --version XX.YY.ZZ [--stability stable] [--sha SHA]\n");
exit(1);
}
$root = realpath($path) ?: $path;
// -- Locate Joomla manifest ---------------------------------------------------
$manifest = null;
// Priority: pkg_*.xml in src/ > any extension XML in src/ > any in root
$candidates = glob("{$root}/src/pkg_*.xml") ?: [];
foreach ($candidates as $f) {
if (strpos(file_get_contents($f), '<extension') !== false) {
$manifest = $f;
break;
}
}
if ($manifest === null) {
$searchDirs = ["{$root}/src", "{$root}"];
foreach ($searchDirs as $dir) {
if (!is_dir($dir)) continue;
foreach (glob("{$dir}/*.xml") ?: [] as $f) {
if (strpos(file_get_contents($f), '<extension') !== false) {
$manifest = $f;
break 2;
}
}
}
}
if ($manifest === null) {
fwrite(STDERR, "No Joomla XML manifest found in {$root}\n");
exit(1);
}
// -- Parse extension metadata -------------------------------------------------
$xml = file_get_contents($manifest);
// Extract fields via regex (more portable than SimpleXML for malformed manifests)
$extName = '';
if (preg_match('/<name>([^<]+)<\/name>/', $xml, $m)) $extName = $m[1];
$extType = '';
if (preg_match('/<extension[^>]*type="([^"]+)"/', $xml, $m)) $extType = $m[1];
$extElement = '';
if (preg_match('/<element>([^<]+)<\/element>/', $xml, $m)) $extElement = $m[1];
if (empty($extElement) && preg_match('/plugin="([^"]+)"/', $xml, $m)) $extElement = $m[1];
if (empty($extElement) && preg_match('/module="([^"]+)"/', $xml, $m)) $extElement = $m[1];
if (empty($extElement)) {
$fname = strtolower(pathinfo($manifest, PATHINFO_FILENAME));
if (in_array($fname, ['templatedetails', 'manifest'])) {
$extElement = strtolower(str_replace([' ', '-'], '', $repo ?: basename($root)));
} else {
$extElement = $fname;
}
}
$extClient = '';
if (preg_match('/<extension[^>]*client="([^"]+)"/', $xml, $m)) $extClient = $m[1];
$extFolder = '';
if (preg_match('/<extension[^>]*group="([^"]+)"/', $xml, $m)) $extFolder = $m[1];
$targetPlatform = '';
if (preg_match('/(<targetplatform[^\/]*\/>)/', $xml, $m)) $targetPlatform = $m[1];
if (empty($targetPlatform)) {
$targetPlatform = '<targetplatform name="joomla" version="((5.[0-9])|(6.[0-9]))" />';
}
$phpMinimum = '';
if (preg_match('/<php_minimum>([^<]+)<\/php_minimum>/', $xml, $m)) $phpMinimum = $m[1];
// Resolve language key names (e.g. PLG_SYSTEM_MOKOJOOMTOS)
if (preg_match('/^[A-Z_]+$/', $extName)) {
$iniFiles = [];
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($root, RecursiveDirectoryIterator::SKIP_DOTS)
);
foreach ($iterator as $file) {
if (preg_match('/\.sys\.ini$/i', $file->getFilename())) {
$iniFiles[] = $file->getPathname();
}
}
foreach ($iniFiles as $ini) {
$content = file_get_contents($ini);
if (preg_match('/^' . preg_quote($extName, '/') . '="([^"]+)"/m', $content, $m)) {
$extName = $m[1];
break;
}
}
}
// Fallbacks
if (empty($extName)) $extName = $repo ?: basename($root);
if (empty($extType)) $extType = 'component';
// -- Build type prefix --------------------------------------------------------
$typePrefix = '';
switch ($extType) {
case 'plugin': $typePrefix = "plg_{$extFolder}_"; break;
case 'module': $typePrefix = 'mod_'; break;
case 'component': $typePrefix = 'com_'; break;
case 'template': $typePrefix = 'tpl_'; break;
case 'library': $typePrefix = 'lib_'; break;
case 'package': $typePrefix = 'pkg_'; break;
}
// -- Export to GITHUB_OUTPUT if requested -------------------------------------
if ($githubOutput) {
$ghOutput = getenv('GITHUB_OUTPUT');
$lines = [
"ext_element={$extElement}",
"ext_name={$extName}",
"ext_type={$extType}",
"ext_folder={$extFolder}",
"type_prefix={$typePrefix}",
];
if ($ghOutput) {
file_put_contents($ghOutput, implode("\n", $lines) . "\n", FILE_APPEND);
fwrite(STDERR, "Exported " . count($lines) . " fields to GITHUB_OUTPUT\n");
} else {
foreach ($lines as $line) echo "{$line}\n";
}
}
// -- Stability suffix map -----------------------------------------------------
$stabilitySuffixMap = [
'stable' => '',
'rc' => '-rc',
'beta' => '-beta',
'alpha' => '-alpha',
'development' => '-dev',
];
$stabilityTagMap = [
'stable' => 'stable',
'rc' => 'rc',
'beta' => 'beta',
'alpha' => 'alpha',
'development' => 'development',
];
// -- Build update entries -----------------------------------------------------
$releaseTag = $stabilityTagMap[$stability] ?? $stability;
// For the primary entry: apply suffix if not stable
$primarySuffix = $stabilitySuffixMap[$stability] ?? '';
$primaryVersion = $version . $primarySuffix;
$downloadUrl = "{$giteaUrl}/{$org}/{$repo}/releases/download/{$releaseTag}/{$typePrefix}{$extElement}-{$primaryVersion}.zip";
$infoUrl = "{$giteaUrl}/{$org}/{$repo}/releases/tag/{$releaseTag}";
// Build client tag
$clientTag = '';
if (!empty($extClient)) {
$clientTag = " <client>{$extClient}</client>";
} elseif ($extType === 'module' || $extType === 'plugin') {
$clientTag = ' <client>site</client>';
}
// Build folder tag
$folderTag = '';
if (!empty($extFolder) && $extType === 'plugin') {
$folderTag = " <folder>{$extFolder}</folder>";
}
// PHP minimum tag
$phpTag = '';
if (!empty($phpMinimum)) {
$phpTag = " <php_minimum>{$phpMinimum}</php_minimum>";
}
// SHA tag
$shaTag = '';
if (!empty($sha)) {
$shaTag = " <sha256>{$sha}</sha256>";
}
/**
* Build a single <update> entry for a given stability tag
*/
function buildEntry(
string $tagName,
string $entryVersion,
string $entryDownloadUrl,
string $extName,
string $extElement,
string $extType,
string $clientTag,
string $folderTag,
string $infoUrl,
string $targetPlatform,
string $phpTag,
string $shaTag
): string {
$lines = [];
$lines[] = ' <update>';
$lines[] = " <name>{$extName}</name>";
$lines[] = " <description>{$extName} update</description>";
$lines[] = " <element>{$extElement}</element>";
$lines[] = " <type>{$extType}</type>";
$lines[] = " <version>{$entryVersion}</version>";
if (!empty($clientTag)) $lines[] = $clientTag;
if (!empty($folderTag)) $lines[] = $folderTag;
$lines[] = " <tags><tag>{$tagName}</tag></tags>";
$lines[] = " <infourl title=\"{$extName}\">{$infoUrl}</infourl>";
$lines[] = ' <downloads>';
$lines[] = " <downloadurl type=\"full\" format=\"zip\">{$entryDownloadUrl}</downloadurl>";
$lines[] = ' </downloads>';
if (!empty($shaTag)) $lines[] = $shaTag;
$lines[] = " {$targetPlatform}";
if (!empty($phpTag)) $lines[] = $phpTag;
$lines[] = ' <maintainer>Moko Consulting</maintainer>';
$lines[] = ' <maintainerurl>https://mokoconsulting.tech</maintainerurl>';
$lines[] = ' </update>';
return implode("\n", $lines);
}
// -- Determine which channels to write ----------------------------------------
// Stable cascades to all channels; pre-releases only write their level and below
// Each channel gets its own suffixed version:
// development -> 04.01.00-dev
// alpha -> 04.01.00-alpha
// beta -> 04.01.00-beta
// rc -> 04.01.00-rc
// stable -> 04.01.00
$allChannels = ['development', 'alpha', 'beta', 'rc', 'stable'];
$stabilityIndex = array_search($stability === 'development' ? 'development' : $stability, $allChannels);
if ($stabilityIndex === false) $stabilityIndex = 4; // default to stable
// Write entries for this stability and all below it
$entries = [];
for ($i = 0; $i <= $stabilityIndex; $i++) {
$channelName = $allChannels[$i];
$channelSuffix = $stabilitySuffixMap[$channelName] ?? '';
$channelVersion = $version . $channelSuffix;
$channelTag = $stabilityTagMap[$channelName] ?? $channelName;
$channelDownloadUrl = "{$giteaUrl}/{$org}/{$repo}/releases/download/{$channelTag}/{$typePrefix}{$extElement}-{$channelVersion}.zip";
$channelInfoUrl = "{$giteaUrl}/{$org}/{$repo}/releases/tag/{$channelTag}";
$entries[] = buildEntry(
$channelName,
$channelVersion,
$channelDownloadUrl,
$extName,
$extElement,
$extType,
$clientTag,
$folderTag,
$channelInfoUrl,
$targetPlatform,
$phpTag,
$shaTag
);
}
// -- Write updates.xml --------------------------------------------------------
$year = date('Y');
$output = <<<XML
<?xml version='1.0' encoding='UTF-8'?>
<!-- Copyright (C) {$year} Moko Consulting <hello@mokoconsulting.tech>
SPDX-License-Identifier: GPL-3.0-or-later
VERSION: {$primaryVersion}
-->
<updates>
XML;
$output .= "\n" . implode("\n", $entries) . "\n</updates>\n";
$dest = $outputFile ?? "{$root}/updates.xml";
file_put_contents($dest, $output);
$channelCount = count($entries);
echo "updates.xml: {$primaryVersion} ({$channelCount} channel(s), stability={$stability})\n";
echo "Output: {$dest}\n";
exit(0);
+169
View File
@@ -0,0 +1,169 @@
#!/usr/bin/env php
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/updates_xml_sync.php
* VERSION: 05.00.01
* BRIEF: Sync updates.xml to target branches via Gitea API
* NOTE: Called by pre-release and auto-release workflows after updates.xml
* is modified on the current branch. Pushes the file to other branches
* without requiring a git checkout (avoids merge conflicts).
*
* Usage:
* php updates_xml_sync.php --path /repo --branches main,dev --current dev
* php updates_xml_sync.php --path /repo --branches main --current dev --version 02.01.27
*
* Options:
* --path Repository root containing updates.xml (default: .)
* --branches Comma-separated target branches to sync to (default: main,dev)
* --current Current branch to skip (required)
* --version Version string for commit message (optional)
* --token Gitea API token (default: env GA_TOKEN)
* --gitea-url Gitea instance URL (default: env GITEA_URL or https://git.mokoconsulting.tech)
* --org Organization (default: env GITEA_ORG)
* --repo Repository name (default: env GITEA_REPO)
*/
declare(strict_types=1);
// ── Argument parsing ────────────────────────────────────────────────────
$path = '.';
$branches = 'main,dev';
$current = '';
$version = '';
$token = getenv('GA_TOKEN') ?: '';
$giteaUrl = getenv('GITEA_URL') ?: 'https://git.mokoconsulting.tech';
$org = getenv('GITEA_ORG') ?: '';
$repo = getenv('GITEA_REPO') ?: '';
foreach ($argv as $i => $arg) {
if ($arg === '--path' && isset($argv[$i + 1])) $path = $argv[$i + 1];
if ($arg === '--branches' && isset($argv[$i + 1])) $branches = $argv[$i + 1];
if ($arg === '--current' && isset($argv[$i + 1])) $current = $argv[$i + 1];
if ($arg === '--version' && isset($argv[$i + 1])) $version = $argv[$i + 1];
if ($arg === '--token' && isset($argv[$i + 1])) $token = $argv[$i + 1];
if ($arg === '--gitea-url' && isset($argv[$i + 1])) $giteaUrl = $argv[$i + 1];
if ($arg === '--org' && isset($argv[$i + 1])) $org = $argv[$i + 1];
if ($arg === '--repo' && isset($argv[$i + 1])) $repo = $argv[$i + 1];
}
if ($current === '') {
fwrite(STDERR, "Error: --current is required\n");
exit(1);
}
if ($token === '') {
fwrite(STDERR, "Error: --token or GA_TOKEN env is required\n");
exit(1);
}
if ($org === '' || $repo === '') {
fwrite(STDERR, "Error: --org and --repo (or GITEA_ORG/GITEA_REPO env) are required\n");
exit(1);
}
$updatesFile = rtrim($path, '/') . '/updates.xml';
if (!file_exists($updatesFile)) {
fwrite(STDERR, "No updates.xml found at {$updatesFile}\n");
exit(0);
}
$content = file_get_contents($updatesFile);
$encoded = base64_encode($content);
$giteaUrl = rtrim($giteaUrl, '/');
$apiBase = "{$giteaUrl}/api/v1/repos/{$org}/{$repo}";
$vLabel = $version !== '' ? " {$version}" : '';
$targets = array_filter(
array_map('trim', explode(',', $branches)),
fn($b) => $b !== '' && $b !== $current
);
if (empty($targets)) {
fwrite(STDERR, "No target branches to sync to (current: {$current})\n");
exit(0);
}
$synced = 0;
$failed = 0;
foreach ($targets as $branch) {
fwrite(STDERR, "Syncing updates.xml -> {$branch}...\n");
$sha = getFileSha($apiBase, $token, $branch);
if ($sha === null) {
fwrite(STDERR, " WARNING: could not get SHA from {$branch}\n");
$failed++;
continue;
}
$ok = putFile($apiBase, $token, $branch, $encoded, $sha,
"chore: sync updates.xml{$vLabel} from {$current} [skip ci]");
if ($ok) {
fwrite(STDERR, " Synced to {$branch}\n");
$synced++;
} else {
fwrite(STDERR, " WARNING: push to {$branch} failed\n");
$failed++;
}
}
fwrite(STDERR, "Done: {$synced} synced, {$failed} failed\n");
exit($failed > 0 ? 1 : 0);
// ═══════════════════════════════════════════════════════════════════════
function getFileSha(string $apiBase, string $token, string $branch): ?string
{
$resp = apiCall('GET', "{$apiBase}/contents/updates.xml?ref={$branch}", $token);
return $resp['sha'] ?? null;
}
function putFile(string $apiBase, string $token, string $branch,
string $encoded, string $sha, string $msg): bool
{
$resp = apiCall('PUT', "{$apiBase}/contents/updates.xml", $token, [
'content' => $encoded,
'sha' => $sha,
'message' => $msg,
'branch' => $branch,
]);
return $resp !== null;
}
function apiCall(string $method, string $url, string $token, ?array $data = null): ?array
{
$headers = [
"Authorization: token {$token}",
'Content-Type: application/json',
'Accept: application/json',
];
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
if ($data !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS,
json_encode($data, JSON_UNESCAPED_SLASHES));
}
$body = curl_exec($ch);
$code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return ($code >= 200 && $code < 300)
? (json_decode($body, true) ?: [])
: null;
}
+3 -4
View File
@@ -5,11 +5,10 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: MokoStandards.CLI
* INGROUP: MokoStandards
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/version_bump.php
* VERSION: 04.06.00
* BRIEF: Auto-increment patch version in README.md — outputs old → new
*/
+3 -4
View File
@@ -5,11 +5,10 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: MokoStandards.CLI
* INGROUP: MokoStandards
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/version_read.php
* VERSION: 04.06.00
* BRIEF: Read VERSION from README.md — outputs just the version string
*/
+55 -14
View File
@@ -5,12 +5,18 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: MokoStandards.CLI
* INGROUP: MokoStandards
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/version_set_platform.php
* VERSION: 04.06.00
* BRIEF: Set version in platform-specific files (Dolibarr $this->version, Joomla <version>)
*
* Usage:
* php version_set_platform.php --path . --version 04.01.00
* php version_set_platform.php --path . --version 04.01.00 --stability alpha
*
* When --stability is set to anything other than "stable", the suffix is
* appended to the version (e.g. 04.01.00-dev, 04.01.00-alpha, 04.01.00-rc).
*/
declare(strict_types=1);
@@ -18,10 +24,13 @@ declare(strict_types=1);
$path = '.';
$version = null;
$branch = null;
$stability = 'stable';
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];
if ($arg === '--stability' && isset($argv[$i + 1])) $stability = $argv[$i + 1];
}
// Auto-detect branch from git or GitHub env
@@ -33,22 +42,54 @@ if ($branch === null) {
}
if ($version === null) {
fwrite(STDERR, "Usage: version_set_platform.php --path . --version development\n");
fwrite(STDERR, "Usage: version_set_platform.php --path . --version 04.01.00 [--stability dev]\n");
exit(1);
}
// Append stability suffix for non-stable releases
$stabilitySuffixMap = [
'stable' => '',
'development' => '-dev',
'dev' => '-dev',
'alpha' => '-alpha',
'beta' => '-beta',
'rc' => '-rc',
'release-candidate' => '-rc',
];
$suffix = $stabilitySuffixMap[$stability] ?? '';
if ($suffix !== '' && !str_ends_with($version, $suffix)) {
$version .= $suffix;
echo "Version with stability suffix: {$version}\n";
}
$root = realpath($path) ?: $path;
// Detect platform
// Detect platform — check manifest.xml first, then legacy .mokostandards
$platform = '';
$mokoStandards = "{$root}/.github/.mokostandards";
if (!file_exists($mokoStandards)) {
$mokoStandards = "{$root}/.mokostandards";
// New format: .mokogitea/manifest.xml (XML with <platform> tag)
$manifestXml = "{$root}/.mokogitea/manifest.xml";
if (file_exists($manifestXml)) {
$xml = @simplexml_load_file($manifestXml);
if ($xml && isset($xml->governance->platform)) {
$platform = (string) $xml->governance->platform;
}
}
if (file_exists($mokoStandards)) {
$content = file_get_contents($mokoStandards);
if (preg_match('/^platform:\s*(.+)/m', $content, $m)) {
$platform = trim($m[1], " \t\n\r\"'");
// Legacy: .mokostandards YAML file
if (empty($platform)) {
$mokoStandards = "{$root}/.github/.mokostandards";
if (!file_exists($mokoStandards)) {
$mokoStandards = "{$root}/.mokogitea/.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\"'");
}
}
}
@@ -91,7 +132,7 @@ if ($platform === 'crm-module') {
}
// Joomla: <version> in XML manifests
if ($platform === 'waas-component') {
if (in_array($platform, ['waas-component', 'joomla'], true)) {
foreach (glob("{$root}/src/*.xml") ?: glob("{$root}/*.xml") ?: [] as $file) {
$content = file_get_contents($file);
if (!str_contains($content, '<extension')) continue;
+3 -4
View File
@@ -2,7 +2,7 @@
"name": "mokoconsulting-tech/enterprise",
"description": "MokoStandards Enterprise API \u2014 PHP implementation",
"type": "library",
"version": "04.05.00",
"version": "05.00.01",
"license": "GPL-3.0-or-later",
"authors": [
{
@@ -17,7 +17,6 @@
"ext-json": "*",
"ext-zip": "*",
"guzzlehttp/guzzle": "^7.8",
"mokoconsulting-tech/enterprise": "dev-version/04",
"monolog/monolog": "^3.5",
"php": ">=8.1",
"phpseclib/phpseclib": "^3.0",
@@ -74,8 +73,8 @@
],
"scripts": {
"test": "phpunit",
"phpcs": "phpcs --standard=phpcs.xml api/",
"phpstan": "phpstan analyse -c phpstan.neon api/",
"phpcs": "phpcs --standard=phpcs.xml lib/ validate/ automation/",
"phpstan": "phpstan analyse -c phpstan.neon lib/ validate/ automation/",
"psalm": "psalm --config=psalm.xml",
"check": [
"@phpcs",
+206
View File
@@ -0,0 +1,206 @@
/**
* Client Repository Structure Definition
* Standard repository structure for managed Joomla client sites (WaaS)
*
* This is NOT a Joomla extension — it's a full managed client site with
* deployment configs, monitoring, SFTP settings, and sync workflows.
* The src/ directory mirrors the Joomla site's public_html.
*
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
* Schema Version: 1.0
*/
locals {
repository_structure = {
metadata = {
name = "Client Site"
description = "Managed Joomla client site — full site structure, not an extension"
repository_type = "client"
platform = "client"
last_updated = "2026-05-09T00:00:00Z"
maintainer = "Moko Consulting"
version = "01.00.00"
schema_version = "1.0"
}
detection_hints = [
"scripts/sftp-config/",
"scripts/sync-dev-to-live.sh",
"monitoring/grafana/",
"src/administrator/",
"src/components/",
"src/plugins/",
"src/templates/",
"src/media/templates/site/mokoonyx/"
]
root_files = [
{
name = "README.md"
extension = "md"
description = "Client site overview and deployment info"
required = true
always_overwrite = false
protected = true
},
{
name = "CHANGELOG.md"
extension = "md"
description = "Release history"
required = true
always_overwrite = false
},
{
name = "LICENSE"
extension = ""
description = "GPL-3.0-or-later license file"
required = true
always_overwrite = true
template = "templates/docs/required/LICENSE"
},
{
name = "Makefile"
extension = ""
description = "Build and deployment targets (includes minify)"
required = true
always_overwrite = false
},
{
name = "composer.json"
extension = "json"
description = "PHP dependencies"
required = true
always_overwrite = false
},
{
name = ".gitignore"
extension = ""
description = "Git ignore rules (must include *.min.css, *.min.js, TODO.md)"
required = true
always_overwrite = false
}
]
directories = [
{
name = "src"
path = "src"
description = "Joomla site public_html mirror — deployed via SFTP"
required = true
purpose = "Contains the full Joomla site directory structure"
subdirectories = [
{ name = "administrator", path = "src/administrator", description = "Joomla admin", required = true },
{ name = "components", path = "src/components", description = "Frontend components", required = true },
{ name = "plugins", path = "src/plugins", description = "Plugins", required = true },
{ name = "modules", path = "src/modules", description = "Modules", required = true },
{ name = "templates", path = "src/templates", description = "Templates", required = true },
{ name = "media", path = "src/media", description = "Media assets", required = true },
{ name = "images", path = "src/images", description = "Site images", required = false },
{ name = "language", path = "src/language", description = "Language files", required = false },
{ name = "libraries", path = "src/libraries", description = "Libraries", required = false },
{ name = "layouts", path = "src/layouts", description = "Layouts", required = false }
]
},
{
name = "scripts"
path = "scripts"
description = "Deployment, sync, and monitoring scripts"
required = true
purpose = "Contains SFTP configs, sync scripts, and monitoring"
subdirectories = [
{
name = "sftp-config"
path = "scripts/sftp-config"
description = "SFTP connection configs (dev + live)"
required = true
files = [
{
name = "sftp-config.dev.json"
extension = "json"
description = "Dev server SFTP connection"
required = true
always_overwrite = false
},
{
name = "sftp-config.rs.json"
extension = "json"
description = "Live/release server SFTP connection"
required = true
always_overwrite = false
}
]
}
]
},
{
name = "monitoring"
path = "monitoring"
description = "Grafana dashboard templates"
required = true
purpose = "Contains Panopticon-style Grafana dashboard JSON"
subdirectories = [
{
name = "grafana"
path = "monitoring/grafana"
description = "Grafana dashboard JSON templates"
required = true
files = [
{
name = "client-joomla-dashboard.json"
extension = "json"
description = "Panopticon-style Grafana dashboard template"
required = true
always_overwrite = true
template = "templates/monitoring/client-joomla-dashboard.json"
}
]
}
]
},
{
name = ".gitea"
path = ".gitea"
description = "Gitea configuration"
required = true
purpose = "Contains Gitea Actions workflows"
subdirectories = [
{
name = "workflows"
path = ".gitea/workflows"
description = "Gitea Actions CI/CD workflows"
required = true
files = [
{
name = "auto-release.yml"
extension = "yml"
description = "Auto-release on merge to main"
required = true
always_overwrite = true
},
{
name = "deploy.yml"
extension = "yml"
description = "Deploy src/ to servers via SFTP"
required = true
always_overwrite = true
},
{
name = "add-endpoint.yml"
extension = "yml"
description = "Add monitoring endpoint to sites.json"
required = true
always_overwrite = true
}
]
}
]
}
]
}
}
output "client_structure" {
description = "Client site repository structure definition"
value = local.repository_structure
}
+12 -1
View File
@@ -86,6 +86,15 @@
"description": "Build automation",
"requirementStatus": "suggested",
"audience": "developer"
},
{
"name": "renovate.json",
"extension": "json",
"description": "Renovate dependency management configuration",
"requirementStatus": "required",
"alwaysOverwrite": false,
"audience": "developer",
"template": "templates/configs/renovate.json"
}
],
"directories": [
@@ -158,7 +167,9 @@
"branch-freeze.yml",
"changelog-validation.yml",
"repository-cleanup.yml",
"sync-version-on-merge.yml"
"sync-version-on-merge.yml",
"cascade-dev.yml",
"gitleaks.yml"
]
}
]
@@ -4,7 +4,6 @@
*
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
* Version: 04.05.00
* Schema Version: 1.0
*/
@@ -13,11 +12,11 @@ locals {
metadata = {
name = "MokoCRM Module"
description = "Standard repository structure for MokoCRM (Dolibarr) modules"
repository_type = "crm-module"
repository_type = "dolibarr"
platform = "dolibarr"
last_updated = "2026-01-07T00:00:00Z"
maintainer = "Moko Consulting"
version = "04.05.00"
version = "05.00.00"
schema_version = "1.0"
}
@@ -167,13 +166,14 @@ EOT
audience = "developer"
},
{
name = ".mokostandards"
extension = "yml"
description = "MokoStandards governance attachment — links this repo back to the standards source"
name = ".gitea/.mokostandards"
extension = "xml"
description = "MokoStandards XML manifest — generated programmatically by RepositorySynchronizer::migrateMokoStandards()"
required = true
always_overwrite = true
always_overwrite = false
audience = "developer"
template = "templates/configs/mokostandards.yml.template"
template = "managed-by-sync"
source_type = "programmatic"
},
{
name = "GOVERNANCE.md"
@@ -184,6 +184,15 @@ EOT
protected = true
audience = "all"
template = "templates/docs/required/GOVERNANCE.md"
},
{
name = "renovate.json"
extension = "json"
description = "Renovate dependency management configuration"
required = true
always_overwrite = false
audience = "developer"
template = "templates/configs/renovate.json"
}
]
@@ -1092,6 +1101,22 @@ EOT
requirement_status = "required"
always_overwrite = true
template = "templates/workflows/dolibarr/repo_health.yml.template"
},
{
name = "cascade-dev.yml"
extension = "yml"
description = "Forward-merge main to all open branches (dev, rc/*, beta/*, alpha/*) on push to main"
requirement_status = "required"
always_overwrite = true
template = "workflows/cascade-dev.yml"
},
{
name = "gitleaks.yml"
extension = "yml"
description = "Secret scanning — detect leaked credentials, API keys, and tokens using Gitleaks"
requirement_status = "required"
always_overwrite = true
template = "workflows/gitleaks.yml"
}
]
},
+7 -7
View File
@@ -4,7 +4,6 @@
*
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
* Version: 04.05.00
* Schema Version: 1.0
*/
@@ -17,7 +16,7 @@ locals {
platform = "multi-platform"
last_updated = "2026-01-15T00:00:00Z"
maintainer = "Moko Consulting"
version = "04.05.00"
version = "05.00.00"
schema_version = "1.0"
}
@@ -119,13 +118,14 @@ locals {
template = "templates/configs/composer.generic.json"
},
{
name = ".mokostandards"
extension = "yml"
description = "MokoStandards governance attachment — links this repo back to the standards source"
name = ".gitea/.mokostandards"
extension = "xml"
description = "MokoStandards XML manifest — generated programmatically by RepositorySynchronizer::migrateMokoStandards()"
required = true
always_overwrite = true
always_overwrite = false
audience = "developer"
template = "templates/configs/mokostandards.yml.template"
template = "managed-by-sync"
source_type = "programmatic"
},
{
name = "GOVERNANCE.md"
@@ -4,7 +4,6 @@
*
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
* Version: 04.05.00
* Schema Version: 1.0
*/
@@ -17,7 +16,7 @@ locals {
platform = "multi-platform"
last_updated = "2026-01-16T00:00:00Z"
maintainer = "Moko Consulting"
version = "04.05.00"
version = "05.00.00"
schema_version = "1.0"
}
@@ -193,6 +192,15 @@ locals {
always_overwrite = false
audience = "developer"
template = "templates/configs/composer.generic.json"
},
{
name = "renovate.json"
extension = "json"
description = "Renovate dependency management configuration"
requirement_status = "required"
always_overwrite = false
audience = "developer"
template = "templates/configs/renovate.json"
}
]
@@ -443,6 +451,22 @@ locals {
requirement_status = "required"
always_overwrite = true
template = "templates/workflows/shared/auto-dev-issue.yml.template"
},
{
name = "cascade-dev.yml"
extension = "yml"
description = "Forward-merge main to all open branches (dev, rc/*, beta/*, alpha/*) on push to main"
requirement_status = "required"
always_overwrite = true
template = "workflows/cascade-dev.yml"
},
{
name = "gitleaks.yml"
extension = "yml"
description = "Secret scanning — detect leaked credentials, API keys, and tokens using Gitleaks"
requirement_status = "required"
always_overwrite = true
template = "workflows/gitleaks.yml"
}
]
},
@@ -580,24 +604,46 @@ locals {
{
branch_pattern = "main"
require_pull_request = true
required_approvals = 1
require_code_owner_review = false
required_approvals = 0
dismiss_stale_reviews = true
require_status_checks = true
required_status_checks = ["ci", "code-quality"]
enforce_admins = false
block_on_rejected_reviews = true
restrict_pushes = true
push_whitelist = ["jmiller"]
enable_force_push = true
force_push_whitelist = ["jmiller"]
enforce_admins = false
},
{
branch_pattern = "master"
require_pull_request = true
required_approvals = 1
require_code_owner_review = false
dismiss_stale_reviews = true
require_status_checks = true
required_status_checks = ["ci"]
enforce_admins = false
restrict_pushes = true
branch_pattern = "dev"
require_pull_request = false
required_approvals = 0
restrict_pushes = false
enable_force_push = true
force_push_whitelist = ["jmiller"]
},
{
branch_pattern = "rc/*"
require_pull_request = false
required_approvals = 0
restrict_pushes = false
enable_force_push = true
force_push_whitelist = ["jmiller"]
},
{
branch_pattern = "beta/*"
require_pull_request = false
required_approvals = 0
restrict_pushes = false
enable_force_push = true
force_push_whitelist = ["jmiller"]
},
{
branch_pattern = "alpha/*"
require_pull_request = false
required_approvals = 0
restrict_pushes = false
enable_force_push = true
force_push_whitelist = ["jmiller"]
}
]
@@ -5,7 +5,6 @@
*
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
* Version: 04.05.00
* Schema Version: 1.0
*
* NOTES
@@ -28,7 +27,7 @@ locals {
platform = "github-private"
last_updated = "2026-03-12T00:00:00Z"
maintainer = "Moko Consulting"
version = "04.05.00"
version = "05.00.00"
schema_version = "1.0"
visibility = "private"
sync_priority = -1
@@ -116,12 +115,13 @@ locals {
audience = "developer"
},
{
name = ".mokostandards.yml"
extension = "yml"
description = "MokoStandards governance marker — identifies this repo as platform=github-private"
name = ".gitea/.mokostandards"
extension = "xml"
description = "MokoStandards XML manifest — generated programmatically by RepositorySynchronizer::migrateMokoStandards()"
required = true
always_overwrite = true
template = "templates/configs/mokostandards.yml.template"
always_overwrite = false
template = "managed-by-sync"
source_type = "programmatic"
}
]
-422
View File
@@ -1,422 +0,0 @@
/**
* MokoWaaS Joomla Template Structure Definition
* Standard repository structure for Joomla template projects
*
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
* Version: 04.06.10
* Schema Version: 1.0
*/
locals {
repository_structure = {
metadata = {
name = "Joomla Template"
description = "Standard repository structure for Joomla templates (site or administrator)"
repository_type = "joomla-template"
platform = "mokowaas"
last_updated = "2026-04-14T00:00:00Z"
maintainer = "Moko Consulting"
version = "04.06.10"
schema_version = "1.0"
}
root_files = [
{
name = "README.md"
extension = "md"
description = "Developer-focused documentation for contributors and maintainers"
required = true
always_overwrite = false
protected = true
audience = "developer"
},
{
name = "LICENSE"
extension = ""
description = "License file (GPL-3.0-or-later) - Default for Joomla templates"
required = true
audience = "general"
template = "templates/licenses/GPL-3.0"
license_type = "GPL-3.0-or-later"
},
{
name = "CHANGELOG.md"
extension = "md"
description = "Version history and changes"
required = true
audience = "general"
},
{
name = "SECURITY.md"
extension = "md"
description = "Security policy and vulnerability reporting"
required = true
always_overwrite = true
template = "templates/docs/required/template-SECURITY.md"
audience = "general"
},
{
name = "CODE_OF_CONDUCT.md"
extension = "md"
description = "Community code of conduct"
required = true
always_overwrite = true
template = "templates/docs/extra/template-CODE_OF_CONDUCT.md"
audience = "contributor"
},
{
name = "CONTRIBUTING.md"
extension = "md"
description = "Contribution guidelines"
required = true
always_overwrite = true
template = "templates/docs/required/template-CONTRIBUTING.md"
audience = "contributor"
},
{
name = "templateDetails.xml"
extension = "xml"
description = "Joomla template manifest — declares template metadata, positions, styles, and dependencies"
required = true
always_overwrite = false
audience = "developer"
stub_content = <<-MOKO_END
<?xml version="1.0" encoding="utf-8"?>
<extension type="template" client="site" method="upgrade">
<name>{{TEMPLATE_NAME}}</name>
<creationDate>{{CREATION_DATE}}</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
<license>GPL-3.0-or-later</license>
<version>{{VERSION}}</version>
<description>{{REPO_DESCRIPTION}}</description>
<files>
<filename>index.php</filename>
<filename>component.php</filename>
<filename>error.php</filename>
<filename>offline.php</filename>
<filename>templateDetails.xml</filename>
<folder>html</folder>
<folder>css</folder>
<folder>js</folder>
<folder>images</folder>
<folder>language</folder>
</files>
<media destination="templates/site/{{TEMPLATE_SHORT_NAME}}" folder="media">
<folder>css</folder>
<folder>js</folder>
<folder>images</folder>
<folder>scss</folder>
</media>
<positions>
<position>topbar</position>
<position>navbar</position>
<position>hero</position>
<position>breadcrumbs</position>
<position>sidebar-left</position>
<position>sidebar-right</position>
<position>main-top</position>
<position>main-bottom</position>
<position>footer</position>
<position>debug</position>
</positions>
<updateservers>
<server type="extension" priority="1" name="{{TEMPLATE_NAME}} Update Server">
https://git.mokoconsulting.tech/MokoConsulting/{{REPO_NAME}}/raw/branch/main/updates.xml
</server>
<server type="extension" priority="2" name="{{TEMPLATE_NAME}} Update Server">
https://raw.githubusercontent.com/mokoconsulting-tech/{{REPO_NAME}}/main/updates.xml
</server>
</updateservers>
<config>
<fields name="params">
<fieldset name="basic">
<field name="logoFile" type="media" label="Logo" />
<field name="siteTitle" type="text" label="Site Title" default="" />
<field name="siteDescription" type="text" label="Site Description" default="" />
<field name="colorScheme" type="list" label="Color Scheme" default="light">
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="auto">Auto (system preference)</option>
</field>
</fieldset>
<fieldset name="advanced">
<field name="fluidContainer" type="radio" label="Fluid Container" default="0">
<option value="0">No</option>
<option value="1">Yes</option>
</field>
</fieldset>
</fields>
</config>
</extension>
MOKO_END
},
{
name = "updates.xml"
extension = "xml"
description = "Joomla template update server manifest — polled by Joomla for new versions (dual-platform: Gitea priority 1, GitHub priority 2)"
required = true
always_overwrite = false
audience = "developer"
stub_content = <<-MOKO_END
<updates>
<update>
<name>{{TEMPLATE_NAME}}</name>
<description>{{REPO_DESCRIPTION}}</description>
<element>tpl_{{TEMPLATE_SHORT_NAME}}</element>
<type>template</type>
<version>{{VERSION}}</version>
<downloads>
<downloadurl type="full" format="zip">
https://git.mokoconsulting.tech/mokoconsulting-tech/{{REPO_NAME}}/releases/download/v{{VERSION}}/{{TEMPLATE_SHORT_NAME}}.zip
</downloadurl>
<downloadurl type="full" format="zip">
https://github.com/mokoconsulting-tech/{{REPO_NAME}}/releases/download/v{{VERSION}}/{{TEMPLATE_SHORT_NAME}}.zip
</downloadurl>
</downloads>
<targetplatform name="joomla" version="[56].*"/>
<php_minimum>8.1</php_minimum>
</update>
</updates>
MOKO_END
},
{
name = "phpstan.neon"
extension = "neon"
description = "PHPStan static analysis config with Joomla framework class stubs"
required = true
always_overwrite = true
audience = "developer"
template = "templates/configs/phpstan.joomla.neon"
},
{
name = "Makefile"
description = "Build automation for Joomla template packaging"
required = true
always_overwrite = true
audience = "developer"
template = "templates/makefiles/Makefile.joomla.template"
},
{
name = ".gitignore"
extension = "gitignore"
description = "Git ignore patterns for Joomla template development"
required = true
always_overwrite = false
audience = "developer"
template = "templates/configs/.gitignore.joomla"
},
{
name = ".editorconfig"
extension = "editorconfig"
description = "Editor configuration for consistent coding style"
required = true
always_overwrite = true
template = "templates/configs/.editorconfig"
audience = "developer"
},
{
name = ".ftpignore"
extension = "ftpignore"
description = "FTP/SFTP ignore patterns for deploy-joomla.php"
required = false
always_overwrite = false
audience = "developer"
},
]
subdirectories = [
{
name = "src"
description = "Template source files — maps to templates/{name}/ on the Joomla server"
required = true
files = [
{
name = "index.php"
description = "Main template entry point"
required = true
always_overwrite = false
stub_content = <<-MOKO_END
<?php
/**
* @package Joomla.Site
* @subpackage Templates.{{TEMPLATE_SHORT_NAME}}
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GPL-3.0-or-later
*/
defined('_JEXEC') or die;
use Joomla\CMS\Document\HtmlDocument;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
/** @var HtmlDocument $this */
$app = Factory::getApplication();
$wa = $this->getWebAssetManager();
$doc = $app->getDocument();
$lang = $app->getLanguage();
// Load template CSS/JS via Web Asset Manager
$wa->useStyle('template.{{TEMPLATE_SHORT_NAME}}.css');
$wa->useScript('template.{{TEMPLATE_SHORT_NAME}}.js');
?>
<!DOCTYPE html>
<html lang="<?php echo $this->language; ?>" dir="<?php echo $this->direction; ?>">
<head>
<jdoc:include type="metas" />
<jdoc:include type="styles" />
<jdoc:include type="scripts" />
</head>
<body class="site <?php echo $this->direction === 'rtl' ? 'rtl' : 'ltr'; ?>">
<header>
<jdoc:include type="modules" name="topbar" style="none" />
<jdoc:include type="modules" name="navbar" style="none" />
</header>
<jdoc:include type="modules" name="hero" style="none" />
<jdoc:include type="modules" name="breadcrumbs" style="none" />
<main>
<jdoc:include type="modules" name="main-top" style="html5" />
<jdoc:include type="message" />
<jdoc:include type="component" />
<jdoc:include type="modules" name="main-bottom" style="html5" />
</main>
<footer>
<jdoc:include type="modules" name="footer" style="none" />
</footer>
<jdoc:include type="modules" name="debug" style="none" />
</body>
</html>
MOKO_END
},
{
name = "error.php"
description = "Error page template (404, 500, etc.)"
required = true
always_overwrite = false
},
{
name = "offline.php"
description = "Offline page template shown when site is in maintenance mode"
required = true
always_overwrite = false
},
{
name = "component.php"
description = "Component-only template (print view / raw output)"
required = false
always_overwrite = false
},
]
},
{
name = "src/html"
description = "Template overrides for Joomla component/module views"
required = true
files = [
{
name = "index.html"
description = "Prevents directory listing"
required = true
always_overwrite = true
stub_content = "<!DOCTYPE html><title></title>"
},
]
},
{
name = "src/css"
description = "Compiled CSS files"
required = true
files = []
},
{
name = "src/js"
description = "JavaScript files"
required = true
files = []
},
{
name = "src/images"
description = "Template images and icons"
required = true
files = [
{
name = "template_preview.png"
description = "Template preview screenshot shown in Joomla admin"
required = false
always_overwrite = false
},
{
name = "template_thumbnail.png"
description = "Template thumbnail shown in template list"
required = false
always_overwrite = false
},
]
},
{
name = "src/language/en-GB"
description = "English language files for the template"
required = true
files = [
{
name = "tpl_{{TEMPLATE_SHORT_NAME}}.ini"
description = "Main language strings"
required = true
always_overwrite = false
},
{
name = "tpl_{{TEMPLATE_SHORT_NAME}}.sys.ini"
description = "System language strings (used in admin template manager)"
required = true
always_overwrite = false
},
]
},
{
name = "media"
description = "Template media files — maps to media/templates/site/{name}/ on the Joomla server"
required = true
files = []
},
{
name = "media/css"
description = "Compiled CSS assets served from /media/"
required = true
files = []
},
{
name = "media/js"
description = "JavaScript assets served from /media/"
required = true
files = []
},
{
name = "media/images"
description = "Image assets served from /media/"
required = true
files = []
},
{
name = "media/scss"
description = "SCSS source files (compiled to media/css/)"
required = false
files = []
},
]
}
}
@@ -4,7 +4,6 @@
*
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
* Version: 04.05.00
* Schema Version: 1.0
*/
@@ -13,11 +12,11 @@ locals {
metadata = {
name = "MokoWaaS Component"
description = "Standard repository structure for MokoWaaS (Joomla) components"
repository_type = "waas-component"
repository_type = "joomla"
platform = "mokowaas"
last_updated = "2026-01-15T00:00:00Z"
maintainer = "Moko Consulting"
version = "04.05.00"
version = "05.00.00"
schema_version = "1.0"
}
@@ -83,13 +82,13 @@ locals {
audience = "contributor"
},
{
name = "update.xml"
name = "updates.xml"
extension = "xml"
description = "Joomla extension update server manifest — lists releases for Joomla auto-update; must be kept in sync with manifest.xml version"
description = "Joomla extension update server manifest — lists releases for Joomla auto-update; managed by release workflow, never overwritten by sync"
required = true
always_overwrite = false
protected = true
audience = "developer"
template = "templates/joomla/update.xml.template"
stub_content = <<-MOKO_END
<!--
Joomla Extension Update Server XML
@@ -101,10 +100,10 @@ locals {
The manifest.xml in this repository must reference this file:
<updateservers>
<server type="extension" priority="1" name="{{EXTENSION_NAME}}">
https://git.mokoconsulting.tech/mokoconsulting-tech/{{REPO_NAME}}/raw/branch/main/update.xml
https://git.mokoconsulting.tech/mokoconsulting-tech/{{REPO_NAME}}/raw/branch/main/updates.xml
</server>
<server type="extension" priority="2" name="{{EXTENSION_NAME}}">
https://raw.githubusercontent.com/mokoconsulting-tech/{{REPO_NAME}}/main/update.xml
https://git.mokoconsulting.tech/MokoConsulting/{{REPO_NAME}}/raw/branch/main/updates.xml
</server>
</updateservers>
@@ -123,7 +122,7 @@ locals {
https://git.mokoconsulting.tech/mokoconsulting-tech/{{REPO_NAME}}/releases/download/v{{VERSION}}/{{EXTENSION_ELEMENT}}.zip
</downloadurl>
<downloadurl type="full" format="zip">
https://github.com/mokoconsulting-tech/{{REPO_NAME}}/releases/download/v{{VERSION}}/{{EXTENSION_ELEMENT}}.zip
https://git.mokoconsulting.tech/MokoConsulting/{{REPO_NAME}}/releases/download/v{{VERSION}}/{{EXTENSION_ELEMENT}}.zip
</downloadurl>
</downloads>
<targetplatform name="joomla" version="[56].*"/>
@@ -221,13 +220,14 @@ locals {
template = "templates/configs/composer.joomla.json"
},
{
name = ".mokostandards"
extension = "yml"
description = "MokoStandards governance attachment — links this repo back to the standards source"
name = ".gitea/.mokostandards"
extension = "xml"
description = "MokoStandards XML manifest — generated programmatically by RepositorySynchronizer::migrateMokoStandards()"
required = true
always_overwrite = true
always_overwrite = false
audience = "developer"
template = "templates/configs/mokostandards.yml.template"
template = "managed-by-sync"
source_type = "programmatic"
},
{
name = "GOVERNANCE.md"
@@ -238,6 +238,15 @@ locals {
protected = true
audience = "all"
template = "templates/docs/required/GOVERNANCE.md"
},
{
name = "renovate.json"
extension = "json"
description = "Renovate dependency management configuration"
required = true
always_overwrite = false
audience = "developer"
template = "templates/configs/renovate.json"
}
]
@@ -377,7 +386,7 @@ locals {
{
name = "update-server.md"
extension = "md"
description = "Joomla update server (update.xml) documentation"
description = "Joomla update server (updates.xml) documentation"
required = true
always_overwrite = true
template = "templates/docs/required/template-update-server-joomla.md"
@@ -425,7 +434,7 @@ locals {
{
name = ".github"
path = ".github"
description = "GitHub-specific configuration"
description = "Gitea/GitHub Actions configuration (Gitea reads .github/workflows natively)"
requirement_status = "suggested"
purpose = "Contains GitHub Actions workflows and configuration"
files = [
@@ -467,7 +476,7 @@ locals {
> | Placeholder | Where to find the value |
> |---|---|
> | `{{REPO_NAME}}` | The GitHub repository name (visible in the URL, `README.md` heading, or `git remote -v`) |
> | `{{REPO_URL}}` | Full GitHub URL, e.g. `https://github.com/mokoconsulting-tech/<repo-name>` |
> | `{{REPO_URL}}` | Full Gitea URL, e.g. `https://git.mokoconsulting.tech/MokoConsulting/<repo-name>` |
> | `{{EXTENSION_NAME}}` | The `<name>` element in `manifest.xml` at the repository root |
> | `{{EXTENSION_TYPE}}` | The `type` attribute of the `<extension>` tag in `manifest.xml` (`component`, `module`, `plugin`, or `template`) |
> | `{{EXTENSION_ELEMENT}}` | The `<element>` tag in `manifest.xml`, or the filename prefix (e.g. `com_myextension`, `mod_mymodule`) |
@@ -478,7 +487,7 @@ locals {
## What This Repo Is
This is a **Moko Consulting MokoWaaS** (Joomla) repository governed by [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards). All coding standards, workflows, and policies are defined there and enforced here via bulk sync.
This is a **Moko Consulting MokoWaaS** (Joomla) repository governed by [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards). All coding standards, workflows, and policies are defined there and enforced here via bulk sync.
Repository URL: {{REPO_URL}}
Extension name: **{{EXTENSION_NAME}}**
@@ -552,13 +561,13 @@ locals {
### Joomla Version Alignment
The version in `README.md` **must always match** the `<version>` tag in `manifest.xml` and the latest entry in `update.xml`. The `make release` command / release workflow updates all three automatically.
The version in `README.md` **must always match** the `<version>` tag in `manifest.xml` and the latest entry in `updates.xml`. The `make release` command / release workflow updates all three automatically.
```xml
<!-- In manifest.xml must match README.md version -->
<version>01.02.04</version>
<!-- In update.xml prepend a new <update> block for every release.
<!-- In updates.xml prepend a new <update> block for every release.
The version="[56].*" regex matches Joomla 5.x and 6.x. -->
<updates>
<update>
@@ -582,7 +591,7 @@ locals {
```
{{REPO_NAME}}/
manifest.xml # Joomla installer manifest (root required)
update.xml # Update server manifest (root required, see below)
updates.xml # Update server manifest (root required, see below)
site/ # Frontend (site) code
controller.php
controllers/
@@ -611,22 +620,22 @@ locals {
---
## update.xml Required in Repo Root
## updates.xml Required in Repo Root
`update.xml` **must exist at the repository root**. It is the Joomla update server manifest that allows Joomla installations to check for new versions of this extension.
`updates.xml` **must exist at the repository root**. It is the Joomla update server manifest that allows Joomla installations to check for new versions of this extension.
The `manifest.xml` must reference it via:
```xml
<updateservers>
<server type="extension" priority="1" name="{{EXTENSION_NAME}}">
{{REPO_URL}}/raw/main/update.xml
{{REPO_URL}}/raw/main/updates.xml
</server>
</updateservers>
```
**Rules:**
- Every release must prepend a new `<update>` block at the top of `update.xml` old entries must be preserved below.
- The `<version>` in `update.xml` must exactly match `<version>` in `manifest.xml` and the version in `README.md`.
- Every release must prepend a new `<update>` block at the top of `updates.xml` old entries must be preserved below.
- The `<version>` in `updates.xml` must exactly match `<version>` in `manifest.xml` and the version in `README.md`.
- The `<downloadurl>` must be a publicly accessible direct download link (GitHub Releases asset URL).
- `<targetplatform name="joomla" version="[56].*">` Joomla treats the version value as a regex; `[56].*` matches Joomla 5.x and 6.x.
@@ -635,8 +644,8 @@ locals {
## manifest.xml Rules
- Lives at the repo root as `manifest.xml` (not inside `site/` or `admin/`).
- `<version>` tag must be kept in sync with `README.md` version and `update.xml`.
- Must include `<updateservers>` block pointing to this repo's `update.xml`.
- `<version>` tag must be kept in sync with `README.md` version and `updates.xml`.
- Must include `<updateservers>` block pointing to this repo's `updates.xml`.
- Must include `<files folder="site">` and `<administration>` sections.
- Joomla 4.x requires `<namespace path="src">Moko\{{EXTENSION_NAME}}</namespace>` for namespaced extensions.
@@ -644,16 +653,16 @@ locals {
## GitHub Actions Token Usage
Every workflow must use **`secrets.GH_TOKEN`** (the org-level Personal Access Token).
Every workflow must use **`secrets.GA_TOKEN`** (the Gitea API token). Use `secrets.GH_TOKEN` only for GitHub mirror operations (stable/RC releases).
```yaml
# Correct
- uses: actions/checkout@v4
with:
token: ${{ secrets.GH_TOKEN }}
token: ${{ secrets.GA_TOKEN }}
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
GA_TOKEN: ${{ secrets.GA_TOKEN }}
```
```yaml
@@ -666,16 +675,16 @@ locals {
## MokoStandards Reference
This repository is governed by [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards). Authoritative policies:
This repository is governed by [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards). Authoritative policies:
| Document | Purpose |
|----------|---------|
| [file-header-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/file-header-standards.md) | Copyright-header rules for every file type |
| [coding-style-guide.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/coding-style-guide.md) | Naming and formatting conventions |
| [branching-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/branching-strategy.md) | Branch naming, hierarchy, and release workflow |
| [merge-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/merge-strategy.md) | Squash-merge policy and PR title/body conventions |
| [changelog-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/changelog-standards.md) | How and when to update CHANGELOG.md |
| [joomla-development-guide.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/guide/waas/joomla-development-guide.md) | MokoWaaS Joomla extension development guide |
| [file-header-standards.md](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards/blob/main/docs/policy/file-header-standards.md) | Copyright-header rules for every file type |
| [coding-style-guide.md](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards/blob/main/docs/policy/coding-style-guide.md) | Naming and formatting conventions |
| [branching-strategy.md](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards/blob/main/docs/policy/branching-strategy.md) | Branch naming, hierarchy, and release workflow |
| [merge-strategy.md](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards/blob/main/docs/policy/merge-strategy.md) | Squash-merge policy and PR title/body conventions |
| [changelog-standards.md](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards/blob/main/docs/policy/changelog-standards.md) | How and when to update CHANGELOG.md |
| [joomla-development-guide.md](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards/blob/main/docs/guide/waas/joomla-development-guide.md) | MokoWaaS Joomla extension development guide |
---
@@ -714,8 +723,8 @@ locals {
| Change type | Documentation to update |
|-------------|------------------------|
| New or renamed PHP class/method | PHPDoc block; `docs/api/` entry |
| New or changed manifest.xml | Update `update.xml` version; bump README.md version |
| New release | Prepend `<update>` block to `update.xml`; update CHANGELOG.md; bump README.md version |
| New or changed manifest.xml | Update `updates.xml` version; bump README.md version |
| New release | Prepend `<update>` block to `updates.xml`; update CHANGELOG.md; bump README.md version |
| New or changed workflow | `docs/workflows/<workflow-name>.md` |
| Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block |
| **Every PR** | **Bump the patch version** increment `XX.YY.ZZ` in `README.md`; `sync-version-on-merge` propagates it |
@@ -728,8 +737,8 @@ locals {
- Never skip the FILE INFORMATION block on a new file
- Never add `defined('_JEXEC') or die;` to CLI scripts or model tests only to web-accessible PHP files
- Never hardcode version numbers in body text update `README.md` and let automation propagate
- Never use `github.token` or `secrets.GITHUB_TOKEN` in workflows always use `secrets.GH_TOKEN`
- Never let `manifest.xml` version, `update.xml` version, and `README.md` version go out of sync
- Use `secrets.GA_TOKEN` for Gitea operations. Use `secrets.GH_TOKEN` only for GitHub mirror (stable/RC). Never use `secrets.GITHUB_TOKEN` directly
- Never let `manifest.xml` version, `updates.xml` version, and `README.md` version go out of sync
MOKO_END
},
{
@@ -762,7 +771,7 @@ locals {
> | Placeholder | Where to find the value |
> |---|---|
> | `{{REPO_NAME}}` | The GitHub repository name (visible in the URL, `README.md` heading, or `git remote -v`) |
> | `{{REPO_URL}}` | Full GitHub URL, e.g. `https://github.com/mokoconsulting-tech/<repo-name>` |
> | `{{REPO_URL}}` | Full Gitea URL, e.g. `https://git.mokoconsulting.tech/MokoConsulting/<repo-name>` |
> | `{{REPO_DESCRIPTION}}` | First paragraph of `README.md` body, or the GitHub repo description |
> | `{{EXTENSION_NAME}}` | The `<name>` element in `manifest.xml` at the repository root |
> | `{{EXTENSION_TYPE}}` | The `type` attribute of the `<extension>` tag in `manifest.xml` (`component`, `module`, `plugin`, or `template`) |
@@ -780,7 +789,7 @@ locals {
Extension type: **{{EXTENSION_TYPE}}** (`{{EXTENSION_ELEMENT}}`)
Repository URL: {{REPO_URL}}
This repository is governed by [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards) the single source of truth for coding standards, file-header policies, GitHub Actions workflows, and Terraform configuration templates across all Moko Consulting repositories.
This repository is governed by [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards) the single source of truth for coding standards, file-header policies, GitHub Actions workflows, and Terraform configuration templates across all Moko Consulting repositories.
---
@@ -789,7 +798,7 @@ locals {
```
{{REPO_NAME}}/
manifest.xml # Joomla installer manifest (root required)
update.xml # Update server manifest (root required)
updates.xml # Update server manifest (root required)
site/ # Frontend (site) code
controller.php
controllers/
@@ -839,32 +848,32 @@ locals {
|------|------------------------|
| `README.md` | `FILE INFORMATION` block + badge |
| `manifest.xml` | `<version>` tag |
| `update.xml` | `<version>` in the most recent `<update>` block |
| `updates.xml` | `<version>` in the most recent `<update>` block |
The `make release` command / release workflow syncs all three automatically.
---
# update.xml Required in Repo Root
# updates.xml Required in Repo Root
`update.xml` is the Joomla update server manifest. It allows Joomla installations to check for new versions of this extension via:
`updates.xml` is the Joomla update server manifest. It allows Joomla installations to check for new versions of this extension via:
```xml
<!-- In manifest.xml -->
<updateservers>
<server type="extension" priority="1" name="{{EXTENSION_NAME}}">
{{REPO_URL}}/raw/main/update.xml
{{REPO_URL}}/raw/main/updates.xml
</server>
</updateservers>
```
**Rules:**
- Every release prepends a new `<update>` block at the top older entries are preserved.
- `<version>` in `update.xml` must exactly match `<version>` in `manifest.xml` and `README.md`.
- `<version>` in `updates.xml` must exactly match `<version>` in `manifest.xml` and `README.md`.
- `<downloadurl>` must be a publicly accessible GitHub Releases asset URL.
- `<targetplatform version="[56].*">` Joomla treats the version value as a regex; `[56].*` matches Joomla 5.x and 6.x.
Example `update.xml` entry for a new release:
Example `updates.xml` entry for a new release:
```xml
<updates>
<update>
@@ -948,16 +957,16 @@ locals {
# GitHub Actions Token Usage
Every workflow must use **`secrets.GH_TOKEN`** (the org-level Personal Access Token).
Every workflow must use **`secrets.GA_TOKEN`** (the Gitea API token). Use `secrets.GH_TOKEN` only for GitHub mirror operations (stable/RC releases).
```yaml
# Correct
- uses: actions/checkout@v4
with:
token: ${{ secrets.GH_TOKEN }}
token: ${{ secrets.GA_TOKEN }}
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
GA_TOKEN: ${{ secrets.GA_TOKEN }}
```
```yaml
@@ -973,8 +982,8 @@ locals {
| Change type | Documentation to update |
|-------------|------------------------|
| New or renamed PHP class/method | PHPDoc block; `docs/api/` entry |
| New or changed `manifest.xml` | Sync version to `update.xml` and `README.md` |
| New release | Prepend `<update>` to `update.xml`; update `CHANGELOG.md`; bump `README.md` |
| New or changed `manifest.xml` | Sync version to `updates.xml` and `README.md` |
| New release | Prepend `<update>` to `updates.xml`; update `CHANGELOG.md`; bump `README.md` |
| New or changed workflow | `docs/workflows/<workflow-name>.md` |
| Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block |
| **Every PR** | **Bump the patch version** increment `XX.YY.ZZ` in `README.md`; `sync-version-on-merge` propagates it |
@@ -985,7 +994,7 @@ locals {
- **Never commit directly to `main`** all changes go through a PR.
- **Never hardcode version numbers** in body text update `README.md` and let automation propagate.
- **Never let `manifest.xml`, `update.xml`, and `README.md` versions diverge.**
- **Never let `manifest.xml`, `updates.xml`, and `README.md` versions diverge.**
- **Never skip the FILE INFORMATION block** on a new source file.
- **Never use bare `catch (\Throwable $e) {}`** always log or re-throw.
- **Never mix tabs and spaces** within a file follow `.editorconfig`.
@@ -999,7 +1008,7 @@ locals {
Before opening a PR, verify:
- [ ] Patch version bumped in `README.md` (e.g. `01.02.03` `01.02.04`)
- [ ] If this is a release: `manifest.xml` version updated; `update.xml` updated with new entry
- [ ] If this is a release: `manifest.xml` version updated; `updates.xml` updated with new entry
- [ ] FILE INFORMATION headers updated in modified files
- [ ] CHANGELOG.md updated
- [ ] Tests pass
@@ -1010,117 +1019,125 @@ locals {
| Document | Purpose |
|----------|---------|
| [file-header-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/file-header-standards.md) | Copyright-header rules for every file type |
| [coding-style-guide.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/coding-style-guide.md) | Naming and formatting conventions |
| [branching-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/branching-strategy.md) | Branch naming, hierarchy, and release workflow |
| [merge-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/merge-strategy.md) | Squash-merge policy and PR conventions |
| [changelog-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/changelog-standards.md) | How and when to update CHANGELOG.md |
| [joomla-development-guide.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/guide/waas/joomla-development-guide.md) | MokoWaaS Joomla extension development guide |
| [file-header-standards.md](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards/blob/main/docs/policy/file-header-standards.md) | Copyright-header rules for every file type |
| [coding-style-guide.md](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards/blob/main/docs/policy/coding-style-guide.md) | Naming and formatting conventions |
| [branching-strategy.md](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards/blob/main/docs/policy/branching-strategy.md) | Branch naming, hierarchy, and release workflow |
| [merge-strategy.md](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards/blob/main/docs/policy/merge-strategy.md) | Squash-merge policy and PR conventions |
| [changelog-standards.md](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards/blob/main/docs/policy/changelog-standards.md) | How and when to update CHANGELOG.md |
| [joomla-development-guide.md](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards/blob/main/docs/guide/waas/joomla-development-guide.md) | MokoWaaS Joomla extension development guide |
MOKO_END
}
]
subdirectories = [
{
name = "workflows"
path = ".github/workflows"
description = "GitHub Actions workflows"
path = ".gitea/workflows"
description = "Gitea Actions CI/CD workflows"
requirement_status = "required"
files = [
{
name = "ci-joomla.yml"
extension = "yml"
description = "Joomla-specific CI workflow"
requirement_status = "required"
always_overwrite = true
template = "templates/workflows/joomla/ci-joomla.yml.template"
},
{
name = "codeql-analysis.yml"
extension = "yml"
description = "CodeQL security analysis workflow"
requirement_status = "required"
always_overwrite = true
template = "templates/workflows/generic/codeql-analysis.yml.template"
},
{
name = "standards-compliance.yml"
extension = "yml"
description = "MokoStandards compliance validation"
requirement_status = "required"
always_overwrite = true
template = ".github/workflows/standards-compliance.yml"
},
{
name = "enterprise-firewall-setup.yml"
extension = "yml"
description = "Enterprise firewall configuration for trusted domain access"
requirement_status = "required"
always_overwrite = true
template = "templates/workflows/shared/enterprise-firewall-setup.yml.template"
},
{
name = "deploy-dev.yml"
extension = "yml"
description = "SFTP deployment of src/ to the development server"
requirement_status = "required"
always_overwrite = true
template = "templates/workflows/shared/deploy-dev.yml.template"
},
{
name = "deploy-demo.yml"
extension = "yml"
description = "SFTP deployment of src/ to the demo server on merge to main"
requirement_status = "required"
always_overwrite = true
template = "templates/workflows/shared/deploy-demo.yml.template"
},
{
name = "deploy-rs.yml"
extension = "yml"
description = "SFTP deployment of src/ to the release staging server on merge to main"
requirement_status = "required"
always_overwrite = true
template = "templates/workflows/shared/deploy-rs.yml.template"
},
{
name = "sync-version-on-merge.yml"
extension = "yml"
description = "Auto-bump patch version on merge and propagate to all file headers"
requirement_status = "required"
always_overwrite = true
template = "templates/workflows/shared/sync-version-on-merge.yml.template"
},
{
name = "auto-release.yml"
extension = "yml"
description = "Auto-create GitHub Release on push to main with version from README.md"
description = "Automated release — builds zip, creates Gitea release, updates SHA in updates.xml. Triggered by push to main (stable) or pre-release tags"
requirement_status = "required"
always_overwrite = true
template = "templates/workflows/shared/auto-release.yml.template"
template = "workflows/auto-release.yml"
},
{
name = "repository-cleanup.yml"
name = "ci-dolibarr.yml"
extension = "yml"
description = "Scheduled cleanup: delete retired workflows, stale branches, old workflow runs"
description = "Continuous integration — PHP linting, PHPStan static analysis, Dolibarr module validation"
requirement_status = "required"
always_overwrite = true
template = "templates/workflows/shared/repository-cleanup.yml.template"
template = "workflows/ci-dolibarr.yml"
},
{
name = "auto-dev-issue.yml"
name = "publish-to-mokodolimods.yml"
extension = "yml"
description = "Auto-create tracking issue when a dev/** branch is pushed"
description = "On release, copies src/ into htdocs/custom/ in mokodolimods repo and opens a PR"
requirement_status = "required"
always_overwrite = true
template = "templates/workflows/shared/auto-dev-issue.yml.template"
template = "workflows/publish-to-mokodolimods.yml"
},
{
name = "repo_health.yml"
name = "pre-release.yml"
extension = "yml"
description = "Joomla-specific repository health check workflow"
description = "Manual pre-release — builds dev/alpha/beta/rc packages with patch version bump"
requirement_status = "required"
always_overwrite = true
template = "templates/workflows/joomla/repo_health.yml.template"
template = "workflows/pre-release.yml"
},
{
name = "deploy-manual.yml"
extension = "yml"
description = "Manual deployment — allows selecting target environment and branch for on-demand deploys"
requirement_status = "required"
always_overwrite = true
template = "workflows/deploy-manual.yml"
},
{
name = "repo-health.yml"
extension = "yml"
description = "Repository health checks — validates required files, structure compliance, and standards alignment"
requirement_status = "required"
always_overwrite = true
template = "workflows/repo-health.yml"
},
{
name = "update-server.yml"
extension = "yml"
description = "Update server maintenance — validates updates.xml format and ensures download URLs are reachable"
requirement_status = "required"
always_overwrite = true
template = "workflows/update-server.yml"
},
{
name = "pr-check.yml"
extension = "yml"
description = "PR gate — validates PHP syntax, manifest XML, and package build before merge to main"
requirement_status = "required"
always_overwrite = true
template = "workflows/pr-check.yml"
},
{
name = "security-audit.yml"
extension = "yml"
description = "Dependency vulnerability scanning — weekly schedule and on PR when lock files change"
requirement_status = "required"
always_overwrite = true
template = "workflows/security-audit.yml"
},
{
name = "notify.yml"
extension = "yml"
description = "Push notifications via ntfy on release success or workflow failure"
requirement_status = "required"
always_overwrite = true
template = "workflows/notify.yml"
},
{
name = "cleanup.yml"
extension = "yml"
description = "Scheduled cleanup — delete merged branches and old workflow runs weekly"
requirement_status = "required"
always_overwrite = true
template = "workflows/cleanup.yml"
},
{
name = "cascade-dev.yml"
extension = "yml"
description = "Forward-merge main to all open branches (dev, rc/*, beta/*, alpha/*) on push to main"
requirement_status = "required"
always_overwrite = true
template = "workflows/cascade-dev.yml"
},
{
name = "gitleaks.yml"
extension = "yml"
description = "Secret scanning — detect leaked credentials, API keys, and tokens using Gitleaks"
requirement_status = "required"
always_overwrite = true
template = "workflows/gitleaks.yml"
}
]
},
+484
View File
@@ -0,0 +1,484 @@
/**
* MCP Server Repository Structure Definition
* Standard repository structure for Model Context Protocol (MCP) server projects
*
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
* Schema Version: 1.0
*/
locals {
repository_structure = {
metadata = {
name = "MCP Server"
description = "Standard repository structure for Model Context Protocol (MCP) server projects — TypeScript/Node.js MCP servers that expose external APIs as AI assistant tools"
repository_type = "mcp-server"
platform = "mcp-server"
last_updated = "2026-05-07T00:00:00Z"
maintainer = "Moko Consulting"
version = "04.06.00"
schema_version = "1.0"
}
root_files = [
{
name = "README.md"
extension = "md"
description = "Project overview with tool reference table, install, and configuration"
requirement_status = "required"
always_overwrite = false
protected = true
audience = "general"
source_path = "templates/docs/required"
source_filename = "template-README.md"
source_type = "template"
destination_path = "."
destination_filename = "README.md"
create_path = false
template = "templates/docs/required/template-README.md"
},
{
name = "LICENSE"
extension = ""
description = "License file (GPL-3.0-or-later)"
requirement_status = "required"
audience = "general"
source_path = "templates/licenses"
source_filename = "GPL-3.0"
source_type = "template"
destination_path = "."
destination_filename = "LICENSE"
create_path = false
template = "templates/licenses/GPL-3.0"
},
{
name = "CHANGELOG.md"
extension = "md"
description = "Version history and changes"
requirement_status = "required"
always_overwrite = false
protected = true
audience = "general"
source_path = "templates/docs/required"
source_filename = "template-CHANGELOG.md"
source_type = "template"
destination_path = "."
destination_filename = "CHANGELOG.md"
create_path = false
template = "templates/docs/required/template-CHANGELOG.md"
},
{
name = "CONTRIBUTING.md"
extension = "md"
description = "Contribution guidelines"
requirement_status = "required"
always_overwrite = false
protected = true
audience = "contributor"
source_path = "templates/docs/required"
source_filename = "template-CONTRIBUTING.md"
source_type = "template"
destination_path = "."
destination_filename = "CONTRIBUTING.md"
create_path = false
template = "templates/docs/required/template-CONTRIBUTING.md"
},
{
name = "SECURITY.md"
extension = "md"
description = "Security policy and vulnerability reporting"
requirement_status = "required"
always_overwrite = false
protected = true
audience = "general"
source_path = "templates/docs/required"
source_filename = "template-SECURITY.md"
source_type = "template"
destination_path = "."
destination_filename = "SECURITY.md"
create_path = false
template = "templates/docs/required/template-SECURITY.md"
},
{
name = "CODE_OF_CONDUCT.md"
extension = "md"
description = "Community code of conduct"
requirement_status = "required"
always_overwrite = false
protected = true
audience = "contributor"
source_path = "templates/docs/extra"
source_filename = "template-CODE_OF_CONDUCT.md"
source_type = "template"
destination_path = "."
destination_filename = "CODE_OF_CONDUCT.md"
create_path = false
template = "templates/docs/extra/template-CODE_OF_CONDUCT.md"
},
{
name = "package.json"
extension = "json"
description = "Node.js project manifest — @mokoconsulting scoped, MCP SDK + Zod dependencies"
requirement_status = "required"
always_overwrite = false
protected = true
audience = "developer"
},
{
name = "tsconfig.json"
extension = "json"
description = "TypeScript configuration — ES2022 target, Node16 module, strict mode"
requirement_status = "required"
always_overwrite = false
audience = "developer"
},
{
name = "config.example.json"
extension = "json"
description = "Example multi-connection configuration file"
requirement_status = "required"
always_overwrite = false
protected = true
audience = "general"
},
{
name = ".gitignore"
extension = "gitignore"
description = "Git ignore patterns"
requirement_status = "required"
always_overwrite = false
audience = "developer"
},
{
name = ".gitattributes"
extension = "gitattributes"
description = "Git attributes configuration"
requirement_status = "required"
audience = "developer"
},
{
name = ".gitmessage"
extension = "gitmessage"
description = "Conventional commit message template"
requirement_status = "required"
always_overwrite = true
audience = "developer"
},
{
name = "Makefile"
description = "Build automation — install, build, dev, clean, setup, start targets"
requirement_status = "required"
always_overwrite = false
audience = "developer"
}
]
directories = [
{
name = "src"
path = "src"
description = "TypeScript source code"
requirement_status = "required"
purpose = "Contains MCP server entry point, API client, config loader, and type definitions"
files = [
{
name = "index.ts"
extension = "ts"
description = "MCP server entry point — registers all API tools with McpServer"
requirement_status = "required"
},
{
name = "client.ts"
extension = "ts"
description = "HTTP client wrapper for the target API (GET/POST/PUT/DELETE)"
requirement_status = "required"
},
{
name = "config.ts"
extension = "ts"
description = "Configuration loader — reads ~/.{project}.json with multi-connection support"
requirement_status = "required"
},
{
name = "types.ts"
extension = "ts"
description = "TypeScript interfaces for connection, config, and API response types"
requirement_status = "required"
}
]
},
{
name = "scripts"
path = "scripts"
description = "Setup and utility scripts"
requirement_status = "required"
purpose = "Contains interactive setup wizard and repo-specific helpers"
files = [
{
name = "setup.mjs"
extension = "mjs"
description = "Interactive setup wizard — prompts for API connection details and writes config"
requirement_status = "required"
always_overwrite = false
protected = true
}
]
},
{
name = "docs"
path = "docs"
description = "Documentation directory"
requirement_status = "required"
purpose = "Contains project documentation"
files = [
{
name = "index.md"
extension = "md"
description = "Documentation index"
requirement_status = "suggested"
}
]
},
{
name = ".gitea"
path = ".gitea"
description = "Gitea-specific configuration"
requirement_status = "required"
purpose = "Contains Gitea Actions workflows and platform configuration"
files = [
{
name = ".mokostandards"
description = "MokoStandards platform declaration — must contain 'platform: mcp-server'"
requirement_status = "required"
always_overwrite = false
}
]
subdirectories = [
{
name = "workflows"
path = ".gitea/workflows"
description = "Gitea Actions workflows"
requirement_status = "required"
files = [
{
name = "auto-release.yml"
extension = "yml"
description = "Auto-create release on push to main"
requirement_status = "required"
always_overwrite = true
template = "templates/workflows/shared/auto-release.yml.template"
},
{
name = "auto-dev-issue.yml"
extension = "yml"
description = "Auto-create tracking issue when a dev/** branch is pushed"
requirement_status = "required"
always_overwrite = true
template = "templates/workflows/shared/auto-dev-issue.yml.template"
},
{
name = "auto-assign.yml"
extension = "yml"
description = "Auto-assign issues and PRs"
requirement_status = "required"
always_overwrite = true
template = "templates/workflows/shared/auto-assign.yml.template"
},
{
name = "standards-compliance.yml"
extension = "yml"
description = "MokoStandards compliance validation"
requirement_status = "required"
always_overwrite = true
template = "templates/workflows/shared/standards-compliance.yml.template"
},
{
name = "codeql-analysis.yml"
extension = "yml"
description = "CodeQL security analysis"
requirement_status = "required"
always_overwrite = true
template = "templates/workflows/shared/codeql-analysis.yml.template"
},
{
name = "changelog-validation.yml"
extension = "yml"
description = "CHANGELOG validation on PR"
requirement_status = "required"
always_overwrite = true
template = "templates/workflows/shared/changelog-validation.yml.template"
},
{
name = "sync-version-on-merge.yml"
extension = "yml"
description = "Auto-bump patch version on merge"
requirement_status = "required"
always_overwrite = true
template = "templates/workflows/shared/sync-version-on-merge.yml.template"
},
{
name = "repository-cleanup.yml"
extension = "yml"
description = "Scheduled cleanup of stale branches and workflow runs"
requirement_status = "required"
always_overwrite = true
template = "templates/workflows/shared/repository-cleanup.yml.template"
},
{
name = "enterprise-firewall-setup.yml"
extension = "yml"
description = "Enterprise firewall configuration for trusted domain access"
requirement_status = "required"
always_overwrite = true
template = "templates/workflows/shared/enterprise-firewall-setup.yml.template"
},
{
name = "deploy-dev.yml"
extension = "yml"
description = "Deployment to development server"
requirement_status = "suggested"
always_overwrite = true
template = "templates/workflows/shared/deploy-dev.yml.template"
},
{
name = "deploy-demo.yml"
extension = "yml"
description = "Deployment to demo server on merge to main"
requirement_status = "suggested"
always_overwrite = true
template = "templates/workflows/shared/deploy-demo.yml.template"
},
{
name = "copilot-agent.yml"
extension = "yml"
description = "Copilot agent workflow for automated code review"
requirement_status = "optional"
always_overwrite = true
template = "templates/workflows/shared/copilot-agent.yml.template"
},
{
name = "mcp-build-test.yml"
extension = "yml"
description = "MCP server build validation — TypeScript compile, dist verification, tool count"
requirement_status = "required"
always_overwrite = true
template = "templates/workflows/mcp/mcp-build-test.yml.template"
},
{
name = "mcp-sdk-check.yml"
extension = "yml"
description = "Weekly check for MCP SDK and Zod updates — creates issue when new version available"
requirement_status = "required"
always_overwrite = true
template = "templates/workflows/mcp/mcp-sdk-check.yml.template"
},
{
name = "mcp-tool-inventory.yml"
extension = "yml"
description = "Generate tool inventory report on push to main"
requirement_status = "suggested"
always_overwrite = true
template = "templates/workflows/mcp/mcp-tool-inventory.yml.template"
}
]
}
]
},
{
name = "dist"
path = "dist"
description = "Compiled JavaScript output (generated)"
requirement_status = "not-allowed"
purpose = "Generated directory that should not be committed"
},
{
name = "node_modules"
path = "node_modules"
description = "Node.js dependencies (generated)"
requirement_status = "not-allowed"
purpose = "Generated directory that should not be committed"
}
]
repository_requirements = {
secrets = [
{
name = "GH_TOKEN"
description = "Org-level Gitea PAT — configure in org Actions secrets"
required = true
scope = "organisation"
used_in = "Gitea Actions workflows"
}
]
variables = [
{
name = "NODE_VERSION"
description = "Node.js version for CI/CD"
default_value = "20"
required = false
scope = "repository"
}
]
branch_protections = [
{
branch_pattern = "main"
require_pull_request = true
required_approvals = 1
require_code_owner_review = false
dismiss_stale_reviews = true
require_status_checks = true
required_status_checks = ["ci"]
enforce_admins = false
restrict_pushes = true
}
]
repository_settings = {
has_issues = true
has_projects = true
has_wiki = false
has_discussions = false
allow_merge_commit = true
allow_squash_merge = true
allow_rebase_merge = false
delete_branch_on_merge = true
allow_auto_merge = false
}
labels = [
{
name = "bug"
color = "d73a4a"
description = "Something isn't working"
},
{
name = "enhancement"
color = "a2eeef"
description = "New feature or request"
},
{
name = "documentation"
color = "0075ca"
description = "Improvements or additions to documentation"
},
{
name = "security"
color = "ee0701"
description = "Security vulnerability or concern"
},
{
name = "new-tool"
color = "5319e7"
description = "New MCP tool/endpoint to add"
},
{
name = "api-change"
color = "fbca04"
description = "Upstream API changed — tool needs update"
}
]
}
}
}
@@ -2,13 +2,12 @@
* Dolibarr Platform Structure Definition
* Standard repository structure for the full Dolibarr ERP/CRM installation
*
* This is distinct from crm-module it defines the ENTIRE Dolibarr platform
* This is distinct from dolibarr it defines the ENTIRE Dolibarr platform
* (htdocs/, not src/). It does NOT have a module descriptor, numero, or
* publish-to-mokodolimods workflow.
*
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
* Version: 04.05.00
* Schema Version: 1.0
*/
@@ -17,11 +16,11 @@ locals {
metadata = {
name = "Dolibarr Platform"
description = "Full Dolibarr ERP/CRM installation — htdocs/ root, not a module"
repository_type = "crm-platform"
repository_type = "platform"
platform = "dolibarr"
last_updated = "2026-03-31T00:00:00Z"
maintainer = "Moko Consulting"
version = "04.05.00"
version = "05.00.00"
schema_version = "1.0"
}
@@ -84,12 +83,22 @@ locals {
template = "templates/configs/ftp_ignore"
},
{
name = ".mokostandards"
extension = ""
description = "MokoStandards platform identifier"
name = ".gitea/.mokostandards"
extension = "xml"
description = "MokoStandards XML manifest — generated programmatically by RepositorySynchronizer::migrateMokoStandards()"
required = true
always_overwrite = true
template = "templates/configs/mokostandards.yml.template"
always_overwrite = false
template = "managed-by-sync"
source_type = "programmatic"
},
{
name = "renovate.json"
extension = "json"
description = "Renovate dependency management configuration"
required = true
always_overwrite = false
audience = "developer"
template = "templates/configs/renovate.json"
}
]
@@ -218,6 +227,22 @@ locals {
requirement_status = "required"
always_overwrite = true
template = "templates/workflows/dolibarr/repo_health.yml.template"
},
{
name = "cascade-dev.yml"
extension = "yml"
description = "Forward-merge main to all open branches (dev, rc/*, beta/*, alpha/*) on push to main"
requirement_status = "required"
always_overwrite = true
template = "workflows/cascade-dev.yml"
},
{
name = "gitleaks.yml"
extension = "yml"
description = "Secret scanning — detect leaked credentials, API keys, and tokens using Gitleaks"
requirement_status = "required"
always_overwrite = true
template = "workflows/gitleaks.yml"
}
]
},
@@ -4,7 +4,6 @@
*
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
* Version: 04.05.00
* Schema Version: 1.0
*/
@@ -17,7 +16,7 @@ locals {
platform = "standards"
last_updated = "2026-03-03T00:00:00Z"
maintainer = "Moko Consulting"
version = "04.05.00"
version = "05.00.00"
schema_version = "1.0"
}
@@ -199,12 +198,23 @@ locals {
audience = "developer"
},
{
name = ".mokostandards"
extension = ""
description = "MokoStandards sync tracking file — records last sync date, version, and compliance status"
name = ".gitea/.mokostandards"
extension = "xml"
description = "MokoStandards XML manifest — generated programmatically by RepositorySynchronizer::migrateMokoStandards()"
requirement_status = "required"
always_overwrite = true
always_overwrite = false
audience = "developer"
template = "managed-by-sync"
source_type = "programmatic"
},
{
name = "renovate.json"
extension = "json"
description = "Renovate dependency management configuration"
required = true
always_overwrite = false
audience = "developer"
template = "templates/configs/renovate.json"
}
]
@@ -495,6 +505,22 @@ locals {
requirement_status = "required"
always_overwrite = true
template = "templates/workflows/shared/auto-dev-issue.yml.template"
},
{
name = "cascade-dev.yml"
extension = "yml"
description = "Forward-merge main to all open branches (dev, rc/*, beta/*, alpha/*) on push to main"
requirement_status = "required"
always_overwrite = true
template = "workflows/cascade-dev.yml"
},
{
name = "gitleaks.yml"
extension = "yml"
description = "Secret scanning — detect leaked credentials, API keys, and tokens using Gitleaks"
requirement_status = "required"
always_overwrite = true
template = "workflows/gitleaks.yml"
}
]
},
@@ -666,20 +692,52 @@ locals {
}
]
branch_protections = {
main = {
required_status_checks = {
strict = true
contexts = ["standards-compliance", "code-quality"]
}
enforce_admins = false
required_pull_request_reviews = {
dismiss_stale_reviews = true
require_code_owner_reviews = true
required_approving_review_count = 1
}
branch_protections = [
{
branch_pattern = "main"
require_pull_request = true
required_approvals = 0
dismiss_stale_reviews = true
block_on_rejected_reviews = true
restrict_pushes = true
push_whitelist = ["jmiller"]
enable_force_push = true
force_push_whitelist = ["jmiller"]
enforce_admins = false
},
{
branch_pattern = "dev"
require_pull_request = false
required_approvals = 0
restrict_pushes = false
enable_force_push = true
force_push_whitelist = ["jmiller"]
},
{
branch_pattern = "rc/*"
require_pull_request = false
required_approvals = 0
restrict_pushes = false
enable_force_push = true
force_push_whitelist = ["jmiller"]
},
{
branch_pattern = "beta/*"
require_pull_request = false
required_approvals = 0
restrict_pushes = false
enable_force_push = true
force_push_whitelist = ["jmiller"]
},
{
branch_pattern = "alpha/*"
require_pull_request = false
required_approvals = 0
restrict_pushes = false
enable_force_push = true
force_push_whitelist = ["jmiller"]
}
}
]
repository_settings = {
has_issues = true
+11
View File
@@ -1,3 +1,14 @@
<!--
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
SPDX-License-Identifier: GPL-3.0-or-later
FILE INFORMATION
DEFGROUP: MokoStandards.Index
INGROUP: MokoStandards.Definitions
REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
PATH: /definitions/index.md
BRIEF: Definitions directory index
-->
# Docs Index: /api/definitions
## Purpose
+1 -2
View File
@@ -83,7 +83,6 @@ locals {
*
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
* Version: 04.05.00
* Schema Version: 1.0
*/
@@ -96,7 +95,7 @@ locals {
platform = "multi-platform"
last_updated = "2026-01-16T00:00:00Z"
maintainer = "Moko Consulting"
version = "04.05.00"
version = "05.00.00"
schema_version = "1.0"
}
+1 -2
View File
@@ -98,7 +98,6 @@ locals {
*
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
* Version: 04.05.00
* Schema Version: 1.0
*/
@@ -111,7 +110,7 @@ locals {
platform = "multi-platform"
last_updated = "2026-01-16T00:00:00Z"
maintainer = "Moko Consulting"
version = "04.05.00"
version = "05.00.00"
schema_version = "1.0"
}
+1 -2
View File
@@ -99,7 +99,6 @@ locals {
*
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
* Version: 04.05.00
* Schema Version: 1.0
*/
@@ -112,7 +111,7 @@ locals {
platform = "multi-platform"
last_updated = "2026-01-16T00:00:00Z"
maintainer = "Moko Consulting"
version = "04.05.00"
version = "05.00.00"
schema_version = "1.0"
}
+1 -2
View File
@@ -89,7 +89,6 @@ locals {
*
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
* Version: 04.05.00
* Schema Version: 1.0
*/
@@ -102,7 +101,7 @@ locals {
platform = "dolibarr"
last_updated = "2026-01-07T00:00:00Z"
maintainer = "Moko Consulting"
version = "04.05.00"
version = "05.00.00"
schema_version = "1.0"
}
+1 -2
View File
@@ -91,7 +91,6 @@ locals {
*
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
* Version: 04.05.00
* Schema Version: 1.0
*/
@@ -104,7 +103,7 @@ locals {
platform = "dolibarr"
last_updated = "2026-01-07T00:00:00Z"
maintainer = "Moko Consulting"
version = "04.05.00"
version = "05.00.00"
schema_version = "1.0"
}
+30 -31
View File
@@ -34,7 +34,7 @@ locals {
{ path = "SECURITY.md" action = "updated" },
{ path = "CODE_OF_CONDUCT.md" action = "updated" },
{ path = "CONTRIBUTING.md" action = "updated" },
{ path = "update.xml" action = "updated" },
{ path = "updates.xml" action = "updated" },
{ path = "phpstan.neon" action = "updated" },
{ path = "Makefile" action = "updated" },
{ path = ".gitignore" action = "updated" },
@@ -86,7 +86,6 @@ locals {
*
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
* Version: 04.05.00
* Schema Version: 1.0
*/
@@ -99,7 +98,7 @@ locals {
platform = "mokowaas"
last_updated = "2026-01-15T00:00:00Z"
maintainer = "Moko Consulting"
version = "04.05.00"
version = "05.00.00"
schema_version = "1.0"
}
@@ -165,13 +164,13 @@ locals {
audience = "contributor"
},
{
name = "update.xml"
name = "updates.xml"
extension = "xml"
description = "Joomla extension update server manifest — lists releases for Joomla auto-update; must be kept in sync with manifest.xml version"
required = true
always_overwrite = false
audience = "developer"
template = "templates/joomla/update.xml.template"
template = "templates/joomla/updates.xml.template"
stub_content = <<-MOKO_END
<!--
Joomla Extension Update Server XML
@@ -183,7 +182,7 @@ locals {
The manifest.xml in this repository must reference this file:
<updateservers>
<server type="extension" priority="1" name="{{EXTENSION_NAME}}">
{{REPO_URL}}/raw/main/update.xml
{{REPO_URL}}/raw/main/updates.xml
</server>
</updateservers>
@@ -454,7 +453,7 @@ locals {
{
name = "update-server.md"
extension = "md"
description = "Joomla update server (update.xml) documentation"
description = "Joomla update server (updates.xml) documentation"
required = true
always_overwrite = true
template = "templates/docs/required/template-update-server-joomla.md"
@@ -629,13 +628,13 @@ locals {
### Joomla Version Alignment
The version in `README.md` **must always match** the `<version>` tag in `manifest.xml` and the latest entry in `update.xml`. The `make release` command / release workflow updates all three automatically.
The version in `README.md` **must always match** the `<version>` tag in `manifest.xml` and the latest entry in `updates.xml`. The `make release` command / release workflow updates all three automatically.
```xml
<!-- In manifest.xml — must match README.md version -->
<version>01.02.04</version>
<!-- In update.xml — prepend a new <update> block for every release.
<!-- In updates.xml — prepend a new <update> block for every release.
Note: the backslash in version="4\.[0-9]+" is a literal backslash character
in the XML attribute value. Joomla's update server treats the value as a
regular expression, so \. matches a literal dot. -->
@@ -661,7 +660,7 @@ locals {
```
{{REPO_NAME}}/
├── manifest.xml # Joomla installer manifest (root — required)
├── update.xml # Update server manifest (root — required, see below)
├── updates.xml # Update server manifest (root — required, see below)
├── site/ # Frontend (site) code
│ ├── controller.php
│ ├── controllers/
@@ -690,22 +689,22 @@ locals {
---
## update.xml — Required in Repo Root
## updates.xml — Required in Repo Root
`update.xml` **must exist at the repository root**. It is the Joomla update server manifest that allows Joomla installations to check for new versions of this extension.
`updates.xml` **must exist at the repository root**. It is the Joomla update server manifest that allows Joomla installations to check for new versions of this extension.
The `manifest.xml` must reference it via:
```xml
<updateservers>
<server type="extension" priority="1" name="{{EXTENSION_NAME}}">
{{REPO_URL}}/raw/main/update.xml
{{REPO_URL}}/raw/main/updates.xml
</server>
</updateservers>
```
**Rules:**
- Every release must prepend a new `<update>` block at the top of `update.xml` — old entries must be preserved below.
- The `<version>` in `update.xml` must exactly match `<version>` in `manifest.xml` and the version in `README.md`.
- Every release must prepend a new `<update>` block at the top of `updates.xml` — old entries must be preserved below.
- The `<version>` in `updates.xml` must exactly match `<version>` in `manifest.xml` and the version in `README.md`.
- The `<downloadurl>` must be a publicly accessible direct download link (GitHub Releases asset URL).
- `<targetplatform name="joomla" version="4\.[0-9]+">` — the backslash is a **literal backslash character** in the XML attribute value; Joomla's update-server parser treats the value as a regular expression, so `\.` matches a literal dot and `[0-9]+` matches one or more digits. Do not double-escape it.
@@ -714,8 +713,8 @@ locals {
## manifest.xml Rules
- Lives at the repo root as `manifest.xml` (not inside `site/` or `admin/`).
- `<version>` tag must be kept in sync with `README.md` version and `update.xml`.
- Must include `<updateservers>` block pointing to this repo's `update.xml`.
- `<version>` tag must be kept in sync with `README.md` version and `updates.xml`.
- Must include `<updateservers>` block pointing to this repo's `updates.xml`.
- Must include `<files folder="site">` and `<administration>` sections.
- Joomla 4.x requires `<namespace path="src">Moko\{{EXTENSION_NAME}}</namespace>` for namespaced extensions.
@@ -793,8 +792,8 @@ locals {
| Change type | Documentation to update |
|-------------|------------------------|
| New or renamed PHP class/method | PHPDoc block; `docs/api/` entry |
| New or changed manifest.xml | Update `update.xml` version; bump README.md version |
| New release | Prepend `<update>` block to `update.xml`; update CHANGELOG.md; bump README.md version |
| New or changed manifest.xml | Update `updates.xml` version; bump README.md version |
| New release | Prepend `<update>` block to `updates.xml`; update CHANGELOG.md; bump README.md version |
| New or changed workflow | `docs/workflows/<workflow-name>.md` |
| Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block |
| **Every PR** | **Bump the patch version** — increment `XX.YY.ZZ` in `README.md`; `sync-version-on-merge` propagates it |
@@ -808,7 +807,7 @@ locals {
- Never add `defined('_JEXEC') or die;` to CLI scripts or model tests — only to web-accessible PHP files
- Never hardcode version numbers in body text — update `README.md` and let automation propagate
- Never use `github.token` or `secrets.GITHUB_TOKEN` in workflows — always use `secrets.GH_TOKEN`
- Never let `manifest.xml` version, `update.xml` version, and `README.md` version go out of sync
- Never let `manifest.xml` version, `updates.xml` version, and `README.md` version go out of sync
MOKO_END
},
{
@@ -868,7 +867,7 @@ locals {
```
{{REPO_NAME}}/
├── manifest.xml # Joomla installer manifest (root — required)
├── update.xml # Update server manifest (root — required)
├── updates.xml # Update server manifest (root — required)
├── site/ # Frontend (site) code
│ ├── controller.php
│ ├── controllers/
@@ -918,32 +917,32 @@ locals {
|------|------------------------|
| `README.md` | `FILE INFORMATION` block + badge |
| `manifest.xml` | `<version>` tag |
| `update.xml` | `<version>` in the most recent `<update>` block |
| `updates.xml` | `<version>` in the most recent `<update>` block |
The `make release` command / release workflow syncs all three automatically.
---
# update.xml — Required in Repo Root
# updates.xml — Required in Repo Root
`update.xml` is the Joomla update server manifest. It allows Joomla installations to check for new versions of this extension via:
`updates.xml` is the Joomla update server manifest. It allows Joomla installations to check for new versions of this extension via:
```xml
<!-- In manifest.xml -->
<updateservers>
<server type="extension" priority="1" name="{{EXTENSION_NAME}}">
{{REPO_URL}}/raw/main/update.xml
{{REPO_URL}}/raw/main/updates.xml
</server>
</updateservers>
```
**Rules:**
- Every release prepends a new `<update>` block at the top — older entries are preserved.
- `<version>` in `update.xml` must exactly match `<version>` in `manifest.xml` and `README.md`.
- `<version>` in `updates.xml` must exactly match `<version>` in `manifest.xml` and `README.md`.
- `<downloadurl>` must be a publicly accessible GitHub Releases asset URL.
- `<targetplatform version="4\.[0-9]+">` — backslash is literal (Joomla regex syntax).
Example `update.xml` entry for a new release:
Example `updates.xml` entry for a new release:
```xml
<updates>
<update>
@@ -1052,8 +1051,8 @@ locals {
| Change type | Documentation to update |
|-------------|------------------------|
| New or renamed PHP class/method | PHPDoc block; `docs/api/` entry |
| New or changed `manifest.xml` | Sync version to `update.xml` and `README.md` |
| New release | Prepend `<update>` to `update.xml`; update `CHANGELOG.md`; bump `README.md` |
| New or changed `manifest.xml` | Sync version to `updates.xml` and `README.md` |
| New release | Prepend `<update>` to `updates.xml`; update `CHANGELOG.md`; bump `README.md` |
| New or changed workflow | `docs/workflows/<workflow-name>.md` |
| Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block |
| **Every PR** | **Bump the patch version** — increment `XX.YY.ZZ` in `README.md`; `sync-version-on-merge` propagates it |
@@ -1064,7 +1063,7 @@ locals {
- **Never commit directly to `main`** — all changes go through a PR.
- **Never hardcode version numbers** in body text — update `README.md` and let automation propagate.
- **Never let `manifest.xml`, `update.xml`, and `README.md` versions diverge.**
- **Never let `manifest.xml`, `updates.xml`, and `README.md` versions diverge.**
- **Never skip the FILE INFORMATION block** on a new source file.
- **Never use bare `catch (\Throwable $e) {}`** — always log or re-throw.
- **Never mix tabs and spaces** within a file — follow `.editorconfig`.
@@ -1078,7 +1077,7 @@ locals {
Before opening a PR, verify:
- [ ] Patch version bumped in `README.md` (e.g. `01.02.03` → `01.02.04`)
- [ ] If this is a release: `manifest.xml` version updated; `update.xml` updated with new entry
- [ ] If this is a release: `manifest.xml` version updated; `updates.xml` updated with new entry
- [ ] FILE INFORMATION headers updated in modified files
- [ ] CHANGELOG.md updated
- [ ] Tests pass
+1 -2
View File
@@ -90,7 +90,6 @@ locals {
*
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
* Version: 04.05.00
* Schema Version: 1.0
*/
@@ -103,7 +102,7 @@ locals {
platform = "dolibarr"
last_updated = "2026-01-07T00:00:00Z"
maintainer = "Moko Consulting"
version = "04.05.00"
version = "05.00.00"
schema_version = "1.0"
}
+1 -2
View File
@@ -90,7 +90,6 @@ locals {
*
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
* Version: 04.05.00
* Schema Version: 1.0
*/
@@ -103,7 +102,7 @@ locals {
platform = "dolibarr"
last_updated = "2026-01-07T00:00:00Z"
maintainer = "Moko Consulting"
version = "04.05.00"
version = "05.00.00"
schema_version = "1.0"
}
+1 -2
View File
@@ -90,7 +90,6 @@ locals {
*
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
* Version: 04.05.00
* Schema Version: 1.0
*/
@@ -103,7 +102,7 @@ locals {
platform = "dolibarr"
last_updated = "2026-01-07T00:00:00Z"
maintainer = "Moko Consulting"
version = "04.05.00"
version = "05.00.00"
schema_version = "1.0"
}
+1 -2
View File
@@ -90,7 +90,6 @@ locals {
*
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
* Version: 04.05.00
* Schema Version: 1.0
*/
@@ -103,7 +102,7 @@ locals {
platform = "dolibarr"
last_updated = "2026-01-07T00:00:00Z"
maintainer = "Moko Consulting"
version = "04.05.00"
version = "05.00.00"
schema_version = "1.0"
}
+1 -2
View File
@@ -90,7 +90,6 @@ locals {
*
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
* Version: 04.05.00
* Schema Version: 1.0
*/
@@ -103,7 +102,7 @@ locals {
platform = "dolibarr"
last_updated = "2026-01-07T00:00:00Z"
maintainer = "Moko Consulting"
version = "04.05.00"
version = "05.00.00"
schema_version = "1.0"
}
+1 -2
View File
@@ -90,7 +90,6 @@ locals {
*
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
* Version: 04.05.00
* Schema Version: 1.0
*/
@@ -103,7 +102,7 @@ locals {
platform = "dolibarr"
last_updated = "2026-01-07T00:00:00Z"
maintainer = "Moko Consulting"
version = "04.05.00"
version = "05.00.00"
schema_version = "1.0"
}
+1 -2
View File
@@ -90,7 +90,6 @@ locals {
*
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
* Version: 04.05.00
* Schema Version: 1.0
*/
@@ -103,7 +102,7 @@ locals {
platform = "dolibarr"
last_updated = "2026-01-07T00:00:00Z"
maintainer = "Moko Consulting"
version = "04.05.00"
version = "05.00.00"
schema_version = "1.0"
}

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