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.
- quoll/__init__.py +0 -0
- quoll/cd/__init__.py +1 -0
- quoll/cd/config.py +75 -0
- quoll/ci/__init__.py +0 -0
- quoll/ci/config.py +213 -0
- quoll/ci/custom.py +32 -0
- quoll/ci/detect.py +139 -0
- quoll/ci/steps.py +85 -0
- quoll/ci/tools.py +104 -0
- quoll/commands/__init__.py +0 -0
- quoll/commands/_common.py +145 -0
- quoll/commands/_workspace.py +83 -0
- quoll/commands/add.py +104 -0
- quoll/commands/add_secret.py +75 -0
- quoll/commands/cd.py +214 -0
- quoll/commands/ci.py +521 -0
- quoll/commands/docker.py +244 -0
- quoll/commands/k8s.py +211 -0
- quoll/commands/new.py +337 -0
- quoll/commands/remove.py +79 -0
- quoll/config/__init__.py +0 -0
- quoll/config/quoll_toml.py +196 -0
- quoll/config/settings.py +22 -0
- quoll/config/store.py +39 -0
- quoll/console.py +3 -0
- quoll/docker/__init__.py +1 -0
- quoll/docker/config.py +64 -0
- quoll/docker/detect.py +41 -0
- quoll/docker/render.py +162 -0
- quoll/files.py +64 -0
- quoll/k8s/__init__.py +0 -0
- quoll/k8s/chart/templates/_helpers.tpl +38 -0
- quoll/k8s/chart/templates/deployment.yaml +93 -0
- quoll/k8s/chart/templates/hpa.yaml +24 -0
- quoll/k8s/chart/templates/ingress.yaml +35 -0
- quoll/k8s/chart/templates/pdb.yaml +16 -0
- quoll/k8s/chart/templates/service.yaml +18 -0
- quoll/k8s/chart/templates/serviceaccount.yaml +10 -0
- quoll/k8s/ci.py +39 -0
- quoll/k8s/config.py +97 -0
- quoll/k8s/deploy.py +22 -0
- quoll/k8s/detect.py +32 -0
- quoll/k8s/render.py +94 -0
- quoll/languages/__init__.py +121 -0
- quoll/languages/base.py +350 -0
- quoll/languages/cpp/Dockerfile.j2 +37 -0
- quoll/languages/cpp/__init__.py +17 -0
- quoll/languages/cpp/ci.py +192 -0
- quoll/languages/cpp/detect.py +84 -0
- quoll/languages/cpp/docker.py +47 -0
- quoll/languages/dotnet/Dockerfile.j2 +21 -0
- quoll/languages/dotnet/__init__.py +17 -0
- quoll/languages/dotnet/ci.py +88 -0
- quoll/languages/dotnet/detect.py +53 -0
- quoll/languages/dotnet/docker.py +47 -0
- quoll/languages/go/Dockerfile.j2 +25 -0
- quoll/languages/go/__init__.py +17 -0
- quoll/languages/go/ci.py +126 -0
- quoll/languages/go/detect.py +30 -0
- quoll/languages/go/docker.py +40 -0
- quoll/languages/java/Dockerfile.j2 +28 -0
- quoll/languages/java/__init__.py +20 -0
- quoll/languages/java/ci.py +163 -0
- quoll/languages/java/detect.py +77 -0
- quoll/languages/java/docker.py +45 -0
- quoll/languages/java/frameworks/__init__.py +1 -0
- quoll/languages/java/frameworks/spring_boot/Dockerfile.j2 +18 -0
- quoll/languages/java/frameworks/spring_boot/__init__.py +10 -0
- quoll/languages/java/frameworks/spring_boot/detect.py +9 -0
- quoll/languages/java/frameworks/spring_boot/docker.py +19 -0
- quoll/languages/java/frameworks/spring_boot/services.py +24 -0
- quoll/languages/node/Dockerfile.j2 +98 -0
- quoll/languages/node/__init__.py +29 -0
- quoll/languages/node/ci.py +200 -0
- quoll/languages/node/detect.py +164 -0
- quoll/languages/node/docker.py +141 -0
- quoll/languages/node/frameworks/__init__.py +1 -0
- quoll/languages/node/frameworks/angular/__init__.py +18 -0
- quoll/languages/node/frameworks/angular/ci.py +34 -0
- quoll/languages/node/frameworks/angular/detect.py +20 -0
- quoll/languages/node/frameworks/angular/docker.py +31 -0
- quoll/languages/node/frameworks/angular/starter.py +74 -0
- quoll/languages/node/frameworks/astro/__init__.py +10 -0
- quoll/languages/node/frameworks/astro/ci.py +17 -0
- quoll/languages/node/frameworks/astro/detect.py +14 -0
- quoll/languages/node/frameworks/astro/docker.py +17 -0
- quoll/languages/node/frameworks/nestjs/__init__.py +7 -0
- quoll/languages/node/frameworks/nestjs/detect.py +9 -0
- quoll/languages/node/frameworks/nestjs/docker.py +10 -0
- quoll/languages/node/frameworks/nextjs/Dockerfile.j2 +14 -0
- quoll/languages/node/frameworks/nextjs/__init__.py +8 -0
- quoll/languages/node/frameworks/nextjs/ci.py +29 -0
- quoll/languages/node/frameworks/nextjs/detect.py +13 -0
- quoll/languages/node/frameworks/nextjs/docker.py +41 -0
- quoll/languages/node/frameworks/nuxt/__init__.py +10 -0
- quoll/languages/node/frameworks/nuxt/ci.py +17 -0
- quoll/languages/node/frameworks/nuxt/detect.py +17 -0
- quoll/languages/node/frameworks/nuxt/docker.py +24 -0
- quoll/languages/node/frameworks/sveltekit/__init__.py +15 -0
- quoll/languages/node/frameworks/sveltekit/ci.py +15 -0
- quoll/languages/node/frameworks/sveltekit/detect.py +18 -0
- quoll/languages/node/frameworks/sveltekit/docker.py +18 -0
- quoll/languages/node/starter.py +42 -0
- quoll/languages/node/templates/express/README.md +11 -0
- quoll/languages/node/templates/express/env.example +2 -0
- quoll/languages/node/templates/express/eslint.config.js +15 -0
- quoll/languages/node/templates/express/gitignore +4 -0
- quoll/languages/node/templates/express/nvmrc +1 -0
- quoll/languages/node/templates/express/package.json +16 -0
- quoll/languages/node/templates/express/prettierignore +6 -0
- quoll/languages/node/templates/express/src/app.ts +27 -0
- quoll/languages/node/templates/express/src/server.ts +11 -0
- quoll/languages/node/templates/express/test/app.test.ts +29 -0
- quoll/languages/node/templates/express/tsconfig.build.json +8 -0
- quoll/languages/node/templates/express/tsconfig.json +14 -0
- quoll/languages/node/templates/express/vitest.config.ts +12 -0
- quoll/languages/php/Dockerfile.j2 +40 -0
- quoll/languages/php/__init__.py +19 -0
- quoll/languages/php/ci.py +147 -0
- quoll/languages/php/detect.py +75 -0
- quoll/languages/php/docker.py +59 -0
- quoll/languages/php/frameworks/__init__.py +1 -0
- quoll/languages/php/frameworks/laravel/__init__.py +17 -0
- quoll/languages/php/frameworks/laravel/ci.py +24 -0
- quoll/languages/php/frameworks/laravel/detect.py +13 -0
- quoll/languages/php/frameworks/laravel/docker.py +29 -0
- quoll/languages/php/frameworks/laravel/services.py +25 -0
- quoll/languages/php/frameworks/symfony/__init__.py +15 -0
- quoll/languages/php/frameworks/symfony/ci.py +22 -0
- quoll/languages/php/frameworks/symfony/detect.py +17 -0
- quoll/languages/php/frameworks/symfony/docker.py +25 -0
- quoll/languages/python/Dockerfile.j2 +61 -0
- quoll/languages/python/__init__.py +19 -0
- quoll/languages/python/ci.py +157 -0
- quoll/languages/python/detect.py +175 -0
- quoll/languages/python/docker.py +80 -0
- quoll/languages/python/frameworks/__init__.py +1 -0
- quoll/languages/python/frameworks/django/__init__.py +16 -0
- quoll/languages/python/frameworks/django/ci.py +43 -0
- quoll/languages/python/frameworks/django/detect.py +17 -0
- quoll/languages/python/frameworks/django/docker.py +20 -0
- quoll/languages/python/frameworks/fastapi/__init__.py +7 -0
- quoll/languages/python/frameworks/fastapi/detect.py +9 -0
- quoll/languages/python/frameworks/fastapi/docker.py +21 -0
- quoll/languages/python/frameworks/flask/__init__.py +7 -0
- quoll/languages/python/frameworks/flask/detect.py +9 -0
- quoll/languages/python/frameworks/flask/docker.py +22 -0
- quoll/languages/ruby/Dockerfile.j2 +45 -0
- quoll/languages/ruby/__init__.py +19 -0
- quoll/languages/ruby/ci.py +110 -0
- quoll/languages/ruby/detect.py +72 -0
- quoll/languages/ruby/docker.py +63 -0
- quoll/languages/ruby/frameworks/__init__.py +1 -0
- quoll/languages/ruby/frameworks/rails/Dockerfile.j2 +16 -0
- quoll/languages/ruby/frameworks/rails/__init__.py +18 -0
- quoll/languages/ruby/frameworks/rails/ci.py +26 -0
- quoll/languages/ruby/frameworks/rails/detect.py +15 -0
- quoll/languages/ruby/frameworks/rails/docker.py +62 -0
- quoll/languages/ruby/frameworks/rails/services.py +10 -0
- quoll/languages/rust/Dockerfile.j2 +23 -0
- quoll/languages/rust/__init__.py +17 -0
- quoll/languages/rust/ci.py +125 -0
- quoll/languages/rust/detect.py +46 -0
- quoll/languages/rust/docker.py +38 -0
- quoll/main.py +50 -0
- quoll/platforms/__init__.py +62 -0
- quoll/platforms/base.py +103 -0
- quoll/platforms/github/__init__.py +4 -0
- quoll/platforms/github/actions.py +78 -0
- quoll/platforms/github/api.py +124 -0
- quoll/platforms/github/cd.py +265 -0
- quoll/platforms/github/ci.py +497 -0
- quoll/platforms/github/dependabot.yml.j2 +49 -0
- quoll/platforms/github/platform.py +52 -0
- quoll/platforms/github/workflow.py +74 -0
- quoll/platforms/gitlab/__init__.py +4 -0
- quoll/platforms/gitlab/api.py +158 -0
- quoll/platforms/gitlab/cd.py +226 -0
- quoll/platforms/gitlab/ci.py +400 -0
- quoll/platforms/gitlab/pipeline.py +136 -0
- quoll/platforms/gitlab/platform.py +54 -0
- quoll/project.py +60 -0
- quoll/services.py +117 -0
- quoll/starters.py +117 -0
- quoll/workspace.py +232 -0
- quoll/yaml_dump.py +137 -0
- quoll_cli-0.2.0.dist-info/METADATA +641 -0
- quoll_cli-0.2.0.dist-info/RECORD +191 -0
- quoll_cli-0.2.0.dist-info/WHEEL +4 -0
- quoll_cli-0.2.0.dist-info/entry_points.txt +2 -0
- 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.<company>…), 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)
|