DigitalOcean.Client DNS record CRUD:
- list_domain_records/2, create_domain_record/3, update_domain_record/4,
delete_domain_record/3 on /v2/domains/:domain/records.
Steps.UpsertDnsRecord — idempotent ensure-record:
- (type, name) absent -> create
- present, data matches -> no-op
- present, data differs -> update, stashing prior state
Match comparison normalizes trailing dots (DO appends them to CNAMEs)
so a re-run doesn't false-positive into an update. compensate: deletes
records it created, restores prior data/ttl for records it updated.
Steps.DeleteDnsRecord — deletes a record by (type, name); idempotent
(already-absent succeeds); compensate recreates from stashed prior state.
Both read dns_domain / dns_record_type / dns_record_name / dns_record_data
/ dns_record_ttl from saga inputs.
Live smoke verified against sky-ai.com with TXT records (non-disruptive):
- [UpsertDnsRecord, Fail]: record created then compensation deleted it
(saga rolled_back, record absent after).
- [UpsertDnsRecord] twice: first outcome "created", second "noop"
(idempotency holds).
- [DeleteDnsRecord]: record deleted, absent after.
Both test records cleaned up — zero leftover.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>