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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
8 changes: 8 additions & 0 deletions changelog.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<changelogs>
<changelog>
<element>com_joomengine_mcp</element>
<type>component</type>
<version>[[[NEXT_VERSION]]]</version>
<fix>
<item>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.</item>
</fix>
</changelog>
<changelog>
<element>com_joomengine_mcp</element>
<type>component</type>
Expand Down
4 changes: 4 additions & 0 deletions docs/IMPLEMENTATION.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
43 changes: 43 additions & 0 deletions tests/release.php
Original file line number Diff line number Diff line change
Expand Up @@ -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, '<changelog>')))
&& 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('<changelogs>', '<changelogs>' . $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
Expand Down
31 changes: 28 additions & 3 deletions tools/release.php
Original file line number Diff line number Diff line change
Expand Up @@ -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('/(<changelogs\b[^>]*>)/', '$1' . "\n <changelog>\n"
. " <element>com_joomengine_mcp</element>\n <type>component</type>\n"
. ' <version>' . $version . "</version>\n <note><item>" . $note
. "</item></note>\n </changelog>", $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
Expand Down
Loading