Files
mattermost/docs/develop/integrate/plugins/migration.md
T
Eva SarafianouandCursor b9c2bd733f docs: restore generated plugin SDK reference pages (#37788)
* docs: restore generated plugin SDK reference pages

Reimplements the Hugo-era plugingodocs/pluginjsdocs/pluginmanifestdocs
shortcode pipeline natively in Docusaurus, so the server plugin SDK,
web app plugin SDK, and manifest reference pages render full generated
content again instead of "Generated content (migrating)" placeholders.

Two new Go generators (gen-plugin-godocs, gen-plugin-manifest-docs) and
one Node generator (gen-plugin-jsdocs.mjs) read server/public/plugin,
server/public/model, and webapp/channels/src/plugins/registry.ts
directly from this monorepo and emit gitignored JSON consumed by new
PluginGoDocs/PluginGoExample/PluginJsDocs/PluginManifestDocs React
components, wired into prestart/prebuild alongside the existing
sidebar/OpenAPI generators.

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs: remove migration plan doc from this PR

Was a handoff/status doc for the implementing agent, not meant to ship
as part of the change itself.

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs: make plugin-godocs/manifest-docs builds atomic

build:plugin-godocs and build:plugin-manifest-docs redirected straight
into data/plugin-*.json, which doesn't exist on a clean checkout (the
redirection itself would fail before the generator ever ran) and, on a
subsequent failed run, would truncate a previously-good JSON file
before failing. Create data/ up front, write to a .tmp file, and only
mv it into place once the generator exits successfully.

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs: fix review findings in plugin doc generators

gen-plugin-manifest-docs:
- jsonFieldName now excludes unexported fields (matching encoding/json)
  and falls back to the Go field name when there's no tag or the tag's
  name component is empty (e.g. json:",omitempty"), instead of
  dropping the field entirely.
- exprTypeDocs's SelectorExpr case now resolves a qualified type only
  when its package qualifier actually points at the model package
  (checked against the declaring file's imports), instead of matching
  any x.Sel identifier by name alone regardless of which package it
  qualifies.

gen-plugin-godocs:
- The example-code loop now falls back to example.Code when
  example.Play is nil (go/doc leaves Play nil when it can't synthesize
  a whole runnable program), and propagates printer.Fprint errors
  instead of discarding them.

No output change for the current server/public/plugin or
server/public/model content — verified via a full regen.

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs: drop legacy-repo comment references, align jsdocs generator parser

Remove "port of the old mattermost-developer-documentation Hugo
shortcode" comments from the plugin doc generators/components now that
they're fully native to this monorepo, and switch gen-plugin-jsdocs.mjs
from the TypeScript compiler API to @typescript-eslint/typescript-estree
to match the old script's parser/shape, while keeping the more robust
reArg-declared-parameter-name and node-scoped comment-attachment logic.

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs: bump plugin doc generators' go.mod to go 1.26

go 1.23 was arbitrarily low; align with a current Go version. go run
auto-downloads a matching toolchain on older local installs, so this
doesn't reintroduce a dependency on server/public/go.mod's version.

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs: handle RestElement in gen-plugin-jsdocs parameter extraction

paramNamesFromPattern silently dropped rest parameters/destructured rest
members (e.g. (a, ...rest) or {a, ...rest}), since RestElement matched
none of its type checks. No effect on today's registry.ts output (no
rest patterns currently used there), but keeps the extraction correct if
one is ever introduced.

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs: simplify plugin doc generators README note

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs: check stdout write errors in plugin doc generators

os.Stdout.Write's return errors were discarded, so a failed/partial
write (e.g. broken pipe, disk full) would still exit 0. Combined with
the npm scripts' > file.tmp && mv pattern, a truncated write could be
treated as a successful generation. Fatal on either write failing.

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs: flatten Server/Webapp plugin SDK reference sidebar nesting

server/server-reference.md and webapp/webapp-reference.md were the only
file in their respective folders, so the sidebar generator (which only
collapses a folder into a single entry when it has an index.md) rendered
them as a "Server" > "Server plugin SDK reference" category with one
child instead of one flat entry, unlike every sibling reference folder
(rest-api/index.md, bot-accounts/index.md, etc.).

Rename both to index.md to match that convention, and update the ~20
cross-referencing links (many with #anchor fragments) that pointed at
the old /reference/server/server-reference and /reference/webapp/webapp-
reference paths.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-21 15:21:21 +03:00

9.4 KiB

title, sidebar_position
title sidebar_position
Migrate plugins 60

The plugin package exposed by Mattermost 5.6 and later drops support for automatically unmarshalling a plugin's configuration onto the struct embedding MattermostPlugin. As server plugins are inherently concurrent (hooks being called asynchronously) and the plugin configuration can change at any time, access to the configuration must be synchronized.

Plugins compiled against 5.5 and earlier will continue to work without modification, automatically unmarshalling a plugin's configuration but with the existing risk of a corrupted read or write. Once the plugin is recompiled against Mattermost 5.6, it will be necessary to manually unmarshal your plugin's configuration. Client-only plugins and server plugins without public fields require no modifications.

Note that you do not need to wait until Mattermost 5.6 to make these changes, as the hardened approach explained below will work with Mattermost 5.5 and earlier. Any implementation of OnConfigurationChange you define overrides the one automatically unmarshalling.

Server changes

Unmarshalling configuration

Previously, any public fields defined on the struct embedding MattermostPlugin would be automatically unmarshalled from the plugin's configuration:

type Plugin struct {
    plugin.MattermostPlugin

    Greeting string
}

func (p *Plugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello %s!", p.Greeting)
}

Writing to Greeting while the plugin may be concurrently reading from same could result in a corrupted read or write. The mattermost-plugin-starter-template and mattermost-plugin-demo have both been updated with more complete examples, but the general idea is to manually handle the OnConfigurationChange hook and synchronize access to these variables. One such way is with a sync.RWMutex:

type Plugin struct {
    plugin.MattermostPlugin

    greetingLock sync.Mutex
    greeting string
}

func (p *Plugin) OnConfigurationChange() error {
    type configuration struct {
        Greeting string
    }

    // Load the public configuration fields from the Mattermost server configuration.
    if err := p.API.LoadPluginConfiguration(configuration); err != nil {
        return errors.Wrap(err, "failed to load plugin configuration")
    }

    p.configurationLock.Lock()
    defer p.configurationLock.Unlock()
    p.greeting = configuration.Greeting

    return nil
}

func (p *Plugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
    p.configurationLock.RLock()
    defer p.configurationLock.RUnlock()

    fmt.Fprintf(w, "Hello %s!", p.greeting)
}

Unfortunately, this adds a fair bit of extra complexity. You may wish to base your updated implementation off of mattermost-plugin-starter-template or mattermost-plugin-demo to simplify your code.

Migrate plugins from Mattermost 5.1 and earlier

Mattermost 5.2 introduces breaking changes to the plugins beta. This page documents the changes necessary to migrate your existing plugins to be compatible with Mattermost 5.2 and later.

See mattermost-plugin-zoom for an example migration involving both a server and web app component.

Server changes

Although the underlying changes are significant, the required migration for server plugins is minimal.

Entry point

The plugin entry point was previously:

import "github.com/mattermost/server/public/plugin/rpcplugin"

func main() {
    rpcplugin.Main(&HelloWorldPlugin{})
}

Change the imported package and invoke ClientMain instead:

import "github.com/mattermost/mattermost/server/public/plugin"

func main() {
    plugin.ClientMain(&HelloWorldPlugin{})
}

Hook parameters

Most hook callbacks now contain a leading plugin.Context parameter. Consult the Hooks documentation for more details, but for example, the ServeHTTP hook was previously:

func (p *MyPlugin) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // ...
}

Change it to:

func (p *MyPlugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
    // ...
}

API changes

Most of the previous API calls remain available and unchanged, with the notable exception of removing the KeyValueStore(). Use KVSet, KVGet and KVDelete instead test:

func (p *MyPlugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
    key := r.URL.Query().Get("key")
    switch r.Method {
    case http.MethodGet:
        value, _ := p.API.KVGet(key)
        fmt.Fprintf(w, string(value))
    case http.MethodPut:
        value := r.URL.Query().Get("value")
        p.API.KVSet(key, []byte(value))
    case http.MethodDelete:
        p.API.KVDelete(key)
    }
}

Any standard error from your plugin will now be captured in the server logs, including output from the standard log package, but there are also explicit API methods for emitting structured logs:

func (p *MyPlugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
    p.API.LogDebug("received http request", "user_agent", r.UserAgent())
    if r.Referer() == "" {
        p.API.LogError("missing referer")
    }
}

This would generate something like the following in your server logs:

{"level":"debug","ts":1531494669.83655,"caller":"app/plugin_api.go:254","msg":"received http request","plugin_id":"my-plugin","user_agent":"HTTPie/0.9.9"}
{"level":"error","ts":1531494669.8368616,"caller":"app/plugin_api.go:260","msg":"missing referer","plugin_id":"my-plugin"}

Web app changes

The changes to web app plugins are more significant than server plugins.

Entry point

The plugin entry point was previously registered by directly manipulating a global variable:

window.plugins['my-plugin'] = new MyPlugin();

Instead, use the globally exported registerPlugin method:

window.registerPlugin('my-plugin', new MyPlugin());

Externalize dependencies

The plugins beta suggested relying on the global export of common libraries from the web app:

const React = window.react;

While this remains supported, it is more natural to leverage Webpack Externals. Configure this in your .webpack.config.js:

module.exports = {
    // ...
    externals: {
        react: 'react',
    },
    // ...
};

and then import your modules naturally:

import React from 'react';

Note however that the exported variables have changed to the following:

Prior to Mattermost 5.2 Mattermost 5.2
window.react window.React
window['react-dom'] window.ReactDom
window.redux window.Redux
window['react-redux'] window.ReactRedux
window['react-bootstrap'] window.ReactBootstrap
window['post-utils'] window.PostUtils
N/A window.PropTypes

Initialization

The initialize callback used to receive a registerComponents callback to configure components, post types and main menu overrides:

import ChannelHeaderButton from './components/channel_header_button';
import MobileChannelHeaderButton from './components/mobile_channel_header_button';
import PostTypeZoom from './components/post_type_zoom';
import {configureZoom} from './actions/zoom';

class MyPlugin {
    initialize(registerComponents) {
        registerComponents(
            {ChannelHeaderButton, MobileChannelHeaderButton},
            {custom_zoom: PostTypeZoom},
            {
                id: 'zoom-configuration',
                text: 'Zoom Configuration',
                action: configureZoom,
            },
        );
    }
}

The initialize callback now receives an instance of the plugin registry. In some cases, the registry's API now requires a more discrete breakdown of the registered component to allow the web app to handle various rendering scenarios:

import ChannelHeaderButtonIcon from './components/channel_header_button/icon';
import MobileChannelHeaderButton from './components/mobile_channel_header_button';
import PostTypeZoom from './components/post_type_zoom';
import {startZoomMeeting, configureZoom} from './actions/zoom';

class MyPlugin {
    initialize(registry) {
        registry.registerChannelHeaderButtonAction(
            ChannelHeaderButtonIcon,
            startZoomMeeting,
            'Start Zoom Meeting',
        );

        registry.registerPostTypeComponent('custom_zoom', PostTypeZoom);

        registry.registerMainMenuAction(
            'Zoom Configuration',
            configureZoom,
            MobileChannelHeaderButton,
        );
    }
}

Restructuring your plugin to use the new registry API will likely prove to be the hardest part of migrating.