Skip to content

fix(actionagent): install and mount cleanly on MySQL and acronym hosts - #359

Merged
TonsOfFun merged 3 commits into
mainfrom
fix/mysql-json-defaults-and-api-inflection
Aug 14, 2026
Merged

fix(actionagent): install and mount cleanly on MySQL and acronym hosts#359
TonsOfFun merged 3 commits into
mainfrom
fix/mysql-json-defaults-and-api-inflection

Conversation

@TonsOfFun

@TonsOfFun TonsOfFun commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Two things stopped a plain mount ActionAgent::Engine from working outside a PostgreSQL host with default inflections. Both were found installing the engine into a Rails 8 app on MySQL 8 that declares inflect.acronym "API", where each was fatal: the first aborted db:migrate, the second made every request to the mount raise.

Releases as 1.2.1. (The CHANGELOG still described 1.2.0 as Unreleased although it is on RubyGems; that heading is corrected here too.)

Migrations no longer set a default on JSON columns outside PostgreSQL

Both templates already chose the column type per adapter — jsonb on PostgreSQL, json elsewhere — but kept default: [] / default: {} for every adapter. MySQL rejects a default on a JSON column outright:

Mysql2::Error: BLOB, TEXT, GEOMETRY or JSON column 'spans' can't have a default value

That aborts create_table, so rails g action_agent:install && rails db:migrate could not complete on any MySQL host: 3 columns in the traces migration, 31 in the dashboard one.

The paired null: false is dropped along with the default on those adapters. Nothing in the models assigns these attributes before validation, so a NOT NULL column with no default would reject the very inserts the default existed to satisfy. Every JSON column is already read through Array(...) / || {}, so a NULL reads as the empty value.

The api namespace resolves under either spelling of the constant

An engine's files are autoloaded by the host's rails.main loader, under the host's inflections. A host declaring the API acronym made Zeitwerk expect ActionAgent::API::TracesController from a file defining ActionAgent::Api::TracesController:

Zeitwerk::NameError: expected file .../api/traces_controller.rb to define constant ActionAgent::API::TracesController

Rails separately resolves a route's controller by camelizing the stored path ("action_agent/api/traces") with the host's global inflections, which no engine-level setting scopes — so fixing the autoloader alone still left the router raising NameError: uninitialized constant ActionAgent::API.

No single module name fixes both kinds of host: a plain host camelizes api to Api, an acronym host to API. So the autoloader is pinned to Api for this engine's own path only — scoped by path rather than through inflect, since the loader is shared and a blanket rule would re-spell the host's own API constants — and the namespace answers to API as well, via const_missing so the controllers stay lazily autoloaded.

Verification

Installed into a Rails 8.1 / MySQL 8 app that declares the API acronym. Before: db:migrate aborted on the first JSON column. After: migrations complete, POST <mount>/api/traces returns 401 without a bearer token and 202 with one, and the dashboard renders. The host app needs no inflection workaround — confirmed by removing the workarounds it previously required and re-running.

Note that bundle exec rake test does not run on this checkout, on this branch or on a clean main: the dummy app fails to boot with cannot load such file -- active_storage/engine. That is pre-existing and unrelated, but it means CI is the first real run of the suite against these changes.

Also: the release workflow skips an already-published version

Releasing this exposed a problem in release.yml. Both gems in this repo share one release tag but version independently, so a release that patches only one of them rebuilds the other at its current version — here, activeagent stays at the 1.2.0 already on RubyGems. gem push rejects that duplicate with a non-zero status, and because both pushes sit in one shell block, the rejection would take down the actionagent push that follows it.

The publish step now checks RubyGems for the built version and skips it if present, so a single-gem patch release publishes the gem that changed and leaves the other alone. 1.2.1 is the first release to hit this.

Two things stopped a plain `mount ActionAgent::Engine` from working outside
a PostgreSQL host with default inflections. Both were found installing the
engine into a Rails 8 app on MySQL 8 that declares `inflect.acronym "API"`.

Migrations no longer set a default on JSON columns outside PostgreSQL.
Both templates already chose the column type per adapter — jsonb on
PostgreSQL, json elsewhere — but kept `default: []` / `default: {}` for
every adapter, and MySQL rejects a default on a JSON column outright
("BLOB, TEXT, GEOMETRY or JSON column can't have a default value"). That
aborts create_table, so `rails g action_agent:install && rails db:migrate`
could not complete: 3 columns in the traces migration, 31 in the dashboard
one. The paired `null: false` goes with the default, since nothing assigns
those attributes before validation and a NOT NULL column with no default
would reject the inserts the default existed to satisfy. Every JSON column
is read through `Array(...)` / `|| {}`, so a NULL reads as the empty value.

The api namespace now resolves under either spelling of the constant. An
engine's files are autoloaded by the host's `rails.main` loader under the
host's inflections, so a host that declares the API acronym made Zeitwerk
expect ActionAgent::API::TracesController from a file defining
ActionAgent::Api::TracesController, and every request to the mount raised
Zeitwerk::NameError. Rails also resolves a route's controller by camelizing
the stored path with the host's global inflections, which no engine-level
setting scopes, so fixing the autoloader alone still left the router
raising NameError. No single module name fixes both kinds of host — a plain
host camelizes api to Api, an acronym host to API — so the autoloader is
pinned to Api for this engine's own path only, and the namespace answers to
API as well.
The two gems in this repo share one release tag but version
independently, so a release that patches only one of them rebuilds the
other at its current version. `gem push` rejects that duplicate with a
non-zero status, and because both pushes are one shell block, the
rejection takes down the gem that actually changed.

1.2.1 is the first release to hit this: it patches actionagent alone,
leaving activeagent at the 1.2.0 already published.
@TonsOfFun
TonsOfFun marked this pull request as ready for review August 14, 2026 21:00
@TonsOfFun
TonsOfFun merged commit 27d141b into main Aug 14, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant