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
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
# Node 22 — postcss-cli's bin script now passes --permission,
# which Node 20 rejects as "bad option" and breaks Hugo's
# PostCSS pipeline.
node-version: '22'

- name: Install PostCSS
run: npm install postcss postcss-cli autoprefixer
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
# Node 22 — postcss-cli's bin script now passes --permission,
# which Node 20 rejects as "bad option" and breaks Hugo's
# PostCSS pipeline.
node-version: '22'

- name: Install PostCSS
run: npm install postcss postcss-cli autoprefixer
Expand Down
139 changes: 139 additions & 0 deletions content/blog/2026-05-05-plugin-architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
---
title: "Plugin Architecture: v1.10 Ships"
date: 2026-05-05
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."
---

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.

**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.

## What changed

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).

When a consumer's `.devrail.yml` declares the plugin:

```yaml
languages:
- python
- elixir # provided by a plugin

plugins:
- source: github.com/community/devrail-plugin-elixir
rev: v1.0.0
languages: [elixir]
```

...the dev-toolchain container does the rest at `make check` time:

1. **Loader (Story 13.2)** validates `plugin.devrail.yml` against schema_version 1, enforcing `devrail_min_version` and per-target shape.
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.
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.
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.

`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.

## Why now

Three forces aligned:

- **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.
- **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`.
- **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.

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.

## Authoring a plugin

If you have a tool you want every DevRail-managed project to use, here's the quickest path:

1. Create a `devrail-plugin-<name>` git repo with a `plugin.devrail.yml`:

```yaml
schema_version: 1
name: elixir
version: 1.0.0
devrail_min_version: 1.10.0

container:
base_image: elixir:1.17-slim
install_script: install.sh
copy_from_builder:
- /usr/local/bin/elixir
- /usr/local/bin/mix
- /usr/local/lib/elixir
env:
MIX_ENV: prod

targets:
lint:
cmd: "mix credo --strict {paths}"
paths_var: ELIXIR_PATHS
paths_default: "lib test"
test:
cmd: "mix test"

gates:
lint: ["mix.exs"]
test: ["mix.exs", "test/"]
```

2. Test against a local consumer workspace via a `file://` URL:

```yaml
plugins:
- source: file:///home/you/devrail-plugin-elixir
rev: v1.0.0
languages: [elixir]
```

Then `make plugins-update && make check` in the consumer.

3. Tag an annotated semver tag (`git tag -a v1.0.0`) and publish.

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).

## Consumer-side declaration

If you're a consumer wanting to pull in someone else's plugin, just declare it in your `.devrail.yml`:

```yaml
languages:
- python
- elixir

plugins:
- source: github.com/community/devrail-plugin-elixir
rev: v1.0.0
languages: [elixir]
```

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.

Per-language overrides work for plugin languages exactly like they do for core:

```yaml
elixir:
linter: dialyxir # replaces the plugin's default `mix credo --strict`
test: "mix test --cover" # replaces the plugin's default `mix test`
```

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.

## What's next

This release is the foundation. Two follow-ups land in v1.11 and v2.0:

- **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.
- **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.

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.

## Try it

`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.

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.

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.
2 changes: 2 additions & 0 deletions content/docs/contributing/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Before contributing, familiarize yourself with:
| Type | Where to Contribute | Guide |
|---|---|---|
| Add a new language | `dev-toolchain` + `devrail-standards` | [Adding a Language](/docs/contributing/adding-a-language/) |
| Author a plugin | A new `devrail-plugin-<name>` repo | [Contributing a Plugin](/docs/contributing/adding-a-plugin/) |
| Fix a bug | The repo where the bug exists | [Pull Requests](/docs/contributing/pull-requests/) |
| Improve documentation | `devrail.dev` (this site) | [Pull Requests](/docs/contributing/pull-requests/) |
| Update a tool version | `dev-toolchain` | [Pull Requests](/docs/contributing/pull-requests/) |
Expand Down Expand Up @@ -57,5 +58,6 @@ To contribute to other DevRail repos, the prerequisites are simpler -- only Dock
## Contribution Guides

- [Adding a New Language](/docs/contributing/adding-a-language/) -- Step-by-step guide for adding language ecosystem support
- [Contributing a Plugin](/docs/contributing/adding-a-plugin/) -- Author a plugin that ships a new language or tool integration without forking the core
- [Submitting Pull Requests](/docs/contributing/pull-requests/) -- Workflow, conventional commits, CI expectations
- [Ecosystem Structure](/docs/contributing/ecosystem/) -- Repo map and relationships
118 changes: 118 additions & 0 deletions content/docs/contributing/adding-a-plugin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
---
title: "Contributing a Plugin"
linkTitle: "Contributing a Plugin"
weight: 15
description: "Step-by-step guide for authoring a DevRail plugin that ships a new language ecosystem or tool integration without forking the core repos."
---

DevRail plugins extend the dev-toolchain image with new languages or tool integrations *without* a fork or PR against the core repos. The plugin loader (shipped in v1.10.0+) reads `plugins:` from a consumer's `.devrail.yml`, resolves each entry to an immutable git ref, builds a project-local extended image (`devrail-local:<hash>`), and dispatches plugin-defined targets inside the existing `make check` recipes.

If you have a tool you want every DevRail-managed project to use, you can ship it as a plugin instead of opening a PR against `dev-toolchain`. This page is the high-level overview; the canonical authoring guide with full templates is in the `devrail-standards` repo.

{{% alert title="Canonical Reference" color="info" %}}
The authoritative, detailed plugin authoring guide lives at [`standards/contributing.md` § Contributing a Plugin](https://github.com/devrail-dev/devrail-standards/blob/main/standards/contributing.md#contributing-a-plugin). This page provides the overview; the canonical guide provides field-by-field schema, container integration patterns, and a publishing checklist.
{{% /alert %}}

## What is a plugin?

A plugin is a git repository containing a `plugin.devrail.yml` manifest at the repo root. The manifest declares:

- **Identity** — `name`, `version`, `schema_version`, `devrail_min_version`
- **Container fragment** — `apt_packages`, `copy_from_builder`, `env`, `install_script` (layered onto the core dev-toolchain image)
- **Targets** — `lint`, `format_check`, `format_fix`, `fix`, `test`, `security` commands
- **Gates** — per-target paths that must exist for the target to run

When a consumer declares your plugin in their `.devrail.yml`, `make check` automatically:

1. Fetches the plugin's manifest at the pinned `rev:` and validates it against the schema
2. Generates a `Dockerfile.devrail` extending the core image with your container fragment
3. Builds `devrail-local:<hash>` (cached by content hash — unchanged plugin sets reuse the image)
4. Runs your targets alongside core-language targets, aggregating results into the same JSON envelope

## Quick start

1. **Create the repo.** Convention is `devrail-plugin-<name>` (the trailing path component is not enforced — the manifest's `name` field is authoritative — but encouraged for discoverability).

2. **Add `plugin.devrail.yml`:**

```yaml
schema_version: 1
name: elixir
version: 1.0.0
devrail_min_version: 1.10.0

container:
base_image: elixir:1.17-slim
install_script: install.sh
copy_from_builder:
- /usr/local/bin/elixir
- /usr/local/bin/mix
- /usr/local/lib/elixir
env:
MIX_ENV: prod

targets:
lint:
cmd: "mix credo --strict {paths}"
paths_var: ELIXIR_PATHS
paths_default: "lib test"
test:
cmd: "mix test"

gates:
lint: ["mix.exs"]
test: ["mix.exs", "test/"]
```

3. **Test locally** against a consumer workspace using a `file://` URL:

```yaml
# In the consumer's .devrail.yml
languages:
- elixir

plugins:
- source: file:///home/you/devrail-plugin-elixir
rev: v1.0.0
languages: [elixir]
```

```bash
make plugins-update # resolver fetches the file:// fixture
make check # full pipeline runs your plugin
```

4. **Tag and publish** an annotated semver tag (`git tag -a v1.0.0`). Consumers pin via `rev: v1.0.0` (or a full SHA) — branch refs are rejected.

## Versioning

- **`schema_version`** is the manifest format. Pinned at `1` for the v1.10.x line. The loader rejects unknown majors.
- **`version`** is your plugin's own semver. Bump on each release.
- **`devrail_min_version`** is the oldest dev-toolchain version your plugin supports. Set to `1.10.0` for plugins targeting the first stable plugin-loader release.
- The consumer's `.devrail.lock` records the resolved SHA + content hash. Re-tagging an existing tag onto different code is detected via content_hash mismatch.

## Override surface

Consumers can override your manifest defaults from their `.devrail.yml`:

```yaml
elixir:
linter: dialyxir # replaces targets.lint.cmd
test: "mix test --cover" # replaces targets.test.cmd
```

Override key map: `lint→linter`, `format_check`/`format_fix→formatter`, `fix→fixer`, `test→test`, `security→security`.

## What's NOT in scope for v1.10

These are deferred to later phases:

- **Plugin signing** — content_hash detects tampering, but not authenticity. Coming in a later release.
- **Sidecar / volume-mounted plugins** — extended image (Option A) is the only execution mode in v1.
- **Parallel plugin execution** — sequential per design; needs shared-state semantics first.

## Next steps

- Read the [canonical plugin authoring guide](https://github.com/devrail-dev/devrail-standards/blob/main/standards/contributing.md#contributing-a-plugin) for field-by-field details, container integration patterns, and the pre-publish checklist.
- Read the [plugin architecture design doc](https://github.com/devrail-dev/dev-toolchain/blob/main/docs/plugin-architecture.md) for the full rationale and lifecycle.
- See the [`plugins:` schema documentation](https://github.com/devrail-dev/devrail-standards/blob/main/standards/devrail-yml-schema.md) for the consumer-side declaration shape.
Binary file added static/images/devrail-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading