From 9202ce1c32ef53bec68e8e23947f55cf26035688 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 23 Apr 2026 10:09:17 +0000 Subject: [PATCH] Add reusable release-plugin workflow Pre-Flight-Check (Tag == Plugin-Header-Version == PHP-Konstante), Changelog-Extraktion aus CHANGELOG.md, ZIP-Bau mit Slug als Top-Level-Ordner, Gitea-Release-Anlage inkl. ZIP-Asset. --- .gitea/workflows/release-plugin.yml | 232 ++++++++++++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 .gitea/workflows/release-plugin.yml diff --git a/.gitea/workflows/release-plugin.yml b/.gitea/workflows/release-plugin.yml new file mode 100644 index 0000000..58701b6 --- /dev/null +++ b/.gitea/workflows/release-plugin.yml @@ -0,0 +1,232 @@ +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- +# secrets: inherit +# +# Erwartete Struktur im aufrufenden Repo: +# - .php (Plugin-Haupt-PHP mit "Version: X.Y.Z" im Header) +# - Irgendwo in einer PHP-Datei: define('IDF__VERSION', 'X.Y.Z') +# - CHANGELOG.md mit Block "## v" +# +# Trigger im aufrufenden Repo: push eines Tags der Form v... + +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: .php)" + required: false + type: string + default: "" + version_constant: + description: "Name der PHP-Versions-Konstante (default: 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 — 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: Changelog-Block extrahieren + id: changelog + shell: bash + env: + VERSION: ${{ steps.vars.outputs.version }} + run: | + set -euo pipefail + if [[ ! -f CHANGELOG.md ]]; then + echo "::error::CHANGELOG.md fehlt im Repo" + exit 1 + fi + BLOCK="$(python3 - <<'PY' + import re, sys, os + version = os.environ["VERSION"] + with open("CHANGELOG.md", encoding="utf-8") as f: + content = f.read() + # Match "## v" optionally followed by spaces and any suffix (e.g. date), + # capture until the next "## v" or a "---" separator line or EOF. + pat = re.compile( + r"^##\s+v" + re.escape(version) + r"(?:\s+.*)?$\n(.*?)(?=^##\s+v\d|^---\s*$|\Z)", + re.MULTILINE | re.DOTALL, + ) + 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(m.group(1).strip()) + PY + )" || { echo "::error::Kein Changelog-Block für v${VERSION} in CHANGELOG.md"; exit 1; } + { + echo "body<> "$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='*.zip' \ + ./ "$WORK/$SLUG/" + (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"