Production engineering guide for automation governance covering patterns, implementation strategies, and operational best practices.
Automation Governance is the discipline of defining, enforcing, and evolving standards for how automation systems are created, deployed, and maintained across an engineering organisation. It matters when multiple teams use different tools, styles, and workflows for automation, leading to inconsistency, drift, and operational debt. Governance ensures that automation is not just a collection of scripts but a coherent, scalable, and self-documenting system of record.
An automation charter is the foundation of governance. It defines what automation is, who owns it, and how it evolves. The charter must be versioned, discoverable, and accessible to all contributors.
The charter is stored in a charter.yml file at the root of the automation repository:
version: 1.0
title: "Automation Governance Charter"
description: |
This charter defines the principles, standards, and workflows for all automation in the organisation.
authors:
- name: Alice Chen
email: [email protected]
role: Lead DevOps Engineer
- name: Jamal Torres
email: [email protected]
role: Platform Architect
review-cycle: quarterly
approval:
required: ["Lead DevOps Engineer", "Platform Architect"]
thresholds:
- name: "Major Change"
min-approvals: 3
reviewers: ["Engineering Director", "Security Lead"]
- name: "Minor Change"
min-approvals: 2
reviewers: ["DevOps Lead", "SRE Team"]
Changes to the charter are submitted as pull requests. Each PR must include a changelog.md entry and a reviewer-approval label. A PR is approved when all required reviewers have left a LGTM (Looks Good To Me) comment.
The charter becomes outdated when teams implement changes without updating it. This happens silently: a team uses a new automation tool without documenting it in the charter, and no one notices. To catch drift, the charter is reviewed every quarter by a rotating committee. A check-charter job runs in CI, verifying that every tool, pattern, and policy in the charter is reflected in the automation codebase.
name: Check Charter Compliance
on:
schedule:
- cron: '0 0 1 * *' # First day of each month
workflow_dispatch:
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Python
run: |
sudo apt-get update
sudo apt-get install -y python3-pip
- name: Install dependencies
run: pip3 install yq pyyaml
- name: Validate charter
run: |
yq -r '.tools[] | select(.name == "ansible") | .documentation | .url' charter.yml
python3 -c '
import yaml
with open("charter.yml") as f:
charter = yaml.safe_load(f)
tools = {t["name"] for t in charter["tools"]}
assert "terraform" in tools
assert "kubernetes" in tools
assert "docker" in tools
'
- name: Report compliance
run: |
echo "Charter compliant: ${{ steps.validate.outputs.compliant }}" >> $GITHUB_OUTPUT
Standards are the rules that govern how automation is written and structured. They include naming conventions, directory layout, configuration practices, and code quality.
A standard is defined in a standards.yaml file, which is validated by a pre-commit hook:
# standards.yaml
naming:
pattern: "service-{env}-{component}-{action}"
examples:
- service-prod-web-deploy
- service-staging-db-backup
- service-dev-api-healthcheck
directory-layout:
root: "automations"
structure:
- "environment/{env}/"
- "tasks/{task-name}.yml"
- "templates/{template-name}.j2"
- "variables/{env}.yml"
- "roles/{role-name}/"
defaults:
- "roles/common/"
- "roles/monitoring/"
code-quality:
linters:
- "ansible-lint"
- "yamllint"
rules:
- name: "Use consistent indentation"
severity: "warning"
check: |
for file in $(find . -name "*.yml" -o -name "*.yaml"); do
if ! grep -q "^\s* " "$file"; then
echo "Indentation should be 2 spaces: $file"
fi
done
- name: "All tasks must have a description"
severity: "error"
check: |
for file in $(find . -name "*.yml"); do
if ! grep -q "description:" "$file"; then
echo "Missing description in $file"
fi
done
The pre-commit hook runs pre-commit run --all-files and fails if any check fails. A common failure: tasks that have description: but not description:, with a trailing space.
description: vs description PitfallIn YAML, description: and description are different keys. A task written as:
- name: Deploy web service
description: Deploy the web service to production
tasks:
- name: Install dependencies
is valid, but the description key is not recognized by the linter, which expects description:. This leads to tasks that look documented but are not.
Policies define what must be true of every automation artifact. They are enforced via CI and can be configured in a policies.yaml file.
# policies.yaml
policies:
- name: "Must have a changelog"
description: "Every automation must include a changelog.md file."
validation:
type: "file-existence"
path: "changelog.md"
required: true
severity: "error"
remediation:
command: |
echo "Add changelog.md with a header and at least one entry."
cat << 'EOF' > changelog.md
# Changelog
## [Unreleased]
### Added
- Initial setup
EOF
required: true
- name: "Must use versioned roles"
description: "All roles must be versioned in Git."
validation:
type: "git-tag"
pattern: "v[0-9]+.[0-9]+.[0-9]+"
severity: "warning"
remediation:
command: |
git tag -a v1.0.0 -m "Initial release"
git push origin v1.0.0
- name: "Must include metadata"
description: "Every automation must define metadata in a metadata.yml file."
validation:
type: "yaml-schema"
schema:
type: object
properties:
name:
type: string
minLength: 1
version:
type: string
pattern: "^v[0-9]+\\.[0-9]+\\.[0-9]+$"
author:
type: string
format: "email"
dependencies:
type: array
items:
type: string
required: ["name", "version", "author"]
A CI job runs validate-policies on every pull request:
# .github/workflows/validate-policies.yml
name: Validate Automation Policies
on: [pull_request]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y jq python3
- name: Run policy validation
run: |
python3 -c '
import json, yaml, subprocess
with open("policies.yaml") as f:
policies = yaml.safe_load(f)
for policy in policies["policies"]:
result = subprocess.run(
["python3", "-c", policy["validation"]["check"]],
capture_output=True,
text=True
)
if result.returncode != 0:
print(f"Policy failed: {policy["name"]}")
print(result.stderr)
if "remediation" in policy:
subprocess.run(policy["remediation"]["command"].split())
else:
print(f"Policy passed: {policy["name"]}")
'
- name: Report results
run: |
echo "Policy results: $(cat policy-results.json)" >> $GITHUB_OUTPUT
A policy requires a metadata.yml file with a schema, but the schema is not validated in CI. The schema checks are written in Python but not tested against real input. A team creates metadata.yml with version: 1.0 instead of version: v1.0.0, and the job passes even though it should fail.
The fix: add a test job that validates the schema against a suite of example files.
# .github/workflows/test-schema.yaml
name: Test Schema Validation
on: [workflow_dispatch]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: pip3 install jsonschema
- name: Run schema tests
run: |
python3 -c '
import json, jsonschema
from jsonschema import validate
with open("policies.yaml") as f:
policies = json.load(f)
for policy in policies["policies"]:
if policy["validation"]["type"] == "yaml-schema":
schema = policy["validation"]["schema"]
for example in [
"examples/metadata-basic.yml",
"examples/metadata-missing-author.yml",
"examples/metadata-wrong-version.yml"
]:
with open(example) as f:
data = json.load(f)
try:
validate(instance=data, schema=schema)
print(f"✓ Schema valid: {example}")
except jsonschema.exceptions.ValidationError as e:
print(f"✗ Schema invalid: {example}")
print(e.message)
'
Governance extends beyond code to the entire lifecycle of automation: from idea to retirement. A lifecycle is defined in a lifecycle.yml file, which specifies stages, responsibilities, and artifacts.
# lifecycle.yml
stages:
- name: "Idea"
description: "Initial concept of automation"
artifacts:
- "idea.md"
- "diagram.png"
owners:
- "Product Team"
handover:
- "Create a new automation repository"
- "Add to automation registry"
- name: "Design"
description: "Detailed specification and architecture"
artifacts:
- "design.md"
- "flowchart.svg"
owners:
- "Platform Team"
handover:
- "Submit PR to automation repository"
- "Add to automation catalog"
- name: "Development"
description: "Implementation of automation"
artifacts:
- "code/"
- "tests/"
- "docs/"
owners:
- "Engineering Team"
handover:
- "Merge to main"
- "Tag release"
- name: "Deployment"
description: "Rollout to production"
artifacts:
- "deployment.yaml"
- "runbook.md"
owners:
- "SRE Team"
handover:
- "Register in production automation registry"
- "Add to monitoring dashboard"
- name: "Maintenance"
description: "Ongoing operation and updates"
artifacts:
- "logs/"
- "incidents/"
- "performance-reports/"
owners:
- "SRE Team"
handover:
- "Update runbook"
- "Schedule reviews"
- name: "Retirement"
description: "End-of-life and decommissioning"
artifacts:
- "retirement-plan.md"
- "archive/"
owners:
- "Platform Team"
handover:
- "Deactivate in registry"
- "Notify stakeholders"
Each stage is tracked via a GitHub project board, with cards that link to the automation’s repository and include the lifecycle.yml as a template.
A team completes the "Development" stage and merges code, but forgets to update the lifecycle.yml. The next team, tasked with "Deployment", finds no documentation on how to hand over the automation. They assume the automation is ready, but the deployment.yaml file is missing, and the runbook has not been written.
The solution: a check-lifecycle job runs on every merge to main, verifying that all stages are complete and that handover artifacts are present:
# .github/workflows/check-lifecycle.yml
name: Check Lifecycle Progress
on: [push]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: sudo apt-get update && sudo apt-get install -y jq
- name: Check lifecycle
run: |
jq -r '
.stages[] |
select(.name == "Development") |
.handover[] |
select(. != "Merge to main") |
"Handover missing: \(.), see lifecycle.yml"
' lifecycle.yml
- name: Report missing handover
run: |
if [ -f "missing-handover.txt" ]; then
echo "Handover incomplete:"
cat missing-handover.txt
exit 1
fi
Governance is not a static process. It must be automated: from policy enforcement to lifecycle tracking.
A governance-dashboard job generates a report of automation health every week:
# .github/workflows/governance-dashboard.yml
name: Governance Dashboard
on:
schedule:
- cron: '0 0 * * 0' # Every Sunday
jobs:
dashboard:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y python3 jq
- name: Generate dashboard
run: |
python3 -c '
import json, os
from pathlib import Path
results = {}
for path in Path(".").rglob("*.yml"):
if path.name == "policies.yaml":
with open(path) as f:
policies = json.load(f)
results["policies"] = len(policies["policies"])
if path.name == "lifecycle.yml":
with open(path) as f:
lifecycle = json.load(f)
results["stages"] = len(lifecycle["stages"])
with open("dashboard.json", "w") as f:
json.dump(results, f, indent=2)
'
- name: Upload to dashboard
run: |
curl -X POST -H "Content-Type: application/json" \
-d @dashboard.json \
https://dashboard.company.com/api/automation/governance
The dashboard is consumed by a governance-report service that aggregates data from multiple repositories and presents it in a weekly summary, including policy compliance, lifecycle progress, and charter drift.
The dashboard job runs weekly, but the curl command fails silently when the dashboard service is down. The dashboard.json is created, but the curl command does not report an error. The dashboard is updated only when the service is up, but no one notices when it fails.
The fix: add a check-dashboard job that verifies the dashboard response and sends alerts on failure.
# .github/workflows/check-dashboard.yml
name: Check Dashboard Integration
on: [workflow_dispatch]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check dashboard
run: |
response=$(curl -s -w "%{http_code}" -o dashboard-response.json https://dashboard.company.com/api/automation/governance)
code=$(echo "$response" | tail -n1)
body=$(echo "$response" | head -n-1)
if [ "$code" -ne 200 ]; then
echo "Dashboard failed: $code"
echo "$body" | jq .
exit 1
fi
echo "Dashboard success: $code"
This completes the feedback loop: governance is not just a set of rules, but a self-improving system.
This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy