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,163 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence
4
+
5
+ from quoll.ci.steps import ROOT, SUMMARY_FILE, Cache, Item, Step, step
6
+ from quoll.ci.tools import install_osv_scanner
7
+ from quoll.languages.base import THRESHOLD_REGEX, CISteps, TestReports
8
+ from quoll.platforms.github.actions import ACTIONS
9
+
10
+ if TYPE_CHECKING:
11
+ from quoll.ci.config import CIConfig
12
+ from quoll.languages.base import Detection
13
+
14
+ _BUILD_TOOL = {
15
+ "maven": "mvn -B -ntp",
16
+ "maven-wrapper": "./mvnw -B -ntp",
17
+ "gradle": "gradle",
18
+ "gradle-wrapper": "./gradlew",
19
+ }
20
+
21
+ # tool: (step name, maven goal, gradle tasks). Static analysis runs on Maven without any pom.xml change (the full
22
+ # plugin coordinates); on Gradle, its plugin must be applied in the build.
23
+ _LINT = {
24
+ "spotless": ("Spotless", "spotless:check", "spotlessCheck"),
25
+ "checkstyle": ("Checkstyle", "checkstyle:check", "checkstyleMain checkstyleTest"),
26
+ "ktlint": ("ktlint", "ktlint:check", "ktlintCheck"),
27
+ "detekt": ("detekt", "detekt:check", "detekt"),
28
+ # SpotBugs analyses the compiled classes.
29
+ "spotbugs": ("SpotBugs", "compile com.github.spotbugs:spotbugs-maven-plugin:4.10.5.0:check", "spotbugsMain"),
30
+ "pmd": ("PMD", "org.apache.maven.plugins:maven-pmd-plugin:3.28.0:check", "pmdMain"),
31
+ }
32
+
33
+ # JaCoCo, without any build file change: Maven runs the plugin's goals around `verify`, Gradle applies the plugin
34
+ # from an init script.
35
+ _JACOCO = "org.jacoco:jacoco-maven-plugin:0.8.16"
36
+ _JACOCO_INIT = "/tmp/quoll-jacoco.gradle"
37
+ _JACOCO_GRADLE = f"""cat > {_JACOCO_INIT} <<'EOF'
38
+ allprojects {{
39
+ pluginManager.withPlugin("java") {{
40
+ apply plugin: "jacoco"
41
+ jacocoTestReport {{ reports {{ xml.required = true }} }}
42
+ test {{ finalizedBy jacocoTestReport }}
43
+ }}
44
+ }}
45
+ EOF"""
46
+ # The line coverage of every module: the last LINE counter of a JaCoCo report is its total.
47
+ _COVERAGE_CHECK = """reports=$(find . -path '*/target/site/jacoco/jacoco.xml' \\
48
+ -o -path '*/build/reports/jacoco/test/jacocoTestReport.xml')
49
+ if [ -z "$reports" ]; then
50
+ echo "::error::No JaCoCo report: does the build override the test JVM arguments (argLine)?"
51
+ exit 1
52
+ fi
53
+ total=$(for f in $reports; do grep -o '<counter type="LINE" missed="[0-9]*" covered="[0-9]*"/>' "$f" | tail -1; done \\
54
+ | sed 's/[^0-9 ]//g' | awk '{{ m += $1; c += $2 }} END {{ printf "%.1f", (m + c) ? c * 100 / (m + c) : 100 }}')
55
+ echo "### Coverage: ${{total}}%" >> {summary}
56
+ awk -v total="$total" -v min={threshold} 'BEGIN {{
57
+ if (total + 0 < min) {{ printf "::error::Coverage %.1f%% is below the %d%% threshold\\n", total, min; exit 1 }}
58
+ printf "Coverage %.1f%% (threshold %d%%)\\n", total, min
59
+ }}'"""
60
+
61
+ def build_tool(package_manager: str) -> str:
62
+ return _BUILD_TOOL[package_manager]
63
+
64
+
65
+ class JavaCI(CISteps):
66
+ version_key = "java-version"
67
+ codeql_language = "java-kotlin"
68
+ # osv-scanner reads the build files directly.
69
+ audit_needs_setup = False
70
+
71
+ def setup(self, cfg: CIConfig, version: Optional[str]) -> List[Item]:
72
+ maven = _is_maven(cfg)
73
+ with_: Dict[str, Any] = {"distribution": "temurin"}
74
+ if version:
75
+ with_["java-version"] = version
76
+ else:
77
+ with_["java-version-file"] = cfg.path(cfg.version_file)
78
+ if maven and cfg.cache:
79
+ with_["cache"] = "maven"
80
+ steps = [step("Set up Java", uses=ACTIONS["setup-java"], with_=with_)]
81
+ if not maven:
82
+ # setup-gradle caches Gradle's dependencies and build outputs.
83
+ with_gradle = None if cfg.cache else {"cache-disabled": True}
84
+ steps.append(step("Set up Gradle", uses=ACTIONS["setup-gradle"], with_=with_gradle))
85
+ return steps
86
+
87
+ def image(self, cfg: CIConfig, version: str) -> str:
88
+ if cfg.package_manager == "maven":
89
+ return f"maven:3-eclipse-temurin-{version}"
90
+ if cfg.package_manager == "gradle":
91
+ return f"gradle:jdk{version}"
92
+ return f"eclipse-temurin:{version}-jdk"
93
+
94
+ def cache(self, cfg: CIConfig) -> Optional[Cache]:
95
+ if not cfg.cache:
96
+ return None
97
+ if _is_maven(cfg):
98
+ env = {"MAVEN_OPTS": f"-Dmaven.repo.local={ROOT}/.m2/repository"}
99
+ return Cache("maven", (".m2/repository",), key_files=("pom.xml",), env=env)
100
+ return Cache("gradle", (".gradle/caches", ".gradle/wrapper"), env={"GRADLE_USER_HOME": f"{ROOT}/.gradle"})
101
+
102
+ def install(self, cfg: CIConfig, packages: Sequence[str] = ()) -> List[Step]:
103
+ # Maven and Gradle resolve dependencies as part of each command.
104
+ return [step("Resolve dependencies", run=cfg.install)] if cfg.install else []
105
+
106
+ def lint(self, cfg: CIConfig, tools: List[str]) -> List[Item]:
107
+ steps = self.install(cfg)
108
+ for tool in tools:
109
+ name, goal, tasks = _LINT[tool]
110
+ steps.append(step(name, run=f"{build_tool(cfg.package_manager)} {goal if _is_maven(cfg) else tasks}"))
111
+ return steps
112
+
113
+ def test(self, cfg: CIConfig) -> List[Item]:
114
+ t = cfg.test
115
+ tool = build_tool(cfg.package_manager)
116
+ if not t.coverage:
117
+ cmd = f"{tool} {'verify' if _is_maven(cfg) else 'check'}"
118
+ return [*self.install(cfg), step("Tests", run=cmd, secrets=t.env_secrets)]
119
+ steps: List[Item] = list(self.install(cfg))
120
+ if _is_maven(cfg):
121
+ cmd = f"{tool} {_JACOCO}:prepare-agent verify {_JACOCO}:report"
122
+ else:
123
+ steps.append(step("JaCoCo for Gradle", run=_JACOCO_GRADLE))
124
+ cmd = f"{tool} check --init-script {_JACOCO_INIT}"
125
+ return [
126
+ *steps,
127
+ step("Tests with coverage", run=cmd, secrets=t.env_secrets),
128
+ step(
129
+ "Coverage threshold",
130
+ run=_COVERAGE_CHECK.format(threshold=t.coverage_threshold, summary=SUMMARY_FILE),
131
+ ),
132
+ ]
133
+
134
+ def coverage_file(self, cfg: CIConfig) -> str:
135
+ if _is_maven(cfg):
136
+ return "target/site/jacoco/jacoco.xml"
137
+ return "build/reports/jacoco/test/jacocoTestReport.xml"
138
+
139
+ def reports(self, cfg: CIConfig) -> TestReports:
140
+ # Surefire and Gradle write JUnit results by themselves.
141
+ regex = THRESHOLD_REGEX if cfg.test.coverage else ""
142
+ if _is_maven(cfg):
143
+ return TestReports(junit="target/surefire-reports/TEST-*.xml", coverage_regex=regex)
144
+ return TestReports(junit="build/test-results/test/TEST-*.xml", coverage_regex=regex)
145
+
146
+ def audit(self, cfg: CIConfig) -> List[Item]:
147
+ return [
148
+ install_osv_scanner(),
149
+ step("Dependency vulnerability scan", run="osv-scanner scan source --recursive ."),
150
+ ]
151
+
152
+ def build(self, cfg: CIConfig) -> List[Item]:
153
+ return [*self.install(cfg), step("Build", run=cfg.build.command)]
154
+
155
+ def build_artifacts(self, det: Detection) -> List[str]:
156
+ return ["target/*.jar"] if det.package_manager.startswith("maven") else ["build/libs/*.jar"]
157
+
158
+ def dependabot_ecosystem(self, cfg: CIConfig) -> str:
159
+ return "maven" if _is_maven(cfg) else "gradle"
160
+
161
+
162
+ def _is_maven(cfg: CIConfig) -> bool:
163
+ return cfg.package_manager.startswith("maven")
@@ -0,0 +1,77 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from typing import List
5
+
6
+ from quoll.languages.base import Detection
7
+ from quoll.languages.java.ci import build_tool
8
+ from quoll.project import Project
9
+ from quoll.services import detect_services
10
+
11
+ JAVA_DEFAULT = "21"
12
+ JAVA_LTS = ("17", "21", "25")
13
+
14
+ _VERSION_PATTERNS = (
15
+ r"<java\.version>\s*(?:1\.)?(\d+)",
16
+ r"<maven\.compiler\.release>\s*(\d+)",
17
+ r"<maven\.compiler\.source>\s*(?:1\.)?(\d+)",
18
+ r"<release>\s*(\d+)\s*</release>",
19
+ r"JavaLanguageVersion\.of\(\s*(\d+)\s*\)",
20
+ r"JavaVersion\.VERSION_(?:1_)?(\d+)",
21
+ r"jvmToolchain\(\s*(\d+)\s*\)",
22
+ r"sourceCompatibility\s*=\s*['\"]?(?:1\.)?(\d+)",
23
+ )
24
+
25
+ # Artifacts revealing a backing service.
26
+ _SERVICE_ARTIFACTS = {
27
+ "postgres": ("org.postgresql", "postgresql:postgresql"),
28
+ "mysql": ("mysql-connector", "com.mysql"),
29
+ "redis": ("spring-boot-starter-data-redis", "jedis", "lettuce-core", "redisson"),
30
+ "mongodb": ("mongodb-driver", "spring-boot-starter-data-mongodb"),
31
+ }
32
+ # Enabled when their plugin name appears in a build file.
33
+ _LINT_TOOLS = ("spotless", "checkstyle", "ktlint", "detekt", "spotbugs", "pmd")
34
+
35
+
36
+ def detect(project: Project) -> Detection:
37
+ det = Detection(install="", lockfile="")
38
+ if project.exists("pom.xml"):
39
+ det.package_manager = "maven-wrapper" if project.exists("mvnw") else "maven"
40
+ else:
41
+ det.package_manager = "gradle-wrapper" if project.exists("gradlew") else "gradle"
42
+ if det.package_manager in ("maven", "gradle"):
43
+ wrapper = "mvnw" if det.package_manager == "maven" else "gradlew"
44
+ det.warnings.append(f"No ./{wrapper} found — the CI uses the runner's {det.package_manager}.")
45
+
46
+ build_files = build_text(project)
47
+ if project.exists(".java-version"):
48
+ det.version_file = ".java-version"
49
+ det.version = project.read_text(".java-version").strip().split(".")[0] or JAVA_DEFAULT
50
+ else:
51
+ det.version = next(
52
+ (m.group(1) for p in _VERSION_PATTERNS for m in [re.search(p, build_files)] if m), JAVA_DEFAULT
53
+ )
54
+ low = int(det.version) if det.version.isdigit() else 0
55
+ det.matrix_versions = [v for v in JAVA_LTS if int(v) >= low] or [det.version]
56
+
57
+ det.lint_tools = [tool for tool in _LINT_TOOLS if tool in build_files]
58
+ if project.glob_any("src/test", "*/src/test"):
59
+ det.test_runner = "junit"
60
+ else:
61
+ det.warnings.append("No src/test directory found — the test job is disabled.")
62
+
63
+ found = [a for artifacts in _SERVICE_ARTIFACTS.values() for a in artifacts if a in build_files]
64
+ det.services = detect_services(found, _SERVICE_ARTIFACTS)
65
+
66
+ tool = build_tool(det.package_manager)
67
+ det.build_command = f"{tool} package -DskipTests" if det.package_manager.startswith("maven") else f"{tool} assemble"
68
+ return det
69
+
70
+
71
+ def build_text(project: Project) -> str:
72
+ """Contents of the root and module build files."""
73
+ names: List[str] = ["pom.xml", "build.gradle", "build.gradle.kts", "settings.gradle", "settings.gradle.kts"]
74
+ files = [project.read_text(n) for n in names]
75
+ files += [p.read_text() for p in project.glob("*/pom.xml", "*/build.gradle", "*/build.gradle.kts")]
76
+ files.append(project.read_text("gradle/libs.versions.toml"))
77
+ return "\n".join(files)
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any, Dict, List
4
+
5
+ from quoll.languages.base import Detection, DockerDefaults, DockerImage
6
+ from quoll.languages.java.ci import build_tool
7
+ from quoll.project import Project
8
+
9
+ if TYPE_CHECKING:
10
+ from quoll.docker.config import DockerConfig
11
+
12
+
13
+ class JavaImage(DockerImage):
14
+ def defaults(self, project: Project, det: Detection) -> DockerDefaults:
15
+ return DockerDefaults(
16
+ port=8080,
17
+ command="java -jar /app/app.jar",
18
+ build_command=self.build_command(det.package_manager),
19
+ warnings=["The image runs the built jar: make sure it is executable (Main-Class in its manifest)."],
20
+ )
21
+
22
+ def build_command(self, package_manager: str) -> str:
23
+ tool = build_tool(package_manager)
24
+ return f"{tool} package -DskipTests" if package_manager.startswith("maven") else f"{tool} assemble"
25
+
26
+ def context(self, project: Project, cfg: DockerConfig) -> Dict[str, Any]:
27
+ pm, version = cfg.package_manager, cfg.version
28
+ maven = pm.startswith("maven")
29
+ # Wrappers fetch their own Maven/Gradle; otherwise build in the official Maven/Gradle image.
30
+ build_image = {
31
+ "maven-wrapper": f"eclipse-temurin:{version}-jdk",
32
+ "gradle-wrapper": f"eclipse-temurin:{version}-jdk",
33
+ "maven": f"maven:3-eclipse-temurin-{version}",
34
+ "gradle": f"gradle:jdk{version}",
35
+ }[pm]
36
+ return {
37
+ "build_image": build_image,
38
+ "cache": "/root/.m2" if maven else ("/home/gradle/.gradle" if pm == "gradle" else "/root/.gradle"),
39
+ "jar_dir": "target" if maven else "build/libs",
40
+ # Wrappers committed from Windows or unzipped lose their executable bit.
41
+ "wrapper": {"maven-wrapper": "mvnw", "gradle-wrapper": "gradlew"}.get(pm, ""),
42
+ }
43
+
44
+ def dockerignore(self, cfg: DockerConfig) -> List[str]:
45
+ return ["target", "build", ".gradle", "**/*.class", ".env", ".env.*", "!.env.example"]
@@ -0,0 +1 @@
1
+ """JVM frameworks."""
@@ -0,0 +1,18 @@
1
+ {% extends "java/Dockerfile.j2" %}
2
+
3
+ {% block runtime %}
4
+ # Split the jar into layers (Spring Boot 3.3+): dependencies change less often than the code.
5
+ FROM eclipse-temurin:{{ cfg.version }}-jre AS layers
6
+ WORKDIR /layers
7
+ COPY --from=build /out/app.jar app.jar
8
+ RUN java -Djarmode=tools -jar app.jar extract --layers --launcher --destination extracted
9
+
10
+ {{ super() }}
11
+ {%- endblock %}
12
+
13
+ {% block app_files %}
14
+ COPY --from=layers /layers/extracted/dependencies/ ./
15
+ COPY --from=layers /layers/extracted/spring-boot-loader/ ./
16
+ COPY --from=layers /layers/extracted/snapshot-dependencies/ ./
17
+ COPY --from=layers /layers/extracted/application/ ./
18
+ {% endblock %}
@@ -0,0 +1,10 @@
1
+ """Spring Boot: Maven/Gradle already run its tests; the image ships the jar as cache-friendly layers."""
2
+
3
+ from quoll.languages.base import Framework
4
+ from quoll.languages.java.frameworks.spring_boot.detect import detect
5
+ from quoll.languages.java.frameworks.spring_boot.docker import SpringBootImage
6
+ from quoll.languages.java.frameworks.spring_boot.services import service_env
7
+
8
+ framework = Framework(
9
+ name="spring-boot", language="java", detect=detect, docker=SpringBootImage(), service_env=service_env
10
+ )
@@ -0,0 +1,9 @@
1
+ from __future__ import annotations
2
+
3
+ from quoll.languages.base import Detection
4
+ from quoll.languages.java.detect import build_text
5
+ from quoll.project import Project
6
+
7
+
8
+ def detect(project: Project, det: Detection) -> bool:
9
+ return "spring-boot" in build_text(project)
@@ -0,0 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+ from quoll.languages.base import Detection, DockerDefaults
4
+ from quoll.languages.java.ci import build_tool
5
+ from quoll.languages.java.docker import JavaImage
6
+ from quoll.project import Project
7
+
8
+
9
+ class SpringBootImage(JavaImage):
10
+ def defaults(self, project: Project, det: Detection) -> DockerDefaults:
11
+ return DockerDefaults(
12
+ port=8080,
13
+ command="java org.springframework.boot.loader.launch.JarLauncher",
14
+ build_command=self.build_command(det.package_manager),
15
+ )
16
+
17
+ def build_command(self, package_manager: str) -> str:
18
+ tool = build_tool(package_manager)
19
+ return f"{tool} package -DskipTests" if package_manager.startswith("maven") else f"{tool} bootJar"
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Dict
4
+
5
+
6
+ def service_env(service: str, host: str, database: str) -> Dict[str, str]:
7
+ """Spring Boot binds SPRING_* variables to its datasource, Redis and MongoDB properties."""
8
+ if service == "postgres":
9
+ return {
10
+ "SPRING_DATASOURCE_URL": f"jdbc:postgresql://{host}:5432/{database}",
11
+ "SPRING_DATASOURCE_USERNAME": "app",
12
+ "SPRING_DATASOURCE_PASSWORD": "app",
13
+ }
14
+ if service == "mysql":
15
+ return {
16
+ "SPRING_DATASOURCE_URL": f"jdbc:mysql://{host}:3306/{database}",
17
+ "SPRING_DATASOURCE_USERNAME": "root",
18
+ "SPRING_DATASOURCE_PASSWORD": "root",
19
+ }
20
+ if service == "redis":
21
+ return {"SPRING_DATA_REDIS_HOST": host}
22
+ if service == "mongodb":
23
+ return {"SPRING_DATA_MONGODB_URI": f"mongodb://{host}:27017/{database}"}
24
+ return {}
@@ -0,0 +1,98 @@
1
+ {% block deps %}
2
+ FROM {{ build_image }} AS deps
3
+ WORKDIR /app
4
+ {% if setup %}
5
+ RUN {{ setup }}
6
+ {% endif %}
7
+ # Dependencies first: code changes don't reinstall them.
8
+ RUN --mount=type=cache,target={{ cache }} \
9
+ {% for f in install_files + install_dirs %}
10
+ --mount=type=bind,source={{ f }},target={{ f }} \
11
+ {% endfor %}
12
+ {{ install }}
13
+ {% endblock %}
14
+
15
+ {% block build %}
16
+ FROM deps AS build
17
+ COPY . .
18
+ {% for key, value in build_env.items() %}
19
+ ENV {{ key }}={{ value }}
20
+ {% endfor %}
21
+ {% if cfg.build_command %}
22
+ RUN {{ cfg.build_command }}
23
+ {% endif %}
24
+ {% if not cfg.static and not cfg.output_dir and prune %}
25
+ # Ship production dependencies only.
26
+ RUN {{ prune }}
27
+ {% endif %}
28
+ {% endblock %}
29
+
30
+ {% block runtime %}
31
+ {% if cfg.static %}
32
+ FROM nginxinc/nginx-unprivileged:1-alpine
33
+ # Security fixes Alpine released since the base image was built.
34
+ USER root
35
+ RUN apk upgrade --no-cache
36
+ USER 101
37
+ {% if cfg.proxy %}
38
+ # Filled in at startup with the API URLs from the environment.
39
+ COPY <<"EOF" /etc/nginx/templates/default.conf.template
40
+ {% else %}
41
+ COPY <<"EOF" /etc/nginx/conf.d/default.conf
42
+ {% endif %}
43
+ server {
44
+ listen {{ cfg.port }};
45
+ root /usr/share/nginx/html;
46
+ {% for path, variable in cfg.proxy.items() %}
47
+ location {{ path }} {
48
+ proxy_pass {{ "${" ~ variable ~ "}" }};
49
+ proxy_set_header Host $host;
50
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
51
+ proxy_set_header X-Forwarded-Proto $scheme;
52
+ }
53
+ {% endfor %}
54
+ location / {
55
+ try_files $uri $uri/ {{ "/index.html" if spa else "$uri.html =404" }};
56
+ }
57
+ {% if not spa %}
58
+ error_page 404 /404.html;
59
+ {% endif %}
60
+ }
61
+ EOF
62
+ {% if cfg.proxy %}
63
+ # Without the API URLs, stop at startup with what to set.
64
+ COPY --chmod=755 <<"EOF" /docker-entrypoint.d/05-quoll-proxy.sh
65
+ #!/bin/sh
66
+ {% for path, variable in cfg.proxy.items() %}
67
+ : "{{ "${" ~ variable }}:?is the URL {{ path }} is forwarded to, e.g. http://api:3000 (no path)}"
68
+ {% endfor %}
69
+ EOF
70
+ {% endif %}
71
+ COPY --from=build /app/{{ cfg.output_dir }} /usr/share/nginx/html
72
+ EXPOSE {{ cfg.port }}
73
+ {% else %}
74
+ FROM {{ runtime_image }}
75
+ {{ os_updates }}
76
+ {% if user == "node" and "npm" not in cfg.command and "npx" not in cfg.command %}
77
+ # npm and corepack built the app; the container only runs it (and they carry their own vulnerabilities).
78
+ RUN rm -rf /usr/local/lib/node_modules/npm /usr/local/lib/node_modules/corepack \
79
+ /usr/local/bin/npm /usr/local/bin/npx /usr/local/bin/corepack
80
+ {% endif %}
81
+ ENV {% for key, value in env.items() %}{{ key }}={{ value }}{{ " \\\n " if not loop.last }}{% endfor %}
82
+
83
+ WORKDIR /app
84
+ {% block app_files %}
85
+ {% if cfg.output_dir %}
86
+ # The build output is self-contained.
87
+ COPY --from=build --chown={{ user }}:{{ user }} /app/{{ cfg.output_dir }} ./{{ cfg.output_dir }}
88
+ {% else %}
89
+ ENV PATH="/app/node_modules/.bin:$PATH"
90
+ COPY --from=build --chown={{ user }}:{{ user }} /app ./
91
+ {% endif %}
92
+ {% endblock %}
93
+ # Numeric: Kubernetes can then check it isn't root (runAsNonRoot).
94
+ USER {{ uid }}
95
+ EXPOSE {{ cfg.port }}
96
+ CMD {{ cfg.command | exec_form }}
97
+ {% endif %}
98
+ {% endblock %}
@@ -0,0 +1,29 @@
1
+ """Node.js (and front-end) projects: npm, pnpm, yarn and bun."""
2
+
3
+ from quoll.languages.base import Language
4
+ from quoll.languages.node.ci import NodeCI
5
+ from quoll.languages.node.detect import detect
6
+ from quoll.languages.node.docker import NodeImage
7
+ from quoll.languages.node.frameworks import angular, astro, nestjs, nextjs, nuxt, sveltekit
8
+ from quoll.languages.node.starter import starter as express
9
+
10
+ language = Language(
11
+ name="node",
12
+ markers=("package.json",),
13
+ package_managers=("npm", "pnpm", "yarn", "yarn-classic", "bun"),
14
+ lint_tools=("eslint", "prettier", "biome", "tsc"),
15
+ test_runners=("jest", "vitest", "bun"),
16
+ detect=detect,
17
+ ci=NodeCI(),
18
+ frameworks=(
19
+ nextjs.framework,
20
+ nuxt.framework,
21
+ sveltekit.framework,
22
+ astro.framework,
23
+ angular.framework,
24
+ nestjs.framework,
25
+ ),
26
+ scripts=True,
27
+ docker=NodeImage(),
28
+ starters=(express,),
29
+ )