Skip to content

Commit 200c0e8

Browse files
feat(container): add Swift and Kotlin language ecosystem support (#19)
* fix(security): pin trivy-action to SHA after supply chain attack All trivy-action tags before 0.35.0 were force-pushed to malicious commits during March 19-20 2026 (aquasecurity/trivy-action#541). Pin to SHA 57a97c7 (v0.35.0) which was not compromised. Our CI runs on March 19 completed at ~04:58 UTC, before the attack window (~17:43 UTC), so no secrets were exposed. This is a preventive fix. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(container): add Swift and Kotlin language ecosystem support Add Swift toolchain (SwiftLint, swift-format, swift test) and Kotlin toolchain (ktlint, detekt, Gradle, JDK 21) to the dev-toolchain container with full Makefile target support for lint, format, fix, test, and security. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(bash): add shellcheck source directives to Swift and Kotlin test scripts Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(bash): fix shfmt formatting in install-kotlin.sh heredoc Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(container): use correct eclipse-temurin tag (21-jdk, not 21-jdk-bookworm) eclipse-temurin images are Ubuntu-based; the bookworm variant does not exist. Using the default 21-jdk tag since we only COPY the JDK directory. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(container): use correct Swift image tag (6.1-bookworm, no slim variant) Swift official images do not have a -slim variant. Use swift:6.1-bookworm as the builder stage. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(container): build swift-format from source in builder stage swift-format has no pre-built Linux binaries on GitHub releases. Build from source (v602.0.0) in the swift-builder stage and COPY the binary to runtime, matching the pattern used for Go tools. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ac2d591 commit 200c0e8

File tree

6 files changed

+471
-2
lines changed

6 files changed

+471
-2
lines changed

Dockerfile

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,20 @@ RUN curl -L --proto '=https' --tlsv1.2 -sSf \
4040
| bash
4141
RUN cargo binstall --no-confirm cargo-audit cargo-deny
4242

43+
# === Swift builder stage ===
44+
# Provides Swift toolchain and builds swift-format from source
45+
# (swift-format has no pre-built Linux binaries)
46+
FROM swift:6.1-bookworm AS swift-builder
47+
RUN git clone --depth 1 --branch 602.0.0 https://github.com/swiftlang/swift-format.git /tmp/swift-format \
48+
&& cd /tmp/swift-format \
49+
&& swift build -c release \
50+
&& install -m 755 .build/release/swift-format /usr/local/bin/swift-format \
51+
&& rm -rf /tmp/swift-format
52+
53+
# === JDK builder stage ===
54+
# Provides JDK 21 for Kotlin tooling (ktlint, detekt, Gradle)
55+
FROM eclipse-temurin:21-jdk AS jdk-builder
56+
4357
# === Node.js base: provides Node runtime for JS/TS tooling ===
4458
FROM node:22-bookworm-slim AS node-base
4559

@@ -110,8 +124,18 @@ COPY --from=rust-builder /usr/local/cargo /usr/local/cargo
110124
ENV RUSTUP_HOME=/usr/local/rustup
111125
ENV CARGO_HOME=/usr/local/cargo
112126

113-
# Set up environment
114-
ENV PATH="/opt/devrail/bin:/usr/local/cargo/bin:/usr/local/go/bin:${PATH}"
127+
# Copy Swift toolchain from swift-builder (selective: binaries + runtime libs + swift-format)
128+
COPY --from=swift-builder /usr/bin/swift /usr/bin/swiftc /usr/bin/swift-build /usr/bin/swift-test /usr/bin/swift-package /usr/bin/swift-run /usr/local/swift/bin/
129+
COPY --from=swift-builder /usr/lib/swift /usr/local/swift/lib/swift
130+
COPY --from=swift-builder /usr/lib/swift_static /usr/local/swift/lib/swift_static
131+
COPY --from=swift-builder /usr/local/bin/swift-format /usr/local/bin/swift-format
132+
133+
# Copy JDK 21 from jdk-builder (required for Kotlin tooling: ktlint, detekt, Gradle)
134+
COPY --from=jdk-builder /opt/java/openjdk /opt/java/openjdk
135+
ENV JAVA_HOME=/opt/java/openjdk
136+
137+
# Set up environment (consolidated PATH — all language runtimes in one line)
138+
ENV PATH="/opt/devrail/bin:/usr/local/cargo/bin:/usr/local/go/bin:/usr/local/swift/bin:/opt/java/openjdk/bin:${PATH}"
115139
ENV DEVRAIL_LIB="/opt/devrail/lib"
116140

117141
# Copy Go SDK from builder (required at runtime by golangci-lint, govulncheck)
@@ -135,6 +159,8 @@ RUN bash /opt/devrail/scripts/install-ruby.sh
135159
RUN bash /opt/devrail/scripts/install-go.sh
136160
RUN bash /opt/devrail/scripts/install-javascript.sh
137161
RUN bash /opt/devrail/scripts/install-rust.sh
162+
RUN bash /opt/devrail/scripts/install-swift.sh
163+
RUN bash /opt/devrail/scripts/install-kotlin.sh
138164
RUN bash /opt/devrail/scripts/install-universal.sh
139165

140166
# Allow git operations on mounted workspaces with different ownership

Makefile

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ HAS_RUBY := $(filter ruby,$(LANGUAGES))
4343
HAS_GO := $(filter go,$(LANGUAGES))
4444
HAS_JAVASCRIPT := $(filter javascript,$(LANGUAGES))
4545
HAS_RUST := $(filter rust,$(LANGUAGES))
46+
HAS_SWIFT := $(filter swift,$(LANGUAGES))
47+
HAS_KOTLIN := $(filter kotlin,$(LANGUAGES))
4648

4749
# ---------------------------------------------------------------------------
4850
# .PHONY declarations
@@ -283,6 +285,39 @@ _lint: _check-config
283285
exit $$overall_exit; \
284286
fi; \
285287
fi; \
288+
if [ -n "$(HAS_SWIFT)" ]; then \
289+
ran_languages="$${ran_languages}\"swift\","; \
290+
swift_files=$$(find . -name '*.swift' -not -path './.git/*' -not -path './.build/*' -not -path './DerivedData/*' 2>/dev/null); \
291+
if [ -n "$$swift_files" ]; then \
292+
swiftlint lint --strict || { overall_exit=1; failed_languages="$${failed_languages}\"swift\","; }; \
293+
else \
294+
echo '{"level":"info","msg":"skipping swift lint: no .swift files found","language":"swift"}' >&2; \
295+
fi; \
296+
if [ "$(DEVRAIL_FAIL_FAST)" = "1" ] && [ $$overall_exit -ne 0 ]; then \
297+
end_time=$$(date +%s%3N); \
298+
duration=$$((end_time - start_time)); \
299+
echo "{\"target\":\"lint\",\"status\":\"fail\",\"duration_ms\":$$duration,\"languages\":[$${ran_languages%,}],\"failed\":[$${failed_languages%,}]}"; \
300+
exit $$overall_exit; \
301+
fi; \
302+
fi; \
303+
if [ -n "$(HAS_KOTLIN)" ]; then \
304+
ran_languages="$${ran_languages}\"kotlin\","; \
305+
kt_files=$$(find . \( -name '*.kt' -o -name '*.kts' \) -not -path './.git/*' -not -path './build/*' -not -path './.gradle/*' 2>/dev/null); \
306+
if [ -n "$$kt_files" ]; then \
307+
ktlint || { overall_exit=1; failed_languages="$${failed_languages}\"kotlin:ktlint\","; }; \
308+
else \
309+
echo '{"level":"info","msg":"skipping kotlin lint: no .kt/.kts files found","language":"kotlin"}' >&2; \
310+
fi; \
311+
if [ -f "detekt.yml" ] && [ -n "$$kt_files" ]; then \
312+
detekt-cli --build-upon-default-config --config detekt.yml || { overall_exit=1; failed_languages="$${failed_languages}\"kotlin:detekt\","; }; \
313+
fi; \
314+
if [ "$(DEVRAIL_FAIL_FAST)" = "1" ] && [ $$overall_exit -ne 0 ]; then \
315+
end_time=$$(date +%s%3N); \
316+
duration=$$((end_time - start_time)); \
317+
echo "{\"target\":\"lint\",\"status\":\"fail\",\"duration_ms\":$$duration,\"languages\":[$${ran_languages%,}],\"failed\":[$${failed_languages%,}]}"; \
318+
exit $$overall_exit; \
319+
fi; \
320+
fi; \
286321
end_time=$$(date +%s%3N); \
287322
duration=$$((end_time - start_time)); \
288323
if [ $$overall_exit -eq 0 ]; then \
@@ -401,6 +436,36 @@ _format: _check-config
401436
exit $$overall_exit; \
402437
fi; \
403438
fi; \
439+
if [ -n "$(HAS_SWIFT)" ]; then \
440+
ran_languages="$${ran_languages}\"swift\","; \
441+
swift_files=$$(find . -name '*.swift' -not -path './.git/*' -not -path './.build/*' -not -path './DerivedData/*' 2>/dev/null); \
442+
if [ -n "$$swift_files" ]; then \
443+
swift-format lint --strict -r . || { overall_exit=1; failed_languages="$${failed_languages}\"swift\","; }; \
444+
else \
445+
echo '{"level":"info","msg":"skipping swift format: no .swift files found","language":"swift"}' >&2; \
446+
fi; \
447+
if [ "$(DEVRAIL_FAIL_FAST)" = "1" ] && [ $$overall_exit -ne 0 ]; then \
448+
end_time=$$(date +%s%3N); \
449+
duration=$$((end_time - start_time)); \
450+
echo "{\"target\":\"format\",\"status\":\"fail\",\"duration_ms\":$$duration,\"languages\":[$${ran_languages%,}],\"failed\":[$${failed_languages%,}]}"; \
451+
exit $$overall_exit; \
452+
fi; \
453+
fi; \
454+
if [ -n "$(HAS_KOTLIN)" ]; then \
455+
ran_languages="$${ran_languages}\"kotlin\","; \
456+
kt_files=$$(find . \( -name '*.kt' -o -name '*.kts' \) -not -path './.git/*' -not -path './build/*' -not -path './.gradle/*' 2>/dev/null); \
457+
if [ -n "$$kt_files" ]; then \
458+
ktlint --format --dry-run || { overall_exit=1; failed_languages="$${failed_languages}\"kotlin\","; }; \
459+
else \
460+
echo '{"level":"info","msg":"skipping kotlin format: no .kt/.kts files found","language":"kotlin"}' >&2; \
461+
fi; \
462+
if [ "$(DEVRAIL_FAIL_FAST)" = "1" ] && [ $$overall_exit -ne 0 ]; then \
463+
end_time=$$(date +%s%3N); \
464+
duration=$$((end_time - start_time)); \
465+
echo "{\"target\":\"format\",\"status\":\"fail\",\"duration_ms\":$$duration,\"languages\":[$${ran_languages%,}],\"failed\":[$${failed_languages%,}]}"; \
466+
exit $$overall_exit; \
467+
fi; \
468+
fi; \
404469
end_time=$$(date +%s%3N); \
405470
duration=$$((end_time - start_time)); \
406471
if [ $$overall_exit -eq 0 ]; then \
@@ -519,6 +584,36 @@ _fix: _check-config
519584
exit $$overall_exit; \
520585
fi; \
521586
fi; \
587+
if [ -n "$(HAS_SWIFT)" ]; then \
588+
ran_languages="$${ran_languages}\"swift\","; \
589+
swift_files=$$(find . -name '*.swift' -not -path './.git/*' -not -path './.build/*' -not -path './DerivedData/*' 2>/dev/null); \
590+
if [ -n "$$swift_files" ]; then \
591+
swift-format format -i -r . || { overall_exit=1; failed_languages="$${failed_languages}\"swift\","; }; \
592+
else \
593+
echo '{"level":"info","msg":"skipping swift fix: no .swift files found","language":"swift"}' >&2; \
594+
fi; \
595+
if [ "$(DEVRAIL_FAIL_FAST)" = "1" ] && [ $$overall_exit -ne 0 ]; then \
596+
end_time=$$(date +%s%3N); \
597+
duration=$$((end_time - start_time)); \
598+
echo "{\"target\":\"fix\",\"status\":\"fail\",\"duration_ms\":$$duration,\"languages\":[$${ran_languages%,}],\"failed\":[$${failed_languages%,}]}"; \
599+
exit $$overall_exit; \
600+
fi; \
601+
fi; \
602+
if [ -n "$(HAS_KOTLIN)" ]; then \
603+
ran_languages="$${ran_languages}\"kotlin\","; \
604+
kt_files=$$(find . \( -name '*.kt' -o -name '*.kts' \) -not -path './.git/*' -not -path './build/*' -not -path './.gradle/*' 2>/dev/null); \
605+
if [ -n "$$kt_files" ]; then \
606+
ktlint --format || { overall_exit=1; failed_languages="$${failed_languages}\"kotlin\","; }; \
607+
else \
608+
echo '{"level":"info","msg":"skipping kotlin fix: no .kt/.kts files found","language":"kotlin"}' >&2; \
609+
fi; \
610+
if [ "$(DEVRAIL_FAIL_FAST)" = "1" ] && [ $$overall_exit -ne 0 ]; then \
611+
end_time=$$(date +%s%3N); \
612+
duration=$$((end_time - start_time)); \
613+
echo "{\"target\":\"fix\",\"status\":\"fail\",\"duration_ms\":$$duration,\"languages\":[$${ran_languages%,}],\"failed\":[$${failed_languages%,}]}"; \
614+
exit $$overall_exit; \
615+
fi; \
616+
fi; \
522617
end_time=$$(date +%s%3N); \
523618
duration=$$((end_time - start_time)); \
524619
if [ $$overall_exit -eq 0 ]; then \
@@ -656,6 +751,37 @@ _test: _check-config
656751
exit $$overall_exit; \
657752
fi; \
658753
fi; \
754+
if [ -n "$(HAS_SWIFT)" ]; then \
755+
swift_files=$$(find . -name '*.swift' -not -path './.git/*' -not -path './.build/*' -not -path './DerivedData/*' 2>/dev/null); \
756+
if [ -n "$$swift_files" ] && [ -f "Package.swift" ]; then \
757+
ran_languages="$${ran_languages}\"swift\","; \
758+
swift test || { overall_exit=1; failed_languages="$${failed_languages}\"swift\","; }; \
759+
else \
760+
skipped_languages="$${skipped_languages}\"swift\","; \
761+
echo '{"level":"info","msg":"skipping swift tests: no .swift files or Package.swift found","language":"swift"}' >&2; \
762+
fi; \
763+
if [ "$(DEVRAIL_FAIL_FAST)" = "1" ] && [ $$overall_exit -ne 0 ]; then \
764+
end_time=$$(date +%s%3N); \
765+
duration=$$((end_time - start_time)); \
766+
echo "{\"target\":\"test\",\"status\":\"fail\",\"duration_ms\":$$duration,\"languages\":[$${ran_languages%,}],\"failed\":[$${failed_languages%,}],\"skipped\":[$${skipped_languages%,}]}"; \
767+
exit $$overall_exit; \
768+
fi; \
769+
fi; \
770+
if [ -n "$(HAS_KOTLIN)" ]; then \
771+
if [ -f "build.gradle.kts" ] || [ -f "build.gradle" ]; then \
772+
ran_languages="$${ran_languages}\"kotlin\","; \
773+
gradle test || { overall_exit=1; failed_languages="$${failed_languages}\"kotlin\","; }; \
774+
else \
775+
skipped_languages="$${skipped_languages}\"kotlin\","; \
776+
echo '{"level":"info","msg":"skipping kotlin tests: no build.gradle.kts or build.gradle found","language":"kotlin"}' >&2; \
777+
fi; \
778+
if [ "$(DEVRAIL_FAIL_FAST)" = "1" ] && [ $$overall_exit -ne 0 ]; then \
779+
end_time=$$(date +%s%3N); \
780+
duration=$$((end_time - start_time)); \
781+
echo "{\"target\":\"test\",\"status\":\"fail\",\"duration_ms\":$$duration,\"languages\":[$${ran_languages%,}],\"failed\":[$${failed_languages%,}],\"skipped\":[$${skipped_languages%,}]}"; \
782+
exit $$overall_exit; \
783+
fi; \
784+
fi; \
659785
end_time=$$(date +%s%3N); \
660786
duration=$$((end_time - start_time)); \
661787
if [ -z "$${ran_languages}" ] && [ -n "$${skipped_languages}" ]; then \
@@ -797,6 +923,25 @@ _security: _check-config
797923
exit $$overall_exit; \
798924
fi; \
799925
fi; \
926+
if [ -n "$(HAS_SWIFT)" ]; then \
927+
skipped_languages="$${skipped_languages}\"swift\","; \
928+
echo '{"level":"info","msg":"skipping swift security: no language-specific scanner","language":"swift"}' >&2; \
929+
fi; \
930+
if [ -n "$(HAS_KOTLIN)" ]; then \
931+
if [ -f "build.gradle.kts" ] || [ -f "build.gradle" ]; then \
932+
ran_languages="$${ran_languages}\"kotlin\","; \
933+
gradle dependencyCheckAnalyze || { overall_exit=1; failed_languages="$${failed_languages}\"kotlin:owasp\","; }; \
934+
else \
935+
skipped_languages="$${skipped_languages}\"kotlin\","; \
936+
echo '{"level":"info","msg":"skipping kotlin security: no build.gradle.kts or build.gradle found","language":"kotlin"}' >&2; \
937+
fi; \
938+
if [ "$(DEVRAIL_FAIL_FAST)" = "1" ] && [ $$overall_exit -ne 0 ]; then \
939+
end_time=$$(date +%s%3N); \
940+
duration=$$((end_time - start_time)); \
941+
echo "{\"target\":\"security\",\"status\":\"fail\",\"duration_ms\":$$duration,\"languages\":[$${ran_languages%,}],\"failed\":[$${failed_languages%,}]}"; \
942+
exit $$overall_exit; \
943+
fi; \
944+
fi; \
800945
end_time=$$(date +%s%3N); \
801946
duration=$$((end_time - start_time)); \
802947
if [ -z "$${ran_languages}" ] && [ -n "$${skipped_languages}" ]; then \

scripts/install-kotlin.sh

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
#!/usr/bin/env bash
2+
# scripts/install-kotlin.sh — Install and verify Kotlin tooling for DevRail
3+
#
4+
# Purpose: Installs Kotlin development tools (ktlint, detekt, Gradle) and verifies
5+
# the JDK is available in the dev-toolchain container. JDK 21 is COPY'd
6+
# from the jdk-builder stage; this script installs additional tools.
7+
# Usage: bash scripts/install-kotlin.sh [--help]
8+
# Dependencies: lib/log.sh, lib/platform.sh
9+
#
10+
# Tools installed/verified:
11+
# - java (JDK 21 — COPY'd from builder)
12+
# - ktlint (Linter/formatter — downloaded binary)
13+
# - detekt-cli (Static analysis — downloaded JAR)
14+
# - gradle (Build tool — downloaded distribution)
15+
16+
set -euo pipefail
17+
18+
# --- Resolve library path ---
19+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
20+
DEVRAIL_LIB="${DEVRAIL_LIB:-${SCRIPT_DIR}/../lib}"
21+
22+
# shellcheck source=../lib/log.sh
23+
source "${DEVRAIL_LIB}/log.sh"
24+
# shellcheck source=../lib/platform.sh
25+
source "${DEVRAIL_LIB}/platform.sh"
26+
27+
# --- Help ---
28+
if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
29+
log_info "install-kotlin.sh — Install and verify Kotlin tooling for DevRail"
30+
log_info "Usage: bash scripts/install-kotlin.sh [--help]"
31+
log_info "Tools: java, ktlint, detekt-cli, gradle"
32+
exit 0
33+
fi
34+
35+
# --- Cleanup trap ---
36+
TMPDIR_CLEANUP=""
37+
cleanup() {
38+
if [[ -n "${TMPDIR_CLEANUP}" && -d "${TMPDIR_CLEANUP}" ]]; then
39+
rm -rf "${TMPDIR_CLEANUP}"
40+
fi
41+
}
42+
trap cleanup EXIT
43+
44+
# --- Tool installation functions ---
45+
46+
install_ktlint() {
47+
if command -v ktlint &>/dev/null; then
48+
log_info "ktlint already installed, skipping"
49+
return 0
50+
fi
51+
52+
log_info "Installing ktlint..."
53+
local version="1.5.0"
54+
curl -fsSL "https://github.com/pinterest/ktlint/releases/download/${version}/ktlint" \
55+
-o /usr/local/bin/ktlint
56+
chmod +x /usr/local/bin/ktlint
57+
58+
require_cmd "ktlint" "Failed to install ktlint"
59+
log_info "ktlint installed successfully"
60+
}
61+
62+
install_detekt() {
63+
if [ -f /usr/local/lib/detekt-cli.jar ]; then
64+
log_info "detekt-cli already installed, skipping"
65+
return 0
66+
fi
67+
68+
log_info "Installing detekt-cli..."
69+
local version="1.23.7"
70+
curl -fsSL "https://github.com/detekt/detekt/releases/download/v${version}/detekt-cli-${version}-all.jar" \
71+
-o /usr/local/lib/detekt-cli.jar
72+
73+
# Create wrapper script
74+
cat >/usr/local/bin/detekt-cli <<'WRAPPER'
75+
#!/usr/bin/env bash
76+
exec java -jar /usr/local/lib/detekt-cli.jar "$@"
77+
WRAPPER
78+
chmod +x /usr/local/bin/detekt-cli
79+
80+
require_cmd "detekt-cli" "Failed to install detekt-cli"
81+
log_info "detekt-cli installed successfully"
82+
}
83+
84+
install_gradle() {
85+
if command -v gradle &>/dev/null; then
86+
log_info "gradle already installed, skipping"
87+
return 0
88+
fi
89+
90+
log_info "Installing Gradle..."
91+
local version="8.12"
92+
TMPDIR_CLEANUP="$(mktemp -d)"
93+
curl -fsSL "https://services.gradle.org/distributions/gradle-${version}-bin.zip" \
94+
-o "${TMPDIR_CLEANUP}/gradle.zip"
95+
unzip -q "${TMPDIR_CLEANUP}/gradle.zip" -d /opt
96+
ln -sf "/opt/gradle-${version}/bin/gradle" /usr/local/bin/gradle
97+
98+
require_cmd "gradle" "Failed to install Gradle"
99+
log_info "Gradle installed successfully"
100+
}
101+
102+
# --- Main ---
103+
log_info "Installing Kotlin tools..."
104+
105+
# Verify JDK is available (COPY'd from builder)
106+
if command -v java &>/dev/null; then
107+
log_info "java is already installed"
108+
else
109+
log_warn "java not found — expected to be copied from JDK builder stage"
110+
fi
111+
112+
install_ktlint
113+
install_detekt
114+
install_gradle
115+
116+
log_info "Kotlin tools installed successfully"

0 commit comments

Comments
 (0)