|
| 1 | +--- |
| 2 | +title: "Plugin Architecture: v1.10 Ships" |
| 3 | +date: 2026-05-05 |
| 4 | +description: "DevRail v1.10 introduces a plugin architecture so anyone can ship a new language or tool integration without forking dev-toolchain. Loader, lockfile, extended-image build, and execution dispatch all in one container, one make check." |
| 5 | +--- |
| 6 | + |
| 7 | +For the first eighteen months of DevRail, every new language meant a PR against `dev-toolchain` -- a Dockerfile change, an install script, Makefile blocks for `_lint` / `_format` / `_test` / `_security`, a standards doc, and a release. That worked while we were stabilizing the eight core ecosystems (Python, Bash, Terraform, Ansible, Ruby, Go, JavaScript/TypeScript, Rust, and most recently Swift and Kotlin), but it doesn't scale to the long tail of languages and tools real teams use. |
| 8 | + |
| 9 | +**v1.10.6 ships a plugin architecture.** Anyone can now publish a `devrail-plugin-<name>` git repo, and any DevRail-managed project can declare it in `.devrail.yml` and pick up new tools at the next `make check`. No fork, no PR, no waiting on a release. The "one container, one make check" guarantee holds throughout. |
| 10 | + |
| 11 | +## What changed |
| 12 | + |
| 13 | +A plugin is a git repository with a `plugin.devrail.yml` manifest at the root. The manifest declares the plugin's container fragment (apt packages, COPY-from-builder paths, install script, env vars) and its targets (lint, format, test, security commands). |
| 14 | + |
| 15 | +When a consumer's `.devrail.yml` declares the plugin: |
| 16 | + |
| 17 | +```yaml |
| 18 | +languages: |
| 19 | + - python |
| 20 | + - elixir # provided by a plugin |
| 21 | + |
| 22 | +plugins: |
| 23 | + - source: github.com/community/devrail-plugin-elixir |
| 24 | + rev: v1.0.0 |
| 25 | + languages: [elixir] |
| 26 | +``` |
| 27 | +
|
| 28 | +...the dev-toolchain container does the rest at `make check` time: |
| 29 | + |
| 30 | +1. **Loader (Story 13.2)** validates `plugin.devrail.yml` against schema_version 1, enforcing `devrail_min_version` and per-target shape. |
| 31 | +2. **Resolver + lockfile (Story 13.3)** resolves `rev:` to an immutable SHA, fetches the plugin tree to a content-addressed cache, and records the resolved metadata in `.devrail.lock`. Branch refs are rejected. Tag-rebases are detected via content_hash mismatch. |
| 32 | +3. **Extended-image build (Story 13.4)** generates a project-local `Dockerfile.devrail` that layers each plugin's apt / COPY / ENV / install_script onto `ghcr.io/devrail-dev/dev-toolchain:v1`, then builds `devrail-local:<hash-of-dockerfile>` via BuildKit. Cache hits are free; first builds take 30 s -- 2 min depending on the plugin. |
| 33 | +4. **Execution loop (Story 13.5)** dispatches each plugin's matching target inside the existing `_lint` / `_format` / `_fix` / `_test` / `_security` recipes, with gate evaluation, `{paths}` interpolation, per-language overrides, and JSON aggregation into the same envelope as core results. Consumers can't tell from the JSON output which results came from core and which from a plugin. |
| 34 | + |
| 35 | +`DEVRAIL_FAIL_FAST=1` short-circuits on plugin failures the same as core. Workspaces without `plugins:` in `.devrail.yml` see byte-identical behavior to v1.9.x -- the loader writes an empty cache, the dispatcher exits immediately, no extra events. |
| 36 | + |
| 37 | +## Why now |
| 38 | + |
| 39 | +Three forces aligned: |
| 40 | + |
| 41 | +- **The core surface stabilized.** With ten languages shipped (the most recent two -- Swift and Kotlin -- landed in March) the patterns for "what goes in `_lint`, `_test`, etc." are clear enough to expose as a contract. |
| 42 | +- **The container model is cheaper than people think.** BuildKit content-addresses every layer; an unchanged plugin set is an instant cache hit. We benchmarked Elixir + Rust + Swift in the same project and the second `make check` was within 200 ms of the first -- the entire build pipeline boils down to a `docker image inspect`. |
| 43 | +- **Real teams have real tools we shouldn't ship.** Mojo. Zig. Roc. Crystal. Internal DSLs. Every one of these comes up in conversation; none of them belongs in `dev-toolchain` core. A plugin gives them a first-class home with the same UX as the languages we do ship. |
| 44 | + |
| 45 | +The architecture is documented in detail in the [design doc on GitHub](https://github.com/devrail-dev/dev-toolchain/blob/main/docs/plugin-architecture.md). The TL;DR: we surveyed Terraform providers, GitHub Actions, pre-commit, and pip extras, then picked declarative YAML manifests + git-repo distribution + immutable refs + a single execution mode (extended container image). The single-mode choice is deliberate -- DevRail's value proposition is one container, one make check, and we kept it. |
| 46 | + |
| 47 | +## Authoring a plugin |
| 48 | + |
| 49 | +If you have a tool you want every DevRail-managed project to use, here's the quickest path: |
| 50 | + |
| 51 | +1. Create a `devrail-plugin-<name>` git repo with a `plugin.devrail.yml`: |
| 52 | + |
| 53 | + ```yaml |
| 54 | + schema_version: 1 |
| 55 | + name: elixir |
| 56 | + version: 1.0.0 |
| 57 | + devrail_min_version: 1.10.0 |
| 58 | +
|
| 59 | + container: |
| 60 | + base_image: elixir:1.17-slim |
| 61 | + install_script: install.sh |
| 62 | + copy_from_builder: |
| 63 | + - /usr/local/bin/elixir |
| 64 | + - /usr/local/bin/mix |
| 65 | + - /usr/local/lib/elixir |
| 66 | + env: |
| 67 | + MIX_ENV: prod |
| 68 | +
|
| 69 | + targets: |
| 70 | + lint: |
| 71 | + cmd: "mix credo --strict {paths}" |
| 72 | + paths_var: ELIXIR_PATHS |
| 73 | + paths_default: "lib test" |
| 74 | + test: |
| 75 | + cmd: "mix test" |
| 76 | +
|
| 77 | + gates: |
| 78 | + lint: ["mix.exs"] |
| 79 | + test: ["mix.exs", "test/"] |
| 80 | + ``` |
| 81 | + |
| 82 | +2. Test against a local consumer workspace via a `file://` URL: |
| 83 | + |
| 84 | + ```yaml |
| 85 | + plugins: |
| 86 | + - source: file:///home/you/devrail-plugin-elixir |
| 87 | + rev: v1.0.0 |
| 88 | + languages: [elixir] |
| 89 | + ``` |
| 90 | + |
| 91 | + Then `make plugins-update && make check` in the consumer. |
| 92 | + |
| 93 | +3. Tag an annotated semver tag (`git tag -a v1.0.0`) and publish. |
| 94 | + |
| 95 | +Full field-by-field guidance, container integration patterns, override surface, and a publish checklist are in the [Contributing a Plugin guide](/docs/contributing/adding-a-plugin/). The canonical authoring doc with copy-pasteable templates is the [`standards/contributing.md` § Contributing a Plugin section](https://github.com/devrail-dev/devrail-standards/blob/main/standards/contributing.md#contributing-a-plugin). |
| 96 | + |
| 97 | +## Consumer-side declaration |
| 98 | + |
| 99 | +If you're a consumer wanting to pull in someone else's plugin, just declare it in your `.devrail.yml`: |
| 100 | + |
| 101 | +```yaml |
| 102 | +languages: |
| 103 | + - python |
| 104 | + - elixir |
| 105 | +
|
| 106 | +plugins: |
| 107 | + - source: github.com/community/devrail-plugin-elixir |
| 108 | + rev: v1.0.0 |
| 109 | + languages: [elixir] |
| 110 | +``` |
| 111 | + |
| 112 | +Run `make plugins-update` once to populate `.devrail.lock`, commit both files, and you're done. Subsequent `make check` invocations verify the lockfile, resolve the cached plugin, build the extended image (or hit the cache), and run plugin tools alongside your core-language tools. |
| 113 | + |
| 114 | +Per-language overrides work for plugin languages exactly like they do for core: |
| 115 | + |
| 116 | +```yaml |
| 117 | +elixir: |
| 118 | + linter: dialyxir # replaces the plugin's default `mix credo --strict` |
| 119 | + test: "mix test --cover" # replaces the plugin's default `mix test` |
| 120 | +``` |
| 121 | +
|
| 122 | +Override key map: `lint→linter`, `format_check`/`format_fix→formatter`, `fix→fixer`, `test→test`, `security→security`. See the full [`.devrail.yml` schema reference](https://github.com/devrail-dev/devrail-standards/blob/main/standards/devrail-yml-schema.md) for the consumer surface. |
| 123 | + |
| 124 | +## What's next |
| 125 | + |
| 126 | +This release is the foundation. Two follow-ups land in v1.11 and v2.0: |
| 127 | + |
| 128 | +- **v1.11.0 -- Kotlin extracted as the reference plugin.** We'll move Kotlin tooling out of the dev-toolchain image into a `devrail-plugin-kotlin` repo and document the extraction recipe so other languages can follow. This proves the model end-to-end against a non-trivial language ecosystem and gives future contributors a working template. |
| 129 | +- **v2.0.0 -- monolithic `HAS_<LANG>` blocks retired.** All language support becomes plugin-based. We'll ship `devrail-init migrate --to v2` to handle the consumer-side cutover. Major version bump. |
| 130 | + |
| 131 | +Plugin signing (cosign-style signature verification opt-in) is a separate track gated on a real supply-chain incident or broader ecosystem signals -- see [Story 13.10 in the epics](https://github.com/devrail-dev/devrail-standards/blob/main/_bmad-output/planning-artifacts/epics.md) for the rationale. |
| 132 | + |
| 133 | +## Try it |
| 134 | + |
| 135 | +`ghcr.io/devrail-dev/dev-toolchain:v1.10.6` and the floating `:v1` tag both ship the plugin loader. If you're already on v1, your next `docker pull` picks it up. Add a `plugins:` block to your `.devrail.yml`, run `make plugins-update`, commit `.devrail.lock`, and the next `make check` runs the plugin's tools alongside your core ones. |
| 136 | + |
| 137 | +If you build a plugin we should know about, open a PR against the (forthcoming) `awesome-devrail` discovery list. For now, drop it in your team's repo or publish on GitHub and link it from your README. |
| 138 | + |
| 139 | +The full [plugin architecture design doc](https://github.com/devrail-dev/dev-toolchain/blob/main/docs/plugin-architecture.md) and the [v1.10.6 changelog entry](https://github.com/devrail-dev/dev-toolchain/blob/main/CHANGELOG.md) cover the contract in detail. Questions, plugin authors who want feedback, or edge cases we should think about -- as always, open an issue. |
0 commit comments