From 0f8c60bc52b063832fb65bb24cffe1ed33c38c1e Mon Sep 17 00:00:00 2001 From: Matthew Mellor Date: Mon, 23 Mar 2026 10:24:14 -0500 Subject: [PATCH 1/7] 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 --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d3ce5e7..7f6d830 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 @@ -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 }} From 30141431895eea92ab174749ed0937263fed9627 Mon Sep 17 00:00:00 2001 From: Matthew Mellor Date: Tue, 24 Mar 2026 16:36:51 -0500 Subject: [PATCH 2/7] 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) --- Dockerfile | 23 +++++- Makefile | 145 ++++++++++++++++++++++++++++++++++++++ scripts/install-kotlin.sh | 116 ++++++++++++++++++++++++++++++ scripts/install-swift.sh | 119 +++++++++++++++++++++++++++++++ tests/test-kotlin.sh | 43 +++++++++++ tests/test-swift.sh | 35 +++++++++ 6 files changed, 479 insertions(+), 2 deletions(-) create mode 100755 scripts/install-kotlin.sh create mode 100755 scripts/install-swift.sh create mode 100755 tests/test-kotlin.sh create mode 100755 tests/test-swift.sh diff --git a/Dockerfile b/Dockerfile index 0d5746b..134ef31 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,6 +40,14 @@ RUN curl -L --proto '=https' --tlsv1.2 -sSf \ | bash RUN cargo binstall --no-confirm cargo-audit cargo-deny +# === Swift builder stage === +# Provides Swift toolchain (swiftc, swift build, swift test, SPM) +FROM swift:6.1-slim-bookworm AS swift-builder + +# === JDK builder stage === +# Provides JDK 21 for Kotlin tooling (ktlint, detekt, Gradle) +FROM eclipse-temurin:21-jdk-bookworm AS jdk-builder + # === Node.js base: provides Node runtime for JS/TS tooling === FROM node:22-bookworm-slim AS node-base @@ -110,8 +118,17 @@ 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 only) +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 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) @@ -135,6 +152,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 diff --git a/Makefile b/Makefile index 4690b5a..d9593af 100644 --- a/Makefile +++ b/Makefile @@ -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 @@ -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 \ @@ -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 \ @@ -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 \ @@ -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 \ @@ -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 \ diff --git a/scripts/install-kotlin.sh b/scripts/install-kotlin.sh new file mode 100755 index 0000000..366ee50 --- /dev/null +++ b/scripts/install-kotlin.sh @@ -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" diff --git a/scripts/install-swift.sh b/scripts/install-swift.sh new file mode 100755 index 0000000..ad5820d --- /dev/null +++ b/scripts/install-swift.sh @@ -0,0 +1,119 @@ +#!/usr/bin/env bash +# scripts/install-swift.sh — Install and verify Swift tooling for DevRail +# +# Purpose: Installs SwiftLint and swift-format, and verifies the Swift toolchain +# is available in the dev-toolchain container. The Swift SDK (swiftc, swift +# build, swift test, Swift Package Manager) is COPY'd from the swift-builder +# stage; this script installs additional tools and confirms all are on PATH. +# Usage: bash scripts/install-swift.sh [--help] +# Dependencies: lib/log.sh, lib/platform.sh +# +# Tools installed/verified: +# - swift (Swift compiler — COPY'd from builder) +# - swiftlint (Linter — installed from GitHub releases) +# - swift-format (Formatter — installed from GitHub releases) + +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-swift.sh — Install and verify Swift tooling for DevRail" + log_info "Usage: bash scripts/install-swift.sh [--help]" + log_info "Tools: swift, swiftlint, swift-format" + 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_swiftlint() { + if command -v swiftlint &>/dev/null; then + log_info "swiftlint already installed, skipping" + return 0 + fi + + log_info "Installing SwiftLint..." + TMPDIR_CLEANUP="$(mktemp -d)" + local arch + arch="$(dpkg --print-architecture)" + # SwiftLint provides pre-built Linux binaries + local version="0.58.0" + if [ "${arch}" = "amd64" ]; then + curl -fsSL "https://github.com/realm/SwiftLint/releases/download/${version}/swiftlint_linux.zip" \ + -o "${TMPDIR_CLEANUP}/swiftlint.zip" + unzip -q "${TMPDIR_CLEANUP}/swiftlint.zip" -d "${TMPDIR_CLEANUP}" + install -m 755 "${TMPDIR_CLEANUP}/swiftlint" /usr/local/bin/swiftlint + elif [ "${arch}" = "arm64" ]; then + curl -fsSL "https://github.com/realm/SwiftLint/releases/download/${version}/swiftlint_linux_aarch64.zip" \ + -o "${TMPDIR_CLEANUP}/swiftlint.zip" + unzip -q "${TMPDIR_CLEANUP}/swiftlint.zip" -d "${TMPDIR_CLEANUP}" + install -m 755 "${TMPDIR_CLEANUP}/swiftlint" /usr/local/bin/swiftlint + else + log_error "SwiftLint: unsupported architecture ${arch}" + return 1 + fi + + require_cmd "swiftlint" "Failed to install SwiftLint" + log_info "SwiftLint installed successfully" +} + +install_swift_format() { + if command -v swift-format &>/dev/null; then + log_info "swift-format already installed, skipping" + return 0 + fi + + log_info "Installing swift-format..." + local version="601.0.0" + local arch + arch="$(dpkg --print-architecture)" + + if [ "${arch}" = "amd64" ]; then + curl -fsSL "https://github.com/swiftlang/swift-format/releases/download/${version}/swift-format-${version}-linux-x86_64.tar.gz" \ + -o "/tmp/swift-format.tar.gz" + tar xzf /tmp/swift-format.tar.gz -C /tmp + install -m 755 /tmp/swift-format /usr/local/bin/swift-format + rm -f /tmp/swift-format.tar.gz /tmp/swift-format + else + log_warn "swift-format pre-built binary not available for ${arch}, building from source..." + git clone --depth 1 --branch "${version}" https://github.com/swiftlang/swift-format.git /tmp/swift-format-src + (cd /tmp/swift-format-src && swift build -c release) + install -m 755 /tmp/swift-format-src/.build/release/swift-format /usr/local/bin/swift-format + rm -rf /tmp/swift-format-src + fi + + require_cmd "swift-format" "Failed to install swift-format" + log_info "swift-format installed successfully" +} + +# --- Main --- +log_info "Installing Swift tools..." + +# Verify Swift compiler is available (COPY'd from builder) +if command -v swift &>/dev/null; then + log_info "swift is already installed" +else + log_warn "swift not found — expected to be copied from Swift builder stage" +fi + +install_swiftlint +install_swift_format + +log_info "Swift tools installed successfully" diff --git a/tests/test-kotlin.sh b/tests/test-kotlin.sh new file mode 100755 index 0000000..b5caf2f --- /dev/null +++ b/tests/test-kotlin.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# tests/test-kotlin.sh — Verify Kotlin tooling is installed correctly +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DEVRAIL_LIB="${DEVRAIL_LIB:-${SCRIPT_DIR}/../lib}" +source "${DEVRAIL_LIB}/log.sh" + +log_info "Testing Kotlin tooling installation..." + +# Verify JDK +if command -v java &>/dev/null; then + log_info "PASS: java found — $(java --version 2>&1 | head -1)" +else + log_error "FAIL: java not found" + exit 1 +fi + +# Verify ktlint +if command -v ktlint &>/dev/null; then + log_info "PASS: ktlint found — $(ktlint --version 2>&1)" +else + log_error "FAIL: ktlint not found" + exit 1 +fi + +# Verify detekt-cli +if command -v detekt-cli &>/dev/null; then + log_info "PASS: detekt-cli found" +else + log_error "FAIL: detekt-cli not found" + exit 1 +fi + +# Verify gradle +if command -v gradle &>/dev/null; then + log_info "PASS: gradle found — $(gradle --version 2>&1 | grep 'Gradle' | head -1)" +else + log_error "FAIL: gradle not found" + exit 1 +fi + +log_info "All Kotlin tools verified successfully" diff --git a/tests/test-swift.sh b/tests/test-swift.sh new file mode 100755 index 0000000..d0ddcf3 --- /dev/null +++ b/tests/test-swift.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# tests/test-swift.sh — Verify Swift tooling is installed correctly +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DEVRAIL_LIB="${DEVRAIL_LIB:-${SCRIPT_DIR}/../lib}" +source "${DEVRAIL_LIB}/log.sh" + +log_info "Testing Swift tooling installation..." + +# Verify swift +if command -v swift &>/dev/null; then + log_info "PASS: swift found — $(swift --version 2>&1 | head -1)" +else + log_error "FAIL: swift not found" + exit 1 +fi + +# Verify swiftlint +if command -v swiftlint &>/dev/null; then + log_info "PASS: swiftlint found — $(swiftlint version)" +else + log_error "FAIL: swiftlint not found" + exit 1 +fi + +# Verify swift-format +if command -v swift-format &>/dev/null; then + log_info "PASS: swift-format found — $(swift-format --version 2>&1 || echo 'version check N/A')" +else + log_error "FAIL: swift-format not found" + exit 1 +fi + +log_info "All Swift tools verified successfully" From 459dec8ee463168f82ce28309729447ba4559033 Mon Sep 17 00:00:00 2001 From: Matthew Mellor Date: Tue, 24 Mar 2026 16:45:20 -0500 Subject: [PATCH 3/7] fix(bash): add shellcheck source directives to Swift and Kotlin test scripts Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/test-kotlin.sh | 2 ++ tests/test-swift.sh | 2 ++ 2 files changed, 4 insertions(+) diff --git a/tests/test-kotlin.sh b/tests/test-kotlin.sh index b5caf2f..8ca99ee 100755 --- a/tests/test-kotlin.sh +++ b/tests/test-kotlin.sh @@ -4,6 +4,8 @@ set -euo pipefail 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" log_info "Testing Kotlin tooling installation..." diff --git a/tests/test-swift.sh b/tests/test-swift.sh index d0ddcf3..52ee8ac 100755 --- a/tests/test-swift.sh +++ b/tests/test-swift.sh @@ -4,6 +4,8 @@ set -euo pipefail 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" log_info "Testing Swift tooling installation..." From d6556ae28f3632c674b24aa5ac381c573c8931d2 Mon Sep 17 00:00:00 2001 From: Matthew Mellor Date: Tue, 24 Mar 2026 16:46:44 -0500 Subject: [PATCH 4/7] fix(bash): fix shfmt formatting in install-kotlin.sh heredoc Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/install-kotlin.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/install-kotlin.sh b/scripts/install-kotlin.sh index 366ee50..e7f66ee 100755 --- a/scripts/install-kotlin.sh +++ b/scripts/install-kotlin.sh @@ -71,7 +71,7 @@ install_detekt() { -o /usr/local/lib/detekt-cli.jar # Create wrapper script - cat > /usr/local/bin/detekt-cli << 'WRAPPER' + cat >/usr/local/bin/detekt-cli <<'WRAPPER' #!/usr/bin/env bash exec java -jar /usr/local/lib/detekt-cli.jar "$@" WRAPPER From eb97368636d449e02470f3048db52593686b8d90 Mon Sep 17 00:00:00 2001 From: Matthew Mellor Date: Wed, 25 Mar 2026 10:30:21 -0500 Subject: [PATCH 5/7] 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) --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 134ef31..cc109f7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -46,7 +46,7 @@ FROM swift:6.1-slim-bookworm AS swift-builder # === JDK builder stage === # Provides JDK 21 for Kotlin tooling (ktlint, detekt, Gradle) -FROM eclipse-temurin:21-jdk-bookworm AS jdk-builder +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 From fd8beae2617aeaf2be149b777d535d75c4efff13 Mon Sep 17 00:00:00 2001 From: Matthew Mellor Date: Wed, 25 Mar 2026 10:33:35 -0500 Subject: [PATCH 6/7] 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) --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index cc109f7..531dcad 100644 --- a/Dockerfile +++ b/Dockerfile @@ -42,7 +42,7 @@ RUN cargo binstall --no-confirm cargo-audit cargo-deny # === Swift builder stage === # Provides Swift toolchain (swiftc, swift build, swift test, SPM) -FROM swift:6.1-slim-bookworm AS swift-builder +FROM swift:6.1-bookworm AS swift-builder # === JDK builder stage === # Provides JDK 21 for Kotlin tooling (ktlint, detekt, Gradle) From c2fd556af3d59c866484def3cdf8065be6d2254e Mon Sep 17 00:00:00 2001 From: Matthew Mellor Date: Wed, 25 Mar 2026 11:28:36 -0500 Subject: [PATCH 7/7] 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) --- Dockerfile | 11 +++++++++-- scripts/install-swift.sh | 31 ++++++------------------------- 2 files changed, 15 insertions(+), 27 deletions(-) diff --git a/Dockerfile b/Dockerfile index 531dcad..60c73bd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -41,8 +41,14 @@ RUN curl -L --proto '=https' --tlsv1.2 -sSf \ RUN cargo binstall --no-confirm cargo-audit cargo-deny # === Swift builder stage === -# Provides Swift toolchain (swiftc, swift build, swift test, SPM) +# 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) @@ -118,10 +124,11 @@ COPY --from=rust-builder /usr/local/cargo /usr/local/cargo ENV RUSTUP_HOME=/usr/local/rustup ENV CARGO_HOME=/usr/local/cargo -# Copy Swift toolchain from swift-builder (selective: binaries + runtime libs only) +# 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 diff --git a/scripts/install-swift.sh b/scripts/install-swift.sh index ad5820d..52ee81c 100755 --- a/scripts/install-swift.sh +++ b/scripts/install-swift.sh @@ -74,33 +74,14 @@ install_swiftlint() { log_info "SwiftLint installed successfully" } -install_swift_format() { +verify_swift_format() { + # swift-format is built from source in the Dockerfile swift-builder stage + # and COPY'd to /usr/local/bin/swift-format. This function only verifies. if command -v swift-format &>/dev/null; then - log_info "swift-format already installed, skipping" - return 0 - fi - - log_info "Installing swift-format..." - local version="601.0.0" - local arch - arch="$(dpkg --print-architecture)" - - if [ "${arch}" = "amd64" ]; then - curl -fsSL "https://github.com/swiftlang/swift-format/releases/download/${version}/swift-format-${version}-linux-x86_64.tar.gz" \ - -o "/tmp/swift-format.tar.gz" - tar xzf /tmp/swift-format.tar.gz -C /tmp - install -m 755 /tmp/swift-format /usr/local/bin/swift-format - rm -f /tmp/swift-format.tar.gz /tmp/swift-format + log_info "swift-format is already installed" else - log_warn "swift-format pre-built binary not available for ${arch}, building from source..." - git clone --depth 1 --branch "${version}" https://github.com/swiftlang/swift-format.git /tmp/swift-format-src - (cd /tmp/swift-format-src && swift build -c release) - install -m 755 /tmp/swift-format-src/.build/release/swift-format /usr/local/bin/swift-format - rm -rf /tmp/swift-format-src + log_warn "swift-format not found — expected to be copied from Swift builder stage" fi - - require_cmd "swift-format" "Failed to install swift-format" - log_info "swift-format installed successfully" } # --- Main --- @@ -114,6 +95,6 @@ else fi install_swiftlint -install_swift_format +verify_swift_format log_info "Swift tools installed successfully"