refactor(release-plugin): CHANGELOG.md als SSOT, readme.txt wird auto-gefüllt

Konsistenz mit Software und Flow: CHANGELOG.md ist einziger Pflege-Ort
für den Changelog über alle IDF-Projekte hinweg. readme.txt bleibt als
WP-Standard-Datei erhalten (Plugin-Metadaten, Stable tag), der
Abschnitt == Changelog == wird beim ZIP-Bau aus CHANGELOG.md generiert
und im ZIP mitgeliefert.

Pre-Flight prüft weiter 4 Sync-Stellen inkl. Stable tag.
CHANGELOG.md und readme.txt sind beide Pflichtdateien im Repo.
This commit is contained in:
2026-04-24 12:17:17 +00:00
parent 4f2f9415ce
commit 54effe0fbe
+101 -36
View File
@@ -13,11 +13,13 @@ name: Release Plugin
# Erwartete Struktur im aufrufenden Repo: # Erwartete Struktur im aufrufenden Repo:
# - <slug>.php (Plugin-Haupt-PHP mit "Version: X.Y.Z" im Header) # - <slug>.php (Plugin-Haupt-PHP mit "Version: X.Y.Z" im Header)
# - Irgendwo in einer PHP-Datei: define('IDF_<UPPER>_VERSION', 'X.Y.Z') # - Irgendwo in einer PHP-Datei: define('IDF_<UPPER>_VERSION', 'X.Y.Z')
# - readme.txt im Repo-Root mit: # - readme.txt im Repo-Root (WP-Standard-Metadaten inkl. "Stable tag: X.Y.Z")
# Stable tag: X.Y.Z # - CHANGELOG.md im Repo-Root mit Block "## v<X.Y.Z> — <Datum>"
# == Changelog == #
# = X.Y.Z = # CHANGELOG.md ist SSOT für den Changelog. readme.txt wird nicht
# * ... # manuell mit Changelog-Einträgen gepflegt — der Abschnitt
# "== Changelog ==" in readme.txt wird beim Build aus CHANGELOG.md
# generiert und ist nur im fertigen ZIP vollständig.
# #
# Trigger im aufrufenden Repo: push eines Tags der Form v<MAJOR>.<MINOR>.<PATCH>. # Trigger im aufrufenden Repo: push eines Tags der Form v<MAJOR>.<MINOR>.<PATCH>.
@@ -85,15 +87,18 @@ jobs:
fi fi
echo "Tag-Format OK: $TAG" echo "Tag-Format OK: $TAG"
- name: Pre-Flight — readme.txt vorhanden - name: Pre-Flight — Pflichtdateien
shell: bash shell: bash
run: | run: |
set -euo pipefail set -euo pipefail
if [[ ! -f readme.txt ]]; then missing=()
echo "::error::readme.txt fehlt im Repo-Root (Pflichtdatei, siehe idf-ci README)" [[ -f CHANGELOG.md ]] || missing+=("CHANGELOG.md")
[[ -f readme.txt ]] || missing+=("readme.txt")
if ((${#missing[@]})); then
echo "::error::Pflichtdateien fehlen im Repo-Root: ${missing[*]}"
exit 1 exit 1
fi fi
echo "readme.txt OK" echo "Pflichtdateien OK"
- name: Pre-Flight — Plugin-Header-Version - name: Pre-Flight — Plugin-Header-Version
shell: bash shell: bash
@@ -150,7 +155,7 @@ jobs:
fi fi
echo "Stable tag OK: $STABLE_TAG" echo "Stable tag OK: $STABLE_TAG"
- name: Changelog-Block aus readme.txt extrahieren - name: Changelog-Block aus CHANGELOG.md extrahieren
id: changelog id: changelog
shell: bash shell: bash
env: env:
@@ -160,30 +165,21 @@ jobs:
BLOCK="$(python3 - <<'PY' BLOCK="$(python3 - <<'PY'
import re, sys, os import re, sys, os
version = os.environ["VERSION"] version = os.environ["VERSION"]
with open("readme.txt", encoding="utf-8") as f: with open("CHANGELOG.md", encoding="utf-8") as f:
content = f.read() content = f.read()
# 1) "== Changelog ==" Abschnitt finden (bis zum nächsten "==…==" oder Dateiende) # "## v<version>" optional gefolgt von " — Datum" und beliebigem Suffix,
section_pat = re.compile( # Inhalt bis zum nächsten "## v<x.y.z>" oder einer "---"-Trennzeile oder EOF.
r"^==\s*Changelog\s*==\s*$\n(.*?)(?=^==[^=\n]+==\s*$|\Z)", pat = re.compile(
re.MULTILINE | re.DOTALL | re.IGNORECASE, r"^##\s+v" + re.escape(version) + r"(?:\s+.*)?$\n(.*?)(?=^##\s+v\d|^---\s*$|\Z)",
)
section_m = section_pat.search(content)
if not section_m:
sys.stderr.write("'== Changelog ==' section not found in readme.txt\n")
sys.exit(1)
section = section_m.group(1)
# 2) Block "= <version> =" bis zum nächsten "= x.y.z =" oder Abschnittsende
block_pat = re.compile(
r"^=\s*" + re.escape(version) + r"\s*=\s*$\n(.*?)(?=^=\s*\d+\.\d+\.\d+\s*=\s*$|\Z)",
re.MULTILINE | re.DOTALL, re.MULTILINE | re.DOTALL,
) )
block_m = block_pat.search(section) m = pat.search(content)
if not block_m: if not m:
sys.stderr.write(f"No block '= {version} =' found in == Changelog == section\n") sys.stderr.write(f"No block for v{version} found in CHANGELOG.md\n")
sys.exit(1) sys.exit(1)
print(block_m.group(1).strip()) print(m.group(1).strip())
PY PY
)" || { echo "::error::Kein Changelog-Block für $VERSION in readme.txt (Abschnitt '== Changelog ==')"; exit 1; } )" || { echo "::error::Kein Changelog-Block für v${VERSION} in CHANGELOG.md"; exit 1; }
{ {
echo "body<<CHANGELOG_EOF" echo "body<<CHANGELOG_EOF"
echo "$BLOCK" echo "$BLOCK"
@@ -191,13 +187,14 @@ jobs:
} >> "$GITHUB_OUTPUT" } >> "$GITHUB_OUTPUT"
echo "Changelog-Block extrahiert (${#BLOCK} Zeichen)." echo "Changelog-Block extrahiert (${#BLOCK} Zeichen)."
- name: ZIP bauen - name: ZIP bauen (readme.txt Changelog-Section aus CHANGELOG.md generieren)
id: zip id: zip
shell: bash shell: bash
env:
SLUG: ${{ steps.vars.outputs.slug }}
ZIPNAME: ${{ steps.vars.outputs.zipname }}
run: | run: |
set -euo pipefail set -euo pipefail
SLUG="${{ steps.vars.outputs.slug }}"
ZIPNAME="${{ steps.vars.outputs.zipname }}"
WORK="/tmp/build" WORK="/tmp/build"
rm -rf "$WORK" rm -rf "$WORK"
mkdir -p "$WORK/$SLUG" mkdir -p "$WORK/$SLUG"
@@ -215,14 +212,82 @@ jobs:
--exclude='tests' \ --exclude='tests' \
--exclude='phpunit.xml*' \ --exclude='phpunit.xml*' \
--exclude='phpcs.xml*' \ --exclude='phpcs.xml*' \
--exclude='CHANGELOG.md' \
--exclude='*.zip' \ --exclude='*.zip' \
./ "$WORK/$SLUG/" ./ "$WORK/$SLUG/"
# readme.txt muss im Build enthalten sein (WP-Standard) # Pflicht-Artefakte im Build
if [[ ! -f "$WORK/$SLUG/readme.txt" ]]; then for f in readme.txt CHANGELOG.md; do
echo "::error::readme.txt fehlt im gebauten Plugin-Ordner" if [[ ! -f "$WORK/$SLUG/$f" ]]; then
echo "::error::$f fehlt im gebauten Plugin-Ordner"
exit 1 exit 1
fi fi
done
# == Changelog == Section in readme.txt aus CHANGELOG.md generieren
python3 - "$WORK/$SLUG/readme.txt" "$WORK/$SLUG/CHANGELOG.md" <<'PY'
import re, sys, pathlib
readme_path = pathlib.Path(sys.argv[1])
changelog_path = pathlib.Path(sys.argv[2])
# CHANGELOG.md parsen: Einträge "## v<x.y.z> — <Datum?>" bis zum nächsten "## v…" oder "---" oder EOF
entry_pat = re.compile(
r"^##\s+v(\d+\.\d+\.\d+)(?:\s+—\s+(\S+))?(?:\s+.*)?$\n(.*?)(?=^##\s+v\d|^---\s*$|\Z)",
re.MULTILINE | re.DOTALL,
)
content = changelog_path.read_text(encoding="utf-8")
entries = [(m.group(1), m.group(2), m.group(3).strip()) for m in entry_pat.finditer(content)]
if not entries:
sys.stderr.write("Keine Versions-Einträge in CHANGELOG.md gefunden\n")
sys.exit(1)
# CHANGELOG-Body in WP-readme.txt-Style konvertieren:
# "### Untertitel" → "**Untertitel**"
# "- Text" → "* Text"
# Mehrfach-Leerzeilen → eine Leerzeile
def to_readme(body):
out = []
for raw in body.split("\n"):
line = raw.rstrip()
if line.startswith("### "):
out.append(f"**{line[4:].strip()}**")
elif line.startswith("- "):
out.append(f"* {line[2:]}")
else:
out.append(line)
cleaned, blank = [], False
for line in out:
if line == "":
if not blank:
cleaned.append(line)
blank = True
else:
cleaned.append(line)
blank = False
return "\n".join(cleaned).strip()
section_lines = ["== Changelog =="]
for version, date, body in entries:
section_lines.append("")
header = f"= {version} ="
section_lines.append(header)
if date:
section_lines.append(f"*{date}*")
section_lines.append("")
section_lines.append(to_readme(body))
new_section = "\n".join(section_lines).strip() + "\n"
readme = readme_path.read_text(encoding="utf-8")
# Vorhandene == Changelog == Section ersetzen (bis zum nächsten == Section == oder EOF)
sec_pat = re.compile(
r"^==\s*Changelog\s*==\s*$.*?(?=^==[^=\n]+==\s*$|\Z)",
re.MULTILINE | re.DOTALL | re.IGNORECASE,
)
if sec_pat.search(readme):
readme_new = sec_pat.sub(new_section + "\n", readme)
else:
readme_new = readme.rstrip() + "\n\n" + new_section
readme_path.write_text(readme_new, encoding="utf-8")
print(f"readme.txt Changelog-Section aktualisiert ({len(entries)} Versionen).")
PY
(cd "$WORK" && zip -qr "/tmp/$ZIPNAME" "$SLUG") (cd "$WORK" && zip -qr "/tmp/$ZIPNAME" "$SLUG")
ls -la "/tmp/$ZIPNAME" ls -la "/tmp/$ZIPNAME"
# Validierung: Top-Level im ZIP ist genau $SLUG/ # Validierung: Top-Level im ZIP ist genau $SLUG/