Migrations of Apps and Groups

As of system version 5.7.0, app developers can include JavaScript migration scripts within their bundles to handle major updates without disrupting users' parameterized Tasks. These scripts enable version-specific or general migrations for Apps and Groups, allowing for seamless updates and enhancements.

Without migration scripts, the system updates existing Task as follows:

  • Newly added parameters are assigned default values specified in the .lf file to all existing Tasks. If the parameter defaults to nil, a previously fully configured App is no longer fully parameterized. This is usually a breaking change for the user, since re-parameterization of all occurrences of the App is required, before the affected Tasks can be executed.

  • Newly removed parameters are deleted from all existing Tasks of a user.

Migration scripts allow to handle further scenarios, such as:

  • Adding new parameters with dependencies to other parameters of the same App or new parameters to an existing struct.

  • Renaming or reducing the value range of parameters.

  • Modifying parameter fields of child Apps of a Group G, in case they previously were access paths referencing G.

By implementing migration scripts, you can confidently update and enhance your Apps while providing a seamless experience for Desk users.

Migration scripts will be executed whenever a new version of a bundle is installed or when a Task is imported and its bundle versions differ from the currently installed bundle.

The migration scripts are placed in the migrations folder of the bundle:

.
└── bundles
    └── test
        ├── manifest.json
        ├── migrations
        │   ├── index.mjs
        │   └── utils.mjs
        ├── resources
        └── sources
            └── Test.lf

During migration, you can use only the resources located in the migrations folder and the manifest.json. The process runs in a sandboxed environment without external connections. However, you can leverage shared migration functions, see import-other-migration-scripts for further information.

Migration scripts

The migrations folder must contain an index.mjs file with a default export. The default export must be an object with state machine names as keys and corresponding migration functions as values. The migration function must be an async function receiving the element to be migrated and a list of parent elements, where the first element in the list is the direct parent:

(element: App | Group, parents: (App | Group)[]) => Promise<void>

Note

A migration function must not run longer than 1 second, otherwise it will timeout and migration for this element will be aborted.

When migrating a Task, the system will traverse all elements, i.e. all contained Groups and Apps, and for each element call a corresponding migration function, if it exists. The elements are traversed depth-first, therefore, when a migration function is executed, it is ensured that all parents have already been migrated.

If the element to be migrated is an App it will contain the following fields:

interface App {
  id: string
  name: string
  link: string
  bundle: {
    name: string
    version: string
  }
  parameter: any
}

If the element to be migrated is a Group it will contain the following fields:

interface Group {
  id: string
  name: string
  libraryItemId: string
  bundle: {
    name: string
    version: string
  }
  parameter: any
  containers: [{ elements: (App | Group | TimelineLink)[] }]
}

The children of Groups can contain timeline links, which are links to other Tasks, however they will not be migrated. They are just included for completeness. A timeline link will contain the following fields:

interface TimelineLink {
  id: string
  name: string
  timelineLink: string
}

An element can be migrated by simply modifying the fields of the given element. The following fields are modifiable:

  • name

  • link for Apps

  • libraryItemId for Groups

  • parameter

  • parameter of children for Groups, but only fields that contain an access path to the Group to be migrated

Modifying any other field will have no effect.

Usually, the bundle version will contain the version of the currently installed bundle. For Tasks that are migrated from a system with version < 5.7.0 the bundle name will be "unknown" and the version will be "0.0".

Note

Whenever the bundle version changes all registered migration functions will potentially be executed, even though the corresponding App or Group might not have changed. Therefore it is crucial that migration scripts take the bundle version into account.

One way to ensure this is as follows:

async function migrate_my_app(element) {
  if (element.bundle.version === '1.0') {
    // perform migration from version 1.0 to current version
  }
}

Warning

Migration scripts must not read from stdin.

The system uses stdin and stdout to communicate with the migration process. Reading from stdin might interfere and cause migration to fail.

Warning

When a migration script fails the action that triggered the task migration will fail, too. A migration script will fail, for example, because it throws an error, the migration function is not async, or migration function takes too long to execute. Actions that will trigger task migration are bundle installation and task import.

Importing other migration scripts

Migration scripts can reuse functionality from other bundles by importing the corresponding functions or variables.

For instance, a utils.mjs file placed in bundle A can be utilized in a migration script in bundle B. To ensure that utils.mjs from bundle A is available during the execution of bundle B's migration script, you need to declare a dependency on bundle A in the manifest of bundle B.

{
  "name": "B",
  "version": "1.0",
  "dependencies": ["A"],
  "globalComponents": []
}

You can then import functions from utils.mjs:

import { some_function_in_A } from 'A/migrations/utils.mjs'

Example

Consider the following App:

my_app {
  ...

  parameterType {
    string param_1;
    string param_2;
    float param_3; -- value in [0, 1]
  }

  ...
}

It is contained in some bundle with version "1.0". Assume we want to change the parameter type in version "1.1" to the following:

my_app {
  ...

  parameterType {
    string new_param_with_default;
    string new_param_without_default;
    string new_param_2;
    float param_3; -- value in [0, 100]
  }

  ...
}

We want to add two fields new_param_with_default and new_param_without_default, delete param_1, rename param_2 to new_param_2 and modify the value range of param_3. This could be accomplished by adding the following migration script:

const migrations = {
  my_app: migrate_my_app,
}

async function migrate_my_app(element) {
  // check that current version of the given element is 1.0
  if (element.bundle.version === '1.0') {
    // logic for migrating element to version 1.1:

    // add a new parameter field with default value
    element.parameter.new_param_with_default = "foo"

    // add a new parameter field without default value
    element.parameter.new_param_without_default = null

    // delete a parameter field
    delete element.parameter.param_1

    // rename a parameter field
    element.parameter.new_param_2 = element.parameter.param_2
    delete element.parameter.param_2

    // modify the value of a parameter field
    element.parameter.param_3 = element.parameter.param_3 * 100
  }
}

export default migrations