Skip to content

feat(core): concurrency conformance via conformance-kit v0.4.0 - #45

Merged
mhenrixon merged 22 commits into
mainfrom
chore/conformance-caller
Oct 15, 2026
Merged

mhenrixon merged 22 commits into
mainfrom
chore/conformance-caller

Conversation

@mhenrixon

@mhenrixon mhenrixon commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

phlex-forms passes conformance-kit v0.4.0's gates: conformance / Conformance (Ruby 4.0) is green, and so are Lint and the Specs on 3.4 and 4.0. This PR adds the kit's caller workflow and makes the gem conform. That conformance work is tracked in #42. Ships as 0.5.0 (## [0.5.0] in the CHANGELOG).

  • Tier 1 (RuboCop): .rubocop.yml inherits conformance-kit: config/rubocop.yml, and the Gemfile adds rubocop-thread_safety >= 0.8. All 22 ThreadSafety/ClassInstanceVariable offences in lib/ are fixed, and nothing is baselined. Three inline disables remain, each with its reason:
    • lib/phlex_forms.rb: one disable-next on the module's class << self (configuration accessors). @configuration is built at load and replaced only on the main Ractor, at boot or between tests. It is shareable once finalized.
    • lib/forms/base.rb: one disable/enable pair around form_options and its two readers. form_options is a class macro that runs while the class body loads, and it stores shareable copies.
    • lib/forms/live.rb: one pair around the live/live_permit/live_deny/live_tags macros and their readers, for the same reason.
  • Tiers 1–2 (contention): spec/phlex_forms/contention_spec.rb runs the kit's thread and Async examples over Theme.resolve, with consistent: :identical.
  • Tier 3 (finalize at boot, automatic):
    • Under Rails, the engine calls PhlexForms.finalize! in config.after_initialize, after every initializer has run, with no app code.
    • Without Rails (no Rails::Engine when the gem loads), PhlexForms.configure finalizes when its block returns. A host that never calls configure calls PhlexForms.finalize! at boot (documented).
    • finalize! stores Ractor-shareable copies of every setting and then freezes the configuration. The caller's own arrays and hashes are copied, never frozen.
    • Finalizing never raises over a value it cannot share. That value is kept as given and works on the main Ractor only, for example a top-level initializer lambda (its self is main) or one that closes over a Logger.
    • New this round: an icon renderer that is not a Proc (an object with #call, a Method) is never copied or frozen. Before, make_shareable(copy: true) replaced a renderer that keeps a cache with a frozen copy, and that copy raised FrozenError on its first render.
    • configure on a finalized configuration edits a copy and swaps it in. A direct write after boot raises FrozenError. A direct write in a Rails initializer, before boot, still works.
    • Theme::DAISY/Theme::PLAIN are built once, when theme.rb loads, and every Theme is frozen. form_options and the live macros store shareable copies.
  • Ractor canary (Ruby 4.0): spec/phlex_forms/ractor_canary_spec.rb (:ractor) runs these inside a non-main Ractor: field option resolution and inference on a Struct model, theme resolution, the icon renderer, and the form_options/live settings. Rendering is a pending example with the Phlex reason.
  • Audit: audit.require: [./spec/conformance_eager_load.rb] eager-loads both Zeitwerk roots and calls finalize!. The audit runs with namespace: [PhlexForms, Forms] and strict_autoloads: true. There is no main_only and no skip_autoloads.
  • Bugs fixed along the way:
    • Without phlex-reactive, the first form raised NameError: Forms::TagField. phlex-reactive is meant to be a soft dependency; in practice it was a hard one.
    • Without Rails but with phlex-reactive bundled, the first form raised NoMethodError: Hash#to_json. Because finalizing builds the themes, configure now raised it too. The cause is phlex-reactive's Sortable, which does not require json. phlex-forms now requires json before phlex-reactive (spec/phlex_forms/plain_boot_spec.rb).
    • Review fixes: configure returns its block's value again. field_variants = nil is stored as [], so a later << is no longer lost.
  • bundle exec rake passes from the root. It used to abort on main. The root RuboCop now excludes docs/, a separate Rails app with its own bundle and .rubocop.yml. The Gemfile's one Bundler/OrderedGems offence is fixed.

Refs #42

Test plan

  • bundle exec conformance-kit check, Ruby 4.0.6, local, kit 0.4.0. It exits 0:
    • doctor: ok (conformance-kit 0.4.0, .conformance.yml)
    • bundle exec rspec --tag ractor: 6 examples, 0 failures, 1 pending (the render canary)
    • conformance-audit: 0 findings in PhlexForms, Forms (0 allowlisted main-only; 70 modules audited, 0 unloaded autoloads skipped)
    • conformance-kit check passed
  • conformance-audit --strict before the bump (0.3.0) and after it (0.4.0) gives the same result: 0 allowlisted main-only names, 0 ancestor: rules. The allowlisted set gained no name.
  • bundle exec rspec, Ruby 4.0.6: 241 examples, 0 failures, 2 pending (the render canary and the existing TimeZoneSelect skip).
  • bundle exec rspec, Ruby 3.4.11: 241 examples, 0 failures, 7 pending. The 6 Ractor examples are skipped with the kit's reason.
  • bundle exec rubocop lib spec (what CI lints) on 4.0.6 and 3.4.11: 99 files, no offences. bundle exec rake exits 0 on both Rubies.
  • New this round, each seen failing first:
    • spec/phlex_forms/rails_boot_spec.rb boots a minimal Rails app in a child process. It checks three things: the default configuration is frozen and shareable after boot with no app code; initializer writes (configure and a direct write) are kept; and a top-level lambda renderer that closes over a Logger boots and still renders. With the engine's after_initialize line removed, it failed 3 of 3 (frozen: false).
    • finalize_spec "keeps a renderer object that holds its own state". It failed before the fix: the renderer had been replaced by a frozen copy.
    • plain_boot_spec boots without Rails in a child process with json not preloaded. Before the fix it failed with NoMethodError: Hash#to_json from configure.
    • finalize_spec "returns the block's value" and "keeps field_variants appendable after it is set to nil": 2 failures before the fixes.
  • Round 1's checks were also seen failing first: the RuboCop offences, finalize_spec, the canary, the audit, the soft-dependency spec and the contention spec.
  • CI on e17c21f: Lint, Specs (3.4), Specs (4.0), Conformance settings, Conformance (Ruby 4.0) (handshake conformance-kit 0.4.0 satisfies the workflow's v0.4), Compiler probes and cubic all pass. 0 unresolved review threads.

Remaining from #42

  • Rendering inside a non-main Ractor. Phlex reads its non-shareable Phlex::ATTRIBUTE_CACHE constant on every element that has attributes. A per-Ractor cache is pending upstream in Give each Ractor its own attribute cache so components render on non-main Ractors yippee-fun/phlex#1055. The canary's render example is pending with that reason, so RSpec reports it once Phlex lands. Under the daisy theme, rendering also hits daisyui's class-level modifier maps (DaisyUI::Input.@modifiers). That part belongs to daisyui's own rollout.
  • Required status checks. conformance / Conformance settings and conformance / Conformance (Ruby 4.0) still need adding to the branch ruleset. That is for the maintainer: this PR changes no repo settings.

Deviations & judgment calls

Round 1:

  • No main_only for Forms::Base. Its class-body ivars are made shareable when written, with a commented inline disable. The audit never sees host subclasses, so an entry would be stale.
  • Themes are built when theme.rb loads, not inside finalize!. finalize! resolves the default theme, so they still load at boot. Theme::DAISY and Theme::PLAIN are new public constants.
  • The configuration stays open until it is finalized, per the issue's Decision. A direct write after boot raises FrozenError; configure keeps working.
  • One default icon renderer for every Ruby: a capture-free lambda made shareable in the class body. On 3.4, make_shareable rejects a Method.
  • Forms::Live.@_dependencies is frozen, not listed in main_only. ActiveSupport::Concern only reads it after the module body has loaded.
  • register_element → tag() in RichTextarea. Phlex 2.0.0, the gemspec floor, already has tag.
  • The contention spec checks Theme.resolve. The issue names Theme.component_for, which doesn't exist.
  • The canary covers more than field inference. It uses a Struct model, as the issue says.
  • New development-only piece: the async gem. Round 1 also added a require "json" to the eager-load file; round 2 moved it into the gem.
  • rubocop:disable-next needs RuboCop 1.92 or later. No lockfile is tracked, so CI resolves the latest RuboCop.
  • No ConformanceKit::Store or Mutex pattern was needed: no lazy state is left.
  • The docs-site Configuration page is not updated. Only the README and CHANGELOG are, because a docs-page change needs screenshots.

Round 2:

  • Auto-finalize was already wired in round 1 (the engine's after_initialize, plus the end of configure without Rails). This round adds the real-boot proof (rails_boot_spec) instead of only the stub_const spec. It runs in a child process, so the engine never loads into the suite. Without the engine, configure finalizes by itself.
  • railties ~> 8.0 added to the development group. The boot spec requires it. It was already in the bundle through phlex-reactive, so the resolution is unchanged.
  • Non-Proc icon renderers are kept as given (PhlexForms::Shareable.callable). The other choice was to share them in place with make_shareable. That would freeze the host's own object and break it the same way.
  • The "outside Rails" boot point is the end of configure, not require. Finalizing at require would load every component at require time (round 1 hit phlex-reactive's missing require "json" doing that). It would also freeze a configuration the host has not configured yet. A host that never calls configure, or loads Rails without booting an app, calls PhlexForms.finalize! (README).
  • No main_only entries to convert to ancestor: rules. There were none before (0 allowlisted), and there are none after. The audit's scope is unchanged: booting the engine into it would have added Rails::Engine class state to allowlist.
  • Dependabot is unchanged. No Gemfile besides the root pins conformance-kit (docs/Gemfile doesn't), and the root Gemfile.lock is not tracked, so there was no lockfile to update.
  • CHANGELOG: this repo never had version headings, and released 0.4.x entries still sat under [Unreleased]. This PR's entries move to a new ## [0.5.0] section, which opens with an "Upgrading to 0.5" list. The released pile below it is headed ## [0.4.1] and earlier; otherwise it is untouched.
  • bundle exec rake: docs/ is excluded from the root RuboCop (comment in .rubocop.yml). The Gemfile is re-sorted: phlex-reactive's block moved below gem-release.
  • Cubic findings:
    • The one-sided Dependabot group PR is intended behaviour. Dependabot cannot wait for both ecosystems; the version handshake fails a one-sided PR, as conformance-kit's README documents.
    • "patterns is invalid at the update-entry level" is a false positive. GitHub's multi-ecosystem docs require it there.
    • Three findings are fixed: the CHANGELOG/README wording on Ractor shareability, configure's return value, and nil field_variants. For the last one, the next configure did not actually fail (nil.dup is nil), but a << after nil was lost.
  • The json require lives in the gem. The eager-load file's workaround was dropped. phlex-reactive's own fix can't be relied on: it is unreleased.

https://claude.ai/code/session_01SXwrW19u1qWNW3fm2pUnrU

Adds the caller, bumps conformance-kit to ~> 0.3 and lets Dependabot
keep both pins current. The conformance check is not required yet: it
turns green, and becomes required, with #42.

Refs #42

Claude-Session: https://claude.ai/code/session_01SXwrW19u1qWNW3fm2pUnrU
@mhenrixon mhenrixon added chore CI, tooling or configuration work with no behaviour change devops CI, the release kit, Docker, deploys and infrastructure labels Oct 15, 2026
@gitar-bot

gitar-bot Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Gitar is working

Verification

Rules No rules evaluated

Functional validation Not enabled

Set up

Auto-approval Not enabled

Set up

Gitar

cubic-dev-ai[bot]
cubic-dev-ai Bot previously approved these changes Oct 15, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 3 files

View guided diff | Turn on auto-fix | Re-trigger cubic

A tag bump without the gem (or the reverse) fails the conformance version
check, so both now arrive in a single multi-ecosystem pull request.

Claude-Session: https://claude.ai/code/session_01SXwrW19u1qWNW3fm2pUnrU

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 1 file (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread .github/dependabot.yml Resolved
@cubic-dev-ai
cubic-dev-ai Bot dismissed their stale review October 15, 2026 15:31

Dismissed because Cubic found issues in a newer review.

@mhenrixon
mhenrixon marked this pull request as draft October 15, 2026 15:33
Adds rubocop-thread_safety (>= 0.8, what conformance-kit doctor checks)
and inherits conformance-kit's config/rubocop.yml, so the Tier 1
ThreadSafety cops run in the Lint job. The offences it reports in lib/
are fixed in the commits that follow, not baselined.

Refs #42

Claude-Session: https://claude.ai/code/session_01SXwrW19u1qWNW3fm2pUnrU
Theme.daisy / Theme.plain named Forms::TagField and its rootless twin as
arguments to reactive_roles, so the classes were resolved before the
defined?(Phlex::Reactive) guard ran. Without phlex-reactive those files
are ignored by the loader, and the first theme lookup raised NameError:
the soft dependency was in fact hard. reactive_roles now takes the
namespace and names the classes only behind the guard.

The spec boots the gem in a child process with phlex-reactive off the
load path.

Refs #42

Claude-Session: https://claude.ai/code/session_01SXwrW19u1qWNW3fm2pUnrU
The configuration and both built-in themes lived in lazy class ivars
(`@configuration ||=`, `@daisy ||=`, `@plain ||=`, plus lazy defaults in
Configuration's readers): check-then-set races, and unreadable from a
non-main Ractor.

- Theme.daisy / Theme.plain are the DAISY / PLAIN constants, built once
  when theme.rb loads; a Theme is frozen on construction, so a theme of
  component classes is Ractor-shareable.
- Configuration sets every default in initialize; no reader memoizes.
  The default icon renderer is a capture-free lambda made shareable in
  the class body, and glyphs_renderer returns a shareable one too.
- PhlexForms.finalize! stores Ractor-shareable copies of every setting
  (PhlexForms::Shareable.copy, which never freezes the caller's objects)
  and freezes the configuration. configure calls it when the Rails
  engine is absent; the engine calls it after_initialize. configure on a
  finalized configuration edits a copy and swaps it in whole.
- A renderer that cannot be shared (a top-level lambda in an
  initializer) is kept as given and still works on the main Ractor.
- The configuration is built when the gem loads, never lazily; one
  inline ThreadSafety/ClassInstanceVariable disable covers the module's
  configuration accessors, with the reason beside it.

Refs #42

Claude-Session: https://claude.ai/code/session_01SXwrW19u1qWNW3fm2pUnrU
conformance-kit's "thread-safe under contention" and "fiber-safe under
an Async reactor" examples over Theme.resolve, the lookup every field
makes, require one Theme object across 8 threads/fibers x 50 calls. On
the lazy `@daisy ||=` themes the thread example saw 8 distinct Theme
objects in 2 of 3 runs; on the load-time constants it cannot race.

Adds async (development only, require: false) so the fiber example runs
instead of skipping.

Refs #42

Claude-Session: https://claude.ai/code/session_01SXwrW19u1qWNW3fm2pUnrU
form_options kept its arguments as-is in the form class's ivars, so a
non-main Ractor reading form_modifiers / form_defaults raised
Ractor::IsolationError. It now stores PhlexForms::Shareable.copy of
both (deep-frozen copies; the caller's objects stay unfrozen, and a
value that cannot be shared, such as a lambda with an unshareable self,
is kept as given for the main Ractor). The readers already build new
values per call, so nothing else changes.

The ThreadSafety/ClassInstanceVariable disable around the macro and its
two readers carries the reason: written while the class body loads, at
boot on the main Ractor, and shareable once written.

Refs #42

Claude-Session: https://claude.ai/code/session_01SXwrW19u1qWNW3fm2pUnrU
live (setup_live), live_permit, live_deny and live_tags stored mutable
strings, arrays and a hash in the form class's ivars, unreadable from a
non-main Ractor. They now store PhlexForms::Shareable.copy of each; the
model class and the debounce were shareable already.

conformance-audit also flagged Forms::Live.@_dependencies, the Array
ActiveSupport::Concern keeps of the concerns Forms::Live includes. It is
filled while the module body loads and only read after (when a form
class includes Forms::Live), so it is frozen at the end of the body.

Refs #42

Claude-Session: https://claude.ai/code/session_01SXwrW19u1qWNW3fm2pUnrU
register_element :lexxy_editor left Forms::RichTextarea with a mutable
per-class element registry (@__registered_elements__, Phlex's own),
which conformance-audit reports as unshareable class state. Phlex's
tag(:lexxy_editor, ...) (Phlex >= 2.0.0, the gemspec floor) renders the
same custom element, attributes and block, with no registry on the
class. The rich-textarea specs are unchanged and green.

Refs #42

Claude-Session: https://claude.ai/code/session_01SXwrW19u1qWNW3fm2pUnrU
conformance-kit's "runs inside a non-main Ractor" (tagged :ractor, Ruby
4.0; skipped with the kit's reason below) runs, with the configuration
finalized: Forms::Field option resolution and PhlexForms::Inference on a
literal Struct model, theme resolution and the icon renderer, and the
form_options / live class settings of constant form classes. Before the
two previous fixes the class-settings examples raised
Ractor::IsolationError on @form_modifiers and @live_scope.

Rendering is a pending example with its reason: Phlex reads its
non-shareable Phlex::ATTRIBUTE_CACHE on every element with attributes
(yippee-fun/phlex#1055). It fails today; once upstream lands the pending
example passes and RSpec reports it, so it cannot go stale silently.

Refs #42

Claude-Session: https://claude.ai/code/session_01SXwrW19u1qWNW3fm2pUnrU
audit.require points at spec/conformance_eager_load.rb, which
eager-loads both Zeitwerk roots (PhlexForms, Forms) and finalizes the
configuration as a host's boot does; audit.strict_autoloads makes the
strict audit fail on any autoload left unloaded. No main_only and no
skip_autoloads entries: nothing is left that needs one. The Ractor gate
runs the :ractor examples; probes.gem_name enables the report-only
compiler probes.

`bundle exec conformance-kit check` on Ruby 4.0.6: doctor ok, 6 :ractor
examples (0 failures, 1 pending: rendering, yippee-fun/phlex#1055), and
conformance-audit: 0 findings, 70 modules audited, 0 unloaded autoloads.

Refs #42

Claude-Session: https://claude.ai/code/session_01SXwrW19u1qWNW3fm2pUnrU
README: a "Configuration is finalized at boot" section (when it
finalizes, writing through configure afterwards, renderers that cannot
be shared). CHANGELOG [Unreleased]: PhlexForms.finalize! and the
shareable themes and class macros (Added), the frozen configuration and
the renderer rule (Changed), and the theme fix without phlex-reactive
(Fixed).

Refs #42

Claude-Session: https://claude.ai/code/session_01SXwrW19u1qWNW3fm2pUnrU
@mhenrixon mhenrixon added enhancement New feature or request theme Themes and the Plain (unstyled) components core Configuration, the Rails engine and soft-dependency wiring and removed chore CI, tooling or configuration work with no behaviour change labels Oct 15, 2026
@mhenrixon mhenrixon changed the title chore(ci): call conformance-kit's conformance workflow at v0.3.0 feat(core): concurrency conformance via conformance-kit v0.3.0 Oct 15, 2026
Moves both pins together: the caller's tag and the Gemfile's ~> 0.4, so the
version handshake matches. No tracked lockfile pins the kit (the root
Gemfile.lock is ignored, docs/ does not bundle it).

Refs #42

Claude-Session: https://claude.ai/code/session_01SXwrW19u1qWNW3fm2pUnrU
Boots a minimal Rails app in a child process (so the engine never loads into
the suite) and checks the configuration is frozen and shareable after
after_initialize with no app code, that initializer writes (configure and a
direct write) are kept, and that an icon renderer which cannot be shared (a
top-level lambda closing over a Logger) boots and still renders. Seen failing
with the engine's after_initialize removed.

Refs #42

Claude-Session: https://claude.ai/code/session_01SXwrW19u1qWNW3fm2pUnrU
Shareable.copy deep-copies and freezes any object it can, so an icon renderer
object with state of its own (a cache) was swapped for a frozen copy that
raised FrozenError on its first render. A renderer that is not a Proc is now
kept as given: shared only if it already is.

Refs #42

Claude-Session: https://claude.ai/code/session_01SXwrW19u1qWNW3fm2pUnrU
The root RuboCop walked docs/, whose .rubocop.yml requires docs_kit/rubocop
from the docs app's own bundle, so `rake` (spec + rubocop) aborted on main.
docs/ is excluded from the root lint, and the one remaining offence
(Bundler/OrderedGems in the Gemfile) is fixed by moving phlex-reactive into
order.

Claude-Session: https://claude.ai/code/session_01SXwrW19u1qWNW3fm2pUnrU
Moves this PR's entries out of the [Unreleased] pile into a [0.5.0] section
with an "Upgrading to 0.5" list (auto-finalize, FrozenError on a direct write
after boot, unshareable values kept as given), and heads the released pile
"[0.4.1] and earlier". The README says when finalizing happens with and
without Rails, and that non-Proc renderers are never copied or frozen.

Refs #42

Claude-Session: https://claude.ai/code/session_01SXwrW19u1qWNW3fm2pUnrU
@mhenrixon mhenrixon changed the title feat(core): concurrency conformance via conformance-kit v0.3.0 feat(core): concurrency conformance via conformance-kit v0.4.0 Oct 15, 2026
@mhenrixon
mhenrixon marked this pull request as ready for review October 15, 2026 20:07

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 23 files

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread .github/dependabot.yml Resolved
Comment thread CHANGELOG.md Resolved
Comment thread lib/phlex_forms.rb Resolved
Comment thread lib/phlex_forms/configuration.rb Resolved
Finalizing builds the themes, which load the tag-field leaves and with them
phlex-reactive's Sortable, which calls Hash#to_json while it loads without
requiring json. A Rails host has json loaded; a plain Ruby host got
NoMethodError from PhlexForms.configure (before this PR, from its first
form). phlex-forms now requires json before phlex-reactive, and the
eager-load file's workaround goes.

Refs #42

Claude-Session: https://claude.ai/code/session_01SXwrW19u1qWNW3fm2pUnrU
Before finalizing, configure was `yield(configuration)` and returned what the
block returned; the finalizing versions returned the configuration or nil.

Refs #42

Claude-Session: https://claude.ai/code/session_01SXwrW19u1qWNW3fm2pUnrU
The reader stopped memoizing `@field_variants ||= []`, so after
`c.field_variants = nil` a later `c.field_variants << :sm` appended to a
throwaway array. The writer now stores nil as an empty list.

Refs #42

Claude-Session: https://claude.ai/code/session_01SXwrW19u1qWNW3fm2pUnrU
The changelog and README promised a Ractor-shareable configuration even when
an unshareable value (a top-level renderer lambda) is kept as given. Also adds
the json fix to the 0.5.0 Fixed list.

Refs #42

Claude-Session: https://claude.ai/code/session_01SXwrW19u1qWNW3fm2pUnrU

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 7 files (changes from recent commits).

View guided diff | Turn on auto-fix | Re-trigger cubic

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 1 file (changes from recent commits).

View guided diff | Turn on auto-fix | Re-trigger cubic

@mhenrixon
mhenrixon merged commit 43ea0b7 into main Oct 15, 2026
8 checks passed
@mhenrixon
mhenrixon deleted the chore/conformance-caller branch October 15, 2026 20:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core Configuration, the Rails engine and soft-dependency wiring devops CI, the release kit, Docker, deploys and infrastructure enhancement New feature or request theme Themes and the Plain (unstyled) components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant