Add plugin metadata support
Plugin metadata
This PR adds plugin metadata to plugins, in a field called info. It allows implementors (and plugin developers) to decorate plugins with:
- Information about the plugin itself (coming from
package.json, or totally custom) - "Ownership" of a plugin
The metadata (PluginInfo) contains name, version, owner, links etc. It will be deduced from package.json, and the way to do the decoration is for a plugin to:
import packageJson from '../package.json';
// ...
export const myPlugin = createPlugin({
id: 'my-plugin',
apis: [ /* ... */ ],
routes: { /* ... */ },
info: { packageJson }, // <-- add fields here, or just packageJson content
});
This means we can use the package.json as the basis for plugin metadata, and plugins can get information from the package.json about themselves or other plugins. The idea here is to re-use the backstage field for backstage.name and backstage.role etc.
An implementor can provide a callback pluginInfoDecorator to createApp() which will be called for each plugin, to allow further decorations of the plugin (extend with links to slack channels or whatnot).
You could argue that adding the package.json to each plugin will grow the webpack bundles. That's true, but it shouldn't be that bad though. A 1.5kB package.json is gzipped down to <600b. Having this would allow other future plugins to inspect this at runtime and indicate package dependency mismatches etc. Could be useful for more than just the metadata explicitly used here.
Ownership
Alongside the metadata is ownership. It's very useful for implementors to be able to decorate their own internal plugins with owners; what team/group owns a certain plugin. Combined with metadata, this would allow for error boundaries, help/support buttons etc to use that information to provide the user with relevant links to contact the corresponding owner for help / bug reports etc. Implementors can choose their own ownership model (maybe a field in package.json) and use the pluginInfoDecorator to decorate that to the PluginInfo blob.
This isn't only useful for internal plugins though, but also for those plugins pulled in from this very open source repo (or any other). There's likely several teams being responsible for certain parts of a backstage implementation. You might have a core team being overall responsible for the implementation, maybe a documentation team, CI/CD team etc. This is where the pluginOwners option to createApp() comes in handy;
An implementor can create a mapping (e.g. in a yaml-file) between package names (npm package names) and internal team names, and these should (preferably) be catalog groups, or an entity-ref. With wild-card matching, you could create a mapping like this:
# plugin-metadata.yaml
owners:
- '*techdocs*': the-doc-team
- '@backstage/plugin-ci*': cicd-team
- '@backstage/plugin-jenkins*': cicd-team
- '*': the-core-team # Fallback unless specified above
You'd then:
import pluginMetadata from './plugin-metadata.yaml';
createApp({
pluginOwners: pluginMetadata.owners,
// ...
});
A follow-up PR will use this.
:heavy_check_mark: Checklist
⚠️ No Changeset found
Latest commit: 284bc0a721be3577699edf30acfb7467463d1aea
Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.
This PR includes no changesets
When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types
Click here to learn what changesets are, and how to add one.
Click here if you're a maintainer who wants to add a changeset to this PR
This PR has been automatically marked as stale because it has not had recent activity from the author. It will be closed if no further activity occurs. If you are the author and the PR has been closed, feel free to re-open the PR and continue the contribution!
Could we proceed with this somehow @Rugvip?
It's quite crucial to get this metadata support for e.g. error handling and Support-button automation.
Would you consider this mergeable if we make the package.json import dynamic (lazy)?
Would be happy to do some renaming too, "decoration"/"info" -> "metadata" e.g. if that makes more sense. And add tests for it. But it makes more sense to nail the design/architecture of it, e.g. whether to go with lazy-loading.
Missing Changesets
The following package(s) are changed by this PR but do not have a changeset:
- @backstage/core-app-api
- @backstage/core-plugin-api
- @backstage/plugin-cicd-statistics
See CONTRIBUTING.md for more information about how to add changesets.
Changed Packages
| Package Name | Package Path | Changeset Bump | Current Version |
|---|---|---|---|
| @backstage/core-app-api | packages/core-app-api | none | v1.2.1-next.1 |
| @backstage/core-plugin-api | packages/core-plugin-api | none | v1.1.1-next.1 |
| @backstage/plugin-cicd-statistics | plugins/cicd-statistics | none | v0.1.14-next.1 |
Changed to lazy-loaded package.json content for per-plugin metadata and refactored quite a bit.
- (in core-app-api) Aside
AppManageris now a classPluginMetadataExtenderwhich is used for app-level metadata decorations (mapping corporate owners to plugins by name) and allowing any custom metadata decoration. - (in core-plugin-api) Aside
Pluginis now a separate file withgetPluginMetadata()which performs the metadata extraction asynchronously. It allows lazy-loadedpackage.jsons, and extracts fields from it. It then hands over to the app-level metadata decorations (for e.g. ownership mapping).
When creating a plugin, you can simply let info be a dyn-loaded import:
export const myPlugin = createPlugin({
id: 'my-plugin',
info: () => import('../package.json'),
/* ... */
});
But you can also specify certain fields if you want, and let only the packageJson part be lazy-loaded:
export const myPlugin = createPlugin({
id: 'my-plugin',
info: {
packageJson: () => import('../package.json'),
description: 'My plugin',
links: [{ title: 'Somewhere', url: 'https://somewhere.com' }],
},
/* ... */
});
On a plugin object (BackstagePlugin) is now getInfo() which asynchronously returns a metadata object Promise<PluginInfo> given the above procedure.
Thanks for the contribution! All commits need to be DCO signed before they are reviewed. Please refer to the the DCO section in CONTRIBUTING.md or the DCO status for more info.
@grantila Ping on this one - DCO and build warnings
Thanks for the contribution! All commits need to be DCO signed before they are reviewed. Please refer to the the DCO section in CONTRIBUTING.md or the DCO status for more info.
Oh, changesets too!
damn it, misclick
For when you're back: we'll want to figure out how this fits into #11404 too. I'm feeling at least the context has some significant overlap, but perhaps the method of configuration too?
This PR has been automatically marked as stale because it has not had recent activity from the author. It will be closed if no further activity occurs. If you are the author and the PR has been closed, feel free to re-open the PR and continue the contribution!
@Rugvip this is up-to-date with master now, with DCO, and name is removed. I think this is pretty clean now, please take a look.
I could maybe add some documentation for this, for:
- How to setup the app (and configure app-level ownership mapping)
- How to add metadata to a plugin (for
createPlugin)
Where, if so, should I add that?
Updated according to comments