DOC_INDEX
THEME
DOCS/Use Cases/Compliance-Gated Change

Compliance-Gated Change

Policy checks, evidence bundles, human approval, and a tamper-evident audit trail for every change

Regulated changes need more than a green pipeline: they need proof. This workflow validates the change ticket, runs every policy check in parallel (collect first, judge once), bundles checksummed evidence for auditors, requires approval, and appends to a hash-chained audit log where each entry commits to the previous one.

Executed for real against OrchStep v0.7.1 - clean changes, a policy violation, and a malformed ticket.

What it demonstrates

CapabilityWhere
Input contractvalidate_ticket (regexMatch on CHG-format)
Collect-then-judge policy runsparallel checks + one judge assert
Evidence as artifactstarball + sha256 in collect_evidence
Human approval with contextprompt carries the evidence hash
Tamper-evident audithash chain in audit_append

The workflow

name: compliance-gated-change
desc: "Run policy checks on a change, collect evidence, require approval, and append to a tamper-evident audit log"

defaults:
  change_dir: "./change"
  audit_log: "./audit/audit.log"
  evidence_dir: "./evidence"
  ticket: "CHG-1042"

tasks:
  setup_demo_change:
    desc: "A change set that passes policy (demo helper)"
    steps:
      - name: scaffold
        func: shell
        do: |
          rm -rf {{ vars.change_dir }}
          mkdir -p {{ vars.change_dir }}
          printf '# Copyright 2026 ExampleCorp\nresource "bucket" "logs" { retention = 90 }\n' > {{ vars.change_dir }}/storage.tf
          printf '# Copyright 2026 ExampleCorp\nresource "role" "reader" { scope = "read-only" }\n' > {{ vars.change_dir }}/iam.tf
          echo "change set ready"

  submit:
    desc: "Policy-check and apply a change under governance"
    steps:
      - name: validate_ticket
        func: assert
        args:
          condition: '{{ regexMatch "^CHG-[0-9]+$" vars.ticket }}'
          desc: "a real change ticket is attached"

      - name: policy_checks
        desc: "Every policy runs even if an early one fails (collect, then judge)"
        parallel:
          - name: license_headers
            func: shell
            do: |
              MISSING=$(grep -rL "Copyright 2026 ExampleCorp" {{ vars.change_dir }} | wc -l | tr -d ' ')
              echo "missing_headers=$MISSING"
            outputs:
              missing: '{{ result.output | regexFind "missing_headers=([0-9]+)" }}'
          - name: forbidden_patterns
            func: shell
            do: |
              HITS=$(grep -rEc "password|secret_key|0\.0\.0\.0/0" {{ vars.change_dir }} | awk -F: '{s+=$2} END {print s+0}')
              echo "forbidden=$HITS"
            outputs:
              hits: '{{ result.output | regexFind "forbidden=([0-9]+)" }}'
          - name: required_retention
            func: shell
            do: |
              OK=$(grep -c "retention" {{ vars.change_dir }}/storage.tf || true)
              echo "retention_rules=$OK"
            outputs:
              rules: '{{ result.output | regexFind "retention_rules=([0-9]+)" }}'

      - name: judge
        func: assert
        args:
          conditions:
            - condition: 'Number(steps.license_headers.missing) === 0'
              desc: "all files carry the license header"
            - condition: 'Number(steps.forbidden_patterns.hits) === 0'
              desc: "no forbidden patterns (secrets, open CIDRs)"
            - condition: 'Number(steps.required_retention.rules) > 0'
              desc: "storage changes declare retention"

      - name: collect_evidence
        desc: "Bundle what was checked - auditors get artifacts, not promises"
        func: shell
        do: |
          mkdir -p {{ vars.evidence_dir }}
          TAR={{ vars.evidence_dir }}/{{ vars.ticket }}.tgz
          tar -czf $TAR {{ vars.change_dir }}
          SHA=$(shasum -a 256 $TAR | cut -d' ' -f1)
          echo "sha=$SHA"
        outputs:
          sha: '{{ result.output | regexFind "sha=([a-f0-9]+)" }}'

      - name: approval
        func: prompt
        args:
          message: "Apply {{ vars.ticket }}? (policies passed, evidence {{ steps.collect_evidence.sha }})"
          type: confirm
          default: "true"

      - name: enforce_approval
        func: assert
        args:
          condition: '{{ eq steps.approval.value "true" }}'

      - name: apply_change
        func: shell
        do: 'echo "applied {{ vars.ticket }} (demo: terraform apply would run here)"'

      - name: audit_append
        desc: "Hash-chained audit entry - each line commits to the previous one"
        func: shell
        do: |
          mkdir -p $(dirname {{ vars.audit_log }})
          touch {{ vars.audit_log }}
          PREV=$(tail -1 {{ vars.audit_log }} | cut -d' ' -f1)
          [ -z "$PREV" ] && PREV=genesis
          ENTRY="{{ vars.ticket }} evidence={{ steps.collect_evidence.sha }} at=$(date -u +%FT%TZ)"
          HASH=$(printf "%s %s" "$PREV" "$ENTRY" | shasum -a 256 | cut -d' ' -f1)
          echo "$HASH $ENTRY" >> {{ vars.audit_log }}
          tail -1 {{ vars.audit_log }}

Run it

orchstep run setup_demo_change
orchstep run submit                          # CHG-1042: passes
orchstep run submit --var ticket=CHG-1043    # second entry chains to the first
orchstep run submit --var ticket=oops        # rejected: ticket format

Verified results - the real audit chain (each hash covers the previous line plus the new entry):

adcb46465577... CHG-1042 evidence=97ad5d81...
61f3d4f3adf8... CHG-1043 evidence=6b2993f5...

Adding a file containing 0.0.0.0/0 to the change set makes forbidden_patterns count it and judge fail (exit 1) - verified.

Design notes

Collect, then judge. The three policy checks run in parallel and always complete; a single judge step evaluates all of them. Auditors (and engineers) see every violation in one run instead of fix-one-rerun.

The chain makes edits visible. Anyone can recompute sha256(prev_hash + entry) down the file; a tampered line breaks every hash after it. It is not a blockchain - it is one shell line that makes quiet history edits impossible.