diff --git a/CHANGELOG.md b/CHANGELOG.md index ec24dd8..f2003ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [[[NEXT_VERSION]]] + +### Fix + +- Allow manually selected releases without pending changelog changes by creating matching maintenance notes in both changelogs. Preserve published history and reject inconsistent pending sections before changing metadata. + ## 1.0.9 ### Fix diff --git a/changelog.xml b/changelog.xml index 99ac946..b463e12 100644 --- a/changelog.xml +++ b/changelog.xml @@ -1,5 +1,13 @@ + + com_joomengine_mcp + component + [[[NEXT_VERSION]]] + + Allow manually selected releases without pending changelog changes by creating matching maintenance notes in both changelogs. Preserve published history and reject inconsistent pending sections before changing metadata. + + com_joomengine_mcp component diff --git a/docs/IMPLEMENTATION.md b/docs/IMPLEMENTATION.md index c5654f8..31d1ba9 100644 --- a/docs/IMPLEMENTATION.md +++ b/docs/IMPLEMENTATION.md @@ -1,5 +1,9 @@ # Implementation status — 2 October 2026 +## Manual releases without pending changes — 12 October 2026 + +The release metadata helper now permits an unused selected version when neither changelog has pending changes. It inserts matching maintenance notes while retaining historical entries verbatim. Existing pending entries still freeze normally; duplicate, malformed and one-sided markers fail before metadata writes. Regression coverage extends `tests/release.php`. Validation and publication results are tracked in the release-fix PR; metadata checks alone do not establish package publication. + ## Language associations — issue #42 Site-menu API partial updates preserve `associations`, including the follow-up diff --git a/docs/RELEASE.md b/docs/RELEASE.md index 5ae4d77..490b9b6 100644 --- a/docs/RELEASE.md +++ b/docs/RELEASE.md @@ -56,6 +56,6 @@ If a component release stops after tagging but before packaging, fix the workflo ## Changelogs and dependencies -Keep `CHANGELOG.md` and `changelog.xml` consistent, with new changes under one literal `[[[NEXT_VERSION]]]` section in each file. Release replaces the marker with its version. Create another pending section when subsequent changes begin; preserve released history. Use Joomla categories `security`, `fix`, `language`, `addition`, `change`, `remove` and `note`, with `item` children and matching Markdown headings. +Keep `CHANGELOG.md` and `changelog.xml` consistent, with new changes under one literal `[[[NEXT_VERSION]]]` section in each file. Release replaces the marker with its version. A new release can also be triggered when neither file has pending changes: the helper adds a matching maintenance Note for the selected version in both files and preserves released history. Duplicate, malformed or one-sided pending sections remain errors. Create another pending section when subsequent changes begin. Use Joomla categories `security`, `fix`, `language`, `addition`, `change`, `remove` and `note`, with `item` children and matching Markdown headings. Maintainers resolve changed dependencies with Composer and commit the lock and complete production runtime under `admin/vendor`. CI checks source installation, relocated dependencies, native metadata and separately installed plugins. No downstream Composer run is required. Source and metadata checks do not claim an actual release or prove configured publication credentials. diff --git a/tests/release.php b/tests/release.php index e3b8a29..aabbffc 100644 --- a/tests/release.php +++ b/tests/release.php @@ -92,6 +92,49 @@ $check((string) $feed->update->downloads->downloadurl === 'https://github.com/joomengine/mcp_component/archive/refs/tags/v1.2.0.zip', 'The next download stays in the component repository.'); $check((string) mcpReleaseXml($root . '/changelog.xml')->changelog[1]->version === '1.1.0', 'Next release retains the previous changelog.'); + $xmlHistory = file_get_contents($root . '/changelog.xml'); + $markdownHistory = file_get_contents($root . '/CHANGELOG.md'); + mcpRelease(['prepare', '1.3.0'], $root); + $check((string) mcpReleaseXml($root . '/joomengine_mcp.xml')->version === '1.3.0', 'A release without pending changes updates the manifest.'); + $check((string) mcpReleaseXml($root . '/changelog.xml')->changelog->version === '1.3.0' + && str_contains(file_get_contents($root . '/CHANGELOG.md'), '## 1.3.0'), 'Maintenance releases receive matching versioned notes.'); + $check(str_contains(file_get_contents($root . '/changelog.xml'), substr($xmlHistory, strpos($xmlHistory, ''))) + && str_contains(file_get_contents($root . '/CHANGELOG.md'), substr($markdownHistory, strpos($markdownHistory, '## '))), 'Maintenance releases preserve historical entries verbatim.'); + + foreach (['duplicate', 'markdown-only', 'xml-only', 'malformed-heading'] as $case) + { + $xml = $xmlHistory; + $markdown = $markdownHistory; + + if ($case !== 'markdown-only') + { + $xml = str_replace('', '' . $pending . ($case === 'duplicate' ? $pending : ''), $xml); + } + + if ($case !== 'xml-only') + { + $markdown = ($case === 'malformed-heading' ? "[[[NEXT_VERSION]]]\n" : "## [[[NEXT_VERSION]]]\n") . $markdown; + } + + file_put_contents($root . '/changelog.xml', $xml); + file_put_contents($root . '/CHANGELOG.md', $markdown); + $before = file_get_contents($root . '/joomengine_mcp.xml'); + $rejected = false; + + try + { + mcpRelease(['prepare', '1.4.0'], $root); + } + catch (RuntimeException) + { + $rejected = true; + } + + $check($rejected && file_get_contents($root . '/changelog.xml') === $xml + && file_get_contents($root . '/CHANGELOG.md') === $markdown + && file_get_contents($root . '/joomengine_mcp.xml') === $before, 'Inconsistent pending metadata must fail without writes: ' . $case); + } + echo json_encode(['checks' => $checks, 'metadataTransitions' => 'passed'], JSON_THROW_ON_ERROR) . "\n"; } finally diff --git a/tools/release.php b/tools/release.php index f40b24f..8b01165 100644 --- a/tools/release.php +++ b/tools/release.php @@ -42,15 +42,40 @@ function mcpRelease(array $arguments, string $root): void if (version_compare($version, (string) $manifest->version, '<') || count($changelog->xpath('changelog[version="' . $version . '"]')) !== 0 - || count($pending) !== 1 || substr_count($markdown, '[[[NEXT_VERSION]]]') !== 1) + || preg_match('/^## ' . preg_quote($version, '/') . '(?:\s|$)/m', $markdown) === 1) { - throw new RuntimeException('Choose an unreleased version at least as new as the manifest, with one pending section in both changelogs.'); + throw new RuntimeException('Choose an unreleased version at least as new as the manifest.'); + } + + $xmlContents = file_get_contents($root . '/changelog.xml'); + $xmlMarkers = substr_count($xmlContents, '[[[NEXT_VERSION]]]'); + $markdownMarkers = substr_count($markdown, '[[[NEXT_VERSION]]]'); + + if ($xmlMarkers === 0 && $markdownMarkers === 0) + { + $note = 'Maintenance release with no additional changelog entries.'; + $xmlContents = preg_replace('/(]*>)/', '$1' . "\n \n" + . " com_joomengine_mcp\n component\n" + . ' ' . $version . "\n " . $note + . "\n ", $xmlContents, 1); + $markdown = preg_replace('/\A(# Changelog[^\r\n]*\r?\n)/', '$1' . "\n## " . $version + . "\n\n### Note\n\n- " . $note . "\n", $markdown, 1, $insertions); + + if ($changelog->getName() !== 'changelogs' || $insertions !== 1) + { + throw new RuntimeException('Expected the native XML changelogs root and Markdown Changelog heading.'); + } + } + elseif (count($pending) !== 1 || $xmlMarkers !== 1 || $markdownMarkers !== 1 + || preg_match('/^## \[\[\[NEXT_VERSION\]\]\]\s*$/m', $markdown) !== 1) + { + throw new RuntimeException('Pending changes require one version section in both changelogs.'); } $manifest->version = $version; $manifest->creationDate = gmdate('F Y'); $writes['joomengine_mcp.xml'] = $manifest->asXML(); - $writes['changelog.xml'] = str_replace('[[[NEXT_VERSION]]]', $version, file_get_contents($root . '/changelog.xml')); + $writes['changelog.xml'] = str_replace('[[[NEXT_VERSION]]]', $version, $xmlContents); $writes['CHANGELOG.md'] = str_replace('[[[NEXT_VERSION]]]', $version, $markdown); } else