quoll-cli 0.2.0__py3-none-any.whl

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 (191) hide show
  1. quoll/__init__.py +0 -0
  2. quoll/cd/__init__.py +1 -0
  3. quoll/cd/config.py +75 -0
  4. quoll/ci/__init__.py +0 -0
  5. quoll/ci/config.py +213 -0
  6. quoll/ci/custom.py +32 -0
  7. quoll/ci/detect.py +139 -0
  8. quoll/ci/steps.py +85 -0
  9. quoll/ci/tools.py +104 -0
  10. quoll/commands/__init__.py +0 -0
  11. quoll/commands/_common.py +145 -0
  12. quoll/commands/_workspace.py +83 -0
  13. quoll/commands/add.py +104 -0
  14. quoll/commands/add_secret.py +75 -0
  15. quoll/commands/cd.py +214 -0
  16. quoll/commands/ci.py +521 -0
  17. quoll/commands/docker.py +244 -0
  18. quoll/commands/k8s.py +211 -0
  19. quoll/commands/new.py +337 -0
  20. quoll/commands/remove.py +79 -0
  21. quoll/config/__init__.py +0 -0
  22. quoll/config/quoll_toml.py +196 -0
  23. quoll/config/settings.py +22 -0
  24. quoll/config/store.py +39 -0
  25. quoll/console.py +3 -0
  26. quoll/docker/__init__.py +1 -0
  27. quoll/docker/config.py +64 -0
  28. quoll/docker/detect.py +41 -0
  29. quoll/docker/render.py +162 -0
  30. quoll/files.py +64 -0
  31. quoll/k8s/__init__.py +0 -0
  32. quoll/k8s/chart/templates/_helpers.tpl +38 -0
  33. quoll/k8s/chart/templates/deployment.yaml +93 -0
  34. quoll/k8s/chart/templates/hpa.yaml +24 -0
  35. quoll/k8s/chart/templates/ingress.yaml +35 -0
  36. quoll/k8s/chart/templates/pdb.yaml +16 -0
  37. quoll/k8s/chart/templates/service.yaml +18 -0
  38. quoll/k8s/chart/templates/serviceaccount.yaml +10 -0
  39. quoll/k8s/ci.py +39 -0
  40. quoll/k8s/config.py +97 -0
  41. quoll/k8s/deploy.py +22 -0
  42. quoll/k8s/detect.py +32 -0
  43. quoll/k8s/render.py +94 -0
  44. quoll/languages/__init__.py +121 -0
  45. quoll/languages/base.py +350 -0
  46. quoll/languages/cpp/Dockerfile.j2 +37 -0
  47. quoll/languages/cpp/__init__.py +17 -0
  48. quoll/languages/cpp/ci.py +192 -0
  49. quoll/languages/cpp/detect.py +84 -0
  50. quoll/languages/cpp/docker.py +47 -0
  51. quoll/languages/dotnet/Dockerfile.j2 +21 -0
  52. quoll/languages/dotnet/__init__.py +17 -0
  53. quoll/languages/dotnet/ci.py +88 -0
  54. quoll/languages/dotnet/detect.py +53 -0
  55. quoll/languages/dotnet/docker.py +47 -0
  56. quoll/languages/go/Dockerfile.j2 +25 -0
  57. quoll/languages/go/__init__.py +17 -0
  58. quoll/languages/go/ci.py +126 -0
  59. quoll/languages/go/detect.py +30 -0
  60. quoll/languages/go/docker.py +40 -0
  61. quoll/languages/java/Dockerfile.j2 +28 -0
  62. quoll/languages/java/__init__.py +20 -0
  63. quoll/languages/java/ci.py +163 -0
  64. quoll/languages/java/detect.py +77 -0
  65. quoll/languages/java/docker.py +45 -0
  66. quoll/languages/java/frameworks/__init__.py +1 -0
  67. quoll/languages/java/frameworks/spring_boot/Dockerfile.j2 +18 -0
  68. quoll/languages/java/frameworks/spring_boot/__init__.py +10 -0
  69. quoll/languages/java/frameworks/spring_boot/detect.py +9 -0
  70. quoll/languages/java/frameworks/spring_boot/docker.py +19 -0
  71. quoll/languages/java/frameworks/spring_boot/services.py +24 -0
  72. quoll/languages/node/Dockerfile.j2 +98 -0
  73. quoll/languages/node/__init__.py +29 -0
  74. quoll/languages/node/ci.py +200 -0
  75. quoll/languages/node/detect.py +164 -0
  76. quoll/languages/node/docker.py +141 -0
  77. quoll/languages/node/frameworks/__init__.py +1 -0
  78. quoll/languages/node/frameworks/angular/__init__.py +18 -0
  79. quoll/languages/node/frameworks/angular/ci.py +34 -0
  80. quoll/languages/node/frameworks/angular/detect.py +20 -0
  81. quoll/languages/node/frameworks/angular/docker.py +31 -0
  82. quoll/languages/node/frameworks/angular/starter.py +74 -0
  83. quoll/languages/node/frameworks/astro/__init__.py +10 -0
  84. quoll/languages/node/frameworks/astro/ci.py +17 -0
  85. quoll/languages/node/frameworks/astro/detect.py +14 -0
  86. quoll/languages/node/frameworks/astro/docker.py +17 -0
  87. quoll/languages/node/frameworks/nestjs/__init__.py +7 -0
  88. quoll/languages/node/frameworks/nestjs/detect.py +9 -0
  89. quoll/languages/node/frameworks/nestjs/docker.py +10 -0
  90. quoll/languages/node/frameworks/nextjs/Dockerfile.j2 +14 -0
  91. quoll/languages/node/frameworks/nextjs/__init__.py +8 -0
  92. quoll/languages/node/frameworks/nextjs/ci.py +29 -0
  93. quoll/languages/node/frameworks/nextjs/detect.py +13 -0
  94. quoll/languages/node/frameworks/nextjs/docker.py +41 -0
  95. quoll/languages/node/frameworks/nuxt/__init__.py +10 -0
  96. quoll/languages/node/frameworks/nuxt/ci.py +17 -0
  97. quoll/languages/node/frameworks/nuxt/detect.py +17 -0
  98. quoll/languages/node/frameworks/nuxt/docker.py +24 -0
  99. quoll/languages/node/frameworks/sveltekit/__init__.py +15 -0
  100. quoll/languages/node/frameworks/sveltekit/ci.py +15 -0
  101. quoll/languages/node/frameworks/sveltekit/detect.py +18 -0
  102. quoll/languages/node/frameworks/sveltekit/docker.py +18 -0
  103. quoll/languages/node/starter.py +42 -0
  104. quoll/languages/node/templates/express/README.md +11 -0
  105. quoll/languages/node/templates/express/env.example +2 -0
  106. quoll/languages/node/templates/express/eslint.config.js +15 -0
  107. quoll/languages/node/templates/express/gitignore +4 -0
  108. quoll/languages/node/templates/express/nvmrc +1 -0
  109. quoll/languages/node/templates/express/package.json +16 -0
  110. quoll/languages/node/templates/express/prettierignore +6 -0
  111. quoll/languages/node/templates/express/src/app.ts +27 -0
  112. quoll/languages/node/templates/express/src/server.ts +11 -0
  113. quoll/languages/node/templates/express/test/app.test.ts +29 -0
  114. quoll/languages/node/templates/express/tsconfig.build.json +8 -0
  115. quoll/languages/node/templates/express/tsconfig.json +14 -0
  116. quoll/languages/node/templates/express/vitest.config.ts +12 -0
  117. quoll/languages/php/Dockerfile.j2 +40 -0
  118. quoll/languages/php/__init__.py +19 -0
  119. quoll/languages/php/ci.py +147 -0
  120. quoll/languages/php/detect.py +75 -0
  121. quoll/languages/php/docker.py +59 -0
  122. quoll/languages/php/frameworks/__init__.py +1 -0
  123. quoll/languages/php/frameworks/laravel/__init__.py +17 -0
  124. quoll/languages/php/frameworks/laravel/ci.py +24 -0
  125. quoll/languages/php/frameworks/laravel/detect.py +13 -0
  126. quoll/languages/php/frameworks/laravel/docker.py +29 -0
  127. quoll/languages/php/frameworks/laravel/services.py +25 -0
  128. quoll/languages/php/frameworks/symfony/__init__.py +15 -0
  129. quoll/languages/php/frameworks/symfony/ci.py +22 -0
  130. quoll/languages/php/frameworks/symfony/detect.py +17 -0
  131. quoll/languages/php/frameworks/symfony/docker.py +25 -0
  132. quoll/languages/python/Dockerfile.j2 +61 -0
  133. quoll/languages/python/__init__.py +19 -0
  134. quoll/languages/python/ci.py +157 -0
  135. quoll/languages/python/detect.py +175 -0
  136. quoll/languages/python/docker.py +80 -0
  137. quoll/languages/python/frameworks/__init__.py +1 -0
  138. quoll/languages/python/frameworks/django/__init__.py +16 -0
  139. quoll/languages/python/frameworks/django/ci.py +43 -0
  140. quoll/languages/python/frameworks/django/detect.py +17 -0
  141. quoll/languages/python/frameworks/django/docker.py +20 -0
  142. quoll/languages/python/frameworks/fastapi/__init__.py +7 -0
  143. quoll/languages/python/frameworks/fastapi/detect.py +9 -0
  144. quoll/languages/python/frameworks/fastapi/docker.py +21 -0
  145. quoll/languages/python/frameworks/flask/__init__.py +7 -0
  146. quoll/languages/python/frameworks/flask/detect.py +9 -0
  147. quoll/languages/python/frameworks/flask/docker.py +22 -0
  148. quoll/languages/ruby/Dockerfile.j2 +45 -0
  149. quoll/languages/ruby/__init__.py +19 -0
  150. quoll/languages/ruby/ci.py +110 -0
  151. quoll/languages/ruby/detect.py +72 -0
  152. quoll/languages/ruby/docker.py +63 -0
  153. quoll/languages/ruby/frameworks/__init__.py +1 -0
  154. quoll/languages/ruby/frameworks/rails/Dockerfile.j2 +16 -0
  155. quoll/languages/ruby/frameworks/rails/__init__.py +18 -0
  156. quoll/languages/ruby/frameworks/rails/ci.py +26 -0
  157. quoll/languages/ruby/frameworks/rails/detect.py +15 -0
  158. quoll/languages/ruby/frameworks/rails/docker.py +62 -0
  159. quoll/languages/ruby/frameworks/rails/services.py +10 -0
  160. quoll/languages/rust/Dockerfile.j2 +23 -0
  161. quoll/languages/rust/__init__.py +17 -0
  162. quoll/languages/rust/ci.py +125 -0
  163. quoll/languages/rust/detect.py +46 -0
  164. quoll/languages/rust/docker.py +38 -0
  165. quoll/main.py +50 -0
  166. quoll/platforms/__init__.py +62 -0
  167. quoll/platforms/base.py +103 -0
  168. quoll/platforms/github/__init__.py +4 -0
  169. quoll/platforms/github/actions.py +78 -0
  170. quoll/platforms/github/api.py +124 -0
  171. quoll/platforms/github/cd.py +265 -0
  172. quoll/platforms/github/ci.py +497 -0
  173. quoll/platforms/github/dependabot.yml.j2 +49 -0
  174. quoll/platforms/github/platform.py +52 -0
  175. quoll/platforms/github/workflow.py +74 -0
  176. quoll/platforms/gitlab/__init__.py +4 -0
  177. quoll/platforms/gitlab/api.py +158 -0
  178. quoll/platforms/gitlab/cd.py +226 -0
  179. quoll/platforms/gitlab/ci.py +400 -0
  180. quoll/platforms/gitlab/pipeline.py +136 -0
  181. quoll/platforms/gitlab/platform.py +54 -0
  182. quoll/project.py +60 -0
  183. quoll/services.py +117 -0
  184. quoll/starters.py +117 -0
  185. quoll/workspace.py +232 -0
  186. quoll/yaml_dump.py +137 -0
  187. quoll_cli-0.2.0.dist-info/METADATA +641 -0
  188. quoll_cli-0.2.0.dist-info/RECORD +191 -0
  189. quoll_cli-0.2.0.dist-info/WHEEL +4 -0
  190. quoll_cli-0.2.0.dist-info/entry_points.txt +2 -0
  191. quoll_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,641 @@
1
+ Metadata-Version: 2.5
2
+ Name: quoll-cli
3
+ Version: 0.2.0
4
+ Summary: Production-grade CI, Dockerfiles, Helm charts and delivery for any repository, kept in sync with one quoll.toml
5
+ Project-URL: Homepage, https://github.com/theo-mrn/Quoll
6
+ Project-URL: Repository, https://github.com/theo-mrn/Quoll
7
+ Project-URL: Issues, https://github.com/theo-mrn/Quoll/issues
8
+ Author-email: theo-mrn <theo.morin1402@gmail.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: cd,ci,cli,devops,docker,github-actions,gitlab-ci,helm,kubernetes,monorepo
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.14
24
+ Classifier: Topic :: Software Development :: Build Tools
25
+ Classifier: Topic :: Software Development :: Quality Assurance
26
+ Requires-Python: >=3.9
27
+ Requires-Dist: httpx>=0.27.0
28
+ Requires-Dist: jinja2>=3.1.0
29
+ Requires-Dist: pydantic-settings>=2.0.0
30
+ Requires-Dist: pydantic>=2.11.0
31
+ Requires-Dist: pynacl>=1.5.0
32
+ Requires-Dist: questionary>=2.0.0
33
+ Requires-Dist: rich>=13.0.0
34
+ Requires-Dist: tomli-w>=1.0.0
35
+ Requires-Dist: tomli>=2.0.0; python_version < '3.11'
36
+ Requires-Dist: typer>=0.12.0
37
+ Provides-Extra: dev
38
+ Requires-Dist: actionlint-py>=1.7.0; extra == 'dev'
39
+ Requires-Dist: check-jsonschema>=0.33.0; extra == 'dev'
40
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
41
+ Requires-Dist: pytest-cov>=5.0.0; extra == 'dev'
42
+ Requires-Dist: pytest-mock>=3.14.0; extra == 'dev'
43
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
44
+ Requires-Dist: pyyaml>=6.0; extra == 'dev'
45
+ Requires-Dist: respx>=0.21.0; extra == 'dev'
46
+ Requires-Dist: ruff>=0.6.0; extra == 'dev'
47
+ Requires-Dist: shellcheck-py>=0.10.0; extra == 'dev'
48
+ Description-Content-Type: text/markdown
49
+
50
+ # quoll
51
+
52
+ A CLI that gives any Python, Node, Go, Rust, Java/Kotlin, PHP, Ruby, .NET or C/C++ repository a production-grade CI, Dockerfile and delivery pipeline in one command — on **GitHub Actions or GitLab CI** — and keeps them maintained.
53
+
54
+ ## Install
55
+
56
+ ```bash
57
+ pipx install quoll-cli # or: uv tool install quoll-cli, pip install quoll-cli
58
+ quoll --version
59
+ ```
60
+
61
+ Python 3.9 or later. Docker is needed for the images, Helm for the charts (only to install them yourself).
62
+
63
+ ## CI
64
+
65
+ ```bash
66
+ cd my-project
67
+ quoll ci init
68
+ ```
69
+
70
+ ### Supported stacks
71
+
72
+ | Language | Package managers | Lint | Tests (coverage threshold) | Frameworks |
73
+ |---|---|---|---|---|
74
+ | Python | pip, uv, poetry | ruff, ruff format, flake8, black, mypy | pytest | Django, FastAPI, Flask |
75
+ | Node.js | npm, pnpm, yarn, bun | eslint, prettier, biome, tsc, any `package.json` script | vitest, jest, bun | Next.js, Nuxt, SvelteKit, Astro, Angular, NestJS |
76
+ | Go | go modules | gofmt, go vet, golangci-lint | go test | |
77
+ | Rust | cargo | rustfmt, clippy | cargo test, nextest (cargo-llvm-cov) | |
78
+ | Java / Kotlin | Maven, Gradle (wrappers supported) | spotless, checkstyle, ktlint, detekt, SpotBugs, PMD | JUnit via `verify` / `check`, JaCoCo coverage | Spring Boot |
79
+ | PHP | composer | php-cs-fixer, phpcs, pint, phpstan, psalm, rector | phpunit, pest | Symfony, Laravel |
80
+ | Ruby | bundler | rubocop, standard | rspec, minitest (SimpleCov) | Rails |
81
+ | .NET | dotnet | dotnet format | dotnet test (coverlet.msbuild) | |
82
+ | C / C++ | CMake (plain, Conan, vcpkg), Meson | clang-format, clang-tidy, cppcheck | ctest, meson test (gcovr; ASan + UBSan) | |
83
+
84
+ Frameworks add what the language alone can't know, for example:
85
+
86
+ - **Django**: `manage.py check` and a missing-migrations check in the lint job.
87
+ - **Symfony**: the console linters (container, YAML, Twig).
88
+ - **Rails**: Brakeman, plus preparing the test database.
89
+ - **Laravel**: `.env` and app key, then `php artisan test`.
90
+ - **Next.js**: build cache.
91
+ - **Nuxt, SvelteKit, Astro**: their own type checks instead of plain `tsc`.
92
+ - **Angular**: `ng test` with headless Chrome.
93
+
94
+ ### Detection
95
+
96
+ quoll reads your project instead of asking you about it:
97
+
98
+ | Detected | From |
99
+ |-------------------------|-------------------------------------------------------------------------------|
100
+ | Stack | `pyproject.toml`, `package.json`, `go.mod`, `Cargo.toml`, `pom.xml` / `build.gradle(.kts)`, `composer.json`, `Gemfile`, `*.sln` / `*.csproj`, `CMakeLists.txt` / `meson.build` |
101
+ | Framework | dependencies and entry points (`manage.py`, `bin/console`, `artisan`, `bin/rails`…) |
102
+ | Package manager | lockfiles (`uv.lock`, `poetry.lock`, `pnpm-lock.yaml`, `yarn.lock`, `bun.lock`…), `packageManager` |
103
+ | Runtime version | `.python-version`, `requires-python`, `.nvmrc`, `.node-version`, `engines`, `go.mod` |
104
+ | Lint tools | ruff / flake8 / black / mypy config, eslint / prettier / biome / tsconfig, package.json scripts |
105
+ | Test runner & coverage | pytest, vitest (+ coverage plugin), jest, bun, `go test` |
106
+ | Build | `[build-system]`, `build` script, `go build` |
107
+ | Branch & repository | `git` remote and default branch |
108
+ | Platform | the remote's host (github.com, gitlab.com, gitlab.&lt;company&gt;…), else an existing `.gitlab-ci.yml` |
109
+
110
+ It then asks only what it can't infer (coverage threshold, version matrix, Codecov, SonarCloud…), writes your choices to **`quoll.toml`** and generates the pipeline:
111
+
112
+ | Platform | Pipeline | Dependency updates (created only if missing) |
113
+ |---|---|---|
114
+ | GitHub | `.github/workflows/ci.yml` and `.github/actions/quoll-setup` (for your own workflows) | `.github/dependabot.yml` |
115
+ | GitLab | `.gitlab-ci.yml` (rules, includes) and `.gitlab/quoll-ci.yml` (jobs) | `renovate.json` |
116
+
117
+ ### Commands
118
+
119
+ | Command | What it does |
120
+ |--------------------|-------------------------------------------------------------------------------|
121
+ | `quoll ci init` | Detect, ask, write `quoll.toml` and the pipeline. `--yes` accepts the detected defaults (scripts, CI), `--platform github\|gitlab` overrides the detection, `--force` replaces a hand-written pipeline, `--local` skips the branch protection and secrets. |
122
+ | `quoll ci update` | Regenerate the pipeline from `quoll.toml`, showing the diff first. |
123
+ | `quoll ci check` | Exit 1 if the pipeline no longer matches `quoll.toml` — run it in CI or pre-commit. |
124
+ | `quoll ci detect` | Print what quoll detects, without writing anything. |
125
+ | `quoll ci schedule`| GitLab: create or update the pipeline schedule of the security scans. |
126
+ | `quoll ci eject` | Stop managing the CI: the files become yours, `[ci]` leaves `quoll.toml`. |
127
+
128
+ At the end of `init`, quoll can also, through the platform's API:
129
+
130
+ - protect the default branch: changes go through pull / merge requests, merged once the pipeline passed — on GitHub the **`CI result`** check (admins included, linear history, no force push); on GitLab no direct push, "Pipelines must succeed" and fast-forward merges. No approval is required by default (authors can't approve their own changes: a single developer would be locked out); a team adds it in the repository's settings;
131
+ - store what the pipeline needs (`.env.example` variables used by tests, `SONAR_TOKEN`, `CODECOV_TOKEN`) as GitHub secrets or masked GitLab CI/CD variables.
132
+
133
+ Tokens come from `QUOLL_GITHUB_TOKEN` / `QUOLL_GITLAB_TOKEN`, `gh auth token` / `glab`, or are asked once and stored in `~/.config/quoll/config.toml`. Self-managed GitLab instances are reached on the remote's host (`QUOLL_GITLAB_URL` for `quoll new --remote`, `QUOLL_GITLAB_GROUP` for the group of new projects).
134
+
135
+ ### The generated pipeline
136
+
137
+ | Job | What it does |
138
+ |---|---|
139
+ | **lint** | the language's formatters, linters and static analyzers (ruff/mypy, eslint/tsc, golangci-lint, clippy, phpstan/psalm, rubocop/Brakeman, clang-tidy/cppcheck…) plus the framework's checks |
140
+ | **test** | tests against one or several versions (matrix), with the backing services, a coverage threshold, sanitizers for C/C++; results (JUnit) and coverage shown in the pull/merge request |
141
+ | **audit** | known vulnerabilities in the dependencies (pip-audit, npm/pnpm/yarn/bun audit, govulncheck, cargo-audit, osv-scanner, composer audit, bundler-audit, `dotnet list package --vulnerable`) |
142
+ | **secrets** | gitleaks over the whole git history (allow-list: `.gitleaksignore`) |
143
+ | **sast** | Semgrep's default rules for every language of the repository (GitLab: report in the MR security widget) |
144
+ | **sbom** | after installing the dependencies: an SBOM (CycloneDX, pipeline artifact; GitLab's dependency list) and a license gate, failing on forbidden licenses (AGPL, SSPL…) by default |
145
+ | | *Licenses are read from the installed packages (`node_modules`, `.venv`, `vendor/`…). A free-text license Trivy can't classify counts as `UNKNOWN` (add it to `license_severity` to block those too); dependencies installed outside the project (pip into the system interpreter, Maven/Go caches) are only partly covered.* |
146
+ | **codeql** | GitHub CodeQL, results in the Security tab — optional, needs GitHub Advanced Security on private repositories |
147
+ | **sonar** | SonarCloud — optional |
148
+ | **build** | the project's build (package, binary, static site…); its output (`[ci.build] artifacts`: `dist/`, `target/*.jar`…) kept a week |
149
+ | **docker** | when there is a Dockerfile: BuildKit's Dockerfile checks, build, then a Trivy scan failing on fixable `CRITICAL`/`HIGH` vulnerabilities — before the image is ever published |
150
+
151
+ On GitHub Actions, a final **`CI result`** job is the single required status check, whatever the jobs and matrix entries:
152
+
153
+ - **Least privilege & hygiene:** `permissions: contents: read`, per-job timeouts, superseded PR runs cancelled, `merge_group` support for merge queues.
154
+ - **Reproducible installs:** lockfile-based (`uv sync --locked`, `npm ci`, `pnpm install --frozen-lockfile`…) with dependency caching.
155
+ - **Useful output:** linter annotations on the PR (ruff, phpstan, psalm, rubocop), failed tests annotated and listed in the job summary, coverage summary, coverage threshold enforced for every test runner that measures it.
156
+ - **Test reports** for every language: pytest, Vitest, Jest (jest-junit, installed outside the project), bun, gotestsum, nextest (`cargo test` has no stable JUnit output), PHPUnit/Pest, RSpec (its JUnit formatter, outside the bundle), Surefire/Gradle, `dotnet test` (TRX converted by trx2junit), CTest/Meson. Cobertura coverage where the runner measures it (Go through gocover-cobertura). Java's coverage: JaCoCo without any build file change (Maven goals, a Gradle init script), the threshold over every module's report.
157
+ - **Security:** dependency audit, secret scanning, Semgrep, optional CodeQL; jobs needing secrets (SonarCloud) skipped on fork PRs where they aren't available.
158
+ - **Supply chain:** every action pinned to a commit SHA (`actions/checkout@<sha> # v7`), proposed updates by Dependabot; the tool images (gitleaks, Semgrep, Trivy) pinned by version.
159
+
160
+ On GitLab CI, the same jobs run in the `test` stage, each in the language's official image (`python`, `ghcr.io/astral-sh/uv`, `node`, `oven/bun`, `golang`, `rust`, `eclipse-temurin` / `maven` / `gradle`, `php` + Composer and the `ext-*` the lock file needs, `ruby`, `dotnet/sdk`):
161
+
162
+ - **Pipelines:** merge requests, pushes to the configured branches, tags and manual runs, without duplicate branch + MR pipelines; a new commit cancels the running pipeline. The pipeline itself is the merge gate.
163
+ - **Caches** kept inside the project (`.venv`, `.npm`, `.go`, `.cargo`, `.m2`, `vendor/bundle`…), keyed on the lockfile, per job and matrix entry.
164
+ - **Matrix:** `parallel:matrix` over the versions, each entry with its resolved image.
165
+ - **Services** started as GitLab services; their variables are exported by the script, so a project CI/CD variable of the same name (which would override job variables) can't point the tests at a real database.
166
+ - **Merge request widgets:** test results (JUnit), coverage on the diff (Cobertura) and the total coverage parsed from the log (badge); Semgrep's report in the security widget (Ultimate).
167
+ - **Codecov** in its own job reading the test job's report; **SonarCloud** only where `SONAR_TOKEN` is available.
168
+ - **Docker layer cache** in the project's registry (`cache:<project>`), shared by the CI's docker job and the delivery; a pipeline without access to the registry (a fork) builds from scratch.
169
+ - **Retries** of the jobs lost to the infrastructure (runner, timeout, API) — never of a failing test, which would hide a flaky one.
170
+ - Steps that use a GitHub Action on GitHub (golangci-lint, govulncheck, cargo tools, osv-scanner…) run the equivalent CLI here.
171
+
172
+ ### `quoll.toml`
173
+
174
+ `quoll.toml` is the source of truth: edit it, then run `quoll ci update` (or `quoll docker update`). Every field is validated, with a clear error on typos. `[project]` is shared by the CI and the Docker image, so they can't disagree about the stack.
175
+
176
+ ```toml
177
+ [project]
178
+ platform = "github" # github | gitlab
179
+ stack = "python" # go | rust | java | dotnet | php | ruby | python | node
180
+ framework = "django" # optional, one of the stack's frameworks
181
+ package_manager = "uv" # pip | uv | poetry | npm | pnpm | yarn | yarn-classic | bun | go | ...
182
+ services = ["postgres", "redis"] # postgres | mysql | redis | mongodb — for the CI tests and compose.yaml
183
+
184
+ [ci]
185
+ versions = ["3.12", "3.13"] # several = test matrix; empty = read version_file (GitHub only)
186
+ version_file = "" # e.g. ".python-version", ".nvmrc", "go.mod"
187
+ install = "uv sync --locked --all-extras"
188
+ source_dir = "src" # measured by coverage, checked by mypy
189
+ branches = ["main"]
190
+ dependency_updates = true # Dependabot (GitHub) / Renovate (GitLab) config, created once
191
+
192
+ [ci.lint]
193
+ enabled = true
194
+ tools = ["ruff", "mypy", "django-check"] # language + framework tools; node also accepts "script:<name>"
195
+
196
+ [ci.test]
197
+ enabled = true
198
+ runner = "pytest"
199
+ coverage_threshold = 80
200
+ codecov = false
201
+ reports = true # JUnit results and Cobertura coverage in pull/merge requests
202
+ sanitizers = false # ASan + UBSan (C/C++; detected on)
203
+ env_secrets = ["DATABASE_URL"] # exposed to tests from GitHub secrets / GitLab CI/CD variables
204
+
205
+ [ci.security]
206
+ audit = true # dependency vulnerabilities
207
+ secrets = true # gitleaks
208
+ sast = true # Semgrep
209
+ codeql = false # GitHub only
210
+ sbom = true # CycloneDX SBOM of the dependencies
211
+ licenses = true # license gate (Trivy)
212
+ license_severity = ["CRITICAL"] # CRITICAL = forbidden (AGPL, SSPL…), HIGH = restricted (GPL, LGPL), MEDIUM = reciprocal (MPL)
213
+ sonar = false
214
+ sonar_organization = ""
215
+
216
+ [ci.build]
217
+ enabled = true
218
+ command = "uv build"
219
+ docker = true # lint, build and scan the image (on when there is a Dockerfile)
220
+ ```
221
+
222
+ ### Going beyond what quoll generates
223
+
224
+ quoll can't know every tool a project needs. The generated pipeline is one part of the CI, not all of it: three levels, pick the one that fits.
225
+
226
+ **1. `quoll.toml`** — the standard pipeline: versions, tools, thresholds, jobs on or off. Plus `[[ci.steps]]` to add a command, or a GitHub Action, to a generated job:
227
+
228
+ ```toml
229
+ [[ci.steps]]
230
+ job = "test" # lint, test, build, audit or sbom
231
+ when = "before" # before: once the dependencies are installed; after: at the end
232
+ name = "Migrate"
233
+ run = "python manage.py migrate"
234
+ secrets = ["DJANGO_SECRET_KEY"] # read from GitHub secrets / GitLab CI/CD variables
235
+
236
+ [[ci.steps]] # GitHub only: any action of the marketplace
237
+ job = "test"
238
+ when = "after"
239
+ name = "Upload screenshots"
240
+ uses = "actions/upload-artifact@v4"
241
+ with = { name = "screenshots", path = "screenshots/" }
242
+ ```
243
+
244
+ Or without writing TOML — the step is added, then the pipeline regenerated:
245
+
246
+ ```bash
247
+ quoll ci add-step --job test --when before --name Migrate --run "python manage.py migrate" --secret DJANGO_SECRET_KEY
248
+ quoll ci add-step -d back # in a workspace: the project's directory; asks what's missing
249
+ ```
250
+
251
+ **Keeping the files in sync**: `quoll ci check` (and `docker check`, `cd check`) fails when a generated file no longer matches `quoll.toml`. With [pre-commit](https://pre-commit.com), quoll's hooks regenerate them when a `quoll.toml` changes:
252
+
253
+ ```yaml
254
+ # .pre-commit-config.yaml
255
+ repos:
256
+ - repo: https://github.com/theo-mrn/Quoll
257
+ rev: v0.2.0
258
+ hooks:
259
+ - id: quoll-ci # quoll ci update; also quoll-docker, quoll-cd
260
+ ```
261
+
262
+ **2. Your own CI, in plain YAML, next to quoll's** — any tool, any action, any runner. quoll gives it the project's environment so you don't rebuild the toolchain setup:
263
+
264
+ - **GitHub**: write your workflows in `.github/workflows/` (quoll only owns `ci.yml`), and start your jobs with the generated action:
265
+
266
+ ```yaml
267
+ steps:
268
+ - uses: actions/checkout@v4
269
+ - uses: ./.github/actions/quoll-setup # toolchain, caches, dependencies — as quoll's CI
270
+ with: { version: "3.13" } # optional
271
+ - uses: microsoft/playwright-github-action@v1
272
+ - run: npx playwright test
273
+ ```
274
+
275
+ - **GitLab**: write `.gitlab/custom-ci.yml`, included last. Your jobs extend the generated `.quoll-setup` template, and any key of quoll's jobs can be overridden there (maps such as `variables` are merged, lists such as `script` replaced):
276
+
277
+ ```yaml
278
+ e2e:
279
+ extends: .quoll-setup # image, caches, dependencies
280
+ script: [npx playwright test]
281
+
282
+ test: # quoll's test job, completed
283
+ tags: [gpu]
284
+ timeout: 45 minutes
285
+ variables: { FEATURE_FLAGS: "on" }
286
+ ```
287
+
288
+ **3. `quoll ci eject`** — the pipeline files become yours, as they are (quoll's header removed), and `[ci]` leaves `quoll.toml`. quoll stops managing the CI; the Docker image and the delivery stay managed.
289
+
290
+ ### Scheduled security scans
291
+
292
+ New vulnerabilities appear without any code change. Every week, the audit, secret scanning, Semgrep, CodeQL, SBOM/licenses and image scan run again on the default branch (lint, tests and build don't):
293
+
294
+ ```toml
295
+ [ci.schedule]
296
+ enabled = true
297
+ cron = "0 6 * * 1" # UTC — Mondays at 06:00
298
+ ```
299
+
300
+ On GitHub the schedule is part of the workflow. On GitLab it is a pipeline schedule, created by `quoll ci init` (or `quoll ci schedule` after changing the cron) through the API. A failed scheduled run notifies like any failed pipeline.
301
+
302
+ ## Monorepos
303
+
304
+ A repository holding several projects — a Next.js front-end and a FastAPI back-end, services in Go and Java… — is a **workspace**. `quoll ci init` at its root finds them (first-level directories, and `apps/*`, `packages/*`, `services/*`) and sets it up:
305
+
306
+ ```
307
+ shop/
308
+ ├── quoll.toml # the workspace: its members, and what they share
309
+ ├── backend/quoll.toml # a project, in the single-project format: [project], [ci], [docker], [cd]
310
+ └── frontend/quoll.toml
311
+ ```
312
+
313
+ ```toml
314
+ # quoll.toml at the root
315
+ [project]
316
+ platform = "github"
317
+
318
+ [workspace]
319
+ members = ["backend", "frontend"]
320
+
321
+ [ci] # the repository's: triggers and repository-wide scans
322
+ branches = ["main"]
323
+
324
+ [ci.schedule]
325
+ cron = "0 6 * * 1"
326
+
327
+ [ci.security]
328
+ secrets = true # gitleaks, once for the whole repository
329
+ sast = true # Semgrep, once
330
+ ```
331
+
332
+ Each member keeps the single-project format, so a project moved out of the repository stays a valid quoll project. In a workspace its `branches`, `schedule`, `secrets` and `sast` are the root's.
333
+
334
+ - **One pipeline**: every member's jobs, prefixed by its name (`backend-test`, `frontend-lint`…) and run in its directory; on GitHub, one `CI result` gate for all.
335
+ - **Only what changed runs**: a pull request touching `frontend/` doesn't run the back-end's jobs. A change to the root `quoll.toml` or to the CI runs everything, as do tags, scheduled and manual runs. GitLab uses `rules: changes`; GitHub a `changes` job (plain `git diff`, no third-party action).
336
+ - **Per member**: Dockerfile, `compose.yaml`, image and delivery (`quoll docker init` / `quoll cd init` at the root do every member). GitHub: one `cd-<member>.yml` per image, run when its directory changed; GitLab: `<member>-publish` jobs, and one release carrying every SBOM.
337
+ - **A root `compose.yaml`** running every member and their databases together: see [Microservices](#microservices).
338
+ - **Building blocks**: `.github/actions/quoll-setup-<member>` / `.quoll-setup-<member>` for your own jobs.
339
+ - Every command works from the root or from a member's directory.
340
+
341
+ ## Docker
342
+
343
+ ```bash
344
+ quoll docker init # detect, ask the port / start command if needed, write Dockerfile, .dockerignore, compose.yaml
345
+ quoll docker update # regenerate from quoll.toml, showing the diff first
346
+ quoll docker check # exit 1 if the Dockerfile no longer matches quoll.toml
347
+ ```
348
+
349
+ Every image is multi-stage, runs as a non-root user, installs dependencies from the lockfile in their own cached layer (BuildKit cache mounts) and ships only what runs. Runtime images on Debian, Ubuntu or Alpine get the OS security fixes released since their base image was built (distroless ones have no package manager: rebuilding picks up Google's). Each framework gets an image that fits it:
350
+
351
+ | Stack | Build | Runtime image |
352
+ |---|---|---|
353
+ | Python (pip, uv, poetry) | virtualenv built in a builder stage | `python:<v>-slim`; gunicorn for Django and Flask, uvicorn for FastAPI (app module found in the code) |
354
+ | Node | lockfile install, build, devDependencies pruned | `node:<v>-slim`, or `oven/bun` |
355
+ | Next.js | `output: "standalone"` when enabled | only the standalone server, `.next/static` and `public/` |
356
+ | Nuxt | Nitro build | only `.output/` |
357
+ | Vite SPA, Angular, Astro, SvelteKit (static) | static build | `nginx-unprivileged` with SPA fallback (or 404 page) |
358
+ | SvelteKit / Astro (node adapter), Angular SSR, NestJS | build | Node server |
359
+ | Go | static binary (`CGO_ENABLED=0`, trimmed) | `distroless/static:nonroot` |
360
+ | Rust | release binary (system OpenSSL when needed) | `distroless/cc:nonroot` |
361
+ | Java / Kotlin | Maven/Gradle, wrapper or official image | `eclipse-temurin:<v>-jre`; Spring Boot jar extracted into cache-friendly layers |
362
+ | PHP, Symfony, Laravel | Composer without dev dependencies, optimized autoloader, framework scripts | FrankenPHP with the `ext-*` from composer.json |
363
+ | Ruby, Rails | Bundler deployment mode, system packages for native gems (pg, mysql2, vips) | `ruby:<v>-slim`; Rails: bootsnap, asset precompilation, `bin/docker-entrypoint`, writable dirs only |
364
+ | .NET | `dotnet publish` of the app project | `aspnet` or `runtime`, built-in non-root user |
365
+ | C / C++ | CMake or Meson release build (Conan / vcpkg dependencies), libstdc++ linked statically | `distroless/cc:nonroot` |
366
+
367
+ ```toml
368
+ [docker]
369
+ version = "3.13" # runtime version of the base image
370
+ port = 8000
371
+ command = "gunicorn shop.wsgi --bind 0.0.0.0:8000" # what the container runs
372
+ build_command = "" # build step, e.g. "npm run build"
373
+ output_dir = "" # what the build produces (static site or self-contained server)
374
+ static = false # serve output_dir with nginx
375
+ ```
376
+
377
+ ## Services
378
+
379
+ quoll reads which databases and caches the app talks to from its client libraries:
380
+
381
+ | Service | Detected from (examples) |
382
+ |---|---|
383
+ | Postgres | psycopg, asyncpg, pg, Prisma `provider = "postgresql"`, pgx, sqlx, org.postgresql, pdo_pgsql, Symfony/Laravel `.env`, pg gem, Npgsql |
384
+ | MySQL | mysqlclient, mysql2, go-sql-driver, mysql-connector, pdo_mysql, trilogy, MySqlConnector |
385
+ | Redis | redis, ioredis, go-redis, jedis/lettuce, predis, StackExchange.Redis |
386
+ | MongoDB | pymongo, mongoose, mongo-driver, mongoid, MongoDB.Driver |
387
+
388
+ They are listed in `[project] services` and used twice:
389
+
390
+ - **CI**: the test job starts them as service containers and waits until they are healthy. It sets `DATABASE_URL`, `REDIS_URL`, `MONGODB_URI` and the `PG*` variables, plus the framework's own: Laravel's `DB_*`, Spring's `SPRING_DATASOURCE_*`, `mysql2://` for Rails. A GitHub secret or GitLab CI/CD variable with the same name is never used for tests.
391
+ - **compose.yaml** (written by `quoll docker init`): the app plus its services, with healthchecks, `depends_on: service_healthy`, persistent volumes and an optional `.env` for secrets. `docker compose up --build` runs everything.
392
+
393
+ ## Delivery (CD)
394
+
395
+ ```bash
396
+ quoll cd init # registry, image name → .github/workflows/cd.yml or .gitlab/cd.yml
397
+ quoll cd update / check
398
+ ```
399
+
400
+ Every push to the default branch and every `v*` tag:
401
+
402
+ 1. builds the image (BuildKit);
403
+ 2. **scans it with Trivy before anything is pushed**, failing on fixable `CRITICAL`/`HIGH` vulnerabilities;
404
+ 3. pushes it with tags `:<branch>`, `:sha-<sha>`, `:latest` (default branch) and `:1.2.3` / `:1.2` (release tags), plus BuildKit's provenance and SBOM attestations;
405
+ 4. generates the SBOM of the pushed digest (CycloneDX, pipeline artifact) and, on `v*` tags, attaches it to the GitHub / GitLab release;
406
+ 5. signs the image and its SBOM with Sigstore: GitHub artifact attestations (`gh attestation verify oci://<image> --owner <org>`), cosign keyless on GitLab (`cosign verify <image> --certificate-identity-regexp … --certificate-oidc-issuer https://gitlab.com`).
407
+
408
+ | | GitHub | GitLab |
409
+ |---|---|---|
410
+ | Pipeline | `.github/workflows/cd.yml` | `.gitlab/cd.yml`, included by quoll's `.gitlab-ci.yml` |
411
+ | Runs after CI | through branch protection | always: its `publish` stage starts once every CI job passed |
412
+ | Default registry (no secret) | GHCR, with the workflow's token | the project's container registry, with the job token |
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) |
415
+
416
+ Docker Hub (`registry = "dockerhub"`) needs `DOCKERHUB_USERNAME` and `DOCKERHUB_TOKEN`. Multi-arch images are a `platforms` entry away.
417
+
418
+ ```toml
419
+ [cd]
420
+ registry = "ghcr" # ghcr (GitHub) | gitlab (GitLab) | dockerhub
421
+ image = "ghcr.io/acme/api"
422
+ branches = ["main"]
423
+ release_tags = true
424
+ platforms = ["linux/amd64"] # e.g. ["linux/amd64", "linux/arm64"]
425
+ scan = true
426
+ severity = ["CRITICAL", "HIGH"]
427
+ ignore_unfixed = true
428
+ 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
+ ```
431
+
432
+ 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
+ ## Kubernetes
435
+
436
+ ```bash
437
+ quoll k8s init # [k8s] in quoll.toml, a Helm chart per project; the CI checks it, the delivery deploys it
438
+ quoll k8s update # regenerate from quoll.toml, showing the diff first
439
+ quoll k8s check # exit 1 if a chart no longer matches quoll.toml
440
+ ```
441
+
442
+ Each project gets a Helm chart in `chart/`. The templates are the same everywhere; the values come from `quoll.toml`:
443
+
444
+ ```
445
+ back/chart/
446
+ ├── Chart.yaml
447
+ ├── values.yaml # image, port, health check, resources, URLs of the other projects…
448
+ ├── values-staging.yaml # what an environment changes: replicas, public host
449
+ ├── values-production.yaml
450
+ └── templates/ # Deployment, Service, ServiceAccount, Ingress, PodDisruptionBudget, HPA
451
+ ```
452
+
453
+ ```toml
454
+ [k8s]
455
+ name = "back" # release, Service and Deployment: the others reach it at http://back:3000
456
+ health_path = "/health" # found in the code (/health, /healthz…); empty: TCP checks on the port
457
+ cpu = "100m" # requests; memory_limit caps the memory (no CPU limit: no throttling)
458
+ memory = "256Mi"
459
+ memory_limit = "512Mi"
460
+ max_replicas = 0 # > 0: a HorizontalPodAutoscaler up to this
461
+ ingress_class = "" # the cluster's default
462
+ cluster_issuer = "" # cert-manager ClusterIssuer for the TLS certificate
463
+ secret = "" # Secret with the app's secrets (DATABASE_URL…), default "<name>-env"
464
+
465
+ [k8s.environments.staging]
466
+ namespace = "shop-staging"
467
+ deploy_on = "branch" # every push to the [cd] branches
468
+ host = "api.staging.example.com" # Ingress; empty: reachable inside the cluster only
469
+ replicas = 1
470
+
471
+ [k8s.environments.production]
472
+ namespace = "shop"
473
+ deploy_on = "tag" # v* tags, after an approval
474
+ replicas = 2
475
+ ```
476
+
477
+ **Secure by default** — the pods run as non-root (every image's user is numeric, so Kubernetes can check it), with a read-only root filesystem (the directories the app writes to — `/tmp`, nginx's cache, Next.js' `.next/cache`… — are `emptyDir` volumes; Laravel, Symfony and Ruby apps keep a writable one: the frameworks write in their own tree, and Ruby refuses `emptyDir`'s world-writable temporary directory), all capabilities dropped, no privilege escalation, `RuntimeDefault` seccomp, no service account token. Rolling updates never remove a ready pod before its replacement is; with two replicas or more, a PodDisruptionBudget keeps one through node drains.
478
+
479
+ **In the CI**, a `chart` job lints the chart with each environment's values, checks the rendered manifests against Kubernetes' schemas (kubeconform) and scans them for misconfigurations (Trivy), failing on `HIGH`/`CRITICAL`.
480
+
481
+ **In the delivery** (with `quoll cd`), once the image is pushed, scanned and signed, a job per environment deploys it **by digest** — what was scanned is what runs:
482
+
483
+ | | GitHub | GitLab |
484
+ |---|---|---|
485
+ | Staging | `deploy-staging`, on pushes to the delivered branches | `deploy-staging` (stage `deploy`), same |
486
+ | Production | `deploy-production`, on `v*` tags — protect the `production` environment with required reviewers | `deploy-production`, on `v*` tags, started by hand (protect the environment to choose who can) |
487
+ | Cluster access | `KUBECONFIG` secret of each GitHub environment (the kubeconfig's content) | `KUBECONFIG` CI/CD variable of type *File*, scoped to each environment |
488
+ | Before deploying | the image's provenance: `gh attestation verify` | its signature: `cosign verify` (this project's pipelines) |
489
+ | Deploying | `helm upgrade --install --rollback-on-failure`: waits until the release is ready, rolls it back otherwise | same |
490
+
491
+ The app's secrets live in the cluster: `kubectl create secret generic back-env --from-env-file=.env.production -n shop`. A private image needs a pull secret (`pull_secret` in `[k8s]`).
492
+
493
+ In a **workspace**, every project deploys to the same namespaces and reaches the others at their Service, `<NAME>_URL=http://<name>:<port>` — the same variables as the root `compose.yaml`, so the front-end's `/api/` proxy works unchanged. `quoll add service` gives a new service its chart (same namespaces), `quoll remove service` drops it from the others' values. `quoll new --k8s` sets it all up from the start.
494
+
495
+ ## Creating a project
496
+
497
+ ```bash
498
+ quoll new # interactive
499
+ quoll new shop --front angular --back express
500
+ quoll new shop --front angular@18 --back express@4 --node 20 --platform gitlab --cd --k8s --remote
501
+ quoll new shop --front angular --back none --service orders=express --service users=express@4
502
+ ```
503
+
504
+ From nothing to a repository whose pipeline passes on its first push:
505
+
506
+ 1. creates the front-end in `front/`, the back-end in `back/` and each `--service` in `services/<name>/` (only one of them: the repository is the project);
507
+ 2. writes, for each, its Dockerfile and `compose.yaml`; then the workspace's CI, its root `compose.yaml`, with `--k8s` a Helm chart each, and with `--cd` the delivery (which then deploys them);
508
+ 3. `git init`, a first commit — and with `--remote`, the GitHub/GitLab repository, pushed, `main` protected.
509
+
510
+ | Starter | Role | How |
511
+ |---|---|---|
512
+ | `angular` | front | Angular's own CLI (`ng new`) at the chosen major — `angular@18` — plus ESLint (angular-eslint) |
513
+ | `express` | back | quoll's template: Express (`express@4` for 4) and TypeScript, Vitest + Supertest, ESLint, `/health`, graceful shutdown |
514
+
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.
516
+
517
+ ### Microservices
518
+
519
+ ```bash
520
+ quoll add service billing=express # services/billing/, from the root or any member
521
+ quoll add service billing=express --in apps # apps/billing/
522
+ ```
523
+
524
+ `quoll add service` creates the project, its Dockerfile and `quoll.toml`, adds it to `[workspace] members`, and regenerates what depends on the members: the pipeline (`billing-lint`, `billing-test`, `billing-docker`…), the root `compose.yaml`, Dependabot's config (unless edited by hand) and, when the other members publish images, its own delivery. Files you wrote yourself are left alone, with a warning.
525
+
526
+ ```bash
527
+ quoll remove service billing # deletes services/billing/ (asks first; -y doesn't)
528
+ quoll remove service billing --keep-files # only takes it out of the workspace
529
+ ```
530
+
531
+ `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
+
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:
534
+
535
+ ```toml
536
+ # front/quoll.toml
537
+ [docker.proxy]
538
+ "/api/" = "BACK_URL" # path prefix = environment variable holding the API's URL (no path)
539
+ ```
540
+
541
+ Without `BACK_URL`, the container stops at startup saying what to set (in production: the API's address in your platform).
542
+
543
+ The root `compose.yaml` (written by `quoll docker init|update` in a workspace, checked by `quoll docker check`) runs the whole platform with `docker compose up --build`:
544
+
545
+ - every member on one network, each reaching the others at `<NAME>_URL` (`BILLING_URL=http://billing:3000`), and listening on its own host port (3000, 3001… when they share one);
546
+ - **one** instance of each backing service, with **a database per member** on it (`orders`, `users`…, Postgres and MySQL), created by a one-shot container at every `up` when missing — so a service added later gets its own too; Redis and MongoDB are shared as they are;
547
+ - each member's `.env` (optional) for its secrets.
548
+
549
+ **Adding a starter** is a plugin's business: a `Starter` in its `starters=(...)`, either a generator (`starter.py` calling the framework's CLI through `quoll.starters.run`, which uses the local tool or a container) or a template directory next to the plugin (`templates/<name>/`, copied with `{{name}}`, `{{version}}`, `{{runtime}}` filled in, then commands such as `npm install`).
550
+
551
+ `quoll new` replaces the scaffolding of 0.1.x (`quoll init`, `quoll create-project`, `quoll list-types`), removed.
552
+
553
+ ## Development
554
+
555
+ ```bash
556
+ uv venv && uv pip install -e ".[dev]"
557
+ .venv/bin/pytest # validates every generated workflow with actionlint, every GitLab pipeline with its schema
558
+ .venv/bin/quoll ci check # quoll's own CI is generated by quoll
559
+
560
+ QUOLL_DOCKER_TESTS=check .venv/bin/pytest tests/docker # + lint every Dockerfile with `docker build --check`
561
+ QUOLL_DOCKER_TESTS=build .venv/bin/pytest tests/docker # + build and run real apps for each stack (slow)
562
+ QUOLL_GITLAB_TESTS=run .venv/bin/pytest tests/platforms # + run real projects' GitLab pipelines with gitlab-ci-local (slow)
563
+ ```
564
+
565
+ ### Architecture
566
+
567
+ Everything language-, framework- or platform-specific lives in its own folder; the rest of quoll never names one.
568
+
569
+ ```
570
+ quoll/
571
+ ├── project.py # safe file access for detectors (missing = empty, invalid = warning)
572
+ ├── languages/
573
+ │ ├── __init__.py # registry: built-ins + third-party plugins (entry points)
574
+ │ ├── base.py # the contract: Language, Framework, Detection, CISteps, FrameworkCI, DockerImage
575
+ │ ├── python/
576
+ │ │ ├── __init__.py # `language = Language(...)`: markers, package managers, tools, frameworks
577
+ │ │ ├── detect.py # Project -> Detection
578
+ │ │ ├── ci.py # CIConfig -> neutral steps (install, lint, test, audit, build) + toolchain:
579
+ │ │ │ # setup actions (GitHub), image and cache (GitLab)
580
+ │ │ ├── docker.py # DockerImage: defaults (port, command...) and template context
581
+ │ │ ├── Dockerfile.j2 # multi-stage template, split into blocks (build, runtime...)
582
+ │ │ └── frameworks/
583
+ │ │ └── django/
584
+ │ │ ├── __init__.py # `framework = Framework(...)`: extra lint tools / test runners
585
+ │ │ ├── detect.py # (Project, Detection) -> bool, refining the detection
586
+ │ │ ├── ci.py # FrameworkCI: only the hooks it needs (env, lint, test, before_build)
587
+ │ │ └── docker.py # subclass of the language's image (+ a Dockerfile.j2 extending it if needed)
588
+ │ ├── node/ go/ rust/ java/ php/ ruby/ dotnet/ cpp/
589
+ ├── platforms/
590
+ │ ├── base.py # the contract: Platform (pipelines), PlatformAPI (repo, secrets, protection)
591
+ │ ├── github/ # workflows, gate job, dependabot, build → scan → push, REST API
592
+ │ └── gitlab/ # .gitlab-ci.yml, renovate.json, .gitlab/cd.yml, REST API
593
+ ├── workspace.py # monorepos: members, the repository's settings (Repo), discovery
594
+ ├── ci/ # [ci] config, detection, the neutral Step / Cache model (steps.py), and the
595
+ │ # language-independent tools: gitleaks, Semgrep, image checks (tools.py)
596
+ ├── docker/ # language-agnostic: [docker] config, Dockerfile, .dockerignore, compose.yaml
597
+ ├── cd/ # [cd] config
598
+ ├── k8s/ # [k8s] config, the Helm chart (fixed templates + generated values), its CI checks
599
+ │ # and deploy commands, shared by both platforms
600
+ ├── services.py # postgres, mysql, redis, mongodb: containers, healthchecks, app variables
601
+ └── config/quoll_toml.py # quoll.toml: shared [project] section + one section per feature
602
+ ```
603
+
604
+ ### Adding a language
605
+
606
+ 1. Create `quoll/languages/<name>/` with:
607
+ - `detect.py`: a `detect(project) -> Detection` function, which also fills `services` from the client libraries (`quoll.services.detect_services`);
608
+ - `ci.py`: a `CISteps` subclass. `setup` installs the toolchain on GitHub, `image` (and optionally `image_setup`, `cache`) provides it on GitLab; every other step that uses a GitHub Action also gives the equivalent `run` command;
609
+ - `docker.py` and `Dockerfile.j2`: a `DockerImage` subclass and its template;
610
+ - `__init__.py`: exposes `language = Language(...)`.
611
+ 2. Add `<name>` to `_BUILTIN` in `quoll/languages/__init__.py`. Its position sets the detection priority.
612
+ 3. Add a sample project to `SAMPLE_BY_LANGUAGE` in `tests/samples.py` and the language's own tests to `tests/languages/test_<name>.py`.
613
+
614
+ ### Adding a framework
615
+
616
+ 1. Create `quoll/languages/<language>/frameworks/<name>/` with:
617
+ - `detect.py`: returns True when the framework is used, and may adjust the detection;
618
+ - `ci.py` (optional): a `FrameworkCI` subclass overriding only the hooks it needs;
619
+ - `docker.py` (optional): a subclass of the language's image overriding `defaults` or `context`. Add a `Dockerfile.j2` only if the image differs: it starts with `{% extends "<language>/Dockerfile.j2" %}` and overrides only the blocks that change;
620
+ - `services.py` (optional): a `service_env(service, host)` function, when the framework reads its database settings from its own variables;
621
+ - `__init__.py`: exposes `framework = Framework(...)`.
622
+ 2. List it in the language's `frameworks=(...)`. Order sets the detection priority.
623
+ 3. Add a sample project to `SAMPLE_BY_FRAMEWORK`.
624
+
625
+ The contract tests (`tests/languages/test_contract.py`) then check that every sample is detected correctly. They also render the pipelines of both platforms for every declared package manager, lint tool and test runner, and validate each one (actionlint, GitLab's schema). quoll.toml validation, `quoll ci detect`, the prompts and dependabot pick up new languages and frameworks automatically.
626
+
627
+ A separate package can also ship a language or a framework without changing quoll:
628
+
629
+ ```toml
630
+ # pyproject.toml of the plugin
631
+ [project.entry-points."quoll.languages"]
632
+ elixir = "quoll_elixir:language"
633
+
634
+ [project.entry-points."quoll.frameworks"]
635
+ remix = "quoll_remix:framework" # Framework(language="node", ...)
636
+ ```
637
+
638
+ ## Requirements
639
+
640
+ - Python 3.9+
641
+ - A GitHub or GitLab repository (optional, for branch protection and secrets)