Compare Snapshots
ajat diff compares two on-disk rule trees produced by ajat export. It is purely local — no Jira credentials, no API call, no network — which makes it cheap to run in CI, on an air-gapped review machine, or against an archived snapshot you no longer have credentials for.
The pairing is the point: an export taken yesterday, plus an export taken today, plus ajat diff, is an auditable change history for your automation estate — without storing any additional state.
diff reads two directories that already exist on disk. Produce them first
with ajat export; keep timestamped output directories
so you always have a “before” and an “after” to compare.
Usage
ajat diff <left-dir> <right-dir> [--format table|json] [--check]<left-dir>is the “before” snapshot.<right-dir>is the “after” snapshot.--format table(default) renders a human-readable table on stdout.--format jsonemits a single JSON object — script-friendly.--checkreturns a non-zero exit code when any difference is detected — the CI drift gate.
diff is a read-only, credential-free command. It never contacts Jira and
never mutates anything — it only reads the JSON files in the two directories.
Change buckets
Three categories are surfaced:
| Bucket | Meaning |
|---|---|
| added | UUIDs present in <right-dir> only. |
| removed | UUIDs present in <left-dir> only. |
| changed | UUIDs present in both with at least one watched-field delta. |
Watched fields
A rule lands in the changed bucket when any of these fields differ between the two snapshots:
namestate(ENABLED ↔ DISABLED)descriptionactor(actor.accountId)trigger(the rule’s trigger component type)labels(order-insensitive set diff)scope(theruleScopeARIsset; an empty list is rendered asglobal)conditions,actions,branches— by component count
Deeper structural changes — the schema of one specific component, for example — do not produce a per-field delta. If you need that level of detail, the rule JSON files on disk are the source of truth; diff them with your tool of choice.
Table and JSON output
--format table (the default) prints a readable summary on stdout, grouped by bucket:
ajat diff ./snapshots/2026-05-18 ./snapshots/2026-05-19--format json emits a single JSON object with added, removed, and changed arrays — ideal for scripting and dashboards:
# List the names of every rule that changed between two snapshots
ajat diff ./baseline ./current --format json | jq '.changed[] | .name'Because the diff is fully local, you can run it as often as you like without touching the Automation API rate budget.
Examples
# What changed in the last 24 hours?
ajat export --output-dir ./snapshots/2026-05-19
ajat diff ./snapshots/2026-05-18 ./snapshots/2026-05-19
# Treat the diff as a CI gate.
ajat diff ./baseline ./current --check
# Machine-readable output.
ajat diff ./baseline ./current --format json | jq '.changed[] | .name'
# What did the import job actually apply?
ajat export --output-dir ./pre-import-snapshot
ajat import --input-dir ./source-snapshot --yes
ajat export --output-dir ./post-import-snapshot
ajat diff ./pre-import-snapshot ./post-import-snapshotExit codes
| Code | Meaning |
|---|---|
| 0 | The run completed; --check was off, or --check found no differences. |
| 1 | Bootstrap failure (unreadable directory, bad --format), or --check detected at least one diff. |
| 130 | Interrupted (SIGINT / SIGTERM). |
CI drift gate
--check turns diff into a governance guardrail: keep a committed baseline snapshot, export the current estate on every run, and fail the pipeline when the two diverge. This catches automation added, removed, or edited outside your change process.
# .github/workflows/automation-drift-gate.yaml
on:
schedule: [{ cron: "0 6 * * 1" }] # Monday 06:00 UTC
workflow_dispatch:
jobs:
drift-gate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6 # repo holds ./baseline from the last approved review
- run: |
# Download URL comes from your Climakers order email; store it as a CI secret.
curl -sL "${{ secrets.AJAT_DOWNLOAD_URL }}" -o ajat.zip
unzip ajat.zip && chmod +x ajat && sudo mv ajat /usr/local/bin/
- run: ajat export --output-dir ./current --no-progress
env:
AJAT_JIRA_DOMAIN: ${{ secrets.AJAT_JIRA_DOMAIN }}
AJAT_JIRA_USERNAME: ${{ secrets.AJAT_JIRA_USERNAME }}
AJAT_JIRA_API_TOKEN: ${{ secrets.AJAT_JIRA_API_TOKEN }}
# Fails the job (exit 1) the moment the live estate drifts from the baseline.
- run: ajat diff ./baseline ./current --checkWhen the gate fires, re-run the same command without --check (or with
--format json) to see exactly what drifted, then either investigate the
change or promote ./current to the new approved baseline. Use the
CI/CD Automation license edition for scheduled runs — see
Licensing.
Pairing with the state file
ajat export writes .ajat_state.json next to each export directory. The incremental state already answers “which rules changed since the last successful export” — and ajat diff answers “what changed about them” by comparing the on-disk JSON. Keeping both around makes drift questions answerable without re-running export.
Related
- Export the Estate — produces the on-disk tree that
diffconsumes. - HTML Reports —
report consistencyanalyzes a single snapshot for label hygiene and naming drift; complementary to two-snapshot diff. - Search the Estate — query live, summary-level rule state when you do not need full bodies or a second snapshot.
- Import and Restore — export, import, then diff to confirm exactly what an import applied.