Files
idf-ci/.gitea/workflows/release-plugin.yml
T
claude 6aaad24917 refactor(release-plugin): readme.txt als Changelog-SSOT
- Pre-Flight prüft jetzt zusätzlich Stable tag: in readme.txt
- Changelog-Block wird aus readme.txt == Changelog == gezogen
- CHANGELOG.md fällt als Workflow-Abhängigkeit weg
- readme.txt ist Pflichtdatei im Plugin-Repo

Grund: readme.txt ist WP-natives Format, WordPress und Master-Key
parsen es direkt (für CPT-Sync der öffentlichen Plugin-Seite).
Damit gibt es eine SSOT zum Pflegen statt parallele CHANGELOG.md.

Entscheidung dokumentiert in Notion:
https://www.notion.so/32b36ebfe9178147ad16ee214302c994
2026-04-24 11:56:41 +00:00

274 lines
10 KiB
YAML

name: Release Plugin
# Reusable Workflow für WordPress-Plugin-Releases.
# Aufruf aus einem Plugin-Repo via:
#
# jobs:
# release:
# uses: ideenfabrik/idf-ci/.gitea/workflows/release-plugin.yml@v1
# with:
# slug: idf-<plugin-slug>
# secrets: inherit
#
# 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 =
# * ...
#
# Trigger im aufrufenden Repo: push eines Tags der Form v<MAJOR>.<MINOR>.<PATCH>.
on:
workflow_call:
inputs:
slug:
description: "Plugin-Slug (Top-Level-Ordner im ZIP, Dateiname-Basis)"
required: true
type: string
main_file:
description: "Plugin-Haupt-PHP (default: <slug>.php)"
required: false
type: string
default: ""
version_constant:
description: "Name der PHP-Versions-Konstante (default: IDF_<UPPER_SLUG_OHNE_IDF_>_VERSION)"
required: false
type: string
default: ""
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout Tag
uses: actions/checkout@v4
- name: Variablen ableiten
id: vars
shell: bash
run: |
set -euo pipefail
SLUG="${{ inputs.slug }}"
TAG="${GITHUB_REF_NAME}"
VERSION="${TAG#v}"
MAIN_FILE="${{ inputs.main_file }}"
[[ -z "$MAIN_FILE" ]] && MAIN_FILE="${SLUG}.php"
CONST="${{ inputs.version_constant }}"
if [[ -z "$CONST" ]]; then
base="${SLUG#idf-}"
base_upper="$(echo "$base" | tr '[:lower:]-' '[:upper:]_')"
CONST="IDF_${base_upper}_VERSION"
fi
ZIPNAME="${SLUG}_v${VERSION}.zip"
{
echo "slug=$SLUG"
echo "tag=$TAG"
echo "version=$VERSION"
echo "main_file=$MAIN_FILE"
echo "const=$CONST"
echo "zipname=$ZIPNAME"
} >> "$GITHUB_OUTPUT"
echo "Ermittelt: slug=$SLUG tag=$TAG version=$VERSION main_file=$MAIN_FILE const=$CONST"
- name: Pre-Flight — Tag-Format
shell: bash
run: |
set -euo pipefail
TAG="${{ steps.vars.outputs.tag }}"
if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::Tag '$TAG' entspricht nicht vMAJOR.MINOR.PATCH"
exit 1
fi
echo "Tag-Format OK: $TAG"
- name: Pre-Flight — readme.txt vorhanden
shell: bash
run: |
set -euo pipefail
if [[ ! -f readme.txt ]]; then
echo "::error::readme.txt fehlt im Repo-Root (Pflichtdatei, siehe idf-ci README)"
exit 1
fi
echo "readme.txt OK"
- name: Pre-Flight — Plugin-Header-Version
shell: bash
run: |
set -euo pipefail
MAIN="${{ steps.vars.outputs.main_file }}"
VERSION="${{ steps.vars.outputs.version }}"
if [[ ! -f "$MAIN" ]]; then
echo "::error::Plugin-Haupt-PHP nicht gefunden: $MAIN"
exit 1
fi
HEADER_VERSION="$(grep -iE '^[[:space:]]*\*?[[:space:]]*Version:[[:space:]]*' "$MAIN" | head -n1 | sed -E 's/^[^0-9]*([0-9]+\.[0-9]+\.[0-9]+).*$/\1/')"
if [[ -z "$HEADER_VERSION" ]]; then
echo "::error::Konnte 'Version: …' Zeile in $MAIN nicht finden"
exit 1
fi
if [[ "$HEADER_VERSION" != "$VERSION" ]]; then
echo "::error::Plugin-Header-Version '$HEADER_VERSION' != Tag '$VERSION'"
exit 1
fi
echo "Header-Version OK: $HEADER_VERSION"
- name: Pre-Flight — Versions-Konstante
shell: bash
run: |
set -euo pipefail
CONST="${{ steps.vars.outputs.const }}"
VERSION="${{ steps.vars.outputs.version }}"
CONST_LINE="$(grep -rhE "define[[:space:]]*\([[:space:]]*['\"]${CONST}['\"]" --include='*.php' . | head -n1 || true)"
if [[ -z "$CONST_LINE" ]]; then
echo "::error::Versions-Konstante $CONST nicht in PHP-Dateien gefunden"
exit 1
fi
CONST_VERSION="$(echo "$CONST_LINE" | sed -E "s/.*['\"]([0-9]+\.[0-9]+\.[0-9]+)['\"].*/\1/")"
if [[ "$CONST_VERSION" != "$VERSION" ]]; then
echo "::error::Konstante $CONST='$CONST_VERSION' != Tag '$VERSION'"
exit 1
fi
echo "Konstante OK: $CONST=$CONST_VERSION"
- name: Pre-Flight — readme.txt Stable tag
shell: bash
run: |
set -euo pipefail
VERSION="${{ steps.vars.outputs.version }}"
STABLE_TAG="$(grep -iE '^[[:space:]]*Stable tag:[[:space:]]*' readme.txt | head -n1 | sed -E 's/^[^0-9]*([0-9]+\.[0-9]+\.[0-9]+).*$/\1/')"
if [[ -z "$STABLE_TAG" ]]; then
echo "::error::Konnte 'Stable tag: …' Zeile in readme.txt nicht finden"
exit 1
fi
if [[ "$STABLE_TAG" != "$VERSION" ]]; then
echo "::error::readme.txt Stable tag '$STABLE_TAG' != Tag '$VERSION'"
exit 1
fi
echo "Stable tag OK: $STABLE_TAG"
- name: Changelog-Block aus readme.txt extrahieren
id: changelog
shell: bash
env:
VERSION: ${{ steps.vars.outputs.version }}
run: |
set -euo pipefail
BLOCK="$(python3 - <<'PY'
import re, sys, os
version = os.environ["VERSION"]
with open("readme.txt", 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)",
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")
sys.exit(1)
print(block_m.group(1).strip())
PY
)" || { echo "::error::Kein Changelog-Block für $VERSION in readme.txt (Abschnitt '== Changelog ==')"; exit 1; }
{
echo "body<<CHANGELOG_EOF"
echo "$BLOCK"
echo "CHANGELOG_EOF"
} >> "$GITHUB_OUTPUT"
echo "Changelog-Block extrahiert (${#BLOCK} Zeichen)."
- name: ZIP bauen
id: zip
shell: bash
run: |
set -euo pipefail
SLUG="${{ steps.vars.outputs.slug }}"
ZIPNAME="${{ steps.vars.outputs.zipname }}"
WORK="/tmp/build"
rm -rf "$WORK"
mkdir -p "$WORK/$SLUG"
rsync -a \
--exclude='.git' \
--exclude='.gitea' \
--exclude='.github' \
--exclude='.gitignore' \
--exclude='.gitattributes' \
--exclude='.editorconfig' \
--exclude='.vscode' \
--exclude='.idea' \
--exclude='.DS_Store' \
--exclude='node_modules' \
--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
(cd "$WORK" && zip -qr "/tmp/$ZIPNAME" "$SLUG")
ls -la "/tmp/$ZIPNAME"
# Validierung: Top-Level im ZIP ist genau $SLUG/
TOP_DIRS="$(unzip -l "/tmp/$ZIPNAME" | awk 'NR>3 && NF>=4 {print $4}' | grep -E '^[^/]+/' | awk -F/ '{print $1}' | sort -u)"
if [[ "$TOP_DIRS" != "$SLUG" ]]; then
echo "::error::Unerwartete Top-Level-Ordner im ZIP: '$TOP_DIRS' (erwartet: '$SLUG')"
exit 1
fi
echo "ZIP-Top-Level OK: $SLUG/"
echo "zippath=/tmp/$ZIPNAME" >> "$GITHUB_OUTPUT"
- name: Gitea-Release anlegen und Asset anhängen
shell: bash
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN || github.token }}
TAG: ${{ steps.vars.outputs.tag }}
ZIPPATH: ${{ steps.zip.outputs.zippath }}
ZIPNAME: ${{ steps.vars.outputs.zipname }}
BODY: ${{ steps.changelog.outputs.body }}
run: |
set -euo pipefail
SERVER="${GITHUB_SERVER_URL:-https://git.ihre-ideenfabrik.de}"
REPO="${GITHUB_REPOSITORY}"
PAYLOAD="$(jq -n \
--arg tag "$TAG" \
--arg name "$TAG" \
--arg body "$BODY" \
'{tag_name:$tag, name:$name, body:$body, draft:false, prerelease:false}')"
RESP="$(curl -sSf \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-X POST \
-d "$PAYLOAD" \
"$SERVER/api/v1/repos/$REPO/releases")"
REL_ID="$(echo "$RESP" | jq -r '.id')"
if [[ -z "$REL_ID" || "$REL_ID" == "null" ]]; then
echo "::error::Release-Anlage fehlgeschlagen: $RESP"
exit 1
fi
echo "Release angelegt: id=$REL_ID"
curl -sSf \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/octet-stream" \
--data-binary "@$ZIPPATH" \
-X POST \
"$SERVER/api/v1/repos/$REPO/releases/$REL_ID/assets?name=$ZIPNAME" \
> /dev/null
echo "Asset hochgeladen: $ZIPNAME"