Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,20 @@ jobs:
DEVRAIL_IMAGE: ${{ env.IMAGE_NAME }}
DEVRAIL_TAG: ${{ env.IMAGE_TAG }}

# Phase 2h: Kotlin reference plugin extraction smoke test (Story 13.7)
# Validates the kotlin reference plugin's manifest against the
# validator, resolves it via file:// URL from the vendored fixture,
# loads it into the plugin cache, and asserts target/gate parity
# with the in-core HAS_KOTLIN behaviour. Does NOT exercise the full
# docker-build of devrail-local:<hash> with real ktlint/detekt/gradle
# downloads — that's a maintainer-run manual check (see fixture
# README).
- name: Kotlin plugin extraction smoke test
run: bash tests/test-kotlin-plugin-extraction.sh
env:
DEVRAIL_IMAGE: ${{ env.IMAGE_NAME }}
DEVRAIL_TAG: ${{ env.IMAGE_TAG }}

# Phase 3: Security scans
# 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.
Expand Down
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- **Kotlin reference plugin extraction (Story 13.7).** The first language
ecosystem has been extracted out of dev-toolchain core into a standalone
plugin repo: [`github.com/devrail-dev/devrail-plugin-kotlin`](https://github.com/devrail-dev/devrail-plugin-kotlin)
v1.0.0. Consumers can declare it via `plugins:` in their `.devrail.yml`
to use Kotlin tooling through the v1.10 plugin model instead of the
in-core path. Tools shipped: ktlint 1.5.0, detekt 1.23.7, Gradle 8.12,
JDK 21 (Eclipse Temurin) — same versions and behaviour as the in-core
HAS_KOTLIN blocks.
- **Extraction is additive in v1.11.x.** Kotlin remains in dev-toolchain
core. Existing consumers with `languages: [kotlin]` see ZERO
behavioural change — the loader's "core wins over plugin" precedence
rule keeps them on the in-core path. v2.0.0 (Story 13.9) removes the
in-core path; until then, the plugin is available alongside.
- **Extraction recipe documented at**
[`devrail-standards/standards/contributing.md` § "Extracting a core language as a plugin"](https://github.com/devrail-dev/devrail-standards/blob/main/standards/contributing.md#extracting-a-core-language-as-a-plugin)
— step-by-step guide for surface inventory, Makefile-block →
manifest-target mapping, install-script porting (no lib/log.sh deps),
container-fragment construction, file:// URL validation, and tag/
announce. Kotlin extraction is the canonical example.
- **New regression smoke: `tests/test-kotlin-plugin-extraction.sh`.**
Validates the plugin manifest against schema_version 1, resolves it
via file:// URL from a vendored fixture, loads it into the dispatcher
cache, and asserts target / gate shape parity with the in-core
HAS_KOTLIN blocks (including the `ktlint && detekt-cli` chaining
preserves both tools). Hermetic — no network access at test time.
Wired into CI as Phase 2h.
- **Vendored plugin fixture**: `tests/fixtures/kotlin-via-plugin/`
pinned to `devrail-plugin-kotlin` v1.0.0. Refresh procedure documented
in the fixture's README.

## [1.10.6] - 2026-05-05

### Other
Expand Down
2 changes: 1 addition & 1 deletion STABILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ DevRail has reached **v1.0** across all repositories. The core standards, toolch
| **CI workflow templates** | Stable | GitHub Actions workflows and GitLab CI pipeline shipped in template repos. |
| **Pre-commit hooks** | Stable | Conventional commit hook and per-language hooks configured in template repos. |
| **Documentation site** | Stable | [devrail.dev](https://devrail.dev) is live with full standards coverage. |
| **Plugin loader + resolver + lockfile + build pipeline + execution loop** | Preview (v1.10.x) | Validates `plugin.devrail.yml` manifests, resolves `rev:` to immutable SHAs (`make plugins-update`), records reproducibility metadata in `.devrail.lock`, and auto-builds a project-local extended image (`devrail-local:<hash>`) when plugins are declared. Each loaded plugin's `targets` are dispatched inside `_lint`/`_format`/`_fix`/`_test`/`_security` with gate evaluation, `{paths}` interpolation, per-language overrides, and JSON aggregation into the existing event shape. `DEVRAIL_FAIL_FAST=1` short-circuits on plugin failures the same as core. No-op when `plugins:` is absent — v1.9.x behaviour unchanged. |
| **Plugin loader + resolver + lockfile + build pipeline + execution loop** | Stable (v1.10.x baseline; reference plugin v1.11.x) | Validates `plugin.devrail.yml` manifests, resolves `rev:` to immutable SHAs (`make plugins-update`), records reproducibility metadata in `.devrail.lock`, and auto-builds a project-local extended image (`devrail-local:<hash>`) when plugins are declared. Each loaded plugin's `targets` are dispatched inside `_lint`/`_format`/`_fix`/`_test`/`_security` with gate evaluation, `{paths}` interpolation, per-language overrides, and JSON aggregation into the existing event shape. `DEVRAIL_FAIL_FAST=1` short-circuits on plugin failures the same as core. No-op when `plugins:` is absent — v1.9.x behaviour unchanged. **As of v1.11.0** the first reference plugin ([`devrail-plugin-kotlin`](https://github.com/devrail-dev/devrail-plugin-kotlin)) is published; the extraction is **additive** through the v1.x line (Kotlin remains in core for back-compat). v2.0.0 retires the in-core HAS_<LANG> blocks. |

## Consumer responsibilities

Expand Down
50 changes: 50 additions & 0 deletions tests/fixtures/kotlin-via-plugin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Fixture: kotlin-via-plugin

Vendored snapshot of `github.com/devrail-dev/devrail-plugin-kotlin` v1.0.0,
used by `tests/test-kotlin-plugin-extraction.sh` to run the resolver,
loader, and validator against the kotlin reference plugin without a
network fetch.

## Refresh procedure

When the upstream plugin cuts a new tag:

```sh
cd ~/Work/github.com/devrail-dev/devrail-plugin-kotlin
git checkout vX.Y.Z

cd ~/Work/github.com/devrail-dev/dev-toolchain
cp ../devrail-plugin-kotlin/plugin.devrail.yml tests/fixtures/kotlin-via-plugin/
cp ../devrail-plugin-kotlin/install.sh tests/fixtures/kotlin-via-plugin/
```

Then re-run the smoke test and update the version pin in the test if
the manifest's `version` field changed:

```sh
bash tests/test-kotlin-plugin-extraction.sh
```

## Why a vendored copy

- Hermetic — no network access at test time
- Reproducible — the test pins to a known plugin version
- Independent of GitHub uptime / rate limits during CI

## What's NOT covered by this fixture

The fixture lets us exercise the manifest validator, resolver, and
loader against a real plugin manifest. It does NOT exercise the full
docker-build of `devrail-local:<hash>` — that pulls real ktlint, detekt,
and gradle from upstream and takes ~5 minutes. Maintainers run the full
build manually:

```sh
cd ~/Work/github.com/devrail-dev/devrail-plugin-kotlin
# Create a tiny Kotlin sample workspace pointing back at this checkout
# via a file:// URL, then `make plugins-update && make check` and
# observe the extended image build + Kotlin tooling run.
```

This trade-off mirrors what we did for the `minimal-v1` fixture — keep
CI fast, maintainers do the heavy validation by hand.
69 changes: 69 additions & 0 deletions tests/fixtures/kotlin-via-plugin/install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/usr/bin/env bash
# install.sh — Install Kotlin tooling inside the project-local extended image
#
# Purpose: Runs during `docker build` of the consumer's `Dockerfile.devrail`.
# Downloads ktlint, detekt-cli, and Gradle into /usr/local/. The JDK
# itself is COPY'd from the eclipse-temurin:21-jdk builder stage by
# the manifest's container.copy_from_builder block.
#
# Usage: bash install.sh
#
# Self-contained: NO dependency on dev-toolchain's lib/log.sh — plugin install
# scripts run during the docker build step before any DevRail libs are
# available in the image. Status messages go to stderr via plain printf.

set -euo pipefail

log() {
printf '[install-kotlin] %s\n' "$*" >&2
}

# --- ktlint ---
if command -v ktlint >/dev/null 2>&1; then
log "ktlint already installed; skipping"
else
KTLINT_VERSION="1.5.0"
log "installing ktlint ${KTLINT_VERSION}"
curl -fsSL "https://github.com/pinterest/ktlint/releases/download/${KTLINT_VERSION}/ktlint" \
-o /usr/local/bin/ktlint
chmod +x /usr/local/bin/ktlint
fi

# --- detekt-cli ---
if [[ -f /usr/local/lib/detekt-cli.jar ]]; then
log "detekt-cli already installed; skipping"
else
DETEKT_VERSION="1.23.7"
log "installing detekt-cli ${DETEKT_VERSION}"
mkdir -p /usr/local/lib
curl -fsSL "https://github.com/detekt/detekt/releases/download/v${DETEKT_VERSION}/detekt-cli-${DETEKT_VERSION}-all.jar" \
-o /usr/local/lib/detekt-cli.jar
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
fi

# --- Gradle ---
if command -v gradle >/dev/null 2>&1; then
log "gradle already installed; skipping"
else
GRADLE_VERSION="8.12"
log "installing gradle ${GRADLE_VERSION}"
TMPDIR_INSTALL="$(mktemp -d)"
trap 'rm -rf "${TMPDIR_INSTALL}"' EXIT
curl -fsSL "https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip" \
-o "${TMPDIR_INSTALL}/gradle.zip"
unzip -q "${TMPDIR_INSTALL}/gradle.zip" -d /opt
ln -sf "/opt/gradle-${GRADLE_VERSION}/bin/gradle" /usr/local/bin/gradle
fi

# --- Verify ---
log "verifying installation"
java -version 2>&1 | head -1 >&2
ktlint --version 2>&1 | head -1 >&2
detekt-cli --version 2>&1 | head -1 >&2
gradle --version 2>&1 | grep '^Gradle' | head -1 >&2

log "kotlin plugin tools installed successfully"
62 changes: 62 additions & 0 deletions tests/fixtures/kotlin-via-plugin/plugin.devrail.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# plugin.devrail.yml — devrail-plugin-kotlin
#
# Reference plugin extracted from dev-toolchain core (Story 13.7).
# Provides Kotlin language ecosystem support: ktlint (lint+format),
# detekt (static analysis), Gradle (build/test), OWASP dependency-check
# (security), JDK 21 (Eclipse Temurin).
#
# Consumers declare in .devrail.yml:
#
# plugins:
# - source: github.com/devrail-dev/devrail-plugin-kotlin
# rev: v1.0.0
# languages: [kotlin]
#
# The dev-toolchain build pipeline (Story 13.4) layers this plugin's
# container fragment onto ghcr.io/devrail-dev/dev-toolchain:v1 to produce
# a project-local extended image (devrail-local:<hash>).

schema_version: 1
name: kotlin
version: 1.0.0
description: "Kotlin language ecosystem for DevRail (ktlint, detekt, Gradle, JDK 21)"
devrail_min_version: 1.10.0

container:
base_image: eclipse-temurin:21-jdk
copy_from_builder:
- /opt/java/openjdk
env:
JAVA_HOME: /opt/java/openjdk
PATH: "/opt/java/openjdk/bin:${PATH}"
install_script: install.sh

# Targets mirror dev-toolchain's HAS_KOTLIN behaviour. Composite linting
# (ktlint + detekt) is collapsed into a single `lint.cmd` via && — the v1
# plugin contract is one cmd per target. Plugin authors can override
# either tool individually via the consumer's .devrail.yml:
#
# kotlin:
# linter: "ktlint" # drops detekt
#
targets:
lint:
cmd: "ktlint && (test -f detekt.yml && detekt-cli --build-upon-default-config --config detekt.yml || detekt-cli --build-upon-default-config)"
format_check:
cmd: "ktlint --format --dry-run"
format_fix:
cmd: "ktlint --format"
test:
cmd: "gradle test --no-daemon"
security:
cmd: "gradle dependencyCheckAnalyze --no-daemon"

# Gates: a Gradle Kotlin project is identified by build.gradle.kts (or
# build.gradle for Groovy DSL projects that still use Kotlin sources).
# We gate on either to support both.
gates:
lint: ["build.gradle.kts"]
format_check: ["build.gradle.kts"]
format_fix: ["build.gradle.kts"]
test: ["build.gradle.kts"]
security: ["build.gradle.kts"]
Loading
Loading