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
10 changes: 9 additions & 1 deletion release_automation/scripts/changelog_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -554,11 +554,19 @@ def format_api_section(
else:
first_sentence = f"**{title} {version} is ...**"

comparison_baseline = api.get("comparison_baseline", "")
lines = [
f"## {title} {version}",
"",
first_sentence,
"",
]
if comparison_baseline:
lines.extend([
f"Changes documented below are compared to version {comparison_baseline}.",
"",
])
lines.extend([
"- API definition **with inline documentation**:",
f" - [View it on ReDoc](https://redocly.github.io/redoc/"
f"?url={raw_url}/{release_tag}/{yaml_path}&nocors)",
Expand All @@ -585,6 +593,6 @@ def format_api_section(
"### Removed",
"",
"* N/A",
]
])

return "\n".join(lines)
53 changes: 52 additions & 1 deletion release_automation/scripts/snapshot_creator.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@
from .wip_checker import check_wip_versions


COMPARE_BASE_UNSET = object()


@dataclass
class SnapshotConfig:
"""Configuration for snapshot creation."""
Expand Down Expand Up @@ -322,8 +325,14 @@ def create_snapshot(
result.warnings.append(f"README update failed: {e}")

# Step 12c: Generate CHANGELOG draft
api_comparison_baselines: Dict[str, str] = {}
try:
repo_name = self.gh.repo.split("/")[-1]
release_type = metadata.get("repository", {}).get("release_type", "")
compare_base = self._get_compare_base(release_type, config.release_tag)
api_comparison_baselines = self._get_api_comparison_baselines(
metadata, compare_base
)
self._generate_changelog(
temp_dir,
config,
Expand All @@ -333,6 +342,8 @@ def create_snapshot(
repo_name,
commonalities_version=commonalities_version,
icm_version=icm_version,
compare_base=compare_base,
api_comparison_baselines=api_comparison_baselines,
)
git_ops.commit_all(
f"Add CHANGELOG draft for {config.release_tag}"
Expand All @@ -350,6 +361,7 @@ def create_snapshot(
snapshot_id,
api_versions,
release_plan,
api_comparison_baselines,
)
result.release_pr_number = pr_info.number
result.release_pr_url = pr_info.url
Expand Down Expand Up @@ -643,6 +655,7 @@ def _create_release_pr(
snapshot_id: str,
api_versions: Dict[str, str],
release_plan: Dict[str, Any],
api_comparison_baselines: Optional[Dict[str, str]] = None,
) -> PullRequestInfo:
"""
Create the Release PR.
Expand All @@ -653,6 +666,7 @@ def _create_release_pr(
snapshot_id: Snapshot ID
api_versions: Calculated API versions
release_plan: Release plan dict
api_comparison_baselines: Previous API version by api_name

Returns:
PullRequestInfo with PR number and URL
Expand All @@ -667,6 +681,7 @@ def _create_release_pr(

# Build PR body from template with enriched context
apis = []
api_comparison_baselines = api_comparison_baselines or {}
for api_plan in release_plan.get("apis", []):
name = api_plan.get("api_name", "unknown")
status = api_plan.get("target_api_status", "")
Expand All @@ -684,6 +699,7 @@ def _create_release_pr(
"api_version": api_versions.get(name, "—"),
"target_api_status": status,
"status_label": status_label,
"comparison_baseline": api_comparison_baselines.get(name),
})

# Dependencies from release plan
Expand Down Expand Up @@ -817,6 +833,30 @@ def _get_candidate_changes(
"""
return self.gh.generate_release_notes(release_tag, previous_release)

def _get_api_comparison_baselines(
self, metadata: Dict[str, Any], compare_base: Optional[str]
) -> Dict[str, str]:
"""Return previous API versions for APIs present in comparison metadata."""
current_apis = metadata.get("apis", [])
if not compare_base or not current_apis:
return {}

comparison_metadata = self.gh.get_release_metadata(compare_base)
if not comparison_metadata:
return {}

previous_versions = {
api.get("api_name"): api.get("api_version")
for api in comparison_metadata.get("apis", [])
if api.get("api_name") and api.get("api_version")
}

return {
api["api_name"]: previous_versions[api["api_name"]]
for api in current_apis
if api.get("api_name") in previous_versions
}

def _update_readme(
self,
temp_dir: str,
Expand Down Expand Up @@ -953,6 +993,8 @@ def _generate_changelog(
repo_name: str,
commonalities_version: str = "",
icm_version: str = "",
compare_base: Any = COMPARE_BASE_UNSET,
api_comparison_baselines: Optional[Dict[str, str]] = None,
) -> str:
"""Generate CHANGELOG draft on release-review branch.

Expand All @@ -964,11 +1006,20 @@ def _generate_changelog(
Relative path to the written CHANGELOG file.
"""
release_type = metadata.get("repository", {}).get("release_type", "")
compare_base = self._get_compare_base(release_type, config.release_tag)
if compare_base is COMPARE_BASE_UNSET:
compare_base = self._get_compare_base(release_type, config.release_tag)
candidate_changes = self._get_candidate_changes(
config.release_tag, compare_base
)
changelog_metadata = deepcopy(metadata)
if api_comparison_baselines is None:
api_comparison_baselines = self._get_api_comparison_baselines(
metadata, compare_base
)
for api in changelog_metadata.get("apis", []):
baseline = api_comparison_baselines.get(api.get("api_name", ""))
if baseline:
api["comparison_baseline"] = baseline
if commonalities_version:
changelog_metadata.setdefault("dependencies", {})[
"commonalities_release"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ Edit and review this PR before merging it into the release snapshot. After Codeo

### Release contents

| API | Version | Status |
|-----|---------|--------|
| API | Version | Status | Comparison target |
|-----|---------|--------|---------------------|
{{#apis}}
| {{api_name}} | `{{api_version}}` | {{status_label}} |
| {{api_name}} | `{{api_version}}` | {{status_label}} | `{{#comparison_baseline}}{{comparison_baseline}}{{/comparison_baseline}}{{^comparison_baseline}}N/A{{/comparison_baseline}}` |
{{/apis}}

{{#commonalities_release}}**Dependencies:** Commonalities {{commonalities_release}}{{#identity_consent_management_release}}, ICM {{identity_consent_management_release}}{{/identity_consent_management_release}}{{/commonalities_release}}
Expand All @@ -23,7 +23,7 @@ _Tick each box once done. Release Management review starts when all three boxes
What to do:
- Copy all API-consumer-relevant changes from the provided list into the appropriate Breaking changes / Added / Changed / Fixed / Removed sections for each API. List breaking changes both in Breaking changes and in their normal change category.
- Do not copy administrative, tooling-only, or internal maintenance changes unless they affect API consumers.
- Check which kinds of changes must be listed for *this* release type — the rules are stated at the top of the CHANGELOG file and are easily overlooked.
- For each API, fill the CHANGELOG against the stated comparison target, following the release-type rules at the top of the CHANGELOG.
Comment thread
hdamker marked this conversation as resolved.

- [ ] **Document deferred validation warnings (and hints)**

Expand Down
44 changes: 44 additions & 0 deletions release_automation/tests/test_changelog_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,39 @@ def test_format_api_section_without_title(self):
result = ChangelogGenerator.format_api_section(api, "r1.1", "TestRepo")
assert "## fallback-api v1.0.0" in result

def test_format_api_section_includes_comparison_baseline_after_descriptor(self):
api = {
"api_name": "stable-api",
"api_version": "v1.0.0",
"api_file_name": "stable-api",
"comparison_baseline": "v0.9.0",
}
result = ChangelogGenerator.format_api_section(
api, "r2.1", "TestRepo", release_type="public-release"
)

descriptor_pos = result.index(
"**stable-api v1.0.0 is the first stable version of this API.**"
)
baseline_pos = result.index(
"Changes documented below are compared to version v0.9.0."
)
links_pos = result.index("- API definition **with inline documentation**:")

assert descriptor_pos < baseline_pos < links_pos

def test_format_api_section_omits_comparison_baseline_when_absent(self):
api = {
"api_name": "new-api",
"api_version": "v0.1.0-alpha.1",
"api_file_name": "new-api",
}
result = ChangelogGenerator.format_api_section(
api, "r1.1", "TestRepo", release_type="pre-release-alpha"
)

assert "Changes documented below are compared to version" not in result


class TestVersionSentence:
"""Tests for the generated first sentence per version + release_type.
Expand Down Expand Up @@ -374,6 +407,17 @@ def test_generate_draft_uses_api_name_not_title_in_summary(self, generator):
assert "* quality-on-demand v1.0.0-alpha.1" in result
assert "CAMARA Quality On Demand" not in result

def test_generate_draft_renders_api_comparison_baseline(self, generator, single_api_metadata):
single_api_metadata["apis"][0]["comparison_baseline"] = "v1.0.0"

result = generator.generate_draft(
release_tag="r4.1",
metadata=single_api_metadata,
repo_name="QualityOnDemand",
)

assert "Changes documented below are compared to version v1.0.0." in result


# --- File Writing ---

Expand Down
72 changes: 72 additions & 0 deletions release_automation/tests/test_snapshot_creator.py
Original file line number Diff line number Diff line change
Expand Up @@ -1274,6 +1274,42 @@ def test_get_candidate_changes_returns_none_on_api_error(
result = snapshot_creator._get_candidate_changes("r4.1", "r3.2")
assert result is None

def test_get_api_comparison_baselines_reads_matching_api_versions(
self, snapshot_creator, mock_github_client
):
"""Per-API baselines come from matching api_name in comparison metadata."""
current_metadata = {
"apis": [
{"api_name": "quality-on-demand", "api_version": "3.2.0-rc.2"},
{"api_name": "fresh-api", "api_version": "0.1.0-alpha.1"},
]
}
mock_github_client.get_release_metadata.return_value = {
"apis": [
{"api_name": "quality-on-demand", "api_version": "3.2.0-rc.1"},
{"api_name": "other-api", "api_version": "1.0.0"},
]
}

result = snapshot_creator._get_api_comparison_baselines(
current_metadata, "r4.1"
)

assert result == {"quality-on-demand": "3.2.0-rc.1"}
mock_github_client.get_release_metadata.assert_called_once_with("r4.1")

def test_get_api_comparison_baselines_skips_missing_comparison_release(
self, snapshot_creator, mock_github_client
):
current_metadata = {"apis": [{"api_name": "new-api", "api_version": "0.1.0"}]}

result = snapshot_creator._get_api_comparison_baselines(
current_metadata, None
)

assert result == {}
mock_github_client.get_release_metadata.assert_not_called()

def test_update_readme_returns_false_when_no_readme(
self, snapshot_creator, tmp_path, sample_release_plan
):
Expand Down Expand Up @@ -1374,6 +1410,42 @@ def test_generate_changelog_uses_commonalities_version_only(
== "0.5.0-rc.1"
)

@patch("release_automation.scripts.snapshot_creator.ChangelogGenerator")
def test_generate_changelog_adds_api_comparison_baselines(
self, mock_gen_cls, snapshot_creator, mock_github_client, tmp_path
):
"""CHANGELOG metadata is enriched with per-API baselines when present."""
mock_github_client.generate_release_notes.return_value = None

mock_instance = Mock()
mock_instance.generate_draft.return_value = "# r4.2\n\nContent\n"
mock_instance.write_changelog.return_value = "CHANGELOG/CHANGELOG-r4.md"
mock_gen_cls.return_value = mock_instance

metadata = {
"repository": {"release_type": "pre-release-rc"},
"apis": [
{"api_name": "quality-on-demand", "api_version": "3.2.0-rc.2"},
{"api_name": "fresh-api", "api_version": "0.1.0-alpha.1"},
],
"dependencies": {},
}

snapshot_creator._generate_changelog(
str(tmp_path),
SnapshotConfig(release_tag="r4.2"),
{},
{},
metadata,
"TestRepo-QoD",
compare_base="r4.1",
api_comparison_baselines={"quality-on-demand": "3.2.0-rc.1"},
)

draft_metadata = mock_instance.generate_draft.call_args.kwargs["metadata"]
assert draft_metadata["apis"][0]["comparison_baseline"] == "3.2.0-rc.1"
assert "comparison_baseline" not in draft_metadata["apis"][1]

@patch("release_automation.scripts.snapshot_creator.ChangelogGenerator")
def test_generate_changelog_rc_uses_public_release_as_base(
self, mock_gen_cls, snapshot_creator, mock_github_client, tmp_path
Expand Down
34 changes: 30 additions & 4 deletions release_automation/tests/test_template_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ def test_render_release_review_pr_template(self):
result = render_template("release_review_pr", context)

assert "## Release Review: r4.1 rc" in result
assert "| API | Version | Status |" in result
assert "| QualityOnDemand | `v1.0.0` | rc |" in result
assert "| DeviceLocation | `v2.0.0` | rc |" in result
assert "| API | Version | Status | Comparison target |" in result
assert "| QualityOnDemand | `v1.0.0` | rc | `N/A` |" in result
assert "| DeviceLocation | `v2.0.0` | rc | `N/A` |" in result
assert "### Codeowner Actions" in result
assert "### Release Management Actions" in result
# Three status-independent codeowner actions
Expand Down Expand Up @@ -161,7 +161,33 @@ def test_loader_render_release_review_pr(self):
result = loader.render("release_review_pr", context)

assert "## Release Review: r4.2 rc" in result
assert "| TestAPI | `v1.0.0` | rc |" in result
assert "| TestAPI | `v1.0.0` | rc | `N/A` |" in result

def test_render_release_review_pr_includes_comparison_baseline_column(self):
context = {
"release_tag": "r4.2",
"snapshot_id": "r4.2-111222",
"short_type": "rc",
"apis": [
{
"api_name": "TestAPI",
"api_version": "v1.2.0-rc.2",
"status_label": "rc",
"comparison_baseline": "v1.2.0-rc.1",
},
{
"api_name": "NewAPI",
"api_version": "v0.1.0-alpha.1",
"status_label": "alpha",
},
],
}

result = render_template("release_review_pr", context)

assert "| API | Version | Status | Comparison target |" in result
assert "| TestAPI | `v1.2.0-rc.2` | rc | `v1.2.0-rc.1` |" in result
assert "| NewAPI | `v0.1.0-alpha.1` | alpha | `N/A` |" in result

def test_loader_render_sync_pr(self):
"""Test TemplateLoader.render for sync PR."""
Expand Down