Changes and upgrade notes
v0.1.14
This page lists the releases of Mishka Installer in the 0.1 line, newest first, up to 0.1.14. 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
- Find the release you run.
Application.spec(:mishka_installer, :vsn)returns it as a charlist, such as~c"0.1.14". - Read each entry between that release and the one you want. Do what its "When you upgrade" list says.
- Run
mix deps.update mishka_installer, then your tests. - 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
| Release | What breaks | What to do |
|---|---|---|
| 0.1.14 | The installer refuses a version that is not a semantic version. | Use MAJOR.MINOR.PATCH versions. Delete the extra records of one application. |
| 0.1.13 | An 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.12 | req 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.11 | restart 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.6 | The 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.3 | Hook.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.2 | Elixir 1.17 or later is required. | Upgrade Elixir. |
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.1and1.0.0+build.5pass.1.0andv1.0.0do not. - Records that are already stored are left as they are. A stored record with the version
1.0is 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/0and keep the newest record of each application:
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.InvalidVersionErrorfor 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 withaction: :semver. A loaded application whose own version is not semantic is refused withaction: :compare_version_with_installed_appandfield: :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 andInstaller.uninstall/1removed 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:
# 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, loggedplugin pipeline raisedand answered the state it was given, so what the plugins returned was thrown away.
Added
- The
cannot_run:option ofHook.call/3andHook.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.exsholdsreq,phoenix_pubsub,plugorguarded_structbelow those versions, raise it. Then runmix deps.update mishka_installer req. - The library uses only
Req.request!/1and a%Req.Response{}match, and nothing it uses changed in Req 0.7. If you set:downloader_req_optionsor: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 inhtml_sanitize_ex, fixed in 1.5.5. Both are fixed only by updating. Runmix deps.update hpax html_sanitize_ex, thenmix 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, orenable/0on the plugin. It is the only way out of:stopped. The error forrestartreads "This plugin is stopped, use `enable/2` to switch it back on." and hasaction: :plugin_status. - The fields you declare in code,
:priority,:extraand:depends, are applied again on every boot. A value you changed in the registry withEvent.write/3is replaced by the value in code. The stored:id,:statusand:inserted_atare kept. - If you write statuses yourself with
Event.write/3, callEvent.sync_dependents/2afterwards.
Added
Event.enable/2, andenable/0on a plugin. A plugin can override it like the other generated functions.Event.dependents/1andEvent.sync_dependents/2. Nothing keeps a list of dependents: the library reads them from the storeddepends.
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/3checks every condition before it writes, so a failed restart leaves no status behind.Event.stop/3accepts a:heldplugin, 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_errormessage was broadcast, and nothing listens to it by default, so a plugin with apriorityoutside 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 registerlines. Each names the plugin, the event and the reason.
0.1.9
Released on 2026-07-03.
Fixed
- A plugin that is stuck in
:registeredis 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:stoppedor:heldrecord 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 ofHook.call/3. It hands read-only context to the plugins that definecall/2. A plugin that definescall/1runs as before, anduse MishkaInstaller.Event.Hookaccepts 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
- The installer takes only pre-built packages. It never runs
mixor compiles. A package is anebinfolder with an<app>.appfile. The release removed the compile code, theInstaller.Collectmodule andInstaller.uninstall/2. 0.1.5 accepted thetypevalues:hex,:github,:github_latest_release,:github_latest_tag,:urland:extracted. 0.1.6 accepts:path, which is the default,:url,:github_tagand:github_latest_release. The fieldsrelease,branch,custom_command,dependency_typeandcompile_typeare gone, andassetandchecksumare new. What to do: build theebinpackage yourself and change the maps you give toInstaller. See the install a library documentation. - Remote installs are closed until you allow them. 0.1.5 had no allowlist. Now a
:urlinstall needs the host inurl_hosts, and a:github_tagor:github_latest_releaseinstall needs the repository ingithub_repos. An empty or missing list blocks them. A:pathinstall is not affected. The library also refuses to install over, or to uninstall, an application inprotected_apps, which holdsmishka_installerby default. What to do: setconfig :mishka_installer, :allowlist. See the configuration documentation. - The stored
Installertable 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/0then returns[],Installer.get(:app, app)returns a record with its fields in the wrong places, andInstaller.write/1fails with{:aborted, {:bad_type, ...}}andaction: :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) - The helper modules moved. The library used
MnesiaAssistantandMishkaDeveloperTools.Helper.ExtraandUUIDfrom themishka_developer_toolspackage. It now has its ownMishkaInstaller.Helper.MnesiaAssistant,MishkaInstaller.Helper.ExtraandMishkaInstaller.Helper.UUID, and it no longer depends onmishka_developer_tools. What to do: if your code calls the old modules through this library, change the names. If you usemishka_developer_toolsitself, add it to your own deps. See the helper modules documentation. - The dependency requirements changed.
reqis~> 0.6.1, up from~> 0.5.17.guarded_structis~> 0.1.0-beta.8, up from~> 0.0.5.libgraph ~> 0.16is new, andmishka_developer_toolsis gone. What to do: raisereqandguarded_structin your ownmix.exsif you pin them.
Added
- A downloader built on
Reqfor the:urland:github_*types, and an optional SHA-256checksum:that is checked before extraction. - Replay on boot:
Installer.CompileHandlerloads 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/0callback, andHook.plugin_health/2,Hook.event_health/2andHook.health/1. - The optional
on_dependency_error/1callback, andHook.profile/2, which measures the run time of each plugin in a compiled chain. - Dynamic cluster membership in the Mnesia store:
:cluster_nodesas:autoor a list of nodes, disc copies, and handling of nodes that go up and down. See the deploy to production documentation. - A configurable
:extensions_pathand Mnesia directory, so they can sit on a volume. - The helper modules
Helper.Extra,Helper.UUIDandQueueAssistant.
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.Hookmacro 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/2into infinite recursion. - Path traversal: an
apporversionwhose 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.19andtelemetry ~> 1.4. - 0.1.5 raised
guarded_structto~> 0.0.5andmishka_developer_toolsto~> 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_toolsmoved 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
Hookmacro now keeps only the basic, serializable values ofMix.Project.config()::app,:version,:elixir,:deps,:name,:source_url,:homepage_urland:description.
Changed
- Dependency requirements:
req ~> 0.5.15,plug ~> 1.18.1andguarded_struct ~> 0.0.4.
0.1.1
Released on 2024-11-25.
Added
- Callbacks and a behaviour for the
MishkaInstaller.Event.Hookmacro. - The
guarded_structdependency.
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.