Skip to content

Commit b533d17

Browse files
docs(standards): plugin architecture v1.10 docs + blog (Story 13.6) (#26)
* docs(standards): plugin architecture v1.10 docs + blog (Story 13.6) - content/docs/contributing/adding-a-plugin.md — new "Contributing a Plugin" guide mirroring the canonical OrgDocs version. Field overview, quick start, versioning, override surface, what's-not-in-scope. - content/docs/contributing/_index.md — links the new page from the contribution table and guide list. - content/blog/2026-05-05-plugin-architecture.md — v1.10.6 release blog post covering motivation (plugins vs forks), what shipped (loader / resolver / build / execute), authoring quickstart, consumer declaration, and what's next (v1.11 Kotlin extraction, v2.0 monolith retirement). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci(ci): bump Node 20 → 22 to unbreak Hugo PostCSS pipeline postcss-cli's bin script now passes `--permission` (Node 22 experimental permission model). Node 20 rejects it as "bad option" and Hugo's RelPermalink chain fails to transform /scss/main.css. Bumping CI and deploy workflows to Node 22. This is a pre-existing failure on `main` (the most recent `Deploy to Cloudflare Pages` run is also red); folding the fix into this PR so the v1.10 plugin architecture marketing release can land. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 874d884 commit b533d17

6 files changed

Lines changed: 267 additions & 2 deletions

File tree

.github/workflows/ci.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,10 @@ jobs:
3232
- name: Setup Node.js
3333
uses: actions/setup-node@v4
3434
with:
35-
node-version: '20'
35+
# Node 22 — postcss-cli's bin script now passes --permission,
36+
# which Node 20 rejects as "bad option" and breaks Hugo's
37+
# PostCSS pipeline.
38+
node-version: '22'
3639

3740
- name: Install PostCSS
3841
run: npm install postcss postcss-cli autoprefixer

.github/workflows/deploy.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@ jobs:
3131
- name: Setup Node.js
3232
uses: actions/setup-node@v4
3333
with:
34-
node-version: '20'
34+
# Node 22 — postcss-cli's bin script now passes --permission,
35+
# which Node 20 rejects as "bad option" and breaks Hugo's
36+
# PostCSS pipeline.
37+
node-version: '22'
3538

3639
- name: Install PostCSS
3740
run: npm install postcss postcss-cli autoprefixer
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
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.

content/docs/contributing/_index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Before contributing, familiarize yourself with:
2020
| Type | Where to Contribute | Guide |
2121
|---|---|---|
2222
| Add a new language | `dev-toolchain` + `devrail-standards` | [Adding a Language](/docs/contributing/adding-a-language/) |
23+
| Author a plugin | A new `devrail-plugin-<name>` repo | [Contributing a Plugin](/docs/contributing/adding-a-plugin/) |
2324
| Fix a bug | The repo where the bug exists | [Pull Requests](/docs/contributing/pull-requests/) |
2425
| Improve documentation | `devrail.dev` (this site) | [Pull Requests](/docs/contributing/pull-requests/) |
2526
| Update a tool version | `dev-toolchain` | [Pull Requests](/docs/contributing/pull-requests/) |
@@ -57,5 +58,6 @@ To contribute to other DevRail repos, the prerequisites are simpler -- only Dock
5758
## Contribution Guides
5859

5960
- [Adding a New Language](/docs/contributing/adding-a-language/) -- Step-by-step guide for adding language ecosystem support
61+
- [Contributing a Plugin](/docs/contributing/adding-a-plugin/) -- Author a plugin that ships a new language or tool integration without forking the core
6062
- [Submitting Pull Requests](/docs/contributing/pull-requests/) -- Workflow, conventional commits, CI expectations
6163
- [Ecosystem Structure](/docs/contributing/ecosystem/) -- Repo map and relationships
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
---
2+
title: "Contributing a Plugin"
3+
linkTitle: "Contributing a Plugin"
4+
weight: 15
5+
description: "Step-by-step guide for authoring a DevRail plugin that ships a new language ecosystem or tool integration without forking the core repos."
6+
---
7+
8+
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.
9+
10+
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.
11+
12+
{{% alert title="Canonical Reference" color="info" %}}
13+
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.
14+
{{% /alert %}}
15+
16+
## What is a plugin?
17+
18+
A plugin is a git repository containing a `plugin.devrail.yml` manifest at the repo root. The manifest declares:
19+
20+
- **Identity**`name`, `version`, `schema_version`, `devrail_min_version`
21+
- **Container fragment**`apt_packages`, `copy_from_builder`, `env`, `install_script` (layered onto the core dev-toolchain image)
22+
- **Targets**`lint`, `format_check`, `format_fix`, `fix`, `test`, `security` commands
23+
- **Gates** — per-target paths that must exist for the target to run
24+
25+
When a consumer declares your plugin in their `.devrail.yml`, `make check` automatically:
26+
27+
1. Fetches the plugin's manifest at the pinned `rev:` and validates it against the schema
28+
2. Generates a `Dockerfile.devrail` extending the core image with your container fragment
29+
3. Builds `devrail-local:<hash>` (cached by content hash — unchanged plugin sets reuse the image)
30+
4. Runs your targets alongside core-language targets, aggregating results into the same JSON envelope
31+
32+
## Quick start
33+
34+
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).
35+
36+
2. **Add `plugin.devrail.yml`:**
37+
38+
```yaml
39+
schema_version: 1
40+
name: elixir
41+
version: 1.0.0
42+
devrail_min_version: 1.10.0
43+
44+
container:
45+
base_image: elixir:1.17-slim
46+
install_script: install.sh
47+
copy_from_builder:
48+
- /usr/local/bin/elixir
49+
- /usr/local/bin/mix
50+
- /usr/local/lib/elixir
51+
env:
52+
MIX_ENV: prod
53+
54+
targets:
55+
lint:
56+
cmd: "mix credo --strict {paths}"
57+
paths_var: ELIXIR_PATHS
58+
paths_default: "lib test"
59+
test:
60+
cmd: "mix test"
61+
62+
gates:
63+
lint: ["mix.exs"]
64+
test: ["mix.exs", "test/"]
65+
```
66+
67+
3. **Test locally** against a consumer workspace using a `file://` URL:
68+
69+
```yaml
70+
# In the consumer's .devrail.yml
71+
languages:
72+
- elixir
73+
74+
plugins:
75+
- source: file:///home/you/devrail-plugin-elixir
76+
rev: v1.0.0
77+
languages: [elixir]
78+
```
79+
80+
```bash
81+
make plugins-update # resolver fetches the file:// fixture
82+
make check # full pipeline runs your plugin
83+
```
84+
85+
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.
86+
87+
## Versioning
88+
89+
- **`schema_version`** is the manifest format. Pinned at `1` for the v1.10.x line. The loader rejects unknown majors.
90+
- **`version`** is your plugin's own semver. Bump on each release.
91+
- **`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.
92+
- 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.
93+
94+
## Override surface
95+
96+
Consumers can override your manifest defaults from their `.devrail.yml`:
97+
98+
```yaml
99+
elixir:
100+
linter: dialyxir # replaces targets.lint.cmd
101+
test: "mix test --cover" # replaces targets.test.cmd
102+
```
103+
104+
Override key map: `lint→linter`, `format_check`/`format_fix→formatter`, `fix→fixer`, `test→test`, `security→security`.
105+
106+
## What's NOT in scope for v1.10
107+
108+
These are deferred to later phases:
109+
110+
- **Plugin signing** — content_hash detects tampering, but not authenticity. Coming in a later release.
111+
- **Sidecar / volume-mounted plugins** — extended image (Option A) is the only execution mode in v1.
112+
- **Parallel plugin execution** — sequential per design; needs shared-state semantics first.
113+
114+
## Next steps
115+
116+
- 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.
117+
- 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.
118+
- 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.

static/images/devrail-icon.png

15.6 KB
Loading

0 commit comments

Comments
 (0)