Compare commits
525 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 54d2878376 | |||
| 3c834df309 | |||
| eb1dd88e5b | |||
| 75d407e290 | |||
| e79ba9511f | |||
| 529950d42d | |||
| f634826ed9 | |||
| 10c55470e5 | |||
| db1f1c6e1b | |||
| db2de9e2cd | |||
| 0095179c22 | |||
| 266ab233dd | |||
| c91e8c2dfb | |||
| 9f31e7b883 | |||
| d4e05c3ce3 | |||
| 4b8dcd650a | |||
| 06e1917e93 | |||
| 0de0e7e7dd | |||
| 958cd0eaab | |||
| 8dc533a477 | |||
| 279f18ae46 | |||
| db8dac0827 | |||
| aac2e66826 | |||
| 376d57a64f | |||
| cc801bd18e | |||
| 46b751ef8f | |||
| 12057b1ba3 | |||
| b8f31e3139 | |||
| d05da949a3 | |||
| 5f9b38d003 | |||
| 168c301a4f | |||
| d8324ab057 | |||
| a1711416c4 | |||
| 31c127db57 | |||
| 00a2eb23f8 | |||
| ae4e2715c5 | |||
| 7552a744da | |||
| bc7ad7898c | |||
| e3ba5cf773 | |||
| 8e678f3bd7 | |||
| 1543b5559c | |||
| 72d46fdbaa | |||
| d956630e7c | |||
| ff6b0ba34b | |||
| 9a0f5e4a9b | |||
| a3dc48fdc5 | |||
| b585f0e726 | |||
| 932286e166 | |||
| fc9df44275 | |||
| 649c2628e8 | |||
| dd0efe0029 | |||
| 9809e85e42 | |||
| b58ee80198 | |||
| dbc163faa5 | |||
| f6a535bd5b | |||
| ae9d38731c | |||
| 82bdd3442f | |||
| 66c149d64f | |||
| 59dc59c049 | |||
| 20e854376e | |||
| ba5221f58e | |||
| 0cb6814ca2 | |||
| 50e6e8e320 | |||
| cd05983abe | |||
| 8ff5c944a8 | |||
| 789d69c5d2 | |||
| b9259ca7fa | |||
| 6a75500593 | |||
| 041b70a296 | |||
| 479f5be516 | |||
| 59fecbafdc | |||
| c7eda18c47 | |||
| 022cac4b99 | |||
| 8c9f2cbf14 | |||
| 358a4a6000 | |||
| 2029cf6999 | |||
| d229f6fa18 | |||
| 27415afce8 | |||
| feacb79752 | |||
| 954e2250ed | |||
| 92c9813b2e | |||
| d32c50bfe5 | |||
| 706adedbe6 | |||
| bef76abc9f | |||
| bd548cd1cb | |||
| c335a3f451 | |||
| 2cc8bc9639 | |||
| 7a3fd0e40c | |||
| 0625bc9e7e | |||
| b7e5b9abe0 | |||
| 6d8beed12d | |||
| da20b814b9 | |||
| d2fd5167c7 | |||
| a1a01fbe3b | |||
| 247b2b9710 | |||
| 03bcca6b8c | |||
| e71a47687d | |||
| 4099b45b83 | |||
| 39c58e7a87 | |||
| 97c9490238 | |||
| 103246413a | |||
| 3aff660885 | |||
| 22be873c79 | |||
| 32ce878a62 | |||
| 77c643ccc9 | |||
| 39e4b4a4d3 | |||
| 9fbc4060b9 | |||
| c26a8f664f | |||
| 9ac51e0490 | |||
| 28fcac2371 | |||
| 1a753b805e | |||
| 23ef7532ef | |||
| d04a4fefed | |||
| 1c87dca5fc | |||
| b322acca8b | |||
| 03a1ae3697 | |||
| cf0cceacf1 | |||
| 5eba97790a | |||
| 9c2a269053 | |||
| 2351c92c79 | |||
| 8f3a1a16ac | |||
| 1ec4ca7087 | |||
| a0e0ff3420 | |||
| ea973565ad | |||
| f12dffa339 | |||
| 21e67efe81 | |||
| 055bcfcea9 | |||
| 232bf77cbf | |||
| 3cb1b7b228 | |||
| 1e9c246ff5 | |||
| 0e300c4859 | |||
| 98333c124a | |||
| da71973fae | |||
| c13dc3be1d | |||
| 31aa281717 | |||
| 1fb91a9059 | |||
| dff3a8ae8c | |||
| 43a03968b9 | |||
| 10c19f5315 | |||
| ed00b190a4 | |||
| 6fe827f389 | |||
| 3766f7484f | |||
| 73599c2843 | |||
| 3c3949bbf8 | |||
| 7dba4aa16a | |||
| 97c190b598 | |||
| 00fe90d586 | |||
| 43334b320e | |||
| b874764e4e | |||
| d189c2e71a | |||
| 35817df622 | |||
| 4ef533d9b4 | |||
| 0a35f32815 | |||
| b8778dc44f | |||
| 6d07191967 | |||
| 13917ed430 | |||
| c7b691e13d | |||
| 65f4c033cc | |||
| e1b65cff69 | |||
| 830c665bd4 | |||
| e241cf1e88 | |||
| 32916d535b | |||
| d139358fc0 | |||
| 74d4ae0bbe | |||
| a873e15a39 | |||
| 5cd9132e60 | |||
| f62cf51cbe | |||
| de925cd70e | |||
| 1acef83112 | |||
| 0baf1d36c4 | |||
| 179942adc2 | |||
| d3b3552b74 | |||
| 9f8151f0eb | |||
| e3a15c15e2 | |||
| a027e30bb4 | |||
| 46556024dd | |||
| 892c5938a3 | |||
| 44431c9d6c | |||
| 78bf1bf4e4 | |||
| 09348e7fb1 | |||
| 8e4b70a6bb | |||
| cc4aa7a6fc | |||
| 5dfdccaf26 | |||
| cdf005c7b3 | |||
| ed2af44103 | |||
| 95863871b7 | |||
| 50636074a3 | |||
| 8d09a65701 | |||
| aaa3ff7463 | |||
| 218fbfa4f0 | |||
| 9da204bb68 | |||
| 1e1a8d04e0 | |||
| f36b202cc4 | |||
| 8a84068d38 | |||
| 35bf804f9d | |||
| 83921824ae | |||
| 32ba1153cd | |||
| 5f362fe1b9 | |||
| 7990d2fcfc | |||
| b759e2a303 | |||
| b3306c0951 | |||
| 391ff1463a | |||
| 5e52fb6463 | |||
| 77248351a1 | |||
| 0cd2bbf781 | |||
| aadf76daef | |||
| b279cccd61 | |||
| 929aa6c4c8 | |||
| 14074a857a | |||
| ec4fbe0bce | |||
| 27f0ae08d4 | |||
| f5affdc5ff | |||
| 70ebb4a080 | |||
| c79efa4e18 | |||
| 6f18eef8ac | |||
| 792655b5ef | |||
| 84d9241856 | |||
| a1e9955527 | |||
| 516be97fa2 | |||
| 7d1945b5e4 | |||
| 785a677682 | |||
| 235f53e82c | |||
| 19381f6327 | |||
| 1aa3646014 | |||
| 8b89ed5764 | |||
| 69d2646336 | |||
| d460fea5bb | |||
| 8d9dff3951 | |||
| 42323ccc7f | |||
| 4af3aaf818 | |||
| c01bed6ab1 | |||
| b8123642ef | |||
| a0867144b4 | |||
| fc699603a1 | |||
| 867acd7a37 | |||
| ca82b43736 | |||
| 5a345ed639 | |||
| bd74826a00 | |||
| 0b0154aa54 | |||
| 59ec74d316 | |||
| ac1d1d5f00 | |||
| 7d065a5a6d | |||
| 838ea7cfd3 | |||
| 8b2f080d82 | |||
| a6ef9853dc | |||
| 92be2c806b | |||
| 23ac94800a | |||
| c80fe52994 | |||
| 7ac12d04a8 | |||
| 9ab7fb560a | |||
| 61471c8fc5 | |||
| 5d9994013f | |||
| 0dda151184 | |||
| 5397fbabb7 | |||
| f0206bba50 | |||
| d3be34ebe9 | |||
| bef5080125 | |||
| bde97c32c5 | |||
| c749d458fe | |||
| f5e3b5ce78 | |||
| 5604c03626 | |||
| 968dc5ea37 | |||
| 528f8c5e94 | |||
| 7c6d6a04e6 | |||
| b9fe5fe90e | |||
| 34c302c90d | |||
| be6f03f9b7 | |||
| 7a0cd25c6b | |||
| a86e2f774f | |||
| 8bac730846 | |||
| 0cf88aa345 | |||
| 712a1dfc71 | |||
| 880b251945 | |||
| 07d71c1c78 | |||
| f4c3d3f9a4 | |||
| 374f7e150f | |||
| 4ef0b40b3c | |||
| 3bc2539ebb | |||
| 2cc2017e08 | |||
| e2a873ed59 | |||
| 3ba28dfeac | |||
| 0160d3ceab | |||
| 319c53f43a | |||
| 3027ae4add | |||
| 1a974ae8ff | |||
| a2072cb392 | |||
| 56d277747d | |||
| 5cb47a3a11 | |||
| 6506ebe91f | |||
| ee63f345ed | |||
| 51af98c7e9 | |||
| 42f8183ff5 | |||
| d522fc729d | |||
| 225180beb9 | |||
| ef712ca0ba | |||
| 3e88731742 | |||
| 76ae6dff12 | |||
| db2bed371e | |||
| 92470542ec | |||
| 2296ae90f9 | |||
| 8debddbb19 | |||
| 6008bae389 | |||
| 9a2435d07b | |||
| 4f68f4b6da | |||
| 48920f7c95 | |||
| abe74ba364 | |||
| 7c76719802 | |||
| e851e27ee0 | |||
| d646fbd1c2 | |||
| 4f42802084 | |||
| 8811819ed3 | |||
| 3eaa682485 | |||
| d0bbe1d87d | |||
| b593a3c573 | |||
| ee63182514 | |||
| 28790a305d | |||
| 03bbac3d94 | |||
| 3fb3fff984 | |||
| 66fda0b1ba | |||
| c0de26787c | |||
| 08c71092fc | |||
| fa328dca21 | |||
| 7f5f201278 | |||
| e1b808792a | |||
| 3f070cfa4d | |||
| 6608bcd98e | |||
| 5b8191ee50 | |||
| 2925f6b5cf | |||
| 3ce45baeed | |||
| 2a9ca24da4 | |||
| 9e3bac35b8 | |||
| 4dc48d3bd0 | |||
| 920153e8aa | |||
| f0079b6918 | |||
| 522e6f7400 | |||
| a160574f92 | |||
| 8bcea5ad07 | |||
| 2e91f3a578 | |||
| db3c80d5b3 | |||
| 577b93a330 | |||
| 0c7369d68e | |||
| 88810dd92b | |||
| 8a7d803560 | |||
| 859728b1e1 | |||
| 10c5b174c1 | |||
| f274e3a7b3 | |||
| e6784fb96a | |||
| d32f7b404d | |||
| 9d71d863be | |||
| a03ad3c8e5 | |||
| b5cea63e7c | |||
| 1244e32329 | |||
| 66a477aa43 | |||
| 8147d89cbb | |||
| 2a9881d315 | |||
| 5d35207d34 | |||
| 1ef41025d6 | |||
| d9eb102135 | |||
| 26610c5a05 | |||
| 4dc7792d63 | |||
| 5ccf23a61e | |||
| c8cc3c48f3 | |||
| 5e36ab2e41 | |||
| 362f93b0be | |||
| 70004ab833 | |||
| 66274758fb | |||
| 7811df4f62 | |||
| 697e4b4dbb | |||
| 4e60a2e0b4 | |||
| 3519b0bbef | |||
| f8a3771f7d | |||
| 1319cbf5a5 | |||
| 809c864024 | |||
| 005b29901d | |||
| daef5653d9 | |||
| ecdbfd9d22 | |||
| 63b6bf8768 | |||
| 92bc541b08 | |||
| 5f6897f81c | |||
| a2df8fd353 | |||
| 5262415589 | |||
| c36d6449ee | |||
| 3d2fa57574 | |||
| c4a0290cd7 | |||
| fc34ce75a7 | |||
| 4fae6658f3 | |||
| cd619c6e04 | |||
| 341fa45e89 | |||
| 23ad8dc843 | |||
| 68756ec570 | |||
| ba43b805d4 | |||
| 06b8142391 | |||
| 18c31a6a53 | |||
| a3aad2adf8 | |||
| 8fba4e7c3d | |||
| 7dbd99ade1 | |||
| a76a79b49c | |||
| bc132a1e5b | |||
| 0fdefa75ca | |||
| ac91f28deb | |||
| 7f63ba3d2f | |||
| 064946b9e1 | |||
| b1743893ad | |||
| 6f0e1b47a0 | |||
| c42f1017c3 | |||
| 4bb13e3fbd | |||
| a39ea2f6ca | |||
| 950730f5a9 | |||
| 7375b5fe58 | |||
| 33581716cb | |||
| 2b27a798be | |||
| 4eb54ae62a | |||
| 1aa6382274 | |||
| 5ca34eaca3 | |||
| 6b359c44ce | |||
| 151c808c59 | |||
| 401ff59483 | |||
| d5e0202bf6 | |||
| 10c1f5f7b6 | |||
| ba269af964 | |||
| 51f4091388 | |||
| aee58c0f5a | |||
| b607d4181b | |||
| 0fb6523941 | |||
| 013924b699 | |||
| d5c38b390b | |||
| 9f3e43f8ef | |||
| 2a32530f47 | |||
| 39473e1490 | |||
| a6d5b885e3 | |||
| 8173681e89 | |||
| b07ed5302b | |||
| c23a0ef66f | |||
| 3e7439a067 | |||
| 760d398dee | |||
| e9cb01d723 | |||
| 095c01d98f | |||
| edb53bb6b7 | |||
| ed1d3acbe9 | |||
| 8d3900e423 | |||
| f7212d2895 | |||
| 8e29ee1a1b | |||
| ba27e29e02 | |||
| ff6d081829 | |||
| a56a3f6f45 | |||
| b245de09b8 | |||
| c766ac5c66 | |||
| 13fb62b9a1 | |||
| 6b6367220b | |||
| f8b2bd1cde | |||
| 8f54dd9fe9 | |||
| b94a7e4fbe | |||
| 066b9c59ac | |||
| bc041ef97b | |||
| 1281e7eecf | |||
| 67c11ac20b | |||
| 8c8f9a56d2 | |||
| 6437669abf | |||
| 587a2af752 | |||
| 91e0380b3d | |||
| 43d8ceec87 | |||
| f3f9d8b7a4 | |||
| 54e5874277 | |||
| 655fc4e488 | |||
| c7e7ed01c5 | |||
| f3821a4882 | |||
| f2906c5923 | |||
| ca87e6e523 | |||
| 037a35ba80 | |||
| 3cb2e7f079 | |||
| 6a8869520e | |||
| a335eca50e | |||
| a26f2a2528 | |||
| b5e3f79b02 | |||
| 84507f1403 | |||
| 61e1d3fd2b | |||
| ac6fa96eeb | |||
| c002f7499c | |||
| e2ca60cbf9 | |||
| 88e97f410d | |||
| 2a31162223 | |||
| 22051a3d32 | |||
| 85c6749c46 | |||
| bbd3c3e37b | |||
| 4dac035b92 | |||
| 6c2a87508e | |||
| db487fdd57 | |||
| 2d2442f9ae | |||
| 7072493fe2 | |||
| 57ba1053f5 | |||
| 0144e1e440 | |||
| cf88c15a1b | |||
| e043d02078 | |||
| c47b1afd84 | |||
| 42b471c7fe | |||
| 2093b9f651 | |||
| 788c82ba6f | |||
| ac0b3d01ac | |||
| 5c16823219 | |||
| bdf0020f9d | |||
| 3f89873f19 | |||
| 8cdb543b05 | |||
| e28a2cc4dc | |||
| eae854a9b5 | |||
| 512364e46d | |||
| f01529627f | |||
| 222c343df8 | |||
| f809f8d069 | |||
| b28f3bea96 | |||
| a97a072be4 | |||
| 67ea51a256 | |||
| 551934f76b | |||
| 770deb1fb5 | |||
| 61dce0abac | |||
| 00302af06d | |||
| 1a63ed6722 | |||
| e6e90d30bc | |||
| 920d82e151 | |||
| d7fddb49d3 | |||
| 07504255f6 | |||
| 17f5ce7342 | |||
| ea0a028128 | |||
| f8b07e5f38 | |||
| 4c8ce365e0 | |||
| d22e05765f |
@@ -1,309 +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: MokoStandards.Templates.GitHub
|
|
||||||
INGROUP: MokoStandards.Templates
|
|
||||||
REPO: https://github.com/mokoconsulting-tech/MokoStandards
|
|
||||||
PATH: /templates/github/copilot-instructions.md.template
|
|
||||||
VERSION: XX.YY.ZZ
|
|
||||||
BRIEF: Standard GitHub Copilot custom instructions template for Moko Consulting governed repositories
|
|
||||||
NOTE: Synced to .github/copilot-instructions.md in all governed repositories via bulk sync.
|
|
||||||
Tokens replaced at sync time: MokoStandards-Template-Generic, https://github.com/mokoconsulting-tech/MokoStandards-Template-Generic, PHP, Generic project
|
|
||||||
-->
|
|
||||||
|
|
||||||
> [!IMPORTANT]
|
|
||||||
> **🔧 AI Self-Update Required on First Use**
|
|
||||||
>
|
|
||||||
> This file may contain unfilled `{{TOKEN}}` placeholders. The MokoStandards bulk-sync system
|
|
||||||
> replaces them automatically at sync time; if you can still see them, sync has not yet run for
|
|
||||||
> this repository — or this is a brand-new repo.
|
|
||||||
>
|
|
||||||
> **On your very first interaction in this repository, before answering any other question:**
|
|
||||||
>
|
|
||||||
> 1. Check whether any `{{TOKEN}}` placeholders remain in this file.
|
|
||||||
> 2. If they do, locate the values as described in the table below and replace every occurrence
|
|
||||||
> in **both** `.github/copilot-instructions.md` and `.github/CLAUDE.md`.
|
|
||||||
> 3. After replacing all tokens, **delete this entire notice block** (from `> [!IMPORTANT]`
|
|
||||||
> through the closing `> ---` line) in both files.
|
|
||||||
> 4. Tell the developer: *"I've self-updated the AI context files with this repository's
|
|
||||||
> values — all set."*
|
|
||||||
>
|
|
||||||
> | Placeholder | Where to find the value |
|
|
||||||
> |---|---|
|
|
||||||
> | `MokoStandards-Template-Generic` | The GitHub repository name (visible in the URL, `README.md` heading, or `git remote -v`) |
|
|
||||||
> | `https://github.com/mokoconsulting-tech/MokoStandards-Template-Generic` | Full GitHub URL, e.g. `https://github.com/mokoconsulting-tech/<repo-name>` |
|
|
||||||
> | `PHP` | The dominant programming language (check file extensions in the repository) |
|
|
||||||
> | `Generic project` | The project type: `PHP library`, `Joomla extension`, `Dolibarr module`, `WaaS site`, etc. — infer from repo structure |
|
|
||||||
>
|
|
||||||
> ---
|
|
||||||
|
|
||||||
# MokoStandards-Template-Generic — GitHub Copilot Custom Instructions
|
|
||||||
|
|
||||||
## What This Repo Is
|
|
||||||
|
|
||||||
This is a **Moko Consulting** repository governed by [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards). All coding standards, workflows, and policies are defined there and enforced here via bulk sync.
|
|
||||||
|
|
||||||
Repository URL: https://github.com/mokoconsulting-tech/MokoStandards-Template-Generic
|
|
||||||
Primary language: **PHP**
|
|
||||||
Platform type: **Generic project**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Primary Language
|
|
||||||
|
|
||||||
**PHP is the primary language for this repository.** Follow the conventions documented in [MokoStandards coding-style-guide](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/coding-style-guide.md).
|
|
||||||
|
|
||||||
YAML uses 2-space indentation (spaces, not tabs). All other text files use tabs per `.editorconfig`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## File Header — Always Required on New Files
|
|
||||||
|
|
||||||
Every new file needs a copyright header as its first content. Use the minimal form unless the file is a policy doc, README, or public API.
|
|
||||||
|
|
||||||
**PHP:**
|
|
||||||
```php
|
|
||||||
<?php
|
|
||||||
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
*
|
|
||||||
* This file is part of a Moko Consulting project.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* FILE INFORMATION
|
|
||||||
* DEFGROUP: MokoStandards-Template-Generic.Module
|
|
||||||
* INGROUP: MokoStandards-Template-Generic
|
|
||||||
* REPO: https://github.com/mokoconsulting-tech/MokoStandards-Template-Generic
|
|
||||||
* PATH: /path/to/file.php
|
|
||||||
* VERSION: XX.YY.ZZ
|
|
||||||
* BRIEF: One-line description of purpose
|
|
||||||
*/
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
```
|
|
||||||
|
|
||||||
**Markdown:**
|
|
||||||
```markdown
|
|
||||||
<!--
|
|
||||||
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: MokoStandards-Template-Generic.Documentation
|
|
||||||
INGROUP: MokoStandards-Template-Generic
|
|
||||||
REPO: https://github.com/mokoconsulting-tech/MokoStandards-Template-Generic
|
|
||||||
PATH: /docs/file.md
|
|
||||||
VERSION: XX.YY.ZZ
|
|
||||||
BRIEF: One-line description
|
|
||||||
-->
|
|
||||||
```
|
|
||||||
|
|
||||||
**YAML / Shell:** Use `#` comments with the same fields. JSON files are exempt.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Version Management
|
|
||||||
|
|
||||||
**`README.md` is the single source of truth for the repository version.**
|
|
||||||
|
|
||||||
- **Bump the patch version on every PR** — increment `XX.YY.ZZ` (e.g. `01.02.03` → `01.02.04`) in `README.md` before opening the PR; the `sync-version-on-merge` workflow propagates it automatically to all badges and `FILE INFORMATION` headers on merge to `main`.
|
|
||||||
- The `VERSION: XX.YY.ZZ` field in the README.md `FILE INFORMATION` block governs all other version references.
|
|
||||||
- Update the version in `README.md` only — the `sync-version-on-merge` workflow propagates it automatically to all badges and `FILE INFORMATION` headers on merge to `main`.
|
|
||||||
- Version format is zero-padded semver: `XX.YY.ZZ` (e.g. `04.00.04`).
|
|
||||||
- Never hardcode a specific version in document body text — use the badge or FILE INFORMATION header only.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## GitHub Actions — Token Usage
|
|
||||||
|
|
||||||
Every workflow must use **`secrets.GH_TOKEN`** (the org-level Personal Access Token). This applies to all `actions/checkout`, `gh` CLI calls, and any step that talks to the GitHub API.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# ✅ Correct
|
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GH_TOKEN }}
|
|
||||||
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
|
||||||
```
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# ❌ Wrong — never use these in workflows
|
|
||||||
token: ${{ github.token }}
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
```
|
|
||||||
|
|
||||||
PHP scripts read the token with: `getenv('GH_TOKEN') ?: getenv('GITHUB_TOKEN')` — `GH_TOKEN` is always preferred; `GITHUB_TOKEN` is accepted only as a local-dev fallback.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Composer Package (PHP repositories)
|
|
||||||
|
|
||||||
This repository requires the MokoStandards enterprise library. The `composer.json` must include:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"repositories": [
|
|
||||||
{
|
|
||||||
"type": "vcs",
|
|
||||||
"url": "https://github.com/mokoconsulting-tech/MokoStandards"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"require": {
|
|
||||||
"mokoconsulting/mokostandards": "^4.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Run `composer install` after adding the dependency. See [package-installation.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/guide/package-installation.md) for full instructions.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PHP Script Pattern
|
|
||||||
|
|
||||||
All PHP scripts **must** extend `MokoStandards\Enterprise\CliFramework`. Never write standalone classes or extend the legacy `CliBase`.
|
|
||||||
|
|
||||||
```php
|
|
||||||
#!/usr/bin/env php
|
|
||||||
<?php
|
|
||||||
/* … file header … */
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
require_once __DIR__ . '/vendor/autoload.php';
|
|
||||||
|
|
||||||
use MokoStandards\Enterprise\CliFramework;
|
|
||||||
|
|
||||||
class MyScript extends CliFramework
|
|
||||||
{
|
|
||||||
protected function configure(): void
|
|
||||||
{
|
|
||||||
$this->setDescription('One-line description');
|
|
||||||
$this->addArgument('--path', 'Repository root', '.');
|
|
||||||
$this->addArgument('--dry-run', 'Preview without writing', false);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function run(): int
|
|
||||||
{
|
|
||||||
$path = $this->getArgument('--path');
|
|
||||||
$dryRun = (bool) $this->getArgument('--dry-run');
|
|
||||||
|
|
||||||
$this->log('INFO', "Processing: {$path}");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$script = new MyScript('my_script', 'One-line description');
|
|
||||||
exit($script->execute());
|
|
||||||
```
|
|
||||||
|
|
||||||
**Key rules:**
|
|
||||||
- Abstract methods to implement: `configure()` and `run()` — **not** `execute()`
|
|
||||||
- `execute()` is the **public entry point** that orchestrates setup (arg parsing, `initialize()`) and then calls your `run()` implementation; call it at the bottom with `exit($script->execute())`
|
|
||||||
- Entry point at the bottom: `$script->execute()` — **not** `$script->run()`
|
|
||||||
- Constructor always takes `(string $name, string $description = '')`; pass the description here — `setDescription()` inside `configure()` is only needed to override it
|
|
||||||
- `log(string $level, string $message)` — level is the **first** argument (INFO / SUCCESS / WARNING / ERROR)
|
|
||||||
- `$this->dryRun` and `$this->verbose` are set automatically from `--dry-run` / `--verbose`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Naming Conventions
|
|
||||||
|
|
||||||
| Context | Convention | Example |
|
|
||||||
|---------|-----------|---------|
|
|
||||||
| PHP class | `PascalCase` | `MyService` |
|
|
||||||
| PHP method / function | `camelCase` | `getUserData()` |
|
|
||||||
| PHP variable | `$snake_case` | `$repo_path` |
|
|
||||||
| PHP constant | `UPPER_SNAKE_CASE` | `DEFAULT_THRESHOLD` |
|
|
||||||
| PHP class file | `PascalCase.php` | `ApiClient.php` |
|
|
||||||
| PHP script file | `snake_case.php` | `check_health.php` |
|
|
||||||
| YAML workflow | `kebab-case.yml` | `bulk-repo-sync.yml` |
|
|
||||||
| Markdown doc | `kebab-case.md` | `coding-style-guide.md` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Commit Messages
|
|
||||||
|
|
||||||
Format: `<type>(<scope>): <subject>` — imperative, lower-case subject, no trailing period.
|
|
||||||
|
|
||||||
Valid types: `feat` · `fix` · `docs` · `chore` · `ci` · `refactor` · `style` · `test` · `perf` · `revert` · `build`
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
- `feat(module): add user preference caching`
|
|
||||||
- `fix(api): handle null response from external service`
|
|
||||||
- `docs(readme): update installation instructions`
|
|
||||||
- `chore(deps): bump phpunit to 11.x`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Branch Naming
|
|
||||||
|
|
||||||
Approved prefixes: `dev/` · `rc/` · `version/` · `copilot/` · `dependabot/`
|
|
||||||
|
|
||||||
- `dev/XX.YY` or `dev/feature-name` — development (version optional)
|
|
||||||
- `rc/XX.YY.ZZ` — release candidate (three-part required)
|
|
||||||
- `version/XX.YY` — archive branch (auto-created, two-part)
|
|
||||||
- Release tags: `vXX` (major only — one release per major version)
|
|
||||||
- Patch `00` = development (no release), first release = `01`
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
- ✅ `dev/04.06` · `dev/new-dashboard` · `rc/04.06.01`
|
|
||||||
- ❌ `feature/my-thing` — rejected by branch protection
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Keeping Documentation Current
|
|
||||||
|
|
||||||
Whenever you make code changes, update the corresponding documentation in the same commit or PR. Do not leave docs stale.
|
|
||||||
|
|
||||||
| Change type | Documentation to update |
|
|
||||||
|-------------|------------------------|
|
|
||||||
| New or renamed public PHP method | PHPDoc block on the method; `docs/api/` index for that class |
|
|
||||||
| New or changed CLI script argument | Script's own `--help` text; `docs/api/` or equivalent |
|
|
||||||
| New or changed GitHub Actions workflow | `docs/workflows/<workflow-name>.md` |
|
|
||||||
| New or changed policy | Corresponding file under `docs/policy/` |
|
|
||||||
| New library class or major feature | `CHANGELOG.md` entry under `Added` |
|
|
||||||
| Bug fix | `CHANGELOG.md` entry under `Fixed` |
|
|
||||||
| Breaking change | `CHANGELOG.md` entry under `Changed`; update `CONTRIBUTING.md` if contributor steps change |
|
|
||||||
| Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block |
|
|
||||||
| **Every PR** | **Bump the patch version** — increment `XX.YY.ZZ` in `README.md`; `sync-version-on-merge` propagates it to all headers and badges on merge |
|
|
||||||
|
|
||||||
If your code change makes any existing doc sentence false or incomplete, fix the doc before closing the PR.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Key Constraints
|
|
||||||
|
|
||||||
- Never commit directly to `main` — all changes go via PR, squash-merged
|
|
||||||
- Never skip the FILE INFORMATION block on a new file
|
|
||||||
- Never use bare `catch (\Throwable $e) {}` without logging or re-throwing
|
|
||||||
- Never hardcode version numbers in body text — update `README.md` and let automation propagate
|
|
||||||
- Never use `github.token` or `secrets.GITHUB_TOKEN` in workflows — always use `secrets.GH_TOKEN`
|
|
||||||
- Never extend `CliBase` in PHP scripts — extend `MokoStandards\Enterprise\CliFramework`
|
|
||||||
- Never call `$script->run()` as the entry point — call `$script->execute()`
|
|
||||||
- Policy documents and guides must not be mixed
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## MokoStandards Reference
|
|
||||||
|
|
||||||
This repository is governed by [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards). Authoritative policies:
|
|
||||||
|
|
||||||
| Document | Purpose |
|
|
||||||
|----------|---------|
|
|
||||||
| [file-header-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/file-header-standards.md) | Copyright-header rules for every file type |
|
|
||||||
| [coding-style-guide.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/coding-style-guide.md) | Naming and formatting conventions |
|
|
||||||
| [branching-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/branching-strategy.md) | Branch naming, hierarchy, and release workflow |
|
|
||||||
| [merge-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/merge-strategy.md) | Squash-merge policy and PR title/body conventions |
|
|
||||||
| [changelog-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/changelog-standards.md) | How and when to update CHANGELOG.md |
|
|
||||||
| [scripting-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/scripting-standards.md) | PHP script requirements and CliFramework usage |
|
|
||||||
| [package-installation.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/guide/package-installation.md) | Installing `mokoconsulting/mokostandards` via Composer |
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# CODEOWNERS — require approval from jmiller-moko for protected paths
|
|
||||||
# Synced from MokoStandards. Do not edit manually.
|
|
||||||
#
|
|
||||||
# Changes to these paths require review from the listed owners before merge.
|
|
||||||
# Combined with branch protection (require PR reviews), this prevents
|
|
||||||
# unauthorized modifications to workflows, configs, and governance files.
|
|
||||||
|
|
||||||
# ── Synced workflows (managed by MokoStandards — do not edit manually) ────
|
|
||||||
/.github/workflows/deploy-dev.yml @jmiller-moko
|
|
||||||
/.github/workflows/deploy-demo.yml @jmiller-moko
|
|
||||||
/.github/workflows/deploy-manual.yml @jmiller-moko
|
|
||||||
/.github/workflows/auto-release.yml @jmiller-moko
|
|
||||||
/.github/workflows/auto-dev-issue.yml @jmiller-moko
|
|
||||||
/.github/workflows/auto-assign.yml @jmiller-moko
|
|
||||||
/.github/workflows/sync-version-on-merge.yml @jmiller-moko
|
|
||||||
/.github/workflows/enterprise-firewall-setup.yml @jmiller-moko
|
|
||||||
/.github/workflows/repository-cleanup.yml @jmiller-moko
|
|
||||||
/.github/workflows/standards-compliance.yml @jmiller-moko
|
|
||||||
/.github/workflows/codeql-analysis.yml @jmiller-moko
|
|
||||||
/.github/workflows/repo_health.yml @jmiller-moko
|
|
||||||
/.github/workflows/ci-joomla.yml @jmiller-moko
|
|
||||||
/.github/workflows/update-server.yml @jmiller-moko
|
|
||||||
/.github/workflows/deploy-manual.yml @jmiller-moko
|
|
||||||
/.github/workflows/ci-dolibarr.yml @jmiller-moko
|
|
||||||
/.github/workflows/publish-to-mokodolimods.yml @jmiller-moko
|
|
||||||
/.github/workflows/changelog-validation.yml @jmiller-moko
|
|
||||||
# Custom workflows in .github/workflows/ not listed above are repo-owned.
|
|
||||||
|
|
||||||
# ── GitHub configuration ─────────────────────────────────────────────────
|
|
||||||
/.github/ISSUE_TEMPLATE/ @jmiller-moko
|
|
||||||
/.github/CODEOWNERS @jmiller-moko
|
|
||||||
/.github/copilot.yml @jmiller-moko
|
|
||||||
/.github/copilot-instructions.md @jmiller-moko
|
|
||||||
/.github/CLAUDE.md @jmiller-moko
|
|
||||||
/.github/.mokostandards @jmiller-moko
|
|
||||||
|
|
||||||
# ── Build and config files ───────────────────────────────────────────────
|
|
||||||
/composer.json @jmiller-moko
|
|
||||||
/phpstan.neon @jmiller-moko
|
|
||||||
/Makefile @jmiller-moko
|
|
||||||
/.ftpignore @jmiller-moko
|
|
||||||
/.gitignore @jmiller-moko
|
|
||||||
/.gitattributes @jmiller-moko
|
|
||||||
/.editorconfig @jmiller-moko
|
|
||||||
|
|
||||||
# ── Governance documents ─────────────────────────────────────────────────
|
|
||||||
/LICENSE @jmiller-moko
|
|
||||||
/CONTRIBUTING.md @jmiller-moko
|
|
||||||
/SECURITY.md @jmiller-moko
|
|
||||||
/GOVERNANCE.md @jmiller-moko
|
|
||||||
/CODE_OF_CONDUCT.md @jmiller-moko
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
---
|
|
||||||
name: License Request
|
|
||||||
about: Request an organization license for Sublime Text
|
|
||||||
title: '[LICENSE REQUEST] Sublime Text - [Your Name]'
|
|
||||||
labels: ['license-request', 'admin']
|
|
||||||
assignees: []
|
|
||||||
---
|
|
||||||
|
|
||||||
<!--
|
|
||||||
SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
Copyright (C) 2024-2026 Moko Consulting Tech
|
|
||||||
|
|
||||||
File: .github/ISSUE_TEMPLATE/request-license.md
|
|
||||||
Description: Issue template for organization software license requests
|
|
||||||
Project: .github-private
|
|
||||||
Author: Moko Consulting Tech
|
|
||||||
Version: 03.02.00
|
|
||||||
|
|
||||||
Revision History:
|
|
||||||
- 2026-03-11: Added SPDX header and version to match MokoStandards 03.02.00
|
|
||||||
-->
|
|
||||||
|
|
||||||
## License Request
|
|
||||||
|
|
||||||
### Tool Information
|
|
||||||
**Tool Name**: Sublime Text
|
|
||||||
|
|
||||||
**License Type Requested**: Organization Pool
|
|
||||||
|
|
||||||
**Personal Purchase**:
|
|
||||||
- [ ] I prefer to purchase my own license ($99 USD - recommended, immediate access)
|
|
||||||
- [ ] I prefer an organization license (1-2 business days, organization use only)
|
|
||||||
- [ ] I have already purchased my own license (registration only for support)
|
|
||||||
|
|
||||||
### Requestor Information
|
|
||||||
**Name**:
|
|
||||||
**GitHub Username**: @
|
|
||||||
**Email**: @mokoconsulting.tech
|
|
||||||
**Team/Department**:
|
|
||||||
**Manager**: @
|
|
||||||
|
|
||||||
### Justification
|
|
||||||
**Why do you need this license?**
|
|
||||||
|
|
||||||
**Primary use case**:
|
|
||||||
- [ ] Remote development (SFTP to servers)
|
|
||||||
- [ ] Local development
|
|
||||||
- [ ] Code review
|
|
||||||
- [ ] Documentation editing
|
|
||||||
- [ ] Other (specify):
|
|
||||||
|
|
||||||
**Which projects/repositories will you work on?**
|
|
||||||
|
|
||||||
**Have you evaluated the free trial?**
|
|
||||||
- [ ] Yes, I've used the trial and Sublime Text meets my needs
|
|
||||||
- [ ] No, requesting license before trial
|
|
||||||
|
|
||||||
**Alternative tools considered**:
|
|
||||||
- [ ] VS Code (free alternative)
|
|
||||||
- [ ] Vim/Neovim (free, terminal-based)
|
|
||||||
- [ ] Other: _______________
|
|
||||||
|
|
||||||
### Platform
|
|
||||||
- [ ] Windows
|
|
||||||
- [ ] macOS
|
|
||||||
- [ ] Linux (distribution: ________)
|
|
||||||
|
|
||||||
### Urgency
|
|
||||||
- [ ] Urgent (needed within 24 hours - please justify)
|
|
||||||
- [ ] Normal (1-2 business days)
|
|
||||||
- [ ] Low priority (when available)
|
|
||||||
|
|
||||||
**If urgent, please explain why:**
|
|
||||||
|
|
||||||
### SFTP Plugin
|
|
||||||
**Note**: Sublime SFTP plugin ($16 USD) is a **separate personal purchase** and is NOT provided by the organization.
|
|
||||||
|
|
||||||
- [ ] I understand SFTP plugin requires separate personal purchase
|
|
||||||
- [ ] I have already purchased SFTP plugin
|
|
||||||
- [ ] I will purchase SFTP plugin if needed for my work
|
|
||||||
- [ ] I don't need SFTP plugin (local development only)
|
|
||||||
|
|
||||||
### Acknowledgments
|
|
||||||
- [ ] I have read the License Management Policy (/docs/github-private/LICENSE_MANAGEMENT.md)
|
|
||||||
- [ ] I understand organization licenses are for work use only
|
|
||||||
- [ ] I understand organization licenses must be returned upon leaving
|
|
||||||
- [ ] I understand personal purchases ($99) are an alternative with lifetime access
|
|
||||||
- [ ] I understand SFTP plugin ($16) requires separate personal purchase
|
|
||||||
- [ ] I agree to the terms of use
|
|
||||||
|
|
||||||
### Additional Information
|
|
||||||
|
|
||||||
**Expected daily usage hours**: _____ hours/day
|
|
||||||
|
|
||||||
**Duration of need**:
|
|
||||||
- [ ] Permanent (ongoing role)
|
|
||||||
- [ ] Temporary project (_____ months)
|
|
||||||
- [ ] Trial/Evaluation (_____ weeks)
|
|
||||||
|
|
||||||
**Comments/Questions**:
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## For Admin Use Only
|
|
||||||
|
|
||||||
**Do not edit below this line**
|
|
||||||
|
|
||||||
- [ ] Manager approval received (@manager-username)
|
|
||||||
- [ ] License available in pool (current: __/20)
|
|
||||||
- [ ] License type confirmed (Organization / Personal registration)
|
|
||||||
- [ ] License key sent via encrypted email
|
|
||||||
- [ ] Activation confirmed by user
|
|
||||||
- [ ] Added to license tracking sheet
|
|
||||||
- [ ] User notified of SFTP plugin requirement
|
|
||||||
|
|
||||||
**License Key ID**: _____________
|
|
||||||
**Date Issued**: _____________
|
|
||||||
**Issued By**: @_____________
|
|
||||||
|
|
||||||
**Notes**:
|
|
||||||
@@ -1,309 +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: MokoStandards.Templates.GitHub
|
|
||||||
INGROUP: MokoStandards.Templates
|
|
||||||
REPO: https://github.com/mokoconsulting-tech/MokoStandards
|
|
||||||
PATH: /templates/github/copilot-instructions.md.template
|
|
||||||
VERSION: XX.YY.ZZ
|
|
||||||
BRIEF: Standard GitHub Copilot custom instructions template for Moko Consulting governed repositories
|
|
||||||
NOTE: Synced to .github/copilot-instructions.md in all governed repositories via bulk sync.
|
|
||||||
Tokens replaced at sync time: MokoStandards-Template-Generic, https://github.com/mokoconsulting-tech/MokoStandards-Template-Generic, PHP, Generic project
|
|
||||||
-->
|
|
||||||
|
|
||||||
> [!IMPORTANT]
|
|
||||||
> **🔧 AI Self-Update Required on First Use**
|
|
||||||
>
|
|
||||||
> This file may contain unfilled `{{TOKEN}}` placeholders. The MokoStandards bulk-sync system
|
|
||||||
> replaces them automatically at sync time; if you can still see them, sync has not yet run for
|
|
||||||
> this repository — or this is a brand-new repo.
|
|
||||||
>
|
|
||||||
> **On your very first interaction in this repository, before answering any other question:**
|
|
||||||
>
|
|
||||||
> 1. Check whether any `{{TOKEN}}` placeholders remain in this file.
|
|
||||||
> 2. If they do, locate the values as described in the table below and replace every occurrence
|
|
||||||
> in **both** `.github/copilot-instructions.md` and `.github/CLAUDE.md`.
|
|
||||||
> 3. After replacing all tokens, **delete this entire notice block** (from `> [!IMPORTANT]`
|
|
||||||
> through the closing `> ---` line) in both files.
|
|
||||||
> 4. Tell the developer: *"I've self-updated the AI context files with this repository's
|
|
||||||
> values — all set."*
|
|
||||||
>
|
|
||||||
> | Placeholder | Where to find the value |
|
|
||||||
> |---|---|
|
|
||||||
> | `MokoStandards-Template-Generic` | The GitHub repository name (visible in the URL, `README.md` heading, or `git remote -v`) |
|
|
||||||
> | `https://github.com/mokoconsulting-tech/MokoStandards-Template-Generic` | Full GitHub URL, e.g. `https://github.com/mokoconsulting-tech/<repo-name>` |
|
|
||||||
> | `PHP` | The dominant programming language (check file extensions in the repository) |
|
|
||||||
> | `Generic project` | The project type: `PHP library`, `Joomla extension`, `Dolibarr module`, `WaaS site`, etc. — infer from repo structure |
|
|
||||||
>
|
|
||||||
> ---
|
|
||||||
|
|
||||||
# MokoStandards-Template-Generic — GitHub Copilot Custom Instructions
|
|
||||||
|
|
||||||
## What This Repo Is
|
|
||||||
|
|
||||||
This is a **Moko Consulting** repository governed by [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards). All coding standards, workflows, and policies are defined there and enforced here via bulk sync.
|
|
||||||
|
|
||||||
Repository URL: https://github.com/mokoconsulting-tech/MokoStandards-Template-Generic
|
|
||||||
Primary language: **PHP**
|
|
||||||
Platform type: **Generic project**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Primary Language
|
|
||||||
|
|
||||||
**PHP is the primary language for this repository.** Follow the conventions documented in [MokoStandards coding-style-guide](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/coding-style-guide.md).
|
|
||||||
|
|
||||||
YAML uses 2-space indentation (spaces, not tabs). All other text files use tabs per `.editorconfig`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## File Header — Always Required on New Files
|
|
||||||
|
|
||||||
Every new file needs a copyright header as its first content. Use the minimal form unless the file is a policy doc, README, or public API.
|
|
||||||
|
|
||||||
**PHP:**
|
|
||||||
```php
|
|
||||||
<?php
|
|
||||||
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
*
|
|
||||||
* This file is part of a Moko Consulting project.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* FILE INFORMATION
|
|
||||||
* DEFGROUP: MokoStandards-Template-Generic.Module
|
|
||||||
* INGROUP: MokoStandards-Template-Generic
|
|
||||||
* REPO: https://github.com/mokoconsulting-tech/MokoStandards-Template-Generic
|
|
||||||
* PATH: /path/to/file.php
|
|
||||||
* VERSION: XX.YY.ZZ
|
|
||||||
* BRIEF: One-line description of purpose
|
|
||||||
*/
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
```
|
|
||||||
|
|
||||||
**Markdown:**
|
|
||||||
```markdown
|
|
||||||
<!--
|
|
||||||
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: MokoStandards-Template-Generic.Documentation
|
|
||||||
INGROUP: MokoStandards-Template-Generic
|
|
||||||
REPO: https://github.com/mokoconsulting-tech/MokoStandards-Template-Generic
|
|
||||||
PATH: /docs/file.md
|
|
||||||
VERSION: XX.YY.ZZ
|
|
||||||
BRIEF: One-line description
|
|
||||||
-->
|
|
||||||
```
|
|
||||||
|
|
||||||
**YAML / Shell:** Use `#` comments with the same fields. JSON files are exempt.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Version Management
|
|
||||||
|
|
||||||
**`README.md` is the single source of truth for the repository version.**
|
|
||||||
|
|
||||||
- **Bump the patch version on every PR** — increment `XX.YY.ZZ` (e.g. `01.02.03` → `01.02.04`) in `README.md` before opening the PR; the `sync-version-on-merge` workflow propagates it automatically to all badges and `FILE INFORMATION` headers on merge to `main`.
|
|
||||||
- The `VERSION: XX.YY.ZZ` field in the README.md `FILE INFORMATION` block governs all other version references.
|
|
||||||
- Update the version in `README.md` only — the `sync-version-on-merge` workflow propagates it automatically to all badges and `FILE INFORMATION` headers on merge to `main`.
|
|
||||||
- Version format is zero-padded semver: `XX.YY.ZZ` (e.g. `04.00.04`).
|
|
||||||
- Never hardcode a specific version in document body text — use the badge or FILE INFORMATION header only.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## GitHub Actions — Token Usage
|
|
||||||
|
|
||||||
Every workflow must use **`secrets.GH_TOKEN`** (the org-level Personal Access Token). This applies to all `actions/checkout`, `gh` CLI calls, and any step that talks to the GitHub API.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# ✅ Correct
|
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GH_TOKEN }}
|
|
||||||
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
|
||||||
```
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# ❌ Wrong — never use these in workflows
|
|
||||||
token: ${{ github.token }}
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
```
|
|
||||||
|
|
||||||
PHP scripts read the token with: `getenv('GH_TOKEN') ?: getenv('GITHUB_TOKEN')` — `GH_TOKEN` is always preferred; `GITHUB_TOKEN` is accepted only as a local-dev fallback.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Composer Package (PHP repositories)
|
|
||||||
|
|
||||||
This repository requires the MokoStandards enterprise library. The `composer.json` must include:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"repositories": [
|
|
||||||
{
|
|
||||||
"type": "vcs",
|
|
||||||
"url": "https://github.com/mokoconsulting-tech/MokoStandards"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"require": {
|
|
||||||
"mokoconsulting/mokostandards": "^4.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Run `composer install` after adding the dependency. See [package-installation.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/guide/package-installation.md) for full instructions.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PHP Script Pattern
|
|
||||||
|
|
||||||
All PHP scripts **must** extend `MokoStandards\Enterprise\CliFramework`. Never write standalone classes or extend the legacy `CliBase`.
|
|
||||||
|
|
||||||
```php
|
|
||||||
#!/usr/bin/env php
|
|
||||||
<?php
|
|
||||||
/* … file header … */
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
require_once __DIR__ . '/vendor/autoload.php';
|
|
||||||
|
|
||||||
use MokoStandards\Enterprise\CliFramework;
|
|
||||||
|
|
||||||
class MyScript extends CliFramework
|
|
||||||
{
|
|
||||||
protected function configure(): void
|
|
||||||
{
|
|
||||||
$this->setDescription('One-line description');
|
|
||||||
$this->addArgument('--path', 'Repository root', '.');
|
|
||||||
$this->addArgument('--dry-run', 'Preview without writing', false);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function run(): int
|
|
||||||
{
|
|
||||||
$path = $this->getArgument('--path');
|
|
||||||
$dryRun = (bool) $this->getArgument('--dry-run');
|
|
||||||
|
|
||||||
$this->log('INFO', "Processing: {$path}");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$script = new MyScript('my_script', 'One-line description');
|
|
||||||
exit($script->execute());
|
|
||||||
```
|
|
||||||
|
|
||||||
**Key rules:**
|
|
||||||
- Abstract methods to implement: `configure()` and `run()` — **not** `execute()`
|
|
||||||
- `execute()` is the **public entry point** that orchestrates setup (arg parsing, `initialize()`) and then calls your `run()` implementation; call it at the bottom with `exit($script->execute())`
|
|
||||||
- Entry point at the bottom: `$script->execute()` — **not** `$script->run()`
|
|
||||||
- Constructor always takes `(string $name, string $description = '')`; pass the description here — `setDescription()` inside `configure()` is only needed to override it
|
|
||||||
- `log(string $level, string $message)` — level is the **first** argument (INFO / SUCCESS / WARNING / ERROR)
|
|
||||||
- `$this->dryRun` and `$this->verbose` are set automatically from `--dry-run` / `--verbose`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Naming Conventions
|
|
||||||
|
|
||||||
| Context | Convention | Example |
|
|
||||||
|---------|-----------|---------|
|
|
||||||
| PHP class | `PascalCase` | `MyService` |
|
|
||||||
| PHP method / function | `camelCase` | `getUserData()` |
|
|
||||||
| PHP variable | `$snake_case` | `$repo_path` |
|
|
||||||
| PHP constant | `UPPER_SNAKE_CASE` | `DEFAULT_THRESHOLD` |
|
|
||||||
| PHP class file | `PascalCase.php` | `ApiClient.php` |
|
|
||||||
| PHP script file | `snake_case.php` | `check_health.php` |
|
|
||||||
| YAML workflow | `kebab-case.yml` | `bulk-repo-sync.yml` |
|
|
||||||
| Markdown doc | `kebab-case.md` | `coding-style-guide.md` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Commit Messages
|
|
||||||
|
|
||||||
Format: `<type>(<scope>): <subject>` — imperative, lower-case subject, no trailing period.
|
|
||||||
|
|
||||||
Valid types: `feat` · `fix` · `docs` · `chore` · `ci` · `refactor` · `style` · `test` · `perf` · `revert` · `build`
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
- `feat(module): add user preference caching`
|
|
||||||
- `fix(api): handle null response from external service`
|
|
||||||
- `docs(readme): update installation instructions`
|
|
||||||
- `chore(deps): bump phpunit to 11.x`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Branch Naming
|
|
||||||
|
|
||||||
Approved prefixes: `dev/` · `rc/` · `version/` · `copilot/` · `dependabot/`
|
|
||||||
|
|
||||||
- `dev/XX.YY` or `dev/feature-name` — development (version optional)
|
|
||||||
- `rc/XX.YY.ZZ` — release candidate (three-part required)
|
|
||||||
- `version/XX.YY` — archive branch (auto-created, two-part)
|
|
||||||
- Release tags: `vXX` (major only — one release per major version)
|
|
||||||
- Patch `00` = development (no release), first release = `01`
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
- ✅ `dev/04.06` · `dev/new-dashboard` · `rc/04.06.01`
|
|
||||||
- ❌ `feature/my-thing` — rejected by branch protection
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Keeping Documentation Current
|
|
||||||
|
|
||||||
Whenever you make code changes, update the corresponding documentation in the same commit or PR. Do not leave docs stale.
|
|
||||||
|
|
||||||
| Change type | Documentation to update |
|
|
||||||
|-------------|------------------------|
|
|
||||||
| New or renamed public PHP method | PHPDoc block on the method; `docs/api/` index for that class |
|
|
||||||
| New or changed CLI script argument | Script's own `--help` text; `docs/api/` or equivalent |
|
|
||||||
| New or changed GitHub Actions workflow | `docs/workflows/<workflow-name>.md` |
|
|
||||||
| New or changed policy | Corresponding file under `docs/policy/` |
|
|
||||||
| New library class or major feature | `CHANGELOG.md` entry under `Added` |
|
|
||||||
| Bug fix | `CHANGELOG.md` entry under `Fixed` |
|
|
||||||
| Breaking change | `CHANGELOG.md` entry under `Changed`; update `CONTRIBUTING.md` if contributor steps change |
|
|
||||||
| Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block |
|
|
||||||
| **Every PR** | **Bump the patch version** — increment `XX.YY.ZZ` in `README.md`; `sync-version-on-merge` propagates it to all headers and badges on merge |
|
|
||||||
|
|
||||||
If your code change makes any existing doc sentence false or incomplete, fix the doc before closing the PR.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Key Constraints
|
|
||||||
|
|
||||||
- Never commit directly to `main` — all changes go via PR, squash-merged
|
|
||||||
- Never skip the FILE INFORMATION block on a new file
|
|
||||||
- Never use bare `catch (\Throwable $e) {}` without logging or re-throwing
|
|
||||||
- Never hardcode version numbers in body text — update `README.md` and let automation propagate
|
|
||||||
- Never use `github.token` or `secrets.GITHUB_TOKEN` in workflows — always use `secrets.GH_TOKEN`
|
|
||||||
- Never extend `CliBase` in PHP scripts — extend `MokoStandards\Enterprise\CliFramework`
|
|
||||||
- Never call `$script->run()` as the entry point — call `$script->execute()`
|
|
||||||
- Policy documents and guides must not be mixed
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## MokoStandards Reference
|
|
||||||
|
|
||||||
This repository is governed by [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards). Authoritative policies:
|
|
||||||
|
|
||||||
| Document | Purpose |
|
|
||||||
|----------|---------|
|
|
||||||
| [file-header-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/file-header-standards.md) | Copyright-header rules for every file type |
|
|
||||||
| [coding-style-guide.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/coding-style-guide.md) | Naming and formatting conventions |
|
|
||||||
| [branching-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/branching-strategy.md) | Branch naming, hierarchy, and release workflow |
|
|
||||||
| [merge-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/merge-strategy.md) | Squash-merge policy and PR title/body conventions |
|
|
||||||
| [changelog-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/changelog-standards.md) | How and when to update CHANGELOG.md |
|
|
||||||
| [scripting-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/scripting-standards.md) | PHP script requirements and CliFramework usage |
|
|
||||||
| [package-installation.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/guide/package-installation.md) | Installing `mokoconsulting/mokostandards` via Composer |
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
<!-- 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
|
|
||||||
|
|
||||||
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 (./LICENSE).
|
|
||||||
|
|
||||||
# FILE INFORMATION
|
|
||||||
DEFGROUP:
|
|
||||||
INGROUP: Project.Infrastructure
|
|
||||||
REPO: mokoconsulting-tech/MokoStandards-Template-Generic
|
|
||||||
VERSION: 00.00.01
|
|
||||||
PATH: ./.github/copilot/README.md
|
|
||||||
BRIEF: GitHub Copilot firewall configuration documentation
|
|
||||||
-->
|
|
||||||
|
|
||||||
# GitHub Copilot Firewall Configuration
|
|
||||||
|
|
||||||
This directory contains firewall configuration files for GitHub Copilot coding agent to access enterprise-ready sites and external resources.
|
|
||||||
|
|
||||||
## Files
|
|
||||||
|
|
||||||
### firewall-allowlist.json
|
|
||||||
|
|
||||||
JSON configuration file defining domains and URLs that should be accessible through the firewall. This includes:
|
|
||||||
|
|
||||||
- **License Sources**: gnu.org, opensource.org, apache.org, creativecommons.org
|
|
||||||
- **Standards Organizations**: fsf.org, spdx.org
|
|
||||||
- **Code Repositories**: github.com, raw.githubusercontent.com
|
|
||||||
|
|
||||||
The configuration is organized into categories with priority levels for better management.
|
|
||||||
|
|
||||||
### setup-firewall.sh
|
|
||||||
|
|
||||||
Bash script that reads the `firewall-allowlist.json` configuration and exports the allowlist as an environment variable for GitHub Actions workflows.
|
|
||||||
|
|
||||||
## Usage in GitHub Actions
|
|
||||||
|
|
||||||
To use this firewall configuration in your GitHub Actions workflows, add the following step **before** the Copilot agent runs:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- name: Configure Copilot Firewall
|
|
||||||
run: |
|
|
||||||
bash .github/copilot/setup-firewall.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
This will:
|
|
||||||
1. Read the firewall allowlist configuration
|
|
||||||
2. Export `COPILOT_FIREWALL_ALLOWLIST` environment variable
|
|
||||||
3. Make the domains accessible to the Copilot agent
|
|
||||||
|
|
||||||
## Example Workflow
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
name: Copilot Agent
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [opened, synchronize]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
copilot:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Configure Copilot Firewall
|
|
||||||
run: bash .github/copilot/setup-firewall.sh
|
|
||||||
|
|
||||||
- name: Run Copilot Agent
|
|
||||||
uses: github/copilot-swe-agent@v1
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Adding New Domains
|
|
||||||
|
|
||||||
To add new domains to the allowlist:
|
|
||||||
|
|
||||||
1. Edit `firewall-allowlist.json`
|
|
||||||
2. Add the domain to the `allowlist.domains` array
|
|
||||||
3. Optionally add specific URLs to `allowlist.urls`
|
|
||||||
4. Categorize the domain in the `categories` section
|
|
||||||
5. Commit and push the changes
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"allowlist": {
|
|
||||||
"domains": [
|
|
||||||
"existing-domain.com",
|
|
||||||
"new-domain.com"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Security Considerations
|
|
||||||
|
|
||||||
- Only add trusted domains to the allowlist
|
|
||||||
- Use specific URLs when possible instead of wildcard domains
|
|
||||||
- Review allowlist changes carefully in pull requests
|
|
||||||
- Keep the allowlist minimal - only include necessary domains
|
|
||||||
- Document the purpose of each domain/URL in the categories section
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
If the Copilot agent cannot access a required site:
|
|
||||||
|
|
||||||
1. Check if the domain is in `firewall-allowlist.json`
|
|
||||||
2. Verify the setup script ran in the GitHub Actions workflow
|
|
||||||
3. Check workflow logs for firewall configuration messages
|
|
||||||
4. Ensure the domain format is correct (e.g., `www.example.com` not `http://www.example.com`)
|
|
||||||
5. For wildcard patterns, ensure they follow the correct format (e.g., `*.example.com`)
|
|
||||||
|
|
||||||
## Revision History
|
|
||||||
|
|
||||||
| Date | Version | Author | Notes |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| 2026-01-16 | 0.1.0 | Copilot | Initial firewall configuration setup |
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://docs.github.com/assets/schemas/copilot-firewall-allowlist.json",
|
|
||||||
"version": "1.0",
|
|
||||||
"description": "Firewall allowlist configuration for GitHub Copilot coding agent to access enterprise-ready sites and license sources",
|
|
||||||
"allowlist": {
|
|
||||||
"domains": [
|
|
||||||
"*.gnu.org",
|
|
||||||
"fsf.org",
|
|
||||||
"www.fsf.org",
|
|
||||||
"spdx.org",
|
|
||||||
"www.spdx.org",
|
|
||||||
"opensource.org",
|
|
||||||
"www.opensource.org",
|
|
||||||
"creativecommons.org",
|
|
||||||
"www.creativecommons.org",
|
|
||||||
"apache.org",
|
|
||||||
"www.apache.org",
|
|
||||||
"github.com",
|
|
||||||
"api.github.com",
|
|
||||||
"raw.githubusercontent.com"
|
|
||||||
],
|
|
||||||
"urls": [
|
|
||||||
"https://www.gnu.org/licenses/gpl-3.0.txt",
|
|
||||||
"https://www.gnu.org/licenses/gpl-3.0.html",
|
|
||||||
"https://www.gnu.org/licenses/agpl-3.0.txt",
|
|
||||||
"https://www.gnu.org/licenses/lgpl-3.0.txt",
|
|
||||||
"https://www.apache.org/licenses/LICENSE-2.0.txt",
|
|
||||||
"https://opensource.org/licenses/MIT",
|
|
||||||
"https://spdx.org/licenses/",
|
|
||||||
"https://creativecommons.org/licenses/"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"categories": [
|
|
||||||
{
|
|
||||||
"name": "license-sources",
|
|
||||||
"description": "Official license text sources",
|
|
||||||
"priority": "high",
|
|
||||||
"hosts": [
|
|
||||||
"www.gnu.org",
|
|
||||||
"opensource.org",
|
|
||||||
"spdx.org",
|
|
||||||
"apache.org",
|
|
||||||
"creativecommons.org"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "code-repositories",
|
|
||||||
"description": "Source code and package repositories",
|
|
||||||
"priority": "high",
|
|
||||||
"hosts": [
|
|
||||||
"github.com",
|
|
||||||
"api.github.com",
|
|
||||||
"raw.githubusercontent.com"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "standards-organizations",
|
|
||||||
"description": "Standards and specification sources",
|
|
||||||
"priority": "medium",
|
|
||||||
"hosts": [
|
|
||||||
"fsf.org",
|
|
||||||
"spdx.org"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"notes": [
|
|
||||||
"This configuration allows access to common license sources and enterprise-ready sites",
|
|
||||||
"Domains use wildcard patterns where appropriate (e.g., *.gnu.org)",
|
|
||||||
"Specific license file URLs are explicitly allowlisted",
|
|
||||||
"Categories help organize and prioritize different types of access",
|
|
||||||
"This file should be referenced in GitHub Actions setup steps before firewall initialization"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
# SPDX-LICENSE-IDENTIFIER: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# Setup script for configuring firewall allowlist in GitHub Actions
|
|
||||||
# This script should be run in GitHub Actions setup steps before the firewall is enabled
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
ALLOWLIST_FILE="${SCRIPT_DIR}/firewall-allowlist.json"
|
|
||||||
|
|
||||||
echo "=== GitHub Copilot Firewall Setup ==="
|
|
||||||
echo "Configuring firewall allowlist for enterprise-ready sites..."
|
|
||||||
|
|
||||||
if [ ! -f "$ALLOWLIST_FILE" ]; then
|
|
||||||
echo "ERROR: Allowlist file not found: $ALLOWLIST_FILE"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Read domains from the allowlist configuration
|
|
||||||
if ! DOMAINS=$(jq -r '.allowlist.domains[]' "$ALLOWLIST_FILE" 2>&1); then
|
|
||||||
echo "ERROR: Failed to parse allowlist configuration: $DOMAINS"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$DOMAINS" ]; then
|
|
||||||
echo "WARNING: No domains found in allowlist configuration"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Domains to allowlist:"
|
|
||||||
echo "$DOMAINS" | while read -r domain; do
|
|
||||||
echo " - $domain"
|
|
||||||
done
|
|
||||||
|
|
||||||
# Export environment variable for GitHub Copilot
|
|
||||||
# This tells the Copilot agent which domains are allowed
|
|
||||||
export COPILOT_FIREWALL_ALLOWLIST=$(jq -c '.allowlist.domains' "$ALLOWLIST_FILE")
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "Firewall allowlist configured successfully"
|
|
||||||
echo "Environment variable set: COPILOT_FIREWALL_ALLOWLIST"
|
|
||||||
echo ""
|
|
||||||
echo "To use this in GitHub Actions, add to your workflow:"
|
|
||||||
echo ""
|
|
||||||
echo " - name: Configure Copilot Firewall"
|
|
||||||
echo " run: |"
|
|
||||||
echo " bash .github/copilot/setup-firewall.sh"
|
|
||||||
echo " echo \"COPILOT_FIREWALL_ALLOWLIST=\$COPILOT_FIREWALL_ALLOWLIST\" >> \$GITHUB_ENV"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Optionally output the configuration for GitHub Actions to use
|
|
||||||
if [ "$GITHUB_ACTIONS" = "true" ]; then
|
|
||||||
echo "COPILOT_FIREWALL_ALLOWLIST=${COPILOT_FIREWALL_ALLOWLIST}" >> "$GITHUB_ENV"
|
|
||||||
echo "✓ Firewall allowlist exported to GitHub Actions environment"
|
|
||||||
fi
|
|
||||||
@@ -200,3 +200,5 @@ venv/
|
|||||||
*.coverage
|
*.coverage
|
||||||
hypothesis/
|
hypothesis/
|
||||||
|
|
||||||
|
profile.ps1
|
||||||
|
.mcp.json
|
||||||
|
|||||||
@@ -7,21 +7,6 @@ assignees: ''
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<!--
|
|
||||||
SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
Copyright (C) 2024-2026 Moko Consulting Tech
|
|
||||||
|
|
||||||
File: .github/ISSUE_TEMPLATE/adr.md
|
|
||||||
Description: Issue template for Architecture Decision Records
|
|
||||||
Project: .github-private
|
|
||||||
Author: Moko Consulting Tech
|
|
||||||
Version: 03.02.00
|
|
||||||
|
|
||||||
Revision History:
|
|
||||||
- 2026-01-04: Added MokoStandards compliant header with copyright, file info, and metadata
|
|
||||||
- 2026-03-11: Version bump to 03.02.00 to match MokoStandards
|
|
||||||
- 2024: Initial creation
|
|
||||||
-->
|
|
||||||
|
|
||||||
## ADR Number
|
## ADR Number
|
||||||
ADR-XXXX
|
ADR-XXXX
|
||||||
@@ -7,21 +7,6 @@ assignees: ''
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<!--
|
|
||||||
SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
Copyright (C) 2024-2026 Moko Consulting Tech
|
|
||||||
|
|
||||||
File: .github/ISSUE_TEMPLATE/bug_report.md
|
|
||||||
Description: Issue template for bug reports
|
|
||||||
Project: .github-private
|
|
||||||
Author: Moko Consulting Tech
|
|
||||||
Version: 03.02.00
|
|
||||||
|
|
||||||
Revision History:
|
|
||||||
- 2026-01-04: Added MokoStandards compliant header with copyright, file info, and metadata
|
|
||||||
- 2026-03-11: Version bump to 03.02.00 to match MokoStandards
|
|
||||||
- 2024: Initial creation
|
|
||||||
-->
|
|
||||||
|
|
||||||
## Bug Description
|
## Bug Description
|
||||||
A clear and concise description of what the bug is.
|
A clear and concise description of what the bug is.
|
||||||
@@ -8,10 +8,10 @@ contact_links:
|
|||||||
url: https://mokoconsulting.tech/
|
url: https://mokoconsulting.tech/
|
||||||
about: Get help or ask questions through our website
|
about: Get help or ask questions through our website
|
||||||
- name: 📚 MokoStandards Documentation
|
- name: 📚 MokoStandards Documentation
|
||||||
url: https://github.com/mokoconsulting-tech/MokoStandards
|
url: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
about: View our coding standards and best practices
|
about: View our coding standards and best practices
|
||||||
- name: 🔒 Report a Security Vulnerability
|
- name: 🔒 Report a Security Vulnerability
|
||||||
url: https://github.com/mokoconsulting-tech/.github-private/security/advisories/new
|
url: https://git.mokoconsulting.tech/mokoconsulting-tech/.github-private/security/advisories/new
|
||||||
about: Report security vulnerabilities privately (for critical issues)
|
about: Report security vulnerabilities privately (for critical issues)
|
||||||
- name: 💡 Community Discussions
|
- name: 💡 Community Discussions
|
||||||
url: https://github.com/orgs/mokoconsulting-tech/discussions
|
url: https://github.com/orgs/mokoconsulting-tech/discussions
|
||||||
@@ -7,21 +7,6 @@ assignees: ''
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<!--
|
|
||||||
SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
Copyright (C) 2024-2026 Moko Consulting Tech
|
|
||||||
|
|
||||||
File: .github/ISSUE_TEMPLATE/documentation.md
|
|
||||||
Description: Issue template for documentation-related issues
|
|
||||||
Project: .github-private
|
|
||||||
Author: Moko Consulting Tech
|
|
||||||
Version: 03.02.00
|
|
||||||
|
|
||||||
Revision History:
|
|
||||||
- 2026-01-04: Added MokoStandards compliant header with copyright, file info, and metadata
|
|
||||||
- 2026-03-11: Version bump to 03.02.00 to match MokoStandards
|
|
||||||
- 2024: Initial creation
|
|
||||||
-->
|
|
||||||
|
|
||||||
## Documentation Issue
|
## Documentation Issue
|
||||||
|
|
||||||
-15
@@ -7,21 +7,6 @@ assignees: ''
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<!--
|
|
||||||
SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
Copyright (C) 2024-2026 Moko Consulting Tech
|
|
||||||
|
|
||||||
File: .github/ISSUE_TEMPLATE/enterprise_support.md
|
|
||||||
Description: Issue template for enterprise support requests
|
|
||||||
Project: .github-private
|
|
||||||
Author: Moko Consulting Tech
|
|
||||||
Version: 03.02.00
|
|
||||||
|
|
||||||
Revision History:
|
|
||||||
- 2026-01-04: Added MokoStandards compliant header with copyright, file info, and metadata
|
|
||||||
- 2026-03-11: Version bump to 03.02.00 to match MokoStandards
|
|
||||||
- 2024: Initial creation
|
|
||||||
-->
|
|
||||||
|
|
||||||
## Support Request Type
|
## Support Request Type
|
||||||
- [ ] Critical Production Issue
|
- [ ] Critical Production Issue
|
||||||
+1
-16
@@ -7,21 +7,6 @@ assignees: ''
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<!--
|
|
||||||
SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
Copyright (C) 2024-2026 Moko Consulting Tech
|
|
||||||
|
|
||||||
File: .github/ISSUE_TEMPLATE/feature_request.md
|
|
||||||
Description: Issue template for feature requests
|
|
||||||
Project: .github-private
|
|
||||||
Author: Moko Consulting Tech
|
|
||||||
Version: 03.02.00
|
|
||||||
|
|
||||||
Revision History:
|
|
||||||
- 2026-01-04: Added MokoStandards compliant header with copyright, file info, and metadata
|
|
||||||
- 2026-03-11: Version bump to 03.02.00 to match MokoStandards
|
|
||||||
- 2024: Initial creation
|
|
||||||
-->
|
|
||||||
|
|
||||||
## Feature Description
|
## Feature Description
|
||||||
A clear and concise description of the feature you'd like to see.
|
A clear and concise description of the feature you'd like to see.
|
||||||
@@ -52,7 +37,7 @@ If you have ideas about how this could be implemented, share them here:
|
|||||||
Add any other context, mockups, or screenshots about the feature request here.
|
Add any other context, mockups, or screenshots about the feature request here.
|
||||||
|
|
||||||
## Relevant Standards
|
## Relevant Standards
|
||||||
Does this relate to any standards in [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards)?
|
Does this relate to any standards in [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards)?
|
||||||
- [ ] Accessibility (WCAG 2.1 AA)
|
- [ ] Accessibility (WCAG 2.1 AA)
|
||||||
- [ ] Localization (en_US/en_GB)
|
- [ ] Localization (en_US/en_GB)
|
||||||
- [ ] Security best practices
|
- [ ] Security best practices
|
||||||
+1
-14
@@ -3,22 +3,9 @@ name: Firewall Request
|
|||||||
about: Request firewall rule changes or access to external resources
|
about: Request firewall rule changes or access to external resources
|
||||||
title: '[FIREWALL] [Resource Name] - [Brief Description]'
|
title: '[FIREWALL] [Resource Name] - [Brief Description]'
|
||||||
labels: ['firewall-request', 'infrastructure', 'security']
|
labels: ['firewall-request', 'infrastructure', 'security']
|
||||||
assignees: []
|
assignees: ['jmiller']
|
||||||
---
|
---
|
||||||
|
|
||||||
<!--
|
|
||||||
SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
Copyright (C) 2024-2026 Moko Consulting Tech
|
|
||||||
|
|
||||||
File: .github/ISSUE_TEMPLATE/firewall-request.md
|
|
||||||
Description: Issue template for firewall rule change and access requests
|
|
||||||
Project: .github-private
|
|
||||||
Author: Moko Consulting Tech
|
|
||||||
Version: 03.02.00
|
|
||||||
|
|
||||||
Revision History:
|
|
||||||
- 2026-03-11: Added SPDX header and version to match MokoStandards 03.02.00
|
|
||||||
-->
|
|
||||||
|
|
||||||
## Firewall Request
|
## Firewall Request
|
||||||
|
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
---
|
||||||
|
name: API Integration Request
|
||||||
|
about: Request integration with a new REST API or service
|
||||||
|
title: '[API] '
|
||||||
|
labels: 'enhancement, api-integration'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API Integration Request
|
||||||
|
|
||||||
|
### Target API
|
||||||
|
- **Service Name**: [e.g., Akeeba Backup, Joomla Web Services]
|
||||||
|
- **API Documentation**: [URL to API docs]
|
||||||
|
- **API Type**: [REST / GraphQL / SOAP]
|
||||||
|
- **Authentication**: [API Key / OAuth / Bearer Token / Basic Auth]
|
||||||
|
|
||||||
|
### Proposed Tools
|
||||||
|
List the MCP tools this integration would provide:
|
||||||
|
|
||||||
|
| Tool Name | HTTP Method | Endpoint | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `service_list` | GET | `/api/items` | List all items |
|
||||||
|
| `service_get` | GET | `/api/items/{id}` | Get single item |
|
||||||
|
| `service_create` | POST | `/api/items` | Create item |
|
||||||
|
|
||||||
|
### Multi-Connection
|
||||||
|
- [ ] Single instance only
|
||||||
|
- [ ] Multiple instances (production, staging, dev)
|
||||||
|
- [ ] Multi-tenant (one connection per client)
|
||||||
|
|
||||||
|
### Use Case
|
||||||
|
Describe the workflow this integration enables for AI assistants.
|
||||||
|
|
||||||
|
### Priority
|
||||||
|
- [ ] Critical — blocking current work
|
||||||
|
- [ ] High — needed soon
|
||||||
|
- [ ] Medium — would improve workflow
|
||||||
|
- [ ] Low — nice to have
|
||||||
|
|
||||||
|
### Existing Alternatives
|
||||||
|
Are there other ways to accomplish this today? If so, why is an MCP integration better?
|
||||||
|
|
||||||
|
### Checklist
|
||||||
|
- [ ] API documentation is available and accessible
|
||||||
|
- [ ] API supports the required authentication method
|
||||||
|
- [ ] I have tested the API endpoints manually
|
||||||
|
- [ ] The integration follows the Template-MCP architecture pattern
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
---
|
||||||
|
name: MCP Connection Issue
|
||||||
|
about: Report a connection, authentication, or API communication issue
|
||||||
|
title: '[CONNECTION] '
|
||||||
|
labels: 'bug, mcp-connection'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Connection Issue
|
||||||
|
|
||||||
|
### Issue Type
|
||||||
|
- [ ] Authentication failure (401/403)
|
||||||
|
- [ ] Connection refused / timeout
|
||||||
|
- [ ] TLS / SSL certificate error
|
||||||
|
- [ ] Wrong connection used (wrong environment)
|
||||||
|
- [ ] Config file not found / parse error
|
||||||
|
- [ ] API response error (4xx / 5xx)
|
||||||
|
|
||||||
|
### MCP Server
|
||||||
|
- **Server Name**: [e.g., mcp_mokowaas]
|
||||||
|
- **Server Version**: [e.g., 1.0.0]
|
||||||
|
- **Node.js Version**: [e.g., 20.x]
|
||||||
|
|
||||||
|
### Connection Details
|
||||||
|
- **Connection Name**: [e.g., production, staging, default]
|
||||||
|
- **API Base URL**: [e.g., https://api.example.com] *(do not include API keys)*
|
||||||
|
- **Insecure Mode**: [Yes / No]
|
||||||
|
|
||||||
|
### Error Message
|
||||||
|
```
|
||||||
|
Paste the exact error message here
|
||||||
|
```
|
||||||
|
|
||||||
|
### Steps to Reproduce
|
||||||
|
1. Configure connection with `npm run setup`
|
||||||
|
2. Call tool `...` with parameters `...`
|
||||||
|
3. See error
|
||||||
|
|
||||||
|
### Expected Behavior
|
||||||
|
What should have happened.
|
||||||
|
|
||||||
|
### Debugging Attempted
|
||||||
|
- [ ] Tested API directly with curl
|
||||||
|
- [ ] Verified API key is valid
|
||||||
|
- [ ] Checked config file exists and is valid JSON
|
||||||
|
- [ ] Tested with `list_connections` tool
|
||||||
|
- [ ] Ran server manually: `node dist/index.js 2> debug.log`
|
||||||
|
|
||||||
|
### Config File
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"defaultConnection": "...",
|
||||||
|
"connections": {
|
||||||
|
"connection_name": {
|
||||||
|
"baseUrl": "https://...",
|
||||||
|
"apiKey": "REDACTED"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
*(Redact all API keys and tokens)*
|
||||||
|
|
||||||
|
### Environment
|
||||||
|
- **OS**: [e.g., macOS 14, Ubuntu 22.04, Windows 11]
|
||||||
|
- **Claude Code Version**: [e.g., latest]
|
||||||
|
- **Registration**: [.mcp.json / ~/.claude.json]
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
name: New MCP Tool Request
|
||||||
|
about: Request a new tool to be added to this MCP server
|
||||||
|
title: '[TOOL] '
|
||||||
|
labels: 'enhancement, mcp-tool'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tool Request
|
||||||
|
|
||||||
|
### Tool Name
|
||||||
|
Proposed tool name (snake_case): `resource_action`
|
||||||
|
|
||||||
|
### Description
|
||||||
|
What should this tool do? What API endpoint(s) does it map to?
|
||||||
|
|
||||||
|
### API Endpoint(s)
|
||||||
|
- **Method**: [GET / POST / PUT / PATCH / DELETE]
|
||||||
|
- **Endpoint**: `/api/v1/...`
|
||||||
|
- **Auth**: [API Key / Token / None]
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `id` | number | Yes | Resource ID |
|
||||||
|
| `search` | string | No | Search filter |
|
||||||
|
|
||||||
|
### Expected Response
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Example"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Use Case
|
||||||
|
Describe when and why someone would use this tool from Claude or another AI assistant.
|
||||||
|
|
||||||
|
### Connection Scope
|
||||||
|
- [ ] Works with all connections
|
||||||
|
- [ ] Specific to certain API versions
|
||||||
|
- [ ] Requires additional permissions
|
||||||
|
|
||||||
|
### Checklist
|
||||||
|
- [ ] I have checked this tool does not already exist
|
||||||
|
- [ ] I have verified the API endpoint exists and is documented
|
||||||
|
- [ ] The proposed name follows the `resource_action` convention
|
||||||
@@ -3,13 +3,9 @@ name: Question
|
|||||||
about: Ask a question about usage, features, or best practices
|
about: Ask a question about usage, features, or best practices
|
||||||
title: '[QUESTION] '
|
title: '[QUESTION] '
|
||||||
labels: ['question']
|
labels: ['question']
|
||||||
assignees: []
|
assignees: ['jmiller']
|
||||||
---
|
---
|
||||||
|
|
||||||
<!--
|
|
||||||
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
-->
|
|
||||||
|
|
||||||
## Question
|
## Question
|
||||||
|
|
||||||
@@ -7,21 +7,6 @@ assignees: ''
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<!--
|
|
||||||
SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
Copyright (C) 2024-2026 Moko Consulting Tech
|
|
||||||
|
|
||||||
File: .github/ISSUE_TEMPLATE/rfc.md
|
|
||||||
Description: Issue template for Request for Comments proposals
|
|
||||||
Project: .github-private
|
|
||||||
Author: Moko Consulting Tech
|
|
||||||
Version: 03.02.00
|
|
||||||
|
|
||||||
Revision History:
|
|
||||||
- 2026-01-04: Added MokoStandards compliant header with copyright, file info, and metadata
|
|
||||||
- 2026-03-11: Version bump to 03.02.00 to match MokoStandards
|
|
||||||
- 2024: Initial creation
|
|
||||||
-->
|
|
||||||
|
|
||||||
## RFC Summary
|
## RFC Summary
|
||||||
One-paragraph summary of the proposal.
|
One-paragraph summary of the proposal.
|
||||||
@@ -7,21 +7,6 @@ assignees: ''
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<!--
|
|
||||||
SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
Copyright (C) 2024-2026 Moko Consulting Tech
|
|
||||||
|
|
||||||
File: .github/ISSUE_TEMPLATE/security.md
|
|
||||||
Description: Issue template for security vulnerability reports
|
|
||||||
Project: .github-private
|
|
||||||
Author: Moko Consulting Tech
|
|
||||||
Version: 03.02.00
|
|
||||||
|
|
||||||
Revision History:
|
|
||||||
- 2026-01-04: Added MokoStandards compliant header with copyright, file info, and metadata
|
|
||||||
- 2026-03-11: Version bump to 03.02.00 to match MokoStandards
|
|
||||||
- 2024: Initial creation
|
|
||||||
-->
|
|
||||||
|
|
||||||
## ⚠️ IMPORTANT: Private Disclosure Required
|
## ⚠️ IMPORTANT: Private Disclosure Required
|
||||||
|
|
||||||
@@ -50,7 +35,7 @@ Use this template only for:
|
|||||||
<!-- Describe how this could be addressed -->
|
<!-- Describe how this could be addressed -->
|
||||||
|
|
||||||
## Standards Reference
|
## Standards Reference
|
||||||
Does this relate to security standards in [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards)?
|
Does this relate to security standards in [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards)?
|
||||||
- [ ] SPDX license identifiers
|
- [ ] SPDX license identifiers
|
||||||
- [ ] Secret management
|
- [ ] Secret management
|
||||||
- [ ] Dependency security
|
- [ ] Dependency security
|
||||||
@@ -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
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
|
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
|
||||||
# PATH: /.github/workflows/auto-assign.yml
|
# PATH: /.github/workflows/auto-assign.yml
|
||||||
# VERSION: 04.06.00
|
# VERSION: 04.06.00
|
||||||
# BRIEF: Auto-assign jmiller-moko to unassigned issues and PRs every 15 minutes
|
# BRIEF: Auto-assign jmiller to unassigned issues and PRs every 15 minutes
|
||||||
|
|
||||||
name: Auto-Assign Issues & PRs
|
name: Auto-Assign Issues & PRs
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ jobs:
|
|||||||
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
|
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
|
||||||
run: |
|
run: |
|
||||||
REPO="${{ github.repository }}"
|
REPO="${{ github.repository }}"
|
||||||
ASSIGNEE="jmiller-moko"
|
ASSIGNEE="jmiller"
|
||||||
|
|
||||||
echo "## 🏷️ Auto-Assign Report" >> $GITHUB_STEP_SUMMARY
|
echo "## 🏷️ Auto-Assign Report" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
@@ -135,7 +135,7 @@ jobs:
|
|||||||
--title "$SUB_FULL_TITLE" \
|
--title "$SUB_FULL_TITLE" \
|
||||||
--body "$SUB_BODY" \
|
--body "$SUB_BODY" \
|
||||||
--label "${SUB_LABELS}" \
|
--label "${SUB_LABELS}" \
|
||||||
--assignee "jmiller-moko" 2>&1)
|
--assignee "jmiller" 2>&1)
|
||||||
|
|
||||||
SUB_NUM=$(echo "$SUB_URL" | grep -oE '[0-9]+$')
|
SUB_NUM=$(echo "$SUB_URL" | grep -oE '[0-9]+$')
|
||||||
if [ -n "$SUB_NUM" ]; then
|
if [ -n "$SUB_NUM" ]; then
|
||||||
@@ -154,7 +154,7 @@ jobs:
|
|||||||
--title "$TITLE" \
|
--title "$TITLE" \
|
||||||
--body "$PARENT_BODY" \
|
--body "$PARENT_BODY" \
|
||||||
--label "${LABEL_TYPE},version" \
|
--label "${LABEL_TYPE},version" \
|
||||||
--assignee "jmiller-moko" 2>&1)
|
--assignee "jmiller" 2>&1)
|
||||||
|
|
||||||
PARENT_NUM=$(echo "$PARENT_URL" | grep -oE '[0-9]+$')
|
PARENT_NUM=$(echo "$PARENT_URL" | grep -oE '[0-9]+$')
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -94,7 +94,7 @@ jobs:
|
|||||||
AUTHORIZED="false"
|
AUTHORIZED="false"
|
||||||
|
|
||||||
# Hardcoded authorized users — always allowed to deploy
|
# Hardcoded authorized users — always allowed to deploy
|
||||||
AUTHORIZED_USERS="jmiller-moko github-actions[bot]"
|
AUTHORIZED_USERS="jmiller github-actions[bot]"
|
||||||
for user in $AUTHORIZED_USERS; do
|
for user in $AUTHORIZED_USERS; do
|
||||||
if [ "$ACTOR" = "$user" ]; then
|
if [ "$ACTOR" = "$user" ]; then
|
||||||
AUTHORIZED="true"
|
AUTHORIZED="true"
|
||||||
@@ -704,7 +704,7 @@ jobs:
|
|||||||
--title "$TITLE" \
|
--title "$TITLE" \
|
||||||
--body "$BODY" \
|
--body "$BODY" \
|
||||||
--label "$LABEL" \
|
--label "$LABEL" \
|
||||||
--assignee "jmiller-moko" \
|
--assignee "jmiller" \
|
||||||
| tee -a "$GITHUB_STEP_SUMMARY"
|
| tee -a "$GITHUB_STEP_SUMMARY"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ jobs:
|
|||||||
AUTHORIZED="false"
|
AUTHORIZED="false"
|
||||||
|
|
||||||
# Hardcoded authorized users — always allowed to deploy
|
# Hardcoded authorized users — always allowed to deploy
|
||||||
AUTHORIZED_USERS="jmiller-moko github-actions[bot]"
|
AUTHORIZED_USERS="jmiller github-actions[bot]"
|
||||||
for user in $AUTHORIZED_USERS; do
|
for user in $AUTHORIZED_USERS; do
|
||||||
if [ "$ACTOR" = "$user" ]; then
|
if [ "$ACTOR" = "$user" ]; then
|
||||||
AUTHORIZED="true"
|
AUTHORIZED="true"
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
MokoStandards Repository Manifest
|
||||||
|
Auto-generated by cleanup script.
|
||||||
|
See: https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home
|
||||||
|
-->
|
||||||
|
<moko-platform xmlns="https://standards.mokoconsulting.tech/moko-platform/1.0" schema-version="1.0">
|
||||||
|
<identity>
|
||||||
|
<name>joomla-api-mcp</name>
|
||||||
|
<org>MokoConsulting</org>
|
||||||
|
<description>MCP server for Joomla Web Services API operations</description>
|
||||||
|
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
|
||||||
|
</identity>
|
||||||
|
<governance>
|
||||||
|
<platform>nodejs</platform>
|
||||||
|
<standards-version>04.07.00</standards-version>
|
||||||
|
<standards-source>https://git.mokoconsulting.tech/MokoConsulting/moko-platform</standards-source>
|
||||||
|
<last-synced>2026-05-10T19:51:10+00:00</last-synced>
|
||||||
|
</governance>
|
||||||
|
<build>
|
||||||
|
<language>TypeScript</language>
|
||||||
|
<package-type>mcp-server</package-type>
|
||||||
|
<entry-point>src/</entry-point>
|
||||||
|
</build>
|
||||||
|
</moko-platform>
|
||||||
@@ -0,0 +1,278 @@
|
|||||||
|
# MCP Server Auto-Release
|
||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# MCP-specific release pipeline that builds TypeScript, runs validation,
|
||||||
|
# attaches the compiled dist/ as a release artifact, and creates a GitHub
|
||||||
|
# Release with tool inventory in the release notes.
|
||||||
|
#
|
||||||
|
# This replaces the generic auto-release.yml for MCP server repos.
|
||||||
|
|
||||||
|
name: MCP Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- 'src/**'
|
||||||
|
- 'package.json'
|
||||||
|
- 'tsconfig.json'
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-release:
|
||||||
|
name: Build, Validate & Release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: >-
|
||||||
|
!contains(github.event.head_commit.message, '[skip ci]') &&
|
||||||
|
github.actor != 'github-actions[bot]'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GH_TOKEN || github.token }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
# ── Build ────────────────────────────────────────────────────────
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: TypeScript compile check
|
||||||
|
run: npx tsc --noEmit
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Verify dist output
|
||||||
|
run: |
|
||||||
|
for f in index.js client.js config.js types.js; do
|
||||||
|
test -f "dist/${f}" || (echo "ERROR: dist/${f} not found" && exit 1)
|
||||||
|
done
|
||||||
|
echo "✓ All dist files present"
|
||||||
|
|
||||||
|
# ── Tool Inventory ───────────────────────────────────────────────
|
||||||
|
- name: Generate tool inventory
|
||||||
|
id: tools
|
||||||
|
run: |
|
||||||
|
TOOL_COUNT=$(grep -c "server\.tool(" src/index.ts || echo "0")
|
||||||
|
echo "count=${TOOL_COUNT}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
# Extract tool names
|
||||||
|
TOOL_LIST=$(grep -oE "'[a-z_]+'" src/index.ts | head -100 | tr -d "'" | sort -u)
|
||||||
|
echo "Tools registered: ${TOOL_COUNT}"
|
||||||
|
|
||||||
|
# Generate inventory for release notes
|
||||||
|
echo "## Tool Inventory (${TOOL_COUNT} tools)" > /tmp/tool-inventory.md
|
||||||
|
echo "" >> /tmp/tool-inventory.md
|
||||||
|
grep -B0 -A1 "server\.tool(" src/index.ts | grep -oE "'[^']+'" | while IFS= read -r name; do
|
||||||
|
read -r desc 2>/dev/null || true
|
||||||
|
CLEAN_NAME=$(echo "$name" | tr -d "'")
|
||||||
|
CLEAN_DESC=$(echo "$desc" | tr -d "'" | sed 's/,$//')
|
||||||
|
if [ -n "$CLEAN_NAME" ] && [ -n "$CLEAN_DESC" ]; then
|
||||||
|
echo "- \`${CLEAN_NAME}\` — ${CLEAN_DESC}" >> /tmp/tool-inventory.md
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── Version ──────────────────────────────────────────────────────
|
||||||
|
- 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: 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
|
||||||
|
|
||||||
|
MAJOR=$(echo "$VERSION" | awk -F. '{print $1}')
|
||||||
|
MINOR=$(echo "$VERSION" | awk -F. '{printf "%s.%s", $1, $2}')
|
||||||
|
PATCH=$(echo "$VERSION" | awk -F. '{print $3}')
|
||||||
|
|
||||||
|
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "branch=version/${MAJOR}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "major=$MAJOR" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "minor=$MINOR" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "release_tag=v${MAJOR}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
if [ "$PATCH" = "00" ]; then
|
||||||
|
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||||
|
if [ "$PATCH" = "01" ]; then
|
||||||
|
echo "is_first=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "is_first=false" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Check if already released
|
||||||
|
if: steps.version.outputs.skip != 'true'
|
||||||
|
id: check
|
||||||
|
run: |
|
||||||
|
TAG="${{ steps.version.outputs.release_tag }}"
|
||||||
|
TAG_EXISTS=false
|
||||||
|
git rev-parse "$TAG" >/dev/null 2>&1 && TAG_EXISTS=true
|
||||||
|
echo "tag_exists=$TAG_EXISTS" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
# ── Release Artifact ─────────────────────────────────────────────
|
||||||
|
- name: Package dist
|
||||||
|
if: steps.version.outputs.skip != 'true'
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
|
REPO_NAME="${{ github.event.repository.name }}"
|
||||||
|
tar -czf "/tmp/${REPO_NAME}-${VERSION}.tar.gz" -C dist .
|
||||||
|
echo "artifact=/tmp/${REPO_NAME}-${VERSION}.tar.gz" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
# ── Version Updates ──────────────────────────────────────────────
|
||||||
|
- name: Set platform version
|
||||||
|
if: >-
|
||||||
|
steps.version.outputs.skip != 'true' &&
|
||||||
|
steps.check.outputs.tag_exists != 'true'
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
|
php /tmp/mokostandards/api/cli/version_set_platform.php \
|
||||||
|
--path . --version "$VERSION" --branch main
|
||||||
|
|
||||||
|
- name: Update version badges
|
||||||
|
if: >-
|
||||||
|
steps.version.outputs.skip != 'true' &&
|
||||||
|
steps.check.outputs.tag_exists != '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
|
||||||
|
|
||||||
|
- name: Commit release changes
|
||||||
|
if: >-
|
||||||
|
steps.version.outputs.skip != 'true' &&
|
||||||
|
steps.check.outputs.tag_exists != '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
|
||||||
|
|
||||||
|
# ── Version Branch ───────────────────────────────────────────────
|
||||||
|
- name: Archive version branch
|
||||||
|
if: steps.check.outputs.tag_exists != 'true'
|
||||||
|
run: |
|
||||||
|
BRANCH="${{ steps.version.outputs.branch }}"
|
||||||
|
git push origin HEAD:"$BRANCH" --force
|
||||||
|
echo "Updated archive branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# ── Tag & Release ────────────────────────────────────────────────
|
||||||
|
- name: Create git tag
|
||||||
|
if: >-
|
||||||
|
steps.version.outputs.skip != 'true' &&
|
||||||
|
steps.check.outputs.tag_exists != 'true' &&
|
||||||
|
steps.version.outputs.is_first == 'true'
|
||||||
|
run: |
|
||||||
|
TAG="${{ steps.version.outputs.release_tag }}"
|
||||||
|
if ! git rev-parse "$TAG" >/dev/null 2>&1; then
|
||||||
|
git tag "$TAG"
|
||||||
|
git push origin "$TAG"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: 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 }}"
|
||||||
|
MAJOR="${{ steps.version.outputs.major }}"
|
||||||
|
BRANCH="${{ steps.version.outputs.branch }}"
|
||||||
|
TOOL_COUNT="${{ steps.tools.outputs.count }}"
|
||||||
|
REPO_NAME="${{ github.event.repository.name }}"
|
||||||
|
|
||||||
|
# Build release notes
|
||||||
|
NOTES=$(php /tmp/mokostandards/api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null)
|
||||||
|
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "$NOTES"
|
||||||
|
echo ""
|
||||||
|
echo "---"
|
||||||
|
echo ""
|
||||||
|
echo "### MCP Server Info"
|
||||||
|
echo "- **Tools registered**: ${TOOL_COUNT}"
|
||||||
|
echo "- **Node.js**: 20+"
|
||||||
|
echo "- **MCP SDK**: $(node -p \"require('./package.json').dependencies['@modelcontextprotocol/sdk']\" 2>/dev/null || echo 'unknown')"
|
||||||
|
echo ""
|
||||||
|
cat /tmp/tool-inventory.md 2>/dev/null || true
|
||||||
|
} > /tmp/release_notes.md
|
||||||
|
|
||||||
|
EXISTING=$(gh release view "$RELEASE_TAG" --json tagName -q .tagName 2>/dev/null || true)
|
||||||
|
|
||||||
|
ARTIFACT="/tmp/${REPO_NAME}-${VERSION}.tar.gz"
|
||||||
|
|
||||||
|
if [ -z "$EXISTING" ]; then
|
||||||
|
gh release create "$RELEASE_TAG" \
|
||||||
|
--title "v${MAJOR} (latest: ${VERSION})" \
|
||||||
|
--notes-file /tmp/release_notes.md \
|
||||||
|
--target "$BRANCH" \
|
||||||
|
"$ARTIFACT"
|
||||||
|
echo "Release created: ${RELEASE_TAG} (${VERSION})" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
gh release edit "$RELEASE_TAG" \
|
||||||
|
--title "v${MAJOR} (latest: ${VERSION})" \
|
||||||
|
--notes-file /tmp/release_notes.md
|
||||||
|
gh release upload "$RELEASE_TAG" "$ARTIFACT" --clobber 2>/dev/null || true
|
||||||
|
echo "Release updated: ${RELEASE_TAG} -> ${VERSION}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Summary ──────────────────────────────────────────────────────
|
||||||
|
- name: Pipeline Summary
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
|
TOOL_COUNT="${{ steps.tools.outputs.count }}"
|
||||||
|
if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
|
||||||
|
echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "## MCP Release Complete" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Detail | Value |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Tools | ${TOOL_COUNT} |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Tag | \`${{ steps.version.outputs.release_tag }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# MCP Server Build & Validation
|
||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# Builds the MCP server, validates TypeScript compilation, and checks
|
||||||
|
# that tools are properly registered with valid Zod schemas.
|
||||||
|
|
||||||
|
name: MCP Build & Validate
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main, dev/**]
|
||||||
|
paths: ['src/**', 'package.json', 'tsconfig.json']
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
paths: ['src/**', 'package.json', 'tsconfig.json']
|
||||||
|
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [20, 22]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: TypeScript compile
|
||||||
|
run: npx tsc --noEmit
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Verify dist output exists
|
||||||
|
run: |
|
||||||
|
test -f dist/index.js || (echo "ERROR: dist/index.js not found" && exit 1)
|
||||||
|
test -f dist/client.js || (echo "ERROR: dist/client.js not found" && exit 1)
|
||||||
|
test -f dist/config.js || (echo "ERROR: dist/config.js not found" && exit 1)
|
||||||
|
test -f dist/types.js || (echo "ERROR: dist/types.js not found" && exit 1)
|
||||||
|
echo "✓ All required dist files present"
|
||||||
|
|
||||||
|
- name: Verify shebang in index.js
|
||||||
|
run: |
|
||||||
|
head -1 dist/index.js | grep -q "#!/usr/bin/env node" || echo "WARNING: Missing shebang in dist/index.js"
|
||||||
|
|
||||||
|
- name: Count registered tools
|
||||||
|
run: |
|
||||||
|
TOOL_COUNT=$(grep -c "server\.tool(" src/index.ts || true)
|
||||||
|
echo "Registered tools: ${TOOL_COUNT}"
|
||||||
|
if [ "${TOOL_COUNT}" -eq 0 ]; then
|
||||||
|
echo "ERROR: No tools registered in src/index.ts"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
# MCP SDK Version Check
|
||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# Weekly check for MCP SDK updates. Creates an issue when a new version
|
||||||
|
# of @modelcontextprotocol/sdk is available.
|
||||||
|
|
||||||
|
name: MCP SDK Version Check
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 9 * * 1' # Every Monday at 9am UTC
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-sdk:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
|
- name: Check for SDK updates
|
||||||
|
id: sdk-check
|
||||||
|
run: |
|
||||||
|
CURRENT=$(node -p "require('./package.json').dependencies['@modelcontextprotocol/sdk']" | sed 's/[\^~]//')
|
||||||
|
LATEST=$(npm view @modelcontextprotocol/sdk version 2>/dev/null || echo "unknown")
|
||||||
|
|
||||||
|
echo "current=${CURRENT}" >> $GITHUB_OUTPUT
|
||||||
|
echo "latest=${LATEST}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
if [ "${CURRENT}" != "${LATEST}" ] && [ "${LATEST}" != "unknown" ]; then
|
||||||
|
echo "update_available=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "MCP SDK update available: ${CURRENT} → ${LATEST}"
|
||||||
|
else
|
||||||
|
echo "update_available=false" >> $GITHUB_OUTPUT
|
||||||
|
echo "MCP SDK is up to date: ${CURRENT}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Check for Zod updates
|
||||||
|
id: zod-check
|
||||||
|
run: |
|
||||||
|
CURRENT=$(node -p "require('./package.json').dependencies['zod']" | sed 's/[\^~]//')
|
||||||
|
LATEST=$(npm view zod version 2>/dev/null || echo "unknown")
|
||||||
|
|
||||||
|
echo "current=${CURRENT}" >> $GITHUB_OUTPUT
|
||||||
|
echo "latest=${LATEST}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
if [ "${CURRENT}" != "${LATEST}" ] && [ "${LATEST}" != "unknown" ]; then
|
||||||
|
echo "update_available=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "update_available=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Create update issue
|
||||||
|
if: steps.sdk-check.outputs.update_available == 'true'
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const title = `chore(deps): update @modelcontextprotocol/sdk ${process.env.CURRENT} → ${process.env.LATEST}`;
|
||||||
|
const body = [
|
||||||
|
'## MCP SDK Update Available',
|
||||||
|
'',
|
||||||
|
`| Package | Current | Latest |`,
|
||||||
|
`|---------|---------|--------|`,
|
||||||
|
`| @modelcontextprotocol/sdk | ${process.env.CURRENT} | ${process.env.LATEST} |`,
|
||||||
|
`| zod | ${process.env.ZOD_CURRENT} | ${process.env.ZOD_LATEST} |`,
|
||||||
|
'',
|
||||||
|
'### Steps',
|
||||||
|
'1. Update package.json',
|
||||||
|
'2. Run `npm install`',
|
||||||
|
'3. Run `npm run build` to verify compilation',
|
||||||
|
'4. Test all tools against target API',
|
||||||
|
'',
|
||||||
|
'### Changelog',
|
||||||
|
`https://github.com/modelcontextprotocol/typescript-sdk/releases`,
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
// Check for existing open issue
|
||||||
|
const existing = await github.rest.issues.listForRepo({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
state: 'open',
|
||||||
|
labels: 'api-change',
|
||||||
|
});
|
||||||
|
|
||||||
|
const alreadyExists = existing.data.some(i => i.title.includes('@modelcontextprotocol/sdk'));
|
||||||
|
if (!alreadyExists) {
|
||||||
|
await github.rest.issues.create({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
labels: ['api-change', 'chore'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
env:
|
||||||
|
CURRENT: ${{ steps.sdk-check.outputs.current }}
|
||||||
|
LATEST: ${{ steps.sdk-check.outputs.latest }}
|
||||||
|
ZOD_CURRENT: ${{ steps.zod-check.outputs.current }}
|
||||||
|
ZOD_LATEST: ${{ steps.zod-check.outputs.latest }}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# MCP Tool Inventory
|
||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# Generates a tool inventory report on each push to main.
|
||||||
|
# Extracts tool names, descriptions, and parameter counts from src/index.ts.
|
||||||
|
|
||||||
|
name: MCP Tool Inventory
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths: ['src/index.ts']
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
inventory:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Generate tool inventory
|
||||||
|
run: |
|
||||||
|
echo "# MCP Tool Inventory" > TOOLS.md
|
||||||
|
echo "" >> TOOLS.md
|
||||||
|
echo "Auto-generated from \`src/index.ts\` on $(date -u +%Y-%m-%dT%H:%M:%SZ)" >> TOOLS.md
|
||||||
|
echo "" >> TOOLS.md
|
||||||
|
|
||||||
|
# Count tools
|
||||||
|
TOOL_COUNT=$(grep -c "server\.tool(" src/index.ts || true)
|
||||||
|
echo "**Total tools: ${TOOL_COUNT}**" >> TOOLS.md
|
||||||
|
echo "" >> TOOLS.md
|
||||||
|
|
||||||
|
# Extract tool names and descriptions
|
||||||
|
echo "| Tool | Description |" >> TOOLS.md
|
||||||
|
echo "|------|-------------|" >> TOOLS.md
|
||||||
|
|
||||||
|
grep -A1 "server\.tool(" src/index.ts | grep -E "^\s*'" | while read -r line; do
|
||||||
|
TOOL_NAME=$(echo "$line" | sed "s/.*'\([^']*\)'.*/\1/")
|
||||||
|
# Get next line for description
|
||||||
|
DESC=$(grep -A2 "'${TOOL_NAME}'" src/index.ts | grep -E "^\s*'" | tail -1 | sed "s/.*'\([^']*\)'.*/\1/" || echo "")
|
||||||
|
echo "| \`${TOOL_NAME}\` | ${DESC} |" >> TOOLS.md
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "" >> TOOLS.md
|
||||||
|
echo "---" >> TOOLS.md
|
||||||
|
echo "*Generated by MCP Tool Inventory workflow*" >> TOOLS.md
|
||||||
|
|
||||||
|
cat TOOLS.md
|
||||||
|
|
||||||
|
- name: Upload inventory artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: tool-inventory
|
||||||
|
path: TOOLS.md
|
||||||
|
retention-days: 90
|
||||||
@@ -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
|
||||||
@@ -80,7 +80,7 @@ jobs:
|
|||||||
echo "✅ Scheduled run — authorized"
|
echo "✅ Scheduled run — authorized"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
AUTHORIZED_USERS="jmiller-moko github-actions[bot]"
|
AUTHORIZED_USERS="jmiller github-actions[bot]"
|
||||||
for user in $AUTHORIZED_USERS; do
|
for user in $AUTHORIZED_USERS; do
|
||||||
if [ "$ACTOR" = "$user" ]; then
|
if [ "$ACTOR" = "$user" ]; then
|
||||||
echo "✅ ${ACTOR} authorized"
|
echo "✅ ${ACTOR} authorized"
|
||||||
@@ -2601,7 +2601,7 @@ jobs:
|
|||||||
echo "Updated issue #${EXISTING}"
|
echo "Updated issue #${EXISTING}"
|
||||||
else
|
else
|
||||||
gh issue create --repo "$REPO" --title "$TITLE" --body "$BODY" \
|
gh issue create --repo "$REPO" --title "$TITLE" --body "$BODY" \
|
||||||
--label "$LABEL" --assignee "jmiller-moko"
|
--label "$LABEL" --assignee "jmiller"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# CUSTOMIZATION:
|
# CUSTOMIZATION:
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# 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: /.mokogitea/workflows/auto-assign.yml
|
||||||
|
# VERSION: 04.06.00
|
||||||
|
# BRIEF: Auto-assign jmiller to unassigned issues and PRs every 15 minutes
|
||||||
|
|
||||||
|
name: "Universal: Auto-Assign"
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
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
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
# 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 .mokogitea/workflows/auto-dev-issue.yml in all governed repos.
|
||||||
|
|
||||||
|
name: "Universal: 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" 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" 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
|
||||||
@@ -0,0 +1,324 @@
|
|||||||
|
# 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, type-prefixed packages |
|
||||||
|
# | dolibarr: mod*.class.php, update.txt, dev version reset |
|
||||||
|
# | generic: README-only, no update stream |
|
||||||
|
# | |
|
||||||
|
# +========================================================================+
|
||||||
|
|
||||||
|
name: "Universal: Build & Release"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, closed]
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
action:
|
||||||
|
description: 'Action to perform'
|
||||||
|
required: false
|
||||||
|
type: choice
|
||||||
|
default: release
|
||||||
|
options:
|
||||||
|
- release
|
||||||
|
- promote-rc
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||||||
|
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# ── PR Opened → Rename branch to RC and build RC release ─────────────────────
|
||||||
|
promote-rc:
|
||||||
|
name: Promote to RC
|
||||||
|
runs-on: release
|
||||||
|
if: >-
|
||||||
|
(github.event.action == 'opened' && github.event.pull_request.merged != true) ||
|
||||||
|
(github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
fetch-depth: 1
|
||||||
|
|
||||||
|
- name: Setup moko-platform tools
|
||||||
|
env:
|
||||||
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
|
run: |
|
||||||
|
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
|
||||||
|
echo Using pre-installed /opt/moko-platform
|
||||||
|
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo Falling back to fresh clone
|
||||||
|
if ! command -v composer > /dev/null 2>&1; then
|
||||||
|
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
|
||||||
|
fi
|
||||||
|
rm -rf /tmp/moko-platform-api
|
||||||
|
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git
|
||||||
|
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/moko-platform-api
|
||||||
|
cd /tmp/moko-platform-api
|
||||||
|
composer install --no-dev --no-interaction --quiet
|
||||||
|
echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Rename branch to rc
|
||||||
|
run: |
|
||||||
|
php ${MOKO_CLI}/branch_rename.php \
|
||||||
|
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
|
||||||
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
|
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
|
||||||
|
--pr "${{ github.event.pull_request.number }}"
|
||||||
|
|
||||||
|
- name: Checkout rc and configure git
|
||||||
|
run: |
|
||||||
|
git fetch origin rc
|
||||||
|
git checkout rc
|
||||||
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
|
git config --local user.name "gitea-actions[bot]"
|
||||||
|
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||||
|
|
||||||
|
- name: Publish RC release
|
||||||
|
run: |
|
||||||
|
php ${MOKO_CLI}/release_publish.php \
|
||||||
|
--path . --stability rc --bump minor --branch rc \
|
||||||
|
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
|
- name: Summary
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
|
||||||
|
release:
|
||||||
|
name: Build & Release Pipeline
|
||||||
|
runs-on: release
|
||||||
|
if: >-
|
||||||
|
github.event.pull_request.merged == true ||
|
||||||
|
(github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Configure git for bot pushes
|
||||||
|
run: |
|
||||||
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
|
git config --local user.name "gitea-actions[bot]"
|
||||||
|
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||||
|
|
||||||
|
- name: Check for merge conflict markers
|
||||||
|
run: |
|
||||||
|
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
|
||||||
|
if [ -n "$CONFLICTS" ]; then
|
||||||
|
echo "::error::Merge conflict markers found — aborting release"
|
||||||
|
echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "No conflict markers found"
|
||||||
|
|
||||||
|
- name: Setup moko-platform tools
|
||||||
|
env:
|
||||||
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
|
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
|
||||||
|
run: |
|
||||||
|
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
|
||||||
|
echo Using pre-installed /opt/moko-platform
|
||||||
|
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo Falling back to fresh clone
|
||||||
|
if ! command -v composer > /dev/null 2>&1; then
|
||||||
|
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
|
||||||
|
fi
|
||||||
|
rm -rf /tmp/moko-platform-api
|
||||||
|
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git
|
||||||
|
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/moko-platform-api
|
||||||
|
cd /tmp/moko-platform-api
|
||||||
|
composer install --no-dev --no-interaction --quiet
|
||||||
|
echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: "Publish stable release"
|
||||||
|
run: |
|
||||||
|
php ${MOKO_CLI}/release_publish.php \
|
||||||
|
--path . --stability stable --bump minor --branch main \
|
||||||
|
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
|
- name: Update release notes from CHANGELOG.md
|
||||||
|
run: |
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
|
||||||
|
# Extract [Unreleased] section from changelog
|
||||||
|
if [ -f "CHANGELOG.md" ]; then
|
||||||
|
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
|
||||||
|
[ -z "$NOTES" ] && NOTES="Stable release"
|
||||||
|
else
|
||||||
|
NOTES="Stable release"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Update release body via API
|
||||||
|
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
|
"${API_BASE}/releases/tags/stable" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [ -n "$RELEASE_ID" ]; then
|
||||||
|
python3 -c "
|
||||||
|
import json, urllib.request
|
||||||
|
body = open('/dev/stdin').read()
|
||||||
|
payload = json.dumps({'body': body}).encode()
|
||||||
|
req = urllib.request.Request(
|
||||||
|
'${API_BASE}/releases/${RELEASE_ID}',
|
||||||
|
data=payload, method='PATCH',
|
||||||
|
headers={
|
||||||
|
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
})
|
||||||
|
urllib.request.urlopen(req)
|
||||||
|
" <<< "$NOTES"
|
||||||
|
echo "Release notes updated from CHANGELOG.md"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
|
||||||
|
- name: "Step 9: Mirror release to GitHub"
|
||||||
|
if: >-
|
||||||
|
steps.version.outputs.skip != 'true' &&
|
||||||
|
secrets.GH_MIRROR_TOKEN != ''
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
|
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
||||||
|
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
php ${MOKO_CLI}/release_mirror.php \
|
||||||
|
--version "$VERSION" --tag "$RELEASE_TAG" \
|
||||||
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
|
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
|
||||||
|
--branch main 2>&1 || true
|
||||||
|
echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# -- STEP 10: Sync main branch to GitHub mirror ----------------------------
|
||||||
|
- name: "Step 10: Push main to GitHub mirror"
|
||||||
|
if: >-
|
||||||
|
steps.version.outputs.skip != 'true' &&
|
||||||
|
secrets.GH_MIRROR_TOKEN != ''
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
||||||
|
GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
|
||||||
|
GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
|
||||||
|
git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
|
||||||
|
git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
|
||||||
|
git fetch origin main --depth=1
|
||||||
|
git push github origin/main:refs/heads/main --force 2>/dev/null \
|
||||||
|
&& echo "main branch pushed to GitHub mirror" \
|
||||||
|
|| echo "WARNING: GitHub mirror push failed"
|
||||||
|
|
||||||
|
- name: "Step 11: Delete rc branch and recreate dev from main"
|
||||||
|
if: steps.version.outputs.skip != 'true'
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
|
# Delete rc branch (ephemeral — created by promote-rc)
|
||||||
|
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
|
||||||
|
"${API_BASE}/branches/rc" 2>/dev/null \
|
||||||
|
&& echo "Deleted rc branch" || echo "rc branch not found"
|
||||||
|
|
||||||
|
# Delete dev branch
|
||||||
|
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
|
||||||
|
"${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
|
||||||
|
|
||||||
|
# Recreate dev from main (now includes version bump + changelog promotion)
|
||||||
|
curl -sf -X POST -H "Authorization: token ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API_BASE}/branches" \
|
||||||
|
-d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
|
||||||
|
|
||||||
|
echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
- name: "Step 12: Create version branch from main"
|
||||||
|
if: steps.version.outputs.skip != 'true'
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
|
BRANCH_NAME="version/${VERSION}"
|
||||||
|
MAIN_SHA=$(git rev-parse HEAD)
|
||||||
|
|
||||||
|
# Delete old version branch if it exists (same version re-release)
|
||||||
|
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}"
|
||||||
|
|
||||||
|
# Create version/XX.YY.ZZ from main
|
||||||
|
curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed"
|
||||||
|
|
||||||
|
echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# -- Dolibarr post-release: Reset dev version -----------------------------
|
||||||
|
- name: "Post-release: Reset dev version"
|
||||||
|
if: steps.version.outputs.skip != 'true'
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
php ${MOKO_CLI}/version_reset_dev.php \
|
||||||
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
|
||||||
|
--branch dev --path . 2>&1 || true
|
||||||
|
|
||||||
|
# -- Summary --------------------------------------------------------------
|
||||||
|
- name: Pipeline Summary
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
|
PLATFORM="${{ steps.platform.outputs.platform }}"
|
||||||
|
if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
|
||||||
|
echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
|
||||||
|
elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
|
||||||
|
echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
# 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 .mokogitea/workflows/changelog-validation.yml in governed repos.
|
||||||
|
|
||||||
|
name: "Universal: Changelog Validation"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
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
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: moko-platform.Maintenance
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
|
# PATH: /.gitea/workflows/cleanup.yml
|
||||||
|
# VERSION: 01.00.00
|
||||||
|
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
|
||||||
|
|
||||||
|
name: "Universal: Repository Cleanup"
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 3 * * 0' # Weekly on Sunday at 03:00 UTC
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
env:
|
||||||
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cleanup:
|
||||||
|
name: Clean Merged Branches
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.GA_TOKEN }}
|
||||||
|
|
||||||
|
- name: Delete merged branches
|
||||||
|
env:
|
||||||
|
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||||
|
run: |
|
||||||
|
echo "=== Merged Branch Cleanup ==="
|
||||||
|
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||||
|
|
||||||
|
# List branches via API
|
||||||
|
BRANCHES=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
|
||||||
|
"${API}/branches?limit=50" | jq -r '.[].name')
|
||||||
|
|
||||||
|
DELETED=0
|
||||||
|
for BRANCH in $BRANCHES; do
|
||||||
|
# Skip protected branches
|
||||||
|
case "$BRANCH" in
|
||||||
|
main|master|develop|release/*|hotfix/*) continue ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Check if branch is merged into main
|
||||||
|
if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then
|
||||||
|
echo " Deleting merged branch: ${BRANCH}"
|
||||||
|
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
|
||||||
|
"${API}/branches/${BRANCH}" 2>/dev/null || true
|
||||||
|
DELETED=$((DELETED + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Deleted ${DELETED} merged branch(es)"
|
||||||
|
|
||||||
|
- name: Clean old workflow runs
|
||||||
|
env:
|
||||||
|
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||||
|
run: |
|
||||||
|
echo "=== Workflow Run Cleanup ==="
|
||||||
|
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||||
|
CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ)
|
||||||
|
|
||||||
|
# Get old completed runs
|
||||||
|
RUNS=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
|
||||||
|
"${API}/actions/runs?status=completed&limit=50" | \
|
||||||
|
jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null)
|
||||||
|
|
||||||
|
DELETED=0
|
||||||
|
for RUN_ID in $RUNS; do
|
||||||
|
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
|
||||||
|
"${API}/actions/runs/${RUN_ID}" 2>/dev/null || true
|
||||||
|
DELETED=$((DELETED + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Deleted ${DELETED} old workflow run(s)"
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
# 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.Security
|
||||||
|
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
|
||||||
|
# PATH: /templates/workflows/generic/codeql-analysis.yml.template
|
||||||
|
# VERSION: 04.05.00
|
||||||
|
# BRIEF: CodeQL security scanning workflow (generic — all repo types)
|
||||||
|
# NOTE: Deployed to .mokogitea/workflows/codeql-analysis.yml in governed repos.
|
||||||
|
# CodeQL does not support PHP directly; JavaScript scans JSON/YAML/shell.
|
||||||
|
# For PHP-specific security scanning see standards-compliance.yml.
|
||||||
|
|
||||||
|
name: "Universal: CodeQL Analysis"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- dev/**
|
||||||
|
- rc/**
|
||||||
|
- version/**
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- dev/**
|
||||||
|
- rc/**
|
||||||
|
schedule:
|
||||||
|
# Weekly on Monday at 06:00 UTC
|
||||||
|
- cron: '0 6 * * 1'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
contents: read
|
||||||
|
security-events: write
|
||||||
|
pull-requests: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze (${{ matrix.language }})
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 360
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
# CodeQL does not support PHP. Use 'javascript' to scan JSON, YAML,
|
||||||
|
# and shell scripts. Add 'actions' to scan GitHub Actions workflows.
|
||||||
|
language: ['javascript', 'actions']
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v3
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
queries: security-extended,security-and-quality
|
||||||
|
|
||||||
|
- name: Autobuild
|
||||||
|
uses: github/codeql-action/autobuild@v3
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v3
|
||||||
|
with:
|
||||||
|
category: "/language:${{ matrix.language }}"
|
||||||
|
upload: true
|
||||||
|
output: sarif-results
|
||||||
|
wait-for-processing: true
|
||||||
|
|
||||||
|
- name: Upload SARIF results
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.5.0
|
||||||
|
with:
|
||||||
|
name: codeql-results-${{ matrix.language }}
|
||||||
|
path: sarif-results
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
|
- name: Step summary
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
echo "### 🔍 CodeQL — ${{ matrix.language }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
URL="https://github.com/${{ github.repository }}/security/code-scanning"
|
||||||
|
echo "See the [Security tab]($URL) for findings." >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Severity | SLA |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "|----------|-----|" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Critical | 7 days |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| High | 14 days |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Medium | 30 days |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Low | 60 days / next release |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
summary:
|
||||||
|
name: Security Scan Summary
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: analyze
|
||||||
|
if: always()
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Summary
|
||||||
|
run: |
|
||||||
|
echo "### 🛡️ CodeQL Complete" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "**Trigger:** ${{ github.event_name }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "**Branch:** ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
SECURITY_URL="https://github.com/${{ github.repository }}/security"
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "📊 [View all security alerts]($SECURITY_URL)" >> $GITHUB_STEP_SUMMARY
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
# SPDX-LICENSE-IDENTIFIER: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# GitHub Actions workflow for Copilot coding agent
|
||||||
|
# This workflow demonstrates how to use the firewall configuration
|
||||||
|
|
||||||
|
name: "MCP: Copilot Agent"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize, reopened]
|
||||||
|
issue_comment:
|
||||||
|
types: [created]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
copilot-agent:
|
||||||
|
name: Run Copilot Coding Agent
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Configure Copilot Firewall
|
||||||
|
run: |
|
||||||
|
echo "Configuring firewall allowlist for enterprise-ready sites..."
|
||||||
|
bash .github/copilot/setup-firewall.sh
|
||||||
|
echo "Firewall configuration completed"
|
||||||
|
|
||||||
|
- name: Run Copilot Agent
|
||||||
|
uses: github/copilot-swe-agent@v1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
issue_number: ${{ github.event.issue.number || github.event.pull_request.number }}
|
||||||
|
env:
|
||||||
|
# Environment variables are set by setup-firewall.sh
|
||||||
|
COPILOT_FIREWALL_ALLOWLIST: ${{ env.COPILOT_FIREWALL_ALLOWLIST }}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: moko-platform.Deploy
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
|
# PATH: /templates/workflows/joomla/deploy-manual.yml.template
|
||||||
|
# VERSION: 04.07.00
|
||||||
|
# BRIEF: Manual SFTP deploy to dev server for Joomla repos
|
||||||
|
|
||||||
|
name: "Universal: Deploy to Dev (Manual)"
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
clear_remote:
|
||||||
|
description: 'Delete all remote files before uploading'
|
||||||
|
required: false
|
||||||
|
default: 'false'
|
||||||
|
type: boolean
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
name: SFTP Deploy to Dev
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
run: |
|
||||||
|
php -v && composer --version
|
||||||
|
|
||||||
|
- name: Setup moko-platform tools
|
||||||
|
env:
|
||||||
|
GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
|
||||||
|
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
|
||||||
|
MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
|
||||||
|
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
|
||||||
|
run: |
|
||||||
|
git clone --depth 1 --branch main --quiet \
|
||||||
|
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
||||||
|
/tmp/moko-platform-api 2>/dev/null || true
|
||||||
|
if [ -d "/tmp/moko-platform-api" ] && [ -f "/tmp/moko-platform-api/composer.json" ]; then
|
||||||
|
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Check FTP configuration
|
||||||
|
id: check
|
||||||
|
env:
|
||||||
|
HOST: ${{ vars.DEV_FTP_HOST }}
|
||||||
|
PATH_VAR: ${{ vars.DEV_FTP_PATH }}
|
||||||
|
PORT: ${{ vars.DEV_FTP_PORT }}
|
||||||
|
run: |
|
||||||
|
if [ -z "$HOST" ] || [ -z "$PATH_VAR" ]; then
|
||||||
|
echo "DEV_FTP_HOST or DEV_FTP_PATH not configured -- cannot deploy"
|
||||||
|
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "host=$HOST" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
REMOTE="${PATH_VAR%/}"
|
||||||
|
echo "remote=$REMOTE" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
[ -z "$PORT" ] && PORT="22"
|
||||||
|
echo "port=$PORT" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Deploy via SFTP
|
||||||
|
if: steps.check.outputs.skip != 'true'
|
||||||
|
env:
|
||||||
|
SFTP_KEY: ${{ secrets.DEV_FTP_KEY }}
|
||||||
|
SFTP_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
|
||||||
|
SFTP_USER: ${{ vars.DEV_FTP_USERNAME }}
|
||||||
|
run: |
|
||||||
|
SOURCE_DIR="src"
|
||||||
|
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
||||||
|
[ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ -- nothing to deploy"; exit 0; }
|
||||||
|
|
||||||
|
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
|
||||||
|
"${{ steps.check.outputs.host }}" "${{ steps.check.outputs.port }}" "$SFTP_USER" "${{ steps.check.outputs.remote }}" \
|
||||||
|
> /tmp/sftp-config.json
|
||||||
|
|
||||||
|
if [ -n "$SFTP_KEY" ]; then
|
||||||
|
echo "$SFTP_KEY" > /tmp/deploy_key
|
||||||
|
chmod 600 /tmp/deploy_key
|
||||||
|
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
|
||||||
|
else
|
||||||
|
printf ',"password":"%s"}' "$SFTP_PASS" >> /tmp/sftp-config.json
|
||||||
|
fi
|
||||||
|
|
||||||
|
DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json)
|
||||||
|
[ "${{ inputs.clear_remote }}" = "true" ] && DEPLOY_ARGS+=(--clear-remote)
|
||||||
|
|
||||||
|
PLATFORM=$(php /tmp/moko-platform-api/cli/platform_detect.php --path . 2>/dev/null || true)
|
||||||
|
if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/moko-platform-api/deploy/deploy-joomla.php" ]; then
|
||||||
|
php /tmp/moko-platform-api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}"
|
||||||
|
else
|
||||||
|
php /tmp/moko-platform-api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f /tmp/deploy_key /tmp/sftp-config.json
|
||||||
|
|
||||||
|
|
||||||
|
- name: Post-deploy health check
|
||||||
|
if: success() && steps.check.outputs.skip != 'true'
|
||||||
|
run: |
|
||||||
|
if [ -f "deploy/health-check.php" ]; then
|
||||||
|
SITE_URL="${{ vars.DEV_SITE_URL }}"
|
||||||
|
if [ -n "$SITE_URL" ]; then
|
||||||
|
php deploy/health-check.php --url "$SITE_URL" --checks http --timeout 30 || echo "::warning::Health check failed after deploy"
|
||||||
|
else
|
||||||
|
echo "DEV_SITE_URL not configured, skipping health check"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Summary
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
if [ "${{ steps.check.outputs.skip }}" = "true" ]; then
|
||||||
|
echo "### Deploy Skipped -- FTP not configured" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "### Manual Dev Deploy Complete" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Host | \`${{ steps.check.outputs.host }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Remote | \`${{ steps.check.outputs.remote }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Clear | ${{ inputs.clear_remote }} |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
@@ -0,0 +1,758 @@
|
|||||||
|
# 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: "MCP: Enterprise Firewall"
|
||||||
|
|
||||||
|
# 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
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: moko-platform.Security
|
||||||
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
||||||
|
# PATH: /templates/workflows/gitleaks.yml.template
|
||||||
|
# VERSION: 01.00.00
|
||||||
|
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
|
||||||
|
#
|
||||||
|
# +========================================================================+
|
||||||
|
# | SECRET SCANNING |
|
||||||
|
# +========================================================================+
|
||||||
|
# | |
|
||||||
|
# | Scans commits for leaked secrets using Gitleaks. |
|
||||||
|
# | |
|
||||||
|
# | - PR scan: only new commits in the PR |
|
||||||
|
# | - Scheduled: full repo scan weekly |
|
||||||
|
# | - Alerts via ntfy on findings |
|
||||||
|
# | |
|
||||||
|
# +========================================================================+
|
||||||
|
|
||||||
|
name: "Universal: Secret Scanning"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- 'dev/**'
|
||||||
|
schedule:
|
||||||
|
- cron: '0 5 * * 1' # Weekly Monday 05:00 UTC
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
env:
|
||||||
|
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
|
||||||
|
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
gitleaks:
|
||||||
|
name: Gitleaks Secret Scan
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Install Gitleaks
|
||||||
|
run: |
|
||||||
|
GITLEAKS_VERSION="8.21.2"
|
||||||
|
curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \
|
||||||
|
| tar -xz -C /usr/local/bin gitleaks
|
||||||
|
gitleaks version
|
||||||
|
|
||||||
|
- name: Scan for secrets
|
||||||
|
id: scan
|
||||||
|
run: |
|
||||||
|
echo "### Secret Scanning" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ARGS="--source . --verbose --report-format json --report-path /tmp/gitleaks-report.json"
|
||||||
|
|
||||||
|
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
||||||
|
# Scan only PR commits
|
||||||
|
ARGS="$ARGS --log-opts=${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}"
|
||||||
|
echo "Scanning PR commits only" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "Full repository scan" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
if gitleaks detect $ARGS 2>&1; then
|
||||||
|
echo "result=clean" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "**No secrets detected.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "result=found" >> "$GITHUB_OUTPUT"
|
||||||
|
FINDINGS=$(jq length /tmp/gitleaks-report.json 2>/dev/null || echo "unknown")
|
||||||
|
echo "**${FINDINGS} potential secret(s) detected.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Review the findings and rotate any exposed credentials immediately." >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Notify on findings
|
||||||
|
if: failure() && steps.scan.outputs.result == 'found'
|
||||||
|
run: |
|
||||||
|
REPO="${{ github.event.repository.name }}"
|
||||||
|
curl -sS \
|
||||||
|
-H "Title: ${REPO} — secrets detected in code" \
|
||||||
|
-H "Tags: rotating_light,key" \
|
||||||
|
-H "Priority: urgent" \
|
||||||
|
-d "Gitleaks found potential secrets. Review and rotate credentials immediately." \
|
||||||
|
"${NTFY_URL}/${NTFY_TOPIC}" || true
|
||||||
@@ -0,0 +1,278 @@
|
|||||||
|
# MCP Server Auto-Release
|
||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# MCP-specific release pipeline that builds TypeScript, runs validation,
|
||||||
|
# attaches the compiled dist/ as a release artifact, and creates a GitHub
|
||||||
|
# Release with tool inventory in the release notes.
|
||||||
|
#
|
||||||
|
# This replaces the generic auto-release.yml for MCP server repos.
|
||||||
|
|
||||||
|
name: "MCP: Build & Release"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- 'src/**'
|
||||||
|
- 'package.json'
|
||||||
|
- 'tsconfig.json'
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-release:
|
||||||
|
name: Build, Validate & Release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: >-
|
||||||
|
!contains(github.event.head_commit.message, '[skip ci]') &&
|
||||||
|
github.actor != 'github-actions[bot]'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GH_TOKEN || github.token }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
# ── Build ────────────────────────────────────────────────────────
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: TypeScript compile check
|
||||||
|
run: npx tsc --noEmit
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Verify dist output
|
||||||
|
run: |
|
||||||
|
for f in index.js client.js config.js types.js; do
|
||||||
|
test -f "dist/${f}" || (echo "ERROR: dist/${f} not found" && exit 1)
|
||||||
|
done
|
||||||
|
echo "✓ All dist files present"
|
||||||
|
|
||||||
|
# ── Tool Inventory ───────────────────────────────────────────────
|
||||||
|
- name: Generate tool inventory
|
||||||
|
id: tools
|
||||||
|
run: |
|
||||||
|
TOOL_COUNT=$(grep -c "server\.tool(" src/index.ts || echo "0")
|
||||||
|
echo "count=${TOOL_COUNT}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
# Extract tool names
|
||||||
|
TOOL_LIST=$(grep -oE "'[a-z_]+'" src/index.ts | head -100 | tr -d "'" | sort -u)
|
||||||
|
echo "Tools registered: ${TOOL_COUNT}"
|
||||||
|
|
||||||
|
# Generate inventory for release notes
|
||||||
|
echo "## Tool Inventory (${TOOL_COUNT} tools)" > /tmp/tool-inventory.md
|
||||||
|
echo "" >> /tmp/tool-inventory.md
|
||||||
|
grep -B0 -A1 "server\.tool(" src/index.ts | grep -oE "'[^']+'" | while IFS= read -r name; do
|
||||||
|
read -r desc 2>/dev/null || true
|
||||||
|
CLEAN_NAME=$(echo "$name" | tr -d "'")
|
||||||
|
CLEAN_DESC=$(echo "$desc" | tr -d "'" | sed 's/,$//')
|
||||||
|
if [ -n "$CLEAN_NAME" ] && [ -n "$CLEAN_DESC" ]; then
|
||||||
|
echo "- \`${CLEAN_NAME}\` — ${CLEAN_DESC}" >> /tmp/tool-inventory.md
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── Version ──────────────────────────────────────────────────────
|
||||||
|
- 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: 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
|
||||||
|
|
||||||
|
MAJOR=$(echo "$VERSION" | awk -F. '{print $1}')
|
||||||
|
MINOR=$(echo "$VERSION" | awk -F. '{printf "%s.%s", $1, $2}')
|
||||||
|
PATCH=$(echo "$VERSION" | awk -F. '{print $3}')
|
||||||
|
|
||||||
|
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "branch=version/${MAJOR}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "major=$MAJOR" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "minor=$MINOR" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "release_tag=v${MAJOR}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
if [ "$PATCH" = "00" ]; then
|
||||||
|
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||||
|
if [ "$PATCH" = "01" ]; then
|
||||||
|
echo "is_first=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "is_first=false" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Check if already released
|
||||||
|
if: steps.version.outputs.skip != 'true'
|
||||||
|
id: check
|
||||||
|
run: |
|
||||||
|
TAG="${{ steps.version.outputs.release_tag }}"
|
||||||
|
TAG_EXISTS=false
|
||||||
|
git rev-parse "$TAG" >/dev/null 2>&1 && TAG_EXISTS=true
|
||||||
|
echo "tag_exists=$TAG_EXISTS" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
# ── Release Artifact ─────────────────────────────────────────────
|
||||||
|
- name: Package dist
|
||||||
|
if: steps.version.outputs.skip != 'true'
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
|
REPO_NAME="${{ github.event.repository.name }}"
|
||||||
|
tar -czf "/tmp/${REPO_NAME}-${VERSION}.tar.gz" -C dist .
|
||||||
|
echo "artifact=/tmp/${REPO_NAME}-${VERSION}.tar.gz" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
# ── Version Updates ──────────────────────────────────────────────
|
||||||
|
- name: Set platform version
|
||||||
|
if: >-
|
||||||
|
steps.version.outputs.skip != 'true' &&
|
||||||
|
steps.check.outputs.tag_exists != 'true'
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
|
php /tmp/mokostandards/api/cli/version_set_platform.php \
|
||||||
|
--path . --version "$VERSION" --branch main
|
||||||
|
|
||||||
|
- name: Update version badges
|
||||||
|
if: >-
|
||||||
|
steps.version.outputs.skip != 'true' &&
|
||||||
|
steps.check.outputs.tag_exists != '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
|
||||||
|
|
||||||
|
- name: Commit release changes
|
||||||
|
if: >-
|
||||||
|
steps.version.outputs.skip != 'true' &&
|
||||||
|
steps.check.outputs.tag_exists != '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
|
||||||
|
|
||||||
|
# ── Version Branch ───────────────────────────────────────────────
|
||||||
|
- name: Archive version branch
|
||||||
|
if: steps.check.outputs.tag_exists != 'true'
|
||||||
|
run: |
|
||||||
|
BRANCH="${{ steps.version.outputs.branch }}"
|
||||||
|
git push origin HEAD:"$BRANCH" --force
|
||||||
|
echo "Updated archive branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# ── Tag & Release ────────────────────────────────────────────────
|
||||||
|
- name: Create git tag
|
||||||
|
if: >-
|
||||||
|
steps.version.outputs.skip != 'true' &&
|
||||||
|
steps.check.outputs.tag_exists != 'true' &&
|
||||||
|
steps.version.outputs.is_first == 'true'
|
||||||
|
run: |
|
||||||
|
TAG="${{ steps.version.outputs.release_tag }}"
|
||||||
|
if ! git rev-parse "$TAG" >/dev/null 2>&1; then
|
||||||
|
git tag "$TAG"
|
||||||
|
git push origin "$TAG"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: 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 }}"
|
||||||
|
MAJOR="${{ steps.version.outputs.major }}"
|
||||||
|
BRANCH="${{ steps.version.outputs.branch }}"
|
||||||
|
TOOL_COUNT="${{ steps.tools.outputs.count }}"
|
||||||
|
REPO_NAME="${{ github.event.repository.name }}"
|
||||||
|
|
||||||
|
# Build release notes
|
||||||
|
NOTES=$(php /tmp/mokostandards/api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null)
|
||||||
|
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "$NOTES"
|
||||||
|
echo ""
|
||||||
|
echo "---"
|
||||||
|
echo ""
|
||||||
|
echo "### MCP Server Info"
|
||||||
|
echo "- **Tools registered**: ${TOOL_COUNT}"
|
||||||
|
echo "- **Node.js**: 20+"
|
||||||
|
echo "- **MCP SDK**: $(node -p \"require('./package.json').dependencies['@modelcontextprotocol/sdk']\" 2>/dev/null || echo 'unknown')"
|
||||||
|
echo ""
|
||||||
|
cat /tmp/tool-inventory.md 2>/dev/null || true
|
||||||
|
} > /tmp/release_notes.md
|
||||||
|
|
||||||
|
EXISTING=$(gh release view "$RELEASE_TAG" --json tagName -q .tagName 2>/dev/null || true)
|
||||||
|
|
||||||
|
ARTIFACT="/tmp/${REPO_NAME}-${VERSION}.tar.gz"
|
||||||
|
|
||||||
|
if [ -z "$EXISTING" ]; then
|
||||||
|
gh release create "$RELEASE_TAG" \
|
||||||
|
--title "v${MAJOR} (latest: ${VERSION})" \
|
||||||
|
--notes-file /tmp/release_notes.md \
|
||||||
|
--target "$BRANCH" \
|
||||||
|
"$ARTIFACT"
|
||||||
|
echo "Release created: ${RELEASE_TAG} (${VERSION})" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
gh release edit "$RELEASE_TAG" \
|
||||||
|
--title "v${MAJOR} (latest: ${VERSION})" \
|
||||||
|
--notes-file /tmp/release_notes.md
|
||||||
|
gh release upload "$RELEASE_TAG" "$ARTIFACT" --clobber 2>/dev/null || true
|
||||||
|
echo "Release updated: ${RELEASE_TAG} -> ${VERSION}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Summary ──────────────────────────────────────────────────────
|
||||||
|
- name: Pipeline Summary
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
|
TOOL_COUNT="${{ steps.tools.outputs.count }}"
|
||||||
|
if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
|
||||||
|
echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "## MCP Release Complete" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Detail | Value |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Tools | ${TOOL_COUNT} |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Tag | \`${{ steps.version.outputs.release_tag }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# MCP Server Build & Validation
|
||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# Builds the MCP server, validates TypeScript compilation, and checks
|
||||||
|
# that tools are properly registered with valid Zod schemas.
|
||||||
|
|
||||||
|
name: "MCP: Build & Validate"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main, dev/**]
|
||||||
|
paths: ['src/**', 'package.json', 'tsconfig.json']
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
paths: ['src/**', 'package.json', 'tsconfig.json']
|
||||||
|
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [20, 22]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: TypeScript compile
|
||||||
|
run: npx tsc --noEmit
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Verify dist output exists
|
||||||
|
run: |
|
||||||
|
test -f dist/index.js || (echo "ERROR: dist/index.js not found" && exit 1)
|
||||||
|
test -f dist/client.js || (echo "ERROR: dist/client.js not found" && exit 1)
|
||||||
|
test -f dist/config.js || (echo "ERROR: dist/config.js not found" && exit 1)
|
||||||
|
test -f dist/types.js || (echo "ERROR: dist/types.js not found" && exit 1)
|
||||||
|
echo "✓ All required dist files present"
|
||||||
|
|
||||||
|
- name: Verify shebang in index.js
|
||||||
|
run: |
|
||||||
|
head -1 dist/index.js | grep -q "#!/usr/bin/env node" || echo "WARNING: Missing shebang in dist/index.js"
|
||||||
|
|
||||||
|
- name: Count registered tools
|
||||||
|
run: |
|
||||||
|
TOOL_COUNT=$(grep -c "server\.tool(" src/index.ts || true)
|
||||||
|
echo "Registered tools: ${TOOL_COUNT}"
|
||||||
|
if [ "${TOOL_COUNT}" -eq 0 ]; then
|
||||||
|
echo "ERROR: No tools registered in src/index.ts"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
# MCP SDK Version Check
|
||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# Weekly check for MCP SDK updates. Creates an issue when a new version
|
||||||
|
# of @modelcontextprotocol/sdk is available.
|
||||||
|
|
||||||
|
name: "MCP: SDK Version Check"
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 9 * * 1' # Every Monday at 9am UTC
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-sdk:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
|
- name: Check for SDK updates
|
||||||
|
id: sdk-check
|
||||||
|
run: |
|
||||||
|
CURRENT=$(node -p "require('./package.json').dependencies['@modelcontextprotocol/sdk']" | sed 's/[\^~]//')
|
||||||
|
LATEST=$(npm view @modelcontextprotocol/sdk version 2>/dev/null || echo "unknown")
|
||||||
|
|
||||||
|
echo "current=${CURRENT}" >> $GITHUB_OUTPUT
|
||||||
|
echo "latest=${LATEST}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
if [ "${CURRENT}" != "${LATEST}" ] && [ "${LATEST}" != "unknown" ]; then
|
||||||
|
echo "update_available=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "MCP SDK update available: ${CURRENT} → ${LATEST}"
|
||||||
|
else
|
||||||
|
echo "update_available=false" >> $GITHUB_OUTPUT
|
||||||
|
echo "MCP SDK is up to date: ${CURRENT}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Check for Zod updates
|
||||||
|
id: zod-check
|
||||||
|
run: |
|
||||||
|
CURRENT=$(node -p "require('./package.json').dependencies['zod']" | sed 's/[\^~]//')
|
||||||
|
LATEST=$(npm view zod version 2>/dev/null || echo "unknown")
|
||||||
|
|
||||||
|
echo "current=${CURRENT}" >> $GITHUB_OUTPUT
|
||||||
|
echo "latest=${LATEST}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
if [ "${CURRENT}" != "${LATEST}" ] && [ "${LATEST}" != "unknown" ]; then
|
||||||
|
echo "update_available=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "update_available=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Create update issue
|
||||||
|
if: steps.sdk-check.outputs.update_available == 'true'
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const title = `chore(deps): update @modelcontextprotocol/sdk ${process.env.CURRENT} → ${process.env.LATEST}`;
|
||||||
|
const body = [
|
||||||
|
'## MCP SDK Update Available',
|
||||||
|
'',
|
||||||
|
`| Package | Current | Latest |`,
|
||||||
|
`|---------|---------|--------|`,
|
||||||
|
`| @modelcontextprotocol/sdk | ${process.env.CURRENT} | ${process.env.LATEST} |`,
|
||||||
|
`| zod | ${process.env.ZOD_CURRENT} | ${process.env.ZOD_LATEST} |`,
|
||||||
|
'',
|
||||||
|
'### Steps',
|
||||||
|
'1. Update package.json',
|
||||||
|
'2. Run `npm install`',
|
||||||
|
'3. Run `npm run build` to verify compilation',
|
||||||
|
'4. Test all tools against target API',
|
||||||
|
'',
|
||||||
|
'### Changelog',
|
||||||
|
`https://github.com/modelcontextprotocol/typescript-sdk/releases`,
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
// Check for existing open issue
|
||||||
|
const existing = await github.rest.issues.listForRepo({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
state: 'open',
|
||||||
|
labels: 'api-change',
|
||||||
|
});
|
||||||
|
|
||||||
|
const alreadyExists = existing.data.some(i => i.title.includes('@modelcontextprotocol/sdk'));
|
||||||
|
if (!alreadyExists) {
|
||||||
|
await github.rest.issues.create({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
labels: ['api-change', 'chore'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
env:
|
||||||
|
CURRENT: ${{ steps.sdk-check.outputs.current }}
|
||||||
|
LATEST: ${{ steps.sdk-check.outputs.latest }}
|
||||||
|
ZOD_CURRENT: ${{ steps.zod-check.outputs.current }}
|
||||||
|
ZOD_LATEST: ${{ steps.zod-check.outputs.latest }}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# MCP Tool Inventory
|
||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# Generates a tool inventory report on each push to main.
|
||||||
|
# Extracts tool names, descriptions, and parameter counts from src/index.ts.
|
||||||
|
|
||||||
|
name: "MCP: Tool Inventory"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths: ['src/index.ts']
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
inventory:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Generate tool inventory
|
||||||
|
run: |
|
||||||
|
echo "# MCP Tool Inventory" > TOOLS.md
|
||||||
|
echo "" >> TOOLS.md
|
||||||
|
echo "Auto-generated from \`src/index.ts\` on $(date -u +%Y-%m-%dT%H:%M:%SZ)" >> TOOLS.md
|
||||||
|
echo "" >> TOOLS.md
|
||||||
|
|
||||||
|
# Count tools
|
||||||
|
TOOL_COUNT=$(grep -c "server\.tool(" src/index.ts || true)
|
||||||
|
echo "**Total tools: ${TOOL_COUNT}**" >> TOOLS.md
|
||||||
|
echo "" >> TOOLS.md
|
||||||
|
|
||||||
|
# Extract tool names and descriptions
|
||||||
|
echo "| Tool | Description |" >> TOOLS.md
|
||||||
|
echo "|------|-------------|" >> TOOLS.md
|
||||||
|
|
||||||
|
grep -A1 "server\.tool(" src/index.ts | grep -E "^\s*'" | while read -r line; do
|
||||||
|
TOOL_NAME=$(echo "$line" | sed "s/.*'\([^']*\)'.*/\1/")
|
||||||
|
# Get next line for description
|
||||||
|
DESC=$(grep -A2 "'${TOOL_NAME}'" src/index.ts | grep -E "^\s*'" | tail -1 | sed "s/.*'\([^']*\)'.*/\1/" || echo "")
|
||||||
|
echo "| \`${TOOL_NAME}\` | ${DESC} |" >> TOOLS.md
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "" >> TOOLS.md
|
||||||
|
echo "---" >> TOOLS.md
|
||||||
|
echo "*Generated by MCP Tool Inventory workflow*" >> TOOLS.md
|
||||||
|
|
||||||
|
cat TOOLS.md
|
||||||
|
|
||||||
|
- name: Upload inventory artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: tool-inventory
|
||||||
|
path: TOOLS.md
|
||||||
|
retention-days: 90
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: moko-platform.Notifications
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
|
# PATH: /.gitea/workflows/notify.yml
|
||||||
|
# VERSION: 01.00.00
|
||||||
|
# BRIEF: Push notifications via ntfy on release success or workflow failure
|
||||||
|
|
||||||
|
name: "Universal: Notifications"
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows:
|
||||||
|
- "Joomla Build & Release"
|
||||||
|
- "Joomla Extension CI"
|
||||||
|
- "Deploy"
|
||||||
|
types:
|
||||||
|
- completed
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
env:
|
||||||
|
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
|
||||||
|
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-releases' }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
notify:
|
||||||
|
name: Send Notification
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: >-
|
||||||
|
github.event.workflow_run.conclusion == 'success' ||
|
||||||
|
github.event.workflow_run.conclusion == 'failure'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Notify on success (releases only)
|
||||||
|
if: >-
|
||||||
|
github.event.workflow_run.conclusion == 'success' &&
|
||||||
|
contains(github.event.workflow_run.name, 'Release')
|
||||||
|
run: |
|
||||||
|
REPO="${{ github.event.repository.name }}"
|
||||||
|
WORKFLOW="${{ github.event.workflow_run.name }}"
|
||||||
|
URL="${{ github.event.workflow_run.html_url }}"
|
||||||
|
|
||||||
|
curl -sS \
|
||||||
|
-H "Title: ${REPO} released" \
|
||||||
|
-H "Tags: white_check_mark,package" \
|
||||||
|
-H "Priority: default" \
|
||||||
|
-H "Click: ${URL}" \
|
||||||
|
-d "${WORKFLOW} completed successfully." \
|
||||||
|
"${NTFY_URL}/${NTFY_TOPIC}"
|
||||||
|
|
||||||
|
- name: Notify on failure
|
||||||
|
if: github.event.workflow_run.conclusion == 'failure'
|
||||||
|
run: |
|
||||||
|
REPO="${{ github.event.repository.name }}"
|
||||||
|
WORKFLOW="${{ github.event.workflow_run.name }}"
|
||||||
|
URL="${{ github.event.workflow_run.html_url }}"
|
||||||
|
|
||||||
|
curl -sS \
|
||||||
|
-H "Title: ${REPO} workflow failed" \
|
||||||
|
-H "Tags: x,warning" \
|
||||||
|
-H "Priority: high" \
|
||||||
|
-H "Click: ${URL}" \
|
||||||
|
-d "${WORKFLOW} failed. Check the run for details." \
|
||||||
|
"${NTFY_URL}/${NTFY_TOPIC}"
|
||||||
@@ -0,0 +1,508 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: moko-platform.CI
|
||||||
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
||||||
|
# PATH: /templates/workflows/universal/pr-check.yml.template
|
||||||
|
# VERSION: 09.23.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
|
||||||
|
;;
|
||||||
|
patch/*)
|
||||||
|
if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then
|
||||||
|
ALLOWED=false
|
||||||
|
REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
hotfix/*)
|
||||||
|
if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
|
||||||
|
ALLOWED=false
|
||||||
|
REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
rc)
|
||||||
|
if [ "$BASE" != "main" ]; then
|
||||||
|
ALLOWED=false
|
||||||
|
REASON="RC branch can only merge into '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: Check for merge conflict markers
|
||||||
|
run: |
|
||||||
|
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
|
||||||
|
if [ -n "$CONFLICTS" ]; then
|
||||||
|
echo "::error::Merge conflict markers found in source files"
|
||||||
|
echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "No conflict markers found"
|
||||||
|
|
||||||
|
- name: Detect platform
|
||||||
|
id: platform
|
||||||
|
run: |
|
||||||
|
# Read platform from XML manifest (<platform> tag) or plain text fallback
|
||||||
|
PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1)
|
||||||
|
[ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]')
|
||||||
|
[ -z "$PLATFORM" ] && PLATFORM="generic"
|
||||||
|
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
|
||||||
|
run: |
|
||||||
|
if ! command -v php &> /dev/null; then
|
||||||
|
sudo apt-get update -qq
|
||||||
|
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: PHP syntax check
|
||||||
|
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
|
||||||
|
run: |
|
||||||
|
ERRORS=0
|
||||||
|
while IFS= read -r -d '' file; do
|
||||||
|
if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0)
|
||||||
|
echo "PHP lint: ${ERRORS} error(s)"
|
||||||
|
[ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
|
||||||
|
|
||||||
|
- name: Joomla JEXEC guard check
|
||||||
|
if: steps.platform.outputs.platform == 'joomla'
|
||||||
|
run: |
|
||||||
|
ERRORS=0
|
||||||
|
while IFS= read -r -d '' file; do
|
||||||
|
# Skip vendor, node_modules, and index.html stub files
|
||||||
|
case "$file" in ./vendor/*|./node_modules/*) continue ;; esac
|
||||||
|
# Check first 10 lines for JEXEC or JPATH guard
|
||||||
|
if ! head -20 "$file" | grep -qE "defined\s*\(\s*['\"](_JEXEC|JPATH_BASE|\\\\JPATH_PLATFORM)['\"]"; then
|
||||||
|
echo "::error file=${file}::Missing JEXEC guard: ${file}"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
done < <(find . -name "*.php" -path "*/src/*" -not -path "./.git/*" -not -path "./vendor/*" -print0)
|
||||||
|
if [ "$ERRORS" -gt 0 ]; then
|
||||||
|
echo "::error::${ERRORS} PHP file(s) missing defined('_JEXEC') or die guard"
|
||||||
|
echo "## JEXEC Guard Check: Failed" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "${ERRORS} file(s) in src/ are missing the Joomla execution guard." >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "JEXEC guard: OK"
|
||||||
|
|
||||||
|
- name: Joomla directory listing protection
|
||||||
|
if: steps.platform.outputs.platform == 'joomla'
|
||||||
|
run: |
|
||||||
|
MISSING=0
|
||||||
|
SOURCE_DIR="src"
|
||||||
|
[ ! -d "$SOURCE_DIR" ] && exit 0
|
||||||
|
while IFS= read -r dir; do
|
||||||
|
if [ ! -f "${dir}/index.html" ]; then
|
||||||
|
echo "::warning::Missing index.html in ${dir} (directory listing protection)"
|
||||||
|
MISSING=$((MISSING + 1))
|
||||||
|
fi
|
||||||
|
done < <(find "$SOURCE_DIR" -type d -not -path "./.git/*" -not -path "*/vendor/*" -not -path "*/node_modules/*")
|
||||||
|
if [ "$MISSING" -gt 0 ]; then
|
||||||
|
echo "## Directory Protection" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "${MISSING} director(ies) missing index.html" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
echo "Directory protection: ${MISSING} missing (advisory)"
|
||||||
|
|
||||||
|
- name: Joomla script file and asset checks
|
||||||
|
if: steps.platform.outputs.platform == 'joomla'
|
||||||
|
run: |
|
||||||
|
ERRORS=0
|
||||||
|
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
||||||
|
[ -z "$MANIFEST" ] && exit 0
|
||||||
|
MANIFEST_DIR=$(dirname "$MANIFEST")
|
||||||
|
|
||||||
|
# Check scriptfile exists if declared
|
||||||
|
SCRIPTFILE=$(sed -n 's/.*<scriptfile>\([^<]*\)<\/scriptfile>.*/\1/p' "$MANIFEST" 2>/dev/null)
|
||||||
|
if [ -n "$SCRIPTFILE" ]; then
|
||||||
|
if [ ! -f "${MANIFEST_DIR}/${SCRIPTFILE}" ]; then
|
||||||
|
echo "::error::Manifest declares <scriptfile>${SCRIPTFILE}</scriptfile> but file not found at ${MANIFEST_DIR}/${SCRIPTFILE}"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "Script file: ${MANIFEST_DIR}/${SCRIPTFILE} (OK)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Require joomla.asset.json and validate it
|
||||||
|
ASSET_JSON=$(find "$MANIFEST_DIR" -name "joomla.asset.json" -not -path "./.git/*" 2>/dev/null | head -1)
|
||||||
|
if [ -z "$ASSET_JSON" ]; then
|
||||||
|
echo "::error::joomla.asset.json not found — Joomla asset system is required"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
if command -v php &> /dev/null; then
|
||||||
|
php -r "json_decode(file_get_contents('$ASSET_JSON')); if(json_last_error()!==JSON_ERROR_NONE){echo json_last_error_msg();exit(1);}" 2>&1 || {
|
||||||
|
echo "::error::joomla.asset.json is not valid JSON"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
echo "joomla.asset.json: valid"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validate all XML files in src/ are well-formed
|
||||||
|
XML_ERRORS=0
|
||||||
|
if command -v php &> /dev/null; then
|
||||||
|
while IFS= read -r -d '' xmlfile; do
|
||||||
|
if ! php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$xmlfile'); if(!\$x){foreach(libxml_get_errors() as \$e) echo trim(\$e->message) . ' in $xmlfile'; exit(1);}" 2>&1; then
|
||||||
|
XML_ERRORS=$((XML_ERRORS + 1))
|
||||||
|
fi
|
||||||
|
done < <(find "$MANIFEST_DIR" -name "*.xml" -not -path "./.git/*" -print0)
|
||||||
|
fi
|
||||||
|
if [ "$XML_ERRORS" -gt 0 ]; then
|
||||||
|
echo "::error::${XML_ERRORS} XML file(s) are malformed"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "XML well-formedness: OK"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ "$ERRORS" -gt 0 ] && exit 1
|
||||||
|
echo "Joomla asset checks: OK"
|
||||||
|
|
||||||
|
- 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
|
||||||
|
# Block legacy raw/branch update server URLs on MokoGitea
|
||||||
|
RAW_URLS=$(grep -n 'raw/branch' "$MANIFEST" | grep -i 'mokoconsulting\|mokogitea\|git\.mokoconsulting\.tech' || true)
|
||||||
|
if [ -n "$RAW_URLS" ]; then
|
||||||
|
echo "::error::Manifest contains legacy raw/branch update server URL on MokoGitea. Use the Gitea Pages URL instead (e.g. /{REPO}/updates.xml not /{REPO}/raw/branch/main/updates.xml)"
|
||||||
|
echo "$RAW_URLS"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
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: Validate Joomla language files
|
||||||
|
if: steps.platform.outputs.platform == 'joomla'
|
||||||
|
run: |
|
||||||
|
ERRORS=0
|
||||||
|
WARNINGS=0
|
||||||
|
|
||||||
|
# Require both en-GB and en-US language directories
|
||||||
|
LANG_ROOT=$(find . -path "*/language" -type d -not -path "./.git/*" 2>/dev/null | head -1)
|
||||||
|
if [ -z "$LANG_ROOT" ]; then
|
||||||
|
echo "No language/ directory found — skipping"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d "$LANG_ROOT/en-GB" ]; then
|
||||||
|
echo "::error::Missing en-GB language directory (${LANG_ROOT}/en-GB)"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
if [ ! -d "$LANG_ROOT/en-US" ]; then
|
||||||
|
echo "::error::Missing en-US language directory (${LANG_ROOT}/en-US)"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check that en-GB and en-US have matching .ini files
|
||||||
|
if [ -d "$LANG_ROOT/en-GB" ] && [ -d "$LANG_ROOT/en-US" ]; then
|
||||||
|
for GB_INI in "$LANG_ROOT/en-GB"/*.ini; do
|
||||||
|
[ ! -f "$GB_INI" ] && continue
|
||||||
|
US_INI="$LANG_ROOT/en-US/$(basename "$GB_INI")"
|
||||||
|
if [ ! -f "$US_INI" ]; then
|
||||||
|
echo "::error::$(basename "$GB_INI") exists in en-GB but missing from en-US"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
for US_INI in "$LANG_ROOT/en-US"/*.ini; do
|
||||||
|
[ ! -f "$US_INI" ] && continue
|
||||||
|
GB_INI="$LANG_ROOT/en-GB/$(basename "$US_INI")"
|
||||||
|
if [ ! -f "$GB_INI" ]; then
|
||||||
|
echo "::error::$(basename "$US_INI") exists in en-US but missing from en-GB"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Find all .ini language files
|
||||||
|
INI_FILES=$(find . -path "*/language/*/*.ini" -not -path "./.git/*" 2>/dev/null)
|
||||||
|
if [ -z "$INI_FILES" ]; then
|
||||||
|
echo "No .ini language files found"
|
||||||
|
[ "$ERRORS" -gt 0 ] && exit 1
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Found $(echo "$INI_FILES" | wc -l) language file(s)"
|
||||||
|
|
||||||
|
for FILE in $INI_FILES; do
|
||||||
|
FNAME=$(basename "$FILE")
|
||||||
|
LINENUM=0
|
||||||
|
SEEN_KEYS=""
|
||||||
|
|
||||||
|
while IFS= read -r line || [ -n "$line" ]; do
|
||||||
|
LINENUM=$((LINENUM + 1))
|
||||||
|
|
||||||
|
# Skip empty lines and comments
|
||||||
|
[ -z "$line" ] && continue
|
||||||
|
echo "$line" | grep -qE '^\s*;' && continue
|
||||||
|
echo "$line" | grep -qE '^\s*$' && continue
|
||||||
|
|
||||||
|
# Must match KEY="VALUE" format
|
||||||
|
if ! echo "$line" | grep -qE '^[A-Z_][A-Z0-9_]*=".*"$'; then
|
||||||
|
echo "::error file=${FILE},line=${LINENUM}::Malformed line: ${line}"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract key and check for duplicates
|
||||||
|
KEY=$(echo "$line" | sed 's/=.*//')
|
||||||
|
if echo "$SEEN_KEYS" | grep -qx "$KEY"; then
|
||||||
|
echo "::error file=${FILE},line=${LINENUM}::Duplicate key: ${KEY}"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
SEEN_KEYS="${SEEN_KEYS}
|
||||||
|
${KEY}"
|
||||||
|
done < "$FILE"
|
||||||
|
|
||||||
|
echo " ${FILE}: checked ${LINENUM} lines"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Cross-check en-GB vs en-US key consistency
|
||||||
|
GB_DIR=$(find . -path "*/language/en-GB" -type d -not -path "./.git/*" 2>/dev/null | head -1)
|
||||||
|
US_DIR=$(find . -path "*/language/en-US" -type d -not -path "./.git/*" 2>/dev/null | head -1)
|
||||||
|
|
||||||
|
if [ -n "$GB_DIR" ] && [ -n "$US_DIR" ]; then
|
||||||
|
for GB_FILE in "$GB_DIR"/*.ini; do
|
||||||
|
[ ! -f "$GB_FILE" ] && continue
|
||||||
|
FNAME=$(basename "$GB_FILE")
|
||||||
|
US_FILE="$US_DIR/$FNAME"
|
||||||
|
[ ! -f "$US_FILE" ] && continue
|
||||||
|
|
||||||
|
GB_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$GB_FILE" 2>/dev/null | sort)
|
||||||
|
US_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$US_FILE" 2>/dev/null | sort)
|
||||||
|
|
||||||
|
# Keys in en-GB but not en-US
|
||||||
|
MISSING_US=$(comm -23 <(echo "$GB_KEYS") <(echo "$US_KEYS"))
|
||||||
|
if [ -n "$MISSING_US" ]; then
|
||||||
|
echo "::warning::Keys in en-GB/$FNAME but missing from en-US/$FNAME:"
|
||||||
|
echo "$MISSING_US" | while read -r k; do echo " - $k"; done
|
||||||
|
WARNINGS=$((WARNINGS + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Keys in en-US but not en-GB
|
||||||
|
MISSING_GB=$(comm -13 <(echo "$GB_KEYS") <(echo "$US_KEYS"))
|
||||||
|
if [ -n "$MISSING_GB" ]; then
|
||||||
|
echo "::warning::Keys in en-US/$FNAME but missing from en-GB/$FNAME:"
|
||||||
|
echo "$MISSING_GB" | while read -r k; do echo " - $k"; done
|
||||||
|
WARNINGS=$((WARNINGS + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "### Language File Validation"
|
||||||
|
echo "| Metric | Count |"
|
||||||
|
echo "|---|---|"
|
||||||
|
echo "| Files checked | $(echo "$INI_FILES" | wc -l) |"
|
||||||
|
echo "| Errors | ${ERRORS} |"
|
||||||
|
echo "| Warnings | ${WARNINGS} |"
|
||||||
|
} >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
if [ "$ERRORS" -gt 0 ]; then
|
||||||
|
echo "::error::Language validation failed with ${ERRORS} error(s)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Language files: OK (${WARNINGS} warning(s))"
|
||||||
|
|
||||||
|
- name: Check changelog has unreleased entry
|
||||||
|
run: |
|
||||||
|
if [ ! -f "CHANGELOG.md" ]; then
|
||||||
|
echo "::warning::No CHANGELOG.md found"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
# Check for content under [Unreleased] section
|
||||||
|
if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then
|
||||||
|
echo "::error::CHANGELOG.md missing [Unreleased] section"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased
|
||||||
|
UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true)
|
||||||
|
if [ "$UNRELEASED_CONTENT" -eq 0 ]; then
|
||||||
|
echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes."
|
||||||
|
echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]"
|
||||||
|
|
||||||
|
- 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; }
|
||||||
|
|
||||||
|
# ── Pre-Release RC Build ─────────────────────────────────────────────────
|
||||||
|
pre-release:
|
||||||
|
name: Build RC Package
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [branch-policy, validate]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Trigger RC pre-release
|
||||||
|
env:
|
||||||
|
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
BRANCH: ${{ github.head_ref }}
|
||||||
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
run: |
|
||||||
|
curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
|
||||||
|
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# ── Issue Reporter ──────────────────────────────────────────────────────
|
||||||
|
report-issues:
|
||||||
|
name: Report Issues
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [branch-policy, validate]
|
||||||
|
if: >-
|
||||||
|
always() &&
|
||||||
|
needs.validate.result == 'failure'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
sparse-checkout: automation/ci-issue-reporter.sh
|
||||||
|
sparse-checkout-cone-mode: false
|
||||||
|
|
||||||
|
- name: "File issue for PR validation failure"
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
run: |
|
||||||
|
chmod +x automation/ci-issue-reporter.sh
|
||||||
|
./automation/ci-issue-reporter.sh \
|
||||||
|
--gate "PR Validation" \
|
||||||
|
--workflow "PR Check" \
|
||||||
|
--severity error \
|
||||||
|
--details "PR validation failed (syntax, manifest, changelog, or source checks). See the CI run for the specific check that failed."
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
# 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.01.00
|
||||||
|
# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches
|
||||||
|
|
||||||
|
name: "Universal: Pre-Release"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
- 'fix/**'
|
||||||
|
- 'patch/**'
|
||||||
|
- 'hotfix/**'
|
||||||
|
- 'bugfix/**'
|
||||||
|
- 'chore/**'
|
||||||
|
- alpha
|
||||||
|
- beta
|
||||||
|
- rc
|
||||||
|
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 || github.ref_name }})"
|
||||||
|
runs-on: release
|
||||||
|
if: >-
|
||||||
|
github.event_name == 'workflow_dispatch' ||
|
||||||
|
github.event_name == 'push'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
ref: ${{ github.ref_name }}
|
||||||
|
|
||||||
|
- name: Setup moko-platform tools
|
||||||
|
env:
|
||||||
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
|
run: |
|
||||||
|
# Use pre-installed /opt/moko-platform if available (updated by cron every 6h)
|
||||||
|
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/cli/manifest_element.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
|
||||||
|
echo Using pre-installed /opt/moko-platform
|
||||||
|
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo Falling back to fresh clone
|
||||||
|
if ! command -v composer > /dev/null 2>&1; then
|
||||||
|
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
|
||||||
|
fi
|
||||||
|
rm -rf /tmp/moko-platform-api
|
||||||
|
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git
|
||||||
|
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/moko-platform-api
|
||||||
|
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
|
||||||
|
echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Detect platform
|
||||||
|
id: platform
|
||||||
|
run: |
|
||||||
|
# Auto-detect and update platform if not set in manifest
|
||||||
|
php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true
|
||||||
|
php ${MOKO_CLI}/manifest_read.php --path . --github-output
|
||||||
|
|
||||||
|
- name: Resolve metadata and bump version
|
||||||
|
id: meta
|
||||||
|
run: |
|
||||||
|
# Auto-detect stability from branch name on push, or use input on dispatch
|
||||||
|
if [ "${{ github.event_name }}" = "push" ]; then
|
||||||
|
case "${{ github.ref_name }}" in
|
||||||
|
rc) STABILITY="release-candidate" ;;
|
||||||
|
alpha) STABILITY="alpha" ;;
|
||||||
|
beta) STABILITY="beta" ;;
|
||||||
|
*) STABILITY="development" ;;
|
||||||
|
esac
|
||||||
|
else
|
||||||
|
STABILITY="${{ inputs.stability || 'development' }}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$STABILITY" in
|
||||||
|
development) SUFFIX="-dev"; TAG="development" ;;
|
||||||
|
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
|
||||||
|
beta) SUFFIX="-beta"; TAG="beta" ;;
|
||||||
|
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Bump version via CLI: patch for dev/alpha/beta, minor for RC
|
||||||
|
case "$STABILITY" in
|
||||||
|
release-candidate) BUMP="minor" ;;
|
||||||
|
*) BUMP="patch" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
php ${MOKO_CLI}/version_bump.php --path . $([ "$BUMP" = "minor" ] && echo "--minor") 2>/dev/null || true
|
||||||
|
|
||||||
|
# Set stability suffix and verify consistency
|
||||||
|
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "00.00.01")
|
||||||
|
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
||||||
|
|
||||||
|
php ${MOKO_CLI}/version_set_platform.php \
|
||||||
|
--path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true
|
||||||
|
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
|
||||||
|
|
||||||
|
# Ensure licensing tags (updateservers, dlid) if enabled in manifest.xml
|
||||||
|
php ${MOKO_CLI}/manifest_licensing.php --path . --fix 2>/dev/null || true
|
||||||
|
|
||||||
|
# Append suffix for output
|
||||||
|
if [ -n "$SUFFIX" ]; then
|
||||||
|
VERSION="${VERSION}${SUFFIX}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Commit version bump
|
||||||
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
|
git config --local user.name "gitea-actions[bot]"
|
||||||
|
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||||
|
git add -A
|
||||||
|
git diff --cached --quiet || {
|
||||||
|
git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]"
|
||||||
|
git push origin HEAD 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Auto-detect element via manifest_element.php
|
||||||
|
php ${MOKO_CLI}/manifest_element.php \
|
||||||
|
--path . --version "$VERSION" --stability "$STABILITY" \
|
||||||
|
--repo "${GITEA_REPO}" --github-output
|
||||||
|
|
||||||
|
# Read back element outputs
|
||||||
|
EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
|
||||||
|
ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
|
||||||
|
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
|
||||||
|
[ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.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: Create release
|
||||||
|
id: release
|
||||||
|
run: |
|
||||||
|
TAG="${{ steps.meta.outputs.tag }}"
|
||||||
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
php ${MOKO_CLI}/release_create.php \
|
||||||
|
--path . --version "$VERSION" --tag "$TAG" \
|
||||||
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
|
--repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease
|
||||||
|
|
||||||
|
- name: Update release notes from CHANGELOG.md
|
||||||
|
run: |
|
||||||
|
TAG="${{ steps.meta.outputs.tag }}"
|
||||||
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
|
||||||
|
# Extract [Unreleased] section from changelog (everything between [Unreleased] and next ## heading)
|
||||||
|
if [ -f "CHANGELOG.md" ]; then
|
||||||
|
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
|
||||||
|
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
|
||||||
|
else
|
||||||
|
NOTES="Release ${VERSION}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Update release body via API
|
||||||
|
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
|
"${API_BASE}/releases/tags/${TAG}" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [ -n "$RELEASE_ID" ]; then
|
||||||
|
python3 -c "
|
||||||
|
import json, urllib.request
|
||||||
|
body = open('/dev/stdin').read()
|
||||||
|
payload = json.dumps({'body': body}).encode()
|
||||||
|
req = urllib.request.Request(
|
||||||
|
'${API_BASE}/releases/${RELEASE_ID}',
|
||||||
|
data=payload, method='PATCH',
|
||||||
|
headers={
|
||||||
|
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
})
|
||||||
|
urllib.request.urlopen(req)
|
||||||
|
" <<< "$NOTES"
|
||||||
|
echo "Release notes updated from CHANGELOG.md"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Build package and upload
|
||||||
|
id: package
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
|
TAG="${{ steps.meta.outputs.tag }}"
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
php ${MOKO_CLI}/release_package.php \
|
||||||
|
--path . --version "$VERSION" --tag "$TAG" \
|
||||||
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
|
--repo "${GITEA_REPO}" --output /tmp || true
|
||||||
|
|
||||||
|
# updates.xml is generated dynamically by MokoGitea license server
|
||||||
|
# No need to build, commit, or sync updates.xml from workflows
|
||||||
|
|
||||||
|
- 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.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
|
php ${MOKO_CLI}/release_cascade.php \
|
||||||
|
--stability "${{ steps.meta.outputs.stability }}" \
|
||||||
|
--token "${TOKEN}" \
|
||||||
|
--api-base "${API_BASE}"
|
||||||
|
|
||||||
|
- name: Summary
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
|
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||||
|
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
|
||||||
|
SHA256="${{ steps.package.outputs.sha256_zip }}"
|
||||||
|
echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
@@ -0,0 +1,711 @@
|
|||||||
|
# ============================================================================
|
||||||
|
# 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: moko-platform.Validation
|
||||||
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
||||||
|
# PATH: /templates/workflows/joomla/repo_health.yml.template
|
||||||
|
# VERSION: 09.23.00
|
||||||
|
# BRIEF: Enforces repository guardrails by validating scripts governance, tooling availability, and core repository health artifacts.
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
name: "Generic: Repo Health"
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
profile:
|
||||||
|
description: 'Validation profile: all, scripts, or repo'
|
||||||
|
required: true
|
||||||
|
default: all
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- all
|
||||||
|
- scripts
|
||||||
|
- repo
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
env:
|
||||||
|
# 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,.mokogitea/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: .mokogitea/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.MOKOGITEA_TOKEN || secrets.MOKOGITEA_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
|
||||||
|
|
||||||
|
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|scripts|repo) ;;
|
||||||
|
*)
|
||||||
|
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ "${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
|
||||||
|
|
||||||
|
if [ -n "${SCRIPTS_REQUIRED_DIRS:-}" ]; then IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}"; else required_dirs=(); fi
|
||||||
|
IFS=',' read -r -a allowed_dirs <<< "${SCRIPTS_ALLOWED_DIRS}"
|
||||||
|
|
||||||
|
missing_dirs=()
|
||||||
|
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|scripts|repo) ;;
|
||||||
|
*)
|
||||||
|
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ "${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
|
||||||
|
|
||||||
|
IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}"
|
||||||
|
IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}"
|
||||||
|
if [ -n "${REPO_DISALLOWED_DIRS:-}" ]; then IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}"; else disallowed_dirs=(); fi
|
||||||
|
IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES:-}"
|
||||||
|
|
||||||
|
missing_required=()
|
||||||
|
missing_optional=()
|
||||||
|
|
||||||
|
# Source directory: src/ or htdocs/ (either is valid for extension repos)
|
||||||
|
SOURCE_DIR=""
|
||||||
|
if [ -d "src" ]; then
|
||||||
|
SOURCE_DIR="src"
|
||||||
|
elif [ -d "htdocs" ]; then
|
||||||
|
SOURCE_DIR="htdocs"
|
||||||
|
elif [ -d "deploy" ] || [ -d "cli" ] || [ -d "monitoring" ]; then
|
||||||
|
# Platform/tooling repos don't need src/
|
||||||
|
SOURCE_DIR=""
|
||||||
|
else
|
||||||
|
missing_required+=("src/ or htdocs/ (source directory required)")
|
||||||
|
fi
|
||||||
|
|
||||||
|
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 ] && [ "${#dev_branches[@]}" -eq 0 ]; then
|
||||||
|
missing_required+=("dev or dev/* branch")
|
||||||
|
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=$(printf '{"profile":"%s","missing_required":%d,"missing_optional":%d,"content_warnings":%d}' "$profile" "${#missing_required[@]}" "${#missing_optional[@]}" "${#content_warnings[@]}")
|
||||||
|
|
||||||
|
{
|
||||||
|
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
|
||||||
|
|
||||||
|
if [ -n "${SOURCE_DIR}" ]; then
|
||||||
|
INDEX_DIRS=("${SOURCE_DIR}" "${SOURCE_DIR}/admin" "${SOURCE_DIR}/site")
|
||||||
|
for dir in "${INDEX_DIRS[@]}"; do
|
||||||
|
if [ -d "${dir}" ] && [ ! -f "${dir}/index.html" ]; then
|
||||||
|
joomla_findings+=("${dir}/index.html missing (directory listing protection)")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${#joomla_findings[@]}" -gt 0 ]; then
|
||||||
|
{
|
||||||
|
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=""
|
||||||
|
while IFS= read -r docline; do
|
||||||
|
for link in $(echo "$docline" | grep -oE '\]\([^)]+\)' | sed 's/\](//' | sed 's/)$//' || true); do
|
||||||
|
case "$link" in http://*|https://*|"#"*|mailto:*) continue ;; esac
|
||||||
|
linkpath="${link%%#*}"
|
||||||
|
linkpath="${linkpath%%\?*}"
|
||||||
|
[ -z "$linkpath" ] && continue
|
||||||
|
if [ "${linkpath:0:1}" = "/" ]; then
|
||||||
|
testpath="${linkpath#/}"
|
||||||
|
else
|
||||||
|
testpath="$(dirname "${DOCS_INDEX}")/${linkpath}"
|
||||||
|
fi
|
||||||
|
[ ! -e "$testpath" ] && missing_links="${missing_links}${testpath} "
|
||||||
|
done
|
||||||
|
done < "${DOCS_INDEX}"
|
||||||
|
if [ -n "${missing_links}" ]; then
|
||||||
|
extended_findings+=("docs/docs-index.md contains broken relative links")
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Docs index link integrity'
|
||||||
|
printf '%s\n' 'Broken relative links:'
|
||||||
|
for bl in ${missing_links}; do
|
||||||
|
printf '%s\n' "- ${bl}"
|
||||||
|
done
|
||||||
|
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 policy | N/A | Releases handled by MokoGitea |'
|
||||||
|
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}"
|
||||||
|
|
||||||
|
|
||||||
|
site-health:
|
||||||
|
name: Site Health
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event_name == 'workflow_dispatch'
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: '8.3'
|
||||||
|
|
||||||
|
- name: Uptime check
|
||||||
|
if: env.URLS != ''
|
||||||
|
run: |
|
||||||
|
echo "$URLS" > /tmp/urls.txt
|
||||||
|
php monitoring/uptime-probe.php --urls /tmp/urls.txt --timeout 15 || echo "::warning::Some sites are down"
|
||||||
|
rm -f /tmp/urls.txt
|
||||||
|
env:
|
||||||
|
URLS: ${{ vars.MONITORED_URLS }}
|
||||||
|
|
||||||
|
- name: SSL certificate check
|
||||||
|
if: env.DOMAINS != ''
|
||||||
|
run: |
|
||||||
|
echo "$DOMAINS" > /tmp/domains.txt
|
||||||
|
php monitoring/ssl-check.php --domains /tmp/domains.txt --warn-days 30 || echo "::warning::SSL certificates expiring soon"
|
||||||
|
rm -f /tmp/domains.txt
|
||||||
|
env:
|
||||||
|
DOMAINS: ${{ vars.MONITORED_DOMAINS }}
|
||||||
|
|
||||||
|
- name: Summary
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
echo "### Site Health" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Uptime and SSL checks completed." >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════
|
||||||
|
# Issue Reporter — file issues for failed gates
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════
|
||||||
|
report-issues:
|
||||||
|
name: "Report Issues"
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [access_check, scripts_governance, repo_health]
|
||||||
|
if: >-
|
||||||
|
always() &&
|
||||||
|
(needs.scripts_governance.result == 'failure' ||
|
||||||
|
needs.repo_health.result == 'failure')
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
sparse-checkout: automation/ci-issue-reporter.sh
|
||||||
|
sparse-checkout-cone-mode: false
|
||||||
|
|
||||||
|
- name: "File issues for failed gates"
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
run: |
|
||||||
|
chmod +x automation/ci-issue-reporter.sh
|
||||||
|
REPORTER="./automation/ci-issue-reporter.sh"
|
||||||
|
WF="Repo Health"
|
||||||
|
|
||||||
|
report_gate() {
|
||||||
|
local gate="$1" result="$2" details="$3"
|
||||||
|
if [ "$result" = "failure" ]; then
|
||||||
|
"$REPORTER" --gate "$gate" --details "$details" --workflow "$WF" --severity error
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
report_gate "Scripts Governance" \
|
||||||
|
"${{ needs.scripts_governance.result }}" \
|
||||||
|
"Scripts directory policy violations detected. Review required and allowed directories."
|
||||||
|
|
||||||
|
report_gate "Repository Health" \
|
||||||
|
"${{ needs.repo_health.result }}" \
|
||||||
|
"Repository health checks failed — missing required artifacts, disallowed files, or content warnings. Check the CI run summary."
|
||||||
@@ -0,0 +1,525 @@
|
|||||||
|
# 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 .mokogitea/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: "Universal: 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 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
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: moko-platform.Security
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
|
# PATH: /.gitea/workflows/security-audit.yml
|
||||||
|
# VERSION: 01.00.00
|
||||||
|
# BRIEF: Dependency vulnerability scanning for composer and npm packages
|
||||||
|
|
||||||
|
name: "Universal: Security Audit"
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 6 * * 1' # Weekly on Monday at 06:00 UTC
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- 'composer.json'
|
||||||
|
- 'composer.lock'
|
||||||
|
- 'package.json'
|
||||||
|
- 'package-lock.json'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
env:
|
||||||
|
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
|
||||||
|
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
audit:
|
||||||
|
name: Dependency Audit
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Composer audit
|
||||||
|
if: hashFiles('composer.lock') != ''
|
||||||
|
run: |
|
||||||
|
echo "=== Composer Security Audit ==="
|
||||||
|
if ! command -v composer &> /dev/null; then
|
||||||
|
sudo apt-get update -qq
|
||||||
|
sudo apt-get install -y -qq php-cli composer >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
composer audit --format=plain 2>&1 | tee /tmp/composer-audit.txt
|
||||||
|
RESULT=$?
|
||||||
|
if [ $RESULT -ne 0 ]; then
|
||||||
|
echo "::warning::Composer vulnerabilities found"
|
||||||
|
echo "composer_vulnerable=true" >> "$GITHUB_ENV"
|
||||||
|
else
|
||||||
|
echo "No known vulnerabilities in composer dependencies"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: NPM audit
|
||||||
|
if: hashFiles('package-lock.json') != ''
|
||||||
|
run: |
|
||||||
|
echo "=== NPM Security Audit ==="
|
||||||
|
npm audit --production 2>&1 | tee /tmp/npm-audit.txt || true
|
||||||
|
if npm audit --production 2>&1 | grep -q "found 0 vulnerabilities"; then
|
||||||
|
echo "No known vulnerabilities in npm dependencies"
|
||||||
|
else
|
||||||
|
echo "::warning::NPM vulnerabilities found"
|
||||||
|
echo "npm_vulnerable=true" >> "$GITHUB_ENV"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Notify on vulnerabilities
|
||||||
|
if: env.composer_vulnerable == 'true' || env.npm_vulnerable == 'true'
|
||||||
|
run: |
|
||||||
|
REPO="${{ github.event.repository.name }}"
|
||||||
|
curl -sS \
|
||||||
|
-H "Title: ${REPO} has vulnerable dependencies" \
|
||||||
|
-H "Tags: lock,warning" \
|
||||||
|
-H "Priority: high" \
|
||||||
|
-d "Security audit found vulnerabilities. Review dependency updates." \
|
||||||
|
"${NTFY_URL}/${NTFY_TOPIC}" || true
|
||||||
|
|
||||||
|
|
||||||
|
- name: Joomla version audit
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
if [ -f "monitoring/joomla-version-audit.php" ] && [ -n "$JOOMLA_SITES" ]; then
|
||||||
|
echo "$JOOMLA_SITES" > /tmp/sites.json
|
||||||
|
php monitoring/joomla-version-audit.php --sites /tmp/sites.json || true
|
||||||
|
echo "### Joomla Version Audit" >> $GITHUB_STEP_SUMMARY
|
||||||
|
rm -f /tmp/sites.json
|
||||||
|
else
|
||||||
|
echo "Joomla audit skipped (no script or JOOMLA_SITES_JSON not configured)"
|
||||||
|
fi
|
||||||
|
env:
|
||||||
|
JOOMLA_SITES: ${{ vars.JOOMLA_SITES_JSON }}
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,133 @@
|
|||||||
|
# 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 .mokogitea/workflows/sync-version-on-merge.yml in all governed repos.
|
||||||
|
# README.md is the single source of truth for the repository version.
|
||||||
|
|
||||||
|
name: "Universal: Sync Version on Merge"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
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
|
||||||
|
|
||||||
|
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 == 'push' && 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
|
||||||
+4
-59
@@ -1,66 +1,11 @@
|
|||||||
<!-- 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
|
|
||||||
|
|
||||||
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 (./LICENSE).
|
|
||||||
|
|
||||||
# FILE INFORMATION
|
|
||||||
DEFGROUP:
|
|
||||||
INGROUP: Project.Documentation
|
|
||||||
REPO: mokoconsulting-tech/MokoStandards-Template-Generic
|
|
||||||
VERSION: 00.00.01
|
|
||||||
PATH: ./CHANGELOG.md
|
|
||||||
BRIEF: Version history and change log
|
|
||||||
-->
|
|
||||||
|
|
||||||
# Changelog
|
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Added
|
|
||||||
- GitHub Actions firewall configuration for Copilot agent
|
|
||||||
- `.github/copilot/firewall-allowlist.json`: Allowlist for enterprise-ready sites and license sources
|
|
||||||
- `.github/copilot/setup-firewall.sh`: Setup script to configure firewall in GitHub Actions
|
|
||||||
- `.github/copilot/README.md`: Documentation for firewall configuration usage
|
|
||||||
- GitHub Actions workflow for Copilot coding agent
|
|
||||||
- `.github/workflows/copilot-agent.yml`: Example workflow with firewall configuration integrated
|
|
||||||
- Firewall allowlist includes: gnu.org, fsf.org, opensource.org, apache.org, spdx.org, creativecommons.org, github.com
|
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Renamed LICENSE.md to LICENSE (removed .md extension per convention)
|
- Migrated all workflow and template paths from `.github/` to `.mokogitea/`
|
||||||
- Updated all references from LICENSE.md to LICENSE across documentation
|
- Template source paths updated: `templates/gitea/` to `templates/mokogitea/`
|
||||||
|
- HCL definition files removed -- Template repos are now the canonical source
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- Initial repository structure
|
- `branch-cleanup.yml`: auto-delete merged feature branches after PR merge
|
||||||
- Basic documentation templates
|
|
||||||
- MokoStandards-compliant README.md
|
|
||||||
- LICENSE with GPL-3.0-or-later
|
|
||||||
- CONTRIBUTING.md with contribution guidelines
|
|
||||||
- CODE_OF_CONDUCT.md (Contributor Covenant v1.3.0)
|
|
||||||
- CHANGELOG.md for version tracking
|
|
||||||
|
|
||||||
## [0.1.0] - 2026-01-16
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- Initial project setup
|
|
||||||
- Repository template structure for generic coding projects
|
|
||||||
- Documentation index files
|
|
||||||
- EditorConfig for consistent coding styles
|
|
||||||
- Git configuration files (.gitattributes, .gitignore, .gitmessage)
|
|
||||||
|
|
||||||
## Revision History
|
|
||||||
|
|
||||||
| Date | Version | Author | Notes |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| 2026-01-16 | 0.1.0 | Copilot | Initial changelog |
|
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code when working with this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
**joomla-api-mcp** -- MCP server for Joomla Web Services API operations
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Platform** | mcp-server |
|
||||||
|
| **Language** | TypeScript |
|
||||||
|
| **Default branch** | main |
|
||||||
|
| **License** | GPL-3.0-or-later |
|
||||||
|
| **Wiki** | [joomla-api-mcp Wiki](https://git.mokoconsulting.tech/MokoConsulting/joomla-api-mcp/wiki) |
|
||||||
|
| **Standards** | [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home) |
|
||||||
|
|
||||||
|
## Common Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install # Install dependencies
|
||||||
|
npm run build # Compile TypeScript
|
||||||
|
npm run dev # Development mode
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
This is an MCP (Model Context Protocol) server. Key files:
|
||||||
|
- `src/index.ts` -- server entry point and tool registration
|
||||||
|
- `src/config.ts` -- configuration loading
|
||||||
|
- `src/tools/` -- individual tool implementations
|
||||||
|
- `dist/` -- compiled output (gitignored)
|
||||||
|
|
||||||
|
## 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)
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
<!-- Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
|
<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
|
||||||
This file is part of a Moko Consulting project.
|
This file is part of a Moko Consulting project.
|
||||||
|
|
||||||
|
|||||||
+36
-2
@@ -1,4 +1,4 @@
|
|||||||
<!-- Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
|
<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
|
||||||
This file is part of a Moko Consulting project.
|
This file is part of a Moko Consulting project.
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
DEFGROUP:
|
DEFGROUP:
|
||||||
INGROUP: Project.Documentation
|
INGROUP: Project.Documentation
|
||||||
REPO: mokoconsulting-tech/MokoStandards-Template-Generic
|
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-Template-Generic
|
||||||
VERSION: 00.00.01
|
VERSION: 00.00.01
|
||||||
PATH: ./CONTRIBUTING.md
|
PATH: ./CONTRIBUTING.md
|
||||||
BRIEF: Contribution guidelines for the project
|
BRIEF: Contribution guidelines for the project
|
||||||
@@ -125,3 +125,37 @@ If you have questions about contributing, feel free to open an issue or contact
|
|||||||
| Date | Version | Author | Notes |
|
| Date | Version | Author | Notes |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| 2026-01-16 | 0.1.0 | Copilot | Initial contributing guidelines |
|
| 2026-01-16 | 0.1.0 | Copilot | Initial contributing guidelines |
|
||||||
|
|
||||||
|
## Infrastructure Standards
|
||||||
|
|
||||||
|
All repositories in the MokoConsulting org follow these conventions:
|
||||||
|
|
||||||
|
### Release Tags
|
||||||
|
|
||||||
|
Every repo maintains 5 standard release channel tags:
|
||||||
|
|
||||||
|
- `development` - Active development builds
|
||||||
|
- `alpha` - Early internal testing
|
||||||
|
- `beta` - Broader testing / client UAT
|
||||||
|
- `release-candidate` - Final QA before production
|
||||||
|
- `stable` - Production release
|
||||||
|
|
||||||
|
### Branch Protection
|
||||||
|
|
||||||
|
- `main` is protected; only `jmiller` can push directly
|
||||||
|
- All other contributors must use pull requests
|
||||||
|
- PRs are automatically reviewed by Claude Code
|
||||||
|
|
||||||
|
### CI/CD
|
||||||
|
|
||||||
|
- Gitea Actions runs all CI workflows
|
||||||
|
- GitHub Actions are disabled on mirrored repos
|
||||||
|
- Workflows live in both `.github/workflows/` and `.gitea/workflows/`
|
||||||
|
|
||||||
|
### Update Servers (Joomla)
|
||||||
|
|
||||||
|
In manifest `<updateservers>`, Gitea must be priority 1, GitHub priority 2.
|
||||||
|
|
||||||
|
### Secrets
|
||||||
|
|
||||||
|
All repos have `GA_TOKEN` and `GH_TOKEN` as Actions secrets for API access.
|
||||||
|
|||||||
@@ -1,133 +1,104 @@
|
|||||||
<!-- Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
|
# joomla-api-mcp
|
||||||
|
|
||||||
This file is part of a Moko Consulting project.
|
MCP server for Joomla Web Services API operations
|
||||||
|
|
||||||
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 (./LICENSE).
|
|
||||||
|
|
||||||
# FILE INFORMATION
|
|
||||||
DEFGROUP:
|
|
||||||
INGROUP: Project.Documentation
|
|
||||||
REPO: mokoconsulting-tech/MokoStandards-Template-Generic
|
|
||||||
VERSION: 00.00.01
|
|
||||||
PATH: ./README.md
|
|
||||||
BRIEF: Generic coding project template according to MokoStandards
|
|
||||||
-->
|
|
||||||
|
|
||||||
|
|
||||||
[](https://github.com/mokoconsulting-tech/MokoStandards-Template-Generic/releases/tag/v00)
|
Model Context Protocol server for the Joomla Web Services API -- full CRUD for articles, categories, users, menus, contacts, tags, media, plugins, and more.
|
||||||
[](LICENSE)
|
|
||||||
[](https://www.php.net)
|
|
||||||
|
|
||||||
# MokoStandards-Template-Generic
|
---
|
||||||
|
|
||||||
[](https://github.com/RichardLitt/standard-readme)
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Type** | MCP Server |
|
||||||
|
| **Language** | Node.js |
|
||||||
|
| **Tools** | 67 tools across 15 resource groups |
|
||||||
|
| **License** | GPL-3.0-or-later |
|
||||||
|
| **Platform** | [Gitea](https://git.mokoconsulting.tech/MokoConsulting/joomla-api-mcp) (primary) |
|
||||||
|
|
||||||
A template repository for generic coding projects that follows MokoStandards conventions.
|
---
|
||||||
|
|
||||||
This template provides a standardized structure for any coding project, including proper documentation, licensing, contribution guidelines, and project organization. It is designed to help you quickly bootstrap new projects with best practices and consistent conventions.
|
## Overview
|
||||||
|
|
||||||
## Table of Contents
|
joomla-api-mcp wraps the entire Joomla Web Services REST API into MCP tools that Claude Code can call directly. It supports multi-site connections via named connection configs.
|
||||||
|
|
||||||
- [Background](#background)
|
### Resource Coverage
|
||||||
- [Install](#install)
|
|
||||||
- [Usage](#usage)
|
|
||||||
- [Structure](#structure)
|
|
||||||
- [Contributing](#contributing)
|
|
||||||
- [License](#license)
|
|
||||||
- [Maintainers](#maintainers)
|
|
||||||
|
|
||||||
## Background
|
| Group | Operations |
|
||||||
|
|-------|------------|
|
||||||
|
| Articles | list, get, create, update, delete |
|
||||||
|
| Categories | list, create, update, delete |
|
||||||
|
| Users | list, get, create, update, delete, groups |
|
||||||
|
| Contacts | list, get, create, update, delete |
|
||||||
|
| Menus | list types, list/get/create/update/delete items |
|
||||||
|
| Tags | list, get, create, update, delete |
|
||||||
|
| Custom Fields | list, get, create, delete |
|
||||||
|
| Plugins | list, update (enable/disable) |
|
||||||
|
| Modules | list |
|
||||||
|
| Templates | list |
|
||||||
|
| Media | list, get file, delete file, create folder |
|
||||||
|
| Banners | list, get, create, delete, list clients |
|
||||||
|
| Newsfeeds | list, get, create, delete |
|
||||||
|
| Messages | list, get, send, delete |
|
||||||
|
| Redirects | list, create, delete |
|
||||||
|
| Config | get, update |
|
||||||
|
| Content History | list versions |
|
||||||
|
| Checkin | unlock checked-out items |
|
||||||
|
| Associations | list multilingual associations |
|
||||||
|
| Generic | raw API request, list connections |
|
||||||
|
|
||||||
MokoStandards-Template-Generic is a repository template designed to provide a consistent foundation for generic coding projects. It includes:
|
---
|
||||||
|
|
||||||
- Standard documentation structure (README, LICENSE, CONTRIBUTING, CODE_OF_CONDUCT, CHANGELOG)
|
## Wiki Pages
|
||||||
- MokoStandards-compliant file headers and metadata
|
|
||||||
- EditorConfig for consistent coding styles across editors
|
|
||||||
- Git configuration templates
|
|
||||||
- Documentation index system for easy navigation
|
|
||||||
|
|
||||||
This template follows the [standard-readme](https://github.com/RichardLitt/standard-readme) specification and incorporates MokoStandards conventions for enterprise-grade project organization.
|
### Guides
|
||||||
|
|
||||||
## Install
|
- [Installation](https://git.mokoconsulting.tech/MokoConsulting/joomla-api-mcp/wiki/INSTALLATION) -- setup, .env configuration, Claude Code registration, multi-site connections
|
||||||
|
|
||||||
To use this template:
|
### Reference
|
||||||
|
|
||||||
1. Click the "Use this template" button on GitHub
|
- [API](https://git.mokoconsulting.tech/MokoConsulting/joomla-api-mcp/wiki/API) -- all 67 tools with parameters, grouped by resource type
|
||||||
2. Create a new repository from this template
|
- [Architecture](https://git.mokoconsulting.tech/MokoConsulting/joomla-api-mcp/wiki/ARCHITECTURE) -- source file map, component design, connection handling
|
||||||
3. Clone your new repository locally:
|
|
||||||
|
|
||||||
```sh
|
### Templates
|
||||||
git clone https://github.com/your-username/your-new-repo.git
|
|
||||||
cd your-new-repo
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Update the project-specific details:
|
- [Templates Index](https://git.mokoconsulting.tech/MokoConsulting/joomla-api-mcp/wiki/templates-index) -- workflow and config templates
|
||||||
- Update README.md with your project name and description
|
- [README Template](templates-README-template) -- README template for MCP repos
|
||||||
- Update LICENSE if using a different license
|
|
||||||
- Update file headers with appropriate REPO, DEFGROUP, and BRIEF values
|
|
||||||
- Update CHANGELOG.md with your version history
|
|
||||||
|
|
||||||
## Usage
|
---
|
||||||
|
|
||||||
This template is designed to be customized for your specific project needs.
|
## Related Wikis
|
||||||
|
|
||||||
### Getting Started
|
| Repo | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| [backup-mcp](https://git.mokoconsulting.tech/MokoConsulting/backup-mcp/wiki) | Backup MCP (uses Joomla API for Akeeba) |
|
||||||
|
| [MokoWaaS](https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/wiki) | Joomla WaaS system plugin |
|
||||||
|
| [Template-Client-WaaS](https://git.mokoconsulting.tech/MokoConsulting/Template-Client-WaaS/wiki) | Client site template |
|
||||||
|
| [MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/wiki) | Joomla admin template |
|
||||||
|
|
||||||
1. Replace placeholder text with your project details
|
---
|
||||||
2. Add your source code to the `src/` directory
|
|
||||||
3. Add scripts to the `scripts/` directory
|
|
||||||
4. Add documentation to the `docs/` directory
|
|
||||||
5. Update the CHANGELOG.md as you make changes
|
|
||||||
|
|
||||||
### Project Structure
|
> **[MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki)** -- central standards hub for all Moko Consulting projects.
|
||||||
|
|
||||||
```
|
---
|
||||||
.
|
|
||||||
├── docs/ # Documentation files
|
|
||||||
├── scripts/ # Build and utility scripts
|
|
||||||
├── src/ # Source code
|
|
||||||
├── README.md # This file
|
|
||||||
├── LICENSE # License information
|
|
||||||
├── CONTRIBUTING.md # Contribution guidelines
|
|
||||||
├── CODE_OF_CONDUCT.md # Code of conduct
|
|
||||||
└── CHANGELOG.md # Version history
|
|
||||||
```
|
|
||||||
|
|
||||||
## Structure
|
|
||||||
|
|
||||||
The repository follows MokoStandards conventions:
|
|
||||||
|
|
||||||
- **Documentation**: All `.md` files include copyright headers and file metadata
|
---
|
||||||
- **Index Files**: Each directory contains an `index.md` for navigation
|
|
||||||
- **EditorConfig**: Maintains consistent coding styles (tabs, width 2)
|
## Documentation
|
||||||
- **Git Configuration**: Includes `.gitattributes`, `.gitignore`, and `.gitmessage` templates
|
|
||||||
|
Full documentation is available on the [Wiki](https://git.mokoconsulting.tech/MokoConsulting/joomla-api-mcp/wiki).
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests.
|
See the wiki for development guidelines and contribution instructions.
|
||||||
|
|
||||||
This project follows the [Contributor Covenant](CODE_OF_CONDUCT.md) Code of Conduct.
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This project is licensed under the GNU General Public License v3.0 or later - see the [LICENSE](LICENSE) file for details.
|
This project is licensed under the GNU General Public License v3.0 or later -- see the [LICENSE](LICENSE) file.
|
||||||
|
|
||||||
Copyright © 2025 Moko Consulting <hello@mokoconsulting.tech>
|
---
|
||||||
|
|
||||||
## Maintainers
|
*[Moko Consulting](https://mokoconsulting.tech) -- [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
||||||
|
|
||||||
[@mokoconsulting-tech](https://github.com/mokoconsulting-tech)
|
|
||||||
|
|
||||||
For questions or support, please contact: hello@mokoconsulting.tech
|
|
||||||
|
|
||||||
## Revision History
|
|
||||||
|
|
||||||
| Date | Version | Author | Notes |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| 2026-01-16 | 0.1.0 | Copilot | Initial MokoStandards-compliant README |
|
|
||||||
|
|||||||
-142
@@ -1,142 +0,0 @@
|
|||||||
<!--
|
|
||||||
Copyright (C) 2026 [Organization Name] <contact@example.com>
|
|
||||||
|
|
||||||
This file is part of [Project Name].
|
|
||||||
|
|
||||||
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/>.
|
|
||||||
-->
|
|
||||||
|
|
||||||
# [Project Name] Roadmap
|
|
||||||
|
|
||||||
## Scope and Intent
|
|
||||||
|
|
||||||
This document defines the roadmap for [Project Name] starting with version **[X.Y.Z]**. It establishes the sequencing, intent, and maturity expectations for deliverables governed by this project.
|
|
||||||
|
|
||||||
This roadmap is forward-looking by design. Completed work prior to the current baseline version is considered foundational and is intentionally excluded. This file tracks **current and future-state development only**.
|
|
||||||
|
|
||||||
## Version [X.Y.Z] — [Phase Name] ✅ COMPLETED / 🔄 IN PROGRESS / 📋 PLANNED
|
|
||||||
|
|
||||||
Focus: [Brief description of this version's primary focus]
|
|
||||||
|
|
||||||
### Completed Deliverables
|
|
||||||
|
|
||||||
* ✅ **[Category Name]**
|
|
||||||
* ✅ [Specific deliverable or feature]
|
|
||||||
* ✅ [Specific deliverable or feature]
|
|
||||||
* ✅ **[Category Name]**
|
|
||||||
* ✅ [Specific deliverable or feature]
|
|
||||||
|
|
||||||
### In Progress
|
|
||||||
|
|
||||||
* 🔄 [Item currently being worked on]
|
|
||||||
* 🔄 [Item currently being worked on]
|
|
||||||
|
|
||||||
### Planned Deliverables
|
|
||||||
|
|
||||||
* [Future planned item]
|
|
||||||
* [Future planned item]
|
|
||||||
|
|
||||||
### Achieved Outcomes
|
|
||||||
|
|
||||||
* ✅ [Measurable outcome or benefit]
|
|
||||||
* ✅ [Measurable outcome or benefit]
|
|
||||||
* 🔄 [Partially achieved outcome]
|
|
||||||
|
|
||||||
## Version [X.Y+1.Z] — [Next Phase Name]
|
|
||||||
|
|
||||||
Focus: [Brief description of next version's primary focus]
|
|
||||||
|
|
||||||
### In-Scope Deliverables
|
|
||||||
|
|
||||||
* [Deliverable for next version]
|
|
||||||
* [Deliverable for next version]
|
|
||||||
|
|
||||||
### Outcomes
|
|
||||||
|
|
||||||
* [Expected outcome or benefit]
|
|
||||||
* [Expected outcome or benefit]
|
|
||||||
|
|
||||||
## Version [X.Y+2.Z] — [Future Phase Name]
|
|
||||||
|
|
||||||
Focus: [Brief description of future version's primary focus]
|
|
||||||
|
|
||||||
### Planned Deliverables
|
|
||||||
|
|
||||||
* [Future deliverable]
|
|
||||||
* [Future deliverable]
|
|
||||||
|
|
||||||
### Outcomes
|
|
||||||
|
|
||||||
* [Expected outcome]
|
|
||||||
* [Expected outcome]
|
|
||||||
|
|
||||||
## Version [X.Y+3.Z] and Beyond — [Long-term Vision]
|
|
||||||
|
|
||||||
Focus: [Long-term strategic direction]
|
|
||||||
|
|
||||||
### Forward-Looking Initiatives
|
|
||||||
|
|
||||||
* [Strategic initiative]
|
|
||||||
* [Strategic initiative]
|
|
||||||
|
|
||||||
### Outcomes
|
|
||||||
|
|
||||||
* [Long-term goal]
|
|
||||||
* [Long-term goal]
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
This roadmap is intentionally conservative and additive. New versions extend prior guarantees without breaking existing contracts.
|
|
||||||
|
|
||||||
## Guidelines for Version Planning
|
|
||||||
|
|
||||||
### Version Numbering
|
|
||||||
|
|
||||||
Use semantic versioning for clarity:
|
|
||||||
- **Major versions** (X.0.0): Breaking changes, major architectural shifts
|
|
||||||
- **Minor versions** (X.Y.0): New features, significant enhancements
|
|
||||||
- **Patch versions** (X.Y.Z): Bug fixes, minor improvements
|
|
||||||
|
|
||||||
### Planning Horizon
|
|
||||||
|
|
||||||
- **Current Version**: Actively being delivered
|
|
||||||
- **Next 2-3 Versions**: Clearly defined with specific deliverables
|
|
||||||
- **Future Versions**: High-level themes and strategic direction
|
|
||||||
|
|
||||||
### Status Indicators
|
|
||||||
|
|
||||||
- ✅ **COMPLETED**: Delivered and validated
|
|
||||||
- 🔄 **IN PROGRESS**: Active development underway
|
|
||||||
- 📋 **PLANNED**: Scheduled but not yet started
|
|
||||||
- ⏸️ **PAUSED**: Temporarily on hold
|
|
||||||
- ❌ **CANCELLED**: No longer pursuing
|
|
||||||
|
|
||||||
## Metadata
|
|
||||||
|
|
||||||
```
|
|
||||||
Owner: [Owner Name/Role]
|
|
||||||
Reviewers: [Reviewer Names/Roles]
|
|
||||||
Status: Active
|
|
||||||
Last Updated: [YYYY-MM-DD]
|
|
||||||
Next Review: [YYYY-MM-DD]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Revision History
|
|
||||||
|
|
||||||
| Date | Version | Author | Notes |
|
|
||||||
| ---------- | -------- | -------------- | ------------------------------- |
|
|
||||||
| YYYY-MM-DD | X.Y.Z | [Author Name] | [Description of changes] |
|
|
||||||
| YYYY-MM-DD | X.Y.Z | [Author Name] | [Description of changes] |
|
|
||||||
@@ -45,6 +45,11 @@ Security updates are provided for the following versions:
|
|||||||
Only the current major version receives security updates. Users should upgrade to the latest supported version to receive security patches.
|
Only the current major version receives security updates. Users should upgrade to the latest supported version to receive security patches.
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
Report security vulnerabilities via Gitea issue (preferred):
|
||||||
|
https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-Template-Generic/issues/new?template=security.yaml
|
||||||
|
|
||||||
|
Or email: hello@mokoconsulting.tech
|
||||||
|
|
||||||
### Where to Report
|
### Where to Report
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# TODO
|
||||||
|
|
||||||
|
> **Note:** This file is not tracked in version control (.gitignore). It is for local task tracking only.
|
||||||
|
|
||||||
|
## Critical
|
||||||
|
-
|
||||||
|
|
||||||
|
## Normal
|
||||||
|
-
|
||||||
|
|
||||||
|
## Low
|
||||||
|
-
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ============================================================================
|
||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Automation.CI
|
||||||
|
# INGROUP: moko-platform.Automation
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
|
# PATH: /automation/ci-issue-reporter.sh
|
||||||
|
# VERSION: 09.23.00
|
||||||
|
# BRIEF: Creates or updates a Gitea issue when a CI gate fails.
|
||||||
|
# Deduplicates by searching open issues with the "ci-auto" label
|
||||||
|
# whose title matches the gate. If a matching issue exists, a comment
|
||||||
|
# is appended instead of opening a duplicate.
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ── Defaults ────────────────────────────────────────────────────────────────
|
||||||
|
GITEA_URL="${GITEA_URL:-https://git.mokoconsulting.tech}"
|
||||||
|
GITEA_TOKEN="${GITEA_TOKEN:-}"
|
||||||
|
REPO="${GITHUB_REPOSITORY:-}"
|
||||||
|
RUN_URL="${GITHUB_SERVER_URL:-${GITEA_URL}}/${REPO}/actions/runs/${GITHUB_RUN_ID:-0}"
|
||||||
|
LABEL_NAME="ci-auto"
|
||||||
|
LABEL_COLOR="#e11d48"
|
||||||
|
|
||||||
|
GATE=""
|
||||||
|
DETAILS=""
|
||||||
|
SEVERITY="error"
|
||||||
|
WORKFLOW=""
|
||||||
|
|
||||||
|
# ── Parse arguments ─────────────────────────────────────────────────────────
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: ci-issue-reporter.sh --gate NAME --details TEXT [OPTIONS]
|
||||||
|
|
||||||
|
Required:
|
||||||
|
--gate CI gate name (e.g. "Code Quality", "Self-Health")
|
||||||
|
--details Human-readable failure description
|
||||||
|
|
||||||
|
Optional:
|
||||||
|
--severity "error" (default) or "warning"
|
||||||
|
--workflow Workflow name for the issue title
|
||||||
|
--repo owner/repo (default: \$GITHUB_REPOSITORY)
|
||||||
|
--run-url URL to the CI run (auto-detected from env)
|
||||||
|
--token Gitea API token (default: \$GITEA_TOKEN)
|
||||||
|
--url Gitea base URL (default: \$GITEA_URL)
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--gate) GATE="$2"; shift 2 ;;
|
||||||
|
--details) DETAILS="$2"; shift 2 ;;
|
||||||
|
--severity) SEVERITY="$2"; shift 2 ;;
|
||||||
|
--workflow) WORKFLOW="$2"; shift 2 ;;
|
||||||
|
--repo) REPO="$2"; shift 2 ;;
|
||||||
|
--run-url) RUN_URL="$2"; shift 2 ;;
|
||||||
|
--token) GITEA_TOKEN="$2"; shift 2 ;;
|
||||||
|
--url) GITEA_URL="$2"; shift 2 ;;
|
||||||
|
-h|--help) usage ;;
|
||||||
|
*) echo "Unknown option: $1"; usage ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -z "$GATE" ]] && { echo "ERROR: --gate is required"; usage; }
|
||||||
|
[[ -z "$DETAILS" ]] && { echo "ERROR: --details is required"; usage; }
|
||||||
|
[[ -z "$GITEA_TOKEN" ]] && { echo "ERROR: GITEA_TOKEN not set"; exit 1; }
|
||||||
|
[[ -z "$REPO" ]] && { echo "ERROR: GITHUB_REPOSITORY not set"; exit 1; }
|
||||||
|
|
||||||
|
API="${GITEA_URL}/api/v1/repos/${REPO}"
|
||||||
|
|
||||||
|
# ── Build title ─────────────────────────────────────────────────────────────
|
||||||
|
if [[ -n "$WORKFLOW" ]]; then
|
||||||
|
TITLE="[CI] ${WORKFLOW}: ${GATE} failed"
|
||||||
|
else
|
||||||
|
TITLE="[CI] ${GATE} failed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Ensure label exists ─────────────────────────────────────────────────────
|
||||||
|
ensure_label() {
|
||||||
|
local exists
|
||||||
|
exists=$(curl -sf -o /dev/null -w '%{http_code}' \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
"${API}/labels" 2>/dev/null || echo "000")
|
||||||
|
|
||||||
|
if [[ "$exists" == "200" ]]; then
|
||||||
|
# Check if label already exists
|
||||||
|
local found
|
||||||
|
found=$(curl -sf \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
"${API}/labels" 2>/dev/null \
|
||||||
|
| grep -o "\"name\":\"${LABEL_NAME}\"" || true)
|
||||||
|
|
||||||
|
if [[ -z "$found" ]]; then
|
||||||
|
curl -sf -X POST \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API}/labels" \
|
||||||
|
-d "{\"name\":\"${LABEL_NAME}\",\"color\":\"${LABEL_COLOR}\",\"description\":\"Auto-created by CI issue reporter\"}" \
|
||||||
|
> /dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Search for existing open issue ──────────────────────────────────────────
|
||||||
|
find_existing_issue() {
|
||||||
|
# URL-encode the gate name for the query
|
||||||
|
local query
|
||||||
|
query=$(printf '%s' "[CI] ${GATE}" | sed 's/ /%20/g; s/\[/%5B/g; s/\]/%5D/g')
|
||||||
|
|
||||||
|
local response
|
||||||
|
response=$(curl -sf \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
"${API}/issues?type=issues&state=open&labels=${LABEL_NAME}&q=${query}&limit=5" \
|
||||||
|
2>/dev/null || echo "[]")
|
||||||
|
|
||||||
|
# Extract the first matching issue number
|
||||||
|
echo "$response" \
|
||||||
|
| grep -oP '"number":\s*\K[0-9]+' \
|
||||||
|
| head -1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Build issue body ────────────────────────────────────────────────────────
|
||||||
|
build_body() {
|
||||||
|
local severity_badge
|
||||||
|
if [[ "$SEVERITY" == "error" ]]; then
|
||||||
|
severity_badge="**Severity:** Error"
|
||||||
|
else
|
||||||
|
severity_badge="**Severity:** Warning"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat <<BODY
|
||||||
|
## CI Gate Failure: ${GATE}
|
||||||
|
|
||||||
|
${severity_badge}
|
||||||
|
**Workflow:** ${WORKFLOW:-unknown}
|
||||||
|
**Branch:** ${GITHUB_REF_NAME:-unknown}
|
||||||
|
**Commit:** \`${GITHUB_SHA:0:8}\`
|
||||||
|
**Run:** [View CI run](${RUN_URL})
|
||||||
|
|
||||||
|
### Details
|
||||||
|
|
||||||
|
${DETAILS}
|
||||||
|
|
||||||
|
### Resolution
|
||||||
|
|
||||||
|
Fix the issue described above and push a new commit. This issue will be closed automatically when the gate passes, or can be closed manually.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Auto-created by [ci-issue-reporter](${GITEA_URL}/${REPO}/src/branch/main/automation/ci-issue-reporter.sh)*
|
||||||
|
BODY
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Build comment body (for existing issues) ────────────────────────────────
|
||||||
|
build_comment() {
|
||||||
|
cat <<COMMENT
|
||||||
|
### CI failure recurrence
|
||||||
|
|
||||||
|
**Branch:** ${GITHUB_REF_NAME:-unknown}
|
||||||
|
**Commit:** \`${GITHUB_SHA:0:8}\`
|
||||||
|
**Run:** [View CI run](${RUN_URL})
|
||||||
|
|
||||||
|
${DETAILS}
|
||||||
|
COMMENT
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Main ────────────────────────────────────────────────────────────────────
|
||||||
|
ensure_label
|
||||||
|
|
||||||
|
EXISTING=$(find_existing_issue)
|
||||||
|
|
||||||
|
if [[ -n "$EXISTING" ]]; then
|
||||||
|
# Append comment to existing issue
|
||||||
|
COMMENT_BODY=$(build_comment)
|
||||||
|
COMMENT_JSON=$(printf '%s' "$COMMENT_BODY" | python3 -c "
|
||||||
|
import sys, json
|
||||||
|
print(json.dumps({'body': sys.stdin.read()}))" 2>/dev/null)
|
||||||
|
|
||||||
|
HTTP=$(curl -sf -o /dev/null -w '%{http_code}' -X POST \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API}/issues/${EXISTING}/comments" \
|
||||||
|
-d "${COMMENT_JSON}" 2>/dev/null || echo "000")
|
||||||
|
|
||||||
|
if [[ "$HTTP" == "201" ]]; then
|
||||||
|
echo "Commented on existing issue #${EXISTING}"
|
||||||
|
else
|
||||||
|
echo "WARNING: Failed to comment on issue #${EXISTING} (HTTP ${HTTP})"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Create new issue
|
||||||
|
ISSUE_BODY=$(build_body)
|
||||||
|
ISSUE_JSON=$(python3 -c "
|
||||||
|
import sys, json
|
||||||
|
body = sys.stdin.read()
|
||||||
|
print(json.dumps({
|
||||||
|
'title': sys.argv[1],
|
||||||
|
'body': body,
|
||||||
|
'labels': []
|
||||||
|
}))" "$TITLE" <<< "$ISSUE_BODY" 2>/dev/null)
|
||||||
|
|
||||||
|
# Create the issue
|
||||||
|
RESPONSE=$(curl -sf -X POST \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API}/issues" \
|
||||||
|
-d "${ISSUE_JSON}" 2>/dev/null || echo "{}")
|
||||||
|
|
||||||
|
ISSUE_NUM=$(echo "$RESPONSE" | grep -oP '"number":\s*\K[0-9]+' | head -1)
|
||||||
|
|
||||||
|
if [[ -n "$ISSUE_NUM" ]]; then
|
||||||
|
# Apply label (separate call — more reliable across Gitea versions)
|
||||||
|
LABEL_ID=$(curl -sf \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
"${API}/labels" 2>/dev/null \
|
||||||
|
| grep -oP "\"id\":\s*\K[0-9]+(?=[^}]*\"name\":\s*\"${LABEL_NAME}\")" \
|
||||||
|
| head -1 || true)
|
||||||
|
|
||||||
|
if [[ -n "$LABEL_ID" ]]; then
|
||||||
|
curl -sf -X POST \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API}/issues/${ISSUE_NUM}/labels" \
|
||||||
|
-d "{\"labels\":[${LABEL_ID}]}" \
|
||||||
|
> /dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Created issue #${ISSUE_NUM}: ${TITLE}"
|
||||||
|
else
|
||||||
|
echo "WARNING: Failed to create issue"
|
||||||
|
echo "Response: ${RESPONSE}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"defaultConnection": "production",
|
||||||
|
"connections": {
|
||||||
|
"local-dev": {
|
||||||
|
"baseUrl": "https://localhost:8080",
|
||||||
|
"apiToken": "your-joomla-api-token-here",
|
||||||
|
"insecure": true
|
||||||
|
},
|
||||||
|
"production": {
|
||||||
|
"baseUrl": "https://www.example.com",
|
||||||
|
"apiToken": "your-production-api-token"
|
||||||
|
},
|
||||||
|
"staging": {
|
||||||
|
"baseUrl": "https://staging.example.com",
|
||||||
|
"apiToken": "your-staging-api-token"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,441 +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
|
|
||||||
PATH: /docs/INSTALLATION.md
|
|
||||||
VERSION: 04.00.15
|
|
||||||
BRIEF: Installation and setup instructions for [PROJECT_NAME]
|
|
||||||
-->
|
|
||||||
|
|
||||||
# Installation
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
This document provides comprehensive installation and setup instructions for **[PROJECT_NAME]**.
|
|
||||||
|
|
||||||
## Table of Contents
|
|
||||||
|
|
||||||
- [Prerequisites](#prerequisites)
|
|
||||||
- [Installation Methods](#installation-methods)
|
|
||||||
- [Quick Start](#quick-start)
|
|
||||||
- [Detailed Installation](#detailed-installation)
|
|
||||||
- [Configuration](#configuration)
|
|
||||||
- [Verification](#verification)
|
|
||||||
- [Troubleshooting](#troubleshooting)
|
|
||||||
- [Next Steps](#next-steps)
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
### System Requirements
|
|
||||||
|
|
||||||
- **Operating System**: [Specify supported OS versions]
|
|
||||||
- **Runtime**: [e.g., PHP 8.1+, Node.js 20+, Python 3.9+]
|
|
||||||
- **Memory**: [Minimum RAM required]
|
|
||||||
- **Disk Space**: [Minimum disk space required]
|
|
||||||
|
|
||||||
### Software Dependencies
|
|
||||||
|
|
||||||
**Required:**
|
|
||||||
- [List required dependencies with versions]
|
|
||||||
- Example: Git 2.30+
|
|
||||||
- Example: Composer 2.0+
|
|
||||||
|
|
||||||
**Optional:**
|
|
||||||
- [List optional dependencies]
|
|
||||||
|
|
||||||
### Access Requirements
|
|
||||||
|
|
||||||
- [Any required access permissions, credentials, or accounts]
|
|
||||||
- Example: GitHub account for cloning private repositories
|
|
||||||
- Example: Database access credentials
|
|
||||||
|
|
||||||
## Installation Methods
|
|
||||||
|
|
||||||
### Method 1: Using Package Manager (Recommended)
|
|
||||||
|
|
||||||
**For [Platform/Package Manager]:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Installation command
|
|
||||||
[package-manager] install [package-name]
|
|
||||||
|
|
||||||
# Verify installation
|
|
||||||
[package-name] --version
|
|
||||||
```
|
|
||||||
|
|
||||||
### Method 2: From Source
|
|
||||||
|
|
||||||
**Clone the repository:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Clone from GitHub
|
|
||||||
git clone https://github.com/[organization]/[repository].git
|
|
||||||
cd [repository]
|
|
||||||
|
|
||||||
# Checkout stable version (recommended)
|
|
||||||
git checkout tags/v[VERSION]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Method 3: Using Pre-built Binary/Package
|
|
||||||
|
|
||||||
**Download and install:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Download release
|
|
||||||
wget https://github.com/[organization]/[repository]/releases/download/v[VERSION]/[package-name]
|
|
||||||
|
|
||||||
# Make executable (if applicable)
|
|
||||||
chmod +x [package-name]
|
|
||||||
|
|
||||||
# Move to system path (optional)
|
|
||||||
sudo mv [package-name] /usr/local/bin/
|
|
||||||
```
|
|
||||||
|
|
||||||
## Quick Start
|
|
||||||
|
|
||||||
For users who want to get started quickly:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Install
|
|
||||||
[installation-command]
|
|
||||||
|
|
||||||
# 2. Configure
|
|
||||||
[configuration-command]
|
|
||||||
|
|
||||||
# 3. Run
|
|
||||||
[run-command]
|
|
||||||
|
|
||||||
# 4. Verify
|
|
||||||
[verification-command]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Detailed Installation
|
|
||||||
|
|
||||||
### Step 1: Prepare Environment
|
|
||||||
|
|
||||||
**1.1 Install System Dependencies**
|
|
||||||
|
|
||||||
For Ubuntu/Debian:
|
|
||||||
```bash
|
|
||||||
sudo apt update
|
|
||||||
sudo apt install [dependencies]
|
|
||||||
```
|
|
||||||
|
|
||||||
For macOS:
|
|
||||||
```bash
|
|
||||||
brew install [dependencies]
|
|
||||||
```
|
|
||||||
|
|
||||||
For Windows:
|
|
||||||
```powershell
|
|
||||||
# PowerShell commands or link to Windows-specific guide
|
|
||||||
```
|
|
||||||
|
|
||||||
**1.2 Set Up Environment Variables**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Add to ~/.bashrc or ~/.zshrc
|
|
||||||
export [VAR_NAME]=[value]
|
|
||||||
|
|
||||||
# Reload shell configuration
|
|
||||||
source ~/.bashrc
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 2: Install Application
|
|
||||||
|
|
||||||
**2.1 Install via [Method]**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
[Detailed installation commands with explanations]
|
|
||||||
```
|
|
||||||
|
|
||||||
**2.2 Install Dependencies**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# For PHP projects
|
|
||||||
composer install --no-dev
|
|
||||||
|
|
||||||
# For Node.js projects
|
|
||||||
npm install --production
|
|
||||||
|
|
||||||
# For Python projects
|
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 3: Initial Configuration
|
|
||||||
|
|
||||||
**3.1 Create Configuration File**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Copy example configuration
|
|
||||||
cp config/config.example.php config/config.php
|
|
||||||
|
|
||||||
# Or use configuration wizard
|
|
||||||
php bin/configure.php
|
|
||||||
```
|
|
||||||
|
|
||||||
**3.2 Configure Database (if applicable)**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Create database
|
|
||||||
mysql -u root -p -e "CREATE DATABASE [db_name];"
|
|
||||||
|
|
||||||
# Import schema
|
|
||||||
mysql -u root -p [db_name] < database/schema.sql
|
|
||||||
|
|
||||||
# Update configuration
|
|
||||||
nano config/database.php
|
|
||||||
```
|
|
||||||
|
|
||||||
**3.3 Set Permissions**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Set appropriate ownership
|
|
||||||
sudo chown -R www-data:www-data /var/www/[project]
|
|
||||||
|
|
||||||
# Set directory permissions (755)
|
|
||||||
find /var/www/[project] -type d -exec chmod 755 {} \;
|
|
||||||
|
|
||||||
# Set file permissions (644 for most files)
|
|
||||||
find /var/www/[project] -type f -exec chmod 644 {} \;
|
|
||||||
|
|
||||||
# Make executable files executable (if needed)
|
|
||||||
chmod +x /var/www/[project]/bin/*
|
|
||||||
|
|
||||||
# Restrict sensitive directories (storage, cache, logs)
|
|
||||||
chmod 750 /var/www/[project]/storage
|
|
||||||
chmod 750 /var/www/[project]/cache
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 4: Initialize Application
|
|
||||||
|
|
||||||
**4.1 Run Setup Script**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run initialization
|
|
||||||
php bin/setup.php
|
|
||||||
|
|
||||||
# Or for other platforms
|
|
||||||
./scripts/setup.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
**4.2 Create Admin User (if applicable)**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Create first admin user
|
|
||||||
php bin/create-admin.php --email=admin@example.com --name="Admin User"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
### Configuration Files
|
|
||||||
|
|
||||||
| File | Purpose | Required |
|
|
||||||
|------|---------|----------|
|
|
||||||
| `config/config.php` | Main configuration | Yes |
|
|
||||||
| `config/database.php` | Database settings | Yes |
|
|
||||||
| `config/cache.php` | Cache configuration | No |
|
|
||||||
| `.env` | Environment variables | Yes |
|
|
||||||
|
|
||||||
### Essential Configuration Options
|
|
||||||
|
|
||||||
**config/config.php:**
|
|
||||||
|
|
||||||
```php
|
|
||||||
return [
|
|
||||||
'app_name' => '[APPLICATION_NAME]',
|
|
||||||
'app_url' => 'https://example.com',
|
|
||||||
'debug' => false, // Set to true for development
|
|
||||||
'timezone' => 'UTC',
|
|
||||||
];
|
|
||||||
```
|
|
||||||
|
|
||||||
**Database Configuration:**
|
|
||||||
|
|
||||||
```php
|
|
||||||
return [
|
|
||||||
'host' => 'localhost',
|
|
||||||
'port' => 3306,
|
|
||||||
'database' => '[db_name]',
|
|
||||||
'username' => '[db_user]',
|
|
||||||
'password' => '[db_password]',
|
|
||||||
];
|
|
||||||
```
|
|
||||||
|
|
||||||
### Environment Variables
|
|
||||||
|
|
||||||
Create `.env` file:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
APP_ENV=production
|
|
||||||
APP_DEBUG=false
|
|
||||||
APP_URL=https://example.com
|
|
||||||
|
|
||||||
DB_HOST=localhost
|
|
||||||
DB_PORT=3306
|
|
||||||
DB_DATABASE=[db_name]
|
|
||||||
DB_USERNAME=[db_user]
|
|
||||||
DB_PASSWORD=[db_password]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Verification
|
|
||||||
|
|
||||||
### Verify Installation
|
|
||||||
|
|
||||||
**Check version:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
[command] --version
|
|
||||||
# Expected output: v[VERSION]
|
|
||||||
```
|
|
||||||
|
|
||||||
**Run health check:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
[command] health-check
|
|
||||||
# or
|
|
||||||
php bin/health-check.php
|
|
||||||
```
|
|
||||||
|
|
||||||
**Test basic functionality:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run test command
|
|
||||||
[command] test
|
|
||||||
|
|
||||||
# Access web interface
|
|
||||||
curl http://localhost:[port]/health
|
|
||||||
```
|
|
||||||
|
|
||||||
### Expected Output
|
|
||||||
|
|
||||||
```
|
|
||||||
✓ Application installed successfully
|
|
||||||
✓ Database connection established
|
|
||||||
✓ All dependencies available
|
|
||||||
✓ Configuration valid
|
|
||||||
✓ System ready for use
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Common Issues
|
|
||||||
|
|
||||||
#### Issue: Installation fails with dependency error
|
|
||||||
|
|
||||||
**Symptom:**
|
|
||||||
```
|
|
||||||
Error: Package [package-name] not found
|
|
||||||
```
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
```bash
|
|
||||||
# Update package manager
|
|
||||||
[package-manager] update
|
|
||||||
|
|
||||||
# Retry installation
|
|
||||||
[package-manager] install [package-name]
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Issue: Database connection fails
|
|
||||||
|
|
||||||
**Symptom:**
|
|
||||||
```
|
|
||||||
Error: SQLSTATE[HY000] [2002] Connection refused
|
|
||||||
```
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
1. Verify database service is running:
|
|
||||||
```bash
|
|
||||||
sudo systemctl status mysql
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Check database credentials in configuration
|
|
||||||
|
|
||||||
3. Verify database host and port are correct
|
|
||||||
|
|
||||||
#### Issue: Permission denied errors
|
|
||||||
|
|
||||||
**Symptom:**
|
|
||||||
```
|
|
||||||
Error: Permission denied: /var/www/[project]/storage
|
|
||||||
```
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
```bash
|
|
||||||
# Fix ownership
|
|
||||||
sudo chown -R www-data:www-data /var/www/[project]
|
|
||||||
|
|
||||||
# Fix permissions
|
|
||||||
sudo chmod -R 755 /var/www/[project]/storage
|
|
||||||
```
|
|
||||||
|
|
||||||
### Getting Help
|
|
||||||
|
|
||||||
If you encounter issues not covered here:
|
|
||||||
|
|
||||||
1. **Check Logs:**
|
|
||||||
```bash
|
|
||||||
tail -f logs/application.log
|
|
||||||
tail -f /var/log/apache2/error.log
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Enable Debug Mode:**
|
|
||||||
```bash
|
|
||||||
# In config/config.php
|
|
||||||
'debug' => true
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Consult Documentation:**
|
|
||||||
- [Troubleshooting Guide](guide/troubleshooting.md)
|
|
||||||
- [FAQ](guide/faq.md)
|
|
||||||
|
|
||||||
4. **Community Support:**
|
|
||||||
- GitHub Issues: [link]
|
|
||||||
- Discussion Forum: [link]
|
|
||||||
- Email: support@example.com
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
|
|
||||||
After successful installation:
|
|
||||||
|
|
||||||
1. **Review Configuration:**
|
|
||||||
- [Configuration Guide](guide/configuration.md)
|
|
||||||
- [Security Hardening](guide/security.md)
|
|
||||||
|
|
||||||
2. **Read Getting Started:**
|
|
||||||
- [Quick Start Guide](guide/quickstart.md)
|
|
||||||
- [User Guide](guide/user-guide.md)
|
|
||||||
|
|
||||||
3. **For Developers:**
|
|
||||||
- [Development Setup](development/setup.md)
|
|
||||||
- [Contributing Guidelines](../CONTRIBUTING.md)
|
|
||||||
|
|
||||||
4. **For Operators:**
|
|
||||||
- [Deployment Guide](deployment/procedures.md)
|
|
||||||
- [Monitoring Setup](operations/monitoring.md)
|
|
||||||
|
|
||||||
## Additional Resources
|
|
||||||
|
|
||||||
- [Project Documentation](README.md)
|
|
||||||
- [API Reference](reference/api/)
|
|
||||||
- [Change Log](../CHANGELOG.md)
|
|
||||||
- [Security Policy](../SECURITY.md)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
For installation support:
|
|
||||||
- **Documentation**: Review all guides in [docs/guide/](guide/)
|
|
||||||
- **Issues**: Report problems at [GitHub Issues](https://github.com/[organization]/[repository]/issues)
|
|
||||||
- **Email**: support@mokoconsulting.tech
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Last Updated: [DATE]*
|
|
||||||
*Version: [VERSION]*
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# Docs Index: /templates/docs
|
|
||||||
|
|
||||||
## Purpose
|
|
||||||
|
|
||||||
This index provides navigation to documentation within this folder.
|
|
||||||
|
|
||||||
## Subfolders
|
|
||||||
|
|
||||||
- [extra/](./extra/index.md)
|
|
||||||
- [required/](./required/index.md)
|
|
||||||
|
|
||||||
## Documents
|
|
||||||
|
|
||||||
- [README](./README.md)
|
|
||||||
|
|
||||||
## Metadata
|
|
||||||
|
|
||||||
- **Document Type:** index
|
|
||||||
- **Auto-generated:** This file is automatically generated by rebuild_indexes.py
|
|
||||||
|
|
||||||
## Revision History
|
|
||||||
|
|
||||||
| Change | Notes | Author |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| Automated update | Generated by documentation index automation | rebuild_indexes.py |
|
|
||||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
<!-- Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
|
<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
|
||||||
This file is part of a Moko Consulting project.
|
This file is part of a Moko Consulting project.
|
||||||
|
|
||||||
|
|||||||
+10
-33
@@ -1,8 +1,7 @@
|
|||||||
{
|
{
|
||||||
// The tab key will cycle through the settings when first created
|
// Sublime Text SFTP Plugin
|
||||||
// Visit https://codexns.io/products/sftp_for_subime/settings for help
|
// Visit https://codexns.io/products/sftp_for_subime/settings for help
|
||||||
|
|
||||||
// sftp, ftp or ftps
|
|
||||||
"type": "sftp",
|
"type": "sftp",
|
||||||
|
|
||||||
"save_before_upload": true,
|
"save_before_upload": true,
|
||||||
@@ -14,43 +13,21 @@
|
|||||||
"confirm_sync": true,
|
"confirm_sync": true,
|
||||||
"confirm_overwrite_newer": false,
|
"confirm_overwrite_newer": false,
|
||||||
|
|
||||||
"host": "example.com",
|
"host": "dev.mokoconsulting.tech",
|
||||||
"user": "username",
|
"user": "mokoconsulting_dev",
|
||||||
|
"port": "22",
|
||||||
|
|
||||||
//Default
|
"ssh_key_file": "C:/Users/jmill/OneDrive/Documents/Keys/repos/joomla-api-mcp",
|
||||||
//"port": "22",
|
//"password": "",
|
||||||
|
|
||||||
//Uncomment One:
|
"remote_path": "/home/mokoconsulting_dev/",
|
||||||
//"ssh_key_file": "absolute/path/to/key/file",
|
|
||||||
//"password": "password",
|
|
||||||
|
|
||||||
"remote_path": "/example/path/",
|
|
||||||
"ignore_regexes": [
|
"ignore_regexes": [
|
||||||
"\\.sublime-(project|workspace|settings)", "\\.libsass.json/",
|
"\\.sublime-(project|workspace|settings)", "\\.libsass.json/",
|
||||||
"sftp-config(-alt\\d?)?\\.json",
|
"sftp-config(-alt\\d?)?\\.json", "sftp-config.json.template",
|
||||||
"sftp-settings\\.json", "/venv/", "\\.svn/", "\\.hg/", "\\.git*",
|
"sftp-settings\\.json", "/venv/", "\\.svn/", "\\.hg/", "\\.git*",
|
||||||
"\\.bzr", "_darcs", "CVS", "\\.DS_Store", "Thumbs\\.db", "robots\\.txt",
|
"\\.bzr", "_darcs", "CVS", "\\.DS_Store", "Thumbs\\.db", "robots\\.txt",
|
||||||
"desktop\\.ini", "configuration\\.php",
|
"desktop\\.ini", "\\.ffs*", "\\.editorconfig", "\\.md", "\\.zip", "docs/"
|
||||||
"administrator/components/com_akeebabackup/backup", "\\.ffs*", "\\.md",
|
|
||||||
"\\.zip", "\\.editorconfig"
|
|
||||||
],
|
],
|
||||||
//"file_permissions": "664",
|
|
||||||
//"dir_permissions": "775",
|
|
||||||
|
|
||||||
//"extra_list_connections": 0,
|
"connect_timeout": 30
|
||||||
|
|
||||||
"connect_timeout": 30,
|
|
||||||
//"keepalive": 120,
|
|
||||||
//"ftp_passive_mode": true,
|
|
||||||
//"ftp_obey_passive_host": false,
|
|
||||||
//"ssh_key_file": "~/.ssh/id_rsa",
|
|
||||||
//"sftp_sudo": false,
|
|
||||||
//"sftp_debug": false,
|
|
||||||
//"sftp_flags": ["-F", "/path/to/ssh_config"],
|
|
||||||
|
|
||||||
//"preserve_modification_times": false,
|
|
||||||
//"remote_time_offset_in_hours": 0,
|
|
||||||
//"remote_encoding": "utf-8",
|
|
||||||
//"remote_locale": "C",
|
|
||||||
//"allow_config_upload": false,
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "@mokoconsulting/mcp-mokowaas-api",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "MCP server for Joomla Web Services API operations",
|
||||||
|
"type": "module",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"bin": {
|
||||||
|
"joomla-api-mcp": "dist/index.js"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"dev": "tsc --watch",
|
||||||
|
"start": "node dist/index.js",
|
||||||
|
"lint": "eslint src/",
|
||||||
|
"setup": "node scripts/setup.mjs",
|
||||||
|
"clean": "rm -rf dist/"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
||||||
|
"zod": "^3.24.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.15.3",
|
||||||
|
"typescript": "^5.8.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
},
|
||||||
|
"license": "GPL-3.0-or-later",
|
||||||
|
"author": "Moko Consulting <hello@mokoconsulting.tech>",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://git.mokoconsulting.tech/MokoConsulting/joomla-api-mcp.git"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/* 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: joomla-api-mcp.Scripts
|
||||||
|
* INGROUP: joomla-api-mcp
|
||||||
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/joomla-api-mcp
|
||||||
|
* PATH: /scripts/setup.mjs
|
||||||
|
* VERSION: 00.00.01
|
||||||
|
* BRIEF: Interactive setup — prompts for Joomla API connection details and writes config
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createInterface } from 'node:readline/promises';
|
||||||
|
import { readFile, writeFile } from 'node:fs/promises';
|
||||||
|
import { resolve } from 'node:path';
|
||||||
|
import { homedir } from 'node:os';
|
||||||
|
|
||||||
|
const CONFIG_PATH = resolve(homedir(), '.joomla-api-mcp.json');
|
||||||
|
|
||||||
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
||||||
|
|
||||||
|
async function prompt(question, defaultValue) {
|
||||||
|
const suffix = defaultValue ? ` [${defaultValue}]` : '';
|
||||||
|
const answer = await rl.question(`${question}${suffix}: `);
|
||||||
|
return answer.trim() || defaultValue || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function promptRequired(question) {
|
||||||
|
let answer = '';
|
||||||
|
while (!answer) {
|
||||||
|
answer = (await rl.question(`${question}: `)).trim();
|
||||||
|
if (!answer) {
|
||||||
|
console.log(' This field is required.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return answer;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log('');
|
||||||
|
console.log('=== joomla-api-mcp Setup ===');
|
||||||
|
console.log('');
|
||||||
|
console.log('This will create your configuration file at:');
|
||||||
|
console.log(` ${CONFIG_PATH}`);
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// Check for existing config
|
||||||
|
let existing = null;
|
||||||
|
try {
|
||||||
|
const raw = await readFile(CONFIG_PATH, 'utf-8');
|
||||||
|
existing = JSON.parse(raw);
|
||||||
|
console.log('Existing config found. You can add a new connection or overwrite.');
|
||||||
|
console.log(` Current connections: ${Object.keys(existing.connections).join(', ')}`);
|
||||||
|
console.log('');
|
||||||
|
} catch {
|
||||||
|
// No existing config
|
||||||
|
}
|
||||||
|
|
||||||
|
const connectionName = await prompt('Connection name', 'production');
|
||||||
|
const baseUrl = await promptRequired('Joomla site URL (e.g. https://www.example.com)');
|
||||||
|
const apiToken = await promptRequired('Joomla API token');
|
||||||
|
|
||||||
|
const cleanUrl = baseUrl.replace(/\/+$/, '');
|
||||||
|
|
||||||
|
const insecureAnswer = await prompt('Skip TLS verification for self-signed certs? (y/N)', 'N');
|
||||||
|
const insecure = insecureAnswer.toLowerCase() === 'y';
|
||||||
|
|
||||||
|
const connection = { baseUrl: cleanUrl, apiToken };
|
||||||
|
if (insecure) {
|
||||||
|
connection.insecure = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let config;
|
||||||
|
if (existing) {
|
||||||
|
config = existing;
|
||||||
|
config.connections[connectionName] = connection;
|
||||||
|
const setDefault = await prompt(`Set "${connectionName}" as default connection? (y/N)`, 'N');
|
||||||
|
if (setDefault.toLowerCase() === 'y') {
|
||||||
|
config.defaultConnection = connectionName;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
config = {
|
||||||
|
defaultConnection: connectionName,
|
||||||
|
connections: {
|
||||||
|
[connectionName]: connection,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await writeFile(CONFIG_PATH, JSON.stringify(config, null, '\t') + '\n', 'utf-8');
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
console.log(`Config written to ${CONFIG_PATH}`);
|
||||||
|
console.log(` Connection "${connectionName}" configured for ${cleanUrl}`);
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
const addAnother = await prompt('Add another connection? (y/N)', 'N');
|
||||||
|
if (addAnother.toLowerCase() === 'y') {
|
||||||
|
rl.close();
|
||||||
|
// Re-run to add another
|
||||||
|
const { execFileSync } = await import('node:child_process');
|
||||||
|
execFileSync('node', [new URL(import.meta.url).pathname], { stdio: 'inherit' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Setup complete. You can now use the MCP server.');
|
||||||
|
console.log('');
|
||||||
|
rl.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error(`Setup failed: ${err.message}`);
|
||||||
|
rl.close();
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
+141
@@ -0,0 +1,141 @@
|
|||||||
|
/* 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: joomla-api-mcp.Client
|
||||||
|
* INGROUP: joomla-api-mcp
|
||||||
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/joomla-api-mcp
|
||||||
|
* PATH: /src/client.ts
|
||||||
|
* VERSION: 01.00.00
|
||||||
|
* BRIEF: HTTP client for Joomla Web Services API (v1)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as https from 'node:https';
|
||||||
|
import * as http from 'node:http';
|
||||||
|
import type { JoomlaConnection, ApiResponse } from './types.js';
|
||||||
|
|
||||||
|
const API_PREFIX = '/api/index.php/v1';
|
||||||
|
const TIMEOUT_MS = 30_000;
|
||||||
|
|
||||||
|
export class JoomlaClient {
|
||||||
|
private readonly base_url: string;
|
||||||
|
private readonly headers: Record<string, string>;
|
||||||
|
private readonly insecure: boolean;
|
||||||
|
|
||||||
|
constructor(conn: JoomlaConnection) {
|
||||||
|
this.base_url = conn.baseUrl.replace(/\/+$/, '') + API_PREFIX;
|
||||||
|
this.headers = {
|
||||||
|
'Authorization': `Bearer ${conn.apiToken}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/vnd.api+json',
|
||||||
|
};
|
||||||
|
this.insecure = conn.insecure ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(endpoint: string, params?: Record<string, string>): Promise<ApiResponse> {
|
||||||
|
const url = this.buildUrl(endpoint, params);
|
||||||
|
return this.request(url, 'GET');
|
||||||
|
}
|
||||||
|
|
||||||
|
async post(endpoint: string, body?: unknown): Promise<ApiResponse> {
|
||||||
|
const url = this.buildUrl(endpoint);
|
||||||
|
return this.request(url, 'POST', body);
|
||||||
|
}
|
||||||
|
|
||||||
|
async patch(endpoint: string, body: unknown): Promise<ApiResponse> {
|
||||||
|
const url = this.buildUrl(endpoint);
|
||||||
|
return this.request(url, 'PATCH', body);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(endpoint: string): Promise<ApiResponse> {
|
||||||
|
const url = this.buildUrl(endpoint);
|
||||||
|
return this.request(url, 'DELETE');
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildUrl(endpoint: string, params?: Record<string, string>): string {
|
||||||
|
const path = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
|
||||||
|
const url = new URL(`${this.base_url}${path}`);
|
||||||
|
if (params) {
|
||||||
|
for (const [key, value] of Object.entries(params)) {
|
||||||
|
url.searchParams.set(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private tryParseJson(raw: string): unknown | null {
|
||||||
|
// Try full string first
|
||||||
|
try { return JSON.parse(raw); } catch { /* fall through */ }
|
||||||
|
|
||||||
|
// Joomla may append HTML after JSON — find the last } or ] and try parsing up to that point
|
||||||
|
const trimmed = raw.trimStart();
|
||||||
|
if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) return null;
|
||||||
|
|
||||||
|
const closer = trimmed.startsWith('{') ? '}' : ']';
|
||||||
|
let idx = raw.lastIndexOf(closer);
|
||||||
|
while (idx > 0) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(raw.substring(0, idx + 1));
|
||||||
|
} catch {
|
||||||
|
idx = raw.lastIndexOf(closer, idx - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private request(url: string, method: string, body?: unknown): Promise<ApiResponse> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
const is_https = parsed.protocol === 'https:';
|
||||||
|
const transport = is_https ? https : http;
|
||||||
|
|
||||||
|
const options: https.RequestOptions = {
|
||||||
|
hostname: parsed.hostname,
|
||||||
|
port: parsed.port || (is_https ? 443 : 80),
|
||||||
|
path: parsed.pathname + parsed.search,
|
||||||
|
method,
|
||||||
|
headers: { ...this.headers },
|
||||||
|
timeout: TIMEOUT_MS,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.insecure && is_https) {
|
||||||
|
options.rejectUnauthorized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = body !== undefined ? JSON.stringify(body) : undefined;
|
||||||
|
if (payload) {
|
||||||
|
(options.headers as Record<string, string>)['Content-Length'] = Buffer.byteLength(payload).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const req = transport.request(options, (res) => {
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
res.on('data', (chunk: Buffer) => chunks.push(chunk));
|
||||||
|
res.on('end', () => {
|
||||||
|
const raw = Buffer.concat(chunks).toString('utf-8');
|
||||||
|
let data: unknown;
|
||||||
|
|
||||||
|
// Joomla API may return JSON with text/html content-type,
|
||||||
|
// and may append HTML error fragments after valid JSON.
|
||||||
|
// Try to parse as JSON, trimming trailing non-JSON content.
|
||||||
|
data = this.tryParseJson(raw) ?? raw;
|
||||||
|
|
||||||
|
resolve({ status: res.statusCode ?? 0, data });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', (err) => reject(err));
|
||||||
|
req.on('timeout', () => {
|
||||||
|
req.destroy();
|
||||||
|
reject(new Error('Request timed out'));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (payload) {
|
||||||
|
req.write(payload);
|
||||||
|
}
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
/* 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: joomla-api-mcp.Config
|
||||||
|
* INGROUP: joomla-api-mcp
|
||||||
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/joomla-api-mcp
|
||||||
|
* PATH: /src/config.ts
|
||||||
|
* VERSION: 01.00.00
|
||||||
|
* BRIEF: Configuration loader for Joomla API MCP connections
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { readFile } from 'node:fs/promises';
|
||||||
|
import { resolve } from 'node:path';
|
||||||
|
import { homedir } from 'node:os';
|
||||||
|
import type { JoomlaConfig, JoomlaConnection } from './types.js';
|
||||||
|
|
||||||
|
const CONFIG_FILENAME = '.mcp_mokowaas.json';
|
||||||
|
|
||||||
|
export async function loadConfig(): Promise<JoomlaConfig> {
|
||||||
|
const config_path = process.env.JOOMLA_API_MCP_CONFIG
|
||||||
|
? resolve(process.env.JOOMLA_API_MCP_CONFIG)
|
||||||
|
: resolve(homedir(), CONFIG_FILENAME);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const raw = await readFile(config_path, 'utf-8');
|
||||||
|
const parsed = JSON.parse(raw) as Partial<JoomlaConfig>;
|
||||||
|
|
||||||
|
if (!parsed.connections || Object.keys(parsed.connections).length === 0) {
|
||||||
|
throw new Error('No connections defined in config');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
connections: parsed.connections,
|
||||||
|
defaultConnection: parsed.defaultConnection ?? Object.keys(parsed.connections)[0],
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
throw new Error(
|
||||||
|
`Failed to load config from ${config_path}: ${message}\n` +
|
||||||
|
`Create ${config_path} — see config.example.json for format.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getConnection(config: JoomlaConfig, name?: string): JoomlaConnection {
|
||||||
|
const key = name ?? config.defaultConnection;
|
||||||
|
const conn = config.connections[key];
|
||||||
|
if (!conn) {
|
||||||
|
throw new Error(
|
||||||
|
`Connection "${key}" not found. Available: ${Object.keys(config.connections).join(', ')}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return conn;
|
||||||
|
}
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
# Docs Index: /templates/repos/generic/src
|
|
||||||
|
|
||||||
## Purpose
|
|
||||||
|
|
||||||
This index provides navigation to documentation within this folder.
|
|
||||||
|
|
||||||
## Metadata
|
|
||||||
|
|
||||||
- **Document Type:** index
|
|
||||||
- **Auto-generated:** This file is automatically generated by rebuild_indexes.py
|
|
||||||
|
|
||||||
## Revision History
|
|
||||||
|
|
||||||
| Change | Notes | Author |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| Automated update | Generated by documentation index automation | rebuild_indexes.py |
|
|
||||||
+1314
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,32 @@
|
|||||||
|
/* 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: joomla-api-mcp.Types
|
||||||
|
* INGROUP: joomla-api-mcp
|
||||||
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/joomla-api-mcp
|
||||||
|
* PATH: /src/types.ts
|
||||||
|
* VERSION: 01.00.00
|
||||||
|
* BRIEF: TypeScript type definitions for Joomla API MCP server
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface JoomlaConnection {
|
||||||
|
baseUrl: string;
|
||||||
|
apiToken: string;
|
||||||
|
/** Skip TLS certificate verification (self-signed certs) */
|
||||||
|
insecure?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JoomlaConfig {
|
||||||
|
connections: Record<string, JoomlaConnection>;
|
||||||
|
defaultConnection: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiResponse {
|
||||||
|
status: number;
|
||||||
|
data: unknown;
|
||||||
|
errors?: Array<{ title: string; detail?: string }>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "Node16",
|
||||||
|
"moduleResolution": "Node16",
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user