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:

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.

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.

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:

1. Built-in commands

cumulocity-cypress-ctrl provides a set of predefined commands and takes care of authorization, application language, and other Cumulocity-specific concerns.

2. Ability to pluck shell version and pick the right test suite

Imagine your plugin is compatible with different versions of the shell, e.g., 1018 and 1020, but their layouts are slightly different, and you want to provide different test cases for each version. With cumulocity-cypress-ctrl, you can easily declare which shell app versions a particular test case supports:

it('config component should be present v1020',
  { requires: { shell: ['1020.x.x'] } },
  () => {...}
);

it('config component should be present v1018',
  { requires: { shell: ['>=1018.0.0 <1020.0.0'] } },
  () => {...}
);

In the example above, we provided semver ranges as strings (though it’s actually an array of semver ranges). If we run tests against shell version 1018, the ctrl package will read the shell version, and only the second test case will be picked and executed. The first one doesn’t match the shell version, so it will be skipped.

3. Recording and mocking API requests

One of the most powerful features of cumulocity-cypress-ctrl is its ability to create recordings of API requests and then use these recordings to run Cypress tests offline. Recordings only need to be created manually once, after which we can reuse them. This ensures our test runs are not affected by downtime of any endpoint and are much quicker to execute.

Conclusion

The introduction of version matrices and automated E2E testing has significantly improved the development and maintenance of micro frontends for Cumulocity IoT. Key takeaways include:

  • Version matrices allow plugin developers to clearly specify compatibility with different versions of the Cumulocity Web SDK and API, helping prevent incompatibility issues when installing plugins.
  • Automated E2E testing with Cypress enables testing plugins against multiple versions of the Cumulocity shell applications, catching compatibility problems early in the development process.
  • The custom GitHub Actions workflow automates the process of collecting shell versions, building plugins, and running tests, providing continuous validation of plugin compatibility.
  • The cumulocity-cypress-ctrl package enhances Cypress testing capabilities specifically for Cumulocity, with features like version-specific test cases and API request recording/mocking.
  • Together, these tools and practices allow plugin developers to confidently support multiple Cumulocity versions while catching incompatibilities early, improving the overall reliability and maintainability of the micro frontend ecosystem.

By leveraging these capabilities, developers can create more robust plugins that work seamlessly across Cumulocity versions, enabling the continued growth and adoption of micro frontends within the Cumulocity IoT platform.

Related resources

4 Likes