Writing scaffolder templates
Published on May 16th, 2022Overview
The Roadie Backstage scaffolder is a feature that allows you to define software templates to create new software projects, update existing ones or simply perform repeated tasks in a consistent manner.
Scaffolder templates are defined in YAML files and loaded into the Backstage catalog in the same way that other entities are loaded into Backstage. A template contains one or more steps
which run sequentially during execution.
A Scaffolder template is then run on demand by the users of Backstage to execute the software template. Roadie will execute the software template in an ephemeral container that is destroyed after the execution completes.
You can find a step by step guide to adding templates in Roadie here
See the Backstage Scaffolder in action
Request a Roadie demoComponents of a Template
A Scaffolder template is a configurable process that will run one or more Scaffolder steps
. The template will be run when a user visits the “Create Component” page in Backstage. https://<tenant-name>.roadie.so/create
.
Templates are defined by a Backstage Entity YAML file with a Template
kind and imported into the Backstage catalog. You can create multiple templates, each of which can perform a different set of steps. For example, you can have one template that creates a React application, and another that creates a serverless app.
Template YAML input forms can be tested at /templates/edit
using a live template preview viewer.
Here is an example of a very basic Scaffolder template that prompts the user for a name, and then prints back the text “Hello, name!”
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: hello-world-template
title: Hello World
description: Says Hello to a specified name.
spec:
owner: backstage/techdocs-core
type: service
parameters:
- title: You are about to say hello to your first Backstage Template
required:
- name
properties:
name:
type: string
steps:
- id: log-message
name: Log Message
action: debug:log
input:
message: 'Hello, ${{ parameters.name }}!'
Header Section
The header section is required for every template
and contains information to configure the task and show details about the task on the “Create Component” page.
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: hello-world-template
title: Hello World
description: Says Hello to a specified name.
spec:
owner: default/engineering
type: service
apiVersion
This is a required field and should be set to scaffolder.backstage.io/v1beta3
kind
A Scaffolder template is also an Entity in Backstage. In order to configure this entity as a template you must set the kind to Template
metadata
The metadata field contains some data that appears on the template card that appears on the “Create Component” page.
spec
The spec field contains owner
and type
. Owner refers to the Backstage group or user that owns the Scaffolder task e.g. default/engineering
. Type refers to the type of template. It can be set to anything and appears on the scaffolder template card in the “Create Component” page.
parameters
The parameters property is a list of parameters that can be prompted from the user when they run a template. Each array element contains the configuration for a single page of items to be filled by the user running the template. The parameter pages must contain title
, required
and properties
.
The parameters yaml is based on react-jsonschema-form. You can find the available syntax options there and examples here.
You can choose to break up the parameter prompting into form steps
or collect all the parameters in one single step.
Each parameter can be one of a few types: string
, number
, array
or object
.
Here is the most basic example:
parameters:
properties:
name:
type: string
Validation
You can use react-jsonschema-form to perform validation on input fields using a regex or character counts.
parameters:
properties:
name:
title: Simple text input
type: string
description: Description about input
maxLength: 8
pattern: '^([a-zA-Z][a-zA-Z0-9]*)(-[a-zA-Z0-9]+)*$'
ui:autofocus: true
ui:help: 'Hint: additional description...'
string
You may collect text data from the user by using the string type. Here is the most basic example. It will prompt the user for a name.
parameters:
properties:
name:
type: string
Entity picker
You can prompt the user with a list of catalog entities using the ui:field: EntityPicker
option as follows:
parameters:
properties:
entity:
type: string
ui:field: EntityPicker
Owned entity picker
Alternatively if you would like the user to only select entities that they already own, you might want to use the OwnedEntityPicker.
parameters:
properties:
ownedEntity:
type: string
ui:field: OwnedEntityPicker
Entity name picker
If you would like a little validation when the user enters an Entity name, you can use the EntityNamePicker. It will prevent the user from entering an entity name that is not an acceptable entity name.
parameters:
properties:
ownedEntity:
type: string
ui:field: EntityNamePicker
Repository picker
The respository picker can allow the user to select the name and location of a new repository. The picker restricts the target location of the repository to make it a little easier for the user to select a target location.
The following example, will only allow the user to enter a new repository name targeting the GitHub using the AcmeInc organization.
parameters:
properties:
repoUrl:
type: string
ui:field: RepoUrlPicker
ui:options:
allowedHosts:
- github.com
allowedOwners:
- AcmeInc
The RepoUrlPicker
uses the allowedHosts
to decide how to build the repo url output value. If you use bitbucket.org
it will output a valid repo url for Bitbucket.
parameters:
properties:
repoUrl:
type: string
ui:field: RepoUrlPicker
ui:options:
allowedHosts:
- bitbucket.org
Owner picker
The owner picker, allows the user to select a user / group in the Backstage catalog. e.g.
parameters:
properties:
owner:
type: string
ui:field: OwnerPicker
This returns a variable in the format group:<namespace>/<group-or-user-name>
. You can extract the entity name using replace when you refer to the parameter like so: ${{ parameters.owner | replace(\"group:.*/\", \"\") }}
Picker from external API source
This custom scaffolder field, makes an API call to the Backstage backend and allows the result to be rendered to a list.
parameters:
properties:
custom:
title: custom
type: string
description: Custom field from external API
# Use `SelectFieldFromApi` to configure the select field for the entry.
ui:field: SelectFieldFromApi
ui:options:
# The Path on the Backstage API and the parameters to fetch the data for the dropdown
path: 'catalog/entity-facets'
params:
facet: 'kind'
# This selects the array element from the API fetch response. It finds the array with the name kind
# under the facets object
arraySelector: 'facets.kind'
# (Optional) This selects the field in the array to use for the value of each select item. If its not specified
# it will use the value of the item directly.
valueSelector: 'count'
# (Optional) This selects the field in the array to use for the label of each select item.
labelSelector: 'value'
Some of the SelectFieldFromApi
options allow using parameters from earlier parameter pages to be used to template the options. The templated options are params
, path
, valueSelector
and labelSelector
. e.g.
parameters:
- title: Select an Entity kind
required:
- kind
properties:
kind:
title: Kind
type: string
enum:
- template
- location
default: template
ui:autofocus: true
ui:options:
rows: 5
- title: Select the specific entity you want
properties:
obj:
title: custom
type: string
description: Entity Selector
ui:field: SelectFieldFromApi
ui:options:
path: "catalog/entities"
params:
filter: "kind={{ parameters.kind }}"
valueSelector: "metadata.name"
labelSelector: "metadata.description"
number
You can allow the user to enter a number using the number
type:
parameters:
properties:
size:
type: number
object
The object
allows the collection of more complex types of data from the user. It contains the properties
option to add variables to the object as follows:
parameters:
properties:
person:
type: object
properties:
name:
type: string
age:
type: number
You may choose to make an object property to be mandatory using the required
property.
parameters:
properties:
person:
type: object
required:
- name
properties:
name:
type: string
age:
type: number
array
You can prompt for an array of properties using the array option. The items
option can be any type: array
, object
, string
or number
as you like.
parameters:
properties:
languages:
type: array
items:
type: string
If you would like to prompt the user to add entity tags, you can use the ui:field: EntityTagPicker
as shown below.
parameters:
properties:
entityTags:
type: array
ui:field: EntityTagsPicker
Outputs
Parameters can be retrieved later on by steps using parameter outputs. Here is an example of a parameter name
being used by a debug:log
step.
parameters:
properties:
name:
type: string
steps:
- id: log-message
name: Log Message
action: debug:log
input:
message: 'Hello, ${{ parameters.name }}!'
If you need to reference elements of an array parameter you can refer to them using the following syntax:
steps:
- id: log-message
name: Log Message
action: debug:log
input:
message: 'Hello, ${{ parameters.names[0] }}!'
An object
parameter values can be reference in the way you might expect.
steps:
- id: log-message
name: Log Message
action: debug:log
input:
message: 'Hello, ${{ parameters.person.name }}!'
Common Options
If you would like to default the value of a field you can use the default
option:
parameters:
properties:
name:
type: string
default: 'world!'
If you would like to prompt the users for a fixed list of options, you may use the enum
option.
parameters:
properties:
size:
type: number
enum: [50, 100, 200]
You can display a more human description to a field value by using title
and description
parameters:
properties:
name:
type: string
title: 'Name'
description: 'Name to say hello to'
Form Steps
It might be jarring for your user to enter a lot of parameters one after another on the same page, especially if some properties require validation. As such Backstage have provided form steps.
You can make use of form steps using the following example.
parameters:
- title: 'Fill in the Name'
properties:
name:
type: string
- title: 'Fill in the Age'
properties:
age:
type: number
Previewing parameters
Template Preview, which is accessible via Tools > Template Preview
provides a preview page for templates, where you can see a live preview of the template form. This is done in order to provide an easy way to preview scaffolder template form UIs without running your own local instance of the plugin or committing changes to the template.
More Reading
You can read more about parameter configuration in the official backstage docs here.
steps
Steps define the actions that are taken by the scaffolder template when it is run as a task. The scaffolder initially creates a temporary directory referred to as the workspace, in which files are downloaded, generated, updated and pushed to some external system. Each step that is defined is run in order.
Step Inputs
Parameter Values
You can refer to the value of a parameter using the syntax ${{ parameters["name"] }}
e.g.
steps:
- id: log-message
name: Log Message
action: debug:log
input:
message: 'Hello, ${{ parameters["name"] }}'
If the parameter id does not contain a special character you can also refer to it using the dot syntax ${{ parameters.name }}
Outputs from previous steps
You can refer to the output of a previous step using the following syntax:
${{ steps["publish-step-id"].output.repoContentsUrl }}
If the step id does not contain a special character you can also refer to it using the dot syntax.
${{ steps.publish.output.repoContentsUrl }}
Looping
You can use array type form inputs or step outputs to repeat action steps multiple times. e.g.
- id: log-description
name: Log Message
each: ['Brian', 'Ian']
action: debug:log
input:
message: 'Hello, ${{ each.value }}!'
Accessing the logged in user
You can refer to the user entity reference for the logged in user using the following syntax:
${{ user.ref }}
If this entity reference exists in the Backstage Catalog, you can also make use of the details contained within the users entity by using the following:
${{ user.entity.metadata.name }}
or access the details contained within the user’s profile.
${{ user.entity.spec.profile.email }}
Actions
You can find all the available actions to your Roadie instance by visiting the following page from within Roadie:
https://<tenant-name>.roadie.so/templates/actions
Conditional Steps
You can conditionally execute a scaffolder action based on an input parameter.
steps:
- action: debug:log
id: debug-log
if: ${{ parameters.name }}
name: Log Hello World
input:
message: 'Hello, ${{ parameters.name }}!'
Advanced
Calling an internal API
If you need a scaffolder step to contact a custom authenticated service or any public API for that matter that is not currently supported by a built-in action, you can do that using a combination of the http:backstage:request
action and a backstage proxy configuration.
Start by creating a proxy configuration as described in this page
Then you can add a step to call that API using the http:backstage:request
action as follows:
steps:
- action: http:backstage:request
id: http-request
name: Create a thing on the acme service
input:
method: POST
path: "/api/proxy/acme/thing"
- action: debug:log
id: log-result
name: Log the result of creating the thing
input:
message: "The response code was ${{ steps["http-request"].output.code }}'
Escaping syntax
If you need to pass variable substitution syntax through without it being interpreted you can escape the syntax by wrapping it like so ${{ '${{ parameters.something }}' }}
.
Creating re-usable snippets
You can inject in re-usable snippets of yaml into a template using the $yaml
operator like so:
templates/debug-step.yaml
- name: Debug log 2
id: debug_log_2
action: 'debug:log'
input:
message: Second log
logging-template.yaml
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: placeholder-example
title: Demonstrating the placeholder usage
description: Shows how to inject in a single re-usable step
spec:
owner: default/engineering
type: service
steps:
- name: Debug log 1
id: debug_log_1
action: 'debug:log'
input:
message: First log
$yaml: https://github.com/yourOrg/some-repo/blob/templates/debug-step.yaml
NB: This can only be done for a single step as the re-usable section must be valid yaml.
Using a user’s Github Token to execute template steps
You can use the user that runs the scaffolder template to open a PR or other Github based actions rather than opening it on behalf of the Roadie Github App by specifying the token field. The token must first be injected via the parameters by the RepoUrlPicker parameter as documented here
parameters:
- title: Choose a location
required:
- repoUrl
properties:
repoUrl:
title: Repository Location
type: string
ui:field: RepoUrlPicker
ui:options:
# Here's the option you can pass to the RepoUrlPicker
requestUserCredentials:
secretsKey: USER_OAUTH_TOKEN
additionalScopes:
github:
- workflow
allowedHosts:
- github.com
steps:
- action: publish:github:pull-request
id: create-pull-request
name: Create a pull request
input:
repoUrl: 'github.com?repo=reponame&owner=AcmeInc'
branchName: ticketNumber-123
title: 'Make some changes to the files'
description: 'This pull request makes changes to the files in the reponame repository in the AcmeInc organization'
# here's where the secret can be used
token: ${{ secrets.USER_OAUTH_TOKEN }}