Release-Workflow: ZIP bei jedem Merge nach main
Some checks are pending
CI / PHP Lint (pull_request) Waiting to run
CI / PHP Lint-1 (pull_request) Waiting to run
CI / Unit Tests & Static Analysis (pull_request) Waiting to run
CI / PHP Lint (push) Waiting to run
CI / PHP Lint-1 (push) Waiting to run
CI / Unit Tests & Static Analysis (push) Waiting to run

.github/workflows/release.yml baut bei jedem Push auf main (also auch
nach jedem gemergten Pull Request) ein installierbares Paket und hängt es
an das rollende Vorab-Release "latest-main". Der Download-Link bleibt
damit stabil und zeigt immer auf den aktuellen Stand. Ein Tag v* erzeugt
mit derselben Mechanik ein reguläres Release.

Details:
- das ZIP entsteht per `git archive`, die Auswahl steuert .gitattributes
  (export-ignore) – docs/, tests/, tools/ und CI bleiben draußen, das
  Paket ist rund 1,4 MB groß
- eine Prüfschritt kontrolliert, dass Kerndateien wirklich enthalten sind,
  dazu gibt es eine .sha256-Datei
- zusätzlich als Build-Artefakt abgelegt (optional, bricht nicht ab, wenn
  der Artefakt-Speicher fehlt)
- Release-API wird über den automatisch bereitgestellten Token
  angesprochen, FORGEJO_TOKEN dient als Ausweichweg; ohne Token wird der
  Schritt übersprungen statt fehlzuschlagen

Neu ist die Datei VERSION als einzige Quelle der Versionsnummer: sie
benennt das Paket und erscheint über Ui::version() unten in der
Admin-Seitenleiste ("v2.6.0 · Entwickelt von Loheide.eu").

Geprüft: Paket lokal gebaut, entpackt und die Anwendung daraus gestartet –
alle Seiten antworten mit 200, keine fehlenden Dateien.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Friederich Loheide 2026-09-23 14:50:10 +00:00
parent b7f13d8fac
commit 8facc71455
15 changed files with 216 additions and 3 deletions

15
.gitattributes vendored Normal file
View file

@ -0,0 +1,15 @@
# Zeilenenden nicht anfassen einige Dateien liegen bewusst mit CRLF vor.
* -text
# Dateien, die nicht ins Release-ZIP gehoeren.
# `git archive` (siehe .github/workflows/release.yml) wertet export-ignore aus.
/.gitattributes export-ignore
/.gitignore export-ignore
/.dockerignore export-ignore
/.github export-ignore
/docs export-ignore
/tests export-ignore
/tools export-ignore
/phpunit.xml.dist export-ignore
/phpstan.neon export-ignore
/composer.lock export-ignore

152
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,152 @@
name: Release-Paket
# Bei jedem Merge nach main entsteht ein installierbares ZIP und landet als
# Vorab-Release "latest-main" im Repository. Wird ein Tag v* gepusht, wird
# daraus ein regulaeres Release mit derselben Mechanik.
on:
push:
branches: [ main ]
tags: [ 'v*' ]
workflow_dispatch:
jobs:
package:
name: ZIP bauen und veröffentlichen
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Version und Dateinamen bestimmen
id: meta
run: |
set -eu
VERSION="$(tr -d ' \r\n' < VERSION)"
SHORT_SHA="$(git rev-parse --short HEAD)"
BUILD_DATE="$(date -u +%Y-%m-%d)"
if [ "${GITHUB_REF_TYPE:-branch}" = "tag" ]; then
TAG="${GITHUB_REF_NAME}"
NAME="unifi-voucher-tool-${TAG}"
TITLE="Version ${TAG}"
PRERELEASE="false"
else
TAG="latest-main"
NAME="unifi-voucher-tool-${VERSION}+${BUILD_DATE}.${SHORT_SHA}"
TITLE="Aktueller Stand von main ${VERSION} (${SHORT_SHA})"
PRERELEASE="true"
fi
{
echo "version=${VERSION}"
echo "short_sha=${SHORT_SHA}"
echo "build_date=${BUILD_DATE}"
echo "tag=${TAG}"
echo "name=${NAME}"
echo "title=${TITLE}"
echo "prerelease=${PRERELEASE}"
} >> "$GITHUB_OUTPUT"
- name: ZIP erzeugen
run: |
set -eu
mkdir -p dist
# git archive wertet die export-ignore-Regeln aus .gitattributes aus,
# docs/, tests/, tools/ und CI-Dateien bleiben also draußen.
git archive --format=zip -9 \
--prefix="unifi-voucher-tool/" \
-o "dist/${{ steps.meta.outputs.name }}.zip" HEAD
cd dist
sha256sum "${{ steps.meta.outputs.name }}.zip" > "${{ steps.meta.outputs.name }}.zip.sha256"
ls -lh
- name: Inhalt kurz prüfen
run: |
set -eu
# Ein paar Dateien muessen enthalten sein, sonst ist das Paket kaputt.
for required in \
unifi-voucher-tool/index.php \
unifi-voucher-tool/install.php \
unifi-voucher-tool/database.sql \
unifi-voucher-tool/assets/global.css \
unifi-voucher-tool/assets/vendor/inter/inter.css \
unifi-voucher-tool/includes/Ui.php
do
if ! unzip -l "dist/${{ steps.meta.outputs.name }}.zip" | grep -q "$required"; then
echo "Fehlt im Paket: $required" >&2
exit 1
fi
done
echo "Paket vollständig."
- name: Als Build-Artefakt sichern
uses: actions/upload-artifact@v3
continue-on-error: true # Artefakt-Speicher ist optional
with:
name: ${{ steps.meta.outputs.name }}
path: dist/*
retention-days: 30
- name: Release anlegen bzw. auffrischen
env:
# Forgejo stellt den Token automatisch bereit; FORGEJO_TOKEN
# (persönlicher Token) dient als Ausweichweg.
TOKEN: ${{ secrets.GITHUB_TOKEN || secrets.FORGEJO_TOKEN }}
API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
TAG: ${{ steps.meta.outputs.tag }}
NAME: ${{ steps.meta.outputs.name }}
TITLE: ${{ steps.meta.outputs.title }}
PRERELEASE: ${{ steps.meta.outputs.prerelease }}
VERSION: ${{ steps.meta.outputs.version }}
SHORT_SHA: ${{ steps.meta.outputs.short_sha }}
BUILD_DATE: ${{ steps.meta.outputs.build_date }}
run: |
set -eu
if [ -z "${TOKEN:-}" ]; then
echo "Kein Token vorhanden Release wird übersprungen." >&2
exit 0
fi
AUTH="Authorization: token ${TOKEN}"
# Rollendes Vorab-Release durch ein frisches ersetzen, damit der
# Download-Link stabil bleibt und auf den aktuellen Stand zeigt.
if [ "$TAG" = "latest-main" ]; then
OLD_ID="$(curl -sf -H "$AUTH" "${API}/releases/tags/${TAG}" \
| grep -o '"id":[0-9]*' | head -1 | cut -d: -f2 || true)"
if [ -n "${OLD_ID:-}" ]; then
curl -sf -X DELETE -H "$AUTH" "${API}/releases/${OLD_ID}" || true
curl -sf -X DELETE -H "$AUTH" "${API}/tags/${TAG}" || true
fi
fi
# Release-Text bewusst ohne Anfuehrungszeichen und Backslashes,
# damit er ohne jq direkt in den JSON-Body passt (\n bleibt literal).
BODY="Automatisch gebaut aus Commit ${SHORT_SHA}.\n\n"
BODY="${BODY}| | |\n|---|---|\n"
BODY="${BODY}| Version | ${VERSION} |\n"
BODY="${BODY}| Commit | ${SHORT_SHA} |\n"
BODY="${BODY}| Gebaut am | ${BUILD_DATE} |\n\n"
BODY="${BODY}**Neuinstallation:** ZIP entpacken, Dateien auf den Webserver legen, install.php aufrufen.\n\n"
BODY="${BODY}**Update einer bestehenden Installation:** config.php, uploads/ und updater/storage/ nicht ueberschreiben "
BODY="${BODY}- oder gleich den eingebauten Updater unter Administration, System-Update verwenden.\n\n"
BODY="${BODY}Pruefsumme: siehe beigelegte .sha256-Datei."
RELEASE_ID="$(curl -sf -X POST -H "$AUTH" -H 'Content-Type: application/json' \
-d "{\"tag_name\":\"${TAG}\",\"target_commitish\":\"${GITHUB_SHA}\",\"name\":\"${TITLE}\",\"body\":\"${BODY}\",\"draft\":false,\"prerelease\":${PRERELEASE}}" \
"${API}/releases" | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2)"
if [ -z "${RELEASE_ID:-}" ]; then
echo "Release konnte nicht angelegt werden." >&2
exit 1
fi
for file in "dist/${NAME}.zip" "dist/${NAME}.zip.sha256"; do
curl -sf -X POST -H "$AUTH" \
-F "attachment=@${file}" \
"${API}/releases/${RELEASE_ID}/assets?name=$(basename "$file")" > /dev/null
echo "Angehängt: $(basename "$file")"
done
echo "Release ${TITLE} steht bereit: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/releases/tag/${TAG}"

View file

@ -519,6 +519,10 @@ vendor/bin/phpunit # 29 Tests (Crypto, TOTP, API-Keys, Upload, Ui)
vendor/bin/phpstan analyse # Level 5
```
Die Versionsnummer steht in der Datei **`VERSION`** im Projektstamm. Sie wird
im Admin-Bereich unten in der Seitenleiste angezeigt und benennt das
Release-Paket für eine neue Version also dort (und im Badge oben) anheben.
Die Pipeline (`.github/workflows/ci.yml`) führt zusätzlich einen
Syntax-Check über alle PHP-Dateien aus und prüft, ob `lang/de.php` und
`lang/en.php` dieselben Schlüssel enthalten und jeder im Code verwendete
@ -540,6 +544,27 @@ git clone https://git.loheide.cloud/friloo/Unifi-Voucher-Tool.git
git clone ssh://git@git.loheide.cloud:2222/friloo/Unifi-Voucher-Tool.git
```
### Fertige Pakete
Jeder Merge nach `main` erzeugt automatisch ein installierbares ZIP
(`.github/workflows/release.yml`) und hängt es an das rollende Vorab-Release
**`latest-main`**:
**<https://git.loheide.cloud/friloo/Unifi-Voucher-Tool/releases>**
Das Paket enthält nur die Laufzeit-Dateien `docs/`, `tests/`, `tools/` und die
CI-Konfiguration bleiben draußen (rund 1,4 MB). Wird ein Tag `v*` gepusht,
entsteht daraus ein reguläres Release mit derselben Mechanik.
> Beim **Update einer bestehenden Installation** `config.php`, `uploads/` und
> `updater/storage/` nicht überschreiben oder gleich den eingebauten Updater
> verwenden, der genau diese Pfade schützt.
Voraussetzung ist ein registrierter **Forgejo-Actions-Runner**; ohne Runner
bleiben die Workflows in der Warteschlange stehen.
### Mitwirken
Fehlerberichte und Änderungsvorschläge laufen über die **Issues** und **Pull
Requests** dort. Für die Kommandozeile eignet sich [`tea`](https://gitea.com/gitea/tea),
die Gitea-/Forgejo-CLI:

1
VERSION Normal file
View file

@ -0,0 +1 @@
2.6.0

Binary file not shown.

Before

Width:  |  Height:  |  Size: 390 KiB

After

Width:  |  Height:  |  Size: 392 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 384 KiB

After

Width:  |  Height:  |  Size: 385 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 320 KiB

After

Width:  |  Height:  |  Size: 321 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 313 KiB

After

Width:  |  Height:  |  Size: 315 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 309 KiB

After

Width:  |  Height:  |  Size: 311 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 330 KiB

After

Width:  |  Height:  |  Size: 331 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 278 KiB

After

Width:  |  Height:  |  Size: 280 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 285 KiB

After

Width:  |  Height:  |  Size: 285 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 416 KiB

After

Width:  |  Height:  |  Size: 417 KiB

Before After
Before After

View file

@ -116,6 +116,21 @@ class Ui
. '</style>';
}
/**
* Version aus der Datei VERSION im Projektstamm.
* Damit tragen Oberfläche und Release-Paket dieselbe Nummer.
*/
public static function version(): string
{
static $version = null;
if ($version === null) {
$file = self::root() . '/VERSION';
$version = is_file($file) ? trim((string)file_get_contents($file)) : '';
}
return $version;
}
/** Entwicklerhinweis bewusst an einer Stelle gepflegt. */
public const CREDIT_NAME = 'Loheide.eu';
public const CREDIT_URL = 'https://loheide.eu';
@ -123,11 +138,16 @@ class Ui
/**
* Dezenter Hinweis auf den Entwickler, wie er im Seitenfuß erscheint.
*/
public static function credit(): string
public static function credit(bool $withVersion = false): string
{
$label = function_exists('__') ? __('credit_by') : 'Entwickelt von';
return '<p class="app-credit">' . htmlspecialchars($label) . ' '
$prefix = '';
if ($withVersion && self::version() !== '') {
$prefix = 'v' . htmlspecialchars(self::version()) . ' · ';
}
return '<p class="app-credit">' . $prefix . htmlspecialchars($label) . ' '
. '<a href="' . self::CREDIT_URL . '" target="_blank" rel="noopener">'
. self::CREDIT_NAME . '</a></p>';
}

View file

@ -89,7 +89,7 @@ foreach ($navGroups as $items) {
</a>
</div>
<?php endif; ?>
<?= Ui::credit() ?>
<?= Ui::credit(true) ?>
</div>
</aside>