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
11 changes: 11 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,17 @@ jobs:
DEVRAIL_IMAGE: ${{ env.IMAGE_NAME }}
DEVRAIL_TAG: ${{ env.IMAGE_TAG }}

# Phase 2g: Plugin execution smoke test (Story 13.5)
# Drives lib/plugin-execute.sh against hand-crafted loader-cache
# fixtures: dispatcher no-op, pass / fail, gate skip / run, paths
# interpolation, per-language override, DEVRAIL_FAIL_FAST short-circuit,
# partial targets, and JSON-shape regression.
- name: Plugin execution smoke test
run: bash tests/test-plugin-execution.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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Plugin execution loop and JSON aggregation (Story 13.5, Epic 13 / v1.10.x preview):
- **`_lint` / `_format` / `_fix` / `_test` / `_security` now dispatch each loaded plugin's matching target after the core `HAS_<LANG>` blocks.** The new `lib/plugin-execute.sh` library exposes `dispatch_plugin_target` (per-recipe), `evaluate_gate`, `render_cmd`, and `apply_override`. Plugin results enter the existing `ran_languages` / `failed_languages` arrays and the same JSON envelope — consumers cannot distinguish plugin vs core results from the JSON shape.
- **Per-target gate evaluation.** When a plugin manifest declares `gates: { <target>: [path, ...] }`, the dispatcher only runs the target when every gate path exists (file, directory, or glob match). Absolute paths are rejected. Skips emit a structured `plugin gate skipped` event.
- **`{paths}` interpolation.** When a target's `cmd` references `{paths}`, the value of `${<paths_var>}` (filtered to existing paths) is substituted, mirroring how `RUBY_PATHS` is filtered in the core Ruby block. Falls back to `paths_default` when the env var is unset.
- **Per-language overrides for plugin languages.** `.devrail.yml` entries like `elixir: { linter: dialyxir }` replace the manifest's default `targets.<name>.cmd` for that target. Override key map: `lint` → `linter`, `format_check` / `format_fix` → `formatter`, `fix` → `fixer`, `test` → `test`, `security` → `security`.
- **`DEVRAIL_FAIL_FAST=1` parity.** A plugin failure under fail-fast short-circuits the dispatcher and the recipe — no later plugins run.
- **No-op when `plugins:` is absent.** `_plugins-load` writes an empty cache; the dispatcher exits immediately. v1.9.x consumers see byte-identical JSON output and no behavioural change.
- **`SHELL := /bin/bash`** — the Makefile now pins recipes to bash so `lib/plugin-execute.sh` (uses `[[`, `((`, indirect parameter expansion) can be sourced directly. Existing POSIX-sh recipes remain valid.
- Smoke test: `tests/test-plugin-execution.sh` exercises 10 cases (no-op, pass, fail, gate skip / run, paths interpolation, override, fail-fast, partial targets, JSON regression).

## [1.10.4] - 2026-05-04

### Added
Expand Down
68 changes: 63 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@
# ---------------------------------------------------------------------------
# Variables (overridable via environment)
# ---------------------------------------------------------------------------
# Use bash for all recipes so we can source bash-only libs (plugin-execute.sh
# uses [[, ((, and indirect parameter expansion). Existing POSIX-sh recipes
# remain valid because bash is a superset.
SHELL := /bin/bash

DEVRAIL_IMAGE ?= ghcr.io/devrail-dev/dev-toolchain
DEVRAIL_TAG ?= local
DEVRAIL_FAIL_FAST ?= 0
Expand Down Expand Up @@ -365,8 +370,15 @@ _plugins-load: _plugins-verify
if [ "$$failed" -gt 0 ]; then exit 2; fi

# --- _lint: language-specific linting ---
# After the core HAS_<LANG> blocks, plugin targets (Story 13.5) are dispatched
# via lib/plugin-execute.sh. The dispatcher updates overall_exit /
# ran_languages / failed_languages in this recipe's shell scope, so plugin
# results aggregate into the same JSON envelope as core results. The
# DEVRAIL_FAIL_FAST short-circuit pattern is mirrored after the dispatch
# call to keep behaviour symmetric with the per-language blocks.
_lint: _plugins-load
@start_time=$$(date +%s%3N); \
@. /opt/devrail/lib/plugin-execute.sh; \
start_time=$$(date +%s%3N); \
overall_exit=0; \
ran_languages=""; \
failed_languages=""; \
Expand Down Expand Up @@ -572,6 +584,13 @@ _lint: _plugins-load
exit $$overall_exit; \
fi; \
fi; \
dispatch_plugin_target lint; \
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; \
end_time=$$(date +%s%3N); \
duration=$$((end_time - start_time)); \
if [ $$overall_exit -eq 0 ]; then \
Expand All @@ -583,7 +602,8 @@ _lint: _plugins-load

# --- _format: language-specific format checking ---
_format: _plugins-load
@start_time=$$(date +%s%3N); \
@. /opt/devrail/lib/plugin-execute.sh; \
start_time=$$(date +%s%3N); \
overall_exit=0; \
ran_languages=""; \
failed_languages=""; \
Expand Down Expand Up @@ -722,6 +742,13 @@ _format: _plugins-load
exit $$overall_exit; \
fi; \
fi; \
dispatch_plugin_target format_check; \
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; \
end_time=$$(date +%s%3N); \
duration=$$((end_time - start_time)); \
if [ $$overall_exit -eq 0 ]; then \
Expand All @@ -733,7 +760,8 @@ _format: _plugins-load

# --- _fix: language-specific format fixing (in-place) ---
_fix: _plugins-load
@start_time=$$(date +%s%3N); \
@. /opt/devrail/lib/plugin-execute.sh; \
start_time=$$(date +%s%3N); \
overall_exit=0; \
ran_languages=""; \
failed_languages=""; \
Expand Down Expand Up @@ -872,6 +900,20 @@ _fix: _plugins-load
exit $$overall_exit; \
fi; \
fi; \
dispatch_plugin_target format_fix; \
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; \
dispatch_plugin_target fix; \
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; \
end_time=$$(date +%s%3N); \
duration=$$((end_time - start_time)); \
if [ $$overall_exit -eq 0 ]; then \
Expand All @@ -883,7 +925,8 @@ _fix: _plugins-load

# --- _test: language-specific test runners ---
_test: _plugins-load
@start_time=$$(date +%s%3N); \
@. /opt/devrail/lib/plugin-execute.sh; \
start_time=$$(date +%s%3N); \
overall_exit=0; \
ran_languages=""; \
failed_languages=""; \
Expand Down Expand Up @@ -1051,6 +1094,13 @@ _test: _plugins-load
exit $$overall_exit; \
fi; \
fi; \
dispatch_plugin_target test; \
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; \
end_time=$$(date +%s%3N); \
duration=$$((end_time - start_time)); \
if [ -z "$${ran_languages}" ] && [ -n "$${skipped_languages}" ]; then \
Expand All @@ -1064,7 +1114,8 @@ _test: _plugins-load

# --- _security: language-specific security scanners ---
_security: _plugins-load
@start_time=$$(date +%s%3N); \
@. /opt/devrail/lib/plugin-execute.sh; \
start_time=$$(date +%s%3N); \
overall_exit=0; \
ran_languages=""; \
failed_languages=""; \
Expand Down Expand Up @@ -1211,6 +1262,13 @@ _security: _plugins-load
exit $$overall_exit; \
fi; \
fi; \
dispatch_plugin_target security; \
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; \
end_time=$$(date +%s%3N); \
duration=$$((end_time - start_time)); \
if [ -z "$${ran_languages}" ] && [ -n "$${skipped_languages}" ]; then \
Expand Down
4 changes: 2 additions & 2 deletions STABILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ DevRail has reached **v1.0** across all repositories. The core standards, toolch
| Component | Status | Notes |
|---|---|---|
| **Container image** | Stable | Multi-arch (amd64 + arm64), signed with cosign, weekly rebuilds. |
| **Makefile contract** | Stable | Two-layer delegation pattern, JSON summary output, `init` scaffolding. |
| **Makefile contract** | Stable | Two-layer delegation pattern, JSON summary output, `init` scaffolding. As of v1.10.x the reference Makefile pins `SHELL := /bin/bash` so plugin libraries can be sourced directly into recipes; consumer template repos that inherit this Makefile require `bash` on the host (already the default on Debian/Ubuntu/macOS — only relevant for busybox/Alpine without bash). |
| **Shell conventions** | Stable | `lib/log.sh`, `lib/platform.sh`, header format, and idempotency patterns are settled. |
| **Conventional commits** | Stable | Types, scopes, and format are finalized. Pre-commit hook published. |
| **Language standards** | Stable | Python, Bash, Terraform, Ansible, Ruby, Go, JavaScript/TypeScript, Rust — all 8 ecosystems shipped. |
Expand All @@ -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** | 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. Verifies lockfile + content_hash on every `make check`. No-op when `plugins:` is absent — v1.9.x behaviour unchanged. Plugin command execution (running plugin-defined `targets:`) ships in Story 13.5. |
| **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. |

## Consumer responsibilities

Expand Down
Loading
Loading