Automation Governance

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.

Establishing an Automation Charter

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.

Key Failure Mode: Charter Drift

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

Defining Automation Standards

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.

Sharp Edge: The description: vs description Pitfall

In 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.

Enforcing Automation Policies

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

Sharp Edge: The Unchecked Schema Validation

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)
          '

Managing Automation Lifecycle

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.

Sharp Edge: The Forgotten Handover

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

Automating Governance Itself

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.

Sharp Edge: The Silent Failure of the Dashboard

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.