diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..7e20944 --- /dev/null +++ b/.gitattributes @@ -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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..3fc3001 --- /dev/null +++ b/.github/workflows/release.yml @@ -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}" diff --git a/Readme.md b/Readme.md index 89353eb..890d36d 100644 --- a/Readme.md +++ b/Readme.md @@ -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`**: + +**** + +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: diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..e70b452 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +2.6.0 diff --git a/docs/screenshots/admin-dashboard-dark.png b/docs/screenshots/admin-dashboard-dark.png index b615914..24cb92e 100644 Binary files a/docs/screenshots/admin-dashboard-dark.png and b/docs/screenshots/admin-dashboard-dark.png differ diff --git a/docs/screenshots/admin-dashboard.png b/docs/screenshots/admin-dashboard.png index bfb3012..006348c 100644 Binary files a/docs/screenshots/admin-dashboard.png and b/docs/screenshots/admin-dashboard.png differ diff --git a/docs/screenshots/api-keys.png b/docs/screenshots/api-keys.png index 98e3dd0..c8664c7 100644 Binary files a/docs/screenshots/api-keys.png and b/docs/screenshots/api-keys.png differ diff --git a/docs/screenshots/integrations.png b/docs/screenshots/integrations.png index 41fb539..57edb8d 100644 Binary files a/docs/screenshots/integrations.png and b/docs/screenshots/integrations.png differ diff --git a/docs/screenshots/settings-branding.png b/docs/screenshots/settings-branding.png index cec6e04..b826df4 100644 Binary files a/docs/screenshots/settings-branding.png and b/docs/screenshots/settings-branding.png differ diff --git a/docs/screenshots/settings-login.png b/docs/screenshots/settings-login.png index 3c7d00f..cdc97f9 100644 Binary files a/docs/screenshots/settings-login.png and b/docs/screenshots/settings-login.png differ diff --git a/docs/screenshots/settings.png b/docs/screenshots/settings.png index cb6f489..aabd60f 100644 Binary files a/docs/screenshots/settings.png and b/docs/screenshots/settings.png differ diff --git a/docs/screenshots/two-factor.png b/docs/screenshots/two-factor.png index 7d9704b..809a849 100644 Binary files a/docs/screenshots/two-factor.png and b/docs/screenshots/two-factor.png differ diff --git a/docs/screenshots/vouchers.png b/docs/screenshots/vouchers.png index 230dc97..38d79d9 100644 Binary files a/docs/screenshots/vouchers.png and b/docs/screenshots/vouchers.png differ diff --git a/includes/Ui.php b/includes/Ui.php index 40bff32..2800034 100644 --- a/includes/Ui.php +++ b/includes/Ui.php @@ -116,6 +116,21 @@ class Ui . ''; } + /** + * 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 '

' . htmlspecialchars($label) . ' ' + $prefix = ''; + if ($withVersion && self::version() !== '') { + $prefix = 'v' . htmlspecialchars(self::version()) . ' · '; + } + + return '

' . $prefix . htmlspecialchars($label) . ' ' . '' . self::CREDIT_NAME . '

'; } diff --git a/includes/admin_nav.php b/includes/admin_nav.php index 6d4b671..5a5a5a1 100644 --- a/includes/admin_nav.php +++ b/includes/admin_nav.php @@ -89,7 +89,7 @@ foreach ($navGroups as $items) { - +