Changes and upgrade notes

v0.1.16

This page lists the releases of Mishka Installer in the 0.1 line, newest first, up to 0.1.16. Each entry says what changed and what to do when you upgrade past it. A change that can break code you already run is marked Breaking.

The entries follow the CHANGELOG file of the project, and each breaking change was checked against the code of its release. The releases 0.1.2 to 0.1.5 and 0.1.7 have no CHANGELOG entry, so their entries come from the code. The 0.0 releases came before the rewrite and are not covered.

How to upgrade

  1. Find the release you run. Application.spec(:mishka_installer, :vsn) returns it as a charlist, such as ~c"0.1.14".
  2. Read each entry between that release and the one you want. Do what its "When you upgrade" list says.
  3. Run mix deps.update mishka_installer, then your tests.
  4. Restart the node. The library rebuilds its events on boot.

Only the entry for 0.1.6 changes what the library stores in Mnesia.

Breaking changes at a glance

ReleaseWhat breaksWhat to do
0.1.16A plugin that is only :registered is no longer in its event's chain until it starts. return: false answers the result of the plugins, not the input.Call start/0 after register/0 where you rebuild an event without starting the plugin. Pass return: true to keep the input.
0.1.15A :url install takes only an https address, and a download follows a redirect only to a host it may use. A probe that raises answers {:error, {:exit, ...}}.Use https addresses and list every host a download redirects to in url_hosts. Match :exit where you matched :raised.
0.1.14The installer refuses a version that is not a semantic version.Use MAJOR.MINOR.PATCH versions. Delete the extra records of one application.
0.1.13An event that cannot run answers your data, not an error tuple.Remove the code that matches the error. Pass cannot_run: where the answer must refuse.
0.1.12req must be ~> 0.7, and the lowest versions of phoenix_pubsub, plug and guarded_struct went up.Raise those requirements in your own mix.exs.
0.1.11restart on a stopped plugin is an error. Registering a plugin again updates its row. Plugins that depend on another change status on their own.Use enable to switch a stopped plugin on.
0.1.6The installer takes only pre-built packages, remote installs are closed until you allow them, the Installer table has new columns, and the helper modules moved.Follow every step of the entry.
0.1.3Hook.call/3 answers your data, not an error, for an event that has no module.Remove the code that matches {:error, :undefined_function_error}.
0.1.2Elixir 1.17 or later is required.Upgrade Elixir.

0.1.16

Released on 2026-11-04. A plugin that raises can make the call answer a value you choose, a group of plugins can run under its own supervisor, and the chain of an event holds only plugins that started.

When you upgrade

  • A plugin process starts itself after it registers, and the boot starts every plugin that is not stopped. Code that registers a plugin and rebuilds its event without starting it must call start/0, or Event.start/3, after register/0.
  • Change return: false to return: true where the call must answer its input. A value other than true, false or nil raises an ArgumentError that names the option.
  • Event.plugin_status/1 is gone.
  • Hook.call/3 and call!/3 raise an ArgumentError that names the argument when args is not a list, before any plugin runs.
  • A private that is not a map or a keyword list raises an ArgumentError in the caller. It was logged as a plugin crash before. A keyword list now merges into a map result.

Added

  • Hook.call/3 and call!/3 take raised:, the answer when a plugin of the event raises or replies with something other than {:reply, _} or {:reply, :halt, _}. The plugins after it do not run. Without it the call answers the state it was given, as before. The log line names the event and carries the stacktrace, so it shows the plugin that raised.
  • MishkaInstaller.Event.PluginSupervisor runs a list of plugins as one :one_for_one group in your own tree, with its own :max_restarts and :max_seconds. A burst of plugin deaths stops only the group, and its parent starts it again.
  • Transaction.dirty/1 and the dirty_* functions of Query read a table without a lock.

Changed

  • Hook.call/3 runs a compiled event with one :persistent_term read and no export check. ModuleStateCompiler.event_module/1 finds a module only while it is loaded, and purge/1 takes the event out of it before it deletes the code.
  • An event's module runs its chain as one private function for each plugin, so building it takes time linear in the chain. The release notes measured 99 plugins at 2.5 s before and 0.22 s now.
  • The registry reads Event.get/0, get/1, get/2, group_events/1 and Installer.get/0, get/1, get/2 are dirty reads: an ETS lookup on the node's copy of the table, with no lock. They never wait for a status write, and they answer [], nil or the error tuple while Mnesia is stopped. Event.dirty_get/2 is the same read as get(:name, _).
  • The log line of a plugin that raised is a plugin of event ... raised: followed by the stacktrace. It was plugin pipeline raised in event ....

Fixed

  • Rebuilding an event no longer kills a caller that still runs an earlier version, such as a request waiting inside a slow plugin. The new version is loaded into the event's own module name only when no process runs the old one, and otherwise into the next free slot. A rebuild drops every slot that no process runs any more.
  • Building an event prints no the following clause will never match warning for a plugin that always replies {:reply, state}.
  • A plugin process no longer crashes once a second while Mnesia is stopped. Its status poll copies the status from its event's compiled chain and reads no table.

The call an event documentation shows raised:, and the write a plugin documentation shows the plugin supervisor.

0.1.15

Released on 2026-10-30. The installer checks where a download comes from, where a path points and what a record names, and a download that fails returns an error.

When you upgrade

  • Install from https addresses. http://, file:// and an address without a host are refused before a request is made.
  • The installer follows a redirect itself, at most 3 times. Each hop must be https, and its host must be the host of the first address, a host in url_hosts, or, for an address on github.com or api.github.com, GitHub's release CDN (release-assets.githubusercontent.com or objects.githubusercontent.com). If a download relied on another host, add that host to url_hosts.
  • The options in :downloader_req_options go with every hop, because the installer applies them to each request it makes.
  • A download that cannot be made is {:error, [%{action: :download}]}. Code that rescued Req.TransportError around install/1 can match the error instead.
  • Hook.run_health/2 answers {:error, {:exit, {exception, stacktrace}}} for a probe that raises, where it answered {:error, {:raised, exception}}. Match :exit.

Added

  • config :mishka_installer, :allowlist, require_checksum: true refuses a remote install that has no checksum. Without it, such an install logs a warning.
  • MishkaInstaller.Helper.Extra.isolate/1 runs a function in a process that ends with its caller and answers {:ok, result} or {:exit, reason}.
  • MishkaInstaller.Helper.Record holds the Mnesia record functions that Event and Installer share.
  • ModuleStateCompiler.event_module/1 returns the module compiled for an event, or nil.

Security

  • A :path install must be below the extensions directory. The directory itself and a sibling such as extensions-old were accepted before.
  • An uninstall takes "<app>-<version>" as one path segment, so a version such as "0.1.0/../other-2.0.0" cannot point it at another package.
  • An application name ends at the end of the string. "foo\n" matched before. Installer.write/1 and write/3 refuse a record whose app is not a valid name.
  • The boot replay builds the ebin path from the extensions directory and the app and version of the record, after the name and protected-app checks of install/1. The prepend_paths of a record are no longer put on the code path.
  • Hook.call/3, call!/3, profile/2 and event_health/2 no longer create an atom or a :persistent_term key for an event name.
  • A GitHub install checks path ("owner/repo"), tag and asset before they reach a URL, and the asset address the API answers with must be an https address on github.com.
  • ModuleStateCompiler.create/3 no longer hands the compiler's error text to its caller.

Fixed

  • An upgrade keeps the inserted_at of the record. Only updated_at moves.
  • Installer.install/1 no longer changes the working directory of the node, and no longer fails when no project path is set but :extensions_path is.
  • A module of an event that cannot be built is {:error, [%{action: :compile}]}. It is built in an isolated process, and the event handler marks the :compile telemetry event :error for it.
  • LibraryHandler.read_app/2 answers an error for an .app file that is not valid UTF-8, and extract/3 takes a single folder name.
  • Installer.write/3 reads the record under a lock inside the transaction, as Event.write/3 does, so two writers cannot lose one another's fields.
  • Installer.unique/2 says "library", not "event".

0.1.14

Released on 2026-10-17. The installer checks versions as semantic versions and keeps one record for each application.

When you upgrade

  • Give every library you install a semantic version. 0.1.7, 1.0.0-rc.1 and 1.0.0+build.5 pass. 1.0 and v1.0.0 do not.
  • Records that are already stored are left as they are. A stored record with the version 1.0 is read back unchanged.
  • If you upgraded libraries with 0.1.13 or earlier, the registry can hold several records for one application. List them with Installer.get/0 and keep the newest record of each application:
elixir
alias MishkaInstaller.Installer.Installer

Installer.get()
|> Enum.group_by(& &1.app)
|> Enum.each(fn {_app, records} ->
  [_newest | older] = Enum.sort_by(records, & &1.inserted_at, :desc)
  Enum.each(older, &Installer.delete(:id, &1.id))
end)

Fixed

  • An install no longer raises Version.InvalidVersionError for a version such as "1.0" when an application of that name is loaded. With none loaded, such a version was installed as it was. A version that is not semantic is now refused with action: :semver. A loaded application whose own version is not semantic is refused with action: :compare_version_with_installed_app and field: :app. If the installer installed that copy, uninstall it first, then install again.
  • An upgrade updates the record of the application instead of writing a second one. Before, each upgrade left another record, so Installer.get(:app, app) could answer either one and Installer.uninstall/1 removed only one.

The installer reference documentation lists the error shapes, and the install a library documentation shows how to fix each.

0.1.13

Released on 2026-10-12. An event whose plugins cannot run hands your data back, and plugins whose code is gone are cleaned up on boot.

When you upgrade

  • Delete the code that matches that error. Every caller had to tell it apart from the answer of the plugins before it could go on without them.
  • Pass cannot_run: where "no plugin ran" must not look like "no plugin objected", such as a gate that decides access:
elixir
# 0.1.12 and earlier
case Hook.call("auth:gate:check", gate) do
  {:error, [%{action: :call, field: :event}]} -> %{gate | allowed?: false}
  answer -> answer
end

# 0.1.13 and later
Hook.call("auth:gate:check", gate, cannot_run: %{gate | allowed?: false})

An event whose plugins all run ignores the option. The call an event documentation explains it in full.

Changed on boot

After the run-time libraries are loaded and before the events start, the library unregisters every plugin whose application is loaded, no longer lists the plugin's module, and whose module is not loaded either. It logs a warning for each one and emits the [:mishka_installer, :event, :unregistered_gone] telemetry event, with :plugin, :event and :extension. Whatever depended on that plugin is held, as Event.unregister/3 holds it. The same pass runs each time the events start, so it also runs when the store announces that it is ready again.

Before, a plugin that a newer version of its library removed or renamed stayed registered. Its event then ran none of its plugins until you unregistered the record by hand. A plugin whose application is not loaded is left as it was, because that library may still be on its way in. Compiling such an event now logs an error that names the plugins it waits for, once for each compile.

You change nothing. The plugins in installed libraries documentation covers what an upgrade or an uninstall leaves behind for a library that ships plugins.

Fixed

  • An event whose state is not a list, a map, {:ok, _} or {:error, _} comes back as its plugins left it. This covers a LiveView answer such as {:noreply, socket} or {:ok, socket, layout: ...}, and a binary. Before, every call raised inside the chain, logged plugin pipeline raised and answered the state it was given, so what the plugins returned was thrown away.

Added

  • The cannot_run: option of Hook.call/3 and Hook.call!/3.
  • Event.unregister_gone/1.
  • The [:mishka_installer, :event, :unregistered_gone] telemetry event. See the telemetry documentation.

0.1.12

Released on 2026-08-28. The dependency requirements moved up.

When you upgrade

  • If your own mix.exs holds req, phoenix_pubsub, plug or guarded_struct below those versions, raise it. Then run mix deps.update mishka_installer req.
  • The library uses only Req.request!/1 and a %Req.Response{} match, and nothing it uses changed in Req 0.7. If you set :downloader_req_options or :proxy, those options go to Req as they are, so check them against Req 0.7.

Fixed

  • Two advisories in the dependency tree: a denial of service in hpax, fixed in 1.0.4, and issues in html_sanitize_ex, fixed in 1.5.5. Both are fixed only by updating. Run mix deps.update hpax html_sanitize_ex, then mix hex.audit.

0.1.11

Released on 2026-08-03. Plugins that depend on each other change status together, and status changes survive a crash.

When you upgrade

  • Switch a stopped plugin on with Event.enable/2, or enable/0 on the plugin. It is the only way out of :stopped. The error for restart reads "This plugin is stopped, use `enable/2` to switch it back on." and has action: :plugin_status.
  • The fields you declare in code, :priority, :extra and :depends, are applied again on every boot. A value you changed in the registry with Event.write/3 is replaced by the value in code. The stored :id, :status and :inserted_at are kept.
  • If you write statuses yourself with Event.write/3, call Event.sync_dependents/2 afterwards.

Added

  • Event.enable/2, and enable/0 on a plugin. A plugin can override it like the other generated functions.
  • Event.dependents/1 and Event.sync_dependents/2. Nothing keeps a list of dependents: the library reads them from the stored depends.

Fixed

  • A status change is durable. Lifecycle writes use Transaction.durable_transaction/1, which commits synchronously and flushes the log. See the helper modules documentation. Before, a plugin that an operator disabled came back enabled when the node died within about three minutes.
  • A plugin that leaves an event, because it is stopped, held or unregistered, rebuilds the chain at once. Before, it ran until the next boot.
  • Event.restart/3 checks every condition before it writes, so a failed restart leaves no status behind.
  • Event.stop/3 accepts a :held plugin, so an operator can switch off a plugin that a missing dependency blocks.
  • A disabled plugin is never brought back by one of its dependencies starting.
  • A plugin held at boot because a dependency on another event had not started is released when that dependency starts. Before, it stayed dark until the next restart.

The plugin statuses documentation explains the five statuses, and the manage plugins documentation shows enable in use.

0.1.10

Released on 2026-07-03.

Fixed

  • A registration that fails at boot is logged as an error. Before, only a :register_error message was broadcast, and nothing listens to it by default, so a plugin with a priority outside 0 to 100 was rejected without a trace and sat with no record.

When you upgrade

  • After the upgrade, look in the log for failed to register lines. Each names the plugin, the event and the reason.

0.1.9

Released on 2026-07-03.

Fixed

  • A plugin that is stuck in :registered is started again at boot. That status means its very first start never finished, for example because the boot was interrupted between the two Mnesia writes. A :stopped or :held record is still respected, so the switch of an operator survives restarts.

When you upgrade

  • Nothing to change. Plugins that were stuck start on the next boot.

0.1.8

Released on 2026-06-24.

Added

  • The meta: option of Hook.call/3. It hands read-only context to the plugins that define call/2. A plugin that defines call/1 runs as before, and use MishkaInstaller.Event.Hook accepts a plugin that defines either. See the call an event documentation and the write a plugin documentation.

When you upgrade

  • Nothing to change.

0.1.7

Released on 2026-06-20. This release has no CHANGELOG entry. The code did not change, only its documentation text. The guarded_struct requirement moved from ~> 0.1.0-beta.8 to ~> 0.1.0.

When you upgrade

  • Nothing to change.

0.1.6

Released on 2026-06-20. The installer was rebuilt to load pre-built packages, the Mnesia store can form a cluster, and the helper modules moved into the library.

Breaking

  1. The installer takes only pre-built packages. It never runs mix or compiles. A package is an ebin folder with an <app>.app file. The release removed the compile code, the Installer.Collect module and Installer.uninstall/2. 0.1.5 accepted the type values :hex, :github, :github_latest_release, :github_latest_tag, :url and :extracted. 0.1.6 accepts :path, which is the default, :url, :github_tag and :github_latest_release. The fields release, branch, custom_command, dependency_type and compile_type are gone, and asset and checksum are new. What to do: build the ebin package yourself and change the maps you give to Installer. See the install a library documentation.
  2. Remote installs are closed until you allow them. 0.1.5 had no allowlist. Now a :url install needs the host in url_hosts, and a :github_tag or :github_latest_release install needs the repository in github_repos. An empty or missing list blocks them. A :path install is not affected. The library also refuses to install over, or to uninstall, an application in protected_apps, which holds mishka_installer by default. What to do: set config :mishka_installer, :allowlist. See the configuration documentation.
  3. The stored Installer table has new columns. The library creates a table that is missing and never changes one that exists. A node that stored installs with 0.1.5 or earlier keeps the old table. Installer.get/0 then returns [], Installer.get(:app, app) returns a record with its fields in the wrong places, and Installer.write/1 fails with {:aborted, {:bad_type, ...}} and action: :database. The plugin table, Event, has the same fields as before. What to do: delete the old table once, restart the node, and install your libraries again. The library creates the table with the new columns on boot:
    elixir
    :mnesia.delete_table(MishkaInstaller.Installer.Installer)
  4. The helper modules moved. The library used MnesiaAssistant and MishkaDeveloperTools.Helper.Extra and UUID from the mishka_developer_tools package. It now has its own MishkaInstaller.Helper.MnesiaAssistant, MishkaInstaller.Helper.Extra and MishkaInstaller.Helper.UUID, and it no longer depends on mishka_developer_tools. What to do: if your code calls the old modules through this library, change the names. If you use mishka_developer_tools itself, add it to your own deps. See the helper modules documentation.
  5. The dependency requirements changed. req is ~> 0.6.1, up from ~> 0.5.17. guarded_struct is ~> 0.1.0-beta.8, up from ~> 0.0.5. libgraph ~> 0.16 is new, and mishka_developer_tools is gone. What to do: raise req and guarded_struct in your own mix.exs if you pin them.

Added

  • A downloader built on Req for the :url and :github_* types, and an optional SHA-256 checksum: that is checked before extraction.
  • Replay on boot: Installer.CompileHandler loads and starts the installed libraries again from the disc and from Mnesia, also in the embedded code-loading mode of a release.
  • Plugin health checks: the optional health_check/0 callback, and Hook.plugin_health/2, Hook.event_health/2 and Hook.health/1.
  • The optional on_dependency_error/1 callback, and Hook.profile/2, which measures the run time of each plugin in a compiled chain.
  • Dynamic cluster membership in the Mnesia store: :cluster_nodes as :auto or a list of nodes, disc copies, and handling of nodes that go up and down. See the deploy to production documentation.
  • A configurable :extensions_path and Mnesia directory, so they can sit on a volume.
  • The helper modules Helper.Extra, Helper.UUID and QueueAssistant.

Changed

  • The plugin chain of an event is unrolled when the event module is compiled, so each call is a direct call. The contract is the same: {:reply, state} continues and {:reply, :halt, state} halts. The map from an event name to its module is kept in :persistent_term.
  • The use MishkaInstaller.Event.Hook macro only delegates, and the library loads the modules of an installed application explicitly, so it works in a release.

Fixed

  • A held plugin never started on its own when the dependency it waited for became active, because the lifecycle broadcasts carried no data.
  • An unknown aborted-tuple reason sent Helper.MnesiaAssistant.Error.error_description/2 into infinite recursion.
  • Path traversal: an app or version whose package path leaves the extensions directory is refused before anything is removed, renamed or extracted, on install and on uninstall.
  • Tar-slip: an archive member with an absolute path or a .. path is refused before extraction.
  • Compiler warnings from @impl, deprecations and generated code.

0.1.5 and 0.1.4

0.1.5 was released on 2026-04-19. 0.1.4 was tagged in git on the same day, but it was not published to Hex. Neither changed the code of the library. These releases have no CHANGELOG entry.

Changed

  • 0.1.4 raised the dependency requirements to phoenix_pubsub ~> 2.2, req ~> 0.5.17, plug ~> 1.19 and telemetry ~> 1.4.
  • 0.1.5 raised guarded_struct to ~> 0.0.5 and mishka_developer_tools to ~> 0.1.10.

0.1.3

Released on 2025-09-13. This release has no CHANGELOG entry.

When you upgrade

  • Remove the code that matches {:error, :undefined_function_error}. An event with no plugins hands your data back. See the call an event documentation.

Changed

  • mishka_developer_tools moved to ~> 0.1.9.

0.1.2

Released on 2025-09-12. This release has no CHANGELOG entry.

Fixed

  • A problem with Elixir 1.18. The Hook macro now keeps only the basic, serializable values of Mix.Project.config(): :app, :version, :elixir, :deps, :name, :source_url, :homepage_url and :description.

Changed

  • Dependency requirements: req ~> 0.5.15, plug ~> 1.18.1 and guarded_struct ~> 0.0.4.

0.1.1

Released on 2024-11-25.

Added

  • Callbacks and a behaviour for the MishkaInstaller.Event.Hook macro.
  • The guarded_struct dependency.

Changed

  • Dependencies updated and tested with Elixir 1.17.2.

0.1.0

Released on 2024-07-03. The whole project was rewritten on Erlang's built-in database, Mnesia. The CHANGELOG states that earlier versions are not supported, so there is no upgrade path from the 0.0 releases. Start from the get started documentation.