Skip to content
Merged
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ jobs:
# Blocking scan: OS packages only. We control the base image and can act on
# these. ignore-unfixed skips CVEs with no Debian patch available yet.
- name: Run trivy OS package scan (blocking)
uses: aquasecurity/trivy-action@0.28.0
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0
with:
image-ref: ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}
severity: CRITICAL,HIGH
Expand All @@ -59,7 +59,7 @@ jobs:
# are uploaded to the GitHub Security tab for visibility. Not blocking
# because Go binary CVEs depend on upstream tool releases we don't control.
- name: Run trivy full scan (SARIF, advisory)
uses: aquasecurity/trivy-action@0.28.0
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0
if: always()
with:
image-ref: ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}
Expand Down
30 changes: 28 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,20 @@ RUN curl -L --proto '=https' --tlsv1.2 -sSf \
| bash
RUN cargo binstall --no-confirm cargo-audit cargo-deny

# === Swift builder stage ===
# Provides Swift toolchain and builds swift-format from source
# (swift-format has no pre-built Linux binaries)
FROM swift:6.1-bookworm AS swift-builder
RUN git clone --depth 1 --branch 602.0.0 https://github.com/swiftlang/swift-format.git /tmp/swift-format \
&& cd /tmp/swift-format \
&& swift build -c release \
&& install -m 755 .build/release/swift-format /usr/local/bin/swift-format \
&& rm -rf /tmp/swift-format

# === JDK builder stage ===
# Provides JDK 21 for Kotlin tooling (ktlint, detekt, Gradle)
FROM eclipse-temurin:21-jdk AS jdk-builder

# === Node.js base: provides Node runtime for JS/TS tooling ===
FROM node:22-bookworm-slim AS node-base

Expand Down Expand Up @@ -110,8 +124,18 @@ COPY --from=rust-builder /usr/local/cargo /usr/local/cargo
ENV RUSTUP_HOME=/usr/local/rustup
ENV CARGO_HOME=/usr/local/cargo

# Set up environment
ENV PATH="/opt/devrail/bin:/usr/local/cargo/bin:/usr/local/go/bin:${PATH}"
# Copy Swift toolchain from swift-builder (selective: binaries + runtime libs + swift-format)
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/
COPY --from=swift-builder /usr/lib/swift /usr/local/swift/lib/swift
COPY --from=swift-builder /usr/lib/swift_static /usr/local/swift/lib/swift_static
COPY --from=swift-builder /usr/local/bin/swift-format /usr/local/bin/swift-format

# Copy JDK 21 from jdk-builder (required for Kotlin tooling: ktlint, detekt, Gradle)
COPY --from=jdk-builder /opt/java/openjdk /opt/java/openjdk
ENV JAVA_HOME=/opt/java/openjdk

# Set up environment (consolidated PATH — all language runtimes in one line)
ENV PATH="/opt/devrail/bin:/usr/local/cargo/bin:/usr/local/go/bin:/usr/local/swift/bin:/opt/java/openjdk/bin:${PATH}"
ENV DEVRAIL_LIB="/opt/devrail/lib"

# Copy Go SDK from builder (required at runtime by golangci-lint, govulncheck)
Expand All @@ -135,6 +159,8 @@ RUN bash /opt/devrail/scripts/install-ruby.sh
RUN bash /opt/devrail/scripts/install-go.sh
RUN bash /opt/devrail/scripts/install-javascript.sh
RUN bash /opt/devrail/scripts/install-rust.sh
RUN bash /opt/devrail/scripts/install-swift.sh
RUN bash /opt/devrail/scripts/install-kotlin.sh
RUN bash /opt/devrail/scripts/install-universal.sh

# Allow git operations on mounted workspaces with different ownership
Expand Down
145 changes: 145 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ HAS_RUBY := $(filter ruby,$(LANGUAGES))
HAS_GO := $(filter go,$(LANGUAGES))
HAS_JAVASCRIPT := $(filter javascript,$(LANGUAGES))
HAS_RUST := $(filter rust,$(LANGUAGES))
HAS_SWIFT := $(filter swift,$(LANGUAGES))
HAS_KOTLIN := $(filter kotlin,$(LANGUAGES))

# ---------------------------------------------------------------------------
# .PHONY declarations
Expand Down Expand Up @@ -283,6 +285,39 @@ _lint: _check-config
exit $$overall_exit; \
fi; \
fi; \
if [ -n "$(HAS_SWIFT)" ]; then \
ran_languages="$${ran_languages}\"swift\","; \
swift_files=$$(find . -name '*.swift' -not -path './.git/*' -not -path './.build/*' -not -path './DerivedData/*' 2>/dev/null); \
if [ -n "$$swift_files" ]; then \
swiftlint lint --strict || { overall_exit=1; failed_languages="$${failed_languages}\"swift\","; }; \
else \
echo '{"level":"info","msg":"skipping swift lint: no .swift files found","language":"swift"}' >&2; \
fi; \
if [ "$(DEVRAIL_FAIL_FAST)" = "1" ] && [ $$overall_exit -ne 0 ]; then \
end_time=$$(date +%s%3N); \
duration=$$((end_time - start_time)); \
echo "{\"target\":\"lint\",\"status\":\"fail\",\"duration_ms\":$$duration,\"languages\":[$${ran_languages%,}],\"failed\":[$${failed_languages%,}]}"; \
exit $$overall_exit; \
fi; \
fi; \
if [ -n "$(HAS_KOTLIN)" ]; then \
ran_languages="$${ran_languages}\"kotlin\","; \
kt_files=$$(find . \( -name '*.kt' -o -name '*.kts' \) -not -path './.git/*' -not -path './build/*' -not -path './.gradle/*' 2>/dev/null); \
if [ -n "$$kt_files" ]; then \
ktlint || { overall_exit=1; failed_languages="$${failed_languages}\"kotlin:ktlint\","; }; \
else \
echo '{"level":"info","msg":"skipping kotlin lint: no .kt/.kts files found","language":"kotlin"}' >&2; \
fi; \
if [ -f "detekt.yml" ] && [ -n "$$kt_files" ]; then \
detekt-cli --build-upon-default-config --config detekt.yml || { overall_exit=1; failed_languages="$${failed_languages}\"kotlin:detekt\","; }; \
fi; \
if [ "$(DEVRAIL_FAIL_FAST)" = "1" ] && [ $$overall_exit -ne 0 ]; then \
end_time=$$(date +%s%3N); \
duration=$$((end_time - start_time)); \
echo "{\"target\":\"lint\",\"status\":\"fail\",\"duration_ms\":$$duration,\"languages\":[$${ran_languages%,}],\"failed\":[$${failed_languages%,}]}"; \
exit $$overall_exit; \
fi; \
fi; \
end_time=$$(date +%s%3N); \
duration=$$((end_time - start_time)); \
if [ $$overall_exit -eq 0 ]; then \
Expand Down Expand Up @@ -401,6 +436,36 @@ _format: _check-config
exit $$overall_exit; \
fi; \
fi; \
if [ -n "$(HAS_SWIFT)" ]; then \
ran_languages="$${ran_languages}\"swift\","; \
swift_files=$$(find . -name '*.swift' -not -path './.git/*' -not -path './.build/*' -not -path './DerivedData/*' 2>/dev/null); \
if [ -n "$$swift_files" ]; then \
swift-format lint --strict -r . || { overall_exit=1; failed_languages="$${failed_languages}\"swift\","; }; \
else \
echo '{"level":"info","msg":"skipping swift format: no .swift files found","language":"swift"}' >&2; \
fi; \
if [ "$(DEVRAIL_FAIL_FAST)" = "1" ] && [ $$overall_exit -ne 0 ]; then \
end_time=$$(date +%s%3N); \
duration=$$((end_time - start_time)); \
echo "{\"target\":\"format\",\"status\":\"fail\",\"duration_ms\":$$duration,\"languages\":[$${ran_languages%,}],\"failed\":[$${failed_languages%,}]}"; \
exit $$overall_exit; \
fi; \
fi; \
if [ -n "$(HAS_KOTLIN)" ]; then \
ran_languages="$${ran_languages}\"kotlin\","; \
kt_files=$$(find . \( -name '*.kt' -o -name '*.kts' \) -not -path './.git/*' -not -path './build/*' -not -path './.gradle/*' 2>/dev/null); \
if [ -n "$$kt_files" ]; then \
ktlint --format --dry-run || { overall_exit=1; failed_languages="$${failed_languages}\"kotlin\","; }; \
else \
echo '{"level":"info","msg":"skipping kotlin format: no .kt/.kts files found","language":"kotlin"}' >&2; \
fi; \
if [ "$(DEVRAIL_FAIL_FAST)" = "1" ] && [ $$overall_exit -ne 0 ]; then \
end_time=$$(date +%s%3N); \
duration=$$((end_time - start_time)); \
echo "{\"target\":\"format\",\"status\":\"fail\",\"duration_ms\":$$duration,\"languages\":[$${ran_languages%,}],\"failed\":[$${failed_languages%,}]}"; \
exit $$overall_exit; \
fi; \
fi; \
end_time=$$(date +%s%3N); \
duration=$$((end_time - start_time)); \
if [ $$overall_exit -eq 0 ]; then \
Expand Down Expand Up @@ -519,6 +584,36 @@ _fix: _check-config
exit $$overall_exit; \
fi; \
fi; \
if [ -n "$(HAS_SWIFT)" ]; then \
ran_languages="$${ran_languages}\"swift\","; \
swift_files=$$(find . -name '*.swift' -not -path './.git/*' -not -path './.build/*' -not -path './DerivedData/*' 2>/dev/null); \
if [ -n "$$swift_files" ]; then \
swift-format format -i -r . || { overall_exit=1; failed_languages="$${failed_languages}\"swift\","; }; \
else \
echo '{"level":"info","msg":"skipping swift fix: no .swift files found","language":"swift"}' >&2; \
fi; \
if [ "$(DEVRAIL_FAIL_FAST)" = "1" ] && [ $$overall_exit -ne 0 ]; then \
end_time=$$(date +%s%3N); \
duration=$$((end_time - start_time)); \
echo "{\"target\":\"fix\",\"status\":\"fail\",\"duration_ms\":$$duration,\"languages\":[$${ran_languages%,}],\"failed\":[$${failed_languages%,}]}"; \
exit $$overall_exit; \
fi; \
fi; \
if [ -n "$(HAS_KOTLIN)" ]; then \
ran_languages="$${ran_languages}\"kotlin\","; \
kt_files=$$(find . \( -name '*.kt' -o -name '*.kts' \) -not -path './.git/*' -not -path './build/*' -not -path './.gradle/*' 2>/dev/null); \
if [ -n "$$kt_files" ]; then \
ktlint --format || { overall_exit=1; failed_languages="$${failed_languages}\"kotlin\","; }; \
else \
echo '{"level":"info","msg":"skipping kotlin fix: no .kt/.kts files found","language":"kotlin"}' >&2; \
fi; \
if [ "$(DEVRAIL_FAIL_FAST)" = "1" ] && [ $$overall_exit -ne 0 ]; then \
end_time=$$(date +%s%3N); \
duration=$$((end_time - start_time)); \
echo "{\"target\":\"fix\",\"status\":\"fail\",\"duration_ms\":$$duration,\"languages\":[$${ran_languages%,}],\"failed\":[$${failed_languages%,}]}"; \
exit $$overall_exit; \
fi; \
fi; \
end_time=$$(date +%s%3N); \
duration=$$((end_time - start_time)); \
if [ $$overall_exit -eq 0 ]; then \
Expand Down Expand Up @@ -656,6 +751,37 @@ _test: _check-config
exit $$overall_exit; \
fi; \
fi; \
if [ -n "$(HAS_SWIFT)" ]; then \
swift_files=$$(find . -name '*.swift' -not -path './.git/*' -not -path './.build/*' -not -path './DerivedData/*' 2>/dev/null); \
if [ -n "$$swift_files" ] && [ -f "Package.swift" ]; then \
ran_languages="$${ran_languages}\"swift\","; \
swift test || { overall_exit=1; failed_languages="$${failed_languages}\"swift\","; }; \
else \
skipped_languages="$${skipped_languages}\"swift\","; \
echo '{"level":"info","msg":"skipping swift tests: no .swift files or Package.swift found","language":"swift"}' >&2; \
fi; \
if [ "$(DEVRAIL_FAIL_FAST)" = "1" ] && [ $$overall_exit -ne 0 ]; then \
end_time=$$(date +%s%3N); \
duration=$$((end_time - start_time)); \
echo "{\"target\":\"test\",\"status\":\"fail\",\"duration_ms\":$$duration,\"languages\":[$${ran_languages%,}],\"failed\":[$${failed_languages%,}],\"skipped\":[$${skipped_languages%,}]}"; \
exit $$overall_exit; \
fi; \
fi; \
if [ -n "$(HAS_KOTLIN)" ]; then \
if [ -f "build.gradle.kts" ] || [ -f "build.gradle" ]; then \
ran_languages="$${ran_languages}\"kotlin\","; \
gradle test || { overall_exit=1; failed_languages="$${failed_languages}\"kotlin\","; }; \
else \
skipped_languages="$${skipped_languages}\"kotlin\","; \
echo '{"level":"info","msg":"skipping kotlin tests: no build.gradle.kts or build.gradle found","language":"kotlin"}' >&2; \
fi; \
if [ "$(DEVRAIL_FAIL_FAST)" = "1" ] && [ $$overall_exit -ne 0 ]; then \
end_time=$$(date +%s%3N); \
duration=$$((end_time - start_time)); \
echo "{\"target\":\"test\",\"status\":\"fail\",\"duration_ms\":$$duration,\"languages\":[$${ran_languages%,}],\"failed\":[$${failed_languages%,}],\"skipped\":[$${skipped_languages%,}]}"; \
exit $$overall_exit; \
fi; \
fi; \
end_time=$$(date +%s%3N); \
duration=$$((end_time - start_time)); \
if [ -z "$${ran_languages}" ] && [ -n "$${skipped_languages}" ]; then \
Expand Down Expand Up @@ -797,6 +923,25 @@ _security: _check-config
exit $$overall_exit; \
fi; \
fi; \
if [ -n "$(HAS_SWIFT)" ]; then \
skipped_languages="$${skipped_languages}\"swift\","; \
echo '{"level":"info","msg":"skipping swift security: no language-specific scanner","language":"swift"}' >&2; \
fi; \
if [ -n "$(HAS_KOTLIN)" ]; then \
if [ -f "build.gradle.kts" ] || [ -f "build.gradle" ]; then \
ran_languages="$${ran_languages}\"kotlin\","; \
gradle dependencyCheckAnalyze || { overall_exit=1; failed_languages="$${failed_languages}\"kotlin:owasp\","; }; \
else \
skipped_languages="$${skipped_languages}\"kotlin\","; \
echo '{"level":"info","msg":"skipping kotlin security: no build.gradle.kts or build.gradle found","language":"kotlin"}' >&2; \
fi; \
if [ "$(DEVRAIL_FAIL_FAST)" = "1" ] && [ $$overall_exit -ne 0 ]; then \
end_time=$$(date +%s%3N); \
duration=$$((end_time - start_time)); \
echo "{\"target\":\"security\",\"status\":\"fail\",\"duration_ms\":$$duration,\"languages\":[$${ran_languages%,}],\"failed\":[$${failed_languages%,}]}"; \
exit $$overall_exit; \
fi; \
fi; \
end_time=$$(date +%s%3N); \
duration=$$((end_time - start_time)); \
if [ -z "$${ran_languages}" ] && [ -n "$${skipped_languages}" ]; then \
Expand Down
116 changes: 116 additions & 0 deletions scripts/install-kotlin.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
#!/usr/bin/env bash
# scripts/install-kotlin.sh — Install and verify Kotlin tooling for DevRail
#
# Purpose: Installs Kotlin development tools (ktlint, detekt, Gradle) and verifies
# the JDK is available in the dev-toolchain container. JDK 21 is COPY'd
# from the jdk-builder stage; this script installs additional tools.
# Usage: bash scripts/install-kotlin.sh [--help]
# Dependencies: lib/log.sh, lib/platform.sh
#
# Tools installed/verified:
# - java (JDK 21 — COPY'd from builder)
# - ktlint (Linter/formatter — downloaded binary)
# - detekt-cli (Static analysis — downloaded JAR)
# - gradle (Build tool — downloaded distribution)

set -euo pipefail

# --- Resolve library path ---
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DEVRAIL_LIB="${DEVRAIL_LIB:-${SCRIPT_DIR}/../lib}"

# shellcheck source=../lib/log.sh
source "${DEVRAIL_LIB}/log.sh"
# shellcheck source=../lib/platform.sh
source "${DEVRAIL_LIB}/platform.sh"

# --- Help ---
if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
log_info "install-kotlin.sh — Install and verify Kotlin tooling for DevRail"
log_info "Usage: bash scripts/install-kotlin.sh [--help]"
log_info "Tools: java, ktlint, detekt-cli, gradle"
exit 0
fi

# --- Cleanup trap ---
TMPDIR_CLEANUP=""
cleanup() {
if [[ -n "${TMPDIR_CLEANUP}" && -d "${TMPDIR_CLEANUP}" ]]; then
rm -rf "${TMPDIR_CLEANUP}"
fi
}
trap cleanup EXIT

# --- Tool installation functions ---

install_ktlint() {
if command -v ktlint &>/dev/null; then
log_info "ktlint already installed, skipping"
return 0
fi

log_info "Installing ktlint..."
local version="1.5.0"
curl -fsSL "https://github.com/pinterest/ktlint/releases/download/${version}/ktlint" \
-o /usr/local/bin/ktlint
chmod +x /usr/local/bin/ktlint

require_cmd "ktlint" "Failed to install ktlint"
log_info "ktlint installed successfully"
}

install_detekt() {
if [ -f /usr/local/lib/detekt-cli.jar ]; then
log_info "detekt-cli already installed, skipping"
return 0
fi

log_info "Installing detekt-cli..."
local version="1.23.7"
curl -fsSL "https://github.com/detekt/detekt/releases/download/v${version}/detekt-cli-${version}-all.jar" \
-o /usr/local/lib/detekt-cli.jar

# Create wrapper script
cat >/usr/local/bin/detekt-cli <<'WRAPPER'
#!/usr/bin/env bash
exec java -jar /usr/local/lib/detekt-cli.jar "$@"
WRAPPER
chmod +x /usr/local/bin/detekt-cli

require_cmd "detekt-cli" "Failed to install detekt-cli"
log_info "detekt-cli installed successfully"
}

install_gradle() {
if command -v gradle &>/dev/null; then
log_info "gradle already installed, skipping"
return 0
fi

log_info "Installing Gradle..."
local version="8.12"
TMPDIR_CLEANUP="$(mktemp -d)"
curl -fsSL "https://services.gradle.org/distributions/gradle-${version}-bin.zip" \
-o "${TMPDIR_CLEANUP}/gradle.zip"
unzip -q "${TMPDIR_CLEANUP}/gradle.zip" -d /opt
ln -sf "/opt/gradle-${version}/bin/gradle" /usr/local/bin/gradle

require_cmd "gradle" "Failed to install Gradle"
log_info "Gradle installed successfully"
}

# --- Main ---
log_info "Installing Kotlin tools..."

# Verify JDK is available (COPY'd from builder)
if command -v java &>/dev/null; then
log_info "java is already installed"
else
log_warn "java not found — expected to be copied from JDK builder stage"
fi

install_ktlint
install_detekt
install_gradle

log_info "Kotlin tools installed successfully"
Loading
Loading