quoll-cli 0.2.0__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/PKG-INFO +31 -6
  2. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/README.md +30 -5
  3. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/pyproject.toml +3 -1
  4. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/cd/config.py +7 -0
  5. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/ci/custom.py +4 -2
  6. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/commands/_common.py +19 -11
  7. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/commands/add.py +6 -3
  8. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/commands/cd.py +66 -8
  9. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/commands/new.py +60 -21
  10. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/config/store.py +8 -6
  11. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/base.py +9 -3
  12. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/go/__init__.py +2 -0
  13. quoll_cli-0.3.0/quoll/languages/go/starter.py +29 -0
  14. quoll_cli-0.3.0/quoll/languages/go/templates/http/README.md +9 -0
  15. quoll_cli-0.3.0/quoll/languages/go/templates/http/gitignore +5 -0
  16. quoll_cli-0.3.0/quoll/languages/go/templates/http/go.mod.tmpl +3 -0
  17. quoll_cli-0.3.0/quoll/languages/go/templates/http/main.go +84 -0
  18. quoll_cli-0.3.0/quoll/languages/go/templates/http/main_test.go +102 -0
  19. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/node/Dockerfile.j2 +1 -1
  20. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/node/__init__.py +2 -1
  21. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/node/ci.py +14 -2
  22. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/node/detect.py +3 -3
  23. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/node/docker.py +15 -1
  24. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/node/frameworks/angular/starter.py +10 -13
  25. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/node/frameworks/nextjs/__init__.py +4 -1
  26. quoll_cli-0.3.0/quoll/languages/node/frameworks/nextjs/starter.py +110 -0
  27. quoll_cli-0.3.0/quoll/languages/node/react.py +73 -0
  28. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/node/starter.py +4 -3
  29. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/php/ci.py +11 -1
  30. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/python/ci.py +1 -1
  31. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/python/frameworks/fastapi/__init__.py +4 -1
  32. quoll_cli-0.3.0/quoll/languages/python/frameworks/fastapi/starter.py +41 -0
  33. quoll_cli-0.3.0/quoll/languages/python/frameworks/fastapi/template/README.md +10 -0
  34. quoll_cli-0.3.0/quoll/languages/python/frameworks/fastapi/template/app/main.py +13 -0
  35. quoll_cli-0.3.0/quoll/languages/python/frameworks/fastapi/template/env.example +1 -0
  36. quoll_cli-0.3.0/quoll/languages/python/frameworks/fastapi/template/gitignore +7 -0
  37. quoll_cli-0.3.0/quoll/languages/python/frameworks/fastapi/template/pyproject +16 -0
  38. quoll_cli-0.3.0/quoll/languages/python/frameworks/fastapi/template/python-version +1 -0
  39. quoll_cli-0.3.0/quoll/languages/python/frameworks/fastapi/template/tests/test_main.py +15 -0
  40. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/platforms/base.py +8 -0
  41. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/platforms/github/actions.py +2 -0
  42. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/platforms/github/api.py +29 -1
  43. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/platforms/github/cd.py +11 -1
  44. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/platforms/github/ci.py +18 -0
  45. quoll_cli-0.3.0/quoll/platforms/github/release.py +141 -0
  46. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/starters.py +20 -4
  47. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/tests/conftest.py +9 -0
  48. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/tests/docker/test_build.py +7 -0
  49. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/tests/docker/test_images.py +18 -1
  50. quoll_cli-0.3.0/tests/languages/__init__.py +0 -0
  51. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/tests/languages/test_php.py +3 -1
  52. quoll_cli-0.3.0/tests/platforms/__init__.py +0 -0
  53. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/tests/platforms/test_api.py +17 -0
  54. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/tests/platforms/test_gitlab_ci.py +6 -3
  55. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/tests/test_cd.py +148 -0
  56. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/tests/test_ci_render.py +11 -0
  57. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/tests/test_custom.py +11 -2
  58. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/tests/test_new.py +209 -10
  59. quoll_cli-0.3.0/tests/test_store.py +30 -0
  60. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/.gitignore +0 -0
  61. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/LICENSE +0 -0
  62. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/__init__.py +0 -0
  63. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/cd/__init__.py +0 -0
  64. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/ci/__init__.py +0 -0
  65. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/ci/config.py +0 -0
  66. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/ci/detect.py +0 -0
  67. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/ci/steps.py +0 -0
  68. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/ci/tools.py +0 -0
  69. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/commands/__init__.py +0 -0
  70. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/commands/_workspace.py +0 -0
  71. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/commands/add_secret.py +0 -0
  72. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/commands/ci.py +0 -0
  73. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/commands/docker.py +0 -0
  74. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/commands/k8s.py +0 -0
  75. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/commands/remove.py +0 -0
  76. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/config/__init__.py +0 -0
  77. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/config/quoll_toml.py +0 -0
  78. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/config/settings.py +0 -0
  79. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/console.py +0 -0
  80. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/docker/__init__.py +0 -0
  81. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/docker/config.py +0 -0
  82. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/docker/detect.py +0 -0
  83. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/docker/render.py +0 -0
  84. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/files.py +0 -0
  85. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/k8s/__init__.py +0 -0
  86. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/k8s/chart/templates/_helpers.tpl +0 -0
  87. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/k8s/chart/templates/deployment.yaml +0 -0
  88. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/k8s/chart/templates/hpa.yaml +0 -0
  89. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/k8s/chart/templates/ingress.yaml +0 -0
  90. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/k8s/chart/templates/pdb.yaml +0 -0
  91. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/k8s/chart/templates/service.yaml +0 -0
  92. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/k8s/chart/templates/serviceaccount.yaml +0 -0
  93. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/k8s/ci.py +0 -0
  94. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/k8s/config.py +0 -0
  95. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/k8s/deploy.py +0 -0
  96. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/k8s/detect.py +0 -0
  97. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/k8s/render.py +0 -0
  98. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/__init__.py +0 -0
  99. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/cpp/Dockerfile.j2 +0 -0
  100. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/cpp/__init__.py +0 -0
  101. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/cpp/ci.py +0 -0
  102. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/cpp/detect.py +0 -0
  103. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/cpp/docker.py +0 -0
  104. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/dotnet/Dockerfile.j2 +0 -0
  105. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/dotnet/__init__.py +0 -0
  106. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/dotnet/ci.py +0 -0
  107. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/dotnet/detect.py +0 -0
  108. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/dotnet/docker.py +0 -0
  109. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/go/Dockerfile.j2 +0 -0
  110. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/go/ci.py +0 -0
  111. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/go/detect.py +0 -0
  112. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/go/docker.py +0 -0
  113. /quoll_cli-0.2.0/tests/__init__.py → /quoll_cli-0.3.0/quoll/languages/go/templates/http/go.sum +0 -0
  114. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/java/Dockerfile.j2 +0 -0
  115. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/java/__init__.py +0 -0
  116. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/java/ci.py +0 -0
  117. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/java/detect.py +0 -0
  118. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/java/docker.py +0 -0
  119. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/java/frameworks/__init__.py +0 -0
  120. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/java/frameworks/spring_boot/Dockerfile.j2 +0 -0
  121. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/java/frameworks/spring_boot/__init__.py +0 -0
  122. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/java/frameworks/spring_boot/detect.py +0 -0
  123. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/java/frameworks/spring_boot/docker.py +0 -0
  124. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/java/frameworks/spring_boot/services.py +0 -0
  125. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/node/frameworks/__init__.py +0 -0
  126. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/node/frameworks/angular/__init__.py +0 -0
  127. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/node/frameworks/angular/ci.py +0 -0
  128. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/node/frameworks/angular/detect.py +0 -0
  129. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/node/frameworks/angular/docker.py +0 -0
  130. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/node/frameworks/astro/__init__.py +0 -0
  131. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/node/frameworks/astro/ci.py +0 -0
  132. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/node/frameworks/astro/detect.py +0 -0
  133. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/node/frameworks/astro/docker.py +0 -0
  134. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/node/frameworks/nestjs/__init__.py +0 -0
  135. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/node/frameworks/nestjs/detect.py +0 -0
  136. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/node/frameworks/nestjs/docker.py +0 -0
  137. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/node/frameworks/nextjs/Dockerfile.j2 +0 -0
  138. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/node/frameworks/nextjs/ci.py +0 -0
  139. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/node/frameworks/nextjs/detect.py +0 -0
  140. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/node/frameworks/nextjs/docker.py +0 -0
  141. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/node/frameworks/nuxt/__init__.py +0 -0
  142. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/node/frameworks/nuxt/ci.py +0 -0
  143. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/node/frameworks/nuxt/detect.py +0 -0
  144. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/node/frameworks/nuxt/docker.py +0 -0
  145. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/node/frameworks/sveltekit/__init__.py +0 -0
  146. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/node/frameworks/sveltekit/ci.py +0 -0
  147. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/node/frameworks/sveltekit/detect.py +0 -0
  148. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/node/frameworks/sveltekit/docker.py +0 -0
  149. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/node/templates/express/README.md +0 -0
  150. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/node/templates/express/env.example +0 -0
  151. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/node/templates/express/eslint.config.js +0 -0
  152. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/node/templates/express/gitignore +0 -0
  153. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/node/templates/express/nvmrc +0 -0
  154. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/node/templates/express/package.json +0 -0
  155. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/node/templates/express/prettierignore +0 -0
  156. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/node/templates/express/src/app.ts +0 -0
  157. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/node/templates/express/src/server.ts +0 -0
  158. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/node/templates/express/test/app.test.ts +0 -0
  159. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/node/templates/express/tsconfig.build.json +0 -0
  160. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/node/templates/express/tsconfig.json +0 -0
  161. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/node/templates/express/vitest.config.ts +0 -0
  162. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/php/Dockerfile.j2 +0 -0
  163. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/php/__init__.py +0 -0
  164. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/php/detect.py +0 -0
  165. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/php/docker.py +0 -0
  166. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/php/frameworks/__init__.py +0 -0
  167. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/php/frameworks/laravel/__init__.py +0 -0
  168. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/php/frameworks/laravel/ci.py +0 -0
  169. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/php/frameworks/laravel/detect.py +0 -0
  170. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/php/frameworks/laravel/docker.py +0 -0
  171. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/php/frameworks/laravel/services.py +0 -0
  172. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/php/frameworks/symfony/__init__.py +0 -0
  173. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/php/frameworks/symfony/ci.py +0 -0
  174. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/php/frameworks/symfony/detect.py +0 -0
  175. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/php/frameworks/symfony/docker.py +0 -0
  176. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/python/Dockerfile.j2 +0 -0
  177. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/python/__init__.py +0 -0
  178. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/python/detect.py +0 -0
  179. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/python/docker.py +0 -0
  180. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/python/frameworks/__init__.py +0 -0
  181. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/python/frameworks/django/__init__.py +0 -0
  182. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/python/frameworks/django/ci.py +0 -0
  183. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/python/frameworks/django/detect.py +0 -0
  184. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/python/frameworks/django/docker.py +0 -0
  185. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/python/frameworks/fastapi/detect.py +0 -0
  186. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/python/frameworks/fastapi/docker.py +0 -0
  187. {quoll_cli-0.2.0/tests/docker → quoll_cli-0.3.0/quoll/languages/python/frameworks/fastapi/template/app}/__init__.py +0 -0
  188. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/python/frameworks/flask/__init__.py +0 -0
  189. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/python/frameworks/flask/detect.py +0 -0
  190. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/python/frameworks/flask/docker.py +0 -0
  191. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/ruby/Dockerfile.j2 +0 -0
  192. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/ruby/__init__.py +0 -0
  193. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/ruby/ci.py +0 -0
  194. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/ruby/detect.py +0 -0
  195. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/ruby/docker.py +0 -0
  196. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/ruby/frameworks/__init__.py +0 -0
  197. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/ruby/frameworks/rails/Dockerfile.j2 +0 -0
  198. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/ruby/frameworks/rails/__init__.py +0 -0
  199. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/ruby/frameworks/rails/ci.py +0 -0
  200. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/ruby/frameworks/rails/detect.py +0 -0
  201. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/ruby/frameworks/rails/docker.py +0 -0
  202. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/ruby/frameworks/rails/services.py +0 -0
  203. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/rust/Dockerfile.j2 +0 -0
  204. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/rust/__init__.py +0 -0
  205. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/rust/ci.py +0 -0
  206. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/rust/detect.py +0 -0
  207. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/languages/rust/docker.py +0 -0
  208. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/main.py +0 -0
  209. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/platforms/__init__.py +0 -0
  210. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/platforms/github/__init__.py +0 -0
  211. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/platforms/github/dependabot.yml.j2 +0 -0
  212. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/platforms/github/platform.py +0 -0
  213. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/platforms/github/workflow.py +0 -0
  214. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/platforms/gitlab/__init__.py +0 -0
  215. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/platforms/gitlab/api.py +0 -0
  216. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/platforms/gitlab/cd.py +0 -0
  217. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/platforms/gitlab/ci.py +0 -0
  218. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/platforms/gitlab/pipeline.py +0 -0
  219. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/platforms/gitlab/platform.py +0 -0
  220. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/project.py +0 -0
  221. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/services.py +0 -0
  222. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/workspace.py +0 -0
  223. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/quoll/yaml_dump.py +0 -0
  224. {quoll_cli-0.2.0/tests/k8s → quoll_cli-0.3.0/tests}/__init__.py +0 -0
  225. {quoll_cli-0.2.0/tests/languages → quoll_cli-0.3.0/tests/docker}/__init__.py +0 -0
  226. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/tests/docker/test_cli.py +0 -0
  227. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/tests/helpers.py +0 -0
  228. {quoll_cli-0.2.0/tests/platforms → quoll_cli-0.3.0/tests/k8s}/__init__.py +0 -0
  229. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/tests/k8s/test_kind.py +0 -0
  230. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/tests/languages/test_contract.py +0 -0
  231. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/tests/languages/test_cpp.py +0 -0
  232. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/tests/languages/test_dotnet.py +0 -0
  233. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/tests/languages/test_go.py +0 -0
  234. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/tests/languages/test_java.py +0 -0
  235. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/tests/languages/test_node.py +0 -0
  236. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/tests/languages/test_python.py +0 -0
  237. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/tests/languages/test_registry.py +0 -0
  238. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/tests/languages/test_ruby.py +0 -0
  239. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/tests/languages/test_rust.py +0 -0
  240. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/tests/platforms/test_add_secret.py +0 -0
  241. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/tests/platforms/test_gitlab_run.py +0 -0
  242. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/tests/platforms/test_platforms.py +0 -0
  243. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/tests/samples.py +0 -0
  244. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/tests/test_ci_cli.py +0 -0
  245. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/tests/test_ci_detect.py +0 -0
  246. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/tests/test_k8s.py +0 -0
  247. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/tests/test_services.py +0 -0
  248. {quoll_cli-0.2.0 → quoll_cli-0.3.0}/tests/test_workspace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: quoll-cli
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Production-grade CI, Dockerfiles, Helm charts and delivery for any repository, kept in sync with one quoll.toml
5
5
  Project-URL: Homepage, https://github.com/theo-mrn/Quoll
6
6
  Project-URL: Repository, https://github.com/theo-mrn/Quoll
@@ -254,7 +254,7 @@ quoll ci add-step -d back # in a workspace: the project's directory; asks wh
254
254
  # .pre-commit-config.yaml
255
255
  repos:
256
256
  - repo: https://github.com/theo-mrn/Quoll
257
- rev: v0.2.0
257
+ rev: v0.3.0
258
258
  hooks:
259
259
  - id: quoll-ci # quoll ci update; also quoll-docker, quoll-cd
260
260
  ```
@@ -411,7 +411,7 @@ Every push to the default branch and every `v*` tag:
411
411
  | Runs after CI | through branch protection | always: its `publish` stage starts once every CI job passed |
412
412
  | Default registry (no secret) | GHCR, with the workflow's token | the project's container registry, with the job token |
413
413
  | Build | Buildx, GitHub Actions cache | Docker-in-Docker, a Buildx container builder; Trivy runs next to the image |
414
- | Signature | artifact attestations (public repositories, or GitHub Enterprise Cloud for private ones) | cosign keyless with the job's OIDC token (gitlab.com: Sigstore's public instance doesn't trust self-managed issuers) |
414
+ | Signature | artifact attestations (public repositories, or GitHub Enterprise Cloud for private ones: `quoll cd init` turns signing off on a private repository, `--sign` keeps it) | cosign keyless with the job's OIDC token (gitlab.com: Sigstore's public instance doesn't trust self-managed issuers) |
415
415
 
416
416
  Docker Hub (`registry = "dockerhub"`) needs `DOCKERHUB_USERNAME` and `DOCKERHUB_TOKEN`. Multi-arch images are a `platforms` entry away.
417
417
 
@@ -421,16 +421,35 @@ registry = "ghcr" # ghcr (GitHub) | gitlab (GitLab) | dockerhub
421
421
  image = "ghcr.io/acme/api"
422
422
  branches = ["main"]
423
423
  release_tags = true
424
+ release_pr = true # GitHub: release-please opens the release PR that tags v* (below)
424
425
  platforms = ["linux/amd64"] # e.g. ["linux/amd64", "linux/arm64"]
425
426
  scan = true
426
427
  severity = ["CRITICAL", "HIGH"]
427
428
  ignore_unfixed = true
428
429
  sbom = true # SBOM of the image: artifact + release asset
429
- sign = true # Sigstore signature and attestations (set false on a private repo without GitHub Enterprise)
430
+ sign = true # Sigstore signature and attestations (`cd init` writes false on a private GitHub repo)
430
431
  ```
431
432
 
432
433
  On GitHub, branch protection (set by `quoll ci init`) makes sure only commits that passed CI reach the default branch, and therefore the registry.
433
434
 
435
+ ### Releases
436
+
437
+ On GitHub, nobody has to push the `v*` tags: with `release_pr` (the default of `quoll cd init`), [release-please](https://github.com/googleapis/release-please) reads the [Conventional Commits](https://www.conventionalcommits.org/) on the default branch and keeps a **release PR** open with the next version and its `CHANGELOG.md`. Merging it tags `v<version>` and creates the GitHub release, which publishes the image and, with a chart, deploys to production after the approval.
438
+
439
+ ```
440
+ feat: … fix: … feat!: … → release PR "chore(main): release 1.3.0" → merge → v1.3.0 → CD
441
+ ```
442
+
443
+ - `fix:` bumps the patch, `feat:` the minor, a breaking change (`feat!:`, `BREAKING CHANGE:`) the major. Before `1.0.0`, a breaking change bumps the minor.
444
+ - Node projects get their `package.json` (and `package-lock.json`) version bumped, Rust their `Cargo.toml` and `Cargo.lock`. The other stacks keep their version in the tag: release-please can't update every lockfile (`uv.lock` records the project's version).
445
+ - A workspace releases as a whole, under one tag, like a `v*` tag pushed by hand.
446
+ - `release-please-config.json` and `.release-please-manifest.json` are written once, starting from the latest `v*` tag (`0.1.0` for a project never released), and are yours afterwards: release-please updates the manifest at each release.
447
+ - The release PR and the tag are created with the workflow's own token, so they start no workflow by themselves (GitHub's rule against recursive runs). The release workflow dispatches the CI on the release PR and the delivery on the tag. A dispatched run isn't attached to the pull request, so its `CI result` job also reports to the commit as a status, which branch protection reads.
448
+ - On a public repository, GitHub also creates the release PR's `pull_request` run but waits for an approval, since the bot counts as a first-time contributor. It isn't required: the dispatched run already reported.
449
+ - GitHub must let Actions open pull requests (Settings → Actions → General). `quoll new --remote` turns it on.
450
+
451
+ GitLab has no release PR: push the `v*` tags there.
452
+
434
453
  ## Kubernetes
435
454
 
436
455
  ```bash
@@ -497,6 +516,8 @@ In a **workspace**, every project deploys to the same namespaces and reaches the
497
516
  ```bash
498
517
  quoll new # interactive
499
518
  quoll new shop --front angular --back express
519
+ quoll new shop --front react --back fastapi --python 3.13
520
+ quoll new shop --front nextjs --back go
500
521
  quoll new shop --front angular@18 --back express@4 --node 20 --platform gitlab --cd --k8s --remote
501
522
  quoll new shop --front angular --back none --service orders=express --service users=express@4
502
523
  ```
@@ -510,9 +531,13 @@ From nothing to a repository whose pipeline passes on its first push:
510
531
  | Starter | Role | How |
511
532
  |---|---|---|
512
533
  | `angular` | front | Angular's own CLI (`ng new`) at the chosen major — `angular@18` — plus ESLint (angular-eslint) |
534
+ | `react` | front | Vite's own generator (`create-vite`, TypeScript), React's major with `react@18`; Vitest + Testing Library, Prettier |
535
+ | `nextjs` | front | Next.js' own generator (`create-next-app`: App Router, ESLint, Tailwind), `nextjs@15`; standalone build, Vitest, Prettier |
513
536
  | `express` | back | quoll's template: Express (`express@4` for 4) and TypeScript, Vitest + Supertest, ESLint, `/health`, graceful shutdown |
537
+ | `fastapi` | back | quoll's template: FastAPI with uv, pytest (TestClient), ruff, `/health` |
538
+ | `go` | back | quoll's template: the standard library's `net/http` (Go 1.22+ routing), httptest, graceful shutdown, `/health` |
514
539
 
515
- **Versions.** `name@version` picks the framework's major; `--node 20` the runtime: the generators then run in `node:20` (so the dependencies and the lockfile match it), and `.nvmrc` makes the CI and the Dockerfile use it too. A version its maintainers no longer support is flagged at creation: the CI's dependency audit would fail on its known vulnerabilities.
540
+ **Versions.** `name@version` picks the framework's major; `--node 22`, `--python 3.13`, `--go 1.26` the runtime: the generators then run in that runtime's image (so the dependencies and the lockfile match it), and `.nvmrc`, `.python-version` or `go.mod` make the CI and the Dockerfile use it too. Asked interactively, each framework offers its latest version and the older majors its maintainers still fix (Angular 22 to 20, React 19 and 18, Next.js 16 and 15, Express 5 and 4), or any other. A version they no longer fix is flagged at creation: the CI's dependency audit would fail on its known vulnerabilities.
516
541
 
517
542
  ### Microservices
518
543
 
@@ -530,7 +555,7 @@ quoll remove service billing --keep-files # only takes it out of the workspac
530
555
 
531
556
  `quoll remove service` does the opposite: the project leaves the members, its jobs, its compose service and `BILLING_URL`, its delivery and setup action are removed. It refuses to delete a directory with uncommitted changes.
532
557
 
533
- **Front-end → API.** With a front-end and a back-end, `quoll new` makes the front-end call the API on its own origin, at `/api/` (no CORS, no URL baked into the build): `ng serve` forwards there (`proxy.conf.json`), and so does nginx in the image, to the URL in `BACK_URL`, which the root `compose.yaml` sets:
558
+ **Front-end → API.** With a front-end and a back-end, `quoll new` makes the front-end call the API on its own origin, at `/api/` (no CORS, no URL baked into the build). In development, the dev server forwards there (`proxy.conf.json` for Angular, Vite's `server.proxy`); deployed, to the URL in `BACK_URL`, which the root `compose.yaml` and the charts set — through nginx for a static site, through Next.js' own `proxy.ts` (read on each request) for Next.js:
534
559
 
535
560
  ```toml
536
561
  # front/quoll.toml
@@ -205,7 +205,7 @@ quoll ci add-step -d back # in a workspace: the project's directory; asks wh
205
205
  # .pre-commit-config.yaml
206
206
  repos:
207
207
  - repo: https://github.com/theo-mrn/Quoll
208
- rev: v0.2.0
208
+ rev: v0.3.0
209
209
  hooks:
210
210
  - id: quoll-ci # quoll ci update; also quoll-docker, quoll-cd
211
211
  ```
@@ -362,7 +362,7 @@ Every push to the default branch and every `v*` tag:
362
362
  | Runs after CI | through branch protection | always: its `publish` stage starts once every CI job passed |
363
363
  | Default registry (no secret) | GHCR, with the workflow's token | the project's container registry, with the job token |
364
364
  | Build | Buildx, GitHub Actions cache | Docker-in-Docker, a Buildx container builder; Trivy runs next to the image |
365
- | Signature | artifact attestations (public repositories, or GitHub Enterprise Cloud for private ones) | cosign keyless with the job's OIDC token (gitlab.com: Sigstore's public instance doesn't trust self-managed issuers) |
365
+ | Signature | artifact attestations (public repositories, or GitHub Enterprise Cloud for private ones: `quoll cd init` turns signing off on a private repository, `--sign` keeps it) | cosign keyless with the job's OIDC token (gitlab.com: Sigstore's public instance doesn't trust self-managed issuers) |
366
366
 
367
367
  Docker Hub (`registry = "dockerhub"`) needs `DOCKERHUB_USERNAME` and `DOCKERHUB_TOKEN`. Multi-arch images are a `platforms` entry away.
368
368
 
@@ -372,16 +372,35 @@ registry = "ghcr" # ghcr (GitHub) | gitlab (GitLab) | dockerhub
372
372
  image = "ghcr.io/acme/api"
373
373
  branches = ["main"]
374
374
  release_tags = true
375
+ release_pr = true # GitHub: release-please opens the release PR that tags v* (below)
375
376
  platforms = ["linux/amd64"] # e.g. ["linux/amd64", "linux/arm64"]
376
377
  scan = true
377
378
  severity = ["CRITICAL", "HIGH"]
378
379
  ignore_unfixed = true
379
380
  sbom = true # SBOM of the image: artifact + release asset
380
- sign = true # Sigstore signature and attestations (set false on a private repo without GitHub Enterprise)
381
+ sign = true # Sigstore signature and attestations (`cd init` writes false on a private GitHub repo)
381
382
  ```
382
383
 
383
384
  On GitHub, branch protection (set by `quoll ci init`) makes sure only commits that passed CI reach the default branch, and therefore the registry.
384
385
 
386
+ ### Releases
387
+
388
+ On GitHub, nobody has to push the `v*` tags: with `release_pr` (the default of `quoll cd init`), [release-please](https://github.com/googleapis/release-please) reads the [Conventional Commits](https://www.conventionalcommits.org/) on the default branch and keeps a **release PR** open with the next version and its `CHANGELOG.md`. Merging it tags `v<version>` and creates the GitHub release, which publishes the image and, with a chart, deploys to production after the approval.
389
+
390
+ ```
391
+ feat: … fix: … feat!: … → release PR "chore(main): release 1.3.0" → merge → v1.3.0 → CD
392
+ ```
393
+
394
+ - `fix:` bumps the patch, `feat:` the minor, a breaking change (`feat!:`, `BREAKING CHANGE:`) the major. Before `1.0.0`, a breaking change bumps the minor.
395
+ - Node projects get their `package.json` (and `package-lock.json`) version bumped, Rust their `Cargo.toml` and `Cargo.lock`. The other stacks keep their version in the tag: release-please can't update every lockfile (`uv.lock` records the project's version).
396
+ - A workspace releases as a whole, under one tag, like a `v*` tag pushed by hand.
397
+ - `release-please-config.json` and `.release-please-manifest.json` are written once, starting from the latest `v*` tag (`0.1.0` for a project never released), and are yours afterwards: release-please updates the manifest at each release.
398
+ - The release PR and the tag are created with the workflow's own token, so they start no workflow by themselves (GitHub's rule against recursive runs). The release workflow dispatches the CI on the release PR and the delivery on the tag. A dispatched run isn't attached to the pull request, so its `CI result` job also reports to the commit as a status, which branch protection reads.
399
+ - On a public repository, GitHub also creates the release PR's `pull_request` run but waits for an approval, since the bot counts as a first-time contributor. It isn't required: the dispatched run already reported.
400
+ - GitHub must let Actions open pull requests (Settings → Actions → General). `quoll new --remote` turns it on.
401
+
402
+ GitLab has no release PR: push the `v*` tags there.
403
+
385
404
  ## Kubernetes
386
405
 
387
406
  ```bash
@@ -448,6 +467,8 @@ In a **workspace**, every project deploys to the same namespaces and reaches the
448
467
  ```bash
449
468
  quoll new # interactive
450
469
  quoll new shop --front angular --back express
470
+ quoll new shop --front react --back fastapi --python 3.13
471
+ quoll new shop --front nextjs --back go
451
472
  quoll new shop --front angular@18 --back express@4 --node 20 --platform gitlab --cd --k8s --remote
452
473
  quoll new shop --front angular --back none --service orders=express --service users=express@4
453
474
  ```
@@ -461,9 +482,13 @@ From nothing to a repository whose pipeline passes on its first push:
461
482
  | Starter | Role | How |
462
483
  |---|---|---|
463
484
  | `angular` | front | Angular's own CLI (`ng new`) at the chosen major — `angular@18` — plus ESLint (angular-eslint) |
485
+ | `react` | front | Vite's own generator (`create-vite`, TypeScript), React's major with `react@18`; Vitest + Testing Library, Prettier |
486
+ | `nextjs` | front | Next.js' own generator (`create-next-app`: App Router, ESLint, Tailwind), `nextjs@15`; standalone build, Vitest, Prettier |
464
487
  | `express` | back | quoll's template: Express (`express@4` for 4) and TypeScript, Vitest + Supertest, ESLint, `/health`, graceful shutdown |
488
+ | `fastapi` | back | quoll's template: FastAPI with uv, pytest (TestClient), ruff, `/health` |
489
+ | `go` | back | quoll's template: the standard library's `net/http` (Go 1.22+ routing), httptest, graceful shutdown, `/health` |
465
490
 
466
- **Versions.** `name@version` picks the framework's major; `--node 20` the runtime: the generators then run in `node:20` (so the dependencies and the lockfile match it), and `.nvmrc` makes the CI and the Dockerfile use it too. A version its maintainers no longer support is flagged at creation: the CI's dependency audit would fail on its known vulnerabilities.
491
+ **Versions.** `name@version` picks the framework's major; `--node 22`, `--python 3.13`, `--go 1.26` the runtime: the generators then run in that runtime's image (so the dependencies and the lockfile match it), and `.nvmrc`, `.python-version` or `go.mod` make the CI and the Dockerfile use it too. Asked interactively, each framework offers its latest version and the older majors its maintainers still fix (Angular 22 to 20, React 19 and 18, Next.js 16 and 15, Express 5 and 4), or any other. A version they no longer fix is flagged at creation: the CI's dependency audit would fail on its known vulnerabilities.
467
492
 
468
493
  ### Microservices
469
494
 
@@ -481,7 +506,7 @@ quoll remove service billing --keep-files # only takes it out of the workspac
481
506
 
482
507
  `quoll remove service` does the opposite: the project leaves the members, its jobs, its compose service and `BILLING_URL`, its delivery and setup action are removed. It refuses to delete a directory with uncommitted changes.
483
508
 
484
- **Front-end → API.** With a front-end and a back-end, `quoll new` makes the front-end call the API on its own origin, at `/api/` (no CORS, no URL baked into the build): `ng serve` forwards there (`proxy.conf.json`), and so does nginx in the image, to the URL in `BACK_URL`, which the root `compose.yaml` sets:
509
+ **Front-end → API.** With a front-end and a back-end, `quoll new` makes the front-end call the API on its own origin, at `/api/` (no CORS, no URL baked into the build). In development, the dev server forwards there (`proxy.conf.json` for Angular, Vite's `server.proxy`); deployed, to the URL in `BACK_URL`, which the root `compose.yaml` and the charts set — through nginx for a static site, through Next.js' own `proxy.ts` (read on each request) for Next.js:
485
510
 
486
511
  ```toml
487
512
  # front/quoll.toml
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "quoll-cli"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "Production-grade CI, Dockerfiles, Helm charts and delivery for any repository, kept in sync with one quoll.toml"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -78,6 +78,8 @@ testpaths = ["tests"]
78
78
  [tool.ruff]
79
79
  line-length = 120
80
80
  target-version = "py39"
81
+ # Starters' templates are the generated projects' code, checked there (with their own settings).
82
+ extend-exclude = ["quoll/languages/*/frameworks/*/template", "quoll/languages/*/templates"]
81
83
 
82
84
  [tool.ruff.lint]
83
85
  select = ["E", "F", "I", "B"]
@@ -27,6 +27,9 @@ class CDConfig(Section, PlatformField):
27
27
  branches: List[str] = Field(default_factory=lambda: ["main"])
28
28
  # Git tags v1.2.3 publish :1.2.3 and :1.2.
29
29
  release_tags: bool = True
30
+ # GitHub: release-please keeps a release pull request open from the Conventional Commits on the default branch
31
+ # (next version, CHANGELOG.md); merging it tags v<version>, which publishes the image and deploys to production.
32
+ release_pr: bool = False
30
33
  platforms: List[str] = Field(default_factory=lambda: ["linux/amd64"])
31
34
  # Trivy scans the image before it is pushed; findings of these severities block the push.
32
35
  scan: bool = True
@@ -60,6 +63,10 @@ class CDConfig(Section, PlatformField):
60
63
  raise ValueError(f"unknown severity '{unknown[0]}' (expected some of: {', '.join(SEVERITIES)})")
61
64
  if self.scan and not self.severity:
62
65
  raise ValueError("severity must list at least one level when scan is true")
66
+ if self.release_pr and self.platform != "github":
67
+ raise ValueError("release_pr is GitHub only (release-please): push the v* tags yourself")
68
+ if self.release_pr and not self.release_tags:
69
+ raise ValueError("release_pr needs release_tags: the release it tags is what publishes")
63
70
  if not self.branches and not self.release_tags:
64
71
  raise ValueError("nothing would publish the image: set branches or release_tags")
65
72
  if not self.platforms:
@@ -16,8 +16,10 @@ def extend(cfg: CIConfig, job: str, items: List[Item]) -> List[Item]:
16
16
  after = [_step(s) for s in cfg.steps if s.job == job and s.when == "after"]
17
17
  if not before and not after:
18
18
  return items
19
- installed = cfg.language.ci.install(cfg)
20
- last = max((i for i, item in enumerate(items) if item in installed), default=-1)
19
+ # By name: a job may run its own variant of the install step (C/C++ configures the test build with
20
+ # coverage and sanitizers, the release build optimised), still the step that installs the dependencies.
21
+ installed = {item.name for item in cfg.language.ci.install(cfg)} - {""}
22
+ last = max((i for i, item in enumerate(items) if item.name in installed), default=-1)
21
23
  return [*items[: last + 1], *before, *items[last + 1 :], *after]
22
24
 
23
25
 
@@ -38,22 +38,30 @@ def check_platform(name: Optional[str]) -> None:
38
38
  raise typer.Exit(1)
39
39
 
40
40
 
41
- def ensure_token(platform: str, host: Optional[str] = None) -> bool:
42
- """A token for `platform` in settings: from the environment, the platform's CLI (gh, glab: the session the user
43
- keeps fresh), the one stored in ~/.config/quoll, or asked."""
41
+ def find_token(platform: str, host: Optional[str] = None) -> bool:
42
+ """A token for `platform` in settings, without asking: from the environment, the platform's CLI (gh, glab: the
43
+ session the user keeps fresh) or the one stored in ~/.config/quoll."""
44
44
  attr = f"{platform}_token"
45
45
  if getattr(settings, attr):
46
46
  return True
47
47
  token = _cli_token(platform, host) or load_stored_token(platform)
48
+ if token:
49
+ setattr(settings, attr, token)
50
+ return bool(token)
51
+
52
+
53
+ def ensure_token(platform: str, host: Optional[str] = None) -> bool:
54
+ """A token for `platform` in settings: found (see `find_token`), or asked."""
55
+ if find_token(platform, host):
56
+ return True
57
+ label = platforms.get(platform).label
58
+ scope = "repo" if platform == "github" else "api"
59
+ token = ask(questionary.password(f"{label} token ({scope} scope)"))
48
60
  if not token:
49
- label = platforms.get(platform).label
50
- scope = "repo" if platform == "github" else "api"
51
- token = ask(questionary.password(f"{label} token ({scope} scope)"))
52
- if not token:
53
- return False
54
- save_token(token, platform)
55
- console.print("[dim]Token saved to ~/.config/quoll/config.toml[/dim]")
56
- setattr(settings, attr, token)
61
+ return False
62
+ save_token(token, platform)
63
+ console.print("[dim]Token saved to ~/.config/quoll/config.toml[/dim]")
64
+ setattr(settings, f"{platform}_token", token)
57
65
  return True
58
66
 
59
67
 
@@ -37,7 +37,9 @@ def service(
37
37
  spec: str = typer.Argument(..., help="NAME=STARTER[@VERSION], e.g. orders=express or orders=express@4."),
38
38
  project_dir: Path = DirOption,
39
39
  within: str = typer.Option(SERVICES_DIRECTORY, "--in", help="Directory holding the services."),
40
- node: Optional[str] = typer.Option(None, "--node", help="Node.js version, e.g. 20."),
40
+ node: Optional[str] = typer.Option(None, "--node", help="Node.js version, e.g. 22."),
41
+ python: Optional[str] = typer.Option(None, "--python", help="Python version, e.g. 3.13."),
42
+ go: Optional[str] = typer.Option(None, "--go", help="Go version, e.g. 1.26."),
41
43
  ) -> None:
42
44
  """Create a service in services/<name>, and add it to the workspace's CI, compose.yaml and delivery."""
43
45
  root = find_root(resolve_dir(project_dir))
@@ -67,7 +69,8 @@ def service(
67
69
  platform = configured_platform(root) or platforms.detect(root, platforms.remote(root))
68
70
  before = snapshot(root, platform) if workspace else Snapshot(set(), None)
69
71
  with Progress(SpinnerColumn(), TextColumn("{task.description}"), console=console) as progress:
70
- notes = create_part(root, root.name, Part(directory, starter, version, name), {"node": node or ""}, progress)
72
+ runtimes = {"node": node or "", "python": python or "", "go": go or ""}
73
+ notes = create_part(root, root.name, Part(directory, starter, version, name), runtimes, progress)
71
74
  quiet("Docker", docker_init(root / directory), progress)
72
75
 
73
76
  task = progress.add_task("Adding it to the workspace...")
@@ -85,7 +88,7 @@ def service(
85
88
  reference = next(load_cd_config(root / m) for m in members if "cd" in quoll_toml.read(root / m))
86
89
  quiet("CD", lambda: cd_command._configure(
87
90
  root / directory, root, member_name(directory), yes=True, force=False, platform=platform,
88
- registry=reference.registry, image=None,
91
+ registry=reference.registry, image=None, release_pr=reference.release_pr, sign=reference.sign,
89
92
  ), progress)
90
93
 
91
94
  # And its chart, deployed with the others: same namespaces.
@@ -16,6 +16,7 @@ from quoll.commands._common import (
16
16
  check_files,
17
17
  check_platform,
18
18
  confirm_overwrite,
19
+ find_token,
19
20
  platform_option,
20
21
  resolve_dir,
21
22
  update_files,
@@ -39,6 +40,15 @@ def init(
39
40
  None, help="ghcr (GitHub) or gitlab (GitLab) — the default —, or dockerhub."
40
41
  ),
41
42
  image: Optional[str] = typer.Option(None, help="Image name, e.g. ghcr.io/acme/api."),
43
+ release_pr: Optional[bool] = typer.Option(
44
+ None, "--release-pr/--no-release-pr",
45
+ help="GitHub: a release PR from the Conventional Commits (release-please) tags the releases (default: yes).",
46
+ ),
47
+ sign: Optional[bool] = typer.Option(
48
+ None, "--sign/--no-sign",
49
+ help="Sign the image and its SBOM with Sigstore (default: yes, but no for a private GitHub repository: "
50
+ "artifact attestations need GitHub Enterprise Cloud there).",
51
+ ),
42
52
  ) -> None:
43
53
  """Write [cd] in quoll.toml and generate the delivery pipeline.
44
54
 
@@ -53,7 +63,7 @@ def init(
53
63
  raise typer.Exit(1)
54
64
  if workspace is None:
55
65
  cfg = _configure(project_dir, root, "", yes=yes, force=force, platform=platform, registry=registry,
56
- image=image)
66
+ image=image, release_pr=release_pr, sign=sign)
57
67
  if cfg is None:
58
68
  raise typer.Exit(0)
59
69
  deliveries: List[Delivery] = [Delivery("", ".", cfg)]
@@ -70,11 +80,16 @@ def init(
70
80
  raise typer.Exit(1)
71
81
  for member in members:
72
82
  _configure(root / member, root, member_name(member), yes=yes, force=force, platform=platform,
73
- registry=registry, image=image)
83
+ registry=registry, image=image, release_pr=release_pr, sign=sign)
84
+ # A workspace releases as a whole, from one repository: asked once.
85
+ if release_pr is None:
86
+ release_pr = _read(root / member).get("cd", {}).get("release_pr", False)
87
+ if sign is None and "cd" in _read(root / member):
88
+ sign = _read(root / member)["cd"].get("sign", True)
74
89
  deliveries = _deliveries(root)
75
90
 
76
91
  cfg = deliveries[0].cfg
77
- files = _plan(root, deliveries)
92
+ files = _writable(_plan(root, deliveries))
78
93
  if not confirm_overwrite(files, yes=yes, force=force):
79
94
  raise typer.Exit(1)
80
95
  for delivery in deliveries:
@@ -94,6 +109,16 @@ def init(
94
109
  console.print(f"Set the {names} {target.label} {target.secrets_label}.")
95
110
  if cfg.platform == "gitlab":
96
111
  _check_gitlab_include(root)
112
+ if any(d.cfg.release_pr for d in deliveries):
113
+ console.print(RELEASE_NOTE)
114
+
115
+
116
+ RELEASE_NOTE = (
117
+ "\nReleases: commit with Conventional Commits ([bold]feat:[/bold], [bold]fix:[/bold], [bold]feat!:[/bold]); "
118
+ "release-please keeps a release PR open, and merging it tags the version, which publishes and deploys.\n"
119
+ "GitHub must let Actions open pull requests: Settings → Actions → General → "
120
+ "[bold]Allow GitHub Actions to create and approve pull requests[/bold] (`quoll new --remote` turns it on)."
121
+ )
97
122
 
98
123
 
99
124
  def _configure(
@@ -106,6 +131,8 @@ def _configure(
106
131
  platform: Optional[str],
107
132
  registry: Optional[str],
108
133
  image: Optional[str],
134
+ release_pr: Optional[bool] = None,
135
+ sign: Optional[bool] = None,
109
136
  ) -> Optional[CDConfig]:
110
137
  """The [cd] of one project, asked or defaulted; saved in its quoll.toml. None when kept as it was."""
111
138
  console.print(f"\n[bold]quoll cd[/bold] — {member or project_dir.name}\n")
@@ -134,12 +161,27 @@ def _configure(
134
161
  if image is None:
135
162
  image = default_image if yes else ask(questionary.text("Image", default=default_image))
136
163
 
137
- note = " (private GitHub repositories need GitHub Enterprise Cloud)" if name == "github" else ""
138
- sign = True if yes else ask(questionary.confirm(f"Sign the image and its SBOM with Sigstore{note}?", default=True))
164
+ if sign is None:
165
+ private = _private_repo(name, remote)
166
+ if private:
167
+ console.print("[dim]Private GitHub repository: artifact attestations need GitHub Enterprise Cloud there, "
168
+ "so signing defaults to off (--sign turns it on).[/dim]")
169
+ note = " (private GitHub repositories need GitHub Enterprise Cloud)" if name == "github" else ""
170
+ sign = not private if yes else ask(
171
+ questionary.confirm(f"Sign the image and its SBOM with Sigstore{note}?", default=not private)
172
+ )
173
+ if name != "github":
174
+ release_pr = False
175
+ elif release_pr is None:
176
+ release_pr = True if yes else ask(questionary.confirm(
177
+ "Open a release PR from the Conventional Commits (release-please)? Merging it tags the version.",
178
+ default=True,
179
+ ))
139
180
 
140
181
  try:
141
182
  cfg = CDConfig(
142
- platform=name, registry=registry, image=image.lower(), branches=[default_branch(root)], sign=sign
183
+ platform=name, registry=registry, image=image.lower(), branches=[default_branch(root)], sign=sign,
184
+ release_pr=release_pr,
143
185
  )
144
186
  except ValueError as e:
145
187
  console.print(f"[red]Error:[/red] {e}")
@@ -150,6 +192,15 @@ def _configure(
150
192
  return cfg
151
193
 
152
194
 
195
+ def _private_repo(name: str, remote: Optional[platforms.Remote]) -> bool:
196
+ """True when the remote is a private repository on github.com, where a signed delivery would fail: artifact
197
+ attestations need GitHub Enterprise Cloud on private repositories. Looked up only with a token already at hand
198
+ (never asked); unknown counts as public."""
199
+ if name != "github" or remote is None or remote.host != "github.com" or not find_token("github"):
200
+ return False
201
+ return platforms.get("github").api().is_private(remote.path) is True
202
+
203
+
153
204
  @app.command()
154
205
  def update(
155
206
  project_dir: Path = DirOption,
@@ -158,14 +209,14 @@ def update(
158
209
  ) -> None:
159
210
  """Regenerate the delivery workflow from quoll.toml (the workspace's, from any of its directories)."""
160
211
  root = find_root(resolve_dir(project_dir))
161
- update_files(root, _plan(root, _deliveries(root)), yes=yes, force=force, what="CD is")
212
+ update_files(root, _writable(_plan(root, _deliveries(root))), yes=yes, force=force, what="CD is")
162
213
 
163
214
 
164
215
  @app.command()
165
216
  def check(project_dir: Path = DirOption) -> None:
166
217
  """Fail if the delivery pipeline differs from what quoll.toml describes."""
167
218
  root = find_root(resolve_dir(project_dir))
168
- check_files(_plan(root, _deliveries(root)), "cd")
219
+ check_files([f for f in _plan(root, _deliveries(root)) if not f.create_only], "cd")
169
220
 
170
221
 
171
222
  def _plan(root: Path, deliveries: List[Delivery]) -> List[PlannedFile]:
@@ -173,6 +224,11 @@ def _plan(root: Path, deliveries: List[Delivery]) -> List[PlannedFile]:
173
224
  return platforms.get(deliveries[0].cfg.platform).cd_files(root, target)
174
225
 
175
226
 
227
+ def _writable(files: List[PlannedFile]) -> List[PlannedFile]:
228
+ """Without release-please's configuration and manifest once written: they are the project's then."""
229
+ return [f for f in files if not (f.create_only and f.existing is not None)]
230
+
231
+
176
232
  def _deliveries(root: Path) -> List[Delivery]:
177
233
  try:
178
234
  return load_deliveries(root)
@@ -204,6 +260,8 @@ def _print_config(cfg: CDConfig) -> None:
204
260
  table.add_row("Platforms", ", ".join(cfg.platforms))
205
261
  table.add_row("SBOM", "artifact + release asset" if cfg.sbom else "off")
206
262
  table.add_row("Signature", "Sigstore" if cfg.sign else "off")
263
+ if cfg.platform == "github":
264
+ table.add_row("Releases", "release PR (release-please)" if cfg.release_pr else "v* tags pushed by hand")
207
265
  console.print(table)
208
266
 
209
267
 
@@ -29,6 +29,12 @@ from quoll.workspace import member_name
29
29
  # Where each part goes when the repository holds several.
30
30
  DIRECTORIES = {"front": "front", "back": "back"}
31
31
  SERVICES_DIRECTORY = "services"
32
+ # Runtime versions offered when none is given: (label, versions), the default first.
33
+ RUNTIME_CHOICES = {
34
+ "node": ("Node.js", ["26", "24", "22"]),
35
+ "python": ("Python", ["3.14", "3.13", "3.12"]),
36
+ "go": ("Go", ["1.27", "1.26"]),
37
+ }
32
38
 
33
39
  @dataclass
34
40
  class Part:
@@ -64,7 +70,9 @@ def new(
64
70
  services: List[str] = typer.Option(
65
71
  [], "--service", help="A service in services/<name>: NAME=STARTER[@VERSION], e.g. orders=express (repeatable)."
66
72
  ),
67
- node: Optional[str] = typer.Option(None, "--node", help="Node.js version of the Node-based parts, e.g. 20."),
73
+ node: Optional[str] = typer.Option(None, "--node", help="Node.js version of the Node-based parts, e.g. 22."),
74
+ python: Optional[str] = typer.Option(None, "--python", help="Python version of the Python parts, e.g. 3.13."),
75
+ go: Optional[str] = typer.Option(None, "--go", help="Go version of the Go parts, e.g. 1.26."),
68
76
  platform: Optional[str] = platform_option(),
69
77
  delivery: Optional[bool] = typer.Option(None, "--cd/--no-cd", help="Also publish the images (quoll cd)."),
70
78
  kubernetes: Optional[bool] = typer.Option(
@@ -101,11 +109,14 @@ def new(
101
109
  Part(DIRECTORIES[role] if several else ".", starter, version, f"{name}-{role}" if several else name)
102
110
  for role, starter, version in picked
103
111
  ] + [Part(f"{SERVICES_DIRECTORY}/{n}" if several else ".", s, v, n) for n, s, v in picked_services]
104
- if node is None and not yes and any(p.starter.runtime == "node" for p in parts):
105
- node = ask(questionary.select(
106
- "Node.js version", choices=["24", "22", "20"], default=parts[0].starter.runtime_default or "24"
107
- ))
108
- runtimes = {"node": node or ""}
112
+ runtimes = {"node": node, "python": python, "go": go}
113
+ for runtime, version in runtimes.items():
114
+ used = [p.starter for p in parts if p.starter.runtime == runtime]
115
+ if version is None and not yes and used:
116
+ label, choices = RUNTIME_CHOICES[runtime]
117
+ runtimes[runtime] = ask(questionary.select(f"{label} version", choices=choices,
118
+ default=used[0].runtime_default or choices[0]))
119
+ runtimes = {runtime: version or "" for runtime, version in runtimes.items()}
109
120
  if platform is None:
110
121
  platform = "github" if yes else ask(questionary.select(
111
122
  "Hosting platform",
@@ -168,8 +179,12 @@ def new(
168
179
  # Before the delivery: it deploys the charts.
169
180
  steps.append(("Kubernetes", lambda: k8s_command.init(project_dir=root, yes=True, force=False)))
170
181
  if delivery:
182
+ # The repository just created says its visibility: a private one on GitHub can't hold attestations
183
+ # (without Enterprise Cloud). Otherwise cd init looks it up from the remote.
184
+ sign = False if repo and private and platform == "github" else None
171
185
  steps.append(("CD", lambda: cd_command.init(
172
- project_dir=root, yes=True, force=False, platform=platform, registry=None, image=None
186
+ project_dir=root, yes=True, force=False, platform=platform, registry=None, image=None, release_pr=None,
187
+ sign=sign,
173
188
  )))
174
189
  for label, run in steps:
175
190
  quiet(label, run, progress)
@@ -186,6 +201,8 @@ def new(
186
201
  try:
187
202
  api = target.api()
188
203
  warnings = api.protect_branch(repo.path, "main", required_checks=target.required_checks)
204
+ if delivery:
205
+ warnings += api.allow_release_prs(repo.path)
189
206
  if target.schedules_via_api:
190
207
  api.ensure_schedule(repo.path, "main", "0 6 * * 1")
191
208
  except PlatformError as e:
@@ -207,11 +224,12 @@ def create_part(root: Path, name: str, part: Part, runtimes: Dict[str, str], pro
207
224
  where = root / part.directory
208
225
  where.mkdir(parents=True, exist_ok=True)
209
226
  runtime = runtimes.get(part.starter.runtime, "")
210
- details = f" (Node.js {runtime})" if runtime else ""
227
+ details = f" ({RUNTIME_CHOICES[part.starter.runtime][0]} {runtime})" if runtime else ""
211
228
  shown = f"{name}/" if part.directory == "." else f"{name}/{part.directory}/"
212
229
  task = progress.add_task(f"Creating {part.label}{details} in {shown}...")
213
230
  try:
214
231
  notes = part.starter.create(where, StarterOptions(part.package, part.version, runtime)) or []
232
+ notes += [n for n in [unmaintained(part.starter, part.version)] if n]
215
233
  except StarterError as e:
216
234
  progress.stop()
217
235
  console.print(f"\n[red]Error:[/red] {e}")
@@ -225,19 +243,20 @@ def docker_init(directory: Path) -> Callable[[], None]:
225
243
 
226
244
 
227
245
  def proxy_api(root: Path, front: Part, back: Part) -> Callable[[], None]:
228
- """The front-end calls the back-end at /api/, on its own origin (no CORS): its dev server forwards there, and
229
- so does nginx in its image, to BACK_URL (set by the workspace's compose.yaml)."""
246
+ """The front-end calls the back-end at /api/, on its own origin (no CORS): in development, through its dev
247
+ server; deployed, to BACK_URL (set by the workspace's compose.yaml and charts) — through nginx for a static
248
+ site, the app's own server otherwise."""
230
249
 
231
250
  def run() -> None:
232
- docker = docker_command._load(root / front.directory)
233
- if not docker.static: # a server-rendered front-end calls the API itself
234
- return
251
+ variable = f"{member_name(back.directory).upper().replace('-', '_')}_URL"
235
252
  if front.starter.proxy:
236
253
  port = docker_command._load(root / back.directory).port
237
- front.starter.proxy(root / front.directory, "/api", f"http://localhost:{port}")
238
- docker.proxy = {"/api/": f"{member_name(back.directory).upper().replace('-', '_')}_URL"}
239
- save_docker_config(root / front.directory, docker)
240
- docker_command.update(project_dir=root / front.directory, yes=True, force=False)
254
+ front.starter.proxy(root / front.directory, "/api", f"http://localhost:{port}", variable)
255
+ docker = docker_command._load(root / front.directory)
256
+ if docker.static:
257
+ docker.proxy = {"/api/": variable}
258
+ save_docker_config(root / front.directory, docker)
259
+ docker_command.update(project_dir=root / front.directory, yes=True, force=False)
241
260
 
242
261
  return run
243
262
 
@@ -288,11 +307,31 @@ def _pick(role: str, choice: Optional[str], yes: bool) -> Optional[Tuple[str, St
288
307
  console.print(f"[red]Error:[/red] unknown {role}-end '{starter_name}' (expected one of: {', '.join(by_name)}).")
289
308
  raise typer.Exit(1)
290
309
  starter = by_name[starter_name]
291
- if asked:
292
- version = ask(questionary.text(f"{starter.label} version (Enter: the latest)", default=""))
310
+ if asked and starter.versions:
311
+ version = ask(questionary.select(f"{starter.label} version", choices=[
312
+ questionary.Choice(f"Latest ({starter.versions[0]})", ""),
313
+ *(questionary.Choice(v, v) for v in starter.versions[1:]),
314
+ questionary.Choice("Other…", OTHER),
315
+ ]))
316
+ if version == OTHER:
317
+ version = ask(questionary.text(f"{starter.label} version, e.g. {starter.versions[-1]}"))
293
318
  return role, starter, version.strip()
294
319
 
295
320
 
321
+ OTHER = "other"
322
+
323
+
324
+ def unmaintained(starter: Starter, version: str) -> str:
325
+ """A note when `version` is older than the majors `starter`'s maintainers still fix."""
326
+ major = version.split(".")[0]
327
+ if not starter.versions or not major.isdigit() or int(major) >= int(starter.versions[-1]):
328
+ return ""
329
+ return (
330
+ f"{starter.label} {major} no longer gets security fixes: the CI's dependency audit will fail on its known "
331
+ f"vulnerabilities. Maintained: {', '.join(starter.versions)}."
332
+ )
333
+
334
+
296
335
  def _valid_name(value: str):
297
336
  if re.fullmatch(r"[a-z][a-z0-9-]*", value or ""):
298
337
  return True
@@ -329,8 +368,8 @@ def _summary(root: Path, name: str, parts: List[Part], repo) -> None:
329
368
  console.print("\n[dim]Next steps:[/dim]")
330
369
  for part in parts:
331
370
  where = name if part.directory == "." else f"{name}/{part.directory}"
332
- run = "npm start" if part.starter.role == "front" else "npm run dev"
333
- console.print(f" cd {where} && {run}")
371
+ if part.starter.dev:
372
+ console.print(f" cd {where} && {part.starter.dev}")
334
373
  if len(parts) > 1:
335
374
  console.print(f" cd {name} && docker compose up --build # everything, with the databases")
336
375
  else: