diff --git a/.github/workflows/release-automation-reusable.yml b/.github/workflows/release-automation-reusable.yml index d293783..acf8b02 100644 --- a/.github/workflows/release-automation-reusable.yml +++ b/.github/workflows/release-automation-reusable.yml @@ -1586,6 +1586,7 @@ jobs: id: changelog uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 with: + github-token: ${{ steps.app-token.outputs.token || github.token }} script: | const snapshotBranch = '${{ needs.derive-state.outputs.snapshot_branch }}'; const releaseTag = '${{ needs.derive-state.outputs.release_tag }}'; @@ -1602,6 +1603,7 @@ jobs: let changelogContent = ''; let changelogPath = ''; + let changelogSha = ''; for (const path of candidates) { try { const response = await github.rest.repos.getContent({ @@ -1612,6 +1614,7 @@ jobs: }); changelogContent = Buffer.from(response.data.content, 'base64').toString('utf-8'); changelogPath = path; + changelogSha = response.data.sha; console.log(`Fetched ${path} (${changelogContent.length} chars)`); break; } catch (error) { @@ -1622,9 +1625,54 @@ jobs: if (!changelogContent) { console.log('::warning::No CHANGELOG file found on snapshot branch'); core.setOutput('release_notes', ''); + core.setOutput('candidate_block_stripped', ''); return; } + // --- Strip AUTOGENERATED:CANDIDATE_CHANGES block if present --- + const START_MARKER = ''; + const END_MARKER = ''; + + const startPos = changelogContent.indexOf(START_MARKER); + const endPos = changelogContent.indexOf(END_MARKER); + let candidateBlockStripped = false; + + if (startPos !== -1 && endPos !== -1 && endPos > startPos) { + // Both markers present — strip the entire block (inclusive) + changelogContent = ( + changelogContent.substring(0, startPos) + + changelogContent.substring(endPos + END_MARKER.length) + ).replace(/\n{3,}/g, '\n\n'); + candidateBlockStripped = true; + console.log('Stripped full candidate changes block'); + } else if (startPos !== -1 || endPos !== -1) { + // Orphan marker — codeowner partially deleted the block. + // Strip just the orphan marker line (HTML comment, always safe to remove). + const orphanMarker = startPos !== -1 ? START_MARKER : END_MARKER; + changelogContent = changelogContent + .split('\n') + .filter(line => line.trim() !== orphanMarker) + .join('\n'); + candidateBlockStripped = true; + console.log(`Stripped orphan marker: ${orphanMarker}`); + } + // If neither marker found: codeowner already removed the block — no action needed + + if (candidateBlockStripped) { + await github.rest.repos.createOrUpdateFileContents({ + owner: context.repo.owner, + repo: context.repo.repo, + path: changelogPath, + message: 'chore: auto-remove candidate changes block from CHANGELOG', + content: Buffer.from(changelogContent).toString('base64'), + sha: changelogSha, + branch: snapshotBranch + }); + console.log(`Committed cleaned CHANGELOG to ${snapshotBranch}`); + } + + core.setOutput('candidate_block_stripped', candidateBlockStripped ? 'true' : ''); + // Extract section from "## Release Notes" to next "# rX.Y" heading or EOF const lines = changelogContent.split('\n'); let startIdx = -1; @@ -1749,7 +1797,8 @@ jobs: { "draft_release_url": "${{ steps.create-draft.outputs.draft_release_url }}", "release_pr_number": "${{ github.event.pull_request.number }}", - "release_pr_url": "${{ github.event.pull_request.html_url }}" + "release_pr_url": "${{ github.event.pull_request.html_url }}", + "candidate_block_stripped": "${{ steps.changelog.outputs.candidate_block_stripped }}" } # ───────────────────────────────────────────────────────────────────────────── diff --git a/release_automation/scripts/bot_context.py b/release_automation/scripts/bot_context.py index 5727e09..e081b6b 100644 --- a/release_automation/scripts/bot_context.py +++ b/release_automation/scripts/bot_context.py @@ -79,6 +79,7 @@ class BotContext: # Display fields workflow_run_url: str = "" draft_release_url: str = "" + candidate_block_stripped: str = "" reason: str = "" # Publication fields @@ -175,6 +176,7 @@ def to_dict(self) -> Dict[str, Any]: # Display fields "workflow_run_url": self.workflow_run_url, "draft_release_url": self.draft_release_url, + "candidate_block_stripped": self.candidate_block_stripped, "reason": self.reason, # Publication fields "release_url": self.release_url, diff --git a/release_automation/templates/bot_messages/draft_created.md b/release_automation/templates/bot_messages/draft_created.md index 1b015d2..f7fe0a2 100644 --- a/release_automation/templates/bot_messages/draft_created.md +++ b/release_automation/templates/bot_messages/draft_created.md @@ -1,6 +1,9 @@ **📦 Draft release created — State: `draft-ready`** Triggered by merge of [Release PR #{{release_pr_number}}]({{release_pr_url}}). **Draft release:** [`{{release_tag}}`]({{draft_release_url}}) +{{#candidate_block_stripped}} +Release notes cleaned (candidate block removed). +{{/candidate_block_stripped}}
Configuration: Release {{release_tag}}{{#short_type}} ({{short_type}}{{#has_meta_release}}, {{meta_release}}{{/has_meta_release}}){{/short_type}} diff --git a/release_automation/templates/changelog/release_section.mustache b/release_automation/templates/changelog/release_section.mustache index 908a9bf..7e2d10a 100644 --- a/release_automation/templates/changelog/release_section.mustache +++ b/release_automation/templates/changelog/release_section.mustache @@ -13,8 +13,10 @@ The API definition(s) are based on {{#candidate_changes}} -> **Working area — candidate changes (must be removed before merge)** -> Use these entries to fill the Added/Changed/Fixed/Removed sections below. **Delete this block before merge.** +> **Working area — candidate changes (auto-removed on merge)** +> Copy relevant entries into the Added/Changed/Fixed/Removed sections below. +> You may edit this list while triaging; it will be removed on merge. +> This working-area section is removed automatically when the PR is merged.
Candidate changes (auto-generated from merged PRs) diff --git a/release_automation/templates/pr_bodies/release_review_pr.mustache b/release_automation/templates/pr_bodies/release_review_pr.mustache index 7239eb0..06a8226 100644 --- a/release_automation/templates/pr_bodies/release_review_pr.mustache +++ b/release_automation/templates/pr_bodies/release_review_pr.mustache @@ -15,8 +15,7 @@ Codeowners update and review this PR. After codeowner and release management app ### Codeowner Review **Update this PR:** -- [ ] Move CHANGELOG candidate changes into the correct categories (Added/Changed/Fixed/Removed) and complete them -- [ ] Remove the autogenerated candidate changes block from the CHANGELOG +- [ ] All relevant changes copied into Added / Changed / Fixed / Removed (per API as needed) **Confirm readiness:** - [ ] API version numbers match the intent declared in `release-plan.yaml` diff --git a/release_automation/tests/test_bot_context.py b/release_automation/tests/test_bot_context.py index b6a42ff..f8446c8 100644 --- a/release_automation/tests/test_bot_context.py +++ b/release_automation/tests/test_bot_context.py @@ -172,7 +172,7 @@ def test_to_dict_returns_all_keys(self): "trigger_workflow_dispatch", "trigger_issue_close", "trigger_release_plan_change", "has_meta_release", "has_reason", - "workflow_run_url", "draft_release_url", "reason", + "workflow_run_url", "draft_release_url", "candidate_block_stripped", "reason", # Publication fields "release_url", "reference_tag", "reference_tag_url", "sync_pr_number", "sync_pr_url", @@ -251,7 +251,7 @@ def test_returns_complete_dict(self): "trigger_workflow_dispatch", "trigger_issue_close", "trigger_release_plan_change", "has_meta_release", "has_reason", - "workflow_run_url", "draft_release_url", "reason", + "workflow_run_url", "draft_release_url", "candidate_block_stripped", "reason", # Publication fields "release_url", "reference_tag", "reference_tag_url", "sync_pr_number", "sync_pr_url", diff --git a/release_automation/tests/test_changelog_generator.py b/release_automation/tests/test_changelog_generator.py index 53acc6e..b59d6e0 100644 --- a/release_automation/tests/test_changelog_generator.py +++ b/release_automation/tests/test_changelog_generator.py @@ -221,7 +221,7 @@ def test_generate_draft_with_candidate_changes( assert CANDIDATE_CHANGES_START_MARKER in result assert CANDIDATE_CHANGES_END_MARKER in result # Instruction text visible (not collapsed) - assert "must be removed before merge" in result + assert "auto-removed on merge" in result # Full Changelog link at end, outside markers assert "compare/r3.2...r4.1" in result end_marker_pos = result.index(CANDIDATE_CHANGES_END_MARKER) diff --git a/release_automation/tests/test_template_loader.py b/release_automation/tests/test_template_loader.py index 3f93361..ceca443 100644 --- a/release_automation/tests/test_template_loader.py +++ b/release_automation/tests/test_template_loader.py @@ -36,7 +36,7 @@ def test_render_release_review_pr_rc_template(self): assert "**Verify snapshot content (during automation introduction phase only):**" in result assert "**Update this PR:**" in result assert "**Confirm readiness:**" in result - assert "Move CHANGELOG candidate changes" in result + assert "All relevant changes copied into Added" in result assert "declared Commonalities version" in result assert "Commonalities r3.4" in result assert "mandatory release assets for the APIs are present per the API status and confirmed" in result @@ -64,7 +64,7 @@ def test_render_release_review_pr_alpha_template(self): assert "| NumberVerification | `v0.3.0-alpha.1` | alpha |" in result assert "API definitions are consistent with the declared API version" in result assert "API documentation (`info.description`) is up to date" in result - assert "Move CHANGELOG candidate changes" in result + assert "All relevant changes copied into Added" in result # Alpha should NOT have rc/public-specific items assert "Enhanced test cases" not in result