DOC_INDEX
THEME
DOCS/Use Cases/Service Scaffolding

Service Scaffolding

A platform golden path - new microservices created the standard way, registered in a catalog, validated by their own smoke test

Platform teams do not scale by reviewing every new repo - they scale by making the right way the easy way. This golden path prompts for a name and language, enforces naming standards, scaffolds the repo with the team's conventions baked in, registers it in a service catalog, and proves the result passes its own smoke test.

Executed for real against OrchStep v0.7.1 - all three languages, plus both guard failure paths.

What it demonstrates

CapabilityWhere
Interactive golden pathprompt steps with CLI pre-answers
Input validationregexMatch kebab-case assert
Idempotency guardguard_unique refuses to overwrite
Language variantsswitch on the language answer
Day-one discoverabilitycatalog registration + verification
Self-validating outputscaffold runs its own make build/test

The workflow

name: service-scaffold
desc: "Platform golden path: scaffold a new microservice repo with standards baked in"

defaults:
  workspace: "./services"
  catalog: "./services/catalog.yml"
  team: "payments"

tasks:
  new_service:
    desc: "Create a service the standard way (interactive; flags work too)"
    steps:
      - name: service_name
        func: prompt
        args:
          message: "Service name (kebab-case)"
          type: text
          default: "payments-worker"

      - name: language
        func: prompt
        args:
          message: "Language?"
          type: select
          options: [go, python, node]
          default: "go"

      - name: validate_name
        func: assert
        args:
          condition: '{{ regexMatch "^[a-z][a-z0-9-]+$" steps.service_name.value }}'
          desc: "service names are kebab-case"

      - name: guard_unique
        desc: "Refuse to scaffold over an existing service"
        func: shell
        do: |
          # NOTE: multi-line do returns the LAST command's exit code, so the
          # guard must be chained with && (or use `set -e`).
          test ! -d {{ vars.workspace }}/{{ steps.service_name.value }} && echo "unique=yes"

      - name: scaffold_common
        func: shell
        do: |
          DIR={{ vars.workspace }}/{{ steps.service_name.value }}
          mkdir -p $DIR/src
          cat > $DIR/README.md <<R
          # {{ steps.service_name.value }}

          Owned by team {{ vars.team }}. Scaffolded by the golden path.

          ## Runbook
          - deploy: orchstep run deploy --env staging
          - oncall: see incident-runbook
          R
          cat > $DIR/Makefile <<R
          .PHONY: build test
          build:
          	@echo "building {{ steps.service_name.value }}"
          test:
          	@echo "testing {{ steps.service_name.value }}"
          R
          echo "dir=$DIR"
        outputs:
          dir: '{{ result.output | regexFind "dir=(.+)" }}'

      - name: scaffold_language
        switch:
          value: "{{ steps.language.value }}"
          cases:
            - when: go
              then:
                - name: go_files
                  func: shell
                  do: |
                    cat > {{ steps.scaffold_common.dir }}/src/main.go <<R
                    package main
                    func main() { println("{{ steps.service_name.value }} up") }
                    R
                    echo "entry=src/main.go"
                  outputs: { entry: '{{ result.output | regexFind "entry=(.+)" }}' }
            - when: python
              then:
                - name: py_files
                  func: shell
                  do: |
                    cat > {{ steps.scaffold_common.dir }}/src/main.py <<R
                    print("{{ steps.service_name.value }} up")
                    R
                    echo "entry=src/main.py"
                  outputs: { entry: '{{ result.output | regexFind "entry=(.+)" }}' }
            - when: node
              then:
                - name: js_files
                  func: shell
                  do: |
                    cat > {{ steps.scaffold_common.dir }}/src/main.js <<R
                    console.log("{{ steps.service_name.value }} up");
                    R
                    echo "entry=src/main.js"
                  outputs: { entry: '{{ result.output | regexFind "entry=(.+)" }}' }
        outputs:
          entry: '{{ steps.go_files.entry | default steps.py_files.entry | default steps.js_files.entry }}'

      - name: init_repo
        func: shell
        do: |
          cd {{ steps.scaffold_common.dir }}
          git init -q -b main
          git config user.email platform@orchstep.dev && git config user.name "Golden Path"
          git add -A && git commit -qm "scaffold {{ steps.service_name.value }} ({{ steps.language.value }})"
          git log --oneline | head -1

      - name: register_in_catalog
        desc: "Every service is discoverable from day one"
        func: shell
        do: |
          mkdir -p {{ vars.workspace }}
          touch {{ vars.catalog }}
          cat >> {{ vars.catalog }} <<R
          - name: {{ steps.service_name.value }}
            team: {{ vars.team }}
            language: {{ steps.language.value }}
            entry: {{ steps.scaffold_language.entry }}
          R
          grep -c "name: {{ steps.service_name.value }}" {{ vars.catalog }}

      - name: smoke
        desc: "The scaffold must pass its own standards"
        func: shell
        do: |
          cd {{ steps.scaffold_common.dir }}
          make build && make test
          test -f README.md && test -f {{ steps.scaffold_language.entry }}
          echo "smoke=pass"
        outputs:
          smoke: '{{ result.output | regexFind "smoke=([a-z]+)" }}'

      - name: summary
        func: shell
        do: 'echo "SCAFFOLDED {{ steps.service_name.value }} ({{ steps.language.value }}) at {{ steps.scaffold_common.dir }} smoke={{ steps.smoke.smoke }}"'

Run it

orchstep run new_service                                        # interactive
orchstep run new_service --var service_name=fraud-scorer --var language=python

Verified results:

SCAFFOLDED payments-worker (go) at ./services/payments-worker smoke=pass
SCAFFOLDED fraud-scorer (python) at ./services/fraud-scorer smoke=pass
# rerun with the same name -> exit 1 (guard_unique)
# --var service_name=BadName  -> exit 1 (kebab-case assert)

Design notes

Guards before generation. Name validation and the uniqueness check run before anything touches disk - a failed scaffold should leave nothing behind.

Shell exit-code gotcha (verified). A multi-line do: returns the last command's exit code, so guard steps chain with && (test ! -d $DIR && echo ok) or set set -e explicitly. Without that, a trailing echo quietly swallows the failure.

Take it to production

Replace the heredocs with your real templates (or a template repo the workflow clones), push the new repo to your forge with gh repo create, and register in your real catalog (Backstage et al). Teams get standards without tickets.