From cd8caf14d0d87b0b8eb00328c35a0988b7a02f0b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 4 Mar 2026 20:57:29 +0000
Subject: [PATCH 1/3] Initial plan
From 3367a05523ba07cf5add59091a81eee696148ae6 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 4 Mar 2026 21:05:30 +0000
Subject: [PATCH 2/3] Add Changelog root element to Bjoern specifications
Co-authored-by: Mehtrick <4493019+Mehtrick@users.noreply.github.com>
---
README.md | 4 ++++
.../BjoernFeatureTestClassBuilder.java | 3 +++
.../mehtrick/bjoern/parser/modell/Bjoern.java | 12 +++++++++-
.../bjoern/parser/modell/BjoernZGRModell.java | 17 ++++++++++++--
.../validator/validations/BjoernKeywords.java | 2 +-
src/main/resources/asciidoc.ftlh | 4 ++++
.../bjoern/asciidoc/AsciiDocBuildTest.java | 22 +++++++++++++++++++
.../BjoernFeatureTestClassBuilderTest.java | 12 ++++++++++
.../bjoern/parser/BjoernValidatorTest.java | 4 ++--
src/test/resources/changelog.zgr | 10 +++++++++
10 files changed, 84 insertions(+), 6 deletions(-)
create mode 100644 src/test/resources/changelog.zgr
diff --git a/README.md b/README.md
index 4c7a550..d870564 100755
--- a/README.md
+++ b/README.md
@@ -96,6 +96,10 @@ The optional `Reference` field links a spec to an external ticket or resource. I
- In generated Java: rendered as a `@see text` Javadoc tag on the abstract class.
- In generated docs: rendered as an AsciiDoc hyperlink (`link:url[text]`) below the feature title.
+The optional `Changelog` field allows you to document changes, background information, or technical rationale for the specification. It accepts arbitrary free text:
+- In generated Java: rendered as a `@Changelog` tag in the Javadoc of the abstract class.
+- In generated docs: inserted after the introduction (version/reference), only when present.
+
## Code generation
Bjoern will then generate the TestClasses based on the spec
diff --git a/src/main/java/de/mehtrick/bjoern/generator/builder/BjoernFeatureTestClassBuilder.java b/src/main/java/de/mehtrick/bjoern/generator/builder/BjoernFeatureTestClassBuilder.java
index 8377006..fc685be 100644
--- a/src/main/java/de/mehtrick/bjoern/generator/builder/BjoernFeatureTestClassBuilder.java
+++ b/src/main/java/de/mehtrick/bjoern/generator/builder/BjoernFeatureTestClassBuilder.java
@@ -48,6 +48,9 @@ private void addJavaDoc(Bjoern bjoern, Builder featureClassBuilder) {
if (StringUtils.isNotBlank(bjoern.getReference())) {
javadoc.append("\n@see ").append(bjoern.getReferenceAsJavadoc());
}
+ if (StringUtils.isNotBlank(bjoern.getChangelog())) {
+ javadoc.append("\n@Changelog ").append(bjoern.getChangelog());
+ }
featureClassBuilder.addJavadoc("$L", javadoc.toString());
}
diff --git a/src/main/java/de/mehtrick/bjoern/parser/modell/Bjoern.java b/src/main/java/de/mehtrick/bjoern/parser/modell/Bjoern.java
index ab80c8d..0fb737a 100644
--- a/src/main/java/de/mehtrick/bjoern/parser/modell/Bjoern.java
+++ b/src/main/java/de/mehtrick/bjoern/parser/modell/Bjoern.java
@@ -15,6 +15,7 @@ public class Bjoern {
private String feature;
private String version;
private String reference;
+ private String changelog;
private BjoernBackground background;
private List scenarios;
private String filePath;
@@ -23,6 +24,7 @@ public Bjoern(BjoernZGRModell yamlModell, String path) {
setFeature(yamlModell.getFeature());
setVersion(yamlModell.getVersion());
setReference(yamlModell.getReference());
+ setChangelog(yamlModell.getChangelog());
setScenarios(yamlModell.getScenarios().stream().map(BjoernScenario::new).collect(Collectors.toList()));
setFilePath(path);
if (yamlModell.getBackground() != null) {
@@ -58,6 +60,14 @@ public void setReference(String reference) {
this.reference = reference;
}
+ public String getChangelog() {
+ return this.changelog;
+ }
+
+ public void setChangelog(String changelog) {
+ this.changelog = changelog;
+ }
+
/**
* Returns the reference formatted as a Javadoc hyperlink if it is a Markdown link ([text](url))
* with an allowed URL scheme (http/https), otherwise returns the plain reference text.
@@ -136,6 +146,6 @@ public void setFilePath(String filePath) {
public String toString() {
- return "Bjoern(feature=" + this.getFeature() + ", version=" + this.getVersion() + ", reference=" + this.getReference() + ", background=" + this.getBackground() + ", scenarios=" + this.getScenarios() + ", filePath=" + this.getFilePath() + ")";
+ return "Bjoern(feature=" + this.getFeature() + ", version=" + this.getVersion() + ", reference=" + this.getReference() + ", changelog=" + this.getChangelog() + ", background=" + this.getBackground() + ", scenarios=" + this.getScenarios() + ", filePath=" + this.getFilePath() + ")";
}
}
diff --git a/src/main/java/de/mehtrick/bjoern/parser/modell/BjoernZGRModell.java b/src/main/java/de/mehtrick/bjoern/parser/modell/BjoernZGRModell.java
index 539406e..725cce6 100644
--- a/src/main/java/de/mehtrick/bjoern/parser/modell/BjoernZGRModell.java
+++ b/src/main/java/de/mehtrick/bjoern/parser/modell/BjoernZGRModell.java
@@ -15,7 +15,7 @@
*
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
-@JsonPropertyOrder({ "Feature", "Version", "Reference", "Scenarios" })
+@JsonPropertyOrder({ "Feature", "Version", "Reference", "Changelog", "Scenarios" })
public class BjoernZGRModell implements Serializable {
@JsonProperty("Feature")
@@ -27,6 +27,9 @@ public class BjoernZGRModell implements Serializable {
@JsonProperty("Reference")
private String reference;
+ @JsonProperty("Changelog")
+ private String changelog;
+
@JsonProperty("Background")
private BjoernZGRBackground background;
@@ -64,6 +67,16 @@ public void setReference(String reference) {
this.reference = reference;
}
+ @JsonProperty("Changelog")
+ public String getChangelog() {
+ return changelog;
+ }
+
+ @JsonProperty("Changelog")
+ public void setChangelog(String changelog) {
+ this.changelog = changelog;
+ }
+
@JsonProperty("Scenarios")
public List getScenarios() {
return bjoernZGRScenarios;
@@ -85,6 +98,6 @@ public void setBackground(BjoernZGRBackground background) {
}
public String toString() {
- return "BjoernZGRModell(feature=" + this.getFeature() + ", version=" + this.getVersion() + ", reference=" + this.getReference() + ", background=" + this.getBackground() + ", bjoernZGRScenarios=" + this.bjoernZGRScenarios + ")";
+ return "BjoernZGRModell(feature=" + this.getFeature() + ", version=" + this.getVersion() + ", reference=" + this.getReference() + ", changelog=" + this.getChangelog() + ", background=" + this.getBackground() + ", bjoernZGRScenarios=" + this.bjoernZGRScenarios + ")";
}
}
diff --git a/src/main/java/de/mehtrick/bjoern/parser/validator/validations/BjoernKeywords.java b/src/main/java/de/mehtrick/bjoern/parser/validator/validations/BjoernKeywords.java
index b28488d..41b90b2 100644
--- a/src/main/java/de/mehtrick/bjoern/parser/validator/validations/BjoernKeywords.java
+++ b/src/main/java/de/mehtrick/bjoern/parser/validator/validations/BjoernKeywords.java
@@ -6,7 +6,7 @@
public enum BjoernKeywords {
- GIVEN("Given:"), WHEN("When:"), THEN("Then:"), BACKGROUND("Background:"), FEATURE("Feature:"), VERSION("Version:"), REFERENCE("Reference:"), SCENARIO("- Scenario:"), SCENARIOS("Scenarios:"), STATEMENT("-");
+ GIVEN("Given:"), WHEN("When:"), THEN("Then:"), BACKGROUND("Background:"), FEATURE("Feature:"), VERSION("Version:"), REFERENCE("Reference:"), CHANGELOG("Changelog:"), SCENARIO("- Scenario:"), SCENARIOS("Scenarios:"), STATEMENT("-");
public String keyword;
diff --git a/src/main/resources/asciidoc.ftlh b/src/main/resources/asciidoc.ftlh
index c4caacc..4c7930a 100644
--- a/src/main/resources/asciidoc.ftlh
+++ b/src/main/resources/asciidoc.ftlh
@@ -8,6 +8,10 @@ Version: ${version}
<#if referenceAsAsciidoc?? && referenceAsAsciidoc?has_content>
Reference: ${referenceAsAsciidoc}
+#if>
+<#if changelog?? && changelog?has_content>
+Changelog: ${changelog}
+
#if>
<#if gitHistory?? && gitHistory?has_content>
diff --git a/src/test/java/de/mehtrick/bjoern/asciidoc/AsciiDocBuildTest.java b/src/test/java/de/mehtrick/bjoern/asciidoc/AsciiDocBuildTest.java
index 6ffd386..b802df7 100644
--- a/src/test/java/de/mehtrick/bjoern/asciidoc/AsciiDocBuildTest.java
+++ b/src/test/java/de/mehtrick/bjoern/asciidoc/AsciiDocBuildTest.java
@@ -53,6 +53,28 @@ public void testDocGenerationWithReference() throws IOException, BjoernMissingPr
assertThat(content).contains("Reference: link:https://example.com/TICKET-123[TICKET-123]");
}
+ @Test
+ @DisplayName("Test Doc Generation with Changelog")
+ public void testDocGenerationWithChangelog() throws IOException, BjoernMissingPropertyException, NotSupportedJunitVersionException {
+ BjoernDocApplication.main(new String[]{"path=src/test/resources/changelog.zgr", "docdir=src/gen/resources"});
+ File generatedFile = new File("src/gen/resources/changelog.adoc");
+ assertThat(generatedFile).exists();
+ String content = new String(Files.readAllBytes(generatedFile.toPath()), StandardCharsets.UTF_8);
+ assertThat(content).contains("= Test mit Changelog");
+ assertThat(content).contains("Changelog: This is a changelog entry describing what changed in this spec.");
+ }
+
+ @Test
+ @DisplayName("Test Doc Generation without Changelog does not include changelog section")
+ public void testDocGenerationWithoutChangelog() throws IOException, BjoernMissingPropertyException, NotSupportedJunitVersionException {
+ BjoernDocApplication.main(new String[]{"path=src/test/resources/version.zgr", "docdir=src/gen/resources"});
+ File generatedFile = new File("src/gen/resources/version.adoc");
+ assertThat(generatedFile).exists();
+ String content = new String(Files.readAllBytes(generatedFile.toPath()), StandardCharsets.UTF_8);
+ assertThat(content).contains("= Test mit Version");
+ assertThat(content).doesNotContain("Changelog:");
+ }
+
@Test
@DisplayName("Test Generation of Empty Given")
public void testGenerationOfEmptyGiven() throws BjoernMissingPropertyException, FileNotFoundException, NotSupportedJunitVersionException {
diff --git a/src/test/java/de/mehtrick/bjoern/generator/builder/BjoernFeatureTestClassBuilderTest.java b/src/test/java/de/mehtrick/bjoern/generator/builder/BjoernFeatureTestClassBuilderTest.java
index ac9e398..3a7bb7c 100644
--- a/src/test/java/de/mehtrick/bjoern/generator/builder/BjoernFeatureTestClassBuilderTest.java
+++ b/src/test/java/de/mehtrick/bjoern/generator/builder/BjoernFeatureTestClassBuilderTest.java
@@ -93,4 +93,16 @@ void testJavadocWithVersion() {
Assertions.assertThat(mappedFeature.javadoc.toString()).contains("1.0.0");
}
+ @Test
+ void testJavadocWithChangelog() {
+ //given
+ bjoern = getBjoern("src/test/resources/changelog.zgr");
+ //when
+ TypeSpec mappedFeature = new BjoernFeatureTestClassBuilder(bjoernCodeGeneratorConfig).build(bjoern).getFeatureClass();
+ //then
+ Assertions.assertThat(mappedFeature.javadoc.toString()).contains("Test mit Changelog");
+ Assertions.assertThat(mappedFeature.javadoc.toString()).contains("@Changelog");
+ Assertions.assertThat(mappedFeature.javadoc.toString()).contains("This is a changelog entry describing what changed in this spec.");
+ }
+
}
diff --git a/src/test/java/de/mehtrick/bjoern/parser/BjoernValidatorTest.java b/src/test/java/de/mehtrick/bjoern/parser/BjoernValidatorTest.java
index 61184c0..f4b9c54 100644
--- a/src/test/java/de/mehtrick/bjoern/parser/BjoernValidatorTest.java
+++ b/src/test/java/de/mehtrick/bjoern/parser/BjoernValidatorTest.java
@@ -21,8 +21,8 @@ public void testInvalidKeyword() {
Assertions.assertThatExceptionOfType(BjoernValidatorException.class).isThrownBy(() -> {
bjoernValidator.validate(" WrongKeyword \r\n \r\n anotherWrongOne", "defaultpath");
}
- ).withMessageContaining("ValidationError at line 1: The line starts with an invalid Keyword. Found \" WrongKeyword \". Allowed Keywords are: Given:,When:,Then:,Background:,Feature:,Version:,Reference:,- Scenario:,Scenarios:,-. This check is case-sensitive!")
- .withMessageContaining("ValidationError at line 3: The line starts with an invalid Keyword. Found \" anotherWrongOne\". Allowed Keywords are: Given:,When:,Then:,Background:,Feature:,Version:,Reference:,- Scenario:,Scenarios:,-. This check is case-sensitive!");
+ ).withMessageContaining("ValidationError at line 1: The line starts with an invalid Keyword. Found \" WrongKeyword \". Allowed Keywords are: Given:,When:,Then:,Background:,Feature:,Version:,Reference:,Changelog:,- Scenario:,Scenarios:,-. This check is case-sensitive!")
+ .withMessageContaining("ValidationError at line 3: The line starts with an invalid Keyword. Found \" anotherWrongOne\". Allowed Keywords are: Given:,When:,Then:,Background:,Feature:,Version:,Reference:,Changelog:,- Scenario:,Scenarios:,-. This check is case-sensitive!");
}
@Test
diff --git a/src/test/resources/changelog.zgr b/src/test/resources/changelog.zgr
new file mode 100644
index 0000000..cb9ed9c
--- /dev/null
+++ b/src/test/resources/changelog.zgr
@@ -0,0 +1,10 @@
+Feature: Test mit Changelog
+Changelog: "This is a changelog entry describing what changed in this spec."
+Scenarios:
+ - Scenario: Einfaches Szenario
+ Given:
+ - Ein Benutzer
+ When:
+ - Benutzer tut etwas
+ Then:
+ - Ergebnis ist korrekt
From c3646ab51ae2684b437014c04bd08d0580d0cb76 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 4 Mar 2026 21:13:06 +0000
Subject: [PATCH 3/3] Sanitize changelog for AsciiDoc and support YAML
multiline blocks
Co-authored-by: Mehtrick <4493019+Mehtrick@users.noreply.github.com>
---
.../mehtrick/bjoern/parser/modell/Bjoern.java | 11 ++++++
.../validations/InvalidKeywordValidation.java | 29 +++++++++++++++
src/main/resources/asciidoc.ftlh | 4 +--
.../bjoern/asciidoc/AsciiDocBuildTest.java | 3 +-
.../BjoernFeatureTestClassBuilderTest.java | 3 +-
.../bjoern/parser/BjoernValidatorTest.java | 36 +++++++++++++++++++
src/test/resources/changelog.zgr | 4 ++-
7 files changed, 85 insertions(+), 5 deletions(-)
diff --git a/src/main/java/de/mehtrick/bjoern/parser/modell/Bjoern.java b/src/main/java/de/mehtrick/bjoern/parser/modell/Bjoern.java
index 0fb737a..46fe449 100644
--- a/src/main/java/de/mehtrick/bjoern/parser/modell/Bjoern.java
+++ b/src/main/java/de/mehtrick/bjoern/parser/modell/Bjoern.java
@@ -1,6 +1,7 @@
package de.mehtrick.bjoern.parser.modell;
import de.mehtrick.bjoern.parser.BjoernTextParser;
+import de.mehtrick.bjoern.parser.replacer.AsciidocReplacer;
import org.apache.commons.text.StringEscapeUtils;
import java.util.List;
@@ -68,6 +69,16 @@ public void setChangelog(String changelog) {
this.changelog = changelog;
}
+ /**
+ * Returns the changelog with AsciiDoc-specific characters escaped (e.g. pipe characters).
+ */
+ public String getChangelogAsAsciidoc() {
+ if (changelog == null) {
+ return null;
+ }
+ return AsciidocReplacer.replace(changelog);
+ }
+
/**
* Returns the reference formatted as a Javadoc hyperlink if it is a Markdown link ([text](url))
* with an allowed URL scheme (http/https), otherwise returns the plain reference text.
diff --git a/src/main/java/de/mehtrick/bjoern/parser/validator/validations/InvalidKeywordValidation.java b/src/main/java/de/mehtrick/bjoern/parser/validator/validations/InvalidKeywordValidation.java
index db7bdcf..2013a70 100644
--- a/src/main/java/de/mehtrick/bjoern/parser/validator/validations/InvalidKeywordValidation.java
+++ b/src/main/java/de/mehtrick/bjoern/parser/validator/validations/InvalidKeywordValidation.java
@@ -9,10 +9,39 @@ public InvalidKeywordValidation(String errorText) {
@Override
public void validate(String[] lines, int index) throws BjoernValidationsException {
+ if (isInsideChangelogBlock(lines, index)) {
+ return;
+ }
String trimmedLine = getTrimmedLine(lines, index);
boolean lineDoesNotStartWithKnownKeyword = BjoernKeywords.getKeywordValues().stream().noneMatch(trimmedLine::startsWith);
if (lineDoesNotStartWithKnownKeyword) {
throw new BjoernValidationsException(index, errorText, lines[index], BjoernKeywords.getKeywordsAsSingleString());
}
}
+
+ /**
+ * Returns true when the current line is an indented continuation of a YAML block scalar Changelog value
+ * (i.e. the Changelog key was followed by a block scalar indicator such as | or >).
+ */
+ private boolean isInsideChangelogBlock(String[] lines, int index) {
+ if (lines[index].isEmpty() || !Character.isWhitespace(lines[index].charAt(0))) {
+ return false;
+ }
+ for (int i = index - 1; i >= 0; i--) {
+ if (lines[i].trim().isEmpty()) {
+ continue;
+ }
+ if (!Character.isWhitespace(lines[i].charAt(0))) {
+ // Found the last root-level non-empty line
+ int colonIndex = lines[i].indexOf(':');
+ if (colonIndex < 0) {
+ return false;
+ }
+ String afterKey = lines[i].substring(colonIndex + 1).trim();
+ return lines[i].trim().startsWith(BjoernKeywords.CHANGELOG.keyword)
+ && (afterKey.startsWith("|") || afterKey.startsWith(">"));
+ }
+ }
+ return false;
+ }
}
diff --git a/src/main/resources/asciidoc.ftlh b/src/main/resources/asciidoc.ftlh
index 4c7930a..ad9ecef 100644
--- a/src/main/resources/asciidoc.ftlh
+++ b/src/main/resources/asciidoc.ftlh
@@ -9,8 +9,8 @@ Version: ${version}
Reference: ${referenceAsAsciidoc}
#if>
-<#if changelog?? && changelog?has_content>
-Changelog: ${changelog}
+<#if changelogAsAsciidoc?? && changelogAsAsciidoc?has_content>
+Changelog: ${changelogAsAsciidoc}
#if>
diff --git a/src/test/java/de/mehtrick/bjoern/asciidoc/AsciiDocBuildTest.java b/src/test/java/de/mehtrick/bjoern/asciidoc/AsciiDocBuildTest.java
index b802df7..31ac13f 100644
--- a/src/test/java/de/mehtrick/bjoern/asciidoc/AsciiDocBuildTest.java
+++ b/src/test/java/de/mehtrick/bjoern/asciidoc/AsciiDocBuildTest.java
@@ -61,7 +61,8 @@ public void testDocGenerationWithChangelog() throws IOException, BjoernMissingPr
assertThat(generatedFile).exists();
String content = new String(Files.readAllBytes(generatedFile.toPath()), StandardCharsets.UTF_8);
assertThat(content).contains("= Test mit Changelog");
- assertThat(content).contains("Changelog: This is a changelog entry describing what changed in this spec.");
+ assertThat(content).contains("First line of the changelog.");
+ assertThat(content).contains("Second line with a pipe \\| character.");
}
@Test
diff --git a/src/test/java/de/mehtrick/bjoern/generator/builder/BjoernFeatureTestClassBuilderTest.java b/src/test/java/de/mehtrick/bjoern/generator/builder/BjoernFeatureTestClassBuilderTest.java
index 3a7bb7c..ae5cf82 100644
--- a/src/test/java/de/mehtrick/bjoern/generator/builder/BjoernFeatureTestClassBuilderTest.java
+++ b/src/test/java/de/mehtrick/bjoern/generator/builder/BjoernFeatureTestClassBuilderTest.java
@@ -102,7 +102,8 @@ void testJavadocWithChangelog() {
//then
Assertions.assertThat(mappedFeature.javadoc.toString()).contains("Test mit Changelog");
Assertions.assertThat(mappedFeature.javadoc.toString()).contains("@Changelog");
- Assertions.assertThat(mappedFeature.javadoc.toString()).contains("This is a changelog entry describing what changed in this spec.");
+ Assertions.assertThat(mappedFeature.javadoc.toString()).contains("First line of the changelog.");
+ Assertions.assertThat(mappedFeature.javadoc.toString()).contains("Second line with a pipe | character.");
}
}
diff --git a/src/test/java/de/mehtrick/bjoern/parser/BjoernValidatorTest.java b/src/test/java/de/mehtrick/bjoern/parser/BjoernValidatorTest.java
index f4b9c54..ac51bee 100644
--- a/src/test/java/de/mehtrick/bjoern/parser/BjoernValidatorTest.java
+++ b/src/test/java/de/mehtrick/bjoern/parser/BjoernValidatorTest.java
@@ -159,4 +159,40 @@ public void correctsmaple() {
//THEN
//No Error
}
+
+ @Test
+ public void changelogMultilineLiteralBlock() {
+ String zgr = "Feature: Feature\r\n" +
+ "Changelog: |\r\n" +
+ " First line of changelog.\r\n" +
+ " Second line with | pipe.\r\n" +
+ "Scenarios: \r\n" +
+ " - Scenario: Scenario \r\n" +
+ " Given: \r\n" +
+ " - Ein Benutzer";
+
+ //WHEN
+ bjoernValidator.validate(zgr, "default");
+
+ //THEN
+ //No Error - multiline Changelog block scalar continuation lines must not trigger InvalidKeywordValidation
+ }
+
+ @Test
+ public void changelogMultilineFoldedBlock() {
+ String zgr = "Feature: Feature\r\n" +
+ "Changelog: >\r\n" +
+ " First line of changelog.\r\n" +
+ " Second line.\r\n" +
+ "Scenarios: \r\n" +
+ " - Scenario: Scenario \r\n" +
+ " Given: \r\n" +
+ " - Ein Benutzer";
+
+ //WHEN
+ bjoernValidator.validate(zgr, "default");
+
+ //THEN
+ //No Error - folded block scalar continuation lines must also be accepted
+ }
}
diff --git a/src/test/resources/changelog.zgr b/src/test/resources/changelog.zgr
index cb9ed9c..15ce8e7 100644
--- a/src/test/resources/changelog.zgr
+++ b/src/test/resources/changelog.zgr
@@ -1,5 +1,7 @@
Feature: Test mit Changelog
-Changelog: "This is a changelog entry describing what changed in this spec."
+Changelog: |
+ First line of the changelog.
+ Second line with a pipe | character.
Scenarios:
- Scenario: Einfaches Szenario
Given: