diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4cd1fb5..bc41a01 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b05c24f..f04ccc8 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -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 diff --git a/content/blog/2026-05-05-plugin-architecture.md b/content/blog/2026-05-05-plugin-architecture.md new file mode 100644 index 0000000..8853eb3 --- /dev/null +++ b/content/blog/2026-05-05-plugin-architecture.md @@ -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-` 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:` 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-` 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_` 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. diff --git a/content/docs/contributing/_index.md b/content/docs/contributing/_index.md index 816a2ae..b819917 100644 --- a/content/docs/contributing/_index.md +++ b/content/docs/contributing/_index.md @@ -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-` 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/) | @@ -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 diff --git a/content/docs/contributing/adding-a-plugin.md b/content/docs/contributing/adding-a-plugin.md new file mode 100644 index 0000000..80d0fcc --- /dev/null +++ b/content/docs/contributing/adding-a-plugin.md @@ -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:`), 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:` (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-` (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. diff --git a/static/images/devrail-icon.png b/static/images/devrail-icon.png new file mode 100644 index 0000000..624234e Binary files /dev/null and b/static/images/devrail-icon.png differ