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
| Capability | Where |
|---|---|
| Interactive golden path | prompt steps with CLI pre-answers |
| Input validation | regexMatch kebab-case assert |
| Idempotency guard | guard_unique refuses to overwrite |
| Language variants | switch on the language answer |
| Day-one discoverability | catalog registration + verification |
| Self-validating output | scaffold 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=pythonVerified 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.