Development

Source, issues and pull requests live at https://github.com/cloudchristoph/certbot-dns-azure-modern. The plugin is a single module, certbot_dns_azure/_internal/dns_azure.py, implementing certbot’s certbot.plugins.dns_common.DNSAuthenticator. There is no public Python API beyond the certbot plugin interface.

Local setup

git clone https://github.com/cloudchristoph/certbot-dns-azure-modern.git
cd certbot-dns-azure-modern
python -m venv .venv && . .venv/bin/activate
pip install certbot 'azure-identity>=1.19.0' 'azure-mgmt-dns>=8.2.0' 'azure-core>=1.38.0' pytest build
pip install --no-deps -e .

Unit tests

The unit tests mock the Azure SDK and run without any Azure access:

python -m pytest -q tests/ -W error::DeprecationWarning

CI runs them on Python 3.10 to 3.13 against certbot 3.x and the latest release, and against both supported azure-mgmt-dns lines (8.x and 9.x). When touching compatibility, also test the oldest supported line locally (certbot>=3.0,<4.0 together with pyOpenSSL<26, and azure-mgmt-dns<9).

Integration tests

azure_tests/ issues real certificates from the Let’s Encrypt staging environment against dedicated Azure DNS zones. They need a zone whose name starts with certbot-test. (the test module refuses anything else, so it can never touch a production zone) plus the following environment variables:

export AZURE_TENANT_ID=... AZURE_SUBSCRIPTION_ID=... \
       AZURE_DNS_RESOURCE_GROUP=... AZURE_DNS_TEST_DOMAIN=certbot-test.example.com \
       EMAIL=you@example.com
pytest -rA azure_tests/

The tests authenticate with the current Azure CLI login, or with a service principal when AZURE_CLIENT_ID and AZURE_CLIENT_SECRET are set, and only delete _acme-challenge* TXT records that they created themselves. The required zone layout (a base zone with static CNAME/TXT records for the delegation tests plus the delegated zone1 and zone2) is described at the top of azure_tests/integration_test.py.

CI runs them on pull requests that change code, on release tags and once a week.

Dependency audit

.github/workflows/dependency-audit.yml checks the runtime dependencies against the OSV vulnerability database every Monday, on manual dispatch and on pull requests that change setup.py. It audits two resolutions on Python 3.10 and 3.13: the latest versions a fresh install gets, and the declared lower bounds. pip keeps an older, already installed version as long as it satisfies the bound, so a finding in the lower-bound run means the bound in setup.py has to be raised. A failing scheduled run opens an issue (or comments on the open one). To run the same check locally:

uv pip compile setup.py --resolution lowest-direct > /tmp/resolved.txt
uvx pip-audit --no-deps --disable-pip -r /tmp/resolved.txt --vulnerability-service osv

Build the docs

pip install -r docs/requirements.txt
sphinx-build -W -b html docs docs/_build/html

.github/workflows/docs.yml publishes them to GitHub Pages when a push to main changes docs/, CHANGELOG.md or setup.py. Pull requests only build them.

Release checklist

  1. Bump version in setup.py and update CHANGELOG.md.

  2. Open a pull request, wait for the checks and merge it; main only accepts changes through pull requests.

  3. Tag with the bare version (git tag 2.8.0 && git push origin 2.8.0). The build job refuses a tag that differs from the setup.py version.

  4. The workflow builds the wheel and sdist, runs the integration tests, publishes to PyPI via trusted publishing and creates the GitHub release.