Plugin Guide
Whitebox's plugin system allows for easy extension of functionality through self-contained modules. Official Whitebox plugins are available on PyPI.
A few example plugins are as follows:
Plugin Structure¶
A typical plugin structure looks like this:
whitebox_plugin_<plugin_name>
├── whitebox_plugin_<plugin_name>
│ ├── static
│ │ ├── whitebox_plugin_<plugin_name>
│ │ │ ├── whitebox_plugin_<plugin_name>.css
│ │ │ └── whitebox_plugin_<plugin_name>.js
│ │ └── assets
│ │ ├── logo.svg
│ │ └── my_image.png
│ ├── templates
│ │ └── whitebox_plugin_<plugin_name>
│ │ └── whitebox_plugin_<plugin_name>.html
| ├── jsx
│ │ └── whitebox_plugin_<plugin_name>
│ │ ├── SomeComponent.jsx
│ │ └── SomeOtherComponent.jsx
│ ├── __init__.py
│ ├── whitebox_plugin_<plugin_name>.py
│ └── any_other_file.xyz
├── whitebox_test_plugin_<plugin_name>
│ ├── __init__.py
│ ├── test_whitebox_plugin_<plugin_name>.py
│ ├── test_whitebox_plugin_<plugin_name>_browser.py
│ └── test_whitebox_plugin_<plugin_name>_integration.py
├── LICENSE
├── Makefile
├── README.md
├── pyproject.toml
└── poetry.lock
For whitebox to be able to discover and load plugins dynamically, the plugin package must be named whitebox_plugin_<plugin_name>
. The plugin package must contain a whitebox_plugin_<plugin_name>.py
file that defines the plugin_class
attribute for the plugin class to be exported correctly.
Additionally, for plugin tests to be able to discover and load dynamically, the test package must be named whitebox_test_plugin_<plugin_name>
. The test files must start with test_
and they may end with _browser
or _integration
to indicate the type of test to ensure readability.
To initialize a new plugin project, run:
Each plugin is a Python package with its own set of resources, including static
files, templates and JSX files. If any additional assets are required, they
should be placed in the assets
directory within the static
folder.
JSX files should be placed in the jsx
directory within the plugin package.
These files will be transpiled and made available as frontend components via
module federation, using vite-plugin-federation.
If any additional files are required for the plugin to function. Maybe a text file plugin needs to read from or any other file, they should be placed in the root directory of the plugin package not in the root directory of the project. This ensures that poetry can package the plugin correctly when it is published to PyPI.
This structure allows for clear separation of concerns and makes it easy to distribute and install plugins. Additionally, each plugin is expected to have its own repository for version control, CI and documentation if needed.
Plugin API¶
Plugins must implement the base plugin class provided by the Whitebox. Depending on the plugin's requirements, they can export some or all of the following attributes and methods:
import whitebox
class MyPlugin(whitebox.Plugin):
name = "My Plugin"
plugin_template = "plugin_name/plugin_name.html"
plugin_template_embed = "plugin_name/plugin_name_embed.html"
plugin_css = [
"/static/plugin_name/plugin_name.css",
]
plugin_js = [
"/static/plugin_name/plugin_name.js",
]
plugin_class = MyPlugin
If a plugin needs to do some processing before sending template or static files, it can override the following methods:
import whitebox
class MyPlugin(whitebox.Plugin):
name = "My Plugin"
def get_template(self) -> str:
"""Return the name of the plugin's main template."""
pass
def get_template_embed(self) -> str:
"""
Return the path to the HTML template file, that will be embedded in an
iframe for the plugin.
"""
pass
def get_css(self) -> list:
"""Return the path to the plugin's CSS files."""
pass
def get_js(self) -> list:
"""Return the path to the plugin's JavaScript files."""
pass
plugin_class = MyPlugin
To ensure that all static file paths are correctly resolved, it is recommended to use django.templatetags.static.static
. This function can resolve the correct URL path for serving static files like images exported by plugins. For example, use:
from django.templatetags.static import static
class MyDevicePlugin(Plugin):
device_image_url = static("whitebox_plugin_device_xyz/path/to/image.webp")
plugin_class = MyDevicePlugin
In the end, the plugin class must be exported as plugin_class
. If this attribute is not present, the plugin will not be loadable by Whitebox.
Standard API¶
Whitebox provides a standard API for plugins to interact with the system and other plugins on the backend side. This API includes methods for:
- Registering event callbacks
- Unregistering event callbacks
- Emitting events
- Accessing shared resources
- Interacting with the database
Example of registering an event callback:
import whitebox
class MyPlugin(whitebox.Plugin):
def __init__(self):
self.whitebox.register_event_callback("flight_start", self.on_flight_start)
async def on_flight_start(self, data):
print("Flight started")
Example of emitting an event:
import whitebox
class MyPlugin(whitebox.Plugin):
async def update_location(self, lat, lon, alt):
# Emit a location update
await self.whitebox.api.location.emit_location_update(lat, lon, alt)
# Emit a custom event
await self.whitebox.emit_event("custom_event", {"data": "example"})
Refer to Plugin API Reference for more details on the available methods and properties.
JSX API¶
In addition to Python code, plugins can also supply JSX code defining React components. Using this feature, plugins can provide custom UI components that can be used in addition, or as an augmentation of other plugins, fully utilizing the design elements, and features of React such as state management.
Defining the JSX component¶
To define a JSX component, create a file in the jsx
directory of the plugin,
within a plugin name's directory. For example, if the plugin name is
whitebox_plugin_r2d2
, the JSX component file should be placed in
whitebox_plugin_r2d2/jsx/whitebox_plugin_r2d2/MyComponent.jsx
. This goes
inline with how static files are resolved in the plugin system.
The JSX component file should export a React component, both as a default export, and a named one. For example:
const MyComponent = () => {
const [isTranslated, setIsTranslated] = useState(false);
return (
<>
<p>
R2D2 says: {isTranslated ? "Beep Boop" : "Hello there"}
</p>
<button onClick={() => setIsTranslated(!isTranslated)}>
Translate
</button>
</>
);
};
export {
MyComponent,
};
export default MyComponent;
An app rarely uses only a single component, and your plugin can define as many components as needed. Each component should be defined in a separate file, and exported in the same manner as the example above.
You can easily import JSX components from the same plugin with the usual
import
statement. For example, if you had the following files:
whitebox_plugin_r2d2/jsx/whitebox_plugin_r2d2/First.jsx
whitebox_plugin_r2d2/jsx/whitebox_plugin_r2d2/Second.jsx
you could import them like this:
// from within `First.jsx`
import Second from "./Second.jsx";
// or from within `Second.jsx`
import First from "./First.jsx";
The JSX component will be transpiled and made available to core and other plugins via the module federation registry. To use components from the core or other plugins, you can use the methods below.
Using JSX components through capabilities¶
Plugins may define capabilities that they provide, or require capabilities
that they need to work. This allows the core and other plugins to interact
in a plugin-agnostic way. For example, a plugin may require the map
capability to render a map component, or provide the map-tiles
capability to
augment rendered maps with custom tiles.
Defining capabilities & slot components¶
A plugin may define capabilities that it provides, along with the JSX components
that are provided through those capabilities. To define a capability, add a
provides_capabilities
attribute to the plugin class. You can then define a
mapping of capability names to JSX components that are provided through those
capabilities, as well as define a mapping of components exposed directly by
a specific name:
import whitebox
class MyPlugin(whitebox.Plugin):
...
provides_capabilities = ["map"]
slot_component_map = {
"map.display": "my_plugin/MyMapComponent",
}
exposed_component_map = {
"map": {
"SpecificMapDisplay": "my_plugin/MyMapComponent",
}
}
This will make the MyMapComponent
, located at path
PLUGIN_ROOT/jsx/my_plugin/MyMapComponent.jsx
available to the core and other
plugins to use, both as a map.display
component implementation, and as a
map.SpecificMapDisplay
component to be used directly by others, as explained
in below sections.
Using other plugins' slot components¶
To use other plugin's capabilities, Whitebox offers a SlotLoader
component,
which is available globally (it is attached to the window
object). It takes a
name
prop to define what slot component to load, and will pass all the other
props to the slot component.
For example, you'd like to render a map somewhere within your plugin's UI, but
you don't want to hardcode any specific map components. Instead, you'd like to
use the map component provided by the map.display
slot component.
First, you would want to ensure that the map
capability is available, by
defining it in your plugin's requires_capabilities
attribute:
Then, you would use the Slot
component to render the map component:
const MyComponent = () => {
return (
<div>
<h1>Look at this shiny map below!</h1>
<SlotLoader name="map.display" />
</div>
);
};
This will render the map component provided by the plugin that provides the
map.display
capability. If the component accepts a darkMode
prop, you
can pass it along with the other props:
const MyComponent = () => {
const [darkMode, setDarkMode] = useState(false);
return (
<div>
<h1>Look at this shiny map below!</h1>
<SlotLoader name="map.display" darkMode={darkMode} />
<button onClick={() => setDarkMode(!darkMode)}>Toggle dark mode</button>
</div>
);
};
In the same way that you've used map.display
to render the slot component
implementation, you can use an exposed component (in the above example, the
map.SpecificMapDisplay
) to render the specific component, no matter the
capability:
Using the JSX components directly¶
In addition to using the JSX components through the SlotLoader
, you can also
use a component directly, as if you imported it. This is useful when you want to
have a more fine-grained control over the component, or when you want to use the
component in a more complex way.
To use a component from another plugin or from the core, you'll need to use the
importWhiteboxComponent
utility, which is available globally (it is attached
to the window
object). This utility takes a slot name as a prop, and will
return the component that is registered with that slot name.
For example, if you wanted to render the MyMapComponent
from the example above,
you could use:
const MapDisplay = importWhiteboxComponent("map.display");
const MyComponent = () => {
const [darkMode, setDarkMode] = useState(false);
return (
<div>
<h1>Look at this shiny map below!</h1>
<MapDisplay darkMode={darkMode} />
<button onClick={() => setDarkMode(!darkMode)}>Toggle dark mode</button>
</div>
);
};
In the same way that you've imported map.display
to import the slot component
implementation, you can import an exposed component (in the above example, the
map.SpecificMapDisplay
) to import the specific component, no matter the
capability:
In addition to the plugins' JSX components, you can also use the core's JSX components. They are "top-level" components that are not under a capability namespace. Some of the available components are:
PrimaryButton
SecondaryButton
Logo
For example, if you wanted to render the PrimaryButton
component from the
kernel, you could use:
const PrimaryButton = importWhiteboxComponent("PrimaryButton");
const MyComponent = () => {
const [count, setCount] = useState(0);
return (
<div>
<h1>Click the button below! (clicked {count} times so far)</h1>
<PrimaryButton text="Click me" onClick={() => setCount(count + 1)} />
</div>
);
};
To see the available components from the kernel, you can check the
whitebox/frontend/src/utils/components.jsx
file, which has the full list.
Additionally, you can check which components it links to, giving you the
components' full spec.
Using slots¶
In addition to using the SlotLoader
component, you can also define slots in
your components, allowing other plugins to augment your components with their
own content. This is useful when you want to provide a way for other plugins to
extend your components.
To define slots in your component, you can use the useComponentSlots
hook,
which is available globally (it is attached to the window
object). This hook
returns a Slot
component that you can use to define default content, or render
the content provided by the parent components.
For example, if you wanted to define a slot in your component, you could use:
const MyComponent = ({children}) => {
const [Slot] = useComponentSlots(children);
return (
<div>
<h1>My component</h1>
<Slot name="my_slot">
<p>This is the default content for the slot</p>
</Slot>
</div>
);
};
This will define a slot named my_slot
in your component, with the default
content being the <p>
element. When another plugin uses your component, they
can provide their own content for the slot, which will replace the default
content.
For example, to replace the default content of the my_slot
slot, another
plugin could use your component like this:
const MyComponent = importWhiteboxComponent("..."); // Import the component
const MyOtherComponent = () => {
return (
<div className="outer">
<MyComponent>
<h1 slot="my_slot">This is an override</h1>
</MyComponent>
</div>
);
};
This will replace the default content of the my_slot
slot (<p>
) with the
provided <h1>
override. The resulting output will be:
Understanding the Plugin System¶
Whitebox employs a dynamic plugin discovery and management system. The process of loading and unloading plugins is as follows:
- On startup, the system scans the environment for installed plugins with the
whitebox_plugin_
prefix. - Each discovered plugin is instantiated and registered with the system.
- Plugin resources (templates, static files) are registered with Django's asset pipeline.
- Event callbacks registered by plugins are added to the event system.
- Device classes available in the plugin are registered with the device manager.
- JSX components available in the plugin are transpiled, and then registered with to the module federation registry, making them available to frontend.
- Plugins can be unloaded at runtime, removing their resources and event callbacks from the system by simply removing the plugin package
(poetry remove <plugin_name>)
and calling/plugins/refresh/
endpoint.
When /plugins/refresh/
is called, the system will rescan the environment for plugins and remove or add any new plugins without requiring a server restart or altering what is currently running. This process ensures that plugins are properly integrated into the system without requiring manual configuration for each new plugin.
Plugin Development Workflow¶
Plugin must be initialized using poetry
and should adhere to the structure outlined in the Plugin Structure section. The plugin should implement the base plugin class provided by Whitebox and export it as plugin_class
as outlined in the Plugin API section. The plugin can then interact with the system using the Standard API provided by Whitebox.
To set up a development environment for a plugin, follow these steps:
- Run the development environment container for Whitebox.
- In plugins folder, create a new plugin project using poetry.
- Add the plugin to Whitebox using the following command:
poetry add -e path/to/plugin
within the backend development container. - Run the Whitebox server.
This installs the plugin in editable mode, allowing you to make changes to the plugin code and see the effects immediately without reinstalling the plugin in whitebox.
Testing plugins on CI environment (including sandbox)¶
To include the plugin during CI runs, you need to add the plugin to the
whitebox
project as a dependency via specific git ref, which means that you'd
have to, after testing, replace that dependency with the actual plugin version
before merging.
As this is mundane and prone to human error, the CI environment allows you to
add a "temporary" dependency that will be used in every CI step where whitebox
is being installed. This is done through the poetry
's optional temporary-dependencies
dependency group.
This mechanism will install those dependencies to use for testing and sandbox deploys, and they will be removed by the CI upon merge.
To add a plugin to the temporary group, you can run:
poetry add --group temporary-dependencies git+https://gitlab.com/whitebox-aero/whitebox-plugin-name.git#feature/whitebox-1337
This will add the plugin by git repository and branch. To test that everything works well, you can run:
Take note that, when you include --with temporary-dependencies
, those dependencies
will take precedence over the ones defined in the standard groups. That means
that you can freely add the plugin to the temporary-dependencies
group with a
specific git ref, without needing to remove it from the standard groups.
Augmentation through the Frontend API¶
In addition to just extending the backend, plugins can also extend the frontend by providing custom templates, styles, and scripts. The frontend API allows plugins to define custom templates, styles, and scripts that will be loaded into the frontend when the plugin is active.
Plugin scripts have access to the global Whitebox
object, which allow for plugins
to interact with the core as well as with other plugins.
Registering plugins¶
To register a plugin, you need to create an object representing the plugin, register
it with the Whitebox
:
const init = () => {
console.log('Plugin loaded!')
}
const module = {
name: 'my_first_plugin',
providesCapabilities: ['map'],
requiresSockets: ['flight'],
init: init,
}
Whitebox.plugins.registerPlugin(module)
Plugin capabilities¶
Plugins can provide and require capabilities. Capabilities are a way to define what functionality a plugin provides or requires. This allows other plugins to interact with the plugin that they want to extend, based on the capabilities it provides.
If you'd like to augment the behavior of a plugin that provides a map
capability,
or your plugin requires the capability to be present in order to work, you can
require that capability in your plugin:
const module = {
name: 'my_second_plugin',
providesCapabilities: ['capability_1'],
requiresCapabilities: ['map'],
init: init,
}
For example, the gps-display
plugin
provides map
capability, which can be used by other plugins to augment the map. You can see
its implementation of the map
capability through the MapExtension
here.
At the moment, there are no mechanisms to ensure that a plugin can or cannot be loaded based on the available capabilities, but this is a planned feature.
Plugin sockets¶
Plugins can also require sockets. As multiple plugins can require the same socket connection, the Whitebox will ensure that the socket is only connected once, and all plugins requiring the same socket will receive the same connection.
The plugin can then use the socket directly to send the events, as well as add event listeners to receive events from the socket:
const init = () => {
Whitebox.sockets.addEventListener('flight', 'message', (event) => {
const data = JSON.parse(event.data);
if (data.type === "location_update") {
console.log('We are now located at ', data.lat, data.lon);
}
})
Whitebox.sockets['flight'].send(JSON.stringify({ type: 'get_location' }))
}
Plugin extensions¶
Plugins can also extend the core functionality by adding extensions that standardize the way plugins can interact with the core and with each other. Extensions are a way to define a set of methods that plugins can use to interact with the core or with other plugins.
They are the abstract classes that provide an extensible interface for plugins
to implement, as a form of a contract that plugins need to implement for core to
be able to use, and plugins to be able to interact with each other. This approach
allows any plugin to fully implement the feature of the core. For example, the
gps-display
implements the map through Leaflet.js
, using the MapExtension
,
allowing gps-display-icons
to augment the map with custom icons. Another plugin
may want to augment the map with custom layers, or reimplement the map using a
different map library, in place of gps-display
. As long as the plugin implements
the MapExtension
properly, it can be used as a map provider, and the gps-display-icons
will be able to augment its own map without any additional changes.
At the moment, extensions are defined in the core, in file frontend/src/bridge/extensions.js
.
In the future, these will be moved into the plugins themselves, to allow for clean
separation of concerns between core and plugins [GitLab issue].
You can see an example of a map extension implementation in the GPS Display plugin, and how it's being interacted with in the GPS Display Icons plugin.
Helper utils¶
Additionally, Whitebox provides a set of helper utilities that plugins can use:
Whitebox.apiURL
(string
): URL to the Whitebox API
External assets¶
Large static assets¶
In some cases, plugins may require packaging of large assets. As PyPI imposes a limit of 100MB for any packages it hosts, Whitebox offers a way for plugins to depend on externally hosted assets, which can, from runtime's perspective, be considered as a part of the package itself.
Upon plugin loading, Whitebox will ensure that all the external files are downloaded
and ready to be served. To specify external files, create a file in the plugin's root
directory called external-asset-manifest.json
, for example:
whitebox_plugin_<plugin_name>
├── whitebox_plugin_<plugin_name>
│ ├── __init__.py
│ ├── whitebox_plugin_<plugin_name>.py
│ └── external-asset-manifest.json <--- this one
├── pyproject.toml
Every external file needs to have 3 components:
-
URL from which it'll be sourced from
-
Integrity hash
Every file must have an integrity hash to verify that the downloaded file is
a proper one. Within the integrity string, you should specify what hashing
algorithm is used, in format [ALGORITHM]-[INTEGRITY_HASH]
.
Supported hashing algorithms: sha1
, sha256
.
Upon plugin loading, all the files will be checked, and:
- If a file does not exist, it will be downloaded
-
If a file exists, but the file hash does not match, it will be downloaded, replacing the existing file. This behavior allows you to freely update your manifest file with new files, without worrying whether the files will be stale.
-
Target path where it will be saved locally and served from
Whitebox saves these files in a special location for asset files, which will be
available to plugins in the same manner as if they were the ordinary static files
within the plugins' static/
folder.
For example, if your plugin's package name was whitebox_plugin_r2d2
, and the
target path is voices/beep-boop.mp3
, the file will be available for plugin's
use at path /static/whitebox_plugin_r2d2/voices/beep-boop.mp3
.
Additionally, you can freely use
{% static "whitebox_plugin_r2d2/voices/beep-boop.mp3" %}
template tags in the templates to reference these files, or alternatively use
django.templatetags.static.static("whitebox_plugin_r2d2/voices/beep-boop.mp3")
for the same purpose from within the code.
For the above example, the asset manifest file would look like this:
{
"sources": [
{
"url": "https://example.org/r2d2/asset-file.mp3",
"integrity": "sha1-8dfa2f3e56f3abd46119b698bf6a91cb18482c85",
"target_path": "voices/beep-boop.mp3"
},
... more files
]
}
To verify whether the manifest file is proper, you can use the Django's
verify_external_asset_manifest
command, by providing either the installed plugin's
module name, or the path to file, e.g.
poetry run python whitebox/manage.py verify_external_asset_manifest --module-name whitebox_plugin_r2d2
, orpoetry run python whitebox/manage.py verify_external_asset_manifest path/to/plugin-root/whitebox_plugin_r2d2/external_asset_manifest.json
Testing Plugins¶
Plugins can only be tested from within the whitebox environment. To run tests, you need to have whitebox running locally. The test runner will automatically discover and run all tests in the whitebox_test_plugin_<plugin_name>
package as long as they follow the naming convention outlined in the Plugin Structure section.
Unit & Integration tests would usually begin with the following structure:
from django.test import TestCase
from plugin.manager import plugin_manager
class TestWhiteboxPluginExamplePlugin(TestCase):
def setUp(self) -> None:
self.plugin = next(
(
x
for x in plugin_manager.plugins
if x.__class__.__name__ == "WhiteboxPluginExamplePlugin"
),
None,
)
return super().setUp()
def test_plugin_loaded(self):
self.assertIsNotNone(self.plugin)
def test_plugin_name(self):
self.assertEqual(self.plugin.name, "Example Plugin")
# Add more tests here
While browser tests would usually begin with the following structure:
import os
import logging
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
from django.urls import reverse
from playwright.sync_api import sync_playwright
# Disable warnings
logging.basicConfig(level=logging.ERROR)
logger = logging.getLogger(__name__)
logging.getLogger("django.request").setLevel(logging.ERROR)
logging.getLogger("django.server").setLevel(logging.ERROR)
class TestWhiteboxPluginExamplePluginBrowser(StaticLiveServerTestCase):
@classmethod
def setUpClass(cls):
os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"
super().setUpClass()
cls.playwright = sync_playwright().start()
cls.browser = cls.playwright.chromium.launch(headless=True)
cls.context = cls.browser.new_context()
cls.page = cls.context.new_page()
@classmethod
def tearDownClass(cls):
cls.page.close()
cls.context.close()
cls.browser.close()
cls.playwright.stop()
super().tearDownClass()
def setUp(self):
self.page.goto(f"{self.live_server_url}{reverse('index')}")
# Add more tests here
Additionally to ensure browser tests run correctly, you would have to install playwright additional dependencies. To install playwright dependencies, follow the steps below:
- Run:
poetry run playwright install
- Run:
poetry run playwright install-deps
(optional, for Linux systems only) - Ensure you have added plugin to whitebox:
poetry add -e path/to/plugin
.
Finally, you would run test on whitebox using the following command:
Writing CI¶
The CI for a plugin would almost always get extended from the whitebox shared CI file. The CI would usually look like this:
image: python:3.10
stages:
- setup
- lint
- test
- update_version
- publish
include:
- project: "whitebox-aero/whitebox"
ref: "main"
file: ".gitlab/config/shared-ci.yml"
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.pip-cache"
POETRY_HOME: "$CI_PROJECT_DIR/.poetry"
cache:
paths:
- .pip-cache/
- .poetry/
- .venv/
run_setup:
extends: .shared_plugin_setup
run_lint:
extends: .shared_plugin_lint
run_test:
extends: .shared_plugin_test
update_version:
extends: .shared_plugin_update_version
publish:
extends: .shared_plugin_publish
Versioning Plugins¶
Whitebox uses Semantic Versioning for versioning plugins.
In the CI file above, the update_version
stage is responsible for updating the
version of the plugin.
When a merge request is merged to main
, first, patch version will be bumped in the
pyproject.toml
file. A commit will be made with the new version, along with a new tag,
which the CI will push to the repository. After that, the plugin will be published,
as outlined below.
It is important to ensure that the version is incrementally updated, so that the plugin can be published correctly to PyPI.
Automatic versioning setup¶
The following steps apply for setting up automatic versioning on GitLab,
orchestrated by .gitlab-ci.yml
.
-
Set up an
Access Token
that CI will use to update the version -
Go to your repository's settings on Gitlab:
Settings > Access Tokens
- Create a new token with the following settings:
- Name: name that you want to appear as the committer (e.g.
Whitebox CI
) - Role:
Maintainer
- Scopes:
read_repository
,write_repository
- Name: name that you want to appear as the committer (e.g.
-
Copy the token, as it won't be displayed again
-
Add the token to your repository's CI/CD settings
-
Go to your repository's settings on Gitlab:
Settings > CI/CD
- Open
Variables
-
Add a new variable with the following settings:
- Type:
Variable
- Environment scope:
All
- Visibility:
Masked
- Key:
PUSH_TOKEN
- Value: the token you copied
- Type:
-
Ensure that the
update_version
stage is set up in your.gitlab-ci.yml
-
The stage should include the following job:
After this is set up, the CI will automatically update the version of the plugin
when a merge request is merged to the default branch (main
). This version will
automatically be used when the plugin is published.
Publishing Plugins¶
-
Initial Setup for New Plugins/Repositories:
- If the plugin is new and does not have a PyPI project, you need to create it using your PyPI account.
-
Perform the initial publish using the command:
-
This should be done from your local machine to automatically create the package on PyPI.
- Once the project is set up on PyPI, add
antoviaque
as an owner to the project to share access and management.
-
Setting Up PyPI Access Tokens for CI:
- Create a new access token on your PyPI account. This token should be scoped specifically for the project to limit permissions effectively.
- Add this project-scoped token to your CI environment configuration to
enable automated publishing for future releases, similarly to the
PUSH_TOKEN
setup above: - Type:
Variable
- Environment scope:
All
- Visibility:
Masked
- Key:
PYPI_TOKEN
- Value: the token you created
-
For Existing PyPI Projects:
- If there is already a PyPI project for the repository, you will need to request access from one of the current maintainers. You can find the maintainers listed on the project’s page on the PyPI website.
Next Steps¶
- Learn more about the Whitebox Architecture
- Check out the Development Guide
- Read more about the rationale behind the plugin architecture in the WIP 0002 - Plugin Architecture Implementation Proposal