From c7db9900d161d09339584a34fd5092f8cf5682c6 Mon Sep 17 00:00:00 2001 From: soeren Date: Sun, 16 Aug 2026 22:10:03 +0200 Subject: [PATCH 1/4] Harden self-hosted release workflow --- .beads/interactions.jsonl | 5 + .env.docker.example | 3 + .github/dependabot.yml | 9 + .github/workflows/ci.yml | 64 +- .github/workflows/release-validation.yml | 4 +- .github/workflows/release.yml | 48 +- .github/workflows/security-audit.yml | 2 +- Dockerfile | 2 +- README.md | 66 +- app/Console/Commands/InstallApplication.php | 17 +- compose.caddy.yml | 2 +- compose.yml | 4 +- composer.json | 6 +- composer.lock | 643 ++++++++++++------ docs/self-hosting.md | 46 +- install.sh | 147 +++- .../Feature/InstallApplicationCommandTest.php | 13 + 17 files changed, 813 insertions(+), 268 deletions(-) diff --git a/.beads/interactions.jsonl b/.beads/interactions.jsonl index a19c8bb..5338e49 100644 --- a/.beads/interactions.jsonl +++ b/.beads/interactions.jsonl @@ -49,3 +49,8 @@ {"id":"int-e502a2517628e89f5d584691cab7e640","kind":"field_change","created_at":"2026-08-16T18:37:16.575687995Z","actor":"soeren","issue_id":"versiontracker-ylt","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Enabled the approved master ruleset with pull-request-only updates, five required GitHub Actions checks, and an admin emergency bypass."}} {"id":"int-11fe797c40aae0c72e5c3c835e4e716a","kind":"field_change","created_at":"2026-08-16T19:00:22.088661938Z","actor":"soeren","issue_id":"versiontracker-17l","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Versioned Docker self-hosting, installer, release automation, and CI checks implemented"}} {"id":"int-6f561cd3894c5d25e159a71c02adb186","kind":"field_change","created_at":"2026-08-16T19:20:47.925237745Z","actor":"soeren","issue_id":"versiontracker-2cw","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Hardened Docker installer, self-hosting docs, and container/release validation"}} +{"id":"int-0d151ef4ec7821968bfab2e1e18e8a66","kind":"field_change","created_at":"2026-08-16T19:50:45.14562513Z","actor":"soeren","issue_id":"versiontracker-0z1","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Validated v0.1.1 installation release with Docker, browser DevTools, backup, and proxy/Caddy compose configuration."}} +{"id":"int-45ff5b83c06f318fa99a39159f8fea58","kind":"field_change","created_at":"2026-08-16T20:09:15.43629742Z","actor":"soeren","issue_id":"versiontracker-58y","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Implemented and locally validated release acceptance, deployment bundle, operational hardening, CI security, and PHP 8.4/PHPUnit 13 upgrade."}} +{"id":"int-dc8ba7de281af8232cadf095dccff699","kind":"field_change","created_at":"2026-08-16T20:09:15.653609064Z","actor":"soeren","issue_id":"versiontracker-rzz","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Implemented and locally validated release acceptance, deployment bundle, operational hardening, CI security, and PHP 8.4/PHPUnit 13 upgrade."}} +{"id":"int-1f901c67824319fd2c927e54a9d71d0e","kind":"field_change","created_at":"2026-08-16T20:09:15.863678001Z","actor":"soeren","issue_id":"versiontracker-9hd","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Implemented and locally validated release acceptance, deployment bundle, operational hardening, CI security, and PHP 8.4/PHPUnit 13 upgrade."}} +{"id":"int-04ba3562d9d50496714fd5d68afe3422","kind":"field_change","created_at":"2026-08-16T20:09:16.095500058Z","actor":"soeren","issue_id":"versiontracker-ez6","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Implemented and locally validated release acceptance, deployment bundle, operational hardening, CI security, and PHP 8.4/PHPUnit 13 upgrade."}} diff --git a/.env.docker.example b/.env.docker.example index 4737dee..1875d61 100644 --- a/.env.docker.example +++ b/.env.docker.example @@ -1,4 +1,7 @@ VERSION= +IMAGE_REPOSITORY=ghcr.io/soerennb/version-tracker +MARIADB_IMAGE=mariadb:11.8.8 +CADDY_IMAGE=caddy:2.11.4 DEPLOYMENT_MODE= APP_NAME=VersionTracker APP_ENV=production diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 7ce13c7..614f36d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -60,3 +60,12 @@ updates: - dependency-name: "*" update-types: - version-update:semver-major + + - package-ecosystem: docker + directory: / + schedule: + interval: weekly + day: monday + time: "06:00" + timezone: Europe/Berlin + open-pull-requests-limit: 5 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9b2777b..fc8ad58 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,16 +30,16 @@ jobs: - name: Set up PHP uses: shivammathur/setup-php@v2 with: - php-version: "8.3" + php-version: "8.4" tools: composer - extensions: mbstring, pdo_sqlite, sqlite3 + extensions: intl, mbstring, pdo_sqlite, sqlite3 - name: Cache Composer dependencies uses: actions/cache@v6 with: path: ~/.cache/composer/files - key: ${{ runner.os }}-php-8.3-${{ hashFiles('composer.lock') }} - restore-keys: ${{ runner.os }}-php-8.3- + key: ${{ runner.os }}-php-8.4-${{ hashFiles('composer.lock') }} + restore-keys: ${{ runner.os }}-php-8.4- - name: Install PHP dependencies run: composer install --no-interaction --no-progress --prefer-dist @@ -70,7 +70,7 @@ jobs: - name: Set up PHP uses: shivammathur/setup-php@v2 with: - php-version: "8.3" + php-version: "8.4" tools: composer - name: Install PHP dependencies @@ -120,9 +120,9 @@ jobs: - name: Set up PHP uses: shivammathur/setup-php@v2 with: - php-version: "8.3" + php-version: "8.4" tools: composer - extensions: mbstring, pdo_mysql + extensions: intl, mbstring, pdo_mysql - name: Install PHP dependencies run: composer install --no-interaction --no-progress --prefer-dist @@ -163,6 +163,16 @@ jobs: cache-from: type=gha,scope=container cache-to: type=gha,mode=max,scope=container + - name: Scan application image + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 + with: + image-ref: ghcr.io/soerennb/version-tracker:test + format: table + exit-code: "1" + ignore-unfixed: true + severity: CRITICAL,HIGH + vuln-type: os,library + - name: Prepare Compose environment run: | cp .env.docker.example .env.docker @@ -189,6 +199,39 @@ jobs: docker compose --env-file .env.docker -f compose.yml -f compose.proxy.yml logs exit 1 + - name: Back up and restore application data + run: | + backup_directory="$(./install.sh backup)" + test -s "$backup_directory/database.sql" + test -s "$backup_directory/storage.tar.gz" + test "$(stat --format=%a "$backup_directory/environment.backup")" = 600 + + docker compose --env-file .env.docker -f compose.yml -f compose.proxy.yml down --volumes --remove-orphans + docker compose --env-file .env.docker -f compose.yml -f compose.proxy.yml up --detach db + + for attempt in {1..30}; do + if docker compose --env-file .env.docker -f compose.yml -f compose.proxy.yml exec --no-TTY db healthcheck.sh --connect --innodb_initialized; then + break + fi + + sleep 2 + done + + docker compose --env-file .env.docker -f compose.yml -f compose.proxy.yml exec --no-TTY db sh -c 'exec mariadb -uroot -p"$MARIADB_ROOT_PASSWORD" "$MARIADB_DATABASE"' < "$backup_directory/database.sql" + docker compose --env-file .env.docker -f compose.yml -f compose.proxy.yml run --rm app sh -c 'tar -xzf - -C /var/www/html' < "$backup_directory/storage.tar.gz" + docker compose --env-file .env.docker -f compose.yml -f compose.proxy.yml up --detach app + + for attempt in {1..30}; do + if curl --fail --silent http://127.0.0.1:8080/up; then + exit 0 + fi + + sleep 2 + done + + docker compose --env-file .env.docker -f compose.yml -f compose.proxy.yml logs + exit 1 + - name: Clean up containers if: always() run: docker compose --env-file .env.docker -f compose.yml -f compose.proxy.yml down --volumes --remove-orphans @@ -207,6 +250,9 @@ jobs: docker compose --env-file .env.docker -f compose.yml -f compose.proxy.yml config --quiet docker compose --env-file .env.docker -f compose.yml -f compose.caddy.yml config --quiet + - name: Validate Caddyfile + run: docker run --rm --env CADDY_DOMAIN=example.invalid --env CADDY_EMAIL=ci@example.invalid --volume "$GITHUB_WORKSPACE/Caddyfile:/etc/caddy/Caddyfile:ro" caddy:2.11.4 caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile + shell: name: Installer script runs-on: ubuntu-latest @@ -218,4 +264,6 @@ jobs: run: sudo apt-get update && sudo apt-get install --yes shellcheck - name: Lint installer - run: shellcheck install.sh + run: | + test -x install.sh + shellcheck install.sh diff --git a/.github/workflows/release-validation.yml b/.github/workflows/release-validation.yml index 7339fcf..c0e17d8 100644 --- a/.github/workflows/release-validation.yml +++ b/.github/workflows/release-validation.yml @@ -17,9 +17,9 @@ jobs: - name: Set up PHP uses: shivammathur/setup-php@v2 with: - php-version: "8.3" + php-version: "8.4" tools: composer - extensions: mbstring, pdo_sqlite, sqlite3 + extensions: intl, mbstring, pdo_sqlite, sqlite3 - name: Set up Node.js uses: actions/setup-node@v7 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 452ae3f..7ebfab8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -89,6 +89,50 @@ jobs: docker logs versiontracker exit 1 + - name: Scan published image + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 + with: + image-ref: ghcr.io/${{ github.repository }}@${{ steps.image.outputs.digest }} + format: table + exit-code: "1" + ignore-unfixed: true + severity: CRITICAL,HIGH + vuln-type: os,library + + - name: Validate published installer path + env: + VERSION: ${{ github.ref_name }} + COMPOSE_PROJECT_NAME: versiontracker-release-install + run: | + test -x install.sh + printf '%s\n' 'Release Test Password 2026' | ./install.sh install \ + --version "$VERSION" \ + --mode proxy \ + --port 18080 \ + --admin-name 'Release Test' \ + --admin-email release-test@example.invalid \ + --admin-password-stdin + ./install.sh status + backup_directory="$(./install.sh backup)" + test -s "$backup_directory/database.sql" + test -s "$backup_directory/storage.tar.gz" + + - name: Build deployment bundle + env: + VERSION: ${{ github.ref_name }} + run: | + bundle="versiontracker-deploy-${VERSION}.tar.gz" + tar --create --gzip --file "$bundle" \ + --transform "s,^,versiontracker-deploy-${VERSION}/," \ + install.sh compose.yml compose.proxy.yml compose.caddy.yml Caddyfile .env.docker.example docs/self-hosting.md + sha256sum "$bundle" > "${bundle}.sha256" + + - name: Clean up release installation test + if: always() + env: + COMPOSE_PROJECT_NAME: versiontracker-release-install + run: docker compose --env-file .env.docker -f compose.yml -f compose.proxy.yml down --volumes --remove-orphans + - name: Create GitHub release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -98,4 +142,6 @@ jobs: gh release create "$GITHUB_REF_NAME" \ --generate-notes \ --title "$GITHUB_REF_NAME" \ - --notes $'### Container image\n\n`'"$IMAGE@$DIGEST"$'\n\n### Upgrade notes\n\nSee the self-hosting guide for backup, update, and rollback instructions.' + --notes $'### Container image\n\n`'"$IMAGE@$DIGEST"$'\n\n### Deployment bundle\n\nDownload the `versiontracker-deploy-` archive and verify its SHA-256 checksum before installation.\n\n### Upgrade notes\n\nSee the self-hosting guide for backup, update, and rollback instructions.' \ + "versiontracker-deploy-${GITHUB_REF_NAME}.tar.gz" \ + "versiontracker-deploy-${GITHUB_REF_NAME}.tar.gz.sha256" diff --git a/.github/workflows/security-audit.yml b/.github/workflows/security-audit.yml index 4652184..953aed2 100644 --- a/.github/workflows/security-audit.yml +++ b/.github/workflows/security-audit.yml @@ -20,7 +20,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: "8.3" + php-version: "8.4" tools: composer extensions: mbstring, sqlite, pdo_sqlite diff --git a/Dockerfile b/Dockerfile index 286a803..a9758ff 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,7 +21,7 @@ COPY public ./public COPY vite.config.js ./ RUN npm run build -FROM php:8.3-apache-bookworm +FROM php:8.4-apache-bookworm RUN apt-get update \ && apt-get install --yes --no-install-recommends curl libfreetype6-dev libicu-dev libjpeg62-turbo-dev libonig-dev libpng-dev libzip-dev unzip \ diff --git a/README.md b/README.md index fccc814..f4b2050 100644 --- a/README.md +++ b/README.md @@ -11,42 +11,45 @@ VersionTracker is a Laravel 13 application that centralizes software versions, r ## Stack -- PHP 8.3 · Laravel 13 · Livewire 4 · Filament 5 +- PHP 8.4 · Laravel 13 · Livewire 4 · Filament 5 - MariaDB/MySQL/PostgreSQL/SQLite (Default: SQLite) - Node 22.18+ · Vite 8 · Vue 3 · Vue Router 5 · Vue I18n 11 · Tailwind CSS 4 ## Requirements -- PHP >= 8.3 + Composer 2.x +- PHP >= 8.4.1 + Composer 2.x - Node.js >= 22.18 + npm 10 - SQLite (default) or an alternative database ## Installation 1. **Clone Repository** - ```bash - git clone https://github.com//versiontracker.git - cd versiontracker - ``` + + ```bash + git clone https://github.com//versiontracker.git + cd versiontracker + ``` 2. **Install the Application** - ```bash - composer run setup - ``` - This creates the SQLite database, generates the application key, runs migrations, installs locked frontend dependencies, and builds the assets. + + ```bash + composer run setup + ``` + + This creates the SQLite database, generates the application key, runs migrations, installs locked frontend dependencies, and builds the assets. 3. **Start Application** - ```bash - composer run dev - ``` - App available at `http://localhost:8000`. The Filament panel is at `/admin`. + ```bash + composer run dev + ``` + App available at `http://localhost:8000`. The Filament panel is at `/admin`. For demo data, run `php artisan db:seed` after setup. ## Demo Accounts -| Environment | User | Password | -| ----------- | ----------------- | --------- | +| Environment | User | Password | +| -------------- | ------------------ | ---------- | | Filament Admin | `demo@example.com` | `password` | ## Frontend Access @@ -64,7 +67,22 @@ For demo data, run `php artisan db:seed` after setup. ## Self-Hosting with Docker -Each `v0.x.y` GitHub release publishes a container image at `ghcr.io/soerennb/version-tracker`. The installer requires a concrete release tag; production deployments never use `latest`. +Each `v0.x.y` GitHub release publishes a container image at `ghcr.io/soerennb/version-tracker` and a compact deployment bundle. The installer requires a concrete release tag; production deployments never use `latest`. + +### Download a deployment bundle + +Download the exact bundle and checksum from the GitHub Release page, then verify the archive before unpacking it: + +```bash +VERSION=v0.1.2 +curl -fsSLO "https://github.com/soerennb/version-tracker/releases/download/${VERSION}/versiontracker-deploy-${VERSION}.tar.gz" +curl -fsSLO "https://github.com/soerennb/version-tracker/releases/download/${VERSION}/versiontracker-deploy-${VERSION}.tar.gz.sha256" +sha256sum --check "versiontracker-deploy-${VERSION}.tar.gz.sha256" +tar -xzf "versiontracker-deploy-${VERSION}.tar.gz" +cd "versiontracker-deploy-${VERSION}" +``` + +Cloning the repository remains supported for contributors, but operators only need this deployment bundle. ### Existing reverse proxy or local network @@ -74,7 +92,19 @@ cd version-tracker ./install.sh install ``` -Choose `N` for Caddy, then enter the release tag (for example `v0.1.0`) and the exposed HTTP port. Point your existing reverse proxy at this port and configure `TRUSTED_PROXIES` in `.env.docker` with the proxy address. +Choose `N` for Caddy, then enter the release tag (for example `v0.1.2`) and the exposed HTTP port. Point your existing reverse proxy at this port and configure `TRUSTED_PROXIES` in `.env.docker` with the proxy address. + +For unattended installations, provide the selected values explicitly and pipe only the administrator password through standard input: + +```bash +printf '%s\n' 'choose-a-long-unique-password' | ./install.sh install \ + --version v0.1.2 \ + --mode proxy \ + --port 8080 \ + --admin-name 'Administrator' \ + --admin-email admin@example.com \ + --admin-password-stdin +``` ### Public server with automatic HTTPS diff --git a/app/Console/Commands/InstallApplication.php b/app/Console/Commands/InstallApplication.php index bfcdb9a..753a798 100644 --- a/app/Console/Commands/InstallApplication.php +++ b/app/Console/Commands/InstallApplication.php @@ -10,7 +10,12 @@ use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Hash; -#[Signature('app:install {--demo : Seed the demo user and data instead of creating an administrator}')] +#[Signature('app:install + {--demo : Seed the demo user and data instead of creating an administrator} + {--no-demo : Do not ask whether to seed demo data} + {--admin-name= : First administrator name} + {--admin-email= : First administrator email} + {--admin-password-stdin : Read the first administrator password from standard input}')] #[Description('Initialize the application database and its first administrator')] class InstallApplication extends Command { @@ -24,7 +29,7 @@ public function handle(): int return self::FAILURE; } - $shouldCreateDemoData = $this->option('demo') || $this->confirm('Create demo data?', false); + $shouldCreateDemoData = $this->option('demo') || (! $this->option('no-demo') && $this->confirm('Create demo data?', false)); if ($shouldCreateDemoData) { if (! $this->confirm('Demo data creates a public administrator password. Continue?', false)) { @@ -54,15 +59,17 @@ public function handle(): int private function createAdministrator(): void { - $name = $this->ask('Administrator name'); - $email = $this->ask('Administrator email'); + $name = $this->option('admin-name') ?: $this->ask('Administrator name'); + $email = $this->option('admin-email') ?: $this->ask('Administrator email'); while (! filter_var($email, FILTER_VALIDATE_EMAIL)) { $this->components->error('Enter a valid email address.'); $email = $this->ask('Administrator email'); } - $password = $this->secret('Administrator password (at least 12 characters)'); + $password = $this->option('admin-password-stdin') + ? trim((string) stream_get_contents(STDIN)) + : $this->secret('Administrator password (at least 12 characters)'); while (mb_strlen((string) $password) < 12) { $this->components->error('The password must be at least 12 characters long.'); diff --git a/compose.caddy.yml b/compose.caddy.yml index eba41d0..0eea1b9 100644 --- a/compose.caddy.yml +++ b/compose.caddy.yml @@ -4,7 +4,7 @@ services: TRUSTED_PROXIES: "*" caddy: - image: caddy:2 + image: ${CADDY_IMAGE:-caddy:2.11.4} depends_on: app: condition: service_healthy diff --git a/compose.yml b/compose.yml index 515dc71..ec42a05 100644 --- a/compose.yml +++ b/compose.yml @@ -1,6 +1,6 @@ services: app: - image: ghcr.io/soerennb/version-tracker:${VERSION:?Set VERSION to a concrete v0.x.y release tag} + image: ${IMAGE_REPOSITORY:-ghcr.io/soerennb/version-tracker}:${VERSION:?Set VERSION to a concrete v0.x.y release tag} env_file: - .env.docker depends_on: @@ -11,7 +11,7 @@ services: - app_storage:/var/www/html/storage db: - image: mariadb:11 + image: ${MARIADB_IMAGE:-mariadb:11.8.8} environment: MARIADB_DATABASE: ${DB_DATABASE} MARIADB_USER: ${DB_USERNAME} diff --git a/composer.json b/composer.json index 6c5b398..c2c2ecc 100644 --- a/composer.json +++ b/composer.json @@ -6,7 +6,7 @@ "keywords": ["laravel", "framework"], "license": "MIT", "require": { - "php": "^8.3", + "php": "^8.4.1", "barryvdh/laravel-dompdf": "^3.1.2", "darkaonline/l5-swagger": "^11.0", "filament/filament": "^5.0", @@ -24,7 +24,7 @@ "laravel/sail": "^1.54", "mockery/mockery": "^1.6", "nunomaduro/collision": "^8.6", - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^13.0" }, "autoload": { "psr-4": { @@ -90,7 +90,7 @@ "php-http/discovery": true }, "platform": { - "php": "8.3.0" + "php": "8.4.1" } }, "minimum-stability": "stable", diff --git a/composer.lock b/composer.lock index f9a611d..754318f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "9e5b3ee3300d81316d0a150c36cff5ed", + "content-hash": "7bab30ccfe834107f1687c87c767f713", "packages": [ { "name": "anourvalar/eloquent-serialize", @@ -10826,33 +10826,35 @@ }, { "name": "phpunit/php-code-coverage", - "version": "12.5.7", + "version": "14.3.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "186dab580576598076de6818596d12b61801880e" + "reference": "6ce313bb110384148d1dc7695a99175f59529069" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/186dab580576598076de6818596d12b61801880e", - "reference": "186dab580576598076de6818596d12b61801880e", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/6ce313bb110384148d1dc7695a99175f59529069", + "reference": "6ce313bb110384148d1dc7695a99175f59529069", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", + "ext-mbstring": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^5.7.0", - "php": ">=8.3", - "phpunit/php-text-template": "^5.0", - "sebastian/complexity": "^5.0", - "sebastian/environment": "^8.1.2", - "sebastian/lines-of-code": "^4.0.1", - "sebastian/version": "^6.0", + "nikic/php-parser": "^5.8.0", + "php": ">=8.4", + "phpunit/php-text-template": "^6.0", + "sebastian/complexity": "^6.0", + "sebastian/environment": "^9.3.2", + "sebastian/git-state": "^1.0", + "sebastian/lines-of-code": "^5.0.2", + "sebastian/version": "^7.0", "theseer/tokenizer": "^2.0.1" }, "require-dev": { - "phpunit/phpunit": "^12.5.28" + "phpunit/phpunit": "^13.3.1" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -10861,7 +10863,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "12.5.x-dev" + "dev-main": "14.3.x-dev" } }, "autoload": { @@ -10890,7 +10892,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.7" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/14.3.1" }, "funding": [ { @@ -10910,32 +10912,32 @@ "type": "tidelift" } ], - "time": "2026-06-01T13:24:19+00:00" + "time": "2026-08-16T05:23:47+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "6.0.1", + "version": "7.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5" + "reference": "3ccaa29123548190af12fee7af078dcd7f3ddfab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", - "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3ccaa29123548190af12fee7af078dcd7f3ddfab", + "reference": "3ccaa29123548190af12fee7af078dcd7f3ddfab", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=8.4" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^13.3.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -10963,7 +10965,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/6.0.1" + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/7.0.1" }, "funding": [ { @@ -10983,28 +10985,28 @@ "type": "tidelift" } ], - "time": "2026-02-02T14:04:18+00:00" + "time": "2026-08-10T06:43:12+00:00" }, { "name": "phpunit/php-invoker", - "version": "6.0.0", + "version": "7.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-invoker.git", - "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406" + "reference": "42e5c5cae0c65df12d1b1a3ab52bf3f50f244d88" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/12b54e689b07a25a9b41e57736dfab6ec9ae5406", - "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/42e5c5cae0c65df12d1b1a3ab52bf3f50f244d88", + "reference": "42e5c5cae0c65df12d1b1a3ab52bf3f50f244d88", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=8.4" }, "require-dev": { "ext-pcntl": "*", - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^13.0" }, "suggest": { "ext-pcntl": "*" @@ -11012,7 +11014,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -11039,40 +11041,52 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-invoker/issues", "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", - "source": "https://github.com/sebastianbergmann/php-invoker/tree/6.0.0" + "source": "https://github.com/sebastianbergmann/php-invoker/tree/7.0.0" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-invoker", + "type": "tidelift" } ], - "time": "2025-02-07T04:58:58+00:00" + "time": "2026-02-06T04:34:47+00:00" }, { "name": "phpunit/php-text-template", - "version": "5.0.0", + "version": "6.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53" + "reference": "a47af19f93f76aa3368303d752aa5272ca3299f4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/e1367a453f0eda562eedb4f659e13aa900d66c53", - "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/a47af19f93f76aa3368303d752aa5272ca3299f4", + "reference": "a47af19f93f76aa3368303d752aa5272ca3299f4", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=8.4" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^13.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -11099,40 +11113,52 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-text-template/issues", "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", - "source": "https://github.com/sebastianbergmann/php-text-template/tree/5.0.0" + "source": "https://github.com/sebastianbergmann/php-text-template/tree/6.0.0" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-text-template", + "type": "tidelift" } ], - "time": "2025-02-07T04:59:16+00:00" + "time": "2026-02-06T04:36:37+00:00" }, { "name": "phpunit/php-timer", - "version": "8.0.0", + "version": "9.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc" + "reference": "a0e12065831f6ab0d83120dc61513eb8d9a966f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", - "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/a0e12065831f6ab0d83120dc61513eb8d9a966f6", + "reference": "a0e12065831f6ab0d83120dc61513eb8d9a966f6", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=8.4" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^13.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "8.0-dev" + "dev-main": "9.0-dev" } }, "autoload": { @@ -11159,28 +11185,40 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-timer/issues", "security": "https://github.com/sebastianbergmann/php-timer/security/policy", - "source": "https://github.com/sebastianbergmann/php-timer/tree/8.0.0" + "source": "https://github.com/sebastianbergmann/php-timer/tree/9.0.0" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-timer", + "type": "tidelift" } ], - "time": "2025-02-07T04:59:38+00:00" + "time": "2026-02-06T04:37:53+00:00" }, { "name": "phpunit/phpunit", - "version": "12.5.33", + "version": "13.3.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "b98e028a26c5c5ba7e4a54be96ccf35f2914d184" + "reference": "fc024931d6ad047404e9d86536735923fe63a06b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b98e028a26c5c5ba7e4a54be96ccf35f2914d184", - "reference": "b98e028a26c5c5ba7e4a54be96ccf35f2914d184", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/fc024931d6ad047404e9d86536735923fe63a06b", + "reference": "fc024931d6ad047404e9d86536735923fe63a06b", "shasum": "" }, "require": { @@ -11190,25 +11228,27 @@ "ext-libxml": "*", "ext-mbstring": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.13.4", + "myclabs/deep-copy": "^1.14.0", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", - "php": ">=8.3", - "phpunit/php-code-coverage": "^12.5.7", - "phpunit/php-file-iterator": "^6.0.1", - "phpunit/php-invoker": "^6.0.0", - "phpunit/php-text-template": "^5.0.0", - "phpunit/php-timer": "^8.0.0", - "sebastian/cli-parser": "^4.2.1", - "sebastian/comparator": "^7.1.8", - "sebastian/diff": "^7.0.0", - "sebastian/environment": "^8.1.2", - "sebastian/exporter": "^7.0.3", - "sebastian/global-state": "^8.0.3", - "sebastian/object-enumerator": "^7.0.0", - "sebastian/recursion-context": "^7.0.1", - "sebastian/type": "^6.0.4", - "sebastian/version": "^6.0.0", + "php": ">=8.4.1", + "phpunit/php-code-coverage": "^14.3", + "phpunit/php-file-iterator": "^7.0.1", + "phpunit/php-invoker": "^7.0.0", + "phpunit/php-text-template": "^6.0.0", + "phpunit/php-timer": "^9.0.0", + "sebastian/cli-parser": "^5.0.1", + "sebastian/comparator": "^8.4", + "sebastian/diff": "^9.0", + "sebastian/environment": "^9.3.2", + "sebastian/exporter": "^8.2.1", + "sebastian/file-filter": "^1.0", + "sebastian/git-state": "^1.0", + "sebastian/global-state": "^9.0.1", + "sebastian/object-enumerator": "^8.1.0", + "sebastian/recursion-context": "^8.0.1", + "sebastian/type": "^7.0.2", + "sebastian/version": "^7.0.0", "staabm/side-effects-detector": "^1.0.5" }, "bin": [ @@ -11217,7 +11257,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "12.5-dev" + "dev-main": "13.3-dev" } }, "autoload": { @@ -11249,7 +11289,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.33" + "source": "https://github.com/sebastianbergmann/phpunit/tree/13.3.1" }, "funding": [ { @@ -11257,32 +11297,32 @@ "type": "other" } ], - "time": "2026-07-28T13:58:09+00:00" + "time": "2026-08-13T13:14:23+00:00" }, { "name": "sebastian/cli-parser", - "version": "4.2.1", + "version": "5.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "7d05781b13f7dec9043a629a21d086ed74582a15" + "reference": "eeb759ad3146b7096fb59c3195d39e071cd409e3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/7d05781b13f7dec9043a629a21d086ed74582a15", - "reference": "7d05781b13f7dec9043a629a21d086ed74582a15", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/eeb759ad3146b7096fb59c3195d39e071cd409e3", + "reference": "eeb759ad3146b7096fb59c3195d39e071cd409e3", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=8.4" }, "require-dev": { - "phpunit/phpunit": "^12.5.25" + "phpunit/phpunit": "^13.2.6" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "4.2-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -11306,7 +11346,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/cli-parser/issues", "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/4.2.1" + "source": "https://github.com/sebastianbergmann/cli-parser/tree/5.0.1" }, "funding": [ { @@ -11326,31 +11366,31 @@ "type": "tidelift" } ], - "time": "2026-05-17T05:29:34+00:00" + "time": "2026-08-01T04:27:14+00:00" }, { "name": "sebastian/comparator", - "version": "7.1.8", + "version": "8.4.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "7c65c1e79836812819705b473a90c12399542485" + "reference": "3b070e608146cba00fd6fd1f0ffba89e5a8897fb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/7c65c1e79836812819705b473a90c12399542485", - "reference": "7c65c1e79836812819705b473a90c12399542485", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/3b070e608146cba00fd6fd1f0ffba89e5a8897fb", + "reference": "3b070e608146cba00fd6fd1f0ffba89e5a8897fb", "shasum": "" }, "require": { "ext-dom": "*", "ext-mbstring": "*", - "php": ">=8.3", - "sebastian/diff": "^7.0", - "sebastian/exporter": "^7.0.3" + "php": ">=8.4", + "sebastian/diff": "^9.0", + "sebastian/exporter": "^8.2" }, "require-dev": { - "phpunit/phpunit": "^12.5.25" + "phpunit/phpunit": "^13.3" }, "suggest": { "ext-bcmath": "For comparing BcMath\\Number objects" @@ -11358,7 +11398,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "7.1-dev" + "dev-main": "8.4-dev" } }, "autoload": { @@ -11398,7 +11438,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/7.1.8" + "source": "https://github.com/sebastianbergmann/comparator/tree/8.4.0" }, "funding": [ { @@ -11418,33 +11458,33 @@ "type": "tidelift" } ], - "time": "2026-05-21T04:45:25+00:00" + "time": "2026-08-07T07:23:13+00:00" }, { "name": "sebastian/complexity", - "version": "5.0.0", + "version": "6.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb" + "reference": "c5651c795c98093480df79350cb050813fc7a2f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/bad4316aba5303d0221f43f8cee37eb58d384bbb", - "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/c5651c795c98093480df79350cb050813fc7a2f3", + "reference": "c5651c795c98093480df79350cb050813fc7a2f3", "shasum": "" }, "require": { "nikic/php-parser": "^5.0", - "php": ">=8.3" + "php": ">=8.4" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^13.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -11468,41 +11508,53 @@ "support": { "issues": "https://github.com/sebastianbergmann/complexity/issues", "security": "https://github.com/sebastianbergmann/complexity/security/policy", - "source": "https://github.com/sebastianbergmann/complexity/tree/5.0.0" + "source": "https://github.com/sebastianbergmann/complexity/tree/6.0.0" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/complexity", + "type": "tidelift" } ], - "time": "2025-02-07T04:55:25+00:00" + "time": "2026-02-06T04:41:32+00:00" }, { "name": "sebastian/diff", - "version": "7.0.0", + "version": "9.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "7ab1ea946c012266ca32390913653d844ecd085f" + "reference": "a3fb6a298a265ff487a91bbea46e03cd01dbb226" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7ab1ea946c012266ca32390913653d844ecd085f", - "reference": "7ab1ea946c012266ca32390913653d844ecd085f", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/a3fb6a298a265ff487a91bbea46e03cd01dbb226", + "reference": "a3fb6a298a265ff487a91bbea46e03cd01dbb226", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=8.4" }, "require-dev": { - "phpunit/phpunit": "^12.0", - "symfony/process": "^7.2" + "phpunit/phpunit": "^13.2", + "symfony/process": "^7.4.13" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "7.0-dev" + "dev-main": "9.0-dev" } }, "autoload": { @@ -11535,35 +11587,47 @@ "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", "security": "https://github.com/sebastianbergmann/diff/security/policy", - "source": "https://github.com/sebastianbergmann/diff/tree/7.0.0" + "source": "https://github.com/sebastianbergmann/diff/tree/9.0.0" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/diff", + "type": "tidelift" } ], - "time": "2025-02-07T04:55:46+00:00" + "time": "2026-06-05T03:04:51+00:00" }, { "name": "sebastian/environment", - "version": "8.1.2", + "version": "9.3.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "9d32c685773823b1983e256ae4ecd48a10d6e439" + "reference": "6c9e487c9eb706a8d258102a1c0b0a3e53e86c2e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/9d32c685773823b1983e256ae4ecd48a10d6e439", - "reference": "9d32c685773823b1983e256ae4ecd48a10d6e439", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/6c9e487c9eb706a8d258102a1c0b0a3e53e86c2e", + "reference": "6c9e487c9eb706a8d258102a1c0b0a3e53e86c2e", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=8.4" }, "require-dev": { - "phpunit/phpunit": "^12.5.26" + "phpunit/phpunit": "^13.1.11" }, "suggest": { "ext-posix": "*" @@ -11571,7 +11635,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "8.1-dev" + "dev-main": "9.3-dev" } }, "autoload": { @@ -11599,7 +11663,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", "security": "https://github.com/sebastianbergmann/environment/security/policy", - "source": "https://github.com/sebastianbergmann/environment/tree/8.1.2" + "source": "https://github.com/sebastianbergmann/environment/tree/9.3.2" }, "funding": [ { @@ -11619,34 +11683,34 @@ "type": "tidelift" } ], - "time": "2026-05-25T13:40:20+00:00" + "time": "2026-05-25T13:41:38+00:00" }, { "name": "sebastian/exporter", - "version": "7.0.3", + "version": "8.2.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23" + "reference": "24a3b69bba4a12ab615fca9d34680c5598d9ab7a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23", - "reference": "c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/24a3b69bba4a12ab615fca9d34680c5598d9ab7a", + "reference": "24a3b69bba4a12ab615fca9d34680c5598d9ab7a", "shasum": "" }, "require": { "ext-mbstring": "*", - "php": ">=8.3", - "sebastian/recursion-context": "^7.0.1" + "php": ">=8.4", + "sebastian/recursion-context": "^8.0.1" }, "require-dev": { - "phpunit/phpunit": "^12.5.25" + "phpunit/phpunit": "^13.3" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "7.0-dev" + "dev-main": "8.2-dev" } }, "autoload": { @@ -11689,7 +11753,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", "security": "https://github.com/sebastianbergmann/exporter/security/policy", - "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.3" + "source": "https://github.com/sebastianbergmann/exporter/tree/8.2.1" }, "funding": [ { @@ -11709,35 +11773,173 @@ "type": "tidelift" } ], - "time": "2026-05-20T04:37:17+00:00" + "time": "2026-08-07T07:22:06+00:00" + }, + { + "name": "sebastian/file-filter", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/file-filter.git", + "reference": "33a26f394330f6faa7684bb9cc73afb7727aae93" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/file-filter/zipball/33a26f394330f6faa7684bb9cc73afb7727aae93", + "reference": "33a26f394330f6faa7684bb9cc73afb7727aae93", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for filtering files", + "homepage": "https://github.com/sebastianbergmann/file-filter", + "support": { + "issues": "https://github.com/sebastianbergmann/file-filter/issues", + "security": "https://github.com/sebastianbergmann/file-filter/security/policy", + "source": "https://github.com/sebastianbergmann/file-filter/tree/1.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/file-filter", + "type": "tidelift" + } + ], + "time": "2026-04-22T07:20:04+00:00" + }, + { + "name": "sebastian/git-state", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/git-state.git", + "reference": "792a952e0eba55b6960a48aeceb9f371aad1f76b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/git-state/zipball/792a952e0eba55b6960a48aeceb9f371aad1f76b", + "reference": "792a952e0eba55b6960a48aeceb9f371aad1f76b", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for describing the state of a Git checkout", + "homepage": "https://github.com/sebastianbergmann/git-state", + "support": { + "issues": "https://github.com/sebastianbergmann/git-state/issues", + "security": "https://github.com/sebastianbergmann/git-state/security/policy", + "source": "https://github.com/sebastianbergmann/git-state/tree/1.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/git-state", + "type": "tidelift" + } + ], + "time": "2026-03-21T12:54:28+00:00" }, { "name": "sebastian/global-state", - "version": "8.0.3", + "version": "9.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "b164d3274d6537ab462591c5755f76a8f5b1aae9" + "reference": "ba68ba79da690cf7eddefd3ce5b78b20b9ba9945" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b164d3274d6537ab462591c5755f76a8f5b1aae9", - "reference": "b164d3274d6537ab462591c5755f76a8f5b1aae9", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/ba68ba79da690cf7eddefd3ce5b78b20b9ba9945", + "reference": "ba68ba79da690cf7eddefd3ce5b78b20b9ba9945", "shasum": "" }, "require": { - "php": ">=8.3", - "sebastian/object-reflector": "^5.0", - "sebastian/recursion-context": "^7.0.1" + "php": ">=8.4", + "sebastian/object-reflector": "^6.0", + "sebastian/recursion-context": "^8.0" }, "require-dev": { "ext-dom": "*", - "phpunit/phpunit": "^12.5.28" + "phpunit/phpunit": "^13.1.13" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "8.0-dev" + "dev-main": "9.0-dev" } }, "autoload": { @@ -11763,7 +11965,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", "security": "https://github.com/sebastianbergmann/global-state/security/policy", - "source": "https://github.com/sebastianbergmann/global-state/tree/8.0.3" + "source": "https://github.com/sebastianbergmann/global-state/tree/9.0.1" }, "funding": [ { @@ -11783,33 +11985,33 @@ "type": "tidelift" } ], - "time": "2026-06-01T15:10:33+00:00" + "time": "2026-06-01T15:11:33+00:00" }, { "name": "sebastian/lines-of-code", - "version": "4.0.1", + "version": "5.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "d543b8ef219dcd8da262cbb958639a96bedba10e" + "reference": "d1b6f8fce682505dbd048977f1abedf1b8ad3ff8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d543b8ef219dcd8da262cbb958639a96bedba10e", - "reference": "d543b8ef219dcd8da262cbb958639a96bedba10e", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d1b6f8fce682505dbd048977f1abedf1b8ad3ff8", + "reference": "d1b6f8fce682505dbd048977f1abedf1b8ad3ff8", "shasum": "" }, "require": { - "nikic/php-parser": "^5.7.0", - "php": ">=8.3" + "nikic/php-parser": "^5.8.0", + "php": ">=8.4" }, "require-dev": { - "phpunit/phpunit": "^12.5.25" + "phpunit/phpunit": "^13.2.4" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "4.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -11833,7 +12035,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/4.0.1" + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/5.0.2" }, "funding": [ { @@ -11853,34 +12055,33 @@ "type": "tidelift" } ], - "time": "2026-05-19T16:22:07+00:00" + "time": "2026-07-09T08:42:34+00:00" }, { "name": "sebastian/object-enumerator", - "version": "7.0.0", + "version": "8.1.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894" + "reference": "511064ecde82bd747e2ba2fab3dda8d977b59576" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1effe8e9b8e068e9ae228e542d5d11b5d16db894", - "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/511064ecde82bd747e2ba2fab3dda8d977b59576", + "reference": "511064ecde82bd747e2ba2fab3dda8d977b59576", "shasum": "" }, "require": { - "php": ">=8.3", - "sebastian/object-reflector": "^5.0", - "sebastian/recursion-context": "^7.0" + "php": ">=8.4", + "sebastian/recursion-context": "^8.0.1" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^13.3.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "7.0-dev" + "dev-main": "8.1-dev" } }, "autoload": { @@ -11903,40 +12104,52 @@ "support": { "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", - "source": "https://github.com/sebastianbergmann/object-enumerator/tree/7.0.0" + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/8.1.0" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/object-enumerator", + "type": "tidelift" } ], - "time": "2025-02-07T04:57:48+00:00" + "time": "2026-08-13T07:05:05+00:00" }, { "name": "sebastian/object-reflector", - "version": "5.0.0", + "version": "6.1.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "4bfa827c969c98be1e527abd576533293c634f6a" + "reference": "f71bbcdc4f95456b4622810bec64eb06372e25b2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/4bfa827c969c98be1e527abd576533293c634f6a", - "reference": "4bfa827c969c98be1e527abd576533293c634f6a", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/f71bbcdc4f95456b4622810bec64eb06372e25b2", + "reference": "f71bbcdc4f95456b4622810bec64eb06372e25b2", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=8.4" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^13.3.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -11959,40 +12172,52 @@ "support": { "issues": "https://github.com/sebastianbergmann/object-reflector/issues", "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", - "source": "https://github.com/sebastianbergmann/object-reflector/tree/5.0.0" + "source": "https://github.com/sebastianbergmann/object-reflector/tree/6.1.0" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/object-reflector", + "type": "tidelift" } ], - "time": "2025-02-07T04:58:17+00:00" + "time": "2026-08-13T06:34:36+00:00" }, { "name": "sebastian/recursion-context", - "version": "7.0.1", + "version": "8.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c" + "reference": "32dba72f2b4642d6a93db22d6c0a9280ff2e3ca0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", - "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/32dba72f2b4642d6a93db22d6c0a9280ff2e3ca0", + "reference": "32dba72f2b4642d6a93db22d6c0a9280ff2e3ca0", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=8.4" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^13.2.6" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "7.0-dev" + "dev-main": "8.0-dev" } }, "autoload": { @@ -12023,7 +12248,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/7.0.1" + "source": "https://github.com/sebastianbergmann/recursion-context/tree/8.0.1" }, "funding": [ { @@ -12043,32 +12268,32 @@ "type": "tidelift" } ], - "time": "2025-08-13T04:44:59+00:00" + "time": "2026-08-03T05:58:12+00:00" }, { "name": "sebastian/type", - "version": "6.0.4", + "version": "7.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "82ff822c2edc46724be9f7411d3163021f602773" + "reference": "bd1df467864cb95140414059a535b2d906173fcf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/82ff822c2edc46724be9f7411d3163021f602773", - "reference": "82ff822c2edc46724be9f7411d3163021f602773", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/bd1df467864cb95140414059a535b2d906173fcf", + "reference": "bd1df467864cb95140414059a535b2d906173fcf", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=8.4" }, "require-dev": { - "phpunit/phpunit": "^12.5.25" + "phpunit/phpunit": "^13.3.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -12092,7 +12317,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/type/issues", "security": "https://github.com/sebastianbergmann/type/security/policy", - "source": "https://github.com/sebastianbergmann/type/tree/6.0.4" + "source": "https://github.com/sebastianbergmann/type/tree/7.0.2" }, "funding": [ { @@ -12112,29 +12337,29 @@ "type": "tidelift" } ], - "time": "2026-05-20T06:45:45+00:00" + "time": "2026-08-10T08:00:57+00:00" }, { "name": "sebastian/version", - "version": "6.0.0", + "version": "7.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/version.git", - "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c" + "reference": "ad37a5552c8e2b88572249fdc19b6da7792e021b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/3e6ccf7657d4f0a59200564b08cead899313b53c", - "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/ad37a5552c8e2b88572249fdc19b6da7792e021b", + "reference": "ad37a5552c8e2b88572249fdc19b6da7792e021b", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=8.4" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -12158,15 +12383,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/version/issues", "security": "https://github.com/sebastianbergmann/version/security/policy", - "source": "https://github.com/sebastianbergmann/version/tree/6.0.0" + "source": "https://github.com/sebastianbergmann/version/tree/7.0.0" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/version", + "type": "tidelift" } ], - "time": "2025-02-07T05:00:38+00:00" + "time": "2026-02-06T04:52:52+00:00" }, { "name": "staabm/side-effects-detector", @@ -12277,11 +12514,11 @@ "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": "^8.3" + "php": "^8.4.1" }, "platform-dev": {}, "platform-overrides": { - "php": "8.3.0" + "php": "8.4.1" }, "plugin-api-version": "2.6.0" } diff --git a/docs/self-hosting.md b/docs/self-hosting.md index 19207b7..14cea9d 100644 --- a/docs/self-hosting.md +++ b/docs/self-hosting.md @@ -1,6 +1,6 @@ # Self-hosting -VersionTracker is distributed as a versioned Docker image. Install a concrete GitHub release tag such as `v0.1.0`; do not substitute `latest` in a production deployment. +VersionTracker is distributed as a versioned Docker image and a compact deployment bundle. Install a concrete GitHub release tag such as `v0.1.2`; do not substitute `latest` in a production deployment. ## Prerequisites @@ -8,16 +8,38 @@ VersionTracker is distributed as a versioned Docker image. Install a concrete Gi - A public DNS record and open ports 80 and 443 when using Caddy. - A reverse proxy and an available local HTTP port when Caddy is not used. -Clone the repository, change into it, and run the installer: +Download the exact deployment bundle and verify its checksum before unpacking it: ```bash -git clone https://github.com/soerennb/version-tracker.git -cd versiontracker +VERSION=v0.1.2 +curl -fsSLO "https://github.com/soerennb/version-tracker/releases/download/${VERSION}/versiontracker-deploy-${VERSION}.tar.gz" +curl -fsSLO "https://github.com/soerennb/version-tracker/releases/download/${VERSION}/versiontracker-deploy-${VERSION}.tar.gz.sha256" +sha256sum --check "versiontracker-deploy-${VERSION}.tar.gz.sha256" +tar -xzf "versiontracker-deploy-${VERSION}.tar.gz" +cd "versiontracker-deploy-${VERSION}" ./install.sh install ``` The installer verifies Docker, generates `.env.docker` with mode `0600`, creates database secrets, records the selected deployment mode, starts MariaDB, and creates the initial administrator. Demo data requires a second confirmation because it includes a public password. +Repository cloning remains supported for contributors. Operators only need the deployment bundle. + +### Unattended installation + +Supply all configuration options and pass only the administrator password over standard input. Do not place the password in a command-line argument, `.env.docker`, CI log, or shell history. + +```bash +printf '%s\n' 'choose-a-long-unique-password' | ./install.sh install \ + --version v0.1.2 \ + --mode proxy \ + --port 8080 \ + --admin-name 'Administrator' \ + --admin-email admin@example.com \ + --admin-password-stdin +``` + +For Caddy, replace `--mode proxy --port 8080` with `--mode caddy --domain example.com --email ops@example.com`. The selected domain must already resolve to the server and ports 80 and 443 must be available. + ## Deployment modes Choose `caddy` for a public host with automatic HTTPS. Caddy receives ports 80 and 443, obtains the certificate, and forwards requests to the internal application container. @@ -26,13 +48,15 @@ Choose `proxy` when an existing reverse proxy terminates TLS or the application Important `.env.docker` settings: -| Setting | Purpose | -| --- | --- | -| `VERSION` | Required exact `v0.x.y` image tag. | -| `DEPLOYMENT_MODE` | `proxy` or `caddy`; updates reuse this choice. | -| `APP_URL` | Public URL used for generated links. | +| Setting | Purpose | +| ----------------------------------- | --------------------------------------------------------- | +| `VERSION` | Required exact `v0.x.y` image tag. | +| `IMAGE_REPOSITORY` | Published VersionTracker image repository. | +| `MARIADB_IMAGE` / `CADDY_IMAGE` | Tested, pinned supporting images. | +| `DEPLOYMENT_MODE` | `proxy` or `caddy`; updates reuse this choice. | +| `APP_URL` | Public URL used for generated links. | | `TRUSTED_HOSTS` / `TRUSTED_PROXIES` | Restrict the public hostname and trusted proxy addresses. | -| `DB_PASSWORD` / `DB_ROOT_PASSWORD` | Generated MariaDB credentials; keep them private. | +| `DB_PASSWORD` / `DB_ROOT_PASSWORD` | Generated MariaDB credentials; keep them private. | ## Operations @@ -50,6 +74,8 @@ Back up MariaDB, Laravel storage, and the current environment file before every Backups are stored under `backups/versiontracker-/` with owner-only permissions. Copy them off the server; they contain database data and credentials. +Schedule this command with the server's existing scheduler and copy its completed backup directory to independent storage. For example, run it daily through a systemd timer or cron, redirect the output to an operator-only log, and alert when the command fails. A backup retained only on the deployment host does not protect against host loss. + Upgrade by entering the next exact release tag: ```bash diff --git a/install.sh b/install.sh index 1b24d49..8a38905 100755 --- a/install.sh +++ b/install.sh @@ -5,6 +5,15 @@ set -euo pipefail readonly environment_file=.env.docker readonly backup_directory=backups +install_version='' +install_mode='' +install_port='' +install_domain='' +install_email='' +install_admin_name='' +install_admin_email='' +install_admin_password_stdin=false + set_environment_value() { local key="$1" local value="$2" @@ -50,6 +59,83 @@ is_valid_version() { [[ "$1" =~ ^v0\.[0-9]+\.[0-9]+$ ]] } +usage() { + cat <<'EOF' +Usage: + ./install.sh install [--version v0.x.y] [--mode proxy|caddy] [--port PORT] + [--domain DOMAIN --email EMAIL] + [--admin-name NAME --admin-email EMAIL --admin-password-stdin] + ./install.sh update + ./install.sh status + ./install.sh backup + +When all install options are supplied, pipe the administrator password to standard input. +EOF +} + +parse_install_options() { + while [[ $# -gt 0 ]]; do + case "$1" in + --version) + install_version="$2" + shift 2 + ;; + --mode) + install_mode="$2" + shift 2 + ;; + --port) + install_port="$2" + shift 2 + ;; + --domain) + install_domain="$2" + shift 2 + ;; + --email) + install_email="$2" + shift 2 + ;; + --admin-name) + install_admin_name="$2" + shift 2 + ;; + --admin-email) + install_admin_email="$2" + shift 2 + ;; + --admin-password-stdin) + install_admin_password_stdin=true + shift + ;; + --help|-h) + usage + exit 0 + ;; + *) + echo "Unknown install option: $1" >&2 + usage >&2 + exit 1 + ;; + esac + done + + if [[ -n "$install_mode" && "$install_mode" != proxy && "$install_mode" != caddy ]]; then + echo 'Install mode must be either proxy or caddy.' >&2 + exit 1 + fi + + if [[ -n "$install_domain$install_email" && "$install_mode" != caddy ]]; then + echo 'Domain and ACME email require Caddy mode.' >&2 + exit 1 + fi + + if [[ "$install_admin_password_stdin" == true ]] && [[ -z "$install_admin_name" || -z "$install_admin_email" ]]; then + echo 'Administrator name and email are required with --admin-password-stdin.' >&2 + exit 1 + fi +} + check_port() { local port="$1" @@ -79,22 +165,35 @@ configure_installation() { chmod 600 "$environment_file" - local version - local mode - read -r -p 'Release image version (for example v0.1.0): ' version + local version="$install_version" + local mode="$install_mode" + + if [[ -z "$version" ]]; then + read -r -p 'Release image version (for example v0.1.0): ' version + fi + is_valid_version "$version" || { echo 'Enter a concrete v0.x.y release tag.' >&2 exit 1 } set_environment_value VERSION "$version" - read -r -p 'Use Caddy automatic HTTPS? [y/N]: ' mode + if [[ -z "$mode" ]]; then + read -r -p 'Use Caddy automatic HTTPS? [y/N]: ' mode + fi + + if [[ "$mode" =~ ^[Yy]$ || "$mode" == caddy ]]; then + local domain="$install_domain" + local email="$install_email" + + if [[ -z "$domain" ]]; then + read -r -p 'Public domain: ' domain + fi + + if [[ -z "$email" ]]; then + read -r -p 'ACME email: ' email + fi - if [[ "$mode" =~ ^[Yy]$ ]]; then - local domain - local email - read -r -p 'Public domain: ' domain - read -r -p 'ACME email: ' email [[ -n "$domain" && -n "$email" ]] || { echo 'A domain and email are required for Caddy.' >&2; exit 1; } check_port 80 @@ -110,8 +209,12 @@ configure_installation() { return fi - local port - read -r -p 'Application port [8080]: ' port + local port="$install_port" + + if [[ -z "$port" ]]; then + read -r -p 'Application port [8080]: ' port + fi + port="${port:-8080}" [[ "$port" =~ ^[0-9]+$ ]] || { echo 'Application port must be numeric.' >&2; exit 1; } check_port "$port" @@ -121,6 +224,22 @@ configure_installation() { compose_file_for_mode proxy } +initialize_application() { + local compose_file="$1" + + if [[ "$install_admin_password_stdin" == true ]]; then + compose "$compose_file" run --rm -T app php artisan app:install \ + --no-demo \ + --admin-name="$install_admin_name" \ + --admin-email="$install_admin_email" \ + --admin-password-stdin + + return + fi + + compose "$compose_file" run --rm app php artisan app:install +} + configured_compose_file() { [[ -f "$environment_file" ]] || { echo 'Run ./install.sh install first.' >&2; exit 1; } @@ -156,7 +275,7 @@ install() { compose "$compose_file" config >/dev/null compose "$compose_file" pull compose "$compose_file" up -d db - compose "$compose_file" run --rm app php artisan app:install + initialize_application "$compose_file" compose "$compose_file" up -d wait_for_health "$compose_file" compose "$compose_file" ps @@ -214,6 +333,8 @@ backup() { case "${1:-}" in install) + shift + parse_install_options "$@" install ;; update) @@ -226,7 +347,7 @@ case "${1:-}" in backup ;; *) - echo 'Usage: ./install.sh {install|update|status|backup}' >&2 + usage >&2 exit 1 ;; esac diff --git a/tests/Feature/InstallApplicationCommandTest.php b/tests/Feature/InstallApplicationCommandTest.php index 1ce2c14..9eedc42 100644 --- a/tests/Feature/InstallApplicationCommandTest.php +++ b/tests/Feature/InstallApplicationCommandTest.php @@ -27,6 +27,19 @@ public function test_it_creates_an_administrator_for_an_empty_application(): voi $this->assertSame(UserRole::ADMIN, $administrator->role); } + public function test_it_accepts_administrator_identity_options_for_unattended_installation(): void + { + $this->artisan('app:install --no-demo --admin-name="Automated Administrator" --admin-email=automated@example.com') + ->expectsQuestion('Administrator password (at least 12 characters)', 'secure-password') + ->assertSuccessful(); + + $this->assertDatabaseHas('users', [ + 'name' => 'Automated Administrator', + 'email' => 'automated@example.com', + 'role' => UserRole::ADMIN->value, + ]); + } + public function test_it_seeds_demo_data_when_requested(): void { $this->artisan('app:install --demo') From 69f9966c801216063f05507d9d1ed86bf9d9af5d Mon Sep 17 00:00:00 2001 From: soeren Date: Sun, 16 Aug 2026 22:15:23 +0200 Subject: [PATCH 2/4] Fix Caddy deployment configuration --- Caddyfile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Caddyfile b/Caddyfile index cfe56dd..7177fad 100644 --- a/Caddyfile +++ b/Caddyfile @@ -1,4 +1,7 @@ -{$CADDY_DOMAIN} { +{ email {$CADDY_EMAIL} +} + +{$CADDY_DOMAIN} { reverse_proxy app:80 } From 5faea988bdc557e719c2daab51422d77d0077504 Mon Sep 17 00:00:00 2001 From: soeren Date: Sun, 16 Aug 2026 22:17:57 +0200 Subject: [PATCH 3/4] Patch container base image dependencies --- .github/workflows/ci.yml | 5 ++++- Dockerfile | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc8ad58..34d8f06 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -234,7 +234,10 @@ jobs: - name: Clean up containers if: always() - run: docker compose --env-file .env.docker -f compose.yml -f compose.proxy.yml down --volumes --remove-orphans + run: | + if [[ -f .env.docker ]]; then + docker compose --env-file .env.docker -f compose.yml -f compose.proxy.yml down --volumes --remove-orphans + fi compose: name: Compose configuration diff --git a/Dockerfile b/Dockerfile index a9758ff..5ed3d8f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,6 +24,7 @@ RUN npm run build FROM php:8.4-apache-bookworm RUN apt-get update \ + && apt-get upgrade --yes \ && apt-get install --yes --no-install-recommends curl libfreetype6-dev libicu-dev libjpeg62-turbo-dev libonig-dev libpng-dev libzip-dev unzip \ && docker-php-ext-configure gd --with-freetype --with-jpeg \ && docker-php-ext-install -j"$(nproc)" gd intl mbstring pdo_mysql zip \ From 4367c19b7fadc716c551b7008d8d7316c994ef08 Mon Sep 17 00:00:00 2001 From: soeren Date: Sun, 16 Aug 2026 22:23:22 +0200 Subject: [PATCH 4/4] Fix backup restore CI fixture --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 34d8f06..2e3bf02 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -177,6 +177,7 @@ jobs: run: | cp .env.docker.example .env.docker sed -i 's/^VERSION=$/VERSION=test/' .env.docker + sed -i 's/^DEPLOYMENT_MODE=$/DEPLOYMENT_MODE=proxy/' .env.docker sed -i 's/^APP_KEY=$/APP_KEY=base64:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=/' .env.docker sed -i 's/^DB_PASSWORD=$/DB_PASSWORD=versiontracker/' .env.docker sed -i 's/^DB_ROOT_PASSWORD=$/DB_ROOT_PASSWORD=root/' .env.docker @@ -218,6 +219,7 @@ jobs: done docker compose --env-file .env.docker -f compose.yml -f compose.proxy.yml exec --no-TTY db sh -c 'exec mariadb -uroot -p"$MARIADB_ROOT_PASSWORD" "$MARIADB_DATABASE"' < "$backup_directory/database.sql" + docker compose --env-file .env.docker -f compose.yml -f compose.proxy.yml exec --no-TTY db sh -c 'mariadb -uroot -p"$MARIADB_ROOT_PASSWORD" "$MARIADB_DATABASE" --skip-column-names --execute="SELECT COUNT(*) FROM migrations"' | grep --extended-regexp '^[1-9][0-9]*$' docker compose --env-file .env.docker -f compose.yml -f compose.proxy.yml run --rm app sh -c 'tar -xzf - -C /var/www/html' < "$backup_directory/storage.tar.gz" docker compose --env-file .env.docker -f compose.yml -f compose.proxy.yml up --detach app