Merge branch 'feature/kubeconfig-encrypted'
Some checks are pending
CI / checks (push) Waiting to run

This fixes the new developer issue with kubeconfig not being se automatically.
This commit is contained in:
Tero Halla-aho 2025-12-15 20:31:27 +02:00
commit 8048fc6138
8 changed files with 535 additions and 42 deletions

View file

@ -36,6 +36,12 @@ JOKER_DYNDNS_USERNAME=
JOKER_DYNDNS_PASSWORD= JOKER_DYNDNS_PASSWORD=
REGISTRY_USERNAME= REGISTRY_USERNAME=
REGISTRY_PASSWORD= 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 bootstrap (used by seed/reset scripts)
ADMIN_EMAIL= ADMIN_EMAIL=

View file

@ -1,44 +1,50 @@
# Encrypted with sops (age). To edit: sops creds/secrets.enc.env # 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] AUTH_SECRET=ENC[AES256_GCM,data:70URX0qSS/z4AYb58kAmGA/gnEt9bBGPPN5L/xpg3vsna8A9jw+X,iv:iKicMbvIaBvPXncKcmJwsbjQzjxOa7xmkm8SXeZ8RUQ=,tag:2CXRV4I5YVz52aFm65WGhQ==,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] 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:qlmMVJEkDwfiRYGFUdP/,iv:syUEM2Cx64jE7IW8zRSqfe9uAL+JEMl9Tu5nw12kdWI=,tag:VoAJgXgGy8OXksUvn/SpUQ==,type:str] DB_HOST=ENC[AES256_GCM,data:Jvk5Zes8XH50096YHApn,iv:GDU3tu24u8lwuMLQbrv1chNLspzW8oIaPgS9ZgiZkro=,tag:vmTdIZL2E7CRkb6TYxr5eg==,type:str]
DB_PORT=ENC[AES256_GCM,data:vLjEIHpK,iv:ym9R0E/S5WKLKF+0thz9tmwI4pYa7skrY4sDpY42y0E=,tag:SK8gDGrnx8/YAPi8qB7KeQ==,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:KIlxtjxjx0EIDxXxHKY=,iv:dlhX4pIe6AE8fzT+TVB3hg9gJ46sq85Qp8f4pkxO9n8=,tag:WCMaOPpnI8SA8WMRDA392w==,type:str] DB_USER=ENC[AES256_GCM,data:K+rYqv9CQpV5p8zDXV0=,iv:TiGMbFAiyVPtbe0JmXxsilAYv4WmgTdPu4muCgSmSgY=,tag:4/kA2T3w7BUytcW0CVyFCg==,type:str]
DB_PASS=ENC[AES256_GCM,data:CJEh2+yQrPptlTnthe2UWsc/baNXBIAC0lpxtm8DI47gTg==,iv:UPrDDlMbWaiAiz5QcjRqXRIiVohMRF7wtmi5ioONcLE=,tag:jLSfXcefy8+cll8bVclkSQ==,type:str] DB_PASS=ENC[AES256_GCM,data:w49QEtuN2mFthnn4X/DvoxwbhImYVqDUYG1Ptq4ba22P9w==,iv:cSOmskpZAi/aYpzj3+QBmVIQZfEs2itDqaMZA9eX8v8=,tag:NJrdaN6zIRE0DBpVTzDL7g==,type:str]
APP_URL=ENC[AES256_GCM,data:1vZsUQ/d4MTbBM+juPJeVwseAVz4O2M=,iv:KrsOr3w1i3j3R19KAowFxFbdSm1B9IikqRoqU9tmQus=,tag:BwR3v/2R7bAFxHnB5waFIA==,type:str] APP_URL=ENC[AES256_GCM,data:xr9j3Gt/y5Uq8bHzTyTKsYtpSRg4GKU=,iv:oV68MYwwipB5tLpXXDFln2YF4hPHWiufyRm3csamssg=,tag:Bj1K3tC3LyP8ezNJG8bpIA==,type:str]
SMTP_HOST=ENC[AES256_GCM,data:H+47VDofg1EEBo6q1UEleQ==,iv:KlAxFj/bKOMikubGbqPYkKPZt18oIdXxrLjEVnbxhN8=,tag:LpC930G7lsjt9YuSEFxW8A==,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:F6fiIz4=,iv:Pelfn6ciIGvqF4nMPX5WrsWqkLtAxPBmBOSBvn4RUmo=,tag:UWBGXnDAYf5zCQJagiSE9Q==,type:str] SMTP_PORT=ENC[AES256_GCM,data:6r6Zsqg=,iv:eNzU1Dk3vx7kwJu72tMZDx4r9ngHUKx1t1zAYSyFFb4=,tag:aqryLBX9xPM6DvGd5Fwl5g==,type:str]
SMTP_USER=ENC[AES256_GCM,data:CnbiHSRC0w/TOwwUwto=,iv:s8tJxs3X3BaFf5LrR+hHVvRIbDEfNI6ZszdTi+lg3PQ=,tag:DAbkk3szBzmhUrNTPZdA6A==,type:str] SMTP_USER=ENC[AES256_GCM,data:IVpG+wm7l0Kk8UfhrAo=,iv:GiQChcgXDzD/h8fBHXAeKWez3IXR4NX90NU9582hThg=,tag:b8jcQY2G2HAo3pQjc7+B0Q==,type:str]
SMTP_PASS=ENC[AES256_GCM,data:GmmDQ2mROFz9zpGpXgtwlUvU2dmWg9KHsw==,iv:pl7O1QCjkaaqZ0sGS6kIBUKJ5glE8FTyA1vCYz0BfoU=,tag:/eE1ZWrLlKsa043VY3p3DQ==,type:str] SMTP_PASS=ENC[AES256_GCM,data:siqtgQg2WRrRbD6yqTawLAo++u2fWvpVLg==,iv:rL5AZQ6b3PaOJhG3vGZYzXeFxm6yd+qhxNzYIN6pxQk=,tag:r0LTjC8DQ9rsLTQa4XLhAg==,type:str]
SMTP_FROM=ENC[AES256_GCM,data:XY9BFPRmpO5KblsSMB2680DNx/bvUndfXA==,iv:yCUsaOMu1MrHtfR9AwfuIscyiEqsl0OYvO8UiKO6IYk=,tag:MpEIu2UJXEjE+9n1MVaHRQ==,type:str] SMTP_FROM=ENC[AES256_GCM,data:WH8AW4SVhZkeXB6cNZCfj7opSTwoqYm8TA==,iv:Hy55AgFV0VSurplLVuPPqtLcyDQr/IIvGHF6Ppl01t0=,tag:vKtXoZRFgSeMYqSL9enElQ==,type:str]
SMTP_TLS=ENC[AES256_GCM,data:sBPcDhWs,iv:lZRjMBtrtAinC1blRFE0RH5jzyEcdLf+UOgEHuKitEk=,tag:R3Q4+ri3m5LB+xzohsYJHA==,type:str] SMTP_TLS=ENC[AES256_GCM,data:42eaSf82,iv:sMLqYMHN5hzu9wrjmH9DhHcSPK06f0ZGqv/HPMsMPo8=,tag:rxoUQyT2jEC2VjRJMJV4Sw==,type:str]
SMTP_SSL=ENC[AES256_GCM,data:dYAo31bHTw==,iv:OG0c0HYzewLwlsgPlyn0nz+vwdeuSCjJ5ifdE9bfQvI=,tag:EJ5CX6+weXcrcjdrr1uicg==,type:str] SMTP_SSL=ENC[AES256_GCM,data:Am3rKylCqQ==,iv:P6nQdWsV1QkBYfg2oeR3JbEsJYNGvd0h/ZFza2PObY8=,tag:z5VAAD4IjNi7hMISgjsisQ==,type:str]
SMTP_REJECT_UNAUTHORIZED=ENC[AES256_GCM,data:HhBgopyX,iv:7W9TYVN1aEH5QWK1NXGE9mw9gCd7yagg/LFVNHSdMV4=,tag:rcWGdoXH2wXtqWaImLEfHA==,type:str] SMTP_REJECT_UNAUTHORIZED=ENC[AES256_GCM,data:1wLP1f9a,iv:exUSGxwA7CZ6IuBTBuAikerCe+algCybvhW6xji6h2U=,tag:Ehf8ok6P5Bx2Ky6kzOplIQ==,type:str]
DKIM_SELECTOR=ENC[AES256_GCM,data:taqEDa6Ah1G6yQ==,iv:n4KfE3lFBIyndBFkgvoBvzAlqP2HV4RIyL8c7lLoQSU=,tag:nos80rdqlj7YQshANBoTvw==,type:str] DKIM_SELECTOR=ENC[AES256_GCM,data:gv0POpTZG/69Cw==,iv:eBeiuZPNWo6DPQ25Uupo8+1EacBGOx3FUOR9arhELV8=,tag:/6gICHQGYlO4KLeu/r0VcA==,type:str]
DKIM_DOMAIN=ENC[AES256_GCM,data:ZQkoRT8bQ7/Kdt0O/M4kRB8=,iv:wSbk2ALpbcxRT8neFrcQBEj8ja54ZtWeKTfllm8Pr5s=,tag:o5ZvS1enT31pV4+EM8i5LA==,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:8hbyhw/+I1VWGzoDNIKMY3avRvSmt17EmHiwv7WpeJZzalEXqTmDB+bpkVEQ,iv:/7bUU6boXo1HPccvNBCDnLyql0hGaMXmGIeQEqpFnwM=,tag:nTE+dR0JB/Y1ri5h/FaC3A==,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:bAHiVYhjXw==,iv:/jKFQ5L+meSNF6tn54Dc8dpK1I0+zG/o/F0QqMInmwU=,tag:9f7mb2QN17AoualTjkLeSQ==,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:+Avly5xD/jgkeiiHg7WNVH/OyGRf9L+iKDh/SvD7LrFuJlltORwzY253l29vD6cucaYfFanTWwDaV6TKuatNiRXxzy7aSoOfs0y4qonXofIRb3BH2/DarGWxQtRi5LRrXFIpCRQfN7x+XGbeF8ZZBW/DHFYywH0E2fU9li/iafXVOKBRcV4WYTIA3sJJuObHFjxgRYMB2SWtw8noZvkK+pzQi6lCKA==,iv:WTbvAzkjoDLAuDIdbFjt9BU5+2pGDv5Ksc4xru0Wx20=,tag:b8ddMgRoq+kMlS994yPx9A==,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:QGIhIhzn775yXZq9OYKTwGwcLnQAIJn8TM95TRFb5jWY/gTevspeYLDVrrGDKv/WHX8aTg4r4z9t6cbgbSX677fSlrNy2c2YEb5uCLGh0QUK2LoPt9R9yF/iyi5CU2obRlqamtF6knUGG5mdOCewKx/k+updDLmOe+CPD91H6nWbE9+1Pmbdf22bM+rOt9t4+MS6UP0UbIYU0sBEtfPTvN68260fUA==,iv:eYzIudN/i0A38X1up+Jnxv7qE5aiUEu0WQHEtQ4DvlA=,tag:qM7noB1GvXt2F69aVd0KVQ==,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" HETZNER_API_TOKEN="hoRULGviS8G3OGaJ68josx00M53efhuntVM5Rfft1AOvUR0ZQTXlO6yivhGqBM5o"
HCLOUD_TOKEN=ENC[AES256_GCM,data:0qm+oR2daCd9e0qc+2wApobRod0RsUeA2QL1kQarQW77FvPAgQ7jBkML39kBP3xsYsrdmiZuIdg63Q3xeBQ9mVq2,iv:JUjR7vGyX00TOlUCOD8M9jiB6iio4IiX73sLzW/CADA=,tag:9p0kgDD7ul8s5T0cL7vsfA==,type:str] HCLOUD_TOKEN=ENC[AES256_GCM,data:SZMlHgX2keD98hk2VU5G3aGdla3MsaSR4BqnCM4F9fNp3DNdx9zQyiRJ8LDBi62HUPBBubj1YSnUPnJFJ5bXtv6A,iv:mVor150MZPe7jMHEBtcXcoT434bAX1t7WSTLOG/hexY=,tag:DKWHOSg61vG/7qKsMDz+uw==,type:str]
HETZNER_TOKEN=ENC[AES256_GCM,data:e95QAx49/qkoBnAmyup7NBs/Xm/fkWutMIWzlAv4P1ybSJNoCyC4ZibpNTCtYtK/3Hux5ItdOBm/jhssjlX363cI,iv:4UzDqJd0mYP3IJgeMn9xz9UjsVyODZ44CDAz5PPaXbk=,tag:ZEAqueoXY4kBfe+89nA+Kg==,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:FBAh4B+vxosvxfHbrZukSNea,iv:4FexfnCieAaZ7iUSvaiATr5THpqoHaebX/QigA9Wl58=,tag:dY6lQnHTJWOZa7WhHwPELA==,type:str] JOKER_DYNDNS_USERNAME=ENC[AES256_GCM,data:jQEKhX2ISB9FlwMnnrp7oZ4D,iv:9nnUG5nD7DzPAAYiVDNd4C64YTgF9BSnq0+/WajJPVA=,tag:HLYprQoKRkTtaB82mPLnbQ==,type:str]
JOKER_DYNDNS_PASSWORD=ENC[AES256_GCM,data:1neBKFvQhFGfOgdPI6EHZKKI,iv:fC1J7QjAoQMUKxx6lMO32n6zjQ5tQa/icBDn19Lih2k=,tag:W110zqgwbsrVbkZzav/F0Q==,type:str] JOKER_DYNDNS_PASSWORD=ENC[AES256_GCM,data:LUHbGwOLq+A3CsjYJ4iu9vCn,iv:Phl3sv3hCe8wRWX3TqlyeBsqhKiu2bvJF8M6f1ESnHY=,tag:lmQIynYBEl7jVg3pG/TOAg==,type:str]
REGISTRY_USERNAME=ENC[AES256_GCM,data:Mt1mk15DPH8=,iv:Hxg4fRLRZ2hKRACTIIUMSODRBOgRv5Y6KfZ8477cwc0=,tag:Ufq2jYQjYzjiB88PF0gpBA==,type:str] REGISTRY_USERNAME=ENC[AES256_GCM,data:IJKWI15AROA=,iv:crKgYBH39sKioPVNw69fv9H5NCGadc6X9DebqUIyC4M=,tag:7PBRGErLYdLi+ku/+U+9UA==,type:str]
REGISTRY_PASSWORD=ENC[AES256_GCM,data:4l2oMR65+X+1,iv:cfh6Esayb6zJEqKrtfY0LAncIrPi7lj/ckcbMgTMFys=,tag:vhsiagOyGVjN7+GNPwZ8LA==,type:str] REGISTRY_PASSWORD=ENC[AES256_GCM,data:viuSD5MpNuGc,iv:/2Rcuv2cecP8iECeUfK/NowP3Fv2p+EPyqkCJoLQZnw=,tag:BApcmBr3jR+NoQ3ZqRaI8w==,type:str]
NETDATA_USER=ENC[AES256_GCM,data:6laH3H7JKgpU,iv:PCI8HL6S68AaBRg11YlNbK9R12G1ROMqRIpXKIlsBDQ=,tag:qL1uWrtWz5+c6f6Xbv4Low==,type:str] NETDATA_USER=ENC[AES256_GCM,data:T51yECIfawfQ,iv:9q/drpZy7C6KzscGB+Lxgu0Rsg4Ov9qgvmUt2lfYfAE=,tag:XS1rN2wy3OAizpA6Ji5FhQ==,type:str]
NETDATA_PASS=ENC[AES256_GCM,data:CZxY7WeoHuUNmECOiL6XLVeTt/k5PAHFyV4dVBrF,iv:Zp00OVvA5hI8c9AD4pvDB1s99yw9iOlgBV4K4nbFFVg=,tag:Y077Bp8qk/SoEOajOdvMLg==,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:QEHB348q+BQubmvIJkH/8mie2fjb0mE=,iv:ZUNZA64cuflqQq+JS/e0wByTzny0eE/4rvaxzNN1JFw=,tag:YfmJWJCd1MQW2Q2bWferLg==,type:str] NETDATA_HOST_NODE1=ENC[AES256_GCM,data:9k1sqAJV2bLEStTC2L8KZ9OryFwLhDw=,iv:VWKvsEjOXBtfYHpiNG9wBX4HqG8i3XGW9wK6uOM0qbU=,tag:qKqYvnC0InnK+f3c7O3iTg==,type:str]
NETDATA_HOST_DB1=ENC[AES256_GCM,data:B8mSqI+rusKxazFcziGabYMM1i1q,iv:1hDrheKjdo7dMF1m7qOWQ9llye2glEQm79LONauLiYk=,tag:H1BVhIKfymLBzGS6CZEp3Q==,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:n0G+MZWEWQK9,iv:11id08TbMpc5NUqtKsoXiIFfM5GJCFStzJ5aY1AGplc=,tag:gXWcgEXko4P7qMwVLGnF1Q==,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:ylpbhrcFFKC5qiyt+RRPZhCWt098tPhGWxabgsO31tR7+g==,iv:O4b5DsNfCpqlhZ3GmXMRzp2/fUpMtLnhSqmoaD4+Q04=,tag:TG7ge49hjzkWcBO93hoXuA==,type:str] NETDATA_PG_PASS=ENC[AES256_GCM,data:NEENSnAkLmBY7LxTzoqjNtG9YCFAdvF2yOj2wkzBvKbZew==,iv:7ZJNwLldd3i0BhO3uujzflhR01qDT9gDcqVtsUOXubg=,tag:rukJnDEItfa4csPxN3tvPg==,type:str]
NETDATA_PG_ROLE=ENC[AES256_GCM,data:6/ESugRIiDgBM2e8,iv:kXJOKx7fbO4k9zV2APTXCxOrGJ/93dCkmVA33v9fzrY=,tag:HwEWzVagm6cDnPychaekaw==,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:AuhvA1iUOnMcE7el4TSE7cbLAg==,iv:eWNxWDA978OWQO10/ywc8CpKuUhC0uTqObmD5Tefgtc=,tag:pI4SXW1ZrZPthD2qRAPrIw==,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:WiJm5VO57/qLu8fK0rZqFDk=,iv:2SwLMvMUjwkEDZo8VRtPHfs+cabz2L+9ZQAaWyn1o9c=,tag:+TunF3tuLxYvMeJsYNQuCQ==,type:str] ADMIN_INITIAL_PASSWORD=ENC[AES256_GCM,data:VXiQWltYpxkts/xLNA4p8F8=,iv:as8Uq4S05e8peTP9phv5otdbyb7uyQe5gJ/ig7VKC2E=,tag:gsXXzVtft6AQBjQgrrhfng==,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 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_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_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_lastmodified=2025-12-14T12:06:30Z
sops_mac=ENC[AES256_GCM,data:bnDwxj/t2X+vkq1nd2Bej23GBn3hALXW6PAp4FyoAlvwajztp9U2eyF7voLQDeX1kurVBuACPExzzMnerEXOebF9l5SGcIYfvtVj9kk4I0WRCbVBt/QKgEtqYJ3l1TXrDe8ZPTj6O2rK6WW36RDExFDu3tzzvVEaHErZMjAhD1U=,iv:hBwrHOvabZEqeWHSFGvk5sHYbJiF6/3wY1JXgaevB9w=,tag:2fZQ9AQoHQJ+zte2yLpi5g==,type:str] sops_mac=ENC[AES256_GCM,data:R06Pa93hx3HOijkBKdgm+MgBMVgkAoMS2wzZe55E1b5TlHLfTaa8DuqBNHkor/bPsJW584ByO0AA3DiLkeCLgo9yMulYhuizRIpzKsOI2FH5KcrTGOxj1h5Wxno0ToRoXNtgy71s2aDf7hmzLQhGetAOsmEl+HFy1DMs2dauU5Q=,iv:jh4OLLqTG+eBac6MmwR0CXN4irX+QZZbeTUcrQUOyQ8=,tag:ViE3djLoGcrbqMJgD9r3SQ==,type:str]
sops_version=3.11.0 sops_version=3.11.0

View file

@ -19,6 +19,7 @@
<li><a href="./architecture.html">Logical Architecture</a></li> <li><a href="./architecture.html">Logical Architecture</a></li>
<li><a href="./sequences.html">Feature Sequences</a></li> <li><a href="./sequences.html">Feature Sequences</a></li>
<li><a href="./security.html">Security Testing</a></li> <li><a href="./security.html">Security Testing</a></li>
<li><a href="./redmine.html">Redmine Integration</a></li>
</ul> </ul>
</section> </section>
<section class="card"> <section class="card">

50
docs/redmine.html Normal file
View file

@ -0,0 +1,50 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Redmine Integration</title>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<header>
<h1>Redmine Integration</h1>
<div class="meta">File tickets automatically when tests fail and review open work by tracker.</div>
</header>
<main class="grid">
<section class="card">
<h2>Setup</h2>
<ul>
<li>Env vars (see <code>.env.example</code>): <code>REDMINE_URL</code>, <code>REDMINE_API_KEY</code>, <code>REDMINE_PROJECT_ID</code>, <code>REDMINE_TRACKER_BUG_ID</code>, <code>REDMINE_TRACKER_SECURITY_ID</code> (optional, falls back to bug), <code>REDMINE_ASSIGNEE_ID</code> (optional default owner).</li>
<li>Uses the Redmine REST API with the API key for authentication.</li>
<li>Ensure the API key is scoped to the project and can create issues.</li>
</ul>
</section>
<section class="card">
<h2>Automatic tickets on failures</h2>
<ul>
<li><code>scripts/run-test-suite.sh</code> now files a Redmine issue whenever any check fails. Security-related failures (npm audit, Trivy, ZAP) use the security tracker when configured.</li>
<li>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.</li>
<li>Manual wrapper for other test commands: <code>./scripts/run-tests-with-redmine.sh npm test</code> (set <code>TEST_NAME</code> or <code>TRACKER</code> to override labels).</li>
</ul>
</section>
<section class="card">
<h2>CLI tools</h2>
<ul>
<li>List open tickets grouped by tracker: <code>REDMINE_URL=... REDMINE_API_KEY=... REDMINE_PROJECT_ID=... ./scripts/redmine-report.js list-open</code></li>
<li>Manual issue creation from a log file: <code>./scripts/redmine-report.js create-test-issue --suite my-tests --failures-file /path/to/log --tracker bug</code></li>
<li>Outputs go to stdout; non-zero exit code on API errors so CI can fail fast.</li>
</ul>
</section>
<section class="card">
<h2>Notes</h2>
<ul>
<li>Tickets include a short fingerprint in the subject and description; keep it when editing so future runs keep de-duplicating.</li>
<li>Summary/log paths are included in the issue body to help locate artifacts from the run.</li>
</ul>
</section>
</main>
</body>
</html>

View file

@ -7,6 +7,8 @@ set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SECRETS_FILE="${SECRETS_FILE:-$ROOT_DIR/creds/secrets.env}" SECRETS_FILE="${SECRETS_FILE:-$ROOT_DIR/creds/secrets.env}"
ENCRYPTED_FILE="${ENCRYPTED_FILE:-$ROOT_DIR/creds/secrets.enc.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() { ensure_decrypted() {
if [[ -f "$SECRETS_FILE" ]]; then if [[ -f "$SECRETS_FILE" ]]; then
@ -24,8 +26,33 @@ ensure_decrypted() {
} }
ensure_decrypted || exit 0 ensure_decrypted || exit 0
echo "Loading secrets from $SECRETS_FILE" echo "Loading secrets from $SECRETS_FILE"
set -a set -a
source "$SECRETS_FILE" source "$SECRETS_FILE"
set +a 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

297
scripts/redmine-report.js Executable file
View file

@ -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 <name> --run <id> --fail-count <n> --failures-file <path> [--target <url>] [--tracker bug|security] [--fingerprint <id>]
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();

View file

@ -97,6 +97,49 @@ ${items:-" <li>No runs found.</li>"}
EOF 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 # 1) npm audit
if command -v npm >/dev/null 2>&1; then if command -v npm >/dev/null 2>&1; then
log "Running npm audit (high)..." log "Running npm audit (high)..."
@ -192,8 +235,8 @@ cat >"$SUMMARY_FILE" <<EOF
<thead><tr><th>Check</th><th>Status</th><th>Details</th></tr></thead> <thead><tr><th>Check</th><th>Status</th><th>Details</th></tr></thead>
<tbody> <tbody>
${SUMMARY_ROWS[*]} ${SUMMARY_ROWS[*]}
</tbody> </tbody>
</table> </table>
</body> </body>
</html> </html>
EOF EOF
@ -207,6 +250,10 @@ FAIL_COUNT=${FAIL_COUNT}
SKIP_COUNT=${SKIP_COUNT} SKIP_COUNT=${SKIP_COUNT}
EOF EOF
if [ "$FAIL_COUNT" -gt 0 ]; then
notify_redmine
fi
update_index update_index
log "Summary:" log "Summary:"

View file

@ -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 <command ...>
if [ "$#" -eq 0 ]; then
echo "Usage: $0 <command ...>"
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"