Internal Developer Platforms Series – Part 16: Backstage Software Templates III

In the last two blog posts, we have introduced Software Templates including Backstage and Scaffolder. When using Scaffolder, you can easily create custom workflows and design some user interface forms with the built-in mechanism. Backstage already provider a lot of built-in actions that can be used in Scaffolder workflows. However, from time to time, the built-in mechanism are not sufficient and you need to implement some custom code. While there are additional third-party actions (they can be found on the Backstage Plug-ins page: https://backstage.io/plugins), writing you own action is sometimes necessary. This will be the topic of this blog post.

Writing a Custom Action

Writing a custom action involves three steps:

  • First, you must implement your custom action
  • In a second step, the action must be registered with Backstage
  • Third, you can use the custom action from within a Scaffolder workflow.

Let’s walkthrough through these steps

Implementation of custom action

Before writing the code, you need to setup the action. Luckily, this is very easy and can be done via CLI:

$ yarn backstage-cli new
? What do you want to create?
  plugin-common - A new isomorphic common plugin package
  plugin-node - A new Node.js library plugin package
  plugin-react - A new web library plugin package
> scaffolder-module - An module exporting custom actions for @backstage/plugin-scaffolder-backend

After that a directory has been setup, a directoy has been created in the packages/backend folder. You can rename the example.ts file according to your need.

So let’s discuss the following (simple) example:

import { createTemplateAction } from '@backstage/plugin-scaffolder-node';

export const buildRepoURL = () => {
    return createTemplateAction<{ repoName: string; }>({
        id: 'sc:repoUrl:build',
        schema: {
            input: {
                type: 'object',
                required: ['repoName'],
                properties: {
                    repoName: {
                        type: 'string',
                        title: 'Repository name',
                        description: 'Name of the repo',
                    },
                },
            },
            output: {
                type: 'object',
                properties: {
                    repoUrl: {
                        title: "Repository url",
                        description: "Repository url in format https://<TOKEN>@github.com/<USER>/<REPONAME>",
                        type: "string",
                    },
                }
            }
        },
        async handler(ctx) {
            let repo = ctx.input.repoName
            let secret = process.env.GITHUB_TOKEN
            ctx.output('repoUrl', 'https://'+secret+'@github.com/gsoeldner/'+repo)
        },
    });
};

First, you have to import the creatTemplateAction-action.

In the following codeblock we define a custom action, called buildRepoURL and define the id, which can be used in the Scaffolder workflow later (sc:repoURL:build). The schema block defines the input and the output and is based on jsonschema. The logic itself is in the handler function. In our use case, we grap the repoName input, read the GITHUB_TOKEN environment variable and construct the output.

For the schema, you can alternatively use the zod library. The zod library is a TypeScript-first schema declaration and validation library. It allows you to define schemas for your data models and then validate data against those schemas. Zod is commonly used in React applications to ensure that the data being used in components is of the expected shape and type. It integrates well with TypeScript, providing strong type inference and runtime validation.

Register action

In the following, we describe how to register an action with the new backend system. If you are using the old one, please follow the instructions in the documentation..

Now you should create a new module.ts file in your plugin and add your action there. The following shows an example.

import { scaffolderActionsExtensionPoint } from '@backstage/plugin-scaffolder-node/alpha';
import { createBackendModule } from '@backstage/backend-plugin-api';
import {
  buildRepoURL,
} from './actions';

export const gitopsScaffolderModuleCustomExtensions = createBackendModule({
  pluginId: 'scaffolder',
  moduleId: 'sc-build-repo-url,
  register(env) {
    env.registerInit({
      deps: {
        scaffolder: scaffolderActionsExtensionPoint,
      },
      async init({ scaffolder }) {
        scaffolder.addActions(createBuildRepoURL());
      },
    });
  },
});

Last, but not least, you can navigate to the packages/backend/src/index.ts file and load the module. This can be done by adding a line similar like this:

backend.add(scaffolderModuleCustomExtensions());

Use the Action

Finally, we can use the action within a Scaffolder template:

    - id: build-repo-url
      name: "Fetch Repository information"
      action: sc:repoURL:build
      input:
        repoName: ${{ parameters.repoName }}

This concludes our blog post.

Autor

Dr. Guido Söldner

Geschäftsführer

Guido Söldner ist Geschäftsführer und Principal Consultant bei Söldner Consult. Sein Themenfeld umfasst Cloud Infrastruktur, Automatisierung und DevOps, Kubernetes, Machine Learning und Enterprise Programmierung mit Spring.