Compare commits
724 Commits
beta
...
development
| Author | SHA1 | Date | |
|---|---|---|---|
| ee7a42e14b | |||
| 1000f028d2 | |||
| b048b47e7c | |||
| 6e0d5387cf | |||
| 76bbf7ad85 | |||
| a5dc00e056 | |||
| c6475ff29a | |||
| ffd98a19d9 | |||
| 34469609dd | |||
| d766b0568a | |||
| cfea9fac99 | |||
| 7d6d654d6d | |||
| dca452e49d | |||
| b827b3382a | |||
| 42841f7335 | |||
| 0f354422aa | |||
| 86aae39be1 | |||
| b5e932d78b | |||
| c738eb6669 | |||
| e0f98dc5e2 | |||
| ede07c6675 | |||
| 1fe8422fc0 | |||
| 6e216de0dc | |||
| 86e40fb978 | |||
| b2f52c191b | |||
| 8e1040efee | |||
| c203e970b9 | |||
| bf3c986113 | |||
| 955c08a387 | |||
| 52dbefbb14 | |||
| 379ca36613 | |||
| 35ca1af6b8 | |||
| e9ec664f03 | |||
| 6929c636b9 | |||
| 20797d663f | |||
| 11d5fd2019 | |||
| 1fa965dddb | |||
| 5a3ec7d9b1 | |||
| aef5ca43f6 | |||
| 0631d80fa1 | |||
| e84c10b14f | |||
| 87e543ef1c | |||
| 32236ad7ff | |||
| d6e462f3b7 | |||
| d470669634 | |||
| feaccf0758 | |||
| 9542c88ba4 | |||
| 7e3d366043 | |||
| 4c5919f209 | |||
| 0b3e699f29 | |||
| 406242fb7d | |||
| e6cb6eb531 | |||
| bdceb4256f | |||
| 81591477b2 | |||
| cc907a5aa2 | |||
| 5360c641e9 | |||
| d7a4066261 | |||
| 478eb262b9 | |||
| ea934ba04b | |||
| a92c1ce772 | |||
| 0e55a546ff | |||
| 14e94518ba | |||
| b32d91c446 | |||
| 03e0b6d13b | |||
| 21cb335727 | |||
| d2700f96fe | |||
| 7f7cdb4cc9 | |||
| b1b21e79d8 | |||
| e44336d24a | |||
| 76431371c3 | |||
| 2d667ba4cd | |||
| 4bc23ae54f | |||
| 8843f721e5 | |||
| 0a6744644d | |||
| e541e24741 | |||
| b30e6813fe | |||
| f61e8e53b5 | |||
| dff7d73009 | |||
| 186d129101 | |||
| 35ac2aaed2 | |||
| d825621502 | |||
| 1aa1a8282f | |||
| 6ca75bfbe7 | |||
| 0fd342cccd | |||
| f5ca5aadea | |||
| 6400bc9243 | |||
| cd15dddc3f | |||
| b98c2e0b6e | |||
| 943a19934e | |||
| dd57c23716 | |||
| 14a96eeb60 | |||
| 25c4d81e58 | |||
| 177f233edb | |||
| e01ed29bad | |||
| 640114e00c | |||
| b82c8c4ba0 | |||
| 448592440a | |||
| 474430ebba | |||
| 66183a6ec0 | |||
| 28faea267c | |||
| 46d5bd0841 | |||
| 6140b38662 | |||
| 1553289af1 | |||
| 7f90b83992 | |||
| 5164edaf7f | |||
| 665582b913 | |||
| f2e1dfe114 | |||
| 0357364908 | |||
| e7de6e4c9a | |||
| c08efa81fe | |||
| ea16fd92d7 | |||
| 0f446709b8 | |||
| 3817d7ed13 | |||
| b27ef3aee3 | |||
| 030423a16d | |||
| c4d8381828 | |||
| 590e1eb385 | |||
| f1f2785f0f | |||
| 138af226d1 | |||
| 17eaaf2347 | |||
| 21020027d0 | |||
| fa4622c0ca | |||
| 0d280717f1 | |||
| a45a6cb59c | |||
| 8edced75d3 | |||
| 018b197147 | |||
| 142ee2387e | |||
| ea66ad4b4a | |||
| 48cb040505 | |||
| 8df006876b | |||
| aec849c9ae | |||
| d3281066dc | |||
| b17b36e02e | |||
| cb6d19ece8 | |||
| 5020b58da1 | |||
| c97432495b | |||
| b22842f302 | |||
| 6a186ed365 | |||
| 42d530bfbf | |||
| 307dc37d47 | |||
| 2e4fdcb07e | |||
| 0ec18b9868 | |||
| e3f4890e5d | |||
| 6e81585177 | |||
| ff69e1d7fd | |||
| eefe9d134a | |||
| bfb159d0f0 | |||
| fc47aee5ba | |||
| 6b95c0aef9 | |||
| 047e296ee3 | |||
| 47faa1b289 | |||
| 48372e2f4f | |||
| 155a8b92b0 | |||
| ba0029e286 | |||
| b46506cdb7 | |||
| ebbac5760c | |||
| aa72835288 | |||
| 479a6c03f8 | |||
| 55219d758c | |||
| 27db3762ad | |||
| a2f34db524 | |||
| 37aa2bd854 | |||
| ccbd1dc2d5 | |||
| 59f0d1c166 | |||
| 72cf207f76 | |||
| 8a66abc711 | |||
| c2f5ca7754 | |||
| 1a6dfee74e | |||
| 25f8baf58b | |||
| 8c60927bbf | |||
| f80bf700ca | |||
| 717d81d80e | |||
| be4a0888c0 | |||
| 37a96bafa9 | |||
| 9ccd27e809 | |||
| 1a9815d96e | |||
| 326277010d | |||
| 3f78570267 | |||
| ea2ed31cb4 | |||
| 57c259f951 | |||
| 6c10d51b2f | |||
| 49b4fa85a9 | |||
| 1c1b541bc5 | |||
| 0bc5504e16 | |||
| c5ff1a5ada | |||
| f63e46030d | |||
| 34df31b086 | |||
| ec847a5eed | |||
| f3882bc96f | |||
| 9417a530a2 | |||
| 94b63ae08f | |||
| d7d7ab84a7 | |||
| 6e817fe547 | |||
| 71ad73bd04 | |||
| a5ed9a8130 | |||
| f0097899e1 | |||
| d82e10abb5 | |||
| 390f8ae3a9 | |||
| b54a6b85d8 | |||
| dec8b5df6d | |||
| 40ed2f028a | |||
| 35ff05c547 | |||
| f62278ea77 | |||
| 073f2b7750 | |||
| 3e6dad039d | |||
| 512277cfb1 | |||
| c1cfa9ede8 | |||
| 49d05c4a1f | |||
| 444b8c2853 | |||
| 83bfe6c552 | |||
| 18f64b5b68 | |||
| 3c705a785d | |||
| 66831ce4af | |||
| fbfc2ad9c5 | |||
| d1e2555f00 | |||
| 78d6b66f40 | |||
| 83195862af | |||
| e3752b772d | |||
| 022c5e2181 | |||
| 8c3af3a728 | |||
| ec509f15ba | |||
| e06f7a7224 | |||
| 21e6e7f09d | |||
| 82b24f1673 | |||
| 233387a607 | |||
| e728cc6b8d | |||
| e063766630 | |||
| 9ccb0c3359 | |||
| 6afa813830 | |||
| e154a9eaec | |||
| b381273357 | |||
| cd12e60ca4 | |||
| 5f4965ae6e | |||
| cb258d286c | |||
| 61b0a9b7b5 | |||
| 7db21d2406 | |||
| dde259814f | |||
| 16a71b13fb | |||
| 66dc9d39c1 | |||
| 51564af6b0 | |||
| 665d6d69fc | |||
| f2f821e3d1 | |||
| 9be02b4e53 | |||
| 263036f674 | |||
| 25c000638e | |||
| aac6d459d1 | |||
| 7a901a558f | |||
| 88f1cedcf2 | |||
| 83b8d9e447 | |||
| e20a2a3238 | |||
| a733168d1c | |||
| a432f8065f | |||
| 3c780f68db | |||
| d4cac3cd1f | |||
| 80e9e7f538 | |||
| 5d892e4464 | |||
| bfc2e88f10 | |||
| b4894ae7d3 | |||
| 700a3f636c | |||
| c6e547415d | |||
| e9df2602d2 | |||
| 5ac4e361a2 | |||
| c30446ddb5 | |||
| 9a885bc418 | |||
| 7a2ba6f8e0 | |||
| 233d741229 | |||
| 0397fc073e | |||
| 5ecdd31b47 | |||
| 1240aac31a | |||
| 4b4e25c21e | |||
| 606fdeead7 | |||
| 0e9ff43937 | |||
| 2074eb49ec | |||
| 3e14f5d195 | |||
| 08a3efb146 | |||
| 849762e782 | |||
| ccedb9ddd9 | |||
| a23910a779 | |||
| 10a31e1018 | |||
| a695bfcbdb | |||
| fc9ad5a538 | |||
| 3df248c304 | |||
| ac99573ec9 | |||
| 9e7587048b | |||
| 2b1ca3dd8d | |||
| cdddecf442 | |||
| c876438955 | |||
| d20cf6aaeb | |||
| 55b08a27ba | |||
| 8fa41f5d6b | |||
| df983c3e05 | |||
| 19e78b7ca6 | |||
| c61aaf09ea | |||
| ad4b400718 | |||
| 2f9ad83dbd | |||
| 8531bd07f3 | |||
| 01be59b2bd | |||
| b13d00d459 | |||
| 0c6e784486 | |||
| a0ea762f21 | |||
| f0e5a532fa | |||
| a0f3e42861 | |||
| 76e0da69bb | |||
| 06fb750319 | |||
| 319d4710c8 | |||
| accdc6d967 | |||
| 203bc09414 | |||
| 145d14b60a | |||
| b520eeaa70 | |||
| d8105c5861 | |||
| 2b69d7aa65 | |||
| 3a107db67b | |||
| b6d590cfe0 | |||
| 42782edea1 | |||
| 2ce52b2deb | |||
| 5e49f97cc4 | |||
| e4050ae453 | |||
| 981ce68983 | |||
| 8360b9e348 | |||
| 786f4f43ef | |||
| 9d8e325010 | |||
| f6e004546f | |||
| d6ef4585b6 | |||
| f6788a5fbd | |||
| 513385ae00 | |||
| c05bd14d3c | |||
| 4d2023f7aa | |||
| 7d699ff1a2 | |||
| a8e80d7c57 | |||
| b3bda839d8 | |||
| bce159b543 | |||
| bda0318a01 | |||
| e4a4a83c77 | |||
| 549c2a1395 | |||
| 4a781513b2 | |||
| c39ad4fa9c | |||
| 11312ef146 | |||
| b70dee40ce | |||
| 9824a6807f | |||
| 057b06d1f1 | |||
| 071878d21f | |||
| 757883e21b | |||
| f9087d4f43 | |||
| da304f28d3 | |||
| c47e8f4eee | |||
| 69ccf73b41 | |||
| 043dbc5a63 | |||
| 050769d143 | |||
| e79dd65b28 | |||
| d4a37b6f06 | |||
| 92cf8e8b1a | |||
| b5c6c16362 | |||
| b873ca263a | |||
| 0b1e4546c6 | |||
| d34b2a534b | |||
| 1404cdb026 | |||
| 0dacff33b0 | |||
| aaed2f6a59 | |||
| 534c3639f5 | |||
| c7290c0569 | |||
| c6d798c5bd | |||
| 438b6d18ab | |||
| 5fd8530a40 | |||
| 4c38eb75e1 | |||
| 8836c867ad | |||
| 84ec0dd06f | |||
| 5ca47190fc | |||
| 2cce153087 | |||
| 7a3f0e7b42 | |||
| 6c245be56a | |||
| aaeb65bca8 | |||
| 6cad4b1256 | |||
| ac75f13a04 | |||
| 299b526572 | |||
| b39c245407 | |||
| 01d6ba8148 | |||
| 1ce005e329 | |||
| 1bc0f70044 | |||
| 566b90d844 | |||
| 40559a92d6 | |||
| 63e90f57f1 | |||
| c923deb62d | |||
| 134e830a34 | |||
| 2f3a1eaf5d | |||
| 6d714ee1d2 | |||
| 6a7c137d36 | |||
| 03ee7a95f3 | |||
| b475e98997 | |||
| 2b475c4e3c | |||
| 9f15a4ccae | |||
| c3669bc55a | |||
| de89f7ab0f | |||
| 22bb4067d0 | |||
| 1d922a70e6 | |||
| 5c2b5e132d | |||
| ca49dc3847 | |||
| 6e7cad59cd | |||
| b8f99156ff | |||
| 305f0c99ae | |||
| 4e7ae81fe5 | |||
| 5ec700793d | |||
| f013c3f6cd | |||
| 905258efbc | |||
| 2820734346 | |||
| 27e52450d5 | |||
| fa173532c3 | |||
| f0ec07dfe4 | |||
| 32c71c2af8 | |||
| 17f7121107 | |||
| 4668c7a520 | |||
| cfcbf75b99 | |||
| 97c3258f15 | |||
| 9f957b6089 | |||
| 631af053a5 | |||
| 0c10d8e853 | |||
| 2563d43392 | |||
| 5915384416 | |||
| 729c059273 | |||
| 58701a55d2 | |||
| 96e5440422 | |||
| bdc9e1c1c4 | |||
| 5584090b26 | |||
| 4d51d7efbc | |||
| 66fb8a9bda | |||
| 8d85056fe4 | |||
| e533b44b4b | |||
| 45a3a9702c | |||
| 68df7fcf07 | |||
| 3a28c437c6 | |||
| 2ec124cc3f | |||
| a9a4b8ecdd | |||
| bb154cb76d | |||
| f94b54fe5b | |||
| 82aaa6a0ec | |||
| 43b9599393 | |||
| f68df52a54 | |||
| ac935ccdba | |||
| f96ef7c57f | |||
| 627fe4466a | |||
| 77b79a4f00 | |||
| 62cd0ed6c4 | |||
| cd9fb32f2c | |||
| d09753c295 | |||
| 9744d1d060 | |||
| 0252656596 | |||
| 3ca8f993ff | |||
| e3ac488127 | |||
| 71274012b0 | |||
| 276ffecbe5 | |||
| 3b6f2b0598 | |||
| 34382a0ef1 | |||
| 3038a2d500 | |||
| 15449b211a | |||
| 4150461686 | |||
| 978301ccd7 | |||
| 29cae8a1f6 | |||
| fa38ccd379 | |||
| 6eab825dc2 | |||
| cbc9b4415f | |||
| d9ee30e429 | |||
| 3e4ec51e75 | |||
| 059e5c2632 | |||
| b34e7e371e | |||
| c157347ec6 | |||
| aebdac71fd | |||
| aa952f9fe5 | |||
| c3c71329bb | |||
| 0af5b35de1 | |||
| 1b6a545a0a | |||
| 2948d316ff | |||
| d59f99c476 | |||
| 6001afdc76 | |||
| 0a14377c72 | |||
| 7c3f0f68f7 | |||
| e8934f42bd | |||
| 682942ea2b | |||
| ee867ce409 | |||
| bb57e95538 | |||
| e93438c2a6 | |||
| a94d9de3b8 | |||
| 1437d529fd | |||
| 2934bab5ee | |||
| f8d815e79b | |||
| 7c96e10554 | |||
| 35b3e2df8f | |||
| 3168a7a9d3 | |||
| 88f8024667 | |||
| 42b85593ce | |||
| db8850ac83 | |||
| 9ed0818044 | |||
| bcd94747e0 | |||
| 4b3f487d7a | |||
| 2dd0361538 | |||
| bfc698039b | |||
| 59fb68c828 | |||
| c3722b30da | |||
| e1b1a90ee1 | |||
| f1468db2bb | |||
| 53910d5eb9 | |||
| 923554eff8 | |||
| f501d50e34 | |||
| 828bed1f94 | |||
| 89301db510 | |||
| 2bd075d072 | |||
| 19e6c47d6f | |||
| 4d5b239c20 | |||
| 9e84f0c410 | |||
| 7907500b70 | |||
| c02bf3ea70 | |||
| e2aabae598 | |||
| cb7e5771c0 | |||
| 064f77867b | |||
| ae1c9bb04c | |||
| c20c0b2504 | |||
| e6c2513ef8 | |||
| 998d35c887 | |||
| 2cf88f917f | |||
| 87d04690c0 | |||
| c336713df4 | |||
| e982f3afdb | |||
| 308f24c347 | |||
| 14cdea40a9 | |||
| 2f56a74c55 | |||
| 6504ff6ed4 | |||
| 65958dc595 | |||
| d525bbb35b | |||
| b77099227d | |||
| 89947e2444 | |||
| fcfa520f0a | |||
| 598944e8a8 | |||
| 94466ca09c | |||
| 4871d5a55c | |||
| 634ef50c58 | |||
| 26015fd63c | |||
| 3a21267ea5 | |||
| 3366a54709 | |||
| 81ff44cbd4 | |||
| 5e4d102af4 | |||
| a116bc5933 | |||
| 224884750f | |||
| 2250452847 | |||
| 76387f4414 | |||
| d8e7be8e8b | |||
| 76ab32519d | |||
| 17db5ec9e0 | |||
| 406626ac3a | |||
| 1b967eb195 | |||
| 7476e18bb3 | |||
| ff354831ca | |||
| 5af75fe5a4 | |||
| f11d3567b7 | |||
| a27c0acbc1 | |||
| a83903c48e | |||
| e404a217a6 | |||
| 2e94f278a1 | |||
| 623256c9c2 | |||
| 49227e8561 | |||
| 2f97c9da73 | |||
| d747abb7d3 | |||
| cddd802645 | |||
| 2bc9f05929 | |||
| d2b16d688f | |||
| 2591555a18 | |||
| c6861ad59e | |||
| fc5266e58e | |||
| e0dbdd9dea | |||
| 1358407e7e | |||
| b14d7d8d2f | |||
| 4ccb75b618 | |||
| a352a53f3e | |||
| 958714108d | |||
| c2321decf6 | |||
| 9153e7e7df | |||
| 7f6cb5ac10 | |||
| 3101d52142 | |||
| 9203f8ac04 | |||
| 07f18b028e | |||
| 0829d3cf95 | |||
| e4cfbb4249 | |||
| a82d51ab97 | |||
| cae1b72ccb | |||
| e206c4cc5e | |||
| e5c6b09f94 | |||
| 144e64cad6 | |||
| a821b31d71 | |||
| b8be68da79 | |||
| 39509c3acf | |||
| 2a3b11a5d4 | |||
| 4eeb559bc7 | |||
| a2fd3ce157 | |||
| e0bf218ec9 | |||
| c4691f9134 | |||
| 40648923a1 | |||
| 089c29aafa | |||
| a1a3dee5ba | |||
| 14d9fe07e9 | |||
| d199ad7b8c | |||
| c4099a150f | |||
| 46cc6f2c4d | |||
| 5014a0ec02 | |||
| 525df19ca2 | |||
| 4c72ddfb3d | |||
| 2ef5fdf3c5 | |||
| d54265ca9f | |||
| 92994bcfec | |||
| 09a3a1aa95 | |||
| 78abc4e7e0 | |||
| 16ebfabd4e | |||
| 8d8dfe5b28 | |||
| 8c2022e041 | |||
| de824b2752 | |||
| 39637b1004 | |||
| 2c8e7d7693 | |||
| 9c5149bb10 | |||
| df2087c51f | |||
| ef3a3be171 | |||
| 4358e1c66b | |||
| cfc0d9b68f | |||
| 77f53d3347 | |||
| fb38029872 | |||
| 2385213400 | |||
| 39eea64996 | |||
| b1f111bc91 | |||
| f30d9e53f8 | |||
| e8a38bfe9e | |||
| fb1944bc15 | |||
| 95132d8f85 | |||
| ff2fcb7b5c | |||
| b3f2a61251 | |||
| 9833749796 | |||
| 3582ab0908 | |||
| 1df3c49312 | |||
| fd1ccad779 | |||
| 72dfc47cf4 | |||
| 1712291df1 | |||
| bc157dbecd | |||
| 1c506b0296 | |||
| c3c733ef13 | |||
| 69a9985403 | |||
| 36c66418cb | |||
| a3dafb82c5 | |||
| 9ff176606f | |||
| d3aebc53ab | |||
| eb942cfb9e | |||
| df661625e0 | |||
| e4107ab773 | |||
| 9bec46c446 | |||
| c6a5194096 | |||
| 6f48e4703e | |||
| 4ce04d1833 | |||
| 98fdb29852 | |||
| e6ec97a4b6 | |||
| 7375c198e6 | |||
| 932de46177 | |||
| d073dcb017 | |||
| 9c49727018 | |||
| e413a4a6d8 | |||
| 13162e26cd | |||
| a7cef01ff1 | |||
| 8cae9b1565 | |||
| 4a5b94e510 | |||
| 466bc72bb9 | |||
| d87c0680e8 | |||
| 0a7db75f66 | |||
| e8e6c93295 | |||
| 2057fc71a0 | |||
| 040b22782f | |||
| b31c6fe190 | |||
| e7c371130a | |||
| 3a46d20c52 | |||
| 0241fe7299 | |||
| d6c9387591 | |||
| f5f36f37d5 | |||
| 50268c715a | |||
| 40dc5cce94 | |||
| 83b047e58f | |||
| 89e73a7557 | |||
| bed8c304ba | |||
| 4374e3b382 | |||
| d252f7c358 | |||
| fccaf67024 | |||
| 2eea9a4f3c | |||
| a59daf7f55 | |||
| df32770484 | |||
| 637785638b | |||
| fe99ea2012 | |||
| af6af35ee8 | |||
| 1a26cc16ca | |||
| 86bf172c43 | |||
| f5cb9d6575 | |||
| 6ec898e889 | |||
| 36381d68ac | |||
| d62e2d42d9 | |||
| 90003a5afa | |||
| d922d09e95 | |||
| cb03a89f98 | |||
| ffb2930781 | |||
| 94d53edcd8 | |||
| 8a3b0d1954 | |||
| eafe65e321 | |||
| 2acd166437 | |||
| 3d40188217 | |||
| dd8edbfaea | |||
| e408207f1a | |||
| ea3ea5724f | |||
| d4a5367eed | |||
| 76b4ddaad8 | |||
| 6c9dd7bffa | |||
| bd0ce7345c | |||
| d3e0cbe8f0 | |||
| 1ec5174820 | |||
| 5c1722b3cb | |||
| f47f4be8e3 | |||
| 60b06a987c | |||
| 769a7ec43a | |||
| 566bf8fff2 | |||
| bf75614190 | |||
| 43bd0e2031 | |||
| 68f897ffe4 | |||
| 3df40214f3 | |||
| d4d0b2b276 | |||
| 9eda3fd497 | |||
| 5fd7961fad | |||
| 9178aeaa9c |
@@ -1,20 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: MokoStandards.Templates.Config
|
|
||||||
# INGROUP: MokoStandards.Templates
|
|
||||||
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
|
|
||||||
# PATH: /templates/configs/moko-standards.yml
|
|
||||||
# VERSION: 04.04.01
|
|
||||||
# BRIEF: Governance attachment template — synced to .mokostandards in every governed repository
|
|
||||||
# NOTE: Tokens replaced at sync time: mokoconsulting-tech, MokoWaaS, waas-component, 04.04.00
|
|
||||||
#
|
|
||||||
# This file is managed automatically by MokoStandards bulk sync.
|
|
||||||
# Do not edit manually — changes will be overwritten on the next sync.
|
|
||||||
# To update governance settings, open a PR in MokoStandards instead:
|
|
||||||
# https://github.com/mokoconsulting-tech/MokoStandards
|
|
||||||
|
|
||||||
standards_source: "https://github.com/mokoconsulting-tech/MokoStandards"
|
|
||||||
standards_version: "04.04.00"
|
|
||||||
platform: "waas-component"
|
|
||||||
governed_repo: "mokoconsulting-tech/MokoWaaS"
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: GitHub.Workflow
|
|
||||||
# INGROUP: MokoStandards.Workflows.Shared
|
|
||||||
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
|
|
||||||
# PATH: /.github/workflows/auto-assign.yml
|
|
||||||
# VERSION: 04.06.00
|
|
||||||
# BRIEF: Auto-assign jmiller-moko to unassigned issues and PRs every 15 minutes
|
|
||||||
|
|
||||||
name: Auto-Assign Issues & PRs
|
|
||||||
|
|
||||||
on:
|
|
||||||
issues:
|
|
||||||
types: [opened]
|
|
||||||
pull_request_target:
|
|
||||||
types: [opened]
|
|
||||||
schedule:
|
|
||||||
- cron: '0 */12 * * *'
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
issues: write
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
auto-assign:
|
|
||||||
name: Assign unassigned issues and PRs
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Assign unassigned issues
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
|
|
||||||
run: |
|
|
||||||
REPO="${{ github.repository }}"
|
|
||||||
ASSIGNEE="jmiller-moko"
|
|
||||||
|
|
||||||
echo "## 🏷️ Auto-Assign Report" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
ASSIGNED_ISSUES=0
|
|
||||||
ASSIGNED_PRS=0
|
|
||||||
|
|
||||||
# Assign unassigned open issues
|
|
||||||
ISSUES=$(gh api "repos/$REPO/issues?state=open&per_page=100&assignee=none" --jq '.[].number' 2>/dev/null || true)
|
|
||||||
for NUM in $ISSUES; do
|
|
||||||
# Skip PRs (the issues endpoint returns PRs too)
|
|
||||||
IS_PR=$(gh api "repos/$REPO/issues/$NUM" --jq '.pull_request // empty' 2>/dev/null || true)
|
|
||||||
if [ -z "$IS_PR" ]; then
|
|
||||||
gh api "repos/$REPO/issues/$NUM/assignees" -X POST -f "assignees[]=$ASSIGNEE" --silent 2>/dev/null && {
|
|
||||||
ASSIGNED_ISSUES=$((ASSIGNED_ISSUES + 1))
|
|
||||||
echo " Assigned issue #$NUM"
|
|
||||||
} || true
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Assign unassigned open PRs
|
|
||||||
PRS=$(gh api "repos/$REPO/pulls?state=open&per_page=100" --jq '.[] | select(.assignees | length == 0) | .number' 2>/dev/null || true)
|
|
||||||
for NUM in $PRS; do
|
|
||||||
gh api "repos/$REPO/issues/$NUM/assignees" -X POST -f "assignees[]=$ASSIGNEE" --silent 2>/dev/null && {
|
|
||||||
ASSIGNED_PRS=$((ASSIGNED_PRS + 1))
|
|
||||||
echo " Assigned PR #$NUM"
|
|
||||||
} || true
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "| Type | Assigned |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "|------|----------|" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Issues | $ASSIGNED_ISSUES |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Pull Requests | $ASSIGNED_PRS |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
if [ "$ASSIGNED_ISSUES" -eq 0 ] && [ "$ASSIGNED_PRS" -eq 0 ]; then
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "✅ All issues and PRs already have assignees" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
@@ -1,207 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# This file is part of a Moko Consulting project.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: GitHub.Workflow
|
|
||||||
# INGROUP: MokoStandards.Automation
|
|
||||||
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
|
|
||||||
# PATH: /templates/workflows/shared/auto-dev-issue.yml.template
|
|
||||||
# VERSION: 04.06.00
|
|
||||||
# BRIEF: Auto-create tracking issue with sub-issues for dev/rc branch workflow
|
|
||||||
# NOTE: Synced via bulk-repo-sync to .github/workflows/auto-dev-issue.yml in all governed repos.
|
|
||||||
|
|
||||||
name: Dev/RC Branch Issue
|
|
||||||
|
|
||||||
on:
|
|
||||||
# Auto-create on RC branch creation
|
|
||||||
create:
|
|
||||||
# Manual trigger for dev branches
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
branch:
|
|
||||||
description: 'Branch name (e.g., dev/my-feature or dev/04.06)'
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
|
|
||||||
env:
|
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
issues: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
create-issue:
|
|
||||||
name: Create version tracking issue
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: >-
|
|
||||||
(github.event_name == 'workflow_dispatch') ||
|
|
||||||
(github.event.ref_type == 'branch' &&
|
|
||||||
(startsWith(github.event.ref, 'rc/') ||
|
|
||||||
startsWith(github.event.ref, 'alpha/') ||
|
|
||||||
startsWith(github.event.ref, 'beta/')))
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Create tracking issue and sub-issues
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
|
|
||||||
run: |
|
|
||||||
# For manual dispatch, use input; for auto, use event ref
|
|
||||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
|
||||||
BRANCH="${{ inputs.branch }}"
|
|
||||||
else
|
|
||||||
BRANCH="${{ github.event.ref }}"
|
|
||||||
fi
|
|
||||||
REPO="${{ github.repository }}"
|
|
||||||
ACTOR="${{ github.actor }}"
|
|
||||||
NOW=$(date -u '+%Y-%m-%d %H:%M UTC')
|
|
||||||
|
|
||||||
# Determine branch type and version
|
|
||||||
if [[ "$BRANCH" == rc/* ]]; then
|
|
||||||
VERSION="${BRANCH#rc/}"
|
|
||||||
BRANCH_TYPE="Release Candidate"
|
|
||||||
LABEL_TYPE="type: release"
|
|
||||||
TITLE_PREFIX="rc"
|
|
||||||
elif [[ "$BRANCH" == beta/* ]]; then
|
|
||||||
VERSION="${BRANCH#beta/}"
|
|
||||||
BRANCH_TYPE="Beta"
|
|
||||||
LABEL_TYPE="type: release"
|
|
||||||
TITLE_PREFIX="beta"
|
|
||||||
elif [[ "$BRANCH" == alpha/* ]]; then
|
|
||||||
VERSION="${BRANCH#alpha/}"
|
|
||||||
BRANCH_TYPE="Alpha"
|
|
||||||
LABEL_TYPE="type: release"
|
|
||||||
TITLE_PREFIX="alpha"
|
|
||||||
else
|
|
||||||
VERSION="${BRANCH#dev/}"
|
|
||||||
BRANCH_TYPE="Development"
|
|
||||||
LABEL_TYPE="type: feature"
|
|
||||||
TITLE_PREFIX="feat"
|
|
||||||
fi
|
|
||||||
|
|
||||||
TITLE="${TITLE_PREFIX}(${VERSION}): ${BRANCH_TYPE} tracking for ${BRANCH}"
|
|
||||||
|
|
||||||
# Check for existing issue with same title prefix
|
|
||||||
EXISTING=$(gh api "repos/${REPO}/issues?state=open&per_page=10" \
|
|
||||||
--jq ".[] | select(.title | startswith(\"${TITLE_PREFIX}(${VERSION})\")) | .number" 2>/dev/null | head -1)
|
|
||||||
|
|
||||||
if [ -n "$EXISTING" ]; then
|
|
||||||
echo "ℹ️ Issue #${EXISTING} already exists for ${VERSION}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Define sub-issues for the workflow ─────────────────────────
|
|
||||||
if [[ "$BRANCH" == rc/* ]]; then
|
|
||||||
SUB_ISSUES=(
|
|
||||||
"RC Testing|Verify all features work on rc branch|type: test,release-candidate"
|
|
||||||
"Regression Testing|Run full regression suite before merge|type: test,release-candidate"
|
|
||||||
"Version Bump|Bump version in README.md and all headers|type: version,release-candidate"
|
|
||||||
"Changelog Update|Update CHANGELOG.md with release notes|documentation,release-candidate"
|
|
||||||
"Merge to Version Branch|Create PR to version/XX|type: release,needs-review"
|
|
||||||
)
|
|
||||||
elif [[ "$BRANCH" == alpha/* ]] || [[ "$BRANCH" == beta/* ]]; then
|
|
||||||
SUB_ISSUES=(
|
|
||||||
"Testing|Verify features on ${BRANCH_TYPE} branch|type: test,status: in-progress"
|
|
||||||
"Bug Fixes|Fix issues found during ${BRANCH_TYPE} testing|type: bug,status: pending"
|
|
||||||
"Promote to Next Stage|Create PR to promote to next release stage|type: release,needs-review"
|
|
||||||
)
|
|
||||||
else
|
|
||||||
SUB_ISSUES=(
|
|
||||||
"Development|Implement feature/fix on dev branch|type: feature,status: in-progress"
|
|
||||||
"Unit Testing|Write and pass unit tests|type: test,status: pending"
|
|
||||||
"Code Review|Request and complete code review|needs-review,status: pending"
|
|
||||||
"Version Bump|Bump version in README.md and all headers|type: version,status: pending"
|
|
||||||
"Changelog Update|Update CHANGELOG.md with release notes|documentation,status: pending"
|
|
||||||
"Create RC Branch|Promote dev to rc branch for final testing|type: release,status: pending"
|
|
||||||
"Merge to Main|Create PR from rc/dev to main|type: release,needs-review,status: pending"
|
|
||||||
)
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Create sub-issues first ───────────────────────────────────────
|
|
||||||
SUB_LIST=""
|
|
||||||
SUB_NUMBERS=""
|
|
||||||
for SUB in "${SUB_ISSUES[@]}"; do
|
|
||||||
IFS='|' read -r SUB_TITLE SUB_DESC SUB_LABELS <<< "$SUB"
|
|
||||||
SUB_FULL_TITLE="${TITLE_PREFIX}(${VERSION}): ${SUB_TITLE}"
|
|
||||||
|
|
||||||
SUB_BODY=$(printf '### %s\n\n%s\n\n| Field | Value |\n|-------|-------|\n| **Parent Branch** | `%s` |\n| **Version** | `%s` |\n\n---\n*Sub-issue of the %s tracking issue for `%s`.*' \
|
|
||||||
"$SUB_TITLE" "$SUB_DESC" "$BRANCH" "$VERSION" "$BRANCH_TYPE" "$BRANCH")
|
|
||||||
|
|
||||||
SUB_URL=$(gh issue create \
|
|
||||||
--repo "$REPO" \
|
|
||||||
--title "$SUB_FULL_TITLE" \
|
|
||||||
--body "$SUB_BODY" \
|
|
||||||
--label "${SUB_LABELS}" \
|
|
||||||
--assignee "jmiller-moko" 2>&1)
|
|
||||||
|
|
||||||
SUB_NUM=$(echo "$SUB_URL" | grep -oE '[0-9]+$')
|
|
||||||
if [ -n "$SUB_NUM" ]; then
|
|
||||||
SUB_LIST="${SUB_LIST}\n- [ ] ${SUB_TITLE} (#${SUB_NUM})"
|
|
||||||
SUB_NUMBERS="${SUB_NUMBERS} #${SUB_NUM}"
|
|
||||||
fi
|
|
||||||
sleep 0.3
|
|
||||||
done
|
|
||||||
|
|
||||||
# ── Create parent tracking issue ──────────────────────────────────
|
|
||||||
PARENT_BODY=$(printf '## %s Branch Created\n\n| Field | Value |\n|-------|-------|\n| **Branch** | `%s` |\n| **Version** | `%s` |\n| **Type** | %s |\n| **Created by** | @%s |\n| **Created at** | %s |\n| **Repository** | `%s` |\n\n## Workflow Sub-Issues\n\n%b\n\n---\n*Auto-created by [auto-dev-issue.yml](.github/workflows/auto-dev-issue.yml) on branch creation.*' \
|
|
||||||
"$BRANCH_TYPE" "$BRANCH" "$VERSION" "$BRANCH_TYPE" "$ACTOR" "$NOW" "$REPO" "$SUB_LIST")
|
|
||||||
|
|
||||||
PARENT_URL=$(gh issue create \
|
|
||||||
--repo "$REPO" \
|
|
||||||
--title "$TITLE" \
|
|
||||||
--body "$PARENT_BODY" \
|
|
||||||
--label "${LABEL_TYPE},version" \
|
|
||||||
--assignee "jmiller-moko" 2>&1)
|
|
||||||
|
|
||||||
PARENT_NUM=$(echo "$PARENT_URL" | grep -oE '[0-9]+$')
|
|
||||||
|
|
||||||
# ── Link sub-issues back to parent ────────────────────────────────
|
|
||||||
if [ -n "$PARENT_NUM" ]; then
|
|
||||||
for SUB in "${SUB_ISSUES[@]}"; do
|
|
||||||
IFS='|' read -r SUB_TITLE _ _ <<< "$SUB"
|
|
||||||
SUB_FULL_TITLE="${TITLE_PREFIX}(${VERSION}): ${SUB_TITLE}"
|
|
||||||
SUB_NUM=$(gh api "repos/${REPO}/issues?state=open&per_page=20" \
|
|
||||||
--jq ".[] | select(.title == \"${SUB_FULL_TITLE}\") | .number" 2>/dev/null | head -1)
|
|
||||||
if [ -n "$SUB_NUM" ]; then
|
|
||||||
gh api "repos/${REPO}/issues/${SUB_NUM}" -X PATCH \
|
|
||||||
-f body="$(gh api "repos/${REPO}/issues/${SUB_NUM}" --jq '.body' 2>/dev/null)
|
|
||||||
|
|
||||||
> **Parent Issue:** #${PARENT_NUM}" --silent 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
sleep 0.2
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Create or update prerelease for alpha/beta/rc ────────────────
|
|
||||||
if [[ "$BRANCH" == rc/* ]] || [[ "$BRANCH" == alpha/* ]] || [[ "$BRANCH" == beta/* ]]; then
|
|
||||||
case "$BRANCH_TYPE" in
|
|
||||||
Alpha) RELEASE_TAG="alpha" ;;
|
|
||||||
Beta) RELEASE_TAG="beta" ;;
|
|
||||||
"Release Candidate") RELEASE_TAG="release-candidate" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
EXISTING=$(gh release view "$RELEASE_TAG" --json tagName -q .tagName 2>/dev/null || true)
|
|
||||||
if [ -z "$EXISTING" ]; then
|
|
||||||
gh release create "$RELEASE_TAG" \
|
|
||||||
--title "${RELEASE_TAG} (${VERSION})" \
|
|
||||||
--notes "## ${BRANCH_TYPE} ${VERSION}\n\nBranch: \`${BRANCH}\`\nTracking issue: ${PARENT_URL}" \
|
|
||||||
--prerelease \
|
|
||||||
--target main 2>/dev/null || true
|
|
||||||
echo "${BRANCH_TYPE} release created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
gh release edit "$RELEASE_TAG" \
|
|
||||||
--title "${RELEASE_TAG} (${VERSION})" --prerelease 2>/dev/null || true
|
|
||||||
echo "${BRANCH_TYPE} release updated: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Summary ───────────────────────────────────────────────────────
|
|
||||||
echo "## Dev Workflow Issues Created" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Item | Issue |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "|------|-------|" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| **Parent** | ${PARENT_URL} |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| **Sub-issues** |${SUB_NUMBERS} |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
@@ -1,560 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: GitHub.Workflow
|
|
||||||
# INGROUP: MokoStandards.Release
|
|
||||||
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
|
|
||||||
# PATH: /templates/workflows/joomla/auto-release.yml.template
|
|
||||||
# VERSION: 04.06.00
|
|
||||||
# BRIEF: Joomla build & release — ZIP package, updates.xml, SHA-256 checksum
|
|
||||||
#
|
|
||||||
# +========================================================================+
|
|
||||||
# | BUILD & RELEASE PIPELINE (JOOMLA) |
|
|
||||||
# +========================================================================+
|
|
||||||
# | |
|
|
||||||
# | Triggers on push to main (skips bot commits + [skip ci]): |
|
|
||||||
# | |
|
|
||||||
# | Every push: |
|
|
||||||
# | 1. Read version from README.md |
|
|
||||||
# | 3. Set platform version (Joomla <version>) |
|
|
||||||
# | 4. Update [VERSION: XX.YY.ZZ] badges in markdown files |
|
|
||||||
# | 5. Write updates.xml (Joomla update server XML) |
|
|
||||||
# | 6. Create git tag vXX.YY.ZZ |
|
|
||||||
# | 7a. Patch: update existing GitHub Release for this minor |
|
|
||||||
# | 8. Build ZIP, upload asset, write SHA-256 to updates.xml |
|
|
||||||
# | |
|
|
||||||
# | Every version change: archives main -> version/XX.YY branch |
|
|
||||||
# | Patch 00 = development (no release). First release = patch 01. |
|
|
||||||
# | First release only (patch == 01): |
|
|
||||||
# | 7b. Create new GitHub Release |
|
|
||||||
# | |
|
|
||||||
# +========================================================================+
|
|
||||||
|
|
||||||
name: Build & Release
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [closed]
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- 'src/**'
|
|
||||||
- 'htdocs/**'
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
env:
|
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release:
|
|
||||||
name: Build & Release Pipeline
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: >-
|
|
||||||
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch'
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GH_TOKEN || github.token }}
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Setup MokoStandards tools
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
|
|
||||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}'
|
|
||||||
run: |
|
|
||||||
git clone --depth 1 --branch version/04 --quiet \
|
|
||||||
"https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \
|
|
||||||
/tmp/mokostandards
|
|
||||||
cd /tmp/mokostandards
|
|
||||||
composer install --no-dev --no-interaction --quiet
|
|
||||||
|
|
||||||
# -- STEP 1: Read version -----------------------------------------------
|
|
||||||
- name: "Step 1: Read version from README.md"
|
|
||||||
id: version
|
|
||||||
run: |
|
|
||||||
VERSION=$(php /tmp/mokostandards/api/cli/version_read.php --path . 2>/dev/null)
|
|
||||||
if [ -z "$VERSION" ]; then
|
|
||||||
echo "No VERSION in README.md — skipping release"
|
|
||||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
# Derive major.minor for branch naming (patches update existing branch)
|
|
||||||
MINOR=$(echo "$VERSION" | awk -F. '{printf "%s.%s", $1, $2}')
|
|
||||||
PATCH=$(echo "$VERSION" | awk -F. '{print $3}')
|
|
||||||
|
|
||||||
MAJOR=$(echo "$VERSION" | awk -F. '{print $1}')
|
|
||||||
MINOR_NUM=$(echo "$VERSION" | awk -F. '{print $2}')
|
|
||||||
|
|
||||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "branch=version/${MAJOR}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "minor=$MINOR" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "major=$MAJOR" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "release_tag=v${MAJOR}" >> "$GITHUB_OUTPUT"
|
|
||||||
if [ "$PATCH" = "00" ]; then
|
|
||||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "is_minor=false" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "Version: $VERSION (patch 00 = development — skipping release)"
|
|
||||||
else
|
|
||||||
echo "skip=false" >> "$GITHUB_OUTPUT"
|
|
||||||
if [ "$PATCH" = "01" ]; then
|
|
||||||
echo "is_minor=true" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "Version: $VERSION (first release — full pipeline)"
|
|
||||||
else
|
|
||||||
echo "is_minor=false" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "Version: $VERSION (patch — platform version + badges only)"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Check if already released
|
|
||||||
if: steps.version.outputs.skip != 'true'
|
|
||||||
id: check
|
|
||||||
run: |
|
|
||||||
TAG="${{ steps.version.outputs.release_tag }}"
|
|
||||||
BRANCH="${{ steps.version.outputs.branch }}"
|
|
||||||
|
|
||||||
TAG_EXISTS=false
|
|
||||||
BRANCH_EXISTS=false
|
|
||||||
|
|
||||||
git rev-parse "$TAG" >/dev/null 2>&1 && TAG_EXISTS=true
|
|
||||||
git ls-remote --heads origin "$BRANCH" 2>/dev/null | grep -q "$BRANCH" && BRANCH_EXISTS=true
|
|
||||||
|
|
||||||
echo "tag_exists=$TAG_EXISTS" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "branch_exists=$BRANCH_EXISTS" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
if [ "$TAG_EXISTS" = "true" ] && [ "$BRANCH_EXISTS" = "true" ]; then
|
|
||||||
echo "already_released=true" >> "$GITHUB_OUTPUT"
|
|
||||||
else
|
|
||||||
echo "already_released=false" >> "$GITHUB_OUTPUT"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# -- SANITY CHECKS -------------------------------------------------------
|
|
||||||
- name: "Sanity: Pre-release validation"
|
|
||||||
if: >-
|
|
||||||
steps.version.outputs.skip != 'true' &&
|
|
||||||
steps.check.outputs.already_released != 'true'
|
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.version.outputs.version }}"
|
|
||||||
ERRORS=0
|
|
||||||
|
|
||||||
echo "## Pre-Release Sanity Checks (Joomla)" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
# -- Version drift check (must pass before release) --------
|
|
||||||
README_VER=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1)
|
|
||||||
if [ "$README_VER" != "$VERSION" ]; then
|
|
||||||
echo "- Version drift: README says \`${README_VER}\` but releasing \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
|
|
||||||
ERRORS=$((ERRORS+1))
|
|
||||||
else
|
|
||||||
echo "- Version consistent: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check CHANGELOG version matches
|
|
||||||
CL_VER=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' CHANGELOG.md 2>/dev/null | head -1)
|
|
||||||
if [ -n "$CL_VER" ] && [ "$CL_VER" != "$VERSION" ]; then
|
|
||||||
echo "- CHANGELOG drift: \`${CL_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
|
|
||||||
ERRORS=$((ERRORS+1))
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check composer.json version if present
|
|
||||||
if [ -f "composer.json" ]; then
|
|
||||||
COMP_VER=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' composer.json 2>/dev/null | head -1)
|
|
||||||
if [ -n "$COMP_VER" ] && [ "$COMP_VER" != "$VERSION" ]; then
|
|
||||||
echo "- composer.json drift: \`${COMP_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
|
|
||||||
ERRORS=$((ERRORS+1))
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Common checks
|
|
||||||
if [ ! -f "LICENSE" ]; then
|
|
||||||
echo "- Missing LICENSE file" >> $GITHUB_STEP_SUMMARY
|
|
||||||
ERRORS=$((ERRORS+1))
|
|
||||||
else
|
|
||||||
echo "- LICENSE present" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -d "src" ] && [ ! -d "htdocs" ]; then
|
|
||||||
echo "- Warning: No src/ or htdocs/ directory" >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "- Source directory present" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
# -- Joomla: manifest version drift --------
|
|
||||||
MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
|
||||||
if [ -n "$MANIFEST" ]; then
|
|
||||||
XML_VER=$(sed -n 's/.*<version>\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
|
|
||||||
if [ -n "$XML_VER" ] && [ "$XML_VER" != "$VERSION" ]; then
|
|
||||||
echo "- Manifest drift: \`${XML_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
|
|
||||||
ERRORS=$((ERRORS+1))
|
|
||||||
else
|
|
||||||
echo "- Manifest version: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# -- Joomla: XML manifest existence --------
|
|
||||||
if [ -z "$MANIFEST" ]; then
|
|
||||||
echo "- No Joomla XML manifest found" >> $GITHUB_STEP_SUMMARY
|
|
||||||
ERRORS=$((ERRORS+1))
|
|
||||||
else
|
|
||||||
echo "- Manifest: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
# -- Joomla: extension type check --------
|
|
||||||
TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" 2>/dev/null)
|
|
||||||
echo "- Extension type: ${TYPE:-unknown}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
if [ "$ERRORS" -gt 0 ]; then
|
|
||||||
echo "**${ERRORS} error(s) — release may be incomplete**" >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "**All sanity checks passed**" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
# -- STEP 2: Create or update version/XX.YY archive branch ---------------
|
|
||||||
# Always runs — every version change on main archives to version/XX.YY
|
|
||||||
- name: "Step 2: Version archive branch"
|
|
||||||
if: steps.check.outputs.already_released != 'true'
|
|
||||||
run: |
|
|
||||||
BRANCH="${{ steps.version.outputs.branch }}"
|
|
||||||
IS_MINOR="${{ steps.version.outputs.is_minor }}"
|
|
||||||
PATCH="${{ steps.version.outputs.version }}"
|
|
||||||
PATCH_NUM=$(echo "$PATCH" | awk -F. '{print $3}')
|
|
||||||
|
|
||||||
# Check if branch exists
|
|
||||||
if git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then
|
|
||||||
git push origin HEAD:"$BRANCH" --force
|
|
||||||
echo "Updated archive branch: ${BRANCH} (patch ${PATCH_NUM})" >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
git checkout -b "$BRANCH" 2>/dev/null || git checkout "$BRANCH"
|
|
||||||
git push origin "$BRANCH" --force
|
|
||||||
echo "Created archive branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
# -- STEP 3: Set platform version ----------------------------------------
|
|
||||||
- name: "Step 3: Set platform version"
|
|
||||||
if: >-
|
|
||||||
steps.version.outputs.skip != 'true' &&
|
|
||||||
steps.check.outputs.already_released != 'true'
|
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.version.outputs.version }}"
|
|
||||||
php /tmp/mokostandards/api/cli/version_set_platform.php \
|
|
||||||
--path . --version "$VERSION" --branch main
|
|
||||||
|
|
||||||
# -- STEP 4: Update version badges ----------------------------------------
|
|
||||||
- name: "Step 4: Update version badges"
|
|
||||||
if: >-
|
|
||||||
steps.version.outputs.skip != 'true' &&
|
|
||||||
steps.check.outputs.already_released != 'true'
|
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.version.outputs.version }}"
|
|
||||||
find . -name "*.md" ! -path "./.git/*" ! -path "./vendor/*" | while read -r f; do
|
|
||||||
if grep -q '\[VERSION:' "$f" 2>/dev/null; then
|
|
||||||
sed -i "s/\[VERSION:[[:space:]]*[0-9]\{2\}\.[0-9]\{2\}\.[0-9]\{2\}\]/[VERSION: ${VERSION}]/" "$f"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# -- STEP 5: Write updates.xml (Joomla update server) ---------------------
|
|
||||||
- name: "Step 5: Write updates.xml"
|
|
||||||
if: >-
|
|
||||||
steps.version.outputs.skip != 'true' &&
|
|
||||||
steps.check.outputs.already_released != 'true'
|
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.version.outputs.version }}"
|
|
||||||
REPO="${{ github.repository }}"
|
|
||||||
|
|
||||||
# -- Parse extension metadata from XML manifest ----------------
|
|
||||||
MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
|
||||||
if [ -z "$MANIFEST" ]; then
|
|
||||||
echo "Warning: No Joomla XML manifest found — skipping updates.xml" >> $GITHUB_STEP_SUMMARY
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Extract fields using sed (portable — no grep -P)
|
|
||||||
EXT_NAME=$(sed -n 's/.*<name>\([^<]*\)<\/name>.*/\1/p' "$MANIFEST" | head -1)
|
|
||||||
EXT_TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
|
|
||||||
EXT_ELEMENT=$(sed -n 's/.*<element>\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" | head -1)
|
|
||||||
EXT_CLIENT=$(sed -n 's/.*<extension[^>]*client="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
|
|
||||||
EXT_FOLDER=$(sed -n 's/.*<extension[^>]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
|
|
||||||
TARGET_PLATFORM=$(sed -n 's/.*\(<targetplatform[^/]*\/>\).*/\1/p' "$MANIFEST" | head -1)
|
|
||||||
PHP_MINIMUM=$(sed -n 's/.*<php_minimum>\([^<]*\)<\/php_minimum>.*/\1/p' "$MANIFEST" | head -1)
|
|
||||||
|
|
||||||
# Fallbacks
|
|
||||||
[ -z "$EXT_NAME" ] && EXT_NAME="${{ github.event.repository.name }}"
|
|
||||||
[ -z "$EXT_TYPE" ] && EXT_TYPE="component"
|
|
||||||
|
|
||||||
# Templates/modules don't have <element> — derive from <name> (lowercased)
|
|
||||||
if [ -z "$EXT_ELEMENT" ]; then
|
|
||||||
EXT_ELEMENT=$(echo "$EXT_NAME" | tr '[:upper:]' '[:lower:]' | tr -d ' ')
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Build client tag: plugins and frontend modules need <client>site</client>
|
|
||||||
CLIENT_TAG=""
|
|
||||||
if [ -n "$EXT_CLIENT" ]; then
|
|
||||||
CLIENT_TAG="<client>${EXT_CLIENT}</client>"
|
|
||||||
elif [ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]; then
|
|
||||||
CLIENT_TAG="<client>site</client>"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Build folder tag for plugins (required for Joomla to match the update)
|
|
||||||
FOLDER_TAG=""
|
|
||||||
if [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ]; then
|
|
||||||
FOLDER_TAG="<folder>${EXT_FOLDER}</folder>"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Build targetplatform (fallback to Joomla 5 if not in manifest)
|
|
||||||
if [ -z "$TARGET_PLATFORM" ]; then
|
|
||||||
TARGET_PLATFORM=$(printf '<targetplatform name="joomla" version="5.*" %s>' "/")
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Build php_minimum tag
|
|
||||||
PHP_TAG=""
|
|
||||||
if [ -n "$PHP_MINIMUM" ]; then
|
|
||||||
PHP_TAG="<php_minimum>${PHP_MINIMUM}</php_minimum>"
|
|
||||||
fi
|
|
||||||
|
|
||||||
DOWNLOAD_URL="https://github.com/${REPO}/releases/download/v${VERSION}/${EXT_ELEMENT}-${VERSION}.zip"
|
|
||||||
INFO_URL="https://github.com/${REPO}/releases/tag/v${VERSION}"
|
|
||||||
|
|
||||||
# -- Build stable entry to temp file ─────────────────────────
|
|
||||||
{
|
|
||||||
printf '%s\n' ' <update>'
|
|
||||||
printf '%s\n' " <name>${EXT_NAME}</name>"
|
|
||||||
printf '%s\n' " <description>${EXT_NAME} update</description>"
|
|
||||||
printf '%s\n' " <element>${EXT_ELEMENT}</element>"
|
|
||||||
printf '%s\n' " <type>${EXT_TYPE}</type>"
|
|
||||||
printf '%s\n' " <version>${VERSION}</version>"
|
|
||||||
[ -n "$CLIENT_TAG" ] && printf '%s\n' " ${CLIENT_TAG}"
|
|
||||||
[ -n "$FOLDER_TAG" ] && printf '%s\n' " ${FOLDER_TAG}"
|
|
||||||
printf '%s\n' ' <tags>'
|
|
||||||
printf '%s\n' ' <tag>stable</tag>'
|
|
||||||
printf '%s\n' ' </tags>'
|
|
||||||
printf '%s\n' " <infourl title=\"${EXT_NAME}\">${INFO_URL}</infourl>"
|
|
||||||
printf '%s\n' ' <downloads>'
|
|
||||||
printf '%s\n' " <downloadurl type=\"full\" format=\"zip\">${DOWNLOAD_URL}</downloadurl>"
|
|
||||||
printf '%s\n' ' </downloads>'
|
|
||||||
printf '%s\n' " ${TARGET_PLATFORM}"
|
|
||||||
[ -n "$PHP_TAG" ] && printf '%s\n' " ${PHP_TAG}"
|
|
||||||
printf '%s\n' ' <maintainer>Moko Consulting</maintainer>'
|
|
||||||
printf '%s\n' ' <maintainerurl>https://mokoconsulting.tech</maintainerurl>'
|
|
||||||
printf '%s\n' ' </update>'
|
|
||||||
} > /tmp/stable_entry.xml
|
|
||||||
|
|
||||||
# -- Write updates.xml preserving dev/rc entries ──────────────
|
|
||||||
# Extract existing entries for other stability levels
|
|
||||||
# Order reflects release workflow: development → alpha → beta → rc → stable
|
|
||||||
if [ -f "updates.xml" ]; then
|
|
||||||
printf 'import re, sys\n' > /tmp/extract.py
|
|
||||||
printf 'with open("updates.xml") as f: c = f.read()\n' >> /tmp/extract.py
|
|
||||||
printf 'tag = sys.argv[1]\n' >> /tmp/extract.py
|
|
||||||
printf 'm = re.search(r"( <update>.*?<tag>" + re.escape(tag) + r"</tag>.*?</update>)", c, re.DOTALL)\n' >> /tmp/extract.py
|
|
||||||
printf 'if m: print(m.group(1))\n' >> /tmp/extract.py
|
|
||||||
fi
|
|
||||||
DEV_ENTRY=$(python3 /tmp/extract.py development 2>/dev/null || true)
|
|
||||||
ALPHA_ENTRY=$(python3 /tmp/extract.py alpha 2>/dev/null || true)
|
|
||||||
BETA_ENTRY=$(python3 /tmp/extract.py beta 2>/dev/null || true)
|
|
||||||
RC_ENTRY=$(python3 /tmp/extract.py rc 2>/dev/null || true)
|
|
||||||
|
|
||||||
{
|
|
||||||
printf '%s\n' '<?xml version="1.0" encoding="utf-8"?>'
|
|
||||||
printf '%s\n' '<updates>'
|
|
||||||
[ -n "$DEV_ENTRY" ] && echo "$DEV_ENTRY"
|
|
||||||
[ -n "$ALPHA_ENTRY" ] && echo "$ALPHA_ENTRY"
|
|
||||||
[ -n "$BETA_ENTRY" ] && echo "$BETA_ENTRY"
|
|
||||||
[ -n "$RC_ENTRY" ] && echo "$RC_ENTRY"
|
|
||||||
cat /tmp/stable_entry.xml
|
|
||||||
printf '%s\n' '</updates>'
|
|
||||||
} > updates.xml
|
|
||||||
|
|
||||||
echo "updates.xml: ${VERSION} (stable + rc/dev preserved)" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
# -- Commit all changes ---------------------------------------------------
|
|
||||||
- name: Commit release changes
|
|
||||||
if: >-
|
|
||||||
steps.version.outputs.skip != 'true' &&
|
|
||||||
steps.check.outputs.already_released != 'true'
|
|
||||||
run: |
|
|
||||||
if git diff --quiet && git diff --cached --quiet; then
|
|
||||||
echo "No changes to commit"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
VERSION="${{ steps.version.outputs.version }}"
|
|
||||||
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
|
||||||
git config --local user.name "github-actions[bot]"
|
|
||||||
git add -A
|
|
||||||
git commit -m "chore(release): build ${VERSION} [skip ci]" \
|
|
||||||
--author="github-actions[bot] <github-actions[bot]@users.noreply.github.com>"
|
|
||||||
git push
|
|
||||||
|
|
||||||
# -- STEP 6: Create tag ---------------------------------------------------
|
|
||||||
- name: "Step 6: Create git tag"
|
|
||||||
if: >-
|
|
||||||
steps.version.outputs.skip != 'true' &&
|
|
||||||
steps.check.outputs.tag_exists != 'true' &&
|
|
||||||
steps.version.outputs.is_minor == 'true'
|
|
||||||
run: |
|
|
||||||
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
|
||||||
# Only create the major release tag if it doesn't exist yet
|
|
||||||
if ! git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then
|
|
||||||
git tag "$RELEASE_TAG"
|
|
||||||
git push origin "$RELEASE_TAG"
|
|
||||||
echo "Tag created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "Tag ${RELEASE_TAG} already exists" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
echo "Tag: ${TAG}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
# -- STEP 7: Create or update GitHub Release ------------------------------
|
|
||||||
- name: "Step 7: GitHub Release"
|
|
||||||
if: >-
|
|
||||||
steps.version.outputs.skip != 'true' &&
|
|
||||||
steps.check.outputs.tag_exists != 'true'
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
|
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.version.outputs.version }}"
|
|
||||||
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
|
||||||
BRANCH="${{ steps.version.outputs.branch }}"
|
|
||||||
MAJOR="${{ steps.version.outputs.major }}"
|
|
||||||
|
|
||||||
NOTES=$(php /tmp/mokostandards/api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null)
|
|
||||||
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
|
|
||||||
echo "$NOTES" > /tmp/release_notes.md
|
|
||||||
|
|
||||||
# Check if the major release already exists
|
|
||||||
EXISTING=$(gh release view "$RELEASE_TAG" --json tagName -q .tagName 2>/dev/null || true)
|
|
||||||
|
|
||||||
if [ -z "$EXISTING" ]; then
|
|
||||||
# First release for this major
|
|
||||||
gh release create "$RELEASE_TAG" \
|
|
||||||
--title "v${MAJOR} (latest: ${VERSION})" \
|
|
||||||
--notes-file /tmp/release_notes.md \
|
|
||||||
--target "$BRANCH"
|
|
||||||
echo "Release created: ${RELEASE_TAG} (${VERSION})" >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
# Append version notes to existing major release
|
|
||||||
CURRENT_NOTES=$(gh release view "$RELEASE_TAG" --json body -q .body 2>/dev/null || true)
|
|
||||||
{
|
|
||||||
echo "$CURRENT_NOTES"
|
|
||||||
echo ""
|
|
||||||
echo "---"
|
|
||||||
echo "### ${VERSION}"
|
|
||||||
echo ""
|
|
||||||
cat /tmp/release_notes.md
|
|
||||||
} > /tmp/updated_notes.md
|
|
||||||
|
|
||||||
gh release edit "$RELEASE_TAG" \
|
|
||||||
--title "v${MAJOR} (latest: ${VERSION})" \
|
|
||||||
--notes-file /tmp/updated_notes.md
|
|
||||||
echo "Release updated: ${RELEASE_TAG} -> ${VERSION}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
# -- STEP 8: Build Joomla install ZIP + SHA-256 checksum ------------------
|
|
||||||
# Every patch builds an install-ready ZIP and uploads it to the minor release.
|
|
||||||
# Result: one Release per minor version with a ZIP for each patch.
|
|
||||||
- name: "Step 8: Build Joomla package and update checksum"
|
|
||||||
if: >-
|
|
||||||
steps.version.outputs.skip != 'true'
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
|
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.version.outputs.version }}"
|
|
||||||
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
|
||||||
REPO="${{ github.repository }}"
|
|
||||||
|
|
||||||
# All ZIPs upload to the major release tag (vXX)
|
|
||||||
gh release view "$RELEASE_TAG" --json tagName > /dev/null 2>&1 || {
|
|
||||||
echo "No release ${RELEASE_TAG} found — skipping ZIP upload"
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# Find extension element name from manifest
|
|
||||||
MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true)
|
|
||||||
[ -z "$MANIFEST" ] && exit 0
|
|
||||||
|
|
||||||
EXT_ELEMENT=$(sed -n 's/.*<element>\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1 || basename "$MANIFEST" .xml)
|
|
||||||
ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip"
|
|
||||||
TAR_NAME="${EXT_ELEMENT}-${VERSION}.tar.gz"
|
|
||||||
|
|
||||||
# -- Build install packages from src/ ----------------------------
|
|
||||||
SOURCE_DIR="src"
|
|
||||||
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
|
||||||
[ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ — skipping package"; exit 0; }
|
|
||||||
|
|
||||||
EXCLUDES=".ftpignore sftp-config* *.ppk *.pem *.key .env*"
|
|
||||||
|
|
||||||
# ZIP package
|
|
||||||
cd "$SOURCE_DIR"
|
|
||||||
zip -r "/tmp/${ZIP_NAME}" . -x $EXCLUDES
|
|
||||||
cd ..
|
|
||||||
|
|
||||||
# tar.gz package
|
|
||||||
tar -czf "/tmp/${TAR_NAME}" -C "$SOURCE_DIR" \
|
|
||||||
--exclude='.ftpignore' --exclude='sftp-config*' \
|
|
||||||
--exclude='*.ppk' --exclude='*.pem' --exclude='*.key' --exclude='.env*' .
|
|
||||||
|
|
||||||
ZIP_SIZE=$(stat -c%s "/tmp/${ZIP_NAME}" 2>/dev/null || stat -f%z "/tmp/${ZIP_NAME}" 2>/dev/null || echo "unknown")
|
|
||||||
TAR_SIZE=$(stat -c%s "/tmp/${TAR_NAME}" 2>/dev/null || stat -f%z "/tmp/${TAR_NAME}" 2>/dev/null || echo "unknown")
|
|
||||||
|
|
||||||
# -- Calculate SHA-256 for both ----------------------------------
|
|
||||||
SHA256_ZIP=$(sha256sum "/tmp/${ZIP_NAME}" | cut -d' ' -f1)
|
|
||||||
SHA256_TAR=$(sha256sum "/tmp/${TAR_NAME}" | cut -d' ' -f1)
|
|
||||||
|
|
||||||
# -- Upload both to release tag ----------------------------------
|
|
||||||
gh release upload "$RELEASE_TAG" "/tmp/${ZIP_NAME}" --clobber 2>/dev/null || true
|
|
||||||
gh release upload "$RELEASE_TAG" "/tmp/${TAR_NAME}" --clobber 2>/dev/null || true
|
|
||||||
|
|
||||||
# -- Update updates.xml with both download formats ---------------
|
|
||||||
if [ -f "updates.xml" ]; then
|
|
||||||
ZIP_URL="https://github.com/${REPO}/releases/download/${RELEASE_TAG}/${ZIP_NAME}"
|
|
||||||
TAR_URL="https://github.com/${REPO}/releases/download/${RELEASE_TAG}/${TAR_NAME}"
|
|
||||||
|
|
||||||
# Replace downloads block with both formats + SHA
|
|
||||||
sed -i "s|<downloads>.*</downloads>|<downloads>\n <downloadurl type=\"full\" format=\"zip\">${ZIP_URL}</downloadurl>\n <downloadurl type=\"full\" format=\"tar.gz\">${TAR_URL}</downloadurl>\n </downloads>|" updates.xml 2>/dev/null || true
|
|
||||||
if grep -q '<sha256>' updates.xml; then
|
|
||||||
sed -i "s|<sha256>.*</sha256>|<sha256>sha256:${SHA256_ZIP}</sha256>|" updates.xml
|
|
||||||
else
|
|
||||||
sed -i "s|</downloads>|</downloads>\n <sha256>sha256:${SHA256_ZIP}</sha256>|" updates.xml
|
|
||||||
fi
|
|
||||||
|
|
||||||
git add updates.xml
|
|
||||||
git commit -m "chore(release): ZIP + tar.gz for ${VERSION} [skip ci]" \
|
|
||||||
--author="github-actions[bot] <github-actions[bot]@users.noreply.github.com>" || true
|
|
||||||
git push || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "### Joomla Packages" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Package | Size | SHA-256 |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "|---------|------|---------|" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| \`${ZIP_NAME}\` | ${ZIP_SIZE} | \`${SHA256_ZIP}\` |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| \`${TAR_NAME}\` | ${TAR_SIZE} | \`${SHA256_TAR}\` |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Release | \`${RELEASE_TAG}\` | |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Download | [${PACKAGE_NAME}](https://github.com/${REPO}/releases/download/${RELEASE_TAG}/${PACKAGE_NAME}) |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
# -- Summary --------------------------------------------------------------
|
|
||||||
- name: Pipeline Summary
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.version.outputs.version }}"
|
|
||||||
if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
|
|
||||||
echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
|
|
||||||
elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
|
|
||||||
echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "## Build & Release Complete (Joomla)" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Release | [View](https://github.com/${{ github.repository }}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: GitHub.Workflow
|
|
||||||
# INGROUP: MokoStandards.Automation
|
|
||||||
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
|
|
||||||
# PATH: /templates/workflows/shared/branch-freeze.yml.template
|
|
||||||
# VERSION: 04.06.00
|
|
||||||
# BRIEF: Freeze or unfreeze any branch via ruleset — manual workflow_dispatch
|
|
||||||
|
|
||||||
name: Branch Freeze
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
branch:
|
|
||||||
description: 'Branch to freeze/unfreeze (e.g., version/04, dev/feature)'
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
action:
|
|
||||||
description: 'Action to perform'
|
|
||||||
required: true
|
|
||||||
type: choice
|
|
||||||
options:
|
|
||||||
- freeze
|
|
||||||
- unfreeze
|
|
||||||
|
|
||||||
env:
|
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
manage-freeze:
|
|
||||||
name: "${{ inputs.action }} branch: ${{ inputs.branch }}"
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Check permissions
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
|
|
||||||
run: |
|
|
||||||
ACTOR="${{ github.actor }}"
|
|
||||||
REPO="${{ github.repository }}"
|
|
||||||
PERMISSION=$(gh api "repos/${REPO}/collaborators/${ACTOR}/permission" \
|
|
||||||
--jq '.permission' 2>/dev/null || echo "read")
|
|
||||||
if [ "$PERMISSION" != "admin" ]; then
|
|
||||||
echo "Denied: only admins can freeze/unfreeze branches (${ACTOR} has ${PERMISSION})"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: "${{ inputs.action }} branch"
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
|
|
||||||
run: |
|
|
||||||
BRANCH="${{ inputs.branch }}"
|
|
||||||
ACTION="${{ inputs.action }}"
|
|
||||||
REPO="${{ github.repository }}"
|
|
||||||
RULESET_NAME="FROZEN: ${BRANCH}"
|
|
||||||
|
|
||||||
echo "## Branch Freeze" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
if [ "$ACTION" = "freeze" ]; then
|
|
||||||
# Check if ruleset already exists
|
|
||||||
EXISTING=$(gh api "repos/${REPO}/rulesets" \
|
|
||||||
--jq ".[] | select(.name == \"${RULESET_NAME}\") | .id" 2>/dev/null || true)
|
|
||||||
|
|
||||||
if [ -n "$EXISTING" ]; then
|
|
||||||
echo "Branch \`${BRANCH}\` is already frozen (ruleset #${EXISTING})" >> $GITHUB_STEP_SUMMARY
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create freeze ruleset — blocks all updates except admin bypass
|
|
||||||
printf '{"name":"%s","target":"branch","enforcement":"active",' "${RULESET_NAME}" > /tmp/ruleset.json
|
|
||||||
printf '"bypass_actors":[{"actor_id":5,"actor_type":"RepositoryRole","bypass_mode":"always"}],' >> /tmp/ruleset.json
|
|
||||||
printf '"conditions":{"ref_name":{"include":["refs/heads/%s"],"exclude":[]}},' "${BRANCH}" >> /tmp/ruleset.json
|
|
||||||
printf '"rules":[{"type":"update"},{"type":"deletion"},{"type":"non_fast_forward"}]}' >> /tmp/ruleset.json
|
|
||||||
|
|
||||||
RESULT=$(gh api "repos/${REPO}/rulesets" -X POST --input /tmp/ruleset.json --jq '.id' 2>&1) || true
|
|
||||||
|
|
||||||
if echo "$RESULT" | grep -qE '^[0-9]+$'; then
|
|
||||||
echo "Frozen \`${BRANCH}\` — ruleset #${RESULT}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Branch | \`${BRANCH}\` |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Ruleset | #${RESULT} |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Rules | No updates, no deletion, no force push |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Bypass | Repository admins only |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "Failed to freeze: ${RESULT}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
elif [ "$ACTION" = "unfreeze" ]; then
|
|
||||||
# Find and delete the freeze ruleset
|
|
||||||
RULESET_ID=$(gh api "repos/${REPO}/rulesets" \
|
|
||||||
--jq ".[] | select(.name == \"${RULESET_NAME}\") | .id" 2>/dev/null || true)
|
|
||||||
|
|
||||||
if [ -z "$RULESET_ID" ]; then
|
|
||||||
echo "Branch \`${BRANCH}\` is not frozen (no ruleset found)" >> $GITHUB_STEP_SUMMARY
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
gh api "repos/${REPO}/rulesets/${RULESET_ID}" -X DELETE --silent 2>/dev/null
|
|
||||||
|
|
||||||
echo "Unfrozen \`${BRANCH}\` — ruleset #${RULESET_ID} deleted" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
rm -f /tmp/ruleset.json
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# This file is part of a Moko Consulting project.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: GitHub.Workflow.Template
|
|
||||||
# INGROUP: MokoStandards.CI
|
|
||||||
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
|
|
||||||
# PATH: /templates/workflows/shared/changelog-validation.yml.template
|
|
||||||
# VERSION: 04.06.00
|
|
||||||
# BRIEF: Validates CHANGELOG.md format and version consistency
|
|
||||||
# NOTE: Deployed to .github/workflows/changelog-validation.yml in governed repos.
|
|
||||||
|
|
||||||
name: Changelog Validation
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- 'dev/**'
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
env:
|
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
validate-changelog:
|
|
||||||
name: Validate CHANGELOG.md
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
||||||
|
|
||||||
- name: Check CHANGELOG.md exists
|
|
||||||
run: |
|
|
||||||
echo "### Changelog Validation" >> $GITHUB_STEP_SUMMARY
|
|
||||||
if [ ! -f "CHANGELOG.md" ]; then
|
|
||||||
echo "CHANGELOG.md not found in repository root." >> $GITHUB_STEP_SUMMARY
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "CHANGELOG.md exists." >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
- name: Check VERSION header matches README.md
|
|
||||||
run: |
|
|
||||||
# Extract version from README.md FILE INFORMATION block
|
|
||||||
README_VERSION=$(grep -oP '^\s*VERSION:\s*\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' README.md | head -1)
|
|
||||||
if [ -z "$README_VERSION" ]; then
|
|
||||||
echo "No VERSION found in README.md FILE INFORMATION block." >> $GITHUB_STEP_SUMMARY
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check that CHANGELOG.md has a matching version header
|
|
||||||
CHANGELOG_VERSION=$(grep -oP '^\#\#\s*\[\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' CHANGELOG.md | head -1)
|
|
||||||
if [ -z "$CHANGELOG_VERSION" ]; then
|
|
||||||
echo "No version header found in CHANGELOG.md (expected \`## [XX.YY.ZZ] - YYYY-MM-DD\`)." >> $GITHUB_STEP_SUMMARY
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$CHANGELOG_VERSION" != "$README_VERSION" ]; then
|
|
||||||
echo "CHANGELOG latest version \`${CHANGELOG_VERSION}\` does not match README VERSION \`${README_VERSION}\`." >> $GITHUB_STEP_SUMMARY
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "CHANGELOG version \`${CHANGELOG_VERSION}\` matches README VERSION." >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
- name: Validate conventional changelog format
|
|
||||||
run: |
|
|
||||||
ERRORS=0
|
|
||||||
|
|
||||||
# Check that version entries follow ## [XX.YY.ZZ] - YYYY-MM-DD format
|
|
||||||
while IFS= read -r LINE; do
|
|
||||||
if ! echo "$LINE" | grep -qP '^\#\#\s*\[[0-9]{2}\.[0-9]{2}\.[0-9]{2}\]\s*-\s*[0-9]{4}-[0-9]{2}-[0-9]{2}'; then
|
|
||||||
echo "Malformed version header: \`${LINE}\`" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo " Expected format: \`## [XX.YY.ZZ] - YYYY-MM-DD\`" >> $GITHUB_STEP_SUMMARY
|
|
||||||
ERRORS=$((ERRORS + 1))
|
|
||||||
fi
|
|
||||||
done < <(grep -P '^\#\#\s*\[' CHANGELOG.md)
|
|
||||||
|
|
||||||
ENTRY_COUNT=$(grep -cP '^\#\#\s*\[' CHANGELOG.md || echo "0")
|
|
||||||
if [ "$ENTRY_COUNT" -eq 0 ]; then
|
|
||||||
echo "No version entries found in CHANGELOG.md." >> $GITHUB_STEP_SUMMARY
|
|
||||||
ERRORS=$((ERRORS + 1))
|
|
||||||
else
|
|
||||||
echo "Found ${ENTRY_COUNT} version entr(ies) in CHANGELOG.md." >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
if [ "${ERRORS}" -gt 0 ]; then
|
|
||||||
echo "**${ERRORS} format issue(s) found.**" >> $GITHUB_STEP_SUMMARY
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
echo "**Changelog format validation passed.**" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
# GitHub Copilot Configuration
|
|
||||||
# This file configures GitHub Copilot settings for the repository
|
|
||||||
|
|
||||||
# Allowed domains for Copilot to access
|
|
||||||
# These domains are trusted sources that Copilot can fetch information from
|
|
||||||
allowed_domains:
|
|
||||||
# Standard license providers
|
|
||||||
- "www.gnu.org" # GNU licenses (GPL, LGPL, AGPL)
|
|
||||||
- "opensource.org" # Open Source Initiative
|
|
||||||
- "choosealicense.com" # GitHub's license chooser
|
|
||||||
- "spdx.org" # Software Package Data Exchange
|
|
||||||
- "creativecommons.org" # Creative Commons licenses
|
|
||||||
- "apache.org" # Apache Software Foundation
|
|
||||||
- "fsf.org" # Free Software Foundation
|
|
||||||
|
|
||||||
# Documentation and standards
|
|
||||||
- "semver.org" # Semantic Versioning
|
|
||||||
- "keepachangelog.com" # Changelog standards
|
|
||||||
- "conventionalcommits.org" # Commit message standards
|
|
||||||
|
|
||||||
# GitHub and related
|
|
||||||
- "github.com" # GitHub main site
|
|
||||||
- "docs.github.com" # GitHub documentation
|
|
||||||
- "raw.githubusercontent.com" # GitHub raw content
|
|
||||||
|
|
||||||
# Package managers and registries
|
|
||||||
- "npmjs.com" # npm registry
|
|
||||||
- "pypi.org" # Python Package Index
|
|
||||||
- "packagist.org" # PHP Composer packages
|
|
||||||
- "rubygems.org" # Ruby gems
|
|
||||||
|
|
||||||
# Standards and specifications
|
|
||||||
- "json-schema.org" # JSON Schema
|
|
||||||
- "w3.org" # W3C standards
|
|
||||||
- "ietf.org" # IETF RFCs and standards
|
|
||||||
|
|
||||||
# PHP and Joomla specific
|
|
||||||
- "joomla.org" # Joomla CMS
|
|
||||||
- "docs.joomla.org" # Joomla documentation
|
|
||||||
- "downloads.joomla.org" # Joomla core downloads
|
|
||||||
- "php.net" # PHP documentation
|
|
||||||
- "getcomposer.org" # Composer dependency manager
|
|
||||||
- "packagist.org" # Composer package registry (also listed under packages)
|
|
||||||
|
|
||||||
# Dolibarr specific
|
|
||||||
- "dolibarr.org" # Dolibarr ERP/CRM
|
|
||||||
- "wiki.dolibarr.org" # Dolibarr wiki
|
|
||||||
- "docs.dolibarr.org" # Dolibarr developer documentation
|
|
||||||
|
|
||||||
# Moko Consulting
|
|
||||||
- "mokoconsulting.tech" # Moko Consulting main site
|
|
||||||
- "*.mokoconsulting.tech" # All Moko Consulting subdomains (API, docs, CDN, etc.)
|
|
||||||
|
|
||||||
# Google services
|
|
||||||
- "drive.google.com" # Google Drive (file sharing and assets)
|
|
||||||
- "docs.google.com" # Google Docs
|
|
||||||
- "sheets.google.com" # Google Sheets
|
|
||||||
- "accounts.google.com" # Google authentication
|
|
||||||
- "storage.googleapis.com" # Google Cloud Storage
|
|
||||||
- "*.googleapis.com" # Google APIs (Maps, Fonts, etc.)
|
|
||||||
- "*.googleusercontent.com" # Google user-uploaded content and CDN
|
|
||||||
- "fonts.googleapis.com" # Google Fonts CSS
|
|
||||||
- "fonts.gstatic.com" # Google Fonts static assets
|
|
||||||
|
|
||||||
# GitHub extended
|
|
||||||
- "api.github.com" # GitHub REST API
|
|
||||||
- "upload.github.com" # GitHub file uploads
|
|
||||||
- "objects.githubusercontent.com" # GitHub release assets and LFS
|
|
||||||
- "user-images.githubusercontent.com" # GitHub issue/PR image attachments
|
|
||||||
- "codeload.github.com" # GitHub archive downloads
|
|
||||||
- "ghcr.io" # GitHub Container Registry
|
|
||||||
- "pkg.github.com" # GitHub Packages
|
|
||||||
|
|
||||||
# Developer reference
|
|
||||||
- "developer.mozilla.org" # MDN Web Docs
|
|
||||||
- "stackoverflow.com" # Stack Overflow
|
|
||||||
- "git-scm.com" # Git documentation
|
|
||||||
|
|
||||||
# CDN and infrastructure
|
|
||||||
- "cdn.jsdelivr.net" # jsDelivr CDN
|
|
||||||
- "unpkg.com" # unpkg CDN
|
|
||||||
- "cdnjs.cloudflare.com" # Cloudflare CDN
|
|
||||||
- "img.shields.io" # Shields.io badge images
|
|
||||||
- "shields.io" # Shields.io badge service
|
|
||||||
|
|
||||||
# Container registries
|
|
||||||
- "hub.docker.com" # Docker Hub
|
|
||||||
- "registry-1.docker.io" # Docker registry pulls
|
|
||||||
- "index.docker.io" # Docker index
|
|
||||||
|
|
||||||
# CI / code quality
|
|
||||||
- "codecov.io" # Code coverage reporting
|
|
||||||
- "coveralls.io" # Coveralls coverage service
|
|
||||||
- "sonarcloud.io" # SonarCloud static analysis
|
|
||||||
|
|
||||||
# Terraform / infrastructure
|
|
||||||
- "registry.terraform.io" # Terraform provider registry
|
|
||||||
- "releases.hashicorp.com" # HashiCorp release downloads
|
|
||||||
- "checkpoint-api.hashicorp.com" # HashiCorp update checks
|
|
||||||
|
|
||||||
# Settings for code generation and suggestions
|
|
||||||
copilot:
|
|
||||||
# Enable Copilot for this repository
|
|
||||||
enabled: true
|
|
||||||
|
|
||||||
# File patterns to include for Copilot suggestions
|
|
||||||
include:
|
|
||||||
- "**/*.py"
|
|
||||||
- "**/*.js"
|
|
||||||
- "**/*.php"
|
|
||||||
- "**/*.md"
|
|
||||||
- "**/*.yml"
|
|
||||||
- "**/*.yaml"
|
|
||||||
- "**/*.json"
|
|
||||||
- "**/*.xml"
|
|
||||||
- "**/*.sh"
|
|
||||||
|
|
||||||
# File patterns to exclude from Copilot suggestions
|
|
||||||
exclude:
|
|
||||||
- "**/node_modules/**"
|
|
||||||
- "**/vendor/**"
|
|
||||||
- "**/build/**"
|
|
||||||
- "**/dist/**"
|
|
||||||
- "**/.git/**"
|
|
||||||
- "**/LICENSE"
|
|
||||||
- "**/CHANGELOG.md"
|
|
||||||
|
|
||||||
# Notes:
|
|
||||||
# ------
|
|
||||||
# - This configuration allows GitHub Copilot to fetch information from trusted sources
|
|
||||||
# - License providers are included to help with license text and compliance information
|
|
||||||
# - Package registries help with dependency management and version checking
|
|
||||||
# - Standards organizations provide authoritative specifications
|
|
||||||
# - Platform-specific sites (Joomla, Dolibarr, PHP) support our technology stack
|
|
||||||
# - All domains listed are well-known, reputable sources in their respective domains
|
|
||||||
# - This list focuses on read-only access to public information
|
|
||||||
# - No authentication credentials should be used with these domains
|
|
||||||
@@ -1,758 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# This file is part of a Moko Consulting project.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: GitHub.Workflow
|
|
||||||
# INGROUP: MokoStandards.Firewall
|
|
||||||
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
|
|
||||||
# PATH: /templates/workflows/shared/enterprise-firewall-setup.yml.template
|
|
||||||
# VERSION: 04.06.00
|
|
||||||
# BRIEF: Enterprise firewall configuration — generates outbound allow-rules including SFTP deployment server
|
|
||||||
# NOTE: Reads DEV_FTP_HOST / DEV_FTP_PORT variables to include SFTP egress rules alongside HTTPS rules.
|
|
||||||
|
|
||||||
name: Enterprise Firewall Configuration
|
|
||||||
|
|
||||||
# This workflow provides firewall configuration guidance for enterprise-ready sites
|
|
||||||
# It generates firewall rules for allowing outbound access to trusted domains
|
|
||||||
# including license providers, documentation sources, package registries,
|
|
||||||
# and the SFTP deployment server (DEV_FTP_HOST / DEV_FTP_PORT).
|
|
||||||
#
|
|
||||||
# Runs automatically when:
|
|
||||||
# - Coding agent workflows are triggered (pull requests with copilot/ prefix)
|
|
||||||
# - Manual workflow dispatch for custom configurations
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
firewall_type:
|
|
||||||
description: 'Target firewall type'
|
|
||||||
required: true
|
|
||||||
type: choice
|
|
||||||
options:
|
|
||||||
- 'iptables'
|
|
||||||
- 'ufw'
|
|
||||||
- 'firewalld'
|
|
||||||
- 'aws-security-group'
|
|
||||||
- 'azure-nsg'
|
|
||||||
- 'gcp-firewall'
|
|
||||||
- 'cloudflare'
|
|
||||||
- 'all'
|
|
||||||
default: 'all'
|
|
||||||
output_format:
|
|
||||||
description: 'Output format'
|
|
||||||
required: true
|
|
||||||
type: choice
|
|
||||||
options:
|
|
||||||
- 'shell-script'
|
|
||||||
- 'json'
|
|
||||||
- 'yaml'
|
|
||||||
- 'markdown'
|
|
||||||
- 'all'
|
|
||||||
default: 'markdown'
|
|
||||||
|
|
||||||
# Auto-run when coding agent creates or updates PRs
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- 'copilot/**'
|
|
||||||
- 'agent/**'
|
|
||||||
types: [opened, synchronize, reopened]
|
|
||||||
|
|
||||||
# Auto-run on push to coding agent branches
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- 'copilot/**'
|
|
||||||
- 'agent/**'
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
actions: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
generate-firewall-rules:
|
|
||||||
name: Generate Firewall Rules
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
||||||
|
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@v6
|
|
||||||
with:
|
|
||||||
python-version: '3.11'
|
|
||||||
|
|
||||||
- name: Apply Firewall Rules to Runner (Auto-run only)
|
|
||||||
if: github.event_name != 'workflow_dispatch'
|
|
||||||
env:
|
|
||||||
DEV_FTP_HOST: ${{ vars.DEV_FTP_HOST }}
|
|
||||||
DEV_FTP_PORT: ${{ vars.DEV_FTP_PORT }}
|
|
||||||
run: |
|
|
||||||
echo "🔥 Applying firewall rules for coding agent environment..."
|
|
||||||
echo ""
|
|
||||||
echo "This step ensures the GitHub Actions runner can access trusted domains"
|
|
||||||
echo "including license providers, package registries, and documentation sources."
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Note: GitHub Actions runners are ephemeral and run in controlled environments
|
|
||||||
# This step documents what domains are being accessed during the workflow
|
|
||||||
# Actual firewall configuration is managed by GitHub
|
|
||||||
|
|
||||||
cat > /tmp/trusted-domains.txt << 'EOF'
|
|
||||||
# Trusted domains for coding agent environment
|
|
||||||
# License Providers
|
|
||||||
www.gnu.org
|
|
||||||
opensource.org
|
|
||||||
choosealicense.com
|
|
||||||
spdx.org
|
|
||||||
creativecommons.org
|
|
||||||
apache.org
|
|
||||||
fsf.org
|
|
||||||
|
|
||||||
# Documentation & Standards
|
|
||||||
semver.org
|
|
||||||
keepachangelog.com
|
|
||||||
conventionalcommits.org
|
|
||||||
|
|
||||||
# GitHub & Related
|
|
||||||
github.com
|
|
||||||
api.github.com
|
|
||||||
docs.github.com
|
|
||||||
raw.githubusercontent.com
|
|
||||||
ghcr.io
|
|
||||||
|
|
||||||
# Package Registries
|
|
||||||
npmjs.com
|
|
||||||
registry.npmjs.org
|
|
||||||
pypi.org
|
|
||||||
files.pythonhosted.org
|
|
||||||
packagist.org
|
|
||||||
repo.packagist.org
|
|
||||||
rubygems.org
|
|
||||||
|
|
||||||
# Platform-Specific
|
|
||||||
joomla.org
|
|
||||||
downloads.joomla.org
|
|
||||||
docs.joomla.org
|
|
||||||
php.net
|
|
||||||
getcomposer.org
|
|
||||||
dolibarr.org
|
|
||||||
wiki.dolibarr.org
|
|
||||||
docs.dolibarr.org
|
|
||||||
|
|
||||||
# Moko Consulting
|
|
||||||
mokoconsulting.tech
|
|
||||||
|
|
||||||
# SFTP Deployment Server (DEV_FTP_HOST)
|
|
||||||
${DEV_FTP_HOST:-<not configured>}
|
|
||||||
|
|
||||||
# Google Services
|
|
||||||
drive.google.com
|
|
||||||
docs.google.com
|
|
||||||
sheets.google.com
|
|
||||||
accounts.google.com
|
|
||||||
storage.googleapis.com
|
|
||||||
fonts.googleapis.com
|
|
||||||
fonts.gstatic.com
|
|
||||||
|
|
||||||
# GitHub Extended
|
|
||||||
upload.github.com
|
|
||||||
objects.githubusercontent.com
|
|
||||||
user-images.githubusercontent.com
|
|
||||||
codeload.github.com
|
|
||||||
pkg.github.com
|
|
||||||
|
|
||||||
# Developer Reference
|
|
||||||
developer.mozilla.org
|
|
||||||
stackoverflow.com
|
|
||||||
git-scm.com
|
|
||||||
|
|
||||||
# CDN & Infrastructure
|
|
||||||
cdn.jsdelivr.net
|
|
||||||
unpkg.com
|
|
||||||
cdnjs.cloudflare.com
|
|
||||||
img.shields.io
|
|
||||||
|
|
||||||
# Container Registries
|
|
||||||
hub.docker.com
|
|
||||||
registry-1.docker.io
|
|
||||||
|
|
||||||
# CI & Code Quality
|
|
||||||
codecov.io
|
|
||||||
sonarcloud.io
|
|
||||||
|
|
||||||
# Terraform & Infrastructure
|
|
||||||
registry.terraform.io
|
|
||||||
releases.hashicorp.com
|
|
||||||
checkpoint-api.hashicorp.com
|
|
||||||
EOF
|
|
||||||
|
|
||||||
echo "✓ Trusted domains documented for this runner"
|
|
||||||
echo "✓ GitHub Actions runners have network access to these domains"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Test connectivity to key domains
|
|
||||||
echo "Testing connectivity to key domains..."
|
|
||||||
for domain in "github.com" "www.gnu.org" "npmjs.com" "pypi.org"; do
|
|
||||||
if curl -s --max-time 3 -o /dev/null -w "%{http_code}" "https://$domain" | grep -q "200\|301\|302"; then
|
|
||||||
echo " ✓ $domain is accessible"
|
|
||||||
else
|
|
||||||
echo " ⚠️ $domain connectivity check failed (may be expected)"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Test SFTP server connectivity (TCP port check)
|
|
||||||
SFTP_HOST="${DEV_FTP_HOST:-}"
|
|
||||||
SFTP_PORT="${DEV_FTP_PORT:-22}"
|
|
||||||
if [ -n "$SFTP_HOST" ]; then
|
|
||||||
# Strip any embedded :port suffix
|
|
||||||
SFTP_HOST="${SFTP_HOST%%:*}"
|
|
||||||
echo ""
|
|
||||||
echo "Testing SFTP deployment server connectivity..."
|
|
||||||
if timeout 5 bash -c "echo >/dev/tcp/${SFTP_HOST}/${SFTP_PORT}" 2>/dev/null; then
|
|
||||||
echo " ✓ SFTP server ${SFTP_HOST}:${SFTP_PORT} is reachable"
|
|
||||||
else
|
|
||||||
echo " ⚠️ SFTP server ${SFTP_HOST}:${SFTP_PORT} is not reachable from runner (firewall rule needed)"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo ""
|
|
||||||
echo " ℹ️ DEV_FTP_HOST not configured — skipping SFTP connectivity check"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Generate Firewall Configuration
|
|
||||||
id: generate
|
|
||||||
env:
|
|
||||||
DEV_FTP_HOST: ${{ vars.DEV_FTP_HOST }}
|
|
||||||
DEV_FTP_PORT: ${{ vars.DEV_FTP_PORT }}
|
|
||||||
run: |
|
|
||||||
cat > generate_firewall_config.py << 'PYTHON_EOF'
|
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Enterprise Firewall Configuration Generator
|
|
||||||
|
|
||||||
Generates firewall rules for enterprise-ready deployments allowing
|
|
||||||
access to trusted domains including license providers, documentation
|
|
||||||
sources, package registries, and platform-specific sites.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import yaml
|
|
||||||
import sys
|
|
||||||
from typing import List, Dict
|
|
||||||
|
|
||||||
# SFTP deployment server from org variables
|
|
||||||
_sftp_host_raw = os.environ.get("DEV_FTP_HOST", "").strip()
|
|
||||||
_sftp_port = os.environ.get("DEV_FTP_PORT", "").strip() or "22"
|
|
||||||
# Strip embedded :port suffix if present
|
|
||||||
_sftp_host = _sftp_host_raw.split(":")[0] if _sftp_host_raw else ""
|
|
||||||
if ":" in _sftp_host_raw and not _sftp_port:
|
|
||||||
_sftp_port = _sftp_host_raw.split(":")[1]
|
|
||||||
|
|
||||||
SFTP_HOST = _sftp_host
|
|
||||||
SFTP_PORT = int(_sftp_port) if _sftp_port.isdigit() else 22
|
|
||||||
|
|
||||||
# Trusted domains from .github/copilot.yml
|
|
||||||
TRUSTED_DOMAINS = {
|
|
||||||
"license_providers": [
|
|
||||||
"www.gnu.org",
|
|
||||||
"opensource.org",
|
|
||||||
"choosealicense.com",
|
|
||||||
"spdx.org",
|
|
||||||
"creativecommons.org",
|
|
||||||
"apache.org",
|
|
||||||
"fsf.org",
|
|
||||||
],
|
|
||||||
"documentation_standards": [
|
|
||||||
"semver.org",
|
|
||||||
"keepachangelog.com",
|
|
||||||
"conventionalcommits.org",
|
|
||||||
],
|
|
||||||
"github_related": [
|
|
||||||
"github.com",
|
|
||||||
"api.github.com",
|
|
||||||
"docs.github.com",
|
|
||||||
"raw.githubusercontent.com",
|
|
||||||
"ghcr.io",
|
|
||||||
],
|
|
||||||
"package_registries": [
|
|
||||||
"npmjs.com",
|
|
||||||
"registry.npmjs.org",
|
|
||||||
"pypi.org",
|
|
||||||
"files.pythonhosted.org",
|
|
||||||
"packagist.org",
|
|
||||||
"repo.packagist.org",
|
|
||||||
"rubygems.org",
|
|
||||||
],
|
|
||||||
"standards_organizations": [
|
|
||||||
"json-schema.org",
|
|
||||||
"w3.org",
|
|
||||||
"ietf.org",
|
|
||||||
],
|
|
||||||
"platform_specific": [
|
|
||||||
"joomla.org",
|
|
||||||
"downloads.joomla.org",
|
|
||||||
"docs.joomla.org",
|
|
||||||
"php.net",
|
|
||||||
"getcomposer.org",
|
|
||||||
"dolibarr.org",
|
|
||||||
"wiki.dolibarr.org",
|
|
||||||
"docs.dolibarr.org",
|
|
||||||
],
|
|
||||||
"moko_consulting": [
|
|
||||||
"mokoconsulting.tech",
|
|
||||||
],
|
|
||||||
"google_services": [
|
|
||||||
"drive.google.com",
|
|
||||||
"docs.google.com",
|
|
||||||
"sheets.google.com",
|
|
||||||
"accounts.google.com",
|
|
||||||
"storage.googleapis.com",
|
|
||||||
"fonts.googleapis.com",
|
|
||||||
"fonts.gstatic.com",
|
|
||||||
],
|
|
||||||
"github_extended": [
|
|
||||||
"upload.github.com",
|
|
||||||
"objects.githubusercontent.com",
|
|
||||||
"user-images.githubusercontent.com",
|
|
||||||
"codeload.github.com",
|
|
||||||
"pkg.github.com",
|
|
||||||
],
|
|
||||||
"developer_reference": [
|
|
||||||
"developer.mozilla.org",
|
|
||||||
"stackoverflow.com",
|
|
||||||
"git-scm.com",
|
|
||||||
],
|
|
||||||
"cdn_and_infrastructure": [
|
|
||||||
"cdn.jsdelivr.net",
|
|
||||||
"unpkg.com",
|
|
||||||
"cdnjs.cloudflare.com",
|
|
||||||
"img.shields.io",
|
|
||||||
],
|
|
||||||
"container_registries": [
|
|
||||||
"hub.docker.com",
|
|
||||||
"registry-1.docker.io",
|
|
||||||
],
|
|
||||||
"ci_code_quality": [
|
|
||||||
"codecov.io",
|
|
||||||
"sonarcloud.io",
|
|
||||||
],
|
|
||||||
"terraform_infrastructure": [
|
|
||||||
"registry.terraform.io",
|
|
||||||
"releases.hashicorp.com",
|
|
||||||
"checkpoint-api.hashicorp.com",
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
# Inject SFTP deployment server as a separate category (port 22, not 443)
|
|
||||||
if SFTP_HOST:
|
|
||||||
TRUSTED_DOMAINS["sftp_deployment_server"] = [SFTP_HOST]
|
|
||||||
print(f"ℹ️ SFTP deployment server: {SFTP_HOST}:{SFTP_PORT}")
|
|
||||||
|
|
||||||
def generate_sftp_iptables_rules(host: str, port: int) -> str:
|
|
||||||
"""Generate iptables rules specifically for SFTP egress"""
|
|
||||||
return (
|
|
||||||
f"# Allow SFTP to deployment server {host}:{port}\n"
|
|
||||||
f"iptables -A OUTPUT -p tcp -d $(dig +short {host} | head -1)"
|
|
||||||
f" --dport {port} -j ACCEPT # SFTP deploy\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
def generate_sftp_ufw_rules(host: str, port: int) -> str:
|
|
||||||
"""Generate UFW rules for SFTP egress"""
|
|
||||||
return (
|
|
||||||
f"# Allow SFTP to deployment server\n"
|
|
||||||
f"ufw allow out to $(dig +short {host} | head -1)"
|
|
||||||
f" port {port} proto tcp comment 'SFTP deploy to {host}'\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
def generate_sftp_firewalld_rules(host: str, port: int) -> str:
|
|
||||||
"""Generate firewalld rules for SFTP egress"""
|
|
||||||
return (
|
|
||||||
f"# Allow SFTP to deployment server\n"
|
|
||||||
f"firewall-cmd --permanent --add-rich-rule='"
|
|
||||||
f"rule family=ipv4 destination address=$(dig +short {host} | head -1)"
|
|
||||||
f" port port={port} protocol=tcp accept' # SFTP deploy\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
def generate_iptables_rules(domains: List[str]) -> str:
|
|
||||||
"""Generate iptables firewall rules"""
|
|
||||||
rules = ["#!/bin/bash", "", "# Enterprise Firewall Rules - iptables", ""]
|
|
||||||
rules.append("# Allow outbound HTTPS to trusted domains")
|
|
||||||
rules.append("")
|
|
||||||
|
|
||||||
for domain in domains:
|
|
||||||
rules.append(f"# Allow {domain}")
|
|
||||||
rules.append(f"iptables -A OUTPUT -p tcp -d $(dig +short {domain} | head -1) --dport 443 -j ACCEPT")
|
|
||||||
|
|
||||||
rules.append("")
|
|
||||||
rules.append("# Allow DNS lookups")
|
|
||||||
rules.append("iptables -A OUTPUT -p udp --dport 53 -j ACCEPT")
|
|
||||||
rules.append("iptables -A OUTPUT -p tcp --dport 53 -j ACCEPT")
|
|
||||||
|
|
||||||
return "\n".join(rules)
|
|
||||||
|
|
||||||
def generate_ufw_rules(domains: List[str]) -> str:
|
|
||||||
"""Generate UFW firewall rules"""
|
|
||||||
rules = ["#!/bin/bash", "", "# Enterprise Firewall Rules - UFW", ""]
|
|
||||||
rules.append("# Allow outbound HTTPS to trusted domains")
|
|
||||||
rules.append("")
|
|
||||||
|
|
||||||
for domain in domains:
|
|
||||||
rules.append(f"# Allow {domain}")
|
|
||||||
rules.append(f"ufw allow out to $(dig +short {domain} | head -1) port 443 proto tcp comment 'Allow {domain}'")
|
|
||||||
|
|
||||||
rules.append("")
|
|
||||||
rules.append("# Allow DNS")
|
|
||||||
rules.append("ufw allow out 53/udp comment 'Allow DNS UDP'")
|
|
||||||
rules.append("ufw allow out 53/tcp comment 'Allow DNS TCP'")
|
|
||||||
|
|
||||||
return "\n".join(rules)
|
|
||||||
|
|
||||||
def generate_firewalld_rules(domains: List[str]) -> str:
|
|
||||||
"""Generate firewalld rules"""
|
|
||||||
rules = ["#!/bin/bash", "", "# Enterprise Firewall Rules - firewalld", ""]
|
|
||||||
rules.append("# Add trusted domains to firewall")
|
|
||||||
rules.append("")
|
|
||||||
|
|
||||||
for domain in domains:
|
|
||||||
rules.append(f"# Allow {domain}")
|
|
||||||
rules.append(f"firewall-cmd --permanent --add-rich-rule='rule family=ipv4 destination address=$(dig +short {domain} | head -1) port port=443 protocol=tcp accept'")
|
|
||||||
|
|
||||||
rules.append("")
|
|
||||||
rules.append("# Reload firewall")
|
|
||||||
rules.append("firewall-cmd --reload")
|
|
||||||
|
|
||||||
return "\n".join(rules)
|
|
||||||
|
|
||||||
def generate_aws_security_group(domains: List[str]) -> Dict:
|
|
||||||
"""Generate AWS Security Group rules (JSON format)"""
|
|
||||||
rules = {
|
|
||||||
"SecurityGroupRules": {
|
|
||||||
"Egress": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for domain in domains:
|
|
||||||
rules["SecurityGroupRules"]["Egress"].append({
|
|
||||||
"Description": f"Allow HTTPS to {domain}",
|
|
||||||
"IpProtocol": "tcp",
|
|
||||||
"FromPort": 443,
|
|
||||||
"ToPort": 443,
|
|
||||||
"CidrIp": "0.0.0.0/0", # In practice, resolve to specific IPs
|
|
||||||
"Tags": [{
|
|
||||||
"Key": "Domain",
|
|
||||||
"Value": domain
|
|
||||||
}]
|
|
||||||
})
|
|
||||||
|
|
||||||
# Add DNS
|
|
||||||
rules["SecurityGroupRules"]["Egress"].append({
|
|
||||||
"Description": "Allow DNS",
|
|
||||||
"IpProtocol": "udp",
|
|
||||||
"FromPort": 53,
|
|
||||||
"ToPort": 53,
|
|
||||||
"CidrIp": "0.0.0.0/0"
|
|
||||||
})
|
|
||||||
|
|
||||||
return rules
|
|
||||||
|
|
||||||
def generate_markdown_documentation(domains_by_category: Dict[str, List[str]]) -> str:
|
|
||||||
"""Generate markdown documentation"""
|
|
||||||
md = ["# Enterprise Firewall Configuration Guide", ""]
|
|
||||||
md.append("## Overview")
|
|
||||||
md.append("")
|
|
||||||
md.append("This document provides firewall configuration guidance for enterprise-ready deployments.")
|
|
||||||
md.append("It lists trusted domains that should be whitelisted for outbound access to ensure")
|
|
||||||
md.append("proper functionality of license validation, package management, and documentation access.")
|
|
||||||
md.append("")
|
|
||||||
|
|
||||||
md.append("## Trusted Domains by Category")
|
|
||||||
md.append("")
|
|
||||||
|
|
||||||
all_domains = []
|
|
||||||
for category, domains in domains_by_category.items():
|
|
||||||
category_name = category.replace("_", " ").title()
|
|
||||||
md.append(f"### {category_name}")
|
|
||||||
md.append("")
|
|
||||||
md.append("| Domain | Purpose |")
|
|
||||||
md.append("|--------|---------|")
|
|
||||||
|
|
||||||
for domain in domains:
|
|
||||||
all_domains.append(domain)
|
|
||||||
purpose = get_domain_purpose(domain)
|
|
||||||
md.append(f"| `{domain}` | {purpose} |")
|
|
||||||
|
|
||||||
md.append("")
|
|
||||||
|
|
||||||
md.append("## Implementation Examples")
|
|
||||||
md.append("")
|
|
||||||
|
|
||||||
md.append("### iptables Example")
|
|
||||||
md.append("")
|
|
||||||
md.append("```bash")
|
|
||||||
md.append("# Allow HTTPS to trusted domain")
|
|
||||||
md.append(f"iptables -A OUTPUT -p tcp -d $(dig +short {all_domains[0]}) --dport 443 -j ACCEPT")
|
|
||||||
md.append("```")
|
|
||||||
md.append("")
|
|
||||||
|
|
||||||
md.append("### UFW Example")
|
|
||||||
md.append("")
|
|
||||||
md.append("```bash")
|
|
||||||
md.append("# Allow HTTPS to trusted domain")
|
|
||||||
md.append(f"ufw allow out to {all_domains[0]} port 443 proto tcp")
|
|
||||||
md.append("```")
|
|
||||||
md.append("")
|
|
||||||
|
|
||||||
md.append("### AWS Security Group Example")
|
|
||||||
md.append("")
|
|
||||||
md.append("```json")
|
|
||||||
md.append("{")
|
|
||||||
md.append(' "IpPermissions": [{')
|
|
||||||
md.append(' "IpProtocol": "tcp",')
|
|
||||||
md.append(' "FromPort": 443,')
|
|
||||||
md.append(' "ToPort": 443,')
|
|
||||||
md.append(' "IpRanges": [{"CidrIp": "0.0.0.0/0", "Description": "HTTPS to trusted domains"}]')
|
|
||||||
md.append(" }]")
|
|
||||||
md.append("}")
|
|
||||||
md.append("```")
|
|
||||||
md.append("")
|
|
||||||
|
|
||||||
md.append("## Ports Required")
|
|
||||||
md.append("")
|
|
||||||
md.append("| Port | Protocol | Purpose |")
|
|
||||||
md.append("|------|----------|---------|")
|
|
||||||
md.append("| 443 | TCP | HTTPS (secure web access) |")
|
|
||||||
md.append("| 80 | TCP | HTTP (redirects to HTTPS) |")
|
|
||||||
md.append("| 53 | UDP/TCP | DNS resolution |")
|
|
||||||
md.append("")
|
|
||||||
|
|
||||||
md.append("## Security Considerations")
|
|
||||||
md.append("")
|
|
||||||
md.append("1. **DNS Resolution**: Ensure DNS queries are allowed (port 53 UDP/TCP)")
|
|
||||||
md.append("2. **Certificate Validation**: HTTPS requires ability to reach certificate authorities")
|
|
||||||
md.append("3. **Dynamic IPs**: Some domains use CDNs with dynamic IPs - consider using FQDNs in rules")
|
|
||||||
md.append("4. **Regular Updates**: Review and update whitelist as services change")
|
|
||||||
md.append("5. **Logging**: Enable logging for blocked connections to identify missing rules")
|
|
||||||
md.append("")
|
|
||||||
|
|
||||||
md.append("## Compliance Notes")
|
|
||||||
md.append("")
|
|
||||||
md.append("- All listed domains provide read-only access to public information")
|
|
||||||
md.append("- License providers enable GPL compliance verification")
|
|
||||||
md.append("- Package registries support dependency security scanning")
|
|
||||||
md.append("- No authentication credentials are transmitted to these domains")
|
|
||||||
md.append("")
|
|
||||||
|
|
||||||
return "\n".join(md)
|
|
||||||
|
|
||||||
def get_domain_purpose(domain: str) -> str:
|
|
||||||
"""Get human-readable purpose for a domain"""
|
|
||||||
purposes = {
|
|
||||||
"www.gnu.org": "GNU licenses and documentation",
|
|
||||||
"opensource.org": "Open Source Initiative resources",
|
|
||||||
"choosealicense.com": "GitHub license selection tool",
|
|
||||||
"spdx.org": "Software Package Data Exchange identifiers",
|
|
||||||
"creativecommons.org": "Creative Commons licenses",
|
|
||||||
"apache.org": "Apache Software Foundation licenses",
|
|
||||||
"fsf.org": "Free Software Foundation resources",
|
|
||||||
"semver.org": "Semantic versioning specification",
|
|
||||||
"keepachangelog.com": "Changelog format standards",
|
|
||||||
"conventionalcommits.org": "Commit message conventions",
|
|
||||||
"github.com": "GitHub platform access",
|
|
||||||
"api.github.com": "GitHub API access",
|
|
||||||
"docs.github.com": "GitHub documentation",
|
|
||||||
"raw.githubusercontent.com": "GitHub raw content access",
|
|
||||||
"npmjs.com": "npm package registry",
|
|
||||||
"pypi.org": "Python Package Index",
|
|
||||||
"packagist.org": "PHP Composer package registry",
|
|
||||||
"rubygems.org": "Ruby gems registry",
|
|
||||||
"joomla.org": "Joomla CMS platform",
|
|
||||||
"php.net": "PHP documentation and downloads",
|
|
||||||
"dolibarr.org": "Dolibarr ERP/CRM platform",
|
|
||||||
}
|
|
||||||
return purposes.get(domain, "Trusted resource")
|
|
||||||
|
|
||||||
def main():
|
|
||||||
# Use inputs if provided (manual dispatch), otherwise use defaults (auto-run)
|
|
||||||
firewall_type = "${{ github.event.inputs.firewall_type }}" or "all"
|
|
||||||
output_format = "${{ github.event.inputs.output_format }}" or "markdown"
|
|
||||||
|
|
||||||
print(f"Running in {'manual' if '${{ github.event.inputs.firewall_type }}' else 'automatic'} mode")
|
|
||||||
print(f"Firewall type: {firewall_type}")
|
|
||||||
print(f"Output format: {output_format}")
|
|
||||||
print("")
|
|
||||||
|
|
||||||
# Collect all domains
|
|
||||||
all_domains = []
|
|
||||||
for domains in TRUSTED_DOMAINS.values():
|
|
||||||
all_domains.extend(domains)
|
|
||||||
|
|
||||||
# Remove duplicates and sort
|
|
||||||
all_domains = sorted(set(all_domains))
|
|
||||||
|
|
||||||
print(f"Generating firewall rules for {len(all_domains)} trusted domains...")
|
|
||||||
print("")
|
|
||||||
|
|
||||||
# Exclude SFTP server from HTTPS rule generation (different port)
|
|
||||||
https_domains = [d for d in all_domains if d != SFTP_HOST]
|
|
||||||
|
|
||||||
# Generate based on firewall type
|
|
||||||
if firewall_type in ["iptables", "all"]:
|
|
||||||
rules = generate_iptables_rules(https_domains)
|
|
||||||
if SFTP_HOST:
|
|
||||||
rules += "\n# ── SFTP Deployment Server ──────────────────────────────\n"
|
|
||||||
rules += generate_sftp_iptables_rules(SFTP_HOST, SFTP_PORT)
|
|
||||||
with open("firewall-rules-iptables.sh", "w") as f:
|
|
||||||
f.write(rules)
|
|
||||||
print("✓ Generated iptables rules: firewall-rules-iptables.sh")
|
|
||||||
|
|
||||||
if firewall_type in ["ufw", "all"]:
|
|
||||||
rules = generate_ufw_rules(https_domains)
|
|
||||||
if SFTP_HOST:
|
|
||||||
rules += "\n# ── SFTP Deployment Server ──────────────────────────────\n"
|
|
||||||
rules += generate_sftp_ufw_rules(SFTP_HOST, SFTP_PORT)
|
|
||||||
with open("firewall-rules-ufw.sh", "w") as f:
|
|
||||||
f.write(rules)
|
|
||||||
print("✓ Generated UFW rules: firewall-rules-ufw.sh")
|
|
||||||
|
|
||||||
if firewall_type in ["firewalld", "all"]:
|
|
||||||
rules = generate_firewalld_rules(https_domains)
|
|
||||||
if SFTP_HOST:
|
|
||||||
rules += "\n# ── SFTP Deployment Server ──────────────────────────────\n"
|
|
||||||
rules += generate_sftp_firewalld_rules(SFTP_HOST, SFTP_PORT)
|
|
||||||
with open("firewall-rules-firewalld.sh", "w") as f:
|
|
||||||
f.write(rules)
|
|
||||||
print("✓ Generated firewalld rules: firewall-rules-firewalld.sh")
|
|
||||||
|
|
||||||
if firewall_type in ["aws-security-group", "all"]:
|
|
||||||
rules = generate_aws_security_group(all_domains)
|
|
||||||
with open("firewall-rules-aws-sg.json", "w") as f:
|
|
||||||
json.dump(rules, f, indent=2)
|
|
||||||
print("✓ Generated AWS Security Group rules: firewall-rules-aws-sg.json")
|
|
||||||
|
|
||||||
if output_format in ["yaml", "all"]:
|
|
||||||
with open("trusted-domains.yml", "w") as f:
|
|
||||||
yaml.dump(TRUSTED_DOMAINS, f, default_flow_style=False)
|
|
||||||
print("✓ Generated YAML domain list: trusted-domains.yml")
|
|
||||||
|
|
||||||
if output_format in ["json", "all"]:
|
|
||||||
with open("trusted-domains.json", "w") as f:
|
|
||||||
json.dump(TRUSTED_DOMAINS, f, indent=2)
|
|
||||||
print("✓ Generated JSON domain list: trusted-domains.json")
|
|
||||||
|
|
||||||
if output_format in ["markdown", "all"]:
|
|
||||||
md = generate_markdown_documentation(TRUSTED_DOMAINS)
|
|
||||||
with open("FIREWALL_CONFIGURATION.md", "w") as f:
|
|
||||||
f.write(md)
|
|
||||||
print("✓ Generated documentation: FIREWALL_CONFIGURATION.md")
|
|
||||||
|
|
||||||
print("")
|
|
||||||
print("Domain Categories:")
|
|
||||||
for category, domains in TRUSTED_DOMAINS.items():
|
|
||||||
print(f" - {category}: {len(domains)} domains")
|
|
||||||
|
|
||||||
print("")
|
|
||||||
print("Total unique domains: ", len(all_domains))
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
PYTHON_EOF
|
|
||||||
|
|
||||||
chmod +x generate_firewall_config.py
|
|
||||||
pip install PyYAML
|
|
||||||
python3 generate_firewall_config.py
|
|
||||||
|
|
||||||
- name: Upload Firewall Configuration Artifacts
|
|
||||||
uses: actions/upload-artifact@v6
|
|
||||||
with:
|
|
||||||
name: firewall-configurations
|
|
||||||
path: |
|
|
||||||
firewall-rules-*.sh
|
|
||||||
firewall-rules-*.json
|
|
||||||
trusted-domains.*
|
|
||||||
FIREWALL_CONFIGURATION.md
|
|
||||||
retention-days: 90
|
|
||||||
|
|
||||||
- name: Display Summary
|
|
||||||
run: |
|
|
||||||
echo "## Firewall Configuration" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
|
||||||
echo "**Mode**: Manual Execution" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "Firewall rules have been generated for enterprise-ready deployments." >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "**Mode**: Automatic Execution (Coding Agent Active)" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "This workflow ran automatically because a coding agent (GitHub Copilot) is active." >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "Firewall configuration has been validated for the coding agent environment." >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "### Files Generated" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
if ls firewall-rules-* trusted-domains.* FIREWALL_CONFIGURATION.md 2>/dev/null; then
|
|
||||||
ls -lh firewall-rules-* trusted-domains.* FIREWALL_CONFIGURATION.md 2>/dev/null | awk '{print "- " $9 " (" $5 ")"}' >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "- Documentation generated" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
|
||||||
echo "### Download Artifacts" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "Download the generated firewall configurations from the workflow artifacts." >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "### Trusted Domains Active" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "The coding agent has access to:" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "- License providers (GPL, OSI, SPDX, Apache, etc.)" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "- Package registries (npm, PyPI, Packagist, RubyGems)" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "- Documentation sources (GitHub, Joomla, Dolibarr, PHP)" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "- Standards organizations (W3C, IETF, JSON Schema)" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Usage Instructions:
|
|
||||||
#
|
|
||||||
# This workflow runs in two modes:
|
|
||||||
#
|
|
||||||
# 1. AUTOMATIC MODE (Coding Agent):
|
|
||||||
# - Triggers when coding agent branches (copilot/**, agent/**) are pushed or PR'd
|
|
||||||
# - Validates firewall configuration for the coding agent environment
|
|
||||||
# - Documents accessible domains for compliance
|
|
||||||
# - Ensures license sources and package registries are available
|
|
||||||
#
|
|
||||||
# 2. MANUAL MODE (Enterprise Configuration):
|
|
||||||
# - Manually trigger from the Actions tab
|
|
||||||
# - Select desired firewall type and output format
|
|
||||||
# - Download generated artifacts
|
|
||||||
# - Apply firewall rules to your enterprise environment
|
|
||||||
#
|
|
||||||
# Configuration:
|
|
||||||
# - Trusted domains are sourced from .github/copilot.yml
|
|
||||||
# - Modify copilot.yml to add/remove trusted domains
|
|
||||||
# - Changes automatically propagate to firewall rules
|
|
||||||
#
|
|
||||||
# Important Notes:
|
|
||||||
# - Review generated rules before applying to production
|
|
||||||
# - Some domains may use CDNs with dynamic IPs
|
|
||||||
# - Consider using FQDN-based rules where supported
|
|
||||||
# - Test thoroughly in staging environment first
|
|
||||||
# - Monitor logs for blocked connections
|
|
||||||
# - Update rules as domains/services change
|
|
||||||
@@ -1,525 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# This file is part of a Moko Consulting project.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: GitHub.Workflow
|
|
||||||
# INGROUP: MokoStandards.Maintenance
|
|
||||||
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
|
|
||||||
# PATH: /templates/workflows/shared/repository-cleanup.yml.template
|
|
||||||
# VERSION: 04.06.00
|
|
||||||
# BRIEF: Recurring repository maintenance — labels, branches, workflows, logs, doc indexes
|
|
||||||
# NOTE: Synced via bulk-repo-sync to .github/workflows/repository-cleanup.yml in all governed repos.
|
|
||||||
# Runs on the 1st and 15th of each month at 6:00 AM UTC, and on manual dispatch.
|
|
||||||
|
|
||||||
name: Repository Cleanup
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: '0 6 1,15 * *'
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
reset_labels:
|
|
||||||
description: 'Delete ALL existing labels and recreate the standard set'
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
clean_branches:
|
|
||||||
description: 'Delete old chore/sync-mokostandards-* branches'
|
|
||||||
type: boolean
|
|
||||||
default: true
|
|
||||||
clean_workflows:
|
|
||||||
description: 'Delete orphaned workflow runs (cancelled, stale)'
|
|
||||||
type: boolean
|
|
||||||
default: true
|
|
||||||
clean_logs:
|
|
||||||
description: 'Delete workflow run logs older than 30 days'
|
|
||||||
type: boolean
|
|
||||||
default: true
|
|
||||||
fix_templates:
|
|
||||||
description: 'Strip copyright comment blocks from issue templates'
|
|
||||||
type: boolean
|
|
||||||
default: true
|
|
||||||
rebuild_indexes:
|
|
||||||
description: 'Rebuild docs/ index files'
|
|
||||||
type: boolean
|
|
||||||
default: true
|
|
||||||
delete_closed_issues:
|
|
||||||
description: 'Delete issues that have been closed for more than 30 days'
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
|
|
||||||
env:
|
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
issues: write
|
|
||||||
actions: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
cleanup:
|
|
||||||
name: Repository Maintenance
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GH_TOKEN || github.token }}
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Check actor permission
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
|
|
||||||
run: |
|
|
||||||
ACTOR="${{ github.actor }}"
|
|
||||||
# Schedule triggers use github-actions[bot]
|
|
||||||
if [ "${{ github.event_name }}" = "schedule" ]; then
|
|
||||||
echo "✅ Scheduled run — authorized"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
AUTHORIZED_USERS="jmiller-moko github-actions[bot]"
|
|
||||||
for user in $AUTHORIZED_USERS; do
|
|
||||||
if [ "$ACTOR" = "$user" ]; then
|
|
||||||
echo "✅ ${ACTOR} authorized"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
PERMISSION=$(gh api "repos/${{ github.repository }}/collaborators/${ACTOR}/permission" \
|
|
||||||
--jq '.permission' 2>/dev/null)
|
|
||||||
case "$PERMISSION" in
|
|
||||||
admin|maintain) echo "✅ ${ACTOR} has ${PERMISSION}" ;;
|
|
||||||
*) echo "❌ Admin or maintain required"; exit 1 ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# ── Determine which tasks to run ─────────────────────────────────────
|
|
||||||
# On schedule: run all tasks with safe defaults (labels NOT reset)
|
|
||||||
# On dispatch: use input toggles
|
|
||||||
- name: Set task flags
|
|
||||||
id: tasks
|
|
||||||
run: |
|
|
||||||
if [ "${{ github.event_name }}" = "schedule" ]; then
|
|
||||||
echo "reset_labels=false" >> $GITHUB_OUTPUT
|
|
||||||
echo "clean_branches=true" >> $GITHUB_OUTPUT
|
|
||||||
echo "clean_workflows=true" >> $GITHUB_OUTPUT
|
|
||||||
echo "clean_logs=true" >> $GITHUB_OUTPUT
|
|
||||||
echo "fix_templates=true" >> $GITHUB_OUTPUT
|
|
||||||
echo "rebuild_indexes=true" >> $GITHUB_OUTPUT
|
|
||||||
echo "delete_closed_issues=false" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "reset_labels=${{ inputs.reset_labels }}" >> $GITHUB_OUTPUT
|
|
||||||
echo "clean_branches=${{ inputs.clean_branches }}" >> $GITHUB_OUTPUT
|
|
||||||
echo "clean_workflows=${{ inputs.clean_workflows }}" >> $GITHUB_OUTPUT
|
|
||||||
echo "clean_logs=${{ inputs.clean_logs }}" >> $GITHUB_OUTPUT
|
|
||||||
echo "fix_templates=${{ inputs.fix_templates }}" >> $GITHUB_OUTPUT
|
|
||||||
echo "rebuild_indexes=${{ inputs.rebuild_indexes }}" >> $GITHUB_OUTPUT
|
|
||||||
echo "delete_closed_issues=${{ inputs.delete_closed_issues }}" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── DELETE RETIRED WORKFLOWS (always runs) ────────────────────────────
|
|
||||||
- name: Delete retired workflow files
|
|
||||||
run: |
|
|
||||||
echo "## 🗑️ Retired Workflow Cleanup" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
RETIRED=(
|
|
||||||
".github/workflows/build.yml"
|
|
||||||
".github/workflows/code-quality.yml"
|
|
||||||
".github/workflows/release-cycle.yml"
|
|
||||||
".github/workflows/release-pipeline.yml"
|
|
||||||
".github/workflows/branch-cleanup.yml"
|
|
||||||
".github/workflows/auto-update-changelog.yml"
|
|
||||||
".github/workflows/enterprise-issue-manager.yml"
|
|
||||||
".github/workflows/flush-actions-cache.yml"
|
|
||||||
".github/workflows/mokostandards-script-runner.yml"
|
|
||||||
".github/workflows/unified-ci.yml"
|
|
||||||
".github/workflows/unified-platform-testing.yml"
|
|
||||||
".github/workflows/reusable-build.yml"
|
|
||||||
".github/workflows/reusable-ci-validation.yml"
|
|
||||||
".github/workflows/reusable-deploy.yml"
|
|
||||||
".github/workflows/reusable-php-quality.yml"
|
|
||||||
".github/workflows/reusable-platform-testing.yml"
|
|
||||||
".github/workflows/reusable-project-detector.yml"
|
|
||||||
".github/workflows/reusable-release.yml"
|
|
||||||
".github/workflows/reusable-script-executor.yml"
|
|
||||||
".github/workflows/rebuild-docs-indexes.yml"
|
|
||||||
".github/workflows/setup-project-v2.yml"
|
|
||||||
".github/workflows/sync-docs-to-project.yml"
|
|
||||||
".github/workflows/release.yml"
|
|
||||||
".github/workflows/sync-changelogs.yml"
|
|
||||||
".github/workflows/version_branch.yml"
|
|
||||||
"update.json"
|
|
||||||
".github/workflows/auto-version-branch.yml"
|
|
||||||
".github/workflows/publish-to-mokodolibarr.yml"
|
|
||||||
".github/workflows/ci.yml"
|
|
||||||
".github/workflows/deploy-rs.yml"
|
|
||||||
"sftp-config.json"
|
|
||||||
"sftp-config.json.template"
|
|
||||||
"scripts/sftp-config"
|
|
||||||
)
|
|
||||||
|
|
||||||
DELETED=0
|
|
||||||
for wf in "${RETIRED[@]}"; do
|
|
||||||
if [ -f "$wf" ]; then
|
|
||||||
git rm "$wf" 2>/dev/null || rm -f "$wf"
|
|
||||||
echo " Deleted: \`$(basename $wf)\`" >> $GITHUB_STEP_SUMMARY
|
|
||||||
DELETED=$((DELETED+1))
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ "$DELETED" -gt 0 ]; then
|
|
||||||
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
|
||||||
git config --local user.name "github-actions[bot]"
|
|
||||||
git add -A
|
|
||||||
git commit -m "chore: delete ${DELETED} retired workflow file(s) [skip ci]" \
|
|
||||||
--author="github-actions[bot] <github-actions[bot]@users.noreply.github.com>"
|
|
||||||
git push
|
|
||||||
echo "✅ ${DELETED} retired workflow(s) deleted" >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "✅ No retired workflows found" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── LABEL RESET ──────────────────────────────────────────────────────
|
|
||||||
- name: Reset labels to standard set
|
|
||||||
if: steps.tasks.outputs.reset_labels == 'true'
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
|
|
||||||
run: |
|
|
||||||
REPO="${{ github.repository }}"
|
|
||||||
echo "## 🏷️ Label Reset" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
gh api "repos/${REPO}/labels?per_page=100" --paginate --jq '.[].name' | while read -r label; do
|
|
||||||
ENCODED=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$label', safe=''))")
|
|
||||||
gh api -X DELETE "repos/${REPO}/labels/${ENCODED}" --silent 2>/dev/null || true
|
|
||||||
done
|
|
||||||
|
|
||||||
while IFS='|' read -r name color description; do
|
|
||||||
[ -z "$name" ] && continue
|
|
||||||
gh api "repos/${REPO}/labels" \
|
|
||||||
-f name="$name" -f color="$color" -f description="$description" \
|
|
||||||
--silent 2>/dev/null || true
|
|
||||||
done << 'LABELS'
|
|
||||||
joomla|7F52FF|Joomla extension or component
|
|
||||||
dolibarr|FF6B6B|Dolibarr module or extension
|
|
||||||
generic|808080|Generic project or library
|
|
||||||
php|4F5D95|PHP code changes
|
|
||||||
javascript|F7DF1E|JavaScript code changes
|
|
||||||
typescript|3178C6|TypeScript code changes
|
|
||||||
python|3776AB|Python code changes
|
|
||||||
css|1572B6|CSS/styling changes
|
|
||||||
html|E34F26|HTML template changes
|
|
||||||
documentation|0075CA|Documentation changes
|
|
||||||
ci-cd|000000|CI/CD pipeline changes
|
|
||||||
docker|2496ED|Docker configuration changes
|
|
||||||
tests|00FF00|Test suite changes
|
|
||||||
security|FF0000|Security-related changes
|
|
||||||
dependencies|0366D6|Dependency updates
|
|
||||||
config|F9D0C4|Configuration file changes
|
|
||||||
build|FFA500|Build system changes
|
|
||||||
automation|8B4513|Automated processes or scripts
|
|
||||||
mokostandards|B60205|MokoStandards compliance
|
|
||||||
needs-review|FBCA04|Awaiting code review
|
|
||||||
work-in-progress|D93F0B|Work in progress, not ready for merge
|
|
||||||
breaking-change|D73A4A|Breaking API or functionality change
|
|
||||||
priority: critical|B60205|Critical priority, must be addressed immediately
|
|
||||||
priority: high|D93F0B|High priority
|
|
||||||
priority: medium|FBCA04|Medium priority
|
|
||||||
priority: low|0E8A16|Low priority
|
|
||||||
type: bug|D73A4A|Something isn't working
|
|
||||||
type: feature|A2EEEF|New feature or request
|
|
||||||
type: enhancement|84B6EB|Enhancement to existing feature
|
|
||||||
type: refactor|F9D0C4|Code refactoring
|
|
||||||
type: chore|FEF2C0|Maintenance tasks
|
|
||||||
type: version|0E8A16|Version-related change
|
|
||||||
status: pending|FBCA04|Pending action or decision
|
|
||||||
status: in-progress|0E8A16|Currently being worked on
|
|
||||||
status: blocked|B60205|Blocked by another issue or dependency
|
|
||||||
status: on-hold|D4C5F9|Temporarily on hold
|
|
||||||
status: wontfix|FFFFFF|This will not be worked on
|
|
||||||
size/xs|C5DEF5|Extra small change (1-10 lines)
|
|
||||||
size/s|6FD1E2|Small change (11-30 lines)
|
|
||||||
size/m|F9DD72|Medium change (31-100 lines)
|
|
||||||
size/l|FFA07A|Large change (101-300 lines)
|
|
||||||
size/xl|FF6B6B|Extra large change (301-1000 lines)
|
|
||||||
size/xxl|B60205|Extremely large change (1000+ lines)
|
|
||||||
health: excellent|0E8A16|Health score 90-100
|
|
||||||
health: good|FBCA04|Health score 70-89
|
|
||||||
health: fair|FFA500|Health score 50-69
|
|
||||||
health: poor|FF6B6B|Health score below 50
|
|
||||||
standards-update|B60205|MokoStandards sync update
|
|
||||||
standards-drift|FBCA04|Repository drifted from MokoStandards
|
|
||||||
sync-report|0075CA|Bulk sync run report
|
|
||||||
sync-failure|D73A4A|Bulk sync failure requiring attention
|
|
||||||
push-failure|D73A4A|File push failure requiring attention
|
|
||||||
health-check|0E8A16|Repository health check results
|
|
||||||
version-drift|FFA500|Version mismatch detected
|
|
||||||
deploy-failure|CC0000|Automated deploy failure tracking
|
|
||||||
template-validation-failure|D73A4A|Template workflow validation failure
|
|
||||||
version|0E8A16|Version bump or release
|
|
||||||
LABELS
|
|
||||||
|
|
||||||
echo "✅ Standard labels created" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
# ── BRANCH CLEANUP ───────────────────────────────────────────────────
|
|
||||||
- name: Delete old sync branches
|
|
||||||
if: steps.tasks.outputs.clean_branches == 'true'
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
|
|
||||||
run: |
|
|
||||||
REPO="${{ github.repository }}"
|
|
||||||
CURRENT="chore/sync-mokostandards-v04.05"
|
|
||||||
echo "## 🌿 Branch Cleanup" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
FOUND=false
|
|
||||||
gh api "repos/${REPO}/branches?per_page=100" --jq '.[].name' | \
|
|
||||||
grep "^chore/sync-mokostandards" | \
|
|
||||||
grep -v "^${CURRENT}$" | while read -r branch; do
|
|
||||||
gh pr list --repo "$REPO" --head "$branch" --state open --json number --jq '.[].number' 2>/dev/null | while read -r pr; do
|
|
||||||
gh pr close "$pr" --repo "$REPO" --comment "Superseded by \`${CURRENT}\`" 2>/dev/null || true
|
|
||||||
echo " Closed PR #${pr}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
done
|
|
||||||
gh api -X DELETE "repos/${REPO}/git/refs/heads/${branch}" --silent 2>/dev/null || true
|
|
||||||
echo " Deleted: \`${branch}\`" >> $GITHUB_STEP_SUMMARY
|
|
||||||
FOUND=true
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ "$FOUND" != "true" ]; then
|
|
||||||
echo "✅ No old sync branches found" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── WORKFLOW RUN CLEANUP ─────────────────────────────────────────────
|
|
||||||
- name: Clean up workflow runs
|
|
||||||
if: steps.tasks.outputs.clean_workflows == 'true'
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
|
|
||||||
run: |
|
|
||||||
REPO="${{ github.repository }}"
|
|
||||||
echo "## 🔄 Workflow Run Cleanup" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
DELETED=0
|
|
||||||
# Delete cancelled and stale workflow runs
|
|
||||||
for status in cancelled stale; do
|
|
||||||
gh api "repos/${REPO}/actions/runs?status=${status}&per_page=100" \
|
|
||||||
--jq '.workflow_runs[].id' 2>/dev/null | while read -r run_id; do
|
|
||||||
gh api -X DELETE "repos/${REPO}/actions/runs/${run_id}" --silent 2>/dev/null || true
|
|
||||||
DELETED=$((DELETED+1))
|
|
||||||
done
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "✅ Cleaned cancelled/stale workflow runs" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
# ── LOG CLEANUP ──────────────────────────────────────────────────────
|
|
||||||
- name: Delete old workflow run logs
|
|
||||||
if: steps.tasks.outputs.clean_logs == 'true'
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
|
|
||||||
run: |
|
|
||||||
REPO="${{ github.repository }}"
|
|
||||||
CUTOFF=$(date -u -d '30 days ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -v-30d +%Y-%m-%dT%H:%M:%SZ)
|
|
||||||
echo "## 📋 Log Cleanup" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "Deleting logs older than: ${CUTOFF}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
DELETED=0
|
|
||||||
gh api "repos/${REPO}/actions/runs?created=<${CUTOFF}&per_page=100" \
|
|
||||||
--jq '.workflow_runs[].id' 2>/dev/null | while read -r run_id; do
|
|
||||||
gh api -X DELETE "repos/${REPO}/actions/runs/${run_id}/logs" --silent 2>/dev/null || true
|
|
||||||
DELETED=$((DELETED+1))
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "✅ Cleaned old workflow run logs" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
# ── ISSUE TEMPLATE FIX ──────────────────────────────────────────────
|
|
||||||
- name: Strip copyright headers from issue templates
|
|
||||||
if: steps.tasks.outputs.fix_templates == 'true'
|
|
||||||
run: |
|
|
||||||
echo "## 📋 Issue Template Cleanup" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
FIXED=0
|
|
||||||
for f in .github/ISSUE_TEMPLATE/*.md; do
|
|
||||||
[ -f "$f" ] || continue
|
|
||||||
if grep -q '^<!--$' "$f"; then
|
|
||||||
sed -i '/^<!--$/,/^-->$/d' "$f"
|
|
||||||
echo " Cleaned: \`$(basename $f)\`" >> $GITHUB_STEP_SUMMARY
|
|
||||||
FIXED=$((FIXED+1))
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ "$FIXED" -gt 0 ]; then
|
|
||||||
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
|
||||||
git config --local user.name "github-actions[bot]"
|
|
||||||
git add .github/ISSUE_TEMPLATE/
|
|
||||||
git commit -m "fix: strip copyright comment blocks from issue templates [skip ci]" \
|
|
||||||
--author="github-actions[bot] <github-actions[bot]@users.noreply.github.com>"
|
|
||||||
git push
|
|
||||||
echo "✅ ${FIXED} template(s) cleaned and committed" >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "✅ No templates need cleaning" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── REBUILD DOC INDEXES ─────────────────────────────────────────────
|
|
||||||
- name: Rebuild docs/ index files
|
|
||||||
if: steps.tasks.outputs.rebuild_indexes == 'true'
|
|
||||||
run: |
|
|
||||||
echo "## 📚 Documentation Index Rebuild" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
if [ ! -d "docs" ]; then
|
|
||||||
echo "⏭️ No docs/ directory — skipping" >> $GITHUB_STEP_SUMMARY
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
UPDATED=0
|
|
||||||
# Generate index.md for each docs/ subdirectory
|
|
||||||
find docs -type d | while read -r dir; do
|
|
||||||
INDEX="${dir}/index.md"
|
|
||||||
FILES=$(find "$dir" -maxdepth 1 -name "*.md" ! -name "index.md" -printf "- [%f](./%f)\n" 2>/dev/null | sort)
|
|
||||||
if [ -z "$FILES" ]; then
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
cat > "$INDEX" << INDEXEOF
|
|
||||||
# $(basename "$dir")
|
|
||||||
|
|
||||||
## Documents
|
|
||||||
|
|
||||||
${FILES}
|
|
||||||
|
|
||||||
---
|
|
||||||
*Auto-generated by repository-cleanup workflow*
|
|
||||||
INDEXEOF
|
|
||||||
# Dedent
|
|
||||||
sed -i 's/^ //' "$INDEX"
|
|
||||||
UPDATED=$((UPDATED+1))
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ "$UPDATED" -gt 0 ]; then
|
|
||||||
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
|
||||||
git config --local user.name "github-actions[bot]"
|
|
||||||
git add docs/
|
|
||||||
if ! git diff --cached --quiet; then
|
|
||||||
git commit -m "docs: rebuild documentation indexes [skip ci]" \
|
|
||||||
--author="github-actions[bot] <github-actions[bot]@users.noreply.github.com>"
|
|
||||||
git push
|
|
||||||
echo "✅ ${UPDATED} index file(s) rebuilt and committed" >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "✅ All indexes already up to date" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "✅ No indexes to rebuild" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── VERSION DRIFT DETECTION ──────────────────────────────────────────
|
|
||||||
- name: Check for version drift
|
|
||||||
run: |
|
|
||||||
echo "## 📦 Version Drift Check" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
if [ ! -f "README.md" ]; then
|
|
||||||
echo "⏭️ No README.md — skipping" >> $GITHUB_STEP_SUMMARY
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
README_VERSION=$(grep -oP '^\s*VERSION:\s*\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' README.md 2>/dev/null | head -1)
|
|
||||||
if [ -z "$README_VERSION" ]; then
|
|
||||||
echo "⚠️ No VERSION found in README.md FILE INFORMATION block" >> $GITHUB_STEP_SUMMARY
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "**README version:** \`${README_VERSION}\`" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
DRIFT=0
|
|
||||||
CHECKED=0
|
|
||||||
|
|
||||||
# Check all files with FILE INFORMATION blocks
|
|
||||||
while IFS= read -r -d '' file; do
|
|
||||||
FILE_VERSION=$(grep -oP '^\s*\*?\s*VERSION:\s*\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' "$file" 2>/dev/null | head -1)
|
|
||||||
[ -z "$FILE_VERSION" ] && continue
|
|
||||||
CHECKED=$((CHECKED+1))
|
|
||||||
if [ "$FILE_VERSION" != "$README_VERSION" ]; then
|
|
||||||
echo " ⚠️ \`${file}\`: \`${FILE_VERSION}\` (expected \`${README_VERSION}\`)" >> $GITHUB_STEP_SUMMARY
|
|
||||||
DRIFT=$((DRIFT+1))
|
|
||||||
fi
|
|
||||||
done < <(find . -maxdepth 4 -type f \( -name "*.php" -o -name "*.md" -o -name "*.yml" \) ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" -print0 2>/dev/null)
|
|
||||||
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
if [ "$DRIFT" -gt 0 ]; then
|
|
||||||
echo "⚠️ **${DRIFT}** file(s) out of ${CHECKED} have version drift" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "Run \`sync-version-on-merge\` workflow or update manually" >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "✅ All ${CHECKED} file(s) match README version \`${README_VERSION}\`" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── PROTECT CUSTOM WORKFLOWS ────────────────────────────────────────
|
|
||||||
- name: Ensure custom workflow directory exists
|
|
||||||
run: |
|
|
||||||
echo "## 🔧 Custom Workflows" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
if [ ! -d ".github/workflows/custom" ]; then
|
|
||||||
mkdir -p .github/workflows/custom
|
|
||||||
cat > .github/workflows/custom/README.md << 'CWEOF'
|
|
||||||
# Custom Workflows
|
|
||||||
|
|
||||||
Place repo-specific workflows here. Files in this directory are:
|
|
||||||
- **Never overwritten** by MokoStandards bulk sync
|
|
||||||
- **Never deleted** by the repository-cleanup workflow
|
|
||||||
- Safe for custom CI, notifications, or repo-specific automation
|
|
||||||
|
|
||||||
Synced workflows live in `.github/workflows/` (parent directory).
|
|
||||||
CWEOF
|
|
||||||
sed -i 's/^ //' .github/workflows/custom/README.md
|
|
||||||
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
|
||||||
git config --local user.name "github-actions[bot]"
|
|
||||||
git add .github/workflows/custom/
|
|
||||||
if ! git diff --cached --quiet; then
|
|
||||||
git commit -m "chore: create .github/workflows/custom/ for repo-specific workflows [skip ci]" \
|
|
||||||
--author="github-actions[bot] <github-actions[bot]@users.noreply.github.com>"
|
|
||||||
git push
|
|
||||||
echo "✅ Created \`.github/workflows/custom/\` directory" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
CUSTOM_COUNT=$(find .github/workflows/custom -name "*.yml" -o -name "*.yaml" 2>/dev/null | wc -l)
|
|
||||||
echo "✅ Custom workflow directory exists (${CUSTOM_COUNT} workflow(s))" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── DELETE CLOSED ISSUES ──────────────────────────────────────────────
|
|
||||||
- name: Delete old closed issues
|
|
||||||
if: steps.tasks.outputs.delete_closed_issues == 'true'
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
|
|
||||||
run: |
|
|
||||||
REPO="${{ github.repository }}"
|
|
||||||
CUTOFF=$(date -u -d '30 days ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -v-30d +%Y-%m-%dT%H:%M:%SZ)
|
|
||||||
echo "## 🗑️ Closed Issue Cleanup" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "Deleting issues closed before: ${CUTOFF}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
DELETED=0
|
|
||||||
gh api "repos/${REPO}/issues?state=closed&since=1970-01-01T00:00:00Z&per_page=100&sort=updated&direction=asc" \
|
|
||||||
--jq ".[] | select(.closed_at < \"${CUTOFF}\") | .number" 2>/dev/null | while read -r num; do
|
|
||||||
# Lock and close with "not_planned" to mark as cleaned up
|
|
||||||
gh api "repos/${REPO}/issues/${num}/lock" -X PUT -f lock_reason="resolved" --silent 2>/dev/null || true
|
|
||||||
echo " Locked issue #${num}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
DELETED=$((DELETED+1))
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ "$DELETED" -eq 0 ] 2>/dev/null; then
|
|
||||||
echo "✅ No old closed issues found" >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "✅ Locked ${DELETED} old closed issue(s)" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Summary
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "---" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "*Run by @${{ github.actor }} — trigger: ${{ github.event_name }}*" >> $GITHUB_STEP_SUMMARY
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,135 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# This file is part of a Moko Consulting project.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: GitHub.Workflow
|
|
||||||
# INGROUP: MokoStandards.Automation
|
|
||||||
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
|
|
||||||
# PATH: /templates/workflows/shared/sync-version-on-merge.yml.template
|
|
||||||
# VERSION: 04.06.00
|
|
||||||
# BRIEF: Auto-bump patch version on every push to main and propagate to all file headers
|
|
||||||
# NOTE: Synced via bulk-repo-sync to .github/workflows/sync-version-on-merge.yml in all governed repos.
|
|
||||||
# README.md is the single source of truth for the repository version.
|
|
||||||
|
|
||||||
name: Sync Version from README
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [closed]
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
dry_run:
|
|
||||||
description: 'Dry run (preview only, no commit)'
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
issues: write
|
|
||||||
|
|
||||||
env:
|
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
sync-version:
|
|
||||||
name: Propagate README version
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: >-
|
|
||||||
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch'
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GH_TOKEN || github.token }}
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Set up PHP
|
|
||||||
uses: shivammathur/setup-php@fcafdd6392932010c2bd5094439b8e33be2a8a09 # v2.37.0
|
|
||||||
with:
|
|
||||||
php-version: '8.1'
|
|
||||||
tools: composer
|
|
||||||
|
|
||||||
- name: Setup MokoStandards tools
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
|
|
||||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}'
|
|
||||||
run: |
|
|
||||||
git clone --depth 1 --branch version/04 --quiet \
|
|
||||||
"https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \
|
|
||||||
/tmp/mokostandards
|
|
||||||
cd /tmp/mokostandards
|
|
||||||
composer install --no-dev --no-interaction --quiet
|
|
||||||
|
|
||||||
- name: Auto-bump patch version
|
|
||||||
if: ${{ github.event_name != 'workflow_dispatch' && github.actor != 'github-actions[bot]' }}
|
|
||||||
run: |
|
|
||||||
if git diff --name-only HEAD~1 HEAD 2>/dev/null | grep -q '^README\.md$'; then
|
|
||||||
echo "README.md changed in this push — skipping auto-bump"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
RESULT=$(php /tmp/mokostandards/api/cli/version_bump.php --path .) || {
|
|
||||||
echo "⚠️ Could not bump version — skipping"
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
echo "Auto-bumping patch: $RESULT"
|
|
||||||
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
|
||||||
git config --local user.name "github-actions[bot]"
|
|
||||||
git add README.md
|
|
||||||
git commit -m "chore(version): auto-bump patch ${RESULT} [skip ci]" \
|
|
||||||
--author="github-actions[bot] <github-actions[bot]@users.noreply.github.com>"
|
|
||||||
git push
|
|
||||||
|
|
||||||
- name: Extract version from README.md
|
|
||||||
id: readme_version
|
|
||||||
run: |
|
|
||||||
git pull --ff-only 2>/dev/null || true
|
|
||||||
VERSION=$(php /tmp/mokostandards/api/cli/version_read.php --path . 2>/dev/null)
|
|
||||||
if [ -z "$VERSION" ]; then
|
|
||||||
echo "⚠️ No VERSION in README.md — skipping propagation"
|
|
||||||
echo "skip=true" >> $GITHUB_OUTPUT
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
|
||||||
echo "skip=false" >> $GITHUB_OUTPUT
|
|
||||||
echo "✅ README.md version: $VERSION"
|
|
||||||
|
|
||||||
- name: Run version sync
|
|
||||||
if: ${{ steps.readme_version.outputs.skip != 'true' && inputs.dry_run != true }}
|
|
||||||
run: |
|
|
||||||
php /tmp/mokostandards/api/maintenance/update_version_from_readme.php \
|
|
||||||
--path . \
|
|
||||||
--create-issue \
|
|
||||||
--repo "${{ github.repository }}"
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
|
|
||||||
|
|
||||||
- name: Commit updated files
|
|
||||||
if: ${{ steps.readme_version.outputs.skip != 'true' && inputs.dry_run != true }}
|
|
||||||
run: |
|
|
||||||
git pull --ff-only 2>/dev/null || true
|
|
||||||
if git diff --quiet; then
|
|
||||||
echo "ℹ️ No version changes needed — already up to date"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
VERSION="${{ steps.readme_version.outputs.version }}"
|
|
||||||
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
|
||||||
git config --local user.name "github-actions[bot]"
|
|
||||||
git add -A
|
|
||||||
git commit -m "chore(version): sync badges and headers to ${VERSION} [skip ci]" \
|
|
||||||
--author="github-actions[bot] <github-actions[bot]@users.noreply.github.com>"
|
|
||||||
git push
|
|
||||||
|
|
||||||
- name: Summary
|
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.readme_version.outputs.version }}"
|
|
||||||
echo "## 📦 Version Sync — ${VERSION}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "**Source:** \`README.md\` FILE INFORMATION block" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "**Version:** \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
name: Update MokoCassiopeia Payload
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
update-payload:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Get latest MokoCassiopeia release URL
|
|
||||||
id: moko
|
|
||||||
run: |
|
|
||||||
DOWNLOAD_URL=$(curl -s https://api.github.com/repos/mokoconsulting-tech/MokoCassiopeia/releases \
|
|
||||||
| jq -r '[.[] | select(.prerelease == false and .draft == false and (.assets | length > 0)][0].assets[0].browser_download_url')
|
|
||||||
echo "url=$DOWNLOAD_URL" >> $GITHUB_OUTPUT
|
|
||||||
echo "Found: $DOWNLOAD_URL"
|
|
||||||
|
|
||||||
- name: Download MokoCassiopeia zip
|
|
||||||
if: steps.moko.outputs.url != 'null'
|
|
||||||
run: |
|
|
||||||
mkdir -p src/payload
|
|
||||||
curl -sL "${{ steps.moko.outputs.url }}" -o src/payload/mokocassiopeia.zip
|
|
||||||
ls -la src/payload/
|
|
||||||
|
|
||||||
- name: Check if payload changed
|
|
||||||
id: diff
|
|
||||||
run: |
|
|
||||||
git add src/payload/mokocassiopeia.zip
|
|
||||||
if git diff --cached --quiet; then
|
|
||||||
echo "changed=false" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "changed=true" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Commit updated payload
|
|
||||||
if: steps.diff.outputs.changed == 'true'
|
|
||||||
run: |
|
|
||||||
git config user.name "github-actions[bot]"
|
|
||||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
||||||
git commit -m "chore: update mokocassiopeia payload"
|
|
||||||
git push
|
|
||||||
@@ -1,346 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: GitHub.Workflow
|
|
||||||
# INGROUP: MokoStandards.Joomla
|
|
||||||
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
|
|
||||||
# PATH: /templates/workflows/joomla/update-server.yml.template
|
|
||||||
# VERSION: 04.06.00
|
|
||||||
# BRIEF: Update Joomla update server XML feed with stable/rc/dev entries
|
|
||||||
#
|
|
||||||
# Writes updates.xml with multiple <update> entries:
|
|
||||||
# - <tag>stable</tag> on push to main (from auto-release)
|
|
||||||
# - <tag>rc</tag> on push to rc/**
|
|
||||||
# - <tag>development</tag> on push to dev/**
|
|
||||||
#
|
|
||||||
# Joomla filters by user's "Minimum Stability" setting.
|
|
||||||
|
|
||||||
name: Update Joomla Update Server XML Feed
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [closed]
|
|
||||||
branches:
|
|
||||||
- 'dev/**'
|
|
||||||
- 'alpha/**'
|
|
||||||
- 'beta/**'
|
|
||||||
- 'rc/**'
|
|
||||||
paths:
|
|
||||||
- 'src/**'
|
|
||||||
- 'htdocs/**'
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
stability:
|
|
||||||
description: 'Stability tag'
|
|
||||||
required: true
|
|
||||||
default: 'development'
|
|
||||||
type: choice
|
|
||||||
options:
|
|
||||||
- development
|
|
||||||
- alpha
|
|
||||||
- beta
|
|
||||||
- rc
|
|
||||||
- stable
|
|
||||||
|
|
||||||
env:
|
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
update-xml:
|
|
||||||
name: Update updates.xml
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: >-
|
|
||||||
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch'
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GH_TOKEN || github.token }}
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Setup MokoStandards tools
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
|
|
||||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}'
|
|
||||||
run: |
|
|
||||||
git clone --depth 1 --branch version/04 --quiet \
|
|
||||||
"https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \
|
|
||||||
/tmp/mokostandards 2>/dev/null || true
|
|
||||||
if [ -d "/tmp/mokostandards" ] && [ -f "/tmp/mokostandards/composer.json" ]; then
|
|
||||||
cd /tmp/mokostandards && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Generate updates.xml entry
|
|
||||||
run: |
|
|
||||||
BRANCH="${{ github.ref_name }}"
|
|
||||||
REPO="${{ github.repository }}"
|
|
||||||
VERSION=$(php /tmp/mokostandards/api/cli/version_read.php --path . 2>/dev/null || echo "0.0.0")
|
|
||||||
|
|
||||||
# Auto-bump patch on alpha/beta/rc branches (not dev — dev bumps manually)
|
|
||||||
if [[ "$BRANCH" != dev/* ]]; then
|
|
||||||
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
|
||||||
git config --local user.name "github-actions[bot]"
|
|
||||||
BUMPED=$(php /tmp/mokostandards/api/cli/version_bump.php --path . 2>/dev/null || true)
|
|
||||||
if [ -n "$BUMPED" ]; then
|
|
||||||
VERSION=$(php /tmp/mokostandards/api/cli/version_read.php --path . 2>/dev/null || echo "$VERSION")
|
|
||||||
git add -A
|
|
||||||
git commit -m "chore(version): auto-bump patch ${VERSION} [skip ci]" \
|
|
||||||
--author="github-actions[bot] <github-actions[bot]@users.noreply.github.com>" 2>/dev/null || true
|
|
||||||
git push 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Determine stability from branch or input
|
|
||||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
|
||||||
STABILITY="${{ inputs.stability }}"
|
|
||||||
elif [[ "$BRANCH" == rc/* ]]; then
|
|
||||||
STABILITY="rc"
|
|
||||||
elif [[ "$BRANCH" == beta/* ]]; then
|
|
||||||
STABILITY="beta"
|
|
||||||
elif [[ "$BRANCH" == alpha/* ]]; then
|
|
||||||
STABILITY="alpha"
|
|
||||||
elif [[ "$BRANCH" == dev/* ]]; then
|
|
||||||
STABILITY="development"
|
|
||||||
else
|
|
||||||
STABILITY="stable"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Parse manifest (portable — no grep -P)
|
|
||||||
MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
|
||||||
if [ -z "$MANIFEST" ]; then
|
|
||||||
echo "No Joomla manifest found — skipping"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Extract fields using sed (works on all runners)
|
|
||||||
EXT_NAME=$(sed -n 's/.*<name>\([^<]*\)<\/name>.*/\1/p' "$MANIFEST" | head -1)
|
|
||||||
EXT_TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
|
|
||||||
EXT_ELEMENT=$(sed -n 's/.*<element>\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" | head -1)
|
|
||||||
EXT_CLIENT=$(sed -n 's/.*<extension[^>]*client="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
|
|
||||||
EXT_FOLDER=$(sed -n 's/.*<extension[^>]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
|
|
||||||
EXT_VERSION=$(sed -n 's/.*<version>\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1)
|
|
||||||
TARGET_PLATFORM=$(sed -n 's/.*\(<targetplatform[^/]*\/>\).*/\1/p' "$MANIFEST" | head -1)
|
|
||||||
PHP_MINIMUM=$(sed -n 's/.*<php_minimum>\([^<]*\)<\/php_minimum>.*/\1/p' "$MANIFEST" | head -1)
|
|
||||||
|
|
||||||
# Fallbacks
|
|
||||||
[ -z "$EXT_NAME" ] && EXT_NAME="${{ github.event.repository.name }}"
|
|
||||||
[ -z "$EXT_TYPE" ] && EXT_TYPE="component"
|
|
||||||
|
|
||||||
# Templates and modules don't have <element> — derive from <name>
|
|
||||||
if [ -z "$EXT_ELEMENT" ]; then
|
|
||||||
EXT_ELEMENT=$(echo "$EXT_NAME" | tr '[:upper:]' '[:lower:]' | tr -d ' ')
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Use manifest version if README version is empty
|
|
||||||
[ "$VERSION" = "0.0.0" ] && [ -n "$EXT_VERSION" ] && VERSION="$EXT_VERSION"
|
|
||||||
|
|
||||||
[ -z "$TARGET_PLATFORM" ] && TARGET_PLATFORM=$(printf '<targetplatform name="joomla" version="5.*" %s>' "/")
|
|
||||||
|
|
||||||
CLIENT_TAG=""
|
|
||||||
[ -n "$EXT_CLIENT" ] && CLIENT_TAG="<client>${EXT_CLIENT}</client>"
|
|
||||||
[ -z "$CLIENT_TAG" ] && ([ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]) && CLIENT_TAG="<client>site</client>"
|
|
||||||
|
|
||||||
FOLDER_TAG=""
|
|
||||||
[ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ] && FOLDER_TAG="<folder>${EXT_FOLDER}</folder>"
|
|
||||||
|
|
||||||
PHP_TAG=""
|
|
||||||
[ -n "$PHP_MINIMUM" ] && PHP_TAG="<php_minimum>${PHP_MINIMUM}</php_minimum>"
|
|
||||||
|
|
||||||
# Version suffix for non-stable
|
|
||||||
DISPLAY_VERSION="$VERSION"
|
|
||||||
case "$STABILITY" in
|
|
||||||
development) DISPLAY_VERSION="${VERSION}-dev" ;;
|
|
||||||
alpha) DISPLAY_VERSION="${VERSION}-alpha" ;;
|
|
||||||
beta) DISPLAY_VERSION="${VERSION}-beta" ;;
|
|
||||||
rc) DISPLAY_VERSION="${VERSION}-rc" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
MAJOR=$(echo "$VERSION" | awk -F. '{print $1}')
|
|
||||||
|
|
||||||
# Each stability level has its own release tag
|
|
||||||
case "$STABILITY" in
|
|
||||||
development) RELEASE_TAG="development" ;;
|
|
||||||
alpha) RELEASE_TAG="alpha" ;;
|
|
||||||
beta) RELEASE_TAG="beta" ;;
|
|
||||||
rc) RELEASE_TAG="release-candidate" ;;
|
|
||||||
*) RELEASE_TAG="v${MAJOR}" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
PACKAGE_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.zip"
|
|
||||||
DOWNLOAD_URL="https://github.com/${REPO}/releases/download/${RELEASE_TAG}/${PACKAGE_NAME}"
|
|
||||||
INFO_URL="https://github.com/${REPO}"
|
|
||||||
|
|
||||||
# ── Build install packages (ZIP + tar.gz) ───────────────────
|
|
||||||
SOURCE_DIR="src"
|
|
||||||
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
|
||||||
if [ -d "$SOURCE_DIR" ]; then
|
|
||||||
EXCLUDES=".ftpignore sftp-config* *.ppk *.pem *.key .env*"
|
|
||||||
TAR_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.tar.gz"
|
|
||||||
|
|
||||||
cd "$SOURCE_DIR"
|
|
||||||
zip -r "/tmp/${PACKAGE_NAME}" . -x $EXCLUDES
|
|
||||||
cd ..
|
|
||||||
tar -czf "/tmp/${TAR_NAME}" -C "$SOURCE_DIR" \
|
|
||||||
--exclude='.ftpignore' --exclude='sftp-config*' \
|
|
||||||
--exclude='*.ppk' --exclude='*.pem' --exclude='*.key' --exclude='.env*' .
|
|
||||||
|
|
||||||
SHA256=$(sha256sum "/tmp/${PACKAGE_NAME}" | cut -d' ' -f1)
|
|
||||||
|
|
||||||
# Ensure release exists
|
|
||||||
gh release view "$RELEASE_TAG" --json tagName > /dev/null 2>&1 || \
|
|
||||||
gh release create "$RELEASE_TAG" --title "${RELEASE_TAG} (${DISPLAY_VERSION})" --notes "${STABILITY} release" --prerelease --target main 2>/dev/null || true
|
|
||||||
|
|
||||||
# Upload both formats
|
|
||||||
gh release upload "$RELEASE_TAG" "/tmp/${PACKAGE_NAME}" --clobber 2>/dev/null || true
|
|
||||||
gh release upload "$RELEASE_TAG" "/tmp/${TAR_NAME}" --clobber 2>/dev/null || true
|
|
||||||
|
|
||||||
echo "Packages: ${PACKAGE_NAME} + ${TAR_NAME} (SHA: ${SHA256})" >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
SHA256=""
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Build the new entry ───────────────────────────────────────
|
|
||||||
NEW_ENTRY=""
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <update>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <name>${EXT_NAME}</name>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <description>${EXT_NAME} (${STABILITY})</description>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <element>${EXT_ELEMENT}</element>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <type>${EXT_TYPE}</type>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <version>${DISPLAY_VERSION}</version>\n"
|
|
||||||
[ -n "$CLIENT_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${CLIENT_TAG}\n"
|
|
||||||
[ -n "$FOLDER_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${FOLDER_TAG}\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <tags>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <tag>${STABILITY}</tag>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} </tags>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <infourl title=\"${EXT_NAME}\">${INFO_URL}</infourl>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <downloads>\n"
|
|
||||||
TAR_URL="https://github.com/${REPO}/releases/download/${RELEASE_TAG}/${EXT_ELEMENT}-${DISPLAY_VERSION}.tar.gz"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <downloadurl type=\"full\" format=\"zip\">${DOWNLOAD_URL}</downloadurl>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <downloadurl type=\"full\" format=\"tar.gz\">${TAR_URL}</downloadurl>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} </downloads>\n"
|
|
||||||
[ -n "$SHA256" ] && NEW_ENTRY="${NEW_ENTRY} <sha256>sha256:${SHA256}</sha256>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} ${TARGET_PLATFORM}\n"
|
|
||||||
[ -n "$PHP_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${PHP_TAG}\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <maintainer>Moko Consulting</maintainer>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <maintainerurl>https://mokoconsulting.tech</maintainerurl>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} </update>"
|
|
||||||
|
|
||||||
# ── Write new entry to temp file ───────────────────────────────
|
|
||||||
printf '%b' "$NEW_ENTRY" > /tmp/new_entry.xml
|
|
||||||
|
|
||||||
# ── Merge into updates.xml ─────────────────────────────────────
|
|
||||||
if [ ! -f "updates.xml" ]; then
|
|
||||||
printf '%s\n' '<?xml version="1.0" encoding="utf-8"?>' > updates.xml
|
|
||||||
printf '%s\n' '<updates>' >> updates.xml
|
|
||||||
cat /tmp/new_entry.xml >> updates.xml
|
|
||||||
printf '\n%s\n' '</updates>' >> updates.xml
|
|
||||||
else
|
|
||||||
# Remove existing entry for this stability, insert new one
|
|
||||||
printf 'import re\nstability = "%s"\n' "${STABILITY}" > /tmp/merge_xml.py
|
|
||||||
printf 'with open("updates.xml") as f: content = f.read()\n' >> /tmp/merge_xml.py
|
|
||||||
printf 'with open("/tmp/new_entry.xml") as f: new_entry = f.read()\n' >> /tmp/merge_xml.py
|
|
||||||
printf 'pattern = r" <update>.*?<tag>" + re.escape(stability) + r"</tag>.*?</update>\\n?"\n' >> /tmp/merge_xml.py
|
|
||||||
printf 'content = re.sub(pattern, "", content, flags=re.DOTALL)\n' >> /tmp/merge_xml.py
|
|
||||||
printf 'content = content.replace("</updates>", new_entry + "\\n</updates>")\n' >> /tmp/merge_xml.py
|
|
||||||
printf 'content = re.sub(r"\\n{3,}", "\\n\\n", content)\n' >> /tmp/merge_xml.py
|
|
||||||
printf 'with open("updates.xml", "w") as f: f.write(content)\n' >> /tmp/merge_xml.py
|
|
||||||
python3 /tmp/merge_xml.py 2>/dev/null || {
|
|
||||||
# Fallback: rebuild keeping other stability entries
|
|
||||||
{
|
|
||||||
printf '%s\n' '<?xml version="1.0" encoding="utf-8"?>'
|
|
||||||
printf '%s\n' '<updates>'
|
|
||||||
for TAG in stable rc development; do
|
|
||||||
[ "$TAG" = "${STABILITY}" ] && continue
|
|
||||||
if grep -q "<tag>${TAG}</tag>" updates.xml 2>/dev/null; then
|
|
||||||
sed -n "/<update>/,/<\/update>/{ /<tag>${TAG}<\/tag>/p; }" updates.xml
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
cat /tmp/new_entry.xml
|
|
||||||
printf '\n%s\n' '</updates>'
|
|
||||||
} > /tmp/updates_new.xml
|
|
||||||
mv /tmp/updates_new.xml updates.xml
|
|
||||||
}
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Commit
|
|
||||||
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
|
||||||
git config --local user.name "github-actions[bot]"
|
|
||||||
git add updates.xml
|
|
||||||
git diff --cached --quiet || {
|
|
||||||
git commit -m "chore: update updates.xml (${STABILITY}: ${DISPLAY_VERSION}) [skip ci]" \
|
|
||||||
--author="github-actions[bot] <github-actions[bot]@users.noreply.github.com>"
|
|
||||||
git push
|
|
||||||
}
|
|
||||||
|
|
||||||
- name: SFTP deploy to dev server
|
|
||||||
if: contains(github.ref, 'dev/')
|
|
||||||
env:
|
|
||||||
DEV_HOST: ${{ vars.DEV_FTP_HOST }}
|
|
||||||
DEV_PATH: ${{ vars.DEV_FTP_PATH }}
|
|
||||||
DEV_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
|
|
||||||
DEV_USER: ${{ vars.DEV_FTP_USERNAME }}
|
|
||||||
DEV_PORT: ${{ vars.DEV_FTP_PORT }}
|
|
||||||
DEV_KEY: ${{ secrets.DEV_FTP_KEY }}
|
|
||||||
DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
|
|
||||||
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
|
|
||||||
run: |
|
|
||||||
# ── Permission check: admin or maintain role required ──────
|
|
||||||
ACTOR="${{ github.actor }}"
|
|
||||||
REPO="${{ github.repository }}"
|
|
||||||
PERMISSION=$(gh api "repos/${REPO}/collaborators/${ACTOR}/permission" \
|
|
||||||
--jq '.permission' 2>/dev/null || \
|
|
||||||
gh api "repos/${REPO}/collaborators/${ACTOR}" \
|
|
||||||
--jq '.role' 2>/dev/null || echo "read")
|
|
||||||
case "$PERMISSION" in
|
|
||||||
admin|maintain|write) ;;
|
|
||||||
*)
|
|
||||||
echo "Deploy denied: ${ACTOR} has '${PERMISSION}' — requires admin, maintain, or write"
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
[ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured — skipping SFTP"; exit 0; }
|
|
||||||
|
|
||||||
SOURCE_DIR="src"
|
|
||||||
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
|
||||||
[ ! -d "$SOURCE_DIR" ] && exit 0
|
|
||||||
|
|
||||||
PORT="${DEV_PORT:-22}"
|
|
||||||
REMOTE="${DEV_PATH%/}"
|
|
||||||
[ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}"
|
|
||||||
|
|
||||||
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
|
|
||||||
"$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json
|
|
||||||
if [ -n "$DEV_KEY" ]; then
|
|
||||||
echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key
|
|
||||||
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
|
|
||||||
else
|
|
||||||
printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json
|
|
||||||
fi
|
|
||||||
|
|
||||||
PLATFORM=$(php /tmp/mokostandards/api/cli/platform_detect.php --path . 2>/dev/null || true)
|
|
||||||
if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards/api/deploy/deploy-joomla.php" ]; then
|
|
||||||
php /tmp/mokostandards/api/deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
|
|
||||||
elif [ -f "/tmp/mokostandards/api/deploy/deploy-sftp.php" ]; then
|
|
||||||
php /tmp/mokostandards/api/deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
|
|
||||||
fi
|
|
||||||
rm -f /tmp/deploy_key /tmp/sftp-config.json
|
|
||||||
echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
- name: Summary
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
echo "## Joomla Update Server" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Version | \`${DISPLAY_VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Element | \`${EXT_ELEMENT}\` |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Download | [ZIP](${DOWNLOAD_URL}) |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
@@ -9,6 +9,8 @@ TODO.md
|
|||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
.env.*.local
|
.env.*.local
|
||||||
|
.joomla-api-mcp.json
|
||||||
|
.mcp.json
|
||||||
*.local.php
|
*.local.php
|
||||||
*.secret.php
|
*.secret.php
|
||||||
configuration.php
|
configuration.php
|
||||||
@@ -100,6 +102,7 @@ replit.md
|
|||||||
*.tar.gz
|
*.tar.gz
|
||||||
*.tgz
|
*.tgz
|
||||||
*.zip
|
*.zip
|
||||||
|
!src/payload/*.zip
|
||||||
artifacts/
|
artifacts/
|
||||||
release/
|
release/
|
||||||
releases/
|
releases/
|
||||||
@@ -200,3 +203,5 @@ venv/
|
|||||||
*.coverage
|
*.coverage
|
||||||
hypothesis/
|
hypothesis/
|
||||||
|
|
||||||
|
profile.ps1
|
||||||
|
TODO.md
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
blank_issues_enabled: true
|
||||||
|
contact_links:
|
||||||
|
- name: 💼 Enterprise Support
|
||||||
|
url: https://mokoconsulting.tech/enterprise
|
||||||
|
about: Enterprise-level support and consultation services
|
||||||
|
- name: 💬 Ask a Question
|
||||||
|
url: https://mokoconsulting.tech/
|
||||||
|
about: Get help or ask questions through our website
|
||||||
|
- name: 📚 MokoStandards Documentation
|
||||||
|
url: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
|
about: View our coding standards and best practices
|
||||||
|
- name: 🔒 Report a Security Vulnerability
|
||||||
|
url: https://git.mokoconsulting.tech/mokoconsulting-tech/.github-private/security/advisories/new
|
||||||
|
about: Report security vulnerabilities privately (for critical issues)
|
||||||
|
- name: 💡 Community Discussions
|
||||||
|
url: https://github.com/orgs/mokoconsulting-tech/discussions
|
||||||
|
about: Join community discussions and Q&A
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
name: Feature Request
|
||||||
|
about: Suggest a new feature or enhancement
|
||||||
|
title: '[FEATURE] '
|
||||||
|
labels: 'enhancement'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## Feature Description
|
||||||
|
A clear and concise description of the feature you'd like to see.
|
||||||
|
|
||||||
|
## Problem or Use Case
|
||||||
|
Describe the problem this feature would solve or the use case it addresses.
|
||||||
|
Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
## Proposed Solution
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
## Alternative Solutions
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
Describe how this feature would benefit users:
|
||||||
|
- Who would use this feature?
|
||||||
|
- What problems does it solve?
|
||||||
|
- What value does it add?
|
||||||
|
|
||||||
|
## Implementation Details (Optional)
|
||||||
|
If you have ideas about how this could be implemented, share them here:
|
||||||
|
- Technical approach
|
||||||
|
- Files/components that might need changes
|
||||||
|
- Any concerns or challenges you foresee
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
Add any other context, mockups, or screenshots about the feature request here.
|
||||||
|
|
||||||
|
## Relevant Standards
|
||||||
|
Does this relate to any standards in [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards)?
|
||||||
|
- [ ] Accessibility (WCAG 2.1 AA)
|
||||||
|
- [ ] Localization (en_US/en_GB)
|
||||||
|
- [ ] Security best practices
|
||||||
|
- [ ] Code quality standards
|
||||||
|
- [ ] Other: [specify]
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
- [ ] I have searched for similar feature requests before creating this one
|
||||||
|
- [ ] I have clearly described the use case and benefits
|
||||||
|
- [ ] I have considered alternative solutions
|
||||||
|
- [ ] This feature aligns with the project's goals and scope
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
---
|
||||||
|
name: Question
|
||||||
|
about: Ask a question about usage, features, or best practices
|
||||||
|
title: '[QUESTION] '
|
||||||
|
labels: ['question']
|
||||||
|
assignees: ['jmiller']
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## Question
|
||||||
|
|
||||||
|
**Your question:**
|
||||||
|
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
**What are you trying to accomplish?**
|
||||||
|
|
||||||
|
|
||||||
|
**What have you already tried?**
|
||||||
|
|
||||||
|
|
||||||
|
**Category**:
|
||||||
|
- [ ] Script usage
|
||||||
|
- [ ] Configuration
|
||||||
|
- [ ] Workflow setup
|
||||||
|
- [ ] Documentation interpretation
|
||||||
|
- [ ] Best practices
|
||||||
|
- [ ] Integration
|
||||||
|
- [ ] Other: __________
|
||||||
|
|
||||||
|
## Environment (if relevant)
|
||||||
|
|
||||||
|
**Your setup**:
|
||||||
|
- Operating System:
|
||||||
|
- Version:
|
||||||
|
|
||||||
|
## What You've Researched
|
||||||
|
|
||||||
|
**Documentation reviewed**:
|
||||||
|
- [ ] README.md
|
||||||
|
- [ ] Project documentation
|
||||||
|
- [ ] Other (specify): __________
|
||||||
|
|
||||||
|
**Similar issues/questions found**:
|
||||||
|
- #
|
||||||
|
- #
|
||||||
|
|
||||||
|
## Expected Outcome
|
||||||
|
|
||||||
|
**What result are you hoping for?**
|
||||||
|
|
||||||
|
|
||||||
|
## Code/Configuration Samples
|
||||||
|
|
||||||
|
**Relevant code or configuration** (if applicable):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Your code here
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
|
||||||
|
**Any other relevant information:**
|
||||||
|
|
||||||
|
|
||||||
|
**Screenshots** (if helpful):
|
||||||
|
|
||||||
|
|
||||||
|
## Urgency
|
||||||
|
|
||||||
|
- [ ] Urgent (blocking work)
|
||||||
|
- [ ] Normal (can work on other things meanwhile)
|
||||||
|
- [ ] Low priority (just curious)
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
- [ ] I have searched existing issues and discussions
|
||||||
|
- [ ] I have reviewed relevant documentation
|
||||||
|
- [ ] I have provided sufficient context
|
||||||
|
- [ ] I have included code/configuration samples if relevant
|
||||||
|
- [ ] This is a genuine question (not a bug report or feature request)
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
name: Security Vulnerability Report
|
||||||
|
about: Report a security vulnerability (use only for non-critical issues)
|
||||||
|
title: '[SECURITY] '
|
||||||
|
labels: 'security'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## ⚠️ IMPORTANT: Private Disclosure Required
|
||||||
|
|
||||||
|
**For critical security vulnerabilities, DO NOT use this template.**
|
||||||
|
Follow the process in [SECURITY.md](../SECURITY.md) for responsible disclosure.
|
||||||
|
|
||||||
|
Use this template only for:
|
||||||
|
- Security improvements
|
||||||
|
- Non-critical security suggestions
|
||||||
|
- Security documentation updates
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security Issue
|
||||||
|
|
||||||
|
**Severity**:
|
||||||
|
<!-- Low, Medium, or informational only -->
|
||||||
|
|
||||||
|
## Description
|
||||||
|
<!-- Describe the security concern or improvement suggestion -->
|
||||||
|
|
||||||
|
## Affected Components
|
||||||
|
<!-- List the affected files, features, or components -->
|
||||||
|
|
||||||
|
## Suggested Mitigation
|
||||||
|
<!-- Describe how this could be addressed -->
|
||||||
|
|
||||||
|
## Standards Reference
|
||||||
|
Does this relate to security standards in [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards)?
|
||||||
|
- [ ] SPDX license identifiers
|
||||||
|
- [ ] Secret management
|
||||||
|
- [ ] Dependency security
|
||||||
|
- [ ] Access control
|
||||||
|
- [ ] Other: [specify]
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
<!-- Add any other context about the security concern -->
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
- [ ] This is NOT a critical vulnerability requiring private disclosure
|
||||||
|
- [ ] I have reviewed the SECURITY.md policy
|
||||||
|
- [ ] I have provided sufficient detail for evaluation
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
name: Version Bump
|
||||||
|
about: Request or track a version change
|
||||||
|
title: '[VERSION] '
|
||||||
|
labels: 'version, type: version'
|
||||||
|
assignees: 'jmiller'
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version Change
|
||||||
|
|
||||||
|
**Current version**: <!-- e.g., 01.02.03 -->
|
||||||
|
**Requested version**: <!-- e.g., 01.03.00 -->
|
||||||
|
**Change type**: <!-- patch / minor / major -->
|
||||||
|
|
||||||
|
## Reason
|
||||||
|
|
||||||
|
<!-- Why is this version bump needed? -->
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
- [ ] README.md `VERSION:` field updated
|
||||||
|
- [ ] CHANGELOG.md entry added
|
||||||
|
- [ ] Module descriptor version updated (Dolibarr: `$this->version`, Joomla: `<version>`)
|
||||||
|
- [ ] All file headers will be auto-propagated by `sync-version-on-merge` workflow
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
---
|
||||||
|
name: WaaS Client Site Issue
|
||||||
|
about: Report an issue with a WaaS client site (branding, deployment, media sync)
|
||||||
|
title: '[WAAS] '
|
||||||
|
labels: 'waas, client-site'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Site Issue Type
|
||||||
|
- [ ] Branding / CSS not applying
|
||||||
|
- [ ] Deployment failure
|
||||||
|
- [ ] Media sync issue
|
||||||
|
- [ ] Template override not working
|
||||||
|
- [ ] Module positioning issue
|
||||||
|
- [ ] Mobile / responsive layout
|
||||||
|
- [ ] Performance issue
|
||||||
|
|
||||||
|
## Client Site
|
||||||
|
- **Client Org**: [e.g., ClarksvilleFurs]
|
||||||
|
- **Repo**: [e.g., client-waas-clarksvillefurs]
|
||||||
|
- **Environment**: [Dev / Production]
|
||||||
|
- **Site URL**: [dev or production URL — omit if private]
|
||||||
|
|
||||||
|
## Issue Description
|
||||||
|
Describe the issue clearly.
|
||||||
|
|
||||||
|
## Steps to Reproduce
|
||||||
|
1. Visit [page URL]
|
||||||
|
2. Look at [element]
|
||||||
|
3. See error
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
What the site should look like or how it should behave.
|
||||||
|
|
||||||
|
## Actual Behavior
|
||||||
|
What is happening instead.
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
Attach screenshots showing the issue (desktop and mobile if relevant).
|
||||||
|
|
||||||
|
## Deployment Status
|
||||||
|
- **Last deploy**: [date or "unknown"]
|
||||||
|
- **Deploy workflow**: [succeeded / failed / not run]
|
||||||
|
- **Branch**: [dev / main]
|
||||||
|
|
||||||
|
## Media Sync
|
||||||
|
- [ ] Images missing after sync
|
||||||
|
- [ ] Sync direction: [dev-to-prod / prod-to-dev / bidirectional]
|
||||||
|
- [ ] Last sync: [date]
|
||||||
|
|
||||||
|
## Template Details
|
||||||
|
- **Joomla Version**: [e.g., 5.x]
|
||||||
|
- **Template Name**: [e.g., clienttemplate]
|
||||||
|
- **MokoWaaS Plugin**: [Active / Inactive]
|
||||||
|
- **MokoOnyx Admin**: [Active / Inactive]
|
||||||
|
|
||||||
|
## CSS Custom Properties
|
||||||
|
If branding issue, list the relevant CSS variables:
|
||||||
|
```css
|
||||||
|
:root {
|
||||||
|
--client-primary: #...;
|
||||||
|
--client-secondary: #...;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Browser / Device
|
||||||
|
- **Browser**: [e.g., Chrome 120, Safari 17]
|
||||||
|
- **Device**: [Desktop / Tablet / Mobile]
|
||||||
|
- **Screen Width**: [e.g., 1920px, 768px, 375px]
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
- [ ] I have cleared Joomla cache
|
||||||
|
- [ ] I have hard-refreshed the browser (Ctrl+Shift+R)
|
||||||
|
- [ ] I have checked the deploy workflow completed
|
||||||
|
- [ ] I have verified the change is on the correct branch
|
||||||
|
- [ ] No credentials or PII are included in this issue
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
---
|
||||||
|
name: Architecture Decision Record (ADR)
|
||||||
|
about: Propose or document an architectural decision
|
||||||
|
title: '[ADR] '
|
||||||
|
labels: 'architecture, decision'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## ADR Number
|
||||||
|
ADR-XXXX
|
||||||
|
|
||||||
|
## Status
|
||||||
|
- [ ] Proposed
|
||||||
|
- [ ] Accepted
|
||||||
|
- [ ] Deprecated
|
||||||
|
- [ ] Superseded by ADR-XXXX
|
||||||
|
|
||||||
|
## Context
|
||||||
|
Describe the issue or problem that motivates this decision.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
State the architecture decision and provide rationale.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
### Positive
|
||||||
|
- List positive consequences
|
||||||
|
|
||||||
|
### Negative
|
||||||
|
- List negative consequences or trade-offs
|
||||||
|
|
||||||
|
### Neutral
|
||||||
|
- List neutral aspects
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
### Alternative 1
|
||||||
|
- Description
|
||||||
|
- Pros
|
||||||
|
- Cons
|
||||||
|
- Why not chosen
|
||||||
|
|
||||||
|
### Alternative 2
|
||||||
|
- Description
|
||||||
|
- Pros
|
||||||
|
- Cons
|
||||||
|
- Why not chosen
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
1. Step 1
|
||||||
|
2. Step 2
|
||||||
|
3. Step 3
|
||||||
|
|
||||||
|
## Stakeholders
|
||||||
|
- **Decision Makers**: @user1, @user2
|
||||||
|
- **Consulted**: @user3, @user4
|
||||||
|
- **Informed**: team-name
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
### Architecture Diagram
|
||||||
|
```
|
||||||
|
[Add diagram or link]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
- Dependency 1
|
||||||
|
- Dependency 2
|
||||||
|
|
||||||
|
### Impact Analysis
|
||||||
|
- **Performance**: [Impact description]
|
||||||
|
- **Security**: [Impact description]
|
||||||
|
- **Scalability**: [Impact description]
|
||||||
|
- **Maintainability**: [Impact description]
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
- [ ] Unit tests
|
||||||
|
- [ ] Integration tests
|
||||||
|
- [ ] Performance tests
|
||||||
|
- [ ] Security tests
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
- [ ] Architecture documentation updated
|
||||||
|
- [ ] API documentation updated
|
||||||
|
- [ ] Developer guide updated
|
||||||
|
- [ ] Runbook created
|
||||||
|
|
||||||
|
## Migration Path
|
||||||
|
Describe how to migrate from current state to new architecture.
|
||||||
|
|
||||||
|
## Rollback Plan
|
||||||
|
Describe how to rollback if issues occur.
|
||||||
|
|
||||||
|
## Timeline
|
||||||
|
- **Proposal Date**:
|
||||||
|
- **Decision Date**:
|
||||||
|
- **Implementation Start**:
|
||||||
|
- **Expected Completion**:
|
||||||
|
|
||||||
|
## References
|
||||||
|
- Related ADRs:
|
||||||
|
- External resources:
|
||||||
|
- RFCs:
|
||||||
|
|
||||||
|
## Review Checklist
|
||||||
|
- [ ] Aligns with enterprise architecture principles
|
||||||
|
- [ ] Security implications reviewed
|
||||||
|
- [ ] Performance implications reviewed
|
||||||
|
- [ ] Cost implications reviewed
|
||||||
|
- [ ] Compliance requirements met
|
||||||
|
- [ ] Team consensus achieved
|
||||||
@@ -0,0 +1,949 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: MokoStandards.Release
|
||||||
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
||||||
|
# PATH: /templates/workflows/joomla/auto-release.yml.template
|
||||||
|
# VERSION: 04.06.00
|
||||||
|
# BRIEF: Joomla build & release — ZIP package, updates.xml, SHA-256 checksum
|
||||||
|
#
|
||||||
|
# +========================================================================+
|
||||||
|
# | BUILD & RELEASE PIPELINE (JOOMLA) |
|
||||||
|
# +========================================================================+
|
||||||
|
# | |
|
||||||
|
# | Triggers on push to main (skips bot commits + [skip ci]): |
|
||||||
|
# | |
|
||||||
|
# | Every push: |
|
||||||
|
# | 1. Read version from README.md |
|
||||||
|
# | 3. Set platform version (Joomla <version>) |
|
||||||
|
# | 4. Update [VERSION: XX.YY.ZZ] badges in markdown files |
|
||||||
|
# | 5. Write updates.xml (Joomla update server XML) |
|
||||||
|
# | 6. Create git tag vXX.YY.ZZ |
|
||||||
|
# | 7a. Patch: update existing Gitea Release for this minor |
|
||||||
|
# | 8. Build ZIP, upload asset, write SHA-256 to updates.xml |
|
||||||
|
# | |
|
||||||
|
# | Every version change: archives main -> version/XX.YY branch |
|
||||||
|
# | All patches release (including 00). Patch 00/01 = full pipeline. |
|
||||||
|
# | First release only (patch == 01): |
|
||||||
|
# | 7b. Create new Gitea Release |
|
||||||
|
# | |
|
||||||
|
# | GitHub mirror: stable/rc releases only (continue-on-error) |
|
||||||
|
# | |
|
||||||
|
# +========================================================================+
|
||||||
|
|
||||||
|
name: Build & Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [closed]
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- 'src/**'
|
||||||
|
- 'htdocs/**'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||||||
|
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
name: Build & Release Pipeline
|
||||||
|
runs-on: release
|
||||||
|
if: >-
|
||||||
|
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GA_TOKEN }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup MokoStandards tools
|
||||||
|
env:
|
||||||
|
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||||
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
|
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN }}"}}'
|
||||||
|
run: |
|
||||||
|
# Ensure PHP + Composer are available
|
||||||
|
if ! command -v composer &> /dev/null; then
|
||||||
|
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
git clone --depth 1 --branch main --quiet \
|
||||||
|
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
|
||||||
|
/tmp/mokostandards-api
|
||||||
|
cd /tmp/mokostandards-api
|
||||||
|
composer install --no-dev --no-interaction --quiet
|
||||||
|
|
||||||
|
# -- STEP 1: Read version -----------------------------------------------
|
||||||
|
- name: "Step 1: Read version from README.md"
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
VERSION=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null)
|
||||||
|
if [ -z "$VERSION" ]; then
|
||||||
|
echo "No VERSION in README.md — skipping release"
|
||||||
|
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
# Derive major.minor for branch naming (patches update existing branch)
|
||||||
|
MINOR=$(echo "$VERSION" | awk -F. '{printf "%s.%s", $1, $2}')
|
||||||
|
PATCH=$(echo "$VERSION" | awk -F. '{print $3}')
|
||||||
|
|
||||||
|
MAJOR=$(echo "$VERSION" | awk -F. '{print $1}')
|
||||||
|
MINOR_NUM=$(echo "$VERSION" | awk -F. '{print $2}')
|
||||||
|
|
||||||
|
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "branch=version/${MAJOR}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "minor=$MINOR" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "major=$MAJOR" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "release_tag=stable" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "stability=stable" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||||
|
if [ "$PATCH" = "00" ] || [ "$PATCH" = "01" ]; then
|
||||||
|
echo "is_minor=true" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Version: $VERSION (first release for this minor — full pipeline)"
|
||||||
|
else
|
||||||
|
echo "is_minor=false" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Version: $VERSION (patch — platform version + badges only)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -- STEP 1b: Bump minor version (stable = minor bump, reset patch) ------
|
||||||
|
- name: "Step 1b: Bump minor version for stable release"
|
||||||
|
if: steps.version.outputs.skip != 'true'
|
||||||
|
id: bump
|
||||||
|
run: |
|
||||||
|
CURRENT=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1)
|
||||||
|
[ -z "$CURRENT" ] && { echo "skip=true" >> "$GITHUB_OUTPUT"; exit 0; }
|
||||||
|
|
||||||
|
MAJOR=$((10#$(echo "$CURRENT" | cut -d. -f1)))
|
||||||
|
MINOR=$((10#$(echo "$CURRENT" | cut -d. -f2)))
|
||||||
|
|
||||||
|
# Minor bump, reset patch. Rollover if minor > 99
|
||||||
|
MINOR=$((MINOR + 1))
|
||||||
|
if [ $MINOR -gt 99 ]; then
|
||||||
|
MINOR=0
|
||||||
|
MAJOR=$((MAJOR + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
VERSION=$(printf "%02d.%02d.00" $MAJOR $MINOR)
|
||||||
|
TODAY=$(date +%Y-%m-%d)
|
||||||
|
|
||||||
|
echo "Stable bump: ${CURRENT} → ${VERSION} (minor)"
|
||||||
|
|
||||||
|
# Update README.md
|
||||||
|
sed -i "s/VERSION:[[:space:]]*${CURRENT}/VERSION: ${VERSION}/" README.md
|
||||||
|
|
||||||
|
# Update manifest
|
||||||
|
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
||||||
|
if [ -n "$MANIFEST" ]; then
|
||||||
|
MANIFEST_VER=$(sed -n 's/.*<version>\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1)
|
||||||
|
[ -n "$MANIFEST_VER" ] && sed -i "s|<version>${MANIFEST_VER}</version>|<version>${VERSION}</version>|" "$MANIFEST"
|
||||||
|
sed -i "s|<creationDate>[^<]*</creationDate>|<creationDate>${TODAY}</creationDate>|" "$MANIFEST"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Promote [Unreleased] section in CHANGELOG.md to new version
|
||||||
|
if [ -f "CHANGELOG.md" ] && grep -qi "Unreleased" CHANGELOG.md; then
|
||||||
|
sed -i "s|## \[Unreleased\]|## [${VERSION}] --- ${TODAY}|" CHANGELOG.md
|
||||||
|
sed -i "s|## Unreleased|## [${VERSION}] --- ${TODAY}|" CHANGELOG.md
|
||||||
|
sed -i "2i ## [Unreleased]" CHANGELOG.md
|
||||||
|
sed -i "3i \\ " CHANGELOG.md
|
||||||
|
echo "CHANGELOG promoted to [${VERSION}]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Commit and push
|
||||||
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
|
git config --local user.name "gitea-actions[bot]"
|
||||||
|
git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||||
|
git add -A
|
||||||
|
git diff --cached --quiet || {
|
||||||
|
git commit -m "chore(version): bump ${CURRENT} → ${VERSION} [skip ci]"
|
||||||
|
git push origin HEAD:main 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Override version output for rest of pipeline
|
||||||
|
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "major=$(printf "%02d" $MAJOR)" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Check if already released
|
||||||
|
if: steps.version.outputs.skip != 'true'
|
||||||
|
id: check
|
||||||
|
run: |
|
||||||
|
TAG="${{ steps.version.outputs.release_tag }}"
|
||||||
|
BRANCH="${{ steps.version.outputs.branch }}"
|
||||||
|
|
||||||
|
TAG_EXISTS=false
|
||||||
|
BRANCH_EXISTS=false
|
||||||
|
|
||||||
|
git rev-parse "$TAG" >/dev/null 2>&1 && TAG_EXISTS=true
|
||||||
|
git ls-remote --heads origin "$BRANCH" 2>/dev/null | grep -q "$BRANCH" && BRANCH_EXISTS=true
|
||||||
|
|
||||||
|
echo "tag_exists=$TAG_EXISTS" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "branch_exists=$BRANCH_EXISTS" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
# Tag and branch may persist across patch releases — never skip
|
||||||
|
echo "already_released=false" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
# -- SANITY CHECKS -------------------------------------------------------
|
||||||
|
- name: "Sanity: Pre-release validation"
|
||||||
|
if: >-
|
||||||
|
steps.version.outputs.skip != 'true' &&
|
||||||
|
steps.check.outputs.already_released != 'true'
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
|
ERRORS=0
|
||||||
|
|
||||||
|
echo "## Pre-Release Sanity Checks (Joomla)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# -- Version drift check (must pass before release) --------
|
||||||
|
README_VER=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1)
|
||||||
|
if [ "$README_VER" != "$VERSION" ]; then
|
||||||
|
echo "- Version drift: README says \`${README_VER}\` but releasing \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS+1))
|
||||||
|
else
|
||||||
|
echo "- Version consistent: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check CHANGELOG version matches
|
||||||
|
CL_VER=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' CHANGELOG.md 2>/dev/null | head -1)
|
||||||
|
if [ -n "$CL_VER" ] && [ "$CL_VER" != "$VERSION" ]; then
|
||||||
|
echo "- CHANGELOG drift: \`${CL_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS+1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check composer.json version if present
|
||||||
|
if [ -f "composer.json" ]; then
|
||||||
|
COMP_VER=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' composer.json 2>/dev/null | head -1)
|
||||||
|
if [ -n "$COMP_VER" ] && [ "$COMP_VER" != "$VERSION" ]; then
|
||||||
|
echo "- composer.json drift: \`${COMP_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS+1))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Common checks
|
||||||
|
if [ ! -f "LICENSE" ]; then
|
||||||
|
echo "- Missing LICENSE file" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS+1))
|
||||||
|
else
|
||||||
|
echo "- LICENSE present" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d "src" ] && [ ! -d "htdocs" ]; then
|
||||||
|
echo "- Warning: No src/ or htdocs/ directory" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "- Source directory present" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -- Joomla: manifest version drift --------
|
||||||
|
MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
||||||
|
if [ -n "$MANIFEST" ]; then
|
||||||
|
XML_VER=$(sed -n 's/.*<version>\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
|
||||||
|
if [ -n "$XML_VER" ] && [ "$XML_VER" != "$VERSION" ]; then
|
||||||
|
echo "- Manifest drift: \`${XML_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS+1))
|
||||||
|
else
|
||||||
|
echo "- Manifest version: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -- Joomla: XML manifest existence --------
|
||||||
|
if [ -z "$MANIFEST" ]; then
|
||||||
|
echo "- No Joomla XML manifest found" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS+1))
|
||||||
|
else
|
||||||
|
echo "- Manifest: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# -- Joomla: extension type check --------
|
||||||
|
TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" 2>/dev/null)
|
||||||
|
echo "- Extension type: ${TYPE:-unknown}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
if [ "$ERRORS" -gt 0 ]; then
|
||||||
|
echo "**${ERRORS} error(s) — release may be incomplete**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "**All sanity checks passed**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -- STEP 2: Create or update version/XX.YY archive branch ---------------
|
||||||
|
# Always runs — every version change on main archives to version/XX.YY
|
||||||
|
- name: "Step 2: Version archive branch"
|
||||||
|
if: steps.check.outputs.already_released != 'true'
|
||||||
|
run: |
|
||||||
|
BRANCH="${{ steps.version.outputs.branch }}"
|
||||||
|
IS_MINOR="${{ steps.version.outputs.is_minor }}"
|
||||||
|
PATCH="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
|
PATCH_NUM=$(echo "$PATCH" | awk -F. '{print $3}')
|
||||||
|
|
||||||
|
# Check if branch exists
|
||||||
|
if git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then
|
||||||
|
git push origin HEAD:"$BRANCH" --force
|
||||||
|
echo "Updated archive branch: ${BRANCH} (patch ${PATCH_NUM})" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
git checkout -b "$BRANCH" 2>/dev/null || git checkout "$BRANCH"
|
||||||
|
git push origin "$BRANCH" --force
|
||||||
|
echo "Created archive branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -- STEP 3: Set platform version ----------------------------------------
|
||||||
|
- name: "Step 3: Set platform version"
|
||||||
|
if: >-
|
||||||
|
steps.version.outputs.skip != 'true' &&
|
||||||
|
steps.check.outputs.already_released != 'true'
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
|
php /tmp/mokostandards-api/cli/version_set_platform.php \
|
||||||
|
--path . --version "$VERSION" --branch main
|
||||||
|
|
||||||
|
# -- STEP 4: Update version badges ----------------------------------------
|
||||||
|
- name: "Step 4: Update version badges"
|
||||||
|
if: >-
|
||||||
|
steps.version.outputs.skip != 'true' &&
|
||||||
|
steps.check.outputs.already_released != 'true'
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
|
find . -name "*.md" ! -path "./.git/*" ! -path "./vendor/*" | while read -r f; do
|
||||||
|
if grep -q '\[VERSION:' "$f" 2>/dev/null; then
|
||||||
|
sed -i "s/\[VERSION:[[:space:]]*[0-9]\{2\}\.[0-9]\{2\}\.[0-9]\{2\}\]/[VERSION: ${VERSION}]/" "$f"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# -- STEP 5: Write updates.xml (Joomla update server) ---------------------
|
||||||
|
- name: "Step 5: Write updates.xml"
|
||||||
|
id: updates
|
||||||
|
if: >-
|
||||||
|
steps.version.outputs.skip != 'true' &&
|
||||||
|
steps.check.outputs.already_released != 'true'
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
|
REPO="${{ github.repository }}"
|
||||||
|
|
||||||
|
# -- Parse extension metadata from XML manifest ----------------
|
||||||
|
MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
||||||
|
if [ -z "$MANIFEST" ]; then
|
||||||
|
echo "Warning: No Joomla XML manifest found — skipping updates.xml" >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract fields using sed (portable — no grep -P)
|
||||||
|
EXT_NAME=$(sed -n 's/.*<name>\([^<]*\)<\/name>.*/\1/p' "$MANIFEST" | head -1)
|
||||||
|
EXT_TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
|
||||||
|
EXT_ELEMENT=$(sed -n 's/.*<element>\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" | head -1)
|
||||||
|
EXT_CLIENT=$(sed -n 's/.*<extension[^>]*client="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
|
||||||
|
EXT_FOLDER=$(sed -n 's/.*<extension[^>]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
|
||||||
|
TARGET_PLATFORM=$(sed -n 's/.*\(<targetplatform[^/]*\/>\).*/\1/p' "$MANIFEST" | head -1)
|
||||||
|
PHP_MINIMUM=$(sed -n 's/.*<php_minimum>\([^<]*\)<\/php_minimum>.*/\1/p' "$MANIFEST" | head -1)
|
||||||
|
|
||||||
|
# If EXT_NAME is a language key (e.g. PLG_SYSTEM_MOKOJGDPC), resolve from .ini
|
||||||
|
if echo "$EXT_NAME" | grep -qE '^[A-Z_]+$'; then
|
||||||
|
INI_NAME=$(find . -name "*.sys.ini" -path "*/en-GB/*" -exec grep -h "^${EXT_NAME}=" {} \; 2>/dev/null | head -1 | cut -d'"' -f2)
|
||||||
|
[ -z "$INI_NAME" ] && INI_NAME=$(find . -name "*.sys.ini" -exec grep -h "^${EXT_NAME}=" {} \; 2>/dev/null | head -1 | cut -d'"' -f2)
|
||||||
|
[ -n "$INI_NAME" ] && EXT_NAME="$INI_NAME"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fallbacks
|
||||||
|
[ -z "$EXT_NAME" ] && EXT_NAME="${{ github.event.repository.name }}"
|
||||||
|
[ -z "$EXT_TYPE" ] && EXT_TYPE="component"
|
||||||
|
|
||||||
|
# Derive element if not in manifest:
|
||||||
|
# 1. plugin="xxx" attribute (plugins)
|
||||||
|
# 2. module="xxx" attribute (modules)
|
||||||
|
# 3. XML filename (components, packages)
|
||||||
|
# 4. Repo name fallback (templates, anything else)
|
||||||
|
if [ -z "$EXT_ELEMENT" ]; then
|
||||||
|
EXT_ELEMENT=$(sed -n 's/.*plugin="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
|
||||||
|
fi
|
||||||
|
if [ -z "$EXT_ELEMENT" ]; then
|
||||||
|
EXT_ELEMENT=$(sed -n 's/.*module="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
|
||||||
|
fi
|
||||||
|
if [ -z "$EXT_ELEMENT" ]; then
|
||||||
|
FNAME=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]')
|
||||||
|
# If filename is generic (templateDetails, manifest), use repo name
|
||||||
|
case "$FNAME" in
|
||||||
|
templatedetails|manifest) EXT_ELEMENT=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;;
|
||||||
|
*) EXT_ELEMENT="$FNAME" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
# Final fallback
|
||||||
|
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
|
||||||
|
|
||||||
|
# Save for Steps 7, 8, 8b
|
||||||
|
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "ext_name=${EXT_NAME}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "ext_type=${EXT_TYPE}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "ext_folder=${EXT_FOLDER}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
# Build client tag: plugins and frontend modules need <client>site</client>
|
||||||
|
CLIENT_TAG=""
|
||||||
|
if [ -n "$EXT_CLIENT" ]; then
|
||||||
|
CLIENT_TAG="<client>${EXT_CLIENT}</client>"
|
||||||
|
elif [ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]; then
|
||||||
|
CLIENT_TAG="<client>site</client>"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build folder tag for plugins (required for Joomla to match the update)
|
||||||
|
FOLDER_TAG=""
|
||||||
|
if [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ]; then
|
||||||
|
FOLDER_TAG="<folder>${EXT_FOLDER}</folder>"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build targetplatform (fallback to Joomla 5 if not in manifest)
|
||||||
|
if [ -z "$TARGET_PLATFORM" ]; then
|
||||||
|
TARGET_PLATFORM=$(printf '<targetplatform name="joomla" version="((5.[0-9])|(6.[0-9]))" %s>' "/")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build php_minimum tag
|
||||||
|
PHP_TAG=""
|
||||||
|
if [ -n "$PHP_MINIMUM" ]; then
|
||||||
|
PHP_TAG="<php_minimum>${PHP_MINIMUM}</php_minimum>"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build TYPE_PREFIX for download URL
|
||||||
|
TYPE_PREFIX=""
|
||||||
|
case "${EXT_TYPE}" in
|
||||||
|
plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;;
|
||||||
|
module) TYPE_PREFIX="mod_" ;;
|
||||||
|
component) TYPE_PREFIX="com_" ;;
|
||||||
|
template) TYPE_PREFIX="tpl_" ;;
|
||||||
|
library) TYPE_PREFIX="lib_" ;;
|
||||||
|
package) TYPE_PREFIX="pkg_" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
DOWNLOAD_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/stable/${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip"
|
||||||
|
INFO_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/stable"
|
||||||
|
|
||||||
|
# -- Build update entry for a given stability tag
|
||||||
|
build_entry() {
|
||||||
|
local TAG_NAME="$1"
|
||||||
|
printf '%s\n' ' <update>'
|
||||||
|
printf '%s\n' " <name>${EXT_NAME}</name>"
|
||||||
|
printf '%s\n' " <description>${EXT_NAME} update</description>"
|
||||||
|
printf '%s\n' " <element>${EXT_ELEMENT}</element>"
|
||||||
|
printf '%s\n' " <type>${EXT_TYPE}</type>"
|
||||||
|
printf '%s\n' " <version>${VERSION}</version>"
|
||||||
|
[ -n "$CLIENT_TAG" ] && printf '%s\n' " ${CLIENT_TAG}"
|
||||||
|
[ -n "$FOLDER_TAG" ] && printf '%s\n' " ${FOLDER_TAG}"
|
||||||
|
printf '%s\n' " <tags><tag>${TAG_NAME}</tag></tags>"
|
||||||
|
printf '%s\n' " <infourl title=\"${EXT_NAME}\">${INFO_URL}</infourl>"
|
||||||
|
printf '%s\n' ' <downloads>'
|
||||||
|
printf '%s\n' " <downloadurl type=\"full\" format=\"zip\">${DOWNLOAD_URL}</downloadurl>"
|
||||||
|
printf '%s\n' ' </downloads>'
|
||||||
|
printf '%s\n' " ${TARGET_PLATFORM}"
|
||||||
|
[ -n "$PHP_TAG" ] && printf '%s\n' " ${PHP_TAG}"
|
||||||
|
printf '%s\n' ' <maintainer>Moko Consulting</maintainer>'
|
||||||
|
printf '%s\n' ' <maintainerurl>https://mokoconsulting.tech</maintainerurl>'
|
||||||
|
printf '%s\n' ' </update>'
|
||||||
|
}
|
||||||
|
|
||||||
|
# -- Write updates.xml with cascading channels
|
||||||
|
# Stable release updates ALL channels (development, alpha, beta, rc, stable)
|
||||||
|
{
|
||||||
|
printf '%s\n' "<?xml version='1.0' encoding='UTF-8'?>"
|
||||||
|
printf '%s\n' "<!-- Copyright (C) $(date +%Y) Moko Consulting <hello@mokoconsulting.tech>"
|
||||||
|
printf '%s\n' " SPDX-License-Identifier: GPL-3.0-or-later"
|
||||||
|
printf '%s\n' " VERSION: ${VERSION}"
|
||||||
|
printf '%s\n' " -->"
|
||||||
|
printf '%s\n' ""
|
||||||
|
printf '%s\n' '<updates>'
|
||||||
|
build_entry "development"
|
||||||
|
build_entry "alpha"
|
||||||
|
build_entry "beta"
|
||||||
|
build_entry "rc"
|
||||||
|
build_entry "stable"
|
||||||
|
printf '%s\n' '</updates>'
|
||||||
|
} > updates.xml
|
||||||
|
|
||||||
|
echo "updates.xml: ${VERSION} (all channels updated to stable)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# -- Commit all changes ---------------------------------------------------
|
||||||
|
- name: Commit release changes
|
||||||
|
if: >-
|
||||||
|
steps.version.outputs.skip != 'true' &&
|
||||||
|
steps.check.outputs.already_released != 'true'
|
||||||
|
run: |
|
||||||
|
if git diff --quiet && git diff --cached --quiet; then
|
||||||
|
echo "No changes to commit"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
|
git config --local user.name "gitea-actions[bot]"
|
||||||
|
# Set push URL with token for branch-protected repos
|
||||||
|
git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||||
|
git add -A
|
||||||
|
git commit -m "chore(release): build ${VERSION} [skip ci]" \
|
||||||
|
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
|
||||||
|
git push -u origin HEAD
|
||||||
|
|
||||||
|
# -- STEP 6: Create tag ---------------------------------------------------
|
||||||
|
- name: "Step 6: Create git tag"
|
||||||
|
if: >-
|
||||||
|
steps.version.outputs.skip != 'true' &&
|
||||||
|
steps.check.outputs.tag_exists != 'true' &&
|
||||||
|
steps.version.outputs.is_minor == 'true'
|
||||||
|
run: |
|
||||||
|
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
||||||
|
# Only create the major release tag if it doesn't exist yet
|
||||||
|
if ! git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then
|
||||||
|
git tag "$RELEASE_TAG"
|
||||||
|
git push origin "$RELEASE_TAG"
|
||||||
|
echo "Tag created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "Tag ${RELEASE_TAG} already exists" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
echo "Tag: ${TAG}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# -- STEP 7: Create or update Gitea Release --------------------------------
|
||||||
|
- name: "Step 7: Gitea Release"
|
||||||
|
if: >-
|
||||||
|
steps.version.outputs.skip != 'true'
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
|
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
||||||
|
BRANCH="${{ steps.version.outputs.branch }}"
|
||||||
|
MAJOR="${{ steps.version.outputs.major }}"
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
|
||||||
|
# Reuse metadata from Step 5 (single source of truth)
|
||||||
|
EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}"
|
||||||
|
EXT_NAME="${{ steps.updates.outputs.ext_name }}"
|
||||||
|
EXT_TYPE="${{ steps.updates.outputs.ext_type }}"
|
||||||
|
EXT_FOLDER="${{ steps.updates.outputs.ext_folder }}"
|
||||||
|
|
||||||
|
# Fallbacks if Step 5 was skipped
|
||||||
|
if [ -z "$EXT_ELEMENT" ]; then
|
||||||
|
EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
|
||||||
|
fi
|
||||||
|
[ -z "$EXT_NAME" ] && EXT_NAME="${GITEA_REPO}"
|
||||||
|
|
||||||
|
NOTES=$(php /tmp/mokostandards-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null)
|
||||||
|
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
|
||||||
|
|
||||||
|
# Build release name: "Pretty Name VERSION (type_element-VERSION)"
|
||||||
|
TYPE_PREFIX=""
|
||||||
|
case "${EXT_TYPE}" in
|
||||||
|
plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;;
|
||||||
|
module) TYPE_PREFIX="mod_" ;;
|
||||||
|
component) TYPE_PREFIX="com_" ;;
|
||||||
|
template) TYPE_PREFIX="tpl_" ;;
|
||||||
|
library) TYPE_PREFIX="lib_" ;;
|
||||||
|
package) TYPE_PREFIX="pkg_" ;;
|
||||||
|
esac
|
||||||
|
RELEASE_NAME="${EXT_NAME} ${VERSION} (${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION})"
|
||||||
|
|
||||||
|
# Delete existing release if present (overwrite, not append)
|
||||||
|
EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
||||||
|
"${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true)
|
||||||
|
EXISTING_ID=$(echo "$EXISTING" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('id',''))" 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [ -n "$EXISTING_ID" ]; then
|
||||||
|
curl -sS -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
||||||
|
"${API_BASE}/releases/${EXISTING_ID}" 2>/dev/null || true
|
||||||
|
curl -sS -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
||||||
|
"${API_BASE}/tags/${RELEASE_TAG}" 2>/dev/null || true
|
||||||
|
echo "Deleted previous stable release (id: ${EXISTING_ID})"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create fresh release
|
||||||
|
curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API_BASE}/releases" \
|
||||||
|
-d "$(python3 -c "import json; print(json.dumps({
|
||||||
|
'tag_name': '${RELEASE_TAG}',
|
||||||
|
'name': '${RELEASE_NAME}',
|
||||||
|
'body': '''## ${VERSION} ($(date +%Y-%m-%d))\n${NOTES}''',
|
||||||
|
'target_commitish': '${BRANCH}'
|
||||||
|
}))")"
|
||||||
|
echo "Release created: ${RELEASE_NAME}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# -- STEP 8: Build Joomla install ZIP + SHA-256 checksum ------------------
|
||||||
|
- name: "Step 8: Build Joomla package and update checksum"
|
||||||
|
if: >-
|
||||||
|
steps.version.outputs.skip != 'true'
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
|
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
||||||
|
REPO="${{ github.repository }}"
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
|
||||||
|
# All ZIPs upload to the major release tag (vXX)
|
||||||
|
RELEASE_JSON=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
||||||
|
"${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true)
|
||||||
|
RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
||||||
|
if [ -z "$RELEASE_ID" ]; then
|
||||||
|
echo "No release ${RELEASE_TAG} found — skipping ZIP upload"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Find extension element name from manifest
|
||||||
|
MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true)
|
||||||
|
[ -z "$MANIFEST" ] && exit 0
|
||||||
|
|
||||||
|
# Reuse element from Step 5, with same fallback chain
|
||||||
|
EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}"
|
||||||
|
if [ -z "$EXT_ELEMENT" ]; then
|
||||||
|
EXT_ELEMENT=$(sed -n 's/.*<element>\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
|
||||||
|
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(sed -n 's/.*plugin="\([^"]*\)".*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
|
||||||
|
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]')
|
||||||
|
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
|
||||||
|
fi
|
||||||
|
# ZIP name: type_folder_element-VERSION (e.g. plg_system_mokojgdpc-01.01.00.zip)
|
||||||
|
EXT_TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
|
||||||
|
EXT_FOLDER=$(sed -n 's/.*<extension[^>]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
|
||||||
|
TYPE_PREFIX=""
|
||||||
|
case "${EXT_TYPE}" in
|
||||||
|
plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;;
|
||||||
|
module) TYPE_PREFIX="mod_" ;;
|
||||||
|
component) TYPE_PREFIX="com_" ;;
|
||||||
|
template) TYPE_PREFIX="tpl_" ;;
|
||||||
|
library) TYPE_PREFIX="lib_" ;;
|
||||||
|
package) TYPE_PREFIX="pkg_" ;;
|
||||||
|
esac
|
||||||
|
ZIP_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip"
|
||||||
|
TAR_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.tar.gz"
|
||||||
|
|
||||||
|
# -- Build install packages from src/ ----------------------------
|
||||||
|
SOURCE_DIR="src"
|
||||||
|
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
||||||
|
[ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ — skipping package"; exit 0; }
|
||||||
|
|
||||||
|
EXCLUDES=".ftpignore sftp-config* *.ppk *.pem *.key .env*"
|
||||||
|
|
||||||
|
# ZIP package
|
||||||
|
cd "$SOURCE_DIR"
|
||||||
|
zip -r "/tmp/${ZIP_NAME}" . -x $EXCLUDES
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
# tar.gz package
|
||||||
|
tar -czf "/tmp/${TAR_NAME}" -C "$SOURCE_DIR" \
|
||||||
|
--exclude='.ftpignore' --exclude='sftp-config*' \
|
||||||
|
--exclude='*.ppk' --exclude='*.pem' --exclude='*.key' --exclude='.env*' .
|
||||||
|
|
||||||
|
ZIP_SIZE=$(stat -c%s "/tmp/${ZIP_NAME}" 2>/dev/null || stat -f%z "/tmp/${ZIP_NAME}" 2>/dev/null || echo "unknown")
|
||||||
|
TAR_SIZE=$(stat -c%s "/tmp/${TAR_NAME}" 2>/dev/null || stat -f%z "/tmp/${TAR_NAME}" 2>/dev/null || echo "unknown")
|
||||||
|
|
||||||
|
# -- Calculate SHA-256 for both ----------------------------------
|
||||||
|
SHA256_ZIP=$(sha256sum "/tmp/${ZIP_NAME}" | cut -d' ' -f1)
|
||||||
|
SHA256_TAR=$(sha256sum "/tmp/${TAR_NAME}" | cut -d' ' -f1)
|
||||||
|
|
||||||
|
# -- Delete existing assets with same name before uploading ------
|
||||||
|
ASSETS=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
||||||
|
"${API_BASE}/releases/${RELEASE_ID}/assets" 2>/dev/null || echo "[]")
|
||||||
|
for ASSET_NAME in "$ZIP_NAME" "$TAR_NAME"; do
|
||||||
|
ASSET_ID=$(echo "$ASSETS" | python3 -c "
|
||||||
|
import sys,json
|
||||||
|
assets = json.load(sys.stdin)
|
||||||
|
for a in assets:
|
||||||
|
if a['name'] == '${ASSET_NAME}':
|
||||||
|
print(a['id']); break
|
||||||
|
" 2>/dev/null || true)
|
||||||
|
if [ -n "$ASSET_ID" ]; then
|
||||||
|
curl -sf -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
||||||
|
"${API_BASE}/releases/${RELEASE_ID}/assets/${ASSET_ID}" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# -- Upload both to release tag ----------------------------------
|
||||||
|
curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
||||||
|
-H "Content-Type: application/octet-stream" \
|
||||||
|
--data-binary @"/tmp/${ZIP_NAME}" \
|
||||||
|
"${API_BASE}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" > /dev/null 2>&1 || true
|
||||||
|
|
||||||
|
curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
||||||
|
-H "Content-Type: application/octet-stream" \
|
||||||
|
--data-binary @"/tmp/${TAR_NAME}" \
|
||||||
|
"${API_BASE}/releases/${RELEASE_ID}/assets?name=${TAR_NAME}" > /dev/null 2>&1 || true
|
||||||
|
|
||||||
|
# -- Update updates.xml with both download formats ---------------
|
||||||
|
if [ -f "updates.xml" ]; then
|
||||||
|
ZIP_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${ZIP_NAME}"
|
||||||
|
TAR_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${TAR_NAME}"
|
||||||
|
|
||||||
|
# Use Python to update only the stable entry's downloads + sha256
|
||||||
|
export PY_ZIP_URL="$ZIP_URL" PY_TAR_URL="$TAR_URL" PY_SHA="$SHA256_ZIP"
|
||||||
|
python3 << 'PYEOF'
|
||||||
|
import re, os
|
||||||
|
|
||||||
|
with open("updates.xml") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
zip_url = os.environ["PY_ZIP_URL"]
|
||||||
|
tar_url = os.environ["PY_TAR_URL"]
|
||||||
|
sha = os.environ["PY_SHA"]
|
||||||
|
|
||||||
|
# Find the stable update block and replace its downloads + sha256
|
||||||
|
def replace_stable(m):
|
||||||
|
block = m.group(0)
|
||||||
|
# Replace downloads block
|
||||||
|
new_downloads = (
|
||||||
|
" <downloads>\n"
|
||||||
|
f" <downloadurl type=\"full\" format=\"zip\">{zip_url}</downloadurl>\n"
|
||||||
|
" </downloads>"
|
||||||
|
)
|
||||||
|
block = re.sub(r' <downloads>.*?</downloads>', new_downloads, block, flags=re.DOTALL)
|
||||||
|
# Add or replace sha256
|
||||||
|
if '<sha256>' in block:
|
||||||
|
block = re.sub(r' <sha256>.*?</sha256>', f' <sha256>{sha}</sha256>', block)
|
||||||
|
else:
|
||||||
|
block = block.replace('</downloads>', f'</downloads>\n <sha256>{sha}</sha256>')
|
||||||
|
return block
|
||||||
|
|
||||||
|
content = re.sub(
|
||||||
|
r' <update>.*?<tag>stable</tag>.*?</update>',
|
||||||
|
replace_stable,
|
||||||
|
content,
|
||||||
|
flags=re.DOTALL
|
||||||
|
)
|
||||||
|
|
||||||
|
with open("updates.xml", "w") as f:
|
||||||
|
f.write(content)
|
||||||
|
PYEOF
|
||||||
|
|
||||||
|
CURRENT_BRANCH="${{ github.ref_name }}"
|
||||||
|
git add updates.xml
|
||||||
|
git commit -m "chore(release): ZIP + tar.gz for ${VERSION} [skip ci]" \
|
||||||
|
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" || true
|
||||||
|
git push || true
|
||||||
|
|
||||||
|
# Sync updates.xml to main via direct API (always runs — may be on version/XX branch)
|
||||||
|
GA_TOKEN="${{ secrets.GA_TOKEN }}"
|
||||||
|
API="${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}"
|
||||||
|
|
||||||
|
FILE_SHA=$(curl -sf -H "Authorization: token ${GA_TOKEN}" \
|
||||||
|
"${API}/contents/updates.xml?ref=main" | jq -r '.sha // empty')
|
||||||
|
|
||||||
|
if [ -n "$FILE_SHA" ]; then
|
||||||
|
CONTENT=$(base64 -w0 updates.xml)
|
||||||
|
curl -sf -X PUT -H "Authorization: token ${GA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API}/contents/updates.xml" \
|
||||||
|
-d "$(jq -n \
|
||||||
|
--arg content "$CONTENT" \
|
||||||
|
--arg sha "$FILE_SHA" \
|
||||||
|
--arg msg "chore: sync updates.xml ${VERSION} [skip ci]" \
|
||||||
|
--arg branch "main" \
|
||||||
|
'{content: $content, sha: $sha, message: $msg, branch: $branch}'
|
||||||
|
)" > /dev/null 2>&1 \
|
||||||
|
&& echo "updates.xml synced to main via API" \
|
||||||
|
|| echo "WARNING: failed to sync updates.xml to main"
|
||||||
|
else
|
||||||
|
echo "WARNING: could not get updates.xml SHA from main"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "### Joomla Packages" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Package | Size | SHA-256 |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "|---------|------|---------|" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| \`${ZIP_NAME}\` | ${ZIP_SIZE} | \`${SHA256_ZIP}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| \`${TAR_NAME}\` | ${TAR_SIZE} | \`${SHA256_TAR}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Release | \`${RELEASE_TAG}\` | |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Download | [${ZIP_NAME}](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${ZIP_NAME}) |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# -- STEP 8b: Update release description with changelog + SHA ----------------
|
||||||
|
- name: "Step 8b: Update release body with changelog and SHA"
|
||||||
|
if: steps.version.outputs.skip != 'true'
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
|
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}"
|
||||||
|
EXT_TYPE="${{ steps.updates.outputs.ext_type }}"
|
||||||
|
EXT_FOLDER="${{ steps.updates.outputs.ext_folder }}"
|
||||||
|
|
||||||
|
# Build TYPE_PREFIX to match Step 8's ZIP naming
|
||||||
|
TYPE_PREFIX=""
|
||||||
|
case "${EXT_TYPE}" in
|
||||||
|
plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;;
|
||||||
|
module) TYPE_PREFIX="mod_" ;;
|
||||||
|
component) TYPE_PREFIX="com_" ;;
|
||||||
|
template) TYPE_PREFIX="tpl_" ;;
|
||||||
|
library) TYPE_PREFIX="lib_" ;;
|
||||||
|
package) TYPE_PREFIX="pkg_" ;;
|
||||||
|
esac
|
||||||
|
ZIP_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip"
|
||||||
|
TAR_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.tar.gz"
|
||||||
|
|
||||||
|
# Get SHA from the built files
|
||||||
|
SHA256_ZIP=""
|
||||||
|
[ -f "/tmp/${ZIP_NAME}" ] && SHA256_ZIP=$(sha256sum "/tmp/${ZIP_NAME}" | cut -d' ' -f1)
|
||||||
|
SHA256_TAR=""
|
||||||
|
[ -f "/tmp/${TAR_NAME}" ] && SHA256_TAR=$(sha256sum "/tmp/${TAR_NAME}" | cut -d' ' -f1)
|
||||||
|
|
||||||
|
# Extract latest changelog entry (strip the ## header to avoid duplicate)
|
||||||
|
CHANGELOG=""
|
||||||
|
if [ -f "CHANGELOG.md" ]; then
|
||||||
|
CHANGELOG=$(sed -n "/^## \[*${VERSION}/,/^## \[*[0-9]/p" CHANGELOG.md | sed '$d' | sed '1d')
|
||||||
|
[ -z "$CHANGELOG" ] && CHANGELOG=$(sed -n '/^## /,/^## /p' CHANGELOG.md | sed '$d' | sed '1d' | head -30)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build release body (single header, no duplicate from changelog)
|
||||||
|
BODY="## ${VERSION} ($(date +%Y-%m-%d))\n\n"
|
||||||
|
if [ -n "$CHANGELOG" ]; then
|
||||||
|
BODY="${BODY}${CHANGELOG}\n\n"
|
||||||
|
fi
|
||||||
|
BODY="${BODY}---\n\n### Checksums\n\n"
|
||||||
|
BODY="${BODY}| File | SHA-256 |\n|------|--------|\n"
|
||||||
|
[ -n "$SHA256_ZIP" ] && BODY="${BODY}| \`${ZIP_NAME}\` | \`${SHA256_ZIP}\` |\n"
|
||||||
|
[ -n "$SHA256_TAR" ] && BODY="${BODY}| \`${TAR_NAME}\` | \`${SHA256_TAR}\` |\n"
|
||||||
|
|
||||||
|
# Get release ID and update body
|
||||||
|
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
||||||
|
"${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null | \
|
||||||
|
python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "None" ]; then
|
||||||
|
python3 -c "
|
||||||
|
import json, urllib.request
|
||||||
|
body = '''$(printf '%b' "$BODY")'''
|
||||||
|
data = json.dumps({'body': body}).encode()
|
||||||
|
req = urllib.request.Request(
|
||||||
|
'${API_BASE}/releases/${RELEASE_ID}',
|
||||||
|
data=data,
|
||||||
|
headers={'Authorization': 'token ${{ secrets.GA_TOKEN }}', 'Content-Type': 'application/json'},
|
||||||
|
method='PATCH'
|
||||||
|
)
|
||||||
|
urllib.request.urlopen(req)
|
||||||
|
" 2>/dev/null && echo "Release body updated with changelog + SHA" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
|
||||||
|
- name: "Step 9: Mirror release to GitHub"
|
||||||
|
if: >-
|
||||||
|
steps.version.outputs.skip != 'true' &&
|
||||||
|
steps.version.outputs.stability == 'stable' &&
|
||||||
|
secrets.GH_TOKEN != ''
|
||||||
|
continue-on-error: true
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
|
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
||||||
|
MAJOR="${{ steps.version.outputs.major }}"
|
||||||
|
BRANCH="${{ steps.version.outputs.branch }}"
|
||||||
|
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
||||||
|
|
||||||
|
NOTES=$(php /tmp/mokostandards-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null || true)
|
||||||
|
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
|
||||||
|
echo "$NOTES" > /tmp/release_notes.md
|
||||||
|
|
||||||
|
EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/tags/$RELEASE_TAG" 2>/dev/null | jq -r ".tag_name // empty" || true)
|
||||||
|
|
||||||
|
if [ -z "$EXISTING" ]; then
|
||||||
|
gh release create "$RELEASE_TAG" \
|
||||||
|
--repo "$GH_REPO" \
|
||||||
|
--title "v${MAJOR} (latest: ${VERSION})" \
|
||||||
|
--notes-file /tmp/release_notes.md \
|
||||||
|
--target "$BRANCH" || true
|
||||||
|
else
|
||||||
|
gh release edit "$RELEASE_TAG" \
|
||||||
|
--repo "$GH_REPO" \
|
||||||
|
--title "v${MAJOR} (latest: ${VERSION})" || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Upload assets to GitHub mirror
|
||||||
|
for PKG in /tmp/${EXT_ELEMENT:-pkg}-${VERSION}.*; do
|
||||||
|
if [ -f "$PKG" ]; then
|
||||||
|
_RELID=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/tags/$RELEASE_TAG" 2>/dev/null | jq -r ".id // empty")
|
||||||
|
[ -n "$_RELID" ] && curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" -H "Content-Type: application/octet-stream" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/${_RELID}/assets?name=$(basename $PKG)" --data-binary "@$PKG" > /dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "GitHub mirror updated: ${GH_REPO} ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# -- STEP 10: Sync main branch to GitHub mirror ----------------------------
|
||||||
|
- name: "Step 10: Push main to GitHub mirror"
|
||||||
|
if: >-
|
||||||
|
steps.version.outputs.skip != 'true' &&
|
||||||
|
secrets.GH_TOKEN != ''
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
||||||
|
GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
|
||||||
|
GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
|
||||||
|
git remote add github "https://x-access-token:${{ secrets.GH_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
|
||||||
|
git remote set-url github "https://x-access-token:${{ secrets.GH_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
|
||||||
|
git fetch origin main --depth=1
|
||||||
|
git push github origin/main:refs/heads/main --force 2>/dev/null \
|
||||||
|
&& echo "main branch pushed to GitHub mirror" \
|
||||||
|
|| echo "WARNING: GitHub mirror push failed"
|
||||||
|
|
||||||
|
# -- Clean up lesser pre-releases (cascade) ---------------------------------
|
||||||
|
# stable → deletes all | rc → beta,alpha,dev | beta → alpha,dev | alpha → dev
|
||||||
|
- name: "Delete lesser pre-release channels"
|
||||||
|
if: steps.version.outputs.skip != 'true'
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
TOKEN="${{ secrets.GA_TOKEN }}"
|
||||||
|
|
||||||
|
# Stable deletes all pre-release channels
|
||||||
|
TAGS_TO_DELETE="development alpha beta release-candidate"
|
||||||
|
|
||||||
|
DELETED=0
|
||||||
|
for TAG in $TAGS_TO_DELETE; do
|
||||||
|
RELEASE_ID=$(curl -sS -H "Authorization: token ${TOKEN}" \
|
||||||
|
"${API_BASE}/releases/tags/${TAG}" 2>/dev/null | \
|
||||||
|
python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "None" ]; then
|
||||||
|
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
|
||||||
|
"${API_BASE}/releases/${RELEASE_ID}" 2>/dev/null || true
|
||||||
|
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
|
||||||
|
"${API_BASE}/tags/${TAG}" 2>/dev/null || true
|
||||||
|
echo "Deleted: ${TAG} (id: ${RELEASE_ID})"
|
||||||
|
DELETED=$((DELETED + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "Cleaned up ${DELETED} pre-release channel(s)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# -- STEP 11: Reset dev branch from main ------------------------------------
|
||||||
|
- name: "Step 11: Delete and recreate dev branch from main"
|
||||||
|
if: steps.version.outputs.skip != 'true'
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
TOKEN="${{ secrets.GA_TOKEN }}"
|
||||||
|
|
||||||
|
# Delete dev branch
|
||||||
|
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
|
||||||
|
"${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
|
||||||
|
|
||||||
|
# Recreate dev from main (now includes version bump + changelog promotion)
|
||||||
|
curl -sf -X POST -H "Authorization: token ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API_BASE}/branches" \
|
||||||
|
-d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
|
||||||
|
|
||||||
|
echo "Dev branch reset from main (keeps dev ahead after release)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# -- Summary --------------------------------------------------------------
|
||||||
|
- name: Pipeline Summary
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
|
if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
|
||||||
|
echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
|
||||||
|
elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
|
||||||
|
echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "## Build & Release Complete (Joomla)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
---
|
||||||
|
name: Bug Report
|
||||||
|
about: Report a bug or issue with the project
|
||||||
|
title: '[BUG] '
|
||||||
|
labels: 'bug'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## Bug Description
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
## Steps to Reproduce
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '...'
|
||||||
|
3. Scroll down to '...'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
## Actual Behavior
|
||||||
|
A clear and concise description of what actually happened.
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
- **Project**: [e.g., MokoDoliTools, moko-cassiopeia]
|
||||||
|
- **Version**: [e.g., 1.2.3]
|
||||||
|
- **Platform**: [e.g., Dolibarr 18.0, Joomla 5.0]
|
||||||
|
- **PHP Version**: [e.g., 8.1]
|
||||||
|
- **Database**: [e.g., MySQL 8.0, PostgreSQL 14]
|
||||||
|
- **Browser** (if applicable): [e.g., Chrome 120, Firefox 121]
|
||||||
|
- **OS**: [e.g., Ubuntu 22.04, Windows 11]
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
Add any other context about the problem here.
|
||||||
|
|
||||||
|
## Possible Solution
|
||||||
|
If you have suggestions on how to fix the issue, please describe them here.
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
- [ ] I have searched for similar issues before creating this one
|
||||||
|
- [ ] I have provided all the requested information
|
||||||
|
- [ ] I have tested this on the latest stable version
|
||||||
|
- [ ] I have checked the documentation and couldn't find a solution
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: MokoStandards.Maintenance
|
||||||
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
||||||
|
# PATH: /templates/workflows/cascade-dev.yml.template
|
||||||
|
# VERSION: 02.00.00
|
||||||
|
# BRIEF: Forward-merge main → all open branches after every push to main
|
||||||
|
#
|
||||||
|
# +========================================================================+
|
||||||
|
# | CASCADE MAIN → ALL BRANCHES |
|
||||||
|
# +========================================================================+
|
||||||
|
# | |
|
||||||
|
# | Triggers on every push to main (PR merges, bot commits, etc.) |
|
||||||
|
# | |
|
||||||
|
# | 1. List all branches matching: dev, rc/*, beta/*, alpha/* |
|
||||||
|
# | 2. For each: create PR (main → branch), auto-merge if clean |
|
||||||
|
# | 3. On conflict: leave PR open for manual resolution |
|
||||||
|
# | |
|
||||||
|
# +========================================================================+
|
||||||
|
|
||||||
|
name: Cascade Main → Dev
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||||||
|
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cascade:
|
||||||
|
name: Cascade main → branches
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: >-
|
||||||
|
!contains(github.event.head_commit.message, '[skip ci]') &&
|
||||||
|
!contains(github.event.head_commit.message, '[skip cascade]')
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Discover target branches
|
||||||
|
id: branches
|
||||||
|
env:
|
||||||
|
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||||
|
run: |
|
||||||
|
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
|
||||||
|
# Fetch all branches (paginated)
|
||||||
|
PAGE=1
|
||||||
|
ALL_BRANCHES=""
|
||||||
|
while true; do
|
||||||
|
BATCH=$(curl -sS \
|
||||||
|
-H "Authorization: token ${GA_TOKEN}" \
|
||||||
|
"${API}/branches?page=${PAGE}&limit=50" \
|
||||||
|
| jq -r '.[].name // empty')
|
||||||
|
[ -z "$BATCH" ] && break
|
||||||
|
ALL_BRANCHES="$ALL_BRANCHES $BATCH"
|
||||||
|
PAGE=$((PAGE + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
# Filter to cascade targets: dev, dev/*, rc/*, beta/*, alpha/*
|
||||||
|
TARGETS=""
|
||||||
|
for BRANCH in $ALL_BRANCHES; do
|
||||||
|
case "$BRANCH" in
|
||||||
|
dev|dev/*|rc/*|beta/*|alpha/*)
|
||||||
|
TARGETS="$TARGETS $BRANCH"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
TARGETS=$(echo "$TARGETS" | xargs) # trim whitespace
|
||||||
|
|
||||||
|
if [ -z "$TARGETS" ]; then
|
||||||
|
echo "targets=" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "ℹ️ No cascade target branches found"
|
||||||
|
else
|
||||||
|
echo "targets=$TARGETS" >> "$GITHUB_OUTPUT"
|
||||||
|
COUNT=$(echo "$TARGETS" | wc -w)
|
||||||
|
echo "📋 Found ${COUNT} target branch(es): ${TARGETS}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Cascade to all target branches
|
||||||
|
if: steps.branches.outputs.targets != ''
|
||||||
|
env:
|
||||||
|
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||||
|
run: |
|
||||||
|
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
SHORT_SHA="${GITHUB_SHA:0:7}"
|
||||||
|
TARGETS="${{ steps.branches.outputs.targets }}"
|
||||||
|
|
||||||
|
SUCCESS=0
|
||||||
|
CONFLICTS=0
|
||||||
|
SKIPPED=0
|
||||||
|
FAILED=0
|
||||||
|
|
||||||
|
for BRANCH in $TARGETS; do
|
||||||
|
echo ""
|
||||||
|
echo "═══ main → ${BRANCH} ═══"
|
||||||
|
|
||||||
|
# Check if branch is already up to date
|
||||||
|
ENCODED_BRANCH=$(echo "$BRANCH" | sed 's|/|%2F|g')
|
||||||
|
RESPONSE=$(curl -sS \
|
||||||
|
-H "Authorization: token ${GA_TOKEN}" \
|
||||||
|
"${API}/compare/${ENCODED_BRANCH}...main")
|
||||||
|
|
||||||
|
AHEAD=$(echo "$RESPONSE" | jq '.total_commits // 0')
|
||||||
|
|
||||||
|
if [ "$AHEAD" -eq 0 ]; then
|
||||||
|
echo " ✅ Already up to date"
|
||||||
|
SKIPPED=$((SKIPPED + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " ℹ️ main is ${AHEAD} commit(s) ahead"
|
||||||
|
|
||||||
|
# Check for existing cascade PR
|
||||||
|
EXISTING=$(curl -sS \
|
||||||
|
-H "Authorization: token ${GA_TOKEN}" \
|
||||||
|
"${API}/pulls?state=open&head=${GITEA_ORG}:main&base=${ENCODED_BRANCH}&limit=1")
|
||||||
|
|
||||||
|
EXISTING_COUNT=$(echo "$EXISTING" | jq 'length')
|
||||||
|
PR_NUMBER=""
|
||||||
|
|
||||||
|
if [ "$EXISTING_COUNT" -gt 0 ]; then
|
||||||
|
PR_NUMBER=$(echo "$EXISTING" | jq -r '.[0].number')
|
||||||
|
echo " ℹ️ Reusing existing PR #${PR_NUMBER}"
|
||||||
|
else
|
||||||
|
# Create cascade PR
|
||||||
|
PR_RESPONSE=$(curl -sS -w "\n%{http_code}" \
|
||||||
|
-X POST \
|
||||||
|
-H "Authorization: token ${GA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{
|
||||||
|
\"title\": \"chore: cascade main → ${BRANCH} (${SHORT_SHA}) [skip ci]\",
|
||||||
|
\"body\": \"## Automatic cascade\\n\\nForward-merging \`main\` (${SHORT_SHA}) into \`${BRANCH}\`.\\n\\nIf conflicts exist, resolve manually and merge.\\n\\n> Auto-created by **Cascade Main → Dev**.\",
|
||||||
|
\"head\": \"main\",
|
||||||
|
\"base\": \"${BRANCH}\"
|
||||||
|
}" \
|
||||||
|
"${API}/pulls")
|
||||||
|
|
||||||
|
HTTP_CODE=$(echo "$PR_RESPONSE" | tail -1)
|
||||||
|
BODY=$(echo "$PR_RESPONSE" | sed '$d')
|
||||||
|
PR_NUMBER=$(echo "$BODY" | jq -r '.number // empty')
|
||||||
|
|
||||||
|
if [ "$HTTP_CODE" != "201" ] || [ -z "$PR_NUMBER" ]; then
|
||||||
|
MSG=$(echo "$BODY" | jq -r '.message // .' 2>/dev/null | head -1)
|
||||||
|
echo " ❌ Failed to create PR (HTTP ${HTTP_CODE}): ${MSG}"
|
||||||
|
FAILED=$((FAILED + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " ✅ Created PR #${PR_NUMBER}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Try auto-merge
|
||||||
|
PR_DATA=$(curl -sS \
|
||||||
|
-H "Authorization: token ${GA_TOKEN}" \
|
||||||
|
"${API}/pulls/${PR_NUMBER}")
|
||||||
|
|
||||||
|
MERGEABLE=$(echo "$PR_DATA" | jq -r '.mergeable // false')
|
||||||
|
|
||||||
|
if [ "$MERGEABLE" != "true" ]; then
|
||||||
|
echo " ⚠️ Conflicts — PR #${PR_NUMBER} left open"
|
||||||
|
CONFLICTS=$((CONFLICTS + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
MERGE_RESPONSE=$(curl -sS -w "\n%{http_code}" \
|
||||||
|
-X POST \
|
||||||
|
-H "Authorization: token ${GA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{
|
||||||
|
\"Do\": \"merge\",
|
||||||
|
\"merge_message_field\": \"chore: cascade main → ${BRANCH} [skip ci]\",
|
||||||
|
\"delete_branch_after_merge\": false
|
||||||
|
}" \
|
||||||
|
"${API}/pulls/${PR_NUMBER}/merge")
|
||||||
|
|
||||||
|
MERGE_HTTP=$(echo "$MERGE_RESPONSE" | tail -1)
|
||||||
|
|
||||||
|
if [ "$MERGE_HTTP" = "200" ] || [ "$MERGE_HTTP" = "204" ]; then
|
||||||
|
echo " ✅ Merged — ${BRANCH} is in sync"
|
||||||
|
SUCCESS=$((SUCCESS + 1))
|
||||||
|
else
|
||||||
|
MERGE_BODY=$(echo "$MERGE_RESPONSE" | sed '$d')
|
||||||
|
echo " ⚠️ Merge failed (HTTP ${MERGE_HTTP}) — PR #${PR_NUMBER} left open"
|
||||||
|
CONFLICTS=$((CONFLICTS + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
echo ""
|
||||||
|
echo "════════════════════════════════════════"
|
||||||
|
echo " ✅ Merged: ${SUCCESS}"
|
||||||
|
echo " ⚠️ Conflicts: ${CONFLICTS}"
|
||||||
|
echo " ⏭️ Up to date: ${SKIPPED}"
|
||||||
|
echo " ❌ Failed: ${FAILED}"
|
||||||
|
echo "════════════════════════════════════════"
|
||||||
|
|
||||||
|
if [ "$FAILED" -gt 0 ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -5,13 +5,12 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: GitHub.Workflow.Template
|
# DEFGROUP: Gitea.Workflow.Template
|
||||||
# INGROUP: MokoStandards.CI
|
# INGROUP: MokoStandards.CI
|
||||||
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
||||||
# PATH: /templates/workflows/joomla/ci-joomla.yml.template
|
# PATH: /templates/workflows/joomla/ci-joomla.yml.template
|
||||||
# VERSION: 04.06.00
|
# VERSION: 04.06.00
|
||||||
# BRIEF: CI workflow for Joomla extensions — lint, validate, test
|
# BRIEF: CI workflow for Joomla extensions — lint, validate, test
|
||||||
# NOTE: Deployed to .github/workflows/ci-joomla.yml in governed Joomla extension repos.
|
|
||||||
|
|
||||||
name: Joomla Extension CI
|
name: Joomla Extension CI
|
||||||
|
|
||||||
@@ -39,24 +38,22 @@ jobs:
|
|||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
- name: Setup PHP
|
- name: Setup PHP
|
||||||
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0
|
run: |
|
||||||
with:
|
php -v && composer --version
|
||||||
php-version: '8.2'
|
|
||||||
extensions: mbstring, xml, zip, gd, curl, json, simplexml
|
|
||||||
tools: composer:v2
|
|
||||||
coverage: none
|
|
||||||
|
|
||||||
- name: Clone MokoStandards
|
- name: Clone MokoStandards
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
|
GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
|
||||||
|
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
|
||||||
|
MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
|
||||||
run: |
|
run: |
|
||||||
git clone --depth 1 --branch version/04 --quiet \
|
git clone --depth 1 --branch main --quiet \
|
||||||
"https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \
|
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
|
||||||
/tmp/mokostandards
|
/tmp/mokostandards-api
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
env:
|
env:
|
||||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}'
|
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
|
||||||
run: |
|
run: |
|
||||||
if [ -f "composer.json" ]; then
|
if [ -f "composer.json" ]; then
|
||||||
composer install \
|
composer install \
|
||||||
@@ -344,16 +341,12 @@ jobs:
|
|||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
- name: Setup PHP ${{ matrix.php }}
|
- name: Setup PHP ${{ matrix.php }}
|
||||||
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0
|
run: |
|
||||||
with:
|
php -v && composer --version
|
||||||
php-version: ${{ matrix.php }}
|
|
||||||
extensions: mbstring, xml, zip, gd, curl, json, simplexml
|
|
||||||
tools: composer:v2
|
|
||||||
coverage: none
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
env:
|
env:
|
||||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}'
|
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
|
||||||
run: |
|
run: |
|
||||||
if [ -f "composer.json" ]; then
|
if [ -f "composer.json" ]; then
|
||||||
composer install \
|
composer install \
|
||||||
@@ -382,3 +375,76 @@ jobs:
|
|||||||
else
|
else
|
||||||
echo "No phpunit.xml found — skipping tests." >> $GITHUB_STEP_SUMMARY
|
echo "No phpunit.xml found — skipping tests." >> $GITHUB_STEP_SUMMARY
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
static-analysis:
|
||||||
|
name: PHPStan Analysis
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: lint-and-validate
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
run: php -v && composer --version
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
env:
|
||||||
|
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
|
||||||
|
run: |
|
||||||
|
if [ -f "composer.json" ]; then
|
||||||
|
composer install --no-interaction --prefer-dist --optimize-autoloader
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Install PHPStan
|
||||||
|
run: |
|
||||||
|
if ! command -v vendor/bin/phpstan &> /dev/null; then
|
||||||
|
composer require --dev phpstan/phpstan --no-interaction 2>/dev/null || \
|
||||||
|
composer global require phpstan/phpstan --no-interaction
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Run PHPStan
|
||||||
|
run: |
|
||||||
|
echo "### PHPStan Static Analysis" >> $GITHUB_STEP_SUMMARY
|
||||||
|
PHPSTAN="vendor/bin/phpstan"
|
||||||
|
if [ ! -f "$PHPSTAN" ]; then
|
||||||
|
PHPSTAN=$(composer global config bin-dir --absolute 2>/dev/null)/phpstan
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Determine source directory
|
||||||
|
SRC_DIR=""
|
||||||
|
for DIR in src/ htdocs/ lib/; do
|
||||||
|
if [ -d "$DIR" ]; then
|
||||||
|
SRC_DIR="$DIR"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$SRC_DIR" ]; then
|
||||||
|
echo "No source directory found (src/, htdocs/, lib/) — skipping." >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use repo phpstan.neon if present, otherwise use baseline config
|
||||||
|
ARGS="analyse ${SRC_DIR} --memory-limit=512M --no-progress --error-format=table"
|
||||||
|
if [ -f "phpstan.neon" ] || [ -f "phpstan.neon.dist" ]; then
|
||||||
|
echo "Using project PHPStan config." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
ARGS="$ARGS --level=3"
|
||||||
|
echo "No phpstan.neon found — using level 3 (type inference)." >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
$PHPSTAN $ARGS 2>&1 | tee /tmp/phpstan-output.txt
|
||||||
|
EXIT=${PIPESTATUS[0]}
|
||||||
|
|
||||||
|
if [ $EXIT -eq 0 ]; then
|
||||||
|
echo "**No errors found.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
ERRORS=$(grep -c "ERROR" /tmp/phpstan-output.txt 2>/dev/null || echo "some")
|
||||||
|
echo "**${ERRORS} error(s) found.** Review output above." >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
|
tail -30 /tmp/phpstan-output.txt >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
exit $EXIT
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: MokoStandards.Maintenance
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
||||||
|
# PATH: /.gitea/workflows/cleanup.yml
|
||||||
|
# VERSION: 01.00.00
|
||||||
|
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
|
||||||
|
|
||||||
|
name: Repository Cleanup
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 3 * * 0' # Weekly on Sunday at 03:00 UTC
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
env:
|
||||||
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cleanup:
|
||||||
|
name: Clean Merged Branches
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.GA_TOKEN }}
|
||||||
|
|
||||||
|
- name: Delete merged branches
|
||||||
|
env:
|
||||||
|
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||||
|
run: |
|
||||||
|
echo "=== Merged Branch Cleanup ==="
|
||||||
|
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||||
|
|
||||||
|
# List branches via API
|
||||||
|
BRANCHES=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
|
||||||
|
"${API}/branches?limit=50" | jq -r '.[].name')
|
||||||
|
|
||||||
|
DELETED=0
|
||||||
|
for BRANCH in $BRANCHES; do
|
||||||
|
# Skip protected branches
|
||||||
|
case "$BRANCH" in
|
||||||
|
main|master|develop|release/*|hotfix/*) continue ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Check if branch is merged into main
|
||||||
|
if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then
|
||||||
|
echo " Deleting merged branch: ${BRANCH}"
|
||||||
|
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
|
||||||
|
"${API}/branches/${BRANCH}" 2>/dev/null || true
|
||||||
|
DELETED=$((DELETED + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Deleted ${DELETED} merged branch(es)"
|
||||||
|
|
||||||
|
- name: Clean old workflow runs
|
||||||
|
env:
|
||||||
|
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||||
|
run: |
|
||||||
|
echo "=== Workflow Run Cleanup ==="
|
||||||
|
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||||
|
CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ)
|
||||||
|
|
||||||
|
# Get old completed runs
|
||||||
|
RUNS=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
|
||||||
|
"${API}/actions/runs?status=completed&limit=50" | \
|
||||||
|
jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null)
|
||||||
|
|
||||||
|
DELETED=0
|
||||||
|
for RUN_ID in $RUNS; do
|
||||||
|
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
|
||||||
|
"${API}/actions/runs/${RUN_ID}" 2>/dev/null || true
|
||||||
|
DELETED=$((DELETED + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Deleted ${DELETED} old workflow run(s)"
|
||||||
@@ -3,14 +3,12 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: GitHub.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: MokoStandards.Deploy
|
# INGROUP: MokoStandards.Deploy
|
||||||
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
|
||||||
# PATH: /templates/workflows/joomla/deploy-manual.yml.template
|
# PATH: /templates/workflows/joomla/deploy-manual.yml.template
|
||||||
# VERSION: 04.06.00
|
# VERSION: 04.07.00
|
||||||
# BRIEF: Manual SFTP deploy to dev server for Joomla repos
|
# BRIEF: Manual SFTP deploy to dev server for Joomla repos
|
||||||
# NOTE: Joomla repos use update.xml for distribution. This is for manual
|
|
||||||
# dev server testing only — triggered via workflow_dispatch.
|
|
||||||
|
|
||||||
name: Deploy to Dev (Manual)
|
name: Deploy to Dev (Manual)
|
||||||
|
|
||||||
@@ -39,23 +37,21 @@ jobs:
|
|||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
- name: Setup PHP
|
- name: Setup PHP
|
||||||
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0
|
run: |
|
||||||
with:
|
php -v && composer --version
|
||||||
php-version: '8.2'
|
|
||||||
extensions: json, ssh2
|
|
||||||
tools: composer
|
|
||||||
coverage: none
|
|
||||||
|
|
||||||
- name: Setup MokoStandards tools
|
- name: Setup MokoStandards tools
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
|
GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
|
||||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}'
|
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
|
||||||
|
MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
|
||||||
|
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
|
||||||
run: |
|
run: |
|
||||||
git clone --depth 1 --branch version/04 --quiet \
|
git clone --depth 1 --branch main --quiet \
|
||||||
"https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \
|
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
|
||||||
/tmp/mokostandards 2>/dev/null || true
|
/tmp/mokostandards-api 2>/dev/null || true
|
||||||
if [ -d "/tmp/mokostandards" ] && [ -f "/tmp/mokostandards/composer.json" ]; then
|
if [ -d "/tmp/mokostandards-api" ] && [ -f "/tmp/mokostandards-api/composer.json" ]; then
|
||||||
cd /tmp/mokostandards && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
|
cd /tmp/mokostandards-api && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Check FTP configuration
|
- name: Check FTP configuration
|
||||||
@@ -63,11 +59,10 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
HOST: ${{ vars.DEV_FTP_HOST }}
|
HOST: ${{ vars.DEV_FTP_HOST }}
|
||||||
PATH_VAR: ${{ vars.DEV_FTP_PATH }}
|
PATH_VAR: ${{ vars.DEV_FTP_PATH }}
|
||||||
SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
|
|
||||||
PORT: ${{ vars.DEV_FTP_PORT }}
|
PORT: ${{ vars.DEV_FTP_PORT }}
|
||||||
run: |
|
run: |
|
||||||
if [ -z "$HOST" ] || [ -z "$PATH_VAR" ]; then
|
if [ -z "$HOST" ] || [ -z "$PATH_VAR" ]; then
|
||||||
echo "DEV_FTP_HOST or DEV_FTP_PATH not configured — cannot deploy"
|
echo "DEV_FTP_HOST or DEV_FTP_PATH not configured -- cannot deploy"
|
||||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
@@ -75,7 +70,6 @@ jobs:
|
|||||||
echo "host=$HOST" >> "$GITHUB_OUTPUT"
|
echo "host=$HOST" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
REMOTE="${PATH_VAR%/}"
|
REMOTE="${PATH_VAR%/}"
|
||||||
[ -n "$SUFFIX" ] && REMOTE="${REMOTE}/${SUFFIX#/}"
|
|
||||||
echo "remote=$REMOTE" >> "$GITHUB_OUTPUT"
|
echo "remote=$REMOTE" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
[ -z "$PORT" ] && PORT="22"
|
[ -z "$PORT" ] && PORT="22"
|
||||||
@@ -90,7 +84,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
SOURCE_DIR="src"
|
SOURCE_DIR="src"
|
||||||
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
||||||
[ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ — nothing to deploy"; exit 0; }
|
[ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ -- nothing to deploy"; exit 0; }
|
||||||
|
|
||||||
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
|
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
|
||||||
"${{ steps.check.outputs.host }}" "${{ steps.check.outputs.port }}" "$SFTP_USER" "${{ steps.check.outputs.remote }}" \
|
"${{ steps.check.outputs.host }}" "${{ steps.check.outputs.port }}" "$SFTP_USER" "${{ steps.check.outputs.remote }}" \
|
||||||
@@ -107,11 +101,11 @@ jobs:
|
|||||||
DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json)
|
DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json)
|
||||||
[ "${{ inputs.clear_remote }}" = "true" ] && DEPLOY_ARGS+=(--clear-remote)
|
[ "${{ inputs.clear_remote }}" = "true" ] && DEPLOY_ARGS+=(--clear-remote)
|
||||||
|
|
||||||
PLATFORM=$(php /tmp/mokostandards/api/cli/platform_detect.php --path . 2>/dev/null || true)
|
PLATFORM=$(php /tmp/mokostandards-api/cli/platform_detect.php --path . 2>/dev/null || true)
|
||||||
if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards/api/deploy/deploy-joomla.php" ]; then
|
if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards-api/deploy/deploy-joomla.php" ]; then
|
||||||
php /tmp/mokostandards/api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}"
|
php /tmp/mokostandards-api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}"
|
||||||
else
|
else
|
||||||
php /tmp/mokostandards/api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}"
|
php /tmp/mokostandards-api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
rm -f /tmp/deploy_key /tmp/sftp-config.json
|
rm -f /tmp/deploy_key /tmp/sftp-config.json
|
||||||
@@ -120,7 +114,7 @@ jobs:
|
|||||||
if: always()
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
if [ "${{ steps.check.outputs.skip }}" = "true" ]; then
|
if [ "${{ steps.check.outputs.skip }}" = "true" ]; then
|
||||||
echo "### Deploy Skipped — FTP not configured" >> $GITHUB_STEP_SUMMARY
|
echo "### Deploy Skipped -- FTP not configured" >> $GITHUB_STEP_SUMMARY
|
||||||
else
|
else
|
||||||
echo "### Manual Dev Deploy Complete" >> $GITHUB_STEP_SUMMARY
|
echo "### Manual Dev Deploy Complete" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
---
|
||||||
|
name: Documentation Issue
|
||||||
|
about: Report an issue with documentation
|
||||||
|
title: '[DOCS] '
|
||||||
|
labels: 'documentation'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## Documentation Issue
|
||||||
|
|
||||||
|
**Location**:
|
||||||
|
<!-- Specify the file, page, or section with the issue -->
|
||||||
|
|
||||||
|
## Issue Type
|
||||||
|
<!-- Mark the relevant option with an "x" -->
|
||||||
|
- [ ] Typo or grammar error
|
||||||
|
- [ ] Outdated information
|
||||||
|
- [ ] Missing documentation
|
||||||
|
- [ ] Unclear explanation
|
||||||
|
- [ ] Broken links
|
||||||
|
- [ ] Missing examples
|
||||||
|
- [ ] Other (specify below)
|
||||||
|
|
||||||
|
## Description
|
||||||
|
<!-- Clearly describe the documentation issue -->
|
||||||
|
|
||||||
|
## Current Content
|
||||||
|
<!-- Quote or describe the current documentation (if applicable) -->
|
||||||
|
```
|
||||||
|
Current text here
|
||||||
|
```
|
||||||
|
|
||||||
|
## Suggested Improvement
|
||||||
|
<!-- Provide your suggestion for how to improve the documentation -->
|
||||||
|
```
|
||||||
|
Suggested text here
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
<!-- Add any other context, screenshots, or references -->
|
||||||
|
|
||||||
|
## Standards Alignment
|
||||||
|
- [ ] Follows MokoStandards documentation guidelines
|
||||||
|
- [ ] Uses en_US/en_GB localization
|
||||||
|
- [ ] Includes proper SPDX headers where applicable
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
- [ ] I have searched for similar documentation issues
|
||||||
|
- [ ] I have provided a clear description
|
||||||
|
- [ ] I have suggested an improvement (if applicable)
|
||||||
@@ -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,87 @@
|
|||||||
|
---
|
||||||
|
name: Joomla Extension Issue
|
||||||
|
about: Report an issue with a Joomla extension
|
||||||
|
title: '[JOOMLA] '
|
||||||
|
labels: 'joomla'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## Issue Type
|
||||||
|
- [ ] Component Issue
|
||||||
|
- [ ] Module Issue
|
||||||
|
- [ ] Plugin Issue
|
||||||
|
- [ ] Template Issue
|
||||||
|
|
||||||
|
## Extension Details
|
||||||
|
- **Extension Name**: [e.g., moko-cassiopeia]
|
||||||
|
- **Extension Version**: [e.g., 1.2.3]
|
||||||
|
- **Extension Type**: [Component / Module / Plugin / Template]
|
||||||
|
|
||||||
|
## Joomla Environment
|
||||||
|
- **Joomla Version**: [e.g., 4.4.0, 5.0.0]
|
||||||
|
- **PHP Version**: [e.g., 8.1.0]
|
||||||
|
- **Database**: [MySQL / PostgreSQL / MariaDB]
|
||||||
|
- **Database Version**: [e.g., 8.0]
|
||||||
|
- **Server**: [Apache / Nginx / IIS]
|
||||||
|
- **Hosting**: [Shared / VPS / Dedicated / Cloud]
|
||||||
|
|
||||||
|
## Issue Description
|
||||||
|
Provide a clear and detailed description of the issue.
|
||||||
|
|
||||||
|
## Steps to Reproduce
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '...'
|
||||||
|
3. Configure '...'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
What you expected to happen.
|
||||||
|
|
||||||
|
## Actual Behavior
|
||||||
|
What actually happened.
|
||||||
|
|
||||||
|
## Error Messages
|
||||||
|
```
|
||||||
|
# Paste any error messages from Joomla error logs
|
||||||
|
# Location: administrator/logs/error.php
|
||||||
|
```
|
||||||
|
|
||||||
|
## Browser Console Errors
|
||||||
|
```javascript
|
||||||
|
// Paste any JavaScript console errors (F12 in browser)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
Add screenshots to help explain the issue.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
```ini
|
||||||
|
# Paste extension configuration (sanitize sensitive data)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installed Extensions
|
||||||
|
List other installed extensions that might conflict:
|
||||||
|
- Extension 1 (version)
|
||||||
|
- Extension 2 (version)
|
||||||
|
|
||||||
|
## Template Overrides
|
||||||
|
- [ ] Using template overrides
|
||||||
|
- [ ] Custom CSS
|
||||||
|
- [ ] Custom JavaScript
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
- **Multilingual Site**: [Yes / No]
|
||||||
|
- **Cache Enabled**: [Yes / No]
|
||||||
|
- **Debug Mode**: [Yes / No]
|
||||||
|
- **SEF URLs**: [Yes / No]
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
- [ ] I have cleared Joomla cache
|
||||||
|
- [ ] I have disabled other extensions to test for conflicts
|
||||||
|
- [ ] I have checked Joomla error logs
|
||||||
|
- [ ] I have tested with a default Joomla template
|
||||||
|
- [ ] I have checked browser console for JavaScript errors
|
||||||
|
- [ ] I have searched for similar issues
|
||||||
|
- [ ] I am using a supported Joomla version
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
Moko Platform Repository Manifest
|
||||||
|
See: https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home
|
||||||
|
-->
|
||||||
|
<moko-platform xmlns="https://standards.mokoconsulting.tech/moko-platform/1.0" schema-version="1.0">
|
||||||
|
<identity>
|
||||||
|
<name>MokoWaaS</name>
|
||||||
|
<org>MokoConsulting</org>
|
||||||
|
<description>White-label identity, security hardening, and tenant restriction layer for WaaS-managed Joomla environments</description>
|
||||||
|
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
|
||||||
|
</identity>
|
||||||
|
<governance>
|
||||||
|
<platform>joomla</platform>
|
||||||
|
<standards-version>05.00.00</standards-version>
|
||||||
|
<standards-source>https://git.mokoconsulting.tech/MokoConsulting/moko-platform</standards-source>
|
||||||
|
<last-synced>2026-05-21T20:48:00+00:00</last-synced>
|
||||||
|
</governance>
|
||||||
|
<build>
|
||||||
|
<language>PHP</language>
|
||||||
|
<package-type>package</package-type>
|
||||||
|
<entry-point>src/</entry-point>
|
||||||
|
</build>
|
||||||
|
</moko-platform>
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: MokoStandards.Notifications
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
||||||
|
# PATH: /.gitea/workflows/notify.yml
|
||||||
|
# VERSION: 01.00.00
|
||||||
|
# BRIEF: Push notifications via ntfy on release success or workflow failure
|
||||||
|
|
||||||
|
name: Notifications
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows:
|
||||||
|
- "Joomla Build & Release"
|
||||||
|
- "Joomla Extension CI"
|
||||||
|
- "Deploy"
|
||||||
|
- "Cascade Main → Dev"
|
||||||
|
types:
|
||||||
|
- completed
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
env:
|
||||||
|
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
|
||||||
|
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-releases' }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
notify:
|
||||||
|
name: Send Notification
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: >-
|
||||||
|
github.event.workflow_run.conclusion == 'success' ||
|
||||||
|
github.event.workflow_run.conclusion == 'failure'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Notify on success (releases only)
|
||||||
|
if: >-
|
||||||
|
github.event.workflow_run.conclusion == 'success' &&
|
||||||
|
contains(github.event.workflow_run.name, 'Release')
|
||||||
|
run: |
|
||||||
|
REPO="${{ github.event.repository.name }}"
|
||||||
|
WORKFLOW="${{ github.event.workflow_run.name }}"
|
||||||
|
URL="${{ github.event.workflow_run.html_url }}"
|
||||||
|
|
||||||
|
curl -sS \
|
||||||
|
-H "Title: ${REPO} released" \
|
||||||
|
-H "Tags: white_check_mark,package" \
|
||||||
|
-H "Priority: default" \
|
||||||
|
-H "Click: ${URL}" \
|
||||||
|
-d "${WORKFLOW} completed successfully." \
|
||||||
|
"${NTFY_URL}/${NTFY_TOPIC}"
|
||||||
|
|
||||||
|
- name: Notify on failure
|
||||||
|
if: github.event.workflow_run.conclusion == 'failure'
|
||||||
|
run: |
|
||||||
|
REPO="${{ github.event.repository.name }}"
|
||||||
|
WORKFLOW="${{ github.event.workflow_run.name }}"
|
||||||
|
URL="${{ github.event.workflow_run.html_url }}"
|
||||||
|
|
||||||
|
curl -sS \
|
||||||
|
-H "Title: ${REPO} workflow failed" \
|
||||||
|
-H "Tags: x,warning" \
|
||||||
|
-H "Priority: high" \
|
||||||
|
-H "Click: ${URL}" \
|
||||||
|
-d "${WORKFLOW} failed. Check the run for details." \
|
||||||
|
"${NTFY_URL}/${NTFY_TOPIC}"
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: MokoStandards.CI
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
||||||
|
# PATH: /.gitea/workflows/pr-check.yml
|
||||||
|
# VERSION: 01.00.00
|
||||||
|
# BRIEF: PR gate — validates code quality and manifest before merge to main
|
||||||
|
|
||||||
|
name: PR Check
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
types: [opened, synchronize, reopened]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
validate:
|
||||||
|
name: Validate PR
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
run: |
|
||||||
|
if ! command -v php &> /dev/null; then
|
||||||
|
sudo apt-get update -qq
|
||||||
|
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: PHP syntax check
|
||||||
|
run: |
|
||||||
|
echo "=== PHP Lint ==="
|
||||||
|
ERRORS=0
|
||||||
|
while IFS= read -r -d '' file; do
|
||||||
|
if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0)
|
||||||
|
echo "Checked files, errors: ${ERRORS}"
|
||||||
|
[ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
|
||||||
|
|
||||||
|
- name: Validate Joomla manifest
|
||||||
|
run: |
|
||||||
|
echo "=== Manifest Validation ==="
|
||||||
|
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
||||||
|
if [ -z "$MANIFEST" ]; then
|
||||||
|
echo "::warning::No Joomla manifest found"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "Manifest: ${MANIFEST}"
|
||||||
|
|
||||||
|
# Check well-formed XML
|
||||||
|
if ! php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}"; then
|
||||||
|
echo "::error::Manifest XML is malformed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check required elements
|
||||||
|
for ELEMENT in name version description; do
|
||||||
|
if ! grep -q "<${ELEMENT}>" "$MANIFEST"; then
|
||||||
|
echo "::error::Missing <${ELEMENT}> in manifest"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "Manifest valid"
|
||||||
|
|
||||||
|
- name: Check updates.xml format
|
||||||
|
run: |
|
||||||
|
if [ ! -f "updates.xml" ]; then
|
||||||
|
echo "No updates.xml — skipping"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "=== updates.xml Validation ==="
|
||||||
|
if ! php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}"; then
|
||||||
|
echo "::error::updates.xml is malformed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "updates.xml valid"
|
||||||
|
|
||||||
|
- name: Verify package builds
|
||||||
|
run: |
|
||||||
|
echo "=== Package Build Test ==="
|
||||||
|
SOURCE_DIR="src"
|
||||||
|
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
||||||
|
if [ ! -d "$SOURCE_DIR" ]; then
|
||||||
|
echo "::warning::No src/ or htdocs/ directory"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
# Dry-run: ensure zip would succeed
|
||||||
|
FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
|
||||||
|
echo "Source contains ${FILE_COUNT} files — package will build"
|
||||||
|
[ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; }
|
||||||
@@ -0,0 +1,341 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: MokoStandards.Release
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
||||||
|
# PATH: /.gitea/workflows/pre-release.yml
|
||||||
|
# VERSION: 01.00.00
|
||||||
|
# BRIEF: Manual pre-release — builds dev/alpha/beta/rc packages from any branch
|
||||||
|
|
||||||
|
name: Pre-Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
stability:
|
||||||
|
description: 'Pre-release channel'
|
||||||
|
required: true
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- development
|
||||||
|
- alpha
|
||||||
|
- beta
|
||||||
|
- release-candidate
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
env:
|
||||||
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||||||
|
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: "Build Pre-Release (${{ inputs.stability }})"
|
||||||
|
runs-on: release
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.GA_TOKEN }}
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
run: |
|
||||||
|
if ! command -v php &> /dev/null; then
|
||||||
|
sudo apt-get update -qq
|
||||||
|
sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Resolve metadata
|
||||||
|
id: meta
|
||||||
|
run: |
|
||||||
|
STABILITY="${{ inputs.stability }}"
|
||||||
|
|
||||||
|
case "$STABILITY" in
|
||||||
|
development) SUFFIX="-dev"; TAG="development" ;;
|
||||||
|
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
|
||||||
|
beta) SUFFIX="-beta"; TAG="beta" ;;
|
||||||
|
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Read and bump patch version (with rollover)
|
||||||
|
CURRENT=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1)
|
||||||
|
[ -z "$CURRENT" ] && CURRENT="00.00.00"
|
||||||
|
|
||||||
|
MAJOR=$(echo "$CURRENT" | cut -d. -f1)
|
||||||
|
MINOR=$(echo "$CURRENT" | cut -d. -f2)
|
||||||
|
PATCH=$(echo "$CURRENT" | cut -d. -f3)
|
||||||
|
|
||||||
|
# Patch bump with rollover: ZZ=99 → bump minor, YY=99 → bump major
|
||||||
|
NEW_PATCH=$((10#$PATCH + 1))
|
||||||
|
NEW_MINOR=$((10#$MINOR))
|
||||||
|
NEW_MAJOR=$((10#$MAJOR))
|
||||||
|
|
||||||
|
if [ $NEW_PATCH -gt 99 ]; then
|
||||||
|
NEW_PATCH=0
|
||||||
|
NEW_MINOR=$((NEW_MINOR + 1))
|
||||||
|
fi
|
||||||
|
if [ $NEW_MINOR -gt 99 ]; then
|
||||||
|
NEW_MINOR=0
|
||||||
|
NEW_MAJOR=$((NEW_MAJOR + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
VERSION=$(printf "%02d.%02d.%02d" $NEW_MAJOR $NEW_MINOR $NEW_PATCH)
|
||||||
|
TODAY=$(date +%Y-%m-%d)
|
||||||
|
|
||||||
|
echo "Bumping: ${CURRENT} → ${VERSION} (patch)"
|
||||||
|
|
||||||
|
# Update README.md
|
||||||
|
sed -i "s/VERSION:[[:space:]]*${CURRENT}/VERSION: ${VERSION}/" README.md
|
||||||
|
|
||||||
|
# Update manifest
|
||||||
|
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
||||||
|
if [ -n "$MANIFEST" ]; then
|
||||||
|
MANIFEST_VER=$(sed -n 's/.*<version>\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1)
|
||||||
|
sed -i "s|<version>${MANIFEST_VER}</version>|<version>${VERSION}</version>|" "$MANIFEST"
|
||||||
|
sed -i "s|<creationDate>[^<]*</creationDate>|<creationDate>${TODAY}</creationDate>|" "$MANIFEST"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Commit version bump
|
||||||
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
|
git config --local user.name "gitea-actions[bot]"
|
||||||
|
git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||||
|
git add -A
|
||||||
|
git diff --cached --quiet || {
|
||||||
|
git commit -m "chore(version): bump ${CURRENT} → ${VERSION} [skip ci]"
|
||||||
|
git push origin HEAD 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Auto-detect element from manifest
|
||||||
|
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
||||||
|
EXT_ELEMENT=""
|
||||||
|
if [ -n "$MANIFEST" ]; then
|
||||||
|
EXT_ELEMENT=$(sed -n 's/.*<element>\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
|
||||||
|
if [ -z "$EXT_ELEMENT" ]; then
|
||||||
|
EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]')
|
||||||
|
case "$EXT_ELEMENT" in
|
||||||
|
templatedetails|manifest) EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
|
||||||
|
fi
|
||||||
|
|
||||||
|
ZIP_NAME="${EXT_ELEMENT}-${VERSION}${SUFFIX}.zip"
|
||||||
|
|
||||||
|
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
|
||||||
|
|
||||||
|
- name: Build package
|
||||||
|
run: |
|
||||||
|
SOURCE_DIR="src"
|
||||||
|
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
||||||
|
if [ ! -d "$SOURCE_DIR" ]; then
|
||||||
|
echo "::error::No src/ or htdocs/ directory"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p build/package
|
||||||
|
rsync -a \
|
||||||
|
--exclude='sftp-config*' \
|
||||||
|
--exclude='.ftpignore' \
|
||||||
|
--exclude='*.ppk' \
|
||||||
|
--exclude='*.pem' \
|
||||||
|
--exclude='*.key' \
|
||||||
|
--exclude='.env*' \
|
||||||
|
--exclude='*.local' \
|
||||||
|
--exclude='.build-trigger' \
|
||||||
|
"${SOURCE_DIR}/" build/package/
|
||||||
|
|
||||||
|
- name: Create ZIP
|
||||||
|
id: zip
|
||||||
|
run: |
|
||||||
|
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
|
||||||
|
cd build/package
|
||||||
|
zip -r "../${ZIP_NAME}" .
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
SHA256=$(sha256sum "${ZIP_NAME}" | cut -d' ' -f1)
|
||||||
|
echo "sha256=${SHA256}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "ZIP: ${ZIP_NAME} (SHA: ${SHA256:0:16}...)"
|
||||||
|
|
||||||
|
- name: Create or replace Gitea release
|
||||||
|
id: release
|
||||||
|
run: |
|
||||||
|
TAG="${{ steps.meta.outputs.tag }}"
|
||||||
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
|
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||||
|
SHA256="${{ steps.zip.outputs.sha256 }}"
|
||||||
|
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
|
||||||
|
EXT_ELEMENT="${{ steps.meta.outputs.ext_element }}"
|
||||||
|
TOKEN="${{ secrets.GA_TOKEN }}"
|
||||||
|
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
BRANCH=$(git branch --show-current)
|
||||||
|
|
||||||
|
BODY="## ${VERSION} ($(date +%Y-%m-%d))
|
||||||
|
**Channel:** ${STABILITY}
|
||||||
|
**SHA-256:** \`${SHA256}\`"
|
||||||
|
|
||||||
|
# Delete existing release
|
||||||
|
EXISTING_ID=$(curl -sS -H "Authorization: token ${TOKEN}" \
|
||||||
|
"${API}/releases/tags/${TAG}" | jq -r '.id // empty' 2>/dev/null)
|
||||||
|
if [ -n "$EXISTING_ID" ]; then
|
||||||
|
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
|
||||||
|
"${API}/releases/${EXISTING_ID}" 2>/dev/null || true
|
||||||
|
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
|
||||||
|
"${API}/tags/${TAG}" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create release
|
||||||
|
RELEASE_ID=$(curl -sS -X POST -H "Authorization: token ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API}/releases" \
|
||||||
|
-d "$(jq -n \
|
||||||
|
--arg tag "$TAG" \
|
||||||
|
--arg target "$BRANCH" \
|
||||||
|
--arg name "${EXT_ELEMENT} ${VERSION} (${STABILITY})" \
|
||||||
|
--arg body "$BODY" \
|
||||||
|
'{tag_name: $tag, target_commitish: $target, name: $name, body: $body, prerelease: true}'
|
||||||
|
)" | jq -r '.id')
|
||||||
|
|
||||||
|
echo "release_id=${RELEASE_ID}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
# Upload ZIP
|
||||||
|
curl -sS -X POST -H "Authorization: token ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/octet-stream" \
|
||||||
|
"${API}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" \
|
||||||
|
--data-binary "@build/${ZIP_NAME}"
|
||||||
|
|
||||||
|
echo "Released: ${EXT_ELEMENT} ${VERSION} (${STABILITY})"
|
||||||
|
|
||||||
|
- name: Update updates.xml
|
||||||
|
run: |
|
||||||
|
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||||
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
|
SHA256="${{ steps.zip.outputs.sha256 }}"
|
||||||
|
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
|
||||||
|
TAG="${{ steps.meta.outputs.tag }}"
|
||||||
|
DATE=$(date +%Y-%m-%d)
|
||||||
|
|
||||||
|
if [ ! -f "updates.xml" ]; then
|
||||||
|
echo "No updates.xml — skipping"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
export PY_STABILITY="$STABILITY" PY_VERSION="$VERSION" PY_SHA256="$SHA256" \
|
||||||
|
PY_ZIP_NAME="$ZIP_NAME" PY_TAG="$TAG" PY_DATE="$DATE" \
|
||||||
|
PY_GITEA_ORG="$GITEA_ORG" PY_GITEA_REPO="$GITEA_REPO"
|
||||||
|
python3 << 'PYEOF'
|
||||||
|
import re, os
|
||||||
|
|
||||||
|
stability = os.environ["PY_STABILITY"]
|
||||||
|
version = os.environ["PY_VERSION"]
|
||||||
|
sha256 = os.environ["PY_SHA256"]
|
||||||
|
zip_name = os.environ["PY_ZIP_NAME"]
|
||||||
|
tag = os.environ["PY_TAG"]
|
||||||
|
date = os.environ["PY_DATE"]
|
||||||
|
gitea_org = os.environ["PY_GITEA_ORG"]
|
||||||
|
gitea_repo = os.environ["PY_GITEA_REPO"]
|
||||||
|
download_url = f"https://git.mokoconsulting.tech/{gitea_org}/{gitea_repo}/releases/download/{tag}/{zip_name}"
|
||||||
|
|
||||||
|
with open("updates.xml", "r") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Map stability to XML tag name
|
||||||
|
tag_map = {"development": "development", "alpha": "alpha", "beta": "beta", "release-candidate": "rc"}
|
||||||
|
xml_tag = tag_map.get(stability, stability)
|
||||||
|
|
||||||
|
pattern = r"(<update>(?:(?!</update>).)*?<tag>" + re.escape(xml_tag) + r"</tag>.*?</update>)"
|
||||||
|
match = re.search(pattern, content, re.DOTALL)
|
||||||
|
if match:
|
||||||
|
block = match.group(1)
|
||||||
|
updated = re.sub(r"<version>[^<]*</version>", f"<version>{version}</version>", block)
|
||||||
|
updated = re.sub(r"<creationDate>[^<]*</creationDate>", f"<creationDate>{date}</creationDate>", updated)
|
||||||
|
if "<sha256>" in updated:
|
||||||
|
updated = re.sub(r"<sha256>[^<]*</sha256>", f"<sha256>{sha256}</sha256>", updated)
|
||||||
|
else:
|
||||||
|
updated = updated.replace("</downloads>", f"</downloads>\n <sha256>{sha256}</sha256>")
|
||||||
|
updated = re.sub(r"(<downloadurl[^>]*>)[^<]*(</downloadurl>)", rf"\g<1>{download_url}\g<2>", updated)
|
||||||
|
content = content.replace(block, updated)
|
||||||
|
print(f"Updated {xml_tag} channel: version={version}")
|
||||||
|
else:
|
||||||
|
print(f"WARNING: No <tag>{xml_tag}</tag> block in updates.xml")
|
||||||
|
|
||||||
|
with open("updates.xml", "w") as f:
|
||||||
|
f.write(content)
|
||||||
|
PYEOF
|
||||||
|
|
||||||
|
# Commit and push to current branch
|
||||||
|
if ! git diff --quiet updates.xml 2>/dev/null; then
|
||||||
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
|
git config --local user.name "gitea-actions[bot]"
|
||||||
|
git add updates.xml
|
||||||
|
git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
|
||||||
|
git push origin HEAD 2>&1 || echo "WARNING: push failed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: "Sync updates.xml to all branches"
|
||||||
|
run: |
|
||||||
|
CURRENT_BRANCH="${{ github.ref_name }}"
|
||||||
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
|
git config --local user.name "gitea-actions[bot]"
|
||||||
|
|
||||||
|
# Sync updates.xml to main and dev (whichever isn't current)
|
||||||
|
for BRANCH in main dev; do
|
||||||
|
[ "$BRANCH" = "$CURRENT_BRANCH" ] && continue
|
||||||
|
|
||||||
|
echo "Syncing updates.xml → ${BRANCH}"
|
||||||
|
git fetch origin "${BRANCH}" 2>/dev/null || continue
|
||||||
|
git checkout "origin/${BRANCH}" -- . 2>/dev/null || continue
|
||||||
|
git checkout "${CURRENT_BRANCH}" -- updates.xml
|
||||||
|
if ! git diff --quiet updates.xml 2>/dev/null; then
|
||||||
|
git add updates.xml
|
||||||
|
git commit -m "chore: sync updates.xml from ${CURRENT_BRANCH} [skip ci]"
|
||||||
|
git push origin HEAD:refs/heads/${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed"
|
||||||
|
fi
|
||||||
|
git checkout "${CURRENT_BRANCH}" 2>/dev/null
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: "Delete lesser pre-release channels (cascade)"
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
TOKEN="${{ secrets.GA_TOKEN }}"
|
||||||
|
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||||
|
|
||||||
|
# Cascade: rc → beta,alpha,dev | beta → alpha,dev | alpha → dev | dev → nothing
|
||||||
|
case "$STABILITY" in
|
||||||
|
release-candidate) TAGS_TO_DELETE="beta alpha development" ;;
|
||||||
|
beta) TAGS_TO_DELETE="alpha development" ;;
|
||||||
|
alpha) TAGS_TO_DELETE="development" ;;
|
||||||
|
*) TAGS_TO_DELETE="" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
[ -z "$TAGS_TO_DELETE" ] && exit 0
|
||||||
|
|
||||||
|
for TAG in $TAGS_TO_DELETE; do
|
||||||
|
RELEASE_ID=$(curl -sS -H "Authorization: token ${TOKEN}" \
|
||||||
|
"${API_BASE}/releases/tags/${TAG}" 2>/dev/null | \
|
||||||
|
python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "None" ]; then
|
||||||
|
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
|
||||||
|
"${API_BASE}/releases/${RELEASE_ID}" 2>/dev/null || true
|
||||||
|
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
|
||||||
|
"${API_BASE}/tags/${TAG}" 2>/dev/null || true
|
||||||
|
echo "Deleted: ${TAG} (id: ${RELEASE_ID})"
|
||||||
|
fi
|
||||||
|
done
|
||||||
@@ -0,0 +1,600 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# This file is part of a Moko Consulting project.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: MokoStandards.Joomla
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
||||||
|
# PATH: /.gitea/workflows/release.yml
|
||||||
|
# VERSION: 02.00.00
|
||||||
|
# BRIEF: Generic Joomla release — auto-detects element from manifest, stream tags, cascade
|
||||||
|
|
||||||
|
name: Create Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'stable'
|
||||||
|
- 'release-candidate'
|
||||||
|
- 'beta'
|
||||||
|
- 'alpha'
|
||||||
|
- 'development'
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
stability:
|
||||||
|
description: 'Stability tag'
|
||||||
|
required: true
|
||||||
|
default: 'stable'
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- stable
|
||||||
|
- release-candidate
|
||||||
|
- beta
|
||||||
|
- alpha
|
||||||
|
- development
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
env:
|
||||||
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||||||
|
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build Release Package
|
||||||
|
runs-on: release
|
||||||
|
|
||||||
|
steps:
|
||||||
|
# Always checkout main for tag triggers (avoids detached HEAD).
|
||||||
|
# For workflow_dispatch, checkout whatever branch was selected.
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event_name == 'push' && 'main' || github.ref }}
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.GA_TOKEN }}
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
run: |
|
||||||
|
if ! command -v php &> /dev/null; then
|
||||||
|
sudo apt-get update -qq
|
||||||
|
sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
echo "PHP: $(php -v | head -1)"
|
||||||
|
echo "Composer: $(composer --version 2>&1 | head -1)"
|
||||||
|
|
||||||
|
- name: Get version and stability
|
||||||
|
id: meta
|
||||||
|
run: |
|
||||||
|
echo "=== Meta ==="
|
||||||
|
echo "event_name: ${{ github.event_name }}"
|
||||||
|
echo "ref: ${{ github.ref }}"
|
||||||
|
echo "ref_name: ${{ github.ref_name }}"
|
||||||
|
echo "sha: ${{ github.sha }}"
|
||||||
|
|
||||||
|
# Derive stability from tag name or dispatch input
|
||||||
|
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||||
|
STABILITY="${{ inputs.stability }}"
|
||||||
|
else
|
||||||
|
TAG_PUSHED="${GITHUB_REF#refs/tags/}"
|
||||||
|
case "$TAG_PUSHED" in
|
||||||
|
stable) STABILITY="stable" ;;
|
||||||
|
release-candidate) STABILITY="rc" ;;
|
||||||
|
beta) STABILITY="beta" ;;
|
||||||
|
alpha) STABILITY="alpha" ;;
|
||||||
|
development) STABILITY="development" ;;
|
||||||
|
*) STABILITY="stable" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Read version from README.md (will be bumped in next step)
|
||||||
|
VERSION=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1)
|
||||||
|
[ -z "$VERSION" ] && VERSION="00.00.00"
|
||||||
|
|
||||||
|
# Auto-detect extension element from Joomla manifest
|
||||||
|
# Search depth 3 covers src/admin/com_xxx.xml and similar nested structures
|
||||||
|
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "./build/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
||||||
|
EXT_ELEMENT=""
|
||||||
|
if [ -n "$MANIFEST" ]; then
|
||||||
|
EXT_ELEMENT=$(sed -n 's/.*<element>\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
|
||||||
|
# If no <element> tag, derive from manifest filename or repo name
|
||||||
|
if [ -z "$EXT_ELEMENT" ]; then
|
||||||
|
EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]')
|
||||||
|
case "$EXT_ELEMENT" in
|
||||||
|
templatedetails|manifest) EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
echo "Manifest: ${MANIFEST}, element: ${EXT_ELEMENT}"
|
||||||
|
else
|
||||||
|
EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
|
||||||
|
echo "No manifest found, using repo name: ${EXT_ELEMENT}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$STABILITY" in
|
||||||
|
development) SUFFIX="-dev"; TAG_NAME="development" ;;
|
||||||
|
alpha) SUFFIX="-alpha"; TAG_NAME="alpha" ;;
|
||||||
|
beta) SUFFIX="-beta"; TAG_NAME="beta" ;;
|
||||||
|
rc) SUFFIX="-rc"; TAG_NAME="release-candidate" ;;
|
||||||
|
stable) SUFFIX=""; TAG_NAME="stable" ;;
|
||||||
|
*) SUFFIX="-dev"; TAG_NAME="development" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
PRERELEASE="true"
|
||||||
|
[ "$STABILITY" = "stable" ] && PRERELEASE="false"
|
||||||
|
|
||||||
|
ZIP_NAME="${EXT_ELEMENT}-${VERSION}${SUFFIX}.zip"
|
||||||
|
|
||||||
|
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "prerelease=${PRERELEASE}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "tag_name=${TAG_NAME}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
echo "=== Resolved ==="
|
||||||
|
echo "VERSION=${VERSION}"
|
||||||
|
echo "STABILITY=${STABILITY}"
|
||||||
|
echo "TAG_NAME=${TAG_NAME}"
|
||||||
|
echo "ZIP_NAME=${ZIP_NAME}"
|
||||||
|
echo "Branch: $(git branch --show-current)"
|
||||||
|
|
||||||
|
- name: Auto-bump patch version
|
||||||
|
id: bump
|
||||||
|
env:
|
||||||
|
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||||
|
INPUT_VERSION: ${{ steps.meta.outputs.version }}
|
||||||
|
INPUT_STABILITY: ${{ steps.meta.outputs.stability }}
|
||||||
|
INPUT_SUFFIX: ${{ steps.meta.outputs.suffix }}
|
||||||
|
EXT_ELEMENT: ${{ steps.meta.outputs.ext_element }}
|
||||||
|
run: |
|
||||||
|
BRANCH=$(git branch --show-current)
|
||||||
|
GITEA_API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||||
|
|
||||||
|
echo "=== Version Bump ==="
|
||||||
|
echo "On branch: ${BRANCH}"
|
||||||
|
|
||||||
|
# Read current version from README.md
|
||||||
|
CURRENT=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1)
|
||||||
|
echo "Current version in README: ${CURRENT}"
|
||||||
|
|
||||||
|
if [ -z "$CURRENT" ]; then
|
||||||
|
echo "No VERSION in README.md — using input version"
|
||||||
|
echo "version=${INPUT_VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "zip_name=${EXT_ELEMENT}-${INPUT_VERSION}${INPUT_SUFFIX}.zip" >> "$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Bump patch: XX.YY.ZZ → XX.YY.(ZZ+1)
|
||||||
|
MAJOR=$(echo "$CURRENT" | cut -d. -f1)
|
||||||
|
MINOR=$(echo "$CURRENT" | cut -d. -f2)
|
||||||
|
PATCH=$(echo "$CURRENT" | cut -d. -f3)
|
||||||
|
NEW_PATCH=$(printf "%02d" $((10#$PATCH + 1)))
|
||||||
|
NEW_VERSION="${MAJOR}.${MINOR}.${NEW_PATCH}"
|
||||||
|
TODAY=$(date +%Y-%m-%d)
|
||||||
|
|
||||||
|
echo "Bumping: ${CURRENT} → ${NEW_VERSION} (date: ${TODAY})"
|
||||||
|
|
||||||
|
# Update README.md
|
||||||
|
sed -i "s/VERSION:[[:space:]]*${CURRENT}/VERSION: ${NEW_VERSION}/" README.md
|
||||||
|
|
||||||
|
# Update manifest (templateDetails.xml / *.xml with <extension>)
|
||||||
|
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "./build/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
||||||
|
if [ -n "$MANIFEST" ]; then
|
||||||
|
echo "Manifest: ${MANIFEST}"
|
||||||
|
sed -i "s|<version>${CURRENT}</version>|<version>${NEW_VERSION}</version>|" "$MANIFEST"
|
||||||
|
sed -i "s|<creationDate>[^<]*</creationDate>|<creationDate>${TODAY}</creationDate>|" "$MANIFEST"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Update matching stability channel in updates.xml
|
||||||
|
if [ -f "updates.xml" ]; then
|
||||||
|
export PY_OLD="$CURRENT" PY_NEW="$NEW_VERSION" PY_STABILITY="$INPUT_STABILITY" PY_DATE="$TODAY"
|
||||||
|
python3 << 'PYEOF'
|
||||||
|
import re, os
|
||||||
|
old = os.environ["PY_OLD"]
|
||||||
|
new = os.environ["PY_NEW"]
|
||||||
|
stability = os.environ["PY_STABILITY"]
|
||||||
|
date = os.environ["PY_DATE"]
|
||||||
|
with open("updates.xml") as f:
|
||||||
|
content = f.read()
|
||||||
|
pattern = r"(<update>(?:(?!</update>).)*?<tag>" + re.escape(stability) + r"</tag>.*?</update>)"
|
||||||
|
match = re.search(pattern, content, re.DOTALL)
|
||||||
|
if match:
|
||||||
|
block = match.group(1)
|
||||||
|
updated = block.replace(old, new)
|
||||||
|
updated = re.sub(r"<creationDate>[^<]*</creationDate>", f"<creationDate>{date}</creationDate>", updated)
|
||||||
|
content = content.replace(block, updated)
|
||||||
|
print(f"Updated {stability} channel: {old} -> {new}")
|
||||||
|
else:
|
||||||
|
print(f"WARNING: No <update> block found for <tag>{stability}</tag>")
|
||||||
|
with open("updates.xml", "w") as f:
|
||||||
|
f.write(content)
|
||||||
|
PYEOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Commit and push version bump
|
||||||
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
|
git config --local user.name "gitea-actions[bot]"
|
||||||
|
git remote set-url origin "https://jmiller:${GA_TOKEN}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||||
|
git add -A
|
||||||
|
git diff --cached --quiet && echo "No changes to commit" || {
|
||||||
|
git commit -m "chore(version): bump ${CURRENT} → ${NEW_VERSION} [skip ci]" \
|
||||||
|
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
|
||||||
|
echo "Pushing version bump to ${BRANCH}..."
|
||||||
|
git push origin HEAD:${BRANCH} 2>&1
|
||||||
|
echo "Push exit code: $?"
|
||||||
|
}
|
||||||
|
|
||||||
|
# For stable releases from non-main: merge to main via Gitea API
|
||||||
|
if [ "$INPUT_STABILITY" = "stable" ] && [ "$BRANCH" != "main" ]; then
|
||||||
|
echo "Merging ${BRANCH} → main via Gitea API..."
|
||||||
|
HTTP_CODE=$(curl -sS -o /tmp/merge_response.json -w "%{http_code}" \
|
||||||
|
-X POST -H "Authorization: token ${GA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${GITEA_API}/merges" \
|
||||||
|
-d "$(jq -n \
|
||||||
|
--arg base "main" \
|
||||||
|
--arg head "${BRANCH}" \
|
||||||
|
--arg msg "chore(release): merge ${BRANCH} for stable ${NEW_VERSION} [skip ci]" \
|
||||||
|
'{base: $base, head: $head, merge_message_field: $msg}'
|
||||||
|
)")
|
||||||
|
echo "Merge response (HTTP ${HTTP_CODE}):"
|
||||||
|
cat /tmp/merge_response.json 2>/dev/null; echo
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "version=${NEW_VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "zip_name=${EXT_ELEMENT}-${NEW_VERSION}${INPUT_SUFFIX}.zip" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "=== Bump complete: ${NEW_VERSION} ==="
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
env:
|
||||||
|
COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.GA_TOKEN }}"}}}'
|
||||||
|
run: |
|
||||||
|
if [ -f "composer.json" ]; then
|
||||||
|
echo "Installing composer dependencies..."
|
||||||
|
composer install --no-dev --optimize-autoloader --no-interaction 2>&1
|
||||||
|
else
|
||||||
|
echo "No composer.json — skipping"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: Minify CSS and JS
|
||||||
|
run: |
|
||||||
|
if [ -f "package.json" ] && [ -f "scripts/minify.js" ]; then
|
||||||
|
npm ci --ignore-scripts
|
||||||
|
node scripts/minify.js
|
||||||
|
else
|
||||||
|
echo "No minify setup — skipping"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Create package
|
||||||
|
run: |
|
||||||
|
# Detect source directory (src/ or htdocs/)
|
||||||
|
SOURCE_DIR="src"
|
||||||
|
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
||||||
|
if [ ! -d "$SOURCE_DIR" ]; then
|
||||||
|
echo "::error::No src/ or htdocs/ directory found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Source directory: ${SOURCE_DIR}"
|
||||||
|
|
||||||
|
mkdir -p build/package
|
||||||
|
rsync -av \
|
||||||
|
--exclude='sftp-config*' \
|
||||||
|
--exclude='.ftpignore' \
|
||||||
|
--exclude='*.ppk' \
|
||||||
|
--exclude='*.pem' \
|
||||||
|
--exclude='*.key' \
|
||||||
|
--exclude='.env*' \
|
||||||
|
--exclude='*.local' \
|
||||||
|
--exclude='.build-trigger' \
|
||||||
|
--exclude='.beta-trigger' \
|
||||||
|
--exclude='.rc-trigger' \
|
||||||
|
"${SOURCE_DIR}/" build/package/
|
||||||
|
echo "Package contents:"
|
||||||
|
ls -la build/package/ | head -20
|
||||||
|
|
||||||
|
- name: Build ZIP
|
||||||
|
id: zip
|
||||||
|
run: |
|
||||||
|
ZIP_NAME="${{ steps.bump.outputs.zip_name }}"
|
||||||
|
echo "Building: ${ZIP_NAME}"
|
||||||
|
cd build/package
|
||||||
|
zip -r "../${ZIP_NAME}" .
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
SHA256=$(sha256sum "${ZIP_NAME}" | cut -d' ' -f1)
|
||||||
|
SIZE=$(stat -c%s "${ZIP_NAME}")
|
||||||
|
|
||||||
|
echo "sha256=${SHA256}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "size=${SIZE}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "=== Package Built ==="
|
||||||
|
echo "ZIP: ${ZIP_NAME}"
|
||||||
|
echo "SHA-256: ${SHA256}"
|
||||||
|
echo "Size: ${SIZE} bytes"
|
||||||
|
|
||||||
|
# ── Gitea Release (PRIMARY) ─────────────────────────���────────────
|
||||||
|
- name: "Gitea: Create or update release"
|
||||||
|
id: gitea_release
|
||||||
|
env:
|
||||||
|
EXT_ELEMENT: ${{ steps.meta.outputs.ext_element }}
|
||||||
|
run: |
|
||||||
|
TAG="${{ steps.meta.outputs.tag_name }}"
|
||||||
|
VERSION="${{ steps.bump.outputs.version }}"
|
||||||
|
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||||
|
SHA256="${{ steps.zip.outputs.sha256 }}"
|
||||||
|
TOKEN="${{ secrets.GA_TOKEN }}"
|
||||||
|
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
BRANCH=$(git branch --show-current)
|
||||||
|
MAX_HISTORY=5
|
||||||
|
|
||||||
|
IS_PRE="true"
|
||||||
|
[ "$STABILITY" = "stable" ] && IS_PRE="false"
|
||||||
|
|
||||||
|
# Build this version's entry
|
||||||
|
NEW_ENTRY="## ${VERSION} ($(date +%Y-%m-%d))
|
||||||
|
**SHA-256:** \`${SHA256}\`"
|
||||||
|
|
||||||
|
if [ -f "CHANGELOG.md" ]; then
|
||||||
|
NOTES=$(awk "/## \[${VERSION}\]/,/## \[/{if(/## \[${VERSION}\]/)next;if(/## \[/)exit;print}" CHANGELOG.md)
|
||||||
|
[ -n "$NOTES" ] && NEW_ENTRY="## ${VERSION} ($(date +%Y-%m-%d))
|
||||||
|
${NOTES}
|
||||||
|
**SHA-256:** \`${SHA256}\`"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for existing release — keep last N versions in body
|
||||||
|
EXISTING_BODY=""
|
||||||
|
EXISTING_ID=""
|
||||||
|
RELEASE_JSON=$(curl -sS -H "Authorization: token ${TOKEN}" \
|
||||||
|
"${API}/releases/tags/${TAG}" 2>/dev/null)
|
||||||
|
EXISTING_ID=$(echo "$RELEASE_JSON" | jq -r '.id // empty')
|
||||||
|
|
||||||
|
if [ -n "$EXISTING_ID" ]; then
|
||||||
|
echo "Existing release found: id=${EXISTING_ID}"
|
||||||
|
EXISTING_BODY=$(echo "$RELEASE_JSON" | jq -r '.body // ""')
|
||||||
|
|
||||||
|
# Keep only last (MAX_HISTORY - 1) version entries to make room for new one
|
||||||
|
TRIMMED_BODY=$(echo "$EXISTING_BODY" | python3 -c "
|
||||||
|
import sys, re
|
||||||
|
content = sys.stdin.read()
|
||||||
|
# Split on version headers (## XX.YY.ZZ)
|
||||||
|
parts = re.split(r'(?=^## \d)', content, flags=re.MULTILINE)
|
||||||
|
# Keep only version entries (skip any preamble)
|
||||||
|
versions = [p for p in parts if re.match(r'^## \d', p)]
|
||||||
|
# Keep last $((MAX_HISTORY - 1)) entries
|
||||||
|
kept = versions[:$((MAX_HISTORY - 1))]
|
||||||
|
print('\n---\n'.join(kept))
|
||||||
|
" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
# Delete old release and tag so we can recreate
|
||||||
|
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
|
||||||
|
"${API}/releases/${EXISTING_ID}" 2>/dev/null || true
|
||||||
|
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
|
||||||
|
"${API}/tags/${TAG}" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Compose full body: new entry + previous entries
|
||||||
|
if [ -n "$TRIMMED_BODY" ]; then
|
||||||
|
FULL_BODY="${NEW_ENTRY}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
${TRIMMED_BODY}"
|
||||||
|
else
|
||||||
|
FULL_BODY="${NEW_ENTRY}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== Create Release ==="
|
||||||
|
echo "TAG=${TAG} VERSION=${VERSION} BRANCH=${BRANCH} PRE=${IS_PRE} HISTORY=${MAX_HISTORY}"
|
||||||
|
|
||||||
|
HTTP_CODE=$(curl -sS -o /tmp/create_release.json -w "%{http_code}" \
|
||||||
|
-X POST -H "Authorization: token ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API}/releases" \
|
||||||
|
-d "$(jq -n \
|
||||||
|
--arg tag "$TAG" \
|
||||||
|
--arg target "$BRANCH" \
|
||||||
|
--arg name "${EXT_ELEMENT} ${VERSION} (${STABILITY})" \
|
||||||
|
--arg body "$FULL_BODY" \
|
||||||
|
--argjson pre "$IS_PRE" \
|
||||||
|
'{tag_name: $tag, target_commitish: $target, name: $name, body: $body, prerelease: $pre}'
|
||||||
|
)")
|
||||||
|
|
||||||
|
echo "Response (HTTP ${HTTP_CODE}):"
|
||||||
|
cat /tmp/create_release.json | jq . 2>/dev/null || cat /tmp/create_release.json
|
||||||
|
echo
|
||||||
|
|
||||||
|
RELEASE_ID=$(jq -r '.id // empty' /tmp/create_release.json)
|
||||||
|
if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" = "null" ]; then
|
||||||
|
echo "::error::Failed to create release (HTTP ${HTTP_CODE})"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "release_id=${RELEASE_ID}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Release created: id=${RELEASE_ID}"
|
||||||
|
|
||||||
|
- name: "Gitea: Upload ZIP"
|
||||||
|
run: |
|
||||||
|
RELEASE_ID="${{ steps.gitea_release.outputs.release_id }}"
|
||||||
|
ZIP_NAME="${{ steps.bump.outputs.zip_name }}"
|
||||||
|
TOKEN="${{ secrets.GA_TOKEN }}"
|
||||||
|
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
|
||||||
|
echo "Uploading ${ZIP_NAME} to release ${RELEASE_ID}..."
|
||||||
|
HTTP_CODE=$(curl -sS -o /tmp/upload_response.json -w "%{http_code}" \
|
||||||
|
-X POST \
|
||||||
|
-H "Authorization: token ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/octet-stream" \
|
||||||
|
"${API}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" \
|
||||||
|
--data-binary "@build/${ZIP_NAME}")
|
||||||
|
|
||||||
|
echo "Upload response (HTTP ${HTTP_CODE}):"
|
||||||
|
cat /tmp/upload_response.json | jq . 2>/dev/null || cat /tmp/upload_response.json
|
||||||
|
echo
|
||||||
|
|
||||||
|
if [ "$HTTP_CODE" -ge 400 ]; then
|
||||||
|
echo "::error::Upload failed (HTTP ${HTTP_CODE})"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Uploaded ${ZIP_NAME}"
|
||||||
|
|
||||||
|
# ── Update updates.xml ──────────────────────────────────────────
|
||||||
|
- name: "Update updates.xml with SHA and sync to main"
|
||||||
|
run: |
|
||||||
|
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||||
|
VERSION="${{ steps.bump.outputs.version }}"
|
||||||
|
SHA256="${{ steps.zip.outputs.sha256 }}"
|
||||||
|
ZIP_NAME="${{ steps.bump.outputs.zip_name }}"
|
||||||
|
TAG="${{ steps.meta.outputs.tag_name }}"
|
||||||
|
DATE=$(date +%Y-%m-%d)
|
||||||
|
BRANCH=$(git branch --show-current)
|
||||||
|
|
||||||
|
echo "=== Update updates.xml ==="
|
||||||
|
echo "STABILITY=${STABILITY} VERSION=${VERSION} SHA=${SHA256:0:16}..."
|
||||||
|
|
||||||
|
if [ ! -f "updates.xml" ] || [ -z "$SHA256" ]; then
|
||||||
|
echo "No updates.xml or no SHA — skipping"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Cascade map: each stability level updates itself + all lower levels
|
||||||
|
# stable → all | rc → rc,beta,alpha,dev | beta → beta,alpha,dev | alpha → alpha,dev | dev → dev
|
||||||
|
case "$STABILITY" in
|
||||||
|
stable) CASCADE="development,alpha,beta,rc,stable" ;;
|
||||||
|
rc) CASCADE="development,alpha,beta,rc" ;;
|
||||||
|
beta) CASCADE="development,alpha,beta" ;;
|
||||||
|
alpha) CASCADE="development,alpha" ;;
|
||||||
|
development) CASCADE="development" ;;
|
||||||
|
*) CASCADE="$STABILITY" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo "Cascade: ${STABILITY} → ${CASCADE}"
|
||||||
|
|
||||||
|
export PY_CASCADE="$CASCADE" PY_VERSION="$VERSION" PY_SHA256="$SHA256" \
|
||||||
|
PY_ZIP_NAME="$ZIP_NAME" PY_TAG="$TAG" PY_DATE="$DATE" \
|
||||||
|
PY_GITEA_ORG="$GITEA_ORG" PY_GITEA_REPO="$GITEA_REPO"
|
||||||
|
python3 << 'PYEOF'
|
||||||
|
import re, os
|
||||||
|
|
||||||
|
cascade = os.environ["PY_CASCADE"].split(",")
|
||||||
|
version = os.environ["PY_VERSION"]
|
||||||
|
sha256 = os.environ["PY_SHA256"]
|
||||||
|
zip_name = os.environ["PY_ZIP_NAME"]
|
||||||
|
tag = os.environ["PY_TAG"]
|
||||||
|
date = os.environ["PY_DATE"]
|
||||||
|
gitea_org = os.environ["PY_GITEA_ORG"]
|
||||||
|
gitea_repo = os.environ["PY_GITEA_REPO"]
|
||||||
|
|
||||||
|
gitea_url = f"https://git.mokoconsulting.tech/{gitea_org}/{gitea_repo}/releases/download/{tag}/{zip_name}"
|
||||||
|
|
||||||
|
with open("updates.xml", "r") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
for xml_tag in cascade:
|
||||||
|
xml_tag = xml_tag.strip()
|
||||||
|
block_pattern = r"(<update>(?:(?!</update>).)*?<tag>" + re.escape(xml_tag) + r"</tag>.*?</update>)"
|
||||||
|
match = re.search(block_pattern, content, re.DOTALL)
|
||||||
|
|
||||||
|
if not match:
|
||||||
|
print(f" SKIP: no <tag>{xml_tag}</tag> block found")
|
||||||
|
continue
|
||||||
|
|
||||||
|
block = match.group(1)
|
||||||
|
original_block = block
|
||||||
|
|
||||||
|
# Update version and date
|
||||||
|
block = re.sub(r"<version>[^<]*</version>", f"<version>{version}</version>", block)
|
||||||
|
block = re.sub(r"<creationDate>[^<]*</creationDate>", f"<creationDate>{date}</creationDate>", block)
|
||||||
|
|
||||||
|
# Set SHA — add if missing, update if present, never leave empty
|
||||||
|
if "<sha256>" in block:
|
||||||
|
block = re.sub(r"<sha256>[^<]*</sha256>", f"<sha256>{sha256}</sha256>", block)
|
||||||
|
else:
|
||||||
|
block = block.replace("</downloads>", f"</downloads>\n <sha256>{sha256}</sha256>")
|
||||||
|
|
||||||
|
# Update download URL
|
||||||
|
block = re.sub(
|
||||||
|
r"(<downloadurl[^>]*>)https://git\.mokoconsulting\.tech/[^<]*(</downloadurl>)",
|
||||||
|
rf"\g<1>{gitea_url}\g<2>",
|
||||||
|
block
|
||||||
|
)
|
||||||
|
|
||||||
|
content = content.replace(original_block, block)
|
||||||
|
print(f" OK: {xml_tag} → version={version}, sha={sha256[:16]}...")
|
||||||
|
|
||||||
|
with open("updates.xml", "w") as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
print(f"Cascaded {len(cascade)} channel(s)")
|
||||||
|
PYEOF
|
||||||
|
|
||||||
|
# Commit and push
|
||||||
|
if git diff --quiet updates.xml 2>/dev/null; then
|
||||||
|
echo "No changes to updates.xml"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
git add updates.xml
|
||||||
|
git commit -m "chore: update ${STABILITY} SHA-256 for ${VERSION} [skip ci]" \
|
||||||
|
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
|
||||||
|
echo "Pushing updates.xml to ${BRANCH}..."
|
||||||
|
git push origin HEAD:${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed"
|
||||||
|
|
||||||
|
# Always sync updates.xml to main via API (Joomla reads from main)
|
||||||
|
GA_TOKEN="${{ secrets.GA_TOKEN }}"
|
||||||
|
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||||
|
|
||||||
|
echo "Syncing updates.xml to main via API..."
|
||||||
|
FILE_SHA=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
|
||||||
|
"${API}/contents/updates.xml?ref=main" | jq -r '.sha // empty')
|
||||||
|
|
||||||
|
if [ -n "$FILE_SHA" ]; then
|
||||||
|
CONTENT=$(base64 -w0 updates.xml)
|
||||||
|
HTTP_CODE=$(curl -sS -o /tmp/sync_response.json -w "%{http_code}" \
|
||||||
|
-X PUT -H "Authorization: token ${GA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API}/contents/updates.xml" \
|
||||||
|
-d "$(jq -n \
|
||||||
|
--arg content "$CONTENT" \
|
||||||
|
--arg sha "$FILE_SHA" \
|
||||||
|
--arg msg "chore: sync updates.xml ${STABILITY} ${VERSION} [skip ci]" \
|
||||||
|
--arg branch "main" \
|
||||||
|
'{content: $content, sha: $sha, message: $msg, branch: $branch}'
|
||||||
|
)")
|
||||||
|
echo "Sync response (HTTP ${HTTP_CODE}):"
|
||||||
|
cat /tmp/sync_response.json | jq -r '.content.name // .message // "unknown"' 2>/dev/null
|
||||||
|
if [ "$HTTP_CODE" -ge 400 ]; then
|
||||||
|
echo "::warning::Sync to main failed (HTTP ${HTTP_CODE})"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "::warning::Could not get updates.xml SHA from main"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Summary
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.bump.outputs.version }}"
|
||||||
|
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||||
|
ZIP_NAME="${{ steps.bump.outputs.zip_name }}"
|
||||||
|
SHA256="${{ steps.zip.outputs.sha256 }}"
|
||||||
|
TAG="${{ steps.meta.outputs.tag_name }}"
|
||||||
|
|
||||||
|
echo "### Release Created" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Stability | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Tag | \`${TAG}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| SHA-256 | \`${SHA256}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Gitea | [Release](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${TAG}) |" >> $GITHUB_STEP_SUMMARY
|
||||||
@@ -6,13 +6,12 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: GitHub.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: MokoStandards.Validation
|
# INGROUP: MokoStandards.Validation
|
||||||
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
||||||
# PATH: /.github/workflows/repo_health.yml
|
# PATH: /templates/workflows/joomla/repo_health.yml.template
|
||||||
# VERSION: 04.06.00
|
# VERSION: 04.06.00
|
||||||
# BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts.
|
# BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts.
|
||||||
# NOTE: Field is user-managed.
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
name: Repo Health
|
name: Repo Health
|
||||||
@@ -50,13 +49,11 @@ env:
|
|||||||
RELEASE_OPTIONAL_REPO_VARS: DEV_FTP_SUFFIX
|
RELEASE_OPTIONAL_REPO_VARS: DEV_FTP_SUFFIX
|
||||||
|
|
||||||
# Scripts governance policy
|
# Scripts governance policy
|
||||||
# Note: directories listed without a trailing slash.
|
|
||||||
SCRIPTS_REQUIRED_DIRS:
|
SCRIPTS_REQUIRED_DIRS:
|
||||||
SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate
|
SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate
|
||||||
|
|
||||||
# Repo health policy
|
# Repo health policy
|
||||||
# Files are listed as-is; directories must end with a trailing slash.
|
REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.gitea/workflows/
|
||||||
REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.github/workflows/
|
|
||||||
REPO_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore,README.md,docs/
|
REPO_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore,README.md,docs/
|
||||||
REPO_DISALLOWED_DIRS:
|
REPO_DISALLOWED_DIRS:
|
||||||
REPO_DISALLOWED_FILES: TODO.md,todo.md
|
REPO_DISALLOWED_FILES: TODO.md,todo.md
|
||||||
@@ -64,10 +61,10 @@ env:
|
|||||||
# Extended checks toggles
|
# Extended checks toggles
|
||||||
EXTENDED_CHECKS: "true"
|
EXTENDED_CHECKS: "true"
|
||||||
|
|
||||||
# File / directory variables (moved to top-level env)
|
# File / directory variables
|
||||||
DOCS_INDEX: docs/docs-index.md
|
DOCS_INDEX: docs/docs-index.md
|
||||||
SCRIPT_DIR: scripts
|
SCRIPT_DIR: scripts
|
||||||
WORKFLOWS_DIR: .github/workflows
|
WORKFLOWS_DIR: .gitea/workflows
|
||||||
SHELLCHECK_PATTERN: '*.sh'
|
SHELLCHECK_PATTERN: '*.sh'
|
||||||
SPDX_FILE_GLOBS: '*.sh,*.php,*.js,*.ts,*.css,*.xml,*.yml,*.yaml'
|
SPDX_FILE_GLOBS: '*.sh,*.php,*.js,*.ts,*.css,*.xml,*.yml,*.yaml'
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
@@ -87,62 +84,56 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Check actor permission (admin only)
|
- name: Check actor permission (admin only)
|
||||||
id: perm
|
id: perm
|
||||||
uses: actions/github-script@v7
|
env:
|
||||||
with:
|
TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
|
||||||
github-token: ${{ secrets.GH_TOKEN }}
|
REPO: ${{ github.repository }}
|
||||||
script: |
|
ACTOR: ${{ github.actor }}
|
||||||
const actor = context.actor;
|
run: |
|
||||||
let permission = "unknown";
|
set -euo pipefail
|
||||||
let allowed = false;
|
ALLOWED=false
|
||||||
let method = "";
|
PERMISSION=unknown
|
||||||
|
METHOD=""
|
||||||
|
|
||||||
// Hardcoded authorized users — always allowed
|
# Hardcoded authorized users — always allowed
|
||||||
const authorizedUsers = ["jmiller-moko", "github-actions[bot]"];
|
case "$ACTOR" in
|
||||||
if (authorizedUsers.includes(actor)) {
|
jmiller|gitea-actions[bot])
|
||||||
allowed = true;
|
ALLOWED=true
|
||||||
permission = "admin";
|
PERMISSION=admin
|
||||||
method = "hardcoded allowlist";
|
METHOD="hardcoded allowlist"
|
||||||
} else {
|
;;
|
||||||
// Check via API for other actors
|
*)
|
||||||
try {
|
# Detect platform and check permissions via API
|
||||||
const res = await github.rest.repos.getCollaboratorPermissionLevel({
|
API_BASE="${GITHUB_API_URL:-${GITEA_API_URL:-https://api.github.com}}"
|
||||||
owner: context.repo.owner,
|
RESP=$(curl -sf -H "Authorization: token ${TOKEN}" \
|
||||||
repo: context.repo.repo,
|
"${API_BASE}/repos/${REPO}/collaborators/${ACTOR}/permission" 2>/dev/null || echo '{}')
|
||||||
username: actor,
|
PERMISSION=$(echo "$RESP" | grep -oP '"permission"\s*:\s*"\K[^"]+' || echo "unknown")
|
||||||
});
|
if [ "$PERMISSION" = "admin" ] || [ "$PERMISSION" = "maintain" ] || [ "$PERMISSION" = "owner" ]; then
|
||||||
permission = (res?.data?.permission || "unknown").toLowerCase();
|
ALLOWED=true
|
||||||
allowed = permission === "admin" || permission === "maintain";
|
fi
|
||||||
method = "repo collaborator API";
|
METHOD="collaborator API"
|
||||||
} catch (error) {
|
;;
|
||||||
core.warning(`Could not fetch permissions for '${actor}': ${error.message}`);
|
esac
|
||||||
permission = "unknown";
|
|
||||||
allowed = false;
|
|
||||||
method = "API error";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
core.setOutput("permission", permission);
|
echo "permission=${PERMISSION}" >> "$GITHUB_OUTPUT"
|
||||||
core.setOutput("allowed", allowed ? "true" : "false");
|
echo "allowed=${ALLOWED}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
const lines = [
|
{
|
||||||
"## 🔐 Access Authorization",
|
echo "## Access Authorization"
|
||||||
"",
|
echo ""
|
||||||
"| Field | Value |",
|
echo "| Field | Value |"
|
||||||
"|-------|-------|",
|
echo "|-------|-------|"
|
||||||
`| **Actor** | \`${actor}\` |`,
|
echo "| **Actor** | \`${ACTOR}\` |"
|
||||||
`| **Repository** | \`${context.repo.owner}/${context.repo.repo}\` |`,
|
echo "| **Repository** | \`${REPO}\` |"
|
||||||
`| **Permission** | \`${permission}\` |`,
|
echo "| **Permission** | \`${PERMISSION}\` |"
|
||||||
`| **Method** | ${method} |`,
|
echo "| **Method** | ${METHOD} |"
|
||||||
`| **Authorized** | ${allowed} |`,
|
echo "| **Authorized** | ${ALLOWED} |"
|
||||||
`| **Trigger** | \`${context.eventName}\` |`,
|
echo ""
|
||||||
`| **Branch** | \`${context.ref.replace('refs/heads/', '')}\` |`,
|
if [ "$ALLOWED" = "true" ]; then
|
||||||
"",
|
echo "${ACTOR} authorized (${METHOD})"
|
||||||
allowed
|
else
|
||||||
? `✅ ${actor} authorized (${method})`
|
echo "${ACTOR} is NOT authorized. Requires admin or maintain role."
|
||||||
: `❌ ${actor} is NOT authorized. Requires admin or maintain role, or be in the hardcoded allowlist.`,
|
fi
|
||||||
];
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
await core.summary.addRaw(lines.join("\n")).write();
|
|
||||||
|
|
||||||
- name: Deny execution when not permitted
|
- name: Deny execution when not permitted
|
||||||
if: ${{ steps.perm.outputs.allowed != 'true' }}
|
if: ${{ steps.perm.outputs.allowed != 'true' }}
|
||||||
@@ -427,7 +418,6 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
# Optional entries: handle files and directories (trailing slash indicates dir)
|
|
||||||
for f in "${optional_files[@]}"; do
|
for f in "${optional_files[@]}"; do
|
||||||
if printf '%s' "${f}" | grep -q '/$'; then
|
if printf '%s' "${f}" | grep -q '/$'; then
|
||||||
d="${f%/}"
|
d="${f%/}"
|
||||||
@@ -451,8 +441,6 @@ jobs:
|
|||||||
dev_paths=()
|
dev_paths=()
|
||||||
dev_branches=()
|
dev_branches=()
|
||||||
|
|
||||||
# Look for remote branches matching origin/dev*.
|
|
||||||
# A plain origin/dev is considered invalid; we require dev/<something> branches.
|
|
||||||
while IFS= read -r b; do
|
while IFS= read -r b; do
|
||||||
name="${b#origin/}"
|
name="${b#origin/}"
|
||||||
if [ "${name}" = 'dev' ]; then
|
if [ "${name}" = 'dev' ]; then
|
||||||
@@ -462,12 +450,10 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
done < <(git branch -r --list 'origin/dev*' | sed 's/^ *//')
|
done < <(git branch -r --list 'origin/dev*' | sed 's/^ *//')
|
||||||
|
|
||||||
# If there are no dev/* branches, fail the guardrail.
|
|
||||||
if [ "${#dev_paths[@]}" -eq 0 ]; then
|
if [ "${#dev_paths[@]}" -eq 0 ]; then
|
||||||
missing_required+=("dev/* branch (e.g. dev/01.00.00)")
|
missing_required+=("dev/* branch (e.g. dev/01.00.00)")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# If a plain dev branch exists (origin/dev), flag it as invalid.
|
|
||||||
if [ "${#dev_branches[@]}" -gt 0 ]; then
|
if [ "${#dev_branches[@]}" -gt 0 ]; then
|
||||||
missing_required+=("invalid branch dev (must be dev/<version>)")
|
missing_required+=("invalid branch dev (must be dev/<version>)")
|
||||||
fi
|
fi
|
||||||
@@ -559,48 +545,39 @@ jobs:
|
|||||||
} >> "${GITHUB_STEP_SUMMARY}"
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Joomla-specific checks ───────────────────────────────────────
|
# -- Joomla-specific checks --
|
||||||
joomla_findings=()
|
joomla_findings=()
|
||||||
|
|
||||||
# XML manifest: find any XML file containing <extension
|
|
||||||
MANIFEST="$(find . -maxdepth 2 -name '*.xml' -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true)"
|
MANIFEST="$(find . -maxdepth 2 -name '*.xml' -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true)"
|
||||||
if [ -z "${MANIFEST}" ]; then
|
if [ -z "${MANIFEST}" ]; then
|
||||||
joomla_findings+=("Joomla XML manifest not found (no *.xml with <extension> tag)")
|
joomla_findings+=("Joomla XML manifest not found (no *.xml with <extension> tag)")
|
||||||
else
|
else
|
||||||
# Check <version> tag exists
|
|
||||||
if ! grep -qP '<version>' "${MANIFEST}"; then
|
if ! grep -qP '<version>' "${MANIFEST}"; then
|
||||||
joomla_findings+=("XML manifest: <version> tag missing")
|
joomla_findings+=("XML manifest: <version> tag missing")
|
||||||
fi
|
fi
|
||||||
# Check extension type attribute
|
|
||||||
if ! grep -qP 'type="(component|module|plugin|library|package|template|language)"' "${MANIFEST}"; then
|
if ! grep -qP 'type="(component|module|plugin|library|package|template|language)"' "${MANIFEST}"; then
|
||||||
joomla_findings+=("XML manifest: type attribute missing or invalid")
|
joomla_findings+=("XML manifest: type attribute missing or invalid")
|
||||||
fi
|
fi
|
||||||
# Check <name> tag
|
|
||||||
if ! grep -qP '<name>' "${MANIFEST}"; then
|
if ! grep -qP '<name>' "${MANIFEST}"; then
|
||||||
joomla_findings+=("XML manifest: <name> tag missing")
|
joomla_findings+=("XML manifest: <name> tag missing")
|
||||||
fi
|
fi
|
||||||
# Check <author> tag
|
|
||||||
if ! grep -qP '<author>' "${MANIFEST}"; then
|
if ! grep -qP '<author>' "${MANIFEST}"; then
|
||||||
joomla_findings+=("XML manifest: <author> tag missing")
|
joomla_findings+=("XML manifest: <author> tag missing")
|
||||||
fi
|
fi
|
||||||
# Check <namespace> for Joomla 5+
|
|
||||||
if ! grep -qP '<namespace' "${MANIFEST}"; then
|
if ! grep -qP '<namespace' "${MANIFEST}"; then
|
||||||
joomla_findings+=("XML manifest: <namespace> missing (required for Joomla 5+)")
|
joomla_findings+=("XML manifest: <namespace> missing (required for Joomla 5+)")
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Language files: check for at least one .ini file
|
|
||||||
INI_COUNT="$(find . -name '*.ini' -type f 2>/dev/null | wc -l)"
|
INI_COUNT="$(find . -name '*.ini' -type f 2>/dev/null | wc -l)"
|
||||||
if [ "${INI_COUNT}" -eq 0 ]; then
|
if [ "${INI_COUNT}" -eq 0 ]; then
|
||||||
joomla_findings+=("No .ini language files found")
|
joomla_findings+=("No .ini language files found")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# updates.xml must exist in root (Joomla update server)
|
|
||||||
if [ ! -f 'updates.xml' ]; then
|
if [ ! -f 'updates.xml' ]; then
|
||||||
joomla_findings+=("updates.xml missing in root (required for Joomla update server)")
|
joomla_findings+=("updates.xml missing in root (required for Joomla update server)")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# index.html files for directory listing protection
|
|
||||||
INDEX_DIRS=("${SOURCE_DIR}" "${SOURCE_DIR}/admin" "${SOURCE_DIR}/site")
|
INDEX_DIRS=("${SOURCE_DIR}" "${SOURCE_DIR}/admin" "${SOURCE_DIR}/site")
|
||||||
for dir in "${INDEX_DIRS[@]}"; do
|
for dir in "${INDEX_DIRS[@]}"; do
|
||||||
if [ -d "${dir}" ] && [ ! -f "${dir}/index.html" ]; then
|
if [ -d "${dir}" ] && [ ! -f "${dir}/index.html" ]; then
|
||||||
@@ -630,14 +607,12 @@ jobs:
|
|||||||
extended_findings=()
|
extended_findings=()
|
||||||
|
|
||||||
if [ "${extended_enabled}" = 'true' ]; then
|
if [ "${extended_enabled}" = 'true' ]; then
|
||||||
# CODEOWNERS presence
|
|
||||||
if [ -f '.github/CODEOWNERS' ] || [ -f 'CODEOWNERS' ] || [ -f 'docs/CODEOWNERS' ]; then
|
if [ -f '.github/CODEOWNERS' ] || [ -f 'CODEOWNERS' ] || [ -f 'docs/CODEOWNERS' ]; then
|
||||||
:
|
:
|
||||||
else
|
else
|
||||||
extended_findings+=("CODEOWNERS not found (.github/CODEOWNERS preferred)")
|
extended_findings+=("CODEOWNERS not found (.github/CODEOWNERS preferred)")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Workflow pinning advisory: flag uses @main/@master
|
|
||||||
if ls "${WORKFLOWS_DIR}"/*.yml >/dev/null 2>&1 || ls "${WORKFLOWS_DIR}"/*.yaml >/dev/null 2>&1; then
|
if ls "${WORKFLOWS_DIR}"/*.yml >/dev/null 2>&1 || ls "${WORKFLOWS_DIR}"/*.yaml >/dev/null 2>&1; then
|
||||||
bad_refs="$(grep -RIn --include='*.yml' --include='*.yaml' -E '^[[:space:]]*uses:[[:space:]]*[^#]+@(main|master)\b' "${WORKFLOWS_DIR}" 2>/dev/null || true)"
|
bad_refs="$(grep -RIn --include='*.yml' --include='*.yaml' -E '^[[:space:]]*uses:[[:space:]]*[^#]+@(main|master)\b' "${WORKFLOWS_DIR}" 2>/dev/null || true)"
|
||||||
if [ -n "${bad_refs}" ]; then
|
if [ -n "${bad_refs}" ]; then
|
||||||
@@ -653,7 +628,6 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Docs index link integrity (docs/docs-index.md)
|
|
||||||
if [ -f "${DOCS_INDEX}" ]; then
|
if [ -f "${DOCS_INDEX}" ]; then
|
||||||
missing_links="$(python3 - <<'PY'
|
missing_links="$(python3 - <<'PY'
|
||||||
import os
|
import os
|
||||||
@@ -697,7 +671,6 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ShellCheck advisory
|
|
||||||
if [ -d "${SCRIPT_DIR}" ]; then
|
if [ -d "${SCRIPT_DIR}" ]; then
|
||||||
if ! command -v shellcheck >/dev/null 2>&1; then
|
if ! command -v shellcheck >/dev/null 2>&1; then
|
||||||
sudo apt-get update -qq
|
sudo apt-get update -qq
|
||||||
@@ -726,7 +699,6 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# SPDX header advisory for common source types
|
|
||||||
spdx_missing=()
|
spdx_missing=()
|
||||||
IFS=',' read -r -a spdx_globs <<< "${SPDX_FILE_GLOBS}"
|
IFS=',' read -r -a spdx_globs <<< "${SPDX_FILE_GLOBS}"
|
||||||
spdx_args=()
|
spdx_args=()
|
||||||
@@ -749,9 +721,8 @@ jobs:
|
|||||||
} >> "${GITHUB_STEP_SUMMARY}"
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Git hygiene advisory: branches older than 180 days (remote)
|
|
||||||
stale_cutoff_days=180
|
stale_cutoff_days=180
|
||||||
stale_branches="$(git for-each-ref --format='%(refname:short) %(committerdate:unix)' refs/remotes/origin 2>/dev/null | awk -v now="$(date +%s)" -v days="${stale_cutoff_days}" '{if (now-$2 [...]
|
stale_branches="$(git for-each-ref --format='%(refname:short) %(committerdate:unix)' refs/remotes/origin 2>/dev/null | awk -v now="$(date +%s)" -v days="${stale_cutoff_days}" '{if (now-$2 > days*86400) print $1}' | head -50)"
|
||||||
if [ -n "${stale_branches}" ]; then
|
if [ -n "${stale_branches}" ]; then
|
||||||
extended_findings+=("Stale remote branches detected (advisory)")
|
extended_findings+=("Stale remote branches detected (advisory)")
|
||||||
{
|
{
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
---
|
||||||
|
name: Request for Comments (RFC)
|
||||||
|
about: Propose a significant change for community discussion
|
||||||
|
title: '[RFC] '
|
||||||
|
labels: 'rfc, discussion'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## RFC Summary
|
||||||
|
One-paragraph summary of the proposal.
|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
Why are we doing this? What use cases does it support? What is the expected outcome?
|
||||||
|
|
||||||
|
## Detailed Design
|
||||||
|
### Overview
|
||||||
|
Provide a detailed explanation of the proposed change.
|
||||||
|
|
||||||
|
### API Changes (if applicable)
|
||||||
|
```php
|
||||||
|
// Before
|
||||||
|
function oldApi($param1) { }
|
||||||
|
|
||||||
|
// After
|
||||||
|
function newApi($param1, $param2) { }
|
||||||
|
```
|
||||||
|
|
||||||
|
### User Experience Changes
|
||||||
|
Describe how users will interact with this change.
|
||||||
|
|
||||||
|
### Implementation Approach
|
||||||
|
High-level implementation strategy.
|
||||||
|
|
||||||
|
## Drawbacks
|
||||||
|
Why should we *not* do this?
|
||||||
|
|
||||||
|
## Alternatives
|
||||||
|
What other designs have been considered? What is the impact of not doing this?
|
||||||
|
|
||||||
|
### Alternative 1
|
||||||
|
- Description
|
||||||
|
- Trade-offs
|
||||||
|
|
||||||
|
### Alternative 2
|
||||||
|
- Description
|
||||||
|
- Trade-offs
|
||||||
|
|
||||||
|
## Adoption Strategy
|
||||||
|
How will existing users adopt this? Is this a breaking change?
|
||||||
|
|
||||||
|
### Migration Guide
|
||||||
|
```bash
|
||||||
|
# Steps to migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deprecation Timeline
|
||||||
|
- **Announcement**:
|
||||||
|
- **Deprecation**:
|
||||||
|
- **Removal**:
|
||||||
|
|
||||||
|
## Unresolved Questions
|
||||||
|
- Question 1
|
||||||
|
- Question 2
|
||||||
|
|
||||||
|
## Future Possibilities
|
||||||
|
What future work does this enable?
|
||||||
|
|
||||||
|
## Impact Assessment
|
||||||
|
### Performance
|
||||||
|
Expected performance impact.
|
||||||
|
|
||||||
|
### Security
|
||||||
|
Security considerations and implications.
|
||||||
|
|
||||||
|
### Compatibility
|
||||||
|
- **Backward Compatible**: [Yes / No]
|
||||||
|
- **Breaking Changes**: [List]
|
||||||
|
|
||||||
|
### Maintenance
|
||||||
|
Long-term maintenance considerations.
|
||||||
|
|
||||||
|
## Community Input
|
||||||
|
### Stakeholders
|
||||||
|
- [ ] Core team
|
||||||
|
- [ ] Module developers
|
||||||
|
- [ ] End users
|
||||||
|
- [ ] Enterprise customers
|
||||||
|
|
||||||
|
### Feedback Period
|
||||||
|
**Duration**: [e.g., 2 weeks]
|
||||||
|
**Deadline**: [date]
|
||||||
|
|
||||||
|
## Implementation Timeline
|
||||||
|
### Phase 1: Design
|
||||||
|
- [ ] RFC discussion
|
||||||
|
- [ ] Design finalization
|
||||||
|
- [ ] Approval
|
||||||
|
|
||||||
|
### Phase 2: Implementation
|
||||||
|
- [ ] Core implementation
|
||||||
|
- [ ] Tests
|
||||||
|
- [ ] Documentation
|
||||||
|
|
||||||
|
### Phase 3: Release
|
||||||
|
- [ ] Beta release
|
||||||
|
- [ ] Feedback collection
|
||||||
|
- [ ] Stable release
|
||||||
|
|
||||||
|
## Success Metrics
|
||||||
|
How will we measure success?
|
||||||
|
- Metric 1
|
||||||
|
- Metric 2
|
||||||
|
|
||||||
|
## References
|
||||||
|
- Related RFCs:
|
||||||
|
- External documentation:
|
||||||
|
- Prior art:
|
||||||
|
|
||||||
|
## Open Questions for Community
|
||||||
|
1. Question 1?
|
||||||
|
2. Question 2?
|
||||||
|
|
||||||
|
---
|
||||||
|
**Note**: This RFC is open for community discussion. Please provide feedback in the comments below.
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: MokoStandards.Security
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
||||||
|
# PATH: /.gitea/workflows/security-audit.yml
|
||||||
|
# VERSION: 01.00.00
|
||||||
|
# BRIEF: Dependency vulnerability scanning for composer and npm packages
|
||||||
|
|
||||||
|
name: Security Audit
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 6 * * 1' # Weekly on Monday at 06:00 UTC
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- 'composer.json'
|
||||||
|
- 'composer.lock'
|
||||||
|
- 'package.json'
|
||||||
|
- 'package-lock.json'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
env:
|
||||||
|
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
|
||||||
|
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
audit:
|
||||||
|
name: Dependency Audit
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Composer audit
|
||||||
|
if: hashFiles('composer.lock') != ''
|
||||||
|
run: |
|
||||||
|
echo "=== Composer Security Audit ==="
|
||||||
|
if ! command -v composer &> /dev/null; then
|
||||||
|
sudo apt-get update -qq
|
||||||
|
sudo apt-get install -y -qq php-cli composer >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
composer audit --format=plain 2>&1 | tee /tmp/composer-audit.txt
|
||||||
|
RESULT=$?
|
||||||
|
if [ $RESULT -ne 0 ]; then
|
||||||
|
echo "::warning::Composer vulnerabilities found"
|
||||||
|
echo "composer_vulnerable=true" >> "$GITHUB_ENV"
|
||||||
|
else
|
||||||
|
echo "No known vulnerabilities in composer dependencies"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: NPM audit
|
||||||
|
if: hashFiles('package-lock.json') != ''
|
||||||
|
run: |
|
||||||
|
echo "=== NPM Security Audit ==="
|
||||||
|
npm audit --production 2>&1 | tee /tmp/npm-audit.txt || true
|
||||||
|
if npm audit --production 2>&1 | grep -q "found 0 vulnerabilities"; then
|
||||||
|
echo "No known vulnerabilities in npm dependencies"
|
||||||
|
else
|
||||||
|
echo "::warning::NPM vulnerabilities found"
|
||||||
|
echo "npm_vulnerable=true" >> "$GITHUB_ENV"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Notify on vulnerabilities
|
||||||
|
if: env.composer_vulnerable == 'true' || env.npm_vulnerable == 'true'
|
||||||
|
run: |
|
||||||
|
REPO="${{ github.event.repository.name }}"
|
||||||
|
curl -sS \
|
||||||
|
-H "Title: ${REPO} has vulnerable dependencies" \
|
||||||
|
-H "Tags: lock,warning" \
|
||||||
|
-H "Priority: high" \
|
||||||
|
-d "Security audit found vulnerabilities. Review dependency updates." \
|
||||||
|
"${NTFY_URL}/${NTFY_TOPIC}" || true
|
||||||
@@ -0,0 +1,464 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: MokoStandards.Joomla
|
||||||
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
||||||
|
# PATH: /templates/workflows/joomla/update-server.yml.template
|
||||||
|
# VERSION: 04.06.00
|
||||||
|
# BRIEF: Update Joomla update server XML feed with stable/rc/dev entries
|
||||||
|
#
|
||||||
|
# Writes updates.xml with multiple <update> entries:
|
||||||
|
# - <tag>stable</tag> on push to main (from auto-release)
|
||||||
|
# - <tag>rc</tag> on push to rc/**
|
||||||
|
# - <tag>development</tag> on push to dev or dev/**
|
||||||
|
#
|
||||||
|
# Joomla filters by user's "Minimum Stability" setting.
|
||||||
|
|
||||||
|
name: Update Joomla Update Server XML Feed
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'dev'
|
||||||
|
- 'dev/**'
|
||||||
|
- 'alpha/**'
|
||||||
|
- 'beta/**'
|
||||||
|
- 'rc/**'
|
||||||
|
paths:
|
||||||
|
- 'src/**'
|
||||||
|
- 'htdocs/**'
|
||||||
|
pull_request:
|
||||||
|
types: [closed]
|
||||||
|
branches:
|
||||||
|
- 'dev'
|
||||||
|
- 'dev/**'
|
||||||
|
- 'alpha/**'
|
||||||
|
- 'beta/**'
|
||||||
|
- 'rc/**'
|
||||||
|
paths:
|
||||||
|
- 'src/**'
|
||||||
|
- 'htdocs/**'
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
stability:
|
||||||
|
description: 'Stability tag'
|
||||||
|
required: true
|
||||||
|
default: 'development'
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- development
|
||||||
|
- alpha
|
||||||
|
- beta
|
||||||
|
- rc
|
||||||
|
- stable
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||||||
|
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update-xml:
|
||||||
|
name: Update updates.xml
|
||||||
|
runs-on: release
|
||||||
|
if: >-
|
||||||
|
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || github.event_name == 'push'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GA_TOKEN }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup MokoStandards tools
|
||||||
|
env:
|
||||||
|
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||||
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
|
COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.GA_TOKEN }}"}}}'
|
||||||
|
run: |
|
||||||
|
if ! command -v composer &> /dev/null; then
|
||||||
|
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
git clone --depth 1 --branch main --quiet \
|
||||||
|
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
|
||||||
|
/tmp/mokostandards-api 2>/dev/null || true
|
||||||
|
if [ -d "/tmp/mokostandards-api" ] && [ -f "/tmp/mokostandards-api/composer.json" ]; then
|
||||||
|
cd /tmp/mokostandards-api && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Generate updates.xml entry
|
||||||
|
id: update
|
||||||
|
run: |
|
||||||
|
BRANCH="${{ github.ref_name }}"
|
||||||
|
REPO="${{ github.repository }}"
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
VERSION=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null || echo "0.0.0")
|
||||||
|
|
||||||
|
# Auto-bump patch on all branches (dev, alpha, beta, rc)
|
||||||
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
|
git config --local user.name "gitea-actions[bot]"
|
||||||
|
BUMPED=$(php /tmp/mokostandards-api/cli/version_bump.php --path . 2>/dev/null || true)
|
||||||
|
if [ -n "$BUMPED" ]; then
|
||||||
|
VERSION=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null || echo "$VERSION")
|
||||||
|
git add -A
|
||||||
|
git commit -m "chore(version): auto-bump patch ${VERSION} [skip ci]" \
|
||||||
|
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" 2>/dev/null || true
|
||||||
|
git push 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Determine stability from branch or input
|
||||||
|
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||||
|
STABILITY="${{ inputs.stability }}"
|
||||||
|
elif [[ "$BRANCH" == rc/* ]]; then
|
||||||
|
STABILITY="rc"
|
||||||
|
elif [[ "$BRANCH" == beta/* ]]; then
|
||||||
|
STABILITY="beta"
|
||||||
|
elif [[ "$BRANCH" == alpha/* ]]; then
|
||||||
|
STABILITY="alpha"
|
||||||
|
elif [[ "$BRANCH" == dev/* ]] || [[ "$BRANCH" == "dev" ]]; then
|
||||||
|
STABILITY="development"
|
||||||
|
else
|
||||||
|
STABILITY="stable"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
# Parse manifest (portable — no grep -P)
|
||||||
|
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "./build/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
||||||
|
if [ -z "$MANIFEST" ]; then
|
||||||
|
echo "No Joomla manifest found — skipping"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract fields using sed (works on all runners)
|
||||||
|
EXT_NAME=$(sed -n 's/.*<name>\([^<]*\)<\/name>.*/\1/p' "$MANIFEST" | head -1)
|
||||||
|
EXT_TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
|
||||||
|
EXT_ELEMENT=$(sed -n 's/.*<element>\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" | head -1)
|
||||||
|
EXT_CLIENT=$(sed -n 's/.*<extension[^>]*client="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
|
||||||
|
EXT_FOLDER=$(sed -n 's/.*<extension[^>]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
|
||||||
|
EXT_VERSION=$(sed -n 's/.*<version>\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1)
|
||||||
|
TARGET_PLATFORM=$(sed -n 's/.*\(<targetplatform[^/]*\/>\).*/\1/p' "$MANIFEST" | head -1)
|
||||||
|
PHP_MINIMUM=$(sed -n 's/.*<php_minimum>\([^<]*\)<\/php_minimum>.*/\1/p' "$MANIFEST" | head -1)
|
||||||
|
|
||||||
|
# Fallbacks
|
||||||
|
[ -z "$EXT_NAME" ] && EXT_NAME="${{ github.event.repository.name }}"
|
||||||
|
[ -z "$EXT_TYPE" ] && EXT_TYPE="component"
|
||||||
|
|
||||||
|
# Derive element if not in manifest: try XML filename, then repo name
|
||||||
|
if [ -z "$EXT_ELEMENT" ]; then
|
||||||
|
EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]')
|
||||||
|
case "$EXT_ELEMENT" in
|
||||||
|
templatedetails|manifest|*.xml) EXT_ELEMENT=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use manifest version if README version is empty
|
||||||
|
[ "$VERSION" = "0.0.0" ] && [ -n "$EXT_VERSION" ] && VERSION="$EXT_VERSION"
|
||||||
|
|
||||||
|
[ -z "$TARGET_PLATFORM" ] && TARGET_PLATFORM=$(printf '<targetplatform name="joomla" version="((5.[0-9])|(6.[0-9]))" %s>' "/")
|
||||||
|
|
||||||
|
CLIENT_TAG=""
|
||||||
|
[ -n "$EXT_CLIENT" ] && CLIENT_TAG="<client>${EXT_CLIENT}</client>"
|
||||||
|
[ -z "$CLIENT_TAG" ] && ([ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]) && CLIENT_TAG="<client>site</client>"
|
||||||
|
|
||||||
|
FOLDER_TAG=""
|
||||||
|
[ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ] && FOLDER_TAG="<folder>${EXT_FOLDER}</folder>"
|
||||||
|
|
||||||
|
PHP_TAG=""
|
||||||
|
[ -n "$PHP_MINIMUM" ] && PHP_TAG="<php_minimum>${PHP_MINIMUM}</php_minimum>"
|
||||||
|
|
||||||
|
# Version suffix for non-stable
|
||||||
|
DISPLAY_VERSION="$VERSION"
|
||||||
|
case "$STABILITY" in
|
||||||
|
development) DISPLAY_VERSION="${VERSION}-dev" ;;
|
||||||
|
alpha) DISPLAY_VERSION="${VERSION}-alpha" ;;
|
||||||
|
beta) DISPLAY_VERSION="${VERSION}-beta" ;;
|
||||||
|
rc) DISPLAY_VERSION="${VERSION}-rc" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
MAJOR=$(echo "$VERSION" | awk -F. '{print $1}')
|
||||||
|
|
||||||
|
# Each stability level has its own release tag
|
||||||
|
case "$STABILITY" in
|
||||||
|
development) RELEASE_TAG="development" ;;
|
||||||
|
alpha) RELEASE_TAG="alpha" ;;
|
||||||
|
beta) RELEASE_TAG="beta" ;;
|
||||||
|
rc) RELEASE_TAG="release-candidate" ;;
|
||||||
|
*) RELEASE_TAG="v${MAJOR}" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
PACKAGE_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.zip"
|
||||||
|
DOWNLOAD_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${PACKAGE_NAME}"
|
||||||
|
INFO_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
|
||||||
|
# -- Build install packages (ZIP + tar.gz) --------------------
|
||||||
|
SOURCE_DIR="src"
|
||||||
|
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
||||||
|
if [ -d "$SOURCE_DIR" ]; then
|
||||||
|
EXCLUDES=".ftpignore sftp-config* *.ppk *.pem *.key .env*"
|
||||||
|
TAR_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.tar.gz"
|
||||||
|
|
||||||
|
cd "$SOURCE_DIR"
|
||||||
|
zip -r "/tmp/${PACKAGE_NAME}" . -x $EXCLUDES
|
||||||
|
cd ..
|
||||||
|
tar -czf "/tmp/${TAR_NAME}" -C "$SOURCE_DIR" \
|
||||||
|
--exclude='.ftpignore' --exclude='sftp-config*' \
|
||||||
|
--exclude='*.ppk' --exclude='*.pem' --exclude='*.key' --exclude='.env*' .
|
||||||
|
|
||||||
|
SHA256=$(sha256sum "/tmp/${PACKAGE_NAME}" | cut -d' ' -f1)
|
||||||
|
|
||||||
|
# Ensure release exists on Gitea
|
||||||
|
RELEASE_JSON=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
||||||
|
"${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true)
|
||||||
|
RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [ -z "$RELEASE_ID" ]; then
|
||||||
|
# Create release
|
||||||
|
RELEASE_JSON=$(curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API_BASE}/releases" \
|
||||||
|
-d "$(python3 -c "import json; print(json.dumps({
|
||||||
|
'tag_name': '${RELEASE_TAG}',
|
||||||
|
'name': '${RELEASE_TAG} (${DISPLAY_VERSION})',
|
||||||
|
'body': '${STABILITY} release',
|
||||||
|
'prerelease': True,
|
||||||
|
'target_commitish': 'main'
|
||||||
|
}))")" 2>/dev/null || true)
|
||||||
|
RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$RELEASE_ID" ]; then
|
||||||
|
# Delete existing assets with same name before uploading
|
||||||
|
ASSETS=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
||||||
|
"${API_BASE}/releases/${RELEASE_ID}/assets" 2>/dev/null || echo "[]")
|
||||||
|
for ASSET_FILE in "$PACKAGE_NAME" "$TAR_NAME"; do
|
||||||
|
ASSET_ID=$(echo "$ASSETS" | python3 -c "
|
||||||
|
import sys,json
|
||||||
|
assets = json.load(sys.stdin)
|
||||||
|
for a in assets:
|
||||||
|
if a['name'] == '${ASSET_FILE}':
|
||||||
|
print(a['id']); break
|
||||||
|
" 2>/dev/null || true)
|
||||||
|
if [ -n "$ASSET_ID" ]; then
|
||||||
|
curl -sf -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
||||||
|
"${API_BASE}/releases/${RELEASE_ID}/assets/${ASSET_ID}" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Upload both formats
|
||||||
|
curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
||||||
|
-H "Content-Type: application/octet-stream" \
|
||||||
|
--data-binary @"/tmp/${PACKAGE_NAME}" \
|
||||||
|
"${API_BASE}/releases/${RELEASE_ID}/assets?name=${PACKAGE_NAME}" > /dev/null 2>&1 || true
|
||||||
|
|
||||||
|
curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
||||||
|
-H "Content-Type: application/octet-stream" \
|
||||||
|
--data-binary @"/tmp/${TAR_NAME}" \
|
||||||
|
"${API_BASE}/releases/${RELEASE_ID}/assets?name=${TAR_NAME}" > /dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Packages: ${PACKAGE_NAME} + ${TAR_NAME} (SHA: ${SHA256})" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
SHA256=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -- Build the new entry (canonical format matching release.yml) --
|
||||||
|
NEW_ENTRY=""
|
||||||
|
NEW_ENTRY="${NEW_ENTRY} <update>\n"
|
||||||
|
NEW_ENTRY="${NEW_ENTRY} <name>${EXT_NAME}</name>\n"
|
||||||
|
NEW_ENTRY="${NEW_ENTRY} <description>${EXT_NAME} ${STABILITY} build.</description>\n"
|
||||||
|
NEW_ENTRY="${NEW_ENTRY} <element>${EXT_ELEMENT}</element>\n"
|
||||||
|
NEW_ENTRY="${NEW_ENTRY} <type>${EXT_TYPE}</type>\n"
|
||||||
|
[ -n "$CLIENT_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${CLIENT_TAG}\n"
|
||||||
|
[ -n "$FOLDER_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${FOLDER_TAG}\n"
|
||||||
|
NEW_ENTRY="${NEW_ENTRY} <version>${VERSION}</version>\n"
|
||||||
|
NEW_ENTRY="${NEW_ENTRY} <creationDate>$(date +%Y-%m-%d)</creationDate>\n"
|
||||||
|
NEW_ENTRY="${NEW_ENTRY} <infourl title='${EXT_NAME}'>https://git.mokoconsulting.tech/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${RELEASE_TAG}</infourl>\n"
|
||||||
|
NEW_ENTRY="${NEW_ENTRY} <downloads>\n"
|
||||||
|
NEW_ENTRY="${NEW_ENTRY} <downloadurl type='full' format='zip'>${DOWNLOAD_URL}</downloadurl>\n"
|
||||||
|
NEW_ENTRY="${NEW_ENTRY} </downloads>\n"
|
||||||
|
[ -n "$SHA256" ] && NEW_ENTRY="${NEW_ENTRY} <sha256>${SHA256}</sha256>\n"
|
||||||
|
NEW_ENTRY="${NEW_ENTRY} <tags><tag>${STABILITY}</tag></tags>\n"
|
||||||
|
NEW_ENTRY="${NEW_ENTRY} <maintainer>Moko Consulting</maintainer>\n"
|
||||||
|
NEW_ENTRY="${NEW_ENTRY} <maintainerurl>https://mokoconsulting.tech</maintainerurl>\n"
|
||||||
|
NEW_ENTRY="${NEW_ENTRY} <targetplatform name='joomla' version='(5|6).*'/>\n"
|
||||||
|
[ -n "$PHP_MINIMUM" ] && NEW_ENTRY="${NEW_ENTRY} <php_minimum>${PHP_MINIMUM}</php_minimum>\n"
|
||||||
|
NEW_ENTRY="${NEW_ENTRY} </update>"
|
||||||
|
|
||||||
|
# -- Write new entry to temp file --------------------------------
|
||||||
|
printf '%b' "$NEW_ENTRY" > /tmp/new_entry.xml
|
||||||
|
|
||||||
|
# -- Merge into updates.xml ----------------------------------------
|
||||||
|
# Cascade: stable→all | rc→rc+lower | beta→beta+lower | alpha→alpha+dev | dev→dev
|
||||||
|
CASCADE_MAP="stable:development,alpha,beta,rc,stable rc:development,alpha,beta,rc beta:development,alpha,beta alpha:development,alpha development:development"
|
||||||
|
TARGETS=""
|
||||||
|
for entry in $CASCADE_MAP; do
|
||||||
|
key="${entry%%:*}"
|
||||||
|
vals="${entry#*:}"
|
||||||
|
if [ "$key" = "${STABILITY}" ]; then
|
||||||
|
TARGETS="$vals"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
[ -z "$TARGETS" ] && TARGETS="${STABILITY}"
|
||||||
|
|
||||||
|
echo "Cascade: ${STABILITY} → ${TARGETS}"
|
||||||
|
|
||||||
|
# Create updates.xml if missing
|
||||||
|
if [ ! -f "updates.xml" ]; then
|
||||||
|
printf '%s\n' "<?xml version='1.0' encoding='UTF-8'?>" > updates.xml
|
||||||
|
printf '%s\n' "<!-- Copyright (C) $(date +%Y) Moko Consulting -->" >> updates.xml
|
||||||
|
printf '%s\n' "<updates>" >> updates.xml
|
||||||
|
printf '%s\n' "</updates>" >> updates.xml
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Update existing blocks or create missing ones
|
||||||
|
export PY_TARGETS="$TARGETS" PY_VERSION="$VERSION" PY_DATE="$(date +%Y-%m-%d)"
|
||||||
|
python3 << 'PYEOF'
|
||||||
|
import re, os
|
||||||
|
|
||||||
|
targets = os.environ["PY_TARGETS"].split(",")
|
||||||
|
version = os.environ["PY_VERSION"]
|
||||||
|
date = os.environ["PY_DATE"]
|
||||||
|
|
||||||
|
with open("updates.xml") as f:
|
||||||
|
content = f.read()
|
||||||
|
with open("/tmp/new_entry.xml") as f:
|
||||||
|
new_entry_template = f.read()
|
||||||
|
|
||||||
|
for tag in targets:
|
||||||
|
tag = tag.strip()
|
||||||
|
# Build entry with this tag's name
|
||||||
|
new_entry = re.sub(r"<tag>[^<]*</tag>", f"<tag>{tag}</tag>", new_entry_template)
|
||||||
|
|
||||||
|
# Try to find existing block (handles both single-line and multi-line <tags>)
|
||||||
|
block_pattern = r"(<update>(?:(?!</update>).)*?<tag>" + re.escape(tag) + r"</tag>.*?</update>)"
|
||||||
|
match = re.search(block_pattern, content, re.DOTALL)
|
||||||
|
|
||||||
|
if match:
|
||||||
|
# Update in place — replace entire block
|
||||||
|
content = content.replace(match.group(1), new_entry.strip())
|
||||||
|
print(f" UPDATED: <tag>{tag}</tag> → {version}")
|
||||||
|
else:
|
||||||
|
# Create — insert before </updates>
|
||||||
|
content = content.replace("</updates>", "\n" + new_entry.strip() + "\n\n</updates>")
|
||||||
|
print(f" CREATED: <tag>{tag}</tag> → {version}")
|
||||||
|
|
||||||
|
# Clean up excessive blank lines
|
||||||
|
content = re.sub(r"\n{3,}", "\n\n", content)
|
||||||
|
|
||||||
|
with open("updates.xml", "w") as f:
|
||||||
|
f.write(content)
|
||||||
|
PYEOF
|
||||||
|
|
||||||
|
# Commit
|
||||||
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
|
git config --local user.name "gitea-actions[bot]"
|
||||||
|
git add updates.xml
|
||||||
|
git diff --cached --quiet || {
|
||||||
|
git commit -m "chore: update updates.xml (${STABILITY}: ${DISPLAY_VERSION}) [skip ci]" \
|
||||||
|
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
|
||||||
|
git push
|
||||||
|
}
|
||||||
|
|
||||||
|
# -- Sync updates.xml to main (for non-main branches) ----------------------
|
||||||
|
- name: Sync updates.xml to main
|
||||||
|
if: github.ref_name != 'main'
|
||||||
|
run: |
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
GA_TOKEN="${{ secrets.GA_TOKEN }}"
|
||||||
|
|
||||||
|
FILE_SHA=$(curl -sf -H "Authorization: token ${GA_TOKEN}" \
|
||||||
|
"${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then
|
||||||
|
CONTENT=$(base64 -w0 updates.xml)
|
||||||
|
curl -sf -X PUT -H "Authorization: token ${GA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API_BASE}/contents/updates.xml" \
|
||||||
|
-d "$(python3 -c "import json; print(json.dumps({
|
||||||
|
'content': '${CONTENT}',
|
||||||
|
'sha': '${FILE_SHA}',
|
||||||
|
'message': 'chore: sync updates.xml from ${STABILITY} [skip ci]',
|
||||||
|
'branch': 'main'
|
||||||
|
}))")" > /dev/null 2>&1 \
|
||||||
|
&& echo "updates.xml synced to main (${STABILITY})" >> $GITHUB_STEP_SUMMARY \
|
||||||
|
|| echo "WARNING: failed to sync updates.xml to main" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "WARNING: could not get updates.xml SHA from main" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: SFTP deploy to dev server
|
||||||
|
if: contains(github.ref, 'dev/') || github.ref == 'refs/heads/dev'
|
||||||
|
env:
|
||||||
|
DEV_HOST: ${{ vars.DEV_FTP_HOST }}
|
||||||
|
DEV_PATH: ${{ vars.DEV_FTP_PATH }}
|
||||||
|
DEV_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
|
||||||
|
DEV_USER: ${{ vars.DEV_FTP_USERNAME }}
|
||||||
|
DEV_PORT: ${{ vars.DEV_FTP_PORT }}
|
||||||
|
DEV_KEY: ${{ secrets.DEV_FTP_KEY }}
|
||||||
|
DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
# -- Permission check: admin or maintain role required --------
|
||||||
|
ACTOR="${{ github.actor }}"
|
||||||
|
REPO="${{ github.repository }}"
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
|
||||||
|
PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
||||||
|
"${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \
|
||||||
|
python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read")
|
||||||
|
case "$PERMISSION" in
|
||||||
|
admin|maintain|write) ;;
|
||||||
|
*)
|
||||||
|
echo "Deploy denied: ${ACTOR} has '${PERMISSION}' — requires admin, maintain, or write"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
[ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured — skipping SFTP"; exit 0; }
|
||||||
|
|
||||||
|
SOURCE_DIR="src"
|
||||||
|
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
||||||
|
[ ! -d "$SOURCE_DIR" ] && exit 0
|
||||||
|
|
||||||
|
PORT="${DEV_PORT:-22}"
|
||||||
|
REMOTE="${DEV_PATH%/}"
|
||||||
|
[ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}"
|
||||||
|
|
||||||
|
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
|
||||||
|
"$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json
|
||||||
|
if [ -n "$DEV_KEY" ]; then
|
||||||
|
echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key
|
||||||
|
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
|
||||||
|
else
|
||||||
|
printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json
|
||||||
|
fi
|
||||||
|
|
||||||
|
PLATFORM=$(php /tmp/mokostandards-api/cli/platform_detect.php --path . 2>/dev/null || true)
|
||||||
|
if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards-api/deploy/deploy-joomla.php" ]; then
|
||||||
|
php /tmp/mokostandards-api/deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
|
||||||
|
elif [ -f "/tmp/mokostandards-api/deploy/deploy-sftp.php" ]; then
|
||||||
|
php /tmp/mokostandards-api/deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
|
||||||
|
fi
|
||||||
|
rm -f /tmp/deploy_key /tmp/sftp-config.json
|
||||||
|
echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
- name: Summary
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
echo "## Joomla Update Server" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Version | \`${DISPLAY_VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Element | \`${EXT_ELEMENT}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Download | [ZIP](${DOWNLOAD_URL}) |" >> $GITHUB_STEP_SUMMARY
|
||||||
@@ -0,0 +1,763 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: moko-platform.Release
|
||||||
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
||||||
|
# PATH: /templates/workflows/universal/auto-release.yml.template
|
||||||
|
# VERSION: 05.00.00
|
||||||
|
# BRIEF: Universal build & release � detects platform from manifest.xml
|
||||||
|
#
|
||||||
|
# +========================================================================+
|
||||||
|
# | UNIVERSAL BUILD & RELEASE PIPELINE |
|
||||||
|
# +========================================================================+
|
||||||
|
# | |
|
||||||
|
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
|
||||||
|
# | |
|
||||||
|
# | Platform-specific: |
|
||||||
|
# | joomla: XML manifest, updates.xml, type-prefixed packages |
|
||||||
|
# | dolibarr: mod*.class.php, update.txt, dev version reset |
|
||||||
|
# | generic: README-only, no update stream |
|
||||||
|
# | |
|
||||||
|
# +========================================================================+
|
||||||
|
|
||||||
|
name: "Universal: Build & Release"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [closed]
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- 'src/**'
|
||||||
|
- 'htdocs/**'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||||||
|
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
name: Build & Release Pipeline
|
||||||
|
runs-on: release
|
||||||
|
if: >-
|
||||||
|
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GA_TOKEN }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup moko-platform tools
|
||||||
|
env:
|
||||||
|
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||||
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
|
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN }}"}}'
|
||||||
|
run: |
|
||||||
|
# Ensure PHP + Composer are available
|
||||||
|
if ! command -v composer &> /dev/null; then
|
||||||
|
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
git clone --depth 1 --branch main --quiet \
|
||||||
|
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
||||||
|
/tmp/moko-platform-api
|
||||||
|
cd /tmp/moko-platform-api
|
||||||
|
composer install --no-dev --no-interaction --quiet
|
||||||
|
|
||||||
|
|
||||||
|
# -- PLATFORM DETECTION ---------------------------------------------------
|
||||||
|
- name: Detect platform
|
||||||
|
id: platform
|
||||||
|
run: |
|
||||||
|
php /tmp/moko-platform-api/cli/manifest_read.php --path . --github-output
|
||||||
|
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true)
|
||||||
|
MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1 || true)
|
||||||
|
echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "mod_file=${MOD_FILE}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: "Step 1: Read version"
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
VERSION=$(php /tmp/moko-platform-api/cli/version_read.php --path .)
|
||||||
|
if [ -z "$VERSION" ]; then
|
||||||
|
echo "::error::No VERSION in README.md"
|
||||||
|
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
MAJOR=$(echo "$VERSION" | cut -d. -f1)
|
||||||
|
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "release_tag=v${MAJOR}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "branch=version/${MAJOR}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: "Step 1b: Bump version"
|
||||||
|
id: bump
|
||||||
|
if: steps.version.outputs.skip != 'true'
|
||||||
|
run: |
|
||||||
|
MOKO_API="/tmp/moko-platform-api/cli"
|
||||||
|
BUMP=$(php ${MOKO_API}/version_bump.php --path . --minor)
|
||||||
|
VERSION=$(echo "$BUMP" | grep -oP '\d{2}\.\d{2}\.\d{2}$' || true)
|
||||||
|
[ -z "$VERSION" ] && VERSION=$(php ${MOKO_API}/version_read.php --path .)
|
||||||
|
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Bumped to: ${VERSION}"
|
||||||
|
|
||||||
|
- name: Check if already released
|
||||||
|
if: steps.version.outputs.skip != 'true'
|
||||||
|
id: check
|
||||||
|
run: |
|
||||||
|
TAG="${{ steps.version.outputs.release_tag }}"
|
||||||
|
BRANCH="${{ steps.version.outputs.branch }}"
|
||||||
|
|
||||||
|
TAG_EXISTS=false
|
||||||
|
BRANCH_EXISTS=false
|
||||||
|
|
||||||
|
git rev-parse "$TAG" >/dev/null 2>&1 && TAG_EXISTS=true
|
||||||
|
git ls-remote --heads origin "$BRANCH" 2>/dev/null | grep -q "$BRANCH" && BRANCH_EXISTS=true
|
||||||
|
|
||||||
|
echo "tag_exists=$TAG_EXISTS" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "branch_exists=$BRANCH_EXISTS" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
# Tag and branch may persist across patch releases — never skip
|
||||||
|
echo "already_released=false" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
# -- SANITY CHECKS -------------------------------------------------------
|
||||||
|
- name: "Sanity: Pre-release validation"
|
||||||
|
if: >-
|
||||||
|
steps.version.outputs.skip != 'true' &&
|
||||||
|
steps.check.outputs.already_released != 'true'
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
|
ERRORS=0
|
||||||
|
|
||||||
|
PLATFORM="${{ steps.platform.outputs.platform }}"
|
||||||
|
MANIFEST="${{ steps.platform.outputs.manifest }}"
|
||||||
|
MOD_FILE="${{ steps.platform.outputs.mod_file }}"
|
||||||
|
echo "## Pre-Release Sanity Checks (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# -- Version drift check (must pass before release) --------
|
||||||
|
README_VER=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1)
|
||||||
|
if [ "$README_VER" != "$VERSION" ]; then
|
||||||
|
echo "- Version drift: README says \`${README_VER}\` but releasing \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS+1))
|
||||||
|
else
|
||||||
|
echo "- Version consistent: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check CHANGELOG version matches
|
||||||
|
CL_VER=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' CHANGELOG.md 2>/dev/null | head -1)
|
||||||
|
if [ -n "$CL_VER" ] && [ "$CL_VER" != "$VERSION" ]; then
|
||||||
|
echo "- CHANGELOG drift: \`${CL_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS+1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check composer.json version if present
|
||||||
|
if [ -f "composer.json" ]; then
|
||||||
|
COMP_VER=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' composer.json 2>/dev/null | head -1)
|
||||||
|
if [ -n "$COMP_VER" ] && [ "$COMP_VER" != "$VERSION" ]; then
|
||||||
|
echo "- composer.json drift: \`${COMP_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS+1))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Common checks
|
||||||
|
if [ ! -f "LICENSE" ]; then
|
||||||
|
echo "- Missing LICENSE file" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS+1))
|
||||||
|
else
|
||||||
|
echo "- LICENSE present" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d "src" ] && [ ! -d "htdocs" ]; then
|
||||||
|
echo "- Warning: No src/ or htdocs/ directory" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "- Source directory present" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -- Platform-specific checks --------
|
||||||
|
case "$PLATFORM" in
|
||||||
|
joomla)
|
||||||
|
if [ -n "$MANIFEST" ]; then
|
||||||
|
XML_VER=$(sed -n 's/.*<version>\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
|
||||||
|
if [ -n "$XML_VER" ] && [ "$XML_VER" != "$VERSION" ]; then
|
||||||
|
echo "- Manifest drift: \`${XML_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS+1))
|
||||||
|
else
|
||||||
|
echo "- Manifest version: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" 2>/dev/null)
|
||||||
|
echo "- Extension type: ${TYPE:-unknown}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "- No Joomla XML manifest (WaaS site)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi ;;
|
||||||
|
dolibarr)
|
||||||
|
if [ -n "$MOD_FILE" ]; then
|
||||||
|
MOD_VER=$(sed -n "s/.*\\\$this->version = '\([^']*\)'.*/\1/p" "$MOD_FILE" 2>/dev/null | head -1)
|
||||||
|
if [ -n "$MOD_VER" ] && [ "$MOD_VER" != "$VERSION" ]; then
|
||||||
|
echo "- Module drift: \`${MOD_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS+1))
|
||||||
|
else
|
||||||
|
echo "- Module version: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "- No mod*.class.php found" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS+1))
|
||||||
|
fi
|
||||||
|
if [ ! -f "update.txt" ]; then
|
||||||
|
echo "- Missing update.txt" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS+1))
|
||||||
|
fi ;;
|
||||||
|
*) echo "- Generic platform � no manifest checks" >> $GITHUB_STEP_SUMMARY ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
if [ "$ERRORS" -gt 0 ]; then
|
||||||
|
echo "**${ERRORS} error(s) — release may be incomplete**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "**All sanity checks passed**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -- STEP 2: Create or update version/XX.YY archive branch ---------------
|
||||||
|
# Always runs — every version change on main archives to version/XX.YY
|
||||||
|
- name: "Step 2: Version archive branch"
|
||||||
|
if: steps.check.outputs.already_released != 'true'
|
||||||
|
run: |
|
||||||
|
BRANCH="${{ steps.version.outputs.branch }}"
|
||||||
|
IS_MINOR="${{ steps.version.outputs.is_minor }}"
|
||||||
|
PATCH="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
|
PATCH_NUM=$(echo "$PATCH" | awk -F. '{print $3}')
|
||||||
|
|
||||||
|
# Check if branch exists
|
||||||
|
if git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then
|
||||||
|
git push origin HEAD:"$BRANCH" --force
|
||||||
|
echo "Updated archive branch: ${BRANCH} (patch ${PATCH_NUM})" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
git checkout -b "$BRANCH" 2>/dev/null || git checkout "$BRANCH"
|
||||||
|
git push origin "$BRANCH" --force
|
||||||
|
echo "Created archive branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -- STEP 3: Set platform version ----------------------------------------
|
||||||
|
- name: "Step 3: Set platform version"
|
||||||
|
if: >-
|
||||||
|
steps.version.outputs.skip != 'true' &&
|
||||||
|
steps.check.outputs.already_released != 'true'
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
|
php /tmp/moko-platform-api/cli/version_set_platform.php \
|
||||||
|
--path . --version "$VERSION" --branch main
|
||||||
|
|
||||||
|
# -- STEP 4: Update version badges ----------------------------------------
|
||||||
|
- name: "Step 4: Update version badges"
|
||||||
|
if: steps.version.outputs.skip != 'true'
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
|
php /tmp/moko-platform-api/cli/badge_update.php --path . --version "${VERSION}" 2>/dev/null || true
|
||||||
|
|
||||||
|
- name: "Step 5: Write update stream"
|
||||||
|
if: >-
|
||||||
|
steps.version.outputs.skip != 'true' &&
|
||||||
|
steps.platform.outputs.platform == 'joomla'
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
|
php /tmp/moko-platform-api/cli/updates_xml_build.php \
|
||||||
|
--path . --version "${VERSION}" --stability stable \
|
||||||
|
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
|
||||||
|
--github-output
|
||||||
|
|
||||||
|
- name: Commit release changes
|
||||||
|
if: >-
|
||||||
|
steps.version.outputs.skip != 'true' &&
|
||||||
|
steps.check.outputs.already_released != 'true'
|
||||||
|
run: |
|
||||||
|
if git diff --quiet && git diff --cached --quiet; then
|
||||||
|
echo "No changes to commit"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
|
git config --local user.name "gitea-actions[bot]"
|
||||||
|
# Set push URL with token for branch-protected repos
|
||||||
|
git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||||
|
git add -A
|
||||||
|
git commit -m "chore(release): build ${VERSION} [skip ci]" \
|
||||||
|
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
|
||||||
|
git push -u origin HEAD
|
||||||
|
|
||||||
|
# -- STEP 6: Create tag ---------------------------------------------------
|
||||||
|
- name: "Step 6: Create git tag"
|
||||||
|
if: >-
|
||||||
|
steps.version.outputs.skip != 'true' &&
|
||||||
|
steps.check.outputs.tag_exists != 'true' &&
|
||||||
|
steps.version.outputs.is_minor == 'true'
|
||||||
|
run: |
|
||||||
|
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
||||||
|
# Only create the major release tag if it doesn't exist yet
|
||||||
|
if ! git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then
|
||||||
|
git tag "$RELEASE_TAG"
|
||||||
|
git push origin "$RELEASE_TAG"
|
||||||
|
echo "Tag created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "Tag ${RELEASE_TAG} already exists" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
echo "Tag: ${TAG}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# -- STEP 7: Create or update Gitea Release --------------------------------
|
||||||
|
- name: "Step 7: Gitea Release"
|
||||||
|
if: >-
|
||||||
|
steps.version.outputs.skip != 'true'
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
|
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
||||||
|
BRANCH="${{ steps.version.outputs.branch }}"
|
||||||
|
MAJOR="${{ steps.version.outputs.major }}"
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
|
||||||
|
# Reuse metadata from Step 5 (single source of truth)
|
||||||
|
EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}"
|
||||||
|
EXT_NAME="${{ steps.updates.outputs.ext_name }}"
|
||||||
|
EXT_TYPE="${{ steps.updates.outputs.ext_type }}"
|
||||||
|
EXT_FOLDER="${{ steps.updates.outputs.ext_folder }}"
|
||||||
|
|
||||||
|
# Fallbacks if Step 5 was skipped
|
||||||
|
if [ -z "$EXT_ELEMENT" ]; then
|
||||||
|
EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
|
||||||
|
fi
|
||||||
|
[ -z "$EXT_NAME" ] && EXT_NAME="${GITEA_REPO}"
|
||||||
|
|
||||||
|
NOTES=$(php /tmp/moko-platform-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null)
|
||||||
|
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
|
||||||
|
|
||||||
|
# Build release name: "Pretty Name VERSION (type_element-VERSION)"
|
||||||
|
TYPE_PREFIX=""
|
||||||
|
case "${EXT_TYPE}" in
|
||||||
|
plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;;
|
||||||
|
module) TYPE_PREFIX="mod_" ;;
|
||||||
|
component) TYPE_PREFIX="com_" ;;
|
||||||
|
template) TYPE_PREFIX="tpl_" ;;
|
||||||
|
library) TYPE_PREFIX="lib_" ;;
|
||||||
|
package) TYPE_PREFIX="pkg_" ;;
|
||||||
|
esac
|
||||||
|
RELEASE_NAME="${EXT_NAME} ${VERSION} (${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION})"
|
||||||
|
|
||||||
|
# Delete existing release if present (overwrite, not append)
|
||||||
|
EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
||||||
|
"${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true)
|
||||||
|
EXISTING_ID=$(echo "$EXISTING" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('id',''))" 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [ -n "$EXISTING_ID" ]; then
|
||||||
|
curl -sS -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
||||||
|
"${API_BASE}/releases/${EXISTING_ID}" 2>/dev/null || true
|
||||||
|
curl -sS -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
||||||
|
"${API_BASE}/tags/${RELEASE_TAG}" 2>/dev/null || true
|
||||||
|
echo "Deleted previous stable release (id: ${EXISTING_ID})"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create fresh release
|
||||||
|
curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API_BASE}/releases" \
|
||||||
|
-d "$(python3 -c "import json; print(json.dumps({
|
||||||
|
'tag_name': '${RELEASE_TAG}',
|
||||||
|
'name': '${RELEASE_NAME}',
|
||||||
|
'body': '''## ${VERSION} ($(date +%Y-%m-%d))\n${NOTES}''',
|
||||||
|
'target_commitish': '${BRANCH}'
|
||||||
|
}))")"
|
||||||
|
echo "Release created: ${RELEASE_NAME}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# -- STEP 8: Build Joomla install ZIP + SHA-256 checksum ------------------
|
||||||
|
- name: "Step 8: Build package and update checksum"
|
||||||
|
if: >-
|
||||||
|
steps.version.outputs.skip != 'true'
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
|
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
||||||
|
REPO="${{ github.repository }}"
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
|
||||||
|
# All ZIPs upload to the major release tag (vXX)
|
||||||
|
RELEASE_JSON=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
||||||
|
"${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true)
|
||||||
|
RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
||||||
|
if [ -z "$RELEASE_ID" ]; then
|
||||||
|
echo "No release ${RELEASE_TAG} found — skipping ZIP upload"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Find extension element name from manifest
|
||||||
|
MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true)
|
||||||
|
[ -z "$MANIFEST" ] && exit 0
|
||||||
|
|
||||||
|
# Reuse element from Step 5, with same fallback chain
|
||||||
|
EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}"
|
||||||
|
if [ -z "$EXT_ELEMENT" ]; then
|
||||||
|
EXT_ELEMENT=$(sed -n 's/.*<element>\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
|
||||||
|
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(sed -n 's/.*plugin="\([^"]*\)".*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
|
||||||
|
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]')
|
||||||
|
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
|
||||||
|
fi
|
||||||
|
# ZIP name: type_folder_element-VERSION (e.g. plg_system_mokojgdpc-01.01.00.zip)
|
||||||
|
EXT_TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
|
||||||
|
EXT_FOLDER=$(sed -n 's/.*<extension[^>]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
|
||||||
|
TYPE_PREFIX=""
|
||||||
|
case "${EXT_TYPE}" in
|
||||||
|
plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;;
|
||||||
|
module) TYPE_PREFIX="mod_" ;;
|
||||||
|
component) TYPE_PREFIX="com_" ;;
|
||||||
|
template) TYPE_PREFIX="tpl_" ;;
|
||||||
|
library) TYPE_PREFIX="lib_" ;;
|
||||||
|
package) TYPE_PREFIX="pkg_" ;;
|
||||||
|
esac
|
||||||
|
ZIP_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip"
|
||||||
|
TAR_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.tar.gz"
|
||||||
|
|
||||||
|
# -- Build install packages from src/ ----------------------------
|
||||||
|
SOURCE_DIR="src"
|
||||||
|
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
||||||
|
[ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/"; exit 0; }
|
||||||
|
|
||||||
|
# ZIP package (type-aware via moko-platform PHP API)
|
||||||
|
php /tmp/moko-platform-api/cli/joomla_build.php --path . --version "${VERSION}" --output /tmp
|
||||||
|
# Match the expected ZIP_NAME for upload
|
||||||
|
BUILT_ZIP=$(ls /tmp/${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip 2>/dev/null | head -1 || true)
|
||||||
|
if [ -n "$BUILT_ZIP" ] && [ "$BUILT_ZIP" != "/tmp/${ZIP_NAME}" ]; then
|
||||||
|
mv "$BUILT_ZIP" "/tmp/${ZIP_NAME}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# tar.gz package (flat source archive)
|
||||||
|
tar -czf "/tmp/${TAR_NAME}" -C "$SOURCE_DIR" --exclude='.ftpignore' --exclude='sftp-config*' --exclude='*.ppk' --exclude='*.pem' --exclude='*.key' --exclude='.env*' .
|
||||||
|
|
||||||
|
ZIP_SIZE=$(stat -c%s "/tmp/${ZIP_NAME}" 2>/dev/null || stat -f%z "/tmp/${ZIP_NAME}" 2>/dev/null || echo "unknown")
|
||||||
|
TAR_SIZE=$(stat -c%s "/tmp/${TAR_NAME}" 2>/dev/null || stat -f%z "/tmp/${TAR_NAME}" 2>/dev/null || echo "unknown")
|
||||||
|
|
||||||
|
# -- Calculate SHA-256 for both ----------------------------------
|
||||||
|
SHA256_ZIP=$(sha256sum "/tmp/${ZIP_NAME}" | cut -d' ' -f1)
|
||||||
|
SHA256_TAR=$(sha256sum "/tmp/${TAR_NAME}" | cut -d' ' -f1)
|
||||||
|
|
||||||
|
# -- Delete existing assets with same name before uploading ------
|
||||||
|
ASSETS=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
||||||
|
"${API_BASE}/releases/${RELEASE_ID}/assets" 2>/dev/null || echo "[]")
|
||||||
|
for ASSET_NAME in "$ZIP_NAME" "$TAR_NAME"; do
|
||||||
|
ASSET_ID=$(echo "$ASSETS" | python3 -c "
|
||||||
|
import sys,json
|
||||||
|
assets = json.load(sys.stdin)
|
||||||
|
for a in assets:
|
||||||
|
if a['name'] == '${ASSET_NAME}':
|
||||||
|
print(a['id']); break
|
||||||
|
" 2>/dev/null || true)
|
||||||
|
if [ -n "$ASSET_ID" ]; then
|
||||||
|
curl -sf -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
||||||
|
"${API_BASE}/releases/${RELEASE_ID}/assets/${ASSET_ID}" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# -- Upload both to release tag ----------------------------------
|
||||||
|
curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
||||||
|
-H "Content-Type: application/octet-stream" \
|
||||||
|
--data-binary @"/tmp/${ZIP_NAME}" \
|
||||||
|
"${API_BASE}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" > /dev/null 2>&1 || true
|
||||||
|
|
||||||
|
curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
||||||
|
-H "Content-Type: application/octet-stream" \
|
||||||
|
--data-binary @"/tmp/${TAR_NAME}" \
|
||||||
|
"${API_BASE}/releases/${RELEASE_ID}/assets?name=${TAR_NAME}" > /dev/null 2>&1 || true
|
||||||
|
|
||||||
|
# -- Update updates.xml with both download formats ---------------
|
||||||
|
if [ -f "updates.xml" ]; then
|
||||||
|
ZIP_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${ZIP_NAME}"
|
||||||
|
TAR_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${TAR_NAME}"
|
||||||
|
|
||||||
|
# Use Python to update only the stable entry's downloads + sha256
|
||||||
|
export PY_ZIP_URL="$ZIP_URL" PY_TAR_URL="$TAR_URL" PY_SHA="$SHA256_ZIP"
|
||||||
|
python3 << 'PYEOF'
|
||||||
|
import re, os
|
||||||
|
|
||||||
|
with open("updates.xml") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
zip_url = os.environ["PY_ZIP_URL"]
|
||||||
|
tar_url = os.environ["PY_TAR_URL"]
|
||||||
|
sha = os.environ["PY_SHA"]
|
||||||
|
|
||||||
|
# Find the stable update block and replace its downloads + sha256
|
||||||
|
def replace_stable(m):
|
||||||
|
block = m.group(0)
|
||||||
|
# Replace downloads block
|
||||||
|
new_downloads = (
|
||||||
|
" <downloads>\n"
|
||||||
|
f" <downloadurl type=\"full\" format=\"zip\">{zip_url}</downloadurl>\n"
|
||||||
|
" </downloads>"
|
||||||
|
)
|
||||||
|
block = re.sub(r' <downloads>.*?</downloads>', new_downloads, block, flags=re.DOTALL)
|
||||||
|
# Add or replace sha256
|
||||||
|
if '<sha256>' in block:
|
||||||
|
block = re.sub(r' <sha256>.*?</sha256>', f' <sha256>{sha}</sha256>', block)
|
||||||
|
else:
|
||||||
|
block = block.replace('</downloads>', f'</downloads>\n <sha256>{sha}</sha256>')
|
||||||
|
return block
|
||||||
|
|
||||||
|
content = re.sub(
|
||||||
|
r' <update>.*?<tag>stable</tag>.*?</update>',
|
||||||
|
replace_stable,
|
||||||
|
content,
|
||||||
|
flags=re.DOTALL
|
||||||
|
)
|
||||||
|
|
||||||
|
with open("updates.xml", "w") as f:
|
||||||
|
f.write(content)
|
||||||
|
PYEOF
|
||||||
|
|
||||||
|
CURRENT_BRANCH="${{ github.ref_name }}"
|
||||||
|
git add updates.xml
|
||||||
|
git commit -m "chore(release): ZIP + tar.gz for ${VERSION} [skip ci]" \
|
||||||
|
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" || true
|
||||||
|
git push || true
|
||||||
|
|
||||||
|
# Sync updates.xml to main via direct API (always runs — may be on version/XX branch)
|
||||||
|
GA_TOKEN="${{ secrets.GA_TOKEN }}"
|
||||||
|
API="${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}"
|
||||||
|
|
||||||
|
FILE_SHA=$(curl -sf -H "Authorization: token ${GA_TOKEN}" \
|
||||||
|
"${API}/contents/updates.xml?ref=main" | jq -r '.sha // empty')
|
||||||
|
|
||||||
|
if [ -n "$FILE_SHA" ]; then
|
||||||
|
CONTENT=$(base64 -w0 updates.xml)
|
||||||
|
curl -sf -X PUT -H "Authorization: token ${GA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API}/contents/updates.xml" \
|
||||||
|
-d "$(jq -n \
|
||||||
|
--arg content "$CONTENT" \
|
||||||
|
--arg sha "$FILE_SHA" \
|
||||||
|
--arg msg "chore: sync updates.xml ${VERSION} [skip ci]" \
|
||||||
|
--arg branch "main" \
|
||||||
|
'{content: $content, sha: $sha, message: $msg, branch: $branch}'
|
||||||
|
)" > /dev/null 2>&1 \
|
||||||
|
&& echo "updates.xml synced to main via API" \
|
||||||
|
|| echo "WARNING: failed to sync updates.xml to main"
|
||||||
|
else
|
||||||
|
echo "WARNING: could not get updates.xml SHA from main"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "### Packages" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Package | Size | SHA-256 |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "|---------|------|---------|" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| \`${ZIP_NAME}\` | ${ZIP_SIZE} | \`${SHA256_ZIP}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| \`${TAR_NAME}\` | ${TAR_SIZE} | \`${SHA256_TAR}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Release | \`${RELEASE_TAG}\` | |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Download | [${ZIP_NAME}](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${ZIP_NAME}) |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# -- STEP 8b: Update release description with changelog + SHA ----------------
|
||||||
|
- name: "Step 8b: Update release body with changelog and SHA"
|
||||||
|
if: steps.version.outputs.skip != 'true'
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
|
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}"
|
||||||
|
EXT_TYPE="${{ steps.updates.outputs.ext_type }}"
|
||||||
|
EXT_FOLDER="${{ steps.updates.outputs.ext_folder }}"
|
||||||
|
|
||||||
|
# Build TYPE_PREFIX to match Step 8's ZIP naming
|
||||||
|
TYPE_PREFIX=""
|
||||||
|
case "${EXT_TYPE}" in
|
||||||
|
plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;;
|
||||||
|
module) TYPE_PREFIX="mod_" ;;
|
||||||
|
component) TYPE_PREFIX="com_" ;;
|
||||||
|
template) TYPE_PREFIX="tpl_" ;;
|
||||||
|
library) TYPE_PREFIX="lib_" ;;
|
||||||
|
package) TYPE_PREFIX="pkg_" ;;
|
||||||
|
esac
|
||||||
|
ZIP_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip"
|
||||||
|
TAR_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.tar.gz"
|
||||||
|
|
||||||
|
# Get SHA from the built files
|
||||||
|
SHA256_ZIP=""
|
||||||
|
[ -f "/tmp/${ZIP_NAME}" ] && SHA256_ZIP=$(sha256sum "/tmp/${ZIP_NAME}" | cut -d' ' -f1)
|
||||||
|
SHA256_TAR=""
|
||||||
|
[ -f "/tmp/${TAR_NAME}" ] && SHA256_TAR=$(sha256sum "/tmp/${TAR_NAME}" | cut -d' ' -f1)
|
||||||
|
|
||||||
|
# Extract latest changelog entry (strip the ## header to avoid duplicate)
|
||||||
|
CHANGELOG=""
|
||||||
|
if [ -f "CHANGELOG.md" ]; then
|
||||||
|
CHANGELOG=$(sed -n "/^## \[*${VERSION}/,/^## \[*[0-9]/p" CHANGELOG.md | sed '$d' | sed '1d')
|
||||||
|
[ -z "$CHANGELOG" ] && CHANGELOG=$(sed -n '/^## /,/^## /p' CHANGELOG.md | sed '$d' | sed '1d' | head -30)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build release body (single header, no duplicate from changelog)
|
||||||
|
BODY="## ${VERSION} ($(date +%Y-%m-%d))\n\n"
|
||||||
|
if [ -n "$CHANGELOG" ]; then
|
||||||
|
BODY="${BODY}${CHANGELOG}\n\n"
|
||||||
|
fi
|
||||||
|
BODY="${BODY}---\n\n### Checksums\n\n"
|
||||||
|
BODY="${BODY}| File | SHA-256 |\n|------|--------|\n"
|
||||||
|
[ -n "$SHA256_ZIP" ] && BODY="${BODY}| \`${ZIP_NAME}\` | \`${SHA256_ZIP}\` |\n"
|
||||||
|
[ -n "$SHA256_TAR" ] && BODY="${BODY}| \`${TAR_NAME}\` | \`${SHA256_TAR}\` |\n"
|
||||||
|
|
||||||
|
# Get release ID and update body
|
||||||
|
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
||||||
|
"${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null | \
|
||||||
|
python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "None" ]; then
|
||||||
|
python3 -c "
|
||||||
|
import json, urllib.request
|
||||||
|
body = '''$(printf '%b' "$BODY")'''
|
||||||
|
data = json.dumps({'body': body}).encode()
|
||||||
|
req = urllib.request.Request(
|
||||||
|
'${API_BASE}/releases/${RELEASE_ID}',
|
||||||
|
data=data,
|
||||||
|
headers={'Authorization': 'token ${{ secrets.GA_TOKEN }}', 'Content-Type': 'application/json'},
|
||||||
|
method='PATCH'
|
||||||
|
)
|
||||||
|
urllib.request.urlopen(req)
|
||||||
|
" 2>/dev/null && echo "Release body updated with changelog + SHA" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
|
||||||
|
- name: "Step 9: Mirror release to GitHub"
|
||||||
|
if: >-
|
||||||
|
steps.version.outputs.skip != 'true' &&
|
||||||
|
steps.version.outputs.stability == 'stable' &&
|
||||||
|
secrets.GH_TOKEN != ''
|
||||||
|
continue-on-error: true
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
|
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
||||||
|
MAJOR="${{ steps.version.outputs.major }}"
|
||||||
|
BRANCH="${{ steps.version.outputs.branch }}"
|
||||||
|
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
||||||
|
|
||||||
|
NOTES=$(php /tmp/moko-platform-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null || true)
|
||||||
|
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
|
||||||
|
echo "$NOTES" > /tmp/release_notes.md
|
||||||
|
|
||||||
|
EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/tags/$RELEASE_TAG" 2>/dev/null | jq -r ".tag_name // empty" || true)
|
||||||
|
|
||||||
|
if [ -z "$EXISTING" ]; then
|
||||||
|
gh release create "$RELEASE_TAG" \
|
||||||
|
--repo "$GH_REPO" \
|
||||||
|
--title "v${MAJOR} (latest: ${VERSION})" \
|
||||||
|
--notes-file /tmp/release_notes.md \
|
||||||
|
--target "$BRANCH" || true
|
||||||
|
else
|
||||||
|
gh release edit "$RELEASE_TAG" \
|
||||||
|
--repo "$GH_REPO" \
|
||||||
|
--title "v${MAJOR} (latest: ${VERSION})" || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Upload assets to GitHub mirror
|
||||||
|
for PKG in /tmp/${EXT_ELEMENT:-pkg}-${VERSION}.*; do
|
||||||
|
if [ -f "$PKG" ]; then
|
||||||
|
_RELID=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/tags/$RELEASE_TAG" 2>/dev/null | jq -r ".id // empty")
|
||||||
|
[ -n "$_RELID" ] && curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" -H "Content-Type: application/octet-stream" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/${_RELID}/assets?name=$(basename $PKG)" --data-binary "@$PKG" > /dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "GitHub mirror updated: ${GH_REPO} ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# -- STEP 10: Sync main branch to GitHub mirror ----------------------------
|
||||||
|
- name: "Step 10: Push main to GitHub mirror"
|
||||||
|
if: >-
|
||||||
|
steps.version.outputs.skip != 'true' &&
|
||||||
|
secrets.GH_TOKEN != ''
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
||||||
|
GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
|
||||||
|
GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
|
||||||
|
git remote add github "https://x-access-token:${{ secrets.GH_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
|
||||||
|
git remote set-url github "https://x-access-token:${{ secrets.GH_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
|
||||||
|
git fetch origin main --depth=1
|
||||||
|
git push github origin/main:refs/heads/main --force 2>/dev/null \
|
||||||
|
&& echo "main branch pushed to GitHub mirror" \
|
||||||
|
|| echo "WARNING: GitHub mirror push failed"
|
||||||
|
|
||||||
|
# -- Clean up lesser pre-releases (cascade) ---------------------------------
|
||||||
|
# stable → deletes all | rc → beta,alpha,dev | beta → alpha,dev | alpha → dev
|
||||||
|
- name: "Delete lesser pre-release channels"
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
php /tmp/moko-platform-api/cli/release_cascade.php \
|
||||||
|
--stability stable \
|
||||||
|
--token "${{ secrets.GA_TOKEN }}" \
|
||||||
|
--org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
|
||||||
|
--gitea-url "${GITEA_URL}" 2>/dev/null || true
|
||||||
|
|
||||||
|
- name: "Step 11: Delete and recreate dev branch from main"
|
||||||
|
if: steps.version.outputs.skip != 'true'
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
TOKEN="${{ secrets.GA_TOKEN }}"
|
||||||
|
|
||||||
|
# Delete dev branch
|
||||||
|
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
|
||||||
|
"${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
|
||||||
|
|
||||||
|
# Recreate dev from main (now includes version bump + changelog promotion)
|
||||||
|
curl -sf -X POST -H "Authorization: token ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API_BASE}/branches" \
|
||||||
|
-d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
|
||||||
|
|
||||||
|
echo "Dev branch reset from main (keeps dev ahead after release)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
|
||||||
|
# -- Dolibarr post-release: Reset dev version -----------------------------
|
||||||
|
- name: "Dolibarr: Reset dev version"
|
||||||
|
if: >-
|
||||||
|
steps.version.outputs.skip != 'true' &&
|
||||||
|
steps.platform.outputs.platform == 'dolibarr' &&
|
||||||
|
steps.platform.outputs.mod_file != ''
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
TOKEN="${{ secrets.GA_TOKEN }}"
|
||||||
|
MOD_FILE="${{ steps.platform.outputs.mod_file }}"
|
||||||
|
ENCODED_PATH=$(echo "$MOD_FILE" | sed 's|^\./||' | python3 -c "import sys,urllib.parse; print(urllib.parse.quote(sys.stdin.read().strip()))")
|
||||||
|
FILE_RESP=$(curl -sf -H "Authorization: token ${TOKEN}" "${API_BASE}/contents/${ENCODED_PATH}?ref=dev" 2>/dev/null || true)
|
||||||
|
FILE_SHA=$(echo "$FILE_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
|
||||||
|
FILE_CONTENT=$(echo "$FILE_RESP" | python3 -c "import sys,json,base64; print(base64.b64decode(json.load(sys.stdin).get('content','')).decode())" 2>/dev/null || true)
|
||||||
|
if [ -n "$FILE_SHA" ] && [ -n "$FILE_CONTENT" ]; then
|
||||||
|
UPDATED=$(echo "$FILE_CONTENT" | sed "s/\$this->version = '[^']*'/\$this->version = 'development'/")
|
||||||
|
ENCODED=$(echo "$UPDATED" | base64 -w0)
|
||||||
|
curl -sf -X PUT -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/contents/${ENCODED_PATH}" \
|
||||||
|
-d "$(jq -n --arg content \"$ENCODED\" --arg sha \"$FILE_SHA\" --arg msg \"chore(version): reset dev version [skip ci]\" --arg branch \"dev\" '{content:$content,sha:$sha,message:$msg,branch:$branch}')" > /dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -- Summary --------------------------------------------------------------
|
||||||
|
- name: Pipeline Summary
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
|
PLATFORM="${{ steps.platform.outputs.platform }}"
|
||||||
|
if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
|
||||||
|
echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
|
||||||
|
elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
|
||||||
|
echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: MokoStandards.Maintenance
|
||||||
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
||||||
|
# PATH: /templates/workflows/cascade-dev.yml.template
|
||||||
|
# VERSION: 02.00.00
|
||||||
|
# BRIEF: Forward-merge main → all open branches after every push to main
|
||||||
|
#
|
||||||
|
# +========================================================================+
|
||||||
|
# | CASCADE MAIN → ALL BRANCHES |
|
||||||
|
# +========================================================================+
|
||||||
|
# | |
|
||||||
|
# | Triggers on every push to main (PR merges, bot commits, etc.) |
|
||||||
|
# | |
|
||||||
|
# | 1. List all branches matching: dev, rc/*, beta/*, alpha/* |
|
||||||
|
# | 2. For each: create PR (main → branch), auto-merge if clean |
|
||||||
|
# | 3. On conflict: leave PR open for manual resolution |
|
||||||
|
# | |
|
||||||
|
# +========================================================================+
|
||||||
|
|
||||||
|
name: "Universal: Cascade Main → Dev"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||||||
|
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cascade:
|
||||||
|
name: Cascade main → branches
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: >-
|
||||||
|
!contains(github.event.head_commit.message, '[skip ci]') &&
|
||||||
|
!contains(github.event.head_commit.message, '[skip cascade]')
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Discover target branches
|
||||||
|
id: branches
|
||||||
|
env:
|
||||||
|
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||||
|
run: |
|
||||||
|
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
|
||||||
|
# Fetch all branches (paginated)
|
||||||
|
PAGE=1
|
||||||
|
ALL_BRANCHES=""
|
||||||
|
while true; do
|
||||||
|
BATCH=$(curl -sS \
|
||||||
|
-H "Authorization: token ${GA_TOKEN}" \
|
||||||
|
"${API}/branches?page=${PAGE}&limit=50" \
|
||||||
|
| jq -r '.[].name // empty')
|
||||||
|
[ -z "$BATCH" ] && break
|
||||||
|
ALL_BRANCHES="$ALL_BRANCHES $BATCH"
|
||||||
|
PAGE=$((PAGE + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
# Filter to cascade targets: dev, dev/*, rc/*, beta/*, alpha/*
|
||||||
|
TARGETS=""
|
||||||
|
for BRANCH in $ALL_BRANCHES; do
|
||||||
|
case "$BRANCH" in
|
||||||
|
dev|dev/*|rc/*|beta/*|alpha/*)
|
||||||
|
TARGETS="$TARGETS $BRANCH"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
TARGETS=$(echo "$TARGETS" | xargs) # trim whitespace
|
||||||
|
|
||||||
|
if [ -z "$TARGETS" ]; then
|
||||||
|
echo "targets=" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "ℹ️ No cascade target branches found"
|
||||||
|
else
|
||||||
|
echo "targets=$TARGETS" >> "$GITHUB_OUTPUT"
|
||||||
|
COUNT=$(echo "$TARGETS" | wc -w)
|
||||||
|
echo "📋 Found ${COUNT} target branch(es): ${TARGETS}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Cascade to all target branches
|
||||||
|
if: steps.branches.outputs.targets != ''
|
||||||
|
env:
|
||||||
|
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||||
|
run: |
|
||||||
|
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
SHORT_SHA="${GITHUB_SHA:0:7}"
|
||||||
|
TARGETS="${{ steps.branches.outputs.targets }}"
|
||||||
|
|
||||||
|
SUCCESS=0
|
||||||
|
CONFLICTS=0
|
||||||
|
SKIPPED=0
|
||||||
|
FAILED=0
|
||||||
|
|
||||||
|
for BRANCH in $TARGETS; do
|
||||||
|
echo ""
|
||||||
|
echo "═══ main → ${BRANCH} ═══"
|
||||||
|
|
||||||
|
# Check if branch is already up to date
|
||||||
|
ENCODED_BRANCH=$(echo "$BRANCH" | sed 's|/|%2F|g')
|
||||||
|
RESPONSE=$(curl -sS \
|
||||||
|
-H "Authorization: token ${GA_TOKEN}" \
|
||||||
|
"${API}/compare/${ENCODED_BRANCH}...main")
|
||||||
|
|
||||||
|
AHEAD=$(echo "$RESPONSE" | jq '.total_commits // 0')
|
||||||
|
|
||||||
|
if [ "$AHEAD" -eq 0 ]; then
|
||||||
|
echo " ✅ Already up to date"
|
||||||
|
SKIPPED=$((SKIPPED + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " ℹ️ main is ${AHEAD} commit(s) ahead"
|
||||||
|
|
||||||
|
# Check for existing cascade PR
|
||||||
|
EXISTING=$(curl -sS \
|
||||||
|
-H "Authorization: token ${GA_TOKEN}" \
|
||||||
|
"${API}/pulls?state=open&head=${GITEA_ORG}:main&base=${ENCODED_BRANCH}&limit=1")
|
||||||
|
|
||||||
|
EXISTING_COUNT=$(echo "$EXISTING" | jq 'length')
|
||||||
|
PR_NUMBER=""
|
||||||
|
|
||||||
|
if [ "$EXISTING_COUNT" -gt 0 ]; then
|
||||||
|
PR_NUMBER=$(echo "$EXISTING" | jq -r '.[0].number')
|
||||||
|
echo " ℹ️ Reusing existing PR #${PR_NUMBER}"
|
||||||
|
else
|
||||||
|
# Create cascade PR
|
||||||
|
PR_RESPONSE=$(curl -sS -w "\n%{http_code}" \
|
||||||
|
-X POST \
|
||||||
|
-H "Authorization: token ${GA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{
|
||||||
|
\"title\": \"chore: cascade main → ${BRANCH} (${SHORT_SHA}) [skip ci]\",
|
||||||
|
\"body\": \"## Automatic cascade\\n\\nForward-merging \`main\` (${SHORT_SHA}) into \`${BRANCH}\`.\\n\\nIf conflicts exist, resolve manually and merge.\\n\\n> Auto-created by **Cascade Main → Dev**.\",
|
||||||
|
\"head\": \"main\",
|
||||||
|
\"base\": \"${BRANCH}\"
|
||||||
|
}" \
|
||||||
|
"${API}/pulls")
|
||||||
|
|
||||||
|
HTTP_CODE=$(echo "$PR_RESPONSE" | tail -1)
|
||||||
|
BODY=$(echo "$PR_RESPONSE" | sed '$d')
|
||||||
|
PR_NUMBER=$(echo "$BODY" | jq -r '.number // empty')
|
||||||
|
|
||||||
|
if [ "$HTTP_CODE" != "201" ] || [ -z "$PR_NUMBER" ]; then
|
||||||
|
MSG=$(echo "$BODY" | jq -r '.message // .' 2>/dev/null | head -1)
|
||||||
|
echo " ❌ Failed to create PR (HTTP ${HTTP_CODE}): ${MSG}"
|
||||||
|
FAILED=$((FAILED + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " ✅ Created PR #${PR_NUMBER}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Try auto-merge
|
||||||
|
PR_DATA=$(curl -sS \
|
||||||
|
-H "Authorization: token ${GA_TOKEN}" \
|
||||||
|
"${API}/pulls/${PR_NUMBER}")
|
||||||
|
|
||||||
|
MERGEABLE=$(echo "$PR_DATA" | jq -r '.mergeable // false')
|
||||||
|
|
||||||
|
if [ "$MERGEABLE" != "true" ]; then
|
||||||
|
echo " ⚠️ Conflicts — PR #${PR_NUMBER} left open"
|
||||||
|
CONFLICTS=$((CONFLICTS + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
MERGE_RESPONSE=$(curl -sS -w "\n%{http_code}" \
|
||||||
|
-X POST \
|
||||||
|
-H "Authorization: token ${GA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{
|
||||||
|
\"Do\": \"merge\",
|
||||||
|
\"merge_message_field\": \"chore: cascade main → ${BRANCH} [skip ci]\",
|
||||||
|
\"delete_branch_after_merge\": false
|
||||||
|
}" \
|
||||||
|
"${API}/pulls/${PR_NUMBER}/merge")
|
||||||
|
|
||||||
|
MERGE_HTTP=$(echo "$MERGE_RESPONSE" | tail -1)
|
||||||
|
|
||||||
|
if [ "$MERGE_HTTP" = "200" ] || [ "$MERGE_HTTP" = "204" ]; then
|
||||||
|
echo " ✅ Merged — ${BRANCH} is in sync"
|
||||||
|
SUCCESS=$((SUCCESS + 1))
|
||||||
|
else
|
||||||
|
MERGE_BODY=$(echo "$MERGE_RESPONSE" | sed '$d')
|
||||||
|
echo " ⚠️ Merge failed (HTTP ${MERGE_HTTP}) — PR #${PR_NUMBER} left open"
|
||||||
|
CONFLICTS=$((CONFLICTS + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
echo ""
|
||||||
|
echo "════════════════════════════════════════"
|
||||||
|
echo " ✅ Merged: ${SUCCESS}"
|
||||||
|
echo " ⚠️ Conflicts: ${CONFLICTS}"
|
||||||
|
echo " ⏭️ Up to date: ${SKIPPED}"
|
||||||
|
echo " ❌ Failed: ${FAILED}"
|
||||||
|
echo "════════════════════════════════════════"
|
||||||
|
|
||||||
|
if [ "$FAILED" -gt 0 ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -0,0 +1,450 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# This file is part of a Moko Consulting project.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow.Template
|
||||||
|
# INGROUP: MokoStandards.CI
|
||||||
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
||||||
|
# PATH: /templates/workflows/joomla/ci-joomla.yml.template
|
||||||
|
# VERSION: 04.06.00
|
||||||
|
# BRIEF: CI workflow for Joomla extensions — lint, validate, test
|
||||||
|
|
||||||
|
name: "Joomla: Extension CI"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- 'dev/**'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint-and-validate:
|
||||||
|
name: Lint & Validate
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
run: |
|
||||||
|
php -v && composer --version
|
||||||
|
|
||||||
|
- name: Clone MokoStandards
|
||||||
|
env:
|
||||||
|
GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
|
||||||
|
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
|
||||||
|
MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
|
||||||
|
run: |
|
||||||
|
git clone --depth 1 --branch main --quiet \
|
||||||
|
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
|
||||||
|
/tmp/mokostandards-api
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
env:
|
||||||
|
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
|
||||||
|
run: |
|
||||||
|
if [ -f "composer.json" ]; then
|
||||||
|
composer install \
|
||||||
|
--no-interaction \
|
||||||
|
--prefer-dist \
|
||||||
|
--optimize-autoloader
|
||||||
|
else
|
||||||
|
echo "No composer.json found — skipping dependency install"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: PHP syntax check
|
||||||
|
run: |
|
||||||
|
ERRORS=0
|
||||||
|
for DIR in src/ htdocs/; do
|
||||||
|
if [ -d "$DIR" ]; then
|
||||||
|
FOUND=1
|
||||||
|
while IFS= read -r -d '' FILE; do
|
||||||
|
OUTPUT=$(php -l "$FILE" 2>&1)
|
||||||
|
if echo "$OUTPUT" | grep -q "Parse error"; then
|
||||||
|
echo "::error file=${FILE}::${OUTPUT}"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
done < <(find "$DIR" -name "*.php" -print0)
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "### PHP Syntax Check" >> $GITHUB_STEP_SUMMARY
|
||||||
|
if [ "${ERRORS}" -gt 0 ]; then
|
||||||
|
echo "**${ERRORS} syntax error(s) found.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "All PHP files passed syntax check." >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: XML manifest validation
|
||||||
|
run: |
|
||||||
|
echo "### XML Manifest Validation" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=0
|
||||||
|
|
||||||
|
# Find the extension manifest (XML with <extension tag)
|
||||||
|
MANIFEST=""
|
||||||
|
for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
|
||||||
|
if grep -q "<extension" "$XML_FILE" 2>/dev/null; then
|
||||||
|
MANIFEST="$XML_FILE"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$MANIFEST" ]; then
|
||||||
|
echo "No Joomla extension manifest found (XML file with \`<extension\` tag)." >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "Manifest found: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# Validate well-formed XML
|
||||||
|
php -r "
|
||||||
|
\$xml = @simplexml_load_file('$MANIFEST');
|
||||||
|
if (\$xml === false) {
|
||||||
|
echo 'INVALID';
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
echo 'VALID';
|
||||||
|
" > /tmp/xml_result 2>&1
|
||||||
|
XML_RESULT=$(cat /tmp/xml_result)
|
||||||
|
if [ "$XML_RESULT" != "VALID" ]; then
|
||||||
|
echo "Manifest is not well-formed XML." >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "Manifest is well-formed XML." >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check required tags: name, version, author, namespace (Joomla 5+)
|
||||||
|
for TAG in name version author namespace; do
|
||||||
|
if ! grep -q "<${TAG}>" "$MANIFEST" 2>/dev/null; then
|
||||||
|
echo "Missing required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "Found required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${ERRORS}" -gt 0 ]; then
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "**${ERRORS} manifest issue(s) found.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "**Manifest validation passed.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Check language files referenced in manifest
|
||||||
|
run: |
|
||||||
|
echo "### Language File Check" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=0
|
||||||
|
|
||||||
|
MANIFEST=""
|
||||||
|
for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
|
||||||
|
if grep -q "<extension" "$XML_FILE" 2>/dev/null; then
|
||||||
|
MANIFEST="$XML_FILE"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -n "$MANIFEST" ]; then
|
||||||
|
# Extract language file references from manifest
|
||||||
|
LANG_FILES=$(grep -oP 'language\s+tag="[^"]*"[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null || true)
|
||||||
|
if [ -z "$LANG_FILES" ]; then
|
||||||
|
echo "No language file references found in manifest — skipping." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
while IFS= read -r LANG_FILE; do
|
||||||
|
LANG_FILE=$(echo "$LANG_FILE" | xargs)
|
||||||
|
if [ -z "$LANG_FILE" ]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
# Check in common locations
|
||||||
|
FOUND=0
|
||||||
|
for BASE in "." "src" "htdocs"; do
|
||||||
|
if [ -f "${BASE}/${LANG_FILE}" ]; then
|
||||||
|
FOUND=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ "$FOUND" -eq 0 ]; then
|
||||||
|
echo "Missing language file: \`${LANG_FILE}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "Language file present: \`${LANG_FILE}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
done <<< "$LANG_FILES"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "No manifest found — skipping language check." >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${ERRORS}" -gt 0 ]; then
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "**${ERRORS} missing language file(s).**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "**Language file check passed.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Check index.html files in directories
|
||||||
|
run: |
|
||||||
|
echo "### Index.html Check" >> $GITHUB_STEP_SUMMARY
|
||||||
|
MISSING=0
|
||||||
|
CHECKED=0
|
||||||
|
|
||||||
|
for DIR in src/ htdocs/; do
|
||||||
|
if [ -d "$DIR" ]; then
|
||||||
|
while IFS= read -r -d '' SUBDIR; do
|
||||||
|
CHECKED=$((CHECKED + 1))
|
||||||
|
if [ ! -f "${SUBDIR}/index.html" ]; then
|
||||||
|
echo "Missing index.html in: \`${SUBDIR}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
MISSING=$((MISSING + 1))
|
||||||
|
fi
|
||||||
|
done < <(find "$DIR" -type d -print0)
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "${CHECKED}" -eq 0 ]; then
|
||||||
|
echo "No src/ or htdocs/ directories found — skipping." >> $GITHUB_STEP_SUMMARY
|
||||||
|
elif [ "${MISSING}" -gt 0 ]; then
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "**${MISSING} director(ies) missing index.html out of ${CHECKED} checked.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "All ${CHECKED} directories contain index.html." >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
release-readiness:
|
||||||
|
name: Release Readiness Check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event_name == 'pull_request' && github.base_ref == 'main'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
|
- name: Validate release readiness
|
||||||
|
run: |
|
||||||
|
echo "## Release Readiness" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=0
|
||||||
|
|
||||||
|
# Extract version from README.md
|
||||||
|
README_VERSION=$(grep -oP '^\s*VERSION:\s*\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' README.md | head -1)
|
||||||
|
if [ -z "$README_VERSION" ]; then
|
||||||
|
echo "No VERSION found in README.md FILE INFORMATION block." >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "README version: \`${README_VERSION}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Find the extension manifest
|
||||||
|
MANIFEST=""
|
||||||
|
for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
|
||||||
|
if grep -q "<extension" "$XML_FILE" 2>/dev/null; then
|
||||||
|
MANIFEST="$XML_FILE"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$MANIFEST" ]; then
|
||||||
|
echo "No Joomla extension manifest found." >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "Manifest: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# Check <version> matches README VERSION
|
||||||
|
MANIFEST_VERSION=$(grep -oP '<version>\K[^<]+' "$MANIFEST" | head -1)
|
||||||
|
if [ -z "$MANIFEST_VERSION" ]; then
|
||||||
|
echo "No \`<version>\` tag in manifest." >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
elif [ -n "$README_VERSION" ] && [ "$MANIFEST_VERSION" != "$README_VERSION" ]; then
|
||||||
|
echo "Manifest version \`${MANIFEST_VERSION}\` does not match README \`${README_VERSION}\`." >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "Manifest version: \`${MANIFEST_VERSION}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check extension type, element, client attributes
|
||||||
|
EXT_TYPE=$(grep -oP '<extension[^>]*\btype="\K[^"]+' "$MANIFEST" | head -1)
|
||||||
|
if [ -z "$EXT_TYPE" ]; then
|
||||||
|
echo "Missing \`type\` attribute on \`<extension>\` tag." >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "Extension type: \`${EXT_TYPE}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Element check (component/module/plugin name)
|
||||||
|
HAS_ELEMENT=$(grep -cP '<(element|name)>' "$MANIFEST" 2>/dev/null || echo "0")
|
||||||
|
if [ "$HAS_ELEMENT" -eq 0 ]; then
|
||||||
|
echo "Missing \`<element>\` or \`<name>\` in manifest." >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Client attribute for site/admin modules and plugins
|
||||||
|
if echo "$EXT_TYPE" | grep -qP "^(module|plugin)$"; then
|
||||||
|
HAS_CLIENT=$(grep -cP '<extension[^>]*\bclient=' "$MANIFEST" 2>/dev/null || echo "0")
|
||||||
|
if [ "$HAS_CLIENT" -eq 0 ]; then
|
||||||
|
echo "Missing \`client\` attribute for ${EXT_TYPE} extension." >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check updates.xml exists
|
||||||
|
if [ -f "updates.xml" ] || [ -f "updates.xml" ]; then
|
||||||
|
echo "Update XML present." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "No updates.xml found." >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check CHANGELOG.md exists
|
||||||
|
if [ -f "CHANGELOG.md" ]; then
|
||||||
|
echo "CHANGELOG.md present." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "No CHANGELOG.md found." >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
if [ $ERRORS -gt 0 ]; then
|
||||||
|
echo "**${ERRORS} issue(s) must be resolved before release.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "**Extension is ready for release.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
test:
|
||||||
|
name: Tests (PHP ${{ matrix.php }})
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: lint-and-validate
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
php: ['8.2', '8.3']
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
|
- name: Setup PHP ${{ matrix.php }}
|
||||||
|
run: |
|
||||||
|
php -v && composer --version
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
env:
|
||||||
|
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
|
||||||
|
run: |
|
||||||
|
if [ -f "composer.json" ]; then
|
||||||
|
composer install \
|
||||||
|
--no-interaction \
|
||||||
|
--prefer-dist \
|
||||||
|
--optimize-autoloader
|
||||||
|
else
|
||||||
|
echo "No composer.json found — skipping dependency install"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: |
|
||||||
|
echo "### Test Results (PHP ${{ matrix.php }})" >> $GITHUB_STEP_SUMMARY
|
||||||
|
if [ -f "phpunit.xml" ] || [ -f "phpunit.xml.dist" ]; then
|
||||||
|
vendor/bin/phpunit --testdox 2>&1 | tee /tmp/test-output.log
|
||||||
|
EXIT=${PIPESTATUS[0]}
|
||||||
|
if [ $EXIT -eq 0 ]; then
|
||||||
|
echo "All tests passed." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "Test failures detected — see log." >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
|
cat /tmp/test-output.log >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
exit $EXIT
|
||||||
|
else
|
||||||
|
echo "No phpunit.xml found — skipping tests." >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
static-analysis:
|
||||||
|
name: PHPStan Analysis
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: lint-and-validate
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
run: php -v && composer --version
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
env:
|
||||||
|
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
|
||||||
|
run: |
|
||||||
|
if [ -f "composer.json" ]; then
|
||||||
|
composer install --no-interaction --prefer-dist --optimize-autoloader
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Install PHPStan
|
||||||
|
run: |
|
||||||
|
if ! command -v vendor/bin/phpstan &> /dev/null; then
|
||||||
|
composer require --dev phpstan/phpstan --no-interaction 2>/dev/null || \
|
||||||
|
composer global require phpstan/phpstan --no-interaction
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Run PHPStan
|
||||||
|
run: |
|
||||||
|
echo "### PHPStan Static Analysis" >> $GITHUB_STEP_SUMMARY
|
||||||
|
PHPSTAN="vendor/bin/phpstan"
|
||||||
|
if [ ! -f "$PHPSTAN" ]; then
|
||||||
|
PHPSTAN=$(composer global config bin-dir --absolute 2>/dev/null)/phpstan
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Determine source directory
|
||||||
|
SRC_DIR=""
|
||||||
|
for DIR in src/ htdocs/ lib/; do
|
||||||
|
if [ -d "$DIR" ]; then
|
||||||
|
SRC_DIR="$DIR"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$SRC_DIR" ]; then
|
||||||
|
echo "No source directory found (src/, htdocs/, lib/) — skipping." >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use repo phpstan.neon if present, otherwise use baseline config
|
||||||
|
ARGS="analyse ${SRC_DIR} --memory-limit=512M --no-progress --error-format=table"
|
||||||
|
if [ -f "phpstan.neon" ] || [ -f "phpstan.neon.dist" ]; then
|
||||||
|
echo "Using project PHPStan config." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
ARGS="$ARGS --level=3"
|
||||||
|
echo "No phpstan.neon found — using level 3 (type inference)." >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
$PHPSTAN $ARGS 2>&1 | tee /tmp/phpstan-output.txt
|
||||||
|
EXIT=${PIPESTATUS[0]}
|
||||||
|
|
||||||
|
if [ $EXIT -eq 0 ]; then
|
||||||
|
echo "**No errors found.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
ERRORS=$(grep -c "ERROR" /tmp/phpstan-output.txt 2>/dev/null || echo "some")
|
||||||
|
echo "**${ERRORS} error(s) found.** Review output above." >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
|
tail -30 /tmp/phpstan-output.txt >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
exit $EXIT
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: MokoStandards.Maintenance
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
||||||
|
# PATH: /.gitea/workflows/cleanup.yml
|
||||||
|
# VERSION: 01.00.00
|
||||||
|
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
|
||||||
|
|
||||||
|
name: "Universal: Repository Cleanup"
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 3 * * 0' # Weekly on Sunday at 03:00 UTC
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
env:
|
||||||
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cleanup:
|
||||||
|
name: Clean Merged Branches
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.GA_TOKEN }}
|
||||||
|
|
||||||
|
- name: Delete merged branches
|
||||||
|
env:
|
||||||
|
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||||
|
run: |
|
||||||
|
echo "=== Merged Branch Cleanup ==="
|
||||||
|
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||||
|
|
||||||
|
# List branches via API
|
||||||
|
BRANCHES=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
|
||||||
|
"${API}/branches?limit=50" | jq -r '.[].name')
|
||||||
|
|
||||||
|
DELETED=0
|
||||||
|
for BRANCH in $BRANCHES; do
|
||||||
|
# Skip protected branches
|
||||||
|
case "$BRANCH" in
|
||||||
|
main|master|develop|release/*|hotfix/*) continue ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Check if branch is merged into main
|
||||||
|
if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then
|
||||||
|
echo " Deleting merged branch: ${BRANCH}"
|
||||||
|
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
|
||||||
|
"${API}/branches/${BRANCH}" 2>/dev/null || true
|
||||||
|
DELETED=$((DELETED + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Deleted ${DELETED} merged branch(es)"
|
||||||
|
|
||||||
|
- name: Clean old workflow runs
|
||||||
|
env:
|
||||||
|
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||||
|
run: |
|
||||||
|
echo "=== Workflow Run Cleanup ==="
|
||||||
|
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||||
|
CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ)
|
||||||
|
|
||||||
|
# Get old completed runs
|
||||||
|
RUNS=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
|
||||||
|
"${API}/actions/runs?status=completed&limit=50" | \
|
||||||
|
jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null)
|
||||||
|
|
||||||
|
DELETED=0
|
||||||
|
for RUN_ID in $RUNS; do
|
||||||
|
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
|
||||||
|
"${API}/actions/runs/${RUN_ID}" 2>/dev/null || true
|
||||||
|
DELETED=$((DELETED + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Deleted ${DELETED} old workflow run(s)"
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: MokoStandards.Security
|
||||||
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
||||||
|
# PATH: /templates/workflows/gitleaks.yml.template
|
||||||
|
# VERSION: 01.00.00
|
||||||
|
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
|
||||||
|
#
|
||||||
|
# +========================================================================+
|
||||||
|
# | SECRET SCANNING |
|
||||||
|
# +========================================================================+
|
||||||
|
# | |
|
||||||
|
# | Scans commits for leaked secrets using Gitleaks. |
|
||||||
|
# | |
|
||||||
|
# | - PR scan: only new commits in the PR |
|
||||||
|
# | - Scheduled: full repo scan weekly |
|
||||||
|
# | - Alerts via ntfy on findings |
|
||||||
|
# | |
|
||||||
|
# +========================================================================+
|
||||||
|
|
||||||
|
name: "Universal: Secret Scanning"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- 'dev/**'
|
||||||
|
schedule:
|
||||||
|
- cron: '0 5 * * 1' # Weekly Monday 05:00 UTC
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
env:
|
||||||
|
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
|
||||||
|
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
gitleaks:
|
||||||
|
name: Gitleaks Secret Scan
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Install Gitleaks
|
||||||
|
run: |
|
||||||
|
GITLEAKS_VERSION="8.21.2"
|
||||||
|
curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \
|
||||||
|
| tar -xz -C /usr/local/bin gitleaks
|
||||||
|
gitleaks version
|
||||||
|
|
||||||
|
- name: Scan for secrets
|
||||||
|
id: scan
|
||||||
|
run: |
|
||||||
|
echo "### Secret Scanning" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ARGS="--source . --verbose --report-format json --report-path /tmp/gitleaks-report.json"
|
||||||
|
|
||||||
|
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
||||||
|
# Scan only PR commits
|
||||||
|
ARGS="$ARGS --log-opts=${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}"
|
||||||
|
echo "Scanning PR commits only" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "Full repository scan" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
if gitleaks detect $ARGS 2>&1; then
|
||||||
|
echo "result=clean" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "**No secrets detected.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "result=found" >> "$GITHUB_OUTPUT"
|
||||||
|
FINDINGS=$(jq length /tmp/gitleaks-report.json 2>/dev/null || echo "unknown")
|
||||||
|
echo "**${FINDINGS} potential secret(s) detected.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Review the findings and rotate any exposed credentials immediately." >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Notify on findings
|
||||||
|
if: failure() && steps.scan.outputs.result == 'found'
|
||||||
|
run: |
|
||||||
|
REPO="${{ github.event.repository.name }}"
|
||||||
|
curl -sS \
|
||||||
|
-H "Title: ${REPO} — secrets detected in code" \
|
||||||
|
-H "Tags: rotating_light,key" \
|
||||||
|
-H "Priority: urgent" \
|
||||||
|
-d "Gitleaks found potential secrets. Review and rotate credentials immediately." \
|
||||||
|
"${NTFY_URL}/${NTFY_TOPIC}" || true
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: MokoStandards.Notifications
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
||||||
|
# PATH: /.gitea/workflows/notify.yml
|
||||||
|
# VERSION: 01.00.00
|
||||||
|
# BRIEF: Push notifications via ntfy on release success or workflow failure
|
||||||
|
|
||||||
|
name: "Universal: Notifications"
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows:
|
||||||
|
- "Joomla Build & Release"
|
||||||
|
- "Joomla Extension CI"
|
||||||
|
- "Deploy"
|
||||||
|
- "Cascade Main → Dev"
|
||||||
|
types:
|
||||||
|
- completed
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
env:
|
||||||
|
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
|
||||||
|
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-releases' }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
notify:
|
||||||
|
name: Send Notification
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: >-
|
||||||
|
github.event.workflow_run.conclusion == 'success' ||
|
||||||
|
github.event.workflow_run.conclusion == 'failure'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Notify on success (releases only)
|
||||||
|
if: >-
|
||||||
|
github.event.workflow_run.conclusion == 'success' &&
|
||||||
|
contains(github.event.workflow_run.name, 'Release')
|
||||||
|
run: |
|
||||||
|
REPO="${{ github.event.repository.name }}"
|
||||||
|
WORKFLOW="${{ github.event.workflow_run.name }}"
|
||||||
|
URL="${{ github.event.workflow_run.html_url }}"
|
||||||
|
|
||||||
|
curl -sS \
|
||||||
|
-H "Title: ${REPO} released" \
|
||||||
|
-H "Tags: white_check_mark,package" \
|
||||||
|
-H "Priority: default" \
|
||||||
|
-H "Click: ${URL}" \
|
||||||
|
-d "${WORKFLOW} completed successfully." \
|
||||||
|
"${NTFY_URL}/${NTFY_TOPIC}"
|
||||||
|
|
||||||
|
- name: Notify on failure
|
||||||
|
if: github.event.workflow_run.conclusion == 'failure'
|
||||||
|
run: |
|
||||||
|
REPO="${{ github.event.repository.name }}"
|
||||||
|
WORKFLOW="${{ github.event.workflow_run.name }}"
|
||||||
|
URL="${{ github.event.workflow_run.html_url }}"
|
||||||
|
|
||||||
|
curl -sS \
|
||||||
|
-H "Title: ${REPO} workflow failed" \
|
||||||
|
-H "Tags: x,warning" \
|
||||||
|
-H "Priority: high" \
|
||||||
|
-H "Click: ${URL}" \
|
||||||
|
-d "${WORKFLOW} failed. Check the run for details." \
|
||||||
|
"${NTFY_URL}/${NTFY_TOPIC}"
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: MokoStandards.CI
|
||||||
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
||||||
|
# PATH: /templates/workflows/universal/pr-check.yml.template
|
||||||
|
# VERSION: 05.00.00
|
||||||
|
# BRIEF: PR gate — branch policy + code validation before merge
|
||||||
|
|
||||||
|
name: "Universal: PR Check"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize, reopened, edited]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# ── Branch Policy ──────────────────────────────────────────────────────
|
||||||
|
branch-policy:
|
||||||
|
name: Branch Policy
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check branch merge target
|
||||||
|
run: |
|
||||||
|
HEAD="${{ github.head_ref }}"
|
||||||
|
BASE="${{ github.base_ref }}"
|
||||||
|
|
||||||
|
echo "PR: ${HEAD} → ${BASE}"
|
||||||
|
|
||||||
|
ALLOWED=true
|
||||||
|
REASON=""
|
||||||
|
|
||||||
|
case "$HEAD" in
|
||||||
|
feature/*|feat/*)
|
||||||
|
if [ "$BASE" != "dev" ]; then
|
||||||
|
ALLOWED=false
|
||||||
|
REASON="Feature branches must target 'dev', not '${BASE}'"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
fix/*|bugfix/*)
|
||||||
|
if [ "$BASE" != "dev" ]; then
|
||||||
|
ALLOWED=false
|
||||||
|
REASON="Fix branches must target 'dev', not '${BASE}'"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
hotfix/*)
|
||||||
|
if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
|
||||||
|
ALLOWED=false
|
||||||
|
REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
alpha/*|beta/*)
|
||||||
|
if [ "$BASE" != "dev" ]; then
|
||||||
|
ALLOWED=false
|
||||||
|
REASON="Pre-release branches must target 'dev', not '${BASE}'"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
rc/*)
|
||||||
|
if [ "$BASE" != "main" ]; then
|
||||||
|
ALLOWED=false
|
||||||
|
REASON="Release candidate branches must target 'main', not '${BASE}'"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
dev)
|
||||||
|
if [ "$BASE" != "main" ]; then
|
||||||
|
ALLOWED=false
|
||||||
|
REASON="Dev branch can only merge into 'main', not '${BASE}'"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ "$ALLOWED" = false ]; then
|
||||||
|
echo "::error::${REASON}"
|
||||||
|
echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "${REASON}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Branch policy: OK (${HEAD} → ${BASE})"
|
||||||
|
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# ── Code Validation ────────────────────────────────────────────────────
|
||||||
|
validate:
|
||||||
|
name: Validate PR
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Detect platform
|
||||||
|
id: platform
|
||||||
|
run: |
|
||||||
|
# Parse manifest for platform detection
|
||||||
|
PLATFORM=$(php /tmp/mokostandards-api/cli/manifest_read.php --path . --field platform 2>/dev/null)
|
||||||
|
[ -z "$PLATFORM" ] && PLATFORM="generic"
|
||||||
|
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
|
||||||
|
run: |
|
||||||
|
if ! command -v php &> /dev/null; then
|
||||||
|
sudo apt-get update -qq
|
||||||
|
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: PHP syntax check
|
||||||
|
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
|
||||||
|
run: |
|
||||||
|
ERRORS=0
|
||||||
|
while IFS= read -r -d '' file; do
|
||||||
|
if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0)
|
||||||
|
echo "PHP lint: ${ERRORS} error(s)"
|
||||||
|
[ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
|
||||||
|
|
||||||
|
- name: Validate platform manifest
|
||||||
|
run: |
|
||||||
|
PLATFORM="${{ steps.platform.outputs.platform }}"
|
||||||
|
case "$PLATFORM" in
|
||||||
|
joomla)
|
||||||
|
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
||||||
|
if [ -z "$MANIFEST" ]; then
|
||||||
|
echo "::warning::No Joomla manifest found (WaaS site)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "Manifest: ${MANIFEST}"
|
||||||
|
if command -v php &> /dev/null; then
|
||||||
|
php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::Manifest XML is malformed"; exit 1; }
|
||||||
|
fi
|
||||||
|
for ELEMENT in name version description; do
|
||||||
|
grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; }
|
||||||
|
done
|
||||||
|
echo "Joomla manifest valid"
|
||||||
|
;;
|
||||||
|
dolibarr)
|
||||||
|
MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1)
|
||||||
|
if [ -z "$MOD_FILE" ]; then
|
||||||
|
echo "::error::No mod*.class.php found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Dolibarr module: ${MOD_FILE}"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Generic platform — no manifest validation"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
- name: Check update stream format
|
||||||
|
run: |
|
||||||
|
PLATFORM="${{ steps.platform.outputs.platform }}"
|
||||||
|
case "$PLATFORM" in
|
||||||
|
joomla)
|
||||||
|
if [ -f "updates.xml" ]; then
|
||||||
|
if command -v php &> /dev/null; then
|
||||||
|
php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::updates.xml is malformed"; exit 1; }
|
||||||
|
fi
|
||||||
|
echo "updates.xml valid"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
dolibarr)
|
||||||
|
[ -f "update.txt" ] && echo "update.txt present" || echo "::warning::No update.txt"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
- name: Verify package source
|
||||||
|
run: |
|
||||||
|
SOURCE_DIR="src"
|
||||||
|
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
||||||
|
if [ ! -d "$SOURCE_DIR" ]; then
|
||||||
|
echo "::warning::No src/ or htdocs/ directory"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
|
||||||
|
echo "Source: ${FILE_COUNT} files"
|
||||||
|
[ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; }
|
||||||
|
|
||||||
|
# ── Changelog Gate ────────────────────────────────────────────────────
|
||||||
|
changelog:
|
||||||
|
name: Changelog Updated
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.base_ref == 'main'
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Check CHANGELOG.md was updated
|
||||||
|
run: |
|
||||||
|
BASE="${{ github.event.pull_request.base.sha }}"
|
||||||
|
HEAD="${{ github.event.pull_request.head.sha }}"
|
||||||
|
|
||||||
|
if git diff --name-only "$BASE" "$HEAD" | grep -q "^CHANGELOG.md$"; then
|
||||||
|
echo "CHANGELOG.md updated"
|
||||||
|
else
|
||||||
|
# Allow [skip changelog] in PR title or body
|
||||||
|
PR_TITLE="${{ github.event.pull_request.title }}"
|
||||||
|
PR_BODY="${{ github.event.pull_request.body }}"
|
||||||
|
if echo "$PR_TITLE $PR_BODY" | grep -qi "\[skip changelog\]"; then
|
||||||
|
echo "::warning::Changelog skip requested via [skip changelog]"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "::error::CHANGELOG.md must be updated before merging to main. Add [skip changelog] to the PR title to bypass."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -0,0 +1,260 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: moko-platform.Release
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
|
# PATH: /templates/workflows/universal/pre-release.yml.template
|
||||||
|
# VERSION: 05.00.00
|
||||||
|
# BRIEF: Manual pre-release — builds dev/alpha/beta/rc packages from any branch
|
||||||
|
|
||||||
|
name: "Universal: Pre-Release"
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
stability:
|
||||||
|
description: 'Pre-release channel'
|
||||||
|
required: true
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- development
|
||||||
|
- alpha
|
||||||
|
- beta
|
||||||
|
- release-candidate
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
env:
|
||||||
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||||||
|
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: "Build Pre-Release (${{ inputs.stability }})"
|
||||||
|
runs-on: release
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.GA_TOKEN }}
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
run: |
|
||||||
|
if ! command -v php &> /dev/null; then
|
||||||
|
sudo apt-get update -qq
|
||||||
|
sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Setup moko-platform tools
|
||||||
|
env:
|
||||||
|
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||||
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
|
run: |
|
||||||
|
git clone --depth 1 --branch main --quiet "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" /tmp/moko-platform-api
|
||||||
|
|
||||||
|
- name: Detect platform
|
||||||
|
id: platform
|
||||||
|
run: |
|
||||||
|
php /tmp/moko-platform-api/cli/manifest_read.php --path . --github-output
|
||||||
|
|
||||||
|
- name: Resolve metadata
|
||||||
|
id: meta
|
||||||
|
run: |
|
||||||
|
STABILITY="${{ inputs.stability }}"
|
||||||
|
MOKO_API="/tmp/moko-platform-api/cli"
|
||||||
|
|
||||||
|
case "$STABILITY" in
|
||||||
|
development) SUFFIX="-dev"; TAG="development" ;;
|
||||||
|
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
|
||||||
|
beta) SUFFIX="-beta"; TAG="beta" ;;
|
||||||
|
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Read current version from manifest (priority) or README — no bump yet
|
||||||
|
VERSION=$(php ${MOKO_API}/version_read.php --path .)
|
||||||
|
echo "Version: ${VERSION}"
|
||||||
|
|
||||||
|
# Ensure platform-specific manifest matches
|
||||||
|
php ${MOKO_API}/version_set_platform.php --path . --version "${VERSION}"
|
||||||
|
|
||||||
|
# Git setup for later commits
|
||||||
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
|
git config --local user.name "gitea-actions[bot]"
|
||||||
|
git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||||
|
|
||||||
|
# Detect element from Joomla/Dolibarr manifest
|
||||||
|
set +o pipefail
|
||||||
|
PLATFORM="${{ steps.platform.outputs.platform }}"
|
||||||
|
EXT_ELEMENT=$(php ${MOKO_API}/manifest_read.php --path . --field name 2>/dev/null | tr -d ' ' | tr '[:upper:]' '[:lower:]' || true)
|
||||||
|
# For Joomla, prefer <element> tag
|
||||||
|
if [ "$PLATFORM" = "joomla" ]; then
|
||||||
|
MANIFEST=$(find . -maxdepth 4 -name "*.xml" ! -path "./.git/*" -print0 2>/dev/null | xargs -0 grep -l '<extension' 2>/dev/null | head -1 || true)
|
||||||
|
if [ -n "$MANIFEST" ]; then
|
||||||
|
ELEM=$(grep -oP "<element>\K[^<]+" "$MANIFEST" 2>/dev/null | head -1 || true)
|
||||||
|
[ -n "$ELEM" ] && EXT_ELEMENT="$ELEM"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
|
ZIP_NAME="${EXT_ELEMENT}-${VERSION}${SUFFIX}.zip"
|
||||||
|
|
||||||
|
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
|
||||||
|
|
||||||
|
- name: Build package
|
||||||
|
id: zip
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
|
SUFFIX="${{ steps.meta.outputs.suffix }}"
|
||||||
|
PLATFORM="${{ steps.platform.outputs.platform }}"
|
||||||
|
|
||||||
|
if [ "$PLATFORM" = "joomla" ]; then
|
||||||
|
php /tmp/moko-platform-api/cli/joomla_build.php --path . --version "${VERSION}" --suffix "${SUFFIX}" --output build --github-output
|
||||||
|
else
|
||||||
|
# Generic build: zip src/ directory
|
||||||
|
SOURCE_DIR="src"
|
||||||
|
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
||||||
|
[ ! -d "$SOURCE_DIR" ] && { echo "::error::No src/ or htdocs/"; exit 1; }
|
||||||
|
EXT_ELEMENT="${{ steps.meta.outputs.ext_element }}"
|
||||||
|
ZIP_NAME="${EXT_ELEMENT}-${VERSION}${SUFFIX}.zip"
|
||||||
|
mkdir -p build
|
||||||
|
cd "$SOURCE_DIR" && zip -r "../build/${ZIP_NAME}" . && cd ..
|
||||||
|
SHA256=$(sha256sum "build/${ZIP_NAME}" | cut -d' ' -f1)
|
||||||
|
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "zip_path=build/${ZIP_NAME}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "sha256=${SHA256}" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Create or replace Gitea release
|
||||||
|
id: release
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
TAG="${{ steps.meta.outputs.tag }}"
|
||||||
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
|
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||||
|
SHA256="${{ steps.zip.outputs.sha256 }}"
|
||||||
|
ZIP_NAME="${{ steps.zip.outputs.zip_name }}"
|
||||||
|
EXT_ELEMENT="${{ steps.meta.outputs.ext_element }}"
|
||||||
|
TOKEN="${{ secrets.GA_TOKEN }}"
|
||||||
|
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
BRANCH=$(git branch --show-current)
|
||||||
|
|
||||||
|
BODY="## ${VERSION} ($(date +%Y-%m-%d))
|
||||||
|
**Channel:** ${STABILITY}
|
||||||
|
**SHA-256:** \`${SHA256}\`"
|
||||||
|
|
||||||
|
# Delete existing release
|
||||||
|
EXISTING_ID=$(curl -sS -H "Authorization: token ${TOKEN}" \
|
||||||
|
"${API}/releases/tags/${TAG}" | jq -r '.id // empty' 2>/dev/null)
|
||||||
|
if [ -n "$EXISTING_ID" ]; then
|
||||||
|
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
|
||||||
|
"${API}/releases/${EXISTING_ID}" 2>/dev/null || true
|
||||||
|
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
|
||||||
|
"${API}/tags/${TAG}" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create release
|
||||||
|
RELEASE_ID=$(curl -sS -X POST -H "Authorization: token ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API}/releases" \
|
||||||
|
-d "$(jq -n \
|
||||||
|
--arg tag "$TAG" \
|
||||||
|
--arg target "$BRANCH" \
|
||||||
|
--arg name "${EXT_ELEMENT} ${VERSION} (${STABILITY})" \
|
||||||
|
--arg body "$BODY" \
|
||||||
|
'{tag_name: $tag, target_commitish: $target, name: $name, body: $body, prerelease: true}'
|
||||||
|
)" | jq -r '.id')
|
||||||
|
|
||||||
|
echo "release_id=${RELEASE_ID}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
# Upload ZIP
|
||||||
|
curl -sS -X POST -H "Authorization: token ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/octet-stream" \
|
||||||
|
"${API}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" \
|
||||||
|
--data-binary "@${{ steps.zip.outputs.zip_path }}"
|
||||||
|
|
||||||
|
echo "Released: ${EXT_ELEMENT} ${VERSION} (${STABILITY})"
|
||||||
|
|
||||||
|
- name: "Update updates.xml"
|
||||||
|
if: steps.platform.outputs.platform == 'joomla'
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
|
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||||
|
SHA256="${{ steps.zip.outputs.sha256 }}"
|
||||||
|
php /tmp/moko-platform-api/cli/updates_xml_build.php --path . --version "$VERSION" --stability "$STABILITY" --sha "$SHA256" --gitea-url "$GITEA_URL" --org "$GITEA_ORG" --repo "$GITEA_REPO"
|
||||||
|
if ! git diff --quiet updates.xml 2>/dev/null; then
|
||||||
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
|
git config --local user.name "gitea-actions[bot]"
|
||||||
|
git add updates.xml
|
||||||
|
git commit -m "chore: update $STABILITY channel $VERSION [skip ci]"
|
||||||
|
git push origin HEAD 2>&1 || echo "WARNING: push failed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: "Sync updates.xml to all branches"
|
||||||
|
if: steps.platform.outputs.platform == 'joomla'
|
||||||
|
run: |
|
||||||
|
php /tmp/moko-platform-api/cli/updates_xml_sync.php --path . --current "${{ github.ref_name }}" --branches main,dev --version "${{ steps.meta.outputs.version }}" --token "${{ secrets.GA_TOKEN }}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" --gitea-url "${GITEA_URL}"
|
||||||
|
|
||||||
|
- name: "Delete lesser pre-release channels (cascade)"
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
TOKEN="${{ secrets.GA_TOKEN }}"
|
||||||
|
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||||
|
|
||||||
|
# Cascade: rc → beta,alpha,dev | beta → alpha,dev | alpha → dev | dev → nothing
|
||||||
|
case "$STABILITY" in
|
||||||
|
release-candidate) TAGS_TO_DELETE="beta alpha development" ;;
|
||||||
|
beta) TAGS_TO_DELETE="alpha development" ;;
|
||||||
|
alpha) TAGS_TO_DELETE="development" ;;
|
||||||
|
*) TAGS_TO_DELETE="" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
[ -z "$TAGS_TO_DELETE" ] && exit 0
|
||||||
|
|
||||||
|
for TAG in $TAGS_TO_DELETE; do
|
||||||
|
RELEASE_ID=$(curl -sS -H "Authorization: token ${TOKEN}" \
|
||||||
|
"${API_BASE}/releases/tags/${TAG}" 2>/dev/null | \
|
||||||
|
python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "None" ]; then
|
||||||
|
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
|
||||||
|
"${API_BASE}/releases/${RELEASE_ID}" 2>/dev/null || true
|
||||||
|
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
|
||||||
|
"${API_BASE}/tags/${TAG}" 2>/dev/null || true
|
||||||
|
echo "Deleted: ${TAG} (id: ${RELEASE_ID})"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: "Post-release version bump"
|
||||||
|
run: |
|
||||||
|
MOKO_API="/tmp/moko-platform-api/cli"
|
||||||
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
|
|
||||||
|
# Bump patch for next dev cycle
|
||||||
|
BUMP_OUTPUT=$(php ${MOKO_API}/version_bump.php --path .)
|
||||||
|
NEXT=$(echo "$BUMP_OUTPUT" | grep -oP '\d{2}\.\d{2}\.\d{2}$' || true)
|
||||||
|
[ -z "$NEXT" ] && exit 0
|
||||||
|
|
||||||
|
# Update platform-specific manifest to next version
|
||||||
|
php ${MOKO_API}/version_set_platform.php --path . --version "${NEXT}"
|
||||||
|
|
||||||
|
git add -A
|
||||||
|
git diff --cached --quiet || {
|
||||||
|
git commit -m "chore: update development channel ${VERSION} [skip ci]"
|
||||||
|
git push origin HEAD 2>&1
|
||||||
|
}
|
||||||
@@ -0,0 +1,766 @@
|
|||||||
|
# ============================================================================
|
||||||
|
# Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# This file is part of a Moko Consulting project.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: MokoStandards.Validation
|
||||||
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
||||||
|
# PATH: /templates/workflows/joomla/repo_health.yml.template
|
||||||
|
# VERSION: 04.06.00
|
||||||
|
# BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts.
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
name: "Joomla: Repo Health"
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: repo-health-${{ github.repository }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
profile:
|
||||||
|
description: 'Validation profile: all, release, scripts, or repo'
|
||||||
|
required: true
|
||||||
|
default: all
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- all
|
||||||
|
- release
|
||||||
|
- scripts
|
||||||
|
- repo
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
env:
|
||||||
|
# Release policy - Repository Variables Only
|
||||||
|
RELEASE_REQUIRED_REPO_VARS: RS_FTP_PATH_SUFFIX
|
||||||
|
RELEASE_OPTIONAL_REPO_VARS: DEV_FTP_SUFFIX
|
||||||
|
|
||||||
|
# Scripts governance policy
|
||||||
|
SCRIPTS_REQUIRED_DIRS:
|
||||||
|
SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate
|
||||||
|
|
||||||
|
# Repo health policy
|
||||||
|
REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.gitea/workflows/
|
||||||
|
REPO_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore,README.md,docs/
|
||||||
|
REPO_DISALLOWED_DIRS:
|
||||||
|
REPO_DISALLOWED_FILES: TODO.md,todo.md
|
||||||
|
|
||||||
|
# Extended checks toggles
|
||||||
|
EXTENDED_CHECKS: "true"
|
||||||
|
|
||||||
|
# File / directory variables
|
||||||
|
DOCS_INDEX: docs/docs-index.md
|
||||||
|
SCRIPT_DIR: scripts
|
||||||
|
WORKFLOWS_DIR: .gitea/workflows
|
||||||
|
SHELLCHECK_PATTERN: '*.sh'
|
||||||
|
SPDX_FILE_GLOBS: '*.sh,*.php,*.js,*.ts,*.css,*.xml,*.yml,*.yaml'
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
access_check:
|
||||||
|
name: Access control
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 10
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
allowed: ${{ steps.perm.outputs.allowed }}
|
||||||
|
permission: ${{ steps.perm.outputs.permission }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check actor permission (admin only)
|
||||||
|
id: perm
|
||||||
|
env:
|
||||||
|
TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
ACTOR: ${{ github.actor }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
ALLOWED=false
|
||||||
|
PERMISSION=unknown
|
||||||
|
METHOD=""
|
||||||
|
|
||||||
|
# Hardcoded authorized users — always allowed
|
||||||
|
case "$ACTOR" in
|
||||||
|
jmiller|gitea-actions[bot])
|
||||||
|
ALLOWED=true
|
||||||
|
PERMISSION=admin
|
||||||
|
METHOD="hardcoded allowlist"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
# Detect platform and check permissions via API
|
||||||
|
API_BASE="${GITHUB_API_URL:-${GITEA_API_URL:-https://api.github.com}}"
|
||||||
|
RESP=$(curl -sf -H "Authorization: token ${TOKEN}" \
|
||||||
|
"${API_BASE}/repos/${REPO}/collaborators/${ACTOR}/permission" 2>/dev/null || echo '{}')
|
||||||
|
PERMISSION=$(echo "$RESP" | grep -oP '"permission"\s*:\s*"\K[^"]+' || echo "unknown")
|
||||||
|
if [ "$PERMISSION" = "admin" ] || [ "$PERMISSION" = "maintain" ] || [ "$PERMISSION" = "owner" ]; then
|
||||||
|
ALLOWED=true
|
||||||
|
fi
|
||||||
|
METHOD="collaborator API"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo "permission=${PERMISSION}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "allowed=${ALLOWED}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "## Access Authorization"
|
||||||
|
echo ""
|
||||||
|
echo "| Field | Value |"
|
||||||
|
echo "|-------|-------|"
|
||||||
|
echo "| **Actor** | \`${ACTOR}\` |"
|
||||||
|
echo "| **Repository** | \`${REPO}\` |"
|
||||||
|
echo "| **Permission** | \`${PERMISSION}\` |"
|
||||||
|
echo "| **Method** | ${METHOD} |"
|
||||||
|
echo "| **Authorized** | ${ALLOWED} |"
|
||||||
|
echo ""
|
||||||
|
if [ "$ALLOWED" = "true" ]; then
|
||||||
|
echo "${ACTOR} authorized (${METHOD})"
|
||||||
|
else
|
||||||
|
echo "${ACTOR} is NOT authorized. Requires admin or maintain role."
|
||||||
|
fi
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
|
- name: Deny execution when not permitted
|
||||||
|
if: ${{ steps.perm.outputs.allowed != 'true' }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
printf '%s\n' 'ERROR: Access denied. Admin permission required.' >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
release_config:
|
||||||
|
name: Release configuration
|
||||||
|
needs: access_check
|
||||||
|
if: ${{ needs.access_check.outputs.allowed == 'true' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 20
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Guardrails release vars
|
||||||
|
env:
|
||||||
|
PROFILE_RAW: ${{ github.event.inputs.profile }}
|
||||||
|
RS_FTP_PATH_SUFFIX: ${{ vars.RS_FTP_PATH_SUFFIX }}
|
||||||
|
DEV_FTP_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
profile="${PROFILE_RAW:-all}"
|
||||||
|
case "${profile}" in
|
||||||
|
all|release|scripts|repo) ;;
|
||||||
|
*)
|
||||||
|
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ "${profile}" = 'scripts' ] || [ "${profile}" = 'repo' ]; then
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Release configuration (Repository Variables)'
|
||||||
|
printf '%s\n' "Profile: ${profile}"
|
||||||
|
printf '%s\n' 'Status: SKIPPED'
|
||||||
|
printf '%s\n' 'Reason: profile excludes release validation'
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
IFS=',' read -r -a required <<< "${RELEASE_REQUIRED_REPO_VARS}"
|
||||||
|
IFS=',' read -r -a optional <<< "${RELEASE_OPTIONAL_REPO_VARS}"
|
||||||
|
|
||||||
|
missing=()
|
||||||
|
missing_optional=()
|
||||||
|
|
||||||
|
for k in "${required[@]}"; do
|
||||||
|
v="${!k:-}"
|
||||||
|
[ -z "${v}" ] && missing+=("${k}")
|
||||||
|
done
|
||||||
|
|
||||||
|
for k in "${optional[@]}"; do
|
||||||
|
v="${!k:-}"
|
||||||
|
[ -z "${v}" ] && missing_optional+=("${k}")
|
||||||
|
done
|
||||||
|
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Release configuration (Repository Variables)'
|
||||||
|
printf '%s\n' "Profile: ${profile}"
|
||||||
|
printf '%s\n' '| Variable | Status |'
|
||||||
|
printf '%s\n' '|---|---|'
|
||||||
|
printf '%s\n' "| RS_FTP_PATH_SUFFIX | ${RS_FTP_PATH_SUFFIX:-NOT SET} |"
|
||||||
|
printf '%s\n' "| DEV_FTP_SUFFIX | ${DEV_FTP_SUFFIX:-NOT SET} |"
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
|
if [ "${#missing_optional[@]}" -gt 0 ]; then
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Missing optional repository variables'
|
||||||
|
for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${#missing[@]}" -gt 0 ]; then
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Missing required repository variables'
|
||||||
|
for m in "${missing[@]}"; do printf '%s\n' "- ${m}"; done
|
||||||
|
printf '%s\n' 'ERROR: Guardrails failed. Missing required repository variables.'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Repository variables validation result'
|
||||||
|
printf '%s\n' 'Status: OK'
|
||||||
|
printf '%s\n' 'All required repository variables present.'
|
||||||
|
printf '%s\n' ''
|
||||||
|
printf '%s\n' '**Note**: Organization secrets (RS_FTP_HOST, RS_FTP_USER, etc.) are validated at deployment time, not in repository health checks.'
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
|
scripts_governance:
|
||||||
|
name: Scripts governance
|
||||||
|
needs: access_check
|
||||||
|
if: ${{ needs.access_check.outputs.allowed == 'true' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Scripts folder checks
|
||||||
|
env:
|
||||||
|
PROFILE_RAW: ${{ github.event.inputs.profile }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
profile="${PROFILE_RAW:-all}"
|
||||||
|
case "${profile}" in
|
||||||
|
all|release|scripts|repo) ;;
|
||||||
|
*)
|
||||||
|
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ "${profile}" = 'release' ] || [ "${profile}" = 'repo' ]; then
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Scripts governance'
|
||||||
|
printf '%s\n' "Profile: ${profile}"
|
||||||
|
printf '%s\n' 'Status: SKIPPED'
|
||||||
|
printf '%s\n' 'Reason: profile excludes scripts governance'
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d "${SCRIPT_DIR}" ]; then
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Scripts governance'
|
||||||
|
printf '%s\n' 'Status: OK (advisory)'
|
||||||
|
printf '%s\n' 'scripts/ directory not present. No scripts governance enforced.'
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}"
|
||||||
|
IFS=',' read -r -a allowed_dirs <<< "${SCRIPTS_ALLOWED_DIRS}"
|
||||||
|
|
||||||
|
missing_dirs=()
|
||||||
|
unapproved_dirs=()
|
||||||
|
|
||||||
|
for d in "${required_dirs[@]}"; do
|
||||||
|
req="${d%/}"
|
||||||
|
[ ! -d "${req}" ] && missing_dirs+=("${req}/")
|
||||||
|
done
|
||||||
|
|
||||||
|
while IFS= read -r d; do
|
||||||
|
allowed=false
|
||||||
|
for a in "${allowed_dirs[@]}"; do
|
||||||
|
a_norm="${a%/}"
|
||||||
|
[ "${d%/}" = "${a_norm}" ] && allowed=true
|
||||||
|
done
|
||||||
|
[ "${allowed}" = false ] && unapproved_dirs+=("${d%/}/")
|
||||||
|
done < <(find "${SCRIPT_DIR}" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sed 's#^\./##')
|
||||||
|
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Scripts governance'
|
||||||
|
printf '%s\n' "Profile: ${profile}"
|
||||||
|
printf '%s\n' '| Area | Status | Notes |'
|
||||||
|
printf '%s\n' '|---|---|---|'
|
||||||
|
|
||||||
|
if [ "${#missing_dirs[@]}" -gt 0 ]; then
|
||||||
|
printf '%s\n' '| Required directories | Warning | Missing required subfolders |'
|
||||||
|
else
|
||||||
|
printf '%s\n' '| Required directories | OK | All required subfolders present |'
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${#unapproved_dirs[@]}" -gt 0 ]; then
|
||||||
|
printf '%s\n' '| Directory policy | Warning | Unapproved directories detected |'
|
||||||
|
else
|
||||||
|
printf '%s\n' '| Directory policy | OK | No unapproved directories |'
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' '| Enforcement mode | Advisory | scripts folder is optional |'
|
||||||
|
printf '\n'
|
||||||
|
|
||||||
|
if [ "${#missing_dirs[@]}" -gt 0 ]; then
|
||||||
|
printf '%s\n' 'Missing required script directories:'
|
||||||
|
for m in "${missing_dirs[@]}"; do printf '%s\n' "- ${m}"; done
|
||||||
|
printf '\n'
|
||||||
|
else
|
||||||
|
printf '%s\n' 'Missing required script directories: none.'
|
||||||
|
printf '\n'
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${#unapproved_dirs[@]}" -gt 0 ]; then
|
||||||
|
printf '%s\n' 'Unapproved script directories detected:'
|
||||||
|
for m in "${unapproved_dirs[@]}"; do printf '%s\n' "- ${m}"; done
|
||||||
|
printf '\n'
|
||||||
|
else
|
||||||
|
printf '%s\n' 'Unapproved script directories detected: none.'
|
||||||
|
printf '\n'
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' 'Scripts governance completed in advisory mode.'
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
|
repo_health:
|
||||||
|
name: Repository health
|
||||||
|
needs: access_check
|
||||||
|
if: ${{ needs.access_check.outputs.allowed == 'true' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 20
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Repository health checks
|
||||||
|
env:
|
||||||
|
PROFILE_RAW: ${{ github.event.inputs.profile }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
profile="${PROFILE_RAW:-all}"
|
||||||
|
case "${profile}" in
|
||||||
|
all|release|scripts|repo) ;;
|
||||||
|
*)
|
||||||
|
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ "${profile}" = 'release' ] || [ "${profile}" = 'scripts' ]; then
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Repository health'
|
||||||
|
printf '%s\n' "Profile: ${profile}"
|
||||||
|
printf '%s\n' 'Status: SKIPPED'
|
||||||
|
printf '%s\n' 'Reason: profile excludes repository health'
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Source directory: src/ or htdocs/ (either is valid)
|
||||||
|
if [ -d "src" ]; then
|
||||||
|
SOURCE_DIR="src"
|
||||||
|
elif [ -d "htdocs" ]; then
|
||||||
|
SOURCE_DIR="htdocs"
|
||||||
|
else
|
||||||
|
missing_required+=("src/ or htdocs/ (source directory required)")
|
||||||
|
fi
|
||||||
|
|
||||||
|
IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}"
|
||||||
|
IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}"
|
||||||
|
IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}"
|
||||||
|
IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES}"
|
||||||
|
|
||||||
|
missing_required=()
|
||||||
|
missing_optional=()
|
||||||
|
|
||||||
|
for item in "${required_artifacts[@]}"; do
|
||||||
|
if printf '%s' "${item}" | grep -q '/$'; then
|
||||||
|
d="${item%/}"
|
||||||
|
[ ! -d "${d}" ] && missing_required+=("${item}")
|
||||||
|
else
|
||||||
|
[ ! -f "${item}" ] && missing_required+=("${item}")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
for f in "${optional_files[@]}"; do
|
||||||
|
if printf '%s' "${f}" | grep -q '/$'; then
|
||||||
|
d="${f%/}"
|
||||||
|
[ ! -d "${d}" ] && missing_optional+=("${f}")
|
||||||
|
else
|
||||||
|
[ ! -f "${f}" ] && missing_optional+=("${f}")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
for d in "${disallowed_dirs[@]}"; do
|
||||||
|
d_norm="${d%/}"
|
||||||
|
[ -d "${d_norm}" ] && missing_required+=("${d_norm}/ (disallowed)")
|
||||||
|
done
|
||||||
|
|
||||||
|
for f in "${disallowed_files[@]}"; do
|
||||||
|
[ -f "${f}" ] && missing_required+=("${f} (disallowed)")
|
||||||
|
done
|
||||||
|
|
||||||
|
git fetch origin --prune
|
||||||
|
|
||||||
|
dev_paths=()
|
||||||
|
dev_branches=()
|
||||||
|
|
||||||
|
while IFS= read -r b; do
|
||||||
|
name="${b#origin/}"
|
||||||
|
if [ "${name}" = 'dev' ]; then
|
||||||
|
dev_branches+=("${name}")
|
||||||
|
else
|
||||||
|
dev_paths+=("${name}")
|
||||||
|
fi
|
||||||
|
done < <(git branch -r --list 'origin/dev*' | sed 's/^ *//')
|
||||||
|
|
||||||
|
if [ "${#dev_paths[@]}" -eq 0 ]; then
|
||||||
|
missing_required+=("dev/* branch (e.g. dev/01.00.00)")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${#dev_branches[@]}" -gt 0 ]; then
|
||||||
|
missing_required+=("invalid branch dev (must be dev/<version>)")
|
||||||
|
fi
|
||||||
|
|
||||||
|
content_warnings=()
|
||||||
|
|
||||||
|
if [ -f 'CHANGELOG.md' ] && ! grep -Eq '^# Changelog' CHANGELOG.md; then
|
||||||
|
content_warnings+=("CHANGELOG.md missing '# Changelog' header")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f 'CHANGELOG.md' ] && grep -Eq '^[# ]*Unreleased' CHANGELOG.md; then
|
||||||
|
content_warnings+=("CHANGELOG.md contains Unreleased section (review release readiness)")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f 'LICENSE' ] && ! grep -qiE 'GNU GENERAL PUBLIC LICENSE|GPL' LICENSE; then
|
||||||
|
content_warnings+=("LICENSE does not look like a GPL text")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f 'README.md' ] && ! grep -qiE 'moko|Moko' README.md; then
|
||||||
|
content_warnings+=("README.md missing expected brand keyword")
|
||||||
|
fi
|
||||||
|
|
||||||
|
export PROFILE_RAW="${profile}"
|
||||||
|
export MISSING_REQUIRED="$(printf '%s\n' "${missing_required[@]:-}")"
|
||||||
|
export MISSING_OPTIONAL="$(printf '%s\n' "${missing_optional[@]:-}")"
|
||||||
|
export CONTENT_WARNINGS="$(printf '%s\n' "${content_warnings[@]:-}")"
|
||||||
|
|
||||||
|
report_json="$(python3 - <<'PY'
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
profile = os.environ.get('PROFILE_RAW') or 'all'
|
||||||
|
|
||||||
|
missing_required = os.environ.get('MISSING_REQUIRED', '').splitlines() if os.environ.get('MISSING_REQUIRED') else []
|
||||||
|
missing_optional = os.environ.get('MISSING_OPTIONAL', '').splitlines() if os.environ.get('MISSING_OPTIONAL') else []
|
||||||
|
content_warnings = os.environ.get('CONTENT_WARNINGS', '').splitlines() if os.environ.get('CONTENT_WARNINGS') else []
|
||||||
|
|
||||||
|
out = {
|
||||||
|
'profile': profile,
|
||||||
|
'missing_required': [x for x in missing_required if x],
|
||||||
|
'missing_optional': [x for x in missing_optional if x],
|
||||||
|
'content_warnings': [x for x in content_warnings if x],
|
||||||
|
}
|
||||||
|
|
||||||
|
print(json.dumps(out, indent=2))
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Repository health'
|
||||||
|
printf '%s\n' "Profile: ${profile}"
|
||||||
|
printf '%s\n' '| Metric | Value |'
|
||||||
|
printf '%s\n' '|---|---|'
|
||||||
|
printf '%s\n' "| Missing required | ${#missing_required[@]} |"
|
||||||
|
printf '%s\n' "| Missing optional | ${#missing_optional[@]} |"
|
||||||
|
printf '%s\n' "| Content warnings | ${#content_warnings[@]} |"
|
||||||
|
printf '\n'
|
||||||
|
|
||||||
|
printf '%s\n' '### Guardrails report (JSON)'
|
||||||
|
printf '%s\n' '```json'
|
||||||
|
printf '%s\n' "${report_json}"
|
||||||
|
printf '%s\n' '```'
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
|
if [ "${#missing_required[@]}" -gt 0 ]; then
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Missing required repo artifacts'
|
||||||
|
for m in "${missing_required[@]}"; do printf '%s\n' "- ${m}"; done
|
||||||
|
printf '%s\n' 'ERROR: Guardrails failed. Missing required repository artifacts.'
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${#missing_optional[@]}" -gt 0 ]; then
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Missing optional repo artifacts'
|
||||||
|
for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${#content_warnings[@]}" -gt 0 ]; then
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Repo content warnings'
|
||||||
|
for m in "${content_warnings[@]}"; do printf '%s\n' "- ${m}"; done
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -- Joomla-specific checks --
|
||||||
|
joomla_findings=()
|
||||||
|
|
||||||
|
MANIFEST="$(find . -maxdepth 2 -name '*.xml' -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true)"
|
||||||
|
if [ -z "${MANIFEST}" ]; then
|
||||||
|
joomla_findings+=("Joomla XML manifest not found (no *.xml with <extension> tag)")
|
||||||
|
else
|
||||||
|
if ! grep -qP '<version>' "${MANIFEST}"; then
|
||||||
|
joomla_findings+=("XML manifest: <version> tag missing")
|
||||||
|
fi
|
||||||
|
if ! grep -qP 'type="(component|module|plugin|library|package|template|language)"' "${MANIFEST}"; then
|
||||||
|
joomla_findings+=("XML manifest: type attribute missing or invalid")
|
||||||
|
fi
|
||||||
|
if ! grep -qP '<name>' "${MANIFEST}"; then
|
||||||
|
joomla_findings+=("XML manifest: <name> tag missing")
|
||||||
|
fi
|
||||||
|
if ! grep -qP '<author>' "${MANIFEST}"; then
|
||||||
|
joomla_findings+=("XML manifest: <author> tag missing")
|
||||||
|
fi
|
||||||
|
if ! grep -qP '<namespace' "${MANIFEST}"; then
|
||||||
|
joomla_findings+=("XML manifest: <namespace> missing (required for Joomla 5+)")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
INI_COUNT="$(find . -name '*.ini' -type f 2>/dev/null | wc -l)"
|
||||||
|
if [ "${INI_COUNT}" -eq 0 ]; then
|
||||||
|
joomla_findings+=("No .ini language files found")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f 'updates.xml' ]; then
|
||||||
|
joomla_findings+=("updates.xml missing in root (required for Joomla update server)")
|
||||||
|
fi
|
||||||
|
|
||||||
|
INDEX_DIRS=("${SOURCE_DIR}" "${SOURCE_DIR}/admin" "${SOURCE_DIR}/site")
|
||||||
|
for dir in "${INDEX_DIRS[@]}"; do
|
||||||
|
if [ -d "${dir}" ] && [ ! -f "${dir}/index.html" ]; then
|
||||||
|
joomla_findings+=("${dir}/index.html missing (directory listing protection)")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "${#joomla_findings[@]}" -gt 0 ]; then
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Joomla extension checks'
|
||||||
|
printf '%s\n' '| Check | Status |'
|
||||||
|
printf '%s\n' '|---|---|'
|
||||||
|
for f in "${joomla_findings[@]}"; do
|
||||||
|
printf '%s\n' "| ${f} | Warning |"
|
||||||
|
done
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
else
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Joomla extension checks'
|
||||||
|
printf '%s\n' 'All Joomla-specific checks passed.'
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
extended_enabled="${EXTENDED_CHECKS:-true}"
|
||||||
|
extended_findings=()
|
||||||
|
|
||||||
|
if [ "${extended_enabled}" = 'true' ]; then
|
||||||
|
if [ -f '.github/CODEOWNERS' ] || [ -f 'CODEOWNERS' ] || [ -f 'docs/CODEOWNERS' ]; then
|
||||||
|
:
|
||||||
|
else
|
||||||
|
extended_findings+=("CODEOWNERS not found (.github/CODEOWNERS preferred)")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ls "${WORKFLOWS_DIR}"/*.yml >/dev/null 2>&1 || ls "${WORKFLOWS_DIR}"/*.yaml >/dev/null 2>&1; then
|
||||||
|
bad_refs="$(grep -RIn --include='*.yml' --include='*.yaml' -E '^[[:space:]]*uses:[[:space:]]*[^#]+@(main|master)\b' "${WORKFLOWS_DIR}" 2>/dev/null || true)"
|
||||||
|
if [ -n "${bad_refs}" ]; then
|
||||||
|
extended_findings+=("Workflows reference actions @main/@master (pin versions): see log excerpt")
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Workflow pinning advisory'
|
||||||
|
printf '%s\n' 'Found uses: entries pinned to main/master:'
|
||||||
|
printf '%s\n' '```'
|
||||||
|
printf '%s\n' "${bad_refs}"
|
||||||
|
printf '%s\n' '```'
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "${DOCS_INDEX}" ]; then
|
||||||
|
missing_links="$(python3 - <<'PY'
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
idx = os.environ.get('DOCS_INDEX', 'docs/docs-index.md')
|
||||||
|
base = os.getcwd()
|
||||||
|
|
||||||
|
bad = []
|
||||||
|
pat = re.compile(r'\[[^\]]+\]\(([^)]+)\)')
|
||||||
|
|
||||||
|
with open(idx, 'r', encoding='utf-8') as f:
|
||||||
|
for line in f:
|
||||||
|
for m in pat.findall(line):
|
||||||
|
link = m.strip()
|
||||||
|
if link.startswith('http://') or link.startswith('https://') or link.startswith('#') or link.startswith('mailto:'):
|
||||||
|
continue
|
||||||
|
if link.startswith('/'):
|
||||||
|
rel = link.lstrip('/')
|
||||||
|
else:
|
||||||
|
rel = os.path.normpath(os.path.join(os.path.dirname(idx), link))
|
||||||
|
rel = rel.split('#', 1)[0]
|
||||||
|
rel = rel.split('?', 1)[0]
|
||||||
|
if not rel:
|
||||||
|
continue
|
||||||
|
p = os.path.join(base, rel)
|
||||||
|
if not os.path.exists(p):
|
||||||
|
bad.append(rel)
|
||||||
|
|
||||||
|
print('\n'.join(sorted(set(bad))))
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
if [ -n "${missing_links}" ]; then
|
||||||
|
extended_findings+=("docs/docs-index.md contains broken relative links")
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Docs index link integrity'
|
||||||
|
printf '%s\n' 'Broken relative links:'
|
||||||
|
while IFS= read -r l; do [ -n "${l}" ] && printf '%s\n' "- ${l}"; done <<< "${missing_links}"
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -d "${SCRIPT_DIR}" ]; then
|
||||||
|
if ! command -v shellcheck >/dev/null 2>&1; then
|
||||||
|
sudo apt-get update -qq
|
||||||
|
sudo apt-get install -y shellcheck >/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
sc_out=''
|
||||||
|
while IFS= read -r shf; do
|
||||||
|
[ -z "${shf}" ] && continue
|
||||||
|
out_one="$(shellcheck -S warning -x "${shf}" 2>/dev/null || true)"
|
||||||
|
if [ -n "${out_one}" ]; then
|
||||||
|
sc_out="${sc_out}${out_one}\n"
|
||||||
|
fi
|
||||||
|
done < <(find "${SCRIPT_DIR}" -type f -name "${SHELLCHECK_PATTERN}" 2>/dev/null | sort)
|
||||||
|
|
||||||
|
if [ -n "${sc_out}" ]; then
|
||||||
|
extended_findings+=("ShellCheck warnings detected (advisory)")
|
||||||
|
sc_head="$(printf '%s' "${sc_out}" | head -n 200)"
|
||||||
|
{
|
||||||
|
printf '%s\n' '### ShellCheck (advisory)'
|
||||||
|
printf '%s\n' '```'
|
||||||
|
printf '%s\n' "${sc_head}"
|
||||||
|
printf '%s\n' '```'
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
spdx_missing=()
|
||||||
|
IFS=',' read -r -a spdx_globs <<< "${SPDX_FILE_GLOBS}"
|
||||||
|
spdx_args=()
|
||||||
|
for g in "${spdx_globs[@]}"; do spdx_args+=("${g}"); done
|
||||||
|
|
||||||
|
while IFS= read -r f; do
|
||||||
|
[ -z "${f}" ] && continue
|
||||||
|
if ! head -n 40 "${f}" | grep -q 'SPDX-License-Identifier:'; then
|
||||||
|
spdx_missing+=("${f}")
|
||||||
|
fi
|
||||||
|
done < <(git ls-files "${spdx_args[@]}" 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [ "${#spdx_missing[@]}" -gt 0 ]; then
|
||||||
|
extended_findings+=("SPDX header missing in some tracked files (advisory)")
|
||||||
|
{
|
||||||
|
printf '%s\n' '### SPDX header advisory'
|
||||||
|
printf '%s\n' 'Files missing SPDX-License-Identifier (first 40 lines scan):'
|
||||||
|
for f in "${spdx_missing[@]}"; do printf '%s\n' "- ${f}"; done
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
stale_cutoff_days=180
|
||||||
|
stale_branches="$(git for-each-ref --format='%(refname:short) %(committerdate:unix)' refs/remotes/origin 2>/dev/null | awk -v now="$(date +%s)" -v days="${stale_cutoff_days}" '{if (now-$2 > days*86400) print $1}' | head -50)"
|
||||||
|
if [ -n "${stale_branches}" ]; then
|
||||||
|
extended_findings+=("Stale remote branches detected (advisory)")
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Git hygiene advisory'
|
||||||
|
printf '%s\n' "Branches with last commit older than ${stale_cutoff_days} days (sample up to 50):"
|
||||||
|
while IFS= read -r b; do [ -n "${b}" ] && printf '%s\n' "- ${b}"; done <<< "${stale_branches}"
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Guardrails coverage matrix'
|
||||||
|
printf '%s\n' '| Domain | Status | Notes |'
|
||||||
|
printf '%s\n' '|---|---|---|'
|
||||||
|
printf '%s\n' '| Access control | OK | Admin-only execution gate |'
|
||||||
|
printf '%s\n' '| Release variables | OK | Repository variables validation |'
|
||||||
|
printf '%s\n' '| Scripts governance | OK | Directory policy and advisory reporting |'
|
||||||
|
printf '%s\n' '| Repo required artifacts | OK | Required, optional, disallowed enforcement |'
|
||||||
|
printf '%s\n' '| Repo content heuristics | OK | Brand, license, changelog structure |'
|
||||||
|
if [ "${extended_enabled}" = 'true' ]; then
|
||||||
|
if [ "${#extended_findings[@]}" -gt 0 ]; then
|
||||||
|
printf '%s\n' '| Extended checks | Warning | See extended findings below |'
|
||||||
|
else
|
||||||
|
printf '%s\n' '| Extended checks | OK | No findings |'
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
printf '%s\n' '| Extended checks | SKIPPED | EXTENDED_CHECKS disabled |'
|
||||||
|
fi
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
|
if [ "${extended_enabled}" = 'true' ] && [ "${#extended_findings[@]}" -gt 0 ]; then
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Extended findings (advisory)'
|
||||||
|
for f in "${extended_findings[@]}"; do printf '%s\n' "- ${f}"; done
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' 'Repository health guardrails passed.' >> "${GITHUB_STEP_SUMMARY}"
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: MokoStandards.Security
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
||||||
|
# PATH: /.gitea/workflows/security-audit.yml
|
||||||
|
# VERSION: 01.00.00
|
||||||
|
# BRIEF: Dependency vulnerability scanning for composer and npm packages
|
||||||
|
|
||||||
|
name: "Universal: Security Audit"
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 6 * * 1' # Weekly on Monday at 06:00 UTC
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- 'composer.json'
|
||||||
|
- 'composer.lock'
|
||||||
|
- 'package.json'
|
||||||
|
- 'package-lock.json'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
env:
|
||||||
|
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
|
||||||
|
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
audit:
|
||||||
|
name: Dependency Audit
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Composer audit
|
||||||
|
if: hashFiles('composer.lock') != ''
|
||||||
|
run: |
|
||||||
|
echo "=== Composer Security Audit ==="
|
||||||
|
if ! command -v composer &> /dev/null; then
|
||||||
|
sudo apt-get update -qq
|
||||||
|
sudo apt-get install -y -qq php-cli composer >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
composer audit --format=plain 2>&1 | tee /tmp/composer-audit.txt
|
||||||
|
RESULT=$?
|
||||||
|
if [ $RESULT -ne 0 ]; then
|
||||||
|
echo "::warning::Composer vulnerabilities found"
|
||||||
|
echo "composer_vulnerable=true" >> "$GITHUB_ENV"
|
||||||
|
else
|
||||||
|
echo "No known vulnerabilities in composer dependencies"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: NPM audit
|
||||||
|
if: hashFiles('package-lock.json') != ''
|
||||||
|
run: |
|
||||||
|
echo "=== NPM Security Audit ==="
|
||||||
|
npm audit --production 2>&1 | tee /tmp/npm-audit.txt || true
|
||||||
|
if npm audit --production 2>&1 | grep -q "found 0 vulnerabilities"; then
|
||||||
|
echo "No known vulnerabilities in npm dependencies"
|
||||||
|
else
|
||||||
|
echo "::warning::NPM vulnerabilities found"
|
||||||
|
echo "npm_vulnerable=true" >> "$GITHUB_ENV"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Notify on vulnerabilities
|
||||||
|
if: env.composer_vulnerable == 'true' || env.npm_vulnerable == 'true'
|
||||||
|
run: |
|
||||||
|
REPO="${{ github.event.repository.name }}"
|
||||||
|
curl -sS \
|
||||||
|
-H "Title: ${REPO} has vulnerable dependencies" \
|
||||||
|
-H "Tags: lock,warning" \
|
||||||
|
-H "Priority: high" \
|
||||||
|
-d "Security audit found vulnerabilities. Review dependency updates." \
|
||||||
|
"${NTFY_URL}/${NTFY_TOPIC}" || true
|
||||||
@@ -0,0 +1,464 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: MokoStandards.Joomla
|
||||||
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
||||||
|
# PATH: /templates/workflows/joomla/update-server.yml.template
|
||||||
|
# VERSION: 04.06.00
|
||||||
|
# BRIEF: Update Joomla update server XML feed with stable/rc/dev entries
|
||||||
|
#
|
||||||
|
# Writes updates.xml with multiple <update> entries:
|
||||||
|
# - <tag>stable</tag> on push to main (from auto-release)
|
||||||
|
# - <tag>rc</tag> on push to rc/**
|
||||||
|
# - <tag>development</tag> on push to dev or dev/**
|
||||||
|
#
|
||||||
|
# Joomla filters by user's "Minimum Stability" setting.
|
||||||
|
|
||||||
|
name: "Joomla: Update Server"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'dev'
|
||||||
|
- 'dev/**'
|
||||||
|
- 'alpha/**'
|
||||||
|
- 'beta/**'
|
||||||
|
- 'rc/**'
|
||||||
|
paths:
|
||||||
|
- 'src/**'
|
||||||
|
- 'htdocs/**'
|
||||||
|
pull_request:
|
||||||
|
types: [closed]
|
||||||
|
branches:
|
||||||
|
- 'dev'
|
||||||
|
- 'dev/**'
|
||||||
|
- 'alpha/**'
|
||||||
|
- 'beta/**'
|
||||||
|
- 'rc/**'
|
||||||
|
paths:
|
||||||
|
- 'src/**'
|
||||||
|
- 'htdocs/**'
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
stability:
|
||||||
|
description: 'Stability tag'
|
||||||
|
required: true
|
||||||
|
default: 'development'
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- development
|
||||||
|
- alpha
|
||||||
|
- beta
|
||||||
|
- rc
|
||||||
|
- stable
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||||||
|
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update-xml:
|
||||||
|
name: Update updates.xml
|
||||||
|
runs-on: release
|
||||||
|
if: >-
|
||||||
|
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || github.event_name == 'push'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GA_TOKEN }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup MokoStandards tools
|
||||||
|
env:
|
||||||
|
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||||
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
|
COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.GA_TOKEN }}"}}}'
|
||||||
|
run: |
|
||||||
|
if ! command -v composer &> /dev/null; then
|
||||||
|
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
git clone --depth 1 --branch main --quiet \
|
||||||
|
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
|
||||||
|
/tmp/mokostandards-api 2>/dev/null || true
|
||||||
|
if [ -d "/tmp/mokostandards-api" ] && [ -f "/tmp/mokostandards-api/composer.json" ]; then
|
||||||
|
cd /tmp/mokostandards-api && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Generate updates.xml entry
|
||||||
|
id: update
|
||||||
|
run: |
|
||||||
|
BRANCH="${{ github.ref_name }}"
|
||||||
|
REPO="${{ github.repository }}"
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
VERSION=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null || echo "0.0.0")
|
||||||
|
|
||||||
|
# Auto-bump patch on all branches (dev, alpha, beta, rc)
|
||||||
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
|
git config --local user.name "gitea-actions[bot]"
|
||||||
|
BUMPED=$(php /tmp/mokostandards-api/cli/version_bump.php --path . 2>/dev/null || true)
|
||||||
|
if [ -n "$BUMPED" ]; then
|
||||||
|
VERSION=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null || echo "$VERSION")
|
||||||
|
git add -A
|
||||||
|
git commit -m "chore(version): auto-bump patch ${VERSION} [skip ci]" \
|
||||||
|
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" 2>/dev/null || true
|
||||||
|
git push 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Determine stability from branch or input
|
||||||
|
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||||
|
STABILITY="${{ inputs.stability }}"
|
||||||
|
elif [[ "$BRANCH" == rc/* ]]; then
|
||||||
|
STABILITY="rc"
|
||||||
|
elif [[ "$BRANCH" == beta/* ]]; then
|
||||||
|
STABILITY="beta"
|
||||||
|
elif [[ "$BRANCH" == alpha/* ]]; then
|
||||||
|
STABILITY="alpha"
|
||||||
|
elif [[ "$BRANCH" == dev/* ]] || [[ "$BRANCH" == "dev" ]]; then
|
||||||
|
STABILITY="development"
|
||||||
|
else
|
||||||
|
STABILITY="stable"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
# Parse manifest (portable — no grep -P)
|
||||||
|
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "./build/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
||||||
|
if [ -z "$MANIFEST" ]; then
|
||||||
|
echo "No Joomla manifest found — skipping"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract fields using sed (works on all runners)
|
||||||
|
EXT_NAME=$(sed -n 's/.*<name>\([^<]*\)<\/name>.*/\1/p' "$MANIFEST" | head -1)
|
||||||
|
EXT_TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
|
||||||
|
EXT_ELEMENT=$(sed -n 's/.*<element>\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" | head -1)
|
||||||
|
EXT_CLIENT=$(sed -n 's/.*<extension[^>]*client="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
|
||||||
|
EXT_FOLDER=$(sed -n 's/.*<extension[^>]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
|
||||||
|
EXT_VERSION=$(sed -n 's/.*<version>\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1)
|
||||||
|
TARGET_PLATFORM=$(sed -n 's/.*\(<targetplatform[^/]*\/>\).*/\1/p' "$MANIFEST" | head -1)
|
||||||
|
PHP_MINIMUM=$(sed -n 's/.*<php_minimum>\([^<]*\)<\/php_minimum>.*/\1/p' "$MANIFEST" | head -1)
|
||||||
|
|
||||||
|
# Fallbacks
|
||||||
|
[ -z "$EXT_NAME" ] && EXT_NAME="${{ github.event.repository.name }}"
|
||||||
|
[ -z "$EXT_TYPE" ] && EXT_TYPE="component"
|
||||||
|
|
||||||
|
# Derive element if not in manifest: try XML filename, then repo name
|
||||||
|
if [ -z "$EXT_ELEMENT" ]; then
|
||||||
|
EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]')
|
||||||
|
case "$EXT_ELEMENT" in
|
||||||
|
templatedetails|manifest|*.xml) EXT_ELEMENT=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use manifest version if README version is empty
|
||||||
|
[ "$VERSION" = "0.0.0" ] && [ -n "$EXT_VERSION" ] && VERSION="$EXT_VERSION"
|
||||||
|
|
||||||
|
[ -z "$TARGET_PLATFORM" ] && TARGET_PLATFORM=$(printf '<targetplatform name="joomla" version="((5.[0-9])|(6.[0-9]))" %s>' "/")
|
||||||
|
|
||||||
|
CLIENT_TAG=""
|
||||||
|
[ -n "$EXT_CLIENT" ] && CLIENT_TAG="<client>${EXT_CLIENT}</client>"
|
||||||
|
[ -z "$CLIENT_TAG" ] && ([ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]) && CLIENT_TAG="<client>site</client>"
|
||||||
|
|
||||||
|
FOLDER_TAG=""
|
||||||
|
[ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ] && FOLDER_TAG="<folder>${EXT_FOLDER}</folder>"
|
||||||
|
|
||||||
|
PHP_TAG=""
|
||||||
|
[ -n "$PHP_MINIMUM" ] && PHP_TAG="<php_minimum>${PHP_MINIMUM}</php_minimum>"
|
||||||
|
|
||||||
|
# Version suffix for non-stable
|
||||||
|
DISPLAY_VERSION="$VERSION"
|
||||||
|
case "$STABILITY" in
|
||||||
|
development) DISPLAY_VERSION="${VERSION}-dev" ;;
|
||||||
|
alpha) DISPLAY_VERSION="${VERSION}-alpha" ;;
|
||||||
|
beta) DISPLAY_VERSION="${VERSION}-beta" ;;
|
||||||
|
rc) DISPLAY_VERSION="${VERSION}-rc" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
MAJOR=$(echo "$VERSION" | awk -F. '{print $1}')
|
||||||
|
|
||||||
|
# Each stability level has its own release tag
|
||||||
|
case "$STABILITY" in
|
||||||
|
development) RELEASE_TAG="development" ;;
|
||||||
|
alpha) RELEASE_TAG="alpha" ;;
|
||||||
|
beta) RELEASE_TAG="beta" ;;
|
||||||
|
rc) RELEASE_TAG="release-candidate" ;;
|
||||||
|
*) RELEASE_TAG="v${MAJOR}" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
PACKAGE_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.zip"
|
||||||
|
DOWNLOAD_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${PACKAGE_NAME}"
|
||||||
|
INFO_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
|
||||||
|
# -- Build install packages (ZIP + tar.gz) --------------------
|
||||||
|
SOURCE_DIR="src"
|
||||||
|
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
||||||
|
if [ -d "$SOURCE_DIR" ]; then
|
||||||
|
EXCLUDES=".ftpignore sftp-config* *.ppk *.pem *.key .env*"
|
||||||
|
TAR_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.tar.gz"
|
||||||
|
|
||||||
|
cd "$SOURCE_DIR"
|
||||||
|
zip -r "/tmp/${PACKAGE_NAME}" . -x $EXCLUDES
|
||||||
|
cd ..
|
||||||
|
tar -czf "/tmp/${TAR_NAME}" -C "$SOURCE_DIR" \
|
||||||
|
--exclude='.ftpignore' --exclude='sftp-config*' \
|
||||||
|
--exclude='*.ppk' --exclude='*.pem' --exclude='*.key' --exclude='.env*' .
|
||||||
|
|
||||||
|
SHA256=$(sha256sum "/tmp/${PACKAGE_NAME}" | cut -d' ' -f1)
|
||||||
|
|
||||||
|
# Ensure release exists on Gitea
|
||||||
|
RELEASE_JSON=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
||||||
|
"${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true)
|
||||||
|
RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [ -z "$RELEASE_ID" ]; then
|
||||||
|
# Create release
|
||||||
|
RELEASE_JSON=$(curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API_BASE}/releases" \
|
||||||
|
-d "$(python3 -c "import json; print(json.dumps({
|
||||||
|
'tag_name': '${RELEASE_TAG}',
|
||||||
|
'name': '${RELEASE_TAG} (${DISPLAY_VERSION})',
|
||||||
|
'body': '${STABILITY} release',
|
||||||
|
'prerelease': True,
|
||||||
|
'target_commitish': 'main'
|
||||||
|
}))")" 2>/dev/null || true)
|
||||||
|
RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$RELEASE_ID" ]; then
|
||||||
|
# Delete existing assets with same name before uploading
|
||||||
|
ASSETS=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
||||||
|
"${API_BASE}/releases/${RELEASE_ID}/assets" 2>/dev/null || echo "[]")
|
||||||
|
for ASSET_FILE in "$PACKAGE_NAME" "$TAR_NAME"; do
|
||||||
|
ASSET_ID=$(echo "$ASSETS" | python3 -c "
|
||||||
|
import sys,json
|
||||||
|
assets = json.load(sys.stdin)
|
||||||
|
for a in assets:
|
||||||
|
if a['name'] == '${ASSET_FILE}':
|
||||||
|
print(a['id']); break
|
||||||
|
" 2>/dev/null || true)
|
||||||
|
if [ -n "$ASSET_ID" ]; then
|
||||||
|
curl -sf -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
||||||
|
"${API_BASE}/releases/${RELEASE_ID}/assets/${ASSET_ID}" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Upload both formats
|
||||||
|
curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
||||||
|
-H "Content-Type: application/octet-stream" \
|
||||||
|
--data-binary @"/tmp/${PACKAGE_NAME}" \
|
||||||
|
"${API_BASE}/releases/${RELEASE_ID}/assets?name=${PACKAGE_NAME}" > /dev/null 2>&1 || true
|
||||||
|
|
||||||
|
curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
||||||
|
-H "Content-Type: application/octet-stream" \
|
||||||
|
--data-binary @"/tmp/${TAR_NAME}" \
|
||||||
|
"${API_BASE}/releases/${RELEASE_ID}/assets?name=${TAR_NAME}" > /dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Packages: ${PACKAGE_NAME} + ${TAR_NAME} (SHA: ${SHA256})" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
SHA256=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -- Build the new entry (canonical format matching release.yml) --
|
||||||
|
NEW_ENTRY=""
|
||||||
|
NEW_ENTRY="${NEW_ENTRY} <update>\n"
|
||||||
|
NEW_ENTRY="${NEW_ENTRY} <name>${EXT_NAME}</name>\n"
|
||||||
|
NEW_ENTRY="${NEW_ENTRY} <description>${EXT_NAME} ${STABILITY} build.</description>\n"
|
||||||
|
NEW_ENTRY="${NEW_ENTRY} <element>${EXT_ELEMENT}</element>\n"
|
||||||
|
NEW_ENTRY="${NEW_ENTRY} <type>${EXT_TYPE}</type>\n"
|
||||||
|
[ -n "$CLIENT_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${CLIENT_TAG}\n"
|
||||||
|
[ -n "$FOLDER_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${FOLDER_TAG}\n"
|
||||||
|
NEW_ENTRY="${NEW_ENTRY} <version>${VERSION}</version>\n"
|
||||||
|
NEW_ENTRY="${NEW_ENTRY} <creationDate>$(date +%Y-%m-%d)</creationDate>\n"
|
||||||
|
NEW_ENTRY="${NEW_ENTRY} <infourl title='${EXT_NAME}'>https://git.mokoconsulting.tech/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${RELEASE_TAG}</infourl>\n"
|
||||||
|
NEW_ENTRY="${NEW_ENTRY} <downloads>\n"
|
||||||
|
NEW_ENTRY="${NEW_ENTRY} <downloadurl type='full' format='zip'>${DOWNLOAD_URL}</downloadurl>\n"
|
||||||
|
NEW_ENTRY="${NEW_ENTRY} </downloads>\n"
|
||||||
|
[ -n "$SHA256" ] && NEW_ENTRY="${NEW_ENTRY} <sha256>${SHA256}</sha256>\n"
|
||||||
|
NEW_ENTRY="${NEW_ENTRY} <tags><tag>${STABILITY}</tag></tags>\n"
|
||||||
|
NEW_ENTRY="${NEW_ENTRY} <maintainer>Moko Consulting</maintainer>\n"
|
||||||
|
NEW_ENTRY="${NEW_ENTRY} <maintainerurl>https://mokoconsulting.tech</maintainerurl>\n"
|
||||||
|
NEW_ENTRY="${NEW_ENTRY} <targetplatform name='joomla' version='(5|6).*'/>\n"
|
||||||
|
[ -n "$PHP_MINIMUM" ] && NEW_ENTRY="${NEW_ENTRY} <php_minimum>${PHP_MINIMUM}</php_minimum>\n"
|
||||||
|
NEW_ENTRY="${NEW_ENTRY} </update>"
|
||||||
|
|
||||||
|
# -- Write new entry to temp file --------------------------------
|
||||||
|
printf '%b' "$NEW_ENTRY" > /tmp/new_entry.xml
|
||||||
|
|
||||||
|
# -- Merge into updates.xml ----------------------------------------
|
||||||
|
# Cascade: stable→all | rc→rc+lower | beta→beta+lower | alpha→alpha+dev | dev→dev
|
||||||
|
CASCADE_MAP="stable:development,alpha,beta,rc,stable rc:development,alpha,beta,rc beta:development,alpha,beta alpha:development,alpha development:development"
|
||||||
|
TARGETS=""
|
||||||
|
for entry in $CASCADE_MAP; do
|
||||||
|
key="${entry%%:*}"
|
||||||
|
vals="${entry#*:}"
|
||||||
|
if [ "$key" = "${STABILITY}" ]; then
|
||||||
|
TARGETS="$vals"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
[ -z "$TARGETS" ] && TARGETS="${STABILITY}"
|
||||||
|
|
||||||
|
echo "Cascade: ${STABILITY} → ${TARGETS}"
|
||||||
|
|
||||||
|
# Create updates.xml if missing
|
||||||
|
if [ ! -f "updates.xml" ]; then
|
||||||
|
printf '%s\n' "<?xml version='1.0' encoding='UTF-8'?>" > updates.xml
|
||||||
|
printf '%s\n' "<!-- Copyright (C) $(date +%Y) Moko Consulting -->" >> updates.xml
|
||||||
|
printf '%s\n' "<updates>" >> updates.xml
|
||||||
|
printf '%s\n' "</updates>" >> updates.xml
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Update existing blocks or create missing ones
|
||||||
|
export PY_TARGETS="$TARGETS" PY_VERSION="$VERSION" PY_DATE="$(date +%Y-%m-%d)"
|
||||||
|
python3 << 'PYEOF'
|
||||||
|
import re, os
|
||||||
|
|
||||||
|
targets = os.environ["PY_TARGETS"].split(",")
|
||||||
|
version = os.environ["PY_VERSION"]
|
||||||
|
date = os.environ["PY_DATE"]
|
||||||
|
|
||||||
|
with open("updates.xml") as f:
|
||||||
|
content = f.read()
|
||||||
|
with open("/tmp/new_entry.xml") as f:
|
||||||
|
new_entry_template = f.read()
|
||||||
|
|
||||||
|
for tag in targets:
|
||||||
|
tag = tag.strip()
|
||||||
|
# Build entry with this tag's name
|
||||||
|
new_entry = re.sub(r"<tag>[^<]*</tag>", f"<tag>{tag}</tag>", new_entry_template)
|
||||||
|
|
||||||
|
# Try to find existing block (handles both single-line and multi-line <tags>)
|
||||||
|
block_pattern = r"(<update>(?:(?!</update>).)*?<tag>" + re.escape(tag) + r"</tag>.*?</update>)"
|
||||||
|
match = re.search(block_pattern, content, re.DOTALL)
|
||||||
|
|
||||||
|
if match:
|
||||||
|
# Update in place — replace entire block
|
||||||
|
content = content.replace(match.group(1), new_entry.strip())
|
||||||
|
print(f" UPDATED: <tag>{tag}</tag> → {version}")
|
||||||
|
else:
|
||||||
|
# Create — insert before </updates>
|
||||||
|
content = content.replace("</updates>", "\n" + new_entry.strip() + "\n\n</updates>")
|
||||||
|
print(f" CREATED: <tag>{tag}</tag> → {version}")
|
||||||
|
|
||||||
|
# Clean up excessive blank lines
|
||||||
|
content = re.sub(r"\n{3,}", "\n\n", content)
|
||||||
|
|
||||||
|
with open("updates.xml", "w") as f:
|
||||||
|
f.write(content)
|
||||||
|
PYEOF
|
||||||
|
|
||||||
|
# Commit
|
||||||
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
|
git config --local user.name "gitea-actions[bot]"
|
||||||
|
git add updates.xml
|
||||||
|
git diff --cached --quiet || {
|
||||||
|
git commit -m "chore: update updates.xml (${STABILITY}: ${DISPLAY_VERSION}) [skip ci]" \
|
||||||
|
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
|
||||||
|
git push
|
||||||
|
}
|
||||||
|
|
||||||
|
# -- Sync updates.xml to main (for non-main branches) ----------------------
|
||||||
|
- name: Sync updates.xml to main
|
||||||
|
if: github.ref_name != 'main'
|
||||||
|
run: |
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
GA_TOKEN="${{ secrets.GA_TOKEN }}"
|
||||||
|
|
||||||
|
FILE_SHA=$(curl -sf -H "Authorization: token ${GA_TOKEN}" \
|
||||||
|
"${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then
|
||||||
|
CONTENT=$(base64 -w0 updates.xml)
|
||||||
|
curl -sf -X PUT -H "Authorization: token ${GA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API_BASE}/contents/updates.xml" \
|
||||||
|
-d "$(python3 -c "import json; print(json.dumps({
|
||||||
|
'content': '${CONTENT}',
|
||||||
|
'sha': '${FILE_SHA}',
|
||||||
|
'message': 'chore: sync updates.xml from ${STABILITY} [skip ci]',
|
||||||
|
'branch': 'main'
|
||||||
|
}))")" > /dev/null 2>&1 \
|
||||||
|
&& echo "updates.xml synced to main (${STABILITY})" >> $GITHUB_STEP_SUMMARY \
|
||||||
|
|| echo "WARNING: failed to sync updates.xml to main" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "WARNING: could not get updates.xml SHA from main" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: SFTP deploy to dev server
|
||||||
|
if: contains(github.ref, 'dev/') || github.ref == 'refs/heads/dev'
|
||||||
|
env:
|
||||||
|
DEV_HOST: ${{ vars.DEV_FTP_HOST }}
|
||||||
|
DEV_PATH: ${{ vars.DEV_FTP_PATH }}
|
||||||
|
DEV_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
|
||||||
|
DEV_USER: ${{ vars.DEV_FTP_USERNAME }}
|
||||||
|
DEV_PORT: ${{ vars.DEV_FTP_PORT }}
|
||||||
|
DEV_KEY: ${{ secrets.DEV_FTP_KEY }}
|
||||||
|
DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
# -- Permission check: admin or maintain role required --------
|
||||||
|
ACTOR="${{ github.actor }}"
|
||||||
|
REPO="${{ github.repository }}"
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
|
||||||
|
PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
||||||
|
"${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \
|
||||||
|
python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read")
|
||||||
|
case "$PERMISSION" in
|
||||||
|
admin|maintain|write) ;;
|
||||||
|
*)
|
||||||
|
echo "Deploy denied: ${ACTOR} has '${PERMISSION}' — requires admin, maintain, or write"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
[ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured — skipping SFTP"; exit 0; }
|
||||||
|
|
||||||
|
SOURCE_DIR="src"
|
||||||
|
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
||||||
|
[ ! -d "$SOURCE_DIR" ] && exit 0
|
||||||
|
|
||||||
|
PORT="${DEV_PORT:-22}"
|
||||||
|
REMOTE="${DEV_PATH%/}"
|
||||||
|
[ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}"
|
||||||
|
|
||||||
|
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
|
||||||
|
"$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json
|
||||||
|
if [ -n "$DEV_KEY" ]; then
|
||||||
|
echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key
|
||||||
|
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
|
||||||
|
else
|
||||||
|
printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json
|
||||||
|
fi
|
||||||
|
|
||||||
|
PLATFORM=$(php /tmp/mokostandards-api/cli/platform_detect.php --path . 2>/dev/null || true)
|
||||||
|
if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards-api/deploy/deploy-joomla.php" ]; then
|
||||||
|
php /tmp/mokostandards-api/deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
|
||||||
|
elif [ -f "/tmp/mokostandards-api/deploy/deploy-sftp.php" ]; then
|
||||||
|
php /tmp/mokostandards-api/deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
|
||||||
|
fi
|
||||||
|
rm -f /tmp/deploy_key /tmp/sftp-config.json
|
||||||
|
echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
- name: Summary
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
echo "## Joomla Update Server" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Version | \`${DISPLAY_VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Element | \`${EXT_ELEMENT}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Download | [ZIP](${DOWNLOAD_URL}) |" >> $GITHUB_STEP_SUMMARY
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: MokoStandards.Templates.Config
|
|
||||||
# INGROUP: MokoStandards.Templates
|
|
||||||
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
|
|
||||||
# PATH: /templates/configs/moko-standards.yml
|
|
||||||
# VERSION: 04.04.01
|
|
||||||
# BRIEF: Governance attachment template — synced to .mokostandards in every governed repository
|
|
||||||
# NOTE: Tokens replaced at sync time: mokoconsulting-tech, MokoWaaS, waas-component, 04.04.00
|
|
||||||
#
|
|
||||||
# This file is managed automatically by MokoStandards bulk sync.
|
|
||||||
# Do not edit manually — changes will be overwritten on the next sync.
|
|
||||||
# To update governance settings, open a PR in MokoStandards instead:
|
|
||||||
# https://github.com/mokoconsulting-tech/MokoStandards
|
|
||||||
|
|
||||||
standards_source: "https://github.com/mokoconsulting-tech/MokoStandards"
|
|
||||||
standards_version: "04.04.00"
|
|
||||||
platform: "waas-component"
|
|
||||||
governed_repo: "mokoconsulting-tech/MokoWaaS"
|
|
||||||
+99
-1
@@ -28,10 +28,108 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Planned
|
### Planned
|
||||||
- Heartbeat telemetry to WaaS dashboard (#54)
|
|
||||||
- License/subscription check
|
- License/subscription check
|
||||||
- System email template branding (DB approach)
|
- System email template branding (DB approach)
|
||||||
|
|
||||||
|
## [02.03.10] - 2026-05-24
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Canonical URL injection for alias domains (prevents SEO duplication)
|
||||||
|
- Primary Domain config field in Site Aliases tab
|
||||||
|
- Heartbeat registration for alias domains (each alias gets Grafana datasource)
|
||||||
|
- Plugin protection: hidden from non-master users, disable/uninstall blocked
|
||||||
|
- Dynamic plugin version read from manifest XML (no more hardcoded strings)
|
||||||
|
- Package structure: `pkg_mokowaas` with system plugin, webservices plugin, and component
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Alias offline mode uses Joomla's native template offline.php (not custom HTML)
|
||||||
|
- Alias detection simplified: direct lookup in aliases list (no primary host comparison)
|
||||||
|
- handleSiteAlias() moved to onAfterRoute (client type resolved at that point)
|
||||||
|
- Package script.php enables plugins on every install/update and sends heartbeat
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Alias domain matching: strip trailing slashes, handle Joomla subform stdClass format
|
||||||
|
- Backend redirect: use primary_domain setting instead of Uri::root() (returned alias domain on mirrors)
|
||||||
|
- CI: version_bump reads manifest XML with priority over README.md VERSION header
|
||||||
|
- CI: version bump occurs after release build, not before
|
||||||
|
- CI: pipefail disabled during element detection (SIGPIPE on find|head)
|
||||||
|
- CI: pkg_pkg_ prefix duplication in zip names and updates.xml URLs
|
||||||
|
- CI: updates_xml_build preserves existing channel entries (stable not wiped by dev releases)
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- deploy-manual.yml workflow — using Joomla update server for distribution
|
||||||
|
- Accidentally committed profile.ps1 and TODO.md
|
||||||
|
|
||||||
|
## [02.01.43] - 2026-05-23
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Site Aliases tab with Joomla subform repeatable-table UI
|
||||||
|
- Per-alias offline toggle with custom maintenance message (503 response)
|
||||||
|
- Per-alias robots meta directive (index/noindex/follow/nofollow/none)
|
||||||
|
- Per-alias backend redirect (admin panel redirects to primary domain)
|
||||||
|
- 6 MokoWaaS API endpoints: health, install, update, cache, backup, info
|
||||||
|
- Remote plugin install via `/?mokowaas=install` endpoint
|
||||||
|
- Remote update trigger via `/?mokowaas=update` endpoint
|
||||||
|
- Remote cache clear via `/?mokowaas=cache` endpoint (site + admin + opcache)
|
||||||
|
- Remote Akeeba Backup trigger via `/?mokowaas=backup` endpoint
|
||||||
|
- Compact site info via `/?mokowaas=info` endpoint
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Site aliases moved from comma-separated text field to structured subform
|
||||||
|
- Each alias now stores domain, offline, offline_message, robots, redirect_backend
|
||||||
|
- Heartbeat provisioning updated for subform alias format
|
||||||
|
- Grafana datasource names use domain-only (removed "MokoWaaS - " prefix)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Heartbeat receiver accepts any 200 status (registered/updated/ok)
|
||||||
|
- script.php uses heartbeat receiver instead of Grafana API (fixes 403 RBAC)
|
||||||
|
|
||||||
|
## [02.01.37] - 2026-05-23
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Health check endpoint at `/?mokowaas=health` with 16 diagnostic checks (#54)
|
||||||
|
- Core checks: database latency, filesystem writability/size, cache, extensions
|
||||||
|
- Backup checks: Akeeba Backup last backup date/status/size, days since, frequency
|
||||||
|
- Security checks: Admin Tools WAF status, blocked requests 24h/7d
|
||||||
|
- SSL certificate: expiry date, days left, issuer (degraded <30d, error <7d)
|
||||||
|
- Scheduled tasks: Joomla task scheduler status, failed tasks 24h
|
||||||
|
- Error log: PHP error log size, recent errors, last error message
|
||||||
|
- Database size: total MB, table count, top 5 largest tables
|
||||||
|
- Content stats: articles, categories, menu items, modules
|
||||||
|
- User activity: total users, active sessions, failed logins 24h, last login
|
||||||
|
- Mail system: mailer type, from address, SMTP host, queue count
|
||||||
|
- SEO health: robots.txt, sitemap, htaccess, SEF status
|
||||||
|
- Template info: site/admin template names, override count
|
||||||
|
- Configuration drift: debug mode, error reporting, force SSL, caching
|
||||||
|
- Human-readable `reason` field explaining degraded/error status
|
||||||
|
- Site size reporting (images, media, tmp, cache, logs directories)
|
||||||
|
- Heartbeat provisioning via receiver at bench.mokoconsulting.tech
|
||||||
|
- Grafana datasource auto-provisioning via YAML (no API token needed)
|
||||||
|
- ntfy notifications on heartbeat registration (mokowaas-heartbeat topic)
|
||||||
|
- Grafana dashboard with 9 rows covering all 16 health checks
|
||||||
|
- Auto-generated health API token (separate from Joomla user tokens)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Health endpoint always enabled — no config toggle needed
|
||||||
|
- Grafana provisioning uses heartbeat receiver pattern (replaces direct API)
|
||||||
|
- Removed config fields: enable_health_endpoint, grafana_url, grafana_api_key
|
||||||
|
- Migrated .gitea/ to .mokogitea/ directory standard
|
||||||
|
- Updated all references from MokoStandards to moko-platform
|
||||||
|
- Renamed Gitea references to MokoGitea in docs
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- SSL verification disabled for Grafana cURL calls (shared hosting)
|
||||||
|
- cURL follow redirects enabled
|
||||||
|
- updates.xml download URL uses correct `development` tag
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- Plugin hidden from plugin list for non-master users
|
||||||
|
- Plugin settings restricted to master user only
|
||||||
|
- Self-healing lock (enforceLocked) runs every page load
|
||||||
|
- Uninstall blocked in preflight
|
||||||
|
- Health endpoint requires HTTPS + bearer token
|
||||||
|
- Heartbeat shared secret for receiver authentication
|
||||||
|
|
||||||
## [02.01.08] - 2026-04-07
|
## [02.01.08] - 2026-04-07
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code when working with this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
**MokoWaaS** -- MokoWaaS is a Joomla 5.x / 6.x system plugin that provides a configurable white-label identity layer for the MokoWaaS platform.
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Platform** | joomla |
|
||||||
|
| **Language** | PHP |
|
||||||
|
| **Default branch** | main |
|
||||||
|
| **License** | GPL-3.0-or-later |
|
||||||
|
| **Wiki** | [MokoWaaS Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/wiki) |
|
||||||
|
| **Standards** | [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home) |
|
||||||
|
|
||||||
|
## Common Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer install # Install PHP dependencies
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
This is a Joomla extension. Key directories:
|
||||||
|
- `src/` -- extension source (deployed to Joomla)
|
||||||
|
- `src/*.xml` -- manifest file (version, files, params)
|
||||||
|
- `src/src/` or `src/services/` -- PHP classes
|
||||||
|
- `src/language/` -- translation strings
|
||||||
|
- `src/media/` -- CSS/JS/images
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- **Workflow directory**: `.mokogitea/` (not `.gitea/` or `.github/`)
|
||||||
|
|
||||||
|
- **Never commit** `.claude/`, `.mcp.json`, `TODO.md`, or `*.min.css`/`*.min.js`
|
||||||
|
- **Attribution**: use `Authored-by: Moko Consulting` in commits
|
||||||
|
- **Branch strategy**: develop on `dev`, merge to `main` for release
|
||||||
|
- **Minification**: handled at build time (CI) and runtime (MokoMinifyHelper for Joomla templates)
|
||||||
|
- **Wiki**: documentation lives in the Gitea wiki, not in `docs/` files
|
||||||
|
- **Standards**: this repo follows [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
DEFGROUP: Joomla.Plugin
|
DEFGROUP: Joomla.Plugin
|
||||||
INGROUP: MokoWaaS
|
INGROUP: MokoWaaS
|
||||||
REPO: https://github.com/mokoconsulting-tech/mokowaas
|
REPO: https://github.com/mokoconsulting-tech/mokowaas
|
||||||
VERSION: 02.01.13
|
VERSION: 02.03.12
|
||||||
PATH: /README.md
|
PATH: /README.md
|
||||||
BRIEF: Rebranding plugin for MokoWaaS platform
|
BRIEF: Rebranding plugin for MokoWaaS platform
|
||||||
NOTE: Internal WaaS identity abstraction layer
|
NOTE: Internal WaaS identity abstraction layer
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
|
|
||||||
# MokoWaaS Plugin
|
# MokoWaaS Plugin
|
||||||
|
|
||||||
[](https://github.com/mokoconsulting-tech/MokoWaaS/releases/tag/v02)
|
[](https://github.com/mokoconsulting-tech/MokoWaaS/releases/tag/v02)
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
[](https://www.joomla.org)
|
[](https://www.joomla.org)
|
||||||
[](https://www.php.net)
|
[](https://www.php.net)
|
||||||
@@ -59,7 +59,11 @@ The MokoWaaS plugin operationalizes a unified naming convention, brand-controlle
|
|||||||
- **Joomla 5.x / 6.x Compatible**: Built using modern Joomla plugin architecture with dependency injection
|
- **Joomla 5.x / 6.x Compatible**: Built using modern Joomla plugin architecture with dependency injection
|
||||||
- **Multi-Language Support**: en-GB and en-US locales
|
- **Multi-Language Support**: en-GB and en-US locales
|
||||||
- **Admin & Frontend Coverage**: Dashboard, footer, login, installer, system info, update component, error pages, and more
|
- **Admin & Frontend Coverage**: Dashboard, footer, login, installer, system info, update component, error pages, and more
|
||||||
- **Governance Compliant**: Aligned with [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards)
|
- **Health Monitoring**: 16 diagnostic checks via `/?mokowaas=health` — database, filesystem, cache, extensions, Akeeba Backup, Admin Tools, SSL, cron, errors, DB size, content, users, mail, SEO, templates, config
|
||||||
|
- **Grafana Integration**: Auto-provisions Infinity datasource via heartbeat receiver — 9-row dashboard with all health metrics
|
||||||
|
- **ntfy Notifications**: Heartbeat events pushed to `mokowaas-heartbeat` topic
|
||||||
|
- **Plugin Protection**: Hidden from non-super-admins, self-healing lock, uninstall blocked
|
||||||
|
- **Governance Compliant**: Aligned with [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform)
|
||||||
|
|
||||||
## System Requirements
|
## System Requirements
|
||||||
|
|
||||||
@@ -322,22 +326,18 @@ See [LICENSE.md](LICENSE.md) for the full license text.
|
|||||||
|
|
||||||
This extension follows the [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards) version governance model using semantic versioning: `MAJOR.MINOR.PATCH`
|
This extension follows the [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards) version governance model using semantic versioning: `MAJOR.MINOR.PATCH`
|
||||||
|
|
||||||
Current version: **01.04.00**
|
Current version: **02.01.18**
|
||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
See [CHANGELOG.md](CHANGELOG.md) for a complete version history.
|
See [CHANGELOG.md](CHANGELOG.md) for a complete version history.
|
||||||
|
|
||||||
### Recent Changes (v01.04.00 - 2026-02-22)
|
### Recent Changes (v02.01.18 - 2026-04-23)
|
||||||
|
|
||||||
- Added complete Joomla 5.x system plugin implementation
|
- Always install and lock MokoOnyx template on install/update
|
||||||
- Created main plugin class with event handlers
|
- Always unlock MokoCassiopeia on install/update (allow uninstall)
|
||||||
- Implemented plugin manifest with Joomla 5.x namespace support
|
- Bundle MokoOnyx payload (replaces MokoCassiopeia payload)
|
||||||
- Added dependency injection service provider
|
- Update payload workflow to fetch MokoOnyx from Gitea releases
|
||||||
- Created plugin language files
|
|
||||||
- Integrated with language override system
|
|
||||||
- Enhanced language overrides (57+ strings)
|
|
||||||
- Fixed typo in error messages (OCCURRED)
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
|||||||
@@ -327,4 +327,4 @@ Templates and plugins must remain synchronized to avoid inconsistencies.
|
|||||||
|
|
||||||
| Date | Author | Description |
|
| Date | Author | Description |
|
||||||
| ---------- | ------------------------------- | ------------------------------- |
|
| ---------- | ------------------------------- | ------------------------------- |
|
||||||
| 2025-12-11 | Jonathan Miller (@jmiller-moko) | Initial creation of build guide |
|
| 2025-12-11 | Jonathan Miller (@jmiller) | Initial creation of build guide |
|
||||||
|
|||||||
@@ -265,5 +265,5 @@ Restricted components are automatically hidden from the admin menu via `onPrepro
|
|||||||
|
|
||||||
| Version | Date | Author | Description |
|
| Version | Date | Author | Description |
|
||||||
| -------- | ---------- | ------------------------------- | ---------------------------------------------- |
|
| -------- | ---------- | ------------------------------- | ---------------------------------------------- |
|
||||||
| 01.02.00 | 2025-12-11 | Jonathan Miller (@jmiller-moko) | Initial standalone configuration guide created |
|
| 01.02.00 | 2025-12-11 | Jonathan Miller (@jmiller) | Initial standalone configuration guide created |
|
||||||
| 02.01.08 | 2026-04-07 | Jonathan Miller (@jmiller-moko) | Full rewrite: WaaS access, visual branding, tenant restrictions, security, maintenance, action logs |
|
| 02.01.08 | 2026-04-07 | Jonathan Miller (@jmiller) | Full rewrite: WaaS access, visual branding, tenant restrictions, security, maintenance, action logs |
|
||||||
|
|||||||
@@ -84,4 +84,4 @@ If the plugin introduces issues or conflicts:
|
|||||||
|
|
||||||
| Date | Author | Description |
|
| Date | Author | Description |
|
||||||
| ---------- | ------------------------------- | ---------------------------- |
|
| ---------- | ------------------------------- | ---------------------------- |
|
||||||
| 2025-12-11 | Jonathan Miller (@jmiller-moko) | Rewrite for version 01.03.00 |
|
| 2025-12-11 | Jonathan Miller (@jmiller) | Rewrite for version 01.03.00 |
|
||||||
|
|||||||
@@ -128,4 +128,4 @@ Administrators should factor these dependencies into maintenance and upgrade pla
|
|||||||
|
|
||||||
| Date | Author | Description |
|
| Date | Author | Description |
|
||||||
| ---------- | ------------------------------- | ---------------------------- |
|
| ---------- | ------------------------------- | ---------------------------- |
|
||||||
| 2025-12-11 | Jonathan Miller (@jmiller-moko) | Rewrite for version 01.03.00 |
|
| 2025-12-11 | Jonathan Miller (@jmiller) | Rewrite for version 01.03.00 |
|
||||||
|
|||||||
@@ -107,4 +107,4 @@ These strategies improve long term WaaS platform stability.
|
|||||||
|
|
||||||
| Date | Author | Description |
|
| Date | Author | Description |
|
||||||
| ---------- | ------------------------------- | ------------------------------------------- |
|
| ---------- | ------------------------------- | ------------------------------------------- |
|
||||||
| 2025-12-11 | Jonathan Miller (@jmiller-moko) | Full rewrite and update to version 01.03.00 |
|
| 2025-12-11 | Jonathan Miller (@jmiller) | Full rewrite and update to version 01.03.00 |
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
| 4 | Check admin dashboard | "Welcome to MokoWaaS!" appears in control panel | [ ] |
|
| 4 | Check admin dashboard | "Welcome to MokoWaaS!" appears in control panel | [ ] |
|
||||||
| 5 | Check admin footer | "Powered by MokoWaaS" appears | [ ] |
|
| 5 | Check admin footer | "Powered by MokoWaaS" appears | [ ] |
|
||||||
| 6 | Check admin login page | "MokoWaaS Administrator Login" title, support links show "Moko Consulting" | [ ] |
|
| 6 | Check admin login page | "MokoWaaS Administrator Login" title, support links show "Moko Consulting" | [ ] |
|
||||||
| 7 | Check frontend footer | "Powered by MokoWaaS" in Cassiopeia template | [ ] |
|
| 7 | Check frontend footer | "Powered by MokoWaaS" in MokoOnyx template | [ ] |
|
||||||
| 8 | Check Joomla override files at `administrator/language/overrides/en-GB.override.ini` | Contains `BEGIN MokoWaaS Overrides` sentinel block | [ ] |
|
| 8 | Check Joomla override files at `administrator/language/overrides/en-GB.override.ini` | Contains `BEGIN MokoWaaS Overrides` sentinel block | [ ] |
|
||||||
| 9 | Check Joomla override files at `language/overrides/en-GB.override.ini` | Contains `BEGIN MokoWaaS Overrides` sentinel block | [ ] |
|
| 9 | Check Joomla override files at `language/overrides/en-GB.override.ini` | Contains `BEGIN MokoWaaS Overrides` sentinel block | [ ] |
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@
|
|||||||
|---|------|-----------------|------|
|
|---|------|-----------------|------|
|
||||||
| 1 | Set Enable Branding to "No", save | Save succeeds | [ ] |
|
| 1 | Set Enable Branding to "No", save | Save succeeds | [ ] |
|
||||||
| 2 | Reload admin dashboard | Default Joomla strings appear (e.g., "Welcome to Joomla!") | [ ] |
|
| 2 | Reload admin dashboard | Default Joomla strings appear (e.g., "Welcome to Joomla!") | [ ] |
|
||||||
| 3 | Check frontend footer | Default "Powered by Joomla" or Cassiopeia default | [ ] |
|
| 3 | Check frontend footer | Default "Powered by Joomla" or MokoOnyx default | [ ] |
|
||||||
| 4 | Set Enable Branding back to "Yes", save | Branding strings restored immediately | [ ] |
|
| 4 | Set Enable Branding back to "Yes", save | Branding strings restored immediately | [ ] |
|
||||||
|
|
||||||
### 2.7 Update (Upgrade from Previous Version)
|
### 2.7 Update (Upgrade from Previous Version)
|
||||||
@@ -138,7 +138,7 @@ Verify the following admin areas no longer show "Joomla":
|
|||||||
|
|
||||||
| # | Location | Expected Brand Text | Pass |
|
| # | Location | Expected Brand Text | Pass |
|
||||||
|---|----------|-------------------|------|
|
|---|----------|-------------------|------|
|
||||||
| 1 | Cassiopeia footer | "Powered by {brand}" | [ ] |
|
| 1 | MokoOnyx footer | "Powered by {brand}" | [ ] |
|
||||||
| 2 | Site offline page | Maintenance message (no Joomla reference) | [ ] |
|
| 2 | Site offline page | Maintenance message (no Joomla reference) | [ ] |
|
||||||
| 3 | 404 error page | "Page Not Found" (no Joomla reference) | [ ] |
|
| 3 | 404 error page | "Page Not Found" (no Joomla reference) | [ ] |
|
||||||
| 4 | Frontend login support | "{company} Support" / "{brand} Documentation" | [ ] |
|
| 4 | Frontend login support | "{company} Support" / "{brand} Documentation" | [ ] |
|
||||||
@@ -298,4 +298,4 @@ grep -r 'Version:' src/**/*.ini | grep -v '02.01.08'
|
|||||||
|
|
||||||
| Version | Date | Author | Description |
|
| Version | Date | Author | Description |
|
||||||
| -------- | ---------- | ------------------------------- | ------------------------------- |
|
| -------- | ---------- | ------------------------------- | ------------------------------- |
|
||||||
| 02.01.08 | 2026-04-07 | Jonathan Miller (@jmiller-moko) | Full testing guide: 17 suites covering install, branding, access, visual, security, maintenance, edge cases |
|
| 02.01.08 | 2026-04-07 | Jonathan Miller (@jmiller) | Full testing guide: 17 suites covering install, branding, access, visual, security, maintenance, edge cases |
|
||||||
|
|||||||
@@ -155,4 +155,4 @@ To reduce incidents and ensure operational stability:
|
|||||||
|
|
||||||
| Date | Author | Description |
|
| Date | Author | Description |
|
||||||
| ---------- | ------------------------------- | ------------------------------------------- |
|
| ---------- | ------------------------------- | ------------------------------------------- |
|
||||||
| 2025-12-11 | Jonathan Miller (@jmiller-moko) | Full rewrite and update to version 01.03.00 |
|
| 2025-12-11 | Jonathan Miller (@jmiller) | Full rewrite and update to version 01.03.00 |
|
||||||
|
|||||||
@@ -114,4 +114,4 @@ To minimize disruptions:
|
|||||||
|
|
||||||
| Date | Author | Description |
|
| Date | Author | Description |
|
||||||
| ---------- | ------------------------------- | ------------------------------------------- |
|
| ---------- | ------------------------------- | ------------------------------------------- |
|
||||||
| 2025-12-11 | Jonathan Miller (@jmiller-moko) | Full rewrite and update to version 01.03.00 |
|
| 2025-12-11 | Jonathan Miller (@jmiller) | Full rewrite and update to version 01.03.00 |
|
||||||
|
|||||||
+1
-1
@@ -75,4 +75,4 @@ Contributors should:
|
|||||||
| Date | Author | Description |
|
| Date | Author | Description |
|
||||||
| ---------- | ------------------------------- | ------------------------------------------- |
|
| ---------- | ------------------------------- | ------------------------------------------- |
|
||||||
| 2026-02-22 | GitHub Copilot | Update to version 01.04.00 |
|
| 2026-02-22 | GitHub Copilot | Update to version 01.04.00 |
|
||||||
| 2025-12-11 | Jonathan Miller (@jmiller-moko) | Full rewrite and update to version 01.03.00 |
|
| 2025-12-11 | Jonathan Miller (@jmiller) | Full rewrite and update to version 01.03.00 |
|
||||||
|
|||||||
@@ -122,4 +122,4 @@ While the plugin provides broad branding coverage, certain constraints apply:
|
|||||||
| Date | Author | Description |
|
| Date | Author | Description |
|
||||||
| ---------- | ------------------------------- | ---------------------------- |
|
| ---------- | ------------------------------- | ---------------------------- |
|
||||||
| 2026-02-22 | GitHub Copilot | Update for version 01.04.00 |
|
| 2026-02-22 | GitHub Copilot | Update for version 01.04.00 |
|
||||||
| 2025-12-11 | Jonathan Miller (@jmiller-moko) | Rewrite for version 01.03.00 |
|
| 2025-12-11 | Jonathan Miller (@jmiller) | Rewrite for version 01.03.00 |
|
||||||
|
|||||||
+32
-8
@@ -29,8 +29,30 @@ Joomla checks for extension updates by fetching an XML file from the URL defined
|
|||||||
| Event | Workflow | `<tag>` | `<version>` |
|
| Event | Workflow | `<tag>` | `<version>` |
|
||||||
|-------|----------|---------|-------------|
|
|-------|----------|---------|-------------|
|
||||||
| Merge to `main` | `auto-release.yml` | `stable` | `XX.YY.ZZ` |
|
| Merge to `main` | `auto-release.yml` | `stable` | `XX.YY.ZZ` |
|
||||||
| Push to `dev/**` | `deploy-dev.yml` | `development` | `development` |
|
| Push to `dev` or `dev/**` | `update-server.yml` | `development` | `XX.YY.ZZ-dev` |
|
||||||
| Push to `rc/**` | `deploy-dev.yml` | `rc` | `XX.YY.ZZ-rc` |
|
| Push to `alpha/**` | `update-server.yml` | `alpha` | `XX.YY.ZZ-alpha` |
|
||||||
|
| Push to `beta/**` | `update-server.yml` | `beta` | `XX.YY.ZZ-beta` |
|
||||||
|
| Push to `rc/**` | `update-server.yml` | `rc` | `XX.YY.ZZ-rc` |
|
||||||
|
|
||||||
|
**Trigger behavior**: `update-server.yml` triggers on both direct pushes and PR merges to `dev`, `dev/**`, `alpha/**`, `beta/**`, and `rc/**` branches. It supports bare `dev` branches (not just `dev/**` patterns).
|
||||||
|
|
||||||
|
### Cascade Release Channels
|
||||||
|
|
||||||
|
Each stability level writes itself **and all lower channels** to `updates.xml`:
|
||||||
|
|
||||||
|
| Release Stream | Channels written |
|
||||||
|
|---------------|-----------------|
|
||||||
|
| development | `development` |
|
||||||
|
| alpha | `development`, `alpha` |
|
||||||
|
| beta | `development`, `alpha`, `beta` |
|
||||||
|
| rc | `development`, `alpha`, `beta`, `rc` |
|
||||||
|
| stable | `development`, `alpha`, `beta`, `rc`, `stable` |
|
||||||
|
|
||||||
|
This ensures Joomla sites on any "Minimum Stability" setting always see the latest available release.
|
||||||
|
|
||||||
|
### Sync to Main
|
||||||
|
|
||||||
|
Since Joomla sites read `updates.xml` from the `main` branch, the `update-server.yml` workflow **syncs `updates.xml` to `main` via the Gitea API** after building on non-main branches. This ensures pre-release channel entries are visible to sites checking for updates without requiring a PR merge to main.
|
||||||
|
|
||||||
### Generated XML Structure
|
### Generated XML Structure
|
||||||
|
|
||||||
@@ -94,14 +116,16 @@ Your XML manifest must include an `<updateservers>` tag pointing to the `update.
|
|||||||
### Branch Lifecycle
|
### Branch Lifecycle
|
||||||
|
|
||||||
```
|
```
|
||||||
dev/XX.YY.ZZ → rc/XX.YY.ZZ → main → version/XX.YY
|
dev → [alpha] → [beta] → rc → version/XX → main → dev
|
||||||
(development) (rc) (stable) (frozen snapshot)
|
optional optional (integration) (production) (feedback)
|
||||||
```
|
```
|
||||||
|
|
||||||
1. **Development** (`dev/**`): `update.xml` with `<tag>development</tag>`, download points to branch archive
|
1. **Development** (`dev` or `dev/**`): `updates.xml` with `<tag>development</tag>`, download points to Gitea release ZIP
|
||||||
2. **Release Candidate** (`rc/**`): `update.xml` with `<tag>rc</tag>`, version set to `XX.YY.ZZ-rc`
|
2. **Alpha** (`alpha/**`): `updates.xml` with `<tag>alpha</tag>`, cascades to development channel
|
||||||
3. **Stable Release** (merge to `main`): `update.xml` with `<tag>stable</tag>`, download points to GitHub Release asset
|
3. **Beta** (`beta/**`): `updates.xml` with `<tag>beta</tag>`, cascades to alpha + development channels
|
||||||
4. **Frozen Snapshot** (`version/XX.YY`): immutable, never force-pushed
|
4. **Release Candidate** (`rc/**`): `updates.xml` with `<tag>rc</tag>`, cascades to beta + alpha + development channels
|
||||||
|
5. **Stable Release** (merge to `main`): `updates.xml` with `<tag>stable</tag>`, cascades to all channels, download points to GitHub Release asset
|
||||||
|
6. **Frozen Snapshot** (`version/XX`): immutable, never force-pushed
|
||||||
|
|
||||||
### Health Checks
|
### Health Checks
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @package MokoWaaS
|
||||||
|
* @subpackage com_mokowaas
|
||||||
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
|
* @license GNU General Public License version 3 or later; see LICENSE
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined('_JEXEC') or die;
|
||||||
|
|
||||||
|
use Joomla\CMS\Dispatcher\ComponentDispatcherFactoryInterface;
|
||||||
|
use Joomla\CMS\Extension\ComponentInterface;
|
||||||
|
use Joomla\CMS\Extension\Service\Provider\ComponentDispatcherFactory;
|
||||||
|
use Joomla\CMS\Extension\Service\Provider\MVCFactory;
|
||||||
|
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
|
||||||
|
use Joomla\DI\Container;
|
||||||
|
use Joomla\DI\ServiceProviderInterface;
|
||||||
|
|
||||||
|
return new class implements ServiceProviderInterface
|
||||||
|
{
|
||||||
|
public function register(Container $container): void
|
||||||
|
{
|
||||||
|
$container->registerServiceProvider(new MVCFactory('\\Moko\\Component\\MokoWaaS'));
|
||||||
|
$container->registerServiceProvider(new ComponentDispatcherFactory('\\Moko\\Component\\MokoWaaS'));
|
||||||
|
|
||||||
|
$container->set(
|
||||||
|
ComponentInterface::class,
|
||||||
|
function (Container $container) {
|
||||||
|
$component = new \Joomla\CMS\Extension\MVCComponent(
|
||||||
|
$container->get(ComponentDispatcherFactoryInterface::class)
|
||||||
|
);
|
||||||
|
$component->setMVCFactory($container->get(MVCFactoryInterface::class));
|
||||||
|
|
||||||
|
return $component;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @package MokoWaaS
|
||||||
|
* @subpackage com_mokowaas
|
||||||
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
|
* @license GNU General Public License version 3 or later; see LICENSE
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Moko\Component\MokoWaaS\Api\Controller;
|
||||||
|
|
||||||
|
defined('_JEXEC') or die;
|
||||||
|
|
||||||
|
use Joomla\CMS\Factory;
|
||||||
|
use Joomla\CMS\MVC\Controller\BaseController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache management API controller.
|
||||||
|
*
|
||||||
|
* POST /api/index.php/v1/mokowaas/cache
|
||||||
|
*
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
class CacheController extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Clear all Joomla caches.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
public function execute(): void
|
||||||
|
{
|
||||||
|
$app = Factory::getApplication();
|
||||||
|
|
||||||
|
if ($app->input->getMethod() !== 'POST')
|
||||||
|
{
|
||||||
|
$this->sendJson(405, ['error' => 'POST required']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $app->getIdentity();
|
||||||
|
if (!$user->authorise('core.manage', 'com_plugins'))
|
||||||
|
{
|
||||||
|
$this->sendJson(403, ['error' => 'Not authorized']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
$cache = Factory::getCache('');
|
||||||
|
$cache->clean('');
|
||||||
|
|
||||||
|
$adminCache = Factory::getCache('', 'callback', 'administrator');
|
||||||
|
$adminCache->clean('');
|
||||||
|
|
||||||
|
if (function_exists('opcache_reset'))
|
||||||
|
{
|
||||||
|
opcache_reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->sendJson(200, [
|
||||||
|
'status' => 'ok',
|
||||||
|
'message' => 'Cache cleared',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
catch (\Throwable $e)
|
||||||
|
{
|
||||||
|
$this->sendJson(500, [
|
||||||
|
'error' => 'Cache clear failed',
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $code HTTP status code
|
||||||
|
* @param array $payload Response data
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function sendJson(int $code, array $payload): void
|
||||||
|
{
|
||||||
|
$app = Factory::getApplication();
|
||||||
|
$app->setHeader('Content-Type', 'application/json', true);
|
||||||
|
$app->setHeader('Status', (string) $code, true);
|
||||||
|
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
||||||
|
$app->close();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @package MokoWaaS
|
||||||
|
* @subpackage com_mokowaas
|
||||||
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
|
* @license GNU General Public License version 3 or later; see LICENSE
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Moko\Component\MokoWaaS\Api\Controller;
|
||||||
|
|
||||||
|
defined('_JEXEC') or die;
|
||||||
|
|
||||||
|
use Joomla\CMS\Factory;
|
||||||
|
use Joomla\CMS\MVC\Controller\BaseController;
|
||||||
|
use Joomla\CMS\Plugin\PluginHelper;
|
||||||
|
use Joomla\CMS\Uri\Uri;
|
||||||
|
use Joomla\Registry\Registry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Health check API controller.
|
||||||
|
*
|
||||||
|
* GET /api/index.php/v1/mokowaas/health
|
||||||
|
*
|
||||||
|
* Returns full health diagnostics from the MokoWaaS system plugin.
|
||||||
|
* Requires a Joomla API token with core.manage permissions.
|
||||||
|
*
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
class HealthController extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Return full health check data.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
public function displayList(): void
|
||||||
|
{
|
||||||
|
$app = Factory::getApplication();
|
||||||
|
$user = $app->getIdentity();
|
||||||
|
|
||||||
|
if (!$user->authorise('core.manage', 'com_plugins'))
|
||||||
|
{
|
||||||
|
$this->sendJson(403, ['error' => 'Not authorized']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$plugin = PluginHelper::getPlugin('system', 'mokowaas');
|
||||||
|
|
||||||
|
if (!$plugin)
|
||||||
|
{
|
||||||
|
$this->sendJson(503, ['error' => 'MokoWaaS system plugin not enabled']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$params = new Registry($plugin->params);
|
||||||
|
$config = Factory::getConfig();
|
||||||
|
$db = Factory::getDbo();
|
||||||
|
|
||||||
|
// Collect basic health data
|
||||||
|
$payload = [
|
||||||
|
'status' => 'ok',
|
||||||
|
'timestamp' => gmdate('Y-m-d\TH:i:s\Z'),
|
||||||
|
'site' => [
|
||||||
|
'name' => $config->get('sitename', ''),
|
||||||
|
'url' => rtrim(Uri::root(), '/'),
|
||||||
|
'joomla_version' => JVERSION,
|
||||||
|
'php_version' => PHP_VERSION,
|
||||||
|
'db_type' => $db->getName(),
|
||||||
|
'offline' => (bool) $config->get('offline', 0),
|
||||||
|
'debug' => (bool) $config->get('debug', 0),
|
||||||
|
'sef' => (bool) $config->get('sef', 0),
|
||||||
|
'caching' => (bool) $config->get('caching', 0),
|
||||||
|
],
|
||||||
|
'plugin' => [
|
||||||
|
'brand' => $params->get('brand_name', 'MokoWaaS'),
|
||||||
|
'company' => $params->get('company_name', 'Moko Consulting'),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
// Database check
|
||||||
|
try
|
||||||
|
{
|
||||||
|
$db->setQuery('SELECT 1');
|
||||||
|
$db->loadResult();
|
||||||
|
$payload['checks']['database'] = ['status' => 'ok'];
|
||||||
|
}
|
||||||
|
catch (\Throwable $e)
|
||||||
|
{
|
||||||
|
$payload['status'] = 'error';
|
||||||
|
$payload['checks']['database'] = ['status' => 'error', 'message' => $e->getMessage()];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disk space
|
||||||
|
$free = @disk_free_space(JPATH_ROOT);
|
||||||
|
$total = @disk_total_space(JPATH_ROOT);
|
||||||
|
if ($free !== false && $total !== false)
|
||||||
|
{
|
||||||
|
$freeMb = round($free / 1048576);
|
||||||
|
$payload['checks']['filesystem'] = [
|
||||||
|
'status' => $freeMb < 100 ? 'degraded' : 'ok',
|
||||||
|
'free_disk_mb' => $freeMb,
|
||||||
|
'total_disk_mb' => round($total / 1048576),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content counts
|
||||||
|
$db->setQuery($db->getQuery(true)->select('COUNT(*)')->from($db->quoteName('#__content')));
|
||||||
|
$payload['counts']['articles'] = (int) $db->loadResult();
|
||||||
|
|
||||||
|
$db->setQuery($db->getQuery(true)->select('COUNT(*)')->from($db->quoteName('#__users')));
|
||||||
|
$payload['counts']['users'] = (int) $db->loadResult();
|
||||||
|
|
||||||
|
$db->setQuery($db->getQuery(true)->select('COUNT(*)')->from($db->quoteName('#__extensions'))->where($db->quoteName('enabled') . ' = 1'));
|
||||||
|
$payload['counts']['extensions'] = (int) $db->loadResult();
|
||||||
|
|
||||||
|
$this->sendJson(200, $payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a JSON response and close.
|
||||||
|
*
|
||||||
|
* @param int $code HTTP status code
|
||||||
|
* @param array $payload Response data
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
private function sendJson(int $code, array $payload): void
|
||||||
|
{
|
||||||
|
$app = Factory::getApplication();
|
||||||
|
$app->setHeader('Content-Type', 'application/json', true);
|
||||||
|
$app->setHeader('Status', (string) $code, true);
|
||||||
|
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
|
||||||
|
$app->close();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @package MokoWaaS
|
||||||
|
* @subpackage com_mokowaas
|
||||||
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
|
* @license GNU General Public License version 3 or later; see LICENSE
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Moko\Component\MokoWaaS\Api\Controller;
|
||||||
|
|
||||||
|
defined('_JEXEC') or die;
|
||||||
|
|
||||||
|
use Joomla\CMS\Factory;
|
||||||
|
use Joomla\CMS\MVC\Controller\BaseController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update check API controller.
|
||||||
|
*
|
||||||
|
* POST /api/index.php/v1/mokowaas/update
|
||||||
|
*
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
class UpdateController extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Trigger Joomla update finder and return count of available updates.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
public function execute(): void
|
||||||
|
{
|
||||||
|
$app = Factory::getApplication();
|
||||||
|
|
||||||
|
if ($app->input->getMethod() !== 'POST')
|
||||||
|
{
|
||||||
|
$this->sendJson(405, ['error' => 'POST required']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $app->getIdentity();
|
||||||
|
if (!$user->authorise('core.manage', 'com_installer'))
|
||||||
|
{
|
||||||
|
$this->sendJson(403, ['error' => 'Not authorized']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
$db = Factory::getDbo();
|
||||||
|
|
||||||
|
$db->setQuery($db->getQuery(true)->delete($db->quoteName('#__updates')));
|
||||||
|
$db->execute();
|
||||||
|
|
||||||
|
\Joomla\CMS\Updater\Updater::getInstance()->findUpdates();
|
||||||
|
|
||||||
|
$db->setQuery(
|
||||||
|
$db->getQuery(true)
|
||||||
|
->select('COUNT(*)')
|
||||||
|
->from($db->quoteName('#__updates'))
|
||||||
|
->where($db->quoteName('extension_id') . ' != 0')
|
||||||
|
);
|
||||||
|
$count = (int) $db->loadResult();
|
||||||
|
|
||||||
|
$this->sendJson(200, [
|
||||||
|
'status' => 'ok',
|
||||||
|
'updates_found' => $count,
|
||||||
|
'message' => $count . ' update(s) available',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
catch (\Throwable $e)
|
||||||
|
{
|
||||||
|
$this->sendJson(500, [
|
||||||
|
'error' => 'Update check failed',
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $code HTTP status code
|
||||||
|
* @param array $payload Response data
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function sendJson(int $code, array $payload): void
|
||||||
|
{
|
||||||
|
$app = Factory::getApplication();
|
||||||
|
$app->setHeader('Content-Type', 'application/json', true);
|
||||||
|
$app->setHeader('Status', (string) $code, true);
|
||||||
|
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
||||||
|
$app->close();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<extension type="component" method="upgrade">
|
||||||
|
<name>MokoWaaS API</name>
|
||||||
|
<author>Moko Consulting</author>
|
||||||
|
<creationDate>2026-05-23</creationDate>
|
||||||
|
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
|
||||||
|
<license>GPL-3.0-or-later</license>
|
||||||
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||||
|
<version>02.03.11</version>
|
||||||
|
<description>Minimal API-only component for MokoWaaS. Provides REST endpoints for site health, cache, updates, and backups.</description>
|
||||||
|
<namespace path="api/src">Moko\Component\MokoWaaS\Api</namespace>
|
||||||
|
<administration>
|
||||||
|
<files folder="admin">
|
||||||
|
<folder>services</folder>
|
||||||
|
</files>
|
||||||
|
</administration>
|
||||||
|
<api>
|
||||||
|
<files folder="api">
|
||||||
|
<folder>src</folder>
|
||||||
|
</files>
|
||||||
|
</api>
|
||||||
|
</extension>
|
||||||
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -15,5 +15,5 @@
|
|||||||
; Variables: (none)
|
; Variables: (none)
|
||||||
; -----------------------------------------------------------------------------
|
; -----------------------------------------------------------------------------
|
||||||
|
|
||||||
PLG_SYSTEM_MOKOWAAS="System - MokoWaaS"
|
PLG_SYSTEM_MOKOWAAS="System - Moko WaaS"
|
||||||
PLG_SYSTEM_MOKOWAAS_XML_DESCRIPTION="This plugin rebrands the Joomla system interface with MokoWaaS identity. It applies language overrides and ensures consistent branding across the platform."
|
PLG_SYSTEM_MOKOWAAS_XML_DESCRIPTION="This plugin rebrands the Joomla system interface with MokoWaaS identity. It applies language overrides and ensures consistent branding across the platform."
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user