Skip to content
Latchkey

PR: Run INTEGRATION tests workflow (DNSControl/dnscontrol)

The PR: Run INTEGRATION tests workflow from DNSControl/dnscontrol, explained and optimized by Latchkey.

C

CI health: C - fair

Point runs-on at Latchkey and get run de-duplication, job timeouts, self-healing for flaky steps, and up to 58% lower cost, applied automatically.

Grade your own workflow free or run it on Latchkey →
Source: DNSControl/dnscontrol.github/workflows/pr_integration_tests.ymlLicense MITView source

What it does

This is the PR: Run INTEGRATION tests workflow from the DNSControl/dnscontrol repository, a real project running GitHub Actions. It is shown here with attribution under its MIT license.

Below, Latchkey shows a faster, safer version produced by its optimization engine.

The workflow

workflow (.yml)
name: "PR: Run INTEGRATION tests"

permissions:
  contents: read

# When will this pipeline be activated?
# 1. If someone pushes to a branch called "tlim_testpr".
# 2. On a schedule (currently set to run every day at 4am UTC).

# When are "smoke tests" vs "full tests" run?
# 1. Is this a PR?  If the label "longtest" exists, run full tests. Otherwise, run smoke tests.
# 1. Is this a scheduled run? Run all the tests.
# 2. Manually triggered? Run all the tests.

on:
  pull_request:

  schedule:
    - cron: "0 4 * * *"

  workflow_dispatch:
  # Want to trigger all the tests without making a PR?
  # Run: git push origin main:tlim_testpr --force
  # This will trigger a full PR test on the main branch.
  # See https://github.com/DNSControl/dnscontrol/actions/workflows/pr_integration_tests.yml?query=branch%3Atlim_testpr

  push:
    branches:
      - "tlim_testpr"

# Environment Variables
env:
  # cache-key: Change to force cache reset `pwsh > Get-Date -UFormat %s`
  cache-key: 1639697695
  # go-mod-path: Where go-mod writes temp files
  go-mod-path: /go/pkg/mod
  # BIND_DOMAIN: BIND is the one providers that we always test. By
  # defining this here, we know it will always be set.
  BIND_DOMAIN: example.com

jobs:
  # integration-test-providers: Determine which providers have a _DOMAIN variable set.
  # That variable enables testing for the provider. The results are
  # stored in a JSON blob stashed in # needs.integration-test-providers.outputs.integration_test_providers
  # where integration-tests can pick it up.
  integration-test-providers:
    #needs: build
    runs-on: ubuntu-latest
    outputs:
      integration_test_providers: ${{ steps.get_integration_test_providers.outputs.integration_test_providers }}
    steps:
      - uses: actions/checkout@v7
        with:
          sparse-checkout: integrationTest/profiles.json
          sparse-checkout-cone-mode: false
      - name: Set Integration Test Providers
        id: get_integration_test_providers
        shell: pwsh
        run: |
          $Providers = @()
          $AllProviders = (Get-Content integrationTest/profiles.json | ConvertFrom-Json -AsHashtable).Keys
          $EnvContext = ConvertFrom-Json -InputObject $env:ENV_CONTEXT
          $VarsContext = ConvertFrom-Json -InputObject $env:VARS_CONTEXT
          $SecretsContext = ConvertFrom-Json -InputObject $env:SECRETS_CONTEXT
          $AllProviders | ForEach-Object {
            if(($null -ne $EnvContext."$($_)_DOMAIN") -or ($null -ne $VarsContext."$($_)_DOMAIN") -or ($null -ne $SecretsContext."$($_)_DOMAIN")) {
              $Providers += $_
            }
          }
          Write-Host "Integration test providers: $Providers"
          echo "integration_test_providers=$(ConvertTo-Json -InputObject $Providers -Compress)" >> $env:GITHUB_OUTPUT
        env:
          ENV_CONTEXT: ${{ toJson(env) }}
          VARS_CONTEXT: ${{ toJson(vars) }}
          SECRETS_CONTEXT: ${{ toJson(secrets) }}

  # integration-tests: Run the integration tests on any provider listed
  # in needs.integration-test-providers.outputs.integration_test_providers.
  integration-tests:
    if: github.ref != 'refs/heads/master' && github.ref != 'refs/heads/main'
    runs-on: ubuntu-latest
    container:
      image: golang:1.26
    needs:
      - integration-test-providers
    env:
      TEST_RESULTS: "/tmp/test-results"
      GOTESTSUM_FORMAT: testname

      # PROVIDER DOMAIN LIST
      # These providers will be tested if the env variable is set.
      # Set it to the domain name to use during the test.
      ALIDNS_DOMAIN: ${{ vars.ALIDNS_DOMAIN }}
      AXFRDDNS_DOMAIN: ${{ vars.AXFRDDNS_DOMAIN }}
      AXFRDDNS_DNSSEC_DOMAIN: ${{ vars.AXFRDDNS_DNSSEC_DOMAIN }}
      AZURE_DNS_DOMAIN: ${{ vars.AZURE_DNS_DOMAIN }}
      AZURE_PRIVATE_DNS_DOMAIN: ${{ vars.AZURE_PRIVATE_DNS_DOMAIN }}
      BIND_DOMAIN: ${{ vars.BIND_DOMAIN }}
      BUNNY_DNS_DOMAIN: ${{ vars.BUNNY_DNS_DOMAIN }}
      CLOUDFLAREAPI_DOMAIN: ${{ vars.CLOUDFLAREAPI_DOMAIN }}
      CLOUDNS_DOMAIN: ${{ vars.CLOUDNS_DOMAIN }}
      CNR_DOMAIN: ${{ vars.CNR_DOMAIN }}
      CSCGLOBAL_DOMAIN: ${{ vars.CSCGLOBAL_DOMAIN }}
      DIGITALOCEAN_DOMAIN: ${{ vars.DIGITALOCEAN_DOMAIN }}
      DYNU_DOMAIN: ${{ vars.DYNU_DOMAIN }}
      FORTIGATE_DOMAIN: ${{ vars.FORTIGATE_DOMAIN }}
      GANDI_V5_DOMAIN: ${{ vars.GANDI_V5_DOMAIN }}
      GCLOUD_DOMAIN: ${{ vars.GCLOUD_DOMAIN }}
      GIDINET_DOMAIN: ${{ vars.GIDINET_DOMAIN }}
      HEDNS_DOMAIN: ${{ vars.HEDNS_DOMAIN }}
      HETZNER_V2_DOMAIN: ${{ vars.HETZNER_V2_DOMAIN }}
      HUAWEICLOUD_DOMAIN: ${{ vars.HUAWEICLOUD_DOMAIN }}
      JOKER_DOMAIN: ${{ vars.JOKER_DOMAIN }}
      LINODE_DOMAIN: ${{ vars.LINODE_DOMAIN }}
      MIKROTIK_DOMAIN: ${{ vars.MIKROTIK_DOMAIN }}
      MYTHICBEASTS_DOMAIN: ${{ vars.MYTHICBEASTS_DOMAIN }}
      NAMEDOTCOM_DOMAIN: ${{ vars.NAMEDOTCOM_DOMAIN }}
      NETBIRD_DOMAIN: ${{ vars.NETBIRD_DOMAIN }}
      NETNOD_DOMAIN: ${{ vars.NETNOD_DOMAIN }}
      NS1_DOMAIN: ${{ vars.NS1_DOMAIN }}
      POWERDNS_DOMAIN: ${{ vars.POWERDNS_DOMAIN }}
      ROUTE53_DOMAIN: ${{ vars.ROUTE53_DOMAIN }}
      SAKURACLOUD_DOMAIN: ${{ vars.SAKURACLOUD_DOMAIN }}
      TRANSIP_DOMAIN: ${{ vars.TRANSIP_DOMAIN }}
      UNIFI_DOMAIN: ${{ vars.UNIFI_DOMAIN }}
      VERCEL_DOMAIN: ${{ vars.VERCEL_DOMAIN }}

      # PROVIDER SECRET LIST
      # The above providers have additional env variables they
      # need for credentials and such.
      #
      ALIDNS_ACCESS_KEY_ID: ${{ secrets.ALIDNS_ACCESS_KEY_ID }}
      ALIDNS_ACCESS_KEY_SECRET: ${{ secrets.ALIDNS_ACCESS_KEY_SECRET }}
      #
      AXFRDDNS_MASTER: ${{ secrets.AXFRDDNS_MASTER }}
      AXFRDDNS_NAMESERVERS: ${{ secrets.AXFRDDNS_NAMESERVERS }}
      AXFRDDNS_TRANSFER_KEY: ${{ secrets.AXFRDDNS_TRANSFER_KEY }}
      AXFRDDNS_TRANSFER_MODE: ${{ secrets.AXFRDDNS_TRANSFER_MODE }}
      AXFRDDNS_UPDATE_KEY: ${{ secrets.AXFRDDNS_UPDATE_KEY }}
      AXFRDDNS_UPDATE_MODE: ${{ secrets.AXFRDDNS_UPDATE_MODE }}
      #
      AXFRDDNS_DNSSEC_MASTER: ${{ secrets.AXFRDDNS_DNSSEC_MASTER }}
      AXFRDDNS_DNSSEC_NAMESERVERS: ${{ secrets.AXFRDDNS_DNSSEC_NAMESERVERS }}
      AXFRDDNS_DNSSEC_TRANSFER_KEY: ${{ secrets.AXFRDDNS_DNSSEC_TRANSFER_KEY }}
      AXFRDDNS_DNSSEC_TRANSFER_MODE: ${{ secrets.AXFRDDNS_DNSSEC_TRANSFER_MODE }}
      AXFRDDNS_DNSSEC_UPDATE_KEY: ${{ secrets.AXFRDDNS_DNSSEC_UPDATE_KEY }}
      AXFRDDNS_DNSSEC_UPDATE_MODE: ${{ secrets.AXFRDDNS_DNSSEC_UPDATE_MODE }}
      #
      AZURE_DNS_CLIENT_ID: ${{ secrets.AZURE_DNS_CLIENT_ID }}
      AZURE_DNS_CLIENT_SECRET: ${{ secrets.AZURE_DNS_CLIENT_SECRET }}
      AZURE_DNS_RESOURCE_GROUP: ${{ secrets.AZURE_DNS_RESOURCE_GROUP }}
      AZURE_DNS_SUBSCRIPTION_ID: ${{ secrets.AZURE_DNS_SUBSCRIPTION_ID }}
      AZURE_DNS_TENANT_ID: ${{ secrets.AZURE_DNS_TENANT_ID }}
      #
      AZURE_PRIVATE_DNS_CLIENT_ID: ${{ secrets.AZURE_PRIVATE_DNS_CLIENT_ID }}
      AZURE_PRIVATE_DNS_CLIENT_SECRET: ${{ secrets.AZURE_PRIVATE_DNS_CLIENT_SECRET }}
      AZURE_PRIVATE_DNS_RESOURCE_GROUP: ${{ secrets.AZURE_PRIVATE_DNS_RESOURCE_GROUP }}
      AZURE_PRIVATE_DNS_SUBSCRIPTION_ID: ${{ secrets.AZURE_PRIVATE_DNS_SUBSCRIPTION_ID }}
      AZURE_PRIVATE_DNS_TENANT_ID: ${{ secrets.AZURE_PRIVATE_DNS_TENANT_ID }}
      #
      BUNNY_DNS_API_KEY: ${{ secrets.BUNNY_DNS_API_KEY }}
      #
      CLOUDFLAREAPI_ACCOUNTID: ${{ secrets.CLOUDFLAREAPI_ACCOUNTID }}
      CLOUDFLAREAPI_TOKEN: ${{ secrets.CLOUDFLAREAPI_TOKEN }}
      #
      CLOUDNS_AUTH_ID: ${{ secrets.CLOUDNS_AUTH_ID }}
      CLOUDNS_AUTH_PASSWORD: ${{ secrets.CLOUDNS_AUTH_PASSWORD }}
      #
      CSCGLOBAL_APIKEY: ${{ secrets.CSCGLOBAL_APIKEY }}
      CSCGLOBAL_USERTOKEN: ${{ secrets.CSCGLOBAL_USERTOKEN }}
      #
      CNR_UID: ${{ secrets.CNR_UID }}
      CNR_PW: ${{ secrets.CNR_PW }}
      CNR_ENTITY: ${{ secrets.CNR_ENTITY }}
      #
      DIGITALOCEAN_TOKEN: ${{ secrets.DIGITALOCEAN_TOKEN }}
      #
      DYNU_API_KEY: ${{ secrets.DYNU_API_KEY }}
      #
      FORTIGATE_API_KEY: ${{ secrets.FORTIGATE_API_KEY }}
      FORTIGATE_VDOM: ${{ secrets.FORTIGATE_VDOM }}
      FORTIGATE_HOST: ${{ secrets.FORTIGATE_HOST }}
      #
      GANDI_V5_APIKEY: ${{ secrets.GANDI_V5_APIKEY }}
      GANDI_V5_TOKEN: ${{ secrets.GANDI_V5_TOKEN }}
      #
      GCLOUD_EMAIL: ${{ secrets.GCLOUD_EMAIL }}
      GCLOUD_PRIVATEKEY: ${{ secrets.GCLOUD_PRIVATEKEY }}
      GCLOUD_PROJECT: ${{ secrets.GCLOUD_PROJECT }}
      GCLOUD_TYPE: ${{ secrets.GCLOUD_TYPE }}
      #
      GIDINET_PASSWORD: ${{ secrets.GIDINET_PASSWORD }}
      GIDINET_USERNAME: ${{ secrets.GIDINET_USERNAME }}
      #
      HEDNS_PASSWORD: ${{ secrets.HEDNS_PASSWORD }}
      HEDNS_TOTP_SECRET: ${{ secrets.HEDNS_TOTP_SECRET }}
      HEDNS_USERNAME: ${{ secrets.HEDNS_USERNAME }}
      #
      HETZNER_V2_API_TOKEN: ${{ secrets.HETZNER_V2_API_TOKEN }}
      #
      HUAWEICLOUD_REGION: ${{ secrets.HUAWEICLOUD_REGION }}
      HUAWEICLOUD_KEY_ID: ${{ secrets.HUAWEICLOUD_KEY_ID }}
      HUAWEICLOUD_KEY: ${{ secrets.HUAWEICLOUD_KEY }}
      #
      JOKER_USERNAME: ${{ secrets.JOKER_USERNAME }}
      JOKER_PASSWORD: ${{ secrets.JOKER_PASSWORD }}
      #
      LINODE_TOKEN: ${{ secrets.LINODE_TOKEN }}
      #
      MIKROTIK_HOST: ${{ secrets.MIKROTIK_HOST }}
      MIKROTIK_USERNAME: ${{ secrets.MIKROTIK_USERNAME }}
      MIKROTIK_PASSWORD: ${{ secrets.MIKROTIK_PASSWORD }}
      #
      MYTHICBEASTS_KEYID: ${{ secrets.MYTHICBEASTS_KEYID }}
      MYTHICBEASTS_SECRET: ${{ secrets.MYTHICBEASTS_SECRET }}
      #
      NAMEDOTCOM_KEY: ${{ secrets.NAMEDOTCOM_KEY }}
      NAMEDOTCOM_URL: ${{ secrets.NAMEDOTCOM_URL }}
      NAMEDOTCOM_USER: ${{ secrets.NAMEDOTCOM_USER }}
      #
      NETBIRD_TOKEN: ${{ secrets.NETBIRD_TOKEN }}
      #
      NETNOD_APIKEY: ${{ secrets.NETNOD_APIKEY }}
      NETNOD_APIURL: ${{ secrets.NETNOD_APIURL }}
      #
      NS1_TOKEN: ${{ secrets.NS1_TOKEN }}
      #
      POWERDNS_APIKEY: ${{ secrets.POWERDNS_APIKEY }}
      POWERDNS_APIURL: ${{ secrets.POWERDNS_APIURL }}
      POWERDNS_SERVERNAME: ${{ secrets.POWERDNS_SERVERNAME }}
      #
      ROUTE53_KEY: ${{ secrets.ROUTE53_KEY }}
      ROUTE53_KEY_ID: ${{ secrets.ROUTE53_KEY_ID }}
      #
      SAKURACLOUD_ACCESS_TOKEN: ${{ secrets.SAKURACLOUD_ACCESS_TOKEN }}
      SAKURACLOUD_ACCESS_TOKEN_SECRET: ${{ secrets.SAKURACLOUD_ACCESS_TOKEN_SECRET }}
      #
      TRANSIP_ACCOUNT_NAME: ${{ secrets.TRANSIP_ACCOUNT_NAME }}
      TRANSIP_PRIVATE_KEY: ${{ secrets.TRANSIP_PRIVATE_KEY }}
      #
      UNIFI_API_KEY: ${{ secrets.UNIFI_API_KEY }}
      UNIFI_HOST: ${{ secrets.UNIFI_HOST }}
      UNIFI_SITE: ${{ secrets.UNIFI_SITE }}
      UNIFI_SKIP_TLS_VERIFY: ${{ secrets.UNIFI_SKIP_TLS_VERIFY }}
      #
      VERCEL_TEAM_ID: ${{ secrets.VERCEL_TEAM_ID }}
      VERCEL_API_TOKEN: ${{ secrets.VERCEL_API_TOKEN }}
      TENCENTDNS_SECRET_ID: ${{ secrets.TENCENTDNS_SECRET_ID }}
      TENCENTDNS_SECRET_KEY: ${{ secrets.TENCENTDNS_SECRET_KEY }}

    concurrency:
      group: ${{ github.workflow }}-${{ matrix.provider }}
    strategy:
      fail-fast: false
      matrix:
        provider: ${{ fromJson(needs.integration-test-providers.outputs.integration_test_providers )}}
    steps:
      - uses: actions/checkout@v7
      - run: mkdir -p "$TEST_RESULTS"
      - name: restore_cache
        uses: actions/cache@v6.1.0
        with:
          key: linux-go-${{ hashFiles('go.sum') }}-${{ env.cache-key }}
          restore-keys: linux-go-${{ hashFiles('go.sum') }}-${{ env.cache-key }}
          path: ${{ env.go-mod-path }}
      - name: Set END value for PRs
        # For PRs we only run a smoke test (the first 3 tests) to save time.
        # For pushes to main or if the PR has the 'longtest' label, we run all tests.
        id: set_end
        shell: bash
        env:
          # Converts label names into a single string for easier grep/matching
          LABELS: ${{ join(github.event.pull_request.labels.*.name, ' ') }}
        run: |
          END=3
          echo "END: Default set to $END"

          # Only for PRs
          if [[ "${{ github.event_name }}" == "pull_request" ]]; then
            # Check for 'longtest' label
            #if [[ "${{ github.event.pull_request.labels.*.name }}" =~ "longtest" ]]; then
            if [[ " $LABELS " =~ " longtest " ]]; then
              END=999
              echo "END: Pull request + longtest label. END=$END"
            else
              echo "END: longtest not set. No change."
            fi
          else
              echo "END: Not a pull request. No change."
          fi
          # Check if branch is main
          # NB(tlim): This used to be github.base_ref but it was changed to
          #           github.ref_name in 2026-05-07 because that seemed more correct.
          #           The original statement was created by an LLM and might have been
          #           wrong.
          if [[ "${{ github.ref_name }}" == "main" ]]; then
            END=999
            echo "END: github.ref_name is main. END=$END"
          fi
          # VERCEL it limited to 100 updates per hour. Therefore we never run more than the first few tests.
          if [[ "${{ matrix.provider }}" == "VERCEL" ]]; then
            END=3
            echo "END: Exception for VERCEL: Run fewer tests. END=$END"
          fi
          # HEDNS is very slow. Therefore, we only run some tests. The goal is to run enough tests to make
          # the time about the same as other tests
          if [[ "${{ matrix.provider }}" == "VERCEL" ]]; then
            END=10
            echo "END: Exception for HEDNS: Run fewer tests. END=$END"
          fi
          echo "END: Final value: END=$END"
          echo "END=$END" >> $GITHUB_ENV

      - name: Run integration tests for ${{ matrix.provider }} provider
        shell: bash -eo pipefail {0}
        run: |-
          #
          echo "END: Running tests 0 to $END"
          go install gotest.tools/gotestsum@latest
          if [ -n "$${{ matrix.provider }}_DOMAIN" ] ; then
            # NB(tlim): THe output of gotestsum needs to go to stdout AND the
            # gotestsum-output.txt file. That's why "tee" is used.
            gotestsum --junitfile ${TEST_RESULTS}/gotestsum-report.xml -- \
                -timeout 70m -v \
                -verbose \
                -profile ${{ matrix.provider }} \
                -cfworkers=true \
                -cfredirect=true \
                -cfflatten=false \
                -cftags=false \
                -end $END \
                ./... >&1 | \
                tee ${TEST_RESULTS}/gotestsum-output.txt
          else
            echo "Skip test for ${{ matrix.provider }} provider"
          fi
        working-directory: integrationTest
      - uses: actions/upload-artifact@v7.0.1
        with:
          name: integration-tests-${{ matrix.provider }}
          path: ${{ env.TEST_RESULTS }}

The same workflow, on Latchkey

Removes redundant runs and caps runaway jobs. Added and changed lines are highlighted.

name: "PR: Run INTEGRATION tests"
 
permissions:
  contents: read
 
# When will this pipeline be activated?
# 1. If someone pushes to a branch called "tlim_testpr".
# 2. On a schedule (currently set to run every day at 4am UTC).
 
# When are "smoke tests" vs "full tests" run?
# 1. Is this a PR?  If the label "longtest" exists, run full tests. Otherwise, run smoke tests.
# 1. Is this a scheduled run? Run all the tests.
# 2. Manually triggered? Run all the tests.
 
on:
  pull_request:
 
  schedule:
    - cron: "0 4 * * *"
 
  workflow_dispatch:
  # Want to trigger all the tests without making a PR?
  # Run: git push origin main:tlim_testpr --force
  # This will trigger a full PR test on the main branch.
  # See https://github.com/DNSControl/dnscontrol/actions/workflows/pr_integration_tests.yml?query=branch%3Atlim_testpr
 
  push:
    branches:
      - "tlim_testpr"
 
# Environment Variables
env:
  # cache-key: Change to force cache reset `pwsh > Get-Date -UFormat %s`
  cache-key: 1639697695
  # go-mod-path: Where go-mod writes temp files
  go-mod-path: /go/pkg/mod
  # BIND_DOMAIN: BIND is the one providers that we always test. By
  # defining this here, we know it will always be set.
  BIND_DOMAIN: example.com
 
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true
 
jobs:
  # integration-test-providers: Determine which providers have a _DOMAIN variable set.
  # That variable enables testing for the provider. The results are
  # stored in a JSON blob stashed in # needs.integration-test-providers.outputs.integration_test_providers
  # where integration-tests can pick it up.
  integration-test-providers:
    timeout-minutes: 30
    #needs: build
    runs-on: latchkey-small
    outputs:
      integration_test_providers: ${{ steps.get_integration_test_providers.outputs.integration_test_providers }}
    steps:
      - uses: actions/checkout@v7
        with:
          sparse-checkout: integrationTest/profiles.json
          sparse-checkout-cone-mode: false
      - name: Set Integration Test Providers
        id: get_integration_test_providers
        shell: pwsh
        run: |
          $Providers = @()
          $AllProviders = (Get-Content integrationTest/profiles.json | ConvertFrom-Json -AsHashtable).Keys
          $EnvContext = ConvertFrom-Json -InputObject $env:ENV_CONTEXT
          $VarsContext = ConvertFrom-Json -InputObject $env:VARS_CONTEXT
          $SecretsContext = ConvertFrom-Json -InputObject $env:SECRETS_CONTEXT
          $AllProviders | ForEach-Object {
            if(($null -ne $EnvContext."$($_)_DOMAIN") -or ($null -ne $VarsContext."$($_)_DOMAIN") -or ($null -ne $SecretsContext."$($_)_DOMAIN")) {
              $Providers += $_
            }
          }
          Write-Host "Integration test providers: $Providers"
          echo "integration_test_providers=$(ConvertTo-Json -InputObject $Providers -Compress)" >> $env:GITHUB_OUTPUT
        env:
          ENV_CONTEXT: ${{ toJson(env) }}
          VARS_CONTEXT: ${{ toJson(vars) }}
          SECRETS_CONTEXT: ${{ toJson(secrets) }}
 
  # integration-tests: Run the integration tests on any provider listed
  # in needs.integration-test-providers.outputs.integration_test_providers.
  integration-tests:
    timeout-minutes: 30
    if: github.ref != 'refs/heads/master' && github.ref != 'refs/heads/main'
    runs-on: latchkey-small
    container:
      image: golang:1.26
    needs:
      - integration-test-providers
    env:
      TEST_RESULTS: "/tmp/test-results"
      GOTESTSUM_FORMAT: testname
 
      # PROVIDER DOMAIN LIST
      # These providers will be tested if the env variable is set.
      # Set it to the domain name to use during the test.
      ALIDNS_DOMAIN: ${{ vars.ALIDNS_DOMAIN }}
      AXFRDDNS_DOMAIN: ${{ vars.AXFRDDNS_DOMAIN }}
      AXFRDDNS_DNSSEC_DOMAIN: ${{ vars.AXFRDDNS_DNSSEC_DOMAIN }}
      AZURE_DNS_DOMAIN: ${{ vars.AZURE_DNS_DOMAIN }}
      AZURE_PRIVATE_DNS_DOMAIN: ${{ vars.AZURE_PRIVATE_DNS_DOMAIN }}
      BIND_DOMAIN: ${{ vars.BIND_DOMAIN }}
      BUNNY_DNS_DOMAIN: ${{ vars.BUNNY_DNS_DOMAIN }}
      CLOUDFLAREAPI_DOMAIN: ${{ vars.CLOUDFLAREAPI_DOMAIN }}
      CLOUDNS_DOMAIN: ${{ vars.CLOUDNS_DOMAIN }}
      CNR_DOMAIN: ${{ vars.CNR_DOMAIN }}
      CSCGLOBAL_DOMAIN: ${{ vars.CSCGLOBAL_DOMAIN }}
      DIGITALOCEAN_DOMAIN: ${{ vars.DIGITALOCEAN_DOMAIN }}
      DYNU_DOMAIN: ${{ vars.DYNU_DOMAIN }}
      FORTIGATE_DOMAIN: ${{ vars.FORTIGATE_DOMAIN }}
      GANDI_V5_DOMAIN: ${{ vars.GANDI_V5_DOMAIN }}
      GCLOUD_DOMAIN: ${{ vars.GCLOUD_DOMAIN }}
      GIDINET_DOMAIN: ${{ vars.GIDINET_DOMAIN }}
      HEDNS_DOMAIN: ${{ vars.HEDNS_DOMAIN }}
      HETZNER_V2_DOMAIN: ${{ vars.HETZNER_V2_DOMAIN }}
      HUAWEICLOUD_DOMAIN: ${{ vars.HUAWEICLOUD_DOMAIN }}
      JOKER_DOMAIN: ${{ vars.JOKER_DOMAIN }}
      LINODE_DOMAIN: ${{ vars.LINODE_DOMAIN }}
      MIKROTIK_DOMAIN: ${{ vars.MIKROTIK_DOMAIN }}
      MYTHICBEASTS_DOMAIN: ${{ vars.MYTHICBEASTS_DOMAIN }}
      NAMEDOTCOM_DOMAIN: ${{ vars.NAMEDOTCOM_DOMAIN }}
      NETBIRD_DOMAIN: ${{ vars.NETBIRD_DOMAIN }}
      NETNOD_DOMAIN: ${{ vars.NETNOD_DOMAIN }}
      NS1_DOMAIN: ${{ vars.NS1_DOMAIN }}
      POWERDNS_DOMAIN: ${{ vars.POWERDNS_DOMAIN }}
      ROUTE53_DOMAIN: ${{ vars.ROUTE53_DOMAIN }}
      SAKURACLOUD_DOMAIN: ${{ vars.SAKURACLOUD_DOMAIN }}
      TRANSIP_DOMAIN: ${{ vars.TRANSIP_DOMAIN }}
      UNIFI_DOMAIN: ${{ vars.UNIFI_DOMAIN }}
      VERCEL_DOMAIN: ${{ vars.VERCEL_DOMAIN }}
 
      # PROVIDER SECRET LIST
      # The above providers have additional env variables they
      # need for credentials and such.
      #
      ALIDNS_ACCESS_KEY_ID: ${{ secrets.ALIDNS_ACCESS_KEY_ID }}
      ALIDNS_ACCESS_KEY_SECRET: ${{ secrets.ALIDNS_ACCESS_KEY_SECRET }}
      #
      AXFRDDNS_MASTER: ${{ secrets.AXFRDDNS_MASTER }}
      AXFRDDNS_NAMESERVERS: ${{ secrets.AXFRDDNS_NAMESERVERS }}
      AXFRDDNS_TRANSFER_KEY: ${{ secrets.AXFRDDNS_TRANSFER_KEY }}
      AXFRDDNS_TRANSFER_MODE: ${{ secrets.AXFRDDNS_TRANSFER_MODE }}
      AXFRDDNS_UPDATE_KEY: ${{ secrets.AXFRDDNS_UPDATE_KEY }}
      AXFRDDNS_UPDATE_MODE: ${{ secrets.AXFRDDNS_UPDATE_MODE }}
      #
      AXFRDDNS_DNSSEC_MASTER: ${{ secrets.AXFRDDNS_DNSSEC_MASTER }}
      AXFRDDNS_DNSSEC_NAMESERVERS: ${{ secrets.AXFRDDNS_DNSSEC_NAMESERVERS }}
      AXFRDDNS_DNSSEC_TRANSFER_KEY: ${{ secrets.AXFRDDNS_DNSSEC_TRANSFER_KEY }}
      AXFRDDNS_DNSSEC_TRANSFER_MODE: ${{ secrets.AXFRDDNS_DNSSEC_TRANSFER_MODE }}
      AXFRDDNS_DNSSEC_UPDATE_KEY: ${{ secrets.AXFRDDNS_DNSSEC_UPDATE_KEY }}
      AXFRDDNS_DNSSEC_UPDATE_MODE: ${{ secrets.AXFRDDNS_DNSSEC_UPDATE_MODE }}
      #
      AZURE_DNS_CLIENT_ID: ${{ secrets.AZURE_DNS_CLIENT_ID }}
      AZURE_DNS_CLIENT_SECRET: ${{ secrets.AZURE_DNS_CLIENT_SECRET }}
      AZURE_DNS_RESOURCE_GROUP: ${{ secrets.AZURE_DNS_RESOURCE_GROUP }}
      AZURE_DNS_SUBSCRIPTION_ID: ${{ secrets.AZURE_DNS_SUBSCRIPTION_ID }}
      AZURE_DNS_TENANT_ID: ${{ secrets.AZURE_DNS_TENANT_ID }}
      #
      AZURE_PRIVATE_DNS_CLIENT_ID: ${{ secrets.AZURE_PRIVATE_DNS_CLIENT_ID }}
      AZURE_PRIVATE_DNS_CLIENT_SECRET: ${{ secrets.AZURE_PRIVATE_DNS_CLIENT_SECRET }}
      AZURE_PRIVATE_DNS_RESOURCE_GROUP: ${{ secrets.AZURE_PRIVATE_DNS_RESOURCE_GROUP }}
      AZURE_PRIVATE_DNS_SUBSCRIPTION_ID: ${{ secrets.AZURE_PRIVATE_DNS_SUBSCRIPTION_ID }}
      AZURE_PRIVATE_DNS_TENANT_ID: ${{ secrets.AZURE_PRIVATE_DNS_TENANT_ID }}
      #
      BUNNY_DNS_API_KEY: ${{ secrets.BUNNY_DNS_API_KEY }}
      #
      CLOUDFLAREAPI_ACCOUNTID: ${{ secrets.CLOUDFLAREAPI_ACCOUNTID }}
      CLOUDFLAREAPI_TOKEN: ${{ secrets.CLOUDFLAREAPI_TOKEN }}
      #
      CLOUDNS_AUTH_ID: ${{ secrets.CLOUDNS_AUTH_ID }}
      CLOUDNS_AUTH_PASSWORD: ${{ secrets.CLOUDNS_AUTH_PASSWORD }}
      #
      CSCGLOBAL_APIKEY: ${{ secrets.CSCGLOBAL_APIKEY }}
      CSCGLOBAL_USERTOKEN: ${{ secrets.CSCGLOBAL_USERTOKEN }}
      #
      CNR_UID: ${{ secrets.CNR_UID }}
      CNR_PW: ${{ secrets.CNR_PW }}
      CNR_ENTITY: ${{ secrets.CNR_ENTITY }}
      #
      DIGITALOCEAN_TOKEN: ${{ secrets.DIGITALOCEAN_TOKEN }}
      #
      DYNU_API_KEY: ${{ secrets.DYNU_API_KEY }}
      #
      FORTIGATE_API_KEY: ${{ secrets.FORTIGATE_API_KEY }}
      FORTIGATE_VDOM: ${{ secrets.FORTIGATE_VDOM }}
      FORTIGATE_HOST: ${{ secrets.FORTIGATE_HOST }}
      #
      GANDI_V5_APIKEY: ${{ secrets.GANDI_V5_APIKEY }}
      GANDI_V5_TOKEN: ${{ secrets.GANDI_V5_TOKEN }}
      #
      GCLOUD_EMAIL: ${{ secrets.GCLOUD_EMAIL }}
      GCLOUD_PRIVATEKEY: ${{ secrets.GCLOUD_PRIVATEKEY }}
      GCLOUD_PROJECT: ${{ secrets.GCLOUD_PROJECT }}
      GCLOUD_TYPE: ${{ secrets.GCLOUD_TYPE }}
      #
      GIDINET_PASSWORD: ${{ secrets.GIDINET_PASSWORD }}
      GIDINET_USERNAME: ${{ secrets.GIDINET_USERNAME }}
      #
      HEDNS_PASSWORD: ${{ secrets.HEDNS_PASSWORD }}
      HEDNS_TOTP_SECRET: ${{ secrets.HEDNS_TOTP_SECRET }}
      HEDNS_USERNAME: ${{ secrets.HEDNS_USERNAME }}
      #
      HETZNER_V2_API_TOKEN: ${{ secrets.HETZNER_V2_API_TOKEN }}
      #
      HUAWEICLOUD_REGION: ${{ secrets.HUAWEICLOUD_REGION }}
      HUAWEICLOUD_KEY_ID: ${{ secrets.HUAWEICLOUD_KEY_ID }}
      HUAWEICLOUD_KEY: ${{ secrets.HUAWEICLOUD_KEY }}
      #
      JOKER_USERNAME: ${{ secrets.JOKER_USERNAME }}
      JOKER_PASSWORD: ${{ secrets.JOKER_PASSWORD }}
      #
      LINODE_TOKEN: ${{ secrets.LINODE_TOKEN }}
      #
      MIKROTIK_HOST: ${{ secrets.MIKROTIK_HOST }}
      MIKROTIK_USERNAME: ${{ secrets.MIKROTIK_USERNAME }}
      MIKROTIK_PASSWORD: ${{ secrets.MIKROTIK_PASSWORD }}
      #
      MYTHICBEASTS_KEYID: ${{ secrets.MYTHICBEASTS_KEYID }}
      MYTHICBEASTS_SECRET: ${{ secrets.MYTHICBEASTS_SECRET }}
      #
      NAMEDOTCOM_KEY: ${{ secrets.NAMEDOTCOM_KEY }}
      NAMEDOTCOM_URL: ${{ secrets.NAMEDOTCOM_URL }}
      NAMEDOTCOM_USER: ${{ secrets.NAMEDOTCOM_USER }}
      #
      NETBIRD_TOKEN: ${{ secrets.NETBIRD_TOKEN }}
      #
      NETNOD_APIKEY: ${{ secrets.NETNOD_APIKEY }}
      NETNOD_APIURL: ${{ secrets.NETNOD_APIURL }}
      #
      NS1_TOKEN: ${{ secrets.NS1_TOKEN }}
      #
      POWERDNS_APIKEY: ${{ secrets.POWERDNS_APIKEY }}
      POWERDNS_APIURL: ${{ secrets.POWERDNS_APIURL }}
      POWERDNS_SERVERNAME: ${{ secrets.POWERDNS_SERVERNAME }}
      #
      ROUTE53_KEY: ${{ secrets.ROUTE53_KEY }}
      ROUTE53_KEY_ID: ${{ secrets.ROUTE53_KEY_ID }}
      #
      SAKURACLOUD_ACCESS_TOKEN: ${{ secrets.SAKURACLOUD_ACCESS_TOKEN }}
      SAKURACLOUD_ACCESS_TOKEN_SECRET: ${{ secrets.SAKURACLOUD_ACCESS_TOKEN_SECRET }}
      #
      TRANSIP_ACCOUNT_NAME: ${{ secrets.TRANSIP_ACCOUNT_NAME }}
      TRANSIP_PRIVATE_KEY: ${{ secrets.TRANSIP_PRIVATE_KEY }}
      #
      UNIFI_API_KEY: ${{ secrets.UNIFI_API_KEY }}
      UNIFI_HOST: ${{ secrets.UNIFI_HOST }}
      UNIFI_SITE: ${{ secrets.UNIFI_SITE }}
      UNIFI_SKIP_TLS_VERIFY: ${{ secrets.UNIFI_SKIP_TLS_VERIFY }}
      #
      VERCEL_TEAM_ID: ${{ secrets.VERCEL_TEAM_ID }}
      VERCEL_API_TOKEN: ${{ secrets.VERCEL_API_TOKEN }}
      TENCENTDNS_SECRET_ID: ${{ secrets.TENCENTDNS_SECRET_ID }}
      TENCENTDNS_SECRET_KEY: ${{ secrets.TENCENTDNS_SECRET_KEY }}
 
    concurrency:
      group: ${{ github.workflow }}-${{ matrix.provider }}
    strategy:
      fail-fast: false
      matrix:
        provider: ${{ fromJson(needs.integration-test-providers.outputs.integration_test_providers )}}
    steps:
      - uses: actions/checkout@v7
      - run: mkdir -p "$TEST_RESULTS"
      - name: restore_cache
        uses: actions/cache@v6.1.0
        with:
          key: linux-go-${{ hashFiles('go.sum') }}-${{ env.cache-key }}
          restore-keys: linux-go-${{ hashFiles('go.sum') }}-${{ env.cache-key }}
          path: ${{ env.go-mod-path }}
      - name: Set END value for PRs
        # For PRs we only run a smoke test (the first 3 tests) to save time.
        # For pushes to main or if the PR has the 'longtest' label, we run all tests.
        id: set_end
        shell: bash
        env:
          # Converts label names into a single string for easier grep/matching
          LABELS: ${{ join(github.event.pull_request.labels.*.name, ' ') }}
        run: |
          END=3
          echo "END: Default set to $END"
 
          # Only for PRs
          if [[ "${{ github.event_name }}" == "pull_request" ]]; then
            # Check for 'longtest' label
            #if [[ "${{ github.event.pull_request.labels.*.name }}" =~ "longtest" ]]; then
            if [[ " $LABELS " =~ " longtest " ]]; then
              END=999
              echo "END: Pull request + longtest label. END=$END"
            else
              echo "END: longtest not set. No change."
            fi
          else
              echo "END: Not a pull request. No change."
          fi
          # Check if branch is main
          # NB(tlim): This used to be github.base_ref but it was changed to
          #           github.ref_name in 2026-05-07 because that seemed more correct.
          #           The original statement was created by an LLM and might have been
          #           wrong.
          if [[ "${{ github.ref_name }}" == "main" ]]; then
            END=999
            echo "END: github.ref_name is main. END=$END"
          fi
          # VERCEL it limited to 100 updates per hour. Therefore we never run more than the first few tests.
          if [[ "${{ matrix.provider }}" == "VERCEL" ]]; then
            END=3
            echo "END: Exception for VERCEL: Run fewer tests. END=$END"
          fi
          # HEDNS is very slow. Therefore, we only run some tests. The goal is to run enough tests to make
          # the time about the same as other tests
          if [[ "${{ matrix.provider }}" == "VERCEL" ]]; then
            END=10
            echo "END: Exception for HEDNS: Run fewer tests. END=$END"
          fi
          echo "END: Final value: END=$END"
          echo "END=$END" >> $GITHUB_ENV
 
      - name: Run integration tests for ${{ matrix.provider }} provider
        shell: bash -eo pipefail {0}
        run: |-
          #
          echo "END: Running tests 0 to $END"
          go install gotest.tools/gotestsum@latest
          if [ -n "$${{ matrix.provider }}_DOMAIN" ] ; then
            # NB(tlim): THe output of gotestsum needs to go to stdout AND the
            # gotestsum-output.txt file. That's why "tee" is used.
            gotestsum --junitfile ${TEST_RESULTS}/gotestsum-report.xml -- \
                -timeout 70m -v \
                -verbose \
                -profile ${{ matrix.provider }} \
                -cfworkers=true \
                -cfredirect=true \
                -cfflatten=false \
                -cftags=false \
                -end $END \
                ./... >&1 | \
                tee ${TEST_RESULTS}/gotestsum-output.txt
          else
            echo "Skip test for ${{ matrix.provider }} provider"
          fi
        working-directory: integrationTest
      - uses: actions/upload-artifact@v7.0.1
        with:
          name: integration-tests-${{ matrix.provider }}
          path: ${{ env.TEST_RESULTS }}
 

What changed

This workflow runs 2 jobs per trigger. On Latchkey the same minutes cost up to 58% less than GitHub-hosted, with zero queue time.

Actions used in this workflow