724 Commits

Author SHA1 Message Date
gitea-actions[bot] ee7a42e14b chore: update updates.xml (development: 02.03.12-dev) [skip ci] 2026-05-24 08:54:00 +00:00
gitea-actions[bot] 1000f028d2 chore(version): auto-bump patch 02.03.12 [skip ci] 2026-05-24 08:53:59 +00:00
Jonathan Miller b048b47e7c security: protected status prevents disable/uninstall
Joomla: Repo Health / Access control (push) Successful in 1s
Joomla: Update Server / Update updates.xml (push) Successful in 25s
Joomla: Repo Health / Release configuration (push) Failing after 4s
Joomla: Repo Health / Scripts governance (push) Successful in 4s
Joomla: Repo Health / Repository health (push) Failing after 3s
- Set protected=1, locked=0 on MokoWaaS extensions via package script
- Self-healing: plugin checks and restores protected flag each session
- Block non-master disable via plugin list toggle (plugins.publish)
- Block non-master uninstall via installer manage
- Joomla framework natively enforces protected status (greys out toggles)
- Master users can still manage settings and updates

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 03:53:33 -05:00
Jonathan Miller 6e0d5387cf chore: update CHANGELOG for 02.03.10
Joomla: Repo Health / Access control (push) Successful in 1s
Joomla: Repo Health / Release configuration (push) Failing after 3s
Joomla: Repo Health / Scripts governance (push) Successful in 3s
Joomla: Repo Health / Repository health (push) Failing after 3s
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 03:49:16 -05:00
gitea-actions[bot] 76bbf7ad85 chore: update development channel 02.03.10 [skip ci] 2026-05-24 08:47:22 +00:00
gitea-actions[bot] a5dc00e056 chore: update development channel 02.03.10 [skip ci] 2026-05-24 08:47:21 +00:00
Jonathan Miller c6475ff29a feat: canonical URLs, alias heartbeats, package migration, cleanup
Joomla: Repo Health / Access control (push) Successful in 1s
Joomla: Update Server / Update updates.xml (push) Failing after 13m55s
Joomla: Repo Health / Scripts governance (push) Successful in 4s
Joomla: Repo Health / Repository health (push) Failing after 4s
Joomla: Repo Health / Release configuration (push) Failing after 5s
- manifest.xml: package-type plugin → package
- Canonical URL injection for alias domains (prevents SEO duplication)
- Heartbeat registration for alias domains (each alias gets Grafana datasource)
- Package script.php: enable plugins on every install/update, heartbeat on postflight
- Remove accidentally committed profile.ps1 and TODO.md
- Add profile.ps1 and TODO.md to .gitignore

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 03:46:23 -05:00
gitea-actions[bot] ffd98a19d9 chore: update updates.xml (development: 02.03.10-dev) [skip ci] 2026-05-24 08:38:28 +00:00
gitea-actions[bot] 34469609dd chore(version): auto-bump patch 02.03.10 [skip ci] 2026-05-24 08:38:27 +00:00
Jonathan Miller d766b0568a fix: alias offline uses Joomla native offline mode (template offline.php)
Joomla: Repo Health / Access control (push) Successful in 2s
Joomla: Update Server / Update updates.xml (push) Successful in 25s
Joomla: Repo Health / Release configuration (push) Failing after 4s
Joomla: Repo Health / Scripts governance (push) Successful in 4s
Joomla: Repo Health / Repository health (push) Failing after 4s
Instead of rendering a custom HTML page, set config('offline', 1) at
onAfterRoute so Joomla renders the site template's offline.php layout.
Custom offline_message is passed via config if set.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 03:38:01 -05:00
gitea-actions[bot] cfea9fac99 chore: update updates.xml (development: 02.03.09-dev) [skip ci] 2026-05-24 08:35:34 +00:00
gitea-actions[bot] 7d6d654d6d chore(version): auto-bump patch 02.03.09 [skip ci] 2026-05-24 08:35:33 +00:00
Jonathan Miller dca452e49d fix: alias detection, offline page, backend redirect working
Joomla: Repo Health / Access control (push) Successful in 1s
Joomla: Update Server / Update updates.xml (push) Successful in 25s
Joomla: Repo Health / Release configuration (push) Has been cancelled
Joomla: Repo Health / Scripts governance (push) Has been cancelled
Joomla: Repo Health / Repository health (push) Has been cancelled
- Removed primaryHost check from getCurrentAlias() — just look up
  current host in aliases list directly
- Handle Joomla subform stdClass→array conversion
- Strip trailing slashes from alias domains
- Moved handleSiteAlias() to onAfterRoute (client type resolved)
- Use http_response_code(503) + die() for offline page
- Cast offline value to string for comparison

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 03:35:06 -05:00
gitea-actions[bot] b827b3382a chore: update development channel 02.03.07 [skip ci] 2026-05-24 08:07:35 +00:00
gitea-actions[bot] 42841f7335 chore: update development channel 02.03.07 [skip ci] 2026-05-24 08:07:34 +00:00
gitea-actions[bot] 0f354422aa chore: update updates.xml (development: 02.03.07-dev) [skip ci] 2026-05-24 04:53:47 +00:00
gitea-actions[bot] 86aae39be1 chore(version): auto-bump patch 02.03.07 [skip ci] 2026-05-24 04:53:46 +00:00
Jonathan Miller b5e932d78b fix: backend redirect uses primary_domain setting instead of Uri::root
Joomla: Repo Health / Access control (push) Successful in 1s
Joomla: Update Server / Update updates.xml (push) Successful in 25s
Joomla: Repo Health / Release configuration (push) Failing after 3s
Joomla: Repo Health / Scripts governance (push) Successful in 3s
Joomla: Repo Health / Repository health (push) Failing after 3s
Uri::root() returns the current request domain, so redirecting from an
alias to Uri::root()/administrator redirects back to the alias. Added
primary_domain field to Site Aliases tab and getPrimaryHost() method
that checks: plugin setting → $live_site → alias exclusion fallback.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 23:53:20 -05:00
gitea-actions[bot] c738eb6669 chore: update updates.xml (development: 02.03.06-dev) [skip ci] 2026-05-24 04:40:20 +00:00
gitea-actions[bot] e0f98dc5e2 chore(version): auto-bump patch 02.03.06 [skip ci] 2026-05-24 04:40:19 +00:00
Jonathan Miller ede07c6675 feat: dynamic plugin version + plugin protection (no lock)
Joomla: Repo Health / Access control (push) Successful in 1s
Joomla: Update Server / Update updates.xml (push) Successful in 28s
Joomla: Repo Health / Release configuration (push) Has been cancelled
Joomla: Repo Health / Scripts governance (push) Has been cancelled
Joomla: Repo Health / Repository health (push) Has been cancelled
- Read plugin_version from manifest XML instead of hardcoding
- Hide MokoWaaS from plugin/installer list for non-master users
- Block non-master uninstall and disable attempts
- No self-healing lock — master users can still disable if needed

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 23:39:46 -05:00
gitea-actions[bot] 1fe8422fc0 chore: update development channel 02.03.04 [skip ci] 2026-05-24 04:25:59 +00:00
gitea-actions[bot] 6e216de0dc chore: update development channel 02.03.04 [skip ci] 2026-05-24 04:25:58 +00:00
Jonathan Miller 86e40fb978 chore: sync plugin manifest version to 02.03.04 [skip ci]
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 23:25:26 -05:00
gitea-actions[bot] b2f52c191b chore: update development channel 02.03.03 [skip ci] 2026-05-24 04:21:25 +00:00
gitea-actions[bot] 8e1040efee chore: update development channel 02.03.03 [skip ci] 2026-05-24 04:21:24 +00:00
gitea-actions[bot] c203e970b9 chore: update development channel 02.03.02 [skip ci] 2026-05-24 04:16:49 +00:00
gitea-actions[bot] bf3c986113 chore: update development channel 02.03.02 [skip ci] 2026-05-24 04:16:48 +00:00
Jonathan Miller 955c08a387 fix(ci): disable pipefail during element detection to prevent SIGPIPE exit
Joomla: Repo Health / Access control (push) Successful in 1s
Joomla: Repo Health / Repository health (push) Failing after 5s
Joomla: Repo Health / Release configuration (push) Failing after 5s
Joomla: Repo Health / Scripts governance (push) Failing after 5s
The find|grep|head pipe under bash pipefail causes SIGPIPE when head
closes the pipe after reading one line, making the step exit with code 1
even though the version bump and push succeeded.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 23:16:22 -05:00
Jonathan Miller 52dbefbb14 fix(ci): version bump after release, not before
Joomla: Repo Health / Access control (push) Successful in 1s
Joomla: Repo Health / Release configuration (push) Has been cancelled
Joomla: Repo Health / Scripts governance (push) Has been cancelled
Joomla: Repo Health / Repository health (push) Has been cancelled
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 23:13:49 -05:00
jmiller 379ca36613 Merge pull request 'chore: cascade main → dev [skip ci]' (#32) from main into dev
chore: cascade main → dev [skip ci]
2026-05-24 04:11:53 +00:00
jmiller 35ca1af6b8 Merge pull request 'feat: convert to package with webservices API + heartbeat fix' (#31) from dev into main
chore: cascade main → dev [skip ci]
2026-05-24 04:10:09 +00:00
Moko Consulting e9ec664f03 chore: update CHANGELOG for deploy workflow removal
Joomla: Repo Health / Access control (push) Successful in 2s
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 3s
Joomla: Repo Health / Release configuration (push) Failing after 3s
Joomla: Repo Health / Scripts governance (push) Successful in 4s
Joomla: Repo Health / Repository health (push) Failing after 4s
2026-05-24 04:09:59 +00:00
gitea-actions[bot] 6929c636b9 chore(version): bump to 02.03.02 [skip ci] 2026-05-24 04:06:06 +00:00
gitea-actions[bot] 20797d663f chore(version): bump to 02.03.01 [skip ci] 2026-05-24 03:48:44 +00:00
jmiller 11d5fd2019 chore: sync updates.xml from [skip ci] 2026-05-24 03:44:46 +00:00
gitea-actions[bot] 1fa965dddb chore: update updates.xml (development: 02.01.46-dev) [skip ci] 2026-05-24 03:44:45 +00:00
gitea-actions[bot] 5a3ec7d9b1 chore(version): auto-bump patch 02.01.46 [skip ci] 2026-05-24 03:44:44 +00:00
Jonathan Miller aef5ca43f6 chore(version): bump to 02.03.00
Joomla: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 4s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 3s
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Has been skipped
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Has been skipped
Joomla: Extension CI / PHPStan Analysis (pull_request) Has been skipped
Joomla: Repo Health / Access control (pull_request) Successful in 2s
Universal: PR Check / Changelog Updated (pull_request) Failing after 5s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Failing after 7s
Joomla: Update Server / Update updates.xml (push) Successful in 37s
Joomla: Repo Health / Release configuration (push) Failing after 3s
Joomla: Repo Health / Scripts governance (push) Successful in 3s
Joomla: Repo Health / Repository health (push) Failing after 4s
Joomla: Repo Health / Release configuration (pull_request) Has been cancelled
Joomla: Repo Health / Scripts governance (pull_request) Has been cancelled
Joomla: Repo Health / Repository health (pull_request) Has been cancelled
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 22:44:04 -05:00
jmiller 0631d80fa1 chore: sync updates.xml from [skip ci] 2026-05-24 03:42:32 +00:00
gitea-actions[bot] e84c10b14f chore: update updates.xml (development: 02.01.45-dev) [skip ci] 2026-05-24 03:42:31 +00:00
gitea-actions[bot] 87e543ef1c chore(version): auto-bump patch 02.01.45 [skip ci] 2026-05-24 03:42:31 +00:00
Jonathan Miller 32236ad7ff feat(package): convert to package with webservices API plugin
Joomla: Repo Health / Access control (push) Successful in 1s
Joomla: Update Server / Update updates.xml (push) Successful in 26s
Joomla: Repo Health / Release configuration (push) Has been cancelled
Joomla: Repo Health / Scripts governance (push) Has been cancelled
Joomla: Repo Health / Repository health (push) Has been cancelled
Restructure MokoWaaS from a standalone system plugin into a Joomla
package containing:

- plg_system_mokowaas — existing system plugin (moved to packages/)
- com_mokowaas — minimal API-only component with health, cache, and
  update controllers for Joomla Web Services API
- plg_webservices_mokowaas — registers /api/v1/mokowaas/* routes

Package script auto-enables both plugins on fresh install.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 22:42:02 -05:00
jmiller d6e462f3b7 Merge pull request 'chore: cascade main → dev (d470669) [skip ci]' (#30) from main into dev
chore: cascade main → dev [skip ci]
2026-05-24 03:40:53 +00:00
jmiller d470669634 chore: remove deploy workflow — switching to Joomla update server method
Joomla: Repo Health / Access control (push) Successful in 2s
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 3s
Joomla: Repo Health / Release configuration (push) Has been cancelled
Joomla: Repo Health / Scripts governance (push) Has been cancelled
Joomla: Repo Health / Repository health (push) Has been cancelled
2026-05-24 03:40:50 +00:00
jmiller feaccf0758 fix: updates.xml add stable channel entry [skip ci] 2026-05-24 03:39:45 +00:00
Jonathan Miller 9542c88ba4 fix: updates.xml missing stable entry — add both stable and dev channels
Joomla: Repo Health / Access control (push) Successful in 1s
Joomla: Repo Health / Release configuration (push) Has been cancelled
Joomla: Repo Health / Scripts governance (push) Has been cancelled
Joomla: Repo Health / Repository health (push) Has been cancelled
The pre-release workflow overwrites updates.xml with only the dev entry,
removing the stable channel. Joomla updater on sites set to stable channel
cannot see any updates.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 22:38:16 -05:00
jmiller 7e3d366043 chore: sync updates.xml 02.01.44 from dev [skip ci] 2026-05-24 03:33:54 +00:00
gitea-actions[bot] 4c5919f209 chore: update development channel 02.01.44 [skip ci] 2026-05-24 03:33:53 +00:00
gitea-actions[bot] 0b3e699f29 chore(version): bump to 02.01.44 [skip ci] 2026-05-24 03:33:52 +00:00
jmiller 406242fb7d chore: sync updates.xml 02.02.00 [skip ci] 2026-05-24 03:08:12 +00:00
jmiller e6cb6eb531 Merge pull request 'chore: cascade main → dev (bdceb42) [skip ci]' (#28) from main into dev
chore: cascade main → dev [skip ci]
2026-05-23 23:39:10 +00:00
jmiller bdceb4256f Merge pull request 'Release 02.01.43: Site aliases tab, API endpoints, heartbeat fix' (#27) from dev into main
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 3s
2026-05-23 23:39:06 +00:00
Jonathan Miller 81591477b2 chore: resolve merge conflicts (version 02.01.43)
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Joomla: Extension CI / Release Readiness Check (pull_request) Successful in 4s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 5s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 5s
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Has been skipped
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Has been skipped
Joomla: Extension CI / PHPStan Analysis (pull_request) Has been skipped
Universal: PR Check / Changelog Updated (pull_request) Successful in 4s
Universal: Build & Release / Build & Release Pipeline (pull_request) Failing after 26s
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 18:38:51 -05:00
Jonathan Miller cc907a5aa2 fix(heartbeat): only send heartbeat for primary domain, not aliases
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 3s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 4s
Joomla: Extension CI / Release Readiness Check (pull_request) Successful in 4s
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Has been skipped
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Has been skipped
Joomla: Extension CI / PHPStan Analysis (pull_request) Has been skipped
Universal: PR Check / Validate PR (pull_request) Failing after 3s
Universal: PR Check / Changelog Updated (pull_request) Successful in 3s
Alias domains were creating separate Grafana datasources with unique
UIDs, causing provisioning failures (duplicate UID) when Grafana
restarts. Only the primary domain should be registered.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 18:34:57 -05:00
Jonathan Miller 5360c641e9 chore: update CHANGELOG and README for 02.01.43 release
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 18:34:26 -05:00
jmiller d7a4066261 chore: sync updates.xml 02.01.43 from dev [skip ci] 2026-05-23 23:12:31 +00:00
gitea-actions[bot] 478eb262b9 chore: update development channel 02.01.43 [skip ci] 2026-05-23 23:12:31 +00:00
gitea-actions[bot] ea934ba04b chore(version): bump to 02.01.43 [skip ci] 2026-05-23 23:12:30 +00:00
Jonathan Miller a92c1ce772 feat: site aliases tab with per-alias offline, robots, and backend redirect
Move site aliases from a comma-separated text field in Diagnostics to its
own tab using Joomla's subform repeatable-table layout. Each alias entry
now supports: domain, offline toggle with custom message, robots meta
directive, and backend redirect to primary domain. Frontend stays on the
alias domain while admin requests can be redirected to the primary.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 18:11:07 -05:00
jmiller 0e55a546ff chore: sync updates.xml 02.01.42 from dev [skip ci] 2026-05-23 22:58:03 +00:00
gitea-actions[bot] 14e94518ba chore: update development channel 02.01.42 [skip ci] 2026-05-23 22:58:03 +00:00
gitea-actions[bot] b32d91c446 chore(version): bump to 02.01.42 [skip ci] 2026-05-23 22:58:02 +00:00
Jonathan Miller 03e0b6d13b fix: accept any 200 status from heartbeat (registered/updated/ok)
The receiver returns 'updated' when re-registering an existing site,
but the code only accepted 'registered', causing false 'HTTP 200 — Unknown' warnings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 17:30:52 -05:00
jmiller 21cb335727 chore: stable 02.01.42 [skip ci] 2026-05-23 22:25:01 +00:00
jmiller d2700f96fe chore: stable 02.01.42 [skip ci] 2026-05-23 22:25:01 +00:00
gitea-actions[bot] 7f7cdb4cc9 chore: update release-candidate channel 02.01.42 [skip ci] 2026-05-23 22:22:55 +00:00
jmiller b1b21e79d8 chore: sync updates.xml 02.01.42 from main [skip ci] 2026-05-23 22:22:55 +00:00
gitea-actions[bot] e44336d24a chore(version): bump to 02.01.42 [skip ci] 2026-05-23 22:22:53 +00:00
Jonathan Miller 76431371c3 Merge remote-tracking branch 'origin/dev' 2026-05-23 17:16:22 -05:00
jmiller 2d667ba4cd chore: sync updates.xml 02.01.41 from dev [skip ci] 2026-05-23 21:56:35 +00:00
gitea-actions[bot] 4bc23ae54f chore: update development channel 02.01.41 [skip ci] 2026-05-23 21:56:34 +00:00
gitea-actions[bot] 8843f721e5 chore(version): bump to 02.01.41 [skip ci] 2026-05-23 21:56:33 +00:00
Jonathan Miller 0a6744644d fix: script.php uses heartbeat receiver instead of Grafana API
The postflight still had the old Grafana API code with obfuscated tokens,
causing 403 RBAC errors on install/update. Now uses the heartbeat receiver
at bench.mokoconsulting.tech/api/waas-heartbeat/register.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 16:55:58 -05:00
jmiller e541e24741 chore: sync updates.xml 02.01.40 from dev [skip ci] 2026-05-23 21:52:55 +00:00
gitea-actions[bot] b30e6813fe chore: update development channel 02.01.40 [skip ci] 2026-05-23 21:52:54 +00:00
gitea-actions[bot] f61e8e53b5 chore(version): bump to 02.01.40 [skip ci] 2026-05-23 21:52:53 +00:00
Jonathan Miller dff7d73009 feat: MokoWaaS API — 6 endpoints for remote management
New API endpoints (all token-authenticated, HTTPS-only):
  ?mokowaas=health  — 16 diagnostic checks (GET)
  ?mokowaas=install — install extension from URL (POST)
  ?mokowaas=update  — trigger Joomla update check (POST)
  ?mokowaas=cache   — clear all caches + opcache (POST)
  ?mokowaas=backup  — trigger Akeeba Backup (POST)
  ?mokowaas=info    — compact site summary (GET)

Also adds:
  - Centralized API router with shared token auth
  - site_aliases config field for multi-domain support
  - Each alias registers its own Grafana datasource

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 16:51:32 -05:00
jmiller 186d129101 chore: sync updates.xml 02.01.39 from dev [skip ci] 2026-05-23 21:16:19 +00:00
gitea-actions[bot] 35ac2aaed2 chore: update development channel 02.01.39 [skip ci] 2026-05-23 21:16:19 +00:00
gitea-actions[bot] d825621502 chore(version): bump to 02.01.39 [skip ci] 2026-05-23 21:16:18 +00:00
Jonathan Miller 1aa1a8282f feat: rebuild with heartbeat receiver, 16 checks, multi-domain support
Reconstructed from git history after cascade revert wiped dev:
- 16 health checks (database, filesystem, cache, extensions, backup,
  security, SSL, cron, errors, db_size, content, users, mail, SEO,
  template, config)
- Heartbeat receiver provisioning (replaces Grafana API)
- Multi-domain support via site_aliases config field
- Each alias domain registers its own Grafana datasource
- Human-readable reason field for degraded status

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 16:15:45 -05:00
jmiller 6ca75bfbe7 chore: stable 02.01.39 [skip ci] 2026-05-23 20:51:38 +00:00
jmiller 0fd342cccd chore: stable 02.01.39 [skip ci] 2026-05-23 20:51:37 +00:00
jmiller f5ca5aadea chore: stable 02.01.39 [skip ci] 2026-05-23 20:50:47 +00:00
jmiller 6400bc9243 chore: sync updates.xml 02.01.39 from main [skip ci] 2026-05-23 20:45:17 +00:00
gitea-actions[bot] cd15dddc3f chore: update release-candidate channel 02.01.39 [skip ci] 2026-05-23 20:45:16 +00:00
gitea-actions[bot] b98c2e0b6e chore(version): bump to 02.01.39 [skip ci] 2026-05-23 20:45:15 +00:00
jmiller 943a19934e fix: sync README to 02.01.38 [skip ci] 2026-05-23 20:44:32 +00:00
jmiller dd57c23716 fix: sync manifest to 02.01.38 [skip ci] 2026-05-23 20:44:32 +00:00
jmiller 14a96eeb60 fix: sync manifest to 02.01.38 [skip ci] 2026-05-23 20:44:31 +00:00
jmiller 25c4d81e58 chore: sync updates.xml 02.01.38 from main [skip ci] 2026-05-23 20:27:28 +00:00
gitea-actions[bot] 177f233edb chore: update release-candidate channel 02.01.38 [skip ci] 2026-05-23 20:27:27 +00:00
gitea-actions[bot] e01ed29bad chore(version): bump to 02.01.38 [skip ci] 2026-05-23 20:27:26 +00:00
jmiller 640114e00c fix: sync manifest version to 02.01.37 [skip ci] 2026-05-23 20:26:29 +00:00
jmiller b82c8c4ba0 fix: sync version to 02.01.37 [skip ci] 2026-05-23 20:26:29 +00:00
jmiller 448592440a fix: sync manifest version to 02.01.37 [skip ci] 2026-05-23 20:26:28 +00:00
jmiller 474430ebba fix: sync version to 02.01.37 [skip ci] 2026-05-23 20:26:28 +00:00
jmiller 66183a6ec0 chore: sync updates.xml 02.01.36 from main [skip ci] 2026-05-23 20:24:29 +00:00
gitea-actions[bot] 28faea267c chore: update release-candidate channel 02.01.36 [skip ci] 2026-05-23 20:24:28 +00:00
gitea-actions[bot] 46d5bd0841 chore(version): bump to 02.01.36 [skip ci] 2026-05-23 20:24:27 +00:00
jmiller 6140b38662 chore: sync stable 02.01.37 to dev [skip ci] 2026-05-23 19:56:11 +00:00
jmiller 1553289af1 chore: update stable channel 02.01.37 [skip ci] 2026-05-23 19:56:11 +00:00
jmiller 7f90b83992 Merge pull request 'chore: cascade main → dev (5164eda) [skip ci]' (#24) from main into dev
chore: cascade main → dev [skip ci]
2026-05-23 19:49:43 +00:00
jmiller 5164edaf7f Merge pull request 'feat: health endpoint with 16 diagnostic checks (#54)' (#23) from dev into main
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 2s
2026-05-23 19:49:39 +00:00
jmiller 665582b913 fix(ci): add branch output to auto-release [skip ci] 2026-05-23 19:47:53 +00:00
Jonathan Miller f2e1dfe114 docs: update CHANGELOG and README for 02.01.37 release
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 3s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 3s
Universal: PR Check / Validate PR (pull_request) Failing after 4s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 4s
Universal: PR Check / Changelog Updated (pull_request) Successful in 4s
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Has been skipped
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Has been skipped
Joomla: Extension CI / PHPStan Analysis (pull_request) Has been skipped
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 02:09:01 -05:00
jmiller 0357364908 Merge pull request 'chore: cascade main → dev (e7de6e4) [skip ci]' (#22) from main into dev
chore: cascade main → dev [skip ci]
2026-05-23 05:09:28 +00:00
Jonathan Miller e7de6e4c9a Revert "Merge remote-tracking branch 'origin/dev'"
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 3s
This reverts commit b27ef3aee3, reversing
changes made to c4d8381828.
2026-05-23 00:09:22 -05:00
jmiller c08efa81fe chore: sync updates.xml 02.01.37 from main [skip ci] 2026-05-23 05:02:16 +00:00
gitea-actions[bot] ea16fd92d7 chore: update release-candidate channel 02.01.37 [skip ci] 2026-05-23 05:02:15 +00:00
gitea-actions[bot] 0f446709b8 chore(version): bump to 02.01.37 [skip ci] 2026-05-23 05:02:15 +00:00
jmiller 3817d7ed13 Merge pull request 'chore: cascade main → dev (b27ef3a) [skip ci]' (#21) from main into dev
chore: cascade main → dev [skip ci]
2026-05-23 04:57:00 +00:00
Jonathan Miller b27ef3aee3 Merge remote-tracking branch 'origin/dev'
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 2s
2026-05-22 23:56:45 -05:00
gitea-actions[bot] 030423a16d chore: update development channel 02.01.36 [skip ci] 2026-05-23 04:42:48 +00:00
jmiller c4d8381828 chore: sync updates.xml 02.01.36 from dev [skip ci] 2026-05-23 04:42:48 +00:00
gitea-actions[bot] 590e1eb385 chore(version): bump to 02.01.36 [skip ci] 2026-05-23 04:42:46 +00:00
Jonathan Miller f1f2785f0f feat: add 10 new health checks — SSL, cron, errors, DB size, content, users, mail, SEO, template, config
New checks:
- ssl: certificate expiry, days left, issuer
- cron: scheduled tasks, failed count, last run
- errors: PHP error log size, recent errors
- db_size: total MB, table count, largest tables
- content: articles, categories, menu items, modules
- users: total, active sessions, failed logins, last login
- mail: mailer type, from address, queue count
- seo: robots.txt, sitemap, htaccess, SEF
- template: site/admin template, override count
- config: debug mode, error reporting, force SSL, caching

Degraded reasons for SSL expiry, failed cron tasks, config drift.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-22 23:23:19 -05:00
Jonathan Miller 138af226d1 feat: add site size and total disk to filesystem health check
Reports total_disk_mb and site_size_mb (images, media, tmp, cache, logs).
Shows in Infrastructure table on Grafana dashboard.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-22 23:13:20 -05:00
Jonathan Miller 17eaaf2347 feat: add Akeeba Backup and Admin Tools checks to health endpoint
New health checks:
- backup: last backup date/status/size, days since, total count, 7d count
- security: Admin Tools WAF status, blocked requests 24h/7d

Degraded reasons:
- No backups found
- Last backup older than 7 days
- Last backup failed/incomplete

Dashboard updated with Backup and Security rows.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-22 22:43:41 -05:00
Jonathan Miller 21020027d0 feat: add human-readable reason field to health endpoint
Status 'degraded' now includes reason like '3 extension updates available'
or 'Low disk space: 45 MB free'. Shows in dashboard Site Info table.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-22 22:00:33 -05:00
jmiller fa4622c0ca Merge pull request 'chore: cascade main → dev (a45a6cb) [skip ci]' (#20) from main into dev
chore: cascade main → dev [skip ci]
2026-05-23 01:15:04 +00:00
Jonathan Miller 0d280717f1 Revert "Merge pull request 'chore: merge dev to main' (#19) from dev into main"
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 1s
This reverts commit a45a6cb59c, reversing
changes made to 018b197147.
2026-05-22 20:12:46 -05:00
jmiller a45a6cb59c Merge pull request 'chore: merge dev to main' (#19) from dev into main
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 2s
2026-05-23 01:10:42 +00:00
gitea-actions[bot] 8edced75d3 chore: update development channel 02.01.39 [skip ci]
Universal: Build & Release / Build & Release Pipeline (pull_request) Failing after 25s
2026-05-23 01:08:12 +00:00
jmiller 018b197147 chore: sync updates.xml 02.01.39 from dev [skip ci] 2026-05-23 01:08:12 +00:00
gitea-actions[bot] 142ee2387e chore(version): bump to 02.01.39 [skip ci] 2026-05-23 01:08:10 +00:00
Jonathan Miller ea66ad4b4a security: hide MokoWaaS from plugin list for non-master users
Injects JS on com_plugins that removes the MokoWaaS row from the
plugin table. Combined with the edit/save block, non-master users
cannot see, edit, or save the plugin settings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-22 05:53:14 -05:00
Jonathan Miller 48cb040505 security: restrict plugin settings to master user + rename Gitea to MokoGitea
- Non-master users blocked from editing MokoWaaS plugin config
- isOurPlugin() helper checks extension_id against our plugin
- Blocks both edit view and save task for non-master users
- Renamed bare 'Gitea' references to 'MokoGitea' in docs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-22 05:51:20 -05:00
jmiller 8df006876b chore: sync updates.xml 02.01.38 from dev [skip ci] 2026-05-22 09:57:19 +00:00
gitea-actions[bot] aec849c9ae chore: update development channel 02.01.38 [skip ci] 2026-05-22 09:57:19 +00:00
gitea-actions[bot] d3281066dc chore(version): bump to 02.01.38 [skip ci] 2026-05-22 09:57:18 +00:00
Jonathan Miller b17b36e02e security: make plugin hard to disable + block uninstall
- enforceLocked() runs every page load — re-enables, re-locks, re-protects
  if someone tampers with the database flags
- preflight() blocks uninstall attempts with error message
- Logs tampering attempts to mokowaas log category

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-22 04:56:48 -05:00
jmiller cb6d19ece8 chore: sync updates.xml 02.01.37 from dev [skip ci] 2026-05-22 09:41:25 +00:00
gitea-actions[bot] 5020b58da1 chore: update development channel 02.01.37 [skip ci] 2026-05-22 09:41:24 +00:00
gitea-actions[bot] c97432495b chore(version): bump to 02.01.37 [skip ci] 2026-05-22 09:41:23 +00:00
Jonathan Miller b22842f302 refactor: replace Grafana API with heartbeat receiver provisioning
Remove all Grafana API code (630 lines), obfuscated tokens, SA tokens,
ensureGrafanaPlugin, provisionGrafanaDatasource, buildDashboardModel.

Replace with simple HTTP POST to heartbeat receiver on bench server.
Receiver writes Grafana provisioning YAML and restarts Grafana container.
No API tokens or RBAC permissions needed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-22 04:40:51 -05:00
jmiller 6a186ed365 chore: sync updates.xml 02.01.36 from dev [skip ci] 2026-05-22 04:02:26 +00:00
gitea-actions[bot] 42d530bfbf chore: update development channel 02.01.36 [skip ci] 2026-05-22 04:02:26 +00:00
gitea-actions[bot] 307dc37d47 chore(version): bump to 02.01.36 [skip ci] 2026-05-22 04:02:25 +00:00
Jonathan Miller 2e4fdcb07e fix: new Grafana SA token with datasource:create + visible heartbeat errors
- New service account token with correct RBAC permissions
- script.php postflight now shows success/failure messages to admin
- Logs all heartbeat attempts with HTTP code and cURL errors

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 23:01:33 -05:00
Jonathan Miller 0ec18b9868 Merge remote-tracking branch 'origin/dev'
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 3s
2026-05-21 22:52:14 -05:00
Jonathan Miller e3f4890e5d docs: update CHANGELOG for production release 02.01.35 [skip ci]
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 22:51:13 -05:00
jmiller 6e81585177 chore: sync updates.xml 02.01.35 from dev [skip ci] 2026-05-22 03:49:22 +00:00
gitea-actions[bot] ff69e1d7fd chore: update development channel 02.01.35 [skip ci] 2026-05-22 03:49:21 +00:00
gitea-actions[bot] eefe9d134a chore(version): bump to 02.01.35 [skip ci] 2026-05-22 03:49:20 +00:00
Jonathan Miller bfb159d0f0 fix: add SSL bypass and error logging to Grafana provisioning
- Add CURLOPT_SSL_VERIFYPEER=false for shared hosting environments
- Add CURLOPT_FOLLOWLOCATION to handle redirects
- Log all Grafana heartbeat attempts with HTTP code and cURL errors
- Helps debug provisioning failures on DreamHost and similar hosts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 22:48:49 -05:00
jmiller fc47aee5ba chore: sync updates.xml 02.01.34 from dev [skip ci] 2026-05-22 03:43:18 +00:00
gitea-actions[bot] 6b95c0aef9 chore: update development channel 02.01.34 [skip ci] 2026-05-22 03:43:18 +00:00
gitea-actions[bot] 047e296ee3 chore(version): bump to 02.01.34 [skip ci] 2026-05-22 03:43:17 +00:00
Jonathan Miller 47faa1b289 fix: update Grafana API token (Admin SA) [skip ci]
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 22:42:49 -05:00
jmiller 48372e2f4f chore: sync updates.xml 02.01.33 from dev [skip ci] 2026-05-22 03:36:20 +00:00
gitea-actions[bot] 155a8b92b0 chore: update development channel 02.01.33 [skip ci] 2026-05-22 03:36:19 +00:00
gitea-actions[bot] ba0029e286 chore(version): bump to 02.01.33 [skip ci] 2026-05-22 03:36:18 +00:00
jmiller b46506cdb7 fix: updates.xml download URL tag dev -> development [skip ci] 2026-05-22 03:34:46 +00:00
jmiller ebbac5760c fix: updates.xml download URL tag dev -> development [skip ci] 2026-05-22 03:34:46 +00:00
jmiller aa72835288 fix(ci): pre-release php-curl + continue-on-error + CLI updates.xml [skip ci] 2026-05-22 03:31:14 +00:00
jmiller 479a6c03f8 chore: sync updates.xml 02.01.32 from dev [skip ci] 2026-05-22 03:29:13 +00:00
gitea-actions[bot] 55219d758c chore: update development channel 02.01.32 [skip ci] 2026-05-22 03:29:12 +00:00
gitea-actions[bot] 27db3762ad chore(version): bump to 02.01.32 [skip ci] 2026-05-22 03:29:11 +00:00
jmiller a2f34db524 fix(ci): add php-curl [skip ci] 2026-05-22 03:28:41 +00:00
gitea-actions[bot] 37aa2bd854 chore: update development channel 02.01.31 [skip ci] 2026-05-22 03:25:43 +00:00
gitea-actions[bot] ccbd1dc2d5 chore(version): bump to 02.01.31 [skip ci] 2026-05-22 03:25:42 +00:00
jmiller 59f0d1c166 fix(ci): pre-release continue-on-error + CLI updates.xml [skip ci] 2026-05-22 03:25:15 +00:00
jmiller 72cf207f76 chore: sync updates.xml 02.01.30 [skip ci] 2026-05-22 03:20:57 +00:00
gitea-actions[bot] 8a66abc711 chore: update development channel 02.01.30 [skip ci] 2026-05-22 03:19:38 +00:00
gitea-actions[bot] c2f5ca7754 chore(version): bump to 02.01.30 [skip ci] 2026-05-22 03:19:37 +00:00
jmiller 1a6dfee74e fix(ci): zip_name/zip_path for upload [skip ci] 2026-05-22 03:19:12 +00:00
gitea-actions[bot] 25f8baf58b chore(version): bump to 02.01.29 [skip ci] 2026-05-22 03:14:30 +00:00
jmiller 8c60927bbf fix(ci): clean pre-release.yml [skip ci] 2026-05-22 03:14:01 +00:00
jmiller f80bf700ca fix(ci): clean pre-release.yml control chars [skip ci] 2026-05-22 03:10:40 +00:00
jmiller 717d81d80e fix(ci): sync auto-release.yml to dev [skip ci] 2026-05-22 02:59:00 +00:00
jmiller be4a0888c0 fix(ci): sync pre-release.yml to dev [skip ci] 2026-05-22 02:59:00 +00:00
gitea-actions[bot] 37a96bafa9 chore: update development channel 02.01.28 [skip ci] 2026-05-22 02:56:55 +00:00
gitea-actions[bot] 9ccd27e809 chore(version): bump 02.01.27 → 02.01.28 [skip ci] 2026-05-22 02:56:54 +00:00
jmiller 1a9815d96e refactor(ci): sync auto-release.yml — CLI-based workflow [skip ci] 2026-05-22 02:56:16 +00:00
jmiller 326277010d refactor(ci): pre-release uses CLI tools [skip ci] 2026-05-22 02:49:41 +00:00
jmiller 3f78570267 fix(ci): sync pre-release.yml — CLI-based updates.xml sync [skip ci] 2026-05-22 02:40:09 +00:00
jmiller ea2ed31cb4 fix(ci): sync pre-release.yml — updates.xml API sync (#34) [skip ci] 2026-05-22 02:35:55 +00:00
jmiller 57c259f951 chore: sync updates.xml from dev (02.01.27 development) [skip ci] 2026-05-22 02:31:27 +00:00
Jonathan Miller 6c10d51b2f docs: update CHANGELOG for 02.01.27 [skip ci]
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 21:29:12 -05:00
gitea-actions[bot] 49b4fa85a9 chore: update development channel 02.01.27 [skip ci] 2026-05-22 02:25:31 +00:00
gitea-actions[bot] 1c1b541bc5 chore(version): bump 02.01.26 → 02.01.27 [skip ci] 2026-05-22 02:25:30 +00:00
Jonathan Miller 0bc5504e16 security: obfuscate Grafana credentials with XOR+base64
API key and URL stored as XOR-encoded base64 constants. Deobfuscated
at runtime only when needed. Prevents plain-text grep discovery.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 21:25:01 -05:00
gitea-actions[bot] c5ff1a5ada chore: update development channel 02.01.26 [skip ci] 2026-05-22 02:21:15 +00:00
gitea-actions[bot] f63e46030d chore(version): bump 02.01.25 → 02.01.26 [skip ci] 2026-05-22 02:21:14 +00:00
Jonathan Miller 34df31b086 feat: hardcode Grafana credentials, always-on health endpoint
- Health endpoint always enabled when plugin is installed
- Grafana URL and API key hardcoded as constants
- Removed enable_health_endpoint, grafana_url, grafana_api_key from config UI
- Token still auto-generated and shown as read-only

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 21:20:44 -05:00
Jonathan Miller ec847a5eed docs: update CHANGELOG with 02.01.25 health endpoint release [skip ci]
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 21:12:20 -05:00
jmiller f3882bc96f chore: sync updates.xml from dev (02.01.25 development) [skip ci] 2026-05-22 02:10:29 +00:00
gitea-actions[bot] 9417a530a2 chore: update development channel 02.01.25 [skip ci] 2026-05-22 02:02:18 +00:00
gitea-actions[bot] 94b63ae08f chore(version): bump 02.01.24 → 02.01.25 [skip ci] 2026-05-22 02:02:17 +00:00
jmiller d7d7ab84a7 fix(ci): restore correct auto-release.yml from moko-platform [skip ci] 2026-05-22 02:01:48 +00:00
jmiller 6e817fe547 fix(ci): restore correct pre-release.yml from moko-platform [skip ci] 2026-05-22 02:01:47 +00:00
Jonathan Miller 71ad73bd04 chore: migrate to .mokogitea and remove legacy standards files
- Rename .gitea/ → .mokogitea/ (where applicable)
- Remove .moko-standards and .mokostandards files
- Manifest.xml is the single source of repo metadata

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 17:43:21 -05:00
jmiller a5ed9a8130 chore: sync security-audit.yml from moko-platform [skip ci] 2026-05-21 22:30:11 +00:00
jmiller f0097899e1 chore: sync repo-health.yml from moko-platform [skip ci] 2026-05-21 22:30:10 +00:00
jmiller d82e10abb5 chore: sync pre-release.yml from moko-platform [skip ci] 2026-05-21 22:30:10 +00:00
jmiller 390f8ae3a9 chore: sync pr-check.yml from moko-platform [skip ci] 2026-05-21 22:30:09 +00:00
jmiller b54a6b85d8 chore: sync notify.yml from moko-platform [skip ci] 2026-05-21 22:30:08 +00:00
jmiller dec8b5df6d chore: sync gitleaks.yml from moko-platform [skip ci] 2026-05-21 22:30:07 +00:00
jmiller 40ed2f028a chore: sync deploy-manual.yml from moko-platform [skip ci] 2026-05-21 22:30:07 +00:00
jmiller 35ff05c547 chore: sync cleanup.yml from moko-platform [skip ci] 2026-05-21 22:30:06 +00:00
jmiller f62278ea77 chore: sync cascade-dev.yml from moko-platform [skip ci] 2026-05-21 22:30:05 +00:00
jmiller 073f2b7750 chore: sync auto-release.yml from moko-platform [skip ci] 2026-05-21 22:30:05 +00:00
Jonathan Miller 3e6dad039d chore: fix manifest.xml to proper moko-platform format
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 17:26:10 -05:00
Jonathan Miller 512277cfb1 chore(ci): use manifest.xml for platform detection, remove .moko-platform
Authored-by: Moko Consulting

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 17:19:22 -05:00
Jonathan Miller c1cfa9ede8 chore: remove docs/ directory — content lives in wiki
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 17:07:44 -05:00
gitea-actions[bot] 49d05c4a1f chore: update development channel 02.01.24 [skip ci] 2026-05-21 21:00:30 +00:00
gitea-actions[bot] 444b8c2853 chore(version): bump 02.01.23 → 02.01.24 [skip ci] 2026-05-21 21:00:29 +00:00
Jonathan Miller 83bfe6c552 fix(ci): replace rsync with cp in pre-release build step
rsync is not available in all runner containers (exit 127).
cp -a is universally available and handles the directory copy.
Excluded files are removed in a separate rm step.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 16:00:01 -05:00
gitea-actions[bot] 18f64b5b68 chore(version): bump 02.01.22 → 02.01.23 [skip ci] 2026-05-21 20:56:44 +00:00
Jonathan Miller 3c705a785d fix(ci): add .moko-platform file and fix pipefail in pre-release
- Create .mokogitea/.moko-platform with 'joomla' platform identifier
- Add || true to find|grep|head pipelines in Detect Platform step
  to prevent grep exit-code 1 from killing the step under bash -e -o pipefail

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 15:56:22 -05:00
Jonathan Miller 66831ce4af chore(ci): migrate .gitea to .mokogitea and update workflows
Renamed .gitea/ to .mokogitea/ for MokoConsulting convention.
Updated release workflows with Joomla package type support.

Authored-by: Moko Consulting

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 15:54:11 -05:00
Jonathan Miller fbfc2ad9c5 chore(ci): update release workflows with package type support
Synced from Template-Joomla: pre-release.yml and auto-release.yml now
handle Joomla package extensions (type="package" with sub-extensions).

Authored-by: Moko Consulting

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 15:53:06 -05:00
Jonathan Miller d1e2555f00 feat(diagnostics): add health endpoint with Grafana auto-provisioning (#54)
Implements heartbeat telemetry for WaaS dashboard monitoring:
- JSON health endpoint at /?mokowaas=health with token auth
- Database, filesystem, cache, and extension health checks
- Auto-generated API token (separate from Joomla user tokens)
- Grafana Infinity datasource auto-provisioning via REST API
- Shared dashboard with endpoint dropdown variable
- Auto-provision on plugin install/update via script.php
- Grafana plugin install via API (replaces deprecated CLI)
- Deprovisioning on disable (datasource cleanup)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 15:44:04 -05:00
jmiller 78d6b66f40 chore: update CLAUDE.md to reference .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 20:09:09 +00:00
jmiller 83195862af chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:42 +00:00
jmiller e3752b772d chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:42 +00:00
jmiller 022c5e2181 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:41 +00:00
jmiller 8c3af3a728 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:41 +00:00
jmiller ec509f15ba chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:40 +00:00
jmiller e06f7a7224 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:40 +00:00
jmiller 21e6e7f09d chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:39 +00:00
jmiller 82b24f1673 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:38 +00:00
jmiller 233387a607 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:38 +00:00
jmiller e728cc6b8d chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:37 +00:00
jmiller e063766630 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:37 +00:00
jmiller 9ccb0c3359 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:36 +00:00
jmiller 6afa813830 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:36 +00:00
jmiller e154a9eaec chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:35 +00:00
jmiller b381273357 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:35 +00:00
jmiller cd12e60ca4 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:34 +00:00
jmiller 5f4965ae6e chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:34 +00:00
jmiller cb258d286c chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:33 +00:00
jmiller 61b0a9b7b5 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:33 +00:00
jmiller 7db21d2406 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:32 +00:00
jmiller dde259814f chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:32 +00:00
jmiller 16a71b13fb chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:31 +00:00
jmiller 66dc9d39c1 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:31 +00:00
jmiller 51564af6b0 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:30 +00:00
jmiller 665d6d69fc chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:29 +00:00
jmiller f2f821e3d1 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:29 +00:00
jmiller 9be02b4e53 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:28 +00:00
jmiller 263036f674 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:27 +00:00
jmiller 25c000638e chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:27 +00:00
jmiller aac6d459d1 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:26 +00:00
jmiller 7a901a558f chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:26 +00:00
jmiller 88f1cedcf2 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:25 +00:00
jmiller 83b8d9e447 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:25 +00:00
jmiller e20a2a3238 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:24 +00:00
jmiller a733168d1c chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:24 +00:00
jmiller a432f8065f chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:23 +00:00
jmiller 3c780f68db chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:22 +00:00
jmiller d4cac3cd1f chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:22 +00:00
jmiller 80e9e7f538 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:21 +00:00
jmiller 5d892e4464 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:21 +00:00
jmiller bfc2e88f10 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:20 +00:00
jmiller b4894ae7d3 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:20 +00:00
jmiller 700a3f636c chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:19 +00:00
jmiller c6e547415d chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:19 +00:00
jmiller e9df2602d2 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:18 +00:00
jmiller 5ac4e361a2 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:17 +00:00
jmiller c30446ddb5 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:17 +00:00
jmiller 9a885bc418 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:16 +00:00
jmiller 7a2ba6f8e0 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:16 +00:00
jmiller 233d741229 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:15 +00:00
jmiller 0397fc073e chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:15 +00:00
jmiller 5ecdd31b47 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:14 +00:00
jmiller 1240aac31a chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:13 +00:00
jmiller 4b4e25c21e chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:13 +00:00
jmiller 606fdeead7 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:12 +00:00
jmiller 0e9ff43937 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:12 +00:00
jmiller 2074eb49ec chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:11 +00:00
jmiller 3e14f5d195 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:11 +00:00
jmiller 08a3efb146 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:10 +00:00
jmiller 849762e782 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:10 +00:00
jmiller ccedb9ddd9 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:09 +00:00
jmiller a23910a779 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:09 +00:00
jmiller 10a31e1018 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:08 +00:00
jmiller a695bfcbdb chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:07 +00:00
jmiller fc9ad5a538 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:07 +00:00
jmiller 3df248c304 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:06 +00:00
jmiller ac99573ec9 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:06 +00:00
jmiller 9e7587048b chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:05 +00:00
jmiller 2b1ca3dd8d chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:05 +00:00
jmiller cdddecf442 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:04 +00:00
jmiller c876438955 chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:04 +00:00
jmiller d20cf6aaeb chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:03 +00:00
jmiller 55b08a27ba chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:03 +00:00
jmiller 8fa41f5d6b chore: rename .gitea/ to .mokogitea/ [skip ci]
Authored-by: Moko Consulting
2026-05-21 17:19:02 +00:00
jmiller df983c3e05 chore: add issue templates [skip ci] 2026-05-20 00:36:18 +00:00
jmiller 19e78b7ca6 chore: sync issue templates from template repo [skip ci] 2026-05-20 00:29:47 +00:00
jmiller c61aaf09ea chore: sync issue templates from template repo [skip ci] 2026-05-20 00:29:44 +00:00
jmiller ad4b400718 chore: sync issue templates from template repo [skip ci] 2026-05-20 00:29:43 +00:00
jmiller 2f9ad83dbd chore: sync issue templates from template repo [skip ci] 2026-05-20 00:29:42 +00:00
jmiller 8531bd07f3 chore: sync issue templates from template repo [skip ci] 2026-05-20 00:29:40 +00:00
jmiller 01be59b2bd chore: sync issue templates from template repo [skip ci] 2026-05-20 00:29:38 +00:00
jmiller b13d00d459 chore: sync issue templates from template repo [skip ci] 2026-05-20 00:29:37 +00:00
jmiller 0c6e784486 chore: sync issue templates from template repo [skip ci] 2026-05-20 00:29:36 +00:00
jmiller a0ea762f21 chore: sync issue templates from template repo [skip ci] 2026-05-20 00:29:35 +00:00
jmiller f0e5a532fa chore: sync issue templates from template repo [skip ci] 2026-05-20 00:29:33 +00:00
jmiller a0f3e42861 fix(lang): update pretty name to Joomla convention [skip ci] 2026-05-16 22:57:25 +00:00
jmiller 76e0da69bb fix(lang): update pretty name to Joomla convention [skip ci] 2026-05-16 22:57:25 +00:00
jmiller 06fb750319 chore: remove docs/ — documentation lives in wiki [skip ci] 2026-05-16 22:19:50 +00:00
jmiller 319d4710c8 chore: remove docs/ — documentation lives in wiki [skip ci] 2026-05-16 22:19:49 +00:00
jmiller accdc6d967 chore: remove docs/ — documentation lives in wiki [skip ci] 2026-05-16 22:19:49 +00:00
jmiller 203bc09414 chore: remove docs/ — documentation lives in wiki [skip ci] 2026-05-16 22:19:48 +00:00
jmiller 145d14b60a chore: rename Setup step to moko-platform [skip ci] 2026-05-16 22:19:19 +00:00
jmiller b520eeaa70 chore: rename Setup step to moko-platform [skip ci] 2026-05-16 22:19:18 +00:00
jmiller d8105c5861 feat(ci): deploy auto-release to dev [skip ci] 2026-05-16 18:58:34 +00:00
jmiller 2b69d7aa65 feat(ci): CB plugin detection + platform auto-sense + remove GitHub mirror [skip ci] 2026-05-16 18:57:53 +00:00
jmiller 3a107db67b chore: remove .mokogitea/workflows/update-server.yml (moved to .gitea/) [skip ci] 2026-05-16 18:36:09 +00:00
jmiller b6d590cfe0 chore: move .mokogitea/workflows/update-server.yml to .gitea/workflows/update-server.yml [skip ci] 2026-05-16 18:36:08 +00:00
jmiller 42782edea1 chore: remove .mokogitea/workflows/security-audit.yml (moved to .gitea/) [skip ci] 2026-05-16 18:36:08 +00:00
jmiller 2ce52b2deb chore: move .mokogitea/workflows/security-audit.yml to .gitea/workflows/security-audit.yml [skip ci] 2026-05-16 18:36:07 +00:00
jmiller 5e49f97cc4 chore: remove .mokogitea/workflows/repo-health.yml (moved to .gitea/) [skip ci] 2026-05-16 18:36:06 +00:00
jmiller e4050ae453 chore: move .mokogitea/workflows/repo-health.yml to .gitea/workflows/repo-health.yml [skip ci] 2026-05-16 18:36:06 +00:00
jmiller 981ce68983 chore: remove .mokogitea/workflows/pre-release.yml (moved to .gitea/) [skip ci] 2026-05-16 18:36:05 +00:00
jmiller 8360b9e348 chore: move .mokogitea/workflows/pre-release.yml to .gitea/workflows/pre-release.yml [skip ci] 2026-05-16 18:36:04 +00:00
jmiller 786f4f43ef chore: remove .mokogitea/workflows/pr-check.yml (moved to .gitea/) [skip ci] 2026-05-16 18:36:04 +00:00
jmiller 9d8e325010 chore: move .mokogitea/workflows/pr-check.yml to .gitea/workflows/pr-check.yml [skip ci] 2026-05-16 18:36:03 +00:00
jmiller f6e004546f chore: remove .mokogitea/workflows/notify.yml (moved to .gitea/) [skip ci] 2026-05-16 18:36:02 +00:00
jmiller d6ef4585b6 chore: move .mokogitea/workflows/notify.yml to .gitea/workflows/notify.yml [skip ci] 2026-05-16 18:36:02 +00:00
jmiller f6788a5fbd chore: remove .mokogitea/workflows/gitleaks.yml (moved to .gitea/) [skip ci] 2026-05-16 18:36:01 +00:00
jmiller 513385ae00 chore: move .mokogitea/workflows/gitleaks.yml to .gitea/workflows/gitleaks.yml [skip ci] 2026-05-16 18:36:00 +00:00
jmiller c05bd14d3c chore: remove .mokogitea/workflows/deploy-manual.yml (moved to .gitea/) [skip ci] 2026-05-16 18:36:00 +00:00
jmiller 4d2023f7aa chore: move .mokogitea/workflows/deploy-manual.yml to .gitea/workflows/deploy-manual.yml [skip ci] 2026-05-16 18:35:59 +00:00
jmiller 7d699ff1a2 chore: remove .mokogitea/workflows/cleanup.yml (moved to .gitea/) [skip ci] 2026-05-16 18:35:58 +00:00
jmiller a8e80d7c57 chore: move .mokogitea/workflows/cleanup.yml to .gitea/workflows/cleanup.yml [skip ci] 2026-05-16 18:35:58 +00:00
jmiller b3bda839d8 chore: remove .mokogitea/workflows/ci-joomla.yml (moved to .gitea/) [skip ci] 2026-05-16 18:35:57 +00:00
jmiller bce159b543 chore: move .mokogitea/workflows/ci-joomla.yml to .gitea/workflows/ci-joomla.yml [skip ci] 2026-05-16 18:35:56 +00:00
jmiller bda0318a01 chore: remove .mokogitea/workflows/cascade-dev.yml (moved to .gitea/) [skip ci] 2026-05-16 18:35:56 +00:00
jmiller e4a4a83c77 chore: move .mokogitea/workflows/cascade-dev.yml to .gitea/workflows/cascade-dev.yml [skip ci] 2026-05-16 18:35:55 +00:00
jmiller 549c2a1395 chore: remove .mokogitea/workflows/auto-release.yml (exists in .gitea/) [skip ci] 2026-05-16 18:35:55 +00:00
jmiller 4a781513b2 chore: remove .mokogitea/update-server.yml (moved to .gitea/) [skip ci] 2026-05-16 18:35:54 +00:00
jmiller c39ad4fa9c chore: move .mokogitea/update-server.yml to .gitea/update-server.yml [skip ci] 2026-05-16 18:35:53 +00:00
jmiller 11312ef146 chore: remove .mokogitea/security-audit.yml (moved to .gitea/) [skip ci] 2026-05-16 18:35:53 +00:00
jmiller b70dee40ce chore: move .mokogitea/security-audit.yml to .gitea/security-audit.yml [skip ci] 2026-05-16 18:35:52 +00:00
jmiller 9824a6807f chore: remove .mokogitea/repo-health.yml (moved to .gitea/) [skip ci] 2026-05-16 18:35:51 +00:00
jmiller 057b06d1f1 chore: move .mokogitea/repo-health.yml to .gitea/repo-health.yml [skip ci] 2026-05-16 18:35:51 +00:00
jmiller 071878d21f chore: remove .mokogitea/pre-release.yml (moved to .gitea/) [skip ci] 2026-05-16 18:35:50 +00:00
jmiller 757883e21b chore: move .mokogitea/pre-release.yml to .gitea/pre-release.yml [skip ci] 2026-05-16 18:35:50 +00:00
jmiller f9087d4f43 chore: remove .mokogitea/pr-check.yml (moved to .gitea/) [skip ci] 2026-05-16 18:35:49 +00:00
jmiller da304f28d3 chore: move .mokogitea/pr-check.yml to .gitea/pr-check.yml [skip ci] 2026-05-16 18:35:48 +00:00
jmiller c47e8f4eee chore: remove .mokogitea/pr-branch-check.yml (moved to .gitea/) [skip ci] 2026-05-16 18:35:48 +00:00
jmiller 69ccf73b41 chore: move .mokogitea/pr-branch-check.yml to .gitea/pr-branch-check.yml [skip ci] 2026-05-16 18:35:47 +00:00
jmiller 043dbc5a63 chore: remove .mokogitea/notify.yml (moved to .gitea/) [skip ci] 2026-05-16 18:35:46 +00:00
jmiller 050769d143 chore: move .mokogitea/notify.yml to .gitea/notify.yml [skip ci] 2026-05-16 18:35:46 +00:00
jmiller e79dd65b28 chore: remove .mokogitea/manifest.xml (moved to .gitea/) [skip ci] 2026-05-16 18:35:45 +00:00
jmiller d4a37b6f06 chore: move .mokogitea/manifest.xml to .gitea/manifest.xml [skip ci] 2026-05-16 18:35:44 +00:00
jmiller 92cf8e8b1a chore: remove .mokogitea/gitleaks.yml (moved to .gitea/) [skip ci] 2026-05-16 18:35:44 +00:00
jmiller b5c6c16362 chore: move .mokogitea/gitleaks.yml to .gitea/gitleaks.yml [skip ci] 2026-05-16 18:35:43 +00:00
jmiller b873ca263a chore: remove .mokogitea/deploy-manual.yml (moved to .gitea/) [skip ci] 2026-05-16 18:35:43 +00:00
jmiller 0b1e4546c6 chore: move .mokogitea/deploy-manual.yml to .gitea/deploy-manual.yml [skip ci] 2026-05-16 18:35:42 +00:00
jmiller d34b2a534b chore: remove .mokogitea/cleanup.yml (moved to .gitea/) [skip ci] 2026-05-16 18:35:41 +00:00
jmiller 1404cdb026 chore: move .mokogitea/cleanup.yml to .gitea/cleanup.yml [skip ci] 2026-05-16 18:35:41 +00:00
jmiller 0dacff33b0 chore: remove .mokogitea/ci-joomla.yml (moved to .gitea/) [skip ci] 2026-05-16 18:35:40 +00:00
jmiller aaed2f6a59 chore: move .mokogitea/ci-joomla.yml to .gitea/ci-joomla.yml [skip ci] 2026-05-16 18:35:39 +00:00
jmiller 534c3639f5 chore: remove .mokogitea/cascade-dev.yml (moved to .gitea/) [skip ci] 2026-05-16 18:35:39 +00:00
jmiller c7290c0569 chore: move .mokogitea/cascade-dev.yml to .gitea/cascade-dev.yml [skip ci] 2026-05-16 18:35:38 +00:00
jmiller c6d798c5bd chore: remove .mokogitea/auto-release.yml (moved to .gitea/) [skip ci] 2026-05-16 18:35:37 +00:00
jmiller 438b6d18ab chore: move .mokogitea/auto-release.yml to .gitea/auto-release.yml [skip ci] 2026-05-16 18:35:37 +00:00
jmiller 5fd8530a40 chore: remove .mokogitea/ISSUE_TEMPLATE/version.md (moved to .gitea/) [skip ci] 2026-05-16 18:35:36 +00:00
jmiller 4c38eb75e1 chore: move .mokogitea/ISSUE_TEMPLATE/version.md to .gitea/ISSUE_TEMPLATE/version.md [skip ci] 2026-05-16 18:35:35 +00:00
jmiller 8836c867ad chore: remove .mokogitea/ISSUE_TEMPLATE/security.md (moved to .gitea/) [skip ci] 2026-05-16 18:35:35 +00:00
jmiller 84ec0dd06f chore: move .mokogitea/ISSUE_TEMPLATE/security.md to .gitea/ISSUE_TEMPLATE/security.md [skip ci] 2026-05-16 18:35:34 +00:00
jmiller 5ca47190fc chore: remove .mokogitea/ISSUE_TEMPLATE/rfc.md (moved to .gitea/) [skip ci] 2026-05-16 18:35:33 +00:00
jmiller 2cce153087 chore: move .mokogitea/ISSUE_TEMPLATE/rfc.md to .gitea/ISSUE_TEMPLATE/rfc.md [skip ci] 2026-05-16 18:35:33 +00:00
jmiller 7a3f0e7b42 chore: remove .mokogitea/ISSUE_TEMPLATE/question.md (moved to .gitea/) [skip ci] 2026-05-16 18:35:32 +00:00
jmiller 6c245be56a chore: move .mokogitea/ISSUE_TEMPLATE/question.md to .gitea/ISSUE_TEMPLATE/question.md [skip ci] 2026-05-16 18:35:31 +00:00
jmiller aaeb65bca8 chore: remove .mokogitea/ISSUE_TEMPLATE/joomla_issue.md (moved to .gitea/) [skip ci] 2026-05-16 18:35:30 +00:00
jmiller 6cad4b1256 chore: move .mokogitea/ISSUE_TEMPLATE/joomla_issue.md to .gitea/ISSUE_TEMPLATE/joomla_issue.md [skip ci] 2026-05-16 18:35:30 +00:00
jmiller ac75f13a04 chore: remove .mokogitea/ISSUE_TEMPLATE/feature_request.md (moved to .gitea/) [skip ci] 2026-05-16 18:35:29 +00:00
jmiller 299b526572 chore: move .mokogitea/ISSUE_TEMPLATE/feature_request.md to .gitea/ISSUE_TEMPLATE/feature_request.md [skip ci] 2026-05-16 18:35:29 +00:00
jmiller b39c245407 chore: remove .mokogitea/ISSUE_TEMPLATE/documentation.md (moved to .gitea/) [skip ci] 2026-05-16 18:35:28 +00:00
jmiller 01d6ba8148 chore: move .mokogitea/ISSUE_TEMPLATE/documentation.md to .gitea/ISSUE_TEMPLATE/documentation.md [skip ci] 2026-05-16 18:35:27 +00:00
jmiller 1ce005e329 chore: remove .mokogitea/ISSUE_TEMPLATE/config.yml (moved to .gitea/) [skip ci] 2026-05-16 18:35:27 +00:00
jmiller 1bc0f70044 chore: move .mokogitea/ISSUE_TEMPLATE/config.yml to .gitea/ISSUE_TEMPLATE/config.yml [skip ci] 2026-05-16 18:35:26 +00:00
jmiller 566b90d844 chore: remove .mokogitea/ISSUE_TEMPLATE/bug_report.md (moved to .gitea/) [skip ci] 2026-05-16 18:35:25 +00:00
jmiller 40559a92d6 chore: move .mokogitea/ISSUE_TEMPLATE/bug_report.md to .gitea/ISSUE_TEMPLATE/bug_report.md [skip ci] 2026-05-16 18:35:25 +00:00
jmiller 63e90f57f1 chore: remove .mokogitea/ISSUE_TEMPLATE/adr.md (moved to .gitea/) [skip ci] 2026-05-16 18:35:24 +00:00
jmiller c923deb62d chore: move .mokogitea/ISSUE_TEMPLATE/adr.md to .gitea/ISSUE_TEMPLATE/adr.md [skip ci] 2026-05-16 18:35:23 +00:00
jmiller 134e830a34 fix(ci): platform auto-sense, .mokogitea precedence, remove GitHub mirror [skip ci] 2026-05-16 18:27:12 +00:00
jmiller 2f3a1eaf5d fix(ci): auto-release on PR merge + dispatch, bump dev after [skip ci] 2026-05-16 18:00:37 +00:00
jmiller 6d714ee1d2 fix(ci): restrict auto-release to workflow_dispatch only [skip ci] 2026-05-16 17:57:18 +00:00
jmiller 6a7c137d36 fix(ci): deploy auto-release with new version bump protocol [skip ci] 2026-05-16 17:41:43 +00:00
Jonathan Miller 03ee7a95f3 fix(lang): update pretty name to Joomla convention [skip ci]
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-16 09:35:02 -05:00
Jonathan Miller b475e98997 feat(ci): add changelog gate to PR checks [skip ci]
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-16 09:11:25 -05:00
Jonathan Miller 2b475c4e3c chore(ci): version bump targets dev branch instead of main [skip ci]
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-16 09:05:39 -05:00
jmiller 9f15a4ccae chore: remove .mokogitea/.moko-platform [skip ci] 2026-05-12 19:28:40 +00:00
jmiller c3669bc55a chore: move .mokogitea/manifest.xml to .mokogitea/ [skip ci] 2026-05-12 19:28:40 +00:00
jmiller de89f7ab0f chore: force-sync .mokogitea/ISSUE_TEMPLATE/version.md [skip ci] 2026-05-12 19:28:39 +00:00
jmiller 22bb4067d0 chore: force-sync .mokogitea/ISSUE_TEMPLATE/security.md [skip ci] 2026-05-12 19:28:39 +00:00
jmiller 1d922a70e6 chore: force-sync .mokogitea/ISSUE_TEMPLATE/rfc.md [skip ci] 2026-05-12 19:28:38 +00:00
jmiller 5c2b5e132d chore: force-sync .mokogitea/ISSUE_TEMPLATE/question.md [skip ci] 2026-05-12 19:28:38 +00:00
jmiller ca49dc3847 chore: force-sync .mokogitea/ISSUE_TEMPLATE/joomla_issue.md [skip ci] 2026-05-12 19:28:37 +00:00
jmiller 6e7cad59cd chore: force-sync .mokogitea/ISSUE_TEMPLATE/feature_request.md [skip ci] 2026-05-12 19:28:36 +00:00
jmiller b8f99156ff chore: force-sync .mokogitea/ISSUE_TEMPLATE/documentation.md [skip ci] 2026-05-12 19:28:36 +00:00
jmiller 305f0c99ae chore: force-sync .mokogitea/ISSUE_TEMPLATE/config.yml [skip ci] 2026-05-12 19:28:35 +00:00
jmiller 4e7ae81fe5 chore: force-sync .mokogitea/ISSUE_TEMPLATE/bug_report.md [skip ci] 2026-05-12 19:28:35 +00:00
jmiller 5ec700793d chore: force-sync .mokogitea/ISSUE_TEMPLATE/adr.md [skip ci] 2026-05-12 19:28:34 +00:00
jmiller f013c3f6cd chore: force-sync .mokogitea/workflows/update-server.yml [skip ci] 2026-05-12 19:28:34 +00:00
jmiller 905258efbc chore: force-sync .mokogitea/workflows/security-audit.yml [skip ci] 2026-05-12 19:28:33 +00:00
jmiller 2820734346 chore: force-sync .mokogitea/workflows/repo-health.yml [skip ci] 2026-05-12 19:28:32 +00:00
jmiller 27e52450d5 chore: force-sync .mokogitea/workflows/pre-release.yml [skip ci] 2026-05-12 19:28:32 +00:00
jmiller fa173532c3 chore: force-sync .mokogitea/workflows/pr-check.yml [skip ci] 2026-05-12 19:28:31 +00:00
jmiller f0ec07dfe4 chore: force-sync .mokogitea/workflows/notify.yml [skip ci] 2026-05-12 19:28:31 +00:00
jmiller 32c71c2af8 chore: force-sync .mokogitea/workflows/gitleaks.yml [skip ci] 2026-05-12 19:28:30 +00:00
jmiller 17f7121107 chore: force-sync .mokogitea/workflows/deploy-manual.yml [skip ci] 2026-05-12 19:28:30 +00:00
jmiller 4668c7a520 chore: force-sync .mokogitea/workflows/cleanup.yml [skip ci] 2026-05-12 19:28:29 +00:00
jmiller cfcbf75b99 chore: force-sync .mokogitea/workflows/ci-joomla.yml [skip ci] 2026-05-12 19:28:28 +00:00
jmiller 97c3258f15 chore: force-sync .mokogitea/workflows/cascade-dev.yml [skip ci] 2026-05-12 19:28:28 +00:00
jmiller 9f957b6089 chore: force-sync .mokogitea/ISSUE_TEMPLATE/version.md [skip ci] 2026-05-12 19:28:27 +00:00
jmiller 631af053a5 chore: force-sync .mokogitea/workflows/auto-release.yml [skip ci] 2026-05-12 19:28:27 +00:00
jmiller 0c10d8e853 chore: force-sync .mokogitea/ISSUE_TEMPLATE/security.md [skip ci] 2026-05-12 19:28:26 +00:00
jmiller 2563d43392 chore: force-sync .mokogitea/ISSUE_TEMPLATE/rfc.md [skip ci] 2026-05-12 19:28:26 +00:00
jmiller 5915384416 chore: force-sync .mokogitea/ISSUE_TEMPLATE/question.md [skip ci] 2026-05-12 19:28:25 +00:00
jmiller 729c059273 chore: force-sync .mokogitea/ISSUE_TEMPLATE/joomla_issue.md [skip ci] 2026-05-12 19:28:25 +00:00
jmiller 58701a55d2 chore: force-sync .mokogitea/ISSUE_TEMPLATE/feature_request.md [skip ci] 2026-05-12 19:28:25 +00:00
jmiller 96e5440422 chore: force-sync .mokogitea/ISSUE_TEMPLATE/documentation.md [skip ci] 2026-05-12 19:28:24 +00:00
jmiller bdc9e1c1c4 chore: force-sync .mokogitea/ISSUE_TEMPLATE/config.yml [skip ci] 2026-05-12 19:28:24 +00:00
jmiller 5584090b26 chore: force-sync .mokogitea/ISSUE_TEMPLATE/bug_report.md [skip ci] 2026-05-12 19:28:23 +00:00
jmiller 4d51d7efbc chore: force-sync .mokogitea/ISSUE_TEMPLATE/adr.md [skip ci] 2026-05-12 19:28:23 +00:00
jmiller 66fb8a9bda chore: force-sync .mokogitea/workflows/update-server.yml [skip ci] 2026-05-12 19:28:23 +00:00
jmiller 8d85056fe4 chore: force-sync .mokogitea/workflows/security-audit.yml [skip ci] 2026-05-12 19:28:22 +00:00
jmiller e533b44b4b chore: force-sync .mokogitea/workflows/repo-health.yml [skip ci] 2026-05-12 19:28:22 +00:00
jmiller 45a3a9702c chore: force-sync .mokogitea/workflows/pre-release.yml [skip ci] 2026-05-12 19:28:21 +00:00
jmiller 68df7fcf07 chore: force-sync .mokogitea/workflows/pr-check.yml [skip ci] 2026-05-12 19:28:21 +00:00
jmiller 3a28c437c6 chore: force-sync .mokogitea/workflows/notify.yml [skip ci] 2026-05-12 19:28:21 +00:00
jmiller 2ec124cc3f chore: force-sync .mokogitea/workflows/gitleaks.yml [skip ci] 2026-05-12 19:28:20 +00:00
jmiller a9a4b8ecdd chore: force-sync .mokogitea/workflows/deploy-manual.yml [skip ci] 2026-05-12 19:28:20 +00:00
jmiller bb154cb76d chore: force-sync .mokogitea/workflows/cleanup.yml [skip ci] 2026-05-12 19:28:19 +00:00
jmiller f94b54fe5b chore: force-sync .mokogitea/workflows/ci-joomla.yml [skip ci] 2026-05-12 19:28:19 +00:00
jmiller 82aaa6a0ec chore: force-sync .mokogitea/workflows/cascade-dev.yml [skip ci] 2026-05-12 19:28:19 +00:00
jmiller 43b9599393 chore: force-sync .mokogitea/workflows/auto-release.yml [skip ci] 2026-05-12 19:28:18 +00:00
jmiller f68df52a54 chore: sync .mokogitea/ISSUE_TEMPLATE/version.md from template [skip ci] 2026-05-12 18:58:01 +00:00
jmiller ac935ccdba chore: sync .mokogitea/ISSUE_TEMPLATE/security.md from template [skip ci] 2026-05-12 18:58:00 +00:00
jmiller f96ef7c57f chore: sync .mokogitea/ISSUE_TEMPLATE/rfc.md from template [skip ci] 2026-05-12 18:58:00 +00:00
jmiller 627fe4466a chore: sync .mokogitea/ISSUE_TEMPLATE/question.md from template [skip ci] 2026-05-12 18:58:00 +00:00
jmiller 77b79a4f00 chore: sync .mokogitea/ISSUE_TEMPLATE/joomla_issue.md from template [skip ci] 2026-05-12 18:57:59 +00:00
jmiller 62cd0ed6c4 chore: sync .mokogitea/ISSUE_TEMPLATE/feature_request.md from template [skip ci] 2026-05-12 18:57:59 +00:00
jmiller cd9fb32f2c chore: sync .mokogitea/ISSUE_TEMPLATE/documentation.md from template [skip ci] 2026-05-12 18:57:58 +00:00
jmiller d09753c295 chore: sync .mokogitea/ISSUE_TEMPLATE/config.yml from template [skip ci] 2026-05-12 18:57:58 +00:00
jmiller 9744d1d060 chore: sync .mokogitea/ISSUE_TEMPLATE/bug_report.md from template [skip ci] 2026-05-12 18:57:58 +00:00
jmiller 0252656596 chore: sync .mokogitea/ISSUE_TEMPLATE/adr.md from template [skip ci] 2026-05-12 18:57:57 +00:00
jmiller 3ca8f993ff chore: sync .mokogitea/workflows/update-server.yml from template [skip ci] 2026-05-12 18:57:57 +00:00
jmiller e3ac488127 chore: sync .mokogitea/workflows/security-audit.yml from template [skip ci] 2026-05-12 18:57:57 +00:00
jmiller 71274012b0 chore: sync .mokogitea/workflows/repo-health.yml from template [skip ci] 2026-05-12 18:57:56 +00:00
jmiller 276ffecbe5 chore: sync .mokogitea/workflows/pre-release.yml from template [skip ci] 2026-05-12 18:57:56 +00:00
jmiller 3b6f2b0598 chore: sync .mokogitea/workflows/pr-check.yml from template [skip ci] 2026-05-12 18:57:55 +00:00
jmiller 34382a0ef1 chore: sync .mokogitea/workflows/notify.yml from template [skip ci] 2026-05-12 18:57:55 +00:00
jmiller 3038a2d500 chore: sync .mokogitea/workflows/gitleaks.yml from template [skip ci] 2026-05-12 18:57:55 +00:00
jmiller 15449b211a chore: sync .mokogitea/workflows/deploy-manual.yml from template [skip ci] 2026-05-12 18:57:54 +00:00
jmiller 4150461686 chore: sync .mokogitea/workflows/cleanup.yml from template [skip ci] 2026-05-12 18:57:54 +00:00
jmiller 978301ccd7 chore: sync .mokogitea/workflows/ci-joomla.yml from template [skip ci] 2026-05-12 18:57:54 +00:00
jmiller 29cae8a1f6 chore: sync .mokogitea/workflows/cascade-dev.yml from template [skip ci] 2026-05-12 18:57:53 +00:00
jmiller fa38ccd379 chore: sync .mokogitea/workflows/auto-release.yml from template [skip ci] 2026-05-12 18:57:53 +00:00
jmiller 6eab825dc2 chore: sync .mokogitea/ISSUE_TEMPLATE/version.md from template [skip ci] 2026-05-12 18:57:52 +00:00
jmiller cbc9b4415f chore: sync .mokogitea/ISSUE_TEMPLATE/security.md from template [skip ci] 2026-05-12 18:57:51 +00:00
jmiller d9ee30e429 chore: sync .mokogitea/ISSUE_TEMPLATE/rfc.md from template [skip ci] 2026-05-12 18:57:51 +00:00
jmiller 3e4ec51e75 chore: sync .mokogitea/ISSUE_TEMPLATE/question.md from template [skip ci] 2026-05-12 18:57:50 +00:00
jmiller 059e5c2632 chore: sync .mokogitea/ISSUE_TEMPLATE/joomla_issue.md from template [skip ci] 2026-05-12 18:57:50 +00:00
jmiller b34e7e371e chore: sync .mokogitea/ISSUE_TEMPLATE/feature_request.md from template [skip ci] 2026-05-12 18:57:49 +00:00
jmiller c157347ec6 chore: sync .mokogitea/ISSUE_TEMPLATE/documentation.md from template [skip ci] 2026-05-12 18:57:49 +00:00
jmiller aebdac71fd chore: sync .mokogitea/ISSUE_TEMPLATE/config.yml from template [skip ci] 2026-05-12 18:57:48 +00:00
jmiller aa952f9fe5 chore: sync .mokogitea/ISSUE_TEMPLATE/bug_report.md from template [skip ci] 2026-05-12 18:57:48 +00:00
jmiller c3c71329bb chore: sync .mokogitea/ISSUE_TEMPLATE/adr.md from template [skip ci] 2026-05-12 18:57:47 +00:00
jmiller 0af5b35de1 chore: sync .mokogitea/workflows/update-server.yml from template [skip ci] 2026-05-12 18:57:46 +00:00
jmiller 1b6a545a0a chore: sync .mokogitea/workflows/security-audit.yml from template [skip ci] 2026-05-12 18:57:46 +00:00
jmiller 2948d316ff chore: sync .mokogitea/workflows/repo-health.yml from template [skip ci] 2026-05-12 18:57:45 +00:00
jmiller d59f99c476 chore: sync .mokogitea/workflows/pre-release.yml from template [skip ci] 2026-05-12 18:57:45 +00:00
jmiller 6001afdc76 chore: sync .mokogitea/workflows/pr-check.yml from template [skip ci] 2026-05-12 18:57:44 +00:00
jmiller 0a14377c72 chore: sync .mokogitea/workflows/notify.yml from template [skip ci] 2026-05-12 18:57:44 +00:00
jmiller 7c3f0f68f7 chore: sync .mokogitea/workflows/gitleaks.yml from template [skip ci] 2026-05-12 18:57:43 +00:00
jmiller e8934f42bd chore: sync .mokogitea/workflows/deploy-manual.yml from template [skip ci] 2026-05-12 18:57:42 +00:00
jmiller 682942ea2b chore: sync .mokogitea/workflows/cleanup.yml from template [skip ci] 2026-05-12 18:57:42 +00:00
jmiller ee867ce409 chore: sync .mokogitea/workflows/ci-joomla.yml from template [skip ci] 2026-05-12 18:57:41 +00:00
jmiller bb57e95538 chore: sync .mokogitea/workflows/cascade-dev.yml from template [skip ci] 2026-05-12 18:57:41 +00:00
jmiller e93438c2a6 chore: sync .mokogitea/workflows/auto-release.yml from template [skip ci] 2026-05-12 18:57:40 +00:00
jmiller a94d9de3b8 chore: remove .gitea/workflows/update-server.yml (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:54 +00:00
jmiller 1437d529fd chore: move .gitea/workflows/update-server.yml to .mokogitea/update-server.yml [skip ci] 2026-05-12 05:12:53 +00:00
jmiller 2934bab5ee chore: remove .gitea/workflows/security-audit.yml (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:53 +00:00
jmiller f8d815e79b chore: move .gitea/workflows/security-audit.yml to .mokogitea/security-audit.yml [skip ci] 2026-05-12 05:12:52 +00:00
jmiller 7c96e10554 chore: remove .gitea/workflows/repo-health.yml (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:51 +00:00
jmiller 35b3e2df8f chore: move .gitea/workflows/repo-health.yml to .mokogitea/repo-health.yml [skip ci] 2026-05-12 05:12:51 +00:00
jmiller 3168a7a9d3 chore: remove .gitea/workflows/pre-release.yml (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:50 +00:00
jmiller 88f8024667 chore: move .gitea/workflows/pre-release.yml to .mokogitea/pre-release.yml [skip ci] 2026-05-12 05:12:50 +00:00
jmiller 42b85593ce chore: remove .gitea/workflows/pr-check.yml (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:49 +00:00
jmiller db8850ac83 chore: move .gitea/workflows/pr-check.yml to .mokogitea/pr-check.yml [skip ci] 2026-05-12 05:12:49 +00:00
jmiller 9ed0818044 chore: remove .gitea/workflows/pr-branch-check.yml (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:48 +00:00
jmiller bcd94747e0 chore: move .gitea/workflows/pr-branch-check.yml to .mokogitea/pr-branch-check.yml [skip ci] 2026-05-12 05:12:48 +00:00
jmiller 4b3f487d7a chore: remove .gitea/workflows/notify.yml (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:47 +00:00
jmiller 2dd0361538 chore: move .gitea/workflows/notify.yml to .mokogitea/notify.yml [skip ci] 2026-05-12 05:12:46 +00:00
jmiller bfc698039b chore: remove .gitea/workflows/gitleaks.yml (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:46 +00:00
jmiller 59fb68c828 chore: move .gitea/workflows/gitleaks.yml to .mokogitea/gitleaks.yml [skip ci] 2026-05-12 05:12:45 +00:00
jmiller c3722b30da chore: remove .gitea/workflows/deploy-manual.yml (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:45 +00:00
jmiller e1b1a90ee1 chore: move .gitea/workflows/deploy-manual.yml to .mokogitea/deploy-manual.yml [skip ci] 2026-05-12 05:12:44 +00:00
jmiller f1468db2bb chore: remove .gitea/workflows/cleanup.yml (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:43 +00:00
jmiller 53910d5eb9 chore: move .gitea/workflows/cleanup.yml to .mokogitea/cleanup.yml [skip ci] 2026-05-12 05:12:43 +00:00
jmiller 923554eff8 chore: remove .gitea/workflows/ci-joomla.yml (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:42 +00:00
jmiller f501d50e34 chore: move .gitea/workflows/ci-joomla.yml to .mokogitea/ci-joomla.yml [skip ci] 2026-05-12 05:12:42 +00:00
jmiller 828bed1f94 chore: remove .gitea/workflows/cascade-dev.yml (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:41 +00:00
jmiller 89301db510 chore: move .gitea/workflows/cascade-dev.yml to .mokogitea/cascade-dev.yml [skip ci] 2026-05-12 05:12:41 +00:00
jmiller 2bd075d072 chore: remove .gitea/workflows/auto-release.yml (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:40 +00:00
jmiller 19e6c47d6f chore: move .gitea/workflows/auto-release.yml to .mokogitea/auto-release.yml [skip ci] 2026-05-12 05:12:39 +00:00
jmiller 4d5b239c20 chore: remove .gitea/.moko-platform (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:39 +00:00
jmiller 9e84f0c410 chore: remove .github/pull_request_template.md (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:38 +00:00
jmiller 7907500b70 chore: move .gitea/.moko-platform to .mokogitea/.moko-platform [skip ci] 2026-05-12 05:12:38 +00:00
jmiller c02bf3ea70 chore: move .github/pull_request_template.md to .mokogitea/pull_request_template.md [skip ci] 2026-05-12 05:12:37 +00:00
jmiller e2aabae598 chore: remove .github/copilot-instructions.md (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:37 +00:00
jmiller cb7e5771c0 chore: move .github/copilot-instructions.md to .mokogitea/copilot-instructions.md [skip ci] 2026-05-12 05:12:36 +00:00
jmiller 064f77867b chore: remove .github/ISSUE_TEMPLATE/security_review.md (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:36 +00:00
jmiller ae1c9bb04c chore: move .github/ISSUE_TEMPLATE/security_review.md to .mokogitea/security_review.md [skip ci] 2026-05-12 05:12:36 +00:00
jmiller c20c0b2504 chore: remove .github/ISSUE_TEMPLATE/security.md (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:35 +00:00
jmiller e6c2513ef8 chore: move .github/ISSUE_TEMPLATE/security.md to .mokogitea/security.md [skip ci] 2026-05-12 05:12:35 +00:00
jmiller 998d35c887 chore: remove .github/ISSUE_TEMPLATE/runbook.md (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:35 +00:00
jmiller 2cf88f917f chore: move .github/ISSUE_TEMPLATE/runbook.md to .mokogitea/runbook.md [skip ci] 2026-05-12 05:12:34 +00:00
jmiller 87d04690c0 chore: remove .github/ISSUE_TEMPLATE/risk_register_entry.md (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:34 +00:00
jmiller c336713df4 chore: move .github/ISSUE_TEMPLATE/risk_register_entry.md to .mokogitea/risk_register_entry.md [skip ci] 2026-05-12 05:12:33 +00:00
jmiller e982f3afdb chore: remove .github/ISSUE_TEMPLATE/rfc.md (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:33 +00:00
jmiller 308f24c347 chore: move .github/ISSUE_TEMPLATE/rfc.md to .mokogitea/rfc.md [skip ci] 2026-05-12 05:12:33 +00:00
jmiller 14cdea40a9 chore: remove .github/ISSUE_TEMPLATE/request-license.md (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:32 +00:00
jmiller 2f56a74c55 chore: move .github/ISSUE_TEMPLATE/request-license.md to .mokogitea/request-license.md [skip ci] 2026-05-12 05:12:32 +00:00
jmiller 6504ff6ed4 chore: remove .github/ISSUE_TEMPLATE/question.md (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:31 +00:00
jmiller 65958dc595 chore: move .github/ISSUE_TEMPLATE/question.md to .mokogitea/question.md [skip ci] 2026-05-12 05:12:31 +00:00
jmiller d525bbb35b chore: remove .github/ISSUE_TEMPLATE/migration_plan.md (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:31 +00:00
jmiller b77099227d chore: move .github/ISSUE_TEMPLATE/migration_plan.md to .mokogitea/migration_plan.md [skip ci] 2026-05-12 05:12:30 +00:00
jmiller 89947e2444 chore: remove .github/ISSUE_TEMPLATE/joomla_issue.md (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:30 +00:00
jmiller fcfa520f0a chore: move .github/ISSUE_TEMPLATE/joomla_issue.md to .mokogitea/joomla_issue.md [skip ci] 2026-05-12 05:12:29 +00:00
jmiller 598944e8a8 chore: remove .github/ISSUE_TEMPLATE/incident_report.md (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:29 +00:00
jmiller 94466ca09c chore: move .github/ISSUE_TEMPLATE/incident_report.md to .mokogitea/incident_report.md [skip ci] 2026-05-12 05:12:29 +00:00
jmiller 4871d5a55c chore: remove .github/ISSUE_TEMPLATE/firewall-request.md (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:28 +00:00
jmiller 634ef50c58 chore: move .github/ISSUE_TEMPLATE/firewall-request.md to .mokogitea/firewall-request.md [skip ci] 2026-05-12 05:12:28 +00:00
jmiller 26015fd63c chore: remove .github/ISSUE_TEMPLATE/feature_request.md (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:28 +00:00
jmiller 3a21267ea5 chore: move .github/ISSUE_TEMPLATE/feature_request.md to .mokogitea/feature_request.md [skip ci] 2026-05-12 05:12:27 +00:00
jmiller 3366a54709 chore: remove .github/ISSUE_TEMPLATE/escalation.md (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:27 +00:00
jmiller 81ff44cbd4 chore: move .github/ISSUE_TEMPLATE/escalation.md to .mokogitea/escalation.md [skip ci] 2026-05-12 05:12:26 +00:00
jmiller 5e4d102af4 chore: remove .github/ISSUE_TEMPLATE/enterprise_support.md (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:26 +00:00
jmiller a116bc5933 chore: move .github/ISSUE_TEMPLATE/enterprise_support.md to .mokogitea/enterprise_support.md [skip ci] 2026-05-12 05:12:26 +00:00
jmiller 224884750f chore: remove .github/ISSUE_TEMPLATE/documentation_change.md (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:25 +00:00
jmiller 2250452847 chore: move .github/ISSUE_TEMPLATE/documentation_change.md to .mokogitea/documentation_change.md [skip ci] 2026-05-12 05:12:25 +00:00
jmiller 76387f4414 chore: remove .github/ISSUE_TEMPLATE/documentation.md (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:25 +00:00
jmiller d8e7be8e8b chore: move .github/ISSUE_TEMPLATE/documentation.md to .mokogitea/documentation.md [skip ci] 2026-05-12 05:12:24 +00:00
jmiller 76ab32519d chore: remove .github/ISSUE_TEMPLATE/deployment_plan.md (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:24 +00:00
jmiller 17db5ec9e0 chore: move .github/ISSUE_TEMPLATE/deployment_plan.md to .mokogitea/deployment_plan.md [skip ci] 2026-05-12 05:12:23 +00:00
jmiller 406626ac3a chore: remove .github/ISSUE_TEMPLATE/config.yml (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:23 +00:00
jmiller 1b967eb195 chore: move .github/ISSUE_TEMPLATE/config.yml to .mokogitea/config.yml [skip ci] 2026-05-12 05:12:23 +00:00
jmiller 7476e18bb3 chore: remove .github/ISSUE_TEMPLATE/bug_report.md (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:22 +00:00
jmiller ff354831ca chore: move .github/ISSUE_TEMPLATE/bug_report.md to .mokogitea/bug_report.md [skip ci] 2026-05-12 05:12:22 +00:00
jmiller 5af75fe5a4 chore: remove .github/ISSUE_TEMPLATE/adr.md (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:21 +00:00
jmiller f11d3567b7 chore: move .github/ISSUE_TEMPLATE/adr.md to .mokogitea/adr.md [skip ci] 2026-05-12 05:12:21 +00:00
jmiller a27c0acbc1 chore: remove .github/CODEOWNERS (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:20 +00:00
jmiller a83903c48e chore: move .github/CODEOWNERS to .mokogitea/CODEOWNERS [skip ci] 2026-05-12 05:12:20 +00:00
jmiller e404a217a6 chore: remove .github/CLAUDE.md (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:20 +00:00
jmiller 2e94f278a1 chore: move .github/CLAUDE.md to .mokogitea/CLAUDE.md [skip ci] 2026-05-12 05:12:19 +00:00
jmiller 623256c9c2 chore: remove .github/.mokostandards (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:19 +00:00
jmiller 49227e8561 chore: move .github/.mokostandards to .mokogitea/.mokostandards [skip ci] 2026-05-12 05:12:19 +00:00
jmiller 2f97c9da73 chore: remove .gitea/workflows/update-server.yml (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:18 +00:00
jmiller d747abb7d3 chore: move .gitea/workflows/update-server.yml to .mokogitea/update-server.yml [skip ci] 2026-05-12 05:12:18 +00:00
jmiller cddd802645 chore: remove .gitea/workflows/security-audit.yml (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:17 +00:00
jmiller 2bc9f05929 chore: move .gitea/workflows/security-audit.yml to .mokogitea/security-audit.yml [skip ci] 2026-05-12 05:12:17 +00:00
jmiller d2b16d688f chore: remove .gitea/workflows/repo-health.yml (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:17 +00:00
jmiller 2591555a18 chore: move .gitea/workflows/repo-health.yml to .mokogitea/repo-health.yml [skip ci] 2026-05-12 05:12:16 +00:00
jmiller c6861ad59e chore: remove .gitea/workflows/release.yml (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:16 +00:00
jmiller fc5266e58e chore: move .gitea/workflows/release.yml to .mokogitea/release.yml [skip ci] 2026-05-12 05:12:15 +00:00
jmiller e0dbdd9dea chore: remove .gitea/workflows/pre-release.yml (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:15 +00:00
jmiller 1358407e7e chore: move .gitea/workflows/pre-release.yml to .mokogitea/pre-release.yml [skip ci] 2026-05-12 05:12:15 +00:00
jmiller b14d7d8d2f chore: remove .gitea/workflows/pr-check.yml (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:14 +00:00
jmiller 4ccb75b618 chore: move .gitea/workflows/pr-check.yml to .mokogitea/pr-check.yml [skip ci] 2026-05-12 05:12:14 +00:00
jmiller a352a53f3e chore: remove .gitea/workflows/pr-branch-check.yml (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:14 +00:00
jmiller 958714108d chore: move .gitea/workflows/pr-branch-check.yml to .mokogitea/pr-branch-check.yml [skip ci] 2026-05-12 05:12:13 +00:00
jmiller c2321decf6 chore: remove .gitea/workflows/notify.yml (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:13 +00:00
jmiller 9153e7e7df chore: move .gitea/workflows/notify.yml to .mokogitea/notify.yml [skip ci] 2026-05-12 05:12:12 +00:00
jmiller 7f6cb5ac10 chore: remove .gitea/workflows/gitleaks.yml (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:12 +00:00
jmiller 3101d52142 chore: move .gitea/workflows/gitleaks.yml to .mokogitea/gitleaks.yml [skip ci] 2026-05-12 05:12:12 +00:00
jmiller 9203f8ac04 chore: remove .gitea/workflows/deploy-manual.yml (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:11 +00:00
jmiller 07f18b028e chore: move .gitea/workflows/deploy-manual.yml to .mokogitea/deploy-manual.yml [skip ci] 2026-05-12 05:12:11 +00:00
jmiller 0829d3cf95 chore: remove .gitea/workflows/cleanup.yml (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:11 +00:00
jmiller e4cfbb4249 chore: move .gitea/workflows/cleanup.yml to .mokogitea/cleanup.yml [skip ci] 2026-05-12 05:12:10 +00:00
jmiller a82d51ab97 chore: remove .gitea/workflows/ci-joomla.yml (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:10 +00:00
jmiller cae1b72ccb chore: move .gitea/workflows/ci-joomla.yml to .mokogitea/ci-joomla.yml [skip ci] 2026-05-12 05:12:10 +00:00
jmiller e206c4cc5e chore: remove .gitea/workflows/cascade-dev.yml (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:09 +00:00
jmiller e5c6b09f94 chore: move .gitea/workflows/cascade-dev.yml to .mokogitea/cascade-dev.yml [skip ci] 2026-05-12 05:12:09 +00:00
jmiller 144e64cad6 chore: remove .gitea/workflows/auto-release.yml (moved to .mokogitea/) [skip ci] 2026-05-12 05:12:08 +00:00
jmiller a821b31d71 chore: move .gitea/workflows/auto-release.yml to .mokogitea/auto-release.yml [skip ci] 2026-05-12 05:12:08 +00:00
jmiller b8be68da79 chore: sync security-audit.yml from Template-Joomla [skip ci] 2026-05-11 21:28:14 +00:00
jmiller 39509c3acf chore: sync repo-health.yml from Template-Joomla [skip ci] 2026-05-11 21:28:13 +00:00
jmiller 2a3b11a5d4 chore: sync pre-release.yml from Template-Joomla [skip ci] 2026-05-11 21:28:12 +00:00
jmiller 4eeb559bc7 chore: sync pr-check.yml from Template-Joomla [skip ci] 2026-05-11 21:28:10 +00:00
jmiller a2fd3ce157 chore: sync pr-branch-check.yml from Template-Joomla [skip ci] 2026-05-11 21:28:08 +00:00
jmiller e0bf218ec9 chore: sync notify.yml from Template-Joomla [skip ci] 2026-05-11 21:28:07 +00:00
jmiller c4691f9134 chore: sync gitleaks.yml from Template-Joomla [skip ci] 2026-05-11 21:28:05 +00:00
jmiller 40648923a1 chore: sync deploy-manual.yml from Template-Joomla [skip ci] 2026-05-11 21:28:04 +00:00
jmiller 089c29aafa chore: sync cleanup.yml from Template-Joomla [skip ci] 2026-05-11 21:28:02 +00:00
jmiller a1a3dee5ba chore: sync ci-joomla.yml from Template-Joomla [skip ci] 2026-05-11 21:28:00 +00:00
jmiller 14d9fe07e9 chore: sync cascade-dev.yml from Template-Joomla [skip ci] 2026-05-11 21:27:58 +00:00
jmiller d199ad7b8c chore: sync auto-release.yml from Template-Joomla [skip ci] 2026-05-11 21:27:56 +00:00
jmiller c4099a150f chore: sync pre-release.yml from Template-Joomla [skip ci] 2026-05-11 21:22:49 +00:00
jmiller 46cc6f2c4d chore: sync notify.yml from Template-Joomla [skip ci] 2026-05-11 21:22:46 +00:00
jmiller 5014a0ec02 chore: sync auto-release.yml from Template-Joomla [skip ci] 2026-05-11 21:22:39 +00:00
jmiller 525df19ca2 chore: sync update-server.yml from Template-Joomla [skip ci] 2026-05-11 21:18:01 +00:00
jmiller 4c72ddfb3d chore: add PR branch policy check workflow [skip ci] 2026-05-11 17:15:52 +00:00
jmiller 2ef5fdf3c5 chore: sync auto-release.yml with changelog+SHA+pretty-name fixes [skip ci] 2026-05-11 16:43:33 +00:00
jmiller d54265ca9f chore: remove renovate.json [skip ci] 2026-05-10 19:57:21 +00:00
jmiller 92994bcfec docs: add CLAUDE.md for Claude Code context [skip ci] 2026-05-10 19:55:17 +00:00
jmiller 09a3a1aa95 chore: add .moko-platform manifest [skip ci] 2026-05-10 19:51:07 +00:00
jmiller 78abc4e7e0 chore: remove deprecated .mokostandards (now .moko-platform) [skip ci] 2026-05-10 19:48:56 +00:00
jmiller 16ebfabd4e docs: update README from wiki Home
Cascade Main → Dev / Cascade main → branches (push) Successful in 2s
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
Repository Cleanup / Clean Merged Branches (push) Successful in 5s
Secret Scanning / Gitleaks Secret Scan (push) Successful in 5s
Security Audit / Dependency Audit (push) Successful in 3s
2026-05-10 18:39:21 +00:00
Jonathan Miller 8d8dfe5b28 ci: add PHPStan static analysis to CI pipeline [skip ci] 2026-05-07 15:10:27 -05:00
Moko Standards Bot 8c2022e041 ci: add Gitleaks secret scanning + Renovate dependency config [skip ci] 2026-05-07 15:00:00 -05:00
Jonathan Miller de824b2752 ci: cascade v2 — forward-merge main to all open branches [skip ci] 2026-05-07 14:35:14 -05:00
jmiller 39637b1004 ci: add cascade main → dev workflow [skip ci] 2026-05-05 16:55:04 -05:00
Jonathan Miller 2c8e7d7693 chore: set platform to joomla
Repo Health / Access control (push) Successful in 2s
Repo Health / Release configuration (push) Failing after 4s
Repo Health / Scripts governance (push) Successful in 3s
Repo Health / Repository health (push) Failing after 4s
Repository Cleanup / Clean Merged Branches (push) Successful in 4s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-05 15:24:03 -05:00
Jonathan Miller 9c5149bb10 fix: WORKFLOWS_DIR .github → .gitea
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 4s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-05 15:00:21 -05:00
Jonathan Miller df2087c51f fix: stable release = minor bump (not major)
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-05 14:39:04 -05:00
Jonathan Miller ef3a3be171 fix: include element name in stable release title
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
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-04 13:52:55 -05:00
Jonathan Miller 4358e1c66b feat: cascade delete lesser pre-releases on promotion
Repo Health / Access control (push) Successful in 1s
Repo Health / Release configuration (push) Failing after 2s
Repo Health / Repository health (push) Failing after 3s
Repo Health / Scripts governance (push) Successful in 3s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-04 13:37:05 -05:00
Jonathan Miller cfc0d9b68f fix: stable release = major version bump (XX+1.00.00)
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
Repository Cleanup / Clean Merged Branches (push) Successful in 25s
Security Audit / Dependency Audit (push) Successful in 3s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-02 18:45:05 -05:00
Jonathan Miller 77f53d3347 fix: version bump logic — stable=minor, pre-release=patch
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 2s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-02 18:31:27 -05:00
gitea-actions[bot] fb38029872 chore: enrich .mokostandards with build/deploy/scripts
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
2026-05-02 18:12:47 -05:00
gitea-actions[bot] 2385213400 chore: add XML .mokostandards manifest
Repo Health / Access control (push) Has been cancelled
Repo Health / Release configuration (push) Has been cancelled
Repo Health / Scripts governance (push) Has been cancelled
Repo Health / Repository health (push) Has been cancelled
2026-05-02 18:05:35 -05:00
Jonathan Miller 39eea64996 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:08 -05:00
Jonathan Miller b1f111bc91 chore: remove auto-deploy workflow (deploy is manual only)
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 2s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-02 16:42:27 -05:00
Jonathan Miller f30d9e53f8 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 2s
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:26 -05:00
Jonathan Miller e8a38bfe9e 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 2s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-02 16:30:22 -05:00
Jonathan Miller fb1944bc15 chore: sync workflows to MokoOnyx standard (6 workflows)
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
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-02 16:25:52 -05:00
Jonathan Miller 95132d8f85 fix: overwrite release instead of prepending version history [skip ci] 2026-05-02 15:57:54 -05:00
jmiller ff2fcb7b5c chore: sync updates.xml stable 02.01.24 [skip ci] 2026-05-01 16:40:08 +00:00
gitea-actions[bot] b3f2a61251 chore: update stable SHA-256 for 02.01.24 [skip ci] 2026-05-01 16:40:08 +00:00
gitea-actions[bot] 9833749796 chore(version): bump 02.01.23 → 02.01.24 [skip ci] 2026-05-01 16:40:06 +00:00
Jonathan Miller 3582ab0908 fix(build): remove empty composer.json breaking release workflow
The empty file passed the existence check but failed JSON parsing,
preventing the release ZIP from being built.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-01 11:38:46 -05:00
gitea-actions[bot] 1df3c49312 chore(version): bump 02.01.22 → 02.01.23 [skip ci] 2026-05-01 16:34:47 +00:00
gitea-actions[bot] fd1ccad779 fix(ci): canonical updates.xml format + in-place update + create missing [skip ci] 2026-04-30 10:26:15 -05:00
gitea-actions[bot] 72dfc47cf4 chore(version): bump 02.01.21 → 02.01.22 [skip ci] 2026-04-30 15:23:55 +00:00
Jonathan Miller 1712291df1 feat: add production-ready site reset maintenance actions
Create Release / Build Release Package (push) Failing after 3s
Add 8 new one-shot maintenance toggles for resetting a site after
development: reset votes, normalize publish dates, clear action logs,
clear redirect logs, clear user notes, flush mail queue, flush sessions,
and set robots to index/follow.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-30 10:21:53 -05:00
gitea-actions[bot] bc157dbecd fix(ci): manifest version bump reads own version, not README [skip ci] 2026-04-30 10:01:00 -05:00
Jonathan Miller 1c506b0296 chore: chore: cleanup 2026-04-26 23:13:13 -05:00
Jonathan Miller c3c733ef13 chore: remove duplicate .mokostandards from root (canonical location is .gitea/) 2026-04-26 23:07:04 -05:00
gitea-actions[bot] 69a9985403 chore: remove .github/ — all workflows in .gitea/ [skip ci] 2026-04-26 22:54:34 -05:00
Jonathan Miller 36c66418cb chore: replace jmiller-moko with jmiller, move .mokostandards to .gitea/ 2026-04-26 22:38:57 -05:00
gitea-actions[bot] a3dafb82c5 chore: Gitea-only workflows + remove GitHub update server [skip ci] 2026-04-26 21:54:16 -05:00
gitea-actions[bot] 9ff176606f chore: Gitea-only workflows + remove GitHub update server [skip ci] 2026-04-26 21:54:09 -05:00
gitea-actions[bot] d3aebc53ab ci: sync release workflows v2 [skip ci] 2026-04-26 21:48:22 -05:00
gitea-actions[bot] eb942cfb9e fix(ci): extension type compatibility — htdocs fallback, deeper manifest search [skip ci] 2026-04-26 19:36:36 -05:00
gitea-actions[bot] df661625e0 ci: generic release.yml v2 + update-server.yml — stream tags, cascade, manifest auto-detect [skip ci] 2026-04-26 19:22:02 -05:00
Jonathan Miller e4107ab773 chore: add profile.ps1 to .gitignore
Repo Health / Access control (push) Failing after 2s
Standards Compliance / Secret Scanning (push) Failing after 2s
Standards Compliance / License Header Validation (push) Successful in 3s
Standards Compliance / Repository Structure Validation (push) Successful in 3s
Standards Compliance / Coding Standards Check (push) Failing after 4s
Standards Compliance / Workflow Configuration Check (push) Failing after 3s
Standards Compliance / Documentation Quality Check (push) Successful in 3s
Standards Compliance / README Completeness Check (push) Successful in 3s
Standards Compliance / Git Repository Hygiene (push) Successful in 3s
Standards Compliance / Script Integrity Validation (push) Successful in 4s
Standards Compliance / Line Length Check (push) Failing after 3s
Standards Compliance / File Naming Standards (push) Successful in 4s
Standards Compliance / Insecure Code Pattern Detection (push) Successful in 3s
Standards Compliance / Dead Code Detection (push) Successful in 4s
Standards Compliance / File Size Limits (push) Successful in 4s
Standards Compliance / Binary File Detection (push) Successful in 3s
Standards Compliance / TODO/FIXME Tracking (push) Successful in 2s
Standards Compliance / Version Consistency Check (push) Successful in 42s
Standards Compliance / Code Complexity Analysis (push) Successful in 38s
Standards Compliance / Code Duplication Detection (push) Successful in 39s
Standards Compliance / Broken Link Detection (push) Successful in 3s
Standards Compliance / API Documentation Coverage (push) Successful in 3s
Standards Compliance / Accessibility Check (push) Successful in 3s
Standards Compliance / Performance Metrics (push) Successful in 3s
Standards Compliance / Dependency Vulnerability Scanning (push) Successful in 41s
Standards Compliance / Terraform Configuration Validation (push) Successful in 6s
Standards Compliance / Unused Dependencies Check (push) Successful in 45s
Standards Compliance / Enterprise Readiness Check (push) Failing after 40s
Standards Compliance / Repository Health Check (push) Failing after 39s
Update MokoOnyx Payload / update-payload (push) Failing after 31s
Repo Health / Scripts governance (push) Has been skipped
Repo Health / Release configuration (push) Has been skipped
Repo Health / Repository health (push) Has been skipped
Standards Compliance / Compliance Summary (push) Failing after 1s
Auto-Assign Issues & PRs / Assign unassigned issues and PRs (push) Successful in 1s
2026-04-26 16:05:22 -05:00
jmiller 9bec46c446 chore: add TODO.md from MokoStandards
Repo Health / Access control (push) Failing after 2s
Repo Health / Release configuration (push) Has been skipped
Repo Health / Scripts governance (push) Has been skipped
Repo Health / Repository health (push) Has been skipped
2026-04-26 16:35:47 +00:00
jmiller c6a5194096 chore: add TODO.md from MokoStandards
Repo Health / Access control (push) Failing after 2s
Standards Compliance / Secret Scanning (push) Failing after 3s
Standards Compliance / License Header Validation (push) Successful in 3s
Standards Compliance / Repository Structure Validation (push) Successful in 3s
Standards Compliance / Coding Standards Check (push) Failing after 3s
Standards Compliance / Workflow Configuration Check (push) Failing after 3s
Standards Compliance / Documentation Quality Check (push) Successful in 3s
Standards Compliance / README Completeness Check (push) Successful in 3s
Standards Compliance / Git Repository Hygiene (push) Successful in 3s
Standards Compliance / Script Integrity Validation (push) Successful in 4s
Standards Compliance / Line Length Check (push) Failing after 3s
Standards Compliance / File Naming Standards (push) Successful in 3s
Standards Compliance / Insecure Code Pattern Detection (push) Successful in 3s
Standards Compliance / Version Consistency Check (push) Successful in 40s
Standards Compliance / Code Complexity Analysis (push) Successful in 45s
Standards Compliance / Dead Code Detection (push) Successful in 4s
Standards Compliance / File Size Limits (push) Successful in 2s
Standards Compliance / Binary File Detection (push) Successful in 3s
Standards Compliance / TODO/FIXME Tracking (push) Successful in 2s
Standards Compliance / Code Duplication Detection (push) Successful in 43s
Standards Compliance / API Documentation Coverage (push) Successful in 3s
Standards Compliance / Broken Link Detection (push) Successful in 4s
Standards Compliance / Accessibility Check (push) Successful in 3s
Standards Compliance / Performance Metrics (push) Successful in 3s
Standards Compliance / Dependency Vulnerability Scanning (push) Successful in 51s
Standards Compliance / Unused Dependencies Check (push) Successful in 52s
Standards Compliance / Terraform Configuration Validation (push) Successful in 7s
Standards Compliance / Repository Health Check (push) Failing after 47s
Standards Compliance / Enterprise Readiness Check (push) Failing after 50s
Update MokoOnyx Payload / update-payload (push) Failing after 32s
Repo Health / Release configuration (push) Has been skipped
Repo Health / Scripts governance (push) Has been skipped
Repo Health / Repository health (push) Has been skipped
Standards Compliance / Compliance Summary (push) Failing after 2s
2026-04-26 16:35:46 +00:00
jmiller 6f48e4703e chore: sync updates.xml 02.01.21 [skip ci] 2026-04-23 23:02:31 +00:00
gitea-actions[bot] 4ce04d1833 chore(release): ZIP + tar.gz for 02.01.21 [skip ci] 2026-04-23 23:02:30 +00:00
gitea-actions[bot] 98fdb29852 chore(release): build 02.01.21 [skip ci] 2026-04-23 23:02:29 +00:00
jmiller e6ec97a4b6 chore: sync updates.xml from [skip ci] 2026-04-23 22:57:16 +00:00
gitea-actions[bot] 7375c198e6 chore: update updates.xml (development: 02.01.22-dev) [skip ci] 2026-04-23 22:57:15 +00:00
gitea-actions[bot] 932de46177 chore(version): auto-bump patch 02.01.22 [skip ci] 2026-04-23 22:57:14 +00:00
jmiller d073dcb017 fix: auto-release Step 7 tag_exists gate blocking patches (#9)
Repo Health / Access control (push) Failing after 2s
Standards Compliance / Secret Scanning (push) Failing after 2s
Standards Compliance / License Header Validation (push) Successful in 3s
Standards Compliance / Repository Structure Validation (push) Successful in 3s
Standards Compliance / Coding Standards Check (push) Failing after 3s
Standards Compliance / Workflow Configuration Check (push) Failing after 3s
Standards Compliance / Documentation Quality Check (push) Successful in 3s
Standards Compliance / README Completeness Check (push) Successful in 3s
Standards Compliance / Git Repository Hygiene (push) Successful in 3s
Standards Compliance / Script Integrity Validation (push) Successful in 4s
Standards Compliance / Line Length Check (push) Failing after 3s
Standards Compliance / File Naming Standards (push) Successful in 3s
Standards Compliance / Insecure Code Pattern Detection (push) Successful in 3s
Standards Compliance / Dead Code Detection (push) Successful in 4s
Standards Compliance / File Size Limits (push) Successful in 2s
Standards Compliance / Binary File Detection (push) Successful in 3s
Standards Compliance / TODO/FIXME Tracking (push) Successful in 3s
Standards Compliance / Version Consistency Check (push) Successful in 51s
Standards Compliance / Code Complexity Analysis (push) Successful in 50s
Standards Compliance / Code Duplication Detection (push) Successful in 51s
Standards Compliance / Broken Link Detection (push) Successful in 3s
Standards Compliance / API Documentation Coverage (push) Successful in 3s
Standards Compliance / Accessibility Check (push) Successful in 3s
Standards Compliance / Performance Metrics (push) Successful in 2s
Standards Compliance / Dependency Vulnerability Scanning (push) Successful in 48s
Standards Compliance / Terraform Configuration Validation (push) Successful in 6s
Standards Compliance / Unused Dependencies Check (push) Successful in 53s
Repo Health / Release configuration (push) Has been skipped
Repo Health / Scripts governance (push) Has been skipped
Repo Health / Repository health (push) Has been skipped
Standards Compliance / Repository Health Check (push) Failing after 49s
Standards Compliance / Enterprise Readiness Check (push) Failing after 50s
Standards Compliance / Compliance Summary (push) Failing after 1s
Update MokoOnyx Payload / update-payload (push) Failing after 37s
Auto-Assign Issues & PRs / Assign unassigned issues and PRs (push) Successful in 1s
fix: auto-release Step 7 tag_exists gate blocking patches
2026-04-23 22:57:13 +00:00
Jonathan Miller 9c49727018 fix: remove tag_exists gate from auto-release Step 7
Update Joomla Update Server XML Feed / Update updates.xml (push) Successful in 11s
Repo Health / Access control (push) Failing after 2s
Changelog Validation / Validate CHANGELOG.md (pull_request) Failing after 3s
Joomla Extension CI / Release Readiness Check (pull_request) Successful in 3s
Repo Health / Access control (pull_request) Failing after 2s
Standards Compliance / Secret Scanning (pull_request) Failing after 3s
Standards Compliance / License Header Validation (pull_request) Successful in 3s
Standards Compliance / Repository Structure Validation (pull_request) Successful in 3s
Standards Compliance / Coding Standards Check (pull_request) Failing after 4s
Standards Compliance / Workflow Configuration Check (pull_request) Failing after 3s
Standards Compliance / Documentation Quality Check (pull_request) Successful in 3s
Standards Compliance / README Completeness Check (pull_request) Successful in 3s
Standards Compliance / Git Repository Hygiene (pull_request) Successful in 2s
Standards Compliance / Script Integrity Validation (pull_request) Successful in 4s
Standards Compliance / Line Length Check (pull_request) Failing after 3s
Standards Compliance / Insecure Code Pattern Detection (pull_request) Successful in 2s
Standards Compliance / File Naming Standards (pull_request) Successful in 2s
Standards Compliance / Version Consistency Check (pull_request) Successful in 47s
Standards Compliance / Dead Code Detection (pull_request) Successful in 4s
Standards Compliance / File Size Limits (pull_request) Successful in 2s
Joomla Extension CI / Lint & Validate (pull_request) Failing after 1m10s
Joomla Extension CI / Tests (PHP 8.2) (pull_request) Has been skipped
Joomla Extension CI / Tests (PHP 8.3) (pull_request) Has been skipped
Standards Compliance / Binary File Detection (pull_request) Successful in 4s
Standards Compliance / TODO/FIXME Tracking (pull_request) Successful in 3s
Standards Compliance / Code Complexity Analysis (pull_request) Successful in 54s
Standards Compliance / Broken Link Detection (pull_request) Successful in 3s
Standards Compliance / Code Duplication Detection (pull_request) Successful in 58s
Standards Compliance / API Documentation Coverage (pull_request) Successful in 3s
Standards Compliance / Accessibility Check (pull_request) Successful in 3s
Standards Compliance / Performance Metrics (pull_request) Successful in 3s
Standards Compliance / Dependency Vulnerability Scanning (pull_request) Successful in 58s
Standards Compliance / Unused Dependencies Check (pull_request) Successful in 1m0s
Standards Compliance / Terraform Configuration Validation (pull_request) Successful in 5s
Standards Compliance / Enterprise Readiness Check (pull_request) Failing after 50s
Standards Compliance / Repository Health Check (pull_request) Failing after 50s
Sync Version from README / Propagate README version (pull_request) Failing after 36s
Repo Health / Release configuration (push) Has been skipped
Repo Health / Scripts governance (push) Has been skipped
Repo Health / Repository health (push) Has been skipped
Repo Health / Release configuration (pull_request) Has been skipped
Repo Health / Scripts governance (pull_request) Has been skipped
Repo Health / Repository health (pull_request) Has been skipped
Standards Compliance / Compliance Summary (pull_request) Failing after 2s
Step 7 already handles create vs update internally — the tag_exists
gate was preventing patch releases from updating the Gitea release.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 17:56:58 -05:00
jmiller e413a4a6d8 chore: sync updates.xml 02.01.21 [skip ci] 2026-04-23 22:52:09 +00:00
gitea-actions[bot] 13162e26cd chore(release): ZIP + tar.gz for 02.01.21 [skip ci] 2026-04-23 22:52:08 +00:00
gitea-actions[bot] a7cef01ff1 chore(release): build 02.01.21 [skip ci] 2026-04-23 22:52:07 +00:00
jmiller 8cae9b1565 chore: sync updates.xml 02.01.21 [skip ci] 2026-04-23 22:42:07 +00:00
jmiller 4a5b94e510 fix: auto-release skip gate blocking patch releases (#8)
Repo Health / Access control (push) Failing after 2s
Standards Compliance / Secret Scanning (push) Failing after 2s
Standards Compliance / License Header Validation (push) Successful in 3s
Standards Compliance / Repository Structure Validation (push) Successful in 3s
Standards Compliance / Coding Standards Check (push) Failing after 3s
Standards Compliance / Workflow Configuration Check (push) Failing after 3s
Standards Compliance / Documentation Quality Check (push) Successful in 3s
Update MokoOnyx Payload / update-payload (push) Failing after 32s
Standards Compliance / README Completeness Check (push) Successful in 2s
Standards Compliance / Git Repository Hygiene (push) Successful in 3s
Standards Compliance / Script Integrity Validation (push) Successful in 3s
Standards Compliance / Line Length Check (push) Failing after 3s
Standards Compliance / File Naming Standards (push) Successful in 3s
Standards Compliance / Insecure Code Pattern Detection (push) Successful in 2s
Standards Compliance / Version Consistency Check (push) Successful in 46s
Standards Compliance / Dead Code Detection (push) Successful in 4s
Standards Compliance / File Size Limits (push) Successful in 2s
Standards Compliance / TODO/FIXME Tracking (push) Successful in 3s
Standards Compliance / Binary File Detection (push) Successful in 4s
Standards Compliance / Code Duplication Detection (push) Successful in 47s
Standards Compliance / Code Complexity Analysis (push) Successful in 49s
Standards Compliance / Broken Link Detection (push) Successful in 3s
Standards Compliance / API Documentation Coverage (push) Successful in 3s
Standards Compliance / Accessibility Check (push) Successful in 3s
Standards Compliance / Performance Metrics (push) Successful in 3s
Standards Compliance / Dependency Vulnerability Scanning (push) Successful in 50s
Standards Compliance / Unused Dependencies Check (push) Successful in 53s
Standards Compliance / Terraform Configuration Validation (push) Successful in 5s
Repo Health / Release configuration (push) Has been skipped
Repo Health / Scripts governance (push) Has been skipped
Repo Health / Repository health (push) Has been skipped
Standards Compliance / Repository Health Check (push) Failing after 48s
Standards Compliance / Enterprise Readiness Check (push) Failing after 48s
Standards Compliance / Compliance Summary (push) Failing after 1s
fix: auto-release skip gate blocking patch releases
2026-04-23 22:41:51 +00:00
jmiller 466bc72bb9 chore: sync updates.xml 02.01.20 [skip ci] 2026-04-23 22:38:40 +00:00
jmiller d87c0680e8 chore: sync updates.xml 02.01.20 [skip ci] 2026-04-23 20:14:41 +00:00
jmiller 0a7db75f66 chore: sync updates.xml from [skip ci] 2026-04-23 20:14:32 +00:00
jmiller e8e6c93295 fix: remove CSS injection, lock MokoWaaS + MokoOnyx (#7)
Standards Compliance / Secret Scanning (push) Failing after 3s
Standards Compliance / License Header Validation (push) Successful in 2s
Standards Compliance / Repository Structure Validation (push) Successful in 3s
Standards Compliance / Coding Standards Check (push) Failing after 3s
Standards Compliance / Documentation Quality Check (push) Successful in 3s
Standards Compliance / Workflow Configuration Check (push) Failing after 4s
Standards Compliance / README Completeness Check (push) Successful in 3s
Standards Compliance / Git Repository Hygiene (push) Successful in 3s
Standards Compliance / Line Length Check (push) Failing after 3s
Standards Compliance / Script Integrity Validation (push) Successful in 4s
Standards Compliance / File Naming Standards (push) Successful in 3s
Standards Compliance / Insecure Code Pattern Detection (push) Successful in 3s
Standards Compliance / Dead Code Detection (push) Successful in 4s
Standards Compliance / File Size Limits (push) Successful in 3s
Standards Compliance / Binary File Detection (push) Successful in 4s
Standards Compliance / TODO/FIXME Tracking (push) Successful in 2s
Standards Compliance / Version Consistency Check (push) Successful in 48s
Standards Compliance / Code Complexity Analysis (push) Successful in 46s
Standards Compliance / Code Duplication Detection (push) Successful in 47s
Standards Compliance / Broken Link Detection (push) Successful in 3s
Standards Compliance / API Documentation Coverage (push) Successful in 2s
Standards Compliance / Accessibility Check (push) Successful in 3s
Standards Compliance / Performance Metrics (push) Successful in 2s
Standards Compliance / Dependency Vulnerability Scanning (push) Successful in 48s
Standards Compliance / Terraform Configuration Validation (push) Successful in 5s
Standards Compliance / Unused Dependencies Check (push) Successful in 53s
Repo Health / Access control (push) Failing after 2s
Repo Health / Release configuration (push) Has been skipped
Repo Health / Scripts governance (push) Has been skipped
Repo Health / Repository health (push) Has been skipped
Standards Compliance / Enterprise Readiness Check (push) Failing after 50s
Update MokoOnyx Payload / update-payload (push) Failing after 33s
Standards Compliance / Repository Health Check (push) Failing after 51s
Standards Compliance / Compliance Summary (push) Failing after 1s
fix: remove CSS injection, lock MokoWaaS + MokoOnyx
2026-04-23 20:14:20 +00:00
jmiller 2057fc71a0 Release 02.01.21: MokoOnyx switch, cascade channels, docs (#6)
Repo Health / Access control (push) Failing after 2s
Standards Compliance / Secret Scanning (push) Failing after 2s
Standards Compliance / License Header Validation (push) Successful in 2s
Standards Compliance / Repository Structure Validation (push) Successful in 3s
Standards Compliance / Coding Standards Check (push) Failing after 4s
Standards Compliance / Workflow Configuration Check (push) Failing after 3s
Standards Compliance / Documentation Quality Check (push) Successful in 3s
Standards Compliance / README Completeness Check (push) Successful in 3s
Standards Compliance / Git Repository Hygiene (push) Successful in 3s
Standards Compliance / Script Integrity Validation (push) Successful in 4s
Standards Compliance / Line Length Check (push) Failing after 3s
Standards Compliance / File Naming Standards (push) Successful in 2s
Standards Compliance / Insecure Code Pattern Detection (push) Successful in 2s
Standards Compliance / Dead Code Detection (push) Successful in 4s
Standards Compliance / File Size Limits (push) Successful in 3s
Standards Compliance / Binary File Detection (push) Successful in 3s
Standards Compliance / TODO/FIXME Tracking (push) Successful in 3s
Standards Compliance / Version Consistency Check (push) Successful in 48s
Standards Compliance / Code Complexity Analysis (push) Successful in 47s
Standards Compliance / Code Duplication Detection (push) Successful in 46s
Standards Compliance / Broken Link Detection (push) Successful in 3s
Standards Compliance / API Documentation Coverage (push) Successful in 3s
Standards Compliance / Accessibility Check (push) Successful in 2s
Standards Compliance / Performance Metrics (push) Successful in 3s
Standards Compliance / Dependency Vulnerability Scanning (push) Successful in 48s
Standards Compliance / Terraform Configuration Validation (push) Successful in 5s
Standards Compliance / Unused Dependencies Check (push) Successful in 52s
Repo Health / Release configuration (push) Has been skipped
Repo Health / Scripts governance (push) Has been skipped
Repo Health / Repository health (push) Has been skipped
Update MokoOnyx Payload / update-payload (push) Failing after 30s
Standards Compliance / Repository Health Check (push) Failing after 50s
Standards Compliance / Enterprise Readiness Check (push) Failing after 52s
Standards Compliance / Compliance Summary (push) Failing after 1s
Release 02.01.21: MokoOnyx switch, cascade channels, docs
2026-04-23 20:03:23 +00:00
Jonathan Miller 040b22782f ci: auto-update MokoOnyx payload on schedule
Adds cron (6am/6pm UTC), repository_dispatch, and GH_TOKEN checkout.
Payload commits use [skip ci] to avoid recursive triggers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 14:53:46 -05:00
Jonathan Miller b31c6fe190 ci: auto-check MokoOnyx payload twice daily + on dispatch
Adds cron schedule (6am/6pm UTC) and repository_dispatch trigger
so MokoWaaS payload stays current with MokoOnyx stable releases.
Uses GH_TOKEN for checkout and [skip ci] on payload commits.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 14:40:24 -05:00
jmiller e7c371130a chore: sync updates.xml 02.01.20 [skip ci] 2026-04-23 19:34:21 +00:00
jmiller 3a46d20c52 Release 02.01.20 — brand buttons, dev mode, ATS overrides
Standards Compliance / Secret Scanning (push) Failing after 3s
Standards Compliance / Repository Structure Validation (push) Successful in 2s
Standards Compliance / License Header Validation (push) Successful in 2s
Standards Compliance / Coding Standards Check (push) Failing after 3s
Standards Compliance / Workflow Configuration Check (push) Failing after 3s
Standards Compliance / Documentation Quality Check (push) Successful in 3s
Standards Compliance / README Completeness Check (push) Successful in 3s
Standards Compliance / Git Repository Hygiene (push) Successful in 3s
Standards Compliance / Line Length Check (push) Failing after 3s
Standards Compliance / Script Integrity Validation (push) Successful in 3s
Standards Compliance / File Naming Standards (push) Successful in 3s
Standards Compliance / Insecure Code Pattern Detection (push) Successful in 2s
Standards Compliance / Dead Code Detection (push) Successful in 4s
Standards Compliance / File Size Limits (push) Successful in 3s
Standards Compliance / Binary File Detection (push) Successful in 4s
Standards Compliance / TODO/FIXME Tracking (push) Successful in 3s
Standards Compliance / Version Consistency Check (push) Successful in 49s
Standards Compliance / Code Duplication Detection (push) Successful in 43s
Standards Compliance / Broken Link Detection (push) Successful in 3s
Standards Compliance / Code Complexity Analysis (push) Successful in 46s
Standards Compliance / API Documentation Coverage (push) Successful in 3s
Standards Compliance / Accessibility Check (push) Successful in 3s
Standards Compliance / Performance Metrics (push) Successful in 2s
Standards Compliance / Dependency Vulnerability Scanning (push) Successful in 49s
Standards Compliance / Terraform Configuration Validation (push) Successful in 6s
Update MokoOnyx Payload / update-payload (push) Successful in 3s
Repo Health / Access control (push) Failing after 2s
Repo Health / Release configuration (push) Has been skipped
Repo Health / Scripts governance (push) Has been skipped
Repo Health / Repository health (push) Has been skipped
Standards Compliance / Unused Dependencies Check (push) Successful in 53s
Standards Compliance / Enterprise Readiness Check (push) Failing after 47s
Standards Compliance / Repository Health Check (push) Failing after 49s
Standards Compliance / Compliance Summary (push) Failing after 1s
2026-04-23 19:33:59 +00:00
jmiller 0241fe7299 chore: sync updates.xml from [skip ci] 2026-04-23 19:32:03 +00:00
gitea-actions[bot] d6c9387591 chore: update updates.xml (development: 02.01.20-dev) [skip ci]
Build & Release / Build & Release Pipeline (pull_request) Successful in 9s
Sync Version from README / Propagate README version (pull_request) Failing after 41s
2026-04-23 19:32:02 +00:00
gitea-actions[bot] f5f36f37d5 chore(version): auto-bump patch 02.01.20 [skip ci] 2026-04-23 19:32:01 +00:00
Jonathan Miller 50268c715a fix: add updates.xml sync-to-main step in update-server workflow
Repo Health / Access control (push) Failing after 2s
Repo Health / Release configuration (push) Has been skipped
Repo Health / Scripts governance (push) Has been skipped
Repo Health / Repository health (push) Has been skipped
Update Joomla Update Server XML Feed / Update updates.xml (push) Successful in 10s
The workflow was committing updates.xml to the dev branch but never
syncing it to main where Joomla sites read from.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 14:31:29 -05:00
Jonathan Miller 40dc5cce94 feat: add Akeeba Ticket System (ATS) language overrides
Rebrand ATS ticket strings with {{BRAND_NAME}} placeholders across
all four override files (admin + site, en-GB + en-US).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 14:31:11 -05:00
gitea-actions[bot] 83b047e58f chore: update updates.xml (development: 02.01.19-dev) [skip ci] 2026-04-23 19:10:44 +00:00
gitea-actions[bot] 89e73a7557 chore(version): auto-bump patch 02.01.19 [skip ci] 2026-04-23 19:10:43 +00:00
Jonathan Miller bed8c304ba feat: extend MokoWaaS plugin capabilities
Repo Health / Access control (push) Failing after 1s
Update Joomla Update Server XML Feed / Update updates.xml (push) Successful in 9s
Repo Health / Release configuration (push) Has been skipped
Repo Health / Scripts governance (push) Has been skipped
Repo Health / Repository health (push) Has been skipped
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 14:10:12 -05:00
Jonathan Miller 4374e3b382 fix: use updated update-server template with push triggers + bare dev support
Repo Health / Access control (push) Failing after 1s
Repo Health / Release configuration (push) Has been skipped
Repo Health / Scripts governance (push) Has been skipped
Repo Health / Repository health (push) Has been skipped
- Replace dev-release.yml with fixed update-server.yml that supports
  push triggers and bare 'dev' branch (synced from MokoStandards-API)
- Template now handles cascade channels for all release streams

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 13:07:59 -05:00
Jonathan Miller d252f7c358 feat: add dev-release workflow with cascade channel pattern
Repo Health / Access control (push) Failing after 2s
Repo Health / Release configuration (push) Has been skipped
Repo Health / Scripts governance (push) Has been skipped
Repo Health / Repository health (push) Has been skipped
- Build ZIP and upload to Gitea development release on push to dev
- Sync updates.xml to main via API (development channel only)
- Parse manifest metadata dynamically (not hardcoded)
- Document cascade pattern: dev→dev, alpha→dev+alpha, etc.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 12:27:05 -05:00
Jonathan Miller fccaf67024 feat: always install MokoOnyx and unlock MokoCassiopeia on update
Repo Health / Access control (push) Failing after 2s
Repo Health / Release configuration (push) Has been skipped
Repo Health / Scripts governance (push) Has been skipped
Repo Health / Repository health (push) Has been skipped
- Rewrite ensureMokoCassiopeia() to always install+lock MokoOnyx
  and always unlock MokoCassiopeia (allow uninstall)
- Bundle MokoOnyx 01.00.17 stable payload (replaces MokoCassiopeia)
- Update payload workflow to fetch MokoOnyx from Gitea releases
- Bump version to 02.01.18

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 11:14:16 -05:00
Jonathan Miller 2eea9a4f3c fix: remove tar.gz from updates.xml in update-server
Repo Health / Access control (push) Failing after 2s
Repo Health / Release configuration (push) Has been skipped
Repo Health / Scripts governance (push) Has been skipped
Repo Health / Repository health (push) Has been skipped
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 02:17:53 -05:00
jmiller a59daf7f55 chore: sync updates.xml 02.01.17 [skip ci] 2026-04-23 07:07:45 +00:00
Jonathan Miller df32770484 Bump 02.01.17
Repo Health / Access control (push) Failing after 2s
Repo Health / Release configuration (push) Has been skipped
Repo Health / Scripts governance (push) Has been skipped
Repo Health / Repository health (push) Has been skipped
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 02:07:14 -05:00
jmiller 637785638b fix: remove tar.gz from updates.xml [skip ci] 2026-04-23 07:06:47 +00:00
Jonathan Miller fe99ea2012 fix: remove tar.gz from updates.xml (causes SHA mismatch)
Repo Health / Access control (push) Failing after 1s
Repo Health / Release configuration (push) Has been skipped
Repo Health / Scripts governance (push) Has been skipped
Repo Health / Repository health (push) Has been skipped
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 02:04:41 -05:00
Jonathan Miller af6af35ee8 fix: direct API sync for updates.xml (PR approach blocked by reviews)
Repo Health / Access control (push) Failing after 1s
Repo Health / Release configuration (push) Has been skipped
Repo Health / Scripts governance (push) Has been skipped
Repo Health / Repository health (push) Has been skipped
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-22 22:02:54 -05:00
jmiller 1a26cc16ca fix: sync correct SHA for 02.01.16 [skip ci] 2026-04-23 01:02:44 +00:00
Jonathan Miller 86bf172c43 fix: PR sync always runs, cleans up stale branches
Repo Health / Access control (push) Failing after 2s
Standards Compliance / Secret Scanning (push) Failing after 2s
Standards Compliance / License Header Validation (push) Successful in 3s
Standards Compliance / Repository Structure Validation (push) Successful in 3s
Standards Compliance / Coding Standards Check (push) Failing after 4s
Standards Compliance / Workflow Configuration Check (push) Failing after 3s
Standards Compliance / Documentation Quality Check (push) Successful in 3s
Standards Compliance / README Completeness Check (push) Successful in 2s
Standards Compliance / Git Repository Hygiene (push) Successful in 2s
Standards Compliance / File Naming Standards (push) Successful in 2s
Standards Compliance / Script Integrity Validation (push) Successful in 4s
Standards Compliance / Line Length Check (push) Failing after 3s
Standards Compliance / Insecure Code Pattern Detection (push) Successful in 3s
Standards Compliance / Dead Code Detection (push) Successful in 4s
Standards Compliance / File Size Limits (push) Successful in 2s
Standards Compliance / Binary File Detection (push) Successful in 3s
Standards Compliance / TODO/FIXME Tracking (push) Successful in 3s
Standards Compliance / Version Consistency Check (push) Successful in 50s
Standards Compliance / Code Complexity Analysis (push) Successful in 48s
Standards Compliance / Broken Link Detection (push) Successful in 4s
Standards Compliance / Code Duplication Detection (push) Successful in 52s
Standards Compliance / API Documentation Coverage (push) Successful in 3s
Standards Compliance / Accessibility Check (push) Successful in 2s
Standards Compliance / Performance Metrics (push) Successful in 2s
Standards Compliance / Dependency Vulnerability Scanning (push) Successful in 51s
Standards Compliance / Terraform Configuration Validation (push) Successful in 5s
Repo Health / Release configuration (push) Has been skipped
Repo Health / Scripts governance (push) Has been skipped
Repo Health / Repository health (push) Has been skipped
Standards Compliance / Unused Dependencies Check (push) Successful in 52s
Standards Compliance / Enterprise Readiness Check (push) Failing after 49s
Standards Compliance / Repository Health Check (push) Failing after 49s
Standards Compliance / Compliance Summary (push) Failing after 1s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-22 04:29:48 -05:00
jmiller f5cb9d6575 chore: sync updates.xml 02.01.16 to main [skip ci] 2026-04-22 09:23:47 +00:00
Jonathan Miller 6ec898e889 fix: runs-on: release (dedicated runner)
Repo Health / Access control (push) Has been cancelled
Repo Health / Release configuration (push) Has been cancelled
Repo Health / Scripts governance (push) Has been cancelled
Repo Health / Repository health (push) Has been cancelled
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-22 04:15:00 -05:00
Jonathan Miller 36381d68ac Bump 02.01.16
Repo Health / Access control (push) Failing after 2s
Repo Health / Release configuration (push) Has been skipped
Repo Health / Scripts governance (push) Has been skipped
Repo Health / Repository health (push) Has been skipped
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-22 04:05:54 -05:00
Jonathan Miller d62e2d42d9 fix: create payload dir, bundle MokoOnyx ZIP during release build
Repo Health / Access control (push) Failing after 2s
Repo Health / Release configuration (push) Has been skipped
Repo Health / Scripts governance (push) Has been skipped
Repo Health / Repository health (push) Has been skipped
- Added payload/index.html placeholder
- script.php: look for mokoonyx.zip first, fallback to mokocassiopeia.zip
- auto-release.yml: download MokoOnyx stable ZIP into payload/ during build

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-22 04:01:55 -05:00
Jonathan Miller 90003a5afa fix: install PHP+Composer if missing in workflows
Repo Health / Access control (push) Failing after 1s
Repo Health / Release configuration (push) Has been skipped
Repo Health / Scripts governance (push) Has been skipped
Repo Health / Repository health (push) Has been skipped
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-22 03:52:40 -05:00
Jonathan Miller d922d09e95 Merge dev: Gitea priority 1, PR-based sync, element fix [skip ci]
# Conflicts:
#	updates.xml
2026-04-22 03:44:55 -05:00
Jonathan Miller cb03a89f98 fix: Gitea priority 1, GitHub priority 2 for update server
Repo Health / Access control (push) Failing after 1s
Repo Health / Release configuration (push) Has been skipped
Repo Health / Scripts governance (push) Has been skipped
Repo Health / Repository health (push) Has been skipped
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-22 03:34:53 -05:00
jmiller ffb2930781 fix: remove sha256 tags to skip checksum verification [skip ci] 2026-04-22 08:32:55 +00:00
jmiller 94d53edcd8 fix: infourl tag stable [skip ci] 2026-04-22 08:27:24 +00:00
Jonathan Miller 8a3b0d1954 chore: sync workflows from latest MokoStandards-API templates
Repo Health / Access control (push) Failing after 2s
Repo Health / Release configuration (push) Has been skipped
Repo Health / Scripts governance (push) Has been skipped
Repo Health / Repository health (push) Has been skipped
- PR-based updates.xml sync to main
- Stream-based tags (stable not vXX)
- Element derived from XML filename
- VERSION header in updates.xml
- GH_TOKEN not GH_MIRROR_TOKEN

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-22 03:19:47 -05:00
jmiller eafe65e321 chore: update stable channel to 02.01.15 on main [skip ci] 2026-04-22 08:10:16 +00:00
jmiller 2acd166437 fix: remove stale update.xml (correct file is updates.xml) [skip ci] 2026-04-22 07:56:33 +00:00
jmiller 3d40188217 fix: correct updates.xml — element=mokowaas, stable tag URLs [skip ci] 2026-04-22 07:55:53 +00:00
Jonathan Miller dd8edbfaea fix: remove stale update.xml (correct file is updates.xml)
Repo Health / Access control (push) Failing after 2s
Standards Compliance / Secret Scanning (push) Failing after 2s
Standards Compliance / License Header Validation (push) Successful in 2s
Standards Compliance / Repository Structure Validation (push) Successful in 2s
Standards Compliance / Coding Standards Check (push) Failing after 2s
Standards Compliance / Workflow Configuration Check (push) Failing after 3s
Standards Compliance / Documentation Quality Check (push) Successful in 2s
Standards Compliance / README Completeness Check (push) Successful in 3s
Standards Compliance / Git Repository Hygiene (push) Successful in 2s
Standards Compliance / Script Integrity Validation (push) Successful in 3s
Standards Compliance / Line Length Check (push) Failing after 3s
Standards Compliance / File Naming Standards (push) Successful in 2s
Standards Compliance / Insecure Code Pattern Detection (push) Successful in 2s
Standards Compliance / Version Consistency Check (push) Successful in 48s
Standards Compliance / File Size Limits (push) Successful in 3s
Standards Compliance / Dead Code Detection (push) Successful in 4s
Standards Compliance / TODO/FIXME Tracking (push) Successful in 3s
Standards Compliance / Binary File Detection (push) Successful in 4s
Standards Compliance / Code Complexity Analysis (push) Successful in 50s
Standards Compliance / Broken Link Detection (push) Successful in 3s
Standards Compliance / API Documentation Coverage (push) Successful in 3s
Standards Compliance / Accessibility Check (push) Successful in 2s
Standards Compliance / Performance Metrics (push) Successful in 2s
Standards Compliance / Code Duplication Detection (push) Successful in 53s
Standards Compliance / Dependency Vulnerability Scanning (push) Successful in 49s
Standards Compliance / Terraform Configuration Validation (push) Successful in 6s
Standards Compliance / Unused Dependencies Check (push) Successful in 54s
Repo Health / Release configuration (push) Has been skipped
Repo Health / Scripts governance (push) Has been skipped
Repo Health / Repository health (push) Has been skipped
Standards Compliance / Enterprise Readiness Check (push) Failing after 49s
Standards Compliance / Repository Health Check (push) Failing after 45s
Standards Compliance / Compliance Summary (push) Failing after 1s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-22 02:54:04 -05:00
gitea-actions[bot] e408207f1a chore: update updates.xml (development: 02.01.15-dev) [skip ci] 2026-04-22 07:46:19 +00:00
gitea-actions[bot] ea3ea5724f chore(version): auto-bump patch 02.01.15 [skip ci] 2026-04-22 07:46:19 +00:00
Jonathan Miller d4a5367eed fix: add <element>mokowaas</element> to manifest for correct ZIP naming
Repo Health / Access control (push) Failing after 1s
Repo Health / Release configuration (push) Has been skipped
Repo Health / Scripts governance (push) Has been skipped
Repo Health / Repository health (push) Has been skipped
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-22 02:42:34 -05:00
gitea-actions[bot] 76b4ddaad8 chore: update updates.xml (development: 02.01.14-dev) [skip ci] 2026-04-22 07:23:16 +00:00
Jonathan Miller 6c9dd7bffa Merge branch 'main' into dev
Repo Health / Access control (push) Failing after 2s
Repo Health / Release configuration (push) Has been skipped
Repo Health / Scripts governance (push) Has been skipped
Repo Health / Repository health (push) Has been skipped
# Conflicts:
#	updates.xml
2026-04-22 02:22:24 -05:00
gitea-actions[bot] bd0ce7345c chore(release): ZIP + tar.gz for 02.01.14 [skip ci] 2026-04-22 07:21:42 +00:00
gitea-actions[bot] d3e0cbe8f0 chore(release): build 02.01.14 [skip ci] 2026-04-22 07:21:40 +00:00
Jonathan Miller 1ec5174820 fix: correct updates.xml (element=mokowaas, stream-based tags, proper URLs)
Update MokoCassiopeia Payload / update-payload (push) Failing after 3s
Repo Health / Access control (push) Failing after 2s
Standards Compliance / Secret Scanning (push) Failing after 2s
Standards Compliance / License Header Validation (push) Successful in 2s
Standards Compliance / Repository Structure Validation (push) Successful in 2s
Standards Compliance / Coding Standards Check (push) Failing after 3s
Standards Compliance / Workflow Configuration Check (push) Failing after 3s
Standards Compliance / Documentation Quality Check (push) Successful in 2s
Standards Compliance / README Completeness Check (push) Successful in 2s
Standards Compliance / Git Repository Hygiene (push) Successful in 3s
Standards Compliance / File Naming Standards (push) Successful in 2s
Standards Compliance / Script Integrity Validation (push) Successful in 4s
Standards Compliance / Line Length Check (push) Failing after 3s
Standards Compliance / Insecure Code Pattern Detection (push) Successful in 3s
Standards Compliance / Dead Code Detection (push) Successful in 3s
Standards Compliance / File Size Limits (push) Successful in 3s
Standards Compliance / Binary File Detection (push) Successful in 4s
Standards Compliance / TODO/FIXME Tracking (push) Successful in 2s
Standards Compliance / Version Consistency Check (push) Successful in 47s
Standards Compliance / Code Complexity Analysis (push) Successful in 50s
Standards Compliance / Code Duplication Detection (push) Successful in 51s
Standards Compliance / Broken Link Detection (push) Successful in 4s
Standards Compliance / API Documentation Coverage (push) Successful in 2s
Standards Compliance / Accessibility Check (push) Successful in 3s
Standards Compliance / Performance Metrics (push) Successful in 2s
Standards Compliance / Dependency Vulnerability Scanning (push) Successful in 49s
Standards Compliance / Terraform Configuration Validation (push) Successful in 6s
Repo Health / Release configuration (push) Has been skipped
Repo Health / Scripts governance (push) Has been skipped
Repo Health / Repository health (push) Has been skipped
Standards Compliance / Unused Dependencies Check (push) Successful in 50s
Standards Compliance / Repository Health Check (push) Failing after 45s
Standards Compliance / Enterprise Readiness Check (push) Failing after 47s
Standards Compliance / Compliance Summary (push) Failing after 1s
Auto-Assign Issues & PRs / Assign unassigned issues and PRs (push) Successful in 1s
- element: mokowaas (not system-mokowaas)
- tags: development/alpha/beta/rc/stable (not v02)
- download URLs use /stable/ tag path
- Added VERSION header and SHA-256

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-22 02:15:43 -05:00
gitea-actions[bot] 5c1722b3cb chore(release): ZIP + tar.gz for 02.01.14 [skip ci] 2026-04-22 04:02:35 +00:00
gitea-actions[bot] f47f4be8e3 chore(release): build 02.01.14 [skip ci] 2026-04-22 04:02:33 +00:00
Jonathan Miller 60b06a987c fix: git push -u origin HEAD for version branch (no upstream)
Update MokoCassiopeia Payload / update-payload (push) Failing after 3s
Repo Health / Access control (push) Failing after 2s
Standards Compliance / Secret Scanning (push) Failing after 2s
Standards Compliance / License Header Validation (push) Successful in 3s
Standards Compliance / Repository Structure Validation (push) Successful in 2s
Standards Compliance / Coding Standards Check (push) Failing after 3s
Standards Compliance / Workflow Configuration Check (push) Failing after 3s
Standards Compliance / Documentation Quality Check (push) Successful in 2s
Standards Compliance / Version Consistency Check (push) Successful in 35s
Standards Compliance / README Completeness Check (push) Successful in 3s
Standards Compliance / Git Repository Hygiene (push) Successful in 3s
Standards Compliance / Script Integrity Validation (push) Successful in 3s
Standards Compliance / Line Length Check (push) Failing after 3s
Standards Compliance / File Naming Standards (push) Successful in 2s
Standards Compliance / Insecure Code Pattern Detection (push) Successful in 3s
Standards Compliance / Code Complexity Analysis (push) Successful in 37s
Standards Compliance / Code Duplication Detection (push) Successful in 36s
Standards Compliance / File Size Limits (push) Successful in 2s
Standards Compliance / Dead Code Detection (push) Successful in 4s
Standards Compliance / TODO/FIXME Tracking (push) Successful in 2s
Standards Compliance / Binary File Detection (push) Successful in 4s
Standards Compliance / Dependency Vulnerability Scanning (push) Successful in 34s
Standards Compliance / Broken Link Detection (push) Successful in 3s
Standards Compliance / Unused Dependencies Check (push) Successful in 36s
Standards Compliance / API Documentation Coverage (push) Successful in 2s
Standards Compliance / Accessibility Check (push) Successful in 3s
Standards Compliance / Performance Metrics (push) Successful in 2s
Standards Compliance / Enterprise Readiness Check (push) Failing after 35s
Standards Compliance / Repository Health Check (push) Failing after 34s
Repo Health / Release configuration (push) Has been skipped
Repo Health / Scripts governance (push) Has been skipped
Repo Health / Repository health (push) Has been skipped
Standards Compliance / Terraform Configuration Validation (push) Successful in 5s
Standards Compliance / Compliance Summary (push) Failing after 1s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 21:30:14 -05:00
Jonathan Miller 769a7ec43a fix: GH_MIRROR_TOKEN → GH_TOKEN (secret name mismatch)
Update MokoCassiopeia Payload / update-payload (push) Failing after 3s
Repo Health / Access control (push) Failing after 2s
Standards Compliance / Secret Scanning (push) Failing after 3s
Standards Compliance / License Header Validation (push) Successful in 2s
Standards Compliance / Repository Structure Validation (push) Successful in 3s
Standards Compliance / Coding Standards Check (push) Failing after 2s
Standards Compliance / Workflow Configuration Check (push) Failing after 2s
Standards Compliance / Documentation Quality Check (push) Successful in 2s
Standards Compliance / README Completeness Check (push) Successful in 3s
Standards Compliance / Git Repository Hygiene (push) Successful in 2s
Standards Compliance / Script Integrity Validation (push) Successful in 4s
Standards Compliance / Line Length Check (push) Failing after 3s
Standards Compliance / File Naming Standards (push) Successful in 2s
Standards Compliance / Insecure Code Pattern Detection (push) Successful in 3s
Standards Compliance / Version Consistency Check (push) Successful in 34s
Standards Compliance / Code Complexity Analysis (push) Successful in 34s
Standards Compliance / Dead Code Detection (push) Successful in 4s
Standards Compliance / File Size Limits (push) Successful in 3s
Standards Compliance / Code Duplication Detection (push) Successful in 34s
Standards Compliance / Binary File Detection (push) Successful in 4s
Standards Compliance / TODO/FIXME Tracking (push) Successful in 3s
Standards Compliance / Dependency Vulnerability Scanning (push) Successful in 35s
Standards Compliance / Broken Link Detection (push) Successful in 3s
Standards Compliance / Unused Dependencies Check (push) Successful in 38s
Standards Compliance / API Documentation Coverage (push) Successful in 2s
Standards Compliance / Accessibility Check (push) Successful in 3s
Standards Compliance / Performance Metrics (push) Successful in 3s
Standards Compliance / Enterprise Readiness Check (push) Failing after 32s
Standards Compliance / Repository Health Check (push) Failing after 33s
Standards Compliance / Terraform Configuration Validation (push) Successful in 5s
Repo Health / Release configuration (push) Has been cancelled
Repo Health / Scripts governance (push) Has been cancelled
Repo Health / Repository health (push) Has been cancelled
Standards Compliance / Compliance Summary (push) Has been cancelled
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 21:11:40 -05:00
Jonathan Miller 566bf8fff2 fix: clone MokoStandards-API from main (version/04 doesn't exist)
Repo Health / Access control (push) Failing after 2s
Standards Compliance / Secret Scanning (push) Failing after 2s
Standards Compliance / License Header Validation (push) Successful in 3s
Standards Compliance / Repository Structure Validation (push) Successful in 3s
Standards Compliance / Coding Standards Check (push) Failing after 3s
Standards Compliance / Workflow Configuration Check (push) Failing after 3s
Standards Compliance / Documentation Quality Check (push) Successful in 3s
Standards Compliance / README Completeness Check (push) Successful in 2s
Standards Compliance / Git Repository Hygiene (push) Successful in 3s
Standards Compliance / Script Integrity Validation (push) Successful in 4s
Standards Compliance / Line Length Check (push) Failing after 4s
Standards Compliance / File Naming Standards (push) Successful in 3s
Standards Compliance / Insecure Code Pattern Detection (push) Successful in 3s
Standards Compliance / Version Consistency Check (push) Successful in 40s
Standards Compliance / Code Complexity Analysis (push) Successful in 46s
Standards Compliance / Dead Code Detection (push) Successful in 3s
Standards Compliance / File Size Limits (push) Successful in 3s
Standards Compliance / Code Duplication Detection (push) Successful in 43s
Standards Compliance / Binary File Detection (push) Successful in 4s
Standards Compliance / TODO/FIXME Tracking (push) Successful in 4s
Standards Compliance / Dependency Vulnerability Scanning (push) Successful in 43s
Standards Compliance / Unused Dependencies Check (push) Successful in 43s
Standards Compliance / Broken Link Detection (push) Successful in 4s
Standards Compliance / API Documentation Coverage (push) Successful in 3s
Standards Compliance / Accessibility Check (push) Successful in 2s
Standards Compliance / Performance Metrics (push) Successful in 3s
Standards Compliance / Enterprise Readiness Check (push) Failing after 38s
Standards Compliance / Repository Health Check (push) Failing after 37s
Update MokoCassiopeia Payload / update-payload (push) Failing after 3s
Standards Compliance / Terraform Configuration Validation (push) Successful in 5s
Repo Health / Release configuration (push) Has been skipped
Repo Health / Scripts governance (push) Has been skipped
Repo Health / Repository health (push) Has been skipped
Standards Compliance / Compliance Summary (push) Failing after 1s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 21:02:50 -05:00
Jonathan Miller bf75614190 chore: sync workflows from MokoStandards-API templates
Repo Health / Access control (push) Failing after 2s
Standards Compliance / Secret Scanning (push) Failing after 2s
Standards Compliance / License Header Validation (push) Successful in 3s
Standards Compliance / Repository Structure Validation (push) Successful in 2s
Standards Compliance / Coding Standards Check (push) Failing after 2s
Standards Compliance / Version Consistency Check (push) Successful in 33s
Standards Compliance / Workflow Configuration Check (push) Failing after 2s
Standards Compliance / Documentation Quality Check (push) Successful in 2s
Standards Compliance / README Completeness Check (push) Successful in 3s
Standards Compliance / Git Repository Hygiene (push) Successful in 2s
Standards Compliance / Script Integrity Validation (push) Successful in 4s
Standards Compliance / Line Length Check (push) Failing after 3s
Standards Compliance / File Naming Standards (push) Successful in 3s
Standards Compliance / Insecure Code Pattern Detection (push) Successful in 3s
Standards Compliance / Code Complexity Analysis (push) Successful in 34s
Standards Compliance / Dead Code Detection (push) Successful in 4s
Standards Compliance / File Size Limits (push) Successful in 2s
Standards Compliance / Binary File Detection (push) Successful in 3s
Standards Compliance / TODO/FIXME Tracking (push) Successful in 3s
Standards Compliance / Code Duplication Detection (push) Successful in 34s
Standards Compliance / Dependency Vulnerability Scanning (push) Successful in 38s
Standards Compliance / Broken Link Detection (push) Successful in 3s
Standards Compliance / Unused Dependencies Check (push) Successful in 38s
Standards Compliance / API Documentation Coverage (push) Successful in 3s
Standards Compliance / Accessibility Check (push) Successful in 2s
Standards Compliance / Performance Metrics (push) Successful in 2s
Standards Compliance / Enterprise Readiness Check (push) Failing after 34s
Standards Compliance / Repository Health Check (push) Failing after 33s
Update MokoCassiopeia Payload / update-payload (push) Failing after 2s
Standards Compliance / Terraform Configuration Validation (push) Successful in 6s
Repo Health / Release configuration (push) Has been skipped
Repo Health / Scripts governance (push) Has been skipped
Repo Health / Repository health (push) Has been skipped
Standards Compliance / Compliance Summary (push) Failing after 2s
Auto-Assign Issues & PRs / Assign unassigned issues and PRs (push) Successful in 1s
- auto-release.yml: cascading channels, GA_TOKEN, Gitea-primary
- update-server.yml: cascading channels, auto-bump on all branches

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 17:31:53 -05:00
Jonathan Miller 43bd0e2031 Bump 02.01.14 — GitHub-primary update server, MokoOnyx lock support
Repo Health / Access control (push) Failing after 1s
Standards Compliance / Secret Scanning (push) Failing after 2s
Standards Compliance / License Header Validation (push) Successful in 2s
Standards Compliance / Repository Structure Validation (push) Successful in 2s
Standards Compliance / Coding Standards Check (push) Failing after 3s
Standards Compliance / Workflow Configuration Check (push) Failing after 3s
Standards Compliance / Documentation Quality Check (push) Successful in 2s
Standards Compliance / README Completeness Check (push) Successful in 2s
Standards Compliance / Git Repository Hygiene (push) Successful in 3s
Standards Compliance / Script Integrity Validation (push) Successful in 3s
Standards Compliance / Line Length Check (push) Failing after 2s
Standards Compliance / File Naming Standards (push) Successful in 3s
Standards Compliance / Insecure Code Pattern Detection (push) Successful in 2s
Standards Compliance / Version Consistency Check (push) Successful in 34s
Standards Compliance / Code Complexity Analysis (push) Successful in 33s
Standards Compliance / Code Duplication Detection (push) Successful in 32s
Standards Compliance / Dead Code Detection (push) Successful in 4s
Standards Compliance / File Size Limits (push) Successful in 3s
Standards Compliance / TODO/FIXME Tracking (push) Successful in 3s
Standards Compliance / Binary File Detection (push) Successful in 4s
Standards Compliance / Dependency Vulnerability Scanning (push) Successful in 40s
Standards Compliance / Broken Link Detection (push) Successful in 3s
Standards Compliance / Unused Dependencies Check (push) Successful in 44s
Standards Compliance / API Documentation Coverage (push) Successful in 2s
Standards Compliance / Accessibility Check (push) Successful in 2s
Standards Compliance / Performance Metrics (push) Successful in 3s
Standards Compliance / Enterprise Readiness Check (push) Failing after 38s
Standards Compliance / Repository Health Check (push) Failing after 37s
Update MokoCassiopeia Payload / update-payload (push) Failing after 3s
Standards Compliance / Terraform Configuration Validation (push) Successful in 5s
Repo Health / Release configuration (push) Has been skipped
Repo Health / Scripts governance (push) Has been skipped
Repo Health / Repository health (push) Has been skipped
Standards Compliance / Compliance Summary (push) Failing after 1s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 17:22:26 -05:00
Jonathan Miller 68f897ffe4 feat: add all update channels, Gitea-primary update server URL
Repo Health / Access control (push) Failing after 5s
Standards Compliance / Secret Scanning (push) Failing after 2s
Standards Compliance / License Header Validation (push) Successful in 3s
Standards Compliance / Repository Structure Validation (push) Successful in 3s
Standards Compliance / Coding Standards Check (push) Failing after 2s
Standards Compliance / Workflow Configuration Check (push) Failing after 2s
Standards Compliance / Documentation Quality Check (push) Successful in 3s
Standards Compliance / README Completeness Check (push) Successful in 3s
Standards Compliance / Git Repository Hygiene (push) Successful in 2s
Standards Compliance / Script Integrity Validation (push) Successful in 4s
Standards Compliance / Line Length Check (push) Failing after 3s
Standards Compliance / File Naming Standards (push) Successful in 3s
Standards Compliance / Insecure Code Pattern Detection (push) Successful in 2s
Standards Compliance / Version Consistency Check (push) Successful in 36s
Standards Compliance / Code Complexity Analysis (push) Successful in 42s
Standards Compliance / Dead Code Detection (push) Successful in 4s
Standards Compliance / Code Duplication Detection (push) Successful in 37s
Standards Compliance / File Size Limits (push) Successful in 3s
Standards Compliance / TODO/FIXME Tracking (push) Successful in 3s
Standards Compliance / Binary File Detection (push) Successful in 4s
Standards Compliance / Dependency Vulnerability Scanning (push) Successful in 35s
Standards Compliance / Broken Link Detection (push) Successful in 3s
Standards Compliance / Unused Dependencies Check (push) Successful in 37s
Standards Compliance / API Documentation Coverage (push) Successful in 2s
Standards Compliance / Accessibility Check (push) Successful in 3s
Standards Compliance / Performance Metrics (push) Successful in 2s
Standards Compliance / Enterprise Readiness Check (push) Failing after 33s
Standards Compliance / Repository Health Check (push) Failing after 32s
Standards Compliance / Terraform Configuration Validation (push) Successful in 5s
Update MokoCassiopeia Payload / update-payload (push) Failing after 3s
Repo Health / Release configuration (push) Has been skipped
Repo Health / Scripts governance (push) Has been skipped
Repo Health / Repository health (push) Has been skipped
Standards Compliance / Compliance Summary (push) Failing after 1s
- updates.xml: all 5 channels (dev, alpha, beta, rc, stable)
- manifest: Gitea priority 1, GitHub priority 2

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 17:17:53 -05:00
Jonathan Miller 3df40214f3 feat: prefer MokoOnyx over MokoCassiopeia — lock Onyx, unlock Cassiopeia
Repo Health / Access control (push) Failing after 4s
Standards Compliance / Secret Scanning (push) Failing after 2s
Standards Compliance / License Header Validation (push) Successful in 2s
Standards Compliance / Repository Structure Validation (push) Successful in 3s
Standards Compliance / Coding Standards Check (push) Failing after 3s
Standards Compliance / Workflow Configuration Check (push) Failing after 3s
Standards Compliance / Documentation Quality Check (push) Successful in 3s
Standards Compliance / README Completeness Check (push) Successful in 2s
Standards Compliance / Git Repository Hygiene (push) Successful in 3s
Standards Compliance / Version Consistency Check (push) Successful in 35s
Standards Compliance / Line Length Check (push) Failing after 3s
Standards Compliance / File Naming Standards (push) Successful in 2s
Standards Compliance / Insecure Code Pattern Detection (push) Successful in 3s
Standards Compliance / Script Integrity Validation (push) Successful in 39s
Standards Compliance / Code Complexity Analysis (push) Successful in 35s
Standards Compliance / Dead Code Detection (push) Successful in 4s
Standards Compliance / File Size Limits (push) Successful in 2s
Standards Compliance / Binary File Detection (push) Successful in 4s
Standards Compliance / TODO/FIXME Tracking (push) Successful in 3s
Standards Compliance / Code Duplication Detection (push) Successful in 39s
Standards Compliance / Dependency Vulnerability Scanning (push) Successful in 34s
Standards Compliance / Broken Link Detection (push) Successful in 4s
Standards Compliance / API Documentation Coverage (push) Successful in 2s
Standards Compliance / Accessibility Check (push) Successful in 3s
Standards Compliance / Performance Metrics (push) Successful in 2s
Standards Compliance / Unused Dependencies Check (push) Successful in 39s
Standards Compliance / Enterprise Readiness Check (push) Failing after 37s
Standards Compliance / Terraform Configuration Validation (push) Successful in 5s
Standards Compliance / Repository Health Check (push) Failing after 34s
Repo Health / Release configuration (push) Has been skipped
Repo Health / Scripts governance (push) Has been skipped
Repo Health / Repository health (push) Has been skipped
Standards Compliance / Compliance Summary (push) Failing after 1s
Update MokoCassiopeia Payload / update-payload (push) Failing after 10s
If MokoOnyx is installed, lock it and set as default.
Unlock MokoCassiopeia to allow uninstall.
Falls back to MokoCassiopeia if Onyx not present.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 17:09:18 -05:00
jmiller d4d0b2b276 chore: update updates.xml from MokoStandards
Repo Health / Access control (push) Failing after 1s
Repo Health / Release configuration (push) Has been skipped
Repo Health / Scripts governance (push) Has been skipped
Repo Health / Repository health (push) Has been skipped
2026-04-19 05:54:19 +00:00
jmiller 9eda3fd497 chore: update updates.xml from MokoStandards
Repo Health / Access control (push) Failing after 2s
Standards Compliance / Secret Scanning (push) Failing after 3s
Standards Compliance / License Header Validation (push) Successful in 3s
Standards Compliance / Repository Structure Validation (push) Successful in 4s
Standards Compliance / Coding Standards Check (push) Failing after 4s
Standards Compliance / Workflow Configuration Check (push) Failing after 3s
Standards Compliance / Documentation Quality Check (push) Successful in 3s
Standards Compliance / README Completeness Check (push) Successful in 2s
Standards Compliance / Git Repository Hygiene (push) Successful in 2s
Standards Compliance / Script Integrity Validation (push) Successful in 4s
Standards Compliance / Line Length Check (push) Failing after 3s
Standards Compliance / File Naming Standards (push) Successful in 3s
Standards Compliance / Insecure Code Pattern Detection (push) Successful in 3s
Standards Compliance / Version Consistency Check (push) Successful in 38s
Standards Compliance / Dead Code Detection (push) Successful in 4s
Standards Compliance / File Size Limits (push) Successful in 2s
Standards Compliance / Binary File Detection (push) Successful in 4s
Standards Compliance / TODO/FIXME Tracking (push) Successful in 2s
Standards Compliance / Code Complexity Analysis (push) Successful in 39s
Standards Compliance / Code Duplication Detection (push) Successful in 40s
Standards Compliance / Broken Link Detection (push) Successful in 3s
Standards Compliance / API Documentation Coverage (push) Successful in 3s
Standards Compliance / Accessibility Check (push) Successful in 2s
Standards Compliance / Performance Metrics (push) Successful in 3s
Standards Compliance / Dependency Vulnerability Scanning (push) Successful in 38s
Standards Compliance / Unused Dependencies Check (push) Successful in 39s
Standards Compliance / Terraform Configuration Validation (push) Successful in 6s
Standards Compliance / Enterprise Readiness Check (push) Failing after 37s
Standards Compliance / Repository Health Check (push) Failing after 36s
Update MokoCassiopeia Payload / update-payload (push) Failing after 5s
Repo Health / Release configuration (push) Has been skipped
Repo Health / Scripts governance (push) Has been skipped
Repo Health / Repository health (push) Has been skipped
Standards Compliance / Compliance Summary (push) Failing after 1s
Auto-Assign Issues & PRs / Assign unassigned issues and PRs (push) Successful in 1s
2026-04-19 05:54:19 +00:00
jmiller 5fd7961fad chore: update update.xml from MokoStandards
Repo Health / Access control (push) Failing after 2s
Repo Health / Release configuration (push) Has been skipped
Repo Health / Scripts governance (push) Has been skipped
Repo Health / Repository health (push) Has been skipped
2026-04-17 10:47:39 +00:00
jmiller 9178aeaa9c chore: update update.xml from MokoStandards
Repo Health / Access control (push) Failing after 1s
Standards Compliance / Secret Scanning (push) Failing after 2s
Standards Compliance / License Header Validation (push) Successful in 3s
Standards Compliance / Repository Structure Validation (push) Successful in 3s
Standards Compliance / Workflow Configuration Check (push) Failing after 3s
Standards Compliance / Coding Standards Check (push) Failing after 3s
Standards Compliance / Documentation Quality Check (push) Successful in 3s
Standards Compliance / README Completeness Check (push) Successful in 3s
Standards Compliance / Git Repository Hygiene (push) Successful in 4s
Standards Compliance / Script Integrity Validation (push) Successful in 4s
Standards Compliance / Line Length Check (push) Failing after 3s
Standards Compliance / File Naming Standards (push) Successful in 3s
Standards Compliance / Insecure Code Pattern Detection (push) Successful in 3s
Standards Compliance / Version Consistency Check (push) Successful in 52s
Standards Compliance / Dead Code Detection (push) Successful in 4s
Standards Compliance / File Size Limits (push) Successful in 2s
Standards Compliance / Binary File Detection (push) Successful in 4s
Standards Compliance / Code Complexity Analysis (push) Successful in 48s
Standards Compliance / Code Duplication Detection (push) Successful in 47s
Standards Compliance / TODO/FIXME Tracking (push) Successful in 2s
Standards Compliance / Broken Link Detection (push) Successful in 3s
Standards Compliance / API Documentation Coverage (push) Successful in 3s
Standards Compliance / Accessibility Check (push) Successful in 3s
Standards Compliance / Performance Metrics (push) Successful in 2s
Standards Compliance / Dependency Vulnerability Scanning (push) Successful in 50s
Standards Compliance / Unused Dependencies Check (push) Successful in 52s
Standards Compliance / Terraform Configuration Validation (push) Successful in 6s
Standards Compliance / Enterprise Readiness Check (push) Failing after 47s
Update MokoCassiopeia Payload / update-payload (push) Failing after 3s
Standards Compliance / Repository Health Check (push) Failing after 46s
Repo Health / Release configuration (push) Has been skipped
Repo Health / Scripts governance (push) Has been skipped
Repo Health / Repository health (push) Has been skipped
Standards Compliance / Compliance Summary (push) Failing after 1s
Auto-Assign Issues & PRs / Assign unassigned issues and PRs (push) Successful in 1s
2026-04-17 10:47:38 +00:00
133 changed files with 13475 additions and 8551 deletions
-20
View File
@@ -1,20 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# SPDX-License-Identifier: GPL-3.0-or-later
# FILE INFORMATION
# DEFGROUP: MokoStandards.Templates.Config
# INGROUP: MokoStandards.Templates
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /templates/configs/moko-standards.yml
# VERSION: 04.04.01
# BRIEF: Governance attachment template — synced to .mokostandards in every governed repository
# NOTE: Tokens replaced at sync time: mokoconsulting-tech, MokoWaaS, waas-component, 04.04.00
#
# This file is managed automatically by MokoStandards bulk sync.
# Do not edit manually — changes will be overwritten on the next sync.
# To update governance settings, open a PR in MokoStandards instead:
# https://github.com/mokoconsulting-tech/MokoStandards
standards_source: "https://github.com/mokoconsulting-tech/MokoStandards"
standards_version: "04.04.00"
platform: "waas-component"
governed_repo: "mokoconsulting-tech/MokoWaaS"
-76
View File
@@ -1,76 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: GitHub.Workflow
# INGROUP: MokoStandards.Workflows.Shared
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /.github/workflows/auto-assign.yml
# VERSION: 04.06.00
# BRIEF: Auto-assign jmiller-moko to unassigned issues and PRs every 15 minutes
name: Auto-Assign Issues & PRs
on:
issues:
types: [opened]
pull_request_target:
types: [opened]
schedule:
- cron: '0 */12 * * *'
workflow_dispatch:
permissions:
issues: write
pull-requests: write
jobs:
auto-assign:
name: Assign unassigned issues and PRs
runs-on: ubuntu-latest
steps:
- name: Assign unassigned issues
env:
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
run: |
REPO="${{ github.repository }}"
ASSIGNEE="jmiller-moko"
echo "## 🏷️ Auto-Assign Report" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
ASSIGNED_ISSUES=0
ASSIGNED_PRS=0
# Assign unassigned open issues
ISSUES=$(gh api "repos/$REPO/issues?state=open&per_page=100&assignee=none" --jq '.[].number' 2>/dev/null || true)
for NUM in $ISSUES; do
# Skip PRs (the issues endpoint returns PRs too)
IS_PR=$(gh api "repos/$REPO/issues/$NUM" --jq '.pull_request // empty' 2>/dev/null || true)
if [ -z "$IS_PR" ]; then
gh api "repos/$REPO/issues/$NUM/assignees" -X POST -f "assignees[]=$ASSIGNEE" --silent 2>/dev/null && {
ASSIGNED_ISSUES=$((ASSIGNED_ISSUES + 1))
echo " Assigned issue #$NUM"
} || true
fi
done
# Assign unassigned open PRs
PRS=$(gh api "repos/$REPO/pulls?state=open&per_page=100" --jq '.[] | select(.assignees | length == 0) | .number' 2>/dev/null || true)
for NUM in $PRS; do
gh api "repos/$REPO/issues/$NUM/assignees" -X POST -f "assignees[]=$ASSIGNEE" --silent 2>/dev/null && {
ASSIGNED_PRS=$((ASSIGNED_PRS + 1))
echo " Assigned PR #$NUM"
} || true
done
echo "| Type | Assigned |" >> $GITHUB_STEP_SUMMARY
echo "|------|----------|" >> $GITHUB_STEP_SUMMARY
echo "| Issues | $ASSIGNED_ISSUES |" >> $GITHUB_STEP_SUMMARY
echo "| Pull Requests | $ASSIGNED_PRS |" >> $GITHUB_STEP_SUMMARY
if [ "$ASSIGNED_ISSUES" -eq 0 ] && [ "$ASSIGNED_PRS" -eq 0 ]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "✅ All issues and PRs already have assignees" >> $GITHUB_STEP_SUMMARY
fi
-207
View File
@@ -1,207 +0,0 @@
# 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: GitHub.Workflow
# INGROUP: MokoStandards.Automation
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /templates/workflows/shared/auto-dev-issue.yml.template
# VERSION: 04.06.00
# BRIEF: Auto-create tracking issue with sub-issues for dev/rc branch workflow
# NOTE: Synced via bulk-repo-sync to .github/workflows/auto-dev-issue.yml in all governed repos.
name: Dev/RC Branch Issue
on:
# Auto-create on RC branch creation
create:
# Manual trigger for dev branches
workflow_dispatch:
inputs:
branch:
description: 'Branch name (e.g., dev/my-feature or dev/04.06)'
required: true
type: string
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
permissions:
contents: read
issues: write
jobs:
create-issue:
name: Create version tracking issue
runs-on: ubuntu-latest
if: >-
(github.event_name == 'workflow_dispatch') ||
(github.event.ref_type == 'branch' &&
(startsWith(github.event.ref, 'rc/') ||
startsWith(github.event.ref, 'alpha/') ||
startsWith(github.event.ref, 'beta/')))
steps:
- name: Create tracking issue and sub-issues
env:
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
run: |
# For manual dispatch, use input; for auto, use event ref
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
BRANCH="${{ inputs.branch }}"
else
BRANCH="${{ github.event.ref }}"
fi
REPO="${{ github.repository }}"
ACTOR="${{ github.actor }}"
NOW=$(date -u '+%Y-%m-%d %H:%M UTC')
# Determine branch type and version
if [[ "$BRANCH" == rc/* ]]; then
VERSION="${BRANCH#rc/}"
BRANCH_TYPE="Release Candidate"
LABEL_TYPE="type: release"
TITLE_PREFIX="rc"
elif [[ "$BRANCH" == beta/* ]]; then
VERSION="${BRANCH#beta/}"
BRANCH_TYPE="Beta"
LABEL_TYPE="type: release"
TITLE_PREFIX="beta"
elif [[ "$BRANCH" == alpha/* ]]; then
VERSION="${BRANCH#alpha/}"
BRANCH_TYPE="Alpha"
LABEL_TYPE="type: release"
TITLE_PREFIX="alpha"
else
VERSION="${BRANCH#dev/}"
BRANCH_TYPE="Development"
LABEL_TYPE="type: feature"
TITLE_PREFIX="feat"
fi
TITLE="${TITLE_PREFIX}(${VERSION}): ${BRANCH_TYPE} tracking for ${BRANCH}"
# Check for existing issue with same title prefix
EXISTING=$(gh api "repos/${REPO}/issues?state=open&per_page=10" \
--jq ".[] | select(.title | startswith(\"${TITLE_PREFIX}(${VERSION})\")) | .number" 2>/dev/null | head -1)
if [ -n "$EXISTING" ]; then
echo "️ Issue #${EXISTING} already exists for ${VERSION}" >> $GITHUB_STEP_SUMMARY
exit 0
fi
# ── Define sub-issues for the workflow ─────────────────────────
if [[ "$BRANCH" == rc/* ]]; then
SUB_ISSUES=(
"RC Testing|Verify all features work on rc branch|type: test,release-candidate"
"Regression Testing|Run full regression suite before merge|type: test,release-candidate"
"Version Bump|Bump version in README.md and all headers|type: version,release-candidate"
"Changelog Update|Update CHANGELOG.md with release notes|documentation,release-candidate"
"Merge to Version Branch|Create PR to version/XX|type: release,needs-review"
)
elif [[ "$BRANCH" == alpha/* ]] || [[ "$BRANCH" == beta/* ]]; then
SUB_ISSUES=(
"Testing|Verify features on ${BRANCH_TYPE} branch|type: test,status: in-progress"
"Bug Fixes|Fix issues found during ${BRANCH_TYPE} testing|type: bug,status: pending"
"Promote to Next Stage|Create PR to promote to next release stage|type: release,needs-review"
)
else
SUB_ISSUES=(
"Development|Implement feature/fix on dev branch|type: feature,status: in-progress"
"Unit Testing|Write and pass unit tests|type: test,status: pending"
"Code Review|Request and complete code review|needs-review,status: pending"
"Version Bump|Bump version in README.md and all headers|type: version,status: pending"
"Changelog Update|Update CHANGELOG.md with release notes|documentation,status: pending"
"Create RC Branch|Promote dev to rc branch for final testing|type: release,status: pending"
"Merge to Main|Create PR from rc/dev to main|type: release,needs-review,status: pending"
)
fi
# ── Create sub-issues first ───────────────────────────────────────
SUB_LIST=""
SUB_NUMBERS=""
for SUB in "${SUB_ISSUES[@]}"; do
IFS='|' read -r SUB_TITLE SUB_DESC SUB_LABELS <<< "$SUB"
SUB_FULL_TITLE="${TITLE_PREFIX}(${VERSION}): ${SUB_TITLE}"
SUB_BODY=$(printf '### %s\n\n%s\n\n| Field | Value |\n|-------|-------|\n| **Parent Branch** | `%s` |\n| **Version** | `%s` |\n\n---\n*Sub-issue of the %s tracking issue for `%s`.*' \
"$SUB_TITLE" "$SUB_DESC" "$BRANCH" "$VERSION" "$BRANCH_TYPE" "$BRANCH")
SUB_URL=$(gh issue create \
--repo "$REPO" \
--title "$SUB_FULL_TITLE" \
--body "$SUB_BODY" \
--label "${SUB_LABELS}" \
--assignee "jmiller-moko" 2>&1)
SUB_NUM=$(echo "$SUB_URL" | grep -oE '[0-9]+$')
if [ -n "$SUB_NUM" ]; then
SUB_LIST="${SUB_LIST}\n- [ ] ${SUB_TITLE} (#${SUB_NUM})"
SUB_NUMBERS="${SUB_NUMBERS} #${SUB_NUM}"
fi
sleep 0.3
done
# ── Create parent tracking issue ──────────────────────────────────
PARENT_BODY=$(printf '## %s Branch Created\n\n| Field | Value |\n|-------|-------|\n| **Branch** | `%s` |\n| **Version** | `%s` |\n| **Type** | %s |\n| **Created by** | @%s |\n| **Created at** | %s |\n| **Repository** | `%s` |\n\n## Workflow Sub-Issues\n\n%b\n\n---\n*Auto-created by [auto-dev-issue.yml](.github/workflows/auto-dev-issue.yml) on branch creation.*' \
"$BRANCH_TYPE" "$BRANCH" "$VERSION" "$BRANCH_TYPE" "$ACTOR" "$NOW" "$REPO" "$SUB_LIST")
PARENT_URL=$(gh issue create \
--repo "$REPO" \
--title "$TITLE" \
--body "$PARENT_BODY" \
--label "${LABEL_TYPE},version" \
--assignee "jmiller-moko" 2>&1)
PARENT_NUM=$(echo "$PARENT_URL" | grep -oE '[0-9]+$')
# ── Link sub-issues back to parent ────────────────────────────────
if [ -n "$PARENT_NUM" ]; then
for SUB in "${SUB_ISSUES[@]}"; do
IFS='|' read -r SUB_TITLE _ _ <<< "$SUB"
SUB_FULL_TITLE="${TITLE_PREFIX}(${VERSION}): ${SUB_TITLE}"
SUB_NUM=$(gh api "repos/${REPO}/issues?state=open&per_page=20" \
--jq ".[] | select(.title == \"${SUB_FULL_TITLE}\") | .number" 2>/dev/null | head -1)
if [ -n "$SUB_NUM" ]; then
gh api "repos/${REPO}/issues/${SUB_NUM}" -X PATCH \
-f body="$(gh api "repos/${REPO}/issues/${SUB_NUM}" --jq '.body' 2>/dev/null)
> **Parent Issue:** #${PARENT_NUM}" --silent 2>/dev/null || true
fi
sleep 0.2
done
fi
# ── Create or update prerelease for alpha/beta/rc ────────────────
if [[ "$BRANCH" == rc/* ]] || [[ "$BRANCH" == alpha/* ]] || [[ "$BRANCH" == beta/* ]]; then
case "$BRANCH_TYPE" in
Alpha) RELEASE_TAG="alpha" ;;
Beta) RELEASE_TAG="beta" ;;
"Release Candidate") RELEASE_TAG="release-candidate" ;;
esac
EXISTING=$(gh release view "$RELEASE_TAG" --json tagName -q .tagName 2>/dev/null || true)
if [ -z "$EXISTING" ]; then
gh release create "$RELEASE_TAG" \
--title "${RELEASE_TAG} (${VERSION})" \
--notes "## ${BRANCH_TYPE} ${VERSION}\n\nBranch: \`${BRANCH}\`\nTracking issue: ${PARENT_URL}" \
--prerelease \
--target main 2>/dev/null || true
echo "${BRANCH_TYPE} release created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
else
gh release edit "$RELEASE_TAG" \
--title "${RELEASE_TAG} (${VERSION})" --prerelease 2>/dev/null || true
echo "${BRANCH_TYPE} release updated: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
fi
fi
# ── Summary ───────────────────────────────────────────────────────
echo "## Dev Workflow Issues Created" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Item | Issue |" >> $GITHUB_STEP_SUMMARY
echo "|------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| **Parent** | ${PARENT_URL} |" >> $GITHUB_STEP_SUMMARY
echo "| **Sub-issues** |${SUB_NUMBERS} |" >> $GITHUB_STEP_SUMMARY
-560
View File
@@ -1,560 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: GitHub.Workflow
# INGROUP: MokoStandards.Release
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /templates/workflows/joomla/auto-release.yml.template
# VERSION: 04.06.00
# BRIEF: Joomla build & release — ZIP package, updates.xml, SHA-256 checksum
#
# +========================================================================+
# | BUILD & RELEASE PIPELINE (JOOMLA) |
# +========================================================================+
# | |
# | Triggers on push to main (skips bot commits + [skip ci]): |
# | |
# | Every push: |
# | 1. Read version from README.md |
# | 3. Set platform version (Joomla <version>) |
# | 4. Update [VERSION: XX.YY.ZZ] badges in markdown files |
# | 5. Write updates.xml (Joomla update server XML) |
# | 6. Create git tag vXX.YY.ZZ |
# | 7a. Patch: update existing GitHub Release for this minor |
# | 8. Build ZIP, upload asset, write SHA-256 to updates.xml |
# | |
# | Every version change: archives main -> version/XX.YY branch |
# | Patch 00 = development (no release). First release = patch 01. |
# | First release only (patch == 01): |
# | 7b. Create new GitHub Release |
# | |
# +========================================================================+
name: Build & Release
on:
pull_request:
types: [closed]
branches:
- main
paths:
- 'src/**'
- 'htdocs/**'
workflow_dispatch:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
permissions:
contents: write
jobs:
release:
name: Build & Release Pipeline
runs-on: ubuntu-latest
if: >-
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch'
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.GH_TOKEN || github.token }}
fetch-depth: 0
- name: Setup MokoStandards tools
env:
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}'
run: |
git clone --depth 1 --branch version/04 --quiet \
"https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \
/tmp/mokostandards
cd /tmp/mokostandards
composer install --no-dev --no-interaction --quiet
# -- STEP 1: Read version -----------------------------------------------
- name: "Step 1: Read version from README.md"
id: version
run: |
VERSION=$(php /tmp/mokostandards/api/cli/version_read.php --path . 2>/dev/null)
if [ -z "$VERSION" ]; then
echo "No VERSION in README.md — skipping release"
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
# Derive major.minor for branch naming (patches update existing branch)
MINOR=$(echo "$VERSION" | awk -F. '{printf "%s.%s", $1, $2}')
PATCH=$(echo "$VERSION" | awk -F. '{print $3}')
MAJOR=$(echo "$VERSION" | awk -F. '{print $1}')
MINOR_NUM=$(echo "$VERSION" | awk -F. '{print $2}')
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "branch=version/${MAJOR}" >> "$GITHUB_OUTPUT"
echo "minor=$MINOR" >> "$GITHUB_OUTPUT"
echo "major=$MAJOR" >> "$GITHUB_OUTPUT"
echo "release_tag=v${MAJOR}" >> "$GITHUB_OUTPUT"
if [ "$PATCH" = "00" ]; then
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "is_minor=false" >> "$GITHUB_OUTPUT"
echo "Version: $VERSION (patch 00 = development — skipping release)"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
if [ "$PATCH" = "01" ]; then
echo "is_minor=true" >> "$GITHUB_OUTPUT"
echo "Version: $VERSION (first release — full pipeline)"
else
echo "is_minor=false" >> "$GITHUB_OUTPUT"
echo "Version: $VERSION (patch — platform version + badges only)"
fi
fi
- 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"
if [ "$TAG_EXISTS" = "true" ] && [ "$BRANCH_EXISTS" = "true" ]; then
echo "already_released=true" >> "$GITHUB_OUTPUT"
else
echo "already_released=false" >> "$GITHUB_OUTPUT"
fi
# -- SANITY CHECKS -------------------------------------------------------
- name: "Sanity: Pre-release validation"
if: >-
steps.version.outputs.skip != 'true' &&
steps.check.outputs.already_released != 'true'
run: |
VERSION="${{ steps.version.outputs.version }}"
ERRORS=0
echo "## Pre-Release Sanity Checks (Joomla)" >> $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
# -- Joomla: manifest version drift --------
MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
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
fi
# -- Joomla: XML manifest existence --------
if [ -z "$MANIFEST" ]; then
echo "- No Joomla XML manifest found" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
else
echo "- Manifest: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY
# -- Joomla: extension type check --------
TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" 2>/dev/null)
echo "- Extension type: ${TYPE:-unknown}" >> $GITHUB_STEP_SUMMARY
fi
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.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.version.outputs.version }}"
php /tmp/mokostandards/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' &&
steps.check.outputs.already_released != 'true'
run: |
VERSION="${{ steps.version.outputs.version }}"
find . -name "*.md" ! -path "./.git/*" ! -path "./vendor/*" | while read -r f; do
if grep -q '\[VERSION:' "$f" 2>/dev/null; then
sed -i "s/\[VERSION:[[:space:]]*[0-9]\{2\}\.[0-9]\{2\}\.[0-9]\{2\}\]/[VERSION: ${VERSION}]/" "$f"
fi
done
# -- STEP 5: Write updates.xml (Joomla update server) ---------------------
- name: "Step 5: Write updates.xml"
if: >-
steps.version.outputs.skip != 'true' &&
steps.check.outputs.already_released != 'true'
run: |
VERSION="${{ steps.version.outputs.version }}"
REPO="${{ github.repository }}"
# -- Parse extension metadata from XML manifest ----------------
MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
if [ -z "$MANIFEST" ]; then
echo "Warning: No Joomla XML manifest found — skipping updates.xml" >> $GITHUB_STEP_SUMMARY
exit 0
fi
# Extract fields using sed (portable — no grep -P)
EXT_NAME=$(sed -n 's/.*<name>\([^<]*\)<\/name>.*/\1/p' "$MANIFEST" | head -1)
EXT_TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
EXT_ELEMENT=$(sed -n 's/.*<element>\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" | head -1)
EXT_CLIENT=$(sed -n 's/.*<extension[^>]*client="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
EXT_FOLDER=$(sed -n 's/.*<extension[^>]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
TARGET_PLATFORM=$(sed -n 's/.*\(<targetplatform[^/]*\/>\).*/\1/p' "$MANIFEST" | head -1)
PHP_MINIMUM=$(sed -n 's/.*<php_minimum>\([^<]*\)<\/php_minimum>.*/\1/p' "$MANIFEST" | head -1)
# Fallbacks
[ -z "$EXT_NAME" ] && EXT_NAME="${{ github.event.repository.name }}"
[ -z "$EXT_TYPE" ] && EXT_TYPE="component"
# Templates/modules don't have <element> — derive from <name> (lowercased)
if [ -z "$EXT_ELEMENT" ]; then
EXT_ELEMENT=$(echo "$EXT_NAME" | tr '[:upper:]' '[:lower:]' | tr -d ' ')
fi
# Build client tag: plugins and frontend modules need <client>site</client>
CLIENT_TAG=""
if [ -n "$EXT_CLIENT" ]; then
CLIENT_TAG="<client>${EXT_CLIENT}</client>"
elif [ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]; then
CLIENT_TAG="<client>site</client>"
fi
# Build folder tag for plugins (required for Joomla to match the update)
FOLDER_TAG=""
if [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ]; then
FOLDER_TAG="<folder>${EXT_FOLDER}</folder>"
fi
# Build targetplatform (fallback to Joomla 5 if not in manifest)
if [ -z "$TARGET_PLATFORM" ]; then
TARGET_PLATFORM=$(printf '<targetplatform name="joomla" version="5.*" %s>' "/")
fi
# Build php_minimum tag
PHP_TAG=""
if [ -n "$PHP_MINIMUM" ]; then
PHP_TAG="<php_minimum>${PHP_MINIMUM}</php_minimum>"
fi
DOWNLOAD_URL="https://github.com/${REPO}/releases/download/v${VERSION}/${EXT_ELEMENT}-${VERSION}.zip"
INFO_URL="https://github.com/${REPO}/releases/tag/v${VERSION}"
# -- Build stable entry to temp file ─────────────────────────
{
printf '%s\n' ' <update>'
printf '%s\n' " <name>${EXT_NAME}</name>"
printf '%s\n' " <description>${EXT_NAME} update</description>"
printf '%s\n' " <element>${EXT_ELEMENT}</element>"
printf '%s\n' " <type>${EXT_TYPE}</type>"
printf '%s\n' " <version>${VERSION}</version>"
[ -n "$CLIENT_TAG" ] && printf '%s\n' " ${CLIENT_TAG}"
[ -n "$FOLDER_TAG" ] && printf '%s\n' " ${FOLDER_TAG}"
printf '%s\n' ' <tags>'
printf '%s\n' ' <tag>stable</tag>'
printf '%s\n' ' </tags>'
printf '%s\n' " <infourl title=\"${EXT_NAME}\">${INFO_URL}</infourl>"
printf '%s\n' ' <downloads>'
printf '%s\n' " <downloadurl type=\"full\" format=\"zip\">${DOWNLOAD_URL}</downloadurl>"
printf '%s\n' ' </downloads>'
printf '%s\n' " ${TARGET_PLATFORM}"
[ -n "$PHP_TAG" ] && printf '%s\n' " ${PHP_TAG}"
printf '%s\n' ' <maintainer>Moko Consulting</maintainer>'
printf '%s\n' ' <maintainerurl>https://mokoconsulting.tech</maintainerurl>'
printf '%s\n' ' </update>'
} > /tmp/stable_entry.xml
# -- Write updates.xml preserving dev/rc entries ──────────────
# Extract existing entries for other stability levels
# Order reflects release workflow: development → alpha → beta → rc → stable
if [ -f "updates.xml" ]; then
printf 'import re, sys\n' > /tmp/extract.py
printf 'with open("updates.xml") as f: c = f.read()\n' >> /tmp/extract.py
printf 'tag = sys.argv[1]\n' >> /tmp/extract.py
printf 'm = re.search(r"( <update>.*?<tag>" + re.escape(tag) + r"</tag>.*?</update>)", c, re.DOTALL)\n' >> /tmp/extract.py
printf 'if m: print(m.group(1))\n' >> /tmp/extract.py
fi
DEV_ENTRY=$(python3 /tmp/extract.py development 2>/dev/null || true)
ALPHA_ENTRY=$(python3 /tmp/extract.py alpha 2>/dev/null || true)
BETA_ENTRY=$(python3 /tmp/extract.py beta 2>/dev/null || true)
RC_ENTRY=$(python3 /tmp/extract.py rc 2>/dev/null || true)
{
printf '%s\n' '<?xml version="1.0" encoding="utf-8"?>'
printf '%s\n' '<updates>'
[ -n "$DEV_ENTRY" ] && echo "$DEV_ENTRY"
[ -n "$ALPHA_ENTRY" ] && echo "$ALPHA_ENTRY"
[ -n "$BETA_ENTRY" ] && echo "$BETA_ENTRY"
[ -n "$RC_ENTRY" ] && echo "$RC_ENTRY"
cat /tmp/stable_entry.xml
printf '%s\n' '</updates>'
} > updates.xml
echo "updates.xml: ${VERSION} (stable + rc/dev preserved)" >> $GITHUB_STEP_SUMMARY
# -- Commit all changes ---------------------------------------------------
- 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.version.outputs.version }}"
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git add -A
git commit -m "chore(release): build ${VERSION} [skip ci]" \
--author="github-actions[bot] <github-actions[bot]@users.noreply.github.com>"
git push
# -- 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 GitHub Release ------------------------------
- name: "Step 7: GitHub Release"
if: >-
steps.version.outputs.skip != 'true' &&
steps.check.outputs.tag_exists != 'true'
env:
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
run: |
VERSION="${{ steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
BRANCH="${{ steps.version.outputs.branch }}"
MAJOR="${{ steps.version.outputs.major }}"
NOTES=$(php /tmp/mokostandards/api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null)
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
echo "$NOTES" > /tmp/release_notes.md
# Check if the major release already exists
EXISTING=$(gh release view "$RELEASE_TAG" --json tagName -q .tagName 2>/dev/null || true)
if [ -z "$EXISTING" ]; then
# First release for this major
gh release create "$RELEASE_TAG" \
--title "v${MAJOR} (latest: ${VERSION})" \
--notes-file /tmp/release_notes.md \
--target "$BRANCH"
echo "Release created: ${RELEASE_TAG} (${VERSION})" >> $GITHUB_STEP_SUMMARY
else
# Append version notes to existing major release
CURRENT_NOTES=$(gh release view "$RELEASE_TAG" --json body -q .body 2>/dev/null || true)
{
echo "$CURRENT_NOTES"
echo ""
echo "---"
echo "### ${VERSION}"
echo ""
cat /tmp/release_notes.md
} > /tmp/updated_notes.md
gh release edit "$RELEASE_TAG" \
--title "v${MAJOR} (latest: ${VERSION})" \
--notes-file /tmp/updated_notes.md
echo "Release updated: ${RELEASE_TAG} -> ${VERSION}" >> $GITHUB_STEP_SUMMARY
fi
# -- STEP 8: Build Joomla install ZIP + SHA-256 checksum ------------------
# Every patch builds an install-ready ZIP and uploads it to the minor release.
# Result: one Release per minor version with a ZIP for each patch.
- name: "Step 8: Build Joomla package and update checksum"
if: >-
steps.version.outputs.skip != 'true'
env:
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
run: |
VERSION="${{ steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
REPO="${{ github.repository }}"
# All ZIPs upload to the major release tag (vXX)
gh release view "$RELEASE_TAG" --json tagName > /dev/null 2>&1 || {
echo "No release ${RELEASE_TAG} found — skipping ZIP upload"
exit 0
}
# 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
EXT_ELEMENT=$(sed -n 's/.*<element>\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1 || basename "$MANIFEST" .xml)
ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip"
TAR_NAME="${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/ — skipping package"; exit 0; }
EXCLUDES=".ftpignore sftp-config* *.ppk *.pem *.key .env*"
# ZIP package
cd "$SOURCE_DIR"
zip -r "/tmp/${ZIP_NAME}" . -x $EXCLUDES
cd ..
# tar.gz package
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)
# -- Upload both to release tag ----------------------------------
gh release upload "$RELEASE_TAG" "/tmp/${ZIP_NAME}" --clobber 2>/dev/null || true
gh release upload "$RELEASE_TAG" "/tmp/${TAR_NAME}" --clobber 2>/dev/null || true
# -- Update updates.xml with both download formats ---------------
if [ -f "updates.xml" ]; then
ZIP_URL="https://github.com/${REPO}/releases/download/${RELEASE_TAG}/${ZIP_NAME}"
TAR_URL="https://github.com/${REPO}/releases/download/${RELEASE_TAG}/${TAR_NAME}"
# Replace downloads block with both formats + SHA
sed -i "s|<downloads>.*</downloads>|<downloads>\n <downloadurl type=\"full\" format=\"zip\">${ZIP_URL}</downloadurl>\n <downloadurl type=\"full\" format=\"tar.gz\">${TAR_URL}</downloadurl>\n </downloads>|" updates.xml 2>/dev/null || true
if grep -q '<sha256>' updates.xml; then
sed -i "s|<sha256>.*</sha256>|<sha256>sha256:${SHA256_ZIP}</sha256>|" updates.xml
else
sed -i "s|</downloads>|</downloads>\n <sha256>sha256:${SHA256_ZIP}</sha256>|" updates.xml
fi
git add updates.xml
git commit -m "chore(release): ZIP + tar.gz for ${VERSION} [skip ci]" \
--author="github-actions[bot] <github-actions[bot]@users.noreply.github.com>" || true
git push || true
fi
echo "### Joomla 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 | [${PACKAGE_NAME}](https://github.com/${REPO}/releases/download/${RELEASE_TAG}/${PACKAGE_NAME}) |" >> $GITHUB_STEP_SUMMARY
# -- Summary --------------------------------------------------------------
- name: Pipeline Summary
if: always()
run: |
VERSION="${{ steps.version.outputs.version }}"
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 (Joomla)" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
echo "|------|--------|" >> $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](https://github.com/${{ github.repository }}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
fi
-114
View File
@@ -1,114 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: GitHub.Workflow
# INGROUP: MokoStandards.Automation
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /templates/workflows/shared/branch-freeze.yml.template
# VERSION: 04.06.00
# BRIEF: Freeze or unfreeze any branch via ruleset — manual workflow_dispatch
name: Branch Freeze
on:
workflow_dispatch:
inputs:
branch:
description: 'Branch to freeze/unfreeze (e.g., version/04, dev/feature)'
required: true
type: string
action:
description: 'Action to perform'
required: true
type: choice
options:
- freeze
- unfreeze
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
permissions:
contents: read
jobs:
manage-freeze:
name: "${{ inputs.action }} branch: ${{ inputs.branch }}"
runs-on: ubuntu-latest
steps:
- name: Check permissions
env:
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
run: |
ACTOR="${{ github.actor }}"
REPO="${{ github.repository }}"
PERMISSION=$(gh api "repos/${REPO}/collaborators/${ACTOR}/permission" \
--jq '.permission' 2>/dev/null || echo "read")
if [ "$PERMISSION" != "admin" ]; then
echo "Denied: only admins can freeze/unfreeze branches (${ACTOR} has ${PERMISSION})"
exit 1
fi
- name: "${{ inputs.action }} branch"
env:
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
run: |
BRANCH="${{ inputs.branch }}"
ACTION="${{ inputs.action }}"
REPO="${{ github.repository }}"
RULESET_NAME="FROZEN: ${BRANCH}"
echo "## Branch Freeze" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ "$ACTION" = "freeze" ]; then
# Check if ruleset already exists
EXISTING=$(gh api "repos/${REPO}/rulesets" \
--jq ".[] | select(.name == \"${RULESET_NAME}\") | .id" 2>/dev/null || true)
if [ -n "$EXISTING" ]; then
echo "Branch \`${BRANCH}\` is already frozen (ruleset #${EXISTING})" >> $GITHUB_STEP_SUMMARY
exit 0
fi
# Create freeze ruleset — blocks all updates except admin bypass
printf '{"name":"%s","target":"branch","enforcement":"active",' "${RULESET_NAME}" > /tmp/ruleset.json
printf '"bypass_actors":[{"actor_id":5,"actor_type":"RepositoryRole","bypass_mode":"always"}],' >> /tmp/ruleset.json
printf '"conditions":{"ref_name":{"include":["refs/heads/%s"],"exclude":[]}},' "${BRANCH}" >> /tmp/ruleset.json
printf '"rules":[{"type":"update"},{"type":"deletion"},{"type":"non_fast_forward"}]}' >> /tmp/ruleset.json
RESULT=$(gh api "repos/${REPO}/rulesets" -X POST --input /tmp/ruleset.json --jq '.id' 2>&1) || true
if echo "$RESULT" | grep -qE '^[0-9]+$'; then
echo "Frozen \`${BRANCH}\` — ruleset #${RESULT}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Branch | \`${BRANCH}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Ruleset | #${RESULT} |" >> $GITHUB_STEP_SUMMARY
echo "| Rules | No updates, no deletion, no force push |" >> $GITHUB_STEP_SUMMARY
echo "| Bypass | Repository admins only |" >> $GITHUB_STEP_SUMMARY
else
echo "Failed to freeze: ${RESULT}" >> $GITHUB_STEP_SUMMARY
exit 1
fi
elif [ "$ACTION" = "unfreeze" ]; then
# Find and delete the freeze ruleset
RULESET_ID=$(gh api "repos/${REPO}/rulesets" \
--jq ".[] | select(.name == \"${RULESET_NAME}\") | .id" 2>/dev/null || true)
if [ -z "$RULESET_ID" ]; then
echo "Branch \`${BRANCH}\` is not frozen (no ruleset found)" >> $GITHUB_STEP_SUMMARY
exit 0
fi
gh api "repos/${REPO}/rulesets/${RULESET_ID}" -X DELETE --silent 2>/dev/null
echo "Unfrozen \`${BRANCH}\` — ruleset #${RULESET_ID} deleted" >> $GITHUB_STEP_SUMMARY
fi
rm -f /tmp/ruleset.json
@@ -1,99 +0,0 @@
# 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: GitHub.Workflow.Template
# INGROUP: MokoStandards.CI
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /templates/workflows/shared/changelog-validation.yml.template
# VERSION: 04.06.00
# BRIEF: Validates CHANGELOG.md format and version consistency
# NOTE: Deployed to .github/workflows/changelog-validation.yml in governed repos.
name: Changelog Validation
on:
pull_request:
branches:
- main
- 'dev/**'
workflow_dispatch:
permissions:
contents: read
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
validate-changelog:
name: Validate CHANGELOG.md
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Check CHANGELOG.md exists
run: |
echo "### Changelog Validation" >> $GITHUB_STEP_SUMMARY
if [ ! -f "CHANGELOG.md" ]; then
echo "CHANGELOG.md not found in repository root." >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "CHANGELOG.md exists." >> $GITHUB_STEP_SUMMARY
- name: Check VERSION header matches README.md
run: |
# Extract version from README.md FILE INFORMATION block
README_VERSION=$(grep -oP '^\s*VERSION:\s*\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' README.md | head -1)
if [ -z "$README_VERSION" ]; then
echo "No VERSION found in README.md FILE INFORMATION block." >> $GITHUB_STEP_SUMMARY
exit 1
fi
# Check that CHANGELOG.md has a matching version header
CHANGELOG_VERSION=$(grep -oP '^\#\#\s*\[\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' CHANGELOG.md | head -1)
if [ -z "$CHANGELOG_VERSION" ]; then
echo "No version header found in CHANGELOG.md (expected \`## [XX.YY.ZZ] - YYYY-MM-DD\`)." >> $GITHUB_STEP_SUMMARY
exit 1
fi
if [ "$CHANGELOG_VERSION" != "$README_VERSION" ]; then
echo "CHANGELOG latest version \`${CHANGELOG_VERSION}\` does not match README VERSION \`${README_VERSION}\`." >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "CHANGELOG version \`${CHANGELOG_VERSION}\` matches README VERSION." >> $GITHUB_STEP_SUMMARY
- name: Validate conventional changelog format
run: |
ERRORS=0
# Check that version entries follow ## [XX.YY.ZZ] - YYYY-MM-DD format
while IFS= read -r LINE; do
if ! echo "$LINE" | grep -qP '^\#\#\s*\[[0-9]{2}\.[0-9]{2}\.[0-9]{2}\]\s*-\s*[0-9]{4}-[0-9]{2}-[0-9]{2}'; then
echo "Malformed version header: \`${LINE}\`" >> $GITHUB_STEP_SUMMARY
echo " Expected format: \`## [XX.YY.ZZ] - YYYY-MM-DD\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
done < <(grep -P '^\#\#\s*\[' CHANGELOG.md)
ENTRY_COUNT=$(grep -cP '^\#\#\s*\[' CHANGELOG.md || echo "0")
if [ "$ENTRY_COUNT" -eq 0 ]; then
echo "No version entries found in CHANGELOG.md." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "Found ${ENTRY_COUNT} version entr(ies) in CHANGELOG.md." >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${ERRORS}" -gt 0 ]; then
echo "**${ERRORS} format issue(s) found.**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "**Changelog format validation passed.**" >> $GITHUB_STEP_SUMMARY
fi
-137
View File
@@ -1,137 +0,0 @@
# GitHub Copilot Configuration
# This file configures GitHub Copilot settings for the repository
# Allowed domains for Copilot to access
# These domains are trusted sources that Copilot can fetch information from
allowed_domains:
# Standard license providers
- "www.gnu.org" # GNU licenses (GPL, LGPL, AGPL)
- "opensource.org" # Open Source Initiative
- "choosealicense.com" # GitHub's license chooser
- "spdx.org" # Software Package Data Exchange
- "creativecommons.org" # Creative Commons licenses
- "apache.org" # Apache Software Foundation
- "fsf.org" # Free Software Foundation
# Documentation and standards
- "semver.org" # Semantic Versioning
- "keepachangelog.com" # Changelog standards
- "conventionalcommits.org" # Commit message standards
# GitHub and related
- "github.com" # GitHub main site
- "docs.github.com" # GitHub documentation
- "raw.githubusercontent.com" # GitHub raw content
# Package managers and registries
- "npmjs.com" # npm registry
- "pypi.org" # Python Package Index
- "packagist.org" # PHP Composer packages
- "rubygems.org" # Ruby gems
# Standards and specifications
- "json-schema.org" # JSON Schema
- "w3.org" # W3C standards
- "ietf.org" # IETF RFCs and standards
# PHP and Joomla specific
- "joomla.org" # Joomla CMS
- "docs.joomla.org" # Joomla documentation
- "downloads.joomla.org" # Joomla core downloads
- "php.net" # PHP documentation
- "getcomposer.org" # Composer dependency manager
- "packagist.org" # Composer package registry (also listed under packages)
# Dolibarr specific
- "dolibarr.org" # Dolibarr ERP/CRM
- "wiki.dolibarr.org" # Dolibarr wiki
- "docs.dolibarr.org" # Dolibarr developer documentation
# Moko Consulting
- "mokoconsulting.tech" # Moko Consulting main site
- "*.mokoconsulting.tech" # All Moko Consulting subdomains (API, docs, CDN, etc.)
# Google services
- "drive.google.com" # Google Drive (file sharing and assets)
- "docs.google.com" # Google Docs
- "sheets.google.com" # Google Sheets
- "accounts.google.com" # Google authentication
- "storage.googleapis.com" # Google Cloud Storage
- "*.googleapis.com" # Google APIs (Maps, Fonts, etc.)
- "*.googleusercontent.com" # Google user-uploaded content and CDN
- "fonts.googleapis.com" # Google Fonts CSS
- "fonts.gstatic.com" # Google Fonts static assets
# GitHub extended
- "api.github.com" # GitHub REST API
- "upload.github.com" # GitHub file uploads
- "objects.githubusercontent.com" # GitHub release assets and LFS
- "user-images.githubusercontent.com" # GitHub issue/PR image attachments
- "codeload.github.com" # GitHub archive downloads
- "ghcr.io" # GitHub Container Registry
- "pkg.github.com" # GitHub Packages
# Developer reference
- "developer.mozilla.org" # MDN Web Docs
- "stackoverflow.com" # Stack Overflow
- "git-scm.com" # Git documentation
# CDN and infrastructure
- "cdn.jsdelivr.net" # jsDelivr CDN
- "unpkg.com" # unpkg CDN
- "cdnjs.cloudflare.com" # Cloudflare CDN
- "img.shields.io" # Shields.io badge images
- "shields.io" # Shields.io badge service
# Container registries
- "hub.docker.com" # Docker Hub
- "registry-1.docker.io" # Docker registry pulls
- "index.docker.io" # Docker index
# CI / code quality
- "codecov.io" # Code coverage reporting
- "coveralls.io" # Coveralls coverage service
- "sonarcloud.io" # SonarCloud static analysis
# Terraform / infrastructure
- "registry.terraform.io" # Terraform provider registry
- "releases.hashicorp.com" # HashiCorp release downloads
- "checkpoint-api.hashicorp.com" # HashiCorp update checks
# Settings for code generation and suggestions
copilot:
# Enable Copilot for this repository
enabled: true
# File patterns to include for Copilot suggestions
include:
- "**/*.py"
- "**/*.js"
- "**/*.php"
- "**/*.md"
- "**/*.yml"
- "**/*.yaml"
- "**/*.json"
- "**/*.xml"
- "**/*.sh"
# File patterns to exclude from Copilot suggestions
exclude:
- "**/node_modules/**"
- "**/vendor/**"
- "**/build/**"
- "**/dist/**"
- "**/.git/**"
- "**/LICENSE"
- "**/CHANGELOG.md"
# Notes:
# ------
# - This configuration allows GitHub Copilot to fetch information from trusted sources
# - License providers are included to help with license text and compliance information
# - Package registries help with dependency management and version checking
# - Standards organizations provide authoritative specifications
# - Platform-specific sites (Joomla, Dolibarr, PHP) support our technology stack
# - All domains listed are well-known, reputable sources in their respective domains
# - This list focuses on read-only access to public information
# - No authentication credentials should be used with these domains
@@ -1,758 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# This file is part of a Moko Consulting project.
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# FILE INFORMATION
# DEFGROUP: GitHub.Workflow
# INGROUP: MokoStandards.Firewall
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /templates/workflows/shared/enterprise-firewall-setup.yml.template
# VERSION: 04.06.00
# BRIEF: Enterprise firewall configuration — generates outbound allow-rules including SFTP deployment server
# NOTE: Reads DEV_FTP_HOST / DEV_FTP_PORT variables to include SFTP egress rules alongside HTTPS rules.
name: Enterprise Firewall Configuration
# This workflow provides firewall configuration guidance for enterprise-ready sites
# It generates firewall rules for allowing outbound access to trusted domains
# including license providers, documentation sources, package registries,
# and the SFTP deployment server (DEV_FTP_HOST / DEV_FTP_PORT).
#
# Runs automatically when:
# - Coding agent workflows are triggered (pull requests with copilot/ prefix)
# - Manual workflow dispatch for custom configurations
on:
workflow_dispatch:
inputs:
firewall_type:
description: 'Target firewall type'
required: true
type: choice
options:
- 'iptables'
- 'ufw'
- 'firewalld'
- 'aws-security-group'
- 'azure-nsg'
- 'gcp-firewall'
- 'cloudflare'
- 'all'
default: 'all'
output_format:
description: 'Output format'
required: true
type: choice
options:
- 'shell-script'
- 'json'
- 'yaml'
- 'markdown'
- 'all'
default: 'markdown'
# Auto-run when coding agent creates or updates PRs
pull_request:
branches:
- 'copilot/**'
- 'agent/**'
types: [opened, synchronize, reopened]
# Auto-run on push to coding agent branches
push:
branches:
- 'copilot/**'
- 'agent/**'
permissions:
contents: read
actions: read
jobs:
generate-firewall-rules:
name: Generate Firewall Rules
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.11'
- name: Apply Firewall Rules to Runner (Auto-run only)
if: github.event_name != 'workflow_dispatch'
env:
DEV_FTP_HOST: ${{ vars.DEV_FTP_HOST }}
DEV_FTP_PORT: ${{ vars.DEV_FTP_PORT }}
run: |
echo "🔥 Applying firewall rules for coding agent environment..."
echo ""
echo "This step ensures the GitHub Actions runner can access trusted domains"
echo "including license providers, package registries, and documentation sources."
echo ""
# Note: GitHub Actions runners are ephemeral and run in controlled environments
# This step documents what domains are being accessed during the workflow
# Actual firewall configuration is managed by GitHub
cat > /tmp/trusted-domains.txt << 'EOF'
# Trusted domains for coding agent environment
# License Providers
www.gnu.org
opensource.org
choosealicense.com
spdx.org
creativecommons.org
apache.org
fsf.org
# Documentation & Standards
semver.org
keepachangelog.com
conventionalcommits.org
# GitHub & Related
github.com
api.github.com
docs.github.com
raw.githubusercontent.com
ghcr.io
# Package Registries
npmjs.com
registry.npmjs.org
pypi.org
files.pythonhosted.org
packagist.org
repo.packagist.org
rubygems.org
# Platform-Specific
joomla.org
downloads.joomla.org
docs.joomla.org
php.net
getcomposer.org
dolibarr.org
wiki.dolibarr.org
docs.dolibarr.org
# Moko Consulting
mokoconsulting.tech
# SFTP Deployment Server (DEV_FTP_HOST)
${DEV_FTP_HOST:-<not configured>}
# Google Services
drive.google.com
docs.google.com
sheets.google.com
accounts.google.com
storage.googleapis.com
fonts.googleapis.com
fonts.gstatic.com
# GitHub Extended
upload.github.com
objects.githubusercontent.com
user-images.githubusercontent.com
codeload.github.com
pkg.github.com
# Developer Reference
developer.mozilla.org
stackoverflow.com
git-scm.com
# CDN & Infrastructure
cdn.jsdelivr.net
unpkg.com
cdnjs.cloudflare.com
img.shields.io
# Container Registries
hub.docker.com
registry-1.docker.io
# CI & Code Quality
codecov.io
sonarcloud.io
# Terraform & Infrastructure
registry.terraform.io
releases.hashicorp.com
checkpoint-api.hashicorp.com
EOF
echo "✓ Trusted domains documented for this runner"
echo "✓ GitHub Actions runners have network access to these domains"
echo ""
# Test connectivity to key domains
echo "Testing connectivity to key domains..."
for domain in "github.com" "www.gnu.org" "npmjs.com" "pypi.org"; do
if curl -s --max-time 3 -o /dev/null -w "%{http_code}" "https://$domain" | grep -q "200\|301\|302"; then
echo " ✓ $domain is accessible"
else
echo " ⚠️ $domain connectivity check failed (may be expected)"
fi
done
# Test SFTP server connectivity (TCP port check)
SFTP_HOST="${DEV_FTP_HOST:-}"
SFTP_PORT="${DEV_FTP_PORT:-22}"
if [ -n "$SFTP_HOST" ]; then
# Strip any embedded :port suffix
SFTP_HOST="${SFTP_HOST%%:*}"
echo ""
echo "Testing SFTP deployment server connectivity..."
if timeout 5 bash -c "echo >/dev/tcp/${SFTP_HOST}/${SFTP_PORT}" 2>/dev/null; then
echo " ✓ SFTP server ${SFTP_HOST}:${SFTP_PORT} is reachable"
else
echo " ⚠️ SFTP server ${SFTP_HOST}:${SFTP_PORT} is not reachable from runner (firewall rule needed)"
fi
else
echo ""
echo " ️ DEV_FTP_HOST not configured — skipping SFTP connectivity check"
fi
- name: Generate Firewall Configuration
id: generate
env:
DEV_FTP_HOST: ${{ vars.DEV_FTP_HOST }}
DEV_FTP_PORT: ${{ vars.DEV_FTP_PORT }}
run: |
cat > generate_firewall_config.py << 'PYTHON_EOF'
#!/usr/bin/env python3
"""
Enterprise Firewall Configuration Generator
Generates firewall rules for enterprise-ready deployments allowing
access to trusted domains including license providers, documentation
sources, package registries, and platform-specific sites.
"""
import json
import os
import yaml
import sys
from typing import List, Dict
# SFTP deployment server from org variables
_sftp_host_raw = os.environ.get("DEV_FTP_HOST", "").strip()
_sftp_port = os.environ.get("DEV_FTP_PORT", "").strip() or "22"
# Strip embedded :port suffix if present
_sftp_host = _sftp_host_raw.split(":")[0] if _sftp_host_raw else ""
if ":" in _sftp_host_raw and not _sftp_port:
_sftp_port = _sftp_host_raw.split(":")[1]
SFTP_HOST = _sftp_host
SFTP_PORT = int(_sftp_port) if _sftp_port.isdigit() else 22
# Trusted domains from .github/copilot.yml
TRUSTED_DOMAINS = {
"license_providers": [
"www.gnu.org",
"opensource.org",
"choosealicense.com",
"spdx.org",
"creativecommons.org",
"apache.org",
"fsf.org",
],
"documentation_standards": [
"semver.org",
"keepachangelog.com",
"conventionalcommits.org",
],
"github_related": [
"github.com",
"api.github.com",
"docs.github.com",
"raw.githubusercontent.com",
"ghcr.io",
],
"package_registries": [
"npmjs.com",
"registry.npmjs.org",
"pypi.org",
"files.pythonhosted.org",
"packagist.org",
"repo.packagist.org",
"rubygems.org",
],
"standards_organizations": [
"json-schema.org",
"w3.org",
"ietf.org",
],
"platform_specific": [
"joomla.org",
"downloads.joomla.org",
"docs.joomla.org",
"php.net",
"getcomposer.org",
"dolibarr.org",
"wiki.dolibarr.org",
"docs.dolibarr.org",
],
"moko_consulting": [
"mokoconsulting.tech",
],
"google_services": [
"drive.google.com",
"docs.google.com",
"sheets.google.com",
"accounts.google.com",
"storage.googleapis.com",
"fonts.googleapis.com",
"fonts.gstatic.com",
],
"github_extended": [
"upload.github.com",
"objects.githubusercontent.com",
"user-images.githubusercontent.com",
"codeload.github.com",
"pkg.github.com",
],
"developer_reference": [
"developer.mozilla.org",
"stackoverflow.com",
"git-scm.com",
],
"cdn_and_infrastructure": [
"cdn.jsdelivr.net",
"unpkg.com",
"cdnjs.cloudflare.com",
"img.shields.io",
],
"container_registries": [
"hub.docker.com",
"registry-1.docker.io",
],
"ci_code_quality": [
"codecov.io",
"sonarcloud.io",
],
"terraform_infrastructure": [
"registry.terraform.io",
"releases.hashicorp.com",
"checkpoint-api.hashicorp.com",
],
}
# Inject SFTP deployment server as a separate category (port 22, not 443)
if SFTP_HOST:
TRUSTED_DOMAINS["sftp_deployment_server"] = [SFTP_HOST]
print(f"️ SFTP deployment server: {SFTP_HOST}:{SFTP_PORT}")
def generate_sftp_iptables_rules(host: str, port: int) -> str:
"""Generate iptables rules specifically for SFTP egress"""
return (
f"# Allow SFTP to deployment server {host}:{port}\n"
f"iptables -A OUTPUT -p tcp -d $(dig +short {host} | head -1)"
f" --dport {port} -j ACCEPT # SFTP deploy\n"
)
def generate_sftp_ufw_rules(host: str, port: int) -> str:
"""Generate UFW rules for SFTP egress"""
return (
f"# Allow SFTP to deployment server\n"
f"ufw allow out to $(dig +short {host} | head -1)"
f" port {port} proto tcp comment 'SFTP deploy to {host}'\n"
)
def generate_sftp_firewalld_rules(host: str, port: int) -> str:
"""Generate firewalld rules for SFTP egress"""
return (
f"# Allow SFTP to deployment server\n"
f"firewall-cmd --permanent --add-rich-rule='"
f"rule family=ipv4 destination address=$(dig +short {host} | head -1)"
f" port port={port} protocol=tcp accept' # SFTP deploy\n"
)
def generate_iptables_rules(domains: List[str]) -> str:
"""Generate iptables firewall rules"""
rules = ["#!/bin/bash", "", "# Enterprise Firewall Rules - iptables", ""]
rules.append("# Allow outbound HTTPS to trusted domains")
rules.append("")
for domain in domains:
rules.append(f"# Allow {domain}")
rules.append(f"iptables -A OUTPUT -p tcp -d $(dig +short {domain} | head -1) --dport 443 -j ACCEPT")
rules.append("")
rules.append("# Allow DNS lookups")
rules.append("iptables -A OUTPUT -p udp --dport 53 -j ACCEPT")
rules.append("iptables -A OUTPUT -p tcp --dport 53 -j ACCEPT")
return "\n".join(rules)
def generate_ufw_rules(domains: List[str]) -> str:
"""Generate UFW firewall rules"""
rules = ["#!/bin/bash", "", "# Enterprise Firewall Rules - UFW", ""]
rules.append("# Allow outbound HTTPS to trusted domains")
rules.append("")
for domain in domains:
rules.append(f"# Allow {domain}")
rules.append(f"ufw allow out to $(dig +short {domain} | head -1) port 443 proto tcp comment 'Allow {domain}'")
rules.append("")
rules.append("# Allow DNS")
rules.append("ufw allow out 53/udp comment 'Allow DNS UDP'")
rules.append("ufw allow out 53/tcp comment 'Allow DNS TCP'")
return "\n".join(rules)
def generate_firewalld_rules(domains: List[str]) -> str:
"""Generate firewalld rules"""
rules = ["#!/bin/bash", "", "# Enterprise Firewall Rules - firewalld", ""]
rules.append("# Add trusted domains to firewall")
rules.append("")
for domain in domains:
rules.append(f"# Allow {domain}")
rules.append(f"firewall-cmd --permanent --add-rich-rule='rule family=ipv4 destination address=$(dig +short {domain} | head -1) port port=443 protocol=tcp accept'")
rules.append("")
rules.append("# Reload firewall")
rules.append("firewall-cmd --reload")
return "\n".join(rules)
def generate_aws_security_group(domains: List[str]) -> Dict:
"""Generate AWS Security Group rules (JSON format)"""
rules = {
"SecurityGroupRules": {
"Egress": []
}
}
for domain in domains:
rules["SecurityGroupRules"]["Egress"].append({
"Description": f"Allow HTTPS to {domain}",
"IpProtocol": "tcp",
"FromPort": 443,
"ToPort": 443,
"CidrIp": "0.0.0.0/0", # In practice, resolve to specific IPs
"Tags": [{
"Key": "Domain",
"Value": domain
}]
})
# Add DNS
rules["SecurityGroupRules"]["Egress"].append({
"Description": "Allow DNS",
"IpProtocol": "udp",
"FromPort": 53,
"ToPort": 53,
"CidrIp": "0.0.0.0/0"
})
return rules
def generate_markdown_documentation(domains_by_category: Dict[str, List[str]]) -> str:
"""Generate markdown documentation"""
md = ["# Enterprise Firewall Configuration Guide", ""]
md.append("## Overview")
md.append("")
md.append("This document provides firewall configuration guidance for enterprise-ready deployments.")
md.append("It lists trusted domains that should be whitelisted for outbound access to ensure")
md.append("proper functionality of license validation, package management, and documentation access.")
md.append("")
md.append("## Trusted Domains by Category")
md.append("")
all_domains = []
for category, domains in domains_by_category.items():
category_name = category.replace("_", " ").title()
md.append(f"### {category_name}")
md.append("")
md.append("| Domain | Purpose |")
md.append("|--------|---------|")
for domain in domains:
all_domains.append(domain)
purpose = get_domain_purpose(domain)
md.append(f"| `{domain}` | {purpose} |")
md.append("")
md.append("## Implementation Examples")
md.append("")
md.append("### iptables Example")
md.append("")
md.append("```bash")
md.append("# Allow HTTPS to trusted domain")
md.append(f"iptables -A OUTPUT -p tcp -d $(dig +short {all_domains[0]}) --dport 443 -j ACCEPT")
md.append("```")
md.append("")
md.append("### UFW Example")
md.append("")
md.append("```bash")
md.append("# Allow HTTPS to trusted domain")
md.append(f"ufw allow out to {all_domains[0]} port 443 proto tcp")
md.append("```")
md.append("")
md.append("### AWS Security Group Example")
md.append("")
md.append("```json")
md.append("{")
md.append(' "IpPermissions": [{')
md.append(' "IpProtocol": "tcp",')
md.append(' "FromPort": 443,')
md.append(' "ToPort": 443,')
md.append(' "IpRanges": [{"CidrIp": "0.0.0.0/0", "Description": "HTTPS to trusted domains"}]')
md.append(" }]")
md.append("}")
md.append("```")
md.append("")
md.append("## Ports Required")
md.append("")
md.append("| Port | Protocol | Purpose |")
md.append("|------|----------|---------|")
md.append("| 443 | TCP | HTTPS (secure web access) |")
md.append("| 80 | TCP | HTTP (redirects to HTTPS) |")
md.append("| 53 | UDP/TCP | DNS resolution |")
md.append("")
md.append("## Security Considerations")
md.append("")
md.append("1. **DNS Resolution**: Ensure DNS queries are allowed (port 53 UDP/TCP)")
md.append("2. **Certificate Validation**: HTTPS requires ability to reach certificate authorities")
md.append("3. **Dynamic IPs**: Some domains use CDNs with dynamic IPs - consider using FQDNs in rules")
md.append("4. **Regular Updates**: Review and update whitelist as services change")
md.append("5. **Logging**: Enable logging for blocked connections to identify missing rules")
md.append("")
md.append("## Compliance Notes")
md.append("")
md.append("- All listed domains provide read-only access to public information")
md.append("- License providers enable GPL compliance verification")
md.append("- Package registries support dependency security scanning")
md.append("- No authentication credentials are transmitted to these domains")
md.append("")
return "\n".join(md)
def get_domain_purpose(domain: str) -> str:
"""Get human-readable purpose for a domain"""
purposes = {
"www.gnu.org": "GNU licenses and documentation",
"opensource.org": "Open Source Initiative resources",
"choosealicense.com": "GitHub license selection tool",
"spdx.org": "Software Package Data Exchange identifiers",
"creativecommons.org": "Creative Commons licenses",
"apache.org": "Apache Software Foundation licenses",
"fsf.org": "Free Software Foundation resources",
"semver.org": "Semantic versioning specification",
"keepachangelog.com": "Changelog format standards",
"conventionalcommits.org": "Commit message conventions",
"github.com": "GitHub platform access",
"api.github.com": "GitHub API access",
"docs.github.com": "GitHub documentation",
"raw.githubusercontent.com": "GitHub raw content access",
"npmjs.com": "npm package registry",
"pypi.org": "Python Package Index",
"packagist.org": "PHP Composer package registry",
"rubygems.org": "Ruby gems registry",
"joomla.org": "Joomla CMS platform",
"php.net": "PHP documentation and downloads",
"dolibarr.org": "Dolibarr ERP/CRM platform",
}
return purposes.get(domain, "Trusted resource")
def main():
# Use inputs if provided (manual dispatch), otherwise use defaults (auto-run)
firewall_type = "${{ github.event.inputs.firewall_type }}" or "all"
output_format = "${{ github.event.inputs.output_format }}" or "markdown"
print(f"Running in {'manual' if '${{ github.event.inputs.firewall_type }}' else 'automatic'} mode")
print(f"Firewall type: {firewall_type}")
print(f"Output format: {output_format}")
print("")
# Collect all domains
all_domains = []
for domains in TRUSTED_DOMAINS.values():
all_domains.extend(domains)
# Remove duplicates and sort
all_domains = sorted(set(all_domains))
print(f"Generating firewall rules for {len(all_domains)} trusted domains...")
print("")
# Exclude SFTP server from HTTPS rule generation (different port)
https_domains = [d for d in all_domains if d != SFTP_HOST]
# Generate based on firewall type
if firewall_type in ["iptables", "all"]:
rules = generate_iptables_rules(https_domains)
if SFTP_HOST:
rules += "\n# ── SFTP Deployment Server ──────────────────────────────\n"
rules += generate_sftp_iptables_rules(SFTP_HOST, SFTP_PORT)
with open("firewall-rules-iptables.sh", "w") as f:
f.write(rules)
print("✓ Generated iptables rules: firewall-rules-iptables.sh")
if firewall_type in ["ufw", "all"]:
rules = generate_ufw_rules(https_domains)
if SFTP_HOST:
rules += "\n# ── SFTP Deployment Server ──────────────────────────────\n"
rules += generate_sftp_ufw_rules(SFTP_HOST, SFTP_PORT)
with open("firewall-rules-ufw.sh", "w") as f:
f.write(rules)
print("✓ Generated UFW rules: firewall-rules-ufw.sh")
if firewall_type in ["firewalld", "all"]:
rules = generate_firewalld_rules(https_domains)
if SFTP_HOST:
rules += "\n# ── SFTP Deployment Server ──────────────────────────────\n"
rules += generate_sftp_firewalld_rules(SFTP_HOST, SFTP_PORT)
with open("firewall-rules-firewalld.sh", "w") as f:
f.write(rules)
print("✓ Generated firewalld rules: firewall-rules-firewalld.sh")
if firewall_type in ["aws-security-group", "all"]:
rules = generate_aws_security_group(all_domains)
with open("firewall-rules-aws-sg.json", "w") as f:
json.dump(rules, f, indent=2)
print("✓ Generated AWS Security Group rules: firewall-rules-aws-sg.json")
if output_format in ["yaml", "all"]:
with open("trusted-domains.yml", "w") as f:
yaml.dump(TRUSTED_DOMAINS, f, default_flow_style=False)
print("✓ Generated YAML domain list: trusted-domains.yml")
if output_format in ["json", "all"]:
with open("trusted-domains.json", "w") as f:
json.dump(TRUSTED_DOMAINS, f, indent=2)
print("✓ Generated JSON domain list: trusted-domains.json")
if output_format in ["markdown", "all"]:
md = generate_markdown_documentation(TRUSTED_DOMAINS)
with open("FIREWALL_CONFIGURATION.md", "w") as f:
f.write(md)
print("✓ Generated documentation: FIREWALL_CONFIGURATION.md")
print("")
print("Domain Categories:")
for category, domains in TRUSTED_DOMAINS.items():
print(f" - {category}: {len(domains)} domains")
print("")
print("Total unique domains: ", len(all_domains))
if __name__ == "__main__":
main()
PYTHON_EOF
chmod +x generate_firewall_config.py
pip install PyYAML
python3 generate_firewall_config.py
- name: Upload Firewall Configuration Artifacts
uses: actions/upload-artifact@v6
with:
name: firewall-configurations
path: |
firewall-rules-*.sh
firewall-rules-*.json
trusted-domains.*
FIREWALL_CONFIGURATION.md
retention-days: 90
- name: Display Summary
run: |
echo "## Firewall Configuration" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "**Mode**: Manual Execution" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Firewall rules have been generated for enterprise-ready deployments." >> $GITHUB_STEP_SUMMARY
else
echo "**Mode**: Automatic Execution (Coding Agent Active)" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "This workflow ran automatically because a coding agent (GitHub Copilot) is active." >> $GITHUB_STEP_SUMMARY
echo "Firewall configuration has been validated for the coding agent environment." >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Files Generated" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if ls firewall-rules-* trusted-domains.* FIREWALL_CONFIGURATION.md 2>/dev/null; then
ls -lh firewall-rules-* trusted-domains.* FIREWALL_CONFIGURATION.md 2>/dev/null | awk '{print "- " $9 " (" $5 ")"}' >> $GITHUB_STEP_SUMMARY
else
echo "- Documentation generated" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "### Download Artifacts" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Download the generated firewall configurations from the workflow artifacts." >> $GITHUB_STEP_SUMMARY
else
echo "### Trusted Domains Active" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "The coding agent has access to:" >> $GITHUB_STEP_SUMMARY
echo "- License providers (GPL, OSI, SPDX, Apache, etc.)" >> $GITHUB_STEP_SUMMARY
echo "- Package registries (npm, PyPI, Packagist, RubyGems)" >> $GITHUB_STEP_SUMMARY
echo "- Documentation sources (GitHub, Joomla, Dolibarr, PHP)" >> $GITHUB_STEP_SUMMARY
echo "- Standards organizations (W3C, IETF, JSON Schema)" >> $GITHUB_STEP_SUMMARY
fi
# Usage Instructions:
#
# This workflow runs in two modes:
#
# 1. AUTOMATIC MODE (Coding Agent):
# - Triggers when coding agent branches (copilot/**, agent/**) are pushed or PR'd
# - Validates firewall configuration for the coding agent environment
# - Documents accessible domains for compliance
# - Ensures license sources and package registries are available
#
# 2. MANUAL MODE (Enterprise Configuration):
# - Manually trigger from the Actions tab
# - Select desired firewall type and output format
# - Download generated artifacts
# - Apply firewall rules to your enterprise environment
#
# Configuration:
# - Trusted domains are sourced from .github/copilot.yml
# - Modify copilot.yml to add/remove trusted domains
# - Changes automatically propagate to firewall rules
#
# Important Notes:
# - Review generated rules before applying to production
# - Some domains may use CDNs with dynamic IPs
# - Consider using FQDN-based rules where supported
# - Test thoroughly in staging environment first
# - Monitor logs for blocked connections
# - Update rules as domains/services change
-525
View File
@@ -1,525 +0,0 @@
# 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: GitHub.Workflow
# INGROUP: MokoStandards.Maintenance
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /templates/workflows/shared/repository-cleanup.yml.template
# VERSION: 04.06.00
# BRIEF: Recurring repository maintenance — labels, branches, workflows, logs, doc indexes
# NOTE: Synced via bulk-repo-sync to .github/workflows/repository-cleanup.yml in all governed repos.
# Runs on the 1st and 15th of each month at 6:00 AM UTC, and on manual dispatch.
name: Repository Cleanup
on:
schedule:
- cron: '0 6 1,15 * *'
workflow_dispatch:
inputs:
reset_labels:
description: 'Delete ALL existing labels and recreate the standard set'
type: boolean
default: false
clean_branches:
description: 'Delete old chore/sync-mokostandards-* branches'
type: boolean
default: true
clean_workflows:
description: 'Delete orphaned workflow runs (cancelled, stale)'
type: boolean
default: true
clean_logs:
description: 'Delete workflow run logs older than 30 days'
type: boolean
default: true
fix_templates:
description: 'Strip copyright comment blocks from issue templates'
type: boolean
default: true
rebuild_indexes:
description: 'Rebuild docs/ index files'
type: boolean
default: true
delete_closed_issues:
description: 'Delete issues that have been closed for more than 30 days'
type: boolean
default: false
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
permissions:
contents: write
issues: write
actions: write
jobs:
cleanup:
name: Repository Maintenance
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.GH_TOKEN || github.token }}
fetch-depth: 0
- name: Check actor permission
env:
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
run: |
ACTOR="${{ github.actor }}"
# Schedule triggers use github-actions[bot]
if [ "${{ github.event_name }}" = "schedule" ]; then
echo "✅ Scheduled run — authorized"
exit 0
fi
AUTHORIZED_USERS="jmiller-moko github-actions[bot]"
for user in $AUTHORIZED_USERS; do
if [ "$ACTOR" = "$user" ]; then
echo "✅ ${ACTOR} authorized"
exit 0
fi
done
PERMISSION=$(gh api "repos/${{ github.repository }}/collaborators/${ACTOR}/permission" \
--jq '.permission' 2>/dev/null)
case "$PERMISSION" in
admin|maintain) echo "✅ ${ACTOR} has ${PERMISSION}" ;;
*) echo "❌ Admin or maintain required"; exit 1 ;;
esac
# ── Determine which tasks to run ─────────────────────────────────────
# On schedule: run all tasks with safe defaults (labels NOT reset)
# On dispatch: use input toggles
- name: Set task flags
id: tasks
run: |
if [ "${{ github.event_name }}" = "schedule" ]; then
echo "reset_labels=false" >> $GITHUB_OUTPUT
echo "clean_branches=true" >> $GITHUB_OUTPUT
echo "clean_workflows=true" >> $GITHUB_OUTPUT
echo "clean_logs=true" >> $GITHUB_OUTPUT
echo "fix_templates=true" >> $GITHUB_OUTPUT
echo "rebuild_indexes=true" >> $GITHUB_OUTPUT
echo "delete_closed_issues=false" >> $GITHUB_OUTPUT
else
echo "reset_labels=${{ inputs.reset_labels }}" >> $GITHUB_OUTPUT
echo "clean_branches=${{ inputs.clean_branches }}" >> $GITHUB_OUTPUT
echo "clean_workflows=${{ inputs.clean_workflows }}" >> $GITHUB_OUTPUT
echo "clean_logs=${{ inputs.clean_logs }}" >> $GITHUB_OUTPUT
echo "fix_templates=${{ inputs.fix_templates }}" >> $GITHUB_OUTPUT
echo "rebuild_indexes=${{ inputs.rebuild_indexes }}" >> $GITHUB_OUTPUT
echo "delete_closed_issues=${{ inputs.delete_closed_issues }}" >> $GITHUB_OUTPUT
fi
# ── DELETE RETIRED WORKFLOWS (always runs) ────────────────────────────
- name: Delete retired workflow files
run: |
echo "## 🗑️ Retired Workflow Cleanup" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
RETIRED=(
".github/workflows/build.yml"
".github/workflows/code-quality.yml"
".github/workflows/release-cycle.yml"
".github/workflows/release-pipeline.yml"
".github/workflows/branch-cleanup.yml"
".github/workflows/auto-update-changelog.yml"
".github/workflows/enterprise-issue-manager.yml"
".github/workflows/flush-actions-cache.yml"
".github/workflows/mokostandards-script-runner.yml"
".github/workflows/unified-ci.yml"
".github/workflows/unified-platform-testing.yml"
".github/workflows/reusable-build.yml"
".github/workflows/reusable-ci-validation.yml"
".github/workflows/reusable-deploy.yml"
".github/workflows/reusable-php-quality.yml"
".github/workflows/reusable-platform-testing.yml"
".github/workflows/reusable-project-detector.yml"
".github/workflows/reusable-release.yml"
".github/workflows/reusable-script-executor.yml"
".github/workflows/rebuild-docs-indexes.yml"
".github/workflows/setup-project-v2.yml"
".github/workflows/sync-docs-to-project.yml"
".github/workflows/release.yml"
".github/workflows/sync-changelogs.yml"
".github/workflows/version_branch.yml"
"update.json"
".github/workflows/auto-version-branch.yml"
".github/workflows/publish-to-mokodolibarr.yml"
".github/workflows/ci.yml"
".github/workflows/deploy-rs.yml"
"sftp-config.json"
"sftp-config.json.template"
"scripts/sftp-config"
)
DELETED=0
for wf in "${RETIRED[@]}"; do
if [ -f "$wf" ]; then
git rm "$wf" 2>/dev/null || rm -f "$wf"
echo " Deleted: \`$(basename $wf)\`" >> $GITHUB_STEP_SUMMARY
DELETED=$((DELETED+1))
fi
done
if [ "$DELETED" -gt 0 ]; then
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git add -A
git commit -m "chore: delete ${DELETED} retired workflow file(s) [skip ci]" \
--author="github-actions[bot] <github-actions[bot]@users.noreply.github.com>"
git push
echo "✅ ${DELETED} retired workflow(s) deleted" >> $GITHUB_STEP_SUMMARY
else
echo "✅ No retired workflows found" >> $GITHUB_STEP_SUMMARY
fi
# ── LABEL RESET ──────────────────────────────────────────────────────
- name: Reset labels to standard set
if: steps.tasks.outputs.reset_labels == 'true'
env:
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
run: |
REPO="${{ github.repository }}"
echo "## 🏷️ Label Reset" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
gh api "repos/${REPO}/labels?per_page=100" --paginate --jq '.[].name' | while read -r label; do
ENCODED=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$label', safe=''))")
gh api -X DELETE "repos/${REPO}/labels/${ENCODED}" --silent 2>/dev/null || true
done
while IFS='|' read -r name color description; do
[ -z "$name" ] && continue
gh api "repos/${REPO}/labels" \
-f name="$name" -f color="$color" -f description="$description" \
--silent 2>/dev/null || true
done << 'LABELS'
joomla|7F52FF|Joomla extension or component
dolibarr|FF6B6B|Dolibarr module or extension
generic|808080|Generic project or library
php|4F5D95|PHP code changes
javascript|F7DF1E|JavaScript code changes
typescript|3178C6|TypeScript code changes
python|3776AB|Python code changes
css|1572B6|CSS/styling changes
html|E34F26|HTML template changes
documentation|0075CA|Documentation changes
ci-cd|000000|CI/CD pipeline changes
docker|2496ED|Docker configuration changes
tests|00FF00|Test suite changes
security|FF0000|Security-related changes
dependencies|0366D6|Dependency updates
config|F9D0C4|Configuration file changes
build|FFA500|Build system changes
automation|8B4513|Automated processes or scripts
mokostandards|B60205|MokoStandards compliance
needs-review|FBCA04|Awaiting code review
work-in-progress|D93F0B|Work in progress, not ready for merge
breaking-change|D73A4A|Breaking API or functionality change
priority: critical|B60205|Critical priority, must be addressed immediately
priority: high|D93F0B|High priority
priority: medium|FBCA04|Medium priority
priority: low|0E8A16|Low priority
type: bug|D73A4A|Something isn't working
type: feature|A2EEEF|New feature or request
type: enhancement|84B6EB|Enhancement to existing feature
type: refactor|F9D0C4|Code refactoring
type: chore|FEF2C0|Maintenance tasks
type: version|0E8A16|Version-related change
status: pending|FBCA04|Pending action or decision
status: in-progress|0E8A16|Currently being worked on
status: blocked|B60205|Blocked by another issue or dependency
status: on-hold|D4C5F9|Temporarily on hold
status: wontfix|FFFFFF|This will not be worked on
size/xs|C5DEF5|Extra small change (1-10 lines)
size/s|6FD1E2|Small change (11-30 lines)
size/m|F9DD72|Medium change (31-100 lines)
size/l|FFA07A|Large change (101-300 lines)
size/xl|FF6B6B|Extra large change (301-1000 lines)
size/xxl|B60205|Extremely large change (1000+ lines)
health: excellent|0E8A16|Health score 90-100
health: good|FBCA04|Health score 70-89
health: fair|FFA500|Health score 50-69
health: poor|FF6B6B|Health score below 50
standards-update|B60205|MokoStandards sync update
standards-drift|FBCA04|Repository drifted from MokoStandards
sync-report|0075CA|Bulk sync run report
sync-failure|D73A4A|Bulk sync failure requiring attention
push-failure|D73A4A|File push failure requiring attention
health-check|0E8A16|Repository health check results
version-drift|FFA500|Version mismatch detected
deploy-failure|CC0000|Automated deploy failure tracking
template-validation-failure|D73A4A|Template workflow validation failure
version|0E8A16|Version bump or release
LABELS
echo "✅ Standard labels created" >> $GITHUB_STEP_SUMMARY
# ── BRANCH CLEANUP ───────────────────────────────────────────────────
- name: Delete old sync branches
if: steps.tasks.outputs.clean_branches == 'true'
env:
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
run: |
REPO="${{ github.repository }}"
CURRENT="chore/sync-mokostandards-v04.05"
echo "## 🌿 Branch Cleanup" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
FOUND=false
gh api "repos/${REPO}/branches?per_page=100" --jq '.[].name' | \
grep "^chore/sync-mokostandards" | \
grep -v "^${CURRENT}$" | while read -r branch; do
gh pr list --repo "$REPO" --head "$branch" --state open --json number --jq '.[].number' 2>/dev/null | while read -r pr; do
gh pr close "$pr" --repo "$REPO" --comment "Superseded by \`${CURRENT}\`" 2>/dev/null || true
echo " Closed PR #${pr}" >> $GITHUB_STEP_SUMMARY
done
gh api -X DELETE "repos/${REPO}/git/refs/heads/${branch}" --silent 2>/dev/null || true
echo " Deleted: \`${branch}\`" >> $GITHUB_STEP_SUMMARY
FOUND=true
done
if [ "$FOUND" != "true" ]; then
echo "✅ No old sync branches found" >> $GITHUB_STEP_SUMMARY
fi
# ── WORKFLOW RUN CLEANUP ─────────────────────────────────────────────
- name: Clean up workflow runs
if: steps.tasks.outputs.clean_workflows == 'true'
env:
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
run: |
REPO="${{ github.repository }}"
echo "## 🔄 Workflow Run Cleanup" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
DELETED=0
# Delete cancelled and stale workflow runs
for status in cancelled stale; do
gh api "repos/${REPO}/actions/runs?status=${status}&per_page=100" \
--jq '.workflow_runs[].id' 2>/dev/null | while read -r run_id; do
gh api -X DELETE "repos/${REPO}/actions/runs/${run_id}" --silent 2>/dev/null || true
DELETED=$((DELETED+1))
done
done
echo "✅ Cleaned cancelled/stale workflow runs" >> $GITHUB_STEP_SUMMARY
# ── LOG CLEANUP ──────────────────────────────────────────────────────
- name: Delete old workflow run logs
if: steps.tasks.outputs.clean_logs == 'true'
env:
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
run: |
REPO="${{ github.repository }}"
CUTOFF=$(date -u -d '30 days ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -v-30d +%Y-%m-%dT%H:%M:%SZ)
echo "## 📋 Log Cleanup" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Deleting logs older than: ${CUTOFF}" >> $GITHUB_STEP_SUMMARY
DELETED=0
gh api "repos/${REPO}/actions/runs?created=<${CUTOFF}&per_page=100" \
--jq '.workflow_runs[].id' 2>/dev/null | while read -r run_id; do
gh api -X DELETE "repos/${REPO}/actions/runs/${run_id}/logs" --silent 2>/dev/null || true
DELETED=$((DELETED+1))
done
echo "✅ Cleaned old workflow run logs" >> $GITHUB_STEP_SUMMARY
# ── ISSUE TEMPLATE FIX ──────────────────────────────────────────────
- name: Strip copyright headers from issue templates
if: steps.tasks.outputs.fix_templates == 'true'
run: |
echo "## 📋 Issue Template Cleanup" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
FIXED=0
for f in .github/ISSUE_TEMPLATE/*.md; do
[ -f "$f" ] || continue
if grep -q '^<!--$' "$f"; then
sed -i '/^<!--$/,/^-->$/d' "$f"
echo " Cleaned: \`$(basename $f)\`" >> $GITHUB_STEP_SUMMARY
FIXED=$((FIXED+1))
fi
done
if [ "$FIXED" -gt 0 ]; then
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git add .github/ISSUE_TEMPLATE/
git commit -m "fix: strip copyright comment blocks from issue templates [skip ci]" \
--author="github-actions[bot] <github-actions[bot]@users.noreply.github.com>"
git push
echo "✅ ${FIXED} template(s) cleaned and committed" >> $GITHUB_STEP_SUMMARY
else
echo "✅ No templates need cleaning" >> $GITHUB_STEP_SUMMARY
fi
# ── REBUILD DOC INDEXES ─────────────────────────────────────────────
- name: Rebuild docs/ index files
if: steps.tasks.outputs.rebuild_indexes == 'true'
run: |
echo "## 📚 Documentation Index Rebuild" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ ! -d "docs" ]; then
echo "⏭️ No docs/ directory — skipping" >> $GITHUB_STEP_SUMMARY
exit 0
fi
UPDATED=0
# Generate index.md for each docs/ subdirectory
find docs -type d | while read -r dir; do
INDEX="${dir}/index.md"
FILES=$(find "$dir" -maxdepth 1 -name "*.md" ! -name "index.md" -printf "- [%f](./%f)\n" 2>/dev/null | sort)
if [ -z "$FILES" ]; then
continue
fi
cat > "$INDEX" << INDEXEOF
# $(basename "$dir")
## Documents
${FILES}
---
*Auto-generated by repository-cleanup workflow*
INDEXEOF
# Dedent
sed -i 's/^ //' "$INDEX"
UPDATED=$((UPDATED+1))
done
if [ "$UPDATED" -gt 0 ]; then
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git add docs/
if ! git diff --cached --quiet; then
git commit -m "docs: rebuild documentation indexes [skip ci]" \
--author="github-actions[bot] <github-actions[bot]@users.noreply.github.com>"
git push
echo "✅ ${UPDATED} index file(s) rebuilt and committed" >> $GITHUB_STEP_SUMMARY
else
echo "✅ All indexes already up to date" >> $GITHUB_STEP_SUMMARY
fi
else
echo "✅ No indexes to rebuild" >> $GITHUB_STEP_SUMMARY
fi
# ── VERSION DRIFT DETECTION ──────────────────────────────────────────
- name: Check for version drift
run: |
echo "## 📦 Version Drift Check" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ ! -f "README.md" ]; then
echo "⏭️ No README.md — skipping" >> $GITHUB_STEP_SUMMARY
exit 0
fi
README_VERSION=$(grep -oP '^\s*VERSION:\s*\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' README.md 2>/dev/null | head -1)
if [ -z "$README_VERSION" ]; then
echo "⚠️ No VERSION found in README.md FILE INFORMATION block" >> $GITHUB_STEP_SUMMARY
exit 0
fi
echo "**README version:** \`${README_VERSION}\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
DRIFT=0
CHECKED=0
# Check all files with FILE INFORMATION blocks
while IFS= read -r -d '' file; do
FILE_VERSION=$(grep -oP '^\s*\*?\s*VERSION:\s*\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' "$file" 2>/dev/null | head -1)
[ -z "$FILE_VERSION" ] && continue
CHECKED=$((CHECKED+1))
if [ "$FILE_VERSION" != "$README_VERSION" ]; then
echo " ⚠️ \`${file}\`: \`${FILE_VERSION}\` (expected \`${README_VERSION}\`)" >> $GITHUB_STEP_SUMMARY
DRIFT=$((DRIFT+1))
fi
done < <(find . -maxdepth 4 -type f \( -name "*.php" -o -name "*.md" -o -name "*.yml" \) ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" -print0 2>/dev/null)
echo "" >> $GITHUB_STEP_SUMMARY
if [ "$DRIFT" -gt 0 ]; then
echo "⚠️ **${DRIFT}** file(s) out of ${CHECKED} have version drift" >> $GITHUB_STEP_SUMMARY
echo "Run \`sync-version-on-merge\` workflow or update manually" >> $GITHUB_STEP_SUMMARY
else
echo "✅ All ${CHECKED} file(s) match README version \`${README_VERSION}\`" >> $GITHUB_STEP_SUMMARY
fi
# ── PROTECT CUSTOM WORKFLOWS ────────────────────────────────────────
- name: Ensure custom workflow directory exists
run: |
echo "## 🔧 Custom Workflows" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ ! -d ".github/workflows/custom" ]; then
mkdir -p .github/workflows/custom
cat > .github/workflows/custom/README.md << 'CWEOF'
# Custom Workflows
Place repo-specific workflows here. Files in this directory are:
- **Never overwritten** by MokoStandards bulk sync
- **Never deleted** by the repository-cleanup workflow
- Safe for custom CI, notifications, or repo-specific automation
Synced workflows live in `.github/workflows/` (parent directory).
CWEOF
sed -i 's/^ //' .github/workflows/custom/README.md
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git add .github/workflows/custom/
if ! git diff --cached --quiet; then
git commit -m "chore: create .github/workflows/custom/ for repo-specific workflows [skip ci]" \
--author="github-actions[bot] <github-actions[bot]@users.noreply.github.com>"
git push
echo "✅ Created \`.github/workflows/custom/\` directory" >> $GITHUB_STEP_SUMMARY
fi
else
CUSTOM_COUNT=$(find .github/workflows/custom -name "*.yml" -o -name "*.yaml" 2>/dev/null | wc -l)
echo "✅ Custom workflow directory exists (${CUSTOM_COUNT} workflow(s))" >> $GITHUB_STEP_SUMMARY
fi
# ── DELETE CLOSED ISSUES ──────────────────────────────────────────────
- name: Delete old closed issues
if: steps.tasks.outputs.delete_closed_issues == 'true'
env:
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
run: |
REPO="${{ github.repository }}"
CUTOFF=$(date -u -d '30 days ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -v-30d +%Y-%m-%dT%H:%M:%SZ)
echo "## 🗑️ Closed Issue Cleanup" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Deleting issues closed before: ${CUTOFF}" >> $GITHUB_STEP_SUMMARY
DELETED=0
gh api "repos/${REPO}/issues?state=closed&since=1970-01-01T00:00:00Z&per_page=100&sort=updated&direction=asc" \
--jq ".[] | select(.closed_at < \"${CUTOFF}\") | .number" 2>/dev/null | while read -r num; do
# Lock and close with "not_planned" to mark as cleaned up
gh api "repos/${REPO}/issues/${num}/lock" -X PUT -f lock_reason="resolved" --silent 2>/dev/null || true
echo " Locked issue #${num}" >> $GITHUB_STEP_SUMMARY
DELETED=$((DELETED+1))
done
if [ "$DELETED" -eq 0 ] 2>/dev/null; then
echo "✅ No old closed issues found" >> $GITHUB_STEP_SUMMARY
else
echo "✅ Locked ${DELETED} old closed issue(s)" >> $GITHUB_STEP_SUMMARY
fi
- name: Summary
if: always()
run: |
echo "" >> $GITHUB_STEP_SUMMARY
echo "---" >> $GITHUB_STEP_SUMMARY
echo "*Run by @${{ github.actor }} — trigger: ${{ github.event_name }}*" >> $GITHUB_STEP_SUMMARY
File diff suppressed because it is too large Load Diff
-135
View File
@@ -1,135 +0,0 @@
# 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: GitHub.Workflow
# INGROUP: MokoStandards.Automation
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /templates/workflows/shared/sync-version-on-merge.yml.template
# VERSION: 04.06.00
# BRIEF: Auto-bump patch version on every push to main and propagate to all file headers
# NOTE: Synced via bulk-repo-sync to .github/workflows/sync-version-on-merge.yml in all governed repos.
# README.md is the single source of truth for the repository version.
name: Sync Version from README
on:
pull_request:
types: [closed]
branches:
- main
workflow_dispatch:
inputs:
dry_run:
description: 'Dry run (preview only, no commit)'
type: boolean
default: false
permissions:
contents: write
issues: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
sync-version:
name: Propagate README version
runs-on: ubuntu-latest
if: >-
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch'
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.GH_TOKEN || github.token }}
fetch-depth: 0
- name: Set up PHP
uses: shivammathur/setup-php@fcafdd6392932010c2bd5094439b8e33be2a8a09 # v2.37.0
with:
php-version: '8.1'
tools: composer
- name: Setup MokoStandards tools
env:
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}'
run: |
git clone --depth 1 --branch version/04 --quiet \
"https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \
/tmp/mokostandards
cd /tmp/mokostandards
composer install --no-dev --no-interaction --quiet
- name: Auto-bump patch version
if: ${{ github.event_name != 'workflow_dispatch' && github.actor != 'github-actions[bot]' }}
run: |
if git diff --name-only HEAD~1 HEAD 2>/dev/null | grep -q '^README\.md$'; then
echo "README.md changed in this push — skipping auto-bump"
exit 0
fi
RESULT=$(php /tmp/mokostandards/api/cli/version_bump.php --path .) || {
echo "⚠️ Could not bump version — skipping"
exit 0
}
echo "Auto-bumping patch: $RESULT"
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git add README.md
git commit -m "chore(version): auto-bump patch ${RESULT} [skip ci]" \
--author="github-actions[bot] <github-actions[bot]@users.noreply.github.com>"
git push
- name: Extract version from README.md
id: readme_version
run: |
git pull --ff-only 2>/dev/null || true
VERSION=$(php /tmp/mokostandards/api/cli/version_read.php --path . 2>/dev/null)
if [ -z "$VERSION" ]; then
echo "⚠️ No VERSION in README.md — skipping propagation"
echo "skip=true" >> $GITHUB_OUTPUT
exit 0
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "skip=false" >> $GITHUB_OUTPUT
echo "✅ README.md version: $VERSION"
- name: Run version sync
if: ${{ steps.readme_version.outputs.skip != 'true' && inputs.dry_run != true }}
run: |
php /tmp/mokostandards/api/maintenance/update_version_from_readme.php \
--path . \
--create-issue \
--repo "${{ github.repository }}"
env:
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
- name: Commit updated files
if: ${{ steps.readme_version.outputs.skip != 'true' && inputs.dry_run != true }}
run: |
git pull --ff-only 2>/dev/null || true
if git diff --quiet; then
echo "️ No version changes needed — already up to date"
exit 0
fi
VERSION="${{ steps.readme_version.outputs.version }}"
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git add -A
git commit -m "chore(version): sync badges and headers to ${VERSION} [skip ci]" \
--author="github-actions[bot] <github-actions[bot]@users.noreply.github.com>"
git push
- name: Summary
run: |
VERSION="${{ steps.readme_version.outputs.version }}"
echo "## 📦 Version Sync — ${VERSION}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Source:** \`README.md\` FILE INFORMATION block" >> $GITHUB_STEP_SUMMARY
echo "**Version:** \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
-48
View File
@@ -1,48 +0,0 @@
name: Update MokoCassiopeia Payload
on:
push:
branches: [main]
workflow_dispatch:
jobs:
update-payload:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: Get latest MokoCassiopeia release URL
id: moko
run: |
DOWNLOAD_URL=$(curl -s https://api.github.com/repos/mokoconsulting-tech/MokoCassiopeia/releases \
| jq -r '[.[] | select(.prerelease == false and .draft == false and (.assets | length > 0)][0].assets[0].browser_download_url')
echo "url=$DOWNLOAD_URL" >> $GITHUB_OUTPUT
echo "Found: $DOWNLOAD_URL"
- name: Download MokoCassiopeia zip
if: steps.moko.outputs.url != 'null'
run: |
mkdir -p src/payload
curl -sL "${{ steps.moko.outputs.url }}" -o src/payload/mokocassiopeia.zip
ls -la src/payload/
- name: Check if payload changed
id: diff
run: |
git add src/payload/mokocassiopeia.zip
if git diff --cached --quiet; then
echo "changed=false" >> $GITHUB_OUTPUT
else
echo "changed=true" >> $GITHUB_OUTPUT
fi
- name: Commit updated payload
if: steps.diff.outputs.changed == 'true'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git commit -m "chore: update mokocassiopeia payload"
git push
-346
View File
@@ -1,346 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: GitHub.Workflow
# INGROUP: MokoStandards.Joomla
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /templates/workflows/joomla/update-server.yml.template
# VERSION: 04.06.00
# BRIEF: Update Joomla update server XML feed with stable/rc/dev entries
#
# Writes updates.xml with multiple <update> entries:
# - <tag>stable</tag> on push to main (from auto-release)
# - <tag>rc</tag> on push to rc/**
# - <tag>development</tag> on push to dev/**
#
# Joomla filters by user's "Minimum Stability" setting.
name: Update Joomla Update Server XML Feed
on:
pull_request:
types: [closed]
branches:
- 'dev/**'
- 'alpha/**'
- 'beta/**'
- 'rc/**'
paths:
- 'src/**'
- 'htdocs/**'
workflow_dispatch:
inputs:
stability:
description: 'Stability tag'
required: true
default: 'development'
type: choice
options:
- development
- alpha
- beta
- rc
- stable
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
permissions:
contents: write
jobs:
update-xml:
name: Update updates.xml
runs-on: ubuntu-latest
if: >-
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch'
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.GH_TOKEN || github.token }}
fetch-depth: 0
- name: Setup MokoStandards tools
env:
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}'
run: |
git clone --depth 1 --branch version/04 --quiet \
"https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \
/tmp/mokostandards 2>/dev/null || true
if [ -d "/tmp/mokostandards" ] && [ -f "/tmp/mokostandards/composer.json" ]; then
cd /tmp/mokostandards && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
fi
- name: Generate updates.xml entry
run: |
BRANCH="${{ github.ref_name }}"
REPO="${{ github.repository }}"
VERSION=$(php /tmp/mokostandards/api/cli/version_read.php --path . 2>/dev/null || echo "0.0.0")
# Auto-bump patch on alpha/beta/rc branches (not dev — dev bumps manually)
if [[ "$BRANCH" != dev/* ]]; then
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
BUMPED=$(php /tmp/mokostandards/api/cli/version_bump.php --path . 2>/dev/null || true)
if [ -n "$BUMPED" ]; then
VERSION=$(php /tmp/mokostandards/api/cli/version_read.php --path . 2>/dev/null || echo "$VERSION")
git add -A
git commit -m "chore(version): auto-bump patch ${VERSION} [skip ci]" \
--author="github-actions[bot] <github-actions[bot]@users.noreply.github.com>" 2>/dev/null || true
git push 2>/dev/null || true
fi
fi
# Determine stability from branch or input
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
STABILITY="${{ inputs.stability }}"
elif [[ "$BRANCH" == rc/* ]]; then
STABILITY="rc"
elif [[ "$BRANCH" == beta/* ]]; then
STABILITY="beta"
elif [[ "$BRANCH" == alpha/* ]]; then
STABILITY="alpha"
elif [[ "$BRANCH" == dev/* ]]; then
STABILITY="development"
else
STABILITY="stable"
fi
# Parse manifest (portable — no grep -P)
MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
if [ -z "$MANIFEST" ]; then
echo "No Joomla manifest found — skipping"
exit 0
fi
# Extract fields using sed (works on all runners)
EXT_NAME=$(sed -n 's/.*<name>\([^<]*\)<\/name>.*/\1/p' "$MANIFEST" | head -1)
EXT_TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
EXT_ELEMENT=$(sed -n 's/.*<element>\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" | head -1)
EXT_CLIENT=$(sed -n 's/.*<extension[^>]*client="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
EXT_FOLDER=$(sed -n 's/.*<extension[^>]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
EXT_VERSION=$(sed -n 's/.*<version>\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1)
TARGET_PLATFORM=$(sed -n 's/.*\(<targetplatform[^/]*\/>\).*/\1/p' "$MANIFEST" | head -1)
PHP_MINIMUM=$(sed -n 's/.*<php_minimum>\([^<]*\)<\/php_minimum>.*/\1/p' "$MANIFEST" | head -1)
# Fallbacks
[ -z "$EXT_NAME" ] && EXT_NAME="${{ github.event.repository.name }}"
[ -z "$EXT_TYPE" ] && EXT_TYPE="component"
# Templates and modules don't have <element> — derive from <name>
if [ -z "$EXT_ELEMENT" ]; then
EXT_ELEMENT=$(echo "$EXT_NAME" | tr '[:upper:]' '[:lower:]' | tr -d ' ')
fi
# Use manifest version if README version is empty
[ "$VERSION" = "0.0.0" ] && [ -n "$EXT_VERSION" ] && VERSION="$EXT_VERSION"
[ -z "$TARGET_PLATFORM" ] && TARGET_PLATFORM=$(printf '<targetplatform name="joomla" version="5.*" %s>' "/")
CLIENT_TAG=""
[ -n "$EXT_CLIENT" ] && CLIENT_TAG="<client>${EXT_CLIENT}</client>"
[ -z "$CLIENT_TAG" ] && ([ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]) && CLIENT_TAG="<client>site</client>"
FOLDER_TAG=""
[ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ] && FOLDER_TAG="<folder>${EXT_FOLDER}</folder>"
PHP_TAG=""
[ -n "$PHP_MINIMUM" ] && PHP_TAG="<php_minimum>${PHP_MINIMUM}</php_minimum>"
# Version suffix for non-stable
DISPLAY_VERSION="$VERSION"
case "$STABILITY" in
development) DISPLAY_VERSION="${VERSION}-dev" ;;
alpha) DISPLAY_VERSION="${VERSION}-alpha" ;;
beta) DISPLAY_VERSION="${VERSION}-beta" ;;
rc) DISPLAY_VERSION="${VERSION}-rc" ;;
esac
MAJOR=$(echo "$VERSION" | awk -F. '{print $1}')
# Each stability level has its own release tag
case "$STABILITY" in
development) RELEASE_TAG="development" ;;
alpha) RELEASE_TAG="alpha" ;;
beta) RELEASE_TAG="beta" ;;
rc) RELEASE_TAG="release-candidate" ;;
*) RELEASE_TAG="v${MAJOR}" ;;
esac
PACKAGE_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.zip"
DOWNLOAD_URL="https://github.com/${REPO}/releases/download/${RELEASE_TAG}/${PACKAGE_NAME}"
INFO_URL="https://github.com/${REPO}"
# ── Build install packages (ZIP + tar.gz) ───────────────────
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
if [ -d "$SOURCE_DIR" ]; then
EXCLUDES=".ftpignore sftp-config* *.ppk *.pem *.key .env*"
TAR_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.tar.gz"
cd "$SOURCE_DIR"
zip -r "/tmp/${PACKAGE_NAME}" . -x $EXCLUDES
cd ..
tar -czf "/tmp/${TAR_NAME}" -C "$SOURCE_DIR" \
--exclude='.ftpignore' --exclude='sftp-config*' \
--exclude='*.ppk' --exclude='*.pem' --exclude='*.key' --exclude='.env*' .
SHA256=$(sha256sum "/tmp/${PACKAGE_NAME}" | cut -d' ' -f1)
# Ensure release exists
gh release view "$RELEASE_TAG" --json tagName > /dev/null 2>&1 || \
gh release create "$RELEASE_TAG" --title "${RELEASE_TAG} (${DISPLAY_VERSION})" --notes "${STABILITY} release" --prerelease --target main 2>/dev/null || true
# Upload both formats
gh release upload "$RELEASE_TAG" "/tmp/${PACKAGE_NAME}" --clobber 2>/dev/null || true
gh release upload "$RELEASE_TAG" "/tmp/${TAR_NAME}" --clobber 2>/dev/null || true
echo "Packages: ${PACKAGE_NAME} + ${TAR_NAME} (SHA: ${SHA256})" >> $GITHUB_STEP_SUMMARY
else
SHA256=""
fi
# ── Build the new entry ───────────────────────────────────────
NEW_ENTRY=""
NEW_ENTRY="${NEW_ENTRY} <update>\n"
NEW_ENTRY="${NEW_ENTRY} <name>${EXT_NAME}</name>\n"
NEW_ENTRY="${NEW_ENTRY} <description>${EXT_NAME} (${STABILITY})</description>\n"
NEW_ENTRY="${NEW_ENTRY} <element>${EXT_ELEMENT}</element>\n"
NEW_ENTRY="${NEW_ENTRY} <type>${EXT_TYPE}</type>\n"
NEW_ENTRY="${NEW_ENTRY} <version>${DISPLAY_VERSION}</version>\n"
[ -n "$CLIENT_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${CLIENT_TAG}\n"
[ -n "$FOLDER_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${FOLDER_TAG}\n"
NEW_ENTRY="${NEW_ENTRY} <tags>\n"
NEW_ENTRY="${NEW_ENTRY} <tag>${STABILITY}</tag>\n"
NEW_ENTRY="${NEW_ENTRY} </tags>\n"
NEW_ENTRY="${NEW_ENTRY} <infourl title=\"${EXT_NAME}\">${INFO_URL}</infourl>\n"
NEW_ENTRY="${NEW_ENTRY} <downloads>\n"
TAR_URL="https://github.com/${REPO}/releases/download/${RELEASE_TAG}/${EXT_ELEMENT}-${DISPLAY_VERSION}.tar.gz"
NEW_ENTRY="${NEW_ENTRY} <downloadurl type=\"full\" format=\"zip\">${DOWNLOAD_URL}</downloadurl>\n"
NEW_ENTRY="${NEW_ENTRY} <downloadurl type=\"full\" format=\"tar.gz\">${TAR_URL}</downloadurl>\n"
NEW_ENTRY="${NEW_ENTRY} </downloads>\n"
[ -n "$SHA256" ] && NEW_ENTRY="${NEW_ENTRY} <sha256>sha256:${SHA256}</sha256>\n"
NEW_ENTRY="${NEW_ENTRY} ${TARGET_PLATFORM}\n"
[ -n "$PHP_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${PHP_TAG}\n"
NEW_ENTRY="${NEW_ENTRY} <maintainer>Moko Consulting</maintainer>\n"
NEW_ENTRY="${NEW_ENTRY} <maintainerurl>https://mokoconsulting.tech</maintainerurl>\n"
NEW_ENTRY="${NEW_ENTRY} </update>"
# ── Write new entry to temp file ───────────────────────────────
printf '%b' "$NEW_ENTRY" > /tmp/new_entry.xml
# ── Merge into updates.xml ─────────────────────────────────────
if [ ! -f "updates.xml" ]; then
printf '%s\n' '<?xml version="1.0" encoding="utf-8"?>' > updates.xml
printf '%s\n' '<updates>' >> updates.xml
cat /tmp/new_entry.xml >> updates.xml
printf '\n%s\n' '</updates>' >> updates.xml
else
# Remove existing entry for this stability, insert new one
printf 'import re\nstability = "%s"\n' "${STABILITY}" > /tmp/merge_xml.py
printf 'with open("updates.xml") as f: content = f.read()\n' >> /tmp/merge_xml.py
printf 'with open("/tmp/new_entry.xml") as f: new_entry = f.read()\n' >> /tmp/merge_xml.py
printf 'pattern = r" <update>.*?<tag>" + re.escape(stability) + r"</tag>.*?</update>\\n?"\n' >> /tmp/merge_xml.py
printf 'content = re.sub(pattern, "", content, flags=re.DOTALL)\n' >> /tmp/merge_xml.py
printf 'content = content.replace("</updates>", new_entry + "\\n</updates>")\n' >> /tmp/merge_xml.py
printf 'content = re.sub(r"\\n{3,}", "\\n\\n", content)\n' >> /tmp/merge_xml.py
printf 'with open("updates.xml", "w") as f: f.write(content)\n' >> /tmp/merge_xml.py
python3 /tmp/merge_xml.py 2>/dev/null || {
# Fallback: rebuild keeping other stability entries
{
printf '%s\n' '<?xml version="1.0" encoding="utf-8"?>'
printf '%s\n' '<updates>'
for TAG in stable rc development; do
[ "$TAG" = "${STABILITY}" ] && continue
if grep -q "<tag>${TAG}</tag>" updates.xml 2>/dev/null; then
sed -n "/<update>/,/<\/update>/{ /<tag>${TAG}<\/tag>/p; }" updates.xml
fi
done
cat /tmp/new_entry.xml
printf '\n%s\n' '</updates>'
} > /tmp/updates_new.xml
mv /tmp/updates_new.xml updates.xml
}
fi
# Commit
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git add updates.xml
git diff --cached --quiet || {
git commit -m "chore: update updates.xml (${STABILITY}: ${DISPLAY_VERSION}) [skip ci]" \
--author="github-actions[bot] <github-actions[bot]@users.noreply.github.com>"
git push
}
- name: SFTP deploy to dev server
if: contains(github.ref, 'dev/')
env:
DEV_HOST: ${{ vars.DEV_FTP_HOST }}
DEV_PATH: ${{ vars.DEV_FTP_PATH }}
DEV_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
DEV_USER: ${{ vars.DEV_FTP_USERNAME }}
DEV_PORT: ${{ vars.DEV_FTP_PORT }}
DEV_KEY: ${{ secrets.DEV_FTP_KEY }}
DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
run: |
# ── Permission check: admin or maintain role required ──────
ACTOR="${{ github.actor }}"
REPO="${{ github.repository }}"
PERMISSION=$(gh api "repos/${REPO}/collaborators/${ACTOR}/permission" \
--jq '.permission' 2>/dev/null || \
gh api "repos/${REPO}/collaborators/${ACTOR}" \
--jq '.role' 2>/dev/null || echo "read")
case "$PERMISSION" in
admin|maintain|write) ;;
*)
echo "Deploy denied: ${ACTOR} has '${PERMISSION}' — requires admin, maintain, or write"
exit 0
;;
esac
[ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured — skipping SFTP"; exit 0; }
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
[ ! -d "$SOURCE_DIR" ] && exit 0
PORT="${DEV_PORT:-22}"
REMOTE="${DEV_PATH%/}"
[ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}"
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
"$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json
if [ -n "$DEV_KEY" ]; then
echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
else
printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json
fi
PLATFORM=$(php /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 --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
elif [ -f "/tmp/mokostandards/api/deploy/deploy-sftp.php" ]; then
php /tmp/mokostandards/api/deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
fi
rm -f /tmp/deploy_key /tmp/sftp-config.json
echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY
- name: Summary
if: always()
run: |
echo "## Joomla Update Server" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Version | \`${DISPLAY_VERSION}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Element | \`${EXT_ELEMENT}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Download | [ZIP](${DOWNLOAD_URL}) |" >> $GITHUB_STEP_SUMMARY
+5
View File
@@ -9,6 +9,8 @@ TODO.md
.env .env
.env.local .env.local
.env.*.local .env.*.local
.joomla-api-mcp.json
.mcp.json
*.local.php *.local.php
*.secret.php *.secret.php
configuration.php configuration.php
@@ -100,6 +102,7 @@ replit.md
*.tar.gz *.tar.gz
*.tgz *.tgz
*.zip *.zip
!src/payload/*.zip
artifacts/ artifacts/
release/ release/
releases/ releases/
@@ -200,3 +203,5 @@ venv/
*.coverage *.coverage
hypothesis/ hypothesis/
profile.ps1
TODO.md
+18
View File
@@ -0,0 +1,18 @@
---
blank_issues_enabled: true
contact_links:
- name: 💼 Enterprise Support
url: https://mokoconsulting.tech/enterprise
about: Enterprise-level support and consultation services
- name: 💬 Ask a Question
url: https://mokoconsulting.tech/
about: Get help or ask questions through our website
- name: 📚 MokoStandards Documentation
url: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
about: View our coding standards and best practices
- name: 🔒 Report a Security Vulnerability
url: https://git.mokoconsulting.tech/mokoconsulting-tech/.github-private/security/advisories/new
about: Report security vulnerabilities privately (for critical issues)
- name: 💡 Community Discussions
url: https://github.com/orgs/mokoconsulting-tech/discussions
about: Join community discussions and Q&A
@@ -0,0 +1,51 @@
---
name: Feature Request
about: Suggest a new feature or enhancement
title: '[FEATURE] '
labels: 'enhancement'
assignees: ''
---
## Feature Description
A clear and concise description of the feature you'd like to see.
## Problem or Use Case
Describe the problem this feature would solve or the use case it addresses.
Ex. I'm always frustrated when [...]
## Proposed Solution
A clear and concise description of what you want to happen.
## Alternative Solutions
A clear and concise description of any alternative solutions or features you've considered.
## Benefits
Describe how this feature would benefit users:
- Who would use this feature?
- What problems does it solve?
- What value does it add?
## Implementation Details (Optional)
If you have ideas about how this could be implemented, share them here:
- Technical approach
- Files/components that might need changes
- Any concerns or challenges you foresee
## Additional Context
Add any other context, mockups, or screenshots about the feature request here.
## Relevant Standards
Does this relate to any standards in [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards)?
- [ ] Accessibility (WCAG 2.1 AA)
- [ ] Localization (en_US/en_GB)
- [ ] Security best practices
- [ ] Code quality standards
- [ ] Other: [specify]
## Checklist
- [ ] I have searched for similar feature requests before creating this one
- [ ] I have clearly described the use case and benefits
- [ ] I have considered alternative solutions
- [ ] This feature aligns with the project's goals and scope
+82
View File
@@ -0,0 +1,82 @@
---
name: Question
about: Ask a question about usage, features, or best practices
title: '[QUESTION] '
labels: ['question']
assignees: ['jmiller']
---
## Question
**Your question:**
## Context
**What are you trying to accomplish?**
**What have you already tried?**
**Category**:
- [ ] Script usage
- [ ] Configuration
- [ ] Workflow setup
- [ ] Documentation interpretation
- [ ] Best practices
- [ ] Integration
- [ ] Other: __________
## Environment (if relevant)
**Your setup**:
- Operating System:
- Version:
## What You've Researched
**Documentation reviewed**:
- [ ] README.md
- [ ] Project documentation
- [ ] Other (specify): __________
**Similar issues/questions found**:
- #
- #
## Expected Outcome
**What result are you hoping for?**
## Code/Configuration Samples
**Relevant code or configuration** (if applicable):
```bash
# Your code here
```
## Additional Context
**Any other relevant information:**
**Screenshots** (if helpful):
## Urgency
- [ ] Urgent (blocking work)
- [ ] Normal (can work on other things meanwhile)
- [ ] Low priority (just curious)
## Checklist
- [ ] I have searched existing issues and discussions
- [ ] I have reviewed relevant documentation
- [ ] I have provided sufficient context
- [ ] I have included code/configuration samples if relevant
- [ ] This is a genuine question (not a bug report or feature request)
+51
View File
@@ -0,0 +1,51 @@
---
name: Security Vulnerability Report
about: Report a security vulnerability (use only for non-critical issues)
title: '[SECURITY] '
labels: 'security'
assignees: ''
---
## ⚠️ IMPORTANT: Private Disclosure Required
**For critical security vulnerabilities, DO NOT use this template.**
Follow the process in [SECURITY.md](../SECURITY.md) for responsible disclosure.
Use this template only for:
- Security improvements
- Non-critical security suggestions
- Security documentation updates
---
## Security Issue
**Severity**:
<!-- Low, Medium, or informational only -->
## Description
<!-- Describe the security concern or improvement suggestion -->
## Affected Components
<!-- List the affected files, features, or components -->
## Suggested Mitigation
<!-- Describe how this could be addressed -->
## Standards Reference
Does this relate to security standards in [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards)?
- [ ] SPDX license identifiers
- [ ] Secret management
- [ ] Dependency security
- [ ] Access control
- [ ] Other: [specify]
## Additional Context
<!-- Add any other context about the security concern -->
## Checklist
- [ ] This is NOT a critical vulnerability requiring private disclosure
- [ ] I have reviewed the SECURITY.md policy
- [ ] I have provided sufficient detail for evaluation
+24
View File
@@ -0,0 +1,24 @@
---
name: Version Bump
about: Request or track a version change
title: '[VERSION] '
labels: 'version, type: version'
assignees: 'jmiller'
---
## Version Change
**Current version**: <!-- e.g., 01.02.03 -->
**Requested version**: <!-- e.g., 01.03.00 -->
**Change type**: <!-- patch / minor / major -->
## Reason
<!-- Why is this version bump needed? -->
## Checklist
- [ ] README.md `VERSION:` field updated
- [ ] CHANGELOG.md entry added
- [ ] Module descriptor version updated (Dolibarr: `$this->version`, Joomla: `<version>`)
- [ ] All file headers will be auto-propagated by `sync-version-on-merge` workflow
@@ -0,0 +1,77 @@
---
name: WaaS Client Site Issue
about: Report an issue with a WaaS client site (branding, deployment, media sync)
title: '[WAAS] '
labels: 'waas, client-site'
assignees: ''
---
## Site Issue Type
- [ ] Branding / CSS not applying
- [ ] Deployment failure
- [ ] Media sync issue
- [ ] Template override not working
- [ ] Module positioning issue
- [ ] Mobile / responsive layout
- [ ] Performance issue
## Client Site
- **Client Org**: [e.g., ClarksvilleFurs]
- **Repo**: [e.g., client-waas-clarksvillefurs]
- **Environment**: [Dev / Production]
- **Site URL**: [dev or production URL — omit if private]
## Issue Description
Describe the issue clearly.
## Steps to Reproduce
1. Visit [page URL]
2. Look at [element]
3. See error
## Expected Behavior
What the site should look like or how it should behave.
## Actual Behavior
What is happening instead.
## Screenshots
Attach screenshots showing the issue (desktop and mobile if relevant).
## Deployment Status
- **Last deploy**: [date or "unknown"]
- **Deploy workflow**: [succeeded / failed / not run]
- **Branch**: [dev / main]
## Media Sync
- [ ] Images missing after sync
- [ ] Sync direction: [dev-to-prod / prod-to-dev / bidirectional]
- [ ] Last sync: [date]
## Template Details
- **Joomla Version**: [e.g., 5.x]
- **Template Name**: [e.g., clienttemplate]
- **MokoWaaS Plugin**: [Active / Inactive]
- **MokoOnyx Admin**: [Active / Inactive]
## CSS Custom Properties
If branding issue, list the relevant CSS variables:
```css
:root {
--client-primary: #...;
--client-secondary: #...;
}
```
## Browser / Device
- **Browser**: [e.g., Chrome 120, Safari 17]
- **Device**: [Desktop / Tablet / Mobile]
- **Screen Width**: [e.g., 1920px, 768px, 375px]
## Checklist
- [ ] I have cleared Joomla cache
- [ ] I have hard-refreshed the browser (Ctrl+Shift+R)
- [ ] I have checked the deploy workflow completed
- [ ] I have verified the change is on the correct branch
- [ ] No credentials or PII are included in this issue
+110
View File
@@ -0,0 +1,110 @@
---
name: Architecture Decision Record (ADR)
about: Propose or document an architectural decision
title: '[ADR] '
labels: 'architecture, decision'
assignees: ''
---
## ADR Number
ADR-XXXX
## Status
- [ ] Proposed
- [ ] Accepted
- [ ] Deprecated
- [ ] Superseded by ADR-XXXX
## Context
Describe the issue or problem that motivates this decision.
## Decision
State the architecture decision and provide rationale.
## Consequences
### Positive
- List positive consequences
### Negative
- List negative consequences or trade-offs
### Neutral
- List neutral aspects
## Alternatives Considered
### Alternative 1
- Description
- Pros
- Cons
- Why not chosen
### Alternative 2
- Description
- Pros
- Cons
- Why not chosen
## Implementation Plan
1. Step 1
2. Step 2
3. Step 3
## Stakeholders
- **Decision Makers**: @user1, @user2
- **Consulted**: @user3, @user4
- **Informed**: team-name
## Technical Details
### Architecture Diagram
```
[Add diagram or link]
```
### Dependencies
- Dependency 1
- Dependency 2
### Impact Analysis
- **Performance**: [Impact description]
- **Security**: [Impact description]
- **Scalability**: [Impact description]
- **Maintainability**: [Impact description]
## Testing Strategy
- [ ] Unit tests
- [ ] Integration tests
- [ ] Performance tests
- [ ] Security tests
## Documentation
- [ ] Architecture documentation updated
- [ ] API documentation updated
- [ ] Developer guide updated
- [ ] Runbook created
## Migration Path
Describe how to migrate from current state to new architecture.
## Rollback Plan
Describe how to rollback if issues occur.
## Timeline
- **Proposal Date**:
- **Decision Date**:
- **Implementation Start**:
- **Expected Completion**:
## References
- Related ADRs:
- External resources:
- RFCs:
## Review Checklist
- [ ] Aligns with enterprise architecture principles
- [ ] Security implications reviewed
- [ ] Performance implications reviewed
- [ ] Cost implications reviewed
- [ ] Compliance requirements met
- [ ] Team consensus achieved
+949
View File
@@ -0,0 +1,949 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Release
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
# PATH: /templates/workflows/joomla/auto-release.yml.template
# VERSION: 04.06.00
# BRIEF: Joomla build & release — ZIP package, updates.xml, SHA-256 checksum
#
# +========================================================================+
# | BUILD & RELEASE PIPELINE (JOOMLA) |
# +========================================================================+
# | |
# | Triggers on push to main (skips bot commits + [skip ci]): |
# | |
# | Every push: |
# | 1. Read version from README.md |
# | 3. Set platform version (Joomla <version>) |
# | 4. Update [VERSION: XX.YY.ZZ] badges in markdown files |
# | 5. Write updates.xml (Joomla update server XML) |
# | 6. Create git tag vXX.YY.ZZ |
# | 7a. Patch: update existing Gitea Release for this minor |
# | 8. Build ZIP, upload asset, write SHA-256 to updates.xml |
# | |
# | Every version change: archives main -> version/XX.YY branch |
# | All patches release (including 00). Patch 00/01 = full pipeline. |
# | First release only (patch == 01): |
# | 7b. Create new Gitea Release |
# | |
# | GitHub mirror: stable/rc releases only (continue-on-error) |
# | |
# +========================================================================+
name: 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 MokoStandards 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}/MokoStandards-API.git" \
/tmp/mokostandards-api
cd /tmp/mokostandards-api
composer install --no-dev --no-interaction --quiet
# -- STEP 1: Read version -----------------------------------------------
- name: "Step 1: Read version from README.md"
id: version
run: |
VERSION=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null)
if [ -z "$VERSION" ]; then
echo "No VERSION in README.md — skipping release"
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
# Derive major.minor for branch naming (patches update existing branch)
MINOR=$(echo "$VERSION" | awk -F. '{printf "%s.%s", $1, $2}')
PATCH=$(echo "$VERSION" | awk -F. '{print $3}')
MAJOR=$(echo "$VERSION" | awk -F. '{print $1}')
MINOR_NUM=$(echo "$VERSION" | awk -F. '{print $2}')
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "branch=version/${MAJOR}" >> "$GITHUB_OUTPUT"
echo "minor=$MINOR" >> "$GITHUB_OUTPUT"
echo "major=$MAJOR" >> "$GITHUB_OUTPUT"
echo "release_tag=stable" >> "$GITHUB_OUTPUT"
echo "stability=stable" >> "$GITHUB_OUTPUT"
echo "skip=false" >> "$GITHUB_OUTPUT"
if [ "$PATCH" = "00" ] || [ "$PATCH" = "01" ]; then
echo "is_minor=true" >> "$GITHUB_OUTPUT"
echo "Version: $VERSION (first release for this minor — full pipeline)"
else
echo "is_minor=false" >> "$GITHUB_OUTPUT"
echo "Version: $VERSION (patch — platform version + badges only)"
fi
# -- STEP 1b: Bump minor version (stable = minor bump, reset patch) ------
- name: "Step 1b: Bump minor version for stable release"
if: steps.version.outputs.skip != 'true'
id: bump
run: |
CURRENT=$(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)
[ -z "$CURRENT" ] && { echo "skip=true" >> "$GITHUB_OUTPUT"; exit 0; }
MAJOR=$((10#$(echo "$CURRENT" | cut -d. -f1)))
MINOR=$((10#$(echo "$CURRENT" | cut -d. -f2)))
# Minor bump, reset patch. Rollover if minor > 99
MINOR=$((MINOR + 1))
if [ $MINOR -gt 99 ]; then
MINOR=0
MAJOR=$((MAJOR + 1))
fi
VERSION=$(printf "%02d.%02d.00" $MAJOR $MINOR)
TODAY=$(date +%Y-%m-%d)
echo "Stable bump: ${CURRENT} → ${VERSION} (minor)"
# Update README.md
sed -i "s/VERSION:[[:space:]]*${CURRENT}/VERSION: ${VERSION}/" README.md
# Update manifest
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
if [ -n "$MANIFEST" ]; then
MANIFEST_VER=$(sed -n 's/.*<version>\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1)
[ -n "$MANIFEST_VER" ] && sed -i "s|<version>${MANIFEST_VER}</version>|<version>${VERSION}</version>|" "$MANIFEST"
sed -i "s|<creationDate>[^<]*</creationDate>|<creationDate>${TODAY}</creationDate>|" "$MANIFEST"
fi
# Promote [Unreleased] section in CHANGELOG.md to new version
if [ -f "CHANGELOG.md" ] && grep -qi "Unreleased" CHANGELOG.md; then
sed -i "s|## \[Unreleased\]|## [${VERSION}] --- ${TODAY}|" CHANGELOG.md
sed -i "s|## Unreleased|## [${VERSION}] --- ${TODAY}|" CHANGELOG.md
sed -i "2i ## [Unreleased]" CHANGELOG.md
sed -i "3i \\ " CHANGELOG.md
echo "CHANGELOG promoted to [${VERSION}]"
fi
# Commit and push
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 ${CURRENT} → ${VERSION} [skip ci]"
git push origin HEAD:main 2>&1
}
# Override version output for rest of pipeline
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "major=$(printf "%02d" $MAJOR)" >> "$GITHUB_OUTPUT"
- 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
echo "## Pre-Release Sanity Checks (Joomla)" >> $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
# -- Joomla: manifest version drift --------
MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
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
fi
# -- Joomla: XML manifest existence --------
if [ -z "$MANIFEST" ]; then
echo "- No Joomla XML manifest found" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
else
echo "- Manifest: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY
# -- Joomla: extension type check --------
TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" 2>/dev/null)
echo "- Extension type: ${TYPE:-unknown}" >> $GITHUB_STEP_SUMMARY
fi
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/mokostandards-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' &&
steps.check.outputs.already_released != 'true'
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
find . -name "*.md" ! -path "./.git/*" ! -path "./vendor/*" | while read -r f; do
if grep -q '\[VERSION:' "$f" 2>/dev/null; then
sed -i "s/\[VERSION:[[:space:]]*[0-9]\{2\}\.[0-9]\{2\}\.[0-9]\{2\}\]/[VERSION: ${VERSION}]/" "$f"
fi
done
# -- STEP 5: Write updates.xml (Joomla update server) ---------------------
- name: "Step 5: Write updates.xml"
id: updates
if: >-
steps.version.outputs.skip != 'true' &&
steps.check.outputs.already_released != 'true'
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
REPO="${{ github.repository }}"
# -- Parse extension metadata from XML manifest ----------------
MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
if [ -z "$MANIFEST" ]; then
echo "Warning: No Joomla XML manifest found — skipping updates.xml" >> $GITHUB_STEP_SUMMARY
exit 0
fi
# Extract fields using sed (portable — no grep -P)
EXT_NAME=$(sed -n 's/.*<name>\([^<]*\)<\/name>.*/\1/p' "$MANIFEST" | head -1)
EXT_TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
EXT_ELEMENT=$(sed -n 's/.*<element>\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" | head -1)
EXT_CLIENT=$(sed -n 's/.*<extension[^>]*client="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
EXT_FOLDER=$(sed -n 's/.*<extension[^>]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
TARGET_PLATFORM=$(sed -n 's/.*\(<targetplatform[^/]*\/>\).*/\1/p' "$MANIFEST" | head -1)
PHP_MINIMUM=$(sed -n 's/.*<php_minimum>\([^<]*\)<\/php_minimum>.*/\1/p' "$MANIFEST" | head -1)
# If EXT_NAME is a language key (e.g. PLG_SYSTEM_MOKOJGDPC), resolve from .ini
if echo "$EXT_NAME" | grep -qE '^[A-Z_]+$'; then
INI_NAME=$(find . -name "*.sys.ini" -path "*/en-GB/*" -exec grep -h "^${EXT_NAME}=" {} \; 2>/dev/null | head -1 | cut -d'"' -f2)
[ -z "$INI_NAME" ] && INI_NAME=$(find . -name "*.sys.ini" -exec grep -h "^${EXT_NAME}=" {} \; 2>/dev/null | head -1 | cut -d'"' -f2)
[ -n "$INI_NAME" ] && EXT_NAME="$INI_NAME"
fi
# Fallbacks
[ -z "$EXT_NAME" ] && EXT_NAME="${{ github.event.repository.name }}"
[ -z "$EXT_TYPE" ] && EXT_TYPE="component"
# Derive element if not in manifest:
# 1. plugin="xxx" attribute (plugins)
# 2. module="xxx" attribute (modules)
# 3. XML filename (components, packages)
# 4. Repo name fallback (templates, anything else)
if [ -z "$EXT_ELEMENT" ]; then
EXT_ELEMENT=$(sed -n 's/.*plugin="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
fi
if [ -z "$EXT_ELEMENT" ]; then
EXT_ELEMENT=$(sed -n 's/.*module="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
fi
if [ -z "$EXT_ELEMENT" ]; then
FNAME=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]')
# If filename is generic (templateDetails, manifest), use repo name
case "$FNAME" in
templatedetails|manifest) EXT_ELEMENT=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;;
*) EXT_ELEMENT="$FNAME" ;;
esac
fi
# Final fallback
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
# Save for Steps 7, 8, 8b
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
echo "ext_name=${EXT_NAME}" >> "$GITHUB_OUTPUT"
echo "ext_type=${EXT_TYPE}" >> "$GITHUB_OUTPUT"
echo "ext_folder=${EXT_FOLDER}" >> "$GITHUB_OUTPUT"
# Build client tag: plugins and frontend modules need <client>site</client>
CLIENT_TAG=""
if [ -n "$EXT_CLIENT" ]; then
CLIENT_TAG="<client>${EXT_CLIENT}</client>"
elif [ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]; then
CLIENT_TAG="<client>site</client>"
fi
# Build folder tag for plugins (required for Joomla to match the update)
FOLDER_TAG=""
if [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ]; then
FOLDER_TAG="<folder>${EXT_FOLDER}</folder>"
fi
# Build targetplatform (fallback to Joomla 5 if not in manifest)
if [ -z "$TARGET_PLATFORM" ]; then
TARGET_PLATFORM=$(printf '<targetplatform name="joomla" version="((5.[0-9])|(6.[0-9]))" %s>' "/")
fi
# Build php_minimum tag
PHP_TAG=""
if [ -n "$PHP_MINIMUM" ]; then
PHP_TAG="<php_minimum>${PHP_MINIMUM}</php_minimum>"
fi
# Build TYPE_PREFIX for download URL
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
DOWNLOAD_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/stable/${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip"
INFO_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/stable"
# -- Build update entry for a given stability tag
build_entry() {
local TAG_NAME="$1"
printf '%s\n' ' <update>'
printf '%s\n' " <name>${EXT_NAME}</name>"
printf '%s\n' " <description>${EXT_NAME} update</description>"
printf '%s\n' " <element>${EXT_ELEMENT}</element>"
printf '%s\n' " <type>${EXT_TYPE}</type>"
printf '%s\n' " <version>${VERSION}</version>"
[ -n "$CLIENT_TAG" ] && printf '%s\n' " ${CLIENT_TAG}"
[ -n "$FOLDER_TAG" ] && printf '%s\n' " ${FOLDER_TAG}"
printf '%s\n' " <tags><tag>${TAG_NAME}</tag></tags>"
printf '%s\n' " <infourl title=\"${EXT_NAME}\">${INFO_URL}</infourl>"
printf '%s\n' ' <downloads>'
printf '%s\n' " <downloadurl type=\"full\" format=\"zip\">${DOWNLOAD_URL}</downloadurl>"
printf '%s\n' ' </downloads>'
printf '%s\n' " ${TARGET_PLATFORM}"
[ -n "$PHP_TAG" ] && printf '%s\n' " ${PHP_TAG}"
printf '%s\n' ' <maintainer>Moko Consulting</maintainer>'
printf '%s\n' ' <maintainerurl>https://mokoconsulting.tech</maintainerurl>'
printf '%s\n' ' </update>'
}
# -- Write updates.xml with cascading channels
# Stable release updates ALL channels (development, alpha, beta, rc, stable)
{
printf '%s\n' "<?xml version='1.0' encoding='UTF-8'?>"
printf '%s\n' "<!-- Copyright (C) $(date +%Y) Moko Consulting <hello@mokoconsulting.tech>"
printf '%s\n' " SPDX-License-Identifier: GPL-3.0-or-later"
printf '%s\n' " VERSION: ${VERSION}"
printf '%s\n' " -->"
printf '%s\n' ""
printf '%s\n' '<updates>'
build_entry "development"
build_entry "alpha"
build_entry "beta"
build_entry "rc"
build_entry "stable"
printf '%s\n' '</updates>'
} > updates.xml
echo "updates.xml: ${VERSION} (all channels updated to stable)" >> $GITHUB_STEP_SUMMARY
# -- Commit all changes ---------------------------------------------------
- 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/mokostandards-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 Joomla 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/ — skipping package"; exit 0; }
EXCLUDES=".ftpignore sftp-config* *.ppk *.pem *.key .env*"
# ZIP package
cd "$SOURCE_DIR"
zip -r "/tmp/${ZIP_NAME}" . -x $EXCLUDES
cd ..
# tar.gz package
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 "### Joomla 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/mokostandards-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"
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 }}"
# Stable deletes all pre-release channels
TAGS_TO_DELETE="development alpha beta release-candidate"
DELETED=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})"
DELETED=$((DELETED + 1))
fi
done
echo "Cleaned up ${DELETED} pre-release channel(s)" >> $GITHUB_STEP_SUMMARY
# -- STEP 11: Reset dev branch from main ------------------------------------
- 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
# -- Summary --------------------------------------------------------------
- name: Pipeline Summary
if: always()
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
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 (Joomla)" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
echo "|------|--------|" >> $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
+48
View File
@@ -0,0 +1,48 @@
---
name: Bug Report
about: Report a bug or issue with the project
title: '[BUG] '
labels: 'bug'
assignees: ''
---
## Bug Description
A clear and concise description of what the bug is.
## Steps to Reproduce
1. Go to '...'
2. Click on '...'
3. Scroll down to '...'
4. See error
## Expected Behavior
A clear and concise description of what you expected to happen.
## Actual Behavior
A clear and concise description of what actually happened.
## Screenshots
If applicable, add screenshots to help explain your problem.
## Environment
- **Project**: [e.g., MokoDoliTools, moko-cassiopeia]
- **Version**: [e.g., 1.2.3]
- **Platform**: [e.g., Dolibarr 18.0, Joomla 5.0]
- **PHP Version**: [e.g., 8.1]
- **Database**: [e.g., MySQL 8.0, PostgreSQL 14]
- **Browser** (if applicable): [e.g., Chrome 120, Firefox 121]
- **OS**: [e.g., Ubuntu 22.04, Windows 11]
## Additional Context
Add any other context about the problem here.
## Possible Solution
If you have suggestions on how to fix the issue, please describe them here.
## Checklist
- [ ] I have searched for similar issues before creating this one
- [ ] I have provided all the requested information
- [ ] I have tested this on the latest stable version
- [ ] I have checked the documentation and couldn't find a solution
+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: MokoStandards.Maintenance
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
# 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: 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
@@ -5,13 +5,12 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: GitHub.Workflow.Template # DEFGROUP: Gitea.Workflow.Template
# INGROUP: MokoStandards.CI # INGROUP: MokoStandards.CI
# REPO: https://github.com/mokoconsulting-tech/MokoStandards # REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
# PATH: /templates/workflows/joomla/ci-joomla.yml.template # PATH: /templates/workflows/joomla/ci-joomla.yml.template
# VERSION: 04.06.00 # VERSION: 04.06.00
# BRIEF: CI workflow for Joomla extensions — lint, validate, test # BRIEF: CI workflow for Joomla extensions — lint, validate, test
# NOTE: Deployed to .github/workflows/ci-joomla.yml in governed Joomla extension repos.
name: Joomla Extension CI name: Joomla Extension CI
@@ -39,24 +38,22 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup PHP - name: Setup PHP
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 run: |
with: php -v && composer --version
php-version: '8.2'
extensions: mbstring, xml, zip, gd, curl, json, simplexml
tools: composer:v2
coverage: none
- name: Clone MokoStandards - name: Clone MokoStandards
env: env:
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} 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' }}
run: | run: |
git clone --depth 1 --branch version/04 --quiet \ git clone --depth 1 --branch main --quiet \
"https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
/tmp/mokostandards /tmp/mokostandards-api
- name: Install dependencies - name: Install dependencies
env: env:
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
run: | run: |
if [ -f "composer.json" ]; then if [ -f "composer.json" ]; then
composer install \ composer install \
@@ -344,16 +341,12 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup PHP ${{ matrix.php }} - name: Setup PHP ${{ matrix.php }}
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 run: |
with: php -v && composer --version
php-version: ${{ matrix.php }}
extensions: mbstring, xml, zip, gd, curl, json, simplexml
tools: composer:v2
coverage: none
- name: Install dependencies - name: Install dependencies
env: env:
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
run: | run: |
if [ -f "composer.json" ]; then if [ -f "composer.json" ]; then
composer install \ composer install \
@@ -382,3 +375,76 @@ jobs:
else else
echo "No phpunit.xml found — skipping tests." >> $GITHUB_STEP_SUMMARY echo "No phpunit.xml found — skipping tests." >> $GITHUB_STEP_SUMMARY
fi fi
static-analysis:
name: PHPStan Analysis
runs-on: ubuntu-latest
needs: lint-and-validate
continue-on-error: true
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup PHP
run: php -v && composer --version
- name: Install dependencies
env:
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
run: |
if [ -f "composer.json" ]; then
composer install --no-interaction --prefer-dist --optimize-autoloader
fi
- name: Install PHPStan
run: |
if ! command -v vendor/bin/phpstan &> /dev/null; then
composer require --dev phpstan/phpstan --no-interaction 2>/dev/null || \
composer global require phpstan/phpstan --no-interaction
fi
- name: Run PHPStan
run: |
echo "### PHPStan Static Analysis" >> $GITHUB_STEP_SUMMARY
PHPSTAN="vendor/bin/phpstan"
if [ ! -f "$PHPSTAN" ]; then
PHPSTAN=$(composer global config bin-dir --absolute 2>/dev/null)/phpstan
fi
# Determine source directory
SRC_DIR=""
for DIR in src/ htdocs/ lib/; do
if [ -d "$DIR" ]; then
SRC_DIR="$DIR"
break
fi
done
if [ -z "$SRC_DIR" ]; then
echo "No source directory found (src/, htdocs/, lib/) — skipping." >> $GITHUB_STEP_SUMMARY
exit 0
fi
# Use repo phpstan.neon if present, otherwise use baseline config
ARGS="analyse ${SRC_DIR} --memory-limit=512M --no-progress --error-format=table"
if [ -f "phpstan.neon" ] || [ -f "phpstan.neon.dist" ]; then
echo "Using project PHPStan config." >> $GITHUB_STEP_SUMMARY
else
ARGS="$ARGS --level=3"
echo "No phpstan.neon found — using level 3 (type inference)." >> $GITHUB_STEP_SUMMARY
fi
$PHPSTAN $ARGS 2>&1 | tee /tmp/phpstan-output.txt
EXIT=${PIPESTATUS[0]}
if [ $EXIT -eq 0 ]; then
echo "**No errors found.**" >> $GITHUB_STEP_SUMMARY
else
ERRORS=$(grep -c "ERROR" /tmp/phpstan-output.txt 2>/dev/null || echo "some")
echo "**${ERRORS} error(s) found.** Review output above." >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
tail -30 /tmp/phpstan-output.txt >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
fi
exit $EXIT
+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: MokoStandards.Maintenance
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /.gitea/workflows/cleanup.yml
# VERSION: 01.00.00
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
name: 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)"
@@ -3,14 +3,12 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: GitHub.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Deploy # INGROUP: MokoStandards.Deploy
# REPO: https://github.com/mokoconsulting-tech/MokoStandards # REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
# PATH: /templates/workflows/joomla/deploy-manual.yml.template # 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 # 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: Deploy to Dev (Manual)
@@ -39,23 +37,21 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup PHP - name: Setup PHP
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 run: |
with: php -v && composer --version
php-version: '8.2'
extensions: json, ssh2
tools: composer
coverage: none
- name: Setup MokoStandards tools - name: Setup MokoStandards tools
env: env:
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_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: | run: |
git clone --depth 1 --branch version/04 --quiet \ git clone --depth 1 --branch main --quiet \
"https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
/tmp/mokostandards 2>/dev/null || true /tmp/mokostandards-api 2>/dev/null || true
if [ -d "/tmp/mokostandards" ] && [ -f "/tmp/mokostandards/composer.json" ]; then if [ -d "/tmp/mokostandards-api" ] && [ -f "/tmp/mokostandards-api/composer.json" ]; then
cd /tmp/mokostandards && composer install --no-dev --no-interaction --quiet 2>/dev/null || true cd /tmp/mokostandards-api && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
fi fi
- name: Check FTP configuration - name: Check FTP configuration
@@ -63,11 +59,10 @@ jobs:
env: env:
HOST: ${{ vars.DEV_FTP_HOST }} HOST: ${{ vars.DEV_FTP_HOST }}
PATH_VAR: ${{ vars.DEV_FTP_PATH }} PATH_VAR: ${{ vars.DEV_FTP_PATH }}
SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
PORT: ${{ vars.DEV_FTP_PORT }} PORT: ${{ vars.DEV_FTP_PORT }}
run: | run: |
if [ -z "$HOST" ] || [ -z "$PATH_VAR" ]; then 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" echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0 exit 0
fi fi
@@ -75,7 +70,6 @@ jobs:
echo "host=$HOST" >> "$GITHUB_OUTPUT" echo "host=$HOST" >> "$GITHUB_OUTPUT"
REMOTE="${PATH_VAR%/}" REMOTE="${PATH_VAR%/}"
[ -n "$SUFFIX" ] && REMOTE="${REMOTE}/${SUFFIX#/}"
echo "remote=$REMOTE" >> "$GITHUB_OUTPUT" echo "remote=$REMOTE" >> "$GITHUB_OUTPUT"
[ -z "$PORT" ] && PORT="22" [ -z "$PORT" ] && PORT="22"
@@ -90,7 +84,7 @@ jobs:
run: | run: |
SOURCE_DIR="src" SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" [ ! -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"' \ printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
"${{ steps.check.outputs.host }}" "${{ steps.check.outputs.port }}" "$SFTP_USER" "${{ steps.check.outputs.remote }}" \ "${{ steps.check.outputs.host }}" "${{ steps.check.outputs.port }}" "$SFTP_USER" "${{ steps.check.outputs.remote }}" \
@@ -107,11 +101,11 @@ jobs:
DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json) DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json)
[ "${{ inputs.clear_remote }}" = "true" ] && DEPLOY_ARGS+=(--clear-remote) [ "${{ inputs.clear_remote }}" = "true" ] && DEPLOY_ARGS+=(--clear-remote)
PLATFORM=$(php /tmp/mokostandards/api/cli/platform_detect.php --path . 2>/dev/null || true) 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 if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards-api/deploy/deploy-joomla.php" ]; then
php /tmp/mokostandards/api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}" php /tmp/mokostandards-api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}"
else else
php /tmp/mokostandards/api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}" php /tmp/mokostandards-api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}"
fi fi
rm -f /tmp/deploy_key /tmp/sftp-config.json rm -f /tmp/deploy_key /tmp/sftp-config.json
@@ -120,7 +114,7 @@ jobs:
if: always() if: always()
run: | run: |
if [ "${{ steps.check.outputs.skip }}" = "true" ]; then 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 else
echo "### Manual Dev Deploy Complete" >> $GITHUB_STEP_SUMMARY echo "### Manual Dev Deploy Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY
+52
View File
@@ -0,0 +1,52 @@
---
name: Documentation Issue
about: Report an issue with documentation
title: '[DOCS] '
labels: 'documentation'
assignees: ''
---
## Documentation Issue
**Location**:
<!-- Specify the file, page, or section with the issue -->
## Issue Type
<!-- Mark the relevant option with an "x" -->
- [ ] Typo or grammar error
- [ ] Outdated information
- [ ] Missing documentation
- [ ] Unclear explanation
- [ ] Broken links
- [ ] Missing examples
- [ ] Other (specify below)
## Description
<!-- Clearly describe the documentation issue -->
## Current Content
<!-- Quote or describe the current documentation (if applicable) -->
```
Current text here
```
## Suggested Improvement
<!-- Provide your suggestion for how to improve the documentation -->
```
Suggested text here
```
## Additional Context
<!-- Add any other context, screenshots, or references -->
## Standards Alignment
- [ ] Follows MokoStandards documentation guidelines
- [ ] Uses en_US/en_GB localization
- [ ] Includes proper SPDX headers where applicable
## Checklist
- [ ] I have searched for similar documentation issues
- [ ] I have provided a clear description
- [ ] I have suggested an improvement (if applicable)
+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: MokoStandards.Security
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
# 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: 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
+87
View File
@@ -0,0 +1,87 @@
---
name: Joomla Extension Issue
about: Report an issue with a Joomla extension
title: '[JOOMLA] '
labels: 'joomla'
assignees: ''
---
## Issue Type
- [ ] Component Issue
- [ ] Module Issue
- [ ] Plugin Issue
- [ ] Template Issue
## Extension Details
- **Extension Name**: [e.g., moko-cassiopeia]
- **Extension Version**: [e.g., 1.2.3]
- **Extension Type**: [Component / Module / Plugin / Template]
## Joomla Environment
- **Joomla Version**: [e.g., 4.4.0, 5.0.0]
- **PHP Version**: [e.g., 8.1.0]
- **Database**: [MySQL / PostgreSQL / MariaDB]
- **Database Version**: [e.g., 8.0]
- **Server**: [Apache / Nginx / IIS]
- **Hosting**: [Shared / VPS / Dedicated / Cloud]
## Issue Description
Provide a clear and detailed description of the issue.
## Steps to Reproduce
1. Go to '...'
2. Click on '...'
3. Configure '...'
4. See error
## Expected Behavior
What you expected to happen.
## Actual Behavior
What actually happened.
## Error Messages
```
# Paste any error messages from Joomla error logs
# Location: administrator/logs/error.php
```
## Browser Console Errors
```javascript
// Paste any JavaScript console errors (F12 in browser)
```
## Screenshots
Add screenshots to help explain the issue.
## Configuration
```ini
# Paste extension configuration (sanitize sensitive data)
```
## Installed Extensions
List other installed extensions that might conflict:
- Extension 1 (version)
- Extension 2 (version)
## Template Overrides
- [ ] Using template overrides
- [ ] Custom CSS
- [ ] Custom JavaScript
## Additional Context
- **Multilingual Site**: [Yes / No]
- **Cache Enabled**: [Yes / No]
- **Debug Mode**: [Yes / No]
- **SEF URLs**: [Yes / No]
## Checklist
- [ ] I have cleared Joomla cache
- [ ] I have disabled other extensions to test for conflicts
- [ ] I have checked Joomla error logs
- [ ] I have tested with a default Joomla template
- [ ] I have checked browser console for JavaScript errors
- [ ] I have searched for similar issues
- [ ] I am using a supported Joomla version
+24
View File
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Moko Platform Repository Manifest
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>MokoWaaS</name>
<org>MokoConsulting</org>
<description>White-label identity, security hardening, and tenant restriction layer for WaaS-managed Joomla environments</description>
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
</identity>
<governance>
<platform>joomla</platform>
<standards-version>05.00.00</standards-version>
<standards-source>https://git.mokoconsulting.tech/MokoConsulting/moko-platform</standards-source>
<last-synced>2026-05-21T20:48:00+00:00</last-synced>
</governance>
<build>
<language>PHP</language>
<package-type>package</package-type>
<entry-point>src/</entry-point>
</build>
</moko-platform>
+71
View File
@@ -0,0 +1,71 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Notifications
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /.gitea/workflows/notify.yml
# VERSION: 01.00.00
# BRIEF: Push notifications via ntfy on release success or workflow failure
name: Notifications
on:
workflow_run:
workflows:
- "Joomla Build & Release"
- "Joomla Extension CI"
- "Deploy"
- "Cascade Main → Dev"
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}"
+90
View File
@@ -0,0 +1,90 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# SPDX-License-Identifier: GPL-3.0-or-later
#
# 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
+106
View File
@@ -0,0 +1,106 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.CI
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /.gitea/workflows/pr-check.yml
# VERSION: 01.00.00
# BRIEF: PR gate — validates code quality and manifest before merge to main
name: PR Check
on:
pull_request:
branches:
- main
types: [opened, synchronize, reopened]
permissions:
contents: read
pull-requests: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
validate:
name: Validate PR
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup PHP
run: |
if ! command -v php &> /dev/null; then
sudo apt-get update -qq
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
fi
- name: PHP syntax check
run: |
echo "=== PHP Lint ==="
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 "Checked files, errors: ${ERRORS}"
[ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
- name: Validate Joomla manifest
run: |
echo "=== Manifest Validation ==="
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"
exit 0
fi
echo "Manifest: ${MANIFEST}"
# Check well-formed XML
if ! 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);}"; then
echo "::error::Manifest XML is malformed"
exit 1
fi
# Check required elements
for ELEMENT in name version description; do
if ! grep -q "<${ELEMENT}>" "$MANIFEST"; then
echo "::error::Missing <${ELEMENT}> in manifest"
exit 1
fi
done
echo "Manifest valid"
- name: Check updates.xml format
run: |
if [ ! -f "updates.xml" ]; then
echo "No updates.xml — skipping"
exit 0
fi
echo "=== updates.xml Validation ==="
if ! 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);}"; then
echo "::error::updates.xml is malformed"
exit 1
fi
echo "updates.xml valid"
- name: Verify package builds
run: |
echo "=== Package Build Test ==="
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
if [ ! -d "$SOURCE_DIR" ]; then
echo "::warning::No src/ or htdocs/ directory"
exit 0
fi
# Dry-run: ensure zip would succeed
FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
echo "Source contains ${FILE_COUNT} files — package will build"
[ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; }
+341
View File
@@ -0,0 +1,341 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /.gitea/workflows/pre-release.yml
# VERSION: 01.00.00
# BRIEF: Manual pre-release — builds dev/alpha/beta/rc packages from any branch
name: 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 >/dev/null 2>&1
fi
- name: Resolve metadata
id: meta
run: |
STABILITY="${{ inputs.stability }}"
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
# Read and bump patch version (with rollover)
CURRENT=$(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)
[ -z "$CURRENT" ] && CURRENT="00.00.00"
MAJOR=$(echo "$CURRENT" | cut -d. -f1)
MINOR=$(echo "$CURRENT" | cut -d. -f2)
PATCH=$(echo "$CURRENT" | cut -d. -f3)
# Patch bump with rollover: ZZ=99 → bump minor, YY=99 → bump major
NEW_PATCH=$((10#$PATCH + 1))
NEW_MINOR=$((10#$MINOR))
NEW_MAJOR=$((10#$MAJOR))
if [ $NEW_PATCH -gt 99 ]; then
NEW_PATCH=0
NEW_MINOR=$((NEW_MINOR + 1))
fi
if [ $NEW_MINOR -gt 99 ]; then
NEW_MINOR=0
NEW_MAJOR=$((NEW_MAJOR + 1))
fi
VERSION=$(printf "%02d.%02d.%02d" $NEW_MAJOR $NEW_MINOR $NEW_PATCH)
TODAY=$(date +%Y-%m-%d)
echo "Bumping: ${CURRENT} → ${VERSION} (patch)"
# Update README.md
sed -i "s/VERSION:[[:space:]]*${CURRENT}/VERSION: ${VERSION}/" README.md
# Update manifest
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
if [ -n "$MANIFEST" ]; then
MANIFEST_VER=$(sed -n 's/.*<version>\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1)
sed -i "s|<version>${MANIFEST_VER}</version>|<version>${VERSION}</version>|" "$MANIFEST"
sed -i "s|<creationDate>[^<]*</creationDate>|<creationDate>${TODAY}</creationDate>|" "$MANIFEST"
fi
# 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 ${CURRENT} → ${VERSION} [skip ci]"
git push origin HEAD 2>&1
}
# Auto-detect element from manifest
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
EXT_ELEMENT=""
if [ -n "$MANIFEST" ]; then
EXT_ELEMENT=$(sed -n 's/.*<element>\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
if [ -z "$EXT_ELEMENT" ]; then
EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]')
case "$EXT_ELEMENT" in
templatedetails|manifest) EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;;
esac
fi
else
EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
fi
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 "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT"
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
- name: Build package
run: |
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
if [ ! -d "$SOURCE_DIR" ]; then
echo "::error::No src/ or htdocs/ directory"
exit 1
fi
mkdir -p build/package
rsync -a \
--exclude='sftp-config*' \
--exclude='.ftpignore' \
--exclude='*.ppk' \
--exclude='*.pem' \
--exclude='*.key' \
--exclude='.env*' \
--exclude='*.local' \
--exclude='.build-trigger' \
"${SOURCE_DIR}/" build/package/
- name: Create ZIP
id: zip
run: |
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
cd build/package
zip -r "../${ZIP_NAME}" .
cd ..
SHA256=$(sha256sum "${ZIP_NAME}" | cut -d' ' -f1)
echo "sha256=${SHA256}" >> "$GITHUB_OUTPUT"
echo "ZIP: ${ZIP_NAME} (SHA: ${SHA256:0:16}...)"
- name: Create or replace Gitea release
id: release
run: |
TAG="${{ steps.meta.outputs.tag }}"
VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
SHA256="${{ steps.zip.outputs.sha256 }}"
ZIP_NAME="${{ steps.meta.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 "@build/${ZIP_NAME}"
echo "Released: ${EXT_ELEMENT} ${VERSION} (${STABILITY})"
- name: Update updates.xml
run: |
STABILITY="${{ steps.meta.outputs.stability }}"
VERSION="${{ steps.meta.outputs.version }}"
SHA256="${{ steps.zip.outputs.sha256 }}"
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
TAG="${{ steps.meta.outputs.tag }}"
DATE=$(date +%Y-%m-%d)
if [ ! -f "updates.xml" ]; then
echo "No updates.xml — skipping"
exit 0
fi
export PY_STABILITY="$STABILITY" PY_VERSION="$VERSION" PY_SHA256="$SHA256" \
PY_ZIP_NAME="$ZIP_NAME" PY_TAG="$TAG" PY_DATE="$DATE" \
PY_GITEA_ORG="$GITEA_ORG" PY_GITEA_REPO="$GITEA_REPO"
python3 << 'PYEOF'
import re, os
stability = os.environ["PY_STABILITY"]
version = os.environ["PY_VERSION"]
sha256 = os.environ["PY_SHA256"]
zip_name = os.environ["PY_ZIP_NAME"]
tag = os.environ["PY_TAG"]
date = os.environ["PY_DATE"]
gitea_org = os.environ["PY_GITEA_ORG"]
gitea_repo = os.environ["PY_GITEA_REPO"]
download_url = f"https://git.mokoconsulting.tech/{gitea_org}/{gitea_repo}/releases/download/{tag}/{zip_name}"
with open("updates.xml", "r") as f:
content = f.read()
# Map stability to XML tag name
tag_map = {"development": "development", "alpha": "alpha", "beta": "beta", "release-candidate": "rc"}
xml_tag = tag_map.get(stability, stability)
pattern = r"(<update>(?:(?!</update>).)*?<tag>" + re.escape(xml_tag) + r"</tag>.*?</update>)"
match = re.search(pattern, content, re.DOTALL)
if match:
block = match.group(1)
updated = re.sub(r"<version>[^<]*</version>", f"<version>{version}</version>", block)
updated = re.sub(r"<creationDate>[^<]*</creationDate>", f"<creationDate>{date}</creationDate>", updated)
if "<sha256>" in updated:
updated = re.sub(r"<sha256>[^<]*</sha256>", f"<sha256>{sha256}</sha256>", updated)
else:
updated = updated.replace("</downloads>", f"</downloads>\n <sha256>{sha256}</sha256>")
updated = re.sub(r"(<downloadurl[^>]*>)[^<]*(</downloadurl>)", rf"\g<1>{download_url}\g<2>", updated)
content = content.replace(block, updated)
print(f"Updated {xml_tag} channel: version={version}")
else:
print(f"WARNING: No <tag>{xml_tag}</tag> block in updates.xml")
with open("updates.xml", "w") as f:
f.write(content)
PYEOF
# Commit and push to current branch
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"
run: |
CURRENT_BRANCH="${{ github.ref_name }}"
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
# Sync updates.xml to main and dev (whichever isn't current)
for BRANCH in main dev; do
[ "$BRANCH" = "$CURRENT_BRANCH" ] && continue
echo "Syncing updates.xml → ${BRANCH}"
git fetch origin "${BRANCH}" 2>/dev/null || continue
git checkout "origin/${BRANCH}" -- . 2>/dev/null || continue
git checkout "${CURRENT_BRANCH}" -- updates.xml
if ! git diff --quiet updates.xml 2>/dev/null; then
git add updates.xml
git commit -m "chore: sync updates.xml from ${CURRENT_BRANCH} [skip ci]"
git push origin HEAD:refs/heads/${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed"
fi
git checkout "${CURRENT_BRANCH}" 2>/dev/null
done
- 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
+600
View File
@@ -0,0 +1,600 @@
# 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: Gitea.Workflow
# INGROUP: MokoStandards.Joomla
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /.gitea/workflows/release.yml
# VERSION: 02.00.00
# BRIEF: Generic Joomla release — auto-detects element from manifest, stream tags, cascade
name: Create Release
on:
push:
tags:
- 'stable'
- 'release-candidate'
- 'beta'
- 'alpha'
- 'development'
workflow_dispatch:
inputs:
stability:
description: 'Stability tag'
required: true
default: 'stable'
type: choice
options:
- stable
- release-candidate
- beta
- alpha
- development
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 Release Package
runs-on: release
steps:
# Always checkout main for tag triggers (avoids detached HEAD).
# For workflow_dispatch, checkout whatever branch was selected.
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'push' && 'main' || github.ref }}
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 composer >/dev/null 2>&1
fi
echo "PHP: $(php -v | head -1)"
echo "Composer: $(composer --version 2>&1 | head -1)"
- name: Get version and stability
id: meta
run: |
echo "=== Meta ==="
echo "event_name: ${{ github.event_name }}"
echo "ref: ${{ github.ref }}"
echo "ref_name: ${{ github.ref_name }}"
echo "sha: ${{ github.sha }}"
# Derive stability from tag name or dispatch input
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
STABILITY="${{ inputs.stability }}"
else
TAG_PUSHED="${GITHUB_REF#refs/tags/}"
case "$TAG_PUSHED" in
stable) STABILITY="stable" ;;
release-candidate) STABILITY="rc" ;;
beta) STABILITY="beta" ;;
alpha) STABILITY="alpha" ;;
development) STABILITY="development" ;;
*) STABILITY="stable" ;;
esac
fi
# Read version from README.md (will be bumped in next step)
VERSION=$(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)
[ -z "$VERSION" ] && VERSION="00.00.00"
# Auto-detect extension element from Joomla manifest
# Search depth 3 covers src/admin/com_xxx.xml and similar nested structures
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "./build/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
EXT_ELEMENT=""
if [ -n "$MANIFEST" ]; then
EXT_ELEMENT=$(sed -n 's/.*<element>\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
# If no <element> tag, derive from manifest filename or repo name
if [ -z "$EXT_ELEMENT" ]; then
EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]')
case "$EXT_ELEMENT" in
templatedetails|manifest) EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;;
esac
fi
echo "Manifest: ${MANIFEST}, element: ${EXT_ELEMENT}"
else
EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
echo "No manifest found, using repo name: ${EXT_ELEMENT}"
fi
case "$STABILITY" in
development) SUFFIX="-dev"; TAG_NAME="development" ;;
alpha) SUFFIX="-alpha"; TAG_NAME="alpha" ;;
beta) SUFFIX="-beta"; TAG_NAME="beta" ;;
rc) SUFFIX="-rc"; TAG_NAME="release-candidate" ;;
stable) SUFFIX=""; TAG_NAME="stable" ;;
*) SUFFIX="-dev"; TAG_NAME="development" ;;
esac
PRERELEASE="true"
[ "$STABILITY" = "stable" ] && PRERELEASE="false"
ZIP_NAME="${EXT_ELEMENT}-${VERSION}${SUFFIX}.zip"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
echo "prerelease=${PRERELEASE}" >> "$GITHUB_OUTPUT"
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
echo "tag_name=${TAG_NAME}" >> "$GITHUB_OUTPUT"
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
echo "=== Resolved ==="
echo "VERSION=${VERSION}"
echo "STABILITY=${STABILITY}"
echo "TAG_NAME=${TAG_NAME}"
echo "ZIP_NAME=${ZIP_NAME}"
echo "Branch: $(git branch --show-current)"
- name: Auto-bump patch version
id: bump
env:
GA_TOKEN: ${{ secrets.GA_TOKEN }}
INPUT_VERSION: ${{ steps.meta.outputs.version }}
INPUT_STABILITY: ${{ steps.meta.outputs.stability }}
INPUT_SUFFIX: ${{ steps.meta.outputs.suffix }}
EXT_ELEMENT: ${{ steps.meta.outputs.ext_element }}
run: |
BRANCH=$(git branch --show-current)
GITEA_API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
echo "=== Version Bump ==="
echo "On branch: ${BRANCH}"
# Read current version from README.md
CURRENT=$(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 "Current version in README: ${CURRENT}"
if [ -z "$CURRENT" ]; then
echo "No VERSION in README.md — using input version"
echo "version=${INPUT_VERSION}" >> "$GITHUB_OUTPUT"
echo "zip_name=${EXT_ELEMENT}-${INPUT_VERSION}${INPUT_SUFFIX}.zip" >> "$GITHUB_OUTPUT"
exit 0
fi
# Bump patch: XX.YY.ZZ → XX.YY.(ZZ+1)
MAJOR=$(echo "$CURRENT" | cut -d. -f1)
MINOR=$(echo "$CURRENT" | cut -d. -f2)
PATCH=$(echo "$CURRENT" | cut -d. -f3)
NEW_PATCH=$(printf "%02d" $((10#$PATCH + 1)))
NEW_VERSION="${MAJOR}.${MINOR}.${NEW_PATCH}"
TODAY=$(date +%Y-%m-%d)
echo "Bumping: ${CURRENT} → ${NEW_VERSION} (date: ${TODAY})"
# Update README.md
sed -i "s/VERSION:[[:space:]]*${CURRENT}/VERSION: ${NEW_VERSION}/" README.md
# Update manifest (templateDetails.xml / *.xml with <extension>)
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "./build/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
if [ -n "$MANIFEST" ]; then
echo "Manifest: ${MANIFEST}"
sed -i "s|<version>${CURRENT}</version>|<version>${NEW_VERSION}</version>|" "$MANIFEST"
sed -i "s|<creationDate>[^<]*</creationDate>|<creationDate>${TODAY}</creationDate>|" "$MANIFEST"
fi
# Update matching stability channel in updates.xml
if [ -f "updates.xml" ]; then
export PY_OLD="$CURRENT" PY_NEW="$NEW_VERSION" PY_STABILITY="$INPUT_STABILITY" PY_DATE="$TODAY"
python3 << 'PYEOF'
import re, os
old = os.environ["PY_OLD"]
new = os.environ["PY_NEW"]
stability = os.environ["PY_STABILITY"]
date = os.environ["PY_DATE"]
with open("updates.xml") as f:
content = f.read()
pattern = r"(<update>(?:(?!</update>).)*?<tag>" + re.escape(stability) + r"</tag>.*?</update>)"
match = re.search(pattern, content, re.DOTALL)
if match:
block = match.group(1)
updated = block.replace(old, new)
updated = re.sub(r"<creationDate>[^<]*</creationDate>", f"<creationDate>{date}</creationDate>", updated)
content = content.replace(block, updated)
print(f"Updated {stability} channel: {old} -> {new}")
else:
print(f"WARNING: No <update> block found for <tag>{stability}</tag>")
with open("updates.xml", "w") as f:
f.write(content)
PYEOF
fi
# Commit and push 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:${GA_TOKEN}@git.mokoconsulting.tech/${{ github.repository }}.git"
git add -A
git diff --cached --quiet && echo "No changes to commit" || {
git commit -m "chore(version): bump ${CURRENT} → ${NEW_VERSION} [skip ci]" \
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
echo "Pushing version bump to ${BRANCH}..."
git push origin HEAD:${BRANCH} 2>&1
echo "Push exit code: $?"
}
# For stable releases from non-main: merge to main via Gitea API
if [ "$INPUT_STABILITY" = "stable" ] && [ "$BRANCH" != "main" ]; then
echo "Merging ${BRANCH} → main via Gitea API..."
HTTP_CODE=$(curl -sS -o /tmp/merge_response.json -w "%{http_code}" \
-X POST -H "Authorization: token ${GA_TOKEN}" \
-H "Content-Type: application/json" \
"${GITEA_API}/merges" \
-d "$(jq -n \
--arg base "main" \
--arg head "${BRANCH}" \
--arg msg "chore(release): merge ${BRANCH} for stable ${NEW_VERSION} [skip ci]" \
'{base: $base, head: $head, merge_message_field: $msg}'
)")
echo "Merge response (HTTP ${HTTP_CODE}):"
cat /tmp/merge_response.json 2>/dev/null; echo
fi
echo "version=${NEW_VERSION}" >> "$GITHUB_OUTPUT"
echo "zip_name=${EXT_ELEMENT}-${NEW_VERSION}${INPUT_SUFFIX}.zip" >> "$GITHUB_OUTPUT"
echo "=== Bump complete: ${NEW_VERSION} ==="
- name: Install dependencies
env:
COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.GA_TOKEN }}"}}}'
run: |
if [ -f "composer.json" ]; then
echo "Installing composer dependencies..."
composer install --no-dev --optimize-autoloader --no-interaction 2>&1
else
echo "No composer.json — skipping"
fi
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Minify CSS and JS
run: |
if [ -f "package.json" ] && [ -f "scripts/minify.js" ]; then
npm ci --ignore-scripts
node scripts/minify.js
else
echo "No minify setup — skipping"
fi
- name: Create package
run: |
# Detect source directory (src/ or htdocs/)
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
if [ ! -d "$SOURCE_DIR" ]; then
echo "::error::No src/ or htdocs/ directory found"
exit 1
fi
echo "Source directory: ${SOURCE_DIR}"
mkdir -p build/package
rsync -av \
--exclude='sftp-config*' \
--exclude='.ftpignore' \
--exclude='*.ppk' \
--exclude='*.pem' \
--exclude='*.key' \
--exclude='.env*' \
--exclude='*.local' \
--exclude='.build-trigger' \
--exclude='.beta-trigger' \
--exclude='.rc-trigger' \
"${SOURCE_DIR}/" build/package/
echo "Package contents:"
ls -la build/package/ | head -20
- name: Build ZIP
id: zip
run: |
ZIP_NAME="${{ steps.bump.outputs.zip_name }}"
echo "Building: ${ZIP_NAME}"
cd build/package
zip -r "../${ZIP_NAME}" .
cd ..
SHA256=$(sha256sum "${ZIP_NAME}" | cut -d' ' -f1)
SIZE=$(stat -c%s "${ZIP_NAME}")
echo "sha256=${SHA256}" >> "$GITHUB_OUTPUT"
echo "size=${SIZE}" >> "$GITHUB_OUTPUT"
echo "=== Package Built ==="
echo "ZIP: ${ZIP_NAME}"
echo "SHA-256: ${SHA256}"
echo "Size: ${SIZE} bytes"
# ── Gitea Release (PRIMARY) ─────────────────────────────────────
- name: "Gitea: Create or update release"
id: gitea_release
env:
EXT_ELEMENT: ${{ steps.meta.outputs.ext_element }}
run: |
TAG="${{ steps.meta.outputs.tag_name }}"
VERSION="${{ steps.bump.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
SHA256="${{ steps.zip.outputs.sha256 }}"
TOKEN="${{ secrets.GA_TOKEN }}"
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
BRANCH=$(git branch --show-current)
MAX_HISTORY=5
IS_PRE="true"
[ "$STABILITY" = "stable" ] && IS_PRE="false"
# Build this version's entry
NEW_ENTRY="## ${VERSION} ($(date +%Y-%m-%d))
**SHA-256:** \`${SHA256}\`"
if [ -f "CHANGELOG.md" ]; then
NOTES=$(awk "/## \[${VERSION}\]/,/## \[/{if(/## \[${VERSION}\]/)next;if(/## \[/)exit;print}" CHANGELOG.md)
[ -n "$NOTES" ] && NEW_ENTRY="## ${VERSION} ($(date +%Y-%m-%d))
${NOTES}
**SHA-256:** \`${SHA256}\`"
fi
# Check for existing release — keep last N versions in body
EXISTING_BODY=""
EXISTING_ID=""
RELEASE_JSON=$(curl -sS -H "Authorization: token ${TOKEN}" \
"${API}/releases/tags/${TAG}" 2>/dev/null)
EXISTING_ID=$(echo "$RELEASE_JSON" | jq -r '.id // empty')
if [ -n "$EXISTING_ID" ]; then
echo "Existing release found: id=${EXISTING_ID}"
EXISTING_BODY=$(echo "$RELEASE_JSON" | jq -r '.body // ""')
# Keep only last (MAX_HISTORY - 1) version entries to make room for new one
TRIMMED_BODY=$(echo "$EXISTING_BODY" | python3 -c "
import sys, re
content = sys.stdin.read()
# Split on version headers (## XX.YY.ZZ)
parts = re.split(r'(?=^## \d)', content, flags=re.MULTILINE)
# Keep only version entries (skip any preamble)
versions = [p for p in parts if re.match(r'^## \d', p)]
# Keep last $((MAX_HISTORY - 1)) entries
kept = versions[:$((MAX_HISTORY - 1))]
print('\n---\n'.join(kept))
" 2>/dev/null || echo "")
# Delete old release and tag so we can recreate
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
# Compose full body: new entry + previous entries
if [ -n "$TRIMMED_BODY" ]; then
FULL_BODY="${NEW_ENTRY}
---
${TRIMMED_BODY}"
else
FULL_BODY="${NEW_ENTRY}"
fi
echo "=== Create Release ==="
echo "TAG=${TAG} VERSION=${VERSION} BRANCH=${BRANCH} PRE=${IS_PRE} HISTORY=${MAX_HISTORY}"
HTTP_CODE=$(curl -sS -o /tmp/create_release.json -w "%{http_code}" \
-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 "$FULL_BODY" \
--argjson pre "$IS_PRE" \
'{tag_name: $tag, target_commitish: $target, name: $name, body: $body, prerelease: $pre}'
)")
echo "Response (HTTP ${HTTP_CODE}):"
cat /tmp/create_release.json | jq . 2>/dev/null || cat /tmp/create_release.json
echo
RELEASE_ID=$(jq -r '.id // empty' /tmp/create_release.json)
if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" = "null" ]; then
echo "::error::Failed to create release (HTTP ${HTTP_CODE})"
exit 1
fi
echo "release_id=${RELEASE_ID}" >> "$GITHUB_OUTPUT"
echo "Release created: id=${RELEASE_ID}"
- name: "Gitea: Upload ZIP"
run: |
RELEASE_ID="${{ steps.gitea_release.outputs.release_id }}"
ZIP_NAME="${{ steps.bump.outputs.zip_name }}"
TOKEN="${{ secrets.GA_TOKEN }}"
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
echo "Uploading ${ZIP_NAME} to release ${RELEASE_ID}..."
HTTP_CODE=$(curl -sS -o /tmp/upload_response.json -w "%{http_code}" \
-X POST \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/octet-stream" \
"${API}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" \
--data-binary "@build/${ZIP_NAME}")
echo "Upload response (HTTP ${HTTP_CODE}):"
cat /tmp/upload_response.json | jq . 2>/dev/null || cat /tmp/upload_response.json
echo
if [ "$HTTP_CODE" -ge 400 ]; then
echo "::error::Upload failed (HTTP ${HTTP_CODE})"
exit 1
fi
echo "Uploaded ${ZIP_NAME}"
# ── Update updates.xml ──────────────────────────────────────────
- name: "Update updates.xml with SHA and sync to main"
run: |
STABILITY="${{ steps.meta.outputs.stability }}"
VERSION="${{ steps.bump.outputs.version }}"
SHA256="${{ steps.zip.outputs.sha256 }}"
ZIP_NAME="${{ steps.bump.outputs.zip_name }}"
TAG="${{ steps.meta.outputs.tag_name }}"
DATE=$(date +%Y-%m-%d)
BRANCH=$(git branch --show-current)
echo "=== Update updates.xml ==="
echo "STABILITY=${STABILITY} VERSION=${VERSION} SHA=${SHA256:0:16}..."
if [ ! -f "updates.xml" ] || [ -z "$SHA256" ]; then
echo "No updates.xml or no SHA — skipping"
exit 0
fi
# Cascade map: each stability level updates itself + all lower levels
# stable → all | rc → rc,beta,alpha,dev | beta → beta,alpha,dev | alpha → alpha,dev | dev → dev
case "$STABILITY" in
stable) CASCADE="development,alpha,beta,rc,stable" ;;
rc) CASCADE="development,alpha,beta,rc" ;;
beta) CASCADE="development,alpha,beta" ;;
alpha) CASCADE="development,alpha" ;;
development) CASCADE="development" ;;
*) CASCADE="$STABILITY" ;;
esac
echo "Cascade: ${STABILITY} → ${CASCADE}"
export PY_CASCADE="$CASCADE" PY_VERSION="$VERSION" PY_SHA256="$SHA256" \
PY_ZIP_NAME="$ZIP_NAME" PY_TAG="$TAG" PY_DATE="$DATE" \
PY_GITEA_ORG="$GITEA_ORG" PY_GITEA_REPO="$GITEA_REPO"
python3 << 'PYEOF'
import re, os
cascade = os.environ["PY_CASCADE"].split(",")
version = os.environ["PY_VERSION"]
sha256 = os.environ["PY_SHA256"]
zip_name = os.environ["PY_ZIP_NAME"]
tag = os.environ["PY_TAG"]
date = os.environ["PY_DATE"]
gitea_org = os.environ["PY_GITEA_ORG"]
gitea_repo = os.environ["PY_GITEA_REPO"]
gitea_url = f"https://git.mokoconsulting.tech/{gitea_org}/{gitea_repo}/releases/download/{tag}/{zip_name}"
with open("updates.xml", "r") as f:
content = f.read()
for xml_tag in cascade:
xml_tag = xml_tag.strip()
block_pattern = r"(<update>(?:(?!</update>).)*?<tag>" + re.escape(xml_tag) + r"</tag>.*?</update>)"
match = re.search(block_pattern, content, re.DOTALL)
if not match:
print(f" SKIP: no <tag>{xml_tag}</tag> block found")
continue
block = match.group(1)
original_block = block
# Update version and date
block = re.sub(r"<version>[^<]*</version>", f"<version>{version}</version>", block)
block = re.sub(r"<creationDate>[^<]*</creationDate>", f"<creationDate>{date}</creationDate>", block)
# Set SHA — add if missing, update if present, never leave empty
if "<sha256>" in block:
block = re.sub(r"<sha256>[^<]*</sha256>", f"<sha256>{sha256}</sha256>", block)
else:
block = block.replace("</downloads>", f"</downloads>\n <sha256>{sha256}</sha256>")
# Update download URL
block = re.sub(
r"(<downloadurl[^>]*>)https://git\.mokoconsulting\.tech/[^<]*(</downloadurl>)",
rf"\g<1>{gitea_url}\g<2>",
block
)
content = content.replace(original_block, block)
print(f" OK: {xml_tag} → version={version}, sha={sha256[:16]}...")
with open("updates.xml", "w") as f:
f.write(content)
print(f"Cascaded {len(cascade)} channel(s)")
PYEOF
# Commit and push
if git diff --quiet updates.xml 2>/dev/null; then
echo "No changes to updates.xml"
exit 0
fi
git add updates.xml
git commit -m "chore: update ${STABILITY} SHA-256 for ${VERSION} [skip ci]" \
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
echo "Pushing updates.xml to ${BRANCH}..."
git push origin HEAD:${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed"
# Always sync updates.xml to main via API (Joomla reads from main)
GA_TOKEN="${{ secrets.GA_TOKEN }}"
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
echo "Syncing updates.xml to main via API..."
FILE_SHA=$(curl -sS -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)
HTTP_CODE=$(curl -sS -o /tmp/sync_response.json -w "%{http_code}" \
-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 ${STABILITY} ${VERSION} [skip ci]" \
--arg branch "main" \
'{content: $content, sha: $sha, message: $msg, branch: $branch}'
)")
echo "Sync response (HTTP ${HTTP_CODE}):"
cat /tmp/sync_response.json | jq -r '.content.name // .message // "unknown"' 2>/dev/null
if [ "$HTTP_CODE" -ge 400 ]; then
echo "::warning::Sync to main failed (HTTP ${HTTP_CODE})"
fi
else
echo "::warning::Could not get updates.xml SHA from main"
fi
- name: Summary
if: always()
run: |
VERSION="${{ steps.bump.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
ZIP_NAME="${{ steps.bump.outputs.zip_name }}"
SHA256="${{ steps.zip.outputs.sha256 }}"
TAG="${{ steps.meta.outputs.tag_name }}"
echo "### Release Created" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Stability | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY
echo "| Tag | \`${TAG}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY
echo "| SHA-256 | \`${SHA256}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Gitea | [Release](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${TAG}) |" >> $GITHUB_STEP_SUMMARY
@@ -6,13 +6,12 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: GitHub.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Validation # INGROUP: MokoStandards.Validation
# REPO: https://github.com/mokoconsulting-tech/MokoStandards # REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
# PATH: /.github/workflows/repo_health.yml # PATH: /templates/workflows/joomla/repo_health.yml.template
# VERSION: 04.06.00 # VERSION: 04.06.00
# BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts. # 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 name: Repo Health
@@ -50,13 +49,11 @@ env:
RELEASE_OPTIONAL_REPO_VARS: DEV_FTP_SUFFIX RELEASE_OPTIONAL_REPO_VARS: DEV_FTP_SUFFIX
# Scripts governance policy # Scripts governance policy
# Note: directories listed without a trailing slash.
SCRIPTS_REQUIRED_DIRS: SCRIPTS_REQUIRED_DIRS:
SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate
# Repo health policy # 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,.gitea/workflows/
REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.github/workflows/
REPO_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore,README.md,docs/ REPO_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore,README.md,docs/
REPO_DISALLOWED_DIRS: REPO_DISALLOWED_DIRS:
REPO_DISALLOWED_FILES: TODO.md,todo.md REPO_DISALLOWED_FILES: TODO.md,todo.md
@@ -64,10 +61,10 @@ env:
# Extended checks toggles # Extended checks toggles
EXTENDED_CHECKS: "true" EXTENDED_CHECKS: "true"
# File / directory variables (moved to top-level env) # File / directory variables
DOCS_INDEX: docs/docs-index.md DOCS_INDEX: docs/docs-index.md
SCRIPT_DIR: scripts SCRIPT_DIR: scripts
WORKFLOWS_DIR: .github/workflows WORKFLOWS_DIR: .gitea/workflows
SHELLCHECK_PATTERN: '*.sh' SHELLCHECK_PATTERN: '*.sh'
SPDX_FILE_GLOBS: '*.sh,*.php,*.js,*.ts,*.css,*.xml,*.yml,*.yaml' SPDX_FILE_GLOBS: '*.sh,*.php,*.js,*.ts,*.css,*.xml,*.yml,*.yaml'
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
@@ -87,62 +84,56 @@ jobs:
steps: steps:
- name: Check actor permission (admin only) - name: Check actor permission (admin only)
id: perm id: perm
uses: actions/github-script@v7 env:
with: TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
github-token: ${{ secrets.GH_TOKEN }} REPO: ${{ github.repository }}
script: | ACTOR: ${{ github.actor }}
const actor = context.actor; run: |
let permission = "unknown"; set -euo pipefail
let allowed = false; ALLOWED=false
let method = ""; PERMISSION=unknown
METHOD=""
// Hardcoded authorized users — always allowed # Hardcoded authorized users — always allowed
const authorizedUsers = ["jmiller-moko", "github-actions[bot]"]; case "$ACTOR" in
if (authorizedUsers.includes(actor)) { jmiller|gitea-actions[bot])
allowed = true; ALLOWED=true
permission = "admin"; PERMISSION=admin
method = "hardcoded allowlist"; METHOD="hardcoded allowlist"
} else { ;;
// Check via API for other actors *)
try { # Detect platform and check permissions via API
const res = await github.rest.repos.getCollaboratorPermissionLevel({ API_BASE="${GITHUB_API_URL:-${GITEA_API_URL:-https://api.github.com}}"
owner: context.repo.owner, RESP=$(curl -sf -H "Authorization: token ${TOKEN}" \
repo: context.repo.repo, "${API_BASE}/repos/${REPO}/collaborators/${ACTOR}/permission" 2>/dev/null || echo '{}')
username: actor, PERMISSION=$(echo "$RESP" | grep -oP '"permission"\s*:\s*"\K[^"]+' || echo "unknown")
}); if [ "$PERMISSION" = "admin" ] || [ "$PERMISSION" = "maintain" ] || [ "$PERMISSION" = "owner" ]; then
permission = (res?.data?.permission || "unknown").toLowerCase(); ALLOWED=true
allowed = permission === "admin" || permission === "maintain"; fi
method = "repo collaborator API"; METHOD="collaborator API"
} catch (error) { ;;
core.warning(`Could not fetch permissions for '${actor}': ${error.message}`); esac
permission = "unknown";
allowed = false;
method = "API error";
}
}
core.setOutput("permission", permission); echo "permission=${PERMISSION}" >> "$GITHUB_OUTPUT"
core.setOutput("allowed", allowed ? "true" : "false"); echo "allowed=${ALLOWED}" >> "$GITHUB_OUTPUT"
const lines = [ {
"## 🔐 Access Authorization", echo "## Access Authorization"
"", echo ""
"| Field | Value |", echo "| Field | Value |"
"|-------|-------|", echo "|-------|-------|"
`| **Actor** | \`${actor}\` |`, echo "| **Actor** | \`${ACTOR}\` |"
`| **Repository** | \`${context.repo.owner}/${context.repo.repo}\` |`, echo "| **Repository** | \`${REPO}\` |"
`| **Permission** | \`${permission}\` |`, echo "| **Permission** | \`${PERMISSION}\` |"
`| **Method** | ${method} |`, echo "| **Method** | ${METHOD} |"
`| **Authorized** | ${allowed} |`, echo "| **Authorized** | ${ALLOWED} |"
`| **Trigger** | \`${context.eventName}\` |`, echo ""
`| **Branch** | \`${context.ref.replace('refs/heads/', '')}\` |`, if [ "$ALLOWED" = "true" ]; then
"", echo "${ACTOR} authorized (${METHOD})"
allowed else
? `✅ ${actor} authorized (${method})` echo "${ACTOR} is NOT authorized. Requires admin or maintain role."
: `❌ ${actor} is NOT authorized. Requires admin or maintain role, or be in the hardcoded allowlist.`, fi
]; } >> "${GITHUB_STEP_SUMMARY}"
await core.summary.addRaw(lines.join("\n")).write();
- name: Deny execution when not permitted - name: Deny execution when not permitted
if: ${{ steps.perm.outputs.allowed != 'true' }} if: ${{ steps.perm.outputs.allowed != 'true' }}
@@ -427,7 +418,6 @@ jobs:
fi fi
done done
# Optional entries: handle files and directories (trailing slash indicates dir)
for f in "${optional_files[@]}"; do for f in "${optional_files[@]}"; do
if printf '%s' "${f}" | grep -q '/$'; then if printf '%s' "${f}" | grep -q '/$'; then
d="${f%/}" d="${f%/}"
@@ -451,8 +441,6 @@ jobs:
dev_paths=() dev_paths=()
dev_branches=() 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 while IFS= read -r b; do
name="${b#origin/}" name="${b#origin/}"
if [ "${name}" = 'dev' ]; then if [ "${name}" = 'dev' ]; then
@@ -462,12 +450,10 @@ jobs:
fi fi
done < <(git branch -r --list 'origin/dev*' | sed 's/^ *//') done < <(git branch -r --list 'origin/dev*' | sed 's/^ *//')
# If there are no dev/* branches, fail the guardrail.
if [ "${#dev_paths[@]}" -eq 0 ]; then if [ "${#dev_paths[@]}" -eq 0 ]; then
missing_required+=("dev/* branch (e.g. dev/01.00.00)") missing_required+=("dev/* branch (e.g. dev/01.00.00)")
fi fi
# If a plain dev branch exists (origin/dev), flag it as invalid.
if [ "${#dev_branches[@]}" -gt 0 ]; then if [ "${#dev_branches[@]}" -gt 0 ]; then
missing_required+=("invalid branch dev (must be dev/<version>)") missing_required+=("invalid branch dev (must be dev/<version>)")
fi fi
@@ -559,48 +545,39 @@ jobs:
} >> "${GITHUB_STEP_SUMMARY}" } >> "${GITHUB_STEP_SUMMARY}"
fi fi
# ── Joomla-specific checks ─────────────────────────────────────── # -- Joomla-specific checks --
joomla_findings=() 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)" MANIFEST="$(find . -maxdepth 2 -name '*.xml' -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true)"
if [ -z "${MANIFEST}" ]; then if [ -z "${MANIFEST}" ]; then
joomla_findings+=("Joomla XML manifest not found (no *.xml with <extension> tag)") joomla_findings+=("Joomla XML manifest not found (no *.xml with <extension> tag)")
else else
# Check <version> tag exists
if ! grep -qP '<version>' "${MANIFEST}"; then if ! grep -qP '<version>' "${MANIFEST}"; then
joomla_findings+=("XML manifest: <version> tag missing") joomla_findings+=("XML manifest: <version> tag missing")
fi fi
# Check extension type attribute
if ! grep -qP 'type="(component|module|plugin|library|package|template|language)"' "${MANIFEST}"; then if ! grep -qP 'type="(component|module|plugin|library|package|template|language)"' "${MANIFEST}"; then
joomla_findings+=("XML manifest: type attribute missing or invalid") joomla_findings+=("XML manifest: type attribute missing or invalid")
fi fi
# Check <name> tag
if ! grep -qP '<name>' "${MANIFEST}"; then if ! grep -qP '<name>' "${MANIFEST}"; then
joomla_findings+=("XML manifest: <name> tag missing") joomla_findings+=("XML manifest: <name> tag missing")
fi fi
# Check <author> tag
if ! grep -qP '<author>' "${MANIFEST}"; then if ! grep -qP '<author>' "${MANIFEST}"; then
joomla_findings+=("XML manifest: <author> tag missing") joomla_findings+=("XML manifest: <author> tag missing")
fi fi
# Check <namespace> for Joomla 5+
if ! grep -qP '<namespace' "${MANIFEST}"; then if ! grep -qP '<namespace' "${MANIFEST}"; then
joomla_findings+=("XML manifest: <namespace> missing (required for Joomla 5+)") joomla_findings+=("XML manifest: <namespace> missing (required for Joomla 5+)")
fi fi
fi fi
# Language files: check for at least one .ini file
INI_COUNT="$(find . -name '*.ini' -type f 2>/dev/null | wc -l)" INI_COUNT="$(find . -name '*.ini' -type f 2>/dev/null | wc -l)"
if [ "${INI_COUNT}" -eq 0 ]; then if [ "${INI_COUNT}" -eq 0 ]; then
joomla_findings+=("No .ini language files found") joomla_findings+=("No .ini language files found")
fi fi
# updates.xml must exist in root (Joomla update server)
if [ ! -f 'updates.xml' ]; then if [ ! -f 'updates.xml' ]; then
joomla_findings+=("updates.xml missing in root (required for Joomla update server)") joomla_findings+=("updates.xml missing in root (required for Joomla update server)")
fi fi
# index.html files for directory listing protection
INDEX_DIRS=("${SOURCE_DIR}" "${SOURCE_DIR}/admin" "${SOURCE_DIR}/site") INDEX_DIRS=("${SOURCE_DIR}" "${SOURCE_DIR}/admin" "${SOURCE_DIR}/site")
for dir in "${INDEX_DIRS[@]}"; do for dir in "${INDEX_DIRS[@]}"; do
if [ -d "${dir}" ] && [ ! -f "${dir}/index.html" ]; then if [ -d "${dir}" ] && [ ! -f "${dir}/index.html" ]; then
@@ -630,14 +607,12 @@ jobs:
extended_findings=() extended_findings=()
if [ "${extended_enabled}" = 'true' ]; then if [ "${extended_enabled}" = 'true' ]; then
# CODEOWNERS presence
if [ -f '.github/CODEOWNERS' ] || [ -f 'CODEOWNERS' ] || [ -f 'docs/CODEOWNERS' ]; then if [ -f '.github/CODEOWNERS' ] || [ -f 'CODEOWNERS' ] || [ -f 'docs/CODEOWNERS' ]; then
: :
else else
extended_findings+=("CODEOWNERS not found (.github/CODEOWNERS preferred)") extended_findings+=("CODEOWNERS not found (.github/CODEOWNERS preferred)")
fi 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 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)" 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 if [ -n "${bad_refs}" ]; then
@@ -653,7 +628,6 @@ jobs:
fi fi
fi fi
# Docs index link integrity (docs/docs-index.md)
if [ -f "${DOCS_INDEX}" ]; then if [ -f "${DOCS_INDEX}" ]; then
missing_links="$(python3 - <<'PY' missing_links="$(python3 - <<'PY'
import os import os
@@ -697,7 +671,6 @@ jobs:
fi fi
fi fi
# ShellCheck advisory
if [ -d "${SCRIPT_DIR}" ]; then if [ -d "${SCRIPT_DIR}" ]; then
if ! command -v shellcheck >/dev/null 2>&1; then if ! command -v shellcheck >/dev/null 2>&1; then
sudo apt-get update -qq sudo apt-get update -qq
@@ -726,7 +699,6 @@ jobs:
fi fi
fi fi
# SPDX header advisory for common source types
spdx_missing=() spdx_missing=()
IFS=',' read -r -a spdx_globs <<< "${SPDX_FILE_GLOBS}" IFS=',' read -r -a spdx_globs <<< "${SPDX_FILE_GLOBS}"
spdx_args=() spdx_args=()
@@ -749,9 +721,8 @@ jobs:
} >> "${GITHUB_STEP_SUMMARY}" } >> "${GITHUB_STEP_SUMMARY}"
fi fi
# Git hygiene advisory: branches older than 180 days (remote)
stale_cutoff_days=180 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 if [ -n "${stale_branches}" ]; then
extended_findings+=("Stale remote branches detected (advisory)") extended_findings+=("Stale remote branches detected (advisory)")
{ {
+126
View File
@@ -0,0 +1,126 @@
---
name: Request for Comments (RFC)
about: Propose a significant change for community discussion
title: '[RFC] '
labels: 'rfc, discussion'
assignees: ''
---
## RFC Summary
One-paragraph summary of the proposal.
## Motivation
Why are we doing this? What use cases does it support? What is the expected outcome?
## Detailed Design
### Overview
Provide a detailed explanation of the proposed change.
### API Changes (if applicable)
```php
// Before
function oldApi($param1) { }
// After
function newApi($param1, $param2) { }
```
### User Experience Changes
Describe how users will interact with this change.
### Implementation Approach
High-level implementation strategy.
## Drawbacks
Why should we *not* do this?
## Alternatives
What other designs have been considered? What is the impact of not doing this?
### Alternative 1
- Description
- Trade-offs
### Alternative 2
- Description
- Trade-offs
## Adoption Strategy
How will existing users adopt this? Is this a breaking change?
### Migration Guide
```bash
# Steps to migrate
```
### Deprecation Timeline
- **Announcement**:
- **Deprecation**:
- **Removal**:
## Unresolved Questions
- Question 1
- Question 2
## Future Possibilities
What future work does this enable?
## Impact Assessment
### Performance
Expected performance impact.
### Security
Security considerations and implications.
### Compatibility
- **Backward Compatible**: [Yes / No]
- **Breaking Changes**: [List]
### Maintenance
Long-term maintenance considerations.
## Community Input
### Stakeholders
- [ ] Core team
- [ ] Module developers
- [ ] End users
- [ ] Enterprise customers
### Feedback Period
**Duration**: [e.g., 2 weeks]
**Deadline**: [date]
## Implementation Timeline
### Phase 1: Design
- [ ] RFC discussion
- [ ] Design finalization
- [ ] Approval
### Phase 2: Implementation
- [ ] Core implementation
- [ ] Tests
- [ ] Documentation
### Phase 3: Release
- [ ] Beta release
- [ ] Feedback collection
- [ ] Stable release
## Success Metrics
How will we measure success?
- Metric 1
- Metric 2
## References
- Related RFCs:
- External documentation:
- Prior art:
## Open Questions for Community
1. Question 1?
2. Question 2?
---
**Note**: This RFC is open for community discussion. Please provide feedback in the comments below.
+82
View File
@@ -0,0 +1,82 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Security
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /.gitea/workflows/security-audit.yml
# VERSION: 01.00.00
# BRIEF: Dependency vulnerability scanning for composer and npm packages
name: 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
+464
View File
@@ -0,0 +1,464 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Joomla
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
# PATH: /templates/workflows/joomla/update-server.yml.template
# VERSION: 04.06.00
# BRIEF: Update Joomla update server XML feed with stable/rc/dev entries
#
# Writes updates.xml with multiple <update> entries:
# - <tag>stable</tag> on push to main (from auto-release)
# - <tag>rc</tag> on push to rc/**
# - <tag>development</tag> on push to dev or dev/**
#
# Joomla filters by user's "Minimum Stability" setting.
name: Update Joomla Update Server XML Feed
on:
push:
branches:
- 'dev'
- 'dev/**'
- 'alpha/**'
- 'beta/**'
- 'rc/**'
paths:
- 'src/**'
- 'htdocs/**'
pull_request:
types: [closed]
branches:
- 'dev'
- 'dev/**'
- 'alpha/**'
- 'beta/**'
- 'rc/**'
paths:
- 'src/**'
- 'htdocs/**'
workflow_dispatch:
inputs:
stability:
description: 'Stability tag'
required: true
default: 'development'
type: choice
options:
- development
- alpha
- beta
- rc
- stable
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
permissions:
contents: write
jobs:
update-xml:
name: Update updates.xml
runs-on: release
if: >-
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || github.event_name == 'push'
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.GA_TOKEN }}
fetch-depth: 0
- name: Setup MokoStandards tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.GA_TOKEN }}"}}}'
run: |
if ! command -v composer &> /dev/null; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
fi
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
/tmp/mokostandards-api 2>/dev/null || true
if [ -d "/tmp/mokostandards-api" ] && [ -f "/tmp/mokostandards-api/composer.json" ]; then
cd /tmp/mokostandards-api && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
fi
- name: Generate updates.xml entry
id: update
run: |
BRANCH="${{ github.ref_name }}"
REPO="${{ github.repository }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
VERSION=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null || echo "0.0.0")
# Auto-bump patch on all branches (dev, alpha, beta, rc)
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
BUMPED=$(php /tmp/mokostandards-api/cli/version_bump.php --path . 2>/dev/null || true)
if [ -n "$BUMPED" ]; then
VERSION=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null || echo "$VERSION")
git add -A
git commit -m "chore(version): auto-bump patch ${VERSION} [skip ci]" \
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" 2>/dev/null || true
git push 2>/dev/null || true
fi
# Determine stability from branch or input
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
STABILITY="${{ inputs.stability }}"
elif [[ "$BRANCH" == rc/* ]]; then
STABILITY="rc"
elif [[ "$BRANCH" == beta/* ]]; then
STABILITY="beta"
elif [[ "$BRANCH" == alpha/* ]]; then
STABILITY="alpha"
elif [[ "$BRANCH" == dev/* ]] || [[ "$BRANCH" == "dev" ]]; then
STABILITY="development"
else
STABILITY="stable"
fi
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
# Parse manifest (portable — no grep -P)
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "./build/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
if [ -z "$MANIFEST" ]; then
echo "No Joomla manifest found — skipping"
exit 0
fi
# Extract fields using sed (works on all runners)
EXT_NAME=$(sed -n 's/.*<name>\([^<]*\)<\/name>.*/\1/p' "$MANIFEST" | head -1)
EXT_TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
EXT_ELEMENT=$(sed -n 's/.*<element>\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" | head -1)
EXT_CLIENT=$(sed -n 's/.*<extension[^>]*client="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
EXT_FOLDER=$(sed -n 's/.*<extension[^>]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
EXT_VERSION=$(sed -n 's/.*<version>\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1)
TARGET_PLATFORM=$(sed -n 's/.*\(<targetplatform[^/]*\/>\).*/\1/p' "$MANIFEST" | head -1)
PHP_MINIMUM=$(sed -n 's/.*<php_minimum>\([^<]*\)<\/php_minimum>.*/\1/p' "$MANIFEST" | head -1)
# Fallbacks
[ -z "$EXT_NAME" ] && EXT_NAME="${{ github.event.repository.name }}"
[ -z "$EXT_TYPE" ] && EXT_TYPE="component"
# Derive element if not in manifest: try XML filename, then repo name
if [ -z "$EXT_ELEMENT" ]; then
EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]')
case "$EXT_ELEMENT" in
templatedetails|manifest|*.xml) EXT_ELEMENT=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;;
esac
fi
# Use manifest version if README version is empty
[ "$VERSION" = "0.0.0" ] && [ -n "$EXT_VERSION" ] && VERSION="$EXT_VERSION"
[ -z "$TARGET_PLATFORM" ] && TARGET_PLATFORM=$(printf '<targetplatform name="joomla" version="((5.[0-9])|(6.[0-9]))" %s>' "/")
CLIENT_TAG=""
[ -n "$EXT_CLIENT" ] && CLIENT_TAG="<client>${EXT_CLIENT}</client>"
[ -z "$CLIENT_TAG" ] && ([ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]) && CLIENT_TAG="<client>site</client>"
FOLDER_TAG=""
[ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ] && FOLDER_TAG="<folder>${EXT_FOLDER}</folder>"
PHP_TAG=""
[ -n "$PHP_MINIMUM" ] && PHP_TAG="<php_minimum>${PHP_MINIMUM}</php_minimum>"
# Version suffix for non-stable
DISPLAY_VERSION="$VERSION"
case "$STABILITY" in
development) DISPLAY_VERSION="${VERSION}-dev" ;;
alpha) DISPLAY_VERSION="${VERSION}-alpha" ;;
beta) DISPLAY_VERSION="${VERSION}-beta" ;;
rc) DISPLAY_VERSION="${VERSION}-rc" ;;
esac
MAJOR=$(echo "$VERSION" | awk -F. '{print $1}')
# Each stability level has its own release tag
case "$STABILITY" in
development) RELEASE_TAG="development" ;;
alpha) RELEASE_TAG="alpha" ;;
beta) RELEASE_TAG="beta" ;;
rc) RELEASE_TAG="release-candidate" ;;
*) RELEASE_TAG="v${MAJOR}" ;;
esac
PACKAGE_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.zip"
DOWNLOAD_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${PACKAGE_NAME}"
INFO_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}"
# -- Build install packages (ZIP + tar.gz) --------------------
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
if [ -d "$SOURCE_DIR" ]; then
EXCLUDES=".ftpignore sftp-config* *.ppk *.pem *.key .env*"
TAR_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.tar.gz"
cd "$SOURCE_DIR"
zip -r "/tmp/${PACKAGE_NAME}" . -x $EXCLUDES
cd ..
tar -czf "/tmp/${TAR_NAME}" -C "$SOURCE_DIR" \
--exclude='.ftpignore' --exclude='sftp-config*' \
--exclude='*.ppk' --exclude='*.pem' --exclude='*.key' --exclude='.env*' .
SHA256=$(sha256sum "/tmp/${PACKAGE_NAME}" | cut -d' ' -f1)
# Ensure release exists on Gitea
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
# Create release
RELEASE_JSON=$(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_TAG} (${DISPLAY_VERSION})',
'body': '${STABILITY} release',
'prerelease': True,
'target_commitish': 'main'
}))")" 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)
fi
if [ -n "$RELEASE_ID" ]; then
# 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_FILE in "$PACKAGE_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_FILE}':
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 formats
curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
-H "Content-Type: application/octet-stream" \
--data-binary @"/tmp/${PACKAGE_NAME}" \
"${API_BASE}/releases/${RELEASE_ID}/assets?name=${PACKAGE_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
fi
echo "Packages: ${PACKAGE_NAME} + ${TAR_NAME} (SHA: ${SHA256})" >> $GITHUB_STEP_SUMMARY
else
SHA256=""
fi
# -- Build the new entry (canonical format matching release.yml) --
NEW_ENTRY=""
NEW_ENTRY="${NEW_ENTRY} <update>\n"
NEW_ENTRY="${NEW_ENTRY} <name>${EXT_NAME}</name>\n"
NEW_ENTRY="${NEW_ENTRY} <description>${EXT_NAME} ${STABILITY} build.</description>\n"
NEW_ENTRY="${NEW_ENTRY} <element>${EXT_ELEMENT}</element>\n"
NEW_ENTRY="${NEW_ENTRY} <type>${EXT_TYPE}</type>\n"
[ -n "$CLIENT_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${CLIENT_TAG}\n"
[ -n "$FOLDER_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${FOLDER_TAG}\n"
NEW_ENTRY="${NEW_ENTRY} <version>${VERSION}</version>\n"
NEW_ENTRY="${NEW_ENTRY} <creationDate>$(date +%Y-%m-%d)</creationDate>\n"
NEW_ENTRY="${NEW_ENTRY} <infourl title='${EXT_NAME}'>https://git.mokoconsulting.tech/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${RELEASE_TAG}</infourl>\n"
NEW_ENTRY="${NEW_ENTRY} <downloads>\n"
NEW_ENTRY="${NEW_ENTRY} <downloadurl type='full' format='zip'>${DOWNLOAD_URL}</downloadurl>\n"
NEW_ENTRY="${NEW_ENTRY} </downloads>\n"
[ -n "$SHA256" ] && NEW_ENTRY="${NEW_ENTRY} <sha256>${SHA256}</sha256>\n"
NEW_ENTRY="${NEW_ENTRY} <tags><tag>${STABILITY}</tag></tags>\n"
NEW_ENTRY="${NEW_ENTRY} <maintainer>Moko Consulting</maintainer>\n"
NEW_ENTRY="${NEW_ENTRY} <maintainerurl>https://mokoconsulting.tech</maintainerurl>\n"
NEW_ENTRY="${NEW_ENTRY} <targetplatform name='joomla' version='(5|6).*'/>\n"
[ -n "$PHP_MINIMUM" ] && NEW_ENTRY="${NEW_ENTRY} <php_minimum>${PHP_MINIMUM}</php_minimum>\n"
NEW_ENTRY="${NEW_ENTRY} </update>"
# -- Write new entry to temp file --------------------------------
printf '%b' "$NEW_ENTRY" > /tmp/new_entry.xml
# -- Merge into updates.xml ----------------------------------------
# Cascade: stable→all | rc→rc+lower | beta→beta+lower | alpha→alpha+dev | dev→dev
CASCADE_MAP="stable:development,alpha,beta,rc,stable rc:development,alpha,beta,rc beta:development,alpha,beta alpha:development,alpha development:development"
TARGETS=""
for entry in $CASCADE_MAP; do
key="${entry%%:*}"
vals="${entry#*:}"
if [ "$key" = "${STABILITY}" ]; then
TARGETS="$vals"
break
fi
done
[ -z "$TARGETS" ] && TARGETS="${STABILITY}"
echo "Cascade: ${STABILITY} → ${TARGETS}"
# Create updates.xml if missing
if [ ! -f "updates.xml" ]; then
printf '%s\n' "<?xml version='1.0' encoding='UTF-8'?>" > updates.xml
printf '%s\n' "<!-- Copyright (C) $(date +%Y) Moko Consulting -->" >> updates.xml
printf '%s\n' "<updates>" >> updates.xml
printf '%s\n' "</updates>" >> updates.xml
fi
# Update existing blocks or create missing ones
export PY_TARGETS="$TARGETS" PY_VERSION="$VERSION" PY_DATE="$(date +%Y-%m-%d)"
python3 << 'PYEOF'
import re, os
targets = os.environ["PY_TARGETS"].split(",")
version = os.environ["PY_VERSION"]
date = os.environ["PY_DATE"]
with open("updates.xml") as f:
content = f.read()
with open("/tmp/new_entry.xml") as f:
new_entry_template = f.read()
for tag in targets:
tag = tag.strip()
# Build entry with this tag's name
new_entry = re.sub(r"<tag>[^<]*</tag>", f"<tag>{tag}</tag>", new_entry_template)
# Try to find existing block (handles both single-line and multi-line <tags>)
block_pattern = r"(<update>(?:(?!</update>).)*?<tag>" + re.escape(tag) + r"</tag>.*?</update>)"
match = re.search(block_pattern, content, re.DOTALL)
if match:
# Update in place — replace entire block
content = content.replace(match.group(1), new_entry.strip())
print(f" UPDATED: <tag>{tag}</tag> → {version}")
else:
# Create — insert before </updates>
content = content.replace("</updates>", "\n" + new_entry.strip() + "\n\n</updates>")
print(f" CREATED: <tag>{tag}</tag> → {version}")
# Clean up excessive blank lines
content = re.sub(r"\n{3,}", "\n\n", content)
with open("updates.xml", "w") as f:
f.write(content)
PYEOF
# Commit
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git add updates.xml
git diff --cached --quiet || {
git commit -m "chore: update updates.xml (${STABILITY}: ${DISPLAY_VERSION}) [skip ci]" \
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
git push
}
# -- Sync updates.xml to main (for non-main branches) ----------------------
- name: Sync updates.xml to main
if: github.ref_name != 'main'
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
GA_TOKEN="${{ secrets.GA_TOKEN }}"
FILE_SHA=$(curl -sf -H "Authorization: token ${GA_TOKEN}" \
"${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then
CONTENT=$(base64 -w0 updates.xml)
curl -sf -X PUT -H "Authorization: token ${GA_TOKEN}" \
-H "Content-Type: application/json" \
"${API_BASE}/contents/updates.xml" \
-d "$(python3 -c "import json; print(json.dumps({
'content': '${CONTENT}',
'sha': '${FILE_SHA}',
'message': 'chore: sync updates.xml from ${STABILITY} [skip ci]',
'branch': 'main'
}))")" > /dev/null 2>&1 \
&& echo "updates.xml synced to main (${STABILITY})" >> $GITHUB_STEP_SUMMARY \
|| echo "WARNING: failed to sync updates.xml to main" >> $GITHUB_STEP_SUMMARY
else
echo "WARNING: could not get updates.xml SHA from main" >> $GITHUB_STEP_SUMMARY
fi
- name: SFTP deploy to dev server
if: contains(github.ref, 'dev/') || github.ref == 'refs/heads/dev'
env:
DEV_HOST: ${{ vars.DEV_FTP_HOST }}
DEV_PATH: ${{ vars.DEV_FTP_PATH }}
DEV_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
DEV_USER: ${{ vars.DEV_FTP_USERNAME }}
DEV_PORT: ${{ vars.DEV_FTP_PORT }}
DEV_KEY: ${{ secrets.DEV_FTP_KEY }}
DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
run: |
# -- Permission check: admin or maintain role required --------
ACTOR="${{ github.actor }}"
REPO="${{ github.repository }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
"${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \
python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read")
case "$PERMISSION" in
admin|maintain|write) ;;
*)
echo "Deploy denied: ${ACTOR} has '${PERMISSION}' — requires admin, maintain, or write"
exit 0
;;
esac
[ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured — skipping SFTP"; exit 0; }
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
[ ! -d "$SOURCE_DIR" ] && exit 0
PORT="${DEV_PORT:-22}"
REMOTE="${DEV_PATH%/}"
[ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}"
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
"$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json
if [ -n "$DEV_KEY" ]; then
echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
else
printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json
fi
PLATFORM=$(php /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 --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
elif [ -f "/tmp/mokostandards-api/deploy/deploy-sftp.php" ]; then
php /tmp/mokostandards-api/deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
fi
rm -f /tmp/deploy_key /tmp/sftp-config.json
echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY
- name: Summary
if: always()
run: |
echo "## Joomla Update Server" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Version | \`${DISPLAY_VERSION}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Element | \`${EXT_ELEMENT}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Download | [ZIP](${DOWNLOAD_URL}) |" >> $GITHUB_STEP_SUMMARY
+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: MokoStandards.Maintenance
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
# 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
+450
View File
@@ -0,0 +1,450 @@
# 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: Gitea.Workflow.Template
# INGROUP: MokoStandards.CI
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
# PATH: /templates/workflows/joomla/ci-joomla.yml.template
# VERSION: 04.06.00
# BRIEF: CI workflow for Joomla extensions — lint, validate, test
name: "Joomla: Extension CI"
on:
pull_request:
branches:
- main
- 'dev/**'
workflow_dispatch:
permissions:
contents: read
pull-requests: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
lint-and-validate:
name: Lint & Validate
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup PHP
run: |
php -v && composer --version
- name: Clone MokoStandards
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' }}
run: |
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
/tmp/mokostandards-api
- name: Install dependencies
env:
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
run: |
if [ -f "composer.json" ]; then
composer install \
--no-interaction \
--prefer-dist \
--optimize-autoloader
else
echo "No composer.json found — skipping dependency install"
fi
- name: PHP syntax check
run: |
ERRORS=0
for DIR in src/ htdocs/; do
if [ -d "$DIR" ]; then
FOUND=1
while IFS= read -r -d '' FILE; do
OUTPUT=$(php -l "$FILE" 2>&1)
if echo "$OUTPUT" | grep -q "Parse error"; then
echo "::error file=${FILE}::${OUTPUT}"
ERRORS=$((ERRORS + 1))
fi
done < <(find "$DIR" -name "*.php" -print0)
fi
done
echo "### PHP Syntax Check" >> $GITHUB_STEP_SUMMARY
if [ "${ERRORS}" -gt 0 ]; then
echo "**${ERRORS} syntax error(s) found.**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "All PHP files passed syntax check." >> $GITHUB_STEP_SUMMARY
fi
- name: XML manifest validation
run: |
echo "### XML Manifest Validation" >> $GITHUB_STEP_SUMMARY
ERRORS=0
# Find the extension manifest (XML with <extension tag)
MANIFEST=""
for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
if grep -q "<extension" "$XML_FILE" 2>/dev/null; then
MANIFEST="$XML_FILE"
break
fi
done
if [ -z "$MANIFEST" ]; then
echo "No Joomla extension manifest found (XML file with \`<extension\` tag)." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "Manifest found: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY
# Validate well-formed XML
php -r "
\$xml = @simplexml_load_file('$MANIFEST');
if (\$xml === false) {
echo 'INVALID';
exit(1);
}
echo 'VALID';
" > /tmp/xml_result 2>&1
XML_RESULT=$(cat /tmp/xml_result)
if [ "$XML_RESULT" != "VALID" ]; then
echo "Manifest is not well-formed XML." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "Manifest is well-formed XML." >> $GITHUB_STEP_SUMMARY
fi
# Check required tags: name, version, author, namespace (Joomla 5+)
for TAG in name version author namespace; do
if ! grep -q "<${TAG}>" "$MANIFEST" 2>/dev/null; then
echo "Missing required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "Found required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY
fi
done
fi
if [ "${ERRORS}" -gt 0 ]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "**${ERRORS} manifest issue(s) found.**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Manifest validation passed.**" >> $GITHUB_STEP_SUMMARY
fi
- name: Check language files referenced in manifest
run: |
echo "### Language File Check" >> $GITHUB_STEP_SUMMARY
ERRORS=0
MANIFEST=""
for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
if grep -q "<extension" "$XML_FILE" 2>/dev/null; then
MANIFEST="$XML_FILE"
break
fi
done
if [ -n "$MANIFEST" ]; then
# Extract language file references from manifest
LANG_FILES=$(grep -oP 'language\s+tag="[^"]*"[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null || true)
if [ -z "$LANG_FILES" ]; then
echo "No language file references found in manifest — skipping." >> $GITHUB_STEP_SUMMARY
else
while IFS= read -r LANG_FILE; do
LANG_FILE=$(echo "$LANG_FILE" | xargs)
if [ -z "$LANG_FILE" ]; then
continue
fi
# Check in common locations
FOUND=0
for BASE in "." "src" "htdocs"; do
if [ -f "${BASE}/${LANG_FILE}" ]; then
FOUND=1
break
fi
done
if [ "$FOUND" -eq 0 ]; then
echo "Missing language file: \`${LANG_FILE}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "Language file present: \`${LANG_FILE}\`" >> $GITHUB_STEP_SUMMARY
fi
done <<< "$LANG_FILES"
fi
else
echo "No manifest found — skipping language check." >> $GITHUB_STEP_SUMMARY
fi
if [ "${ERRORS}" -gt 0 ]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "**${ERRORS} missing language file(s).**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Language file check passed.**" >> $GITHUB_STEP_SUMMARY
fi
- name: Check index.html files in directories
run: |
echo "### Index.html Check" >> $GITHUB_STEP_SUMMARY
MISSING=0
CHECKED=0
for DIR in src/ htdocs/; do
if [ -d "$DIR" ]; then
while IFS= read -r -d '' SUBDIR; do
CHECKED=$((CHECKED + 1))
if [ ! -f "${SUBDIR}/index.html" ]; then
echo "Missing index.html in: \`${SUBDIR}\`" >> $GITHUB_STEP_SUMMARY
MISSING=$((MISSING + 1))
fi
done < <(find "$DIR" -type d -print0)
fi
done
if [ "${CHECKED}" -eq 0 ]; then
echo "No src/ or htdocs/ directories found — skipping." >> $GITHUB_STEP_SUMMARY
elif [ "${MISSING}" -gt 0 ]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "**${MISSING} director(ies) missing index.html out of ${CHECKED} checked.**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "All ${CHECKED} directories contain index.html." >> $GITHUB_STEP_SUMMARY
fi
release-readiness:
name: Release Readiness Check
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' && github.base_ref == 'main'
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Validate release readiness
run: |
echo "## Release Readiness" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
ERRORS=0
# Extract version from README.md
README_VERSION=$(grep -oP '^\s*VERSION:\s*\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' README.md | head -1)
if [ -z "$README_VERSION" ]; then
echo "No VERSION found in README.md FILE INFORMATION block." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "README version: \`${README_VERSION}\`" >> $GITHUB_STEP_SUMMARY
fi
# Find the extension manifest
MANIFEST=""
for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
if grep -q "<extension" "$XML_FILE" 2>/dev/null; then
MANIFEST="$XML_FILE"
break
fi
done
if [ -z "$MANIFEST" ]; then
echo "No Joomla extension manifest found." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "Manifest: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY
# Check <version> matches README VERSION
MANIFEST_VERSION=$(grep -oP '<version>\K[^<]+' "$MANIFEST" | head -1)
if [ -z "$MANIFEST_VERSION" ]; then
echo "No \`<version>\` tag in manifest." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
elif [ -n "$README_VERSION" ] && [ "$MANIFEST_VERSION" != "$README_VERSION" ]; then
echo "Manifest version \`${MANIFEST_VERSION}\` does not match README \`${README_VERSION}\`." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "Manifest version: \`${MANIFEST_VERSION}\`" >> $GITHUB_STEP_SUMMARY
fi
# Check extension type, element, client attributes
EXT_TYPE=$(grep -oP '<extension[^>]*\btype="\K[^"]+' "$MANIFEST" | head -1)
if [ -z "$EXT_TYPE" ]; then
echo "Missing \`type\` attribute on \`<extension>\` tag." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "Extension type: \`${EXT_TYPE}\`" >> $GITHUB_STEP_SUMMARY
fi
# Element check (component/module/plugin name)
HAS_ELEMENT=$(grep -cP '<(element|name)>' "$MANIFEST" 2>/dev/null || echo "0")
if [ "$HAS_ELEMENT" -eq 0 ]; then
echo "Missing \`<element>\` or \`<name>\` in manifest." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
# Client attribute for site/admin modules and plugins
if echo "$EXT_TYPE" | grep -qP "^(module|plugin)$"; then
HAS_CLIENT=$(grep -cP '<extension[^>]*\bclient=' "$MANIFEST" 2>/dev/null || echo "0")
if [ "$HAS_CLIENT" -eq 0 ]; then
echo "Missing \`client\` attribute for ${EXT_TYPE} extension." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
fi
fi
# Check updates.xml exists
if [ -f "updates.xml" ] || [ -f "updates.xml" ]; then
echo "Update XML present." >> $GITHUB_STEP_SUMMARY
else
echo "No updates.xml found." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
# Check CHANGELOG.md exists
if [ -f "CHANGELOG.md" ]; then
echo "CHANGELOG.md present." >> $GITHUB_STEP_SUMMARY
else
echo "No CHANGELOG.md found." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
echo "" >> $GITHUB_STEP_SUMMARY
if [ $ERRORS -gt 0 ]; then
echo "**${ERRORS} issue(s) must be resolved before release.**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "**Extension is ready for release.**" >> $GITHUB_STEP_SUMMARY
fi
test:
name: Tests (PHP ${{ matrix.php }})
runs-on: ubuntu-latest
needs: lint-and-validate
strategy:
fail-fast: false
matrix:
php: ['8.2', '8.3']
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup PHP ${{ matrix.php }}
run: |
php -v && composer --version
- name: Install dependencies
env:
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
run: |
if [ -f "composer.json" ]; then
composer install \
--no-interaction \
--prefer-dist \
--optimize-autoloader
else
echo "No composer.json found — skipping dependency install"
fi
- name: Run tests
run: |
echo "### Test Results (PHP ${{ matrix.php }})" >> $GITHUB_STEP_SUMMARY
if [ -f "phpunit.xml" ] || [ -f "phpunit.xml.dist" ]; then
vendor/bin/phpunit --testdox 2>&1 | tee /tmp/test-output.log
EXIT=${PIPESTATUS[0]}
if [ $EXIT -eq 0 ]; then
echo "All tests passed." >> $GITHUB_STEP_SUMMARY
else
echo "Test failures detected — see log." >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
cat /tmp/test-output.log >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
fi
exit $EXIT
else
echo "No phpunit.xml found — skipping tests." >> $GITHUB_STEP_SUMMARY
fi
static-analysis:
name: PHPStan Analysis
runs-on: ubuntu-latest
needs: lint-and-validate
continue-on-error: true
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup PHP
run: php -v && composer --version
- name: Install dependencies
env:
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
run: |
if [ -f "composer.json" ]; then
composer install --no-interaction --prefer-dist --optimize-autoloader
fi
- name: Install PHPStan
run: |
if ! command -v vendor/bin/phpstan &> /dev/null; then
composer require --dev phpstan/phpstan --no-interaction 2>/dev/null || \
composer global require phpstan/phpstan --no-interaction
fi
- name: Run PHPStan
run: |
echo "### PHPStan Static Analysis" >> $GITHUB_STEP_SUMMARY
PHPSTAN="vendor/bin/phpstan"
if [ ! -f "$PHPSTAN" ]; then
PHPSTAN=$(composer global config bin-dir --absolute 2>/dev/null)/phpstan
fi
# Determine source directory
SRC_DIR=""
for DIR in src/ htdocs/ lib/; do
if [ -d "$DIR" ]; then
SRC_DIR="$DIR"
break
fi
done
if [ -z "$SRC_DIR" ]; then
echo "No source directory found (src/, htdocs/, lib/) — skipping." >> $GITHUB_STEP_SUMMARY
exit 0
fi
# Use repo phpstan.neon if present, otherwise use baseline config
ARGS="analyse ${SRC_DIR} --memory-limit=512M --no-progress --error-format=table"
if [ -f "phpstan.neon" ] || [ -f "phpstan.neon.dist" ]; then
echo "Using project PHPStan config." >> $GITHUB_STEP_SUMMARY
else
ARGS="$ARGS --level=3"
echo "No phpstan.neon found — using level 3 (type inference)." >> $GITHUB_STEP_SUMMARY
fi
$PHPSTAN $ARGS 2>&1 | tee /tmp/phpstan-output.txt
EXIT=${PIPESTATUS[0]}
if [ $EXIT -eq 0 ]; then
echo "**No errors found.**" >> $GITHUB_STEP_SUMMARY
else
ERRORS=$(grep -c "ERROR" /tmp/phpstan-output.txt 2>/dev/null || echo "some")
echo "**${ERRORS} error(s) found.** Review output above." >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
tail -30 /tmp/phpstan-output.txt >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
fi
exit $EXIT
+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: MokoStandards.Maintenance
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# 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)"
+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: MokoStandards.Security
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
# 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
+71
View File
@@ -0,0 +1,71 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Notifications
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# 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"
- "Cascade Main → Dev"
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}"
+224
View File
@@ -0,0 +1,224 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.CI
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
# 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: |
# Parse manifest for platform detection
PLATFORM=$(php /tmp/mokostandards-api/cli/manifest_read.php --path . --field platform 2>/dev/null)
[ -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; }
# ── Changelog Gate ────────────────────────────────────────────────────
changelog:
name: Changelog Updated
runs-on: ubuntu-latest
if: github.base_ref == 'main'
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check CHANGELOG.md was updated
run: |
BASE="${{ github.event.pull_request.base.sha }}"
HEAD="${{ github.event.pull_request.head.sha }}"
if git diff --name-only "$BASE" "$HEAD" | grep -q "^CHANGELOG.md$"; then
echo "CHANGELOG.md updated"
else
# Allow [skip changelog] in PR title or body
PR_TITLE="${{ github.event.pull_request.title }}"
PR_BODY="${{ github.event.pull_request.body }}"
if echo "$PR_TITLE $PR_BODY" | grep -qi "\[skip changelog\]"; then
echo "::warning::Changelog skip requested via [skip changelog]"
exit 0
fi
echo "::error::CHANGELOG.md must be updated before merging to main. Add [skip changelog] to the PR title to bypass."
exit 1
fi
+260
View File
@@ -0,0 +1,260 @@
# 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
# Read current version from manifest (priority) or README — no bump yet
VERSION=$(php ${MOKO_API}/version_read.php --path .)
echo "Version: ${VERSION}"
# Ensure platform-specific manifest matches
php ${MOKO_API}/version_set_platform.php --path . --version "${VERSION}"
# Git setup for later commits
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"
# Detect element from Joomla/Dolibarr manifest
set +o pipefail
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 4 -name "*.xml" ! -path "./.git/*" -print0 2>/dev/null | xargs -0 grep -l '<extension' 2>/dev/null | head -1 || true)
if [ -n "$MANIFEST" ]; then
ELEM=$(grep -oP "<element>\K[^<]+" "$MANIFEST" 2>/dev/null | head -1 || true)
[ -n "$ELEM" ] && EXT_ELEMENT="$ELEM"
fi
fi
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
set -o pipefail
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
- name: "Post-release version bump"
run: |
MOKO_API="/tmp/moko-platform-api/cli"
VERSION="${{ steps.meta.outputs.version }}"
# Bump patch for next dev cycle
BUMP_OUTPUT=$(php ${MOKO_API}/version_bump.php --path .)
NEXT=$(echo "$BUMP_OUTPUT" | grep -oP '\d{2}\.\d{2}\.\d{2}$' || true)
[ -z "$NEXT" ] && exit 0
# Update platform-specific manifest to next version
php ${MOKO_API}/version_set_platform.php --path . --version "${NEXT}"
git add -A
git diff --cached --quiet || {
git commit -m "chore: update development channel ${VERSION} [skip ci]"
git push origin HEAD 2>&1
}
+766
View File
@@ -0,0 +1,766 @@
# ============================================================================
# Copyright (C) 2025 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: Gitea.Workflow
# INGROUP: MokoStandards.Validation
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
# 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.
# ============================================================================
name: "Joomla: Repo Health"
concurrency:
group: repo-health-${{ github.repository }}-${{ github.ref }}
cancel-in-progress: true
defaults:
run:
shell: bash
on:
workflow_dispatch:
inputs:
profile:
description: 'Validation profile: all, release, scripts, or repo'
required: true
default: all
type: choice
options:
- all
- release
- scripts
- repo
pull_request:
push:
permissions:
contents: read
env:
# Release policy - Repository Variables Only
RELEASE_REQUIRED_REPO_VARS: RS_FTP_PATH_SUFFIX
RELEASE_OPTIONAL_REPO_VARS: DEV_FTP_SUFFIX
# Scripts governance policy
SCRIPTS_REQUIRED_DIRS:
SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate
# Repo health policy
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
# Extended checks toggles
EXTENDED_CHECKS: "true"
# File / directory variables
DOCS_INDEX: docs/docs-index.md
SCRIPT_DIR: scripts
WORKFLOWS_DIR: .gitea/workflows
SHELLCHECK_PATTERN: '*.sh'
SPDX_FILE_GLOBS: '*.sh,*.php,*.js,*.ts,*.css,*.xml,*.yml,*.yaml'
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
access_check:
name: Access control
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
outputs:
allowed: ${{ steps.perm.outputs.allowed }}
permission: ${{ steps.perm.outputs.permission }}
steps:
- name: Check actor permission (admin only)
id: perm
env:
TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
REPO: ${{ github.repository }}
ACTOR: ${{ github.actor }}
run: |
set -euo pipefail
ALLOWED=false
PERMISSION=unknown
METHOD=""
# Hardcoded authorized users — always allowed
case "$ACTOR" in
jmiller|gitea-actions[bot])
ALLOWED=true
PERMISSION=admin
METHOD="hardcoded allowlist"
;;
*)
# Detect platform and check permissions via API
API_BASE="${GITHUB_API_URL:-${GITEA_API_URL:-https://api.github.com}}"
RESP=$(curl -sf -H "Authorization: token ${TOKEN}" \
"${API_BASE}/repos/${REPO}/collaborators/${ACTOR}/permission" 2>/dev/null || echo '{}')
PERMISSION=$(echo "$RESP" | grep -oP '"permission"\s*:\s*"\K[^"]+' || echo "unknown")
if [ "$PERMISSION" = "admin" ] || [ "$PERMISSION" = "maintain" ] || [ "$PERMISSION" = "owner" ]; then
ALLOWED=true
fi
METHOD="collaborator API"
;;
esac
echo "permission=${PERMISSION}" >> "$GITHUB_OUTPUT"
echo "allowed=${ALLOWED}" >> "$GITHUB_OUTPUT"
{
echo "## Access Authorization"
echo ""
echo "| Field | Value |"
echo "|-------|-------|"
echo "| **Actor** | \`${ACTOR}\` |"
echo "| **Repository** | \`${REPO}\` |"
echo "| **Permission** | \`${PERMISSION}\` |"
echo "| **Method** | ${METHOD} |"
echo "| **Authorized** | ${ALLOWED} |"
echo ""
if [ "$ALLOWED" = "true" ]; then
echo "${ACTOR} authorized (${METHOD})"
else
echo "${ACTOR} is NOT authorized. Requires admin or maintain role."
fi
} >> "${GITHUB_STEP_SUMMARY}"
- name: Deny execution when not permitted
if: ${{ steps.perm.outputs.allowed != 'true' }}
run: |
set -euo pipefail
printf '%s\n' 'ERROR: Access denied. Admin permission required.' >> "${GITHUB_STEP_SUMMARY}"
exit 1
release_config:
name: Release configuration
needs: access_check
if: ${{ needs.access_check.outputs.allowed == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
- name: Guardrails release vars
env:
PROFILE_RAW: ${{ github.event.inputs.profile }}
RS_FTP_PATH_SUFFIX: ${{ vars.RS_FTP_PATH_SUFFIX }}
DEV_FTP_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
run: |
set -euo pipefail
profile="${PROFILE_RAW:-all}"
case "${profile}" in
all|release|scripts|repo) ;;
*)
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
exit 1
;;
esac
if [ "${profile}" = 'scripts' ] || [ "${profile}" = 'repo' ]; then
{
printf '%s\n' '### Release configuration (Repository Variables)'
printf '%s\n' "Profile: ${profile}"
printf '%s\n' 'Status: SKIPPED'
printf '%s\n' 'Reason: profile excludes release validation'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
exit 0
fi
IFS=',' read -r -a required <<< "${RELEASE_REQUIRED_REPO_VARS}"
IFS=',' read -r -a optional <<< "${RELEASE_OPTIONAL_REPO_VARS}"
missing=()
missing_optional=()
for k in "${required[@]}"; do
v="${!k:-}"
[ -z "${v}" ] && missing+=("${k}")
done
for k in "${optional[@]}"; do
v="${!k:-}"
[ -z "${v}" ] && missing_optional+=("${k}")
done
{
printf '%s\n' '### Release configuration (Repository Variables)'
printf '%s\n' "Profile: ${profile}"
printf '%s\n' '| Variable | Status |'
printf '%s\n' '|---|---|'
printf '%s\n' "| RS_FTP_PATH_SUFFIX | ${RS_FTP_PATH_SUFFIX:-NOT SET} |"
printf '%s\n' "| DEV_FTP_SUFFIX | ${DEV_FTP_SUFFIX:-NOT SET} |"
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
if [ "${#missing_optional[@]}" -gt 0 ]; then
{
printf '%s\n' '### Missing optional repository variables'
for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
if [ "${#missing[@]}" -gt 0 ]; then
{
printf '%s\n' '### Missing required repository variables'
for m in "${missing[@]}"; do printf '%s\n' "- ${m}"; done
printf '%s\n' 'ERROR: Guardrails failed. Missing required repository variables.'
} >> "${GITHUB_STEP_SUMMARY}"
exit 1
fi
{
printf '%s\n' '### Repository variables validation result'
printf '%s\n' 'Status: OK'
printf '%s\n' 'All required repository variables present.'
printf '%s\n' ''
printf '%s\n' '**Note**: Organization secrets (RS_FTP_HOST, RS_FTP_USER, etc.) are validated at deployment time, not in repository health checks.'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
scripts_governance:
name: Scripts governance
needs: access_check
if: ${{ needs.access_check.outputs.allowed == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
- name: Scripts folder checks
env:
PROFILE_RAW: ${{ github.event.inputs.profile }}
run: |
set -euo pipefail
profile="${PROFILE_RAW:-all}"
case "${profile}" in
all|release|scripts|repo) ;;
*)
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
exit 1
;;
esac
if [ "${profile}" = 'release' ] || [ "${profile}" = 'repo' ]; then
{
printf '%s\n' '### Scripts governance'
printf '%s\n' "Profile: ${profile}"
printf '%s\n' 'Status: SKIPPED'
printf '%s\n' 'Reason: profile excludes scripts governance'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
exit 0
fi
if [ ! -d "${SCRIPT_DIR}" ]; then
{
printf '%s\n' '### Scripts governance'
printf '%s\n' 'Status: OK (advisory)'
printf '%s\n' 'scripts/ directory not present. No scripts governance enforced.'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
exit 0
fi
IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}"
IFS=',' read -r -a allowed_dirs <<< "${SCRIPTS_ALLOWED_DIRS}"
missing_dirs=()
unapproved_dirs=()
for d in "${required_dirs[@]}"; do
req="${d%/}"
[ ! -d "${req}" ] && missing_dirs+=("${req}/")
done
while IFS= read -r d; do
allowed=false
for a in "${allowed_dirs[@]}"; do
a_norm="${a%/}"
[ "${d%/}" = "${a_norm}" ] && allowed=true
done
[ "${allowed}" = false ] && unapproved_dirs+=("${d%/}/")
done < <(find "${SCRIPT_DIR}" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sed 's#^\./##')
{
printf '%s\n' '### Scripts governance'
printf '%s\n' "Profile: ${profile}"
printf '%s\n' '| Area | Status | Notes |'
printf '%s\n' '|---|---|---|'
if [ "${#missing_dirs[@]}" -gt 0 ]; then
printf '%s\n' '| Required directories | Warning | Missing required subfolders |'
else
printf '%s\n' '| Required directories | OK | All required subfolders present |'
fi
if [ "${#unapproved_dirs[@]}" -gt 0 ]; then
printf '%s\n' '| Directory policy | Warning | Unapproved directories detected |'
else
printf '%s\n' '| Directory policy | OK | No unapproved directories |'
fi
printf '%s\n' '| Enforcement mode | Advisory | scripts folder is optional |'
printf '\n'
if [ "${#missing_dirs[@]}" -gt 0 ]; then
printf '%s\n' 'Missing required script directories:'
for m in "${missing_dirs[@]}"; do printf '%s\n' "- ${m}"; done
printf '\n'
else
printf '%s\n' 'Missing required script directories: none.'
printf '\n'
fi
if [ "${#unapproved_dirs[@]}" -gt 0 ]; then
printf '%s\n' 'Unapproved script directories detected:'
for m in "${unapproved_dirs[@]}"; do printf '%s\n' "- ${m}"; done
printf '\n'
else
printf '%s\n' 'Unapproved script directories detected: none.'
printf '\n'
fi
printf '%s\n' 'Scripts governance completed in advisory mode.'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
repo_health:
name: Repository health
needs: access_check
if: ${{ needs.access_check.outputs.allowed == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
- name: Repository health checks
env:
PROFILE_RAW: ${{ github.event.inputs.profile }}
run: |
set -euo pipefail
profile="${PROFILE_RAW:-all}"
case "${profile}" in
all|release|scripts|repo) ;;
*)
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
exit 1
;;
esac
if [ "${profile}" = 'release' ] || [ "${profile}" = 'scripts' ]; then
{
printf '%s\n' '### Repository health'
printf '%s\n' "Profile: ${profile}"
printf '%s\n' 'Status: SKIPPED'
printf '%s\n' 'Reason: profile excludes repository health'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
exit 0
fi
# Source directory: src/ or htdocs/ (either is valid)
if [ -d "src" ]; then
SOURCE_DIR="src"
elif [ -d "htdocs" ]; then
SOURCE_DIR="htdocs"
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%/}"
[ ! -d "${d}" ] && missing_required+=("${item}")
else
[ ! -f "${item}" ] && missing_required+=("${item}")
fi
done
for f in "${optional_files[@]}"; do
if printf '%s' "${f}" | grep -q '/$'; then
d="${f%/}"
[ ! -d "${d}" ] && missing_optional+=("${f}")
else
[ ! -f "${f}" ] && missing_optional+=("${f}")
fi
done
for d in "${disallowed_dirs[@]}"; do
d_norm="${d%/}"
[ -d "${d_norm}" ] && missing_required+=("${d_norm}/ (disallowed)")
done
for f in "${disallowed_files[@]}"; do
[ -f "${f}" ] && missing_required+=("${f} (disallowed)")
done
git fetch origin --prune
dev_paths=()
dev_branches=()
while IFS= read -r b; do
name="${b#origin/}"
if [ "${name}" = 'dev' ]; then
dev_branches+=("${name}")
else
dev_paths+=("${name}")
fi
done < <(git branch -r --list 'origin/dev*' | sed 's/^ *//')
if [ "${#dev_paths[@]}" -eq 0 ]; then
missing_required+=("dev/* branch (e.g. dev/01.00.00)")
fi
if [ "${#dev_branches[@]}" -gt 0 ]; then
missing_required+=("invalid branch dev (must be dev/<version>)")
fi
content_warnings=()
if [ -f 'CHANGELOG.md' ] && ! grep -Eq '^# Changelog' CHANGELOG.md; then
content_warnings+=("CHANGELOG.md missing '# Changelog' header")
fi
if [ -f 'CHANGELOG.md' ] && grep -Eq '^[# ]*Unreleased' CHANGELOG.md; then
content_warnings+=("CHANGELOG.md contains Unreleased section (review release readiness)")
fi
if [ -f 'LICENSE' ] && ! grep -qiE 'GNU GENERAL PUBLIC LICENSE|GPL' LICENSE; then
content_warnings+=("LICENSE does not look like a GPL text")
fi
if [ -f 'README.md' ] && ! grep -qiE 'moko|Moko' README.md; then
content_warnings+=("README.md missing expected brand keyword")
fi
export PROFILE_RAW="${profile}"
export MISSING_REQUIRED="$(printf '%s\n' "${missing_required[@]:-}")"
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
)"
{
printf '%s\n' '### Repository health'
printf '%s\n' "Profile: ${profile}"
printf '%s\n' '| Metric | Value |'
printf '%s\n' '|---|---|'
printf '%s\n' "| Missing required | ${#missing_required[@]} |"
printf '%s\n' "| Missing optional | ${#missing_optional[@]} |"
printf '%s\n' "| Content warnings | ${#content_warnings[@]} |"
printf '\n'
printf '%s\n' '### Guardrails report (JSON)'
printf '%s\n' '```json'
printf '%s\n' "${report_json}"
printf '%s\n' '```'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
if [ "${#missing_required[@]}" -gt 0 ]; then
{
printf '%s\n' '### Missing required repo artifacts'
for m in "${missing_required[@]}"; do printf '%s\n' "- ${m}"; done
printf '%s\n' 'ERROR: Guardrails failed. Missing required repository artifacts.'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
exit 1
fi
if [ "${#missing_optional[@]}" -gt 0 ]; then
{
printf '%s\n' '### Missing optional repo artifacts'
for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
if [ "${#content_warnings[@]}" -gt 0 ]; then
{
printf '%s\n' '### Repo content warnings'
for m in "${content_warnings[@]}"; do printf '%s\n' "- ${m}"; done
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
# -- Joomla-specific checks --
joomla_findings=()
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
if ! grep -qP '<version>' "${MANIFEST}"; then
joomla_findings+=("XML manifest: <version> tag missing")
fi
if ! grep -qP 'type="(component|module|plugin|library|package|template|language)"' "${MANIFEST}"; then
joomla_findings+=("XML manifest: type attribute missing or invalid")
fi
if ! grep -qP '<name>' "${MANIFEST}"; then
joomla_findings+=("XML manifest: <name> tag missing")
fi
if ! grep -qP '<author>' "${MANIFEST}"; then
joomla_findings+=("XML manifest: <author> tag missing")
fi
if ! grep -qP '<namespace' "${MANIFEST}"; then
joomla_findings+=("XML manifest: <namespace> missing (required for Joomla 5+)")
fi
fi
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
if [ ! -f 'updates.xml' ]; then
joomla_findings+=("updates.xml missing in root (required for Joomla update server)")
fi
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 [ "${#joomla_findings[@]}" -gt 0 ]; then
{
printf '%s\n' '### Joomla extension checks'
printf '%s\n' '| Check | Status |'
printf '%s\n' '|---|---|'
for f in "${joomla_findings[@]}"; do
printf '%s\n' "| ${f} | Warning |"
done
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
else
{
printf '%s\n' '### Joomla extension checks'
printf '%s\n' 'All Joomla-specific checks passed.'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
extended_enabled="${EXTENDED_CHECKS:-true}"
extended_findings=()
if [ "${extended_enabled}" = 'true' ]; then
if [ -f '.github/CODEOWNERS' ] || [ -f 'CODEOWNERS' ] || [ -f 'docs/CODEOWNERS' ]; then
:
else
extended_findings+=("CODEOWNERS not found (.github/CODEOWNERS preferred)")
fi
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
extended_findings+=("Workflows reference actions @main/@master (pin versions): see log excerpt")
{
printf '%s\n' '### Workflow pinning advisory'
printf '%s\n' 'Found uses: entries pinned to main/master:'
printf '%s\n' '```'
printf '%s\n' "${bad_refs}"
printf '%s\n' '```'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
fi
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
)"
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}"
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
fi
if [ -d "${SCRIPT_DIR}" ]; then
if ! command -v shellcheck >/dev/null 2>&1; then
sudo apt-get update -qq
sudo apt-get install -y shellcheck >/dev/null
fi
sc_out=''
while IFS= read -r shf; do
[ -z "${shf}" ] && continue
out_one="$(shellcheck -S warning -x "${shf}" 2>/dev/null || true)"
if [ -n "${out_one}" ]; then
sc_out="${sc_out}${out_one}\n"
fi
done < <(find "${SCRIPT_DIR}" -type f -name "${SHELLCHECK_PATTERN}" 2>/dev/null | sort)
if [ -n "${sc_out}" ]; then
extended_findings+=("ShellCheck warnings detected (advisory)")
sc_head="$(printf '%s' "${sc_out}" | head -n 200)"
{
printf '%s\n' '### ShellCheck (advisory)'
printf '%s\n' '```'
printf '%s\n' "${sc_head}"
printf '%s\n' '```'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
fi
spdx_missing=()
IFS=',' read -r -a spdx_globs <<< "${SPDX_FILE_GLOBS}"
spdx_args=()
for g in "${spdx_globs[@]}"; do spdx_args+=("${g}"); done
while IFS= read -r f; do
[ -z "${f}" ] && continue
if ! head -n 40 "${f}" | grep -q 'SPDX-License-Identifier:'; then
spdx_missing+=("${f}")
fi
done < <(git ls-files "${spdx_args[@]}" 2>/dev/null || true)
if [ "${#spdx_missing[@]}" -gt 0 ]; then
extended_findings+=("SPDX header missing in some tracked files (advisory)")
{
printf '%s\n' '### SPDX header advisory'
printf '%s\n' 'Files missing SPDX-License-Identifier (first 40 lines scan):'
for f in "${spdx_missing[@]}"; do printf '%s\n' "- ${f}"; done
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
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 > days*86400) print $1}' | head -50)"
if [ -n "${stale_branches}" ]; then
extended_findings+=("Stale remote branches detected (advisory)")
{
printf '%s\n' '### Git hygiene advisory'
printf '%s\n' "Branches with last commit older than ${stale_cutoff_days} days (sample up to 50):"
while IFS= read -r b; do [ -n "${b}" ] && printf '%s\n' "- ${b}"; done <<< "${stale_branches}"
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
fi
{
printf '%s\n' '### Guardrails coverage matrix'
printf '%s\n' '| Domain | Status | Notes |'
printf '%s\n' '|---|---|---|'
printf '%s\n' '| Access control | OK | Admin-only execution gate |'
printf '%s\n' '| Release variables | OK | Repository variables validation |'
printf '%s\n' '| Scripts governance | OK | Directory policy and advisory reporting |'
printf '%s\n' '| Repo required artifacts | OK | Required, optional, disallowed enforcement |'
printf '%s\n' '| Repo content heuristics | OK | Brand, license, changelog structure |'
if [ "${extended_enabled}" = 'true' ]; then
if [ "${#extended_findings[@]}" -gt 0 ]; then
printf '%s\n' '| Extended checks | Warning | See extended findings below |'
else
printf '%s\n' '| Extended checks | OK | No findings |'
fi
else
printf '%s\n' '| Extended checks | SKIPPED | EXTENDED_CHECKS disabled |'
fi
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
if [ "${extended_enabled}" = 'true' ] && [ "${#extended_findings[@]}" -gt 0 ]; then
{
printf '%s\n' '### Extended findings (advisory)'
for f in "${extended_findings[@]}"; do printf '%s\n' "- ${f}"; done
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
printf '%s\n' 'Repository health guardrails passed.' >> "${GITHUB_STEP_SUMMARY}"
+82
View File
@@ -0,0 +1,82 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Security
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /.gitea/workflows/security-audit.yml
# VERSION: 01.00.00
# BRIEF: Dependency vulnerability scanning for composer and npm packages
name: "Universal: Security Audit"
on:
schedule:
- cron: '0 6 * * 1' # Weekly on Monday at 06:00 UTC
pull_request:
branches:
- main
paths:
- 'composer.json'
- 'composer.lock'
- 'package.json'
- 'package-lock.json'
workflow_dispatch:
permissions:
contents: read
env:
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }}
jobs:
audit:
name: Dependency Audit
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Composer audit
if: hashFiles('composer.lock') != ''
run: |
echo "=== Composer Security Audit ==="
if ! command -v composer &> /dev/null; then
sudo apt-get update -qq
sudo apt-get install -y -qq php-cli composer >/dev/null 2>&1
fi
composer audit --format=plain 2>&1 | tee /tmp/composer-audit.txt
RESULT=$?
if [ $RESULT -ne 0 ]; then
echo "::warning::Composer vulnerabilities found"
echo "composer_vulnerable=true" >> "$GITHUB_ENV"
else
echo "No known vulnerabilities in composer dependencies"
fi
- name: NPM audit
if: hashFiles('package-lock.json') != ''
run: |
echo "=== NPM Security Audit ==="
npm audit --production 2>&1 | tee /tmp/npm-audit.txt || true
if npm audit --production 2>&1 | grep -q "found 0 vulnerabilities"; then
echo "No known vulnerabilities in npm dependencies"
else
echo "::warning::NPM vulnerabilities found"
echo "npm_vulnerable=true" >> "$GITHUB_ENV"
fi
- name: Notify on vulnerabilities
if: env.composer_vulnerable == 'true' || env.npm_vulnerable == 'true'
run: |
REPO="${{ github.event.repository.name }}"
curl -sS \
-H "Title: ${REPO} has vulnerable dependencies" \
-H "Tags: lock,warning" \
-H "Priority: high" \
-d "Security audit found vulnerabilities. Review dependency updates." \
"${NTFY_URL}/${NTFY_TOPIC}" || true
+464
View File
@@ -0,0 +1,464 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Joomla
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
# PATH: /templates/workflows/joomla/update-server.yml.template
# VERSION: 04.06.00
# BRIEF: Update Joomla update server XML feed with stable/rc/dev entries
#
# Writes updates.xml with multiple <update> entries:
# - <tag>stable</tag> on push to main (from auto-release)
# - <tag>rc</tag> on push to rc/**
# - <tag>development</tag> on push to dev or dev/**
#
# Joomla filters by user's "Minimum Stability" setting.
name: "Joomla: Update Server"
on:
push:
branches:
- 'dev'
- 'dev/**'
- 'alpha/**'
- 'beta/**'
- 'rc/**'
paths:
- 'src/**'
- 'htdocs/**'
pull_request:
types: [closed]
branches:
- 'dev'
- 'dev/**'
- 'alpha/**'
- 'beta/**'
- 'rc/**'
paths:
- 'src/**'
- 'htdocs/**'
workflow_dispatch:
inputs:
stability:
description: 'Stability tag'
required: true
default: 'development'
type: choice
options:
- development
- alpha
- beta
- rc
- stable
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
permissions:
contents: write
jobs:
update-xml:
name: Update updates.xml
runs-on: release
if: >-
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || github.event_name == 'push'
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.GA_TOKEN }}
fetch-depth: 0
- name: Setup MokoStandards tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.GA_TOKEN }}"}}}'
run: |
if ! command -v composer &> /dev/null; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
fi
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
/tmp/mokostandards-api 2>/dev/null || true
if [ -d "/tmp/mokostandards-api" ] && [ -f "/tmp/mokostandards-api/composer.json" ]; then
cd /tmp/mokostandards-api && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
fi
- name: Generate updates.xml entry
id: update
run: |
BRANCH="${{ github.ref_name }}"
REPO="${{ github.repository }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
VERSION=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null || echo "0.0.0")
# Auto-bump patch on all branches (dev, alpha, beta, rc)
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
BUMPED=$(php /tmp/mokostandards-api/cli/version_bump.php --path . 2>/dev/null || true)
if [ -n "$BUMPED" ]; then
VERSION=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null || echo "$VERSION")
git add -A
git commit -m "chore(version): auto-bump patch ${VERSION} [skip ci]" \
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" 2>/dev/null || true
git push 2>/dev/null || true
fi
# Determine stability from branch or input
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
STABILITY="${{ inputs.stability }}"
elif [[ "$BRANCH" == rc/* ]]; then
STABILITY="rc"
elif [[ "$BRANCH" == beta/* ]]; then
STABILITY="beta"
elif [[ "$BRANCH" == alpha/* ]]; then
STABILITY="alpha"
elif [[ "$BRANCH" == dev/* ]] || [[ "$BRANCH" == "dev" ]]; then
STABILITY="development"
else
STABILITY="stable"
fi
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
# Parse manifest (portable — no grep -P)
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "./build/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
if [ -z "$MANIFEST" ]; then
echo "No Joomla manifest found — skipping"
exit 0
fi
# Extract fields using sed (works on all runners)
EXT_NAME=$(sed -n 's/.*<name>\([^<]*\)<\/name>.*/\1/p' "$MANIFEST" | head -1)
EXT_TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
EXT_ELEMENT=$(sed -n 's/.*<element>\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" | head -1)
EXT_CLIENT=$(sed -n 's/.*<extension[^>]*client="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
EXT_FOLDER=$(sed -n 's/.*<extension[^>]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
EXT_VERSION=$(sed -n 's/.*<version>\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1)
TARGET_PLATFORM=$(sed -n 's/.*\(<targetplatform[^/]*\/>\).*/\1/p' "$MANIFEST" | head -1)
PHP_MINIMUM=$(sed -n 's/.*<php_minimum>\([^<]*\)<\/php_minimum>.*/\1/p' "$MANIFEST" | head -1)
# Fallbacks
[ -z "$EXT_NAME" ] && EXT_NAME="${{ github.event.repository.name }}"
[ -z "$EXT_TYPE" ] && EXT_TYPE="component"
# Derive element if not in manifest: try XML filename, then repo name
if [ -z "$EXT_ELEMENT" ]; then
EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]')
case "$EXT_ELEMENT" in
templatedetails|manifest|*.xml) EXT_ELEMENT=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;;
esac
fi
# Use manifest version if README version is empty
[ "$VERSION" = "0.0.0" ] && [ -n "$EXT_VERSION" ] && VERSION="$EXT_VERSION"
[ -z "$TARGET_PLATFORM" ] && TARGET_PLATFORM=$(printf '<targetplatform name="joomla" version="((5.[0-9])|(6.[0-9]))" %s>' "/")
CLIENT_TAG=""
[ -n "$EXT_CLIENT" ] && CLIENT_TAG="<client>${EXT_CLIENT}</client>"
[ -z "$CLIENT_TAG" ] && ([ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]) && CLIENT_TAG="<client>site</client>"
FOLDER_TAG=""
[ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ] && FOLDER_TAG="<folder>${EXT_FOLDER}</folder>"
PHP_TAG=""
[ -n "$PHP_MINIMUM" ] && PHP_TAG="<php_minimum>${PHP_MINIMUM}</php_minimum>"
# Version suffix for non-stable
DISPLAY_VERSION="$VERSION"
case "$STABILITY" in
development) DISPLAY_VERSION="${VERSION}-dev" ;;
alpha) DISPLAY_VERSION="${VERSION}-alpha" ;;
beta) DISPLAY_VERSION="${VERSION}-beta" ;;
rc) DISPLAY_VERSION="${VERSION}-rc" ;;
esac
MAJOR=$(echo "$VERSION" | awk -F. '{print $1}')
# Each stability level has its own release tag
case "$STABILITY" in
development) RELEASE_TAG="development" ;;
alpha) RELEASE_TAG="alpha" ;;
beta) RELEASE_TAG="beta" ;;
rc) RELEASE_TAG="release-candidate" ;;
*) RELEASE_TAG="v${MAJOR}" ;;
esac
PACKAGE_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.zip"
DOWNLOAD_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${PACKAGE_NAME}"
INFO_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}"
# -- Build install packages (ZIP + tar.gz) --------------------
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
if [ -d "$SOURCE_DIR" ]; then
EXCLUDES=".ftpignore sftp-config* *.ppk *.pem *.key .env*"
TAR_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.tar.gz"
cd "$SOURCE_DIR"
zip -r "/tmp/${PACKAGE_NAME}" . -x $EXCLUDES
cd ..
tar -czf "/tmp/${TAR_NAME}" -C "$SOURCE_DIR" \
--exclude='.ftpignore' --exclude='sftp-config*' \
--exclude='*.ppk' --exclude='*.pem' --exclude='*.key' --exclude='.env*' .
SHA256=$(sha256sum "/tmp/${PACKAGE_NAME}" | cut -d' ' -f1)
# Ensure release exists on Gitea
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
# Create release
RELEASE_JSON=$(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_TAG} (${DISPLAY_VERSION})',
'body': '${STABILITY} release',
'prerelease': True,
'target_commitish': 'main'
}))")" 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)
fi
if [ -n "$RELEASE_ID" ]; then
# 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_FILE in "$PACKAGE_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_FILE}':
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 formats
curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
-H "Content-Type: application/octet-stream" \
--data-binary @"/tmp/${PACKAGE_NAME}" \
"${API_BASE}/releases/${RELEASE_ID}/assets?name=${PACKAGE_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
fi
echo "Packages: ${PACKAGE_NAME} + ${TAR_NAME} (SHA: ${SHA256})" >> $GITHUB_STEP_SUMMARY
else
SHA256=""
fi
# -- Build the new entry (canonical format matching release.yml) --
NEW_ENTRY=""
NEW_ENTRY="${NEW_ENTRY} <update>\n"
NEW_ENTRY="${NEW_ENTRY} <name>${EXT_NAME}</name>\n"
NEW_ENTRY="${NEW_ENTRY} <description>${EXT_NAME} ${STABILITY} build.</description>\n"
NEW_ENTRY="${NEW_ENTRY} <element>${EXT_ELEMENT}</element>\n"
NEW_ENTRY="${NEW_ENTRY} <type>${EXT_TYPE}</type>\n"
[ -n "$CLIENT_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${CLIENT_TAG}\n"
[ -n "$FOLDER_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${FOLDER_TAG}\n"
NEW_ENTRY="${NEW_ENTRY} <version>${VERSION}</version>\n"
NEW_ENTRY="${NEW_ENTRY} <creationDate>$(date +%Y-%m-%d)</creationDate>\n"
NEW_ENTRY="${NEW_ENTRY} <infourl title='${EXT_NAME}'>https://git.mokoconsulting.tech/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${RELEASE_TAG}</infourl>\n"
NEW_ENTRY="${NEW_ENTRY} <downloads>\n"
NEW_ENTRY="${NEW_ENTRY} <downloadurl type='full' format='zip'>${DOWNLOAD_URL}</downloadurl>\n"
NEW_ENTRY="${NEW_ENTRY} </downloads>\n"
[ -n "$SHA256" ] && NEW_ENTRY="${NEW_ENTRY} <sha256>${SHA256}</sha256>\n"
NEW_ENTRY="${NEW_ENTRY} <tags><tag>${STABILITY}</tag></tags>\n"
NEW_ENTRY="${NEW_ENTRY} <maintainer>Moko Consulting</maintainer>\n"
NEW_ENTRY="${NEW_ENTRY} <maintainerurl>https://mokoconsulting.tech</maintainerurl>\n"
NEW_ENTRY="${NEW_ENTRY} <targetplatform name='joomla' version='(5|6).*'/>\n"
[ -n "$PHP_MINIMUM" ] && NEW_ENTRY="${NEW_ENTRY} <php_minimum>${PHP_MINIMUM}</php_minimum>\n"
NEW_ENTRY="${NEW_ENTRY} </update>"
# -- Write new entry to temp file --------------------------------
printf '%b' "$NEW_ENTRY" > /tmp/new_entry.xml
# -- Merge into updates.xml ----------------------------------------
# Cascade: stable→all | rc→rc+lower | beta→beta+lower | alpha→alpha+dev | dev→dev
CASCADE_MAP="stable:development,alpha,beta,rc,stable rc:development,alpha,beta,rc beta:development,alpha,beta alpha:development,alpha development:development"
TARGETS=""
for entry in $CASCADE_MAP; do
key="${entry%%:*}"
vals="${entry#*:}"
if [ "$key" = "${STABILITY}" ]; then
TARGETS="$vals"
break
fi
done
[ -z "$TARGETS" ] && TARGETS="${STABILITY}"
echo "Cascade: ${STABILITY} → ${TARGETS}"
# Create updates.xml if missing
if [ ! -f "updates.xml" ]; then
printf '%s\n' "<?xml version='1.0' encoding='UTF-8'?>" > updates.xml
printf '%s\n' "<!-- Copyright (C) $(date +%Y) Moko Consulting -->" >> updates.xml
printf '%s\n' "<updates>" >> updates.xml
printf '%s\n' "</updates>" >> updates.xml
fi
# Update existing blocks or create missing ones
export PY_TARGETS="$TARGETS" PY_VERSION="$VERSION" PY_DATE="$(date +%Y-%m-%d)"
python3 << 'PYEOF'
import re, os
targets = os.environ["PY_TARGETS"].split(",")
version = os.environ["PY_VERSION"]
date = os.environ["PY_DATE"]
with open("updates.xml") as f:
content = f.read()
with open("/tmp/new_entry.xml") as f:
new_entry_template = f.read()
for tag in targets:
tag = tag.strip()
# Build entry with this tag's name
new_entry = re.sub(r"<tag>[^<]*</tag>", f"<tag>{tag}</tag>", new_entry_template)
# Try to find existing block (handles both single-line and multi-line <tags>)
block_pattern = r"(<update>(?:(?!</update>).)*?<tag>" + re.escape(tag) + r"</tag>.*?</update>)"
match = re.search(block_pattern, content, re.DOTALL)
if match:
# Update in place — replace entire block
content = content.replace(match.group(1), new_entry.strip())
print(f" UPDATED: <tag>{tag}</tag> → {version}")
else:
# Create — insert before </updates>
content = content.replace("</updates>", "\n" + new_entry.strip() + "\n\n</updates>")
print(f" CREATED: <tag>{tag}</tag> → {version}")
# Clean up excessive blank lines
content = re.sub(r"\n{3,}", "\n\n", content)
with open("updates.xml", "w") as f:
f.write(content)
PYEOF
# Commit
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git add updates.xml
git diff --cached --quiet || {
git commit -m "chore: update updates.xml (${STABILITY}: ${DISPLAY_VERSION}) [skip ci]" \
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
git push
}
# -- Sync updates.xml to main (for non-main branches) ----------------------
- name: Sync updates.xml to main
if: github.ref_name != 'main'
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
GA_TOKEN="${{ secrets.GA_TOKEN }}"
FILE_SHA=$(curl -sf -H "Authorization: token ${GA_TOKEN}" \
"${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then
CONTENT=$(base64 -w0 updates.xml)
curl -sf -X PUT -H "Authorization: token ${GA_TOKEN}" \
-H "Content-Type: application/json" \
"${API_BASE}/contents/updates.xml" \
-d "$(python3 -c "import json; print(json.dumps({
'content': '${CONTENT}',
'sha': '${FILE_SHA}',
'message': 'chore: sync updates.xml from ${STABILITY} [skip ci]',
'branch': 'main'
}))")" > /dev/null 2>&1 \
&& echo "updates.xml synced to main (${STABILITY})" >> $GITHUB_STEP_SUMMARY \
|| echo "WARNING: failed to sync updates.xml to main" >> $GITHUB_STEP_SUMMARY
else
echo "WARNING: could not get updates.xml SHA from main" >> $GITHUB_STEP_SUMMARY
fi
- name: SFTP deploy to dev server
if: contains(github.ref, 'dev/') || github.ref == 'refs/heads/dev'
env:
DEV_HOST: ${{ vars.DEV_FTP_HOST }}
DEV_PATH: ${{ vars.DEV_FTP_PATH }}
DEV_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
DEV_USER: ${{ vars.DEV_FTP_USERNAME }}
DEV_PORT: ${{ vars.DEV_FTP_PORT }}
DEV_KEY: ${{ secrets.DEV_FTP_KEY }}
DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
run: |
# -- Permission check: admin or maintain role required --------
ACTOR="${{ github.actor }}"
REPO="${{ github.repository }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
"${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \
python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read")
case "$PERMISSION" in
admin|maintain|write) ;;
*)
echo "Deploy denied: ${ACTOR} has '${PERMISSION}' — requires admin, maintain, or write"
exit 0
;;
esac
[ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured — skipping SFTP"; exit 0; }
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
[ ! -d "$SOURCE_DIR" ] && exit 0
PORT="${DEV_PORT:-22}"
REMOTE="${DEV_PATH%/}"
[ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}"
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
"$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json
if [ -n "$DEV_KEY" ]; then
echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
else
printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json
fi
PLATFORM=$(php /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 --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
elif [ -f "/tmp/mokostandards-api/deploy/deploy-sftp.php" ]; then
php /tmp/mokostandards-api/deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
fi
rm -f /tmp/deploy_key /tmp/sftp-config.json
echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY
- name: Summary
if: always()
run: |
echo "## Joomla Update Server" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Version | \`${DISPLAY_VERSION}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Element | \`${EXT_ELEMENT}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Download | [ZIP](${DOWNLOAD_URL}) |" >> $GITHUB_STEP_SUMMARY
-20
View File
@@ -1,20 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# SPDX-License-Identifier: GPL-3.0-or-later
# FILE INFORMATION
# DEFGROUP: MokoStandards.Templates.Config
# INGROUP: MokoStandards.Templates
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /templates/configs/moko-standards.yml
# VERSION: 04.04.01
# BRIEF: Governance attachment template — synced to .mokostandards in every governed repository
# NOTE: Tokens replaced at sync time: mokoconsulting-tech, MokoWaaS, waas-component, 04.04.00
#
# This file is managed automatically by MokoStandards bulk sync.
# Do not edit manually — changes will be overwritten on the next sync.
# To update governance settings, open a PR in MokoStandards instead:
# https://github.com/mokoconsulting-tech/MokoStandards
standards_source: "https://github.com/mokoconsulting-tech/MokoStandards"
standards_version: "04.04.00"
platform: "waas-component"
governed_repo: "mokoconsulting-tech/MokoWaaS"
+99 -1
View File
@@ -28,10 +28,108 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Planned ### Planned
- Heartbeat telemetry to WaaS dashboard (#54)
- License/subscription check - License/subscription check
- System email template branding (DB approach) - System email template branding (DB approach)
## [02.03.10] - 2026-05-24
### Added
- Canonical URL injection for alias domains (prevents SEO duplication)
- Primary Domain config field in Site Aliases tab
- Heartbeat registration for alias domains (each alias gets Grafana datasource)
- Plugin protection: hidden from non-master users, disable/uninstall blocked
- Dynamic plugin version read from manifest XML (no more hardcoded strings)
- Package structure: `pkg_mokowaas` with system plugin, webservices plugin, and component
### Changed
- Alias offline mode uses Joomla's native template offline.php (not custom HTML)
- Alias detection simplified: direct lookup in aliases list (no primary host comparison)
- handleSiteAlias() moved to onAfterRoute (client type resolved at that point)
- Package script.php enables plugins on every install/update and sends heartbeat
### Fixed
- Alias domain matching: strip trailing slashes, handle Joomla subform stdClass format
- Backend redirect: use primary_domain setting instead of Uri::root() (returned alias domain on mirrors)
- CI: version_bump reads manifest XML with priority over README.md VERSION header
- CI: version bump occurs after release build, not before
- CI: pipefail disabled during element detection (SIGPIPE on find|head)
- CI: pkg_pkg_ prefix duplication in zip names and updates.xml URLs
- CI: updates_xml_build preserves existing channel entries (stable not wiped by dev releases)
### Removed
- deploy-manual.yml workflow — using Joomla update server for distribution
- Accidentally committed profile.ps1 and TODO.md
## [02.01.43] - 2026-05-23
### Added
- Site Aliases tab with Joomla subform repeatable-table UI
- Per-alias offline toggle with custom maintenance message (503 response)
- Per-alias robots meta directive (index/noindex/follow/nofollow/none)
- Per-alias backend redirect (admin panel redirects to primary domain)
- 6 MokoWaaS API endpoints: health, install, update, cache, backup, info
- Remote plugin install via `/?mokowaas=install` endpoint
- Remote update trigger via `/?mokowaas=update` endpoint
- Remote cache clear via `/?mokowaas=cache` endpoint (site + admin + opcache)
- Remote Akeeba Backup trigger via `/?mokowaas=backup` endpoint
- Compact site info via `/?mokowaas=info` endpoint
### Changed
- Site aliases moved from comma-separated text field to structured subform
- Each alias now stores domain, offline, offline_message, robots, redirect_backend
- Heartbeat provisioning updated for subform alias format
- Grafana datasource names use domain-only (removed "MokoWaaS - " prefix)
### Fixed
- Heartbeat receiver accepts any 200 status (registered/updated/ok)
- script.php uses heartbeat receiver instead of Grafana API (fixes 403 RBAC)
## [02.01.37] - 2026-05-23
### Added
- Health check endpoint at `/?mokowaas=health` with 16 diagnostic checks (#54)
- Core checks: database latency, filesystem writability/size, cache, extensions
- Backup checks: Akeeba Backup last backup date/status/size, days since, frequency
- Security checks: Admin Tools WAF status, blocked requests 24h/7d
- SSL certificate: expiry date, days left, issuer (degraded <30d, error <7d)
- Scheduled tasks: Joomla task scheduler status, failed tasks 24h
- Error log: PHP error log size, recent errors, last error message
- Database size: total MB, table count, top 5 largest tables
- Content stats: articles, categories, menu items, modules
- User activity: total users, active sessions, failed logins 24h, last login
- Mail system: mailer type, from address, SMTP host, queue count
- SEO health: robots.txt, sitemap, htaccess, SEF status
- Template info: site/admin template names, override count
- Configuration drift: debug mode, error reporting, force SSL, caching
- Human-readable `reason` field explaining degraded/error status
- Site size reporting (images, media, tmp, cache, logs directories)
- Heartbeat provisioning via receiver at bench.mokoconsulting.tech
- Grafana datasource auto-provisioning via YAML (no API token needed)
- ntfy notifications on heartbeat registration (mokowaas-heartbeat topic)
- Grafana dashboard with 9 rows covering all 16 health checks
- Auto-generated health API token (separate from Joomla user tokens)
### Changed
- Health endpoint always enabled — no config toggle needed
- Grafana provisioning uses heartbeat receiver pattern (replaces direct API)
- Removed config fields: enable_health_endpoint, grafana_url, grafana_api_key
- Migrated .gitea/ to .mokogitea/ directory standard
- Updated all references from MokoStandards to moko-platform
- Renamed Gitea references to MokoGitea in docs
### Fixed
- SSL verification disabled for Grafana cURL calls (shared hosting)
- cURL follow redirects enabled
- updates.xml download URL uses correct `development` tag
### Security
- Plugin hidden from plugin list for non-master users
- Plugin settings restricted to master user only
- Self-healing lock (enforceLocked) runs every page load
- Uninstall blocked in preflight
- Health endpoint requires HTTPS + bearer token
- Heartbeat shared secret for receiver authentication
## [02.01.08] - 2026-04-07 ## [02.01.08] - 2026-04-07
### Added ### Added
+42
View File
@@ -0,0 +1,42 @@
# CLAUDE.md
This file provides guidance to Claude Code when working with this repository.
## Project Overview
**MokoWaaS** -- MokoWaaS is a Joomla 5.x / 6.x system plugin that provides a configurable white-label identity layer for the MokoWaaS platform.
| Field | Value |
|---|---|
| **Platform** | joomla |
| **Language** | PHP |
| **Default branch** | main |
| **License** | GPL-3.0-or-later |
| **Wiki** | [MokoWaaS Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/wiki) |
| **Standards** | [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home) |
## Common Commands
```bash
composer install # Install PHP dependencies
```
## Architecture
This is a Joomla extension. Key directories:
- `src/` -- extension source (deployed to Joomla)
- `src/*.xml` -- manifest file (version, files, params)
- `src/src/` or `src/services/` -- PHP classes
- `src/language/` -- translation strings
- `src/media/` -- CSS/JS/images
## 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)
+13 -13
View File
@@ -15,7 +15,7 @@
DEFGROUP: Joomla.Plugin DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS INGROUP: MokoWaaS
REPO: https://github.com/mokoconsulting-tech/mokowaas REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.01.13 VERSION: 02.03.12
PATH: /README.md PATH: /README.md
BRIEF: Rebranding plugin for MokoWaaS platform BRIEF: Rebranding plugin for MokoWaaS platform
NOTE: Internal WaaS identity abstraction layer NOTE: Internal WaaS identity abstraction layer
@@ -23,7 +23,7 @@
# MokoWaaS Plugin # MokoWaaS Plugin
[![Version](https://img.shields.io/badge/version-02.01.11-blue.svg?logo=v&logoColor=white)](https://github.com/mokoconsulting-tech/MokoWaaS/releases/tag/v02) [![Version](https://img.shields.io/badge/version-02.03.00-blue.svg?logo=v&logoColor=white)](https://github.com/mokoconsulting-tech/MokoWaaS/releases/tag/v02)
[![License](https://img.shields.io/badge/license-GPL--3.0--or--later-green.svg?logo=gnu&logoColor=white)](LICENSE) [![License](https://img.shields.io/badge/license-GPL--3.0--or--later-green.svg?logo=gnu&logoColor=white)](LICENSE)
[![Joomla](https://img.shields.io/badge/Joomla-5.x%20%7C%206.x-red.svg?logo=joomla&logoColor=white)](https://www.joomla.org) [![Joomla](https://img.shields.io/badge/Joomla-5.x%20%7C%206.x-red.svg?logo=joomla&logoColor=white)](https://www.joomla.org)
[![PHP](https://img.shields.io/badge/PHP-8.1%2B-777BB4.svg?logo=php&logoColor=white)](https://www.php.net) [![PHP](https://img.shields.io/badge/PHP-8.1%2B-777BB4.svg?logo=php&logoColor=white)](https://www.php.net)
@@ -59,7 +59,11 @@ The MokoWaaS plugin operationalizes a unified naming convention, brand-controlle
- **Joomla 5.x / 6.x Compatible**: Built using modern Joomla plugin architecture with dependency injection - **Joomla 5.x / 6.x Compatible**: Built using modern Joomla plugin architecture with dependency injection
- **Multi-Language Support**: en-GB and en-US locales - **Multi-Language Support**: en-GB and en-US locales
- **Admin & Frontend Coverage**: Dashboard, footer, login, installer, system info, update component, error pages, and more - **Admin & Frontend Coverage**: Dashboard, footer, login, installer, system info, update component, error pages, and more
- **Governance Compliant**: Aligned with [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards) - **Health Monitoring**: 16 diagnostic checks via `/?mokowaas=health` — database, filesystem, cache, extensions, Akeeba Backup, Admin Tools, SSL, cron, errors, DB size, content, users, mail, SEO, templates, config
- **Grafana Integration**: Auto-provisions Infinity datasource via heartbeat receiver — 9-row dashboard with all health metrics
- **ntfy Notifications**: Heartbeat events pushed to `mokowaas-heartbeat` topic
- **Plugin Protection**: Hidden from non-super-admins, self-healing lock, uninstall blocked
- **Governance Compliant**: Aligned with [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform)
## System Requirements ## System Requirements
@@ -322,22 +326,18 @@ See [LICENSE.md](LICENSE.md) for the full license text.
This extension follows the [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards) version governance model using semantic versioning: `MAJOR.MINOR.PATCH` This extension follows the [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards) version governance model using semantic versioning: `MAJOR.MINOR.PATCH`
Current version: **01.04.00** Current version: **02.01.18**
## Changelog ## Changelog
See [CHANGELOG.md](CHANGELOG.md) for a complete version history. See [CHANGELOG.md](CHANGELOG.md) for a complete version history.
### Recent Changes (v01.04.00 - 2026-02-22) ### Recent Changes (v02.01.18 - 2026-04-23)
- Added complete Joomla 5.x system plugin implementation - Always install and lock MokoOnyx template on install/update
- Created main plugin class with event handlers - Always unlock MokoCassiopeia on install/update (allow uninstall)
- Implemented plugin manifest with Joomla 5.x namespace support - Bundle MokoOnyx payload (replaces MokoCassiopeia payload)
- Added dependency injection service provider - Update payload workflow to fetch MokoOnyx from Gitea releases
- Created plugin language files
- Integrated with language override system
- Enhanced language overrides (57+ strings)
- Fixed typo in error messages (OCCURRED)
## Contributing ## Contributing
+1 -1
View File
@@ -327,4 +327,4 @@ Templates and plugins must remain synchronized to avoid inconsistencies.
| Date | Author | Description | | Date | Author | Description |
| ---------- | ------------------------------- | ------------------------------- | | ---------- | ------------------------------- | ------------------------------- |
| 2025-12-11 | Jonathan Miller (@jmiller-moko) | Initial creation of build guide | | 2025-12-11 | Jonathan Miller (@jmiller) | Initial creation of build guide |
+2 -2
View File
@@ -265,5 +265,5 @@ Restricted components are automatically hidden from the admin menu via `onPrepro
| Version | Date | Author | Description | | Version | Date | Author | Description |
| -------- | ---------- | ------------------------------- | ---------------------------------------------- | | -------- | ---------- | ------------------------------- | ---------------------------------------------- |
| 01.02.00 | 2025-12-11 | Jonathan Miller (@jmiller-moko) | Initial standalone configuration guide created | | 01.02.00 | 2025-12-11 | Jonathan Miller (@jmiller) | Initial standalone configuration guide created |
| 02.01.08 | 2026-04-07 | Jonathan Miller (@jmiller-moko) | Full rewrite: WaaS access, visual branding, tenant restrictions, security, maintenance, action logs | | 02.01.08 | 2026-04-07 | Jonathan Miller (@jmiller) | Full rewrite: WaaS access, visual branding, tenant restrictions, security, maintenance, action logs |
+1 -1
View File
@@ -84,4 +84,4 @@ If the plugin introduces issues or conflicts:
| Date | Author | Description | | Date | Author | Description |
| ---------- | ------------------------------- | ---------------------------- | | ---------- | ------------------------------- | ---------------------------- |
| 2025-12-11 | Jonathan Miller (@jmiller-moko) | Rewrite for version 01.03.00 | | 2025-12-11 | Jonathan Miller (@jmiller) | Rewrite for version 01.03.00 |
+1 -1
View File
@@ -128,4 +128,4 @@ Administrators should factor these dependencies into maintenance and upgrade pla
| Date | Author | Description | | Date | Author | Description |
| ---------- | ------------------------------- | ---------------------------- | | ---------- | ------------------------------- | ---------------------------- |
| 2025-12-11 | Jonathan Miller (@jmiller-moko) | Rewrite for version 01.03.00 | | 2025-12-11 | Jonathan Miller (@jmiller) | Rewrite for version 01.03.00 |
+1 -1
View File
@@ -107,4 +107,4 @@ These strategies improve long term WaaS platform stability.
| Date | Author | Description | | Date | Author | Description |
| ---------- | ------------------------------- | ------------------------------------------- | | ---------- | ------------------------------- | ------------------------------------------- |
| 2025-12-11 | Jonathan Miller (@jmiller-moko) | Full rewrite and update to version 01.03.00 | | 2025-12-11 | Jonathan Miller (@jmiller) | Full rewrite and update to version 01.03.00 |
+4 -4
View File
@@ -41,7 +41,7 @@
| 4 | Check admin dashboard | "Welcome to MokoWaaS!" appears in control panel | [ ] | | 4 | Check admin dashboard | "Welcome to MokoWaaS!" appears in control panel | [ ] |
| 5 | Check admin footer | "Powered by MokoWaaS" appears | [ ] | | 5 | Check admin footer | "Powered by MokoWaaS" appears | [ ] |
| 6 | Check admin login page | "MokoWaaS Administrator Login" title, support links show "Moko Consulting" | [ ] | | 6 | Check admin login page | "MokoWaaS Administrator Login" title, support links show "Moko Consulting" | [ ] |
| 7 | Check frontend footer | "Powered by MokoWaaS" in Cassiopeia template | [ ] | | 7 | Check frontend footer | "Powered by MokoWaaS" in MokoOnyx template | [ ] |
| 8 | Check Joomla override files at `administrator/language/overrides/en-GB.override.ini` | Contains `BEGIN MokoWaaS Overrides` sentinel block | [ ] | | 8 | Check Joomla override files at `administrator/language/overrides/en-GB.override.ini` | Contains `BEGIN MokoWaaS Overrides` sentinel block | [ ] |
| 9 | Check Joomla override files at `language/overrides/en-GB.override.ini` | Contains `BEGIN MokoWaaS Overrides` sentinel block | [ ] | | 9 | Check Joomla override files at `language/overrides/en-GB.override.ini` | Contains `BEGIN MokoWaaS Overrides` sentinel block | [ ] |
@@ -87,7 +87,7 @@
|---|------|-----------------|------| |---|------|-----------------|------|
| 1 | Set Enable Branding to "No", save | Save succeeds | [ ] | | 1 | Set Enable Branding to "No", save | Save succeeds | [ ] |
| 2 | Reload admin dashboard | Default Joomla strings appear (e.g., "Welcome to Joomla!") | [ ] | | 2 | Reload admin dashboard | Default Joomla strings appear (e.g., "Welcome to Joomla!") | [ ] |
| 3 | Check frontend footer | Default "Powered by Joomla" or Cassiopeia default | [ ] | | 3 | Check frontend footer | Default "Powered by Joomla" or MokoOnyx default | [ ] |
| 4 | Set Enable Branding back to "Yes", save | Branding strings restored immediately | [ ] | | 4 | Set Enable Branding back to "Yes", save | Branding strings restored immediately | [ ] |
### 2.7 Update (Upgrade from Previous Version) ### 2.7 Update (Upgrade from Previous Version)
@@ -138,7 +138,7 @@ Verify the following admin areas no longer show "Joomla":
| # | Location | Expected Brand Text | Pass | | # | Location | Expected Brand Text | Pass |
|---|----------|-------------------|------| |---|----------|-------------------|------|
| 1 | Cassiopeia footer | "Powered by {brand}" | [ ] | | 1 | MokoOnyx footer | "Powered by {brand}" | [ ] |
| 2 | Site offline page | Maintenance message (no Joomla reference) | [ ] | | 2 | Site offline page | Maintenance message (no Joomla reference) | [ ] |
| 3 | 404 error page | "Page Not Found" (no Joomla reference) | [ ] | | 3 | 404 error page | "Page Not Found" (no Joomla reference) | [ ] |
| 4 | Frontend login support | "{company} Support" / "{brand} Documentation" | [ ] | | 4 | Frontend login support | "{company} Support" / "{brand} Documentation" | [ ] |
@@ -298,4 +298,4 @@ grep -r 'Version:' src/**/*.ini | grep -v '02.01.08'
| Version | Date | Author | Description | | Version | Date | Author | Description |
| -------- | ---------- | ------------------------------- | ------------------------------- | | -------- | ---------- | ------------------------------- | ------------------------------- |
| 02.01.08 | 2026-04-07 | Jonathan Miller (@jmiller-moko) | Full testing guide: 17 suites covering install, branding, access, visual, security, maintenance, edge cases | | 02.01.08 | 2026-04-07 | Jonathan Miller (@jmiller) | Full testing guide: 17 suites covering install, branding, access, visual, security, maintenance, edge cases |
+1 -1
View File
@@ -155,4 +155,4 @@ To reduce incidents and ensure operational stability:
| Date | Author | Description | | Date | Author | Description |
| ---------- | ------------------------------- | ------------------------------------------- | | ---------- | ------------------------------- | ------------------------------------------- |
| 2025-12-11 | Jonathan Miller (@jmiller-moko) | Full rewrite and update to version 01.03.00 | | 2025-12-11 | Jonathan Miller (@jmiller) | Full rewrite and update to version 01.03.00 |
+1 -1
View File
@@ -114,4 +114,4 @@ To minimize disruptions:
| Date | Author | Description | | Date | Author | Description |
| ---------- | ------------------------------- | ------------------------------------------- | | ---------- | ------------------------------- | ------------------------------------------- |
| 2025-12-11 | Jonathan Miller (@jmiller-moko) | Full rewrite and update to version 01.03.00 | | 2025-12-11 | Jonathan Miller (@jmiller) | Full rewrite and update to version 01.03.00 |
+1 -1
View File
@@ -75,4 +75,4 @@ Contributors should:
| Date | Author | Description | | Date | Author | Description |
| ---------- | ------------------------------- | ------------------------------------------- | | ---------- | ------------------------------- | ------------------------------------------- |
| 2026-02-22 | GitHub Copilot | Update to version 01.04.00 | | 2026-02-22 | GitHub Copilot | Update to version 01.04.00 |
| 2025-12-11 | Jonathan Miller (@jmiller-moko) | Full rewrite and update to version 01.03.00 | | 2025-12-11 | Jonathan Miller (@jmiller) | Full rewrite and update to version 01.03.00 |
+1 -1
View File
@@ -122,4 +122,4 @@ While the plugin provides broad branding coverage, certain constraints apply:
| Date | Author | Description | | Date | Author | Description |
| ---------- | ------------------------------- | ---------------------------- | | ---------- | ------------------------------- | ---------------------------- |
| 2026-02-22 | GitHub Copilot | Update for version 01.04.00 | | 2026-02-22 | GitHub Copilot | Update for version 01.04.00 |
| 2025-12-11 | Jonathan Miller (@jmiller-moko) | Rewrite for version 01.03.00 | | 2025-12-11 | Jonathan Miller (@jmiller) | Rewrite for version 01.03.00 |
+32 -8
View File
@@ -29,8 +29,30 @@ Joomla checks for extension updates by fetching an XML file from the URL defined
| Event | Workflow | `<tag>` | `<version>` | | Event | Workflow | `<tag>` | `<version>` |
|-------|----------|---------|-------------| |-------|----------|---------|-------------|
| Merge to `main` | `auto-release.yml` | `stable` | `XX.YY.ZZ` | | Merge to `main` | `auto-release.yml` | `stable` | `XX.YY.ZZ` |
| Push to `dev/**` | `deploy-dev.yml` | `development` | `development` | | Push to `dev` or `dev/**` | `update-server.yml` | `development` | `XX.YY.ZZ-dev` |
| Push to `rc/**` | `deploy-dev.yml` | `rc` | `XX.YY.ZZ-rc` | | Push to `alpha/**` | `update-server.yml` | `alpha` | `XX.YY.ZZ-alpha` |
| Push to `beta/**` | `update-server.yml` | `beta` | `XX.YY.ZZ-beta` |
| Push to `rc/**` | `update-server.yml` | `rc` | `XX.YY.ZZ-rc` |
**Trigger behavior**: `update-server.yml` triggers on both direct pushes and PR merges to `dev`, `dev/**`, `alpha/**`, `beta/**`, and `rc/**` branches. It supports bare `dev` branches (not just `dev/**` patterns).
### Cascade Release Channels
Each stability level writes itself **and all lower channels** to `updates.xml`:
| Release Stream | Channels written |
|---------------|-----------------|
| development | `development` |
| alpha | `development`, `alpha` |
| beta | `development`, `alpha`, `beta` |
| rc | `development`, `alpha`, `beta`, `rc` |
| stable | `development`, `alpha`, `beta`, `rc`, `stable` |
This ensures Joomla sites on any "Minimum Stability" setting always see the latest available release.
### Sync to Main
Since Joomla sites read `updates.xml` from the `main` branch, the `update-server.yml` workflow **syncs `updates.xml` to `main` via the Gitea API** after building on non-main branches. This ensures pre-release channel entries are visible to sites checking for updates without requiring a PR merge to main.
### Generated XML Structure ### Generated XML Structure
@@ -94,14 +116,16 @@ Your XML manifest must include an `<updateservers>` tag pointing to the `update.
### Branch Lifecycle ### Branch Lifecycle
``` ```
dev/XX.YY.ZZ → rc/XX.YY.ZZ → main version/XX.YY dev → [alpha] → [beta] → rc → version/XX → main → dev
(development) (rc) (stable) (frozen snapshot) optional optional (integration) (production) (feedback)
``` ```
1. **Development** (`dev/**`): `update.xml` with `<tag>development</tag>`, download points to branch archive 1. **Development** (`dev` or `dev/**`): `updates.xml` with `<tag>development</tag>`, download points to Gitea release ZIP
2. **Release Candidate** (`rc/**`): `update.xml` with `<tag>rc</tag>`, version set to `XX.YY.ZZ-rc` 2. **Alpha** (`alpha/**`): `updates.xml` with `<tag>alpha</tag>`, cascades to development channel
3. **Stable Release** (merge to `main`): `update.xml` with `<tag>stable</tag>`, download points to GitHub Release asset 3. **Beta** (`beta/**`): `updates.xml` with `<tag>beta</tag>`, cascades to alpha + development channels
4. **Frozen Snapshot** (`version/XX.YY`): immutable, never force-pushed 4. **Release Candidate** (`rc/**`): `updates.xml` with `<tag>rc</tag>`, cascades to beta + alpha + development channels
5. **Stable Release** (merge to `main`): `updates.xml` with `<tag>stable</tag>`, cascades to all channels, download points to GitHub Release asset
6. **Frozen Snapshot** (`version/XX`): immutable, never force-pushed
### Health Checks ### Health Checks
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,38 @@
<?php
/**
* @package MokoWaaS
* @subpackage com_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
defined('_JEXEC') or die;
use Joomla\CMS\Dispatcher\ComponentDispatcherFactoryInterface;
use Joomla\CMS\Extension\ComponentInterface;
use Joomla\CMS\Extension\Service\Provider\ComponentDispatcherFactory;
use Joomla\CMS\Extension\Service\Provider\MVCFactory;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
return new class implements ServiceProviderInterface
{
public function register(Container $container): void
{
$container->registerServiceProvider(new MVCFactory('\\Moko\\Component\\MokoWaaS'));
$container->registerServiceProvider(new ComponentDispatcherFactory('\\Moko\\Component\\MokoWaaS'));
$container->set(
ComponentInterface::class,
function (Container $container) {
$component = new \Joomla\CMS\Extension\MVCComponent(
$container->get(ComponentDispatcherFactoryInterface::class)
);
$component->setMVCFactory($container->get(MVCFactoryInterface::class));
return $component;
}
);
}
};
@@ -0,0 +1,89 @@
<?php
/**
* @package MokoWaaS
* @subpackage com_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoWaaS\Api\Controller;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Controller\BaseController;
/**
* Cache management API controller.
*
* POST /api/index.php/v1/mokowaas/cache
*
* @since 1.0.0
*/
class CacheController extends BaseController
{
/**
* Clear all Joomla caches.
*
* @return void
*
* @since 1.0.0
*/
public function execute(): void
{
$app = Factory::getApplication();
if ($app->input->getMethod() !== 'POST')
{
$this->sendJson(405, ['error' => 'POST required']);
return;
}
$user = $app->getIdentity();
if (!$user->authorise('core.manage', 'com_plugins'))
{
$this->sendJson(403, ['error' => 'Not authorized']);
return;
}
try
{
$cache = Factory::getCache('');
$cache->clean('');
$adminCache = Factory::getCache('', 'callback', 'administrator');
$adminCache->clean('');
if (function_exists('opcache_reset'))
{
opcache_reset();
}
$this->sendJson(200, [
'status' => 'ok',
'message' => 'Cache cleared',
]);
}
catch (\Throwable $e)
{
$this->sendJson(500, [
'error' => 'Cache clear failed',
'message' => $e->getMessage(),
]);
}
}
/**
* @param int $code HTTP status code
* @param array $payload Response data
* @return void
*/
private function sendJson(int $code, array $payload): void
{
$app = Factory::getApplication();
$app->setHeader('Content-Type', 'application/json', true);
$app->setHeader('Status', (string) $code, true);
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
$app->close();
}
}
@@ -0,0 +1,139 @@
<?php
/**
* @package MokoWaaS
* @subpackage com_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoWaaS\Api\Controller;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Controller\BaseController;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\CMS\Uri\Uri;
use Joomla\Registry\Registry;
/**
* Health check API controller.
*
* GET /api/index.php/v1/mokowaas/health
*
* Returns full health diagnostics from the MokoWaaS system plugin.
* Requires a Joomla API token with core.manage permissions.
*
* @since 1.0.0
*/
class HealthController extends BaseController
{
/**
* Return full health check data.
*
* @return void
*
* @since 1.0.0
*/
public function displayList(): void
{
$app = Factory::getApplication();
$user = $app->getIdentity();
if (!$user->authorise('core.manage', 'com_plugins'))
{
$this->sendJson(403, ['error' => 'Not authorized']);
return;
}
$plugin = PluginHelper::getPlugin('system', 'mokowaas');
if (!$plugin)
{
$this->sendJson(503, ['error' => 'MokoWaaS system plugin not enabled']);
return;
}
$params = new Registry($plugin->params);
$config = Factory::getConfig();
$db = Factory::getDbo();
// Collect basic health data
$payload = [
'status' => 'ok',
'timestamp' => gmdate('Y-m-d\TH:i:s\Z'),
'site' => [
'name' => $config->get('sitename', ''),
'url' => rtrim(Uri::root(), '/'),
'joomla_version' => JVERSION,
'php_version' => PHP_VERSION,
'db_type' => $db->getName(),
'offline' => (bool) $config->get('offline', 0),
'debug' => (bool) $config->get('debug', 0),
'sef' => (bool) $config->get('sef', 0),
'caching' => (bool) $config->get('caching', 0),
],
'plugin' => [
'brand' => $params->get('brand_name', 'MokoWaaS'),
'company' => $params->get('company_name', 'Moko Consulting'),
],
];
// Database check
try
{
$db->setQuery('SELECT 1');
$db->loadResult();
$payload['checks']['database'] = ['status' => 'ok'];
}
catch (\Throwable $e)
{
$payload['status'] = 'error';
$payload['checks']['database'] = ['status' => 'error', 'message' => $e->getMessage()];
}
// Disk space
$free = @disk_free_space(JPATH_ROOT);
$total = @disk_total_space(JPATH_ROOT);
if ($free !== false && $total !== false)
{
$freeMb = round($free / 1048576);
$payload['checks']['filesystem'] = [
'status' => $freeMb < 100 ? 'degraded' : 'ok',
'free_disk_mb' => $freeMb,
'total_disk_mb' => round($total / 1048576),
];
}
// Content counts
$db->setQuery($db->getQuery(true)->select('COUNT(*)')->from($db->quoteName('#__content')));
$payload['counts']['articles'] = (int) $db->loadResult();
$db->setQuery($db->getQuery(true)->select('COUNT(*)')->from($db->quoteName('#__users')));
$payload['counts']['users'] = (int) $db->loadResult();
$db->setQuery($db->getQuery(true)->select('COUNT(*)')->from($db->quoteName('#__extensions'))->where($db->quoteName('enabled') . ' = 1'));
$payload['counts']['extensions'] = (int) $db->loadResult();
$this->sendJson(200, $payload);
}
/**
* Send a JSON response and close.
*
* @param int $code HTTP status code
* @param array $payload Response data
*
* @return void
*
* @since 1.0.0
*/
private function sendJson(int $code, array $payload): void
{
$app = Factory::getApplication();
$app->setHeader('Content-Type', 'application/json', true);
$app->setHeader('Status', (string) $code, true);
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
$app->close();
}
}
@@ -0,0 +1,94 @@
<?php
/**
* @package MokoWaaS
* @subpackage com_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoWaaS\Api\Controller;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Controller\BaseController;
/**
* Update check API controller.
*
* POST /api/index.php/v1/mokowaas/update
*
* @since 1.0.0
*/
class UpdateController extends BaseController
{
/**
* Trigger Joomla update finder and return count of available updates.
*
* @return void
*
* @since 1.0.0
*/
public function execute(): void
{
$app = Factory::getApplication();
if ($app->input->getMethod() !== 'POST')
{
$this->sendJson(405, ['error' => 'POST required']);
return;
}
$user = $app->getIdentity();
if (!$user->authorise('core.manage', 'com_installer'))
{
$this->sendJson(403, ['error' => 'Not authorized']);
return;
}
try
{
$db = Factory::getDbo();
$db->setQuery($db->getQuery(true)->delete($db->quoteName('#__updates')));
$db->execute();
\Joomla\CMS\Updater\Updater::getInstance()->findUpdates();
$db->setQuery(
$db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__updates'))
->where($db->quoteName('extension_id') . ' != 0')
);
$count = (int) $db->loadResult();
$this->sendJson(200, [
'status' => 'ok',
'updates_found' => $count,
'message' => $count . ' update(s) available',
]);
}
catch (\Throwable $e)
{
$this->sendJson(500, [
'error' => 'Update check failed',
'message' => $e->getMessage(),
]);
}
}
/**
* @param int $code HTTP status code
* @param array $payload Response data
* @return void
*/
private function sendJson(int $code, array $payload): void
{
$app = Factory::getApplication();
$app->setHeader('Content-Type', 'application/json', true);
$app->setHeader('Status', (string) $code, true);
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
$app->close();
}
}
+23
View File
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<extension type="component" method="upgrade">
<name>MokoWaaS API</name>
<author>Moko Consulting</author>
<creationDate>2026-05-23</creationDate>
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.03.11</version>
<description>Minimal API-only component for MokoWaaS. Provides REST endpoints for site health, cache, updates, and backups.</description>
<namespace path="api/src">Moko\Component\MokoWaaS\Api</namespace>
<administration>
<files folder="admin">
<folder>services</folder>
</files>
</administration>
<api>
<files folder="api">
<folder>src</folder>
</files>
</api>
</extension>
File diff suppressed because it is too large Load Diff
@@ -15,5 +15,5 @@
; Variables: (none) ; Variables: (none)
; ----------------------------------------------------------------------------- ; -----------------------------------------------------------------------------
PLG_SYSTEM_MOKOWAAS="System - MokoWaaS" PLG_SYSTEM_MOKOWAAS="System - Moko WaaS"
PLG_SYSTEM_MOKOWAAS_XML_DESCRIPTION="This plugin rebrands the Joomla system interface with MokoWaaS identity. It applies language overrides and ensures consistent branding across the platform." PLG_SYSTEM_MOKOWAAS_XML_DESCRIPTION="This plugin rebrands the Joomla system interface with MokoWaaS identity. It applies language overrides and ensures consistent branding across the platform."

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