From 426e9deb55d79a77614970cdb22edb6105ca3b23 Mon Sep 17 00:00:00 2001 From: Matthew Mellor Date: Mon, 4 May 2026 22:54:47 -0500 Subject: [PATCH 1/2] docs(standards): plugin architecture v1.10 docs + blog (Story 13.6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- .../blog/2026-05-05-plugin-architecture.md | 139 ++++++++++++++++++ content/docs/contributing/_index.md | 2 + content/docs/contributing/adding-a-plugin.md | 118 +++++++++++++++ static/images/devrail-icon.png | Bin 0 -> 15927 bytes 4 files changed, 259 insertions(+) create mode 100644 content/blog/2026-05-05-plugin-architecture.md create mode 100644 content/docs/contributing/adding-a-plugin.md create mode 100644 static/images/devrail-icon.png 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 0000000000000000000000000000000000000000..624234e667213910d9e10aa44054861a4ab72296 GIT binary patch literal 15927 zcmZvDbyU<%81C${NQsn`(h^EaiG;99V+&Jg{#WKH%cj|fTnU~sk?@&{oqXYm@tE$}80RRbqA_4g+_-Ds&-~fPY z1l60@b^TISC#ZrNP{H+wMqU6cD$n}b+SoFn;)9;AGAb^z(@5Ho2XTg{u)*6B}ZjAdG}>C=d5 z>y-6WTpwQEk@ZwfikVfI+73+HPWb}Jzc4zdrY0}2&U8h!t{$oV=(LVoE}4XW#bt-6udUd;z@UPc^&Rvz_vzEODiTeT9D2g!wSp zXX)C(vaI=(gzEy+ZDX%~E{jUrN}bMCEW=RK+$ijWzX$U#%*D{pTR zgAZL2*E})rd{#Pks&f}h(mv}MFTSxqKji${C2GH@Qcq@E#W=aj*1}>YAh??WNQMG1 zplY{3m}t>G)33AdA@YPXhyBPS_&~NM8+HF)pYv-yYK{S)Ni6^F@xWS1+D8I7>OIq)odL$G&FD{ zv?Y0cmx* zJ!i~6vVSdqHuWTLYv+(ib}akl9)`n3_$J0^ zO-o6|P}+MMb5V^+W$XP|igoc8vj?IAAy=g;ip{aKJb+7ACm&B`RkpwEUAVV{Z2@6*vPtVd41#AHB&5rV`_Vu5G|}{ z*A=uTzpVsUya=I2=^HK|xxO!*jzXeP2%x!8(xNZ?x&AtaZR!I}6n%#GME60jU3Z zk}<62`2#-1@xnw^&OLH$(kIn0GEwi8X693;hR%>*riOJ4`OEzxRpySzHXDsOeJHTE zS$nzM%EULtPM;};$LTB-#DP%c>m5X#7-|pPofby)3KOhu5UnG&SzYc$w88bVm3L_r zTJMV_27hB!qUH5Ju0MKSIj6-$nJh!0O@ajcQm&JAJ3m#%<-Lc~f8S}PK@A(+;^J^6 z4(zQ)ouMqHL4lR;TTho=!x;bM)r39}y8@0Q=}~aqT~mgPBdb z33Fj^Lp6+aXt{Kv6(Wr*RTXaw2RRtg6dVlxF!OW48<&L4c%a}E;cI!Nb~6Cs!vicd zsOje=)n<ETr%1T>XDpcu-zvik8G`w_rEVX99YFAm55^D z;Ey%jF>`!v?vgA9XxYkboI*3s3o&_CQOPht1phM&3l>W)IkUE1-B`$JdL4c69>rUzXXWz;fFocN* z=&Qj6^}#z68nmbRk!Voug8MpVQZ#Of1k-!Wj`jW9_}4U)$RBj5S3ZQ2zLrKtfm9`| z-o|O`l-lEIjq4eg$+)gsjo~qB<6?_LqM%nUrsXH=%ct_Wy6t!w#x0a)D=iw>w<--A zRo}o|HxmTCeVxu}G6A;lOZ{Ur^2veb(BR;)LQgE`@|q3;Yd@0l9a++0EAv~({@io$ z?qckstZ~oLaa+hX3VgUQV?O;>BS$7q4L?QoE|d~|Us%0n?Tyk9@d^T~BAKw@F&`vC zd7A3nE6{6^ntJHlYBi}PecXTspds*@>(1b_Z}G1~pK}8#ZMphCZtULG-7JPY0BgD% zkS3e0{jc7uxKDXq`7%W*0v;8SvCTu@&E>P8 z$uGtzQBg8l#Z@$Js{keW3S2JN;O{?PA2Z`UYCMHCy4sR>-$(SE*At7D%VZWU!syBw3`zwIV4q1u9}*L>ya-# z2igLQJAav%Y5u+b_Nf74FOy$dwRGy5teG}53M=O}v3CFUW^qE`V@`_lMtS&8u zBw37+gZ{T|!UuuQN9PYJsDQ<7)(fd=0>fX{J`9uRJag>N<&ItSZ>;pl-yqU~bE;9* z^E)@c9joWR4b;BO=Vfv~%Z7;Qn=k(scNX-X7B4zg$DiaCD2@6pkSK+0I%~f`yzZ6K2UB zyQUXLsM&vWz;hzy9Fk>h0zgs5!wB3GgtXnzTw350eHT~P%o3RDX3YugFAcT7A>dyH zH4ukG#ZlN)iKHK56+^X#EiN(?!2bG^NNX(?wiRtr@*nz>E)LfWY{^1IZ^;6dVfOu+&th}{J;0XSb~;#{N8v2T=rgR(@87Kr>HysH_6w{Pf*`2( zGDGDQ7+zJ>Pwt@vTuYJa{F099*}pk?t$6egRC1XcM_ATRO=yw>z>}FKP$GQxz$J$K zr7nP4L!PZynp8X5I(;yGS8NI3k3CjonJ-PzU`@(1dk z(bLgU0QGU}vaOzoXg(xBd#khOt_s|%R=7+b3rwrFb)4_g0kG%nE@AjiUvRD@j%j!c z(d5lsZ`yovKJs4rhe#~CT!acnmCA8Z6)W$$F*Aj~)K_o~0r;Jp-B-d-ksV?LL_XS( zV3+q+1gwr-f9p_WCSHhJVCR}+I1|^t#Zl&IT1F*lCI2OW4t+UkIwp~DU0iFHZ!g#E z;Ve13c_R+#F%)nI8nnjzs0s~I3+NuO_Jkkuz2l&W z{(ERHn(6BI9v&$_E%S_eEsei81s?7gsTQ`W!9ham`}gl-DzINFUi|tMsa}!eRr>UW z`5B@5)sf%^f!(g!?3s6i^y?k3?o;S@oks8YxnG!Di#VNLV?w#WCcAd1w!kS+7IwfJ z@;#mR=9%-X4u_$i7gVY2Uvgui@#x($QoyU>#U@Pbp!y-Y^6|i9YpwO+P@{=Z*^PbM zE+n=j=27e_(fi}uNE4%7?_3O8MbA-s3$d^54IIhHT-Z%SWaZ z(S6loW8&kDuOfv)1j>~Ho?OkGuJd66K?L!_nS-#b7*!Sg##+5ul}l~njeAy&<^q;h z5h;mk)l;0TZPaQno*mnw?<%-7X0EbP#3oAmq++D4?Fh`raz;3ZQ@x*#tVaubCiq_l z4(kO6uNbgUnd(awn8bJR%?kW_e+C#G5Sg7L6W-((8py5jD481Vvo}A4+ic$WkxURJyE;&z=L5kl_(wVBR zT5=@BEO75q5Yr^-b>5!_R7}Du{%QEMwdWqy3K(bNBe@Vl20#}q?Hle||MqKbrnN60 zI-IQY-|nNo0qZL(i(|4IwN1CGgyegQ4ua~G1VHb{zrlkGoul|^!`h-(Hb1P68u^~y zUzZ@(43!WyXnqLEMC&t12+XBv+6{P;RPNQv_;_={jPc`z8=sT@eMGQ$Lsy|1& zvjFh1kGB3hEH6)Ez{^el5zs~j5;MC(;B7DMeEs9LvUljYciGtf#Wfjwh8lYH@~+pc z{>z0~DZQioWS>`m21Z2&cx?CP1zBfYG|s)cQffZdo@_29D5hA!L)JM_X7OuGB=Yxx zwnl^Jon(dR^syfilPPyE)lFARcywOZnZyi@cy8{FXCHUp;q#t5{_AueUedf+$)q(I z+wx$274oNlW41MHH`yGo5zlB)Ws%Nu5FQRL)S7Sj=jYzZGT+1X964TE-(Sc74t=}AoV-5~GCHNSEhByOgAk~= zd+?!CRZF4yPr~uBB+-ayMXaA((VjR!pes`5r?T!MMSBq$wY9bVEX!ISExdoLZnPfZ zyyqJQ^f8p(se_8shezVX0bc&)objTRdXK-gL3nGzq1^UV?BLSv>COcTfQs8QP4My- zNfJRY{R3}0Mv1-i4xy^D?(}@exeYm!9SRZuI*3?k8GXtiJaHvVxYt}O3E)|;*KLdZ zYnOdEMG%YwfEK6FntqdR$ynq{hpy;KxujJ4hejY>r^+97ivfX3-z&R)_=z!Atm6ur z3+e~a13T;4>d;GuGIDsyUGGeW1l+KaIG!_MS4)Ep{H=c?VYoSoKw$rrht;|epV6Zq zQvp-c!{x+Ae5pmd%ei+)BF|h4Ek&@#dRh%!+XaIRJtFpvGjvIXd*hspfSkl}?n1e( zh#VTp?}ljF7G!pi+)O-V34+< zP;H>i47*#vG4HH|9O3q%4LfD<#(dV1Q(_|l7u++qYHf#`*)$Kt`NnvBC#jrz^v!(I zcQ34%A(pd|{!co-2j45*M<|d+0!!WeB2ND@y;uHlJ2gr*7^)lv%$0#|sR&3Ja< zY4;blijFR_Ug0^E6z|XS^p~uXoR7HOPb{eLxqM4*x3qBEW&; z7@DZKiOzI6g#y`>FT*B`SWd%KBBd_2&W;_8!#?B8>zz~+!)bw4hQ zj-~Lyz*8%wnfypqj!t;`CC#B7KpQ^Vb86ZX^|&(Dyon5a`(}h}x+~tg*}$baG!QxL z9eDRwdKd$#wSw!G%s2vDnx}iUW%Vy#SA%eA5`wGPe?u}f_iYEmlq?PS*1iqDwHvjT zU}aX*WHhM&^e;`E&oCX~puyPi2ifq_5;go|H=^svSN53QzfWn8fQW+V2iQ{&fFOZ7L-wTB zRE=QVR$vywRsnP-N#0REH=hba-~10F>U^P|b4aq3oyrIhzw3J3sprIcb>23U}G#$`O99brXBBzRf7DsT!y{ppxZxc~p z-3h&Q8hu$LSaIiE^$8aqIIo;bwi+uBBvkK{gDoStwAUpT8~k+;77H5RtzjRiHVRWy zbz7}@Dt<#mX~@gRqj2j{>dBRvu|}!BpOirf69BYVhuX8|5gI%|Sbd38m_P#1TuHG$ zdxO&Xvf!b_1p#}>x#Tl&f~;|Ov!9#kE+-4zLu`E6<^%v?8zEaggd1W5UQW|-aB|3w z#IXg$z}f$`+W?A$u%+>sSW|xFZ>I?-To((aMx}{=&QYw*fg$r}3Gl>{E3khm$?qxV z2Exw6jVmm$6}LrX!JJ1E1qoC!ZRQ76%5Ym*vicUIDb~m2Ey0FDJDkbWm*PI(8fxQZ z0qhifvkCa0*EHGgq))n9tD(Z|^1a>2;K|V*qaIFT`U>@vUbQhR36H&Kt!o+~I2o%~ zna~B7YTdqlOmQ^)8uy&SWz|s{uw3^tqz2#Dm8SDs+X~hhxZ&V`43}}!Gi1PS%6!FK z4Gwq1K0SE<3*tXt#d>x$RHZ!isj+u`J@zdXyE8^fj-bLwpkyidK7sl%Xv^PTIUvwKhSZqYwpK> zaVb3b>t~*AO(sMdt)sYwWaFwvtP+;W^h!E z5JE*?_UQg4an#M+;RT)1iix9A07AbemVrOG#mTgi2dSt;H4#OVizMb6)=mCfEX~Xc)MD;7TXsqHWfVG45VK>Sr7hQ zl8BX4@+ksMcf(fyjSokz9L$<|0*R!p@`v97Q5k$^TarqLnPS_ysZgMoeypZOEo>D( zYj$E(^)TKHWo2cwb1YtRe5O@zyF!To=zagRmBFfYMo__{0bacMY+jOoP^VNab~v+b zGs_vt2+&_1ERPqMocgDrMa$aeIw_*F_p!nDJ_V~R4g%)W9zUuxCPPR*t0!;9KI+N)@^eEw>q@2zSra(n3kg%#-%eR)@auns>JB}4W}0`b}sp= zqTfuQ&1_LCEede4^_>LyMs{Cb69khU*Lf>33ohB!9D@D#eg0U{U0m$o1(j&h<^_vS@5~lxfwlMB~ind`%3o{k+Y->27s802VnT z69;~F=3+dd?Px}+lMlNp-vs}19+~B!HxJIRdL}Oob0=7Fa^89;(@W()ijH;AoHDOZJ*6qtdxceP7I}gT702RBN zfX6&K#^oZLx){sv!j5wNT5VJB>`!-D>m8N z_lNa^yNnZIEb8(PtNHu}LO#&WXF-BTSJvi2BTtH1XI9yvGBxRB7r&DFc8e;$T{prv zXWePx$~Qo3p|!hPdl#97q!qss_x86;PyFwyN#SgLVdmt#P0?P04aSc1!EW}*lVgYi zf`_{1jmvcDfcDnR+YG~UbF{yse5r3_86{x2t*;85Ng!Nugc$87OYjZyV@ zS&NTwxC`a{>@j+NdAk*fU7$d4g&v7xFxBSw1v_+qaEx7Clbwoh79;Qi?4R%-D+cLG zSID5oJY4<)cv?=!wYcnCD~*c#s89X6Koiqm&ZPdM-xweXN})-&6~7%Jb!fwEcIHkd%gz*7 z+Z_X}+m_bB@m?D!O%wtfZ$Ali&yZLr2TPTWt9Q2)LKI$g^@LkB0d!l;+R z&F52lJPuirB@?2~GS`FwEz;9Fyj5QA3SJd^32V8|{%gaKoz(qcSBONzzg$Lg$CLnm0yove%n|m?P|5SL z6V$NoP_y2n7l0pVuZiCY16FXX54>w7D3$+OS%u)?4F>3nW@vqan_3YRSor5nrZLn- z=(oDiN`{4_ji=xIS4sD%1N>J{eKZ|qaAHXCF~a`9ulswhWC}b`47KOS;|Q7)$zv=V zk(YV@D>|YCPzt!7TSDM8{6))6*iia!rY)QVoIbMif6bICa^@!-9(!>#hw;C;aNRhH zC4ON-{wKzZv?Ch16RrQD#buZqK>O=tNgT;i3_4v-rpp?zrS$J@hiy3Q;pgR?I@K`n zH~D0d+idOZ?NiMW8z=u<=S!d3d73G2Gw~VD*bS3GbFY%M zd?pJc3=D+ejfSVJ(zdWC>JVdp&JPCM(H)X4#_LsDdf-2?unVjIw)3GdwA_&lCpStE z#6b-JXR%TDtC1SfY-M|5e8vwC#UB`xawRMWe=}mGmwL)7s24ox!;b<+ahd`JimqE1 zLZ7#}OcDNNG;Ytz0jmgq-CkN-&*G)+$sm1@@xXukBj-$Kd|ih4&O>6f04~)u&Fq3b zdAY3dE`IA33GC+q9zPi6ZhK#^mnHJT31p!F%tfiJd~NvH6P4zb2t#s$^>qj0qI(u4 z8RCV~fn*?fi|)E|v#XiuF8){MnV2m)by8rYi68?yFgC|dtha4n@?O!y{ z`Mw;zYOpJf`Jl0P=*ljXgg_g_nSb$WEpz%2^RYz!m?UO@Z2~SA4Gm6FdYb2R2i28m zH*?1bVfxOxPq|7*PjnO)H_9p#aS!dhRTc+(qT9FkhFvvGIydqXtqe7)d7z#l81YXM zJRj~fD?i--#$Yp>D#lR&bTQ>|dzgBoltwq1gO)N}f*QW$8vbHKYXM=UVuA7;&q>gD zb8tM%ixA!#-19G*?6ouKRiihre(+aD0$)qnexD(~BTEX3#D)@98n4(tsBBGiLcuPpB83MK6iqgqxbTi3-= ze;R+vWKZuT@Z=-7_J%bjw%*775QUzRJs;s|+dnGI^=MU zq?OX|jBfXgSHbgzsm&#Yw|>K#Cen>3odx7P#Jjn+);_I&6=|GUjCZAchym@#U){n^ zVVPGCn$z?V`wWQn@*!IAuIR6-)8JScHNYK;4B2AO`S7F2r{^~T!eU!eYi}8GRf&%J!W{Ar(b>r?6YtCbz_(cGimVVQ*yDCsy z2==o!p92N!Szp-hTc^???qmI*2%_|$Iz(nJww;F|zqyZtNwMz@ySl34E4Y9?P{YzcJdR0fKNjweMGJ&Hl0 z0NSyzqc`H+10>jx%~gr+ITAQdsbC~SqocX&h#mN8E8|qIN8o~`H&;GBeq2d%7|@DR z2G+y0lNVv3)T6~(fzu%E4(x|ZL!sxc5;#jqfw_jYJQ+m$=R!Y2@jP^gXbt79R`-SA zox-B4Z#~p>{~$96*$EK6&;|ozcEe{uVUdX#>|>+i5k^^LT~g=5@M?%|G7NJ9)Zm3` zlc`TelLq%mn{r?Nm4jU)Y%zMv@{VUfr1c5HqY^9cb_IKPA-Iw?Cq8K2h27HHY~u~3 z%`5di!HB==S53H}$W|77_RlBTMpH^J!R5VS;t4#0QAhsmHC0lqLE&od*g%RXEK`0P z;gCFFQm;x1h}}1a+MQwJ(Ak!+Uw6j^wn<~zkQ<|%5Q52LRB>IgXTXr&ktOyOL`|-X z%UAL@F;iJ2=#z~86<_G)*~_uNYvfB<9v6(2dd7_{Q>QM5`M+MTmJ!ajvl^FxUz%_B z%0*e&dj67wzG8N@|9wh;KWY*A_5lrj?D5@dlXV6OQdp=cB5T48;ohMzZoT8giTCpI z(|5cNV;H@spXq+|DAM@#mZT|1GnumG=Ud`;Xz9vz4u+Tu-~Eo+S`F&DsuY+NTUQB#m8VmvqT$wDjXoYdvq z3CqTU{RW9*{QcIks=kju3z0r{|2G}halIo=w9LTCDfyb6(#uxsPd3}O|98vR@0B;q zg_%4XV;mCaX`4n`_n#9iNgw|zf2gY2L&})mSiOJL#kkiK^}MvDxk02~Y86JncL}$W zYe^qRtMEDoh~~ixKNehzOEDo*q4myg8=H3?zFX2puxwMqb0FExvXHDp=1l=sS2GEH zO@=3B4qT~eV`Id?kLuurVlv5sKZ(MyS{LDc%+06X41DzxBMDMFAq=t7_+PjXwn#nH11uC#WmS?kZws|^K&Eo`6Xd@GmdXt!E3QKcSk zPXEuBpsZAfcFKXKOqboK?|t}#lABzp5o6D4!c6wvC20z`2=P4~2X{h}5{#gt!Y~?p zQC&rMJBZ$^*30L>r_r}c;+EkRrR~4|b7XG*8L7LJo_@@bpILJXL`&bY&YCNL=UXbK za*HvVOkL-W_Zh%5=aC$ulD`~wI%dSQ-z*RTIT0cuPNGKg0ZAuuv8LYMnh?5eWruqN zr~8<16o8B6$odn;$;DFtET!=kwNy=fNa|Ctj^4=t|7y+*4ipjCZJ9Yww1`%FKrJW&p!IUciS6R zU{bt}N7y8@66Ve}*8^QRPu0oa_ak%X?j1guiOen%zZ_yVa zPUMq6=wlU+MW8*ZZ%v8c=789Hnf3gqQA4FStWuc>^uxitN{=$Qk?&Zom5kS)VfLiH zBhb7_!uW7zCs<t`#15=$pCTew)F}IV%bJ3L20cd=>ql8 z2KduqyRaXN;Dqta56;etQ+RKO&>TpNSU!)r0WCntce`_I=xjDcgp^jNcP*x5wVd~1 zUAAYo-*iW|M<>j&WdU%m=jzq%TGm72FvwV)2IBBrBq{|1D~G@fv&&ibLn~hp95|A!H%fpWV^8L_z12T zxI0HOCS`MEEhupf>qJDCWb zqz?Y5^Q#jW5y$k%vRV%4<(5AEH}X4KBVI`aLHh!fdLo|EsFUxPYaUYwKnHXqs*O-4 zdwOSus7wg;qX*&?FLW;9!X8R&t4*v%14$BP`bPK45j<-dRqaiB=vPRkVOTAWhLqG> zpDB@dFaq4X$MJd`)_%zb8wlfnnU&Sb$i3=s^RvS#X1n<15Rb~Ii2s96?uHYQU2_L( zZ?NRev`w))m`>A{pATS;R$Y9GT4!OMN*8R%MjqQavpggN6|aJ-od~y1(pVN28JN^7 zCp&0_72o^*{rmX2r}Zwr(7vO-PaLh{C`nyrS0fS@+vU~r44g#f`MuN1)noR zRhLa08*&yxU-e9x1QF&b4;NZp+T&hbGk@n*p8gUwFVS@Eb>#dNzE~F{-zYkbb_U}r zHDQieuQA3FooMf$esu1D3B zYe2mg)i~?%(k{G^JsL~7MB8-t{PQC3Nq+g_bQqdBU14D@O6~z?yaLxjEqld?ozmb# zZ4b1IkayLI33DF72`Wvbb;wd>g~^cjS02|sh0)_{JMwoW+=h?z8TKi$t`8puibwxc zaEVCahPpdG*z-#5bO^;`ZCwzA4=QQAxuHc0?SxNC?Q;hh9&WmK;7@_>*yMZ7i!OK+ zc=5I?P2WG^sY)L5KcsVpgmMu2y-{bi?!Sa_w00+Ah-z#mQZK5U_YSeQj|NQ5F-gqx_ zww!=FTf>bNfJ`dF!#&F2uO$YREa(J;l5SxK55jQRPXl;#`xFCt<^FY6fyK zQb|e+AlFJ;Vo`K(Vgce2OshO-JysGC;o4!u3RG4_H5exkA|dhKFQdFX=zYbDQZ3}~ z*UNhf%HfiYza~TxAS2{dguB_BE*FCfA|Gr7L4!##PHb#H7Fh&Al5;F~#M3x`8&}!P zhBaL^5xkmTD+UdcoHiDGlu0pBsL#si5xE63;Q$Of1jaxNz9nCR!#w+soijk&B&xt( z;2--r3L@40^1D5Rsf!{b_(LOU@FH^1XFDzz*e=LS z+D6pfG7x0}8NrXm=j(2~$JxG2jBljOcxNS!%iZ%Crcr8z`t(zwG-uabKqD{R**5S8 zlIDyhk`%R@6^sFhwk`@%N?#95zo|P5@zT<>dTrb5Dwl^1C%{&Vq_0QpxVgP0!5T@Q z3$JX{!_e|bZ;}8Wm&%!DvkRa2`xw4mC_mHG(D|;?%^~~mz9UMQ7NCX3S;9vzkexah zR&BloFV^>_rj|-bh8vo=jsO)O5j~P68QW8jL1}+vvLXxSeMTKbn^%<*S(t|QM%5IH z(+CS@XhlxEJ7Zo{djaz07{3RE`l^WzV`~3gLSe7%hhJy0KBjGY^cc{FWRBq*71P)2 zZXfq-#IMpC6j-CQXVLclMLwHn>?7808O_cchg&Z*S(r%3J?d@bRA@0He zAMYJkQyy)|6gJ|EJ{K zPvRX&YL1CNp31W!3Zd@bpa^^?k(3r&RH?t{5(aPaSM!g@8X|{8jmtKWPy%&ma*ssq-7N!Sa!gFTCIu{HOob5rzWVVl*vQdn$6o1Gq4wfBxjO zvI&Glta+?EfW!v#A675pNJx&CQQkl193?Xh*w`+`#Rn-Ru_>bO7^XqOY5u;RsLUbt zg$o!F@})>{+4{4tTJ}-T*m$ex>8p1WT@j%2Q_Ig*&*}!MW9xj2+qA&epe(UgI;pd8 z&=j+1LBNnSHN?SNZCifKOl5o4E}J?Ak~wc;@-T(9FpaF;<>QzN8Pn8Nl(uY$jK_V8 z&%#(|_7f;X8KI5DQ9hKG(u4D4eDK7L5lu3;eO^CsG7ppV=Nc0D^_dRoMP_Zg3b$mP zck=SOXf8O{kg8Fm8vI2Ek`OxeJ*!9U;?<&TNU49&q8V%?!U>bxoi;5hxiByo@o%pi z9+&f>Ib1->P;hXYt+h;ldF?S0&{-4}`m(KikFK;#QNDXF3pDjE?o5`*u8vQ|RTgn&8{&1XmHJa^=vR%uEWF;Z@mN z7%freAu7o9{uE#$FOaZIe93j093UWLIHabMPzM6Pz|_gL!q)Qs>Si&&L=sx`=*siP z&s7OWyU0X#Dz46kp;1GB#Z=7YdkFF`UID)rTM|o_)*$(7`m#ll$(tVBF`Cu@g{4)= zo)_^-`Ruy1riwhtYW`M`TdMQ-^HVP2>it5=h4Lxp@VgZl7*Ive5oO(iOF{-9A9!i*jx|Z zm6xpxCev3Z`*)|mA_28!$cq2m{Vn|KM&_qXj94bf^-ZyiIR3QO%?J`%&sQ$(uLU*W z?!ZR{$X*@@6-zfiY>&4zA5CNJsK7wf;P#L8b2D{<3CpH@_7w1G#2Y;lv)nExR^HC&~AwckWWyrd#^jT@gieR8)5Hm zYC6%gt+1IuZ^jL@A={|IptHf*tu);OLj`5{`|5`YCVwZwl|L3AO3>jfVtfpl^h=a- zLHl2!W~{Rk@6l-xiKIk7_3{#4d)d00D>aqQ2R3B%w9bUgUfoMS1reHp2ZHGJMX#RC zgN~3gLN})<_^{?%Q!kdVTzmiIjmW=Ob`s%}4n_FHHD3yB-eP|&Hp-!zxLmVN!4oD6 ze$6eiOw~I(vrkF!$Ny^Wp(|zrA1rRR;*};w7gMX9$6Ep)uAIZ%q=VTP7(i0?GWaty z)Xoau!kpsX@W(AF{&Jr!|Mn;p-LWCI4Bx4=S}Be&BEVpC1&`b3 z&zys+070u4#cEBsmb`jge7QU78Z9WL>0l_XU1d8K-xQ$yRCt+|!`S=KNi<>q1El67 zK5{{S>V!Z^P5omsyOy)hIUc^vc3Aq9v=}9L;E`qTz|0FjG!DZhf z7t;C?6X51{?1OlOh76HadX!?`xD$M4H(YSx#Wsw>8(jdRR ztVqOv^S}2)mq}4z;A`KWhV9Po&=_WrjEoAtbh<}^vnew;yahWEMS`U^xE!%It!$4o zNzwI%@U2=*R}QqHJ^*hxW<)PnsZKJ@zdb?AmzEYEU4E^8fGA;P*=9Dt3iA}?@f*JBg9LF!&(=a7&83Z#}=m^6&=49 zFXME4iAe_B*8$odBJ#qEk^4J;0-q}eC26ywt#!R>TUi1hWb5D&!z&jG$$>ouA(i4< ziQmV{$TM^XN~gad2ekBhc3gYYi_%-I*oWONa8$sxFuDi*+Ni$0v!m`}p~|F*Qrme1 z_gJ5U;dzhp%56+3FPaMO%$x(u)teoa0)|3jgPi`DNuYl=;~b`M()pm=BU3M?xMwba z$b+W30JyAc!Ki)?K={==f0J=GX}V zSXtTmPK%Yb5G_aMgHY>dZBL8Z)9ZX2L!#CPl&9DutzB$~6hA~jBl8;#DqCm#*O+SI zj)Lqr7TFu=YxGZlVM93Az1gaBb)x0_7TYJ1uQ&^mW5rRBkd@*UFwNa^^&F}*TF&VY zS?G5!M+Q_|-t$VbqDtIS)e47Wec(2p5N6|a91LKj6sv0dEB-D{@w>Z~kk^XeGam(^ zoi@AWcqXY(CByZefPJH)8C*f3bb*Jg9b;7=DL*tw{`U)8m=yJJWG|v0nLF4ie=1Fp z^D%Gy0wJmXbHTM9dt<}Y9@iih17l8Tf1JG8xpN2i*Pv5avPDsoQ>;I?WzRck48ykT z_D8IAJ1>51_#cs(c-KG5-p}ZM{)qJV@AU-Jor}{X@Z~I!x~D%JGdlOFYQ%j7agKa% zxZABvv8ZTusBaIsN}OM`#(46Y97*no1iTpiK#bQsa9!+gjaqKpbU&7H=`F{5J3DW- zlASzU#6tBAzA~$4&pVK-boX};(2#gmq-L)cvKJ-I`4bwvmgXM`j2D-9{~QUu!~dT9 zTjMLsVX!&7T)WlUbH~TOko^nmK*?@NIL@w{W^u}Npiw?|{LRb@pWi-cd7nm-sbag0 sF}bmGt2^Pwl+Q$xpGjP!wI_1Ei1FWYKj#Ktw*a7e>+a3s8y2De11SjFGXMYp literal 0 HcmV?d00001 From 0996213983a526c4b0187889969881d51ad05bd5 Mon Sep 17 00:00:00 2001 From: Matthew Mellor Date: Mon, 4 May 2026 23:11:17 -0500 Subject: [PATCH 2/2] =?UTF-8?q?ci(ci):=20bump=20Node=2020=20=E2=86=92=2022?= =?UTF-8?q?=20to=20unbreak=20Hugo=20PostCSS=20pipeline?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .github/workflows/ci.yml | 5 ++++- .github/workflows/deploy.yml | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) 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