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,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
|
+
)
|