Scaling Micro-Frontend Reliability: Advanced Techniques in Version Management and Automated Testing

Introduction

This article is a follow-up to The power of micro frontends – How to dynamically extend Cumulocity IoT Frontends. Since the original publication, significant changes have occurred, with an increasing number of plugins being created by both Cumulocity teams and the community. As the use of micro frontends within the Cumulocity platform grew, new challenges emerged. One of the most frequently mentioned issues in the micro frontend space is dependency version incompatibility. We’ve addressed this challenge using a set of metadata called the version matrix and Cypress tests. But first, let’s look at what’s changed…

What has changed since the first micro frontend article?

A major update to our Web SDK was the deprecation of the @c8y/cli package. It has been replaced by the @c8y/devkit package, which now serves as a builder for ng-cli. In simpler terms, instead of handling all commands through our own CLI, we now extend Angular CLI. This means you don’t call @c8y/devkit directly; instead, all commands start with ng <command>. For more information about ng-cli, check out the Quickstart section.

Another change is that you no longer declare your exports in package.json. Instead, they are now defined in cumulocity.config.ts (which is later compiled to cumulocity.json).

If you’re ready to build your own plugin, refer to the Microfrontends documentation for detailed, step-by-step guidance on creating a plugin.

Version matrix

Introduced in Web SDK version 10.19.0, the version matrix is a part of the plugin designed to ensure compatibility between the plugin, Web SDK, and shell API. To use it, simply add it to your cumulocity.config.ts file.

Here’s an example of how a version matrix might look (content of cumulocity.config.ts):

    export default {
      runTime: {
        ...
        versioningMatrix: {
          '2.1.6': {
            sdk: '>=1016.0.0 <1018.0.0',
            api: '>=1016.0.0 <1018.0.0',
          },
          '3.2.1': {
            sdk: '>=1018.0.0',
            api: '>=1018.0.0',
          }
        },
      },
      buildTime: {
        ...
      }
    } as const satisfies EnvironmentOptions;

The top-level keys (2.1.6, 3.2.1) represent the plugin versions, while the sdk and api values specify semver ranges of compatible Web SDK and API versions, respectively.

To illustrate how the version matrix works, let’s try installing version 3.2.1 of a plugin with the above matrix into Cockpit version 1017.0.543. You’ll receive this warning:

Plugin installation

As you can see, you can still install the plugin, but you’re warned that versions might be incompatible. Some features might not work, or the plugin could fail to load and potentially break your app. This warning won’t appear for Cockpit version 1018.0.0 or higher.

E2E testing plugins against shell

While the versioning matrix works almost effortlessly when properly declared, how do we determine which versions are compatible with our plugin? Manual testing is always an option, but it’s time-consuming and prone to human error.

This concern led us to develop a solution that informs us about incompatibilities between our plugin and the shell app.

Cypress to the rescue!

The most effective way to ensure our plugin works correctly with a particular shell app (without hiring a new QA specialist) is through end-to-end (E2E) tests. For this type of testing, we use the Cypress library along with cumulocity-cypress and cumulocity-cypress-ctrl packages, developed by our team.

We chose our Cumulocity community plugins repository to introduce this feature. The plan was straightforward - we introduced a new workflow that:

1. Collects shell versions

First, we need to decide which shell versions to test our plugin against. For this purpose, we created the plugins-e2e-setup repository containing a custom Github action collect-shell-versions. This action provides information about the latest versions for relevant distribution tags. By default, it returns the last 3 yearly release tags (and if there aren’t 3 yearly releases yet, it also includes the 1018.0-lts tag). Here’s an example result of this action:

    [
      { "tag": "y2025-lts", "version": "1020.26.2" },
      { "tag": "y2024-lts", "version": "1018.503.119" },
      { "tag": "1018.0-lts", "version": "1018.0.278" }
    ]

2. Builds the plugin

An obvious step - we need to build the plugin to run it in the shell in later steps.

3. Gets shell apps, hosts them, and runs E2E tests

Now that we know which shell versions we want to test our plugin against, we need to obtain builds of these apps. In our previously mentioned plugins-e2e-setup repository, we have a second custom Github action - get-shell-app. This action downloads the build of the requested app (Cockpit, Administration, or Device Management) for the specified version.

With both the shell and plugin apps in hand, we can use cumulocity-cypress-ctrl to host the apps, run Cypress test suites, and verify if the plugins are usable in the shell apps.

Usage of the workflow

Currently, we run this workflow in two scenarios:

  • On each pull request
  • Scheduled with CRON for nightly runs

The first use case helps us detect incompatibilities mainly due to changes in the plugin itself.

First use case

If tests fail, the actions that can be taken are:

  • If tests fail because test suites are out of date - update the tests
  • If tests fail because the plugin is not compatible with some shell versions:
    • Update the versioning matrix to indicate compatible Web SDK versions and align the workflow to test against relevant shell versions
    • Or review the changes and select a different approach to ensure plugin compatibility with the shell

While the first use case is mostly relevant to recent changes in the plugin, the second use case targets changes in the shell app. Even if a new plugin version hasn’t been released for days or weeks, the workflow is triggered every night, and tests are run against current versions of the shell app.

Second use case

Thanks to these scheduled runs, we can receive notifications if a new version of Cockpit is not compatible with our plugin, allowing us to take the actions mentioned above.

Of course, it doesn’t make sense to test the same version of the plugin with the same version of the shell repeatedly. That’s why our CRON job creates a cache of test results, so if no new version of the plugin or shell is released, the test case can be skipped.

The power of cumulocity-cypress-ctrl

You might wonder why we use the cumulocity-cypress-ctrl package instead of simply using the http-server package to host the shell and plugin, letting Cypress handle running test suites. There are several compelling reasons. Read full topic here to find out what they are.