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:
@@ -13,11 +13,13 @@ name: Release Plugin
|
||||
# Erwartete Struktur im aufrufenden Repo:
|
||||
# - <slug>.php (Plugin-Haupt-PHP mit "Version: X.Y.Z" im Header)
|
||||
# - Irgendwo in einer PHP-Datei: define('IDF_<UPPER>_VERSION', 'X.Y.Z')
|
||||
# - readme.txt im Repo-Root mit:
|
||||
# Stable tag: X.Y.Z
|
||||
# == Changelog ==
|
||||
# = X.Y.Z =
|
||||
# * ...
|
||||
# - readme.txt im Repo-Root (WP-Standard-Metadaten inkl. "Stable tag: X.Y.Z")
|
||||
# - CHANGELOG.md im Repo-Root mit Block "## v<X.Y.Z> — <Datum>"
|
||||
#
|
||||
# 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>.
|
||||
|
||||
@@ -85,15 +87,18 @@ jobs:
|
||||
fi
|
||||
echo "Tag-Format OK: $TAG"
|
||||
|
||||
- name: Pre-Flight — readme.txt vorhanden
|
||||
- name: Pre-Flight — Pflichtdateien
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ ! -f readme.txt ]]; then
|
||||
echo "::error::readme.txt fehlt im Repo-Root (Pflichtdatei, siehe idf-ci README)"
|
||||
missing=()
|
||||
[[ -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
|
||||
fi
|
||||
echo "readme.txt OK"
|
||||
echo "Pflichtdateien OK"
|
||||
|
||||
- name: Pre-Flight — Plugin-Header-Version
|
||||
shell: bash
|
||||
@@ -150,7 +155,7 @@ jobs:
|
||||
fi
|
||||
echo "Stable tag OK: $STABLE_TAG"
|
||||
|
||||
- name: Changelog-Block aus readme.txt extrahieren
|
||||
- name: Changelog-Block aus CHANGELOG.md extrahieren
|
||||
id: changelog
|
||||
shell: bash
|
||||
env:
|
||||
@@ -160,30 +165,21 @@ jobs:
|
||||
BLOCK="$(python3 - <<'PY'
|
||||
import re, sys, os
|
||||
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()
|
||||
# 1) "== Changelog ==" Abschnitt finden (bis zum nächsten "==…==" oder Dateiende)
|
||||
section_pat = re.compile(
|
||||
r"^==\s*Changelog\s*==\s*$\n(.*?)(?=^==[^=\n]+==\s*$|\Z)",
|
||||
re.MULTILINE | re.DOTALL | re.IGNORECASE,
|
||||
)
|
||||
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)",
|
||||
# "## v<version>" optional gefolgt von " — Datum" und beliebigem Suffix,
|
||||
# Inhalt bis zum nächsten "## v<x.y.z>" oder einer "---"-Trennzeile oder EOF.
|
||||
pat = re.compile(
|
||||
r"^##\s+v" + re.escape(version) + r"(?:\s+.*)?$\n(.*?)(?=^##\s+v\d|^---\s*$|\Z)",
|
||||
re.MULTILINE | re.DOTALL,
|
||||
)
|
||||
block_m = block_pat.search(section)
|
||||
if not block_m:
|
||||
sys.stderr.write(f"No block '= {version} =' found in == Changelog == section\n")
|
||||
m = pat.search(content)
|
||||
if not m:
|
||||
sys.stderr.write(f"No block for v{version} found in CHANGELOG.md\n")
|
||||
sys.exit(1)
|
||||
print(block_m.group(1).strip())
|
||||
print(m.group(1).strip())
|
||||
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 "$BLOCK"
|
||||
@@ -191,13 +187,14 @@ jobs:
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
echo "Changelog-Block extrahiert (${#BLOCK} Zeichen)."
|
||||
|
||||
- name: ZIP bauen
|
||||
- name: ZIP bauen (readme.txt Changelog-Section aus CHANGELOG.md generieren)
|
||||
id: zip
|
||||
shell: bash
|
||||
env:
|
||||
SLUG: ${{ steps.vars.outputs.slug }}
|
||||
ZIPNAME: ${{ steps.vars.outputs.zipname }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
SLUG="${{ steps.vars.outputs.slug }}"
|
||||
ZIPNAME="${{ steps.vars.outputs.zipname }}"
|
||||
WORK="/tmp/build"
|
||||
rm -rf "$WORK"
|
||||
mkdir -p "$WORK/$SLUG"
|
||||
@@ -215,14 +212,82 @@ jobs:
|
||||
--exclude='tests' \
|
||||
--exclude='phpunit.xml*' \
|
||||
--exclude='phpcs.xml*' \
|
||||
--exclude='CHANGELOG.md' \
|
||||
--exclude='*.zip' \
|
||||
./ "$WORK/$SLUG/"
|
||||
# readme.txt muss im Build enthalten sein (WP-Standard)
|
||||
if [[ ! -f "$WORK/$SLUG/readme.txt" ]]; then
|
||||
echo "::error::readme.txt fehlt im gebauten Plugin-Ordner"
|
||||
exit 1
|
||||
fi
|
||||
# Pflicht-Artefakte im Build
|
||||
for f in readme.txt CHANGELOG.md; do
|
||||
if [[ ! -f "$WORK/$SLUG/$f" ]]; then
|
||||
echo "::error::$f fehlt im gebauten Plugin-Ordner"
|
||||
exit 1
|
||||
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")
|
||||
ls -la "/tmp/$ZIPNAME"
|
||||
# Validierung: Top-Level im ZIP ist genau $SLUG/
|
||||
|
||||
Reference in New Issue
Block a user