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
| Capability | Where |
|---|---|
| Input contract | validate_ticket (regexMatch on CHG-format) |
| Collect-then-judge policy runs | parallel checks + one judge assert |
| Evidence as artifacts | tarball + sha256 in collect_evidence |
| Human approval with context | prompt carries the evidence hash |
| Tamper-evident audit | hash 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 formatVerified 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.