Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/workflows/build-ddev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -771,9 +771,46 @@ jobs:
with:
skip-existing: true

- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "${{ env.PYTHON_VERSION }}"

- name: Install uv
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
with:
enable-cache: false

- name: Install ddev from local folder
working-directory: .
run: uv pip install --system -e ./datadog_checks_dev[cli] -e ./ddev

- name: Configure ddev
working-directory: .
run: |-
ddev config override
ddev config set upgrade_check false

# Extraction is best-effort: PyPI publish has already happened by this point, so any failure
# here must not abort the workflow before the GitHub release is created. On failure we log a
# warning, leave the body file empty, and let the release proceed with an empty body.
- name: Extract release notes from CHANGELOG
id: release-notes
working-directory: .
run: |
version="${GITHUB_REF_NAME#ddev-v}"
notes_file="${RUNNER_TEMP}/release-notes.md"
: > "$notes_file"
if ! ddev release changelog show ddev "$version" --file "$notes_file"; then
echo "::warning::Failed to extract changelog section for ddev $version; release body will be empty."
: > "$notes_file"
fi
echo "path=$notes_file" >> "$GITHUB_OUTPUT"

- name: Add assets to current release
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
with:
body_path: ${{ steps.release-notes.outputs.path }}
files: |-
archives/*
installers/*
1 change: 1 addition & 0 deletions ddev/changelog.d/23586.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add `ddev release changelog show` command to print the section of a target's `CHANGELOG.md` for a given version.
2 changes: 2 additions & 0 deletions ddev/src/ddev/cli/release/changelog/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from ddev.cli.release.changelog.draft import draft
from ddev.cli.release.changelog.fix import fix
from ddev.cli.release.changelog.new import new
from ddev.cli.release.changelog.show import show


@click.group(short_help='Manage changelogs')
Expand All @@ -18,3 +19,4 @@ def changelog():
changelog.add_command(draft)
changelog.add_command(fix)
changelog.add_command(new)
changelog.add_command(show)
80 changes: 80 additions & 0 deletions ddev/src/ddev/cli/release/changelog/show.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# (C) Datadog, Inc. 2026-present
# All rights reserved
# Licensed under a 3-clause BSD style license (see LICENSE)
from __future__ import annotations

from typing import TYPE_CHECKING

import click

from ddev.utils.fs import Path

if TYPE_CHECKING:
from collections.abc import Iterator

from ddev.cli.application import Application


@click.command(short_help="Show the changelog section for a target's version")
@click.argument('target')
@click.argument('version')
@click.option(
'--file',
'-f',
'output_file',
type=click.Path(dir_okay=False, writable=True, path_type=Path),
help='Write the extracted section to this file (overwrites if it exists) instead of stdout',
)
@click.pass_obj
def show(app: Application, target: str, version: str, output_file: Path | None):
"""
Print the section of TARGET's CHANGELOG.md that corresponds to VERSION.

The output is the markdown content between the ``## VERSION`` heading and the
next ``## `` heading, with surrounding blank lines stripped. Useful for
populating GitHub release notes from the just-built changelog.
"""
try:
integration = app.repo.integrations.get(target)
except OSError:
app.abort(f'Unknown target: {target}')

changelog_path = integration.path / 'CHANGELOG.md'
if not changelog_path.is_file():
app.abort(f'Changelog not found: {changelog_path}')

section = _extract_version_section(changelog_path, version)
if section is None:
app.abort(f'No changelog section found for {target} version {version}')

if output_file:
output_file.parent.mkdir(parents=True, exist_ok=True)
output_file.write_text(section, encoding='utf-8')
app.display_success(f'Wrote changelog section for {target} {version} to {output_file}')
else:
app.display_markdown(section)


def _iter_version_section(path: Path, version: str) -> Iterator[str]:
"""Yield the lines of the requested version's section, lazily.

Stops as soon as the next ``## `` heading after the section is encountered,
so we never read past the requested release in a long changelog.
"""
with path.open(mode='r', encoding='utf-8') as f:
in_section = False
for line in f:
if line.startswith('## '):
tokens = line[3:].split()
if in_section:
return
if tokens and tokens[0] == version:
in_section = True
continue
if in_section:
yield line


def _extract_version_section(path: Path, version: str) -> str | None:
section = ''.join(_iter_version_section(path, version)).strip('\n')
return section or None
168 changes: 168 additions & 0 deletions ddev/tests/cli/release/changelog/test_show.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# (C) Datadog, Inc. 2026-present
# All rights reserved
# Licensed under a 3-clause BSD style license (see LICENSE)
from __future__ import annotations

import pytest

from ddev.cli.release.changelog.show import _extract_version_section
from ddev.utils.fs import Path
from tests.helpers.git import ClonedRepo
from tests.helpers.runner import CliRunner

CHANGELOG_BODY = """\
# CHANGELOG - ddev

<!-- towncrier release notes start -->

## 16.1.1 / 2026-04-29

***Fixed***:

* Bumped datadog_checks_dev to version 38.0.0. Fixes dependency issues. ([#23516](https://github.com/DataDog/integrations-core/pull/23516))

## 16.1.0 / 2026-04-29

***Added***:

* Add async GitHub API client. ([#22734](https://github.com/DataDog/integrations-core/pull/22734))
* Use uv as the installer for hatch test environments. ([#23497](https://github.com/DataDog/integrations-core/pull/23497))

## 1.0.10 / 2024-01-01

***Fixed***:

* Older release that should not match against 1.0.1. ([#1](https://github.com/DataDog/integrations-core/pull/1))

## 1.0.1 / 2024-01-02

***Fixed***:

* Patch release. ([#2](https://github.com/DataDog/integrations-core/pull/2))
"""


@pytest.fixture
def ddev_changelog(repo_with_towncrier: ClonedRepo) -> Path:
changelog = repo_with_towncrier.path / 'ddev' / 'CHANGELOG.md'
changelog.write_text(CHANGELOG_BODY)
return changelog


@pytest.mark.parametrize(
'version, expected_substring, forbidden_substring',
[
pytest.param(
'16.1.1',
'* Bumped datadog_checks_dev to version 38.0.0',
'## 16.1.0',
id='middle_section',
),
pytest.param(
'16.1.0',
'* Add async GitHub API client.',
'## 16.1.1',
id='multi_bullet_section',
),
pytest.param(
'1.0.1',
'* Patch release.',
'* Older release',
id='strict_match_does_not_substring_into_1_0_10',
),
],
)
def test_extract_version_section_returns_expected_block(
ddev_changelog: Path, version: str, expected_substring: str, forbidden_substring: str
):
section = _extract_version_section(ddev_changelog, version)

assert section is not None
assert expected_substring in section
assert forbidden_substring not in section
assert not section.startswith('\n')
assert not section.endswith('\n')


def test_extract_version_section_returns_none_for_missing_version(ddev_changelog: Path):
assert _extract_version_section(ddev_changelog, '99.99.99') is None


def test_show_prints_section_to_stdout(ddev: CliRunner, ddev_changelog: Path, helpers):
result = ddev('release', 'changelog', 'show', 'ddev', '16.1.1')

assert result.exit_code == 0, result.output
output = helpers.remove_trailing_spaces(result.output)
assert 'Fixed:' in output
assert 'Bumped datadog_checks_dev to version 38.0.0' in output
assert '16.1.0' not in output


def test_show_writes_to_file(ddev: CliRunner, ddev_changelog: Path, tmp_path: Path):
output_file = tmp_path / 'release-notes.md'

result = ddev('release', 'changelog', 'show', 'ddev', '16.1.0', '--file', str(output_file))

assert result.exit_code == 0, result.output
assert f'Wrote changelog section for ddev 16.1.0 to {output_file}' in result.output
contents = output_file.read_text()
assert '* Add async GitHub API client.' in contents
assert '## 16.1.1' not in contents


def test_show_creates_missing_parent_directories(ddev: CliRunner, ddev_changelog: Path, tmp_path: Path):
output_file = tmp_path / 'nested' / 'sub' / 'release-notes.md'

result = ddev('release', 'changelog', 'show', 'ddev', '16.1.1', '--file', str(output_file))

assert result.exit_code == 0, result.output
assert output_file.is_file()
assert '* Bumped datadog_checks_dev to version 38.0.0' in output_file.read_text()


def test_show_strict_version_matching_against_substring(ddev: CliRunner, ddev_changelog: Path, helpers):
result = ddev('release', 'changelog', 'show', 'ddev', '1.0.1')

assert result.exit_code == 0, result.output
output = helpers.remove_trailing_spaces(result.output)
assert 'Patch release.' in output
assert 'Older release' not in output


def test_show_aborts_when_version_missing(ddev: CliRunner, ddev_changelog: Path):
result = ddev('release', 'changelog', 'show', 'ddev', '99.99.99')

assert result.exit_code != 0
assert 'No changelog section found for ddev version 99.99.99' in result.output


def test_show_aborts_when_changelog_missing(ddev: CliRunner, repo_with_towncrier: ClonedRepo):
changelog = repo_with_towncrier.path / 'ddev' / 'CHANGELOG.md'
if changelog.is_file():
changelog.unlink()

result = ddev('release', 'changelog', 'show', 'ddev', '1.0.0')

assert result.exit_code != 0
assert 'Changelog not found' in result.output


def test_show_aborts_for_unknown_target(ddev: CliRunner, repo_with_towncrier: ClonedRepo):
result = ddev('release', 'changelog', 'show', 'definitely_not_an_integration', '1.0.0')

assert result.exit_code != 0
assert 'Unknown target: definitely_not_an_integration' in result.output


@pytest.mark.parametrize(
'args, expected_message',
[
pytest.param([], "Missing argument 'TARGET'", id='no_args'),
pytest.param(['ddev'], "Missing argument 'VERSION'", id='target_only'),
],
)
def test_show_argument_errors(ddev: CliRunner, repo_with_towncrier: ClonedRepo, args: list[str], expected_message: str):
result = ddev('release', 'changelog', 'show', *args)

assert result.exit_code != 0
assert expected_message in result.output
1 change: 1 addition & 0 deletions kafka_consumer/changelog.d/23428.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add an `out_of_sync_broker_id` tag to the `kafka.partition.*` metrics (when `enable_cluster_monitoring` is true) identifying each assigned replica that is not in the partition's ISR. Use it to attribute under-replicated partitions to specific broker IDs.
Original file line number Diff line number Diff line change
Expand Up @@ -571,12 +571,17 @@ def _collect_topic_metadata(self, metadata, highwater_offsets):
for replica in replicas:
partition_broker_tags.append(f'replica_broker_id:{replica}')

isr_set = set(isrs)
out_of_sync_broker_ids = [broker_id for broker_id in replicas if broker_id not in isr_set]
for broker_id in out_of_sync_broker_ids:
partition_broker_tags.append(f'out_of_sync_broker_id:{broker_id}')

self.check.gauge('partition.replicas', len(replicas), tags=partition_broker_tags)
self.check.gauge('partition.isr', len(isrs), tags=partition_broker_tags)

self.check.gauge('partition.size', partition_size, tags=partition_broker_tags)

is_under_replicated = len(isrs) < len(replicas)
is_under_replicated = bool(out_of_sync_broker_ids)
self.check.gauge(
'partition.under_replicated',
1 if is_under_replicated else 0,
Expand Down
56 changes: 56 additions & 0 deletions kafka_consumer/tests/test_cluster_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -1351,3 +1351,59 @@ def test_schema_registry_url_encodes_subject_names(check):
collector.http.get.assert_called_with(
'http://localhost:8081/subjects/google%2Fprotobuf%2Ftimestamp.proto/versions/latest'
)


@pytest.mark.parametrize(
"replicas, isrs, expected_oos, expected_under",
[
pytest.param([1, 2], [1, 2], [], 0, id="fully_in_sync"),
pytest.param([1, 2], [1], [2], 1, id="single_oos"),
pytest.param([1, 2, 3], [1], [2, 3], 1, id="multiple_oos"),
pytest.param([1, 2], [], [1, 2], 1, id="empty_isr"),
pytest.param([1], [1], [], 0, id="single_replica"),
],
)
def test_partition_out_of_sync_broker_id_tag(
check, dd_run_check, aggregator, replicas, isrs, expected_oos, expected_under
):
"""Under-replicated partitions expose an ``out_of_sync_broker_id`` tag per replica missing from the ISR."""
instance = {
'kafka_connect_str': 'localhost:9092',
'enable_cluster_monitoring': True,
'tags': ['test_tag:test_value'],
}

kafka_consumer_check = check(instance)
mock_kafka_client = seed_mock_kafka_client()

topic_metadata = mock_kafka_client.kafka_client.list_topics.return_value.topics['test-topic']
topic_metadata.partitions[0].replicas = replicas
topic_metadata.partitions[0].isrs = isrs

kafka_consumer_check.client = mock_kafka_client
kafka_consumer_check.metadata_collector.client = mock_kafka_client
mock_schema_registry_methods(kafka_consumer_check.metadata_collector)

kafka_consumer_check.read_persistent_cache = mock.Mock(return_value=None)
kafka_consumer_check.write_persistent_cache = mock.Mock()
kafka_consumer_check.event_platform_event = mock.Mock()

dd_run_check(kafka_consumer_check)

expected_tags = [
'test_tag:test_value',
'kafka_cluster_id:test-cluster-id',
'topic:test-topic',
'partition:0',
'leader_broker_id:1',
*(f'replica_broker_id:{r}' for r in replicas),
*(f'out_of_sync_broker_id:{b}' for b in expected_oos),
]
aggregator.assert_metric('kafka.partition.under_replicated', value=expected_under, tags=expected_tags)
for metric in (
'kafka.partition.replicas',
'kafka.partition.isr',
'kafka.partition.size',
'kafka.partition.offline',
):
aggregator.assert_metric(metric, tags=expected_tags)
1 change: 1 addition & 0 deletions nutanix/changelog.d/23583.fixed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix `nutanix.vm.disk_capacity_bytes` to report allocated disk capacity per VM.
Loading
Loading