Commit f318a56d1e1 for woocommerce

commit f318a56d1e1320a20ab42bce489ddff8025a3326
Author: Rostislav Wolný <1082140+costasovo@users.noreply.github.com>
Date:   Wed Jul 29 11:59:28 2026 +0200

    Fix changelogger destroying major entries on package release (#67059)

    * Fix changelogger destroying major entries and truncating text

    The changelogger rewrites a package's entire CHANGELOG.md on every release,
    so parse() must be able to read back what format() writes. It could not, in
    two ways.

    format() appends "[ **BREAKING CHANGE** ]" after a major significance, but
    parse() derived the significance from everything before the first " - ",
    so the marker was included, failed the patch/minor/major check, and the
    significance became null. The next release then re-emitted the entry as
    "-    - text": the breaking-change annotation was gone from released
    history and the 5-space indent broke markdownlint (MD007). It also emitted
    a PHP 8.2 ucfirst(null) deprecation. This already corrupted two 2.0.0
    entries in @woocommerce/email-editor.

    Separately, parse() kept only $row_segments[1], so any entry whose text
    contains " - " lost everything after its own separator. Three entries in
    the data, currency and onboarding packages are currently set up to be
    truncated on their next release.

    The marker is now a shared constant, since the bug was really that the
    writer and reader each carried their own copy of it. Running the real
    email-editor changelog through parse()/format() alters 0 of 106 entries
    after this change, against 2 before it.

    * Strip the changelog bullet as a literal prefix

    The bullet was interpolated into a regex, but it is a configurable property:
    Legacy_Core_Formatter sets it to "* ", which builds the invalid pattern "/* /".
    preg_replace then returns null rather than a string, so every row it touched
    was discarded and that formatter could not parse a changelog at all.

    The match was also unanchored, so on a line that did not begin with a bullet
    the first occurrence was removed from the middle of the entry text.

    Comparing a literal prefix addresses both and needs no escaping.

diff --git a/tools/changelogger/class-formatter.php b/tools/changelogger/class-formatter.php
index 230f75f8b5b..a707129140b 100644
--- a/tools/changelogger/class-formatter.php
+++ b/tools/changelogger/class-formatter.php
@@ -26,6 +26,14 @@ use Automattic\Jetpack\Changelogger\PluginTrait;
 class Formatter extends KeepAChangelogParser {
 	use PluginTrait;

+	/**
+	 * Marker appended after the significance of a major change. Written by format() and stripped by
+	 * parse(), so both sides must share this definition for a changelog to survive a round trip.
+	 *
+	 * @var string
+	 */
+	const BREAKING_CHANGE_MARKER = '[ **BREAKING CHANGE** ]';
+
 	/**
 	 * Bullet for changes.
 	 *
@@ -178,9 +186,19 @@ class Formatter extends KeepAChangelogParser {
 				$changes = array();
 				$rows    = explode( "\n", $content );
 				foreach ( $rows as $row ) {
-					$row          = trim( $row );
-					$row          = preg_replace( '/' . $this->bullet . '/', '', $row, 1 );
-					$row_segments = explode( ' - ', $row );
+					$row = trim( $row );
+					// Strip the bullet only as a literal prefix. It is configurable ('* ' in the legacy core
+					// formatter), so it is not safe to interpolate into a pattern, and removing a later
+					// occurrence would corrupt the entry text.
+					if ( 0 === strpos( $row, $this->bullet ) ) {
+						$row = substr( $row, strlen( $this->bullet ) );
+					}
+					// Drop the marker that format() appends after a major significance, otherwise it lands in the
+					// significance segment below and the entry is re-emitted with no significance at all.
+					$row = preg_replace( '/^(major)\s*' . preg_quote( self::BREAKING_CHANGE_MARKER, '/' ) . '/i', '$1', $row );
+
+					// Limit the split so that content containing " - " is not truncated at its own separator.
+					$row_segments = explode( ' - ', $row, 2 );
 					$significance = trim( strtolower( $row_segments[0] ) );

 					array_push(
@@ -253,7 +271,7 @@ class Formatter extends KeepAChangelogParser {
 			foreach ( $entry->getChangesBySubheading() as $heading => $changes ) {
 				foreach ( $changes as $change ) {
 					$significance    = $change->getSignificance();
-					$breaking_change = 'major' === $significance ? ' [ **BREAKING CHANGE** ]' : '';
+					$breaking_change = 'major' === $significance ? ' ' . self::BREAKING_CHANGE_MARKER : '';
 					$text            = trim( $change->getContent() );
 					if ( '' !== $text ) {
 						$preamble = $is_subentry ? '' : $bullet . ucfirst( $significance ) . $breaking_change . ' - ';