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:
|
# 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/
|
||||||
|
|||||||
Reference in New Issue
Block a user