You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@logging.apache.org by vy...@apache.org on 2022/12/11 21:06:53 UTC

[logging-log4j-tools] branch master updated: LOG4J2-3628 Added `log4j-changelog` module.

This is an automated email from the ASF dual-hosted git repository.

vy pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/logging-log4j-tools.git


The following commit(s) were added to refs/heads/master by this push:
     new b748116  LOG4J2-3628 Added `log4j-changelog` module.
b748116 is described below

commit b748116be400714135324a3cd2e7e4ebe657121f
Author: Volkan Yazıcı <vo...@yazi.ci>
AuthorDate: Sun Dec 11 22:07:17 2022 +0100

    LOG4J2-3628 Added `log4j-changelog` module.
---
 CHANGELOG.adoc                                     |   2 +-
 README.adoc                                        |   5 +-
 log4j-changelog/README.adoc                        | 238 +++++++++++++
 log4j-changelog/pom.xml                            |   7 +-
 .../org/apache/logging/log4j/AsciiDocUtils.java    |  40 +++
 .../java/org/apache/logging/log4j/FileUtils.java   |  63 ++++
 .../logging/log4j/PositionalSaxEventHandler.java   | 103 ++++++
 .../org/apache/logging/log4j/PropertyUtils.java    |  58 +++
 .../java/org/apache/logging/log4j/StringUtils.java |  31 ++
 .../org/apache/logging/log4j/VersionUtils.java     |  38 ++
 .../java/org/apache/logging/log4j/XmlReader.java   | 117 ++++++
 .../java/org/apache/logging/log4j/XmlUtils.java    |  71 ++++
 .../java/org/apache/logging/log4j/XmlWriter.java   | 108 ++++++
 .../logging/log4j/changelog/ChangelogEntry.java    | 207 +++++++++++
 .../logging/log4j/changelog/ChangelogFiles.java    |  79 ++++
 .../logging/log4j/changelog/ChangelogRelease.java  |  69 ++++
 .../log4j/changelog/exporter/AsciiDocExporter.java | 396 +++++++++++++++++++++
 .../changelog/exporter/AsciiDocExporterArgs.java   |  40 +++
 .../log4j/changelog/importer/MavenChanges.java     | 203 +++++++++++
 .../changelog/importer/MavenChangesImporter.java   | 140 ++++++++
 .../importer/MavenChangesImporterArgs.java         |  45 +++
 .../changelog/releaser/ChangelogReleaser.java      | 138 +++++++
 .../changelog/releaser/ChangelogReleaserArgs.java  |  44 +++
 pom.xml                                            |  12 -
 24 files changed, 2234 insertions(+), 20 deletions(-)

diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc
index c9a6830..18d6767 100644
--- a/CHANGELOG.adoc
+++ b/CHANGELOG.adoc
@@ -17,4 +17,4 @@ limitations under the License.
 
 == 0.1.0 (????-??-??)
 
-* The first GA release.
+* Added `log4j-changelog` module (for https://issues.apache.org/jira/browse/LOG4J2-3628[LOG4J2-3628] by `vy`)
diff --git a/README.adoc b/README.adoc
index 228975b..925042f 100644
--- a/README.adoc
+++ b/README.adoc
@@ -19,7 +19,10 @@ https://github.com/apache/logging-log4j-tools/actions[image:https://github.com/a
 https://search.maven.org/search?q=g:org.apache.logging.log4j%20a:log4j-tools[image:https://img.shields.io/maven-central/v/org.apache.logging.log4j/log4j-tools.svg[Maven Central]]
 https://www.apache.org/licenses/LICENSE-2.0.txt[image:https://img.shields.io/github/license/apache/logging-log4j-tools.svg[License]]
 
-Tooling internally used by the Apache Log4j project infrastructure.
+Tooling internally used by https://logging.apache.org/log4j/2.x/[the Apache Log4j project] infrastructure.
+
+xref:log4j-changelog/README.adoc[`log4j-changelog`]::
+Tooling to export AsciiDoc-formatted changelog files.
 
 == License
 
diff --git a/log4j-changelog/README.adoc b/log4j-changelog/README.adoc
new file mode 100644
index 0000000..75f995e
--- /dev/null
+++ b/log4j-changelog/README.adoc
@@ -0,0 +1,238 @@
+////
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+////
+
+This project contains tooling for changelogs of Apache Log4j projects.
+
+[#what-is-a-changelog]
+== What is a changelog?
+
+A changelog is a log of all notable changes made to a project.
+
+[#why-different]
+== Why yet another changelog tool?
+
+Existing changelog practices (e.g., https://keepachangelog.com[Keep a changelog], https://maven.apache.org/plugins/maven-changes-plugin/[maven-changes-plugin]) store changelog entries in the same file.
+This creates merge conflicts between different branches.
+Imagine multiple people working on multiple branches each containing a change to `CHANGELOG.md`.
+Whoever succeeds in merging their branch to `master` first will cause a merge-conflict for the others, even though their work might be totally unrelated from each other.
+
+This project embraces a model where changelog entries are kept in separate files and hence are not prone to merge conflicts.
+Similar to `maven-changes-plugin`, changelog sources and their exports (e.g., AsciiDoc-formatted) are split by design.
+
+[#look]
+== How does it look like?
+
+All changelog _sources_ are stored in folders under _changelog directory_ (e.g., `/src/changelog`):
+
+[source]
+----
+$ ls -a -1 src/changelog
+.
+..
+# ...
+2.18.0
+2.19.0
+.2.x.x
+----
+
+Changelog sources of _released versions_ are stored in `<changelogDirectory>/<releaseVersion>` folders (e.g., `/src/changelog/2.19.0`):
+
+[source]
+----
+$ ls -a -1 src/changelog/2.19.0
+.
+..
+.intro.adoc
+LOG4J2-2975_Add_implementation_of_SLF4J2_fluent_API.xml
+LOG4J2-3545_Add_correct_manifest_entries_for_OSGi_to_log4j_jcl.xml
+LOG4J2-3548_Improve_support_for_passwordless_keystores.xml
+# ...
+LOG4J2-3589_Allow_Plugins_to_be_injected_with_the_LoggerContext_referenc.xml
+LOG4J2-3590_Remove_SLF4J_1_8_x_binding.xml
+LOG4J2-3614_Harden_InstantFormatter_against_delegate_failures.xml
+LOG4J2-708_Add_async_support_to_Log4jServletFilter.xml
+.release.xml
+----
+
+Changelog sources of _upcoming releases_ are stored in `<changelogDirectory>/.<releaseVersionMajor>.x.x` folders (e.g., `/src/changelog/.2.x.x`):
+
+[source]
+----
+$ ls -a -1 src/changelog/.2.x.x
+.
+..
+LOG4J2-2678_Add_LogEvent_timestamp_to_ProducerRecord_in_KafkaAppender.xml
+LOG4J2-3628_new_changelog_infra.xml
+LOG4J2-3631_Fix_Configurator_setLevel_for_internal_classes.xml
+LOG4J2-3634_Fix_level_propagation_in_Log4jBridgeHandler.xml
+----
+
+A typical changelog entry looks as follows:
+
+[source]
+----
+$ cat src/changelog/.2.x.x/LOG4J2-3628_new_changelog_infra.xml
+<?xml version="1.0" encoding="UTF-8"?>
+<entry type="changed">
+  <issue id="LOG4J2-3628" link="https://issues.apache.org/jira/browse/LOG4J2-3628"/>
+  <author id="vy"/>
+  <description format="asciidoc">
+    Replaced `maven-changes-plugin` with a custom changelog implementation
+    </description>
+</entry>
+----
+
+All changelog folders, including the ones for the upcoming releases, can be exported to AsciiDoc:
+
+[source]
+----
+$ ls -1 target/generated-sources/site/asciidoc/changelog
+# ...
+2.18.0.adoc
+2.19.0.adoc
+2.x.x.adoc
+index.adoc
+----
+
+[#released-version-changelogs]
+== Released version changelogs
+
+Changelogs of past released versions are contained in `<changelogDirectory>/<releaseVersion>` directories (e.g., `/src/changelog/2.19.0`).
+A released version changelog directory consists of following files:
+
+`.release.xml`::
+the meta information about the release (e.g., release version and date)
+
+`.intro.adoc`::
+the introductory content used by the AsciiDoc exporter
+
+`[<issueId>_]<shortSummary>.xml`::
+changelog entry associated with a change
+
+The AsciiDoc exporter will compile these source files and output a `<releaseVersion>.adoc` (e.g., `2.19.0.adoc`) file for each release and an `index.adoc`.
+
+[#unreleased-version-changelogs]
+== Unreleased version changelogs
+
+Changelogs of upcoming release versions are stored in `<changelogDirectory>/.<releaseVersionMajor>.x.x` directories (e.g., `/src/changelog/.2.x.x`).
+Compared to released version changelog directories (e.g., `2.19.0`), `.<releaseVersionMajor>.x.x` directories only consist of changelog entry files (i.e., `[<issueId>_]<shortSummary>.xml`).
+
+The AsciiDoc exporter will compile these source files and output `<releaseVersionMajor>.x.x.adoc` (e.g., `2.x.x.adoc`) file for each upcoming release.
+
+[#changelog-entry-file]
+== Changelog entry file
+
+A changelog entry file consists of short meta information regarding a particular change.
+They are named following the `[<issueId>_]<shortSummary>.xml` pattern.
+Consider the following examples:
+
+* `LOG4J2-3556_JsonTemplateLayout_stack_trace_truncation_fix.xml`
+* `LOG4J2-3578_Generate_new_SSL_certs_for_testing.xml`
+* `Update_jackson_2_11_0_2_11_2.xml`
+
+A sample _changelog entry_ file is shared below.
+
+.`src/changelog/LOG4J2-3556_JsonTemplateLayout_stack_trace_truncation_fix.xml` file contents
+[source,xml]
+----
+<entry type="fixed">
+  <issue id="LOG4J2-3556" link="https://issues.apache.org/jira/browse/LOG4J2-3556"/>
+  <author id="vy"/>
+  <author name="Arthur Gavlyukovskiy"/>
+  <description format="asciidoc">Make `JsonTemplateLayout` stack trace truncation operate for each label block.</description>
+</entry>
+----
+
+Some remarks about the structure of changelog entry files:
+
+* The root element must be named `entry`.
+* `entry.type` attribute is required and must be one of the https://keepachangelog.com/en/1.0.0/#how[_Keep a Changelog_ change types]:
+** `added` – for new features
+** `changed` – for changes in existing functionality
+** `deprecated` – for soon-to-be removed features
+** `removed` – for now removed features
+** `fixed` – for any bug fixes
+** `security` – for vulnerabilities
+* `issue` element is optional, and, if present, must contain `id` and `link` attributes.
+* `author` element must have at least one of `id` or `name` attributes.
+* There must be at least one author.
+* There must be a single `description` element with non-blank content and `format="asciidoc"` attribute.
+
+[#qa]
+== Q&A
+
+[#qa-entry]
+=== How can I add an entry for a change I am about to commit?
+
+You have just committed, or better, about to commit a great feature you have been working on.
+Simply create a <<#changelog-entry-file>> and commit it along with your change!
+
+[#qa-generate]
+=== How can I export changelogs to AsciiDoc files?
+
+You need to use `AsciiDocExporter` as follows:
+
+[source,bash]
+----
+java \
+    -cp /path/to/log4j-changelog.jar \
+    -Dlog4j.changelog.directory=/path/to/changelog/directory \
+    -Dlog4j.changelog.exporter.outputDirectory=/path/to/asciiDocOutputDirectory \
+    org.apache.logging.log4j.changelog.exporter.AsciiDocExporter
+----
+
+[#qa-deploy-release]
+=== I am about to deploy a new Log4j release. What shall I do?
+
+Just before a release, two things need to happen in the changelog sources:
+
+. Changelog entries of the upcoming release directory `<changelogDirectory>/.<releaseVersionMajor>.x.x` needs to be moved to the release changelog directory `<changelogDirectory>/<releaseVersion>`
+. `.index.adoc` and `.release.xml` need to be created in the release changelog directory `<changelogDirectory>/<releaseVersion>`
+
+Due to the nature of release candidates, above steps might need to be repeated multiple times.
+
+[TIP]
+====
+Log4j _releases_ and _release candidates_ all get deployed to the same https://repository.apache.org/#stagingRepositories[_staging repository_].
+Their `pom.xml` files all contain the same release version, e.g., `2.19.0`.
+There are no `-rc1`, `-rc2`, etc. suffixes in the version of a release candidate.
+Once a release candidate voting reaches to a consensus for release, associated artifacts simply get promoted from the _staging_ to the _public_ repository.
+Hence, there are no differences between releases and release candidates.
+====
+
+How to carry out aforementioned two changes are explained below in steps:
+
+. Populate the `<changelogDirectory>/<releaseVersion>` directory (e.g., `/src/changelog/2.19.0`) from the upcoming release changelog directory (e.g., `<changelogDirectory>/.2.x.x`):
++
+[source,bash]
+----
+java \
+    -cp /path/to/log4j-changelog.jar \
+    -Dlog4j.changelog.directory=/path/to/changelog/directory \
+    -Dlog4j.changelog.releaseVersion=X.Y.Z \
+    org.apache.logging.log4j.changelog.releaser.ChangelogReleaser
+----
+. Verify that `<changelogDirectory>/.<releaseVersionMajor>.x.x` directory (e.g., `/src/changelog/.2.x.x`) is emptied
+. Verify that `<changelogDirectory>/<releaseVersion>` directory (e.g., `/src/changelog/2.19.0`) is created, and it contains `.intro.adoc`, `.release.xml`, and changelog entry files
++
+[IMPORTANT]
+====
+If `<changelogDirectory>/<releaseVersion>` directory (e.g., `/src/changelog/2.19.0`) already exists with certain content, `ChangelogReleaser` will only move new changelog entry files and override `.release.xml`; `.intro.adoc` will not be touched, if exists.
+This allows one to run `ChangelogReleaser` multiple times, e.g., to incorporate changes added to a release candidate.
+====
+. Edit the created `.intro.adoc`
+. `git add` the changes in the changelog directory (e.g., `/src/changelog`) and commit them
diff --git a/log4j-changelog/pom.xml b/log4j-changelog/pom.xml
index 255d59a..a2763dc 100644
--- a/log4j-changelog/pom.xml
+++ b/log4j-changelog/pom.xml
@@ -29,9 +29,4 @@
 
   <artifactId>log4j-changelog</artifactId>
 
-  <properties>
-    <maven.compiler.source>17</maven.compiler.source>
-    <maven.compiler.target>17</maven.compiler.target>
-  </properties>
-
-</project>
\ No newline at end of file
+</project>
diff --git a/log4j-changelog/src/main/java/org/apache/logging/log4j/AsciiDocUtils.java b/log4j-changelog/src/main/java/org/apache/logging/log4j/AsciiDocUtils.java
new file mode 100644
index 0000000..711c63e
--- /dev/null
+++ b/log4j-changelog/src/main/java/org/apache/logging/log4j/AsciiDocUtils.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j;
+
+public final class AsciiDocUtils {
+
+    public static final String LICENSE_COMMENT_BLOCK = "////\n" +
+            "    Licensed to the Apache Software Foundation (ASF) under one or more\n" +
+            "    contributor license agreements.  See the NOTICE file distributed with\n" +
+            "    this work for additional information regarding copyright ownership.\n" +
+            "    The ASF licenses this file to You under the Apache License, Version 2.0\n" +
+            "    (the \"License\"); you may not use this file except in compliance with\n" +
+            "    the License.  You may obtain a copy of the License at\n" +
+            "\n" +
+            "         https://www.apache.org/licenses/LICENSE-2.0\n" +
+            "\n" +
+            "    Unless required by applicable law or agreed to in writing, software\n" +
+            "    distributed under the License is distributed on an \"AS IS\" BASIS,\n" +
+            "    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n" +
+            "    See the License for the specific language governing permissions and\n" +
+            "    limitations under the License.\n" +
+            "////\n";
+
+    private AsciiDocUtils() {}
+
+}
diff --git a/log4j-changelog/src/main/java/org/apache/logging/log4j/FileUtils.java b/log4j-changelog/src/main/java/org/apache/logging/log4j/FileUtils.java
new file mode 100644
index 0000000..d62089b
--- /dev/null
+++ b/log4j-changelog/src/main/java/org/apache/logging/log4j/FileUtils.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.stream.Stream;
+
+public final class FileUtils {
+
+    private FileUtils() {}
+
+    /**
+     * Finds files non-recursively in the given directory.
+     * <p>
+     * Given directory itself and hidden files are filtered out.
+     * </p>
+     */
+    @SuppressWarnings("RedundantIfStatement")
+    public static Stream<Path> findAdjacentFiles(final Path directory) {
+        try {
+            return Files
+                    .walk(directory, 1)
+                    .filter(path -> {
+
+                        // Skip the directory itself.
+                        if (path.equals(directory)) {
+                            return false;
+                        }
+
+                        // Skip hidden files.
+                        boolean hiddenFile = path.getFileName().toString().startsWith(".");
+                        if (hiddenFile) {
+                            return false;
+                        }
+
+                        // Accept the rest.
+                        return true;
+
+                    });
+        } catch (final IOException error) {
+            final String message = String.format("failed walking directory: `%s`", directory);
+            throw new UncheckedIOException(message, error);
+        }
+    }
+
+}
diff --git a/log4j-changelog/src/main/java/org/apache/logging/log4j/PositionalSaxEventHandler.java b/log4j-changelog/src/main/java/org/apache/logging/log4j/PositionalSaxEventHandler.java
new file mode 100644
index 0000000..d01ae52
--- /dev/null
+++ b/log4j-changelog/src/main/java/org/apache/logging/log4j/PositionalSaxEventHandler.java
@@ -0,0 +1,103 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j;
+
+import java.util.Stack;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.xml.sax.Attributes;
+import org.xml.sax.Locator;
+import org.xml.sax.helpers.DefaultHandler;
+
+/**
+ * A SAX2 event handler adding the associated line number to each emitted nodes' user data.
+ * <p>
+ * The added node user data is keyed with {@code lineNumber}.
+ * </p>
+ */
+final class PositionalSaxEventHandler extends DefaultHandler {
+
+    private final Stack<Element> elementStack = new Stack<>();
+
+    private final StringBuilder textBuffer = new StringBuilder();
+
+    private final Document document;
+
+    private Locator locator;
+
+    PositionalSaxEventHandler(final Document document) {
+        this.document = document;
+    }
+
+    @Override
+    public void setDocumentLocator(final Locator locator) {
+        this.locator = locator;
+    }
+
+    @Override
+    public void startElement(
+            final String uri,
+            final String localName,
+            final String qName,
+            final Attributes attributes) {
+        addTextIfNeeded();
+        final Element element = document.createElement(qName);
+        for (int attributeIndex = 0; attributeIndex < attributes.getLength(); attributeIndex++) {
+            final String attributeQName = attributes.getQName(attributeIndex);
+            final String attributeValue = attributes.getValue(attributeIndex);
+            element.setAttribute(attributeQName, attributeValue);
+        }
+        element.setUserData("lineNumber", String.valueOf(locator.getLineNumber()), null);
+        elementStack.push(element);
+    }
+
+    @Override
+    public void endElement(
+            final String uri,
+            final String localName,
+            final String qName) {
+        addTextIfNeeded();
+        final Element closedElement = elementStack.pop();
+        final boolean rootElement = elementStack.isEmpty();
+        if (rootElement) {
+            document.appendChild(closedElement);
+        } else {
+            final Element parentElement = elementStack.peek();
+            parentElement.appendChild(closedElement);
+        }
+    }
+
+    @Override
+    public void characters(final char[] buffer, final int start, final int length) {
+        textBuffer.append(buffer, start, length);
+    }
+
+    /**
+     * Outputs text accumulated under the current node.
+     */
+    private void addTextIfNeeded() {
+        if (textBuffer.length() > 0) {
+            final Element element = elementStack.peek();
+            final Node textNode = document.createTextNode(textBuffer.toString());
+            element.appendChild(textNode);
+            textBuffer.delete(0, textBuffer.length());
+        }
+    }
+
+}
diff --git a/log4j-changelog/src/main/java/org/apache/logging/log4j/PropertyUtils.java b/log4j-changelog/src/main/java/org/apache/logging/log4j/PropertyUtils.java
new file mode 100644
index 0000000..7aaba35
--- /dev/null
+++ b/log4j-changelog/src/main/java/org/apache/logging/log4j/PropertyUtils.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j;
+
+import java.nio.file.InvalidPathException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import static org.apache.logging.log4j.StringUtils.isBlank;
+
+public final class PropertyUtils {
+
+    private PropertyUtils() {}
+
+    public static Path requireNonBlankPathProperty(final String key) {
+        final String value = requireNonBlankStringProperty(key);
+        try {
+            return Paths.get(value);
+        } catch (final InvalidPathException error) {
+            final String message = String.format("system property `%s` doesn't contain a valid path: `%s`", key, value);
+            throw new IllegalArgumentException(message, error);
+        }
+    }
+
+    public static int requireNonBlankIntProperty(final String key) {
+        final String value = requireNonBlankStringProperty(key);
+        try {
+            return Integer.parseInt(value);
+        } catch (final NumberFormatException error) {
+            final String message = String.format("system property `%s` doesn't contain a valid integer: `%s`", key, value);
+            throw new IllegalArgumentException(message, error);
+        }
+    }
+
+    public static String requireNonBlankStringProperty(final String key) {
+        final String value = System.getProperty(key);
+        if (isBlank(value)) {
+            final String message = String.format("blank system property: `%s`", key);
+            throw new IllegalArgumentException(message);
+        }
+        return value;
+    }
+
+}
diff --git a/log4j-changelog/src/main/java/org/apache/logging/log4j/StringUtils.java b/log4j-changelog/src/main/java/org/apache/logging/log4j/StringUtils.java
new file mode 100644
index 0000000..7c6e8c5
--- /dev/null
+++ b/log4j-changelog/src/main/java/org/apache/logging/log4j/StringUtils.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j;
+
+public final class StringUtils {
+
+    private StringUtils() {}
+
+    public static String trimNullable(final String input) {
+        return input != null ? input.trim() : null;
+    }
+
+    public static boolean isBlank(final String input) {
+        return input == null || input.matches("\\s*");
+    }
+
+}
diff --git a/log4j-changelog/src/main/java/org/apache/logging/log4j/VersionUtils.java b/log4j-changelog/src/main/java/org/apache/logging/log4j/VersionUtils.java
new file mode 100644
index 0000000..becf004
--- /dev/null
+++ b/log4j-changelog/src/main/java/org/apache/logging/log4j/VersionUtils.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j;
+
+public final class VersionUtils {
+
+    public static final String VERSION_PATTERN = "^\\d+\\.\\d+.\\d+$";
+
+    private VersionUtils() {}
+
+    public static void requireSemanticVersioning(final String version, final String name) {
+        if (version.matches(VERSION_PATTERN)) {
+            final String message = String.format(
+                    "provided version in `%s` does not match the expected `major.minor.patch` pattern: `%s`",
+                    name, version);
+            throw new IllegalArgumentException(message);
+        }
+    }
+
+    public static int versionMajor(final String version) {
+        return Integer.parseInt(version.split("\\.", 2)[0]);
+    }
+
+}
diff --git a/log4j-changelog/src/main/java/org/apache/logging/log4j/XmlReader.java b/log4j-changelog/src/main/java/org/apache/logging/log4j/XmlReader.java
new file mode 100644
index 0000000..46f3143
--- /dev/null
+++ b/log4j-changelog/src/main/java/org/apache/logging/log4j/XmlReader.java
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j;
+
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+import javax.xml.parsers.*;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+/**
+ * A SAX2-based XML reader.
+ */
+public final class XmlReader {
+
+    private XmlReader() {}
+
+    public static Element readXmlFileRootElement(final Path path, final String rootElementName) {
+        try (final InputStream inputStream = new FileInputStream(path.toFile())) {
+            final Document document = readXml(inputStream);
+            final Element rootElement = document.getDocumentElement();
+            if (!rootElementName.equals(rootElement.getNodeName())) {
+                final String message = String.format(
+                        "was expecting root element to be called `%s`, found: `%s`",
+                        rootElementName, rootElement.getNodeName());
+                throw new IllegalArgumentException(message);
+            }
+            return rootElement;
+        } catch (final Exception error) {
+            final String message = String.format(
+                    "XML read failure for file `%s` and root element `%s`", path, rootElementName);
+            throw new RuntimeException(message, error);
+        }
+    }
+
+    private static Document readXml(final InputStream inputStream) throws Exception {
+        final SAXParserFactory parserFactory = SAXParserFactory.newInstance();
+        final SAXParser parser = parserFactory.newSAXParser();
+        final DocumentBuilderFactory documentBuilderFactory = XmlUtils.createDocumentBuilderFactory();
+        final DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
+        final Document document = documentBuilder.newDocument();
+        PositionalSaxEventHandler handler = new PositionalSaxEventHandler(document);
+        parser.parse(inputStream, handler);
+        return document;
+    }
+
+    public static Stream<Element> findChildElementsMatchingName(final Element parentElement, final String childElementName) {
+        final NodeList childNodes = parentElement.getChildNodes();
+        return IntStream
+                .range(0, childNodes.getLength())
+                .mapToObj(childNodes::item)
+                .filter(childNode -> childNode.getNodeType() == Node.ELEMENT_NODE && childElementName.equals(childNode.getNodeName()))
+                .map(childNode -> (Element) childNode);
+    }
+
+    public static Element requireChildElementMatchingName(final Element parentElement, final String childElementName) {
+        final List<Element> childElements = findChildElementsMatchingName(parentElement, childElementName)
+                .collect(Collectors.toList());
+        final int childElementCount = childElements.size();
+        if (childElementCount != 1) {
+            throw failureAtXmlNode(
+                    parentElement,
+                    "was expecting a single `%s` element, found: %d",
+                    childElementName,
+                    childElementCount);
+        }
+        return childElements.get(0);
+    }
+
+    public static String requireAttribute(final Element element, final String attributeName) {
+        if (!element.hasAttribute(attributeName)) {
+            throw failureAtXmlNode(element, "missing attribute: `%s`", attributeName);
+        }
+        return element.getAttribute(attributeName);
+    }
+
+    public static RuntimeException failureAtXmlNode(
+            final Node node,
+            final String messageFormat,
+            final Object... messageArgs) {
+        return failureAtXmlNode(null, node, messageFormat, messageArgs);
+    }
+
+    public static RuntimeException failureAtXmlNode(
+            final Throwable cause,
+            final Node node,
+            final String messageFormat,
+            final Object... messageArgs) {
+        final Object lineNumber = node.getUserData("lineNumber");
+        final String messagePrefix = String.format("[line %s] ", lineNumber);
+        final String message = String.format(messagePrefix + messageFormat, messageArgs);
+        return new IllegalArgumentException(message, cause);
+    }
+
+}
diff --git a/log4j-changelog/src/main/java/org/apache/logging/log4j/XmlUtils.java b/log4j-changelog/src/main/java/org/apache/logging/log4j/XmlUtils.java
new file mode 100644
index 0000000..0370815
--- /dev/null
+++ b/log4j-changelog/src/main/java/org/apache/logging/log4j/XmlUtils.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+final class XmlUtils {
+
+    private XmlUtils() {}
+
+    /**
+     * @return a {@link DocumentBuilderFactory} instance configured with certain XXE protection measures
+     * @see <a href="https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html#jaxp-documentbuilderfactory-saxparserfactory-and-dom4j">XML External Entity Prevention Cheat Sheet</a>
+     */
+    static DocumentBuilderFactory createDocumentBuilderFactory() {
+        final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
+        String feature = null;
+        try {
+
+            // This is the PRIMARY defense.
+            // If DTDs (doctypes) are disallowed, almost all XML entity attacks are prevented.
+            // Xerces 2 only - http://xerces.apache.org/xerces2-j/features.html#disallow-doctype-decl
+            feature = "http://apache.org/xml/features/disallow-doctype-decl";
+            dbf.setFeature(feature, true);
+
+            // If you can't completely disable DTDs, then at least do the following:
+            // Xerces 1 - http://xerces.apache.org/xerces-j/features.html#external-general-entities
+            // Xerces 2 - http://xerces.apache.org/xerces2-j/features.html#external-general-entities
+            // JDK7+ - http://xml.org/sax/features/external-general-entities
+            // This feature has to be used together with the following one, otherwise it will not protect you from XXE for sure.
+            feature = "http://xml.org/sax/features/external-general-entities";
+            dbf.setFeature(feature, false);
+
+            // Xerces 1 - http://xerces.apache.org/xerces-j/features.html#external-parameter-entities
+            // Xerces 2 - http://xerces.apache.org/xerces2-j/features.html#external-parameter-entities
+            // JDK7+ - http://xml.org/sax/features/external-parameter-entities
+            // This feature has to be used together with the previous one, otherwise it will not protect you from XXE for sure.
+            feature = "http://xml.org/sax/features/external-parameter-entities";
+            dbf.setFeature(feature, false);
+
+            // Disable external DTDs as well
+            feature = "http://apache.org/xml/features/nonvalidating/load-external-dtd";
+            dbf.setFeature(feature, false);
+
+            // and these as well, per Timothy Morgan's 2014 paper: "XML Schema, DTD, and Entity Attacks"
+            dbf.setXIncludeAware(false);
+            dbf.setExpandEntityReferences(false);
+
+        } catch (final ParserConfigurationException error) {
+            final String message = String.format("`%s` is probably not supported by your XML processor", feature);
+            throw new RuntimeException(message, error);
+        }
+        return dbf;
+    }
+
+}
diff --git a/log4j-changelog/src/main/java/org/apache/logging/log4j/XmlWriter.java b/log4j-changelog/src/main/java/org/apache/logging/log4j/XmlWriter.java
new file mode 100644
index 0000000..3776063
--- /dev/null
+++ b/log4j-changelog/src/main/java/org/apache/logging/log4j/XmlWriter.java
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j;
+
+import java.io.StringWriter;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.function.Consumer;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+
+import org.w3c.dom.Comment;
+import org.w3c.dom.Document;
+
+public final class XmlWriter {
+
+    private static final Charset ENCODING = StandardCharsets.UTF_8;
+
+    private XmlWriter() {}
+
+    public static void toFile(final Path filepath, final Consumer<Document> documentConsumer) {
+        try {
+            final String xml = toString(documentConsumer);
+            final byte[] xmlBytes = xml.getBytes(ENCODING);
+            Files.createDirectories(filepath.getParent());
+            Files.write(filepath, xmlBytes, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
+        } catch (final Exception error) {
+            final String message = String.format("failed writing XML to file `%s`", filepath);
+            throw new RuntimeException(message, error);
+        }
+    }
+
+    public static String toString(final Consumer<Document> documentConsumer) {
+        try {
+
+            // Create the document.
+            final DocumentBuilderFactory documentBuilderFactory = XmlUtils.createDocumentBuilderFactory();
+            final DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
+
+            // Append the license comment.
+            final Document document = documentBuilder.newDocument();
+            document.setXmlStandalone(true);
+            final Comment licenseComment = document.createComment("\n" +
+                    "   Licensed to the Apache Software Foundation (ASF) under one or more\n" +
+                    "   contributor license agreements.  See the NOTICE file distributed with\n" +
+                    "   this work for additional information regarding copyright ownership.\n" +
+                    "   The ASF licenses this file to You under the Apache License, Version 2.0\n" +
+                    "   (the \"License\"); you may not use this file except in compliance with\n" +
+                    "   the License.  You may obtain a copy of the License at\n" +
+                    "\n" +
+                    "       http://www.apache.org/licenses/LICENSE-2.0\n" +
+                    "\n" +
+                    "   Unless required by applicable law or agreed to in writing, software\n" +
+                    "   distributed under the License is distributed on an \"AS IS\" BASIS,\n" +
+                    "   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n" +
+                    "   See the License for the specific language governing permissions and\n" +
+                    "   limitations under the License." +
+                    "\n");
+            document.appendChild(licenseComment);
+
+            // Execute request changes.
+            documentConsumer.accept(document);
+
+            // Serialize the document.
+            return serializeXmlDocument(document);
+
+        } catch (final Exception error) {
+            throw new RuntimeException("failed writing XML", error);
+        }
+    }
+
+    private static String serializeXmlDocument(final Document document) throws Exception {
+        final Transformer transformer = TransformerFactory.newInstance().newTransformer();
+        final StreamResult result = new StreamResult(new StringWriter());
+        final DOMSource source = new DOMSource(document);
+        transformer.setOutputProperty(OutputKeys.ENCODING, ENCODING.name());
+        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
+        transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
+        transformer.transform(source, result);
+        return result.getWriter().toString()
+                // Life is too short to solve DOM transformer issues decently.
+                .replace("?><!--", "?>\n<!--")
+                .replace("--><", "-->\n<");
+    }
+
+}
diff --git a/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/ChangelogEntry.java b/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/ChangelogEntry.java
new file mode 100644
index 0000000..59f5513
--- /dev/null
+++ b/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/ChangelogEntry.java
@@ -0,0 +1,207 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.changelog;
+
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Locale;
+import java.util.stream.Collectors;
+
+import org.apache.logging.log4j.XmlReader;
+import org.apache.logging.log4j.XmlWriter;
+import org.w3c.dom.Element;
+
+import static org.apache.logging.log4j.StringUtils.trimNullable;
+
+public final class ChangelogEntry {
+
+    public final Type type;
+
+    public final List<Issue> issues;
+
+    public final List<Author> authors;
+
+    public final Description description;
+
+    public enum Type {
+
+        ADDED,
+
+        CHANGED,
+
+        DEPRECATED,
+
+        REMOVED,
+
+        FIXED,
+
+        SECURITY;
+
+        private String toXmlAttribute() {
+            return toString().toLowerCase(Locale.US);
+        }
+
+        private static Type fromXmlAttribute(final String attribute) {
+            final String upperCaseAttribute = attribute != null ? attribute.toUpperCase(Locale.US) : null;
+            return Type.valueOf(upperCaseAttribute);
+        }
+
+    }
+
+    public static final class Issue {
+
+        public final String id;
+
+        public final String link;
+
+        public Issue(final String id, final String link) {
+            this.id = id;
+            this.link = link;
+        }
+
+    }
+
+    public static final class Author {
+
+        public final String id;
+
+        public final String name;
+
+        public Author(final String id, final String name) {
+            this.id = id;
+            this.name = name;
+        }
+
+    }
+
+    public static final class Description {
+
+        public final String format;
+
+        public final String text;
+
+        public Description(final String format, final String text) {
+            this.format = format;
+            this.text = text;
+        }
+
+    }
+
+    public ChangelogEntry(
+            final Type type,
+            final List<Issue> issues,
+            final List<Author> authors,
+            final Description description) {
+        this.type = type;
+        this.issues = issues;
+        this.authors = authors;
+        this.description = description;
+    }
+
+    public void writeToXmlFile(final Path path) {
+        XmlWriter.toFile(path, document -> {
+
+            // Create the `entry` root element.
+            final Element entryElement = document.createElement("entry");
+            entryElement.setAttribute("type", type.toXmlAttribute());
+            document.appendChild(entryElement);
+
+            // Create the `issue` elements.
+            issues.forEach(issue -> {
+                final Element issueElement = document.createElement("issue");
+                issueElement.setAttribute("id", issue.id);
+                issueElement.setAttribute("link", issue.link);
+                entryElement.appendChild(issueElement);
+            });
+
+            // Create the `author` elements.
+            authors.forEach(author -> {
+                final Element authorElement = document.createElement("author");
+                if (author.id != null) {
+                    authorElement.setAttribute("id", author.id);
+                } else {
+                    authorElement.setAttribute("name", author.name);
+                }
+                entryElement.appendChild(authorElement);
+            });
+
+            // Create the `description` element.
+            final Element descriptionElement = document.createElement("description");
+            if (description.format != null) {
+                descriptionElement.setAttribute("format", description.format);
+            }
+            descriptionElement.setTextContent(description.text);
+            entryElement.appendChild(descriptionElement);
+
+        });
+    }
+
+    public static ChangelogEntry readFromXmlFile(final Path path) {
+
+        // Read the `entry` root element.
+        final Element entryElement = XmlReader.readXmlFileRootElement(path, "entry");
+        final String typeAttribute = XmlReader.requireAttribute(entryElement, "type");
+        final Type type;
+        try {
+            type = Type.fromXmlAttribute(typeAttribute);
+        } catch (final Exception error) {
+            throw XmlReader.failureAtXmlNode(error, entryElement, "`type` attribute read failure");
+        }
+
+        // Read the `issue` elements.
+        final List<Issue> issues = XmlReader
+                .findChildElementsMatchingName(entryElement, "issue")
+                .map(issueElement -> {
+                    final String issueId = XmlReader.requireAttribute(issueElement, "id");
+                    final String issueLink = XmlReader.requireAttribute(issueElement, "link");
+                    return new Issue(issueId, issueLink);
+                })
+                .collect(Collectors.toList());
+
+        // Read the `author` elements.
+        final List<Author> authors = XmlReader
+                .findChildElementsMatchingName(entryElement, "author")
+                .map(authorElement -> {
+                    final String authorId = authorElement.hasAttribute("id")
+                            ? authorElement.getAttribute("id")
+                            : null;
+                    final String authorName = authorElement.hasAttribute("name")
+                            ? authorElement.getAttribute("name")
+                            : null;
+                    if (authorId == null && authorName == null) {
+                        throw XmlReader.failureAtXmlNode(
+                                authorElement, "`author` must have at least one of `id` or `name` attributes");
+                    }
+                    return new Author(authorId, authorName);
+                })
+                .collect(Collectors.toList());
+        if (authors.isEmpty()) {
+            throw XmlReader.failureAtXmlNode(entryElement, "no `author` elements found");
+        }
+
+        // Read the `description` element.
+        final Element descriptionElement = XmlReader.requireChildElementMatchingName(entryElement, "description");
+        final String descriptionFormat = XmlReader.requireAttribute(descriptionElement, "format");
+        final String descriptionText = trimNullable(descriptionElement.getTextContent());
+        final Description description = new Description(descriptionFormat, descriptionText);
+
+        // Create the instance.
+        return new ChangelogEntry(type, issues, authors, description);
+
+    }
+
+}
diff --git a/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/ChangelogFiles.java b/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/ChangelogFiles.java
new file mode 100644
index 0000000..aaade2c
--- /dev/null
+++ b/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/ChangelogFiles.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.changelog;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public final class ChangelogFiles {
+
+    private ChangelogFiles() {}
+
+    public static Path unreleasedDirectory(final Path changelogDirectory, final int versionMajor) {
+        final String filename = String.format(".%d.x.x", versionMajor);
+        return changelogDirectory.resolve(filename);
+    }
+
+    public static Set<Integer> unreleasedDirectoryVersionMajors(final Path changelogDirectory) {
+        try {
+            return Files
+                    .walk(changelogDirectory, 1)
+                    .flatMap(path -> {
+
+                        // Skip the directory itself.
+                        if (path.equals(changelogDirectory)) {
+                            return Stream.empty();
+                        }
+
+                        // Only select directories matching with the `^\.(\d+)\.x\.x$` pattern.
+                        final Pattern versionPattern = Pattern.compile("^\\.(\\d+)\\.x\\.x$");
+                        final Matcher versionMatcher = versionPattern.matcher(path.getFileName().toString());
+                        if (!versionMatcher.matches()) {
+                            return Stream.empty();
+                        }
+                        final String versionMajorString = versionMatcher.group(1);
+                        final int versionMajor = Integer.parseInt(versionMajorString);
+                        return Stream.of(versionMajor);
+
+                    })
+                    .collect(Collectors.toSet());
+        } catch (final IOException error) {
+            final String message = String.format("failed walking directory: `%s`", changelogDirectory);
+            throw new UncheckedIOException(message, error);
+        }
+    }
+
+    public static Path releaseDirectory(final Path changelogDirectory, final String releaseVersion) {
+        return changelogDirectory.resolve(releaseVersion);
+    }
+
+    public static Path releaseXmlFile(final Path releaseDirectory) {
+        return releaseDirectory.resolve(".release.xml");
+    }
+
+    public static Path introAsciiDocFile(final Path releaseDirectory) {
+        return releaseDirectory.resolve(".intro.adoc");
+    }
+
+}
diff --git a/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/ChangelogRelease.java b/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/ChangelogRelease.java
new file mode 100644
index 0000000..b7ab562
--- /dev/null
+++ b/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/ChangelogRelease.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.changelog;
+
+import java.nio.file.Path;
+
+import org.apache.logging.log4j.XmlReader;
+import org.apache.logging.log4j.XmlWriter;
+import org.w3c.dom.Element;
+
+import static org.apache.logging.log4j.StringUtils.trimNullable;
+
+public final class ChangelogRelease {
+
+    public final String version;
+
+    public final String date;
+
+    public ChangelogRelease(final String version, final String date) {
+        this.version = version;
+        this.date = date;
+    }
+
+    public void writeToXmlFile(final Path path) {
+        XmlWriter.toFile(path, document -> {
+            final Element releaseElement = document.createElement("release");
+            releaseElement.setAttribute("version", version);
+            releaseElement.setAttribute("date", date);
+            document.appendChild(releaseElement);
+        });
+    }
+
+    public static ChangelogRelease readFromXmlFile(final Path path) {
+
+        // Read the XML file.
+        final Element releaseElement = XmlReader.readXmlFileRootElement(path, "release");
+
+        // Read the `version` attribute.
+        final String version = trimNullable(releaseElement.getAttribute("version"));
+        if (version == null) {
+            throw new IllegalArgumentException("blank or missing attribute: `version`");
+        }
+
+        // Read the `date` attribute.
+        final String date = trimNullable(releaseElement.getAttribute("date"));
+        if (date == null) {
+            throw new IllegalArgumentException("blank or missing attribute: `date`");
+        }
+
+        // Create the instance.
+        return new ChangelogRelease(version, date);
+
+    }
+
+}
diff --git a/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/exporter/AsciiDocExporter.java b/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/exporter/AsciiDocExporter.java
new file mode 100644
index 0000000..f8c383c
--- /dev/null
+++ b/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/exporter/AsciiDocExporter.java
@@ -0,0 +1,396 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.changelog.exporter;
+
+import org.apache.logging.log4j.AsciiDocUtils;
+import org.apache.logging.log4j.FileUtils;
+import org.apache.logging.log4j.changelog.ChangelogEntry;
+import org.apache.logging.log4j.changelog.ChangelogFiles;
+import org.apache.logging.log4j.changelog.ChangelogRelease;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public final class AsciiDocExporter {
+
+    private static final String AUTO_GENERATION_WARNING_ASCIIDOC = "////\n" +
+            "*DO NOT EDIT THIS FILE!*\n" +
+            "This file is automatically generated from the release changelog directory!\n" +
+            "////\n";
+
+    private AsciiDocExporter() {}
+
+    public static void main(final String[] mainArgs) {
+
+        // Read arguments.
+        final AsciiDocExporterArgs args = AsciiDocExporterArgs.fromSystemProperties();
+
+        // Find release directories.
+        final List<Path> releaseDirectories = FileUtils
+                .findAdjacentFiles(args.changelogDirectory)
+                .filter(file -> file.toFile().isDirectory())
+                .sorted(Comparator.comparing(releaseDirectory -> {
+                    final Path releaseXmlFile = ChangelogFiles.releaseXmlFile(releaseDirectory);
+                    final ChangelogRelease changelogRelease = ChangelogRelease.readFromXmlFile(releaseXmlFile);
+                    return changelogRelease.date;
+                }))
+                .collect(Collectors.toList());
+        final int releaseDirectoryCount = releaseDirectories.size();
+
+        // Read the release information files.
+        final List<ChangelogRelease> changelogReleases = releaseDirectories
+                .stream()
+                .map(releaseDirectory -> {
+                    final Path releaseXmlFile = ChangelogFiles.releaseXmlFile(releaseDirectory);
+                    return ChangelogRelease.readFromXmlFile(releaseXmlFile);
+                })
+                .collect(Collectors.toList());
+
+        // Export releases.
+        if (releaseDirectoryCount > 0) {
+
+            // Export each release directory.
+            for (int releaseIndex = 0; releaseIndex < releaseDirectories.size(); releaseIndex++) {
+                final Path releaseDirectory = releaseDirectories.get(releaseIndex);
+                final ChangelogRelease changelogRelease = changelogReleases.get(releaseIndex);
+                try {
+                    exportRelease(args.outputDirectory, releaseDirectory, changelogRelease);
+                } catch (final Exception error) {
+                    final String message =
+                            String.format("failed exporting release from directory `%s`", releaseDirectory);
+                    throw new RuntimeException(message, error);
+                }
+            }
+
+            // Report the operation.
+            if (releaseDirectoryCount == 1) {
+                System.out.format("exported a single release directory: `%s`%n", releaseDirectories.get(0));
+            } else {
+                System.out.format(
+                        "exported %d release directories: ..., `%s`%n",
+                        releaseDirectories.size(),
+                        releaseDirectories.get(releaseDirectoryCount - 1));
+            }
+
+        }
+
+        // Export unreleased.
+        ChangelogFiles
+                .unreleasedDirectoryVersionMajors(args.changelogDirectory)
+                .stream()
+                .sorted(Comparator.reverseOrder())
+                .forEach(upcomingReleaseVersionMajor -> {
+                    final Path upcomingReleaseDirectory =
+                            ChangelogFiles.unreleasedDirectory(args.changelogDirectory, upcomingReleaseVersionMajor);
+                    final ChangelogRelease upcomingRelease = upcomingRelease(upcomingReleaseVersionMajor);
+                    System.out.format("exporting upcoming release directory: `%s`%n", upcomingReleaseDirectory);
+                    exportUnreleased(args.outputDirectory, upcomingReleaseDirectory, upcomingRelease);
+                    changelogReleases.add(upcomingRelease);
+                });
+
+        // Export the release index.
+        exportReleaseIndex(args.outputDirectory, changelogReleases);
+
+    }
+
+    private static void exportRelease(
+            final Path outputDirectory,
+            final Path releaseDirectory,
+            final ChangelogRelease changelogRelease) {
+        final String introAsciiDoc = readIntroAsciiDoc(releaseDirectory);
+        final List<ChangelogEntry> changelogEntries = readChangelogEntries(releaseDirectory);
+        try {
+            exportRelease(outputDirectory, changelogRelease, introAsciiDoc, changelogEntries);
+        } catch (final IOException error) {
+            final String message = String.format("failed exporting release from directory `%s`", releaseDirectory);
+            throw new UncheckedIOException(message, error);
+        }
+    }
+
+    private static String readIntroAsciiDoc(final Path releaseDirectory) {
+
+        // Determine the file to be read.
+        final Path introAsciiDocFile = ChangelogFiles.introAsciiDocFile(releaseDirectory);
+        if (!Files.exists(introAsciiDocFile)) {
+            return "";
+        }
+
+        // Read the file.
+        final List<String> introAsciiDocLines;
+        try {
+            introAsciiDocLines = Files.readAllLines(introAsciiDocFile, StandardCharsets.UTF_8);
+        } catch (final IOException error) {
+            final String message = String.format("failed reading intro AsciiDoc file: `%s`", introAsciiDocFile);
+            throw new UncheckedIOException(message, error);
+        }
+
+        // Erase comment blocks.
+        final boolean[] inCommentBlock = {false};
+        return introAsciiDocLines
+                .stream()
+                .filter(line -> {
+                    final boolean commentBlock = "////".equals(line);
+                    if (commentBlock) {
+                        inCommentBlock[0] = !inCommentBlock[0];
+                        return false;
+                    }
+                    return !inCommentBlock[0];
+                })
+                .collect(Collectors.joining("\n"))
+                + "\n";
+
+    }
+
+    private static List<ChangelogEntry> readChangelogEntries(final Path releaseDirectory) {
+        return FileUtils
+                .findAdjacentFiles(releaseDirectory)
+                // Sorting is needed to generate the same output between different runs.
+                .sorted()
+                .map(ChangelogEntry::readFromXmlFile)
+                .collect(Collectors.toList());
+    }
+
+    private static void exportRelease(
+            final Path outputDirectory,
+            final ChangelogRelease release,
+            final String introAsciiDoc,
+            final List<ChangelogEntry> entries)
+            throws IOException {
+        final String asciiDocFilename = changelogReleaseAsciiDocFilename(release);
+        final Path asciiDocFile = outputDirectory.resolve(asciiDocFilename);
+        Files.createDirectories(asciiDocFile.getParent());
+        final String asciiDoc = exportReleaseToAsciiDoc(release, introAsciiDoc, entries);
+        final byte[] asciiDocBytes = asciiDoc.getBytes(StandardCharsets.UTF_8);
+        Files.write(asciiDocFile, asciiDocBytes, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
+    }
+
+    private static String exportReleaseToAsciiDoc(
+            final ChangelogRelease release,
+            final String introAsciiDoc,
+            final List<ChangelogEntry> entries) {
+
+        // Write the header.
+        final StringBuilder stringBuilder = new StringBuilder();
+        stringBuilder
+                .append(AsciiDocUtils.LICENSE_COMMENT_BLOCK)
+                .append('\n')
+                .append(AUTO_GENERATION_WARNING_ASCIIDOC)
+                .append('\n')
+                .append("= ")
+                .append(release.version);
+        if (release.date != null) {
+            stringBuilder
+                    .append(" (")
+                    .append(release.date)
+                    .append(")\n")
+                    .append(introAsciiDoc)
+                    .append("\n");
+        } else {
+            stringBuilder.append("\n\nChanges staged for the next version that is yet to be released.\n\n");
+        }
+
+        if (!entries.isEmpty()) {
+
+            stringBuilder.append("== Changes\n");
+
+            // Group entries by type.
+            final Map<ChangelogEntry.Type, List<ChangelogEntry>> entriesByType = entries
+                    .stream()
+                    .collect(Collectors.groupingBy(changelogEntry -> changelogEntry.type));
+
+            // Write entries for each type.
+            entriesByType
+                    .keySet()
+                    .stream()
+                    // Sorting is necessary for a consistent layout across different runs.
+                    .sorted()
+                    .forEach(type -> {
+                        stringBuilder.append('\n');
+                        appendEntryTypeHeader(stringBuilder, type);
+                        entriesByType.get(type).forEach(entry -> appendEntry(stringBuilder, entry));
+                    });
+
+        }
+
+        // Return the accumulated document so far.
+        return stringBuilder.toString();
+
+    }
+
+    private static void appendEntryTypeHeader(final StringBuilder stringBuilder, final ChangelogEntry.Type type) {
+        final String typeName = type.toString().toLowerCase(Locale.US);
+        final String header = typeName.substring(0, 1).toUpperCase(Locale.US) + typeName.substring(1);
+        stringBuilder
+                .append("=== ")
+                .append(header)
+                .append("\n\n");
+    }
+
+    private static void appendEntry(final StringBuilder stringBuilder, final ChangelogEntry entry) {
+        stringBuilder.append("* ");
+        appendEntryDescription(stringBuilder, entry.description);
+        final boolean containingIssues = !entry.issues.isEmpty();
+        final boolean containingAuthors = !entry.authors.isEmpty();
+        if (containingIssues || containingAuthors) {
+            stringBuilder.append(" (");
+            if (containingIssues) {
+                appendEntryIssues(stringBuilder, entry.issues);
+            }
+            if (containingIssues && containingAuthors) {
+                stringBuilder.append(' ');
+            }
+            if (containingAuthors) {
+                appendEntryAuthors(stringBuilder, entry.authors);
+            }
+            stringBuilder.append(")");
+        }
+        stringBuilder.append('\n');
+    }
+
+    private static void appendEntryDescription(
+            final StringBuilder stringBuilder,
+            final ChangelogEntry.Description description) {
+        if (!"asciidoc".equals(description.format)) {
+            final String message = String.format("unsupported description format: `%s`", description.format);
+            throw new RuntimeException(message);
+        }
+        stringBuilder.append(description.text);
+    }
+
+    private static void appendEntryIssues(
+            final StringBuilder stringBuilder,
+            final List<ChangelogEntry.Issue> issues) {
+        stringBuilder.append("for ");
+        final int issueCount = issues.size();
+        for (int issueIndex = 0; issueIndex < issueCount; issueIndex++) {
+            final ChangelogEntry.Issue issue = issues.get(issueIndex);
+            appendEntryIssue(stringBuilder, issue);
+            if ((issueIndex + 1) != issueCount) {
+                stringBuilder.append(", ");
+            }
+        }
+    }
+
+    private static void appendEntryIssue(final StringBuilder stringBuilder, final ChangelogEntry.Issue issue) {
+        stringBuilder
+                .append(issue.link)
+                .append('[')
+                .append(issue.id)
+                .append(']');
+    }
+
+    private static void appendEntryAuthors(
+            final StringBuilder stringBuilder,
+            final List<ChangelogEntry.Author> authors) {
+        stringBuilder.append("by ");
+        final int authorCount = authors.size();
+        for (int authorIndex = 0; authorIndex < authors.size(); authorIndex++) {
+            final ChangelogEntry.Author author = authors.get(authorIndex);
+            appendEntryAuthor(stringBuilder, author);
+            if ((authorIndex + 1) != authorCount) {
+                stringBuilder.append(", ");
+            }
+        }
+    }
+
+    private static void appendEntryAuthor(final StringBuilder stringBuilder, final ChangelogEntry.Author author) {
+        if (author.id != null) {
+            stringBuilder
+                    .append('`')
+                    .append(author.id)
+                    .append('`');
+        } else {
+            // Normalize author names written in `Doe, John` form.
+            if (author.name.contains(",")) {
+                String[] nameFields = author.name.split(",", 2);
+                stringBuilder.append(nameFields[1].trim());
+                stringBuilder.append(nameFields[0].trim());
+            } else {
+                stringBuilder.append(author.name);
+            }
+        }
+    }
+
+    private static void exportUnreleased(
+            final Path outputDirectory,
+            final Path upcomingReleaseDirectory,
+            final ChangelogRelease upcomingRelease) {
+        final List<ChangelogEntry> changelogEntries = readChangelogEntries(upcomingReleaseDirectory);
+        try {
+            exportRelease(outputDirectory, upcomingRelease, null, changelogEntries);
+        } catch (final IOException error) {
+            throw new UncheckedIOException("failed exporting unreleased changes", error);
+        }
+    }
+
+    private static ChangelogRelease upcomingRelease(final int versionMajor) {
+        final String releaseVersion = versionMajor + ".x.x";
+        return new ChangelogRelease(releaseVersion, null);
+    }
+
+    private static void exportReleaseIndex(
+            final Path outputDirectory,
+            final List<ChangelogRelease> changelogReleases) {
+        final String asciiDoc = exportReleaseIndexToAsciiDoc(changelogReleases);
+        final byte[] asciiDocBytes = asciiDoc.getBytes(StandardCharsets.UTF_8);
+        final Path asciiDocFile = outputDirectory.resolve("index.adoc");
+        System.out.format("exporting release index to `%s`%n", asciiDocFile);
+        try {
+            Files.write(asciiDocFile, asciiDocBytes);
+        } catch (final IOException error) {
+            throw new UncheckedIOException(error);
+        }
+    }
+
+    private static String exportReleaseIndexToAsciiDoc(final List<ChangelogRelease> changelogReleases) {
+        final StringBuilder stringBuilder = new StringBuilder();
+        stringBuilder
+                .append(AsciiDocUtils.LICENSE_COMMENT_BLOCK)
+                .append('\n')
+                .append(AUTO_GENERATION_WARNING_ASCIIDOC)
+                .append("\n= Release changelogs\n\n");
+        for (int releaseIndex = changelogReleases.size() - 1; releaseIndex >= 0; releaseIndex--) {
+            final ChangelogRelease changelogRelease = changelogReleases.get(releaseIndex);
+            final String asciiDocFilename = changelogReleaseAsciiDocFilename(changelogRelease);
+            final String asciiDocBulletDateSuffix = changelogRelease.date != null
+                    ? (" (" + changelogRelease.date + ')')
+                    : "";
+            final String asciiDocBullet = String.format(
+                    "* xref:%s[%s]%s\n",
+                    asciiDocFilename,
+                    changelogRelease.version,
+                    asciiDocBulletDateSuffix);
+            stringBuilder.append(asciiDocBullet);
+        }
+        return stringBuilder.toString();
+    }
+
+    private static String changelogReleaseAsciiDocFilename(final ChangelogRelease changelogRelease) {
+        // Using only the version (that is, avoiding the date) in the filename so that one can determine the link to the changelog of a particular release with only version information.
+        return String.format("%s.adoc", changelogRelease.version);
+    }
+
+}
diff --git a/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/exporter/AsciiDocExporterArgs.java b/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/exporter/AsciiDocExporterArgs.java
new file mode 100644
index 0000000..5fb7597
--- /dev/null
+++ b/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/exporter/AsciiDocExporterArgs.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.changelog.exporter;
+
+import java.nio.file.Path;
+
+import static org.apache.logging.log4j.PropertyUtils.requireNonBlankPathProperty;
+
+final class AsciiDocExporterArgs {
+
+    final Path changelogDirectory;
+
+    final Path outputDirectory;
+
+    private AsciiDocExporterArgs(final Path changelogDirectory, final Path outputDirectory) {
+        this.changelogDirectory = changelogDirectory;
+        this.outputDirectory = outputDirectory;
+    }
+
+    static AsciiDocExporterArgs fromSystemProperties() {
+        final Path changelogDirectory = requireNonBlankPathProperty("log4j.changelog.directory");
+        final Path outputDirectory = requireNonBlankPathProperty("log4j.changelog.exporter.outputDirectory");
+        return new AsciiDocExporterArgs(changelogDirectory, outputDirectory);
+    }
+
+}
diff --git a/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/importer/MavenChanges.java b/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/importer/MavenChanges.java
new file mode 100644
index 0000000..743b6e6
--- /dev/null
+++ b/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/importer/MavenChanges.java
@@ -0,0 +1,203 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.changelog.importer;
+
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+import org.apache.logging.log4j.XmlReader;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import static org.apache.logging.log4j.StringUtils.isBlank;
+import static org.apache.logging.log4j.StringUtils.trimNullable;
+import static org.apache.logging.log4j.XmlReader.failureAtXmlNode;
+import static org.apache.logging.log4j.XmlReader.readXmlFileRootElement;
+
+final class MavenChanges {
+
+    final List<Release> releases;
+
+    private MavenChanges(final List<Release> releases) {
+        this.releases = releases;
+    }
+
+    static MavenChanges readFromFile(final Path file) {
+
+        // Read the root element.
+        final Element documentElement = readXmlFileRootElement(file, "document");
+
+        // Read the `body` element.
+        final Element bodyElement = XmlReader.requireChildElementMatchingName(documentElement, "body");
+
+        // Read releases.
+        final List<Release> releases = new ArrayList<>();
+        final NodeList releaseNodes = bodyElement.getChildNodes();
+        final int releaseNodeCount = releaseNodes.getLength();
+        for (int releaseNodeIndex = 0; releaseNodeIndex < releaseNodeCount; releaseNodeIndex++) {
+            final Node releaseNode = releaseNodes.item(releaseNodeIndex);
+            if ("release".equals(releaseNode.getNodeName()) && Node.ELEMENT_NODE == releaseNode.getNodeType()) {
+                final Element releaseElement = (Element) releaseNode;
+                final Release release = Release.fromElement(releaseElement);
+                releases.add(release);
+            }
+        }
+
+        // Create the instance.
+        return new MavenChanges(releases);
+
+    }
+
+    static final class Release {
+
+        final String version;
+
+        final String date;
+
+        final List<Action> actions;
+
+        private Release(final String version, final String date, final List<Action> actions) {
+            this.version = version;
+            this.date = date;
+            this.actions = actions;
+        }
+
+        private static Release fromElement(final Element element) {
+
+            // Read `version`.
+            final String version = trimNullable(element.getAttribute("version"));
+            if (isBlank(version)) {
+                throw XmlReader.failureAtXmlNode(element, "blank attribute: `version`");
+            }
+
+            // Read `date`.
+            final String date = trimNullable(element.getAttribute("date"));
+            final String datePattern = "^(TBD|[0-9]{4}-[0-9]{2}-[0-9]{2})$";
+            if (!date.matches(datePattern)) {
+                throw XmlReader.failureAtXmlNode(element, "`date` doesn't match with the `%s` pattern: `%s`", datePattern, date);
+            }
+
+            // Read actions.
+            final List<Action> actions = new ArrayList<>();
+            final NodeList actionNodes = element.getChildNodes();
+            final int actionNodeCount = actionNodes.getLength();
+            for (int actionNodeIndex = 0; actionNodeIndex < actionNodeCount; actionNodeIndex++) {
+                final Node actionNode = actionNodes.item(actionNodeIndex);
+                if ("action".equals(actionNode.getNodeName()) && Node.ELEMENT_NODE == actionNode.getNodeType()) {
+                    Element actionElement = (Element) actionNode;
+                    Action action = Action.fromElement(actionElement);
+                    actions.add(action);
+                }
+            }
+
+            // Create the instance.
+            return new Release(version, date, actions);
+
+        }
+
+        @Override
+        public String toString() {
+            return version + " @ " + date;
+        }
+
+    }
+
+    static final class Action {
+
+        final String issue;
+
+        final Type type;
+
+        final String dev;
+
+        final String dueTo;
+
+        final String description;
+
+        enum Type {ADD, FIX, UPDATE, REMOVE}
+
+        private Action(
+                final String issue,
+                final Type type,
+                final String dev,
+                final String dueTo,
+                final String description) {
+            this.issue = issue;
+            this.type = type;
+            this.dev = dev;
+            this.dueTo = dueTo;
+            this.description = description;
+        }
+
+        private static Action fromElement(final Element element) {
+
+            // Read `issue`.
+            String issue = trimNullable(element.getAttribute("issue"));
+            final String issuePattern = "^LOG4J2-[0-9]+$";
+            if (isBlank(issue)) {
+                issue = null;
+            } else if (!issue.matches(issuePattern)) {
+                throw XmlReader.failureAtXmlNode(element, "`issue` doesn't match with the `%s` pattern: `%s`", issuePattern, issue);
+            }
+
+            // Read `type`.
+            final String typeString = trimNullable(element.getAttribute("type"));
+            final Type type;
+            if (isBlank(typeString)) {
+                type = Type.UPDATE;
+            } else {
+                try {
+                    type = Type.valueOf(typeString.toUpperCase(Locale.US));
+                } catch (IllegalArgumentException error) {
+                    throw failureAtXmlNode(error, element, "invalid type: `%s`", typeString);
+                }
+            }
+
+            // Read `dev`.
+            final String dev = trimNullable(element.getAttribute("dev"));
+            if (isBlank(dev)) {
+                throw XmlReader.failureAtXmlNode(element, "blank attribute: `dev`");
+            }
+
+            // Read `dueTo`.
+            String dueTo = trimNullable(element.getAttribute("due-to"));
+            if (isBlank(dueTo)) {
+                dueTo = null;
+            }
+
+            // Read `description`.
+            final String description = trimNullable(element.getTextContent());
+            if (isBlank(description)) {
+                throw XmlReader.failureAtXmlNode(element, "blank `description`");
+            }
+
+            // Create the instance.
+            return new Action(issue, type, dev, dueTo, description);
+
+        }
+
+        @Override
+        public String toString() {
+            return issue != null ? issue : "unknown";
+        }
+
+    }
+
+}
diff --git a/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/importer/MavenChangesImporter.java b/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/importer/MavenChangesImporter.java
new file mode 100644
index 0000000..e57dcb0
--- /dev/null
+++ b/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/importer/MavenChangesImporter.java
@@ -0,0 +1,140 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.changelog.importer;
+
+import org.apache.logging.log4j.changelog.ChangelogEntry;
+import org.apache.logging.log4j.changelog.ChangelogFiles;
+import org.apache.logging.log4j.changelog.ChangelogRelease;
+
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.apache.logging.log4j.StringUtils.isBlank;
+
+public final class MavenChangesImporter {
+
+    private MavenChangesImporter() {}
+
+    public static void main(final String[] mainArgs) {
+        final MavenChangesImporterArgs args = MavenChangesImporterArgs.fromSystemProperties();
+        final MavenChanges mavenChanges = MavenChanges.readFromFile(args.changesXmlFile);
+        mavenChanges.releases.forEach(release -> {
+            if ("TBD".equals(release.date)) {
+                writeUnreleased(args.changelogDirectory, args.releaseVersionMajor, release);
+            } else {
+                writeReleased(args.changelogDirectory, release);
+            }
+        });
+    }
+
+    private static void writeUnreleased(
+            final Path changelogDirectory,
+            final int releaseVersionMajor,
+            final MavenChanges.Release release) {
+        final Path releaseDirectory = ChangelogFiles.unreleasedDirectory(changelogDirectory, releaseVersionMajor);
+        release.actions.forEach(action -> writeAction(releaseDirectory, action));
+    }
+
+    private static void writeReleased(final Path changelogDirectory, final MavenChanges.Release release) {
+
+        // Determine the directory for this particular release.
+        final Path releaseDirectory = ChangelogFiles.releaseDirectory(changelogDirectory, release.version);
+
+        // Write release information.
+        final Path releaseFile = ChangelogFiles.releaseXmlFile(releaseDirectory);
+        final ChangelogRelease changelogRelease = new ChangelogRelease(release.version, release.date);
+        changelogRelease.writeToXmlFile(releaseFile);
+
+        // Write release actions.
+        release.actions.forEach(action -> writeAction(releaseDirectory, action));
+
+    }
+
+    private static void writeAction(final Path releaseDirectory, final MavenChanges.Action action) {
+        final ChangelogEntry changelogEntry = changelogEntry(action);
+        final String changelogEntryFilename = changelogEntryFilename(action);
+        final Path changelogEntryFile = releaseDirectory.resolve(changelogEntryFilename);
+        changelogEntry.writeToXmlFile(changelogEntryFile);
+    }
+
+    private static String changelogEntryFilename(final MavenChanges.Action action) {
+        final StringBuilder actionRelativeFileBuilder = new StringBuilder();
+        if (action.issue != null) {
+            actionRelativeFileBuilder
+                    .append(action.issue)
+                    .append('_');
+        }
+        final String sanitizedDescription = action
+                .description
+                .substring(0, Math.min(action.description.length(), 60))
+                .replaceAll("[^A-Za-z0-9]", "_")
+                .replaceAll("_+", "_")
+                .replaceAll("[^A-Za-z0-9]$", "");
+        actionRelativeFileBuilder.append(sanitizedDescription);
+        actionRelativeFileBuilder.append(".xml");
+        return actionRelativeFileBuilder.toString();
+    }
+
+    private static ChangelogEntry changelogEntry(final MavenChanges.Action action) {
+
+        // Create the `type`.
+        final ChangelogEntry.Type type = changelogType(action.type);
+
+        // Create the `issue`s.
+        final List<ChangelogEntry.Issue> issues = new ArrayList<>(1);
+        if (action.issue != null) {
+            final String issueLink = String.format("https://issues.apache.org/jira/browse/%s", action.issue);
+            final ChangelogEntry.Issue issue = new ChangelogEntry.Issue(action.issue, issueLink);
+            issues.add(issue);
+        }
+
+        // Create the `author`s.
+        final List<ChangelogEntry.Author> authors = new ArrayList<>(2);
+        for (final String authorId : action.dev.split("\\s*,\\s*")) {
+            if (!isBlank(authorId)) {
+                authors.add(new ChangelogEntry.Author(authorId, null));
+            }
+        }
+        if (action.dueTo != null) {
+            authors.add(new ChangelogEntry.Author(null, action.dueTo));
+        }
+
+        // Create the `description`.
+        final ChangelogEntry.Description description = new ChangelogEntry.Description("asciidoc", action.description);
+
+        // Create the instance.
+        return new ChangelogEntry(type, issues, authors, description);
+
+    }
+
+    /**
+     * Maps `maven-changes-plugin` action types to their `Keep a Changelog` equivalents.
+     */
+    private static ChangelogEntry.Type changelogType(final MavenChanges.Action.Type type) {
+        if (MavenChanges.Action.Type.ADD.equals(type)) {
+            return ChangelogEntry.Type.ADDED;
+        } else if (MavenChanges.Action.Type.FIX.equals(type)) {
+            return ChangelogEntry.Type.FIXED;
+        } else if (MavenChanges.Action.Type.REMOVE.equals(type)) {
+            return ChangelogEntry.Type.REMOVED;
+        } else {
+            return ChangelogEntry.Type.CHANGED;
+        }
+    }
+
+}
diff --git a/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/importer/MavenChangesImporterArgs.java b/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/importer/MavenChangesImporterArgs.java
new file mode 100644
index 0000000..78e541d
--- /dev/null
+++ b/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/importer/MavenChangesImporterArgs.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.changelog.importer;
+
+import java.nio.file.Path;
+
+import static org.apache.logging.log4j.PropertyUtils.requireNonBlankIntProperty;
+import static org.apache.logging.log4j.PropertyUtils.requireNonBlankPathProperty;
+
+final class MavenChangesImporterArgs {
+
+    final Path changelogDirectory;
+
+    final Path changesXmlFile;
+
+    final int releaseVersionMajor;
+
+    private MavenChangesImporterArgs(final Path changelogDirectory, final Path changesXmlFile, final int releaseVersionMajor) {
+        this.changelogDirectory = changelogDirectory;
+        this.changesXmlFile = changesXmlFile;
+        this.releaseVersionMajor = releaseVersionMajor;
+    }
+
+    static MavenChangesImporterArgs fromSystemProperties() {
+        final Path changelogDirectory = requireNonBlankPathProperty("log4j.changelog.directory");
+        final Path changesXmlFile = requireNonBlankPathProperty("log4j.changelog.changesXmlFile");
+        final int releaseVersion = requireNonBlankIntProperty("log4j.changelog.releaseVersionMajor");
+        return new MavenChangesImporterArgs(changelogDirectory, changesXmlFile, releaseVersion);
+    }
+
+}
diff --git a/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/releaser/ChangelogReleaser.java b/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/releaser/ChangelogReleaser.java
new file mode 100644
index 0000000..17745cb
--- /dev/null
+++ b/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/releaser/ChangelogReleaser.java
@@ -0,0 +1,138 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.changelog.releaser;
+
+import org.apache.logging.log4j.AsciiDocUtils;
+import org.apache.logging.log4j.FileUtils;
+import org.apache.logging.log4j.VersionUtils;
+import org.apache.logging.log4j.changelog.ChangelogFiles;
+import org.apache.logging.log4j.changelog.ChangelogRelease;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.LocalDate;
+
+import static java.time.format.DateTimeFormatter.ISO_DATE;
+import static org.apache.logging.log4j.changelog.ChangelogFiles.releaseDirectory;
+
+public final class ChangelogReleaser {
+
+    private ChangelogReleaser() {}
+
+    public static void main(final String[] mainArgs) throws Exception {
+
+        // Read arguments.
+        final ChangelogReleaserArgs args = ChangelogReleaserArgs.fromSystemProperties();
+
+        // Read the release date and version.
+        final String releaseDate = ISO_DATE.format(LocalDate.now());
+        final int releaseVersionMajor = VersionUtils.versionMajor(args.releaseVersion);
+        System.out.format("using `%s` for the release date%n", releaseDate);
+
+        // Populate the changelog entry files in the release directory.
+        final Path releaseDirectory = releaseDirectory(args.changelogDirectory, args.releaseVersion);
+        populateChangelogEntryFiles(args.changelogDirectory, releaseVersionMajor, releaseDirectory);
+
+        // Write the release information.
+        populateReleaseXmlFiles(releaseDate, args.releaseVersion, releaseDirectory);
+
+        // Write the release introduction.
+        populateReleaseIntroAsciiDocFile(releaseDirectory);
+
+    }
+
+    private static void populateChangelogEntryFiles(
+            final Path changelogDirectory,
+            int releaseVersionMajor,
+            final Path releaseDirectory)
+            throws IOException {
+        final Path unreleasedDirectory = ChangelogFiles.unreleasedDirectory(changelogDirectory, releaseVersionMajor);
+        if (Files.exists(releaseDirectory)) {
+            System.out.format(
+                    "release directory `%s` already exists, only moving the changelog entry files from `%s`%n",
+                    releaseDirectory, unreleasedDirectory);
+            moveUnreleasedChangelogEntryFiles(unreleasedDirectory, releaseDirectory);
+        } else {
+            System.out.format(
+                    "release directory `%s` doesn't exist, simply renaming the unreleased directory `%s`%n",
+                    releaseDirectory, unreleasedDirectory);
+            moveUnreleasedDirectory(unreleasedDirectory, releaseDirectory);
+        }
+    }
+
+    private static void moveUnreleasedChangelogEntryFiles(final Path unreleasedDirectory, final Path releaseDirectory) {
+        FileUtils
+                .findAdjacentFiles(unreleasedDirectory)
+                .forEach(unreleasedChangelogEntryFile -> {
+                    final String fileName = unreleasedChangelogEntryFile.getFileName().toString();
+                    final Path releasedChangelogEntryFile = releaseDirectory.resolve(fileName);
+                    System.out.format(
+                            "moving changelog entry file `%s` to `%s`%n",
+                            unreleasedChangelogEntryFile, releasedChangelogEntryFile);
+                    try {
+                        Files.move(unreleasedChangelogEntryFile, releasedChangelogEntryFile);
+                    } catch (final IOException error) {
+                        final String message = String.format(
+                                "failed to move `%s` to `%s`",
+                                unreleasedChangelogEntryFile,
+                                releasedChangelogEntryFile);
+                        throw new UncheckedIOException(message, error);
+                    }
+                });
+    }
+
+    private static void moveUnreleasedDirectory(
+            final Path unreleasedDirectory,
+            final Path releaseDirectory)
+            throws IOException {
+        if (!Files.exists(unreleasedDirectory)) {
+            final String message = String.format(
+                    "`%s` does not exist! A release without any changelogs don't make sense!",
+                    unreleasedDirectory);
+            throw new IllegalStateException(message);
+        }
+        System.out.format("moving changelog directory `%s` to `%s`%n", unreleasedDirectory, releaseDirectory);
+        Files.move(unreleasedDirectory, releaseDirectory);
+        Files.createDirectories(unreleasedDirectory);
+    }
+
+    private static void populateReleaseXmlFiles(
+            final String releaseDate,
+            final String releaseVersion,
+            final Path releaseDirectory)
+            throws IOException {
+        final Path releaseXmlFile = ChangelogFiles.releaseXmlFile(releaseDirectory);
+        System.out.format("writing release information to `%s`%n", releaseXmlFile);
+        final ChangelogRelease changelogRelease = new ChangelogRelease(releaseVersion, releaseDate);
+        Files.deleteIfExists(releaseXmlFile);
+        changelogRelease.writeToXmlFile(releaseXmlFile);
+    }
+
+    private static void populateReleaseIntroAsciiDocFile(final Path releaseDirectory) throws IOException {
+        final Path introAsciiDocFile = ChangelogFiles.introAsciiDocFile(releaseDirectory);
+        if (Files.exists(introAsciiDocFile)) {
+            System.out.format("keeping the existing intro file: `%s`%n", introAsciiDocFile);
+        } else {
+            Files.write(introAsciiDocFile, AsciiDocUtils.LICENSE_COMMENT_BLOCK.getBytes(StandardCharsets.UTF_8));
+            System.out.format("created the intro file: `%s`%n", introAsciiDocFile);
+        }
+    }
+
+}
diff --git a/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/releaser/ChangelogReleaserArgs.java b/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/releaser/ChangelogReleaserArgs.java
new file mode 100644
index 0000000..f1ea212
--- /dev/null
+++ b/log4j-changelog/src/main/java/org/apache/logging/log4j/changelog/releaser/ChangelogReleaserArgs.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.changelog.releaser;
+
+import java.nio.file.Path;
+
+import static org.apache.logging.log4j.PropertyUtils.requireNonBlankPathProperty;
+import static org.apache.logging.log4j.PropertyUtils.requireNonBlankStringProperty;
+import static org.apache.logging.log4j.VersionUtils.requireSemanticVersioning;
+
+final class ChangelogReleaserArgs {
+
+    final Path changelogDirectory;
+
+    final String releaseVersion;
+
+    private ChangelogReleaserArgs(final Path changelogDirectory, final String releaseVersion) {
+        this.changelogDirectory = changelogDirectory;
+        this.releaseVersion = releaseVersion;
+    }
+
+    static ChangelogReleaserArgs fromSystemProperties() {
+        final Path changelogDirectory = requireNonBlankPathProperty("log4j.changelog.directory");
+        final String releaseVersionProperty = "log4j.changelog.releaseVersion";
+        final String releaseVersion = requireNonBlankStringProperty(releaseVersionProperty);
+        requireSemanticVersioning(releaseVersion, releaseVersionProperty);
+        return new ChangelogReleaserArgs(changelogDirectory, releaseVersion);
+    }
+
+}
diff --git a/pom.xml b/pom.xml
index d66a1ed..7f6b61c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -57,9 +57,6 @@
     <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
     <java.version>1.8</java.version>
 
-    <!-- library versions -->
-    <errorprone.version>2.16</errorprone.version>
-
     <!-- plugin versions -->
     <flatten-maven-plugin.version>1.3.0</flatten-maven-plugin.version>
 
@@ -79,16 +76,7 @@
           <fork>true</fork>
           <compilerArgs>
             <arg>-Xlint:all</arg>
-            <arg>-XDcompilePolicy=simple</arg>
-            <arg>-Xplugin:ErrorProne</arg>
           </compilerArgs>
-          <annotationProcessorPaths>
-            <path>
-              <groupId>com.google.errorprone</groupId>
-              <artifactId>error_prone_core</artifactId>
-              <version>${errorprone.version}</version>
-            </path>
-          </annotationProcessorPaths>
         </configuration>
       </plugin>