From 30fa6ac28bc7819ccbcdb0d1ccaefd02d148ee65 Mon Sep 17 00:00:00 2001 From: Matthew Mellor Date: Mon, 4 May 2026 23:48:29 -0500 Subject: [PATCH] feat(makefile): kotlin reference plugin extraction smoke test (Story 13.7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dev-toolchain side of the kotlin-as-reference-plugin extraction. The new repo at github.com/devrail-dev/devrail-plugin-kotlin v1.0.0 ships the actual plugin; this commit adds: - tests/test-kotlin-plugin-extraction.sh — 4-case hermetic smoke that validates the plugin manifest against the validator, resolves it via file:// URL from the vendored fixture, loads it into the dispatcher cache, and asserts target/gate shape parity with the in-core HAS_KOTLIN blocks (including ktlint && detekt-cli chaining). - tests/fixtures/kotlin-via-plugin/ — vendored snapshot of the plugin manifest + install.sh, pinned to v1.0.0. Refresh procedure in README.md. Hermetic — no network access at test time. - .github/workflows/ci.yml — new Phase 2h step "Kotlin plugin extraction smoke test" runs the new test on every push. - CHANGELOG.md — [Unreleased] entry frames v1.11 as the additive extraction; v1.10.x kotlin-in-core unchanged. - STABILITY.md — Plugin row promoted from "Preview" to "Stable (v1.10.x baseline; reference plugin v1.11.x)" with the additive back-compat note. NO Dockerfile or Makefile changes — kotlin stays in dev-toolchain core through v1.x. v2.0.0 (Story 13.9) is where the in-core path retires. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ci.yml | 14 ++ CHANGELOG.md | 32 +++ STABILITY.md | 2 +- tests/fixtures/kotlin-via-plugin/README.md | 50 +++++ tests/fixtures/kotlin-via-plugin/install.sh | 69 ++++++ .../kotlin-via-plugin/plugin.devrail.yml | 62 ++++++ tests/test-kotlin-plugin-extraction.sh | 207 ++++++++++++++++++ 7 files changed, 435 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/kotlin-via-plugin/README.md create mode 100755 tests/fixtures/kotlin-via-plugin/install.sh create mode 100644 tests/fixtures/kotlin-via-plugin/plugin.devrail.yml create mode 100755 tests/test-kotlin-plugin-extraction.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8affa6d..00196c1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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: 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. diff --git a/CHANGELOG.md b/CHANGELOG.md index 79ded7d..aea2c67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/STABILITY.md b/STABILITY.md index f9bdab2..e894e1c 100644 --- a/STABILITY.md +++ b/STABILITY.md @@ -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:`) 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:`) 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_ blocks. | ## Consumer responsibilities diff --git a/tests/fixtures/kotlin-via-plugin/README.md b/tests/fixtures/kotlin-via-plugin/README.md new file mode 100644 index 0000000..c1b4dc3 --- /dev/null +++ b/tests/fixtures/kotlin-via-plugin/README.md @@ -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:` — 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. diff --git a/tests/fixtures/kotlin-via-plugin/install.sh b/tests/fixtures/kotlin-via-plugin/install.sh new file mode 100755 index 0000000..97ec638 --- /dev/null +++ b/tests/fixtures/kotlin-via-plugin/install.sh @@ -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" diff --git a/tests/fixtures/kotlin-via-plugin/plugin.devrail.yml b/tests/fixtures/kotlin-via-plugin/plugin.devrail.yml new file mode 100644 index 0000000..c63d3d4 --- /dev/null +++ b/tests/fixtures/kotlin-via-plugin/plugin.devrail.yml @@ -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:). + +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"] diff --git a/tests/test-kotlin-plugin-extraction.sh b/tests/test-kotlin-plugin-extraction.sh new file mode 100755 index 0000000..1c574e6 --- /dev/null +++ b/tests/test-kotlin-plugin-extraction.sh @@ -0,0 +1,207 @@ +#!/usr/bin/env bash +# tests/test-kotlin-plugin-extraction.sh — Validate the Kotlin reference plugin +# +# Story 13.7 ships `devrail-plugin-kotlin` as the first reference plugin +# extracted from dev-toolchain core. This test validates structural +# parity: the plugin's manifest is accepted by the validator, the +# resolver fetches it cleanly (against a vendored fixture clone), and +# the loader records the expected per-target/gate shape. +# +# Out of scope here: the full docker-build of `devrail-local:` +# with real ktlint/detekt/gradle downloads. That's a several-minute +# build that's exercised manually by maintainers via `make check` on a +# real Kotlin workspace — NOT a smoke test. See the docs at +# tests/fixtures/kotlin-via-plugin/README.md for the manual check. +# +# Cases: +# 1. Plugin manifest validates against schema_version 1 +# 2. Resolver fetches the plugin from a file:// URL → SHA + content +# hash recorded in .devrail.lock +# 3. Loader populates the plugin cache with the expected name, version, +# targets, and gates (the parity check vs dev-toolchain's HAS_KOTLIN) +# 4. Loader-cache target shape: lint, format_check, format_fix, test, +# security all present with non-empty cmd +# +# Usage: bash tests/test-kotlin-plugin-extraction.sh + +set -euo pipefail + +IMAGE="${DEVRAIL_IMAGE:-ghcr.io/devrail-dev/dev-toolchain}:${DEVRAIL_TAG:-local}" +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +FIXTURE_ROOT="$REPO_ROOT/tests/fixtures/kotlin-via-plugin" +WORKDIR="$(mktemp -d)" + +cleanup() { + if [ -n "${WORKDIR:-}" ] && [ -d "$WORKDIR" ]; then + docker run --rm -v "$WORKDIR:/cleanup" "$IMAGE" \ + sh -c 'rm -rf /cleanup/* /cleanup/.[!.]* 2>/dev/null || true' >/dev/null 2>&1 || true + rm -rf "$WORKDIR" 2>/dev/null || true + fi +} +trap cleanup EXIT + +assert_eq() { + local expected="$1" actual="$2" context="$3" + if [ "$expected" != "$actual" ]; then + echo "FAIL [$context]: expected '$expected', got '$actual'" >&2 + exit 1 + fi +} + +# --- Setup: stage a checked-in copy of the kotlin plugin manifest --- +# We bundle the canonical manifest under tests/fixtures/kotlin-via-plugin/ +# so the test is hermetic (no network fetch from +# github.com/devrail-dev/devrail-plugin-kotlin). Maintainers refresh the +# fixture when bumping the upstream plugin. +if [ ! -r "$FIXTURE_ROOT/plugin.devrail.yml" ]; then + echo "FAIL [setup]: fixture missing at $FIXTURE_ROOT/plugin.devrail.yml" >&2 + echo " refresh it from github.com/devrail-dev/devrail-plugin-kotlin" >&2 + exit 1 +fi + +# --- Case 1: validator accepts the manifest --- +echo "==> Case 1: plugin-validator accepts kotlin manifest" +docker run --rm \ + -v "$FIXTURE_ROOT/plugin.devrail.yml:/plugin/plugin.devrail.yml:ro" \ + "$IMAGE" \ + bash /opt/devrail/scripts/plugin-validator.sh /plugin/plugin.devrail.yml || + { + echo "FAIL [case1]: validator rejected kotlin manifest" >&2 + exit 1 + } + +# --- Case 2: resolver fetches via file:// URL --- +# Build a local-fs git repo from the fixture and have the resolver +# pin it to v1.0.0. +echo "==> Case 2: resolver fetches kotlin plugin via file:// URL" +mkdir -p "$WORKDIR/case2-fixture" "$WORKDIR/case2-ws" "$WORKDIR/case2-host-cache" +docker run --rm \ + -v "$FIXTURE_ROOT:/src:ro" \ + -v "$WORKDIR/case2-fixture:/repo" \ + "$IMAGE" \ + sh -c ' + set -e + cp -a /src/. /repo/ + cd /repo + git init --quiet + git config user.email "test@example.com" + git config user.name "Test" + git config commit.gpgsign false + git add -A + git commit --quiet -m "v1.0.0" + git tag v1.0.0 + ' +cat >"$WORKDIR/case2-ws/.devrail.yml" <&1)" || { + echo "FAIL [case2]: _plugins-update failed" >&2 + echo "$resolver_out" >&2 + exit 1 +} +[ -r "$WORKDIR/case2-ws/.devrail.lock" ] || { + echo "FAIL [case2]: .devrail.lock not written" >&2 + exit 1 +} + +# Confirm the lockfile has the kotlin plugin with content_hash set. +docker run --rm \ + -v "$WORKDIR/case2-ws:/ws:ro" "$IMAGE" \ + sh -c 'yq -e ".plugins[0].source" /ws/.devrail.lock | grep -q "case2-fixture$"' || { + echo "FAIL [case2]: lockfile source mismatch" >&2 + cat "$WORKDIR/case2-ws/.devrail.lock" >&2 + exit 1 +} +docker run --rm \ + -v "$WORKDIR/case2-ws:/ws:ro" "$IMAGE" \ + sh -c 'yq -e ".plugins[0].content_hash" /ws/.devrail.lock | grep -q "^sha256:"' || { + echo "FAIL [case2]: content_hash missing or wrong format" >&2 + cat "$WORKDIR/case2-ws/.devrail.lock" >&2 + exit 1 +} + +# --- Case 3: loader cache contains expected name + version + devrail_min --- +# YAML parsing runs INSIDE the container (mikefarah/yq v4 with strenv); +# the host's `yq` is the kislyuk Python wrapper (jq-based) that lacks +# strenv. Saving the cache to disk and bind-mounting back lets us parse +# from a single docker run. +echo "==> Case 3: loader populates cache with kotlin plugin metadata" +docker run --rm \ + -v "$WORKDIR:$WORKDIR" \ + -v "$WORKDIR/case2-ws:/workspace" \ + -v "$REPO_ROOT/Makefile:/workspace/Makefile:ro" \ + -v "$WORKDIR/case2-host-cache:/opt/devrail/plugins" \ + -e DEVRAIL_VERSION=1.11.0 \ + -w /workspace \ + "$IMAGE" \ + bash -c ' + set -e + make _plugins-load >&2 + cp /tmp/devrail-plugins-loaded.yaml /workspace/.devrail-loader-cache.yaml + ' >/dev/null 2>&1 + +LOADER_CACHE="$WORKDIR/case2-ws/.devrail-loader-cache.yaml" +[ -r "$LOADER_CACHE" ] || { + echo "FAIL [case3]: loader cache not produced" >&2 + exit 1 +} + +# Helper that runs yq inside the container against the cache. +yqc() { + docker run --rm \ + -v "$LOADER_CACHE:/cache:ro" \ + "$IMAGE" \ + yq "$@" /cache +} + +loader_name="$(yqc -r '.plugins[0].name')" +loader_version="$(yqc -r '.plugins[0].version')" +loader_min="$(yqc -r '.plugins[0].devrail_min_version')" +assert_eq "kotlin" "$loader_name" "case3 plugin name" +assert_eq "1.0.0" "$loader_version" "case3 plugin version" +assert_eq "1.10.0" "$loader_min" "case3 devrail_min_version" + +# --- Case 4: target/gate shape parity with dev-toolchain HAS_KOTLIN blocks --- +echo "==> Case 4: target/gate shape mirrors HAS_KOTLIN behaviour" +for tgt in lint format_check format_fix test security; do + cmd="$(docker run --rm \ + -v "$LOADER_CACHE:/cache:ro" "$IMAGE" \ + bash -c "TGT='$tgt' yq -r '.plugins[0].targets[strenv(TGT)].cmd // \"\"' /cache")" + if [ -z "$cmd" ] || [ "$cmd" = "null" ]; then + echo "FAIL [case4]: target '$tgt' has no cmd in loader cache" >&2 + exit 1 + fi + gate0="$(docker run --rm \ + -v "$LOADER_CACHE:/cache:ro" "$IMAGE" \ + bash -c "TGT='$tgt' yq -r '.plugins[0].gates[strenv(TGT)][0] // \"\"' /cache")" + if [ -z "$gate0" ] || [ "$gate0" = "null" ]; then + echo "FAIL [case4]: target '$tgt' has no gate path in loader cache" >&2 + exit 1 + fi +done + +# Specific assertions on the cmd shape — guards regressions where someone +# changes the manifest but doesn't update behaviour parity. +lint_cmd="$(yqc -r '.plugins[0].targets.lint.cmd')" +echo "$lint_cmd" | grep -q "ktlint" || { + echo "FAIL [case4]: lint cmd missing ktlint, got: $lint_cmd" >&2 + exit 1 +} +echo "$lint_cmd" | grep -q "detekt-cli" || { + echo "FAIL [case4]: lint cmd missing detekt-cli (ktlint AND detekt parity), got: $lint_cmd" >&2 + exit 1 +} + +echo "==> All kotlin-plugin-extraction smoke checks passed (4/4)"