variables:
  # Not normally needed, but may be if some script uses `apt-get install`.
  DEBIAN_FRONTEND: noninteractive
  # Locale settings do not affect the build, but might affect tests.
  LC_ALL: C

  # automated commits will inherit identification from pipeline trigger
  GIT_AUTHOR_NAME: "$GITLAB_USER_NAME (GitLab job $CI_JOB_ID)"
  GIT_AUTHOR_EMAIL: "$GITLAB_USER_EMAIL"
  GIT_COMMITTER_NAME: "$GIT_AUTHOR_NAME"
  GIT_COMMITTER_EMAIL: "$GIT_AUTHOR_EMAIL"

  CI_REGISTRY_IMAGE: registry.gitlab.isc.org/isc-projects/images/bind9
  CCACHE_DIR: "/ccache"

  GIT_DEPTH: 1
  GIT_CLEAN_FLAGS: -ffdxq

  # The following values may be overwritten in GitLab's CI/CD Variables Settings.
  BUILD_PARALLEL_JOBS: 6
  TEST_PARALLEL_JOBS: 4

  CONFIGURE: ./configure
  CLANG_VERSION: 21
  CLANG: "clang-${CLANG_VERSION}"
  SCAN_BUILD: "scan-build-${CLANG_VERSION}"
  LLVM_SYMBOLIZER: "/usr/lib/llvm-${CLANG_VERSION}/bin/llvm-symbolizer"
  ASAN_SYMBOLIZER_PATH: "/usr/lib/llvm-${CLANG_VERSION}/bin/llvm-symbolizer"
  CLANG_FORMAT: "clang-format-${CLANG_VERSION}"

  CFLAGS_COMMON: -fno-omit-frame-pointer -fno-optimize-sibling-calls -O1 -g -Wall -Wextra

  # Pass run-time flags to AddressSanitizer to get core dumps on error.
  ASAN_OPTIONS: abort_on_error=1:disable_coredump=0:unmap_shadow_on_exit=1

  TSAN_OPTIONS_COMMON: "disable_coredump=0 second_deadlock_stack=1 atexit_sleep_ms=1000 history_size=7 log_exe_name=true log_path=tsan"
  TSAN_SUPPRESSIONS: "suppressions=${CI_PROJECT_DIR}/.tsan-suppress"
  TSAN_OPTIONS_DEBIAN: "${TSAN_OPTIONS_COMMON} ${TSAN_SUPPRESSIONS} external_symbolizer_path=${LLVM_SYMBOLIZER}"
  TSAN_OPTIONS_FEDORA: "${TSAN_OPTIONS_COMMON} ${TSAN_SUPPRESSIONS} external_symbolizer_path=/usr/bin/llvm-symbolizer"

  UBSAN_OPTIONS: "halt_on_error=1:abort_on_error=1:disable_coredump=0"

  AM_COLOR_TESTS: always

  WITHOUT_READLINE: "--without-readline"
  WITH_READLINE: "--with-readline"
  WITH_READLINE_EDITLINE: "--with-readline=editline"
  WITH_READLINE_LIBEDIT: "--with-readline=libedit"
  WITH_READLINE_READLINE: "--with-readline=readline"

  INSTALL_PATH: "${CI_PROJECT_DIR}/.local"

  # In multithreaded unit tests, abort on the first failure
  CMOCKA_TEST_ABORT: 1

  # Disable pytest's "cacheprovider" plugin to prevent it from creating
  # cross-testrun files as there is no need to use that feature in CI.
  PYTEST_ADDOPTS: "-p no:cacheprovider"

  HYPOTHESIS_PROFILE: "ci"

  # Some jobs may clean up the build artifacts unless this is set to 0.
  CLEAN_BUILD_ARTIFACTS_ON_SUCCESS: 1

  # DNS Shotgun performance testing defaults
  SHOTGUN_ROUNDS: 1
  SHOTGUN_DURATION: 120
  # allow unlimited improvements against baseline
  SHOTGUN_EVAL_THRESHOLD_CPU_MIN: '-inf'
  SHOTGUN_EVAL_THRESHOLD_MEMORY_MIN: '-inf'
  SHOTGUN_EVAL_THRESHOLD_RCODE_MAX: '+inf'
  SHOTGUN_EVAL_THRESHOLD_LATENCY_PCTL_MIN: '-inf'
  SHOTGUN_EVAL_THRESHOLD_LATENCY_PCTL_DRIFT_MIN: '-inf'

  # Even though there's only one job per runtime environment, the GitLab
  # "instance" executor insists on cloning the Git repository to a path that
  # contains a variable number from zero to the "maximum concurrent instances
  # count" allowed on the GitLab Runner. See the "0" directory in this example
  # path: /home/ec2-user/builds/t1_4FZzvz/0/isc-projects/bind9/.git/.
  #
  # This is not a problem for isolated jobs like "stress" tests that depend on
  # no other jobs. However, it is a problem for jobs that need other jobs'
  # artifacts. For example, a system test job that has its Git repo cloned to
  # the "/1/" sub-path will fail if it downloads build job artifacts that have
  # ./configure output files with "/0/" in its sub-path recorded.
  GIT_CLONE_PATH_INSTANCE_EXECUTOR: "/home/ec2-user/builds/${CI_PROJECT_PATH}/"

default:
  # Allow all running CI jobs to be automatically canceled when a new
  # version of a branch is pushed.
  #
  # See: https://docs.gitlab.com/ee/ci/pipelines/settings.html#auto-cancel-redundant-pipelines
  interruptible: true

  # AWS can interrupt the spot instance anytime, so let's retry the job when
  # the interruption event happens to avoid a pipeline failure.
  retry:
    max: 2
    when:
      - runner_system_failure

stages:
  - autoconf
  - quick-checks
  - other-checks
  - build
  - unit
  - system
  - performance
  - docs
  - postmerge
  - release

### Runner Tag Templates

# Autoscaling GitLab Runner on AWS EC2 (amd64)

.linux-amd64: &linux_amd64
  tags:
    - linux
    - aws
    - runner-manager
    - amd64

# Autoscaling GitLab Runner on AWS EC2 (arm64)

.linux-arm64: &linux_arm64
  tags:
    - linux
    - aws
    - runner-manager
    - aarch64

.freebsd-autoscaler-13-amd64-tags: &freebsd_autoscaler_13_amd64_tags
  tags:
    - amd64
    - autoscaler
    - aws
    - bsd-stress-test-1
    - shell
    - stress-test

.freebsd-autoscaler-14-amd64-tags: &freebsd_autoscaler_14_amd64_tags
  tags:
    - amd64
    - autoscaler
    - aws
    - bsd-stress-test-2
    - shell
    - stress-test

.freebsd-autoscaler-15-amd64-tags: &freebsd_autoscaler_15_amd64_tags
  tags:
    - amd64
    - autoscaler
    - aws
    - bsd-stress-test-3
    - shell
    - stress-test

### Docker Image Templates

# Alpine Linux

.alpine-3.23-amd64: &alpine_3_23_amd64_image
  image: "$CI_REGISTRY_IMAGE:alpine-3.23-amd64"
  <<: *linux_amd64

# AlmaLinux

.almalinux-8-amd64: &almalinux_8_amd64_image
  image: "$CI_REGISTRY_IMAGE:almalinux-8-amd64"
  <<: *linux_amd64

.almalinux-9-amd64: &almalinux_9_amd64_image
  image: "$CI_REGISTRY_IMAGE:almalinux-9-amd64"
  <<: *linux_amd64

.almalinux-10-amd64: &almalinux_10_amd64_image
  image: "$CI_REGISTRY_IMAGE:almalinux-10-amd64"
  <<: *linux_amd64

# Debian

.debian-bookworm-amd64: &debian_bookworm_amd64_image
  image: "$CI_REGISTRY_IMAGE:debian-bookworm-amd64"
  <<: *linux_amd64

.debian-trixie-amd64: &debian_trixie_amd64_image
  image: "$CI_REGISTRY_IMAGE:debian-trixie-amd64"
  <<: *linux_amd64

.tsan-debian-trixie-amd64: &tsan_debian_trixie_amd64_image
  image: "$CI_REGISTRY_IMAGE:tsan-debian-trixie-amd64"
  <<: *linux_amd64

.debian-trixie-amd64cross32: &debian_trixie_amd64cross32_image
  image: "$CI_REGISTRY_IMAGE:debian-trixie-amd64cross32"
  <<: *linux_amd64

.debian-sid-amd64: &debian_sid_amd64_image
  image: "$CI_REGISTRY_IMAGE:debian-sid-amd64"
  <<: *linux_amd64

# openSUSE Tumbleweed

.tumbleweed-latest-amd64: &tumbleweed_latest_amd64_image
  image: "$CI_REGISTRY_IMAGE:tumbleweed-latest-amd64"
  <<: *linux_amd64

# Fedora

.tsan-fedora-43-amd64: &tsan_fedora_43_amd64_image
  image: "$CI_REGISTRY_IMAGE:tsan-fedora-43-amd64"
  <<: *linux_amd64

.fedora-43-amd64: &fedora_43_amd64_image
  image: "$CI_REGISTRY_IMAGE:fedora-43-amd64"
  <<: *linux_amd64

.fedora-43-arm64: &fedora_43_arm64_image
  image: "$CI_REGISTRY_IMAGE:fedora-43-arm64"
  <<: *linux_arm64

# Ubuntu

.ubuntu-jammy-amd64: &ubuntu_jammy_amd64_image
  image: "$CI_REGISTRY_IMAGE:ubuntu-jammy-amd64"
  <<: *linux_amd64

.ubuntu-noble-amd64: &ubuntu_noble_amd64_image
  image: "$CI_REGISTRY_IMAGE:ubuntu-noble-amd64"
  <<: *linux_amd64

# Base image
# This is a meta image that is used as a base for non-specific jobs

.base: &base_image
  <<: *debian_trixie_amd64_image

### Job Templates

.rule_mr_code: &rule_mr_code
  - if: '$CI_MERGE_REQUEST_DIFF_BASE_SHA != null'
    changes:
      - '**/*.c'
      - '**/*.h'
      - '**/meson.build'

.rule_mr_shell: &rule_mr_shell
  - if: '$CI_MERGE_REQUEST_DIFF_BASE_SHA != null'
    changes:
      - '**/*.sh'
      - '**/*.sh.in'
      - 'bin/tests/system/org.isc.bind.system'
      - 'bin/tests/system/custom-test-driver'

.rule_mr_python: &rule_mr_python
  - if: '$CI_MERGE_REQUEST_DIFF_BASE_SHA != null'
    changes:
      - '**/*.py'

.rule_mr_system_tests: &rule_mr_system_tests
  - if: '$CI_MERGE_REQUEST_DIFF_BASE_SHA != null'
    changes:
      - 'bin/tests/system/**/*'

.rule_mr_manual: &rule_mr_manual
  - if: '$CI_MERGE_REQUEST_DIFF_BASE_SHA != null'
    when: manual  # only run on MR if requested
    allow_failure: true  # don't block the pipeline or the pipeline result

.rule_tag: &rule_tag
  - if: '$CI_PROJECT_NAMESPACE == "isc-private" && $CI_COMMIT_TAG != null'

.rule_tag_open_source: &rule_tag_open_source
  - if: '$CI_PROJECT_NAMESPACE == "isc-private" && $CI_COMMIT_TAG != null && $CI_COMMIT_TAG !~ /-S/'

.rule_tag_security: &rule_tag_security
  - if: '$CI_PROJECT_NAMESPACE == "isc-private" && $CI_COMMIT_TAG != null && $RELEASE_TYPE == "security"'

.rule_tag_security_or_subscription: &rule_tag_security_or_subscription
  - if: '$CI_PROJECT_NAMESPACE == "isc-private" && $CI_COMMIT_TAG != null && ($RELEASE_TYPE == "security" || $CI_COMMIT_TAG =~ /-S/)'

.rule_source_other_than_mr: &rule_source_other_than_mr
  - if: '$CI_PIPELINE_SOURCE =~ /^(api|pipeline|schedule|trigger|web)$/ && $REBASE_ONLY != "1"'

.rule_source_all: &rule_source_all
  - if: '$CI_PIPELINE_SOURCE =~ /^(api|merge_request_event|pipeline|schedule|trigger|web)$/ && $REBASE_ONLY != "1"'

.api-pipelines-schedules-tags-triggers-web-triggering-rules: &api_pipelines_schedules_tags_triggers_web_triggering_rules
  rules:
    - *rule_tag
    - *rule_source_other_than_mr

.default-triggering-rules_list: &default_triggering_rules_list
  - *rule_tag
  - *rule_source_all

.default-triggering-rules: &default_triggering_rules
  rules:
    - *default_triggering_rules_list

.code-triggering-rules: &code_triggering_rules
  rules:
    - *rule_mr_code
    - *rule_mr_manual
    - *rule_tag
    - *rule_source_other_than_mr

.shell-triggering-rules: &shell_triggering_rules
  rules:
    - *rule_mr_shell
    - *rule_mr_manual
    - *rule_tag
    - *rule_source_other_than_mr

.python-triggering-rules: &python_triggering_rules
  rules:
    - *rule_mr_python
    - *rule_mr_manual
    - *rule_tag
    - *rule_source_other_than_mr

.extra-system-tests-triggering-rules: &extra_system_tests_triggering_rules
  rules:
    - *rule_tag
    - *rule_source_other_than_mr
    - *rule_mr_system_tests

.quick-check: &quick_checks_job
  <<: *default_triggering_rules
  <<: *base_image
  stage: quick-checks
  needs: []
  tags:
    - smalljob

.other-check: &other_checks_job
  <<: *default_triggering_rules
  <<: *base_image
  stage: other-checks
  needs: []

.autoconf: &autoconf_job
  <<: *default_triggering_rules
  <<: *base_image
  stage: autoconf
  tags:
    - smalljob
  script:
    - autoreconf -fi
  artifacts:
    untracked: true

.configure: &configure
    - ${CONFIGURE}
      --disable-maintainer-mode
      --enable-developer
      --enable-option-checking=fatal
      --enable-dnstap
      --with-cmocka
      --with-libxml2
      --with-json-c
      $EXTRA_CONFIGURE
      || (test -s config.log && cat config.log; exit 1)

.git-clone-bind9-qa: &git_clone_bind9-qa
  - git clone --depth 1 https://gitlab.isc.org/isc-projects/bind9-qa.git "$CI_PROJECT_DIR"/bind9-qa

# change directory to the workspace before including this
.find_python: &find_python
  - PYTHON="$(source bin/tests/system/conf.sh; echo $PYTHON)"
  - test -x "$PYTHON"

.find_pytest: &find_pytest
  - PYTEST="$(source bin/tests/system/conf.sh; echo $PYTEST)"
  - test -x "$PYTEST"

.parse_tsan: &parse_tsan
    - *find_python
    - find -name 'tsan.*' -exec "$PYTHON" "$CI_PROJECT_DIR"/bind9-qa/ci/parse_tsan.py {} \;

.check_readline_setup: &check_readline_setup
    - if [[ -n "${WITHOUT_READLINE}" ]]; then
        ! grep "^#define HAVE_READLINE" config.h;
      elif [[ -n "${WITH_READLINE}" ]]; then
        grep -e "^#define HAVE_READLINE_READLINE"
             -e "^#define HAVE_READLINE_LIBEDIT"
             -e "^#define HAVE_READLINE_EDITLINE" config.h;
      elif [[ -n "${WITH_READLINE_EDITLINE}" ]]; then
        grep "^#define HAVE_READLINE_EDITLINE" config.h;
      elif [[ -n "${WITH_READLINE_LIBEDIT}" ]]; then
        grep "^#define HAVE_READLINE_LIBEDIT" config.h;
      elif [[ -n "${WITH_READLINE_READLINE}" ]]; then
        grep "^#define HAVE_READLINE_READLINE" config.h;
      fi

.list_installed_package_versions: &list_installed_package_versions
  - echo -e "\e[0Ksection_start:`date +%s`:installed_packages_section[collapsed=true]\r\e[0KHeader of the installed packages collapsible section"
  - ( pip3 list || pip list || echo "no pip" ) 2>/dev/null
  - for cmd in "apk info --verbose" "dpkg-query --show --showformat='\${Package}-\${Version}\n'" "pkg info --quiet" "rpm -qa | sort"; do
      eval "$cmd" 2>/dev/null && break;
    done || true
  - echo -e "\e[0Ksection_end:`date +%s`:installed_packages_section\r\e[0K"

# Unpack release tarball and continue work in the extracted directory.
.unpack_release_tarball: &unpack_release_tarball
  - tar --extract --file bind-*.tar.xz
  - rm -f bind-*.tar.xz
  - cd bind-*

.build: &build_job
  <<: *default_triggering_rules
  stage: build
  before_script:
    - test -w "${CCACHE_DIR}" && export PATH="/usr/lib/ccache:${PATH}"
    - test -n "${OUT_OF_TREE_WORKSPACE}" && mkdir "${OUT_OF_TREE_WORKSPACE}" && cd "${OUT_OF_TREE_WORKSPACE}"
    - *list_installed_package_versions
  script:
    - *configure
    - *check_readline_setup
    - make -j${BUILD_PARALLEL_JOBS:-1} -k all V=1
    - test -z "${RUN_MAKE_INSTALL}" || make DESTDIR="${INSTALL_PATH}" install
    - test -z "${RUN_MAKE_INSTALL}" || DESTDIR="${INSTALL_PATH}" sh util/check-make-install.sh
    - if [[ "${CFLAGS}" == *"-fsanitize=address"* ]]; then ( ! grep -F AddressSanitizer config.log ); fi
    - test -z "${CROSS_COMPILATION}" || grep -F -A 1 "checking whether we are cross compiling" config.log | grep -q "result.*yes"
    - test -z "${CROSS_COMPILATION}" || file lib/dns/gen | grep -F -q "ELF 64-bit LSB"
    - test -z "${CROSS_COMPILATION}" || ( ! git ls-files -z --others --exclude lib/dns/gen | xargs -0 file | grep "ELF 64-bit LSB" )
    - if test -z "${OUT_OF_TREE_WORKSPACE}" && test "$(git status --porcelain | grep -Ev '\?\?' | wc -l)" -gt "0"; then git status --short; exit 1; fi
    - bin/named/named -V
  needs:
    - job: autoreconf
      artifacts: true
  artifacts:
    untracked: true
    when: always

.setup_interfaces: &setup_interfaces
    - if [ "$(id -u)" -eq "0" ]; then
        sh -x bin/tests/system/ifconfig.sh up;
      else
        sudo sh -x bin/tests/system/ifconfig.sh up;
      fi

.display_pytest_failures: &display_pytest_failures
    - awk '/^=+ FAILURES =+/{flag=1;next}/^=+.*=+$/{flag=0}flag' bin/tests/system/pytest.out.txt || true
    - awk '/^=+ ERRORS =+/{flag=1;next}/^=+.*=+$/{flag=0}flag' bin/tests/system/pytest.out.txt || true

.shotgun: &shotgun_job
  <<: *base_image
  stage: performance
  rules:  # FIXME disabled shotgun jobs temporarily due to infra issue
    # - if: '$CI_MERGE_REQUEST_DIFF_BASE_SHA != null'
    #   changes:
    #     - '**/*.c'
    #     - '**/*.h'
    #   variables:
    #     BASELINE: '$CI_MERGE_REQUEST_DIFF_BASE_SHA'
    - &shotgun_rule_mr_manual
      if: '$CI_MERGE_REQUEST_DIFF_BASE_SHA != null'
      variables:
        BASELINE: '$CI_MERGE_REQUEST_DIFF_BASE_SHA'
      when: manual  # don't run on each MR unless requested
      allow_failure: true
    # - &shotgun_rule_tag
    #   if: '$CI_PROJECT_NAMESPACE == "isc-private" && $CI_COMMIT_TAG != null'
    #   variables:
    #     SHOTGUN_ROUNDS: 3
    # - &shotgun_rule_other
    #   if: '$CI_PIPELINE_SOURCE =~ /^(api|pipeline|schedule|trigger|web)$/ && $REBASE_ONLY != "1"'
  # when using data from a single run, the overall instability of the results
  # causes quite high false positive rate, rerun the test to attemp to reduce those
  retry: 1
  script:
    - if [ -z "$BASELINE" ]; then export BASELINE=$BIND_BASELINE_VERSION; fi  # this dotenv variable can't be set in the rules section, because rules are evaluated before any jobs run
    - PIPELINE_ID=$(curl -s -X POST --fail
        -F "token=$CI_JOB_TOKEN"
        -F ref=main
        -F "variables[SHOTGUN_TEST_VERSION]=['$CI_COMMIT_REF_NAME', '$BASELINE']"
        -F "variables[SHOTGUN_DURATION]=300"
        -F "variables[SHOTGUN_ROUNDS]=$SHOTGUN_ROUNDS"
        -F "variables[SHOTGUN_TRAFFIC_MULTIPLIER]=$SHOTGUN_TRAFFIC_MULTIPLIER"
        -F "variables[SHOTGUN_SCENARIO]=$SHOTGUN_SCENARIO"
        -F "variables[SHOTGUN_EVAL_THRESHOLD_CPU_MIN]=$SHOTGUN_EVAL_THRESHOLD_CPU_MIN"
        -F "variables[SHOTGUN_EVAL_THRESHOLD_CPU_MAX]=$SHOTGUN_EVAL_THRESHOLD_CPU_MAX"
        -F "variables[SHOTGUN_EVAL_THRESHOLD_MEMORY_MIN]=$SHOTGUN_EVAL_THRESHOLD_MEMORY_MIN"
        -F "variables[SHOTGUN_EVAL_THRESHOLD_MEMORY_MAX]=$SHOTGUN_EVAL_THRESHOLD_MEMORY_MAX"
        -F "variables[SHOTGUN_EVAL_THRESHOLD_RCODE_MIN]=$SHOTGUN_EVAL_THRESHOLD_RCODE_MIN"
        -F "variables[SHOTGUN_EVAL_THRESHOLD_RCODE_MAX]=$SHOTGUN_EVAL_THRESHOLD_RCODE_MAX"
        -F "variables[SHOTGUN_EVAL_THRESHOLD_LATENCY_PCTL_MIN]=$SHOTGUN_EVAL_THRESHOLD_LATENCY_PCTL_MIN"
        -F "variables[SHOTGUN_EVAL_THRESHOLD_LATENCY_PCTL_MAX]=$SHOTGUN_EVAL_THRESHOLD_LATENCY_PCTL_MAX"
        -F "variables[SHOTGUN_EVAL_THRESHOLD_LATENCY_PCTL_DRIFT_MIN]=$SHOTGUN_EVAL_THRESHOLD_LATENCY_PCTL_DRIFT_MIN"
        -F "variables[SHOTGUN_EVAL_THRESHOLD_LATENCY_PCTL_DRIFT_MAX]=$SHOTGUN_EVAL_THRESHOLD_LATENCY_PCTL_DRIFT_MAX"
        https://gitlab.isc.org/api/v4/projects/188/trigger/pipeline | jq .id)
    - *git_clone_bind9-qa
    - >
      "$CI_PROJECT_DIR"/bind9-qa/ci/wait_shotgun.py $PIPELINE_ID
  needs:
    - job: ci-variables
      artifacts: true
  timeout: 2h

.system_test_common: &system_test_job
  <<: *default_triggering_rules
  stage: system
  before_script:
    - test -n "${OUT_OF_TREE_WORKSPACE}" && cp -r bin/tests/system/* "${OUT_OF_TREE_WORKSPACE}/bin/tests/system/" && cd "${OUT_OF_TREE_WORKSPACE}"
    - *setup_interfaces
  # This script needs to: 1) fail if the system tests fail, 2) fail if
  # the junit.xml file is broken, 3) produce the junit.xml file even if
  # the system tests fail.  Therefore, $RET is used to "cache" the
  # result of running pytest as interrupting the script immediately when
  # system tests fail would make checking the contents of the junit.xml
  # file impossible (GitLab Runner uses "set -o pipefail").
  script:
    - *find_pytest
    - *find_python
    - *git_clone_bind9-qa
    - ( if [ "${CI_DISPOSABLE_ENVIRONMENT}" = "true" ]; then sleep 3000; "$PYTHON" "${CI_PROJECT_DIR}/bind9-qa/ci/get_running_system_tests.py"; fi ) &
    - cd bin/tests/system
    - RET=0
    - >
      ("$PYTEST" --junit-xml="$CI_PROJECT_DIR"/junit_pytest.xml -n "$TEST_PARALLEL_JOBS" | tee pytest.out.txt) || RET=1
    - >
      "$PYTHON" "$CI_PROJECT_DIR"/bind9-qa/ci/postprocess_junit_files.py "$CI_PROJECT_DIR"/junit_pytest.xml --output "$CI_PROJECT_DIR"/junit.xml
    - (exit $RET)
    - '( ! grep -F "grep: warning:" pytest.out.txt )'
    - test "$CLEAN_BUILD_ARTIFACTS_ON_SUCCESS" -eq 0 || ( cd ../../.. && make clean >/dev/null 2>&1 )
  after_script:
    - test -n "${OUT_OF_TREE_WORKSPACE}" && cd "${OUT_OF_TREE_WORKSPACE}"
    - *display_pytest_failures
  artifacts:
    untracked: true
    exclude:
      - "**/__pycache__/**/*"
    when: always
    reports:
      junit: junit.xml

.system_test_make_check: &system_test_make_check_job
  <<: *system_test_job
  # This script needs to: 1) fail if the system tests fail, 2) fail if
  # the junit.xml file is broken, 3) produce the junit.xml file even if
  # the system tests fail.  Therefore, $RET is used to "cache" the
  # result of "make check" as interrupting the script immediately when
  # system tests fail would prevent the junit.xml file from being
  # produced.
  script:
    - cd bin/tests/system
    - RET=0
    # With pytest 9.0, there's the following error in pytest_ignore_collect():
    #   The (path: py.path.local) argument is deprecated, please use (collection_path: pathlib.Path).
    # This should be fixed before pytest 9.1, when it becomes ineffective.
    - if pytest --version | grep -F "pytest 9.0" >/dev/null; then echo "filterwarnings = ignore::pytest.PytestRemovedIn9Warning" >> pytest.ini; fi
    - make -j${TEST_PARALLEL_JOBS:-1} check || RET=$?
    - cd "$CI_PROJECT_DIR"
    - *find_python
    - >
      "$PYTHON" bin/tests/convert_trs_to_junit.py . > "$CI_PROJECT_DIR"/junit_system.xml
    - *git_clone_bind9-qa
    - >
      "$PYTHON" "$CI_PROJECT_DIR"/bind9-qa/ci/postprocess_junit_files.py "$CI_PROJECT_DIR"/junit_system.xml --output "$CI_PROJECT_DIR"/junit.xml
    - (exit $RET)
  after_script:
    - cat bin/tests/system/test-suite.log || true

.system_test_tsan: &system_test_tsan_job
  <<: *system_test_job
  after_script:
    - *display_pytest_failures
    - find bin/tests/system -name "*dig.*" | xargs grep "error" || true
    - *parse_tsan

.unit_test_common: &unit_test_job
  <<: *default_triggering_rules
  stage: unit
  before_script:
    - test -n "${OUT_OF_TREE_WORKSPACE}" && cd "${OUT_OF_TREE_WORKSPACE}"
  # This script needs to: 1) fail if the unit tests fail, 2) fail if the
  # junit.xml file is broken, 3) produce the junit.xml file even if the
  # unit tests fail.  Therefore, $RET is used to "cache" the result of
  # "make unit" as interrupting the script immediately when unit tests
  # fail would prevent the junit.xml file from being produced.
  script:
    - RET=0
    - make -j${TEST_PARALLEL_JOBS:-1} -k unit V=1 || RET=$?
    - *find_python
    - *git_clone_bind9-qa
    - >
      "$PYTHON" "$CI_PROJECT_DIR"/bin/tests/convert_trs_to_junit.py . > "$CI_PROJECT_DIR"/junit_unit.xml
    - >
      "$PYTHON" "$CI_PROJECT_DIR"/bind9-qa/ci/postprocess_junit_files.py "$CI_PROJECT_DIR"/junit_unit.xml --output "$CI_PROJECT_DIR"/junit.xml
    - (exit $RET)
    - test "$CLEAN_BUILD_ARTIFACTS_ON_SUCCESS" -eq 0 || make clean >/dev/null 2>&1
  artifacts:
    untracked: true
    when: always
    reports:
      junit: junit.xml

.unit_test_tsan: &unit_test_tsan_job
  <<: *unit_test_job
  after_script:
    - *parse_tsan

.docs: &docs_job
  variables:
    DOC_MAKE_TARGET: doc
  stage: docs
  script:
    - *configure
    - make -j${BUILD_PARALLEL_JOBS:-1} -k ${DOC_MAKE_TARGET} V=1
    - find doc/man/ -maxdepth 1 -name "*.[0-9]" -exec mandoc -T lint "{}" \; | ( ! grep -v -e "skipping paragraph macro. sp after" -e "unknown font, skipping request. ft C" -e "input text line longer than 80 bytes" )

.respdiff: &respdiff_job
  <<: *code_triggering_rules
  stage: system
  before_script:
    - autoreconf -fi
    - *configure
    - make -j${BUILD_PARALLEL_JOBS:-1} V=1
    - *setup_interfaces
    - *git_clone_bind9-qa
    - cd "$CI_PROJECT_DIR"/bind9-qa/respdiff
  needs: []
  artifacts:
    paths:
      - bind9-qa/respdiff
    exclude:
      - bind9-qa/respdiff/rspworkdir/data.mdb # Exclude a 10 GB file.
    untracked: true
    when: always

### Job Definitions

# Jobs in the quick-checks stage

autoreconf:
  <<: *autoconf_job

misc:
  <<: *quick_checks_job
  script:
    - sh util/checklibs.sh > checklibs.out
    - sh util/check-categories.sh
    - sh util/check-gitignore.sh
    - sh util/check-trailing-whitespace.sh
    - if git grep SYSTEMTESTTOP -- ':!.gitlab-ci.yml'; then echo 'Please use relative paths instead of $SYSTEMTESTTOP.'; exit 1; fi
    - bash util/unused-headers.sh
    - bash util/xmllint-html.sh
    # Check dangling symlinks in the repository
    - if find . -xtype l | grep .; then exit 1; fi
  artifacts:
    paths:
      - checklibs.out
    when: on_failure

black:
  <<: *quick_checks_job
  <<: *python_triggering_rules
  script:
    - black $(git ls-files '*.py')
    - git diff > black.patch
    - if test "$(git status --porcelain | grep -Ev '\?\?' | wc -l)" -gt "0"; then git status --short; exit 1; fi
  artifacts:
    paths:
      - black.patch
    expire_in: "1 week"
    when: on_failure

vulture:
  <<: *quick_checks_job
  <<: *python_triggering_rules
  script:
    - vulture --exclude "*ans.py,conftest.py,get_algorithms.py,re_compile_checker.py,isctest" --ignore-names "after_servers_start,bootstrap,pytestmark,autouse_*" bin/tests/system/

ci-variables:
  <<: *quick_checks_job
  script:
    - export BIND_BASELINE_BRANCH="$(sed -n -E "s|^m4_define\(\[bind_VERSION_MINOR\], ([0-9]+)\)dnl$|\1|p" configure.ac)"
    # When testing a .0 release, compare it against the previous development
    # release (e.g., 9.19.0 and 9.18.0 should both be compared against 9.17.22).
    - if [ "$(sed -n -E "s|^m4_define\(\[bind_VERSION_PATCH\], ([0-9]+)\)dnl$|\1|p" configure.ac)" = "0" ]; then export BIND_BASELINE_BRANCH=$((BIND_BASELINE_BRANCH - 1 - (BIND_BASELINE_BRANCH % 2))); fi
    - BIND_BASELINE_VERSION="$(curl -s "https://gitlab.isc.org/api/v4/projects/1/repository/tags?search=^v9.${BIND_BASELINE_BRANCH}&order_by=version" | jq -r ".[0].name")"
    - echo "BIND_BASELINE_VERSION=$BIND_BASELINE_VERSION" >> ci_vars.env
  needs:
    - job: autoreconf
      artifacts: true
  artifacts:
    reports:
      dotenv: ci_vars.env

ci-orphaned-anchors:
  <<: *quick_checks_job
  script:
    - *git_clone_bind9-qa
    - >
      "$CI_PROJECT_DIR"/bind9-qa/ci-orphaned-anchors/check_orphaned_anchors_ci.py .gitlab-ci.yml
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
      changes:
        - .gitlab-ci.yml

clang-format:
  <<: *quick_checks_job
  rules:
    - if: '$CI_MERGE_REQUEST_DIFF_BASE_SHA != null'
      changes:
        - '**/*.c'
        - '**/*.h'
        - '**/.clang-format'
        - '**/.clang-format.headers'
    - *rule_mr_manual
    - *rule_tag
    - *rule_source_other_than_mr
  script:
    - if [ -r .clang-format ]; then "${CLANG_FORMAT}" -i -style=file $(git ls-files '*.c' '*.h'); fi
    - git diff > clang-format.patch
    - if test "$(git status --porcelain | grep -Ev '\?\?' | wc -l)" -gt "0"; then git status --short; exit 1; fi
  artifacts:
    paths:
      - clang-format.patch
    expire_in: "1 week"
    when: on_failure

doctest:
  <<: *quick_checks_job
  # This script needs to: 1) fail if the doctest fails, 2) fail if
  # the junit.xml file is broken, 3) produce the junit.xml file even if
  # the doctest fails.  Therefore, $RET is used to "cache" the
  # result of running pytest as interrupting the script immediately when
  # a doctest fails would make checking the contents of the junit.xml
  # file impossible (GitLab Runner uses "set -o pipefail").
  script:
    - *configure
    - *find_python
    - *find_pytest
    - export TOP_BUILDDIR=$(cat bin/tests/system/isctest/vars/.ac_vars/TOP_BUILDDIR)
    - cd bin/tests/system/isctest
    - RET=0
    - >
      "$PYTEST" --noconftest --doctest-modules --junit-xml="$CI_PROJECT_DIR/junit_doctest.xml" || RET=1
    - *git_clone_bind9-qa
    - >
      "$PYTHON" "$CI_PROJECT_DIR"/bind9-qa/ci/postprocess_junit_files.py "$CI_PROJECT_DIR/junit_doctest.xml" --output "$CI_PROJECT_DIR/junit.xml"
    - (exit $RET)
  needs:
    - job: autoreconf
      artifacts: true
  artifacts:
    reports:
      junit:
        - junit.xml

pylint:
  <<: *quick_checks_job
  <<: *python_triggering_rules
  variables:
    PYTHONPATH: "${CI_PROJECT_DIR}/bin/tests/system"
  script:
    - pylint --rcfile $CI_PROJECT_DIR/.pylintrc $(git ls-files '*.py' | grep -vE '(ans\.py|dangerfile\.py|^bin/tests/system/|^contrib/)')
      # Ignore Pylint wrong-import-position error in system test to enable use of pytest.importorskip
    - pylint --rcfile $CI_PROJECT_DIR/.pylintrc --load-plugins re_compile_checker --disable=wrong-import-position $(git ls-files 'bin/tests/system/*.py' | grep -vE '(ans\.py|vulture_ignore_list\.py)')

reuse:
  <<: *quick_checks_job
  image:
    name: docker.io/fsfe/reuse:latest
    entrypoint: [""]
  script:
    - reuse lint

shfmt:
  <<: *quick_checks_job
  <<: *shell_triggering_rules
  script:
    - shfmt -w -i 2 -ci -bn . $(find . -name "*.sh.in")
    - git diff > shfmt.patch
    - if test "$(git status --porcelain | grep -Ev '\?\?' | wc -l)" -gt "0"; then git status --short; exit 1; fi
  artifacts:
    paths:
      - shfmt.patch
    expire_in: "1 week"
    when: on_failure

danger:
  <<: *quick_checks_job
  script:
    - pip install git+https://gitlab.isc.org/isc-projects/hazard.git
    - hazard
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'

mypy:
  <<: *quick_checks_job
  <<: *python_triggering_rules
  script:
    - mypy "bin/tests/system/isctest/"

linkcheck:
  <<: *quick_checks_job
  script:
    # Some domains tested by linkchecker may think that we connect to them too
    # often and will refuse connection or reply with and error code, which
    # makes this job fail. Let's check links only on Wednesdays.
    - if [ "$(date +%w)" != "3" ]; then exit 0; fi
    - pushd doc/arm/ > /dev/null && sphinx-build -b linkcheck . linkcheck_output/
  artifacts:
    paths:
      - doc/arm/linkcheck_output/
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'

generate-stress-test-configs:
  <<: *quick_checks_job
  script:
    - *git_clone_bind9-qa
    - >
      "$CI_PROJECT_DIR"/bind9-qa/stress/generate_stress_test_configs.py > stress-test-configs.yml
  artifacts:
    paths:
      - stress-test-configs.yml

generate-tsan-stress-test-configs:
  <<: *quick_checks_job
  script:
    - *git_clone_bind9-qa
    - >
      "$CI_PROJECT_DIR"/bind9-qa/stress/generate_tsan_stress_jobs.py > tsan-stress-test-configs.yml
  artifacts:
    paths:
      - tsan-stress-test-configs.yml
  when: manual

# Jobs in the other-checks stage

checkbashisms:
  <<: *other_checks_job
  <<: *shell_triggering_rules
  script:
    - checkbashisms $(find . -path './.git' -prune -o -type f -exec sh -c 'head -n 1 "{}" | grep -qsF "#!/bin/sh"' \; -print)

coccinelle:
  <<: *other_checks_job
  rules:
    - if: '$CI_MERGE_REQUEST_DIFF_BASE_SHA != null'
      changes:
        - '**/*.c'
        - '**/*.h'
        - 'cocci/**'
        - 'util/check-cocci.sh'
    - *rule_mr_manual
    - *rule_tag
    - *rule_source_other_than_mr
  script:
    - util/check-cocci.sh
    - if test "$(git status --porcelain | grep -Ev '\?\?' | wc -l)" -gt "0"; then git status --short; exit 1; fi

# Jobs for scan-build builds on Debian 13 "trixie" (amd64)

.scan_build: &scan_build
  - ${SCAN_BUILD} --html-title="BIND 9 ($CI_COMMIT_SHORT_SHA)"
                  --keep-cc
                  --status-bugs
                  --keep-going
                  -o scan-build.reports make -j${BUILD_PARALLEL_JOBS:-1} all V=1

scan-build:
  <<: *other_checks_job
  variables:
    CC: "${CLANG}"
    CFLAGS: "${CFLAGS_COMMON}"
    CONFIGURE: "${SCAN_BUILD} ./configure"
    EXTRA_CONFIGURE: "--with-libidn2"
  before_script:
    - *list_installed_package_versions
  script:
    - *configure
    - *scan_build
  needs:
    - job: autoreconf
      artifacts: true
  artifacts:
    paths:
      - scan-build.reports/
    when: on_failure

# Coverity Scan analysis upload

.coverity_prep: &coverity_prep
  - curl --output /tmp/cov-analysis-linux64.md5 https://scan.coverity.com/download/linux64
         --form project=$COVERITY_SCAN_PROJECT_NAME
         --form token=$COVERITY_SCAN_TOKEN
         --form md5=1
  - curl --output /tmp/cov-analysis-linux64.tgz https://scan.coverity.com/download/linux64
         --form project=$COVERITY_SCAN_PROJECT_NAME
         --form token=$COVERITY_SCAN_TOKEN
  - test "$(md5sum /tmp/cov-analysis-linux64.tgz | awk '{ print $1 }')" = "$(cat /tmp/cov-analysis-linux64.md5)"
  - tar --extract --gzip --file=/tmp/cov-analysis-linux64.tgz --directory=/tmp
  - test -d /tmp/cov-analysis-linux64-*

.coverity_build: &coverity_build
  - /tmp/cov-analysis-linux64-*/bin/cov-build --dir /tmp/cov-int sh -c 'make -j${BUILD_PARALLEL_JOBS:-1} -k all V=1'
  - tar --create --gzip --file=/tmp/cov-int.tar.gz --directory /tmp cov-int
  - curl -v https://scan.coverity.com/builds?project=$COVERITY_SCAN_PROJECT_NAME
        --form token=$COVERITY_SCAN_TOKEN
        --form email=bind-changes@isc.org
        --form file=@/tmp/cov-int.tar.gz
        --form version="$(git rev-parse --short HEAD)"
        --form description="$(git rev-parse --short HEAD) / $CI_COMMIT_TITLE / $CI_COMMIT_REF_NAME:$CI_PIPELINE_ID" 2>&1
        | tee curl-response.txt
  - grep -q 'Build successfully submitted' curl-response.txt

coverity:
  <<: *other_checks_job
  variables:
    CC: gcc
    CFLAGS: "${CFLAGS_COMMON} -Og"
    EXTRA_CONFIGURE: "--with-libidn2"
  script:
    - *coverity_prep
    - *configure
    - *coverity_build
  after_script:
    - mv -v /tmp/cov-int.tar.gz ${CI_PROJECT_DIR}/
  needs:
    - job: autoreconf
      artifacts: true
  artifacts:
    paths:
      - curl-response.txt
      - cov-int.tar.gz
    expire_in: "1 week"
    when: on_failure
  rules:
    - if: '$COVERITY_SCAN_PROJECT_NAME != null && $COVERITY_SCAN_TOKEN != null'

tarball-create:
  <<: *build_job
  <<: *base_image
  script:
    - ./configure --enable-maintainer-mode
    - make maintainer-clean
    - autoreconf -fi
    - ./configure --enable-maintainer-mode
    - make -j${BUILD_PARALLEL_JOBS:-1} all V=1
    - if test "$(git status --porcelain | grep -Ev '\?\?' | wc -l)" -gt "0"; then git status --short; git diff > diff.patch; exit 1; fi
    - make -j${BUILD_PARALLEL_JOBS:-1} dist V=1
  artifacts:
    paths:
      - diff.patch
      - bind-*.tar.xz
    when: always
  needs:
    - job: autoreconf
      artifacts: true

# Jobs for doc builds on Debian 13 "trixie" (amd64)

changelog:
  <<: *base_image
  <<: *docs_job
  rules:
    - if: '$CI_MERGE_REQUEST_TITLE =~ /\s(dev|usr|pkg):/'
  variables:
    DOC_MAKE_TARGET: html
  before_script:
    - echo -e "$CI_MERGE_REQUEST_TITLE\n" > commitmsg
    - sed -i 's/^Draft:\s*//' commitmsg
    - echo -e "$CI_MERGE_REQUEST_DESCRIPTION" >> commitmsg
    - git commit --allow-empty -F commitmsg
    - export CHANGELOG=$(ls doc/changelog/changelog-9.* | sort --version-sort | tail -n 1)
    - printf "\n" >> $CHANGELOG
    - ./contrib/gitchangelog/gitchangelog.py HEAD^..HEAD >> $CHANGELOG
  after_script:
    - git diff
  needs:
    - job: autoreconf
      artifacts: true
  artifacts:
    untracked: true

docs:
  <<: *default_triggering_rules
  <<: *base_image
  <<: *docs_job
  before_script:
    - *list_installed_package_versions
  needs:
    - job: autoreconf
      artifacts: true
  artifacts:
    untracked: true

docs:tarball:
  <<: *default_triggering_rules
  <<: *base_image
  <<: *docs_job
  before_script:
    - *unpack_release_tarball
  needs:
    - job: tarball-create
      artifacts: true

docs:pdf:
  <<: *api_pipelines_schedules_tags_triggers_web_triggering_rules
  <<: *base_image
  stage: docs
  before_script:
    - apt-get update
    - apt-get -y install qpdf texlive-full texlive-xetex xindy
  script:
    - *configure
    - make -C doc/arm/ pdf V=1
    - qpdf --check doc/arm/_build/latex/Bv9ARM.pdf
  needs:
    - job: autoreconf
      artifacts: true
  artifacts:
    untracked: true

# Job detecting named.conf breakage introduced since the previous point release

cross-version-config-tests:
  stage: system
  <<: *base_image
  <<: *default_triggering_rules
  variables:
    CC: gcc
    CFLAGS: "${CFLAGS_COMMON}"
    # Disable option checking to prevent problems with new default options in
    # the &configure anchor.
    EXTRA_CONFIGURE: "--disable-option-checking"
  script:
    - *configure
    - *setup_interfaces
    - make -j${BUILD_PARALLEL_JOBS:-1}
    - *find_python
    - *find_pytest
    - git clone --branch "${BIND_BASELINE_VERSION}" --depth 1 https://gitlab.isc.org/isc-projects/bind9.git "bind-${BIND_BASELINE_VERSION}"
    - cd "bind-${BIND_BASELINE_VERSION}"
    - autoreconf -fi
    - *configure
    - make -j${BUILD_PARALLEL_JOBS:-1}
    # The cross-version-config-tests job would fail when a system test is
    # removed from the upcoming release. To avoid this, remove the system test
    # also from the $BIND_BASELINE_VERSION.
    - find bin/tests/system/ -mindepth 1 -maxdepth 1 -type d -exec sh -c 'test -e ../"$0" || rm -rfv -- "$0"' {} \;
    - cd bin/tests/system
    # Run the setup phase of all system tests in the most recently tagged BIND 9
    # release using the binaries built for the current BIND 9 version.  This
    # intends to detect obvious backward compatibility issues with the latter.
    - sed -i "s|export \(.*\)=\$TOP_BUILDDIR|export \1=\$CI_PROJECT_DIR|" conf.sh
    # This script needs to: 1) fail if the tests fail, 2) fail if
    # the junit.xml file is broken, 3) produce the junit.xml file even if
    # the  tests fail.  Therefore, $RET is used to "cache" the
    # result of running pytest as interrupting the script immediately when
    # system tests fail would make checking the contents of the junit.xml
    # file impossible (GitLab Runner uses "set -o pipefail").
    - RET=0
    - >
      "$PYTEST" --setup-only --junit-xml="$CI_PROJECT_DIR"/junit_pytest.xml -n "${TEST_PARALLEL_JOBS:-1}" || RET=1
    - *git_clone_bind9-qa
    - >
      "$PYTHON" "$CI_PROJECT_DIR"/bind9-qa/ci/postprocess_junit_files.py "$CI_PROJECT_DIR"/junit_pytest.xml --output "$CI_PROJECT_DIR"/junit.xml
    - (exit $RET)
  needs:
    - job: autoreconf
      artifacts: true
    - job: ci-variables
      artifacts: true
  artifacts:
    reports:
      junit: junit.xml
    paths:
      - bind-*
      - junit.xml
    untracked: true
    expire_in: "1 day"
    when: always

# Jobs for regular GCC builds on Alpine Linux 3.23 (amd64)

gcc:alpine3.23:amd64:
  variables:
    CC: gcc
    CFLAGS: "${CFLAGS_COMMON}"
    EXTRA_CONFIGURE: "${WITHOUT_READLINE}"
  <<: *alpine_3_23_amd64_image
  <<: *build_job

system:gcc:alpine3.23:amd64:
  <<: *alpine_3_23_amd64_image
  <<: *system_test_job
  needs:
    - job: gcc:alpine3.23:amd64
      artifacts: true

unit:gcc:alpine3.23:amd64:
  <<: *alpine_3_23_amd64_image
  <<: *unit_test_job
  needs:
    - job: gcc:alpine3.23:amd64
      artifacts: true

# Jobs for regular GCC builds on Alma Linux 8 (amd64)

gcc:almalinux8:amd64:
  variables:
    CC: gcc
    CFLAGS: "${CFLAGS_COMMON}"
    EXTRA_CONFIGURE: "--with-libidn2"
  <<: *almalinux_8_amd64_image
  <<: *build_job

system:gcc:almalinux8:amd64:
  <<: *almalinux_8_amd64_image
  <<: *system_test_job
  <<: *extra_system_tests_triggering_rules
  needs:
    - job: gcc:almalinux8:amd64
      artifacts: true

unit:gcc:almalinux8:amd64:
  <<: *almalinux_8_amd64_image
  <<: *unit_test_job
  <<: *api_pipelines_schedules_tags_triggers_web_triggering_rules
  needs:
    - job: gcc:almalinux8:amd64
      artifacts: true

# Jobs for regular GCC builds on Alma Linux 9 (amd64)

gcc:almalinux9:amd64:
  variables:
    CC: gcc
    CFLAGS: "${CFLAGS_COMMON}"
    EXTRA_CONFIGURE: "--with-libidn2 --disable-developer"
  <<: *almalinux_9_amd64_image
  <<: *build_job

system:gcc:almalinux9:amd64:
  <<: *almalinux_9_amd64_image
  <<: *system_test_job
  needs:
    - job: gcc:almalinux9:amd64
      artifacts: true

unit:gcc:almalinux9:amd64:
  <<: *almalinux_9_amd64_image
  <<: *unit_test_job
  needs:
    - job: gcc:almalinux9:amd64
      artifacts: true

# Jobs for regular GCC builds on Alma Linux 10 (amd64)

gcc:almalinux10:amd64:
  variables:
    CC: gcc
    CFLAGS: "${CFLAGS_COMMON}"
    EXTRA_CONFIGURE: "--with-libidn2"
  <<: *almalinux_10_amd64_image
  <<: *build_job

system:gcc:almalinux10:amd64:
  <<: *almalinux_10_amd64_image
  <<: *system_test_job
  needs:
    - job: gcc:almalinux10:amd64
      artifacts: true

unit:gcc:almalinux10:amd64:
  <<: *almalinux_10_amd64_image
  <<: *unit_test_job
  needs:
    - job: gcc:almalinux10:amd64
      artifacts: true

gcc:tarball:nosphinx:
  variables:
    CC: gcc
    CFLAGS: "${CFLAGS_COMMON}"
    EXTRA_CONFIGURE: "--with-libidn2 --disable-developer"
    RUN_MAKE_INSTALL: 1
  <<: *almalinux_9_amd64_image
  <<: *build_job
  before_script:
    - *list_installed_package_versions
    - (! command -v sphinx-build >/dev/null)
    - *unpack_release_tarball
  needs:
    - job: tarball-create
      artifacts: true

# Jobs for regular GCC builds on Debian 12 "bookworm" (amd64)

gcc:bookworm:amd64:
  variables:
    CC: gcc
    CFLAGS: "${CFLAGS_COMMON} -DOPENSSL_API_COMPAT=10100"
    # See https://gitlab.isc.org/isc-projects/bind9/-/issues/3444
    EXTRA_CONFIGURE: "--without-jemalloc"
  <<: *debian_bookworm_amd64_image
  <<: *build_job

system:gcc:bookworm:amd64:
  <<: *debian_bookworm_amd64_image
  <<: *system_test_job
  # Set up environment variables that allow the "keyfromlabel" system test to be run
  variables:
    DEFAULT_OPENSSL_CONF: "/etc/ssl/openssl.cnf"
    OPENSSL_CONF: "/var/tmp/etc/openssl.cnf"
    SOFTHSM2_CONF: "/var/tmp/softhsm2/softhsm2.conf"
    SOFTHSM2_MODULE: "/usr/lib/softhsm/libsofthsm2.so"
  needs:
    - job: gcc:bookworm:amd64
      artifacts: true

unit:gcc:bookworm:amd64:
  <<: *debian_bookworm_amd64_image
  <<: *unit_test_job
  needs:
    - job: gcc:bookworm:amd64
      artifacts: true

# Jobs for regular GCC builds on Debian 13 "trixie" (amd64)

gcc:trixie:amd64:
  variables:
    CC: gcc
    CFLAGS: "${CFLAGS_COMMON} --coverage -O0"
    EXTRA_CONFIGURE: "--with-libidn2 ${WITH_READLINE_LIBEDIT}"
    RUN_MAKE_INSTALL: 1
  <<: *debian_trixie_amd64_image
  <<: *build_job

system:gcc:trixie:amd64:
  <<: *debian_trixie_amd64_image
  <<: *system_test_job
  variables:
    CI_ENABLE_LONG_TESTS: 1
    CLEAN_BUILD_ARTIFACTS_ON_SUCCESS: 0
    TZ: Australia/Sydney
  # using artifacts from unit test job is required for gcov
  needs:
    - job: unit:gcc:trixie:amd64
      artifacts: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      variables:
        CI_ENABLE_LIVE_INTERNET_TESTS: 1
    - *default_triggering_rules_list

unit:gcc:trixie:amd64:
  <<: *debian_trixie_amd64_image
  <<: *unit_test_job
  variables:
    CI_ENABLE_LONG_TESTS: 1
    CLEAN_BUILD_ARTIFACTS_ON_SUCCESS: 0
  needs:
    - job: gcc:trixie:amd64
      artifacts: true

# Build job for cross-compiled GCC builds on 64-bit Debian 13 "trixie"
# (amd64) with 32-bit BIND 9.

gcc:trixie:amd64cross32:
  variables:
    CFLAGS: "${CFLAGS_COMMON}"
    CROSS_COMPILATION: 1
    EXTRA_CONFIGURE: "--build=x86_64-linux-gnu --host=i686-linux-gnu --with-libidn2 ${WITH_READLINE_LIBEDIT}"
  <<: *debian_trixie_amd64cross32_image
  <<: *build_job

# Jobs for regular GCC builds on Debian "sid" (amd64)
# Also tests configration option: --without-lmdb.

gcc:sid:amd64:
  variables:
    CC: gcc
    CFLAGS: "${CFLAGS_COMMON} -O3"
    EXTRA_CONFIGURE: "--with-libidn2 --without-lmdb ${WITH_READLINE}"
    GNUMAKEFLAGS: "--shuffle=reverse"
  <<: *debian_sid_amd64_image
  <<: *build_job

system:gcc:sid:amd64:
  <<: *debian_sid_amd64_image
  <<: *system_test_job
  <<: *extra_system_tests_triggering_rules
  needs:
    - job: gcc:sid:amd64
      artifacts: true

unit:gcc:sid:amd64:
  <<: *debian_sid_amd64_image
  <<: *unit_test_job
  <<: *api_pipelines_schedules_tags_triggers_web_triggering_rules
  needs:
    - job: gcc:sid:amd64
      artifacts: true

# Job for out-of-tree GCC build on Debian 13 "trixie" (amd64)
# Also tests configration option: --with-lmdb.

gcc:out-of-tree:
  variables:
    CC: gcc
    CFLAGS: "${CFLAGS_COMMON} -Og"
    CONFIGURE: "${CI_PROJECT_DIR}/configure"
    EXTRA_CONFIGURE: "--with-libidn2 --with-lmdb"
    RUN_MAKE_INSTALL: 1
    OUT_OF_TREE_WORKSPACE: workspace
  <<: *base_image
  <<: *build_job

system:gcc:out-of-tree:
  variables:
    OUT_OF_TREE_WORKSPACE: workspace
  needs:
    - job: gcc:out-of-tree
      artifacts: true
  <<: *base_image
  <<: *system_test_job

unit:gcc:out-of-tree:
  variables:
    OUT_OF_TREE_WORKSPACE: workspace
  needs:
    - job: gcc:out-of-tree
      artifacts: true
  <<: *base_image
  <<: *unit_test_job

# Jobs for tarball GCC builds on Debian 13 "trixie" (amd64)

gcc:tarball:
  variables:
    CC: gcc
    EXTRA_CONFIGURE: "--with-libidn2"
    RUN_MAKE_INSTALL: 1
  <<: *base_image
  <<: *build_job
  before_script:
    - *list_installed_package_versions
    - *unpack_release_tarball
  needs:
    - job: tarball-create
      artifacts: true

system:gcc:tarball:
  <<: *base_image
  <<: *system_test_job
  before_script:
    - cd bind-*
    - *setup_interfaces
  after_script:
    - cd bind-*
    - *display_pytest_failures
  needs:
    - job: gcc:tarball
      artifacts: true

unit:gcc:tarball:
  <<: *base_image
  <<: *unit_test_job
  before_script:
    - cd bind-*
  needs:
    - job: gcc:tarball
      artifacts: true

# Jobs for debug GCC builds on openSUSE Tumbleweed (amd64)

gcc:tumbleweed:amd64:
  variables:
    CC: gcc
    CFLAGS: "${CFLAGS_COMMON} -DDEBUG"
    # NOTE: Testing DNSRPS-enabled builds currently requires an
    # operating system with glibc 2.34+.  This requirement will go away
    # once the DNSRPS dynamic loading code gets reworked to use libuv's
    # dlopen() API.
    #
    # NOTE: This does *not* enable testing of the DNSRPS feature itself.
    # Doing that requires a DNSRPS provider library to be present on the
    # test host.
    EXTRA_CONFIGURE: "--enable-dnsrps --enable-dnsrps-dl --with-libidn2 --without-gssapi ${WITH_READLINE_READLINE}"
    GNUMAKEFLAGS: "--shuffle=random"
  <<: *tumbleweed_latest_amd64_image
  <<: *build_job

system:gcc:tumbleweed:amd64:
  <<: *tumbleweed_latest_amd64_image
  <<: *system_test_make_check_job
  needs:
    - job: gcc:tumbleweed:amd64
      artifacts: true

unit:gcc:tumbleweed:amd64:
  <<: *tumbleweed_latest_amd64_image
  <<: *unit_test_job
  needs:
    - job: gcc:tumbleweed:amd64
      artifacts: true

# Jobs for regular GCC builds on Ubuntu 22.04 Jammy Jellyfish (amd64)

gcc:jammy:amd64:
  variables:
    CC: gcc
    CFLAGS: "${CFLAGS_COMMON} -O2"
    EXTRA_CONFIGURE: "--with-libidn2 --disable-dnstap --without-cmocka --without-gssapi --disable-geoip --disable-doh"
  <<: *ubuntu_jammy_amd64_image
  <<: *build_job

system:gcc:jammy:amd64:
  <<: *ubuntu_jammy_amd64_image
  <<: *system_test_job
  <<: *extra_system_tests_triggering_rules
  needs:
    - job: gcc:jammy:amd64
      artifacts: true

unit:gcc:jammy:amd64:
  <<: *ubuntu_jammy_amd64_image
  <<: *unit_test_job
  <<: *api_pipelines_schedules_tags_triggers_web_triggering_rules
  needs:
    - job: gcc:jammy:amd64
      artifacts: true

# Jobs for regular GCC builds on Ubuntu 24.04 Noble Numbat (amd64)

gcc:noble:amd64:
  variables:
    CC: gcc
    CFLAGS: "${CFLAGS_COMMON}"
    EXTRA_CONFIGURE: "--with-libidn2"
  <<: *ubuntu_noble_amd64_image
  <<: *build_job

system:gcc:noble:amd64:
  <<: *ubuntu_noble_amd64_image
  <<: *system_test_job
  needs:
    - job: gcc:noble:amd64
      artifacts: true

unit:gcc:noble:amd64:
  <<: *ubuntu_noble_amd64_image
  <<: *unit_test_job
  needs:
    - job: gcc:noble:amd64
      artifacts: true

# Jobs for ASAN builds on Fedora 43 (amd64)

gcc:asan:
  variables:
    CC: gcc
    CFLAGS: "${CFLAGS_COMMON} -fsanitize=address,undefined"
    LDFLAGS: "-fsanitize=address,undefined"
    EXTRA_CONFIGURE: "--with-libidn2 --without-jemalloc"
  <<: *fedora_43_amd64_image
  <<: *build_job

system:gcc:asan:
  variables:
    LSAN_OPTIONS: "suppressions=$CI_PROJECT_DIR/suppr-lsan.txt"
  <<: *fedora_43_amd64_image
  <<: *system_test_job
  needs:
    - job: gcc:asan
      artifacts: true

unit:gcc:asan:
  <<: *fedora_43_amd64_image
  <<: *unit_test_job
  needs:
    - job: gcc:asan
      artifacts: true

clang:asan:
  variables:
    CC: ${CLANG}
    CFLAGS: "${CFLAGS_COMMON} -fsanitize=address,undefined"
    LDFLAGS: "-fsanitize=address,undefined"
    EXTRA_CONFIGURE: "--with-libidn2 --without-jemalloc"
  <<: *base_image
  <<: *build_job

system:clang:asan:
  <<: *base_image
  <<: *system_test_job
  <<: *extra_system_tests_triggering_rules
  needs:
    - job: clang:asan
      artifacts: true

unit:clang:asan:
  <<: *base_image
  <<: *unit_test_job
  <<: *api_pipelines_schedules_tags_triggers_web_triggering_rules
  needs:
    - job: clang:asan
      artifacts: true

# Jobs for TSAN builds on Fedora 43 (amd64)

gcc:tsan:
  variables:
    CC: gcc
    CFLAGS: "${CFLAGS_COMMON} -Wno-stringop-overread -ggdb -O2 -fsanitize=thread"
    LDFLAGS: "-fsanitize=thread"
    OPENSSL_CFLAGS: -I/opt/tsan/include
    OPENSSL_LIBS: -L/opt/tsan/lib -lssl -lcrypto
    EXTRA_CONFIGURE: "--with-libidn2 --enable-pthread-rwlock --without-jemalloc PKG_CONFIG_PATH=/opt/tsan/lib/pkgconfig"
  <<: *tsan_fedora_43_amd64_image
  <<: *build_job

system:gcc:tsan:
  variables:
    TSAN_OPTIONS: "${TSAN_OPTIONS_FEDORA}"
  <<: *tsan_fedora_43_amd64_image
  <<: *system_test_tsan_job
  <<: *extra_system_tests_triggering_rules
  needs:
    - job: gcc:tsan
      artifacts: true

unit:gcc:tsan:
  variables:
    TSAN_OPTIONS: "${TSAN_OPTIONS_FEDORA}"
  <<: *tsan_fedora_43_amd64_image
  <<: *unit_test_tsan_job
  <<: *api_pipelines_schedules_tags_triggers_web_triggering_rules
  needs:
    - job: gcc:tsan
      artifacts: true

clang:tsan:
  <<: *tsan_debian_trixie_amd64_image
  <<: *build_job
  variables:
    CC: "${CLANG}"
    CFLAGS: "${CFLAGS_COMMON} -ggdb -O2 -fsanitize=thread"
    LDFLAGS: "-fsanitize=thread"
    OPENSSL_CFLAGS: -I/opt/tsan/include
    OPENSSL_LIBS: -L/opt/tsan/lib -lssl -lcrypto
    EXTRA_CONFIGURE: "--with-libidn2 --enable-pthread-rwlock --without-jemalloc PKG_CONFIG_PATH=/opt/tsan/lib/pkgconfig"

system:clang:tsan:
  variables:
    TSAN_OPTIONS: "${TSAN_OPTIONS_DEBIAN}"
  <<: *tsan_debian_trixie_amd64_image
  <<: *system_test_tsan_job
  needs:
    - job: clang:tsan
      artifacts: true

unit:clang:tsan:
  variables:
    TSAN_OPTIONS: "${TSAN_OPTIONS_DEBIAN}"
  <<: *tsan_debian_trixie_amd64_image
  <<: *unit_test_tsan_job
  needs:
    - job: clang:tsan
      artifacts: true

tsan:stress:
  <<: *default_triggering_rules
  stage: system
  variables:
    PARENT_PIPELINE_ID: $CI_PIPELINE_ID
  trigger:
    include:
      - artifact: tsan-stress-test-configs.yml
        job: generate-tsan-stress-test-configs
  needs:
    - job: generate-tsan-stress-test-configs
      artifacts: true
    - job: gcc:tsan
      artifacts: true
    - job: clang:tsan
      artifacts: true

# Jobs for Clang builds on Debian 13 "trixie" (amd64)

clang:trixie:amd64:
  variables:
    CC: ${CLANG}
    CFLAGS: "${CFLAGS_COMMON}"
    RUN_MAKE_INSTALL: 1
  <<: *debian_trixie_amd64_image
  <<: *build_job

system:clang:trixie:amd64:
  <<: *debian_trixie_amd64_image
  <<: *system_test_job
  <<: *extra_system_tests_triggering_rules
  needs:
    - job: clang:trixie:amd64
      artifacts: true

unit:clang:trixie:amd64:
  <<: *debian_trixie_amd64_image
  <<: *unit_test_job
  <<: *api_pipelines_schedules_tags_triggers_web_triggering_rules
  needs:
    - job: clang:trixie:amd64
      artifacts: true

# Jobs for Clang builds on FreeBSD 13 (amd64)

clang:freebsd13:amd64:
  variables:
    CC: clang
    CFLAGS: "${CFLAGS_COMMON} -Og"
    GIT_CLONE_PATH: "${GIT_CLONE_PATH_INSTANCE_EXECUTOR}"
    # Use MIT Kerberos5 for BIND 9 GSS-API support because of FreeBSD Heimdal
    # incompatibility; see https://bugs.freebsd.org/275241.
    EXTRA_CONFIGURE: "${WITH_READLINE_EDITLINE} --with-gssapi=/usr/local/bin/krb5-config"
  <<: *build_job
  <<: *freebsd_autoscaler_13_amd64_tags

system:clang:freebsd13:amd64:
  variables:
    GIT_CLONE_PATH: "${GIT_CLONE_PATH_INSTANCE_EXECUTOR}"
  needs:
    - job: clang:freebsd13:amd64
      artifacts: true
  <<: *system_test_job
  <<: *freebsd_autoscaler_13_amd64_tags
  <<: *extra_system_tests_triggering_rules

unit:clang:freebsd13:amd64:
  variables:
    GIT_CLONE_PATH: "${GIT_CLONE_PATH_INSTANCE_EXECUTOR}"
  needs:
    - job: clang:freebsd13:amd64
      artifacts: true
  <<: *unit_test_job
  <<: *freebsd_autoscaler_13_amd64_tags
  <<: *api_pipelines_schedules_tags_triggers_web_triggering_rules

# Jobs for Clang builds on FreeBSD 14 (amd64)

clang:freebsd14:amd64:
  variables:
    CC: clang
    CFLAGS: "${CFLAGS_COMMON} -Og"
    GIT_CLONE_PATH: "${GIT_CLONE_PATH_INSTANCE_EXECUTOR}"
    # Use MIT Kerberos5 for BIND 9 GSS-API support because of FreeBSD Heimdal
    # incompatibility; see https://bugs.freebsd.org/275241.
    EXTRA_CONFIGURE: "${WITH_READLINE_EDITLINE} --with-gssapi=/usr/local/bin/krb5-config"
  <<: *build_job
  <<: *freebsd_autoscaler_14_amd64_tags

system:clang:freebsd14:amd64:
  variables:
    GIT_CLONE_PATH: "${GIT_CLONE_PATH_INSTANCE_EXECUTOR}"
  needs:
    - job: clang:freebsd14:amd64
      artifacts: true
  <<: *system_test_job
  <<: *freebsd_autoscaler_14_amd64_tags

unit:clang:freebsd14:amd64:
  variables:
    GIT_CLONE_PATH: "${GIT_CLONE_PATH_INSTANCE_EXECUTOR}"
  needs:
    - job: clang:freebsd14:amd64
      artifacts: true
  <<: *unit_test_job
  <<: *freebsd_autoscaler_14_amd64_tags

# Jobs for Clang builds on FreeBSD 15 (amd64)

clang:freebsd15:amd64:
  variables:
    CC: clang
    CFLAGS: "${CFLAGS_COMMON} -Og"
    GIT_CLONE_PATH: "${GIT_CLONE_PATH_INSTANCE_EXECUTOR}"
    # Use MIT Kerberos5 for BIND 9 GSS-API support because of FreeBSD Heimdal
    # incompatibility; see https://bugs.freebsd.org/275241.
    EXTRA_CONFIGURE: "${WITH_READLINE_EDITLINE} --with-gssapi=/usr/local/bin/krb5-config"
  <<: *build_job
  <<: *freebsd_autoscaler_15_amd64_tags

system:clang:freebsd15:amd64:
  variables:
    GIT_CLONE_PATH: "${GIT_CLONE_PATH_INSTANCE_EXECUTOR}"
  needs:
    - job: clang:freebsd15:amd64
      artifacts: true
  <<: *system_test_job
  <<: *freebsd_autoscaler_15_amd64_tags

unit:clang:freebsd15:amd64:
  variables:
    GIT_CLONE_PATH: "${GIT_CLONE_PATH_INSTANCE_EXECUTOR}"
  needs:
    - job: clang:freebsd15:amd64
      artifacts: true
  <<: *unit_test_job
  <<: *freebsd_autoscaler_15_amd64_tags

# Job producing a release directory

release:
  <<: *base_image
  stage: release
  script:
    - export RELEASE_DIRECTORY="bind-${CI_COMMIT_TAG}-release"
    - export BIND_VERSION="bind-${CI_COMMIT_TAG#v}"
    # Prepare release tarball contents (tarballs + documentation)
    - mkdir -p "${RELEASE_DIRECTORY}/doc/arm"
    - pushd "${RELEASE_DIRECTORY}"
    - mv "../${BIND_VERSION}.tar.xz" .
    - tar --extract --file="${BIND_VERSION}.tar.xz" --strip-components=1 "${BIND_VERSION}"/{COPYRIGHT,LICENSE,README.md,srcid}
    - mv "../doc/arm/_build/html" doc/arm/
    - mv "../doc/arm/_build/latex/Bv9ARM.pdf" doc/arm/
    - mv "../doc/arm/_build/epub/Bv9ARM.epub" doc/arm/
    - echo '<!DOCTYPE HTML><html lang="en"><meta http-equiv="refresh" content="0; url=doc/arm/html/notes.html"><title>Redirect</title></html>' > "RELEASE-NOTES-${BIND_VERSION}.html"
    - echo '<!DOCTYPE HTML><html lang="en"><meta http-equiv="refresh" content="0; url=doc/arm/html/changelog.html"><title>Redirect</title></html>' > "CHANGELOG-${BIND_VERSION}.html"
    - popd
  needs:
    - job: tarball-create
      artifacts: true
    - job: docs
      artifacts: true
    - job: docs:pdf
      artifacts: true
  tags:
    - smalljob
  rules:
    - *rule_tag
  artifacts:
    paths:
      - bind-${CI_COMMIT_TAG}-release
    expire_in: "1 month"

.manual_release_job: &manual_release_job
  stage: release
  when: manual

.signer-ssh-job: &signer_ssh_job
  <<: *manual_release_job
  allow_failure: false
  tags:
    - signer
  script:
    - ( rm -f "/tmp/${CI_JOB_NAME}.log" "/tmp/${CI_JOB_NAME}-done" && umask 111 && touch "/tmp/${CI_JOB_NAME}.log" "/tmp/${CI_JOB_NAME}-done" )
    - |
      cat > "/tmp/${CI_JOB_NAME}.sh" <<EOF
      #!/bin/sh
      set -e -x
      {
        ${SSH_SCRIPT_CLIENT}
        echo "${CI_COMMIT_TAG}" > "/tmp/${CI_JOB_NAME}-done"
      } 2>&1 | tee "/tmp/${CI_JOB_NAME}.log"
      EOF
    - chmod +x "/tmp/${CI_JOB_NAME}.sh"
    - /bin/sh -c "set -e -x; ${SSH_SCRIPT_RUNNER_PRE}"
    - echo -e "\e[31m*** Sleeping until /tmp/${CI_JOB_NAME}.sh is executed over SSH... ⌛\e[0m"
    - while [ "$(cat "/tmp/${CI_JOB_NAME}-done")" != "${CI_COMMIT_TAG}" ]; do sleep 10; done
    - /bin/sh -c "set -e -x; ${SSH_SCRIPT_RUNNER_POST}"
    - cp "/tmp/${CI_JOB_NAME}.log" "${CI_PROJECT_DIR}/${CI_JOB_NAME}-${CI_COMMIT_TAG}.log"
    - rm -f "/tmp/${CI_JOB_NAME}.log" "/tmp/${CI_JOB_NAME}-done" "/tmp/${CI_JOB_NAME}.sh"

# Job signing the source tarballs in the release directory

sign:
  <<: *signer_ssh_job
  before_script:
    - export SOURCE_TARBALL="bind-${CI_COMMIT_TAG#v}.tar.xz"
  variables:
    RELEASE_DIRECTORY: bind-${CI_COMMIT_TAG}-release
    SSH_SCRIPT_RUNNER_PRE: |-
      ( umask 111 && cat "${RELEASE_DIRECTORY}/$${SOURCE_TARBALL}" > "/tmp/${CI_COMMIT_TAG}.bin" )
    SSH_SCRIPT_CLIENT: |-
      gpg2 --local-user "$${SIGNING_KEY_FINGERPRINT}" --armor --digest-algo SHA512 --detach-sign --output "/tmp/${CI_COMMIT_TAG}.asc" "/tmp/${CI_COMMIT_TAG}.bin"
    SSH_SCRIPT_RUNNER_POST: |-
      cat "/tmp/${CI_COMMIT_TAG}.asc" > "${RELEASE_DIRECTORY}/$${SOURCE_TARBALL}.asc"
      tar --create --file="${RELEASE_DIRECTORY}".tar.gz --gzip "${RELEASE_DIRECTORY}"
      rm -f "/tmp/${CI_COMMIT_TAG}.bin" "/tmp/${CI_COMMIT_TAG}.asc"
  artifacts:
    paths:
      - bind-${CI_COMMIT_TAG}-release.tar.gz
      - sign-${CI_COMMIT_TAG}.log
    expire_in: never
  needs:
    - job: release
      artifacts: true
  rules:
    - *rule_tag

# Job staging the signed tarballs

staging:
  <<: *signer_ssh_job
  variables:
    RELEASE_TARBALL: bind-${CI_COMMIT_TAG}-release.tar.gz
    SSH_SCRIPT_RUNNER_PRE: |-
      mv "${RELEASE_TARBALL}" "/tmp/${RELEASE_TARBALL}"
    SSH_SCRIPT_CLIENT: |-
      scp "/tmp/${RELEASE_TARBALL}" "${STAGING_USER_UPLOAD}@${STAGING_HOST}:${STAGING_DIR}"
      ssh "${STAGING_USER_ACTIONS}@${STAGING_HOST}" "unpack ${CI_COMMIT_TAG}"
    SSH_SCRIPT_RUNNER_POST: |-
      rm -f "/tmp/${RELEASE_TARBALL}"
  artifacts:
    paths:
      - staging-${CI_COMMIT_TAG}.log
    expire_in: "1 month"
  needs:
    - job: sign
      artifacts: true
  rules:
    - *rule_tag

# Job copying a staged release to a secret location

publish-private:
  <<: *signer_ssh_job
  variables:
    SSH_SCRIPT_CLIENT: |-
      ssh "${STAGING_USER_ACTIONS}@${STAGING_HOST}" "publish-private ${CI_COMMIT_TAG}"
    SSH_SCRIPT_RUNNER_POST: |-
      awk '/^Public Use URL:/ {print $$NF}' "/tmp/${CI_JOB_NAME}.log" > "url-${CI_COMMIT_TAG}.txt"
  artifacts:
    paths:
      - publish-private-${CI_COMMIT_TAG}.log
      - url-${CI_COMMIT_TAG}.txt
    expire_in: "1 month"
  needs:
    - job: staging
      artifacts: false
  rules:
    - *rule_tag_security_or_subscription

# Job copying a staged release to a well-known location

publish:
  <<: *signer_ssh_job
  variables:
    SSH_SCRIPT_CLIENT: |-
      ssh "${STAGING_USER_ACTIONS}@${STAGING_HOST}" "publish ${CI_COMMIT_TAG}"
  artifacts:
    paths:
      - publish-${CI_COMMIT_TAG}.log
    expire_in: "1 month"
  needs:
    - job: staging
      artifacts: false
  rules:
    - *rule_tag_open_source

.manual_release_job_qa: &manual_release_job_qa
  <<: *manual_release_job
  <<: *base_image
  before_script:
    - *git_clone_bind9-qa
  needs:
    - job: staging
      artifacts: false
  tags:
    - smalljob

# Setting the FORCE_CVE_IDS environment variable to a comma-separated
# list of CVE IDs enables overriding the autodetected ones.
#
# Setting the FORCE_SECURITY_RELEASES environment variable to a
# comma-separated list of BIND 9 versions enables overriding the
# autodetected ones.
.printing_press_job: &printing_press_job
  <<: *manual_release_job_qa
  variables:
    GIT_DEPTH: 1
  script:
    - >
      "$CI_PROJECT_DIR"/bind9-qa/releng/printing_press_mr.py --document "${DOCUMENT}" --metadata "$CI_PROJECT_DIR"/bind9-qa/releng/metadata.json ${FORCE_CVE_IDS:+--force-cve-ids ${FORCE_CVE_IDS}} ${FORCE_SECURITY_RELEASES:+--force-security-releases ${FORCE_SECURITY_RELEASES}}
  artifacts:
    paths:
      - printing-press/
    when: on_failure

# Job creating the release announcement MR in Printing Press

prepare-release-announcement:
  <<: *printing_press_job
  variables:
    DOCUMENT: release-announcement
  rules:
    - *rule_tag_open_source

# Job preparing an EVN MR in Printing Press

prepare-evn:
  <<: *printing_press_job
  variables:
    DOCUMENT: evn
  rules:
    - *rule_tag_security

# Job preparing a security pre-announcement MR in Printing Press

prepare-preannouncement:
  <<: *printing_press_job
  variables:
    DOCUMENT: security-preannouncement
  rules:
    - *rule_tag_security

# Job preparing a packager notification MR in Printing Press

prepare-package-notification:
  <<: *printing_press_job
  variables:
    DOCUMENT: packager-notification
  rules:
    - *rule_tag_security

# Job preparing a post-disclosure notification MR in Printing Press

prepare-post-disclosure-notification:
  <<: *printing_press_job
  variables:
    DOCUMENT: post-disclosure-notification
  rules:
    - *rule_tag_security

# Job merging the tag back into its base branch

merge-tag:
  <<: *manual_release_job_qa
  variables:
    GIT_DEPTH: 100
  script:
    - >
      "$CI_PROJECT_DIR"/bind9-qa/releng/merge_tag.py --tag "$CI_COMMIT_TAG"
  rules:
    - *rule_tag_open_source
  artifacts:
    paths:
      - bind9/
    when: on_failure

.customer-git: &customer_git
  <<: *base_image
  stage: release
  when: manual
  variables:  # ensure clean git environment and sufficient history
    GIT_STRATEGY: clone
    GIT_DEPTH: 1000

# To trigger this job and push a branch to a customer, you must set the
# CUSTOMER job variable by clicking on the manual job (not the play button) and
# set it to the name of the target customer.
customer-git:branch:
  <<: *customer_git
  needs: []
  rules:
    - if: '$CI_PROJECT_NAMESPACE == "isc-private" && $CI_PIPELINE_SOURCE == "merge_request_event"'
      variables:
        BRANCH: '$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME'
    - if: '$CI_PROJECT_NAMESPACE == "isc-private" && $CI_PIPELINE_SOURCE =~ /^(api|pipeline|trigger|web)$/ && $REBASE_ONLY != "1"'
      variables:
        BRANCH: '$CI_COMMIT_BRANCH'
  before_script:
    - test -n "$CUSTOMER"
    - *git_clone_bind9-qa
  script:
    - git checkout -b "$BRANCH"  # ensure refs/heads/$BRANCH exists; GitLab clones with detached HEAD
    - >
      "$CI_PROJECT_DIR"/bind9-qa/releng/push_to_customer_repository.py --branch "$BRANCH" --customer "$CUSTOMER" --force

customer-git:tag:
  <<: *customer_git
  needs:
    - job: release
      artifacts: false
  rules:
    - *rule_tag
  before_script:
    - *git_clone_bind9-qa
    - git clone --depth 1 "https://token:${ISC_CUSTOMERS_WRITE_TOKEN}@gitlab.isc.org/isc-customers/isc-customer-settings.git"
  script:
    - >
      "$CI_PROJECT_DIR"/bind9-qa/releng/push_to_customer_repository.py --tag "$CI_COMMIT_TAG" --entitlements isc-customer-settings/entitlements.yaml --force

# Respdiff tests

respdiff:
  <<: *respdiff_job
  <<: *base_image
  variables:
    CC: gcc
    CFLAGS: "${CFLAGS_COMMON} -Og"
    MAX_DISAGREEMENTS_PERCENTAGE: "0.3"
  script:
    - bash respdiff.sh -s named -q "${PWD}/100k_mixed.txt" -c 3 -w "${PWD}/rspworkdir" "${CI_PROJECT_DIR}" "/usr/local/respdiff-reference-bind/sbin/named"
    - cd ../.. && make clean >/dev/null 2>&1

respdiff:asan:
  <<: *respdiff_job
  <<: *base_image
  variables:
    CC: gcc
    CFLAGS: "${CFLAGS_COMMON} -Og -fsanitize=address,undefined"
    LDFLAGS: "-fsanitize=address,undefined"
    EXTRA_CONFIGURE: "--disable-dnsrps --without-jemalloc"
    MAX_DISAGREEMENTS_PERCENTAGE: "0.3"
  script:
    - bash respdiff.sh -s named -q "${PWD}/100k_mixed.txt" -c 3 -w "${PWD}/rspworkdir" "${CI_PROJECT_DIR}" "/usr/local/respdiff-reference-bind/sbin/named"
    - cd ../.. && make clean >/dev/null 2>&1

respdiff:tsan:
  <<: *respdiff_job
  <<: *tsan_debian_trixie_amd64_image
  variables:
    CC: "${CLANG}"
    CFLAGS: "${CFLAGS_COMMON} -ggdb -O2 -fsanitize=thread"
    LDFLAGS: "-fsanitize=thread -Wl,--disable-new-dtags"
    OPENSSL_CFLAGS: -I/opt/tsan/include
    OPENSSL_LIBS: -L/opt/tsan/lib -lssl -lcrypto
    EXTRA_CONFIGURE: "--disable-dnsrps --enable-pthread-rwlock --without-jemalloc PKG_CONFIG_PATH=/opt/tsan/lib/pkgconfig"
    MAX_DISAGREEMENTS_PERCENTAGE: "0.3"
    TSAN_OPTIONS: "${TSAN_OPTIONS_DEBIAN}"
  script:
    - bash respdiff.sh -s named -q "${PWD}/100k_mixed.txt" -c 3 -w "${PWD}/rspworkdir" "${CI_PROJECT_DIR}" "/usr/local/respdiff-reference-bind/sbin/named"
    - cd ../.. && make clean >/dev/null 2>&1
  after_script:
    - *parse_tsan

respdiff-third-party:
  <<: *respdiff_job
  <<: *base_image
  variables:
    CC: gcc
    CFLAGS: "${CFLAGS_COMMON} -Og"
    MAX_DISAGREEMENTS_PERCENTAGE: "0.4"
  script:
    - bash respdiff.sh -s third_party -q "${PWD}/100k_mixed.txt" -c 1 -w "${PWD}/rspworkdir" "${CI_PROJECT_DIR}"
    - cd ../.. && make clean >/dev/null 2>&1

.respdiff-recent-named: &respdiff_recent_named
  <<: *respdiff_job
  <<: *base_image
  needs:
    - job: ci-variables
      artifacts: true
  script:
    - cd ${CI_PROJECT_DIR}
    - mkdir version-under-test
    - cp -ar bin version-under-test/
    - BASELINE=${CI_MERGE_REQUEST_TARGET_BRANCH_NAME:-$BIND_BASELINE_VERSION}
    - git fetch --unshallow origin ${BASELINE}
    - git checkout FETCH_HEAD
    - autoreconf -fi
    - *configure
    - make -j${BUILD_PARALLEL_JOBS:-1} V=1
    - cd "$CI_PROJECT_DIR"/bind9-qa/respdiff
    - bash respdiff.sh -s named -q "${PWD}/100k_mixed.txt" -c 3 -w "${PWD}/rspworkdir" "${CI_PROJECT_DIR}/version-under-test" "${CI_PROJECT_DIR}/bin/named/named"

respdiff:recent-named:
  <<: *respdiff_recent_named
  variables:
    CC: gcc
    CFLAGS: "${CFLAGS_COMMON} -Og -DISC_TRACK_PTHREADS_OBJECTS"
    MAX_DISAGREEMENTS_PERCENTAGE: "0.1"

# Performance tests

shotgun:udp:
  <<: *shotgun_job
  variables:
    SHOTGUN_SCENARIO: udp
    SHOTGUN_TRAFFIC_MULTIPLIER: 10

shotgun:tcp:
  <<: *shotgun_job
  variables:
    SHOTGUN_SCENARIO: tcp
    SHOTGUN_TRAFFIC_MULTIPLIER: 4

shotgun:dot:
  <<: *shotgun_job
  variables:
    SHOTGUN_SCENARIO: dot
    SHOTGUN_TRAFFIC_MULTIPLIER: 4
  rules:  # FIXME disabled shotgun jobs temporarily due to infra issue
    - *shotgun_rule_mr_manual
  #   - *shotgun_rule_tag
  #   - *shotgun_rule_other

shotgun:doh-get:
  <<: *shotgun_job
  variables:
    SHOTGUN_SCENARIO: doh-get
    SHOTGUN_TRAFFIC_MULTIPLIER: 2
    SHOTGUN_EVAL_THRESHOLD_LATENCY_PCTL_MAX: 0.4  # bump from the default due to increased tail-end jitter
  rules:  # FIXME disabled shotgun jobs temporarily due to infra issue
    - *shotgun_rule_mr_manual
  #   - *shotgun_rule_tag
  #   - *shotgun_rule_other

stress-test-child-pipeline:
  <<: *default_triggering_rules
  stage: performance
  rules:
    - if: '$CI_MERGE_REQUEST_DIFF_BASE_SHA != null'
      changes:
        - '**/*.c'
        - '**/*.h'
    - if: '$CI_MERGE_REQUEST_DIFF_BASE_SHA != null'
      when: manual  # don't run on each MR unless requested
      allow_failure: true
    - *rule_tag
    - if: '$CI_PIPELINE_SOURCE =~ /^(api|pipeline|schedule|trigger|web)$/ && $REBASE_ONLY != "1"'
  trigger:
    include:
      - artifact: stress-test-configs.yml
        job: generate-stress-test-configs
  needs:
    - job: generate-stress-test-configs
      artifacts: true

gcov:
  <<: *default_triggering_rules
  <<: *base_image
  stage: system
  needs:
    - job: system:gcc:trixie:amd64
      artifacts: true
  script:
    # Ensure gcov files for unit tests are found via tests/ rather than
    # lib/*/tests/ to prevent confusing gcovr.
    - find lib/ -name tests -type l -delete
    # The "a-conftest.gcno" file is result of the ./configure step and
    # should be removed as it does not belong to the BIND 9 code base.
    - rm a-conftest.gcno
    # Generate XML file in the Cobertura XML format suitable for use by GitLab
    # for the purpose of displaying code coverage information in the diff view
    # of a given merge request.
    - gcovr --exclude-directories bin/tests --exclude-directories doc --exclude-directories fuzz --exclude tests --cobertura-pretty -o coverage.xml
    - gcovr --exclude-directories bin/tests --exclude-directories doc --exclude-directories fuzz --exclude tests --html-details -o coverage.html
    - gcovr --exclude-directories bin/tests --exclude-directories doc --exclude-directories fuzz --exclude tests --txt -o coverage.txt
    - tail -n 3 coverage.txt
  coverage: /^TOTAL.*\s+(\d+\%)$/
  artifacts:
    paths:
      - coverage*.html
      - coverage.css
      - coverage.txt
      - coverage.xml
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage.xml

# Pairwise testing of ./configure options

pairwise:
  <<: *base_image
  stage: build
  needs:
    - job: autoreconf
      artifacts: true
  script:
      - util/pairwise-testing.sh
  artifacts:
    paths:
      - pairwise-commands.txt
      - pairwise-model.txt
      - pairwise-output.*.txt
    when: on_failure
  rules:
    - if: '$PAIRWISE_TESTING != null'

.post_merge_template: &post_merge
  <<: *base_image
  stage: postmerge
  needs: []
  tags:
    - smalljob
  # post-merge processes should run even if another MR was merged while the job was running (or queued)
  interruptible: false
  variables:
    # avoid leftover branches from previous jobs
    GIT_STRATEGY: clone
    # assumed max depth of a MR for backport or a rebased force-push
    GIT_DEPTH: 1000
  before_script:
    # force-pushes should not trigger process automation (happens only in -sub branches)
    - >
      echo "previous branch tip: $CI_COMMIT_BEFORE_SHA"
    - set +o pipefail; git log --format='%H' | grep --silent "$CI_COMMIT_BEFORE_SHA" && PREVIOUS_TIP_REACHABLE=1
    - test "$PREVIOUS_TIP_REACHABLE" != "1" && echo "force-push detected, stop" && exit 0
    # non-fast-forward merges are disabled so we have to have merge commit on top
    - MERGE_REQUEST_ID="$(git log -1 --format='%b' | sed --silent -e "s|^See merge request ${CI_PROJECT_PATH}\!||p")"
    - >
      : stop if this is not a merge request in the current project\'s namespace
    - test -n "$MERGE_REQUEST_ID" || exit 0
    - *git_clone_bind9-qa

backports:
  <<: *post_merge
  rules:
    # -sub branches are handled manually
    - if: '$CI_PIPELINE_SOURCE == "push" && ($CI_COMMIT_REF_NAME =~ /^bind-9.[0-9]+$/ || $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH)'
  script:
    # CI job token is not sufficient for push operations
    - git remote get-url origin | sed -e "s/gitlab-ci-token:$CI_JOB_TOKEN/oauth2:$BIND_TEAM_WRITE_TOKEN/" | xargs git remote set-url --push origin
    - >
      "$CI_PROJECT_DIR"/bind9-qa/releng/backport_mr.py $CI_PROJECT_ID "$MERGE_REQUEST_ID"

merged-metadata:
  <<: *post_merge
  rules:
    - if: '$CI_PIPELINE_SOURCE == "push" && ($CI_COMMIT_REF_NAME =~ /^bind-9.[0-9]+(-sub)?$/ || $CI_COMMIT_REF_NAME =~ /^v9.[0-9]+.[0-9]+-release$/ || $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH)'
  script:
    - >
      "$CI_PROJECT_DIR"/bind9-qa/releng/after_merge.py "$CI_PROJECT_ID" "$MERGE_REQUEST_ID"

auto-rebase-trigger:
  stage: postmerge
  rules:
    - if: '$CI_PROJECT_NAMESPACE == "isc-projects" && $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_REF_NAME =~ /^bind-9.[0-9]+$/'
  needs: []
  interruptible: true
  inherit:
      variables: false
  variables:
    REBASE_ONLY: 1
  trigger:
      project: isc-private/bind9
      branch: "${CI_COMMIT_BRANCH}-sub"
