PlatformEngr® Backstage course

Module 06 · Golden paths and docs · 60 min · You are: Platform team

Golden paths with software templates

In this module

  • You learn: How a template is built: parameters (a form), steps (actions run on the backend) and outputs, plus the template editor's dry run.
  • You do: Add the platform's template and its skeleton, register it, dry-run it in the editor, then create a real cart service from it.
  • You end with: The portal offers one template, Node.js service. A cart repo exists on GitHub, created by the template and already in the catalog, owned by team-checkout.

Every team that starts a service by copying an old repo copies its mistakes too. A template makes the right way the easy way: the owner, the system and the docs are there from the first commit, and the service is in the catalog before anyone forgets. The platform team owns the template; product teams only fill in a form.

Open this module in the interactive course to track progress.

Concepts

Parameters
JSON Schema with ui: hints. Each property becomes parameters.<name>. Pickers like OwnerPicker read the catalog.
Steps and actions
Each step runs one action on the backend: fetch:template renders files, publish:github creates a repo, catalog:register adds it. Outputs feed later steps.
Skeleton
Files with ${{ values.x }} placeholders, rendered by fetch:template with Nunjucks.
Dry run
The template editor runs the steps without side effects and shows the files, log and output. Actions that do not support dry run are skipped.

Files to know

Open them in your editor or on GitHub; the bullets say what to look at.

Steps

1. Add the template

Copy the skeleton from the reference; write the template yourself so you read every part.

mkdir -p ~/work/portal/templates/node-service && cp -R ~/work/backstage-workshop/portal/templates/node-service/skeleton ~/work/portal/templates/node-service/

Create ~/work/portal/templates/node-service/template.yaml

# Owned by the platform team. The golden path for a new Node.js service.
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
  name: node-service
  title: Node.js service
  description: A new Node.js service with docs, an owner and a place in the catalog.
  tags: [nodejs, recommended]
spec:
  owner: group:default/platform
  type: service

  # One page of the form per item. Each property becomes parameters.<name>.
  parameters:
    - title: About the service
      required: [name, owner, system]
      properties:
        name:
          title: Name
          type: string
          description: Lowercase, dashes allowed. Also the repository name.
          ui:field: EntityNamePicker
          ui:autofocus: true
        description:
          title: Description
          type: string
        owner:
          title: Owner
          type: string
          ui:field: OwnerPicker
          ui:options:
            catalogFilter:
              kind: Group
              spec.type: team
        system:
          title: System
          type: string
          ui:field: EntityPicker
          ui:options:
            catalogFilter:
              kind: System
    - title: Where the code goes
      required: [repoUrl]
      properties:
        repoUrl:
          title: Repository
          type: string
          ui:field: RepoUrlPicker
          ui:options:
            allowedHosts: [github.com]
            allowedOwners: [YOUR_GITHUB_USER]

  # Steps run in order on the backend. Each one can read earlier outputs.
  steps:
    - id: fetch
      name: Render the skeleton
      action: fetch:template
      input:
        url: ./skeleton
        values:
          name: ${{ parameters.name }}
          description: ${{ parameters.description }}
          owner: ${{ parameters.owner }}
          ownerTitle: ${{ parameters.owner }}
          system: ${{ parameters.system }}
          repoUrl: ${{ parameters.repoUrl }}

    - id: publish
      name: Create the repository
      action: publish:github
      input:
        repoUrl: ${{ parameters.repoUrl }}
        description: ${{ parameters.description }}
        defaultBranch: main
        repoVisibility: public

    - id: register
      name: Add it to the catalog
      action: catalog:register
      input:
        repoContentsUrl: ${{ steps['publish'].output.repoContentsUrl }}
        catalogInfoPath: /catalog-info.yaml

  output:
    links:
      - title: Repository
        url: ${{ steps['publish'].output.remoteUrl }}
      - title: Open in catalog
        icon: catalog
        entityRef: ${{ steps['register'].output.entityRef }}

2. Register it, drop the example

Templates are entities too. Only the platform's own location may add them.

Change in ~/work/portal/app-config.yaml
Under catalog.locations.

-     # Local example template
-     - type: file
-       target: ../../examples/template/template.yaml
-       rules:
-         - allow: [Template]
+     # Platform-owned software templates (module 06).
+     - type: file
+       target: ../../templates/node-service/template.yaml
+       rules:
+         - allow: [Template]

Save. Expect node-service only:

curl -s -H "Authorization: Bearer $PORTAL_API_TOKEN" "localhost:7007/api/catalog/entities?filter=kind=template" | jq -r '.[].metadata.name'

3. Dry-run it in the template editor

Test a template before anyone uses it. The editor reads the folder from your disk, so it needs Chrome or Edge.

1. Open Create, then the menu (⋮) at the top right, then Template Editor. 2. Load Template Directory and pick ~/work/portal/templates/node-service. 3. Fill the form: name dry-run-test, owner Team Checkout, system shop, repository owner YOUR_GITHUB_USER, name dry-run-test. 4. Click Create. Nothing is created: it is a dry run. 5. Files shows the rendered skeleton: open catalog-info.yaml and find owner: group:default/team-checkout and system: system:default/shop. Log shows each step. Output shows the links.

4. Ship it

cd ~/work/portal && git status --short

Propose it as a pull request. Put it on its own branch and push it:

git switch -c template-node-service && git add -A && git commit -qm "Templates: Node.js service golden path"
git push -qu origin HEAD

Open the pull request. It prints a link:

gh pr create --fill

Open that link in your browser. Files changed shows exactly your diff.

Merge it on GitHub (Squash and merge, then Delete branch), or from the terminal:

gh pr merge --squash --delete-branch

Either way, bring your copy up to date:

git switch -q main && git pull -q

5. As team-checkout: create a service

Now for real. publish:github uses the token in .env to create the repo.

1. Create → Node.js service → Choose. 2. Name cart, description The shopping cart, owner Team Checkout, system shop. Next. 3. Repository owner YOUR_GITHUB_USER, name cart. Review, then Create. 4. Watch the three steps turn green, then click Open in catalog.

The repo exists. Expect cart and its description:

gh repo view YOUR_GITHUB_USER/cart --json name,description

And the catalog knows its owner and system. Expect ownedBy group:default/team-checkout and partOf system:default/shop:

curl -s -H "Authorization: Bearer $PORTAL_API_TOKEN" localhost:7007/api/catalog/entities/by-name/component/default/cart | jq -r '.relations[] | .type + " " + .targetRef'

Simplified for the course

The template creates public repos under your user. In a company, it creates private repos in the organisation, adds branch protection and CODEOWNERS, and the template itself has tests that run a dry run in CI.

Break it on purpose: A tidy-up in the template

Someone on the platform team removes a line from the template's fetch step that "looked unused". The template still loads and runs.

You practise: using the dry run to find a value that does not reach the skeleton.

1. Break it

Delete from ~/work/portal/templates/node-service/template.yaml
In the fetch step's values.

-           system: ${{ parameters.system }}

Dry-run the template again in the editor (reload the directory first) with the same values.

2. What you see

Expected: system: system:default/shop in the rendered catalog-info.yaml. You get: system: with nothing after it. The form still asks for a system; the template does not complain.

3. Fix it yourself

Find where the system value gets lost between the form and the skeleton, and fix it. Prove it with a dry run.

Hint 1

The skeleton reads values.system. Where do values come from?

Hint 2

Compare the parameters with the values of the fetch step: one is missing.

The fix

Add to ~/work/portal/templates/node-service/template.yaml in the fetch step's values, after ownerTitle

          system: ${{ parameters.system }}

Dry-run again: system: system:default/shop is back.

After you fixed it: the five questions
What failed?
A form field was no longer passed to fetch:template, so the skeleton rendered an empty value.
How was it detected?
In the dry run's Files tab. In production it would show up as services without a system.
Who responds?
The platform team: they own the template.
How is it corrected?
Pass the value again and dry-run.
How is it prevented?
Run a dry run of every template in CI and assert key fields of the rendered catalog-info.yaml.

Quiz

  1. Where do template steps run?
    • On the backend, with the backend's credentials, one action per step
    • In the user's browser, with their GitHub session
    • In GitHub Actions in the new repo
  2. What does a dry run in the template editor do?
    • Creates the repo, then deletes it again
    • Runs the steps without side effects and shows the rendered files, log and output
    • Checks only that the YAML is valid
  3. Why does the owner field use OwnerPicker with a catalogFilter?
    • It makes the form load faster
    • GitHub requires it to create a repo
    • It only offers teams that exist in the catalog, so a typo like team-chekout is impossible
Answers
  1. On the backend, with the backend's credentials, one action per step The scaffolder backend runs the actions; that is why publish:github uses the integration token.
  2. Runs the steps without side effects and shows the rendered files, log and output Actions that support dry run simulate; others are skipped.
  3. It only offers teams that exist in the catalog, so a typo like team-chekout is impossible Pickers read the catalog. That prevents the module 03 break at the source.

Field note

The first template a platform team ships should be boring: the service type most teams create most often. A great template nobody needs teaches the company that templates are a toy.

Report a problem with this module