Template Repos, Golden Paths, and Scaffolding Automation
Template Repos, Golden Paths, and Scaffolding Automation
The Failure
The team created a template repository. Six months and twelve services later, the template had been updated 15 times. But the twelve existing services still used the original template. The security scanner was upgraded from Trivy v0.45 to v0.50 in the template, but all existing services ran v0.45. The performance budget was tightened in the template, but existing services had the old budget. The template only helped new services.
Template sync keeps existing services aligned with the template.
The Mechanism
Template Lifecycle
- Create: Platform team builds the template with all pipeline components
- Scaffold: Developer creates a new service from the template
- Customize: Developer adds service-specific logic (API routes, business logic)
- Sync: When the template is updated, existing services receive a PR with the changes
- Override: Services can override template defaults via configuration
Syncable vs Non-Syncable Files
| File | Syncable? | Reason |
|---|---|---|
.github/workflows/ci.yml | Yes | Pipeline gates must be consistent |
.github/workflows/security.yml | Yes | Security policy is global |
Dockerfile | Partially | Base image updates yes, app-specific stages no |
k8s/base/deployment.yaml | No | Service-specific containers |
.trivyignore | No | Service-specific exceptions |
tests/performance/budgets.yaml | No | Service-specific thresholds |
The Implementation
Template Repository Structure
acme/service-template-go/
├── .template/
│ ├── config.yaml # Template metadata
│ ├── sync-include.txt # Files to sync to existing services
│ └── placeholders.yaml # Replacement variables
├── .github/workflows/ # Synced ✓
├── k8s/base/ # Synced (partially) ✓
├── Dockerfile # Synced ✓
├── cmd/server/main.go # Not synced (service-specific)
└── ...
Template Config
# .template/config.yaml
# HARDENED: Template metadata
name: go-service
version: "3.2.0"
languages: [go]
maintainer: platform-team
placeholders:
SERVICE_NAME: "Name of the service (lowercase, hyphenated)"
TEAM_NAME: "Owning team name"
GO_VERSION: "1.22"
PORT: "8080"
sync:
include:
- ".github/workflows/*.yml"
- "Dockerfile"
- ".trivyignore.template"
- ".gitleaks.toml"
exclude:
- ".github/workflows/custom-*.yml"
- "k8s/overlays/**"
Template Sync Workflow
# acme/service-template-go/.github/workflows/sync-template.yml
# HARDENED: Sync template changes to all derived services
name: Sync Template
on:
push:
branches: [main]
paths:
- ".github/workflows/**"
- "Dockerfile"
- ".gitleaks.toml"
jobs:
sync:
runs-on: ubuntu-latest
strategy:
matrix:
repo:
- acme/catalog-service
- acme/checkout-service
- acme/inventory-service
- acme/payments-service
- acme/frontend-shell
steps:
- uses: actions/checkout@v4
with:
path: template
- uses: actions/checkout@v4
with:
repository: ${{ matrix.repo }}
token: ${{ secrets.SYNC_TOKEN }}
path: target
- name: Sync files
run: |
while IFS= read -r pattern; do
for file in template/$pattern; do
if [[ -f "$file" ]]; then
rel="${file#template/}"
mkdir -p "target/$(dirname "$rel")"
cp "$file" "target/$rel"
fi
done
done < template/.template/sync-include.txt
- name: Create sync PR
uses: peter-evans/create-pull-request@v6
with:
path: target
token: ${{ secrets.SYNC_TOKEN }}
branch: template-sync/v${{ github.run_number }}
title: "chore: sync with service template"
body: |
Automated sync from [service-template-go](https://github.com/acme/service-template-go).
Changes in this PR come from the platform template.
Review carefully — some changes may need service-specific adjustments.
labels: template-sync
reviewers: ${{ github.event.pusher.name }}
Override Mechanism
# In a service repo: .template-overrides.yaml
# Services can override template defaults
overrides:
ci:
# Use a custom test command instead of template default
test_command: "go test -tags=custom ./..."
security:
# Additional Trivy severity level
trivy_severity: "CRITICAL,HIGH,MEDIUM"
performance:
# Service-specific budget (not synced from template)
use_template_budget: false
The CI workflow reads overrides:
- name: Read overrides
id: overrides
run: |
if [[ -f .template-overrides.yaml ]]; then
TEST_CMD=$(yq '.overrides.ci.test_command // "go test ./..."' .template-overrides.yaml)
else
TEST_CMD="go test ./..."
fi
echo "test_cmd=$TEST_CMD" >> $GITHUB_OUTPUT
- name: Run tests
run: ${{ steps.overrides.outputs.test_cmd }}
The Gate
The template sync PR is the gate. It must be reviewed and merged within one sprint. If a service rejects a sync PR, the platform team is notified. Persistent rejections indicate the template does not fit the service’s needs — which means either the template needs to be more flexible or the service is a snowflake that needs to be brought into alignment.
The Recovery
Sync PR has merge conflicts: The service modified a synced file. Resolve manually: keep the template’s pipeline structure but preserve service-specific customizations.
Too many sync PRs overwhelm developers: Batch template changes. Instead of syncing on every template commit, sync weekly or on template version tags.
Service needs a file that the template excludes from sync: Add it to .template-overrides.yaml. The service manages that file independently.