diff --git a/.env.example b/.env.example index fb0c72f..04280f9 100644 --- a/.env.example +++ b/.env.example @@ -36,6 +36,12 @@ JOKER_DYNDNS_USERNAME= JOKER_DYNDNS_PASSWORD= REGISTRY_USERNAME= REGISTRY_PASSWORD= +REDMINE_URL= +REDMINE_API_KEY= +REDMINE_PROJECT_ID= +REDMINE_TRACKER_BUG_ID= +REDMINE_TRACKER_SECURITY_ID= +REDMINE_ASSIGNEE_ID= # Admin bootstrap (used by seed/reset scripts) ADMIN_EMAIL= diff --git a/creds/secrets.enc.env b/creds/secrets.enc.env index 21fee45..f0dd03c 100644 --- a/creds/secrets.enc.env +++ b/creds/secrets.enc.env @@ -1,44 +1,50 @@ # Encrypted with sops (age). To edit: sops creds/secrets.enc.env -AUTH_SECRET=ENC[AES256_GCM,data:DQl4MEDrsUe76/NYdI03BZb31/NTTA0nBTUALhsULvT1EvMRRiR1,iv:rXjuUm3Z5OFMa0Jc/h8BSF1DWxddrtH2DyvPwX8KZ3c=,tag:eL7NZj0WE44y9AxVoCXrFA==,type:str] -DATABASE_URL=ENC[AES256_GCM,data:87ysYMPY0lI8LrL46dHPjcZOYo4IlP/aQ4h+psSXS4EKzUiBJuExtF8f+kAZNJBc4Xj+B82Q34CfbbsBC8NdQoeebqvXF6/0IluvmpCBhpN2MKWqwlx/mQZTENZA3J0QiAP+BapeZXuXLc//,iv:n4W+hyWf1oH8IahJJs1XQ3d3i5hJjLXaNE+b4Z66Zbw=,tag:JYsBd5d0APkikYYg8Ea/Fg==,type:str] -DB_HOST=ENC[AES256_GCM,data:qlmMVJEkDwfiRYGFUdP/,iv:syUEM2Cx64jE7IW8zRSqfe9uAL+JEMl9Tu5nw12kdWI=,tag:VoAJgXgGy8OXksUvn/SpUQ==,type:str] -DB_PORT=ENC[AES256_GCM,data:vLjEIHpK,iv:ym9R0E/S5WKLKF+0thz9tmwI4pYa7skrY4sDpY42y0E=,tag:SK8gDGrnx8/YAPi8qB7KeQ==,type:str] -DB_USER=ENC[AES256_GCM,data:KIlxtjxjx0EIDxXxHKY=,iv:dlhX4pIe6AE8fzT+TVB3hg9gJ46sq85Qp8f4pkxO9n8=,tag:WCMaOPpnI8SA8WMRDA392w==,type:str] -DB_PASS=ENC[AES256_GCM,data:CJEh2+yQrPptlTnthe2UWsc/baNXBIAC0lpxtm8DI47gTg==,iv:UPrDDlMbWaiAiz5QcjRqXRIiVohMRF7wtmi5ioONcLE=,tag:jLSfXcefy8+cll8bVclkSQ==,type:str] -APP_URL=ENC[AES256_GCM,data:1vZsUQ/d4MTbBM+juPJeVwseAVz4O2M=,iv:KrsOr3w1i3j3R19KAowFxFbdSm1B9IikqRoqU9tmQus=,tag:BwR3v/2R7bAFxHnB5waFIA==,type:str] -SMTP_HOST=ENC[AES256_GCM,data:H+47VDofg1EEBo6q1UEleQ==,iv:KlAxFj/bKOMikubGbqPYkKPZt18oIdXxrLjEVnbxhN8=,tag:LpC930G7lsjt9YuSEFxW8A==,type:str] -SMTP_PORT=ENC[AES256_GCM,data:F6fiIz4=,iv:Pelfn6ciIGvqF4nMPX5WrsWqkLtAxPBmBOSBvn4RUmo=,tag:UWBGXnDAYf5zCQJagiSE9Q==,type:str] -SMTP_USER=ENC[AES256_GCM,data:CnbiHSRC0w/TOwwUwto=,iv:s8tJxs3X3BaFf5LrR+hHVvRIbDEfNI6ZszdTi+lg3PQ=,tag:DAbkk3szBzmhUrNTPZdA6A==,type:str] -SMTP_PASS=ENC[AES256_GCM,data:GmmDQ2mROFz9zpGpXgtwlUvU2dmWg9KHsw==,iv:pl7O1QCjkaaqZ0sGS6kIBUKJ5glE8FTyA1vCYz0BfoU=,tag:/eE1ZWrLlKsa043VY3p3DQ==,type:str] -SMTP_FROM=ENC[AES256_GCM,data:XY9BFPRmpO5KblsSMB2680DNx/bvUndfXA==,iv:yCUsaOMu1MrHtfR9AwfuIscyiEqsl0OYvO8UiKO6IYk=,tag:MpEIu2UJXEjE+9n1MVaHRQ==,type:str] -SMTP_TLS=ENC[AES256_GCM,data:sBPcDhWs,iv:lZRjMBtrtAinC1blRFE0RH5jzyEcdLf+UOgEHuKitEk=,tag:R3Q4+ri3m5LB+xzohsYJHA==,type:str] -SMTP_SSL=ENC[AES256_GCM,data:dYAo31bHTw==,iv:OG0c0HYzewLwlsgPlyn0nz+vwdeuSCjJ5ifdE9bfQvI=,tag:EJ5CX6+weXcrcjdrr1uicg==,type:str] -SMTP_REJECT_UNAUTHORIZED=ENC[AES256_GCM,data:HhBgopyX,iv:7W9TYVN1aEH5QWK1NXGE9mw9gCd7yagg/LFVNHSdMV4=,tag:rcWGdoXH2wXtqWaImLEfHA==,type:str] -DKIM_SELECTOR=ENC[AES256_GCM,data:taqEDa6Ah1G6yQ==,iv:n4KfE3lFBIyndBFkgvoBvzAlqP2HV4RIyL8c7lLoQSU=,tag:nos80rdqlj7YQshANBoTvw==,type:str] -DKIM_DOMAIN=ENC[AES256_GCM,data:ZQkoRT8bQ7/Kdt0O/M4kRB8=,iv:wSbk2ALpbcxRT8neFrcQBEj8ja54ZtWeKTfllm8Pr5s=,tag:o5ZvS1enT31pV4+EM8i5LA==,type:str] -DKIM_PRIVATE_KEY_PATH=ENC[AES256_GCM,data:8hbyhw/+I1VWGzoDNIKMY3avRvSmt17EmHiwv7WpeJZzalEXqTmDB+bpkVEQ,iv:/7bUU6boXo1HPccvNBCDnLyql0hGaMXmGIeQEqpFnwM=,tag:nTE+dR0JB/Y1ri5h/FaC3A==,type:str] -AUTO_APPROVE_LISTINGS=ENC[AES256_GCM,data:bAHiVYhjXw==,iv:/jKFQ5L+meSNF6tn54Dc8dpK1I0+zG/o/F0QqMInmwU=,tag:9f7mb2QN17AoualTjkLeSQ==,type:str] -OPENAI_API_KEY=ENC[AES256_GCM,data:+Avly5xD/jgkeiiHg7WNVH/OyGRf9L+iKDh/SvD7LrFuJlltORwzY253l29vD6cucaYfFanTWwDaV6TKuatNiRXxzy7aSoOfs0y4qonXofIRb3BH2/DarGWxQtRi5LRrXFIpCRQfN7x+XGbeF8ZZBW/DHFYywH0E2fU9li/iafXVOKBRcV4WYTIA3sJJuObHFjxgRYMB2SWtw8noZvkK+pzQi6lCKA==,iv:WTbvAzkjoDLAuDIdbFjt9BU5+2pGDv5Ksc4xru0Wx20=,tag:b8ddMgRoq+kMlS994yPx9A==,type:str] -OPENAI_TRANSLATIONS_KEY=ENC[AES256_GCM,data:QGIhIhzn775yXZq9OYKTwGwcLnQAIJn8TM95TRFb5jWY/gTevspeYLDVrrGDKv/WHX8aTg4r4z9t6cbgbSX677fSlrNy2c2YEb5uCLGh0QUK2LoPt9R9yF/iyi5CU2obRlqamtF6knUGG5mdOCewKx/k+updDLmOe+CPD91H6nWbE9+1Pmbdf22bM+rOt9t4+MS6UP0UbIYU0sBEtfPTvN68260fUA==,iv:eYzIudN/i0A38X1up+Jnxv7qE5aiUEu0WQHEtQ4DvlA=,tag:qM7noB1GvXt2F69aVd0KVQ==,type:str] +AUTH_SECRET=ENC[AES256_GCM,data:70URX0qSS/z4AYb58kAmGA/gnEt9bBGPPN5L/xpg3vsna8A9jw+X,iv:iKicMbvIaBvPXncKcmJwsbjQzjxOa7xmkm8SXeZ8RUQ=,tag:2CXRV4I5YVz52aFm65WGhQ==,type:str] +DATABASE_URL=ENC[AES256_GCM,data:t/E/j249UXAOY9gcnb15ozJOmvDC7Ffb9RnfzRUvH9HXEqVQPlh8jM8qonygiMHmXcWYqVnqtRcFbExGYNo1h6ZRoAMcHhGfA2RqagLB8JtuDYvJzd9dsdc1z/DdwBDqWiQy2s1hzYBYTSpa,iv:/1BkhzropZ+ZHMN1aCpllI//aCa5qU4T0n765LMsvW8=,tag:6wH1Aly//CIRGAANVz7RdQ==,type:str] +DB_HOST=ENC[AES256_GCM,data:Jvk5Zes8XH50096YHApn,iv:GDU3tu24u8lwuMLQbrv1chNLspzW8oIaPgS9ZgiZkro=,tag:vmTdIZL2E7CRkb6TYxr5eg==,type:str] +DB_PORT=ENC[AES256_GCM,data:DXNr+ZA2,iv:9kY1pEVj9LteVs/GPnCdZrblI+8ubrqNXwiS8IW4954=,tag:YgJalc+nsdYD6m4tNayJcw==,type:str] +DB_USER=ENC[AES256_GCM,data:K+rYqv9CQpV5p8zDXV0=,iv:TiGMbFAiyVPtbe0JmXxsilAYv4WmgTdPu4muCgSmSgY=,tag:4/kA2T3w7BUytcW0CVyFCg==,type:str] +DB_PASS=ENC[AES256_GCM,data:w49QEtuN2mFthnn4X/DvoxwbhImYVqDUYG1Ptq4ba22P9w==,iv:cSOmskpZAi/aYpzj3+QBmVIQZfEs2itDqaMZA9eX8v8=,tag:NJrdaN6zIRE0DBpVTzDL7g==,type:str] +APP_URL=ENC[AES256_GCM,data:xr9j3Gt/y5Uq8bHzTyTKsYtpSRg4GKU=,iv:oV68MYwwipB5tLpXXDFln2YF4hPHWiufyRm3csamssg=,tag:Bj1K3tC3LyP8ezNJG8bpIA==,type:str] +SMTP_HOST=ENC[AES256_GCM,data:wXOHpMWXQ2/spsVqVDH07w==,iv:FHffrNhUk9sY5Ew3b9h6LBQgNCRfCu3kFMKBOYXMY/o=,tag:/PJ/UrZHHbyWYg30+C6dfw==,type:str] +SMTP_PORT=ENC[AES256_GCM,data:6r6Zsqg=,iv:eNzU1Dk3vx7kwJu72tMZDx4r9ngHUKx1t1zAYSyFFb4=,tag:aqryLBX9xPM6DvGd5Fwl5g==,type:str] +SMTP_USER=ENC[AES256_GCM,data:IVpG+wm7l0Kk8UfhrAo=,iv:GiQChcgXDzD/h8fBHXAeKWez3IXR4NX90NU9582hThg=,tag:b8jcQY2G2HAo3pQjc7+B0Q==,type:str] +SMTP_PASS=ENC[AES256_GCM,data:siqtgQg2WRrRbD6yqTawLAo++u2fWvpVLg==,iv:rL5AZQ6b3PaOJhG3vGZYzXeFxm6yd+qhxNzYIN6pxQk=,tag:r0LTjC8DQ9rsLTQa4XLhAg==,type:str] +SMTP_FROM=ENC[AES256_GCM,data:WH8AW4SVhZkeXB6cNZCfj7opSTwoqYm8TA==,iv:Hy55AgFV0VSurplLVuPPqtLcyDQr/IIvGHF6Ppl01t0=,tag:vKtXoZRFgSeMYqSL9enElQ==,type:str] +SMTP_TLS=ENC[AES256_GCM,data:42eaSf82,iv:sMLqYMHN5hzu9wrjmH9DhHcSPK06f0ZGqv/HPMsMPo8=,tag:rxoUQyT2jEC2VjRJMJV4Sw==,type:str] +SMTP_SSL=ENC[AES256_GCM,data:Am3rKylCqQ==,iv:P6nQdWsV1QkBYfg2oeR3JbEsJYNGvd0h/ZFza2PObY8=,tag:z5VAAD4IjNi7hMISgjsisQ==,type:str] +SMTP_REJECT_UNAUTHORIZED=ENC[AES256_GCM,data:1wLP1f9a,iv:exUSGxwA7CZ6IuBTBuAikerCe+algCybvhW6xji6h2U=,tag:Ehf8ok6P5Bx2Ky6kzOplIQ==,type:str] +DKIM_SELECTOR=ENC[AES256_GCM,data:gv0POpTZG/69Cw==,iv:eBeiuZPNWo6DPQ25Uupo8+1EacBGOx3FUOR9arhELV8=,tag:/6gICHQGYlO4KLeu/r0VcA==,type:str] +DKIM_DOMAIN=ENC[AES256_GCM,data:j+C08N0l0+7sM1UFgVtST5A=,iv:YB0enFjwfhhf4wJTN61saaCJx1qnUnHrroLzrlna96E=,tag:Ni6Q31jrzhgfFNvjIRcSNA==,type:str] +DKIM_PRIVATE_KEY_PATH=ENC[AES256_GCM,data:JAykp2gN4keLwzM962rgdJ6T9af+4WFsD5VRUBaIedf5v5v7cGJNJ+FtdrUV,iv:Ds5/3TTcbepjIMXW2YJbEKZ/aBlqk2OlEv+dIFOcQKo=,tag:Sdwnh33uMnnbPlD73rvwRw==,type:str] +AUTO_APPROVE_LISTINGS=ENC[AES256_GCM,data:gkLX0/36sA==,iv:ma0etTesxiB49Ohgtes/2obnrvSJpmRlDZZX1gbXYt4=,tag:OFHkkH0QOEJ9PVBQTauvTg==,type:str] +OPENAI_API_KEY=ENC[AES256_GCM,data:eVJRCNymtUQOMl5IMySoGcxGC+pVoDvIyCD6idP7CoZQFqE+y5dJ1i4LW7Y9Rb5Su8+rutwkcVu2gh3wx9XxgEfFazHKQm1lm9QaeCvKWrT/MkO26kCPAJO/vkFUp8OFnK35TG5v/RrHZZUwviV7YXEcjqvB82E59Tz6AzqlUvt1ENloK7EsK5DkkSZUdapzdMcegwqOaUUpMxZn8n/lU+pWgAp/RQ==,iv:9hLQOS5qNllbLF2Ggi5GgUENHtnM9zHBxHAnv8ppaow=,tag:+8TAn4UmxZDtQoDbeDsGDQ==,type:str] +OPENAI_TRANSLATIONS_KEY=ENC[AES256_GCM,data:Se/eY/f00J2NLSVgvh4EMyklMHDJvnkpmzdqZl2oZWMxnLdxtpTBh8nKEfO2I9ovChdMacRseXJ2J1+oBk/1zw7xSqMastwg5EdftC4y2G8TODkPwWqDnB1t1jNpObA/gXeSxtUdE8JtM8dU4U37+KSMRjM7afdQy++YHRes1OzqBnFUdFvAwNHfi//uya0Q4CEl2unsiha8iLWaNT+skf27EgFOUQ==,iv:oBu3SPls776zktVWH7t9OwEI/p3h4WM3jWEJzluI64M=,tag:tRyJtqFkuxdMcFqEfi7RbQ==,type:str] HETZNER_API_TOKEN="hoRULGviS8G3OGaJ68josx00M53efhuntVM5Rfft1AOvUR0ZQTXlO6yivhGqBM5o" -HCLOUD_TOKEN=ENC[AES256_GCM,data:0qm+oR2daCd9e0qc+2wApobRod0RsUeA2QL1kQarQW77FvPAgQ7jBkML39kBP3xsYsrdmiZuIdg63Q3xeBQ9mVq2,iv:JUjR7vGyX00TOlUCOD8M9jiB6iio4IiX73sLzW/CADA=,tag:9p0kgDD7ul8s5T0cL7vsfA==,type:str] -HETZNER_TOKEN=ENC[AES256_GCM,data:e95QAx49/qkoBnAmyup7NBs/Xm/fkWutMIWzlAv4P1ybSJNoCyC4ZibpNTCtYtK/3Hux5ItdOBm/jhssjlX363cI,iv:4UzDqJd0mYP3IJgeMn9xz9UjsVyODZ44CDAz5PPaXbk=,tag:ZEAqueoXY4kBfe+89nA+Kg==,type:str] -JOKER_DYNDNS_USERNAME=ENC[AES256_GCM,data:FBAh4B+vxosvxfHbrZukSNea,iv:4FexfnCieAaZ7iUSvaiATr5THpqoHaebX/QigA9Wl58=,tag:dY6lQnHTJWOZa7WhHwPELA==,type:str] -JOKER_DYNDNS_PASSWORD=ENC[AES256_GCM,data:1neBKFvQhFGfOgdPI6EHZKKI,iv:fC1J7QjAoQMUKxx6lMO32n6zjQ5tQa/icBDn19Lih2k=,tag:W110zqgwbsrVbkZzav/F0Q==,type:str] -REGISTRY_USERNAME=ENC[AES256_GCM,data:Mt1mk15DPH8=,iv:Hxg4fRLRZ2hKRACTIIUMSODRBOgRv5Y6KfZ8477cwc0=,tag:Ufq2jYQjYzjiB88PF0gpBA==,type:str] -REGISTRY_PASSWORD=ENC[AES256_GCM,data:4l2oMR65+X+1,iv:cfh6Esayb6zJEqKrtfY0LAncIrPi7lj/ckcbMgTMFys=,tag:vhsiagOyGVjN7+GNPwZ8LA==,type:str] -NETDATA_USER=ENC[AES256_GCM,data:6laH3H7JKgpU,iv:PCI8HL6S68AaBRg11YlNbK9R12G1ROMqRIpXKIlsBDQ=,tag:qL1uWrtWz5+c6f6Xbv4Low==,type:str] -NETDATA_PASS=ENC[AES256_GCM,data:CZxY7WeoHuUNmECOiL6XLVeTt/k5PAHFyV4dVBrF,iv:Zp00OVvA5hI8c9AD4pvDB1s99yw9iOlgBV4K4nbFFVg=,tag:Y077Bp8qk/SoEOajOdvMLg==,type:str] -NETDATA_HOST_NODE1=ENC[AES256_GCM,data:QEHB348q+BQubmvIJkH/8mie2fjb0mE=,iv:ZUNZA64cuflqQq+JS/e0wByTzny0eE/4rvaxzNN1JFw=,tag:YfmJWJCd1MQW2Q2bWferLg==,type:str] -NETDATA_HOST_DB1=ENC[AES256_GCM,data:B8mSqI+rusKxazFcziGabYMM1i1q,iv:1hDrheKjdo7dMF1m7qOWQ9llye2glEQm79LONauLiYk=,tag:H1BVhIKfymLBzGS6CZEp3Q==,type:str] -NETDATA_PG_USER=ENC[AES256_GCM,data:n0G+MZWEWQK9,iv:11id08TbMpc5NUqtKsoXiIFfM5GJCFStzJ5aY1AGplc=,tag:gXWcgEXko4P7qMwVLGnF1Q==,type:str] -NETDATA_PG_PASS=ENC[AES256_GCM,data:ylpbhrcFFKC5qiyt+RRPZhCWt098tPhGWxabgsO31tR7+g==,iv:O4b5DsNfCpqlhZ3GmXMRzp2/fUpMtLnhSqmoaD4+Q04=,tag:TG7ge49hjzkWcBO93hoXuA==,type:str] -NETDATA_PG_ROLE=ENC[AES256_GCM,data:6/ESugRIiDgBM2e8,iv:kXJOKx7fbO4k9zV2APTXCxOrGJ/93dCkmVA33v9fzrY=,tag:HwEWzVagm6cDnPychaekaw==,type:str] -ADMIN_EMAIL=ENC[AES256_GCM,data:AuhvA1iUOnMcE7el4TSE7cbLAg==,iv:eWNxWDA978OWQO10/ywc8CpKuUhC0uTqObmD5Tefgtc=,tag:pI4SXW1ZrZPthD2qRAPrIw==,type:str] -ADMIN_INITIAL_PASSWORD=ENC[AES256_GCM,data:WiJm5VO57/qLu8fK0rZqFDk=,iv:2SwLMvMUjwkEDZo8VRtPHfs+cabz2L+9ZQAaWyn1o9c=,tag:+TunF3tuLxYvMeJsYNQuCQ==,type:str] -sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsNlZ6dWs5VnJXa0hrekdp\nYlpzYll6NUFnTnV0Sk0zbnB2WEw5alVwT2swCm82NmlUbjdabEJ2d1pHTGhYRG1m\nZmIyUHFQcVhvZHNzRTJuZVNqYThaWGMKLS0tIGtHTEhzQnh2SFVxdlBvRlpMblVW\nOWdVY0ErK2pVSzdtckc4Y0lPRTdrdmsKv5M0ojCoW5SQhnjXY116SmjvyCtSnehg\nQqtL6jElOv4MeLASHwYLYzznU6dxkZK3OKvcLh6mu+41Pnbl8u26yw==\n-----END AGE ENCRYPTED FILE-----\n +HCLOUD_TOKEN=ENC[AES256_GCM,data:SZMlHgX2keD98hk2VU5G3aGdla3MsaSR4BqnCM4F9fNp3DNdx9zQyiRJ8LDBi62HUPBBubj1YSnUPnJFJ5bXtv6A,iv:mVor150MZPe7jMHEBtcXcoT434bAX1t7WSTLOG/hexY=,tag:DKWHOSg61vG/7qKsMDz+uw==,type:str] +HETZNER_TOKEN=ENC[AES256_GCM,data:oe8Kp7a/j3PRc8IUnU0SUtu6zr4nqdUYWeR/TbmYz/vRoz5egDqbqi7SXi2GC0YD/JATMNiHL1ec5tYWPnceaKNS,iv:pm3IBAQSEEMqELno/hAFZgROXDixJdKv5GqEbAqSwMk=,tag:QLU06Q1D05pWSyQkKV2q3g==,type:str] +JOKER_DYNDNS_USERNAME=ENC[AES256_GCM,data:jQEKhX2ISB9FlwMnnrp7oZ4D,iv:9nnUG5nD7DzPAAYiVDNd4C64YTgF9BSnq0+/WajJPVA=,tag:HLYprQoKRkTtaB82mPLnbQ==,type:str] +JOKER_DYNDNS_PASSWORD=ENC[AES256_GCM,data:LUHbGwOLq+A3CsjYJ4iu9vCn,iv:Phl3sv3hCe8wRWX3TqlyeBsqhKiu2bvJF8M6f1ESnHY=,tag:lmQIynYBEl7jVg3pG/TOAg==,type:str] +REGISTRY_USERNAME=ENC[AES256_GCM,data:IJKWI15AROA=,iv:crKgYBH39sKioPVNw69fv9H5NCGadc6X9DebqUIyC4M=,tag:7PBRGErLYdLi+ku/+U+9UA==,type:str] +REGISTRY_PASSWORD=ENC[AES256_GCM,data:viuSD5MpNuGc,iv:/2Rcuv2cecP8iECeUfK/NowP3Fv2p+EPyqkCJoLQZnw=,tag:BApcmBr3jR+NoQ3ZqRaI8w==,type:str] +NETDATA_USER=ENC[AES256_GCM,data:T51yECIfawfQ,iv:9q/drpZy7C6KzscGB+Lxgu0Rsg4Ov9qgvmUt2lfYfAE=,tag:XS1rN2wy3OAizpA6Ji5FhQ==,type:str] +NETDATA_PASS=ENC[AES256_GCM,data:5j/hxKa4+9ovvaVqJdRWOns7cY2nG+nI5f5p3nkm,iv:/59+59bmpVv0XaeJrVOcLzS6sPEi3XTf3eNiwmsZFCI=,tag:1GrKhx0Zp0IztBtNpzVmxQ==,type:str] +NETDATA_HOST_NODE1=ENC[AES256_GCM,data:9k1sqAJV2bLEStTC2L8KZ9OryFwLhDw=,iv:VWKvsEjOXBtfYHpiNG9wBX4HqG8i3XGW9wK6uOM0qbU=,tag:qKqYvnC0InnK+f3c7O3iTg==,type:str] +NETDATA_HOST_DB1=ENC[AES256_GCM,data:ijCscUEJoup0CHh2vMtTTGzvkMtH,iv:taAvhzY9rpIgsdyR/B30Zo405l0lKO6/BvtAILwUWKA=,tag:ogOvtm7+y7R1TW82BfMWHQ==,type:str] +NETDATA_PG_USER=ENC[AES256_GCM,data:ss8KoNmpRKk/,iv:kTHJyi4YpE+peQmXobd5jTFYrnhOWqypbnhsr251l+o=,tag:zTxRwUoqySysstFjOhF/NA==,type:str] +NETDATA_PG_PASS=ENC[AES256_GCM,data:NEENSnAkLmBY7LxTzoqjNtG9YCFAdvF2yOj2wkzBvKbZew==,iv:7ZJNwLldd3i0BhO3uujzflhR01qDT9gDcqVtsUOXubg=,tag:rukJnDEItfa4csPxN3tvPg==,type:str] +NETDATA_PG_ROLE=ENC[AES256_GCM,data:luPLkyzGXTf/MuYl,iv:VZf1TZlQzNkPqrFkdZhSQ/D+q9MTgz0jW4/88+4X3rs=,tag:ngpPhPvdad5U1ia2h0VMQw==,type:str] +ADMIN_EMAIL=ENC[AES256_GCM,data:YUW3FIusETg/uRJQaP6ZCWjL1w==,iv:ibEncZK+qUsuNBXra6CxAaQQF4Oi9K4msaiOGggad7I=,tag:Sd+9MgTRx8/WA7GusZEuFg==,type:str] +ADMIN_INITIAL_PASSWORD=ENC[AES256_GCM,data:VXiQWltYpxkts/xLNA4p8F8=,iv:as8Uq4S05e8peTP9phv5otdbyb7uyQe5gJ/ig7VKC2E=,tag:gsXXzVtft6AQBjQgrrhfng==,type:str] +REDMINE_API_USER="lomavuokraus-bot" +REDMINE_API_KEY="1826930a88e3732444c55efa5ea3b9ad6a7aeaff" +REDMINE_URL="https://redmine.halla-aho.net" +REDMINE_PROJECT_ID="1" +REDMINE_TRACKER_BUG_ID="1" +REDMINE_TRACKER_SECURITY_ID="1" +sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBMQURYZEQ0SzhGV3NnaE1Z\nYWxucXdSY2FiT2Z1NnRIdlJmYkplRG11NVRRCmZab2VSalB5YjA3SjRYTi80dWhC\nbzVtYTlYeGxMaURHc3NueEg1ZWVjM0EKLS0tIFh3WWhRMXJQaHBCbWlhdEZrV0Fi\neTNZclVSZ3hhcGttWk53UFRIUGZLNncKXF7cGtBMBDVuFN2Y1lpN5hrbZacBOjvI\nUdq1/P3dOSFD3ciQBslZXgRJnK/hAQuP+f1RJ7KUwB0GvLXIiDQs6g==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_0__map_recipient=age1hkehkc2rryjl975c2mg5cghmjr54n4wjshncl292h2eg5l394fhs4uydrh sops_encrypted_regex=^(AUTH_SECRET|DATABASE_URL|DB_.*|APP_URL|SMTP_.*|DKIM_.*|AUTO_APPROVE_LISTINGS|OPENAI_.*|H(ETZNER|CLOUD)_TOKEN|JOKER_DYNDNS_.*|REGISTRY_.*|NETDATA_.*|ADMIN_.*)$ -sops_lastmodified=2025-12-11T11:36:56Z -sops_mac=ENC[AES256_GCM,data:bnDwxj/t2X+vkq1nd2Bej23GBn3hALXW6PAp4FyoAlvwajztp9U2eyF7voLQDeX1kurVBuACPExzzMnerEXOebF9l5SGcIYfvtVj9kk4I0WRCbVBt/QKgEtqYJ3l1TXrDe8ZPTj6O2rK6WW36RDExFDu3tzzvVEaHErZMjAhD1U=,iv:hBwrHOvabZEqeWHSFGvk5sHYbJiF6/3wY1JXgaevB9w=,tag:2fZQ9AQoHQJ+zte2yLpi5g==,type:str] +sops_lastmodified=2025-12-14T12:06:30Z +sops_mac=ENC[AES256_GCM,data:R06Pa93hx3HOijkBKdgm+MgBMVgkAoMS2wzZe55E1b5TlHLfTaa8DuqBNHkor/bPsJW584ByO0AA3DiLkeCLgo9yMulYhuizRIpzKsOI2FH5KcrTGOxj1h5Wxno0ToRoXNtgy71s2aDf7hmzLQhGetAOsmEl+HFy1DMs2dauU5Q=,iv:jh4OLLqTG+eBac6MmwR0CXN4irX+QZZbeTUcrQUOyQ8=,tag:ViE3djLoGcrbqMJgD9r3SQ==,type:str] sops_version=3.11.0 diff --git a/docs/index.html b/docs/index.html index 509e49b..965d19f 100644 --- a/docs/index.html +++ b/docs/index.html @@ -19,6 +19,7 @@
  • Logical Architecture
  • Feature Sequences
  • Security Testing
  • +
  • Redmine Integration
  • diff --git a/docs/redmine.html b/docs/redmine.html new file mode 100644 index 0000000..48b4836 --- /dev/null +++ b/docs/redmine.html @@ -0,0 +1,50 @@ + + + + + Redmine Integration + + + +
    +

    Redmine Integration

    +
    File tickets automatically when tests fail and review open work by tracker.
    +
    +
    +
    +

    Setup

    +
      +
    • Env vars (see .env.example): REDMINE_URL, REDMINE_API_KEY, REDMINE_PROJECT_ID, REDMINE_TRACKER_BUG_ID, REDMINE_TRACKER_SECURITY_ID (optional, falls back to bug), REDMINE_ASSIGNEE_ID (optional default owner).
    • +
    • Uses the Redmine REST API with the API key for authentication.
    • +
    • Ensure the API key is scoped to the project and can create issues.
    • +
    +
    + +
    +

    Automatic tickets on failures

    +
      +
    • scripts/run-test-suite.sh now files a Redmine issue whenever any check fails. Security-related failures (npm audit, Trivy, ZAP) use the security tracker when configured.
    • +
    • Issues are de-duplicated by fingerprinting the suite/target + failure details; reruns of the same failing state will re-use the open ticket instead of creating a duplicate.
    • +
    • Manual wrapper for other test commands: ./scripts/run-tests-with-redmine.sh npm test (set TEST_NAME or TRACKER to override labels).
    • +
    +
    + +
    +

    CLI tools

    +
      +
    • List open tickets grouped by tracker: REDMINE_URL=... REDMINE_API_KEY=... REDMINE_PROJECT_ID=... ./scripts/redmine-report.js list-open
    • +
    • Manual issue creation from a log file: ./scripts/redmine-report.js create-test-issue --suite my-tests --failures-file /path/to/log --tracker bug
    • +
    • Outputs go to stdout; non-zero exit code on API errors so CI can fail fast.
    • +
    +
    + +
    +

    Notes

    +
      +
    • Tickets include a short fingerprint in the subject and description; keep it when editing so future runs keep de-duplicating.
    • +
    • Summary/log paths are included in the issue body to help locate artifacts from the run.
    • +
    +
    +
    + + diff --git a/scripts/load-secrets.sh b/scripts/load-secrets.sh index 87276b7..c78c5ad 100644 --- a/scripts/load-secrets.sh +++ b/scripts/load-secrets.sh @@ -7,6 +7,8 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" SECRETS_FILE="${SECRETS_FILE:-$ROOT_DIR/creds/secrets.env}" ENCRYPTED_FILE="${ENCRYPTED_FILE:-$ROOT_DIR/creds/secrets.enc.env}" +KUBECONFIG_FILE="${KUBECONFIG_FILE:-$ROOT_DIR/creds/kubeconfig.yaml}" +KUBECONFIG_ENC_FILE="${KUBECONFIG_ENC_FILE:-$ROOT_DIR/creds/kubeconfig.enc.yaml}" ensure_decrypted() { if [[ -f "$SECRETS_FILE" ]]; then @@ -24,8 +26,33 @@ ensure_decrypted() { } ensure_decrypted || exit 0 - echo "Loading secrets from $SECRETS_FILE" + set -a source "$SECRETS_FILE" set +a + +ensure_kubeconfig() { + # If user already set KUBECONFIG, respect it. + if [[ -n "${KUBECONFIG:-}" ]]; then + return 0 + fi + + if [[ -f "$KUBECONFIG_FILE" ]]; then + export KUBECONFIG="$KUBECONFIG_FILE" + return 0 + fi + + if [[ -f "$KUBECONFIG_ENC_FILE" ]]; then + if command -v sops >/dev/null 2>&1; then + echo "Decrypting $KUBECONFIG_ENC_FILE -> $KUBECONFIG_FILE" + sops -d "$KUBECONFIG_ENC_FILE" >"$KUBECONFIG_FILE" + export KUBECONFIG="$KUBECONFIG_FILE" + else + echo "sops not found and kubeconfig is missing. Install sops or set KUBECONFIG manually." >&2 + return 1 + fi + fi +} + +ensure_kubeconfig || true diff --git a/scripts/redmine-report.js b/scripts/redmine-report.js new file mode 100755 index 0000000..cfdca10 --- /dev/null +++ b/scripts/redmine-report.js @@ -0,0 +1,297 @@ +#!/usr/bin/env node +const fs = require('fs'); +const crypto = require('crypto'); + +const [command, ...argv] = process.argv.slice(2); + +const parseArgs = (args) => { + const parsed = {}; + let i = 0; + while (i < args.length) { + const arg = args[i]; + if (arg.startsWith('--')) { + const key = arg.replace(/^--/, ''); + const next = args[i + 1]; + if (next && !next.startsWith('--')) { + parsed[key] = next; + i += 2; + } else { + parsed[key] = true; + i += 1; + } + } else { + parsed._ = parsed._ || []; + parsed._.push(arg); + i += 1; + } + } + return parsed; +}; + +const args = parseArgs(argv); + +const ensureEnv = (key, optional = false) => { + const value = process.env[key]; + if (!value && !optional) { + throw new Error(`Missing required env: ${key}`); + } + return value; +}; + +const readConfig = () => { + const baseUrl = ensureEnv('REDMINE_URL'); + const url = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`; + return { + url, + apiKey: ensureEnv('REDMINE_API_KEY'), + projectId: ensureEnv('REDMINE_PROJECT_ID'), + trackerBugId: ensureEnv('REDMINE_TRACKER_BUG_ID'), + trackerSecurityId: process.env.REDMINE_TRACKER_SECURITY_ID, + assigneeId: process.env.REDMINE_ASSIGNEE_ID, + }; +}; + +const redmineUrl = (config, path, params = {}) => { + const url = new URL(path.replace(/^\//, ''), config.url); + Object.entries(params).forEach(([key, value]) => { + if (value !== undefined && value !== null && value !== '') { + url.searchParams.set(key, value); + } + }); + return url; +}; + +const fetchJson = async (config, path, options = {}, params = {}) => { + const url = redmineUrl(config, path, params); + const res = await fetch(url, { + headers: { + 'X-Redmine-API-Key': config.apiKey, + 'Content-Type': 'application/json', + }, + ...options, + }); + + if (!res.ok) { + const text = await res.text(); + throw new Error(`Redmine request failed (${res.status}): ${text}`); + } + + if (res.status === 204) { + return {}; + } + + return res.json(); +}; + +const fetchAllOpenIssues = async (config) => { + const issues = []; + const limit = 100; + let offset = 0; + let total = null; + + while (total === null || offset < total) { + const data = await fetchJson( + config, + '/issues.json', + {}, + { + project_id: config.projectId, + status_id: 'open', + limit, + offset, + sort: 'updated_on:desc', + }, + ); + if (Array.isArray(data.issues)) { + issues.push(...data.issues); + } + total = data.total_count ?? issues.length; + offset += limit; + if (!data.issues || data.issues.length === 0) { + break; + } + } + + return issues; +}; + +const findExistingIssue = async (config, fingerprint) => { + if (!fingerprint) return null; + const issues = await fetchAllOpenIssues(config); + return issues.find( + (issue) => + issue.subject?.includes(fingerprint) || + issue.description?.includes(`Fingerprint: ${fingerprint}`), + ); +}; + +const computeFingerprint = (seed) => { + const hash = crypto.createHash('sha1').update(seed).digest('hex'); + return hash.slice(0, 12); +}; + +const readFailures = (opts) => { + const failures = []; + if (opts['failures-file']) { + try { + const text = fs.readFileSync(opts['failures-file'], 'utf8'); + text + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean) + .forEach((line) => failures.push(line)); + } catch (err) { + console.error(`Could not read failures file: ${err.message}`); + } + } + + if (opts.failures) { + failures.push(opts.failures); + } + + if (failures.length === 0) { + failures.push('Test failure (no details provided)'); + } + + return failures; +}; + +const createIssue = async (config, payload) => { + const body = { + issue: { + project_id: config.projectId, + tracker_id: payload.trackerId, + subject: payload.subject, + description: payload.description, + }, + }; + + if (config.assigneeId) { + body.issue.assigned_to_id = config.assigneeId; + } + + const res = await fetchJson(config, '/issues.json', { + method: 'POST', + body: JSON.stringify(body), + }); + return res.issue || res; +}; + +const handleCreateTestIssue = async () => { + const config = readConfig(); + const suite = args.suite || 'tests'; + const tracker = (args.tracker || 'bug').toLowerCase(); + const trackerId = + tracker === 'security' + ? config.trackerSecurityId || config.trackerBugId + : config.trackerBugId; + + if (!trackerId) { + throw new Error('Missing tracker id. Set REDMINE_TRACKER_BUG_ID (and optionally REDMINE_TRACKER_SECURITY_ID).'); + } + + const failCount = Number(args['fail-count'] || args.failcount || 0); + const failures = readFailures(args); + const fingerprintSeed = + args['fingerprint-seed'] || + `${suite}|${args.target || ''}|${failCount}|${failures.join('|')}`; + const fingerprint = args.fingerprint || computeFingerprint(fingerprintSeed); + const subject = `[${suite}] ${failCount || failures.length} failure${failCount === 1 ? '' : 's'} (${tracker}) [${fingerprint}]`; + + const descriptionLines = [ + `Suite: ${suite}`, + args.run ? `Run: ${args.run}` : null, + args.target ? `Target: ${args.target}` : null, + `Tracker: ${tracker}`, + `Failures (${failures.length}):`, + ...failures.map((line) => `- ${line}`), + args['summary-file'] ? `Summary: ${args['summary-file']}` : null, + args['failures-file'] ? `Log: ${args['failures-file']}` : null, + `Fingerprint: ${fingerprint}`, + ].filter(Boolean); + + const existing = await findExistingIssue(config, fingerprint); + if (existing) { + console.log( + `Open issue #${existing.id} already exists for fingerprint ${fingerprint}; not creating a duplicate.`, + ); + return; + } + + const created = await createIssue(config, { + trackerId, + subject, + description: descriptionLines.join('\n'), + }); + + console.log(`Created Redmine issue #${created.id}: ${subject}`); +}; + +const handleListOpen = async () => { + const config = readConfig(); + const issues = await fetchAllOpenIssues(config); + if (!issues.length) { + console.log('No open issues found for the configured project.'); + return; + } + + const groups = issues.reduce((acc, issue) => { + const tracker = issue.tracker?.name || 'Unknown'; + acc[tracker] = acc[tracker] || []; + acc[tracker].push(issue); + return acc; + }, {}); + + Object.entries(groups).forEach(([trackerName, trackerIssues]) => { + console.log(`${trackerName} (${trackerIssues.length})`); + trackerIssues.forEach((issue) => { + const status = issue.status?.name ? ` [${issue.status.name}]` : ''; + const priority = issue.priority?.name ? ` (${issue.priority.name})` : ''; + console.log(`- #${issue.id}${status}${priority}: ${issue.subject}`); + }); + console.log(''); + }); +}; + +const printHelp = () => { + console.log(`Usage: + redmine-report.js create-test-issue --suite --run --fail-count --failures-file [--target ] [--tracker bug|security] [--fingerprint ] + redmine-report.js list-open + +Env: + REDMINE_URL Base URL, e.g. https://redmine.example.com + REDMINE_API_KEY API key + REDMINE_PROJECT_ID Project to file against + REDMINE_TRACKER_BUG_ID Tracker id for bugs + REDMINE_TRACKER_SECURITY_ID Tracker id for security issues (optional) + REDMINE_ASSIGNEE_ID Default assignee (optional) +`); +}; + +const main = async () => { + try { + switch (command) { + case 'create-test-issue': + await handleCreateTestIssue(); + break; + case 'list-open': + await handleListOpen(); + break; + case '-h': + case '--help': + case undefined: + printHelp(); + process.exit(command ? 0 : 1); + break; + default: + console.error(`Unknown command: ${command}`); + printHelp(); + process.exit(1); + } + } catch (err) { + console.error(`Redmine command failed: ${err.message}`); + process.exit(1); + } +}; + +main(); diff --git a/scripts/run-test-suite.sh b/scripts/run-test-suite.sh index feb7779..51db518 100755 --- a/scripts/run-test-suite.sh +++ b/scripts/run-test-suite.sh @@ -97,6 +97,49 @@ ${items:-"
  • No runs found.
  • "} EOF } +notify_redmine() { + local fail_lines=() + local failures_file="$RUN_DIR/failures.txt" + + for row in "${SUMMARY_TEXT_ROWS[@]}"; do + if [[ "$row" == *"FAIL"* ]]; then + fail_lines+=("$row") + fi + done + + [ "${#fail_lines[@]}" -gt 0 ] || return + + printf "%s\n" "${fail_lines[@]}" >"$failures_file" + + local tracker="bug" + for row in "${fail_lines[@]}"; do + case "$row" in + npm\ audit:*) tracker="security" ;; + Trivy*) tracker="security" ;; + OWASP\ ZAP\ baseline*) tracker="security" ;; + esac + done + + if command -v node >/dev/null 2>&1 && [ -x "${BASH_SOURCE%/*}/redmine-report.js" ]; then + log "Reporting failures to Redmine (${tracker})..." + if node "${BASH_SOURCE%/*}/redmine-report.js" create-test-issue \ + --suite "run-test-suite" \ + --run "$RUN_TS" \ + --fail-count "$FAIL_COUNT" \ + --failures-file "$failures_file" \ + --summary-file "$SUMMARY_FILE" \ + --target "$TARGET" \ + --tracker "$tracker" \ + --fingerprint-seed "${TARGET}|${fail_lines[*]}"; then + log "Redmine notification complete." + else + log "Redmine notification failed or skipped (see output above)." + fi + else + log "Redmine reporter not available; skipping Redmine notification." + fi +} + # 1) npm audit if command -v npm >/dev/null 2>&1; then log "Running npm audit (high)..." @@ -192,8 +235,8 @@ cat >"$SUMMARY_FILE" <CheckStatusDetails ${SUMMARY_ROWS[*]} - - + + EOF @@ -207,6 +250,10 @@ FAIL_COUNT=${FAIL_COUNT} SKIP_COUNT=${SKIP_COUNT} EOF +if [ "$FAIL_COUNT" -gt 0 ]; then + notify_redmine +fi + update_index log "Summary:" diff --git a/scripts/run-tests-with-redmine.sh b/scripts/run-tests-with-redmine.sh new file mode 100755 index 0000000..807e053 --- /dev/null +++ b/scripts/run-tests-with-redmine.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Wraps any test command and files a Redmine ticket on failure. +# Usage: ./scripts/run-tests-with-redmine.sh + +if [ "$#" -eq 0 ]; then + echo "Usage: $0 " + exit 1 +fi + +RUN_TS=$(date +"%Y%m%d-%H%M%S") +RUN_DIR="reports/runs/manual-${RUN_TS}" +mkdir -p "$RUN_DIR" + +CMD=("$@") +NAME="${TEST_NAME:-${CMD[*]}}" +LOG_FILE="$RUN_DIR/command.log" + +echo "[run-with-redmine] Running ${CMD[*]}..." +set +e +"${CMD[@]}" >"$LOG_FILE" 2>&1 +STATUS=$? +set -e + +if [ "$STATUS" -eq 0 ]; then + echo "[run-with-redmine] Command succeeded. Log: $LOG_FILE" + exit 0 +fi + +echo "[run-with-redmine] Command failed with status ${STATUS}. Log: $LOG_FILE" + +TAIL_SNIPPET=$(tail -n 40 "$LOG_FILE" 2>/dev/null || true) +FAILURE_FILE="$RUN_DIR/failures.txt" +{ + echo "Command: ${CMD[*]}" + echo "Exit status: ${STATUS}" + echo "--- tail ---" + echo "${TAIL_SNIPPET}" +} >"$FAILURE_FILE" + +if command -v node >/dev/null 2>&1 && [ -x "${BASH_SOURCE%/*}/redmine-report.js" ]; then + if node "${BASH_SOURCE%/*}/redmine-report.js" create-test-issue \ + --suite "${NAME}" \ + --run "$RUN_TS" \ + --fail-count 1 \ + --failures-file "$FAILURE_FILE" \ + --summary-file "$LOG_FILE" \ + --tracker "${TRACKER:-bug}" \ + --fingerprint-seed "${NAME}|${CMD[*]}|${STATUS}|${TAIL_SNIPPET}"; then + echo "[run-with-redmine] Redmine notification complete." + else + echo "[run-with-redmine] Redmine notification failed or skipped." + fi +else + echo "[run-with-redmine] Redmine reporter not available; skipping Redmine notification." +fi + +exit "$STATUS"