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,84 @@
|
|
|
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.project import Project
|
|
8
|
+
from quoll.services import detect_services
|
|
9
|
+
|
|
10
|
+
# Toolchains, as "<compiler>-<major>" (packages of the Ubuntu 24.04 runners and images).
|
|
11
|
+
COMPILER_DEFAULT = "gcc-14"
|
|
12
|
+
COMPILERS = ("gcc-14", "clang-18")
|
|
13
|
+
|
|
14
|
+
# Libraries (CMake packages, vcpkg ports, Conan references) revealing a backing service.
|
|
15
|
+
_SERVICE_LIBRARIES = {
|
|
16
|
+
"postgres": ("libpq", "libpqxx", "postgresql", "pq"),
|
|
17
|
+
"mysql": ("libmysqlclient", "mysqlclient", "mariadb-connector-c", "mysql-connector-cpp"),
|
|
18
|
+
"redis": ("hiredis", "redis-plus-plus"),
|
|
19
|
+
"mongodb": ("mongo-cxx-driver", "mongocxx", "mongo-c-driver"),
|
|
20
|
+
}
|
|
21
|
+
# What makes CTest run tests.
|
|
22
|
+
_CTEST = re.compile(
|
|
23
|
+
r"\b(enable_testing|add_test|include\s*\(\s*CTest|gtest_discover_tests|catch_discover_tests)\b", re.I
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def detect(project: Project) -> Detection:
|
|
28
|
+
det = Detection(version=COMPILER_DEFAULT, matrix_versions=list(COMPILERS))
|
|
29
|
+
meson = project.exists("meson.build") and not project.exists("CMakeLists.txt")
|
|
30
|
+
if meson:
|
|
31
|
+
det.package_manager = "meson"
|
|
32
|
+
elif project.glob_any("conanfile.txt", "conanfile.py"):
|
|
33
|
+
det.package_manager = "conan"
|
|
34
|
+
det.lockfile = "conan.lock" if project.exists("conan.lock") else ""
|
|
35
|
+
elif project.exists("vcpkg.json"):
|
|
36
|
+
det.package_manager = "vcpkg"
|
|
37
|
+
else:
|
|
38
|
+
det.package_manager = "cmake"
|
|
39
|
+
# Dependencies are resolved while configuring the build (FetchContent, Conan, vcpkg, wraps).
|
|
40
|
+
det.install = "meson setup build" if meson else "cmake -S . -B build"
|
|
41
|
+
|
|
42
|
+
det.lint_tools = [tool for tool, config in (("clang-format", ".clang-format"), ("clang-tidy", ".clang-tidy"))
|
|
43
|
+
if project.exists(config)]
|
|
44
|
+
det.lint_tools.append("cppcheck")
|
|
45
|
+
|
|
46
|
+
build_files = cmake_files(project) if not meson else project.read_text("meson.build")
|
|
47
|
+
if meson and re.search(r"\btest\s*\(", build_files):
|
|
48
|
+
det.test_runner = "meson"
|
|
49
|
+
elif not meson and _CTEST.search(build_files):
|
|
50
|
+
det.test_runner = "ctest"
|
|
51
|
+
else:
|
|
52
|
+
det.warnings.append("No tests found (enable_testing()/add_test() or meson test()) — the test job is disabled.")
|
|
53
|
+
det.sanitizers = True
|
|
54
|
+
|
|
55
|
+
# osv-scanner reads conan.lock; there is no vulnerability database for other C/C++ dependencies.
|
|
56
|
+
det.audit = bool(det.lockfile)
|
|
57
|
+
if not det.audit:
|
|
58
|
+
det.warnings.append("No conan.lock: no dependency audit for C/C++ (Semgrep and the image scan still run).")
|
|
59
|
+
|
|
60
|
+
manifests = " ".join([build_files, project.read_text("vcpkg.json"), project.read_text("conanfile.txt"),
|
|
61
|
+
project.read_text("conanfile.py")]).lower()
|
|
62
|
+
det.services = detect_services(
|
|
63
|
+
[lib for libs in _SERVICE_LIBRARIES.values() for lib in libs if re.search(rf"\b{re.escape(lib)}\b", manifests)],
|
|
64
|
+
_SERVICE_LIBRARIES,
|
|
65
|
+
)
|
|
66
|
+
det.build_command = "meson compile -C build" if meson else "cmake --build build"
|
|
67
|
+
return det
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def cmake_files(project: Project) -> str:
|
|
71
|
+
"""The root CMakeLists.txt and those of the first-level directories (src/, tests/...)."""
|
|
72
|
+
nested = (p.relative_to(project.root).as_posix() for p in project.glob("*/CMakeLists.txt"))
|
|
73
|
+
paths: List[str] = ["CMakeLists.txt", *nested]
|
|
74
|
+
return "\n".join(project.read_text(p) for p in paths)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def executable(project: Project) -> str:
|
|
78
|
+
"""The first executable target that isn't a test."""
|
|
79
|
+
if project.exists("meson.build"):
|
|
80
|
+
pattern, text = r"executable\s*\(\s*['\"]([\w.-]+)['\"]", project.read_text("meson.build")
|
|
81
|
+
else:
|
|
82
|
+
pattern, text = r"add_executable\s*\(\s*([\w.-]+)", cmake_files(project)
|
|
83
|
+
names = [n for n in re.findall(pattern, text, re.I) if not re.search(r"test", n, re.I)]
|
|
84
|
+
return names[0] if names else ""
|
|
@@ -0,0 +1,47 @@
|
|
|
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.cpp.detect import executable
|
|
7
|
+
from quoll.project import Project
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from quoll.docker.config import DockerConfig
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# Build tools on top of the gcc image.
|
|
14
|
+
_PACKAGES = {
|
|
15
|
+
"cmake": ("cmake", "ninja-build"),
|
|
16
|
+
"conan": ("cmake", "ninja-build", "pipx"),
|
|
17
|
+
"vcpkg": ("cmake", "ninja-build", "curl", "zip", "unzip", "tar", "pkg-config"),
|
|
18
|
+
"meson": ("meson", "ninja-build"),
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class CppImage(DockerImage):
|
|
23
|
+
def defaults(self, project: Project, det: Detection) -> DockerDefaults:
|
|
24
|
+
warnings = ["C/C++ programs don't say which port they listen on — check docker.port."]
|
|
25
|
+
target = executable(project)
|
|
26
|
+
if not target:
|
|
27
|
+
warnings.append("No executable target found (add_executable / executable()) — check docker.command.")
|
|
28
|
+
if det.package_manager == "meson":
|
|
29
|
+
build = "meson setup build --buildtype=release && meson compile -C build"
|
|
30
|
+
else:
|
|
31
|
+
build = "cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release && cmake --build build"
|
|
32
|
+
return DockerDefaults(port=8080, command="/app", build_command=build, warnings=warnings)
|
|
33
|
+
|
|
34
|
+
def context(self, project: Project, cfg: DockerConfig) -> Dict[str, Any]:
|
|
35
|
+
compiler, _, major = cfg.version.partition("-")
|
|
36
|
+
return {
|
|
37
|
+
# The official gcc image builds both C and C++; clang projects build with the same major of GCC's
|
|
38
|
+
# libstdc++ either way.
|
|
39
|
+
"gcc_tag": major if compiler == "gcc" and major else "14",
|
|
40
|
+
"target": executable(project) or project.name,
|
|
41
|
+
"packages": " ".join(_PACKAGES[cfg.package_manager]),
|
|
42
|
+
"conan": cfg.package_manager == "conan",
|
|
43
|
+
"vcpkg": cfg.package_manager == "vcpkg",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
def dockerignore(self, cfg: DockerConfig) -> List[str]:
|
|
47
|
+
return ["build", "build-*", "cmake-build-*", "out", ".env", ".env.*", "!.env.example"]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{% block build %}
|
|
2
|
+
FROM mcr.microsoft.com/dotnet/sdk:{{ cfg.version }} AS build
|
|
3
|
+
WORKDIR /src
|
|
4
|
+
COPY . .
|
|
5
|
+
RUN --mount=type=cache,target=/root/.nuget/packages \
|
|
6
|
+
{{ cfg.build_command }}
|
|
7
|
+
{% endblock %}
|
|
8
|
+
|
|
9
|
+
{% block runtime %}
|
|
10
|
+
FROM mcr.microsoft.com/dotnet/{{ "aspnet" if web else "runtime" }}:{{ cfg.version }}
|
|
11
|
+
{{ os_updates }}
|
|
12
|
+
WORKDIR /app
|
|
13
|
+
COPY --from=build /out .
|
|
14
|
+
{% if web %}
|
|
15
|
+
ENV ASPNETCORE_HTTP_PORTS={{ cfg.port }}
|
|
16
|
+
{% endif %}
|
|
17
|
+
# Non-root user provided by the .NET images.
|
|
18
|
+
USER $APP_UID
|
|
19
|
+
EXPOSE {{ cfg.port }}
|
|
20
|
+
CMD {{ cfg.command | exec_form }}
|
|
21
|
+
{% endblock %}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
""".NET: C#, F# and VB projects and solutions."""
|
|
2
|
+
|
|
3
|
+
from quoll.languages.base import Language
|
|
4
|
+
from quoll.languages.dotnet.ci import DotnetCI
|
|
5
|
+
from quoll.languages.dotnet.detect import detect
|
|
6
|
+
from quoll.languages.dotnet.docker import DotnetImage
|
|
7
|
+
|
|
8
|
+
language = Language(
|
|
9
|
+
name="dotnet",
|
|
10
|
+
markers=("*.sln", "*.slnx", "*.csproj", "*.fsproj", "*.vbproj"),
|
|
11
|
+
package_managers=("dotnet",),
|
|
12
|
+
lint_tools=("dotnet-format",),
|
|
13
|
+
test_runners=("dotnet",),
|
|
14
|
+
detect=detect,
|
|
15
|
+
ci=DotnetCI(),
|
|
16
|
+
docker=DotnetImage(),
|
|
17
|
+
)
|
|
@@ -0,0 +1,88 @@
|
|
|
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, Cache, Item, step
|
|
6
|
+
from quoll.languages.base import CISteps, TestReports
|
|
7
|
+
from quoll.platforms.github.actions import ACTIONS
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from quoll.ci.config import CIConfig
|
|
11
|
+
|
|
12
|
+
# `dotnet list package --vulnerable` always exits 0: fail on high or critical advisories ourselves.
|
|
13
|
+
_AUDIT = """dotnet list package --vulnerable --include-transitive 2>&1 | tee vulnerable.txt
|
|
14
|
+
if grep -qE "\\b(High|Critical)\\b" vulnerable.txt; then
|
|
15
|
+
echo "::error::Packages with high or critical vulnerabilities found"
|
|
16
|
+
exit 1
|
|
17
|
+
fi"""
|
|
18
|
+
|
|
19
|
+
# dotnet test writes TRX results; trx2junit, installed outside the project, converts them for the platforms. Tests
|
|
20
|
+
# that fail still get their report: the status is kept until the conversion is done.
|
|
21
|
+
_JUNIT = """status=0
|
|
22
|
+
{test} || status=$?
|
|
23
|
+
dotnet tool install --tool-path /tmp/quoll-trx2junit trx2junit >/dev/null
|
|
24
|
+
/tmp/quoll-trx2junit/trx2junit TestResults/*.trx
|
|
25
|
+
exit $status"""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class DotnetCI(CISteps):
|
|
29
|
+
version_key = "dotnet-version"
|
|
30
|
+
codeql_language = "csharp"
|
|
31
|
+
|
|
32
|
+
def setup(self, cfg: CIConfig, version: Optional[str]) -> List[Item]:
|
|
33
|
+
with_: Dict[str, Any] = (
|
|
34
|
+
{"dotnet-version": version} if version else {"global-json-file": cfg.path(cfg.version_file)}
|
|
35
|
+
)
|
|
36
|
+
if cfg.cache:
|
|
37
|
+
with_["cache"] = True
|
|
38
|
+
with_["cache-dependency-path"] = cfg.path("**/packages.lock.json")
|
|
39
|
+
return [step("Set up .NET", uses=ACTIONS["setup-dotnet"], with_=with_)]
|
|
40
|
+
|
|
41
|
+
def image(self, cfg: CIConfig, version: str) -> str:
|
|
42
|
+
return f"mcr.microsoft.com/dotnet/sdk:{version}"
|
|
43
|
+
|
|
44
|
+
def cache(self, cfg: CIConfig) -> Optional[Cache]:
|
|
45
|
+
if not cfg.cache:
|
|
46
|
+
return None
|
|
47
|
+
return Cache("nuget", (".nuget/packages",), env={"NUGET_PACKAGES": f"{ROOT}/.nuget/packages"})
|
|
48
|
+
|
|
49
|
+
def install(self, cfg: CIConfig, packages: Sequence[str] = ()) -> List[Item]:
|
|
50
|
+
return [step("Restore dependencies", run=cfg.install)]
|
|
51
|
+
|
|
52
|
+
def lint(self, cfg: CIConfig, tools: List[str]) -> List[Item]:
|
|
53
|
+
steps = self.install(cfg)
|
|
54
|
+
if "dotnet-format" in tools:
|
|
55
|
+
steps.append(step("dotnet format", run="dotnet format --verify-no-changes --no-restore"))
|
|
56
|
+
return steps
|
|
57
|
+
|
|
58
|
+
def test(self, cfg: CIConfig) -> List[Item]:
|
|
59
|
+
t = cfg.test
|
|
60
|
+
cmd = "dotnet test --no-restore"
|
|
61
|
+
if t.coverage:
|
|
62
|
+
cmd += (
|
|
63
|
+
" -p:CollectCoverage=true -p:CoverletOutputFormat=cobertura"
|
|
64
|
+
f" -p:Threshold={t.coverage_threshold} -p:ThresholdType=line"
|
|
65
|
+
)
|
|
66
|
+
if t.reports:
|
|
67
|
+
cmd = _JUNIT.format(test=cmd + " --logger trx --results-directory TestResults")
|
|
68
|
+
name = "Tests" + (" with coverage" if t.coverage else "")
|
|
69
|
+
return [*self.install(cfg), step(name, run=cmd, secrets=t.env_secrets)]
|
|
70
|
+
|
|
71
|
+
def coverage_file(self, cfg: CIConfig) -> str:
|
|
72
|
+
return "**/coverage.cobertura.xml"
|
|
73
|
+
|
|
74
|
+
def reports(self, cfg: CIConfig) -> TestReports:
|
|
75
|
+
if not cfg.test.coverage:
|
|
76
|
+
return TestReports(junit="TestResults/*.xml")
|
|
77
|
+
# coverlet's summary table: | Total | 85.5% | ...
|
|
78
|
+
regex = r"/\|\s*Total\s*\|\s*(\d+(?:\.\d+)?)%/"
|
|
79
|
+
return TestReports(junit="TestResults/*.xml", cobertura="**/coverage.cobertura.xml", coverage_regex=regex)
|
|
80
|
+
|
|
81
|
+
def audit(self, cfg: CIConfig) -> List[Item]:
|
|
82
|
+
return [*self.install(cfg), step("Dependency vulnerability scan", run=_AUDIT)]
|
|
83
|
+
|
|
84
|
+
def build(self, cfg: CIConfig) -> List[Item]:
|
|
85
|
+
return [*self.install(cfg), step("Build", run=cfg.build.command)]
|
|
86
|
+
|
|
87
|
+
def dependabot_ecosystem(self, cfg: CIConfig) -> str:
|
|
88
|
+
return "nuget"
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from quoll.languages.base import Detection
|
|
6
|
+
from quoll.project import Project
|
|
7
|
+
from quoll.services import detect_services
|
|
8
|
+
|
|
9
|
+
DOTNET_DEFAULT = "10.0"
|
|
10
|
+
DOTNET_LTS = ("8.0", "10.0")
|
|
11
|
+
|
|
12
|
+
# NuGet packages revealing a backing service.
|
|
13
|
+
_SERVICE_PACKAGES = {
|
|
14
|
+
"postgres": ("Npgsql",),
|
|
15
|
+
"mysql": ("MySqlConnector", "MySql.Data", "Pomelo.EntityFrameworkCore.MySql"),
|
|
16
|
+
"redis": ("StackExchange.Redis",),
|
|
17
|
+
"mongodb": ("MongoDB.Driver",),
|
|
18
|
+
}
|
|
19
|
+
_TEST_MARKERS = ("Microsoft.NET.Test.Sdk", "<IsTestProject>true", "xunit", "NUnit", "MSTest")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def detect(project: Project) -> Detection:
|
|
23
|
+
det = Detection(package_manager="dotnet", lint_tools=["dotnet-format"])
|
|
24
|
+
projects = "\n".join(p.read_text() for p in project.glob("**/*.csproj", "**/*.fsproj", "**/*.vbproj"))
|
|
25
|
+
|
|
26
|
+
locked = project.glob_any("**/packages.lock.json")
|
|
27
|
+
det.lockfile = "packages.lock.json" if locked else ""
|
|
28
|
+
det.install = "dotnet restore --locked-mode" if locked else "dotnet restore"
|
|
29
|
+
# setup-dotnet can only cache NuGet packages keyed on lock files.
|
|
30
|
+
det.cache = locked
|
|
31
|
+
|
|
32
|
+
if project.exists("global.json"):
|
|
33
|
+
det.version_file = "global.json"
|
|
34
|
+
global_json = project.read_json("global.json")
|
|
35
|
+
sdk = global_json.get("sdk", {}) if isinstance(global_json, dict) else {}
|
|
36
|
+
m = re.search(r"(\d+\.\d+)", str(sdk.get("version", "")))
|
|
37
|
+
det.version = m.group(1) if m else DOTNET_DEFAULT
|
|
38
|
+
else:
|
|
39
|
+
frameworks = [int(v) for v in re.findall(r"net(\d+)\.0", projects)]
|
|
40
|
+
det.version = f"{max(frameworks)}.0" if frameworks else DOTNET_DEFAULT
|
|
41
|
+
low = float(det.version)
|
|
42
|
+
det.matrix_versions = [v for v in DOTNET_LTS if float(v) >= low] or [det.version]
|
|
43
|
+
|
|
44
|
+
if any(marker in projects for marker in _TEST_MARKERS):
|
|
45
|
+
det.test_runner = "dotnet"
|
|
46
|
+
# Thresholds need coverlet.msbuild; the default coverlet.collector only reports.
|
|
47
|
+
det.coverage = "coverlet.msbuild" in projects
|
|
48
|
+
else:
|
|
49
|
+
det.warnings.append("No test project found (Microsoft.NET.Test.Sdk) — the test job is disabled.")
|
|
50
|
+
found = [p for packages in _SERVICE_PACKAGES.values() for p in packages if f'"{p}' in projects]
|
|
51
|
+
det.services = detect_services(found, _SERVICE_PACKAGES)
|
|
52
|
+
det.build_command = "dotnet build --no-restore --configuration Release"
|
|
53
|
+
return det
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
from quoll.languages.base import Detection, DockerDefaults, DockerImage
|
|
8
|
+
from quoll.project import Project
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from quoll.docker.config import DockerConfig
|
|
12
|
+
|
|
13
|
+
_TEST_MARKERS = ("Microsoft.NET.Test.Sdk", "<IsTestProject>true")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DotnetImage(DockerImage):
|
|
17
|
+
def defaults(self, project: Project, det: Detection) -> DockerDefaults:
|
|
18
|
+
app = _app_project(project)
|
|
19
|
+
if app is None:
|
|
20
|
+
return DockerDefaults(port=8080, warnings=["No application project found — set docker.build_command."])
|
|
21
|
+
rel = app.relative_to(project.root).as_posix()
|
|
22
|
+
assembly = re.search(r"<AssemblyName>([^<]+)</AssemblyName>", app.read_text())
|
|
23
|
+
name = assembly.group(1) if assembly else app.stem
|
|
24
|
+
return DockerDefaults(
|
|
25
|
+
port=8080,
|
|
26
|
+
command=f"dotnet {name}.dll",
|
|
27
|
+
build_command=f"dotnet publish {rel} --configuration Release --output /out",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def context(self, project: Project, cfg: DockerConfig) -> Dict[str, Any]:
|
|
31
|
+
published = re.search(r"dotnet publish (\S+)", cfg.build_command)
|
|
32
|
+
csproj = project.read_text(published.group(1)) if published else ""
|
|
33
|
+
return {"web": 'Sdk="Microsoft.NET.Sdk.Web"' in csproj}
|
|
34
|
+
|
|
35
|
+
def dockerignore(self, cfg: DockerConfig) -> List[str]:
|
|
36
|
+
return ["**/bin", "**/obj", "**/TestResults", ".vs", "*.user", ".env", ".env.*", "!.env.example"]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _app_project(project: Project) -> Optional[Path]:
|
|
40
|
+
"""The project to publish: a web project first, then any non-test project, shallowest first."""
|
|
41
|
+
candidates = [
|
|
42
|
+
p
|
|
43
|
+
for p in project.glob("*.csproj", "*/*.csproj", "*/*/*.csproj", "*.fsproj", "*/*.fsproj", "*/*/*.fsproj")
|
|
44
|
+
if not any(marker in p.read_text() for marker in _TEST_MARKERS)
|
|
45
|
+
]
|
|
46
|
+
candidates.sort(key=lambda p: ("Microsoft.NET.Sdk.Web" not in p.read_text(), len(p.parts)))
|
|
47
|
+
return candidates[0] if candidates else None
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{% block build %}
|
|
2
|
+
FROM golang:{{ go_tag }} AS build
|
|
3
|
+
WORKDIR /src
|
|
4
|
+
# Dependencies first: code changes don't download them again.
|
|
5
|
+
RUN --mount=type=cache,target=/go/pkg/mod \
|
|
6
|
+
--mount=type=bind,source=go.mod,target=go.mod \
|
|
7
|
+
{% if go_sum %}
|
|
8
|
+
--mount=type=bind,source=go.sum,target=go.sum \
|
|
9
|
+
{% endif %}
|
|
10
|
+
go mod download
|
|
11
|
+
COPY . .
|
|
12
|
+
RUN --mount=type=cache,target=/go/pkg/mod \
|
|
13
|
+
--mount=type=cache,target=/root/.cache/go-build \
|
|
14
|
+
CGO_ENABLED=0 {{ cfg.build_command }}
|
|
15
|
+
{% endblock %}
|
|
16
|
+
|
|
17
|
+
{% block runtime %}
|
|
18
|
+
# Static binary on distroless: no shell, no package manager, non-root.
|
|
19
|
+
FROM gcr.io/distroless/static-debian13:nonroot
|
|
20
|
+
COPY --from=build /out/app /app
|
|
21
|
+
# distroless' nonroot, numeric: Kubernetes can then check it isn't root (runAsNonRoot).
|
|
22
|
+
USER 65532:65532
|
|
23
|
+
EXPOSE {{ cfg.port }}
|
|
24
|
+
CMD {{ cfg.command | exec_form }}
|
|
25
|
+
{% endblock %}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Go modules."""
|
|
2
|
+
|
|
3
|
+
from quoll.languages.base import Language
|
|
4
|
+
from quoll.languages.go.ci import GoCI
|
|
5
|
+
from quoll.languages.go.detect import detect
|
|
6
|
+
from quoll.languages.go.docker import GoImage
|
|
7
|
+
|
|
8
|
+
language = Language(
|
|
9
|
+
name="go",
|
|
10
|
+
markers=("go.mod",),
|
|
11
|
+
package_managers=("go",),
|
|
12
|
+
lint_tools=("gofmt", "vet", "golangci-lint"),
|
|
13
|
+
test_runners=("go",),
|
|
14
|
+
detect=detect,
|
|
15
|
+
ci=GoCI(),
|
|
16
|
+
docker=GoImage(),
|
|
17
|
+
)
|
quoll/languages/go/ci.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
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.languages.base import THRESHOLD_REGEX, CISteps, TestReports
|
|
7
|
+
from quoll.platforms.github.actions import ACTIONS
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from quoll.ci.config import CIConfig
|
|
11
|
+
|
|
12
|
+
# Only tracked files: container jobs keep the module cache inside the project.
|
|
13
|
+
_GOFMT = """unformatted=$(git ls-files -z '*.go' | xargs -0 -r gofmt -l)
|
|
14
|
+
if [ -n "$unformatted" ]; then
|
|
15
|
+
echo "::error::These files are not gofmt-formatted:"
|
|
16
|
+
echo "$unformatted"
|
|
17
|
+
exit 1
|
|
18
|
+
fi"""
|
|
19
|
+
|
|
20
|
+
_COVERAGE_CHECK = """total=$(go tool cover -func=coverage.out | awk '/^total:/ {{ sub("%", "", $3); print $3 }}')
|
|
21
|
+
echo "### Coverage: ${{total}}%" >> {summary}
|
|
22
|
+
awk -v total="$total" -v min={threshold} 'BEGIN {{
|
|
23
|
+
if (total + 0 < min) {{ printf "::error::Coverage %.1f%% is below the %d%% threshold\\n", total, min; exit 1 }}
|
|
24
|
+
printf "Coverage %.1f%% (threshold %d%%)\\n", total, min
|
|
25
|
+
}}'"""
|
|
26
|
+
|
|
27
|
+
_GOTESTSUM = "GOTOOLCHAIN=auto go run gotest.tools/gotestsum@latest"
|
|
28
|
+
_COBERTURA = "GOTOOLCHAIN=auto go run github.com/boumenot/gocover-cobertura@latest < coverage.out > coverage.xml"
|
|
29
|
+
_GOLANGCI_LINT = """curl -sSfL https://golangci-lint.run/install.sh | sh -s -- -b "$(go env GOPATH)/bin"
|
|
30
|
+
"$(go env GOPATH)/bin/golangci-lint" run"""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _working_directory(cfg: CIConfig) -> Dict[str, str]:
|
|
34
|
+
return {"working-directory": cfg.directory} if cfg.directory != "." else {}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class GoCI(CISteps):
|
|
38
|
+
version_key = "go-version"
|
|
39
|
+
codeql_language = "go"
|
|
40
|
+
codeql_build_mode = "autobuild"
|
|
41
|
+
|
|
42
|
+
def setup(self, cfg: CIConfig, version: Optional[str]) -> List[Item]:
|
|
43
|
+
with_: Dict[str, Any] = {"go-version": version} if version else {"go-version-file": cfg.path(cfg.version_file)}
|
|
44
|
+
if not cfg.cache:
|
|
45
|
+
with_["cache"] = False
|
|
46
|
+
elif cfg.directory != ".":
|
|
47
|
+
with_["cache-dependency-path"] = cfg.path("go.sum")
|
|
48
|
+
return [step("Set up Go", uses=ACTIONS["setup-go"], with_=with_)]
|
|
49
|
+
|
|
50
|
+
def image(self, cfg: CIConfig, version: str) -> str:
|
|
51
|
+
if version == "oldstable":
|
|
52
|
+
raise ValueError("Go 'oldstable' has no image: list explicit versions (e.g. 1.25) in [ci] versions")
|
|
53
|
+
# The "1" tag follows the latest release, like setup-go's "stable".
|
|
54
|
+
return f"golang:{'1' if version == 'stable' else version}"
|
|
55
|
+
|
|
56
|
+
def cache(self, cfg: CIConfig) -> Optional[Cache]:
|
|
57
|
+
if not cfg.cache:
|
|
58
|
+
return None
|
|
59
|
+
env = {"GOMODCACHE": f"{ROOT}/.go/mod", "GOCACHE": f"{ROOT}/.go/build"}
|
|
60
|
+
return Cache("go", (".go/mod", ".go/build"), key_files=("go.sum",), env=env)
|
|
61
|
+
|
|
62
|
+
def install(self, cfg: CIConfig, packages: Sequence[str] = ()) -> List[Step]:
|
|
63
|
+
return [step("Download modules", run=cfg.install)]
|
|
64
|
+
|
|
65
|
+
def lint(self, cfg: CIConfig, tools: List[str]) -> List[Item]:
|
|
66
|
+
steps = self.install(cfg)
|
|
67
|
+
for tool in tools:
|
|
68
|
+
if tool == "gofmt":
|
|
69
|
+
steps.append(step("gofmt", run=_GOFMT))
|
|
70
|
+
elif tool == "vet":
|
|
71
|
+
steps.append(step("go vet", run="go vet ./..."))
|
|
72
|
+
elif tool == "golangci-lint":
|
|
73
|
+
steps.append(
|
|
74
|
+
step(
|
|
75
|
+
"golangci-lint",
|
|
76
|
+
run=_GOLANGCI_LINT,
|
|
77
|
+
uses=ACTIONS["golangci-lint"],
|
|
78
|
+
with_={"version": "latest", **_working_directory(cfg)},
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
return steps
|
|
82
|
+
|
|
83
|
+
def test(self, cfg: CIConfig) -> List[Item]:
|
|
84
|
+
t = cfg.test
|
|
85
|
+
# gotestsum runs `go test` and writes JUnit results.
|
|
86
|
+
go_test = f"{_GOTESTSUM} --junitfile junit.xml --" if t.reports else "go test"
|
|
87
|
+
if not t.coverage:
|
|
88
|
+
return [*self.install(cfg), step("Tests", run=f"{go_test} -race ./...", secrets=t.env_secrets)]
|
|
89
|
+
return [
|
|
90
|
+
*self.install(cfg),
|
|
91
|
+
step(
|
|
92
|
+
"Tests with coverage",
|
|
93
|
+
run=f"{go_test} -race -covermode=atomic -coverprofile=coverage.out ./...",
|
|
94
|
+
secrets=t.env_secrets,
|
|
95
|
+
),
|
|
96
|
+
*([step("Coverage report (Cobertura)", run=_COBERTURA)] if t.reports else []),
|
|
97
|
+
step(
|
|
98
|
+
"Coverage threshold",
|
|
99
|
+
run=_COVERAGE_CHECK.format(threshold=t.coverage_threshold, summary=SUMMARY_FILE),
|
|
100
|
+
),
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
def coverage_file(self, cfg: CIConfig) -> str:
|
|
104
|
+
return "coverage.out"
|
|
105
|
+
|
|
106
|
+
def reports(self, cfg: CIConfig) -> TestReports:
|
|
107
|
+
coverage = cfg.test.coverage
|
|
108
|
+
return TestReports(
|
|
109
|
+
junit="junit.xml",
|
|
110
|
+
cobertura="coverage.xml" if coverage else "",
|
|
111
|
+
coverage_regex=THRESHOLD_REGEX if coverage else "",
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def audit(self, cfg: CIConfig) -> List[Item]:
|
|
115
|
+
# govulncheck@latest may need a newer Go than the project's: let go fetch it to build the tool, while the
|
|
116
|
+
# scan itself uses the project's toolchain (setup-go and the golang images pin GOTOOLCHAIN=local).
|
|
117
|
+
return [
|
|
118
|
+
*self.install(cfg),
|
|
119
|
+
step("govulncheck", run="GOTOOLCHAIN=auto go run golang.org/x/vuln/cmd/govulncheck@latest ./..."),
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
def build(self, cfg: CIConfig) -> List[Item]:
|
|
123
|
+
return [*self.install(cfg), step("Build", run=cfg.build.command)]
|
|
124
|
+
|
|
125
|
+
def dependabot_ecosystem(self, cfg: CIConfig) -> str:
|
|
126
|
+
return "gomod"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from quoll.languages.base import Detection
|
|
6
|
+
from quoll.project import Project
|
|
7
|
+
from quoll.services import detect_services
|
|
8
|
+
|
|
9
|
+
# Modules revealing a backing service.
|
|
10
|
+
_SERVICE_MODULES = {
|
|
11
|
+
"postgres": ("github.com/lib/pq", "github.com/jackc/pgx", "gorm.io/driver/postgres"),
|
|
12
|
+
"mysql": ("github.com/go-sql-driver/mysql", "gorm.io/driver/mysql"),
|
|
13
|
+
"redis": ("github.com/redis/go-redis", "github.com/go-redis/redis", "github.com/gomodule/redigo"),
|
|
14
|
+
"mongodb": ("go.mongodb.org/mongo-driver",),
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def detect(project: Project) -> Detection:
|
|
19
|
+
det = Detection(package_manager="go", install="go mod download", version_file="go.mod")
|
|
20
|
+
det.lockfile = "go.sum" if project.exists("go.sum") else ""
|
|
21
|
+
det.cache = bool(det.lockfile)
|
|
22
|
+
m = re.search(r"^go\s+(\d+\.\d+)", project.read_text("go.mod"), re.M)
|
|
23
|
+
det.version = m.group(1) if m else "stable"
|
|
24
|
+
det.matrix_versions = ["oldstable", "stable"]
|
|
25
|
+
det.lint_tools = ["gofmt", "vet", "golangci-lint"]
|
|
26
|
+
det.test_runner = "go"
|
|
27
|
+
det.build_command = "go build ./..."
|
|
28
|
+
go_mod = project.read_text("go.mod")
|
|
29
|
+
det.services = detect_services([m for ms in _SERVICE_MODULES.values() for m in ms if m in go_mod], _SERVICE_MODULES)
|
|
30
|
+
return det
|
|
@@ -0,0 +1,40 @@
|
|
|
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.project import Project
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from quoll.docker.config import DockerConfig
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class GoImage(DockerImage):
|
|
13
|
+
def defaults(self, project: Project, det: Detection) -> DockerDefaults:
|
|
14
|
+
package, warnings = _main_package(project)
|
|
15
|
+
return DockerDefaults(
|
|
16
|
+
port=8080,
|
|
17
|
+
command="/app",
|
|
18
|
+
build_command=f'go build -trimpath -ldflags="-s -w" -o /out/app {package}',
|
|
19
|
+
warnings=warnings,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
def context(self, project: Project, cfg: DockerConfig) -> Dict[str, Any]:
|
|
23
|
+
# go.mod without a go directive reads as "stable": use the latest Go.
|
|
24
|
+
return {"go_tag": "1" if cfg.version == "stable" else cfg.version, "go_sum": project.exists("go.sum")}
|
|
25
|
+
|
|
26
|
+
def dockerignore(self, cfg: DockerConfig) -> List[str]:
|
|
27
|
+
return ["bin", "coverage.out", ".env", ".env.*", "!.env.example"]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _main_package(project: Project):
|
|
31
|
+
"""The package to build: the root, cmd/, or the only cmd/<name>/."""
|
|
32
|
+
if project.exists("main.go"):
|
|
33
|
+
return ".", []
|
|
34
|
+
if project.exists("cmd/main.go"):
|
|
35
|
+
return "./cmd", []
|
|
36
|
+
commands = sorted(p.parent.relative_to(project.root).as_posix() for p in project.glob("cmd/*/main.go"))
|
|
37
|
+
if not commands:
|
|
38
|
+
return ".", ["No main package found (main.go or cmd/<name>/main.go) — check docker.build_command."]
|
|
39
|
+
warnings = [] if len(commands) == 1 else [f"Several commands in cmd/ — building ./{commands[0]}."]
|
|
40
|
+
return f"./{commands[0]}", warnings
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{% block build %}
|
|
2
|
+
FROM {{ build_image }} AS build
|
|
3
|
+
WORKDIR /src
|
|
4
|
+
COPY . .
|
|
5
|
+
RUN --mount=type=cache,target={{ cache }} \
|
|
6
|
+
{% if wrapper %}
|
|
7
|
+
chmod +x {{ wrapper }} \
|
|
8
|
+
&& {{ cfg.build_command }} \
|
|
9
|
+
{% else %}
|
|
10
|
+
{{ cfg.build_command }} \
|
|
11
|
+
{% endif %}
|
|
12
|
+
&& mkdir -p /out \
|
|
13
|
+
&& cp "$(find {{ jar_dir }} -maxdepth 1 -name '*.jar' ! -name '*-plain.jar' ! -name '*-sources.jar' ! -name '*-javadoc.jar' | head -n 1)" /out/app.jar
|
|
14
|
+
{% endblock %}
|
|
15
|
+
|
|
16
|
+
{% block runtime %}
|
|
17
|
+
FROM eclipse-temurin:{{ cfg.version }}-jre
|
|
18
|
+
{{ os_updates }}
|
|
19
|
+
# A numeric user: Kubernetes can then check it isn't root (runAsNonRoot).
|
|
20
|
+
RUN groupadd --system --gid 10001 app && useradd --system --uid 10001 --gid app --no-create-home app
|
|
21
|
+
WORKDIR /app
|
|
22
|
+
{% block app_files %}
|
|
23
|
+
COPY --from=build --chown=app:app /out/app.jar /app/app.jar
|
|
24
|
+
{% endblock %}
|
|
25
|
+
USER 10001:10001
|
|
26
|
+
EXPOSE {{ cfg.port }}
|
|
27
|
+
CMD {{ cfg.command | exec_form }}
|
|
28
|
+
{% endblock %}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""JVM projects (Java, Kotlin) built with Maven or Gradle."""
|
|
2
|
+
|
|
3
|
+
from quoll.languages.base import Language
|
|
4
|
+
from quoll.languages.java.ci import JavaCI
|
|
5
|
+
from quoll.languages.java.detect import detect
|
|
6
|
+
from quoll.languages.java.docker import JavaImage
|
|
7
|
+
from quoll.languages.java.frameworks import spring_boot
|
|
8
|
+
|
|
9
|
+
language = Language(
|
|
10
|
+
name="java",
|
|
11
|
+
markers=("pom.xml", "build.gradle", "build.gradle.kts", "settings.gradle", "settings.gradle.kts"),
|
|
12
|
+
# The -wrapper variants run the committed ./mvnw or ./gradlew.
|
|
13
|
+
package_managers=("maven", "maven-wrapper", "gradle", "gradle-wrapper"),
|
|
14
|
+
lint_tools=("spotless", "checkstyle", "ktlint", "detekt", "spotbugs", "pmd"),
|
|
15
|
+
test_runners=("junit",),
|
|
16
|
+
detect=detect,
|
|
17
|
+
ci=JavaCI(),
|
|
18
|
+
frameworks=(spring_boot.framework,),
|
|
19
|
+
docker=JavaImage(),
|
|
20
|
+
)
|