Documentation / Guides

DNS in CI

A zone checked into git is only useful if something notices when the live one stops matching it. dns diff --check exits 8 when they differ, which is a distinct code from a failed command, so a pipeline can tell "the zone has drifted" apart from "the tool broke".

Capture the zone

Export once and commit the result. JSON is the fuller format — it carries the domain's forwarded mailboxes as well as its records:

namecheap dns export example.com > dns/example.com.json
git add dns/example.com.json

The zone-file format is the more readable one and is what dns edit shows you, but it has no way to express forwarding, so --format zone notes that forwarding exists rather than pretending it round-trips.

namecheap dns export example.com --format zone > dns/example.com.txt

Assert it has not drifted

namecheap dns diff example.com dns/example.com.json --check

Exit 0 means the live zone matches the file. Exit 8 means it does not, and the diff is printed. Any other code is a real failure — bad credentials, a network problem, a domain that is not on the account.

A GitHub Actions job

Authenticate from the environment so there is no stored state to manage. NAMECHEAP_API_KEY is checked before the keychain and before the config file, so nothing else needs to exist on the runner.

name: DNS drift

on:
  schedule: [{ cron: '0 7 * * *' }]
  workflow_dispatch:

jobs:
  check:
    runs-on: ubuntu-latest
    env:
      NAMECHEAP_API_USER: ${{ secrets.NAMECHEAP_API_USER }}
      NAMECHEAP_API_KEY:  ${{ secrets.NAMECHEAP_API_KEY }}
      NAMECHEAP_CLIENT_IP: ${{ secrets.NAMECHEAP_CLIENT_IP }}
    steps:
      - uses: actions/checkout@v4
      - run: cargo install namecheap-cli --locked
      - run: namecheap dns diff example.com dns/example.com.json --check

Namecheap's API is IP-whitelisted, and a hosted runner's address is not stable. Either run this on a self-hosted runner with a fixed address, or accept that a scheduled check from GitHub's pool will need the whitelist kept current. This is a Namecheap constraint, not a tool one.

Applying rather than asserting

The same file drives the write. sync brings the zone into line with it; --delete also removes records the file does not mention, making the file authoritative rather than additive.

# add and update what the file describes, leave everything else alone
namecheap dns sync example.com dns/example.com.json -y

# make the zone match the file exactly
namecheap dns sync example.com dns/example.com.json --delete -y

Mailboxes behave the same way: those in the file are added or updated, and those it does not mention are left alone unless --delete is given. A file with no email_forwarding key says nothing about forwarding, so forwarding is left untouched rather than read as "there should be none".

Use -y only where nobody can answer a prompt, and pair it with --dry-run in a pull-request job so the diff is reviewed before the branch that applies it merges.

Machine-readable output

Every command takes --json. Validation problems come back as an issues array rather than formatted text, which is what you want when something downstream has to decide what to do about them.

namecheap dns list example.com --json | jq '.records[] | select(.type == "MX")'

Exit codes

0Success
1General error
2Authentication error
3Domain not found
4Record not found
5Validation error
6Network error
7Verification failed
8dns diff --check found differences